@thinkingsage/kanon 0.8.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 (199) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/bridge/mcp-server.cjs +14171 -0
  5. package/package.json +98 -0
  6. package/src/adapters/capabilities.ts +178 -0
  7. package/src/adapters/claude-code.ts +110 -0
  8. package/src/adapters/cline.ts +98 -0
  9. package/src/adapters/codex.ts +173 -0
  10. package/src/adapters/copilot.ts +106 -0
  11. package/src/adapters/cursor.ts +97 -0
  12. package/src/adapters/degradation.ts +95 -0
  13. package/src/adapters/index.ts +324 -0
  14. package/src/adapters/kiro-frontmatter.ts +139 -0
  15. package/src/adapters/kiro-inclusion.ts +86 -0
  16. package/src/adapters/kiro.ts +412 -0
  17. package/src/adapters/qdeveloper.ts +115 -0
  18. package/src/adapters/types.ts +81 -0
  19. package/src/adapters/windsurf.ts +96 -0
  20. package/src/admin.ts +283 -0
  21. package/src/asset-conventions.ts +118 -0
  22. package/src/attribution-backfill.ts +319 -0
  23. package/src/attribution-report.ts +95 -0
  24. package/src/attribution.ts +239 -0
  25. package/src/backends/github.ts +194 -0
  26. package/src/backends/http.ts +122 -0
  27. package/src/backends/index.ts +39 -0
  28. package/src/backends/local.ts +47 -0
  29. package/src/backends/s3.ts +157 -0
  30. package/src/backends/types.ts +59 -0
  31. package/src/base-cache.ts +270 -0
  32. package/src/browse-ui.ts +3754 -0
  33. package/src/browse.ts +1038 -0
  34. package/src/build.ts +1108 -0
  35. package/src/catalog.ts +204 -0
  36. package/src/cli-deprecated.ts +29 -0
  37. package/src/cli.ts +773 -0
  38. package/src/collection-admin.ts +287 -0
  39. package/src/collection-builder.ts +464 -0
  40. package/src/collections.ts +116 -0
  41. package/src/compatibility.ts +105 -0
  42. package/src/config.ts +743 -0
  43. package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
  44. package/src/eval.ts +1169 -0
  45. package/src/file-writer.ts +61 -0
  46. package/src/format-registry.ts +141 -0
  47. package/src/guild/auto-updater.ts +163 -0
  48. package/src/guild/backend-resolver.ts +49 -0
  49. package/src/guild/cli.ts +592 -0
  50. package/src/guild/collection-expander.ts +47 -0
  51. package/src/guild/global-cache.ts +247 -0
  52. package/src/guild/hook-generator.ts +100 -0
  53. package/src/guild/manifest.ts +154 -0
  54. package/src/guild/path-utils.ts +12 -0
  55. package/src/guild/sync.ts +622 -0
  56. package/src/guild/version-resolver.ts +42 -0
  57. package/src/help/metadata.ts +445 -0
  58. package/src/help/renderer.ts +265 -0
  59. package/src/help/typo-suggester.ts +25 -0
  60. package/src/hooks/expression.ts +493 -0
  61. package/src/hooks/pipeline.ts +141 -0
  62. package/src/import.ts +773 -0
  63. package/src/importers/claude-code.ts +134 -0
  64. package/src/importers/cline.ts +103 -0
  65. package/src/importers/codex.ts +140 -0
  66. package/src/importers/copilot.ts +103 -0
  67. package/src/importers/cursor.ts +105 -0
  68. package/src/importers/index.ts +390 -0
  69. package/src/importers/kiro.ts +110 -0
  70. package/src/importers/qdeveloper.ts +103 -0
  71. package/src/importers/types.ts +54 -0
  72. package/src/importers/windsurf.ts +104 -0
  73. package/src/install.ts +1005 -0
  74. package/src/manifest-admin.ts +306 -0
  75. package/src/mcp-bridge.ts +240 -0
  76. package/src/mutation/delta.ts +50 -0
  77. package/src/mutation/history.ts +66 -0
  78. package/src/mutation/operators.ts +524 -0
  79. package/src/mutation/runner.ts +332 -0
  80. package/src/new.ts +106 -0
  81. package/src/outcomes/collision.ts +127 -0
  82. package/src/outcomes/normalize.ts +208 -0
  83. package/src/outcomes/registry.ts +173 -0
  84. package/src/parser.ts +446 -0
  85. package/src/provenance-backfill-cli.ts +319 -0
  86. package/src/provenance-backfill.ts +520 -0
  87. package/src/publish.ts +354 -0
  88. package/src/reconcile-orchestrator.ts +502 -0
  89. package/src/reconcile-report-renderer.ts +176 -0
  90. package/src/resolve-body.ts +15 -0
  91. package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
  92. package/src/rosetta/builtins/contracts.ts +1033 -0
  93. package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
  94. package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
  95. package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
  96. package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
  97. package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
  98. package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
  99. package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
  100. package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
  101. package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
  102. package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
  103. package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
  104. package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
  105. package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
  106. package/src/rosetta/builtins/sources/cline-native.ts +176 -0
  107. package/src/rosetta/builtins/sources/codex-native.ts +343 -0
  108. package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
  109. package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
  110. package/src/rosetta/builtins/sources/index.ts +95 -0
  111. package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
  112. package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
  113. package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
  114. package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
  115. package/src/rosetta/builtins/sources/superpowers.ts +240 -0
  116. package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
  117. package/src/rosetta/builtins/targets/claude-code.ts +181 -0
  118. package/src/rosetta/builtins/targets/cline.ts +87 -0
  119. package/src/rosetta/builtins/targets/codex.ts +226 -0
  120. package/src/rosetta/builtins/targets/copilot.ts +103 -0
  121. package/src/rosetta/builtins/targets/cursor.ts +87 -0
  122. package/src/rosetta/builtins/targets/index.ts +60 -0
  123. package/src/rosetta/builtins/targets/kiro.ts +278 -0
  124. package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
  125. package/src/rosetta/builtins/targets/windsurf.ts +87 -0
  126. package/src/rosetta/canonical.ts +729 -0
  127. package/src/rosetta/compatibility.ts +432 -0
  128. package/src/rosetta/contracts.ts +329 -0
  129. package/src/rosetta/detector.ts +724 -0
  130. package/src/rosetta/diagnostics.ts +630 -0
  131. package/src/rosetta/engine-bootstrap.ts +103 -0
  132. package/src/rosetta/engine.ts +744 -0
  133. package/src/rosetta/index.ts +381 -0
  134. package/src/rosetta/inspection.ts +530 -0
  135. package/src/rosetta/plan.ts +448 -0
  136. package/src/rosetta/provenance-digest.ts +369 -0
  137. package/src/rosetta/reconcile.ts +812 -0
  138. package/src/rosetta/redaction.ts +467 -0
  139. package/src/rosetta/registry.ts +712 -0
  140. package/src/rosetta/renderers.ts +571 -0
  141. package/src/rosetta/request-guard.ts +335 -0
  142. package/src/rosetta/resolution.ts +419 -0
  143. package/src/rosetta/source-accounting.ts +233 -0
  144. package/src/rosetta/templates.ts +129 -0
  145. package/src/rosetta-cli.ts +717 -0
  146. package/src/rosetta-docs-generator.ts +793 -0
  147. package/src/rosetta-profiles-cli.ts +367 -0
  148. package/src/schemas.ts +1712 -0
  149. package/src/spec-coordination.ts +1141 -0
  150. package/src/temper.ts +747 -0
  151. package/src/template-bundle-loader.ts +312 -0
  152. package/src/template-engine.ts +53 -0
  153. package/src/translation-application-policy.ts +496 -0
  154. package/src/translation-orchestrator.ts +1013 -0
  155. package/src/translation-plan-applier.ts +473 -0
  156. package/src/tutorial.ts +305 -0
  157. package/src/validate.ts +1093 -0
  158. package/src/versioning.ts +553 -0
  159. package/src/wizard.ts +660 -0
  160. package/src/workspace.ts +237 -0
  161. package/templates/eval-contexts/claude-code.md.njk +6 -0
  162. package/templates/eval-contexts/cline.md.njk +6 -0
  163. package/templates/eval-contexts/copilot.md.njk +6 -0
  164. package/templates/eval-contexts/cursor.md.njk +6 -0
  165. package/templates/eval-contexts/kiro.md.njk +10 -0
  166. package/templates/eval-contexts/qdeveloper.md.njk +6 -0
  167. package/templates/eval-contexts/windsurf.md.njk +6 -0
  168. package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
  169. package/templates/harness-adapters/_base/base.md.njk +16 -0
  170. package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
  171. package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
  172. package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
  173. package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
  174. package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
  175. package/templates/harness-adapters/cline/hook.sh.njk +4 -0
  176. package/templates/harness-adapters/cline/mcp.json.njk +1 -0
  177. package/templates/harness-adapters/cline/rule.md.njk +1 -0
  178. package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
  179. package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
  180. package/templates/harness-adapters/codex/skill.md.njk +27 -0
  181. package/templates/harness-adapters/copilot/agents.md.njk +1 -0
  182. package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
  183. package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
  184. package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
  185. package/templates/harness-adapters/cursor/rule.md.njk +6 -0
  186. package/templates/harness-adapters/kiro/hook.json.njk +1 -0
  187. package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
  188. package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
  189. package/templates/harness-adapters/kiro/power.md.njk +12 -0
  190. package/templates/harness-adapters/kiro/steering.md.njk +16 -0
  191. package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
  192. package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
  193. package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
  194. package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
  195. package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
  196. package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
  197. package/templates/knowledge/hooks.yaml.njk +4 -0
  198. package/templates/knowledge/knowledge.md.njk +53 -0
  199. package/templates/knowledge/mcp-servers.yaml.njk +2 -0
@@ -0,0 +1,553 @@
1
+ import { exists, readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import * as p from "@clack/prompts";
4
+ import chalk from "chalk";
5
+ import { type VersionManifest, VersionManifestSchema } from "./schemas";
6
+
7
+ /**
8
+ * Serialize a VersionManifest to pretty-printed JSON with 2-space indentation.
9
+ */
10
+ export function serializeManifest(manifest: VersionManifest): string {
11
+ return JSON.stringify(manifest, null, 2);
12
+ }
13
+
14
+ /**
15
+ * Parse a JSON string and validate it against VersionManifestSchema.
16
+ * Throws a ZodError if the JSON does not conform.
17
+ */
18
+ export function parseManifest(json: string): VersionManifest {
19
+ return VersionManifestSchema.parse(JSON.parse(json));
20
+ }
21
+
22
+ /**
23
+ * Compare two semver strings numerically.
24
+ * Returns negative if a < b, positive if a > b, 0 if equal.
25
+ */
26
+ export function compareVersions(a: string, b: string): number {
27
+ const partsA = a.split(".").map(Number);
28
+ const partsB = b.split(".").map(Number);
29
+
30
+ for (let i = 0; i < 3; i++) {
31
+ const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
32
+ if (diff !== 0) return diff;
33
+ }
34
+ return 0;
35
+ }
36
+
37
+ export interface MigrationScript {
38
+ fromVersion: string;
39
+ toVersion: string;
40
+ migrate: (
41
+ files: Map<string, string>,
42
+ manifest: VersionManifest,
43
+ ) => Map<string, string>;
44
+ }
45
+
46
+ /**
47
+ * Filter and sort migration scripts that form a chain from `fromVersion` to `toVersion`.
48
+ * Returns migrations in ascending version order (earliest first).
49
+ */
50
+ export function resolveMigrationChain(
51
+ availableMigrations: MigrationScript[],
52
+ fromVersion: string,
53
+ toVersion: string,
54
+ ): MigrationScript[] {
55
+ return availableMigrations
56
+ .filter(
57
+ (m) =>
58
+ compareVersions(m.fromVersion, fromVersion) >= 0 &&
59
+ compareVersions(m.toVersion, toVersion) <= 0,
60
+ )
61
+ .sort((a, b) => compareVersions(a.fromVersion, b.fromVersion));
62
+ }
63
+
64
+ /**
65
+ * Recursively scan a directory for `.forge-manifest.json` files and parse them.
66
+ */
67
+ export async function discoverManifests(
68
+ rootDir: string,
69
+ ): Promise<VersionManifest[]> {
70
+ const manifests: VersionManifest[] = [];
71
+ await scanForManifests(rootDir, manifests);
72
+ return manifests;
73
+ }
74
+
75
+ async function scanForManifests(
76
+ dir: string,
77
+ results: VersionManifest[],
78
+ ): Promise<void> {
79
+ let entries: { name: string; isDirectory(): boolean }[];
80
+ try {
81
+ entries = (await readdir(dir, { withFileTypes: true })) as unknown as {
82
+ name: string;
83
+ isDirectory(): boolean;
84
+ }[];
85
+ } catch {
86
+ return;
87
+ }
88
+
89
+ for (const entry of entries) {
90
+ const fullPath = join(dir, entry.name);
91
+ if (entry.isDirectory()) {
92
+ await scanForManifests(fullPath, results);
93
+ } else if (entry.name === ".forge-manifest.json") {
94
+ try {
95
+ const content = await readFile(fullPath, "utf-8");
96
+ const manifest = parseManifest(content);
97
+ results.push(manifest);
98
+ } catch {
99
+ // Skip invalid manifest files
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Execute an upgrade for a single artifact.
107
+ * Compares the manifest's current version against `latestVersion`,
108
+ * applies the migration chain if available, and returns the result.
109
+ */
110
+ export async function upgradeArtifact(
111
+ manifest: VersionManifest,
112
+ latestVersion: string,
113
+ migrations: MigrationScript[],
114
+ options: { force?: boolean; dryRun?: boolean },
115
+ ): Promise<{ updated: boolean; newManifest?: VersionManifest }> {
116
+ // Already at latest — no upgrade needed
117
+ if (compareVersions(manifest.version, latestVersion) >= 0) {
118
+ return { updated: false };
119
+ }
120
+
121
+ // Resolve the migration chain from current to latest
122
+ const chain = resolveMigrationChain(
123
+ migrations,
124
+ manifest.version,
125
+ latestVersion,
126
+ );
127
+
128
+ if (options.dryRun) {
129
+ return {
130
+ updated: true,
131
+ newManifest: {
132
+ ...manifest,
133
+ version: latestVersion,
134
+ installedAt: new Date().toISOString(),
135
+ },
136
+ };
137
+ }
138
+
139
+ // Apply migrations sequentially if available
140
+ let currentFiles = new Map<string, string>();
141
+ for (const file of manifest.files) {
142
+ currentFiles.set(file, "");
143
+ }
144
+
145
+ for (const migration of chain) {
146
+ currentFiles = migration.migrate(currentFiles, manifest);
147
+ }
148
+
149
+ const newManifest: VersionManifest = {
150
+ ...manifest,
151
+ version: latestVersion,
152
+ installedAt: new Date().toISOString(),
153
+ files: [...currentFiles.keys()],
154
+ };
155
+
156
+ return { updated: true, newManifest };
157
+ }
158
+
159
+ /**
160
+ * Embed a version string in compiled output content.
161
+ * For markdown: prepends `<!-- forge:version X.Y.Z -->` comment.
162
+ * For json: adds `"_forgeVersion": "X.Y.Z"` field.
163
+ */
164
+ export function embedVersion(
165
+ content: string,
166
+ version: string,
167
+ format: "markdown" | "json",
168
+ ): string {
169
+ if (format === "markdown") {
170
+ const comment = `<!-- forge:version ${version} -->`;
171
+ // If content has YAML frontmatter, place the version comment after
172
+ // the closing --- so frontmatter parsers can still detect the block.
173
+ if (content.startsWith("---\n") || content.startsWith("---\r\n")) {
174
+ const closingIdx = content.indexOf("\n---", 3);
175
+ if (closingIdx !== -1) {
176
+ // Find the end of the closing --- line
177
+ const afterClosing = closingIdx + 4; // skip \n---
178
+ const nextNewline =
179
+ content[afterClosing] === "\r"
180
+ ? afterClosing + 2
181
+ : content[afterClosing] === "\n"
182
+ ? afterClosing + 1
183
+ : afterClosing;
184
+ return `${content.slice(0, nextNewline)}${comment}\n${content.slice(nextNewline)}`;
185
+ }
186
+ }
187
+ return `${comment}\n${content}`;
188
+ }
189
+
190
+ // JSON format: inject _forgeVersion field
191
+ try {
192
+ const parsed = JSON.parse(content);
193
+ parsed._forgeVersion = version;
194
+ return JSON.stringify(parsed, null, 2);
195
+ } catch {
196
+ // If content isn't valid JSON, return as-is
197
+ return content;
198
+ }
199
+ }
200
+
201
+ export interface UpgradeOptions {
202
+ force?: boolean;
203
+ dryRun?: boolean;
204
+ project?: string;
205
+ }
206
+
207
+ interface UpgradeCandidate {
208
+ manifest: VersionManifest;
209
+ latestVersion: string;
210
+ changelogEntries: string | null;
211
+ hasMigrations: boolean;
212
+ }
213
+
214
+ /**
215
+ * Read changelog entries between two versions from a CHANGELOG.md file.
216
+ * Returns the relevant section text, or null if not found.
217
+ */
218
+ async function readChangelogEntries(
219
+ artifactPath: string,
220
+ fromVersion: string,
221
+ toVersion: string,
222
+ ): Promise<string | null> {
223
+ const changelogPath = join(artifactPath, "CHANGELOG.md");
224
+ if (!(await exists(changelogPath))) return null;
225
+
226
+ try {
227
+ const content = await readFile(changelogPath, "utf-8");
228
+ const lines = content.split("\n");
229
+ const relevantLines: string[] = [];
230
+ let capturing = false;
231
+
232
+ for (const line of lines) {
233
+ // Match version headers like ## 2.0.0 or ## [2.0.0]
234
+ const versionMatch = line.match(/^##\s+\[?(\d+\.\d+\.\d+)\]?/);
235
+ if (versionMatch) {
236
+ const headerVersion = versionMatch[1];
237
+ if (
238
+ compareVersions(headerVersion, fromVersion) > 0 &&
239
+ compareVersions(headerVersion, toVersion) <= 0
240
+ ) {
241
+ capturing = true;
242
+ relevantLines.push(line);
243
+ } else if (capturing) {
244
+ break;
245
+ }
246
+ } else if (capturing) {
247
+ relevantLines.push(line);
248
+ }
249
+ }
250
+
251
+ return relevantLines.length > 0 ? relevantLines.join("\n").trim() : null;
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Load migration scripts from an artifact's migrations/ directory.
259
+ * Migration files are named like `1.0.0-to-2.0.0.ts`.
260
+ */
261
+ async function loadMigrationScripts(
262
+ artifactPath: string,
263
+ ): Promise<MigrationScript[]> {
264
+ const migrationsDir = join(artifactPath, "migrations");
265
+ if (!(await exists(migrationsDir))) return [];
266
+
267
+ try {
268
+ const entries = await readdir(migrationsDir);
269
+ const scripts: MigrationScript[] = [];
270
+
271
+ for (const entry of entries) {
272
+ const match = entry.match(
273
+ /^(\d+\.\d+\.\d+)-to-(\d+\.\d+\.\d+)\.(ts|js)$/,
274
+ );
275
+ if (match) {
276
+ try {
277
+ const modulePath = join(migrationsDir, entry);
278
+ const mod = await import(modulePath);
279
+ const migrate = mod.default ?? mod.migrate;
280
+ if (typeof migrate === "function") {
281
+ scripts.push({
282
+ fromVersion: match[1],
283
+ toVersion: match[2],
284
+ migrate,
285
+ });
286
+ }
287
+ } catch {
288
+ // Skip unloadable migration scripts
289
+ }
290
+ }
291
+ }
292
+
293
+ return scripts;
294
+ } catch {
295
+ return [];
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Perform a clean reinstall of an artifact for a given harness.
301
+ * Rebuilds from source and writes the new version manifest.
302
+ */
303
+ async function cleanReinstall(
304
+ manifest: VersionManifest,
305
+ latestVersion: string,
306
+ catalogSourceDirs: string[],
307
+ ): Promise<VersionManifest | null> {
308
+ // Dynamically import build to avoid circular deps at module load time
309
+ const { build } = await import("./build");
310
+
311
+ // Build the artifact
312
+ const buildResult = await build({
313
+ knowledgeDirs: catalogSourceDirs,
314
+ distDir: "dist",
315
+ templatesDir: "templates/harness-adapters",
316
+ mcpServersDir: "mcp-servers",
317
+ harness: manifest.harnessName as import("./schemas").HarnessName,
318
+ });
319
+
320
+ if (buildResult.errors.length > 0) {
321
+ return null;
322
+ }
323
+
324
+ // Install the rebuilt artifact
325
+ const { install } = await import("./install");
326
+ await install({
327
+ artifactName: manifest.artifactName,
328
+ harness: manifest.harnessName as import("./schemas").HarnessName,
329
+ force: true,
330
+ source: undefined,
331
+ });
332
+
333
+ return {
334
+ ...manifest,
335
+ version: latestVersion,
336
+ installedAt: new Date().toISOString(),
337
+ };
338
+ }
339
+
340
+ /**
341
+ * The `kanon upgrade` command.
342
+ * Scans for installed manifests, compares versions against the catalog,
343
+ * displays changelog entries, prompts for confirmation, and performs upgrades.
344
+ */
345
+ export async function upgradeCommand(options: UpgradeOptions): Promise<void> {
346
+ const { force, dryRun, project } = options;
347
+ const catalogSourceDirs = ["knowledge", "packages"];
348
+
349
+ // 1. Scan for installed manifests
350
+ const scanDir = project ?? ".";
351
+ const manifests = await discoverManifests(scanDir);
352
+
353
+ if (manifests.length === 0) {
354
+ console.error(
355
+ chalk.yellow(
356
+ "No installed artifacts found (no .forge-manifest.json files detected).",
357
+ ),
358
+ );
359
+ console.error(
360
+ chalk.yellow("Run `kanon install` to install artifacts first."),
361
+ );
362
+ return;
363
+ }
364
+
365
+ // 2. Load catalog to get latest versions
366
+ const { generateCatalog } = await import("./catalog");
367
+ const catalog = await generateCatalog(catalogSourceDirs);
368
+ const catalogByName = new Map(catalog.map((e) => [e.name, e]));
369
+
370
+ // 3. Compare installed versions against latest
371
+ const candidates: UpgradeCandidate[] = [];
372
+
373
+ for (const manifest of manifests) {
374
+ const catalogEntry = catalogByName.get(manifest.artifactName);
375
+ if (!catalogEntry) continue;
376
+
377
+ const latestVersion = catalogEntry.version;
378
+ if (compareVersions(manifest.version, latestVersion) >= 0) continue;
379
+
380
+ // Read changelog entries for the version gap
381
+ const changelogEntries = await readChangelogEntries(
382
+ catalogEntry.path,
383
+ manifest.version,
384
+ latestVersion,
385
+ );
386
+
387
+ candidates.push({
388
+ manifest,
389
+ latestVersion,
390
+ changelogEntries,
391
+ hasMigrations: catalogEntry.migrations,
392
+ });
393
+ }
394
+
395
+ // 4. If no outdated artifacts, report and exit
396
+ if (candidates.length === 0) {
397
+ console.error(chalk.green("✓ All installed artifacts are up to date."));
398
+ return;
399
+ }
400
+
401
+ // 5. Display upgrade plan
402
+ console.error(
403
+ chalk.cyan(
404
+ `\nFound ${candidates.length} artifact(s) with available upgrades:\n`,
405
+ ),
406
+ );
407
+
408
+ for (const candidate of candidates) {
409
+ const { manifest, latestVersion, changelogEntries } = candidate;
410
+ console.error(
411
+ ` ${chalk.bold(manifest.artifactName)} ${chalk.red(manifest.version)} → ${chalk.green(latestVersion)} (${manifest.harnessName})`,
412
+ );
413
+ if (changelogEntries) {
414
+ const indented = changelogEntries
415
+ .split("\n")
416
+ .map((l) => ` ${l}`)
417
+ .join("\n");
418
+ console.error(chalk.dim(indented));
419
+ }
420
+ }
421
+ console.error("");
422
+
423
+ // 6. If dry-run, stop here
424
+ if (dryRun) {
425
+ console.error(chalk.cyan("[dry-run] No files were modified."));
426
+ return;
427
+ }
428
+
429
+ // 7. Prompt for confirmation (unless --force)
430
+ if (!force) {
431
+ const confirmed = await p.confirm({
432
+ message: `Upgrade ${candidates.length} artifact(s)?`,
433
+ });
434
+
435
+ if (p.isCancel(confirmed) || !confirmed) {
436
+ console.error(chalk.yellow("Upgrade cancelled."));
437
+ return;
438
+ }
439
+ }
440
+
441
+ // 8. Perform upgrades
442
+ let upgraded = 0;
443
+ let failed = 0;
444
+
445
+ for (const candidate of candidates) {
446
+ const { manifest, latestVersion, hasMigrations } = candidate;
447
+ const catalogEntry = catalogByName.get(manifest.artifactName);
448
+ if (!catalogEntry) continue;
449
+
450
+ const artifactPath = catalogEntry.path;
451
+
452
+ // Load migration scripts if the artifact has a migrations/ directory
453
+ let migrations: MigrationScript[] = [];
454
+ if (hasMigrations) {
455
+ migrations = await loadMigrationScripts(artifactPath);
456
+ }
457
+
458
+ // Check if migration scripts cover the version gap
459
+ const chain = resolveMigrationChain(
460
+ migrations,
461
+ manifest.version,
462
+ latestVersion,
463
+ );
464
+ const hasMigrationGap = hasMigrations && chain.length === 0;
465
+
466
+ if (hasMigrationGap) {
467
+ console.error(
468
+ chalk.yellow(
469
+ ` ⚠ No migration scripts found for ${manifest.artifactName} ${manifest.version} → ${latestVersion}. Performing clean reinstall.`,
470
+ ),
471
+ );
472
+ }
473
+
474
+ try {
475
+ if (chain.length > 0 && !hasMigrationGap) {
476
+ // Apply migration chain
477
+ const result = await upgradeArtifact(
478
+ manifest,
479
+ latestVersion,
480
+ migrations,
481
+ { force: true },
482
+ );
483
+
484
+ if (result.updated && result.newManifest) {
485
+ // Write updated manifest
486
+ const manifestDir = manifest.sourcePath.includes("/")
487
+ ? manifest.sourcePath
488
+ : ".";
489
+ const _manifestPath = join(manifestDir, ".forge-manifest.json");
490
+ // Perform clean reinstall to get new compiled files
491
+ const reinstallResult = await cleanReinstall(
492
+ manifest,
493
+ latestVersion,
494
+ catalogSourceDirs,
495
+ );
496
+ if (reinstallResult) {
497
+ console.error(
498
+ ` ${chalk.green("✓")} ${manifest.artifactName} upgraded to ${latestVersion}`,
499
+ );
500
+ upgraded++;
501
+ } else {
502
+ console.error(
503
+ chalk.red(
504
+ ` ✗ ${manifest.artifactName}: build failed during upgrade`,
505
+ ),
506
+ );
507
+ failed++;
508
+ }
509
+ }
510
+ } else {
511
+ // Clean reinstall fallback
512
+ const reinstallResult = await cleanReinstall(
513
+ manifest,
514
+ latestVersion,
515
+ catalogSourceDirs,
516
+ );
517
+ if (reinstallResult) {
518
+ console.error(
519
+ ` ${chalk.green("✓")} ${manifest.artifactName} upgraded to ${latestVersion} (clean reinstall)`,
520
+ );
521
+ upgraded++;
522
+ } else {
523
+ console.error(
524
+ chalk.red(
525
+ ` ✗ ${manifest.artifactName}: build failed during upgrade`,
526
+ ),
527
+ );
528
+ failed++;
529
+ }
530
+ }
531
+ } catch (err: unknown) {
532
+ // Migration script error — abort upgrade for this artifact, leave files unchanged
533
+ const msg = err instanceof Error ? err.message : String(err);
534
+ console.error(
535
+ chalk.red(
536
+ ` ✗ ${manifest.artifactName}: migration error — ${msg}. Files left unchanged.`,
537
+ ),
538
+ );
539
+ failed++;
540
+ }
541
+ }
542
+
543
+ // 9. Print summary
544
+ console.error("");
545
+ if (upgraded > 0) {
546
+ console.error(
547
+ chalk.green(`✓ ${upgraded} artifact(s) upgraded successfully.`),
548
+ );
549
+ }
550
+ if (failed > 0) {
551
+ console.error(chalk.red(`✗ ${failed} artifact(s) failed to upgrade.`));
552
+ }
553
+ }