@dalzoubi/dev-agents-sync 1.0.15 → 1.0.17

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.15",
3
+ "version": "1.0.17",
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",
@@ -15,7 +15,8 @@ function filterFileMapByTargets(fileMap, targets) {
15
15
  const out = {};
16
16
  for (const [key, content] of Object.entries(fileMap)) {
17
17
  const prefix = key.split('/')[0];
18
- if (targets.includes(prefix)) {
18
+ // root-targeted files are always checked regardless of the lockfile's targets.
19
+ if (prefix === 'root' || targets.includes(prefix)) {
19
20
  out[key] = content;
20
21
  }
21
22
  }
@@ -6,13 +6,13 @@
6
6
  * Supports --dry-run (no writes).
7
7
  */
8
8
 
9
- import { existsSync } from 'node:fs';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
10
  import path from 'node:path';
11
11
 
12
12
  import { writeLockfile, DEFAULT_SOURCE } from '../lockfile.mjs';
13
13
  import { resolveRange } from '../range.mjs';
14
14
  import { resolveConsumerPath, writeManagedFile, normalizeFileMap } from '../writer.mjs';
15
- import { hasMarker } from '../marker.mjs';
15
+ import { hasMarker, hasMarkerOnFirstLine } from '../marker.mjs';
16
16
 
17
17
  const DEFAULT_RANGE = '^1';
18
18
  const ALL_TARGETS = ['claude', 'cursor'];
@@ -45,7 +45,8 @@ function filterFileMapByTargets(fileMap, targets) {
45
45
  const out = {};
46
46
  for (const [key, content] of Object.entries(fileMap)) {
47
47
  const prefix = key.split('/')[0];
48
- if (targets.includes(prefix)) {
48
+ // root-targeted files are always emitted regardless of the lockfile's targets.
49
+ if (prefix === 'root' || targets.includes(prefix)) {
49
50
  out[key] = content;
50
51
  }
51
52
  }
@@ -109,7 +110,8 @@ export async function runInit(consumerCwd, opts = {}) {
109
110
  const plannedWrites = [];
110
111
  for (const [relKey, content] of Object.entries(scoped)) {
111
112
  const absPath = resolveConsumerPath(consumerCwd, relKey);
112
- plannedWrites.push({ relKey, absPath, content });
113
+ const isNew = relKey.startsWith('root/') && !existsSync(absPath);
114
+ plannedWrites.push({ relKey, absPath, content, isNew });
113
115
  }
114
116
 
115
117
  if (dryRun) {
@@ -122,12 +124,15 @@ export async function runInit(consumerCwd, opts = {}) {
122
124
  };
123
125
  }
124
126
 
125
- // Pre-flight collision check (so we fail before any writes)
127
+ // Pre-flight collision check (so we fail before any writes).
128
+ // Root-targeted files require the marker on the literal first line;
129
+ // other managed files use the lenient (post-frontmatter) check.
126
130
  for (const { absPath, relKey } of plannedWrites) {
127
131
  if (existsSync(absPath) && !force) {
128
- const fs = await import('node:fs');
129
- const existing = fs.readFileSync(absPath, 'utf8');
130
- if (!hasMarker(existing)) {
132
+ const existing = readFileSync(absPath, 'utf8');
133
+ const isRootTarget = relKey.startsWith('root/');
134
+ const managed = isRootTarget ? hasMarkerOnFirstLine(existing) : hasMarker(existing);
135
+ if (!managed) {
131
136
  const err = new Error(
132
137
  `refusing to overwrite unmanaged file at ${relKey} ` +
133
138
  `(${absPath}). Delete/rename it or pass --force to overwrite.`,
@@ -138,8 +143,14 @@ export async function runInit(consumerCwd, opts = {}) {
138
143
  }
139
144
  }
140
145
 
141
- for (const { absPath, relKey, content } of plannedWrites) {
146
+ for (const { absPath, relKey, content, isNew } of plannedWrites) {
142
147
  writeManagedFile({ absPath, relKey, content, force: true });
148
+ if (isNew) {
149
+ const consumerRelPath = relKey.slice('root/'.length);
150
+ process.stderr.write(
151
+ `info: dev-agents-sync: created ${consumerRelPath} (new in v${resolvedVersion}; see release notes)\n`,
152
+ );
153
+ }
143
154
  }
144
155
 
145
156
  writeLockfile(consumerCwd, {
@@ -10,13 +10,14 @@ import { existsSync, readFileSync } from 'node:fs';
10
10
  import { readLockfile, writeLockfile } from '../lockfile.mjs';
11
11
  import { resolveRange } from '../range.mjs';
12
12
  import { resolveConsumerPath, writeManagedFile, normalizeFileMap } from '../writer.mjs';
13
- import { hasMarker } from '../marker.mjs';
13
+ import { hasMarker, hasMarkerOnFirstLine } from '../marker.mjs';
14
14
 
15
15
  function filterFileMapByTargets(fileMap, targets) {
16
16
  const out = {};
17
17
  for (const [key, content] of Object.entries(fileMap)) {
18
18
  const prefix = key.split('/')[0];
19
- if (targets.includes(prefix)) {
19
+ // root-targeted files are always emitted regardless of the lockfile's targets.
20
+ if (prefix === 'root' || targets.includes(prefix)) {
20
21
  out[key] = content;
21
22
  }
22
23
  }
@@ -56,7 +57,8 @@ export async function runUpdate(consumerCwd, opts = {}) {
56
57
  const plannedWrites = [];
57
58
  for (const [relKey, content] of Object.entries(scoped)) {
58
59
  const absPath = resolveConsumerPath(consumerCwd, relKey);
59
- plannedWrites.push({ relKey, absPath, content });
60
+ const isNew = relKey.startsWith('root/') && !existsSync(absPath);
61
+ plannedWrites.push({ relKey, absPath, content, isNew });
60
62
  }
61
63
 
62
64
  if (dryRun) {
@@ -68,11 +70,14 @@ export async function runUpdate(consumerCwd, opts = {}) {
68
70
  };
69
71
  }
70
72
 
71
- // Collision check
73
+ // Collision check. Root-targeted files require the marker on the literal
74
+ // first line; other managed files use the lenient (post-frontmatter) check.
72
75
  for (const { absPath, relKey } of plannedWrites) {
73
76
  if (existsSync(absPath) && !force) {
74
77
  const existing = readFileSync(absPath, 'utf8');
75
- if (!hasMarker(existing)) {
78
+ const isRootTarget = relKey.startsWith('root/');
79
+ const managed = isRootTarget ? hasMarkerOnFirstLine(existing) : hasMarker(existing);
80
+ if (!managed) {
76
81
  const err = new Error(
77
82
  `refusing to overwrite unmanaged file at ${relKey} ` +
78
83
  `(${absPath}). The marker is missing — pass --force to overwrite.`,
@@ -83,8 +88,14 @@ export async function runUpdate(consumerCwd, opts = {}) {
83
88
  }
84
89
  }
85
90
 
86
- for (const { absPath, relKey, content } of plannedWrites) {
91
+ for (const { absPath, relKey, content, isNew } of plannedWrites) {
87
92
  writeManagedFile({ absPath, relKey, content, force: true });
93
+ if (isNew) {
94
+ const consumerRelPath = relKey.slice('root/'.length);
95
+ process.stderr.write(
96
+ `info: dev-agents-sync: created ${consumerRelPath} (new in v${targetVersion}; see release notes)\n`,
97
+ );
98
+ }
88
99
  }
89
100
 
90
101
  writeLockfile(consumerCwd, {
package/src/marker.mjs CHANGED
@@ -51,6 +51,16 @@ export function hasMarker(content) {
51
51
  return MARKER_RE.test(first);
52
52
  }
53
53
 
54
+ /**
55
+ * Stricter check used for root-targeted files: the marker must be the literal
56
+ * first physical line of the file, with no frontmatter strip or blank-line skip.
57
+ */
58
+ export function hasMarkerOnFirstLine(content) {
59
+ if (!content) return false;
60
+ const firstLine = content.split(/\r?\n/)[0] ?? '';
61
+ return MARKER_RE.test(firstLine);
62
+ }
63
+
54
64
  export function extractMarkerVersion(content) {
55
65
  if (!content) return null;
56
66
  const body = stripFrontmatter(content);
package/src/writer.mjs CHANGED
@@ -10,12 +10,15 @@
10
10
 
11
11
  import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
12
12
  import path from 'node:path';
13
- import { hasMarker } from './marker.mjs';
13
+ import { hasMarker, hasMarkerOnFirstLine } from './marker.mjs';
14
14
 
15
15
  const TARGET_PREFIX = {
16
16
  claude: '.claude',
17
17
  cursor: '.cursor',
18
18
  copilot: '.github',
19
+ // root resolves to the consumer repo root (empty prefix, no subdirectory).
20
+ // Root-targeted files are emitted unconditionally regardless of the lockfile's targets.
21
+ root: '',
19
22
  };
20
23
 
21
24
  // Subdirectories that uniquely identify a target. Used to normalize
@@ -40,7 +43,7 @@ export function normalizeFileMap(fileMap) {
40
43
  for (const [key, content] of Object.entries(fileMap)) {
41
44
  const norm = key.replace(/\\/g, '/');
42
45
  const first = norm.split('/')[0];
43
- if (first === 'claude' || first === 'cursor' || first === 'copilot') {
46
+ if (first === 'claude' || first === 'cursor' || first === 'copilot' || first === 'root') {
44
47
  out[norm] = content;
45
48
  continue;
46
49
  }
@@ -90,17 +93,18 @@ export function resolveConsumerPath(consumerRoot, relPath) {
90
93
  }
91
94
 
92
95
  const [target, ...rest] = parts;
93
- const prefix = TARGET_PREFIX[target];
94
- if (!prefix) {
96
+ if (!(target in TARGET_PREFIX)) {
95
97
  throw new PathError(
96
98
  `invalid path: unknown target prefix in ${JSON.stringify(relPath)} ` +
97
99
  `(allowed: ${Object.keys(TARGET_PREFIX).join(', ')})`,
98
100
  );
99
101
  }
102
+ const prefix = TARGET_PREFIX[target];
100
103
  if (rest.length === 0) {
101
104
  throw new PathError(`invalid path (target only): ${JSON.stringify(relPath)}`);
102
105
  }
103
106
 
107
+ // root target: prefix is '' so path.join collapses to consumerRoot/<rest>
104
108
  return path.join(consumerRoot, prefix, ...rest);
105
109
  }
106
110
 
@@ -111,7 +115,12 @@ export function resolveConsumerPath(consumerRoot, relPath) {
111
115
  export function writeManagedFile({ absPath, relKey, content, force }) {
112
116
  if (existsSync(absPath) && !force) {
113
117
  const existing = readFileSync(absPath, 'utf8');
114
- if (!hasMarker(existing)) {
118
+ // Root-targeted files require the marker on the literal first physical line.
119
+ // Any other placement (buried, after frontmatter, after blank line) is treated
120
+ // as user modification — the file is considered unmanaged.
121
+ const isRootTarget = typeof relKey === 'string' && relKey.startsWith('root/');
122
+ const managed = isRootTarget ? hasMarkerOnFirstLine(existing) : hasMarker(existing);
123
+ if (!managed) {
115
124
  const err = new Error(
116
125
  `refusing to overwrite unmanaged file at ${relKey}: ` +
117
126
  `${absPath} has no managed-by marker. ` +