@pmelab/gtd 60.0.0 → 60.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.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +14 -2
  3. package/dist/chunks/testkit-BhTey1Do.mjs +30517 -0
  4. package/dist/gtd.bundle.mjs +10545 -19230
  5. package/dist/gtd.core.mjs +593 -0
  6. package/dist/testkit/gherkin.mjs +2 -2
  7. package/dist/testkit.mjs +1 -1
  8. package/package.json +7 -7
  9. package/src/cli/Cli.test.ts +2 -2
  10. package/src/cli/Cli.ts +68 -51
  11. package/src/cli/Output.test.ts +1 -1
  12. package/src/cli/Output.ts +3 -2
  13. package/src/cli/Program.test.ts +14 -6
  14. package/src/cli/Program.ts +35 -10
  15. package/src/cli/ToolCommands.ts +2 -2
  16. package/src/edge/Edge.test.ts +8 -6
  17. package/src/edge/Edge.ts +3 -3
  18. package/src/flows/runtime.ts +1 -1
  19. package/src/install/Version.ts +9 -3
  20. package/src/install/index.ts +1 -1
  21. package/src/platform/CommandRunner.ts +13 -11
  22. package/src/platform/Commentary.ts +2 -2
  23. package/src/platform/Git.test.ts +4 -5
  24. package/src/platform/Git.ts +42 -38
  25. package/src/platform/Host.ts +1 -1
  26. package/src/platform/NodeServices.ts +11 -0
  27. package/src/platform/Workspace.test.ts +4 -5
  28. package/src/platform/Workspace.ts +3 -3
  29. package/src/platform/WorktreeState.test.ts +2 -3
  30. package/src/platform/WorktreeState.ts +1 -1
  31. package/src/platform/index.ts +2 -1
  32. package/src/step/planStart.ts +3 -3
  33. package/src/twins/EmittedScripts.test.ts +1 -1
  34. package/src/twins/GitTiers.ts +6 -7
  35. package/src/twins/Layers.test.ts +1 -1
  36. package/src/twins/Layers.ts +10 -3
  37. package/src/twins/cliIo.ts +9 -4
  38. package/src/workflow/discovery.test.ts +24 -7
  39. package/src/workflow/discovery.ts +5 -6
  40. package/src/workflow/index.ts +1 -0
  41. package/src/workflow/load.test.ts +60 -3
  42. package/src/workflow/load.ts +316 -166
  43. package/src/workflow/pin.test.ts +159 -0
  44. package/src/workflow/pin.ts +72 -0
  45. package/src/workflow/vars.ts +3 -3
  46. package/dist/chunks/testkit-BvecVjQ0.mjs +0 -32355
@@ -1,7 +1,10 @@
1
- import { existsSync, readFileSync } from "node:fs"
2
- import { createRequire } from "node:module"
3
- import { dirname, join } from "node:path"
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs"
2
+ import { createRequire, isBuiltin } from "node:module"
3
+ import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path"
4
4
  import { fileURLToPath, pathToFileURL } from "node:url"
5
+ import { stripVTControlCharacters } from "node:util"
6
+ import { runInThisContext } from "node:vm"
7
+ import type { Plugin as RolldownPlugin } from "rolldown"
5
8
  import { Context, Effect, Layer } from "effect"
6
9
  import { GtdError, Narrator } from "../platform/index.js"
7
10
  import { accessShapeFault, replay, type TreeView } from "../replay/index.js"
@@ -15,6 +18,7 @@ import { compileConfig, type CompiledConfig } from "./compile.js"
15
18
  import { SETTING_NAME_RULE, isSettingName } from "./vars.js"
16
19
  import { ConfigDiscovery, WORKFLOW_MODULE, type WorkflowModule } from "./discovery.js"
17
20
  import { configFingerprint, type ConfigFingerprint } from "./fingerprint.js"
21
+ import { PIN_KEY } from "../install/index.js"
18
22
  import {
19
23
  dedupeDiagnostics,
20
24
  formatDiagnostic,
@@ -100,9 +104,25 @@ export const refuseLegacyRc: Effect.Effect<void, GtdError, Host | ConfigDiscover
100
104
  )
101
105
  })
102
106
 
107
+ const configModule: Effect.Effect<WorkflowModule, Error, Host | ConfigDiscovery> = Effect.gen(
108
+ function* () {
109
+ const host = yield* Host
110
+ const module = yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)
111
+ if (module !== undefined) return module
112
+ const missing: Diagnostic = {
113
+ severity: "error",
114
+ path: [],
115
+ message: `no workflows configured: no ${WORKFLOW_MODULE} here or above — gtd ships no workflow; \`gtd guide\` says where to get one`,
116
+ origin: host.root,
117
+ }
118
+ return yield* Effect.fail(configError([missing]))
119
+ },
120
+ )
121
+
103
122
  /**
104
- * Find and evaluate `gtd.config.ts`, then check what it exports. Every finding
105
- * is collected into ONE sorted report rather than failing on the first.
123
+ * Find and evaluate `gtd.config.ts` — or, inside a pin, the workflow bundled
124
+ * into it — then check what it exports. Every finding is collected into ONE
125
+ * sorted report rather than failing on the first.
106
126
  */
107
127
  export const load: Effect.Effect<
108
128
  ConfigOperations,
@@ -110,25 +130,30 @@ export const load: Effect.Effect<
110
130
  Narrator | Workspace | Host | ConfigDiscovery
111
131
  > = Effect.gen(function* () {
112
132
  const host = yield* Host
113
- const discovery = yield* ConfigDiscovery
114
133
  yield* refuseLegacyRc
115
- const module = yield* discovery.workflowModule(host.root, host.home)
116
- if (module === undefined) {
117
- const missing: Diagnostic = {
118
- severity: "error",
119
- path: [],
120
- message: `no workflows configured: no ${WORKFLOW_MODULE} here or above — gtd ships no workflow; \`gtd guide\` says where to get one`,
121
- origin: host.root,
122
- }
123
- return yield* Effect.fail(configError([missing]))
124
- }
125
- yield* (yield* Narrator).narrate(`config: module ${module.filepath}`)
126
- const shipped = yield* Effect.tryPromise({
127
- try: shippedModules,
128
- catch: (e) => new GtdError(`gtd: ${e instanceof Error ? e.message : String(e)}`),
129
- })
130
- const loaded = yield* Effect.try({
131
- try: () => loadWorkflows(module, shipped),
134
+ const pinned = pinnedWorkflow()
135
+ const module: WorkflowModule =
136
+ pinned === undefined
137
+ ? yield* configModule
138
+ : { filepath: resolvePath(host.root, pinned.origin), source: "" }
139
+ yield* (yield* Narrator).narrate(
140
+ `config: ${pinned === undefined ? "module" : "pinned module"} ${module.filepath}`,
141
+ )
142
+ const shipped =
143
+ pinned === undefined
144
+ ? yield* Effect.tryPromise({
145
+ try: shippedModules,
146
+ catch: (e) => new GtdError(`gtd: ${e instanceof Error ? e.message : String(e)}`),
147
+ })
148
+ : undefined
149
+ const loaded = yield* Effect.tryPromise({
150
+ try: async () =>
151
+ loadWorkflows(
152
+ module.filepath,
153
+ shipped === undefined
154
+ ? runPinned(pinned!, module.filepath, host.root)
155
+ : await evaluate(module, shipped),
156
+ ),
132
157
  catch: (e) =>
133
158
  new GtdError(
134
159
  `gtd: config:\n - ${module.filepath}: ${e instanceof Error ? e.message : String(e)}`,
@@ -176,11 +201,9 @@ export const load: Effect.Effect<
176
201
  },
177
202
  ...(compiled.ui !== undefined ? { ui: compiled.ui } : {}),
178
203
  warnings: diagnostics.filter((d) => d.severity === "warning"),
179
- fingerprint: configFingerprint(
180
- loaded.file.dependencyFiles,
181
- host.root,
182
- module.read ?? readFromDisk,
183
- ),
204
+ fingerprint:
205
+ pinned?.fingerprint ??
206
+ configFingerprint(loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk),
184
207
  }
185
208
  })
186
209
 
@@ -200,13 +223,11 @@ interface Settings {
200
223
  interface LoadedFile {
201
224
  readonly origin: string
202
225
  /**
203
- * Every real file this module's evaluation touched — `origin` itself plus
204
- * any relative import it (transitively) resolved on disk, in NO particular
205
- * order. A caller that
206
- * needs to know when a workflow's DEFINITION might have changed (the LSP's
207
- * steering-map memo) must watch this whole set, not just `origin`: a
208
- * `gtd.config.ts` re-exporting a sibling module is a documented pattern,
209
- * and jiti resolves that import off disk same as any other require.
226
+ * Every file bundled evaluating this module — `origin` itself plus every
227
+ * local module it (transitively) imports, in NO particular order. A caller
228
+ * that needs to know when a workflow's DEFINITION might have changed (the
229
+ * LSP's steering-map memo) must watch this whole set, not just `origin`: a
230
+ * `gtd.config.ts` re-exporting a sibling module is a documented pattern.
210
231
  */
211
232
  readonly dependencyFiles: readonly string[]
212
233
  }
@@ -231,76 +252,8 @@ interface Loaded {
231
252
  readonly diagnostics: readonly Diagnostic[]
232
253
  }
233
254
 
234
- type JitiInstance = {
235
- evalModule: (
236
- source: string,
237
- options: { filename: string; async?: false; cache?: Record<string, unknown> },
238
- ) => unknown
239
- }
240
- type JitiModule = {
241
- createJiti: (id: string, options: Record<string, unknown>) => JitiInstance
242
- }
243
-
244
- type Transform = (options: { readonly source: string }) => { code: string; error?: unknown }
245
-
246
- // Transpiling is most of a load's cost, and a load re-transpiles
247
- // gtd.config.ts and every shipped module it imports. The same options (the
248
- // source included) always transpile to the same code, so an in-process memo
249
- // is safe; evaluation stays fresh on every load.
250
- const TRANSFORM_MEMO_SIZE = 512
251
- const memoized = (transform: Transform): Transform => {
252
- const memo = new Map<string, ReturnType<Transform>>()
253
- return (options) => {
254
- const key = JSON.stringify(options)
255
- const hit = memo.get(key)
256
- if (hit !== undefined) return hit
257
- const result = transform(options)
258
- if (result.error !== undefined) return result
259
- if (memo.size >= TRANSFORM_MEMO_SIZE) memo.delete(memo.keys().next().value!)
260
- memo.set(key, result)
261
- return result
262
- }
263
- }
264
-
265
- let jitiModule: JitiModule | undefined
266
- let transform: Transform | undefined
267
- /** Relative specifiers a module imports, served from somewhere other than the disk. */
268
- interface LocalModules {
269
- /** The path a specifier names, or nothing when it names no module. */
270
- readonly resolve: (specifier: string) => string | undefined
271
- readonly load: (path: string) => unknown
272
- }
273
-
274
- const jiti = (shipped: ShippedModules, local?: LocalModules): JitiInstance => {
275
- const require = createRequire(import.meta.url)
276
- jitiModule ??= require("jiti") as JitiModule
277
- // jiti's own Babel transform, which its package exports don't name.
278
- transform ??= memoized(
279
- require(join(dirname(require.resolve("jiti/package.json")), "dist", "babel.cjs")) as Transform,
280
- )
281
- return jitiModule.createJiti(import.meta.url, {
282
- // Consulted when a module is required, so a local module is evaluated
283
- // only once an import actually runs.
284
- virtualModules: new Proxy(shipped.modules, {
285
- has: (modules, id) =>
286
- id in modules || (typeof id === "string" && local?.resolve(id) !== undefined),
287
- get: (modules, id) => {
288
- const path = typeof id === "string" ? local?.resolve(id) : undefined
289
- return path === undefined ? modules[id as string] : local!.load(path)
290
- },
291
- }),
292
- alias: shipped.missing,
293
- // No transpile cache on disk, and a fresh module every load — nothing a
294
- // later command could read back instead of the source.
295
- fsCache: false,
296
- moduleCache: false,
297
- transform,
298
- // The whole module, not just its default: the named exports are read too.
299
- interopDefault: false,
300
- })
301
- }
302
-
303
255
  const PACKAGE = "@pmelab/gtd"
256
+ const FLOWS = `${PACKAGE}/flows`
304
257
 
305
258
  interface PackageJson {
306
259
  readonly name?: string
@@ -309,7 +262,7 @@ interface PackageJson {
309
262
 
310
263
  // Only gtd's own package.json: whatever other one sits nearest (a vendored
311
264
  // bundle inside a project) must never get its exports executed.
312
- const ownPackage = (): { readonly root: string; readonly pkg: PackageJson } => {
265
+ export const ownPackage = (): { readonly root: string; readonly pkg: PackageJson } => {
313
266
  const start = dirname(fileURLToPath(import.meta.url))
314
267
  for (let dir = start; ; dir = dirname(dir)) {
315
268
  const file = join(dir, "package.json")
@@ -340,7 +293,7 @@ const shippedModules = (): Promise<ShippedModules> =>
340
293
  (shippedMemo ??= (async () => {
341
294
  const fromSource = import.meta.url.endsWith(".ts")
342
295
  const files = subpathFiles(fromSource ? "source" : "default")
343
- const modules: Record<string, unknown> = { [`${PACKAGE}/flows`]: flows }
296
+ const modules: Record<string, unknown> = { [FLOWS]: flows }
344
297
  const missing: Record<string, string> = {}
345
298
  if (process.features.require_module && !fromSource) {
346
299
  return { modules: requiredOnUse(modules, files), missing }
@@ -582,18 +535,11 @@ const namespaceOf = (exported: unknown): Record<string, unknown> =>
582
535
  /**
583
536
  * Evaluate `gtd.config.ts`. Every export but the reserved names that went
584
537
  * through `workflow()` or is a factory workflow is a workflow named by its
585
- * export. `cache` is jiti's OWN per-call module cache (unrelated to the
586
- * instance's `moduleCache: false`) — passing an empty object in has the side
587
- * effect of collecting every real file jiti resolved evaluating this module,
588
- * module AND every relative import it pulled in, transitively.
538
+ * export.
589
539
  */
590
- const loadWorkflows = (module: WorkflowModule, shipped: ShippedModules): Loaded => {
591
- const { exports: evaluated, files } =
592
- module.read === undefined
593
- ? evaluateFromDisk(module, shipped)
594
- : evaluateRead(module, module.read, shipped)
595
- const exports = namespaceOf(evaluated)
596
- const file: LoadedFile = { origin: module.filepath, dependencyFiles: files }
540
+ const loadWorkflows = (origin: string, evaluated: Evaluated): Loaded => {
541
+ const exports = namespaceOf(evaluated.exports)
542
+ const file: LoadedFile = { origin, dependencyFiles: evaluated.files }
597
543
  const exported = Object.entries(exports).filter(
598
544
  ([name, value]) => !RESERVED_EXPORTS.has(name) && isWorkflow(value),
599
545
  )
@@ -625,7 +571,7 @@ const loadWorkflows = (module: WorkflowModule, shipped: ShippedModules): Loaded
625
571
  catalogue,
626
572
  doors,
627
573
  default: defaultWorkflow && { ...defaultWorkflow, name: defaultName },
628
- config: compileConfig(exports["config"], module.filepath),
574
+ config: compileConfig(exports["config"], origin),
629
575
  diagnostics: [
630
576
  ...retiredExports(exports, file.origin),
631
577
  ...nameRejections(rejections),
@@ -658,17 +604,8 @@ const shadowingDoors = (
658
604
  origin,
659
605
  }))
660
606
 
661
- const evaluateFromDisk = (
662
- module: WorkflowModule,
663
- shipped: ShippedModules,
664
- ): { readonly exports: unknown; readonly files: readonly string[] } => {
665
- const cache: Record<string, unknown> = {}
666
- const exports = jiti(shipped).evalModule(module.source, { filename: module.filepath, cache })
667
- return { exports, files: Object.keys(cache) }
668
- }
669
-
670
- // How jiti finds a TypeScript module behind a specifier: as written, its
671
- // `.js` spelling as `.ts`, with `.ts` appended, or as a directory index.
607
+ // How a TypeScript module is found behind a relative specifier: as written,
608
+ // its `.js` spelling as `.ts`, with `.ts` appended, or as a directory index.
672
609
  const moduleCandidates = (path: string): readonly string[] => [
673
610
  path,
674
611
  path.replace(/\.js$/, ".ts"),
@@ -677,41 +614,255 @@ const moduleCandidates = (path: string): readonly string[] => [
677
614
  join(path, "index.ts"),
678
615
  ]
679
616
 
617
+ interface Evaluated {
618
+ readonly exports: unknown
619
+ readonly files: readonly string[]
620
+ }
621
+
622
+ /** A bundle's one chunk: CommonJS, so it runs as a plain function. */
623
+ type Chunk = (
624
+ exports: unknown,
625
+ require: (id: string) => unknown,
626
+ module: { exports: unknown },
627
+ __filename: string,
628
+ __dirname: string,
629
+ ) => void
630
+
631
+ const execute = (chunk: Chunk, filepath: string, require: (id: string) => unknown): unknown => {
632
+ const cjs = { exports: {} as unknown }
633
+ chunk(cjs.exports, require, cjs, filepath, dirname(filepath))
634
+ return cjs.exports
635
+ }
636
+
680
637
  /**
681
- * Evaluates the config with every local module it imports taken from `read`
682
- * instead of the disk. jiti matches a virtual module on the specifier as
683
- * written, so each module gets its own instance resolving against its own
684
- * directory. A specifier `read` cannot serve falls through to the disk, and
685
- * fails the way a missing file does.
638
+ * Bundles the config and every local module it imports in memory, then
639
+ * evaluates the bundle. With `read`, a relative import `read` can serve is
640
+ * taken from it instead of the disk; one it cannot falls through to the disk,
641
+ * and fails the way a missing file does. An ES module or TypeScript package is
642
+ * bundled too; a CommonJS one is required from where its importer resolved
643
+ * it, as Node would. `@pmelab/gtd/*` is the running gtd's own copy, and
644
+ * `files` leaves out every package file.
686
645
  */
687
- const evaluateRead = (
646
+ const evaluate = async (module: WorkflowModule, shipped: ShippedModules): Promise<Evaluated> => {
647
+ const { code, files } = await bundleConfig(module, { kind: "evaluate", shipped })
648
+ const fromConfig = createRequire(module.filepath)
649
+ const require = Object.assign(
650
+ (id: string): unknown =>
651
+ id in shipped.modules
652
+ ? shipped.modules[id]
653
+ : fromConfig(id in shipped.missing ? shipped.missing[id]! : id),
654
+ { resolve: fromConfig.resolve },
655
+ )
656
+ const chunk = runInThisContext(
657
+ `(function (exports, require, module, __filename, __dirname) {${code}\n})`,
658
+ { filename: module.filepath },
659
+ ) as Chunk
660
+ return { exports: execute(chunk, module.filepath, require), files }
661
+ }
662
+
663
+ /**
664
+ * What a pin registers: its workflow bundled, its origin relative to the
665
+ * repository root, and the fingerprint of the files it was bundled from — the
666
+ * content it runs, whatever the disk holds now.
667
+ */
668
+ interface PinnedWorkflow {
669
+ readonly version: string
670
+ readonly origin: string
671
+ readonly fingerprint: ConfigFingerprint
672
+ readonly run: Chunk
673
+ }
674
+
675
+ const pinnedWorkflow = (): PinnedWorkflow | undefined =>
676
+ (globalThis as { [PIN_KEY]?: PinnedWorkflow })[PIN_KEY]
677
+
678
+ const runPinned = (pinned: PinnedWorkflow, filepath: string, root: string): Evaluated => ({
679
+ // Everything but the engine's own flows and Node's builtins is in the bundle.
680
+ exports: execute(pinned.run, filepath, (id) =>
681
+ id === FLOWS ? flows : createRequire(import.meta.url)(id),
682
+ ),
683
+ files: Object.keys(pinned.fingerprint).map((file) => resolvePath(root, file)),
684
+ })
685
+
686
+ /**
687
+ * The statement that registers `module`'s workflow for a pin: the config and
688
+ * everything it imports — npm packages and gtd's own subpaths included — in
689
+ * one function, so the engine runs it without a config, a package or rolldown.
690
+ */
691
+ export const pinnedWorkflowSource = async (
688
692
  module: WorkflowModule,
689
- read: (path: string) => string | undefined,
690
- shipped: ShippedModules,
691
- ): { readonly exports: unknown; readonly files: readonly string[] } => {
692
- const files = [module.filepath]
693
- const evaluated = new Map<string, unknown>()
694
- const evaluate = (filepath: string, source: string): unknown =>
695
- jiti(shipped, {
696
- resolve: (specifier) =>
697
- /^\.\.?\//.test(specifier)
698
- ? moduleCandidates(join(dirname(filepath), specifier)).find(
699
- (candidate) => read(candidate) !== undefined,
700
- )
701
- : undefined,
702
- load: (path) => {
703
- if (!evaluated.has(path)) {
704
- // A placeholder first, so an import cycle ends here.
705
- evaluated.set(path, {})
706
- files.push(path)
707
- evaluated.set(path, evaluate(path, read(path)!))
708
- }
709
- return evaluated.get(path)
710
- },
711
- }).evalModule(source, { filename: filepath })
712
- return { exports: evaluate(module.filepath, module.source), files }
693
+ root: string,
694
+ version: string,
695
+ ): Promise<string> => {
696
+ const gtd = realpathSync(ownPackage().root) + sep
697
+ const { code, files } = await bundleConfig(module, {
698
+ kind: "pin",
699
+ shipped: subpathFiles(import.meta.url.endsWith(".ts") ? "source" : "default"),
700
+ })
701
+ // gtd's own subpaths are bundled from its package, which only a config
702
+ // living in gtd's own repository may count among its files.
703
+ const own = (file: string) => file.startsWith(gtd) && !module.filepath.startsWith(gtd)
704
+ const registered = {
705
+ version,
706
+ origin: relative(root, module.filepath),
707
+ fingerprint: configFingerprint(
708
+ files.filter((file) => !own(file)),
709
+ root,
710
+ module.read ?? readFromDisk,
711
+ ),
712
+ }
713
+ return `globalThis[Symbol.for(${JSON.stringify(PIN_KEY.description)})] = { ...${JSON.stringify(registered)}, run: function (exports, require, module, __filename, __dirname) {\n${code}\n} };\n`
714
+ }
715
+
716
+ type BundleMode =
717
+ | { readonly kind: "evaluate"; readonly shipped: ShippedModules }
718
+ // Every import goes into the bundle but flows, the engine's own.
719
+ | { readonly kind: "pin"; readonly shipped: ReadonlyMap<string, string> }
720
+
721
+ const shippedImport = (
722
+ source: string,
723
+ mode: BundleMode,
724
+ ): string | { readonly id: string; readonly external: true } | undefined => {
725
+ if (mode.kind === "pin")
726
+ return source === FLOWS ? { id: source, external: true } : mode.shipped.get(source)
727
+ return source in mode.shipped.modules || source in mode.shipped.missing
728
+ ? { id: source, external: true }
729
+ : undefined
713
730
  }
714
731
 
732
+ const bundleConfig = async (
733
+ module: WorkflowModule,
734
+ mode: BundleMode,
735
+ ): Promise<{ readonly code: string; readonly files: readonly string[] }> => {
736
+ const { read } = module
737
+ const served = new Set<string>()
738
+ const fromRead = (path: string): string | null => {
739
+ const hit = moduleCandidates(path).find((candidate) => read?.(candidate) !== undefined)
740
+ if (hit === undefined) return null
741
+ served.add(hit)
742
+ return hit
743
+ }
744
+ const plugin: RolldownPlugin = {
745
+ name: "gtd-config",
746
+ async resolveId(source, importer) {
747
+ if (importer === undefined) return source === module.filepath ? source : null
748
+ const shipped = shippedImport(source, mode)
749
+ if (shipped !== undefined) return shipped
750
+ if (/^\.\.?(\/|$)/.test(source)) return fromRead(join(dirname(importer), source))
751
+ if (!isPackage(source)) return null
752
+ const resolved = await this.resolve(source, importer, { skipSelf: true })
753
+ return mode.kind === "pin" ? resolved : packageImport(source, resolved)
754
+ },
755
+ load(id) {
756
+ if (mode.kind === "pin" && isNativeAddon(id)) throw new Error(nativeAddon(id))
757
+ if (id === module.filepath) return module.source
758
+ return served.has(id) ? read!(id) : null
759
+ },
760
+ // One bundle has one file; each module keeps its own location.
761
+ transform(code, id) {
762
+ return code.replace(/\bimport\.meta\.(url|filename|dirname)\b/g, (_, key: string) =>
763
+ JSON.stringify(
764
+ key === "url" ? pathToFileURL(id).href : key === "filename" ? id : dirname(id),
765
+ ),
766
+ )
767
+ },
768
+ }
769
+ const bundle = await (rolldownModule ??= requireRolldown()).rolldown({
770
+ input: module.filepath,
771
+ platform: "node",
772
+ plugins: [plugin],
773
+ external: (id) => isBuiltin(id),
774
+ resolve: { extensionAlias: { ".js": [".js", ".ts"], ".mjs": [".mjs", ".mts"] } },
775
+ // Every statement runs, as it would unbundled.
776
+ treeshake: false,
777
+ tsconfig: false,
778
+ logLevel: "silent",
779
+ })
780
+ try {
781
+ const { output } = await bundle
782
+ // One chunk, and no `import()` left: the bundle runs as a plain function.
783
+ .generate({
784
+ format: "cjs",
785
+ exports: "named",
786
+ codeSplitting: false,
787
+ dynamicImportInCjs: false,
788
+ })
789
+ .catch((e) => {
790
+ throw bundleError(e)
791
+ })
792
+ const chunk = output[0]
793
+ return {
794
+ code: chunk.code,
795
+ files: chunk.moduleIds.filter((id) => !id.startsWith("\0") && !inPackage(id)),
796
+ }
797
+ } finally {
798
+ await bundle.close()
799
+ }
800
+ }
801
+
802
+ // A pin is plain JavaScript: a compiled addon stays a file on one machine.
803
+ // A package node-gyp builds loads its addon by a path no bundler sees.
804
+ const isNativeAddon = (id: string): boolean => {
805
+ if (id.endsWith(".node")) return true
806
+ const at = id.lastIndexOf(`node_modules${sep}`)
807
+ if (at < 0) return false
808
+ const rest = id.slice(at + `node_modules${sep}`.length).split(sep)
809
+ const root =
810
+ id.slice(0, at) + ["node_modules", ...rest.slice(0, rest[0]!.startsWith("@") ? 2 : 1)].join(sep)
811
+ return existsSync(join(root, "binding.gyp"))
812
+ }
813
+
814
+ const nativeAddon = (path: string): string => {
815
+ const at = path.lastIndexOf(`node_modules${sep}`)
816
+ const rest = at < 0 ? [] : path.slice(at + `node_modules${sep}`.length).split(sep)
817
+ const name = rest[0]?.startsWith("@") ? rest.slice(0, 2).join("/") : rest[0]
818
+ return `${name === undefined ? path : `package "${name}"`} loads a native addon (${path}), which a pin cannot carry`
819
+ }
820
+
821
+ // Bundled, an ES module gets the shipped `@pmelab/gtd/*` like the config
822
+ // does. CommonJS stays Node's: it may read `__dirname`, which a bundle loses.
823
+ const packageImport = (
824
+ source: string,
825
+ resolved: { readonly id: string } | null,
826
+ ): string | { readonly id: string; readonly external: true } =>
827
+ resolved !== null && isEsModule(resolved.id)
828
+ ? resolved.id
829
+ : { id: resolved?.id ?? source, external: true }
830
+
831
+ const isEsModule = (path: string): boolean => {
832
+ if (/\.(mjs|[cm]?tsx?)$/.test(path)) return true
833
+ if (!/\.jsx?$/.test(path)) return false
834
+ for (let dir = dirname(path); dirname(dir) !== dir; dir = dirname(dir)) {
835
+ const file = join(dir, "package.json")
836
+ if (existsSync(file)) {
837
+ return (JSON.parse(readFileSync(file, "utf8")) as { type?: string }).type === "module"
838
+ }
839
+ }
840
+ return false
841
+ }
842
+
843
+ const inPackage = (path: string): boolean => /[\\/]node_modules[\\/]/.test(path)
844
+
845
+ const isPackage = (specifier: string): boolean => !isAbsolute(specifier) && !isBuiltin(specifier)
846
+
847
+ // rolldown reports each error with a colored code frame; a diagnostic takes
848
+ // its headline and location.
849
+ const bundleError = (e: unknown): unknown => {
850
+ const errors = (e as { errors?: readonly { message: string }[] } | null)?.errors
851
+ if (errors === undefined) return e
852
+ const lines = errors.map(({ message }) => {
853
+ const plain = stripVTControlCharacters(message)
854
+ const at = /\[ (.+:\d+:\d+) \]/.exec(plain)?.[1]
855
+ const headline = plain.split("\n")[0]!
856
+ return at === undefined ? headline : `${headline} (${at})`
857
+ })
858
+ return new Error(lines.join("\n"))
859
+ }
860
+
861
+ type Rolldown = typeof import("rolldown")
862
+ let rolldownModule: Rolldown | undefined
863
+ // Required at run time, never bundled: rolldown ships a native binding.
864
+ const requireRolldown = (): Rolldown => createRequire(import.meta.url)("rolldown") as Rolldown
865
+
715
866
  const isWorkflow = (value: unknown): boolean =>
716
867
  markedOptions(value) !== undefined || isFactoryWorkflow(value)
717
868
 
@@ -928,7 +1079,7 @@ const firstStep = (
928
1079
  const empty = watchedEmptyTree()
929
1080
  const vars = watchedEmptySettings()
930
1081
  const env = watchedEmptySettings()
931
- const outcome = yield* Effect.either(
1082
+ const outcome = yield* Effect.result(
932
1083
  replayToFirstStep(loaded, vars.settings, env.settings, empty.tree),
933
1084
  )
934
1085
  const reads = [...settingReads("process", vars), ...settingReads("environment", env)]
@@ -939,8 +1090,8 @@ const firstStep = (
939
1090
  ),
940
1091
  )
941
1092
  }
942
- if (outcome._tag === "Left") return yield* Effect.fail(outcome.left)
943
- const name = outcome.right
1093
+ if (outcome._tag === "Failure") return yield* Effect.fail(outcome.failure)
1094
+ const name = outcome.success
944
1095
  if (!empty.read()) return name
945
1096
  const atHead = yield* replayToFirstStep(loaded, vars.settings, env.settings, head)
946
1097
  if (atHead === name) return name
@@ -959,9 +1110,8 @@ interface ConfigServiceOperations {
959
1110
  >
960
1111
  }
961
1112
 
962
- export class ConfigService extends Context.Tag("ConfigService")<
963
- ConfigService,
964
- ConfigServiceOperations
965
- >() {
1113
+ export class ConfigService extends Context.Service<ConfigService, ConfigServiceOperations>()(
1114
+ "ConfigService",
1115
+ ) {
966
1116
  static Live = Layer.succeed(ConfigService, { load })
967
1117
  }