openfeel 1.0.8 → 1.1.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 (139) hide show
  1. package/README.en.md +11 -11
  2. package/README.md +2 -1
  3. package/README.zh-CN.md +13 -13
  4. package/bin/openfeel.js +2 -2
  5. package/dist/cli/index.d.ts +6 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +26 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/archive.d.ts +1 -1
  10. package/dist/commands/flow.d.ts +1 -1
  11. package/dist/commands/flow.d.ts.map +1 -1
  12. package/dist/commands/flow.js +20 -8
  13. package/dist/commands/flow.js.map +1 -1
  14. package/dist/commands/migrate.d.ts +10 -0
  15. package/dist/commands/migrate.d.ts.map +1 -0
  16. package/dist/commands/migrate.js +97 -0
  17. package/dist/commands/migrate.js.map +1 -0
  18. package/dist/commands/model.d.ts +10 -0
  19. package/dist/commands/model.d.ts.map +1 -0
  20. package/dist/commands/model.js +135 -0
  21. package/dist/commands/model.js.map +1 -0
  22. package/dist/commands/plan.js +2 -2
  23. package/dist/commands/plan.js.map +1 -1
  24. package/dist/commands/stage.d.ts.map +1 -1
  25. package/dist/commands/stage.js +66 -47
  26. package/dist/commands/stage.js.map +1 -1
  27. package/dist/commands/update.d.ts.map +1 -1
  28. package/dist/commands/update.js +12 -0
  29. package/dist/commands/update.js.map +1 -1
  30. package/dist/commands/view.d.ts +1 -1
  31. package/dist/commands/view.d.ts.map +1 -1
  32. package/dist/commands/view.js +2 -1
  33. package/dist/commands/view.js.map +1 -1
  34. package/dist/core/archive/merge.d.ts.map +1 -1
  35. package/dist/core/archive/merge.js +5 -4
  36. package/dist/core/archive/merge.js.map +1 -1
  37. package/dist/core/artifact-graph/resolver.js +2 -2
  38. package/dist/core/artifact-graph/resolver.js.map +1 -1
  39. package/dist/core/config.d.ts +8 -8
  40. package/dist/core/config.d.ts.map +1 -1
  41. package/dist/core/config.js +22 -17
  42. package/dist/core/config.js.map +1 -1
  43. package/dist/core/flow-manager.d.ts +27 -6
  44. package/dist/core/flow-manager.d.ts.map +1 -1
  45. package/dist/core/flow-manager.js +152 -103
  46. package/dist/core/flow-manager.js.map +1 -1
  47. package/dist/core/fs/atomic-write.d.ts +31 -0
  48. package/dist/core/fs/atomic-write.d.ts.map +1 -0
  49. package/dist/core/fs/atomic-write.js +80 -0
  50. package/dist/core/fs/atomic-write.js.map +1 -0
  51. package/dist/core/fs/file-lock.d.ts +34 -0
  52. package/dist/core/fs/file-lock.d.ts.map +1 -0
  53. package/dist/core/fs/file-lock.js +142 -0
  54. package/dist/core/fs/file-lock.js.map +1 -0
  55. package/dist/core/fs/sequence.d.ts +42 -0
  56. package/dist/core/fs/sequence.d.ts.map +1 -0
  57. package/dist/core/fs/sequence.js +69 -0
  58. package/dist/core/fs/sequence.js.map +1 -0
  59. package/dist/core/global-paths.d.ts +25 -0
  60. package/dist/core/global-paths.d.ts.map +1 -0
  61. package/dist/core/global-paths.js +57 -0
  62. package/dist/core/global-paths.js.map +1 -0
  63. package/dist/core/i18n-data/en.d.ts +2 -0
  64. package/dist/core/i18n-data/en.d.ts.map +1 -1
  65. package/dist/core/i18n-data/en.js +69 -1
  66. package/dist/core/i18n-data/en.js.map +1 -1
  67. package/dist/core/i18n-data/zh-CN.d.ts +2 -0
  68. package/dist/core/i18n-data/zh-CN.d.ts.map +1 -1
  69. package/dist/core/i18n-data/zh-CN.js +69 -1
  70. package/dist/core/i18n-data/zh-CN.js.map +1 -1
  71. package/dist/core/i18n.js +4 -4
  72. package/dist/core/i18n.js.map +1 -1
  73. package/dist/core/init.d.ts +3 -3
  74. package/dist/core/init.d.ts.map +1 -1
  75. package/dist/core/init.js +58 -42
  76. package/dist/core/init.js.map +1 -1
  77. package/dist/core/managed-region.d.ts +54 -0
  78. package/dist/core/managed-region.d.ts.map +1 -0
  79. package/dist/core/managed-region.js +171 -0
  80. package/dist/core/managed-region.js.map +1 -0
  81. package/dist/core/metrics.d.ts.map +1 -1
  82. package/dist/core/metrics.js +6 -3
  83. package/dist/core/metrics.js.map +1 -1
  84. package/dist/core/migrate.d.ts +103 -0
  85. package/dist/core/migrate.d.ts.map +1 -0
  86. package/dist/core/migrate.js +533 -0
  87. package/dist/core/migrate.js.map +1 -0
  88. package/dist/core/model-config.d.ts +62 -0
  89. package/dist/core/model-config.d.ts.map +1 -0
  90. package/dist/core/model-config.js +336 -0
  91. package/dist/core/model-config.js.map +1 -0
  92. package/dist/core/opencode-config.d.ts +17 -0
  93. package/dist/core/opencode-config.d.ts.map +1 -0
  94. package/dist/core/opencode-config.js +123 -0
  95. package/dist/core/opencode-config.js.map +1 -0
  96. package/dist/core/plan/path.d.ts +64 -0
  97. package/dist/core/plan/path.d.ts.map +1 -0
  98. package/dist/core/plan/path.js +150 -0
  99. package/dist/core/plan/path.js.map +1 -0
  100. package/dist/core/plan/roadmap.d.ts.map +1 -1
  101. package/dist/core/plan/roadmap.js +3 -2
  102. package/dist/core/plan/roadmap.js.map +1 -1
  103. package/dist/core/plan/scheme.d.ts +4 -3
  104. package/dist/core/plan/scheme.d.ts.map +1 -1
  105. package/dist/core/plan/scheme.js +113 -110
  106. package/dist/core/plan/scheme.js.map +1 -1
  107. package/dist/core/plan/stage.d.ts +2 -2
  108. package/dist/core/plan/stage.d.ts.map +1 -1
  109. package/dist/core/plan/stage.js +47 -32
  110. package/dist/core/plan/stage.js.map +1 -1
  111. package/dist/core/public-logger.d.ts +3 -3
  112. package/dist/core/public-logger.d.ts.map +1 -1
  113. package/dist/core/public-logger.js +36 -40
  114. package/dist/core/public-logger.js.map +1 -1
  115. package/dist/core/template-loader.d.ts +1 -1
  116. package/dist/core/template-loader.d.ts.map +1 -1
  117. package/dist/core/template-loader.js +2849 -2826
  118. package/dist/core/template-loader.js.map +1 -1
  119. package/dist/core/update-infos.d.ts +32 -0
  120. package/dist/core/update-infos.d.ts.map +1 -0
  121. package/dist/core/update-infos.js +153 -0
  122. package/dist/core/update-infos.js.map +1 -0
  123. package/dist/core/update-state.d.ts +18 -0
  124. package/dist/core/update-state.d.ts.map +1 -1
  125. package/dist/core/update-state.js +66 -3
  126. package/dist/core/update-state.js.map +1 -1
  127. package/dist/core/update.d.ts +12 -0
  128. package/dist/core/update.d.ts.map +1 -1
  129. package/dist/core/update.js +340 -485
  130. package/dist/core/update.js.map +1 -1
  131. package/dist/core/view/entry.js +4 -4
  132. package/dist/core/view/entry.js.map +1 -1
  133. package/dist/core/workspace/identity.d.ts.map +1 -1
  134. package/dist/core/workspace/identity.js +13 -10
  135. package/dist/core/workspace/identity.js.map +1 -1
  136. package/dist/core/workspace/knowledge.d.ts.map +1 -1
  137. package/dist/core/workspace/knowledge.js +24 -14
  138. package/dist/core/workspace/knowledge.js.map +1 -1
  139. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ import { type UpdateState } from './update-state.js';
2
+ /** legacy 检测报告(M3 五条判据) */
3
+ export interface LegacyReport {
4
+ projectOpendirAgents: boolean;
5
+ projectOpendirSkills: boolean;
6
+ projectOpendirInstructions: boolean;
7
+ legacyJsoncSkillsMapping: boolean;
8
+ legacyJsoncInstructions: boolean;
9
+ mixedUpdateState: boolean;
10
+ isLegacy: boolean;
11
+ }
12
+ /** manifest 单条记录 */
13
+ export interface ManifestEntry {
14
+ op: 'delete' | 'modify';
15
+ source: string;
16
+ backupPath: string;
17
+ hash: string;
18
+ note?: string;
19
+ }
20
+ /** manifest 元数据 */
21
+ export interface Manifest {
22
+ version: '1.0';
23
+ openfeel_version: string;
24
+ createdAt: string;
25
+ projectPath: string;
26
+ legacy: LegacyReport;
27
+ entries: ManifestEntry[];
28
+ /** REV-1302:本次 migrate 新增/写入的全局 state key(rollback 仅删这些,不触碰历史全局条目) */
29
+ globalStateKeys: string[];
30
+ }
31
+ /** 迁移结果 */
32
+ export interface MigrateResult {
33
+ legacy: LegacyReport;
34
+ backupDir: string | null;
35
+ deployed: string[];
36
+ stateSplit: SplitStateResult;
37
+ cleaned: string[];
38
+ keptCustom: string[];
39
+ jsoncCleaned: boolean;
40
+ assigneeReport: {
41
+ oldName: string;
42
+ newName: string;
43
+ count: number;
44
+ }[];
45
+ remapped: boolean;
46
+ }
47
+ /** state 拆分结果 */
48
+ export interface SplitStateResult {
49
+ movedToGlobal: string[];
50
+ keptInProject: string[];
51
+ unmapped: string[];
52
+ }
53
+ /** legacy 判据 5 条(M3) */
54
+ export declare function detectLegacy(projectPath: string): LegacyReport;
55
+ /**
56
+ * 列出 legacy 项目文件,按「框架同源 / 项目自定义」分类。
57
+ * framework = 文件名归一化后命中框架清单(9 agent + 14 skill + core.md);
58
+ * custom = 其余(项目自定义 agent/skill),保留原位不迁移不删除。
59
+ * 旧名(planner.md 等)经 normalizeAgentName 归一化后再比对(P5 兼容)。
60
+ */
61
+ export declare function listLegacyFiles(projectPath: string, lang?: 'zh-CN' | 'en'): {
62
+ framework: string[];
63
+ custom: string[];
64
+ };
65
+ /** 备份将被删除/改写的项目文件到 .openfeel/backup/{ts}/ 并生成 manifest.json */
66
+ export declare function backupLegacy(projectPath: string, report: LegacyReport, opts?: {
67
+ remapAssignee?: boolean;
68
+ }): {
69
+ backupDir: string;
70
+ manifest: Manifest;
71
+ };
72
+ /**
73
+ * 旧项目 state 框架条目 → 全局 state 重键;项目条目保留。
74
+ * 重键规则:旧 key(.opencode/agents/planner.md)→ normalizeAgentName(文件名) 得新名
75
+ * → 拼全局绝对路径(agentsDir/{新名}.md / skillsDir/{新名}/SKILL.md / core.md)。
76
+ * 无法映射的条目保留项目 state 并记 unmapped 待人工处理。
77
+ * @param globalStateIn 可选:调用方已加载的全局 state(REV-1402 复用避免重复 load)
78
+ */
79
+ export declare function splitUpdateState(projectPath: string, lang?: 'zh-CN' | 'en', globalStateIn?: UpdateState): SplitStateResult;
80
+ /** 扫描 flow.json 旧 assignee,报告旧名→新名映射;dryRun 仅报告不写 */
81
+ export declare function remapAssignees(projectPath: string, dryRun: boolean): {
82
+ changes: string[];
83
+ count: number;
84
+ };
85
+ /** 保留最近 keep 次备份,删除更早的备份目录 */
86
+ export declare function cleanOldBackups(projectPath: string, keep?: number): string[];
87
+ /** 迁移主流程:检测→备份→全局部署→state 拆分→清理→assignee 报告 */
88
+ export declare function migrateProject(projectPath: string, opts?: {
89
+ dryRun?: boolean;
90
+ remapAssignee?: boolean;
91
+ lang?: 'zh-CN' | 'en';
92
+ }): MigrateResult;
93
+ /** 回滚最近一次迁移:按 manifest 逆向恢复项目文件 + 还原全局 state 新增条目 */
94
+ export declare function rollbackMigration(projectPath: string, backupTs?: string): {
95
+ restored: string[];
96
+ stateRestored: boolean;
97
+ };
98
+ /** 回滚预览(REV-1309):读取最新 manifest,返回 entries 列表(source + op 类型),不写盘 */
99
+ export declare function previewRollback(projectPath: string, backupTs?: string): {
100
+ op: 'delete' | 'modify';
101
+ source: string;
102
+ }[];
103
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/core/migrate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,533 @@
1
+ /**
2
+ * openfeel migrate 命令核心模块(stage-39 / P6)
3
+ * 检测 legacy 布局 → 备份(manifest)→ 全局部署 → update_state 拆分/重键
4
+ * → 清理 legacy 项目文件(逐项比对框架清单,保留项目自定义)→ flow.json assignee 报告(可选改写)。
5
+ * 支持 --dry-run 预览与 rollback 回滚。
6
+ */
7
+ import { existsSync, readFileSync, readdirSync, mkdirSync, rmSync, copyFileSync } from 'node:fs';
8
+ import { resolve, join, dirname, relative } from 'node:path';
9
+ import { createHash } from 'node:crypto';
10
+ import { atomicWriteFileSync } from './fs/atomic-write.js';
11
+ import { withFileLock, globalLockPath } from './fs/file-lock.js';
12
+ import { loadUpdateState, saveUpdateState, loadGlobalUpdateState, saveGlobalUpdateState, updateFileHash, getOpenfeelVersion, isLegacyFrameworkKey, } from './update-state.js';
13
+ import { getGlobalAgentsDir, getGlobalSkillsDir, getGlobalCoreMdPath, getGlobalOpencodeJsoncPath, } from './global-paths.js';
14
+ import { loadAgentTemplate, listAgentIds, loadTemplate } from './template-loader.js';
15
+ import { mergeGlobalOpencodeJsonc, parseJsonc } from './opencode-config.js';
16
+ import { normalizeAgentName } from './flow-manager.js';
17
+ import { deployGlobalAsset, SKILL_DEFINITIONS } from './update.js';
18
+ // ─── 常量 ─────────────────────────────────────────────────────────
19
+ /** 备份目录相对项目根的前缀 */
20
+ const BACKUP_DIR = '.openfeel/backup';
21
+ // ─── 检测 ─────────────────────────────────────────────────────────
22
+ // 注:isLegacyFrameworkKey(旧框架 key 识别)由 op-002 在 update-state.ts 提供;
23
+ // op-004 回归统一 import(REV-1304),消除 op-001 阶段的内联 fallback 双实现。
24
+ /** legacy 判据 5 条(M3) */
25
+ export function detectLegacy(projectPath) {
26
+ const agentsDir = resolve(projectPath, '.opencode', 'agents');
27
+ const skillsDir = resolve(projectPath, '.opencode', 'skills');
28
+ const instrCore = resolve(projectPath, '.opencode', 'instructions', 'core.md');
29
+ const jsoncPath = resolve(projectPath, 'opencode.jsonc');
30
+ // 框架同源集合(新名):判据 ①/② 仅认「框架同源」旧布局资产(REV-007);
31
+ // 项目自定义 agent/skill 迁移后仍保留原位,不应使其恒为 legacy(否则幂等 REV-1204① 不成立)。
32
+ const agentNames = new Set(listAgentIds('zh-CN'));
33
+ const skillNames = new Set(Object.keys(SKILL_DEFINITIONS));
34
+ let projectOpendirAgents = false;
35
+ if (existsSync(agentsDir)) {
36
+ try {
37
+ projectOpendirAgents = readdirSync(agentsDir)
38
+ .filter((n) => n.endsWith('.md'))
39
+ .some((n) => agentNames.has(normalizeAgentName(n.replace(/\.md$/, ''))));
40
+ }
41
+ catch {
42
+ projectOpendirAgents = false;
43
+ }
44
+ }
45
+ let projectOpendirSkills = false;
46
+ if (existsSync(skillsDir)) {
47
+ try {
48
+ projectOpendirSkills = readdirSync(skillsDir).some((d) => {
49
+ if (!existsSync(join(skillsDir, d, 'SKILL.md')))
50
+ return false;
51
+ const mapped = remapSkillName(d);
52
+ return mapped !== null && skillNames.has(mapped);
53
+ });
54
+ }
55
+ catch {
56
+ projectOpendirSkills = false;
57
+ }
58
+ }
59
+ const projectOpendirInstructions = existsSync(instrCore);
60
+ let legacyJsoncSkillsMapping = false;
61
+ let legacyJsoncInstructions = false;
62
+ if (existsSync(jsoncPath)) {
63
+ try {
64
+ const obj = parseJsonc(readFileSync(jsoncPath, 'utf-8'));
65
+ // 非法 skills 映射:skills 为对象且不含 paths/urls 结构(旧 {name:path} 形式)
66
+ const skills = obj.skills;
67
+ legacyJsoncSkillsMapping = typeof skills === 'object' && skills !== null
68
+ && !Array.isArray(skills) && !('paths' in skills) && !('urls' in skills);
69
+ legacyJsoncInstructions = 'instructions' in obj;
70
+ }
71
+ catch { /* parse 失败不视为 legacy jsonc(交由 detect 后续按存在性处理) */ }
72
+ }
73
+ const state = loadUpdateState(projectPath);
74
+ const mixedUpdateState = state !== null && Object.keys(state.files).some(isLegacyFrameworkKey);
75
+ const isLegacy = projectOpendirAgents || projectOpendirSkills || projectOpendirInstructions
76
+ || legacyJsoncSkillsMapping || legacyJsoncInstructions || mixedUpdateState;
77
+ return {
78
+ projectOpendirAgents, projectOpendirSkills, projectOpendirInstructions,
79
+ legacyJsoncSkillsMapping, legacyJsoncInstructions, mixedUpdateState, isLegacy,
80
+ };
81
+ }
82
+ // ─── 框架清单比对(REV-007) ─────────────────────────────────────
83
+ /**
84
+ * 列出 legacy 项目文件,按「框架同源 / 项目自定义」分类。
85
+ * framework = 文件名归一化后命中框架清单(9 agent + 14 skill + core.md);
86
+ * custom = 其余(项目自定义 agent/skill),保留原位不迁移不删除。
87
+ * 旧名(planner.md 等)经 normalizeAgentName 归一化后再比对(P5 兼容)。
88
+ */
89
+ export function listLegacyFiles(projectPath, lang = 'zh-CN') {
90
+ const framework = [];
91
+ const custom = [];
92
+ // 框架 agent 名集合(新名)
93
+ const agentNames = new Set(listAgentIds(lang));
94
+ // 框架 skill 名集合(新名,含 openfeel- 前缀)
95
+ const skillNames = new Set(Object.keys(SKILL_DEFINITIONS));
96
+ const agentsDir = resolve(projectPath, '.opencode', 'agents');
97
+ if (existsSync(agentsDir)) {
98
+ for (const f of readdirSync(agentsDir)) {
99
+ if (!f.endsWith('.md'))
100
+ continue;
101
+ const normalized = normalizeAgentName(f.replace(/\.md$/, ''));
102
+ const abs = join(agentsDir, f);
103
+ (agentNames.has(normalized) ? framework : custom).push(abs);
104
+ }
105
+ }
106
+ const skillsDir = resolve(projectPath, '.opencode', 'skills');
107
+ if (existsSync(skillsDir)) {
108
+ for (const d of readdirSync(skillsDir)) {
109
+ const skillMd = join(skillsDir, d, 'SKILL.md');
110
+ if (!existsSync(skillMd))
111
+ continue;
112
+ // 旧无前缀名经 remapSkillName 归一化后与新名集合比对(REV-1303),避免旧名 skill 误归 custom
113
+ const mapped = remapSkillName(d);
114
+ (mapped !== null && skillNames.has(mapped) ? framework : custom).push(skillMd);
115
+ }
116
+ }
117
+ const instrCore = resolve(projectPath, '.opencode', 'instructions', 'core.md');
118
+ if (existsSync(instrCore))
119
+ framework.push(instrCore);
120
+ return { framework, custom };
121
+ }
122
+ // ─── 备份(M2) ─────────────────────────────────────────────────
123
+ function sha256(content) {
124
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
125
+ }
126
+ /** 备份将被删除/改写的项目文件到 .openfeel/backup/{ts}/ 并生成 manifest.json */
127
+ export function backupLegacy(projectPath, report, opts) {
128
+ const ts = new Date().toISOString().replace(/[-:T]/g, '').replace(/\..*$/, '').replace(/Z$/, '');
129
+ const backupDir = resolve(projectPath, BACKUP_DIR, ts);
130
+ mkdirSync(backupDir, { recursive: true });
131
+ const { framework } = listLegacyFiles(projectPath);
132
+ const entries = [];
133
+ // 1. legacy 框架文件(删除型)
134
+ for (const abs of framework) {
135
+ const rel = relative(projectPath, abs);
136
+ const backupPath = join(backupDir, rel);
137
+ mkdirSync(dirname(backupPath), { recursive: true });
138
+ copyFileSync(abs, backupPath);
139
+ entries.push({ op: 'delete', source: rel, backupPath: rel, hash: sha256(readFileSync(abs, 'utf-8')) });
140
+ }
141
+ // 2. 项目 opencode.jsonc(若含非法字段,改写型)
142
+ const jsoncPath = resolve(projectPath, 'opencode.jsonc');
143
+ if (existsSync(jsoncPath) && (report.legacyJsoncSkillsMapping || report.legacyJsoncInstructions)) {
144
+ const rel = 'opencode.jsonc';
145
+ copyFileSync(jsoncPath, join(backupDir, rel));
146
+ entries.push({ op: 'modify', source: rel, backupPath: rel, hash: sha256(readFileSync(jsoncPath, 'utf-8')), note: 'remove legacy skills/instructions' });
147
+ }
148
+ // 3. 项目 update_state.json(若含旧框架 key,改写型)
149
+ const stateRel = '.openfeel/update_state.json';
150
+ const statePath = resolve(projectPath, stateRel);
151
+ if (existsSync(statePath) && report.mixedUpdateState) {
152
+ mkdirSync(join(backupDir, '.openfeel'), { recursive: true });
153
+ copyFileSync(statePath, join(backupDir, stateRel));
154
+ entries.push({ op: 'modify', source: stateRel, backupPath: stateRel, hash: sha256(readFileSync(statePath, 'utf-8')), note: 'split update_state' });
155
+ }
156
+ // 4. flow.json(仅 --remap-assignee 时,改写型;REV-1201)
157
+ if (opts?.remapAssignee) {
158
+ const flowRel = '.openfeel/flow.json';
159
+ const flowPath = resolve(projectPath, flowRel);
160
+ if (existsSync(flowPath)) {
161
+ mkdirSync(join(backupDir, '.openfeel'), { recursive: true });
162
+ copyFileSync(flowPath, join(backupDir, flowRel));
163
+ entries.push({ op: 'modify', source: flowRel, backupPath: flowRel, hash: sha256(readFileSync(flowPath, 'utf-8')), note: 'remap assignee' });
164
+ }
165
+ }
166
+ const manifest = {
167
+ version: '1.0',
168
+ openfeel_version: getOpenfeelVersion(),
169
+ createdAt: new Date().toISOString(),
170
+ projectPath,
171
+ legacy: report,
172
+ entries,
173
+ globalStateKeys: [], // REV-1302:备份时先置空,migrateProject 全局部署/拆分后回填
174
+ };
175
+ atomicWriteFileSync(join(backupDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
176
+ return { backupDir, manifest };
177
+ }
178
+ // ─── state 拆分重键(D39-2) ────────────────────────────────────
179
+ /**
180
+ * 旧项目 state 框架条目 → 全局 state 重键;项目条目保留。
181
+ * 重键规则:旧 key(.opencode/agents/planner.md)→ normalizeAgentName(文件名) 得新名
182
+ * → 拼全局绝对路径(agentsDir/{新名}.md / skillsDir/{新名}/SKILL.md / core.md)。
183
+ * 无法映射的条目保留项目 state 并记 unmapped 待人工处理。
184
+ * @param globalStateIn 可选:调用方已加载的全局 state(REV-1402 复用避免重复 load)
185
+ */
186
+ export function splitUpdateState(projectPath, lang = 'zh-CN', globalStateIn) {
187
+ const state = loadUpdateState(projectPath);
188
+ if (!state)
189
+ return { movedToGlobal: [], keptInProject: [], unmapped: [] };
190
+ // REV-1402:优先复用调用方已加载的全局 state(migrateProject 已 load 一次),
191
+ // 避免重复 IO;未传入时回退内部加载以保持独立可用性。
192
+ const globalState = globalStateIn
193
+ ?? loadGlobalUpdateState()
194
+ ?? { version: '1.0', last_update: new Date().toISOString(), openfeel_version: getOpenfeelVersion(), files: {} };
195
+ const movedToGlobal = [];
196
+ const keptInProject = [];
197
+ const unmapped = [];
198
+ for (const [key, fileState] of Object.entries(state.files)) {
199
+ if (!isLegacyFrameworkKey(key)) {
200
+ keptInProject.push(key);
201
+ continue;
202
+ }
203
+ const mapped = remapLegacyKey(key, lang);
204
+ if (mapped) {
205
+ // 移入全局 state:hash 沿用(旧文件内容 hash 与新全局内容不同时由后续 update 重新计算;
206
+ // 此处仅保留记录,避免「无记录」触发全量追加,REV-911 安全行为)
207
+ globalState.files[mapped] = fileState;
208
+ delete state.files[key];
209
+ movedToGlobal.push(mapped);
210
+ }
211
+ else {
212
+ unmapped.push(key); // 无法映射 → 保留项目 state 待人工处理
213
+ keptInProject.push(key);
214
+ }
215
+ }
216
+ saveUpdateState(projectPath, state);
217
+ saveGlobalUpdateState(globalState);
218
+ return { movedToGlobal, keptInProject, unmapped };
219
+ }
220
+ /** skill 旧名(无前缀)→ 新名(openfeel- 前缀)映射(REV-1303,全 14 skill 无例外) */
221
+ const LEGACY_SKILL_NAME_MAP = {
222
+ 'agent-model-check': 'openfeel-agent-model-check',
223
+ 'bug-acceptance': 'openfeel-bug-acceptance',
224
+ 'check-kb': 'openfeel-check-kb',
225
+ 'get-bugs': 'openfeel-get-bugs',
226
+ 'get-stage-status': 'openfeel-get-stage-status',
227
+ health: 'openfeel-health',
228
+ 'model-check': 'openfeel-model-check',
229
+ 'model-config': 'openfeel-model-config',
230
+ recover: 'openfeel-recover',
231
+ roadmap: 'openfeel-roadmap',
232
+ 'search-kb': 'openfeel-search-kb',
233
+ 'sync-status': 'openfeel-sync-status',
234
+ 'update-stage-status': 'openfeel-update-stage-status',
235
+ wizard: 'openfeel-wizard',
236
+ };
237
+ /** skill 旧名归一化(REV-1303):旧无前缀名 → openfeel- 新名;已是新名幂等返回;未知返回 null */
238
+ function remapSkillName(name) {
239
+ if (name.startsWith('openfeel-'))
240
+ return name; // 幂等:不二次前缀化
241
+ return LEGACY_SKILL_NAME_MAP[name] ?? null;
242
+ }
243
+ /** 旧框架 key → 全局绝对路径 key;无法映射返回 null */
244
+ function remapLegacyKey(key, lang) {
245
+ const norm = key.replace(/\\/g, '/');
246
+ // .opencode/agents/{name}.md → agentsDir/{newName}.md
247
+ if (norm.startsWith('.opencode/agents/') && norm.endsWith('.md')) {
248
+ const name = norm.slice('.opencode/agents/'.length, -3);
249
+ const normalized = normalizeAgentName(name);
250
+ if (!normalized || normalized === name)
251
+ return null; // 无法归一化(非 agent 旧名)
252
+ return join(getGlobalAgentsDir(), `${normalized}.md`);
253
+ }
254
+ // .opencode/skills/{name}/SKILL.md → skillsDir/{newName}/SKILL.md(REV-1303:旧名补 openfeel- 前缀)
255
+ if (norm.startsWith('.opencode/skills/') && norm.endsWith('/SKILL.md')) {
256
+ const name = norm.slice('.opencode/skills/'.length, -'/SKILL.md'.length);
257
+ const mapped = remapSkillName(name);
258
+ if (!mapped)
259
+ return null; // 非 14 skill 旧名(项目自定义 skill)
260
+ return join(getGlobalSkillsDir(), mapped, 'SKILL.md');
261
+ }
262
+ // .opencode/instructions/core.md → 全局 core.md
263
+ if (norm === '.opencode/instructions/core.md') {
264
+ return getGlobalCoreMdPath();
265
+ }
266
+ return null;
267
+ }
268
+ // ─── 清理项目 opencode.jsonc ────────────────────────────────────
269
+ /** 移除项目 opencode.jsonc 的非法 skills 映射与 instructions 字段,保留用户自定义字段 */
270
+ function cleanProjectJsonc(projectPath) {
271
+ const jsoncPath = resolve(projectPath, 'opencode.jsonc');
272
+ if (!existsSync(jsoncPath))
273
+ return false;
274
+ const obj = parseJsonc(readFileSync(jsoncPath, 'utf-8'));
275
+ let changed = false;
276
+ // REV-1305:仅删非法 skills 映射(旧 {name:path} 对象形式);合法 {paths,urls} 结构保留,避免误删
277
+ if ('skills' in obj) {
278
+ const skills = obj.skills;
279
+ const illegalSkills = typeof skills === 'object' && skills !== null
280
+ && !Array.isArray(skills) && !('paths' in skills) && !('urls' in skills);
281
+ if (illegalSkills) {
282
+ delete obj.skills;
283
+ changed = true;
284
+ }
285
+ }
286
+ if ('instructions' in obj) {
287
+ delete obj.instructions;
288
+ changed = true;
289
+ }
290
+ if (changed) {
291
+ atomicWriteFileSync(jsoncPath, JSON.stringify(obj, null, 2) + '\n');
292
+ }
293
+ return changed;
294
+ }
295
+ // ─── assignee 报告 / 改写(M5) ─────────────────────────────────
296
+ /** 扫描 flow.json 旧 assignee,报告旧名→新名映射;dryRun 仅报告不写 */
297
+ export function remapAssignees(projectPath, dryRun) {
298
+ const flowPath = resolve(projectPath, '.openfeel', 'flow.json');
299
+ if (!existsSync(flowPath))
300
+ return { changes: [], count: 0 };
301
+ const data = JSON.parse(readFileSync(flowPath, 'utf-8'));
302
+ const changes = [];
303
+ let count = 0;
304
+ // REV-1301:flow.json 的 stages 为 Record<string, StageData>、stage.ops 为 Record<string, Op>(对象,非数组),
305
+ // 须用 Object.values 遍历,for...of 对象会抛 TypeError。stage 标识用 name 字段(StageData 无 id)。
306
+ for (const stage of Object.values(data.stages ?? {})) {
307
+ for (const op of Object.values(stage.ops ?? {})) {
308
+ if (op.assignee && normalizeAgentName(op.assignee) !== op.assignee) {
309
+ const oldName = op.assignee;
310
+ const newName = normalizeAgentName(op.assignee);
311
+ changes.push(`${stage.name}.${op.id}: ${oldName} → ${newName}`);
312
+ count++;
313
+ if (!dryRun)
314
+ op.assignee = newName;
315
+ }
316
+ }
317
+ }
318
+ if (!dryRun && count > 0) {
319
+ atomicWriteFileSync(flowPath, JSON.stringify(data, null, 2) + '\n');
320
+ }
321
+ return { changes, count };
322
+ }
323
+ // ─── 备份清理(REV-1206) ───────────────────────────────────────
324
+ /** 保留最近 keep 次备份,删除更早的备份目录 */
325
+ export function cleanOldBackups(projectPath, keep = 5) {
326
+ const root = resolve(projectPath, BACKUP_DIR);
327
+ if (!existsSync(root))
328
+ return [];
329
+ const dirs = readdirSync(root).filter((d) => /^\d{14}$/.test(d)).sort();
330
+ const removed = [];
331
+ while (dirs.length > keep) {
332
+ const oldest = dirs.shift();
333
+ rmSync(join(root, oldest), { recursive: true, force: true });
334
+ removed.push(oldest);
335
+ }
336
+ return removed;
337
+ }
338
+ // ─── 主流程(M4) ───────────────────────────────────────────────
339
+ /** 迁移主流程:检测→备份→全局部署→state 拆分→清理→assignee 报告 */
340
+ export function migrateProject(projectPath, opts) {
341
+ const lang = opts?.lang ?? 'zh-CN';
342
+ const dryRun = opts?.dryRun ?? false;
343
+ const remap = opts?.remapAssignee ?? false;
344
+ const legacy = detectLegacy(projectPath);
345
+ const { framework, custom } = listLegacyFiles(projectPath, lang);
346
+ // REV-1307:不论 remap 与否都先扫描报告(remapAssignees(..., true) 仅扫描不写);
347
+ // 否则 --remap-assignee 时 assigneeReport 为空,dry-run 预览缺 assignee 报告。
348
+ const assigneeReport = (() => {
349
+ const r = remapAssignees(projectPath, true); // 仅扫描报告(不写)
350
+ return aggregateAssignee(r);
351
+ })();
352
+ if (dryRun) {
353
+ // 预览:不写盘,返回计划
354
+ return {
355
+ legacy, backupDir: null, deployed: [], stateSplit: { movedToGlobal: [], keptInProject: [], unmapped: [] },
356
+ cleaned: framework, keptCustom: custom, jsoncCleaned: legacy.legacyJsoncSkillsMapping || legacy.legacyJsoncInstructions,
357
+ assigneeReport, remapped: false,
358
+ };
359
+ }
360
+ if (!legacy.isLegacy) {
361
+ return { legacy, backupDir: null, deployed: [], stateSplit: { movedToGlobal: [], keptInProject: [], unmapped: [] }, cleaned: [], keptCustom: [], jsoncCleaned: false, assigneeReport, remapped: false };
362
+ }
363
+ // 1. 备份
364
+ const { backupDir, manifest } = backupLegacy(projectPath, legacy, { remapAssignee: remap });
365
+ // 2. 全局部署(复用 update 能力;REV-1201 全局资产不纳入回滚)
366
+ const deployed = [];
367
+ const globalStateKeys = []; // REV-1302:记录本次写入的全局 state key(rollback 仅删这些)
368
+ const globalState = loadGlobalUpdateState() ?? { version: '1.0', last_update: new Date().toISOString(), openfeel_version: getOpenfeelVersion(), files: {} };
369
+ let stateSplit = { movedToGlobal: [], keptInProject: [], unmapped: [] };
370
+ // REV-1405:全局部署 + state 拆分纳入 try/finally。步骤 2 中途抛异常时仍回填
371
+ // manifest.globalStateKeys(已部署 key),避免 rollback 无法清理已写入的全局 state 记录。
372
+ try {
373
+ const corePath = getGlobalCoreMdPath();
374
+ deployGlobalAsset(corePath, loadTemplate(lang, 'core-instructions'), globalState);
375
+ deployed.push(corePath);
376
+ globalStateKeys.push(corePath);
377
+ const agentsDir = getGlobalAgentsDir();
378
+ for (const name of listAgentIds(lang)) {
379
+ const p = join(agentsDir, `${name}.md`);
380
+ deployGlobalAsset(p, loadAgentTemplate(lang, name), globalState);
381
+ deployed.push(p);
382
+ globalStateKeys.push(p);
383
+ }
384
+ const skillsDir = getGlobalSkillsDir();
385
+ for (const [name, content] of Object.entries(SKILL_DEFINITIONS)) {
386
+ mkdirSync(join(skillsDir, name), { recursive: true });
387
+ const p = join(skillsDir, name, 'SKILL.md');
388
+ deployGlobalAsset(p, content, globalState);
389
+ deployed.push(p);
390
+ globalStateKeys.push(p);
391
+ }
392
+ // REV-1804:框架资产(core.md / agents / skills)写盘后同步全局 state hash,与 update.ts 末尾循环一致;
393
+ // 避免迁移后 hash 缺失导致下次 update 误判为外部修改。global jsonc 的 hash 在下方单独更新,不重复。
394
+ for (const p of deployed) {
395
+ if (existsSync(p)) {
396
+ updateFileHash(globalState, p, readFileSync(p, 'utf-8'));
397
+ }
398
+ }
399
+ // 全局 opencode.jsonc 深度合并(REV-1306:复用 update.ts 的 try-catch 降级,parse 失败保留原文件+告警)
400
+ const globalJsoncPath = getGlobalOpencodeJsoncPath();
401
+ let merged;
402
+ try {
403
+ merged = mergeGlobalOpencodeJsonc(existsSync(globalJsoncPath) ? readFileSync(globalJsoncPath, 'utf-8') : '{}\n');
404
+ }
405
+ catch (err) {
406
+ console.warn(`[migrate] 全局 opencode.jsonc 解析失败(可能含块注释),跳过合并保留原文件: ${err.message}`);
407
+ merged = existsSync(globalJsoncPath) ? readFileSync(globalJsoncPath, 'utf-8') : '{}\n';
408
+ }
409
+ withFileLock(globalLockPath('global-opencode-jsonc'), () => atomicWriteFileSync(globalJsoncPath, merged));
410
+ updateFileHash(globalState, globalJsoncPath, merged);
411
+ saveGlobalUpdateState(globalState);
412
+ globalStateKeys.push(globalJsoncPath);
413
+ // 3. state 拆分/重键(REV-1402:复用上面已加载的 globalState,避免重复 IO)
414
+ stateSplit = splitUpdateState(projectPath, lang, globalState);
415
+ globalStateKeys.push(...stateSplit.movedToGlobal); // REV-1302:拆分移入全局的 key 纳入 rollback 范围
416
+ }
417
+ finally {
418
+ // REV-1302 / REV-1405:无论部署是否成功,均回填 manifest.globalStateKeys
419
+ //(rollback 据此仅删本次写入的全局 state key)。
420
+ manifest.globalStateKeys = globalStateKeys;
421
+ atomicWriteFileSync(join(backupDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
422
+ }
423
+ // 4. 清理 legacy 框架文件(framework 删除,custom 保留)
424
+ for (const abs of framework) {
425
+ rmSync(abs, { force: true });
426
+ // 删除后清理空的父目录(如 skills/{name}/ 空子目录),避免残留导致 legacy 判据 ② 恒真
427
+ const parent = dirname(abs);
428
+ try {
429
+ if (existsSync(parent) && readdirSync(parent).length === 0) {
430
+ rmSync(parent, { recursive: true, force: true });
431
+ }
432
+ }
433
+ catch { /* 忽略 */ }
434
+ }
435
+ // 清理空目录(agents/skills/instructions 若无剩余则删除)
436
+ for (const d of ['.opencode/agents', '.opencode/skills', '.opencode/instructions']) {
437
+ const dir = resolve(projectPath, d);
438
+ try {
439
+ if (existsSync(dir) && readdirSync(dir).length === 0)
440
+ rmSync(dir, { recursive: true, force: true });
441
+ }
442
+ catch { /* 忽略 */ }
443
+ }
444
+ // 5. 清理项目 opencode.jsonc
445
+ const jsoncCleaned = cleanProjectJsonc(projectPath);
446
+ // 6. assignee 改写(可选)
447
+ let remapped = false;
448
+ const finalAssigneeReport = remap ? (() => { const r = remapAssignees(projectPath, false); remapped = r.count > 0; return aggregateAssignee(r); })() : assigneeReport;
449
+ // 7. 备份清理(REV-1206)
450
+ cleanOldBackups(projectPath, 5);
451
+ return {
452
+ legacy, backupDir, deployed, stateSplit,
453
+ cleaned: framework, keptCustom: custom, jsoncCleaned,
454
+ assigneeReport: finalAssigneeReport, remapped,
455
+ };
456
+ }
457
+ /** 将 remapAssignees 的 changes 聚合为 { oldName, newName, count }[] */
458
+ function aggregateAssignee(r) {
459
+ const map = new Map();
460
+ for (const c of r.changes) {
461
+ const m = c.match(/^(.*): (.*) → (.*)$/);
462
+ if (!m)
463
+ continue;
464
+ const key = `${m[2]}→${m[3]}`;
465
+ const cur = map.get(key) ?? { oldName: m[2], newName: m[3], count: 0 };
466
+ cur.count++;
467
+ map.set(key, cur);
468
+ }
469
+ return [...map.values()];
470
+ }
471
+ // ─── 回滚(D39-3 / REV-1201) ──────────────────────────────────
472
+ /** 回滚最近一次迁移:按 manifest 逆向恢复项目文件 + 还原全局 state 新增条目 */
473
+ export function rollbackMigration(projectPath, backupTs) {
474
+ const root = resolve(projectPath, BACKUP_DIR);
475
+ let backupDir;
476
+ if (backupTs) {
477
+ backupDir = join(root, backupTs);
478
+ }
479
+ else {
480
+ const dirs = readdirSync(root).filter((d) => /^\d{14}$/.test(d)).sort();
481
+ if (dirs.length === 0)
482
+ throw new Error('无可回滚的备份');
483
+ backupDir = join(root, dirs[dirs.length - 1]);
484
+ }
485
+ const manifestPath = join(backupDir, 'manifest.json');
486
+ if (!existsSync(manifestPath))
487
+ throw new Error(`备份 manifest 缺失: ${manifestPath}`);
488
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
489
+ const restored = [];
490
+ // 逆向恢复:delete 型 = 源文件已被删,从备份复制回;modify 型 = 源文件被改写,从备份覆盖回
491
+ for (const e of manifest.entries) {
492
+ const sourceAbs = resolve(projectPath, e.source);
493
+ const backupAbs = join(backupDir, e.backupPath);
494
+ if (!existsSync(backupAbs))
495
+ continue;
496
+ mkdirSync(dirname(sourceAbs), { recursive: true });
497
+ copyFileSync(backupAbs, sourceAbs);
498
+ restored.push(e.source);
499
+ }
500
+ // 全局 state 还原:仅移除本次 migrate 写入的 key(REV-1302:以 manifest.globalStateKeys 为准,
501
+ // 不触碰历史全局框架条目;REV-1201 不还原全局文件,仅还原 state)
502
+ let stateRestored = false;
503
+ const globalState = loadGlobalUpdateState();
504
+ if (globalState) {
505
+ const keysToRemove = manifest.globalStateKeys ?? [];
506
+ for (const k of keysToRemove) {
507
+ delete globalState.files[k];
508
+ }
509
+ saveGlobalUpdateState(globalState);
510
+ stateRestored = true;
511
+ }
512
+ return { restored, stateRestored };
513
+ }
514
+ /** 回滚预览(REV-1309):读取最新 manifest,返回 entries 列表(source + op 类型),不写盘 */
515
+ export function previewRollback(projectPath, backupTs) {
516
+ const root = resolve(projectPath, BACKUP_DIR);
517
+ let backupDir;
518
+ if (backupTs) {
519
+ backupDir = join(root, backupTs);
520
+ }
521
+ else {
522
+ const dirs = readdirSync(root).filter((d) => /^\d{14}$/.test(d)).sort();
523
+ if (dirs.length === 0)
524
+ throw new Error('无可回滚的备份');
525
+ backupDir = join(root, dirs[dirs.length - 1]);
526
+ }
527
+ const manifestPath = join(backupDir, 'manifest.json');
528
+ if (!existsSync(manifestPath))
529
+ throw new Error(`备份 manifest 缺失: ${manifestPath}`);
530
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
531
+ return manifest.entries.map((e) => ({ op: e.op, source: e.source }));
532
+ }
533
+ //# sourceMappingURL=migrate.js.map