@basaltkit/cli 1.2.0 → 1.2.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/README.md +6 -0
- package/dist/builtins.d.ts +16 -0
- package/dist/builtins.js +36 -0
- package/dist/command.d.ts +25 -0
- package/dist/command.js +3 -0
- package/dist/dev.d.ts +49 -0
- package/dist/dev.js +126 -0
- package/dist/index.d.ts +9 -217
- package/dist/index.js +9 -455
- package/dist/io.d.ts +14 -0
- package/dist/io.js +49 -0
- package/dist/parse.d.ts +7 -0
- package/dist/parse.js +23 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +12 -0
- package/dist/publish.d.ts +35 -0
- package/dist/publish.js +110 -0
- package/dist/runner.d.ts +16 -0
- package/dist/runner.js +36 -0
- package/dist/upgrade.d.ts +48 -0
- package/dist/upgrade.js +112 -0
- package/package.json +9 -10
package/dist/publish.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { defineCommand } from './command.js';
|
|
2
|
+
const DOCKERFILE = `# syntax=docker/dockerfile:1
|
|
3
|
+
FROM node:22-slim AS base
|
|
4
|
+
ENV NODE_ENV=production
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
FROM base AS deps
|
|
8
|
+
COPY package.json pnpm-lock.yaml* ./
|
|
9
|
+
RUN corepack enable && pnpm install --prod --frozen-lockfile
|
|
10
|
+
|
|
11
|
+
FROM base AS run
|
|
12
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
13
|
+
COPY . .
|
|
14
|
+
EXPOSE 3000
|
|
15
|
+
CMD ["node", "dist/main.js"]
|
|
16
|
+
`;
|
|
17
|
+
const CI_WORKFLOW = `name: ci
|
|
18
|
+
on:
|
|
19
|
+
push: { branches: [main] }
|
|
20
|
+
pull_request:
|
|
21
|
+
jobs:
|
|
22
|
+
build:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: pnpm/action-setup@v4
|
|
27
|
+
- uses: actions/setup-node@v4
|
|
28
|
+
with: { node-version: 22, cache: pnpm }
|
|
29
|
+
- run: pnpm install --frozen-lockfile
|
|
30
|
+
- run: pnpm run build
|
|
31
|
+
- run: pnpm run test
|
|
32
|
+
`;
|
|
33
|
+
const EDITORCONFIG = `root = true
|
|
34
|
+
|
|
35
|
+
[*]
|
|
36
|
+
charset = utf-8
|
|
37
|
+
end_of_line = lf
|
|
38
|
+
insert_final_newline = true
|
|
39
|
+
indent_style = space
|
|
40
|
+
indent_size = 2
|
|
41
|
+
trim_trailing_whitespace = true
|
|
42
|
+
`;
|
|
43
|
+
/** Stubs bundled with the CLI. Apps can register more via the metadata bucket. */
|
|
44
|
+
export const PUBLISHABLES = [
|
|
45
|
+
{ id: 'dockerfile', description: 'Production multi-stage Dockerfile', files: () => [{ path: 'Dockerfile', content: DOCKERFILE }] },
|
|
46
|
+
{ id: 'ci', description: 'GitHub Actions CI workflow', files: () => [{ path: '.github/workflows/ci.yml', content: CI_WORKFLOW }] },
|
|
47
|
+
{ id: 'editorconfig', description: 'Shared .editorconfig', files: () => [{ path: '.editorconfig', content: EDITORCONFIG }] },
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Copies a publishable's files into the target, skipping any that already exist
|
|
51
|
+
* unless `force` is set. Returns which files were written vs skipped.
|
|
52
|
+
*/
|
|
53
|
+
export async function runPublish(publishable, fs, options = {}) {
|
|
54
|
+
const written = [];
|
|
55
|
+
const skipped = [];
|
|
56
|
+
for (const file of publishable.files()) {
|
|
57
|
+
if (!options.force && (await fs.exists(file.path))) {
|
|
58
|
+
skipped.push(file.path);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
await fs.write(file.path, file.content);
|
|
62
|
+
written.push(file.path);
|
|
63
|
+
}
|
|
64
|
+
return { written, skipped };
|
|
65
|
+
}
|
|
66
|
+
/** Node-backed {@link PublishFs} rooted at `dir` (creates parent directories). */
|
|
67
|
+
export function nodePublishFs(dir) {
|
|
68
|
+
return {
|
|
69
|
+
async exists(path) {
|
|
70
|
+
const { access } = await import('node:fs/promises');
|
|
71
|
+
const { join } = await import('node:path');
|
|
72
|
+
return access(join(dir, path)).then(() => true, () => false);
|
|
73
|
+
},
|
|
74
|
+
async write(path, content) {
|
|
75
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
76
|
+
const { dirname, join } = await import('node:path');
|
|
77
|
+
const target = join(dir, path);
|
|
78
|
+
await mkdir(dirname(target), { recursive: true });
|
|
79
|
+
await writeFile(target, content, 'utf8');
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** `basalt publish [<id>] [--dir=<path>] [--force]` — list, or copy a stub group. */
|
|
84
|
+
export const publishCommand = defineCommand({
|
|
85
|
+
name: 'publish',
|
|
86
|
+
description: 'Copy a publishable stub group into the app (run with no id to list)',
|
|
87
|
+
async handle({ io, args, flags }) {
|
|
88
|
+
const id = args[0];
|
|
89
|
+
if (!id) {
|
|
90
|
+
io.log('Publishable groups:');
|
|
91
|
+
io.table(PUBLISHABLES.map((p) => ({ id: p.id, description: p.description })));
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
const publishable = PUBLISHABLES.find((p) => p.id === id);
|
|
95
|
+
if (!publishable) {
|
|
96
|
+
io.error(`Unknown publishable "${id}". Available: ${PUBLISHABLES.map((p) => p.id).join(', ')}.`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
const dir = typeof flags['dir'] === 'string' ? flags['dir'] : process.cwd();
|
|
100
|
+
const result = await runPublish(publishable, nodePublishFs(dir), { force: flags['force'] === true });
|
|
101
|
+
for (const path of result.written)
|
|
102
|
+
io.log(` wrote ${path}`);
|
|
103
|
+
for (const path of result.skipped)
|
|
104
|
+
io.log(` skipped ${path} (exists — use --force to overwrite)`);
|
|
105
|
+
if (result.written.length === 0 && result.skipped.length > 0) {
|
|
106
|
+
io.log('Nothing written. Re-run with --force to overwrite existing files.');
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
},
|
|
110
|
+
});
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BasaltApp } from '@basaltkit/core';
|
|
2
|
+
import type { CommandIo } from './command.js';
|
|
3
|
+
export interface RunCliOptions {
|
|
4
|
+
app: BasaltApp;
|
|
5
|
+
/** Defaults to process.argv.slice(2). */
|
|
6
|
+
argv?: string[];
|
|
7
|
+
/** Defaults to console output — swap in tests. */
|
|
8
|
+
io?: CommandIo;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Boots the app, resolves the command (built-ins + everything registered in
|
|
12
|
+
* the 'commands' metadata bucket), runs it and shuts the app down.
|
|
13
|
+
* Returns the process exit code instead of calling process.exit — the bin
|
|
14
|
+
* wrapper decides what to do with it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runCli(options: RunCliOptions): Promise<number>;
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ensureMetadata } from '@basaltkit/core';
|
|
2
|
+
import { builtinCommands } from './builtins.js';
|
|
3
|
+
import { consoleIo } from './io.js';
|
|
4
|
+
import { parseArgv } from './parse.js';
|
|
5
|
+
/**
|
|
6
|
+
* Boots the app, resolves the command (built-ins + everything registered in
|
|
7
|
+
* the 'commands' metadata bucket), runs it and shuts the app down.
|
|
8
|
+
* Returns the process exit code instead of calling process.exit — the bin
|
|
9
|
+
* wrapper decides what to do with it.
|
|
10
|
+
*/
|
|
11
|
+
export async function runCli(options) {
|
|
12
|
+
const { app } = options;
|
|
13
|
+
const io = options.io ?? consoleIo();
|
|
14
|
+
const { command, args, flags } = parseArgv(options.argv ?? process.argv.slice(2));
|
|
15
|
+
if (app.phase === 'created')
|
|
16
|
+
await app.boot();
|
|
17
|
+
try {
|
|
18
|
+
const registered = ensureMetadata(app.container).get('commands');
|
|
19
|
+
const commands = [...builtinCommands(), ...registered];
|
|
20
|
+
if (command === undefined || command === 'list') {
|
|
21
|
+
io.log('Available commands:');
|
|
22
|
+
io.table(commands.map(({ name, description }) => ({ command: name, description: description ?? '' })));
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const found = commands.find((candidate) => candidate.name === command);
|
|
26
|
+
if (!found) {
|
|
27
|
+
io.error(`Unknown command "${command}". Run "basalt list" to see what is available.`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
const code = await found.handle({ app, container: app.container, io, args, flags });
|
|
31
|
+
return code ?? 0;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await app.shutdown();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type CommandDefinition } from './command.js';
|
|
2
|
+
/**
|
|
3
|
+
* Filesystem surface an upgrade migration works against. Injected so migrations
|
|
4
|
+
* (and `runUpgrade`) are testable with an in-memory tree — no disk needed.
|
|
5
|
+
*/
|
|
6
|
+
export interface UpgradeFs {
|
|
7
|
+
/** Recursively lists files under `dir` (relative paths). Skips node_modules/dist/.git. */
|
|
8
|
+
list(dir: string): Promise<string[]>;
|
|
9
|
+
read(path: string): Promise<string>;
|
|
10
|
+
write(path: string, content: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** A single edit a migration wants to make — surfaced for `--dry` before writing. */
|
|
13
|
+
export interface Edit {
|
|
14
|
+
path: string;
|
|
15
|
+
before: string;
|
|
16
|
+
after: string;
|
|
17
|
+
}
|
|
18
|
+
/** A versioned, idempotent codemod. `plan` computes edits without writing them. */
|
|
19
|
+
export interface Migration {
|
|
20
|
+
id: string;
|
|
21
|
+
description: string;
|
|
22
|
+
plan(fs: UpgradeFs, dir: string): Promise<Edit[]>;
|
|
23
|
+
}
|
|
24
|
+
export interface UpgradeReport {
|
|
25
|
+
migration: string;
|
|
26
|
+
changed: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A codemod that rewrites the deprecated `@machize/*` npm scope to `@basaltkit/*`
|
|
30
|
+
* across package.json and source imports. Unambiguous (scoped rename), so a plain
|
|
31
|
+
* text replace is safe and idempotent.
|
|
32
|
+
*/
|
|
33
|
+
export declare const renameMachizeScope: Migration;
|
|
34
|
+
/** All migrations shipped with the CLI, in application order. */
|
|
35
|
+
export declare const MIGRATIONS: Migration[];
|
|
36
|
+
/**
|
|
37
|
+
* Runs the selected migrations. With `dry`, computes and reports edits without
|
|
38
|
+
* writing. Returns a per-migration report of the files it changed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function runUpgrade(migrations: Migration[], fs: UpgradeFs, options: {
|
|
41
|
+
dir: string;
|
|
42
|
+
dry?: boolean;
|
|
43
|
+
only?: string;
|
|
44
|
+
}): Promise<UpgradeReport[]>;
|
|
45
|
+
/** Node-backed {@link UpgradeFs}. Recursive, skipping build/vcs directories. */
|
|
46
|
+
export declare function nodeUpgradeFs(): UpgradeFs;
|
|
47
|
+
/** `basalt upgrade [--dir=<path>] [--dry] [--only=<migration-id>]` */
|
|
48
|
+
export declare const upgradeCommand: CommandDefinition;
|
package/dist/upgrade.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineCommand } from './command.js';
|
|
2
|
+
/**
|
|
3
|
+
* A codemod that rewrites the deprecated `@machize/*` npm scope to `@basaltkit/*`
|
|
4
|
+
* across package.json and source imports. Unambiguous (scoped rename), so a plain
|
|
5
|
+
* text replace is safe and idempotent.
|
|
6
|
+
*/
|
|
7
|
+
export const renameMachizeScope = {
|
|
8
|
+
id: 'rename-machize-scope',
|
|
9
|
+
description: 'Rewrite the deprecated @machize/* scope to @basaltkit/*',
|
|
10
|
+
async plan(fs, dir) {
|
|
11
|
+
const files = (await fs.list(dir)).filter((p) => /\.(t|j)sx?$|\.json$/.test(p));
|
|
12
|
+
const edits = [];
|
|
13
|
+
for (const path of files) {
|
|
14
|
+
const before = await fs.read(path);
|
|
15
|
+
if (!before.includes('@machize/'))
|
|
16
|
+
continue;
|
|
17
|
+
const after = before.split('@machize/').join('@basaltkit/');
|
|
18
|
+
if (after !== before)
|
|
19
|
+
edits.push({ path, before, after });
|
|
20
|
+
}
|
|
21
|
+
return edits;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
/** All migrations shipped with the CLI, in application order. */
|
|
25
|
+
export const MIGRATIONS = [renameMachizeScope];
|
|
26
|
+
/**
|
|
27
|
+
* Runs the selected migrations. With `dry`, computes and reports edits without
|
|
28
|
+
* writing. Returns a per-migration report of the files it changed.
|
|
29
|
+
*/
|
|
30
|
+
export async function runUpgrade(migrations, fs, options) {
|
|
31
|
+
const selected = options.only ? migrations.filter((m) => m.id === options.only) : migrations;
|
|
32
|
+
const reports = [];
|
|
33
|
+
for (const migration of selected) {
|
|
34
|
+
const edits = await migration.plan(fs, options.dir);
|
|
35
|
+
if (!options.dry) {
|
|
36
|
+
for (const edit of edits)
|
|
37
|
+
await fs.write(edit.path, edit.after);
|
|
38
|
+
}
|
|
39
|
+
reports.push({ migration: migration.id, changed: edits.map((e) => e.path) });
|
|
40
|
+
}
|
|
41
|
+
return reports;
|
|
42
|
+
}
|
|
43
|
+
/** Node-backed {@link UpgradeFs}. Recursive, skipping build/vcs directories. */
|
|
44
|
+
export function nodeUpgradeFs() {
|
|
45
|
+
const SKIP = new Set(['node_modules', 'dist', '.git', '.next', 'build', 'coverage']);
|
|
46
|
+
return {
|
|
47
|
+
async list(dir) {
|
|
48
|
+
const { readdir } = await import('node:fs/promises');
|
|
49
|
+
const { join, relative } = await import('node:path');
|
|
50
|
+
const out = [];
|
|
51
|
+
const walk = async (current) => {
|
|
52
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
if (!SKIP.has(entry.name))
|
|
56
|
+
await walk(join(current, entry.name));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
out.push(relative(dir, join(current, entry.name)));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
await walk(dir);
|
|
64
|
+
return out;
|
|
65
|
+
},
|
|
66
|
+
async read(path) {
|
|
67
|
+
const { readFile } = await import('node:fs/promises');
|
|
68
|
+
const { isAbsolute, join } = await import('node:path');
|
|
69
|
+
return readFile(isAbsolute(path) ? path : join(process.cwd(), path), 'utf8');
|
|
70
|
+
},
|
|
71
|
+
async write(path, content) {
|
|
72
|
+
const { writeFile } = await import('node:fs/promises');
|
|
73
|
+
const { isAbsolute, join } = await import('node:path');
|
|
74
|
+
await writeFile(isAbsolute(path) ? path : join(process.cwd(), path), content, 'utf8');
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** `basalt upgrade [--dir=<path>] [--dry] [--only=<migration-id>]` */
|
|
79
|
+
export const upgradeCommand = defineCommand({
|
|
80
|
+
name: 'upgrade',
|
|
81
|
+
description: 'Apply framework upgrade codemods (--dry to preview, --only=<id>)',
|
|
82
|
+
async handle({ io, flags }) {
|
|
83
|
+
const dir = typeof flags['dir'] === 'string' ? flags['dir'] : process.cwd();
|
|
84
|
+
const dry = flags['dry'] === true;
|
|
85
|
+
const only = typeof flags['only'] === 'string' ? flags['only'] : undefined;
|
|
86
|
+
if (only && !MIGRATIONS.some((m) => m.id === only)) {
|
|
87
|
+
io.error(`Unknown migration "${only}". Available: ${MIGRATIONS.map((m) => m.id).join(', ')}.`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
const reports = await runUpgrade(MIGRATIONS, nodeUpgradeFs(), {
|
|
91
|
+
dir,
|
|
92
|
+
dry,
|
|
93
|
+
...(only ? { only } : {}),
|
|
94
|
+
});
|
|
95
|
+
let total = 0;
|
|
96
|
+
for (const report of reports) {
|
|
97
|
+
if (report.changed.length === 0)
|
|
98
|
+
continue;
|
|
99
|
+
total += report.changed.length;
|
|
100
|
+
io.log(`${dry ? '[dry] ' : ''}${report.migration}: ${report.changed.length} file(s)`);
|
|
101
|
+
for (const path of report.changed)
|
|
102
|
+
io.log(` ${path}`);
|
|
103
|
+
}
|
|
104
|
+
if (total === 0)
|
|
105
|
+
io.log('Nothing to upgrade — everything is up to date.');
|
|
106
|
+
else if (dry)
|
|
107
|
+
io.log(`\n${total} file(s) would change. Re-run without --dry to apply.`);
|
|
108
|
+
else
|
|
109
|
+
io.log(`\nApplied ${total} change(s).`);
|
|
110
|
+
return 0;
|
|
111
|
+
},
|
|
112
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "The 'basalt' command framework for Basalt apps: define commands, run them against a booted app, and inspect routes and schedules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@basaltkit/core": "^1.
|
|
17
|
+
"@basaltkit/core": "^1.1.2"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@types/node": "^
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"vitest": "^3.1.0",
|
|
20
|
+
"@types/node": "^26.3.0",
|
|
21
|
+
"typescript": "^7.0.2",
|
|
22
|
+
"vitest": "^4.1.11",
|
|
24
23
|
"@basaltkit/tsconfig": "^0.24.0"
|
|
25
24
|
},
|
|
26
25
|
"publishConfig": {
|
|
@@ -28,11 +27,11 @@
|
|
|
28
27
|
},
|
|
29
28
|
"repository": {
|
|
30
29
|
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/
|
|
30
|
+
"url": "git+https://github.com/basaltkit/basalt.git",
|
|
32
31
|
"directory": "packages/cli"
|
|
33
32
|
},
|
|
34
|
-
"homepage": "https://github.com/
|
|
35
|
-
"bugs": "https://github.com/
|
|
33
|
+
"homepage": "https://github.com/basaltkit/basalt/tree/main/packages/cli#readme",
|
|
34
|
+
"bugs": "https://github.com/basaltkit/basalt/issues",
|
|
36
35
|
"keywords": [
|
|
37
36
|
"basalt",
|
|
38
37
|
"typescript",
|
|
@@ -40,7 +39,7 @@
|
|
|
40
39
|
"commands"
|
|
41
40
|
],
|
|
42
41
|
"scripts": {
|
|
43
|
-
"build": "
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
44
43
|
"test": "vitest run",
|
|
45
44
|
"typecheck": "tsc --noEmit"
|
|
46
45
|
}
|