project-iris 0.0.6

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 (109) hide show
  1. package/README.md +384 -0
  2. package/dist/bridge/connector-factory.js +27 -0
  3. package/dist/bridge/connectors/antigravity-connector.js +18 -0
  4. package/dist/bridge/connectors/cursor-connector.js +31 -0
  5. package/dist/bridge/connectors/vscode-connector.js +31 -0
  6. package/dist/bridge/connectors/windsurf-connector.js +23 -0
  7. package/dist/bridge/filesystem-connector.js +100 -0
  8. package/dist/bridge/types.js +10 -0
  9. package/dist/cli.js +30 -0
  10. package/dist/commands/ask.js +232 -0
  11. package/dist/commands/bridge.js +259 -0
  12. package/dist/commands/develop.js +108 -0
  13. package/dist/commands/doctor.js +102 -0
  14. package/dist/commands/install.js +57 -0
  15. package/dist/commands/pack.js +27 -0
  16. package/dist/commands/phase.js +38 -0
  17. package/dist/commands/run.js +17 -0
  18. package/dist/commands/status.js +105 -0
  19. package/dist/commands/uninstall.js +12 -0
  20. package/dist/commands/validate.js +87 -0
  21. package/dist/iris/artifact-checker.js +78 -0
  22. package/dist/iris/fixer.js +143 -0
  23. package/dist/iris/guard.js +38 -0
  24. package/dist/iris/include.js +49 -0
  25. package/dist/iris/installer.js +269 -0
  26. package/dist/iris/manifest.js +54 -0
  27. package/dist/iris/packer.js +303 -0
  28. package/dist/iris/policy.js +28 -0
  29. package/dist/iris/report.js +53 -0
  30. package/dist/iris/resolver.js +63 -0
  31. package/dist/iris/router.js +114 -0
  32. package/dist/iris/routes.js +20 -0
  33. package/dist/iris/run-state.js +143 -0
  34. package/dist/iris/state.js +85 -0
  35. package/dist/iris/uninstaller.js +166 -0
  36. package/dist/iris/validator.js +329 -0
  37. package/dist/lib.js +96 -0
  38. package/dist/utils/exit-codes.js +7 -0
  39. package/dist/workflows/bolt-execution.js +238 -0
  40. package/dist/workflows/bolt-plan.js +192 -0
  41. package/dist/workflows/intent-inception.js +188 -0
  42. package/package.json +41 -0
  43. package/src/iris_bundle/.iris/aidlc/README.md +16 -0
  44. package/src/iris_bundle/.iris/aidlc/agents/iris-construction-agent.md +35 -0
  45. package/src/iris_bundle/.iris/aidlc/agents/iris-inception-agent.md +30 -0
  46. package/src/iris_bundle/.iris/aidlc/agents/iris-master-agent.md +35 -0
  47. package/src/iris_bundle/.iris/aidlc/agents/iris-operations-agent.md +29 -0
  48. package/src/iris_bundle/.iris/aidlc/commands/iris-construction-agent.md +18 -0
  49. package/src/iris_bundle/.iris/aidlc/commands/iris-inception-agent.md +18 -0
  50. package/src/iris_bundle/.iris/aidlc/commands/iris-master-agent.md +18 -0
  51. package/src/iris_bundle/.iris/aidlc/commands/iris-operations-agent.md +18 -0
  52. package/src/iris_bundle/.iris/aidlc/context/context-map.md +25 -0
  53. package/src/iris_bundle/.iris/aidlc/context/exclusion-rules.md +13 -0
  54. package/src/iris_bundle/.iris/aidlc/context/load-order.md +25 -0
  55. package/src/iris_bundle/.iris/aidlc/memory/intent-rules.md +9 -0
  56. package/src/iris_bundle/.iris/aidlc/memory/log-rules.md +5 -0
  57. package/src/iris_bundle/.iris/aidlc/memory/memory-bank.yaml +39 -0
  58. package/src/iris_bundle/.iris/aidlc/memory/unit-rules.md +9 -0
  59. package/src/iris_bundle/.iris/aidlc/quick-start.md +24 -0
  60. package/src/iris_bundle/.iris/aidlc/skills/execution/implementation.md +14 -0
  61. package/src/iris_bundle/.iris/aidlc/skills/execution/refactoring.md +13 -0
  62. package/src/iris_bundle/.iris/aidlc/skills/execution/scaffold-generation.md +15 -0
  63. package/src/iris_bundle/.iris/aidlc/skills/governance/escalation.md +13 -0
  64. package/src/iris_bundle/.iris/aidlc/skills/governance/quality-gates.md +14 -0
  65. package/src/iris_bundle/.iris/aidlc/skills/governance/stop-conditions.md +11 -0
  66. package/src/iris_bundle/.iris/aidlc/skills/reasoning/decomposition.md +23 -0
  67. package/src/iris_bundle/.iris/aidlc/skills/reasoning/risk-analysis.md +14 -0
  68. package/src/iris_bundle/.iris/aidlc/skills/reasoning/verification.md +21 -0
  69. package/src/iris_bundle/.iris/aidlc/standards/artifacts-registry.md +38 -0
  70. package/src/iris_bundle/.iris/aidlc/standards/decision-logging.md +16 -0
  71. package/src/iris_bundle/.iris/aidlc/standards/doctrine-structure.md +31 -0
  72. package/src/iris_bundle/.iris/aidlc/standards/documentation-rules.md +15 -0
  73. package/src/iris_bundle/.iris/aidlc/standards/file-structure.md +21 -0
  74. package/src/iris_bundle/.iris/aidlc/standards/naming-conventions.md +18 -0
  75. package/src/iris_bundle/.iris/aidlc/standards/phases-and-gates.md +25 -0
  76. package/src/iris_bundle/.iris/aidlc/standards/routes-and-routing.md +35 -0
  77. package/src/iris_bundle/.iris/aidlc/standards/tool-wrappers.md +32 -0
  78. package/src/iris_bundle/.iris/aidlc/templates/bolt.md +23 -0
  79. package/src/iris_bundle/.iris/aidlc/templates/doctrine-doc-template.md +33 -0
  80. package/src/iris_bundle/.iris/aidlc/templates/intent.md +23 -0
  81. package/src/iris_bundle/.iris/aidlc/templates/log.md +24 -0
  82. package/src/iris_bundle/.iris/aidlc/templates/review.md +21 -0
  83. package/src/iris_bundle/.iris/aidlc/templates/unit.md +31 -0
  84. package/src/iris_bundle/.iris/aidlc/validation/failure-modes.md +16 -0
  85. package/src/iris_bundle/.iris/aidlc/validation/phase-preconditions.md +21 -0
  86. package/src/iris_bundle/.iris/aidlc/validation/quality-checklist.md +20 -0
  87. package/src/iris_bundle/.iris/policy.yaml +27 -0
  88. package/src/iris_bundle/.iris/routes.yaml +98 -0
  89. package/src/iris_bundle/.iris/state.yaml +7 -0
  90. package/src/iris_bundle/.iris/tools/antigravity/.antigravity/knowledge/IRIS.md +6 -0
  91. package/src/iris_bundle/.iris/tools/antigravity/.antigravity/workflows/iris-construction-agent.md +25 -0
  92. package/src/iris_bundle/.iris/tools/antigravity/.antigravity/workflows/iris-inception-agent.md +25 -0
  93. package/src/iris_bundle/.iris/tools/antigravity/.antigravity/workflows/iris-master-agent.md +25 -0
  94. package/src/iris_bundle/.iris/tools/antigravity/.antigravity/workflows/iris-operations-agent.md +25 -0
  95. package/src/iris_bundle/.iris/tools/claude/.claude/claude.md +9 -0
  96. package/src/iris_bundle/.iris/tools/claude/.claude/commands/compare-specs.md +203 -0
  97. package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-construction-agent.md +25 -0
  98. package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-inception-agent.md +25 -0
  99. package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-master-agent.md +25 -0
  100. package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-operations-agent.md +25 -0
  101. package/src/iris_bundle/.iris/tools/codex/AGENTS.md +15 -0
  102. package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-construction-agent.md +25 -0
  103. package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-inception-agent.md +25 -0
  104. package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-master-agent.md +25 -0
  105. package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-operations-agent.md +25 -0
  106. package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-construction-agent.toml +29 -0
  107. package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-inception-agent.toml +29 -0
  108. package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-master-agent.toml +29 -0
  109. package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-operations-agent.toml +29 -0
@@ -0,0 +1,38 @@
1
+ import kleur from "kleur";
2
+ import { validate } from "./validator.js";
3
+ import { EXIT_CODES } from "../utils/exit-codes.js";
4
+ export async function requireValidRepoOrExit(options = {}) {
5
+ // Run validation in read-only mode first?
6
+ // Should we write back validation results for 'run'?
7
+ // Specs: "Runs the same core validator used by navi validate"
8
+ // If we run 'navi run', it probably SHOULD update the last_validation timestamp to show we checked.
9
+ // So writeBack: true (default) is appropriate here.
10
+ const result = await validate({
11
+ apply: false,
12
+ strict: options.strict || false,
13
+ writeBack: true
14
+ });
15
+ if (!result.valid) {
16
+ console.log("");
17
+ console.log(kleur.red().bold("⛔ Repo invalid; run navi validate for details"));
18
+ console.log("");
19
+ // List 1-3 most important issues
20
+ const topErrors = result.errors.slice(0, 3);
21
+ topErrors.forEach((err) => {
22
+ console.log(kleur.red(` [${err.code}] ${err.path || ""}`));
23
+ });
24
+ if (result.errors.length > 3) {
25
+ console.log(kleur.dim(` ... and ${result.errors.length - 3} more.`));
26
+ }
27
+ console.log("");
28
+ process.exit(EXIT_CODES.INVALID);
29
+ }
30
+ // If validated with warnings, we usually proceed unless strict is on.
31
+ // Validator handles strict check internally?
32
+ // No, validate() returns warnings=true. command/validate.ts handled exit.
33
+ if (result.warnings && options.strict) {
34
+ console.log("");
35
+ console.log(kleur.red().bold("⛔ Repo has warnings (strict mode enabled)"));
36
+ process.exit(EXIT_CODES.INVALID);
37
+ }
38
+ }
@@ -0,0 +1,49 @@
1
+ import fs from "fs";
2
+ import fg from "fast-glob";
3
+ import { repoRoot } from "../lib.js";
4
+ import { resolveArtifactPath } from "./resolver.js";
5
+ export const DEFAULT_EXCLUDES = [
6
+ "**/node_modules/**",
7
+ "**/.git/**",
8
+ "**/dist/**",
9
+ "**/build/**",
10
+ "**/.iris/cache/**",
11
+ "**/.DS_Store"
12
+ ];
13
+ export async function resolveIncludes(includeGlobs, excludeGlobs = []) {
14
+ const root = repoRoot();
15
+ if (includeGlobs.length === 0)
16
+ return [];
17
+ const entries = await fg(includeGlobs, {
18
+ cwd: root,
19
+ ignore: [...DEFAULT_EXCLUDES, ...excludeGlobs],
20
+ dot: true,
21
+ absolute: false,
22
+ onlyFiles: true
23
+ });
24
+ // Sort for determinism
25
+ return entries.sort();
26
+ }
27
+ export function isBinary(buffer) {
28
+ const chunk = buffer.subarray(0, Math.min(buffer.length, 8000));
29
+ return chunk.includes(0);
30
+ }
31
+ const MAX_INDIVIDUAL_FILE_SIZE = 1_000_000;
32
+ export function getFileContent(relPath) {
33
+ const root = repoRoot();
34
+ const absPath = resolveArtifactPath(root, relPath);
35
+ try {
36
+ const stats = fs.statSync(absPath);
37
+ if (stats.size > MAX_INDIVIDUAL_FILE_SIZE) {
38
+ return { path: relPath, size: stats.size, skipped: "too_large" };
39
+ }
40
+ const buffer = fs.readFileSync(absPath);
41
+ if (isBinary(buffer)) {
42
+ return { path: relPath, size: stats.size, skipped: "binary" };
43
+ }
44
+ return { path: relPath, size: stats.size, content: buffer.toString("utf8") };
45
+ }
46
+ catch (e) {
47
+ return { path: relPath, size: 0, skipped: "read_error" };
48
+ }
49
+ }
@@ -0,0 +1,269 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import kleur from "kleur";
4
+ import inquirer from "inquirer";
5
+ import { ensureDir, sanitizeFolderName, spawnAsync } from "../lib.js";
6
+ import { updateManifest } from "./manifest.js";
7
+ export const TOOLS = ["claude", "cursor", "gemini", "antigravity", "codex"];
8
+ const BUNDLE_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../src/iris_bundle");
9
+ /**
10
+ * Ensure package.json exists in the target directory.
11
+ * If missing, create a minimal one.
12
+ */
13
+ function ensurePackageJson(root) {
14
+ const pkgPath = path.join(root, "package.json");
15
+ if (fs.existsSync(pkgPath)) {
16
+ console.log(kleur.gray("✓ package.json already exists"));
17
+ return;
18
+ }
19
+ const folderName = path.basename(root);
20
+ const sanitizedName = sanitizeFolderName(folderName);
21
+ const minimalPackage = {
22
+ name: sanitizedName,
23
+ version: "0.0.0",
24
+ private: true,
25
+ description: "Project using IRIS framework",
26
+ scripts: {}
27
+ };
28
+ fs.writeFileSync(pkgPath, JSON.stringify(minimalPackage, null, 2) + "\n", "utf8");
29
+ console.log(kleur.green(`✓ Created package.json (name: ${sanitizedName})`));
30
+ }
31
+ /**
32
+ * Run npm install to generate package-lock.json and install any dependencies
33
+ */
34
+ async function runNpmInstall(root) {
35
+ console.log("");
36
+ console.log(kleur.bold("Running npm install to generate lockfile..."));
37
+ try {
38
+ const result = await spawnAsync("npm", ["install"], root);
39
+ if (result.code === 0) {
40
+ console.log(kleur.green("✓ npm install completed successfully"));
41
+ // Verify lockfile was created
42
+ const lockPath = path.join(root, "package-lock.json");
43
+ if (fs.existsSync(lockPath)) {
44
+ console.log(kleur.green("✓ package-lock.json generated"));
45
+ }
46
+ return true;
47
+ }
48
+ else {
49
+ console.error(kleur.red(`✗ npm install failed with code ${result.code}`));
50
+ return false;
51
+ }
52
+ }
53
+ catch (err) {
54
+ console.error(kleur.red("✗ Failed to run npm install"));
55
+ console.error(kleur.gray(` Error: ${err instanceof Error ? err.message : String(err)}`));
56
+ return false;
57
+ }
58
+ }
59
+ export function detectTools(root) {
60
+ const checks = {
61
+ claude: fs.existsSync(path.join(root, ".claude")),
62
+ cursor: fs.existsSync(path.join(root, ".cursor")),
63
+ gemini: fs.existsSync(path.join(root, ".gemini")),
64
+ antigravity: fs.existsSync(path.join(root, ".antigravity")),
65
+ codex: fs.existsSync(path.join(root, "AGENTS.md")),
66
+ };
67
+ return TOOLS.filter(t => checks[t]);
68
+ }
69
+ export async function installIris(root, tools) {
70
+ // Step 0: Ensure package.json exists (for empty folder support)
71
+ ensurePackageJson(root);
72
+ const installedPaths = [];
73
+ const createdDirs = [];
74
+ // Bundle locations
75
+ const bundleIris = path.join(BUNDLE_ROOT, ".iris");
76
+ const bundleDoctrine = path.join(bundleIris, "aidlc");
77
+ const bundleTools = path.join(bundleIris, "tools");
78
+ // Target locations
79
+ const targetIris = path.join(root, ".iris");
80
+ const targetDoctrine = path.join(targetIris, "aidlc");
81
+ const targetTools = path.join(targetIris, "tools");
82
+ // Step C: Install IRIS core
83
+ ensureDir(targetIris);
84
+ // 1. Doctrine
85
+ let backupPath = "";
86
+ if (fs.existsSync(targetDoctrine)) {
87
+ const { overwrite } = await inquirer.prompt([
88
+ {
89
+ type: "confirm",
90
+ name: "overwrite",
91
+ message: "IRIS doctrine (.iris/aidlc) already exists. Overwrite with fresh bundle?",
92
+ default: false
93
+ }
94
+ ]);
95
+ if (overwrite) {
96
+ backupPath = `${targetDoctrine}.backup.${Date.now()}`;
97
+ fs.renameSync(targetDoctrine, backupPath);
98
+ console.log(kleur.yellow(`Backed up existing doctrine to ${path.relative(root, backupPath)}`));
99
+ copyDirRec(bundleDoctrine, targetDoctrine, installedPaths, root); // copy new
100
+ }
101
+ // else keep existing
102
+ }
103
+ else {
104
+ copyDirRec(bundleDoctrine, targetDoctrine, installedPaths, root);
105
+ }
106
+ // 2. Policy & Routes (Overwrite or keep? Usually overwrite core config if missing, but maybe prompt if exists?)
107
+ // Spec says: "Install .iris/policy.yaml... .iris/routes.yaml"
108
+ // Let's safe install: only if missing? Or overwrite?
109
+ // Spec doesn't explicitly say prompt for policy.
110
+ // "Install IRIS core... .iris/policy.yaml"
111
+ // Let's assume non-destructive for config if it exists, to preserve user edits.
112
+ safeCopy(path.join(bundleIris, "policy.yaml"), path.join(targetIris, "policy.yaml"), installedPaths, root);
113
+ safeCopy(path.join(bundleIris, "routes.yaml"), path.join(targetIris, "routes.yaml"), installedPaths, root);
114
+ // 3. State (only if missing)
115
+ if (!fs.existsSync(path.join(targetIris, "state.yaml"))) {
116
+ fs.copyFileSync(path.join(bundleIris, "state.yaml"), path.join(targetIris, "state.yaml"));
117
+ // Don't track state in manifest as "installed" usually, as it changes?
118
+ // Or do track it so we know we created it? "remove only paths that were installed"
119
+ // If we track it, uninstall might delete it. Good.
120
+ installedPaths.push({
121
+ path: ".iris/state.yaml",
122
+ type: "file",
123
+ status: "installed"
124
+ });
125
+ }
126
+ // Step D: Memory Bank Skeleton
127
+ const memoryDirs = ["intents", "units", "bolts", "standards", "logs"];
128
+ for (const d of memoryDirs) {
129
+ const p = path.join(root, "memory-bank", d);
130
+ if (!fs.existsSync(p)) {
131
+ ensureDir(p);
132
+ createdDirs.push(path.relative(root, p));
133
+ }
134
+ }
135
+ // Step E: Install Tool Wrappers & F: Mirror
136
+ for (const t of tools) {
137
+ const toolBundlePath = path.join(bundleTools, t);
138
+ if (!fs.existsSync(toolBundlePath)) {
139
+ console.warn(kleur.yellow(`Warning: No bundle found for tool '${t}' at ${toolBundlePath}`));
140
+ continue;
141
+ }
142
+ // Mirror first (always safe/overwrite in .iris/tools)
143
+ const mirrorPath = path.join(targetTools, t);
144
+ ensureDir(mirrorPath);
145
+ // We copy the WHOLE bundles content for that tool into mirror.
146
+ // Actually, we should mirror what we *attempt* to install.
147
+ // Spec: "mirror exactly what was installed (and skipped) into .iris/tools/<tool>"
148
+ // "still mirror the bundle version ... and record skipped_existing"
149
+ // So: copy bundle -> mirror.
150
+ copyDirRec(toolBundlePath, mirrorPath, [], root, true); // Don't track mirror files in manifest individually? Or do we?
151
+ // "mirror installed tool payloads... create but only filled for selected tools"
152
+ // Uninstall: "Remove .iris/tools/<tool> mirrors ... Always safe"
153
+ // So we don't strictly need to track every file inside mirror in manifest, just the tool selection implies it.
154
+ // Install to Repo Root
155
+ await installToolWrapper(t, toolBundlePath, root, installedPaths);
156
+ }
157
+ // Step G: Update Manifest
158
+ updateManifest({
159
+ tools_selected: tools,
160
+ paths_installed: installedPaths,
161
+ created_dirs: createdDirs,
162
+ doctrine_path: ".iris/aidlc",
163
+ policy_path: ".iris/policy.yaml"
164
+ });
165
+ // Step H: Run npm install to generate lockfile
166
+ await runNpmInstall(root);
167
+ }
168
+ async function installToolWrapper(tool, srcDir, root, installedPaths) {
169
+ // We need to walk the srcDir and copy to root, respecting collisions.
170
+ // For Claude: srcDir is .../tools/claude. contains .claude/...
171
+ // So we just copy * recursively from srcDir to root.
172
+ // Check for collisions first?
173
+ // Recursive walker needed.
174
+ const entries = getAllFiles(srcDir);
175
+ for (const entry of entries) {
176
+ const relPath = path.relative(srcDir, entry);
177
+ const targetPath = path.join(root, relPath);
178
+ if (fs.existsSync(targetPath)) {
179
+ // Prompt? "Prompt per tool: File exists... Overwrite?"
180
+ // Actually prompting check-per-file might be spammy.
181
+ // "Prompt per tool: File exists... Overwrite?" -> implies one prompt per file clash?
182
+ // "If a target file exists... Prompt per tool (maybe once?)... No, spec says: 'Prompt per tool: "File exists: <path>. Overwrite? (y/N)"'"
183
+ // This looks like per-file.
184
+ // Let's implement per-file prompt for safety.
185
+ // Check content difference!
186
+ if (areFilesEqual(entry, targetPath)) {
187
+ // Identical, skip prompt, mark installed (or skipped? effectively same)
188
+ installedPaths.push({ path: relPath, type: "file", status: "installed" });
189
+ continue;
190
+ }
191
+ const { overwrite } = await inquirer.prompt([
192
+ {
193
+ type: "confirm",
194
+ name: "overwrite",
195
+ message: `File exists: ${relPath}. Overwrite?`,
196
+ default: false
197
+ }
198
+ ]);
199
+ if (overwrite) {
200
+ ensureDir(path.dirname(targetPath));
201
+ fs.copyFileSync(entry, targetPath);
202
+ installedPaths.push({ path: relPath, type: "file", status: "overwritten" });
203
+ console.log(kleur.green(`Overwrote ${relPath}`));
204
+ }
205
+ else {
206
+ console.log(kleur.gray(`Skipped ${relPath}`));
207
+ installedPaths.push({ path: relPath, type: "file", status: "skipped_existing" });
208
+ }
209
+ }
210
+ else {
211
+ ensureDir(path.dirname(targetPath));
212
+ fs.copyFileSync(entry, targetPath);
213
+ installedPaths.push({ path: relPath, type: "file", status: "installed" });
214
+ }
215
+ }
216
+ }
217
+ // Helpers
218
+ function getAllFiles(dir, fileList = []) {
219
+ const files = fs.readdirSync(dir);
220
+ files.forEach(file => {
221
+ const filePath = path.join(dir, file);
222
+ if (fs.statSync(filePath).isDirectory()) {
223
+ getAllFiles(filePath, fileList);
224
+ }
225
+ else {
226
+ fileList.push(filePath);
227
+ }
228
+ });
229
+ return fileList;
230
+ }
231
+ function copyDirRec(src, dest, trackList, root, silentTrack = false) {
232
+ ensureDir(dest);
233
+ const entries = fs.readdirSync(src, { withFileTypes: true });
234
+ for (const entry of entries) {
235
+ const srcPath = path.join(src, entry.name);
236
+ const destPath = path.join(dest, entry.name);
237
+ if (entry.isDirectory()) {
238
+ copyDirRec(srcPath, destPath, trackList, root, silentTrack);
239
+ }
240
+ else {
241
+ fs.copyFileSync(srcPath, destPath);
242
+ if (!silentTrack) {
243
+ trackList.push({
244
+ path: path.relative(root, destPath),
245
+ type: "file",
246
+ status: "installed"
247
+ });
248
+ }
249
+ }
250
+ }
251
+ }
252
+ function safeCopy(src, dest, trackList, root) {
253
+ if (fs.existsSync(dest))
254
+ return; // Skip if exists
255
+ if (!fs.existsSync(src))
256
+ return; // Skip if source missing
257
+ ensureDir(path.dirname(dest));
258
+ fs.copyFileSync(src, dest);
259
+ trackList.push({
260
+ path: path.relative(root, dest),
261
+ type: "file",
262
+ status: "installed"
263
+ });
264
+ }
265
+ function areFilesEqual(a, b) {
266
+ const bufA = fs.readFileSync(a);
267
+ const bufB = fs.readFileSync(b);
268
+ return bufA.equals(bufB);
269
+ }
@@ -0,0 +1,54 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import yaml from "js-yaml";
4
+ import { writeFile } from "../lib.js";
5
+ const MANIFEST_PATH = path.join(process.cwd(), ".iris/manifest.yaml");
6
+ export function loadManifest() {
7
+ if (!fs.existsSync(MANIFEST_PATH))
8
+ return null;
9
+ try {
10
+ const doc = yaml.load(fs.readFileSync(MANIFEST_PATH, "utf8"));
11
+ // Basic migration/defaulting if needed
12
+ return {
13
+ version: 1,
14
+ installed_at: new Date().toISOString(),
15
+ tools_selected: [],
16
+ paths_installed: [],
17
+ created_dirs: [],
18
+ ...doc
19
+ };
20
+ }
21
+ catch (e) {
22
+ console.error("Failed to parse manifest:", e);
23
+ return null;
24
+ }
25
+ }
26
+ export function saveManifest(manifest) {
27
+ writeFile(MANIFEST_PATH, yaml.dump(manifest));
28
+ }
29
+ export function updateManifest(updates) {
30
+ const current = loadManifest() || {
31
+ version: 1,
32
+ installed_at: new Date().toISOString(),
33
+ tools_selected: [],
34
+ paths_installed: [],
35
+ created_dirs: [],
36
+ };
37
+ const updated = {
38
+ ...current,
39
+ ...updates,
40
+ installed_at: new Date().toISOString(), // Always update timestamp
41
+ tools_selected: Array.from(new Set([...current.tools_selected, ...(updates.tools_selected || [])])),
42
+ paths_installed: mergePaths(current.paths_installed, updates.paths_installed || []),
43
+ created_dirs: Array.from(new Set([...current.created_dirs, ...(updates.created_dirs || [])])),
44
+ };
45
+ saveManifest(updated);
46
+ }
47
+ function mergePaths(current, updates) {
48
+ // Map current paths by path string for easy update
49
+ const map = new Map();
50
+ current.forEach(p => map.set(p.path, p));
51
+ // Apply updates
52
+ updates.forEach(p => map.set(p.path, p));
53
+ return Array.from(map.values());
54
+ }