@jennie-shawn/starwork 0.1.0-alpha.5 → 0.1.0-alpha.6

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/cli/README.md CHANGED
@@ -25,10 +25,15 @@ v0.1 只覆盖最小可用安装和适配能力:
25
25
 
26
26
  - `starwork init` 第一版:可以初始化轻量单项目、长期单项目和多项目管理中枢,并通过 Pack 语言配置组装通用工作、内容创作者和中枢管理场景。
27
27
  - `starwork spawn` 第一版:可以从健康 Hub 生成 `satellite-starter` / `satellite-matter` 项目工作台,支持 `--blueprint` 定制目录、路径、规则和 seed,并回写 Hub 项目注册表。
28
- - `starwork doctor` 第一版:可以检查 workspace state、Core 必需角色、Kit 文件、正式事实源、业务工作区和 Pack 落地结果,并支持 `--json` 输出;alpha.4 开始可识别历史模板并给出升级建议;alpha.5 开始输出目录 `inventory` 与语义 `signals`,供 `starworkDoctor` skill 判断。
28
+ - `starwork doctor` 第一版:可以检查 workspace state、Core 必需角色、Kit 文件、正式事实源、业务工作区和 Pack 落地结果,并支持 `--json` 输出;alpha.4 开始可识别历史模板候选;alpha.5 开始输出目录 `inventory` 与语义 `signals`,供 `starworkDoctor` skill 判断。
29
+ - `starwork upgrade` 第一版:可以读取 `starworkUpgrade` skill 生成的升级蓝图,把历史模板或非标准目录安全升级为 StarWork 工作台;v0.1 只支持 `--blueprint`,不自动判断升级方案。
29
30
  - `starwork adapt` 第一版:可以为 Codex、Claude Code、Cursor、Trae 生成或登记轻量适配入口。
30
31
  - `starwork pack install` 第一版:可以在健康工作台上补装 Pack,并更新路径、规则、模板和 workspace state。
31
32
 
33
+ 后续规划:
34
+
35
+ - `starwork update`:面向已经是 StarWork 的工作台,处理未来 Core / Kit / Pack 版本迁移;与 `upgrade` 分开设计。
36
+
32
37
  CLI 不在 v0.1 阶段处理账号、授权、消息平台 gateway 或复杂商业系统。
33
38
 
34
39
  ## 命令规格
@@ -39,6 +44,7 @@ CLI 不在 v0.1 阶段处理账号、授权、消息平台 gateway 或复杂商
39
44
  - [`starwork pack install` SPEC](./pack-install-spec.md)
40
45
  - [`starwork spawn` SPEC](./spawn-spec.md)
41
46
  - [`starwork spawn --blueprint` SPEC](./spawn-blueprint-spec.md)
47
+ - [`starwork upgrade` SPEC](./upgrade-spec.md)
42
48
 
43
49
  ## 本地运行
44
50
 
@@ -46,6 +52,7 @@ CLI 不在 v0.1 阶段处理账号、授权、消息平台 gateway 或复杂商
46
52
  node cli/bin/starwork.js init --type single-light --pack general --dry-run
47
53
  node cli/bin/starwork.js spawn --hub ./my-hub --name "新项目" --target ./new-project --mode matter --dry-run
48
54
  node cli/bin/starwork.js spawn --hub ./my-hub --target ./new-project --blueprint ./blueprint.json --dry-run
55
+ node cli/bin/starwork.js upgrade --target ./old-workspace --blueprint ./upgrade-blueprint.json --dry-run
49
56
  node cli/bin/starwork.js doctor --target ./my-workspace
50
57
  node cli/bin/starwork.js adapt claude --target ./my-workspace --yes
51
58
  node cli/bin/starwork.js pack install content-creator --target ./my-workspace --yes
package/cli/src/cli.js CHANGED
@@ -90,6 +90,11 @@ async function run(argv) {
90
90
  return;
91
91
  }
92
92
 
93
+ if (command === "upgrade") {
94
+ await upgradeWorkspace(argv.slice(1));
95
+ return;
96
+ }
97
+
93
98
  if (command === "adapt") {
94
99
  await adapt(argv.slice(1));
95
100
  return;
@@ -368,6 +373,52 @@ async function packInstall(argv) {
368
373
  console.log("下一步建议:运行 starwork doctor 检查 Pack 落地结果。");
369
374
  }
370
375
 
376
+ async function upgradeWorkspace(argv) {
377
+ const options = parseArgs(argv);
378
+ if (options.help) {
379
+ printUpgradeHelp();
380
+ return;
381
+ }
382
+
383
+ if (!options.blueprint) {
384
+ throw new Error("upgrade v0.1 必须传入 --blueprint <upgrade-blueprint.json>。请先用 starworkUpgrade skill 生成升级蓝图。");
385
+ }
386
+
387
+ const targetDir = path.resolve(options.target || process.cwd());
388
+ if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
389
+ throw new Error(`upgrade 目标目录不存在或不是目录:${targetDir}`);
390
+ }
391
+ if (findWorkspaceRoot(targetDir)) {
392
+ throw new Error("当前目录已经是 StarWork 工作台,不应使用 upgrade。后续请使用 update 或 repair。");
393
+ }
394
+
395
+ const blueprint = loadUpgradeBlueprint(options.blueprint);
396
+ const plan = buildUpgradePlan({ targetDir, blueprint });
397
+
398
+ if (options.json && options.dryRun) {
399
+ console.log(JSON.stringify(renderUpgradePlanJson(plan, true), null, 2));
400
+ return;
401
+ }
402
+
403
+ if (!options.json) {
404
+ printUpgradePlan(plan, options.dryRun);
405
+ }
406
+ if (options.dryRun) return;
407
+
408
+ await confirmOrThrow(options, "是否按 upgrade blueprint 执行升级?");
409
+ applyPlan(plan);
410
+
411
+ if (options.json) {
412
+ console.log(JSON.stringify(renderUpgradeExecutionJson(plan), null, 2));
413
+ return;
414
+ }
415
+
416
+ console.log("");
417
+ console.log("StarWork 工作台升级已完成。");
418
+ console.log("");
419
+ console.log(`下一步建议:运行 starwork doctor --target ${plan.targetDir}`);
420
+ }
421
+
371
422
  function doctor(argv) {
372
423
  const options = parseArgs(argv);
373
424
  if (options.help) {
@@ -619,6 +670,241 @@ function buildPackInstallPlan({ workspaceRoot, state, pack }) {
619
670
  };
620
671
  }
621
672
 
673
+ function loadUpgradeBlueprint(blueprintPath) {
674
+ const filePath = path.resolve(blueprintPath);
675
+ let blueprint;
676
+ try {
677
+ blueprint = JSON.parse(fs.readFileSync(filePath, "utf8"));
678
+ } catch (error) {
679
+ throw new Error(`无法读取 upgrade blueprint:${error.message}`);
680
+ }
681
+ if (blueprint.schema !== "starwork.upgrade_blueprint.v0.1") {
682
+ throw new Error("upgrade blueprint schema 必须是 starwork.upgrade_blueprint.v0.1。");
683
+ }
684
+ if (!blueprint.base || typeof blueprint.base !== "object") {
685
+ throw new Error("upgrade blueprint 缺少 base 配置。");
686
+ }
687
+ const workspaceType = blueprint.base.workspace_type;
688
+ const kit = blueprint.base.kit;
689
+ const allowedKits = {
690
+ "single-light": "local-starter",
691
+ "single-matter": "local-matter"
692
+ };
693
+ if (!allowedKits[workspaceType]) {
694
+ throw new Error("upgrade blueprint base.workspace_type 只支持 single-light 或 single-matter。");
695
+ }
696
+ if (kit !== allowedKits[workspaceType]) {
697
+ throw new Error(`upgrade blueprint base.kit (${kit}) 与 ${workspaceType} 不匹配,应为 ${allowedKits[workspaceType]}。`);
698
+ }
699
+ if (!["zh", "en"].includes(blueprint.base.language)) {
700
+ throw new Error("upgrade blueprint base.language 只支持 zh 或 en。");
701
+ }
702
+ if (!["preserve-names", "add-standard-shell", "standardize-empty-paths"].includes(blueprint.strategy)) {
703
+ throw new Error("upgrade blueprint strategy 暂只支持 preserve-names、add-standard-shell 或 standardize-empty-paths。");
704
+ }
705
+ if (!blueprint.paths?.formal_source || !blueprint.paths?.business_work_area) {
706
+ throw new Error("upgrade blueprint 缺少 paths.formal_source 或 paths.business_work_area。");
707
+ }
708
+ normalizeSafeRelativePath(blueprint.paths.formal_source, "upgrade paths.formal_source");
709
+ normalizeSafeRelativePath(blueprint.paths.business_work_area, "upgrade paths.business_work_area");
710
+ if (!Array.isArray(blueprint.actions) || blueprint.actions.length === 0) {
711
+ throw new Error("upgrade blueprint 必须包含 actions。");
712
+ }
713
+ validateUpgradeBlueprintActions(blueprint, path.dirname(filePath));
714
+ for (const preserved of blueprint.preserve || []) {
715
+ normalizeSafeRelativePath(preserved, "upgrade preserve");
716
+ }
717
+ return {
718
+ ...blueprint,
719
+ __path: filePath,
720
+ __dir: path.dirname(filePath)
721
+ };
722
+ }
723
+
724
+ function validateUpgradeBlueprintActions(blueprint, blueprintDir) {
725
+ const supported = new Set([
726
+ "ensure_dir",
727
+ "write_workspace_state",
728
+ "copy_kit_missing_files",
729
+ "inject_agent_rules",
730
+ "write_file",
731
+ "copy_seed"
732
+ ]);
733
+ for (const action of blueprint.actions) {
734
+ if (!supported.has(action?.type)) {
735
+ throw new Error(`upgrade blueprint 不支持 action.type:${action?.type || "(missing)"}`);
736
+ }
737
+ if (action.path) {
738
+ normalizeSafeRelativePath(action.path, `upgrade action ${action.type}.path`);
739
+ }
740
+ if (action.target) {
741
+ normalizeSafeRelativePath(action.target, `upgrade action ${action.type}.target`);
742
+ }
743
+ if (action.to) {
744
+ normalizeSafeRelativePath(action.to, `upgrade action ${action.type}.to`);
745
+ }
746
+ if (action.from) {
747
+ normalizeSafeSourcePath(action.from, blueprintDir, `upgrade action ${action.type}.from`);
748
+ }
749
+ if (action.type === "inject_agent_rules" && (!action.slot || typeof action.slot !== "string")) {
750
+ throw new Error("upgrade inject_agent_rules action 必须包含 slot。");
751
+ }
752
+ if (action.type === "write_file" && typeof action.content !== "string") {
753
+ throw new Error("upgrade write_file action 必须包含 content 字符串。");
754
+ }
755
+ if (action.type === "copy_seed") {
756
+ const conflict = action.on_conflict || "error";
757
+ if (!["error", "skip"].includes(conflict)) {
758
+ throw new Error("upgrade copy_seed on_conflict 只支持 error 或 skip。");
759
+ }
760
+ }
761
+ }
762
+ }
763
+
764
+ function buildUpgradePlan({ targetDir, blueprint }) {
765
+ const kitDir = path.join(PRODUCT_ROOT, "core", "kits", blueprint.base.kit);
766
+ if (!fs.existsSync(kitDir)) {
767
+ throw new Error(`找不到 Kit:${blueprint.base.kit}`);
768
+ }
769
+
770
+ const packId = blueprint.base.pack || "general";
771
+ const pack = packId ? loadPack(packId, blueprint.base.language) : null;
772
+ if (pack) validatePack(pack, blueprint.base.workspace_type);
773
+
774
+ const now = new Date().toISOString();
775
+ const variables = buildUpgradeVariables(blueprint, { targetDir, pack });
776
+ const actions = [];
777
+ const injectedTargets = new Set((blueprint.actions || [])
778
+ .filter((action) => action.type === "inject_agent_rules")
779
+ .map((action) => normalizeSafeRelativePath(action.target || "AGENTS.md", "upgrade inject target")));
780
+
781
+ for (const action of blueprint.actions) {
782
+ if (action.type === "ensure_dir") {
783
+ actions.push(directoryAction(targetDir, normalizeSafeRelativePath(action.path, "upgrade ensure_dir.path")));
784
+ } else if (action.type === "write_workspace_state") {
785
+ actions.push(strictFileAction(targetDir, path.join(".starwork", "workspace.json"), renderUpgradeWorkspaceState(blueprint, pack, now)));
786
+ } else if (action.type === "copy_kit_missing_files") {
787
+ actions.push(...buildUpgradeKitActions(targetDir, kitDir, injectedTargets));
788
+ } else if (action.type === "inject_agent_rules") {
789
+ actions.push(buildUpgradeAgentRuleAction(targetDir, kitDir, blueprint, action, variables));
790
+ } else if (action.type === "write_file") {
791
+ const target = normalizeSafeRelativePath(action.path, "upgrade write_file.path");
792
+ actions.push(strictFileAction(targetDir, target, renderText(action.content, variables)));
793
+ } else if (action.type === "copy_seed") {
794
+ const source = normalizeSafeSourcePath(action.from, blueprint.__dir, "upgrade copy_seed.from");
795
+ const target = normalizeSafeRelativePath(action.to, "upgrade copy_seed.to");
796
+ const targetPath = path.join(targetDir, target);
797
+ if (fs.existsSync(targetPath)) {
798
+ if ((action.on_conflict || "error") === "skip") continue;
799
+ throw new Error(`upgrade copy_seed 目标已存在:${target}`);
800
+ }
801
+ actions.push(strictFileAction(targetDir, target, renderText(fs.readFileSync(source, "utf8"), variables)));
802
+ }
803
+ }
804
+
805
+ actions.push(directoryAction(targetDir, normalizeSafeRelativePath(blueprint.paths.formal_source, "paths.formal_source")));
806
+ actions.push(directoryAction(targetDir, normalizeSafeRelativePath(blueprint.paths.business_work_area, "paths.business_work_area")));
807
+ if (pack) {
808
+ for (const rolePath of Object.values(pack.paths || {})) {
809
+ actions.push(directoryAction(targetDir, normalizeSafeRelativePath(rolePath, "pack.paths")));
810
+ }
811
+ }
812
+
813
+ return {
814
+ targetDir,
815
+ blueprint,
816
+ strategy: blueprint.strategy,
817
+ workspaceType: blueprint.base.workspace_type,
818
+ kit: blueprint.base.kit,
819
+ language: blueprint.base.language,
820
+ pack,
821
+ actions: dedupeActions(actions.filter(Boolean))
822
+ };
823
+ }
824
+
825
+ function buildUpgradeKitActions(targetDir, kitDir, injectedTargets) {
826
+ const actions = [];
827
+ for (const source of walkFiles(kitDir)) {
828
+ const relativePath = normalizeRelativePath(path.relative(kitDir, source));
829
+ if (injectedTargets.has(relativePath)) continue;
830
+ const target = path.join(targetDir, relativePath);
831
+ if (fs.existsSync(target)) continue;
832
+ actions.push(strictFileAction(targetDir, relativePath, fs.readFileSync(source, "utf8")));
833
+ }
834
+ return actions;
835
+ }
836
+
837
+ function buildUpgradeAgentRuleAction(targetDir, kitDir, blueprint, action, variables) {
838
+ const target = normalizeSafeRelativePath(action.target || "AGENTS.md", "upgrade inject_agent_rules.target");
839
+ const source = normalizeSafeSourcePath(action.from, blueprint.__dir, "upgrade inject_agent_rules.from");
840
+ const slot = action.slot;
841
+ const marker = `StarWork Upgrade: ${slot}`;
842
+ const targetPath = path.join(targetDir, target);
843
+ const kitDefaultPath = path.join(kitDir, target);
844
+ const existing = fs.existsSync(targetPath)
845
+ ? fs.readFileSync(targetPath, "utf8")
846
+ : fs.existsSync(kitDefaultPath)
847
+ ? fs.readFileSync(kitDefaultPath, "utf8")
848
+ : "";
849
+ if (existing.includes(marker)) return null;
850
+
851
+ const ruleContent = renderText(fs.readFileSync(source, "utf8"), variables).trim();
852
+ if (!ruleContent) return null;
853
+ const content = `${existing.trim()}\n\n## StarWork 升级规则\n\n<!-- ${marker} -->\n\n${ruleContent}\n`;
854
+ return fs.existsSync(targetPath)
855
+ ? overwriteFileAction(targetDir, target, content)
856
+ : strictFileAction(targetDir, target, content);
857
+ }
858
+
859
+ function renderUpgradeWorkspaceState(blueprint, pack, now) {
860
+ const packRecord = pack ? [{
861
+ id: pack.id,
862
+ version: pack.version || "0.1.0",
863
+ installed_at: now
864
+ }] : [];
865
+ const state = {
866
+ schema: "starwork.workspace.v0.1",
867
+ core: "0.1",
868
+ workspace_type: blueprint.base.workspace_type,
869
+ kit: blueprint.base.kit,
870
+ packs: packRecord,
871
+ language: blueprint.base.language,
872
+ paths: {
873
+ formal_source: normalizeSafeRelativePath(blueprint.paths.formal_source, "paths.formal_source"),
874
+ business_work_area: normalizeSafeRelativePath(blueprint.paths.business_work_area, "paths.business_work_area")
875
+ },
876
+ upgrade: {
877
+ type: "upgrade_blueprint",
878
+ schema: blueprint.schema,
879
+ source: path.basename(blueprint.__path),
880
+ strategy: blueprint.strategy,
881
+ generated_by: blueprint.generated_by || "starworkUpgrade",
882
+ core_role_mapping: Array.isArray(blueprint.core_role_mapping) ? blueprint.core_role_mapping : [],
883
+ upgraded_at: now
884
+ },
885
+ created_by: "starwork upgrade"
886
+ };
887
+ return `${JSON.stringify(state, null, 2)}\n`;
888
+ }
889
+
890
+ function buildUpgradeVariables(blueprint, { targetDir, pack }) {
891
+ return {
892
+ blueprint,
893
+ workspace: {
894
+ name: path.basename(targetDir),
895
+ type: blueprint.base.workspace_type
896
+ },
897
+ paths: {
898
+ formal_source: normalizeSafeRelativePath(blueprint.paths.formal_source, "paths.formal_source"),
899
+ business_work_area: normalizeSafeRelativePath(blueprint.paths.business_work_area, "paths.business_work_area")
900
+ },
901
+ upgrade: {
902
+ strategy: blueprint.strategy
903
+ },
904
+ pack: pack || null
905
+ };
906
+ }
907
+
622
908
  function renderInstalledPackRules(pack, variables) {
623
909
  const rules = renderPackRules(pack, variables);
624
910
  if (!rules.trim()) return "";
@@ -864,6 +1150,9 @@ function normalizeSafeRelativePath(relativePath, label) {
864
1150
  if (normalized === ".git" || normalized.startsWith(".git/")) {
865
1151
  throw new Error(`${label} 不能写入 .git:${relativePath}`);
866
1152
  }
1153
+ if (normalized === "node_modules" || normalized.startsWith("node_modules/")) {
1154
+ throw new Error(`${label} 不能写入 node_modules:${relativePath}`);
1155
+ }
867
1156
  return normalized;
868
1157
  }
869
1158
 
@@ -2013,6 +2302,14 @@ function fileAction(targetDir, relativePath, content) {
2013
2302
  return { type: "file", mode: "create-new", target: alternate, originalTarget: target, relativePath: path.relative(targetDir, alternate), content };
2014
2303
  }
2015
2304
 
2305
+ function strictFileAction(targetDir, relativePath, content) {
2306
+ const target = path.join(targetDir, relativePath);
2307
+ if (fs.existsSync(target)) {
2308
+ throw new Error(`目标文件已存在,upgrade 不会覆盖:${relativePath}`);
2309
+ }
2310
+ return { type: "file", mode: "create", target, relativePath, content };
2311
+ }
2312
+
2016
2313
  function overwriteFileAction(targetDir, relativePath, content) {
2017
2314
  const target = path.join(targetDir, relativePath);
2018
2315
  return { type: "file", mode: "overwrite", target, relativePath, content };
@@ -2054,7 +2351,7 @@ function dedupeActions(actions) {
2054
2351
 
2055
2352
  function applyPlan(plan) {
2056
2353
  for (const action of plan.actions) {
2057
- if (action.mode === "exists") continue;
2354
+ if (action.mode === "exists" || action.mode === "skip") continue;
2058
2355
  if (action.type === "directory") {
2059
2356
  fs.mkdirSync(action.target, { recursive: true });
2060
2357
  continue;
@@ -2180,6 +2477,101 @@ function printSpawnPlan(plan, dryRun) {
2180
2477
  }
2181
2478
  }
2182
2479
 
2480
+ function printUpgradePlan(plan, dryRun) {
2481
+ const creates = plan.actions.filter((action) => action.type === "file" && action.mode === "create");
2482
+ const overwrites = plan.actions.filter((action) => action.type === "file" && action.mode === "overwrite");
2483
+ const dirs = plan.actions.filter((action) => action.type === "directory" && action.mode === "create");
2484
+ const existingDirs = plan.actions.filter((action) => action.type === "directory" && action.mode === "exists");
2485
+
2486
+ console.log("");
2487
+ console.log(dryRun ? "升级预览(dry run):" : "升级计划:");
2488
+ console.log("");
2489
+ console.log(`目标目录:${plan.targetDir}`);
2490
+ console.log(`Blueprint:${plan.blueprint.__path}`);
2491
+ console.log(`策略:${plan.strategy}`);
2492
+ console.log(`工作区类型:${plan.workspaceType}`);
2493
+ console.log(`Kit:${plan.kit}`);
2494
+ console.log(`语言:${plan.language}`);
2495
+ console.log(`Pack:${plan.pack ? `${plan.pack.name || plan.pack.id} (${plan.pack.id})` : "(none)"}`);
2496
+ console.log(`正式事实源:${plan.blueprint.paths.formal_source}`);
2497
+ console.log(`当前工作区:${plan.blueprint.paths.business_work_area}`);
2498
+ console.log("");
2499
+
2500
+ if (dirs.length) {
2501
+ console.log("将创建目录:");
2502
+ dirs.forEach((action) => console.log(`- ${action.relativePath}`));
2503
+ console.log("");
2504
+ }
2505
+ if (creates.length) {
2506
+ console.log("将创建文件:");
2507
+ creates.slice(0, 60).forEach((action) => console.log(`- ${action.relativePath}`));
2508
+ if (creates.length > 60) console.log(`- ... 另有 ${creates.length - 60} 项`);
2509
+ console.log("");
2510
+ }
2511
+ if (overwrites.length) {
2512
+ console.log("将注入或更新文件:");
2513
+ overwrites.forEach((action) => console.log(`- ${action.relativePath}`));
2514
+ console.log("");
2515
+ }
2516
+ if (existingDirs.length) {
2517
+ console.log("会保留并复用已有目录:");
2518
+ existingDirs.forEach((action) => console.log(`- ${action.relativePath}`));
2519
+ console.log("");
2520
+ }
2521
+ if (plan.blueprint.preserve?.length) {
2522
+ console.log("明确保留不移动:");
2523
+ plan.blueprint.preserve.forEach((item) => console.log(`- ${item}`));
2524
+ console.log("");
2525
+ }
2526
+ }
2527
+
2528
+ function renderUpgradePlanJson(plan, dryRun) {
2529
+ return {
2530
+ schema: "starwork.upgrade.plan_result.v0.1",
2531
+ target: plan.targetDir,
2532
+ dry_run: Boolean(dryRun),
2533
+ ok: true,
2534
+ strategy: plan.strategy,
2535
+ workspace_type: plan.workspaceType,
2536
+ kit: plan.kit,
2537
+ language: plan.language,
2538
+ pack: plan.pack?.id || null,
2539
+ actions: plan.actions.map((action) => ({
2540
+ type: action.type,
2541
+ mode: action.mode,
2542
+ path: action.relativePath,
2543
+ status: action.mode === "exists" ? "exists" : "planned"
2544
+ })),
2545
+ blocked: [],
2546
+ warnings: []
2547
+ };
2548
+ }
2549
+
2550
+ function renderUpgradeExecutionJson(plan) {
2551
+ return {
2552
+ schema: "starwork.upgrade.execution_result.v0.1",
2553
+ target: plan.targetDir,
2554
+ ok: true,
2555
+ executed: plan.actions
2556
+ .filter((action) => action.mode !== "exists" && action.mode !== "skip")
2557
+ .map((action) => ({
2558
+ type: action.type,
2559
+ mode: action.mode,
2560
+ path: action.relativePath,
2561
+ status: "done"
2562
+ })),
2563
+ skipped: plan.actions
2564
+ .filter((action) => action.mode === "exists" || action.mode === "skip")
2565
+ .map((action) => ({
2566
+ type: action.type,
2567
+ mode: action.mode,
2568
+ path: action.relativePath,
2569
+ status: "skipped"
2570
+ })),
2571
+ next_check: `starwork doctor --target ${plan.targetDir}`
2572
+ };
2573
+ }
2574
+
2183
2575
  function walkFiles(dir) {
2184
2576
  const entries = fs.readdirSync(dir, { withFileTypes: true });
2185
2577
  const result = [];
@@ -2221,6 +2613,7 @@ function printHelp() {
2221
2613
  Usage:
2222
2614
  starwork init [options]
2223
2615
  starwork spawn [options]
2616
+ starwork upgrade [options]
2224
2617
  starwork doctor [options]
2225
2618
  starwork adapt [agent] [options]
2226
2619
  starwork pack install <pack> [options]
@@ -2282,6 +2675,22 @@ Options:
2282
2675
  `);
2283
2676
  }
2284
2677
 
2678
+ function printUpgradeHelp() {
2679
+ console.log(`StarWork Upgrade
2680
+
2681
+ Usage:
2682
+ starwork upgrade --target <path> --blueprint <upgrade-blueprint.json> --dry-run
2683
+ starwork upgrade --target <path> --blueprint <upgrade-blueprint.json> --yes
2684
+
2685
+ Options:
2686
+ --target <path>
2687
+ --blueprint <upgrade-blueprint.json>
2688
+ --dry-run
2689
+ --json
2690
+ --yes, -y
2691
+ `);
2692
+ }
2693
+
2285
2694
  function printAdaptHelp() {
2286
2695
  console.log(`StarWork Adapt
2287
2696
 
@@ -432,6 +432,169 @@ test("doctor reports legacy signals for a Chinese matter legacy template", () =>
432
432
  assert.doesNotMatch(result.stdout, /下一步/);
433
433
  });
434
434
 
435
+ test("upgrade blueprint dry-run does not write files", () => {
436
+ const dir = tempDir();
437
+ const blueprintDir = tempDir();
438
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Agent\n", "utf8");
439
+ fs.mkdirSync(path.join(dir, "资料库"), { recursive: true });
440
+ fs.mkdirSync(path.join(dir, "成稿"), { recursive: true });
441
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
442
+ schema: "starwork.upgrade_blueprint.v0.1",
443
+ generated_by: "starworkUpgrade",
444
+ source: {
445
+ doctor_schema: "starwork.doctor.result.v0.1",
446
+ diagnosis: "legacy-template",
447
+ core_fit: "medium"
448
+ },
449
+ base: {
450
+ workspace_type: "single-light",
451
+ kit: "local-starter",
452
+ language: "zh",
453
+ pack: "general"
454
+ },
455
+ strategy: "preserve-names",
456
+ paths: {
457
+ formal_source: "成稿/",
458
+ business_work_area: "资料库/"
459
+ },
460
+ core_role_mapping: [],
461
+ actions: [
462
+ { type: "ensure_dir", path: ".starwork/" },
463
+ { type: "write_workspace_state" },
464
+ { type: "copy_kit_missing_files" }
465
+ ]
466
+ }, null, 2)}\n`, "utf8");
467
+
468
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--dry-run"]);
469
+
470
+ assert.equal(result.status, 0);
471
+ assert.match(result.stdout, /升级预览/);
472
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "workspace.json")), false);
473
+ });
474
+
475
+ test("upgrade applies a blueprint and keeps existing files", () => {
476
+ const dir = tempDir();
477
+ const blueprintDir = tempDir();
478
+ fs.mkdirSync(path.join(blueprintDir, "rules"), { recursive: true });
479
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Agent\n\nKeep me.\n", "utf8");
480
+ fs.mkdirSync(path.join(dir, "资料库"), { recursive: true });
481
+ fs.mkdirSync(path.join(dir, "成稿"), { recursive: true });
482
+ fs.mkdirSync(path.join(dir, "事项"), { recursive: true });
483
+ fs.writeFileSync(path.join(blueprintDir, "rules", "core-boundaries.md"), "正式成果:{{paths.formal_source}}\n当前工作:{{paths.business_work_area}}\n", "utf8");
484
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
485
+ schema: "starwork.upgrade_blueprint.v0.1",
486
+ target: ".",
487
+ generated_by: "starworkUpgrade",
488
+ source: {
489
+ doctor_schema: "starwork.doctor.result.v0.1",
490
+ diagnosis: "legacy-template",
491
+ core_fit: "medium"
492
+ },
493
+ base: {
494
+ workspace_type: "single-matter",
495
+ kit: "local-matter",
496
+ language: "zh",
497
+ pack: "general"
498
+ },
499
+ strategy: "preserve-names",
500
+ paths: {
501
+ formal_source: "成稿/",
502
+ business_work_area: "事项/"
503
+ },
504
+ core_role_mapping: [
505
+ { role: "references", path: "资料库/", confidence: "high", reason: "用户确认" },
506
+ { role: "formal_source", path: "成稿/", confidence: "high", reason: "用户确认" }
507
+ ],
508
+ actions: [
509
+ { type: "ensure_dir", path: ".starwork/" },
510
+ { type: "write_workspace_state" },
511
+ { type: "copy_kit_missing_files" },
512
+ { type: "inject_agent_rules", target: "AGENTS.md", from: "rules/core-boundaries.md", slot: "upgrade.core_boundaries" }
513
+ ],
514
+ preserve: ["资料库/", "成稿/", "事项/"],
515
+ verification: {
516
+ run_doctor_after: true,
517
+ expected_workspace_type: "single-matter"
518
+ }
519
+ }, null, 2)}\n`, "utf8");
520
+
521
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--yes"]);
522
+ const state = readJson(path.join(dir, ".starwork", "workspace.json"));
523
+ const agents = fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8");
524
+ const doctor = runDoctor(["--target", dir, "--json"]);
525
+ const report = JSON.parse(doctor.stdout);
526
+
527
+ assert.equal(result.status, 0);
528
+ assert.equal(state.workspace_type, "single-matter");
529
+ assert.equal(state.kit, "local-matter");
530
+ assert.equal(state.paths.formal_source, "成稿/");
531
+ assert.equal(state.paths.business_work_area, "事项/");
532
+ assert.equal(state.upgrade.type, "upgrade_blueprint");
533
+ assert.match(agents, /Keep me/);
534
+ assert.match(agents, /StarWork Upgrade: upgrade\.core_boundaries/);
535
+ assert.match(agents, /正式成果:成稿\//);
536
+ assert.equal(fs.existsSync(path.join(dir, "_系统", "上下文", "项目状态.md")), true);
537
+ assert.equal(doctor.status, 0);
538
+ assert.equal(report.ok, true);
539
+ });
540
+
541
+ test("upgrade refuses existing StarWork workspaces", () => {
542
+ const dir = tempDir();
543
+ const blueprintDir = tempDir();
544
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
545
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
546
+ schema: "starwork.upgrade_blueprint.v0.1",
547
+ base: {
548
+ workspace_type: "single-light",
549
+ kit: "local-starter",
550
+ language: "zh",
551
+ pack: "general"
552
+ },
553
+ strategy: "preserve-names",
554
+ paths: {
555
+ formal_source: "输出/确认成果/",
556
+ business_work_area: "输出/草稿/"
557
+ },
558
+ actions: [
559
+ { type: "ensure_dir", path: ".starwork/" },
560
+ { type: "write_workspace_state" }
561
+ ]
562
+ }, null, 2)}\n`, "utf8");
563
+
564
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--yes"]);
565
+
566
+ assert.equal(result.status, 1);
567
+ assert.match(result.stderr, /已经是 StarWork 工作台/);
568
+ });
569
+
570
+ test("upgrade rejects unsafe blueprint paths", () => {
571
+ const dir = tempDir();
572
+ const blueprintDir = tempDir();
573
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
574
+ schema: "starwork.upgrade_blueprint.v0.1",
575
+ base: {
576
+ workspace_type: "single-light",
577
+ kit: "local-starter",
578
+ language: "zh",
579
+ pack: "general"
580
+ },
581
+ strategy: "preserve-names",
582
+ paths: {
583
+ formal_source: "../escape/",
584
+ business_work_area: "参考资料/"
585
+ },
586
+ actions: [
587
+ { type: "ensure_dir", path: ".starwork/" },
588
+ { type: "write_workspace_state" }
589
+ ]
590
+ }, null, 2)}\n`, "utf8");
591
+
592
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--yes"]);
593
+
594
+ assert.equal(result.status, 1);
595
+ assert.match(result.stderr, /不能跳出工作区/);
596
+ });
597
+
435
598
  test("adapt creates a Claude adapter and records it in workspace state", () => {
436
599
  const dir = tempDir();
437
600
  runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);