@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.
@@ -0,0 +1,41 @@
1
+ export type OutputPolicy = {
2
+ mode: 'buffered';
3
+ } | {
4
+ mode: 'stream';
5
+ isTTY?: boolean;
6
+ };
7
+ export interface ProcessExecutorConfig {
8
+ defaultTimeout?: number;
9
+ defaultMaxOutput?: number;
10
+ output?: OutputPolicy;
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
+ export interface ProcessResult {
25
+ command: string;
26
+ args: string[];
27
+ exitCode: number | null;
28
+ stdout: string;
29
+ stderr: string;
30
+ signal?: string;
31
+ durationMs: number;
32
+ }
33
+ export interface ProcessExecutor {
34
+ run(options: ProcessOptions): Promise<ProcessResult>;
35
+ }
36
+ export declare class NodeProcessExecutor implements ProcessExecutor {
37
+ private readonly config;
38
+ constructor(config?: ProcessExecutorConfig);
39
+ run(options: ProcessOptions): Promise<ProcessResult>;
40
+ }
41
+ //# sourceMappingURL=process-executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-executor.d.ts","sourceRoot":"","sources":["../src/process-executor.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtF,MAAM,WAAW,qBAAqB;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC5B,GAAG,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACxD;AAED,qBAAa,mBAAoB,YAAW,eAAe;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,GAAE,qBAA0B;IAEzD,GAAG,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CA2C7D"}
@@ -0,0 +1,70 @@
1
+ import { isatty } from 'node:tty';
2
+ import { execa } from 'execa';
3
+ export class NodeProcessExecutor {
4
+ config;
5
+ constructor(config = {}) {
6
+ this.config = config;
7
+ }
8
+ async run(options) {
9
+ const args = options.args ?? [];
10
+ const execaOptions = buildExecaOptions({
11
+ cwd: options.cwd,
12
+ env: options.env,
13
+ timeout: options.timeout ?? this.config.defaultTimeout,
14
+ maxOutput: options.maxOutput ?? this.config.defaultMaxOutput,
15
+ rejectOnError: options.rejectOnError ?? false,
16
+ outputPolicy: this.config.output,
17
+ forceBuffered: options.forceBuffered ?? false,
18
+ });
19
+ try {
20
+ const result = await execa(options.command, args, execaOptions);
21
+ return {
22
+ command: options.command,
23
+ args,
24
+ exitCode: result.exitCode ?? null,
25
+ stdout: asString(result.stdout),
26
+ stderr: asString(result.stderr),
27
+ ...(result.signalDescription !== undefined ? { signal: result.signalDescription } : {}),
28
+ durationMs: result.durationMs,
29
+ };
30
+ }
31
+ catch (error) {
32
+ if (options.rejectOnError)
33
+ throw error;
34
+ const failed = error;
35
+ return {
36
+ command: options.command,
37
+ args,
38
+ exitCode: failed.exitCode ?? null,
39
+ stdout: asString(failed.stdout),
40
+ stderr: asString(failed.stderr),
41
+ ...(failed.signalDescription !== undefined ? { signal: failed.signalDescription } : {}),
42
+ durationMs: failed.durationMs ?? 0,
43
+ };
44
+ }
45
+ }
46
+ }
47
+ function buildExecaOptions(opts) {
48
+ const canStream = !opts.forceBuffered &&
49
+ opts.outputPolicy?.mode === 'stream' &&
50
+ (opts.outputPolicy.isTTY ?? process.stdout.isTTY ?? isatty(1));
51
+ return {
52
+ reject: opts.rejectOnError,
53
+ stdin: 'ignore',
54
+ stripFinalNewline: true,
55
+ ...(canStream ? { stdout: ['inherit', 'pipe'], stderr: ['inherit', 'pipe'] } : { all: true }),
56
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
57
+ ...(opts.env !== undefined ? { env: opts.env } : {}),
58
+ ...(opts.timeout !== undefined ? { timeout: opts.timeout } : {}),
59
+ ...(opts.maxOutput !== undefined ? { maxBuffer: opts.maxOutput } : {}),
60
+ };
61
+ }
62
+ function asString(value) {
63
+ if (typeof value === 'string')
64
+ return value;
65
+ if (value instanceof Uint8Array)
66
+ return new TextDecoder().decode(value);
67
+ if (Array.isArray(value))
68
+ return value.map(String).join('');
69
+ return '';
70
+ }
@@ -0,0 +1,29 @@
1
+ import type { Config } from './config';
2
+ import type { RuntimeContext } from './context';
3
+ import type { FileSystem } from './fs';
4
+ export type RuntimeName = 'node-bun' | 'cloudflare-workers' | 'test';
5
+ export interface RuntimeCapabilities {
6
+ readonly hasFilesystem: boolean;
7
+ readonly hasProcessExecution: boolean;
8
+ readonly hasPersistentStorage: boolean;
9
+ }
10
+ export interface LoadConfigOptions {
11
+ overrides?: Partial<Config>;
12
+ envBindings?: Record<string, unknown>;
13
+ }
14
+ export interface RuntimeFactory {
15
+ readonly runtimeName: RuntimeName;
16
+ readonly capabilities: RuntimeCapabilities;
17
+ createFileSystem(): FileSystem;
18
+ loadConfig(options?: LoadConfigOptions): Promise<Config>;
19
+ createContext?(options?: {
20
+ scope?: string;
21
+ }): RuntimeContext;
22
+ }
23
+ export interface SpanContext {
24
+ traceId: string;
25
+ spanId: string;
26
+ baggage?: Record<string, string>;
27
+ attributes?: Record<string, string | number | boolean>;
28
+ }
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAEvC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,gBAAgB,IAAI,UAAU,CAAC;IAC/B,UAAU,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,aAAa,CAAC,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,cAAc,CAAC;CAChE;AAED,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CAC1D"}
package/dist/types.js ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@gobing-ai/ts-runtime",
3
+ "version": "0.1.0",
4
+ "description": "@gobing-ai/ts-runtime — Runtime abstractions for Bun, Node, and Cloudflare Workers.",
5
+ "type": "module",
6
+ "private": false,
7
+ "sideEffects": false,
8
+ "license": "Apache-2.0",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.build.json && bun ../../scripts/fix-dist-esm-extensions.ts dist",
24
+ "test": "NODE_ENV=test bun test --coverage --coverage-dir=.coverage --reporter=dots",
25
+ "test:full": "NODE_ENV=test bun test --update-snapshots --coverage --coverage-dir=.coverage",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "biome check . && bun run typecheck",
28
+ "format": "biome check . --write",
29
+ "check": "bun run lint && bun run test",
30
+ "prepublishOnly": "bun run build",
31
+ "release": "npm publish --access public"
32
+ },
33
+ "dependencies": {
34
+ "@gobing-ai/ts-utils": "^0.1.0",
35
+ "execa": "^9.5.0",
36
+ "yaml": "^2.7.0",
37
+ "zod": "^4.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bun": "1.3.14"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
package/src/config.ts ADDED
@@ -0,0 +1,169 @@
1
+ import { parse as parseYaml } from 'yaml';
2
+ import { type ZodIssue, z } from 'zod';
3
+
4
+ export const configSchema = z.object({
5
+ app: z
6
+ .object({
7
+ name: z.string().default('app'),
8
+ env: z.enum(['development', 'staging', 'production', 'test']).default('development'),
9
+ port: z.number().int().positive().default(3000),
10
+ })
11
+ .default({ name: 'app', env: 'development', port: 3000 }),
12
+ database: z
13
+ .object({
14
+ url: z.string().default(':memory:'),
15
+ })
16
+ .default({ url: ':memory:' }),
17
+ logging: z
18
+ .object({
19
+ level: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
20
+ console: z.boolean().default(true),
21
+ file: z.boolean().default(false),
22
+ filePath: z.string().optional(),
23
+ json: z.boolean().default(false),
24
+ })
25
+ .default({ level: 'info', console: true, file: false, json: false }),
26
+ });
27
+
28
+ export type Config = z.output<typeof configSchema>;
29
+
30
+ const ENV_INTERPOLATION_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
31
+
32
+ export class ConfigLoadError extends Error {
33
+ readonly issues: ZodIssue[];
34
+
35
+ constructor(message: string, issues: ZodIssue[] = []) {
36
+ super(message);
37
+ this.name = 'ConfigLoadError';
38
+ this.issues = issues;
39
+ }
40
+ }
41
+
42
+ export function getNodeEnv(): string {
43
+ return process.env.NODE_ENV ?? 'development';
44
+ }
45
+
46
+ export function isTestEnv(): boolean {
47
+ return getNodeEnv() === 'test';
48
+ }
49
+
50
+ export function getDatabaseUrl(): string | undefined {
51
+ return process.env.DATABASE_URL;
52
+ }
53
+
54
+ export function interpolateEnv(value: string): string {
55
+ return value.replace(ENV_INTERPOLATION_RE, (_match, name: string) => process.env[name] ?? `\${${name}}`);
56
+ }
57
+
58
+ export function interpolateTree(value: unknown): unknown {
59
+ if (typeof value === 'string') return interpolateEnv(value);
60
+ if (Array.isArray(value)) return value.map(interpolateTree);
61
+ if (isPlainObject(value)) {
62
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, interpolateTree(child)]));
63
+ }
64
+ return value;
65
+ }
66
+
67
+ export function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
68
+ const result = { ...target };
69
+ for (const [key, value] of Object.entries(source)) {
70
+ if (isPlainObject(value) && isPlainObject(result[key])) {
71
+ result[key] = deepMerge(result[key] as Record<string, unknown>, value);
72
+ } else {
73
+ result[key] = value;
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+
79
+ export function flattenKeys(obj: Record<string, unknown>, prefix = ''): Record<string, string> {
80
+ const result: Record<string, string> = {};
81
+ for (const [key, value] of Object.entries(obj)) {
82
+ const fullKey = prefix ? `${prefix}.${key}` : key;
83
+ if (isPlainObject(value)) {
84
+ Object.assign(result, flattenKeys(value, fullKey));
85
+ } else {
86
+ result[fullKey] = JSON.stringify(value);
87
+ }
88
+ }
89
+ return result;
90
+ }
91
+
92
+ export function deFlattenKeys(entries: Record<string, string>): Record<string, unknown> {
93
+ const result: Record<string, unknown> = {};
94
+ for (const [key, rawValue] of Object.entries(entries)) {
95
+ const parts = key.split('.');
96
+ let current = result;
97
+ for (const part of parts.slice(0, -1)) {
98
+ if (!isPlainObject(current[part])) current[part] = {};
99
+ current = current[part] as Record<string, unknown>;
100
+ }
101
+
102
+ const last = parts.at(-1);
103
+ if (last === undefined) continue;
104
+ current[last] = parseConfigValue(rawValue);
105
+ }
106
+ return result;
107
+ }
108
+
109
+ export function buildConfigFromObject(
110
+ raw: Record<string, unknown>,
111
+ options: { overrides?: Partial<Config> } = {},
112
+ ): Config {
113
+ const interpolated = interpolateTree(raw) as Record<string, unknown>;
114
+ const merged = options.overrides
115
+ ? deepMerge(interpolated, options.overrides as unknown as Record<string, unknown>)
116
+ : interpolated;
117
+ const result = configSchema.safeParse(merged);
118
+ if (!result.success) {
119
+ throw new ConfigLoadError('Config validation failed', result.error.issues);
120
+ }
121
+ return deepFreeze(result.data);
122
+ }
123
+
124
+ export function parseConfigYaml(yamlText: string): Record<string, unknown> {
125
+ try {
126
+ const parsed = parseYaml(yamlText);
127
+ if (parsed === null || parsed === undefined) return {};
128
+ if (!isPlainObject(parsed)) {
129
+ throw new ConfigLoadError('Config YAML must parse to an object');
130
+ }
131
+ return parsed;
132
+ } catch (error) {
133
+ if (error instanceof ConfigLoadError) throw error;
134
+ throw new ConfigLoadError(`Config YAML parsing failed: ${(error as Error).message}`);
135
+ }
136
+ }
137
+
138
+ export function buildConfigFromYaml(yamlText: string, options: { overrides?: Partial<Config> } = {}): Config {
139
+ return buildConfigFromObject(parseConfigYaml(yamlText), options);
140
+ }
141
+
142
+ function parseConfigValue(value: string): unknown {
143
+ try {
144
+ return JSON.parse(value);
145
+ } catch {
146
+ return value;
147
+ }
148
+ }
149
+
150
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
151
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
152
+ }
153
+
154
+ function deepFreeze<T extends Record<string, unknown>>(obj: T): T {
155
+ Object.freeze(obj);
156
+ for (const value of Object.values(obj)) {
157
+ if (Array.isArray(value)) {
158
+ Object.freeze(value);
159
+ for (const item of value) {
160
+ if (isPlainObject(item) && !Object.isFrozen(item)) {
161
+ deepFreeze(item as Record<string, unknown>);
162
+ }
163
+ }
164
+ } else if (isPlainObject(value) && !Object.isFrozen(value)) {
165
+ deepFreeze(value);
166
+ }
167
+ }
168
+ return obj;
169
+ }
package/src/context.ts ADDED
@@ -0,0 +1,97 @@
1
+ import type { Config } from './config';
2
+ import { buildConfigFromObject } from './config';
3
+ import type { FileSystem } from './fs';
4
+ import { getFs } from './fs';
5
+ import type { RuntimeCapabilities, RuntimeFactory, RuntimeName } from './types';
6
+
7
+ export type RuntimeScope = 'process' | 'server-request' | 'scheduled-event' | 'test';
8
+
9
+ export interface RuntimeServiceMap {
10
+ config: Config;
11
+ fileSystem: FileSystem;
12
+ [serviceName: string]: unknown;
13
+ }
14
+
15
+ export interface RuntimeContextOptions<TServices extends RuntimeServiceMap = RuntimeServiceMap> {
16
+ scope?: RuntimeScope;
17
+ runtimeName?: RuntimeName;
18
+ capabilities?: RuntimeCapabilities;
19
+ services?: Partial<TServices>;
20
+ factory?: RuntimeFactory;
21
+ }
22
+
23
+ export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeServiceMap> {
24
+ readonly scope: RuntimeScope;
25
+ readonly runtimeName: RuntimeName;
26
+ readonly capabilities: RuntimeCapabilities;
27
+ readonly services = new Map<keyof TServices, TServices[keyof TServices]>();
28
+
29
+ constructor(options: RuntimeContextOptions<TServices> = {}) {
30
+ this.scope = options.scope ?? 'process';
31
+ this.runtimeName = options.runtimeName ?? options.factory?.runtimeName ?? 'node-bun';
32
+ this.capabilities =
33
+ options.capabilities ??
34
+ options.factory?.capabilities ??
35
+ ({
36
+ hasFilesystem: true,
37
+ hasProcessExecution: true,
38
+ hasPersistentStorage: true,
39
+ } satisfies RuntimeCapabilities);
40
+
41
+ this.register('config', (options.services?.config ?? buildConfigFromObject({})) as TServices['config']);
42
+ this.register('fileSystem', (options.services?.fileSystem ?? getFs()) as TServices['fileSystem']);
43
+
44
+ for (const [key, value] of Object.entries(options.services ?? {})) {
45
+ if (value !== undefined) {
46
+ this.register(key as keyof TServices, value as TServices[keyof TServices]);
47
+ }
48
+ }
49
+ }
50
+
51
+ register<K extends keyof TServices>(key: K, service: TServices[K]): this {
52
+ this.services.set(key, service);
53
+ return this;
54
+ }
55
+
56
+ get<K extends keyof TServices>(key: K): TServices[K] | undefined {
57
+ return this.services.get(key) as TServices[K] | undefined;
58
+ }
59
+
60
+ require<K extends keyof TServices>(key: K): TServices[K] {
61
+ const service = this.get(key);
62
+ if (service === undefined) {
63
+ throw new Error(`Runtime service "${String(key)}" is unavailable for runtime ${this.runtimeName}.`);
64
+ }
65
+ return service;
66
+ }
67
+
68
+ has<K extends keyof TServices>(key: K): boolean {
69
+ return this.services.has(key);
70
+ }
71
+
72
+ async dispose(): Promise<void> {
73
+ const errors: Error[] = [];
74
+ for (const service of this.services.values()) {
75
+ if (isDisposable(service)) {
76
+ try {
77
+ await service.dispose();
78
+ } catch (error) {
79
+ errors.push(error instanceof Error ? error : new Error(String(error)));
80
+ }
81
+ }
82
+ }
83
+ if (errors.length > 0) {
84
+ throw new Error(`Failed to dispose some services:\n${errors.map((e) => `- ${e.message}`).join('\n')}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ function isDisposable(value: unknown): value is { dispose(): void | Promise<void> } {
90
+ return typeof value === 'object' && value !== null && 'dispose' in value && typeof value.dispose === 'function';
91
+ }
92
+
93
+ export function createRuntimeContext<TServices extends RuntimeServiceMap = RuntimeServiceMap>(
94
+ options: RuntimeContextOptions<TServices> = {},
95
+ ): RuntimeContext<TServices> {
96
+ return new RuntimeContext<TServices>(options);
97
+ }