@dalzoubi/dev-agents-sync 3.0.2 → 3.0.4
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/README.md +10 -6
- package/package.json +2 -2
- package/src/commands/status.mjs +14 -5
- package/src/hook-manifest.mjs +24 -0
- package/src/marker.mjs +29 -2
- package/src/settings.mjs +10 -20
- package/tests/e2e/steering-hooks-flow.test.mjs +6 -4
- package/tests/hooks-cli-install.test.mjs +12 -8
- package/tests/hooks-wave2-registration.test.mjs +6 -5
- package/tests/marker.test.mjs +18 -0
- package/tests/status-check-diff.test.mjs +15 -0
package/README.md
CHANGED
|
@@ -49,15 +49,17 @@ To adopt the toolkit with OpenAI Codex, pass the `codex` target explicitly:
|
|
|
49
49
|
npx --yes @dalzoubi/dev-agents-sync@1 init --targets codex
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
Codex is **opt-in only
|
|
52
|
+
Codex is **opt-in only** so an existing `AGENTS.md` or `.codex/` configuration is never adopted accidentally. Pass `--targets codex` explicitly; you can combine it with other targets, e.g. `--targets claude,codex`.
|
|
53
53
|
|
|
54
|
-
**Output shape.** `init --targets codex` writes one directory-per-skill file per agent at `.agents/skills/<agent>/SKILL.md
|
|
54
|
+
**Output shape.** `init --targets codex` writes one directory-per-skill file per agent at `.agents/skills/<agent>/SKILL.md`, a native custom-agent definition at `.codex/agents/<agent>.toml`, the shared lifecycle scripts under `.codex/hooks/`, their adapter and `.codex/hooks.json`, plus a thin root `AGENTS.md` index. Every file carries the managed-by marker, and `update`, `status`, and `check` scope to these paths exactly like the other targets.
|
|
55
55
|
|
|
56
56
|
**Standalone.** Codex is a first-class standalone target. Each emitted `SKILL.md` bundles the workflow support docs it references, so a Codex-only install does **not** need `.claude/skills/*.md`, `.cursor/`, or any other target files to run the agent workflows.
|
|
57
57
|
|
|
58
|
-
**
|
|
58
|
+
**Invocation and delegation.** Codex Agent Skills are model-invoked through `/skills`, a `$<name>` mention, or implicit selection. The matching `.codex/agents/*.toml` definitions also make the topology available to Codex's native subagent delegation.
|
|
59
59
|
|
|
60
|
-
**
|
|
60
|
+
**Hooks.** The Codex target registers the same nine lifecycle policies as the Claude target in project-local `.codex/hooks.json`; Claude wiring remains in `.claude/settings.json`. Codex asks the user to trust project hooks the first time it loads them; review with `/hooks` and choose the appropriate trust option. The adapter translates Codex `apply_patch` events into the shared Edit/Write contract, so the same blocking and warning scripts remain the source of truth.
|
|
61
|
+
|
|
62
|
+
**Manual removal.** Like every target, there is no uninstall or cleanup command. Dropping `codex` from the `targets` in `.dev-agents-sync.json` stops future writes, but the already-written `.agents/skills/<agent>/SKILL.md`, `.codex/`, and root `AGENTS.md` files are removed manually.
|
|
61
63
|
|
|
62
64
|
> **Maintainer note — two file shapes.** Do not confuse the Codex output with this toolkit's own authoring sources. In the `dalzoubi/dev-agents` repo, the canonical skill sources are **flat single files** at `.agents/skills/*.md`. The Codex target **output** is the **directory-per-skill** form `.agents/skills/<agent>/SKILL.md`. Both share the `.agents/skills/` prefix, but the flat `.md` files are for authoring and the `SKILL.md` directories are the generated codex output shape.
|
|
63
65
|
|
|
@@ -101,7 +103,7 @@ npx --yes @dalzoubi/dev-agents-sync@1 status
|
|
|
101
103
|
|
|
102
104
|
## Steering and hooks
|
|
103
105
|
|
|
104
|
-
For the `claude` target, `init` and `update`
|
|
106
|
+
For the `claude` target, `init` and `update` seed consumer-owned steering and auto-wire hooks into `.claude/settings.json`. The `codex` target ships the same hook policies through project-local `.codex/hooks.json`; steering remains consumer-owned and the workflow scratch path is `.agents/state/` for either runtime.
|
|
105
107
|
|
|
106
108
|
### Steering scaffold (consumer-owned)
|
|
107
109
|
|
|
@@ -111,7 +113,7 @@ The CLI seeds a blank steering scaffold under `.claude/steering/` — `decisions
|
|
|
111
113
|
|
|
112
114
|
### Hook scripts (managed)
|
|
113
115
|
|
|
114
|
-
`init` and `update` install the toolkit's hook scripts
|
|
116
|
+
`init` and `update` install the toolkit's hook scripts as marker-managed, lockfile-tracked executable files: `.claude/hooks/` for Claude and `.codex/hooks/` for Codex. Most are **warning** hooks: they print one line to stderr and always exit 0. Four are **blocking** hooks that can deny a tool call outright. Claude registrations are merged into `.claude/settings.json`; Codex registrations ship as managed `.codex/hooks.json` plus `.codex/hook-adapter.mjs`.
|
|
115
117
|
|
|
116
118
|
| Script | Event | Matcher | Mode | What it does |
|
|
117
119
|
|---|---|---|---|---|
|
|
@@ -119,6 +121,7 @@ The CLI seeds a blank steering scaffold under `.claude/steering/` — `decisions
|
|
|
119
121
|
| `managed-file-guard.sh` | `PostToolUse` | `Edit\|Write` | warning | Guards managed files against unexpected edits. |
|
|
120
122
|
| `uncommitted-check.sh` | `Stop` | `.*` | warning | Flags uncommitted work when a session ends. |
|
|
121
123
|
| `force-push-guard.sh` | `PreToolUse` | `Bash` | **blocking** | Denies `git push` carrying `--force` / `-f` / `--force-with-lease` at `main` or `master` (by explicit refspec, or by the checked-out branch when the push names no ref). Force-pushing a feature branch, and a plain push to `main`, both pass. |
|
|
124
|
+
| `direct-push-guard.sh` | `PreToolUse` | `Bash` | **blocking** | Denies ANY `git push` — force or not — at `main`, `master`, or `develop` (by explicit refspec, or by the checked-out branch when the push names no ref). Pushing a feature branch passes; open a pull request instead. |
|
|
122
125
|
| `spec-approval-gate.sh` | `PreToolUse` | `Edit\|Write` | **blocking** | Denies an edit that sets a spec to `Status: Approved` while an unresolved clarification marker — a bare bracketed `NEEDS CLARIFICATION` token — remains in prose. Occurrences inside code spans or fenced blocks are documentation and never match. |
|
|
123
126
|
| `handoff-state-guard.sh` | `PreToolUse` | `Write` | **blocking** | Denies a write to `.claude/state/*.json` whose `handoff.status`, `from_agent`, or `to_agent` is outside the vocabulary `.claude/rules/handoff.md` defines. |
|
|
124
127
|
| `spec-drift-warn.sh` | `PostToolUse` | `Edit\|Write` | warning | After an edit to `spec.md` / `design.md` / `tasks.md` under `docs/specs/`, warns when that folder's `spec.md` has already moved past `Draft`. Draft folders stay silent. |
|
|
@@ -135,6 +138,7 @@ A blocking hook runs on `PreToolUse`, where exiting 2 denies the tool call and t
|
|
|
135
138
|
| Code | Raised by | Example message |
|
|
136
139
|
|---|---|---|
|
|
137
140
|
| `EHOOK_FORCE_PUSH` | `force-push-guard.sh` | `blocked: EHOOK_FORCE_PUSH — force-push to protected branch 'main' — push without a force flag or open a pull request; to override, set DEV_AGENTS_HOOKS_BYPASS=force-push-guard` |
|
|
141
|
+
| `EHOOK_DIRECT_PUSH` | `direct-push-guard.sh` | `blocked: EHOOK_DIRECT_PUSH — direct push to protected branch 'main' — open a pull request instead; to override, set DEV_AGENTS_HOOKS_BYPASS=direct-push-guard` |
|
|
138
142
|
| `EHOOK_SPEC_APPROVAL` | `spec-approval-gate.sh` | `blocked: EHOOK_SPEC_APPROVAL — <path> sets Status: Approved while an unresolved clarification marker (a bare bracketed NEEDS CLARIFICATION token) remains in prose — resolve it via /define-analyze or /define before approving; to override, set DEV_AGENTS_HOOKS_BYPASS=spec-approval-gate` |
|
|
139
143
|
| `EHOOK_HANDOFF_STATE` | `handoff-state-guard.sh` | `blocked: EHOOK_HANDOFF_STATE — handoff.status "done" is outside the status enum — fix the value in the state file before writing, or re-run with DEV_AGENTS_HOOKS_BYPASS=handoff-state-guard` |
|
|
140
144
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dalzoubi/dev-agents-sync",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "CLI that syncs managed dev-agent
|
|
5
|
+
"description": "CLI that syncs managed dev-agent workflows into Claude Code, Cursor, Copilot, and Codex projects.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"bin": {
|
|
8
8
|
"dev-agents-sync": "src/cli.mjs"
|
package/src/commands/status.mjs
CHANGED
|
@@ -13,11 +13,7 @@ const TARGET_DIRS = {
|
|
|
13
13
|
cursor: ['.cursor', 'rules'],
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
const segs = TARGET_DIRS[target];
|
|
18
|
-
if (!segs) return 0;
|
|
19
|
-
// Count files recursively under <target-root>/
|
|
20
|
-
const root = path.join(consumerCwd, segs[0]);
|
|
16
|
+
function countFiles(root) {
|
|
21
17
|
if (!existsSync(root)) return 0;
|
|
22
18
|
let n = 0;
|
|
23
19
|
const stack = [root];
|
|
@@ -38,6 +34,19 @@ function countTargetFiles(consumerCwd, target) {
|
|
|
38
34
|
return n;
|
|
39
35
|
}
|
|
40
36
|
|
|
37
|
+
function countTargetFiles(consumerCwd, target) {
|
|
38
|
+
if (target === 'codex') {
|
|
39
|
+
return (existsSync(path.join(consumerCwd, 'AGENTS.md')) ? 1 : 0) +
|
|
40
|
+
countFiles(path.join(consumerCwd, '.agents', 'skills')) +
|
|
41
|
+
countFiles(path.join(consumerCwd, '.codex'));
|
|
42
|
+
}
|
|
43
|
+
const segs = TARGET_DIRS[target];
|
|
44
|
+
if (!segs) return 0;
|
|
45
|
+
// Count files recursively under <target-root>/
|
|
46
|
+
const root = path.join(consumerCwd, segs[0]);
|
|
47
|
+
return countFiles(root);
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
export async function runStatus(consumerCwd, opts = {}) {
|
|
42
51
|
const { availableTags } = opts;
|
|
43
52
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target-neutral lifecycle-hook manifest.
|
|
3
|
+
*
|
|
4
|
+
* The content build and the sync CLI both import this module so Claude settings
|
|
5
|
+
* wiring and generated Codex hooks.json cannot silently drift apart.
|
|
6
|
+
*/
|
|
7
|
+
export const HOOK_MANIFEST = Object.freeze([
|
|
8
|
+
{ event: 'SessionStart', matcher: '.*', file: 'steering-injector.sh' },
|
|
9
|
+
{ event: 'PostToolUse', matcher: 'Edit|Write', file: 'managed-file-guard.sh' },
|
|
10
|
+
{ event: 'Stop', matcher: '.*', file: 'uncommitted-check.sh' },
|
|
11
|
+
{ event: 'PreToolUse', matcher: 'Bash', file: 'force-push-guard.sh' },
|
|
12
|
+
{ event: 'PreToolUse', matcher: 'Bash', file: 'direct-push-guard.sh' },
|
|
13
|
+
{ event: 'PreToolUse', matcher: 'Edit|Write', file: 'spec-approval-gate.sh' },
|
|
14
|
+
{ event: 'PreToolUse', matcher: 'Write', file: 'handoff-state-guard.sh' },
|
|
15
|
+
{ event: 'PostToolUse', matcher: 'Edit|Write', file: 'spec-drift-warn.sh' },
|
|
16
|
+
{ event: 'PreToolUse', matcher: 'Bash', file: 'pre-pr-preflight.sh' },
|
|
17
|
+
{ event: 'Stop', matcher: '.*', file: 'ui-verification-reminder.sh' },
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function hookPathFor(target, file) {
|
|
21
|
+
if (target === 'claude') return `.claude/hooks/${file}`;
|
|
22
|
+
if (target === 'codex') return `.codex/hooks/${file}`;
|
|
23
|
+
throw new Error(`unsupported hook target: ${target}`);
|
|
24
|
+
}
|
package/src/marker.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const MARKER_RE = /^<!--\s*managed-by:\s*dev-agents-sync\s+v([0-9]+\.[0-9]+\.[0-9]+(?:[-+][\w.-]+)?)\s*-->\s*$/;
|
|
12
|
+
const COMMENT_MARKER_RE = /^(?:#|\/\/)\s*managed-by:\s*dev-agents-sync\s+v([0-9]+\.[0-9]+\.[0-9]+(?:[-+][\w.-]+)?)\s*$/;
|
|
12
13
|
|
|
13
14
|
export function buildMarker(version) {
|
|
14
15
|
return `<!-- managed-by: dev-agents-sync v${version} -->`;
|
|
@@ -48,7 +49,22 @@ export function hasMarker(content) {
|
|
|
48
49
|
const body = stripFrontmatter(content);
|
|
49
50
|
const first = firstNonEmptyLine(body);
|
|
50
51
|
if (first === null) return false;
|
|
51
|
-
|
|
52
|
+
if (MARKER_RE.test(first) || COMMENT_MARKER_RE.test(first)) return true;
|
|
53
|
+
|
|
54
|
+
// Executable scripts preserve their shebang on line 1 and carry the marker
|
|
55
|
+
// immediately after it. JSON cannot contain comments, so generated Codex
|
|
56
|
+
// hooks.json stores the unchanged HTML marker string in `_managedBy`.
|
|
57
|
+
const lines = body.split(/\r?\n/);
|
|
58
|
+
if (lines[0]?.startsWith('#!')) {
|
|
59
|
+
const second = firstNonEmptyLine(lines.slice(1).join('\n'));
|
|
60
|
+
return second !== null && COMMENT_MARKER_RE.test(second);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(body);
|
|
64
|
+
return typeof parsed?._managedBy === 'string' && MARKER_RE.test(parsed._managedBy);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
/**
|
|
@@ -66,6 +82,17 @@ export function extractMarkerVersion(content) {
|
|
|
66
82
|
const body = stripFrontmatter(content);
|
|
67
83
|
const first = firstNonEmptyLine(body);
|
|
68
84
|
if (first === null) return null;
|
|
69
|
-
|
|
85
|
+
let m = first.match(MARKER_RE) ?? first.match(COMMENT_MARKER_RE);
|
|
86
|
+
if (!m && first.startsWith('#!')) {
|
|
87
|
+
const second = firstNonEmptyLine(body.split(/\r?\n/).slice(1).join('\n'));
|
|
88
|
+
m = second?.match(COMMENT_MARKER_RE) ?? null;
|
|
89
|
+
}
|
|
90
|
+
if (!m) {
|
|
91
|
+
try {
|
|
92
|
+
m = JSON.parse(body)?._managedBy?.match(MARKER_RE) ?? null;
|
|
93
|
+
} catch {
|
|
94
|
+
m = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
70
97
|
return m ? m[1] : null;
|
|
71
98
|
}
|
package/src/settings.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
27
|
import path from 'node:path';
|
|
28
|
+
import { HOOK_MANIFEST, hookPathFor } from './hook-manifest.mjs';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Property injected onto every CLI-managed hook entry so it can be identified
|
|
@@ -436,26 +437,10 @@ export function hookCommandFor(script) {
|
|
|
436
437
|
* `Bash` matcher — one blocking, one warning — which the merge handles as two
|
|
437
438
|
* independent registrations keyed by command.
|
|
438
439
|
*/
|
|
439
|
-
export const DEFAULT_HOOK_SCRIPTS =
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// by every tool call — every Read, every Bash — to do nothing.
|
|
444
|
-
{ event: 'PostToolUse', matcher: 'Edit|Write', script: '.claude/hooks/managed-file-guard.sh' },
|
|
445
|
-
{ event: 'Stop', matcher: '.*', script: '.claude/hooks/uncommitted-check.sh' },
|
|
446
|
-
// Added by docs/specs/260805-hooks-wave-2/ — the toolkit's first blocking
|
|
447
|
-
// hooks plus two warning hooks. Blockers deny only on a positive violation
|
|
448
|
-
// match, fail open otherwise, and honor DEV_AGENTS_HOOKS_BYPASS.
|
|
449
|
-
{ event: 'PreToolUse', matcher: 'Bash', script: '.claude/hooks/force-push-guard.sh' },
|
|
450
|
-
{ event: 'PreToolUse', matcher: 'Edit|Write', script: '.claude/hooks/spec-approval-gate.sh' },
|
|
451
|
-
{ event: 'PreToolUse', matcher: 'Write', script: '.claude/hooks/handoff-state-guard.sh' },
|
|
452
|
-
{ event: 'PostToolUse', matcher: 'Edit|Write', script: '.claude/hooks/spec-drift-warn.sh' },
|
|
453
|
-
{ event: 'PreToolUse', matcher: 'Bash', script: '.claude/hooks/pre-pr-preflight.sh' },
|
|
454
|
-
// UI/UX framework: a Stop-time reminder that frontend work is governed by the
|
|
455
|
-
// UI Definition of Done, not by a green build. Non-blocking by construction —
|
|
456
|
-
// a Stop hook cannot deny, and this one exits 0 on every path.
|
|
457
|
-
{ event: 'Stop', matcher: '.*', script: '.claude/hooks/ui-verification-reminder.sh' },
|
|
458
|
-
].map((h) => ({ ...h, command: hookCommandFor(h.script) }));
|
|
440
|
+
export const DEFAULT_HOOK_SCRIPTS = HOOK_MANIFEST.map(({ event, matcher, file }) => {
|
|
441
|
+
const script = hookPathFor('claude', file);
|
|
442
|
+
return { event, matcher, script, command: hookCommandFor(script) };
|
|
443
|
+
});
|
|
459
444
|
|
|
460
445
|
/**
|
|
461
446
|
* Hook registrations the toolkit once shipped and has since retired.
|
|
@@ -511,4 +496,9 @@ export const RETIRED_HOOK_SCRIPTS = [
|
|
|
511
496
|
// or by a future regression, and pruning it is always correct since that form
|
|
512
497
|
// is never in DEFAULT_HOOK_SCRIPTS.
|
|
513
498
|
{ event: 'Stop', matcher: '.*', command: '.claude/hooks/ui-verification-reminder.sh' },
|
|
499
|
+
// direct-push-guard.sh ships only in the `bash …` form — same reasoning as
|
|
500
|
+
// ui-verification-reminder.sh above: it never shipped bare, but the
|
|
501
|
+
// bare-path form is pruned defensively so a hand-edit or regression can't
|
|
502
|
+
// register it live alongside the real one.
|
|
503
|
+
{ event: 'PreToolUse', matcher: 'Bash', command: '.claude/hooks/direct-push-guard.sh' },
|
|
514
504
|
];
|
|
@@ -324,10 +324,12 @@ describe('S5 E1 — integrated init → update → check flow', () => {
|
|
|
324
324
|
assert.ok(existsSync(absPath), `update must install .claude/hooks/${file}`);
|
|
325
325
|
|
|
326
326
|
const st = statSync(absPath);
|
|
327
|
-
|
|
328
|
-
(
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
if (process.platform !== 'win32') {
|
|
328
|
+
assert.ok(
|
|
329
|
+
(st.mode & 0o111) !== 0,
|
|
330
|
+
`${file} must be executable after install (mode: 0o${st.mode.toString(8)})`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
331
333
|
}
|
|
332
334
|
|
|
333
335
|
// Lockfile must reference each installed hook script.
|
|
@@ -323,10 +323,12 @@ describe('S4 H1 — init installs hook scripts', () => {
|
|
|
323
323
|
const absPath = path.join(hooksDir, file);
|
|
324
324
|
const st = statSync(absPath);
|
|
325
325
|
// On POSIX: mode & 0o111 checks owner+group+other execute bits.
|
|
326
|
-
|
|
327
|
-
(
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
if (process.platform !== 'win32') {
|
|
327
|
+
assert.ok(
|
|
328
|
+
(st.mode & 0o111) !== 0,
|
|
329
|
+
`${file} must be executable after install (mode: 0o${st.mode.toString(8)})`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
330
332
|
}
|
|
331
333
|
});
|
|
332
334
|
|
|
@@ -412,10 +414,12 @@ describe('S4 H1 — update installs hook scripts', () => {
|
|
|
412
414
|
const hooksDir = path.join(consumerDir, '.claude', 'hooks');
|
|
413
415
|
for (const file of HOOK_FILES) {
|
|
414
416
|
const st = statSync(path.join(hooksDir, file));
|
|
415
|
-
|
|
416
|
-
(
|
|
417
|
-
|
|
418
|
-
|
|
417
|
+
if (process.platform !== 'win32') {
|
|
418
|
+
assert.ok(
|
|
419
|
+
(st.mode & 0o111) !== 0,
|
|
420
|
+
`update-installed ${file} must be executable`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
419
423
|
}
|
|
420
424
|
});
|
|
421
425
|
});
|
|
@@ -295,10 +295,10 @@ describe('W2 — init and update install and wire every registered hook', () =>
|
|
|
295
295
|
}
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
-
it('W2d:
|
|
299
|
-
// force-push-guard and pre-pr-preflight share
|
|
300
|
-
// identity is the COMMAND, so
|
|
301
|
-
// event+matcher would silently drop one.
|
|
298
|
+
it('W2d: all three Bash PreToolUse hooks are wired (a shared event+matcher is not deduped)', async () => {
|
|
299
|
+
// force-push-guard, direct-push-guard, and pre-pr-preflight all share
|
|
300
|
+
// PreToolUse/Bash. Merge identity is the COMMAND, so all three must
|
|
301
|
+
// survive — collapsing on event+matcher would silently drop one.
|
|
302
302
|
await runInit(consumerDir, {
|
|
303
303
|
targets: ['claude'],
|
|
304
304
|
fetcher: makeFixtureFetcher(),
|
|
@@ -312,10 +312,11 @@ describe('W2 — init and update install and wire every registered hook', () =>
|
|
|
312
312
|
assert.deepEqual(
|
|
313
313
|
commands,
|
|
314
314
|
[
|
|
315
|
+
expectedCommand('.claude/hooks/direct-push-guard.sh'),
|
|
315
316
|
expectedCommand('.claude/hooks/force-push-guard.sh'),
|
|
316
317
|
expectedCommand('.claude/hooks/pre-pr-preflight.sh'),
|
|
317
318
|
],
|
|
318
|
-
'
|
|
319
|
+
'all three PreToolUse/Bash hooks must be wired independently',
|
|
319
320
|
);
|
|
320
321
|
});
|
|
321
322
|
|
package/tests/marker.test.mjs
CHANGED
|
@@ -187,4 +187,22 @@ describe('extractMarkerVersion', () => {
|
|
|
187
187
|
const content = '# No marker here\nBody.';
|
|
188
188
|
assert.equal(extractMarkerVersion(content), null);
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
it('extracts versions from TOML, shebang scripts, and JSON manifests', () => {
|
|
192
|
+
assert.equal(extractMarkerVersion('# managed-by: dev-agents-sync v1.2.3\nname = "x"'), '1.2.3');
|
|
193
|
+
assert.equal(extractMarkerVersion('#!/usr/bin/env node\n// managed-by: dev-agents-sync v2.3.4\n'), '2.3.4');
|
|
194
|
+
assert.equal(extractMarkerVersion(JSON.stringify({ _managedBy: '<!-- managed-by: dev-agents-sync v3.4.5 -->' })), '3.4.5');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('hasMarker — Codex native file formats', () => {
|
|
199
|
+
it('accepts TOML comments, shebang-plus-comment scripts, and JSON manifests', () => {
|
|
200
|
+
assert.ok(hasMarker('# managed-by: dev-agents-sync v1.2.3\nname = "x"'));
|
|
201
|
+
assert.ok(hasMarker('#!/usr/bin/env node\n// managed-by: dev-agents-sync v1.2.3\n'));
|
|
202
|
+
assert.ok(hasMarker(JSON.stringify({ _managedBy: '<!-- managed-by: dev-agents-sync v1.2.3 -->' })));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('rejects a comment marker buried after ordinary content', () => {
|
|
206
|
+
assert.equal(hasMarker('name = "x"\n# managed-by: dev-agents-sync v1.2.3'), false);
|
|
207
|
+
});
|
|
190
208
|
});
|
|
@@ -205,6 +205,21 @@ describe('status', () => {
|
|
|
205
205
|
'result must include file counts',
|
|
206
206
|
);
|
|
207
207
|
});
|
|
208
|
+
|
|
209
|
+
it('counts Codex root, skill, hook, and custom-agent files', async () => {
|
|
210
|
+
setupConsumerRepo(consumerDir, { targets: ['codex'], writeFiles: false });
|
|
211
|
+
mkdirSync(path.join(consumerDir, '.agents', 'skills', 'define'), { recursive: true });
|
|
212
|
+
mkdirSync(path.join(consumerDir, '.codex', 'agents'), { recursive: true });
|
|
213
|
+
mkdirSync(path.join(consumerDir, '.codex', 'hooks'), { recursive: true });
|
|
214
|
+
writeFileSync(path.join(consumerDir, 'AGENTS.md'), 'index');
|
|
215
|
+
writeFileSync(path.join(consumerDir, '.agents', 'skills', 'define', 'SKILL.md'), 'skill');
|
|
216
|
+
writeFileSync(path.join(consumerDir, '.codex', 'agents', 'define.toml'), 'agent');
|
|
217
|
+
writeFileSync(path.join(consumerDir, '.codex', 'hooks.json'), '{}');
|
|
218
|
+
writeFileSync(path.join(consumerDir, '.codex', 'hooks', 'guard.sh'), 'exit 0');
|
|
219
|
+
|
|
220
|
+
const result = await runStatus(consumerDir, { availableTags: ['v1.2.0'] });
|
|
221
|
+
assert.equal(result.fileCounts.codex, 5);
|
|
222
|
+
});
|
|
208
223
|
});
|
|
209
224
|
|
|
210
225
|
// ---------------------------------------------------------------------------
|