@supersoniks/concorde 4.9.2 → 4.9.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 +3 -3
- package/ai/README.md +33 -3
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +149 -149
- package/concorde-core.es.js +2645 -2620
- package/dist/concorde-core.bundle.js +149 -149
- package/dist/concorde-core.es.js +2645 -2620
- package/dist/js/core/components/ui/icon/icon.js +17 -1
- package/dist/js/core/components/ui/icon/icon.js.map +1 -1
- package/dist/js/core/components/ui/icon/icons.js +40 -19
- package/dist/js/core/components/ui/icon/icons.js.map +1 -1
- package/dist/types/core/components/ui/icon/icon.d.ts +2 -0
- package/dist/types/core/components/ui/icon/icon.d.ts.map +1 -1
- package/dist/types/core/components/ui/icon/icons.d.ts +2 -0
- package/dist/types/core/components/ui/icon/icons.d.ts.map +1 -1
- package/package.json +3 -12
- package/scripts/ai-init.mjs +353 -67
- package/scripts/ai-init.test.mjs +206 -0
- package/scripts/pre-build.mjs +1 -0
- package/src/core/components/ui/icon/icon.ts +15 -1
- package/src/core/components/ui/icon/icons.ts +44 -18
- package/src/docs/_getting-started/ai-agents.md +24 -4
package/scripts/ai-init.mjs
CHANGED
|
@@ -1,26 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Install Concorde AI agent files into a project.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* node node_modules/@supersoniks/concorde/scripts/ai-init.mjs [options]
|
|
7
|
-
* yarn
|
|
7
|
+
* yarn ai-init
|
|
8
8
|
*
|
|
9
9
|
* Options:
|
|
10
|
-
* --project-root <dir>
|
|
11
|
-
* --concorde-ai <dir>
|
|
12
|
-
* --overlay <dir>
|
|
13
|
-
* --cursor-only
|
|
14
|
-
* --jetbrains-only
|
|
15
|
-
* --
|
|
10
|
+
* --project-root <dir> Project root (default: cwd)
|
|
11
|
+
* --concorde-ai <dir> Concorde ai/ folder (default: auto-detect)
|
|
12
|
+
* --overlay <dir> Extra layer (e.g. starter kit) — uses AGENTS.starter.md
|
|
13
|
+
* --cursor-only Install only Cursor (.cursor/)
|
|
14
|
+
* --jetbrains-only Install only JetBrains (.aiassistant/rules/)
|
|
15
|
+
* --skip-agents Never write AGENTS.md (skills/rules only)
|
|
16
|
+
* --merge-agents Opt-in: keep project preamble, append/refresh Concorde block
|
|
17
|
+
* --force-agents Opt-in: replace AGENTS.md entirely (destructive)
|
|
18
|
+
*
|
|
19
|
+
* AGENTS.md policy (non-invasive by default):
|
|
20
|
+
* - Missing file → create Concorde guide
|
|
21
|
+
* - Existing with <!-- concorde-ai --> → refresh managed zone only
|
|
22
|
+
* - Existing without marker → leave untouched (project-owned, e.g. billetterie)
|
|
23
|
+
* unless --merge-agents or --force-agents
|
|
24
|
+
*
|
|
25
|
+
* Layout when writing:
|
|
26
|
+
*
|
|
27
|
+
* [optional project preamble — short, project-only]
|
|
28
|
+
* <!-- concorde-ai -->
|
|
29
|
+
* <package ai/AGENTS.md>
|
|
30
|
+
* [--- + overlay AGENTS.starter.md]
|
|
31
|
+
*
|
|
32
|
+
* Everything AFTER the marker is owned by Concorde and replaced on every run.
|
|
33
|
+
* Do not copy the full Concorde guide into the preamble — use overlay/AGENTS.starter.md
|
|
34
|
+
* for project-specific agent notes (endpoints, dpKeys, …).
|
|
16
35
|
*/
|
|
17
36
|
|
|
18
37
|
import fs from "node:fs";
|
|
19
38
|
import path from "node:path";
|
|
20
|
-
import {fileURLToPath} from "node:url";
|
|
39
|
+
import {fileURLToPath, pathToFileURL} from "node:url";
|
|
21
40
|
|
|
22
41
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
42
|
|
|
43
|
+
const AGENTS_MARKER = "<!-- concorde-ai -->";
|
|
44
|
+
|
|
24
45
|
const args = process.argv.slice(2);
|
|
25
46
|
|
|
26
47
|
function readArg(name) {
|
|
@@ -38,10 +59,34 @@ const overlayDir = readArg("--overlay")
|
|
|
38
59
|
|
|
39
60
|
const cursorOnly = flags.has("--cursor-only");
|
|
40
61
|
const jetbrainsOnly = flags.has("--jetbrains-only");
|
|
62
|
+
const skipAgents = flags.has("--skip-agents");
|
|
41
63
|
const mergeAgents = flags.has("--merge-agents");
|
|
64
|
+
const forceAgents = flags.has("--force-agents");
|
|
42
65
|
const installCursor = !jetbrainsOnly;
|
|
43
66
|
const installJetbrains = !cursorOnly;
|
|
44
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Decide whether / how to write AGENTS.md.
|
|
70
|
+
* @returns {"skip" | "create" | "refresh" | "merge" | "force"}
|
|
71
|
+
*/
|
|
72
|
+
export function decideAgentsAction({
|
|
73
|
+
existingText = "",
|
|
74
|
+
skipAgents: skip = false,
|
|
75
|
+
mergeAgents: merge = false,
|
|
76
|
+
forceAgents: force = false,
|
|
77
|
+
} = {}) {
|
|
78
|
+
if (skip) return "skip";
|
|
79
|
+
const text = existingText.replace(/\r\n/g, "\n");
|
|
80
|
+
const exists = text.trim().length > 0;
|
|
81
|
+
const hasMarker = text.includes(AGENTS_MARKER);
|
|
82
|
+
|
|
83
|
+
if (!exists) return "create";
|
|
84
|
+
if (hasMarker) return "refresh";
|
|
85
|
+
if (force) return "force";
|
|
86
|
+
if (merge) return "merge";
|
|
87
|
+
return "skip";
|
|
88
|
+
}
|
|
89
|
+
|
|
45
90
|
function detectConcordeAi() {
|
|
46
91
|
const explicit = readArg("--concorde-ai");
|
|
47
92
|
if (explicit) return path.resolve(explicit);
|
|
@@ -72,96 +117,337 @@ function copyDir(src, dest) {
|
|
|
72
117
|
}
|
|
73
118
|
}
|
|
74
119
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
120
|
+
function normalizeHeading(title) {
|
|
121
|
+
return title
|
|
122
|
+
.normalize("NFKD")
|
|
123
|
+
.replace(/\p{M}/gu, "")
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
126
|
+
.trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isStockConcordeH1(title) {
|
|
130
|
+
// "# Agents — Concorde" with optional project suffix in parentheses
|
|
131
|
+
return /^agents\s+concorde(\s|$)/i.test(normalizeHeading(title));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Split markdown into a leading H1 block + ## sections (## title → body).
|
|
136
|
+
* @returns {{ h1: string | null, sections: Array<{ title: string, body: string }> }}
|
|
137
|
+
*/
|
|
138
|
+
function splitAgentsSections(markdown) {
|
|
139
|
+
const text = markdown.replace(/\r\n/g, "\n").trim();
|
|
140
|
+
if (!text) return {h1: null, sections: []};
|
|
141
|
+
|
|
142
|
+
const parts = text.split(/\n(?=##\s+)/);
|
|
143
|
+
let h1 = null;
|
|
144
|
+
const sections = [];
|
|
145
|
+
|
|
146
|
+
for (const part of parts) {
|
|
147
|
+
const trimmed = part.trim();
|
|
148
|
+
if (!trimmed) continue;
|
|
149
|
+
if (trimmed.startsWith("## ")) {
|
|
150
|
+
const nl = trimmed.indexOf("\n");
|
|
151
|
+
const titleLine = nl === -1 ? trimmed : trimmed.slice(0, nl);
|
|
152
|
+
const title = titleLine.replace(/^##\s+/, "").trim();
|
|
153
|
+
const body = nl === -1 ? "" : trimmed.slice(nl + 1).trim();
|
|
154
|
+
sections.push({title, body});
|
|
155
|
+
} else if (h1 === null) {
|
|
156
|
+
h1 = trimmed;
|
|
157
|
+
} else {
|
|
158
|
+
// Orphan content without ## — keep as synthetic section
|
|
159
|
+
sections.push({title: "", body: trimmed});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {h1, sections};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function baseHeadingSet(baseContent) {
|
|
167
|
+
const {sections} = splitAgentsSections(baseContent);
|
|
168
|
+
return new Set(sections.map((s) => normalizeHeading(s.title)).filter(Boolean));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Match "## Skills / rules" with "## Skills / rules (après installation)". */
|
|
172
|
+
function isSameAgentsHeading(a, b) {
|
|
173
|
+
if (!a || !b) return false;
|
|
174
|
+
if (a === b) return true;
|
|
175
|
+
const stripParen = (s) => s.replace(/\s*\(.*\)$/, "").trim();
|
|
176
|
+
const aa = stripParen(a);
|
|
177
|
+
const bb = stripParen(b);
|
|
178
|
+
if (aa === bb) return true;
|
|
179
|
+
return aa.startsWith(bb) || bb.startsWith(aa);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isPackageHeading(titleKey, baseHeads) {
|
|
183
|
+
for (const bh of baseHeads) {
|
|
184
|
+
if (isSameAgentsHeading(titleKey, bh)) return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* True when preamble looks like a full (or near-full) copy of package AGENTS.md.
|
|
191
|
+
*/
|
|
192
|
+
export function preambleDuplicatesBase(preamble, baseContent) {
|
|
193
|
+
const p = preamble.trim();
|
|
194
|
+
const b = baseContent.trim();
|
|
195
|
+
if (!p || !b) return false;
|
|
196
|
+
|
|
197
|
+
const fingerprints = [
|
|
198
|
+
"sonic-fetch",
|
|
199
|
+
"publishermanager",
|
|
200
|
+
"formdataprovider",
|
|
201
|
+
"dataprovide",
|
|
202
|
+
"concorde-get-set-dp",
|
|
203
|
+
];
|
|
204
|
+
const pNorm = p.toLowerCase();
|
|
205
|
+
const hits = fingerprints.filter((fp) => pNorm.includes(fp)).length;
|
|
206
|
+
const longEnough = p.length >= b.length * 0.45;
|
|
207
|
+
const stockTitle = /^#\s*agents\s*[—–-]\s*concorde\b/im.test(p);
|
|
208
|
+
|
|
209
|
+
return stockTitle && longEnough && hits >= 3;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Keep only project-specific preamble bits (unique ## vs package, short custom H1).
|
|
214
|
+
* Drops sections that duplicate Concorde ai/AGENTS.md so re-runs heal stacked copies.
|
|
215
|
+
*/
|
|
216
|
+
export function slimPreamble(preamble, baseContent) {
|
|
217
|
+
const raw = preamble.trim();
|
|
218
|
+
if (!raw) return "";
|
|
219
|
+
|
|
220
|
+
const {h1, sections} = splitAgentsSections(raw);
|
|
221
|
+
const h1Title = h1?.split("\n")[0]?.replace(/^#\s+/, "").trim() ?? "";
|
|
222
|
+
// Full forks OR any preamble titled like Concorde AGENTS → strip package ## sections
|
|
223
|
+
const shouldDedup =
|
|
224
|
+
preambleDuplicatesBase(raw, baseContent) || isStockConcordeH1(h1Title);
|
|
225
|
+
|
|
226
|
+
if (!shouldDedup) {
|
|
227
|
+
return raw;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const baseHeads = baseHeadingSet(baseContent);
|
|
231
|
+
const kept = [];
|
|
232
|
+
|
|
233
|
+
for (const section of sections) {
|
|
234
|
+
const key = normalizeHeading(section.title);
|
|
235
|
+
if (!key) {
|
|
236
|
+
if (!preambleDuplicatesBase(section.body, baseContent)) {
|
|
237
|
+
kept.push(section.body);
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (!isPackageHeading(key, baseHeads)) {
|
|
242
|
+
kept.push(`## ${section.title}\n\n${section.body}`.trim());
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let intro = "";
|
|
247
|
+
if (h1) {
|
|
248
|
+
const firstLine = h1.split("\n")[0].trim();
|
|
249
|
+
const title = firstLine.replace(/^#\s+/, "");
|
|
250
|
+
if (isStockConcordeH1(title) && /[()]/.test(title)) {
|
|
251
|
+
intro = firstLine;
|
|
252
|
+
} else if (!isStockConcordeH1(title)) {
|
|
253
|
+
intro = firstLine;
|
|
254
|
+
}
|
|
80
255
|
}
|
|
81
256
|
|
|
257
|
+
return [intro, ...kept].filter(Boolean).join("\n\n").trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function buildManagedAgents(baseContent, overlayContent) {
|
|
82
261
|
const parts = [baseContent.trim()];
|
|
83
262
|
if (overlayContent?.trim()) {
|
|
84
263
|
parts.push(overlayContent.trim());
|
|
85
264
|
}
|
|
265
|
+
return parts.join("\n\n---\n\n");
|
|
266
|
+
}
|
|
86
267
|
|
|
87
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Build final AGENTS.md. Never stacks managed (post-marker) content.
|
|
270
|
+
* @returns {{ markdown: string, warnings: string[] }}
|
|
271
|
+
*/
|
|
272
|
+
export function buildAgentsFile({
|
|
273
|
+
existing = "",
|
|
274
|
+
baseContent,
|
|
275
|
+
overlayContent = "",
|
|
276
|
+
mergeAgents: merge = false,
|
|
277
|
+
forceAgents: force = false,
|
|
278
|
+
}) {
|
|
279
|
+
const warnings = [];
|
|
280
|
+
const combined = buildManagedAgents(baseContent, overlayContent);
|
|
88
281
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return;
|
|
282
|
+
// Destructive replace: ignore any existing project content
|
|
283
|
+
if (force && !existing.replace(/\r\n/g, "\n").includes(AGENTS_MARKER)) {
|
|
284
|
+
return {markdown: `${combined}\n`, warnings};
|
|
93
285
|
}
|
|
94
286
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
287
|
+
let preamble = "";
|
|
288
|
+
const existingText = existing.replace(/\r\n/g, "\n");
|
|
289
|
+
|
|
290
|
+
if (existingText.includes(AGENTS_MARKER)) {
|
|
291
|
+
preamble = existingText.split(AGENTS_MARKER)[0].trimEnd();
|
|
292
|
+
} else if (merge && existingText.trim()) {
|
|
293
|
+
preamble = existingText.trimEnd();
|
|
99
294
|
}
|
|
100
295
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
);
|
|
106
|
-
|
|
296
|
+
const hadMarker = existingText.includes(AGENTS_MARKER);
|
|
297
|
+
|
|
298
|
+
if (preamble) {
|
|
299
|
+
const before = preamble;
|
|
300
|
+
preamble = slimPreamble(preamble, baseContent);
|
|
301
|
+
if (before.trim() && !preamble) {
|
|
302
|
+
warnings.push(
|
|
303
|
+
"AGENTS.md preamble duplicated Concorde ai/AGENTS.md — dropped. Put project-only notes in overlay AGENTS.starter.md (or a short preamble before <!-- concorde-ai -->).",
|
|
304
|
+
);
|
|
305
|
+
} else if (before.trim() && preamble !== before.trim()) {
|
|
306
|
+
warnings.push(
|
|
307
|
+
"AGENTS.md preamble slimmed: removed sections that duplicate package AGENTS.md.",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
107
310
|
}
|
|
108
311
|
|
|
109
|
-
|
|
312
|
+
// Always emit the marker when refreshing a marked file or merging a preamble,
|
|
313
|
+
// so re-runs stay idempotent even if the preamble was fully dropped.
|
|
314
|
+
const emitMarker = hadMarker || Boolean(preamble) || merge;
|
|
315
|
+
const markdown = emitMarker
|
|
316
|
+
? preamble
|
|
317
|
+
? `${preamble.trimEnd()}\n\n${AGENTS_MARKER}\n\n${combined}\n`
|
|
318
|
+
: `${AGENTS_MARKER}\n\n${combined}\n`
|
|
319
|
+
: `${combined}\n`;
|
|
320
|
+
|
|
321
|
+
return {markdown, warnings};
|
|
110
322
|
}
|
|
111
323
|
|
|
112
|
-
|
|
324
|
+
export function mergeAgentsFile(targetPath, baseContent, overlayContent, options = {}) {
|
|
325
|
+
const merge = options.mergeAgents ?? false;
|
|
326
|
+
const force = options.forceAgents ?? false;
|
|
327
|
+
let existing = "";
|
|
328
|
+
if (fs.existsSync(targetPath)) {
|
|
329
|
+
existing = fs.readFileSync(targetPath, "utf8");
|
|
330
|
+
}
|
|
113
331
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
332
|
+
const {markdown, warnings} = buildAgentsFile({
|
|
333
|
+
existing,
|
|
334
|
+
baseContent,
|
|
335
|
+
overlayContent,
|
|
336
|
+
mergeAgents: merge,
|
|
337
|
+
forceAgents: force,
|
|
338
|
+
});
|
|
117
339
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
340
|
+
fs.mkdirSync(path.dirname(targetPath), {recursive: true});
|
|
341
|
+
fs.writeFileSync(targetPath, markdown);
|
|
342
|
+
return warnings;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function main() {
|
|
346
|
+
const concordeAi = detectConcordeAi();
|
|
347
|
+
|
|
348
|
+
console.log(`Concorde ai-init → ${projectRoot}`);
|
|
349
|
+
console.log(` source: ${concordeAi}`);
|
|
350
|
+
if (overlayDir) console.log(` overlay: ${overlayDir}`);
|
|
127
351
|
|
|
128
|
-
if (
|
|
352
|
+
if (installCursor) {
|
|
129
353
|
copyDir(
|
|
130
|
-
path.join(
|
|
354
|
+
path.join(concordeAi, "cursor", "rules"),
|
|
131
355
|
path.join(projectRoot, ".cursor", "rules"),
|
|
132
356
|
);
|
|
133
357
|
copyDir(
|
|
134
|
-
path.join(
|
|
358
|
+
path.join(concordeAi, "skills"),
|
|
135
359
|
path.join(projectRoot, ".cursor", "skills"),
|
|
136
360
|
);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
361
|
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
362
|
+
if (overlayDir) {
|
|
363
|
+
copyDir(
|
|
364
|
+
path.join(overlayDir, "cursor", "rules"),
|
|
365
|
+
path.join(projectRoot, ".cursor", "rules"),
|
|
366
|
+
);
|
|
367
|
+
copyDir(
|
|
368
|
+
path.join(overlayDir, "skills"),
|
|
369
|
+
path.join(projectRoot, ".cursor", "skills"),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
145
373
|
|
|
146
|
-
if (
|
|
374
|
+
if (installJetbrains) {
|
|
147
375
|
copyDir(
|
|
148
|
-
path.join(
|
|
376
|
+
path.join(concordeAi, "jetbrains", "rules"),
|
|
149
377
|
path.join(projectRoot, ".aiassistant", "rules"),
|
|
150
378
|
);
|
|
379
|
+
|
|
380
|
+
if (overlayDir) {
|
|
381
|
+
copyDir(
|
|
382
|
+
path.join(overlayDir, "jetbrains", "rules"),
|
|
383
|
+
path.join(projectRoot, ".aiassistant", "rules"),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
151
386
|
}
|
|
152
|
-
}
|
|
153
387
|
|
|
154
|
-
const
|
|
155
|
-
let
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
if (fs.existsSync(overlayPath)) {
|
|
159
|
-
overlayAgents = fs.readFileSync(overlayPath, "utf8");
|
|
388
|
+
const agentsPath = path.join(projectRoot, "AGENTS.md");
|
|
389
|
+
let existingAgents = "";
|
|
390
|
+
if (fs.existsSync(agentsPath)) {
|
|
391
|
+
existingAgents = fs.readFileSync(agentsPath, "utf8");
|
|
160
392
|
}
|
|
393
|
+
|
|
394
|
+
const action = decideAgentsAction({
|
|
395
|
+
existingText: existingAgents,
|
|
396
|
+
skipAgents,
|
|
397
|
+
mergeAgents,
|
|
398
|
+
forceAgents,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
if (action === "skip") {
|
|
402
|
+
if (skipAgents) {
|
|
403
|
+
console.log(" ⊘ AGENTS.md skipped (--skip-agents)");
|
|
404
|
+
} else if (existingAgents.trim()) {
|
|
405
|
+
console.log(
|
|
406
|
+
" ⊘ AGENTS.md left untouched (project-owned, no <!-- concorde-ai --> marker).",
|
|
407
|
+
);
|
|
408
|
+
console.log(
|
|
409
|
+
" Use --merge-agents to append a Concorde block, or --force-agents to replace.",
|
|
410
|
+
);
|
|
411
|
+
} else {
|
|
412
|
+
console.log(" ⊘ AGENTS.md skipped");
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
const baseAgents = fs.readFileSync(path.join(concordeAi, "AGENTS.md"), "utf8");
|
|
416
|
+
let overlayAgents = "";
|
|
417
|
+
if (overlayDir) {
|
|
418
|
+
const overlayPath = path.join(overlayDir, "AGENTS.starter.md");
|
|
419
|
+
if (fs.existsSync(overlayPath)) {
|
|
420
|
+
overlayAgents = fs.readFileSync(overlayPath, "utf8");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const warnings = mergeAgentsFile(agentsPath, baseAgents, overlayAgents, {
|
|
425
|
+
mergeAgents: action === "merge" || action === "refresh",
|
|
426
|
+
forceAgents: action === "force",
|
|
427
|
+
});
|
|
428
|
+
for (const w of warnings) {
|
|
429
|
+
console.warn(` ⚠ ${w}`);
|
|
430
|
+
}
|
|
431
|
+
const label =
|
|
432
|
+
action === "create"
|
|
433
|
+
? "created"
|
|
434
|
+
: action === "merge"
|
|
435
|
+
? "merged"
|
|
436
|
+
: action === "force"
|
|
437
|
+
? "replaced (--force-agents)"
|
|
438
|
+
: "refreshed";
|
|
439
|
+
console.log(` ✓ AGENTS.md (${label})`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (installCursor) console.log(" ✓ .cursor/skills + .cursor/rules");
|
|
443
|
+
if (installJetbrains) console.log(" ✓ .aiassistant/rules");
|
|
444
|
+
console.log("Done.");
|
|
161
445
|
}
|
|
162
|
-
mergeAgentsFile(path.join(projectRoot, "AGENTS.md"), baseAgents, overlayAgents);
|
|
163
446
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
447
|
+
const isDirectRun =
|
|
448
|
+
process.argv[1] &&
|
|
449
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
450
|
+
|
|
451
|
+
if (isDirectRun) {
|
|
452
|
+
main();
|
|
453
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildAgentsFile,
|
|
5
|
+
decideAgentsAction,
|
|
6
|
+
preambleDuplicatesBase,
|
|
7
|
+
slimPreamble,
|
|
8
|
+
} from "./ai-init.mjs";
|
|
9
|
+
|
|
10
|
+
const base = `# Agents — Concorde
|
|
11
|
+
|
|
12
|
+
Guide for Concorde.
|
|
13
|
+
|
|
14
|
+
## Skills / rules
|
|
15
|
+
|
|
16
|
+
| File | Role |
|
|
17
|
+
|------|------|
|
|
18
|
+
| \`.cursor/skills/concorde/SKILL.md\` | Patterns |
|
|
19
|
+
|
|
20
|
+
## Conventions impératives
|
|
21
|
+
|
|
22
|
+
- Always **DataProvider**
|
|
23
|
+
- No **sonic-fetch**, no **PublisherManager**
|
|
24
|
+
- Forms: **formDataProvider**
|
|
25
|
+
- Skill **concorde-get-set-dp**
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const overlay = `## Conventions (project)
|
|
29
|
+
|
|
30
|
+
- HTTP routes: \`endpoints.*\`
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
test("decideAgentsAction: missing file → create", () => {
|
|
34
|
+
assert.equal(decideAgentsAction({existingText: ""}), "create");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("decideAgentsAction: project-owned AGENTS.md → skip by default", () => {
|
|
38
|
+
assert.equal(
|
|
39
|
+
decideAgentsAction({existingText: "# Billetterie\n\nProject notes.\n"}),
|
|
40
|
+
"skip",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("decideAgentsAction: --skip-agents always skips", () => {
|
|
45
|
+
assert.equal(
|
|
46
|
+
decideAgentsAction({existingText: "", skipAgents: true}),
|
|
47
|
+
"skip",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("decideAgentsAction: --merge-agents on project-owned → merge", () => {
|
|
52
|
+
assert.equal(
|
|
53
|
+
decideAgentsAction({
|
|
54
|
+
existingText: "# Billetterie\n\nNotes.\n",
|
|
55
|
+
mergeAgents: true,
|
|
56
|
+
}),
|
|
57
|
+
"merge",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("decideAgentsAction: --force-agents on project-owned → force", () => {
|
|
62
|
+
assert.equal(
|
|
63
|
+
decideAgentsAction({
|
|
64
|
+
existingText: "# Billetterie\n\nNotes.\n",
|
|
65
|
+
forceAgents: true,
|
|
66
|
+
}),
|
|
67
|
+
"force",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("decideAgentsAction: marked file → refresh", () => {
|
|
72
|
+
assert.equal(
|
|
73
|
+
decideAgentsAction({
|
|
74
|
+
existingText: "# App\n\n<!-- concorde-ai -->\n\nold\n",
|
|
75
|
+
}),
|
|
76
|
+
"refresh",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("forceAgents replaces project-owned content", () => {
|
|
81
|
+
const {markdown} = buildAgentsFile({
|
|
82
|
+
existing: "# Old project notes\n\nkeep me",
|
|
83
|
+
baseContent: base,
|
|
84
|
+
overlayContent: "",
|
|
85
|
+
forceAgents: true,
|
|
86
|
+
});
|
|
87
|
+
assert.equal(markdown.includes("keep me"), false);
|
|
88
|
+
assert.equal(markdown.includes("<!-- concorde-ai -->"), false);
|
|
89
|
+
assert.match(markdown, /^# Agents — Concorde/m);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("without force/merge, buildAgentsFile still builds content (caller decides skip)", () => {
|
|
93
|
+
const {markdown} = buildAgentsFile({
|
|
94
|
+
existing: "",
|
|
95
|
+
baseContent: base,
|
|
96
|
+
overlayContent: "",
|
|
97
|
+
});
|
|
98
|
+
assert.match(markdown, /^# Agents — Concorde/m);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("merge without marker keeps short unique preamble", () => {
|
|
102
|
+
const {markdown, warnings} = buildAgentsFile({
|
|
103
|
+
existing: "# My app\n\nOnly project notes here.\n",
|
|
104
|
+
baseContent: base,
|
|
105
|
+
overlayContent: "",
|
|
106
|
+
mergeAgents: true,
|
|
107
|
+
});
|
|
108
|
+
assert.match(markdown, /# My app/);
|
|
109
|
+
assert.match(markdown, /<!-- concorde-ai -->/);
|
|
110
|
+
assert.match(markdown, /# Agents — Concorde/);
|
|
111
|
+
assert.equal(warnings.length, 0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("re-run with marker replaces managed zone only (idempotent)", () => {
|
|
115
|
+
const first = buildAgentsFile({
|
|
116
|
+
existing: "# My app\n\nNote.\n",
|
|
117
|
+
baseContent: base,
|
|
118
|
+
overlayContent: "v1 overlay",
|
|
119
|
+
mergeAgents: true,
|
|
120
|
+
}).markdown;
|
|
121
|
+
|
|
122
|
+
const second = buildAgentsFile({
|
|
123
|
+
existing: first.replace("v1 overlay", "STALE"),
|
|
124
|
+
baseContent: base,
|
|
125
|
+
overlayContent: "v2 overlay",
|
|
126
|
+
mergeAgents: true,
|
|
127
|
+
}).markdown;
|
|
128
|
+
|
|
129
|
+
assert.equal(second.includes("STALE"), false);
|
|
130
|
+
assert.match(second, /v2 overlay/);
|
|
131
|
+
assert.equal(second.split("<!-- concorde-ai -->").length, 2);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("duplicate Concorde preamble is dropped", () => {
|
|
135
|
+
const fork = `${base}\n\nExtra copy of the same guide.\n`;
|
|
136
|
+
assert.equal(preambleDuplicatesBase(fork, base), true);
|
|
137
|
+
|
|
138
|
+
const {markdown, warnings} = buildAgentsFile({
|
|
139
|
+
existing: `${fork}\n\n<!-- concorde-ai -->\n\nold managed\n`,
|
|
140
|
+
baseContent: base,
|
|
141
|
+
overlayContent: overlay,
|
|
142
|
+
mergeAgents: true,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
assert.equal(markdown.split("<!-- concorde-ai -->").length, 2);
|
|
146
|
+
assert.equal(
|
|
147
|
+
markdown.indexOf("# Agents — Concorde"),
|
|
148
|
+
markdown.lastIndexOf("# Agents — Concorde"),
|
|
149
|
+
);
|
|
150
|
+
assert.match(markdown, /Conventions \(project\)/);
|
|
151
|
+
assert.ok(warnings.some((w) => /duplicated|slimmed/i.test(w)));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("slimPreamble keeps unique ## sections", () => {
|
|
155
|
+
const preamble = `# Agents — Concorde (my-app)
|
|
156
|
+
|
|
157
|
+
Guide.
|
|
158
|
+
|
|
159
|
+
## Skills / rules
|
|
160
|
+
|
|
161
|
+
dup
|
|
162
|
+
|
|
163
|
+
## Project only
|
|
164
|
+
|
|
165
|
+
- use endpoints.ts
|
|
166
|
+
`;
|
|
167
|
+
const slim = slimPreamble(preamble, base);
|
|
168
|
+
assert.match(slim, /Project only/);
|
|
169
|
+
assert.equal(slim.includes("## Skills / rules"), false);
|
|
170
|
+
assert.match(slim, /Concorde \(my-app\)/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("slimPreamble drops Skills even when package title has suffix", () => {
|
|
174
|
+
const packageBase = `# Agents — Concorde
|
|
175
|
+
|
|
176
|
+
## Skills / rules (après installation)
|
|
177
|
+
|
|
178
|
+
table
|
|
179
|
+
|
|
180
|
+
## Conventions impératives
|
|
181
|
+
|
|
182
|
+
- sonic-fetch
|
|
183
|
+
- PublisherManager
|
|
184
|
+
- formDataProvider
|
|
185
|
+
- concorde-get-set-dp
|
|
186
|
+
`;
|
|
187
|
+
const preamble = `# Agents — Concorde (app)
|
|
188
|
+
|
|
189
|
+
## Skills / rules
|
|
190
|
+
|
|
191
|
+
dup table
|
|
192
|
+
|
|
193
|
+
## Installation / réinstallation
|
|
194
|
+
|
|
195
|
+
custom install cmd
|
|
196
|
+
|
|
197
|
+
## Project endpoints
|
|
198
|
+
|
|
199
|
+
- endpoints.ts
|
|
200
|
+
`;
|
|
201
|
+
const slim = slimPreamble(preamble, packageBase);
|
|
202
|
+
assert.equal(slim.includes("## Skills / rules"), false);
|
|
203
|
+
assert.match(slim, /Project endpoints/);
|
|
204
|
+
// Installation may stay if not in package — package has no Installation ##
|
|
205
|
+
assert.match(slim, /Installation \/ réinstallation|Project endpoints/);
|
|
206
|
+
});
|