synthesisui 0.14.0 → 0.16.0
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/dist/claude-md.js +53 -8
- package/dist/commands/adopt.js +207 -0
- package/dist/commands/doctor.js +31 -6
- package/dist/doctor/tokens.js +3 -1
- package/dist/index.js +10 -0
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -18,7 +18,12 @@ async function readInstalled(projectRoot) {
|
|
|
18
18
|
try {
|
|
19
19
|
const raw = await readFile(join(dsDir, slug, ".lock"), "utf8");
|
|
20
20
|
const lock = JSON.parse(raw);
|
|
21
|
-
locks.push({
|
|
21
|
+
locks.push({
|
|
22
|
+
slug: lock.slug,
|
|
23
|
+
name: lock.name,
|
|
24
|
+
version: lock.version,
|
|
25
|
+
adopted: lock.adopted === true,
|
|
26
|
+
});
|
|
22
27
|
}
|
|
23
28
|
catch {
|
|
24
29
|
// folder without a valid .lock - ignore
|
|
@@ -81,30 +86,70 @@ async function readManifest(projectRoot, ds) {
|
|
|
81
86
|
return null;
|
|
82
87
|
}
|
|
83
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The loop that closes itself, and the one paragraph both kinds of system share.
|
|
91
|
+
*
|
|
92
|
+
* The contract said what to write and never said to check, so verification
|
|
93
|
+
* happened days later, by a human, across a whole repo - archaeology rather
|
|
94
|
+
* than a fix. `doctor <file> --verbose` already existed; nobody was pointing
|
|
95
|
+
* the agent at it.
|
|
96
|
+
*
|
|
97
|
+
* Held in one constant on purpose: the same paragraph typed into two template
|
|
98
|
+
* literals is two paragraphs that drift.
|
|
99
|
+
*/
|
|
100
|
+
const SELF_CHECK = `
|
|
101
|
+
|
|
102
|
+
**After writing or editing any UI file, check it before moving on:**
|
|
103
|
+
|
|
104
|
+
npx synthesisui@latest doctor <the file you just wrote> --verbose
|
|
105
|
+
|
|
106
|
+
It names the token for every hardcoded value it finds. Fix what it names, run it
|
|
107
|
+
again, and only then move to the next thing. Do not batch this up for the end -
|
|
108
|
+
a component checked while you still have its context is a two-line fix; the same
|
|
109
|
+
component found in a repo-wide audit next week is an archaeology exercise.
|
|
110
|
+
|
|
111
|
+
If it reports a value with no token, do NOT invent one. Say which value and what
|
|
112
|
+
you would call it, and let a person decide.`;
|
|
84
113
|
async function renderRegion(projectRoot, installed) {
|
|
85
114
|
if (installed.length === 0) {
|
|
86
115
|
return `${START}\n${END}`;
|
|
87
116
|
}
|
|
88
117
|
const sections = [];
|
|
89
118
|
for (const ds of installed) {
|
|
90
|
-
|
|
119
|
+
// An adopted system has no published version and therefore no `v<n>/`
|
|
120
|
+
// folder - pointing the agent at one points it at a file that does not
|
|
121
|
+
// exist (caught by running it, 27/07).
|
|
122
|
+
const head = ds.adopted
|
|
123
|
+
? `- **${ds.name}** (\`${ds.slug}\`, adopted from this repo) - guide: \`_synthesisui/ds/${ds.slug}/GUIDE.md\``
|
|
124
|
+
: `- **${ds.name}** (\`${ds.slug}\`, v${ds.version}) - guide: \`_synthesisui/ds/${ds.slug}/v${ds.version}/GUIDE.md\``;
|
|
91
125
|
const manifest = await readManifest(projectRoot, ds);
|
|
92
126
|
sections.push(manifest ? `${head}\n${manifest}` : head);
|
|
93
127
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
// Two truths, and asserting the wrong one misleads the agent every time it
|
|
129
|
+
// writes a line. An INSTALLED system speaks `--ds-*` and is scoped with
|
|
130
|
+
// `data-ds`; an ADOPTED one is the project's own vocabulary, already wired,
|
|
131
|
+
// with no `data-ds` anywhere to scope to.
|
|
132
|
+
const onlyAdopted = installed.every((d) => d.adopted);
|
|
133
|
+
const rule = onlyAdopted
|
|
134
|
+
? `**When creating or editing components, read the system's GUIDE.md and follow it:** use the
|
|
135
|
+
project's OWN custom properties, exactly as the guide lists them. Do not write raw colours,
|
|
136
|
+
spacings or radii that a token already covers, and do not invent a new token silently - say so
|
|
137
|
+
instead, because a new token is a decision for a person to make. There is no component
|
|
138
|
+
manifest for an adopted system - the tokens ARE the contract.${SELF_CHECK}`
|
|
139
|
+
: `**When creating or editing components, read the system's GUIDE.md and follow it:** use only semantic tokens
|
|
98
140
|
(\`var(--ds-color-semantic-*)\`, \`--ds-spacing-*\`, etc.), scope the UI with \`data-ds="<slug>"\`,
|
|
99
141
|
and reuse the \`.ds-*\` classes. Do not use raw values outside the system's scale. **Before
|
|
100
142
|
creating any UI element, check the component manifest below - if it exists, use or extend
|
|
101
143
|
it (\`synthesisui component <slug> <name>\` materializes it as your code).** To review a
|
|
102
144
|
component, create an isolated sample page (e.g. \`app/synthesisui-samples/<component>/\`) - do not
|
|
103
|
-
apply it to real production pages unless asked
|
|
145
|
+
apply it to real production pages unless asked.${SELF_CHECK}`;
|
|
146
|
+
const body = `## Design Systems (via SynthesisUI)
|
|
147
|
+
|
|
148
|
+
This project uses design system(s) tracked by the \`synthesisui\` CLI. ${rule}
|
|
104
149
|
|
|
105
150
|
${sections.join("\n")}
|
|
106
151
|
|
|
107
|
-
_Block managed by the CLI - do not edit by hand; run \`synthesisui add <slug
|
|
152
|
+
_Block managed by the CLI - do not edit by hand; run \`synthesisui ${onlyAdopted ? "adopt --write" : "add <slug>"}\` to update._`;
|
|
108
153
|
return `${START}\n${body}\n${END}`;
|
|
109
154
|
}
|
|
110
155
|
/**
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { syncClaudeMd } from "../claude-md.js";
|
|
4
|
+
import { parseRootTokens } from "../doctor/tokens.js";
|
|
5
|
+
import { body, section, snippet } from "../output.js";
|
|
6
|
+
import { walk } from "./doctor.js";
|
|
7
|
+
/**
|
|
8
|
+
* `synthesisui adopt` - the system somebody ALREADY has, made legible.
|
|
9
|
+
*
|
|
10
|
+
* The whole product assumed you start from nothing. `init` installs OUR tokens
|
|
11
|
+
* and therefore needs four manual edits - import, scope with data-ds, wire
|
|
12
|
+
* fonts - which is where people give up (walked 27/07, the next run reads 0%).
|
|
13
|
+
*
|
|
14
|
+
* The reader who feels our pitch hardest does not need any of that. Their
|
|
15
|
+
* `--acme-*` tokens are already in `:root` and already painting the screen.
|
|
16
|
+
* What is missing is not code, it is the CONTRACT: the file their coding agent
|
|
17
|
+
* reads before writing UI.
|
|
18
|
+
*
|
|
19
|
+
* So this command writes NO CSS, and that is the point rather than a
|
|
20
|
+
* limitation:
|
|
21
|
+
*
|
|
22
|
+
* init writes CSS → needs wiring → 4 edits → 0% if one is wrong
|
|
23
|
+
* adopt writes none → no wiring → works on the first command
|
|
24
|
+
*
|
|
25
|
+
* THE PROMISE, printed rather than buried in docs: it reads your code and
|
|
26
|
+
* rewrites none of it; everything it creates lives in `_synthesisui/` and
|
|
27
|
+
* leaves with `clean`; your tokens keep your names.
|
|
28
|
+
*
|
|
29
|
+
* Dry by default. A command that edits somebody's repository on first run is
|
|
30
|
+
* not a command that people with a system in production ever try.
|
|
31
|
+
*/
|
|
32
|
+
const SCOPE_HINT = ":root, html, :host and @theme";
|
|
33
|
+
/** Group by the prefix an author actually used, which is how they think of it. */
|
|
34
|
+
function byKind(tokens) {
|
|
35
|
+
const kinds = new Map();
|
|
36
|
+
const guess = (n) => /color|colour|bg|background|fg|foreground|surface|canvas|ink|brand/.test(n)
|
|
37
|
+
? "colour"
|
|
38
|
+
: /space|spacing|gap|inset/.test(n)
|
|
39
|
+
? "spacing"
|
|
40
|
+
: /radius|round|corner/.test(n)
|
|
41
|
+
? "radius"
|
|
42
|
+
: /font|type|text|leading|tracking|weight/.test(n)
|
|
43
|
+
? "type"
|
|
44
|
+
: /shadow|elevation/.test(n)
|
|
45
|
+
? "shadow"
|
|
46
|
+
: /duration|ease|motion|transition/.test(n)
|
|
47
|
+
? "motion"
|
|
48
|
+
: "other";
|
|
49
|
+
for (const name of tokens.keys()) {
|
|
50
|
+
const k = guess(name);
|
|
51
|
+
kinds.set(k, (kinds.get(k) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
return [...kinds.entries()].sort((a, b) => b[1] - a[1]);
|
|
54
|
+
}
|
|
55
|
+
/** The commonest `--<prefix>-` in their own names - the slug they already use. */
|
|
56
|
+
function guessSlug(tokens, pkgName) {
|
|
57
|
+
const counts = new Map();
|
|
58
|
+
for (const n of tokens.keys()) {
|
|
59
|
+
const m = /^--([a-z0-9]+)-/.exec(n);
|
|
60
|
+
if (m)
|
|
61
|
+
counts.set(m[1], (counts.get(m[1]) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
64
|
+
// Only trust the prefix when it actually dominates; a repo whose tokens are
|
|
65
|
+
// `--color-*`, `--space-*` has no brand prefix and the package name is the
|
|
66
|
+
// better answer.
|
|
67
|
+
if (top &&
|
|
68
|
+
top[1] >= tokens.size * 0.5 &&
|
|
69
|
+
!/^(color|space|font|size)$/.test(top[0])) {
|
|
70
|
+
return top[0];
|
|
71
|
+
}
|
|
72
|
+
return (pkgName
|
|
73
|
+
.replace(/^@[^/]+\//, "")
|
|
74
|
+
.replace(/[^a-z0-9]+/gi, "-")
|
|
75
|
+
.replace(/^-|-$/g, "")
|
|
76
|
+
.toLowerCase() || "my-system");
|
|
77
|
+
}
|
|
78
|
+
async function findTokens(root, only) {
|
|
79
|
+
let files = 0;
|
|
80
|
+
const from = [];
|
|
81
|
+
const all = new Map();
|
|
82
|
+
const take = async (file) => {
|
|
83
|
+
const css = await readFile(file, "utf8").catch(() => "");
|
|
84
|
+
if (!css)
|
|
85
|
+
return;
|
|
86
|
+
files++;
|
|
87
|
+
const found = parseRootTokens(css);
|
|
88
|
+
if (found.size === 0)
|
|
89
|
+
return;
|
|
90
|
+
from.push(`${relative(root, file)} (${found.size})`);
|
|
91
|
+
for (const [k, v] of found)
|
|
92
|
+
if (!all.has(k))
|
|
93
|
+
all.set(k, v);
|
|
94
|
+
};
|
|
95
|
+
if (only) {
|
|
96
|
+
await take(resolve(root, only));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
for await (const f of walk(root)) {
|
|
100
|
+
if (/\.(css|scss|sass|less)$/i.test(f))
|
|
101
|
+
await take(f);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { tokens: all, files, from };
|
|
105
|
+
}
|
|
106
|
+
/** The contract. Names THEIR tokens - there is no translation layer. */
|
|
107
|
+
function guide(name, slug, tokens) {
|
|
108
|
+
const lines = [...tokens.entries()].map(([k, v]) => ` ${k}: ${v};`);
|
|
109
|
+
return `# ${name} - the contract your agent follows
|
|
110
|
+
|
|
111
|
+
Adopted from this repository by \`synthesisui adopt\`. These are YOUR tokens,
|
|
112
|
+
under YOUR names - nothing was renamed and no stylesheet was rewritten.
|
|
113
|
+
|
|
114
|
+
## The rule
|
|
115
|
+
|
|
116
|
+
When writing or editing UI in this project, use these custom properties.
|
|
117
|
+
Do not write raw colours, spacings or radii that a token already covers.
|
|
118
|
+
|
|
119
|
+
✗ background: #3b82f6
|
|
120
|
+
✓ background: var(--${slug}-color-primary)
|
|
121
|
+
|
|
122
|
+
If a value has no token, say so instead of inventing one silently - a new
|
|
123
|
+
token is a decision for a person to make.
|
|
124
|
+
|
|
125
|
+
## The tokens (${tokens.size})
|
|
126
|
+
|
|
127
|
+
\`\`\`css
|
|
128
|
+
:root {
|
|
129
|
+
${lines.join("\n")}
|
|
130
|
+
}
|
|
131
|
+
\`\`\`
|
|
132
|
+
|
|
133
|
+
## Checking your work
|
|
134
|
+
|
|
135
|
+
npx synthesisui@latest doctor
|
|
136
|
+
|
|
137
|
+
Reports how much of the shipped UI resolves to these tokens, and names the
|
|
138
|
+
token for any hardcoded value that already has one.
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
export async function adopt(opts) {
|
|
142
|
+
const root = resolve(opts.dir ?? process.cwd());
|
|
143
|
+
const pkg = await readFile(join(root, "package.json"), "utf8")
|
|
144
|
+
.then((r) => JSON.parse(r))
|
|
145
|
+
.catch(() => ({ name: undefined }));
|
|
146
|
+
const found = await findTokens(root, opts.tokens);
|
|
147
|
+
console.log(section("Adopt"));
|
|
148
|
+
if (found.tokens.size === 0) {
|
|
149
|
+
// Never "failed to adopt". Always: what was looked for, where, and the
|
|
150
|
+
// next thing to type. Being stuck with no move is the worst outcome.
|
|
151
|
+
console.log(body(`Looked in ${SCOPE_HINT} across ${found.files} stylesheet${found.files === 1 ? "" : "s"} and found no custom properties.`));
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(body("If your tokens live somewhere this did not reach:"));
|
|
154
|
+
console.log(snippet(["npx synthesisui@latest adopt --tokens <path/to.css>"]));
|
|
155
|
+
console.log("");
|
|
156
|
+
console.log(body("If you do not have a system yet, start from one:"));
|
|
157
|
+
console.log(body("https://www.synthesisui.com/gallery"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const slug = opts.slug ?? guessSlug(found.tokens, pkg.name ?? "");
|
|
161
|
+
const name = (pkg.name ?? slug).replace(/^@[^/]+\//, "");
|
|
162
|
+
const dir = join(root, "_synthesisui", "ds", slug);
|
|
163
|
+
console.log(body(`Reading your system … ${found.files} stylesheet${found.files === 1 ? "" : "s"}, ${found.tokens.size} tokens found`));
|
|
164
|
+
console.log("");
|
|
165
|
+
for (const [kind, n] of byKind(found.tokens)) {
|
|
166
|
+
console.log(body(` ${kind.padEnd(10)} ${String(n).padStart(3)}`));
|
|
167
|
+
}
|
|
168
|
+
console.log("");
|
|
169
|
+
for (const f of found.from.slice(0, 4))
|
|
170
|
+
console.log(body(` from ${f}`));
|
|
171
|
+
if (found.from.length > 4)
|
|
172
|
+
console.log(body(` and ${found.from.length - 4} more`));
|
|
173
|
+
if (!opts.write) {
|
|
174
|
+
console.log("");
|
|
175
|
+
console.log(body("Would create (nothing written yet):"));
|
|
176
|
+
console.log(body(` _synthesisui/ds/${slug}/system.json your system, described`));
|
|
177
|
+
console.log(body(` _synthesisui/ds/${slug}/GUIDE.md what your agent reads`));
|
|
178
|
+
console.log(body(" CLAUDE.md a managed block pointing at it"));
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(body("Your stylesheets are not touched. Nothing outside"));
|
|
181
|
+
console.log(body("_synthesisui/ is written, and `clean` removes all of it."));
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log(snippet([
|
|
184
|
+
`npx synthesisui@latest adopt --write${opts.slug ? ` --slug ${slug}` : ""}`,
|
|
185
|
+
]));
|
|
186
|
+
console.log(body(`writes it as "${slug}" - pass --slug to name it differently`));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await mkdir(dir, { recursive: true });
|
|
190
|
+
await writeFile(join(dir, "system.json"), `${JSON.stringify({
|
|
191
|
+
meta: { name, slug, adopted: true, source: "repository" },
|
|
192
|
+
tokens: Object.fromEntries(found.tokens),
|
|
193
|
+
}, null, 2)}\n`, "utf8");
|
|
194
|
+
await writeFile(join(dir, "GUIDE.md"), guide(name, slug, found.tokens), "utf8");
|
|
195
|
+
// `version: 0` marks a system nobody published - the doctor and CLAUDE.md
|
|
196
|
+
// read this to know it is adopted rather than installed.
|
|
197
|
+
await writeFile(join(dir, ".lock"), `${JSON.stringify({ slug, name, version: 0, adopted: true }, null, 2)}\n`, "utf8");
|
|
198
|
+
await syncClaudeMd(root);
|
|
199
|
+
console.log("");
|
|
200
|
+
console.log(body(`✓ _synthesisui/ds/${slug}/ written`));
|
|
201
|
+
console.log(body("✓ CLAUDE.md managed block added"));
|
|
202
|
+
console.log("");
|
|
203
|
+
console.log(body("Your agent now knows your system. Nothing of yours changed."));
|
|
204
|
+
console.log("");
|
|
205
|
+
console.log(snippet(["npx synthesisui@latest doctor"]));
|
|
206
|
+
console.log(body("see what it makes measurable"));
|
|
207
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -40,7 +40,7 @@ const SKIP = new Set([
|
|
|
40
40
|
// Our own installed artifacts are the answer, not the problem.
|
|
41
41
|
"_synthesisui",
|
|
42
42
|
]);
|
|
43
|
-
async function* walk(dir) {
|
|
43
|
+
export async function* walk(dir) {
|
|
44
44
|
// `readdir`'s overloads infer a Buffer-named Dirent without an explicit
|
|
45
45
|
// encoding; naming it keeps `e.name` a string.
|
|
46
46
|
const entries = await readdir(dir, {
|
|
@@ -63,7 +63,7 @@ async function* walk(dir) {
|
|
|
63
63
|
}
|
|
64
64
|
/** The same walk over several roots, in the order the caller named them. A
|
|
65
65
|
* single file passed as a scope is read as itself. */
|
|
66
|
-
async function* walkAll(roots) {
|
|
66
|
+
export async function* walkAll(roots) {
|
|
67
67
|
for (const r of roots) {
|
|
68
68
|
if (EXTS.some((x) => r.endsWith(x))) {
|
|
69
69
|
yield r;
|
|
@@ -91,6 +91,7 @@ async function loadSystem(root) {
|
|
|
91
91
|
// reading them all is both correct and what the running app actually sees.
|
|
92
92
|
let css = "";
|
|
93
93
|
let lock = null;
|
|
94
|
+
let adopted = false;
|
|
94
95
|
const recipes = new Map();
|
|
95
96
|
const documents = [];
|
|
96
97
|
for (const slug of slugs) {
|
|
@@ -118,6 +119,24 @@ async function loadSystem(root) {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
css += `\n${real || root}`;
|
|
122
|
+
// An ADOPTED system has no tokens.css of ours - `adopt` deliberately
|
|
123
|
+
// writes no CSS, because the project's own stylesheet already works. Its
|
|
124
|
+
// vocabulary lives in system.json, and without this the doctor falls
|
|
125
|
+
// through to harvesting and then offers to install a system to somebody
|
|
126
|
+
// who just adopted one (caught running it, 27/07).
|
|
127
|
+
if (mine?.adopted) {
|
|
128
|
+
const raw = await readFile(join(dir, "system.json"), "utf8").catch(() => "");
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
for (const [k, v] of Object.entries(parsed.tokens ?? {})) {
|
|
132
|
+
css += `\n:root { ${k}: ${v}; }`;
|
|
133
|
+
}
|
|
134
|
+
adopted = true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Unreadable system.json costs the naming, not the run.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
121
140
|
const docRaw = mine?.version
|
|
122
141
|
? await readFile(join(dir, `v${mine.version}`, "design-system.json"), "utf8").catch(() => "")
|
|
123
142
|
: "";
|
|
@@ -135,7 +154,11 @@ async function loadSystem(root) {
|
|
|
135
154
|
}
|
|
136
155
|
}
|
|
137
156
|
}
|
|
138
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
table: buildTable({ css, lock, source: adopted ? "adopted" : "installed" }),
|
|
159
|
+
recipes,
|
|
160
|
+
documents,
|
|
161
|
+
};
|
|
139
162
|
}
|
|
140
163
|
/**
|
|
141
164
|
* The project's OWN vocabulary, when we did not put one there.
|
|
@@ -321,9 +344,11 @@ export async function doctor(opts) {
|
|
|
321
344
|
// possible first line.
|
|
322
345
|
table.source === "installed"
|
|
323
346
|
? `${table.name ?? table.slug} v${table.version ?? "?"} - ${plural(table.byName.size, "token")}, ${plural(d.scanned, "file")} read`
|
|
324
|
-
: table.source === "
|
|
325
|
-
?
|
|
326
|
-
:
|
|
347
|
+
: table.source === "adopted"
|
|
348
|
+
? `${table.name ?? table.slug} (adopted) - ${plural(table.byName.size, "token")}, ${plural(d.scanned, "file")} read`
|
|
349
|
+
: table.source === "yours"
|
|
350
|
+
? `Your own tokens - ${plural(table.byName.size, "token")} found, ${plural(d.scanned, "file")} read`
|
|
351
|
+
: `No system installed - ${plural(d.scanned, "file")} read`));
|
|
327
352
|
if (scopes.length > 0) {
|
|
328
353
|
console.log(body(`scope: ${opts.scopes?.join(", ")}`));
|
|
329
354
|
}
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -212,7 +212,9 @@ export function parseRootTokens(css) {
|
|
|
212
212
|
}
|
|
213
213
|
export function buildTable(input) {
|
|
214
214
|
const source = input.source ?? "installed";
|
|
215
|
-
const byName = source === "
|
|
215
|
+
const byName = source === "installed"
|
|
216
|
+
? parseTokens(input.css)
|
|
217
|
+
: parseRootTokens(input.css);
|
|
216
218
|
const byValue = new Map();
|
|
217
219
|
for (const [name, value] of byName) {
|
|
218
220
|
const key = normalizeValue(value);
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { add } from "./commands/add.js";
|
|
3
|
+
import { adopt } from "./commands/adopt.js";
|
|
3
4
|
import { advise } from "./commands/advise.js";
|
|
4
5
|
import { clean } from "./commands/clean.js";
|
|
5
6
|
import { component } from "./commands/component.js";
|
|
@@ -123,6 +124,15 @@ async function main() {
|
|
|
123
124
|
const registry = typeof flags.registry === "string" ? flags.registry : undefined;
|
|
124
125
|
const dir = typeof flags.dir === "string" ? flags.dir : undefined;
|
|
125
126
|
switch (command) {
|
|
127
|
+
case "adopt":
|
|
128
|
+
// Dry by default: `--write` is the only way anything lands on disk.
|
|
129
|
+
await adopt({
|
|
130
|
+
dir,
|
|
131
|
+
write: flags.write === true,
|
|
132
|
+
tokens: typeof flags.tokens === "string" ? flags.tokens : undefined,
|
|
133
|
+
slug: typeof flags.slug === "string" ? flags.slug : undefined,
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
126
136
|
case "doctor":
|
|
127
137
|
// positional paths scope the READING (the system is still found from the
|
|
128
138
|
// root): `doctor apps/web packages/ui` in a monorepo
|
package/package.json
CHANGED