@gobing-ai/ts-runtime 0.2.8 → 0.3.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/README.md +116 -18
- package/dist/config.d.ts +10 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -58
- package/dist/context.d.ts +1 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +1 -2
- package/dist/fs.d.ts +19 -0
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +59 -64
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/path.d.ts +7 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +55 -0
- package/dist/process-executor.d.ts +36 -0
- package/dist/process-executor.d.ts.map +1 -1
- package/dist/process-executor.js +74 -0
- package/dist/schema-validation.d.ts +1 -1
- package/dist/schema-validation.d.ts.map +1 -1
- package/dist/schema-validation.js +70 -21
- package/dist/types.d.ts +0 -11
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/config.ts +41 -57
- package/src/context.ts +2 -4
- package/src/fs.ts +75 -59
- package/src/index.ts +1 -0
- package/src/path.ts +54 -0
- package/src/process-executor.ts +128 -0
- package/src/schema-validation.ts +84 -16
- package/src/types.ts +0 -10
package/dist/index.js
CHANGED
package/dist/path.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function normalizeSeparators(path: string): string;
|
|
2
|
+
export declare function isAbsolutePath(path: string): boolean;
|
|
3
|
+
export declare function dirnamePath(path: string): string;
|
|
4
|
+
export declare function joinPath(...segments: string[]): string;
|
|
5
|
+
export declare function resolvePath(...segments: string[]): string;
|
|
6
|
+
export declare function getProcessCwd(): string;
|
|
7
|
+
//# sourceMappingURL=path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAIA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAShD;AAED,wBAAgB,QAAQ,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAMtD;AAED,wBAAgB,WAAW,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAkBzD;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
package/dist/path.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Runtime-portable path math. Deliberately avoids `node:path` so the same logic works on
|
|
2
|
+
// `cloudflare-workers` (no `node:*`) as on node-bun (ADR-008). POSIX-style separators throughout;
|
|
3
|
+
// Windows drive paths (`C:/...`) are normalized and treated as absolute.
|
|
4
|
+
export function normalizeSeparators(path) {
|
|
5
|
+
return path.replaceAll('\\', '/');
|
|
6
|
+
}
|
|
7
|
+
export function isAbsolutePath(path) {
|
|
8
|
+
return path.startsWith('/') || /^[A-Za-z]:\//.test(normalizeSeparators(path));
|
|
9
|
+
}
|
|
10
|
+
export function dirnamePath(path) {
|
|
11
|
+
const input = normalizeSeparators(path);
|
|
12
|
+
if (/^\/+$/.test(input))
|
|
13
|
+
return '/';
|
|
14
|
+
const normalized = input.replace(/\/+$/, '');
|
|
15
|
+
if (normalized === '' || normalized === '/')
|
|
16
|
+
return normalized || '.';
|
|
17
|
+
const index = normalized.lastIndexOf('/');
|
|
18
|
+
if (index < 0)
|
|
19
|
+
return '.';
|
|
20
|
+
if (index === 0)
|
|
21
|
+
return '/';
|
|
22
|
+
return normalized.slice(0, index);
|
|
23
|
+
}
|
|
24
|
+
export function joinPath(...segments) {
|
|
25
|
+
const filtered = segments.filter((segment) => segment.length > 0).map(normalizeSeparators);
|
|
26
|
+
if (filtered.length === 0)
|
|
27
|
+
return '.';
|
|
28
|
+
const absolute = isAbsolutePath(filtered[0] ?? '');
|
|
29
|
+
const joined = filtered.join('/').replace(/\/+/g, '/');
|
|
30
|
+
return absolute ? joined : joined.replace(/^\//, '');
|
|
31
|
+
}
|
|
32
|
+
export function resolvePath(...segments) {
|
|
33
|
+
const candidates = segments.length === 0 ? [getProcessCwd()] : segments;
|
|
34
|
+
let resolved = '';
|
|
35
|
+
for (const segment of candidates.map(normalizeSeparators)) {
|
|
36
|
+
if (segment.length === 0)
|
|
37
|
+
continue;
|
|
38
|
+
resolved = isAbsolutePath(segment) ? segment : joinPath(resolved || getProcessCwd(), segment);
|
|
39
|
+
}
|
|
40
|
+
const parts = [];
|
|
41
|
+
const absolute = isAbsolutePath(resolved);
|
|
42
|
+
for (const part of resolved.split('/')) {
|
|
43
|
+
if (part === '' || part === '.')
|
|
44
|
+
continue;
|
|
45
|
+
if (part === '..') {
|
|
46
|
+
parts.pop();
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
parts.push(part);
|
|
50
|
+
}
|
|
51
|
+
return `${absolute ? '/' : ''}${parts.join('/')}` || (absolute ? '/' : '.');
|
|
52
|
+
}
|
|
53
|
+
export function getProcessCwd() {
|
|
54
|
+
return globalThis.process?.cwd?.() ?? '/';
|
|
55
|
+
}
|
|
@@ -13,6 +13,11 @@ export interface ProcessOptions {
|
|
|
13
13
|
command: string;
|
|
14
14
|
args?: string[];
|
|
15
15
|
cwd?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Environment forwarded verbatim to the child process. Caller-controlled — when launching an
|
|
18
|
+
* untrusted command, pass an explicit allowlist rather than the parent's full environment, so
|
|
19
|
+
* inherited secrets are not leaked into the subprocess.
|
|
20
|
+
*/
|
|
16
21
|
env?: Record<string, string>;
|
|
17
22
|
timeout?: number;
|
|
18
23
|
/** Maximum output buffer size in bytes (maps to execa `maxBuffer`). */
|
|
@@ -33,9 +38,40 @@ export interface ProcessResult {
|
|
|
33
38
|
export interface ProcessExecutor {
|
|
34
39
|
run(options: ProcessOptions): Promise<ProcessResult>;
|
|
35
40
|
}
|
|
41
|
+
export interface SyncProcessExecutor {
|
|
42
|
+
runSync(options: Omit<ProcessOptions, 'timeout'>): ProcessResult;
|
|
43
|
+
}
|
|
44
|
+
export interface PipeProcessOptions {
|
|
45
|
+
command: string;
|
|
46
|
+
args?: string[];
|
|
47
|
+
cwd?: string;
|
|
48
|
+
/** Forwarded verbatim to the child — pass an allowlist for untrusted commands (see {@link ProcessOptions.env}). */
|
|
49
|
+
env?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
export interface PipeProcess {
|
|
52
|
+
readonly pid: number | null;
|
|
53
|
+
readonly stdout: ReadableStream<Uint8Array> | null;
|
|
54
|
+
readonly stderr: ReadableStream<Uint8Array> | null;
|
|
55
|
+
readonly exited: Promise<number | null>;
|
|
56
|
+
writeStdin(input: string | Uint8Array): void;
|
|
57
|
+
endStdin(): void;
|
|
58
|
+
kill(signal?: ProcessSignal): void;
|
|
59
|
+
}
|
|
60
|
+
export interface PipeProcessSpawner {
|
|
61
|
+
spawn(options: PipeProcessOptions): PipeProcess;
|
|
62
|
+
}
|
|
36
63
|
export declare class NodeProcessExecutor implements ProcessExecutor {
|
|
37
64
|
private readonly config;
|
|
38
65
|
constructor(config?: ProcessExecutorConfig);
|
|
39
66
|
run(options: ProcessOptions): Promise<ProcessResult>;
|
|
40
67
|
}
|
|
68
|
+
export declare class BunSyncProcessExecutor implements SyncProcessExecutor {
|
|
69
|
+
runSync(options: Omit<ProcessOptions, 'timeout'>): ProcessResult;
|
|
70
|
+
}
|
|
71
|
+
export declare class BunPipeProcessSpawner implements PipeProcessSpawner {
|
|
72
|
+
spawn(options: PipeProcessOptions): PipeProcess;
|
|
73
|
+
}
|
|
74
|
+
type BunSubprocess = ReturnType<typeof Bun.spawn>;
|
|
75
|
+
type ProcessSignal = Parameters<BunSubprocess['kill']>[0];
|
|
76
|
+
export {};
|
|
41
77
|
//# sourceMappingURL=process-executor.d.ts.map
|
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"process-executor.d.ts","sourceRoot":"","sources":["../src/process-executor.ts"],"names":[],"mappings":"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"}
|
package/dist/process-executor.js
CHANGED
|
@@ -44,6 +44,74 @@ export class NodeProcessExecutor {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
export class BunSyncProcessExecutor {
|
|
48
|
+
runSync(options) {
|
|
49
|
+
const args = options.args ?? [];
|
|
50
|
+
const startedAt = Date.now();
|
|
51
|
+
const result = Bun.spawnSync({
|
|
52
|
+
cmd: [options.command, ...args],
|
|
53
|
+
stdout: 'pipe',
|
|
54
|
+
stderr: 'pipe',
|
|
55
|
+
stdin: 'ignore',
|
|
56
|
+
...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
|
|
57
|
+
...(options.env !== undefined ? { env: options.env } : {}),
|
|
58
|
+
});
|
|
59
|
+
if (options.rejectOnError === true && result.exitCode !== 0) {
|
|
60
|
+
throw new Error(`${options.command} ${args.join(' ')} failed with exit code ${result.exitCode}: ${stripFinalNewline(asString(result.stderr))}`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
command: options.command,
|
|
64
|
+
args,
|
|
65
|
+
exitCode: result.exitCode,
|
|
66
|
+
stdout: stripFinalNewline(asString(result.stdout)),
|
|
67
|
+
stderr: stripFinalNewline(asString(result.stderr)),
|
|
68
|
+
durationMs: Date.now() - startedAt,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class BunPipeProcessSpawner {
|
|
73
|
+
spawn(options) {
|
|
74
|
+
const subprocess = Bun.spawn({
|
|
75
|
+
cmd: [options.command, ...(options.args ?? [])],
|
|
76
|
+
stdin: 'pipe',
|
|
77
|
+
stdout: 'pipe',
|
|
78
|
+
stderr: 'pipe',
|
|
79
|
+
...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
|
|
80
|
+
...(options.env !== undefined ? { env: options.env } : {}),
|
|
81
|
+
});
|
|
82
|
+
return new BunPipeProcess(subprocess);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
class BunPipeProcess {
|
|
86
|
+
subprocess;
|
|
87
|
+
writer;
|
|
88
|
+
constructor(subprocess) {
|
|
89
|
+
this.subprocess = subprocess;
|
|
90
|
+
this.writer = subprocess.stdin;
|
|
91
|
+
}
|
|
92
|
+
get pid() {
|
|
93
|
+
return this.subprocess.pid ?? null;
|
|
94
|
+
}
|
|
95
|
+
get stdout() {
|
|
96
|
+
return isReadableStream(this.subprocess.stdout) ? this.subprocess.stdout : null;
|
|
97
|
+
}
|
|
98
|
+
get stderr() {
|
|
99
|
+
return isReadableStream(this.subprocess.stderr) ? this.subprocess.stderr : null;
|
|
100
|
+
}
|
|
101
|
+
get exited() {
|
|
102
|
+
return this.subprocess.exited;
|
|
103
|
+
}
|
|
104
|
+
writeStdin(input) {
|
|
105
|
+
this.writer.write(input);
|
|
106
|
+
this.writer.flush?.();
|
|
107
|
+
}
|
|
108
|
+
endStdin() {
|
|
109
|
+
this.writer.end?.();
|
|
110
|
+
}
|
|
111
|
+
kill(signal) {
|
|
112
|
+
this.subprocess.kill(signal);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
47
115
|
function buildExecaOptions(opts) {
|
|
48
116
|
const canStream = !opts.forceBuffered &&
|
|
49
117
|
opts.outputPolicy?.mode === 'stream' &&
|
|
@@ -68,3 +136,9 @@ function asString(value) {
|
|
|
68
136
|
return value.map(String).join('');
|
|
69
137
|
return '';
|
|
70
138
|
}
|
|
139
|
+
function stripFinalNewline(value) {
|
|
140
|
+
return value.endsWith('\r\n') ? value.slice(0, -2) : value.endsWith('\n') ? value.slice(0, -1) : value;
|
|
141
|
+
}
|
|
142
|
+
function isReadableStream(value) {
|
|
143
|
+
return value instanceof ReadableStream;
|
|
144
|
+
}
|
|
@@ -38,5 +38,5 @@ export declare class StructuredConfigSchemaError extends Error {
|
|
|
38
38
|
export declare function loadStructuredConfig(path: string, options?: StructuredConfigLoadOptions): Promise<unknown>;
|
|
39
39
|
export declare function parseStructuredConfig(content: string, source: string, options?: StructuredConfigLoadOptions): Promise<unknown>;
|
|
40
40
|
export declare function validateDeclaredJsonSchema(value: unknown, source: string, options?: StructuredConfigLoadOptions): Promise<void>;
|
|
41
|
-
export declare function validateJsonSchema(value: unknown, schema: JsonSchema, path?: string, defs?: Record<string, JsonSchema>): JsonSchemaViolation[];
|
|
41
|
+
export declare function validateJsonSchema(value: unknown, schema: JsonSchema, path?: string, defs?: Record<string, JsonSchema>, seenRefs?: ReadonlySet<string>): JsonSchemaViolation[];
|
|
42
42
|
//# sourceMappingURL=schema-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-validation.d.ts","sourceRoot":"","sources":["../src/schema-validation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema-validation.d.ts","sourceRoot":"","sources":["../src/schema-validation.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,oBAAoB,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC5C,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,2BAA2B;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7C;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACzD;AAED,qBAAa,2BAA4B,SAAQ,KAAK;IAG9C,QAAQ,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE;gBADnD,OAAO,EAAE,MAAM,EACN,UAAU,GAAE,SAAS,mBAAmB,EAAO;CAK/D;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,2BAAgC,GAAG,OAAO,CAAC,OAAO,CAAC,CAGpH;AAED,wBAAsB,qBAAqB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,wBAAsB,0BAA0B,CAC5C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,wBAAgB,kBAAkB,CAC9B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,IAAI,SAAK,EACT,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAM,EACrC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GAC1C,mBAAmB,EAAE,CAwDvB"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { dirname, isAbsolute, join } from 'node:path';
|
|
2
1
|
import { parse as parseYaml } from 'yaml';
|
|
3
2
|
import { getFs } from './fs.js';
|
|
3
|
+
import { dirnamePath, isAbsolutePath, joinPath } from './path.js';
|
|
4
4
|
/** Default time budget for a single remote schema fetch. */
|
|
5
5
|
const REMOTE_SCHEMA_FETCH_TIMEOUT_MS = 5_000;
|
|
6
|
+
/** Upper bound on a remote schema body. A timeout alone lets a slow multi-GB drip exhaust memory. */
|
|
7
|
+
const REMOTE_SCHEMA_MAX_BYTES = 5 * 1024 * 1024;
|
|
6
8
|
export class StructuredConfigSchemaError extends Error {
|
|
7
9
|
violations;
|
|
8
10
|
constructor(message, violations = []) {
|
|
@@ -47,20 +49,27 @@ export async function validateDeclaredJsonSchema(value, source, options = {}) {
|
|
|
47
49
|
.join('; ')}`, violations);
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
|
-
export function validateJsonSchema(value, schema, path = '', defs = {}) {
|
|
52
|
+
export function validateJsonSchema(value, schema, path = '', defs = {}, seenRefs = new Set()) {
|
|
51
53
|
const violations = [];
|
|
52
54
|
// Applicator keywords compose with their siblings (logical AND), per JSON Schema 2020-12 —
|
|
53
55
|
// a node may carry `$ref`/`oneOf`/`anyOf` *and* `type`/`properties`/... and all apply.
|
|
54
56
|
if (schema.$ref !== undefined) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
// Cyclic `$ref` (A→B→A) would recurse until stack overflow — a DoS surface for untrusted
|
|
58
|
+
// schemas. Following a ref already on the current path is a no-op: that branch is being
|
|
59
|
+
// validated higher up the stack, so re-entering adds nothing but unbounded depth.
|
|
60
|
+
if (!seenRefs.has(schema.$ref)) {
|
|
61
|
+
const resolved = resolveRef(schema.$ref, defs, schema.$defs);
|
|
62
|
+
if (resolved !== undefined) {
|
|
63
|
+
const nextSeen = new Set(seenRefs).add(schema.$ref);
|
|
64
|
+
violations.push(...validateJsonSchema(value, resolved, path, defs, nextSeen));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
if (schema.oneOf !== undefined) {
|
|
60
|
-
violations.push(...validateCombinator(value, schema.oneOf, path, defs, 'oneOf'));
|
|
69
|
+
violations.push(...validateCombinator(value, schema.oneOf, path, defs, 'oneOf', seenRefs));
|
|
61
70
|
}
|
|
62
71
|
if (schema.anyOf !== undefined) {
|
|
63
|
-
violations.push(...validateCombinator(value, schema.anyOf, path, defs, 'anyOf'));
|
|
72
|
+
violations.push(...validateCombinator(value, schema.anyOf, path, defs, 'anyOf', seenRefs));
|
|
64
73
|
}
|
|
65
74
|
if (schema.const !== undefined && !jsonEqual(value, schema.const)) {
|
|
66
75
|
violations.push({ path: path || '(root)', message: `expected constant ${JSON.stringify(schema.const)}` });
|
|
@@ -80,14 +89,14 @@ export function validateJsonSchema(value, schema, path = '', defs = {}) {
|
|
|
80
89
|
}
|
|
81
90
|
const hasObjectKeywords = schema.properties !== undefined || schema.required !== undefined || schema.additionalProperties !== undefined;
|
|
82
91
|
if (schema.type === 'object' || (hasObjectKeywords && isObject(value))) {
|
|
83
|
-
violations.push(...validateObject(value, schema, path, defs));
|
|
92
|
+
violations.push(...validateObject(value, schema, path, defs, seenRefs));
|
|
84
93
|
}
|
|
85
94
|
if (schema.type === 'array' || (schema.items !== undefined && Array.isArray(value))) {
|
|
86
|
-
violations.push(...validateArray(value, schema, path, defs));
|
|
95
|
+
violations.push(...validateArray(value, schema, path, defs, seenRefs));
|
|
87
96
|
}
|
|
88
97
|
return violations;
|
|
89
98
|
}
|
|
90
|
-
function validateObject(value, schema, path, defs) {
|
|
99
|
+
function validateObject(value, schema, path, defs, seenRefs) {
|
|
91
100
|
if (!isObject(value))
|
|
92
101
|
return [{ path: path || '(root)', message: `expected object, got ${typeName(value)}` }];
|
|
93
102
|
const violations = [];
|
|
@@ -99,7 +108,7 @@ function validateObject(value, schema, path, defs) {
|
|
|
99
108
|
const properties = schema.properties ?? {};
|
|
100
109
|
for (const [key, childSchema] of Object.entries(properties)) {
|
|
101
110
|
if (key in value) {
|
|
102
|
-
violations.push(...validateJsonSchema(value[key], childSchema, path ? `${path}.${key}` : key, defs));
|
|
111
|
+
violations.push(...validateJsonSchema(value[key], childSchema, path ? `${path}.${key}` : key, defs, seenRefs));
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
114
|
if (schema.additionalProperties === false) {
|
|
@@ -113,21 +122,21 @@ function validateObject(value, schema, path, defs) {
|
|
|
113
122
|
else if (isObject(schema.additionalProperties)) {
|
|
114
123
|
for (const [key, child] of Object.entries(value)) {
|
|
115
124
|
if (!(key in properties)) {
|
|
116
|
-
violations.push(...validateJsonSchema(child, schema.additionalProperties, path ? `${path}.${key}` : key, defs));
|
|
125
|
+
violations.push(...validateJsonSchema(child, schema.additionalProperties, path ? `${path}.${key}` : key, defs, seenRefs));
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
return violations;
|
|
121
130
|
}
|
|
122
|
-
function validateArray(value, schema, path, defs) {
|
|
131
|
+
function validateArray(value, schema, path, defs, seenRefs) {
|
|
123
132
|
if (!Array.isArray(value))
|
|
124
133
|
return [{ path: path || '(root)', message: `expected array, got ${typeName(value)}` }];
|
|
125
134
|
if (schema.items === undefined)
|
|
126
135
|
return [];
|
|
127
|
-
return value.flatMap((entry, index) => validateJsonSchema(entry, schema.items, `${path}[${index}]`, defs));
|
|
136
|
+
return value.flatMap((entry, index) => validateJsonSchema(entry, schema.items, `${path}[${index}]`, defs, seenRefs));
|
|
128
137
|
}
|
|
129
|
-
function validateCombinator(value, schemas, path, defs, mode) {
|
|
130
|
-
const branchViolations = schemas.map((schema) => validateJsonSchema(value, schema, path, defs));
|
|
138
|
+
function validateCombinator(value, schemas, path, defs, mode, seenRefs) {
|
|
139
|
+
const branchViolations = schemas.map((schema) => validateJsonSchema(value, schema, path, defs, seenRefs));
|
|
131
140
|
const passing = branchViolations.filter((violations) => violations.length === 0).length;
|
|
132
141
|
if (mode === 'anyOf' && passing >= 1)
|
|
133
142
|
return [];
|
|
@@ -156,11 +165,11 @@ function resolveRef(ref, defs, localDefs) {
|
|
|
156
165
|
return defs[name] ?? localDefs?.[name];
|
|
157
166
|
}
|
|
158
167
|
function resolveSchemaRef(schemaRef, source, resolve) {
|
|
159
|
-
if (isRemoteRef(schemaRef) ||
|
|
168
|
+
if (isRemoteRef(schemaRef) || isAbsolutePath(schemaRef))
|
|
160
169
|
return schemaRef;
|
|
161
170
|
// Relative ref — resolve against the config file's directory.
|
|
162
171
|
if (schemaRef.startsWith('./') || schemaRef.startsWith('../')) {
|
|
163
|
-
return
|
|
172
|
+
return joinPath(isRemoteRef(source) ? '.' : dirnamePath(source), schemaRef);
|
|
164
173
|
}
|
|
165
174
|
// Bare package specifier (e.g. "@scope/pkg/schemas/x.json") — resolve through node_modules.
|
|
166
175
|
return resolvePackageSchema(schemaRef, source, resolve);
|
|
@@ -174,12 +183,12 @@ function resolvePackageSchema(specifier, source, resolve) {
|
|
|
174
183
|
if (subpath.length === 0) {
|
|
175
184
|
throw new StructuredConfigSchemaError(`Package schema ref "${specifier}" referenced by "${source}" must include a path within the package`);
|
|
176
185
|
}
|
|
177
|
-
const from = isRemoteRef(source) ? process.cwd() :
|
|
186
|
+
const from = isRemoteRef(source) ? process.cwd() : dirnamePath(source);
|
|
178
187
|
try {
|
|
179
188
|
// Resolve the package root via its always-present package.json, then join the subpath.
|
|
180
189
|
// This sidesteps `exports` gating on arbitrary JSON subpaths.
|
|
181
190
|
const manifest = resolveFn(`${pkg}/package.json`, from);
|
|
182
|
-
return
|
|
191
|
+
return joinPath(dirnamePath(manifest), subpath);
|
|
183
192
|
}
|
|
184
193
|
catch (error) {
|
|
185
194
|
throw new StructuredConfigSchemaError(`Cannot resolve package schema "${specifier}" referenced by "${source}": ${errorMessage(error)}`);
|
|
@@ -200,10 +209,50 @@ async function readSchema(schemaLocation, options) {
|
|
|
200
209
|
if (!response.ok) {
|
|
201
210
|
throw new StructuredConfigSchemaError(`Failed to fetch JSON schema "${schemaLocation}": HTTP ${response.status}`);
|
|
202
211
|
}
|
|
203
|
-
return await response
|
|
212
|
+
return await readBoundedBody(response, schemaLocation);
|
|
204
213
|
}
|
|
205
214
|
return await getFs().readFile(schemaLocation);
|
|
206
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Read a response body under a hard byte cap. `Content-Length` is a fast-path reject, but servers
|
|
218
|
+
* lie or omit it, so the stream is also tallied chunk-by-chunk — a multi-GB drip is aborted before
|
|
219
|
+
* it can exhaust memory. Falls back to `.text()` only when the body is not a readable stream.
|
|
220
|
+
*/
|
|
221
|
+
async function readBoundedBody(response, schemaLocation) {
|
|
222
|
+
const declared = Number(response.headers.get('content-length'));
|
|
223
|
+
if (Number.isFinite(declared) && declared > REMOTE_SCHEMA_MAX_BYTES) {
|
|
224
|
+
throw new StructuredConfigSchemaError(`Remote JSON schema "${schemaLocation}" exceeds the ${REMOTE_SCHEMA_MAX_BYTES}-byte limit (Content-Length ${declared})`);
|
|
225
|
+
}
|
|
226
|
+
const body = response.body;
|
|
227
|
+
if (body === null)
|
|
228
|
+
return await response.text();
|
|
229
|
+
const reader = body.getReader();
|
|
230
|
+
const chunks = [];
|
|
231
|
+
let total = 0;
|
|
232
|
+
try {
|
|
233
|
+
while (true) {
|
|
234
|
+
const { done, value } = await reader.read();
|
|
235
|
+
if (done)
|
|
236
|
+
break;
|
|
237
|
+
total += value.byteLength;
|
|
238
|
+
if (total > REMOTE_SCHEMA_MAX_BYTES) {
|
|
239
|
+
await reader.cancel();
|
|
240
|
+
throw new StructuredConfigSchemaError(`Remote JSON schema "${schemaLocation}" exceeds the ${REMOTE_SCHEMA_MAX_BYTES}-byte limit`);
|
|
241
|
+
}
|
|
242
|
+
chunks.push(value);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
reader.releaseLock();
|
|
247
|
+
}
|
|
248
|
+
const merged = new Uint8Array(total);
|
|
249
|
+
let offset = 0;
|
|
250
|
+
for (const chunk of chunks) {
|
|
251
|
+
merged.set(chunk, offset);
|
|
252
|
+
offset += chunk.byteLength;
|
|
253
|
+
}
|
|
254
|
+
return new TextDecoder().decode(merged);
|
|
255
|
+
}
|
|
207
256
|
const defaultResolve = typeof Bun !== 'undefined' ? (specifier, from) => Bun.resolveSync(specifier, from) : undefined;
|
|
208
257
|
/** Default remote fetch, time-bounded so a slow/hung schema host cannot stall config loading. */
|
|
209
258
|
function boundedFetch(input) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { Config } from './config';
|
|
2
|
-
import type { RuntimeContext } from './context';
|
|
3
|
-
import type { FileSystem } from './fs';
|
|
4
2
|
export type RuntimeName = 'node-bun' | 'cloudflare-workers' | 'test';
|
|
5
3
|
export interface RuntimeCapabilities {
|
|
6
4
|
readonly hasFilesystem: boolean;
|
|
@@ -11,15 +9,6 @@ export interface LoadConfigOptions {
|
|
|
11
9
|
overrides?: Partial<Config>;
|
|
12
10
|
envBindings?: Record<string, unknown>;
|
|
13
11
|
}
|
|
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
12
|
export interface SpanContext {
|
|
24
13
|
traceId: string;
|
|
25
14
|
spanId: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,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,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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "@gobing-ai/ts-runtime — Runtime abstractions for Bun, Node, and Cloudflare Workers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-runtime-v<version> && git push --tags' && exit 1"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@gobing-ai/ts-utils": "^0.
|
|
57
|
+
"@gobing-ai/ts-utils": "^0.3.0",
|
|
58
58
|
"execa": "^9.5.0",
|
|
59
59
|
"yaml": "^2.7.0",
|
|
60
60
|
"zod": "^4.1.0"
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { deepMerge } from '@gobing-ai/ts-utils';
|
|
2
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
2
3
|
import { type ZodIssue, z } from 'zod';
|
|
3
4
|
|
|
4
5
|
export const configSchema = z.object({
|
|
@@ -29,6 +30,40 @@ export type Config = z.output<typeof configSchema>;
|
|
|
29
30
|
|
|
30
31
|
const ENV_INTERPOLATION_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
31
32
|
|
|
33
|
+
/** Raised when a YAML text cannot be parsed as a plain object. */
|
|
34
|
+
export class YamlParseError extends Error {
|
|
35
|
+
constructor(
|
|
36
|
+
message: string,
|
|
37
|
+
readonly innerError?: unknown,
|
|
38
|
+
) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'YamlParseError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse YAML text into a plain object. Returns `{}` for null/undefined/empty input. */
|
|
45
|
+
export function parseYamlObject(text: string): Record<string, unknown> {
|
|
46
|
+
let parsed: unknown;
|
|
47
|
+
try {
|
|
48
|
+
parsed = parseYaml(text);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new YamlParseError(
|
|
51
|
+
`YAML parsing failed: ${(error as Error).message}`,
|
|
52
|
+
error instanceof Error ? error : undefined,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (parsed === null || parsed === undefined) return {};
|
|
56
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
57
|
+
throw new YamlParseError('YAML must parse to an object');
|
|
58
|
+
}
|
|
59
|
+
return parsed as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Serialize a plain object to a YAML string. */
|
|
63
|
+
export function stringifyYamlObject(value: Record<string, unknown>): string {
|
|
64
|
+
return stringifyYaml(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
export class ConfigLoadError extends Error {
|
|
33
68
|
readonly issues: ZodIssue[];
|
|
34
69
|
|
|
@@ -39,6 +74,9 @@ export class ConfigLoadError extends Error {
|
|
|
39
74
|
}
|
|
40
75
|
}
|
|
41
76
|
|
|
77
|
+
// These accessors read `process.env` directly and are node-bun only (ADR-008). On
|
|
78
|
+
// `cloudflare-workers` there is no `process`; inject config explicitly rather than calling these.
|
|
79
|
+
|
|
42
80
|
export function getNodeEnv(): string {
|
|
43
81
|
return process.env.NODE_ENV ?? 'development';
|
|
44
82
|
}
|
|
@@ -55,6 +93,7 @@ export function getDatabaseUrl(): string | undefined {
|
|
|
55
93
|
return process.env.DATABASE_URL;
|
|
56
94
|
}
|
|
57
95
|
|
|
96
|
+
/** Node-bun only: interpolates `${VAR}` from `process.env` (see note above). */
|
|
58
97
|
export function interpolateEnv(value: string): string {
|
|
59
98
|
return value.replace(ENV_INTERPOLATION_RE, (_match, name: string) => process.env[name] ?? `\${${name}}`);
|
|
60
99
|
}
|
|
@@ -68,48 +107,6 @@ export function interpolateTree(value: unknown): unknown {
|
|
|
68
107
|
return value;
|
|
69
108
|
}
|
|
70
109
|
|
|
71
|
-
export function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
|
|
72
|
-
const result = { ...target };
|
|
73
|
-
for (const [key, value] of Object.entries(source)) {
|
|
74
|
-
if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
75
|
-
result[key] = deepMerge(result[key] as Record<string, unknown>, value);
|
|
76
|
-
} else {
|
|
77
|
-
result[key] = value;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function flattenKeys(obj: Record<string, unknown>, prefix = ''): Record<string, string> {
|
|
84
|
-
const result: Record<string, string> = {};
|
|
85
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
86
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
87
|
-
if (isPlainObject(value)) {
|
|
88
|
-
Object.assign(result, flattenKeys(value, fullKey));
|
|
89
|
-
} else {
|
|
90
|
-
result[fullKey] = JSON.stringify(value);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function deFlattenKeys(entries: Record<string, string>): Record<string, unknown> {
|
|
97
|
-
const result: Record<string, unknown> = {};
|
|
98
|
-
for (const [key, rawValue] of Object.entries(entries)) {
|
|
99
|
-
const parts = key.split('.');
|
|
100
|
-
let current = result;
|
|
101
|
-
for (const part of parts.slice(0, -1)) {
|
|
102
|
-
if (!isPlainObject(current[part])) current[part] = {};
|
|
103
|
-
current = current[part] as Record<string, unknown>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const last = parts.at(-1);
|
|
107
|
-
if (last === undefined) continue;
|
|
108
|
-
current[last] = parseConfigValue(rawValue);
|
|
109
|
-
}
|
|
110
|
-
return result;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
110
|
export function buildConfigFromObject(
|
|
114
111
|
raw: Record<string, unknown>,
|
|
115
112
|
options: { overrides?: Partial<Config> } = {},
|
|
@@ -127,12 +124,7 @@ export function buildConfigFromObject(
|
|
|
127
124
|
|
|
128
125
|
export function parseConfigYaml(yamlText: string): Record<string, unknown> {
|
|
129
126
|
try {
|
|
130
|
-
|
|
131
|
-
if (parsed === null || parsed === undefined) return {};
|
|
132
|
-
if (!isPlainObject(parsed)) {
|
|
133
|
-
throw new ConfigLoadError('Config YAML must parse to an object');
|
|
134
|
-
}
|
|
135
|
-
return parsed;
|
|
127
|
+
return parseYamlObject(yamlText);
|
|
136
128
|
} catch (error) {
|
|
137
129
|
if (error instanceof ConfigLoadError) throw error;
|
|
138
130
|
throw new ConfigLoadError(`Config YAML parsing failed: ${(error as Error).message}`);
|
|
@@ -143,14 +135,6 @@ export function buildConfigFromYaml(yamlText: string, options: { overrides?: Par
|
|
|
143
135
|
return buildConfigFromObject(parseConfigYaml(yamlText), options);
|
|
144
136
|
}
|
|
145
137
|
|
|
146
|
-
function parseConfigValue(value: string): unknown {
|
|
147
|
-
try {
|
|
148
|
-
return JSON.parse(value);
|
|
149
|
-
} catch {
|
|
150
|
-
return value;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
138
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
155
139
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
156
140
|
}
|
package/src/context.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Config } from './config';
|
|
|
2
2
|
import { buildConfigFromObject } from './config';
|
|
3
3
|
import type { FileSystem } from './fs';
|
|
4
4
|
import { getFs } from './fs';
|
|
5
|
-
import type { RuntimeCapabilities,
|
|
5
|
+
import type { RuntimeCapabilities, RuntimeName } from './types';
|
|
6
6
|
|
|
7
7
|
export type RuntimeScope = 'process' | 'server-request' | 'scheduled-event' | 'test';
|
|
8
8
|
|
|
@@ -17,7 +17,6 @@ export interface RuntimeContextOptions<TServices extends RuntimeServiceMap = Run
|
|
|
17
17
|
runtimeName?: RuntimeName;
|
|
18
18
|
capabilities?: RuntimeCapabilities;
|
|
19
19
|
services?: Partial<TServices>;
|
|
20
|
-
factory?: RuntimeFactory;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeServiceMap> {
|
|
@@ -28,10 +27,9 @@ export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeService
|
|
|
28
27
|
|
|
29
28
|
constructor(options: RuntimeContextOptions<TServices> = {}) {
|
|
30
29
|
this.scope = options.scope ?? 'process';
|
|
31
|
-
this.runtimeName = options.runtimeName ??
|
|
30
|
+
this.runtimeName = options.runtimeName ?? 'node-bun';
|
|
32
31
|
this.capabilities =
|
|
33
32
|
options.capabilities ??
|
|
34
|
-
options.factory?.capabilities ??
|
|
35
33
|
({
|
|
36
34
|
hasFilesystem: true,
|
|
37
35
|
hasProcessExecution: true,
|