@thinkingsage/kanon 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/bridge/mcp-server.cjs +14171 -0
  5. package/package.json +98 -0
  6. package/src/adapters/capabilities.ts +178 -0
  7. package/src/adapters/claude-code.ts +110 -0
  8. package/src/adapters/cline.ts +98 -0
  9. package/src/adapters/codex.ts +173 -0
  10. package/src/adapters/copilot.ts +106 -0
  11. package/src/adapters/cursor.ts +97 -0
  12. package/src/adapters/degradation.ts +95 -0
  13. package/src/adapters/index.ts +324 -0
  14. package/src/adapters/kiro-frontmatter.ts +139 -0
  15. package/src/adapters/kiro-inclusion.ts +86 -0
  16. package/src/adapters/kiro.ts +412 -0
  17. package/src/adapters/qdeveloper.ts +115 -0
  18. package/src/adapters/types.ts +81 -0
  19. package/src/adapters/windsurf.ts +96 -0
  20. package/src/admin.ts +283 -0
  21. package/src/asset-conventions.ts +118 -0
  22. package/src/attribution-backfill.ts +319 -0
  23. package/src/attribution-report.ts +95 -0
  24. package/src/attribution.ts +239 -0
  25. package/src/backends/github.ts +194 -0
  26. package/src/backends/http.ts +122 -0
  27. package/src/backends/index.ts +39 -0
  28. package/src/backends/local.ts +47 -0
  29. package/src/backends/s3.ts +157 -0
  30. package/src/backends/types.ts +59 -0
  31. package/src/base-cache.ts +270 -0
  32. package/src/browse-ui.ts +3754 -0
  33. package/src/browse.ts +1038 -0
  34. package/src/build.ts +1108 -0
  35. package/src/catalog.ts +204 -0
  36. package/src/cli-deprecated.ts +29 -0
  37. package/src/cli.ts +773 -0
  38. package/src/collection-admin.ts +287 -0
  39. package/src/collection-builder.ts +464 -0
  40. package/src/collections.ts +116 -0
  41. package/src/compatibility.ts +105 -0
  42. package/src/config.ts +743 -0
  43. package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
  44. package/src/eval.ts +1169 -0
  45. package/src/file-writer.ts +61 -0
  46. package/src/format-registry.ts +141 -0
  47. package/src/guild/auto-updater.ts +163 -0
  48. package/src/guild/backend-resolver.ts +49 -0
  49. package/src/guild/cli.ts +592 -0
  50. package/src/guild/collection-expander.ts +47 -0
  51. package/src/guild/global-cache.ts +247 -0
  52. package/src/guild/hook-generator.ts +100 -0
  53. package/src/guild/manifest.ts +154 -0
  54. package/src/guild/path-utils.ts +12 -0
  55. package/src/guild/sync.ts +622 -0
  56. package/src/guild/version-resolver.ts +42 -0
  57. package/src/help/metadata.ts +445 -0
  58. package/src/help/renderer.ts +265 -0
  59. package/src/help/typo-suggester.ts +25 -0
  60. package/src/hooks/expression.ts +493 -0
  61. package/src/hooks/pipeline.ts +141 -0
  62. package/src/import.ts +773 -0
  63. package/src/importers/claude-code.ts +134 -0
  64. package/src/importers/cline.ts +103 -0
  65. package/src/importers/codex.ts +140 -0
  66. package/src/importers/copilot.ts +103 -0
  67. package/src/importers/cursor.ts +105 -0
  68. package/src/importers/index.ts +390 -0
  69. package/src/importers/kiro.ts +110 -0
  70. package/src/importers/qdeveloper.ts +103 -0
  71. package/src/importers/types.ts +54 -0
  72. package/src/importers/windsurf.ts +104 -0
  73. package/src/install.ts +1005 -0
  74. package/src/manifest-admin.ts +306 -0
  75. package/src/mcp-bridge.ts +240 -0
  76. package/src/mutation/delta.ts +50 -0
  77. package/src/mutation/history.ts +66 -0
  78. package/src/mutation/operators.ts +524 -0
  79. package/src/mutation/runner.ts +332 -0
  80. package/src/new.ts +106 -0
  81. package/src/outcomes/collision.ts +127 -0
  82. package/src/outcomes/normalize.ts +208 -0
  83. package/src/outcomes/registry.ts +173 -0
  84. package/src/parser.ts +446 -0
  85. package/src/provenance-backfill-cli.ts +319 -0
  86. package/src/provenance-backfill.ts +520 -0
  87. package/src/publish.ts +354 -0
  88. package/src/reconcile-orchestrator.ts +502 -0
  89. package/src/reconcile-report-renderer.ts +176 -0
  90. package/src/resolve-body.ts +15 -0
  91. package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
  92. package/src/rosetta/builtins/contracts.ts +1033 -0
  93. package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
  94. package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
  95. package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
  96. package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
  97. package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
  98. package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
  99. package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
  100. package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
  101. package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
  102. package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
  103. package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
  104. package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
  105. package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
  106. package/src/rosetta/builtins/sources/cline-native.ts +176 -0
  107. package/src/rosetta/builtins/sources/codex-native.ts +343 -0
  108. package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
  109. package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
  110. package/src/rosetta/builtins/sources/index.ts +95 -0
  111. package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
  112. package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
  113. package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
  114. package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
  115. package/src/rosetta/builtins/sources/superpowers.ts +240 -0
  116. package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
  117. package/src/rosetta/builtins/targets/claude-code.ts +181 -0
  118. package/src/rosetta/builtins/targets/cline.ts +87 -0
  119. package/src/rosetta/builtins/targets/codex.ts +226 -0
  120. package/src/rosetta/builtins/targets/copilot.ts +103 -0
  121. package/src/rosetta/builtins/targets/cursor.ts +87 -0
  122. package/src/rosetta/builtins/targets/index.ts +60 -0
  123. package/src/rosetta/builtins/targets/kiro.ts +278 -0
  124. package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
  125. package/src/rosetta/builtins/targets/windsurf.ts +87 -0
  126. package/src/rosetta/canonical.ts +729 -0
  127. package/src/rosetta/compatibility.ts +432 -0
  128. package/src/rosetta/contracts.ts +329 -0
  129. package/src/rosetta/detector.ts +724 -0
  130. package/src/rosetta/diagnostics.ts +630 -0
  131. package/src/rosetta/engine-bootstrap.ts +103 -0
  132. package/src/rosetta/engine.ts +744 -0
  133. package/src/rosetta/index.ts +381 -0
  134. package/src/rosetta/inspection.ts +530 -0
  135. package/src/rosetta/plan.ts +448 -0
  136. package/src/rosetta/provenance-digest.ts +369 -0
  137. package/src/rosetta/reconcile.ts +812 -0
  138. package/src/rosetta/redaction.ts +467 -0
  139. package/src/rosetta/registry.ts +712 -0
  140. package/src/rosetta/renderers.ts +571 -0
  141. package/src/rosetta/request-guard.ts +335 -0
  142. package/src/rosetta/resolution.ts +419 -0
  143. package/src/rosetta/source-accounting.ts +233 -0
  144. package/src/rosetta/templates.ts +129 -0
  145. package/src/rosetta-cli.ts +717 -0
  146. package/src/rosetta-docs-generator.ts +793 -0
  147. package/src/rosetta-profiles-cli.ts +367 -0
  148. package/src/schemas.ts +1712 -0
  149. package/src/spec-coordination.ts +1141 -0
  150. package/src/temper.ts +747 -0
  151. package/src/template-bundle-loader.ts +312 -0
  152. package/src/template-engine.ts +53 -0
  153. package/src/translation-application-policy.ts +496 -0
  154. package/src/translation-orchestrator.ts +1013 -0
  155. package/src/translation-plan-applier.ts +473 -0
  156. package/src/tutorial.ts +305 -0
  157. package/src/validate.ts +1093 -0
  158. package/src/versioning.ts +553 -0
  159. package/src/wizard.ts +660 -0
  160. package/src/workspace.ts +237 -0
  161. package/templates/eval-contexts/claude-code.md.njk +6 -0
  162. package/templates/eval-contexts/cline.md.njk +6 -0
  163. package/templates/eval-contexts/copilot.md.njk +6 -0
  164. package/templates/eval-contexts/cursor.md.njk +6 -0
  165. package/templates/eval-contexts/kiro.md.njk +10 -0
  166. package/templates/eval-contexts/qdeveloper.md.njk +6 -0
  167. package/templates/eval-contexts/windsurf.md.njk +6 -0
  168. package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
  169. package/templates/harness-adapters/_base/base.md.njk +16 -0
  170. package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
  171. package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
  172. package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
  173. package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
  174. package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
  175. package/templates/harness-adapters/cline/hook.sh.njk +4 -0
  176. package/templates/harness-adapters/cline/mcp.json.njk +1 -0
  177. package/templates/harness-adapters/cline/rule.md.njk +1 -0
  178. package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
  179. package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
  180. package/templates/harness-adapters/codex/skill.md.njk +27 -0
  181. package/templates/harness-adapters/copilot/agents.md.njk +1 -0
  182. package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
  183. package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
  184. package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
  185. package/templates/harness-adapters/cursor/rule.md.njk +6 -0
  186. package/templates/harness-adapters/kiro/hook.json.njk +1 -0
  187. package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
  188. package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
  189. package/templates/harness-adapters/kiro/power.md.njk +12 -0
  190. package/templates/harness-adapters/kiro/steering.md.njk +16 -0
  191. package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
  192. package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
  193. package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
  194. package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
  195. package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
  196. package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
  197. package/templates/knowledge/hooks.yaml.njk +4 -0
  198. package/templates/knowledge/knowledge.md.njk +53 -0
  199. package/templates/knowledge/mcp-servers.yaml.njk +2 -0
@@ -0,0 +1,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
+ }