@basaltkit/cli 1.4.1 → 1.5.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 +17 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/launcher.d.ts +46 -0
- package/dist/launcher.js +154 -0
- package/dist/stubs.d.ts +5 -2
- package/dist/stubs.js +6 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -24,6 +24,23 @@ pnpm add @basaltkit/cli
|
|
|
24
24
|
|
|
25
25
|
> Note: the package depends on `@basaltkit/core` (the heart of the framework, where `createApp` and the dependency container live). If you created the project with `create-basalt --cli`, both are already installed.
|
|
26
26
|
|
|
27
|
+
### The global `basalt` launcher
|
|
28
|
+
|
|
29
|
+
Inside an app, `basalt` is a `package.json` script: `pnpm basalt <command>`.
|
|
30
|
+
Install the launcher once to type `basalt <command>` instead:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm i -g @basaltkit/cli
|
|
34
|
+
basalt routes # anywhere inside the app = pnpm basalt routes, in the app's root
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It finds the nearest `package.json` with a `basalt` script, from the current
|
|
38
|
+
directory up, and runs that script with the app's package manager
|
|
39
|
+
(`packageManager`, else the lockfile). So the code that runs is always the
|
|
40
|
+
app's, at its pinned versions. Arguments and the exit code pass through
|
|
41
|
+
unchanged. Outside an app it says so and exits 1. The launcher loads nothing of
|
|
42
|
+
the framework, only Node built-ins.
|
|
43
|
+
|
|
27
44
|
## Get started in 5 minutes
|
|
28
45
|
|
|
29
46
|
1. Create a `bin/basalt.ts` file at the root of the project — this will be the entry point for the `basalt` command:
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The global `basalt` launcher: `npm i -g @basaltkit/cli`, then `basalt <command>`
|
|
3
|
+
* anywhere inside an app does what `pnpm basalt <command>` does there.
|
|
4
|
+
*
|
|
5
|
+
* It never runs commands itself. It finds the app (the nearest package.json
|
|
6
|
+
* with a `basalt` script) and hands the arguments to that script through the
|
|
7
|
+
* app's own package manager — so the code that runs is always the app's, at
|
|
8
|
+
* the versions its lockfile pins, never the launcher's. Node built-ins only:
|
|
9
|
+
* nothing of the framework is loaded.
|
|
10
|
+
*/
|
|
11
|
+
export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
|
|
12
|
+
export interface LauncherProject {
|
|
13
|
+
/** The directory whose package.json declares the `basalt` script. */
|
|
14
|
+
root: string;
|
|
15
|
+
/** The script itself, e.g. `tsx bin/basalt.ts`. */
|
|
16
|
+
script: string;
|
|
17
|
+
pm: PackageManager;
|
|
18
|
+
}
|
|
19
|
+
/** Set on the script's environment: a `basalt` script that calls `basalt` again would loop. */
|
|
20
|
+
export declare const LAUNCHER_ENV = "BASALT_LAUNCHER_ROOT";
|
|
21
|
+
/** The nearest app at or above `from`: a package.json with a `basalt` script. */
|
|
22
|
+
export declare function findProject(from: string): LauncherProject | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* `packageManager` in the nearest package.json that has one, else the nearest
|
|
25
|
+
* lockfile — both walking up, so an app inside a workspace uses the
|
|
26
|
+
* workspace's manager. Nothing found: npm.
|
|
27
|
+
*/
|
|
28
|
+
export declare function detectPackageManager(root: string): PackageManager;
|
|
29
|
+
/**
|
|
30
|
+
* The command that runs the app's `basalt` script with `args`, as given:
|
|
31
|
+
* pnpm, yarn and bun pass everything after the script name through (a `--`
|
|
32
|
+
* included); npm needs a `--` first.
|
|
33
|
+
*/
|
|
34
|
+
export declare function launchCommand(pm: PackageManager, args: readonly string[]): {
|
|
35
|
+
command: string;
|
|
36
|
+
args: string[];
|
|
37
|
+
};
|
|
38
|
+
export interface RunLauncherOptions {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
env?: NodeJS.ProcessEnv;
|
|
41
|
+
/** Where the launcher's own lines go (never the app's output). */
|
|
42
|
+
error?: (line: string) => void;
|
|
43
|
+
log?: (line: string) => void;
|
|
44
|
+
}
|
|
45
|
+
/** Runs `basalt <argv>`: the app's `basalt` script, through the app's package manager. Resolves to the exit code. */
|
|
46
|
+
export declare function runLauncher(argv: readonly string[], options?: RunLauncherOptions): Promise<number>;
|
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The global `basalt` launcher: `npm i -g @basaltkit/cli`, then `basalt <command>`
|
|
3
|
+
* anywhere inside an app does what `pnpm basalt <command>` does there.
|
|
4
|
+
*
|
|
5
|
+
* It never runs commands itself. It finds the app (the nearest package.json
|
|
6
|
+
* with a `basalt` script) and hands the arguments to that script through the
|
|
7
|
+
* app's own package manager — so the code that runs is always the app's, at
|
|
8
|
+
* the versions its lockfile pins, never the launcher's. Node built-ins only:
|
|
9
|
+
* nothing of the framework is loaded.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join, resolve } from 'node:path';
|
|
14
|
+
/** Set on the script's environment: a `basalt` script that calls `basalt` again would loop. */
|
|
15
|
+
export const LAUNCHER_ENV = 'BASALT_LAUNCHER_ROOT';
|
|
16
|
+
function readManifest(dir) {
|
|
17
|
+
const file = join(dir, 'package.json');
|
|
18
|
+
if (!existsSync(file))
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
22
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined; // an unreadable package.json is not an app: keep walking
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Every directory from `from` up to the filesystem root, nearest first. */
|
|
29
|
+
function* ancestors(from) {
|
|
30
|
+
let dir = resolve(from);
|
|
31
|
+
while (true) {
|
|
32
|
+
yield dir;
|
|
33
|
+
const parent = dirname(dir);
|
|
34
|
+
if (parent === dir)
|
|
35
|
+
return;
|
|
36
|
+
dir = parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The nearest app at or above `from`: a package.json with a `basalt` script. */
|
|
40
|
+
export function findProject(from) {
|
|
41
|
+
for (const dir of ancestors(from)) {
|
|
42
|
+
const script = readManifest(dir)?.scripts?.['basalt'];
|
|
43
|
+
if (typeof script === 'string' && script.trim() !== '') {
|
|
44
|
+
return { root: dir, script, pm: detectPackageManager(dir) };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const LOCKFILES = [
|
|
50
|
+
['pnpm', ['pnpm-lock.yaml']],
|
|
51
|
+
['bun', ['bun.lock', 'bun.lockb']],
|
|
52
|
+
['yarn', ['yarn.lock']],
|
|
53
|
+
['npm', ['package-lock.json']],
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* `packageManager` in the nearest package.json that has one, else the nearest
|
|
57
|
+
* lockfile — both walking up, so an app inside a workspace uses the
|
|
58
|
+
* workspace's manager. Nothing found: npm.
|
|
59
|
+
*/
|
|
60
|
+
export function detectPackageManager(root) {
|
|
61
|
+
for (const dir of ancestors(root)) {
|
|
62
|
+
const declared = readManifest(dir)?.packageManager;
|
|
63
|
+
if (typeof declared === 'string') {
|
|
64
|
+
const name = declared.split('@')[0];
|
|
65
|
+
if (name === 'pnpm' || name === 'npm' || name === 'yarn' || name === 'bun')
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
for (const [pm, files] of LOCKFILES)
|
|
69
|
+
if (files.some((file) => existsSync(join(dir, file))))
|
|
70
|
+
return pm;
|
|
71
|
+
}
|
|
72
|
+
return 'npm';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The command that runs the app's `basalt` script with `args`, as given:
|
|
76
|
+
* pnpm, yarn and bun pass everything after the script name through (a `--`
|
|
77
|
+
* included); npm needs a `--` first.
|
|
78
|
+
*/
|
|
79
|
+
export function launchCommand(pm, args) {
|
|
80
|
+
switch (pm) {
|
|
81
|
+
case 'pnpm': return { command: 'pnpm', args: ['run', '--silent', 'basalt', ...args] };
|
|
82
|
+
case 'npm': return { command: 'npm', args: ['run', '--silent', 'basalt', '--', ...args] };
|
|
83
|
+
case 'yarn': return { command: 'yarn', args: ['run', 'basalt', ...args] };
|
|
84
|
+
case 'bun': return { command: 'bun', args: ['run', 'basalt', ...args] };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function launcherVersion() {
|
|
88
|
+
try {
|
|
89
|
+
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
90
|
+
return manifest.version ?? 'unknown';
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return 'unknown';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const NOT_AN_APP = [
|
|
97
|
+
'basalt: no Basalt app here — no package.json with a "basalt" script in this directory or above it.',
|
|
98
|
+
' Start one: npm create basalt@latest my-app',
|
|
99
|
+
' Or cd into an app first; `basalt <command>` then runs `<pm> basalt <command>` there.',
|
|
100
|
+
];
|
|
101
|
+
/** Runs `basalt <argv>`: the app's `basalt` script, through the app's package manager. Resolves to the exit code. */
|
|
102
|
+
export async function runLauncher(argv, options = {}) {
|
|
103
|
+
const cwd = options.cwd ?? process.cwd();
|
|
104
|
+
const env = options.env ?? process.env;
|
|
105
|
+
const error = options.error ?? ((line) => process.stderr.write(`${line}\n`));
|
|
106
|
+
const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
|
|
107
|
+
const project = findProject(cwd);
|
|
108
|
+
if (!project) {
|
|
109
|
+
if (argv[0] === '--version' || argv[0] === '-v') {
|
|
110
|
+
log(`basalt launcher ${launcherVersion()} (@basaltkit/cli)`);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
for (const line of NOT_AN_APP)
|
|
114
|
+
error(line);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
if (env[LAUNCHER_ENV] === project.root) {
|
|
118
|
+
error(`basalt: the "basalt" script in ${join(project.root, 'package.json')} runs \`basalt\` again (${project.script}).`);
|
|
119
|
+
error(' Point it at the app\'s own CLI instead, e.g. "basalt": "tsx bin/basalt.ts".');
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
const { command, args } = launchCommand(project.pm, argv);
|
|
123
|
+
return new Promise((done) => {
|
|
124
|
+
const child = spawn(command, args, {
|
|
125
|
+
cwd: project.root,
|
|
126
|
+
env: { ...env, [LAUNCHER_ENV]: project.root },
|
|
127
|
+
stdio: 'inherit',
|
|
128
|
+
shell: process.platform === 'win32',
|
|
129
|
+
});
|
|
130
|
+
// Ctrl-C reaches the whole foreground group: the app handles it; the
|
|
131
|
+
// launcher only waits for it to finish and reports its code.
|
|
132
|
+
const ignore = () => { };
|
|
133
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
|
134
|
+
for (const signal of signals)
|
|
135
|
+
process.on(signal, ignore);
|
|
136
|
+
const settle = (code) => {
|
|
137
|
+
for (const signal of signals)
|
|
138
|
+
process.off(signal, ignore);
|
|
139
|
+
done(code);
|
|
140
|
+
};
|
|
141
|
+
child.on('error', (cause) => {
|
|
142
|
+
error(cause.code === 'ENOENT'
|
|
143
|
+
? `basalt: ${command} is not installed, and this app uses it (${project.root}).`
|
|
144
|
+
: `basalt: could not start ${command}: ${cause.message}`);
|
|
145
|
+
settle(1);
|
|
146
|
+
});
|
|
147
|
+
child.on('exit', (code, signal) => {
|
|
148
|
+
settle(code ?? (signal ? 128 + signalNumber(signal) : 1));
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function signalNumber(signal) {
|
|
153
|
+
return { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15 }[signal] ?? 1;
|
|
154
|
+
}
|
package/dist/stubs.d.ts
CHANGED
|
@@ -22,9 +22,12 @@ export declare const DOCKERFILE = "# syntax=docker/dockerfile:1\n# Production im
|
|
|
22
22
|
* private keys and VCS metadata into an image layer. Every secret rule is
|
|
23
23
|
* \`**\/\`-prefixed: .dockerignore patterns are anchored at the context root, so
|
|
24
24
|
* a bare \`.env\` or \`*.pem\` would still let \`prisma/.env\` or
|
|
25
|
-
* \`certs/server.key\` through. \`
|
|
25
|
+
* \`certs/server.key\` through. \`.basalt\` and \`backups\` at the root (the
|
|
26
|
+
* deploy's state and database dumps — anchored, so an app's own \`backups\`
|
|
27
|
+
* module is still copied) never belong in an image. \`dist\` is rebuilt
|
|
28
|
+
* inside the image.
|
|
26
29
|
*/
|
|
27
|
-
export declare const DOCKERIGNORE = "**/.env\n**/.env.*\n!**/.env.example\n**/.npmrc\n**/.git\n**/node_modules\n**/coverage\n**/*.log\n**/*.pem\n**/*.key\n**/*.p12\n**/*.pfx\n**/.DS_Store\ndist\n";
|
|
30
|
+
export declare const DOCKERIGNORE = "**/.env\n**/.env.*\n!**/.env.example\n**/.npmrc\n**/.git\n**/node_modules\n**/coverage\n**/*.log\n**/*.pem\n**/*.key\n**/*.p12\n**/*.pfx\n**/.DS_Store\n.basalt\nbackups\ndist\n";
|
|
28
31
|
/** GitHub Actions CI: install, typecheck, build (the production path), test. */
|
|
29
32
|
export declare const CI_WORKFLOW = "name: ci\non:\n push: { branches: [main] }\n pull_request:\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n # Drop `with: version` if package.json declares a packageManager.\n - uses: pnpm/action-setup@v4\n with: { version: 11 }\n - uses: actions/setup-node@v4\n with: { node-version: 22, cache: pnpm }\n - run: pnpm install --frozen-lockfile\n - run: pnpm run typecheck\n - run: pnpm run build\n - run: pnpm run test\n";
|
|
30
33
|
export declare const EDITORCONFIG = "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n";
|
package/dist/stubs.js
CHANGED
|
@@ -73,7 +73,10 @@ CMD ["node", "--enable-source-maps", "${PRODUCTION_ENTRY}"]
|
|
|
73
73
|
* private keys and VCS metadata into an image layer. Every secret rule is
|
|
74
74
|
* \`**\/\`-prefixed: .dockerignore patterns are anchored at the context root, so
|
|
75
75
|
* a bare \`.env\` or \`*.pem\` would still let \`prisma/.env\` or
|
|
76
|
-
* \`certs/server.key\` through. \`
|
|
76
|
+
* \`certs/server.key\` through. \`.basalt\` and \`backups\` at the root (the
|
|
77
|
+
* deploy's state and database dumps — anchored, so an app's own \`backups\`
|
|
78
|
+
* module is still copied) never belong in an image. \`dist\` is rebuilt
|
|
79
|
+
* inside the image.
|
|
77
80
|
*/
|
|
78
81
|
export const DOCKERIGNORE = `**/.env
|
|
79
82
|
**/.env.*
|
|
@@ -88,6 +91,8 @@ export const DOCKERIGNORE = `**/.env
|
|
|
88
91
|
**/*.p12
|
|
89
92
|
**/*.pfx
|
|
90
93
|
**/.DS_Store
|
|
94
|
+
.basalt
|
|
95
|
+
backups
|
|
91
96
|
dist
|
|
92
97
|
`;
|
|
93
98
|
/** GitHub Actions CI: install, typecheck, build (the production path), test. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.5.0"
|
|
6
6
|
},
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"import": "./dist/index.js"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"basalt": "./dist/bin.js"
|
|
19
|
+
},
|
|
17
20
|
"files": [
|
|
18
21
|
"dist"
|
|
19
22
|
],
|
|
@@ -22,9 +25,9 @@
|
|
|
22
25
|
"@basaltkit/http": "^3.0.0"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
|
-
"@types/node": "^26.6.
|
|
28
|
+
"@types/node": "^26.6.5",
|
|
26
29
|
"typescript": "^7.0.2",
|
|
27
|
-
"vitest": "^5.0.
|
|
30
|
+
"vitest": "^5.0.5",
|
|
28
31
|
"@basaltkit/tsconfig": "^0.24.0"
|
|
29
32
|
},
|
|
30
33
|
"publishConfig": {
|