@nusoft/nuos-build-catalogue 0.12.0 → 0.14.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.js +54 -41
- package/dist/commands/init.d.ts +12 -2
- package/dist/commands/init.js +136 -74
- package/dist/commands/plan.d.ts +12 -0
- package/dist/commands/plan.js +83 -0
- package/dist/commands/write.js +16 -5
- package/dist/path-resolution.d.ts +68 -0
- package/dist/path-resolution.js +147 -0
- package/dist/runtime/ac-parse.js +10 -6
- package/dist/runtime/markdown-edit.d.ts +5 -0
- package/dist/runtime/markdown-edit.js +13 -6
- package/dist/runtime/mis-adapter.js +7 -2
- package/package.json +2 -2
- package/templates/hooks/install-hooks.sh +44 -0
- package/templates/hooks/post-commit +96 -0
- package/templates/hooks/pre-commit +162 -0
- package/templates/protocols/end-of-session.md +101 -13
- package/templates/protocols/persona-new.md +64 -30
- package/templates/protocols/plan-orientation.md +122 -0
- package/templates/protocols/start-of-session.md +52 -13
- package/templates/protocols/wu-new.md +75 -50
- package/templates/starter-kit/docs/build/GLOSSARY.md +115 -0
- package/templates/starter-kit/docs/build/STATE.md +30 -16
- package/templates/starter-kit/docs/build/WELCOME.md +79 -0
- package/templates/starter-kit/docs/build/architecture/_index.md +39 -0
- package/templates/starter-kit/docs/build/architecture/module-template.md +47 -0
- package/templates/starter-kit/docs/build/contracts/_index.md +39 -0
- package/templates/starter-kit/docs/build/contracts/contract-template.md +64 -0
- package/templates/starter-kit/docs/build/decisions/_index.md +21 -17
- package/templates/starter-kit/docs/build/design-system/_index.md +57 -0
- package/templates/starter-kit/docs/build/design-system/accessibility.md +77 -0
- package/templates/starter-kit/docs/build/design-system/components/_index.md +29 -0
- package/templates/starter-kit/docs/build/design-system/components/_template.md +60 -0
- package/templates/starter-kit/docs/build/design-system/patterns/_index.md +37 -0
- package/templates/starter-kit/docs/build/design-system/patterns/_template.md +57 -0
- package/templates/starter-kit/docs/build/design-system/tokens-colour.md +52 -0
- package/templates/starter-kit/docs/build/design-system/tokens-motion.md +42 -0
- package/templates/starter-kit/docs/build/design-system/tokens-radius-elevation.md +34 -0
- package/templates/starter-kit/docs/build/design-system/tokens-spacing.md +48 -0
- package/templates/starter-kit/docs/build/design-system/tokens-typography.md +46 -0
- package/templates/starter-kit/docs/build/design-system/voice.md +53 -0
- package/templates/starter-kit/docs/build/maps/01-template.md +15 -112
- package/templates/starter-kit/docs/build/maps/02-template.md +52 -0
- package/templates/starter-kit/docs/build/maps/03-template.md +46 -0
- package/templates/starter-kit/docs/build/maps/99-template-power-user-operational-plan.md +126 -0
- package/templates/starter-kit/docs/build/maps/_index.md +17 -52
- package/templates/starter-kit/docs/build/open-questions/_index.md +27 -13
- package/templates/starter-kit/docs/build/personas/_index.md +26 -60
- package/templates/starter-kit/docs/build/risks/_index.md +20 -13
- package/templates/starter-kit/docs/build/sessions/_index.md +18 -16
- package/templates/starter-kit/docs/build/ui-ux/_index.md +48 -0
- package/templates/starter-kit/docs/build/ui-ux/surface-template.md +72 -0
- package/templates/starter-kit/docs/build/work-units/001-template-simple.md +43 -0
- package/templates/starter-kit/docs/build/work-units/_index.md +18 -20
- package/templates/starter-kit/methodfile.json +19 -8
- /package/templates/starter-kit/docs/build/work-units/{001-template.md → 001-template-full.md} +0 -0
package/dist/cli.js
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
* Implementation note — uses minimist-free arg parsing to keep deps lean.
|
|
11
11
|
* If we need richer parsing later, swap in commander/yargs.
|
|
12
12
|
*/
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
13
|
// Static imports — these don't pull in NuVector / NuFlow transitively.
|
|
16
14
|
// init / migrate / regenerate / summary / list / show / install-protocols all
|
|
17
15
|
// work without those native deps being installed.
|
|
@@ -21,28 +19,7 @@ import { listRegister, showRecord, commandToRegister, listAcrossRegisters, } fro
|
|
|
21
19
|
import { runRegenerate } from './regenerate/check.js';
|
|
22
20
|
import { openPrompt } from './commands/prompt.js';
|
|
23
21
|
import { cmdInit, cmdInstallProtocols } from './commands/init.js';
|
|
24
|
-
|
|
25
|
-
// load NuVector or NuFlow transitively. Loading them at module-parse time
|
|
26
|
-
// would crash on platforms where the NuVector native binary isn't resolved
|
|
27
|
-
// (e.g. fresh npx installs before @nusoft/nuvector ships its platform-specific
|
|
28
|
-
// binaries as optionalDependencies). Lazy-load so the lightweight commands
|
|
29
|
-
// (init, migrate, etc.) work universally; the heavyweight commands degrade
|
|
30
|
-
// gracefully when their deps are missing.
|
|
31
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
32
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..');
|
|
33
|
-
// Defaults resolve in this order: env var > flag-supplied > package-relative
|
|
34
|
-
// fallback. The package-relative fallback only makes sense when running
|
|
35
|
-
// against the nuos catalogue as a sibling (the original WU 110 use case);
|
|
36
|
-
// for any other consumer (Sensight, NuTutor, etc.) the env vars or the
|
|
37
|
-
// per-command flags are the right path. CLAUDE.md guidance for adopters:
|
|
38
|
-
// set NUOS_CATALOGUE_BUILD_ROOT and NUOS_CATALOGUE_WORKFLOWS in your
|
|
39
|
-
// shell profile.
|
|
40
|
-
const DEFAULT_CATALOGUE_ROOT = process.env.NUOS_CATALOGUE_ROOT ?? path.resolve(PACKAGE_ROOT, '../nuos/docs');
|
|
41
|
-
const DEFAULT_BUILD_ROOT = process.env.NUOS_CATALOGUE_BUILD_ROOT ?? path.resolve(PACKAGE_ROOT, '../nuos/docs/build');
|
|
42
|
-
const DEFAULT_INDEX_DIR = process.env.NUOS_CATALOGUE_INDEX_DIR ?? path.resolve(PACKAGE_ROOT, '.nuos-catalogue');
|
|
43
|
-
const DEFAULT_INDEX_PATH = path.join(DEFAULT_INDEX_DIR, 'index.nv');
|
|
44
|
-
const DEFAULT_HASH_PATH = path.join(DEFAULT_INDEX_DIR, 'hashes.json');
|
|
45
|
-
const DEFAULT_WORKFLOWS_PATH = process.env.NUOS_CATALOGUE_WORKFLOWS ?? path.join(DEFAULT_INDEX_DIR, 'workflows.json');
|
|
22
|
+
import { resolveBuildRoot, resolveCatalogueRoot, resolveWorkflowsPath, resolveIndexPath, resolveHashPath, gitignoreCatalogueNote, } from './path-resolution.js';
|
|
46
23
|
function parseArgs(argv) {
|
|
47
24
|
const [command, ...rest] = argv;
|
|
48
25
|
const positional = [];
|
|
@@ -64,9 +41,10 @@ function parseArgs(argv) {
|
|
|
64
41
|
return { command: command ?? 'help', positional, flags };
|
|
65
42
|
}
|
|
66
43
|
async function cmdIndex(flags) {
|
|
67
|
-
const catalogueRoot =
|
|
68
|
-
const
|
|
69
|
-
const
|
|
44
|
+
const catalogueRoot = resolveCatalogueRoot(flags['catalogue']);
|
|
45
|
+
const buildRoot = resolveBuildRoot(flags['build-root']);
|
|
46
|
+
const indexPath = resolveIndexPath(buildRoot, flags['index']);
|
|
47
|
+
const hashPath = resolveHashPath(buildRoot, flags['hash-file']);
|
|
70
48
|
const { selectEmbedderFromEnv } = await import('./embedder/select.js');
|
|
71
49
|
const { openStore } = await import('./store/open.js');
|
|
72
50
|
const { runIndex } = await import('./indexer/upsert.js');
|
|
@@ -99,7 +77,8 @@ async function cmdSearch(positional, flags) {
|
|
|
99
77
|
console.error('Usage: nuos-catalogue search "<query>" [--kind=...] [--status=...] [--limit=N] [--json]');
|
|
100
78
|
process.exit(2);
|
|
101
79
|
}
|
|
102
|
-
const
|
|
80
|
+
const buildRoot = resolveBuildRoot(flags['build-root']);
|
|
81
|
+
const indexPath = resolveIndexPath(buildRoot, flags['index']);
|
|
103
82
|
const { selectEmbedderFromEnv } = await import('./embedder/select.js');
|
|
104
83
|
const { openStore } = await import('./store/open.js');
|
|
105
84
|
const { runSearch } = await import('./search/query.js');
|
|
@@ -132,8 +111,8 @@ async function cmdSearch(positional, flags) {
|
|
|
132
111
|
}
|
|
133
112
|
}
|
|
134
113
|
async function cmdMigrate(flags) {
|
|
135
|
-
const buildRoot =
|
|
136
|
-
const workflowsPath =
|
|
114
|
+
const buildRoot = resolveBuildRoot(flags['build-root']);
|
|
115
|
+
const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
|
|
137
116
|
const dryRun = Boolean(flags['dry-run']);
|
|
138
117
|
console.log(`migrating ${buildRoot}`);
|
|
139
118
|
console.log(` workflows file: ${workflowsPath}`);
|
|
@@ -161,6 +140,17 @@ async function cmdMigrate(flags) {
|
|
|
161
140
|
console.log(' Resolve by renaming the conflicting files (e.g. give them distinct number prefixes) then re-run migrate.');
|
|
162
141
|
}
|
|
163
142
|
console.log(`(${(report.durationMs / 1000).toFixed(2)}s)`);
|
|
143
|
+
// Surface a gitignore hint if the project's .gitignore is missing the
|
|
144
|
+
// `.nuos-catalogue/` entry. Silent if .gitignore is absent or correct.
|
|
145
|
+
// (We do this after the success block so the report is the first thing
|
|
146
|
+
// the operator reads; the note follows.)
|
|
147
|
+
if (!dryRun) {
|
|
148
|
+
const note = gitignoreCatalogueNote(buildRoot);
|
|
149
|
+
if (note) {
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(note);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
164
154
|
}
|
|
165
155
|
async function cmdRegisterDispatch(command, positional, flags) {
|
|
166
156
|
const register = commandToRegister(command);
|
|
@@ -169,8 +159,8 @@ async function cmdRegisterDispatch(command, positional, flags) {
|
|
|
169
159
|
process.exit(2);
|
|
170
160
|
}
|
|
171
161
|
const action = positional[0];
|
|
172
|
-
const
|
|
173
|
-
const
|
|
162
|
+
const buildRoot = resolveBuildRoot(flags['build-root']);
|
|
163
|
+
const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
|
|
174
164
|
const store = await openWorkflowStore(workflowsPath);
|
|
175
165
|
const asJson = Boolean(flags['json']);
|
|
176
166
|
switch (action) {
|
|
@@ -300,8 +290,8 @@ async function cmdRegisterDispatch(command, positional, flags) {
|
|
|
300
290
|
}
|
|
301
291
|
}
|
|
302
292
|
async function cmdRegenerate(flags) {
|
|
303
|
-
const buildRoot =
|
|
304
|
-
const workflowsPath =
|
|
293
|
+
const buildRoot = resolveBuildRoot(flags['build-root']);
|
|
294
|
+
const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
|
|
305
295
|
const write = Boolean(flags['write']);
|
|
306
296
|
const showDiffs = Boolean(flags['diff']);
|
|
307
297
|
const registerFilter = flags['register'] ? String(flags['register']) : undefined;
|
|
@@ -353,7 +343,8 @@ async function cmdRegenerate(flags) {
|
|
|
353
343
|
process.exit(report.differs > 0 || report.missing > 0 ? 1 : 0);
|
|
354
344
|
}
|
|
355
345
|
async function cmdSummary(flags) {
|
|
356
|
-
const
|
|
346
|
+
const buildRoot = resolveBuildRoot(flags['build-root']);
|
|
347
|
+
const workflowsPath = resolveWorkflowsPath(buildRoot, flags['workflows']);
|
|
357
348
|
const store = await openWorkflowStore(workflowsPath);
|
|
358
349
|
const { byRegister, total } = listAcrossRegisters(store);
|
|
359
350
|
if (Boolean(flags['json'])) {
|
|
@@ -371,7 +362,7 @@ function cmdHelp() {
|
|
|
371
362
|
console.log(`nuos-catalogue — NuOS build-catalogue tooling (WU 110, WU 111)
|
|
372
363
|
|
|
373
364
|
Usage:
|
|
374
|
-
nuos-catalogue init [--name=X --tagline="Y" --
|
|
365
|
+
nuos-catalogue init [--name=X --tagline="Y" --role=consumer --interactive]
|
|
375
366
|
(interactive bootstrap of docs/build/, methodfile.json, .claude/commands/<protocols>, CLAUDE.md, .gitignore overrides; refuses if docs/build/ already exists)
|
|
376
367
|
nuos-catalogue install-protocols
|
|
377
368
|
(refresh .claude/commands/<protocols> from this CLI's bundled canonical bodies)
|
|
@@ -387,6 +378,7 @@ Usage:
|
|
|
387
378
|
nuos-catalogue wu create (interactive — multi-step prompts)
|
|
388
379
|
nuos-catalogue wu advance <handle> --to=<status> [--reason="..."]
|
|
389
380
|
nuos-catalogue wu tick <handle> --index=N --evidence="..."
|
|
381
|
+
(--index is 1-based: --index=1 ticks the first AC)
|
|
390
382
|
nuos-catalogue decision list [--status=<s>] [--limit=N] [--json]
|
|
391
383
|
nuos-catalogue decision show <handle> [--json]
|
|
392
384
|
nuos-catalogue decision create (interactive)
|
|
@@ -399,17 +391,25 @@ Usage:
|
|
|
399
391
|
nuos-catalogue persona show <handle> [--json]
|
|
400
392
|
nuos-catalogue persona create (interactive — seven dimensions + acid-test per D046)
|
|
401
393
|
|
|
394
|
+
nuos-catalogue plan status show planning progress across the 5-phase arc
|
|
395
|
+
|
|
402
396
|
nuos-catalogue help
|
|
403
397
|
|
|
404
398
|
Handles accepted: canonical (wu-111, D046, Q009, P001) or friendly
|
|
405
399
|
(WU 111, 111, D45, Q9). Unambiguous integers ("111" under "wu show")
|
|
406
400
|
are normalised to the canonical form.
|
|
407
401
|
|
|
402
|
+
Default locations: when --build-root / --workflows / --catalogue are
|
|
403
|
+
omitted and the matching env vars are unset, the CLI walks up from the
|
|
404
|
+
current working directory looking for a docs/build/ directory (the
|
|
405
|
+
same way git finds its repo root). Invoke from anywhere inside the
|
|
406
|
+
project. The workflow store lives at <project-root>/.nuos-catalogue/.
|
|
407
|
+
|
|
408
408
|
Environment:
|
|
409
|
-
NUOS_CATALOGUE_BUILD_ROOT
|
|
410
|
-
NUOS_CATALOGUE_WORKFLOWS
|
|
411
|
-
NUOS_CATALOGUE_ROOT
|
|
412
|
-
NUOS_CATALOGUE_INDEX_DIR
|
|
409
|
+
NUOS_CATALOGUE_BUILD_ROOT override for --build-root (the catalogue's docs/build/ dir)
|
|
410
|
+
NUOS_CATALOGUE_WORKFLOWS override for --workflows (the JSON workflow store path)
|
|
411
|
+
NUOS_CATALOGUE_ROOT override for --catalogue (semantic-search index source)
|
|
412
|
+
NUOS_CATALOGUE_INDEX_DIR override for parent dir of index.nv + workflows.json
|
|
413
413
|
NUOS_CATALOGUE_EMBEDDER vertex | openai | stub (default: vertex)
|
|
414
414
|
GOOGLE_CLOUD_PROJECT required for vertex
|
|
415
415
|
GOOGLE_CLOUD_LOCATION optional (default: us-central1)
|
|
@@ -434,7 +434,7 @@ async function main() {
|
|
|
434
434
|
tagline: args.flags['tagline'] ? String(args.flags['tagline']) : undefined,
|
|
435
435
|
domain: args.flags['domain'] ? String(args.flags['domain']) : undefined,
|
|
436
436
|
role: args.flags['role'] ? String(args.flags['role']) : undefined,
|
|
437
|
-
|
|
437
|
+
interactive: Boolean(args.flags['interactive']),
|
|
438
438
|
});
|
|
439
439
|
if (result.output)
|
|
440
440
|
console.log(result.output);
|
|
@@ -473,6 +473,19 @@ async function main() {
|
|
|
473
473
|
case 'persona':
|
|
474
474
|
await cmdRegisterDispatch(args.command, args.positional, args.flags);
|
|
475
475
|
break;
|
|
476
|
+
case 'plan': {
|
|
477
|
+
const sub = args.positional[0];
|
|
478
|
+
if (sub === 'status') {
|
|
479
|
+
const { cmdPlanStatus } = await import('./commands/plan.js');
|
|
480
|
+
const code = await cmdPlanStatus({ cwd: process.cwd() });
|
|
481
|
+
if (code !== 0)
|
|
482
|
+
process.exit(code);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
console.error(`unknown plan subcommand: ${sub ?? '(none)'}`);
|
|
486
|
+
console.error('available: plan status');
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
476
489
|
case 'help':
|
|
477
490
|
case '--help':
|
|
478
491
|
case '-h':
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -30,12 +30,22 @@ import type { Prompt } from './prompt.js';
|
|
|
30
30
|
export interface InitOptions {
|
|
31
31
|
/** Target project root (default: cwd). */
|
|
32
32
|
cwd?: string;
|
|
33
|
-
/** Pre-supplied values; if
|
|
33
|
+
/** Pre-supplied values; if omitted, sensible defaults are used. */
|
|
34
34
|
name?: string;
|
|
35
35
|
tagline?: string;
|
|
36
36
|
domain?: string;
|
|
37
37
|
role?: string;
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Opt into the prompt flow (project name, tagline, role, confirm).
|
|
40
|
+
* Default is non-interactive: scaffolds immediately with sensible
|
|
41
|
+
* defaults. The real configuration happens during Phase A of the
|
|
42
|
+
* planning arc, not at init time.
|
|
43
|
+
*/
|
|
44
|
+
interactive?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* @deprecated kept only for backward compat with the older `--yes` flag;
|
|
47
|
+
* has no effect — init is always non-interactive unless `interactive` is set.
|
|
48
|
+
*/
|
|
39
49
|
nonInteractive?: boolean;
|
|
40
50
|
}
|
|
41
51
|
export interface InitResult {
|
package/dist/commands/init.js
CHANGED
|
@@ -30,7 +30,6 @@ import { mkdir, readFile, writeFile, readdir, access } from 'node:fs/promises';
|
|
|
30
30
|
import { existsSync, constants } from 'node:fs';
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
|
-
import { askUntilValid, validate } from './prompt.js';
|
|
34
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
35
34
|
const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..', '..');
|
|
36
35
|
const TEMPLATES_ROOT = path.resolve(PACKAGE_ROOT, 'templates');
|
|
@@ -39,6 +38,7 @@ const PROTOCOL_FILES = [
|
|
|
39
38
|
'end-of-session.md',
|
|
40
39
|
'wu-new.md',
|
|
41
40
|
'persona-new.md',
|
|
41
|
+
'plan-orientation.md',
|
|
42
42
|
];
|
|
43
43
|
/**
|
|
44
44
|
* One-line descriptions used in the frontmatter of installed protocol
|
|
@@ -46,10 +46,11 @@ const PROTOCOL_FILES = [
|
|
|
46
46
|
* read as an imperative summary.
|
|
47
47
|
*/
|
|
48
48
|
const PROTOCOL_DESCRIPTIONS = {
|
|
49
|
-
'start-of-session': 'Read
|
|
50
|
-
'end-of-session': '
|
|
51
|
-
'wu-new': '
|
|
52
|
-
'persona-new': '
|
|
49
|
+
'start-of-session': 'Read where the project is and propose the next concrete action',
|
|
50
|
+
'end-of-session': 'Capture what happened, update state, write session log, commit',
|
|
51
|
+
'wu-new': 'File a new work unit through a guided plain-English conversation',
|
|
52
|
+
'persona-new': 'File a new persona by walking the seven dimensions conversationally',
|
|
53
|
+
'plan-orientation': 'Phase A of planning — project description, personas, the horizon map',
|
|
53
54
|
};
|
|
54
55
|
const TOOLS = {
|
|
55
56
|
claude: {
|
|
@@ -93,44 +94,41 @@ export async function cmdInit(prompt, options = {}) {
|
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
// Gather inputs.
|
|
97
|
+
//
|
|
98
|
+
// Init is **zero-prompt by default**. The user wants `npx ... init` to
|
|
99
|
+
// just work — sensible defaults fill in everything, the scaffold goes
|
|
100
|
+
// down, and the real planning happens in /start-of-session (Phase A of
|
|
101
|
+
// the planning arc), where the AI walks the user through the project
|
|
102
|
+
// description, personas, and the horizon map IN CONTEXT.
|
|
103
|
+
//
|
|
104
|
+
// Old behavior (4 prompts: name, tagline, domain, role) wasn't load-
|
|
105
|
+
// bearing — tagline gets produced during Phase A; domain is a relic;
|
|
106
|
+
// role is a planning-time annotation rarely used downstream. Users who
|
|
107
|
+
// want the prompts back can pass --interactive.
|
|
96
108
|
const projectDefault = path.basename(cwd);
|
|
97
|
-
let name = options.name;
|
|
98
|
-
let tagline = options.tagline;
|
|
99
|
-
let domain = options.domain;
|
|
100
|
-
let role = options.role;
|
|
101
|
-
if (
|
|
102
|
-
prompt.print('
|
|
109
|
+
let name = options.name ?? projectDefault;
|
|
110
|
+
let tagline = options.tagline ?? '';
|
|
111
|
+
let domain = options.domain ?? '';
|
|
112
|
+
let role = options.role ?? 'consumer';
|
|
113
|
+
if (options.interactive) {
|
|
114
|
+
prompt.print('Setting up the catalogue.');
|
|
103
115
|
prompt.print('');
|
|
104
|
-
if (!name) {
|
|
105
|
-
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
name = projectDefault;
|
|
116
|
+
if (!options.name) {
|
|
117
|
+
const answer = (await prompt.ask(`Project name [${projectDefault}]: `)).trim();
|
|
118
|
+
if (answer)
|
|
119
|
+
name = answer;
|
|
109
120
|
}
|
|
110
|
-
if (!tagline) {
|
|
111
|
-
tagline = await
|
|
121
|
+
if (!options.tagline) {
|
|
122
|
+
tagline = (await prompt.ask('One-line description (or empty — Phase A will fill it in): ')).trim();
|
|
112
123
|
}
|
|
113
|
-
if (!
|
|
114
|
-
domain = (await prompt.ask('Domain (e.g. example.com; empty for none): ')).trim() || 'n/a';
|
|
115
|
-
}
|
|
116
|
-
if (!role) {
|
|
124
|
+
if (!options.role) {
|
|
117
125
|
role = await prompt.askChoice('Project role?', ['consumer', 'standalone', 'harness']);
|
|
118
126
|
}
|
|
119
|
-
const confirm = await prompt.confirm(`
|
|
127
|
+
const confirm = await prompt.confirm(`Create docs/build/, install protocols + hooks, set up the catalogue. Proceed?`, true);
|
|
120
128
|
if (!confirm) {
|
|
121
|
-
return { output: 'init cancelled
|
|
129
|
+
return { output: 'init cancelled.', exitCode: 1 };
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
|
-
else {
|
|
125
|
-
if (!name)
|
|
126
|
-
name = projectDefault;
|
|
127
|
-
if (!tagline)
|
|
128
|
-
tagline = '';
|
|
129
|
-
if (!domain)
|
|
130
|
-
domain = 'n/a';
|
|
131
|
-
if (!role)
|
|
132
|
-
role = 'consumer';
|
|
133
|
-
}
|
|
134
132
|
const subs = {
|
|
135
133
|
'{{PROJECT_NAME}}': name,
|
|
136
134
|
'{{PROJECT_TAGLINE}}': tagline,
|
|
@@ -138,10 +136,15 @@ export async function cmdInit(prompt, options = {}) {
|
|
|
138
136
|
'{{PROJECT_ROLE}}': role,
|
|
139
137
|
'{{TODAY}}': today,
|
|
140
138
|
};
|
|
139
|
+
// Per-step "· created X" messages are kept as an in-memory audit trail
|
|
140
|
+
// but NOT printed by default. The end-user-facing output is just a
|
|
141
|
+
// single "Done. Type /start-of-session" at the close. Pass `interactive`
|
|
142
|
+
// to see the per-step lines (useful for debugging).
|
|
141
143
|
const log = [];
|
|
142
144
|
const log_line = (msg) => {
|
|
143
145
|
log.push(msg);
|
|
144
|
-
|
|
146
|
+
if (options.interactive)
|
|
147
|
+
prompt.print(msg);
|
|
145
148
|
};
|
|
146
149
|
// Step 1: docs/build/ scaffold from starter-kit
|
|
147
150
|
log_line(' · creating docs/build/ from bundled starter-kit');
|
|
@@ -165,6 +168,12 @@ export async function cmdInit(prompt, options = {}) {
|
|
|
165
168
|
log_line(` · ${existed ? 'overwrote' : 'installed'} ${tool.destPath(slug)} (${tool.label})`);
|
|
166
169
|
}
|
|
167
170
|
}
|
|
171
|
+
// Step 3b: install the git hooks (pre-commit enforcement + post-commit
|
|
172
|
+
// auto-reindex). Two source files land in scripts/hooks/ so the consumer
|
|
173
|
+
// has the version-controlled source-of-truth; copies are pushed into
|
|
174
|
+
// .git/hooks/ so they fire immediately without the user re-running an
|
|
175
|
+
// installer.
|
|
176
|
+
await installHooks(cwd, log_line);
|
|
168
177
|
// Step 4: CLAUDE.md
|
|
169
178
|
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
170
179
|
const catalogueSection = renderCatalogueSection(name);
|
|
@@ -188,16 +197,10 @@ export async function cmdInit(prompt, options = {}) {
|
|
|
188
197
|
const gitignorePath = path.join(cwd, '.gitignore');
|
|
189
198
|
await ensureGitignoreEntries(gitignorePath, log_line);
|
|
190
199
|
prompt.print('');
|
|
191
|
-
prompt.print(
|
|
200
|
+
prompt.print('✅ Done.');
|
|
192
201
|
prompt.print('');
|
|
193
|
-
prompt.print('
|
|
194
|
-
prompt.print(' 1. Set env vars in your shell profile so the CLI knows where this catalogue lives:');
|
|
195
|
-
prompt.print(` export NUOS_CATALOGUE_BUILD_ROOT="${path.join(cwd, 'docs/build')}"`);
|
|
196
|
-
prompt.print(` export NUOS_CATALOGUE_WORKFLOWS="${path.join(cwd, '.nuos-catalogue/workflows.json')}"`);
|
|
197
|
-
prompt.print(' 2. Edit docs/build/STATE.md to describe the actual current state of this project.');
|
|
198
|
-
prompt.print(' 3. File the first WU: `nuos-catalogue wu create`');
|
|
202
|
+
prompt.print('Now type /start-of-session into Claude Code to begin.');
|
|
199
203
|
prompt.print('');
|
|
200
|
-
prompt.print('To refresh protocols only later (without re-running init): `nuos-catalogue install-protocols`');
|
|
201
204
|
return { output: '', exitCode: 0 };
|
|
202
205
|
}
|
|
203
206
|
export async function cmdInstallProtocols(prompt, options = {}) {
|
|
@@ -230,9 +233,76 @@ export async function cmdInstallProtocols(prompt, options = {}) {
|
|
|
230
233
|
prompt.print(`Refreshing protocols (Claude Code / OpenCode / Codex CLI):`);
|
|
231
234
|
for (const l of lines)
|
|
232
235
|
prompt.print(l);
|
|
236
|
+
// Refresh hooks too — same idempotent shape.
|
|
237
|
+
prompt.print('');
|
|
238
|
+
prompt.print(`Refreshing git hooks (pre-commit enforcement, post-commit auto-reindex):`);
|
|
239
|
+
await installHooks(cwd, (msg) => prompt.print(msg));
|
|
233
240
|
return { output: '', exitCode: 0 };
|
|
234
241
|
}
|
|
235
242
|
// ---------------------------------------------------------------------------
|
|
243
|
+
// installHooks — copy bundled hook sources into the consumer + activate them
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
/**
|
|
246
|
+
* Bundled hooks ship in templates/hooks/. Two source files (pre-commit,
|
|
247
|
+
* post-commit) land in the consumer's scripts/hooks/ so the maintainer
|
|
248
|
+
* has the version-controlled source. Copies also land in .git/hooks/ so
|
|
249
|
+
* they fire immediately. The bash install-hooks.sh script also lands in
|
|
250
|
+
* scripts/ so re-running it after a fresh clone reconstitutes .git/hooks/.
|
|
251
|
+
*
|
|
252
|
+
* Idempotent: byte-identical sources are skipped silently. Permissions
|
|
253
|
+
* are set executable on every install so a chmod isn't required.
|
|
254
|
+
*/
|
|
255
|
+
async function installHooks(cwd, log_line) {
|
|
256
|
+
const hooksTemplatesRoot = path.join(TEMPLATES_ROOT, 'hooks');
|
|
257
|
+
if (!existsSync(hooksTemplatesRoot)) {
|
|
258
|
+
log_line(' · (hooks bundle not present in this CLI install — skipping)');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// 1) Source-of-truth files in <cwd>/scripts/
|
|
262
|
+
const scriptsDir = path.join(cwd, 'scripts');
|
|
263
|
+
const scriptsHooksDir = path.join(scriptsDir, 'hooks');
|
|
264
|
+
await mkdir(scriptsHooksDir, { recursive: true });
|
|
265
|
+
const hookFiles = ['pre-commit', 'post-commit'];
|
|
266
|
+
for (const name of hookFiles) {
|
|
267
|
+
const src = path.join(hooksTemplatesRoot, name);
|
|
268
|
+
const dest = path.join(scriptsHooksDir, name);
|
|
269
|
+
await writeHookFile(src, dest, log_line, ` · `, `scripts/hooks/${name}`);
|
|
270
|
+
}
|
|
271
|
+
// install-hooks.sh — convenience bash installer; sits next to scripts/hooks/
|
|
272
|
+
const installerSrc = path.join(hooksTemplatesRoot, 'install-hooks.sh');
|
|
273
|
+
const installerDest = path.join(scriptsDir, 'install-hooks.sh');
|
|
274
|
+
await writeHookFile(installerSrc, installerDest, log_line, ` · `, `scripts/install-hooks.sh`);
|
|
275
|
+
// 2) Active copies in <cwd>/.git/hooks/ — only if .git/ exists. Some
|
|
276
|
+
// tests run init in a non-git directory; that's fine, skip the active
|
|
277
|
+
// install and let the user run install-hooks.sh later.
|
|
278
|
+
const gitHooksDir = path.join(cwd, '.git', 'hooks');
|
|
279
|
+
if (!existsSync(path.join(cwd, '.git'))) {
|
|
280
|
+
log_line(` · (no .git/ found at ${cwd} — skipping active hook install; run scripts/install-hooks.sh after \`git init\`)`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await mkdir(gitHooksDir, { recursive: true });
|
|
284
|
+
for (const name of hookFiles) {
|
|
285
|
+
const src = path.join(hooksTemplatesRoot, name);
|
|
286
|
+
const dest = path.join(gitHooksDir, name);
|
|
287
|
+
await writeHookFile(src, dest, log_line, ` · `, `.git/hooks/${name}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function writeHookFile(src, dest, log_line, prefix, label) {
|
|
291
|
+
const srcContent = await readFile(src, 'utf8');
|
|
292
|
+
let action = 'created';
|
|
293
|
+
if (existsSync(dest)) {
|
|
294
|
+
const destContent = await readFile(dest, 'utf8');
|
|
295
|
+
action = destContent === srcContent ? 'unchanged' : 'updated';
|
|
296
|
+
}
|
|
297
|
+
if (action !== 'unchanged') {
|
|
298
|
+
await writeFile(dest, srcContent, 'utf8');
|
|
299
|
+
}
|
|
300
|
+
// chmod +x — required for git to actually run them
|
|
301
|
+
const { chmod } = await import('node:fs/promises');
|
|
302
|
+
await chmod(dest, 0o755);
|
|
303
|
+
log_line(`${prefix}${action} ${label}`);
|
|
304
|
+
}
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
236
306
|
// Helpers
|
|
237
307
|
// ---------------------------------------------------------------------------
|
|
238
308
|
function substitute(content, subs) {
|
|
@@ -291,48 +361,40 @@ async function ensureGitignoreEntries(gitignorePath, log_line) {
|
|
|
291
361
|
function renderCatalogueSection(projectName) {
|
|
292
362
|
return `## Build catalogue (NuOS Build Method)
|
|
293
363
|
|
|
294
|
-
This
|
|
364
|
+
This project uses the **NuOS Build Method catalogue** at [docs/build/](docs/build/). It is the project's memory — who it's for, what's been built, what's been decided, what's still unknown, what's at risk. Eleven registers in plain Markdown. The catalogue stays current through two protocols that bookend every session.
|
|
295
365
|
|
|
296
|
-
|
|
366
|
+
**Start here** if you're new:
|
|
297
367
|
|
|
298
|
-
|
|
368
|
+
- [docs/build/WELCOME.md](docs/build/WELCOME.md) — what this catalogue is, in 5 minutes
|
|
369
|
+
- [docs/build/GLOSSARY.md](docs/build/GLOSSARY.md) — every term defined once
|
|
299
370
|
|
|
300
|
-
###
|
|
371
|
+
### Three commands
|
|
301
372
|
|
|
302
|
-
|
|
373
|
+
That's it. Everything else is automatic.
|
|
303
374
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
\`\`\`bash
|
|
309
|
-
export NUOS_CATALOGUE_BUILD_ROOT="$(pwd)/docs/build"
|
|
310
|
-
export NUOS_CATALOGUE_WORKFLOWS="$(pwd)/.nuos-catalogue/workflows.json"
|
|
375
|
+
\`\`\`text
|
|
376
|
+
/start-of-session — every time you begin working
|
|
377
|
+
/end-of-session — every time you stop
|
|
311
378
|
\`\`\`
|
|
312
379
|
|
|
313
|
-
|
|
380
|
+
(\`init\` runs once at the start; you've already done that.)
|
|
314
381
|
|
|
315
|
-
|
|
316
|
-
nuos-catalogue wu create # interactive — file a new WU
|
|
317
|
-
nuos-catalogue wu list # what's in flight
|
|
318
|
-
nuos-catalogue wu advance <handle> --to=in_progress
|
|
319
|
-
nuos-catalogue wu tick <handle> --index=N --evidence="commit abc123"
|
|
320
|
-
nuos-catalogue decision create
|
|
321
|
-
nuos-catalogue question create
|
|
322
|
-
nuos-catalogue regenerate # check store-vs-disk drift
|
|
323
|
-
nuos-catalogue summary # totals by register
|
|
324
|
-
\`\`\`
|
|
382
|
+
If this is a brand-new project, \`/start-of-session\` will detect the empty catalogue and walk you through 5 short planning phases (Orientation, Architecture, UI/UX + Design System, Maps, Initial Work Units) before any building starts. Each phase is its own session. Take them in order; pause whenever you need to.
|
|
325
383
|
|
|
326
|
-
|
|
384
|
+
### The principle that makes it work
|
|
327
385
|
|
|
328
|
-
|
|
329
|
-
nuos-catalogue install-protocols
|
|
330
|
-
\`\`\`
|
|
386
|
+
**Project memory never drifts from project reality.** Every decision made in conversation gets saved before the session ends. Every change to an existing piece flows through a protocol. The pre-commit hook blocks silent edits to accepted decisions; the post-commit hook auto-refreshes the search index after every commit. What the AI finds when you ask "what did we decide about X?" is always current.
|
|
331
387
|
|
|
332
388
|
### What never to do
|
|
333
389
|
|
|
334
|
-
- Never
|
|
335
|
-
- Never
|
|
336
|
-
- Never
|
|
337
|
-
|
|
390
|
+
- **Never close a session without \`/end-of-session\`.** Work that isn't written down is work that's lost.
|
|
391
|
+
- **Never edit an accepted decision file.** If something changes, file a new decision that supersedes the old one. The pre-commit hook will block silent edits.
|
|
392
|
+
- **Never make an architectural decision in conversation without filing it.** If you and the AI agree on "let's go with X", file it as a decision *before moving on*. Drift is the failure mode that makes the catalogue worthless.
|
|
393
|
+
|
|
394
|
+
### If you need more
|
|
395
|
+
|
|
396
|
+
- All registers and their templates live under [docs/build/](docs/build/)
|
|
397
|
+
- The full CLI surface (creating work units / decisions / personas / questions / contracts / surfaces from the command line) is documented at [docs/build/WELCOME.md](docs/build/WELCOME.md)
|
|
398
|
+
- To refresh protocols and hooks later (after a CLI upgrade): \`npx @nusoft/nuos-build-catalogue install-protocols\`
|
|
399
|
+
`;
|
|
338
400
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nuos-catalogue plan status` — read methodfile.json's planning tracker
|
|
3
|
+
* and print a 5-line summary of where the project is in the planning arc.
|
|
4
|
+
*
|
|
5
|
+
* Read-only; never mutates state. The actual phase advancement is done by
|
|
6
|
+
* the relevant plan-* protocol when it finishes its work and updates the
|
|
7
|
+
* methodfile + STATE.md as part of its end-of-session.
|
|
8
|
+
*/
|
|
9
|
+
export interface PlanStatusOptions {
|
|
10
|
+
cwd?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function cmdPlanStatus(options?: PlanStatusOptions): Promise<number>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nuos-catalogue plan status` — read methodfile.json's planning tracker
|
|
3
|
+
* and print a 5-line summary of where the project is in the planning arc.
|
|
4
|
+
*
|
|
5
|
+
* Read-only; never mutates state. The actual phase advancement is done by
|
|
6
|
+
* the relevant plan-* protocol when it finishes its work and updates the
|
|
7
|
+
* methodfile + STATE.md as part of its end-of-session.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
const PHASES = [
|
|
13
|
+
{ key: 'phaseA_orientation', label: 'A. Orientation' },
|
|
14
|
+
{ key: 'phaseB_architecture', label: 'B. Architecture & Contracts' },
|
|
15
|
+
{ key: 'phaseC_uiUxDesignSystem', label: 'C. UI/UX + Design System' },
|
|
16
|
+
{ key: 'phaseD_maps', label: 'D. Maps' },
|
|
17
|
+
{ key: 'phaseE_initialWorkUnits', label: 'E. Initial Work Units' },
|
|
18
|
+
];
|
|
19
|
+
function statusIcon(s) {
|
|
20
|
+
switch (s) {
|
|
21
|
+
case 'complete':
|
|
22
|
+
return '✅';
|
|
23
|
+
case 'in_progress':
|
|
24
|
+
return '🟡';
|
|
25
|
+
default:
|
|
26
|
+
return '🔵';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function statusLabel(s) {
|
|
30
|
+
switch (s) {
|
|
31
|
+
case 'complete':
|
|
32
|
+
return 'complete';
|
|
33
|
+
case 'in_progress':
|
|
34
|
+
return 'in progress';
|
|
35
|
+
default:
|
|
36
|
+
return 'not started';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function cmdPlanStatus(options = {}) {
|
|
40
|
+
const cwd = options.cwd ?? process.cwd();
|
|
41
|
+
const methodfilePath = path.join(cwd, 'methodfile.json');
|
|
42
|
+
if (!existsSync(methodfilePath)) {
|
|
43
|
+
console.error(`No methodfile.json found at ${cwd}.`);
|
|
44
|
+
console.error('Run `nuos-catalogue init` first to set up a catalogue.');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
let mf;
|
|
48
|
+
try {
|
|
49
|
+
mf = JSON.parse(await readFile(methodfilePath, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error(`Couldn't read methodfile.json: ${err.message}`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const planning = mf.planning ?? {};
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log('Planning progress for this project:');
|
|
58
|
+
console.log('');
|
|
59
|
+
for (const { key, label } of PHASES) {
|
|
60
|
+
const status = planning[key];
|
|
61
|
+
console.log(` ${statusIcon(status)} ${label.padEnd(36)} ${statusLabel(status)}`);
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
const firstNotStarted = PHASES.find((p) => planning[p.key] !== 'complete');
|
|
65
|
+
if (!firstNotStarted) {
|
|
66
|
+
console.log('All five phases complete. The project is ready to build —');
|
|
67
|
+
console.log('use /start-of-session and /end-of-session for normal work from here.');
|
|
68
|
+
console.log('');
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (planning[firstNotStarted.key] === 'in_progress') {
|
|
72
|
+
console.log(`Currently in: ${firstNotStarted.label} (in progress)`);
|
|
73
|
+
console.log('Resume by running /start-of-session — the AI reads the last session log');
|
|
74
|
+
console.log('and picks up at the right step.');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(`Next phase: ${firstNotStarted.label}`);
|
|
78
|
+
console.log('Begin by running /start-of-session — the AI detects the empty phase');
|
|
79
|
+
console.log('and routes to the right planning protocol.');
|
|
80
|
+
}
|
|
81
|
+
console.log('');
|
|
82
|
+
return 0;
|
|
83
|
+
}
|