@hraness/sponge 0.1.2 → 0.2.1
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 +164 -17
- package/bin/sponge.js +9 -2
- package/dist/contracts/index.d.ts +164 -0
- package/dist/contracts/index.js +6 -0
- package/dist/contracts/validate.d.ts +14 -0
- package/dist/contracts/validate.js +187 -0
- package/dist/migrations/001.d.ts +3 -0
- package/dist/migrations/001.js +33 -0
- package/dist/migrations/002.d.ts +3 -0
- package/dist/migrations/002.js +6 -0
- package/dist/migrations/003.d.ts +3 -0
- package/dist/migrations/003.js +10 -0
- package/dist/migrations/004.d.ts +3 -0
- package/dist/migrations/004.js +9 -0
- package/dist/src/acquisition/capture.d.ts +29 -0
- package/dist/src/acquisition/capture.js +160 -0
- package/dist/src/acquisition/cleanup.d.ts +6 -0
- package/dist/src/acquisition/cleanup.js +20 -0
- package/dist/src/acquisition/errors.d.ts +3 -0
- package/dist/src/acquisition/errors.js +20 -0
- package/dist/src/acquisition/files.d.ts +4 -0
- package/dist/src/acquisition/files.js +49 -0
- package/dist/src/acquisition/index.d.ts +6 -0
- package/dist/src/acquisition/index.js +4 -0
- package/dist/src/acquisition/lock.d.ts +1 -0
- package/dist/src/acquisition/lock.js +2 -0
- package/dist/src/acquisition/normalize.d.ts +11 -0
- package/dist/src/acquisition/normalize.js +62 -0
- package/dist/src/acquisition/pins.d.ts +3 -0
- package/dist/src/acquisition/pins.js +9 -0
- package/dist/src/acquisition/process.d.ts +16 -0
- package/dist/src/acquisition/process.js +63 -0
- package/dist/src/acquisition/runtime.d.ts +54 -0
- package/dist/src/acquisition/runtime.js +218 -0
- package/dist/src/acquisition/verify-helper.d.ts +1 -0
- package/dist/src/acquisition/verify-helper.js +19 -0
- package/dist/src/app/article.d.ts +23 -0
- package/dist/src/app/article.js +82 -0
- package/dist/src/app/bundle-files.d.ts +8 -0
- package/dist/src/app/bundle-files.js +66 -0
- package/dist/src/app/markdown.d.ts +6 -0
- package/dist/src/app/markdown.js +90 -0
- package/dist/src/app/source.d.ts +15 -0
- package/dist/src/app/source.js +40 -0
- package/dist/src/cli/arguments.d.ts +9 -0
- package/dist/src/cli/arguments.js +106 -0
- package/dist/src/cli/config.d.ts +8 -0
- package/dist/src/cli/config.js +45 -0
- package/dist/src/cli/import.d.ts +4 -0
- package/dist/src/cli/import.js +3 -0
- package/dist/src/cli/indexing.d.ts +7 -0
- package/dist/src/cli/indexing.js +21 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +333 -0
- package/dist/src/cli/portability.d.ts +9 -0
- package/dist/src/cli/portability.js +39 -0
- package/dist/src/cli/queue.d.ts +2 -0
- package/dist/src/cli/queue.js +23 -0
- package/dist/src/cli/terminal.d.ts +2 -0
- package/dist/src/cli/terminal.js +4 -0
- package/dist/src/cli/worker.d.ts +16 -0
- package/dist/src/cli/worker.js +62 -0
- package/dist/src/importers/apply.d.ts +29 -0
- package/dist/src/importers/apply.js +85 -0
- package/dist/src/importers/custody.d.ts +6 -0
- package/dist/src/importers/custody.js +35 -0
- package/dist/src/importers/inspect.d.ts +21 -0
- package/dist/src/importers/inspect.js +97 -0
- package/dist/src/importers/manifest.d.ts +22 -0
- package/dist/src/importers/manifest.js +43 -0
- package/dist/src/importers/verify-helper.d.ts +1 -0
- package/dist/src/importers/verify-helper.js +27 -0
- package/dist/src/indexing/image.d.ts +10 -0
- package/dist/src/indexing/image.js +34 -0
- package/dist/src/indexing/index.d.ts +53 -0
- package/dist/src/indexing/index.js +123 -0
- package/dist/src/indexing/jobs.d.ts +14 -0
- package/dist/src/indexing/jobs.js +49 -0
- package/dist/src/indexing/runner.d.ts +23 -0
- package/dist/src/indexing/runner.js +114 -0
- package/dist/src/inference/asr/bridge.d.ts +1 -0
- package/dist/src/inference/asr/bridge.js +16 -0
- package/dist/src/inference/asr/protocol.d.ts +23 -0
- package/dist/src/inference/asr/protocol.js +60 -0
- package/dist/src/inference/asr/runtime.d.ts +29 -0
- package/dist/src/inference/asr/runtime.js +103 -0
- package/dist/src/inference/asr/windows.d.ts +9 -0
- package/dist/src/inference/asr/windows.js +41 -0
- package/dist/src/inference/embedding/index.d.ts +47 -0
- package/dist/src/inference/embedding/index.js +122 -0
- package/dist/src/media/capture-helper.d.ts +1 -0
- package/dist/src/media/capture-helper.js +16 -0
- package/dist/src/media/capture.d.ts +32 -0
- package/dist/src/media/capture.js +193 -0
- package/dist/src/media/handler.d.ts +22 -0
- package/dist/src/media/handler.js +82 -0
- package/dist/src/media/transcribe.d.ts +29 -0
- package/dist/src/media/transcribe.js +93 -0
- package/dist/src/metadata/article.d.ts +5 -0
- package/dist/src/metadata/article.js +51 -0
- package/dist/src/models/artifacts.d.ts +36 -0
- package/dist/src/models/artifacts.js +274 -0
- package/dist/src/models/catalog.d.ts +35 -0
- package/dist/src/models/catalog.js +71 -0
- package/dist/src/models/download.d.ts +9 -0
- package/dist/src/models/download.js +147 -0
- package/dist/src/models/runtime.d.ts +24 -0
- package/dist/src/models/runtime.js +37 -0
- package/dist/src/pdf/capture-helper.d.ts +1 -0
- package/dist/src/pdf/capture-helper.js +12 -0
- package/dist/src/pdf/capture.d.ts +29 -0
- package/dist/src/pdf/capture.js +156 -0
- package/dist/src/pdf/errors.d.ts +2 -0
- package/dist/src/pdf/errors.js +13 -0
- package/dist/src/pdf/handler.d.ts +24 -0
- package/dist/src/pdf/handler.js +84 -0
- package/dist/src/pdf/index.d.ts +4 -0
- package/dist/src/pdf/index.js +4 -0
- package/dist/src/pdf/input.d.ts +2 -0
- package/dist/src/pdf/input.js +46 -0
- package/dist/src/pdf/normalize.d.ts +10 -0
- package/dist/src/pdf/normalize.js +64 -0
- package/dist/src/pdf/source-identity.d.ts +4 -0
- package/dist/src/pdf/source-identity.js +12 -0
- package/dist/src/pdf/source.d.ts +6 -0
- package/dist/src/pdf/source.js +22 -0
- package/dist/src/pdf/verify-helper.d.ts +1 -0
- package/dist/src/pdf/verify-helper.js +17 -0
- package/dist/src/portability/backup.d.ts +23 -0
- package/dist/src/portability/backup.js +138 -0
- package/dist/src/portability/database.d.ts +14 -0
- package/dist/src/portability/database.js +78 -0
- package/dist/src/portability/export.d.ts +8 -0
- package/dist/src/portability/export.js +36 -0
- package/dist/src/portability/files.d.ts +33 -0
- package/dist/src/portability/files.js +78 -0
- package/dist/src/portability/gc.d.ts +21 -0
- package/dist/src/portability/gc.js +66 -0
- package/dist/src/search/extension.d.ts +3 -0
- package/dist/src/search/extension.js +17 -0
- package/dist/src/search/hybrid.d.ts +39 -0
- package/dist/src/search/hybrid.js +118 -0
- package/dist/src/search/query.d.ts +12 -0
- package/dist/src/search/query.js +47 -0
- package/dist/src/search/vector.d.ts +21 -0
- package/dist/src/search/vector.js +21 -0
- package/dist/src/service/launch-agent.d.ts +47 -0
- package/dist/src/service/launch-agent.js +179 -0
- package/dist/src/service/lifetime.d.ts +7 -0
- package/dist/src/service/lifetime.js +73 -0
- package/dist/src/service/loop.d.ts +21 -0
- package/dist/src/service/loop.js +94 -0
- package/dist/src/store/files.d.ts +12 -0
- package/dist/src/store/files.js +101 -0
- package/dist/src/store/index.d.ts +118 -0
- package/dist/src/store/index.js +727 -0
- package/dist/src/store/types.d.ts +90 -0
- package/dist/src/store/types.js +8 -0
- package/dist/src/worker/basic.d.ts +10 -0
- package/dist/src/worker/basic.js +27 -0
- package/dist/src/worker/custody.d.ts +13 -0
- package/dist/src/worker/custody.js +185 -0
- package/dist/src/worker/engine.d.ts +28 -0
- package/dist/src/worker/engine.js +78 -0
- package/dist/src/worker/identity.d.ts +7 -0
- package/dist/src/worker/identity.js +27 -0
- package/dist/src/worker/journal.d.ts +5 -0
- package/dist/src/worker/journal.js +30 -0
- package/dist/src/worker/supervisor-child.d.ts +1 -0
- package/dist/src/worker/supervisor-child.js +33 -0
- package/dist/src/worker/supervisor.d.ts +3 -0
- package/dist/src/worker/supervisor.js +47 -0
- package/native/runtime-manifest.json +40 -0
- package/package.json +31 -5
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
import { InputError } from '../app/source.js';
|
|
3
|
+
const specifications = {
|
|
4
|
+
init: { count: 0, flags: {} },
|
|
5
|
+
add: { count: 1, flags: { connection: 'value', kind: 'value', wait: 'boolean', language: 'value', 'force-local': 'boolean', 'allow-private-network': 'boolean' } },
|
|
6
|
+
list: { count: 0, flags: {} },
|
|
7
|
+
asset: { count: 2, flags: {} },
|
|
8
|
+
show: { count: 1, flags: { format: 'value' } },
|
|
9
|
+
delete: { count: 1, flags: {} },
|
|
10
|
+
refresh: { count: 1, flags: { wait: 'boolean', language: 'value', 'force-local': 'boolean', 'allow-private-network': 'boolean' } },
|
|
11
|
+
search: { count: 1, flags: { mode: 'value', limit: 'value', profile: 'value' } },
|
|
12
|
+
run: { count: 0, flags: { drain: 'boolean', language: 'value', 'force-local': 'boolean', 'allow-private-network': 'boolean' } },
|
|
13
|
+
setup: { count: 0, flags: { capture: 'boolean', asr: 'boolean', embedding: 'boolean' } },
|
|
14
|
+
doctor: { count: 0, flags: {} },
|
|
15
|
+
queue: { count: -1, flags: {} },
|
|
16
|
+
export: { count: 1, flags: { output: 'value' } },
|
|
17
|
+
backup: { count: 0, flags: { output: 'value' } },
|
|
18
|
+
restore: { count: 1, flags: { into: 'value' } },
|
|
19
|
+
gc: { count: 0, flags: { 'dry-run': 'boolean', apply: 'value' } },
|
|
20
|
+
worker: { count: 1, flags: { language: 'value', 'force-local': 'boolean' } },
|
|
21
|
+
index: { count: -1, flags: { profile: 'value' } },
|
|
22
|
+
import: { count: 1, flags: { apply: 'boolean', 'dry-run': 'boolean', limit: 'value', offset: 'value' } },
|
|
23
|
+
};
|
|
24
|
+
/** No shell grammar, implicit option coercion, duplicate flags, or unbounded argv. */
|
|
25
|
+
export function parseArguments(argv) {
|
|
26
|
+
if (argv.length > 32 || argv.some(arg => Buffer.byteLength(arg) > 8192 || /[\u0000-\u001f\u007f]/u.test(arg)))
|
|
27
|
+
throw new InputError();
|
|
28
|
+
if (argv.length === 0 || argv.length === 1 && ['--help', '-h'].includes(argv[0]))
|
|
29
|
+
return { command: 'help', positional: [], flags: {}, json: false };
|
|
30
|
+
if (argv.length === 1 && argv[0] === '--version')
|
|
31
|
+
return { command: 'version', positional: [], flags: {}, json: false };
|
|
32
|
+
// Global flags are valid before or after the command.
|
|
33
|
+
let command;
|
|
34
|
+
const flags = Object.create(null);
|
|
35
|
+
const positional = [];
|
|
36
|
+
const allFlags = { library: 'value', json: 'boolean' };
|
|
37
|
+
for (const spec of Object.values(specifications))
|
|
38
|
+
Object.assign(allFlags, spec.flags);
|
|
39
|
+
let literal = false;
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const arg = argv[i];
|
|
42
|
+
if (arg === '--' && !literal) {
|
|
43
|
+
literal = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg.startsWith('--') && !literal) {
|
|
47
|
+
const key = arg.slice(2);
|
|
48
|
+
const kind = (command && specifications[command]?.flags[key]) ?? allFlags[key];
|
|
49
|
+
if (!kind || Object.hasOwn(flags, key))
|
|
50
|
+
throw new InputError();
|
|
51
|
+
if (kind === 'boolean')
|
|
52
|
+
flags[key] = true;
|
|
53
|
+
else {
|
|
54
|
+
const value = argv[++i];
|
|
55
|
+
if (!value || value.startsWith('--'))
|
|
56
|
+
throw new InputError();
|
|
57
|
+
flags[key] = value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (!command)
|
|
61
|
+
command = arg;
|
|
62
|
+
else
|
|
63
|
+
positional.push(arg);
|
|
64
|
+
}
|
|
65
|
+
const spec = command && specifications[command];
|
|
66
|
+
if (!command || !spec || (command === 'queue' ? !(['status', 'recover'].includes(positional[0]) && positional.length === 1 || ['cancel', 'retry'].includes(positional[0]) && positional.length === 2) : command === 'index' ? !(['create', 'run'].includes(positional[0]) && positional.length === 1 || ['status', 'rollback'].includes(positional[0]) && positional.length === 2) : positional.length !== spec.count))
|
|
67
|
+
throw new InputError();
|
|
68
|
+
if (Object.keys(flags).some(key => !['library', 'json'].includes(key) && !Object.hasOwn(spec.flags, key)))
|
|
69
|
+
throw new InputError();
|
|
70
|
+
const library = flags.library;
|
|
71
|
+
if (library !== undefined && (typeof library !== 'string' || !isAbsolute(library)))
|
|
72
|
+
throw new InputError();
|
|
73
|
+
if (flags.kind && !['article', 'pdf', 'media'].includes(String(flags.kind)))
|
|
74
|
+
throw new InputError();
|
|
75
|
+
if (flags.format && !['markdown', 'json'].includes(String(flags.format)))
|
|
76
|
+
throw new InputError();
|
|
77
|
+
if (flags.mode && !['lexical', 'hybrid'].includes(String(flags.mode)))
|
|
78
|
+
throw new InputError();
|
|
79
|
+
if (flags.limit && (!/^[1-9]\d{0,2}$/u.test(String(flags.limit)) || Number(flags.limit) > 100))
|
|
80
|
+
throw new InputError();
|
|
81
|
+
if (command === 'run' && !flags.drain || command === 'setup' && [flags.capture, flags.asr, flags.embedding].filter(Boolean).length !== 1)
|
|
82
|
+
throw new InputError();
|
|
83
|
+
if (flags['allow-private-network'] && command !== 'run' && !flags.wait)
|
|
84
|
+
throw new InputError();
|
|
85
|
+
if (['export', 'backup'].includes(command) && (typeof flags.output !== 'string' || !isAbsolute(flags.output)))
|
|
86
|
+
throw new InputError();
|
|
87
|
+
if (command === 'restore' && (typeof flags.into !== 'string' || !isAbsolute(flags.into) || flags.library !== undefined || !isAbsolute(positional[0])))
|
|
88
|
+
throw new InputError();
|
|
89
|
+
if (command === 'gc' && (Boolean(flags['dry-run']) === Boolean(flags.apply) || flags.apply !== undefined && !isAbsolute(String(flags.apply))))
|
|
90
|
+
throw new InputError();
|
|
91
|
+
if (command === 'worker' && !['install', 'status', 'uninstall', 'serve'].includes(positional[0]))
|
|
92
|
+
throw new InputError();
|
|
93
|
+
if (command === 'import' && (!isAbsolute(positional[0]) || flags.apply && flags['dry-run'] || flags.offset !== undefined && !/^(0|[1-9][0-9]{0,5})$/.test(String(flags.offset))))
|
|
94
|
+
throw new InputError();
|
|
95
|
+
if (flags.language !== undefined && flags.language !== 'en')
|
|
96
|
+
throw new InputError();
|
|
97
|
+
if (flags['force-local'] && command === 'worker' && positional[0] === 'install')
|
|
98
|
+
throw new InputError();
|
|
99
|
+
if (flags['force-local'] && !flags.language)
|
|
100
|
+
throw new InputError();
|
|
101
|
+
if ((flags.language || flags['force-local']) && (['add', 'refresh'].includes(command) && !flags.wait || command === 'worker' && !['serve', 'install'].includes(positional[0])))
|
|
102
|
+
throw new InputError();
|
|
103
|
+
if (flags.profile && (!(command === 'search' || command === 'index' && positional[0] === 'create') || !/^[a-zA-Z0-9_-]{1,128}$/.test(String(flags.profile))))
|
|
104
|
+
throw new InputError();
|
|
105
|
+
return { command, positional, flags, library: library, json: flags.json === true || flags.format === 'json' };
|
|
106
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CaptureConnection } from '../acquisition/index.js';
|
|
2
|
+
export interface Connection {
|
|
3
|
+
id: string;
|
|
4
|
+
realm: string;
|
|
5
|
+
capture: CaptureConnection;
|
|
6
|
+
}
|
|
7
|
+
/** Local references only. Tokens, cookies and passwords never belong in this file. */
|
|
8
|
+
export declare function readConnections(root: string): Promise<Connection[]>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
|
+
import { readConfined } from '../acquisition/files.js';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { InputError } from '../app/source.js';
|
|
6
|
+
/** Local references only. Tokens, cookies and passwords never belong in this file. */
|
|
7
|
+
export async function readConnections(root) {
|
|
8
|
+
const path = join(root, 'connections.json');
|
|
9
|
+
let bytes;
|
|
10
|
+
try {
|
|
11
|
+
const stat = await lstat(path);
|
|
12
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 65536 || (stat.mode & 0o077) !== 0)
|
|
13
|
+
throw new InputError();
|
|
14
|
+
bytes = await readConfined(root, 'connections.json', 65536);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code === 'ENOENT')
|
|
18
|
+
return [];
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
let config;
|
|
22
|
+
try {
|
|
23
|
+
config = JSON.parse(bytes.toString('utf8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new InputError();
|
|
27
|
+
}
|
|
28
|
+
if (config?.schemaVersion !== 1 || !Array.isArray(config.connections) || config.connections.length > 100)
|
|
29
|
+
throw new InputError();
|
|
30
|
+
const ids = new Set();
|
|
31
|
+
return config.connections.map((entry) => {
|
|
32
|
+
if (!entry || typeof entry.id !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(entry.id) || ids.has(entry.id))
|
|
33
|
+
throw new InputError();
|
|
34
|
+
ids.add(entry.id);
|
|
35
|
+
let capture;
|
|
36
|
+
if (entry.kind === 'account' && typeof entry.stateHome === 'string' && entry.stateHome.startsWith('/') && typeof entry.accountId === 'string')
|
|
37
|
+
capture = { kind: 'account', stateHome: entry.stateHome, accountId: entry.accountId };
|
|
38
|
+
else if (entry.kind === 'cdp' && typeof entry.endpoint === 'string')
|
|
39
|
+
capture = { kind: 'cdp', endpoint: entry.endpoint };
|
|
40
|
+
else
|
|
41
|
+
throw new InputError();
|
|
42
|
+
const realm = 'connection:' + createHash('sha256').update(JSON.stringify([entry.id, capture])).digest('hex');
|
|
43
|
+
return { id: entry.id, realm, capture };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LibraryStore } from '../store/index.js';
|
|
2
|
+
import { type ImportOptions, type ImportReport } from '../importers/apply.js';
|
|
3
|
+
/** A source manifest is explicit; default execution produces a private dry-run report. */
|
|
4
|
+
export declare function importAction(store: LibraryStore, path: string, options: ImportOptions): Promise<ImportReport>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LibraryStore } from '../store/index.js';
|
|
2
|
+
import { type EmbeddingRuntime } from '../inference/embedding/index.js';
|
|
3
|
+
export declare function indexAction(store: LibraryStore, action: string, id: string | undefined, options?: {
|
|
4
|
+
runtime?: EmbeddingRuntime;
|
|
5
|
+
profile?: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { embeddingIdentity, embeddingFingerprint } from '../inference/embedding/index.js';
|
|
2
|
+
import { drainIndexQueue } from '../indexing/runner.js';
|
|
3
|
+
import { StoreError } from '../store/index.js';
|
|
4
|
+
import { NativeCatalogError } from '../models/catalog.js';
|
|
5
|
+
export async function indexAction(store, action, id, options = {}) {
|
|
6
|
+
if (action === 'status')
|
|
7
|
+
return { generation: store.indexStatus(id) };
|
|
8
|
+
if (action === 'rollback') {
|
|
9
|
+
const generation = store.indexStatus(id);
|
|
10
|
+
if (!['active', 'retired'].includes(generation.state))
|
|
11
|
+
throw new StoreError('invalid-input');
|
|
12
|
+
return { generation: store.activateIndex(id) };
|
|
13
|
+
}
|
|
14
|
+
if (!options.runtime)
|
|
15
|
+
throw new NativeCatalogError('runtime-missing');
|
|
16
|
+
if (action === 'create') {
|
|
17
|
+
const runtime = options.runtime, generation = store.createIndexGeneration({ profileId: options.profile ?? 'default', fingerprint: embeddingFingerprint(runtime.helperSha256), identity: { ...embeddingIdentity, helperSha256: runtime.helperSha256 }, dimensions: embeddingIdentity.dimensions });
|
|
18
|
+
return { generation, job: store.submitIndexJob(generation.id) };
|
|
19
|
+
}
|
|
20
|
+
return { results: await drainIndexQueue(store, { runtime: options.runtime, signal: options.signal }) };
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function main(argv: string[], version: string): Promise<number>;
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { lstat } from 'node:fs/promises';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { terminalSafe } from './terminal.js';
|
|
6
|
+
import { parseArguments } from './arguments.js';
|
|
7
|
+
import { readConnections } from './config.js';
|
|
8
|
+
import { pdfSource } from '../pdf/index.js';
|
|
9
|
+
import { articleSource, mediaSource, InputError } from '../app/source.js';
|
|
10
|
+
import { openLibrary, StoreError } from '../store/index.js';
|
|
11
|
+
import { defaultCapturePins, inspectCaptureRuntime, provisionCapture, captureSetupPlan } from '../acquisition/index.js';
|
|
12
|
+
import { queueAction } from './queue.js';
|
|
13
|
+
import { portabilityAction, restoreAction } from './portability.js';
|
|
14
|
+
import { workerAction, serveWorker, hasDueAcquisition } from './worker.js';
|
|
15
|
+
import { nativeSetupPlan, provisionNative, inspectNative } from '../models/runtime.js';
|
|
16
|
+
import { nativeDownloadAuthGuidance } from '../models/download.js';
|
|
17
|
+
import { drainIndexQueue } from '../indexing/runner.js';
|
|
18
|
+
import { createQueryEmbedder } from '../search/query.js';
|
|
19
|
+
import { indexAction } from './indexing.js';
|
|
20
|
+
import { importAction } from './import.js';
|
|
21
|
+
import { drainLibrary } from '../worker/basic.js';
|
|
22
|
+
import { failure } from '../acquisition/errors.js';
|
|
23
|
+
const help = `sponge — a local article library
|
|
24
|
+
|
|
25
|
+
init Create a private library
|
|
26
|
+
setup --capture|--asr|--embedding Download one pinned runtime
|
|
27
|
+
add URL_OR_PDF [--kind article|pdf|media] [--connection ID] [--wait]
|
|
28
|
+
run --drain Process queued articles sequentially
|
|
29
|
+
queue status Show jobs and their states
|
|
30
|
+
queue cancel JOB Cancel a queued or active job
|
|
31
|
+
queue retry JOB Retry a failed job after resolving its cause
|
|
32
|
+
queue recover Reconcile a stopped worker after cleanup
|
|
33
|
+
list List saved and queued items
|
|
34
|
+
show ITEM [--format markdown|json]
|
|
35
|
+
asset ITEM SHA Write a verified asset to stdout
|
|
36
|
+
search QUERY [--mode hybrid|lexical] [--profile default] [--limit 20]
|
|
37
|
+
refresh ITEM [--wait] Capture a new revision
|
|
38
|
+
delete ITEM Hide an item and remove it from search
|
|
39
|
+
index create [--profile default] Queue a new embedding generation
|
|
40
|
+
index status GENERATION Inspect generation coverage
|
|
41
|
+
index run Drain queued embedding jobs
|
|
42
|
+
index rollback GENERATION Reactivate a compatible complete generation
|
|
43
|
+
doctor Check library and runtime states
|
|
44
|
+
export ITEM --output DIR Export Markdown and local assets
|
|
45
|
+
backup --output FILE Save a verified offline backup
|
|
46
|
+
restore FILE --into NEW_DIR Restore into a new private library
|
|
47
|
+
gc --dry-run Write a report of unreferenced blobs
|
|
48
|
+
gc --apply REPORT Recheck and remove reported blobs
|
|
49
|
+
worker install [--language en] Enable the optional background worker
|
|
50
|
+
worker status Inspect the background worker
|
|
51
|
+
worker uninstall Stop it and preserve the library
|
|
52
|
+
import MANIFEST [--apply] Preview or import existing Wordcell captures
|
|
53
|
+
|
|
54
|
+
Global flags: --library /absolute/path --json
|
|
55
|
+
Use -- before an option-like search query. Capture setup is explicit.
|
|
56
|
+
Media: --language en enables local English ASR; --force-local bypasses captions.
|
|
57
|
+
These flags apply to --wait, run --drain, or worker serve.
|
|
58
|
+
worker install --language en saves the English ASR choice for background work.
|
|
59
|
+
For trusted local sources, use --allow-private-network with --wait or run.
|
|
60
|
+
`;
|
|
61
|
+
const codes = new Set(['grant-permission', 'repair-auth', 'unsupported', 'unavailable', 'rate-limited', 'partial', 'cleanup-required', 'cancelled', 'invalid-input', 'unclassified', 'integrity', 'limit-exceeded', 'runtime-missing', 'schema-newer', 'owner-busy']);
|
|
62
|
+
function errorCode(error, fallback) { const code = error?.code; return code && codes.has(code) ? code : fallback; }
|
|
63
|
+
function exitFor(code) { return code === 'cancelled' ? 130 : code === 'invalid-input' ? 2 : code === 'partial' ? 3 : ['grant-permission', 'repair-auth', 'cleanup-required', 'runtime-missing', 'owner-busy'].includes(code) ? 4 : 1; }
|
|
64
|
+
export async function main(argv, version) {
|
|
65
|
+
const requestId = randomUUID();
|
|
66
|
+
let store;
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const cancel = () => controller.abort();
|
|
69
|
+
process.on('SIGINT', cancel);
|
|
70
|
+
process.on('SIGTERM', cancel);
|
|
71
|
+
try {
|
|
72
|
+
const args = parseArguments(argv), emit = (data, text) => { const output = args.json ? JSON.stringify({ schemaVersion: 1, requestId, ok: true, ...data }) + '\n' : (text ?? JSON.stringify(data)) + '\n'; process.stdout.write(process.stdout.isTTY ? terminalSafe(output) : output); };
|
|
73
|
+
if (args.command === 'help') {
|
|
74
|
+
process.stdout.write(help);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
if (args.command === 'version') {
|
|
78
|
+
process.stdout.write(version + '\n');
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
if (args.command === 'restore') {
|
|
82
|
+
emit(await restoreAction(args.positional[0], String(args.flags.into)));
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
const root = args.library ?? join(homedir(), '.local', 'share', 'sponge');
|
|
86
|
+
if (args.command !== 'init') {
|
|
87
|
+
try {
|
|
88
|
+
await lstat(join(root, '.sponge-library'));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code === 'ENOENT')
|
|
92
|
+
throw new StoreError('unavailable');
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
store = await openLibrary(root);
|
|
97
|
+
if (args.command === 'init') {
|
|
98
|
+
emit({ library: store.root }, `Library: ${store.root}`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (args.command === 'worker' && args.positional[0] !== 'serve') {
|
|
102
|
+
emit(await workerAction(store, args.positional[0], version, undefined, { language: args.flags.language }));
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
if (args.command === 'export' || args.command === 'backup' || args.command === 'gc') {
|
|
106
|
+
emit(await portabilityAction(store, args.command, { itemId: args.positional[0], output: args.flags.output, dryRun: args.flags['dry-run'] === true, report: args.flags.apply }));
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const runtimeRoot = join(store.root, 'runtime', 'capture', defaultCapturePins.id);
|
|
110
|
+
const nativeRoot = join(store.root, 'runtime', 'models');
|
|
111
|
+
if (args.command === 'setup' && !args.flags.capture) {
|
|
112
|
+
const kind = args.flags.asr ? 'asr' : 'embedding', plan = await nativeSetupPlan(kind, version);
|
|
113
|
+
process.stderr.write(JSON.stringify({ setup: kind, ...plan }) + '\n');
|
|
114
|
+
try {
|
|
115
|
+
await provisionNative(kind, nativeRoot, version, controller.signal);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code === 'repair-auth' && !args.json)
|
|
119
|
+
process.stderr.write(nativeDownloadAuthGuidance + '\n');
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
emit({ [kind]: 'ready' });
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (args.command === 'setup') {
|
|
126
|
+
const plan = captureSetupPlan(defaultCapturePins);
|
|
127
|
+
process.stderr.write(`Capture setup: ${plan.downloadBytes} pinned artifact bytes, plus frozen dependency downloads. Temporary installation limit: ${plan.stagingBudgetBytes} bytes; retained runtime limit: ${plan.installationBudgetBytes} bytes; dependency installation deadline: ${plan.installationDeadlineMs / 60000} minutes. ${plan.requiredFreeBytes} free bytes required.\n`);
|
|
128
|
+
await provisionCapture({ runtimeRoot: join(store.root, 'runtime', 'capture'), pins: defaultCapturePins, signal: controller.signal });
|
|
129
|
+
emit({ capture: 'ready' }, 'Capture ready');
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
if (args.command === 'list') {
|
|
133
|
+
const items = store.list();
|
|
134
|
+
emit({ items }, items.map(item => `${item.itemId}\t${item.title}`).join('\n'));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (args.command === 'show') {
|
|
138
|
+
const item = store.read(args.positional[0]);
|
|
139
|
+
if (!item)
|
|
140
|
+
throw new StoreError('unavailable');
|
|
141
|
+
emit({ item }, item.markdown);
|
|
142
|
+
return item.status === 'partial' ? 3 : 0;
|
|
143
|
+
}
|
|
144
|
+
if (args.command === 'asset') {
|
|
145
|
+
const item = store.read(args.positional[0]), asset = item?.assets.find(a => a.sha256 === args.positional[1]);
|
|
146
|
+
if (!asset)
|
|
147
|
+
throw new StoreError('unavailable');
|
|
148
|
+
const bytes = store.readBlob(asset);
|
|
149
|
+
if (args.json)
|
|
150
|
+
emit({ asset });
|
|
151
|
+
else {
|
|
152
|
+
if (process.stdout.isTTY)
|
|
153
|
+
throw new InputError();
|
|
154
|
+
process.stdout.write(bytes);
|
|
155
|
+
}
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
if (args.command === 'index') {
|
|
159
|
+
const action = args.positional[0], runtime = ['create', 'run'].includes(action) ? await inspectNative('embedding', nativeRoot, version) : undefined;
|
|
160
|
+
const result = await indexAction(store, action, args.positional[1], { runtime, profile: args.flags.profile, signal: controller.signal });
|
|
161
|
+
emit(result);
|
|
162
|
+
const failed = result.results?.find(r => !r.ok);
|
|
163
|
+
return failed ? exitFor(failed.code) : 0;
|
|
164
|
+
}
|
|
165
|
+
if (args.command === 'search') {
|
|
166
|
+
if (args.flags.mode === 'lexical') {
|
|
167
|
+
const items = store.search(args.positional[0], Number(args.flags.limit ?? 20));
|
|
168
|
+
emit({ items }, items.map(item => `${item.itemId}\t${item.title}`).join('\n'));
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
let embedding;
|
|
172
|
+
try {
|
|
173
|
+
embedding = await inspectNative('embedding', nativeRoot, version);
|
|
174
|
+
}
|
|
175
|
+
catch { /* Hybrid explicitly reports semantic unavailability and preserves lexical results. */ }
|
|
176
|
+
const result = await store.searchHybrid(args.positional[0], { profileId: args.flags.profile, limit: Number(args.flags.limit ?? 20), signal: controller.signal, ...(embedding ? { embed: createQueryEmbedder(store, embedding) } : {}) });
|
|
177
|
+
emit({ items: result.hits, warnings: result.warnings, generation: result.generation }, [...result.hits.map(item => `${item.itemId}\t${item.title}`), ...result.warnings.map(warning => `Warning: ${warning}`)].join('\n'));
|
|
178
|
+
return controller.signal.aborted ? 130 : 0;
|
|
179
|
+
}
|
|
180
|
+
if (args.command === 'delete') {
|
|
181
|
+
store.delete(args.positional[0]);
|
|
182
|
+
emit({ itemId: args.positional[0], deleted: true }, 'Deleted');
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
if (args.command === 'queue') {
|
|
186
|
+
const result = await queueAction(store, args.positional[0], args.positional[1]);
|
|
187
|
+
emit(result);
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
if (args.command === 'doctor') {
|
|
191
|
+
let capture = 'ready';
|
|
192
|
+
try {
|
|
193
|
+
await inspectCaptureRuntime(runtimeRoot, defaultCapturePins);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
capture = errorCode(error, 'runtime-missing');
|
|
197
|
+
}
|
|
198
|
+
const states = {};
|
|
199
|
+
for (const kind of ['asr', 'embedding']) {
|
|
200
|
+
try {
|
|
201
|
+
await inspectNative(kind, nativeRoot, version);
|
|
202
|
+
states[kind] = 'ready';
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
states[kind] = errorCode(error, 'runtime-missing');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const cleanup = store.pendingCleanupCount();
|
|
209
|
+
emit({ library: 'ready', capture, ...states, pendingCleanup: cleanup }, `Library: ready\nCapture: ${capture}\nASR: ${states.asr}\nEmbedding: ${states.embedding}\nPending cleanup: ${cleanup}`);
|
|
210
|
+
return cleanup || capture !== 'ready' ? 4 : 0;
|
|
211
|
+
}
|
|
212
|
+
if (args.command === 'run' && !hasDueAcquisition(store)) {
|
|
213
|
+
if (store.ownerSnapshot())
|
|
214
|
+
throw new StoreError('owner-busy');
|
|
215
|
+
if (store.pendingCleanupCount())
|
|
216
|
+
throw Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
|
|
217
|
+
if (!store.listJobs().some(job => job.operation === 'index' && job.state === 'queued')) {
|
|
218
|
+
emit({ results: [], indexResults: [] }, 'Queue empty');
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
const runtime = await inspectNative('embedding', nativeRoot, version), indexResults = await drainIndexQueue(store, { runtime, signal: controller.signal });
|
|
222
|
+
emit({ results: [], indexResults });
|
|
223
|
+
const failed = indexResults.find(result => !result.ok);
|
|
224
|
+
return failed && !failed.ok ? exitFor(failed.code) : controller.signal.aborted ? 130 : 0;
|
|
225
|
+
}
|
|
226
|
+
const connections = await readConnections(store.root);
|
|
227
|
+
let jobId, itemId;
|
|
228
|
+
if (args.command === 'add') {
|
|
229
|
+
const connection = args.flags.connection ? connections.find(c => c.id === args.flags.connection) : undefined;
|
|
230
|
+
if (args.flags.connection && !connection)
|
|
231
|
+
throw new InputError();
|
|
232
|
+
const input = args.positional[0], kind = (args.flags.kind ?? (/\.pdf(?:[?#]|$)/i.test(input) || input.startsWith('file:') ? 'pdf' : 'article'));
|
|
233
|
+
const source = (kind === 'pdf' ? pdfSource : kind === 'media' ? mediaSource : articleSource)(input, connection?.realm ?? 'public');
|
|
234
|
+
const submission = store.submitSource({ kind, ...source, idempotencyKey: 'save:' + source.sourceKey });
|
|
235
|
+
jobId = submission.jobId;
|
|
236
|
+
itemId = submission.itemId;
|
|
237
|
+
const job = store.listJobs().find(j => j.id === jobId);
|
|
238
|
+
if (!args.flags.wait) {
|
|
239
|
+
emit({ ...submission, state: job?.state ?? 'queued' }, `${itemId}\t${job?.state ?? 'queued'}`);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
if (job?.state === 'succeeded') {
|
|
243
|
+
const item = store.read(itemId);
|
|
244
|
+
emit({ itemId, jobId, status: item.status }, `${itemId}\t${item.status}`);
|
|
245
|
+
return item.status === 'partial' ? 3 : 0;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (args.command === 'refresh') {
|
|
249
|
+
const previous = store.listJobs().find(j => j.item_id === args.positional[0]), source = previous && store.sourceForJob(String(previous.id));
|
|
250
|
+
if (!source || !['article', 'pdf', 'media'].includes(source.kind))
|
|
251
|
+
throw new StoreError('unavailable');
|
|
252
|
+
const submission = store.submitSource({ ...source, kind: source.kind, intent: 'refresh', idempotencyKey: randomUUID() });
|
|
253
|
+
jobId = submission.jobId;
|
|
254
|
+
itemId = submission.itemId;
|
|
255
|
+
if (!args.flags.wait) {
|
|
256
|
+
emit(submission, `${itemId}\tqueued`);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (jobId) {
|
|
261
|
+
const job = store.listJobs().find(j => j.id === jobId);
|
|
262
|
+
if (job && job.state !== 'queued') {
|
|
263
|
+
const code = job.state === 'cancelled' ? 'cancelled' : job.state === 'cleanup-pending' ? 'cleanup-required' : job.state === 'running' ? 'owner-busy' : errorCode({ code: job.reason }, 'unclassified');
|
|
264
|
+
throw Object.assign(Error(code), { code });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (args.command === 'worker') {
|
|
268
|
+
emit(await serveWorker(store, { signal: controller.signal, language: args.flags.language, forceLocal: args.flags['force-local'] === true, resolveCaptureRuntime: () => inspectCaptureRuntime(runtimeRoot, defaultCapturePins), resolveConnection: (realm) => { if (realm === 'public')
|
|
269
|
+
return undefined; const connection = connections.find(c => c.realm === realm); if (!connection)
|
|
270
|
+
throw Object.assign(Error('repair-auth'), { code: 'repair-auth' }); return connection.capture; } }, version));
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
let runtime;
|
|
274
|
+
try {
|
|
275
|
+
runtime = await inspectCaptureRuntime(runtimeRoot, defaultCapturePins);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
const code = errorCode(error, 'runtime-missing');
|
|
279
|
+
throw Object.assign(Error(code), { code });
|
|
280
|
+
}
|
|
281
|
+
if (args.command === 'import') {
|
|
282
|
+
const report = await importAction(store, args.positional[0], { runtime, dryRun: args.flags.apply !== true, limit: Number(args.flags.limit ?? 20), offset: Number(args.flags.offset ?? 0), signal: controller.signal });
|
|
283
|
+
emit({ report });
|
|
284
|
+
return controller.signal.aborted ? 130 : report.entries.some(entry => entry.status === 'failed' || entry.status === 'unsupported') ? 3 : 0;
|
|
285
|
+
}
|
|
286
|
+
let asrRuntime;
|
|
287
|
+
try {
|
|
288
|
+
asrRuntime = await inspectNative('asr', nativeRoot, version);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (!['runtime-missing', 'unsupported'].includes(errorCode(error, 'integrity')))
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
const workerOptions = { asrRuntime, language: args.flags.language, forceLocal: args.flags['force-local'] === true, runtime, jobId, signal: controller.signal, allowPrivateNetwork: args.flags['allow-private-network'] === true, resolveConnection: (realm) => { if (realm === 'public')
|
|
295
|
+
return undefined; const connection = connections.find(c => c.realm === realm); if (!connection)
|
|
296
|
+
throw Object.assign(Error('repair-auth'), { code: 'repair-auth' }); return connection.capture; } };
|
|
297
|
+
const results = await drainLibrary(store, workerOptions);
|
|
298
|
+
if (jobId && !results.length && !controller.signal.aborted) {
|
|
299
|
+
const job = store.listJobs().find(j => j.id === jobId);
|
|
300
|
+
const code = job?.state === 'cancelled' ? 'cancelled' : job?.state === 'cleanup-pending' ? 'cleanup-required' : job?.state === 'running' ? 'owner-busy' : errorCode({ code: job?.reason }, 'unclassified');
|
|
301
|
+
throw Object.assign(Error(code), { code });
|
|
302
|
+
}
|
|
303
|
+
let indexResults = [];
|
|
304
|
+
if (args.command === 'run' && !controller.signal.aborted && store.listJobs().some(job => job.operation === 'index' && job.state === 'queued')) {
|
|
305
|
+
const embedding = await inspectNative('embedding', nativeRoot, version);
|
|
306
|
+
indexResults = await drainIndexQueue(store, { runtime: embedding, signal: controller.signal });
|
|
307
|
+
const indexFailed = indexResults.find(result => !result.ok);
|
|
308
|
+
if (indexFailed && !indexFailed.ok) {
|
|
309
|
+
process.stderr.write(JSON.stringify(indexFailed) + '\n');
|
|
310
|
+
return exitFor(indexFailed.code);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const failed = results.find(result => !result.ok);
|
|
314
|
+
if (failed && !failed.ok) {
|
|
315
|
+
process.stderr.write(JSON.stringify(failed) + '\n');
|
|
316
|
+
return exitFor(failed.code);
|
|
317
|
+
}
|
|
318
|
+
if (controller.signal.aborted)
|
|
319
|
+
return 130;
|
|
320
|
+
emit({ results, indexResults }, results.map(result => result.ok ? `${result.itemId}\t${result.status}` : '').join('\n') || 'Queue empty');
|
|
321
|
+
return results.some(result => result.ok && result.status === 'partial') ? 3 : 0;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
const value = error?.code, code = value && codes.has(value) ? value : 'unclassified';
|
|
325
|
+
process.stderr.write(JSON.stringify(failure(requestId, code)) + '\n');
|
|
326
|
+
return exitFor(code);
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
store?.close();
|
|
330
|
+
process.off('SIGINT', cancel);
|
|
331
|
+
process.off('SIGTERM', cancel);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LibraryStore } from '../store/index.js';
|
|
2
|
+
import { restoreLibrary } from '../portability/backup.js';
|
|
3
|
+
export declare function portabilityAction(store: LibraryStore, action: 'export' | 'backup' | 'gc', options: {
|
|
4
|
+
itemId?: string;
|
|
5
|
+
output?: string;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
report?: string;
|
|
8
|
+
}): Promise<Record<string, unknown>>;
|
|
9
|
+
export declare const restoreAction: typeof restoreLibrary;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { fstatSync, closeSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { StoreError } from '../store/types.js';
|
|
5
|
+
import { openRead, readExact, writePrivateExclusive } from '../portability/files.js';
|
|
6
|
+
import { backupLibrary, restoreLibrary } from '../portability/backup.js';
|
|
7
|
+
import { exportItem } from '../portability/export.js';
|
|
8
|
+
import { createGcReport, applyGcReport } from '../portability/gc.js';
|
|
9
|
+
export async function portabilityAction(store, action, options) {
|
|
10
|
+
if (action === 'export') {
|
|
11
|
+
if (!options.itemId || !options.output)
|
|
12
|
+
throw new StoreError('invalid-input');
|
|
13
|
+
return exportItem(store, options.itemId, options.output);
|
|
14
|
+
}
|
|
15
|
+
if (action === 'backup') {
|
|
16
|
+
if (!options.output)
|
|
17
|
+
throw new StoreError('invalid-input');
|
|
18
|
+
return backupLibrary(store, options.output);
|
|
19
|
+
}
|
|
20
|
+
if (action !== 'gc' || Boolean(options.dryRun) === Boolean(options.report))
|
|
21
|
+
throw new StoreError('invalid-input');
|
|
22
|
+
if (options.dryRun) {
|
|
23
|
+
const report = createGcReport(store), path = join(store.root, 'runtime', `gc-${randomUUID()}.json`);
|
|
24
|
+
writePrivateExclusive(path, JSON.stringify(report) + '\n');
|
|
25
|
+
return { report: path, candidates: report.candidates.length, bytes: report.candidates.reduce((n, c) => n + c.bytes, 0) };
|
|
26
|
+
}
|
|
27
|
+
const fd = openRead(options.report);
|
|
28
|
+
try {
|
|
29
|
+
const bytes = fstatSync(fd).size;
|
|
30
|
+
if (bytes > 32 * 1024 ** 2)
|
|
31
|
+
throw new StoreError('limit-exceeded');
|
|
32
|
+
return applyGcReport(store, JSON.parse(readExact(fd, bytes).toString('utf8')));
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Restore must dispatch before opening the normal library; it never targets the default root.
|
|
39
|
+
export const restoreAction = restoreLibrary;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { StoreError } from '../store/index.js';
|
|
2
|
+
import { recoverWorker } from '../worker/custody.js';
|
|
3
|
+
export async function queueAction(store, action, jobId) {
|
|
4
|
+
if (action === 'status') {
|
|
5
|
+
if (jobId)
|
|
6
|
+
return { jobs: store.listJobs().filter(j => j.id === jobId) };
|
|
7
|
+
return { jobs: store.listJobs(), owner: store.ownerSnapshot(), pendingCleanup: store.pendingCleanupCount() };
|
|
8
|
+
}
|
|
9
|
+
if (action === 'recover') {
|
|
10
|
+
if (jobId)
|
|
11
|
+
throw new StoreError('invalid-input');
|
|
12
|
+
return await recoverWorker(store);
|
|
13
|
+
}
|
|
14
|
+
if (!jobId)
|
|
15
|
+
throw new StoreError('invalid-input');
|
|
16
|
+
if (action === 'cancel')
|
|
17
|
+
store.requestCancellation(jobId);
|
|
18
|
+
else if (action === 'retry')
|
|
19
|
+
store.retryJob(jobId, { automatic: false });
|
|
20
|
+
else
|
|
21
|
+
throw new StoreError('invalid-input');
|
|
22
|
+
return { jobId, action };
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LibraryStore } from '../store/index.js';
|
|
2
|
+
import { drainLibrary } from '../worker/basic.js';
|
|
3
|
+
import { type ServiceEnvironment } from '../service/launch-agent.js';
|
|
4
|
+
export declare function workerAction(store: LibraryStore, action: 'install' | 'status' | 'uninstall', version: string, environment?: ServiceEnvironment, options?: {
|
|
5
|
+
language?: 'en';
|
|
6
|
+
}): Promise<Record<string, unknown>>;
|
|
7
|
+
type DrainOptions = Parameters<typeof drainLibrary>[1];
|
|
8
|
+
export type ServiceDrainOptions = Omit<DrainOptions, 'runtime'> & {
|
|
9
|
+
runtime?: DrainOptions['runtime'];
|
|
10
|
+
resolveCaptureRuntime?: () => Promise<DrainOptions['runtime']>;
|
|
11
|
+
};
|
|
12
|
+
/** Dispatch only work that claimNext can admit; delayed retries retain their due time. */
|
|
13
|
+
export declare function hasDueAcquisition(store: LibraryStore): boolean;
|
|
14
|
+
export declare function drainServiceQueues(store: LibraryStore, options: ServiceDrainOptions, version: string, signal: AbortSignal): Promise<readonly unknown[]>;
|
|
15
|
+
export declare function serveWorker(store: LibraryStore, options: ServiceDrainOptions, version: string, environment?: ServiceEnvironment): Promise<Record<string, unknown>>;
|
|
16
|
+
export {};
|