@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
package/src/twins/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export { InMemRepo } from "./InMemRepo.js"
|
|
2
2
|
export {
|
|
3
|
+
inMemPath,
|
|
4
|
+
inMemoryHost,
|
|
5
|
+
IN_MEMORY_PID,
|
|
6
|
+
IN_MEMORY_SCRATCH,
|
|
3
7
|
makeInMemoryWorkspaceOps,
|
|
4
8
|
testLayers,
|
|
5
9
|
type CommandTable,
|
|
@@ -13,3 +17,4 @@ export { strictGitOperations } from "./StrictGitOperations.js"
|
|
|
13
17
|
export { fakeGitOperations } from "./FakeGitOperations.js"
|
|
14
18
|
export { takePinBeat } from "./PinBeats.js"
|
|
15
19
|
export { inMemPinFile } from "./PinFile.js"
|
|
20
|
+
export { inMemorySyncIo } from "./SyncIo.js"
|
|
@@ -2,7 +2,7 @@ import { execSync } from "node:child_process"
|
|
|
2
2
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
|
-
import { Effect, Layer } from "effect"
|
|
5
|
+
import { Effect, Layer, type Path } from "effect"
|
|
6
6
|
import { afterEach, describe, expect, it } from "vitest"
|
|
7
7
|
import { GitService, Host, Narrator, Workspace, NodeServices } from "../platform/index.js"
|
|
8
8
|
import { InMemRepo, testLayers } from "../twins/index.js"
|
|
@@ -34,7 +34,7 @@ const config = (imports: string): string =>
|
|
|
34
34
|
].join("\n")
|
|
35
35
|
|
|
36
36
|
const loadWith = (
|
|
37
|
-
layer: Layer.Layer<ConfigService | Narrator | Workspace | Host | ConfigDiscovery>,
|
|
37
|
+
layer: Layer.Layer<ConfigService | Narrator | Workspace | Host | Path.Path | ConfigDiscovery>,
|
|
38
38
|
) =>
|
|
39
39
|
Effect.runPromise(
|
|
40
40
|
Effect.flatMap(ConfigService, (service) => service.load).pipe(
|
|
@@ -63,15 +63,16 @@ const live = (): Tier => {
|
|
|
63
63
|
}
|
|
64
64
|
const host = Host.layer({ root, home: root, env: {} })
|
|
65
65
|
const git = GitService.Live.pipe(Layer.provide(Layer.merge(host, NodeServices.layer)))
|
|
66
|
-
const workspace = Workspace.Live.pipe(
|
|
66
|
+
const workspace = Workspace.Live.pipe(
|
|
67
|
+
Layer.provide(Layer.mergeAll(host, git, NodeServices.layer)),
|
|
68
|
+
)
|
|
67
69
|
return loadWith(
|
|
68
70
|
Layer.mergeAll(
|
|
69
71
|
ConfigService.Live,
|
|
70
72
|
ConfigDiscovery.Live,
|
|
71
|
-
host,
|
|
72
73
|
workspace,
|
|
73
74
|
Narrator.layer(() => {}, false),
|
|
74
|
-
),
|
|
75
|
+
).pipe(Layer.provideMerge(host), Layer.provideMerge(NodeServices.layer)),
|
|
75
76
|
)
|
|
76
77
|
},
|
|
77
78
|
dispose: () => rmSync(root, { recursive: true, force: true }),
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Context, Effect, Layer } from "effect"
|
|
1
|
+
import { Context, Effect, Layer, Path } from "effect"
|
|
2
|
+
import { Host, nodeSyncIo, type SyncIo } from "../platform/index.js"
|
|
4
3
|
|
|
5
4
|
/** The workflow module found on the cwd→home walk, with its source. */
|
|
6
5
|
export interface WorkflowModule {
|
|
@@ -50,17 +49,21 @@ export const LEGACY_RC_FILES = [
|
|
|
50
49
|
]
|
|
51
50
|
|
|
52
51
|
/** The directories searched for a leftover rc file: the walk, plus `home` when the walk misses it. */
|
|
53
|
-
export const legacyRcDirs = (root: string, home: string): readonly string[] => {
|
|
54
|
-
const chain = walkUp(root, home)
|
|
52
|
+
export const legacyRcDirs = (path: Path.Path, root: string, home: string): readonly string[] => {
|
|
53
|
+
const chain = walkUp(path, root, home)
|
|
55
54
|
return chain.includes(home) ? chain : [...chain, home]
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
const legacyRcFileAt = (
|
|
57
|
+
const legacyRcFileAt = (
|
|
58
|
+
io: SyncIo,
|
|
59
|
+
path: string,
|
|
60
|
+
uid: number | undefined,
|
|
61
|
+
): LegacyRcFile | undefined => {
|
|
59
62
|
try {
|
|
60
|
-
const stat =
|
|
61
|
-
if (
|
|
63
|
+
const stat = io.stat(path)
|
|
64
|
+
if (stat?.type !== "file") return undefined
|
|
62
65
|
// Above home, a shared directory (`/tmp`) can hold another user's file.
|
|
63
|
-
return { path, owned:
|
|
66
|
+
return { path, owned: uid === undefined || stat.uid === uid }
|
|
64
67
|
} catch {
|
|
65
68
|
return undefined
|
|
66
69
|
}
|
|
@@ -74,40 +77,43 @@ const legacyRcFileAt = (path: string): LegacyRcFile | undefined => {
|
|
|
74
77
|
* shared implementation, not a second copy in `src/twins/Layers.ts` that
|
|
75
78
|
* could silently diverge.
|
|
76
79
|
*/
|
|
77
|
-
export const walkUp = (from: string, home: string): ReadonlyArray<string> => {
|
|
80
|
+
export const walkUp = (path: Path.Path, from: string, home: string): ReadonlyArray<string> => {
|
|
78
81
|
const chain: Array<string> = []
|
|
79
82
|
let dir = from
|
|
80
83
|
while (true) {
|
|
81
84
|
chain.push(dir)
|
|
82
85
|
if (dir === home) break
|
|
83
|
-
const parent = dirname(dir)
|
|
86
|
+
const parent = path.dirname(dir)
|
|
84
87
|
if (parent === dir) break // filesystem root
|
|
85
88
|
dir = parent
|
|
86
89
|
}
|
|
87
90
|
return chain
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
const liveDiscovery = (
|
|
94
|
+
io: SyncIo,
|
|
95
|
+
path: Path.Path,
|
|
96
|
+
uid: number | undefined,
|
|
97
|
+
): ConfigDiscoveryOps => ({
|
|
98
|
+
legacyRcFiles: (root, home) =>
|
|
99
|
+
Effect.sync(() =>
|
|
100
|
+
legacyRcDirs(path, root, home).flatMap((dir) =>
|
|
101
|
+
LEGACY_RC_FILES.flatMap((name) => legacyRcFileAt(io, path.join(dir, name), uid) ?? []),
|
|
102
|
+
),
|
|
94
103
|
),
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
110
|
-
})
|
|
104
|
+
workflowModule: (root, home) =>
|
|
105
|
+
Effect.try({
|
|
106
|
+
try: () => {
|
|
107
|
+
for (const dir of walkUp(path, root, home)) {
|
|
108
|
+
const filepath = path.join(dir, WORKFLOW_MODULE)
|
|
109
|
+
const source = io.readText(filepath)
|
|
110
|
+
if (source !== undefined) return { filepath, source }
|
|
111
|
+
}
|
|
112
|
+
return undefined
|
|
113
|
+
},
|
|
114
|
+
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
115
|
+
}),
|
|
116
|
+
})
|
|
111
117
|
|
|
112
118
|
/**
|
|
113
119
|
* The discovery seam — a `Context.Service`, not part of `WorkflowFiles` (that
|
|
@@ -122,5 +128,10 @@ const workflowModule = (
|
|
|
122
128
|
export class ConfigDiscovery extends Context.Service<ConfigDiscovery, ConfigDiscoveryOps>()(
|
|
123
129
|
"ConfigDiscovery",
|
|
124
130
|
) {
|
|
125
|
-
static Live = Layer.
|
|
131
|
+
static Live = Layer.effect(
|
|
132
|
+
ConfigDiscovery,
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
return liveDiscovery(nodeSyncIo, yield* Path.Path, (yield* Host).uid)
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
126
137
|
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
import { tmpdir } from "node:os"
|
|
10
10
|
import { join } from "node:path"
|
|
11
11
|
import { describe, expect, it } from "vitest"
|
|
12
|
+
import { nodeSyncIo } from "../platform/index.js"
|
|
13
|
+
import { inMemPath } from "../twins/index.js"
|
|
12
14
|
import { changedConfigFiles, configFingerprint } from "./fingerprint.js"
|
|
13
15
|
|
|
14
16
|
const read = (path: string): string | undefined => {
|
|
@@ -27,6 +29,8 @@ describe("the config fingerprint", () => {
|
|
|
27
29
|
writeFileSync(join(root, "lib", "flow.ts"), "export const a = 1\n")
|
|
28
30
|
|
|
29
31
|
const fingerprint = configFingerprint(
|
|
32
|
+
nodeSyncIo,
|
|
33
|
+
inMemPath,
|
|
30
34
|
[join(root, "gtd.config.ts"), join(root, "lib", "flow.ts"), join(root, "gone.ts")],
|
|
31
35
|
root,
|
|
32
36
|
read,
|
|
@@ -38,7 +42,13 @@ describe("the config fingerprint", () => {
|
|
|
38
42
|
expect(
|
|
39
43
|
changedConfigFiles(
|
|
40
44
|
fingerprint,
|
|
41
|
-
configFingerprint(
|
|
45
|
+
configFingerprint(
|
|
46
|
+
nodeSyncIo,
|
|
47
|
+
inMemPath,
|
|
48
|
+
[join(root, "gtd.config.ts"), join(root, "lib", "flow.ts")],
|
|
49
|
+
root,
|
|
50
|
+
read,
|
|
51
|
+
),
|
|
42
52
|
),
|
|
43
53
|
).toEqual(["lib/flow.ts"])
|
|
44
54
|
})
|
|
@@ -50,7 +60,15 @@ describe("the config fingerprint", () => {
|
|
|
50
60
|
symlinkSync(real, link)
|
|
51
61
|
|
|
52
62
|
expect(
|
|
53
|
-
Object.keys(
|
|
63
|
+
Object.keys(
|
|
64
|
+
configFingerprint(
|
|
65
|
+
nodeSyncIo,
|
|
66
|
+
inMemPath,
|
|
67
|
+
[join(realpathSync(real), "gtd.config.ts")],
|
|
68
|
+
link,
|
|
69
|
+
read,
|
|
70
|
+
),
|
|
71
|
+
),
|
|
54
72
|
).toEqual(["gtd.config.ts"])
|
|
55
73
|
})
|
|
56
74
|
|
|
@@ -58,7 +76,9 @@ describe("the config fingerprint", () => {
|
|
|
58
76
|
const root = mkdtempSync(join(tmpdir(), "gtd-fingerprint-"))
|
|
59
77
|
writeFileSync(join(root, "a\x1b.ts"), "export const a = 1\n")
|
|
60
78
|
|
|
61
|
-
expect(configFingerprint([join(root, "a\x1b.ts")], root, read)).toEqual(
|
|
79
|
+
expect(configFingerprint(nodeSyncIo, inMemPath, [join(root, "a\x1b.ts")], root, read)).toEqual(
|
|
80
|
+
{},
|
|
81
|
+
)
|
|
62
82
|
})
|
|
63
83
|
|
|
64
84
|
it("names a file only one side has as changed", () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { relative, sep } from "node:path"
|
|
1
|
+
import type { Path } from "effect"
|
|
2
|
+
import type { SyncIo } from "../platform/index.js"
|
|
4
3
|
|
|
5
4
|
/** Path (relative to `root`) → content hash of every file a config load touched. */
|
|
6
5
|
export type ConfigFingerprint = Readonly<Record<string, string>>
|
|
@@ -12,12 +11,9 @@ const HASH_LENGTH = 16
|
|
|
12
11
|
// eslint-disable-next-line no-control-regex -- the control range IS the point
|
|
13
12
|
const CONTROL_CHARACTER = /[\x00-\x1f\x7f-\x9f]/
|
|
14
13
|
|
|
15
|
-
const
|
|
16
|
-
createHash("sha256").update(content).digest("hex").slice(0, HASH_LENGTH)
|
|
17
|
-
|
|
18
|
-
const realOr = (path: string): string => {
|
|
14
|
+
const realOr = (io: SyncIo, path: string): string => {
|
|
19
15
|
try {
|
|
20
|
-
return
|
|
16
|
+
return io.realPath(path)
|
|
21
17
|
} catch {
|
|
22
18
|
return path
|
|
23
19
|
}
|
|
@@ -28,17 +24,19 @@ const realOr = (path: string): string => {
|
|
|
28
24
|
// Paths are real paths: a loader may report `/private/var/…` for a root
|
|
29
25
|
// reached through the `/var` symlink, and the two must still relate.
|
|
30
26
|
export const configFingerprint = (
|
|
27
|
+
io: SyncIo,
|
|
28
|
+
paths: Path.Path,
|
|
31
29
|
files: readonly string[],
|
|
32
30
|
root: string,
|
|
33
31
|
read: (path: string) => string | undefined,
|
|
34
32
|
): ConfigFingerprint => {
|
|
35
33
|
const fingerprint: Record<string, string> = {}
|
|
36
|
-
const realRoot = realOr(root)
|
|
34
|
+
const realRoot = realOr(io, root)
|
|
37
35
|
for (const file of files) {
|
|
38
|
-
const path = relative(realRoot, realOr(file)).split(sep).join("/")
|
|
36
|
+
const path = paths.relative(realRoot, realOr(io, file)).split(paths.sep).join("/")
|
|
39
37
|
if (CONTROL_CHARACTER.test(path)) continue
|
|
40
38
|
const content = read(file)
|
|
41
|
-
if (content !== undefined) fingerprint[path] =
|
|
39
|
+
if (content !== undefined) fingerprint[path] = io.hash("sha256", content).slice(0, HASH_LENGTH)
|
|
42
40
|
}
|
|
43
41
|
return fingerprint
|
|
44
42
|
}
|
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, mkdtempSync, rmSync, writeFileSync, mkdirSync
|
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import { tmpdir } from "node:os"
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
6
|
-
import { Cause, Effect, Exit, Layer } from "effect"
|
|
6
|
+
import { Cause, Effect, Exit, Layer, type Path } from "effect"
|
|
7
7
|
import { Narrator, GitService, Host, Workspace, NodeServices } from "../platform/index.js"
|
|
8
8
|
import {
|
|
9
9
|
ConfigDiscovery,
|
|
@@ -23,7 +23,9 @@ type Env = Readonly<Record<string, string>>
|
|
|
23
23
|
const baseLayer = (dir: string, env: Env = {}) => {
|
|
24
24
|
const hostLayer = Host.layer({ root: dir, home: homeDir, env })
|
|
25
25
|
const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeServices.layer)))
|
|
26
|
-
const workspaceLayer = Workspace.Live.pipe(
|
|
26
|
+
const workspaceLayer = Workspace.Live.pipe(
|
|
27
|
+
Layer.provide(Layer.mergeAll(hostLayer, gitLayer, NodeServices.layer)),
|
|
28
|
+
)
|
|
27
29
|
return Layer.mergeAll(hostLayer, gitLayer, workspaceLayer)
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -34,18 +36,25 @@ const layer = (dir: string, env: Env = {}) =>
|
|
|
34
36
|
Layer.mergeAll(
|
|
35
37
|
ConfigService.Live,
|
|
36
38
|
ConfigDiscovery.Live,
|
|
37
|
-
baseLayer(dir, env),
|
|
38
39
|
Narrator.layer(() => {}, false),
|
|
39
|
-
)
|
|
40
|
+
).pipe(Layer.provideMerge(baseLayer(dir, env)), Layer.provideMerge(NodeServices.layer))
|
|
40
41
|
|
|
41
42
|
const run = <A>(
|
|
42
|
-
eff: Effect.Effect<
|
|
43
|
+
eff: Effect.Effect<
|
|
44
|
+
A,
|
|
45
|
+
Error,
|
|
46
|
+
ConfigService | ConfigDiscovery | Narrator | Workspace | Host | Path.Path
|
|
47
|
+
>,
|
|
43
48
|
dir: string = projectDir,
|
|
44
49
|
env: Env = {},
|
|
45
50
|
) => Effect.runPromise(eff.pipe(Effect.provide(layer(dir, env))))
|
|
46
51
|
|
|
47
52
|
const runExit = <A>(
|
|
48
|
-
eff: Effect.Effect<
|
|
53
|
+
eff: Effect.Effect<
|
|
54
|
+
A,
|
|
55
|
+
Error,
|
|
56
|
+
ConfigService | ConfigDiscovery | Narrator | Workspace | Host | Path.Path
|
|
57
|
+
>,
|
|
49
58
|
dir: string = projectDir,
|
|
50
59
|
) => Effect.runPromiseExit(eff.pipe(Effect.provide(layer(dir))))
|
|
51
60
|
|
|
@@ -265,6 +274,7 @@ describe("ConfigService", () => {
|
|
|
265
274
|
others,
|
|
266
275
|
baseLayer(projectDir),
|
|
267
276
|
Narrator.layer((m) => warnings.push(m), false),
|
|
277
|
+
NodeServices.layer,
|
|
268
278
|
),
|
|
269
279
|
),
|
|
270
280
|
),
|