oasis_test 0.1.6 → 0.1.8

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 (2) hide show
  1. package/dist/index.js +180 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16056,6 +16056,26 @@ function parsePlanningIssues(body) {
16056
16056
  return [];
16057
16057
  }
16058
16058
  }
16059
+ function workorderSpecBody(spec) {
16060
+ return WORKORDER_SPEC_PREFIX + JSON.stringify(spec);
16061
+ }
16062
+ function parseWorkorderSpec(body) {
16063
+ const first = body.split("\n", 1)[0] ?? "";
16064
+ if (!first.startsWith(WORKORDER_SPEC_PREFIX)) return null;
16065
+ try {
16066
+ const parsed = JSON.parse(first.slice(WORKORDER_SPEC_PREFIX.length));
16067
+ if (!parsed || typeof parsed !== "object") return null;
16068
+ const out = {};
16069
+ if (typeof parsed.goal === "string" && parsed.goal.trim()) out.goal = parsed.goal.trim();
16070
+ if (Array.isArray(parsed.acceptanceCriteria)) {
16071
+ const items = parsed.acceptanceCriteria.filter((x) => typeof x === "string" && x.trim().length > 0).map((x) => x.trim());
16072
+ if (items.length > 0) out.acceptanceCriteria = items;
16073
+ }
16074
+ return out.goal !== void 0 || out.acceptanceCriteria !== void 0 ? out : null;
16075
+ } catch {
16076
+ return null;
16077
+ }
16078
+ }
16059
16079
  async function buildDynamicWorkorderPlan(input) {
16060
16080
  const schema = input.schema;
16061
16081
  const schemaByName = new Map(schema.map((def) => [def.name, def]));
@@ -16088,6 +16108,15 @@ async function buildDynamicWorkorderPlan(input) {
16088
16108
  description: input.description
16089
16109
  }
16090
16110
  ];
16111
+ const spec = {};
16112
+ if (input.goal?.trim()) spec.goal = input.goal.trim();
16113
+ if (input.acceptanceCriteria?.length) {
16114
+ const items = input.acceptanceCriteria.map((x) => x.trim()).filter(Boolean);
16115
+ if (items.length > 0) spec.acceptanceCriteria = items;
16116
+ }
16117
+ if (spec.goal !== void 0 || spec.acceptanceCriteria !== void 0) {
16118
+ ops.push({ action: "annotate", artifactId: briefId, body: workorderSpecBody(spec), blocks: false });
16119
+ }
16091
16120
  const spawned = [{ id: briefId, type: "brief", role: "pm", owner: briefOwner }];
16092
16121
  const agentGraph = input.agentPlan ? normalizeAgentPlan(input.agentPlan, primaryType) : null;
16093
16122
  if (input.agentPlan && !agentGraph) {
@@ -16420,7 +16449,7 @@ function artifactSlug(idOrWorkspace, description) {
16420
16449
  const slug4 = source.trim().toLowerCase().replace(/^wo-/, "").replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
16421
16450
  return slug4 || `wo-${Date.now()}`;
16422
16451
  }
16423
- var PLANNING_ISSUES_PREFIX, DEFAULT_TYPES;
16452
+ var PLANNING_ISSUES_PREFIX, DEFAULT_TYPES, WORKORDER_SPEC_PREFIX;
16424
16453
  var init_planner = __esm({
16425
16454
  "../server/src/domains/collab/planner.ts"() {
16426
16455
  "use strict";
@@ -16434,6 +16463,7 @@ var init_planner = __esm({
16434
16463
  { name: "dev", contentType: "code", ownerRole: "dev" },
16435
16464
  { name: "qa", contentType: "text", ownerRole: "qa" }
16436
16465
  ];
16466
+ WORKORDER_SPEC_PREFIX = "OASIS_WORKORDER_SPEC ";
16437
16467
  }
16438
16468
  });
16439
16469
 
@@ -16710,6 +16740,13 @@ async function runCommand(kernel, blobs, oplog, actor, command, args, ctx = {})
16710
16740
  if ((createdViaArg ?? "manual-spawn") === "seed" && ctx.schema) {
16711
16741
  const title = optStr(args, "title") ?? optStr(args, "description") ?? workspace;
16712
16742
  const brief = optStr(args, "brief") ?? optStr(args, "body") ?? optStr(args, "description") ?? workspace;
16743
+ const goal = optStr(args, "goal");
16744
+ const rawCriteria = args["acceptance_criteria"] ?? args["acceptanceCriteria"];
16745
+ const acceptanceCriteria = Array.isArray(rawCriteria) ? rawCriteria.filter((x) => typeof x === "string" && x.trim().length > 0).map((x) => x.trim()) : void 0;
16746
+ const briefForPlanner = acceptanceCriteria && acceptanceCriteria.length > 0 ? `${brief}
16747
+
16748
+ \u9A8C\u6536\u6807\u51C6\uFF1A
16749
+ ${acceptanceCriteria.map((c) => `- ${c}`).join("\n")}` : brief;
16713
16750
  const agentIssues = [];
16714
16751
  let agentPlan = null;
16715
16752
  if (ctx.planWorkorderWithAgent) {
@@ -16724,7 +16761,7 @@ async function runCommand(kernel, blobs, oplog, actor, command, args, ctx = {})
16724
16761
  requestedType: type,
16725
16762
  workspace,
16726
16763
  title,
16727
- brief,
16764
+ brief: briefForPlanner,
16728
16765
  artifactTypes: ctx.schema.map((def) => ({
16729
16766
  name: def.name,
16730
16767
  ownerRole: def.ownerRole,
@@ -16757,6 +16794,8 @@ async function runCommand(kernel, blobs, oplog, actor, command, args, ctx = {})
16757
16794
  workspace,
16758
16795
  title,
16759
16796
  description: brief,
16797
+ ...goal !== void 0 ? { goal } : {},
16798
+ ...acceptanceCriteria && acceptanceCriteria.length > 0 ? { acceptanceCriteria } : {},
16760
16799
  actor,
16761
16800
  schema: ctx.schema,
16762
16801
  ...optStr(args, "id") !== void 0 ? { requestedArtifactId: optStr(args, "id") } : {},
@@ -18041,6 +18080,29 @@ function createNodeTokenStore(file) {
18041
18080
  }
18042
18081
  };
18043
18082
  }
18083
+ function createEnrollTokenStore(defaultTtlMs = 30 * 60 * 1e3) {
18084
+ const table = /* @__PURE__ */ new Map();
18085
+ const prune = () => {
18086
+ const now = Date.now();
18087
+ for (const [token, entry] of table) if (entry.expiresAt <= now) table.delete(token);
18088
+ };
18089
+ return {
18090
+ issue(nodeId, ttlMs = defaultTtlMs) {
18091
+ prune();
18092
+ const token = `ent_${(0, import_node_crypto3.randomBytes)(24).toString("base64url")}`;
18093
+ const expiresAt = Date.now() + ttlMs;
18094
+ table.set(token, { nodeId, expiresAt });
18095
+ return { token, expiresAt: new Date(expiresAt).toISOString() };
18096
+ },
18097
+ consume(token) {
18098
+ prune();
18099
+ const entry = table.get(token);
18100
+ if (!entry) return null;
18101
+ table.delete(token);
18102
+ return { nodeId: entry.nodeId };
18103
+ }
18104
+ };
18105
+ }
18044
18106
  var import_node_crypto3, fs;
18045
18107
  var init_tokens = __esm({
18046
18108
  "../server/src/tokens.ts"() {
@@ -19074,6 +19136,7 @@ function buildWorkorderSummaries(model, resolveActor, resolveProject) {
19074
19136
  for (const [workspace, arts] of byWorkspace) {
19075
19137
  const members = arts.map((a) => nodeInfo(model, a));
19076
19138
  const seed = arts.find((a) => a.inputs.length === 0) ?? arts[0];
19139
+ const spec = workorderSpecOf(model, arts);
19077
19140
  const concluded = members.filter((m) => m.concluded).length;
19078
19141
  const lastOps = arts.map((a) => model.lastOpAt.get(a.id)).filter((t) => Boolean(t)).sort();
19079
19142
  const participantIds = [...new Set(arts.map((a) => a.owner))];
@@ -19081,8 +19144,10 @@ function buildWorkorderSummaries(model, resolveActor, resolveProject) {
19081
19144
  out.push({
19082
19145
  id: workspace,
19083
19146
  title: woTitle,
19084
- // 工单目标 = 根产物 description;仅当与标题不同(根产物有独立 title)时给出,避免与 title 重复
19147
+ // 工单完整内容 = 根产物 description;仅当与标题不同(根产物有独立 title)时给出,避免与 title 重复
19085
19148
  ...seed.description && seed.description !== woTitle ? { description: seed.description } : {},
19149
+ // 一句话目标:优先取结构化 spec.goal;缺省时前端回退 description 首段(投影不替前端做回退,只给真值)
19150
+ ...spec?.goal ? { goal: spec.goal } : {},
19086
19151
  stage: deriveStage(members),
19087
19152
  owner: ref(seed.owner),
19088
19153
  participants: participantIds.map(ref),
@@ -19100,6 +19165,16 @@ function planningStatusOf(model, arts) {
19100
19165
  const issues = [...model.annotations.values()].filter((annotation) => ids.has(annotation.anchor.target)).flatMap((annotation) => parsePlanningIssues(annotation.body));
19101
19166
  return issues.length > 0 ? { status: "needs_staffing", issues } : { status: "ready", issues: [] };
19102
19167
  }
19168
+ function workorderSpecOf(model, arts) {
19169
+ const seed = arts.find((a) => a.type === "brief") ?? arts.find((a) => a.inputs.length === 0);
19170
+ if (!seed) return null;
19171
+ for (const annotation of model.annotations.values()) {
19172
+ if (annotation.anchor.target !== seed.id) continue;
19173
+ const spec = parseWorkorderSpec(annotation.body);
19174
+ if (spec) return spec;
19175
+ }
19176
+ return null;
19177
+ }
19103
19178
  var init_workorders = __esm({
19104
19179
  "../server/src/domains/collab/workorders.ts"() {
19105
19180
  "use strict";
@@ -24556,7 +24631,8 @@ var init_daemon_hub = __esm({
24556
24631
  cb(false, 401, "invalid node token");
24557
24632
  return;
24558
24633
  }
24559
- info.req.oasisNodeId = nodeId;
24634
+ const claimed = info.req.headers["x-daemon-id"];
24635
+ info.req.oasisNodeId = claimed || nodeId;
24560
24636
  cb(true);
24561
24637
  }
24562
24638
  } : {}
@@ -27437,7 +27513,7 @@ Write-Host " Remove : Unregister-ScheduledTask -TaskName 'OasisNode-${p.nodeId}
27437
27513
  }
27438
27514
  function buildNpxCmd(p) {
27439
27515
  const namePart = p.nodeName ? ` --name ${p.nodeName}` : "";
27440
- return `npx -y oasis_test@latest start --server-ws ${p.gatewayUrl} --id ${p.nodeId}${namePart} --token ${p.token}`;
27516
+ return `npx -y oasis_test@latest start --server-ws ${p.gatewayUrl} --id ${p.nodeId}${namePart} --enroll-token ${p.token}`;
27441
27517
  }
27442
27518
  var init_connect_script = __esm({
27443
27519
  "../server/src/domains/nodes/connect-script.ts"() {
@@ -27488,9 +27564,39 @@ function nodesDomain(deps) {
27488
27564
  };
27489
27565
  }
27490
27566
  return (router) => {
27567
+ async function syncConnectedDaemon(daemonId, hub) {
27568
+ const d = hub.connectedDaemons().find((c) => c.daemonId === daemonId);
27569
+ if (!d) return;
27570
+ const existing = await deps.nodeStore.getNode(daemonId);
27571
+ await deps.nodeStore.upsertNode({
27572
+ id: daemonId,
27573
+ hostname: d.meta.hostname,
27574
+ nodeVersion: d.meta.nodeVersion ?? "unknown",
27575
+ status: "online",
27576
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
27577
+ enrolledAt: existing?.enrolledAt ?? (/* @__PURE__ */ new Date()).toISOString(),
27578
+ ...d.meta.nodeName ? { name: d.meta.nodeName } : existing?.name ? { name: existing.name } : {}
27579
+ });
27580
+ const capabilities = d.meta.runtimes?.length ? d.meta.runtimes : d.meta.adapters.map((kind) => ({ kind, binary: void 0, version: void 0 }));
27581
+ for (const cap of capabilities) {
27582
+ await deps.nodeStore.upsertRuntime({
27583
+ id: `runtime:${daemonId}:${cap.kind}`,
27584
+ nodeId: daemonId,
27585
+ kind: cap.kind,
27586
+ hostname: d.meta.hostname,
27587
+ ...cap.binary ? { binary: cap.binary } : {},
27588
+ ...cap.version ? { version: cap.version } : {},
27589
+ status: "online",
27590
+ lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
27591
+ });
27592
+ }
27593
+ }
27491
27594
  router.get("/api/nodes", async () => {
27492
27595
  const hub = deps.getHub();
27493
- if (hub) wireHub(hub);
27596
+ if (hub) {
27597
+ wireHub(hub);
27598
+ await Promise.all(hub.connectedDaemons().map((d) => syncConnectedDaemon(d.daemonId, hub)));
27599
+ }
27494
27600
  const nodes = await deps.nodeStore.listNodes();
27495
27601
  const items = await Promise.all(nodes.map(async (n) => {
27496
27602
  const rts = await deps.nodeStore.listRuntimes(n.id);
@@ -27517,7 +27623,10 @@ function nodesDomain(deps) {
27517
27623
  });
27518
27624
  router.get("/api/runtimes", async () => {
27519
27625
  const hub = deps.getHub();
27520
- if (hub) wireHub(hub);
27626
+ if (hub) {
27627
+ wireHub(hub);
27628
+ await Promise.all(hub.connectedDaemons().map((d) => syncConnectedDaemon(d.daemonId, hub)));
27629
+ }
27521
27630
  const rts = await deps.nodeStore.listRuntimes();
27522
27631
  const items = rts.map((r) => ({
27523
27632
  id: r.id,
@@ -27534,28 +27643,28 @@ function nodesDomain(deps) {
27534
27643
  });
27535
27644
  router.post("/api/nodes/enroll", async (req) => {
27536
27645
  const body = req.body ?? {};
27537
- const requested = body.nodeId?.trim();
27538
- const nodeId = requested || `node-${(0, import_node_crypto9.randomUUID)().slice(0, 8)}`;
27539
27646
  const nodeName = body.nodeName?.trim() || void 0;
27540
- for (const { token: token2, nodeId: existing2 } of deps.nodeTokens.list()) {
27541
- if (existing2 === nodeId) deps.nodeTokens.revoke(token2);
27542
- }
27543
- const token = deps.nodeTokens.issue(nodeId);
27647
+ const requestedId = body.nodeId?.trim();
27648
+ const existing = requestedId ? await deps.nodeStore.getNode(requestedId) : null;
27649
+ const nodeId = existing?.id ?? requestedId ?? `node-${(0, import_node_crypto9.randomUUID)().slice(0, 8)}`;
27650
+ const enroll = deps.enrollTokens.issue(nodeId);
27544
27651
  if (nodeName) nodeNames.set(nodeId, nodeName);
27545
- const existing = await deps.nodeStore.getNode(nodeId);
27546
- await deps.nodeStore.upsertNode({
27547
- id: nodeId,
27548
- name: nodeName,
27549
- hostname: existing?.hostname ?? "unknown",
27550
- nodeVersion: existing?.nodeVersion ?? "unknown",
27551
- status: "offline",
27552
- lastSeenAt: existing?.lastSeenAt ?? (/* @__PURE__ */ new Date()).toISOString(),
27553
- enrolledAt: existing?.enrolledAt ?? (/* @__PURE__ */ new Date()).toISOString()
27554
- });
27555
- const params = { gatewayUrl: deps.gatewayUrl, nodeId, token, repoRemote: deps.repoRemote };
27652
+ if (existing && nodeName) await deps.nodeStore.updateNodeName(nodeId, nodeName);
27653
+ const params = { gatewayUrl: deps.gatewayUrl, nodeId, token: enroll.token, repoRemote: deps.repoRemote };
27556
27654
  const script = body.platform === "windows" ? buildWindowsConnectScript(params) : buildConnectScript(params);
27557
- const npxCmd = buildNpxCmd({ gatewayUrl: deps.gatewayUrl, nodeId, token, nodeName });
27558
- return { status: 200, body: { nodeId, nodeName, token, script, gatewayUrl: deps.gatewayUrl, npxCmd } };
27655
+ const npxCmd = buildNpxCmd({ gatewayUrl: deps.gatewayUrl, nodeId, token: enroll.token, nodeName });
27656
+ return { status: 200, body: { nodeId, nodeName, token: enroll.token, script, gatewayUrl: deps.gatewayUrl, npxCmd } };
27657
+ });
27658
+ router.post("/api/nodes/exchange", async (req) => {
27659
+ const body = req.body ?? {};
27660
+ const enrollToken = body.enrollToken?.trim();
27661
+ if (!enrollToken) return { status: 400, body: { error: "missing enrollToken" } };
27662
+ const consumed = deps.enrollTokens.consume(enrollToken);
27663
+ if (!consumed) return { status: 401, body: { error: "invalid or expired enroll token" } };
27664
+ const nodeId = body.nodeId?.trim() || consumed.nodeId;
27665
+ const existing = deps.nodeTokens.list().find((e) => e.nodeId === nodeId);
27666
+ const token = existing?.token ?? deps.nodeTokens.issue(nodeId);
27667
+ return { status: 200, body: { nodeId, token } };
27559
27668
  });
27560
27669
  router.patch("/api/nodes/:nodeId", async (req) => {
27561
27670
  const nodeId = req.params["nodeId"];
@@ -27750,7 +27859,15 @@ function buildWorkorderDetail(model, workspaceId, resolveActor, resolveProject,
27750
27859
  const activity = buildActivity(model, arts, ref);
27751
27860
  const acceptance = buildAcceptance(model, arts);
27752
27861
  const planning = planningStatusOf(model, arts);
27753
- return { summary, graph: { nodes, edges }, activity, acceptance, planning };
27862
+ const spec = workorderSpecOf(model, arts);
27863
+ return {
27864
+ summary,
27865
+ graph: { nodes, edges },
27866
+ activity,
27867
+ acceptance,
27868
+ ...spec?.acceptanceCriteria ? { acceptanceCriteria: spec.acceptanceCriteria } : {},
27869
+ planning
27870
+ };
27754
27871
  }
27755
27872
  function buildRuntimeByArtifact(entries, ref) {
27756
27873
  const byJob = /* @__PURE__ */ new Map();
@@ -29736,6 +29853,7 @@ var init_src4 = __esm({
29736
29853
  init_sender();
29737
29854
  init_routes4();
29738
29855
  init_node_store();
29856
+ init_tokens();
29739
29857
  init_collab();
29740
29858
  init_trace2();
29741
29859
  init_chat_sessions();
@@ -35235,7 +35353,9 @@ function buildClaudeUsage(acc) {
35235
35353
  }
35236
35354
  function scrubLeakyEnv(env) {
35237
35355
  const LEAKY = /^npm_|^PNPM_|^INIT_CWD$|^OLDPWD$/;
35238
- return Object.fromEntries(Object.entries(env).filter(([k]) => !LEAKY.test(k)));
35356
+ const out = Object.fromEntries(Object.entries(env).filter(([k]) => !LEAKY.test(k)));
35357
+ if (out["ANTHROPIC_AUTH_TOKEN"]) delete out["ANTHROPIC_API_KEY"];
35358
+ return out;
35239
35359
  }
35240
35360
  function classifyExit(result) {
35241
35361
  if (!result) return void 0;
@@ -38898,7 +39018,6 @@ async function buildActorProvision(service, actorId) {
38898
39018
  if (raw["FEISHU_APP_ID"] && raw["FEISHU_APP_SECRET"]) {
38899
39019
  try {
38900
39020
  const feishu = new FeishuConnector();
38901
- await feishu.connect({ app_id: raw["FEISHU_APP_ID"], app_secret: raw["FEISHU_APP_SECRET"] });
38902
39021
  const sessionEnv = /* @__PURE__ */ new Map();
38903
39022
  await feishu.inject({ app_id: raw["FEISHU_APP_ID"], app_secret: raw["FEISHU_APP_SECRET"] }, sessionEnv);
38904
39023
  for (const [k, v] of sessionEnv) env[k] = v;
@@ -38991,6 +39110,7 @@ async function startServe(opts) {
38991
39110
  const operatorToken = issuer.issue(operatorActor);
38992
39111
  fs13.writeFileSync(path11.join(opts.dir, "operator-token"), operatorToken, { mode: 384 });
38993
39112
  const nodeTokens = createNodeTokenStore(path11.join(opts.dir, "node-tokens.json"));
39113
+ const enrollTokens = createEnrollTokenStore();
38994
39114
  const nodeStore = await FileNodeStore.open(path11.join(opts.dir, "nodes.json"));
38995
39115
  const registryAuditFile = path11.join(opts.dir, "registry-audit.ndjson");
38996
39116
  const registryListeners = /* @__PURE__ */ new Set();
@@ -39238,6 +39358,7 @@ async function startServe(opts) {
39238
39358
  nodesDomain({
39239
39359
  getHub: () => hub,
39240
39360
  nodeTokens,
39361
+ enrollTokens,
39241
39362
  nodeStore,
39242
39363
  gatewayUrl: opts.gatewayUrl ?? `ws://127.0.0.1:${opts.port ?? 7320}/node-gateway`,
39243
39364
  repoRemote: opts.nodeRepoRemote ?? "git@github.com:open-friday/oasis-core.git"
@@ -39979,7 +40100,7 @@ var USAGE = `oasis \u2014\u2014 artifact-centric \u534F\u4F5C\u5185\u6838 CLI\uF
39979
40100
  accept <artifactId> [--note "\u2026"] # \u5B9A\u7A3F\uFF08\u5148 conclude \u518D accept\uFF09
39980
40101
  assign <artifactId> --owner <actorId>
39981
40102
  gc # \u5B64\u513F blob \u56DE\u6536\uFF08mark-and-sweep\uFF1B\u64CD\u4F5C\u8005\u547D\u4EE4\uFF0CF1\uFF09
39982
- \u8BFB\u4EA7\u7269: status <id>\uFF08\u72B6\u6001\u7D22\u5F15\uFF0C\u4E0D\u542B\u6B63\u6587\uFF09/ content <id> [path]\uFF08\u5B9E\u9645\u5185\u5BB9\uFF0C\u591A\u6587\u4EF6\u7ED9 path \u53D6\u5355\u4E2A\uFF09/ project <id>\uFF08\u9879\u76EE\u76EE\u5F55\uFF09 # \u7ED9 agent\uFF0C\u6E10\u8FDB\u62AB\u9732
40103
+ \u8BFB\u4EA7\u7269: status <id>\uFF08\u72B6\u6001\u7D22\u5F15\uFF0C\u4E0D\u542B\u6B63\u6587\uFF09/ content <id> [path]\uFF08\u5B9E\u9645\u5185\u5BB9\uFF0C\u591A\u6587\u4EF6\u7ED9 path \u53D6\u5355\u4E2A\uFF09/ projects [--search q]\uFF08\u5217/\u641C\u9879\u76EE\uFF09/ project <id>\uFF08\u9879\u76EE\u76EE\u5F55\uFF09 # \u7ED9 agent\uFF0C\u6E10\u8FDB\u62AB\u9732
39983
40104
  graph / show / log / ls # \u5168\u5C40/\u8BE6\u5C3D\u89C6\u56FE\uFF08\u534F\u8C03\u8005\u3001\u4EBA\uFF1Bls \u4EC5\u534F\u8C03\u8005\uFF09
39984
40105
 
39985
40106
  \u6CBB\u7406\uFF08\xA711\uFF09
@@ -40369,6 +40490,16 @@ async function runCli(argv, println = console.log) {
40369
40490
  return;
40370
40491
  }
40371
40492
  switch (command) {
40493
+ case "projects": {
40494
+ const res = await api.request("GET", "/api/projects");
40495
+ const q = flags.get("search")?.trim().toLowerCase();
40496
+ const items = q ? res.items.filter((p) => `${p.project_id} ${p.name} ${p.description ?? ""}`.toLowerCase().includes(q)) : res.items;
40497
+ println(`\u9879\u76EE(${items.length}${q ? ` / \u641C\u300C${q}\u300D` : ""})\u2014\u2014\u8BE6\u60C5\u7528 \`oasis project <id>\`\u3001\u6CA1\u5408\u9002\u7684\u53EF \`oasis project create\`:`);
40498
+ for (const p of items) println(`- ${p.project_id} ${p.name}${p.description ? ` \u76EE\u6807: ${p.description}` : ""}`);
40499
+ if (items.length === 0) println("\uFF08\u65E0\u5339\u914D\u2014\u2014\u53EF project create \u65B0\u5EFA\uFF0C\u6216\u5411\u7528\u6237\u6F84\u6E05\uFF09");
40500
+ if (res.next_cursor) println("\uFF08\u8FD8\u6709\u66F4\u591A\u9879\u76EE\u672A\u5217\u5168\uFF0C\u7528 --search \u7F29\u5C0F\u8303\u56F4\uFF09");
40501
+ break;
40502
+ }
40372
40503
  case "project": {
40373
40504
  const projSub = positional[0];
40374
40505
  if (projSub && !["create", "docs", "progress", "run-state"].includes(projSub)) {
@@ -41129,13 +41260,27 @@ void (async () => {
41129
41260
  if (cmd === "start") {
41130
41261
  const prev = readConfig();
41131
41262
  const serverUrl = flags.get("server-ws") ?? process.env["OASIS_SERVER_WS"] ?? prev?.serverUrl;
41132
- const token = flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
41133
- if (!serverUrl || !token) {
41134
- console.error("Usage: oasis start --server-ws <wss://...> --token <token> [--name <name>] [--id <nodeId>]");
41263
+ if (!serverUrl) {
41264
+ console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--id <nodeId>]");
41265
+ process.exit(1);
41266
+ }
41267
+ const nodeId = prev?.nodeId ?? flags.get("id") ?? `node-${(0, import_node_crypto21.randomUUID)()}`;
41268
+ let token = flags.get("token") ?? process.env["OASIS_NODE_TOKEN"] ?? prev?.token;
41269
+ const enrollToken = flags.get("enroll-token") ?? process.env["OASIS_ENROLL_TOKEN"];
41270
+ if (enrollToken) {
41271
+ const res = await fetch(`${httpBase(serverUrl)}/api/nodes/exchange`, {
41272
+ method: "POST",
41273
+ headers: { "content-type": "application/json" },
41274
+ body: JSON.stringify({ enrollToken, nodeId })
41275
+ });
41276
+ if (!res.ok) throw new Error(`enroll token exchange failed: HTTP ${res.status}`);
41277
+ const body = await res.json();
41278
+ token = body.token;
41279
+ }
41280
+ if (!token) {
41281
+ console.error("Usage: oasis start --server-ws <wss://...> (--enroll-token <ent_...> | --token <ont_...>) [--name <name>] [--id <nodeId>]");
41135
41282
  process.exit(1);
41136
41283
  }
41137
- const isReEnrollment = flags.get("token") && flags.get("token") !== prev?.token;
41138
- const nodeId = isReEnrollment ? flags.get("id") ?? prev?.nodeId ?? `node-${(0, import_node_crypto21.randomUUID)()}` : prev?.nodeId ?? flags.get("id") ?? `node-${(0, import_node_crypto21.randomUUID)()}`;
41139
41284
  const name = flags.get("name") ?? prev?.name;
41140
41285
  const nameChanged = prev?.name !== void 0 && name !== prev.name;
41141
41286
  saveConfig({ serverUrl, token, nodeId, ...name ? { name } : {} });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": { "oasis": "./dist/index.js" },
6
6
  "files": ["dist"],