5 Commits
0.1.7 ... 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.
@@ -41,4 +46,4 @@ After running the command, you should see an output confirming that a new artifa
Congratulations on setting up and running your first script with mini loader! You're now ready to take the next step. Congratulations on setting up and running your first script with mini loader! You're now ready to take the next step.
[Next: Setting Up the Server](./setup-server.md) [Next: Setting Up the Server](./setup-server.md)

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.
@@ -67,4 +67,4 @@ Replace `<id>` with the identifier of the artifact you wish to download.
You're now equipped to manage loads, runs, logs, and artifacts using the mini loader CLI. For advanced usage, such as managing secrets, proceed to the next section. You're now equipped to manage loads, runs, logs, and artifacts using the mini loader CLI. For advanced usage, such as managing secrets, proceed to the next section.
[Next: Managing Secrets](./managing-secrets.md) [Next: Managing Secrets](./managing-secrets.md)

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 = () =>
const cleaned = JSON.parse(JSON.stringify(data)); new Promise<Socket>((resolve, reject) => {
parentPort?.postMessage(cleaned); 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));
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"
}, },