@metagptx/deepflow-suite-cli 0.2.11-dev.20261020.1 → 0.3.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 +44 -50
- package/bin/deepflow-suite.js +4 -4
- package/bundle/deepflow-release.json +21 -46
- package/bundle/skills/deepflow-suite/SKILL.md +7 -1
- package/bundle/skills/df-agentctrl/SKILL.md +5 -1
- package/bundle/skills/df-agentrt/SKILL.md +15 -0
- package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
- package/bundle/skills/df-task/SKILL.md +4 -0
- package/bundle/skills/df-taskdriver/SKILL.md +5 -1
- package/bundle/skills/df-workbench/SKILL.md +10 -2
- package/manifest.json +12 -30
- package/package.json +8 -17
- package/runtime/df-agentctrl/commands.mjs +93 -0
- package/runtime/df-agentctrl/df-agentctrl.mjs +119 -0
- package/runtime/df-agentctrl/release.json +1 -0
- package/runtime/df-agentrt/agentrt.mjs +3 -0
- package/runtime/df-agentrt/df-agentrt.mjs +3 -0
- package/runtime/df-agentrt/release.json +1 -0
- package/runtime/df-agentrt/result.mjs +44 -0
- package/runtime/df-agentrt/runtime.mjs +358 -0
- package/runtime/df-agentrt/usage.mjs +16 -0
- package/runtime/df-task/commands.mjs +188 -0
- package/runtime/df-task/df-task.mjs +167 -0
- package/runtime/df-task/protocol.mjs +3 -0
- package/runtime/df-task/read.mjs +127 -0
- package/runtime/df-task/release.json +1 -0
- package/runtime/df-taskdriver/df-taskdriver.mjs +131 -0
- package/runtime/df-taskdriver/release.json +1 -0
- package/runtime/{df-workbench.mjs → df-workbench/df-workbench.mjs} +11 -2
- package/runtime/df-workbench/release.json +1 -0
- package/scripts/build-package.mjs +29 -25
- package/src/bundle.mjs +4 -1
- package/src/cli-install.mjs +35 -125
- package/src/commands/doctor.mjs +2 -0
- package/src/linker.mjs +2 -2
- package/src/manifest.mjs +7 -13
- package/src/package-cli.mjs +44 -60
- package/bin/df-agentctrl.js +0 -10
- package/bin/df-agentrt.js +0 -10
- package/bin/df-task.js +0 -10
- package/bin/df-taskdriver.js +0 -10
- package/bin/df-workbench.js +0 -10
- package/bundle/skills/deepflow-suite/manifest.yaml +0 -20
- package/bundle/skills/df-task/skill_test.go +0 -201
- package/bundle/skills/df-workbench/manifest.yaml +0 -5
- package/scripts/build-runtime-packages.mjs +0 -38
- package/skills/deepflow-suite/SKILL.md +0 -81
- package/skills/deepflow-suite/manifest.yaml +0 -20
- package/src/cli-launcher.mjs +0 -71
- package/src/runtime-build.mjs +0 -193
- package/src/runtime-package.mjs +0 -31
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { help, commands } from './commands.mjs';
|
|
6
|
+
import { factTypes, actorKinds } from './protocol.mjs';
|
|
7
|
+
import { validateReadOptions, projectResponse, readAllFacts, readResponse } from './read.mjs';
|
|
8
|
+
|
|
9
|
+
const uuid = /^(?:urn:uuid:)?(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\})$/i;
|
|
10
|
+
const valueFlags = new Set(['--base-url', ...Object.values(commands).flatMap((spec) => spec.flags).filter((flag) => flag !== '--all')]);
|
|
11
|
+
export function parse(args) {
|
|
12
|
+
const o = { command: '', args: [], values: {}, json: false, help: false };
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (!arg.startsWith('-')) { if (!o.command) o.command = arg; else o.args.push(arg); continue; }
|
|
17
|
+
const equals = arg.indexOf('=');
|
|
18
|
+
const key = equals < 0 ? arg : arg.slice(0, equals);
|
|
19
|
+
if (seen.has(key)) throw Error(`duplicate option ${key}`);
|
|
20
|
+
seen.add(key);
|
|
21
|
+
if (['--help', '-h', '--json'].includes(key)) {
|
|
22
|
+
if (equals >= 0) throw Error(`${key} does not take a value`);
|
|
23
|
+
o[key === '--json' ? 'json' : 'help'] = true;
|
|
24
|
+
} else if (key === '--all') {
|
|
25
|
+
if (equals >= 0) throw Error(`${key} does not take a value`);
|
|
26
|
+
o.values[key] = 'true';
|
|
27
|
+
} else if (valueFlags.has(key)) {
|
|
28
|
+
if (equals < 0 && (i + 1 === args.length || args[i + 1].startsWith('--'))) throw Error(`${key} requires a value`);
|
|
29
|
+
o.values[key] = equals < 0 ? args[++i] : arg.slice(equals + 1);
|
|
30
|
+
} else throw Error(`unknown option ${key}`);
|
|
31
|
+
}
|
|
32
|
+
o.command ||= 'help';
|
|
33
|
+
if (o.command === 'help' && o.args.length === 1) { o.command = o.args.pop(); o.help = true; }
|
|
34
|
+
return o;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validate(o, env) {
|
|
38
|
+
const spec = Object.hasOwn(commands, o.command) ? commands[o.command] : undefined;
|
|
39
|
+
if (!spec) throw Error(`unknown command ${o.command}`);
|
|
40
|
+
for (const key of Object.keys(o.values)) if (key !== '--base-url' && !spec.flags.includes(key)) throw Error(`${key} is not supported by ${o.command}`);
|
|
41
|
+
for (const key of spec.required) if (!o.values[key]?.trim()) throw Error(`${o.command} requires ${key}`);
|
|
42
|
+
if (spec.needsID) {
|
|
43
|
+
if (o.args.length !== 1) throw Error(`${o.command} requires exactly one task-id`);
|
|
44
|
+
if (!uuid.test(o.args[0])) throw Error('task-id must be a UUID');
|
|
45
|
+
} else if (o.args.length) throw Error(`${o.command} does not accept positional arguments`);
|
|
46
|
+
for (const key of ['--type', '--types']) if (key in o.values) {
|
|
47
|
+
for (const raw of o.values[key].split(',')) {
|
|
48
|
+
const name = raw.trim();
|
|
49
|
+
if (!name) throw Error('--types must not contain an empty entry');
|
|
50
|
+
if (!factTypes.includes(name)) throw Error(`unknown fact type ${name}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ('--run' in o.values && !o.values['--run'].trim()) throw Error('--run must not be empty');
|
|
54
|
+
if ('--metadata' in o.values) object(o.values['--metadata'], '--metadata must be a JSON object');
|
|
55
|
+
for (const key of ['--limit', '--after']) if (key in o.values) {
|
|
56
|
+
const raw = o.values[key];
|
|
57
|
+
if (!/^[+]?\d+$/.test(raw) || BigInt(raw) > 9223372036854775807n || (key === '--limit' && BigInt(raw) === 0n)) throw Error(`${key} must be a non-negative integer`);
|
|
58
|
+
}
|
|
59
|
+
if (spec.writes) {
|
|
60
|
+
if (!env.DF_TASK_ACTOR_KIND?.trim() || !env.DF_TASK_ACTOR_ID?.trim()) throw Error('writing a fact requires DF_TASK_ACTOR_KIND and DF_TASK_ACTOR_ID');
|
|
61
|
+
if (!actorKinds.includes(env.DF_TASK_ACTOR_KIND)) throw Error(`unknown DF_TASK_ACTOR_KIND ${env.DF_TASK_ACTOR_KIND}`);
|
|
62
|
+
if (o.command === 'metadata' && env.DF_TASK_ACTOR_KIND !== 'human') throw Error('metadata requires DF_TASK_ACTOR_KIND=human');
|
|
63
|
+
}
|
|
64
|
+
validateReadOptions(o);
|
|
65
|
+
}
|
|
66
|
+
function object(raw, message) {
|
|
67
|
+
let value;
|
|
68
|
+
try { value = JSON.parse(raw); } catch { throw Error(message); }
|
|
69
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') throw Error(message);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function query(values) {
|
|
73
|
+
const pairs = Object.entries(values).filter(([, value]) => value !== undefined && value !== '');
|
|
74
|
+
pairs.sort(([a], [b]) => a.localeCompare(b));
|
|
75
|
+
const q = new URLSearchParams(pairs).toString();
|
|
76
|
+
return q ? `?${q}` : '';
|
|
77
|
+
}
|
|
78
|
+
export async function requestFor(o) {
|
|
79
|
+
const v = o.values, c = o.command;
|
|
80
|
+
const path = o.args.length ? `/api/v1/tasks/${encodeURIComponent(o.args[0])}` : '/api/v1/tasks';
|
|
81
|
+
const metadata = { metadata_source: v['--metadata-source'], metadata_query_key: v['--metadata-query-key'] };
|
|
82
|
+
const post = (p, body) => ({ method: 'POST', path: p, body: JSON.stringify(body) });
|
|
83
|
+
if (c === 'ls') return { method: 'GET', path: path + query({ ...metadata, limit: v['--limit'] || '50' }) };
|
|
84
|
+
if (c === 'get') return { method: 'GET', path: path + query(metadata) };
|
|
85
|
+
if (c === 'facts' || c === 'stream') return { method: 'GET', path: (c === 'facts' ? `${path}/facts` : '/api/v1/facts') + query({ ...metadata, after_seq: v['--after'], types: v['--types'], limit: v['--limit'] }) };
|
|
86
|
+
// Preserve user JSON bytes, including integer precision and unknown fields.
|
|
87
|
+
if (c === 'create') return { method: 'POST', path, body: `{"title":${JSON.stringify(v['--title'])},"description":${JSON.stringify(v['--description'] || '')}${v['--message'] ? `,"first_message":${JSON.stringify(v['--message'])}` : ''}${v['--metadata'] ? `,"metadata":${v['--metadata']}` : ''}}` };
|
|
88
|
+
if (c === 'title') return post(`${path}/title`, { title: v['--title'] });
|
|
89
|
+
if (c === 'metadata') return { method: 'POST', path: `${path}/metadata${query(metadata)}`, body: `{"metadata":${v['--metadata']}}` };
|
|
90
|
+
if (c === 'append') {
|
|
91
|
+
const raw = await readFile(v['--file'], 'utf8');
|
|
92
|
+
object(raw, `${v['--file']} must contain a JSON object`);
|
|
93
|
+
return { method: 'POST', path: `${path}/facts`, body: `{"type":${JSON.stringify(v['--type'])},"payload":${raw}${v['--idempotency-key'] ? `,"idempotency_key":${JSON.stringify(v['--idempotency-key'])}` : ''}}` };
|
|
94
|
+
}
|
|
95
|
+
let type, payload;
|
|
96
|
+
if (c === 'dispatch') { type = 'dispatch_intent'; payload = { agent: v['--agent'], instruction: v['--instruction'], mode: v['--run'] ? 'message' : 'new_run', ...(v['--run'] ? { target_run_id: v['--run'] } : {}) }; }
|
|
97
|
+
if (c === 'wait') { type = 'waiting_reason'; payload = { waiting_on: v['--on'], reason: v['--reason'], ...(v['--waiting-run'] ? { waiting_run_id: v['--waiting-run'] } : {}) }; }
|
|
98
|
+
if (c === 'handoff') { type = 'handoff_to_human'; payload = { ask: v['--ask'], ...(v['--assignee'] ? { assignee: v['--assignee'] } : {}) }; }
|
|
99
|
+
return post(`${path}/facts`, { type, payload });
|
|
100
|
+
}
|
|
101
|
+
function human(command, data, stdout) {
|
|
102
|
+
const page = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value) || {};
|
|
103
|
+
if (typeof page !== 'object' || Array.isArray(page) || (page.tasks != null && !Array.isArray(page.tasks)) || (page.facts != null && !Array.isArray(page.facts))) { stdout.write(pretty(data) + '\n'); return; }
|
|
104
|
+
if (command === 'ls') {
|
|
105
|
+
for (const task of page.tasks || []) stdout.write(`${task.id} ${task.updated_at} ${task.title}\n`);
|
|
106
|
+
if (!page.tasks?.length) stdout.write('No tasks.\n');
|
|
107
|
+
} else if (command === 'facts' || command === 'stream') {
|
|
108
|
+
for (const f of page.facts || []) {
|
|
109
|
+
const payload = Array.from(JSON.stringify(f.payload, (_key, value) => typeof value === 'bigint' ? JSON.rawJSON(value.toString()) : value));
|
|
110
|
+
const text = payload.length > 120 ? payload.slice(0, 117).join('') + '...' : payload.join('');
|
|
111
|
+
stdout.write(`${String(f.seq).padEnd(8)} ${f.created_at || ''} ${f.task_id ? f.task_id + ' ' : ''}${(f.type || '').padEnd(20)} ${f.actor_kind || ''}:${f.actor_id || ''} ${text}\n`);
|
|
112
|
+
}
|
|
113
|
+
stdout.write(`next_after_seq=${page.next_after_seq || 0}${command === 'stream' ? ` max_seq=${page.max_seq || 0}` : ''}\n`);
|
|
114
|
+
} else stdout.write(pretty(data) + '\n');
|
|
115
|
+
}
|
|
116
|
+
function pretty(raw) {
|
|
117
|
+
const tokens = raw.match(/"(?:\\.|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) || [];
|
|
118
|
+
let level = 0, out = '';
|
|
119
|
+
const newline = () => '\n' + ' '.repeat(level);
|
|
120
|
+
tokens.forEach((token, i) => {
|
|
121
|
+
if (token === '{' || token === '[') { out += token; level++; if (tokens[i + 1] !== (token === '{' ? '}' : ']')) out += newline(); }
|
|
122
|
+
else if (token === '}' || token === ']') { level--; if (tokens[i - 1] !== (token === '}' ? '{' : '[')) out += newline(); out += token; }
|
|
123
|
+
else out += token === ',' ? ',' + newline() : token === ':' ? ': ' : token;
|
|
124
|
+
});
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
export async function run(args, { env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
128
|
+
if (args[0] === '--cli-version') {
|
|
129
|
+
let version = 'workspace';
|
|
130
|
+
try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
|
|
131
|
+
const report = { name: 'df-task', version, entry: fileURLToPath(import.meta.url) };
|
|
132
|
+
stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-task ${version}`) + '\n');
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
let o, base, initialRequest;
|
|
136
|
+
try {
|
|
137
|
+
o = parse(args);
|
|
138
|
+
if (o.command === 'help' && !o.args.length) { stdout.write(help); return 0; }
|
|
139
|
+
if (o.help) {
|
|
140
|
+
const spec = Object.hasOwn(commands, o.command) ? commands[o.command] : undefined; if (!spec) throw Error(`unknown command ${o.command}`);
|
|
141
|
+
stdout.write(`df-task ${spec.usage} [--base-url <url>] [--json]\n\n${spec.description}\n`); return 0;
|
|
142
|
+
}
|
|
143
|
+
validate(o, env);
|
|
144
|
+
base = o.values['--base-url'] || env.DF_TASK_BASE_URL || 'http://127.0.0.1:8090';
|
|
145
|
+
const url = new URL(base);
|
|
146
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.search || url.hash || url.username || url.password) throw Error('invalid base URL');
|
|
147
|
+
initialRequest = await requestFor(o);
|
|
148
|
+
} catch (error) { stderr.write(error.message + '\n'); return 2; }
|
|
149
|
+
const request = async (options, limit = 0) => {
|
|
150
|
+
const req = options === o ? initialRequest : await requestFor(options);
|
|
151
|
+
const headers = req.body ? { 'Content-Type': 'application/json' } : {};
|
|
152
|
+
if (req.method !== 'GET') { headers['X-DF-Actor-Kind'] = env.DF_TASK_ACTOR_KIND; headers['X-DF-Actor-Id'] = env.DF_TASK_ACTOR_ID; }
|
|
153
|
+
const response = await fetch(base.replace(/\/+$/, '') + req.path, { method: req.method, body: req.body, headers, signal: AbortSignal.timeout(30000) });
|
|
154
|
+
const data = await readResponse(response, limit);
|
|
155
|
+
if (response.status >= 400) throw Error(`HTTP ${response.status}: ${data.trim()}`);
|
|
156
|
+
try { JSON.parse(data); } catch { throw Error('server returned invalid JSON'); }
|
|
157
|
+
return data;
|
|
158
|
+
};
|
|
159
|
+
if (o.values['--all']) return readAllFacts(o, base, request, stdout, stderr);
|
|
160
|
+
try {
|
|
161
|
+
let data = await request(o, '--fields' in o.values ? 64 << 20 : 0);
|
|
162
|
+
if ('--fields' in o.values) data = projectResponse(o.command, data, o.values['--fields'].split(','));
|
|
163
|
+
if (o.json) stdout.write(data + '\n'); else human(o.command, data, stdout);
|
|
164
|
+
return 0;
|
|
165
|
+
} catch (error) { stderr.write(error.message + '\n'); return 1; }
|
|
166
|
+
}
|
|
167
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Generated by generate-protocol.mjs from internal/model/fact.go.
|
|
2
|
+
export const factTypes = ["task_lifecycle","user_message","external_result","artifact_published","summary","help_requested","run_outcome","dispatch_intent","waiting_reason","handoff_to_human","adjudication","run_terminated","invocation_started","invocation_accepted","invocation_ended","attempt_failed"];
|
|
3
|
+
export const actorKinds = ["human","external","task_run","scheduler_run","driver"];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdtemp, open, link, rm } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Retain every numeric token verbatim, including values outside JS precision.
|
|
6
|
+
export const decodeRaw = (raw) => JSON.parse(raw, (_key, value, context) => typeof value === 'number' ? JSON.rawJSON(context.source) : value);
|
|
7
|
+
export const encodeRaw = (value) => JSON.stringify(value);
|
|
8
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && !JSON.isRawJSON(value);
|
|
9
|
+
const seq = (value) => {
|
|
10
|
+
if (!JSON.isRawJSON(value) || !/^-?\d+$/.test(value.rawJSON)) throw Error('invalid facts sequence');
|
|
11
|
+
const n = BigInt(value.rawJSON);
|
|
12
|
+
if (n < 0n || n > 9223372036854775807n) throw Error('invalid facts sequence');
|
|
13
|
+
return n;
|
|
14
|
+
};
|
|
15
|
+
const rawSeq = (n) => JSON.rawJSON(BigInt(n).toString());
|
|
16
|
+
|
|
17
|
+
export function validateReadOptions(o) {
|
|
18
|
+
const v = o.values;
|
|
19
|
+
if ('--fields' in v) {
|
|
20
|
+
if (!o.json) throw Error('--fields requires --json');
|
|
21
|
+
const roots = o.command === 'get' ? ['id', 'title', 'description', 'description_version', 'metadata', 'created_at', 'updated_at'] : ['seq', 'type', 'actor_kind', 'actor_id', 'payload', 'idempotency_key', 'created_at'];
|
|
22
|
+
const paths = v['--fields'].split(',');
|
|
23
|
+
paths.forEach((field, i) => {
|
|
24
|
+
const parts = field.split('.');
|
|
25
|
+
if (!roots.includes(parts[0]) || (parts.length > 1 && !['metadata', 'payload'].includes(parts[0]))) throw Error(`unsupported field path ${JSON.stringify(field)} for ${o.command}`);
|
|
26
|
+
if (parts.some((part) => !part || part.trim() !== part || /[\[\]*\/\\\t\r\n]/.test(part))) throw Error(`invalid object field path ${JSON.stringify(field)}`);
|
|
27
|
+
if (paths.slice(0, i).some((earlier) => earlier === field || earlier.startsWith(field + '.') || field.startsWith(earlier + '.'))) throw Error('overlapping field paths');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const all = '--all' in v;
|
|
31
|
+
if (all && (!o.json || !v['--output-file']?.trim() || '--fields' in v)) throw Error('--all requires --json and --output-file, and cannot be combined with --fields');
|
|
32
|
+
for (const flag of ['--output-file', '--max-pages', '--max-bytes']) if (flag in v && !all) throw Error(`${flag} requires --all`);
|
|
33
|
+
for (const [flag, max] of [['--max-pages', 100], ['--max-bytes', 64 << 20]]) {
|
|
34
|
+
if (flag in v && (!/^\+?\d+$/.test(v[flag]) || BigInt(v[flag]) < 1n || BigInt(v[flag]) > BigInt(max))) throw Error(`${flag} must be between 1 and ${max}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectObject(source, paths) {
|
|
39
|
+
if (!isObject(source)) throw Error('expected a JSON object for field selection');
|
|
40
|
+
const selected = Object.create(null), children = new Map();
|
|
41
|
+
for (const field of paths) {
|
|
42
|
+
const [root, ...parts] = field.split('.');
|
|
43
|
+
if (parts.length) { if (!children.has(root)) children.set(root, []); children.get(root).push(parts.join('.')); }
|
|
44
|
+
else if (Object.hasOwn(source, root)) selected[root] = source[root];
|
|
45
|
+
}
|
|
46
|
+
for (const [root, subpaths] of children) {
|
|
47
|
+
if (!Object.hasOwn(source, root) || !isObject(source[root])) continue;
|
|
48
|
+
const projected = projectObject(source[root], subpaths);
|
|
49
|
+
if (Object.keys(projected).length) selected[root] = projected;
|
|
50
|
+
}
|
|
51
|
+
return selected;
|
|
52
|
+
}
|
|
53
|
+
export function projectResponse(command, raw, paths) {
|
|
54
|
+
const data = decodeRaw(raw);
|
|
55
|
+
if (command === 'get') return encodeRaw(projectObject(data, paths));
|
|
56
|
+
if (!isObject(data) || !Array.isArray(data.facts)) throw Error('expected facts array');
|
|
57
|
+
data.facts = data.facts.map((fact) => projectObject(fact, paths));
|
|
58
|
+
return encodeRaw(data);
|
|
59
|
+
}
|
|
60
|
+
const normalizedID = (id) => typeof id === 'string' ? id.toLowerCase().replace(/^urn:uuid:/, '').replace(/[{}-]/g, '') : '';
|
|
61
|
+
export function validateFactPage(raw, taskID, after) {
|
|
62
|
+
const page = decodeRaw(raw);
|
|
63
|
+
if (!isObject(page) || !Array.isArray(page.facts) || !Object.hasOwn(page, 'next_after_seq')) throw Error('invalid facts page identity, facts or cursor');
|
|
64
|
+
if (!/^[0-9a-f]{32}$/.test(normalizedID(page.task_id)) || normalizedID(page.task_id) !== normalizedID(taskID)) throw Error('facts page task_id does not match query');
|
|
65
|
+
let last = after;
|
|
66
|
+
for (const fact of page.facts) {
|
|
67
|
+
if (!isObject(fact) || typeof fact.type !== 'string' || !fact.type || seq(fact.seq) <= last) throw Error(`facts page is not strictly ordered after ${after}`);
|
|
68
|
+
last = seq(fact.seq);
|
|
69
|
+
}
|
|
70
|
+
if (seq(page.next_after_seq) !== last) throw Error(`facts cursor does not match last seq ${last}`);
|
|
71
|
+
return { facts: page.facts, next: last };
|
|
72
|
+
}
|
|
73
|
+
export async function readResponse(response, limit = 0) {
|
|
74
|
+
if (!limit) return response.text();
|
|
75
|
+
let bytes = 0; const chunks = [];
|
|
76
|
+
for await (const chunk of response.body) {
|
|
77
|
+
bytes += chunk.length;
|
|
78
|
+
if (bytes > limit) throw Object.assign(Error('read byte budget exhausted'), { byteLimit: true });
|
|
79
|
+
chunks.push(chunk);
|
|
80
|
+
}
|
|
81
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readAllFacts(o, baseURL, request, stdout, stderr) {
|
|
85
|
+
const output = path.resolve(o.values['--output-file']);
|
|
86
|
+
let temp;
|
|
87
|
+
try {
|
|
88
|
+
try { await lstat(output); throw Error(`output file must not exist: ${output}`); }
|
|
89
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
90
|
+
temp = await mkdtemp(path.join(path.dirname(output), '.task-facts-'));
|
|
91
|
+
} catch (error) { stderr.write(error.message + '\n'); return 2; }
|
|
92
|
+
try {
|
|
93
|
+
const query = { task_id: o.args[0], base_url: baseURL, after_seq: '0', limit: '200' };
|
|
94
|
+
for (const flag of ['--after', '--types', '--limit', '--metadata-source', '--metadata-query-key']) if (flag in o.values) query[flag === '--after' ? 'after_seq' : flag.slice(2).replaceAll('-', '_')] = o.values[flag];
|
|
95
|
+
const manifest = { query, complete: false, stop_reason: 'page_limit', pages: 0, count: 0, next_after_seq: rawSeq(query.after_seq), type_counts: Object.create(null), response_bytes: 0, output_file: output };
|
|
96
|
+
const facts = [], budget = Number(o.values['--max-bytes'] || (8 << 20));
|
|
97
|
+
let cursor = BigInt(query.after_seq);
|
|
98
|
+
for (let i = 0; i < Number(o.values['--max-pages'] || 10); i++) {
|
|
99
|
+
const remaining = budget - manifest.response_bytes;
|
|
100
|
+
if (remaining <= 0) { manifest.stop_reason = 'byte_limit'; break; }
|
|
101
|
+
let raw, page;
|
|
102
|
+
try { raw = await request({ ...o, values: { ...o.values, '--after': String(cursor) } }, remaining); }
|
|
103
|
+
catch (error) { manifest.stop_reason = error.byteLimit ? 'byte_limit' : 'request_error'; stderr.write(error.message + '\n'); break; }
|
|
104
|
+
manifest.response_bytes += Buffer.byteLength(raw);
|
|
105
|
+
try { page = validateFactPage(raw, o.args[0], cursor); }
|
|
106
|
+
catch (error) { manifest.stop_reason = 'invalid_page'; stderr.write(error.message + '\n'); break; }
|
|
107
|
+
manifest.pages++;
|
|
108
|
+
if (!page.facts.length) { manifest.complete = true; manifest.stop_reason = 'empty_page'; break; }
|
|
109
|
+
for (const fact of page.facts) {
|
|
110
|
+
if (!manifest.count) manifest.first_seq = fact.seq;
|
|
111
|
+
manifest.last_seq = fact.seq;
|
|
112
|
+
manifest.type_counts[fact.type] = (manifest.type_counts[fact.type] || 0) + 1;
|
|
113
|
+
manifest.count++;
|
|
114
|
+
}
|
|
115
|
+
facts.push(...page.facts); cursor = page.next; manifest.next_after_seq = rawSeq(cursor);
|
|
116
|
+
}
|
|
117
|
+
const data = Buffer.from(encodeRaw({ query, facts, complete: manifest.complete, stop_reason: manifest.stop_reason, next_after_seq: manifest.next_after_seq }));
|
|
118
|
+
const staging = path.join(temp, 'facts.json'), file = await open(staging, 'wx', 0o600);
|
|
119
|
+
try { await file.writeFile(data); } finally { await file.close(); }
|
|
120
|
+
await link(staging, output);
|
|
121
|
+
manifest.file_bytes = data.length; manifest.file_sha256 = createHash('sha256').update(data).digest('hex');
|
|
122
|
+
stdout.write(encodeRaw(manifest) + '\n');
|
|
123
|
+
if (!manifest.complete) { stderr.write(`incomplete facts read: ${manifest.stop_reason}; next_after_seq=${cursor}\n`); return 1; }
|
|
124
|
+
return 0;
|
|
125
|
+
} catch (error) { stderr.write(error.message + '\n'); return 1; }
|
|
126
|
+
finally { await rm(temp, { recursive: true, force: true }); }
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.3.1","project":"deepflow-task","sourceCommit":"a5b9064298158aad2fef2a67f0baa0a638c89f4b","sourceDirty":false}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const commands = { health: '存活探针:进程还在吗', ready: '就绪探针:能不能干活(未就绪时退出码 1)', locks: '当前哪些 Task 正在被处理、被谁', task: '这个 Task 为什么不动', metrics: '抓取指标,可按子串过滤', help: '打印这段用法' };
|
|
7
|
+
export function displayWidth(text) {
|
|
8
|
+
return Array.from(text).reduce((width, char) => { const r = char.codePointAt(0); return width + ((r >= 0x1100 && r <= 0x115f) || (r >= 0x2e80 && r <= 0xa4cf) || (r >= 0xac00 && r <= 0xd7a3) || (r >= 0xf900 && r <= 0xfaff) || (r >= 0xfe30 && r <= 0xfe4f) || (r >= 0xff00 && r <= 0xff60) || (r >= 0xffe0 && r <= 0xffe6) ? 2 : 1); }, 0);
|
|
9
|
+
}
|
|
10
|
+
export const pad = (text, width) => text + ' '.repeat(Math.max(1, width - displayWidth(text)));
|
|
11
|
+
function usage(out) {
|
|
12
|
+
out.write('df-taskdriver —— deepflow-task-driver 运维 CLI\n\n用法: df-taskdriver [选项] <子命令> [参数]\n\n子命令:\n');
|
|
13
|
+
for (const [name, description] of Object.entries(commands)) out.write(` ${pad(name === 'task' ? 'task <task-id> [limit]' : name === 'metrics' ? 'metrics [过滤子串]' : name, 26)} ${description}\n`);
|
|
14
|
+
out.write('\n选项:\n --base-url string driver 实例的 ops 地址\n --timeout duration 单次请求超时 (default 10s)\n --json 原样输出实例返回的 JSON\n\n退出码: 0 肯定 / 1 实例给出否定回答 / 2 连不上或用法错误\n');
|
|
15
|
+
}
|
|
16
|
+
function duration(raw) {
|
|
17
|
+
if (raw === '0') return 0;
|
|
18
|
+
let total = 0, consumed = '';
|
|
19
|
+
for (const match of raw.matchAll(/(\d+(?:\.\d+)?)(ns|us|µs|μs|ms|s|m|h)/g)) {
|
|
20
|
+
consumed += match[0]; total += Number(match[1]) * { ns: 1e-6, us: 1e-3, 'µs': 1e-3, 'μs': 1e-3, ms: 1, s: 1000, m: 60000, h: 3600000 }[match[2]];
|
|
21
|
+
}
|
|
22
|
+
if (!consumed || consumed !== raw || !Number.isFinite(total)) throw Error(`invalid duration ${raw}`);
|
|
23
|
+
return total;
|
|
24
|
+
}
|
|
25
|
+
export function parse(args, env) {
|
|
26
|
+
const options = { args: [], base: env.DF_TASKDRIVER_BASE_URL || 'http://127.0.0.1:8081', timeout: 10000, json: false };
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
if (!args[i].startsWith('-') || args[i] === '-') { options.args.push(args[i]); continue; }
|
|
29
|
+
if (args[i] === '--') { options.args.push(...args.slice(i + 1)); break; }
|
|
30
|
+
const [key, inline] = args[i].replace(/^-{1,2}/, '').split(/=(.*)/s);
|
|
31
|
+
if (key === 'help' || key === 'h') { options.help = true; break; }
|
|
32
|
+
if (key === 'json') { if (inline !== undefined && !['true', 'false'].includes(inline)) throw Error('invalid boolean value'); options.json = inline !== 'false'; continue; }
|
|
33
|
+
if (!['base-url', 'timeout'].includes(key)) throw Error(`flag provided but not defined: -${key}`);
|
|
34
|
+
const value = inline ?? args[++i]; if (value === undefined) throw Error(`flag needs an argument: -${key}`);
|
|
35
|
+
if (key === 'base-url') options.base = value; else options.timeout = duration(value);
|
|
36
|
+
}
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
39
|
+
const humanMS = (ms) => ms < 0 ? '未知' : `${(Number(ms || 0) / 1000).toFixed(1)}s`;
|
|
40
|
+
const compact = (payload) => Object.keys(payload || {}).sort().map((key) => `${key}=${typeof payload[key] === 'object' ? JSON.stringify(payload[key]) : payload[key]}`).join(' ') || '{}';
|
|
41
|
+
// Format tokens rather than re-encoding parsed numbers: seq values are int64.
|
|
42
|
+
function pretty(raw) {
|
|
43
|
+
const tokens = raw.match(/"(?:\\.|[^"\\])*"|[{}\[\],:]|[^\s{}\[\],:]+/g) || [];
|
|
44
|
+
let level = 0, out = '';
|
|
45
|
+
const newline = () => '\n' + ' '.repeat(level);
|
|
46
|
+
tokens.forEach((token, i) => {
|
|
47
|
+
if (token === '{' || token === '[') { out += token; level++; if (tokens[i + 1] !== (token === '{' ? '}' : ']')) out += newline(); }
|
|
48
|
+
else if (token === '}' || token === ']') { level--; if (tokens[i - 1] !== (token === '}' ? '{' : '[')) out += newline(); out += token; }
|
|
49
|
+
else out += token === ',' ? ',' + newline() : token === ':' ? ': ' : token;
|
|
50
|
+
});
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function human(command, body, status, out) {
|
|
54
|
+
const b = body || {};
|
|
55
|
+
if (command === 'health') out.write(`存活: ${b.status || ''} instance=${b.instance_id || ''}\n`);
|
|
56
|
+
if (command === 'ready') out.write(status === 200 ? '就绪\n' : `未就绪: ${b.error || ''}\n`);
|
|
57
|
+
if (command === 'locks') {
|
|
58
|
+
if (status !== 200) out.write(`读取失败: ${b.error || ''}\n`);
|
|
59
|
+
else if (!b.count) out.write('当前没有任何 Task 被持锁\n');
|
|
60
|
+
else {
|
|
61
|
+
out.write(`${b.count} 个 Task 正在被处理\n`);
|
|
62
|
+
for (const lock of b.locks || []) out.write(` ${lock.task_id} holder=${lock.holder} (${lock.recognized ? '本实例' : '其他实例'}) 剩余 ${humanMS(lock.remaining_ttl_ms)}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (command === 'task') {
|
|
66
|
+
if (status !== 200) { out.write(`读取失败: ${b.error || ''}\n`); return; }
|
|
67
|
+
const d = b.decision || {};
|
|
68
|
+
out.write(`${b.task_id || ''}\n 结论: ${d.should_schedule ? '下一轮会调度' : '不会调度'} —— ${d.reason || ''}\n 事实: ${b.facts || 0} 条,max_seq=${b.max_seq || 0}\n`);
|
|
69
|
+
out.write(` 水位线: ${d.max_watermark || 0},最大触发事实: ${d.max_trigger_seq || 0},本轮锚点: ${d.anchor || 0}\n`);
|
|
70
|
+
out.write(` 失败预算: 组 ${d.budget_group || 0} 内 ${d.failures_in_budget_group || 0}/${d.failure_budget || 0} 次失败${d.budget_exhausted ? '(已耗尽,等人介入;发一条新的用户消息即可恢复)' : ''}\n`);
|
|
71
|
+
if (b.inflight?.length) {
|
|
72
|
+
out.write(' 在飞唤起:\n');
|
|
73
|
+
for (const inv of b.inflight) out.write(` ${inv.run_id} kind=${inv.run_kind} started_seq=${inv.started_seq || 0} intent_seq=${inv.intent_seq || 0} 水位线=${inv.watermark || 0} 锚点=${inv.anchor || 0}${inv.needs_replay ? ' [待重放]' : ''}\n`);
|
|
74
|
+
}
|
|
75
|
+
if (b.unfulfilled_intents?.length) {
|
|
76
|
+
out.write(' 未兑现意图:\n');
|
|
77
|
+
for (const intent of b.unfulfilled_intents) out.write(` seq=${intent.seq} ${compact(intent.payload)}\n`);
|
|
78
|
+
}
|
|
79
|
+
out.write(b.inflight?.length || b.unfulfilled_intents?.length ? ' 驱动这边还有未了之事(见上方在飞唤起/未兑现意图),先看它们卡在哪一步\n' : d.should_schedule ? ' 驱动这边没有未了之事,谓词已成立,下一拍就会起一轮调度——这一刻不用做什么\n' : ' 驱动无未了之事,继续查上游(事实是否写入、调度智能体是否下发)\n');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function run(args, { env = process.env, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
83
|
+
if (args[0] === '--cli-version') {
|
|
84
|
+
let version = 'workspace';
|
|
85
|
+
try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
|
|
86
|
+
const report = { name: 'df-taskdriver', version, entry: fileURLToPath(import.meta.url) };
|
|
87
|
+
stdout.write((args.includes('--json') ? JSON.stringify(report) : `df-taskdriver ${version}`) + '\n');
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
let o, command, rest, path;
|
|
91
|
+
try {
|
|
92
|
+
o = parse(args, env); [command, ...rest] = o.args;
|
|
93
|
+
if (o.help || command === 'help') { usage(stdout); return 0; }
|
|
94
|
+
if (!command || !Object.hasOwn(commands, command)) { if (command) stderr.write(`未知子命令: ${command}\n\n`); usage(stderr); return 2; }
|
|
95
|
+
path = { health: '/healthz', ready: '/readyz', locks: '/debug/locks', metrics: '/metrics' }[command];
|
|
96
|
+
if (command === 'task') {
|
|
97
|
+
if (!rest.length) throw Error('用法: df-taskdriver task <task-id> [limit]');
|
|
98
|
+
if (rest[1] !== undefined && (!/^\+?\d+$/.test(rest[1]) || BigInt(rest[1]) < 1n || BigInt(rest[1]) > 9223372036854775807n)) throw Error(`limit 必须是正整数: ${rest[1]}`);
|
|
99
|
+
path = `/debug/tasks/${encodeURIComponent(rest[0])}${rest[1] !== undefined ? `?limit=${encodeURIComponent(rest[1])}` : ''}`;
|
|
100
|
+
}
|
|
101
|
+
} catch (error) { stderr.write(`错误: ${error.message}\n`); return 2; }
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const stop = () => controller.abort();
|
|
104
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
105
|
+
const timer = o.timeout ? setTimeout(stop, o.timeout) : undefined;
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(o.base.replace(/\/+$/, '') + path, { signal: controller.signal });
|
|
108
|
+
const chunks = []; let length = 0;
|
|
109
|
+
for await (const chunk of response.body) { length += chunk.length; if (length > 8 * 1024 * 1024) throw Error('response exceeds 8 MiB'); chunks.push(chunk); }
|
|
110
|
+
const data = Buffer.concat(chunks).toString('utf8'), affirmative = response.status >= 200 && response.status < 300;
|
|
111
|
+
let body, valid = true;
|
|
112
|
+
if (command !== 'metrics') { try { body = JSON.parse(data, (_key, value, context) => typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source) ? BigInt(context.source) : value); } catch { valid = false; } }
|
|
113
|
+
if (command === 'metrics') {
|
|
114
|
+
stdout.write(rest.length ? data.split('\n').filter((line) => line.includes(rest[0])).join('\n') + '\n' : data);
|
|
115
|
+
return affirmative ? 0 : 1;
|
|
116
|
+
}
|
|
117
|
+
if (o.json) {
|
|
118
|
+
// Raw server JSON preserves unknown fields and integer precision.
|
|
119
|
+
stdout.write(valid ? pretty(data) + '\n' : data + (data.endsWith('\n') ? '' : '\n'));
|
|
120
|
+
return affirmative ? valid ? 0 : 2 : 1;
|
|
121
|
+
}
|
|
122
|
+
if (!valid || (body !== null && (typeof body !== 'object' || Array.isArray(body)))) {
|
|
123
|
+
stderr.write(`错误: 解析 ${path} 的响应失败(HTTP ${response.status},响应体前 200 字节: ${Array.from(data.trim()).slice(0, 200).join('')})\n`);
|
|
124
|
+
return affirmative ? 2 : 1;
|
|
125
|
+
}
|
|
126
|
+
human(command, body, response.status, stdout);
|
|
127
|
+
return command === 'ready' ? response.status === 200 ? 0 : 1 : affirmative ? 0 : 1;
|
|
128
|
+
} catch (error) { stderr.write(`错误: 请求 ${o.base} 失败: ${error.message}(检查 --base-url / DF_TASKDRIVER_BASE_URL)\n`); return 2; }
|
|
129
|
+
finally { clearTimeout(timer); process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop); }
|
|
130
|
+
}
|
|
131
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) process.exitCode = await run(process.argv.slice(2));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.3.1","project":"deepflow-task-driver","sourceCommit":"c05efdc13dfd0d7a9043007f7421dbd1ac6daab2","sourceDirty":false}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { randomUUID, createHash } from "node:crypto";
|
|
4
4
|
import { readFile, stat } from "node:fs/promises";
|
|
5
5
|
import { basename } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { realpathSync } from "node:fs";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_BASE_URL = "http://127.0.0.1:8793";
|
|
9
10
|
const MAX_MESSAGE_LENGTH = 20000;
|
|
@@ -467,6 +468,7 @@ task create/send 可重复传入 --attachment-id <id>,每次最多 10 个。
|
|
|
467
468
|
DF_WORKBENCH_BASE_URL Workbench 根地址,默认 http://127.0.0.1:8793
|
|
468
469
|
DF_WORKBENCH_TOKEN OIDC 环境使用的 personal token
|
|
469
470
|
DF_WORKBENCH_WORKSPACE_ID 当前工作区覆盖值
|
|
471
|
+
Token 获取:公司账号登录 → 个人设置 → 个人 Token
|
|
470
472
|
--base-url <url> 覆盖 Workbench 根地址
|
|
471
473
|
--workspace <id> 覆盖当前工作区
|
|
472
474
|
--expected-actor <user-id> 固定操作身份;身份变化时停止请求
|
|
@@ -769,13 +771,20 @@ async function run(options) {
|
|
|
769
771
|
}
|
|
770
772
|
|
|
771
773
|
export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
774
|
+
if (argv[0] === '--cli-version') {
|
|
775
|
+
let version = 'workspace';
|
|
776
|
+
try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
|
|
777
|
+
const report = { name: 'df-workbench', version, entry: fileURLToPath(import.meta.url) };
|
|
778
|
+
process.stdout.write((argv.includes('--json') ? JSON.stringify(report) : `df-workbench ${version}`) + '\n');
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
772
781
|
const options = parseArgs(argv, env);
|
|
773
782
|
await run(options);
|
|
774
783
|
}
|
|
775
784
|
|
|
776
785
|
if (
|
|
777
786
|
process.argv[1] &&
|
|
778
|
-
import.meta.url ===
|
|
787
|
+
fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
|
|
779
788
|
) {
|
|
780
789
|
main().catch((error) => {
|
|
781
790
|
if (error instanceof UsageError) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.3.1","project":"deepflow-workbench","sourceCommit":"b83963f15f4ed187164464dddbfd2ea460f52971","sourceDirty":false}
|
|
@@ -1,26 +1,30 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { fileURLToPath } from
|
|
3
|
-
import { cp, mkdir, rm, readFile } from
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { chmod, cp, mkdir, rm, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { assembleBundle } from '../src/bundle.mjs';
|
|
7
|
+
import { loadManifest } from '../src/manifest.mjs';
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
await assembleBundle({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
await
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
);
|
|
22
|
-
await
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
9
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const workspaceRoot = path.resolve(packageRoot, '../..');
|
|
11
|
+
const packageJson = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
12
|
+
const manifest = await loadManifest(path.join(packageRoot, 'manifest.json'));
|
|
13
|
+
if (manifest.version !== packageJson.version) throw Error('Manifest and npm package versions differ');
|
|
14
|
+
await promisify(execFile)(process.execPath, ['cli/generate-protocol.mjs', '--check'], { cwd: path.join(workspaceRoot, 'deepflow-task') });
|
|
15
|
+
await assembleBundle({ workspaceRoot, outputDir: path.join(packageRoot, 'bundle'), manifestPath: path.join(packageRoot, 'manifest.json') });
|
|
16
|
+
const runtime = path.join(packageRoot, 'runtime');
|
|
17
|
+
await rm(runtime, { recursive: true, force: true });
|
|
18
|
+
await mkdir(runtime, { recursive: true });
|
|
19
|
+
for (const cli of manifest.clis) {
|
|
20
|
+
const target = path.join(runtime, cli.name);
|
|
21
|
+
await cp(path.join(workspaceRoot, cli.project, 'cli'), target, {
|
|
22
|
+
recursive: true,
|
|
23
|
+
filter: (source) => !/(?:[\\/]contract(?:[\\/]|$)|\.test\.mjs$|[\\/]generate-protocol\.mjs$)/.test(source),
|
|
24
|
+
});
|
|
25
|
+
const { stdout } = await promisify(execFile)('git', ['rev-parse', 'HEAD'], { cwd: path.join(workspaceRoot, cli.project) });
|
|
26
|
+
const { stdout: status } = await promisify(execFile)('git', ['status', '--porcelain'], { cwd: path.join(workspaceRoot, cli.project) });
|
|
27
|
+
await writeFile(path.join(target, 'release.json'), JSON.stringify({ version: packageJson.version, project: cli.project, sourceCommit: stdout.trim(), sourceDirty: !!status.trim() }) + '\n');
|
|
28
|
+
await chmod(path.join(target, path.basename(cli.entry)), 0o755);
|
|
29
|
+
if (cli.name === 'df-agentrt') await chmod(path.join(target, 'agentrt.mjs'), 0o755);
|
|
30
|
+
}
|
package/src/bundle.mjs
CHANGED
|
@@ -65,7 +65,10 @@ async function assembleInto({ workspaceRoot, outputDir, manifestPath }) {
|
|
|
65
65
|
throw new Error(`missing or empty Skill source: ${skillFile}`);
|
|
66
66
|
}
|
|
67
67
|
const destination = path.join(outputDir, "skills", skill.name);
|
|
68
|
-
await cp(skill.sourcePath, destination, {
|
|
68
|
+
await cp(skill.sourcePath, destination, {
|
|
69
|
+
recursive: true, dereference: false,
|
|
70
|
+
filter: (source) => !/(?:_test\.go$|\.test\.mjs$|[\\/]__pycache__(?:[\\/]|$))/.test(source),
|
|
71
|
+
});
|
|
69
72
|
skills.push({
|
|
70
73
|
name: skill.name,
|
|
71
74
|
source: skill.source,
|