@pmelab/gtd 59.0.0 → 60.0.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/.claude-plugin/plugin.json +1 -1
- package/README.md +42 -38
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/{testkit-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
- package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +1101 -1025
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +371 -82
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +68 -61
- package/src/cli/Cli.test.ts +311 -229
- package/src/cli/Cli.ts +486 -585
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +31 -1
- package/src/cli/Program.test.ts +131 -82
- package/src/cli/Program.ts +197 -111
- package/src/edge/Edge.test.ts +64 -56
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +25 -6
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +188 -54
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +481 -98
- package/src/gherkin/messages.test.ts +124 -0
- package/src/gherkin/messages.ts +101 -0
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- package/src/replay/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +9 -9
- package/src/testkit/TestRepo.test.ts +289 -6
- package/src/testkit/TestRepo.ts +317 -41
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +7 -14
- package/src/twins/EmittedScripts.test.ts +4 -2
- package/src/twins/InMemRepo.ts +1 -0
- package/src/wire/Land.ts +1 -1
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +3 -3
- package/src/workflow/load.test.ts +256 -116
- package/src/workflow/load.ts +175 -152
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
package/src/cli/Cli.test.ts
CHANGED
|
@@ -35,8 +35,8 @@ const FLAG_NAMES = [
|
|
|
35
35
|
"--dev",
|
|
36
36
|
"--cost",
|
|
37
37
|
"--model",
|
|
38
|
-
"--workflow",
|
|
39
38
|
"--var",
|
|
39
|
+
"--threshold",
|
|
40
40
|
"--open-questions",
|
|
41
41
|
"--open-threads",
|
|
42
42
|
"--verbose",
|
|
@@ -71,7 +71,8 @@ describe("parseArgv — unknown options", () => {
|
|
|
71
71
|
expect(plan).toEqual({
|
|
72
72
|
kind: "usage",
|
|
73
73
|
stdout: "",
|
|
74
|
-
message:
|
|
74
|
+
message:
|
|
75
|
+
"gtd: unknown option '--jsn' — did you mean `--json`? Run `gtd help <command>` for its options",
|
|
75
76
|
json: false,
|
|
76
77
|
})
|
|
77
78
|
})
|
|
@@ -104,13 +105,18 @@ describe("parseArgv — scope", () => {
|
|
|
104
105
|
if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd land`")
|
|
105
106
|
})
|
|
106
107
|
|
|
107
|
-
it("--json is in scope for next, land, judge and
|
|
108
|
+
it("--json is in scope for next, land, the judge and script commands and the listings — every other command usage-errors on it", () => {
|
|
108
109
|
for (const args of [
|
|
109
110
|
["next", "--json"],
|
|
110
111
|
["land", "--json"],
|
|
111
112
|
["judge", "--json"],
|
|
112
113
|
["judge", "answer", "--json"],
|
|
113
|
-
["
|
|
114
|
+
["validate", "--json"],
|
|
115
|
+
["abandon", "--json"],
|
|
116
|
+
["restore", "--json"],
|
|
117
|
+
["start", "--json"],
|
|
118
|
+
["start", "fix", "--json"],
|
|
119
|
+
["history", "fetch", "--json"],
|
|
114
120
|
]) {
|
|
115
121
|
const ok = parseArgv(["node", "gtd.js", ...args])
|
|
116
122
|
expect(ok.kind).toBe("command")
|
|
@@ -119,18 +125,16 @@ describe("parseArgv — scope", () => {
|
|
|
119
125
|
|
|
120
126
|
for (const args of [
|
|
121
127
|
["lsp", "--json"],
|
|
122
|
-
["validate", "--json"],
|
|
123
128
|
["check", "qa", "TODO.md", "--json"],
|
|
124
|
-
["
|
|
125
|
-
["
|
|
126
|
-
["
|
|
127
|
-
["--workflow", "some-state", "--json"],
|
|
129
|
+
["guide", "--json"],
|
|
130
|
+
["summary", "--json"],
|
|
131
|
+
["base", "--json"],
|
|
128
132
|
]) {
|
|
129
133
|
const plan = parseArgv(["node", "gtd.js", ...args])
|
|
130
134
|
expect(plan.kind).toBe("usage")
|
|
131
135
|
if (plan.kind === "usage") {
|
|
132
136
|
expect(plan.message).toContain("only valid for `gtd next`/`gtd land`")
|
|
133
|
-
expect(plan.message).toContain("gtd
|
|
137
|
+
expect(plan.message).toContain("gtd guide")
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
})
|
|
@@ -166,38 +170,31 @@ describe("parseArgv — scope", () => {
|
|
|
166
170
|
}
|
|
167
171
|
})
|
|
168
172
|
|
|
169
|
-
it("--var
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd land`")
|
|
173
|
+
it("--var outside `gtd start` is rejected", () => {
|
|
174
|
+
for (const args of [
|
|
175
|
+
["land", "--var", "a=1"],
|
|
176
|
+
["history", "judges", "--var", "qualityTriageMinP=0.9"],
|
|
177
|
+
]) {
|
|
178
|
+
const plan = parseArgv(["node", "gtd.js", ...args])
|
|
179
|
+
expect(plan.kind).toBe("usage")
|
|
180
|
+
if (plan.kind === "usage")
|
|
181
|
+
expect(plan.message).toBe("gtd: --var is only valid for `gtd start <name>`")
|
|
182
|
+
}
|
|
180
183
|
})
|
|
181
184
|
|
|
182
|
-
it("--
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
it("--cost and --model on `gtd start` are rejected (landing and starting are different verbs)", () => {
|
|
186
|
+
for (const flag of ["--cost=5", "--model=gpt"]) {
|
|
187
|
+
const plan = parseArgv(["node", "gtd.js", "start", "foo", flag])
|
|
188
|
+
expect(plan.kind).toBe("usage")
|
|
189
|
+
if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd land`")
|
|
190
|
+
}
|
|
186
191
|
})
|
|
187
192
|
|
|
188
|
-
it("--workflow
|
|
189
|
-
for (const args of [
|
|
190
|
-
["validate", "--workflow", "e"],
|
|
191
|
-
["land", "--workflow", "e"],
|
|
192
|
-
]) {
|
|
193
|
+
it("--workflow is gone: an unknown option on its own and after any command", () => {
|
|
194
|
+
for (const args of [["--workflow", "e"], ["--workflow=e"], ["land", "--workflow", "e"]]) {
|
|
193
195
|
const plan = parseArgv(["node", "gtd.js", ...args])
|
|
194
196
|
expect(plan.kind).toBe("usage")
|
|
195
|
-
if (plan.kind === "usage")
|
|
196
|
-
expect(plan.message).toBe(
|
|
197
|
-
"gtd: --workflow is only valid with no other command — use the bare `gtd --workflow <name>` " +
|
|
198
|
-
"form; landing and starting are different verbs",
|
|
199
|
-
)
|
|
200
|
-
}
|
|
197
|
+
if (plan.kind === "usage") expect(plan.message).toContain("unknown option '--workflow")
|
|
201
198
|
}
|
|
202
199
|
})
|
|
203
200
|
|
|
@@ -369,17 +366,52 @@ describe("parseArgv — bare/unknown command under --json", () => {
|
|
|
369
366
|
}
|
|
370
367
|
})
|
|
371
368
|
|
|
372
|
-
it("bare gtd
|
|
369
|
+
it("bare gtd prints a short usage, not the overview, and points at gtd help and gtd guide", () => {
|
|
373
370
|
const plan = parseArgv(["node", "gtd.js"])
|
|
374
371
|
expect(plan.kind).toBe("usage")
|
|
375
372
|
if (plan.kind === "usage") {
|
|
376
373
|
expect(plan.message).toContain("missing command")
|
|
377
|
-
expect(plan.message).toContain("gtd
|
|
378
|
-
expect(plan.message).toContain("gtd
|
|
379
|
-
expect(plan.
|
|
374
|
+
expect(plan.message).toContain("`gtd help`")
|
|
375
|
+
expect(plan.message).toContain("gtd guide")
|
|
376
|
+
expect(plan.stdout).toMatch(/^Usage: gtd <command>/)
|
|
377
|
+
expect(plan.stdout.split("\n").length).toBeLessThan(10)
|
|
380
378
|
}
|
|
381
379
|
})
|
|
382
380
|
|
|
381
|
+
it("an unknown command that looks like a typo names the nearest command", () => {
|
|
382
|
+
expect(parseArgv(["node", "gtd.js", "nxt"])).toEqual({
|
|
383
|
+
kind: "usage",
|
|
384
|
+
stdout: "",
|
|
385
|
+
message:
|
|
386
|
+
"gtd: unknown command 'nxt' — did you mean `gtd next`? Run `gtd help` for every command",
|
|
387
|
+
json: false,
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it("an unknown command nothing resembles points at gtd help only", () => {
|
|
392
|
+
const plan = parseArgv(["node", "gtd.js", "frobnicate"])
|
|
393
|
+
expect(plan.kind === "usage" && plan.message).toBe(
|
|
394
|
+
"gtd: unknown command 'frobnicate' — run `gtd help` for every command",
|
|
395
|
+
)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it("a mistyped history subcommand names the nearest one", () => {
|
|
399
|
+
const plan = parseArgv(["node", "gtd.js", "history", "shwo"])
|
|
400
|
+
expect(plan.kind === "usage" && plan.message).toContain("did you mean `gtd history show`?")
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it("a bare --json followed by a word suggests the = form", () => {
|
|
404
|
+
const plan = parseArgv(["node", "gtd.js", "next", "--json", "kind"])
|
|
405
|
+
expect(plan.kind === "usage" && plan.message).toBe(
|
|
406
|
+
"gtd next: too many arguments — expected none, got: kind — to print one field, write --json=kind",
|
|
407
|
+
)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it("an unknown single-dash option is an unknown option, not an extra argument", () => {
|
|
411
|
+
const plan = parseArgv(["node", "gtd.js", "-j", "next"])
|
|
412
|
+
expect(plan.kind === "usage" && plan.message).toContain("unknown option '-j'")
|
|
413
|
+
})
|
|
414
|
+
|
|
383
415
|
it("an unknown command under --json carries json:true, naming --json's own scope error over the subcommand", () => {
|
|
384
416
|
const plan = parseArgv(["node", "gtd.js", "bogus", "--json"])
|
|
385
417
|
expect(plan.kind).toBe("usage")
|
|
@@ -399,43 +431,16 @@ describe("parseArgv — bare/unknown command under --json", () => {
|
|
|
399
431
|
})
|
|
400
432
|
})
|
|
401
433
|
|
|
402
|
-
describe("parseArgv —
|
|
403
|
-
it("
|
|
404
|
-
const plan = parseArgv(["node", "gtd.js", "--
|
|
405
|
-
expect(plan.kind).toBe("command")
|
|
406
|
-
if (plan.kind === "command") {
|
|
407
|
-
expect(plan.command).toEqual({
|
|
408
|
-
kind: "start",
|
|
409
|
-
actor: "human",
|
|
410
|
-
workflow: "side-entry",
|
|
411
|
-
vars: {},
|
|
412
|
-
label: "gtd --workflow side-entry",
|
|
413
|
-
})
|
|
414
|
-
}
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
it("a bare --workflow with no value is a usage error", () => {
|
|
418
|
-
const plan = parseArgv(["node", "gtd.js", "--workflow"])
|
|
419
|
-
expect(plan.kind).toBe("usage")
|
|
420
|
-
if (plan.kind === "usage") expect(plan.message).toContain("--workflow requires a value")
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
it("a second --workflow occurrence is a usage error (not last-wins)", () => {
|
|
424
|
-
const plan = parseArgv(["node", "gtd.js", "--workflow", "a", "--workflow", "b"])
|
|
425
|
-
expect(plan.kind).toBe("usage")
|
|
426
|
-
if (plan.kind === "usage")
|
|
427
|
-
expect(plan.message).toContain("--workflow may be given at most once")
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
it("a duplicate --var NAME is a usage error", () => {
|
|
431
|
-
const plan = parseArgv(["node", "gtd.js", "--workflow", "e", "--var", "a=1", "--var", "a=2"])
|
|
434
|
+
describe("parseArgv — --var details", () => {
|
|
435
|
+
it("a second --var occurrence of one name is a usage error (not last-wins)", () => {
|
|
436
|
+
const plan = parseArgv(["node", "gtd.js", "start", "e", "--var", "a=1", "--var", "a=2"])
|
|
432
437
|
expect(plan.kind).toBe("usage")
|
|
433
438
|
if (plan.kind === "usage") expect(plan.message).toContain("--var a")
|
|
434
439
|
})
|
|
435
440
|
|
|
436
441
|
it("a --var name outside the setting-name rule is a usage error naming it", () => {
|
|
437
442
|
for (const raw of ["a b=1", "a-b=1", "1a=1", "a\nb=1"]) {
|
|
438
|
-
const plan = parseArgv(["node", "gtd.js", "
|
|
443
|
+
const plan = parseArgv(["node", "gtd.js", "start", "e", "--var", raw])
|
|
439
444
|
expect(plan.kind).toBe("usage")
|
|
440
445
|
if (plan.kind === "usage") {
|
|
441
446
|
expect(plan.message).toContain(
|
|
@@ -446,36 +451,25 @@ describe("parseArgv — entry/var flag details (formerly parseEntryFlags/takeFla
|
|
|
446
451
|
})
|
|
447
452
|
|
|
448
453
|
it("a multiline --var value is a usage error", () => {
|
|
449
|
-
const plan = parseArgv(["node", "gtd.js", "
|
|
454
|
+
const plan = parseArgv(["node", "gtd.js", "start", "e", "--var", "a=1\n2"])
|
|
450
455
|
expect(plan.kind).toBe("usage")
|
|
451
456
|
})
|
|
452
457
|
})
|
|
453
458
|
|
|
454
459
|
describe("parseArgv — --entry is removed", () => {
|
|
455
|
-
it("is an ordinary unknown option
|
|
460
|
+
it("is an ordinary unknown option", () => {
|
|
456
461
|
const plan = parseArgv(["node", "gtd.js", "--entry", "fix-precheck"])
|
|
457
462
|
expect(plan.kind).toBe("usage")
|
|
458
|
-
if (plan.kind === "usage")
|
|
459
|
-
expect(plan.message).toContain("--entry")
|
|
460
|
-
expect(plan.message).not.toContain("--workflow")
|
|
461
|
-
}
|
|
463
|
+
if (plan.kind === "usage") expect(plan.message).toContain("--entry")
|
|
462
464
|
})
|
|
463
465
|
})
|
|
464
466
|
|
|
465
|
-
describe("parseArgv —
|
|
466
|
-
it("
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
expect(plan.command).toMatchObject({ kind: "start",
|
|
471
|
-
}
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
it("gtd --workflow help likewise parses to a start command, not the help text", () => {
|
|
475
|
-
const plan = parseArgv(["node", "gtd.js", "--workflow", "help"])
|
|
476
|
-
expect(plan.kind).toBe("command")
|
|
477
|
-
if (plan.kind === "command") {
|
|
478
|
-
expect(plan.command).toMatchObject({ kind: "start", workflow: "help" })
|
|
467
|
+
describe("parseArgv — gtd start <name>", () => {
|
|
468
|
+
it("a workflow named like a command word still parses to a start command", () => {
|
|
469
|
+
for (const name of ["version", "help"]) {
|
|
470
|
+
const plan = parseArgv(["node", "gtd.js", "start", name])
|
|
471
|
+
expect(plan.kind).toBe("command")
|
|
472
|
+
if (plan.kind === "command") expect(plan.command).toMatchObject({ kind: "start", name })
|
|
479
473
|
}
|
|
480
474
|
})
|
|
481
475
|
})
|
|
@@ -684,7 +678,7 @@ describe("parseArgv — --verbose / -v (the -v/-V swap)", () => {
|
|
|
684
678
|
["land", "--verbose"],
|
|
685
679
|
["next", "--verbose", "--json"],
|
|
686
680
|
["check", "qa", "TODO.md", "--verbose"],
|
|
687
|
-
["
|
|
681
|
+
["start", "some-state", "--verbose"],
|
|
688
682
|
]) {
|
|
689
683
|
const plan = parseArgv(["node", "gtd.js", ...args])
|
|
690
684
|
expect(plan.kind).toBe("command")
|
|
@@ -693,30 +687,39 @@ describe("parseArgv — --verbose / -v (the -v/-V swap)", () => {
|
|
|
693
687
|
})
|
|
694
688
|
})
|
|
695
689
|
|
|
696
|
-
describe("parseArgv — gtd
|
|
697
|
-
it("parses to
|
|
698
|
-
const plan = parseArgv(["node", "gtd.js", "
|
|
690
|
+
describe("parseArgv — gtd guide", () => {
|
|
691
|
+
it("parses to a guide command", () => {
|
|
692
|
+
const plan = parseArgv(["node", "gtd.js", "guide"])
|
|
699
693
|
expect(plan.kind).toBe("command")
|
|
700
694
|
if (plan.kind === "command") {
|
|
701
|
-
expect(plan.command).toEqual({ kind: "
|
|
695
|
+
expect(plan.command).toEqual({ kind: "guide" })
|
|
702
696
|
expect(plan.json).toEqual({ kind: "off" })
|
|
703
697
|
}
|
|
704
698
|
})
|
|
705
699
|
|
|
706
|
-
it("
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
700
|
+
it("install, door and doors are gone: unknown commands", () => {
|
|
701
|
+
expect(parseArgv(["node", "gtd.js", "install"])).toMatchObject({
|
|
702
|
+
kind: "usage",
|
|
703
|
+
message: expect.stringContaining("unknown command 'install'"),
|
|
704
|
+
})
|
|
705
|
+
for (const word of ["door", "doors"]) {
|
|
706
|
+
expect(parseArgv(["node", "gtd.js", word, "fix"])).toMatchObject({
|
|
707
|
+
kind: "usage",
|
|
708
|
+
message: expect.stringContaining(`unknown command '${word}'`),
|
|
709
|
+
})
|
|
710
|
+
}
|
|
711
|
+
expect(parseArgv(["node", "gtd.js", "help", "install"]).kind).toBe("usage")
|
|
712
|
+
expect(renderHelp()).not.toMatch(/^ {2}(install|doors?)\b|--workflow|Older spellings/m)
|
|
710
713
|
})
|
|
711
714
|
|
|
712
|
-
it("gtd
|
|
713
|
-
const plan = parseArgv(["node", "gtd.js", "
|
|
715
|
+
it("gtd guide extra is a usage error (guide takes no argument)", () => {
|
|
716
|
+
const plan = parseArgv(["node", "gtd.js", "guide", "extra"])
|
|
714
717
|
expect(plan.kind).toBe("usage")
|
|
715
718
|
if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
|
|
716
719
|
})
|
|
717
720
|
|
|
718
|
-
it("a scoped-out flag (e.g. --port) is rejected on
|
|
719
|
-
const plan = parseArgv(["node", "gtd.js", "
|
|
721
|
+
it("a scoped-out flag (e.g. --port) is rejected on guide", () => {
|
|
722
|
+
const plan = parseArgv(["node", "gtd.js", "guide", "--port=3"])
|
|
720
723
|
expect(plan.kind).toBe("usage")
|
|
721
724
|
if (plan.kind === "usage") expect(plan.message).toContain("only valid for `gtd ui`")
|
|
722
725
|
})
|
|
@@ -859,7 +862,7 @@ describe("parseArgv — gtd judge run", () => {
|
|
|
859
862
|
[["judge", "run", "--answers", "a.json"], "--answers is only valid with --provider fixed"],
|
|
860
863
|
[["judge", "run", "--provider", "jev", "--model", "x"], "--model"],
|
|
861
864
|
[["judge", "run", "--provider", "fixed", "--model", "x"], "--model"],
|
|
862
|
-
[["next", "--model", "x"], "`gtd land` (with --cost
|
|
865
|
+
[["next", "--model", "x"], "`gtd land` (with --cost: the model to record the cost against)"],
|
|
863
866
|
[["judge", "run", "--provider", "fixed", "--json"], "--json is only valid"],
|
|
864
867
|
[["judge", "run", "--provider", "fixed", "--answers="], "requires a value"],
|
|
865
868
|
[["judge", "--provider", "fixed"], "only valid for `gtd judge run`"],
|
|
@@ -872,61 +875,6 @@ describe("parseArgv — gtd judge run", () => {
|
|
|
872
875
|
})
|
|
873
876
|
})
|
|
874
877
|
|
|
875
|
-
describe("parseArgv — doors", () => {
|
|
876
|
-
const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
|
|
877
|
-
|
|
878
|
-
it("`gtd door <name>` carries the name and no args", () => {
|
|
879
|
-
const plan = parse("door", "fix")
|
|
880
|
-
expect(plan).toMatchObject({
|
|
881
|
-
kind: "command",
|
|
882
|
-
command: { kind: "door", name: "fix", args: [] },
|
|
883
|
-
})
|
|
884
|
-
})
|
|
885
|
-
|
|
886
|
-
it("`gtd door <name> a b` carries every positional after the name, in order", () => {
|
|
887
|
-
const plan = parse("door", "review", "v1.0", "extra")
|
|
888
|
-
expect(plan).toMatchObject({
|
|
889
|
-
kind: "command",
|
|
890
|
-
command: { kind: "door", name: "review", args: ["v1.0", "extra"] },
|
|
891
|
-
})
|
|
892
|
-
})
|
|
893
|
-
|
|
894
|
-
it("`gtd door` with no name is a usage error", () => {
|
|
895
|
-
const plan = parse("door")
|
|
896
|
-
expect(plan.kind).toBe("usage")
|
|
897
|
-
if (plan.kind === "usage") expect(plan.message).toContain("gtd door: missing name argument")
|
|
898
|
-
})
|
|
899
|
-
|
|
900
|
-
it("`gtd doors` parses with every --json shape", () => {
|
|
901
|
-
expect(parse("doors")).toMatchObject({
|
|
902
|
-
kind: "command",
|
|
903
|
-
command: { kind: "doors" },
|
|
904
|
-
json: { kind: "off" },
|
|
905
|
-
})
|
|
906
|
-
expect(parse("doors", "--json")).toMatchObject({ json: { kind: "document" } })
|
|
907
|
-
expect(parse("doors", "--json=0.name")).toMatchObject({
|
|
908
|
-
json: { kind: "select", path: "0.name" },
|
|
909
|
-
})
|
|
910
|
-
})
|
|
911
|
-
|
|
912
|
-
it("`gtd doors` takes no argument", () => {
|
|
913
|
-
const plan = parse("doors", "extra")
|
|
914
|
-
expect(plan.kind).toBe("usage")
|
|
915
|
-
if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
|
|
916
|
-
})
|
|
917
|
-
|
|
918
|
-
it("`gtd door` rejects --json and --var — a door's vars come from its args", () => {
|
|
919
|
-
expect(parse("door", "fix", "--json").kind).toBe("usage")
|
|
920
|
-
expect(parse("door", "fix", "--var", "a=1").kind).toBe("usage")
|
|
921
|
-
})
|
|
922
|
-
|
|
923
|
-
it("help lists both commands", () => {
|
|
924
|
-
const help = renderHelp()
|
|
925
|
-
expect(help).toContain("door <name> [args...]")
|
|
926
|
-
expect(help).toMatch(/^ {2}doors\b/m)
|
|
927
|
-
})
|
|
928
|
-
})
|
|
929
|
-
|
|
930
878
|
describe("parseArgv — gtd history", () => {
|
|
931
879
|
const parse = (...args: string[]) => parseArgv(["node", "gtd.js", ...args])
|
|
932
880
|
const usage = (plan: CliPlan): string => (plan.kind === "usage" ? plan.message : "")
|
|
@@ -953,7 +901,7 @@ describe("parseArgv — gtd history", () => {
|
|
|
953
901
|
expect(parse("history", "metrics", "a", "feat/b")).toMatchObject({
|
|
954
902
|
command: { kind: "historyMetrics", refs: ["a", "feat/b"] },
|
|
955
903
|
})
|
|
956
|
-
expect(parse("history", "judges", "a", "--
|
|
904
|
+
expect(parse("history", "judges", "a", "--threshold", "qualityTriageMinP=0.9")).toMatchObject({
|
|
957
905
|
command: {
|
|
958
906
|
kind: "historyJudges",
|
|
959
907
|
refs: ["a"],
|
|
@@ -966,21 +914,40 @@ describe("parseArgv — gtd history", () => {
|
|
|
966
914
|
})
|
|
967
915
|
})
|
|
968
916
|
|
|
969
|
-
it("judges takes only judge thresholds as --
|
|
970
|
-
expect(
|
|
971
|
-
|
|
917
|
+
it("judges takes only judge thresholds as --threshold, and --jsonl alone", () => {
|
|
918
|
+
expect(
|
|
919
|
+
parse(
|
|
920
|
+
"history",
|
|
921
|
+
"judges",
|
|
922
|
+
"--threshold=qualityTriageMinP=0.9",
|
|
923
|
+
"--threshold",
|
|
924
|
+
"judgeIdenticalMinP=0.5",
|
|
925
|
+
),
|
|
926
|
+
).toMatchObject({
|
|
927
|
+
command: { vars: { qualityTriageMinP: "0.9", judgeIdenticalMinP: "0.5" } },
|
|
928
|
+
})
|
|
929
|
+
expect(usage(parse("history", "judges", "--threshold", "reviewBase=main"))).toBe(
|
|
930
|
+
"gtd history judges: --threshold reviewBase is no judge threshold — use judgeIdenticalMinP or qualityTriageMinP",
|
|
972
931
|
)
|
|
973
|
-
expect(usage(parse("history", "judges", "--
|
|
932
|
+
expect(usage(parse("history", "judges", "--threshold", "toString=0.9"))).toContain(
|
|
974
933
|
"no judge threshold",
|
|
975
934
|
)
|
|
976
|
-
|
|
977
|
-
"
|
|
978
|
-
|
|
935
|
+
for (const value of ["x", "0,9"]) {
|
|
936
|
+
expect(usage(parse("history", "judges", "--threshold", `qualityTriageMinP=${value}`))).toBe(
|
|
937
|
+
`gtd history judges: --threshold qualityTriageMinP must be a number — got "${value}"`,
|
|
938
|
+
)
|
|
939
|
+
}
|
|
979
940
|
expect(usage(parse("history", "judges", "--jsonl", "--json"))).toContain("mutually exclusive")
|
|
980
|
-
expect(
|
|
981
|
-
"
|
|
941
|
+
expect(
|
|
942
|
+
usage(parse("history", "judges", "--jsonl", "--threshold", "qualityTriageMinP=1")),
|
|
943
|
+
).toContain("mutually exclusive")
|
|
944
|
+
expect(usage(parse("history", "metrics", "--threshold", "qualityTriageMinP=1"))).toContain(
|
|
945
|
+
"only valid for `gtd history judges`",
|
|
982
946
|
)
|
|
983
947
|
expect(usage(parse("history", "metrics", "--jsonl"))).toContain("only valid for")
|
|
948
|
+
const help = helpFor("help", "history", "judges")
|
|
949
|
+
expect(help).toContain("--threshold <name>=<value>")
|
|
950
|
+
expect(help).not.toContain("--var")
|
|
984
951
|
})
|
|
985
952
|
|
|
986
953
|
it("bare history and an unknown subcommand are usage errors naming list, show, metrics, fetch", () => {
|
|
@@ -1001,7 +968,7 @@ describe("parseArgv — gtd history", () => {
|
|
|
1001
968
|
expect(usage(parse("history", "fetch", "a", "b"))).toContain("too many arguments")
|
|
1002
969
|
})
|
|
1003
970
|
|
|
1004
|
-
it("--json is accepted for list, show
|
|
971
|
+
it("--json is accepted for list, show, metrics and fetch", () => {
|
|
1005
972
|
expect(parse("history", "list", "--json")).toMatchObject({ json: { kind: "document" } })
|
|
1006
973
|
expect(parse("history", "show", "x", "--json=ended")).toMatchObject({
|
|
1007
974
|
json: { kind: "select", path: "ended" },
|
|
@@ -1009,7 +976,9 @@ describe("parseArgv — gtd history", () => {
|
|
|
1009
976
|
expect(parse("history", "metrics", "--json=total.refactorFirstRate")).toMatchObject({
|
|
1010
977
|
json: { kind: "select", path: "total.refactorFirstRate" },
|
|
1011
978
|
})
|
|
1012
|
-
expect(parse("history", "fetch", "--json")
|
|
979
|
+
expect(parse("history", "fetch", "--json=script")).toMatchObject({
|
|
980
|
+
json: { kind: "select", path: "script" },
|
|
981
|
+
})
|
|
1013
982
|
})
|
|
1014
983
|
|
|
1015
984
|
it("help lists the four history rows", () => {
|
|
@@ -1022,6 +991,70 @@ describe("parseArgv — gtd history", () => {
|
|
|
1022
991
|
})
|
|
1023
992
|
})
|
|
1024
993
|
|
|
994
|
+
describe("parseArgv — gtd start", () => {
|
|
995
|
+
const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
|
|
996
|
+
|
|
997
|
+
it("bare `gtd start` parses with no name, so the loaded config can list what starts", () => {
|
|
998
|
+
expect(parse("start")).toEqual({
|
|
999
|
+
kind: "command",
|
|
1000
|
+
command: { kind: "start", args: [], vars: {} },
|
|
1001
|
+
json: { kind: "off" },
|
|
1002
|
+
verbose: false,
|
|
1003
|
+
})
|
|
1004
|
+
expect(parse("start", "--json")).toMatchObject({ json: { kind: "document" } })
|
|
1005
|
+
})
|
|
1006
|
+
|
|
1007
|
+
it("carries the name, every positional after it, and the --var pins", () => {
|
|
1008
|
+
expect(parse("start", "review", "main", "--var", "a=1", "--var=b=2")).toMatchObject({
|
|
1009
|
+
command: { kind: "start", name: "review", args: ["main"], vars: { a: "1", b: "2" } },
|
|
1010
|
+
})
|
|
1011
|
+
})
|
|
1012
|
+
|
|
1013
|
+
it("--var with no name is a usage error", () => {
|
|
1014
|
+
const plan = parse("start", "--var", "a=1")
|
|
1015
|
+
expect(plan.kind === "usage" && plan.message).toContain("--var needs a <name>")
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
it("its help names doors, workflows, --var and the --json=script idiom", () => {
|
|
1019
|
+
const help = helpFor("help", "start")
|
|
1020
|
+
expect(help).toMatch(/^Usage: gtd start \[name\] \[args\.\.\.\] \[options\]\n/)
|
|
1021
|
+
expect(help).toMatch(/A door\s+shadows a workflow of the same name/)
|
|
1022
|
+
expect(help).toContain("--var <name>=<value>")
|
|
1023
|
+
expect(help).toContain('s=$(gtd start <name> --json=script) && sh -c "$s"')
|
|
1024
|
+
})
|
|
1025
|
+
})
|
|
1026
|
+
|
|
1027
|
+
describe("parseArgv — -- ends the options", () => {
|
|
1028
|
+
const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
|
|
1029
|
+
|
|
1030
|
+
it("every word after -- is a positional, even one that looks like an option", () => {
|
|
1031
|
+
expect(parse("strays", "a..b", "--", "--weird", "-x")).toMatchObject({
|
|
1032
|
+
command: { kind: "strays", globs: ["--weird", "-x"] },
|
|
1033
|
+
})
|
|
1034
|
+
expect(parse("start", "fix", "--json=script", "--", "--var")).toMatchObject({
|
|
1035
|
+
command: { kind: "start", name: "fix", args: ["--var"], vars: {} },
|
|
1036
|
+
json: { kind: "select", path: "script" },
|
|
1037
|
+
})
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
it("--help, -h, --version and -V after -- are arguments, not short-circuits", () => {
|
|
1041
|
+
expect(parse("start", "fix", "--", "--help")).toMatchObject({
|
|
1042
|
+
command: { kind: "start", args: ["--help"] },
|
|
1043
|
+
})
|
|
1044
|
+
expect(parse("start", "fix", "--", "-V").kind).toBe("command")
|
|
1045
|
+
expect(parse("next", "--", "--version")).toMatchObject({ kind: "usage" })
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
it("a bare -- changes nothing", () => {
|
|
1049
|
+
expect(parse("next", "--")).toEqual(parse("next"))
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
it("an option that needs a value never takes -- as it", () => {
|
|
1053
|
+
const plan = parse("start", "fix", "--var", "--", "a=1")
|
|
1054
|
+
expect(plan.kind === "usage" && plan.message).toContain("--var requires a value")
|
|
1055
|
+
})
|
|
1056
|
+
})
|
|
1057
|
+
|
|
1025
1058
|
describe("parseArgv — gtd strays", () => {
|
|
1026
1059
|
const strays = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", "strays", ...args])
|
|
1027
1060
|
|
|
@@ -1061,20 +1094,21 @@ describe("parseArgv — gtd strays", () => {
|
|
|
1061
1094
|
const plan = parseArgv(["node", "gtd.js", "lsp", "--json"])
|
|
1062
1095
|
if (plan.kind === "usage") expect(plan.message).toContain("`gtd strays`")
|
|
1063
1096
|
expect(renderHelp()).toContain("strays <range> [glob...]")
|
|
1064
|
-
|
|
1097
|
+
const help = parseArgv(["node", "gtd.js", "help", "strays"])
|
|
1098
|
+
expect(help.kind === "output" && help.stdout).toContain("--json[=<path>]")
|
|
1065
1099
|
})
|
|
1066
1100
|
})
|
|
1067
1101
|
|
|
1068
1102
|
describe("standaloneKinds / needsOf", () => {
|
|
1069
1103
|
it("pins the standalone kinds", () => {
|
|
1070
|
-
expect(standaloneKinds()).toEqual(["lsp", "check", "uncheck", "
|
|
1104
|
+
expect(standaloneKinds()).toEqual(["lsp", "check", "uncheck", "guide", "judgeRun", "strays"])
|
|
1071
1105
|
})
|
|
1072
1106
|
|
|
1073
1107
|
it("needsOf matches none/fs for the standalone kinds and state for everything else", () => {
|
|
1074
1108
|
expect(needsOf("lsp")).toBe("none")
|
|
1075
1109
|
expect(needsOf("check")).toBe("fs")
|
|
1076
1110
|
expect(needsOf("uncheck")).toBe("fs")
|
|
1077
|
-
expect(needsOf("
|
|
1111
|
+
expect(needsOf("guide")).toBe("none")
|
|
1078
1112
|
expect(needsOf("judgeRun")).toBe("none")
|
|
1079
1113
|
expect(needsOf("strays")).toBe("none")
|
|
1080
1114
|
for (const kind of [
|
|
@@ -1087,8 +1121,6 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1087
1121
|
"ui",
|
|
1088
1122
|
"judge",
|
|
1089
1123
|
"judgeAnswer",
|
|
1090
|
-
"door",
|
|
1091
|
-
"doors",
|
|
1092
1124
|
"historyList",
|
|
1093
1125
|
"historyShow",
|
|
1094
1126
|
"historyMetrics",
|
|
@@ -1100,56 +1132,96 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1100
1132
|
})
|
|
1101
1133
|
})
|
|
1102
1134
|
|
|
1135
|
+
const helpFor = (...args: string[]): string => {
|
|
1136
|
+
const plan = parseArgv(["node", "gtd.js", ...args])
|
|
1137
|
+
if (plan.kind !== "output") throw new Error(`no help for ${args.join(" ")}`)
|
|
1138
|
+
return plan.stdout
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** The command names the overview lists — ` name ` lines, as the docs pins read them. */
|
|
1142
|
+
const listedCommands = (): string[] =>
|
|
1143
|
+
[...renderHelp().matchAll(/^ {2}([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)(?= )/gm)].map(([, n]) => n!)
|
|
1144
|
+
|
|
1103
1145
|
describe("renderHelp", () => {
|
|
1104
|
-
it("
|
|
1146
|
+
it("groups the commands and mentions no removed surface", () => {
|
|
1105
1147
|
const help = renderHelp()
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
expect(help).toContain("
|
|
1110
|
-
expect(help).toContain("abandon")
|
|
1111
|
-
expect(help).toContain("restore")
|
|
1112
|
-
expect(help).toContain("next")
|
|
1113
|
-
expect(help).toContain("validate")
|
|
1114
|
-
expect(help).toContain("lsp")
|
|
1115
|
-
expect(help).not.toContain("visualize")
|
|
1116
|
-
expect(help).not.toMatch(/^ {2}init\b/m)
|
|
1117
|
-
expect(help).not.toContain(".gtdrc")
|
|
1118
|
-
expect(help).toMatch(/^ {2}ui\b/m)
|
|
1148
|
+
for (const heading of ["Core loop:", "Start and stop:", "Inspect:", "Driver plumbing:"]) {
|
|
1149
|
+
expect(help).toContain(`\n${heading}\n`)
|
|
1150
|
+
}
|
|
1151
|
+
expect(help).toContain("start [name] [args...]")
|
|
1119
1152
|
expect(help).toContain("check <mode> <file>")
|
|
1120
|
-
expect(help).toContain("
|
|
1121
|
-
expect(help).toContain("
|
|
1122
|
-
expect(help).
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
expect(help).
|
|
1136
|
-
expect(help).toContain("--version, -V")
|
|
1137
|
-
expect(help).toContain("--help, -h")
|
|
1138
|
-
expect(help).not.toContain("review <commitish>")
|
|
1139
|
-
expect(help).not.toContain("format <file>")
|
|
1140
|
-
expect(help).not.toContain("--debug")
|
|
1141
|
-
expect(help).not.toContain("bin/gtd")
|
|
1142
|
-
expect(help).not.toContain("(no command), loop")
|
|
1143
|
-
expect(help).not.toContain("--dispatch")
|
|
1144
|
-
expect(help).not.toContain("--if-resting")
|
|
1145
|
-
expect(help).not.toMatch(/^ {2}serve\b/m)
|
|
1153
|
+
expect(help).toContain("-V, --version")
|
|
1154
|
+
expect(help).toContain("-h, --help")
|
|
1155
|
+
expect(help).toContain("Examples:")
|
|
1156
|
+
for (const removed of [
|
|
1157
|
+
"--workflow",
|
|
1158
|
+
"Older spellings",
|
|
1159
|
+
"visualize",
|
|
1160
|
+
".gtdrc",
|
|
1161
|
+
"--no-open",
|
|
1162
|
+
"--debug",
|
|
1163
|
+
"--dispatch",
|
|
1164
|
+
"bin/gtd",
|
|
1165
|
+
]) {
|
|
1166
|
+
expect(help).not.toContain(removed)
|
|
1167
|
+
}
|
|
1168
|
+
expect(help).not.toMatch(/^ {2}(init|serve|install|door|doors)\b/m)
|
|
1146
1169
|
expect(help).toMatch(/\n$/)
|
|
1147
1170
|
})
|
|
1148
1171
|
|
|
1149
|
-
it("every
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1172
|
+
it("keeps every line of every help text within 80 columns", () => {
|
|
1173
|
+
for (const text of [renderHelp(), ...listedCommands().map((name) => helpFor("help", name))]) {
|
|
1174
|
+
for (const line of text.split("\n")) expect(line.length, line).toBeLessThanOrEqual(80)
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
it("names no internals", () => {
|
|
1179
|
+
const all = [renderHelp(), ...listedCommands().map((name) => helpFor("help", name))].join("")
|
|
1180
|
+
for (const internal of ["it.processCost", "refs/worktree/gtd/history", "Effect Schema"]) {
|
|
1181
|
+
expect(all).not.toContain(internal)
|
|
1182
|
+
}
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
it("every flag is in the help of a command it is valid for", () => {
|
|
1186
|
+
const all = [renderHelp(), ...listedCommands().map((name) => helpFor("help", name))].join("")
|
|
1187
|
+
for (const name of FLAG_NAMES) expect(all).toContain(name)
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
it("gtd <cmd> --help, gtd <cmd> -h and gtd help <cmd> print that command's own help", () => {
|
|
1191
|
+
const help = helpFor("help", "land")
|
|
1192
|
+
expect(help).toMatch(/^Usage: gtd land \[options\]\n/)
|
|
1193
|
+
expect(help).toContain("--cost=<n>")
|
|
1194
|
+
expect(help).not.toContain("--port")
|
|
1195
|
+
expect(helpFor("land", "--help")).toBe(help)
|
|
1196
|
+
expect(helpFor("land", "-h")).toBe(help)
|
|
1197
|
+
})
|
|
1198
|
+
|
|
1199
|
+
it("a two-word command's help is its own", () => {
|
|
1200
|
+
expect(helpFor("judge", "run", "--help")).toMatch(/^Usage: gtd judge run \[options\]/)
|
|
1201
|
+
expect(helpFor("help", "history", "show")).toMatch(/^Usage: gtd history show <ref>/)
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
it("a flag that means something else per command is described per command", () => {
|
|
1205
|
+
expect(helpFor("help", "land")).toContain("With --cost: the model")
|
|
1206
|
+
expect(helpFor("help", "land")).toContain("nothing runs on it")
|
|
1207
|
+
expect(helpFor("help", "judge", "run")).toContain("The claude model")
|
|
1208
|
+
expect(helpFor("help", "abandon")).toContain("Print { schema, script }")
|
|
1209
|
+
expect(helpFor("help", "next")).not.toContain("Print { schema, script }")
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
it("--help after an unknown command still prints the overview", () => {
|
|
1213
|
+
expect(helpFor("bogus", "--help")).toBe(renderHelp())
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
it("gtd help <unknown> is a usage error naming the nearest command", () => {
|
|
1217
|
+
const plan = parseArgv(["node", "gtd.js", "help", "lnad"])
|
|
1218
|
+
expect(plan).toEqual({
|
|
1219
|
+
kind: "usage",
|
|
1220
|
+
stdout: "",
|
|
1221
|
+
message:
|
|
1222
|
+
"gtd: help: no command 'lnad' — did you mean `gtd help land`? Run `gtd help` for every command",
|
|
1223
|
+
json: false,
|
|
1224
|
+
})
|
|
1153
1225
|
})
|
|
1154
1226
|
|
|
1155
1227
|
it("the docs/cli.md Commands fenced block equals renderHelp()", () => {
|
|
@@ -1159,6 +1231,15 @@ describe("renderHelp", () => {
|
|
|
1159
1231
|
expect(match![1] + "\n").toBe(renderHelp())
|
|
1160
1232
|
})
|
|
1161
1233
|
|
|
1234
|
+
it("docs/cli.md's Every command section is every command's help, in the overview's order", () => {
|
|
1235
|
+
const doc = readFileSync(resolve(import.meta.dirname, "../../docs/cli.md"), "utf8")
|
|
1236
|
+
const section = doc.slice(doc.indexOf("\n## Every command\n"))
|
|
1237
|
+
const documented = [...section.matchAll(/^### `gtd ([^`]+)`\n\n```\n([\s\S]*?)```\n/gm)].map(
|
|
1238
|
+
([, name, help]) => [name, help],
|
|
1239
|
+
)
|
|
1240
|
+
expect(documented).toEqual(listedCommands().map((name) => [name, helpFor("help", name)]))
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1162
1243
|
it("README.md and COMPATIBILITY.md list exactly the commands", () => {
|
|
1163
1244
|
const commands = new Set(
|
|
1164
1245
|
[...renderHelp().matchAll(/^ {2}([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)(?= )/gm)].map(
|
|
@@ -1280,17 +1361,18 @@ describe("runCli — a garbled GTD_FLAG_* value", () => {
|
|
|
1280
1361
|
})
|
|
1281
1362
|
|
|
1282
1363
|
describe("runCli — StartFlags", () => {
|
|
1283
|
-
const CONFIG = `import { agent, human } from "@pmelab/gtd/flows"
|
|
1364
|
+
const CONFIG = `import { agent, human, workflow } from "@pmelab/gtd/flows"
|
|
1284
1365
|
|
|
1285
1366
|
export default async () => {
|
|
1286
1367
|
await human("idle")
|
|
1287
1368
|
}
|
|
1288
1369
|
|
|
1289
|
-
export const side =
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1370
|
+
export const side = workflow(
|
|
1371
|
+
async () => {
|
|
1372
|
+
await agent("working", "w")
|
|
1373
|
+
},
|
|
1374
|
+
{ defaults: { flag_beta: "0" } },
|
|
1375
|
+
)
|
|
1294
1376
|
`
|
|
1295
1377
|
const BETA = { name: "beta", owner: "pmelab", expiry: "2099-01-01", kind: "release" } as const
|
|
1296
1378
|
const start = async (
|
|
@@ -1302,7 +1384,7 @@ export const defaults = { flag_beta: "0" }
|
|
|
1302
1384
|
repo.seedCommit("chore: add workflow")
|
|
1303
1385
|
const { io, result } = makeCapturingCliIo(repo, env)
|
|
1304
1386
|
await Effect.runPromise(
|
|
1305
|
-
runCli(["node", "gtd.js", "
|
|
1387
|
+
runCli(["node", "gtd.js", "start", "side"], { ...io, ...(flags ? { flags } : {}) }),
|
|
1306
1388
|
)
|
|
1307
1389
|
return result().stdout
|
|
1308
1390
|
}
|
|
@@ -1419,7 +1501,7 @@ describe("runCli — stdout stays byte-empty on every failing surface", () => {
|
|
|
1419
1501
|
const { stdout, stderr, exitCode } = captured()
|
|
1420
1502
|
expect(exitCode).toBe(EXIT_RUNTIME_ERROR)
|
|
1421
1503
|
expect(stdout).toBe("")
|
|
1422
|
-
expect(stderr).toContain("gtd
|
|
1504
|
+
expect(stderr).toContain("gtd: the repository has no commits yet")
|
|
1423
1505
|
})
|
|
1424
1506
|
|
|
1425
1507
|
it("runtime error: a handler's own Effect.fail (not the shared repo-root/commit guard) leaves stdout byte-empty", async () => {
|