argos-harness 0.1.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 (217) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/assets/agents/auditor.md +140 -0
  4. package/assets/agents/commit-pr-pilot.md +154 -0
  5. package/assets/agents/explorer.md +93 -0
  6. package/assets/agents/implementer.md +117 -0
  7. package/assets/agents/leader.md +149 -0
  8. package/assets/agents/researcher.md +89 -0
  9. package/assets/agents/review-readability.md +87 -0
  10. package/assets/agents/review-reliability.md +100 -0
  11. package/assets/agents/review-resilience.md +87 -0
  12. package/assets/agents/review-risk.md +87 -0
  13. package/assets/agents/reviewer.md +167 -0
  14. package/assets/agents/ticket-audit.md +129 -0
  15. package/assets/hooks/argos-guard-destructive.sh +127 -0
  16. package/assets/hooks/argos-quality-gate.sh +129 -0
  17. package/assets/managed/aterrizaje.md +24 -0
  18. package/assets/managed/formato-respuesta.md +21 -0
  19. package/assets/managed/identidad.md +48 -0
  20. package/assets/managed/operaciones-seguras.md +14 -0
  21. package/assets/managed/orquestacion.md +169 -0
  22. package/assets/output-styles/argos.md +71 -0
  23. package/assets/skills/ai-sdk-5/SKILL.md +230 -0
  24. package/assets/skills/angular/SKILL.md +19 -0
  25. package/assets/skills/angular/references/architecture.md +137 -0
  26. package/assets/skills/angular/references/core.md +197 -0
  27. package/assets/skills/angular/references/forms.md +115 -0
  28. package/assets/skills/angular/references/performance.md +124 -0
  29. package/assets/skills/apollo-client/SKILL.md +61 -0
  30. package/assets/skills/app-blueprint/SKILL.md +45 -0
  31. package/assets/skills/app-blueprint/assets/module-template.md +48 -0
  32. package/assets/skills/app-blueprint/assets/system-template.md +38 -0
  33. package/assets/skills/app-blueprint/references/workflow.md +101 -0
  34. package/assets/skills/app-builder/SKILL.md +43 -0
  35. package/assets/skills/app-builder/phases/0-product.md +58 -0
  36. package/assets/skills/app-builder/phases/1-scaffold.md +33 -0
  37. package/assets/skills/app-builder/phases/10-store.md +40 -0
  38. package/assets/skills/app-builder/phases/2-data.md +34 -0
  39. package/assets/skills/app-builder/phases/3-domain.md +33 -0
  40. package/assets/skills/app-builder/phases/4-ui-nav.md +32 -0
  41. package/assets/skills/app-builder/phases/5-identity.md +30 -0
  42. package/assets/skills/app-builder/phases/6-polish.md +34 -0
  43. package/assets/skills/app-builder/phases/7-brand.md +31 -0
  44. package/assets/skills/app-builder/phases/8-web.md +30 -0
  45. package/assets/skills/app-builder/phases/9-docs.md +31 -0
  46. package/assets/skills/app-ia/SKILL.md +54 -0
  47. package/assets/skills/astro/SKILL.md +39 -0
  48. package/assets/skills/axios/SKILL.md +61 -0
  49. package/assets/skills/branch-pr/SKILL.md +200 -0
  50. package/assets/skills/bullmq/SKILL.md +55 -0
  51. package/assets/skills/chained-pr/SKILL.md +48 -0
  52. package/assets/skills/chained-pr/references/chaining-details.md +99 -0
  53. package/assets/skills/cognitive-doc-design/SKILL.md +81 -0
  54. package/assets/skills/comment-writer/SKILL.md +74 -0
  55. package/assets/skills/dashboard-ia/SKILL.md +54 -0
  56. package/assets/skills/django-drf/SKILL.md +180 -0
  57. package/assets/skills/go-testing/SKILL.md +47 -0
  58. package/assets/skills/go-testing/references/examples.md +89 -0
  59. package/assets/skills/issue-creation/SKILL.md +223 -0
  60. package/assets/skills/jira-epic/SKILL.md +306 -0
  61. package/assets/skills/jira-task/SKILL.md +382 -0
  62. package/assets/skills/judgment-day/SKILL.md +52 -0
  63. package/assets/skills/judgment-day/references/prompts-and-formats.md +98 -0
  64. package/assets/skills/lightsail-deploy/SKILL.md +44 -0
  65. package/assets/skills/lightsail-deploy/references/runbook.md +102 -0
  66. package/assets/skills/loop-back-debug/SKILL.md +102 -0
  67. package/assets/skills/mantine-form/SKILL.md +57 -0
  68. package/assets/skills/mongoose/SKILL.md +66 -0
  69. package/assets/skills/nextjs-15/SKILL.md +144 -0
  70. package/assets/skills/not-boring-mobile/SKILL.md +43 -0
  71. package/assets/skills/not-boring-mobile/references/not-boring-playbook.md +65 -0
  72. package/assets/skills/playwright/SKILL.md +315 -0
  73. package/assets/skills/pr-comments/SKILL.md +93 -0
  74. package/assets/skills/pr-create/SKILL.md +64 -0
  75. package/assets/skills/promo-video/SKILL.md +52 -0
  76. package/assets/skills/promo-video/assets/package.template.json +22 -0
  77. package/assets/skills/promo-video/assets/promo.template.tsx +469 -0
  78. package/assets/skills/promo-video/assets/theme.template.ts +27 -0
  79. package/assets/skills/promo-video/references/pipeline.md +119 -0
  80. package/assets/skills/promo-video-web/SKILL.md +51 -0
  81. package/assets/skills/promo-video-web/assets/browser-promo.template.tsx +385 -0
  82. package/assets/skills/promo-video-web/assets/capture.template.ts +70 -0
  83. package/assets/skills/promo-video-web/assets/package.template.json +26 -0
  84. package/assets/skills/promo-video-web/references/pipeline.md +84 -0
  85. package/assets/skills/pytest/SKILL.md +180 -0
  86. package/assets/skills/react-19/SKILL.md +118 -0
  87. package/assets/skills/react-hook-form/SKILL.md +59 -0
  88. package/assets/skills/react-router/SKILL.md +60 -0
  89. package/assets/skills/redux-toolkit/SKILL.md +60 -0
  90. package/assets/skills/review-diff/SKILL.md +101 -0
  91. package/assets/skills/ship-docs/SKILL.md +45 -0
  92. package/assets/skills/ship-docs/references/ship-docs-playbook.md +31 -0
  93. package/assets/skills/skill-creator/SKILL.md +97 -0
  94. package/assets/skills/skill-creator/assets/SKILL-TEMPLATE.md +68 -0
  95. package/assets/skills/skill-creator/references/skill-style-guide.md +79 -0
  96. package/assets/skills/skill-improver/SKILL.md +50 -0
  97. package/assets/skills/skill-improver/references/skill-style-guide.md +79 -0
  98. package/assets/skills/socketio/SKILL.md +58 -0
  99. package/assets/skills/spec-bootstrap/SKILL.md +62 -0
  100. package/assets/skills/store-ship/SKILL.md +52 -0
  101. package/assets/skills/store-ship/assets/android-supply.template.md +22 -0
  102. package/assets/skills/store-ship/assets/eas.template.json +32 -0
  103. package/assets/skills/store-ship/assets/maestro-flow.template.yaml +27 -0
  104. package/assets/skills/store-ship/assets/store.config.template.json +28 -0
  105. package/assets/skills/store-ship/references/pipeline.md +198 -0
  106. package/assets/skills/stripe/SKILL.md +83 -0
  107. package/assets/skills/tailwind-4/SKILL.md +193 -0
  108. package/assets/skills/tamagui/SKILL.md +60 -0
  109. package/assets/skills/tanstack-query/SKILL.md +58 -0
  110. package/assets/skills/ticket-intake/SKILL.md +55 -0
  111. package/assets/skills/typescript/SKILL.md +134 -0
  112. package/assets/skills/verify-before-done/SKILL.md +111 -0
  113. package/assets/skills/webapp-rebuilder/SKILL.md +48 -0
  114. package/assets/skills/webapp-rebuilder/assets/charter-template.md +46 -0
  115. package/assets/skills/webapp-rebuilder/references/workflow.md +47 -0
  116. package/assets/skills/winston-logging/SKILL.md +61 -0
  117. package/assets/skills/work-unit-commits/SKILL.md +84 -0
  118. package/assets/skills/zod-4/SKILL.md +210 -0
  119. package/assets/skills/zustand-5/SKILL.md +216 -0
  120. package/bin/argos.js +6 -0
  121. package/dist/commands/adopt.d.ts +35 -0
  122. package/dist/commands/adopt.d.ts.map +1 -0
  123. package/dist/commands/adopt.js +347 -0
  124. package/dist/commands/adopt.js.map +1 -0
  125. package/dist/commands/doctor.d.ts +20 -0
  126. package/dist/commands/doctor.d.ts.map +1 -0
  127. package/dist/commands/doctor.js +478 -0
  128. package/dist/commands/doctor.js.map +1 -0
  129. package/dist/commands/init.d.ts +32 -0
  130. package/dist/commands/init.d.ts.map +1 -0
  131. package/dist/commands/init.js +358 -0
  132. package/dist/commands/init.js.map +1 -0
  133. package/dist/commands/remove.d.ts +62 -0
  134. package/dist/commands/remove.d.ts.map +1 -0
  135. package/dist/commands/remove.js +487 -0
  136. package/dist/commands/remove.js.map +1 -0
  137. package/dist/commands/workspace.d.ts +73 -0
  138. package/dist/commands/workspace.d.ts.map +1 -0
  139. package/dist/commands/workspace.js +354 -0
  140. package/dist/commands/workspace.js.map +1 -0
  141. package/dist/index.d.ts +2 -0
  142. package/dist/index.d.ts.map +1 -0
  143. package/dist/index.js +24 -0
  144. package/dist/index.js.map +1 -0
  145. package/dist/lib/assets.d.ts +29 -0
  146. package/dist/lib/assets.d.ts.map +1 -0
  147. package/dist/lib/assets.js +57 -0
  148. package/dist/lib/assets.js.map +1 -0
  149. package/dist/lib/atomic-write.d.ts +17 -0
  150. package/dist/lib/atomic-write.d.ts.map +1 -0
  151. package/dist/lib/atomic-write.js +41 -0
  152. package/dist/lib/atomic-write.js.map +1 -0
  153. package/dist/lib/backup.d.ts +11 -0
  154. package/dist/lib/backup.d.ts.map +1 -0
  155. package/dist/lib/backup.js +42 -0
  156. package/dist/lib/backup.js.map +1 -0
  157. package/dist/lib/config.d.ts +36 -0
  158. package/dist/lib/config.d.ts.map +1 -0
  159. package/dist/lib/config.js +52 -0
  160. package/dist/lib/config.js.map +1 -0
  161. package/dist/lib/detect.d.ts +58 -0
  162. package/dist/lib/detect.d.ts.map +1 -0
  163. package/dist/lib/detect.js +330 -0
  164. package/dist/lib/detect.js.map +1 -0
  165. package/dist/lib/ficha.d.ts +10 -0
  166. package/dist/lib/ficha.d.ts.map +1 -0
  167. package/dist/lib/ficha.js +38 -0
  168. package/dist/lib/ficha.js.map +1 -0
  169. package/dist/lib/git.d.ts +27 -0
  170. package/dist/lib/git.d.ts.map +1 -0
  171. package/dist/lib/git.js +79 -0
  172. package/dist/lib/git.js.map +1 -0
  173. package/dist/lib/managed-files.d.ts +35 -0
  174. package/dist/lib/managed-files.d.ts.map +1 -0
  175. package/dist/lib/managed-files.js +97 -0
  176. package/dist/lib/managed-files.js.map +1 -0
  177. package/dist/lib/markers.d.ts +64 -0
  178. package/dist/lib/markers.d.ts.map +1 -0
  179. package/dist/lib/markers.js +157 -0
  180. package/dist/lib/markers.js.map +1 -0
  181. package/dist/lib/navori-import.d.ts +42 -0
  182. package/dist/lib/navori-import.d.ts.map +1 -0
  183. package/dist/lib/navori-import.js +65 -0
  184. package/dist/lib/navori-import.js.map +1 -0
  185. package/dist/lib/openclaw-agents.d.ts +53 -0
  186. package/dist/lib/openclaw-agents.d.ts.map +1 -0
  187. package/dist/lib/openclaw-agents.js +118 -0
  188. package/dist/lib/openclaw-agents.js.map +1 -0
  189. package/dist/lib/package-root.d.ts +11 -0
  190. package/dist/lib/package-root.d.ts.map +1 -0
  191. package/dist/lib/package-root.js +23 -0
  192. package/dist/lib/package-root.js.map +1 -0
  193. package/dist/lib/paths.d.ts +11 -0
  194. package/dist/lib/paths.d.ts.map +1 -0
  195. package/dist/lib/paths.js +16 -0
  196. package/dist/lib/paths.js.map +1 -0
  197. package/dist/lib/settings-merge.d.ts +125 -0
  198. package/dist/lib/settings-merge.d.ts.map +1 -0
  199. package/dist/lib/settings-merge.js +373 -0
  200. package/dist/lib/settings-merge.js.map +1 -0
  201. package/dist/lib/version.d.ts +3 -0
  202. package/dist/lib/version.d.ts.map +1 -0
  203. package/dist/lib/version.js +13 -0
  204. package/dist/lib/version.js.map +1 -0
  205. package/dist/lib/which.d.ts +8 -0
  206. package/dist/lib/which.d.ts.map +1 -0
  207. package/dist/lib/which.js +30 -0
  208. package/dist/lib/which.js.map +1 -0
  209. package/dist/lib/workspaces.d.ts +133 -0
  210. package/dist/lib/workspaces.d.ts.map +1 -0
  211. package/dist/lib/workspaces.js +241 -0
  212. package/dist/lib/workspaces.js.map +1 -0
  213. package/dist/lib/zod-messages.d.ts +4 -0
  214. package/dist/lib/zod-messages.d.ts.map +1 -0
  215. package/dist/lib/zod-messages.js +28 -0
  216. package/dist/lib/zod-messages.js.map +1 -0
  217. package/package.json +44 -0
@@ -0,0 +1,373 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { writeFileAtomic } from "./atomic-write.js";
4
+ /**
5
+ * Surgical merge of Argos's `hooks.PreToolUse` entries into
6
+ * `~/.claude/settings.json` — the JSON analogue of the managed-block model
7
+ * used for CLAUDE.md (see `lib/markers.ts`): Argos manages only its own
8
+ * entries and leaves every other key, hook, and array position in the
9
+ * user's file untouched.
10
+ *
11
+ * Ownership identification: a hook command is Argos-owned when it points at
12
+ * a script under `.../hooks/argos-*` — `isArgosHookCommand` below is the
13
+ * general check (any Argos hook), while the merge itself pins each entry to
14
+ * ONE specific `scriptPath`, so re-running `argos init` updates the exact
15
+ * same array slot instead of appending a duplicate.
16
+ */ const ARGOS_HOOK_PATH_RE = /\/hooks\/argos-/;
17
+ const BASH_HOOK_COMMAND_RE = /^bash "(.+)"$/;
18
+ const ARGOS_HOOK_BASENAME_RE = /^argos-/;
19
+ /**
20
+ * True when a hook `command` string points at an Argos-owned script (`.../hooks/argos-*`).
21
+ *
22
+ * This is a bare substring match — good enough for the non-destructive
23
+ * `doctor` diagnostic that uses it, but NOT anchored to any real directory.
24
+ * Destructive operations (deleting hook entries from the user's
25
+ * settings.json) must use `isArgosOwnedHookCommand` below instead, which
26
+ * requires the script to actually resolve inside the managed hooks
27
+ * directory.
28
+ */ export function isArgosHookCommand(command) {
29
+ return typeof command === "string" && ARGOS_HOOK_PATH_RE.test(command);
30
+ }
31
+ /** Extract the script path from Argos's own `bash "<scriptPath>"` hook command shape (see `buildHookCommand`). */ function extractBashScriptPath(command) {
32
+ return BASH_HOOK_COMMAND_RE.exec(command)?.[1];
33
+ }
34
+ /** True when `candidate` resolves to a path inside (a descendant of) `dir`, via real path resolution — not string matching. */ function isPathInsideDir(candidate, dir) {
35
+ const rel = relative(resolve(dir), resolve(candidate));
36
+ if (rel === "" || isAbsolute(rel)) return false;
37
+ return rel !== ".." && !rel.startsWith(`..${sep}`);
38
+ }
39
+ /**
40
+ * True when a hook `command` string is Argos-owned for the purposes of
41
+ * DESTRUCTIVE removal: it must match Argos's own `bash "<scriptPath>"`
42
+ * invocation shape, have a basename starting with `argos-`, AND resolve to a
43
+ * location actually inside `hooksDir` (the real, resolved
44
+ * `<claudeDir>/hooks` directory this install manages).
45
+ *
46
+ * This is intentionally stricter than `isArgosHookCommand`/`ARGOS_HOOK_PATH_RE`,
47
+ * which only substring-match `/hooks/argos-` anywhere in the command string.
48
+ * A user's own hook living at an unrelated path that happens to contain that
49
+ * substring — e.g. `/Users/x/my/hooks/argos-custom.sh`, which is NOT under
50
+ * the managed hooks directory — would false-match the substring check and be
51
+ * silently deleted. Anchoring to the resolved hooks directory (via
52
+ * `path.resolve` + an ancestor check, not `.includes()`) closes that hole.
53
+ */ function isArgosOwnedHookCommand(command, hooksDir) {
54
+ if (typeof command !== "string") return false;
55
+ const scriptPath = extractBashScriptPath(command);
56
+ if (!scriptPath) return false;
57
+ if (!ARGOS_HOOK_BASENAME_RE.test(basename(scriptPath))) return false;
58
+ return isPathInsideDir(scriptPath, hooksDir);
59
+ }
60
+ function isPlainObject(v) {
61
+ return typeof v === "object" && v !== null && !Array.isArray(v);
62
+ }
63
+ function buildHookCommand(spec) {
64
+ const hook = {
65
+ type: "command",
66
+ command: `bash "${spec.scriptPath}"`
67
+ };
68
+ if (spec.timeout !== undefined) hook.timeout = spec.timeout;
69
+ if (spec.statusMessage !== undefined) hook.statusMessage = spec.statusMessage;
70
+ return hook;
71
+ }
72
+ function errorMessage(err) {
73
+ return err instanceof Error ? err.message : String(err);
74
+ }
75
+ /**
76
+ * Merge `specs` into `settingsPath` (normally `resolveClaudeDir()/settings.json`).
77
+ *
78
+ * - Missing file → starts from `{}` (created).
79
+ * - An existing hook entry whose command already targets a spec's
80
+ * `scriptPath` → overwritten in place, at its current array index (never
81
+ * moved, never removed).
82
+ * - No existing entry for a spec → appended into the first `PreToolUse`
83
+ * bucket whose `matcher` equals the spec's matcher, or a new bucket is
84
+ * pushed at the end of `PreToolUse` if none matches. Every other bucket,
85
+ * hook, and top-level key is left exactly as found.
86
+ * - `options.removeScriptPaths` → any existing entry whose command targets
87
+ * one of those paths is stripped out entirely (not replaced) — for a hook
88
+ * whose script write just failed, so it never leaves a dangling entry.
89
+ * - Corrupt JSON (unparsable, not an object, `hooks` present but not an
90
+ * object, or `hooks.PreToolUse` present but not an array) → returns
91
+ * `status: "error"` and writes NOTHING. Never clobber a file the caller
92
+ * can't safely reason about, and never silently discard a foreign `hooks`
93
+ * value just because its shape is unexpected.
94
+ * - Concurrency guard: the file's mtime is captured at read time and
95
+ * re-checked immediately before the write; if it moved, someone else wrote
96
+ * settings.json in the meantime and this call refuses with `status:
97
+ * "error"` rather than silently clobbering that write (last-writer-wins).
98
+ * - The write itself is atomic (temp file + rename, see
99
+ * lib/atomic-write.ts) — a crash mid-write can never leave settings.json
100
+ * torn, which would otherwise hard-block every subsequent Bash call.
101
+ *
102
+ * The file is always re-serialized with 2-space indentation on a write —
103
+ * this normalizes the user's original formatting/whitespace, but every key
104
+ * and value it did not own is preserved (structurally byte-identical
105
+ * content, not necessarily byte-identical bytes).
106
+ */ export function mergeHooksIntoSettings(settingsPath, specs, options = {}) {
107
+ const existed = existsSync(settingsPath);
108
+ let raw = "{}";
109
+ let mtimeAtRead;
110
+ if (existed) {
111
+ try {
112
+ raw = readFileSync(settingsPath, "utf-8");
113
+ mtimeAtRead = statSync(settingsPath).mtimeMs;
114
+ } catch (err) {
115
+ return {
116
+ status: "error",
117
+ detail: errorMessage(err)
118
+ };
119
+ }
120
+ }
121
+ let settings;
122
+ try {
123
+ const parsed = raw.trim().length === 0 ? {} : JSON.parse(raw);
124
+ if (!isPlainObject(parsed)) {
125
+ return {
126
+ status: "error",
127
+ detail: "settings.json existente no es un objeto JSON — arreglalo a mano."
128
+ };
129
+ }
130
+ settings = parsed;
131
+ } catch (err) {
132
+ return {
133
+ status: "error",
134
+ detail: `settings.json existente tiene JSON inválido (${errorMessage(err)}) — arreglalo a mano y volvé a correr argos init.`
135
+ };
136
+ }
137
+ const before = JSON.stringify(settings);
138
+ if (settings.hooks !== undefined && !isPlainObject(settings.hooks)) {
139
+ return {
140
+ status: "error",
141
+ detail: "settings.json tiene hooks que no es un objeto — arreglalo a mano."
142
+ };
143
+ }
144
+ const hooksRoot = isPlainObject(settings.hooks) ? settings.hooks : {};
145
+ const preToolUseRaw = hooksRoot.PreToolUse;
146
+ if (preToolUseRaw !== undefined && !Array.isArray(preToolUseRaw)) {
147
+ return {
148
+ status: "error",
149
+ detail: "settings.json tiene hooks.PreToolUse que no es un array — arreglalo a mano."
150
+ };
151
+ }
152
+ const preToolUse = Array.isArray(preToolUseRaw) ? preToolUseRaw : [];
153
+ const removeScriptPaths = options.removeScriptPaths ?? [];
154
+ if (removeScriptPaths.length > 0) {
155
+ for (const bucket of preToolUse){
156
+ if (!isPlainObject(bucket) || !Array.isArray(bucket.hooks)) continue;
157
+ bucket.hooks = bucket.hooks.filter((h)=>{
158
+ if (!isPlainObject(h) || typeof h.command !== "string") return true;
159
+ const command = h.command;
160
+ return !removeScriptPaths.some((p)=>command.includes(p));
161
+ });
162
+ }
163
+ }
164
+ for (const spec of specs){
165
+ const desired = buildHookCommand(spec);
166
+ // Find our own previous entry anywhere in PreToolUse (matched by
167
+ // scriptPath), to update it in place without moving it.
168
+ let found = false;
169
+ for (const bucket of preToolUse){
170
+ if (!isPlainObject(bucket) || !Array.isArray(bucket.hooks)) continue;
171
+ const bucketHooks = bucket.hooks;
172
+ const idx = bucketHooks.findIndex((h)=>isPlainObject(h) && typeof h.command === "string" && h.command.includes(spec.scriptPath));
173
+ if (idx >= 0) {
174
+ bucketHooks[idx] = desired;
175
+ found = true;
176
+ break;
177
+ }
178
+ }
179
+ if (!found) {
180
+ let bucket = preToolUse.find((b)=>isPlainObject(b) && b.matcher === spec.matcher && Array.isArray(b.hooks));
181
+ if (!bucket) {
182
+ bucket = {
183
+ matcher: spec.matcher,
184
+ hooks: []
185
+ };
186
+ preToolUse.push(bucket);
187
+ }
188
+ bucket.hooks.push(desired);
189
+ }
190
+ }
191
+ hooksRoot.PreToolUse = preToolUse;
192
+ settings.hooks = hooksRoot;
193
+ if (existed && JSON.stringify(settings) === before) {
194
+ return {
195
+ status: "unchanged"
196
+ };
197
+ }
198
+ options.onBeforeWrite?.();
199
+ // Cheap concurrency guard: this is a read-modify-write with no locking, so
200
+ // re-stat the file right before writing and bail if its mtime moved since
201
+ // we read it — someone else wrote settings.json in between, and a plain
202
+ // last-writer-wins here would silently drop their change.
203
+ if (existed && mtimeAtRead !== undefined) {
204
+ let mtimeNow;
205
+ try {
206
+ mtimeNow = statSync(settingsPath).mtimeMs;
207
+ } catch {
208
+ mtimeNow = undefined;
209
+ }
210
+ if (mtimeNow !== undefined && mtimeNow !== mtimeAtRead) {
211
+ return {
212
+ status: "error",
213
+ detail: "settings.json cambió durante el merge — reintenta."
214
+ };
215
+ }
216
+ }
217
+ try {
218
+ writeFileAtomic(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
219
+ } catch (err) {
220
+ return {
221
+ status: "error",
222
+ detail: errorMessage(err)
223
+ };
224
+ }
225
+ return {
226
+ status: existed ? "updated" : "created"
227
+ };
228
+ }
229
+ /**
230
+ * Symmetric counterpart to `mergeHooksIntoSettings`: strips every Argos-owned
231
+ * hook entry (any command matched by `isArgosOwnedHookCommand` against
232
+ * `hooksDir`, not just the current `HOOK_IDS`) out of `hooks.PreToolUse`,
233
+ * wherever it's found. Foreign hooks, buckets, and every other top-level key
234
+ * are left exactly as found — same ownership/refusal contract as the merge
235
+ * path:
236
+ * - `hooksDir` is the resolved `<claudeDir>/hooks` directory this install
237
+ * manages (normally `join(resolveClaudeDir(), "hooks")`). A hook is only
238
+ * ever treated as Argos-owned — and therefore deletable — when its script
239
+ * path actually resolves inside `hooksDir` AND its basename starts with
240
+ * `argos-`; this is deliberately NOT the same as `isArgosHookCommand`'s
241
+ * bare `/hooks/argos-` substring match, which would also match (and
242
+ * delete) an unrelated user hook at a path like
243
+ * `/Users/x/my/hooks/argos-custom.sh`.
244
+ * - Missing file, or no `hooks`/`hooks.PreToolUse` at all → "unchanged",
245
+ * nothing to remove.
246
+ * - Corrupt JSON (unparsable, not an object, `hooks` present but not an
247
+ * object, or `hooks.PreToolUse` present but not an array) → `status:
248
+ * "error"`, writes NOTHING. Never guess at a shape it can't verify.
249
+ * - A `PreToolUse` bucket that becomes empty after removing Argos's own
250
+ * entries is dropped entirely (it only ever existed to hold them — see
251
+ * `mergeHooksIntoSettings`, which creates a fresh bucket per matcher on
252
+ * demand); a bucket that still has foreign hooks left (including one with
253
+ * ZERO Argos-owned hooks in it) keeps its matcher, its hooks, and its
254
+ * position untouched.
255
+ * - Same mtime concurrency guard and atomic write as the merge path.
256
+ */ export function removeAllArgosHooksFromSettings(settingsPath, hooksDir, options = {}) {
257
+ if (!existsSync(settingsPath)) return {
258
+ status: "unchanged",
259
+ removedCount: 0
260
+ };
261
+ let raw;
262
+ let mtimeAtRead;
263
+ try {
264
+ raw = readFileSync(settingsPath, "utf-8");
265
+ mtimeAtRead = statSync(settingsPath).mtimeMs;
266
+ } catch (err) {
267
+ return {
268
+ status: "error",
269
+ detail: errorMessage(err),
270
+ removedCount: 0
271
+ };
272
+ }
273
+ let settings;
274
+ try {
275
+ const parsed = raw.trim().length === 0 ? {} : JSON.parse(raw);
276
+ if (!isPlainObject(parsed)) {
277
+ return {
278
+ status: "error",
279
+ detail: "settings.json existente no es un objeto JSON — arreglalo a mano.",
280
+ removedCount: 0
281
+ };
282
+ }
283
+ settings = parsed;
284
+ } catch (err) {
285
+ return {
286
+ status: "error",
287
+ detail: `settings.json existente tiene JSON inválido (${errorMessage(err)}) — arreglalo a mano.`,
288
+ removedCount: 0
289
+ };
290
+ }
291
+ if (settings.hooks !== undefined && !isPlainObject(settings.hooks)) {
292
+ return {
293
+ status: "error",
294
+ detail: "settings.json tiene hooks que no es un objeto — arreglalo a mano.",
295
+ removedCount: 0
296
+ };
297
+ }
298
+ const hooksRoot = settings.hooks;
299
+ if (!hooksRoot) return {
300
+ status: "unchanged",
301
+ removedCount: 0
302
+ };
303
+ const preToolUseRaw = hooksRoot.PreToolUse;
304
+ if (preToolUseRaw !== undefined && !Array.isArray(preToolUseRaw)) {
305
+ return {
306
+ status: "error",
307
+ detail: "settings.json tiene hooks.PreToolUse que no es un array — arreglalo a mano.",
308
+ removedCount: 0
309
+ };
310
+ }
311
+ if (!Array.isArray(preToolUseRaw) || preToolUseRaw.length === 0) {
312
+ return {
313
+ status: "unchanged",
314
+ removedCount: 0
315
+ };
316
+ }
317
+ let removedCount = 0;
318
+ const nextPreToolUse = [];
319
+ for (const bucket of preToolUseRaw){
320
+ if (!isPlainObject(bucket) || !Array.isArray(bucket.hooks)) {
321
+ nextPreToolUse.push(bucket);
322
+ continue;
323
+ }
324
+ const before = bucket.hooks.length;
325
+ const filteredHooks = bucket.hooks.filter((h)=>!(isPlainObject(h) && isArgosOwnedHookCommand(h.command, hooksDir)));
326
+ removedCount += before - filteredHooks.length;
327
+ if (filteredHooks.length === 0) continue; // bucket only ever held Argos's own entries
328
+ nextPreToolUse.push({
329
+ ...bucket,
330
+ hooks: filteredHooks
331
+ });
332
+ }
333
+ if (removedCount === 0) return {
334
+ status: "unchanged",
335
+ removedCount: 0
336
+ };
337
+ if (options.dryRun) return {
338
+ status: "removed",
339
+ removedCount
340
+ };
341
+ hooksRoot.PreToolUse = nextPreToolUse;
342
+ settings.hooks = hooksRoot;
343
+ options.onBeforeWrite?.();
344
+ // Same cheap concurrency guard as mergeHooksIntoSettings.
345
+ let mtimeNow;
346
+ try {
347
+ mtimeNow = statSync(settingsPath).mtimeMs;
348
+ } catch {
349
+ mtimeNow = undefined;
350
+ }
351
+ if (mtimeNow !== undefined && mtimeNow !== mtimeAtRead) {
352
+ return {
353
+ status: "error",
354
+ detail: "settings.json cambió durante el remove — reintenta.",
355
+ removedCount: 0
356
+ };
357
+ }
358
+ try {
359
+ writeFileAtomic(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
360
+ } catch (err) {
361
+ return {
362
+ status: "error",
363
+ detail: errorMessage(err),
364
+ removedCount: 0
365
+ };
366
+ }
367
+ return {
368
+ status: "removed",
369
+ removedCount
370
+ };
371
+ }
372
+
373
+ //# sourceMappingURL=settings-merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/settings-merge.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { writeFileAtomic } from \"./atomic-write.js\";\n\n/**\n * Surgical merge of Argos's `hooks.PreToolUse` entries into\n * `~/.claude/settings.json` — the JSON analogue of the managed-block model\n * used for CLAUDE.md (see `lib/markers.ts`): Argos manages only its own\n * entries and leaves every other key, hook, and array position in the\n * user's file untouched.\n *\n * Ownership identification: a hook command is Argos-owned when it points at\n * a script under `.../hooks/argos-*` — `isArgosHookCommand` below is the\n * general check (any Argos hook), while the merge itself pins each entry to\n * ONE specific `scriptPath`, so re-running `argos init` updates the exact\n * same array slot instead of appending a duplicate.\n */\n\nconst ARGOS_HOOK_PATH_RE = /\\/hooks\\/argos-/;\nconst BASH_HOOK_COMMAND_RE = /^bash \"(.+)\"$/;\nconst ARGOS_HOOK_BASENAME_RE = /^argos-/;\n\n/**\n * True when a hook `command` string points at an Argos-owned script (`.../hooks/argos-*`).\n *\n * This is a bare substring match — good enough for the non-destructive\n * `doctor` diagnostic that uses it, but NOT anchored to any real directory.\n * Destructive operations (deleting hook entries from the user's\n * settings.json) must use `isArgosOwnedHookCommand` below instead, which\n * requires the script to actually resolve inside the managed hooks\n * directory.\n */\nexport function isArgosHookCommand(command: unknown): command is string {\n return typeof command === \"string\" && ARGOS_HOOK_PATH_RE.test(command);\n}\n\n/** Extract the script path from Argos's own `bash \"<scriptPath>\"` hook command shape (see `buildHookCommand`). */\nfunction extractBashScriptPath(command: string): string | undefined {\n return BASH_HOOK_COMMAND_RE.exec(command)?.[1];\n}\n\n/** True when `candidate` resolves to a path inside (a descendant of) `dir`, via real path resolution — not string matching. */\nfunction isPathInsideDir(candidate: string, dir: string): boolean {\n const rel = relative(resolve(dir), resolve(candidate));\n if (rel === \"\" || isAbsolute(rel)) return false;\n return rel !== \"..\" && !rel.startsWith(`..${sep}`);\n}\n\n/**\n * True when a hook `command` string is Argos-owned for the purposes of\n * DESTRUCTIVE removal: it must match Argos's own `bash \"<scriptPath>\"`\n * invocation shape, have a basename starting with `argos-`, AND resolve to a\n * location actually inside `hooksDir` (the real, resolved\n * `<claudeDir>/hooks` directory this install manages).\n *\n * This is intentionally stricter than `isArgosHookCommand`/`ARGOS_HOOK_PATH_RE`,\n * which only substring-match `/hooks/argos-` anywhere in the command string.\n * A user's own hook living at an unrelated path that happens to contain that\n * substring — e.g. `/Users/x/my/hooks/argos-custom.sh`, which is NOT under\n * the managed hooks directory — would false-match the substring check and be\n * silently deleted. Anchoring to the resolved hooks directory (via\n * `path.resolve` + an ancestor check, not `.includes()`) closes that hole.\n */\nfunction isArgosOwnedHookCommand(command: unknown, hooksDir: string): command is string {\n if (typeof command !== \"string\") return false;\n const scriptPath = extractBashScriptPath(command);\n if (!scriptPath) return false;\n if (!ARGOS_HOOK_BASENAME_RE.test(basename(scriptPath))) return false;\n return isPathInsideDir(scriptPath, hooksDir);\n}\n\nexport interface ArgosHookSpec {\n /** Absolute path to the installed hook script, e.g. `<claudeDir>/hooks/argos-guard-destructive.sh`. */\n scriptPath: string;\n /** Claude Code hook matcher, e.g. \"Bash\". */\n matcher: string;\n /** Hook timeout in seconds (Claude Code's own outer bound on the hook process). */\n timeout?: number;\n statusMessage?: string;\n}\n\nexport type SettingsMergeStatus = \"created\" | \"updated\" | \"unchanged\" | \"error\";\n\nexport interface SettingsMergeResult {\n status: SettingsMergeStatus;\n detail?: string;\n}\n\nexport interface MergeHooksOptions {\n /**\n * Script paths whose entries should be stripped out of `hooks.PreToolUse`\n * entirely (not overwritten with a new `specs` entry — REMOVED), wherever\n * they're found. Used by `argos init` for a hook whose script write just\n * failed: an entry pointing at a script that doesn't exist (or is broken)\n * is a dangling PreToolUse entry that hard-blocks every Bash call, whether\n * it was never written this run or used to work and just broke.\n */\n removeScriptPaths?: string[];\n /**\n * Test-only seam: invoked immediately before the pre-write mtime re-check\n * (see the concurrency guard below), so a test can simulate a concurrent\n * writer racing this merge — mutate `settingsPath` from inside the\n * callback, then let the merge proceed and observe it refuse. Never used\n * outside tests.\n */\n onBeforeWrite?: () => void;\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction buildHookCommand(spec: ArgosHookSpec): Record<string, unknown> {\n const hook: Record<string, unknown> = { type: \"command\", command: `bash \"${spec.scriptPath}\"` };\n if (spec.timeout !== undefined) hook.timeout = spec.timeout;\n if (spec.statusMessage !== undefined) hook.statusMessage = spec.statusMessage;\n return hook;\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Merge `specs` into `settingsPath` (normally `resolveClaudeDir()/settings.json`).\n *\n * - Missing file → starts from `{}` (created).\n * - An existing hook entry whose command already targets a spec's\n * `scriptPath` → overwritten in place, at its current array index (never\n * moved, never removed).\n * - No existing entry for a spec → appended into the first `PreToolUse`\n * bucket whose `matcher` equals the spec's matcher, or a new bucket is\n * pushed at the end of `PreToolUse` if none matches. Every other bucket,\n * hook, and top-level key is left exactly as found.\n * - `options.removeScriptPaths` → any existing entry whose command targets\n * one of those paths is stripped out entirely (not replaced) — for a hook\n * whose script write just failed, so it never leaves a dangling entry.\n * - Corrupt JSON (unparsable, not an object, `hooks` present but not an\n * object, or `hooks.PreToolUse` present but not an array) → returns\n * `status: \"error\"` and writes NOTHING. Never clobber a file the caller\n * can't safely reason about, and never silently discard a foreign `hooks`\n * value just because its shape is unexpected.\n * - Concurrency guard: the file's mtime is captured at read time and\n * re-checked immediately before the write; if it moved, someone else wrote\n * settings.json in the meantime and this call refuses with `status:\n * \"error\"` rather than silently clobbering that write (last-writer-wins).\n * - The write itself is atomic (temp file + rename, see\n * lib/atomic-write.ts) — a crash mid-write can never leave settings.json\n * torn, which would otherwise hard-block every subsequent Bash call.\n *\n * The file is always re-serialized with 2-space indentation on a write —\n * this normalizes the user's original formatting/whitespace, but every key\n * and value it did not own is preserved (structurally byte-identical\n * content, not necessarily byte-identical bytes).\n */\nexport function mergeHooksIntoSettings(\n settingsPath: string,\n specs: ArgosHookSpec[],\n options: MergeHooksOptions = {},\n): SettingsMergeResult {\n const existed = existsSync(settingsPath);\n let raw = \"{}\";\n let mtimeAtRead: number | undefined;\n if (existed) {\n try {\n raw = readFileSync(settingsPath, \"utf-8\");\n mtimeAtRead = statSync(settingsPath).mtimeMs;\n } catch (err) {\n return { status: \"error\", detail: errorMessage(err) };\n }\n }\n\n let settings: Record<string, unknown>;\n try {\n const parsed: unknown = raw.trim().length === 0 ? {} : JSON.parse(raw);\n if (!isPlainObject(parsed)) {\n return { status: \"error\", detail: \"settings.json existente no es un objeto JSON — arreglalo a mano.\" };\n }\n settings = parsed;\n } catch (err) {\n return {\n status: \"error\",\n detail: `settings.json existente tiene JSON inválido (${errorMessage(err)}) — arreglalo a mano y volvé a correr argos init.`,\n };\n }\n\n const before = JSON.stringify(settings);\n\n if (settings.hooks !== undefined && !isPlainObject(settings.hooks)) {\n return {\n status: \"error\",\n detail: \"settings.json tiene hooks que no es un objeto — arreglalo a mano.\",\n };\n }\n const hooksRoot: Record<string, unknown> = isPlainObject(settings.hooks)\n ? (settings.hooks as Record<string, unknown>)\n : {};\n\n const preToolUseRaw = hooksRoot.PreToolUse;\n if (preToolUseRaw !== undefined && !Array.isArray(preToolUseRaw)) {\n return {\n status: \"error\",\n detail: \"settings.json tiene hooks.PreToolUse que no es un array — arreglalo a mano.\",\n };\n }\n const preToolUse: unknown[] = Array.isArray(preToolUseRaw) ? preToolUseRaw : [];\n\n const removeScriptPaths = options.removeScriptPaths ?? [];\n if (removeScriptPaths.length > 0) {\n for (const bucket of preToolUse) {\n if (!isPlainObject(bucket) || !Array.isArray(bucket.hooks)) continue;\n bucket.hooks = (bucket.hooks as unknown[]).filter((h) => {\n if (!isPlainObject(h) || typeof h.command !== \"string\") return true;\n const command = h.command;\n return !removeScriptPaths.some((p) => command.includes(p));\n });\n }\n }\n\n for (const spec of specs) {\n const desired = buildHookCommand(spec);\n\n // Find our own previous entry anywhere in PreToolUse (matched by\n // scriptPath), to update it in place without moving it.\n let found = false;\n for (const bucket of preToolUse) {\n if (!isPlainObject(bucket) || !Array.isArray(bucket.hooks)) continue;\n const bucketHooks = bucket.hooks as unknown[];\n const idx = bucketHooks.findIndex(\n (h) => isPlainObject(h) && typeof h.command === \"string\" && h.command.includes(spec.scriptPath),\n );\n if (idx >= 0) {\n bucketHooks[idx] = desired;\n found = true;\n break;\n }\n }\n\n if (!found) {\n let bucket = preToolUse.find(\n (b): b is Record<string, unknown> => isPlainObject(b) && b.matcher === spec.matcher && Array.isArray(b.hooks),\n );\n if (!bucket) {\n bucket = { matcher: spec.matcher, hooks: [] };\n preToolUse.push(bucket);\n }\n (bucket.hooks as unknown[]).push(desired);\n }\n }\n\n hooksRoot.PreToolUse = preToolUse;\n settings.hooks = hooksRoot;\n\n if (existed && JSON.stringify(settings) === before) {\n return { status: \"unchanged\" };\n }\n\n options.onBeforeWrite?.();\n\n // Cheap concurrency guard: this is a read-modify-write with no locking, so\n // re-stat the file right before writing and bail if its mtime moved since\n // we read it — someone else wrote settings.json in between, and a plain\n // last-writer-wins here would silently drop their change.\n if (existed && mtimeAtRead !== undefined) {\n let mtimeNow: number | undefined;\n try {\n mtimeNow = statSync(settingsPath).mtimeMs;\n } catch {\n mtimeNow = undefined;\n }\n if (mtimeNow !== undefined && mtimeNow !== mtimeAtRead) {\n return {\n status: \"error\",\n detail: \"settings.json cambió durante el merge — reintenta.\",\n };\n }\n }\n\n try {\n writeFileAtomic(settingsPath, `${JSON.stringify(settings, null, 2)}\\n`);\n } catch (err) {\n return { status: \"error\", detail: errorMessage(err) };\n }\n\n return { status: existed ? \"updated\" : \"created\" };\n}\n\nexport type RemoveHooksStatus = \"removed\" | \"unchanged\" | \"error\";\n\nexport interface RemoveHooksResult {\n status: RemoveHooksStatus;\n detail?: string;\n /** Count of Argos-owned hook entries actually removed (0 for \"unchanged\"/\"error\"). */\n removedCount: number;\n}\n\nexport interface RemoveHooksOptions {\n /**\n * Compute what WOULD be removed without writing anything — `argos remove`'s\n * preview mode. Corrupt-JSON refusal still applies identically in dry-run:\n * a caller must not be told \"removed\" for a file it can't safely reason\n * about.\n */\n dryRun?: boolean;\n /** Same test-only seam as `MergeHooksOptions.onBeforeWrite` above. */\n onBeforeWrite?: () => void;\n}\n\n/**\n * Symmetric counterpart to `mergeHooksIntoSettings`: strips every Argos-owned\n * hook entry (any command matched by `isArgosOwnedHookCommand` against\n * `hooksDir`, not just the current `HOOK_IDS`) out of `hooks.PreToolUse`,\n * wherever it's found. Foreign hooks, buckets, and every other top-level key\n * are left exactly as found — same ownership/refusal contract as the merge\n * path:\n * - `hooksDir` is the resolved `<claudeDir>/hooks` directory this install\n * manages (normally `join(resolveClaudeDir(), \"hooks\")`). A hook is only\n * ever treated as Argos-owned — and therefore deletable — when its script\n * path actually resolves inside `hooksDir` AND its basename starts with\n * `argos-`; this is deliberately NOT the same as `isArgosHookCommand`'s\n * bare `/hooks/argos-` substring match, which would also match (and\n * delete) an unrelated user hook at a path like\n * `/Users/x/my/hooks/argos-custom.sh`.\n * - Missing file, or no `hooks`/`hooks.PreToolUse` at all → \"unchanged\",\n * nothing to remove.\n * - Corrupt JSON (unparsable, not an object, `hooks` present but not an\n * object, or `hooks.PreToolUse` present but not an array) → `status:\n * \"error\"`, writes NOTHING. Never guess at a shape it can't verify.\n * - A `PreToolUse` bucket that becomes empty after removing Argos's own\n * entries is dropped entirely (it only ever existed to hold them — see\n * `mergeHooksIntoSettings`, which creates a fresh bucket per matcher on\n * demand); a bucket that still has foreign hooks left (including one with\n * ZERO Argos-owned hooks in it) keeps its matcher, its hooks, and its\n * position untouched.\n * - Same mtime concurrency guard and atomic write as the merge path.\n */\nexport function removeAllArgosHooksFromSettings(\n settingsPath: string,\n hooksDir: string,\n options: RemoveHooksOptions = {},\n): RemoveHooksResult {\n if (!existsSync(settingsPath)) return { status: \"unchanged\", removedCount: 0 };\n\n let raw: string;\n let mtimeAtRead: number;\n try {\n raw = readFileSync(settingsPath, \"utf-8\");\n mtimeAtRead = statSync(settingsPath).mtimeMs;\n } catch (err) {\n return { status: \"error\", detail: errorMessage(err), removedCount: 0 };\n }\n\n let settings: Record<string, unknown>;\n try {\n const parsed: unknown = raw.trim().length === 0 ? {} : JSON.parse(raw);\n if (!isPlainObject(parsed)) {\n return {\n status: \"error\",\n detail: \"settings.json existente no es un objeto JSON — arreglalo a mano.\",\n removedCount: 0,\n };\n }\n settings = parsed;\n } catch (err) {\n return {\n status: \"error\",\n detail: `settings.json existente tiene JSON inválido (${errorMessage(err)}) — arreglalo a mano.`,\n removedCount: 0,\n };\n }\n\n if (settings.hooks !== undefined && !isPlainObject(settings.hooks)) {\n return {\n status: \"error\",\n detail: \"settings.json tiene hooks que no es un objeto — arreglalo a mano.\",\n removedCount: 0,\n };\n }\n const hooksRoot = settings.hooks as Record<string, unknown> | undefined;\n if (!hooksRoot) return { status: \"unchanged\", removedCount: 0 };\n\n const preToolUseRaw = hooksRoot.PreToolUse;\n if (preToolUseRaw !== undefined && !Array.isArray(preToolUseRaw)) {\n return {\n status: \"error\",\n detail: \"settings.json tiene hooks.PreToolUse que no es un array — arreglalo a mano.\",\n removedCount: 0,\n };\n }\n if (!Array.isArray(preToolUseRaw) || preToolUseRaw.length === 0) {\n return { status: \"unchanged\", removedCount: 0 };\n }\n\n let removedCount = 0;\n const nextPreToolUse: unknown[] = [];\n for (const bucket of preToolUseRaw) {\n if (!isPlainObject(bucket) || !Array.isArray(bucket.hooks)) {\n nextPreToolUse.push(bucket);\n continue;\n }\n const before = bucket.hooks.length;\n const filteredHooks = (bucket.hooks as unknown[]).filter(\n (h) => !(isPlainObject(h) && isArgosOwnedHookCommand(h.command, hooksDir)),\n );\n removedCount += before - filteredHooks.length;\n if (filteredHooks.length === 0) continue; // bucket only ever held Argos's own entries\n nextPreToolUse.push({ ...bucket, hooks: filteredHooks });\n }\n\n if (removedCount === 0) return { status: \"unchanged\", removedCount: 0 };\n if (options.dryRun) return { status: \"removed\", removedCount };\n\n hooksRoot.PreToolUse = nextPreToolUse;\n settings.hooks = hooksRoot;\n\n options.onBeforeWrite?.();\n\n // Same cheap concurrency guard as mergeHooksIntoSettings.\n let mtimeNow: number | undefined;\n try {\n mtimeNow = statSync(settingsPath).mtimeMs;\n } catch {\n mtimeNow = undefined;\n }\n if (mtimeNow !== undefined && mtimeNow !== mtimeAtRead) {\n return {\n status: \"error\",\n detail: \"settings.json cambió durante el remove — reintenta.\",\n removedCount: 0,\n };\n }\n\n try {\n writeFileAtomic(settingsPath, `${JSON.stringify(settings, null, 2)}\\n`);\n } catch (err) {\n return { status: \"error\", detail: errorMessage(err), removedCount: 0 };\n }\n\n return { status: \"removed\", removedCount };\n}\n"],"names":["existsSync","readFileSync","statSync","basename","isAbsolute","relative","resolve","sep","writeFileAtomic","ARGOS_HOOK_PATH_RE","BASH_HOOK_COMMAND_RE","ARGOS_HOOK_BASENAME_RE","isArgosHookCommand","command","test","extractBashScriptPath","exec","isPathInsideDir","candidate","dir","rel","startsWith","isArgosOwnedHookCommand","hooksDir","scriptPath","isPlainObject","v","Array","isArray","buildHookCommand","spec","hook","type","timeout","undefined","statusMessage","errorMessage","err","Error","message","String","mergeHooksIntoSettings","settingsPath","specs","options","existed","raw","mtimeAtRead","mtimeMs","status","detail","settings","parsed","trim","length","JSON","parse","before","stringify","hooks","hooksRoot","preToolUseRaw","PreToolUse","preToolUse","removeScriptPaths","bucket","filter","h","some","p","includes","desired","found","bucketHooks","idx","findIndex","find","b","matcher","push","onBeforeWrite","mtimeNow","removeAllArgosHooksFromSettings","removedCount","nextPreToolUse","filteredHooks","dryRun"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** Read the `@argos/cli` package version (used to stamp every marker Argos writes). */
2
+ export declare function readCliVersion(fromUrl?: string): string;
3
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/lib/version.ts"],"names":[],"mappings":"AAIA,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,OAAO,GAAE,MAAwB,GAAG,MAAM,CASxE"}
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolvePackageRoot } from "./package-root.js";
4
+ /** Read the `@argos/cli` package version (used to stamp every marker Argos writes). */ export function readCliVersion(fromUrl = import.meta.url) {
5
+ try {
6
+ const pkg = JSON.parse(readFileSync(join(resolvePackageRoot(fromUrl), "package.json"), "utf-8"));
7
+ return pkg.version ?? "0.0.0";
8
+ } catch {
9
+ return "0.0.0";
10
+ }
11
+ }
12
+
13
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/version.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { resolvePackageRoot } from \"./package-root.js\";\n\n/** Read the `@argos/cli` package version (used to stamp every marker Argos writes). */\nexport function readCliVersion(fromUrl: string = import.meta.url): string {\n try {\n const pkg = JSON.parse(readFileSync(join(resolvePackageRoot(fromUrl), \"package.json\"), \"utf-8\")) as {\n version?: string;\n };\n return pkg.version ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n"],"names":["readFileSync","join","resolvePackageRoot","readCliVersion","fromUrl","url","pkg","JSON","parse","version"],"mappings":"AAAA,SAASA,YAAY,QAAQ,UAAU;AACvC,SAASC,IAAI,QAAQ,YAAY;AACjC,SAASC,kBAAkB,QAAQ,oBAAoB;AAEvD,qFAAqF,GACrF,OAAO,SAASC,eAAeC,UAAkB,YAAYC,GAAG;IAC9D,IAAI;QACF,MAAMC,MAAMC,KAAKC,KAAK,CAACR,aAAaC,KAAKC,mBAAmBE,UAAU,iBAAiB;QAGvF,OAAOE,IAAIG,OAAO,IAAI;IACxB,EAAE,OAAM;QACN,OAAO;IACT;AACF"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Check whether a binary exists in PATH without spawning a shell — safer
3
+ * than `execSync('command -v ...')`, which risks shell interpretation of the
4
+ * name. Used to gate `argos workspace agents --apply` on the `openclaw`
5
+ * binary being present before it tries to spawn it.
6
+ */
7
+ export declare function hasBinary(name: string): boolean;
8
+ //# sourceMappingURL=which.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"which.d.ts","sourceRoot":"","sources":["../../src/lib/which.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAoB/C"}
@@ -0,0 +1,30 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Check whether a binary exists in PATH without spawning a shell — safer
5
+ * than `execSync('command -v ...')`, which risks shell interpretation of the
6
+ * name. Used to gate `argos workspace agents --apply` on the `openclaw`
7
+ * binary being present before it tries to spawn it.
8
+ */ export function hasBinary(name) {
9
+ const pathEnv = process.env.PATH ?? "";
10
+ const sep = process.platform === "win32" ? ";" : ":";
11
+ const dirs = pathEnv.split(sep).filter(Boolean);
12
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [
13
+ ""
14
+ ];
15
+ for (const dir of dirs){
16
+ for (const ext of exts){
17
+ const candidate = join(dir, name + ext);
18
+ if (existsSync(candidate)) {
19
+ try {
20
+ if (statSync(candidate).isFile()) return true;
21
+ } catch {
22
+ // Race between existsSync and statSync (file removed) — keep looking.
23
+ }
24
+ }
25
+ }
26
+ }
27
+ return false;
28
+ }
29
+
30
+ //# sourceMappingURL=which.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/which.ts"],"sourcesContent":["import { existsSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Check whether a binary exists in PATH without spawning a shell — safer\n * than `execSync('command -v ...')`, which risks shell interpretation of the\n * name. Used to gate `argos workspace agents --apply` on the `openclaw`\n * binary being present before it tries to spawn it.\n */\nexport function hasBinary(name: string): boolean {\n const pathEnv = process.env.PATH ?? \"\";\n const sep = process.platform === \"win32\" ? \";\" : \":\";\n const dirs = pathEnv.split(sep).filter(Boolean);\n const exts =\n process.platform === \"win32\" ? (process.env.PATHEXT ?? \".COM;.EXE;.BAT;.CMD\").split(\";\") : [\"\"];\n\n for (const dir of dirs) {\n for (const ext of exts) {\n const candidate = join(dir, name + ext);\n if (existsSync(candidate)) {\n try {\n if (statSync(candidate).isFile()) return true;\n } catch {\n // Race between existsSync and statSync (file removed) — keep looking.\n }\n }\n }\n }\n return false;\n}\n"],"names":["existsSync","statSync","join","hasBinary","name","pathEnv","process","env","PATH","sep","platform","dirs","split","filter","Boolean","exts","PATHEXT","dir","ext","candidate","isFile"],"mappings":"AAAA,SAASA,UAAU,EAAEC,QAAQ,QAAQ,UAAU;AAC/C,SAASC,IAAI,QAAQ,YAAY;AAEjC;;;;;CAKC,GACD,OAAO,SAASC,UAAUC,IAAY;IACpC,MAAMC,UAAUC,QAAQC,GAAG,CAACC,IAAI,IAAI;IACpC,MAAMC,MAAMH,QAAQI,QAAQ,KAAK,UAAU,MAAM;IACjD,MAAMC,OAAON,QAAQO,KAAK,CAACH,KAAKI,MAAM,CAACC;IACvC,MAAMC,OACJT,QAAQI,QAAQ,KAAK,UAAU,AAACJ,CAAAA,QAAQC,GAAG,CAACS,OAAO,IAAI,qBAAoB,EAAGJ,KAAK,CAAC,OAAO;QAAC;KAAG;IAEjG,KAAK,MAAMK,OAAON,KAAM;QACtB,KAAK,MAAMO,OAAOH,KAAM;YACtB,MAAMI,YAAYjB,KAAKe,KAAKb,OAAOc;YACnC,IAAIlB,WAAWmB,YAAY;gBACzB,IAAI;oBACF,IAAIlB,SAASkB,WAAWC,MAAM,IAAI,OAAO;gBAC3C,EAAE,OAAM;gBACN,sEAAsE;gBACxE;YACF;QACF;IACF;IACA,OAAO;AACT"}
@@ -0,0 +1,133 @@
1
+ export declare const WORKSPACES_FILENAME = "workspaces.json";
2
+ export interface WorkspaceMatch {
3
+ remotes: string[];
4
+ paths: string[];
5
+ }
6
+ export interface WorkspaceRepoEntry {
7
+ name: string;
8
+ path: string;
9
+ }
10
+ export interface WorkspaceEntry {
11
+ match: WorkspaceMatch;
12
+ repos: WorkspaceRepoEntry[];
13
+ }
14
+ /** `name -> workspace` machine-local registry, per spec 0003. */
15
+ export type WorkspaceRegistry = Record<string, WorkspaceEntry>;
16
+ /**
17
+ * Thrown by `loadRegistry` when `workspaces.json` exists but isn't valid
18
+ * JSON. Typed so callers can tell "the registry is corrupt" apart from any
19
+ * other unexpected throw and surface a clean, structured message instead of
20
+ * an unhandled parse-error stack trace.
21
+ */
22
+ export declare class RegistryError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ /**
26
+ * Load `~/.argos/workspaces.json`. A missing file is a fresh install —
27
+ * returns `{}`. Throws `RegistryError` when the file exists but isn't valid
28
+ * JSON — callers must not let that crash bubble up as a raw stack trace
29
+ * (see commands/workspace.ts, commands/doctor.ts, commands/adopt.ts).
30
+ */
31
+ export declare function loadRegistry(): WorkspaceRegistry;
32
+ /** Persist the registry (pretty JSON, trailing newline), creating `~/.argos` if needed. */
33
+ export declare function saveRegistry(registry: WorkspaceRegistry): void;
34
+ export interface ResolveWorkspaceInput {
35
+ /** An explicit name the user passed on the command line, e.g. `workspace link bonum`. */
36
+ explicit?: string;
37
+ /** The `workspace` field from the repo's argos.config.json, if set. */
38
+ configWorkspace?: string;
39
+ /** The repo's `origin` remote URL, if it has one. */
40
+ remoteUrl?: string | null;
41
+ /** Absolute path to the repo, used for path-based match rules. */
42
+ repoPath: string;
43
+ }
44
+ export type ResolveMatchSource = "match-remote" | "match-path";
45
+ export type ResolveSource = "explicit" | "config" | ResolveMatchSource;
46
+ export type ResolveWorkspaceResult = {
47
+ kind: "resolved";
48
+ name: string;
49
+ source: ResolveSource;
50
+ } | {
51
+ kind: "ambiguous";
52
+ candidates: string[];
53
+ source: ResolveMatchSource;
54
+ } | {
55
+ kind: "unresolved";
56
+ };
57
+ /**
58
+ * Resolve which workspace a repo belongs to, per spec 0003's chain:
59
+ * explicit name > `workspace` in argos.config.json > match rules (remote
60
+ * substring match first, path prefix/glob as fallback) > unresolved.
61
+ * 2+ candidates at a match stage is ambiguous — resolution never guesses
62
+ * between identities, it reports the candidates for the caller to surface.
63
+ */
64
+ export declare function resolveWorkspaceForRepo(registry: WorkspaceRegistry, input: ResolveWorkspaceInput): ResolveWorkspaceResult;
65
+ export type LinkAction = "added" | "updated-path" | "unchanged";
66
+ export interface LinkRepoResult {
67
+ workspaceName: string;
68
+ createdWorkspace: boolean;
69
+ action: LinkAction;
70
+ previousPath?: string;
71
+ /** Normalized (absolute, symlink-resolved) repo path that was stored. */
72
+ repoPath: string;
73
+ }
74
+ export interface LinkRepoOptions {
75
+ /**
76
+ * Bypass the name-collision refusal below and overwrite the existing
77
+ * entry's path anyway. Mirrors `argos workspace link --force`.
78
+ */
79
+ force?: boolean;
80
+ }
81
+ /**
82
+ * Thrown by `linkRepo` when `repo.name` is already registered in
83
+ * `workspaceName` pointing at a DIFFERENT path that still exists on disk —
84
+ * i.e. two distinct physical repos colliding on the same `config.name`.
85
+ * Overwriting silently here would evict the old entry with no trace (data
86
+ * loss for whoever relies on it), so `linkRepo` refuses unless `force` is
87
+ * passed. Carries both paths so the caller can print a clear diagnostic.
88
+ */
89
+ export declare class WorkspaceNameCollisionError extends Error {
90
+ readonly workspaceName: string;
91
+ readonly repoName: string;
92
+ readonly oldPath: string;
93
+ readonly newPath: string;
94
+ constructor(workspaceName: string, repoName: string, oldPath: string, newPath: string);
95
+ }
96
+ /**
97
+ * Register `repo` under workspace `workspaceName`, creating the workspace if
98
+ * it doesn't exist yet. Re-linking the same repo name updates its path
99
+ * (idempotent no-op when the path is unchanged). The path is normalized to
100
+ * an absolute, symlink-resolved form before it's stored — `workspace show`
101
+ * and `workspace agents` then compare disk state against exactly what's on
102
+ * record. Throws when `repo.path` doesn't exist on disk.
103
+ *
104
+ * When an entry with the same name already exists at a DIFFERENT path, and
105
+ * that old path still exists on disk with a different realpath, this
106
+ * refuses with `WorkspaceNameCollisionError` instead of silently evicting
107
+ * the old entry — unless `options.force` is set. If the old path is gone
108
+ * from disk, it's treated as a legitimate move (repo relocated) and updated
109
+ * as before, force or not.
110
+ */
111
+ export declare function linkRepo(workspaceName: string, repo: WorkspaceRepoEntry, options?: LinkRepoOptions): LinkRepoResult;
112
+ export interface OfferMatchRuleInput {
113
+ createdWorkspace: boolean;
114
+ /** True when the workspace name came from an explicit `link <name>` argument. */
115
+ viaExplicitName: boolean;
116
+ remoteUrl?: string | null;
117
+ currentMatch: WorkspaceMatch;
118
+ }
119
+ export interface OfferMatchRuleResult {
120
+ shouldPersist: boolean;
121
+ identity?: string;
122
+ }
123
+ /**
124
+ * Decide whether a newly-created workspace should learn the repo's remote as
125
+ * a match rule. Per spec: only offered when the workspace is brand new AND
126
+ * the name was given explicitly (so it isn't a false positive from a match
127
+ * rule that already matched something else) AND the repo actually has a
128
+ * parseable remote that isn't already recorded.
129
+ */
130
+ export declare function offerMatchRule(input: OfferMatchRuleInput): OfferMatchRuleResult;
131
+ /** Append `identity` to a workspace's `match.remotes`, if it isn't already there. */
132
+ export declare function addRemoteMatchRule(workspaceName: string, identity: string): void;
133
+ //# sourceMappingURL=workspaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspaces.d.ts","sourceRoot":"","sources":["../../src/lib/workspaces.ts"],"names":[],"mappings":"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"}