@pmelab/gtd 63.0.0 → 64.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 +12 -11
- 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-Cem5i6Xl.mjs} +422 -2
- package/dist/chunks/{testkit-DwypA7cH.mjs → testkit-DfWZQCWQ.mjs} +1073 -328
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +4372 -1310
- package/dist/gtd.core.mjs +126 -118
- package/dist/steering.mjs +2 -2
- 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 +22 -28
- package/src/cli/Output.ts +2 -1
- package/src/cli/Program.test.ts +55 -53
- package/src/cli/Program.ts +34 -34
- package/src/cli/ToolCommands.ts +3 -4
- package/src/edge/Edge.test.ts +38 -12
- package/src/edge/Edge.ts +53 -19
- package/src/emit/SteeringMode.test.ts +4 -2
- package/src/emit/SteeringMode.ts +31 -10
- package/src/flows/index.ts +1 -0
- package/src/flows/runtime.test.ts +2 -1
- package/src/flows/runtime.ts +8 -2
- 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 +16 -13
- package/src/replay/Format.ts +4 -2
- package/src/replay/Replay.test.ts +25 -5
- package/src/replay/Replay.ts +10 -6
- package/src/replay/Trailers.test.ts +10 -10
- package/src/replay/Trailers.ts +23 -1
- package/src/replay/index.ts +1 -0
- package/src/steering/Declared.test.ts +218 -0
- package/src/steering/Declared.ts +607 -0
- package/src/steering/FORMATS.md +3 -0
- package/src/steering/index.ts +12 -0
- package/src/steering/internal.ts +1 -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/EmittedScriptRecognizer.ts +5 -3
- package/src/twins/EmittedScripts.test.ts +19 -0
- 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/SteeringFormats.ts +4 -0
- package/src/workflow/Workflow.ts +2 -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/index.ts +5 -1
- 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/install/Version.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createRequire } from "node:module"
|
|
3
|
-
import { dirname, join } from "node:path"
|
|
4
|
-
import { fileURLToPath } from "node:url"
|
|
5
|
-
|
|
6
|
-
const _require = createRequire(import.meta.url)
|
|
1
|
+
import { NodeRuntime, nodeSyncIo } from "../platform/index.js"
|
|
7
2
|
|
|
8
3
|
// A source-relative literal (`../../package.json`) breaks the built single-file
|
|
9
4
|
// bundle: bundling doesn't rewrite `import.meta.url` per original module, so a
|
|
@@ -11,13 +6,19 @@ const _require = createRequire(import.meta.url)
|
|
|
11
6
|
// dist/ vs src/ — they differ (dist/gtd.bundle.mjs is always one level below
|
|
12
7
|
// repo root; src/install/Version.ts is two). Walking up from wherever this module
|
|
13
8
|
// actually runs from resolves correctly in both the unbundled (vitest/tsc) and
|
|
14
|
-
// bundled (dist/gtd.bundle.mjs) case.
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
// bundled (dist/gtd.bundle.mjs) case. URLs, not Path: this runs at import,
|
|
10
|
+
// before any Effect service exists.
|
|
11
|
+
const readPackageJson = (fromUrl: string): string => {
|
|
12
|
+
let dir = new URL(".", fromUrl)
|
|
17
13
|
for (let i = 0; i < 5; i++) {
|
|
18
|
-
const candidate =
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
const candidate = new URL("package.json", dir)
|
|
15
|
+
try {
|
|
16
|
+
const source = nodeSyncIo.readText(NodeRuntime.fileURLToPath(candidate))
|
|
17
|
+
if (source !== undefined) return source
|
|
18
|
+
} catch {
|
|
19
|
+
// Unreadable counts as absent, as it did for `existsSync`.
|
|
20
|
+
}
|
|
21
|
+
dir = new URL("..", dir)
|
|
21
22
|
}
|
|
22
23
|
throw new Error(`package.json not found by walking up from ${fromUrl}`)
|
|
23
24
|
}
|
|
@@ -30,4 +31,4 @@ export const PIN_KEY = Symbol.for("@pmelab/gtd/pin")
|
|
|
30
31
|
const pinned = (globalThis as { [PIN_KEY]?: { readonly version: string } })[PIN_KEY]?.version
|
|
31
32
|
|
|
32
33
|
export const GTD_VERSION: string =
|
|
33
|
-
pinned ?? (
|
|
34
|
+
pinned ?? (JSON.parse(readPackageJson(import.meta.url)) as { version: string }).version
|
package/src/platform/Host.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs"
|
|
2
|
-
import { homedir, tmpdir } from "node:os"
|
|
3
1
|
import { Context, Effect, Layer } from "effect"
|
|
2
|
+
import { homedir, tmpdir } from "./NodeRuntime.js"
|
|
3
|
+
import { nodeSyncIo } from "./NodeSync.js"
|
|
4
4
|
|
|
5
5
|
const toError = (e: unknown): Error => (e instanceof Error ? e : new Error(String(e)))
|
|
6
6
|
|
|
@@ -8,35 +8,88 @@ export interface HostOps {
|
|
|
8
8
|
readonly root: string
|
|
9
9
|
readonly home: string
|
|
10
10
|
readonly env: Readonly<Record<string, string | undefined>>
|
|
11
|
-
/** `env["TMPDIR"]` when set,
|
|
11
|
+
/** `env["TMPDIR"]` when set, the host's temp dir otherwise — never a `/tmp` literal or `mktemp` (`tests/tooling/no-tmp-assumption.test.ts` scans for both). */
|
|
12
12
|
readonly scratchDir: string
|
|
13
13
|
/** The real, symlink-resolved form of `path` — used to compare a possibly-symlinked cwd (macOS `/tmp` → `/private/tmp`) against git's own resolution. */
|
|
14
14
|
readonly realPath: (path: string) => Effect.Effect<string, Error>
|
|
15
|
+
/** Carried by every scratch or temp file name, so concurrent gtd runs never share one. */
|
|
16
|
+
readonly pid: number
|
|
17
|
+
/** `undefined` where the platform has no POSIX user ids. */
|
|
18
|
+
readonly uid: number | undefined
|
|
19
|
+
/** The command that runs this gtd again: its script under the running runtime, or `gtd` off `PATH` when there is none. */
|
|
20
|
+
readonly self: readonly [string, ...string[]]
|
|
21
|
+
/** Standard input read to its end, as UTF-8. */
|
|
22
|
+
readonly stdin: Effect.Effect<string, Error>
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
readonly
|
|
20
|
-
readonly
|
|
21
|
-
readonly
|
|
22
|
-
|
|
25
|
+
/** The output half of the process's stdio, which the CLI writes before any layer is built. */
|
|
26
|
+
export interface Stdio {
|
|
27
|
+
readonly stdout: (chunk: string | Uint8Array) => void
|
|
28
|
+
readonly stderr: (chunk: string) => void
|
|
29
|
+
readonly exit: (code: number) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The slice of Node's `process` the live host reads; a contract test hands in a fake one. */
|
|
33
|
+
export type NodeProcess = Pick<
|
|
34
|
+
NodeJS.Process,
|
|
35
|
+
"argv" | "execPath" | "pid" | "getuid" | "stdin" | "stdout" | "stderr" | "exitCode"
|
|
36
|
+
>
|
|
37
|
+
|
|
38
|
+
type HostOpsInput = Pick<HostOps, "root" | "home" | "env"> & Partial<HostOps>
|
|
39
|
+
|
|
40
|
+
/** `TMPDIR` when set and non-empty, else the host's own `fallback`. */
|
|
41
|
+
export const scratchDirOf = (env: HostOps["env"], fallback: string): string => {
|
|
42
|
+
const dir = env["TMPDIR"]
|
|
43
|
+
return dir !== undefined && dir.length > 0 ? dir : fallback
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const readAll = (stream: AsyncIterable<Buffer | string>): Effect.Effect<string, Error> =>
|
|
47
|
+
Effect.tryPromise({
|
|
48
|
+
try: async () => {
|
|
49
|
+
const chunks: Buffer[] = []
|
|
50
|
+
for await (const chunk of stream) chunks.push(Buffer.from(chunk))
|
|
51
|
+
return Buffer.concat(chunks).toString("utf8")
|
|
52
|
+
},
|
|
53
|
+
catch: toError,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const makeHostOps = (opts: HostOpsInput, proc: NodeProcess): HostOps => {
|
|
57
|
+
const script = proc.argv[1]
|
|
58
|
+
return {
|
|
59
|
+
scratchDir: scratchDirOf(opts.env, tmpdir()),
|
|
60
|
+
realPath: (path: string) =>
|
|
61
|
+
Effect.try({ try: () => nodeSyncIo.realPath(path), catch: toError }),
|
|
62
|
+
pid: proc.pid,
|
|
63
|
+
uid: proc.getuid?.(),
|
|
64
|
+
self: script === undefined ? ["gtd"] : [proc.execPath, script],
|
|
65
|
+
// Lazy: touching `process.stdin` opens it, which the LSP and the pin hand-over must not see.
|
|
66
|
+
stdin: Effect.suspend(() => readAll(proc.stdin)),
|
|
67
|
+
...opts,
|
|
68
|
+
}
|
|
23
69
|
}
|
|
24
70
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
export const nodeStdio = (proc: NodeProcess = process): Stdio => ({
|
|
72
|
+
// `stdout.write` is asynchronous whenever stdout is a pipe or socket —
|
|
73
|
+
// passing a completion callback (and never force-closing, see `exit`
|
|
74
|
+
// below) keeps a queued remainder from being discarded, so a large
|
|
75
|
+
// artifact still reaches a slow reader even after a non-zero exit.
|
|
76
|
+
stdout: (chunk) => {
|
|
77
|
+
proc.stdout.write(chunk, () => {})
|
|
78
|
+
},
|
|
79
|
+
stderr: (chunk) => {
|
|
80
|
+
proc.stderr.write(chunk)
|
|
81
|
+
},
|
|
82
|
+
// Never `process.exit(code)`: that tears the process down out from under
|
|
83
|
+
// an undrained `stdout.write` above and truncates the artifact mid-pipe.
|
|
84
|
+
// Setting `exitCode` lets the event loop drain any queued write first.
|
|
85
|
+
exit: (code) => {
|
|
86
|
+
proc.exitCode = code
|
|
87
|
+
},
|
|
37
88
|
})
|
|
38
89
|
|
|
90
|
+
/** Every member not given in `opts` comes from Node: `proc`, its OS and its file system. */
|
|
39
91
|
export class Host extends Context.Service<Host, HostOps>()("Host") {
|
|
40
|
-
static layer = (opts: HostOpsInput): Layer.Layer<Host> =>
|
|
92
|
+
static layer = (opts: HostOpsInput, proc: NodeProcess = process): Layer.Layer<Host> =>
|
|
93
|
+
Layer.succeed(Host, makeHostOps(opts, proc))
|
|
41
94
|
static Live = Host.layer({ root: process.cwd(), home: homedir(), env: process.env })
|
|
42
95
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { registerHooks } from "node:module"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
import { nodeSyncIo } from "./NodeSync.js"
|
|
5
|
+
import type { PinFile } from "./PinFile.js"
|
|
6
|
+
|
|
7
|
+
// `cwd`, as the engine's own root is: gtd runs only at the worktree root, and
|
|
8
|
+
// a relocated `$GIT_DIR` leaves no `.git` to walk up to. A file that cannot be
|
|
9
|
+
// read counts as no pin, so the launcher refuses it rather than crashing.
|
|
10
|
+
export const livePinFile =
|
|
11
|
+
(cwd: string, path: string): PinFile =>
|
|
12
|
+
() => {
|
|
13
|
+
const file = join(cwd, path)
|
|
14
|
+
try {
|
|
15
|
+
const bytes = nodeSyncIo.readFile(file)
|
|
16
|
+
if (bytes !== undefined) return { path: file, bytes }
|
|
17
|
+
} catch {
|
|
18
|
+
return { path: file, bytes: new Uint8Array() }
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return { path: file, bytes: nodeSyncIo.gitBytes(cwd, ["show", `HEAD:${path}`]) }
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Runs a pin's module as this process's main program. Node refuses an unknown
|
|
29
|
+
* extension, so a load hook serves the unpacked source under the pin's own
|
|
30
|
+
* URL — `import.meta.url` stays a file in the repository.
|
|
31
|
+
*/
|
|
32
|
+
export const runPinModule = async (path: string, source: string): Promise<void> => {
|
|
33
|
+
const url = pathToFileURL(path).href
|
|
34
|
+
// A pin read from HEAD has no file behind its URL, which Node's own resolve refuses.
|
|
35
|
+
registerHooks({
|
|
36
|
+
resolve: (specifier, context, nextResolve) =>
|
|
37
|
+
specifier === url ? { url, shortCircuit: true } : nextResolve(specifier, context),
|
|
38
|
+
load: (requested, context, nextLoad) =>
|
|
39
|
+
requested === url
|
|
40
|
+
? { format: "module", source, shortCircuit: true }
|
|
41
|
+
: nextLoad(requested, context),
|
|
42
|
+
})
|
|
43
|
+
await import(url)
|
|
44
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire, isBuiltin } from "node:module"
|
|
2
|
+
import { homedir, tmpdir } from "node:os"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { stripVTControlCharacters } from "node:util"
|
|
5
|
+
import { runInThisContext } from "node:vm"
|
|
6
|
+
import { gunzipSync, gzipSync } from "node:zlib"
|
|
7
|
+
|
|
8
|
+
// What the core needs of Node beyond the sync port: module loading, the
|
|
9
|
+
// user's directories, file URLs and gzip. Each is a Node fact with no in-memory twin.
|
|
10
|
+
|
|
11
|
+
export { createRequire, fileURLToPath, homedir, isBuiltin, stripVTControlCharacters, tmpdir }
|
|
12
|
+
|
|
13
|
+
export const gzip = (source: string): Uint8Array => gzipSync(source, { level: 9 })
|
|
14
|
+
|
|
15
|
+
export const gunzip = (bytes: Uint8Array): string => gunzipSync(bytes).toString("utf8")
|
|
16
|
+
|
|
17
|
+
/** Compiles `source` as a script in this realm, reporting errors against `filename`. */
|
|
18
|
+
export const compileScript = (source: string, filename: string): unknown =>
|
|
19
|
+
runInThisContext(source, { filename })
|
|
@@ -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* () {
|