@akira-tl/forgerelay 1.1.2 → 1.2.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 (170) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +14 -0
  3. package/dist/activity/runtime/mcp-query-tools.js +15 -5
  4. package/dist/cli/config/inspect.js +331 -0
  5. package/dist/cli/config/migrate.js +351 -0
  6. package/dist/cli/init/setup-config.js +41 -0
  7. package/dist/cli/init.js +128 -130
  8. package/dist/cli/maintenance.js +15 -5
  9. package/dist/cli/mcp/external-mcp.js +14 -10
  10. package/dist/cli/mcp/status.js +11 -6
  11. package/dist/cli.js +40 -7
  12. package/dist/lsp/language-server-config.js +66 -144
  13. package/dist/lsp/runtime/manager.js +35 -4
  14. package/dist/mcp/hooks/config.js +141 -0
  15. package/dist/mcp/hooks/external-mcp-transform.js +22 -13
  16. package/dist/mcp/hooks/hook-cli.js +62 -52
  17. package/dist/mcp/hooks/hooks.js +111 -85
  18. package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
  19. package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
  20. package/dist/mcp/server/core/capability-support.js +7 -0
  21. package/dist/runtime/config/config.js +142 -64
  22. package/dist/runtime/config/definition/catalog.js +12 -0
  23. package/dist/runtime/config/definition/definition.js +115 -0
  24. package/dist/runtime/config/definition/external-mcp.js +58 -0
  25. package/dist/runtime/config/definition/general-config.js +258 -0
  26. package/dist/runtime/config/definition/language-servers.js +116 -0
  27. package/dist/runtime/config/definition/schema.js +113 -0
  28. package/dist/runtime/config/definition/types.js +2 -0
  29. package/dist/runtime/config/external-mcp-auth-store.js +105 -29
  30. package/dist/runtime/config/external-mcp-config.js +39 -0
  31. package/dist/runtime/config/external-mcp-registry.js +284 -106
  32. package/dist/runtime/config/inspection/live.js +77 -0
  33. package/dist/runtime/config/resolution/general.js +93 -0
  34. package/dist/runtime/config/resolution/hooks.js +315 -0
  35. package/dist/runtime/config/resolution/language-servers.js +237 -0
  36. package/dist/runtime/config/resolution/project-sources.js +159 -0
  37. package/dist/runtime/config/resolution/resolver.js +485 -0
  38. package/dist/runtime/config/resolution/types.js +1 -0
  39. package/dist/runtime/config/runtime/config-runtime.js +74 -0
  40. package/dist/runtime/config/runtime/source-refresh.js +263 -0
  41. package/dist/runtime/config/user-config.js +38 -31
  42. package/dist/runtime/security/project-execution-trust.js +34 -0
  43. package/dist/runtime/testing/server-fixture.js +9 -8
  44. package/dist/server.js +10 -6
  45. package/dist/subagents/profiles.js +336 -53
  46. package/dist/subagents/sessions/execution.js +6 -35
  47. package/dist/subagents/sessions/manager.js +14 -2
  48. package/dist/ui/.vite/manifest.json +275 -275
  49. package/dist/ui/activity-panel-app.html +1 -1
  50. package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
  51. package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
  52. package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
  53. package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
  54. package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
  55. package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
  56. package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
  57. package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
  58. package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
  59. package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
  60. package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
  61. package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
  62. package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
  63. package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
  64. package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
  65. package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
  66. package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
  67. package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
  68. package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
  69. package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
  70. package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
  71. package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
  72. package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
  73. package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
  74. package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
  75. package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
  76. package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
  77. package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
  78. package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
  79. package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
  80. package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
  81. package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
  82. package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
  83. package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
  84. package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
  85. package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
  86. package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
  87. package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
  88. package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
  89. package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
  90. package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
  91. package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
  92. package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
  93. package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
  94. package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
  95. package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
  96. package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
  97. package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
  98. package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
  99. package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
  100. package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
  101. package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
  102. package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
  103. package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
  104. package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
  105. package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
  106. package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
  107. package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
  108. package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
  109. package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
  110. package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
  111. package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
  112. package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
  113. package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
  114. package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
  115. package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
  116. package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
  117. package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
  118. package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
  119. package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
  120. package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
  121. package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
  122. package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
  123. package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
  124. package/dist/workspaces/conversation-test-support.js +3 -11
  125. package/dist/workspaces/sessions.js +2 -0
  126. package/dist/workspaces/state/project-context.js +221 -0
  127. package/dist/workspaces.js +3 -1
  128. package/docs/chatgpt-coding-workflow.md +8 -3
  129. package/docs/configuration.md +99 -41
  130. package/docs/gotchas.md +24 -5
  131. package/docs/security.md +14 -3
  132. package/docs/setup.md +12 -7
  133. package/package.json +12 -3
  134. package/schemas/v1/config.project-local.schema.json +14 -0
  135. package/schemas/v1/config.project.schema.json +14 -0
  136. package/schemas/v1/config.user.schema.json +538 -0
  137. package/schemas/v1/hooks.project-local.schema.json +118 -0
  138. package/schemas/v1/hooks.project.schema.json +118 -0
  139. package/schemas/v1/hooks.user.schema.json +118 -0
  140. package/schemas/v1/language-servers.project-local.schema.json +230 -0
  141. package/schemas/v1/language-servers.project.schema.json +230 -0
  142. package/schemas/v1/language-servers.user.schema.json +230 -0
  143. package/schemas/v1/mcp.project-local.schema.json +143 -0
  144. package/schemas/v1/mcp.project.schema.json +143 -0
  145. package/schemas/v1/mcp.user.schema.json +143 -0
  146. package/scripts/ci/architecture.mjs +54 -4
  147. package/scripts/ci/cmd-acceptance.mjs +9 -9
  148. package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
  149. package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
  150. package/scripts/ci/gates/core-contract.mjs +27 -0
  151. package/scripts/ci/gates/platform-tests.mjs +38 -0
  152. package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
  153. package/scripts/ci/gates/test-shard.mjs +71 -0
  154. package/scripts/ci/powershell51-acceptance.mjs +9 -9
  155. package/scripts/ci/pwsh-acceptance.mjs +9 -9
  156. package/scripts/ci/verify.mjs +2 -0
  157. package/scripts/ci/windows-product-acceptance.mjs +9 -10
  158. package/scripts/config/schema-text.mjs +4 -0
  159. package/scripts/config/schema-text.test.mjs +18 -0
  160. package/scripts/config/schema.mjs +74 -0
  161. package/scripts/lsp/interop.mjs +4 -1
  162. package/scripts/release/pack.mjs +64 -4
  163. package/scripts/release/release-gate.test.mjs +93 -27
  164. package/scripts/release-parity.mjs +19 -2
  165. package/templates/instructions/cmd.md +12 -0
  166. package/templates/instructions/fish.md +11 -0
  167. package/templates/instructions/powershell.md +14 -0
  168. package/templates/instructions/pwsh.md +14 -0
  169. package/templates/instructions/zsh.md +11 -0
  170. package/dist/runtime/instructions/powershell-skill.js +0 -65
@@ -0,0 +1,351 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { configSchemaId } from "../../runtime/config/definition/schema.js";
6
+ import { generalConfigDefinition } from "../../runtime/config/definition/general-config.js";
7
+ import { externalMcpConfigDefinition } from "../../runtime/config/definition/external-mcp.js";
8
+ import { languageServersConfigDefinition } from "../../runtime/config/definition/language-servers.js";
9
+ import { parseExternalMcpServers } from "../../runtime/config/external-mcp-config.js";
10
+ import { loadForgeRelayFiles, } from "../../runtime/config/user-config.js";
11
+ import { normalizeLanguageServerDefinitions } from "../../runtime/config/resolution/language-servers.js";
12
+ import { hooksConfigDefinition, normalizeLegacyHookEntries, } from "../../mcp/hooks/config.js";
13
+ import { mergeHookConfigs, parseHookConfig } from "../../mcp/hooks/hooks.js";
14
+ import { canonicalSubagentProfileDocumentFromLegacy } from "../../subagents/profiles.js";
15
+ import { resolveProjectContext } from "../../workspaces/state/project-context.js";
16
+ export async function runConfigMigration(args) {
17
+ const options = parseMigrationArgs(args);
18
+ const files = loadForgeRelayFiles(process.env, { readLegacyHooks: true });
19
+ const plan = options.scope === "global"
20
+ ? buildGlobalMigrationPlan(files.dir)
21
+ : await buildProjectMigrationPlan(files.dir, options.projectPath);
22
+ if (plan.writes.length === 0 && plan.copies.length === 0 && plan.removals.length === 0) {
23
+ console.log(`No legacy ForgeRelay configuration requires migration for ${plan.label}.`);
24
+ return;
25
+ }
26
+ if (options.dryRun) {
27
+ console.log(`DRY RUN: ${plan.label}`);
28
+ printPlan(plan);
29
+ return;
30
+ }
31
+ const backupRoot = createBackup(plan, files.dir);
32
+ const newTargets = new Set([
33
+ ...plan.writes.filter((write) => !existsSync(write.path)).map((write) => write.path),
34
+ ...plan.copies.filter((copy) => !existsSync(copy.target)).map((copy) => copy.target),
35
+ ]);
36
+ try {
37
+ for (const write of plan.writes)
38
+ atomicWrite(write.path, write.content, write.mode);
39
+ for (const copy of plan.copies)
40
+ atomicCopyDirectory(copy.source, copy.target);
41
+ for (const path of plan.removals)
42
+ rmSync(path, { recursive: true, force: true });
43
+ }
44
+ catch (error) {
45
+ for (const path of [...newTargets].reverse())
46
+ rmSync(path, { recursive: true, force: true });
47
+ restoreBackupSources(plan, backupRoot);
48
+ throw error;
49
+ }
50
+ console.log(`Migration complete: ${plan.label}`);
51
+ console.log(`Backup: ${backupRoot}`);
52
+ printPlan(plan);
53
+ }
54
+ function parseMigrationArgs(args) {
55
+ let dryRun = false;
56
+ let scope;
57
+ let projectPath;
58
+ for (let index = 0; index < args.length; index += 1) {
59
+ const arg = args[index];
60
+ if (arg === "--dry-run") {
61
+ if (dryRun)
62
+ throw new Error("--dry-run may only be supplied once.");
63
+ dryRun = true;
64
+ continue;
65
+ }
66
+ if (arg === "--global") {
67
+ if (scope)
68
+ throw new Error("Choose exactly one migration scope: --global or --project <path>.");
69
+ scope = "global";
70
+ continue;
71
+ }
72
+ if (arg === "--project") {
73
+ if (scope)
74
+ throw new Error("Choose exactly one migration scope: --global or --project <path>.");
75
+ const value = args[index + 1];
76
+ if (!value || value.startsWith("--"))
77
+ throw new Error("--project requires a project path.");
78
+ scope = "project";
79
+ projectPath = resolve(value);
80
+ index += 1;
81
+ continue;
82
+ }
83
+ throw new Error(`Unknown config migrate option: ${arg}`);
84
+ }
85
+ return { dryRun, scope: scope ?? "global", ...(projectPath ? { projectPath } : {}) };
86
+ }
87
+ function buildGlobalMigrationPlan(configDir) {
88
+ const files = loadForgeRelayFiles({ ...process.env, FORGERELAY_CONFIG_DIR: configDir }, { readLegacyHooks: true });
89
+ const writes = [];
90
+ const copies = [];
91
+ const removals = [];
92
+ const backupSources = [];
93
+ const nextConfig = { ...files.config };
94
+ let configChanged = false;
95
+ if (files.config.mcpServers !== undefined) {
96
+ if (!existsSync(join(configDir, "mcp.json"))) {
97
+ const servers = parseExternalMcpServers(files.config.mcpServers);
98
+ writes.push(jsonWrite(join(configDir, "mcp.json"), {
99
+ $schema: configSchemaId(externalMcpConfigDefinition, "user"),
100
+ servers,
101
+ }));
102
+ }
103
+ delete nextConfig.mcpServers;
104
+ configChanged = true;
105
+ }
106
+ if (files.config.languageServers !== undefined) {
107
+ if (!existsSync(join(configDir, "language-servers.json"))) {
108
+ const definitions = normalizeLanguageServerDefinitions(asRecord(files.config.languageServers, "languageServers"));
109
+ writes.push(jsonWrite(join(configDir, "language-servers.json"), {
110
+ $schema: configSchemaId(languageServersConfigDefinition, "user"),
111
+ ...definitions,
112
+ }));
113
+ }
114
+ delete nextConfig.languageServers;
115
+ configChanged = true;
116
+ }
117
+ const inlineHooks = parseHookConfig(files.config.hooks);
118
+ const aggregateHooks = parseHookConfig(files.hooks);
119
+ const mergedHooks = mergeHookConfigs(inlineHooks, aggregateHooks);
120
+ if (Object.keys(mergedHooks).length > 0) {
121
+ writes.push(...planHookWrites(join(configDir, "hooks"), "user", mergedHooks));
122
+ }
123
+ if (files.config.hooks !== undefined) {
124
+ delete nextConfig.hooks;
125
+ configChanged = true;
126
+ }
127
+ if (files.hooksExists)
128
+ removals.push(files.hooksPath);
129
+ const agentsDir = join(configDir, "agents");
130
+ const subagentsDir = join(configDir, "subagents");
131
+ if (existsSync(agentsDir)) {
132
+ writes.push(...planProfileWrites(agentsDir, subagentsDir));
133
+ removals.push(agentsDir);
134
+ }
135
+ const generalSchema = configSchemaId(generalConfigDefinition, "user");
136
+ if (files.configExists && nextConfig.$schema !== generalSchema) {
137
+ nextConfig.$schema = generalSchema;
138
+ configChanged = true;
139
+ }
140
+ if (configChanged)
141
+ writes.push(jsonWrite(files.configPath, nextConfig));
142
+ if (configChanged && files.configExists)
143
+ backupSources.push({ path: files.configPath, relativePath: "config.json" });
144
+ if (files.hooksExists)
145
+ backupSources.push({ path: files.hooksPath, relativePath: "hooks.json" });
146
+ if (existsSync(agentsDir))
147
+ backupSources.push({ path: agentsDir, relativePath: "agents" });
148
+ return deduplicatePlan({ label: "global ForgeRelay configuration", backupSources, writes, copies, removals });
149
+ }
150
+ async function buildProjectMigrationPlan(configDir, projectPath) {
151
+ const project = await resolveProjectContext(configDir, projectPath);
152
+ const writes = [];
153
+ const copies = [];
154
+ const removals = [];
155
+ const backupSources = [];
156
+ const hooksPath = join(project.sharedConfigDir, "hooks.json");
157
+ if (existsSync(hooksPath)) {
158
+ const hooks = parseHookConfig(readJson(hooksPath));
159
+ writes.push(...planHookWrites(join(project.sharedConfigDir, "hooks"), "project", hooks));
160
+ removals.push(hooksPath);
161
+ backupSources.push({ path: hooksPath, relativePath: join("project", project.id, "hooks.json") });
162
+ }
163
+ const agentsDir = join(project.sharedConfigDir, "agents");
164
+ if (existsSync(agentsDir)) {
165
+ writes.push(...planProfileWrites(agentsDir, join(project.sharedConfigDir, "subagents")));
166
+ removals.push(agentsDir);
167
+ backupSources.push({ path: agentsDir, relativePath: join("project", project.id, "agents") });
168
+ }
169
+ return deduplicatePlan({
170
+ label: `Project configuration at ${project.projectRoot}`,
171
+ backupSources,
172
+ writes,
173
+ copies,
174
+ removals,
175
+ });
176
+ }
177
+ function planHookWrites(canonicalDir, scope, legacyConfig) {
178
+ const normalized = normalizeLegacyHookEntries(legacyConfig);
179
+ const writes = [];
180
+ const reserved = new Set(existsSync(canonicalDir)
181
+ ? readdirSync(canonicalDir).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -5))
182
+ : []);
183
+ for (const [key, value] of Object.entries(normalized)) {
184
+ if (reserved.has(key))
185
+ continue;
186
+ if (!Array.isArray(value))
187
+ continue;
188
+ for (const [index, entry] of value.entries()) {
189
+ const stem = availableHookFileStem(key, index, reserved);
190
+ reserved.add(stem);
191
+ writes.push(jsonWrite(join(canonicalDir, `${stem}.json`), canonicalHookDocument(entry, scope)));
192
+ }
193
+ }
194
+ return writes;
195
+ }
196
+ function canonicalHookDocument(entry, scope) {
197
+ return {
198
+ $schema: configSchemaId(hooksConfigDefinition, scope),
199
+ event: entry.event,
200
+ ...(entry.matcher ? { matcher: entry.matcher } : {}),
201
+ command: entry.command,
202
+ ...(entry.timeoutSeconds === undefined ? {} : { timeoutSeconds: entry.timeoutSeconds }),
203
+ ...(entry.report === undefined ? {} : { report: entry.report }),
204
+ };
205
+ }
206
+ function availableHookFileStem(key, index, reserved) {
207
+ const preferred = index === 0 ? key : `${key}--${index + 1}`;
208
+ if (/^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$/.test(preferred) && !reserved.has(preferred))
209
+ return preferred;
210
+ for (let salt = 0;; salt += 1) {
211
+ const candidate = `legacy-${createHash("sha256").update(`${key}\0${index}\0${salt}`).digest("hex").slice(0, 20)}`;
212
+ if (!reserved.has(candidate))
213
+ return candidate;
214
+ }
215
+ }
216
+ function planProfileWrites(legacyDir, canonicalDir) {
217
+ const canonicalNames = new Set();
218
+ const reservedFiles = new Set();
219
+ if (existsSync(canonicalDir)) {
220
+ for (const file of readdirSync(canonicalDir).filter((entry) => entry.endsWith(".md"))) {
221
+ const path = join(canonicalDir, file);
222
+ reservedFiles.add(file);
223
+ canonicalNames.add(profileName(readFileSync(path, "utf8"), path));
224
+ }
225
+ }
226
+ const writes = [];
227
+ for (const file of readdirSync(legacyDir).filter((entry) => entry.endsWith(".md")).sort()) {
228
+ const sourcePath = join(legacyDir, file);
229
+ const content = readFileSync(sourcePath, "utf8");
230
+ const name = profileName(content, sourcePath);
231
+ if (canonicalNames.has(name))
232
+ continue;
233
+ const targetFile = reservedFiles.has(file)
234
+ ? availableProfileFileName(name, file, reservedFiles)
235
+ : file;
236
+ reservedFiles.add(targetFile);
237
+ canonicalNames.add(name);
238
+ writes.push({
239
+ path: join(canonicalDir, targetFile),
240
+ content: canonicalSubagentProfileDocumentFromLegacy(content, sourcePath),
241
+ mode: 0o600,
242
+ });
243
+ }
244
+ return writes;
245
+ }
246
+ function availableProfileFileName(profileName, originalFile, reserved) {
247
+ for (let salt = 0;; salt += 1) {
248
+ const digest = createHash("sha256")
249
+ .update(`${profileName}\0${originalFile}\0${salt}`)
250
+ .digest("hex")
251
+ .slice(0, 20);
252
+ const candidate = `legacy-${digest}.md`;
253
+ if (!reserved.has(candidate))
254
+ return candidate;
255
+ }
256
+ }
257
+ function profileName(content, path) {
258
+ const lines = content.replace(/^\uFEFF/, "").split(/\r?\n/);
259
+ const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
260
+ if (lines[0]?.trim() !== "---" || end < 0)
261
+ throw new Error(`Subagent Profile is invalid: ${path}`);
262
+ const parsed = parseYaml(lines.slice(1, end).join("\n"));
263
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
264
+ throw new Error(`Subagent Profile is invalid: ${path}`);
265
+ const raw = parsed.name;
266
+ return typeof raw === "string" && raw.trim() ? raw.trim() : basename(path, ".md");
267
+ }
268
+ function jsonWrite(path, value) {
269
+ return { path, content: `${JSON.stringify(value, null, 2)}\n`, mode: 0o600 };
270
+ }
271
+ function createBackup(plan, configDir) {
272
+ const suffix = `${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(4).toString("hex")}`;
273
+ const backupRoot = join(configDir, "migration-backups", suffix);
274
+ mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
275
+ for (const source of plan.backupSources) {
276
+ const target = join(backupRoot, source.relativePath);
277
+ mkdirSync(resolve(target, ".."), { recursive: true, mode: 0o700 });
278
+ cpSync(source.path, target, { recursive: true, force: false, errorOnExist: true });
279
+ }
280
+ return backupRoot;
281
+ }
282
+ function atomicWrite(path, content, mode) {
283
+ mkdirSync(resolve(path, ".."), { recursive: true, mode: 0o700 });
284
+ const temp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
285
+ try {
286
+ writeFileSync(temp, content, { mode, flag: "wx" });
287
+ renameSync(temp, path);
288
+ }
289
+ finally {
290
+ rmSync(temp, { force: true });
291
+ }
292
+ }
293
+ function atomicCopyDirectory(source, target) {
294
+ mkdirSync(resolve(target, ".."), { recursive: true, mode: 0o700 });
295
+ const temp = `${target}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
296
+ try {
297
+ cpSync(source, temp, { recursive: statSync(source).isDirectory(), force: false, errorOnExist: true });
298
+ renameSync(temp, target);
299
+ }
300
+ finally {
301
+ rmSync(temp, { recursive: true, force: true });
302
+ }
303
+ }
304
+ function restoreBackupSources(plan, backupRoot) {
305
+ for (const source of plan.backupSources) {
306
+ const backup = join(backupRoot, source.relativePath);
307
+ if (!existsSync(backup))
308
+ continue;
309
+ rmSync(source.path, { recursive: true, force: true });
310
+ mkdirSync(resolve(source.path, ".."), { recursive: true, mode: 0o700 });
311
+ cpSync(backup, source.path, { recursive: true, force: false, errorOnExist: true });
312
+ }
313
+ }
314
+ function deduplicatePlan(plan) {
315
+ const writes = new Map();
316
+ for (const write of plan.writes) {
317
+ const existing = writes.get(write.path);
318
+ if (existing && existing.content !== write.content) {
319
+ throw new Error(`Migration would write conflicting canonical values to ${write.path}.`);
320
+ }
321
+ writes.set(write.path, write);
322
+ }
323
+ const copies = new Map();
324
+ for (const copy of plan.copies) {
325
+ if (copies.has(copy.target))
326
+ throw new Error(`Migration would copy multiple legacy resources to ${copy.target}.`);
327
+ copies.set(copy.target, copy);
328
+ }
329
+ return {
330
+ ...plan,
331
+ writes: [...writes.values()],
332
+ copies: [...copies.values()],
333
+ removals: [...new Set(plan.removals)],
334
+ };
335
+ }
336
+ function printPlan(plan) {
337
+ for (const write of plan.writes)
338
+ console.log(`WRITE ${write.path}`);
339
+ for (const copy of plan.copies)
340
+ console.log(`COPY ${copy.source} -> ${copy.target}`);
341
+ for (const path of plan.removals)
342
+ console.log(`REMOVE ${path}`);
343
+ }
344
+ function readJson(path) {
345
+ return JSON.parse(readFileSync(path, "utf8"));
346
+ }
347
+ function asRecord(value, label) {
348
+ if (!value || typeof value !== "object" || Array.isArray(value))
349
+ throw new Error(`${label} must be an object.`);
350
+ return value;
351
+ }
@@ -0,0 +1,41 @@
1
+ export function applySetupConfig(current, selection) {
2
+ const next = {
3
+ ...current,
4
+ $schema: selection.schema,
5
+ allowedRoots: [...selection.allowedRoots],
6
+ };
7
+ applyNetworkSelection(next, selection.network);
8
+ if (selection.advanced)
9
+ applyAdvancedSelection(next, selection.advanced);
10
+ return next;
11
+ }
12
+ function applyNetworkSelection(config, selection) {
13
+ delete config.host;
14
+ delete config.publicBaseUrl;
15
+ delete config.trustedProxies;
16
+ if (selection.mode === "lan")
17
+ config.host = "0.0.0.0";
18
+ if (selection.mode === "lan" || selection.mode === "proxy") {
19
+ if (selection.publicBaseUrl === undefined || selection.publicBaseUrl === null) {
20
+ throw new Error(`${selection.mode} setup requires a client-facing base URL.`);
21
+ }
22
+ config.publicBaseUrl = selection.publicBaseUrl;
23
+ }
24
+ if (selection.mode === "proxy")
25
+ config.trustedProxies = ["loopback"];
26
+ }
27
+ function applyAdvancedSelection(config, selection) {
28
+ if (selection.port === 7676)
29
+ delete config.port;
30
+ else
31
+ config.port = selection.port;
32
+ config.commandShell = selection.commandShell;
33
+ if (selection.shellInstructions)
34
+ config.shellInstructions = true;
35
+ else
36
+ delete config.shellInstructions;
37
+ if (selection.allowAgentLanguageServerInstall)
38
+ config.allowAgentLanguageServerInstall = true;
39
+ else
40
+ delete config.allowAgentLanguageServerInstall;
41
+ }