openworkgraph 0.2.12
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 +64 -0
- package/dist/api-image-agent.d.ts +32 -0
- package/dist/api-image-agent.js +132 -0
- package/dist/api-image-output.d.ts +8 -0
- package/dist/api-image-output.js +41 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +153 -0
- package/dist/auth.d.ts +58 -0
- package/dist/auth.js +273 -0
- package/dist/backend/adapter.d.ts +35 -0
- package/dist/backend/adapter.js +439 -0
- package/dist/backend/capabilities.d.ts +23 -0
- package/dist/backend/capabilities.js +75 -0
- package/dist/backend/diagnostics.d.ts +9 -0
- package/dist/backend/diagnostics.js +80 -0
- package/dist/backend/index.d.ts +6 -0
- package/dist/backend/index.js +7 -0
- package/dist/backend/models.d.ts +15 -0
- package/dist/backend/models.js +43 -0
- package/dist/backend/paths.d.ts +6 -0
- package/dist/backend/paths.js +69 -0
- package/dist/backend/stdio.d.ts +66 -0
- package/dist/backend/stdio.js +172 -0
- package/dist/backend/types.d.ts +90 -0
- package/dist/backend/types.js +11 -0
- package/dist/backup-api.d.ts +10 -0
- package/dist/backup-api.js +40 -0
- package/dist/blob-store.d.ts +40 -0
- package/dist/blob-store.js +218 -0
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.js +63 -0
- package/dist/cli-output.d.ts +4 -0
- package/dist/cli-output.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +284 -0
- package/dist/client-pairing.d.ts +8 -0
- package/dist/client-pairing.js +30 -0
- package/dist/directories.d.ts +14 -0
- package/dist/directories.js +34 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +7 -0
- package/dist/events.d.ts +22 -0
- package/dist/events.js +155 -0
- package/dist/execution-chains.d.ts +24 -0
- package/dist/execution-chains.js +231 -0
- package/dist/execution-settings.d.ts +10 -0
- package/dist/execution-settings.js +38 -0
- package/dist/graph-document-history.d.ts +43 -0
- package/dist/graph-document-history.js +275 -0
- package/dist/graph-files.d.ts +14 -0
- package/dist/graph-files.js +59 -0
- package/dist/graph-transfer.d.ts +53 -0
- package/dist/graph-transfer.js +501 -0
- package/dist/graphs.d.ts +68 -0
- package/dist/graphs.js +771 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +61 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +42 -0
- package/dist/image-credentials.d.ts +11 -0
- package/dist/image-credentials.js +130 -0
- package/dist/image-providers.d.ts +51 -0
- package/dist/image-providers.js +271 -0
- package/dist/input-materialization.d.ts +17 -0
- package/dist/input-materialization.js +95 -0
- package/dist/inputs.d.ts +74 -0
- package/dist/inputs.js +301 -0
- package/dist/local-control.d.ts +13 -0
- package/dist/local-control.js +37 -0
- package/dist/network.d.ts +11 -0
- package/dist/network.js +48 -0
- package/dist/openai-image-driver.d.ts +42 -0
- package/dist/openai-image-driver.js +368 -0
- package/dist/operations/backups.d.ts +40 -0
- package/dist/operations/backups.js +391 -0
- package/dist/operations/capacity.d.ts +7 -0
- package/dist/operations/capacity.js +35 -0
- package/dist/operations/cli-context.d.ts +7 -0
- package/dist/operations/cli-context.js +58 -0
- package/dist/operations/daemon.d.ts +36 -0
- package/dist/operations/daemon.js +159 -0
- package/dist/operations/files.d.ts +12 -0
- package/dist/operations/files.js +84 -0
- package/dist/operations/lifecycle.d.ts +28 -0
- package/dist/operations/lifecycle.js +155 -0
- package/dist/operations/local-admin.d.ts +6 -0
- package/dist/operations/local-admin.js +38 -0
- package/dist/operations/logs.d.ts +16 -0
- package/dist/operations/logs.js +37 -0
- package/dist/operations/plugins.d.ts +10 -0
- package/dist/operations/plugins.js +11 -0
- package/dist/operations/snapshot-worker.d.ts +1 -0
- package/dist/operations/snapshot-worker.js +18 -0
- package/dist/operations/snapshot.d.ts +1 -0
- package/dist/operations/snapshot.js +14 -0
- package/dist/operations/update.d.ts +26 -0
- package/dist/operations/update.js +216 -0
- package/dist/persistence/asset-business-schema.d.ts +2 -0
- package/dist/persistence/asset-business-schema.js +36 -0
- package/dist/persistence/database.d.ts +7 -0
- package/dist/persistence/database.js +91 -0
- package/dist/persistence/plugin-schema.d.ts +9 -0
- package/dist/persistence/plugin-schema.js +38 -0
- package/dist/persistence/repositories.d.ts +20 -0
- package/dist/persistence/repositories.js +106 -0
- package/dist/persistence/resource-schema.d.ts +2 -0
- package/dist/persistence/resource-schema.js +44 -0
- package/dist/persistence/schema.d.ts +6 -0
- package/dist/persistence/schema.js +236 -0
- package/dist/persistence/workflow-schema.d.ts +2 -0
- package/dist/persistence/workflow-schema.js +19 -0
- package/dist/plugin-api.d.ts +10 -0
- package/dist/plugin-api.js +40 -0
- package/dist/plugin-management.d.ts +36 -0
- package/dist/plugin-management.js +112 -0
- package/dist/plugins.d.ts +55 -0
- package/dist/plugins.js +239 -0
- package/dist/project-discovery.d.ts +3 -0
- package/dist/project-discovery.js +57 -0
- package/dist/project-files-api.d.ts +16 -0
- package/dist/project-files-api.js +243 -0
- package/dist/project-files.d.ts +97 -0
- package/dist/project-files.js +557 -0
- package/dist/project-lock.d.ts +4 -0
- package/dist/project-lock.js +35 -0
- package/dist/project-probe-worker.d.ts +1 -0
- package/dist/project-probe-worker.js +17 -0
- package/dist/project-probe.d.ts +22 -0
- package/dist/project-probe.js +67 -0
- package/dist/projects.d.ts +29 -0
- package/dist/projects.js +219 -0
- package/dist/publication.d.ts +31 -0
- package/dist/publication.js +642 -0
- package/dist/resource-api.d.ts +15 -0
- package/dist/resource-api.js +256 -0
- package/dist/resources-orphans.d.ts +41 -0
- package/dist/resources-orphans.js +235 -0
- package/dist/resources.d.ts +246 -0
- package/dist/resources.js +714 -0
- package/dist/run-api.d.ts +10 -0
- package/dist/run-api.js +308 -0
- package/dist/run-context.d.ts +54 -0
- package/dist/run-context.js +137 -0
- package/dist/runs.d.ts +82 -0
- package/dist/runs.js +506 -0
- package/dist/runtime-backend.d.ts +24 -0
- package/dist/runtime-backend.js +214 -0
- package/dist/runtime.d.ts +97 -0
- package/dist/runtime.js +553 -0
- package/dist/scheduler.d.ts +85 -0
- package/dist/scheduler.js +344 -0
- package/dist/service.d.ts +29 -0
- package/dist/service.js +515 -0
- package/dist/skills-api.d.ts +18 -0
- package/dist/skills-api.js +120 -0
- package/dist/storage-api.d.ts +21 -0
- package/dist/storage-api.js +92 -0
- package/dist/transfer-api.d.ts +10 -0
- package/dist/transfer-api.js +60 -0
- package/dist/vendor/protocol/contracts.typecheck.d.ts +1 -0
- package/dist/vendor/protocol/contracts.typecheck.js +35 -0
- package/dist/vendor/protocol/execution-chain.d.ts +6 -0
- package/dist/vendor/protocol/execution-chain.js +26 -0
- package/dist/vendor/protocol/graph-binary.d.ts +15 -0
- package/dist/vendor/protocol/graph-binary.js +121 -0
- package/dist/vendor/protocol/graph-title.d.ts +2 -0
- package/dist/vendor/protocol/graph-title.js +23 -0
- package/dist/vendor/protocol/index.d.ts +617 -0
- package/dist/vendor/protocol/index.js +20 -0
- package/dist/vendor/protocol/pairing.d.ts +29 -0
- package/dist/vendor/protocol/pairing.js +71 -0
- package/dist/vendor/protocol/preview.d.ts +2 -0
- package/dist/vendor/protocol/preview.js +9 -0
- package/dist/vendor/protocol/resource-mime.d.ts +2 -0
- package/dist/vendor/protocol/resource-mime.js +35 -0
- package/dist/video-thumbnail.d.ts +2 -0
- package/dist/video-thumbnail.js +34 -0
- package/package.json +9 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FileHandle } from 'node:fs/promises';
|
|
2
|
+
export interface PreparedBlob {
|
|
3
|
+
readonly sha256: string;
|
|
4
|
+
readonly bytes: number;
|
|
5
|
+
readonly mime: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly text: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare const supportsText: (mime: string) => boolean;
|
|
10
|
+
export declare const validMime: (mime: string) => boolean;
|
|
11
|
+
export declare const opaqueFileMime: (mime: string) => boolean;
|
|
12
|
+
export declare const mimeMatchesBytes: (declared: string, sniffed: string, allowGenericOctet?: boolean) => boolean;
|
|
13
|
+
export declare const hashBytes: (bytes: Uint8Array) => string;
|
|
14
|
+
/** The source can grow after stat: never let readFile allocate without a bound. */
|
|
15
|
+
export declare function readBounded(file: FileHandle, max: number): Promise<Buffer>;
|
|
16
|
+
import { sniffResourceMime as sniffMime } from "./vendor/protocol/index.js";
|
|
17
|
+
export { sniffMime };
|
|
18
|
+
/** Files are generation-addressed, not overwritten by hash. A committed GC queue
|
|
19
|
+
* can never unlink a later upload of the same bytes. Paths are private service data. */
|
|
20
|
+
export declare class BlobStore {
|
|
21
|
+
readonly root: string;
|
|
22
|
+
readonly maxBytes: number;
|
|
23
|
+
private readonly prepared;
|
|
24
|
+
constructor(root: string, options?: {
|
|
25
|
+
maxBytes?: number;
|
|
26
|
+
});
|
|
27
|
+
private directory;
|
|
28
|
+
private write;
|
|
29
|
+
prepare(bytes: Uint8Array, declaredMime?: string, expectedHash?: string): Promise<PreparedBlob>;
|
|
30
|
+
assertPrepared(blob: PreparedBlob): void;
|
|
31
|
+
writeChunk(bytes: Uint8Array): Promise<string>;
|
|
32
|
+
private validatePath;
|
|
33
|
+
read(path: string): Promise<Buffer>;
|
|
34
|
+
/** Verify immutable media without allocating a full-video Buffer. */
|
|
35
|
+
withVerifiedFile<T>(path: string, expected: {
|
|
36
|
+
bytes: number;
|
|
37
|
+
sha256: string;
|
|
38
|
+
}, use: (path: string) => Promise<T>): Promise<T>;
|
|
39
|
+
remove(path: string): Promise<void>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { WORKGRAPH_UPLOAD_MAX_BYTES } from "./vendor/protocol/index.js";
|
|
3
|
+
import { mkdir, open, realpath, unlink } from 'node:fs/promises';
|
|
4
|
+
import { constants, lstatSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { ServiceError } from './errors.js';
|
|
7
|
+
export const supportsText = (mime) => ['text/plain', 'text/markdown', 'text/csv', 'application/json', 'application/xml', 'text/xml', 'text/yaml', 'text/x-yaml', 'application/yaml', 'application/x-yaml'].includes(mime);
|
|
8
|
+
const SNIFFED_MEDIA = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'application/pdf', 'video/mp4', 'video/webm']);
|
|
9
|
+
export const validMime = (mime) => /^[a-zA-Z0-9.+-]+[/][a-zA-Z0-9.+-]+$/.test(mime);
|
|
10
|
+
export const opaqueFileMime = (mime) => validMime(mime) && mime !== 'application/octet-stream' && !supportsText(mime) && !SNIFFED_MEDIA.has(mime);
|
|
11
|
+
export const mimeMatchesBytes = (declared, sniffed, allowGenericOctet = false) => declared === sniffed ||
|
|
12
|
+
(allowGenericOctet && declared === 'application/octet-stream') ||
|
|
13
|
+
(sniffed === 'text/plain' && (supportsText(declared) || declared === 'application/octet-stream' || opaqueFileMime(declared))) ||
|
|
14
|
+
(sniffed === 'application/octet-stream' && opaqueFileMime(declared));
|
|
15
|
+
export const hashBytes = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
function blobRoot(input) {
|
|
17
|
+
const root = resolve(input);
|
|
18
|
+
if (process.platform !== 'win32')
|
|
19
|
+
return root;
|
|
20
|
+
// Windows may supply 8.3 paths. Reject actual links before canonicalizing spelling.
|
|
21
|
+
let canonical;
|
|
22
|
+
for (let path = root;; path = dirname(path)) {
|
|
23
|
+
try {
|
|
24
|
+
if (lstatSync(path).isSymbolicLink())
|
|
25
|
+
throw new ServiceError('INVALID_REQUEST', 'Blob directories cannot be symlinks');
|
|
26
|
+
canonical ??= join(realpathSync.native(path), relative(path, root));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 'ENOENT')
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
if (dirname(path) === path)
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return canonical ?? root;
|
|
36
|
+
}
|
|
37
|
+
async function syncDirectory(path) {
|
|
38
|
+
// Node cannot open directory handles for fsync on Windows. File data is synced above.
|
|
39
|
+
if (process.platform === 'win32')
|
|
40
|
+
return;
|
|
41
|
+
const directory = await open(path, constants.O_RDONLY);
|
|
42
|
+
try {
|
|
43
|
+
await directory.sync();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await directory.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** The source can grow after stat: never let readFile allocate without a bound. */
|
|
50
|
+
export async function readBounded(file, max) {
|
|
51
|
+
const parts = [];
|
|
52
|
+
let total = 0;
|
|
53
|
+
while (true) {
|
|
54
|
+
const buffer = Buffer.alloc(Math.min(64 * 1024, max - total + 1));
|
|
55
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, null);
|
|
56
|
+
if (!bytesRead)
|
|
57
|
+
break;
|
|
58
|
+
total += bytesRead;
|
|
59
|
+
if (total > max)
|
|
60
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', 'File exceeds maximum');
|
|
61
|
+
parts.push(buffer.subarray(0, bytesRead));
|
|
62
|
+
}
|
|
63
|
+
return Buffer.concat(parts, total);
|
|
64
|
+
}
|
|
65
|
+
import { sniffResourceMime as sniffMime } from "./vendor/protocol/index.js";
|
|
66
|
+
export { sniffMime };
|
|
67
|
+
/** Files are generation-addressed, not overwritten by hash. A committed GC queue
|
|
68
|
+
* can never unlink a later upload of the same bytes. Paths are private service data. */
|
|
69
|
+
export class BlobStore {
|
|
70
|
+
root;
|
|
71
|
+
maxBytes;
|
|
72
|
+
prepared = new WeakSet();
|
|
73
|
+
constructor(root, options = {}) {
|
|
74
|
+
this.root = blobRoot(root);
|
|
75
|
+
this.maxBytes = options.maxBytes ?? WORKGRAPH_UPLOAD_MAX_BYTES;
|
|
76
|
+
if (!Number.isSafeInteger(this.maxBytes) || this.maxBytes < 1)
|
|
77
|
+
throw new Error('Invalid maximum upload size');
|
|
78
|
+
}
|
|
79
|
+
async directory(kind) {
|
|
80
|
+
const directory = join(this.root, kind);
|
|
81
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
82
|
+
if (await realpath(this.root) !== this.root || await realpath(directory) !== directory)
|
|
83
|
+
throw new ServiceError('INVALID_REQUEST', 'Blob directories cannot be symlinks');
|
|
84
|
+
return directory;
|
|
85
|
+
}
|
|
86
|
+
async write(bytes, kind) {
|
|
87
|
+
const directory = await this.directory(kind);
|
|
88
|
+
const path = join(directory, randomUUID());
|
|
89
|
+
const file = await open(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
90
|
+
try {
|
|
91
|
+
await file.writeFile(bytes);
|
|
92
|
+
await file.sync();
|
|
93
|
+
await file.chmod(0o400);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
await file.close();
|
|
97
|
+
await unlink(path).catch(() => { });
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
await file.close();
|
|
102
|
+
await syncDirectory(directory);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
await unlink(path).catch(() => { });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
async prepare(bytes, declaredMime = 'application/octet-stream', expectedHash) {
|
|
111
|
+
if (bytes.byteLength > this.maxBytes)
|
|
112
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', 'Resource exceeds configured maximum');
|
|
113
|
+
const stable = Buffer.from(bytes);
|
|
114
|
+
const sha256 = hashBytes(stable), mime = sniffMime(stable);
|
|
115
|
+
if (expectedHash !== undefined && (!/^[a-f0-9]{64}$/.test(expectedHash) || sha256 !== expectedHash))
|
|
116
|
+
throw new ServiceError('INVALID_REQUEST', 'SHA-256 mismatch');
|
|
117
|
+
const declared = declaredMime.toLowerCase().split(';')[0].trim();
|
|
118
|
+
if (!validMime(declared) || !mimeMatchesBytes(declared, mime, true))
|
|
119
|
+
throw new ServiceError('UNSUPPORTED_MEDIA_TYPE', 'Declared MIME does not match content');
|
|
120
|
+
if (declared === 'application/json') {
|
|
121
|
+
try {
|
|
122
|
+
JSON.parse(stable.toString('utf8'));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new ServiceError('UNSUPPORTED_MEDIA_TYPE', 'Invalid JSON content');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const effectiveMime = supportsText(declared) || opaqueFileMime(declared) ? declared : mime;
|
|
129
|
+
const result = Object.freeze({ sha256, bytes: stable.byteLength, mime: effectiveMime, path: await this.write(stable, 'objects'), text: supportsText(effectiveMime) ? new TextDecoder('utf-8', { fatal: true }).decode(stable) : null });
|
|
130
|
+
this.prepared.add(result);
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
assertPrepared(blob) { if (!this.prepared.has(blob))
|
|
134
|
+
throw new ServiceError('INVALID_REQUEST', 'Expected an immutable blob prepared by this store'); }
|
|
135
|
+
async writeChunk(bytes) { if (bytes.byteLength < 1 || bytes.byteLength > Math.min(this.maxBytes, 4 * 1024 * 1024))
|
|
136
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', 'Chunk exceeds maximum'); return this.write(Buffer.from(bytes), 'chunks'); }
|
|
137
|
+
validatePath(path) {
|
|
138
|
+
if (!['objects', 'chunks'].some(kind => dirname(path) === join(this.root, kind)) || !/^[0-9a-f-]{36}$/.test(path.slice(path.lastIndexOf(sep) + 1)))
|
|
139
|
+
throw new ServiceError('INVALID_REQUEST', 'Unmanaged blob path');
|
|
140
|
+
if (resolve(path) !== path)
|
|
141
|
+
throw new ServiceError('INVALID_REQUEST', 'Unmanaged blob path');
|
|
142
|
+
}
|
|
143
|
+
async read(path) {
|
|
144
|
+
this.validatePath(path);
|
|
145
|
+
if (await realpath(path) !== path)
|
|
146
|
+
throw new ServiceError('INVALID_REQUEST', 'Blob symlinks are forbidden');
|
|
147
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
148
|
+
try {
|
|
149
|
+
const stat = await file.stat();
|
|
150
|
+
if (!stat.isFile())
|
|
151
|
+
throw new ServiceError('INVALID_REQUEST', 'Expected regular blob file');
|
|
152
|
+
if (stat.size > this.maxBytes)
|
|
153
|
+
throw new ServiceError('PAYLOAD_TOO_LARGE', 'Blob exceeds maximum');
|
|
154
|
+
return await readBounded(file, this.maxBytes);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await file.close();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Verify immutable media without allocating a full-video Buffer. */
|
|
161
|
+
async withVerifiedFile(path, expected, use) {
|
|
162
|
+
this.validatePath(path);
|
|
163
|
+
if (await realpath(path) !== path)
|
|
164
|
+
throw new ServiceError('INVALID_REQUEST', 'Blob symlinks are forbidden');
|
|
165
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
166
|
+
try {
|
|
167
|
+
const before = await file.stat({ bigint: true });
|
|
168
|
+
if (!before.isFile() || before.size !== BigInt(expected.bytes) || before.size > BigInt(this.maxBytes))
|
|
169
|
+
throw new ServiceError('INTERNAL_ERROR', 'Stored blob integrity check failed');
|
|
170
|
+
const hash = createHash('sha256');
|
|
171
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
172
|
+
let total = 0;
|
|
173
|
+
while (true) {
|
|
174
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, null);
|
|
175
|
+
if (!bytesRead)
|
|
176
|
+
break;
|
|
177
|
+
total += bytesRead;
|
|
178
|
+
if (total > expected.bytes)
|
|
179
|
+
throw new ServiceError('INTERNAL_ERROR', 'Stored blob integrity check failed');
|
|
180
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
181
|
+
}
|
|
182
|
+
if (total !== expected.bytes || hash.digest('hex') !== expected.sha256)
|
|
183
|
+
throw new ServiceError('INTERNAL_ERROR', 'Stored blob integrity check failed');
|
|
184
|
+
const result = await use(path);
|
|
185
|
+
if (await realpath(path) !== path)
|
|
186
|
+
throw new ServiceError('INVALID_REQUEST', 'Blob symlinks are forbidden');
|
|
187
|
+
const current = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
188
|
+
try {
|
|
189
|
+
const after = await current.stat({ bigint: true });
|
|
190
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs)
|
|
191
|
+
throw new ServiceError('INTERNAL_ERROR', 'Stored blob changed during preview');
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await current.close();
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await file.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async remove(path) {
|
|
203
|
+
this.validatePath(path);
|
|
204
|
+
// Never traverse a replaced parent symlink, even when a deletion was queued.
|
|
205
|
+
if (await realpath(join(path, '..')) !== join(path, '..'))
|
|
206
|
+
throw new ServiceError('INVALID_REQUEST', 'Blob parent symlinks are forbidden');
|
|
207
|
+
try {
|
|
208
|
+
await unlink(path);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error.code !== 'ENOENT')
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
// Marking the durable deletion queue completed must follow a durable unlink.
|
|
215
|
+
await syncDirectory(dirname(path));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=blob-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatHelp(): string;
|
package/dist/cli-help.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SERVICE_VERSION } from "./vendor/protocol/index.js";
|
|
2
|
+
export function formatHelp() {
|
|
3
|
+
const row = (syntax, description) => ' ' + syntax.padEnd(32) + description;
|
|
4
|
+
return [
|
|
5
|
+
'',
|
|
6
|
+
'OpenWorkgraph ' + SERVICE_VERSION,
|
|
7
|
+
'Local workflow service',
|
|
8
|
+
'',
|
|
9
|
+
'Usage',
|
|
10
|
+
' openworkgraph <command> [options]',
|
|
11
|
+
'',
|
|
12
|
+
'Service management',
|
|
13
|
+
row('start', 'Start in the background'),
|
|
14
|
+
row('restart', 'Restart the service; acts as start when it is not running'),
|
|
15
|
+
row('update', 'Stop the service, install the latest package, and restart it if it was running'),
|
|
16
|
+
row('serve', 'Run in the foreground'),
|
|
17
|
+
row('status', 'Show service status'),
|
|
18
|
+
row('logs', 'Show service logs'),
|
|
19
|
+
row('stop', 'Wait for active tasks, then stop; keep queued tasks'),
|
|
20
|
+
row('capacity [COUNT]', 'Show or set concurrency (a positive integer)'),
|
|
21
|
+
'',
|
|
22
|
+
'Client pairing',
|
|
23
|
+
row('pair --client-code <CLIENT_CODE>', 'Verify origin and fingerprint, then approve the client'),
|
|
24
|
+
row('sessions', 'List client sessions'),
|
|
25
|
+
row('revoke-session <SESSION_ID>', 'Revoke a session'),
|
|
26
|
+
'',
|
|
27
|
+
'Backup and restore',
|
|
28
|
+
row('backup create', 'Create a backup'),
|
|
29
|
+
row('backup list', 'List backups'),
|
|
30
|
+
row('backup download <BACKUP_ID>', 'Download a backup; requires --output <NEW_FILE>'),
|
|
31
|
+
row('restore preview <PATH>', 'Preview a restore and its SHA-256'),
|
|
32
|
+
row('restore apply <PATH>', 'Restore locally; requires --sha256 <HASH>'),
|
|
33
|
+
'',
|
|
34
|
+
'Plugins',
|
|
35
|
+
row('plugin register <MANIFEST>', 'Register a trusted plugin JSON manifest'),
|
|
36
|
+
'',
|
|
37
|
+
'Startup options · start / serve only',
|
|
38
|
+
row('--data-dir <PATH>', 'Set and remember the service data directory'),
|
|
39
|
+
row('--host <ADDRESS>', 'Listen address (default: 0.0.0.0)'),
|
|
40
|
+
row('--port <PORT>', 'Listen port (default: 14317)'),
|
|
41
|
+
' restart accepts --host / --port overrides; if stopping takes over 10 seconds, check status.',
|
|
42
|
+
'',
|
|
43
|
+
'Other options',
|
|
44
|
+
row('version', 'Show version'),
|
|
45
|
+
row('--yes', 'pair: explicitly approve in non-interactive scripts'),
|
|
46
|
+
row('--output-json', 'Output JSON for scripts and automation'),
|
|
47
|
+
row('--interrupt-and-stop', 'stop / restart / update: legacy alias; stopping cancels active and queued tasks'),
|
|
48
|
+
row('--idempotency-key <KEY>', 'backup create: set an idempotency key'),
|
|
49
|
+
row('--help', 'Show help'),
|
|
50
|
+
row('--version', 'Show version'),
|
|
51
|
+
'',
|
|
52
|
+
'Examples',
|
|
53
|
+
' openworkgraph start --host 127.0.0.1',
|
|
54
|
+
' openworkgraph pair --client-code 12345678',
|
|
55
|
+
' openworkgraph status',
|
|
56
|
+
' openworkgraph restart',
|
|
57
|
+
' openworkgraph update',
|
|
58
|
+
' openworkgraph backup create',
|
|
59
|
+
' openworkgraph stop',
|
|
60
|
+
'',
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=cli-help.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function formatHumanOutput(command: string, value: unknown): string;
|
|
2
|
+
export declare function printOutput(command: string, value: unknown, outputJson?: boolean): void;
|
|
3
|
+
export declare function formatPairingResult(value: unknown, outputJson?: boolean): string;
|
|
4
|
+
export declare function formatCliError(error: unknown, outputJson?: boolean): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ServiceError } from './errors.js';
|
|
2
|
+
const fields = {
|
|
3
|
+
acceptedTasksUnaffected: 'Accepted tasks unaffected', activeRuns: 'Active runs', available: 'Available', backupId: 'Backup ID', browserName: 'Client name', bytes: 'Size', capacity: 'Capacity', code: 'Pairing code', createdAt: 'Created at', current: 'Current session', currentVersion: 'Current version', endpoints: 'Runtime endpoints', event: 'Event', expiresAt: 'Expires at', fingerprint: 'Public key fingerprint SHA-256', id: 'ID', instanceId: 'Instance ID', integrity: 'Integrity', invalidatesAllSessions: 'Invalidates all sessions', lastUsedAt: 'Last used at', latestVersion: 'Latest version', listenHost: 'Listen address', localEndpoint: 'Local endpoint', location: 'Storage location', mode: 'Mode', occupied: 'Occupied', origin: 'Web origin', output: 'Output file', packageName: 'Package', pairedAt: 'Paired at', pairingCommand: 'Pairing command', pausesQueuedRuns: 'Pauses queued runs', pluginId: 'Plugin ID', registered: 'Registered', requiresMaintenance: 'Requires maintenance', restarted: 'Runtime restarted', restored: 'Restored', revoked: 'Revoked session', schemaVersion: 'Schema version', serviceId: 'Runtime ID', sha256: 'SHA-256', state: 'State', time: 'Time', typeId: 'Type ID', updateAvailable: 'Update available', updated: 'Package updated', version: 'Version',
|
|
4
|
+
};
|
|
5
|
+
const titles = {
|
|
6
|
+
'backup create': 'Backup created', 'backup download': 'Backup downloaded', 'backup list': 'Backups', capacity: 'Runtime capacity', logs: 'Runtime logs', 'plugin register': 'Plugin registered', restart: 'Runtime restarted', 'restore apply': 'Backup restored', 'restore preview': 'Restore preview', 'revoke-session': 'Session revoked', sessions: 'Client sessions', start: 'Runtime started', status: 'Runtime status', stop: 'Runtime stopping', update: 'Runtime update',
|
|
7
|
+
};
|
|
8
|
+
const states = { active: 'Active', creating: 'Creating', draining: 'Draining', expired: 'Expired', failed: 'Failed', interrupting: 'Interrupting', ready: 'Ready', running: 'Running' };
|
|
9
|
+
const events = { backup_failed: 'Backup failed', backup_ready: 'Backup ready', draining: 'Draining started', interrupting: 'Interrupting', restored: 'Restored', shutdown_failed: 'Shutdown failed', started: 'Started', startup_failed: 'Startup failed', stopped: 'Stopped' };
|
|
10
|
+
const values = { pairing: 'Waiting for client pairing', verified: 'Verified' };
|
|
11
|
+
const translatedErrors = new Map([
|
|
12
|
+
['Unknown command; use --help', 'Unknown command. Use --help to see available commands.'],
|
|
13
|
+
['Port must be an integer from 0 to 65535', 'Port must be an integer from 0 to 65535.'],
|
|
14
|
+
['Unexpected arguments', 'Unexpected arguments. Use --help for command syntax.'],
|
|
15
|
+
['Service not running or management unavailable', 'The service is not running or local management is unavailable.'],
|
|
16
|
+
['Startup acknowledgment timed out; inspect status before retrying', 'Startup confirmation timed out. Run status before retrying.'],
|
|
17
|
+
['Service is still draining; interactions/cancel remain available', 'The service is still draining; interactions and cancellation remain available.'],
|
|
18
|
+
]);
|
|
19
|
+
const fallbackErrors = {
|
|
20
|
+
INVALID_REQUEST: 'The command request is invalid.',
|
|
21
|
+
ORIGIN_DENIED: 'The client origin is not allowed.',
|
|
22
|
+
PAIRING_CODE_EXPIRED: 'The pairing code has expired. Generate a new code in the browser.',
|
|
23
|
+
PAIRING_CODE_INVALID: 'The pairing code is invalid. Generate a new code in the browser.',
|
|
24
|
+
PAIRING_CODE_USED: 'The pairing code has already been used. Generate a new code in the browser.',
|
|
25
|
+
SERVICE_MISMATCH: 'The client code belongs to a different runtime.',
|
|
26
|
+
SERVICE_NOT_RUNNING: 'The runtime is not running or local management is unavailable.',
|
|
27
|
+
};
|
|
28
|
+
function displayWidth(value) { return [...value].reduce((width, char) => width + (/[^\x00-\xff]/.test(char) ? 2 : 1), 0); }
|
|
29
|
+
function padLabel(value, width) { return value + ' '.repeat(Math.max(0, width - displayWidth(value))); }
|
|
30
|
+
function formatDate(value) {
|
|
31
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(value))
|
|
32
|
+
return value;
|
|
33
|
+
const date = new Date(value);
|
|
34
|
+
return Number.isNaN(date.valueOf()) ? value : date.toLocaleString('en-US', { hour12: false });
|
|
35
|
+
}
|
|
36
|
+
function formatBytes(value) {
|
|
37
|
+
if (value < 1024)
|
|
38
|
+
return value + ' B';
|
|
39
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
40
|
+
let size = value / 1024;
|
|
41
|
+
let unit = units[0];
|
|
42
|
+
for (let index = 1; index < units.length && size >= 1024; index++) {
|
|
43
|
+
size /= 1024;
|
|
44
|
+
unit = units[index];
|
|
45
|
+
}
|
|
46
|
+
return size.toFixed(size >= 10 ? 1 : 2).replace(/\.0+$|(?<=\.[0-9])0$/, '') + ' ' + unit;
|
|
47
|
+
}
|
|
48
|
+
function scalar(key, value) {
|
|
49
|
+
if (value === null)
|
|
50
|
+
return '—';
|
|
51
|
+
if (typeof value === 'boolean')
|
|
52
|
+
return value ? 'Yes' : 'No';
|
|
53
|
+
if (typeof value === 'number')
|
|
54
|
+
return key === 'bytes' ? formatBytes(value) : String(value);
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
if (key === 'state' && states[value])
|
|
57
|
+
return states[value];
|
|
58
|
+
if (key === 'event' && events[value])
|
|
59
|
+
return events[value];
|
|
60
|
+
if (key.endsWith('At') || key === 'time')
|
|
61
|
+
return formatDate(value);
|
|
62
|
+
return values[value] || value || '—';
|
|
63
|
+
}
|
|
64
|
+
return String(value);
|
|
65
|
+
}
|
|
66
|
+
function recordLines(value, indent = ' ') {
|
|
67
|
+
const entries = Object.entries(value).filter(([, item]) => item !== undefined);
|
|
68
|
+
const scalarEntries = entries.filter(([, item]) => item === null || ['string', 'number', 'boolean'].includes(typeof item));
|
|
69
|
+
const labelWidth = Math.max(0, ...scalarEntries.map(([key]) => displayWidth(fields[key] ?? key)));
|
|
70
|
+
const lines = scalarEntries.map(([key, item]) => indent + padLabel(fields[key] ?? key, labelWidth) + ' ' + scalar(key, item));
|
|
71
|
+
for (const [key, item] of entries) {
|
|
72
|
+
if (item === null || ['string', 'number', 'boolean'].includes(typeof item))
|
|
73
|
+
continue;
|
|
74
|
+
const label = fields[key] ?? key;
|
|
75
|
+
if (Array.isArray(item)) {
|
|
76
|
+
if (item.every(entry => entry === null || ['string', 'number', 'boolean'].includes(typeof entry)))
|
|
77
|
+
lines.push(indent + label + ' ' + (item.length ? item.map(entry => scalar(key, entry)).join(', ') : '—'));
|
|
78
|
+
else {
|
|
79
|
+
lines.push(indent + label);
|
|
80
|
+
lines.push(...listLines(item, indent + ' '));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (typeof item === 'object') {
|
|
84
|
+
lines.push(indent + label);
|
|
85
|
+
lines.push(...recordLines(item, indent + ' '));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
function listLines(items, indent = ' ') {
|
|
91
|
+
if (items.length === 0)
|
|
92
|
+
return [indent + 'No records.'];
|
|
93
|
+
const lines = [];
|
|
94
|
+
items.forEach((item, index) => {
|
|
95
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
96
|
+
lines.push(indent + (index + 1) + '.');
|
|
97
|
+
lines.push(...recordLines(item, indent + ' '));
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
lines.push(indent + (index + 1) + '. ' + scalar('', item));
|
|
101
|
+
if (index < items.length - 1)
|
|
102
|
+
lines.push('');
|
|
103
|
+
});
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
export function formatHumanOutput(command, value) {
|
|
107
|
+
const title = titles[command] ?? 'Command result';
|
|
108
|
+
const lines = Array.isArray(value) ? listLines(value) : value && typeof value === 'object' ? recordLines(value) : [' ' + scalar('', value)];
|
|
109
|
+
return ['', title, '', ...lines, ''].join('\n');
|
|
110
|
+
}
|
|
111
|
+
export function printOutput(command, value, outputJson = false) { console.log(outputJson ? JSON.stringify(value) : formatHumanOutput(command, value)); }
|
|
112
|
+
export function formatPairingResult(value, outputJson = false) {
|
|
113
|
+
if (outputJson)
|
|
114
|
+
return JSON.stringify(value);
|
|
115
|
+
const result = value;
|
|
116
|
+
if (result.code)
|
|
117
|
+
return ['', 'Legacy client authorization completed', '', ' One-time pairing code ' + result.code, '', ' Valid for 5 minutes, one use only', ' Expires at ' + formatDate(result.expiresAt), ' Web origin ' + result.origin, ' Runtime ID ' + result.serviceId, ...(result.endpoints?.length ? [' Runtime endpoints ' + result.endpoints.join(', ')] : []), '', 'Return to the original browser page and enter the pairing code above.', ''].join('\n');
|
|
118
|
+
return ['', 'Client pairing approved', '', ' Web origin ' + result.origin, ' Runtime ID ' + result.serviceId, ' Request expires at ' + formatDate(result.expiresAt), ...(result.endpoints?.length ? [' Runtime endpoints ' + result.endpoints.join(', ')] : []), '', 'The browser will detect approval and connect automatically. Keep the pairing page open.', ''].join('\n');
|
|
119
|
+
}
|
|
120
|
+
export function formatCliError(error, outputJson = false) {
|
|
121
|
+
const code = error instanceof ServiceError ? error.code : 'STARTUP_FAILED';
|
|
122
|
+
const raw = error instanceof Error ? error.message : 'Command failed.';
|
|
123
|
+
const source = raw.startsWith('Background startup refused: ') ? raw.slice('Background startup refused: '.length) : raw;
|
|
124
|
+
const message = translatedErrors.get(source) ?? (/\p{Script=Han}/u.test(source) ? fallbackErrors[code] ?? 'The command failed. See the error code for details.' : source);
|
|
125
|
+
if (outputJson)
|
|
126
|
+
return JSON.stringify({ code, message });
|
|
127
|
+
const width = Math.max(displayWidth('Code'), displayWidth('Reason'));
|
|
128
|
+
return ['', 'Command failed', '', ' ' + padLabel('Code', width) + ' ' + code, ' ' + padLabel('Reason', width) + ' ' + message, ''].join('\n');
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=cli-output.js.map
|
package/dist/cli.d.ts
ADDED