@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,247 @@
1
+ import type { Dirent } from "node:fs";
2
+ import {
3
+ access,
4
+ copyFile,
5
+ mkdir,
6
+ readdir,
7
+ readFile,
8
+ writeFile,
9
+ } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Metadata stored alongside each cached artifact version. */
18
+ export interface CachedArtifactMeta {
19
+ name: string;
20
+ version: string;
21
+ backend: string;
22
+ installedAt: string;
23
+ harnesses: string[];
24
+ }
25
+
26
+ /** A single entry in a collection's cached catalog. */
27
+ export interface CatalogEntry {
28
+ name: string;
29
+ }
30
+
31
+ /** Public API surface for the global artifact cache. */
32
+ export interface GlobalCacheAPI {
33
+ /** Root path of the global cache (platform-aware). */
34
+ readonly root: string;
35
+
36
+ /** List all cached versions for an artifact. */
37
+ listVersions(artifactName: string): Promise<string[]>;
38
+
39
+ /** Construct the dist path for a specific artifact/version/harness. */
40
+ distPath(artifactName: string, version: string, harness: string): string;
41
+
42
+ /** Check whether a specific artifact version exists in the cache. */
43
+ has(artifactName: string, version: string): Promise<boolean>;
44
+
45
+ /** Copy files from `sourceDir` into the cache and write/update `meta.json`. */
46
+ store(
47
+ artifactName: string,
48
+ version: string,
49
+ harness: string,
50
+ sourceDir: string,
51
+ backendLabel: string,
52
+ ): Promise<void>;
53
+
54
+ /** Read the cached catalog for a collection at a given version. */
55
+ readCollectionCatalog(
56
+ collectionName: string,
57
+ version: string,
58
+ ): Promise<CatalogEntry[]>;
59
+
60
+ /** Write catalog metadata for a collection version. */
61
+ writeCatalogMeta(
62
+ collectionName: string,
63
+ version: string,
64
+ entries: CatalogEntry[],
65
+ ): Promise<void>;
66
+
67
+ /** Read the last-sync throttle timestamp, or `null` if not yet written. */
68
+ readThrottleState(): Promise<Date | null>;
69
+
70
+ /** Persist a throttle timestamp. */
71
+ writeThrottleState(timestamp: Date): Promise<void>;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /** Recursively collect all relative file paths under `dir`. */
79
+ async function collectFiles(dir: string, base: string = ""): Promise<string[]> {
80
+ const results: string[] = [];
81
+ let entries: Dirent[];
82
+ try {
83
+ entries = (await readdir(dir, { withFileTypes: true })) as Dirent[];
84
+ } catch {
85
+ return results;
86
+ }
87
+ for (const entry of entries) {
88
+ const rel = base ? `${base}/${entry.name}` : entry.name;
89
+ if (entry.isDirectory()) {
90
+ results.push(...(await collectFiles(join(dir, entry.name), rel)));
91
+ } else {
92
+ results.push(rel);
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+
98
+ /** Check if a path exists on disk. */
99
+ async function pathExists(p: string): Promise<boolean> {
100
+ try {
101
+ await access(p);
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Implementation
110
+ // ---------------------------------------------------------------------------
111
+
112
+ export class GlobalCache implements GlobalCacheAPI {
113
+ readonly root: string;
114
+
115
+ constructor(rootOverride?: string) {
116
+ this.root = rootOverride ?? join(homedir(), ".forge");
117
+ }
118
+
119
+ // -- internal path helpers ------------------------------------------------
120
+
121
+ private artifactsDir(): string {
122
+ return join(this.root, "artifacts");
123
+ }
124
+
125
+ private versionDir(artifactName: string, version: string): string {
126
+ return join(this.artifactsDir(), artifactName, version);
127
+ }
128
+
129
+ private metaPath(artifactName: string, version: string): string {
130
+ return join(this.versionDir(artifactName, version), "meta.json");
131
+ }
132
+
133
+ private collectionsDir(): string {
134
+ return join(this.artifactsDir(), "collections");
135
+ }
136
+
137
+ private catalogPath(collectionName: string, version: string): string {
138
+ return join(this.collectionsDir(), collectionName, version, "catalog.json");
139
+ }
140
+
141
+ private throttlePath(): string {
142
+ return join(this.root, ".last-sync");
143
+ }
144
+
145
+ // -- GlobalCacheAPI -------------------------------------------------------
146
+
147
+ async listVersions(artifactName: string): Promise<string[]> {
148
+ const dir = join(this.artifactsDir(), artifactName);
149
+ try {
150
+ const entries = await readdir(dir, { withFileTypes: true });
151
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
152
+ } catch {
153
+ return [];
154
+ }
155
+ }
156
+
157
+ distPath(artifactName: string, version: string, harness: string): string {
158
+ return join(this.artifactsDir(), artifactName, version, "dist", harness);
159
+ }
160
+
161
+ async has(artifactName: string, version: string): Promise<boolean> {
162
+ return pathExists(this.versionDir(artifactName, version));
163
+ }
164
+
165
+ async store(
166
+ artifactName: string,
167
+ version: string,
168
+ harness: string,
169
+ sourceDir: string,
170
+ backendLabel: string,
171
+ ): Promise<void> {
172
+ const dest = this.distPath(artifactName, version, harness);
173
+ await mkdir(dest, { recursive: true });
174
+
175
+ // Copy every file from sourceDir into the cache dist path
176
+ const files = await collectFiles(sourceDir);
177
+ for (const rel of files) {
178
+ const src = join(sourceDir, rel);
179
+ const target = join(dest, rel);
180
+ await mkdir(dirname(target), { recursive: true });
181
+ await copyFile(src, target);
182
+ }
183
+
184
+ // Read or create meta.json
185
+ const metaFile = this.metaPath(artifactName, version);
186
+ let meta: CachedArtifactMeta;
187
+ try {
188
+ const raw = await readFile(metaFile, "utf-8");
189
+ meta = JSON.parse(raw) as CachedArtifactMeta;
190
+ // Append harness if not already tracked
191
+ if (!meta.harnesses.includes(harness)) {
192
+ meta.harnesses.push(harness);
193
+ }
194
+ } catch {
195
+ meta = {
196
+ name: artifactName,
197
+ version,
198
+ backend: backendLabel,
199
+ installedAt: new Date().toISOString(),
200
+ harnesses: [harness],
201
+ };
202
+ }
203
+
204
+ await writeFile(metaFile, JSON.stringify(meta, null, 2), "utf-8");
205
+ }
206
+
207
+ async readCollectionCatalog(
208
+ collectionName: string,
209
+ version: string,
210
+ ): Promise<CatalogEntry[]> {
211
+ const catalogFile = this.catalogPath(collectionName, version);
212
+ try {
213
+ const raw = await readFile(catalogFile, "utf-8");
214
+ return JSON.parse(raw) as CatalogEntry[];
215
+ } catch {
216
+ return [];
217
+ }
218
+ }
219
+
220
+ async writeCatalogMeta(
221
+ collectionName: string,
222
+ version: string,
223
+ entries: CatalogEntry[],
224
+ ): Promise<void> {
225
+ const catalogFile = this.catalogPath(collectionName, version);
226
+ const dir = dirname(catalogFile);
227
+ await mkdir(dir, { recursive: true });
228
+ await writeFile(catalogFile, JSON.stringify(entries, null, 2), "utf-8");
229
+ }
230
+
231
+ async readThrottleState(): Promise<Date | null> {
232
+ try {
233
+ const raw = await readFile(this.throttlePath(), "utf-8");
234
+ const trimmed = raw.trim();
235
+ if (!trimmed) return null;
236
+ const date = new Date(trimmed);
237
+ return Number.isNaN(date.getTime()) ? null : date;
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+
243
+ async writeThrottleState(timestamp: Date): Promise<void> {
244
+ await mkdir(this.root, { recursive: true });
245
+ await writeFile(this.throttlePath(), timestamp.toISOString(), "utf-8");
246
+ }
247
+ }
@@ -0,0 +1,100 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shell Snippet Generator — generate shell profile snippets for auto-sync
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export type ShellType = "bash" | "zsh" | "fish" | "powershell";
6
+
7
+ /**
8
+ * Generate a shell snippet that auto-syncs on directory change.
9
+ * The snippet detects `.forge/manifest.yaml` and runs
10
+ * `forge guild sync --auto-update` in the background.
11
+ * All output is redirected to /dev/null (or $null on PowerShell).
12
+ *
13
+ * Requirements: 7.1, 7.2, 7.3, 7.4, 7.7
14
+ */
15
+ export function generateHookSnippet(shell: ShellType): string {
16
+ switch (shell) {
17
+ case "bash":
18
+ return bashSnippet();
19
+ case "zsh":
20
+ return zshSnippet();
21
+ case "fish":
22
+ return fishSnippet();
23
+ case "powershell":
24
+ return powershellSnippet();
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Detect the current shell from the SHELL environment variable.
30
+ * Returns null if SHELL is not set.
31
+ *
32
+ * Requirements: 7.5, 7.6
33
+ */
34
+ export function detectShell(): ShellType | null {
35
+ const shellEnv = process.env.SHELL;
36
+ if (!shellEnv) return null;
37
+
38
+ if (shellEnv.includes("zsh")) return "zsh";
39
+ if (shellEnv.includes("bash")) return "bash";
40
+ if (shellEnv.includes("fish")) return "fish";
41
+
42
+ return null;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Shell-specific snippets
47
+ // ---------------------------------------------------------------------------
48
+
49
+ function bashSnippet(): string {
50
+ return `# Kanon auto-sync hook (bash)
51
+ _forge_guild_autosync() {
52
+ if [ -f ".forge/manifest.yaml" ]; then
53
+ forge guild sync --auto-update > /dev/null 2>&1 &
54
+ fi
55
+ }
56
+
57
+ cd() {
58
+ builtin cd "$@" && _forge_guild_autosync
59
+ }`;
60
+ }
61
+
62
+ function zshSnippet(): string {
63
+ return `# Kanon auto-sync hook (zsh)
64
+ _forge_guild_autosync() {
65
+ if [ -f ".forge/manifest.yaml" ]; then
66
+ forge guild sync --auto-update > /dev/null 2>&1 &
67
+ fi
68
+ }
69
+
70
+ autoload -Uz add-zsh-hook
71
+ add-zsh-hook chpwd _forge_guild_autosync`;
72
+ }
73
+
74
+ function fishSnippet(): string {
75
+ return `# Kanon auto-sync hook (fish)
76
+ function _forge_guild_autosync --on-variable PWD
77
+ if test -f ".forge/manifest.yaml"
78
+ forge guild sync --auto-update > /dev/null 2>&1 &
79
+ end
80
+ end`;
81
+ }
82
+
83
+ function powershellSnippet(): string {
84
+ return `# Kanon auto-sync hook (PowerShell)
85
+ function Invoke-ForgeGuildAutoSync {
86
+ if (Test-Path ".forge/manifest.yaml") {
87
+ Start-Process -NoNewWindow -FilePath "forge" -ArgumentList "guild","sync","--auto-update" -RedirectStandardOutput $null -RedirectStandardError $null
88
+ }
89
+ }
90
+
91
+ # Override Set-Location to trigger auto-sync
92
+ $ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction = $null
93
+ function Set-Location {
94
+ [CmdletBinding()]
95
+ param([string]$Path)
96
+ Microsoft.PowerShell.Management\\Set-Location $Path
97
+ Invoke-ForgeGuildAutoSync
98
+ }
99
+ Set-Alias -Name cd -Value Set-Location -Force`;
100
+ }
@@ -0,0 +1,154 @@
1
+ import * as yaml from "js-yaml";
2
+ import { z } from "zod";
3
+ import { SUPPORTED_HARNESSES } from "../schemas";
4
+
5
+ // --- Schemas ---
6
+
7
+ export const ManifestEntryModeSchema = z.enum(["required", "optional"]);
8
+
9
+ /** An individual artifact reference in the manifest. */
10
+ export const ArtifactManifestEntrySchema = z.object({
11
+ name: z.string().min(1),
12
+ version: z.string().min(1),
13
+ mode: ManifestEntryModeSchema.default("required"),
14
+ harnesses: z.array(z.string()).optional(),
15
+ backend: z.string().optional(),
16
+ });
17
+
18
+ /** A collection reference in the manifest. */
19
+ export const CollectionManifestEntrySchema = z.object({
20
+ collection: z.string().min(1),
21
+ version: z.string().min(1),
22
+ mode: ManifestEntryModeSchema.default("required"),
23
+ harnesses: z.array(z.string()).optional(),
24
+ backend: z.string().optional(),
25
+ });
26
+
27
+ export const ManifestEntrySchema = z.union([
28
+ ArtifactManifestEntrySchema,
29
+ CollectionManifestEntrySchema,
30
+ ]);
31
+
32
+ export const ManifestSchema = z
33
+ .object({
34
+ backend: z.string().optional(),
35
+ artifacts: z.array(ManifestEntrySchema).default([]),
36
+ })
37
+ .passthrough();
38
+
39
+ export type ManifestEntryMode = z.infer<typeof ManifestEntryModeSchema>;
40
+ export type ArtifactManifestEntry = z.infer<typeof ArtifactManifestEntrySchema>;
41
+ export type CollectionManifestEntry = z.infer<
42
+ typeof CollectionManifestEntrySchema
43
+ >;
44
+ export type ManifestEntry = z.infer<typeof ManifestEntrySchema>;
45
+ export type Manifest = z.infer<typeof ManifestSchema>;
46
+
47
+ // --- Recognized harness set for filtering ---
48
+
49
+ const RECOGNIZED_HARNESSES = new Set<string>(SUPPORTED_HARNESSES);
50
+
51
+ // --- Functions ---
52
+
53
+ /**
54
+ * Parse YAML string into a validated Manifest.
55
+ *
56
+ * Accepts an optional `warnings` array — unrecognized harness names
57
+ * are filtered out and a warning string is pushed for each one.
58
+ * Entries with both `name` and `collection` set are rejected.
59
+ */
60
+ export function parseManifest(
61
+ yamlContent: string,
62
+ warnings?: string[],
63
+ ): Manifest {
64
+ // 1. YAML parse — catch syntax errors with line/column info
65
+ let raw: unknown;
66
+ try {
67
+ raw = yaml.load(yamlContent);
68
+ } catch (err: unknown) {
69
+ if (err && typeof err === "object" && "mark" in err) {
70
+ const mark = (err as { mark: { line?: number; column?: number } }).mark;
71
+ const line = mark.line != null ? mark.line + 1 : "?";
72
+ const col = mark.column != null ? mark.column + 1 : "?";
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ throw new Error(
75
+ `YAML syntax error at line ${line}, column ${col}: ${msg}`,
76
+ );
77
+ }
78
+ throw err;
79
+ }
80
+
81
+ // Handle empty YAML (null/undefined)
82
+ if (raw == null) {
83
+ raw = {};
84
+ }
85
+
86
+ // 2. Reject entries that have both `name` and `collection` set
87
+ if (
88
+ raw &&
89
+ typeof raw === "object" &&
90
+ "artifacts" in raw &&
91
+ Array.isArray((raw as Record<string, unknown>).artifacts)
92
+ ) {
93
+ const artifacts = (raw as Record<string, unknown>).artifacts as unknown[];
94
+ for (let i = 0; i < artifacts.length; i++) {
95
+ const entry = artifacts[i];
96
+ if (
97
+ entry &&
98
+ typeof entry === "object" &&
99
+ "name" in entry &&
100
+ "collection" in entry
101
+ ) {
102
+ throw new Error(
103
+ `Manifest entry at index ${i} has both "name" and "collection" fields set — each entry must have exactly one`,
104
+ );
105
+ }
106
+ }
107
+ }
108
+
109
+ // 3. Zod validation
110
+ const result = ManifestSchema.safeParse(raw);
111
+ if (!result.success) {
112
+ const issues = result.error.issues
113
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
114
+ .join("; ");
115
+ throw new Error(`Manifest validation failed: ${issues}`);
116
+ }
117
+
118
+ const manifest = result.data;
119
+
120
+ // 4. Filter harnesses — emit warnings for unrecognized names (Req 3.5)
121
+ for (const entry of manifest.artifacts) {
122
+ if (entry.harnesses) {
123
+ const filtered: string[] = [];
124
+ for (const h of entry.harnesses) {
125
+ if (RECOGNIZED_HARNESSES.has(h)) {
126
+ filtered.push(h);
127
+ } else {
128
+ const entryLabel =
129
+ "name" in entry
130
+ ? entry.name
131
+ : (entry as CollectionManifestEntry).collection;
132
+ const msg = `Unrecognized harness "${h}" in entry "${entryLabel}" — skipping`;
133
+ warnings?.push(msg);
134
+ }
135
+ }
136
+ (entry as { harnesses?: string[] }).harnesses =
137
+ filtered.length > 0 ? filtered : undefined;
138
+ }
139
+ }
140
+
141
+ return manifest;
142
+ }
143
+
144
+ /** Serialize a Manifest object to YAML string. */
145
+ export function printManifest(manifest: Manifest): string {
146
+ return yaml.dump(manifest, { lineWidth: -1, noRefs: true });
147
+ }
148
+
149
+ /** Check if a ManifestEntry is a collection reference. */
150
+ export function isCollectionRef(
151
+ entry: ManifestEntry,
152
+ ): entry is CollectionManifestEntry {
153
+ return "collection" in entry && typeof entry.collection === "string";
154
+ }
@@ -0,0 +1,12 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Path utilities for cross-platform consistency
3
+ // ---------------------------------------------------------------------------
4
+
5
+ /**
6
+ * Normalize a file path to use forward slashes only.
7
+ * Used for manifest and sync-lock output to ensure cross-platform consistency
8
+ * in committed files (Req 8.3).
9
+ */
10
+ export function normalizePath(p: string): string {
11
+ return p.replace(/\\/g, "/");
12
+ }