@dalzoubi/dev-agents-sync 1.0.15 → 1.0.16
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 +1 -1
- package/src/commands/check.mjs +2 -1
- package/src/commands/init.mjs +20 -9
- package/src/commands/update.mjs +17 -6
- package/src/marker.mjs +10 -0
- package/src/writer.mjs +14 -5
- package/tests/check-constitution-version.test.mjs +594 -0
- package/tests/first-sync-notice.test.mjs +459 -0
- package/tests/fixtures/release-v1.0.0/root/CONSTITUTION.md +117 -0
- package/tests/root-target.test.mjs +440 -0
package/package.json
CHANGED
package/src/commands/check.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
-
const
|
|
130
|
-
|
|
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, {
|
package/src/commands/update.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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. ` +
|