@organcli/composed-cli 0.1.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/index.js +540 -0
- package/dist/registry-bundle.json +1060 -0
- package/package.json +34 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/add.ts
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
|
|
9
|
+
// src/lib/install-files.ts
|
|
10
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
11
|
+
import { dirname as dirname2, join as join4, relative } from "path";
|
|
12
|
+
|
|
13
|
+
// src/lib/i18n.ts
|
|
14
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
15
|
+
import { join as join2 } from "path";
|
|
16
|
+
|
|
17
|
+
// src/lib/source.ts
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
19
|
+
import { dirname, join } from "path";
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
21
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
function findAncestorContaining(startDir, markerRelPath, maxLevels = 6) {
|
|
23
|
+
let dir = startDir;
|
|
24
|
+
for (let i = 0; i <= maxLevels; i++) {
|
|
25
|
+
if (existsSync(join(dir, markerRelPath))) return dir;
|
|
26
|
+
const parent = dirname(dir);
|
|
27
|
+
if (parent === dir) return void 0;
|
|
28
|
+
dir = parent;
|
|
29
|
+
}
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
function loadFromBundle() {
|
|
33
|
+
const distDir = findAncestorContaining(here, "registry-bundle.json");
|
|
34
|
+
if (!distDir) return void 0;
|
|
35
|
+
const bundle = JSON.parse(
|
|
36
|
+
readFileSync(join(distDir, "registry-bundle.json"), "utf-8")
|
|
37
|
+
);
|
|
38
|
+
return {
|
|
39
|
+
registry: bundle.registry,
|
|
40
|
+
readFile: (path) => {
|
|
41
|
+
const content = bundle.files[path];
|
|
42
|
+
if (content === void 0) {
|
|
43
|
+
throw new Error(`registry-bundle.json is missing file: ${path}`);
|
|
44
|
+
}
|
|
45
|
+
return content;
|
|
46
|
+
},
|
|
47
|
+
readMessages: (locale) => bundle.messages[locale] ?? {}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function loadFromBoilerplateTree() {
|
|
51
|
+
const boilerplateRoot = findAncestorContaining(
|
|
52
|
+
here,
|
|
53
|
+
join("src", "registry", "components.ts")
|
|
54
|
+
);
|
|
55
|
+
if (!boilerplateRoot) return void 0;
|
|
56
|
+
const registryPath = join(boilerplateRoot, "src", "registry", "components.ts");
|
|
57
|
+
const mod = await import(
|
|
58
|
+
/* @vite-ignore */
|
|
59
|
+
pathToFileURL(registryPath).href
|
|
60
|
+
);
|
|
61
|
+
return {
|
|
62
|
+
registry: mod.componentRegistry,
|
|
63
|
+
readFile: (path) => {
|
|
64
|
+
const abs = join(boilerplateRoot, path);
|
|
65
|
+
if (!existsSync(abs)) {
|
|
66
|
+
throw new Error(`Source file missing from boilerplate tree: ${path}`);
|
|
67
|
+
}
|
|
68
|
+
return readFileSync(abs, "utf-8");
|
|
69
|
+
},
|
|
70
|
+
readMessages: (locale) => readMessagesFromTree(boilerplateRoot, locale)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function readMessagesFromTree(boilerplateRoot, locale) {
|
|
74
|
+
const dir = join(boilerplateRoot, "messages", locale);
|
|
75
|
+
const out = {};
|
|
76
|
+
if (!existsSync(dir)) return out;
|
|
77
|
+
for (const file of readdirSync(dir)) {
|
|
78
|
+
if (!file.endsWith(".json")) continue;
|
|
79
|
+
const namespace = file.replace(/\.json$/, "");
|
|
80
|
+
const json = JSON.parse(readFileSync(join(dir, file), "utf-8"));
|
|
81
|
+
for (const [key, value] of Object.entries(json)) {
|
|
82
|
+
if (key === "$schema") continue;
|
|
83
|
+
out[`${namespace}_${key}`] = String(value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
var cached;
|
|
89
|
+
async function getComponentSource() {
|
|
90
|
+
if (cached) return cached;
|
|
91
|
+
const source = loadFromBundle() ?? await loadFromBoilerplateTree();
|
|
92
|
+
if (!source) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Could not locate component source. Expected either a built dist/registry-bundle.json (published package) or a sibling src/registry/components.ts (running inside the boilerplate repo)."
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
cached = source;
|
|
98
|
+
return source;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/lib/i18n.ts
|
|
102
|
+
var STRINGS_RUNTIME = `// Generated by composed-cli. Minimal string lookup for components copied
|
|
103
|
+
// from the BIM boilerplate \u2014 replace with your own i18n solution as needed.
|
|
104
|
+
import strings from './strings.json';
|
|
105
|
+
|
|
106
|
+
export function t(key: keyof typeof strings, params?: Record<string, string | number>) {
|
|
107
|
+
let value = strings[key] ?? key;
|
|
108
|
+
if (params) {
|
|
109
|
+
for (const [name, val] of Object.entries(params)) {
|
|
110
|
+
value = value.replaceAll(\`{\${name}}\`, String(val));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
async function installI18nStrings(cwd, keys, destDir = "src/lib/composed-strings") {
|
|
117
|
+
if (keys.length === 0) return { ok: true, addedKeys: [], path: "" };
|
|
118
|
+
const source = await getComponentSource();
|
|
119
|
+
const enMessages = source.readMessages("en");
|
|
120
|
+
const outDir = join2(cwd, destDir);
|
|
121
|
+
mkdirSync(outDir, { recursive: true });
|
|
122
|
+
const stringsPath = join2(outDir, "strings.json");
|
|
123
|
+
const existing = existsSync2(stringsPath) ? JSON.parse(readFileSync2(stringsPath, "utf-8")) : {};
|
|
124
|
+
const addedKeys = [];
|
|
125
|
+
for (const key of keys) {
|
|
126
|
+
if (!(key in enMessages)) continue;
|
|
127
|
+
if (!(key in existing)) addedKeys.push(key);
|
|
128
|
+
existing[key] = enMessages[key];
|
|
129
|
+
}
|
|
130
|
+
const sorted = Object.fromEntries(
|
|
131
|
+
Object.keys(existing).sort().map((key) => [key, existing[key]])
|
|
132
|
+
);
|
|
133
|
+
writeFileSync(stringsPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
134
|
+
const runtimePath = join2(outDir, "index.ts");
|
|
135
|
+
if (!existsSync2(runtimePath)) {
|
|
136
|
+
writeFileSync(runtimePath, STRINGS_RUNTIME, "utf-8");
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, addedKeys, path: join2(destDir, "index.ts") };
|
|
139
|
+
}
|
|
140
|
+
function rewriteParaglideUsage(source, importPath) {
|
|
141
|
+
let out = source.replace(
|
|
142
|
+
/import\s*\{\s*m\s*\}\s*from\s*['"]#\/paraglide\/messages['"];?\n?/,
|
|
143
|
+
`import { t } from '${importPath}';
|
|
144
|
+
`
|
|
145
|
+
);
|
|
146
|
+
out = out.replace(/\bm\.([a-zA-Z0-9_]+)\(\s*\)/g, "t('$1')");
|
|
147
|
+
out = out.replace(/\bm\.([a-zA-Z0-9_]+)\(\s*(\{[^}]*\})\s*\)/g, "t('$1', $2)");
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/lib/target-config.ts
|
|
152
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
153
|
+
import { join as join3 } from "path";
|
|
154
|
+
var defaultAliases = {
|
|
155
|
+
components: "@/components",
|
|
156
|
+
ui: "@/components/ui",
|
|
157
|
+
utils: "@/lib/utils",
|
|
158
|
+
lib: "@/lib"
|
|
159
|
+
};
|
|
160
|
+
function readTargetAliases(cwd) {
|
|
161
|
+
const configPath = join3(cwd, "components.json");
|
|
162
|
+
if (!existsSync3(configPath)) return defaultAliases;
|
|
163
|
+
try {
|
|
164
|
+
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
165
|
+
return {
|
|
166
|
+
components: config.aliases?.components ?? defaultAliases.components,
|
|
167
|
+
ui: config.aliases?.ui ?? defaultAliases.ui,
|
|
168
|
+
utils: config.aliases?.utils ?? defaultAliases.utils,
|
|
169
|
+
lib: config.aliases?.lib ?? defaultAliases.lib
|
|
170
|
+
};
|
|
171
|
+
} catch {
|
|
172
|
+
return defaultAliases;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function aliasToPath(cwd, alias) {
|
|
176
|
+
const bareAlias = alias.replace(/^[@#]\//, "");
|
|
177
|
+
const base = existsSync3(join3(cwd, "src")) ? join3(cwd, "src") : cwd;
|
|
178
|
+
return join3(base, bareAlias);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/lib/install-files.ts
|
|
182
|
+
async function copyComponentFiles(cwd, entry, opts) {
|
|
183
|
+
const source = await getComponentSource();
|
|
184
|
+
const aliases = readTargetAliases(cwd);
|
|
185
|
+
const uiDir = aliasToPath(cwd, aliases.ui);
|
|
186
|
+
const composedDir = join4(uiDir, "composed", entry.slug);
|
|
187
|
+
const results = [];
|
|
188
|
+
for (const file of entry.files) {
|
|
189
|
+
const fileName = file.path.split("/").pop();
|
|
190
|
+
const targetPath = join4(composedDir, fileName);
|
|
191
|
+
if (existsSync4(targetPath) && !opts.overwrite) {
|
|
192
|
+
results.push({
|
|
193
|
+
entryName: entry.name,
|
|
194
|
+
targetPath,
|
|
195
|
+
skipped: true,
|
|
196
|
+
reason: "already exists (use --overwrite to replace)"
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
let content = source.readFile(file.path);
|
|
201
|
+
if (entry.i18n.required) {
|
|
202
|
+
content = rewriteParaglideUsage(content, opts.stringsImportPath);
|
|
203
|
+
}
|
|
204
|
+
mkdirSync2(dirname2(targetPath), { recursive: true });
|
|
205
|
+
writeFileSync2(targetPath, content, "utf-8");
|
|
206
|
+
results.push({ entryName: entry.name, targetPath, skipped: false });
|
|
207
|
+
}
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
async function copyLocalUtil(cwd, localDep, opts) {
|
|
211
|
+
if (localDep !== "@/lib/utils") return void 0;
|
|
212
|
+
const source = await getComponentSource();
|
|
213
|
+
const aliases = readTargetAliases(cwd);
|
|
214
|
+
const utilsAbsPath = aliasToPath(cwd, aliases.utils) + ".ts";
|
|
215
|
+
if (existsSync4(utilsAbsPath) && !opts.overwrite) {
|
|
216
|
+
return {
|
|
217
|
+
entryName: "cn",
|
|
218
|
+
targetPath: utilsAbsPath,
|
|
219
|
+
skipped: true,
|
|
220
|
+
reason: "already exists"
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const content = source.readFile("src/lib/utils.ts");
|
|
224
|
+
mkdirSync2(dirname2(utilsAbsPath), { recursive: true });
|
|
225
|
+
writeFileSync2(utilsAbsPath, content, "utf-8");
|
|
226
|
+
return { entryName: "cn", targetPath: utilsAbsPath, skipped: false };
|
|
227
|
+
}
|
|
228
|
+
function toDisplayPath(cwd, absPath) {
|
|
229
|
+
return relative(cwd, absPath).split("\\").join("/");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/lib/npm-install.ts
|
|
233
|
+
import { spawnSync } from "child_process";
|
|
234
|
+
|
|
235
|
+
// src/lib/package-manager.ts
|
|
236
|
+
import { existsSync as existsSync5 } from "fs";
|
|
237
|
+
import { join as join5 } from "path";
|
|
238
|
+
var lockfiles = {
|
|
239
|
+
"bun.lock": "bun",
|
|
240
|
+
"bun.lockb": "bun",
|
|
241
|
+
"pnpm-lock.yaml": "pnpm",
|
|
242
|
+
"yarn.lock": "yarn",
|
|
243
|
+
"package-lock.json": "npm"
|
|
244
|
+
};
|
|
245
|
+
function detectPackageManager(cwd) {
|
|
246
|
+
for (const [lockfile, pm] of Object.entries(lockfiles)) {
|
|
247
|
+
if (existsSync5(join5(cwd, lockfile))) return pm;
|
|
248
|
+
}
|
|
249
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
250
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
251
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
252
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
253
|
+
return "npm";
|
|
254
|
+
}
|
|
255
|
+
function installCommand(pm, packages) {
|
|
256
|
+
switch (pm) {
|
|
257
|
+
case "bun":
|
|
258
|
+
return ["bun", ["add", ...packages]];
|
|
259
|
+
case "pnpm":
|
|
260
|
+
return ["pnpm", ["add", ...packages]];
|
|
261
|
+
case "yarn":
|
|
262
|
+
return ["yarn", ["add", ...packages]];
|
|
263
|
+
case "npm":
|
|
264
|
+
default:
|
|
265
|
+
return ["npm", ["install", ...packages]];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function dlxCommand(pm, args) {
|
|
269
|
+
switch (pm) {
|
|
270
|
+
case "bun":
|
|
271
|
+
return ["bunx", args];
|
|
272
|
+
case "pnpm":
|
|
273
|
+
return ["pnpm", ["dlx", ...args]];
|
|
274
|
+
case "yarn":
|
|
275
|
+
return ["yarn", ["dlx", ...args]];
|
|
276
|
+
case "npm":
|
|
277
|
+
default:
|
|
278
|
+
return ["npx", args];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/lib/npm-install.ts
|
|
283
|
+
function installNpmPackages(cwd, packages, pm) {
|
|
284
|
+
if (packages.length === 0) return { ok: true };
|
|
285
|
+
const [cmd, args] = installCommand(pm, packages);
|
|
286
|
+
const result = spawnSync(cmd, args, {
|
|
287
|
+
cwd,
|
|
288
|
+
stdio: "inherit",
|
|
289
|
+
shell: process.platform === "win32"
|
|
290
|
+
});
|
|
291
|
+
if (result.status !== 0) {
|
|
292
|
+
return { ok: false, message: `${cmd} exited with code ${result.status}` };
|
|
293
|
+
}
|
|
294
|
+
return { ok: true };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/lib/registry.ts
|
|
298
|
+
async function loadRegistry() {
|
|
299
|
+
const source = await getComponentSource();
|
|
300
|
+
return source.registry;
|
|
301
|
+
}
|
|
302
|
+
async function findComponent(query) {
|
|
303
|
+
const registry = await loadRegistry();
|
|
304
|
+
const byName = registry.find((e) => e.name === query);
|
|
305
|
+
if (byName) return byName;
|
|
306
|
+
const bySlug = registry.filter((e) => e.slug === query);
|
|
307
|
+
if (bySlug.length === 1) return bySlug[0];
|
|
308
|
+
if (bySlug.length > 1) return bySlug;
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/lib/resolve.ts
|
|
313
|
+
async function resolveQueries(queries) {
|
|
314
|
+
const entries = [];
|
|
315
|
+
const notFound = [];
|
|
316
|
+
const ambiguous = {};
|
|
317
|
+
const seen = /* @__PURE__ */ new Set();
|
|
318
|
+
for (const query of queries) {
|
|
319
|
+
const match = await findComponent(query);
|
|
320
|
+
if (!match) {
|
|
321
|
+
notFound.push(query);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (Array.isArray(match)) {
|
|
325
|
+
ambiguous[query] = match.map((e) => e.name);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (!seen.has(match.name)) {
|
|
329
|
+
seen.add(match.name);
|
|
330
|
+
entries.push(match);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { entries, notFound, ambiguous };
|
|
334
|
+
}
|
|
335
|
+
function unionDependencies(entries) {
|
|
336
|
+
const shadcn = /* @__PURE__ */ new Set();
|
|
337
|
+
const npm = /* @__PURE__ */ new Set();
|
|
338
|
+
const local = /* @__PURE__ */ new Set();
|
|
339
|
+
const i18nKeys = /* @__PURE__ */ new Set();
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
entry.dependencies.shadcn.forEach((s) => shadcn.add(s));
|
|
342
|
+
entry.dependencies.npm.forEach((n) => npm.add(n));
|
|
343
|
+
entry.dependencies.local.forEach((l) => local.add(l));
|
|
344
|
+
entry.i18n.keys.forEach((k) => i18nKeys.add(k));
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
shadcn: [...shadcn],
|
|
348
|
+
npm: [...npm],
|
|
349
|
+
local: [...local],
|
|
350
|
+
i18nKeys: [...i18nKeys]
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
async function allComponentNames() {
|
|
354
|
+
return (await loadRegistry()).map((e) => `${e.name} (${e.slug})`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/lib/shadcn.ts
|
|
358
|
+
import { existsSync as existsSync6 } from "fs";
|
|
359
|
+
import { join as join6 } from "path";
|
|
360
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
361
|
+
function hasShadcnConfig(cwd) {
|
|
362
|
+
return existsSync6(join6(cwd, "components.json"));
|
|
363
|
+
}
|
|
364
|
+
function installShadcnPrimitives(cwd, slugs, pm) {
|
|
365
|
+
if (slugs.length === 0) return [];
|
|
366
|
+
const [cmd, baseArgs] = dlxCommand(pm, ["shadcn@latest", "add", ...slugs, "-y"]);
|
|
367
|
+
const result = spawnSync2(cmd, baseArgs, {
|
|
368
|
+
cwd,
|
|
369
|
+
stdio: "inherit",
|
|
370
|
+
shell: process.platform === "win32"
|
|
371
|
+
});
|
|
372
|
+
const ok = result.status === 0;
|
|
373
|
+
return slugs.map((slug) => ({
|
|
374
|
+
slug,
|
|
375
|
+
ok,
|
|
376
|
+
message: ok ? void 0 : `shadcn add exited with code ${result.status}`
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/lib/validate-install.ts
|
|
381
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
382
|
+
function validateCopiedFiles(files) {
|
|
383
|
+
const issues = [];
|
|
384
|
+
for (const file of files) {
|
|
385
|
+
if (!existsSync7(file.targetPath)) {
|
|
386
|
+
issues.push({ file: file.targetPath, message: "file was not written" });
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const content = readFileSync4(file.targetPath, "utf-8");
|
|
390
|
+
if (/from\s*['"]#\/paraglide\/messages['"]/.test(content)) {
|
|
391
|
+
issues.push({
|
|
392
|
+
file: file.targetPath,
|
|
393
|
+
message: "still imports Paraglide messages (rewrite did not apply)"
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (/\bm\.[a-zA-Z0-9_]+\(/.test(content)) {
|
|
397
|
+
issues.push({
|
|
398
|
+
file: file.targetPath,
|
|
399
|
+
message: "still calls m.*() (rewrite missed a usage)"
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return issues;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/commands/add.ts
|
|
407
|
+
async function runAdd(queries, options) {
|
|
408
|
+
const { cwd } = options;
|
|
409
|
+
if (queries.length === 0) {
|
|
410
|
+
console.error(pc.red("Usage: composed-cli add <component> [components...]"));
|
|
411
|
+
console.error("");
|
|
412
|
+
console.error("Available components:");
|
|
413
|
+
for (const name of await allComponentNames()) console.error(` ${name}`);
|
|
414
|
+
process.exitCode = 1;
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const { entries, notFound, ambiguous } = await resolveQueries(queries);
|
|
418
|
+
if (notFound.length > 0) {
|
|
419
|
+
console.error(pc.red(`Unknown component(s): ${notFound.join(", ")}`));
|
|
420
|
+
}
|
|
421
|
+
for (const [query, names] of Object.entries(ambiguous)) {
|
|
422
|
+
console.error(
|
|
423
|
+
pc.red(
|
|
424
|
+
`"${query}" matches multiple components: ${names.join(", ")}. Use one of those exact names.`
|
|
425
|
+
)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (entries.length === 0) {
|
|
429
|
+
process.exitCode = 1;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!hasShadcnConfig(cwd)) {
|
|
433
|
+
console.error(
|
|
434
|
+
pc.red(
|
|
435
|
+
"No components.json found in this project. Run `shadcn init` before `composed-cli add`."
|
|
436
|
+
)
|
|
437
|
+
);
|
|
438
|
+
process.exitCode = 1;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
console.log(pc.bold(`Installing: ${entries.map((e) => e.name).join(", ")}`));
|
|
442
|
+
const deps = unionDependencies(entries);
|
|
443
|
+
const pm = detectPackageManager(cwd);
|
|
444
|
+
if (deps.shadcn.length > 0 && !options.skipShadcn) {
|
|
445
|
+
console.log(pc.cyan(`
|
|
446
|
+
> Installing shadcn primitives: ${deps.shadcn.join(", ")}`));
|
|
447
|
+
const results = installShadcnPrimitives(cwd, deps.shadcn, pm);
|
|
448
|
+
const failed = results.filter((r) => !r.ok);
|
|
449
|
+
if (failed.length > 0) {
|
|
450
|
+
console.error(
|
|
451
|
+
pc.red(`shadcn add failed for: ${failed.map((f) => f.slug).join(", ")}`)
|
|
452
|
+
);
|
|
453
|
+
process.exitCode = 1;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
} else if (deps.shadcn.length > 0) {
|
|
457
|
+
console.log(pc.dim(`
|
|
458
|
+
> Skipped shadcn primitives (--skip-shadcn): ${deps.shadcn.join(", ")}`));
|
|
459
|
+
}
|
|
460
|
+
if (deps.npm.length > 0 && !options.skipNpm) {
|
|
461
|
+
console.log(pc.cyan(`
|
|
462
|
+
> Installing npm packages: ${deps.npm.join(", ")}`));
|
|
463
|
+
const result = installNpmPackages(cwd, deps.npm, pm);
|
|
464
|
+
if (!result.ok) {
|
|
465
|
+
console.error(pc.red(`npm install failed: ${result.message}`));
|
|
466
|
+
process.exitCode = 1;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
} else if (deps.npm.length > 0) {
|
|
470
|
+
console.log(pc.dim(`
|
|
471
|
+
> Skipped npm packages (--skip-npm): ${deps.npm.join(", ")}`));
|
|
472
|
+
}
|
|
473
|
+
const stringsImportPath = "@/lib/composed-strings";
|
|
474
|
+
let i18nAdded = [];
|
|
475
|
+
if (deps.i18nKeys.length > 0 && !options.skipI18n) {
|
|
476
|
+
const result = await installI18nStrings(cwd, deps.i18nKeys);
|
|
477
|
+
i18nAdded = result.addedKeys;
|
|
478
|
+
}
|
|
479
|
+
console.log(pc.cyan(`
|
|
480
|
+
> Copying component files`));
|
|
481
|
+
const copiedFiles = [];
|
|
482
|
+
for (const entry of entries) {
|
|
483
|
+
const copied = await copyComponentFiles(cwd, entry, {
|
|
484
|
+
overwrite: options.overwrite,
|
|
485
|
+
stringsImportPath
|
|
486
|
+
});
|
|
487
|
+
copiedFiles.push(...copied);
|
|
488
|
+
for (const file of copied) {
|
|
489
|
+
if (file.skipped) {
|
|
490
|
+
console.log(pc.yellow(` skip ${toDisplayPath(cwd, file.targetPath)} (${file.reason})`));
|
|
491
|
+
} else {
|
|
492
|
+
console.log(pc.green(` add ${toDisplayPath(cwd, file.targetPath)}`));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (entry.knownIssues?.length) {
|
|
496
|
+
for (const issue of entry.knownIssues) {
|
|
497
|
+
console.log(pc.yellow(` ! ${entry.name}: ${issue}`));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (const localDep of deps.local) {
|
|
502
|
+
const copied = await copyLocalUtil(cwd, localDep, { overwrite: options.overwrite });
|
|
503
|
+
if (copied) {
|
|
504
|
+
copiedFiles.push(copied);
|
|
505
|
+
const line = copied.skipped ? pc.yellow(` skip ${toDisplayPath(cwd, copied.targetPath)} (${copied.reason})`) : pc.green(` add ${toDisplayPath(cwd, copied.targetPath)}`);
|
|
506
|
+
console.log(line);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (i18nAdded.length > 0) {
|
|
510
|
+
console.log(pc.green(` add src/lib/composed-strings/ (${i18nAdded.length} key(s))`));
|
|
511
|
+
}
|
|
512
|
+
const issues = validateCopiedFiles(copiedFiles);
|
|
513
|
+
if (issues.length > 0) {
|
|
514
|
+
console.log(pc.red(`
|
|
515
|
+
> Validation found ${issues.length} issue(s):`));
|
|
516
|
+
for (const issue of issues) {
|
|
517
|
+
console.log(pc.red(` ${toDisplayPath(cwd, issue.file)}: ${issue.message}`));
|
|
518
|
+
}
|
|
519
|
+
process.exitCode = 1;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
console.log(pc.bold(pc.green(`
|
|
523
|
+
Done. Installed ${entries.length} component(s).`)));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/index.ts
|
|
527
|
+
var program = new Command();
|
|
528
|
+
program.name("composed-cli").description(
|
|
529
|
+
"Install individual composed UI components from the BIM frontend boilerplate into a Next.js + shadcn project."
|
|
530
|
+
).version("0.1.0");
|
|
531
|
+
program.command("add").description("Add one or more composed components to the current project").argument("<components...>", "component name(s) or slug(s), e.g. select dialog table").option("-c, --cwd <path>", "target project directory", process.cwd()).option("-o, --overwrite", "overwrite files that already exist", false).option("--skip-shadcn", "do not run `shadcn add` for required primitives", false).option("--skip-npm", "do not install required npm packages", false).option("--skip-i18n", "do not generate the local i18n strings file", false).action(async (components, opts) => {
|
|
532
|
+
await runAdd(components, {
|
|
533
|
+
cwd: opts.cwd,
|
|
534
|
+
overwrite: opts.overwrite,
|
|
535
|
+
skipShadcn: opts.skipShadcn,
|
|
536
|
+
skipNpm: opts.skipNpm,
|
|
537
|
+
skipI18n: opts.skipI18n
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
program.parseAsync(process.argv);
|