@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,194 @@
|
|
|
1
|
+
import { access, mkdir } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { CatalogEntry, HarnessName } from "../schemas";
|
|
5
|
+
import type { ArtifactBackend, GitHubBackendConfig } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GitHub releases backend.
|
|
9
|
+
*
|
|
10
|
+
* Uses the `gh` CLI for authentication and asset downloads — no separate
|
|
11
|
+
* credential management required beyond `gh auth login`.
|
|
12
|
+
*
|
|
13
|
+
* Asset naming convention for releases:
|
|
14
|
+
* catalog.json — standalone catalog for browsing
|
|
15
|
+
* dist-<harness>.tar.gz — per-harness compiled artifact tree
|
|
16
|
+
* release-manifest.json — version metadata
|
|
17
|
+
*/
|
|
18
|
+
export class GitHubBackend implements ArtifactBackend {
|
|
19
|
+
readonly label: string;
|
|
20
|
+
private readonly releasePrefix: string;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly config: GitHubBackendConfig,
|
|
24
|
+
private readonly tag?: string,
|
|
25
|
+
) {
|
|
26
|
+
this.releasePrefix = config.releasePrefix ?? "v";
|
|
27
|
+
this.label = `github:${config.repo}${tag ? `@${tag}` : ""}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async fetchCatalog(): Promise<CatalogEntry[]> {
|
|
31
|
+
const tag = this.tag ?? (await this.latestTag());
|
|
32
|
+
const tmp = join(tmpdir(), `forge-catalog-${Date.now()}`);
|
|
33
|
+
await mkdir(tmp, { recursive: true });
|
|
34
|
+
const outPath = join(tmp, "catalog.json");
|
|
35
|
+
|
|
36
|
+
const proc = Bun.spawnSync(
|
|
37
|
+
[
|
|
38
|
+
"gh",
|
|
39
|
+
"release",
|
|
40
|
+
"download",
|
|
41
|
+
tag,
|
|
42
|
+
"--repo",
|
|
43
|
+
this.config.repo,
|
|
44
|
+
"--pattern",
|
|
45
|
+
"catalog.json",
|
|
46
|
+
"--output",
|
|
47
|
+
outPath,
|
|
48
|
+
],
|
|
49
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (proc.exitCode !== 0) {
|
|
53
|
+
const stderr = new TextDecoder().decode(proc.stderr);
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Failed to fetch catalog from GitHub release ${tag}: ${stderr}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const raw = await Bun.file(outPath).text();
|
|
60
|
+
return JSON.parse(raw) as CatalogEntry[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async fetchArtifact(
|
|
64
|
+
name: string,
|
|
65
|
+
harness: HarnessName,
|
|
66
|
+
version?: string,
|
|
67
|
+
): Promise<string> {
|
|
68
|
+
const tag = version ?? this.tag ?? (await this.latestTag());
|
|
69
|
+
const assetName = `dist-${harness}.tar.gz`;
|
|
70
|
+
const cacheDir = join(
|
|
71
|
+
process.env.HOME ?? tmpdir(),
|
|
72
|
+
".forge",
|
|
73
|
+
"cache",
|
|
74
|
+
`github-${this.config.repo.replace("/", "-")}`,
|
|
75
|
+
tag,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const extractedDir = join(cacheDir, harness);
|
|
79
|
+
const artifactDir = join(extractedDir, name);
|
|
80
|
+
|
|
81
|
+
// Return cached copy if available
|
|
82
|
+
try {
|
|
83
|
+
await access(artifactDir);
|
|
84
|
+
return artifactDir;
|
|
85
|
+
} catch {
|
|
86
|
+
// not cached yet
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await mkdir(extractedDir, { recursive: true });
|
|
90
|
+
|
|
91
|
+
// Download the per-harness tarball
|
|
92
|
+
const downloadPath = join(cacheDir, assetName);
|
|
93
|
+
const _assetUrl = await this.resolveAssetUrl(tag, assetName);
|
|
94
|
+
|
|
95
|
+
const dlProc = Bun.spawnSync(
|
|
96
|
+
[
|
|
97
|
+
"gh",
|
|
98
|
+
"release",
|
|
99
|
+
"download",
|
|
100
|
+
tag,
|
|
101
|
+
"--repo",
|
|
102
|
+
this.config.repo,
|
|
103
|
+
"--pattern",
|
|
104
|
+
assetName,
|
|
105
|
+
"--output",
|
|
106
|
+
downloadPath,
|
|
107
|
+
],
|
|
108
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (dlProc.exitCode !== 0) {
|
|
112
|
+
const stderr = new TextDecoder().decode(dlProc.stderr);
|
|
113
|
+
throw new Error(`Failed to download ${assetName} from ${tag}: ${stderr}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Extract the tarball
|
|
117
|
+
const extractProc = Bun.spawnSync(
|
|
118
|
+
["tar", "-xzf", downloadPath, "-C", cacheDir],
|
|
119
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (extractProc.exitCode !== 0) {
|
|
123
|
+
throw new Error(`Failed to extract ${assetName}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await access(artifactDir);
|
|
128
|
+
} catch {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Artifact "${name}" not found in ${assetName} for harness "${harness}"`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return artifactDir;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async listVersions(): Promise<string[]> {
|
|
138
|
+
const proc = Bun.spawnSync(
|
|
139
|
+
[
|
|
140
|
+
"gh",
|
|
141
|
+
"release",
|
|
142
|
+
"list",
|
|
143
|
+
"--repo",
|
|
144
|
+
this.config.repo,
|
|
145
|
+
"--json",
|
|
146
|
+
"tagName",
|
|
147
|
+
"--limit",
|
|
148
|
+
"20",
|
|
149
|
+
],
|
|
150
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (proc.exitCode !== 0) return [];
|
|
154
|
+
|
|
155
|
+
const releases = JSON.parse(
|
|
156
|
+
new TextDecoder().decode(proc.stdout),
|
|
157
|
+
) as Array<{ tagName: string }>;
|
|
158
|
+
return releases.map((r) => r.tagName);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async latestTag(): Promise<string> {
|
|
162
|
+
const proc = Bun.spawnSync(
|
|
163
|
+
[
|
|
164
|
+
"gh",
|
|
165
|
+
"release",
|
|
166
|
+
"view",
|
|
167
|
+
"--repo",
|
|
168
|
+
this.config.repo,
|
|
169
|
+
"--json",
|
|
170
|
+
"tagName",
|
|
171
|
+
],
|
|
172
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (proc.exitCode !== 0) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Could not determine latest release for ${this.config.repo}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const data = JSON.parse(new TextDecoder().decode(proc.stdout)) as {
|
|
182
|
+
tagName: string;
|
|
183
|
+
};
|
|
184
|
+
return data.tagName;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async resolveAssetUrl(
|
|
188
|
+
_tag: string,
|
|
189
|
+
_assetName: string,
|
|
190
|
+
): Promise<string> {
|
|
191
|
+
return `repos/${this.config.repo}/releases/assets`; // resolved via gh CLI
|
|
192
|
+
// Note: actual URL resolution handled by gh CLI download command
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { CatalogEntry, HarnessName } from "../schemas";
|
|
5
|
+
import type { ArtifactBackend, HttpBackendConfig } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generic HTTPS backend — works with any HTTP server that hosts kanon artifacts
|
|
9
|
+
* in the standard layout:
|
|
10
|
+
*
|
|
11
|
+
* <baseUrl>/catalog.json
|
|
12
|
+
* <baseUrl>/<version>/catalog.json
|
|
13
|
+
* <baseUrl>/<version>/dist-<harness>.tar.gz
|
|
14
|
+
* <baseUrl>/<version>/release-manifest.json
|
|
15
|
+
*
|
|
16
|
+
* Bearer token auth is optional. Token can reference an env var via ${ENV_VAR} syntax.
|
|
17
|
+
*/
|
|
18
|
+
export class HttpBackend implements ArtifactBackend {
|
|
19
|
+
readonly label: string;
|
|
20
|
+
private readonly token: string | undefined;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly config: HttpBackendConfig,
|
|
24
|
+
private readonly version?: string,
|
|
25
|
+
) {
|
|
26
|
+
this.label = `http:${config.baseUrl}${version ? `@${version}` : ""}`;
|
|
27
|
+
this.token = this.resolveToken(config.token);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async fetchCatalog(): Promise<CatalogEntry[]> {
|
|
31
|
+
const url = this.version
|
|
32
|
+
? `${this.config.baseUrl}/${this.version}/catalog.json`
|
|
33
|
+
: `${this.config.baseUrl}/catalog.json`;
|
|
34
|
+
|
|
35
|
+
const res = await this.fetchUrl(url);
|
|
36
|
+
return res.json() as Promise<CatalogEntry[]>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async fetchArtifact(
|
|
40
|
+
name: string,
|
|
41
|
+
harness: HarnessName,
|
|
42
|
+
version?: string,
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
const ver = version ?? this.version ?? "latest";
|
|
45
|
+
const assetName = `dist-${harness}.tar.gz`;
|
|
46
|
+
const url = `${this.config.baseUrl}/${ver}/${assetName}`;
|
|
47
|
+
|
|
48
|
+
const cacheDir = join(
|
|
49
|
+
process.env.HOME ?? tmpdir(),
|
|
50
|
+
".forge",
|
|
51
|
+
"cache",
|
|
52
|
+
`http-${this.config.baseUrl.replace(/[^a-z0-9]/gi, "-")}`,
|
|
53
|
+
ver,
|
|
54
|
+
harness,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const artifactDir = join(cacheDir, name);
|
|
58
|
+
if (await Bun.file(artifactDir).exists()) {
|
|
59
|
+
return artifactDir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await mkdir(cacheDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const tarPath = join(cacheDir, assetName);
|
|
65
|
+
const res = await this.fetchUrl(url);
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(`HTTP ${res.status} downloading ${url}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const buffer = await res.arrayBuffer();
|
|
71
|
+
await writeFile(tarPath, Buffer.from(buffer));
|
|
72
|
+
|
|
73
|
+
const extractProc = Bun.spawnSync(
|
|
74
|
+
["tar", "-xzf", tarPath, "-C", cacheDir],
|
|
75
|
+
{
|
|
76
|
+
stdout: "pipe",
|
|
77
|
+
stderr: "pipe",
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (extractProc.exitCode !== 0) {
|
|
82
|
+
throw new Error(`Failed to extract ${assetName}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!(await Bun.file(artifactDir).exists())) {
|
|
86
|
+
throw new Error(`Artifact "${name}" not found in ${assetName}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return artifactDir;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async listVersions(): Promise<string[]> {
|
|
93
|
+
try {
|
|
94
|
+
const url = `${this.config.baseUrl}/versions.json`;
|
|
95
|
+
const res = await this.fetchUrl(url);
|
|
96
|
+
if (!res.ok) return [];
|
|
97
|
+
return res.json() as Promise<string[]>;
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async fetchUrl(url: string): Promise<Response> {
|
|
104
|
+
const headers: Record<string, string> = {
|
|
105
|
+
Accept: "application/json, application/octet-stream",
|
|
106
|
+
};
|
|
107
|
+
if (this.token) {
|
|
108
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
109
|
+
}
|
|
110
|
+
return fetch(url, { headers });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private resolveToken(raw: string | undefined): string | undefined {
|
|
114
|
+
if (!raw) return undefined;
|
|
115
|
+
// Support ${ENV_VAR} syntax for referencing environment variables
|
|
116
|
+
const match = raw.match(/^\$\{(.+)\}$/);
|
|
117
|
+
if (match) {
|
|
118
|
+
return process.env[match[1]];
|
|
119
|
+
}
|
|
120
|
+
return raw;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { GitHubBackend } from "./github";
|
|
2
|
+
import { HttpBackend } from "./http";
|
|
3
|
+
import { LocalBackend } from "./local";
|
|
4
|
+
import { S3Backend } from "./s3";
|
|
5
|
+
import type { ArtifactBackend, BackendConfig } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a BackendConfig into a concrete ArtifactBackend instance.
|
|
9
|
+
*
|
|
10
|
+
* @param config - The backend configuration from kanon.config.yaml or CLI flags
|
|
11
|
+
* @param version - Optional version/tag to pin to (passed through to backends that support it)
|
|
12
|
+
*/
|
|
13
|
+
export function resolveBackend(
|
|
14
|
+
config: BackendConfig,
|
|
15
|
+
version?: string,
|
|
16
|
+
): ArtifactBackend {
|
|
17
|
+
switch (config.type) {
|
|
18
|
+
case "local":
|
|
19
|
+
return new LocalBackend(config.path);
|
|
20
|
+
case "github":
|
|
21
|
+
return new GitHubBackend(config, version);
|
|
22
|
+
case "s3":
|
|
23
|
+
return new S3Backend(config, version);
|
|
24
|
+
case "http":
|
|
25
|
+
return new HttpBackend(config, version);
|
|
26
|
+
default: {
|
|
27
|
+
const _exhaustive: never = config;
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Unknown backend type: ${(_exhaustive as { type: string }).type}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { GitHubBackend } from "./github";
|
|
36
|
+
export { HttpBackend } from "./http";
|
|
37
|
+
export { LocalBackend } from "./local";
|
|
38
|
+
export { S3Backend } from "./s3";
|
|
39
|
+
export type { ArtifactBackend, BackendConfig };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { exists } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { generateCatalog } from "../catalog";
|
|
4
|
+
import type { CatalogEntry, HarnessName } from "../schemas";
|
|
5
|
+
import type { ArtifactBackend } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Local filesystem backend — reads from an existing dist/ directory.
|
|
9
|
+
* This is the default backend for `kanon install` when no remote source is configured.
|
|
10
|
+
*/
|
|
11
|
+
export class LocalBackend implements ArtifactBackend {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
|
|
14
|
+
constructor(private readonly basePath: string) {
|
|
15
|
+
this.label = `local:${basePath}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async fetchCatalog(): Promise<CatalogEntry[]> {
|
|
19
|
+
// Try catalog.json first (faster), fall back to scanning source dirs
|
|
20
|
+
const catalogPath = join(this.basePath, "catalog.json");
|
|
21
|
+
if (await exists(catalogPath)) {
|
|
22
|
+
const raw = await Bun.file(catalogPath).text();
|
|
23
|
+
return JSON.parse(raw) as CatalogEntry[];
|
|
24
|
+
}
|
|
25
|
+
// Fall back to generating from source
|
|
26
|
+
return generateCatalog([
|
|
27
|
+
join(this.basePath, "knowledge"),
|
|
28
|
+
join(this.basePath, "packages"),
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async fetchArtifact(name: string, harness: HarnessName): Promise<string> {
|
|
33
|
+
const artifactDistDir = join(this.basePath, "dist", harness, name);
|
|
34
|
+
if (!(await exists(artifactDistDir))) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Artifact "${name}" for harness "${harness}" not found at ${artifactDistDir}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
// For local backend, return the path directly (no copy needed)
|
|
40
|
+
return artifactDistDir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async listVersions(): Promise<string[]> {
|
|
44
|
+
// Local backend doesn't have versions; return current as "local"
|
|
45
|
+
return ["local"];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { CatalogEntry, HarnessName } from "../schemas";
|
|
5
|
+
import type { ArtifactBackend, S3BackendConfig } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* S3 backend — reads from an S3 bucket (or any S3-compatible store like R2 or MinIO).
|
|
9
|
+
*
|
|
10
|
+
* Expected bucket layout:
|
|
11
|
+
* <prefix>/catalog.json
|
|
12
|
+
* <prefix>/<version>/catalog.json
|
|
13
|
+
* <prefix>/<version>/dist-<harness>.tar.gz
|
|
14
|
+
* <prefix>/<version>/release-manifest.json
|
|
15
|
+
*
|
|
16
|
+
* Credentials: standard AWS credential chain (env vars, ~/.aws/credentials, IAM role).
|
|
17
|
+
* For read-only public access, no credentials are required.
|
|
18
|
+
* For presigned URL access, set the FORGE_S3_<BACKEND_NAME>_URL env var.
|
|
19
|
+
*/
|
|
20
|
+
export class S3Backend implements ArtifactBackend {
|
|
21
|
+
readonly label: string;
|
|
22
|
+
private readonly prefix: string;
|
|
23
|
+
private readonly region: string;
|
|
24
|
+
private readonly endpoint: string | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly config: S3BackendConfig,
|
|
28
|
+
private readonly version?: string,
|
|
29
|
+
) {
|
|
30
|
+
this.prefix = config.prefix?.replace(/\/$/, "") ?? "";
|
|
31
|
+
this.region =
|
|
32
|
+
config.region ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1";
|
|
33
|
+
this.endpoint = config.endpoint;
|
|
34
|
+
this.label = `s3://${config.bucket}/${this.prefix}${version ? `@${version}` : ""}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async fetchCatalog(): Promise<CatalogEntry[]> {
|
|
38
|
+
const key = this.version
|
|
39
|
+
? `${this.prefix}/${this.version}/catalog.json`
|
|
40
|
+
: `${this.prefix}/catalog.json`;
|
|
41
|
+
|
|
42
|
+
const content = await this.s3Get(key);
|
|
43
|
+
return JSON.parse(content) as CatalogEntry[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async fetchArtifact(
|
|
47
|
+
name: string,
|
|
48
|
+
harness: HarnessName,
|
|
49
|
+
version?: string,
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
const ver = version ?? this.version ?? "latest";
|
|
52
|
+
const assetName = `dist-${harness}.tar.gz`;
|
|
53
|
+
const key = `${this.prefix}/${ver}/${assetName}`;
|
|
54
|
+
|
|
55
|
+
const cacheDir = join(
|
|
56
|
+
process.env.HOME ?? tmpdir(),
|
|
57
|
+
".forge",
|
|
58
|
+
"cache",
|
|
59
|
+
`s3-${this.config.bucket.replace(/[^a-z0-9]/gi, "-")}`,
|
|
60
|
+
ver,
|
|
61
|
+
harness,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const artifactDir = join(cacheDir, name);
|
|
65
|
+
if (await Bun.file(artifactDir).exists()) {
|
|
66
|
+
return artifactDir;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await mkdir(cacheDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
const tarPath = join(cacheDir, assetName);
|
|
72
|
+
await this.s3Download(key, tarPath);
|
|
73
|
+
|
|
74
|
+
const extractProc = Bun.spawnSync(
|
|
75
|
+
["tar", "-xzf", tarPath, "-C", cacheDir],
|
|
76
|
+
{
|
|
77
|
+
stdout: "pipe",
|
|
78
|
+
stderr: "pipe",
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (extractProc.exitCode !== 0) {
|
|
83
|
+
throw new Error(`Failed to extract ${assetName} from S3`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!(await Bun.file(artifactDir).exists())) {
|
|
87
|
+
throw new Error(`Artifact "${name}" not found in ${assetName}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return artifactDir;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async listVersions(): Promise<string[]> {
|
|
94
|
+
// List common prefixes under the backend prefix to find version dirs
|
|
95
|
+
const args = this.buildAwsArgs([
|
|
96
|
+
"s3api",
|
|
97
|
+
"list-objects-v2",
|
|
98
|
+
"--bucket",
|
|
99
|
+
this.config.bucket,
|
|
100
|
+
"--prefix",
|
|
101
|
+
`${this.prefix}/`,
|
|
102
|
+
"--delimiter",
|
|
103
|
+
"/",
|
|
104
|
+
"--query",
|
|
105
|
+
"CommonPrefixes[].Prefix",
|
|
106
|
+
"--output",
|
|
107
|
+
"json",
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
const proc = Bun.spawnSync(args, { stdout: "pipe", stderr: "pipe" });
|
|
111
|
+
if (proc.exitCode !== 0) return [];
|
|
112
|
+
|
|
113
|
+
const prefixes = JSON.parse(new TextDecoder().decode(proc.stdout)) as
|
|
114
|
+
| string[]
|
|
115
|
+
| null;
|
|
116
|
+
if (!prefixes) return [];
|
|
117
|
+
|
|
118
|
+
// Extract version from prefix: "<prefix>/<version>/" → "<version>"
|
|
119
|
+
return prefixes
|
|
120
|
+
.map((p) => p.replace(`${this.prefix}/`, "").replace(/\/$/, ""))
|
|
121
|
+
.filter((v) => v.length > 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async s3Get(key: string): Promise<string> {
|
|
125
|
+
const args = this.buildAwsArgs([
|
|
126
|
+
"s3api",
|
|
127
|
+
"get-object",
|
|
128
|
+
"--bucket",
|
|
129
|
+
this.config.bucket,
|
|
130
|
+
"--key",
|
|
131
|
+
key,
|
|
132
|
+
"/dev/stdout",
|
|
133
|
+
]);
|
|
134
|
+
const proc = Bun.spawnSync(args, { stdout: "pipe", stderr: "pipe" });
|
|
135
|
+
if (proc.exitCode !== 0) {
|
|
136
|
+
throw new Error(`S3 GET failed for s3://${this.config.bucket}/${key}`);
|
|
137
|
+
}
|
|
138
|
+
return new TextDecoder().decode(proc.stdout);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async s3Download(key: string, destPath: string): Promise<void> {
|
|
142
|
+
const s3Uri = `s3://${this.config.bucket}/${key}`;
|
|
143
|
+
const args = this.buildAwsArgs(["s3", "cp", s3Uri, destPath]);
|
|
144
|
+
const proc = Bun.spawnSync(args, { stdout: "pipe", stderr: "pipe" });
|
|
145
|
+
if (proc.exitCode !== 0) {
|
|
146
|
+
const stderr = new TextDecoder().decode(proc.stderr);
|
|
147
|
+
throw new Error(`S3 download failed for ${s3Uri}: ${stderr}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private buildAwsArgs(base: string[]): string[] {
|
|
152
|
+
const args = [...base];
|
|
153
|
+
if (this.region) args.push("--region", this.region);
|
|
154
|
+
if (this.endpoint) args.push("--endpoint-url", this.endpoint);
|
|
155
|
+
return args;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { CatalogEntry, HarnessName } from "../schemas";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A backend is a source from which artifact dist files can be fetched and
|
|
5
|
+
* to which they can be published. All install sources — local filesystem,
|
|
6
|
+
* GitHub releases, S3 buckets, HTTP endpoints — implement this interface.
|
|
7
|
+
*/
|
|
8
|
+
export interface ArtifactBackend {
|
|
9
|
+
/** Human-readable label, e.g. "github:my-org/kanon@v1.2.0" */
|
|
10
|
+
readonly label: string;
|
|
11
|
+
|
|
12
|
+
/** Fetch and parse the catalog from this backend. */
|
|
13
|
+
fetchCatalog(): Promise<CatalogEntry[]>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Download the pre-built dist for a single artifact and harness.
|
|
17
|
+
* Returns the path to a local temporary directory containing the artifact files.
|
|
18
|
+
*/
|
|
19
|
+
fetchArtifact(
|
|
20
|
+
name: string,
|
|
21
|
+
harness: HarnessName,
|
|
22
|
+
version?: string,
|
|
23
|
+
): Promise<string>;
|
|
24
|
+
|
|
25
|
+
/** List available version identifiers (semver tags, S3 prefixes, etc.). */
|
|
26
|
+
listVersions(): Promise<string[]>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Configuration shapes for each backend type (as stored in kanon.config.yaml). */
|
|
30
|
+
export interface GitHubBackendConfig {
|
|
31
|
+
type: "github";
|
|
32
|
+
repo: string; // e.g. "my-org/kanon-artifacts"
|
|
33
|
+
releasePrefix?: string; // e.g. "v" — default "v"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface S3BackendConfig {
|
|
37
|
+
type: "s3";
|
|
38
|
+
bucket: string;
|
|
39
|
+
prefix?: string; // key prefix, e.g. "skills/"
|
|
40
|
+
region?: string; // default: AWS_DEFAULT_REGION env
|
|
41
|
+
endpoint?: string; // for S3-compatible services (MinIO, R2, etc.)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface HttpBackendConfig {
|
|
45
|
+
type: "http";
|
|
46
|
+
baseUrl: string; // e.g. "https://artifacts.mycompany.com/forge"
|
|
47
|
+
token?: string; // bearer token (or read from env via ${ENV_VAR})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface LocalBackendConfig {
|
|
51
|
+
type: "local";
|
|
52
|
+
path: string; // path to a kanon repo or dist directory
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type BackendConfig =
|
|
56
|
+
| GitHubBackendConfig
|
|
57
|
+
| S3BackendConfig
|
|
58
|
+
| HttpBackendConfig
|
|
59
|
+
| LocalBackendConfig;
|