@gobing-ai/ts-runtime 0.3.1 → 0.3.3

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 (61) hide show
  1. package/README.md +234 -176
  2. package/dist/config.d.ts +13 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +14 -0
  5. package/dist/context.d.ts +28 -1
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +45 -2
  8. package/dist/file-system-cf.d.ts +25 -0
  9. package/dist/file-system-cf.d.ts.map +1 -0
  10. package/dist/file-system-cf.js +59 -0
  11. package/dist/file-system-node.d.ts +29 -0
  12. package/dist/file-system-node.d.ts.map +1 -0
  13. package/dist/file-system-node.js +94 -0
  14. package/dist/file-system.d.ts +47 -0
  15. package/dist/file-system.d.ts.map +1 -0
  16. package/dist/file-system.js +0 -0
  17. package/dist/fs.d.ts +31 -1
  18. package/dist/fs.d.ts.map +1 -1
  19. package/dist/fs.js +32 -19
  20. package/dist/index.d.ts +21 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +10 -2
  23. package/dist/path.d.ts +12 -0
  24. package/dist/path.d.ts.map +1 -1
  25. package/dist/path.js +65 -4
  26. package/dist/platform.d.ts +12 -0
  27. package/dist/platform.d.ts.map +1 -0
  28. package/dist/platform.js +41 -0
  29. package/dist/process-executor.d.ts +77 -19
  30. package/dist/process-executor.d.ts.map +1 -1
  31. package/dist/process-executor.js +209 -37
  32. package/dist/runtime-cf.d.ts +6 -0
  33. package/dist/runtime-cf.d.ts.map +1 -0
  34. package/dist/runtime-cf.js +33 -0
  35. package/dist/runtime-factory.d.ts +24 -0
  36. package/dist/runtime-factory.d.ts.map +1 -0
  37. package/dist/runtime-factory.js +0 -0
  38. package/dist/runtime-node-bun.d.ts +8 -0
  39. package/dist/runtime-node-bun.d.ts.map +1 -0
  40. package/dist/runtime-node-bun.js +67 -0
  41. package/dist/schema-validation.d.ts +16 -0
  42. package/dist/schema-validation.d.ts.map +1 -1
  43. package/dist/schema-validation.js +9 -4
  44. package/dist/types.d.ts +4 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +2 -2
  47. package/src/config.ts +16 -4
  48. package/src/context.ts +58 -4
  49. package/src/file-system-cf.ts +74 -0
  50. package/src/file-system-node.ts +122 -0
  51. package/src/file-system.ts +55 -0
  52. package/src/fs.ts +35 -18
  53. package/src/index.ts +57 -2
  54. package/src/path.ts +68 -4
  55. package/src/platform.ts +47 -0
  56. package/src/process-executor.ts +296 -58
  57. package/src/runtime-cf.ts +44 -0
  58. package/src/runtime-factory.ts +28 -0
  59. package/src/runtime-node-bun.ts +83 -0
  60. package/src/schema-validation.ts +20 -4
  61. package/src/types.ts +4 -0
package/src/config.ts CHANGED
@@ -2,6 +2,7 @@ import { deepMerge } from '@gobing-ai/ts-utils';
2
2
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
3
3
  import { type ZodIssue, z } from 'zod';
4
4
 
5
+ /** Zod schema for the application configuration object, providing defaults for app, database, and logging sections. */
5
6
  export const configSchema = z.object({
6
7
  app: z
7
8
  .object({
@@ -26,6 +27,7 @@ export const configSchema = z.object({
26
27
  .default({ level: 'info', console: true, file: false, json: false }),
27
28
  });
28
29
 
30
+ /** Inferred TypeScript type of a validated configuration object, derived from {@link configSchema}. */
29
31
  export type Config = z.output<typeof configSchema>;
30
32
 
31
33
  const ENV_INTERPOLATION_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
@@ -64,6 +66,7 @@ export function stringifyYamlObject(value: Record<string, unknown>): string {
64
66
  return stringifyYaml(value);
65
67
  }
66
68
 
69
+ /** Error thrown when configuration validation fails, carrying the Zod validation issues for diagnostics. */
67
70
  export class ConfigLoadError extends Error {
68
71
  readonly issues: ZodIssue[];
69
72
 
@@ -77,27 +80,36 @@ export class ConfigLoadError extends Error {
77
80
  // These accessors read `process.env` directly and are node-bun only (ADR-008). On
78
81
  // `cloudflare-workers` there is no `process`; inject config explicitly rather than calling these.
79
82
 
83
+ /** Returns the value of `process.env.NODE_ENV`, or `"development"` as default. Node/Bun only. */
80
84
  export function getNodeEnv(): string {
81
85
  return process.env.NODE_ENV ?? 'development';
82
86
  }
83
-
87
+ /** Returns `true` when `NODE_ENV` is `"test"`. Node/Bun only. */
84
88
  export function isTestEnv(): boolean {
85
89
  return getNodeEnv() === 'test';
86
90
  }
87
91
 
92
+ /** Returns `process.env` as a plain object. Node/Bun only. */
88
93
  export function getProcessEnv(): Record<string, string | undefined> {
89
94
  return process.env;
90
95
  }
91
96
 
97
+ /** Returns `process.env.DATABASE_URL`, or `undefined` if not set. Node/Bun only. */
92
98
  export function getDatabaseUrl(): string | undefined {
93
99
  return process.env.DATABASE_URL;
94
100
  }
95
101
 
102
+ /** Returns the user's home directory (`HOME`, falling back to `USERPROFILE` on Windows), or `undefined` if unset. Node/Bun only. */
103
+ export function getHomeDir(): string | undefined {
104
+ return process.env.HOME ?? process.env.USERPROFILE;
105
+ }
106
+
96
107
  /** Node-bun only: interpolates `${VAR}` from `process.env` (see note above). */
97
108
  export function interpolateEnv(value: string): string {
98
109
  return value.replace(ENV_INTERPOLATION_RE, (_match, name: string) => process.env[name] ?? `\${${name}}`);
99
110
  }
100
111
 
112
+ /** Recursively interpolates `${VAR}` environment variables in all string leaves of a nested object or array. Node/Bun only. */
101
113
  export function interpolateTree(value: unknown): unknown {
102
114
  if (typeof value === 'string') return interpolateEnv(value);
103
115
  if (Array.isArray(value)) return value.map(interpolateTree);
@@ -106,7 +118,7 @@ export function interpolateTree(value: unknown): unknown {
106
118
  }
107
119
  return value;
108
120
  }
109
-
121
+ /** Interpolates env vars, merges overrides, validates against {@link configSchema}, and returns a frozen {@link Config}. */
110
122
  export function buildConfigFromObject(
111
123
  raw: Record<string, unknown>,
112
124
  options: { overrides?: Partial<Config> } = {},
@@ -121,7 +133,7 @@ export function buildConfigFromObject(
121
133
  }
122
134
  return deepFreeze(result.data);
123
135
  }
124
-
136
+ /** Parses a YAML configuration string into a raw object, throwing {@link ConfigLoadError} on failure. */
125
137
  export function parseConfigYaml(yamlText: string): Record<string, unknown> {
126
138
  try {
127
139
  return parseYamlObject(yamlText);
@@ -130,7 +142,7 @@ export function parseConfigYaml(yamlText: string): Record<string, unknown> {
130
142
  throw new ConfigLoadError(`Config YAML parsing failed: ${(error as Error).message}`);
131
143
  }
132
144
  }
133
-
145
+ /** Parses YAML text and builds a validated {@link Config}, equivalent to `buildConfigFromObject(parseConfigYaml(…))`. */
134
146
  export function buildConfigFromYaml(yamlText: string, options: { overrides?: Partial<Config> } = {}): Config {
135
147
  return buildConfigFromObject(parseConfigYaml(yamlText), options);
136
148
  }
package/src/context.ts CHANGED
@@ -1,17 +1,23 @@
1
1
  import type { Config } from './config';
2
2
  import { buildConfigFromObject } from './config';
3
- import type { FileSystem } from './fs';
4
- import { getFs } from './fs';
3
+ import type { FileSystem } from './file-system';
4
+ import { createNodeFileSystem } from './file-system-node';
5
+ import { loadRuntimeFactory } from './platform';
6
+ import type { ProcessExecutor as ProcessExecutorService } from './process-executor';
7
+ import { ProcessExecutor } from './process-executor';
5
8
  import type { RuntimeCapabilities, RuntimeName } from './types';
6
-
9
+ /** Execution scope of a runtime context — determines service lifecycle and availability. */
7
10
  export type RuntimeScope = 'process' | 'server-request' | 'scheduled-event' | 'test';
8
11
 
12
+ /** Map of named services available to a runtime context, including the required `config` and `fileSystem`. */
9
13
  export interface RuntimeServiceMap {
10
14
  config: Config;
11
15
  fileSystem: FileSystem;
16
+ processExecutor?: ProcessExecutorService;
12
17
  [serviceName: string]: unknown;
13
18
  }
14
19
 
20
+ /** Options for constructing a {@link RuntimeContext}. */
15
21
  export interface RuntimeContextOptions<TServices extends RuntimeServiceMap = RuntimeServiceMap> {
16
22
  scope?: RuntimeScope;
17
23
  runtimeName?: RuntimeName;
@@ -19,6 +25,7 @@ export interface RuntimeContextOptions<TServices extends RuntimeServiceMap = Run
19
25
  services?: Partial<TServices>;
20
26
  }
21
27
 
28
+ /** Injectable service container scoped to a runtime environment (process, request, event, or test). */
22
29
  export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeServiceMap> {
23
30
  readonly scope: RuntimeScope;
24
31
  readonly runtimeName: RuntimeName;
@@ -37,7 +44,13 @@ export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeService
37
44
  } satisfies RuntimeCapabilities);
38
45
 
39
46
  this.register('config', (options.services?.config ?? buildConfigFromObject({})) as TServices['config']);
40
- this.register('fileSystem', (options.services?.fileSystem ?? getFs()) as TServices['fileSystem']);
47
+ this.register(
48
+ 'fileSystem',
49
+ (options.services?.fileSystem ?? createNodeFileSystem()) as TServices['fileSystem'],
50
+ );
51
+ if (this.capabilities.hasProcessExecution && options.services?.processExecutor === undefined) {
52
+ this.register('processExecutor', new ProcessExecutor() as TServices['processExecutor']);
53
+ }
41
54
 
42
55
  for (const [key, value] of Object.entries(options.services ?? {})) {
43
56
  if (value !== undefined) {
@@ -88,6 +101,47 @@ function isDisposable(value: unknown): value is { dispose(): void | Promise<void
88
101
  return typeof value === 'object' && value !== null && 'dispose' in value && typeof value.dispose === 'function';
89
102
  }
90
103
 
104
+ /**
105
+ * Create a {@link RuntimeContext} wired to the auto-detected runtime factory.
106
+ *
107
+ * Loads the platform-specific factory via {@link loadRuntimeFactory},
108
+ * auto-wires FileSystem, ProcessExecutor, and Config, then returns a
109
+ * ready-to-use context. Call this at the entry point of any app or worker.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * const ctx = await createRuntimeContextFromFactory();
114
+ * const fs = ctx.require('fileSystem');
115
+ * const config = ctx.require('config');
116
+ * ```
117
+ */
118
+ export async function createRuntimeContextFromFactory<TServices extends RuntimeServiceMap = RuntimeServiceMap>(
119
+ options?: RuntimeContextOptions<TServices>,
120
+ ): Promise<RuntimeContext<TServices>> {
121
+ const factory = await loadRuntimeFactory();
122
+ const config = await factory.loadConfig();
123
+ const fileSystem = factory.createFileSystem();
124
+ const processExecutor = factory.capabilities.hasProcessExecution ? factory.createProcessExecutor() : undefined;
125
+
126
+ return new RuntimeContext<TServices>({
127
+ scope: 'process',
128
+ runtimeName: factory.runtimeName,
129
+ capabilities: factory.capabilities,
130
+ services: {
131
+ config,
132
+ fileSystem,
133
+ ...(processExecutor !== undefined ? { processExecutor } : {}),
134
+ ...options?.services,
135
+ },
136
+ } as RuntimeContextOptions<TServices>);
137
+ }
138
+
139
+ /**
140
+ * @deprecated Use {@link createRuntimeContextFromFactory} instead —
141
+ * it auto-detects the platform and wires the correct services from the factory.
142
+ * This function is kept for backward compatibility with synchronous callers
143
+ * that manually configure services.
144
+ */
91
145
  export function createRuntimeContext<TServices extends RuntimeServiceMap = RuntimeServiceMap>(
92
146
  options: RuntimeContextOptions<TServices> = {},
93
147
  ): RuntimeContext<TServices> {
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Cloudflare Workers {@link FileSystem} stub.
3
+ *
4
+ * CF Workers have only an ephemeral, per-request virtual filesystem.
5
+ * Persistent file operations are not available. This stub throws clear
6
+ * errors directing developers to use D1, KV, or R2 instead.
7
+ *
8
+ * Non-mutating operations (`resolve`, `getProjectRoot`) return
9
+ * Worker-appropriate values without throwing.
10
+ *
11
+ * Note: Cloudflare Workers with `nodejs_compat` + `compatibility_date >= 2025-09-01`
12
+ * DO expose `node:fs` as an ephemeral, per-request virtual filesystem.
13
+ * We deliberately do NOT use it — the ephemeral nature means files written
14
+ * in one request silently vanish in the next. Throwing with guidance toward
15
+ * D1/KV/R2 is better DX than silent data loss.
16
+ */
17
+
18
+ import type { FileSystem } from './file-system';
19
+
20
+ const UNSUPPORTED = 'FileSystem is not available on Cloudflare Workers. Use D1, KV, or R2 for persistent storage.';
21
+
22
+ /**
23
+ * Create a Cloudflare Workers {@link FileSystem} stub.
24
+ *
25
+ * `resolve()` and `getProjectRoot()` work as path utilities.
26
+ * All other methods throw with guidance toward CF-native storage.
27
+ */
28
+ export function createCfFileSystem(): FileSystem {
29
+ return {
30
+ getProjectRoot: () => '/bundle',
31
+
32
+ resolve: (...segments: string[]) => {
33
+ const joined = segments.join('/').replaceAll(/\/+/g, '/');
34
+ return joined.startsWith('/') ? joined : `/${joined}`;
35
+ },
36
+
37
+ exists: (_path: string) => false,
38
+
39
+ readFile: (_path: string): never => {
40
+ throw new Error(`FileSystem.readFile: ${UNSUPPORTED}`);
41
+ },
42
+
43
+ writeFile: (_path: string, _content: string): never => {
44
+ throw new Error(`FileSystem.writeFile: ${UNSUPPORTED}`);
45
+ },
46
+
47
+ appendFile: (_path: string, _content: string): never => {
48
+ throw new Error(`FileSystem.appendFile: ${UNSUPPORTED}`);
49
+ },
50
+
51
+ ensureDir: (_path: string): void => {
52
+ // No-op: CF Workers virtual filesystem handles directory
53
+ // creation internally when files are written.
54
+ },
55
+
56
+ readDir: (_path: string): never => {
57
+ throw new Error(`FileSystem.readDir: ${UNSUPPORTED}`);
58
+ },
59
+
60
+ deleteFile: (_path: string): never => {
61
+ throw new Error(`FileSystem.deleteFile: ${UNSUPPORTED}`);
62
+ },
63
+
64
+ createWriteStream: (_path: string): never => {
65
+ throw new Error(`FileSystem.createWriteStream: ${UNSUPPORTED}`);
66
+ },
67
+
68
+ copy: (_src: string, _dest: string): never => {
69
+ throw new Error(`FileSystem.copy: ${UNSUPPORTED}`);
70
+ },
71
+
72
+ stat: (_path: string) => null,
73
+ };
74
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `node:fs`-backed {@link FileSystem} implementation for Bun/Node.js.
3
+ *
4
+ * This is the production implementation for local development, VPS, and
5
+ * any environment with a real filesystem. Tests should inject a virtual
6
+ * file system.
7
+ *
8
+ * The implementation uses `node:fs` sync APIs by default. Bun polyfills
9
+ * `node:fs` fully, so this works on both runtimes without a Bun-specific
10
+ * variant.
11
+ */
12
+
13
+ import {
14
+ appendFileSync,
15
+ cpSync,
16
+ createWriteStream,
17
+ existsSync,
18
+ mkdirSync,
19
+ readdirSync,
20
+ readFileSync,
21
+ rmSync,
22
+ statSync,
23
+ writeFileSync,
24
+ } from 'node:fs';
25
+ import { dirname, resolve as resolvePath } from 'node:path';
26
+
27
+ import type { FileSystem } from './file-system';
28
+
29
+ /**
30
+ * Create a {@link FileSystem} backed by `node:fs`.
31
+ *
32
+ * @param root - Project root directory (default: walks up from `process.cwd()` looking for `bun.lock` or `package.json`).
33
+ */
34
+ export function createNodeFileSystem(root?: string): FileSystem {
35
+ const projectRoot = root ?? findProjectRoot(process.cwd());
36
+
37
+ return {
38
+ getProjectRoot: () => projectRoot,
39
+
40
+ resolve: (...segments: string[]) => resolvePath(projectRoot, ...segments),
41
+
42
+ exists: (path: string) => existsSync(path),
43
+
44
+ readFile: (path: string) => readFileSync(path, 'utf-8'),
45
+
46
+ writeFile: (path: string, content: string) => {
47
+ ensureParentDir(path);
48
+ writeFileSync(path, content, 'utf-8');
49
+ },
50
+
51
+ appendFile: (path: string, content: string) => {
52
+ ensureParentDir(path);
53
+ appendFileSync(path, content, 'utf-8');
54
+ },
55
+
56
+ ensureDir: (path: string) => {
57
+ mkdirSync(path, { recursive: true });
58
+ },
59
+
60
+ readDir: (path: string) => readdirSync(path),
61
+
62
+ deleteFile: (path: string) => {
63
+ rmSync(path, { recursive: true, force: true });
64
+ },
65
+
66
+ createWriteStream: (path: string) => {
67
+ ensureParentDir(path);
68
+ return createWriteStream(path, { flags: 'a' });
69
+ },
70
+
71
+ copy: (src: string, dest: string) => {
72
+ cpSync(src, dest, { recursive: true });
73
+ },
74
+
75
+ stat: (path: string) => {
76
+ try {
77
+ const s = statSync(path);
78
+ return {
79
+ isFile: () => s.isFile(),
80
+ isDirectory: () => s.isDirectory(),
81
+ size: s.size,
82
+ mtimeMs: s.mtimeMs,
83
+ };
84
+ } catch {
85
+ return null;
86
+ }
87
+ },
88
+ };
89
+ }
90
+
91
+ // ── Helpers ──────────────────────────────────────────────────────────────
92
+
93
+ function ensureParentDir(filePath: string): void {
94
+ const dir = dirname(filePath);
95
+ if (!existsSync(dir)) {
96
+ mkdirSync(dir, { recursive: true });
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Find the project root by walking up from `startDir` looking for a `bun.lock`
102
+ * or `package.json` marker. Uses `existsSync`, so it works on both Node and Bun.
103
+ *
104
+ * This is the single project-root discovery implementation; the deprecated
105
+ * {@link import('./fs').getProjectRoot} delegates here.
106
+ *
107
+ * @internal — exported for reuse by config loading.
108
+ */
109
+ export function findProjectRoot(startDir: string): string {
110
+ let dir = resolvePath(startDir);
111
+ const root = resolvePath('/');
112
+ while (dir !== root) {
113
+ if (existsSync(resolvePath(dir, 'bun.lock')) || existsSync(resolvePath(dir, 'package.json'))) {
114
+ return dir;
115
+ }
116
+ const parent = resolvePath(dir, '..');
117
+ if (parent === dir) break;
118
+ dir = parent;
119
+ }
120
+ // Fallback: return the directory we started from.
121
+ return startDir;
122
+ }
@@ -0,0 +1,55 @@
1
+ /** Portable file stat subset. */
2
+ export interface FileStat {
3
+ isFile(): boolean;
4
+ isDirectory(): boolean;
5
+ size: number;
6
+ mtimeMs: number;
7
+ }
8
+
9
+ /**
10
+ * Runtime-agnostic file system abstraction.
11
+ *
12
+ * Bun/Node backend uses `node:fs`. Cloudflare Workers backend provides stubs
13
+ * that throw clear "not available" errors, directing developers to use D1,
14
+ * KV, or R2 for persistent storage.
15
+ *
16
+ * All code outside `packages/runtime/src/file-system*.ts` MUST use this
17
+ * interface — never import `node:fs` or call `Bun.write`/`Bun.file` directly.
18
+ */
19
+ export interface FileSystem {
20
+ /** Check whether a path exists. */
21
+ exists(path: string): boolean | Promise<boolean>;
22
+
23
+ /** Read file contents as UTF-8 string. Throws if not found. */
24
+ readFile(path: string): string | Promise<string>;
25
+
26
+ /** Write file contents, creating parent directories as needed. */
27
+ writeFile(path: string, content: string): void | Promise<void>;
28
+
29
+ /** Append content to a file, creating it if it doesn't exist. */
30
+ appendFile(path: string, content: string): void | Promise<void>;
31
+
32
+ /** Ensure a directory exists, creating it (and parents) recursively if needed. */
33
+ ensureDir(path: string): void | Promise<void>;
34
+
35
+ /** List directory entries (names only, not full paths). */
36
+ readDir(path: string): string[] | Promise<string[]>;
37
+
38
+ /** Delete a file or directory recursively. */
39
+ deleteFile(path: string): void | Promise<void>;
40
+
41
+ /** Recursively copy a file or directory. */
42
+ copy(src: string, dest: string): void | Promise<void>;
43
+
44
+ /** Get file or directory stats. Returns `null` if the path doesn't exist. */
45
+ stat(path: string): FileStat | null | Promise<FileStat | null>;
46
+
47
+ /** Create a writable stream for append-only output (Node/Bun only). Throws on CF Workers. */
48
+ createWriteStream(path: string): { write(chunk: string): void; end(): void };
49
+
50
+ /** Resolve path segments relative to the project root. */
51
+ resolve(...segments: string[]): string;
52
+
53
+ /** Get the project root directory path. */
54
+ getProjectRoot(): string;
55
+ }
package/src/fs.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { findProjectRoot } from './file-system-node';
2
3
  import { dirnamePath, getProcessCwd, joinPath, resolvePath } from './path';
3
4
 
5
+ /** Portable file stat interface — mirrors the subset of `node:fs.Stats` used by the runtime. */
4
6
  export interface FileStat {
5
7
  isFile(): boolean;
6
8
  isDirectory(): boolean;
@@ -8,11 +10,13 @@ export interface FileStat {
8
10
  mtimeMs: number;
9
11
  }
10
12
 
13
+ /** Write-only append stream for log output. */
11
14
  export interface LogStream {
12
15
  write(chunk: string): void;
13
16
  end(): void;
14
17
  }
15
18
 
19
+ /** @deprecated Use {@link import('./file-system').FileSystem} instead — the new interface has union return types and does not require a separate SyncFileSystem. */
16
20
  export interface FileSystem {
17
21
  readFile(path: string): Promise<string>;
18
22
  writeFile(path: string, content: string): Promise<void>;
@@ -28,6 +32,7 @@ export interface FileSystem {
28
32
  createLogStream(path: string): LogStream;
29
33
  }
30
34
 
35
+ /** @deprecated Use {@link import('./file-system').FileSystem} instead — its union return types make a separate sync interface unnecessary. */
31
36
  export interface SyncFileSystem {
32
37
  readFile(path: string): string;
33
38
  writeFile(path: string, content: string): void;
@@ -54,6 +59,8 @@ function nodeFs(): Promise<NodeFs> {
54
59
  return fsModule;
55
60
  }
56
61
 
62
+ /** {@link FileSystem} backed by `node:fs/promises`. Lazy-loads the module to avoid top-level import cost. */
63
+ /** @deprecated Use createNodeFileSystem() from './file-system-node' instead. */
57
64
  export class NodeFileSystem implements FileSystem {
58
65
  async readFile(path: string): Promise<string> {
59
66
  const { readFile } = await nodeFsPromises();
@@ -132,6 +139,8 @@ export class NodeFileSystem implements FileSystem {
132
139
  }
133
140
  }
134
141
 
142
+ /** {@link SyncFileSystem} backed by `node:fs` synchronous APIs. */
143
+ /** @deprecated Use createNodeFileSystem() from './file-system-node' instead — its FileSystem interface has union return types, eliminating the need for a separate sync implementation. */
135
144
  export class NodeSyncFileSystem implements SyncFileSystem {
136
145
  readFile(path: string): string {
137
146
  return readFileSync(path, 'utf-8');
@@ -214,6 +223,8 @@ class LazyNodeLogStream implements LogStream {
214
223
 
215
224
  const CLOUDFLARE_FS_ERROR = 'FileSystem is not available on Cloudflare Workers. Use D1, KV, or R2.';
216
225
 
226
+ /** {@link FileSystem} stub for Cloudflare Workers — all file operations throw. Use D1, KV, or R2 instead. */
227
+ /** @deprecated Use createCfFileSystem() from './file-system-cf' instead. */
217
228
  export class CloudflareFileSystem implements FileSystem {
218
229
  async readFile(path: string): Promise<string> {
219
230
  throw unsupportedCloudflareFs('readFile', path);
@@ -270,6 +281,8 @@ function unsupportedCloudflareFs(operation: string, path: string): Error {
270
281
 
271
282
  let activeFileSystem: FileSystem = new NodeFileSystem();
272
283
 
284
+ /** Swaps the active global file system, returning a restore function for the previous instance. */
285
+ /** @deprecated Use RuntimeFactory.createFileSystem() or ctx.require('fileSystem') instead. The global swap is replaced by factory-based DI. */
273
286
  export function setFileSystem(fileSystem: FileSystem): () => void {
274
287
  const previous = activeFileSystem;
275
288
  activeFileSystem = fileSystem;
@@ -278,18 +291,24 @@ export function setFileSystem(fileSystem: FileSystem): () => void {
278
291
  };
279
292
  }
280
293
 
294
+ /** Returns the currently active global {@link FileSystem} instance. */
295
+ /** @deprecated Use RuntimeFactory.createFileSystem() or ctx.require('fileSystem') instead. */
281
296
  export function getFs(): FileSystem {
282
297
  return activeFileSystem;
283
298
  }
284
299
 
300
+ /** Creates parent directories for a file path before writing. */
285
301
  export async function ensureDirForFile(path: string, fs = getFs()): Promise<void> {
286
302
  await fs.mkdir(dirnamePath(path));
287
303
  }
288
304
 
305
+ /** Synchronous variant of {@link ensureDirForFile}. */
306
+ /** @deprecated The new createNodeFileSystem() handles parent-directory creation internally. */
289
307
  export function ensureDirForFileSync(path: string, fs: SyncFileSystem): void {
290
308
  fs.mkdir(dirnamePath(path));
291
309
  }
292
310
 
311
+ /** Atomically writes a file by writing to a temp path then renaming, avoiding partial writes on crash. */
293
312
  export async function atomicWriteFile(path: string, content: string, fs = getFs()): Promise<void> {
294
313
  await ensureDirForFile(path, fs);
295
314
  const tempPath = `${path}.${getProcessPid()}.${uniqueToken()}.tmp`;
@@ -297,26 +316,31 @@ export async function atomicWriteFile(path: string, content: string, fs = getFs(
297
316
  await fs.rename(tempPath, path);
298
317
  }
299
318
 
319
+ /** Atomically writes a value as JSON with trailing newline. */
300
320
  export async function atomicWriteJson(path: string, value: unknown, fs = getFs()): Promise<void> {
301
321
  await atomicWriteFile(path, `${JSON.stringify(value, null, 2)}\n`, fs);
302
322
  }
303
323
 
324
+ /** Reads and parses a JSON file. */
304
325
  export async function readJsonFile<T = unknown>(path: string, fs = getFs()): Promise<T> {
305
326
  return JSON.parse(await fs.readFile(path)) as T;
306
327
  }
307
328
 
329
+ /** Writes a value as JSON with 2-space indentation and trailing newline. */
308
330
  export async function writeJsonFile(path: string, value: unknown, fs = getFs()): Promise<void> {
309
331
  await fs.writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
310
332
  }
311
333
 
312
- export async function walkDir(path: string, fs = getFs()): Promise<string[]> {
334
+ /** Recursively walks a directory, returning sorted paths to all files, optionally excluding entries by name. */
335
+ export async function walkDir(path: string, fs = getFs(), exclude?: Set<string>): Promise<string[]> {
313
336
  const entries = (await fs.readDir(path)).sort();
314
337
  const result: string[] = [];
315
338
  for (const entry of entries) {
339
+ if (exclude?.has(entry)) continue;
316
340
  const fullPath = joinPath(path, entry);
317
341
  const entryStat = await fs.stat(fullPath);
318
342
  if (entryStat?.isDirectory()) {
319
- result.push(...(await walkDir(fullPath, fs)));
343
+ result.push(...(await walkDir(fullPath, fs, exclude)));
320
344
  } else if (entryStat?.isFile()) {
321
345
  result.push(fullPath);
322
346
  }
@@ -324,33 +348,26 @@ export async function walkDir(path: string, fs = getFs()): Promise<string[]> {
324
348
  return result;
325
349
  }
326
350
 
351
+ /**
352
+ * Walks up from `startDir` looking for a `package.json` or `bun.lock` to locate the project root.
353
+ *
354
+ * @deprecated Use {@link import('./file-system-node').findProjectRoot} (or `createNodeFileSystem().getProjectRoot()`).
355
+ * This delegates to the single shared implementation.
356
+ */
327
357
  export function getProjectRoot(startDir = getProcessCwd()): string {
328
- let current = resolvePath(startDir);
329
- for (let i = 0; i < 12; i++) {
330
- if (hasBunFile(joinPath(current, 'bun.lock')) || hasBunFile(joinPath(current, 'package.json'))) {
331
- return current;
332
- }
333
- const parent = dirnamePath(current);
334
- if (parent === current) return startDir;
335
- current = parent;
336
- }
337
- return startDir;
358
+ return findProjectRoot(startDir);
338
359
  }
339
360
 
361
+ /** Resolves path segments relative to the project root. */
340
362
  export function resolveProjectPath(...segments: string[]): string {
341
363
  return resolvePath(getProjectRoot(), ...segments);
342
364
  }
343
365
 
366
+ /** Creates a {@link LogStream} at the given path using the active file system. */
344
367
  export function createLogStream(path: string, fs = getFs()): LogStream {
345
368
  return fs.createLogStream(path);
346
369
  }
347
370
 
348
- function hasBunFile(path: string): boolean {
349
- const bun = (globalThis as { Bun?: { file: (path: string) => { size: number } } }).Bun;
350
- if (bun === undefined) return false;
351
- return bun.file(path).size !== 0;
352
- }
353
-
354
371
  function getProcessPid(): number {
355
372
  return (globalThis as { process?: { pid?: number } }).process?.pid ?? 0;
356
373
  }
package/src/index.ts CHANGED
@@ -1,7 +1,62 @@
1
1
  export * from './config';
2
2
  export * from './context';
3
- export * from './fs';
3
+ export { createRuntimeContextFromFactory } from './context';
4
+ export type { FileStat, FileSystem } from './file-system';
5
+ export { createCfFileSystem } from './file-system-cf';
6
+ export { createNodeFileSystem, findProjectRoot } from './file-system-node';
7
+ export {
8
+ atomicWriteFile,
9
+ atomicWriteJson,
10
+ CloudflareFileSystem,
11
+ createLogStream,
12
+ ensureDirForFile,
13
+ ensureDirForFileSync,
14
+ type FileSystem as LegacyFileSystem,
15
+ getFs,
16
+ getProjectRoot,
17
+ NodeFileSystem,
18
+ NodeSyncFileSystem,
19
+ readJsonFile,
20
+ resolveProjectPath,
21
+ type SyncFileSystem,
22
+ setFileSystem,
23
+ walkDir,
24
+ writeJsonFile,
25
+ } from './fs';
4
26
  export * from './path';
5
- export * from './process-executor';
27
+ export { _resetRuntimeFactory, isCloudflareWorkerRuntime, loadRuntimeFactory } from './platform';
28
+ export type {
29
+ OutputPolicy,
30
+ PipeProcess,
31
+ PipeProcessOptions,
32
+ ProcessEventDetail,
33
+ ProcessEventSink,
34
+ ProcessEvents,
35
+ ProcessExecutorConfig,
36
+ ProcessExitReason,
37
+ ProcessOptions,
38
+ ProcessResult,
39
+ ProcessSignal,
40
+ TracerPort,
41
+ } from './process-executor';
42
+ export { ProcessExecutor } from './process-executor';
43
+ export { cloudflareWorkersFactory } from './runtime-cf';
44
+ export type { RuntimeFactory } from './runtime-factory';
45
+ export { _resetNodeFileSystem, nodeBunFactory } from './runtime-node-bun';
6
46
  export * from './schema-validation';
7
47
  export * from './types';
48
+
49
+ // ── Deprecated re-exports (backward compatibility) ──────────────────────
50
+
51
+ export { BunPipeProcessSpawner, BunSyncProcessExecutor, NodeProcessExecutor } from './process-executor';
52
+
53
+ /**
54
+ * @deprecated Use {@link ProcessExecutor} directly for async execution.
55
+ * Use `Bun.spawnSync` or `child_process.spawnSync` for sync.
56
+ */
57
+ export type SyncProcessExecutor = InstanceType<typeof import('./process-executor').BunSyncProcessExecutor>;
58
+
59
+ /**
60
+ * @deprecated Use {@link ProcessExecutor.runStreaming} instead.
61
+ */
62
+ export type PipeProcessSpawner = InstanceType<typeof import('./process-executor').BunPipeProcessSpawner>;