@dalzoubi/dev-agents-sync 1.0.9 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalzoubi/dev-agents-sync",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "type": "module",
5
5
  "description": "CLI that syncs managed dev-agent prompts into consumer repos (.claude/ and/or .cursor/).",
6
6
  "license": "UNLICENSED",
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
- const VALID_TARGETS = new Set(['claude', 'cursor']);
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.
@@ -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;
@@ -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 ['claude', 'cursor']) {
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);
@@ -113,6 +113,20 @@ describe('resolveConsumerPath', () => {
113
113
  assert.equal(result, expected);
114
114
  });
115
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
+
116
130
  it('works with a Windows-style absolute consumer root path', () => {
117
131
  // Simulate a Windows-style path
118
132
  const consumerRoot = 'C:\\Users\\developer\\projects\\my-app';
@@ -184,6 +198,20 @@ describe('init — platform-correct file paths', () => {
184
198
  );
185
199
  });
186
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
+
187
215
  it('written file content is readable via path.join on this platform', async () => {
188
216
  await runInit(consumerDir, {
189
217
  targets: ['claude'],
@@ -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 ['claude', 'cursor']) {
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
+ });