@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
package/src/guild/cli.ts
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Guild CLI — register `forge guild` command group
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import type { Command } from "commander";
|
|
9
|
+
import { resolveBackend } from "../backends/index";
|
|
10
|
+
import { loadForgeConfig, resolveBackendConfigs } from "../config";
|
|
11
|
+
import { SUPPORTED_HARNESSES } from "../schemas";
|
|
12
|
+
import { GlobalCache } from "./global-cache";
|
|
13
|
+
import type { ShellType } from "./hook-generator";
|
|
14
|
+
import { detectShell, generateHookSnippet } from "./hook-generator";
|
|
15
|
+
import type {
|
|
16
|
+
ArtifactManifestEntry,
|
|
17
|
+
CollectionManifestEntry,
|
|
18
|
+
Manifest,
|
|
19
|
+
ManifestEntry,
|
|
20
|
+
} from "./manifest";
|
|
21
|
+
import { isCollectionRef, parseManifest, printManifest } from "./manifest";
|
|
22
|
+
import type { SyncLock } from "./sync";
|
|
23
|
+
import { sync } from "./sync";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const MANIFEST_PATH = ".forge/manifest.yaml";
|
|
30
|
+
|
|
31
|
+
async function pathExists(p: string): Promise<boolean> {
|
|
32
|
+
try {
|
|
33
|
+
await access(p);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read and parse the manifest file, or return a fresh empty manifest.
|
|
42
|
+
*/
|
|
43
|
+
async function readOrCreateManifest(): Promise<Manifest> {
|
|
44
|
+
if (await pathExists(MANIFEST_PATH)) {
|
|
45
|
+
const content = await readFile(MANIFEST_PATH, "utf-8");
|
|
46
|
+
return parseManifest(content);
|
|
47
|
+
}
|
|
48
|
+
return { artifacts: [] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ensure `.forge/` is listed in the repo-root `.gitignore`.
|
|
53
|
+
* Creates the file if it doesn't exist. Preserves existing content.
|
|
54
|
+
* (Req 4.9)
|
|
55
|
+
*/
|
|
56
|
+
async function ensureGitignoreEntry(): Promise<void> {
|
|
57
|
+
const gitignorePath = ".gitignore";
|
|
58
|
+
const entry = ".forge/";
|
|
59
|
+
let content = "";
|
|
60
|
+
|
|
61
|
+
if (await pathExists(gitignorePath)) {
|
|
62
|
+
content = await readFile(gitignorePath, "utf-8");
|
|
63
|
+
// Already present — nothing to do
|
|
64
|
+
if (content.split("\n").some((line) => line.trim() === entry)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
70
|
+
await writeFile(gitignorePath, `${content}${separator}${entry}\n`, "utf-8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Subcommand: guild init <name>
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
interface InitOptions {
|
|
78
|
+
collection?: boolean;
|
|
79
|
+
mode?: "required" | "optional";
|
|
80
|
+
version?: string;
|
|
81
|
+
backend?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function guildInit(name: string, opts: InitOptions): Promise<void> {
|
|
85
|
+
const cache = new GlobalCache();
|
|
86
|
+
const isCollection = opts.collection ?? false;
|
|
87
|
+
const mode = opts.mode ?? "required";
|
|
88
|
+
|
|
89
|
+
// Determine version pin
|
|
90
|
+
let versionPin = opts.version;
|
|
91
|
+
|
|
92
|
+
if (!versionPin) {
|
|
93
|
+
// Query cache for latest version (Req 4.7)
|
|
94
|
+
const collectionVersions = isCollection
|
|
95
|
+
? await cache.readCollectionCatalog(name, "check").then(
|
|
96
|
+
() => cache.listVersions(`collections/${name}`),
|
|
97
|
+
() => [] as string[],
|
|
98
|
+
)
|
|
99
|
+
: [];
|
|
100
|
+
const versions = isCollection
|
|
101
|
+
? collectionVersions
|
|
102
|
+
: await cache.listVersions(name);
|
|
103
|
+
if (versions.length === 0) {
|
|
104
|
+
// Auto-fetch from configured backend
|
|
105
|
+
console.error(
|
|
106
|
+
chalk.yellow(
|
|
107
|
+
`"${name}" is not in the global cache. Attempting to fetch…`,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
const config = await loadForgeConfig();
|
|
111
|
+
const backendConfigs = resolveBackendConfigs(config);
|
|
112
|
+
const backendName =
|
|
113
|
+
opts.backend ??
|
|
114
|
+
[...backendConfigs.keys()].find((k) => k !== "local") ??
|
|
115
|
+
[...backendConfigs.keys()][0];
|
|
116
|
+
const backendConfig = backendConfigs.get(backendName);
|
|
117
|
+
if (!backendConfig) {
|
|
118
|
+
console.error(
|
|
119
|
+
chalk.red("Error: No backends configured in kanon.config.yaml."),
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
const backend = resolveBackend(backendConfig);
|
|
124
|
+
let fetchedVersions: string[];
|
|
125
|
+
try {
|
|
126
|
+
fetchedVersions = await backend.listVersions();
|
|
127
|
+
} catch (err: unknown) {
|
|
128
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
129
|
+
console.error(
|
|
130
|
+
chalk.red(
|
|
131
|
+
`Error: Failed to reach backend "${backendName}": ${reason}`,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
if (fetchedVersions.length === 0) {
|
|
137
|
+
console.error(
|
|
138
|
+
chalk.red(
|
|
139
|
+
`No versions available for "${name}" from backend "${backendName}".`,
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
const latestVersion = fetchedVersions[0];
|
|
145
|
+
|
|
146
|
+
if (isCollection) {
|
|
147
|
+
// Fetch catalog, filter to collection members, store collection catalog + member artifacts
|
|
148
|
+
let catalog: import("../schemas").CatalogEntry[];
|
|
149
|
+
try {
|
|
150
|
+
catalog = await backend.fetchCatalog();
|
|
151
|
+
} catch (err: unknown) {
|
|
152
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
153
|
+
console.error(chalk.red(`Error: Failed to fetch catalog: ${reason}`));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
const members = catalog.filter((e) => e.collections.includes(name));
|
|
157
|
+
if (members.length === 0) {
|
|
158
|
+
console.error(
|
|
159
|
+
chalk.red(
|
|
160
|
+
`No artifacts belong to collection "${name}" in the catalog.`,
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
await cache.writeCatalogMeta(name, latestVersion, members);
|
|
166
|
+
let storedCount = 0;
|
|
167
|
+
for (const entry of members) {
|
|
168
|
+
for (const h of SUPPORTED_HARNESSES) {
|
|
169
|
+
try {
|
|
170
|
+
const tempDir = await backend.fetchArtifact(
|
|
171
|
+
entry.name,
|
|
172
|
+
h,
|
|
173
|
+
latestVersion,
|
|
174
|
+
);
|
|
175
|
+
await cache.store(
|
|
176
|
+
entry.name,
|
|
177
|
+
latestVersion,
|
|
178
|
+
h,
|
|
179
|
+
tempDir,
|
|
180
|
+
backendName,
|
|
181
|
+
);
|
|
182
|
+
storedCount++;
|
|
183
|
+
} catch {
|
|
184
|
+
// skip unavailable harnesses
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.error(
|
|
189
|
+
chalk.green(
|
|
190
|
+
`✓ Fetched collection "${name}" ${latestVersion} — ${members.length} artifact(s), ${storedCount} harness(es) cached.`,
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
versionPin = latestVersion;
|
|
194
|
+
} else {
|
|
195
|
+
let storedCount = 0;
|
|
196
|
+
for (const h of SUPPORTED_HARNESSES) {
|
|
197
|
+
try {
|
|
198
|
+
const tempDir = await backend.fetchArtifact(name, h, latestVersion);
|
|
199
|
+
await cache.store(name, latestVersion, h, tempDir, backendName);
|
|
200
|
+
storedCount++;
|
|
201
|
+
} catch (err: unknown) {
|
|
202
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
203
|
+
console.error(chalk.dim(` ⊘ ${h}: ${reason}`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (storedCount === 0) {
|
|
207
|
+
console.error(
|
|
208
|
+
chalk.red(
|
|
209
|
+
`Error: Failed to fetch "${name}" from backend "${backendName}".`,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
console.error(
|
|
215
|
+
chalk.green(
|
|
216
|
+
`✓ Fetched "${name}" ${latestVersion} (${storedCount} harness(es)) into global cache.`,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
versionPin = latestVersion;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// Pick the highest version (sort ascending, take last)
|
|
223
|
+
const sorted = [...versions].sort(Bun.semver.order);
|
|
224
|
+
versionPin = sorted[sorted.length - 1];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Read or create manifest
|
|
229
|
+
const manifest = await readOrCreateManifest();
|
|
230
|
+
|
|
231
|
+
// Build the new entry
|
|
232
|
+
let newEntry: ManifestEntry;
|
|
233
|
+
if (isCollection) {
|
|
234
|
+
newEntry = {
|
|
235
|
+
collection: name,
|
|
236
|
+
version: versionPin,
|
|
237
|
+
mode,
|
|
238
|
+
} as CollectionManifestEntry;
|
|
239
|
+
} else {
|
|
240
|
+
newEntry = { name, version: versionPin, mode } as ArtifactManifestEntry;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check for existing entry and update or add (Req 4.10)
|
|
244
|
+
const existingIdx = manifest.artifacts.findIndex((entry) => {
|
|
245
|
+
if (isCollection && isCollectionRef(entry)) {
|
|
246
|
+
return (entry as CollectionManifestEntry).collection === name;
|
|
247
|
+
}
|
|
248
|
+
if (!isCollection && !isCollectionRef(entry)) {
|
|
249
|
+
return (entry as ArtifactManifestEntry).name === name;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (existingIdx >= 0) {
|
|
255
|
+
manifest.artifacts[existingIdx] = newEntry;
|
|
256
|
+
console.log(
|
|
257
|
+
chalk.yellow(`Updated existing entry for "${name}" in manifest.`),
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
manifest.artifacts.push(newEntry);
|
|
261
|
+
console.log(chalk.green(`Added "${name}" to manifest.`));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Write manifest (Req 4.1, 4.2)
|
|
265
|
+
await mkdir(".forge", { recursive: true });
|
|
266
|
+
await writeFile(MANIFEST_PATH, printManifest(manifest), "utf-8");
|
|
267
|
+
|
|
268
|
+
// Ensure .forge/ in .gitignore (Req 4.9)
|
|
269
|
+
await ensureGitignoreEntry();
|
|
270
|
+
|
|
271
|
+
// Run sync for the new entry (Req 4.11)
|
|
272
|
+
console.log(chalk.cyan("Syncing..."));
|
|
273
|
+
const result = await sync({ manifestPath: MANIFEST_PATH });
|
|
274
|
+
|
|
275
|
+
for (const w of result.warnings) {
|
|
276
|
+
console.log(chalk.yellow(` ⚠ ${w}`));
|
|
277
|
+
}
|
|
278
|
+
for (const e of result.errors) {
|
|
279
|
+
console.error(chalk.red(` ✗ ${e}`));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (result.errors.length > 0) {
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log(
|
|
287
|
+
chalk.green(
|
|
288
|
+
`✓ Synced ${result.filesWritten} file(s) for "${name}" (${versionPin})`,
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Subcommand: guild sync
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
interface GuildSyncOptions {
|
|
298
|
+
autoUpdate?: boolean;
|
|
299
|
+
throttle?: string;
|
|
300
|
+
dryRun?: boolean;
|
|
301
|
+
harness?: string;
|
|
302
|
+
force?: boolean;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function guildSync(opts: GuildSyncOptions): Promise<void> {
|
|
306
|
+
const throttleMinutes = opts.throttle ? Number(opts.throttle) : 60;
|
|
307
|
+
|
|
308
|
+
const result = await sync({
|
|
309
|
+
manifestPath: MANIFEST_PATH,
|
|
310
|
+
autoUpdate: opts.autoUpdate,
|
|
311
|
+
throttleMinutes,
|
|
312
|
+
dryRun: opts.dryRun,
|
|
313
|
+
harness: opts.harness,
|
|
314
|
+
force: opts.force,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
for (const w of result.warnings) {
|
|
318
|
+
console.log(chalk.yellow(` ⚠ ${w}`));
|
|
319
|
+
}
|
|
320
|
+
for (const e of result.errors) {
|
|
321
|
+
console.error(chalk.red(` ✗ ${e}`));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (result.errors.length > 0) {
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (opts.dryRun) {
|
|
329
|
+
console.log(chalk.cyan("Dry run complete — no files written."));
|
|
330
|
+
} else {
|
|
331
|
+
console.log(chalk.green(`✓ Synced ${result.filesWritten} file(s).`));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Subcommand: guild status
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
async function guildStatus(): Promise<void> {
|
|
340
|
+
// Read manifest
|
|
341
|
+
if (!(await pathExists(MANIFEST_PATH))) {
|
|
342
|
+
console.error(
|
|
343
|
+
chalk.red(
|
|
344
|
+
"No manifest found. Run `forge guild init <name>` to get started.",
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const manifestContent = await readFile(MANIFEST_PATH, "utf-8");
|
|
351
|
+
const manifest = parseManifest(manifestContent);
|
|
352
|
+
|
|
353
|
+
// Read sync-lock if it exists
|
|
354
|
+
const lockPath = join(".forge", "sync-lock.json");
|
|
355
|
+
let syncLock: SyncLock | null = null;
|
|
356
|
+
if (await pathExists(lockPath)) {
|
|
357
|
+
try {
|
|
358
|
+
const raw = await readFile(lockPath, "utf-8");
|
|
359
|
+
syncLock = JSON.parse(raw) as SyncLock;
|
|
360
|
+
} catch {
|
|
361
|
+
// Ignore corrupt sync-lock
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const cache = new GlobalCache();
|
|
366
|
+
|
|
367
|
+
// Build display rows
|
|
368
|
+
interface StatusRow {
|
|
369
|
+
name: string;
|
|
370
|
+
versionPin: string;
|
|
371
|
+
resolvedVersion: string;
|
|
372
|
+
upToDate: string;
|
|
373
|
+
source?: string; // collection name for grouping
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const rows: StatusRow[] = [];
|
|
377
|
+
|
|
378
|
+
for (const entry of manifest.artifacts) {
|
|
379
|
+
if (isCollectionRef(entry)) {
|
|
380
|
+
const col = entry as CollectionManifestEntry;
|
|
381
|
+
// Expand collection members for display (Req 11.6)
|
|
382
|
+
const members = await cache.readCollectionCatalog(
|
|
383
|
+
col.collection,
|
|
384
|
+
col.version,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
for (const member of members) {
|
|
388
|
+
const lockEntry = syncLock?.entries.find(
|
|
389
|
+
(e) => e.name === member.name && e.source === col.collection,
|
|
390
|
+
);
|
|
391
|
+
const resolvedVersion = lockEntry?.version ?? "missing";
|
|
392
|
+
const latestVersions = await cache.listVersions(member.name);
|
|
393
|
+
const sorted = latestVersions.sort(Bun.semver.order);
|
|
394
|
+
const latest = sorted.length > 0 ? sorted[sorted.length - 1] : null;
|
|
395
|
+
const upToDate =
|
|
396
|
+
resolvedVersion !== "missing" &&
|
|
397
|
+
latest !== null &&
|
|
398
|
+
resolvedVersion === latest
|
|
399
|
+
? chalk.green("✓")
|
|
400
|
+
: chalk.red("✗");
|
|
401
|
+
|
|
402
|
+
rows.push({
|
|
403
|
+
name: member.name,
|
|
404
|
+
versionPin: col.version,
|
|
405
|
+
resolvedVersion,
|
|
406
|
+
upToDate,
|
|
407
|
+
source: col.collection,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
const art = entry as ArtifactManifestEntry;
|
|
412
|
+
const lockEntry = syncLock?.entries.find(
|
|
413
|
+
(e) => e.name === art.name && !e.source,
|
|
414
|
+
);
|
|
415
|
+
const resolvedVersion = lockEntry?.version ?? "missing";
|
|
416
|
+
const latestVersions = await cache.listVersions(art.name);
|
|
417
|
+
const sorted = latestVersions.sort(Bun.semver.order);
|
|
418
|
+
const latest = sorted.length > 0 ? sorted[sorted.length - 1] : null;
|
|
419
|
+
const upToDate =
|
|
420
|
+
resolvedVersion !== "missing" &&
|
|
421
|
+
latest !== null &&
|
|
422
|
+
resolvedVersion === latest
|
|
423
|
+
? chalk.green("✓")
|
|
424
|
+
: chalk.red("✗");
|
|
425
|
+
|
|
426
|
+
rows.push({
|
|
427
|
+
name: art.name,
|
|
428
|
+
versionPin: art.version,
|
|
429
|
+
resolvedVersion,
|
|
430
|
+
upToDate,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (rows.length === 0) {
|
|
436
|
+
console.log(chalk.yellow("Manifest is empty — no artifacts declared."));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Print table header
|
|
441
|
+
const colWidths = {
|
|
442
|
+
name: Math.max(10, ...rows.map((r) => r.name.length)) + 2,
|
|
443
|
+
pin: Math.max(7, ...rows.map((r) => r.versionPin.length)) + 2,
|
|
444
|
+
resolved: Math.max(8, ...rows.map((r) => r.resolvedVersion.length)) + 2,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
console.log(
|
|
448
|
+
chalk.bold(
|
|
449
|
+
"Name".padEnd(colWidths.name) +
|
|
450
|
+
"Pin".padEnd(colWidths.pin) +
|
|
451
|
+
"Resolved".padEnd(colWidths.resolved) +
|
|
452
|
+
"Status",
|
|
453
|
+
),
|
|
454
|
+
);
|
|
455
|
+
console.log(
|
|
456
|
+
"─".repeat(colWidths.name + colWidths.pin + colWidths.resolved + 6),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// Group collection members under their collection name (Req 11.6)
|
|
460
|
+
let lastSource: string | undefined;
|
|
461
|
+
|
|
462
|
+
for (const row of rows) {
|
|
463
|
+
if (row.source && row.source !== lastSource) {
|
|
464
|
+
console.log(chalk.cyan(` [${row.source}]`));
|
|
465
|
+
lastSource = row.source;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const indent = row.source ? " " : " ";
|
|
469
|
+
const indicator = row.source ? chalk.dim("(collection)") : "";
|
|
470
|
+
|
|
471
|
+
console.log(
|
|
472
|
+
`${indent}${row.name.padEnd(colWidths.name)}` +
|
|
473
|
+
`${row.versionPin.padEnd(colWidths.pin)}` +
|
|
474
|
+
`${row.resolvedVersion.padEnd(colWidths.resolved)}` +
|
|
475
|
+
`${row.upToDate} ${indicator}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (syncLock) {
|
|
480
|
+
console.log(chalk.dim(`\nLast synced: ${syncLock.syncedAt}`));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// Subcommand: guild hook install
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
interface HookInstallOptions {
|
|
489
|
+
shell?: string;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function guildHookInstall(opts: HookInstallOptions): void {
|
|
493
|
+
let shell: ShellType | null = (opts.shell as ShellType) ?? null;
|
|
494
|
+
|
|
495
|
+
if (!shell) {
|
|
496
|
+
// Try to detect shell (Req 7.5)
|
|
497
|
+
shell = detectShell();
|
|
498
|
+
|
|
499
|
+
if (!shell) {
|
|
500
|
+
const isWindows = process.platform === "win32";
|
|
501
|
+
if (isWindows) {
|
|
502
|
+
// Req 7.7 — default to PowerShell on Windows
|
|
503
|
+
shell = "powershell";
|
|
504
|
+
} else {
|
|
505
|
+
// Req 7.6 — error on non-Windows when SHELL not set
|
|
506
|
+
console.error(
|
|
507
|
+
chalk.red(
|
|
508
|
+
"Error: guild hook install: Cannot detect shell — SHELL environment variable is not set.\n" +
|
|
509
|
+
" Specify the shell explicitly with --shell <bash|zsh|fish|powershell>.",
|
|
510
|
+
),
|
|
511
|
+
);
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const snippet = generateHookSnippet(shell);
|
|
518
|
+
console.log(snippet);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
// Registration
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Register the `forge guild` command group on the root program.
|
|
527
|
+
* Subcommands: init, sync, status, hook install
|
|
528
|
+
*/
|
|
529
|
+
export function registerGuildCommands(program: Command): void {
|
|
530
|
+
const guild = program
|
|
531
|
+
.command("guild")
|
|
532
|
+
.description("Team mode artifact distribution");
|
|
533
|
+
|
|
534
|
+
// --- guild init <name> ---
|
|
535
|
+
guild
|
|
536
|
+
.command("init <name>")
|
|
537
|
+
.description("Add an artifact or collection to the manifest")
|
|
538
|
+
.option("--collection", "Treat <name> as a collection reference")
|
|
539
|
+
.option(
|
|
540
|
+
"--mode <mode>",
|
|
541
|
+
"Dependency mode: required or optional (default: required)",
|
|
542
|
+
"required",
|
|
543
|
+
)
|
|
544
|
+
.option(
|
|
545
|
+
"--version <pin>",
|
|
546
|
+
"Semver version pin (default: latest from cache)",
|
|
547
|
+
)
|
|
548
|
+
.option("--backend <name>", "Named backend from kanon.config.yaml")
|
|
549
|
+
.action(async (name: string, opts: InitOptions) => {
|
|
550
|
+
await guildInit(name, opts);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// --- guild sync ---
|
|
554
|
+
guild
|
|
555
|
+
.command("sync")
|
|
556
|
+
.description(
|
|
557
|
+
"Resolve and materialize manifest artifacts into harness targets",
|
|
558
|
+
)
|
|
559
|
+
.option("--auto-update", "Check backends for newer versions before syncing")
|
|
560
|
+
.option(
|
|
561
|
+
"--throttle <minutes>",
|
|
562
|
+
"Minimum minutes between remote checks (default: 60)",
|
|
563
|
+
)
|
|
564
|
+
.option("--dry-run", "Show what would be synced without writing files")
|
|
565
|
+
.option("--harness <name>", "Materialize only for the specified harness")
|
|
566
|
+
.option(
|
|
567
|
+
"--force",
|
|
568
|
+
"Proceed despite outcome collisions (downgrade to warnings)",
|
|
569
|
+
)
|
|
570
|
+
.action(async (opts: GuildSyncOptions) => {
|
|
571
|
+
await guildSync(opts);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// --- guild status ---
|
|
575
|
+
guild
|
|
576
|
+
.command("status")
|
|
577
|
+
.description("Display manifest entries, resolved versions, and sync state")
|
|
578
|
+
.action(async () => {
|
|
579
|
+
await guildStatus();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// --- guild hook install ---
|
|
583
|
+
const hook = guild.command("hook").description("Shell hook management");
|
|
584
|
+
|
|
585
|
+
hook
|
|
586
|
+
.command("install")
|
|
587
|
+
.description("Output a shell snippet for auto-sync on directory change")
|
|
588
|
+
.option("--shell <name>", "Target shell: bash, zsh, fish, or powershell")
|
|
589
|
+
.action((opts: HookInstallOptions) => {
|
|
590
|
+
guildHookInstall(opts);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Collection Expander — expand collection refs into individual artifact refs
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import type { GlobalCacheAPI } from "./global-cache";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An individual artifact reference produced by expanding a collection.
|
|
9
|
+
* Inherits mode, harnesses, and backend from the originating collection ref.
|
|
10
|
+
*/
|
|
11
|
+
export interface ExpandedArtifact {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
mode: "required" | "optional";
|
|
15
|
+
harnesses?: string[];
|
|
16
|
+
backend?: string;
|
|
17
|
+
source: string; // originating collection name
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Expand a collection ref into individual artifact refs by reading the
|
|
22
|
+
* collection's cached catalog metadata from the GlobalCache.
|
|
23
|
+
*
|
|
24
|
+
* Each member inherits the collection ref's version, mode, harnesses, and
|
|
25
|
+
* backend. The `source` field is set to the originating collection name.
|
|
26
|
+
*
|
|
27
|
+
* Returns an empty array if the collection has no members.
|
|
28
|
+
*/
|
|
29
|
+
export async function expandCollection(
|
|
30
|
+
collectionName: string,
|
|
31
|
+
version: string,
|
|
32
|
+
mode: "required" | "optional",
|
|
33
|
+
harnesses: string[] | undefined,
|
|
34
|
+
backend: string | undefined,
|
|
35
|
+
cache: GlobalCacheAPI,
|
|
36
|
+
): Promise<ExpandedArtifact[]> {
|
|
37
|
+
const members = await cache.readCollectionCatalog(collectionName, version);
|
|
38
|
+
|
|
39
|
+
return members.map((entry) => ({
|
|
40
|
+
name: entry.name,
|
|
41
|
+
version,
|
|
42
|
+
mode,
|
|
43
|
+
...(harnesses ? { harnesses } : {}),
|
|
44
|
+
...(backend ? { backend } : {}),
|
|
45
|
+
source: collectionName,
|
|
46
|
+
}));
|
|
47
|
+
}
|