@pmelab/gtd 23.2.0 → 24.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/.claude-plugin/plugin.json +1 -1
- package/README.md +44 -11
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43830 -56044
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +1953 -1724
- package/package.json +70 -8
- package/skills/authoring/SKILL.md +5 -4
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +32 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +74 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +36 -2
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +99 -0
- package/src/workflows/review/review.ts +33 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/shared/workflows.steps.ts +7 -0
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Command, CommandExecutor } from "@effect/platform"
|
|
2
|
+
import { Context, Effect, Layer, Stream } from "effect"
|
|
3
|
+
import { Host } from "./Host.js"
|
|
4
|
+
|
|
5
|
+
/** One command run's outcome: its exit status (a signal death reported as `null`, matching `spawnSync`) and its combined output (stdout then stderr). */
|
|
6
|
+
interface CommandOutcome {
|
|
7
|
+
readonly status: number | null
|
|
8
|
+
readonly output: string
|
|
9
|
+
/** `stdout`/`stderr` captured separately, alongside the merged `output` — additive, for callers (the tRPC router) that need a refusal's three fields distinct rather than flattened into one string. Optional so existing test doubles built from `{ status, output }` alone still satisfy this interface. */
|
|
10
|
+
readonly stdout?: string
|
|
11
|
+
readonly stderr?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The subprocess port: run one shell command in the repo root, wait for it to
|
|
16
|
+
* finish, and get its combined output back as one string (a workflow
|
|
17
|
+
* `script:` is run by the DRIVER, never by gtd — and a mode's
|
|
18
|
+
* `format:`/`validate:` command is RENDERED into the driver's script rather
|
|
19
|
+
* than spawned here). Its remaining callers are `gtd ui`'s own helpers
|
|
20
|
+
* (openssl, tailscale). `gtd ui`'s loop command is spawned by `Loop.ts`'s own
|
|
21
|
+
* port instead — it needs a per-worktree cwd, a shim-prepended `PATH`, real
|
|
22
|
+
* OS signal control while the child is still running, and never-combined
|
|
23
|
+
* stdout/stderr, none of which this port's single-shot "run to completion"
|
|
24
|
+
* shape supports.
|
|
25
|
+
*/
|
|
26
|
+
export class CommandRunner extends Context.Tag("CommandRunner")<
|
|
27
|
+
CommandRunner,
|
|
28
|
+
{
|
|
29
|
+
/** A SPAWN failure (no `bash`, unreadable cwd) fails the Effect; a non-zero EXIT is a value — how a mode command reports "invalid"/"broken" for the caller to interpret. */
|
|
30
|
+
readonly bash: (command: string) => Effect.Effect<CommandOutcome, Error>
|
|
31
|
+
}
|
|
32
|
+
>() {
|
|
33
|
+
/** A test layer over a canned `bash` implementation — no subprocess. */
|
|
34
|
+
static readonly layer = (
|
|
35
|
+
bash: (command: string) => Effect.Effect<CommandOutcome, Error>,
|
|
36
|
+
): Layer.Layer<CommandRunner> => Layer.succeed(CommandRunner, { bash })
|
|
37
|
+
|
|
38
|
+
static Live = Layer.effect(
|
|
39
|
+
CommandRunner,
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const { root } = yield* Host
|
|
42
|
+
const executor = yield* CommandExecutor.CommandExecutor
|
|
43
|
+
return {
|
|
44
|
+
bash: (command: string) =>
|
|
45
|
+
Effect.scoped(
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const process = yield* executor.start(
|
|
48
|
+
Command.make("bash", "-c", command).pipe(
|
|
49
|
+
Command.workingDirectory(root),
|
|
50
|
+
Command.stdout("pipe"),
|
|
51
|
+
Command.stderr("pipe"),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
const collect = (stream: typeof process.stdout) =>
|
|
55
|
+
stream.pipe(Stream.decodeText(), Stream.mkString)
|
|
56
|
+
const [stdout, stderr, status] = yield* Effect.all(
|
|
57
|
+
[
|
|
58
|
+
collect(process.stdout),
|
|
59
|
+
collect(process.stderr),
|
|
60
|
+
// A signal death fails process.exitCode; map to null to match spawnSync's contract.
|
|
61
|
+
process.exitCode.pipe(
|
|
62
|
+
Effect.map((code): number | null => code),
|
|
63
|
+
Effect.catchAll(() => Effect.succeed(null)),
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
{ concurrency: "unbounded" },
|
|
67
|
+
)
|
|
68
|
+
return { status, output: `${stdout}${stderr}`, stdout, stderr }
|
|
69
|
+
}),
|
|
70
|
+
).pipe(Effect.mapError((e) => (e instanceof Error ? e : new Error(String(e))))),
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { GtdError, Narrator, renderFailure } from "./Commentary.js"
|
|
4
|
+
import { PRESENTATION_FAILURE_WARNING } from "../emit/index.js"
|
|
5
|
+
|
|
6
|
+
describe("Narrator", () => {
|
|
7
|
+
it("is a no-op writer when built with verbose: false", async () => {
|
|
8
|
+
const lines: string[] = []
|
|
9
|
+
await Effect.runPromise(
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const narrator = yield* Narrator
|
|
12
|
+
yield* narrator.narrate("hello")
|
|
13
|
+
}).pipe(Effect.provide(Narrator.layer((chunk) => lines.push(chunk), false))),
|
|
14
|
+
)
|
|
15
|
+
expect(lines).toEqual([])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("writes each narrated line, newline-terminated, when verbose: true", async () => {
|
|
19
|
+
const lines: string[] = []
|
|
20
|
+
await Effect.runPromise(
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const narrator = yield* Narrator
|
|
23
|
+
yield* narrator.narrate("rest resolved: build.review.closing")
|
|
24
|
+
yield* narrator.narrate("config: module /repo/gtd.config.ts")
|
|
25
|
+
}).pipe(Effect.provide(Narrator.layer((chunk) => lines.push(chunk), true))),
|
|
26
|
+
)
|
|
27
|
+
expect(lines).toEqual([
|
|
28
|
+
"rest resolved: build.review.closing\n",
|
|
29
|
+
"config: module /repo/gtd.config.ts\n",
|
|
30
|
+
])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("warn writes to the sink even when verbose: false", async () => {
|
|
34
|
+
const lines: string[] = []
|
|
35
|
+
await Effect.runPromise(
|
|
36
|
+
Effect.gen(function* () {
|
|
37
|
+
const narrator = yield* Narrator
|
|
38
|
+
yield* narrator.warn("careful: something odd")
|
|
39
|
+
}).pipe(Effect.provide(Narrator.layer((chunk) => lines.push(chunk), false))),
|
|
40
|
+
)
|
|
41
|
+
expect(lines).toEqual(["careful: something odd\n"])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("warn writes to the sink when verbose: true too", async () => {
|
|
45
|
+
const lines: string[] = []
|
|
46
|
+
await Effect.runPromise(
|
|
47
|
+
Effect.gen(function* () {
|
|
48
|
+
const narrator = yield* Narrator
|
|
49
|
+
yield* narrator.warn("careful: something odd")
|
|
50
|
+
}).pipe(Effect.provide(Narrator.layer((chunk) => lines.push(chunk), true))),
|
|
51
|
+
)
|
|
52
|
+
expect(lines).toEqual(["careful: something odd\n"])
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe("GtdError", () => {
|
|
57
|
+
it("carries a message and a detail list, defaulting detail to empty", () => {
|
|
58
|
+
const bare = new GtdError("boom")
|
|
59
|
+
expect(bare.message).toBe("boom")
|
|
60
|
+
expect(bare.detail).toEqual([])
|
|
61
|
+
|
|
62
|
+
const detailed = new GtdError("boom", ["because X", "because Y"])
|
|
63
|
+
expect(detailed.message).toBe("boom")
|
|
64
|
+
expect(detailed.detail).toEqual(["because X", "because Y"])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("is a genuine Error — every `error instanceof Error` normalization idiom still catches it", () => {
|
|
68
|
+
expect(new GtdError("boom")).toBeInstanceOf(Error)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe("renderFailure", () => {
|
|
73
|
+
it("prefixes a bare message with 'gtd: '", () => {
|
|
74
|
+
expect(renderFailure(new Error("boom"))).toBe("gtd: boom")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("does not double-prefix a message that already starts with 'gtd:'", () => {
|
|
78
|
+
expect(renderFailure(new Error("gtd: already prefixed"))).toBe("gtd: already prefixed")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("does not double-prefix a message that already starts with 'gtd '", () => {
|
|
82
|
+
expect(renderFailure(new Error("gtd land: out of turn"))).toBe("gtd land: out of turn")
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("stringifies a non-Error thrown value", () => {
|
|
86
|
+
expect(renderFailure("just a string")).toBe("gtd: just a string")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("prints a GtdError's detail lines, each indented two spaces, after the prefixed message", () => {
|
|
90
|
+
const error = new GtdError("gtd: bad config", ["testCommand: gtd.config.ts"])
|
|
91
|
+
expect(renderFailure(error)).toBe("gtd: bad config\n testCommand: gtd.config.ts")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("prints every detail line, in order, for a multi-detail GtdError", () => {
|
|
95
|
+
const error = new GtdError("boom", ["first", "second"])
|
|
96
|
+
expect(renderFailure(error)).toBe("gtd: boom\n first\n second")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("a plain Error (the ~100 unmigrated sites) still renders as exactly one line", () => {
|
|
100
|
+
expect(renderFailure(new Error("plain failure"))).not.toContain("\n")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("prints detail lines for an error shaped like GtdError but not an instance of it — duck-typed, not instanceof-checked, so src/platform/Git.ts's own equivalently-shaped error (which may not import GtdError) still renders its remediation", () => {
|
|
104
|
+
class LookalikeError extends Error {
|
|
105
|
+
readonly detail: readonly string[]
|
|
106
|
+
constructor(message: string, detail: readonly string[]) {
|
|
107
|
+
super(message)
|
|
108
|
+
this.detail = detail
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const error = new LookalikeError("gtd: invalid ref", ["ref: some-ref"])
|
|
112
|
+
expect(error).not.toBeInstanceOf(GtdError)
|
|
113
|
+
expect(renderFailure(error)).toBe("gtd: invalid ref\n ref: some-ref")
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe("the emitted script's own stderr shape stays cross-referenced here", () => {
|
|
118
|
+
// `Emit.ts`'s discarded-optional-failure warning is written by a SCRIPT gtd
|
|
119
|
+
// emits, not by gtd's own process — so it never goes through `renderFailure`
|
|
120
|
+
// — but it lands on the same commentary channel this module owns, so its
|
|
121
|
+
// wording is pinned alongside every other stderr shape rather than only in
|
|
122
|
+
// `Emit.test.ts`.
|
|
123
|
+
it("is gtd:-prefixed, matching this channel's own convention", () => {
|
|
124
|
+
expect(PRESENTATION_FAILURE_WARNING).toContain("gtd: presentation-only follow-up failed")
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One line of stderr commentary — `narrate` is gated entirely by how the
|
|
5
|
+
* layer was built, never by the call site; `warn` is its ungated
|
|
6
|
+
* counterpart, always written regardless of verbosity.
|
|
7
|
+
*/
|
|
8
|
+
export class Narrator extends Context.Tag("Narrator")<
|
|
9
|
+
Narrator,
|
|
10
|
+
{
|
|
11
|
+
readonly narrate: (line: string) => Effect.Effect<void>
|
|
12
|
+
readonly warn: (line: string) => Effect.Effect<void>
|
|
13
|
+
}
|
|
14
|
+
>() {
|
|
15
|
+
/**
|
|
16
|
+
* `write` is the raw stderr sink (`CliIo.stderr` in production, so the
|
|
17
|
+
* in-memory `CliIo` used by `@inmem` e2e scenarios captures narration into
|
|
18
|
+
* the exact same buffer it already captures errors into). `verbose` gates
|
|
19
|
+
* every `narrate` line at construction time — the resulting service is a
|
|
20
|
+
* no-op writer when `false`, never a per-call check a narrating call site
|
|
21
|
+
* has to remember to make. `warn` bypasses that gate unconditionally.
|
|
22
|
+
*/
|
|
23
|
+
static readonly layer = (
|
|
24
|
+
write: (chunk: string) => void,
|
|
25
|
+
verbose: boolean,
|
|
26
|
+
): Layer.Layer<Narrator> =>
|
|
27
|
+
Layer.succeed(Narrator, {
|
|
28
|
+
narrate: (line) =>
|
|
29
|
+
Effect.sync(() => {
|
|
30
|
+
if (verbose) write(`${line}\n`)
|
|
31
|
+
}),
|
|
32
|
+
warn: (line) => Effect.sync(() => write(`${line}\n`)),
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* An error carrying REMEDIATION alongside its message — the offending config
|
|
38
|
+
* key and the layer it came from, a corrupted ref's name, a missing binary's
|
|
39
|
+
* resolved `$PATH`. Only two families construct one (see
|
|
40
|
+
* `src/workflow/load.ts` and `src/ui/Tls.ts`) — every other `Error` site
|
|
41
|
+
* stays a plain `Error` and renders as the single `gtd: `-prefixed line it
|
|
42
|
+
* always has.
|
|
43
|
+
*/
|
|
44
|
+
export class GtdError extends Error {
|
|
45
|
+
readonly detail: readonly string[]
|
|
46
|
+
|
|
47
|
+
constructor(message: string, detail: readonly string[] = []) {
|
|
48
|
+
super(message)
|
|
49
|
+
this.name = "GtdError"
|
|
50
|
+
this.detail = detail
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A `GtdError` that names how gtd was INVOKED, not what it did — `Cli.ts`'s
|
|
56
|
+
* `report` maps this to `EXIT_USAGE_ERROR` alongside `SelectorUsageError`,
|
|
57
|
+
* rather than the `EXIT_RUNTIME_ERROR` every plain `GtdError` gets. `gtd ui`
|
|
58
|
+
* refusing to start on a step it cannot render is the one constructor today
|
|
59
|
+
* (see `ui/Server.ts`).
|
|
60
|
+
*/
|
|
61
|
+
export class GtdUsageError extends GtdError {
|
|
62
|
+
constructor(message: string, detail: readonly string[] = []) {
|
|
63
|
+
super(message, detail)
|
|
64
|
+
this.name = "GtdUsageError"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Structurally, not nominally: an error carrying a `detail: readonly
|
|
70
|
+
* string[]` — `GtdError`'s shape, matched WITHOUT an `instanceof GtdError`
|
|
71
|
+
* check, because `src/platform/Git.ts` reports a corrupted ref's remediation
|
|
72
|
+
* lines through its own equivalently-shaped local error class.
|
|
73
|
+
*/
|
|
74
|
+
const hasDetail = (error: unknown): error is { readonly detail: readonly string[] } =>
|
|
75
|
+
typeof error === "object" &&
|
|
76
|
+
error !== null &&
|
|
77
|
+
"detail" in error &&
|
|
78
|
+
Array.isArray((error as { detail: unknown }).detail)
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The stderr text for a CLI failure: a `gtd: ` prefix UNLESS the message
|
|
82
|
+
* already carries one, then one two-space-indented line per `GtdError`
|
|
83
|
+
* detail (none, for a plain `Error`) — unconditional, at every verbosity.
|
|
84
|
+
* Most gtd errors are authored with a `gtd:`/`gtd <cmd>:` prefix of their own,
|
|
85
|
+
* so a blind prepend would produce a doubled `gtd: gtd: …`.
|
|
86
|
+
*/
|
|
87
|
+
export const renderFailure = (error: unknown): string => {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
89
|
+
const prefixed = /^gtd[: ]/.test(message) ? message : `gtd: ${message}`
|
|
90
|
+
const detail = hasDetail(error) ? error.detail : []
|
|
91
|
+
return [prefixed, ...detail.map((line) => ` ${line}`)].join("\n")
|
|
92
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Cause, Effect, Exit } from "effect"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { InvalidRefError, resolvedRefOrCorrupted } from "./Git.js"
|
|
4
|
+
import { gitTiers, runGitServiceContract, runGitScriptContract } from "../testkit/index.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `GitOperations` contract, run identically against both the Live and
|
|
8
|
+
* InMemory tiers (`src/testkit/GitTiers.ts`) — 20 operations × 2 tiers.
|
|
9
|
+
*/
|
|
10
|
+
for (const makeTier of gitTiers) {
|
|
11
|
+
const name = makeTier().name
|
|
12
|
+
describe(`GitService [${name}]`, () => {
|
|
13
|
+
runGitServiceContract(makeTier)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `resolveRef`'s own validation, unit-tested directly rather than through a
|
|
19
|
+
* real git repo — this is the rarer, defensive branch where the command
|
|
20
|
+
* SUCCEEDS but its output isn't a 40-hex-char hash: a corrupted ref, this
|
|
21
|
+
* module's own `InvalidRefError`.
|
|
22
|
+
*/
|
|
23
|
+
describe("resolvedRefOrCorrupted", () => {
|
|
24
|
+
it("succeeds with a genuine 40-hex-char hash", async () => {
|
|
25
|
+
const hash = "a".repeat(40)
|
|
26
|
+
const result = await Effect.runPromise(resolvedRefOrCorrupted("HEAD", hash))
|
|
27
|
+
expect(result).toBe(hash)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("fails with an InvalidRefError naming the ref, for anything else", async () => {
|
|
31
|
+
const exit = await Effect.runPromiseExit(resolvedRefOrCorrupted("some-ref", "not-a-hash"))
|
|
32
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
33
|
+
if (Exit.isFailure(exit)) {
|
|
34
|
+
const error = Cause.squash(exit.cause)
|
|
35
|
+
expect(error).toBeInstanceOf(InvalidRefError)
|
|
36
|
+
expect(error).toHaveProperty("message", "Invalid ref: some-ref")
|
|
37
|
+
if (error instanceof InvalidRefError) expect(error.detail).toEqual(["ref: some-ref"])
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `GitScript.ts`'s bash builders, run through real `bash` against a real git
|
|
44
|
+
* repo — the Live tier only, since the in-memory fake's root has no shell to
|
|
45
|
+
* execute against. Asserts the same 9 writer postconditions as the contract
|
|
46
|
+
* above, driven via the emitted script instead of the `GitOperations` port.
|
|
47
|
+
*/
|
|
48
|
+
describe("GitScript", () => {
|
|
49
|
+
runGitScriptContract()
|
|
50
|
+
})
|