@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,61 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
import type { CanonicalHook, McpServerDefinition } from "./schemas";
|
|
5
|
+
import type { WizardResult } from "./wizard";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Serialize frontmatter and body into a gray-matter-compatible markdown string.
|
|
9
|
+
*/
|
|
10
|
+
export function buildKnowledgeMd(result: WizardResult): string {
|
|
11
|
+
const frontmatterYaml = yaml.dump(result.frontmatter, { lineWidth: -1 });
|
|
12
|
+
const body =
|
|
13
|
+
result.knowledgeBody.trim().length > 0
|
|
14
|
+
? result.knowledgeBody
|
|
15
|
+
: "TODO: Add your knowledge content here";
|
|
16
|
+
return `---\n${frontmatterYaml}---\n${body}\n`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Serialize hooks array to YAML. Empty array produces `[]\n`.
|
|
21
|
+
*/
|
|
22
|
+
export function buildHooksYaml(hooks: CanonicalHook[]): string {
|
|
23
|
+
if (hooks.length === 0) {
|
|
24
|
+
return "[]\n";
|
|
25
|
+
}
|
|
26
|
+
return yaml.dump(hooks);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Serialize MCP servers array to YAML. Empty array produces `[]\n`.
|
|
31
|
+
*/
|
|
32
|
+
export function buildMcpServersYaml(servers: McpServerDefinition[]): string {
|
|
33
|
+
if (servers.length === 0) {
|
|
34
|
+
return "[]\n";
|
|
35
|
+
}
|
|
36
|
+
return yaml.dump(servers);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Write wizard results to the artifact directory.
|
|
41
|
+
* Overwrites knowledge.md, hooks.yaml, and mcp-servers.yaml.
|
|
42
|
+
* Returns the list of written file paths.
|
|
43
|
+
*/
|
|
44
|
+
export async function writeWizardResult(
|
|
45
|
+
artifactDir: string,
|
|
46
|
+
result: WizardResult,
|
|
47
|
+
): Promise<string[]> {
|
|
48
|
+
const knowledgeMd = buildKnowledgeMd(result);
|
|
49
|
+
const hooksYaml = buildHooksYaml(result.hooks);
|
|
50
|
+
const mcpServersYaml = buildMcpServersYaml(result.mcpServers);
|
|
51
|
+
|
|
52
|
+
const knowledgePath = join(artifactDir, "knowledge.md");
|
|
53
|
+
const hooksPath = join(artifactDir, "hooks.yaml");
|
|
54
|
+
const mcpServersPath = join(artifactDir, "mcp-servers.yaml");
|
|
55
|
+
|
|
56
|
+
await writeFile(knowledgePath, knowledgeMd, "utf-8");
|
|
57
|
+
await writeFile(hooksPath, hooksYaml, "utf-8");
|
|
58
|
+
await writeFile(mcpServersPath, mcpServersYaml, "utf-8");
|
|
59
|
+
|
|
60
|
+
return [knowledgePath, hooksPath, mcpServersPath];
|
|
61
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-harness format definitions and resolution.
|
|
3
|
+
*
|
|
4
|
+
* This module is now a **projection** over the Rosetta Stone built-in format
|
|
5
|
+
* contracts (ADR-RS-002). The public types, harness names, variants, defaults,
|
|
6
|
+
* sorted valid choices, and Kiro power deprecation behavior are preserved — but
|
|
7
|
+
* the data is derived from `src/rosetta/builtins/contracts.ts` rather than
|
|
8
|
+
* independently declared.
|
|
9
|
+
*
|
|
10
|
+
* Requirements: 2.9, 6.3, 14.5, 14.6, 14.7
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { HarnessName } from "./schemas";
|
|
14
|
+
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// Public Types (preserved)
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
export interface HarnessFormatDef {
|
|
20
|
+
formats: readonly string[];
|
|
21
|
+
default: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ResolveFormatResult {
|
|
25
|
+
format: string;
|
|
26
|
+
deprecationWarning?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// Registry Projection (lazy to avoid circular initialization)
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cached registry built on first access.
|
|
35
|
+
* Lazy initialization breaks the circular dependency chain:
|
|
36
|
+
* adapters/index → format-registry → rosetta/builtins/contracts →
|
|
37
|
+
* rosetta/builtins/compatibility-profiles → adapters/capabilities
|
|
38
|
+
*/
|
|
39
|
+
let _cachedRegistry: Record<HarnessName, HarnessFormatDef> | null = null;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build `HARNESS_FORMAT_REGISTRY` by projecting harness-bound built-in
|
|
43
|
+
* contracts. Each harness entry derives its `formats` (sorted variant keys)
|
|
44
|
+
* and `default` (contract's declared defaultVariant) from the frozen contracts.
|
|
45
|
+
*/
|
|
46
|
+
function getRegistry(): Record<HarnessName, HarnessFormatDef> {
|
|
47
|
+
if (_cachedRegistry !== null) {
|
|
48
|
+
return _cachedRegistry;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { BUILTIN_FORMAT_CONTRACTS } = require("./rosetta/builtins/contracts");
|
|
52
|
+
const registry: Partial<Record<HarnessName, HarnessFormatDef>> = {};
|
|
53
|
+
|
|
54
|
+
for (const contract of BUILTIN_FORMAT_CONTRACTS) {
|
|
55
|
+
if (contract.harness === null) continue;
|
|
56
|
+
|
|
57
|
+
const harness = contract.harness as HarnessName;
|
|
58
|
+
|
|
59
|
+
// Only use the primary harness contract (direction: bidirectional or target)
|
|
60
|
+
// Skip source-only contracts like kiro-power, kiro-skill
|
|
61
|
+
if (contract.direction === "source") continue;
|
|
62
|
+
|
|
63
|
+
const variantKeys = Object.keys(contract.variants);
|
|
64
|
+
const defaultVariant = contract.defaultVariant as string;
|
|
65
|
+
|
|
66
|
+
registry[harness] = {
|
|
67
|
+
formats: variantKeys,
|
|
68
|
+
default: defaultVariant,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_cachedRegistry = registry as Record<HarnessName, HarnessFormatDef>;
|
|
73
|
+
return _cachedRegistry;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format registry mapping each supported harness to its available formats
|
|
78
|
+
* and default format. Projected from built-in Rosetta Stone format contracts.
|
|
79
|
+
*
|
|
80
|
+
* Uses a Proxy to lazily initialize on first property access, breaking the
|
|
81
|
+
* circular dependency chain at module load time.
|
|
82
|
+
*/
|
|
83
|
+
export const HARNESS_FORMAT_REGISTRY: Record<HarnessName, HarnessFormatDef> =
|
|
84
|
+
new Proxy({} as Record<HarnessName, HarnessFormatDef>, {
|
|
85
|
+
get(_target, prop) {
|
|
86
|
+
return getRegistry()[prop as HarnessName];
|
|
87
|
+
},
|
|
88
|
+
ownKeys() {
|
|
89
|
+
return Reflect.ownKeys(getRegistry());
|
|
90
|
+
},
|
|
91
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
92
|
+
const registry = getRegistry();
|
|
93
|
+
if (prop in registry) {
|
|
94
|
+
return {
|
|
95
|
+
value: registry[prop as HarnessName],
|
|
96
|
+
writable: true,
|
|
97
|
+
enumerable: true,
|
|
98
|
+
configurable: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
},
|
|
103
|
+
has(_target, prop) {
|
|
104
|
+
return prop in getRegistry();
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
109
|
+
// resolveFormat — delegates to Rosetta Stone resolution semantics
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the output format for a harness from its harness-config section.
|
|
114
|
+
* Falls back to the registry default. Handles Kiro `power: true` backward compat.
|
|
115
|
+
*
|
|
116
|
+
* This function delegates to the same precedence logic used by Rosetta Stone's
|
|
117
|
+
* variant resolution (src/rosetta/resolution.ts) but provides the legacy
|
|
118
|
+
* `ResolveFormatResult` shape for backward compatibility.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveFormat(
|
|
121
|
+
harness: HarnessName,
|
|
122
|
+
harnessConfig: Record<string, unknown> | undefined,
|
|
123
|
+
): ResolveFormatResult {
|
|
124
|
+
// Explicit format field — highest precedence
|
|
125
|
+
if (harnessConfig?.format !== undefined) {
|
|
126
|
+
return { format: harnessConfig.format as string };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Kiro backward compatibility: power: true without format field
|
|
130
|
+
if (harness === "kiro" && harnessConfig?.power === true) {
|
|
131
|
+
return {
|
|
132
|
+
format: "power",
|
|
133
|
+
deprecationWarning:
|
|
134
|
+
'harness-config.kiro.power is deprecated. Migrate to format: "power" in harness-config.kiro.',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Fall back to the projected default from the registry
|
|
139
|
+
const registry = getRegistry();
|
|
140
|
+
return { format: registry[harness].default };
|
|
141
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Auto-Updater — throttled remote version checks, download to GlobalCache
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { resolveBackend } from "../backends/index";
|
|
6
|
+
import type { BackendConfig } from "../backends/types";
|
|
7
|
+
import { SUPPORTED_HARNESSES } from "../schemas";
|
|
8
|
+
import type { GlobalCacheAPI } from "./global-cache";
|
|
9
|
+
import type { ManifestEntry } from "./manifest";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for the auto-update check.
|
|
13
|
+
*/
|
|
14
|
+
export interface AutoUpdateOptions {
|
|
15
|
+
/** Minimum minutes between remote checks (default 60). */
|
|
16
|
+
throttleMinutes: number;
|
|
17
|
+
/** Global cache instance for reading/writing artifacts and throttle state. */
|
|
18
|
+
cache: GlobalCacheAPI;
|
|
19
|
+
/** Available backend configurations keyed by name. */
|
|
20
|
+
configBackends: Map<string, BackendConfig>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check backends for newer versions and download to cache.
|
|
25
|
+
* Respects throttle interval. Silently falls back on network failure.
|
|
26
|
+
*
|
|
27
|
+
* @returns List of updated artifact names and whether the check was skipped.
|
|
28
|
+
*/
|
|
29
|
+
export async function autoUpdate(
|
|
30
|
+
entries: ManifestEntry[],
|
|
31
|
+
manifestBackend: string | undefined,
|
|
32
|
+
options: AutoUpdateOptions,
|
|
33
|
+
): Promise<{ updated: string[]; skipped: boolean }> {
|
|
34
|
+
const { throttleMinutes, cache, configBackends } = options;
|
|
35
|
+
|
|
36
|
+
// --- Throttle check (Req 6.4, 6.7) ---
|
|
37
|
+
const lastCheck = await cache.readThrottleState();
|
|
38
|
+
if (lastCheck !== null) {
|
|
39
|
+
const elapsedMs = Date.now() - lastCheck.getTime();
|
|
40
|
+
const intervalMs = throttleMinutes * 60 * 1000;
|
|
41
|
+
if (elapsedMs < intervalMs) {
|
|
42
|
+
return { updated: [], skipped: true };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const updated: string[] = [];
|
|
47
|
+
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
try {
|
|
50
|
+
await processEntry(
|
|
51
|
+
entry,
|
|
52
|
+
manifestBackend,
|
|
53
|
+
configBackends,
|
|
54
|
+
cache,
|
|
55
|
+
updated,
|
|
56
|
+
);
|
|
57
|
+
} catch {
|
|
58
|
+
// Silently fall back to cache on network failure (Req 6.5, 10.2)
|
|
59
|
+
// No user-visible error — continue with next entry
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Write throttle state after check completes (Req 6.3)
|
|
64
|
+
await cache.writeThrottleState(new Date());
|
|
65
|
+
|
|
66
|
+
return { updated, skipped: false };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Internal helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the backend name for a manifest entry using the precedence chain:
|
|
75
|
+
* entry-level → manifest-level → first available in configBackends (Req 2.9, 12.1).
|
|
76
|
+
*/
|
|
77
|
+
function resolveBackendName(
|
|
78
|
+
entry: ManifestEntry,
|
|
79
|
+
manifestBackend: string | undefined,
|
|
80
|
+
configBackends: Map<string, BackendConfig>,
|
|
81
|
+
): string | undefined {
|
|
82
|
+
// 1. Entry-level backend
|
|
83
|
+
if (entry.backend) return entry.backend;
|
|
84
|
+
// 2. Manifest-level backend
|
|
85
|
+
if (manifestBackend) return manifestBackend;
|
|
86
|
+
// 3. First available backend in config
|
|
87
|
+
const firstKey = configBackends.keys().next();
|
|
88
|
+
return firstKey.done ? undefined : firstKey.value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Process a single manifest entry: check for newer remote versions and
|
|
93
|
+
* download them to the cache if found.
|
|
94
|
+
*/
|
|
95
|
+
async function processEntry(
|
|
96
|
+
entry: ManifestEntry,
|
|
97
|
+
manifestBackend: string | undefined,
|
|
98
|
+
configBackends: Map<string, BackendConfig>,
|
|
99
|
+
cache: GlobalCacheAPI,
|
|
100
|
+
updated: string[],
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
// Skip collection refs — they are expanded elsewhere
|
|
103
|
+
if ("collection" in entry) return;
|
|
104
|
+
|
|
105
|
+
const artifactName = entry.name;
|
|
106
|
+
const versionPin = entry.version;
|
|
107
|
+
|
|
108
|
+
// Resolve which backend to use
|
|
109
|
+
const backendName = resolveBackendName(
|
|
110
|
+
entry,
|
|
111
|
+
manifestBackend,
|
|
112
|
+
configBackends,
|
|
113
|
+
);
|
|
114
|
+
if (!backendName) return;
|
|
115
|
+
|
|
116
|
+
const backendConfig = configBackends.get(backendName);
|
|
117
|
+
if (!backendConfig) return;
|
|
118
|
+
|
|
119
|
+
const backend = resolveBackend(backendConfig);
|
|
120
|
+
|
|
121
|
+
// Fetch remote versions (Req 6.1, 12.2)
|
|
122
|
+
const remoteVersions = await backend.listVersions();
|
|
123
|
+
|
|
124
|
+
// Find versions that satisfy the pin
|
|
125
|
+
const satisfying = remoteVersions.filter((v) =>
|
|
126
|
+
Bun.semver.satisfies(v, versionPin),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (satisfying.length === 0) return;
|
|
130
|
+
|
|
131
|
+
// Pick the highest satisfying version
|
|
132
|
+
const sorted = satisfying.sort(Bun.semver.order);
|
|
133
|
+
const newestRemote = sorted[sorted.length - 1];
|
|
134
|
+
|
|
135
|
+
// Check if this version is already in the cache
|
|
136
|
+
const alreadyCached = await cache.has(artifactName, newestRemote);
|
|
137
|
+
if (alreadyCached) return;
|
|
138
|
+
|
|
139
|
+
// Determine harnesses to fetch
|
|
140
|
+
const harnesses = entry.harnesses ?? [...SUPPORTED_HARNESSES];
|
|
141
|
+
|
|
142
|
+
// Download each harness dist and store in cache (Req 6.2, 12.3)
|
|
143
|
+
for (const harness of harnesses) {
|
|
144
|
+
try {
|
|
145
|
+
const sourceDir = await backend.fetchArtifact(
|
|
146
|
+
artifactName,
|
|
147
|
+
harness as Parameters<typeof backend.fetchArtifact>[1],
|
|
148
|
+
newestRemote,
|
|
149
|
+
);
|
|
150
|
+
await cache.store(
|
|
151
|
+
artifactName,
|
|
152
|
+
newestRemote,
|
|
153
|
+
harness,
|
|
154
|
+
sourceDir,
|
|
155
|
+
backendName,
|
|
156
|
+
);
|
|
157
|
+
} catch {
|
|
158
|
+
// Silently skip individual harness fetch failures (Req 6.5)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
updated.push(artifactName);
|
|
163
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Backend Resolution — shared logic for resolving backend names
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import type { BackendConfig } from "../backends/types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Result of resolving a backend name for a manifest entry.
|
|
9
|
+
*/
|
|
10
|
+
export interface BackendResolution {
|
|
11
|
+
name: string;
|
|
12
|
+
config: BackendConfig;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the backend for a manifest entry using the precedence chain:
|
|
17
|
+
* entry-level → manifest-level → config-level default (first key).
|
|
18
|
+
*
|
|
19
|
+
* Throws if the resolved backend name is not defined in configBackends (Req 12.5).
|
|
20
|
+
* Returns `null` if no backend can be resolved at all.
|
|
21
|
+
*
|
|
22
|
+
* Requirements: 12.1, 12.2, 12.3, 12.4, 12.5
|
|
23
|
+
*/
|
|
24
|
+
export function resolveEntryBackend(
|
|
25
|
+
entryBackend: string | undefined,
|
|
26
|
+
manifestBackend: string | undefined,
|
|
27
|
+
configBackends: Map<string, BackendConfig>,
|
|
28
|
+
): BackendResolution | null {
|
|
29
|
+
// Precedence: entry → manifest → first config key
|
|
30
|
+
const name = entryBackend ?? manifestBackend ?? firstKey(configBackends);
|
|
31
|
+
|
|
32
|
+
if (!name) return null;
|
|
33
|
+
|
|
34
|
+
const config = configBackends.get(name);
|
|
35
|
+
if (!config) {
|
|
36
|
+
const available = [...configBackends.keys()];
|
|
37
|
+
const availableStr = available.length > 0 ? available.join(", ") : "(none)";
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Unknown backend "${name}". Available backends: ${availableStr}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { name, config };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function firstKey(map: Map<string, unknown>): string | undefined {
|
|
47
|
+
const iter = map.keys().next();
|
|
48
|
+
return iter.done ? undefined : iter.value;
|
|
49
|
+
}
|