@metagptx/deepflow-suite-cli 0.2.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +44 -50
  2. package/bin/deepflow-suite.js +4 -4
  3. package/bundle/deepflow-release.json +21 -91
  4. package/bundle/skills/deepflow-suite/SKILL.md +7 -1
  5. package/bundle/skills/df-agentctrl/SKILL.md +92 -10
  6. package/bundle/skills/df-agentrt/SKILL.md +65 -14
  7. package/bundle/skills/df-agentrt-repo/SKILL.md +4 -0
  8. package/bundle/skills/df-task/SKILL.md +98 -26
  9. package/bundle/skills/df-taskdriver/SKILL.md +8 -4
  10. package/bundle/skills/df-workbench/SKILL.md +5 -9
  11. package/manifest.json +12 -30
  12. package/package.json +8 -17
  13. package/runtime/df-agentctrl/commands.mjs +93 -0
  14. package/runtime/df-agentctrl/df-agentctrl.mjs +119 -0
  15. package/runtime/df-agentctrl/release.json +1 -0
  16. package/runtime/df-agentrt/agentrt.mjs +3 -0
  17. package/runtime/df-agentrt/df-agentrt.mjs +3 -0
  18. package/runtime/df-agentrt/release.json +1 -0
  19. package/runtime/df-agentrt/runtime.mjs +326 -0
  20. package/runtime/df-agentrt/usage.mjs +16 -0
  21. package/runtime/df-task/commands.mjs +181 -0
  22. package/runtime/df-task/df-task.mjs +155 -0
  23. package/runtime/df-task/protocol.mjs +3 -0
  24. package/runtime/df-task/release.json +1 -0
  25. package/runtime/df-taskdriver/df-taskdriver.mjs +131 -0
  26. package/runtime/df-taskdriver/release.json +1 -0
  27. package/runtime/{df-workbench.mjs → df-workbench/df-workbench.mjs} +17 -175
  28. package/runtime/df-workbench/release.json +1 -0
  29. package/scripts/build-package.mjs +29 -25
  30. package/src/bundle.mjs +4 -1
  31. package/src/cli-install.mjs +35 -125
  32. package/src/commands/doctor.mjs +2 -0
  33. package/src/linker.mjs +2 -2
  34. package/src/manifest.mjs +7 -13
  35. package/src/package-cli.mjs +44 -60
  36. package/bin/df-agentctrl.js +0 -10
  37. package/bin/df-agentrt.js +0 -10
  38. package/bin/df-task.js +0 -10
  39. package/bin/df-taskdriver.js +0 -10
  40. package/bin/df-workbench.js +0 -10
  41. package/bundle/skills/deepflow-suite/manifest.yaml +0 -20
  42. package/bundle/skills/df-agentctrl/references/environment.md +0 -40
  43. package/bundle/skills/df-agentctrl/references/query-scope.md +0 -50
  44. package/bundle/skills/df-agentrt/references/environment.md +0 -29
  45. package/bundle/skills/df-agentrt/references/invocation-result.md +0 -11
  46. package/bundle/skills/df-agentrt/references/query-scope.md +0 -30
  47. package/bundle/skills/df-agentrt/references/session-create.md +0 -14
  48. package/bundle/skills/df-task/references/environment.md +0 -50
  49. package/bundle/skills/df-task/references/query-scope.md +0 -49
  50. package/bundle/skills/df-task/references/reading.md +0 -9
  51. package/bundle/skills/df-task/skill_test.go +0 -201
  52. package/bundle/skills/df-workbench/manifest.yaml +0 -5
  53. package/scripts/build-runtime-packages.mjs +0 -38
  54. package/skills/deepflow-suite/SKILL.md +0 -81
  55. package/skills/deepflow-suite/manifest.yaml +0 -20
  56. package/src/cli-launcher.mjs +0 -71
  57. package/src/runtime-build.mjs +0 -193
  58. package/src/runtime-package.mjs +0 -31
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { randomUUID, createHash } from "node:crypto";
4
- import { readFile, stat } from "node:fs/promises";
5
- import { basename } from "node:path";
6
- import { pathToFileURL } from "node:url";
3
+ import { randomUUID } from "node:crypto";
4
+ import { readFile } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import { realpathSync } from "node:fs";
7
7
 
8
8
  const DEFAULT_BASE_URL = "http://127.0.0.1:8793";
9
9
  const MAX_MESSAGE_LENGTH = 20000;
@@ -39,7 +39,6 @@ export function parseArgs(argv, env = process.env) {
39
39
  baseUrl: env.DF_WORKBENCH_BASE_URL || DEFAULT_BASE_URL,
40
40
  token: env.DF_WORKBENCH_TOKEN || "",
41
41
  workspace: env.DF_WORKBENCH_WORKSPACE_ID || "",
42
- expectedActor: undefined,
43
42
  json: false,
44
43
  idempotencyKey: "",
45
44
  message: "",
@@ -49,8 +48,6 @@ export function parseArgs(argv, env = process.env) {
49
48
  scope: "mine",
50
49
  squadId: undefined,
51
50
  file: undefined,
52
- attachmentIds: [],
53
- participantIds: [],
54
51
  };
55
52
  const positionals = [];
56
53
  for (let index = 0; index < argv.length; index += 1) {
@@ -70,7 +67,6 @@ export function parseArgs(argv, env = process.env) {
70
67
  [
71
68
  "--base-url",
72
69
  "--workspace",
73
- "--expected-actor",
74
70
  "--idempotency-key",
75
71
  "--message",
76
72
  "--title",
@@ -79,11 +75,6 @@ export function parseArgs(argv, env = process.env) {
79
75
  "--scope",
80
76
  "--squad-id",
81
77
  "--file",
82
- "--attachment-id",
83
- "--member-id",
84
- "--expected-owner",
85
- "--owner-id",
86
- "--participant-id",
87
78
  ].includes(name)
88
79
  ) {
89
80
  const parsed =
@@ -91,11 +82,6 @@ export function parseArgs(argv, env = process.env) {
91
82
  if (inline === undefined) index += 1;
92
83
  if (name === "--base-url") options.baseUrl = parsed;
93
84
  if (name === "--workspace") options.workspace = parsed;
94
- if (name === "--expected-actor") {
95
- if (!parsed.trim() || parsed.length > 160 || /[\r\n]/.test(parsed))
96
- throw new UsageError("--expected-actor 需要有效的用户 ID。");
97
- options.expectedActor = parsed;
98
- }
99
85
  if (name === "--idempotency-key") options.idempotencyKey = parsed;
100
86
  if (name === "--message") options.message = parsed;
101
87
  if (name === "--title") options.title = parsed;
@@ -104,11 +90,6 @@ export function parseArgs(argv, env = process.env) {
104
90
  if (name === "--scope") options.scope = parsed;
105
91
  if (name === "--squad-id") options.squadId = parsed;
106
92
  if (name === "--file") options.file = parsed;
107
- if (name === "--attachment-id") options.attachmentIds.push(parsed);
108
- if (name === "--member-id") options.memberId = parsed;
109
- if (name === "--expected-owner") options.expectedOwner = parsed;
110
- if (name === "--owner-id") options.ownerId = parsed;
111
- if (name === "--participant-id") options.participantIds.push(parsed);
112
93
  continue;
113
94
  }
114
95
  if (argument.startsWith("--"))
@@ -264,13 +245,6 @@ async function currentContext(options) {
264
245
  throw new RequestError("Workbench 没有返回当前用户。", {
265
246
  code: "identity_missing",
266
247
  });
267
- if (
268
- options.expectedActor !== undefined &&
269
- options.expectedActor !== me.user.id
270
- )
271
- throw new RequestError("Workbench 操作身份已变化;未提交业务请求。", {
272
- code: "identity_changed",
273
- });
274
248
  return { me, workspace, actorId: me.user.id };
275
249
  }
276
250
 
@@ -449,19 +423,11 @@ function help() {
449
423
  df-workbench squad get <squad-id> [--json]
450
424
  df-workbench squad create --file <配置.json> [--idempotency-key <key>] [--json]
451
425
  df-workbench squad update <squad-id> --file <配置.json> [--idempotency-key <key>] [--json]
452
- df-workbench workspace members [--json]
453
- df-workbench task create --message <消息> [--title <标题>] [--squad-id <squad-id>] [--owner-id <user-id>] [--participant-id <user-id>] [--idempotency-key <key>]
454
- df-workbench task member-options <task-id>
455
- df-workbench task invite <task-id> --member-id <user-id> [--idempotency-key <key>]
456
- df-workbench task transfer-owner <task-id> --member-id <user-id> --expected-owner <user-id> [--idempotency-key <key>]
426
+ df-workbench task create --message <消息> [--title <标题>] [--squad-id <squad-id>] [--idempotency-key <key>]
457
427
  df-workbench task send <task-id> --message <消息> [--idempotency-key <key>]
458
- df-workbench task lookup <DF-number> [--json]
459
428
  df-workbench task get <task-id> [--json]
460
429
  df-workbench task facts <task-id> [--after <seq>] [--limit <n>] [--json]
461
430
  df-workbench task list [--scope <mine|workspace>] [--limit <n>] [--json]
462
- df-workbench attachment upload --file <文件> [--idempotency-key <key>] [--json]
463
-
464
- task create/send 可重复传入 --attachment-id <id>,每次最多 10 个。
465
431
 
466
432
  全局配置:
467
433
  DF_WORKBENCH_BASE_URL Workbench 根地址,默认 http://127.0.0.1:8793
@@ -469,7 +435,6 @@ task create/send 可重复传入 --attachment-id <id>,每次最多 10 个。
469
435
  DF_WORKBENCH_WORKSPACE_ID 当前工作区覆盖值
470
436
  --base-url <url> 覆盖 Workbench 根地址
471
437
  --workspace <id> 覆盖当前工作区
472
- --expected-actor <user-id> 固定操作身份;身份变化时停止请求
473
438
  --json 输出机器可读 JSON`;
474
439
  }
475
440
 
@@ -479,11 +444,6 @@ async function run(options) {
479
444
  process.stdout.write(`${help()}\n`);
480
445
  return;
481
446
  }
482
- if ((options.ownerId !== undefined || options.participantIds.length) && !(command === "task" && subject === "create")) throw new UsageError("--owner-id 和 --participant-id 仅用于 task create。");
483
- if (options.ownerId !== undefined && !/^[\w-]{1,100}$/.test(options.ownerId)) throw new UsageError("初始负责人 ID 无效。");
484
- if ((options.memberId !== undefined && !(command === "task" && ["invite","transfer-owner"].includes(subject))) ||
485
- (options.expectedOwner !== undefined && !(command === "task" && subject === "transfer-owner")))
486
- throw new UsageError("成员和负责人参数只用于对应的任务成员命令。");
487
447
  if (
488
448
  options.squadId !== undefined &&
489
449
  !(command === "task" && subject === "create")
@@ -493,57 +453,9 @@ async function run(options) {
493
453
  );
494
454
  if (
495
455
  options.file !== undefined &&
496
- !(command === "squad" && ["create", "update"].includes(subject)) &&
497
- !(command === "attachment" && subject === "upload")
498
- )
499
- throw new UsageError(
500
- "--file 仅用于 squad create/update 或 attachment upload。",
501
- );
502
- if (
503
- options.attachmentIds.length &&
504
- !(command === "task" && ["create", "send"].includes(subject))
505
- )
506
- throw new UsageError("--attachment-id 仅用于 task create/send。");
507
- if (
508
- options.attachmentIds.length > 10 ||
509
- new Set(options.attachmentIds).size !== options.attachmentIds.length ||
510
- options.attachmentIds.some((id) => !/^[\w-]{1,100}$/.test(id))
456
+ !(command === "squad" && ["create", "update"].includes(subject))
511
457
  )
512
- throw new UsageError("附件列表无效,每次最多发送 10 个不同附件。");
513
- if (command === "attachment" && subject === "upload") {
514
- if (!options.file) throw new UsageError("attachment upload 需要 --file。");
515
- const fileInfo = await stat(options.file);
516
- if (!fileInfo.isFile() || !fileInfo.size || fileInfo.size > 4 * 1024 * 1024)
517
- throw new UsageError("每个附件需为 1 字节至 4 MiB。");
518
- const bytes = await readFile(options.file);
519
- const filename = basename(options.file);
520
- if (!bytes.length || bytes.length > 4 * 1024 * 1024)
521
- throw new UsageError("每个附件需为 1 字节至 4 MiB。");
522
- if (
523
- !filename.trim() ||
524
- filename.length > 180 ||
525
- /[\x00-\x1f\x7f\\]/.test(filename)
526
- )
527
- throw new UsageError("附件名称无效。");
528
- const context = await currentContext(options);
529
- const value = await request(
530
- options,
531
- "/api/v1/task-attachments",
532
- {
533
- method: "POST",
534
- key: idempotencyKey(options),
535
- body: { filename, bytesBase64: bytes.toString("base64") },
536
- },
537
- context,
538
- );
539
- writeResult(
540
- value,
541
- options,
542
- (value) =>
543
- `已上传附件:${value.attachment?.filename} (${value.attachment?.id})`,
544
- );
545
- return;
546
- }
458
+ throw new UsageError("--file 仅用于 squad create/update。");
547
459
  if (command === "squad") return squadCommand(options, subject, rest);
548
460
  if (command === "health") {
549
461
  const value = await request(options, "/healthz");
@@ -570,44 +482,16 @@ async function run(options) {
570
482
  );
571
483
  return;
572
484
  }
573
- if (command === "workspace" && subject === "members") {
574
- const context = await currentContext(options);
575
- const value = await request(options,"/api/v1/members",{},context);
576
- if (!Array.isArray(value.members)) throw new RequestError("工作区成员目录回执无效。",{code:"invalid_member_directory"});
577
- writeResult(value,options,value => value.members.map(member=>`${member.userId} ${member.name || ""}`).join("\n"));return;
578
- }
579
485
  if (command !== "task") throw new UsageError(`不支持的命令:${command}`);
580
- if (["member-options", "invite", "transfer-owner"].includes(subject)) {
581
- const taskId = rest[0];
582
- if (!taskId || !/^[\w-]{1,160}$/.test(taskId)) throw new UsageError("需要有效的 Task ID。");
583
- const writes = subject !== "member-options";
584
- if (writes && !/^[\w-]{1,100}$/.test(options.memberId || "")) throw new UsageError("需要 --member-id 指定有效的 DF 用户 ID。");
585
- if (subject === "transfer-owner" && !/^[\w-]{1,100}$/.test(options.expectedOwner || "")) throw new UsageError("需要 --expected-owner 指定当前负责人 ID。");
586
- const context = await currentContext(options);
587
- const path = subject === "member-options" ? `/api/v1/task-ui/tasks/${encodeURIComponent(taskId)}/property-options`
588
- : `/api/v1/tasks/${encodeURIComponent(taskId)}/${subject === "invite" ? "member-invitations" : "owner-transfer"}`;
589
- const value = await request(options, path, writes ? {method:"POST", key:idempotencyKey(options), body:subject === "invite"
590
- ? {invitee_ids:[options.memberId]} : {new_owner_id:options.memberId,expected_owner_id:options.expectedOwner}} : {}, context);
591
- writeResult(value, options, value => JSON.stringify(value));
592
- return;
593
- }
594
486
  if (subject === "create") {
595
487
  const body = buildCreateBody(
596
488
  options.message,
597
489
  options.title,
598
490
  options.squadId,
599
491
  );
600
- if (options.ownerId) {
601
- if (![options.ownerId,...options.participantIds].every(id=>/^[\w-]{1,100}$/.test(id))) throw new UsageError("初始负责人和参与人 ID 无效。");
602
- body.initial_owner_id=options.ownerId;
603
- body.first_message="";
604
- body.invitee_ids=[...new Set(options.participantIds)].sort();
605
- } else if(options.participantIds.length) throw new UsageError("--participant-id 需要同时指定 --owner-id。");
606
- if (options.attachmentIds.length && !options.ownerId)
607
- body.attachment_ids = options.attachmentIds;
608
492
  const context = await currentContext(options);
609
493
  const key = idempotencyKey(options);
610
- let value = await request(
494
+ const value = await request(
611
495
  options,
612
496
  "/api/v1/tasks",
613
497
  {
@@ -630,23 +514,6 @@ async function run(options) {
630
514
  idempotencyKey: key,
631
515
  },
632
516
  );
633
- if (options.ownerId) {
634
- const task=value?.task,data=task?.metadata?.data;
635
- if (!task?.id || data?.workspace_id!==context.workspace || data.owner_id!==options.ownerId || data.created_by!==context.actorId ||
636
- ![context.actorId,options.ownerId,...options.participantIds].every(id=>data.members?.includes(id))) throw new RequestError("已创建的任务初始负责人或成员回执不匹配,请保留原幂等键核对,不要重建。",{code:"task_metadata_mismatch",outcome:"unknown",taskId:task?.id,idempotencyKey:key});
637
- // Current squad edits must never change a retry of the original message.
638
- const agentId=data.initial_message_context?.agentId;
639
- if (!agentId) throw new RequestError("任务已创建,但创建时的小队快照没有可接收首条消息的伙伴;请保留原任务与幂等键核对。",{code:"initial_agent_unavailable",outcome:"unknown",taskId:task.id,idempotencyKey:key});
640
- const firstKey="initial-message:"+createHash("sha256").update(key).digest("hex");
641
- let first;
642
- try {
643
- first=await request(options,`/api/v1/tasks/${encodeURIComponent(task.id)}/messages`,{method:"POST",key:firstKey,body:{text:messageValue(options.message),agentIds:[agentId],...(options.attachmentIds.length?{attachment_ids:options.attachmentIds}:{})}},context);
644
- } catch(error) {
645
- throw new RequestError(`任务 ${task.id} 已创建,首条消息回执尚待确认;保留幂等键 ${key} 重试,不换键或重建。`,{code:"initial_message_unconfirmed",outcome:"unknown",taskId:task.id,idempotencyKey:key,causeCode:error.code});
646
- }
647
- if (!Number.isSafeInteger(first?.fact?.seq) || first.fact.task_id!==task.id) throw new RequestError("初始消息回执不匹配;保留原任务与幂等键重试。",{code:"initial_message_unconfirmed",outcome:"unknown",taskId:task.id,idempotencyKey:key});
648
- value={...value,first_fact:first.fact,first_fact_seq:first.fact.seq};
649
- }
650
517
  writeResult(value, options, taskSummary);
651
518
  return;
652
519
  }
@@ -658,16 +525,7 @@ async function run(options) {
658
525
  const value = await request(
659
526
  options,
660
527
  `/api/v1/tasks/${encodeURIComponent(taskId)}/messages`,
661
- {
662
- method: "POST",
663
- key,
664
- body: {
665
- text: messageValue(options.message),
666
- ...(options.attachmentIds.length
667
- ? { attachment_ids: options.attachmentIds }
668
- : {}),
669
- },
670
- },
528
+ { method: "POST", key, body: { text: messageValue(options.message) } },
671
529
  context,
672
530
  );
673
531
  writeResult(
@@ -677,29 +535,6 @@ async function run(options) {
677
535
  );
678
536
  return;
679
537
  }
680
- if (subject === "lookup") {
681
- const number = String(rest[0] || "").toUpperCase();
682
- if (!/^DF-\d+$/.test(number)) throw new UsageError("task lookup 需要 DF 编号。");
683
- const context = await currentContext(options);
684
- for (let page = 0, offset = 0; page < 20; page++) {
685
- const result = await request(options, `/api/v1/tasks?${new URLSearchParams({scope:"workspace",limit:"200",offset:String(offset)})}`, {}, context);
686
- if (!Array.isArray(result.tasks) || typeof result.hasMore !== "boolean") throw new RequestError("工作区任务分页回执不完整,尚不能判断该编号是否存在。",{code:"task_lookup_incomplete"});
687
- const matches = result.tasks.filter(task => task.number === number);
688
- if (matches.length > 1) throw new RequestError("工作区中出现重复任务编号,请核实目标。",{code:"task_lookup_ambiguous"});
689
- if (matches.length) {
690
- const task = matches[0];
691
- if (task.access === "overview") throw new RequestError(`${number} 存在,但当前账号尚未加入任务,请先获得参与权限。`,{code:"task_participation_required",status:403});
692
- const value = await request(options, `/api/v1/tasks/${encodeURIComponent(task.id)}`, {}, context);
693
- if (value.task?.id !== task.id || value.task?.number !== number || value.task?.metadata?.data?.workspace_id !== context.workspace || value.task?.access === "overview")
694
- throw new RequestError("DF 编号、工作区或内容权限读回不一致,暂停操作。",{code:"task_lookup_mismatch"});
695
- writeResult(value, options, taskSummary);return;
696
- }
697
- if (!result.hasMore) throw new RequestError(`当前工作区未找到 ${number},请确认编号。`,{code:"task_not_found",status:404});
698
- if (!result.tasks.length) break;
699
- offset += result.tasks.length;
700
- }
701
- throw new RequestError("工作区任务查询尚未完成,请稍后再试;不会新建任务。",{code:"task_lookup_incomplete"});
702
- }
703
538
  if (subject === "get") {
704
539
  const taskId = rest[0];
705
540
  if (!taskId) throw new UsageError("task get 需要 Task ID。");
@@ -769,13 +604,20 @@ async function run(options) {
769
604
  }
770
605
 
771
606
  export async function main(argv = process.argv.slice(2), env = process.env) {
607
+ if (argv[0] === '--cli-version') {
608
+ let version = 'workspace';
609
+ try { version = JSON.parse(await readFile(new URL('./release.json', import.meta.url), 'utf8')).version; } catch {}
610
+ const report = { name: 'df-workbench', version, entry: fileURLToPath(import.meta.url) };
611
+ process.stdout.write((argv.includes('--json') ? JSON.stringify(report) : `df-workbench ${version}`) + '\n');
612
+ return;
613
+ }
772
614
  const options = parseArgs(argv, env);
773
615
  await run(options);
774
616
  }
775
617
 
776
618
  if (
777
619
  process.argv[1] &&
778
- import.meta.url === pathToFileURL(process.argv[1]).href
620
+ fileURLToPath(import.meta.url) === realpathSync(process.argv[1])
779
621
  ) {
780
622
  main().catch((error) => {
781
623
  if (error instanceof UsageError) {
@@ -0,0 +1 @@
1
+ {"version":"0.3.0","project":"deepflow-workbench","sourceCommit":"6afd4dec2000e93a971c94d4f18126c10f74a300","sourceDirty":false}
@@ -1,26 +1,30 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { cp, mkdir, rm, readFile } from "node:fs/promises";
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
- import { assembleBundle } from "../src/bundle.mjs";
6
- import { buildRuntimePackages } from "../src/runtime-build.mjs";
7
-
8
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
- const workspaceRoot = path.resolve(packageRoot, "../..");
10
- const packageJson = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
11
- await assembleBundle({
12
- workspaceRoot,
13
- outputDir: path.join(packageRoot, "bundle"),
14
- manifestPath: path.join(packageRoot, "manifest.json"),
15
- });
16
- await rm(path.join(packageRoot, "runtime"), { recursive: true, force: true });
17
- await mkdir(path.join(packageRoot, "runtime"), { recursive: true });
18
- await cp(
19
- path.join(workspaceRoot, "deepflow-workbench", "cli", "df-workbench.mjs"),
20
- path.join(packageRoot, "runtime", "df-workbench.mjs"),
21
- );
22
- await buildRuntimePackages({
23
- workspaceRoot,
24
- outputRoot: path.join(packageRoot, "platform-packages"),
25
- version: packageJson.version,
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, { recursive: true, dereference: false });
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,
@@ -1,140 +1,50 @@
1
- import { spawn } from "node:child_process";
2
- import { access, lstat, mkdir, mkdtemp, readlink, rename, rm, symlink, unlink } from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
1
+ import { access, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
5
4
 
6
- function run(command, args, options) {
7
- return new Promise((resolve, reject) => {
8
- const child = spawn(command, args, { cwd: options.cwd, stdio: "inherit", env: options.env });
9
- child.once("error", reject);
10
- child.once("exit", (code, signal) => {
11
- if (code === 0) resolve();
12
- else reject(new Error(`${command} ${args.join(" ")} exited with ${code ?? signal}`));
13
- });
14
- });
15
- }
16
-
17
- async function replaceLink(source, target) {
18
- await mkdir(path.dirname(target), { recursive: true });
5
+ const destinationFor = (binDir) => binDir ? path.resolve(binDir) : path.join(os.homedir(), '.local', 'bin');
6
+ async function inspect(target) { try { return await lstat(target); } catch (error) { if (error.code === 'ENOENT') return null; throw error; } }
7
+ async function matches(target, source) {
19
8
  try {
20
- const stats = await lstat(target);
21
- if (!stats.isSymbolicLink()) {
22
- throw new Error(`refusing to replace non-symlink CLI target: ${target}`);
23
- }
24
- const current = path.resolve(path.dirname(target), await readlink(target));
25
- if (current === source) return "unchanged";
26
- await unlink(target);
27
- } catch (error) {
28
- if (error.code !== "ENOENT") throw error;
29
- }
30
- await symlink(path.relative(path.dirname(target), source), target);
31
- return "created";
9
+ if (process.platform === 'win32') return (await readFile(target, 'utf8')).includes(source);
10
+ return await realpath(target) === await realpath(source);
11
+ } catch { return false; }
32
12
  }
33
-
34
- async function sourceExists(source) {
35
- try {
13
+ export async function installWorkspaceClis({ workspaceRoot, manifest, binDir, force = false }) {
14
+ const destination = destinationFor(binDir), unchanged = [], targets = [];
15
+ for (const cli of manifest.clis) {
16
+ const source = path.resolve(workspaceRoot, cli.project, cli.entry);
36
17
  await access(source);
37
- return true;
38
- } catch {
39
- return false;
18
+ const target = path.join(destination, cli.name + (process.platform === 'win32' ? '.cmd' : ''));
19
+ if (await matches(target, source)) { unchanged.push(cli.name); continue; }
20
+ const stats = await inspect(target);
21
+ if (stats?.isDirectory() || (stats && !force)) throw Error(`refusing to replace existing CLI without --force: ${target}`);
22
+ targets.push({ name: cli.name, source, target });
40
23
  }
41
- }
42
-
43
- export async function installWorkspaceClis({ workspaceRoot, manifest, binDir, force = false }) {
44
- const destination = binDir
45
- ? path.resolve(binDir)
46
- : path.join(process.env.HOME ?? os.homedir(), ".local", "bin");
47
- const result = { linked: [], built: [] };
48
-
49
24
  await mkdir(destination, { recursive: true });
50
- const targets = manifest.clis.map((cli) => ({ cli, target: path.join(destination, cli.name) }));
51
- for (const { cli, target } of targets) {
52
- try {
53
- const stats = await lstat(target);
54
- if (!force) {
55
- throw new Error(`refusing to replace existing CLI without --force: ${target}`);
56
- }
57
- if (stats.isDirectory()) throw new Error(`refusing to replace CLI directory: ${target}`);
58
- } catch (error) {
59
- if (error.code !== "ENOENT") throw error;
60
- }
61
- }
62
-
63
- const backup = await mkdtemp(path.join(destination, ".deepflow-cli-backup-"));
64
- const backedUp = [];
65
- const touched = [];
25
+ const backup = await mkdtemp(path.join(destination, '.deepflow-cli-backup-'));
26
+ const backedUp = [], installed = [];
66
27
  try {
67
- for (const { cli, target } of targets) {
68
- try {
69
- await lstat(target);
70
- await rename(target, path.join(backup, cli.name));
71
- backedUp.push(cli.name);
72
- } catch (error) {
73
- if (error.code !== "ENOENT") throw error;
74
- }
75
- }
76
-
77
- for (const cli of manifest.clis) {
78
- const projectRoot = path.join(workspaceRoot, cli.project);
79
- const target = path.join(destination, cli.name);
80
-
81
- if (cli.name === "df-workbench") {
82
- const source = path.join(projectRoot, "cli", "df-workbench.mjs");
83
- if (!(await sourceExists(source))) throw new Error(`missing CLI source: ${source}`);
84
- await replaceLink(source, target);
85
- touched.push(cli.name);
86
- result.linked.push(cli.name);
87
- continue;
88
- }
89
-
90
- if (cli.name === "df-agentrt") {
91
- await run("make", ["build"], { cwd: projectRoot, env: process.env });
92
- const source = path.join(projectRoot, "deploy", "bin", "df-agentrt");
93
- if (!(await sourceExists(source))) throw new Error(`missing built CLI: ${source}`);
94
- await replaceLink(source, target);
95
- touched.push(cli.name);
96
- result.built.push(cli.name);
97
- continue;
28
+ for (const entry of targets) {
29
+ if (await inspect(entry.target)) { await rename(entry.target, path.join(backup, entry.name)); backedUp.push(entry); }
30
+ if (process.platform === 'win32') await writeFile(entry.target, `@ECHO off\r\n"${process.execPath}" "${entry.source}" %*\r\n`);
31
+ else await symlink(path.relative(destination, entry.source), entry.target);
32
+ installed.push(entry);
98
33
  }
99
-
100
- await run("make", ["install-cli", `BIN_DIR=${destination}`], {
101
- cwd: projectRoot,
102
- env: process.env,
103
- });
104
- touched.push(cli.name);
105
- result.built.push(cli.name);
106
- }
107
-
108
- await rm(backup, { recursive: true, force: true });
109
- return { binDir: destination, ...result };
34
+ return { binDir: destination, linked: installed.map((entry) => entry.name), unchanged };
110
35
  } catch (error) {
111
- for (const { cli } of targets) await rm(path.join(destination, cli.name), { recursive: true, force: true });
112
- for (const name of backedUp) await rename(path.join(backup, name), path.join(destination, name));
36
+ for (const entry of installed) await rm(entry.target, { force: true });
37
+ for (const entry of backedUp) await rename(path.join(backup, entry.name), entry.target);
113
38
  throw error;
114
- } finally {
115
- await rm(backup, { recursive: true, force: true });
116
- }
39
+ } finally { await rm(backup, { recursive: true, force: true }); }
117
40
  }
118
-
119
41
  export async function verifyWorkspaceClis({ workspaceRoot, manifest, binDir }) {
120
- const destination = binDir
121
- ? path.resolve(binDir)
122
- : path.join(process.env.HOME ?? os.homedir(), ".local", "bin");
123
- const missing = [];
124
- const invalid = [];
42
+ const bin = destinationFor(binDir), missing = [], invalid = [];
125
43
  for (const cli of manifest.clis) {
126
- try {
127
- const target = path.join(destination, cli.name);
128
- const stats = await lstat(target);
129
- await access(target);
130
- if (stats.isDirectory()) invalid.push({ name: cli.name, reason: "target is a directory" });
131
- } catch (error) {
132
- if (error.code === "ENOENT") {
133
- missing.push(cli.name);
134
- } else {
135
- invalid.push({ name: cli.name, reason: error.message });
136
- }
137
- }
44
+ const target = path.join(bin, cli.name + (process.platform === 'win32' ? '.cmd' : ''));
45
+ const source = path.resolve(workspaceRoot, cli.project, cli.entry);
46
+ if (!await inspect(target)) missing.push(cli.name);
47
+ else if (!await matches(target, source)) invalid.push({ name: cli.name, reason: 'CLI points to another installation', expected: source, actual: target });
138
48
  }
139
- return { binDir: destination, missing, invalid };
49
+ return { binDir: bin, missing, invalid };
140
50
  }
@@ -25,6 +25,8 @@ export async function doctor(options) {
25
25
  const clis = await verifyPackageClis({
26
26
  packageRoot: path.dirname(options.manifestPath),
27
27
  expectedVersion: manifest.version,
28
+ checkPath: true,
29
+ env: options.env ?? process.env,
28
30
  });
29
31
  return {
30
32
  ok: clis.ok,
package/src/linker.mjs CHANGED
@@ -24,7 +24,7 @@ async function inspectTarget(targetPath) {
24
24
 
25
25
  export async function installWorkspaceSkills({ workspaceRoot, targetDir, force = false, manifestPath }) {
26
26
  const manifest = await loadManifest(manifestPath);
27
- const skills = resolveSkillSources(manifest, workspaceRoot, "workspace");
27
+ const skills = resolveSkillSources(manifest, workspaceRoot);
28
28
  const destination = targetDir ?? path.join(workspaceRoot, ".agents", "skills");
29
29
 
30
30
  for (const skill of skills) {
@@ -68,7 +68,7 @@ export async function installWorkspaceSkills({ workspaceRoot, targetDir, force =
68
68
 
69
69
  export async function verifyWorkspaceSkills({ workspaceRoot, targetDir, manifestPath }) {
70
70
  const manifest = await loadManifest(manifestPath);
71
- const skills = resolveSkillSources(manifest, workspaceRoot, "workspace");
71
+ const skills = resolveSkillSources(manifest, workspaceRoot);
72
72
  const destination = targetDir ?? path.join(workspaceRoot, ".agents", "skills");
73
73
  const missing = [];
74
74
  const invalid = [];
package/src/manifest.mjs CHANGED
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- import { CLI_NAMES, DISTRIBUTION, PACKAGE_NAME, SKILL_NAMES } from "./constants.mjs";
5
+ import { CLI_NAMES, DISTRIBUTION, SKILL_NAMES } from "./constants.mjs";
6
6
 
7
7
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
8
 
@@ -10,6 +10,7 @@ export const defaultManifestPath = path.join(packageRoot, "manifest.json");
10
10
 
11
11
  export async function loadManifest(manifestPath = defaultManifestPath) {
12
12
  const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
13
+ if (!manifest.version) manifest.version = JSON.parse(await readFile(path.join(path.dirname(manifestPath), 'package.json'), 'utf8')).version;
13
14
  validateManifest(manifest);
14
15
  return manifest;
15
16
  }
@@ -50,23 +51,16 @@ export function validateManifest(manifest) {
50
51
  }
51
52
  }
52
53
  for (const cli of manifest.clis) {
53
- if (!cli || typeof cli.name !== "string" || typeof cli.project !== "string" || typeof cli.install !== "string") {
54
- throw new Error("DeepFlow manifest CLIs must define name, project, and install");
54
+ if (!cli || typeof cli.name !== "string" || typeof cli.project !== "string" || typeof cli.entry !== "string") {
55
+ throw new Error("DeepFlow manifest CLIs must define name, project, and entry");
55
56
  }
57
+ if (path.isAbsolute(cli.entry) || cli.entry.split(/[\\/]/).includes('..') || !cli.entry.endsWith('.mjs')) throw new Error(`invalid Node CLI entry: ${cli.name}`);
56
58
  }
57
59
  }
58
60
 
59
- export function resolveSkillSources(manifest, workspaceRoot, mode = "workspace") {
61
+ export function resolveSkillSources(manifest, workspaceRoot) {
60
62
  return manifest.skills.map((skill) => ({
61
63
  ...skill,
62
- sourcePath: path.resolve(workspaceRoot, mode === "package" ? (skill.packageSource || `skills/${skill.name}`) : skill.source),
64
+ sourcePath: path.resolve(workspaceRoot, skill.source),
63
65
  }));
64
66
  }
65
-
66
- export function packageManifest() {
67
- return {
68
- distribution: DISTRIBUTION,
69
- package: PACKAGE_NAME,
70
- cli: "deepflow-suite",
71
- };
72
- }