@hydranium/cli 1.0.0-next.10
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/LICENSE +21 -0
- package/README.md +240 -0
- package/heap-analysis/analyze-heap.mjs +798 -0
- package/heap-analysis/heap/core/aggregate.mjs +152 -0
- package/heap-analysis/heap/core/bucket-label.mjs +23 -0
- package/heap-analysis/heap/core/cutpoints.mjs +51 -0
- package/heap-analysis/heap/core/diff.mjs +56 -0
- package/heap-analysis/heap/core/format.mjs +47 -0
- package/heap-analysis/heap/core/holders.mjs +80 -0
- package/heap-analysis/heap/core/load.mjs +46 -0
- package/heap-analysis/heap/core/report.mjs +68 -0
- package/heap-analysis/heap/core/strings.mjs +80 -0
- package/heap-analysis/heap/langium/concepts.mjs +230 -0
- package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
- package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
- package/heap-analysis/heap/renderer/renderer.mjs +122 -0
- package/lib/cli-version.d.ts +21 -0
- package/lib/cli-version.d.ts.map +1 -0
- package/lib/cli-version.js +30 -0
- package/lib/cli-version.js.map +1 -0
- package/lib/cli.d.ts +11 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +135 -0
- package/lib/cli.js.map +1 -0
- package/lib/commands/analyze-heap-args.d.ts +46 -0
- package/lib/commands/analyze-heap-args.d.ts.map +1 -0
- package/lib/commands/analyze-heap-args.js +174 -0
- package/lib/commands/analyze-heap-args.js.map +1 -0
- package/lib/commands/analyze-heap.d.ts +37 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -0
- package/lib/commands/analyze-heap.js +63 -0
- package/lib/commands/analyze-heap.js.map +1 -0
- package/lib/commands/ast-ground-truth-args.d.ts +23 -0
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-args.js +47 -0
- package/lib/commands/ast-ground-truth-args.js.map +1 -0
- package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
- package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-driver.js +72 -0
- package/lib/commands/ast-ground-truth-driver.js.map +1 -0
- package/lib/commands/ast-ground-truth.d.ts +31 -0
- package/lib/commands/ast-ground-truth.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth.js +36 -0
- package/lib/commands/ast-ground-truth.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts +38 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-args.js +183 -0
- package/lib/commands/generate-transfer-model-args.js.map +1 -0
- package/lib/commands/generate-transfer-model-config.d.ts +32 -0
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-config.js +122 -0
- package/lib/commands/generate-transfer-model-config.js.map +1 -0
- package/lib/commands/generate-transfer-model.d.ts +110 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model.js +587 -0
- package/lib/commands/generate-transfer-model.js.map +1 -0
- package/lib/commands/harness-args.d.ts +198 -0
- package/lib/commands/harness-args.d.ts.map +1 -0
- package/lib/commands/harness-args.js +339 -0
- package/lib/commands/harness-args.js.map +1 -0
- package/lib/commands/headless-harness.d.ts +97 -0
- package/lib/commands/headless-harness.d.ts.map +1 -0
- package/lib/commands/headless-harness.js +121 -0
- package/lib/commands/headless-harness.js.map +1 -0
- package/lib/commands/init-args.d.ts +81 -0
- package/lib/commands/init-args.d.ts.map +1 -0
- package/lib/commands/init-args.js +250 -0
- package/lib/commands/init-args.js.map +1 -0
- package/lib/commands/init-prompt.d.ts +66 -0
- package/lib/commands/init-prompt.d.ts.map +1 -0
- package/lib/commands/init-prompt.js +71 -0
- package/lib/commands/init-prompt.js.map +1 -0
- package/lib/commands/init-templates.d.ts +42 -0
- package/lib/commands/init-templates.d.ts.map +1 -0
- package/lib/commands/init-templates.js +1902 -0
- package/lib/commands/init-templates.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +65 -0
- package/lib/commands/init-wizard.d.ts.map +1 -0
- package/lib/commands/init-wizard.js +302 -0
- package/lib/commands/init-wizard.js.map +1 -0
- package/lib/commands/init-workspace.d.ts +168 -0
- package/lib/commands/init-workspace.d.ts.map +1 -0
- package/lib/commands/init-workspace.js +372 -0
- package/lib/commands/init-workspace.js.map +1 -0
- package/lib/commands/init.d.ts +273 -0
- package/lib/commands/init.d.ts.map +1 -0
- package/lib/commands/init.js +303 -0
- package/lib/commands/init.js.map +1 -0
- package/lib/commands/lint-grammar-args.d.ts +23 -0
- package/lib/commands/lint-grammar-args.d.ts.map +1 -0
- package/lib/commands/lint-grammar-args.js +63 -0
- package/lib/commands/lint-grammar-args.js.map +1 -0
- package/lib/commands/lint-grammar-driver.d.ts +10 -0
- package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
- package/lib/commands/lint-grammar-driver.js +72 -0
- package/lib/commands/lint-grammar-driver.js.map +1 -0
- package/lib/commands/lint-grammar-report.d.ts +24 -0
- package/lib/commands/lint-grammar-report.d.ts.map +1 -0
- package/lib/commands/lint-grammar-report.js +65 -0
- package/lib/commands/lint-grammar-report.js.map +1 -0
- package/lib/commands/lint-grammar.d.ts +33 -0
- package/lib/commands/lint-grammar.d.ts.map +1 -0
- package/lib/commands/lint-grammar.js +42 -0
- package/lib/commands/lint-grammar.js.map +1 -0
- package/lib/commands/measure-memory-args.d.ts +23 -0
- package/lib/commands/measure-memory-args.d.ts.map +1 -0
- package/lib/commands/measure-memory-args.js +106 -0
- package/lib/commands/measure-memory-args.js.map +1 -0
- package/lib/commands/measure-memory-driver.d.ts +10 -0
- package/lib/commands/measure-memory-driver.d.ts.map +1 -0
- package/lib/commands/measure-memory-driver.js +110 -0
- package/lib/commands/measure-memory-driver.js.map +1 -0
- package/lib/commands/measure-memory.d.ts +68 -0
- package/lib/commands/measure-memory.d.ts.map +1 -0
- package/lib/commands/measure-memory.js +100 -0
- package/lib/commands/measure-memory.js.map +1 -0
- package/lib/commands/model-docs-args.d.ts +23 -0
- package/lib/commands/model-docs-args.d.ts.map +1 -0
- package/lib/commands/model-docs-args.js +53 -0
- package/lib/commands/model-docs-args.js.map +1 -0
- package/lib/commands/model-docs-driver.d.ts +10 -0
- package/lib/commands/model-docs-driver.d.ts.map +1 -0
- package/lib/commands/model-docs-driver.js +61 -0
- package/lib/commands/model-docs-driver.js.map +1 -0
- package/lib/commands/model-docs-report.d.ts +18 -0
- package/lib/commands/model-docs-report.d.ts.map +1 -0
- package/lib/commands/model-docs-report.js +123 -0
- package/lib/commands/model-docs-report.js.map +1 -0
- package/lib/commands/model-docs.d.ts +30 -0
- package/lib/commands/model-docs.d.ts.map +1 -0
- package/lib/commands/model-docs.js +37 -0
- package/lib/commands/model-docs.js.map +1 -0
- package/lib/commands/projects-args.d.ts +25 -0
- package/lib/commands/projects-args.d.ts.map +1 -0
- package/lib/commands/projects-args.js +49 -0
- package/lib/commands/projects-args.js.map +1 -0
- package/lib/commands/projects.d.ts +45 -0
- package/lib/commands/projects.d.ts.map +1 -0
- package/lib/commands/projects.js +39 -0
- package/lib/commands/projects.js.map +1 -0
- package/lib/commands/query-args.d.ts +25 -0
- package/lib/commands/query-args.d.ts.map +1 -0
- package/lib/commands/query-args.js +46 -0
- package/lib/commands/query-args.js.map +1 -0
- package/lib/commands/query.d.ts +32 -0
- package/lib/commands/query.d.ts.map +1 -0
- package/lib/commands/query.js +29 -0
- package/lib/commands/query.js.map +1 -0
- package/lib/commands/reflect-args.d.ts +23 -0
- package/lib/commands/reflect-args.d.ts.map +1 -0
- package/lib/commands/reflect-args.js +59 -0
- package/lib/commands/reflect-args.js.map +1 -0
- package/lib/commands/reflect-driver.d.ts +10 -0
- package/lib/commands/reflect-driver.d.ts.map +1 -0
- package/lib/commands/reflect-driver.js +66 -0
- package/lib/commands/reflect-driver.js.map +1 -0
- package/lib/commands/reflect-report.d.ts +20 -0
- package/lib/commands/reflect-report.d.ts.map +1 -0
- package/lib/commands/reflect-report.js +127 -0
- package/lib/commands/reflect-report.js.map +1 -0
- package/lib/commands/reflect.d.ts +30 -0
- package/lib/commands/reflect.d.ts.map +1 -0
- package/lib/commands/reflect.js +38 -0
- package/lib/commands/reflect.js.map +1 -0
- package/lib/commands/save-args.d.ts +26 -0
- package/lib/commands/save-args.d.ts.map +1 -0
- package/lib/commands/save-args.js +59 -0
- package/lib/commands/save-args.js.map +1 -0
- package/lib/commands/save.d.ts +38 -0
- package/lib/commands/save.d.ts.map +1 -0
- package/lib/commands/save.js +48 -0
- package/lib/commands/save.js.map +1 -0
- package/lib/commands/validate-args.d.ts +23 -0
- package/lib/commands/validate-args.d.ts.map +1 -0
- package/lib/commands/validate-args.js +60 -0
- package/lib/commands/validate-args.js.map +1 -0
- package/lib/commands/validate-driver.d.ts +10 -0
- package/lib/commands/validate-driver.d.ts.map +1 -0
- package/lib/commands/validate-driver.js +72 -0
- package/lib/commands/validate-driver.js.map +1 -0
- package/lib/commands/validate-report.d.ts +30 -0
- package/lib/commands/validate-report.d.ts.map +1 -0
- package/lib/commands/validate-report.js +87 -0
- package/lib/commands/validate-report.js.map +1 -0
- package/lib/commands/validate.d.ts +34 -0
- package/lib/commands/validate.d.ts.map +1 -0
- package/lib/commands/validate.js +41 -0
- package/lib/commands/validate.js.map +1 -0
- package/lib/commands/watch-args.d.ts +26 -0
- package/lib/commands/watch-args.d.ts.map +1 -0
- package/lib/commands/watch-args.js +59 -0
- package/lib/commands/watch-args.js.map +1 -0
- package/lib/commands/watch.d.ts +75 -0
- package/lib/commands/watch.d.ts.map +1 -0
- package/lib/commands/watch.js +112 -0
- package/lib/commands/watch.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/log-level.d.ts +39 -0
- package/lib/log-level.d.ts.map +1 -0
- package/lib/log-level.js +47 -0
- package/lib/log-level.js.map +1 -0
- package/lib/package-name.d.ts +33 -0
- package/lib/package-name.d.ts.map +1 -0
- package/lib/package-name.js +37 -0
- package/lib/package-name.js.map +1 -0
- package/lib/spawn-data-server.d.ts +102 -0
- package/lib/spawn-data-server.d.ts.map +1 -0
- package/lib/spawn-data-server.js +154 -0
- package/lib/spawn-data-server.js.map +1 -0
- package/lib/testing/echo-server.d.ts +11 -0
- package/lib/testing/echo-server.d.ts.map +1 -0
- package/lib/testing/echo-server.js +79 -0
- package/lib/testing/echo-server.js.map +1 -0
- package/package.json +105 -0
- package/src/cli-version.ts +32 -0
- package/src/cli.ts +139 -0
- package/src/commands/analyze-heap-args.ts +184 -0
- package/src/commands/analyze-heap.ts +76 -0
- package/src/commands/ast-ground-truth-args.ts +63 -0
- package/src/commands/ast-ground-truth-driver.ts +81 -0
- package/src/commands/ast-ground-truth.ts +49 -0
- package/src/commands/generate-transfer-model-args.ts +203 -0
- package/src/commands/generate-transfer-model-config.ts +128 -0
- package/src/commands/generate-transfer-model.ts +716 -0
- package/src/commands/harness-args.ts +429 -0
- package/src/commands/headless-harness.ts +165 -0
- package/src/commands/init-args.ts +273 -0
- package/src/commands/init-prompt.ts +149 -0
- package/src/commands/init-templates.ts +2037 -0
- package/src/commands/init-wizard.ts +378 -0
- package/src/commands/init-workspace.ts +442 -0
- package/src/commands/init.ts +554 -0
- package/src/commands/lint-grammar-args.ts +79 -0
- package/src/commands/lint-grammar-driver.ts +83 -0
- package/src/commands/lint-grammar-report.ts +74 -0
- package/src/commands/lint-grammar.ts +57 -0
- package/src/commands/measure-memory-args.ts +123 -0
- package/src/commands/measure-memory-driver.ts +128 -0
- package/src/commands/measure-memory.ts +143 -0
- package/src/commands/model-docs-args.ts +69 -0
- package/src/commands/model-docs-driver.ts +68 -0
- package/src/commands/model-docs-report.ts +133 -0
- package/src/commands/model-docs.ts +48 -0
- package/src/commands/projects-args.ts +62 -0
- package/src/commands/projects.ts +76 -0
- package/src/commands/query-args.ts +61 -0
- package/src/commands/query.ts +58 -0
- package/src/commands/reflect-args.ts +75 -0
- package/src/commands/reflect-driver.ts +76 -0
- package/src/commands/reflect-report.ts +138 -0
- package/src/commands/reflect.ts +51 -0
- package/src/commands/save-args.ts +74 -0
- package/src/commands/save.ts +84 -0
- package/src/commands/validate-args.ts +76 -0
- package/src/commands/validate-driver.ts +84 -0
- package/src/commands/validate-report.ts +95 -0
- package/src/commands/validate.ts +58 -0
- package/src/commands/watch-args.ts +74 -0
- package/src/commands/watch.ts +185 -0
- package/src/index.ts +16 -0
- package/src/log-level.ts +49 -0
- package/src/package-name.ts +39 -0
- package/src/spawn-data-server.ts +247 -0
- package/src/testing/echo-server.ts +93 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, 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
|
+
/**
|
|
11
|
+
* The interactive front end of `hydranium-cli init`.
|
|
12
|
+
*
|
|
13
|
+
* **The wizard's product is a command, not files.** It gathers answers, composes
|
|
14
|
+
* an argv, echoes it, and hands it to the same flag parser a non-interactive
|
|
15
|
+
* caller uses. Three properties fall out of that, and they are the reason to
|
|
16
|
+
* prefer it over an interactive mode reaching into the scaffolder:
|
|
17
|
+
*
|
|
18
|
+
* - every answer is flag-expressible by construction, so no wizard-only
|
|
19
|
+
* capability can drift in;
|
|
20
|
+
* - the echoed command is copy-pasteable into a README or CI, which is what
|
|
21
|
+
* keeps a recorded invocation re-runnable;
|
|
22
|
+
* - the wizard reduces to a pure `answers → argv` function
|
|
23
|
+
* ({@link composeInitArgv}), far cheaper to test than a flow that also writes
|
|
24
|
+
* files.
|
|
25
|
+
*
|
|
26
|
+
* What it asks is deliberately short. Most `init` inputs are *derived* and stay
|
|
27
|
+
* derived; the ones here are the ones no rule can reach:
|
|
28
|
+
*
|
|
29
|
+
* - the **file extension**, which is the defect the wizard exists to fix. A
|
|
30
|
+
* project name is silently also an extension choice, because the grammar
|
|
31
|
+
* defaults to the project name and the extension follows the grammar. No
|
|
32
|
+
* derivation can do better — with no second name supplied there is nothing to
|
|
33
|
+
* derive from — so it is asked, with the derived value merely offered as the
|
|
34
|
+
* default.
|
|
35
|
+
* - **workspace membership** and scope, which are facts about the surrounding
|
|
36
|
+
* repo rather than about the language, and so are asked only where a workspace
|
|
37
|
+
* was detected.
|
|
38
|
+
* - **publishability**, which is asked on every path because it is a licence
|
|
39
|
+
* posture rather than a packaging detail: the emission withholds publication
|
|
40
|
+
* AND declares itself UNLICENSED, so a default nobody was shown would settle
|
|
41
|
+
* the licence question by silence.
|
|
42
|
+
*
|
|
43
|
+
* The **language id is deliberately not asked**, though it is the third of the
|
|
44
|
+
* three independent names: its derivation was settled and built separately
|
|
45
|
+
* (`<project>`, qualified to `<project>-<grammar>` when the grammar is
|
|
46
|
+
* separately named), and offering it as a question invites overriding a rule
|
|
47
|
+
* that is right, in the one place a wrong answer renames a routing key every
|
|
48
|
+
* host has already bound.
|
|
49
|
+
*
|
|
50
|
+
* Grammars are collected **one at a time** — each one's own questions together,
|
|
51
|
+
* then an explicit "add another" — rather than as a count followed by N rounds
|
|
52
|
+
* of interrogation. A language project has one grammar far more often than it
|
|
53
|
+
* has three, and this shape charges the common case a single keystroke while
|
|
54
|
+
* letting the multi-grammar case grow naturally.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import * as path from 'node:path';
|
|
58
|
+
import { DEFAULT_INIT_HEADS, INIT_HEADS, type InitHead, isNonEmptyDir, kebab } from './init.js';
|
|
59
|
+
import { InitWizardCancelled, OPTIONAL_HEAD_CHOICES, type PromptPort } from './init-prompt.js';
|
|
60
|
+
import { type WorkspaceDetection } from './init-workspace.js';
|
|
61
|
+
|
|
62
|
+
/** One grammar as the wizard collected it. */
|
|
63
|
+
export interface InitGrammarAnswer {
|
|
64
|
+
/** PascalCase grammar name. */
|
|
65
|
+
readonly name: string;
|
|
66
|
+
/** File extension for its documents, without a leading dot. */
|
|
67
|
+
readonly extension: string;
|
|
68
|
+
/** Scaffold a GLSP diagram for this grammar. */
|
|
69
|
+
readonly diagram: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Everything the wizard collects. Pure data — {@link composeInitArgv} turns it into flags. */
|
|
73
|
+
export interface InitAnswers {
|
|
74
|
+
/** Directory to scaffold into. */
|
|
75
|
+
readonly targetDir: string;
|
|
76
|
+
/** PascalCase project name. */
|
|
77
|
+
readonly name: string;
|
|
78
|
+
/** The protocol heads to start. */
|
|
79
|
+
readonly heads: readonly InitHead[];
|
|
80
|
+
/** The grammars, in the order they were given. */
|
|
81
|
+
readonly grammars: readonly InitGrammarAnswer[];
|
|
82
|
+
/** Scaffold into a non-empty directory anyway. */
|
|
83
|
+
readonly force: boolean;
|
|
84
|
+
/** Join the surrounding npm workspace. */
|
|
85
|
+
readonly monorepo: boolean;
|
|
86
|
+
/** npm scope for the package name, e.g. `@acme`. */
|
|
87
|
+
readonly scope?: string;
|
|
88
|
+
/** Leave the package publishable, which the scaffold does not do by default. */
|
|
89
|
+
readonly public: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** PascalCase a directory name, splitting on any non-alphanumeric run. */
|
|
93
|
+
export function pascalCase(value: string): string {
|
|
94
|
+
return value
|
|
95
|
+
.split(/[^A-Za-z0-9]+/)
|
|
96
|
+
.filter(part => part.length > 0)
|
|
97
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
98
|
+
.join('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** True for a PascalCase identifier legal as a generated TypeScript symbol prefix. */
|
|
102
|
+
function isPascalIdentifier(value: string): boolean {
|
|
103
|
+
return /^[A-Za-z][A-Za-z0-9]*$/.test(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Compose the argv the answers stand for — the wizard's entire product.
|
|
108
|
+
*
|
|
109
|
+
* Minimal by design: a flag is emitted only where the answer differs from what
|
|
110
|
+
* `init` would derive anyway, so the echoed command reads as the short thing a
|
|
111
|
+
* person would have typed rather than as a transcript of the questions. The
|
|
112
|
+
* extension rides on `--grammar` because `--extensions` is grammar-scoped, and
|
|
113
|
+
* naming the grammar after the project leaves the derived language id alone.
|
|
114
|
+
*/
|
|
115
|
+
export function composeInitArgv(answers: InitAnswers): string[] {
|
|
116
|
+
const argv = [answers.targetDir, '--name', answers.name];
|
|
117
|
+
if (answers.heads.join(',') !== DEFAULT_INIT_HEADS.join(',')) {
|
|
118
|
+
argv.push('--heads', answers.heads.join(','));
|
|
119
|
+
}
|
|
120
|
+
const several = answers.grammars.length > 1;
|
|
121
|
+
for (const grammar of answers.grammars) {
|
|
122
|
+
// A lone grammar named after the project, taking the derived extension, is
|
|
123
|
+
// exactly what `--name` alone already means — so it contributes no flags
|
|
124
|
+
// and the echoed command stays the short one a person would have typed.
|
|
125
|
+
const customExtension = grammar.extension !== kebab(grammar.name);
|
|
126
|
+
if (!several && grammar.name === answers.name && !customExtension && !grammar.diagram) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
argv.push('--grammar', grammar.name);
|
|
130
|
+
if (customExtension) {
|
|
131
|
+
argv.push('--extensions', grammar.extension);
|
|
132
|
+
}
|
|
133
|
+
if (grammar.diagram) {
|
|
134
|
+
argv.push('--diagram');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (answers.force) {
|
|
138
|
+
argv.push('--force');
|
|
139
|
+
}
|
|
140
|
+
if (answers.monorepo) {
|
|
141
|
+
argv.push('--monorepo');
|
|
142
|
+
}
|
|
143
|
+
if (answers.scope !== undefined && answers.scope.length > 0) {
|
|
144
|
+
argv.push('--scope', answers.scope);
|
|
145
|
+
}
|
|
146
|
+
if (answers.public) {
|
|
147
|
+
argv.push('--public');
|
|
148
|
+
}
|
|
149
|
+
return argv;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Render an argv as a copy-pasteable command line, quoting only what a shell would mangle. */
|
|
153
|
+
export function formatCommand(argv: readonly string[]): string {
|
|
154
|
+
const quoted = argv.map(argument => (/^[A-Za-z0-9@,._/-]+$/.test(argument) ? argument : `'${argument.replace(/'/g, "'\\''")}'`));
|
|
155
|
+
return `hydranium-cli init ${quoted.join(' ')}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Validate a file extension, and reject one another grammar already claims. */
|
|
159
|
+
function extensionProblem(value: string, taken: ReadonlySet<string>): string | undefined {
|
|
160
|
+
const extension = value.replace(/^\./, '');
|
|
161
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(extension)) {
|
|
162
|
+
return `'${value}' is not a usable file extension.`;
|
|
163
|
+
}
|
|
164
|
+
// Checked here rather than left to the scaffolder so the answer is corrected
|
|
165
|
+
// while the grammar that owns it is still on screen. Langium routes documents
|
|
166
|
+
// to a language BY extension, so a duplicate is a silent mis-route.
|
|
167
|
+
return taken.has(extension) ? `'.${extension}' is already claimed by another grammar.` : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Collect one grammar's settings as a group — its own name, then its own
|
|
172
|
+
* extension — so the answers stay next to the thing they describe.
|
|
173
|
+
*/
|
|
174
|
+
async function askGrammar(
|
|
175
|
+
prompt: PromptPort,
|
|
176
|
+
index: number,
|
|
177
|
+
defaultName: string | undefined,
|
|
178
|
+
takenNames: ReadonlySet<string>,
|
|
179
|
+
takenExtensions: ReadonlySet<string>
|
|
180
|
+
): Promise<InitGrammarAnswer> {
|
|
181
|
+
const label = `Grammar ${index + 1}`;
|
|
182
|
+
const name = await prompt.text({
|
|
183
|
+
message: `${label} · name (PascalCase)`,
|
|
184
|
+
initialValue: defaultName,
|
|
185
|
+
placeholder: 'Domain',
|
|
186
|
+
validate: value => {
|
|
187
|
+
if (!isPascalIdentifier(value)) {
|
|
188
|
+
return `'${value}' is not a PascalCase identifier — letters and digits, starting with a letter.`;
|
|
189
|
+
}
|
|
190
|
+
return takenNames.has(value) ? `'${value}' is already used by another grammar.` : undefined;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const extension = await prompt.text({
|
|
194
|
+
message: `${label} · file extension`,
|
|
195
|
+
initialValue: kebab(name),
|
|
196
|
+
validate: value => extensionProblem(value, takenExtensions)
|
|
197
|
+
});
|
|
198
|
+
return { name, extension: extension.replace(/^\./, ''), diagram: false };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Ask for the protocol head set.
|
|
203
|
+
*
|
|
204
|
+
* `lsp` is not offered, it is stated: it owns the workspace, the build pipeline
|
|
205
|
+
* and the shared tier the other heads read through, so a project without it has
|
|
206
|
+
* nothing for them to serve. Presenting it as a checkbox would offer a choice
|
|
207
|
+
* the scaffolder then refuses.
|
|
208
|
+
*/
|
|
209
|
+
async function askHeads(prompt: PromptPort): Promise<InitHead[]> {
|
|
210
|
+
const optional = await prompt.multiselect({
|
|
211
|
+
message: 'Protocol heads — lsp is always included',
|
|
212
|
+
choices: OPTIONAL_HEAD_CHOICES,
|
|
213
|
+
initialValues: DEFAULT_INIT_HEADS.filter((head): head is Exclude<InitHead, 'lsp'> => head !== 'lsp')
|
|
214
|
+
});
|
|
215
|
+
const chosen = new Set<InitHead>(['lsp', ...optional]);
|
|
216
|
+
return INIT_HEADS.filter(head => chosen.has(head));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Mark the grammar a scaffolded diagram edits.
|
|
221
|
+
*
|
|
222
|
+
* Only a real question with `glsp` on AND several grammars: a diagram type
|
|
223
|
+
* binds exactly ONE grammar, and with a single grammar the scaffolder derives
|
|
224
|
+
* that answer already — so asking would offer a choice whose "no" it would
|
|
225
|
+
* silently overrule.
|
|
226
|
+
*/
|
|
227
|
+
async function askDiagramGrammar(
|
|
228
|
+
prompt: PromptPort,
|
|
229
|
+
heads: readonly InitHead[],
|
|
230
|
+
grammars: readonly InitGrammarAnswer[]
|
|
231
|
+
): Promise<InitGrammarAnswer[]> {
|
|
232
|
+
if (!heads.includes('glsp') || grammars.length < 2) {
|
|
233
|
+
return [...grammars];
|
|
234
|
+
}
|
|
235
|
+
const chosen = await prompt.select({
|
|
236
|
+
message: 'Which grammar does the GLSP diagram edit?',
|
|
237
|
+
choices: grammars.map(grammar => ({ value: grammar.name, label: grammar.name, hint: `.${grammar.extension}` })),
|
|
238
|
+
initialValue: grammars[0].name
|
|
239
|
+
});
|
|
240
|
+
return grammars.map(grammar => ({ ...grammar, diagram: grammar.name === chosen }));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Confirm scaffolding into a directory that already has content, returning
|
|
245
|
+
* whether `--force` is needed.
|
|
246
|
+
*
|
|
247
|
+
* Declining ends the session rather than looping back to the target question:
|
|
248
|
+
* the answer is "not here", and re-prompting for a directory would leave the
|
|
249
|
+
* echoed command describing a different run than the one that was started.
|
|
250
|
+
*/
|
|
251
|
+
async function confirmOccupiedTarget(prompt: PromptPort, targetDir: string, isOccupied: (targetDir: string) => boolean): Promise<boolean> {
|
|
252
|
+
if (!isOccupied(path.resolve(targetDir))) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const force = await prompt.confirm({ message: `${targetDir} is not empty — scaffold into it anyway?`, initialValue: false });
|
|
256
|
+
if (!force) {
|
|
257
|
+
prompt.outro(`Nothing scaffolded — ${targetDir} is not empty.`);
|
|
258
|
+
throw new InitWizardCancelled();
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Ask the workspace questions, or answer them "no" when there is no workspace to join. */
|
|
264
|
+
async function askWorkspaceMembership(
|
|
265
|
+
prompt: PromptPort,
|
|
266
|
+
detection: WorkspaceDetection | undefined
|
|
267
|
+
): Promise<{ monorepo: boolean; scope?: string }> {
|
|
268
|
+
if (detection === undefined) {
|
|
269
|
+
return { monorepo: false };
|
|
270
|
+
}
|
|
271
|
+
const covered =
|
|
272
|
+
detection.coveredBy === undefined
|
|
273
|
+
? `The root manifest has no entry covering it, so one is printed for you to add.`
|
|
274
|
+
: `Already covered by the "${detection.coveredBy}" workspaces entry.`;
|
|
275
|
+
// The placement is named, not just the root. The root alone reads as "this
|
|
276
|
+
// ran somewhere I did not ask for" whenever the command was issued from a
|
|
277
|
+
// different directory than the target, which is the normal case.
|
|
278
|
+
prompt.note(`Root: ${detection.rootDir}\nPlacing: ${detection.targetPath}\n${covered}`, 'Detected an npm workspace');
|
|
279
|
+
|
|
280
|
+
const monorepo = await prompt.confirm({ message: 'Scaffold as a member of that workspace?', initialValue: true });
|
|
281
|
+
if (!monorepo) {
|
|
282
|
+
return { monorepo: false };
|
|
283
|
+
}
|
|
284
|
+
const scope = await prompt.text({
|
|
285
|
+
message: "Package scope ('-' for none)",
|
|
286
|
+
initialValue: detection.scope ?? '-',
|
|
287
|
+
validate: value =>
|
|
288
|
+
value === '-' || /^@[a-z0-9][a-z0-9._-]*$/.test(value)
|
|
289
|
+
? undefined
|
|
290
|
+
: `'${value}' is not an npm scope — try '@acme', or '-' for none.`
|
|
291
|
+
});
|
|
292
|
+
return { monorepo, scope: scope === '-' ? undefined : scope };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Ask the packaging questions.
|
|
297
|
+
*
|
|
298
|
+
* Workspace membership and scope are facts about a surrounding repo, so they are
|
|
299
|
+
* reached only where one was detected. **Publishability is asked on every path**,
|
|
300
|
+
* including a standalone project with no workspace anywhere near it: the scaffold
|
|
301
|
+
* withholds publication because it also emits an UNLICENSED manifest, which makes
|
|
302
|
+
* the answer a licence posture rather than a packaging detail, and a licence
|
|
303
|
+
* posture must not be settled by a default nobody was shown. It knowingly spends
|
|
304
|
+
* a question on the shortest path — the alternative was stating the packaging in
|
|
305
|
+
* the echoed command instead, which informs whoever reads the output and still
|
|
306
|
+
* decides for whoever does not.
|
|
307
|
+
*
|
|
308
|
+
* Asked in the negative, so the offered answer is the emission's own default and
|
|
309
|
+
* a "yes" is the licence decision the adopter has to have made first.
|
|
310
|
+
*/
|
|
311
|
+
async function askPackaging(
|
|
312
|
+
prompt: PromptPort,
|
|
313
|
+
detection: WorkspaceDetection | undefined
|
|
314
|
+
): Promise<{ monorepo: boolean; scope?: string; public: boolean }> {
|
|
315
|
+
const membership = await askWorkspaceMembership(prompt, detection);
|
|
316
|
+
const isPublic = await prompt.confirm({ message: 'Publishable to npm (the manifest says UNLICENSED)?', initialValue: false });
|
|
317
|
+
return { ...membership, public: isPublic };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Run the wizard and return the argv it composed.
|
|
322
|
+
*
|
|
323
|
+
* `detect` is injected rather than called here so the flow stays testable
|
|
324
|
+
* without a real workspace on disk, and because detection needs the target
|
|
325
|
+
* directory — which is itself one of the answers.
|
|
326
|
+
*/
|
|
327
|
+
export async function runInitWizard(
|
|
328
|
+
prompt: PromptPort,
|
|
329
|
+
detect: (targetDir: string) => WorkspaceDetection | undefined,
|
|
330
|
+
options: { readonly targetDir?: string; readonly isOccupied?: (targetDir: string) => boolean } = {}
|
|
331
|
+
): Promise<string[]> {
|
|
332
|
+
prompt.intro('Scaffold a Hydranium language project');
|
|
333
|
+
|
|
334
|
+
const targetDir =
|
|
335
|
+
options.targetDir ??
|
|
336
|
+
(await prompt.text({
|
|
337
|
+
message: 'Target directory',
|
|
338
|
+
placeholder: './my-language',
|
|
339
|
+
validate: value => (value.length > 0 ? undefined : 'A target directory is required.')
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
// Asked FIRST, not left to the scaffolder — which checks only after the whole
|
|
343
|
+
// wizard has run, so a scaffold that cannot happen costs every remaining
|
|
344
|
+
// question before saying so. Answering yes is exactly `--force`, which keeps
|
|
345
|
+
// the recovery flag-expressible rather than a wizard-only escape.
|
|
346
|
+
const force = await confirmOccupiedTarget(prompt, targetDir, options.isOccupied ?? isNonEmptyDir);
|
|
347
|
+
|
|
348
|
+
const name = await prompt.text({
|
|
349
|
+
message: 'Project name (PascalCase)',
|
|
350
|
+
initialValue: pascalCase(path.basename(path.resolve(targetDir))),
|
|
351
|
+
validate: value =>
|
|
352
|
+
isPascalIdentifier(value) ? undefined : `'${value}' is not a PascalCase identifier — letters and digits, starting with a letter.`
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const heads = await askHeads(prompt);
|
|
356
|
+
|
|
357
|
+
// One grammar at a time, each with its own settings, then an explicit "add
|
|
358
|
+
// another" — so the single-grammar case costs two keystrokes and the
|
|
359
|
+
// multi-grammar one never asks for a count up front.
|
|
360
|
+
const grammars: InitGrammarAnswer[] = [];
|
|
361
|
+
const takenNames = new Set<string>();
|
|
362
|
+
const takenExtensions = new Set<string>();
|
|
363
|
+
do {
|
|
364
|
+
const grammar = await askGrammar(prompt, grammars.length, grammars.length === 0 ? name : undefined, takenNames, takenExtensions);
|
|
365
|
+
grammars.push(grammar);
|
|
366
|
+
takenNames.add(grammar.name);
|
|
367
|
+
takenExtensions.add(grammar.extension);
|
|
368
|
+
} while (await prompt.confirm({ message: 'Add another grammar?', initialValue: false }));
|
|
369
|
+
|
|
370
|
+
const withDiagram = await askDiagramGrammar(prompt, heads, grammars);
|
|
371
|
+
const packaging = await askPackaging(prompt, detect(targetDir));
|
|
372
|
+
|
|
373
|
+
const argv = composeInitArgv({ targetDir, name, heads, grammars: withDiagram, force, ...packaging });
|
|
374
|
+
// Echoed as the LAST thing before the scaffold runs, because it is the
|
|
375
|
+
// artefact the session produces: everything above is how it was reached.
|
|
376
|
+
prompt.outro(formatCommand(argv));
|
|
377
|
+
return argv;
|
|
378
|
+
}
|