synthesisui 0.2.0 → 0.3.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/commands/add.js +25 -11
- package/dist/commands/component.js +44 -6
- package/dist/commands/upgrade.js +140 -0
- package/dist/index.js +12 -0
- package/dist/output.js +22 -0
- package/dist/registry.js +14 -0
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { syncClaudeMd } from "../claude-md.js";
|
|
4
4
|
import { resolveRegistry } from "../config.js";
|
|
5
5
|
import { buildGuide } from "../guide.js";
|
|
6
|
+
import { body as line, section, snippet } from "../output.js";
|
|
6
7
|
import { fetchDesignSystem } from "../registry.js";
|
|
7
8
|
async function readRootLock(path) {
|
|
8
9
|
try {
|
|
@@ -109,16 +110,29 @@ export async function add(slug, opts) {
|
|
|
109
110
|
console.log(` philosophy.md → ${sections.length} section(s) (read after rules)`);
|
|
110
111
|
}
|
|
111
112
|
console.log(` CLAUDE.md ${claudeMd.created ? "created" : "updated"} (${claudeMd.count} system(s) installed)`);
|
|
112
|
-
|
|
113
|
+
if (opts.setupHints === false)
|
|
114
|
+
return;
|
|
113
115
|
const hasTheme = cssArtifacts.includes("theme.css");
|
|
114
|
-
|
|
115
|
-
console.log("
|
|
116
|
-
console.log("
|
|
117
|
-
console.log(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
116
|
+
// ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
|
|
117
|
+
console.log(section("One-time setup (once per app)"));
|
|
118
|
+
console.log(line("1. Import the system in your GLOBAL stylesheet, e.g. app/globals.css"));
|
|
119
|
+
console.log(line(" (the path is relative to that file - hence the leading ../):"));
|
|
120
|
+
console.log("");
|
|
121
|
+
console.log(snippet(hasTheme
|
|
122
|
+
? [
|
|
123
|
+
`@import "tailwindcss";`,
|
|
124
|
+
`@import "../_synthesisui/ds/${payload.slug}/tokens.css";`,
|
|
125
|
+
`@import "../_synthesisui/ds/${payload.slug}/theme.css"; /* Tailwind utilities on your tokens */`,
|
|
126
|
+
]
|
|
127
|
+
: [`@import "../_synthesisui/ds/${payload.slug}/tokens.css";`]));
|
|
128
|
+
console.log("");
|
|
129
|
+
console.log(line(`2. Scope your app: add data-ds="${payload.slug}" to a ROOT element, e.g. app/layout.tsx:`));
|
|
130
|
+
console.log("");
|
|
131
|
+
console.log(snippet([`<body data-ds="${payload.slug}">{children}</body>`]));
|
|
132
|
+
console.log(section("Next"));
|
|
133
|
+
console.log(line(`synthesisui component ${payload.slug} button bring a component in as YOUR code`));
|
|
134
|
+
console.log(line(`synthesisui template ${payload.slug} landing materialize a whole page`));
|
|
135
|
+
console.log("");
|
|
136
|
+
console.log(line(`Guide for you and your agent: _synthesisui/ds/${payload.slug}/v${v}/GUIDE.md`));
|
|
137
|
+
console.log("");
|
|
124
138
|
}
|
|
@@ -2,6 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
4
|
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
|
+
import { body, section, snippet } from "../output.js";
|
|
5
6
|
import { fetchComponent, RegistryError } from "../registry.js";
|
|
6
7
|
/** Slugs/names are kebab-case by contract; reject anything else before it ever
|
|
7
8
|
* reaches a filesystem path (defense-in-depth against `../` traversal). */
|
|
@@ -51,20 +52,57 @@ export async function component(slug, name, opts) {
|
|
|
51
52
|
const names = files.map((f) => f.filename).join(", ");
|
|
52
53
|
console.log(`✓ ${config.componentsDir}/${res.name}/ → ${names} (styles: ${config.styles})`);
|
|
53
54
|
}
|
|
55
|
+
// ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
|
|
56
|
+
const tailwind = config.styles === "tailwind";
|
|
57
|
+
const imports = tailwind
|
|
58
|
+
? [
|
|
59
|
+
`@import "tailwindcss";`,
|
|
60
|
+
`@import "../_synthesisui/ds/${slug}/tokens.css";`,
|
|
61
|
+
`@import "../_synthesisui/ds/${slug}/theme.css";`,
|
|
62
|
+
]
|
|
63
|
+
: [`@import "../_synthesisui/ds/${slug}/tokens.css";`];
|
|
64
|
+
console.log(section(`One-time setup (once per app, for "${slug}")`));
|
|
65
|
+
console.log(body(`1. Import the design system in your GLOBAL stylesheet, e.g. app/globals.css`));
|
|
66
|
+
console.log(body(` (the path is relative to that file - hence the leading ../):`));
|
|
54
67
|
console.log("");
|
|
55
|
-
console.log(
|
|
56
|
-
console.log(
|
|
57
|
-
console.log(`
|
|
68
|
+
console.log(snippet(imports));
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(body(`2. Scope your app: add data-ds="${slug}" to a ROOT element, e.g. app/layout.tsx:`));
|
|
71
|
+
console.log("");
|
|
72
|
+
console.log(snippet([`<body data-ds="${slug}">{children}</body>`]));
|
|
73
|
+
console.log("");
|
|
74
|
+
console.log(body(`(If you haven't installed the system yet, run: synthesisui add ${slug})`));
|
|
75
|
+
console.log(section("Use it"));
|
|
58
76
|
if (!opts.artifactsOnly && config.target === "next") {
|
|
59
77
|
const pascalName = res.name
|
|
60
78
|
.split(/[^a-zA-Z0-9]+/)
|
|
61
79
|
.filter(Boolean)
|
|
62
80
|
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
63
81
|
.join("");
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
// Show a real variant in the example when the recipe has one.
|
|
83
|
+
const firstAxis = Object.entries(res.recipe.variants ?? {}).find(([, options]) => Object.values(options).some((block) => Object.keys(block).length > 0));
|
|
84
|
+
const exampleProp = firstAxis
|
|
85
|
+
? ` ${firstAxis[0]}="${Object.keys(firstAxis[1])[0]}"`
|
|
86
|
+
: "";
|
|
87
|
+
console.log(snippet([
|
|
88
|
+
`import { ${pascalName} } from "@/${config.componentsDir}/${res.name}";`,
|
|
89
|
+
"",
|
|
90
|
+
`<${pascalName}${exampleProp} />`,
|
|
91
|
+
]));
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log(body(`(adjust "@/" to your project's import alias if it differs)`));
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log(body("Or ask your agent:"));
|
|
96
|
+
console.log(snippet([
|
|
97
|
+
`"Use the ${pascalName} component from ${config.componentsDir}/${res.name} (SynthesisUI ${slug})."`,
|
|
98
|
+
]));
|
|
66
99
|
}
|
|
67
100
|
else {
|
|
68
|
-
console.log(
|
|
101
|
+
console.log(snippet([
|
|
102
|
+
`@import "../_synthesisui/ds/${slug}/components/${res.name}.css";`,
|
|
103
|
+
"",
|
|
104
|
+
`<div class="ds-${res.name}">…</div>`,
|
|
105
|
+
]));
|
|
69
106
|
}
|
|
107
|
+
console.log("");
|
|
70
108
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { generateComponentFiles } from "../component-codegen.js";
|
|
4
|
+
import { readProjectConfig, resolveRegistry } from "../config.js";
|
|
5
|
+
import { body, section, snippet } from "../output.js";
|
|
6
|
+
import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
|
|
7
|
+
import { add } from "./add.js";
|
|
8
|
+
/**
|
|
9
|
+
* Marco B - `synthesisui upgrade <slug>`: brings the installed system to the
|
|
10
|
+
* latest version, GUIDED. In one run it:
|
|
11
|
+
*
|
|
12
|
+
* 1. compares the installed version (`_synthesisui/ds/<slug>/.lock`) with the
|
|
13
|
+
* registry's latest and re-materializes the artifacts (same flow as `add`;
|
|
14
|
+
* the previous version folder stays for rollback/diff);
|
|
15
|
+
* 2. REGENERATES every component previously materialized into componentsDir
|
|
16
|
+
* from this system (detected by the generated-file header), so YOUR
|
|
17
|
+
* components pick up the new recipes;
|
|
18
|
+
* 3. fetches the deterministic changelog (computed server-side) and writes it
|
|
19
|
+
* to `_synthesisui/ds/<slug>/UPGRADE.md` - the migration brief your agent
|
|
20
|
+
* walks to update the app (breaking changes first).
|
|
21
|
+
*/
|
|
22
|
+
export async function upgrade(slug, opts) {
|
|
23
|
+
const base = resolveRegistry(opts.registry);
|
|
24
|
+
const root = opts.dir ?? process.cwd();
|
|
25
|
+
const slugDir = join(root, "_synthesisui", "ds", slug);
|
|
26
|
+
// installed version - upgrade only makes sense over an existing install
|
|
27
|
+
let installed;
|
|
28
|
+
try {
|
|
29
|
+
const lock = JSON.parse(await readFile(join(slugDir, ".lock"), "utf8"));
|
|
30
|
+
if (!Number.isInteger(lock.version))
|
|
31
|
+
throw new Error("no version");
|
|
32
|
+
installed = lock.version;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new RegistryError(`"${slug}" is not installed here - run \`synthesisui add ${slug}\` first.`);
|
|
36
|
+
}
|
|
37
|
+
console.log(`→ checking "${slug}" (installed: v${installed}) …`);
|
|
38
|
+
const latest = await fetchDesignSystem(base, slug);
|
|
39
|
+
if (latest.version === installed) {
|
|
40
|
+
console.log(`✓ ${slug} is already at the latest version (v${installed}).`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (latest.version < installed) {
|
|
44
|
+
console.log(`✓ ${slug} v${installed} is newer than the registry's v${latest.version} - nothing to do.`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// 1. re-materialize the artifacts (v<latest>/ + root re-exports + .lock);
|
|
48
|
+
// setup hints suppressed - an upgrade means the app is already wired.
|
|
49
|
+
await add(slug, { registry: opts.registry, dir: root, setupHints: false });
|
|
50
|
+
// 2. regenerate YOUR materialized components (the ones `component` wrote)
|
|
51
|
+
const config = await readProjectConfig(root);
|
|
52
|
+
const regenerated = [];
|
|
53
|
+
const failed = [];
|
|
54
|
+
if (config.target === "next") {
|
|
55
|
+
const componentsRoot = join(root, config.componentsDir);
|
|
56
|
+
const marker = `from the "${slug}" design system`;
|
|
57
|
+
let entries = [];
|
|
58
|
+
try {
|
|
59
|
+
entries = await readdir(componentsRoot);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// no componentsDir yet - nothing materialized
|
|
63
|
+
}
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const tsxPath = join(componentsRoot, entry, `${entry}.tsx`);
|
|
66
|
+
let head = "";
|
|
67
|
+
try {
|
|
68
|
+
head = (await readFile(tsxPath, "utf8")).slice(0, 300);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue; // not a materialized component folder
|
|
72
|
+
}
|
|
73
|
+
if (!head.includes("Generated by SynthesisUI") || !head.includes(marker))
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetchComponent(base, slug, entry);
|
|
77
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles);
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
await writeFile(join(componentsRoot, entry, file.filename), file.code, "utf8");
|
|
80
|
+
}
|
|
81
|
+
regenerated.push(entry);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
failed.push(entry); // e.g. component renamed/removed in the new version
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 3. deterministic changelog → UPGRADE.md (the agent's migration brief)
|
|
89
|
+
const log = await fetchChangelog(base, slug, installed, latest.version);
|
|
90
|
+
const upgradePath = join(slugDir, "UPGRADE.md");
|
|
91
|
+
const brief = [
|
|
92
|
+
log.markdown,
|
|
93
|
+
"",
|
|
94
|
+
"---",
|
|
95
|
+
"",
|
|
96
|
+
"## How to migrate this app",
|
|
97
|
+
"",
|
|
98
|
+
"- Fix the **Breaking** items first: search the codebase for each removed",
|
|
99
|
+
" token/component/variant and move usages to the closest replacement.",
|
|
100
|
+
"- Changed tokens re-theme automatically (CSS variables) - review screens",
|
|
101
|
+
" that hardcoded values instead of tokens.",
|
|
102
|
+
regenerated.length > 0
|
|
103
|
+
? `- These materialized components were regenerated and may show in your diff: ${regenerated
|
|
104
|
+
.map((n) => `\`${n}\``)
|
|
105
|
+
.join(", ")}.`
|
|
106
|
+
: "- No materialized components needed regeneration.",
|
|
107
|
+
"- The full new contract lives in `design-system.json` / `GUIDE.md` next to this file.",
|
|
108
|
+
"",
|
|
109
|
+
].join("\n");
|
|
110
|
+
await writeFile(upgradePath, brief, "utf8");
|
|
111
|
+
// ── report ──
|
|
112
|
+
console.log(section(`Upgraded ${slug}: v${installed} → v${latest.version}`));
|
|
113
|
+
if (log.changelog.breaking.length > 0) {
|
|
114
|
+
console.log(body(`Breaking changes (${log.changelog.breaking.length}):`));
|
|
115
|
+
console.log("");
|
|
116
|
+
console.log(snippet(log.changelog.breaking.map((item) => `- ${item}`)));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(body("No breaking changes detected."));
|
|
120
|
+
}
|
|
121
|
+
if (regenerated.length > 0) {
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log(body(`Regenerated ${regenerated.length} materialized component(s): ${regenerated.join(", ")}`));
|
|
124
|
+
}
|
|
125
|
+
if (failed.length > 0) {
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(body(`⚠ Could not regenerate: ${failed.join(", ")} (removed/renamed in v${latest.version}?)`));
|
|
128
|
+
}
|
|
129
|
+
console.log(section("Migrate the app"));
|
|
130
|
+
console.log(body(`The migration brief is at _synthesisui/ds/${slug}/UPGRADE.md`));
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(body("Ask your agent:"));
|
|
133
|
+
console.log(snippet([
|
|
134
|
+
`"Read _synthesisui/ds/${slug}/UPGRADE.md and migrate this app to ${slug} v${latest.version} -`,
|
|
135
|
+
` fix the breaking changes first, then review the changed components."`,
|
|
136
|
+
]));
|
|
137
|
+
console.log("");
|
|
138
|
+
console.log(body(`(rollback: synthesisui add ${slug} --version ${installed})`));
|
|
139
|
+
console.log("");
|
|
140
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { init } from "./commands/init.js";
|
|
|
7
7
|
import { list } from "./commands/list.js";
|
|
8
8
|
import { login } from "./commands/login.js";
|
|
9
9
|
import { template } from "./commands/template.js";
|
|
10
|
+
import { upgrade } from "./commands/upgrade.js";
|
|
10
11
|
import { use } from "./commands/use.js";
|
|
11
12
|
import { RegistryError } from "./registry.js";
|
|
12
13
|
const HELP = `synthesisui - bring SynthesisUI design systems into your project
|
|
@@ -18,6 +19,7 @@ Usage:
|
|
|
18
19
|
synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
|
|
19
20
|
synthesisui template <slug> <name> materialize a whole page from a DS template
|
|
20
21
|
synthesisui component <slug> <name> bring one component in - artifacts + YOUR <Pascal>.tsx in componentsDir
|
|
22
|
+
synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
|
|
21
23
|
synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
|
|
22
24
|
synthesisui advise "<value prop>" engagement-pattern proposals for this project (login required)
|
|
23
25
|
synthesisui generate "<desc>" generate a token-only component recipe for your DS (login required)
|
|
@@ -192,6 +194,16 @@ async function main() {
|
|
|
192
194
|
});
|
|
193
195
|
break;
|
|
194
196
|
}
|
|
197
|
+
case "upgrade": {
|
|
198
|
+
const slug = args[0];
|
|
199
|
+
if (!slug) {
|
|
200
|
+
console.error("error: provide the slug - `synthesisui upgrade <slug>`");
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
await upgrade(slug, { registry, dir });
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
195
207
|
case "use": {
|
|
196
208
|
const slug = args[0];
|
|
197
209
|
if (!slug) {
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal output helpers - the CLI's DX surface. Principles:
|
|
3
|
+
* - breathing room: blank lines around sections, never a wall of bullets;
|
|
4
|
+
* - concrete over abstract: real paths and copy-pasteable snippets, not
|
|
5
|
+
* "import it globally";
|
|
6
|
+
* - one idea per section, titled with a scannable ruled heading.
|
|
7
|
+
*/
|
|
8
|
+
const WIDTH = 66;
|
|
9
|
+
/** `── Title ───────────…` ruled section heading (with breathing room). */
|
|
10
|
+
export function section(title) {
|
|
11
|
+
const head = `── ${title} `;
|
|
12
|
+
const rest = Math.max(4, WIDTH - head.length);
|
|
13
|
+
return `\n${head}${"─".repeat(rest)}\n`;
|
|
14
|
+
}
|
|
15
|
+
/** Indented, copy-pasteable code block (empty lines stay truly empty). */
|
|
16
|
+
export function snippet(lines) {
|
|
17
|
+
return lines.map((line) => (line ? ` ${line}` : "")).join("\n");
|
|
18
|
+
}
|
|
19
|
+
/** Indented body text line. */
|
|
20
|
+
export function body(line) {
|
|
21
|
+
return ` ${line}`;
|
|
22
|
+
}
|
package/dist/registry.js
CHANGED
|
@@ -136,3 +136,17 @@ export async function postGenerate(base, payload) {
|
|
|
136
136
|
}
|
|
137
137
|
return (await res.json());
|
|
138
138
|
}
|
|
139
|
+
/** Changelog determinístico entre duas versões (Marco B) - `?changelog&from=N`. */
|
|
140
|
+
export async function fetchChangelog(base, slug, from, to) {
|
|
141
|
+
const url = new URL(`${base}/api/registry/ds/${encodeURIComponent(slug)}`);
|
|
142
|
+
url.searchParams.set("changelog", "");
|
|
143
|
+
url.searchParams.set("from", String(from));
|
|
144
|
+
if (to != null)
|
|
145
|
+
url.searchParams.set("to", String(to));
|
|
146
|
+
const res = await request(url.toString());
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
const body = (await res.json().catch(() => ({})));
|
|
149
|
+
throw new RegistryError(body.message ?? `Registry responded ${res.status} for the changelog.`);
|
|
150
|
+
}
|
|
151
|
+
return (await res.json());
|
|
152
|
+
}
|