synthesisui 0.2.1 → 0.4.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 +2 -0
- package/dist/commands/refit.js +144 -0
- package/dist/commands/upgrade.js +140 -0
- package/dist/index.js +37 -0
- package/dist/registry.js +68 -0
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -110,6 +110,8 @@ export async function add(slug, opts) {
|
|
|
110
110
|
console.log(` philosophy.md → ${sections.length} section(s) (read after rules)`);
|
|
111
111
|
}
|
|
112
112
|
console.log(` CLAUDE.md ${claudeMd.created ? "created" : "updated"} (${claudeMd.count} system(s) installed)`);
|
|
113
|
+
if (opts.setupHints === false)
|
|
114
|
+
return;
|
|
113
115
|
const hasTheme = cssArtifacts.includes("theme.css");
|
|
114
116
|
// ── DX: concrete paths + copy-pasteable snippets, with breathing room ──
|
|
115
117
|
console.log(section("One-time setup (once per app)"));
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, 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 { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
|
|
7
|
+
/** Slugs materialized under `_synthesisui/ds/` in the project. */
|
|
8
|
+
async function installedSlugs(root) {
|
|
9
|
+
try {
|
|
10
|
+
const entries = await readdir(join(root, "_synthesisui", "ds"), {
|
|
11
|
+
withFileTypes: true,
|
|
12
|
+
});
|
|
13
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Marco B5 - the REVERSE bridge, from the CLI: takes a component that lives in
|
|
21
|
+
* YOUR app (arbitrary React/CSS), re-expresses it in the design system's
|
|
22
|
+
* token vocabulary (hosted refit - gated + metered), SAVES it into your
|
|
23
|
+
* personal DS draft (it ships with the next `publish`), and materializes it
|
|
24
|
+
* back into componentsDir as your typed component. One command closes the
|
|
25
|
+
* loop: app code → on-system recipe → back as code.
|
|
26
|
+
*/
|
|
27
|
+
export async function refit(file, opts) {
|
|
28
|
+
const base = resolveRegistry(opts.registry);
|
|
29
|
+
const root = opts.dir ?? process.cwd();
|
|
30
|
+
// 1. the source component (the server caps at 24k chars - fail fast here)
|
|
31
|
+
let source;
|
|
32
|
+
try {
|
|
33
|
+
source = await readFile(join(root, file), "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new RegistryError(`Could not read "${file}".`);
|
|
37
|
+
}
|
|
38
|
+
if (source.trim().length === 0) {
|
|
39
|
+
throw new RegistryError(`"${file}" is empty.`);
|
|
40
|
+
}
|
|
41
|
+
if (source.length > 24_000) {
|
|
42
|
+
throw new RegistryError(`"${file}" is ${source.length} chars - the refit cap is 24k. Trim it to the component itself.`);
|
|
43
|
+
}
|
|
44
|
+
let support;
|
|
45
|
+
if (opts.support) {
|
|
46
|
+
try {
|
|
47
|
+
support = await readFile(join(root, opts.support), "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new RegistryError(`Could not read support file "${opts.support}".`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 2. target DS (same inference as `generate`: the single installed one)
|
|
54
|
+
let slug = opts.ds;
|
|
55
|
+
if (!slug) {
|
|
56
|
+
const slugs = await installedSlugs(root);
|
|
57
|
+
if (slugs.length === 1)
|
|
58
|
+
slug = slugs[0];
|
|
59
|
+
else if (slugs.length === 0)
|
|
60
|
+
throw new RegistryError("No design system installed here. Run `synthesisui add <slug>` first, or pass --ds <slug>.");
|
|
61
|
+
else
|
|
62
|
+
throw new RegistryError(`Multiple design systems installed (${slugs.join(", ")}). Pick one with --ds <slug>.`);
|
|
63
|
+
}
|
|
64
|
+
// 3. replace mode: fetch the existing recipe so the server keeps its name
|
|
65
|
+
// (deterministic - the AI is not trusted with it)
|
|
66
|
+
let prior;
|
|
67
|
+
if (opts.replace) {
|
|
68
|
+
const existing = await fetchComponent(base, slug, opts.replace);
|
|
69
|
+
prior = { name: existing.name, recipe: existing.recipe };
|
|
70
|
+
}
|
|
71
|
+
console.log(`→ refitting ${basename(file)} into "${slug}"${prior ? ` (replacing ds-${prior.name})` : ""} …`);
|
|
72
|
+
const instruction = [
|
|
73
|
+
opts.instruction,
|
|
74
|
+
!prior && opts.name ? `Name it "${opts.name}".` : undefined,
|
|
75
|
+
]
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join(" ");
|
|
78
|
+
const res = await postRefit(base, {
|
|
79
|
+
slug,
|
|
80
|
+
source,
|
|
81
|
+
support,
|
|
82
|
+
instruction: instruction || undefined,
|
|
83
|
+
prior,
|
|
84
|
+
});
|
|
85
|
+
const tries = `${res.tries} ${res.tries === 1 ? "try" : "tries"}`;
|
|
86
|
+
console.log(`✓ adapted as ds-${res.name} (${res.model}, ${tries})`);
|
|
87
|
+
if (opts.dry) {
|
|
88
|
+
console.log(section("Dry run - nothing saved"));
|
|
89
|
+
console.log(body("The recipe it would save:"));
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log(snippet(JSON.stringify(res.recipe, null, 2).split("\n")));
|
|
92
|
+
console.log("");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// 4. save into the personal DS draft (server re-validates token-only)
|
|
96
|
+
const saved = await postSaveComponent(base, {
|
|
97
|
+
slug,
|
|
98
|
+
name: res.name,
|
|
99
|
+
recipe: res.recipe,
|
|
100
|
+
});
|
|
101
|
+
console.log(`✓ saved into "${slug}" (draft v${saved.version} - ships with your next publish)`);
|
|
102
|
+
// 5. materialize back into the project: artifacts + YOUR typed component
|
|
103
|
+
const artifactsDir = join(root, "_synthesisui", "ds", slug, "components");
|
|
104
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
105
|
+
await writeFile(join(artifactsDir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
|
|
106
|
+
await writeFile(join(artifactsDir, `${res.name}.css`), `${res.css}\n`, "utf8");
|
|
107
|
+
const config = await readProjectConfig(root);
|
|
108
|
+
let materialized = false;
|
|
109
|
+
if (config.target === "next") {
|
|
110
|
+
const compDir = join(root, config.componentsDir, res.name);
|
|
111
|
+
await mkdir(compDir, { recursive: true });
|
|
112
|
+
const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles);
|
|
113
|
+
for (const f of files) {
|
|
114
|
+
await writeFile(join(compDir, f.filename), f.code, "utf8");
|
|
115
|
+
}
|
|
116
|
+
materialized = true;
|
|
117
|
+
console.log(`✓ ${config.componentsDir}/${res.name}/ → ${files.map((f) => f.filename).join(", ")} (styles: ${config.styles})`);
|
|
118
|
+
}
|
|
119
|
+
if (res.suggestedRule) {
|
|
120
|
+
console.log(section("Suggested rule"));
|
|
121
|
+
console.log(body("The AI inferred a reusable rule from this component:"));
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log(snippet([`"${res.suggestedRule}"`]));
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(body(`(save it in the studio if it holds: /dashboard/mine/${slug}/studio)`));
|
|
126
|
+
}
|
|
127
|
+
console.log(section("Done - the loop is closed"));
|
|
128
|
+
console.log(body(`Your component now lives in the design system (docs, studio, showcase)`));
|
|
129
|
+
if (materialized) {
|
|
130
|
+
const pascal = res.name
|
|
131
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.map((p) => p[0].toUpperCase() + p.slice(1))
|
|
134
|
+
.join("");
|
|
135
|
+
console.log(body(`and back in your code, on-system:`));
|
|
136
|
+
console.log("");
|
|
137
|
+
console.log(snippet([
|
|
138
|
+
`import { ${pascal} } from "@/${config.componentsDir}/${res.name}";`,
|
|
139
|
+
]));
|
|
140
|
+
console.log("");
|
|
141
|
+
console.log(body(`(replace the old ${basename(file)} usages with it when you're ready)`));
|
|
142
|
+
}
|
|
143
|
+
console.log("");
|
|
144
|
+
}
|
|
@@ -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
|
@@ -6,7 +6,9 @@ import { generate } from "./commands/generate.js";
|
|
|
6
6
|
import { init } from "./commands/init.js";
|
|
7
7
|
import { list } from "./commands/list.js";
|
|
8
8
|
import { login } from "./commands/login.js";
|
|
9
|
+
import { refit } from "./commands/refit.js";
|
|
9
10
|
import { template } from "./commands/template.js";
|
|
11
|
+
import { upgrade } from "./commands/upgrade.js";
|
|
10
12
|
import { use } from "./commands/use.js";
|
|
11
13
|
import { RegistryError } from "./registry.js";
|
|
12
14
|
const HELP = `synthesisui - bring SynthesisUI design systems into your project
|
|
@@ -18,6 +20,8 @@ Usage:
|
|
|
18
20
|
synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
|
|
19
21
|
synthesisui template <slug> <name> materialize a whole page from a DS template
|
|
20
22
|
synthesisui component <slug> <name> bring one component in - artifacts + YOUR <Pascal>.tsx in componentsDir
|
|
23
|
+
synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
|
|
24
|
+
synthesisui refit <file> [--ds <slug>] send an app component INTO your DS (token-only) and get it back as code
|
|
21
25
|
synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
|
|
22
26
|
synthesisui advise "<value prop>" engagement-pattern proposals for this project (login required)
|
|
23
27
|
synthesisui generate "<desc>" generate a token-only component recipe for your DS (login required)
|
|
@@ -33,6 +37,10 @@ Options:
|
|
|
33
37
|
--components-dir <dir> init: folder where components live (default: components)
|
|
34
38
|
--styles <s> init: component code flavor: css | tailwind (default: css)
|
|
35
39
|
--artifacts-only component: skip the .tsx materialization (recipe + css only)
|
|
40
|
+
--replace <name> refit: replace an existing DS component (keeps its name)
|
|
41
|
+
--support <file> refit: supporting CSS file (globals/vars the code references)
|
|
42
|
+
--instruction <s> refit: extra guidance for the adaptation
|
|
43
|
+
--dry refit: adapt and print, but save nothing
|
|
36
44
|
--out <path> output path for the generated template (default: <pagesDir>/<file>)
|
|
37
45
|
-h, --help this help
|
|
38
46
|
|
|
@@ -192,6 +200,35 @@ async function main() {
|
|
|
192
200
|
});
|
|
193
201
|
break;
|
|
194
202
|
}
|
|
203
|
+
case "refit": {
|
|
204
|
+
const file = args[0];
|
|
205
|
+
if (!file) {
|
|
206
|
+
console.error("error: provide the component file - `synthesisui refit <file> [--ds <slug>]`");
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
await refit(file, {
|
|
211
|
+
registry,
|
|
212
|
+
dir,
|
|
213
|
+
ds: typeof flags.ds === "string" ? flags.ds : undefined,
|
|
214
|
+
name: typeof flags.name === "string" ? flags.name : undefined,
|
|
215
|
+
replace: typeof flags.replace === "string" ? flags.replace : undefined,
|
|
216
|
+
instruction: typeof flags.instruction === "string" ? flags.instruction : undefined,
|
|
217
|
+
support: typeof flags.support === "string" ? flags.support : undefined,
|
|
218
|
+
dry: flags.dry === true,
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "upgrade": {
|
|
223
|
+
const slug = args[0];
|
|
224
|
+
if (!slug) {
|
|
225
|
+
console.error("error: provide the slug - `synthesisui upgrade <slug>`");
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
await upgrade(slug, { registry, dir });
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
195
232
|
case "use": {
|
|
196
233
|
const slug = args[0];
|
|
197
234
|
if (!slug) {
|
package/dist/registry.js
CHANGED
|
@@ -136,3 +136,71 @@ 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
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Refit hospedado (INS-18 / Marco B5): manda código de componente arbitrário e
|
|
155
|
+
* recebe a recipe token-only vestida no DS. Gated + metered server-side:
|
|
156
|
+
* 401 = sem login, 429 = cota diária.
|
|
157
|
+
*/
|
|
158
|
+
export async function postRefit(base, payload) {
|
|
159
|
+
let res;
|
|
160
|
+
try {
|
|
161
|
+
res = await fetch(`${base}/api/ai/studio`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: { "content-type": "application/json", ...(await authHeaders()) },
|
|
164
|
+
body: JSON.stringify(payload),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
throw new RegistryError(`Could not reach the registry at ${base}. ` +
|
|
169
|
+
`Check the URL (--registry / SYNTHESISUI_REGISTRY_URL) and your connection.`);
|
|
170
|
+
}
|
|
171
|
+
if (res.status === 401) {
|
|
172
|
+
throw new RegistryError("Not authenticated. Run `synthesisui login` first.");
|
|
173
|
+
}
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const body = (await res.json().catch(() => ({})));
|
|
176
|
+
throw new RegistryError(body.message ?? `Refit responded ${res.status}.`);
|
|
177
|
+
}
|
|
178
|
+
return (await res.json());
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Persiste um componente no DS pessoal do usuário autenticado
|
|
182
|
+
* (`POST /api/ds/component`) - a metade "salvar" da ponte reversa. O servidor
|
|
183
|
+
* re-valida a recipe (token-only, sem refs órfãs) antes de gravar no rascunho.
|
|
184
|
+
*/
|
|
185
|
+
export async function postSaveComponent(base, payload) {
|
|
186
|
+
let res;
|
|
187
|
+
try {
|
|
188
|
+
res = await fetch(`${base}/api/ds/component`, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "content-type": "application/json", ...(await authHeaders()) },
|
|
191
|
+
body: JSON.stringify(payload),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
throw new RegistryError(`Could not reach the registry at ${base}. ` +
|
|
196
|
+
`Check the URL (--registry / SYNTHESISUI_REGISTRY_URL) and your connection.`);
|
|
197
|
+
}
|
|
198
|
+
if (res.status === 401) {
|
|
199
|
+
throw new RegistryError("Not authenticated. Run `synthesisui login` first.");
|
|
200
|
+
}
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
const body = (await res.json().catch(() => ({})));
|
|
203
|
+
throw new RegistryError(body.message ?? `Save responded ${res.status}.`);
|
|
204
|
+
}
|
|
205
|
+
return (await res.json());
|
|
206
|
+
}
|