@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -3
- package/dist/chunks/{flows-DENt0uWV.mjs → flows-B7ZRCe-B.mjs} +1 -1
- package/dist/chunks/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -1
- package/dist/chunks/{steering-DF_BMadH.mjs → steering-DkA51qDa.mjs} +1 -1
- package/dist/chunks/{testkit-CPzWnl3P.mjs → testkit-zGjNeQNL.mjs} +1144 -345
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +4023 -1326
- package/dist/gtd.core.mjs +120 -119
- package/dist/steering.mjs +1 -1
- package/dist/testkit/gherkin.mjs +2 -2
- package/dist/testkit.mjs +2 -2
- package/dist/workflow-kit.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +5 -8
- package/src/cli/Cli.ts +10 -28
- package/src/cli/Output.ts +2 -1
- package/src/cli/Program.test.ts +55 -53
- package/src/cli/Program.ts +36 -45
- package/src/cli/ToolCommands.ts +3 -4
- package/src/edge/Edge.test.ts +38 -12
- package/src/edge/Edge.ts +98 -13
- package/src/edge/index.ts +1 -0
- package/src/emit/SteeringMode.test.ts +4 -2
- package/src/emit/SteeringMode.ts +10 -9
- package/src/gherkin/messages.test.ts +1 -1
- package/src/gherkin/module-config.feature +1 -1
- package/src/gherkin/testkit.feature +2 -2
- package/src/install/Version.ts +14 -13
- package/src/platform/Host.ts +75 -22
- package/src/platform/NodePinFile.ts +44 -0
- package/src/platform/NodeRuntime.ts +19 -0
- package/src/platform/NodeSync.ts +64 -0
- package/src/platform/PinFile.ts +0 -50
- package/src/platform/Sha.ts +121 -0
- package/src/platform/SyncIo.ts +35 -0
- package/src/platform/Workspace.test.ts +8 -3
- package/src/platform/Workspace.ts +82 -123
- package/src/platform/WorktreeState.test.ts +8 -4
- package/src/platform/WorktreeState.ts +21 -19
- package/src/platform/index.ts +18 -5
- package/src/replay/Format.test.ts +32 -10
- package/src/replay/Format.ts +5 -2
- package/src/replay/Replay.test.ts +110 -3
- package/src/replay/Replay.ts +120 -28
- package/src/replay/Trailers.test.ts +10 -10
- package/src/replay/Trailers.ts +32 -1
- package/src/replay/index.ts +2 -0
- package/src/step/planStart.test.ts +2 -2
- package/src/step/planStep.test.ts +4 -4
- package/src/testkit/TestRepo.test.ts +1 -1
- package/src/testkit/TestRepo.ts +4 -18
- package/src/twins/Host.test.ts +151 -0
- package/src/twins/Layers.test.ts +2 -2
- package/src/twins/Layers.ts +44 -18
- package/src/twins/Path.test.ts +52 -0
- package/src/twins/SyncIo.test.ts +153 -0
- package/src/twins/SyncIo.ts +159 -0
- package/src/twins/cliIo.ts +8 -1
- package/src/twins/index.ts +5 -0
- package/src/workflow/discovery.test.ts +6 -5
- package/src/workflow/discovery.ts +43 -32
- package/src/workflow/fingerprint.test.ts +23 -3
- package/src/workflow/fingerprint.ts +9 -11
- package/src/workflow/load.test.ts +16 -6
- package/src/workflow/load.ts +107 -69
- package/src/workflow/pin.test.ts +3 -2
- 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
|
+
}
|
package/src/platform/PinFile.ts
CHANGED
|
@@ -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(
|
|
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.
|
|
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(
|
|
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* () {
|