@spectrabrain/eds 0.0.4 → 0.1.1

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.
Files changed (62) hide show
  1. package/AI.md +13 -40
  2. package/AI_CONTEXT.json +90 -0
  3. package/CHANGELOG.md +120 -0
  4. package/CONSUMER_AGENT.md +152 -0
  5. package/FRONTEND_CONVENTIONS.md +252 -0
  6. package/QUICKSTART.md +42 -0
  7. package/README.md +38 -69
  8. package/TAILWIND_V4.md +57 -243
  9. package/TOKEN_CONTEXT.md +98 -0
  10. package/consumer/align/templates/components.json +21 -0
  11. package/consumer/align/templates/env.mock +1 -0
  12. package/consumer/align/templates/eslint.config.js +35 -0
  13. package/consumer/align/templates/package.scripts.json +10 -0
  14. package/consumer/align/templates/prettierignore +2 -0
  15. package/consumer/align/templates/prettierrc.json +7 -0
  16. package/consumer/align/templates/src/app/App.tsx +16 -0
  17. package/consumer/align/templates/src/app/main.tsx +27 -0
  18. package/consumer/align/templates/src/app/providers/ErrorBoundary.tsx +58 -0
  19. package/consumer/align/templates/src/app/providers/ErrorHandler.tsx +29 -0
  20. package/consumer/align/templates/src/app/router/index.tsx +23 -0
  21. package/consumer/align/templates/src/app/styles/index.css +2 -0
  22. package/consumer/align/templates/src/mocks/browser.ts +5 -0
  23. package/consumer/align/templates/src/mocks/handlers.ts +4 -0
  24. package/consumer/align/templates/src/shared/api/index.ts +23 -0
  25. package/consumer/align/templates/src/shared/api/queryClient.ts +24 -0
  26. package/consumer/align/templates/src/shared/lib/eventBus.ts +44 -0
  27. package/consumer/align/templates/src/shared/lib/utils/index.ts +6 -0
  28. package/consumer/align/templates/src/vite-env.d.ts +2 -0
  29. package/consumer/align/templates/tsconfig.json +22 -0
  30. package/consumer/align/templates/vite.config.babel.ts +36 -0
  31. package/consumer/align/templates/vite.config.rolldown.ts +35 -0
  32. package/consumer/align-analysis.mjs +1159 -0
  33. package/consumer/align-plan.mjs +377 -0
  34. package/consumer/align-rules.mjs +473 -0
  35. package/consumer/align.mjs +401 -0
  36. package/consumer/cli.mjs +157 -0
  37. package/consumer/css-root.mjs +259 -0
  38. package/consumer/doctor.mjs +204 -0
  39. package/consumer/generate-tailwind.mjs +83 -0
  40. package/consumer/init.mjs +186 -0
  41. package/consumer/managed-block.mjs +126 -0
  42. package/consumer/paths.mjs +266 -0
  43. package/consumer/register-scripts.mjs +129 -0
  44. package/consumer/scan-classes.mjs +71 -0
  45. package/consumer/sync.mjs +155 -0
  46. package/consumer/update-agent-context.mjs +143 -0
  47. package/consumer-postinstall.mjs +60 -0
  48. package/dist/base.css +4 -0
  49. package/dist/components/Button/README.md +70 -80
  50. package/dist/components/Button/spec.json +71 -133
  51. package/dist/components.css +102 -368
  52. package/dist/index.d.mts +40 -1
  53. package/dist/index.d.ts +40 -1
  54. package/dist/index.js +34 -6250
  55. package/dist/index.mjs +36 -6252
  56. package/dist/styles.css +1 -0
  57. package/dist/tokens.d.mts +5 -2
  58. package/dist/tokens.d.ts +5 -2
  59. package/dist/tokens.js +6 -7
  60. package/dist/tokens.mjs +6 -7
  61. package/llms.txt +22 -9
  62. package/package.json +33 -2
@@ -0,0 +1,129 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { cliCommand, detectIndent, detectPackageManager, runnerCommand } from "./paths.mjs";
4
+
5
+ export const syncCommand = "eds sync";
6
+ export const doctorCommand = "eds doctor";
7
+ export const syncScriptName = "sync:eds";
8
+ export const doctorScriptName = "eds:doctor";
9
+
10
+ const legacyRunnerCommand = "node --input-type=module -e \"import('@spectrabrain/eds/consumer-sync.mjs')\"";
11
+ const requiredScripts = {
12
+ [syncScriptName]: syncCommand,
13
+ [doctorScriptName]: doctorCommand,
14
+ };
15
+ const legacyScripts = {
16
+ [syncScriptName]: [
17
+ "npm run update:eds-agent-context && npm run generate:eds-tailwind",
18
+ "npm run update:eds-agent-context && npm run generate-eds-tailwind",
19
+ ],
20
+ "update:eds-agent-context": [legacyRunnerCommand],
21
+ "generate:eds-tailwind": [legacyRunnerCommand],
22
+ };
23
+ const hookScripts = [
24
+ { name: "pretypecheck", target: "typecheck" },
25
+ { name: "prebuild", target: "build" },
26
+ ];
27
+
28
+ export function writeConsumerPackageJson(packagePath, source, consumerPackage) {
29
+ const trailingNewline = source.endsWith("\n") ? "\n" : "";
30
+ fs.writeFileSync(packagePath, JSON.stringify(consumerPackage, null, detectIndent(source)) + trailingNewline);
31
+ }
32
+
33
+ // A consumer may wrap the EDS command (for example `cross-env EDS_DOCTOR=warn eds doctor`). Any script that
34
+ // still runs the EDS command counts as registered; only scripts that do not run it are reported.
35
+ export function isCompatibleCommand(value, command) {
36
+ if (typeof value !== "string") {
37
+ return false;
38
+ }
39
+ const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40
+ return new RegExp("(^|[\\s&|;(])" + escaped + "($|[\\s&|;)])").test(value);
41
+ }
42
+
43
+ export function registerConsumerScripts(
44
+ consumerRoot,
45
+ { log = console.log, dryRun = false, packageManager = detectPackageManager(consumerRoot) } = {},
46
+ ) {
47
+ const packagePath = path.join(consumerRoot, "package.json");
48
+ const source = fs.readFileSync(packagePath, "utf8");
49
+ const consumerPackage = JSON.parse(source);
50
+ const scripts = { ...(consumerPackage.scripts ?? {}) };
51
+ const result = { changed: false, added: [], migrated: [], removed: [], skippedHooks: [], conflicts: [], syncRunCommand: null };
52
+
53
+ for (const [name, command] of Object.entries(requiredScripts)) {
54
+ if (isCompatibleCommand(scripts[name], command)) {
55
+ continue;
56
+ }
57
+ if (scripts[name] === undefined) {
58
+ scripts[name] = command;
59
+ result.added.push(name);
60
+ continue;
61
+ }
62
+ if (legacyScripts[name]?.includes(scripts[name])) {
63
+ scripts[name] = command;
64
+ result.migrated.push(name);
65
+ continue;
66
+ }
67
+ // Never fail the install over a consumer-owned script: leave it alone and let doctor report it.
68
+ result.conflicts.push(name);
69
+ log(
70
+ `[EDS] package.json script ${name} runs ${JSON.stringify(scripts[name])}; left unchanged. ` +
71
+ `eds doctor reports it until the script runs "${command}".`,
72
+ );
73
+ }
74
+
75
+ for (const [name, commands] of Object.entries(legacyScripts)) {
76
+ if (requiredScripts[name] === undefined && commands.includes(scripts[name])) {
77
+ delete scripts[name];
78
+ result.removed.push(name);
79
+ }
80
+ }
81
+
82
+ const hookTargets = [];
83
+ for (const hook of hookScripts) {
84
+ if (scripts[hook.target] === undefined) {
85
+ continue;
86
+ }
87
+ hookTargets.push(hook.target);
88
+ if (isCompatibleCommand(scripts[hook.name], doctorCommand)) {
89
+ continue;
90
+ }
91
+ if (scripts[hook.name] === undefined) {
92
+ scripts[hook.name] = doctorCommand;
93
+ result.added.push(hook.name);
94
+ continue;
95
+ }
96
+ result.skippedHooks.push(hook.name);
97
+ log(
98
+ `[EDS] ${hook.name} already runs ${JSON.stringify(scripts[hook.name])}; left unchanged. Change it to ` +
99
+ `${JSON.stringify(doctorCommand + " && " + scripts[hook.name])} so ${doctorCommand} gates ${hook.target}.`,
100
+ );
101
+ }
102
+
103
+ result.changed = result.added.length > 0 || result.migrated.length > 0 || result.removed.length > 0;
104
+ // When sync:eds belongs to the consumer, point at the CLI instead of their script. The caller prints the next
105
+ // step: init runs sync right away, postinstall tells the consumer to run it.
106
+ result.syncRunCommand = result.conflicts.includes(syncScriptName) ? cliCommand(packageManager, "sync") : runnerCommand(packageManager, syncScriptName);
107
+ if (result.changed && dryRun) {
108
+ log("[EDS] would update package.json scripts: " + [...result.added, ...result.migrated, ...result.removed].join(", "));
109
+ } else if (result.changed) {
110
+ consumerPackage.scripts = scripts;
111
+ writeConsumerPackageJson(packagePath, source, consumerPackage);
112
+ log("[EDS] Added the EDS scripts to package.json: " + [...result.added, ...result.migrated].join(", ") +
113
+ (result.removed.length > 0 ? " (removed " + result.removed.join(", ") + ")" : "") + ".");
114
+ } else if (result.conflicts.length === 0) {
115
+ log("[EDS] The EDS scripts are already registered.");
116
+ } else {
117
+ log("[EDS] The EDS scripts were left unchanged.");
118
+ }
119
+
120
+ // Yarn berry (2+) does not run pre/post lifecycle hooks, so the doctor gate must be inlined by the consumer.
121
+ if (packageManager?.name === "yarn" && packageManager.berry && hookTargets.length > 0) {
122
+ log(
123
+ "[EDS] Yarn 2+ does not run pretypecheck/prebuild hooks. Prepend \"" + doctorCommand +
124
+ " && \" to your " + hookTargets.join(" and ") + " script" + (hookTargets.length > 1 ? "s" : "") +
125
+ " to keep the doctor gate (this tool does not edit them).",
126
+ );
127
+ }
128
+ return result;
129
+ }
@@ -0,0 +1,71 @@
1
+ import { relativeTo } from "./paths.mjs";
2
+
3
+ // Tailwind utilities that take a color. A class such as hover:bg-eds-bg-brand-primary/80 is one token here. Token names keep
4
+ // their case (stroke-focusRing), so a miscased class is reported instead of skipped.
5
+ const colorUtility = "(?:text|bg|border|ring|outline|fill|stroke|decoration|divide|from|to|via|accent|caret|shadow|placeholder|inset-ring)";
6
+ const variantPrefix = "(?:[a-z0-9-]+:)*";
7
+ const boundaryBefore = "(?<![\\w-])";
8
+ const boundaryAfter = "(?![\\w-])";
9
+ const opacity = "(?:/[0-9]{1,3})?";
10
+
11
+ const edsColorClass = new RegExp(boundaryBefore + variantPrefix + colorUtility + "-eds-([A-Za-z0-9-]+)" + opacity + boundaryAfter, "g");
12
+ const edsTypographyClass = new RegExp(boundaryBefore + variantPrefix + "eds-typography-([A-Za-z0-9-]+)" + boundaryAfter, "g");
13
+ const rawColorClass = new RegExp(
14
+ boundaryBefore + variantPrefix + colorUtility +
15
+ "-(?:(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]{2,3}|white|black|\\[(?:#|rgba?\\(|hsla?\\(|oklch\\(|oklab\\(|color\\()[^\\]]*\\])" +
16
+ opacity + boundaryAfter,
17
+ "g",
18
+ );
19
+
20
+ // The public class names are derived exactly like eds sync derives the Tailwind aliases from tokens.json.
21
+ export function publicClassNames(manifest) {
22
+ const colors = new Set();
23
+ const typography = new Set();
24
+ for (const name of Object.keys(manifest.tokens ?? {})) {
25
+ if (name.startsWith("color-")) {
26
+ colors.add("eds-" + name.slice("color-".length));
27
+ continue;
28
+ }
29
+ const match = name.match(/^typography-(.+)-(?:fontFamily|fontWeight|fontSize|lineHeight|letterSpacing)$/);
30
+ if (match) {
31
+ typography.add(match[1]);
32
+ }
33
+ }
34
+ return { colors, typography };
35
+ }
36
+
37
+ // files: [{ path, contents }] as readSources() lists them (the generated eds-tokens.css is not among them).
38
+ // unknown: eds-* classes that no public token backs (always a mistake, Tailwind silently drops them).
39
+ // raw: Tailwind palette, white/black and arbitrary color classes that bypass the EDS tokens (informational).
40
+ export function scanClassNames({ consumerRoot, manifest, files }) {
41
+ const { colors, typography } = publicClassNames(manifest);
42
+ const unknown = [];
43
+ const raw = [];
44
+ for (const file of files) {
45
+ const relativePath = relativeTo(consumerRoot, file.path);
46
+ file.contents.split(/\r?\n/).forEach((line, index) => {
47
+ const location = relativePath + ":" + (index + 1);
48
+ for (const match of line.matchAll(edsColorClass)) {
49
+ // "bg-eds-bg-" + name is composed at runtime; the scan cannot judge it.
50
+ if (match[1].endsWith("-")) {
51
+ continue;
52
+ }
53
+ if (!colors.has("eds-" + match[1])) {
54
+ unknown.push({ location, className: match[0] });
55
+ }
56
+ }
57
+ for (const match of line.matchAll(edsTypographyClass)) {
58
+ if (match[1].endsWith("-")) {
59
+ continue;
60
+ }
61
+ if (!typography.has(match[1])) {
62
+ unknown.push({ location, className: match[0] });
63
+ }
64
+ }
65
+ for (const match of line.matchAll(rawColorClass)) {
66
+ raw.push({ location, className: match[0] });
67
+ }
68
+ });
69
+ }
70
+ return { unknown, raw };
71
+ }
@@ -0,0 +1,155 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ consumerConfigField,
5
+ findTailwindRoot,
6
+ findTailwindV3File,
7
+ planCssRootImports,
8
+ planLegacyJsImportCleanup,
9
+ readSources,
10
+ tailwindRootConfigKey,
11
+ tailwindRootEnvironmentVariable,
12
+ tailwindV3Advice,
13
+ } from "./css-root.mjs";
14
+ import { planTailwind } from "./generate-tailwind.mjs";
15
+ import { cliCommand, createExitError, detectPackageManager, generatedCssRelativePath, normalizeEol, readPackageJson, resolvePackage, runnerCommand, toPosix } from "./paths.mjs";
16
+ import { doctorCommand, doctorScriptName, isCompatibleCommand, writeConsumerPackageJson } from "./register-scripts.mjs";
17
+ import { planAgentContext, planClaudeImport, planDeliveryNotes, planGeminiImport } from "./update-agent-context.mjs";
18
+
19
+ export const syncTargets = new Set(["tailwind", "agents"]);
20
+ const skippedWiring = "skipped " + toPosix(generatedCssRelativePath) + " and the CSS imports.";
21
+ export const noTailwindNote =
22
+ "No Tailwind root CSS found (no @import \"tailwindcss\"); " + skippedWiring + " Without Tailwind, import @spectrabrain/eds/styles.css " +
23
+ "in the app entry. To use Tailwind CSS v4, add @import \"tailwindcss\"; to the app's global CSS and run sync again; set " +
24
+ tailwindRootEnvironmentVariable + "=<file> once only when that file is not detected (sync records it in package.json " +
25
+ consumerConfigField + "." + tailwindRootConfigKey + ").";
26
+
27
+ // The line sync and doctor print when there is no Tailwind root: a Tailwind CSS v3 repository gets its own advice.
28
+ export function describeMissingTailwindRoot(plans) {
29
+ return plans.tailwindV3File === null
30
+ ? noTailwindNote
31
+ : "Tailwind CSS v3 detected (@tailwind directives in " + plans.tailwindV3File + "); " + skippedWiring + " " + tailwindV3Advice;
32
+ }
33
+
34
+ export function collectPlans({ consumerRoot, env = process.env }) {
35
+ const pkg = resolvePackage(consumerRoot);
36
+ const packageManager = detectPackageManager(consumerRoot, env);
37
+ const sources = readSources(consumerRoot);
38
+ const root = findTailwindRoot(consumerRoot, env, sources, packageManager);
39
+ // No Tailwind root: the Tailwind wiring is skipped entirely (no generated CSS, no root edits).
40
+ const tailwind = root === null ? null : planTailwind({ consumerRoot, pkg });
41
+ const cssRoot = root === null ? null : planCssRootImports({ consumerRoot, root, sources });
42
+ const legacyImports = root === null ? [] : planLegacyJsImportCleanup({ consumerRoot });
43
+ const tailwindV3File = root === null ? findTailwindV3File(consumerRoot, sources) : null;
44
+ const errors = [];
45
+ let agents = null;
46
+ try {
47
+ agents = planAgentContext({ consumerRoot, pkg, packageManager });
48
+ } catch (error) {
49
+ errors.push(error);
50
+ }
51
+ const claude = planClaudeImport({ consumerRoot });
52
+ const gemini = planGeminiImport({ consumerRoot });
53
+ const deliveryNotes = planDeliveryNotes({ consumerRoot, agents, packageManager });
54
+ return { consumerRoot, pkg, packageManager, sources, root, tailwindV3File, tailwind, cssRoot, legacyImports, agents, claude, gemini, deliveryNotes, errors };
55
+ }
56
+
57
+ export function planSync({ consumerRoot, env = process.env, only = null }) {
58
+ if (only !== null && !syncTargets.has(only)) {
59
+ throw createExitError("Unknown --only target: " + only + " (use tailwind or agents).", 2);
60
+ }
61
+ const plans = collectPlans({ consumerRoot, env });
62
+ if (plans.errors.length > 0) {
63
+ // Same wording as the doctor line, so the CONSUMER_AGENT.md table applies to both.
64
+ plans.errors[0].message = "invalid: " + plans.errors[0].message;
65
+ throw plans.errors[0];
66
+ }
67
+
68
+ const writes = [];
69
+ if ((only === null || only === "tailwind") && plans.root !== null) {
70
+ writes.push(plans.tailwind, plans.cssRoot, ...plans.legacyImports);
71
+ }
72
+ if (only === null || only === "agents") {
73
+ writes.push(plans.agents);
74
+ if (plans.claude) {
75
+ writes.push(plans.claude);
76
+ }
77
+ if (plans.gemini) {
78
+ writes.push(plans.gemini);
79
+ }
80
+ }
81
+ // Like doctor, an EOL-only difference (an autocrlf checkout of the LF generated CSS) is already synced.
82
+ return { ...plans, only, writes: writes.filter((write) => normalizeEol(write.before) !== normalizeEol(write.after)) };
83
+ }
84
+
85
+ // EDS_TAILWIND_ROOT is a one-time choice: sync records it as package.json "eds": { "tailwindRoot" } so
86
+ // doctor and the pretypecheck/prebuild hooks resolve the same root without the environment variable.
87
+ function persistTailwindRoot(plan, { dryRun, log }) {
88
+ if (plan.root?.source !== "env" || plan.only === "agents") {
89
+ return false;
90
+ }
91
+ const packagePath = path.join(plan.consumerRoot, "package.json");
92
+ const source = fs.readFileSync(packagePath, "utf8");
93
+ const consumerPackage = JSON.parse(source);
94
+ const existing = consumerPackage[consumerConfigField];
95
+ if (existing !== undefined && (typeof existing !== "object" || existing === null || Array.isArray(existing))) {
96
+ log("[EDS] package.json \"" + consumerConfigField + "\" is not an object, so " + tailwindRootEnvironmentVariable + " was not recorded. " +
97
+ "With several Tailwind CSS candidates, doctor and the pretypecheck/prebuild hooks exit 2 again. Check what uses the existing \"" +
98
+ consumerConfigField + "\" value and move it so \"" + consumerConfigField + "\" can be an object, or set " + tailwindRootEnvironmentVariable + " on every run.");
99
+ return false;
100
+ }
101
+ const value = plan.root.relativePath;
102
+ const label = consumerConfigField + "." + tailwindRootConfigKey + " = " + JSON.stringify(value);
103
+ if (existing?.[tailwindRootConfigKey] === value) {
104
+ return false;
105
+ }
106
+ if (dryRun) {
107
+ log("[EDS] would write " + label + " to package.json");
108
+ return false;
109
+ }
110
+ consumerPackage[consumerConfigField] = { ...(existing ?? {}), [tailwindRootConfigKey]: value };
111
+ writeConsumerPackageJson(packagePath, source, consumerPackage);
112
+ log("[EDS] wrote " + label + " to package.json; doctor, sync and the hooks now use this Tailwind root without " + tailwindRootEnvironmentVariable + ".");
113
+ return true;
114
+ }
115
+
116
+ export function applySync(plan, { dryRun = false, log = console.log } = {}) {
117
+ const version = "@spectrabrain/eds@" + plan.pkg.version;
118
+ if (plan.root === null && plan.only !== "agents") {
119
+ log("[EDS] " + describeMissingTailwindRoot(plan));
120
+ } else if (plan.root?.warning) {
121
+ log("[EDS] " + plan.root.warning);
122
+ }
123
+ persistTailwindRoot(plan, { dryRun, log });
124
+ for (const note of plan.deliveryNotes) {
125
+ log("[EDS] note: " + note);
126
+ }
127
+ if (plan.writes.length === 0) {
128
+ log("[EDS] Already synced with " + version + ".");
129
+ return [];
130
+ }
131
+ for (const write of plan.writes) {
132
+ if (dryRun) {
133
+ log("[EDS] would write " + write.relativePath);
134
+ continue;
135
+ }
136
+ fs.mkdirSync(path.dirname(write.path), { recursive: true });
137
+ fs.writeFileSync(write.path, write.after);
138
+ log("[EDS] wrote " + write.relativePath);
139
+ }
140
+ if (!dryRun) {
141
+ // Like doctor's footer: the eds:doctor script only when it runs eds doctor, the CLI otherwise.
142
+ const scripts = readPackageJson(plan.consumerRoot)?.scripts ?? {};
143
+ const doctorRun = isCompatibleCommand(scripts[doctorScriptName], doctorCommand)
144
+ ? runnerCommand(plan.packageManager, doctorScriptName)
145
+ : cliCommand(plan.packageManager, "doctor");
146
+ log("[EDS] Synced with " + version + ". Include the files written above in your change, then run " + doctorRun + ".");
147
+ }
148
+ return plan.writes.map((write) => write.relativePath);
149
+ }
150
+
151
+ export function runSync({ consumerRoot, env = process.env, only = null, dryRun = false, log = console.log }) {
152
+ const plan = planSync({ consumerRoot, env, only });
153
+ applySync(plan, { dryRun, log });
154
+ return 0;
155
+ }
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { applyBlock, locateBlock, maskFencedBlocks, readBlockVersion, readTemplateBlock, renderBlock } from "./managed-block.mjs";
4
+ import { detectEol, findGitRoot, readTextIfExists, realpathOrNull, relativeTo, toPosix, withEol } from "./paths.mjs";
5
+
6
+ export const claudeBeginMarker = "<!-- EDS:CLAUDE:BEGIN -->";
7
+ export const claudeEndMarker = "<!-- EDS:CLAUDE:END -->";
8
+ export const claudeImportLine = "@AGENTS.md";
9
+ export const claudeNestedImportLine = "@../AGENTS.md";
10
+ export const geminiBeginMarker = "<!-- EDS:GEMINI:BEGIN -->";
11
+ export const geminiEndMarker = "<!-- EDS:GEMINI:END -->";
12
+ // Gemini CLI documents its import syntax with an explicit relative path.
13
+ export const geminiImportLine = "@./AGENTS.md";
14
+
15
+ // Claude Code and Gemini CLI accept `@AGENTS.md`, `@./AGENTS.md` and (from .claude/) `@../AGENTS.md` and ignore fenced
16
+ // code blocks.
17
+ const importPattern = /^[ \t]*@(?:\.\.?\/)?AGENTS\.md[ \t]*$/m;
18
+ const endMarkerText = "<!-- EDS:END -->";
19
+
20
+ export function planAgentContext({ consumerRoot, pkg, packageManager = null }) {
21
+ const filePath = path.join(consumerRoot, "AGENTS.md");
22
+ const before = readTextIfExists(filePath);
23
+ // The block is rendered with the target file's dominant line ending; a new file uses LF.
24
+ const eol = before === null ? "\n" : detectEol(before);
25
+ const block = withEol(renderBlock(readTemplateBlock(pkg.packageRoot), pkg.version, packageManager), eol);
26
+ const located = before === null ? null : locateBlock(before);
27
+ return {
28
+ label: "agents",
29
+ path: filePath,
30
+ relativePath: relativeTo(consumerRoot, filePath),
31
+ before,
32
+ after: applyBlock(before, block, eol),
33
+ block,
34
+ existingBlock: located?.block ?? null,
35
+ existingVersion: located ? readBlockVersion(located.block) : null,
36
+ };
37
+ }
38
+
39
+ function hasImport(contents, beginMarker, endMarker) {
40
+ const visible = maskFencedBlocks(contents);
41
+ if (visible.includes(beginMarker) && visible.includes(endMarker)) {
42
+ return true;
43
+ }
44
+ return importPattern.test(visible);
45
+ }
46
+
47
+ // Returns null when the file does not exist, or when it and AGENTS.md are the same file (a symlink in either
48
+ // direction): the managed block already lives in that file and an import would point at itself.
49
+ function planImportFile({ consumerRoot, fileName, label, beginMarker, endMarker, importLine }) {
50
+ const filePath = path.join(consumerRoot, fileName);
51
+ const before = readTextIfExists(filePath);
52
+ if (before === null) {
53
+ return null;
54
+ }
55
+ const realPath = realpathOrNull(filePath);
56
+ const agentsRealPath = realpathOrNull(path.join(consumerRoot, "AGENTS.md"));
57
+ if (realPath !== null && realPath === agentsRealPath) {
58
+ return null;
59
+ }
60
+ const eol = detectEol(before);
61
+ const block = [beginMarker, importLine, endMarker].join(eol);
62
+ const trimmed = before.replace(/\s*$/, "");
63
+ const after = hasImport(before, beginMarker, endMarker)
64
+ ? before
65
+ : (trimmed ? trimmed + eol + eol + block + eol : block + eol);
66
+ return { label, path: filePath, relativePath: relativeTo(consumerRoot, filePath), importLine, before, after };
67
+ }
68
+
69
+ // Claude Code reads CLAUDE.md or .claude/CLAUDE.md instead of AGENTS.md when either exists, so the import goes into
70
+ // whichever the consumer has (CLAUDE.md first). Imports resolve relative to the importing file.
71
+ export function planClaudeImport({ consumerRoot }) {
72
+ const nested = !fs.existsSync(path.join(consumerRoot, "CLAUDE.md")) && fs.existsSync(path.join(consumerRoot, ".claude", "CLAUDE.md"));
73
+ return planImportFile({
74
+ consumerRoot,
75
+ fileName: nested ? path.join(".claude", "CLAUDE.md") : "CLAUDE.md",
76
+ label: "claude",
77
+ beginMarker: claudeBeginMarker,
78
+ endMarker: claudeEndMarker,
79
+ importLine: nested ? claudeNestedImportLine : claudeImportLine,
80
+ });
81
+ }
82
+
83
+ // Gemini CLI reads GEMINI.md by default and AGENTS.md only with a context.fileName setting, so it gets the same import.
84
+ export function planGeminiImport({ consumerRoot }) {
85
+ return planImportFile({
86
+ consumerRoot,
87
+ fileName: "GEMINI.md",
88
+ label: "gemini",
89
+ beginMarker: geminiBeginMarker,
90
+ endMarker: geminiEndMarker,
91
+ importLine: geminiImportLine,
92
+ });
93
+ }
94
+
95
+ const codexBudgetBytes = 32 * 1024;
96
+
97
+ // Where the block may not reach an agent even though AGENTS.md is synced. Notes never change an exit code.
98
+ export function planDeliveryNotes({ consumerRoot, agents, packageManager = null }) {
99
+ const notes = [];
100
+ if (packageManager?.pnp) {
101
+ notes.push(
102
+ "Yarn Plug'n'Play detected: the EDS docs the AGENTS.md block points to are not in node_modules/@spectrabrain/eds, " +
103
+ "and EDS support for Plug'n'Play is undecided. Ask the repository owner before changing nodeLinker.",
104
+ );
105
+ }
106
+ const exists = (relativePath) => fs.existsSync(path.join(consumerRoot, relativePath));
107
+ const hasClaude = exists("CLAUDE.md") || exists(path.join(".claude", "CLAUDE.md"));
108
+ if (!hasClaude && exists("CLAUDE.local.md")) {
109
+ notes.push("CLAUDE.local.md exists but CLAUDE.md does not, so Claude Code reads CLAUDE.local.md instead of AGENTS.md; add a CLAUDE.md and run sync again (sync adds the import).");
110
+ } else {
111
+ // Claude Code is named whenever it has no file. Gemini CLI only rides along then: a repository that set up
112
+ // CLAUDE.md and no GEMINI.md most likely does not use Gemini, and a note on every run would be noise.
113
+ const missing = hasClaude ? [] : ["Claude Code (CLAUDE.md)", ...(exists("GEMINI.md") ? [] : ["Gemini CLI (GEMINI.md)"])];
114
+ if (missing.length > 0) {
115
+ notes.push(
116
+ "no instruction file for " + missing.join(" or ") + " here, so " + (missing.length > 1 ? "they" : "it") +
117
+ " may not load the EDS block in AGENTS.md (Codex reads AGENTS.md). If the team uses " + (missing.length > 1 ? "them" : "it") +
118
+ ", create that file and run sync again; sync adds the import.",
119
+ );
120
+ }
121
+ }
122
+ if (exists("AGENTS.override.md")) {
123
+ notes.push("AGENTS.override.md exists; Codex reads it instead of AGENTS.md, so copy or import the EDS block there.");
124
+ }
125
+ const endIndex = agents?.after?.indexOf(endMarkerText) ?? -1;
126
+ if (endIndex >= 0 && Buffer.byteLength(agents.after.slice(0, endIndex), "utf8") > codexBudgetBytes) {
127
+ notes.push("the EDS block in AGENTS.md ends after the first 32 KiB, where Codex truncates by default; move the block toward the top (sync keeps it where it is).");
128
+ }
129
+ const gitRoot = findGitRoot(consumerRoot);
130
+ const packageRelative = gitRoot === null ? "" : toPosix(path.relative(gitRoot, consumerRoot));
131
+ if (packageRelative !== "") {
132
+ const pointer = packageRelative + "/AGENTS.md";
133
+ const rootMentions = ["AGENTS.md", "CLAUDE.md"].some((fileName) => (readTextIfExists(path.join(gitRoot, fileName)) ?? "").includes(pointer));
134
+ if (!rootMentions) {
135
+ notes.push(
136
+ "this package is " + packageRelative + " inside the repository; agents started at the repository root do not load " + pointer +
137
+ " by default. Add to the root AGENTS.md or CLAUDE.md: \"Before working in " + packageRelative + ", read " + pointer +
138
+ " and run its commands from " + packageRelative + ".\"",
139
+ );
140
+ }
141
+ }
142
+ return notes;
143
+ }
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { registerConsumerScripts } from "./consumer/register-scripts.mjs";
5
+
6
+ const packageRoot = path.dirname(fileURLToPath(import.meta.url));
7
+ const consumerRoot = process.env.INIT_CWD;
8
+
9
+ if (!consumerRoot || path.resolve(consumerRoot) === packageRoot) {
10
+ process.exit(0);
11
+ }
12
+
13
+ const consumerPackagePath = path.join(consumerRoot, "package.json");
14
+ if (!fs.existsSync(consumerPackagePath)) {
15
+ console.log("[EDS] Consumer package.json was not found; run npx @spectrabrain/eds init in the package that depends on @spectrabrain/eds.");
16
+ process.exit(0);
17
+ }
18
+
19
+ function realpathOrNull(filePath) {
20
+ try {
21
+ return fs.realpathSync(filePath);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ const installedPath = realpathOrNull(path.join(consumerRoot, "node_modules", "@spectrabrain", "eds"));
28
+ if (installedPath === null || installedPath !== realpathOrNull(packageRoot)) {
29
+ console.log("[EDS] Skipping consumer setup: this install is not the consumer's node_modules/@spectrabrain/eds. Run npx @spectrabrain/eds init in the package that depends on @spectrabrain/eds.");
30
+ process.exit(0);
31
+ }
32
+
33
+ // pnpm rewrites the consumer package.json after dependency lifecycle scripts, so edits made here are lost.
34
+ if (String(process.env.npm_config_user_agent ?? "").startsWith("pnpm/")) {
35
+ console.log("[EDS] pnpm discards package.json edits made during install; run pnpm exec eds init to register the EDS scripts.");
36
+ process.exit(0);
37
+ }
38
+
39
+ let consumerPackage;
40
+ try {
41
+ consumerPackage = JSON.parse(fs.readFileSync(consumerPackagePath, "utf8"));
42
+ } catch (error) {
43
+ console.log("[EDS] Consumer package.json could not be parsed (" + error.message + "); run npx @spectrabrain/eds init after fixing it.");
44
+ process.exit(0);
45
+ }
46
+
47
+ // A workspaces root hoists the package but is not the app that uses it: registering the scripts and hooks
48
+ // there would make the root build look for src/styles/eds-tokens.css and AGENTS.md in the wrong place.
49
+ if (consumerPackage.workspaces !== undefined) {
50
+ console.log("[EDS] Skipping consumer setup: package.json declares workspaces. Run npx @spectrabrain/eds init inside the app package that depends on @spectrabrain/eds.");
51
+ process.exit(0);
52
+ }
53
+
54
+ // The install must never fail because of this setup step; anything unexpected is reported and left to eds doctor.
55
+ try {
56
+ const result = registerConsumerScripts(consumerRoot);
57
+ console.log("[EDS] Run " + result.syncRunCommand + " to finish setup.");
58
+ } catch (error) {
59
+ console.log("[EDS] Consumer setup was skipped (" + (error instanceof Error ? error.message : String(error)) + "). Run npx @spectrabrain/eds init to register the EDS scripts.");
60
+ }
package/dist/base.css ADDED
@@ -0,0 +1,4 @@
1
+ /* This file is generated by scripts/build-tokens.ts. Do not edit directly. */
2
+ :where(html) {
3
+ font-family: var(--eds-typography-body1-fontFamily);
4
+ }