@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 +8 -1
- package/cli/src/cli.js +410 -1
- package/cli/test/init.test.js +163 -0
- package/cli/upgrade-spec.md +487 -0
- package/package.json +1 -1
- package/skills/README.md +1 -0
- package/skills/starworkDoctor/SKILL.md +1 -1
- package/skills/starworkDoctor-spec.md +3 -2
- package/skills/starworkUpgrade/SKILL.md +158 -0
- package/skills/starworkUpgrade/agents/openai.yaml +7 -0
- package/skills/starworkUpgrade-spec.md +333 -0
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
|
|
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
|
|
package/cli/test/init.test.js
CHANGED
|
@@ -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"]);
|