5 Commits
0.1.8 ... 0.2.0

Author SHA1 Message Date
Morten Olsen
1115ce2fb3 feat: add http gateway (#3) 2024-01-12 21:10:48 +01:00
Morten Olsen
9c5249956e chore: create devcontainer.json (#1) 2024-01-12 17:40:44 +01:00
Morten Olsen
b5d8cf3a51 feat: support multiple contexts 2024-01-12 15:31:44 +01:00
Morten Olsen
5154fbb4a5 ci: add NPM publish 2024-01-12 15:07:26 +01:00
Morten Olsen
59d6faaafc feat: switched from worker API to fs based 2024-01-12 14:35:16 +01:00
70 changed files with 1042 additions and 139 deletions

View File

@@ -0,0 +1,5 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
}
}

View File

@@ -91,33 +91,33 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
# release-npm: release-npm:
# if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# needs: [build, update-release-draft] needs: [build, update-release-draft]
# permissions: permissions:
# contents: read contents: read
# packages: write packages: write
# steps: steps:
# - uses: actions/checkout@v3 - uses: actions/checkout@v3
# with: with:
# fetch-depth: 0 fetch-depth: 0
# - run: corepack enable - run: corepack enable
# - uses: actions/setup-node@v3 - uses: actions/setup-node@v3
# with: with:
# cache: '${{ env.NODE_CACHE }}' cache: '${{ env.NODE_CACHE }}'
# node-version: '${{ env.NODE_VERSION }}' node-version: '${{ env.NODE_VERSION }}'
# scope: '${{ env.NODE_SCOPE }}' scope: '${{ env.NODE_SCOPE }}'
# - uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
# with: with:
# name: lib name: lib
# path: ./ path: ./
# - run: | - run: |
# pnpm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} pnpm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
# pnpm install pnpm install
# git config user.name "Github Actions Bot" git config user.name "Github Actions Bot"
# git config user.email "<>" git config user.email "<>"
# node scripts/set-version.ts $(git describe --tag --abbrev=0) node scripts/set-version.mjs $(git describe --tag --abbrev=0)
# pnpm publish -r --publish-branch main --access public --no-git-checks pnpm publish -r --publish-branch main --access public --no-git-checks
# env: env:
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -15,14 +15,19 @@ npm install -g @morten-olsen/mini-loader-cli
Now, let's write a basic script that outputs a single artifact named “hello”. Create a new file with the following JavaScript code: Now, let's write a basic script that outputs a single artifact named “hello”. Create a new file with the following JavaScript code:
```javascript ```javascript
import { artifacts } from "@morten-olsen/mini-loader"; import { artifacts } from '@morten-olsen/mini-loader';
artifacts.create('hello', 'world'); const run = async () => {
artifacts.create('hello', 'world');
};
run();
``` ```
Save this file as `script.mjs`. Save this file as `script.js`.
#### A Note on Dependencies #### A Note on Dependencies
In this script, we're using the `@morten-olsen/mini-loader` package, which might not be installed in your local environment. No worries though, as mini loader can automatically download necessary packages when preparing the script. Alternatively, for a more structured approach (especially if you're using TypeScript), you can initialize a Node.js project and install the dependencies for complete access to typings. In this script, we're using the `@morten-olsen/mini-loader` package, which might not be installed in your local environment. No worries though, as mini loader can automatically download necessary packages when preparing the script. Alternatively, for a more structured approach (especially if you're using TypeScript), you can initialize a Node.js project and install the dependencies for complete access to typings.
### Step 3: Run the Script Locally ### Step 3: Run the Script Locally
@@ -30,7 +35,7 @@ In this script, we're using the `@morten-olsen/mini-loader` package, which might
To validate that your script is functioning correctly, execute it locally using the following command: To validate that your script is functioning correctly, execute it locally using the following command:
```bash ```bash
mini-loader local run script.mjs -ai mini-loader local run script.js -ai
``` ```
The `-ai` flag instructs the CLI to automatically download any referenced packages when bundling the script. The `-ai` flag instructs the CLI to automatically download any referenced packages when bundling the script.

View File

@@ -58,7 +58,7 @@ mini-loader artifacts ls
To download a specific artifact: To download a specific artifact:
```bash ```bash
mini-loader artifacts pull <id> > myfile.txt mini-loader artifacts pull <id> myfile.txt
``` ```
Replace `<id>` with the identifier of the artifact you wish to download. Replace `<id>` with the identifier of the artifact you wish to download.

View File

@@ -1,6 +1,7 @@
{ {
"name": "@morten-olsen/mini-loader-repo", "name": "@morten-olsen/mini-loader-repo",
"private": "true", "private": "true",
"license": "GPL-3.0",
"packageManager": "pnpm@8.10.4", "packageManager": "pnpm@8.10.4",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
@@ -11,7 +12,6 @@
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC",
"devDependencies": { "devDependencies": {
"@react-native-community/eslint-config": "^3.2.0", "@react-native-community/eslint-config": "^3.2.0",
"eslint": "^8.53.0", "eslint": "^8.53.0",
@@ -23,5 +23,10 @@
"@pnpm/find-workspace-packages": "^6.0.9", "@pnpm/find-workspace-packages": "^6.0.9",
"@types/node": "^20.10.8", "@types/node": "^20.10.8",
"ts-node": "^10.9.2" "ts-node": "^10.9.2"
},
"homepage": "https://github.com/morten-olsen/mini-loader",
"repository": {
"type": "git",
"url": "https://github.com/morten-olsen/mini-loader"
} }
} }

1
packages/cli/README.md Normal file
View File

@@ -0,0 +1 @@
[Go to documentation](https://github.com/morten-olsen/mini-loader)

View File

@@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"main": "./dist/esm/index.js", "main": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts", "types": "./dist/esm/index.d.ts",
"license": "GPL-3.0",
"bin": { "bin": {
"mini-loader": "./bin/index.mjs" "mini-loader": "./bin/index.mjs"
}, },
@@ -27,6 +28,7 @@
"@rollup/plugin-sucrase": "^5.0.2", "@rollup/plugin-sucrase": "^5.0.2",
"@trpc/client": "^10.45.0", "@trpc/client": "^10.45.0",
"commander": "^11.1.0", "commander": "^11.1.0",
"env-paths": "^3.0.0",
"inquirer": "^9.2.12", "inquirer": "^9.2.12",
"ora": "^8.0.1", "ora": "^8.0.1",
"rollup": "^4.9.4", "rollup": "^4.9.4",
@@ -40,5 +42,10 @@
"@morten-olsen/mini-loader-server": "workspace:^", "@morten-olsen/mini-loader-server": "workspace:^",
"@types/inquirer": "^9.0.7", "@types/inquirer": "^9.0.7",
"typescript": "^5.3.3" "typescript": "^5.3.3"
},
"homepage": "https://github.com/morten-olsen/mini-loader",
"repository": {
"type": "git",
"url": "https://github.com/morten-olsen/mini-loader"
} }
} }

View File

@@ -17,12 +17,12 @@ const bundle = async ({ entry, autoInstall }: BundleOptions) => {
const entryFile = resolve(entry); const entryFile = resolve(entry);
const codeBundler = await rollup({ const codeBundler = await rollup({
plugins: [ plugins: [
fix(json)(),
fix(sucrase)({ fix(sucrase)({
transforms: ['typescript', 'jsx'], transforms: ['typescript', 'jsx'],
}), }),
...[autoInstall ? fix(auto) : []], ...[autoInstall ? fix(auto) : []],
nodeResolve({ extensions: ['.js', '.jsx', '.ts', '.tsx'] }), nodeResolve({ preferBuiltins: true, extensions: ['.js', '.jsx', '.ts', '.tsx'] }),
fix(json)(),
fix(commonjs)({ include: /node_modules/ }), fix(commonjs)({ include: /node_modules/ }),
], ],
input: entryFile, input: entryFile,

View File

@@ -2,13 +2,20 @@ import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import superjson from 'superjson'; import superjson from 'superjson';
import type { Runtime } from '@morten-olsen/mini-loader-server'; import type { Runtime } from '@morten-olsen/mini-loader-server';
import type { RootRouter } from '@morten-olsen/mini-loader-server'; import type { RootRouter } from '@morten-olsen/mini-loader-server';
import { Context } from '../context/context.js';
const createClient = () => { const createClient = (context: Context) => {
if (!context.host || !context.token) {
throw new Error('Not signed in');
}
const client = createTRPCProxyClient<RootRouter>({ const client = createTRPCProxyClient<RootRouter>({
transformer: superjson, transformer: superjson,
links: [ links: [
httpBatchLink({ httpBatchLink({
url: 'http://localhost:4500/trpc', url: `${context.host}/trpc`,
headers: {
authorization: `Bearer ${context.token}`,
},
}), }),
], ],
}); });

View File

@@ -1,6 +1,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const list = new Command('list'); const list = new Command('list');
@@ -20,8 +22,10 @@ list
.option('-a, --limit <limit>', 'Limit', '1000') .option('-a, --limit <limit>', 'Limit', '1000')
.action(async () => { .action(async () => {
const { runId, loadId, offset, limit } = list.opts(); const { runId, loadId, offset, limit } = list.opts();
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
const artifacts = await step('Getting artifacts', async () => { const artifacts = await step('Getting artifacts', async () => {
return await client.artifacts.find.query({ return await client.artifacts.find.query({

View File

@@ -0,0 +1,34 @@
import { Command } from 'commander';
import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { dirname, resolve } from 'path';
import { mkdir, writeFile } from 'fs/promises';
import { Config } from '../../config/config.js';
const pull = new Command('pull');
pull
.description('Download artifact')
.argument('<artifact-id>', 'Artifact ID')
.argument('<file>', 'File to save')
.action(async (id, file) => {
const config = new Config();
const context = new Context(config.context);
const target = resolve(file);
const client = await step('Connecting to server', async () => {
return createClient(context);
});
const artifact = await step('Getting artifact', async () => {
const result = await client.artifacts.get.query(id);
if (!result) {
throw new Error('Artifact not found');
}
return result;
});
await mkdir(dirname(target), { recursive: true });
const data = Buffer.from(artifact.data, 'base64').toString('utf-8');
await writeFile(target, data, 'utf-8');
});
export { pull };

View File

@@ -0,0 +1,61 @@
import { Command } from 'commander';
import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import inquirer from 'inquirer';
import { Config } from '../../config/config.js';
const remove = new Command('remove');
const toInt = (value?: string) => {
if (!value) {
return undefined;
}
return parseInt(value, 10);
};
remove
.alias('ls')
.description('List logs')
.option('-r, --run-id <runId>', 'Run ID')
.option('-l, --load-id <loadId>', 'Load ID')
.option('-o, --offset <offset>', 'Offset')
.option('-a, --limit <limit>', 'Limit', '1000')
.action(async () => {
const { runId, loadId, offset, limit } = remove.opts();
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => {
return createClient(context);
});
const response = await step('Preparing to delete', async () => {
return await client.artifacts.prepareRemove.query({
runId,
loadId,
offset: toInt(offset),
limit: toInt(limit),
});
});
if (!response.ids.length) {
console.log('No logs to delete');
return;
}
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to delete ${response.ids.length} logs?`,
},
]);
if (!confirm) {
return;
}
await step('Deleting artifacts', async () => {
await client.artifacts.remove.mutate(response);
});
});
export { remove };

View File

@@ -1,7 +1,11 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { list } from './artifacts.list.js'; import { list } from './artifacts.list.js';
import { remove } from './artifacts.remove.js';
import { pull } from './artifacts.pull.js';
const artifacts = new Command('artifacts'); const artifacts = new Command('artifacts');
artifacts.addCommand(list); artifacts.addCommand(list);
artifacts.addCommand(remove);
artifacts.addCommand(pull);
export { artifacts }; export { artifacts };

View File

@@ -1,16 +1,21 @@
import { Command } from 'commander'; import { Command } from 'commander';
import inquerer from 'inquirer'; import inquerer from 'inquirer';
import { Context } from '../../context/context.js';
import { step } from '../../utils/step.js';
import { Config } from '../../config/config.js';
const login = new Command('login'); const login = new Command('login');
login.description('Login to your account'); login.description('Login to your account');
login.action(async () => { login.action(async () => {
const config = new Config();
const context = new Context(config.context);
const { host, token } = await inquerer.prompt([ const { host, token } = await inquerer.prompt([
{ {
type: 'input', type: 'input',
name: 'host', name: 'host',
message: 'Enter the host of your server', message: 'Enter the host of your server',
default: 'http://localhost:4500', default: context.host ?? 'http://localhost:4500',
}, },
{ {
type: 'password', type: 'password',
@@ -19,7 +24,25 @@ login.action(async () => {
}, },
]); ]);
console.log(host, token); const healthResponse = await step('Getting auth status', async () => {
return await fetch(`${host}/health`, {
headers: {
authorization: `Bearer ${token}`,
},
});
});
if (!healthResponse.ok) {
throw new Error('Invalid token');
}
const health = await healthResponse.json();
if (!health.authorized) {
throw new Error('Invalid token');
}
await step('Saving login', async () => {
await context.saveLogin(host, token);
});
}); });
export { login }; export { login };

View File

@@ -0,0 +1,10 @@
import { Command } from 'commander';
import { Config } from '../../config/config.js';
const current = new Command('current');
current.action(async () => {
const config = new Config();
console.log(config.context);
});
export { current };

View File

@@ -0,0 +1,11 @@
import { Command } from 'commander';
import { Context } from '../../context/context.js';
const list = new Command('list');
list.alias('ls').description('List contexts');
list.action(async () => {
const contexts = await Context.list();
console.table(contexts);
});
export { list };

View File

@@ -0,0 +1,12 @@
import { Command } from 'commander';
import { list } from './contexts.list.js';
import { use } from './contexts.use.js';
import { current } from './contexts.current.js';
const contexts = new Command('contexts');
contexts.description('Manage contexts');
contexts.addCommand(list);
contexts.addCommand(use);
contexts.addCommand(current);
export { contexts };

View File

@@ -0,0 +1,11 @@
import { Command } from 'commander';
import { Config } from '../../config/config.js';
const use = new Command('use');
use.argument('<name>').action(async (name) => {
const config = new Config();
await config.setContext(name);
});
export { use };

View File

@@ -1,6 +1,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const list = new Command('list'); const list = new Command('list');
@@ -8,11 +10,13 @@ list
.alias('ls') .alias('ls')
.description('List loads') .description('List loads')
.action(async () => { .action(async () => {
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
const loads = step('Getting data', async () => { const loads = await step('Getting data', async () => {
await client.loads.find.query({}); return await client.loads.find.query({});
}); });
console.table(loads); console.table(loads);
}); });

View File

@@ -3,6 +3,8 @@ import { resolve } from 'path';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { bundle } from '../../bundler/bundler.js'; import { bundle } from '../../bundler/bundler.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const push = new Command('push'); const push = new Command('push');
@@ -14,14 +16,16 @@ push
.option('-ai, --auto-install', 'Auto install dependencies', false) .option('-ai, --auto-install', 'Auto install dependencies', false)
.action(async (script) => { .action(async (script) => {
const opts = push.opts(); const opts = push.opts();
const config = new Config();
const context = new Context(config.context);
const location = resolve(script); const location = resolve(script);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
const code = await step('Bundling', async () => { const code = await step('Bundling', async () => {
return await bundle({ entry: location, autoInstall: opts.autoInstall }); return await bundle({ entry: location, autoInstall: opts.autoInstall });
}); });
const id = await step('Creating load', async () => { const id = await step(`Creating load ${(code.length / 1024).toFixed(0)}`, async () => {
return await client.loads.set.mutate({ return await client.loads.set.mutate({
id: opts.id, id: opts.id,
name: opts.name, name: opts.name,
@@ -30,9 +34,10 @@ push
}); });
console.log('created load with id', id); console.log('created load with id', id);
if (opts.run) { if (opts.run) {
await step('Creating run', async () => { const runId = await step('Creating run', async () => {
await client.runs.create.mutate({ loadId: id }); return await client.runs.create.mutate({ loadId: id });
}); });
console.log('created run with id', runId);
} }
}); });

View File

@@ -1,6 +1,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const list = new Command('list'); const list = new Command('list');
@@ -22,8 +24,10 @@ list
.option('-s, --sort <order>', 'Sort', 'desc') .option('-s, --sort <order>', 'Sort', 'desc')
.action(async () => { .action(async () => {
const { runId, loadId, severities, offset, limit, order } = list.opts(); const { runId, loadId, severities, offset, limit, order } = list.opts();
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
const logs = await step('Getting logs', async () => { const logs = await step('Getting logs', async () => {
return await client.logs.find.query({ return await client.logs.find.query({
@@ -35,7 +39,7 @@ list
order, order,
}); });
}); });
console.table(logs.reverse()); console.table(logs);
}); });
export { list }; export { list };

View File

@@ -0,0 +1,65 @@
import { Command } from 'commander';
import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import inquirer from 'inquirer';
import { Config } from '../../config/config.js';
const remove = new Command('remove');
const toInt = (value?: string) => {
if (!value) {
return undefined;
}
return parseInt(value, 10);
};
remove
.alias('ls')
.description('List logs')
.option('-r, --run-id <runId>', 'Run ID')
.option('-l, --load-id <loadId>', 'Load ID')
.option('--severities <severities...>', 'Severities')
.option('-o, --offset <offset>', 'Offset')
.option('-a, --limit <limit>', 'Limit', '1000')
.option('-s, --sort <order>', 'Sort', 'desc')
.action(async () => {
const { runId, loadId, severities, offset, limit, order } = remove.opts();
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => {
return createClient(context);
});
const response = await step('Preparing to delete', async () => {
return await client.logs.prepareRemove.query({
runId,
loadId,
severities,
offset: toInt(offset),
limit: toInt(limit),
order,
});
});
if (!response.ids.length) {
console.log('No logs to delete');
return;
}
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to delete ${response.ids.length} logs?`,
},
]);
if (!confirm) {
return;
}
await step('Deleting logs', async () => {
await client.logs.remove.mutate(response);
});
});
export { remove };

View File

@@ -1,7 +1,9 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { list } from './logs.list.js'; import { list } from './logs.list.js';
import { remove } from './logs.remove.js';
const logs = new Command('logs'); const logs = new Command('logs');
logs.addCommand(list); logs.addCommand(list);
logs.addCommand(remove);
export { logs }; export { logs };

View File

@@ -1,6 +1,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const create = new Command('create'); const create = new Command('create');
@@ -8,8 +10,10 @@ create
.description('Create a new run') .description('Create a new run')
.argument('load-id', 'Load ID') .argument('load-id', 'Load ID')
.action(async (loadId) => { .action(async (loadId) => {
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
await step('Creating run', async () => { await step('Creating run', async () => {
await client.runs.create.mutate({ loadId }); await client.runs.create.mutate({ loadId });

View File

@@ -1,16 +1,20 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const list = new Command('create'); const list = new Command('list');
list list
.alias('ls') .alias('ls')
.description('Find a run') .description('Find a run')
.argument('[load-id]', 'Load ID') .argument('[load-id]', 'Load ID')
.action(async (loadId) => { .action(async (loadId) => {
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
const runs = await step('Getting runs', async () => { const runs = await step('Getting runs', async () => {
return await client.runs.find.query({ loadId }); return await client.runs.find.query({ loadId });

View File

@@ -0,0 +1,59 @@
import { Command } from 'commander';
import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import inquirer from 'inquirer';
import { Config } from '../../config/config.js';
const remove = new Command('remove');
const toInt = (value?: string) => {
if (!value) {
return undefined;
}
return parseInt(value, 10);
};
remove
.alias('ls')
.description('List logs')
.option('-l, --load-id <loadId>', 'Load ID')
.option('-o, --offset <offset>', 'Offset')
.option('-a, --limit <limit>', 'Limit', '1000')
.action(async () => {
const { loadId, offset, limit } = remove.opts();
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => {
return createClient(context);
});
const response = await step('Preparing to delete', async () => {
return await client.runs.prepareRemove.query({
loadId,
offset: toInt(offset),
limit: toInt(limit),
});
});
if (!response.ids.length) {
console.log('No logs to delete');
return;
}
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to delete ${response.ids.length} logs?`,
},
]);
if (!confirm) {
return;
}
await step('Deleting artifacts', async () => {
await client.runs.remove.mutate(response);
});
});
export { remove };

View File

@@ -0,0 +1,23 @@
import { Command } from 'commander';
import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const terminate = new Command('terminate');
terminate
.description('Terminate an in progress run')
.argument('run-id', 'Run ID')
.action(async (runId) => {
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => {
return createClient(context);
});
await step('Terminating run', async () => {
await client.runs.terminate.mutate(runId);
});
});
export { terminate };

View File

@@ -1,8 +1,14 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { create } from './runs.create.js'; import { create } from './runs.create.js';
import { list } from './runs.list.js'; import { list } from './runs.list.js';
import { remove } from './runs.remove.js';
import { terminate } from './runs.terminate.js';
const runs = new Command('runs'); const runs = new Command('runs');
runs.description('Manage runs').addCommand(create).addCommand(list); runs.description('Manage runs');
runs.addCommand(create);
runs.addCommand(list);
runs.addCommand(remove);
runs.addCommand(terminate);
export { runs }; export { runs };

View File

@@ -1,6 +1,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const list = new Command('list'); const list = new Command('list');
@@ -18,8 +20,10 @@ list
.option('-a, --limit <limit>', 'Limit', '1000') .option('-a, --limit <limit>', 'Limit', '1000')
.action(async () => { .action(async () => {
const { offset, limit } = list.opts(); const { offset, limit } = list.opts();
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
const secrets = await step('Getting secrets', async () => { const secrets = await step('Getting secrets', async () => {
return await client.secrets.find.query({ return await client.secrets.find.query({

View File

@@ -1,6 +1,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const remove = new Command('remove'); const remove = new Command('remove');
@@ -8,8 +10,10 @@ remove
.alias('rm') .alias('rm')
.argument('<id>') .argument('<id>')
.action(async (id) => { .action(async (id) => {
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
await step('Removing', async () => { await step('Removing', async () => {
await client.secrets.remove.mutate({ await client.secrets.remove.mutate({

View File

@@ -1,6 +1,8 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { createClient } from '../../client/client.js'; import { createClient } from '../../client/client.js';
import { step } from '../../utils/step.js'; import { step } from '../../utils/step.js';
import { Context } from '../../context/context.js';
import { Config } from '../../config/config.js';
const set = new Command('set'); const set = new Command('set');
@@ -8,8 +10,10 @@ set
.argument('<id>') .argument('<id>')
.argument('[value]') .argument('[value]')
.action(async (id, value) => { .action(async (id, value) => {
const config = new Config();
const context = new Context(config.context);
const client = await step('Connecting to server', async () => { const client = await step('Connecting to server', async () => {
return createClient(); return createClient(context);
}); });
await step('Setting secret', async () => { await step('Setting secret', async () => {
await client.secrets.set.mutate({ await client.secrets.set.mutate({

View File

@@ -0,0 +1,44 @@
import envPaths from 'env-paths';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { mkdir } from 'fs/promises';
import { join, dirname } from 'path';
type ConfigValues = {
context?: string;
};
class Config {
#location: string;
#config?: ConfigValues;
constructor() {
const paths = envPaths('mini-loader');
this.#location = join(paths.config, 'config.json');
if (existsSync(this.#location)) {
this.#config = JSON.parse(readFileSync(this.#location, 'utf-8'));
}
}
public get context() {
return this.#config?.context || 'default';
}
public setContext = (context: string) => {
this.#config = {
...(this.#config || {}),
context,
};
this.save();
};
public save = async () => {
if (!this.#config) {
return;
}
const json = JSON.stringify(this.#config);
mkdir(dirname(this.#location), { recursive: true });
writeFileSync(this.#location, json);
};
}
export { Config };

View File

@@ -0,0 +1,59 @@
import envPaths from 'env-paths';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { mkdir, readdir } from 'fs/promises';
import { dirname, join } from 'path';
type ContextValues = {
host: string;
token: string;
};
class Context {
#location: string;
#config?: ContextValues;
constructor(name: string) {
const paths = envPaths('mini-loader');
this.#location = join(paths.config, 'contexts', name);
if (existsSync(this.#location)) {
this.#config = JSON.parse(readFileSync(this.#location, 'utf-8'));
}
}
public get host() {
return this.#config?.host;
}
public get token() {
return this.#config?.token;
}
public saveLogin = (host: string, token: string) => {
this.#config = {
...(this.#config || {}),
host,
token,
};
this.save();
};
public save = async () => {
if (!this.#config) {
return;
}
const json = JSON.stringify(this.#config);
mkdir(dirname(this.#location), { recursive: true });
writeFileSync(this.#location, json);
};
public static list = async () => {
const paths = envPaths('mini-loader');
const location = join(paths.config, 'contexts');
if (!existsSync(location)) {
return [];
}
return await readdir(location);
};
}
export { Context };

View File

@@ -6,6 +6,7 @@ import { artifacts } from './commands/artifacts/artifacts.js';
import { secrets } from './commands/secrets/secrets.js'; import { secrets } from './commands/secrets/secrets.js';
import { local } from './commands/local/local.js'; import { local } from './commands/local/local.js';
import { auth } from './commands/auth/auth.js'; import { auth } from './commands/auth/auth.js';
import { contexts } from './commands/contexts/contexts.js';
program.addCommand(loads); program.addCommand(loads);
program.addCommand(runs); program.addCommand(runs);
@@ -14,5 +15,6 @@ program.addCommand(artifacts);
program.addCommand(secrets); program.addCommand(secrets);
program.addCommand(local); program.addCommand(local);
program.addCommand(auth); program.addCommand(auth);
program.addCommand(contexts);
await program.parseAsync(); await program.parseAsync();

View File

@@ -4,10 +4,10 @@ const step = async <T>(message: string, fn: () => Promise<T>): Promise<T> => {
const spinner = ora(message).start(); const spinner = ora(message).start();
try { try {
const result = await fn(); const result = await fn();
spinner.succeed(); await spinner.succeed();
return result; return result;
} catch (err) { } catch (err) {
spinner.fail(); await spinner.fail();
throw err; throw err;
} }
}; };

View File

@@ -1,6 +1,7 @@
{ {
"name": "@morten-olsen/mini-loader-configs", "name": "@morten-olsen/mini-loader-configs",
"version": "1.0.0", "version": "1.0.0",
"private": true,
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -8,5 +9,10 @@
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC" "license": "GPL-3.0",
"homepage": "https://github.com/morten-olsen/mini-loader",
"repository": {
"type": "git",
"url": "https://github.com/morten-olsen/mini-loader"
}
} }

View File

@@ -1,6 +1,8 @@
{ {
"name": "@morten-olsen/mini-loader-examples", "name": "@morten-olsen/mini-loader-examples",
"version": "1.0.0", "version": "1.0.0",
"license": "GPL-3.0",
"private": true,
"main": "./dist/esm/index.js", "main": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts", "types": "./dist/esm/index.d.ts",
"scripts": { "scripts": {
@@ -16,10 +18,18 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@morten-olsen/mini-loader-configs": "workspace:^",
"@morten-olsen/mini-loader-cli": "workspace:^",
"@morten-olsen/mini-loader": "workspace:^", "@morten-olsen/mini-loader": "workspace:^",
"@morten-olsen/mini-loader-cli": "workspace:^",
"@morten-olsen/mini-loader-configs": "workspace:^",
"@types/node": "^20.10.8", "@types/node": "^20.10.8",
"typescript": "^5.3.3" "typescript": "^5.3.3"
},
"homepage": "https://github.com/morten-olsen/mini-loader",
"repository": {
"type": "git",
"url": "https://github.com/morten-olsen/mini-loader"
},
"dependencies": {
"fastify": "^4.25.2"
} }
} }

View File

@@ -0,0 +1,12 @@
import { http } from '@morten-olsen/mini-loader';
import fastify from 'fastify';
const server = fastify();
server.all('*', async (req) => {
return req.url;
});
server.listen({
path: http.getPath(),
});

View File

@@ -1,5 +1,9 @@
import { artifacts, logger } from '@morten-olsen/mini-loader'; import { artifacts, logger } from '@morten-olsen/mini-loader';
logger.info('Hello world'); const run = async () => {
await logger.info('Hello world');
await artifacts.create('foo', 'bar');
process.exit(0);
};
artifacts.create('foo', 'bar'); run();

View File

@@ -0,0 +1 @@
[Go to documentation](https://github.com/morten-olsen/mini-loader)

View File

@@ -1,6 +1,7 @@
{ {
"name": "@morten-olsen/mini-loader", "name": "@morten-olsen/mini-loader",
"version": "1.0.0", "version": "1.0.0",
"license": "GPL-3.0",
"main": "./dist/esm/index.js", "main": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts", "types": "./dist/esm/index.d.ts",
"scripts": { "scripts": {
@@ -19,5 +20,10 @@
"@morten-olsen/mini-loader-configs": "workspace:^", "@morten-olsen/mini-loader-configs": "workspace:^",
"@types/node": "^20.10.8", "@types/node": "^20.10.8",
"typescript": "^5.3.3" "typescript": "^5.3.3"
},
"homepage": "https://github.com/morten-olsen/mini-loader",
"repository": {
"type": "git",
"url": "https://github.com/morten-olsen/mini-loader"
} }
} }

View File

@@ -8,8 +8,8 @@ type ArtifactCreateEvent = {
}; };
}; };
const create = (name: string, data: Buffer | string) => { const create = async (name: string, data: Buffer | string) => {
send({ await send({
type: 'artifact:create', type: 'artifact:create',
payload: { payload: {
name, name,

View File

@@ -0,0 +1,7 @@
const getPath = () => process.env.HTTP_GATEWAY_PATH!;
const http = {
getPath,
};
export { http };

View File

@@ -8,3 +8,4 @@ export { logger } from './logger/logger.js';
export { artifacts } from './artifacts/artifacts.js'; export { artifacts } from './artifacts/artifacts.js';
export { input } from './input/input.js'; export { input } from './input/input.js';
export { secrets } from './secrets/secrets.js'; export { secrets } from './secrets/secrets.js';
export { http } from './http/http.js';

View File

@@ -1,7 +1,14 @@
import { workerData } from 'worker_threads'; import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
const get = <T>() => { const path = process.env.INPUT_PATH;
return workerData as T; const hasInput = path ? existsSync(path) : false;
const get = () => {
if (!hasInput || !path) {
return undefined;
}
return readFile(path, 'utf-8');
}; };
const input = { const input = {

View File

@@ -9,31 +9,31 @@ type LoggerEvent = {
}; };
}; };
const sendLog = (event: LoggerEvent['payload']) => { const sendLog = async (event: LoggerEvent['payload']) => {
send({ await send({
type: 'log', type: 'log',
payload: event, payload: event,
}); });
}; };
const info = (message: string, data?: unknown) => { const info = async (message: string, data?: unknown) => {
sendLog({ await sendLog({
severity: 'info', severity: 'info',
message, message,
data, data,
}); });
}; };
const warn = (message: string, data?: unknown) => { const warn = async (message: string, data?: unknown) => {
sendLog({ await sendLog({
severity: 'warning', severity: 'warning',
message, message,
data, data,
}); });
}; };
const error = (message: string, data?: unknown) => { const error = async (message: string, data?: unknown) => {
sendLog({ await sendLog({
severity: 'error', severity: 'error',
message, message,
data, data,

View File

@@ -1,8 +1,7 @@
import { workerData } from 'worker_threads'; const secretData = JSON.parse(process.env.SECRETS || '{}');
const get = (id: string) => { const get = (id: string) => {
const items = workerData?.secrets ?? {}; return secretData[id];
return items[id];
}; };
const secrets = { const secrets = {

View File

@@ -1,8 +1,28 @@
import { parentPort } from 'worker_threads'; import { Socket, createConnection } from 'net';
const send = (data: any) => { const connect = () =>
new Promise<Socket>((resolve, reject) => {
const current = createConnection(process.env.HOST_SOCKET!);
current.on('connect', () => {
resolve(current);
});
current.on('error', (error) => {
reject(error);
});
});
const send = async (data: any) =>
new Promise<void>(async (resolve, reject) => {
const connection = await connect();
const cleaned = JSON.parse(JSON.stringify(data)); const cleaned = JSON.parse(JSON.stringify(data));
parentPort?.postMessage(cleaned); connection.write(JSON.stringify(cleaned), 'utf-8', (err) => {
}; if (err) {
reject(err);
} else {
resolve();
}
});
});
export { send }; export { send };

View File

@@ -0,0 +1 @@
[Go to documentation](https://github.com/morten-olsen/mini-loader)

View File

@@ -1,6 +1,7 @@
{ {
"name": "@morten-olsen/mini-loader-runner", "name": "@morten-olsen/mini-loader-runner",
"version": "1.0.0", "version": "1.0.0",
"license": "GPL-3.0",
"main": "./dist/esm/index.js", "main": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts", "types": "./dist/esm/index.d.ts",
"scripts": { "scripts": {
@@ -17,11 +18,17 @@
}, },
"devDependencies": { "devDependencies": {
"@morten-olsen/mini-loader-configs": "workspace:^", "@morten-olsen/mini-loader-configs": "workspace:^",
"@morten-olsen/mini-loader": "workspace:^",
"@types/node": "^20.10.8", "@types/node": "^20.10.8",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"dependencies": { "dependencies": {
"eventemitter3": "^5.0.1" "@morten-olsen/mini-loader": "workspace:^",
"eventemitter3": "^5.0.1",
"nanoid": "^5.0.4"
},
"homepage": "https://github.com/morten-olsen/mini-loader",
"repository": {
"type": "git",
"url": "https://github.com/morten-olsen/mini-loader"
} }
} }

View File

@@ -1,46 +1,62 @@
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
import { EventEmitter } from 'eventemitter3'; import { setup } from './setup/setup.js';
import { Event } from '@morten-olsen/mini-loader';
type RunEvents = {
message: (event: Event) => void;
error: (error: Error) => void;
exit: () => void;
};
type RunOptions = { type RunOptions = {
script: string; script: string;
input?: unknown; input?: Buffer | string;
secrets?: Record<string, string>; secrets?: Record<string, string>;
}; };
const run = async ({ script, input, secrets }: RunOptions) => { const run = async ({ script, input, secrets }: RunOptions) => {
const emitter = new EventEmitter<RunEvents>(); const info = await setup({ script, input, secrets });
const worker = new Worker(script, {
eval: true, const worker = new Worker(info.scriptLocation, {
env: secrets, stdin: false,
workerData: { stdout: false,
input, stderr: false,
secrets, env: info.env,
});
worker.stdout?.on('data', (data) => {
info.emitter.emit('message', {
type: 'log',
payload: {
severity: 'info',
message: data.toString(),
}, },
}); });
});
worker.stderr?.on('data', (data) => {
info.emitter.emit('message', {
type: 'log',
payload: {
severity: 'error',
message: data.toString(),
},
});
});
const promise = new Promise<void>((resolve, reject) => { const promise = new Promise<void>((resolve, reject) => {
worker.on('message', (message: Event) => { worker.on('exit', async () => {
emitter.emit('message', message); await info.teardown();
});
worker.on('exit', () => {
resolve(); resolve();
}); });
worker.on('error', (error) => { worker.on('error', async (error) => {
reject(error); reject(error);
}); });
}); });
return { return {
emitter, ...info,
teardown: async () => {
worker.terminate();
},
promise, promise,
}; };
}; };
type RunInfo = Awaited<ReturnType<typeof run>>;
export type { RunInfo };
export { run }; export { run };

View File

@@ -0,0 +1,71 @@
import { join } from 'path';
import os from 'os';
import { nanoid } from 'nanoid';
import { chmod, mkdir, rm, writeFile } from 'fs/promises';
import { createServer } from 'net';
import { EventEmitter } from 'eventemitter3';
type SetupOptions = {
input?: Buffer | string;
script: string;
secrets?: Record<string, string>;
};
type RunEvents = {
message: (event: any) => void;
error: (error: Error) => void;
exit: () => void;
};
const setup = async (options: SetupOptions) => {
const { input, script, secrets } = options;
const emitter = new EventEmitter<RunEvents>();
const dataDir = join(os.tmpdir(), 'mini-loader', nanoid());
await mkdir(dataDir, { recursive: true });
await chmod(dataDir, 0o700);
const hostSocket = join(dataDir, 'host');
const httpGatewaySocket = join(dataDir, 'socket');
const server = createServer();
const inputLocation = join(dataDir, 'input');
const scriptLocation = join(dataDir, 'script.js');
if (input) {
await writeFile(inputLocation, input);
}
await writeFile(scriptLocation, script);
const env = {
HOST_SOCKET: hostSocket,
SECRETS: JSON.stringify(secrets || {}),
INPUT_PATH: inputLocation,
HTTP_GATEWAY_PATH: httpGatewaySocket,
};
const teardown = async () => {
server.close();
await rm(dataDir, { recursive: true, force: true });
};
server.on('connection', (socket) => {
socket.on('data', (data) => {
const message = JSON.parse(data.toString());
emitter.emit('message', message);
});
});
server.listen(hostSocket);
return {
env,
emitter,
teardown,
httpGatewaySocket,
scriptLocation,
hostSocket,
};
};
type Setup = Awaited<ReturnType<typeof setup>>;
export type { Setup };
export { setup };

View File

@@ -0,0 +1 @@
[Go to documentation](https://github.com/morten-olsen/mini-loader)

View File

@@ -1,6 +1,7 @@
{ {
"name": "@morten-olsen/mini-loader-server", "name": "@morten-olsen/mini-loader-server",
"version": "1.0.0", "version": "1.0.0",
"license": "GPL-3.0",
"main": "./dist/esm/index.js", "main": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts", "types": "./dist/esm/index.d.ts",
"bin": { "bin": {
@@ -26,6 +27,7 @@
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"dependencies": { "dependencies": {
"@fastify/reply-from": "^9.7.0",
"@trpc/client": "^10.45.0", "@trpc/client": "^10.45.0",
"@trpc/server": "^10.45.0", "@trpc/server": "^10.45.0",
"commander": "^11.1.0", "commander": "^11.1.0",
@@ -38,5 +40,10 @@
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"superjson": "^2.2.1", "superjson": "^2.2.1",
"zod": "^3.22.4" "zod": "^3.22.4"
},
"homepage": "https://github.com/morten-olsen/mini-loader",
"repository": {
"type": "git",
"url": "https://github.com/morten-olsen/mini-loader"
} }
} }

View File

@@ -0,0 +1,34 @@
import { FastifyPluginAsync } from 'fastify';
import FastifyReplyFrom from '@fastify/reply-from';
import { escape } from 'querystring';
import { Runtime } from '../runtime/runtime.js';
type Options = {
runtime: Runtime;
};
const gateway: FastifyPluginAsync<Options> = async (fastify, { runtime }) => {
await fastify.register(FastifyReplyFrom, {
http: {},
});
fastify.all('/gateway/*', (req, res) => {
const [runId, ...pathSegments] = (req.params as any)['*'].split('/').filter(Boolean);
const run = runtime.runner.getInstance(runId);
if (!run) {
res.statusCode = 404;
res.send({ error: 'Run not found' });
return;
}
const socketPath = run.run?.httpGatewaySocket;
if (!socketPath) {
res.statusCode = 404;
res.send({ error: 'No socket path to run' });
return;
}
const path = pathSegments.join('/');
res.from(`unix+http://${escape(socketPath)}/${path}`);
});
};
export { gateway };

View File

@@ -27,5 +27,9 @@ program.addCommand(createToken);
await program.parseAsync(process.argv); await program.parseAsync(process.argv);
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
export type { Runtime } from './runtime/runtime.js'; export type { Runtime } from './runtime/runtime.js';
export type { RootRouter } from './router/router.js'; export type { RootRouter } from './router/router.js';

View File

@@ -18,6 +18,13 @@ class ArtifactRepo extends EventEmitter<ArtifactRepoEvents> {
this.#options = options; this.#options = options;
} }
public get = async (id: string) => {
const { database } = this.#options;
const db = await database.instance;
const result = await db('artifacts').where({ id }).first();
return result || null;
};
public add = async (options: AddArtifactOptions) => { public add = async (options: AddArtifactOptions) => {
const { database } = this.#options; const { database } = this.#options;
const db = await database.instance; const db = await database.instance;
@@ -59,8 +66,9 @@ class ArtifactRepo extends EventEmitter<ArtifactRepoEvents> {
query.limit(options.limit); query.limit(options.limit);
} }
const ids = await query; const result = await query;
const token = ids.map((id) => Buffer.from(id.id).toString('base64')).join('|'); const ids = result.map((row) => row.id);
const token = ids.map((id) => Buffer.from(id).toString('base64')).join('|');
const hash = createHash('sha256').update(token).digest('hex'); const hash = createHash('sha256').update(token).digest('hex');
return { return {
ids, ids,

View File

@@ -56,8 +56,9 @@ class LogRepo extends EventEmitter<LogRepoEvents> {
query.whereIn('severity', options.severities); query.whereIn('severity', options.severities);
} }
const ids = await query; const result = await query;
const token = ids.map((id) => Buffer.from(id.id).toString('base64')).join('|'); const ids = result.map((row) => row.id);
const token = ids.map((id) => Buffer.from(id).toString('base64')).join('|');
const hash = createHash('sha256').update(token).digest('hex'); const hash = createHash('sha256').update(token).digest('hex');
return { return {
ids, ids,

View File

@@ -3,6 +3,7 @@ import { EventEmitter } from 'eventemitter3';
import { Database } from '../../database/database.js'; import { Database } from '../../database/database.js';
import { CreateRunOptions, FindRunsOptions, UpdateRunOptions } from './runs.schemas.js'; import { CreateRunOptions, FindRunsOptions, UpdateRunOptions } from './runs.schemas.js';
import { LoadRepo } from '../loads/loads.js'; import { LoadRepo } from '../loads/loads.js';
import { createHash } from 'crypto';
type RunRepoEvents = { type RunRepoEvents = {
created: (args: { id: string; loadId: string }) => void; created: (args: { id: string; loadId: string }) => void;
@@ -18,13 +19,22 @@ type RunRepoOptions = {
class RunRepo extends EventEmitter<RunRepoEvents> { class RunRepo extends EventEmitter<RunRepoEvents> {
#options: RunRepoOptions; #options: RunRepoOptions;
#isReady: Promise<void>;
constructor(options: RunRepoOptions) { constructor(options: RunRepoOptions) {
super(); super();
this.#options = options; this.#options = options;
this.#isReady = this.#setup();
} }
#setup = async () => {
const { database } = this.#options;
const db = await database.instance;
await db('runs').update({ status: 'failed', error: 'server was shut down' }).where({ status: 'running' });
};
public getById = async (id: string) => { public getById = async (id: string) => {
await this.#isReady;
const { database } = this.#options; const { database } = this.#options;
const db = await database.instance; const db = await database.instance;
@@ -36,6 +46,7 @@ class RunRepo extends EventEmitter<RunRepoEvents> {
}; };
public getByLoadId = async (loadId: string) => { public getByLoadId = async (loadId: string) => {
await this.#isReady;
const { database } = this.#options; const { database } = this.#options;
const db = await database.instance; const db = await database.instance;
@@ -44,6 +55,7 @@ class RunRepo extends EventEmitter<RunRepoEvents> {
}; };
public find = async (options: FindRunsOptions) => { public find = async (options: FindRunsOptions) => {
await this.#isReady;
const { database } = this.#options; const { database } = this.#options;
const db = await database.instance; const db = await database.instance;
const query = db('runs').select(['id', 'status', 'startedAt', 'status', 'error', 'endedAt']); const query = db('runs').select(['id', 'status', 'startedAt', 'status', 'error', 'endedAt']);
@@ -62,19 +74,41 @@ class RunRepo extends EventEmitter<RunRepoEvents> {
return runs; return runs;
}; };
public remove = async (options: FindRunsOptions) => { public prepareRemove = async (options: FindRunsOptions) => {
await this.#isReady;
const { database } = this.#options; const { database } = this.#options;
const db = await database.instance; const db = await database.instance;
const query = db('runs'); const query = db('runs').select('id');
if (options.loadId) { if (options.loadId) {
query.where({ loadId: options.loadId }); query.where({ loadId: options.loadId });
} }
await query.del(); const result = await query;
const ids = result.map((row) => row.id);
const token = ids.map((id) => Buffer.from(id).toString('base64')).join('|');
const hash = createHash('sha256').update(token).digest('hex');
return {
ids,
hash,
};
};
public remove = async (hash: string, ids: string[]) => {
const { database } = this.#options;
const db = await database.instance;
const token = ids.map((id) => Buffer.from(id).toString('base64')).join('|');
const actualHash = createHash('sha256').update(token).digest('hex');
if (hash !== actualHash) {
throw new Error('Invalid hash');
}
await db('runs').whereIn('id', ids).delete();
}; };
public started = async (id: string) => { public started = async (id: string) => {
await this.#isReady;
const { database } = this.#options; const { database } = this.#options;
const db = await database.instance; const db = await database.instance;
const current = await this.getById(id); const current = await this.getById(id);
@@ -92,6 +126,7 @@ class RunRepo extends EventEmitter<RunRepoEvents> {
}; };
public finished = async (id: string, options: UpdateRunOptions) => { public finished = async (id: string, options: UpdateRunOptions) => {
await this.#isReady;
const { database } = this.#options; const { database } = this.#options;
const db = await database.instance; const db = await database.instance;
const { loadId } = await this.getById(id); const { loadId } = await this.getById(id);
@@ -114,6 +149,7 @@ class RunRepo extends EventEmitter<RunRepoEvents> {
}; };
public create = async (options: CreateRunOptions) => { public create = async (options: CreateRunOptions) => {
await this.#isReady;
const { database, loads } = this.#options; const { database, loads } = this.#options;
const id = nanoid(); const id = nanoid();
const db = await database.instance; const db = await database.instance;

View File

@@ -11,12 +11,21 @@ const find = publicProcedure.input(findArtifactsSchema).query(async ({ input, ct
return result; return result;
}); });
const get = publicProcedure.input(z.string()).query(async ({ input, ctx }) => {
const { runtime } = ctx;
const { repos } = runtime;
const { artifacts } = repos;
const result = await artifacts.get(input);
return result;
});
const prepareRemove = publicProcedure.input(findArtifactsSchema).query(async ({ input, ctx }) => { const prepareRemove = publicProcedure.input(findArtifactsSchema).query(async ({ input, ctx }) => {
const { runtime } = ctx; const { runtime } = ctx;
const { repos } = runtime; const { repos } = runtime;
const { artifacts } = repos; const { artifacts } = repos;
await artifacts.prepareRemove(input); return await artifacts.prepareRemove(input);
}); });
const remove = publicProcedure const remove = publicProcedure
@@ -35,6 +44,7 @@ const remove = publicProcedure
}); });
const artifactsRouter = router({ const artifactsRouter = router({
get,
find, find,
remove, remove,
prepareRemove, prepareRemove,

View File

@@ -16,7 +16,7 @@ const prepareRemove = publicProcedure.input(findLogsSchema).query(async ({ input
const { repos } = runtime; const { repos } = runtime;
const { logs } = repos; const { logs } = repos;
await logs.prepareRemove(input); return await logs.prepareRemove(input);
}); });
const remove = publicProcedure const remove = publicProcedure

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import { createRunSchema, findRunsSchema } from '../repos/repos.js'; import { createRunSchema, findRunsSchema } from '../repos/repos.js';
import { publicProcedure, router } from './router.utils.js'; import { publicProcedure, router } from './router.utils.js';
@@ -17,17 +18,50 @@ const find = publicProcedure.input(findRunsSchema).query(async ({ input, ctx })
return results; return results;
}); });
const remove = publicProcedure.input(findRunsSchema).mutation(async ({ input, ctx }) => { const prepareRemove = publicProcedure.input(findRunsSchema).query(async ({ input, ctx }) => {
const { runtime } = ctx; const { runtime } = ctx;
const { repos } = runtime; const { repos } = runtime;
const { runs } = repos; const { runs } = repos;
await runs.remove(input); return await runs.prepareRemove(input);
});
const remove = publicProcedure
.input(
z.object({
hash: z.string(),
ids: z.array(z.string()),
}),
)
.mutation(async ({ input, ctx }) => {
const { runtime } = ctx;
const { repos } = runtime;
const { runs } = repos;
for (const id of input.ids) {
const instance = runtime.runner.getInstance(id);
if (instance) {
await instance.run?.teardown();
}
}
await runs.remove(input.hash, input.ids);
});
const terminate = publicProcedure.input(z.string()).mutation(async ({ input, ctx }) => {
const { runtime } = ctx;
const { runner } = runtime;
const instance = runner.getInstance(input);
if (!instance || !instance.run) {
return;
}
await instance.run.teardown();
}); });
const runsRouter = router({ const runsRouter = router({
create, create,
find, find,
remove, remove,
prepareRemove,
terminate,
}); });
export { runsRouter }; export { runsRouter };

View File

@@ -14,7 +14,8 @@ const createContext = async ({ runtime }: ContextOptions) => {
if (!authorization) { if (!authorization) {
throw new Error('No authorization header'); throw new Error('No authorization header');
} }
await auth.validateToken(authorization); const [, token] = authorization.split(' ');
await auth.validateToken(token);
return { return {
runtime, runtime,
}; };

View File

@@ -1,5 +1,5 @@
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { run } from '@morten-olsen/mini-loader-runner'; import { RunInfo, run } from '@morten-olsen/mini-loader-runner';
import { Repos } from '../repos/repos.js'; import { Repos } from '../repos/repos.js';
import { LoggerEvent } from '../../../mini-loader/dist/esm/logger/logger.js'; import { LoggerEvent } from '../../../mini-loader/dist/esm/logger/logger.js';
import { ArtifactCreateEvent } from '../../../mini-loader/dist/esm/artifacts/artifacts.js'; import { ArtifactCreateEvent } from '../../../mini-loader/dist/esm/artifacts/artifacts.js';
@@ -20,12 +20,17 @@ type RunnerInstanceOptions = {
class RunnerInstance extends EventEmitter<RunnerInstanceEvents> { class RunnerInstance extends EventEmitter<RunnerInstanceEvents> {
#options: RunnerInstanceOptions; #options: RunnerInstanceOptions;
#run?: RunInfo;
constructor(options: RunnerInstanceOptions) { constructor(options: RunnerInstanceOptions) {
super(); super();
this.#options = options; this.#options = options;
} }
public get run() {
return this.#run;
}
#addLog = async (event: LoggerEvent['payload']) => { #addLog = async (event: LoggerEvent['payload']) => {
const { repos, id, loadId } = this.#options; const { repos, id, loadId } = this.#options;
const { logs } = repos; const { logs } = repos;
@@ -54,15 +59,17 @@ class RunnerInstance extends EventEmitter<RunnerInstanceEvents> {
const { runs, secrets } = repos; const { runs, secrets } = repos;
try { try {
const { script: scriptHash, input } = await runs.getById(id); const { script: scriptHash, input } = await runs.getById(id);
const scriptLocation = resolve(config.files.location, 'script', `${scriptHash}.js`); const scriptLocation = resolve(config.files.location, 'scripts', `${scriptHash}.js`);
const script = await readFile(scriptLocation, 'utf-8'); const script = await readFile(scriptLocation, 'utf-8');
const allSecrets = await secrets.getAll(); const allSecrets = await secrets.getAll();
await runs.started(id); await runs.started(id);
const { promise, emitter } = await run({ const current = await run({
script, script,
secrets: allSecrets, secrets: allSecrets,
input, input,
}); });
this.#run = current;
const { promise, emitter } = current;
emitter.on('message', (message) => { emitter.on('message', (message) => {
switch (message.type) { switch (message.type) {
case 'log': { case 'log': {
@@ -84,9 +91,11 @@ class RunnerInstance extends EventEmitter<RunnerInstanceEvents> {
} }
await runs.finished(id, { status: 'failed', error: errorMessage }); await runs.finished(id, { status: 'failed', error: errorMessage });
} finally { } finally {
this.#run = undefined;
this.emit('completed', { id }); this.emit('completed', { id });
} }
}; };
} }
export type { RunInfo };
export { RunnerInstance }; export { RunnerInstance };

View File

@@ -36,6 +36,10 @@ class Runner {
this.#instances.set(args.id, instance); this.#instances.set(args.id, instance);
await instance.start(); await instance.start();
}; };
public getInstance = (id: string) => {
return this.#instances.get(id);
};
} }
export { Runner }; export { Runner };

View File

@@ -3,13 +3,33 @@ import fastify from 'fastify';
import { RootRouter, rootRouter } from '../router/router.js'; import { RootRouter, rootRouter } from '../router/router.js';
import { createContext } from '../router/router.utils.js'; import { createContext } from '../router/router.utils.js';
import { Runtime } from '../runtime/runtime.js'; import { Runtime } from '../runtime/runtime.js';
import { gateway } from '../gateway/gateway.js';
const createServer = async (runtime: Runtime) => { const createServer = async (runtime: Runtime) => {
const server = fastify({}); const server = fastify({
maxParamLength: 10000,
bodyLimit: 30 * 1024 * 1024,
logger: {
level: 'warn',
},
});
server.get('/', async () => { server.get('/', async () => {
return { hello: 'world' }; return { hello: 'world' };
}); });
server.get('/health', async (req) => {
let authorized = false;
try {
const { authorization } = req.headers;
if (authorization) {
const [, token] = authorization.split(' ');
await runtime.auth.validateToken(token);
authorized = true;
}
} catch (error) {}
return { authorized, status: 'ok' };
});
server.register(fastifyTRPCPlugin, { server.register(fastifyTRPCPlugin, {
prefix: '/trpc', prefix: '/trpc',
trpcOptions: { trpcOptions: {
@@ -20,6 +40,14 @@ const createServer = async (runtime: Runtime) => {
}, },
} satisfies FastifyTRPCPluginOptions<RootRouter>['trpcOptions'], } satisfies FastifyTRPCPluginOptions<RootRouter>['trpcOptions'],
}); });
server.register(gateway, {
runtime,
});
server.addHook('onError', async (request, reply, error) => {
console.error(error);
});
await server.ready(); await server.ready();
return server; return server;

60
pnpm-lock.yaml generated
View File

@@ -60,6 +60,9 @@ importers:
commander: commander:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0 version: 11.1.0
env-paths:
specifier: ^3.0.0
version: 3.0.0
inquirer: inquirer:
specifier: ^9.2.12 specifier: ^9.2.12
version: 9.2.12 version: 9.2.12
@@ -98,6 +101,10 @@ importers:
packages/configs: {} packages/configs: {}
packages/examples: packages/examples:
dependencies:
fastify:
specifier: ^4.25.2
version: 4.25.2
devDependencies: devDependencies:
'@morten-olsen/mini-loader': '@morten-olsen/mini-loader':
specifier: workspace:^ specifier: workspace:^
@@ -129,13 +136,16 @@ importers:
packages/runner: packages/runner:
dependencies: dependencies:
eventemitter3:
specifier: ^5.0.1
version: 5.0.1
devDependencies:
'@morten-olsen/mini-loader': '@morten-olsen/mini-loader':
specifier: workspace:^ specifier: workspace:^
version: link:../mini-loader version: link:../mini-loader
eventemitter3:
specifier: ^5.0.1
version: 5.0.1
nanoid:
specifier: ^5.0.4
version: 5.0.4
devDependencies:
'@morten-olsen/mini-loader-configs': '@morten-olsen/mini-loader-configs':
specifier: workspace:^ specifier: workspace:^
version: link:../configs version: link:../configs
@@ -148,6 +158,9 @@ importers:
packages/server: packages/server:
dependencies: dependencies:
'@fastify/reply-from':
specifier: ^9.7.0
version: 9.7.0
'@trpc/client': '@trpc/client':
specifier: ^10.45.0 specifier: ^10.45.0
version: 10.45.0(@trpc/server@10.45.0) version: 10.45.0(@trpc/server@10.45.0)
@@ -470,6 +483,11 @@ packages:
fast-uri: 2.3.0 fast-uri: 2.3.0
dev: false dev: false
/@fastify/busboy@2.1.0:
resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==}
engines: {node: '>=14'}
dev: false
/@fastify/deepmerge@1.3.0: /@fastify/deepmerge@1.3.0:
resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==}
dev: false dev: false
@@ -484,6 +502,19 @@ packages:
fast-json-stringify: 5.10.0 fast-json-stringify: 5.10.0
dev: false dev: false
/@fastify/reply-from@9.7.0:
resolution: {integrity: sha512-/F1QBl3FGlTqStjmiuoLRDchVxP967TZh6FZPwQteWhdLsDec8mqSACE+cRzw6qHUj3v9hfdd7JNgmb++fyFhQ==}
dependencies:
'@fastify/error': 3.4.1
end-of-stream: 1.4.4
fast-content-type-parse: 1.1.0
fast-querystring: 1.1.2
fastify-plugin: 4.5.1
pump: 3.0.0
tiny-lru: 11.2.5
undici: 5.28.2
dev: false
/@gar/promisify@1.1.3: /@gar/promisify@1.1.3:
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
requiresBuild: true requiresBuild: true
@@ -2206,6 +2237,11 @@ packages:
dev: false dev: false
optional: true optional: true
/env-paths@3.0.0:
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dev: false
/err-code@2.0.3: /err-code@2.0.3:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
requiresBuild: true requiresBuild: true
@@ -2672,6 +2708,10 @@ packages:
resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==} resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==}
dev: false dev: false
/fastify-plugin@4.5.1:
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
dev: false
/fastify@4.25.2: /fastify@4.25.2:
resolution: {integrity: sha512-SywRouGleDHvRh054onj+lEZnbC1sBCLkR0UY3oyJwjD4BdZJUrxBqfkfCaqn74pVCwBaRHGuL3nEWeHbHzAfw==} resolution: {integrity: sha512-SywRouGleDHvRh054onj+lEZnbC1sBCLkR0UY3oyJwjD4BdZJUrxBqfkfCaqn74pVCwBaRHGuL3nEWeHbHzAfw==}
dependencies: dependencies:
@@ -5140,6 +5180,11 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: false dev: false
/tiny-lru@11.2.5:
resolution: {integrity: sha512-JpqM0K33lG6iQGKiigcwuURAKZlq6rHXfrgeL4/I8/REoyJTGU+tEMszvT/oTRVHG2OiylhGDjqPp1jWMlr3bw==}
engines: {node: '>=12'}
dev: false
/tmp@0.0.33: /tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'} engines: {node: '>=0.6.0'}
@@ -5357,6 +5402,13 @@ packages:
/undici-types@5.26.5: /undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
/undici@5.28.2:
resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==}
engines: {node: '>=14.0'}
dependencies:
'@fastify/busboy': 2.1.0
dev: false
/unique-filename@1.1.1: /unique-filename@1.1.1:
resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==}
requiresBuild: true requiresBuild: true

View File

@@ -1,7 +1,9 @@
import { findWorkspacePackages } from '@pnpm/find-workspace-packages'; import { findWorkspacePackages } from '@pnpm/find-workspace-packages';
import { writeFile } from 'fs/promises'; import { readFile, writeFile } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
const sharedData = JSON.parse(await readFile(join(process.cwd(), 'scripts/shared-data.json')));
const version = process.argv[2]; const version = process.argv[2];
if (!version) { if (!version) {
throw new Error('Version is required'); throw new Error('Version is required');
@@ -11,6 +13,9 @@ const packages = await findWorkspacePackages(process.cwd());
for (const { manifest, dir } of packages) { for (const { manifest, dir } of packages) {
console.log(dir, version); console.log(dir, version);
for (let [key, value] of Object.entries(sharedData || {})) {
manifest[key] = value;
}
manifest.version = version; manifest.version = version;
await writeFile(join(dir, 'package.json'), JSON.stringify(manifest, null, 2)); await writeFile(join(dir, 'package.json'), JSON.stringify(manifest, null, 2));
} }

8
scripts/shared-data.json Normal file
View File

@@ -0,0 +1,8 @@
{
"license": "GPL-3.0",
"homepage": "https://github.com/morten-olsen/mini-loader",
"repository": {
"type": "git",
"url": "https://github.com/morten-olsen/mini-loader"
}
}

View File

@@ -1,15 +1,15 @@
{ {
"include": [], "include": [],
"references": [ "references": [
{
"path": "./packages/mini-loader/tsconfig.json"
},
{ {
"path": "./packages/runner/tsconfig.json" "path": "./packages/runner/tsconfig.json"
}, },
{ {
"path": "./packages/server/tsconfig.json" "path": "./packages/server/tsconfig.json"
}, },
{
"path": "./packages/mini-loader/tsconfig.json"
},
{ {
"path": "./packages/cli/tsconfig.json" "path": "./packages/cli/tsconfig.json"
}, },