@starci/cli 1.0.1 → 1.2.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 +8 -0
- package/completions/_starci +145 -45
- package/completions/starci.bash +143 -43
- package/completions/starci.fish +161 -31
- package/completions/starci.ps1 +143 -43
- package/package.json +2 -2
- package/src/catalog.generated.mjs +1825 -2720
- package/src/help.mjs +7 -8
- package/src/main.mjs +141 -128
- package/src/runtime-install.generated.mjs +1 -1
- package/src/runtime-install.mjs +2 -2
- package/src/runtime-locate.mjs +13 -0
- package/src/runtime-target.mjs +41 -0
- package/src/shim.mjs +10 -2
- package/src/validate-args.mjs +96 -53
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,8 @@ 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 {
|
|
6
|
+
import { retargetNotice, targetOf, treeTargetRefusal } from './runtime-target.mjs';
|
|
7
|
+
import { foreignRuntimeNotice, locateRuntime, ownRuntimeRoot, runtimeEntryOf } from './runtime-locate.mjs';
|
|
7
8
|
import { runtimeEnv } from './shim.mjs';
|
|
8
9
|
|
|
9
10
|
const packageFile = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
@@ -63,27 +64,6 @@ const guardFastPath = async (argv, io) => {
|
|
|
63
64
|
}
|
|
64
65
|
};
|
|
65
66
|
|
|
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
67
|
const handlerArgs = (validated, command, withFlagsBeforeDashes, { includeQuiet = true } = {}) => {
|
|
88
68
|
const extra = [];
|
|
89
69
|
if (validated.global.json === true && command.json !== 'always') extra.push('--json');
|
|
@@ -92,118 +72,96 @@ const handlerArgs = (validated, command, withFlagsBeforeDashes, { includeQuiet =
|
|
|
92
72
|
return withFlagsBeforeDashes(validated.localArgs, extra);
|
|
93
73
|
};
|
|
94
74
|
|
|
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();
|
|
75
|
+
const ioCtx = (io) => ({ io, stdout: io.stdout ?? process.stdout, stderr: io.stderr ?? process.stderr, env: io.env ?? process.env, home: io.home });
|
|
121
76
|
|
|
77
|
+
/** Bare `starci`, `help`/`--help`/`-h` and `--version`; null when argv needs the dispatcher. */
|
|
78
|
+
const earlyOut = (argv, { catalog, version, help, stdout }) => {
|
|
122
79
|
if (argv.length === 0 || (argv.length === 1 && ['help', '--help', '-h'].includes(argv[0]))) {
|
|
123
|
-
writeTo(stdout, topHelp(catalog, version));
|
|
80
|
+
writeTo(stdout, help.topHelp(catalog, version));
|
|
124
81
|
return 0;
|
|
125
82
|
}
|
|
126
83
|
if (argv.length === 1 && argv[0] === '--version') {
|
|
127
84
|
writeTo(stdout, `${version}\n`);
|
|
128
85
|
return 0;
|
|
129
86
|
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
130
89
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
90
|
+
/** `starci help [group] [verb]`; null when argv is not a help invocation. */
|
|
91
|
+
const helpDispatch = (split, { catalog, version, help, stdout, stderr }) => {
|
|
92
|
+
if (split.group !== 'help') return null;
|
|
93
|
+
const verbName = split.args.find((token) => !token.startsWith('-'));
|
|
94
|
+
const target = split.verb ? catalog.groups?.[split.verb] : null;
|
|
95
|
+
if (split.verb && !target) return fail(stderr, `unknown group "${split.verb}"`);
|
|
96
|
+
if (verbName && !target.verbs?.[verbName]) return fail(stderr, `unknown verb "${verbName}" for group "${split.verb}"`);
|
|
97
|
+
let text;
|
|
98
|
+
if (!split.verb) text = help.topHelp(catalog, version);
|
|
99
|
+
else if (verbName) text = help.verbHelp(catalog, split.verb, verbName);
|
|
100
|
+
else text = help.groupHelp(catalog, split.verb);
|
|
101
|
+
writeTo(stdout, text);
|
|
102
|
+
return 0;
|
|
103
|
+
};
|
|
134
104
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
writeTo(stdout,
|
|
105
|
+
/** `starci completion <shell>`; null when argv is not a completion invocation. */
|
|
106
|
+
const completionDispatch = (split, { io, completionFor, completionShells, stdout, stderr }) => {
|
|
107
|
+
if (split.group !== 'completion') return null;
|
|
108
|
+
const shell = split.verb;
|
|
109
|
+
if (split.help && !split.args.length) {
|
|
110
|
+
writeTo(stdout, `Usage: starci completion <${completionShells.join('|')}>\n`);
|
|
141
111
|
return 0;
|
|
142
112
|
}
|
|
143
|
-
if (split.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
writeTo(stdout,
|
|
113
|
+
if (!completionShells.includes(shell) || split.args.length) return fail(stderr, `completion expects one of: ${completionShells.join(', ')}`);
|
|
114
|
+
const text = (io.completionFor ?? completionFor)(shell);
|
|
115
|
+
if (text == null) return fail(stderr, `completion expects one of: ${completionShells.join(', ')}`);
|
|
116
|
+
writeTo(stdout, text);
|
|
117
|
+
return 0;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** `starci explain <group> <verb>`; null when argv is not explain or the catalog lacks the command. */
|
|
121
|
+
const explainDispatch = (split, { catalog, help, stdout, stderr }) => {
|
|
122
|
+
if (split.group !== 'explain' || !(catalog.commands ?? []).includes('explain')) return null;
|
|
123
|
+
if (split.help && !split.verb && !split.args.length) {
|
|
124
|
+
writeTo(stdout, 'Usage: starci explain <group> <verb>\n');
|
|
155
125
|
return 0;
|
|
156
126
|
}
|
|
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;
|
|
127
|
+
const groupName = split.verb;
|
|
128
|
+
const verbNames = split.args.filter((token) => !token.startsWith('-'));
|
|
129
|
+
if (!groupName || verbNames.length !== 1) return fail(stderr, 'explain expects <group> <verb>');
|
|
130
|
+
const target = catalog.groups?.[groupName];
|
|
131
|
+
if (!target) return fail(stderr, `unknown group "${groupName}"`);
|
|
132
|
+
const verbName = verbNames[0];
|
|
133
|
+
if (!target.verbs?.[verbName]) {
|
|
134
|
+
return fail(stderr, `unknown verb "${verbName}" for group "${groupName}" (available: ${Object.keys(target.verbs ?? {}).join(', ')})`);
|
|
173
135
|
}
|
|
136
|
+
writeTo(stdout, help.explainHelp(catalog, groupName, verbName));
|
|
137
|
+
return 0;
|
|
138
|
+
};
|
|
174
139
|
|
|
140
|
+
/** Resolve group/verb against the catalog (printing help or failing); {command, group} or {code}. */
|
|
141
|
+
const resolveCommand = (split, { catalog, version, help, stdout, stderr }) => {
|
|
175
142
|
const wantsHelp = split.help;
|
|
176
143
|
if (!split.group) {
|
|
177
|
-
if (wantsHelp) {
|
|
178
|
-
|
|
179
|
-
return 0;
|
|
180
|
-
}
|
|
181
|
-
return fail(stderr, 'missing command group');
|
|
144
|
+
if (wantsHelp) { writeTo(stdout, help.topHelp(catalog, version)); return { code: 0 }; }
|
|
145
|
+
return { code: fail(stderr, 'missing command group') };
|
|
182
146
|
}
|
|
183
147
|
const group = catalog.groups?.[split.group];
|
|
184
|
-
if (!group) return fail(stderr, `unknown group "${split.group}"`);
|
|
148
|
+
if (!group) return { code: fail(stderr, `unknown group "${split.group}"`) };
|
|
185
149
|
if (!split.verb) {
|
|
186
|
-
if (wantsHelp) {
|
|
187
|
-
|
|
188
|
-
return 0;
|
|
189
|
-
}
|
|
190
|
-
return fail(stderr, `missing verb for group "${split.group}"`);
|
|
150
|
+
if (wantsHelp) { writeTo(stdout, help.groupHelp(catalog, split.group)); return { code: 0 }; }
|
|
151
|
+
return { code: fail(stderr, `missing verb for group "${split.group}"`) };
|
|
191
152
|
}
|
|
192
153
|
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);
|
|
154
|
+
if (!command) return { code: fail(stderr, `unknown verb "${split.verb}" for group "${split.group}"`) };
|
|
155
|
+
if (wantsHelp) { writeTo(stdout, help.verbHelp(catalog, split.group, split.verb)); return { code: 0 }; }
|
|
156
|
+
return { command, group };
|
|
157
|
+
};
|
|
203
158
|
|
|
204
|
-
|
|
159
|
+
/** `starci runtime install|link`; null for any other verb. */
|
|
160
|
+
const runtimeVerb = (split, validated, { io, installRuntime, linkRuntime, home, cwd, stdout, stderr }) => {
|
|
161
|
+
if (split.group !== 'runtime') return null;
|
|
162
|
+
if (split.verb === 'install') {
|
|
205
163
|
const install = io.installRuntime ?? installRuntime;
|
|
206
|
-
return
|
|
164
|
+
return install({
|
|
207
165
|
cwd,
|
|
208
166
|
...(home ? { home } : {}),
|
|
209
167
|
force: validated.values.force === true,
|
|
@@ -211,10 +169,9 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
211
169
|
noBootstrap: validated.values['no-bootstrap'] === true,
|
|
212
170
|
}, io.runtimeInstallDeps ?? {});
|
|
213
171
|
}
|
|
214
|
-
|
|
215
|
-
if (split.group === 'runtime' && split.verb === 'link') {
|
|
172
|
+
if (split.verb === 'link') {
|
|
216
173
|
const link = io.linkRuntime ?? linkRuntime;
|
|
217
|
-
return
|
|
174
|
+
return link({
|
|
218
175
|
cwd,
|
|
219
176
|
...(home ? { home } : {}),
|
|
220
177
|
root: validated.values.root ?? null,
|
|
@@ -224,26 +181,39 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
224
181
|
stderr,
|
|
225
182
|
}, io.runtimeLinkDeps ?? {});
|
|
226
183
|
}
|
|
184
|
+
return null;
|
|
185
|
+
};
|
|
227
186
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
187
|
+
/** A group owned by @starci/hfs runs in-process; null for runtime-owned groups. */
|
|
188
|
+
const hfsDispatch = async (split, group, args, { io, cwd, stdout, stderr }) => {
|
|
189
|
+
if (group.owner !== '@starci/hfs') return null;
|
|
190
|
+
try {
|
|
191
|
+
const hfs = await (io.importHfs ?? importHfs)();
|
|
192
|
+
const entry = hfs.main ?? hfs.default?.main;
|
|
193
|
+
if (typeof entry !== 'function') return fail(stderr, '@starci/hfs does not export main(argv, io)', 1);
|
|
194
|
+
return Number(await entry([split.verb, ...args], {
|
|
195
|
+
...io,
|
|
196
|
+
cwd,
|
|
197
|
+
stdout: (text) => writeTo(stdout, text),
|
|
198
|
+
stderr: (text) => writeTo(stderr, text),
|
|
199
|
+
})) || 0;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return fail(stderr, `cannot load @starci/hfs: ${error?.message ?? error}`, 1);
|
|
242
202
|
}
|
|
203
|
+
};
|
|
243
204
|
|
|
205
|
+
/** Spawn the located runtime for the resolved verb; its exit status is the command's. */
|
|
206
|
+
const runRuntime = (split, args, { io, cwd, env, home, stderr, command }) => {
|
|
244
207
|
const skipped = [];
|
|
245
|
-
const
|
|
246
|
-
if (!
|
|
208
|
+
const record = (io.locateRuntime ?? locateRuntime)({ cwd, env, skipped, ...(home ? { home } : {}) });
|
|
209
|
+
if (!record) return noRuntime(stderr, split.group, skipped);
|
|
210
|
+
const own = (io.ownRuntimeRoot ?? ownRuntimeRoot)();
|
|
211
|
+
const { located, retargeted } = targetOf({ command, args, located: record, own });
|
|
212
|
+
if (retargeted) writeTo(stderr, retargetNotice(retargeted, own));
|
|
213
|
+
const refused = treeTargetRefusal({ command, args, located, own });
|
|
214
|
+
if (refused) return fail(stderr, refused.message, 2);
|
|
215
|
+
const notice = foreignRuntimeNotice(located, own);
|
|
216
|
+
if (notice) writeTo(stderr, notice);
|
|
247
217
|
const spawn = io.spawn ?? spawnSync;
|
|
248
218
|
let result;
|
|
249
219
|
try {
|
|
@@ -262,4 +232,47 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
262
232
|
}
|
|
263
233
|
if (result?.status == null) return fail(stderr, `the runtime stopped on signal ${result?.signal ?? 'unknown'}`, 1);
|
|
264
234
|
return result.status;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** The published CLI entry, with I/O and process seams for deterministic specs. */
|
|
238
|
+
export async function main(argv = process.argv.slice(2), io = {}) {
|
|
239
|
+
const guarded = await guardFastPath(argv, io);
|
|
240
|
+
if (guarded !== null) return guarded;
|
|
241
|
+
const [catalogModule, completionModule, helpModule, installModule, linkModule, argsModule] = await Promise.all([
|
|
242
|
+
import('./catalog.generated.mjs'),
|
|
243
|
+
import('./completion.mjs'),
|
|
244
|
+
import('./help.mjs'),
|
|
245
|
+
import('./runtime-install.mjs'),
|
|
246
|
+
import('./runtime-link.mjs'),
|
|
247
|
+
import('./validate-args.mjs'),
|
|
248
|
+
]);
|
|
249
|
+
const catalog = io.catalog ?? catalogModule.CATALOG;
|
|
250
|
+
const ctx = {
|
|
251
|
+
...ioCtx(io), catalog, version: io.version ?? cliVersion(), initialCwd: io.cwd ?? process.cwd(), globals: catalog.global ?? [],
|
|
252
|
+
help: helpModule, completionFor: completionModule.completionFor, completionShells: completionModule.completionShells,
|
|
253
|
+
installRuntime: installModule.installRuntime, linkRuntime: linkModule.linkRuntime,
|
|
254
|
+
};
|
|
255
|
+
const { splitCommand, validateArgs, withFlagsBeforeDashes } = argsModule;
|
|
256
|
+
|
|
257
|
+
const early = earlyOut(argv, ctx);
|
|
258
|
+
if (early !== null) return early;
|
|
259
|
+
const split = splitCommand(argv, ctx.globals);
|
|
260
|
+
for (const dispatch of [helpDispatch, completionDispatch, explainDispatch]) {
|
|
261
|
+
const out = dispatch(split, ctx);
|
|
262
|
+
if (out !== null) return out;
|
|
263
|
+
}
|
|
264
|
+
const resolved = resolveCommand(split, ctx);
|
|
265
|
+
if (!resolved.command) return resolved.code;
|
|
266
|
+
const { command, group } = resolved;
|
|
267
|
+
|
|
268
|
+
const validated = validateArgs(split.args, { ...command, group: split.group, verb: split.verb }, ctx.globals);
|
|
269
|
+
if (!validated.ok) return fail(ctx.stderr, validated.error, validated.code);
|
|
270
|
+
const cwd = path.resolve(ctx.initialCwd, validated.global.cwd ?? '.');
|
|
271
|
+
const args = handlerArgs(validated, command, withFlagsBeforeDashes);
|
|
272
|
+
|
|
273
|
+
const runtimeOut = await runtimeVerb(split, validated, { ...ctx, cwd });
|
|
274
|
+
if (runtimeOut !== null) return runtimeOut;
|
|
275
|
+
const hfsOut = await hfsDispatch(split, group, args, { ...ctx, cwd });
|
|
276
|
+
if (hfsOut !== null) return hfsOut;
|
|
277
|
+
return runRuntime(split, args, { ...ctx, cwd, command });
|
|
265
278
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/cli/gen-catalog.mjs — do not edit
|
|
2
|
-
export const RUNTIME_VERSION = "1.0.0-alpha.
|
|
2
|
+
export const RUNTIME_VERSION = "1.0.0-alpha.9";
|
package/src/runtime-install.mjs
CHANGED
|
@@ -20,8 +20,8 @@ const npmEntryCandidates = ({ execPath = process.execPath, env = process.env } =
|
|
|
20
20
|
if (typeof env?.npm_execpath === 'string' && path.basename(env.npm_execpath).toLowerCase() === 'npm-cli.js') {
|
|
21
21
|
candidates.push(path.resolve(env.npm_execpath));
|
|
22
22
|
}
|
|
23
|
-
candidates.push(path.join(nodeDirectory, 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
|
24
|
-
|
|
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'));
|
|
25
25
|
return [...new Set(candidates)];
|
|
26
26
|
};
|
|
27
27
|
|
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;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
/** The code a verb that writes the runtime tree is refused with when it would write a tree other than the CLI's own. */
|
|
4
|
+
export const RUNTIME_TREE_FOREIGN = 'RUNTIME_TREE_TARGET_FOREIGN';
|
|
5
|
+
|
|
6
|
+
const sameRoot = (a, b) => (process.platform === 'win32' ? path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase() : path.resolve(a) === path.resolve(b));
|
|
7
|
+
|
|
8
|
+
/** Whether this invocation of `command` writes the runtime tree: its `writesRuntimeTree` is true (always), a flag name that makes it write, or `!flag` (writes unless that flag is given). */
|
|
9
|
+
function writesTree(command, args) {
|
|
10
|
+
const mark = command?.writesRuntimeTree;
|
|
11
|
+
if (mark === true) return true;
|
|
12
|
+
if (typeof mark !== 'string') return false;
|
|
13
|
+
return mark.startsWith('!') ? !args.includes(`--${mark.slice(1)}`) : args.includes(`--${mark}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The refusal of a verb that writes the runtime tree while the located runtime is not the tree that owns the running CLI and nothing chose it
|
|
18
|
+
* explicitly (STARCI_RUNTIME, or the verb's own --root). The per-user record outranks the checkout for reading, so a clone's bin would otherwise
|
|
19
|
+
* write the live tree. Null when the verb does not write the tree, the CLI is not in a runtime, or the target is explicit or the CLI's own. Pure.
|
|
20
|
+
*/
|
|
21
|
+
export function treeTargetRefusal({ command, args, located, own }) {
|
|
22
|
+
if (!located || !own || !writesTree(command, args)) return null;
|
|
23
|
+
if (located.source === 'STARCI_RUNTIME' || args.includes('--root') || sameRoot(located.root, own)) return null;
|
|
24
|
+
return { code: RUNTIME_TREE_FOREIGN, message: `${RUNTIME_TREE_FOREIGN}: this verb writes the runtime tree, and the located runtime ${located.root} (${located.source}) is not the tree ${own} that owns this CLI; set STARCI_RUNTIME=${own} (or pass --root) to say which tree to write` };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The explicit runtime choices that outrank a verb's side: STARCI_RUNTIME, or the verb's own --root. */
|
|
28
|
+
const chosenExplicitly = (located, args) => located.source === 'STARCI_RUNTIME' || args.includes('--root');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The runtime a verb runs: a tree-side verb (one whose result is about the runtime tree that owns the running bin: a check, a test selection, a receipt, a catalog
|
|
32
|
+
* regeneration) runs THIS checkout unless a tree was chosen explicitly, so its verdict is about the tree it ran in; a host-side verb runs the located runtime (the
|
|
33
|
+
* per-user record included). {located, retargeted}: `retargeted` carries the runtime the record pointed at when this checkout replaced it. Pure.
|
|
34
|
+
*/
|
|
35
|
+
export function targetOf({ command, args, located, own }) {
|
|
36
|
+
if (!located || !own || command?.runtimeSide !== 'tree' || chosenExplicitly(located, args) || sameRoot(located.root, own)) return { located, retargeted: null };
|
|
37
|
+
return { located: { root: own, source: 'own-checkout' }, retargeted: located };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The one stderr line saying a tree-side verb ran this checkout although the located runtime is another tree. */
|
|
41
|
+
export const retargetNotice = (retargeted, own) => `starci: this verb is about the runtime tree that owns this CLI, so it ran ${own}, not ${retargeted.root} (${retargeted.source}); set STARCI_RUNTIME to judge another tree\n`;
|
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));
|