@hydranium/cli 1.0.0-next.412 → 1.0.0-next.416
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 +5 -0
- package/lib/cli.js +2 -0
- package/lib/cli.js.map +1 -1
- package/lib/commands/check-skills-args.d.ts +11 -0
- package/lib/commands/check-skills-args.d.ts.map +1 -0
- package/lib/commands/check-skills-args.js +71 -0
- package/lib/commands/check-skills-args.js.map +1 -0
- package/lib/commands/generate-ast-builder.d.ts +1 -1
- package/lib/commands/generate-ast-builder.js +1 -1
- package/lib/commands/init-args.d.ts +3 -18
- package/lib/commands/init-args.d.ts.map +1 -1
- package/lib/commands/init-args.js +62 -9
- package/lib/commands/init-args.js.map +1 -1
- package/lib/commands/init-skills.d.ts +41 -0
- package/lib/commands/init-skills.d.ts.map +1 -0
- package/lib/commands/init-skills.js +110 -0
- package/lib/commands/init-skills.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +7 -0
- package/lib/commands/init-wizard.d.ts.map +1 -1
- package/lib/commands/init-wizard.js +32 -2
- package/lib/commands/init-wizard.js.map +1 -1
- package/lib/commands/init.d.ts +6 -5
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +43 -6
- package/lib/commands/init.js.map +1 -1
- package/lib/commands/skills.d.ts +22 -0
- package/lib/commands/skills.d.ts.map +1 -0
- package/lib/commands/skills.js +129 -0
- package/lib/commands/skills.js.map +1 -0
- package/package.json +26 -20
- package/src/cli.ts +2 -0
- package/src/commands/check-skills-args.ts +70 -0
- package/src/commands/generate-ast-builder.ts +1 -1
- package/src/commands/init-args.ts +64 -9
- package/src/commands/init-skills.ts +129 -0
- package/src/commands/init-wizard.ts +44 -3
- package/src/commands/init.ts +50 -6
- package/src/commands/skills.ts +146 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { exitWithUsage, helpRequested, printHelp } from './harness-args.js';
|
|
12
|
+
import { checkSkills } from './skills.js';
|
|
13
|
+
|
|
14
|
+
export const CHECK_SKILLS_HELP = [
|
|
15
|
+
'Usage: hydranium-cli check-skills [project-directory] [--from <directory>] [--skill <names>]',
|
|
16
|
+
'',
|
|
17
|
+
'Check declared compatibility of installed Hydranium and Langium skills.',
|
|
18
|
+
'Reads .agents/skills and .claude/skills in the project directory (default: cwd).',
|
|
19
|
+
'Checks every copy. Does not download, update, or execute skills.',
|
|
20
|
+
'Exits non-zero for incompatible, missing, malformed, or unverified skills.',
|
|
21
|
+
'',
|
|
22
|
+
' --from <directory> Resolve installed packages from this directory, relative',
|
|
23
|
+
' to the project. Default: the project directory.',
|
|
24
|
+
' Use the server package for workspace-local dependencies.',
|
|
25
|
+
' --skill <names> Check only these comma-separated skill names.',
|
|
26
|
+
' Default: hydranium-*. Use --skill langium to check upstream.',
|
|
27
|
+
'',
|
|
28
|
+
'A compatible result checks declared skill contracts,',
|
|
29
|
+
'not the accuracy of prose.',
|
|
30
|
+
'Upstream skills without Hydranium metadata are unverified.'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export function runCheckSkillsCommand(args: string[]): void {
|
|
34
|
+
if (helpRequested(args, ['--from', '--skill'])) {
|
|
35
|
+
printHelp(CHECK_SKILLS_HELP);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let project: string | undefined;
|
|
39
|
+
let from: string | undefined;
|
|
40
|
+
let names: string[] | undefined;
|
|
41
|
+
for (let index = 0; index < args.length; index++) {
|
|
42
|
+
const argument = args[index];
|
|
43
|
+
if (argument === '--from' || argument === '--skill') {
|
|
44
|
+
const value = args[++index];
|
|
45
|
+
if (!value || value.startsWith('--')) {
|
|
46
|
+
exitWithUsage(`Missing value for ${argument}`);
|
|
47
|
+
}
|
|
48
|
+
if (argument === '--from') {
|
|
49
|
+
from = value;
|
|
50
|
+
} else {
|
|
51
|
+
names = value.split(',');
|
|
52
|
+
if (names.some(name => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))) {
|
|
53
|
+
exitWithUsage('Invalid skill name.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else if (argument.startsWith('-') || project !== undefined) {
|
|
57
|
+
exitWithUsage(`Unexpected argument: ${argument} (hydranium-cli check-skills --help)`);
|
|
58
|
+
} else {
|
|
59
|
+
project = argument;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const root = path.resolve(project ?? '.');
|
|
63
|
+
const results = checkSkills(root, path.resolve(root, from ?? '.'), names);
|
|
64
|
+
for (const result of results) {
|
|
65
|
+
console.log(`${result.status}: ${result.file}: ${result.detail}`);
|
|
66
|
+
}
|
|
67
|
+
if (results.some(result => result.status !== 'compatible')) {
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -48,7 +48,7 @@ const NAMESPACED_AST_TYPE_NAME = 'AstType';
|
|
|
48
48
|
const MERGED_EXPORT_NAME = 'astNode';
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Emit a module binding
|
|
51
|
+
* Emit a module binding `makeAstNodeBuilder` to a project's reflection,
|
|
52
52
|
* once per grammar plus once across all of them.
|
|
53
53
|
*
|
|
54
54
|
* **The binding is boilerplate with exactly one degree of freedom** — the type
|
|
@@ -26,12 +26,14 @@
|
|
|
26
26
|
* association is gone.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import * as path from 'node:path';
|
|
29
30
|
import { assertRequired, exitWithUsage, helpRequested, printHelp, type UsageError } from './harness-args.js';
|
|
30
|
-
import { isNonEmptyDir, runInit, type InitHead } from './init.js';
|
|
31
|
+
import { isNonEmptyDir, runInit, validateInitInstallation, type InitHead } from './init.js';
|
|
31
32
|
import type { InitHost } from './init-hosts.js';
|
|
32
33
|
import { createClackPrompt } from './init-prompt.js';
|
|
33
34
|
import { runInitWizard } from './init-wizard.js';
|
|
34
35
|
import { createNodeWorkspaceProbe, detectWorkspace } from './init-workspace.js';
|
|
36
|
+
import { installSkills, offerSkills, parseSkillInstallation, SKILLS_INSTALLER } from './init-skills.js';
|
|
35
37
|
|
|
36
38
|
/** One grammar as the command line collected it, before any derivation. */
|
|
37
39
|
export interface ParsedInitGrammar {
|
|
@@ -46,6 +48,9 @@ export interface ParsedInitArgs {
|
|
|
46
48
|
targetDir?: string;
|
|
47
49
|
name?: string;
|
|
48
50
|
force: boolean;
|
|
51
|
+
install: boolean;
|
|
52
|
+
skills?: string;
|
|
53
|
+
agents?: string;
|
|
49
54
|
heads?: InitHead[];
|
|
50
55
|
hosts?: InitHost[];
|
|
51
56
|
monorepo: boolean;
|
|
@@ -66,6 +71,9 @@ export const INIT_FLAGS: readonly string[] = [
|
|
|
66
71
|
'--heads',
|
|
67
72
|
'--hosts',
|
|
68
73
|
'--force',
|
|
74
|
+
'--install',
|
|
75
|
+
'--skills',
|
|
76
|
+
'--agents',
|
|
69
77
|
'--monorepo',
|
|
70
78
|
'--scope',
|
|
71
79
|
'--public',
|
|
@@ -80,6 +88,8 @@ export const INIT_FLAGS: readonly string[] = [
|
|
|
80
88
|
* The rest are presence-only.
|
|
81
89
|
*/
|
|
82
90
|
export const INIT_VALUE_FLAGS: readonly string[] = [
|
|
91
|
+
'--skills',
|
|
92
|
+
'--agents',
|
|
83
93
|
'--name',
|
|
84
94
|
'--heads',
|
|
85
95
|
'--hosts',
|
|
@@ -104,6 +114,9 @@ export function parseInitArgs(args: string[], onError: UsageError = exitWithUsag
|
|
|
104
114
|
let targetDir: string | undefined;
|
|
105
115
|
let name: string | undefined;
|
|
106
116
|
let force = false;
|
|
117
|
+
let install = false;
|
|
118
|
+
let skills: string | undefined;
|
|
119
|
+
let agents: string | undefined;
|
|
107
120
|
let heads: InitHead[] | undefined;
|
|
108
121
|
let hosts: InitHost[] | undefined;
|
|
109
122
|
let monorepo = false;
|
|
@@ -131,6 +144,12 @@ export function parseInitArgs(args: string[], onError: UsageError = exitWithUsag
|
|
|
131
144
|
return current;
|
|
132
145
|
};
|
|
133
146
|
switch (flag) {
|
|
147
|
+
case '--skills':
|
|
148
|
+
skills = next();
|
|
149
|
+
break;
|
|
150
|
+
case '--agents':
|
|
151
|
+
agents = next();
|
|
152
|
+
break;
|
|
134
153
|
case '--name':
|
|
135
154
|
name = next();
|
|
136
155
|
break;
|
|
@@ -160,6 +179,9 @@ export function parseInitArgs(args: string[], onError: UsageError = exitWithUsag
|
|
|
160
179
|
case '--force':
|
|
161
180
|
force = true;
|
|
162
181
|
break;
|
|
182
|
+
case '--install':
|
|
183
|
+
install = true;
|
|
184
|
+
break;
|
|
163
185
|
case '--monorepo':
|
|
164
186
|
monorepo = true;
|
|
165
187
|
break;
|
|
@@ -184,7 +206,7 @@ export function parseInitArgs(args: string[], onError: UsageError = exitWithUsag
|
|
|
184
206
|
}
|
|
185
207
|
}
|
|
186
208
|
}
|
|
187
|
-
return { targetDir, name, force, heads, hosts, monorepo, scope, public: isPublic, grammars };
|
|
209
|
+
return { targetDir, name, force, install, skills, agents, heads, hosts, monorepo, scope, public: isPublic, grammars };
|
|
188
210
|
}
|
|
189
211
|
|
|
190
212
|
/**
|
|
@@ -197,7 +219,7 @@ export function parseInitArgs(args: string[], onError: UsageError = exitWithUsag
|
|
|
197
219
|
export const INIT_HELP: readonly string[] = [
|
|
198
220
|
'Usage: hydranium-cli init <target-dir> --name <Name> [--heads <list>]',
|
|
199
221
|
' [--hosts <list>] [--force] [--monorepo]',
|
|
200
|
-
' [--scope <@scope>] [--public]',
|
|
222
|
+
' [--scope <@scope>] [--public] [--install]',
|
|
201
223
|
' ( --grammar <Name> [--extensions <list>]',
|
|
202
224
|
' [--language-id <id>] [--diagram] )...',
|
|
203
225
|
'',
|
|
@@ -210,12 +232,18 @@ export const INIT_HELP: readonly string[] = [
|
|
|
210
232
|
'launch of the VS Code and Theia ones, and theia/ the Theia extension;',
|
|
211
233
|
'workspace/ holds two starter model files per grammar. A standalone project',
|
|
212
234
|
'also gets a root manifest naming the packages as npm workspaces.',
|
|
213
|
-
'
|
|
214
|
-
'
|
|
235
|
+
'Stays offline unless --install or --skills is requested. With --install,',
|
|
236
|
+
'runs `npm install` after writing the scaffold. Building remains a next step.',
|
|
237
|
+
'Refuses a non-empty directory',
|
|
238
|
+
'without --force.',
|
|
215
239
|
'',
|
|
216
240
|
'Run it with no --name on a terminal to be prompted instead. The wizard echoes',
|
|
217
241
|
'the command it composed before running it, so every answer is reproducible as',
|
|
218
242
|
'flags. Without a terminal a missing --name stays an error, so CI never hangs.',
|
|
243
|
+
'For a standalone project, the wizard ends by offering npm install unless',
|
|
244
|
+
'--install already requested it.',
|
|
245
|
+
'After a successful scaffold, the wizard offers coding-agent skills separately.',
|
|
246
|
+
'Skipping or cancelling that offer leaves the scaffold intact.',
|
|
219
247
|
'',
|
|
220
248
|
'Project options (position-free):',
|
|
221
249
|
' <target-dir> The project directory to scaffold into (required).',
|
|
@@ -240,12 +268,22 @@ export const INIT_HELP: readonly string[] = [
|
|
|
240
268
|
' --hosts, the server with its Node entry points, and',
|
|
241
269
|
' with glsp the client/ package a host mounts.',
|
|
242
270
|
' --force Scaffold into a non-empty directory anyway.',
|
|
271
|
+
' --skills <names> After scaffolding, install named skills: hydranium-profiling,',
|
|
272
|
+
` langium. Requires --agents. Downloads with npx ${SKILLS_INSTALLER} add`,
|
|
273
|
+
' --copy, preserving unrelated skills and lock entries.',
|
|
274
|
+
' With --monorepo, skills are written at the workspace root.',
|
|
275
|
+
' --agents <names> Comma-separated agents for --skills: codex, claude-code,',
|
|
276
|
+
' cursor, github-copilot, opencode.',
|
|
277
|
+
' --install Run npm install in the scaffold, with lifecycle scripts',
|
|
278
|
+
' and network access. Cannot be combined with --monorepo:',
|
|
279
|
+
' its parent manifest needs manual edits first.',
|
|
243
280
|
' --monorepo Scaffold the packages as members of the surrounding npm',
|
|
244
281
|
' workspace, with no root of their own: each tsconfig',
|
|
245
282
|
' extends the root config that actually carries',
|
|
246
283
|
' compilerOptions, and the scripts address the server by',
|
|
247
284
|
' --prefix. Errors when no ancestor declares `workspaces`.',
|
|
248
|
-
'
|
|
285
|
+
' Scaffold files stay inside the target; optional skills',
|
|
286
|
+
' go at the workspace root. The root `workspaces`',
|
|
249
287
|
' entries and dependency pins are printed, not added.',
|
|
250
288
|
' --scope <@scope> npm scope for the package names, e.g. @acme. Not inferred:',
|
|
251
289
|
' a root manifest is usually named for the repo, not the',
|
|
@@ -291,18 +329,24 @@ export async function runInitCommand(args: string[]): Promise<void> {
|
|
|
291
329
|
printHelp(INIT_HELP);
|
|
292
330
|
return;
|
|
293
331
|
}
|
|
294
|
-
|
|
332
|
+
let parsed = parseInitArgs(args);
|
|
333
|
+
validateInitInstallation(parsed);
|
|
334
|
+
let selection = parseSkillInstallation(parsed.skills, parsed.agents);
|
|
335
|
+
let prompt: Awaited<ReturnType<typeof createClackPrompt>> | undefined;
|
|
295
336
|
// The wizard's entry point: no `--name` on an interactive terminal. Safe
|
|
296
337
|
// because that combination has no non-wizard meaning, and gated on a TTY
|
|
297
338
|
// because a non-interactive caller must keep failing fast — a prompt in CI
|
|
298
339
|
// hangs the job instead of reporting the missing flag.
|
|
299
340
|
if (parsed.name === undefined && process.stdin.isTTY && process.stdout.isTTY) {
|
|
300
|
-
|
|
341
|
+
prompt = await createClackPrompt();
|
|
301
342
|
const argv = await runInitWizard(prompt, targetDir => detectWorkspace(targetDir, createNodeWorkspaceProbe()), {
|
|
302
343
|
targetDir: parsed.targetDir,
|
|
344
|
+
install: parsed.install,
|
|
345
|
+
skills: parsed.skills,
|
|
346
|
+
agents: parsed.agents,
|
|
303
347
|
isOccupied: isNonEmptyDir
|
|
304
348
|
});
|
|
305
|
-
|
|
349
|
+
parsed = parseInitArgs(argv);
|
|
306
350
|
}
|
|
307
351
|
runInit({
|
|
308
352
|
targetDir: assertRequired(parsed.targetDir, '<target-dir>', 'init'),
|
|
@@ -311,8 +355,19 @@ export async function runInitCommand(args: string[]): Promise<void> {
|
|
|
311
355
|
heads: parsed.heads,
|
|
312
356
|
hosts: parsed.hosts,
|
|
313
357
|
force: parsed.force,
|
|
358
|
+
install: parsed.install,
|
|
314
359
|
monorepo: parsed.monorepo,
|
|
315
360
|
scope: parsed.scope,
|
|
316
361
|
public: parsed.public
|
|
317
362
|
});
|
|
363
|
+
const target = path.resolve(parsed.targetDir!);
|
|
364
|
+
const project = parsed.monorepo ? detectWorkspace(target, createNodeWorkspaceProbe())!.rootDir : target;
|
|
365
|
+
if (selection === undefined && prompt !== undefined) {
|
|
366
|
+
selection = await offerSkills(prompt, project);
|
|
367
|
+
}
|
|
368
|
+
if (selection !== undefined) {
|
|
369
|
+
installSkills(project, path.join(target, 'server'), selection);
|
|
370
|
+
} else {
|
|
371
|
+
process.stdout.write('\nOptional agent skills: see the Work with coding-agent skills guide.\n');
|
|
372
|
+
}
|
|
318
373
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { InitWizardCancelled, type PromptPort } from './init-prompt.js';
|
|
13
|
+
import { checkSkills } from './skills.js';
|
|
14
|
+
|
|
15
|
+
export const SKILLS_INSTALLER = 'skills@1.7.6';
|
|
16
|
+
|
|
17
|
+
export const ADOPTER_SKILLS = {
|
|
18
|
+
'hydranium-profiling': { source: 'eclipse-emfcloud/hydranium', label: 'Hydranium profiling' },
|
|
19
|
+
langium: { source: 'eclipse-langium/langium-ai', label: 'Langium grammar and runtime' }
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export type AdopterSkill = keyof typeof ADOPTER_SKILLS;
|
|
23
|
+
|
|
24
|
+
export const SKILL_AGENTS = {
|
|
25
|
+
codex: 'Codex',
|
|
26
|
+
'claude-code': 'Claude Code',
|
|
27
|
+
cursor: 'Cursor',
|
|
28
|
+
'github-copilot': 'GitHub Copilot',
|
|
29
|
+
opencode: 'OpenCode'
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type SkillAgent = keyof typeof SKILL_AGENTS;
|
|
33
|
+
|
|
34
|
+
export interface SkillInstallation {
|
|
35
|
+
readonly skills: readonly AdopterSkill[];
|
|
36
|
+
readonly agents: readonly SkillAgent[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Validate before scaffolding so a misspelling cannot leave a half-completed command. */
|
|
40
|
+
export function parseSkillInstallation(skills?: string, agents?: string): SkillInstallation | undefined {
|
|
41
|
+
if (skills === undefined && agents === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!skills || !agents) {
|
|
45
|
+
throw new Error('--skills and --agents must be supplied together.');
|
|
46
|
+
}
|
|
47
|
+
const selectedSkills = skills.split(',');
|
|
48
|
+
const selectedAgents = agents.split(',');
|
|
49
|
+
for (const name of selectedSkills) {
|
|
50
|
+
if (!Object.hasOwn(ADOPTER_SKILLS, name)) {
|
|
51
|
+
throw new Error(`Unknown skill '${name}'. Choose ${Object.keys(ADOPTER_SKILLS).join(', ')}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const name of selectedAgents) {
|
|
55
|
+
if (!Object.hasOwn(SKILL_AGENTS, name)) {
|
|
56
|
+
throw new Error(`Unknown agent '${name}'. Choose ${Object.keys(SKILL_AGENTS).join(', ')}.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { skills: [...new Set(selectedSkills)] as AdopterSkill[], agents: [...new Set(selectedAgents)] as SkillAgent[] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function skillInstallArgs(skill: AdopterSkill, agents: readonly SkillAgent[]): string[] {
|
|
63
|
+
return ['--yes', SKILLS_INSTALLER, 'add', ADOPTER_SKILLS[skill].source, '--skill', skill, '--agent', ...agents, '--copy', '--yes'];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A failed optional download leaves the scaffold usable and gives an independently runnable retry. */
|
|
67
|
+
export function installSkills(
|
|
68
|
+
project: string,
|
|
69
|
+
from: string,
|
|
70
|
+
selection: SkillInstallation,
|
|
71
|
+
write: (message: string) => void = message => process.stdout.write(message)
|
|
72
|
+
): void {
|
|
73
|
+
for (const skill of selection.skills) {
|
|
74
|
+
const args = skillInstallArgs(skill, selection.agents);
|
|
75
|
+
write(`\nIn ${project}: npx ${args.join(' ')}\n`);
|
|
76
|
+
try {
|
|
77
|
+
// Only catalogued names reach the Windows shim's shell; paths travel as cwd, never shell text.
|
|
78
|
+
const result = spawnSync(process.platform === 'win32' ? 'npx.cmd' : 'npx', args, {
|
|
79
|
+
cwd: project,
|
|
80
|
+
stdio: 'inherit',
|
|
81
|
+
shell: process.platform === 'win32'
|
|
82
|
+
});
|
|
83
|
+
if (result.signal === 'SIGINT' || result.status === 130) {
|
|
84
|
+
write('Skill installation cancelled. Scaffold kept.\n');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (result.error || result.signal || result.status !== 0) {
|
|
88
|
+
throw result.error ?? new Error(result.signal ?? `exit code ${result.status}`);
|
|
89
|
+
}
|
|
90
|
+
for (const result of checkSkills(project, from, [skill])) {
|
|
91
|
+
write(`${result.status}: ${result.file}: ${result.detail}\n`);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
write(`Skill installation failed (${error instanceof Error ? error.message : String(error)}). Scaffold kept.\n`);
|
|
95
|
+
write(`Retry in ${project}: npx ${args.join(' ')}\n`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const relative = path.relative(project, from) || '.';
|
|
99
|
+
const quoted = `'${relative.replace(/'/g, "'\\''")}'`;
|
|
100
|
+
write(`Check again after installing or updating dependencies: npx hydranium-cli check-skills --from ${quoted}\n`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** This prompt runs only after the scaffold succeeds; cancelling it cannot undo the scaffold. */
|
|
104
|
+
export async function offerSkills(prompt: PromptPort, project: string): Promise<SkillInstallation | undefined> {
|
|
105
|
+
try {
|
|
106
|
+
prompt.intro('Optional coding-agent skills');
|
|
107
|
+
if (!(await prompt.confirm({ message: `Install coding-agent skills in ${project}?`, initialValue: false }))) {
|
|
108
|
+
prompt.outro('Skills skipped.');
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const skills = await prompt.multiselect<AdopterSkill>({
|
|
112
|
+
message: 'Skills to install',
|
|
113
|
+
choices: Object.entries(ADOPTER_SKILLS).map(([value, skill]) => ({ value: value as AdopterSkill, label: skill.label })),
|
|
114
|
+
initialValues: ['hydranium-profiling', 'langium']
|
|
115
|
+
});
|
|
116
|
+
const agents = await prompt.multiselect<SkillAgent>({
|
|
117
|
+
message: 'Coding agents',
|
|
118
|
+
choices: Object.entries(SKILL_AGENTS).map(([value, label]) => ({ value: value as SkillAgent, label }))
|
|
119
|
+
});
|
|
120
|
+
prompt.outro('Installing selected skills as project files. Commit the files and skills-lock.json.');
|
|
121
|
+
return { skills, agents };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof InitWizardCancelled) {
|
|
124
|
+
prompt.outro('Skills skipped. Scaffold kept.');
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
* posture rather than a packaging detail: the emission withholds publication
|
|
40
40
|
* AND declares itself UNLICENSED, so a default nobody was shown would settle
|
|
41
41
|
* the licence question by silence.
|
|
42
|
+
* - **installation**, which opts into network access and lifecycle scripts.
|
|
43
|
+
* Workspace members need parent-manifest edits first, so only standalone
|
|
44
|
+
* projects offer it, unless the caller already supplied `--install`.
|
|
42
45
|
*
|
|
43
46
|
* The **language id is deliberately not asked**, though it is the third of the
|
|
44
47
|
* three independent names: its derivation was settled and built separately
|
|
@@ -91,6 +94,10 @@ export interface InitAnswers {
|
|
|
91
94
|
readonly scope?: string;
|
|
92
95
|
/** Leave the package publishable, which the scaffold does not do by default. */
|
|
93
96
|
readonly public: boolean;
|
|
97
|
+
/** Run npm install after writing the scaffold. */
|
|
98
|
+
readonly install: boolean;
|
|
99
|
+
readonly skills?: string;
|
|
100
|
+
readonly agents?: string;
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
/** PascalCase a directory name, splitting on any non-alphanumeric run. */
|
|
@@ -153,6 +160,12 @@ export function composeInitArgv(answers: InitAnswers): string[] {
|
|
|
153
160
|
if (answers.public) {
|
|
154
161
|
argv.push('--public');
|
|
155
162
|
}
|
|
163
|
+
if (answers.install) {
|
|
164
|
+
argv.push('--install');
|
|
165
|
+
}
|
|
166
|
+
if (answers.skills !== undefined && answers.agents !== undefined) {
|
|
167
|
+
argv.push('--skills', answers.skills, '--agents', answers.agents);
|
|
168
|
+
}
|
|
156
169
|
return argv;
|
|
157
170
|
}
|
|
158
171
|
|
|
@@ -350,7 +363,13 @@ async function askPackaging(
|
|
|
350
363
|
export async function runInitWizard(
|
|
351
364
|
prompt: PromptPort,
|
|
352
365
|
detect: (targetDir: string) => WorkspaceDetection | undefined,
|
|
353
|
-
options: {
|
|
366
|
+
options: {
|
|
367
|
+
readonly targetDir?: string;
|
|
368
|
+
readonly isOccupied?: (targetDir: string) => boolean;
|
|
369
|
+
readonly install?: boolean;
|
|
370
|
+
readonly skills?: string;
|
|
371
|
+
readonly agents?: string;
|
|
372
|
+
} = {}
|
|
354
373
|
): Promise<string[]> {
|
|
355
374
|
prompt.intro('Scaffold a Hydranium language project');
|
|
356
375
|
|
|
@@ -392,9 +411,31 @@ export async function runInitWizard(
|
|
|
392
411
|
} while (await prompt.confirm({ message: 'Add another grammar?', initialValue: false }));
|
|
393
412
|
|
|
394
413
|
const withDiagram = await askDiagramGrammar(prompt, heads, grammars);
|
|
395
|
-
const
|
|
414
|
+
const detection = detect(targetDir);
|
|
415
|
+
if (options.install && detection !== undefined) {
|
|
416
|
+
prompt.note('--install requires a standalone project; workspace membership is skipped.', 'Installation');
|
|
417
|
+
}
|
|
418
|
+
const packaging = await askPackaging(prompt, options.install ? undefined : detection, heads, hosts);
|
|
419
|
+
const install =
|
|
420
|
+
(options.install ?? false) ||
|
|
421
|
+
(!packaging.monorepo &&
|
|
422
|
+
(await prompt.confirm({
|
|
423
|
+
message: 'Install dependencies now (npm install)?',
|
|
424
|
+
initialValue: false
|
|
425
|
+
})));
|
|
396
426
|
|
|
397
|
-
const argv = composeInitArgv({
|
|
427
|
+
const argv = composeInitArgv({
|
|
428
|
+
targetDir,
|
|
429
|
+
name,
|
|
430
|
+
heads,
|
|
431
|
+
hosts,
|
|
432
|
+
grammars: withDiagram,
|
|
433
|
+
force,
|
|
434
|
+
install,
|
|
435
|
+
skills: options.skills,
|
|
436
|
+
agents: options.agents,
|
|
437
|
+
...packaging
|
|
438
|
+
});
|
|
398
439
|
// Echoed as the LAST thing before the scaffold runs, because it is the
|
|
399
440
|
// artefact the session produces: everything above is how it was reached.
|
|
400
441
|
prompt.outro(formatCommand(argv));
|
package/src/commands/init.ts
CHANGED
|
@@ -284,6 +284,8 @@ export interface InitCommandOptions {
|
|
|
284
284
|
readonly hosts?: readonly InitHost[];
|
|
285
285
|
/** Scaffold into a non-empty directory anyway. */
|
|
286
286
|
readonly force?: boolean;
|
|
287
|
+
/** Install dependencies after scaffolding. Only supported for standalone projects. */
|
|
288
|
+
readonly install?: boolean;
|
|
287
289
|
/**
|
|
288
290
|
* Scaffold a member of the surrounding npm workspace: extend its base
|
|
289
291
|
* tsconfig and address the package by `--prefix` in the scripts it prints
|
|
@@ -597,16 +599,21 @@ function formatScaffold(targetDir: string, files: readonly InitFile[], write: (l
|
|
|
597
599
|
}
|
|
598
600
|
}
|
|
599
601
|
|
|
602
|
+
export function validateInitInstallation(options: Pick<InitCommandOptions, 'install' | 'monorepo'>): void {
|
|
603
|
+
if (options.install && options.monorepo) {
|
|
604
|
+
throw new Error('--install cannot be combined with --monorepo: update the parent workspace manifest before running npm install.');
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
600
608
|
/**
|
|
601
609
|
* Scaffold a new Hydranium language project end-to-end: one starter grammar per
|
|
602
610
|
* `--grammar`, `create<Name>Services` DI wiring, a launcher for each head in
|
|
603
|
-
* `--heads`, `langium-config.json`, `package.json`, and scripts.
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* scaffold's oxfmt then formats. Refuses a non-empty target directory unless
|
|
607
|
-
* `force`.
|
|
611
|
+
* `--heads`, `langium-config.json`, `package.json`, and scripts. Stays offline
|
|
612
|
+
* unless `install` opts into npm and its lifecycle scripts. Building remains a
|
|
613
|
+
* next step. Refuses a non-empty target directory unless `force`.
|
|
608
614
|
*/
|
|
609
615
|
export function runInit(options: InitCommandOptions): void {
|
|
616
|
+
validateInitInstallation(options);
|
|
610
617
|
const write = options.write ?? ((line: string) => process.stdout.write(line));
|
|
611
618
|
const targetDir = path.resolve(options.targetDir);
|
|
612
619
|
const detection = options.monorepo ? detectWorkspace(targetDir, options.probe ?? createNodeWorkspaceProbe()) : undefined;
|
|
@@ -691,6 +698,41 @@ export function runInit(options: InitCommandOptions): void {
|
|
|
691
698
|
write(` Sibling packages all use the "${detection.scope}" scope; pass --scope ${detection.scope} to match them.\n`);
|
|
692
699
|
}
|
|
693
700
|
}
|
|
701
|
+
if (options.install) {
|
|
702
|
+
write('\nRunning npm install...\n');
|
|
703
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
704
|
+
// --prefix also changes npm's default globalconfig. Resolve it first without walking into an enclosing workspace.
|
|
705
|
+
const config = spawnSync(npm, ['config', 'get', 'globalconfig', '--workspaces=false'], {
|
|
706
|
+
cwd: targetDir,
|
|
707
|
+
encoding: 'utf8',
|
|
708
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
709
|
+
shell: process.platform === 'win32'
|
|
710
|
+
});
|
|
711
|
+
const globalconfig = config.stdout?.trim();
|
|
712
|
+
if (config.error || config.signal || config.status !== 0 || !globalconfig) {
|
|
713
|
+
const reason = config.error?.message ?? (config.signal ? `signal ${config.signal}` : `exit code ${config.status}`);
|
|
714
|
+
throw new Error(
|
|
715
|
+
`Could not read npm global configuration (${reason}). Scaffold kept at ${targetDir}; installation was not started.`
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toLowerCase() !== 'npm_config_globalconfig'));
|
|
719
|
+
env.npm_config_globalconfig = globalconfig;
|
|
720
|
+
// An explicit prefix keeps npm from selecting an enclosing workspace instead of this standalone root.
|
|
721
|
+
// Windows resolves npm through a command shim; only fixed arguments reach the shell.
|
|
722
|
+
const result = spawnSync(npm, ['--prefix', '.', 'install'], {
|
|
723
|
+
cwd: targetDir,
|
|
724
|
+
stdio: 'inherit',
|
|
725
|
+
shell: process.platform === 'win32',
|
|
726
|
+
env
|
|
727
|
+
});
|
|
728
|
+
if (result.error || result.signal || result.status !== 0) {
|
|
729
|
+
const reason = result.error?.message ?? (result.signal ? `signal ${result.signal}` : `exit code ${result.status}`);
|
|
730
|
+
throw new Error(
|
|
731
|
+
`npm install failed (${reason}). Scaffold kept at ${targetDir}. ` +
|
|
732
|
+
`Set npm_config_globalconfig to ${globalconfig} to preserve npm's global configuration. Run npm --prefix . install there to retry.`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
694
736
|
write('\n');
|
|
695
737
|
write('Next steps:\n');
|
|
696
738
|
// A workspace member's project has no manifest of its own, so its steps run
|
|
@@ -703,7 +745,9 @@ export function runInit(options: InitCommandOptions): void {
|
|
|
703
745
|
if (spawnSync('git', ['check-ignore', '-q', targetDir], { cwd: targetDir }).status === 0) {
|
|
704
746
|
write(' git init # an enclosing repository ignores this folder, so lint and format find no file until then\n');
|
|
705
747
|
}
|
|
706
|
-
|
|
748
|
+
if (!options.install) {
|
|
749
|
+
write(' npm install\n');
|
|
750
|
+
}
|
|
707
751
|
write(' npm run build # langium generate + tsc, every package\n');
|
|
708
752
|
write(' npm test # the server tests\n');
|
|
709
753
|
write(' npm run watch # rebuild the members on every change; watch:<host> adds a host\n');
|