@nusoft/nuos-build-catalogue 0.9.0 → 0.10.1
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/dist/cli.d.ts +13 -0
- package/dist/cli.js +472 -0
- package/dist/commands/create.d.ts +70 -0
- package/dist/commands/create.js +341 -0
- package/dist/commands/format.d.ts +19 -0
- package/dist/commands/format.js +89 -0
- package/dist/commands/handlers.d.ts +35 -0
- package/dist/commands/handlers.js +132 -0
- package/dist/commands/init.d.ts +41 -0
- package/dist/commands/init.js +289 -0
- package/dist/commands/prompt.d.ts +44 -0
- package/dist/commands/prompt.js +100 -0
- package/dist/commands/write.d.ts +39 -0
- package/dist/commands/write.js +247 -0
- package/dist/embedder/ollama.d.ts +54 -0
- package/dist/embedder/ollama.js +164 -0
- package/dist/embedder/openai.d.ts +21 -0
- package/dist/embedder/openai.js +56 -0
- package/dist/embedder/select.d.ts +9 -0
- package/dist/embedder/select.js +27 -0
- package/dist/embedder/stub.d.ts +15 -0
- package/dist/embedder/stub.js +40 -0
- package/dist/embedder/types.d.ts +21 -0
- package/dist/embedder/types.js +6 -0
- package/dist/embedder/vertex.d.ts +41 -0
- package/dist/embedder/vertex.js +94 -0
- package/dist/indexer/chunk.d.ts +20 -0
- package/dist/indexer/chunk.js +196 -0
- package/dist/indexer/crawl.d.ts +20 -0
- package/dist/indexer/crawl.js +66 -0
- package/dist/indexer/metadata.d.ts +21 -0
- package/dist/indexer/metadata.js +126 -0
- package/dist/indexer/upsert.d.ts +26 -0
- package/dist/indexer/upsert.js +152 -0
- package/dist/migrate/parsers.d.ts +17 -0
- package/dist/migrate/parsers.js +123 -0
- package/dist/migrate/run.d.ts +22 -0
- package/dist/migrate/run.js +142 -0
- package/dist/migrate/store.d.ts +20 -0
- package/dist/migrate/store.js +52 -0
- package/dist/migrate/types.d.ts +57 -0
- package/dist/migrate/types.js +13 -0
- package/dist/regenerate/check.d.ts +11 -0
- package/dist/regenerate/check.js +97 -0
- package/dist/regenerate/diff.d.ts +18 -0
- package/dist/regenerate/diff.js +38 -0
- package/dist/regenerate/types.d.ts +52 -0
- package/dist/regenerate/types.js +14 -0
- package/dist/runtime/ac-parse.d.ts +63 -0
- package/dist/runtime/ac-parse.js +196 -0
- package/dist/runtime/markdown-edit.d.ts +53 -0
- package/dist/runtime/markdown-edit.js +101 -0
- package/dist/runtime/markdown-render.d.ts +27 -0
- package/dist/runtime/markdown-render.js +209 -0
- package/dist/runtime/mis-adapter.d.ts +35 -0
- package/dist/runtime/mis-adapter.js +364 -0
- package/dist/runtime/runtime.d.ts +20 -0
- package/dist/runtime/runtime.js +39 -0
- package/dist/search/format.d.ts +6 -0
- package/dist/search/format.js +23 -0
- package/dist/search/query.d.ts +29 -0
- package/dist/search/query.js +71 -0
- package/dist/store/open.d.ts +14 -0
- package/dist/store/open.js +16 -0
- package/package.json +5 -3
- package/templates/protocols/end-of-session.md +19 -0
- package/templates/protocols/persona-new.md +43 -0
- package/templates/protocols/start-of-session.md +19 -0
- package/templates/protocols/wu-new.md +52 -0
- package/templates/starter-kit/CLAUDE.md +73 -0
- package/templates/starter-kit/README.md +116 -0
- package/templates/starter-kit/docs/build/END-OF-SESSION.md +62 -0
- package/templates/starter-kit/docs/build/START-OF-SESSION.md +33 -0
- package/templates/starter-kit/docs/build/STATE.md +47 -0
- package/templates/starter-kit/docs/build/decisions/D001-template.md +38 -0
- package/templates/starter-kit/docs/build/decisions/_index.md +30 -0
- package/templates/starter-kit/docs/build/maps/01-template.md +126 -0
- package/templates/starter-kit/docs/build/maps/_index.md +63 -0
- package/templates/starter-kit/docs/build/open-questions/_index.md +26 -0
- package/templates/starter-kit/docs/build/personas/001-template.md +68 -0
- package/templates/starter-kit/docs/build/personas/_index.md +77 -0
- package/templates/starter-kit/docs/build/risks/_index.md +28 -0
- package/templates/starter-kit/docs/build/sessions/0000-00-00-template.md +47 -0
- package/templates/starter-kit/docs/build/sessions/_index.md +27 -0
- package/templates/starter-kit/docs/build/work-units/001-template.md +82 -0
- package/templates/starter-kit/docs/build/work-units/_index.md +34 -0
- package/templates/starter-kit/docs/contracts/_index.md +26 -0
- package/templates/starter-kit/docs/guides/_index.md +26 -0
- package/templates/starter-kit/docs/philosophy/_index.md +26 -0
- package/templates/starter-kit/methodfile.json +54 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nuos-catalogue init` — bootstrap a new project's catalogue.
|
|
3
|
+
*
|
|
4
|
+
* Single command that does what was previously a six-step manual scaffold:
|
|
5
|
+
* 1. mkdir docs/build/ + copy starter-kit content
|
|
6
|
+
* 2. Substitute {{PROJECT_NAME}} / {{PROJECT_TAGLINE}} / {{TODAY}} in
|
|
7
|
+
* STATE.md and methodfile.json
|
|
8
|
+
* 3. Copy the four protocols into .claude/commands/ (preserving
|
|
9
|
+
* existing files)
|
|
10
|
+
* 4. Append a "Build catalogue (NuOS Build Method)" section to
|
|
11
|
+
* CLAUDE.md (creating it if missing; preserving existing content)
|
|
12
|
+
* 5. Update .gitignore: !docs/build/ override (if `build/` is present
|
|
13
|
+
* anywhere, the catalogue would otherwise be silently ignored —
|
|
14
|
+
* same gotcha caught at nuos Session 53); ignore .nuos-catalogue/
|
|
15
|
+
* per D047
|
|
16
|
+
* 6. Run a first migrate to verify
|
|
17
|
+
*
|
|
18
|
+
* Companion command: `install-protocols` refreshes just step 3 from the
|
|
19
|
+
* canonical bodies bundled in this CLI package.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdir, readFile, writeFile, copyFile, readdir, access } from 'node:fs/promises';
|
|
22
|
+
import { existsSync, constants } from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { askUntilValid, validate } from './prompt.js';
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..', '..');
|
|
28
|
+
const TEMPLATES_ROOT = path.resolve(PACKAGE_ROOT, 'templates');
|
|
29
|
+
const PROTOCOL_FILES = [
|
|
30
|
+
'start-of-session.md',
|
|
31
|
+
'end-of-session.md',
|
|
32
|
+
'wu-new.md',
|
|
33
|
+
'persona-new.md',
|
|
34
|
+
];
|
|
35
|
+
export async function cmdInit(prompt, options = {}) {
|
|
36
|
+
const cwd = options.cwd ?? process.cwd();
|
|
37
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
38
|
+
// Refuse if docs/build/ already exists — this is a one-shot bootstrap.
|
|
39
|
+
if (existsSync(path.join(cwd, 'docs', 'build'))) {
|
|
40
|
+
return {
|
|
41
|
+
output: `nuos-catalogue init: docs/build/ already exists at ${cwd}. Refusing to clobber. If you want to refresh the protocols only, use \`nuos-catalogue install-protocols\` instead.`,
|
|
42
|
+
exitCode: 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Verify templates are bundled with this CLI install.
|
|
46
|
+
if (!existsSync(TEMPLATES_ROOT)) {
|
|
47
|
+
return {
|
|
48
|
+
output: `nuos-catalogue init: bundled templates not found at ${TEMPLATES_ROOT}. The CLI install is incomplete; reinstall the package.`,
|
|
49
|
+
exitCode: 1,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Gather inputs.
|
|
53
|
+
const projectDefault = path.basename(cwd);
|
|
54
|
+
let name = options.name;
|
|
55
|
+
let tagline = options.tagline;
|
|
56
|
+
let domain = options.domain;
|
|
57
|
+
let role = options.role;
|
|
58
|
+
if (!options.nonInteractive) {
|
|
59
|
+
prompt.print('Initialising a NuOS Build Method catalogue.');
|
|
60
|
+
prompt.print('');
|
|
61
|
+
if (!name) {
|
|
62
|
+
name = await askUntilValid(prompt, `Project name [${projectDefault}]: `, (v) => (v.trim().length === 0 ? null : null) // empty allowed; default applied below
|
|
63
|
+
);
|
|
64
|
+
if (!name.trim())
|
|
65
|
+
name = projectDefault;
|
|
66
|
+
}
|
|
67
|
+
if (!tagline) {
|
|
68
|
+
tagline = await askUntilValid(prompt, 'One-sentence tagline (what this project is): ', (v) => validate.nonEmpty(v, 'tagline'));
|
|
69
|
+
}
|
|
70
|
+
if (!domain) {
|
|
71
|
+
domain = (await prompt.ask('Domain (e.g. example.com; empty for none): ')).trim() || 'n/a';
|
|
72
|
+
}
|
|
73
|
+
if (!role) {
|
|
74
|
+
role = await prompt.askChoice('Project role?', ['consumer', 'standalone', 'harness']);
|
|
75
|
+
}
|
|
76
|
+
const confirm = await prompt.confirm(`About to create docs/build/, methodfile.json, .claude/commands/<protocols>, append to CLAUDE.md, update .gitignore, and run first migrate. Proceed?`, true);
|
|
77
|
+
if (!confirm) {
|
|
78
|
+
return { output: 'init cancelled by operator.', exitCode: 1 };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (!name)
|
|
83
|
+
name = projectDefault;
|
|
84
|
+
if (!tagline)
|
|
85
|
+
tagline = '';
|
|
86
|
+
if (!domain)
|
|
87
|
+
domain = 'n/a';
|
|
88
|
+
if (!role)
|
|
89
|
+
role = 'consumer';
|
|
90
|
+
}
|
|
91
|
+
const subs = {
|
|
92
|
+
'{{PROJECT_NAME}}': name,
|
|
93
|
+
'{{PROJECT_TAGLINE}}': tagline,
|
|
94
|
+
'{{PROJECT_DOMAIN}}': domain,
|
|
95
|
+
'{{PROJECT_ROLE}}': role,
|
|
96
|
+
'{{TODAY}}': today,
|
|
97
|
+
};
|
|
98
|
+
const log = [];
|
|
99
|
+
const log_line = (msg) => {
|
|
100
|
+
log.push(msg);
|
|
101
|
+
prompt.print(msg);
|
|
102
|
+
};
|
|
103
|
+
// Step 1: docs/build/ scaffold from starter-kit
|
|
104
|
+
log_line(' · creating docs/build/ from bundled starter-kit');
|
|
105
|
+
await copyDirWithSubstitution(path.join(TEMPLATES_ROOT, 'starter-kit', 'docs', 'build'), path.join(cwd, 'docs', 'build'), subs);
|
|
106
|
+
// Step 2: methodfile.json at repo root
|
|
107
|
+
log_line(' · writing methodfile.json at repo root');
|
|
108
|
+
const methodfileSrc = await readFile(path.join(TEMPLATES_ROOT, 'starter-kit', 'methodfile.json'), 'utf8');
|
|
109
|
+
await writeFile(path.join(cwd, 'methodfile.json'), substitute(methodfileSrc, subs), 'utf8');
|
|
110
|
+
// Step 3: copy protocols into .claude/commands/
|
|
111
|
+
const claudeCommandsDir = path.join(cwd, '.claude', 'commands');
|
|
112
|
+
await mkdir(claudeCommandsDir, { recursive: true });
|
|
113
|
+
for (const protocol of PROTOCOL_FILES) {
|
|
114
|
+
const dest = path.join(claudeCommandsDir, protocol);
|
|
115
|
+
const existed = existsSync(dest);
|
|
116
|
+
await copyFile(path.join(TEMPLATES_ROOT, 'protocols', protocol), dest);
|
|
117
|
+
log_line(` · ${existed ? 'overwrote' : 'installed'} .claude/commands/${protocol}`);
|
|
118
|
+
}
|
|
119
|
+
// Step 4: CLAUDE.md
|
|
120
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
121
|
+
const catalogueSection = renderCatalogueSection(name);
|
|
122
|
+
if (existsSync(claudeMdPath)) {
|
|
123
|
+
const existing = await readFile(claudeMdPath, 'utf8');
|
|
124
|
+
if (existing.includes('## Build catalogue (NuOS Build Method)')) {
|
|
125
|
+
log_line(' · CLAUDE.md already mentions the catalogue; not appending');
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const trailing = existing.endsWith('\n') ? '' : '\n';
|
|
129
|
+
await writeFile(claudeMdPath, `${existing}${trailing}\n${catalogueSection}\n`, 'utf8');
|
|
130
|
+
log_line(' · appended Build catalogue section to CLAUDE.md');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const stub = `# ${name} — Project Bootstrap\n\n${catalogueSection}\n`;
|
|
135
|
+
await writeFile(claudeMdPath, stub, 'utf8');
|
|
136
|
+
log_line(' · created CLAUDE.md with Build catalogue section');
|
|
137
|
+
}
|
|
138
|
+
// Step 5: .gitignore
|
|
139
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
140
|
+
await ensureGitignoreEntries(gitignorePath, log_line);
|
|
141
|
+
prompt.print('');
|
|
142
|
+
prompt.print(`✅ Catalogue initialised at ${path.join(cwd, 'docs/build')}`);
|
|
143
|
+
prompt.print('');
|
|
144
|
+
prompt.print('Next steps:');
|
|
145
|
+
prompt.print(' 1. Set env vars in your shell profile so the CLI knows where this catalogue lives:');
|
|
146
|
+
prompt.print(` export NUOS_CATALOGUE_BUILD_ROOT="${path.join(cwd, 'docs/build')}"`);
|
|
147
|
+
prompt.print(` export NUOS_CATALOGUE_WORKFLOWS="${path.join(cwd, '.nuos-catalogue/workflows.json')}"`);
|
|
148
|
+
prompt.print(' 2. Edit docs/build/STATE.md to describe the actual current state of this project.');
|
|
149
|
+
prompt.print(' 3. File the first WU: `nuos-catalogue wu create`');
|
|
150
|
+
prompt.print('');
|
|
151
|
+
prompt.print('To refresh protocols only later (without re-running init): `nuos-catalogue install-protocols`');
|
|
152
|
+
return { output: '', exitCode: 0 };
|
|
153
|
+
}
|
|
154
|
+
export async function cmdInstallProtocols(prompt, options = {}) {
|
|
155
|
+
const cwd = options.cwd ?? process.cwd();
|
|
156
|
+
if (!existsSync(TEMPLATES_ROOT)) {
|
|
157
|
+
return {
|
|
158
|
+
output: `nuos-catalogue install-protocols: bundled templates not found at ${TEMPLATES_ROOT}.`,
|
|
159
|
+
exitCode: 1,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const claudeCommandsDir = path.join(cwd, '.claude', 'commands');
|
|
163
|
+
await mkdir(claudeCommandsDir, { recursive: true });
|
|
164
|
+
const lines = [];
|
|
165
|
+
for (const protocol of PROTOCOL_FILES) {
|
|
166
|
+
const src = path.join(TEMPLATES_ROOT, 'protocols', protocol);
|
|
167
|
+
const dest = path.join(claudeCommandsDir, protocol);
|
|
168
|
+
let action = 'created';
|
|
169
|
+
if (existsSync(dest)) {
|
|
170
|
+
const [srcContent, destContent] = await Promise.all([
|
|
171
|
+
readFile(src, 'utf8'),
|
|
172
|
+
readFile(dest, 'utf8'),
|
|
173
|
+
]);
|
|
174
|
+
action = srcContent === destContent ? 'unchanged' : 'updated';
|
|
175
|
+
}
|
|
176
|
+
if (action !== 'unchanged') {
|
|
177
|
+
await copyFile(src, dest);
|
|
178
|
+
}
|
|
179
|
+
lines.push(` ${action.padEnd(10)} .claude/commands/${protocol}`);
|
|
180
|
+
}
|
|
181
|
+
prompt.print(`Refreshing protocols at ${claudeCommandsDir}:`);
|
|
182
|
+
for (const l of lines)
|
|
183
|
+
prompt.print(l);
|
|
184
|
+
return { output: '', exitCode: 0 };
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Helpers
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
function substitute(content, subs) {
|
|
190
|
+
let result = content;
|
|
191
|
+
for (const [key, value] of Object.entries(subs)) {
|
|
192
|
+
result = result.split(key).join(value);
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
async function copyDirWithSubstitution(src, dest, subs) {
|
|
197
|
+
await mkdir(dest, { recursive: true });
|
|
198
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
const entryName = entry.name;
|
|
201
|
+
const srcPath = path.join(src, entryName);
|
|
202
|
+
const destPath = path.join(dest, entryName);
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
await copyDirWithSubstitution(srcPath, destPath, subs);
|
|
205
|
+
}
|
|
206
|
+
else if (entry.isFile()) {
|
|
207
|
+
const content = await readFile(srcPath, 'utf8');
|
|
208
|
+
await writeFile(destPath, substitute(content, subs), 'utf8');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function ensureGitignoreEntries(gitignorePath, log_line) {
|
|
213
|
+
let existing = '';
|
|
214
|
+
try {
|
|
215
|
+
await access(gitignorePath, constants.F_OK);
|
|
216
|
+
existing = await readFile(gitignorePath, 'utf8');
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// No .gitignore — we'll create one with just the catalogue rules.
|
|
220
|
+
}
|
|
221
|
+
const additions = [];
|
|
222
|
+
const hasUnanchoredBuild = /(?:^|\n)\s*build\/\s*(?:\n|$)/.test(existing);
|
|
223
|
+
const hasOverride = /!docs\/build\/(?:\*\*)?/.test(existing);
|
|
224
|
+
const hasNuosCatalogueIgnore = /^\s*\.nuos-catalogue\//m.test(existing);
|
|
225
|
+
if (hasUnanchoredBuild && !hasOverride) {
|
|
226
|
+
additions.push('', '# Override: docs/build/ is the NuOS Build Method catalogue, NOT a build artefact.', '# The unanchored `build/` rule above matches it; this negation pattern keeps the', '# catalogue tracked. Same gotcha caught at nuos Session 53.', '!docs/build/', '!docs/build/**');
|
|
227
|
+
}
|
|
228
|
+
if (!hasNuosCatalogueIgnore) {
|
|
229
|
+
additions.push('', '# NuOS Build Method catalogue — local workflow store (per nuos D047).', '# Markdown is canonical in Mode 1; the JSON store is regenerated by', '# `nuos-catalogue migrate` and does not need to live in git.', '.nuos-catalogue/');
|
|
230
|
+
}
|
|
231
|
+
if (additions.length === 0) {
|
|
232
|
+
log_line(' · .gitignore already has the catalogue entries; nothing to add');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const trailing = existing.endsWith('\n') || existing.length === 0 ? '' : '\n';
|
|
236
|
+
const newContent = `${existing}${trailing}${additions.join('\n')}\n`;
|
|
237
|
+
await writeFile(gitignorePath, newContent, 'utf8');
|
|
238
|
+
log_line(existing.length === 0
|
|
239
|
+
? ' · created .gitignore with catalogue rules'
|
|
240
|
+
: ' · appended catalogue rules to .gitignore');
|
|
241
|
+
}
|
|
242
|
+
function renderCatalogueSection(projectName) {
|
|
243
|
+
return `## Build catalogue (NuOS Build Method)
|
|
244
|
+
|
|
245
|
+
This repo runs **the NuOS Build Method**. The catalogue lives at [docs/build/](docs/build/) and tracks work units, decisions, open questions, personas, sessions, and risks.
|
|
246
|
+
|
|
247
|
+
### At the start of every session
|
|
248
|
+
|
|
249
|
+
Run \`/start-of-session\` (or follow [docs/build/START-OF-SESSION.md](docs/build/START-OF-SESSION.md)).
|
|
250
|
+
|
|
251
|
+
### At the end of every session
|
|
252
|
+
|
|
253
|
+
Run \`/end-of-session\`. **Without it, work is lost.**
|
|
254
|
+
|
|
255
|
+
### Daily use via the CLI
|
|
256
|
+
|
|
257
|
+
Set these env vars in your shell profile so commands work without flags:
|
|
258
|
+
|
|
259
|
+
\`\`\`bash
|
|
260
|
+
export NUOS_CATALOGUE_BUILD_ROOT="$(pwd)/docs/build"
|
|
261
|
+
export NUOS_CATALOGUE_WORKFLOWS="$(pwd)/.nuos-catalogue/workflows.json"
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
Then:
|
|
265
|
+
|
|
266
|
+
\`\`\`bash
|
|
267
|
+
nuos-catalogue wu create # interactive — file a new WU
|
|
268
|
+
nuos-catalogue wu list # what's in flight
|
|
269
|
+
nuos-catalogue wu advance <handle> --to=in_progress
|
|
270
|
+
nuos-catalogue wu tick <handle> --index=N --evidence="commit abc123"
|
|
271
|
+
nuos-catalogue decision create
|
|
272
|
+
nuos-catalogue question create
|
|
273
|
+
nuos-catalogue regenerate # check store-vs-disk drift
|
|
274
|
+
nuos-catalogue summary # totals by register
|
|
275
|
+
\`\`\`
|
|
276
|
+
|
|
277
|
+
To refresh the protocol bodies later (after a CLI upgrade):
|
|
278
|
+
|
|
279
|
+
\`\`\`bash
|
|
280
|
+
nuos-catalogue install-protocols
|
|
281
|
+
\`\`\`
|
|
282
|
+
|
|
283
|
+
### What never to do
|
|
284
|
+
|
|
285
|
+
- Never make architectural decisions without recording them in \`docs/build/decisions/\`
|
|
286
|
+
- Never start work outside the active work unit without recording why
|
|
287
|
+
- Never skip end-of-session
|
|
288
|
+
- Never modify a committed \`accepted\` decision file (use \`decision supersede\` instead)`;
|
|
289
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* readline-based prompt helpers for interactive CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Built on node:readline/promises (no new deps). The prompt object
|
|
5
|
+
* carries an open readline interface; callers are responsible for
|
|
6
|
+
* closing it via `prompt.close()` when done. The pattern is:
|
|
7
|
+
*
|
|
8
|
+
* const p = openPrompt();
|
|
9
|
+
* try {
|
|
10
|
+
* const title = await p.ask('Title: ');
|
|
11
|
+
* // ...
|
|
12
|
+
* } finally {
|
|
13
|
+
* p.close();
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Tests substitute a mock implementation by injecting a Prompt object
|
|
17
|
+
* directly to the create handlers; the readline-backed openPrompt is
|
|
18
|
+
* only invoked from the CLI shell.
|
|
19
|
+
*/
|
|
20
|
+
export interface Prompt {
|
|
21
|
+
ask(question: string): Promise<string>;
|
|
22
|
+
askMultiline(question: string, sentinel?: string): Promise<string>;
|
|
23
|
+
askChoice(question: string, choices: string[]): Promise<string>;
|
|
24
|
+
confirm(question: string, defaultYes?: boolean): Promise<boolean>;
|
|
25
|
+
print(line: string): void;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare function openPrompt(): Prompt;
|
|
29
|
+
/**
|
|
30
|
+
* Validation helpers that return `null` when valid, error string when not.
|
|
31
|
+
*/
|
|
32
|
+
export declare const validate: {
|
|
33
|
+
nonEmpty(value: string, fieldName: string): string | null;
|
|
34
|
+
matches(value: string, pattern: RegExp, fieldName: string, hint?: string): string | null;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Repeatedly ask a question until validation passes. Validators can
|
|
38
|
+
* return null (valid) or an error string (invalid; will be shown and
|
|
39
|
+
* the question re-asked).
|
|
40
|
+
*/
|
|
41
|
+
export declare function askUntilValid(p: Prompt, question: string, validator: (value: string) => string | null, options?: {
|
|
42
|
+
multiline?: boolean;
|
|
43
|
+
sentinel?: string;
|
|
44
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* readline-based prompt helpers for interactive CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Built on node:readline/promises (no new deps). The prompt object
|
|
5
|
+
* carries an open readline interface; callers are responsible for
|
|
6
|
+
* closing it via `prompt.close()` when done. The pattern is:
|
|
7
|
+
*
|
|
8
|
+
* const p = openPrompt();
|
|
9
|
+
* try {
|
|
10
|
+
* const title = await p.ask('Title: ');
|
|
11
|
+
* // ...
|
|
12
|
+
* } finally {
|
|
13
|
+
* p.close();
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Tests substitute a mock implementation by injecting a Prompt object
|
|
17
|
+
* directly to the create handlers; the readline-backed openPrompt is
|
|
18
|
+
* only invoked from the CLI shell.
|
|
19
|
+
*/
|
|
20
|
+
import { createInterface } from 'node:readline/promises';
|
|
21
|
+
import { stdin, stdout } from 'node:process';
|
|
22
|
+
export function openPrompt() {
|
|
23
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
24
|
+
return {
|
|
25
|
+
async ask(question) {
|
|
26
|
+
return (await rl.question(question)).trim();
|
|
27
|
+
},
|
|
28
|
+
async askMultiline(question, sentinel = '.') {
|
|
29
|
+
stdout.write(`${question}\n (end with a single line containing only "${sentinel}")\n`);
|
|
30
|
+
const lines = [];
|
|
31
|
+
while (true) {
|
|
32
|
+
const line = await rl.question('');
|
|
33
|
+
if (line.trim() === sentinel)
|
|
34
|
+
break;
|
|
35
|
+
lines.push(line);
|
|
36
|
+
}
|
|
37
|
+
return lines.join('\n').trim();
|
|
38
|
+
},
|
|
39
|
+
async askChoice(question, choices) {
|
|
40
|
+
const numbered = choices.map((c, i) => ` ${i + 1}. ${c}`).join('\n');
|
|
41
|
+
while (true) {
|
|
42
|
+
const answer = (await rl.question(`${question}\n${numbered}\nChoose (1–${choices.length}): `)).trim();
|
|
43
|
+
const idx = parseInt(answer, 10);
|
|
44
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= choices.length) {
|
|
45
|
+
return choices[idx - 1];
|
|
46
|
+
}
|
|
47
|
+
// Allow typing the choice text directly.
|
|
48
|
+
const direct = choices.find((c) => c.toLowerCase() === answer.toLowerCase());
|
|
49
|
+
if (direct)
|
|
50
|
+
return direct;
|
|
51
|
+
stdout.write(`(unrecognised — try a number 1–${choices.length} or the choice text)\n`);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
async confirm(question, defaultYes = true) {
|
|
55
|
+
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
|
|
56
|
+
const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
|
|
57
|
+
if (answer === '')
|
|
58
|
+
return defaultYes;
|
|
59
|
+
return answer === 'y' || answer === 'yes';
|
|
60
|
+
},
|
|
61
|
+
print(line) {
|
|
62
|
+
stdout.write(`${line}\n`);
|
|
63
|
+
},
|
|
64
|
+
close() {
|
|
65
|
+
rl.close();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validation helpers that return `null` when valid, error string when not.
|
|
71
|
+
*/
|
|
72
|
+
export const validate = {
|
|
73
|
+
nonEmpty(value, fieldName) {
|
|
74
|
+
if (value.trim().length === 0)
|
|
75
|
+
return `${fieldName} must be non-empty`;
|
|
76
|
+
return null;
|
|
77
|
+
},
|
|
78
|
+
matches(value, pattern, fieldName, hint) {
|
|
79
|
+
if (!pattern.test(value)) {
|
|
80
|
+
return `${fieldName} did not match expected pattern${hint ? ` (${hint})` : ''}`;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Repeatedly ask a question until validation passes. Validators can
|
|
87
|
+
* return null (valid) or an error string (invalid; will be shown and
|
|
88
|
+
* the question re-asked).
|
|
89
|
+
*/
|
|
90
|
+
export async function askUntilValid(p, question, validator, options = {}) {
|
|
91
|
+
while (true) {
|
|
92
|
+
const value = options.multiline
|
|
93
|
+
? await p.askMultiline(question, options.sentinel)
|
|
94
|
+
: await p.ask(question);
|
|
95
|
+
const error = validator(value);
|
|
96
|
+
if (!error)
|
|
97
|
+
return value;
|
|
98
|
+
p.print(`(${error})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase H part 2 — flag-driven write commands.
|
|
3
|
+
*
|
|
4
|
+
* Each handler:
|
|
5
|
+
* 1. Validates the flags
|
|
6
|
+
* 2. Looks up the target record from the store
|
|
7
|
+
* 3. Builds a typed `CaptureInput` for the relevant workflow
|
|
8
|
+
* 4. Drives the NuFlow lifecycle through the runtime
|
|
9
|
+
* 5. Reports the result
|
|
10
|
+
*
|
|
11
|
+
* No interactive prompts; flag-driven only. Interactive `create`
|
|
12
|
+
* commands are deferred (Phase H part 3).
|
|
13
|
+
*/
|
|
14
|
+
import type { NuFlowRuntime } from '@nusoft/nuflow';
|
|
15
|
+
import type { WorkflowStore } from '../migrate/store.js';
|
|
16
|
+
export interface WriteHandlerResult {
|
|
17
|
+
output: string;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function cmdWuAdvance(store: WorkflowStore, runtime: NuFlowRuntime, args: {
|
|
21
|
+
handle?: string;
|
|
22
|
+
to?: string;
|
|
23
|
+
reason?: string;
|
|
24
|
+
}): Promise<WriteHandlerResult>;
|
|
25
|
+
export declare function cmdWuTick(store: WorkflowStore, runtime: NuFlowRuntime, args: {
|
|
26
|
+
handle?: string;
|
|
27
|
+
index?: number;
|
|
28
|
+
evidence?: string;
|
|
29
|
+
}): Promise<WriteHandlerResult>;
|
|
30
|
+
export declare function cmdDecisionSupersede(store: WorkflowStore, runtime: NuFlowRuntime, args: {
|
|
31
|
+
target?: string;
|
|
32
|
+
by?: string;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}): Promise<WriteHandlerResult>;
|
|
35
|
+
export declare function cmdQuestionResolve(store: WorkflowStore, runtime: NuFlowRuntime, args: {
|
|
36
|
+
qHandle?: string;
|
|
37
|
+
by?: string;
|
|
38
|
+
reason?: string;
|
|
39
|
+
}): Promise<WriteHandlerResult>;
|