@pknx/waterfall-cli 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 +62 -0
- package/bin/waterfall.mjs +14 -0
- package/lib/cli/agent/agent-message.ts +71 -0
- package/lib/cli/agent/agent-translators.ts +145 -0
- package/lib/cli/agent/backend-invoke.ts +133 -0
- package/lib/cli/agent/backends.ts +100 -0
- package/lib/cli/agent/global-prompts.ts +55 -0
- package/lib/cli/commands/bug-start.ts +115 -0
- package/lib/cli/commands/comment-add.ts +47 -0
- package/lib/cli/commands/cr-all.ts +18 -0
- package/lib/cli/commands/cr-finish.ts +176 -0
- package/lib/cli/commands/cr-start.ts +105 -0
- package/lib/cli/commands/cr-to-rq.ts +18 -0
- package/lib/cli/commands/export-pdf.ts +193 -0
- package/lib/cli/commands/horizontal/horizontal.ts +232 -0
- package/lib/cli/commands/horizontal-create.ts +34 -0
- package/lib/cli/commands/horizontal-update.ts +32 -0
- package/lib/cli/commands/join-hint.ts +4 -0
- package/lib/cli/commands/registry.ts +59 -0
- package/lib/cli/commands/resolve-operator-hint.ts +120 -0
- package/lib/cli/commands/rq-all.ts +18 -0
- package/lib/cli/commands/rq-to-uc.ts +18 -0
- package/lib/cli/commands/story-close.ts +124 -0
- package/lib/cli/commands/sync-work-items.ts +59 -0
- package/lib/cli/commands/sys-start.ts +96 -0
- package/lib/cli/commands/test-all.ts +18 -0
- package/lib/cli/commands/test-to-story.ts +18 -0
- package/lib/cli/commands/types.ts +33 -0
- package/lib/cli/commands/uc-all.ts +18 -0
- package/lib/cli/commands/uc-to-story.ts +18 -0
- package/lib/cli/commands/uc-to-test.ts +18 -0
- package/lib/cli/comments/item-comments.ts +285 -0
- package/lib/cli/config/dot-waterfall.ts +404 -0
- package/lib/cli/config/global-cli.ts +21 -0
- package/lib/cli/config/sync-work-item-config.ts +34 -0
- package/lib/cli/core/cli-help-spec.ts +833 -0
- package/lib/cli/core/cli-log.ts +124 -0
- package/lib/cli/core/exec-file.ts +8 -0
- package/lib/cli/core/prompt-map.ts +64 -0
- package/lib/cli/core/slug.ts +44 -0
- package/lib/cli/entry.ts +4 -0
- package/lib/cli/export/collect-md.ts +41 -0
- package/lib/cli/export/export-items.ts +104 -0
- package/lib/cli/export/export-pdf-path.ts +88 -0
- package/lib/cli/export/merge-md.ts +37 -0
- package/lib/cli/export/mermaid-run.ts +104 -0
- package/lib/cli/export/pandoc-pdf.ts +90 -0
- package/lib/cli/export/pdf-bundled-worker.mjs +73 -0
- package/lib/cli/export/pdf-bundled.ts +36 -0
- package/lib/cli/git/cr-agent-context.ts +62 -0
- package/lib/cli/git/git-branch-guards.ts +60 -0
- package/lib/cli/git/git-cli-mock.ts +191 -0
- package/lib/cli/git/git-cli.ts +24 -0
- package/lib/cli/main.ts +434 -0
- package/lib/cli/paths.ts +9 -0
- package/lib/cli/project/pom-json.ts +55 -0
- package/lib/cli/spec/spec-init.ts +216 -0
- package/lib/cli/spec/spec-root.ts +93 -0
- package/lib/cli/sync/apply-remote-comments.ts +87 -0
- package/lib/cli/sync/attachment-category.ts +43 -0
- package/lib/cli/sync/diff-work-items.ts +113 -0
- package/lib/cli/sync/materialize-remote-bugs.ts +66 -0
- package/lib/cli/sync/provider-types.ts +43 -0
- package/lib/cli/sync/providers/direct-provider.ts +27 -0
- package/lib/cli/sync/providers/jira-provider.ts +34 -0
- package/lib/cli/sync/providers/registry.ts +26 -0
- package/lib/cli/sync/run-sync-work-items.ts +202 -0
- package/lib/cli/sync/spec-work-items.ts +226 -0
- package/lib/cli/sync/sync-hint-json.ts +163 -0
- package/lib/cli/sync/work-item-meta.ts +117 -0
- package/lib/cli/work-items/infer-bug-sys.ts +147 -0
- package/lib/cli/work-items/remote-bug-import-scaffold.ts +32 -0
- package/lib/cli/work-items/write-bug-to-spec.ts +158 -0
- package/package.json +54 -0
- package/prompts/commands/bug-start.md +46 -0
- package/prompts/commands/cr-finish.md +44 -0
- package/prompts/commands/cr-start.md +65 -0
- package/prompts/commands/cr-to-rq.md +62 -0
- package/prompts/commands/horizontal-create.md +27 -0
- package/prompts/commands/horizontal-update.md +39 -0
- package/prompts/commands/rq-to-uc.md +62 -0
- package/prompts/commands/story-close-all.md +34 -0
- package/prompts/commands/story-close.md +44 -0
- package/prompts/commands/sync-bugs-refine-imports.md +33 -0
- package/prompts/commands/sys-start.md +63 -0
- package/prompts/commands/test-to-story.md +64 -0
- package/prompts/commands/uc-to-story.md +85 -0
- package/prompts/commands/uc-to-test.md +58 -0
- package/prompts/global/before-changing-spec.md +62 -0
- package/prompts/global/content-requirements-vs-use-cases.md +116 -0
- package/prompts/global/cursor-overview.md +31 -0
- package/prompts/global/git-usage.md +46 -0
- package/prompts/global/horizontal-structure.md +75 -0
- package/prompts/global/workflows-index.md +59 -0
- package/prompts/items/bug-document-structure.md +23 -0
- package/prompts/items/cr-document-structure.md +45 -0
- package/prompts/items/rq-theme-document-structure.md +36 -0
- package/prompts/items/story-document-structure.md +49 -0
- package/prompts/items/sys-document-structure.md +36 -0
- package/prompts/items/tst-document-structure.md +55 -0
- package/prompts/items/uc-document-structure.md +38 -0
- package/spec-template/README.md +11 -0
- package/spec-template/full/doc/spec-structure.md +16 -0
- package/spec-template/full/prompts/before-changing-spec.md +7 -0
- package/spec-template/full/prompts/workflows.md +25 -0
package/lib/cli/main.ts
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waterfall CLI entry (STORY-018 / STORY-019).
|
|
3
|
+
* Run: npm run waterfall -- … or node --import tsx lib/cli/main.ts …
|
|
4
|
+
*/
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { resolveSpecRoot, assertSpecRoot } from "./spec/spec-root";
|
|
9
|
+
import { parseSpecInitCliArgs, runSpecInit } from "./spec/spec-init";
|
|
10
|
+
import {
|
|
11
|
+
parseBackendId,
|
|
12
|
+
checkBackendAvailable,
|
|
13
|
+
describeBackendInvocation,
|
|
14
|
+
} from "./agent/backends";
|
|
15
|
+
import { buildAgentWireMessage, invokeAgentBackend } from "./agent/backend-invoke";
|
|
16
|
+
import {
|
|
17
|
+
parsePromptDelivery,
|
|
18
|
+
} from "./agent/agent-message";
|
|
19
|
+
import {
|
|
20
|
+
defaultLogLevel,
|
|
21
|
+
parseLogLevel,
|
|
22
|
+
traceAgentInvokeDone,
|
|
23
|
+
traceAgentInvokeStart,
|
|
24
|
+
traceDryRun,
|
|
25
|
+
traceTranslatedPayloadForBackend,
|
|
26
|
+
} from "./core/cli-log";
|
|
27
|
+
import { promptFileForStep, type LifecycleStep } from "./core/prompt-map";
|
|
28
|
+
import type { RunAgentPromptOptions } from "./commands/types";
|
|
29
|
+
import { augmentAgentHintWithCrFiles } from "./git/cr-agent-context";
|
|
30
|
+
import { assertBranchForCrScopedCommand } from "./git/git-branch-guards";
|
|
31
|
+
import {
|
|
32
|
+
ensureDotWaterfallFile,
|
|
33
|
+
loadDotWaterfallConfig,
|
|
34
|
+
applyDotWaterfallEnvExports,
|
|
35
|
+
dotWaterfallPath,
|
|
36
|
+
type DotWaterfallParsed,
|
|
37
|
+
} from "./config/dot-waterfall";
|
|
38
|
+
import { dispatchSpecCommand } from "./commands/registry";
|
|
39
|
+
import type { GlobalCli } from "./config/global-cli";
|
|
40
|
+
import type { WaterfallGlobalPrompt } from "./agent/global-prompts";
|
|
41
|
+
import {
|
|
42
|
+
formatCliHelpJson,
|
|
43
|
+
parseHelpFormat,
|
|
44
|
+
} from "./core/cli-help-spec";
|
|
45
|
+
import { WATERFALL_CLI_PACKAGE_ROOT } from "./paths";
|
|
46
|
+
|
|
47
|
+
export type { GlobalCli } from "./config/global-cli";
|
|
48
|
+
|
|
49
|
+
const CLI_ROOT = WATERFALL_CLI_PACKAGE_ROOT;
|
|
50
|
+
|
|
51
|
+
/** Absolute path to shipped workflow + global markdown (`prompts/`). */
|
|
52
|
+
const WATERFALL_PROMPTS_ROOT = path.join(CLI_ROOT, "prompts");
|
|
53
|
+
|
|
54
|
+
function printHelp(): void {
|
|
55
|
+
process.stdout.write(`waterfall — CLI for waterfall-spec workflows
|
|
56
|
+
|
|
57
|
+
Global options:
|
|
58
|
+
--spec-root PATH Override waterfall-spec repository root
|
|
59
|
+
--backend ID cline-ollama | cursor | claude-code (default: cline-ollama)
|
|
60
|
+
--dry-run Print planned agent invocation only (default for prompt commands)
|
|
61
|
+
--execute-agent Invoke backend: cline -y, agent|cursor -p --force, or claude -p (see README; not dry-run)
|
|
62
|
+
--log-level L silent | normal | verbose (overrides WATERFALL_LOG_LEVEL / .waterfall level=)
|
|
63
|
+
--quiet Same as --log-level silent (agent tracing only; subprocess output unchanged)
|
|
64
|
+
--prompt-delivery M embed | reference — embed inlines the workflow .md in the agent message (default); reference sends absolute paths so the agent reads prompts from the CLI install (overrides .waterfall prompt_delivery)
|
|
65
|
+
|
|
66
|
+
User config: .waterfall in the working directory, then ~/.waterfall (merged; cwd wins).
|
|
67
|
+
~/.waterfall is created on first run when missing (commented examples).
|
|
68
|
+
Precedence: flags > env (WATERFALL_*) > ./.waterfall > ~/.waterfall > defaults.
|
|
69
|
+
Log/tracing: WATERFALL_LOG_LEVEL or level=silent|normal|verbose in .waterfall; default normal. With --backend cursor, the full translated JSON is also printed to stdout (unless silent) before agent runs or in dry-run.
|
|
70
|
+
|
|
71
|
+
Operator hints (agent steering and scaffold descriptions): pass inline argv tokens (joined with spaces), a single - to read stdin (UTF-8), or a single @PATH to read a file (UTF-8; PATH relative to cwd unless absolute). On change-request branches, git diff paths vs develop appear in a separate "user" segment from your hint in the agent wire payload (see prompts/global/workflows-index.md). Dry-run logs still show one combined hint line for readability.
|
|
72
|
+
|
|
73
|
+
Git branch (resolved spec repo; exact wording per command: waterfall help -o json → commands[].branchRequirement):
|
|
74
|
+
develop — cr start, story close, comment add …
|
|
75
|
+
feature/CR-<digits>-… — cr to rq, rq to uc, uc to story, uc to test, test to story, cr|rq|uc|test all, sys start, bug start, horizontal create|update, cr finish
|
|
76
|
+
(no spec-root branch check) — help, spec init, export pdf
|
|
77
|
+
|
|
78
|
+
Commands:
|
|
79
|
+
waterfall help | -? | --help (machine spec: waterfall help -o json)
|
|
80
|
+
waterfall spec init --title "…" [--full] [DIR] New repo: pom.json, dirs, number.json, CURSOR.md, git develop. --full adds prompts/ + doc/ from spec-template/full (regeneratable). Default DIR=.
|
|
81
|
+
waterfall cr start <description> (then prompts/commands/cr-start.md — use --execute-agent for AI Summary/Proposed change)
|
|
82
|
+
waterfall sys start <description> (then prompts/commands/sys-start.md — AI Purpose)
|
|
83
|
+
waterfall bug start SYS-NNN <hint> (then prompts/commands/bug-start.md — AI Summary)
|
|
84
|
+
waterfall horizontal create story <name-slug> <purpose> (then prompts/commands/horizontal-create.md — AI Purpose)
|
|
85
|
+
waterfall horizontal update HOR-NNN [<hint>]
|
|
86
|
+
waterfall cr to rq <hint>
|
|
87
|
+
waterfall rq to uc <hint>
|
|
88
|
+
waterfall uc to story <hint>
|
|
89
|
+
waterfall uc to test <hint>
|
|
90
|
+
waterfall test to story <hint>
|
|
91
|
+
waterfall cr all [<hint>]
|
|
92
|
+
waterfall rq all [<hint>]
|
|
93
|
+
waterfall uc all [<hint>]
|
|
94
|
+
waterfall test all [<hint>]
|
|
95
|
+
waterfall story close STORY-NNN | waterfall story close all
|
|
96
|
+
waterfall comment add CR|RQ|UC|SYS|STORY|BUG|HOR|TST-NNN <email> <display-name> <hint…> On develop: append remark to {ITEM-ID}.comments.json (author id = email)
|
|
97
|
+
waterfall cr finish mechanical checks + merge feature/CR-* into develop (substance: prompts/commands/cr-finish.md)
|
|
98
|
+
waterfall sync stories [--dump-spec] [<hint…>] Compare STORY-* meta vs remote (see .waterfall sync_story_*); --dump-spec prints canonical JSON from spec only; optional hint (inline, -, @PATH) for direct (JSON = dump-spec shape); sync applies remote comments to spec then diffs; direct appends spec snapshot JSON (source waterfall-direct-out) after each run
|
|
99
|
+
waterfall sync bugs [--dump-spec] [<hint…>] BUG-* via sync_bug_*; remote-leading materializes new bugs (SYS from sysId or inferred), Draft steering + sync-bugs-refine-imports agent pass; direct appends waterfall-direct-out JSON
|
|
100
|
+
waterfall export pdf [--items CR,RQ,...] [-o PATH] Clean git tree; default items CR,RQ,UC,HOR,TST (no CURSOR.md); default PDF under spec docs/; document title from pom.json (walk up from spec root); use_native_pandoc in .waterfall for system pandoc
|
|
101
|
+
|
|
102
|
+
Spec resolution order: --spec-root → WATERFALL_SPEC_ROOT → ./.waterfall / ~/.waterfall spec_root → waterfall.json → walk to CURSOR.md
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveSpecRootConfigValue(raw: string | undefined): string | undefined {
|
|
107
|
+
if (!raw?.trim()) return undefined;
|
|
108
|
+
const t = raw.trim();
|
|
109
|
+
return path.isAbsolute(t) ? t : path.resolve(process.cwd(), t);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildGlobalCliBase(dot: DotWaterfallParsed): GlobalCli {
|
|
113
|
+
let backend: GlobalCli["backend"] = "cline-ollama";
|
|
114
|
+
try {
|
|
115
|
+
if (dot.backend?.trim()) backend = parseBackendId(dot.backend.trim());
|
|
116
|
+
if (process.env.WATERFALL_BACKEND?.trim()) {
|
|
117
|
+
backend = parseBackendId(process.env.WATERFALL_BACKEND);
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`${(e as Error).message} (check .waterfall in cwd, ~/.waterfall, or WATERFALL_BACKEND)`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
let specRoot: string | undefined = resolveSpecRootConfigValue(dot.specRoot);
|
|
125
|
+
if (process.env.WATERFALL_SPEC_ROOT?.trim()) {
|
|
126
|
+
specRoot = resolveSpecRootConfigValue(process.env.WATERFALL_SPEC_ROOT);
|
|
127
|
+
}
|
|
128
|
+
let executeAgent = dot.executeAgent === true;
|
|
129
|
+
let dryRun = dot.dryRun !== false;
|
|
130
|
+
if (dot.dryRun === true) {
|
|
131
|
+
dryRun = true;
|
|
132
|
+
executeAgent = false;
|
|
133
|
+
}
|
|
134
|
+
if (executeAgent) dryRun = false;
|
|
135
|
+
const logLevel = parseLogLevel(dot.level) ?? defaultLogLevel();
|
|
136
|
+
let promptDelivery: GlobalCli["promptDelivery"] = "embed";
|
|
137
|
+
const fromDot = parsePromptDelivery(dot.promptDelivery);
|
|
138
|
+
if (fromDot !== undefined) promptDelivery = fromDot;
|
|
139
|
+
return {
|
|
140
|
+
cwd: process.cwd(),
|
|
141
|
+
specRoot,
|
|
142
|
+
backend,
|
|
143
|
+
dryRun,
|
|
144
|
+
executeAgent,
|
|
145
|
+
logLevel,
|
|
146
|
+
promptDelivery,
|
|
147
|
+
useNativePandoc: dot.useNativePandoc === true,
|
|
148
|
+
syncStory: dot.syncStory,
|
|
149
|
+
syncBug: dot.syncBug,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseGlobals(argv: string[], base: GlobalCli): { g: GlobalCli; rest: string[] } {
|
|
154
|
+
const g: GlobalCli = { ...base, cwd: process.cwd() };
|
|
155
|
+
const envLevel = parseLogLevel(process.env.WATERFALL_LOG_LEVEL);
|
|
156
|
+
if (envLevel !== undefined) g.logLevel = envLevel;
|
|
157
|
+
const rest: string[] = [];
|
|
158
|
+
let i = 0;
|
|
159
|
+
while (i < argv.length) {
|
|
160
|
+
const a = argv[i]!;
|
|
161
|
+
if (a === "--spec-root") {
|
|
162
|
+
g.specRoot = resolveSpecRootConfigValue(argv[++i]);
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (a === "--backend") {
|
|
167
|
+
g.backend = parseBackendId(argv[++i]);
|
|
168
|
+
i++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (a === "--dry-run") {
|
|
172
|
+
g.dryRun = true;
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (a === "--execute-agent") {
|
|
177
|
+
g.executeAgent = true;
|
|
178
|
+
g.dryRun = false;
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (a === "--log-level") {
|
|
183
|
+
const raw = argv[++i];
|
|
184
|
+
if (!raw) {
|
|
185
|
+
throw new Error("--log-level requires silent | normal | verbose");
|
|
186
|
+
}
|
|
187
|
+
const lv = parseLogLevel(raw);
|
|
188
|
+
if (lv === undefined) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Invalid --log-level "${raw}" (use silent | normal | verbose)`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
g.logLevel = lv;
|
|
194
|
+
i++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (a === "--quiet") {
|
|
198
|
+
g.logLevel = "silent";
|
|
199
|
+
i++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (a === "--prompt-delivery") {
|
|
203
|
+
const raw = argv[++i];
|
|
204
|
+
if (!raw) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"--prompt-delivery requires embed | reference (see help)",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const d = parsePromptDelivery(raw);
|
|
210
|
+
if (d === undefined) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Invalid --prompt-delivery "${raw}" (use embed | reference)`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
g.promptDelivery = d;
|
|
216
|
+
i++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
rest.push(a);
|
|
220
|
+
i++;
|
|
221
|
+
}
|
|
222
|
+
return { g, rest };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function planAgent(
|
|
226
|
+
g: GlobalCli,
|
|
227
|
+
specRoot: string,
|
|
228
|
+
relPrompt: string,
|
|
229
|
+
hint: string,
|
|
230
|
+
): string {
|
|
231
|
+
return describeBackendInvocation(
|
|
232
|
+
g.backend,
|
|
233
|
+
specRoot,
|
|
234
|
+
relPrompt,
|
|
235
|
+
hint,
|
|
236
|
+
g.promptDelivery,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type MaybeRunAgentOptions = {
|
|
241
|
+
/** `prompts/global/*.md` segments in `system` (most important first). */
|
|
242
|
+
globalPrompts: WaterfallGlobalPrompt[];
|
|
243
|
+
orchestrationContext?: string;
|
|
244
|
+
/** @default true — set false when hint already includes develop...HEAD paths (horizontal update). */
|
|
245
|
+
attachCrBranchPaths?: boolean;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
function maybeRunAgent(
|
|
249
|
+
g: GlobalCli,
|
|
250
|
+
specRoot: string,
|
|
251
|
+
relPrompt: string,
|
|
252
|
+
hint: string,
|
|
253
|
+
options: MaybeRunAgentOptions,
|
|
254
|
+
): void {
|
|
255
|
+
const attachCr = options.attachCrBranchPaths !== false;
|
|
256
|
+
const hintForPlan = attachCr
|
|
257
|
+
? augmentAgentHintWithCrFiles(specRoot, hint)
|
|
258
|
+
: hint;
|
|
259
|
+
const line = planAgent(g, specRoot, relPrompt, hintForPlan);
|
|
260
|
+
const absPrompt = path.join(CLI_ROOT, relPrompt);
|
|
261
|
+
if (!fs.existsSync(absPrompt)) {
|
|
262
|
+
throw new Error(`Prompt file missing: ${absPrompt}`);
|
|
263
|
+
}
|
|
264
|
+
if (!g.executeAgent || g.dryRun) {
|
|
265
|
+
if (
|
|
266
|
+
g.logLevel !== "silent" &&
|
|
267
|
+
(g.backend === "cursor" || g.logLevel === "verbose")
|
|
268
|
+
) {
|
|
269
|
+
traceTranslatedPayloadForBackend(
|
|
270
|
+
g.backend,
|
|
271
|
+
g.logLevel,
|
|
272
|
+
buildAgentWireMessage(g.backend, specRoot, absPrompt, hint, {
|
|
273
|
+
orchestrationContext: options.orchestrationContext,
|
|
274
|
+
promptDelivery: g.promptDelivery,
|
|
275
|
+
waterfallPromptsRoot: WATERFALL_PROMPTS_ROOT,
|
|
276
|
+
globalPrompts: options.globalPrompts,
|
|
277
|
+
attachCrBranchPaths: attachCr,
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
traceDryRun(g.logLevel, line);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
checkBackendAvailable(g.backend, {});
|
|
285
|
+
const promptBasename = path.basename(relPrompt);
|
|
286
|
+
const model =
|
|
287
|
+
g.backend === "cline-ollama"
|
|
288
|
+
? process.env.WATERFALL_CLINE_MODEL?.trim()
|
|
289
|
+
: undefined;
|
|
290
|
+
let promptByteLength: number | undefined;
|
|
291
|
+
try {
|
|
292
|
+
promptByteLength = fs.statSync(absPrompt).size;
|
|
293
|
+
} catch {
|
|
294
|
+
promptByteLength = undefined;
|
|
295
|
+
}
|
|
296
|
+
traceAgentInvokeStart(g.logLevel, {
|
|
297
|
+
backend: g.backend,
|
|
298
|
+
promptBasename,
|
|
299
|
+
promptRel: relPrompt,
|
|
300
|
+
specRoot,
|
|
301
|
+
hint: hintForPlan,
|
|
302
|
+
orchestrationContext: options.orchestrationContext,
|
|
303
|
+
model,
|
|
304
|
+
promptByteLength,
|
|
305
|
+
});
|
|
306
|
+
invokeAgentBackend(g.backend, specRoot, absPrompt, hint, {
|
|
307
|
+
orchestrationContext: options.orchestrationContext,
|
|
308
|
+
promptDelivery: g.promptDelivery,
|
|
309
|
+
waterfallPromptsRoot: WATERFALL_PROMPTS_ROOT,
|
|
310
|
+
globalPrompts: options.globalPrompts,
|
|
311
|
+
attachCrBranchPaths: attachCr,
|
|
312
|
+
logLevel: g.logLevel,
|
|
313
|
+
});
|
|
314
|
+
traceAgentInvokeDone(g.logLevel, {
|
|
315
|
+
backend: g.backend,
|
|
316
|
+
promptBasename,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function orchestrate(
|
|
321
|
+
g: GlobalCli,
|
|
322
|
+
specRoot: string,
|
|
323
|
+
order: LifecycleStep[],
|
|
324
|
+
batchLabel: string,
|
|
325
|
+
hint: string,
|
|
326
|
+
globalPrompts: WaterfallGlobalPrompt[],
|
|
327
|
+
): void {
|
|
328
|
+
assertBranchForCrScopedCommand(specRoot, batchLabel);
|
|
329
|
+
for (let i = 0; i < order.length; i++) {
|
|
330
|
+
const step = order[i]!;
|
|
331
|
+
const rel = promptFileForStep(step);
|
|
332
|
+
const orchestrationContext = `${batchLabel} — step ${i + 1}/${order.length}`;
|
|
333
|
+
maybeRunAgent(g, specRoot, rel, `[${step}] ${hint}`, {
|
|
334
|
+
globalPrompts,
|
|
335
|
+
orchestrationContext,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function runWaterfallCli(argv: string[]): number {
|
|
341
|
+
const created = ensureDotWaterfallFile();
|
|
342
|
+
const dot = loadDotWaterfallConfig();
|
|
343
|
+
applyDotWaterfallEnvExports(dot.envExports);
|
|
344
|
+
const base = buildGlobalCliBase(dot);
|
|
345
|
+
const { g, rest } = parseGlobals(argv, base);
|
|
346
|
+
if (created) {
|
|
347
|
+
process.stderr.write(
|
|
348
|
+
`[waterfall] Created ${dotWaterfallPath()} with commented examples (user defaults). Per-project overrides: add .waterfall in the working directory (wins over ~/.waterfall).\n`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (rest.length === 0 || rest[0] === "-?" || rest[0] === "--help") {
|
|
352
|
+
printHelp();
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (rest[0] === "help") {
|
|
357
|
+
try {
|
|
358
|
+
const { format } = parseHelpFormat(rest);
|
|
359
|
+
if (format === "json") {
|
|
360
|
+
process.stdout.write(formatCliHelpJson());
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
} catch (e) {
|
|
364
|
+
process.stderr.write(`${(e as Error).message}\n`);
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
printHelp();
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (rest[0] === "spec" && rest[1] === "init") {
|
|
372
|
+
try {
|
|
373
|
+
const { full, title, targetDir } = parseSpecInitCliArgs(rest.slice(2));
|
|
374
|
+
runSpecInit(g.cwd, targetDir, { full, title });
|
|
375
|
+
process.stdout.write(
|
|
376
|
+
`[waterfall] Initialized spec scaffold${full ? " (full)" : ""}: ${path.resolve(g.cwd, targetDir)}\n`,
|
|
377
|
+
);
|
|
378
|
+
return 0;
|
|
379
|
+
} catch (e) {
|
|
380
|
+
process.stderr.write(`${(e as Error).message}\n`);
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let specRoot: string;
|
|
386
|
+
try {
|
|
387
|
+
specRoot = resolveSpecRoot({
|
|
388
|
+
cwd: g.cwd,
|
|
389
|
+
env: process.env,
|
|
390
|
+
explicitSpecRoot: g.specRoot,
|
|
391
|
+
});
|
|
392
|
+
assertSpecRoot(specRoot);
|
|
393
|
+
} catch (e) {
|
|
394
|
+
process.stderr.write(`${(e as Error).message}\n`);
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const commandCtx = {
|
|
400
|
+
g,
|
|
401
|
+
specRoot,
|
|
402
|
+
runAgentPrompt: (
|
|
403
|
+
relPrompt: string,
|
|
404
|
+
hint: string,
|
|
405
|
+
globalPrompts: WaterfallGlobalPrompt[],
|
|
406
|
+
orchestrationContext?: string,
|
|
407
|
+
runOpts?: RunAgentPromptOptions,
|
|
408
|
+
) =>
|
|
409
|
+
maybeRunAgent(g, specRoot, relPrompt, hint, {
|
|
410
|
+
globalPrompts,
|
|
411
|
+
orchestrationContext,
|
|
412
|
+
attachCrBranchPaths: runOpts?.attachCrBranchPaths,
|
|
413
|
+
}),
|
|
414
|
+
runOrchestrate: (
|
|
415
|
+
order: LifecycleStep[],
|
|
416
|
+
batchLabel: string,
|
|
417
|
+
hint: string,
|
|
418
|
+
globalPrompts: WaterfallGlobalPrompt[],
|
|
419
|
+
) => orchestrate(g, specRoot, order, batchLabel, hint, globalPrompts),
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const dispatched = dispatchSpecCommand(commandCtx, rest);
|
|
423
|
+
if (dispatched !== null) {
|
|
424
|
+
return dispatched;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
process.stderr.write(`Unknown command: ${rest.join(" ")}\n`);
|
|
428
|
+
printHelp();
|
|
429
|
+
return 1;
|
|
430
|
+
} catch (e) {
|
|
431
|
+
process.stderr.write(`${(e as Error).message}\n`);
|
|
432
|
+
return 1;
|
|
433
|
+
}
|
|
434
|
+
}
|
package/lib/cli/paths.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
/** `waterfall-cli` package root (contains `lib/`, `prompts/`, `node_modules/`). */
|
|
5
|
+
export const WATERFALL_CLI_PACKAGE_ROOT = path.resolve(
|
|
6
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
7
|
+
"..",
|
|
8
|
+
"..",
|
|
9
|
+
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const POM_JSON_BASENAME = "pom.json";
|
|
5
|
+
|
|
6
|
+
export type PomJson = {
|
|
7
|
+
title: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Walk upward from `startAbs` (typically spec root) until `pom.json` exists. */
|
|
12
|
+
export function findPomJsonPath(startAbs: string): string | undefined {
|
|
13
|
+
let dir = path.resolve(startAbs);
|
|
14
|
+
const { root } = path.parse(dir);
|
|
15
|
+
for (;;) {
|
|
16
|
+
const candidate = path.join(dir, POM_JSON_BASENAME);
|
|
17
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
18
|
+
if (dir === root) break;
|
|
19
|
+
dir = path.dirname(dir);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Project title from `pom.json` for `export pdf` (PDF metadata and document H1).
|
|
26
|
+
* @throws Error if no file or invalid / missing `title`
|
|
27
|
+
*/
|
|
28
|
+
export function readPomProjectTitle(specRootAbs: string): string {
|
|
29
|
+
const pomPath = findPomJsonPath(specRootAbs);
|
|
30
|
+
if (!pomPath) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`[waterfall] export pdf: no ${POM_JSON_BASENAME} found (searching upward from the spec root). Add a ${POM_JSON_BASENAME} with a non-empty string "title" at the project root, or run: waterfall spec init --title "…" [DIR]`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
let raw: unknown;
|
|
36
|
+
try {
|
|
37
|
+
raw = JSON.parse(fs.readFileSync(pomPath, "utf8"));
|
|
38
|
+
} catch (e) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[waterfall] ${POM_JSON_BASENAME} is not valid JSON at ${pomPath}: ${(e as Error).message}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[waterfall] ${POM_JSON_BASENAME} must be a JSON object at ${pomPath}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const title = (raw as { title?: unknown }).title;
|
|
49
|
+
if (typeof title !== "string" || !title.trim()) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[waterfall] ${POM_JSON_BASENAME} must include a non-empty string "title" at ${pomPath}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return title.trim();
|
|
55
|
+
}
|