@supersoniks/concorde 5.1.2 → 5.1.3
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/.agents/skills/concorde-docs/SKILL.md +1 -1
- package/README.md +1 -1
- package/ai/README.md +29 -41
- package/ai/skills/concorde/SKILL.md +54 -324
- package/ai/skills/concorde/rules/composition.md +32 -0
- package/ai/skills/concorde/rules/data.md +47 -0
- package/ai/skills/concorde/rules/http.md +25 -0
- package/ai/skills/concorde/rules/lists-forms.md +35 -0
- package/ai/skills/concorde/rules/styling.md +46 -0
- package/ai/skills/concorde/rules/troubleshooting.md +45 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +21 -21
- package/concorde-core.es.js +977 -980
- package/custom-elements-manifest.public.config.mjs +8 -0
- package/{.tmp-cem/custom-elements.json → custom-elements.json} +11722 -8868
- package/dist/concorde-core.bundle.js +21 -21
- package/dist/concorde-core.es.js +977 -980
- package/dist/docs-mock-api-sw.js +58 -223
- package/dist/docs-mock-api-sw.js.map +4 -4
- package/dist/img/concorde/plane_01.jpg +0 -0
- package/dist/img/concorde/plane_02.jpg +0 -0
- package/dist/img/concorde/plane_03.jpg +0 -0
- package/dist/img/concorde/plane_04.jpg +0 -0
- package/dist/img/concorde/plane_05.jpg +0 -0
- package/dist/img/concorde/plane_06.jpg +0 -0
- package/dist/img/concorde/plane_07.jpg +0 -0
- package/dist/img/concorde/plane_08.jpg +0 -0
- package/dist/img/concorde/plane_09.jpg +0 -0
- package/dist/img/concorde/plane_10.jpg +0 -0
- package/dist/img/concorde/plane_11.jpg +0 -0
- package/dist/img/concorde/plane_12.jpg +0 -0
- package/dist/img/concorde/plane_13.jpg +0 -0
- package/dist/img/og-concorde.jpg +0 -0
- package/dist/robots.txt +11 -10
- package/docs/.htaccess +7 -0
- package/docs/about.html +111 -0
- package/docs/about.md +22 -0
- package/docs/assets/{index-BgG6FFPt.js → index-6sS76xWj.js} +482 -500
- package/docs/assets/{index-x89XINSG.css → index-DkdPbH0F.css} +1 -1
- package/docs/core/components/functional/date/date.md +1 -1
- package/docs/core/components/functional/list/list.html +7 -5
- package/docs/core/components/functional/list/list.md +6 -2
- package/docs/core/components/ui/card/card.html +1 -1
- package/docs/core/components/ui/card/card.md +1 -1
- package/docs/core/components/ui/image/image.html +17 -17
- package/docs/core/components/ui/image/image.md +17 -17
- package/docs/core/components/ui/modal/modal.html +2 -2
- package/docs/core/components/ui/modal/modal.md +2 -2
- package/docs/custom-elements.json +16559 -0
- package/docs/docs/decorators/bind.html +22 -21
- package/docs/docs/decorators/bind.md +23 -24
- package/docs/docs/getting-started/ai-agents.html +50 -94
- package/docs/docs/getting-started/ai-agents.md +29 -55
- package/docs/docs/getting-started/dataFlow.html +1 -1
- package/docs/docs/getting-started/dataFlow.md +1 -1
- package/docs/docs/index.html +1 -1
- package/docs/hubs/decorators.html +11 -16
- package/docs/hubs/remote-data.html +5 -9
- package/docs/img/og-concorde.jpg +0 -0
- package/docs/index.html +91 -2
- package/docs/index.md +2 -2
- package/docs/llms-full.txt +58 -74
- package/docs/llms.txt +6 -4
- package/docs/privacy.html +122 -0
- package/docs/privacy.md +32 -0
- package/docs/sitemap.xml +6 -0
- package/docs/src/core/components/functional/date/date.md +1 -1
- package/docs/src/core/components/functional/list/list.md +6 -2
- package/docs/src/core/components/functional/sdui/component-catalog.gaps.json +1 -1
- package/docs/src/core/components/functional/sdui/component-catalog.json +1 -1
- package/docs/src/core/components/ui/card/card.md +1 -1
- package/docs/src/core/components/ui/image/image.md +17 -17
- package/docs/src/core/components/ui/modal/modal.md +2 -2
- package/docs/src/docs/decorators/bind.md +23 -24
- package/docs/src/docs/getting-started/ai-agents.md +29 -55
- package/docs/src/docs/getting-started/dataFlow.md +1 -1
- package/docs/src/docs/search/docs-search.json +100 -55
- package/docs/src/tsconfig.json +11 -5
- package/index.html +89 -0
- package/package.json +8 -4
- package/public/img/og-concorde.jpg +0 -0
- package/scripts/ai-init.mjs +104 -379
- package/scripts/ai-init.test.mjs +81 -239
- package/scripts/docs-markdown-vite-plugin.ts +11 -5
- package/scripts/docs-public-url.mjs +6 -0
- package/scripts/docs.htaccess +7 -0
- package/scripts/generate-cem.mjs +244 -0
- package/scripts/generate-cem.test.mjs +122 -0
- package/scripts/generate-crawl-docs.mjs +29 -5
- package/scripts/generate-sdui-catalog.mjs +11 -10
- package/scripts/post-build-docs.js +3 -0
- package/scripts/pre-publish.mjs +2 -1
- package/scripts/site-pages.mjs +212 -0
- package/scripts/site-pages.test.mjs +116 -0
- package/site-pages/about.md +22 -0
- package/site-pages/privacy.md +32 -0
- package/src/core/components/functional/date/date.md +1 -1
- package/src/core/components/functional/list/list.md +6 -2
- package/src/core/components/functional/sdui/component-catalog.gaps.json +1 -1
- package/src/core/components/functional/sdui/component-catalog.json +1 -1
- package/src/core/components/ui/card/card.md +1 -1
- package/src/core/components/ui/image/image.md +17 -17
- package/src/core/components/ui/modal/modal.md +2 -2
- package/src/core/decorators/subscriber/bind.ts +9 -14
- package/src/docs/components/docs-demo-sources.ts +3 -3
- package/src/docs/decorators/bind.md +23 -24
- package/src/docs/docs-location.spec.ts +10 -0
- package/src/docs/docs-location.ts +10 -0
- package/src/docs/example/decorators-demo-bind-demos.ts +11 -28
- package/src/docs/example/docs-toggle-demos.ts +1 -1
- package/src/docs/getting-started/ai-agents.md +29 -55
- package/src/docs/getting-started/dataFlow.md +1 -1
- package/src/docs/home/home.ts +3 -1
- package/src/docs/navigation/navigation.ts +1 -1
- package/src/docs/search/docs-search.json +100 -55
- package/src/docs/search/markdown-renderer.ts +6 -1
- package/src/docs/tailwind/css/docs/content.css +4 -5
- package/src/tsconfig.json +11 -5
- package/src/tsconfig.tsbuildinfo +1 -1
- package/ai/AGENTS.md +0 -56
- package/ai/cursor/rules/concorde-menu.mdc +0 -15
- package/ai/cursor/rules/concorde-scope.mdc +0 -14
- package/ai/cursor/rules/concorde-theme.mdc +0 -13
- package/ai/cursor/rules/concorde.mdc +0 -66
- package/ai/jetbrains/rules/concorde.md +0 -53
- package/ai/skills/concorde-get-set-dp/SKILL.md +0 -194
- package/ai/skills/concorde-imports/SKILL.md +0 -78
- package/ai/skills/concorde-menu/SKILL.md +0 -74
- package/ai/skills/concorde-scope/SKILL.md +0 -70
- package/ai/skills/concorde-theme/SKILL.md +0 -57
package/scripts/ai-init.test.mjs
CHANGED
|
@@ -3,257 +3,99 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
-
import {
|
|
7
|
-
buildAgentsFile,
|
|
8
|
-
decideAgentsAction,
|
|
9
|
-
preambleDuplicatesBase,
|
|
10
|
-
readOverlayAgents,
|
|
11
|
-
slimPreamble,
|
|
12
|
-
} from "./ai-init.mjs";
|
|
6
|
+
import {SKILLS_DIR, linkSkills, syncSkills} from "./ai-init.mjs";
|
|
13
7
|
|
|
14
|
-
|
|
8
|
+
async function makeSkill(dir, name, body = "x") {
|
|
9
|
+
await fs.mkdir(path.join(dir, name), {recursive: true});
|
|
10
|
+
await fs.writeFile(path.join(dir, name, "SKILL.md"), `---\nname: ${name}\n---\n${body}\n`);
|
|
11
|
+
}
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
- HTTP routes: \`endpoints.*\`
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
test("decideAgentsAction: missing file → create", () => {
|
|
38
|
-
assert.equal(decideAgentsAction({existingText: ""}), "create");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("decideAgentsAction: project-owned AGENTS.md → skip by default", () => {
|
|
42
|
-
assert.equal(
|
|
43
|
-
decideAgentsAction({existingText: "# Billetterie\n\nProject notes.\n"}),
|
|
44
|
-
"skip",
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("decideAgentsAction: --skip-agents always skips", () => {
|
|
49
|
-
assert.equal(
|
|
50
|
-
decideAgentsAction({existingText: "", skipAgents: true}),
|
|
51
|
-
"skip",
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("decideAgentsAction: --merge-agents on project-owned → merge", () => {
|
|
56
|
-
assert.equal(
|
|
57
|
-
decideAgentsAction({
|
|
58
|
-
existingText: "# Billetterie\n\nNotes.\n",
|
|
59
|
-
mergeAgents: true,
|
|
60
|
-
}),
|
|
61
|
-
"merge",
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("decideAgentsAction: --force-agents on project-owned → force", () => {
|
|
66
|
-
assert.equal(
|
|
67
|
-
decideAgentsAction({
|
|
68
|
-
existingText: "# Billetterie\n\nNotes.\n",
|
|
69
|
-
forceAgents: true,
|
|
70
|
-
}),
|
|
71
|
-
"force",
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("decideAgentsAction: marked file → refresh", () => {
|
|
76
|
-
assert.equal(
|
|
77
|
-
decideAgentsAction({
|
|
78
|
-
existingText: "# App\n\n<!-- concorde-ai -->\n\nold\n",
|
|
79
|
-
}),
|
|
80
|
-
"refresh",
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("forceAgents replaces project-owned content", () => {
|
|
85
|
-
const {markdown} = buildAgentsFile({
|
|
86
|
-
existing: "# Old project notes\n\nkeep me",
|
|
87
|
-
baseContent: base,
|
|
88
|
-
overlayContent: "",
|
|
89
|
-
forceAgents: true,
|
|
13
|
+
async function withProject(fn) {
|
|
14
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "concorde-skills-"));
|
|
15
|
+
try {
|
|
16
|
+
await fn(root);
|
|
17
|
+
} finally {
|
|
18
|
+
await fs.rm(root, {recursive: true, force: true});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("syncSkills copies a skill with its rules/ folder into .agents/skills", async () => {
|
|
23
|
+
await withProject(async (root) => {
|
|
24
|
+
const pkg = path.join(root, "pkg");
|
|
25
|
+
await makeSkill(pkg, "concorde");
|
|
26
|
+
await fs.mkdir(path.join(pkg, "concorde", "rules"));
|
|
27
|
+
await fs.writeFile(path.join(pkg, "concorde", "rules", "data.md"), "# Data\n");
|
|
28
|
+
assert.deepEqual(syncSkills(path.join(root, "app"), [pkg]), ["concorde"]);
|
|
29
|
+
const rule = path.join(root, "app", SKILLS_DIR, "concorde", "rules", "data.md");
|
|
30
|
+
assert.equal(await fs.readFile(rule, "utf8"), "# Data\n");
|
|
90
31
|
});
|
|
91
|
-
assert.equal(markdown.includes("keep me"), false);
|
|
92
|
-
assert.equal(markdown.includes("<!-- concorde-ai -->"), false);
|
|
93
|
-
assert.match(markdown, /^# Agents — Concorde/m);
|
|
94
32
|
});
|
|
95
33
|
|
|
96
|
-
test("
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
34
|
+
test("syncSkills replaces its own skill and keeps project skills", async () => {
|
|
35
|
+
await withProject(async (root) => {
|
|
36
|
+
const pkg = path.join(root, "pkg");
|
|
37
|
+
const app = path.join(root, "app");
|
|
38
|
+
const installed = path.join(app, SKILLS_DIR);
|
|
39
|
+
await makeSkill(pkg, "concorde");
|
|
40
|
+
await makeSkill(installed, "concorde", "old copy");
|
|
41
|
+
await makeSkill(installed, "concorde-docs", "project");
|
|
42
|
+
syncSkills(app, [pkg]);
|
|
43
|
+
assert.deepEqual((await fs.readdir(installed)).sort(), ["concorde", "concorde-docs"]);
|
|
44
|
+
assert.doesNotMatch(
|
|
45
|
+
await fs.readFile(path.join(installed, "concorde", "SKILL.md"), "utf8"),
|
|
46
|
+
/old copy/,
|
|
47
|
+
);
|
|
48
|
+
assert.match(
|
|
49
|
+
await fs.readFile(path.join(installed, "concorde-docs", "SKILL.md"), "utf8"),
|
|
50
|
+
/project/,
|
|
51
|
+
);
|
|
101
52
|
});
|
|
102
|
-
assert.match(markdown, /^# Agents — Concorde/m);
|
|
103
53
|
});
|
|
104
54
|
|
|
105
|
-
test("
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
55
|
+
test("linkSkills links each skill from .claude and .junie", async () => {
|
|
56
|
+
await withProject(async (root) => {
|
|
57
|
+
await makeSkill(path.join(root, SKILLS_DIR), "concorde");
|
|
58
|
+
const results = linkSkills(root, ["concorde"]);
|
|
59
|
+
assert.deepEqual(
|
|
60
|
+
results.map((r) => r.mode),
|
|
61
|
+
["linked", "linked"],
|
|
62
|
+
);
|
|
63
|
+
for (const dir of [".claude/skills", ".junie/skills"]) {
|
|
64
|
+
const link = path.join(root, dir, "concorde");
|
|
65
|
+
assert.ok((await fs.lstat(link)).isSymbolicLink(), `${dir} not a symlink`);
|
|
66
|
+
assert.equal(await fs.readlink(link), path.join("..", "..", SKILLS_DIR, "concorde"));
|
|
67
|
+
assert.match(await fs.readFile(path.join(link, "SKILL.md"), "utf8"), /concorde/);
|
|
68
|
+
}
|
|
111
69
|
});
|
|
112
|
-
assert.match(markdown, /# My app/);
|
|
113
|
-
assert.match(markdown, /<!-- concorde-ai -->/);
|
|
114
|
-
assert.match(markdown, /# Agents — Concorde/);
|
|
115
|
-
assert.equal(warnings.length, 0);
|
|
116
70
|
});
|
|
117
71
|
|
|
118
|
-
test("
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
assert.equal(second.split("<!-- concorde-ai -->").length, 2);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("duplicate Concorde preamble is dropped", () => {
|
|
139
|
-
const fork = `${base}\n\nExtra copy of the same guide.\n`;
|
|
140
|
-
assert.equal(preambleDuplicatesBase(fork, base), true);
|
|
141
|
-
|
|
142
|
-
const {markdown, warnings} = buildAgentsFile({
|
|
143
|
-
existing: `${fork}\n\n<!-- concorde-ai -->\n\nold managed\n`,
|
|
144
|
-
baseContent: base,
|
|
145
|
-
overlayContent: overlay,
|
|
146
|
-
mergeAgents: true,
|
|
72
|
+
test("linkSkills keeps project skills and whole-folder links", async () => {
|
|
73
|
+
await withProject(async (root) => {
|
|
74
|
+
await makeSkill(path.join(root, SKILLS_DIR), "concorde");
|
|
75
|
+
await makeSkill(path.join(root, SKILLS_DIR), "my-skill");
|
|
76
|
+
// .claude/skills is already a symlink to .agents/skills (this repo's layout)
|
|
77
|
+
await fs.mkdir(path.join(root, ".claude"), {recursive: true});
|
|
78
|
+
await fs.symlink(path.join("..", SKILLS_DIR), path.join(root, ".claude/skills"));
|
|
79
|
+
// .junie has its own skill with the same name as an overlay skill
|
|
80
|
+
await makeSkill(path.join(root, ".junie/skills"), "my-skill", "junie own");
|
|
81
|
+
|
|
82
|
+
const results = linkSkills(root, ["concorde", "my-skill"], {packageNames: ["concorde"]});
|
|
83
|
+
assert.equal(results.find((r) => r.dir === ".claude/skills").mode, "shared");
|
|
84
|
+
assert.ok((await fs.lstat(path.join(root, ".claude/skills"))).isSymbolicLink());
|
|
85
|
+
const own = path.join(root, ".junie/skills/my-skill");
|
|
86
|
+
assert.ok(!(await fs.lstat(own)).isSymbolicLink());
|
|
87
|
+
assert.match(await fs.readFile(path.join(own, "SKILL.md"), "utf8"), /junie own/);
|
|
88
|
+
assert.ok((await fs.lstat(path.join(root, ".junie/skills/concorde"))).isSymbolicLink());
|
|
147
89
|
});
|
|
148
|
-
|
|
149
|
-
assert.equal(markdown.split("<!-- concorde-ai -->").length, 2);
|
|
150
|
-
assert.equal(
|
|
151
|
-
markdown.indexOf("# Agents — Concorde"),
|
|
152
|
-
markdown.lastIndexOf("# Agents — Concorde"),
|
|
153
|
-
);
|
|
154
|
-
assert.match(markdown, /Conventions \(project\)/);
|
|
155
|
-
assert.ok(warnings.some((w) => /duplicated|slimmed/i.test(w)));
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("slimPreamble keeps unique ## sections", () => {
|
|
159
|
-
const preamble = `# Agents — Concorde (my-app)
|
|
160
|
-
|
|
161
|
-
Guide.
|
|
162
|
-
|
|
163
|
-
## Skills / rules
|
|
164
|
-
|
|
165
|
-
dup
|
|
166
|
-
|
|
167
|
-
## Project only
|
|
168
|
-
|
|
169
|
-
- use endpoints.ts
|
|
170
|
-
`;
|
|
171
|
-
const slim = slimPreamble(preamble, base);
|
|
172
|
-
assert.match(slim, /Project only/);
|
|
173
|
-
assert.equal(slim.includes("## Skills / rules"), false);
|
|
174
|
-
assert.match(slim, /Concorde \(my-app\)/);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("slimPreamble drops Skills even when package title has suffix", () => {
|
|
178
|
-
const packageBase = `# Agents — Concorde
|
|
179
|
-
|
|
180
|
-
## Skills / rules (après installation)
|
|
181
|
-
|
|
182
|
-
table
|
|
183
|
-
|
|
184
|
-
## Conventions impératives
|
|
185
|
-
|
|
186
|
-
- sonic-fetch
|
|
187
|
-
- PublisherManager
|
|
188
|
-
- formDataProvider
|
|
189
|
-
- concorde-get-set-dp
|
|
190
|
-
`;
|
|
191
|
-
const preamble = `# Agents — Concorde (app)
|
|
192
|
-
|
|
193
|
-
## Skills / rules
|
|
194
|
-
|
|
195
|
-
dup table
|
|
196
|
-
|
|
197
|
-
## Installation / réinstallation
|
|
198
|
-
|
|
199
|
-
custom install cmd
|
|
200
|
-
|
|
201
|
-
## Project endpoints
|
|
202
|
-
|
|
203
|
-
- endpoints.ts
|
|
204
|
-
`;
|
|
205
|
-
const slim = slimPreamble(preamble, packageBase);
|
|
206
|
-
assert.equal(slim.includes("## Skills / rules"), false);
|
|
207
|
-
assert.match(slim, /Project endpoints/);
|
|
208
|
-
// Installation may stay if not in package — package has no Installation ##
|
|
209
|
-
assert.match(slim, /Installation \/ réinstallation|Project endpoints/);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("readOverlayAgents: prefers AGENTS.concorde.md", async () => {
|
|
213
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "concorde-overlay-"));
|
|
214
|
-
try {
|
|
215
|
-
await fs.writeFile(
|
|
216
|
-
path.join(dir, "AGENTS.concorde.md"),
|
|
217
|
-
"# overlay preferred\n",
|
|
218
|
-
);
|
|
219
|
-
await fs.writeFile(
|
|
220
|
-
path.join(dir, "AGENTS.starter.md"),
|
|
221
|
-
"# overlay legacy\n",
|
|
222
|
-
);
|
|
223
|
-
const {content, warnings} = readOverlayAgents(dir);
|
|
224
|
-
assert.match(content, /overlay preferred/);
|
|
225
|
-
assert.equal(warnings.length, 0);
|
|
226
|
-
} finally {
|
|
227
|
-
await fs.rm(dir, {recursive: true, force: true});
|
|
228
|
-
}
|
|
229
90
|
});
|
|
230
91
|
|
|
231
|
-
test("
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
await
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
assert.match(warnings[0] ?? "", /deprecated AGENTS\.starter\.md/);
|
|
241
|
-
} finally {
|
|
242
|
-
await fs.rm(dir, {recursive: true, force: true});
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test("readOverlayAgents: falls back to AGENTS.overlay.md with warning", async () => {
|
|
247
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "concorde-overlay-"));
|
|
248
|
-
try {
|
|
249
|
-
await fs.writeFile(
|
|
250
|
-
path.join(dir, "AGENTS.overlay.md"),
|
|
251
|
-
"# overlay mid legacy\n",
|
|
252
|
-
);
|
|
253
|
-
const {content, warnings} = readOverlayAgents(dir);
|
|
254
|
-
assert.match(content, /overlay mid legacy/);
|
|
255
|
-
assert.match(warnings[0] ?? "", /deprecated AGENTS\.overlay\.md/);
|
|
256
|
-
} finally {
|
|
257
|
-
await fs.rm(dir, {recursive: true, force: true});
|
|
258
|
-
}
|
|
92
|
+
test("linkSkills replaces an earlier copy of a package skill", async () => {
|
|
93
|
+
await withProject(async (root) => {
|
|
94
|
+
await makeSkill(path.join(root, SKILLS_DIR), "concorde", "new");
|
|
95
|
+
await makeSkill(path.join(root, ".junie/skills"), "concorde", "copied before");
|
|
96
|
+
linkSkills(root, ["concorde"], {packageNames: ["concorde"]});
|
|
97
|
+
const link = path.join(root, ".junie/skills/concorde");
|
|
98
|
+
assert.ok((await fs.lstat(link)).isSymbolicLink());
|
|
99
|
+
assert.match(await fs.readFile(path.join(link, "SKILL.md"), "utf8"), /new/);
|
|
100
|
+
});
|
|
259
101
|
});
|
|
@@ -2,14 +2,18 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import type { Connect, Plugin } from "vite";
|
|
5
|
-
import { toSourceId } from "./docs-public-url.mjs";
|
|
5
|
+
import { SITE_PAGE_SLUGS, toSourceId } from "./docs-public-url.mjs";
|
|
6
6
|
|
|
7
7
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
8
|
const MD_PATH_RE = /^\/((?:docs|core)\/.+\.mdx?)(?:\?|$)/i;
|
|
9
|
+
const SITE_PAGE_MD_RE = new RegExp(
|
|
10
|
+
`^/(${SITE_PAGE_SLUGS.join("|")})\\.md(?:\\?|$)`,
|
|
11
|
+
);
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Dev: serve public `/docs|core/…/*.md` from `src/…` (including underscore
|
|
12
|
-
* sections and `name/name.md` component pages).
|
|
15
|
+
* sections and `name/name.md` component pages), and `/about.md` & co. from
|
|
16
|
+
* `site-pages/`.
|
|
13
17
|
*/
|
|
14
18
|
export function docsMarkdownVitePlugin(): Plugin {
|
|
15
19
|
return {
|
|
@@ -17,13 +21,15 @@ export function docsMarkdownVitePlugin(): Plugin {
|
|
|
17
21
|
configureServer(server) {
|
|
18
22
|
server.middlewares.use(((req, res, next) => {
|
|
19
23
|
const raw = req.url || "";
|
|
24
|
+
const sitePage = SITE_PAGE_MD_RE.exec(raw);
|
|
20
25
|
const match = MD_PATH_RE.exec(raw);
|
|
21
|
-
if (!match) {
|
|
26
|
+
if (!match && !sitePage) {
|
|
22
27
|
next();
|
|
23
28
|
return;
|
|
24
29
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
30
|
+
const filePath = sitePage
|
|
31
|
+
? path.join(root, "site-pages", `${sitePage[1]}.md`)
|
|
32
|
+
: path.join(root, "src", `${toSourceId(match![1])}.md`);
|
|
27
33
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
28
34
|
next();
|
|
29
35
|
return;
|
|
@@ -16,6 +16,12 @@ export const COMPONENT_MD_EXCEPTIONS = new Set([
|
|
|
16
16
|
"core/components/functional/sdui/sdui-extension-points",
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Root-level site pages (`/about` ↔ `site-pages/about.md`, published as
|
|
21
|
+
* `/about.md`). Kept outside src/ so they stay out of doc nav and search.
|
|
22
|
+
*/
|
|
23
|
+
export const SITE_PAGE_SLUGS = ["about", "privacy"];
|
|
24
|
+
|
|
19
25
|
/** Strip leading slash and optional .md/.mdx */
|
|
20
26
|
export function stripMd(rel) {
|
|
21
27
|
return rel.replace(/^\//, "").replace(/\.mdx?$/i, "");
|
package/scripts/docs.htaccess
CHANGED
|
@@ -17,6 +17,13 @@ RewriteCond %{HTTP:Accept} "text/markdown" [NC]
|
|
|
17
17
|
RewriteCond %{HTTP:Accept} !"text/markdown\s*;\s*q\s*=\s*0(\.0+)?(\s*,|$)" [NC]
|
|
18
18
|
RewriteRule ^/?$ index.md [END]
|
|
19
19
|
|
|
20
|
+
# --- Site pages: /about, /privacy (SPA shell with inline content + .md) ---
|
|
21
|
+
RewriteRule ^(about|privacy)/$ /$1 [R=301,L]
|
|
22
|
+
RewriteCond %{HTTP:Accept} "text/markdown" [NC]
|
|
23
|
+
RewriteCond %{HTTP:Accept} !"text/markdown\s*;\s*q\s*=\s*0(\.0+)?(\s*,|$)" [NC]
|
|
24
|
+
RewriteRule ^(about|privacy)$ /$1.md [END]
|
|
25
|
+
RewriteRule ^(about|privacy)$ /$1.html [END]
|
|
26
|
+
|
|
20
27
|
# --- Components: /…/button(.md|.html) → /…/button/button.(md|html) ---
|
|
21
28
|
RewriteCond %{DOCUMENT_ROOT}/$1/$2/$2.md -f
|
|
22
29
|
RewriteRule ^(core/components/.+)/([^/]+)\.md$ /$1/$2/$2.md [END]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishes the Custom Elements Manifest (custom-elements.json) at the package
|
|
3
|
+
* root: the standard machine-readable API of every sonic-* element, for IDEs
|
|
4
|
+
* and coding agents.
|
|
5
|
+
*
|
|
6
|
+
* Starts from the raw analyzer output and:
|
|
7
|
+
* - fills `tagName` (components use `@customElement(tagName)` with a constant
|
|
8
|
+
* the analyzer does not resolve);
|
|
9
|
+
* - expands string-literal union aliases (`ButtonType` → `"default" | …`) so
|
|
10
|
+
* allowed values are visible;
|
|
11
|
+
* - keeps custom elements and their public API only.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import { extractTagNameFromSource, runCem } from "./generate-sdui-catalog.mjs";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
20
|
+
export const CEM_OUT = path.join(ROOT, "custom-elements.json");
|
|
21
|
+
const CEM_CONFIG = path.join(ROOT, "custom-elements-manifest.public.config.mjs");
|
|
22
|
+
const CEM_RAW = path.join(ROOT, ".tmp-cem-public", "custom-elements.json");
|
|
23
|
+
|
|
24
|
+
const LIFECYCLE = new Set([
|
|
25
|
+
"render",
|
|
26
|
+
"connectedCallback",
|
|
27
|
+
"disconnectedCallback",
|
|
28
|
+
"attributeChangedCallback",
|
|
29
|
+
"createRenderRoot",
|
|
30
|
+
"firstUpdated",
|
|
31
|
+
"updated",
|
|
32
|
+
"willUpdate",
|
|
33
|
+
"shouldUpdate",
|
|
34
|
+
"update",
|
|
35
|
+
"getUpdateComplete",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `export type X = "a" | "b"` aliases made only of string literals.
|
|
40
|
+
* A name defined twice with different values is left out.
|
|
41
|
+
* @param {string[]} sources TypeScript source texts
|
|
42
|
+
* @returns {Map<string, string>} alias name → `"a" | "b"`
|
|
43
|
+
*/
|
|
44
|
+
export function collectLiteralUnions(sources) {
|
|
45
|
+
const unions = new Map();
|
|
46
|
+
const conflicts = new Set();
|
|
47
|
+
const re = /export\s+type\s+([A-Z]\w*)\s*=\s*([^;]+);/g;
|
|
48
|
+
for (const src of sources) {
|
|
49
|
+
for (const [, name, body] of src.matchAll(re)) {
|
|
50
|
+
const parts = body
|
|
51
|
+
.split("|")
|
|
52
|
+
.map((p) => p.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
if (!parts.length || !parts.every((p) => /^(["'])[^"']*\1$/.test(p))) continue;
|
|
55
|
+
const text = parts.map((p) => `"${p.slice(1, -1)}"`).join(" | ");
|
|
56
|
+
if (unions.has(name) && unions.get(name) !== text) conflicts.add(name);
|
|
57
|
+
unions.set(name, text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const name of conflicts) unions.delete(name);
|
|
61
|
+
return unions;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Replace alias identifiers in a type text by their literal union. */
|
|
65
|
+
export function expandType(text, unions) {
|
|
66
|
+
if (!text) return text;
|
|
67
|
+
return text
|
|
68
|
+
.replace(/\b[A-Z]\w*\b/g, (id) => unions.get(id) ?? id)
|
|
69
|
+
.replace(/\s+/g, " ")
|
|
70
|
+
.replace(/^\|\s*/, "")
|
|
71
|
+
.trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Names declared with Lit `@property(…)` (fields and accessors) in a source. */
|
|
75
|
+
export function collectLitProperties(source) {
|
|
76
|
+
const names = new Set();
|
|
77
|
+
const re =
|
|
78
|
+
/@property\((?:[^()]|\([^()]*\))*\)\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*(?:(?:declare|readonly|public|override|accessor)\s+)*(?:(?:get|set)\s+)?(\w+)/g;
|
|
79
|
+
for (const [, name] of source.matchAll(re)) names.add(name);
|
|
80
|
+
return names;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function withType(entry, unions) {
|
|
84
|
+
if (!entry?.type?.text) return entry;
|
|
85
|
+
return { ...entry, type: { ...entry.type, text: expandType(entry.type.text, unions) } };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Public API: Lit properties (with or without attribute) and documented
|
|
90
|
+
* public methods. Undocumented helpers that merely lack `private` are dropped.
|
|
91
|
+
*/
|
|
92
|
+
function isPublicMember(member, isLitProperty) {
|
|
93
|
+
if (member.static) return false;
|
|
94
|
+
if (member.privacy === "private" || member.privacy === "protected") return false;
|
|
95
|
+
if (member.name.startsWith("_") || member.name.startsWith("#")) return false;
|
|
96
|
+
if (member.kind === "field") return Boolean(member.attribute) || isLitProperty(member);
|
|
97
|
+
if (member.kind === "method") return !LIFECYCLE.has(member.name) && Boolean(member.description);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {object} raw analyzer manifest
|
|
103
|
+
* @param {{
|
|
104
|
+
* resolveTagName: (modulePath: string) => string | undefined,
|
|
105
|
+
* unions: Map<string, string>,
|
|
106
|
+
* litProperties: (modulePath: string) => Set<string>,
|
|
107
|
+
* exclude?: (modulePath: string) => boolean,
|
|
108
|
+
* }} options
|
|
109
|
+
*/
|
|
110
|
+
export function buildManifest(raw, { resolveTagName, unions, litProperties, exclude = () => false }) {
|
|
111
|
+
const byModule = new Map((raw.modules || []).map((m) => [m.path, m]));
|
|
112
|
+
/** Mixin declaration for `{ name, module | package }` (package paths map to src/). */
|
|
113
|
+
const resolveMixin = (ref) => {
|
|
114
|
+
const modulePath =
|
|
115
|
+
ref.module ??
|
|
116
|
+
(ref.package?.startsWith("@supersoniks/concorde/")
|
|
117
|
+
? `src/${ref.package.slice("@supersoniks/concorde/".length)}.ts`
|
|
118
|
+
: undefined);
|
|
119
|
+
const mod = modulePath && byModule.get(modulePath);
|
|
120
|
+
const decl = mod?.declarations?.find((d) => d.kind === "mixin");
|
|
121
|
+
return decl ? { decl, modulePath, name: ref.name } : undefined;
|
|
122
|
+
};
|
|
123
|
+
/** Own entries first, then each mixin's, skipping names already present. */
|
|
124
|
+
const withMixins = (d, field) => {
|
|
125
|
+
const out = [...(d[field] || [])];
|
|
126
|
+
const seen = new Set(out.map((e) => e.name));
|
|
127
|
+
for (const ref of d.mixins || []) {
|
|
128
|
+
const mixin = resolveMixin(ref);
|
|
129
|
+
for (const entry of mixin?.decl[field] || []) {
|
|
130
|
+
if (seen.has(entry.name)) continue;
|
|
131
|
+
seen.add(entry.name);
|
|
132
|
+
out.push({ ...entry, inheritedFrom: { name: mixin.name, module: mixin.modulePath } });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
};
|
|
137
|
+
const modules = [];
|
|
138
|
+
for (const mod of raw.modules || []) {
|
|
139
|
+
if (exclude(mod.path)) continue;
|
|
140
|
+
const defined = new Set(
|
|
141
|
+
(mod.exports || [])
|
|
142
|
+
.filter((e) => e.kind === "custom-element-definition")
|
|
143
|
+
.map((e) => e.declaration?.name),
|
|
144
|
+
);
|
|
145
|
+
const elements = (mod.declarations || []).filter(
|
|
146
|
+
(d) => d.kind === "class" && (d.customElement || defined.has(d.name)),
|
|
147
|
+
);
|
|
148
|
+
const declarations = [];
|
|
149
|
+
for (const element of elements) {
|
|
150
|
+
const tagName = element.tagName || resolveTagName(mod.path);
|
|
151
|
+
if (!tagName) continue;
|
|
152
|
+
const d = {
|
|
153
|
+
...element,
|
|
154
|
+
attributes: withMixins(element, "attributes"),
|
|
155
|
+
members: withMixins(element, "members"),
|
|
156
|
+
};
|
|
157
|
+
declarations.push({
|
|
158
|
+
kind: "class",
|
|
159
|
+
name: d.name,
|
|
160
|
+
tagName,
|
|
161
|
+
customElement: true,
|
|
162
|
+
...(d.summary && { summary: d.summary }),
|
|
163
|
+
...(d.description && { description: d.description }),
|
|
164
|
+
...(d.attributes?.length && {
|
|
165
|
+
attributes: d.attributes.map((a) => withType(a, unions)),
|
|
166
|
+
}),
|
|
167
|
+
...(d.members?.length && {
|
|
168
|
+
members: d.members
|
|
169
|
+
.filter((m) =>
|
|
170
|
+
isPublicMember(m, (member) =>
|
|
171
|
+
litProperties(member.inheritedFrom?.module ?? mod.path).has(member.name),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
.map((m) => withType(m, unions)),
|
|
175
|
+
}),
|
|
176
|
+
...(d.events?.length && { events: d.events }),
|
|
177
|
+
...(d.slots?.length && { slots: d.slots }),
|
|
178
|
+
...(d.cssProperties?.length && { cssProperties: d.cssProperties }),
|
|
179
|
+
...(d.cssParts?.length && { cssParts: d.cssParts }),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (!declarations.length) continue;
|
|
183
|
+
modules.push({
|
|
184
|
+
kind: "javascript-module",
|
|
185
|
+
path: mod.path,
|
|
186
|
+
declarations,
|
|
187
|
+
exports: declarations.flatMap((d) => [
|
|
188
|
+
{ kind: "js", name: d.name, declaration: { name: d.name, module: mod.path } },
|
|
189
|
+
{
|
|
190
|
+
kind: "custom-element-definition",
|
|
191
|
+
name: d.tagName,
|
|
192
|
+
declaration: { name: d.name, module: mod.path },
|
|
193
|
+
},
|
|
194
|
+
]),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
modules.sort((a, b) => a.path.localeCompare(b.path));
|
|
198
|
+
return { schemaVersion: raw.schemaVersion || "1.0.0", readme: "", modules };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function listTsFiles(dir, files = []) {
|
|
202
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
203
|
+
const p = path.join(dir, entry.name);
|
|
204
|
+
if (entry.isDirectory()) listTsFiles(p, files);
|
|
205
|
+
else if (entry.name.endsWith(".ts") && !/\.(spec|test|demo)\.ts$/.test(entry.name)) {
|
|
206
|
+
files.push(p);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return files;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Writes custom-elements.json at the package root and returns its path. */
|
|
213
|
+
export function generateCem() {
|
|
214
|
+
runCem(CEM_CONFIG);
|
|
215
|
+
const raw = JSON.parse(fs.readFileSync(CEM_RAW, "utf8"));
|
|
216
|
+
const unions = collectLiteralUnions(
|
|
217
|
+
listTsFiles(path.join(ROOT, "src/core")).map((f) => fs.readFileSync(f, "utf8")),
|
|
218
|
+
);
|
|
219
|
+
const propertyCache = new Map();
|
|
220
|
+
const manifest = buildManifest(raw, {
|
|
221
|
+
unions,
|
|
222
|
+
resolveTagName: (modulePath) => extractTagNameFromSource(path.join(ROOT, modulePath)),
|
|
223
|
+
litProperties: (modulePath) => {
|
|
224
|
+
if (!propertyCache.has(modulePath)) {
|
|
225
|
+
const file = path.join(ROOT, modulePath);
|
|
226
|
+
propertyCache.set(
|
|
227
|
+
modulePath,
|
|
228
|
+
fs.existsSync(file) ? collectLitProperties(fs.readFileSync(file, "utf8")) : new Set(),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return propertyCache.get(modulePath);
|
|
232
|
+
},
|
|
233
|
+
// Copy-paste starter, not a library element
|
|
234
|
+
exclude: (modulePath) => modulePath.includes("/functional/example/"),
|
|
235
|
+
});
|
|
236
|
+
fs.writeFileSync(CEM_OUT, JSON.stringify(manifest, null, 2) + "\n");
|
|
237
|
+
const count = manifest.modules.reduce((n, m) => n + m.declarations.length, 0);
|
|
238
|
+
console.log(`✅ custom-elements.json: ${count} elements`);
|
|
239
|
+
return CEM_OUT;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
243
|
+
generateCem();
|
|
244
|
+
}
|