gnd-workflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/bin/gnd-workflow.js +6 -0
- package/package.json +48 -0
- package/src/cli.js +319 -0
- package/src/install.js +268 -0
- package/src/path-policy.js +190 -0
- package/templates/workflow/agents/gnd-diver.agent.md +34 -0
- package/templates/workflow/agents/gnd-navigator.agent.md +164 -0
- package/templates/workflow/skills/gnd-chart/SKILL.md +184 -0
- package/templates/workflow/skills/gnd-critique/SKILL.md +200 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jared Geller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Great Northern Diver
|
|
2
|
+
|
|
3
|
+
A small installer for a planning, execution, and critique prompt set. Writes managed files into `.agents` inside the target repo so the workflow lives next to the code it operates on.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
One-shot scaffold. No global install or project dependency required.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx gnd-workflow@latest install
|
|
11
|
+
# or
|
|
12
|
+
pnpm dlx gnd-workflow@latest install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Target another repo or pin a version:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx gnd-workflow@latest install ../my-repo
|
|
19
|
+
npx gnd-workflow@0.1.0 install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What It Writes
|
|
23
|
+
|
|
24
|
+
The installer writes managed files into `.agents/`. The workflow itself creates
|
|
25
|
+
`.planning/` at runtime for structured plans:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
.agents/ ← managed by the installer
|
|
29
|
+
.gnd-version.json
|
|
30
|
+
agents/
|
|
31
|
+
gnd-diver.agent.md
|
|
32
|
+
gnd-navigator.agent.md
|
|
33
|
+
skills/
|
|
34
|
+
gnd-chart/SKILL.md
|
|
35
|
+
gnd-critique/SKILL.md
|
|
36
|
+
.planning/ ← created by gnd-chart and gnd-navigator
|
|
37
|
+
active-plan-*.md
|
|
38
|
+
archive/ ← completed plans moved here by gnd-critique
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `.gnd-version.json` records which gnd-workflow version scaffolded the files.
|
|
42
|
+
- `gnd-chart` is the planning skill.
|
|
43
|
+
- `gnd-navigator` dispatches approved plan legs.
|
|
44
|
+
- `gnd-diver` executes one leg.
|
|
45
|
+
- `gnd-critique` reviews delivered work and feeds corrections back into the process.
|
|
46
|
+
|
|
47
|
+
All of these are plain text. Whether to track them in git is up to you:
|
|
48
|
+
|
|
49
|
+
- **`.agents/`** — tracking lets collaborators (or yourself on another machine)
|
|
50
|
+
see the workflow files without re-running the installer. Ignoring keeps
|
|
51
|
+
generated files out of your repo; `npx gnd-workflow@latest install`
|
|
52
|
+
re-creates them.
|
|
53
|
+
- **`.planning/`** — tracking preserves plan history alongside code. Ignoring
|
|
54
|
+
treats plans as ephemeral working state.
|
|
55
|
+
|
|
56
|
+
Neither directory needs to be tracked or ignored for the workflow to function.
|
|
57
|
+
|
|
58
|
+
## Philosophy
|
|
59
|
+
|
|
60
|
+
This is an agentic-development-first workflow. The navigator commits and pushes
|
|
61
|
+
directly to the default branch — no feature branches, no pull requests, no
|
|
62
|
+
staging area. Plan → execute → critique → push, in a tight loop.
|
|
63
|
+
|
|
64
|
+
That model works well for solo and hobby projects where you're the only
|
|
65
|
+
collaborator and velocity matters more than ceremony
|
|
66
|
+
([peninsular-reveries](https://github.com/ironloon/peninsular-reveries) is
|
|
67
|
+
the project it was built around). It's a poor fit for teams that rely on branch
|
|
68
|
+
protection, code review gates, or CI pipelines that run before merge.
|
|
69
|
+
|
|
70
|
+
If your project needs those guardrails, you can still use the planning and
|
|
71
|
+
critique skills on their own — just override the navigator's landing step to
|
|
72
|
+
target a branch instead of pushing directly.
|
|
73
|
+
|
|
74
|
+
## Updating
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx gnd-workflow@latest install
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
If a managed file already exists:
|
|
81
|
+
|
|
82
|
+
- unchanged files are left alone
|
|
83
|
+
- changed files trigger an interactive prompt before overwrite
|
|
84
|
+
- non-interactive runs fail closed unless you pass `--force`
|
|
85
|
+
|
|
86
|
+
Flags:
|
|
87
|
+
|
|
88
|
+
- `--dry-run` shows what would change.
|
|
89
|
+
- `--force` replaces differing managed files without prompting.
|
|
90
|
+
- `--version` shows the installed version.
|
|
91
|
+
- `-C, --cwd <path>` resolves the target project root from a specific working directory.
|
|
92
|
+
|
|
93
|
+
The target project root must be a real directory; symlinked and junctioned roots are rejected.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm test
|
|
99
|
+
node ./bin/gnd-workflow.js help
|
|
100
|
+
node ./bin/gnd-workflow.js install --dry-run
|
|
101
|
+
npm pack --dry-run
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Maintainer release steps live in [RELEASING.md](RELEASING.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gnd-workflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Installable agent and skill files for coding projects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gnd-workflow": "bin/gnd-workflow.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/install.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src/!(*.test|*-test-helpers).js",
|
|
15
|
+
"templates",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test src/**/*.test.js package-smoke.test.js",
|
|
21
|
+
"prepublishOnly": "npm test"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"workflow",
|
|
25
|
+
"planning",
|
|
26
|
+
"orchestration",
|
|
27
|
+
"critique",
|
|
28
|
+
"project-planning",
|
|
29
|
+
"agent-workflow",
|
|
30
|
+
"developer-workflow",
|
|
31
|
+
"great-northern-diver"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/ironloon/the-great-northern-diver.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/ironloon/the-great-northern-diver#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/ironloon/the-great-northern-diver/issues"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { DEFAULT_INSTALL_DIR, installWorkflow, readPackageVersion } from "./install.js";
|
|
5
|
+
import { normalizePathForContent } from "./path-policy.js";
|
|
6
|
+
|
|
7
|
+
function printHelp(stream) {
|
|
8
|
+
stream.write(
|
|
9
|
+
[
|
|
10
|
+
"Great Northern Diver",
|
|
11
|
+
"",
|
|
12
|
+
"Usage:",
|
|
13
|
+
" gnd-workflow install [project-root] [options]",
|
|
14
|
+
" gnd-workflow help",
|
|
15
|
+
"",
|
|
16
|
+
`Writes managed agent files into ${normalizePathForContent(DEFAULT_INSTALL_DIR)} in the target project.`,
|
|
17
|
+
"",
|
|
18
|
+
"Options:",
|
|
19
|
+
" --force Replace differing managed files without prompting",
|
|
20
|
+
" --dry-run Print the install plan without writing files",
|
|
21
|
+
" -C, --cwd <path> Resolve paths from a specific working directory",
|
|
22
|
+
" --version Show version number",
|
|
23
|
+
" --help Show this help text",
|
|
24
|
+
""
|
|
25
|
+
].join("\n")
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatPath(projectRoot, filePath) {
|
|
30
|
+
return path.relative(projectRoot, filePath).replaceAll("\\", "/") || ".";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printFileGroup(stream, title, projectRoot, entries) {
|
|
34
|
+
if (entries.length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
stream.write(`${title}:\n`);
|
|
39
|
+
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
stream.write(` ${entry.status.padEnd(9)} ${formatPath(projectRoot, entry.path)}\n`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printConflicts(stream, conflicts) {
|
|
46
|
+
if (conflicts.length === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
stream.write("Conflicts requiring confirmation without --force:\n");
|
|
51
|
+
|
|
52
|
+
for (const conflict of conflicts) {
|
|
53
|
+
stream.write(` overwrite ${conflict.relativePath}\n`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseInstallArgs(argv, cwd) {
|
|
58
|
+
const { values, positionals } = parseArgs({
|
|
59
|
+
args: argv,
|
|
60
|
+
allowPositionals: true,
|
|
61
|
+
strict: true,
|
|
62
|
+
options: {
|
|
63
|
+
"dry-run": {
|
|
64
|
+
type: "boolean"
|
|
65
|
+
},
|
|
66
|
+
force: {
|
|
67
|
+
type: "boolean"
|
|
68
|
+
},
|
|
69
|
+
cwd: {
|
|
70
|
+
type: "string",
|
|
71
|
+
short: "C"
|
|
72
|
+
},
|
|
73
|
+
help: {
|
|
74
|
+
type: "boolean"
|
|
75
|
+
},
|
|
76
|
+
version: {
|
|
77
|
+
type: "boolean"
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (positionals.length > 1) {
|
|
83
|
+
throw new Error(`Unexpected positional argument '${positionals[1]}'.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const baseProjectRoot = values.cwd ? path.resolve(cwd, values.cwd) : cwd;
|
|
87
|
+
const options = {
|
|
88
|
+
projectRoot: baseProjectRoot,
|
|
89
|
+
dryRun: values["dry-run"] ?? false,
|
|
90
|
+
force: values.force ?? false,
|
|
91
|
+
version: values.version ?? false,
|
|
92
|
+
help: values.help ?? false
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (positionals.length === 1) {
|
|
96
|
+
options.projectRoot = path.resolve(baseProjectRoot, positionals[0]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return options;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatErrorMessage(error) {
|
|
103
|
+
return error?.message !== undefined ? String(error.message) : String(error);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatConflictPrompt(conflict) {
|
|
107
|
+
return [
|
|
108
|
+
"Managed file differs from the packaged version:",
|
|
109
|
+
` ${conflict.relativePath}`,
|
|
110
|
+
"Replace it? [y]es/[n]o/[a]ll: "
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const DEFAULT_PROMPT_TIMEOUT_MS = 300_000;
|
|
115
|
+
|
|
116
|
+
function questionWithTtyLifecycle(prompt, input, message, timeoutMs) {
|
|
117
|
+
if (input.readableEnded || input.destroyed) {
|
|
118
|
+
return Promise.resolve(null);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
let settled = false;
|
|
123
|
+
let timer = null;
|
|
124
|
+
|
|
125
|
+
const cleanup = () => {
|
|
126
|
+
if (timer !== null) clearTimeout(timer);
|
|
127
|
+
input.off("end", handleInputEnded);
|
|
128
|
+
input.off("close", handleInputClosed);
|
|
129
|
+
input.off("error", handleInputError);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const settle = (callback) => (value) => {
|
|
133
|
+
if (settled) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
settled = true;
|
|
138
|
+
cleanup();
|
|
139
|
+
callback(value);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleInputEnded = settle(() => {
|
|
143
|
+
prompt.close();
|
|
144
|
+
resolve(null);
|
|
145
|
+
});
|
|
146
|
+
const handleInputClosed = settle(() => {
|
|
147
|
+
prompt.close();
|
|
148
|
+
resolve(null);
|
|
149
|
+
});
|
|
150
|
+
const handleInputError = settle(reject);
|
|
151
|
+
const handleTimeout = settle(() => {
|
|
152
|
+
prompt.close();
|
|
153
|
+
resolve(null);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
input.once("end", handleInputEnded);
|
|
157
|
+
input.once("close", handleInputClosed);
|
|
158
|
+
input.once("error", handleInputError);
|
|
159
|
+
|
|
160
|
+
if (timeoutMs != null && timeoutMs > 0) {
|
|
161
|
+
timer = setTimeout(handleTimeout, timeoutMs);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
prompt.question(message).then(
|
|
165
|
+
settle(resolve),
|
|
166
|
+
settle(reject)
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createInteractiveConflictPrompter(input, output, options = {}) {
|
|
172
|
+
if (!input?.isTTY || !output?.isTTY) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const promptTimeoutMs = options.promptTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS;
|
|
177
|
+
let acceptAll = false;
|
|
178
|
+
let prompt = null;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
async confirmManagedFileConflict(conflict) {
|
|
182
|
+
if (acceptAll) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
prompt ??= createInterface({
|
|
187
|
+
input,
|
|
188
|
+
output
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
while (true) {
|
|
192
|
+
const answer = await questionWithTtyLifecycle(prompt, input, formatConflictPrompt(conflict), promptTimeoutMs);
|
|
193
|
+
|
|
194
|
+
if (answer === null) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const normalizedAnswer = answer.trim().toLowerCase();
|
|
199
|
+
|
|
200
|
+
if (normalizedAnswer === "y" || normalizedAnswer === "yes") {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (normalizedAnswer === "a" || normalizedAnswer === "all") {
|
|
205
|
+
acceptAll = true;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (normalizedAnswer === "" || normalizedAnswer === "n" || normalizedAnswer === "no") {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
output.write("Please answer y, n, or a.\n");
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
close() {
|
|
217
|
+
prompt?.close();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function printInstallResult(stream, result) {
|
|
223
|
+
stream.write("Great Northern Diver install summary\n\n");
|
|
224
|
+
stream.write(`Project root: ${result.projectRoot}\n`);
|
|
225
|
+
stream.write(`Install root: ${normalizePathForContent(result.installDir)}\n`);
|
|
226
|
+
|
|
227
|
+
if (result.dryRun) {
|
|
228
|
+
stream.write("Mode: dry-run\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
stream.write("\n");
|
|
232
|
+
printFileGroup(stream, "Managed files", result.projectRoot, result.managedFiles);
|
|
233
|
+
printConflicts(stream, result.conflicts ?? []);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function runInstallCommand(rest, io, promptController) {
|
|
237
|
+
const stdout = io.stdout ?? process.stdout;
|
|
238
|
+
const stderr = io.stderr ?? process.stderr;
|
|
239
|
+
const cwd = io.cwd ?? process.cwd();
|
|
240
|
+
const runInstallWorkflow = io.installWorkflow ?? installWorkflow;
|
|
241
|
+
|
|
242
|
+
let options;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
options = parseInstallArgs(rest, cwd);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
stderr.write(`${formatErrorMessage(error)}\n\n`);
|
|
248
|
+
printHelp(stderr);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (options.help) {
|
|
253
|
+
printHelp(stdout);
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (options.version) {
|
|
258
|
+
stdout.write(`${await readPackageVersion()}\n`);
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (options.dryRun && options.force) {
|
|
263
|
+
stderr.write("Warning: --force has no effect in --dry-run mode.\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = await runInstallWorkflow(
|
|
267
|
+
promptController !== null && !options.force && !options.dryRun
|
|
268
|
+
? {
|
|
269
|
+
...options,
|
|
270
|
+
confirmManagedFileConflict: promptController.confirmManagedFileConflict
|
|
271
|
+
}
|
|
272
|
+
: options
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
printInstallResult(stdout, result);
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function main(argv, io = {}) {
|
|
280
|
+
const stdout = io.stdout ?? process.stdout;
|
|
281
|
+
const stderr = io.stderr ?? process.stderr;
|
|
282
|
+
const stdin = io.stdin ?? process.stdin;
|
|
283
|
+
const [command = "install", ...rest] = argv;
|
|
284
|
+
const promptController = io.confirmManagedFileConflict
|
|
285
|
+
? {
|
|
286
|
+
confirmManagedFileConflict: io.confirmManagedFileConflict,
|
|
287
|
+
close() {}
|
|
288
|
+
}
|
|
289
|
+
: createInteractiveConflictPrompter(stdin, stderr, { promptTimeoutMs: io.promptTimeoutMs });
|
|
290
|
+
|
|
291
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
292
|
+
stdout.write(`${await readPackageVersion()}\n`);
|
|
293
|
+
promptController?.close();
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
298
|
+
printHelp(stdout);
|
|
299
|
+
promptController?.close();
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (command !== "install") {
|
|
304
|
+
stderr.write(`Unknown command '${command}'.\n\n`);
|
|
305
|
+
printHelp(stderr);
|
|
306
|
+
promptController?.close();
|
|
307
|
+
return 1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return await runInstallCommand(rest, io, promptController);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
stderr.write(`${formatErrorMessage(error)}\n`);
|
|
314
|
+
return 1;
|
|
315
|
+
} finally {
|
|
316
|
+
promptController?.close();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|