@starci/cli 1.0.0 → 1.1.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/CHANGELOG.md +4 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/completions/_starci +65 -50
- package/completions/starci.bash +61 -46
- package/completions/starci.fish +68 -45
- package/completions/starci.ps1 +61 -46
- package/package.json +5 -3
- package/src/catalog.generated.mjs +490 -2596
- package/src/help.mjs +7 -8
- package/src/main.mjs +133 -126
- package/src/runtime-install.generated.mjs +2 -0
- package/src/runtime-install.mjs +6 -4
- package/src/runtime-locate.mjs +13 -0
- package/src/shim.mjs +10 -2
- package/src/validate-args.mjs +83 -53
- package/src/package-entry.spec.mjs +0 -83
package/src/help.mjs
CHANGED
|
@@ -14,10 +14,10 @@ const rows = (entries) => {
|
|
|
14
14
|
|
|
15
15
|
export function topHelp(catalog, version = null) {
|
|
16
16
|
const groups = Object.entries(catalog.groups ?? {}).map(([name, group]) => [name, group.summary ?? '']);
|
|
17
|
-
const options = (catalog.global ?? []).map((flag) => [`--${flag.name}
|
|
17
|
+
const options = (catalog.global ?? []).map((flag) => [`--${flag.name}` + (flag.type === 'boolean' ? '' : ` <${flag.name}>`), flag.summary ?? '']);
|
|
18
18
|
const explainUsage = (catalog.commands ?? []).includes('explain') ? [' starci explain <group> <verb>'] : [];
|
|
19
19
|
return [
|
|
20
|
-
`starci${version ?
|
|
20
|
+
`starci${version ? ' ' + version : ''} - one CLI for apps and the StarCi runtime`,
|
|
21
21
|
'',
|
|
22
22
|
'Usage: starci <group> <verb> [options]',
|
|
23
23
|
' starci help',
|
|
@@ -55,7 +55,9 @@ export function verbHelp(catalog, groupName, verbName) {
|
|
|
55
55
|
const verb = catalog.groups?.[groupName]?.verbs?.[verbName];
|
|
56
56
|
if (!verb) return null;
|
|
57
57
|
const local = (verb.flags ?? []).map((flag) => {
|
|
58
|
-
|
|
58
|
+
let value = ` <${flag.name}>`;
|
|
59
|
+
if (flag.type === 'boolean') value = '';
|
|
60
|
+
else if (flag.type === 'enum') value = ` <${(flag.enum ?? []).join('|')}>`;
|
|
59
61
|
const required = flag.required ? ' (required)' : '';
|
|
60
62
|
return [`--${flag.name}${value}`, `${flag.summary ?? ''}${required}`];
|
|
61
63
|
});
|
|
@@ -75,10 +77,7 @@ export function verbHelp(catalog, groupName, verbName) {
|
|
|
75
77
|
].join('\n');
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
/** Full verb help
|
|
80
|
+
/** Full verb help for a catalogued verb; null when the verb is unknown. */
|
|
79
81
|
export function explainHelp(catalog, groupName, verbName) {
|
|
80
|
-
|
|
81
|
-
if (!verb) return null;
|
|
82
|
-
const help = verbHelp(catalog, groupName, verbName).trimEnd();
|
|
83
|
-
return `${help}${verb.removed?.length ? `\n\nReplaces: ${verb.removed.join(', ')}` : ''}\n`;
|
|
82
|
+
return verbHelp(catalog, groupName, verbName);
|
|
84
83
|
}
|
package/src/main.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { locateRuntime, runtimeEntryOf } from './runtime-locate.mjs';
|
|
6
|
+
import { foreignRuntimeNotice, locateRuntime, ownRuntimeRoot, runtimeEntryOf } from './runtime-locate.mjs';
|
|
7
7
|
import { runtimeEnv } from './shim.mjs';
|
|
8
8
|
|
|
9
9
|
const packageFile = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
@@ -63,27 +63,6 @@ const guardFastPath = async (argv, io) => {
|
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
// `starci <old>` spellings match from the program name; the retired `hfs` and `starci-test-stack` bins also match behind it.
|
|
67
|
-
const RETIRED_BINS = new Set(['hfs', 'starci-test-stack']);
|
|
68
|
-
const retiredMatch = (words, retired) => {
|
|
69
|
-
const input = ['starci', ...words];
|
|
70
|
-
const candidates = retired
|
|
71
|
-
.map((entry) => {
|
|
72
|
-
const tokens = entry.spelling.split(/\s+/);
|
|
73
|
-
if (tokens[0] === 'starci') return { ...entry, tokens, offset: 0 };
|
|
74
|
-
return RETIRED_BINS.has(tokens[0]) ? { ...entry, tokens, offset: 1 } : null;
|
|
75
|
-
})
|
|
76
|
-
.filter((entry) => entry && entry.tokens.every((token, index) => input[index + entry.offset] === token))
|
|
77
|
-
.sort((a, b) => b.tokens.length - a.tokens.length);
|
|
78
|
-
const match = candidates[0];
|
|
79
|
-
if (!match) return null;
|
|
80
|
-
const suffix = input.slice(match.offset + match.tokens.length);
|
|
81
|
-
return {
|
|
82
|
-
spelling: [...input.slice(0, match.offset), ...match.tokens, ...suffix].join(' '),
|
|
83
|
-
use: [...match.use.split(/\s+/), ...suffix].join(' '),
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
|
|
87
66
|
const handlerArgs = (validated, command, withFlagsBeforeDashes, { includeQuiet = true } = {}) => {
|
|
88
67
|
const extra = [];
|
|
89
68
|
if (validated.global.json === true && command.json !== 'always') extra.push('--json');
|
|
@@ -92,118 +71,96 @@ const handlerArgs = (validated, command, withFlagsBeforeDashes, { includeQuiet =
|
|
|
92
71
|
return withFlagsBeforeDashes(validated.localArgs, extra);
|
|
93
72
|
};
|
|
94
73
|
|
|
95
|
-
|
|
96
|
-
export async function main(argv = process.argv.slice(2), io = {}) {
|
|
97
|
-
const guarded = await guardFastPath(argv, io);
|
|
98
|
-
if (guarded !== null) return guarded;
|
|
99
|
-
const [catalogModule, completionModule, helpModule, installModule, linkModule, argsModule] = await Promise.all([
|
|
100
|
-
import('./catalog.generated.mjs'),
|
|
101
|
-
import('./completion.mjs'),
|
|
102
|
-
import('./help.mjs'),
|
|
103
|
-
import('./runtime-install.mjs'),
|
|
104
|
-
import('./runtime-link.mjs'),
|
|
105
|
-
import('./validate-args.mjs'),
|
|
106
|
-
]);
|
|
107
|
-
const { CATALOG, RETIRED } = catalogModule;
|
|
108
|
-
const { completionFor, completionShells } = completionModule;
|
|
109
|
-
const { explainHelp, groupHelp, topHelp, verbHelp } = helpModule;
|
|
110
|
-
const { installRuntime } = installModule;
|
|
111
|
-
const { linkRuntime } = linkModule;
|
|
112
|
-
const { splitCommand, validateArgs, withFlagsBeforeDashes } = argsModule;
|
|
113
|
-
const stdout = io.stdout ?? process.stdout;
|
|
114
|
-
const stderr = io.stderr ?? process.stderr;
|
|
115
|
-
const catalog = io.catalog ?? CATALOG;
|
|
116
|
-
const retired = io.retired ?? RETIRED;
|
|
117
|
-
const version = io.version ?? cliVersion();
|
|
118
|
-
const home = io.home;
|
|
119
|
-
const env = io.env ?? process.env;
|
|
120
|
-
const initialCwd = io.cwd ?? process.cwd();
|
|
74
|
+
const ioCtx = (io) => ({ io, stdout: io.stdout ?? process.stdout, stderr: io.stderr ?? process.stderr, env: io.env ?? process.env, home: io.home });
|
|
121
75
|
|
|
76
|
+
/** Bare `starci`, `help`/`--help`/`-h` and `--version`; null when argv needs the dispatcher. */
|
|
77
|
+
const earlyOut = (argv, { catalog, version, help, stdout }) => {
|
|
122
78
|
if (argv.length === 0 || (argv.length === 1 && ['help', '--help', '-h'].includes(argv[0]))) {
|
|
123
|
-
writeTo(stdout, topHelp(catalog, version));
|
|
79
|
+
writeTo(stdout, help.topHelp(catalog, version));
|
|
124
80
|
return 0;
|
|
125
81
|
}
|
|
126
82
|
if (argv.length === 1 && argv[0] === '--version') {
|
|
127
83
|
writeTo(stdout, `${version}\n`);
|
|
128
84
|
return 0;
|
|
129
85
|
}
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
130
88
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
89
|
+
/** `starci help [group] [verb]`; null when argv is not a help invocation. */
|
|
90
|
+
const helpDispatch = (split, { catalog, version, help, stdout, stderr }) => {
|
|
91
|
+
if (split.group !== 'help') return null;
|
|
92
|
+
const verbName = split.args.find((token) => !token.startsWith('-'));
|
|
93
|
+
const target = split.verb ? catalog.groups?.[split.verb] : null;
|
|
94
|
+
if (split.verb && !target) return fail(stderr, `unknown group "${split.verb}"`);
|
|
95
|
+
if (verbName && !target.verbs?.[verbName]) return fail(stderr, `unknown verb "${verbName}" for group "${split.verb}"`);
|
|
96
|
+
let text;
|
|
97
|
+
if (!split.verb) text = help.topHelp(catalog, version);
|
|
98
|
+
else if (verbName) text = help.verbHelp(catalog, split.verb, verbName);
|
|
99
|
+
else text = help.groupHelp(catalog, split.verb);
|
|
100
|
+
writeTo(stdout, text);
|
|
101
|
+
return 0;
|
|
102
|
+
};
|
|
134
103
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
writeTo(stdout,
|
|
104
|
+
/** `starci completion <shell>`; null when argv is not a completion invocation. */
|
|
105
|
+
const completionDispatch = (split, { io, completionFor, completionShells, stdout, stderr }) => {
|
|
106
|
+
if (split.group !== 'completion') return null;
|
|
107
|
+
const shell = split.verb;
|
|
108
|
+
if (split.help && !split.args.length) {
|
|
109
|
+
writeTo(stdout, `Usage: starci completion <${completionShells.join('|')}>\n`);
|
|
141
110
|
return 0;
|
|
142
111
|
}
|
|
143
|
-
if (split.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
writeTo(stdout,
|
|
112
|
+
if (!completionShells.includes(shell) || split.args.length) return fail(stderr, `completion expects one of: ${completionShells.join(', ')}`);
|
|
113
|
+
const text = (io.completionFor ?? completionFor)(shell);
|
|
114
|
+
if (text == null) return fail(stderr, `completion expects one of: ${completionShells.join(', ')}`);
|
|
115
|
+
writeTo(stdout, text);
|
|
116
|
+
return 0;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** `starci explain <group> <verb>`; null when argv is not explain or the catalog lacks the command. */
|
|
120
|
+
const explainDispatch = (split, { catalog, help, stdout, stderr }) => {
|
|
121
|
+
if (split.group !== 'explain' || !(catalog.commands ?? []).includes('explain')) return null;
|
|
122
|
+
if (split.help && !split.verb && !split.args.length) {
|
|
123
|
+
writeTo(stdout, 'Usage: starci explain <group> <verb>\n');
|
|
155
124
|
return 0;
|
|
156
125
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const target = catalog.groups?.[groupName];
|
|
166
|
-
if (!target) return fail(stderr, `unknown group "${groupName}"`);
|
|
167
|
-
const verbName = verbNames[0];
|
|
168
|
-
if (!target.verbs?.[verbName]) {
|
|
169
|
-
return fail(stderr, `unknown verb "${verbName}" for group "${groupName}" (available: ${Object.keys(target.verbs ?? {}).join(', ')})`);
|
|
170
|
-
}
|
|
171
|
-
writeTo(stdout, explainHelp(catalog, groupName, verbName));
|
|
172
|
-
return 0;
|
|
126
|
+
const groupName = split.verb;
|
|
127
|
+
const verbNames = split.args.filter((token) => !token.startsWith('-'));
|
|
128
|
+
if (!groupName || verbNames.length !== 1) return fail(stderr, 'explain expects <group> <verb>');
|
|
129
|
+
const target = catalog.groups?.[groupName];
|
|
130
|
+
if (!target) return fail(stderr, `unknown group "${groupName}"`);
|
|
131
|
+
const verbName = verbNames[0];
|
|
132
|
+
if (!target.verbs?.[verbName]) {
|
|
133
|
+
return fail(stderr, `unknown verb "${verbName}" for group "${groupName}" (available: ${Object.keys(target.verbs ?? {}).join(', ')})`);
|
|
173
134
|
}
|
|
135
|
+
writeTo(stdout, help.explainHelp(catalog, groupName, verbName));
|
|
136
|
+
return 0;
|
|
137
|
+
};
|
|
174
138
|
|
|
139
|
+
/** Resolve group/verb against the catalog (printing help or failing); {command, group} or {code}. */
|
|
140
|
+
const resolveCommand = (split, { catalog, version, help, stdout, stderr }) => {
|
|
175
141
|
const wantsHelp = split.help;
|
|
176
142
|
if (!split.group) {
|
|
177
|
-
if (wantsHelp) {
|
|
178
|
-
|
|
179
|
-
return 0;
|
|
180
|
-
}
|
|
181
|
-
return fail(stderr, 'missing command group');
|
|
143
|
+
if (wantsHelp) { writeTo(stdout, help.topHelp(catalog, version)); return { code: 0 }; }
|
|
144
|
+
return { code: fail(stderr, 'missing command group') };
|
|
182
145
|
}
|
|
183
146
|
const group = catalog.groups?.[split.group];
|
|
184
|
-
if (!group) return fail(stderr, `unknown group "${split.group}"`);
|
|
147
|
+
if (!group) return { code: fail(stderr, `unknown group "${split.group}"`) };
|
|
185
148
|
if (!split.verb) {
|
|
186
|
-
if (wantsHelp) {
|
|
187
|
-
|
|
188
|
-
return 0;
|
|
189
|
-
}
|
|
190
|
-
return fail(stderr, `missing verb for group "${split.group}"`);
|
|
149
|
+
if (wantsHelp) { writeTo(stdout, help.groupHelp(catalog, split.group)); return { code: 0 }; }
|
|
150
|
+
return { code: fail(stderr, `missing verb for group "${split.group}"`) };
|
|
191
151
|
}
|
|
192
152
|
const command = group.verbs?.[split.verb];
|
|
193
|
-
if (!command) return fail(stderr, `unknown verb "${split.verb}" for group "${split.group}"`);
|
|
194
|
-
if (wantsHelp) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const validated = validateArgs(split.args, { ...command, group: split.group, verb: split.verb }, catalog.global ?? []);
|
|
200
|
-
if (!validated.ok) return fail(stderr, validated.error, validated.code);
|
|
201
|
-
const cwd = path.resolve(initialCwd, validated.global.cwd ?? '.');
|
|
202
|
-
const args = handlerArgs(validated, command, withFlagsBeforeDashes);
|
|
153
|
+
if (!command) return { code: fail(stderr, `unknown verb "${split.verb}" for group "${split.group}"`) };
|
|
154
|
+
if (wantsHelp) { writeTo(stdout, help.verbHelp(catalog, split.group, split.verb)); return { code: 0 }; }
|
|
155
|
+
return { command, group };
|
|
156
|
+
};
|
|
203
157
|
|
|
204
|
-
|
|
158
|
+
/** `starci runtime install|link`; null for any other verb. */
|
|
159
|
+
const runtimeVerb = (split, validated, { io, installRuntime, linkRuntime, home, cwd, stdout, stderr }) => {
|
|
160
|
+
if (split.group !== 'runtime') return null;
|
|
161
|
+
if (split.verb === 'install') {
|
|
205
162
|
const install = io.installRuntime ?? installRuntime;
|
|
206
|
-
return
|
|
163
|
+
return install({
|
|
207
164
|
cwd,
|
|
208
165
|
...(home ? { home } : {}),
|
|
209
166
|
force: validated.values.force === true,
|
|
@@ -211,10 +168,9 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
211
168
|
noBootstrap: validated.values['no-bootstrap'] === true,
|
|
212
169
|
}, io.runtimeInstallDeps ?? {});
|
|
213
170
|
}
|
|
214
|
-
|
|
215
|
-
if (split.group === 'runtime' && split.verb === 'link') {
|
|
171
|
+
if (split.verb === 'link') {
|
|
216
172
|
const link = io.linkRuntime ?? linkRuntime;
|
|
217
|
-
return
|
|
173
|
+
return link({
|
|
218
174
|
cwd,
|
|
219
175
|
...(home ? { home } : {}),
|
|
220
176
|
root: validated.values.root ?? null,
|
|
@@ -224,26 +180,34 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
224
180
|
stderr,
|
|
225
181
|
}, io.runtimeLinkDeps ?? {});
|
|
226
182
|
}
|
|
183
|
+
return null;
|
|
184
|
+
};
|
|
227
185
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
186
|
+
/** A group owned by @starci/hfs runs in-process; null for runtime-owned groups. */
|
|
187
|
+
const hfsDispatch = async (split, group, args, { io, cwd, stdout, stderr }) => {
|
|
188
|
+
if (group.owner !== '@starci/hfs') return null;
|
|
189
|
+
try {
|
|
190
|
+
const hfs = await (io.importHfs ?? importHfs)();
|
|
191
|
+
const entry = hfs.main ?? hfs.default?.main;
|
|
192
|
+
if (typeof entry !== 'function') return fail(stderr, '@starci/hfs does not export main(argv, io)', 1);
|
|
193
|
+
return Number(await entry([split.verb, ...args], {
|
|
194
|
+
...io,
|
|
195
|
+
cwd,
|
|
196
|
+
stdout: (text) => writeTo(stdout, text),
|
|
197
|
+
stderr: (text) => writeTo(stderr, text),
|
|
198
|
+
})) || 0;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return fail(stderr, `cannot load @starci/hfs: ${error?.message ?? error}`, 1);
|
|
242
201
|
}
|
|
202
|
+
};
|
|
243
203
|
|
|
204
|
+
/** Spawn the located runtime for the resolved verb; its exit status is the command's. */
|
|
205
|
+
const runRuntime = (split, args, { io, cwd, env, home, stderr }) => {
|
|
244
206
|
const skipped = [];
|
|
245
207
|
const located = (io.locateRuntime ?? locateRuntime)({ cwd, env, skipped, ...(home ? { home } : {}) });
|
|
246
208
|
if (!located) return noRuntime(stderr, split.group, skipped);
|
|
209
|
+
const notice = foreignRuntimeNotice(located, (io.ownRuntimeRoot ?? ownRuntimeRoot)());
|
|
210
|
+
if (notice) writeTo(stderr, notice);
|
|
247
211
|
const spawn = io.spawn ?? spawnSync;
|
|
248
212
|
let result;
|
|
249
213
|
try {
|
|
@@ -262,4 +226,47 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
262
226
|
}
|
|
263
227
|
if (result?.status == null) return fail(stderr, `the runtime stopped on signal ${result?.signal ?? 'unknown'}`, 1);
|
|
264
228
|
return result.status;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/** The published CLI entry, with I/O and process seams for deterministic specs. */
|
|
232
|
+
export async function main(argv = process.argv.slice(2), io = {}) {
|
|
233
|
+
const guarded = await guardFastPath(argv, io);
|
|
234
|
+
if (guarded !== null) return guarded;
|
|
235
|
+
const [catalogModule, completionModule, helpModule, installModule, linkModule, argsModule] = await Promise.all([
|
|
236
|
+
import('./catalog.generated.mjs'),
|
|
237
|
+
import('./completion.mjs'),
|
|
238
|
+
import('./help.mjs'),
|
|
239
|
+
import('./runtime-install.mjs'),
|
|
240
|
+
import('./runtime-link.mjs'),
|
|
241
|
+
import('./validate-args.mjs'),
|
|
242
|
+
]);
|
|
243
|
+
const catalog = io.catalog ?? catalogModule.CATALOG;
|
|
244
|
+
const ctx = {
|
|
245
|
+
...ioCtx(io), catalog, version: io.version ?? cliVersion(), initialCwd: io.cwd ?? process.cwd(), globals: catalog.global ?? [],
|
|
246
|
+
help: helpModule, completionFor: completionModule.completionFor, completionShells: completionModule.completionShells,
|
|
247
|
+
installRuntime: installModule.installRuntime, linkRuntime: linkModule.linkRuntime,
|
|
248
|
+
};
|
|
249
|
+
const { splitCommand, validateArgs, withFlagsBeforeDashes } = argsModule;
|
|
250
|
+
|
|
251
|
+
const early = earlyOut(argv, ctx);
|
|
252
|
+
if (early !== null) return early;
|
|
253
|
+
const split = splitCommand(argv, ctx.globals);
|
|
254
|
+
for (const dispatch of [helpDispatch, completionDispatch, explainDispatch]) {
|
|
255
|
+
const out = dispatch(split, ctx);
|
|
256
|
+
if (out !== null) return out;
|
|
257
|
+
}
|
|
258
|
+
const resolved = resolveCommand(split, ctx);
|
|
259
|
+
if (!resolved.command) return resolved.code;
|
|
260
|
+
const { command, group } = resolved;
|
|
261
|
+
|
|
262
|
+
const validated = validateArgs(split.args, { ...command, group: split.group, verb: split.verb }, ctx.globals);
|
|
263
|
+
if (!validated.ok) return fail(ctx.stderr, validated.error, validated.code);
|
|
264
|
+
const cwd = path.resolve(ctx.initialCwd, validated.global.cwd ?? '.');
|
|
265
|
+
const args = handlerArgs(validated, command, withFlagsBeforeDashes);
|
|
266
|
+
|
|
267
|
+
const runtimeOut = await runtimeVerb(split, validated, { ...ctx, cwd });
|
|
268
|
+
if (runtimeOut !== null) return runtimeOut;
|
|
269
|
+
const hfsOut = await hfsDispatch(split, group, args, { ...ctx, cwd });
|
|
270
|
+
if (hfsOut !== null) return hfsOut;
|
|
271
|
+
return runRuntime(split, args, { ...ctx, cwd });
|
|
265
272
|
}
|
package/src/runtime-install.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { writeRuntimeShim } from './shim.mjs';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import { RUNTIME_VERSION } from './runtime-install.generated.mjs';
|
|
10
|
+
export { RUNTIME_VERSION };
|
|
10
11
|
|
|
11
12
|
const writeTo = (target, text) => {
|
|
12
13
|
if (typeof target === 'function') target(text);
|
|
@@ -19,8 +20,8 @@ const npmEntryCandidates = ({ execPath = process.execPath, env = process.env } =
|
|
|
19
20
|
if (typeof env?.npm_execpath === 'string' && path.basename(env.npm_execpath).toLowerCase() === 'npm-cli.js') {
|
|
20
21
|
candidates.push(path.resolve(env.npm_execpath));
|
|
21
22
|
}
|
|
22
|
-
candidates.push(path.join(nodeDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
|
23
|
-
|
|
23
|
+
candidates.push(path.join(nodeDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
24
|
+
path.join(path.dirname(nodeDirectory), 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
24
25
|
return [...new Set(candidates)];
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -105,6 +106,7 @@ export function installRuntime({ cwd = process.cwd(), home = os.homedir(), force
|
|
|
105
106
|
const installStatus = statusOf(installed, 'runtime installer', report);
|
|
106
107
|
if (installStatus !== 0) return installStatus;
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
// The download cache supplies the installer; runtime commands must read the installed host's config and bindings.
|
|
110
|
+
writeRuntimeShim({ root: path.resolve(cwd, '.claude'), home, cli: fileURLToPath(new URL('../bin/starci.mjs', import.meta.url)) }, deps);
|
|
109
111
|
return 0;
|
|
110
112
|
}
|
package/src/runtime-locate.mjs
CHANGED
|
@@ -51,4 +51,17 @@ export function locateRuntime({ cwd = process.cwd(), env = process.env, home = o
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** The runtime root of the checkout this CLI file lives in, or null when that checkout is not a runtime root (a published install). */
|
|
55
|
+
export const ownRuntimeRoot = ({ exists = existsSync, embeddedRoot = embeddedRuntimeRoot } = {}) => usableRoot(embeddedRoot, exists);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The one stderr line naming a located runtime that is not the checkout the CLI runs from, or null when they are the same
|
|
59
|
+
* or the CLI lives outside a runtime. The record and STARCI_RUNTIME outrank the checkout, so the mismatch is said, never silent.
|
|
60
|
+
*/
|
|
61
|
+
export function foreignRuntimeNotice(located, own) {
|
|
62
|
+
if (!located || !own) return null;
|
|
63
|
+
const same = process.platform === 'win32' ? located.root.toLowerCase() === own.toLowerCase() : located.root === own;
|
|
64
|
+
return same ? null : `starci: running the runtime at ${located.root} (${located.source}), not this checkout's ${own}; set STARCI_RUNTIME=${own} to use the checkout\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
export const runtimeEntryOf = runtimeEntry;
|
package/src/shim.mjs
CHANGED
|
@@ -10,6 +10,12 @@ const wrapperName = (program, platform) => platform === 'win32' ? `${program}.cm
|
|
|
10
10
|
const wrapperText = ({ program, platform, node, cli }) => platform === 'win32'
|
|
11
11
|
? `@echo off\r\n"${node}" "${cli}" guard raw ${program} -- %*\r\n`
|
|
12
12
|
: `#!/bin/sh\nexec "${node}" "${cli}" guard raw ${program} -- "$@"\n`;
|
|
13
|
+
// The extensionless launcher bash runs (scripts/lib/guard-command.mjs spells the hook command that names it); on Windows
|
|
14
|
+
// its paths use forward slashes, which bash and node both accept.
|
|
15
|
+
const launcherText = ({ platform, node, cli }) => {
|
|
16
|
+
const spelled = (file) => platform === 'win32' ? file.replaceAll('\\', '/') : file;
|
|
17
|
+
return `#!/bin/sh\nexec "${spelled(node)}" "${spelled(cli)}" "$@"\n`;
|
|
18
|
+
};
|
|
13
19
|
|
|
14
20
|
/** Write the per-user runtime record, StarCi launcher and guarded PATH tool wrappers for that runtime. */
|
|
15
21
|
export function writeRuntimeShim({ root, home = os.homedir(), cli: launcher = null } = {}, deps = {}) {
|
|
@@ -29,14 +35,16 @@ export function writeRuntimeShim({ root, home = os.homedir(), cli: launcher = nu
|
|
|
29
35
|
if (platform === 'win32') {
|
|
30
36
|
const shim = path.join(shimDir, 'starci.cmd');
|
|
31
37
|
write(shim, `@echo off\r\n"${node}" "${cli}" %*\r\n`);
|
|
38
|
+
const posixShim = path.join(shimDir, 'starci');
|
|
39
|
+
write(posixShim, launcherText({ platform, node, cli }));
|
|
32
40
|
for (const program of TOOL_WRAPPERS) {
|
|
33
41
|
write(path.join(shimDir, wrapperName(program, platform)), wrapperText({ program, platform, node, cli }));
|
|
34
42
|
}
|
|
35
|
-
return { root: runtimeRoot, runtimeJson, shim };
|
|
43
|
+
return { root: runtimeRoot, runtimeJson, shim, posixShim };
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const shim = path.join(shimDir, 'starci');
|
|
39
|
-
write(shim,
|
|
47
|
+
write(shim, launcherText({ platform, node, cli }));
|
|
40
48
|
chmod(shim, 0o755);
|
|
41
49
|
for (const program of TOOL_WRAPPERS) {
|
|
42
50
|
const wrapper = path.join(shimDir, wrapperName(program, platform));
|
package/src/validate-args.mjs
CHANGED
|
@@ -31,6 +31,84 @@ const typedValue = (flag, raw) => {
|
|
|
31
31
|
return raw;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/** One `--name[=value]` token: type-check it into values/global, or pass it through to localArgs. */
|
|
35
|
+
const applyOption = (argv, index, token, { known, values, global, localArgs }) => {
|
|
36
|
+
const name = optionName(token);
|
|
37
|
+
const flag = known.get(name);
|
|
38
|
+
if (!flag) throw new Error(`unknown option --${name}`);
|
|
39
|
+
const { value: raw, consumed } = optionValue(argv, index, flag, known);
|
|
40
|
+
const value = typedValue(flag, raw);
|
|
41
|
+
if (flag.type === 'list') {
|
|
42
|
+
values[name] = [...(values[name] ?? []), value];
|
|
43
|
+
} else if (Object.hasOwn(values, name)) {
|
|
44
|
+
throw new Error(`--${name} may be given only once`);
|
|
45
|
+
} else {
|
|
46
|
+
values[name] = value;
|
|
47
|
+
}
|
|
48
|
+
if (flag.global) { global[name] = values[name]; }
|
|
49
|
+
else {
|
|
50
|
+
localArgs.push(token);
|
|
51
|
+
if (consumed) localArgs.push(argv[index + consumed]);
|
|
52
|
+
}
|
|
53
|
+
return consumed;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Every required local option must have been given. */
|
|
57
|
+
const requiredCheck = (locals, values) => {
|
|
58
|
+
for (const flag of locals.values()) {
|
|
59
|
+
if (flag.required && !Object.hasOwn(values, flag.name)) throw new Error(`missing required option --${flag.name}`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** The values one positional slot holds: a variadic slot's list, a single value, or none. */
|
|
64
|
+
const candidatesOf = (supplied) => {
|
|
65
|
+
if (Array.isArray(supplied)) return supplied;
|
|
66
|
+
return supplied === undefined ? [] : [supplied];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** A slot with an enum accepts only its members. */
|
|
70
|
+
const checkEnum = (schema, candidates) => {
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
if (schema.enum && !schema.enum.includes(candidate)) throw new Error(`${schema.name} expects one of: ${schema.enum.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Positional arity and enum checks; passthrough tokens fill free slots first. */
|
|
77
|
+
const checkPositionals = (verb, positionals, passthrough) => {
|
|
78
|
+
const positionalSchema = verb.positional ?? [];
|
|
79
|
+
const variadicAt = positionalSchema.findIndex((entry) => entry.variadic === true);
|
|
80
|
+
const maximum = variadicAt >= 0 ? Infinity : positionalSchema.length;
|
|
81
|
+
if (positionals.length > maximum) throw new Error(`too many positional arguments for ${verb.verb ?? 'command'}`);
|
|
82
|
+
positionals.push(...passthrough.slice(0, Math.max(0, maximum - positionals.length)));
|
|
83
|
+
for (const [index, schema] of positionalSchema.entries()) {
|
|
84
|
+
const supplied = variadicAt === index ? positionals.slice(index) : positionals[index];
|
|
85
|
+
const empty = Array.isArray(supplied) ? supplied.length === 0 : supplied === undefined;
|
|
86
|
+
if (schema.required && empty) throw new Error(`missing required positional ${schema.name}`);
|
|
87
|
+
checkEnum(schema, candidatesOf(supplied));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Edition availability, --cwd emptiness and --json support. */
|
|
92
|
+
const checkGlobals = (verb, global) => {
|
|
93
|
+
if (global.edition !== undefined && Array.isArray(verb.editions) && !verb.editions.includes(global.edition)) {
|
|
94
|
+
throw new Error(`${verb.group ?? 'this command'} ${verb.verb ?? ''}`.trim() + ` is not available in the ${global.edition} edition (editions: ${verb.editions.join(', ')})`);
|
|
95
|
+
}
|
|
96
|
+
if (global.cwd === '') throw new Error('--cwd needs a value');
|
|
97
|
+
if (global.json === true && verb.json === 'none') {
|
|
98
|
+
throw new Error(`${verb.group ?? 'this command'} ${verb.verb ?? ''}`.trim() + ' has no machine output');
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** args = supplied locals, else their declared defaults (a list flag's scalar default wraps in an array). */
|
|
103
|
+
const defaultArgs = (locals, values) => {
|
|
104
|
+
const args = {};
|
|
105
|
+
for (const flag of locals.values()) {
|
|
106
|
+
if (Object.hasOwn(values, flag.name)) args[flag.name] = values[flag.name];
|
|
107
|
+
else if (flag.default !== undefined) args[flag.name] = flag.type === 'list' && !Array.isArray(flag.default) ? [flag.default] : flag.default;
|
|
108
|
+
}
|
|
109
|
+
return args;
|
|
110
|
+
};
|
|
111
|
+
|
|
34
112
|
/**
|
|
35
113
|
* Validate the arguments after a catalog group and verb.
|
|
36
114
|
*
|
|
@@ -64,24 +142,7 @@ export function validateArgs(argv, verb, globalFlags = []) {
|
|
|
64
142
|
continue;
|
|
65
143
|
}
|
|
66
144
|
if (token.startsWith('--')) {
|
|
67
|
-
|
|
68
|
-
const flag = known.get(name);
|
|
69
|
-
if (!flag) throw new Error(`unknown option --${name}`);
|
|
70
|
-
const { value: raw, consumed } = optionValue(argv, index, flag, known);
|
|
71
|
-
const value = typedValue(flag, raw);
|
|
72
|
-
index += consumed;
|
|
73
|
-
if (flag.type === 'list') {
|
|
74
|
-
values[name] = [...(values[name] ?? []), value];
|
|
75
|
-
} else if (Object.hasOwn(values, name)) {
|
|
76
|
-
throw new Error(`--${name} may be given only once`);
|
|
77
|
-
} else {
|
|
78
|
-
values[name] = value;
|
|
79
|
-
}
|
|
80
|
-
if (flag.global) global[name] = values[name];
|
|
81
|
-
else {
|
|
82
|
-
localArgs.push(token);
|
|
83
|
-
if (consumed) localArgs.push(argv[index]);
|
|
84
|
-
}
|
|
145
|
+
index += applyOption(argv, index, token, { known, values, global, localArgs });
|
|
85
146
|
continue;
|
|
86
147
|
}
|
|
87
148
|
if (token.startsWith('-')) throw new Error(`unknown option ${token}`);
|
|
@@ -89,41 +150,10 @@ export function validateArgs(argv, verb, globalFlags = []) {
|
|
|
89
150
|
localArgs.push(token);
|
|
90
151
|
}
|
|
91
152
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const positionalSchema = verb.positional ?? [];
|
|
97
|
-
const variadicAt = positionalSchema.findIndex((entry) => entry.variadic === true);
|
|
98
|
-
const maximum = variadicAt >= 0 ? Infinity : positionalSchema.length;
|
|
99
|
-
if (positionals.length > maximum) throw new Error(`too many positional arguments for ${verb.verb ?? 'command'}`);
|
|
100
|
-
positionals.push(...passthrough.slice(0, Math.max(0, maximum - positionals.length)));
|
|
101
|
-
for (let index = 0; index < positionalSchema.length; index += 1) {
|
|
102
|
-
const schema = positionalSchema[index];
|
|
103
|
-
const supplied = variadicAt === index ? positionals.slice(index) : positionals[index];
|
|
104
|
-
const empty = Array.isArray(supplied) ? supplied.length === 0 : supplied === undefined;
|
|
105
|
-
if (schema.required && empty) throw new Error(`missing required positional ${schema.name}`);
|
|
106
|
-
const candidates = Array.isArray(supplied) ? supplied : supplied === undefined ? [] : [supplied];
|
|
107
|
-
for (const candidate of candidates) {
|
|
108
|
-
if (schema.enum && !schema.enum.includes(candidate)) {
|
|
109
|
-
throw new Error(`${schema.name} expects one of: ${schema.enum.join(', ')}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (global.edition !== undefined && Array.isArray(verb.editions) && !verb.editions.includes(global.edition)) {
|
|
115
|
-
throw new Error(`${verb.group ?? 'this command'} ${verb.verb ?? ''}`.trim() + ` is not available in the ${global.edition} edition (editions: ${verb.editions.join(', ')})`);
|
|
116
|
-
}
|
|
117
|
-
if (global.cwd === '') throw new Error('--cwd needs a value');
|
|
118
|
-
if (global.json === true && verb.json === 'none') {
|
|
119
|
-
throw new Error(`${verb.group ?? 'this command'} ${verb.verb ?? ''}`.trim() + ' has no machine output');
|
|
120
|
-
}
|
|
121
|
-
const args = {};
|
|
122
|
-
for (const flag of locals.values()) {
|
|
123
|
-
if (Object.hasOwn(values, flag.name)) args[flag.name] = values[flag.name];
|
|
124
|
-
else if (flag.default !== undefined) args[flag.name] = flag.type === 'list' && !Array.isArray(flag.default) ? [flag.default] : flag.default;
|
|
125
|
-
}
|
|
126
|
-
return { ok: true, values, args, global, localArgs, positionals };
|
|
153
|
+
requiredCheck(locals, values);
|
|
154
|
+
checkPositionals(verb, positionals, passthrough);
|
|
155
|
+
checkGlobals(verb, global);
|
|
156
|
+
return { ok: true, values, args: defaultArgs(locals, values), global, localArgs, positionals };
|
|
127
157
|
} catch (error) {
|
|
128
158
|
return { ok: false, code: 2, error: String(error?.message ?? error) };
|
|
129
159
|
}
|