@basaltkit/cli 1.5.0 → 1.6.0
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/README.md +1 -1
- package/dist/dev.d.ts +33 -2
- package/dist/dev.js +139 -55
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ Without registering anything, `runCli` always provides:
|
|
|
110
110
|
| `basalt list` (or `basalt` with no arguments) | Lists all available commands, with their descriptions |
|
|
111
111
|
| `basalt routes [--json] [--unguarded --require=auth,can [--allow=<glob,…>]]` | Lists the HTTP routes registered by the application with the guards each declares in `meta` — columns `auth`, `can`, `rateLimit`, `tenant` and `guards` (`mfa`, `teamRole`, `scopes`, `subscribed`, `feature`). `--json` prints one JSON array of route rows. `--unguarded` lists the routes that do not declare the `--require`d guards and exits `1` when there is any (see [Route security review](#route-security-review)). Read from the `http:routes` metadata bucket, populated by every HTTP adapter |
|
|
112
112
|
| `basalt schedule:list` | Lists scheduled tasks and their cron expressions (read from the `schedule:entries` bucket, populated by `@basaltkit/scheduler`) |
|
|
113
|
-
| `basalt dev [--entry] [--worker] [--queue] [--no-routes]` | Runs the app with watch + restart, **prints the route table on boot**, and with `--worker` also starts a watched `queue:work` alongside it (server + worker in one command). Delegates watching to `tsx watch` / `node --watch`. |
|
|
113
|
+
| `basalt dev [--entry] [--worker] [--queue] [--no-routes]` | Runs the app with watch + restart, **prints the route table on boot**, and with `--worker` also starts a watched `queue:work` alongside it (server + worker in one command). Delegates watching to `tsx watch` / `node --watch`. Runs `src/dev.ts` when present (else `src/main.ts`, `src/server.ts`, …), and shuts the CLI's own boot down before the server starts: one app per session. A create-basalt `bin/basalt.ts` routes `dev` to `runDev({ loadApp })` before booting anything, so it starts even while the app cannot boot (the reason is printed, the watcher restarts on save). |
|
|
114
114
|
| `basalt upgrade [--dry] [--only=<id>] [--dir=<path>]` | Applies framework upgrade codemods (ships the `@machize/*` → `@basaltkit/*` scope rename; `--dry` previews). `create-basalt update` runs them for you after an install. `nodeUpgradeFs(baseDir)` resolves relative paths against the directory being upgraded, so `--dir=` reads the tree it lists |
|
|
115
115
|
| `basalt publish [<id>] [--force]` | Copies a bundled stub group into the app — `dockerfile` (a multi-stage build that runs `pnpm build` and ships `dist/src/server.js` on plain node as `USER node`, plus a `.dockerignore` keeping `.env` and keys out of the image — the same files create-basalt scaffolds), `ci` (install, typecheck, build, test), `editorconfig` (run with no id to list). The stub contents are exported too: `DOCKERFILE`, `DOCKERIGNORE`, `CI_WORKFLOW`, `EDITORCONFIG`, `PRODUCTION_ENTRY` |
|
|
116
116
|
|
package/dist/dev.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
1
|
+
import { type BasaltApp } from '@basaltkit/core';
|
|
2
|
+
import { type CommandDefinition, type CommandIo } from './command.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default entry files probed in order when `--entry` is not given. A
|
|
5
|
+
* create-basalt app's `src/dev.ts` comes first: it is what `pnpm dev` runs
|
|
6
|
+
* (it loads `.env` and opts into NODE_ENV=development before the server).
|
|
7
|
+
*/
|
|
3
8
|
export declare const DEV_ENTRY_CANDIDATES: string[];
|
|
4
9
|
/**
|
|
5
10
|
* Picks the first candidate that exists. Pure — `exists` is injected so the
|
|
@@ -45,5 +50,31 @@ export declare function devRouteRows(routes: DevRoute[]): {
|
|
|
45
50
|
* runs the server with watch/restart — and, with `--worker`, also starts a
|
|
46
51
|
* watched queue worker alongside it (server + worker in one command, the real
|
|
47
52
|
* producer/worker topology).
|
|
53
|
+
*
|
|
54
|
+
* The CLI's own boot exists only to read the route table: it is shut down
|
|
55
|
+
* before the server starts, so a dev session holds one app (one database
|
|
56
|
+
* pool, one set of connections), not two. A create-basalt `bin/basalt.ts`
|
|
57
|
+
* sends `dev` to {@link runDev} before booting anything, so the session also
|
|
58
|
+
* starts while the app cannot boot; this command is the path for a bin
|
|
59
|
+
* without that prelude.
|
|
48
60
|
*/
|
|
49
61
|
export declare const devCommand: CommandDefinition;
|
|
62
|
+
export interface RunDevOptions {
|
|
63
|
+
/** Defaults to process.argv.slice(2) — `dev` first, as typed. */
|
|
64
|
+
argv?: string[];
|
|
65
|
+
/** Imports and builds the app, unbooted — only to read its route table. */
|
|
66
|
+
loadApp: () => BasaltApp | Promise<BasaltApp>;
|
|
67
|
+
io?: CommandIo;
|
|
68
|
+
/** How long the route table may wait for the boot before the server starts without it. @default 10000 */
|
|
69
|
+
bootTimeoutMs?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `basalt dev` without depending on the app booting. The app is loaded and
|
|
73
|
+
* booted once, only to print the route table, then shut down. When that fails
|
|
74
|
+
* — a variable missing, the database down, code mid-edit — the reason is
|
|
75
|
+
* printed and the watcher starts anyway: it restarts the server on every save,
|
|
76
|
+
* as `pnpm dev` does, so you fix the problem and the session carries on.
|
|
77
|
+
*/
|
|
78
|
+
export declare function runDev(options: RunDevOptions): Promise<number>;
|
|
79
|
+
/** The reason a boot failed, in a few lines: the env report when there is one, else the first line of the message. */
|
|
80
|
+
export declare function bootFailureLines(error: unknown): string[];
|
package/dist/dev.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { ensureMetadata } from '@basaltkit/core';
|
|
2
2
|
import { defineCommand } from './command.js';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { consoleIo } from './io.js';
|
|
4
|
+
import { parseArgv } from './parse.js';
|
|
5
|
+
/**
|
|
6
|
+
* Default entry files probed in order when `--entry` is not given. A
|
|
7
|
+
* create-basalt app's `src/dev.ts` comes first: it is what `pnpm dev` runs
|
|
8
|
+
* (it loads `.env` and opts into NODE_ENV=development before the server).
|
|
9
|
+
*/
|
|
10
|
+
export const DEV_ENTRY_CANDIDATES = ['src/dev.ts', 'src/main.ts', 'src/server.ts', 'src/index.ts', 'src/main.js', 'src/index.js'];
|
|
5
11
|
/**
|
|
6
12
|
* Picks the first candidate that exists. Pure — `exists` is injected so the
|
|
7
13
|
* resolution is testable without touching disk.
|
|
@@ -53,66 +59,144 @@ export function devRouteRows(routes) {
|
|
|
53
59
|
* runs the server with watch/restart — and, with `--worker`, also starts a
|
|
54
60
|
* watched queue worker alongside it (server + worker in one command, the real
|
|
55
61
|
* producer/worker topology).
|
|
62
|
+
*
|
|
63
|
+
* The CLI's own boot exists only to read the route table: it is shut down
|
|
64
|
+
* before the server starts, so a dev session holds one app (one database
|
|
65
|
+
* pool, one set of connections), not two. A create-basalt `bin/basalt.ts`
|
|
66
|
+
* sends `dev` to {@link runDev} before booting anything, so the session also
|
|
67
|
+
* starts while the app cannot boot; this command is the path for a bin
|
|
68
|
+
* without that prelude.
|
|
56
69
|
*/
|
|
57
70
|
export const devCommand = defineCommand({
|
|
58
71
|
name: 'dev',
|
|
59
72
|
description: 'Run the app with watch + restart, print the route table, and (--worker) an embedded queue worker',
|
|
60
|
-
async handle({ container, io, flags }) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
? flags['entry']
|
|
64
|
-
: resolveDevEntry(DEV_ENTRY_CANDIDATES, existsSync);
|
|
65
|
-
if (!entry) {
|
|
66
|
-
io.error(`No entry file found. Looked for ${DEV_ENTRY_CANDIDATES.join(', ')}. Pass --entry=<file>.`);
|
|
73
|
+
async handle({ app, container, io, flags }) {
|
|
74
|
+
const entry = await devEntry(flags, io);
|
|
75
|
+
if (!entry)
|
|
67
76
|
return 1;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
io.log(`Routes (${routes.length}):`);
|
|
75
|
-
io.table(devRouteRows(routes));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const tsx = await canResolve('tsx');
|
|
79
|
-
const server = resolveDevRunner(entry, { tsx });
|
|
80
|
-
const runners = [{ label: 'server', runner: server }];
|
|
81
|
-
// Embedded worker: a watched `queue:work` on the same CLI bin, so jobs
|
|
82
|
-
// process in dev without a second terminal. Separate process (correct
|
|
83
|
-
// producer/worker split) — each restarts independently on file change.
|
|
84
|
-
if (flags['worker'] === true || flags['w'] === true) {
|
|
85
|
-
const bin = process.argv[1] ?? entry;
|
|
86
|
-
const workerBase = resolveDevRunner(bin, { tsx });
|
|
87
|
-
const queue = typeof flags['queue'] === 'string' ? [`--queue=${flags['queue']}`] : [];
|
|
88
|
-
runners.push({ label: 'worker', runner: { command: workerBase.command, args: [...workerBase.args, 'queue:work', ...queue] } });
|
|
89
|
-
}
|
|
90
|
-
for (const { label, runner } of runners) {
|
|
91
|
-
io.log(`▶ ${label}: ${runner.command} ${runner.args.join(' ')}`);
|
|
92
|
-
}
|
|
93
|
-
const { spawn } = await import('node:child_process');
|
|
94
|
-
return await new Promise((resolve) => {
|
|
95
|
-
const children = runners.map(({ runner }) => spawn(runner.command, runner.args, { stdio: 'inherit' }));
|
|
96
|
-
const stop = () => children.forEach((c) => c.kill('SIGINT'));
|
|
97
|
-
process.once('SIGINT', stop);
|
|
98
|
-
let done = false;
|
|
99
|
-
const finish = (code) => {
|
|
100
|
-
if (done)
|
|
101
|
-
return;
|
|
102
|
-
done = true;
|
|
103
|
-
process.removeListener('SIGINT', stop);
|
|
104
|
-
children.forEach((c) => c.kill('SIGINT'));
|
|
105
|
-
resolve(code);
|
|
106
|
-
};
|
|
107
|
-
// The server is primary — when it exits, tear everything down.
|
|
108
|
-
children[0]?.on('exit', (code) => finish(code ?? 0));
|
|
109
|
-
children.forEach((child, i) => child.on('error', (error) => {
|
|
110
|
-
io.error(`Failed to start ${runners[i]?.label}: ${error.message}`);
|
|
111
|
-
finish(1);
|
|
112
|
-
}));
|
|
113
|
-
});
|
|
77
|
+
if (flags['routes'] !== false)
|
|
78
|
+
printRoutes(container, io);
|
|
79
|
+
// Done with the CLI's boot: the server below is the app from here on.
|
|
80
|
+
// runCli's own shutdown afterwards is a no-op.
|
|
81
|
+
await app.shutdown();
|
|
82
|
+
return startDevProcesses(entry, flags, io);
|
|
114
83
|
},
|
|
115
84
|
});
|
|
85
|
+
/**
|
|
86
|
+
* `basalt dev` without depending on the app booting. The app is loaded and
|
|
87
|
+
* booted once, only to print the route table, then shut down. When that fails
|
|
88
|
+
* — a variable missing, the database down, code mid-edit — the reason is
|
|
89
|
+
* printed and the watcher starts anyway: it restarts the server on every save,
|
|
90
|
+
* as `pnpm dev` does, so you fix the problem and the session carries on.
|
|
91
|
+
*/
|
|
92
|
+
export async function runDev(options) {
|
|
93
|
+
const io = options.io ?? consoleIo();
|
|
94
|
+
const { flags } = parseArgv(options.argv ?? process.argv.slice(2));
|
|
95
|
+
const entry = await devEntry(flags, io);
|
|
96
|
+
if (!entry)
|
|
97
|
+
return 1;
|
|
98
|
+
if (flags['routes'] !== false)
|
|
99
|
+
await routesIfItBoots(options, io);
|
|
100
|
+
return startDevProcesses(entry, flags, io);
|
|
101
|
+
}
|
|
102
|
+
async function routesIfItBoots(options, io) {
|
|
103
|
+
let app;
|
|
104
|
+
let timer;
|
|
105
|
+
const timeoutMs = options.bootTimeoutMs ?? 10_000;
|
|
106
|
+
const boot = (async () => {
|
|
107
|
+
app = await options.loadApp();
|
|
108
|
+
if (app.phase === 'created')
|
|
109
|
+
await app.boot();
|
|
110
|
+
return app;
|
|
111
|
+
})();
|
|
112
|
+
const timedOut = new Promise((resolve) => { timer = setTimeout(() => resolve('timeout'), timeoutMs); });
|
|
113
|
+
try {
|
|
114
|
+
const booted = await Promise.race([boot, timedOut]);
|
|
115
|
+
if (booted === 'timeout') {
|
|
116
|
+
io.error(`! The app did not boot within ${timeoutMs / 1000}s, so no route table — starting the server anyway.`);
|
|
117
|
+
// It may still finish: shut it down then, so the session holds one app.
|
|
118
|
+
boot.then((late) => late.shutdown()).catch(() => { });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
printRoutes(booted.container, io);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
io.error('! The app does not boot yet, so no route table — starting the server anyway; it restarts on every save:');
|
|
125
|
+
for (const line of bootFailureLines(error))
|
|
126
|
+
io.error(` ${line}`);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
if (app && app.phase !== 'created')
|
|
131
|
+
await app.shutdown().catch(() => { });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** The reason a boot failed, in a few lines: the env report when there is one, else the first line of the message. */
|
|
135
|
+
export function bootFailureLines(error) {
|
|
136
|
+
const failure = (error ?? {});
|
|
137
|
+
if (process.env['BASALT_DEBUG'] === '1' && typeof failure.stack === 'string')
|
|
138
|
+
return failure.stack.split('\n');
|
|
139
|
+
if (failure.code === 'ENV_INVALID' && Array.isArray(failure.report)) {
|
|
140
|
+
return ['invalid environment variables:', ...failure.report.map((line) => `- ${String(line)}`)];
|
|
141
|
+
}
|
|
142
|
+
const first = String(failure.message ?? error).split('\n')[0] ?? '';
|
|
143
|
+
return [first, '(BASALT_DEBUG=1 shows the stack trace)'];
|
|
144
|
+
}
|
|
145
|
+
async function devEntry(flags, io) {
|
|
146
|
+
if (typeof flags['entry'] === 'string')
|
|
147
|
+
return flags['entry'];
|
|
148
|
+
const { existsSync } = await import('node:fs');
|
|
149
|
+
const entry = resolveDevEntry(DEV_ENTRY_CANDIDATES, existsSync);
|
|
150
|
+
if (!entry)
|
|
151
|
+
io.error(`No entry file found. Looked for ${DEV_ENTRY_CANDIDATES.join(', ')}. Pass --entry=<file>.`);
|
|
152
|
+
return entry;
|
|
153
|
+
}
|
|
154
|
+
/** The route table, from the `http:routes` metadata whichever adapter published. Adapter-agnostic. */
|
|
155
|
+
function printRoutes(container, io) {
|
|
156
|
+
const routes = ensureMetadata(container).get('http:routes');
|
|
157
|
+
if (routes.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
io.log(`Routes (${routes.length}):`);
|
|
160
|
+
io.table(devRouteRows(routes));
|
|
161
|
+
}
|
|
162
|
+
async function startDevProcesses(entry, flags, io) {
|
|
163
|
+
const tsx = await canResolve('tsx');
|
|
164
|
+
const server = resolveDevRunner(entry, { tsx });
|
|
165
|
+
const runners = [{ label: 'server', runner: server }];
|
|
166
|
+
// Embedded worker: a watched `queue:work` on the same CLI bin, so jobs
|
|
167
|
+
// process in dev without a second terminal. Separate process (correct
|
|
168
|
+
// producer/worker split) — each restarts independently on file change.
|
|
169
|
+
if (flags['worker'] === true || flags['w'] === true) {
|
|
170
|
+
const bin = process.argv[1] ?? entry;
|
|
171
|
+
const workerBase = resolveDevRunner(bin, { tsx });
|
|
172
|
+
const queue = typeof flags['queue'] === 'string' ? [`--queue=${flags['queue']}`] : [];
|
|
173
|
+
runners.push({ label: 'worker', runner: { command: workerBase.command, args: [...workerBase.args, 'queue:work', ...queue] } });
|
|
174
|
+
}
|
|
175
|
+
for (const { label, runner } of runners) {
|
|
176
|
+
io.log(`▶ ${label}: ${runner.command} ${runner.args.join(' ')}`);
|
|
177
|
+
}
|
|
178
|
+
const { spawn } = await import('node:child_process');
|
|
179
|
+
return await new Promise((resolve) => {
|
|
180
|
+
const children = runners.map(({ runner }) => spawn(runner.command, runner.args, { stdio: 'inherit' }));
|
|
181
|
+
const stop = () => children.forEach((c) => c.kill('SIGINT'));
|
|
182
|
+
process.once('SIGINT', stop);
|
|
183
|
+
let done = false;
|
|
184
|
+
const finish = (code) => {
|
|
185
|
+
if (done)
|
|
186
|
+
return;
|
|
187
|
+
done = true;
|
|
188
|
+
process.removeListener('SIGINT', stop);
|
|
189
|
+
children.forEach((c) => c.kill('SIGINT'));
|
|
190
|
+
resolve(code);
|
|
191
|
+
};
|
|
192
|
+
// The server is primary — when it exits, tear everything down.
|
|
193
|
+
children[0]?.on('exit', (code) => finish(code ?? 0));
|
|
194
|
+
children.forEach((child, i) => child.on('error', (error) => {
|
|
195
|
+
io.error(`Failed to start ${runners[i]?.label}: ${error.message}`);
|
|
196
|
+
finish(1);
|
|
197
|
+
}));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
116
200
|
/** True if a module specifier resolves from the app's node_modules. */
|
|
117
201
|
async function canResolve(specifier) {
|
|
118
202
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { commandsPlugin } from './plugin.js';
|
|
|
4
4
|
export { consoleIo, memoryIo, renderTable } from './io.js';
|
|
5
5
|
export { parseArgv, type ParsedArgv } from './parse.js';
|
|
6
6
|
export { builtinCommands, routesCommand, routeAllowPattern, UNGUARDED_SCOPE_NOTE, scheduleListCommand, type RouteMetadata, type ScheduleMetadata, } from './builtins.js';
|
|
7
|
-
export { devCommand, resolveDevEntry, resolveDevRunner, devRouteRows, DEV_ENTRY_CANDIDATES, type DevRunner, type DevRoute, } from './dev.js';
|
|
7
|
+
export { devCommand, runDev, bootFailureLines, type RunDevOptions, resolveDevEntry, resolveDevRunner, devRouteRows, DEV_ENTRY_CANDIDATES, type DevRunner, type DevRoute, } from './dev.js';
|
|
8
8
|
export { upgradeCommand, runUpgrade, MIGRATIONS, renameMachizeScope, nodeUpgradeFs, type Migration, type Edit, type UpgradeFs, type UpgradeReport, } from './upgrade.js';
|
|
9
9
|
export { publishCommand, runPublish, PUBLISHABLES, nodePublishFs, type Publishable, type PublishableFile, type PublishFs, type PublishResult, } from './publish.js';
|
|
10
10
|
export { DOCKERFILE, DOCKERIGNORE, CI_WORKFLOW, EDITORCONFIG, PRODUCTION_ENTRY } from './stubs.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { commandsPlugin } from './plugin.js';
|
|
|
4
4
|
export { consoleIo, memoryIo, renderTable } from './io.js';
|
|
5
5
|
export { parseArgv } from './parse.js';
|
|
6
6
|
export { builtinCommands, routesCommand, routeAllowPattern, UNGUARDED_SCOPE_NOTE, scheduleListCommand, } from './builtins.js';
|
|
7
|
-
export { devCommand, resolveDevEntry, resolveDevRunner, devRouteRows, DEV_ENTRY_CANDIDATES, } from './dev.js';
|
|
7
|
+
export { devCommand, runDev, bootFailureLines, resolveDevEntry, resolveDevRunner, devRouteRows, DEV_ENTRY_CANDIDATES, } from './dev.js';
|
|
8
8
|
export { upgradeCommand, runUpgrade, MIGRATIONS, renameMachizeScope, nodeUpgradeFs, } from './upgrade.js';
|
|
9
9
|
export { publishCommand, runPublish, PUBLISHABLES, nodePublishFs, } from './publish.js';
|
|
10
10
|
export { DOCKERFILE, DOCKERIGNORE, CI_WORKFLOW, EDITORCONFIG, PRODUCTION_ENTRY } from './stubs.js';
|