@dalzoubi/dev-agents-sync 1.0.8 → 1.0.10
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 +1 -0
- package/package.json +1 -1
- package/src/lockfile.mjs +3 -1
- package/src/writer.mjs +2 -1
- package/tests/fixtures/release-v1.0.0/copilot/prompts/define.prompt.md +17 -0
- package/tests/fixtures/release-v1.0.0/copilot/prompts/preflight.prompt.md +7 -0
- package/tests/lockfile.test.mjs +10 -0
- package/tests/paths.test.mjs +44 -2
- package/tests/update.test.mjs +113 -2
- package/tests/writer-normalize.test.mjs +4 -0
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ npx --yes @dalzoubi/dev-agents-sync@1 init --targets claude
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
This writes managed files into `.claude/` and creates `.dev-agents-sync.json` with the resolved content version.
|
|
42
|
+
Use `--targets cursor` or `--targets claude,cursor` to install Cursor project agents, rules, and slash commands into `.cursor/agents/`, `.cursor/rules/`, and `.cursor/commands/`.
|
|
42
43
|
|
|
43
44
|
Update to the latest matching content version:
|
|
44
45
|
|
package/package.json
CHANGED
package/src/lockfile.mjs
CHANGED
|
@@ -14,7 +14,9 @@ export const DEFAULT_SOURCE = 'github:dalzoubi/dev-agents';
|
|
|
14
14
|
export const SCHEMA_URL =
|
|
15
15
|
'https://raw.githubusercontent.com/dalzoubi/dev-agents/v1/schema/lockfile.schema.json';
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// TODO(ask-first follow-up): simplify targets — once all consumers adopt Copilot,
|
|
18
|
+
// adding 'cursor' could auto-include 'copilot' so callers don't need to list both.
|
|
19
|
+
const VALID_TARGETS = new Set(['claude', 'cursor', 'copilot']);
|
|
18
20
|
|
|
19
21
|
// Field order is load-bearing for determinism.
|
|
20
22
|
const FIELD_ORDER = [
|
package/src/writer.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { hasMarker } from './marker.mjs';
|
|
|
15
15
|
const TARGET_PREFIX = {
|
|
16
16
|
claude: '.claude',
|
|
17
17
|
cursor: '.cursor',
|
|
18
|
+
copilot: '.github',
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
// Subdirectories that uniquely identify a target. Used to normalize
|
|
@@ -39,7 +40,7 @@ export function normalizeFileMap(fileMap) {
|
|
|
39
40
|
for (const [key, content] of Object.entries(fileMap)) {
|
|
40
41
|
const norm = key.replace(/\\/g, '/');
|
|
41
42
|
const first = norm.split('/')[0];
|
|
42
|
-
if (first === 'claude' || first === 'cursor') {
|
|
43
|
+
if (first === 'claude' || first === 'cursor' || first === 'copilot') {
|
|
43
44
|
out[norm] = content;
|
|
44
45
|
continue;
|
|
45
46
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
mode: "agent"
|
|
3
|
+
description: "Planning and analysis agent for spec-driven development."
|
|
4
|
+
tools: ["Read", "Grep", "Glob", "WebFetch", "WebSearch", "Agent", "TodoWrite", "Write"]
|
|
5
|
+
---
|
|
6
|
+
<!-- managed-by: dev-agents-sync v1.0.0 -->
|
|
7
|
+
You are the **Define** agent: planning and analysis for spec-driven development.
|
|
8
|
+
|
|
9
|
+
## Your posture
|
|
10
|
+
|
|
11
|
+
Senior product-minded engineer. You do not write code. You produce a spec that is concrete enough for the Implement agent to execute.
|
|
12
|
+
|
|
13
|
+
## Hard rules
|
|
14
|
+
|
|
15
|
+
- **Read-only on source code.** You may only write the final spec markdown.
|
|
16
|
+
- **One question at a time.** Never ask multi-part questions.
|
|
17
|
+
- Honor the project's `CLAUDE.md` at the repo root.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
mode: "agent"
|
|
3
|
+
description: "Pre-task orientation command. Run at the start of any session."
|
|
4
|
+
tools: ["Read", "Grep", "Glob"]
|
|
5
|
+
---
|
|
6
|
+
<!-- managed-by: dev-agents-sync v1.0.0 -->
|
|
7
|
+
Read CLAUDE.md and any AGENTS.md files. Orient yourself to the codebase before taking any action.
|
package/tests/lockfile.test.mjs
CHANGED
|
@@ -86,6 +86,16 @@ describe('validateLockfile', () => {
|
|
|
86
86
|
assert.doesNotThrow(() => validateLockfile({ ...VALID_LOCKFILE, targets: ['cursor'] }));
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
it('accepts targets: ["copilot"]', () => {
|
|
90
|
+
assert.doesNotThrow(() => validateLockfile({ ...VALID_LOCKFILE, targets: ['copilot'] }));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('accepts targets: ["claude", "cursor", "copilot"]', () => {
|
|
94
|
+
assert.doesNotThrow(() =>
|
|
95
|
+
validateLockfile({ ...VALID_LOCKFILE, targets: ['claude', 'cursor', 'copilot'] }),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
89
99
|
it('rejects when source is missing', () => {
|
|
90
100
|
const bad = { ...VALID_LOCKFILE };
|
|
91
101
|
delete bad.source;
|
package/tests/paths.test.mjs
CHANGED
|
@@ -37,9 +37,9 @@ import { resolveConsumerPath } from '../src/writer.mjs';
|
|
|
37
37
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
38
|
const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'release-v1.0.0');
|
|
39
39
|
|
|
40
|
-
function buildFixtureFileMap() {
|
|
40
|
+
function buildFixtureFileMap(targets = ['claude', 'cursor', 'copilot']) {
|
|
41
41
|
const map = {};
|
|
42
|
-
for (const target of
|
|
42
|
+
for (const target of targets) {
|
|
43
43
|
const targetDir = path.join(FIXTURE_DIR, target);
|
|
44
44
|
if (!existsSync(targetDir)) continue;
|
|
45
45
|
collectFiles(targetDir, targetDir, map, target);
|
|
@@ -99,6 +99,34 @@ describe('resolveConsumerPath', () => {
|
|
|
99
99
|
assert.equal(result, expected);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
it('translates "cursor/agents/define.md" to .cursor/agents/define.md', () => {
|
|
103
|
+
const consumerRoot = '/some/repo';
|
|
104
|
+
const result = resolveConsumerPath(consumerRoot, 'cursor/agents/define.md');
|
|
105
|
+
const expected = path.join(consumerRoot, '.cursor', 'agents', 'define.md');
|
|
106
|
+
assert.equal(result, expected);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('translates "cursor/commands/define.md" to .cursor/commands/define.md', () => {
|
|
110
|
+
const consumerRoot = '/some/repo';
|
|
111
|
+
const result = resolveConsumerPath(consumerRoot, 'cursor/commands/define.md');
|
|
112
|
+
const expected = path.join(consumerRoot, '.cursor', 'commands', 'define.md');
|
|
113
|
+
assert.equal(result, expected);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('translates "copilot/prompts/define.prompt.md" to .github/prompts/define.prompt.md', () => {
|
|
117
|
+
const consumerRoot = '/some/repo';
|
|
118
|
+
const result = resolveConsumerPath(consumerRoot, 'copilot/prompts/define.prompt.md');
|
|
119
|
+
const expected = path.join(consumerRoot, '.github', 'prompts', 'define.prompt.md');
|
|
120
|
+
assert.equal(result, expected);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('translates "copilot/instructions/foo.instructions.md" to .github/instructions/foo.instructions.md', () => {
|
|
124
|
+
const consumerRoot = '/some/repo';
|
|
125
|
+
const result = resolveConsumerPath(consumerRoot, 'copilot/instructions/foo.instructions.md');
|
|
126
|
+
const expected = path.join(consumerRoot, '.github', 'instructions', 'foo.instructions.md');
|
|
127
|
+
assert.equal(result, expected);
|
|
128
|
+
});
|
|
129
|
+
|
|
102
130
|
it('works with a Windows-style absolute consumer root path', () => {
|
|
103
131
|
// Simulate a Windows-style path
|
|
104
132
|
const consumerRoot = 'C:\\Users\\developer\\projects\\my-app';
|
|
@@ -170,6 +198,20 @@ describe('init — platform-correct file paths', () => {
|
|
|
170
198
|
);
|
|
171
199
|
});
|
|
172
200
|
|
|
201
|
+
it('writes .github/prompts/define.prompt.md using the OS path separator', async () => {
|
|
202
|
+
await runInit(consumerDir, {
|
|
203
|
+
targets: ['copilot'],
|
|
204
|
+
fetcher: makeFixtureFetcher(),
|
|
205
|
+
availableTags: ['v1.0.0', 'v1.2.0'],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const expectedPath = path.join(consumerDir, '.github', 'prompts', 'define.prompt.md');
|
|
209
|
+
assert.ok(
|
|
210
|
+
existsSync(expectedPath),
|
|
211
|
+
`file must exist at OS-correct path: ${expectedPath}`,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
173
215
|
it('written file content is readable via path.join on this platform', async () => {
|
|
174
216
|
await runInit(consumerDir, {
|
|
175
217
|
targets: ['claude'],
|
package/tests/update.test.mjs
CHANGED
|
@@ -42,9 +42,9 @@ function readFixtureContent(relativePath) {
|
|
|
42
42
|
* Builds a FileMap from the fixture directory.
|
|
43
43
|
* Keys are relative paths using forward slashes.
|
|
44
44
|
*/
|
|
45
|
-
function buildFixtureFileMap() {
|
|
45
|
+
function buildFixtureFileMap(targets = ['claude', 'cursor', 'copilot']) {
|
|
46
46
|
const map = {};
|
|
47
|
-
for (const target of
|
|
47
|
+
for (const target of targets) {
|
|
48
48
|
const targetDir = path.join(FIXTURE_DIR, target);
|
|
49
49
|
if (!existsSync(targetDir)) continue;
|
|
50
50
|
collectFiles(targetDir, targetDir, map, target);
|
|
@@ -320,3 +320,114 @@ describe('update — lockfile required', () => {
|
|
|
320
320
|
}
|
|
321
321
|
});
|
|
322
322
|
});
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Copilot target (.github/)
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
describe('update — copilot target writes .github/', () => {
|
|
329
|
+
let consumerDir;
|
|
330
|
+
|
|
331
|
+
beforeEach(() => {
|
|
332
|
+
consumerDir = makeTmpDir();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
afterEach(() => {
|
|
336
|
+
rmSync(consumerDir, { recursive: true, force: true });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('writes .github/prompts/ files when targets includes copilot', async () => {
|
|
340
|
+
writeLockfile(consumerDir, {
|
|
341
|
+
source: 'github:dalzoubi/dev-agents',
|
|
342
|
+
range: '^1',
|
|
343
|
+
resolvedVersion: '1.0.0',
|
|
344
|
+
targets: ['copilot'],
|
|
345
|
+
lastUpdated: '2026-04-01T00:00:00Z',
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await runUpdate(consumerDir, {
|
|
349
|
+
fetcher: makeV120Fetcher(),
|
|
350
|
+
availableTags: ['v1.0.0', 'v1.2.0'],
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const expectedPath = path.join(consumerDir, '.github', 'prompts', 'define.prompt.md');
|
|
354
|
+
assert.ok(existsSync(expectedPath), `.github/prompts/define.prompt.md must be written`);
|
|
355
|
+
const content = readFileSync(expectedPath, 'utf8');
|
|
356
|
+
assert.ok(hasMarker(content), 'written file must have the managed-by marker');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('does not write .claude/ or .cursor/ files when targets is only copilot', async () => {
|
|
360
|
+
writeLockfile(consumerDir, {
|
|
361
|
+
source: 'github:dalzoubi/dev-agents',
|
|
362
|
+
range: '^1',
|
|
363
|
+
resolvedVersion: '1.0.0',
|
|
364
|
+
targets: ['copilot'],
|
|
365
|
+
lastUpdated: '2026-04-01T00:00:00Z',
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
await runUpdate(consumerDir, {
|
|
369
|
+
fetcher: makeV120Fetcher(),
|
|
370
|
+
availableTags: ['v1.0.0', 'v1.2.0'],
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
assert.ok(
|
|
374
|
+
!existsSync(path.join(consumerDir, '.claude')),
|
|
375
|
+
'.claude/ must not be created for copilot-only target',
|
|
376
|
+
);
|
|
377
|
+
assert.ok(
|
|
378
|
+
!existsSync(path.join(consumerDir, '.cursor')),
|
|
379
|
+
'.cursor/ must not be created for copilot-only target',
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('writes all three target dirs when targets includes claude, cursor, and copilot', async () => {
|
|
384
|
+
writeLockfile(consumerDir, {
|
|
385
|
+
source: 'github:dalzoubi/dev-agents',
|
|
386
|
+
range: '^1',
|
|
387
|
+
resolvedVersion: '1.0.0',
|
|
388
|
+
targets: ['claude', 'cursor', 'copilot'],
|
|
389
|
+
lastUpdated: '2026-04-01T00:00:00Z',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await runUpdate(consumerDir, {
|
|
393
|
+
fetcher: makeV120Fetcher(),
|
|
394
|
+
availableTags: ['v1.0.0', 'v1.2.0'],
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
assert.ok(
|
|
398
|
+
existsSync(path.join(consumerDir, '.claude', 'agents', 'define.md')),
|
|
399
|
+
'.claude/agents/define.md must exist',
|
|
400
|
+
);
|
|
401
|
+
assert.ok(
|
|
402
|
+
existsSync(path.join(consumerDir, '.cursor', 'rules', 'define.mdc')),
|
|
403
|
+
'.cursor/rules/define.mdc must exist',
|
|
404
|
+
);
|
|
405
|
+
assert.ok(
|
|
406
|
+
existsSync(path.join(consumerDir, '.github', 'prompts', 'define.prompt.md')),
|
|
407
|
+
'.github/prompts/define.prompt.md must exist',
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('refuses to overwrite an unmanaged .github/ file without --force', async () => {
|
|
412
|
+
writeLockfile(consumerDir, {
|
|
413
|
+
source: 'github:dalzoubi/dev-agents',
|
|
414
|
+
range: '^1',
|
|
415
|
+
resolvedVersion: '1.0.0',
|
|
416
|
+
targets: ['copilot'],
|
|
417
|
+
lastUpdated: '2026-04-01T00:00:00Z',
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Pre-create an unmanaged file at the target path
|
|
421
|
+
const promptsDir = path.join(consumerDir, '.github', 'prompts');
|
|
422
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
423
|
+
writeFileSync(path.join(promptsDir, 'define.prompt.md'), '# Unmanaged', 'utf8');
|
|
424
|
+
|
|
425
|
+
await assert.rejects(
|
|
426
|
+
runUpdate(consumerDir, {
|
|
427
|
+
fetcher: makeV120Fetcher(),
|
|
428
|
+
availableTags: ['v1.0.0', 'v1.2.0'],
|
|
429
|
+
}),
|
|
430
|
+
/unmanaged|marker|force/i,
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
@@ -21,9 +21,13 @@ describe('normalizeFileMap — tolerance + visibility', () => {
|
|
|
21
21
|
const out = normalizeFileMap({
|
|
22
22
|
'claude/agents/define.md': 'A',
|
|
23
23
|
'cursor/rules/x.mdc': 'B',
|
|
24
|
+
'cursor/commands/define.md': 'C',
|
|
25
|
+
'cursor/agents/define.md': 'D',
|
|
24
26
|
});
|
|
25
27
|
assert.equal(out['claude/agents/define.md'], 'A');
|
|
26
28
|
assert.equal(out['cursor/rules/x.mdc'], 'B');
|
|
29
|
+
assert.equal(out['cursor/commands/define.md'], 'C');
|
|
30
|
+
assert.equal(out['cursor/agents/define.md'], 'D');
|
|
27
31
|
});
|
|
28
32
|
|
|
29
33
|
it('infers prefix for known un-prefixed first segments', () => {
|