@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.
- package/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- 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
|
+
}
|