@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,56 @@
|
|
|
1
|
+
import { seededValidateCommand } from "./SteeringFormats.js"
|
|
2
|
+
import { BUILT_IN_MODE_NAMES } from "../steering/index.js"
|
|
3
|
+
import type { ModeDef } from "./Workflow.js"
|
|
4
|
+
import { configProblems, type GtdConfig, type UiSettings } from "../flows/index.js"
|
|
5
|
+
import type { Diagnostic } from "./Diagnostic.js"
|
|
6
|
+
|
|
7
|
+
export interface CompiledConfig {
|
|
8
|
+
/** The built-in modes (qa/review, with gtd's own validators) merged with `config.modes`, per half. */
|
|
9
|
+
readonly modes: Record<string, ModeDef>
|
|
10
|
+
readonly ui?: UiSettings
|
|
11
|
+
readonly skills: Readonly<Record<string, readonly string[]>>
|
|
12
|
+
readonly access: GtdConfig["access"] & {}
|
|
13
|
+
readonly diagnostics: readonly Diagnostic[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A `gtd.config.ts` `config` export (or its absence), checked and compiled. A
|
|
18
|
+
* malformed export compiles to nothing beyond the built-in modes, with every
|
|
19
|
+
* problem reported at `origin`.
|
|
20
|
+
*/
|
|
21
|
+
export const compileConfig = (value: unknown, origin: string): CompiledConfig => {
|
|
22
|
+
const seeded: Record<string, ModeDef> = Object.fromEntries(
|
|
23
|
+
BUILT_IN_MODE_NAMES.map((name) => [name, { validate: seededValidateCommand(name) }]),
|
|
24
|
+
)
|
|
25
|
+
const problems = value === undefined ? [] : configProblems(value)
|
|
26
|
+
if (problems.length > 0) {
|
|
27
|
+
return {
|
|
28
|
+
modes: seeded,
|
|
29
|
+
skills: {},
|
|
30
|
+
access: {},
|
|
31
|
+
diagnostics: problems.map(({ path, message }) => ({
|
|
32
|
+
severity: "error",
|
|
33
|
+
path: ["config", ...path],
|
|
34
|
+
message,
|
|
35
|
+
origin,
|
|
36
|
+
})),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const config = (value ?? {}) as GtdConfig
|
|
40
|
+
// Per half: a `format:` alone, or a half left `undefined` (an unset
|
|
41
|
+
// `process.env.X`), keeps a built-in mode's own validator.
|
|
42
|
+
const modes = { ...seeded }
|
|
43
|
+
for (const [name, entry] of Object.entries(config.modes ?? {}))
|
|
44
|
+
modes[name] = {
|
|
45
|
+
...modes[name],
|
|
46
|
+
...Object.fromEntries(Object.entries(entry).filter(([, command]) => command !== undefined)),
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
modes,
|
|
50
|
+
...(config.ui !== undefined ? { ui: config.ui } : {}),
|
|
51
|
+
// Null prototype: a `__proto__` key must stay an ordinary (reportable) entry.
|
|
52
|
+
skills: Object.assign(Object.create(null), config.skills),
|
|
53
|
+
access: Object.assign(Object.create(null), config.access),
|
|
54
|
+
diagnostics: [],
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { Context, Effect, Layer } from "effect"
|
|
4
|
+
|
|
5
|
+
/** The workflow module found on the cwd→home walk, with its source. */
|
|
6
|
+
export interface WorkflowModule {
|
|
7
|
+
readonly filepath: string
|
|
8
|
+
readonly source: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A leftover rc file, and whether the user running gtd owns it — and so can delete it. */
|
|
12
|
+
interface LegacyRcFile {
|
|
13
|
+
readonly path: string
|
|
14
|
+
readonly owned: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ConfigDiscoveryOps {
|
|
18
|
+
/** Every removed `.gtdrc`-family file on the `root`→`home` walk and in `home` itself, innermost first. */
|
|
19
|
+
readonly legacyRcFiles: (
|
|
20
|
+
root: string,
|
|
21
|
+
home: string,
|
|
22
|
+
) => Effect.Effect<readonly LegacyRcFile[], Error>
|
|
23
|
+
/** The innermost `gtd.config.ts` walking `root` up through `home`, if any. */
|
|
24
|
+
readonly workflowModule: (
|
|
25
|
+
root: string,
|
|
26
|
+
home: string,
|
|
27
|
+
) => Effect.Effect<WorkflowModule | undefined, Error>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const WORKFLOW_MODULE = "gtd.config.ts"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The file names gtd read repository settings from before `gtd.config.ts` took
|
|
34
|
+
* them over. Still looked for, only to refuse them: a leftover one would
|
|
35
|
+
* otherwise be silently ignored. Shared with the in-memory counterpart in
|
|
36
|
+
* `src/testkit/Layers.ts`, so both tiers refuse the same names.
|
|
37
|
+
*/
|
|
38
|
+
export const LEGACY_RC_FILES = [
|
|
39
|
+
".gtdrc",
|
|
40
|
+
".gtdrc.json",
|
|
41
|
+
".gtdrc.yaml",
|
|
42
|
+
".gtdrc.yml",
|
|
43
|
+
"gtd.config.json",
|
|
44
|
+
"gtd.config.yaml",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/** The directories searched for a leftover rc file: the walk, plus `home` when the walk misses it. */
|
|
48
|
+
export const legacyRcDirs = (root: string, home: string): readonly string[] => {
|
|
49
|
+
const chain = walkUp(root, home)
|
|
50
|
+
return chain.includes(home) ? chain : [...chain, home]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const legacyRcFileAt = (path: string): LegacyRcFile | undefined => {
|
|
54
|
+
try {
|
|
55
|
+
const stat = statSync(path)
|
|
56
|
+
if (!stat.isFile()) return undefined
|
|
57
|
+
// Above home, a shared directory (`/tmp`) can hold another user's file.
|
|
58
|
+
return { path, owned: process.getuid === undefined || stat.uid === process.getuid() }
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Enumerate the directory chain from `from` walking UP. Stops after including
|
|
66
|
+
* the user's home dir (inclusive, when it is an ancestor) or after reaching the
|
|
67
|
+
* filesystem root — whichever comes first. Returned innermost→outermost.
|
|
68
|
+
* Exported (via the barrel) for the same reason `SEARCH_PLACES` is — one
|
|
69
|
+
* shared implementation, not a second copy in `src/testkit/Layers.ts` that
|
|
70
|
+
* could silently diverge.
|
|
71
|
+
*/
|
|
72
|
+
export const walkUp = (from: string, home: string): ReadonlyArray<string> => {
|
|
73
|
+
const chain: Array<string> = []
|
|
74
|
+
let dir = from
|
|
75
|
+
while (true) {
|
|
76
|
+
chain.push(dir)
|
|
77
|
+
if (dir === home) break
|
|
78
|
+
const parent = dirname(dir)
|
|
79
|
+
if (parent === dir) break // filesystem root
|
|
80
|
+
dir = parent
|
|
81
|
+
}
|
|
82
|
+
return chain
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const legacyRcFiles = (root: string, home: string): Effect.Effect<readonly LegacyRcFile[], Error> =>
|
|
86
|
+
Effect.sync(() =>
|
|
87
|
+
legacyRcDirs(root, home).flatMap((dir) =>
|
|
88
|
+
LEGACY_RC_FILES.flatMap((name) => legacyRcFileAt(join(dir, name)) ?? []),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const workflowModule = (
|
|
93
|
+
root: string,
|
|
94
|
+
home: string,
|
|
95
|
+
): Effect.Effect<WorkflowModule | undefined, Error> =>
|
|
96
|
+
Effect.try({
|
|
97
|
+
try: () => {
|
|
98
|
+
for (const dir of walkUp(root, home)) {
|
|
99
|
+
const filepath = join(dir, WORKFLOW_MODULE)
|
|
100
|
+
if (existsSync(filepath)) return { filepath, source: readFileSync(filepath, "utf8") }
|
|
101
|
+
}
|
|
102
|
+
return undefined
|
|
103
|
+
},
|
|
104
|
+
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The discovery seam — a `Context.Tag`, not part of `WorkflowFiles` (that
|
|
109
|
+
* port is a plain sync record for `compileWorkflow`'s own content-file-ref
|
|
110
|
+
* reads; discovery is effectful and environment-dependent: `.Live` reads
|
|
111
|
+
* REAL disk, so an `@inmem` scenario's fake
|
|
112
|
+
* `Workspace` — a pure in-memory `Map`, no real files anywhere on disk —
|
|
113
|
+
* cannot back it. `src/testkit/Layers.ts` provides the in-memory
|
|
114
|
+
* counterpart instead, mirroring how `Workspace`/`Host`/`GitService` already
|
|
115
|
+
* each have a production and a fake layer behind one Tag.
|
|
116
|
+
*/
|
|
117
|
+
export class ConfigDiscovery extends Context.Tag("ConfigDiscovery")<
|
|
118
|
+
ConfigDiscovery,
|
|
119
|
+
ConfigDiscoveryOps
|
|
120
|
+
>() {
|
|
121
|
+
static Live = Layer.succeed(ConfigDiscovery, { legacyRcFiles, workflowModule })
|
|
122
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { describe, expect, it } from "vitest"
|
|
5
|
+
import { changedConfigFiles, configFingerprint } from "./fingerprint.js"
|
|
6
|
+
|
|
7
|
+
describe("the config fingerprint", () => {
|
|
8
|
+
it("hashes each file under its root-relative path, skipping one that cannot be read", () => {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "gtd-fingerprint-"))
|
|
10
|
+
mkdirSync(join(root, "lib"))
|
|
11
|
+
writeFileSync(join(root, "gtd.config.ts"), "export default 1\n")
|
|
12
|
+
writeFileSync(join(root, "lib", "flow.ts"), "export const a = 1\n")
|
|
13
|
+
|
|
14
|
+
const fingerprint = configFingerprint(
|
|
15
|
+
[join(root, "gtd.config.ts"), join(root, "lib", "flow.ts"), join(root, "gone.ts")],
|
|
16
|
+
root,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
expect(Object.keys(fingerprint).sort()).toEqual(["gtd.config.ts", "lib/flow.ts"])
|
|
20
|
+
expect(fingerprint["gtd.config.ts"]).toMatch(/^[0-9a-f]{16}$/)
|
|
21
|
+
writeFileSync(join(root, "lib", "flow.ts"), "export const a = 2\n")
|
|
22
|
+
expect(
|
|
23
|
+
changedConfigFiles(
|
|
24
|
+
fingerprint,
|
|
25
|
+
configFingerprint([join(root, "gtd.config.ts"), join(root, "lib", "flow.ts")], root),
|
|
26
|
+
),
|
|
27
|
+
).toEqual(["lib/flow.ts"])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("relates a file reported by its real path to a root reached through a symlink", () => {
|
|
31
|
+
const real = mkdtempSync(join(tmpdir(), "gtd-fingerprint-"))
|
|
32
|
+
writeFileSync(join(real, "gtd.config.ts"), "export default 1\n")
|
|
33
|
+
const link = join(mkdtempSync(join(tmpdir(), "gtd-fingerprint-link-")), "repo")
|
|
34
|
+
symlinkSync(real, link)
|
|
35
|
+
|
|
36
|
+
expect(
|
|
37
|
+
Object.keys(configFingerprint([join(realpathSync(real), "gtd.config.ts")], link)),
|
|
38
|
+
).toEqual(["gtd.config.ts"])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("never records a path with a control character", () => {
|
|
42
|
+
const root = mkdtempSync(join(tmpdir(), "gtd-fingerprint-"))
|
|
43
|
+
writeFileSync(join(root, "a\x1b.ts"), "export const a = 1\n")
|
|
44
|
+
|
|
45
|
+
expect(configFingerprint([join(root, "a\x1b.ts")], root)).toEqual({})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("names a file only one side has as changed", () => {
|
|
49
|
+
expect(changedConfigFiles({ a: "1", b: "2" }, { a: "1", c: "3" })).toEqual(["b", "c"])
|
|
50
|
+
expect(changedConfigFiles({ a: "1" }, { a: "1" })).toEqual([])
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs"
|
|
3
|
+
import { relative, sep } from "node:path"
|
|
4
|
+
|
|
5
|
+
/** Path (relative to `root`) → content hash of every file a config load touched. */
|
|
6
|
+
export type ConfigFingerprint = Readonly<Record<string, string>>
|
|
7
|
+
|
|
8
|
+
const HASH_LENGTH = 16
|
|
9
|
+
|
|
10
|
+
// A path the trailer cannot carry printably is never recorded, so it can never
|
|
11
|
+
// reach a terminal from history.
|
|
12
|
+
// eslint-disable-next-line no-control-regex -- the control range IS the point
|
|
13
|
+
const CONTROL_CHARACTER = /[\x00-\x1f\x7f-\x9f]/
|
|
14
|
+
|
|
15
|
+
export const contentHash = (content: string | Buffer): string =>
|
|
16
|
+
createHash("sha256").update(content).digest("hex").slice(0, HASH_LENGTH)
|
|
17
|
+
|
|
18
|
+
const realOr = (path: string): string => {
|
|
19
|
+
try {
|
|
20
|
+
return realpathSync(path)
|
|
21
|
+
} catch {
|
|
22
|
+
return path
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// An unreadable file is left out rather than failing: the fingerprint only
|
|
27
|
+
// feeds a warning, and the load that listed the file already read it.
|
|
28
|
+
// Paths are real paths: a loader may report `/private/var/…` for a root
|
|
29
|
+
// reached through the `/var` symlink, and the two must still relate.
|
|
30
|
+
export const configFingerprint = (files: readonly string[], root: string): ConfigFingerprint => {
|
|
31
|
+
const fingerprint: Record<string, string> = {}
|
|
32
|
+
const realRoot = realOr(root)
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const path = relative(realRoot, realOr(file)).split(sep).join("/")
|
|
35
|
+
if (CONTROL_CHARACTER.test(path)) continue
|
|
36
|
+
let content: Buffer
|
|
37
|
+
try {
|
|
38
|
+
content = readFileSync(file)
|
|
39
|
+
} catch {
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
fingerprint[path] = contentHash(content)
|
|
43
|
+
}
|
|
44
|
+
return fingerprint
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const changedConfigFiles = (
|
|
48
|
+
recorded: ConfigFingerprint,
|
|
49
|
+
current: ConfigFingerprint,
|
|
50
|
+
): readonly string[] =>
|
|
51
|
+
[...new Set([...Object.keys(recorded), ...Object.keys(current)])]
|
|
52
|
+
.filter((path) => recorded[path] !== current[path])
|
|
53
|
+
.sort()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export {
|
|
2
|
+
load,
|
|
3
|
+
refuseLegacyRc,
|
|
4
|
+
ConfigService,
|
|
5
|
+
type ConfigOperations,
|
|
6
|
+
type ResolvedWorkflow,
|
|
7
|
+
} from "./load.js"
|
|
8
|
+
export {
|
|
9
|
+
ConfigDiscovery,
|
|
10
|
+
LEGACY_RC_FILES,
|
|
11
|
+
legacyRcDirs,
|
|
12
|
+
WORKFLOW_MODULE,
|
|
13
|
+
walkUp,
|
|
14
|
+
} from "./discovery.js"
|
|
15
|
+
export { formatDiagnostic } from "./Diagnostic.js"
|
|
16
|
+
export { resolveVars, multilineSetting, isSettingName, SETTING_NAME_RULE } from "./vars.js"
|
|
17
|
+
export { resolveScopeAccess, resolveScopeSkills } from "./skills.js"
|
|
18
|
+
export { seededValidateCommand, isSeededValidateCommand } from "./SteeringFormats.js"
|
|
19
|
+
export {
|
|
20
|
+
type ChangeStatus,
|
|
21
|
+
type PendingChange,
|
|
22
|
+
type StepDef,
|
|
23
|
+
type WorkflowDefinition,
|
|
24
|
+
knownModes,
|
|
25
|
+
type StateMode,
|
|
26
|
+
type StateName,
|
|
27
|
+
} from "./Workflow.js"
|
|
28
|
+
export {
|
|
29
|
+
changedConfigFiles,
|
|
30
|
+
configFingerprint,
|
|
31
|
+
contentHash,
|
|
32
|
+
type ConfigFingerprint,
|
|
33
|
+
} from "./fingerprint.js"
|