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.
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/assets/agents/auditor.md +140 -0
- package/assets/agents/commit-pr-pilot.md +154 -0
- package/assets/agents/explorer.md +93 -0
- package/assets/agents/implementer.md +117 -0
- package/assets/agents/leader.md +149 -0
- package/assets/agents/researcher.md +89 -0
- package/assets/agents/review-readability.md +87 -0
- package/assets/agents/review-reliability.md +100 -0
- package/assets/agents/review-resilience.md +87 -0
- package/assets/agents/review-risk.md +87 -0
- package/assets/agents/reviewer.md +167 -0
- package/assets/agents/ticket-audit.md +129 -0
- package/assets/hooks/argos-guard-destructive.sh +127 -0
- package/assets/hooks/argos-quality-gate.sh +129 -0
- package/assets/managed/aterrizaje.md +24 -0
- package/assets/managed/formato-respuesta.md +21 -0
- package/assets/managed/identidad.md +48 -0
- package/assets/managed/operaciones-seguras.md +14 -0
- package/assets/managed/orquestacion.md +169 -0
- package/assets/output-styles/argos.md +71 -0
- package/assets/skills/ai-sdk-5/SKILL.md +230 -0
- package/assets/skills/angular/SKILL.md +19 -0
- package/assets/skills/angular/references/architecture.md +137 -0
- package/assets/skills/angular/references/core.md +197 -0
- package/assets/skills/angular/references/forms.md +115 -0
- package/assets/skills/angular/references/performance.md +124 -0
- package/assets/skills/apollo-client/SKILL.md +61 -0
- package/assets/skills/app-blueprint/SKILL.md +45 -0
- package/assets/skills/app-blueprint/assets/module-template.md +48 -0
- package/assets/skills/app-blueprint/assets/system-template.md +38 -0
- package/assets/skills/app-blueprint/references/workflow.md +101 -0
- package/assets/skills/app-builder/SKILL.md +43 -0
- package/assets/skills/app-builder/phases/0-product.md +58 -0
- package/assets/skills/app-builder/phases/1-scaffold.md +33 -0
- package/assets/skills/app-builder/phases/10-store.md +40 -0
- package/assets/skills/app-builder/phases/2-data.md +34 -0
- package/assets/skills/app-builder/phases/3-domain.md +33 -0
- package/assets/skills/app-builder/phases/4-ui-nav.md +32 -0
- package/assets/skills/app-builder/phases/5-identity.md +30 -0
- package/assets/skills/app-builder/phases/6-polish.md +34 -0
- package/assets/skills/app-builder/phases/7-brand.md +31 -0
- package/assets/skills/app-builder/phases/8-web.md +30 -0
- package/assets/skills/app-builder/phases/9-docs.md +31 -0
- package/assets/skills/app-ia/SKILL.md +54 -0
- package/assets/skills/astro/SKILL.md +39 -0
- package/assets/skills/axios/SKILL.md +61 -0
- package/assets/skills/branch-pr/SKILL.md +200 -0
- package/assets/skills/bullmq/SKILL.md +55 -0
- package/assets/skills/chained-pr/SKILL.md +48 -0
- package/assets/skills/chained-pr/references/chaining-details.md +99 -0
- package/assets/skills/cognitive-doc-design/SKILL.md +81 -0
- package/assets/skills/comment-writer/SKILL.md +74 -0
- package/assets/skills/dashboard-ia/SKILL.md +54 -0
- package/assets/skills/django-drf/SKILL.md +180 -0
- package/assets/skills/go-testing/SKILL.md +47 -0
- package/assets/skills/go-testing/references/examples.md +89 -0
- package/assets/skills/issue-creation/SKILL.md +223 -0
- package/assets/skills/jira-epic/SKILL.md +306 -0
- package/assets/skills/jira-task/SKILL.md +382 -0
- package/assets/skills/judgment-day/SKILL.md +52 -0
- package/assets/skills/judgment-day/references/prompts-and-formats.md +98 -0
- package/assets/skills/lightsail-deploy/SKILL.md +44 -0
- package/assets/skills/lightsail-deploy/references/runbook.md +102 -0
- package/assets/skills/loop-back-debug/SKILL.md +102 -0
- package/assets/skills/mantine-form/SKILL.md +57 -0
- package/assets/skills/mongoose/SKILL.md +66 -0
- package/assets/skills/nextjs-15/SKILL.md +144 -0
- package/assets/skills/not-boring-mobile/SKILL.md +43 -0
- package/assets/skills/not-boring-mobile/references/not-boring-playbook.md +65 -0
- package/assets/skills/playwright/SKILL.md +315 -0
- package/assets/skills/pr-comments/SKILL.md +93 -0
- package/assets/skills/pr-create/SKILL.md +64 -0
- package/assets/skills/promo-video/SKILL.md +52 -0
- package/assets/skills/promo-video/assets/package.template.json +22 -0
- package/assets/skills/promo-video/assets/promo.template.tsx +469 -0
- package/assets/skills/promo-video/assets/theme.template.ts +27 -0
- package/assets/skills/promo-video/references/pipeline.md +119 -0
- package/assets/skills/promo-video-web/SKILL.md +51 -0
- package/assets/skills/promo-video-web/assets/browser-promo.template.tsx +385 -0
- package/assets/skills/promo-video-web/assets/capture.template.ts +70 -0
- package/assets/skills/promo-video-web/assets/package.template.json +26 -0
- package/assets/skills/promo-video-web/references/pipeline.md +84 -0
- package/assets/skills/pytest/SKILL.md +180 -0
- package/assets/skills/react-19/SKILL.md +118 -0
- package/assets/skills/react-hook-form/SKILL.md +59 -0
- package/assets/skills/react-router/SKILL.md +60 -0
- package/assets/skills/redux-toolkit/SKILL.md +60 -0
- package/assets/skills/review-diff/SKILL.md +101 -0
- package/assets/skills/ship-docs/SKILL.md +45 -0
- package/assets/skills/ship-docs/references/ship-docs-playbook.md +31 -0
- package/assets/skills/skill-creator/SKILL.md +97 -0
- package/assets/skills/skill-creator/assets/SKILL-TEMPLATE.md +68 -0
- package/assets/skills/skill-creator/references/skill-style-guide.md +79 -0
- package/assets/skills/skill-improver/SKILL.md +50 -0
- package/assets/skills/skill-improver/references/skill-style-guide.md +79 -0
- package/assets/skills/socketio/SKILL.md +58 -0
- package/assets/skills/spec-bootstrap/SKILL.md +62 -0
- package/assets/skills/store-ship/SKILL.md +52 -0
- package/assets/skills/store-ship/assets/android-supply.template.md +22 -0
- package/assets/skills/store-ship/assets/eas.template.json +32 -0
- package/assets/skills/store-ship/assets/maestro-flow.template.yaml +27 -0
- package/assets/skills/store-ship/assets/store.config.template.json +28 -0
- package/assets/skills/store-ship/references/pipeline.md +198 -0
- package/assets/skills/stripe/SKILL.md +83 -0
- package/assets/skills/tailwind-4/SKILL.md +193 -0
- package/assets/skills/tamagui/SKILL.md +60 -0
- package/assets/skills/tanstack-query/SKILL.md +58 -0
- package/assets/skills/ticket-intake/SKILL.md +55 -0
- package/assets/skills/typescript/SKILL.md +134 -0
- package/assets/skills/verify-before-done/SKILL.md +111 -0
- package/assets/skills/webapp-rebuilder/SKILL.md +48 -0
- package/assets/skills/webapp-rebuilder/assets/charter-template.md +46 -0
- package/assets/skills/webapp-rebuilder/references/workflow.md +47 -0
- package/assets/skills/winston-logging/SKILL.md +61 -0
- package/assets/skills/work-unit-commits/SKILL.md +84 -0
- package/assets/skills/zod-4/SKILL.md +210 -0
- package/assets/skills/zustand-5/SKILL.md +216 -0
- package/bin/argos.js +6 -0
- package/dist/commands/adopt.d.ts +35 -0
- package/dist/commands/adopt.d.ts.map +1 -0
- package/dist/commands/adopt.js +347 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/doctor.d.ts +20 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +478 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +32 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +358 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/remove.d.ts +62 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +487 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/workspace.d.ts +73 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +354 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/assets.d.ts +29 -0
- package/dist/lib/assets.d.ts.map +1 -0
- package/dist/lib/assets.js +57 -0
- package/dist/lib/assets.js.map +1 -0
- package/dist/lib/atomic-write.d.ts +17 -0
- package/dist/lib/atomic-write.d.ts.map +1 -0
- package/dist/lib/atomic-write.js +41 -0
- package/dist/lib/atomic-write.js.map +1 -0
- package/dist/lib/backup.d.ts +11 -0
- package/dist/lib/backup.d.ts.map +1 -0
- package/dist/lib/backup.js +42 -0
- package/dist/lib/backup.js.map +1 -0
- package/dist/lib/config.d.ts +36 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +52 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/detect.d.ts +58 -0
- package/dist/lib/detect.d.ts.map +1 -0
- package/dist/lib/detect.js +330 -0
- package/dist/lib/detect.js.map +1 -0
- package/dist/lib/ficha.d.ts +10 -0
- package/dist/lib/ficha.d.ts.map +1 -0
- package/dist/lib/ficha.js +38 -0
- package/dist/lib/ficha.js.map +1 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +79 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/managed-files.d.ts +35 -0
- package/dist/lib/managed-files.d.ts.map +1 -0
- package/dist/lib/managed-files.js +97 -0
- package/dist/lib/managed-files.js.map +1 -0
- package/dist/lib/markers.d.ts +64 -0
- package/dist/lib/markers.d.ts.map +1 -0
- package/dist/lib/markers.js +157 -0
- package/dist/lib/markers.js.map +1 -0
- package/dist/lib/navori-import.d.ts +42 -0
- package/dist/lib/navori-import.d.ts.map +1 -0
- package/dist/lib/navori-import.js +65 -0
- package/dist/lib/navori-import.js.map +1 -0
- package/dist/lib/openclaw-agents.d.ts +53 -0
- package/dist/lib/openclaw-agents.d.ts.map +1 -0
- package/dist/lib/openclaw-agents.js +118 -0
- package/dist/lib/openclaw-agents.js.map +1 -0
- package/dist/lib/package-root.d.ts +11 -0
- package/dist/lib/package-root.d.ts.map +1 -0
- package/dist/lib/package-root.js +23 -0
- package/dist/lib/package-root.js.map +1 -0
- package/dist/lib/paths.d.ts +11 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +16 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/settings-merge.d.ts +125 -0
- package/dist/lib/settings-merge.d.ts.map +1 -0
- package/dist/lib/settings-merge.js +373 -0
- package/dist/lib/settings-merge.js.map +1 -0
- package/dist/lib/version.d.ts +3 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +13 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/lib/which.d.ts +8 -0
- package/dist/lib/which.d.ts.map +1 -0
- package/dist/lib/which.js +30 -0
- package/dist/lib/which.js.map +1 -0
- package/dist/lib/workspaces.d.ts +133 -0
- package/dist/lib/workspaces.d.ts.map +1 -0
- package/dist/lib/workspaces.js +241 -0
- package/dist/lib/workspaces.js.map +1 -0
- package/dist/lib/zod-messages.d.ts +4 -0
- package/dist/lib/zod-messages.d.ts.map +1 -0
- package/dist/lib/zod-messages.js +28 -0
- package/dist/lib/zod-messages.js.map +1 -0
- 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 @@
|
|
|
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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"}
|