@ketvietlab/ketjs 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/agent/capabilities.d.ts +62 -0
- package/dist/agent/capabilities.d.ts.map +1 -0
- package/dist/agent/capabilities.js +89 -0
- package/dist/agent/capabilities.js.map +1 -0
- package/dist/agent/permissions.d.ts +56 -0
- package/dist/agent/permissions.d.ts.map +1 -0
- package/dist/agent/permissions.js +209 -0
- package/dist/agent/permissions.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +698 -0
- package/dist/cli.js.map +1 -0
- package/dist/codegen/dts.d.ts +3 -0
- package/dist/codegen/dts.d.ts.map +1 -0
- package/dist/codegen/dts.js +94 -0
- package/dist/codegen/dts.js.map +1 -0
- package/dist/data/adapter.d.ts +4 -0
- package/dist/data/adapter.d.ts.map +1 -0
- package/dist/data/adapter.js +18 -0
- package/dist/data/adapter.js.map +1 -0
- package/dist/data/changeset.d.ts +49 -0
- package/dist/data/changeset.d.ts.map +1 -0
- package/dist/data/changeset.js +206 -0
- package/dist/data/changeset.js.map +1 -0
- package/dist/data/expr.d.ts +59 -0
- package/dist/data/expr.d.ts.map +1 -0
- package/dist/data/expr.js +61 -0
- package/dist/data/expr.js.map +1 -0
- package/dist/data/fleet.d.ts +28 -0
- package/dist/data/fleet.d.ts.map +1 -0
- package/dist/data/fleet.js +82 -0
- package/dist/data/fleet.js.map +1 -0
- package/dist/data/list-search.d.ts +93 -0
- package/dist/data/list-search.d.ts.map +1 -0
- package/dist/data/list-search.js +268 -0
- package/dist/data/list-search.js.map +1 -0
- package/dist/data/migrate.d.ts +75 -0
- package/dist/data/migrate.d.ts.map +1 -0
- package/dist/data/migrate.js +136 -0
- package/dist/data/migrate.js.map +1 -0
- package/dist/data/pool.d.ts +20 -0
- package/dist/data/pool.d.ts.map +1 -0
- package/dist/data/pool.js +92 -0
- package/dist/data/pool.js.map +1 -0
- package/dist/data/query.d.ts +111 -0
- package/dist/data/query.d.ts.map +1 -0
- package/dist/data/query.js +268 -0
- package/dist/data/query.js.map +1 -0
- package/dist/data/sqlite.d.ts +3 -0
- package/dist/data/sqlite.d.ts.map +1 -0
- package/dist/data/sqlite.js +107 -0
- package/dist/data/sqlite.js.map +1 -0
- package/dist/data/time.d.ts +9 -0
- package/dist/data/time.d.ts.map +1 -0
- package/dist/data/time.js +107 -0
- package/dist/data/time.js.map +1 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/apps.d.ts +43 -0
- package/dist/kernel/apps.d.ts.map +1 -0
- package/dist/kernel/apps.js +234 -0
- package/dist/kernel/apps.js.map +1 -0
- package/dist/kernel/compose.d.ts +6 -0
- package/dist/kernel/compose.d.ts.map +1 -0
- package/dist/kernel/compose.js +976 -0
- package/dist/kernel/compose.js.map +1 -0
- package/dist/kernel/define.d.ts +9 -0
- package/dist/kernel/define.d.ts.map +1 -0
- package/dist/kernel/define.js +140 -0
- package/dist/kernel/define.js.map +1 -0
- package/dist/kernel/diff.d.ts +11 -0
- package/dist/kernel/diff.d.ts.map +1 -0
- package/dist/kernel/diff.js +106 -0
- package/dist/kernel/diff.js.map +1 -0
- package/dist/kernel/errors.d.ts +17 -0
- package/dist/kernel/errors.d.ts.map +1 -0
- package/dist/kernel/errors.js +43 -0
- package/dist/kernel/errors.js.map +1 -0
- package/dist/kernel/graph.d.ts +3 -0
- package/dist/kernel/graph.d.ts.map +1 -0
- package/dist/kernel/graph.js +56 -0
- package/dist/kernel/graph.js.map +1 -0
- package/dist/kernel/i18n.d.ts +43 -0
- package/dist/kernel/i18n.d.ts.map +1 -0
- package/dist/kernel/i18n.js +79 -0
- package/dist/kernel/i18n.js.map +1 -0
- package/dist/kernel/index.d.ts +7 -0
- package/dist/kernel/index.d.ts.map +1 -0
- package/dist/kernel/index.js +7 -0
- package/dist/kernel/index.js.map +1 -0
- package/dist/kernel/layout.d.ts +17 -0
- package/dist/kernel/layout.d.ts.map +1 -0
- package/dist/kernel/layout.js +77 -0
- package/dist/kernel/layout.js.map +1 -0
- package/dist/kernel/menu.d.ts +27 -0
- package/dist/kernel/menu.d.ts.map +1 -0
- package/dist/kernel/menu.js +65 -0
- package/dist/kernel/menu.js.map +1 -0
- package/dist/kernel/modules.d.ts +31 -0
- package/dist/kernel/modules.d.ts.map +1 -0
- package/dist/kernel/modules.js +322 -0
- package/dist/kernel/modules.js.map +1 -0
- package/dist/kernel/routes.d.ts +24 -0
- package/dist/kernel/routes.d.ts.map +1 -0
- package/dist/kernel/routes.js +98 -0
- package/dist/kernel/routes.js.map +1 -0
- package/dist/kernel/types.d.ts +8 -0
- package/dist/kernel/types.d.ts.map +1 -0
- package/dist/kernel/types.js +67 -0
- package/dist/kernel/types.js.map +1 -0
- package/dist/kernel/workspace.d.ts +62 -0
- package/dist/kernel/workspace.d.ts.map +1 -0
- package/dist/kernel/workspace.js +156 -0
- package/dist/kernel/workspace.js.map +1 -0
- package/dist/scaffold/index.d.ts +2 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +49 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/templates/app.test.ts.tmpl +17 -0
- package/dist/scaffold/templates/biome.json.tmpl +30 -0
- package/dist/scaffold/templates/dev.mjs.tmpl +16 -0
- package/dist/scaffold/templates/gitignore.tmpl +24 -0
- package/dist/scaffold/templates/ket.workspace.ts.tmpl +18 -0
- package/dist/scaffold/templates/module.ts.tmpl +27 -0
- package/dist/scaffold/templates/package.json.tmpl +25 -0
- package/dist/scaffold/templates/tsconfig.json.tmpl +15 -0
- package/dist/server/boot.d.ts +173 -0
- package/dist/server/boot.d.ts.map +1 -0
- package/dist/server/boot.js +659 -0
- package/dist/server/boot.js.map +1 -0
- package/dist/server/config.d.ts +48 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +82 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/ctx.d.ts +13 -0
- package/dist/server/ctx.d.ts.map +1 -0
- package/dist/server/ctx.js +496 -0
- package/dist/server/ctx.js.map +1 -0
- package/dist/server/fn.d.ts +34 -0
- package/dist/server/fn.d.ts.map +1 -0
- package/dist/server/fn.js +170 -0
- package/dist/server/fn.js.map +1 -0
- package/dist/server/http.d.ts +79 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +459 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/idem.d.ts +31 -0
- package/dist/server/idem.d.ts.map +1 -0
- package/dist/server/idem.js +68 -0
- package/dist/server/idem.js.map +1 -0
- package/dist/server/jobs.d.ts +5 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +24 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/multipart.d.ts +18 -0
- package/dist/server/multipart.d.ts.map +1 -0
- package/dist/server/multipart.js +183 -0
- package/dist/server/multipart.js.map +1 -0
- package/dist/server/project.d.ts +9 -0
- package/dist/server/project.d.ts.map +1 -0
- package/dist/server/project.js +67 -0
- package/dist/server/project.js.map +1 -0
- package/dist/server/queue.d.ts +80 -0
- package/dist/server/queue.d.ts.map +1 -0
- package/dist/server/queue.js +494 -0
- package/dist/server/queue.js.map +1 -0
- package/dist/server/respond.d.ts +91 -0
- package/dist/server/respond.d.ts.map +1 -0
- package/dist/server/respond.js +99 -0
- package/dist/server/respond.js.map +1 -0
- package/dist/server/runtime.d.ts +14 -0
- package/dist/server/runtime.d.ts.map +1 -0
- package/dist/server/runtime.js +25 -0
- package/dist/server/runtime.js.map +1 -0
- package/dist/server/session.d.ts +55 -0
- package/dist/server/session.d.ts.map +1 -0
- package/dist/server/session.js +172 -0
- package/dist/server/session.js.map +1 -0
- package/dist/server/sessionstore.d.ts +54 -0
- package/dist/server/sessionstore.d.ts.map +1 -0
- package/dist/server/sessionstore.js +232 -0
- package/dist/server/sessionstore.js.map +1 -0
- package/dist/server/storage/index.d.ts +11 -0
- package/dist/server/storage/index.d.ts.map +1 -0
- package/dist/server/storage/index.js +31 -0
- package/dist/server/storage/index.js.map +1 -0
- package/dist/server/storage/local.d.ts +5 -0
- package/dist/server/storage/local.d.ts.map +1 -0
- package/dist/server/storage/local.js +165 -0
- package/dist/server/storage/local.js.map +1 -0
- package/dist/server/storage/s3.d.ts +11 -0
- package/dist/server/storage/s3.d.ts.map +1 -0
- package/dist/server/storage/s3.js +133 -0
- package/dist/server/storage/s3.js.map +1 -0
- package/dist/server/storage/sigv4.d.ts +23 -0
- package/dist/server/storage/sigv4.d.ts.map +1 -0
- package/dist/server/storage/sigv4.js +79 -0
- package/dist/server/storage/sigv4.js.map +1 -0
- package/dist/server/storage/types.d.ts +37 -0
- package/dist/server/storage/types.d.ts.map +1 -0
- package/dist/server/storage/types.js +82 -0
- package/dist/server/storage/types.js.map +1 -0
- package/dist/server/stream.d.ts +43 -0
- package/dist/server/stream.d.ts.map +1 -0
- package/dist/server/stream.js +117 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/server/streamstore.d.ts +29 -0
- package/dist/server/streamstore.d.ts.map +1 -0
- package/dist/server/streamstore.js +140 -0
- package/dist/server/streamstore.js.map +1 -0
- package/dist/server/tenants.d.ts +108 -0
- package/dist/server/tenants.d.ts.map +1 -0
- package/dist/server/tenants.js +172 -0
- package/dist/server/tenants.js.map +1 -0
- package/dist/server/transport/index.d.ts +5 -0
- package/dist/server/transport/index.d.ts.map +1 -0
- package/dist/server/transport/index.js +3 -0
- package/dist/server/transport/index.js.map +1 -0
- package/dist/server/transport/memory.d.ts +14 -0
- package/dist/server/transport/memory.d.ts.map +1 -0
- package/dist/server/transport/memory.js +36 -0
- package/dist/server/transport/memory.js.map +1 -0
- package/dist/server/transport/types.d.ts +44 -0
- package/dist/server/transport/types.d.ts.map +1 -0
- package/dist/server/transport/types.js +79 -0
- package/dist/server/transport/types.js.map +1 -0
- package/dist/server/worker.d.ts +33 -0
- package/dist/server/worker.d.ts.map +1 -0
- package/dist/server/worker.js +428 -0
- package/dist/server/worker.js.map +1 -0
- package/dist/testing.d.ts +146 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +406 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme/contracts.d.ts +4 -0
- package/dist/theme/contracts.d.ts.map +1 -0
- package/dist/theme/contracts.js +87 -0
- package/dist/theme/contracts.js.map +1 -0
- package/dist/theme/index.d.ts +8 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +7 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/joints.d.ts +19 -0
- package/dist/theme/joints.d.ts.map +1 -0
- package/dist/theme/joints.js +126 -0
- package/dist/theme/joints.js.map +1 -0
- package/dist/theme/ktl/compile.d.ts +26 -0
- package/dist/theme/ktl/compile.d.ts.map +1 -0
- package/dist/theme/ktl/compile.js +226 -0
- package/dist/theme/ktl/compile.js.map +1 -0
- package/dist/theme/ktl/lexer.d.ts +20 -0
- package/dist/theme/ktl/lexer.d.ts.map +1 -0
- package/dist/theme/ktl/lexer.js +56 -0
- package/dist/theme/ktl/lexer.js.map +1 -0
- package/dist/theme/ktl/parser.d.ts +64 -0
- package/dist/theme/ktl/parser.d.ts.map +1 -0
- package/dist/theme/ktl/parser.js +220 -0
- package/dist/theme/ktl/parser.js.map +1 -0
- package/dist/theme/render.d.ts +19 -0
- package/dist/theme/render.d.ts.map +1 -0
- package/dist/theme/render.js +246 -0
- package/dist/theme/render.js.map +1 -0
- package/dist/theme/templates.d.ts +2 -0
- package/dist/theme/templates.d.ts.map +1 -0
- package/dist/theme/templates.js +40 -0
- package/dist/theme/templates.js.map +1 -0
- package/dist/theme/tokens.d.ts +4 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/theme/tokens.js +15 -0
- package/dist/theme/tokens.js.map +1 -0
- package/dist/theme/viewmodel.d.ts +6 -0
- package/dist/theme/viewmodel.d.ts.map +1 -0
- package/dist/theme/viewmodel.js +40 -0
- package/dist/theme/viewmodel.js.map +1 -0
- package/dist/types.d.ts +614 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The CLI is deliberately thin: everything it prints is read off the manifest,
|
|
3
|
+
// so there is no second source of truth about what an app contains.
|
|
4
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
5
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
6
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
7
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return path;
|
|
11
|
+
};
|
|
12
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { delimiter, extname, join, normalize, resolve } from 'node:path';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { composeWorkspace, explainWorkspace } from './kernel/workspace.js';
|
|
17
|
+
import { resolveWorkspace } from './kernel/modules.js';
|
|
18
|
+
import { diffManifests, formatDiff } from './kernel/diff.js';
|
|
19
|
+
import { generateDts } from './codegen/dts.js';
|
|
20
|
+
import { agentDescriptor } from './agent/capabilities.js';
|
|
21
|
+
import { reachOf, functionsOf, formatReach, formatInventory, grantsOfRole } from './agent/permissions.js';
|
|
22
|
+
import { readConfig, sqliteStore } from './server/config.js';
|
|
23
|
+
import { schemaFromManifest, planMigration, renderSql } from './data/migrate.js';
|
|
24
|
+
import { migrateOne, migrateFleet, formatFleet } from './data/fleet.js';
|
|
25
|
+
import { createAdapterPool } from './data/pool.js';
|
|
26
|
+
import { sqliteAdapter } from './data/sqlite.js';
|
|
27
|
+
import { bootApp, serveApp } from './server/boot.js';
|
|
28
|
+
import { bootRuntime } from './server/runtime.js';
|
|
29
|
+
import { callFn } from './server/fn.js';
|
|
30
|
+
import { createAppRegistry, restrictManifest } from './kernel/apps.js';
|
|
31
|
+
import { bootWorker, serveWorker } from './server/worker.js';
|
|
32
|
+
import { createQueue } from './server/queue.js';
|
|
33
|
+
import { scaffold } from './scaffold/index.js';
|
|
34
|
+
import { KetError } from './kernel/errors.js';
|
|
35
|
+
import { CookieJar, createTestApp, TestClient, TestHttpError } from './testing.js';
|
|
36
|
+
const [, , cmd = 'help', ...rest] = process.argv;
|
|
37
|
+
const flag = (name) => rest.includes(`--${name}`);
|
|
38
|
+
const opt = (name) => {
|
|
39
|
+
const i = rest.indexOf(`--${name}`);
|
|
40
|
+
if (i >= 0)
|
|
41
|
+
return rest[i + 1];
|
|
42
|
+
const inline = rest.find((item) => item.startsWith(`--${name}=`));
|
|
43
|
+
return inline?.slice(name.length + 3);
|
|
44
|
+
};
|
|
45
|
+
const opts = (name) => {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (let i = 0; i < rest.length; i++) {
|
|
48
|
+
if (rest[i] === `--${name}` && rest[i + 1] !== undefined)
|
|
49
|
+
out.push(rest[++i]);
|
|
50
|
+
else if (rest[i]?.startsWith(`--${name}=`))
|
|
51
|
+
out.push(rest[i].slice(name.length + 3));
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
};
|
|
55
|
+
const positionals = (valueOptions) => {
|
|
56
|
+
const values = new Set(valueOptions);
|
|
57
|
+
const end = rest.indexOf('--');
|
|
58
|
+
const args = end < 0 ? rest : rest.slice(0, end);
|
|
59
|
+
const out = [];
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const item = args[i];
|
|
62
|
+
if (!item.startsWith('--')) {
|
|
63
|
+
out.push(item);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const name = item.slice(2).split('=')[0];
|
|
67
|
+
if (!item.includes('=') && values.has(name))
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
};
|
|
72
|
+
const CANDIDATES = [
|
|
73
|
+
'dist/ket.workspace.js',
|
|
74
|
+
'.build/ket.workspace.js',
|
|
75
|
+
'ket.workspace.js',
|
|
76
|
+
'workspace.js',
|
|
77
|
+
'examples/workspace.js',
|
|
78
|
+
];
|
|
79
|
+
/** Where the apps are declared. Named explicitly, or the first conventional path. */
|
|
80
|
+
const workspacePath = () => {
|
|
81
|
+
const given = opt('workspace');
|
|
82
|
+
if (given) {
|
|
83
|
+
if (/\.[cm]?tsx?$/.test(given) && process.env.KET_DEV_SOURCE !== '1') {
|
|
84
|
+
throw new Error(`refusing to execute source workspace "${given}" — build it and pass the emitted .js artifact`);
|
|
85
|
+
}
|
|
86
|
+
return given;
|
|
87
|
+
}
|
|
88
|
+
const found = CANDIDATES.find((p) => existsSync(p));
|
|
89
|
+
if (!found)
|
|
90
|
+
throw new Error(`no workspace file found (looked for ${CANDIDATES.join(', ')}); pass --workspace FILE`);
|
|
91
|
+
return found;
|
|
92
|
+
};
|
|
93
|
+
const loadWorkspace = async () => {
|
|
94
|
+
const configuredPath = workspacePath();
|
|
95
|
+
const absolutePath = resolve(configuredPath);
|
|
96
|
+
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(absolutePath).href)));
|
|
97
|
+
const declaration = mod.default && typeof mod.default === 'object' && Array.isArray(mod.default.apps)
|
|
98
|
+
? mod.default
|
|
99
|
+
: { apps: mod.apps, modulePaths: mod.modulePaths };
|
|
100
|
+
if (!Array.isArray(declaration.apps))
|
|
101
|
+
throw new Error(`${configuredPath} must default-export defineWorkspace(...) or export \`apps\`, an array of defineApp(...)`);
|
|
102
|
+
const fromEnv = (process.env.KET_MODULE_PATH ?? '')
|
|
103
|
+
.split(delimiter)
|
|
104
|
+
.map((path) => path.trim())
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
const resolved = await resolveWorkspace(declaration, {
|
|
107
|
+
baseUrl: pathToFileURL(absolutePath),
|
|
108
|
+
extraModulePaths: [...fromEnv, ...opts('module-path')],
|
|
109
|
+
allowSource: process.env.KET_DEV_SOURCE === '1',
|
|
110
|
+
});
|
|
111
|
+
return { ws: composeWorkspace(resolved.apps), apps: resolved.apps, resolved };
|
|
112
|
+
};
|
|
113
|
+
const formatModules = (paths, modules) => {
|
|
114
|
+
const lines = ['module paths:'];
|
|
115
|
+
for (const path of paths)
|
|
116
|
+
lines.push(` ${path}`);
|
|
117
|
+
if (!paths.length)
|
|
118
|
+
lines.push(' (none)');
|
|
119
|
+
lines.push('modules:');
|
|
120
|
+
for (const module of modules) {
|
|
121
|
+
lines.push(` ${module.name.padEnd(24)} ${module.version.padEnd(10)} ${module.kind.padEnd(6)} apps=${module.apps.join(',')} ${module.source}`);
|
|
122
|
+
}
|
|
123
|
+
if (!modules.length)
|
|
124
|
+
lines.push(' (none)');
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
};
|
|
127
|
+
/** The AppSpec, not the composed manifest — serving needs what the app declared. */
|
|
128
|
+
const pickSpec = (specs) => {
|
|
129
|
+
const name = opt('app');
|
|
130
|
+
if (!name) {
|
|
131
|
+
const servable = specs.filter((s) => s.serve);
|
|
132
|
+
const one = servable[0] ?? specs[0];
|
|
133
|
+
if (!one)
|
|
134
|
+
throw new Error('the workspace declares no apps');
|
|
135
|
+
return one;
|
|
136
|
+
}
|
|
137
|
+
const found = specs.find((s) => s.name === name);
|
|
138
|
+
if (!found)
|
|
139
|
+
throw new Error(`unknown app "${name}" (have: ${specs.map((s) => s.name).join(', ')})`);
|
|
140
|
+
return found;
|
|
141
|
+
};
|
|
142
|
+
const pickApp = (ws) => {
|
|
143
|
+
const name = opt('app') ?? Object.keys(ws.apps)[0];
|
|
144
|
+
const m = ws.apps[name];
|
|
145
|
+
if (!m)
|
|
146
|
+
throw new Error(`unknown app "${name}" (have: ${Object.keys(ws.apps).join(', ')})`);
|
|
147
|
+
return [name, m];
|
|
148
|
+
};
|
|
149
|
+
const HELP = `ket — zero-dependency fullstack framework
|
|
150
|
+
|
|
151
|
+
ket check compose every app and report contract violations
|
|
152
|
+
ket manifest [--app X] print the composed manifest
|
|
153
|
+
ket workspace show apps, datastores and shared modules
|
|
154
|
+
ket modules show resolved modules and their source paths
|
|
155
|
+
ket types [--app X] generate .ket/types.d.ts from the manifest
|
|
156
|
+
ket agent [--app X] print the agent capability descriptor
|
|
157
|
+
ket permissions every function that exists to be granted
|
|
158
|
+
--grant a,b,c …or what a role granted exactly these can reach
|
|
159
|
+
--module NAME …or what granting one module's whole surface reaches
|
|
160
|
+
--role NAME …or what a role in the database actually grants
|
|
161
|
+
ket migrate [--app X] plan migrations (add --allow-destructive to permit data loss)
|
|
162
|
+
--all …or apply them to every tenant database (add --dry-run)
|
|
163
|
+
ket diff --against FILE compare the current manifest with a stored one
|
|
164
|
+
ket snapshot [--app X] write .ket/manifest.<app>.json for a later diff
|
|
165
|
+
|
|
166
|
+
ket serve [--app X] boot the app and serve it
|
|
167
|
+
ket worker [--app X] run declared queues for the same app and manifest
|
|
168
|
+
ket call FUNCTION exercise a function through its real HTTP endpoint
|
|
169
|
+
--against URL call an already-running development server
|
|
170
|
+
--input JSON|@FILE|- function input (default: {})
|
|
171
|
+
--company ID request company; also --companies and --branches
|
|
172
|
+
--tenant KEY conventional x-tenant header (or use --header)
|
|
173
|
+
--cookie-file FILE load and update an isolated JSON cookie jar
|
|
174
|
+
--login JSON|@FILE POST credentials before the call (default: /login)
|
|
175
|
+
--login-path PATH override the application's login route
|
|
176
|
+
--dry-run report declared writes without applying them
|
|
177
|
+
--idempotency-key KEY exercise idempotent retry handling
|
|
178
|
+
--isolated boot a temporary app/database for this call
|
|
179
|
+
ket provision FUNCTION run a declared one-shot bootstrap function
|
|
180
|
+
--input - read JSON, including secrets, from stdin only
|
|
181
|
+
--tenant KEY required when the app uses tenant databases
|
|
182
|
+
ket test [FILES...] run emitted headless tests with Node's test runner
|
|
183
|
+
--watch rerun when the selected JavaScript artifacts change
|
|
184
|
+
--test-name-pattern P select tests by name; --coverage enables coverage
|
|
185
|
+
ket jobs list inspect durable jobs (--tenant is required for tenant apps)
|
|
186
|
+
ket jobs retry ID make a retryable/discarded job available now
|
|
187
|
+
ket jobs cancel ID cancel a pending or executing job
|
|
188
|
+
ket jobs prune apply the 7/30-day retention policy
|
|
189
|
+
ket dev [--app X] serve compiled output, restarting when an artifact changes
|
|
190
|
+
--all run HTTP and worker together under this one watcher
|
|
191
|
+
(--no-auto-install holds back modules that install themselves)
|
|
192
|
+
ket new NAME [--dir D] scaffold an app that runs
|
|
193
|
+
|
|
194
|
+
Options: --workspace FILE (default: dist/ket.workspace.js, ket.workspace.js, workspace.js)
|
|
195
|
+
--module-path DIR (repeatable; KET_MODULE_PATH uses the platform path separator)
|
|
196
|
+
--app NAME, --port N, --header "Name: value" (repeatable)
|
|
197
|
+
`;
|
|
198
|
+
const TEST_VALUE_OPTIONS = ['test-name-pattern', 'test-concurrency', 'reporter', 'out-dir'];
|
|
199
|
+
const collectTests = (path) => {
|
|
200
|
+
if (!existsSync(path))
|
|
201
|
+
throw new Error(`test path does not exist: ${path}`);
|
|
202
|
+
if (!statSync(path).isDirectory())
|
|
203
|
+
return [path];
|
|
204
|
+
const files = [];
|
|
205
|
+
const visit = (dir) => {
|
|
206
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
207
|
+
const child = join(dir, entry.name);
|
|
208
|
+
if (entry.isDirectory())
|
|
209
|
+
visit(child);
|
|
210
|
+
else if (/\.(?:test|spec)\.[cm]?js$/.test(entry.name))
|
|
211
|
+
files.push(child);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
visit(path);
|
|
215
|
+
return files.sort();
|
|
216
|
+
};
|
|
217
|
+
const emittedTestPath = (input) => {
|
|
218
|
+
const source = normalize(input).replace(/^\.\//, '');
|
|
219
|
+
const extension = extname(source);
|
|
220
|
+
if (extension !== '.ts' && extension !== '.tsx')
|
|
221
|
+
return source;
|
|
222
|
+
const emitted = source.slice(0, -extension.length) + '.js';
|
|
223
|
+
const candidates = [...(opt('out-dir') ? [opt('out-dir')] : []), '.build', 'dist'].map((dir) => join(dir, emitted));
|
|
224
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
225
|
+
if (!found)
|
|
226
|
+
throw new Error(`no emitted test for ${input}; build first (looked for ${candidates.join(', ')})`);
|
|
227
|
+
return found;
|
|
228
|
+
};
|
|
229
|
+
const runTests = async () => {
|
|
230
|
+
const selected = positionals(TEST_VALUE_OPTIONS).flatMap((path) => collectTests(emittedTestPath(path)));
|
|
231
|
+
const defaultDir = [opt('out-dir'), '.build/test', 'dist/test'].find((candidate) => Boolean(candidate && existsSync(candidate)));
|
|
232
|
+
const defaults = selected.length ? selected : defaultDir ? collectTests(defaultDir) : [];
|
|
233
|
+
const nodeArgs = ['--test'];
|
|
234
|
+
if (flag('watch'))
|
|
235
|
+
nodeArgs.push('--watch');
|
|
236
|
+
if (flag('coverage'))
|
|
237
|
+
nodeArgs.push('--experimental-test-coverage');
|
|
238
|
+
if (flag('test-only'))
|
|
239
|
+
nodeArgs.push('--test-only');
|
|
240
|
+
for (const [option, nodeOption] of [
|
|
241
|
+
['test-name-pattern', '--test-name-pattern'],
|
|
242
|
+
['test-concurrency', '--test-concurrency'],
|
|
243
|
+
['reporter', '--test-reporter'],
|
|
244
|
+
]) {
|
|
245
|
+
const value = opt(option);
|
|
246
|
+
if (value)
|
|
247
|
+
nodeArgs.push(`${nodeOption}=${value}`);
|
|
248
|
+
}
|
|
249
|
+
const separator = rest.indexOf('--');
|
|
250
|
+
if (separator >= 0)
|
|
251
|
+
nodeArgs.push(...rest.slice(separator + 1));
|
|
252
|
+
nodeArgs.push(...defaults);
|
|
253
|
+
const child = spawn(process.execPath, nodeArgs, { stdio: 'inherit', env: process.env });
|
|
254
|
+
const code = await new Promise((resolve, reject) => {
|
|
255
|
+
child.on('error', reject);
|
|
256
|
+
child.on('exit', (status, signal) => resolve(status ?? (signal ? 1 : 0)));
|
|
257
|
+
});
|
|
258
|
+
process.exit(code);
|
|
259
|
+
};
|
|
260
|
+
const callHeaders = () => {
|
|
261
|
+
const headers = new Headers();
|
|
262
|
+
for (const raw of opts('header')) {
|
|
263
|
+
const separator = raw.indexOf(':');
|
|
264
|
+
if (separator <= 0)
|
|
265
|
+
throw new Error(`invalid --header "${raw}" (expected "Name: value")`);
|
|
266
|
+
headers.append(raw.slice(0, separator).trim(), raw.slice(separator + 1).trim());
|
|
267
|
+
}
|
|
268
|
+
const cookie = opt('cookie');
|
|
269
|
+
if (cookie)
|
|
270
|
+
headers.set('cookie', cookie);
|
|
271
|
+
return headers;
|
|
272
|
+
};
|
|
273
|
+
const jsonObject = (value, label) => {
|
|
274
|
+
const raw = value === undefined
|
|
275
|
+
? '{}'
|
|
276
|
+
: value === '-'
|
|
277
|
+
? readFileSync(0, 'utf8')
|
|
278
|
+
: value.startsWith('@')
|
|
279
|
+
? readFileSync(value.slice(1), 'utf8')
|
|
280
|
+
: value;
|
|
281
|
+
const parsed = JSON.parse(raw);
|
|
282
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
283
|
+
throw new Error(`${label} must be a JSON object`);
|
|
284
|
+
return parsed;
|
|
285
|
+
};
|
|
286
|
+
const callInput = () => jsonObject(opt('input'), '--input');
|
|
287
|
+
const callWithClient = async (baseUrl) => {
|
|
288
|
+
const fnKey = positionals([
|
|
289
|
+
'against',
|
|
290
|
+
'input',
|
|
291
|
+
'company',
|
|
292
|
+
'companies',
|
|
293
|
+
'branches',
|
|
294
|
+
'tenant',
|
|
295
|
+
'tenant-header',
|
|
296
|
+
'locale',
|
|
297
|
+
'cookie',
|
|
298
|
+
'cookie-file',
|
|
299
|
+
'login',
|
|
300
|
+
'login-path',
|
|
301
|
+
'idempotency-key',
|
|
302
|
+
'header',
|
|
303
|
+
'app',
|
|
304
|
+
'workspace',
|
|
305
|
+
'module-path',
|
|
306
|
+
])[0];
|
|
307
|
+
if (!fnKey)
|
|
308
|
+
throw new Error('usage: ket call FUNCTION [--against URL] [--input JSON|@FILE|-]');
|
|
309
|
+
const cookieFile = opt('cookie-file');
|
|
310
|
+
const jar = cookieFile ? await CookieJar.load(cookieFile) : new CookieJar();
|
|
311
|
+
const list = (name) => opt(name)
|
|
312
|
+
?.split(',')
|
|
313
|
+
.map((item) => item.trim())
|
|
314
|
+
.filter(Boolean);
|
|
315
|
+
const client = new TestClient(baseUrl, {
|
|
316
|
+
jar,
|
|
317
|
+
company: opt('company'),
|
|
318
|
+
companies: list('companies'),
|
|
319
|
+
branches: list('branches'),
|
|
320
|
+
tenant: opt('tenant'),
|
|
321
|
+
tenantHeader: opt('tenant-header'),
|
|
322
|
+
locale: opt('locale'),
|
|
323
|
+
headers: callHeaders(),
|
|
324
|
+
});
|
|
325
|
+
try {
|
|
326
|
+
if (opt('login'))
|
|
327
|
+
await client.login(jsonObject(opt('login'), '--login'), opt('login-path') ?? '/login');
|
|
328
|
+
const result = await client.call(fnKey, callInput(), {
|
|
329
|
+
dryRun: flag('dry-run'),
|
|
330
|
+
...(opt('idempotency-key') ? { idempotencyKey: opt('idempotency-key') } : {}),
|
|
331
|
+
});
|
|
332
|
+
if (cookieFile)
|
|
333
|
+
await jar.save(cookieFile);
|
|
334
|
+
const output = flag('value') ? result.value : result;
|
|
335
|
+
console.log(JSON.stringify(output, null, flag('compact') ? 0 : 2));
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (cookieFile)
|
|
340
|
+
await jar.save(cookieFile);
|
|
341
|
+
if (!(error instanceof TestHttpError))
|
|
342
|
+
throw error;
|
|
343
|
+
console.error(JSON.stringify(error.body, null, 2));
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
try {
|
|
348
|
+
if (cmd === 'help' || cmd === '--help') {
|
|
349
|
+
console.log(HELP);
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
352
|
+
if (cmd === 'new') {
|
|
353
|
+
const name = rest.find((a) => !a.startsWith('--'));
|
|
354
|
+
if (!name)
|
|
355
|
+
throw new Error('usage: ket new NAME [--dir DIR]');
|
|
356
|
+
for (const line of scaffold(name, opt('dir') ?? name))
|
|
357
|
+
console.log(line);
|
|
358
|
+
process.exit(0);
|
|
359
|
+
}
|
|
360
|
+
if (cmd === 'test')
|
|
361
|
+
await runTests();
|
|
362
|
+
// Remote smoke calls need no workspace and do not boot a second app.
|
|
363
|
+
if (cmd === 'call' && opt('against')) {
|
|
364
|
+
const against = new URL(opt('against'));
|
|
365
|
+
if (against.protocol !== 'http:' && against.protocol !== 'https:')
|
|
366
|
+
throw new Error('--against must be an http(s) URL');
|
|
367
|
+
process.exit(await callWithClient(against.toString()));
|
|
368
|
+
}
|
|
369
|
+
if (cmd === 'dev') {
|
|
370
|
+
// This watches emitted JavaScript only. The project owns the compiler watcher;
|
|
371
|
+
// `ket new` wires both sides together without handing source files to Node.
|
|
372
|
+
const childCommand = flag('all') ? 'all' : 'serve';
|
|
373
|
+
const argv = [
|
|
374
|
+
'--watch',
|
|
375
|
+
new URL(import.meta.url).pathname,
|
|
376
|
+
childCommand,
|
|
377
|
+
...rest.filter((item) => item !== '--all'),
|
|
378
|
+
];
|
|
379
|
+
const child = spawn(process.execPath, argv, { stdio: 'inherit', env: { ...process.env, KET_DEV: '1' } });
|
|
380
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
381
|
+
for (const sig of ['SIGINT', 'SIGTERM'])
|
|
382
|
+
process.on(sig, () => child.kill(sig));
|
|
383
|
+
await new Promise(() => { });
|
|
384
|
+
}
|
|
385
|
+
const { ws, apps: specs, resolved } = await loadWorkspace();
|
|
386
|
+
mkdirSync('.ket', { recursive: true });
|
|
387
|
+
if (cmd === 'serve') {
|
|
388
|
+
const spec = pickSpec(specs);
|
|
389
|
+
if (!spec.serve)
|
|
390
|
+
throw new Error(`app "${spec.name}" declares no serve block, so there is nothing to run`);
|
|
391
|
+
const port = opt('port');
|
|
392
|
+
const env = flag('no-auto-install') ? { ...process.env, KET_AUTO_INSTALL: '0' } : process.env;
|
|
393
|
+
await serveApp(spec, { env, ...(port ? { port: Number(port) } : {}) });
|
|
394
|
+
}
|
|
395
|
+
else if (cmd === 'worker') {
|
|
396
|
+
const spec = pickSpec(specs);
|
|
397
|
+
const worker = await serveWorker(spec, { env: process.env });
|
|
398
|
+
console.log(`worker ${worker.workerId} is running (${Object.keys(spec.worker?.queues ?? {}).join(', ')})`);
|
|
399
|
+
}
|
|
400
|
+
else if (cmd === 'call') {
|
|
401
|
+
const spec = pickSpec(specs);
|
|
402
|
+
if (!spec.serve)
|
|
403
|
+
throw new Error(`app "${spec.name}" declares no serve block`);
|
|
404
|
+
if (flag('isolated')) {
|
|
405
|
+
const testApp = await createTestApp(spec, { worker: false });
|
|
406
|
+
try {
|
|
407
|
+
process.exitCode = await callWithClient(testApp.baseUrl);
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
await testApp.close();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const env = {
|
|
415
|
+
...process.env,
|
|
416
|
+
...(flag('no-migrate') ? { KET_MIGRATE: '0' } : {}),
|
|
417
|
+
...(flag('no-auto-install') ? { KET_AUTO_INSTALL: '0' } : {}),
|
|
418
|
+
};
|
|
419
|
+
const app = await bootApp(spec, {
|
|
420
|
+
env,
|
|
421
|
+
port: 0,
|
|
422
|
+
log: flag('verbose') ? (line) => console.error(line) : () => { },
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
process.exitCode = await callWithClient(`http://127.0.0.1:${app.port}`);
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
await app.close();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else if (cmd === 'provision') {
|
|
433
|
+
const fnKey = positionals(['input', 'tenant', 'app', 'workspace', 'module-path'])[0];
|
|
434
|
+
if (!fnKey)
|
|
435
|
+
throw new Error('usage: ket provision FUNCTION --input - [--tenant KEY]');
|
|
436
|
+
if (opt('input') !== '-')
|
|
437
|
+
throw new Error('ket provision reads input only from stdin; pass --input - so secrets never enter argv');
|
|
438
|
+
const spec = pickSpec(specs);
|
|
439
|
+
if (!spec.serve)
|
|
440
|
+
throw new Error(`app "${spec.name}" declares no serve block`);
|
|
441
|
+
const runtime = await bootRuntime(spec, { env: process.env });
|
|
442
|
+
const meta = runtime.manifest.functions[fnKey];
|
|
443
|
+
if (!meta)
|
|
444
|
+
throw new Error(`unknown function "${fnKey}"`);
|
|
445
|
+
if (!meta.provision || meta.exposure !== 'internal')
|
|
446
|
+
throw new Error(`function "${fnKey}" is not declared internal + provision`);
|
|
447
|
+
const tenant = opt('tenant');
|
|
448
|
+
if (spec.serve.tenants && !tenant)
|
|
449
|
+
throw new Error(`app "${spec.name}" has tenant databases; pass --tenant NAME`);
|
|
450
|
+
const adapter = spec.serve.tenants
|
|
451
|
+
? await spec.serve.tenants.open(tenant, runtime.config)
|
|
452
|
+
: await (spec.serve.openStore ?? sqliteStore)(runtime.config);
|
|
453
|
+
if (spec.serve.tenants)
|
|
454
|
+
await adapter.open();
|
|
455
|
+
try {
|
|
456
|
+
await migrateOne(adapter, runtime.manifest);
|
|
457
|
+
const apps = await createAppRegistry(runtime.manifest, adapter, {
|
|
458
|
+
autoInstall: runtime.config.autoInstall,
|
|
459
|
+
});
|
|
460
|
+
const bootstrap = runtime.config.bootstrapApps ?? spec.serve.bootstrap ?? [];
|
|
461
|
+
if ((await apps.enabled()).size === 0)
|
|
462
|
+
for (const name of bootstrap)
|
|
463
|
+
await apps.install(name);
|
|
464
|
+
const live = restrictManifest(runtime.manifest, await apps.enabled());
|
|
465
|
+
const result = await callFn(fnKey, jsonObject('-', '--input'), {
|
|
466
|
+
adapter,
|
|
467
|
+
manifest: live,
|
|
468
|
+
actor: 'system:provision',
|
|
469
|
+
scope: { company: null, branch: null },
|
|
470
|
+
});
|
|
471
|
+
console.log(JSON.stringify(result.value, null, 2));
|
|
472
|
+
if (result.value &&
|
|
473
|
+
typeof result.value === 'object' &&
|
|
474
|
+
'ok' in result.value &&
|
|
475
|
+
result.value.ok === false)
|
|
476
|
+
process.exitCode = 1;
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
await adapter.close();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (cmd === 'all') {
|
|
483
|
+
const spec = pickSpec(specs);
|
|
484
|
+
if (!spec.serve)
|
|
485
|
+
throw new Error(`app "${spec.name}" declares no serve block`);
|
|
486
|
+
if (!spec.worker)
|
|
487
|
+
throw new Error(`app "${spec.name}" declares no worker block`);
|
|
488
|
+
const app = await bootApp(spec, {
|
|
489
|
+
env: process.env,
|
|
490
|
+
...(opt('port') ? { port: Number(opt('port')) } : {}),
|
|
491
|
+
});
|
|
492
|
+
const worker = await bootWorker(spec, { env: process.env });
|
|
493
|
+
worker.start();
|
|
494
|
+
console.log(await app.banner());
|
|
495
|
+
console.log(` worker ${worker.workerId} is running in this development process\n`);
|
|
496
|
+
const close = async () => {
|
|
497
|
+
await worker.close();
|
|
498
|
+
await app.close();
|
|
499
|
+
process.exit(0);
|
|
500
|
+
};
|
|
501
|
+
for (const signal of ['SIGINT', 'SIGTERM'])
|
|
502
|
+
process.on(signal, () => void close());
|
|
503
|
+
await new Promise(() => { });
|
|
504
|
+
}
|
|
505
|
+
else if (cmd === 'jobs') {
|
|
506
|
+
const action = rest.find((item) => !item.startsWith('--'));
|
|
507
|
+
const positional = rest.filter((item, index) => {
|
|
508
|
+
if (item.startsWith('--'))
|
|
509
|
+
return false;
|
|
510
|
+
const previous = rest[index - 1];
|
|
511
|
+
return !previous?.startsWith('--');
|
|
512
|
+
});
|
|
513
|
+
const id = positional[1];
|
|
514
|
+
const spec = pickSpec(specs);
|
|
515
|
+
const config = readConfig(process.env, {
|
|
516
|
+
sqliteFile: `.ket/${spec.name}.db`,
|
|
517
|
+
...spec.serve?.defaults,
|
|
518
|
+
});
|
|
519
|
+
const tenant = opt('tenant');
|
|
520
|
+
if (spec.serve?.tenants && !tenant)
|
|
521
|
+
throw new Error(`app "${spec.name}" has tenant databases; pass --tenant NAME`);
|
|
522
|
+
const adapter = spec.serve?.tenants
|
|
523
|
+
? await spec.serve.tenants.open(tenant, config)
|
|
524
|
+
: await (spec.serve?.openStore ?? sqliteStore)(config);
|
|
525
|
+
if (spec.serve?.tenants)
|
|
526
|
+
await adapter.open();
|
|
527
|
+
try {
|
|
528
|
+
const queue = await createQueue(adapter);
|
|
529
|
+
if (action === 'list') {
|
|
530
|
+
const state = opt('state');
|
|
531
|
+
if (state &&
|
|
532
|
+
![
|
|
533
|
+
'available',
|
|
534
|
+
'scheduled',
|
|
535
|
+
'executing',
|
|
536
|
+
'retryable',
|
|
537
|
+
'completed',
|
|
538
|
+
'discarded',
|
|
539
|
+
'cancelled',
|
|
540
|
+
].includes(state))
|
|
541
|
+
throw new Error(`unknown job state "${state}"`);
|
|
542
|
+
const limit = opt('limit') === undefined ? undefined : Number(opt('limit'));
|
|
543
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1))
|
|
544
|
+
throw new Error('--limit must be a positive integer');
|
|
545
|
+
const rows = await queue.list({
|
|
546
|
+
...(state ? { state } : {}),
|
|
547
|
+
...(opt('queue') ? { queue: opt('queue') } : {}),
|
|
548
|
+
...(limit === undefined ? {} : { limit }),
|
|
549
|
+
});
|
|
550
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
551
|
+
}
|
|
552
|
+
else if (action === 'retry') {
|
|
553
|
+
if (!id)
|
|
554
|
+
throw new Error('usage: ket jobs retry ID [--tenant NAME]');
|
|
555
|
+
if (!(await queue.retryNow(id)))
|
|
556
|
+
throw new Error(`job "${id}" is not retryable or discarded`);
|
|
557
|
+
console.log(`job ${id} is available`);
|
|
558
|
+
}
|
|
559
|
+
else if (action === 'cancel') {
|
|
560
|
+
if (!id)
|
|
561
|
+
throw new Error('usage: ket jobs cancel ID [--tenant NAME]');
|
|
562
|
+
if (!(await queue.cancel(id)))
|
|
563
|
+
throw new Error(`job "${id}" cannot be cancelled`);
|
|
564
|
+
console.log(`job ${id} is cancelled`);
|
|
565
|
+
}
|
|
566
|
+
else if (action === 'prune') {
|
|
567
|
+
console.log(`pruned ${await queue.prune()} job(s)`);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
throw new Error('usage: ket jobs list|retry ID|cancel ID|prune [--tenant NAME]');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
finally {
|
|
574
|
+
await adapter.close();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else if (cmd === 'check') {
|
|
578
|
+
console.log(explainWorkspace(ws));
|
|
579
|
+
console.log('\nall contracts hold');
|
|
580
|
+
}
|
|
581
|
+
else if (cmd === 'workspace') {
|
|
582
|
+
console.log(explainWorkspace(ws));
|
|
583
|
+
}
|
|
584
|
+
else if (cmd === 'modules') {
|
|
585
|
+
console.log(formatModules(resolved.modulePaths, resolved.modules));
|
|
586
|
+
}
|
|
587
|
+
else if (cmd === 'manifest') {
|
|
588
|
+
const [, m] = pickApp(ws);
|
|
589
|
+
console.log(JSON.stringify(m, null, 2));
|
|
590
|
+
}
|
|
591
|
+
else if (cmd === 'types') {
|
|
592
|
+
const [name, m] = pickApp(ws);
|
|
593
|
+
const out = `.ket/types.${name}.d.ts`;
|
|
594
|
+
writeFileSync(out, generateDts(m));
|
|
595
|
+
console.log(`wrote ${out}`);
|
|
596
|
+
}
|
|
597
|
+
else if (cmd === 'permissions') {
|
|
598
|
+
const [, m] = pickApp(ws);
|
|
599
|
+
const module = opt('module');
|
|
600
|
+
const grant = opt('grant');
|
|
601
|
+
const role = opt('role');
|
|
602
|
+
if (role) {
|
|
603
|
+
// The one form that reads the database: a role is data, and what it grants is
|
|
604
|
+
// a fact about a deployment rather than about the code.
|
|
605
|
+
const spec = pickSpec(specs);
|
|
606
|
+
const config = readConfig(process.env, spec.serve?.defaults ?? {});
|
|
607
|
+
const adapter = await (spec.serve?.openStore ?? sqliteStore)(config);
|
|
608
|
+
try {
|
|
609
|
+
const keys = await grantsOfRole(adapter, role);
|
|
610
|
+
console.log(`role "${role}" grants ${keys.length} function(s)\n`);
|
|
611
|
+
console.log(formatReach(reachOf(m, keys)));
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
await adapter.close();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else if (module)
|
|
618
|
+
console.log(formatReach(reachOf(m, functionsOf(m, module))));
|
|
619
|
+
else if (grant)
|
|
620
|
+
console.log(formatReach(reachOf(m, grant
|
|
621
|
+
.split(',')
|
|
622
|
+
.map((s) => s.trim())
|
|
623
|
+
.filter(Boolean))));
|
|
624
|
+
else
|
|
625
|
+
console.log(formatInventory(m));
|
|
626
|
+
}
|
|
627
|
+
else if (cmd === 'agent') {
|
|
628
|
+
const [, m] = pickApp(ws);
|
|
629
|
+
console.log(JSON.stringify(agentDescriptor(m), null, 2));
|
|
630
|
+
}
|
|
631
|
+
else if (cmd === 'snapshot') {
|
|
632
|
+
const [name, m] = pickApp(ws);
|
|
633
|
+
const out = `.ket/manifest.${name}.json`;
|
|
634
|
+
writeFileSync(out, JSON.stringify(m, null, 2));
|
|
635
|
+
console.log(`wrote ${out}`);
|
|
636
|
+
}
|
|
637
|
+
else if (cmd === 'diff') {
|
|
638
|
+
const against = opt('against');
|
|
639
|
+
if (!against || !existsSync(against))
|
|
640
|
+
throw new Error('pass --against <manifest.json> (make one with `ket snapshot`)');
|
|
641
|
+
const [, m] = pickApp(ws);
|
|
642
|
+
const before = JSON.parse(readFileSync(against, 'utf8'));
|
|
643
|
+
const items = diffManifests(before, m);
|
|
644
|
+
console.log(formatDiff(items));
|
|
645
|
+
process.exit(items.some((i) => i.severity === 'breaking') ? 1 : 0);
|
|
646
|
+
}
|
|
647
|
+
else if (cmd === 'migrate') {
|
|
648
|
+
const spec = pickSpec(specs);
|
|
649
|
+
if (flag('all')) {
|
|
650
|
+
// The fleet. A deployment that ships a new module has to reach every tenant
|
|
651
|
+
// database, and one that cannot be opened must not stop the others — a
|
|
652
|
+
// half-migrated fleet you cannot see is worse than one you can.
|
|
653
|
+
const tenants = spec.serve?.tenants;
|
|
654
|
+
if (!tenants)
|
|
655
|
+
throw new Error(`app "${spec.name}" serves a single datastore; drop --all`);
|
|
656
|
+
const config = readConfig(process.env, spec.serve?.defaults ?? {});
|
|
657
|
+
const pool = createAdapterPool({ create: (key) => tenants.open(key, config) });
|
|
658
|
+
try {
|
|
659
|
+
const keys = await tenants.list();
|
|
660
|
+
const m = ws.apps[spec.name];
|
|
661
|
+
const results = await migrateFleet(pool, keys, m, {
|
|
662
|
+
allowDestructive: flag('allow-destructive'),
|
|
663
|
+
dryRun: flag('dry-run'),
|
|
664
|
+
});
|
|
665
|
+
console.log(formatFleet(results));
|
|
666
|
+
process.exit(results.some((r) => r.error) ? 1 : 0);
|
|
667
|
+
}
|
|
668
|
+
finally {
|
|
669
|
+
await pool.close();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const [name, m] = pickApp(ws);
|
|
673
|
+
const adapter = sqliteAdapter();
|
|
674
|
+
adapter.open();
|
|
675
|
+
const snapPath = `.ket/schema.${name}.json`;
|
|
676
|
+
const prev = existsSync(snapPath) ? JSON.parse(readFileSync(snapPath, 'utf8')) : null;
|
|
677
|
+
const next = schemaFromManifest(m);
|
|
678
|
+
const ops = planMigration(prev, next, { allowDestructive: flag('allow-destructive') });
|
|
679
|
+
for (const sql of renderSql(ops, adapter))
|
|
680
|
+
console.log(sql + ';');
|
|
681
|
+
writeFileSync(snapPath, JSON.stringify(next, null, 2));
|
|
682
|
+
console.log(`\n-- ${ops.length} operation(s); schema snapshot written to ${snapPath}`);
|
|
683
|
+
adapter.close();
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
console.error(`unknown command "${cmd}"\n\n${HELP}`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (e) {
|
|
691
|
+
if (e instanceof KetError) {
|
|
692
|
+
console.error(JSON.stringify(e.toJSON(), null, 2));
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
console.error(e.message);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
//# sourceMappingURL=cli.js.map
|