@pmelab/gtd 62.1.0 → 64.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.
Files changed (68) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +8 -3
  3. package/dist/chunks/{flows-DENt0uWV.mjs → flows-B7ZRCe-B.mjs} +1 -1
  4. package/dist/chunks/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -1
  5. package/dist/chunks/{steering-DF_BMadH.mjs → steering-DkA51qDa.mjs} +1 -1
  6. package/dist/chunks/{testkit-CPzWnl3P.mjs → testkit-zGjNeQNL.mjs} +1144 -345
  7. package/dist/flows.mjs +1 -1
  8. package/dist/gtd.bundle.mjs +4023 -1326
  9. package/dist/gtd.core.mjs +120 -119
  10. package/dist/steering.mjs +1 -1
  11. package/dist/testkit/gherkin.mjs +2 -2
  12. package/dist/testkit.mjs +2 -2
  13. package/dist/workflow-kit.mjs +2 -2
  14. package/package.json +1 -1
  15. package/src/cli/Cli.test.ts +5 -8
  16. package/src/cli/Cli.ts +10 -28
  17. package/src/cli/Output.ts +2 -1
  18. package/src/cli/Program.test.ts +55 -53
  19. package/src/cli/Program.ts +36 -45
  20. package/src/cli/ToolCommands.ts +3 -4
  21. package/src/edge/Edge.test.ts +38 -12
  22. package/src/edge/Edge.ts +98 -13
  23. package/src/edge/index.ts +1 -0
  24. package/src/emit/SteeringMode.test.ts +4 -2
  25. package/src/emit/SteeringMode.ts +10 -9
  26. package/src/gherkin/messages.test.ts +1 -1
  27. package/src/gherkin/module-config.feature +1 -1
  28. package/src/gherkin/testkit.feature +2 -2
  29. package/src/install/Version.ts +14 -13
  30. package/src/platform/Host.ts +75 -22
  31. package/src/platform/NodePinFile.ts +44 -0
  32. package/src/platform/NodeRuntime.ts +19 -0
  33. package/src/platform/NodeSync.ts +64 -0
  34. package/src/platform/PinFile.ts +0 -50
  35. package/src/platform/Sha.ts +121 -0
  36. package/src/platform/SyncIo.ts +35 -0
  37. package/src/platform/Workspace.test.ts +8 -3
  38. package/src/platform/Workspace.ts +82 -123
  39. package/src/platform/WorktreeState.test.ts +8 -4
  40. package/src/platform/WorktreeState.ts +21 -19
  41. package/src/platform/index.ts +18 -5
  42. package/src/replay/Format.test.ts +32 -10
  43. package/src/replay/Format.ts +5 -2
  44. package/src/replay/Replay.test.ts +110 -3
  45. package/src/replay/Replay.ts +120 -28
  46. package/src/replay/Trailers.test.ts +10 -10
  47. package/src/replay/Trailers.ts +32 -1
  48. package/src/replay/index.ts +2 -0
  49. package/src/step/planStart.test.ts +2 -2
  50. package/src/step/planStep.test.ts +4 -4
  51. package/src/testkit/TestRepo.test.ts +1 -1
  52. package/src/testkit/TestRepo.ts +4 -18
  53. package/src/twins/Host.test.ts +151 -0
  54. package/src/twins/Layers.test.ts +2 -2
  55. package/src/twins/Layers.ts +44 -18
  56. package/src/twins/Path.test.ts +52 -0
  57. package/src/twins/SyncIo.test.ts +153 -0
  58. package/src/twins/SyncIo.ts +159 -0
  59. package/src/twins/cliIo.ts +8 -1
  60. package/src/twins/index.ts +5 -0
  61. package/src/workflow/discovery.test.ts +6 -5
  62. package/src/workflow/discovery.ts +43 -32
  63. package/src/workflow/fingerprint.test.ts +23 -3
  64. package/src/workflow/fingerprint.ts +9 -11
  65. package/src/workflow/load.test.ts +16 -6
  66. package/src/workflow/load.ts +107 -69
  67. package/src/workflow/pin.test.ts +3 -2
  68. package/src/workflow/pin.ts +17 -11
@@ -0,0 +1,64 @@
1
+ import { execFileSync } from "node:child_process"
2
+ import { createHash } from "node:crypto"
3
+ import {
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readlinkSync,
8
+ realpathSync,
9
+ renameSync,
10
+ statSync,
11
+ writeFileSync,
12
+ type Stats,
13
+ } from "node:fs"
14
+ import type { FileInfo, SyncIo } from "./SyncIo.js"
15
+
16
+ // A tree listing, or one committed file, can overflow execFileSync's 1 MB default.
17
+ const MAX_BUFFER = 256 * 1024 * 1024
18
+
19
+ const isAbsent = (e: unknown): boolean => {
20
+ const code = (e as NodeJS.ErrnoException).code
21
+ return code === "ENOENT" || code === "ENOTDIR"
22
+ }
23
+
24
+ const orAbsent = <A>(read: () => A): A | undefined => {
25
+ try {
26
+ return read()
27
+ } catch (e) {
28
+ if (isAbsent(e)) return undefined
29
+ throw e
30
+ }
31
+ }
32
+
33
+ const infoOf = (stats: Stats): FileInfo => ({
34
+ type: stats.isSymbolicLink()
35
+ ? "symlink"
36
+ : stats.isDirectory()
37
+ ? "directory"
38
+ : stats.isFile()
39
+ ? "file"
40
+ : "other",
41
+ uid: process.platform === "win32" ? undefined : stats.uid,
42
+ })
43
+
44
+ export const nodeSyncIo: SyncIo = {
45
+ readFile: (path) => orAbsent(() => readFileSync(path)),
46
+ readText: (path) => orAbsent(() => readFileSync(path, "utf8")),
47
+ writeText: (path, content) => writeFileSync(path, content, "utf8"),
48
+ makeDirectory: (path) => {
49
+ mkdirSync(path, { recursive: true })
50
+ },
51
+ rename: renameSync,
52
+ stat: (path) => orAbsent(() => infoOf(statSync(path))),
53
+ lstat: (path) => orAbsent(() => infoOf(lstatSync(path))),
54
+ readLink: (path) => readlinkSync(path, "utf8"),
55
+ realPath: (path) => realpathSync(path),
56
+ git: (cwd, args) => execFileSync("git", args, { cwd, encoding: "utf8", maxBuffer: MAX_BUFFER }),
57
+ gitBytes: (cwd, args) =>
58
+ execFileSync("git", args, { cwd, maxBuffer: MAX_BUFFER, stdio: ["ignore", "pipe", "ignore"] }),
59
+ hash: (algorithm, ...parts) => {
60
+ const hash = createHash(algorithm)
61
+ for (const part of parts) hash.update(part)
62
+ return hash.digest("hex")
63
+ },
64
+ }
@@ -1,9 +1,3 @@
1
- import { execFileSync } from "node:child_process"
2
- import { existsSync, readFileSync } from "node:fs"
3
- import { registerHooks } from "node:module"
4
- import { join } from "node:path"
5
- import { pathToFileURL } from "node:url"
6
-
7
1
  /** A process's pin as the launcher found it: the file's bytes and where its engine runs from. */
8
2
  export interface FoundPin {
9
3
  readonly path: string
@@ -15,47 +9,3 @@ export interface FoundPin {
15
9
  * the landing after an unpin beat deleted the file still runs on the pin.
16
10
  */
17
11
  export type PinFile = () => FoundPin | undefined
18
-
19
- // `cwd`, as the engine's own root is: gtd runs only at the worktree root, and
20
- // a relocated `$GIT_DIR` leaves no `.git` to walk up to. A file that cannot be
21
- // read counts as no pin, so the launcher refuses it rather than crashing.
22
- export const livePinFile =
23
- (cwd: string, path: string): PinFile =>
24
- () => {
25
- const file = join(cwd, path)
26
- if (existsSync(file)) {
27
- try {
28
- return { path: file, bytes: readFileSync(file) }
29
- } catch {
30
- return { path: file, bytes: new Uint8Array() }
31
- }
32
- }
33
- try {
34
- const bytes = execFileSync("git", ["show", `HEAD:${path}`], {
35
- cwd,
36
- stdio: ["ignore", "pipe", "ignore"],
37
- })
38
- return { path: file, bytes }
39
- } catch {
40
- return undefined
41
- }
42
- }
43
-
44
- /**
45
- * Runs a pin's module as this process's main program. Node refuses an unknown
46
- * extension, so a load hook serves the unpacked source under the pin's own
47
- * URL — `import.meta.url` stays a file in the repository.
48
- */
49
- export const runPinModule = async (path: string, source: string): Promise<void> => {
50
- const url = pathToFileURL(path).href
51
- // A pin read from HEAD has no file behind its URL, which Node's own resolve refuses.
52
- registerHooks({
53
- resolve: (specifier, context, nextResolve) =>
54
- specifier === url ? { url, shortCircuit: true } : nextResolve(specifier, context),
55
- load: (requested, context, nextLoad) =>
56
- requested === url
57
- ? { format: "module", source, shortCircuit: true }
58
- : nextLoad(requested, context),
59
- })
60
- await import(url)
61
- }
@@ -0,0 +1,121 @@
1
+ import type { HashAlgorithm } from "./SyncIo.js"
2
+
3
+ // The digest for a runtime without Node's crypto, which the in-memory tier runs on.
4
+
5
+ const K256 = Uint32Array.from([
6
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
7
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
8
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
9
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
10
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
11
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
12
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
13
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
14
+ ])
15
+
16
+ const rotl = (x: number, n: number): number => (x << n) | (x >>> (32 - n))
17
+ const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n))
18
+
19
+ /** `data` padded to whole 64-byte blocks, its bit length big-endian at the end. */
20
+ const padded = (data: Uint8Array): DataView => {
21
+ const length = Math.ceil((data.length + 9) / 64) * 64
22
+ const bytes = new Uint8Array(length)
23
+ bytes.set(data)
24
+ bytes[data.length] = 0x80
25
+ const view = new DataView(bytes.buffer)
26
+ view.setUint32(length - 8, Math.floor(data.length / 0x20000000))
27
+ view.setUint32(length - 4, (data.length * 8) >>> 0)
28
+ return view
29
+ }
30
+
31
+ /** Round `i`'s boolean function of `b`, `c`, `d`, plus its constant. */
32
+ const sha1Mix = (i: number, b: number, c: number, d: number): number =>
33
+ i < 20
34
+ ? ((b & c) | (~b & d)) + 0x5a827999
35
+ : i < 40
36
+ ? (b ^ c ^ d) + 0x6ed9eba1
37
+ : i < 60
38
+ ? ((b & c) | (b & d) | (c & d)) + 0x8f1bbcdc
39
+ : (b ^ c ^ d) + 0xca62c1d6
40
+
41
+ const sha1 = (view: DataView): Uint32Array => {
42
+ const h = Uint32Array.from([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0])
43
+ const w = new Uint32Array(80)
44
+ for (let block = 0; block < view.byteLength; block += 64) {
45
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(block + i * 4)
46
+ for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3]! ^ w[i - 8]! ^ w[i - 14]! ^ w[i - 16]!, 1)
47
+ let [a, b, c, d, e] = [h[0]!, h[1]!, h[2]!, h[3]!, h[4]!]
48
+ for (let i = 0; i < 80; i++) {
49
+ const t = (rotl(a, 5) + sha1Mix(i, b, c, d) + e + w[i]!) | 0
50
+ e = d
51
+ d = c
52
+ c = rotl(b, 30)
53
+ b = a
54
+ a = t
55
+ }
56
+ h[0]! += a
57
+ h[1]! += b
58
+ h[2]! += c
59
+ h[3]! += d
60
+ h[4]! += e
61
+ }
62
+ return h
63
+ }
64
+
65
+ const sha256 = (view: DataView): Uint32Array => {
66
+ const h = Uint32Array.from([
67
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
68
+ ])
69
+ const w = new Uint32Array(64)
70
+ for (let block = 0; block < view.byteLength; block += 64) {
71
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(block + i * 4)
72
+ for (let i = 16; i < 64; i++) {
73
+ const x = w[i - 15]!
74
+ const y = w[i - 2]!
75
+ const s0 = rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)
76
+ const s1 = rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10)
77
+ w[i] = w[i - 16]! + s0 + w[i - 7]! + s1
78
+ }
79
+ let [a, b, c, d, e, f, g, hh] = [h[0]!, h[1]!, h[2]!, h[3]!, h[4]!, h[5]!, h[6]!, h[7]!]
80
+ for (let i = 0; i < 64; i++) {
81
+ const t1 =
82
+ (hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K256[i]! + w[i]!) |
83
+ 0
84
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) | 0
85
+ hh = g
86
+ g = f
87
+ f = e
88
+ e = (d + t1) | 0
89
+ d = c
90
+ c = b
91
+ b = a
92
+ a = (t1 + t2) | 0
93
+ }
94
+ h[0]! += a
95
+ h[1]! += b
96
+ h[2]! += c
97
+ h[3]! += d
98
+ h[4]! += e
99
+ h[5]! += f
100
+ h[6]! += g
101
+ h[7]! += hh
102
+ }
103
+ return h
104
+ }
105
+
106
+ const encoder = new TextEncoder()
107
+
108
+ export const pureHash = (
109
+ algorithm: HashAlgorithm,
110
+ ...parts: readonly (string | Uint8Array)[]
111
+ ): string => {
112
+ const chunks = parts.map((part) => (typeof part === "string" ? encoder.encode(part) : part))
113
+ const data = new Uint8Array(chunks.reduce((n, chunk) => n + chunk.length, 0))
114
+ let at = 0
115
+ for (const chunk of chunks) {
116
+ data.set(chunk, at)
117
+ at += chunk.length
118
+ }
119
+ const words = (algorithm === "sha1" ? sha1 : sha256)(padded(data))
120
+ return [...words].map((word) => word.toString(16).padStart(8, "0")).join("")
121
+ }
@@ -0,0 +1,35 @@
1
+ export type HashAlgorithm = "sha1" | "sha256"
2
+
3
+ export interface FileInfo {
4
+ readonly type: "file" | "directory" | "symlink" | "other"
5
+ /** `undefined` where the platform has no POSIX user ids. */
6
+ readonly uid: number | undefined
7
+ }
8
+
9
+ /**
10
+ * The machine reads that must stay synchronous: workflows read the tree
11
+ * between two awaits during replay, so neither the reads nor the hashing
12
+ * behind them can wait on IO. Absence is a value (`undefined`); any other
13
+ * fault throws.
14
+ */
15
+ export interface SyncIo {
16
+ readonly readFile: (path: string) => Uint8Array | undefined
17
+ readonly readText: (path: string) => string | undefined
18
+ readonly writeText: (path: string, content: string) => void
19
+ /** Creates every missing parent too. */
20
+ readonly makeDirectory: (path: string) => void
21
+ readonly rename: (from: string, to: string) => void
22
+ /** Follows a symlink. */
23
+ readonly stat: (path: string) => FileInfo | undefined
24
+ /** Reports a symlink as one. */
25
+ readonly lstat: (path: string) => FileInfo | undefined
26
+ readonly readLink: (path: string) => string
27
+ /** Throws when `path` does not exist. */
28
+ readonly realPath: (path: string) => string
29
+ /** git's stdout; throws on a non-zero exit, with git's stderr passed through. */
30
+ readonly git: (cwd: string, args: readonly string[]) => string
31
+ /** `git`'s raw stdout, with its stderr discarded. */
32
+ readonly gitBytes: (cwd: string, args: readonly string[]) => Uint8Array
33
+ /** Lowercase hex digest of the parts, concatenated; a string part is UTF-8. */
34
+ readonly hash: (algorithm: HashAlgorithm, ...parts: readonly (string | Uint8Array)[]) => string
35
+ }
@@ -54,7 +54,9 @@ const makeLiveTier = (): WorkspaceTier => {
54
54
 
55
55
  const hostLayer = Host.layer({ root, home: root, env: {} })
56
56
  const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeServices.layer)))
57
- const workspaceLayer = Workspace.Live.pipe(Layer.provide(Layer.merge(hostLayer, gitLayer)))
57
+ const workspaceLayer = Workspace.Live.pipe(
58
+ Layer.provide(Layer.mergeAll(hostLayer, gitLayer, NodeServices.layer)),
59
+ )
58
60
 
59
61
  return {
60
62
  root,
@@ -270,8 +272,9 @@ describe("Workspace [Live] worktreeSync", () => {
270
272
  Effect.provide(
271
273
  Workspace.Live.pipe(
272
274
  Layer.provide(
273
- Layer.merge(
275
+ Layer.mergeAll(
274
276
  Host.layer({ root, home: root, env: {} }),
277
+ NodeServices.layer,
275
278
  GitService.Live.pipe(
276
279
  Layer.provide(
277
280
  Layer.merge(Host.layer({ root, home: root, env: {} }), NodeServices.layer),
@@ -361,7 +364,9 @@ const makeLiveEpisodeTier = () => {
361
364
 
362
365
  const hostLayer = Host.layer({ root, home: root, env: {} })
363
366
  const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeServices.layer)))
364
- const workspaceLayer = Workspace.Live.pipe(Layer.provide(Layer.merge(hostLayer, gitLayer)))
367
+ const workspaceLayer = Workspace.Live.pipe(
368
+ Layer.provide(Layer.mergeAll(hostLayer, gitLayer, NodeServices.layer)),
369
+ )
365
370
  const ops = (): Promise<WorkspaceOps> =>
366
371
  Effect.runPromise(
367
372
  Effect.gen(function* () {