@gobing-ai/ts-runtime 0.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/src/fs.ts ADDED
@@ -0,0 +1,239 @@
1
+ import { createWriteStream, mkdirSync } from 'node:fs';
2
+ import {
3
+ access,
4
+ appendFile,
5
+ cp,
6
+ mkdir,
7
+ readdir,
8
+ readFile,
9
+ realpath,
10
+ rename,
11
+ rm,
12
+ stat,
13
+ writeFile,
14
+ } from 'node:fs/promises';
15
+ import { dirname, join, resolve } from 'node:path';
16
+
17
+ export interface FileStat {
18
+ isFile(): boolean;
19
+ isDirectory(): boolean;
20
+ size: number;
21
+ mtimeMs: number;
22
+ }
23
+
24
+ export interface LogStream {
25
+ write(chunk: string): void;
26
+ end(): void;
27
+ }
28
+
29
+ export interface FileSystem {
30
+ readFile(path: string): Promise<string>;
31
+ writeFile(path: string, content: string): Promise<void>;
32
+ appendFile(path: string, content: string): Promise<void>;
33
+ mkdir(path: string): Promise<void>;
34
+ exists(path: string): Promise<boolean>;
35
+ readDir(path: string): Promise<string[]>;
36
+ unlink(path: string): Promise<void>;
37
+ stat(path: string): Promise<FileStat | null>;
38
+ realpath(path: string): Promise<string>;
39
+ copy(src: string, dest: string): Promise<void>;
40
+ rename(src: string, dest: string): Promise<void>;
41
+ createLogStream(path: string): LogStream;
42
+ }
43
+
44
+ export class NodeFileSystem implements FileSystem {
45
+ async readFile(path: string): Promise<string> {
46
+ return await readFile(path, 'utf-8');
47
+ }
48
+
49
+ async writeFile(path: string, content: string): Promise<void> {
50
+ await ensureDirForFile(path, this);
51
+ await writeFile(path, content, 'utf-8');
52
+ }
53
+
54
+ async appendFile(path: string, content: string): Promise<void> {
55
+ await ensureDirForFile(path, this);
56
+ await appendFile(path, content, 'utf-8');
57
+ }
58
+
59
+ async mkdir(path: string): Promise<void> {
60
+ await mkdir(path, { recursive: true });
61
+ }
62
+
63
+ async exists(path: string): Promise<boolean> {
64
+ try {
65
+ await access(path);
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ async readDir(path: string): Promise<string[]> {
73
+ return await readdir(path);
74
+ }
75
+
76
+ async unlink(path: string): Promise<void> {
77
+ await rm(path, { recursive: true, force: true });
78
+ }
79
+
80
+ async stat(path: string): Promise<FileStat | null> {
81
+ try {
82
+ const value = await stat(path);
83
+ return {
84
+ isFile: () => value.isFile(),
85
+ isDirectory: () => value.isDirectory(),
86
+ size: value.size,
87
+ mtimeMs: value.mtimeMs,
88
+ };
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ async realpath(path: string): Promise<string> {
95
+ return await realpath(path);
96
+ }
97
+
98
+ async copy(src: string, dest: string): Promise<void> {
99
+ await cp(src, dest, { recursive: true });
100
+ }
101
+
102
+ async rename(src: string, dest: string): Promise<void> {
103
+ await rename(src, dest);
104
+ }
105
+
106
+ createLogStream(path: string): LogStream {
107
+ mkdirSync(dirname(path), { recursive: true });
108
+ return createWriteStream(path, { flags: 'a' });
109
+ }
110
+ }
111
+
112
+ const CLOUDFLARE_FS_ERROR = 'FileSystem is not available on Cloudflare Workers. Use D1, KV, or R2.';
113
+
114
+ export class CloudflareFileSystem implements FileSystem {
115
+ async readFile(path: string): Promise<string> {
116
+ throw unsupportedCloudflareFs('readFile', path);
117
+ }
118
+
119
+ async writeFile(path: string, _content: string): Promise<void> {
120
+ throw unsupportedCloudflareFs('writeFile', path);
121
+ }
122
+
123
+ async appendFile(path: string, _content: string): Promise<void> {
124
+ throw unsupportedCloudflareFs('appendFile', path);
125
+ }
126
+
127
+ async mkdir(_path: string): Promise<void> {
128
+ return;
129
+ }
130
+
131
+ async exists(_path: string): Promise<boolean> {
132
+ return false;
133
+ }
134
+
135
+ async readDir(path: string): Promise<string[]> {
136
+ throw unsupportedCloudflareFs('readDir', path);
137
+ }
138
+
139
+ async unlink(path: string): Promise<void> {
140
+ throw unsupportedCloudflareFs('unlink', path);
141
+ }
142
+
143
+ async stat(_path: string): Promise<FileStat | null> {
144
+ return null;
145
+ }
146
+
147
+ async realpath(path: string): Promise<string> {
148
+ return resolveProjectPath(path);
149
+ }
150
+
151
+ async copy(src: string, _dest: string): Promise<void> {
152
+ throw unsupportedCloudflareFs('copy', src);
153
+ }
154
+
155
+ async rename(src: string, _dest: string): Promise<void> {
156
+ throw unsupportedCloudflareFs('rename', src);
157
+ }
158
+
159
+ createLogStream(path: string): LogStream {
160
+ throw unsupportedCloudflareFs('createLogStream', path);
161
+ }
162
+ }
163
+
164
+ function unsupportedCloudflareFs(operation: string, path: string): Error {
165
+ return new Error(`CloudflareFileSystem.${operation} failed for "${path}": ${CLOUDFLARE_FS_ERROR}`);
166
+ }
167
+
168
+ let activeFileSystem: FileSystem = new NodeFileSystem();
169
+
170
+ export function setFileSystem(fileSystem: FileSystem): () => void {
171
+ const previous = activeFileSystem;
172
+ activeFileSystem = fileSystem;
173
+ return () => {
174
+ activeFileSystem = previous;
175
+ };
176
+ }
177
+
178
+ export function getFs(): FileSystem {
179
+ return activeFileSystem;
180
+ }
181
+
182
+ export async function ensureDirForFile(path: string, fs = getFs()): Promise<void> {
183
+ await fs.mkdir(dirname(path));
184
+ }
185
+
186
+ export async function atomicWriteFile(path: string, content: string, fs = getFs()): Promise<void> {
187
+ await ensureDirForFile(path, fs);
188
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
189
+ await fs.writeFile(tempPath, content);
190
+ await fs.rename(tempPath, path);
191
+ }
192
+
193
+ export async function atomicWriteJson(path: string, value: unknown, fs = getFs()): Promise<void> {
194
+ await atomicWriteFile(path, `${JSON.stringify(value, null, 2)}\n`, fs);
195
+ }
196
+
197
+ export async function readJsonFile<T = unknown>(path: string, fs = getFs()): Promise<T> {
198
+ return JSON.parse(await fs.readFile(path)) as T;
199
+ }
200
+
201
+ export async function writeJsonFile(path: string, value: unknown, fs = getFs()): Promise<void> {
202
+ await fs.writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
203
+ }
204
+
205
+ export async function walkDir(path: string, fs = getFs()): Promise<string[]> {
206
+ const entries = (await fs.readDir(path)).sort();
207
+ const result: string[] = [];
208
+ for (const entry of entries) {
209
+ const fullPath = join(path, entry);
210
+ const entryStat = await fs.stat(fullPath);
211
+ if (entryStat?.isDirectory()) {
212
+ result.push(...(await walkDir(fullPath, fs)));
213
+ } else if (entryStat?.isFile()) {
214
+ result.push(fullPath);
215
+ }
216
+ }
217
+ return result;
218
+ }
219
+
220
+ export function getProjectRoot(startDir = process.cwd()): string {
221
+ let current = resolve(startDir);
222
+ for (let i = 0; i < 12; i++) {
223
+ if (Bun.file(join(current, 'bun.lock')).size !== 0 || Bun.file(join(current, 'package.json')).size !== 0) {
224
+ return current;
225
+ }
226
+ const parent = dirname(current);
227
+ if (parent === current) return startDir;
228
+ current = parent;
229
+ }
230
+ return startDir;
231
+ }
232
+
233
+ export function resolveProjectPath(...segments: string[]): string {
234
+ return resolve(getProjectRoot(), ...segments);
235
+ }
236
+
237
+ export function createLogStream(path: string, fs = getFs()): LogStream {
238
+ return fs.createLogStream(path);
239
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './config';
2
+ export * from './context';
3
+ export * from './fs';
4
+ export * from './process-executor';
5
+ export * from './types';
@@ -0,0 +1,118 @@
1
+ import { isatty } from 'node:tty';
2
+ import { type Options as ExecaOptions, execa } from 'execa';
3
+
4
+ export type OutputPolicy = { mode: 'buffered' } | { mode: 'stream'; isTTY?: boolean };
5
+
6
+ export interface ProcessExecutorConfig {
7
+ defaultTimeout?: number;
8
+ defaultMaxOutput?: number;
9
+ output?: OutputPolicy;
10
+ }
11
+
12
+ export interface ProcessOptions {
13
+ command: string;
14
+ args?: string[];
15
+ cwd?: string;
16
+ env?: Record<string, string>;
17
+ timeout?: number;
18
+ /** Maximum output buffer size in bytes (maps to execa `maxBuffer`). */
19
+ maxOutput?: number;
20
+ label?: string;
21
+ rejectOnError?: boolean;
22
+ forceBuffered?: boolean;
23
+ }
24
+
25
+ export interface ProcessResult {
26
+ command: string;
27
+ args: string[];
28
+ exitCode: number | null;
29
+ stdout: string;
30
+ stderr: string;
31
+ signal?: string;
32
+ durationMs: number;
33
+ }
34
+
35
+ export interface ProcessExecutor {
36
+ run(options: ProcessOptions): Promise<ProcessResult>;
37
+ }
38
+
39
+ export class NodeProcessExecutor implements ProcessExecutor {
40
+ constructor(private readonly config: ProcessExecutorConfig = {}) {}
41
+
42
+ async run(options: ProcessOptions): Promise<ProcessResult> {
43
+ const args = options.args ?? [];
44
+ const execaOptions = buildExecaOptions({
45
+ cwd: options.cwd,
46
+ env: options.env,
47
+ timeout: options.timeout ?? this.config.defaultTimeout,
48
+ maxOutput: options.maxOutput ?? this.config.defaultMaxOutput,
49
+ rejectOnError: options.rejectOnError ?? false,
50
+ outputPolicy: this.config.output,
51
+ forceBuffered: options.forceBuffered ?? false,
52
+ });
53
+
54
+ try {
55
+ const result = await execa(options.command, args, execaOptions);
56
+ return {
57
+ command: options.command,
58
+ args,
59
+ exitCode: result.exitCode ?? null,
60
+ stdout: asString(result.stdout),
61
+ stderr: asString(result.stderr),
62
+ ...(result.signalDescription !== undefined ? { signal: result.signalDescription } : {}),
63
+ durationMs: result.durationMs,
64
+ };
65
+ } catch (error) {
66
+ if (options.rejectOnError) throw error;
67
+ const failed = error as {
68
+ exitCode?: number;
69
+ stdout?: string | string[] | Uint8Array;
70
+ stderr?: string | string[] | Uint8Array;
71
+ signalDescription?: string;
72
+ durationMs?: number;
73
+ };
74
+ return {
75
+ command: options.command,
76
+ args,
77
+ exitCode: failed.exitCode ?? null,
78
+ stdout: asString(failed.stdout),
79
+ stderr: asString(failed.stderr),
80
+ ...(failed.signalDescription !== undefined ? { signal: failed.signalDescription } : {}),
81
+ durationMs: failed.durationMs ?? 0,
82
+ };
83
+ }
84
+ }
85
+ }
86
+
87
+ function buildExecaOptions(opts: {
88
+ cwd: string | undefined;
89
+ env: Record<string, string> | undefined;
90
+ timeout: number | undefined;
91
+ maxOutput: number | undefined;
92
+ rejectOnError: boolean;
93
+ outputPolicy: OutputPolicy | undefined;
94
+ forceBuffered: boolean;
95
+ }): ExecaOptions {
96
+ const canStream =
97
+ !opts.forceBuffered &&
98
+ opts.outputPolicy?.mode === 'stream' &&
99
+ (opts.outputPolicy.isTTY ?? process.stdout.isTTY ?? isatty(1));
100
+
101
+ return {
102
+ reject: opts.rejectOnError,
103
+ stdin: 'ignore',
104
+ stripFinalNewline: true,
105
+ ...(canStream ? { stdout: ['inherit', 'pipe'] as const, stderr: ['inherit', 'pipe'] as const } : { all: true }),
106
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
107
+ ...(opts.env !== undefined ? { env: opts.env } : {}),
108
+ ...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
109
+ ...(opts.maxOutput !== undefined ? { maxBuffer: opts.maxOutput } : {}),
110
+ };
111
+ }
112
+
113
+ function asString(value: string | string[] | unknown[] | Uint8Array | undefined): string {
114
+ if (typeof value === 'string') return value;
115
+ if (value instanceof Uint8Array) return new TextDecoder().decode(value);
116
+ if (Array.isArray(value)) return value.map(String).join('');
117
+ return '';
118
+ }
package/src/types.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { Config } from './config';
2
+ import type { RuntimeContext } from './context';
3
+ import type { FileSystem } from './fs';
4
+
5
+ export type RuntimeName = 'node-bun' | 'cloudflare-workers' | 'test';
6
+
7
+ export interface RuntimeCapabilities {
8
+ readonly hasFilesystem: boolean;
9
+ readonly hasProcessExecution: boolean;
10
+ readonly hasPersistentStorage: boolean;
11
+ }
12
+
13
+ export interface LoadConfigOptions {
14
+ overrides?: Partial<Config>;
15
+ envBindings?: Record<string, unknown>;
16
+ }
17
+
18
+ export interface RuntimeFactory {
19
+ readonly runtimeName: RuntimeName;
20
+ readonly capabilities: RuntimeCapabilities;
21
+ createFileSystem(): FileSystem;
22
+ loadConfig(options?: LoadConfigOptions): Promise<Config>;
23
+ createContext?(options?: { scope?: string }): RuntimeContext;
24
+ }
25
+
26
+ export interface SpanContext {
27
+ traceId: string;
28
+ spanId: string;
29
+ baggage?: Record<string, string>;
30
+ attributes?: Record<string, string | number | boolean>;
31
+ }