@organcli/composed-cli 0.1.0 → 0.1.2
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 +714 -13
- package/dist/registry-bundle.json +451 -2
- package/package.json +34 -34
package/dist/index.js
CHANGED
|
@@ -10,6 +10,30 @@ import pc from "picocolors";
|
|
|
10
10
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
11
11
|
import { dirname as dirname2, join as join4, relative } from "path";
|
|
12
12
|
|
|
13
|
+
// src/lib/controllers.ts
|
|
14
|
+
var CONTROLLERS_PREFIX = "src/components/common/controllers/";
|
|
15
|
+
function toKebabCase(name) {
|
|
16
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
function findControllerPath(source, exportName) {
|
|
19
|
+
const guessed = `${CONTROLLERS_PREFIX}${toKebabCase(exportName)}.tsx`;
|
|
20
|
+
if (source.hasFile(guessed) && source.readFile(guessed).includes(`export function ${exportName}`)) {
|
|
21
|
+
return guessed;
|
|
22
|
+
}
|
|
23
|
+
for (const path of source.listFiles(CONTROLLERS_PREFIX)) {
|
|
24
|
+
if (!path.endsWith(".tsx")) continue;
|
|
25
|
+
if (path.endsWith("/index.tsx") || path.endsWith("/index.ts")) continue;
|
|
26
|
+
if (source.readFile(path).includes(`export function ${exportName}`)) {
|
|
27
|
+
return path;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
function controllerBarrelExport(exportName, relPath) {
|
|
33
|
+
const fileName = relPath.split("/").pop().replace(/\.tsx?$/, "");
|
|
34
|
+
return `export { ${exportName} } from './${fileName}';`;
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
// src/lib/i18n.ts
|
|
14
38
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
15
39
|
import { join as join2 } from "path";
|
|
@@ -37,6 +61,7 @@ function loadFromBundle() {
|
|
|
37
61
|
);
|
|
38
62
|
return {
|
|
39
63
|
registry: bundle.registry,
|
|
64
|
+
featureRegistry: bundle.featureRegistry ?? [],
|
|
40
65
|
readFile: (path) => {
|
|
41
66
|
const content = bundle.files[path];
|
|
42
67
|
if (content === void 0) {
|
|
@@ -44,22 +69,34 @@ function loadFromBundle() {
|
|
|
44
69
|
}
|
|
45
70
|
return content;
|
|
46
71
|
},
|
|
72
|
+
hasFile: (path) => path in bundle.files,
|
|
73
|
+
listFiles: (prefix) => Object.keys(bundle.files).filter((p) => p === prefix || p.startsWith(prefix)),
|
|
47
74
|
readMessages: (locale) => bundle.messages[locale] ?? {}
|
|
48
75
|
};
|
|
49
76
|
}
|
|
50
77
|
async function loadFromBoilerplateTree() {
|
|
51
|
-
const boilerplateRoot = findAncestorContaining(
|
|
52
|
-
here,
|
|
53
|
-
join("src", "registry", "components.ts")
|
|
54
|
-
);
|
|
78
|
+
const boilerplateRoot = overrideRoot ?? findAncestorContaining(here, join("src", "registry", "components.ts"));
|
|
55
79
|
if (!boilerplateRoot) return void 0;
|
|
80
|
+
if (!existsSync(join(boilerplateRoot, "src", "registry", "components.ts"))) {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
56
83
|
const registryPath = join(boilerplateRoot, "src", "registry", "components.ts");
|
|
57
84
|
const mod = await import(
|
|
58
85
|
/* @vite-ignore */
|
|
59
86
|
pathToFileURL(registryPath).href
|
|
60
87
|
);
|
|
88
|
+
const featuresPath = join(boilerplateRoot, "src", "registry", "features.ts");
|
|
89
|
+
let featureRegistry = [];
|
|
90
|
+
if (existsSync(featuresPath)) {
|
|
91
|
+
const featuresMod = await import(
|
|
92
|
+
/* @vite-ignore */
|
|
93
|
+
pathToFileURL(featuresPath).href
|
|
94
|
+
);
|
|
95
|
+
featureRegistry = featuresMod.featureRegistry;
|
|
96
|
+
}
|
|
61
97
|
return {
|
|
62
98
|
registry: mod.componentRegistry,
|
|
99
|
+
featureRegistry,
|
|
63
100
|
readFile: (path) => {
|
|
64
101
|
const abs = join(boilerplateRoot, path);
|
|
65
102
|
if (!existsSync(abs)) {
|
|
@@ -67,28 +104,54 @@ async function loadFromBoilerplateTree() {
|
|
|
67
104
|
}
|
|
68
105
|
return readFileSync(abs, "utf-8");
|
|
69
106
|
},
|
|
107
|
+
hasFile: (path) => existsSync(join(boilerplateRoot, path)),
|
|
108
|
+
listFiles: (prefix) => listFilesFromTree(boilerplateRoot, prefix),
|
|
70
109
|
readMessages: (locale) => readMessagesFromTree(boilerplateRoot, locale)
|
|
71
110
|
};
|
|
72
111
|
}
|
|
112
|
+
function listFilesFromTree(boilerplateRoot, prefix) {
|
|
113
|
+
const abs = join(boilerplateRoot, prefix);
|
|
114
|
+
if (!existsSync(abs)) return [];
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
117
|
+
const rel = `${prefix.replace(/\\/g, "/").replace(/\/$/, "")}/${entry.name}`;
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
out.push(...listFilesFromTree(boilerplateRoot, rel));
|
|
120
|
+
} else {
|
|
121
|
+
out.push(rel);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
73
126
|
function readMessagesFromTree(boilerplateRoot, locale) {
|
|
74
127
|
const dir = join(boilerplateRoot, "messages", locale);
|
|
75
128
|
const out = {};
|
|
76
129
|
if (!existsSync(dir)) return out;
|
|
77
130
|
for (const file of readdirSync(dir)) {
|
|
78
131
|
if (!file.endsWith(".json")) continue;
|
|
79
|
-
const namespace = file.replace(/\.json$/, "");
|
|
80
132
|
const json = JSON.parse(readFileSync(join(dir, file), "utf-8"));
|
|
81
133
|
for (const [key, value] of Object.entries(json)) {
|
|
82
134
|
if (key === "$schema") continue;
|
|
83
|
-
out[
|
|
135
|
+
out[key] = String(value);
|
|
84
136
|
}
|
|
85
137
|
}
|
|
86
138
|
return out;
|
|
87
139
|
}
|
|
88
140
|
var cached;
|
|
141
|
+
var overrideRoot;
|
|
142
|
+
function setSourceRoot(root) {
|
|
143
|
+
cached = void 0;
|
|
144
|
+
overrideRoot = root;
|
|
145
|
+
}
|
|
146
|
+
function getSourceRoot() {
|
|
147
|
+
return overrideRoot;
|
|
148
|
+
}
|
|
149
|
+
function resolveSourceRoot() {
|
|
150
|
+
return overrideRoot ?? findAncestorContaining(here, join("src", "registry", "components.ts"));
|
|
151
|
+
}
|
|
89
152
|
async function getComponentSource() {
|
|
90
153
|
if (cached) return cached;
|
|
91
|
-
const source = loadFromBundle() ?? await loadFromBoilerplateTree();
|
|
154
|
+
const source = overrideRoot ? await loadFromBoilerplateTree() : loadFromBundle() ?? await loadFromBoilerplateTree();
|
|
92
155
|
if (!source) {
|
|
93
156
|
throw new Error(
|
|
94
157
|
"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)."
|
|
@@ -205,8 +268,41 @@ async function copyComponentFiles(cwd, entry, opts) {
|
|
|
205
268
|
writeFileSync2(targetPath, content, "utf-8");
|
|
206
269
|
results.push({ entryName: entry.name, targetPath, skipped: false });
|
|
207
270
|
}
|
|
271
|
+
if (entry.controller) {
|
|
272
|
+
const controller = await copyController(cwd, entry, opts);
|
|
273
|
+
if (controller) results.push(controller);
|
|
274
|
+
}
|
|
208
275
|
return results;
|
|
209
276
|
}
|
|
277
|
+
async function copyController(cwd, entry, opts) {
|
|
278
|
+
if (!entry.controller) return void 0;
|
|
279
|
+
const source = await getComponentSource();
|
|
280
|
+
const controllerPath = findControllerPath(source, entry.controller);
|
|
281
|
+
if (!controllerPath) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Could not find controller file for ${entry.name} (${entry.controller})`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const aliases = readTargetAliases(cwd);
|
|
287
|
+
const fileName = controllerPath.split("/").pop();
|
|
288
|
+
const targetPath = join4(
|
|
289
|
+
aliasToPath(cwd, aliases.components),
|
|
290
|
+
"common",
|
|
291
|
+
"controllers",
|
|
292
|
+
fileName
|
|
293
|
+
);
|
|
294
|
+
if (existsSync4(targetPath) && !opts.overwrite) {
|
|
295
|
+
return {
|
|
296
|
+
entryName: entry.controller,
|
|
297
|
+
targetPath,
|
|
298
|
+
skipped: true,
|
|
299
|
+
reason: "already exists (use --overwrite to replace)"
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
mkdirSync2(dirname2(targetPath), { recursive: true });
|
|
303
|
+
writeFileSync2(targetPath, source.readFile(controllerPath), "utf-8");
|
|
304
|
+
return { entryName: entry.controller, targetPath, skipped: false };
|
|
305
|
+
}
|
|
210
306
|
async function copyLocalUtil(cwd, localDep, opts) {
|
|
211
307
|
if (localDep !== "@/lib/utils") return void 0;
|
|
212
308
|
const source = await getComponentSource();
|
|
@@ -294,6 +390,10 @@ function installNpmPackages(cwd, packages, pm) {
|
|
|
294
390
|
return { ok: true };
|
|
295
391
|
}
|
|
296
392
|
|
|
393
|
+
// src/lib/resolve.ts
|
|
394
|
+
import { existsSync as existsSync6 } from "fs";
|
|
395
|
+
import { dirname as dirname3, isAbsolute, relative as relative2, resolve } from "path";
|
|
396
|
+
|
|
297
397
|
// src/lib/registry.ts
|
|
298
398
|
async function loadRegistry() {
|
|
299
399
|
const source = await getComponentSource();
|
|
@@ -316,6 +416,20 @@ async function resolveQueries(queries) {
|
|
|
316
416
|
const ambiguous = {};
|
|
317
417
|
const seen = /* @__PURE__ */ new Set();
|
|
318
418
|
for (const query of queries) {
|
|
419
|
+
const fromPath = looksLikePath(query) ? await findComponentsByPath(query) : void 0;
|
|
420
|
+
if (fromPath) {
|
|
421
|
+
if (fromPath.length === 0) {
|
|
422
|
+
notFound.push(query);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
for (const match2 of fromPath) {
|
|
426
|
+
if (!seen.has(match2.name)) {
|
|
427
|
+
seen.add(match2.name);
|
|
428
|
+
entries.push(match2);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
319
433
|
const match = await findComponent(query);
|
|
320
434
|
if (!match) {
|
|
321
435
|
notFound.push(query);
|
|
@@ -353,13 +467,58 @@ function unionDependencies(entries) {
|
|
|
353
467
|
async function allComponentNames() {
|
|
354
468
|
return (await loadRegistry()).map((e) => `${e.name} (${e.slug})`);
|
|
355
469
|
}
|
|
470
|
+
function looksLikePath(query) {
|
|
471
|
+
return isAbsolute(query) || /[\\/]/.test(query);
|
|
472
|
+
}
|
|
473
|
+
function toPosix(query) {
|
|
474
|
+
return query.replace(/\\/g, "/").replace(/\/$/, "");
|
|
475
|
+
}
|
|
476
|
+
function registryPrefixes(query) {
|
|
477
|
+
const posix = toPosix(query);
|
|
478
|
+
if (!posix || isAbsolute(query)) return [];
|
|
479
|
+
const prefixes = [posix];
|
|
480
|
+
if (!posix.startsWith("src/")) prefixes.push(`src/${posix}`);
|
|
481
|
+
return prefixes;
|
|
482
|
+
}
|
|
483
|
+
function matchesPrefix(filePath, prefix) {
|
|
484
|
+
return filePath === prefix || filePath.startsWith(`${prefix}/`);
|
|
485
|
+
}
|
|
486
|
+
function findBoilerplateRoot(start) {
|
|
487
|
+
let dir = start;
|
|
488
|
+
for (let i = 0; i <= 8; i++) {
|
|
489
|
+
if (existsSync6(resolve(dir, "src", "registry", "components.ts"))) return dir;
|
|
490
|
+
const parent = dirname3(dir);
|
|
491
|
+
if (parent === dir) return void 0;
|
|
492
|
+
dir = parent;
|
|
493
|
+
}
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
function queryToRelPath(query) {
|
|
497
|
+
const base = getSourceRoot() ?? process.cwd();
|
|
498
|
+
const abs = isAbsolute(query) ? query : resolve(base, query);
|
|
499
|
+
if (!existsSync6(abs)) return void 0;
|
|
500
|
+
const root = getSourceRoot() ?? findBoilerplateRoot(abs) ?? findBoilerplateRoot(process.cwd());
|
|
501
|
+
if (!root) return void 0;
|
|
502
|
+
const rel = relative2(root, abs).split("\\").join("/");
|
|
503
|
+
if (!rel || rel.startsWith("..")) return void 0;
|
|
504
|
+
return rel.replace(/\/$/, "");
|
|
505
|
+
}
|
|
506
|
+
async function findComponentsByPath(query) {
|
|
507
|
+
const rel = queryToRelPath(query);
|
|
508
|
+
const prefixes = rel ? [rel] : registryPrefixes(query);
|
|
509
|
+
if (prefixes.length === 0) return void 0;
|
|
510
|
+
const registry = await loadRegistry();
|
|
511
|
+
return registry.filter(
|
|
512
|
+
(entry) => entry.files.some((file) => prefixes.some((prefix) => matchesPrefix(file.path, prefix)))
|
|
513
|
+
);
|
|
514
|
+
}
|
|
356
515
|
|
|
357
516
|
// src/lib/shadcn.ts
|
|
358
|
-
import { existsSync as
|
|
517
|
+
import { existsSync as existsSync7 } from "fs";
|
|
359
518
|
import { join as join6 } from "path";
|
|
360
519
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
361
520
|
function hasShadcnConfig(cwd) {
|
|
362
|
-
return
|
|
521
|
+
return existsSync7(join6(cwd, "components.json"));
|
|
363
522
|
}
|
|
364
523
|
function installShadcnPrimitives(cwd, slugs, pm) {
|
|
365
524
|
if (slugs.length === 0) return [];
|
|
@@ -378,11 +537,11 @@ function installShadcnPrimitives(cwd, slugs, pm) {
|
|
|
378
537
|
}
|
|
379
538
|
|
|
380
539
|
// src/lib/validate-install.ts
|
|
381
|
-
import { existsSync as
|
|
540
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
|
|
382
541
|
function validateCopiedFiles(files) {
|
|
383
542
|
const issues = [];
|
|
384
543
|
for (const file of files) {
|
|
385
|
-
if (!
|
|
544
|
+
if (!existsSync8(file.targetPath)) {
|
|
386
545
|
issues.push({ file: file.targetPath, message: "file was not written" });
|
|
387
546
|
continue;
|
|
388
547
|
}
|
|
@@ -523,12 +682,487 @@ async function runAdd(queries, options) {
|
|
|
523
682
|
Done. Installed ${entries.length} component(s).`)));
|
|
524
683
|
}
|
|
525
684
|
|
|
685
|
+
// src/commands/create.ts
|
|
686
|
+
import { existsSync as existsSync12 } from "fs";
|
|
687
|
+
import pc2 from "picocolors";
|
|
688
|
+
|
|
689
|
+
// src/lib/framework-detect.ts
|
|
690
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
691
|
+
import { join as join7 } from "path";
|
|
692
|
+
function readPackageJson(cwd) {
|
|
693
|
+
const pkgPath = join7(cwd, "package.json");
|
|
694
|
+
if (!existsSync9(pkgPath)) return void 0;
|
|
695
|
+
try {
|
|
696
|
+
return JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
697
|
+
} catch {
|
|
698
|
+
return void 0;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function allDeps(pkg) {
|
|
702
|
+
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
703
|
+
}
|
|
704
|
+
function detectTargetFramework(cwd) {
|
|
705
|
+
const pkg = readPackageJson(cwd);
|
|
706
|
+
if (pkg) {
|
|
707
|
+
const deps = allDeps(pkg);
|
|
708
|
+
if ("@tanstack/react-start" in deps || "@tanstack/start" in deps) {
|
|
709
|
+
return {
|
|
710
|
+
framework: "tanstack",
|
|
711
|
+
reason: "package.json depends on @tanstack/react-start"
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if ("next" in deps) {
|
|
715
|
+
return { framework: "nextjs", reason: "package.json depends on next" };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (existsSync9(join7(cwd, "next.config.js")) || existsSync9(join7(cwd, "next.config.ts")) || existsSync9(join7(cwd, "next.config.mjs"))) {
|
|
719
|
+
return { framework: "nextjs", reason: "found a next.config.* file" };
|
|
720
|
+
}
|
|
721
|
+
if (existsSync9(join7(cwd, "app")) && (existsSync9(join7(cwd, "app", "layout.tsx")) || existsSync9(join7(cwd, "app", "layout.js")))) {
|
|
722
|
+
return { framework: "nextjs", reason: "found app/layout.tsx (Next.js App Router)" };
|
|
723
|
+
}
|
|
724
|
+
if (existsSync9(join7(cwd, "src", "app")) && (existsSync9(join7(cwd, "src", "app", "layout.tsx")) || existsSync9(join7(cwd, "src", "app", "layout.js")))) {
|
|
725
|
+
return { framework: "nextjs", reason: "found src/app/layout.tsx (Next.js App Router)" };
|
|
726
|
+
}
|
|
727
|
+
if (existsSync9(join7(cwd, "src", "routeTree.gen.ts"))) {
|
|
728
|
+
return {
|
|
729
|
+
framework: "tanstack",
|
|
730
|
+
reason: "found src/routeTree.gen.ts (TanStack Router)"
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
if (existsSync9(join7(cwd, "src", "routes")) && existsSync9(join7(cwd, "src", "router.tsx"))) {
|
|
734
|
+
return {
|
|
735
|
+
framework: "tanstack",
|
|
736
|
+
reason: "found src/routes and src/router.tsx (TanStack Start layout)"
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
return void 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/lib/feature-registry.ts
|
|
743
|
+
async function loadFeatureRegistry() {
|
|
744
|
+
const source = await getComponentSource();
|
|
745
|
+
return source.featureRegistry;
|
|
746
|
+
}
|
|
747
|
+
async function findFeature(name) {
|
|
748
|
+
const registry = await loadFeatureRegistry();
|
|
749
|
+
return registry.find((e) => e.name === name);
|
|
750
|
+
}
|
|
751
|
+
async function listFeatureNames() {
|
|
752
|
+
return (await loadFeatureRegistry()).map((e) => e.name);
|
|
753
|
+
}
|
|
754
|
+
function frameworkLabel(framework) {
|
|
755
|
+
return framework === "tanstack" ? "TanStack Start" : "Next.js";
|
|
756
|
+
}
|
|
757
|
+
function supportedFrameworksFor(entry) {
|
|
758
|
+
return Object.keys(entry.frameworks);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/lib/install-feature.ts
|
|
762
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
763
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
764
|
+
function nextAppDir(cwd) {
|
|
765
|
+
if (existsSync10(join8(cwd, "src", "app"))) return "src/app";
|
|
766
|
+
if (existsSync10(join8(cwd, "app"))) return "app";
|
|
767
|
+
if (existsSync10(join8(cwd, "src"))) return "src/app";
|
|
768
|
+
return "app";
|
|
769
|
+
}
|
|
770
|
+
function toFeatureTargetPath(cwd, sourceRelPath, impl, framework) {
|
|
771
|
+
let rel = sourceRelPath.replace(/\\/g, "/");
|
|
772
|
+
const prefix = impl.sourcePrefix?.replace(/\\/g, "/");
|
|
773
|
+
if (prefix && rel.startsWith(prefix)) rel = rel.slice(prefix.length);
|
|
774
|
+
if (framework === "nextjs" && (rel === "src/app" || rel.startsWith("src/app/"))) {
|
|
775
|
+
const appDir = nextAppDir(cwd);
|
|
776
|
+
if (appDir === "app") rel = rel.replace(/^src\/app/, "app");
|
|
777
|
+
}
|
|
778
|
+
return rel;
|
|
779
|
+
}
|
|
780
|
+
function adaptContent(content, framework) {
|
|
781
|
+
if (framework !== "nextjs") return content;
|
|
782
|
+
return rewriteParaglideUsage(content, "@/lib/composed-strings").replaceAll("#/", "@/");
|
|
783
|
+
}
|
|
784
|
+
function sourceContent(source, relPath, framework) {
|
|
785
|
+
return adaptContent(source.readFile(relPath), framework);
|
|
786
|
+
}
|
|
787
|
+
function fileMatchesSource(cwd, sourceRelPath, targetRelPath, source, framework) {
|
|
788
|
+
const targetPath = join8(cwd, targetRelPath);
|
|
789
|
+
if (!existsSync10(targetPath)) return false;
|
|
790
|
+
return readFileSync6(targetPath, "utf-8") === sourceContent(source, sourceRelPath, framework);
|
|
791
|
+
}
|
|
792
|
+
async function installFeatureFiles(cwd, impl, opts) {
|
|
793
|
+
const source = await getComponentSource();
|
|
794
|
+
const results = [];
|
|
795
|
+
for (const relPath of impl.files) {
|
|
796
|
+
const targetRel = toFeatureTargetPath(cwd, relPath, impl, opts.framework);
|
|
797
|
+
const targetPath = join8(cwd, targetRel);
|
|
798
|
+
if (existsSync10(targetPath) && !opts.overwrite) {
|
|
799
|
+
if (fileMatchesSource(cwd, relPath, targetRel, source, opts.framework)) {
|
|
800
|
+
results.push({
|
|
801
|
+
path: targetRel,
|
|
802
|
+
status: "skipped",
|
|
803
|
+
reason: "already installed (identical)"
|
|
804
|
+
});
|
|
805
|
+
} else {
|
|
806
|
+
results.push({
|
|
807
|
+
path: targetRel,
|
|
808
|
+
status: "skipped",
|
|
809
|
+
reason: "already exists (use --overwrite to replace)"
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const content = sourceContent(source, relPath, opts.framework);
|
|
815
|
+
mkdirSync3(dirname4(targetPath), { recursive: true });
|
|
816
|
+
writeFileSync3(targetPath, content, "utf-8");
|
|
817
|
+
results.push({ path: targetRel, status: "added" });
|
|
818
|
+
}
|
|
819
|
+
return results;
|
|
820
|
+
}
|
|
821
|
+
async function findFeatureFileConflicts(cwd, impl, framework) {
|
|
822
|
+
const source = await getComponentSource();
|
|
823
|
+
const conflicts = [];
|
|
824
|
+
for (const relPath of impl.files) {
|
|
825
|
+
const targetRel = toFeatureTargetPath(cwd, relPath, impl, framework);
|
|
826
|
+
if (existsSync10(join8(cwd, targetRel)) && !fileMatchesSource(cwd, relPath, targetRel, source, framework)) {
|
|
827
|
+
conflicts.push(targetRel);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return conflicts;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/lib/move-files.ts
|
|
834
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
835
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
836
|
+
async function copyEntryToRepo(destRoot, entry, opts) {
|
|
837
|
+
const source = await getComponentSource();
|
|
838
|
+
const results = [];
|
|
839
|
+
for (const file of entry.files) {
|
|
840
|
+
results.push(writeRelPath(destRoot, file.path, source.readFile(file.path), entry.name, opts));
|
|
841
|
+
}
|
|
842
|
+
if (entry.controller) {
|
|
843
|
+
const controllerPath = findControllerPath(source, entry.controller);
|
|
844
|
+
if (!controllerPath) {
|
|
845
|
+
throw new Error(
|
|
846
|
+
`Could not find controller file for ${entry.name} (${entry.controller})`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
results.push(
|
|
850
|
+
writeRelPath(
|
|
851
|
+
destRoot,
|
|
852
|
+
controllerPath,
|
|
853
|
+
source.readFile(controllerPath),
|
|
854
|
+
entry.controller,
|
|
855
|
+
opts
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
upsertControllerBarrel(destRoot, entry.controller, controllerPath, results);
|
|
859
|
+
}
|
|
860
|
+
return results;
|
|
861
|
+
}
|
|
862
|
+
async function mergeI18nIntoRepo(destRoot, keys) {
|
|
863
|
+
if (keys.length === 0) return { added: [] };
|
|
864
|
+
const messagesRoot = join9(destRoot, "messages");
|
|
865
|
+
const source = await getComponentSource();
|
|
866
|
+
const added = [];
|
|
867
|
+
for (const locale of ["en", "ar"]) {
|
|
868
|
+
const localeDir = join9(messagesRoot, locale);
|
|
869
|
+
const messages = source.readMessages(locale);
|
|
870
|
+
const namespaces = existsSync11(localeDir) ? readdirSync2(localeDir).filter((file) => file.endsWith(".json")).map((file) => file.replace(/\.json$/, "")) : [];
|
|
871
|
+
const grouped = {};
|
|
872
|
+
for (const key of keys) {
|
|
873
|
+
if (!(key in messages)) continue;
|
|
874
|
+
const ns = namespaceForKey(key, namespaces);
|
|
875
|
+
grouped[ns] ??= {};
|
|
876
|
+
grouped[ns][key] = messages[key];
|
|
877
|
+
}
|
|
878
|
+
for (const [ns, kvs] of Object.entries(grouped)) {
|
|
879
|
+
const filePath = join9(localeDir, `${ns}.json`);
|
|
880
|
+
const existing = existsSync11(filePath) ? JSON.parse(readFileSync7(filePath, "utf-8")) : { $schema: "https://inlang.com/schema/inlang-message-format" };
|
|
881
|
+
for (const [key, value] of Object.entries(kvs)) {
|
|
882
|
+
if (!(key in existing)) added.push(`${locale}/${ns}.json#${key}`);
|
|
883
|
+
existing[key] = value;
|
|
884
|
+
}
|
|
885
|
+
const schema = existing.$schema;
|
|
886
|
+
delete existing.$schema;
|
|
887
|
+
const sorted = Object.fromEntries(
|
|
888
|
+
Object.keys(existing).sort().map((key) => [key, existing[key]])
|
|
889
|
+
);
|
|
890
|
+
const out = schema ? { $schema: schema, ...sorted } : sorted;
|
|
891
|
+
mkdirSync4(dirname5(filePath), { recursive: true });
|
|
892
|
+
writeFileSync4(filePath, JSON.stringify(out, null, 2) + "\n", "utf-8");
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return { added };
|
|
896
|
+
}
|
|
897
|
+
function writeRelPath(destRoot, relPath, content, entryName, opts) {
|
|
898
|
+
const targetPath = join9(destRoot, relPath);
|
|
899
|
+
if (existsSync11(targetPath) && !opts.overwrite) {
|
|
900
|
+
return {
|
|
901
|
+
entryName,
|
|
902
|
+
targetPath,
|
|
903
|
+
skipped: true,
|
|
904
|
+
reason: "already exists (use --overwrite to replace)"
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
mkdirSync4(dirname5(targetPath), { recursive: true });
|
|
908
|
+
writeFileSync4(targetPath, content, "utf-8");
|
|
909
|
+
return { entryName, targetPath, skipped: false };
|
|
910
|
+
}
|
|
911
|
+
function upsertControllerBarrel(destRoot, exportName, controllerPath, results) {
|
|
912
|
+
const barrelRel = "src/components/common/controllers/index.ts";
|
|
913
|
+
const barrelAbs = join9(destRoot, barrelRel);
|
|
914
|
+
const line = controllerBarrelExport(exportName, controllerPath);
|
|
915
|
+
const existing = existsSync11(barrelAbs) ? readFileSync7(barrelAbs, "utf-8") : "";
|
|
916
|
+
if (existing.includes(`export { ${exportName} }`)) return;
|
|
917
|
+
const next = existing.trimEnd() ? `${existing.replace(/\s*$/, "\n")}${line}
|
|
918
|
+
` : `${line}
|
|
919
|
+
`;
|
|
920
|
+
mkdirSync4(dirname5(barrelAbs), { recursive: true });
|
|
921
|
+
writeFileSync4(barrelAbs, next, "utf-8");
|
|
922
|
+
results.push({ entryName: exportName, targetPath: barrelAbs, skipped: false });
|
|
923
|
+
}
|
|
924
|
+
function namespaceForKey(key, namespaces) {
|
|
925
|
+
const existing = namespaces.filter((ns) => key === ns || key.startsWith(`${ns}_`)).sort((a, b) => b.length - a.length)[0];
|
|
926
|
+
if (existing) return existing;
|
|
927
|
+
const underscoreIndex = key.indexOf("_");
|
|
928
|
+
return underscoreIndex === -1 ? key : key.slice(0, underscoreIndex);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/commands/create.ts
|
|
932
|
+
async function runCreateFeature(featureName, options) {
|
|
933
|
+
const { cwd } = options;
|
|
934
|
+
if (!featureName) {
|
|
935
|
+
console.error(pc2.red("Usage: composed-cli create feature <name> [--protected|--public]"));
|
|
936
|
+
process.exitCode = 1;
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (options.protected && options.public) {
|
|
940
|
+
console.error(pc2.red("Pass either --protected or --public, not both."));
|
|
941
|
+
process.exitCode = 1;
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (!existsSync12(cwd)) {
|
|
945
|
+
console.error(pc2.red(`Target directory does not exist: ${cwd}`));
|
|
946
|
+
process.exitCode = 1;
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const feature = await findFeature(featureName);
|
|
950
|
+
if (!feature) {
|
|
951
|
+
const available = await listFeatureNames();
|
|
952
|
+
console.error(pc2.red(`Feature "${featureName}" is not available in the boilerplate.`));
|
|
953
|
+
console.error("");
|
|
954
|
+
console.error(pc2.bold("Available features:"));
|
|
955
|
+
for (const name of available) console.error(pc2.dim(` - ${name}`));
|
|
956
|
+
process.exitCode = 1;
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const detected = detectTargetFramework(cwd);
|
|
960
|
+
if (!detected) {
|
|
961
|
+
console.error(
|
|
962
|
+
pc2.red(
|
|
963
|
+
`Could not detect the target project's framework at ${cwd}. Expected to find a package.json with a recognizable framework dependency (e.g. "next" or "@tanstack/react-start"), or matching project structure.`
|
|
964
|
+
)
|
|
965
|
+
);
|
|
966
|
+
process.exitCode = 1;
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const impl = feature.frameworks[detected.framework];
|
|
970
|
+
if (!impl) {
|
|
971
|
+
const supported = supportedFrameworksFor(feature);
|
|
972
|
+
console.error(
|
|
973
|
+
pc2.red(
|
|
974
|
+
`Feature "${feature.name}" exists, but ${frameworkLabel(detected.framework)} is not currently supported for this feature.`
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
console.error("");
|
|
978
|
+
console.error(pc2.bold("Supported frameworks:"));
|
|
979
|
+
for (const fw of supported) console.error(pc2.dim(` - ${frameworkLabel(fw)}`));
|
|
980
|
+
process.exitCode = 1;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const requestedScope = options.protected ? "protected" : options.public ? "public" : void 0;
|
|
984
|
+
if (requestedScope && impl.routeScope && requestedScope !== impl.routeScope) {
|
|
985
|
+
console.error(
|
|
986
|
+
pc2.red(
|
|
987
|
+
`--${requestedScope} does not apply to "${feature.name}" for ${frameworkLabel(detected.framework)}: this implementation's route is fixed at "${impl.routeScope}".`
|
|
988
|
+
)
|
|
989
|
+
);
|
|
990
|
+
process.exitCode = 1;
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (requestedScope && !impl.routeScope) {
|
|
994
|
+
console.error(
|
|
995
|
+
pc2.red(
|
|
996
|
+
`--${requestedScope} does not apply to "${feature.name}" for ${frameworkLabel(detected.framework)}: this implementation has no protected/public route distinction.`
|
|
997
|
+
)
|
|
998
|
+
);
|
|
999
|
+
process.exitCode = 1;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
console.log(pc2.dim(`Detected framework: ${frameworkLabel(detected.framework)} (${detected.reason})`));
|
|
1003
|
+
const conflicts = await findFeatureFileConflicts(cwd, impl, detected.framework);
|
|
1004
|
+
if (conflicts.length > 0 && !options.overwrite) {
|
|
1005
|
+
console.error(
|
|
1006
|
+
pc2.red(`Feature "${feature.name}" already exists \u2014 the following file(s) already have different content:`)
|
|
1007
|
+
);
|
|
1008
|
+
for (const conflict of conflicts) {
|
|
1009
|
+
console.error(pc2.red(` ${conflict}`));
|
|
1010
|
+
}
|
|
1011
|
+
console.error("");
|
|
1012
|
+
console.error(pc2.dim("Pass --overwrite to replace them."));
|
|
1013
|
+
process.exitCode = 1;
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (conflicts.length > 0) {
|
|
1017
|
+
console.log(pc2.yellow(`Overwriting ${conflicts.length} existing file(s).`));
|
|
1018
|
+
}
|
|
1019
|
+
console.log(pc2.bold(`\u2714 Feature: ${feature.name}`));
|
|
1020
|
+
console.log(pc2.bold(`\u2714 Framework implementation: ${frameworkLabel(detected.framework)}`));
|
|
1021
|
+
const copied = await installFeatureFiles(cwd, impl, {
|
|
1022
|
+
overwrite: options.overwrite,
|
|
1023
|
+
framework: detected.framework
|
|
1024
|
+
});
|
|
1025
|
+
for (const file of copied) {
|
|
1026
|
+
if (file.status === "skipped") {
|
|
1027
|
+
console.log(pc2.yellow(` skip ${file.path} (${file.reason})`));
|
|
1028
|
+
} else {
|
|
1029
|
+
console.log(pc2.green(` add ${toDisplayPath(cwd, `${cwd}/${file.path}`)}`));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (detected.framework === "nextjs") {
|
|
1033
|
+
const i18n = await installI18nStrings(cwd, impl.i18nKeys);
|
|
1034
|
+
if (i18n.addedKeys.length > 0) {
|
|
1035
|
+
console.log(pc2.green(` add ${i18n.path} (${i18n.addedKeys.length} key(s))`));
|
|
1036
|
+
}
|
|
1037
|
+
} else {
|
|
1038
|
+
const i18n = await mergeI18nIntoRepo(cwd, impl.i18nKeys);
|
|
1039
|
+
if (i18n.added.length > 0) {
|
|
1040
|
+
console.log(pc2.green(` add messages/ (${i18n.added.length} key(s))`));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
console.log("");
|
|
1044
|
+
console.log(pc2.bold(pc2.green(`Done. Installed feature "${feature.name}" (${frameworkLabel(detected.framework)}) into ${cwd}.`)));
|
|
1045
|
+
if (detected.framework === "tanstack") {
|
|
1046
|
+
console.log(
|
|
1047
|
+
pc2.dim(
|
|
1048
|
+
"Restart `bun run dev` (or run a build) so paraglideVitePlugin regenerates src/paraglide/ with any new message keys."
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/commands/move.ts
|
|
1055
|
+
import { existsSync as existsSync13, statSync } from "fs";
|
|
1056
|
+
import { resolve as resolve2 } from "path";
|
|
1057
|
+
import pc3 from "picocolors";
|
|
1058
|
+
async function runMove(queries, options) {
|
|
1059
|
+
if (queries.length === 0) {
|
|
1060
|
+
console.error(
|
|
1061
|
+
pc3.red(
|
|
1062
|
+
"Usage: composed-cli move <component-or-path> [--to <repo>]"
|
|
1063
|
+
)
|
|
1064
|
+
);
|
|
1065
|
+
console.error("");
|
|
1066
|
+
console.error("Pass a registry name (labeled-select) or a path like components/ui.");
|
|
1067
|
+
console.error("Available components:");
|
|
1068
|
+
for (const name of await allComponentNames()) console.error(` ${name}`);
|
|
1069
|
+
process.exitCode = 1;
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (options.from) {
|
|
1073
|
+
const fromRoot = resolve2(options.from);
|
|
1074
|
+
if (!existsSync13(fromRoot) || !statSync(fromRoot).isDirectory()) {
|
|
1075
|
+
console.error(pc3.red(`Source is not a directory: ${fromRoot}`));
|
|
1076
|
+
process.exitCode = 1;
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
setSourceRoot(fromRoot);
|
|
1080
|
+
}
|
|
1081
|
+
const destRoot = resolve2(options.to);
|
|
1082
|
+
if (!existsSync13(destRoot) || !statSync(destRoot).isDirectory()) {
|
|
1083
|
+
console.error(pc3.red(`Destination is not a directory: ${destRoot}`));
|
|
1084
|
+
process.exitCode = 1;
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const sourceRoot = resolveSourceRoot();
|
|
1088
|
+
if (sourceRoot && resolve2(sourceRoot) === destRoot) {
|
|
1089
|
+
console.error(pc3.red("Source and destination are the same repository."));
|
|
1090
|
+
console.error("Pass --to <other-repo>, for example:");
|
|
1091
|
+
console.error(
|
|
1092
|
+
pc3.cyan(" npx tsx cli/src/index.ts move src/components/ui --to M:\\my-next-app")
|
|
1093
|
+
);
|
|
1094
|
+
process.exitCode = 1;
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const { entries, notFound, ambiguous } = await resolveQueries(queries);
|
|
1098
|
+
if (notFound.length > 0) {
|
|
1099
|
+
console.error(pc3.red(`Unknown component(s): ${notFound.join(", ")}`));
|
|
1100
|
+
console.error(
|
|
1101
|
+
pc3.dim(
|
|
1102
|
+
"Use a name like labeled-select, or a path like components/ui. No local boilerplate is required."
|
|
1103
|
+
)
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
for (const [query, names] of Object.entries(ambiguous)) {
|
|
1107
|
+
console.error(
|
|
1108
|
+
pc3.red(
|
|
1109
|
+
`"${query}" matches multiple components: ${names.join(", ")}. Use one of those exact names.`
|
|
1110
|
+
)
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
if (entries.length === 0) {
|
|
1114
|
+
process.exitCode = 1;
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (!existsSync13(resolve2(destRoot, ".git"))) {
|
|
1118
|
+
console.log(pc3.yellow(`Warning: ${destRoot} has no .git directory.`));
|
|
1119
|
+
}
|
|
1120
|
+
if (hasShadcnConfig(destRoot)) {
|
|
1121
|
+
await runAdd(
|
|
1122
|
+
entries.map((e) => e.name),
|
|
1123
|
+
{
|
|
1124
|
+
cwd: destRoot,
|
|
1125
|
+
overwrite: options.overwrite,
|
|
1126
|
+
skipShadcn: false,
|
|
1127
|
+
skipNpm: false,
|
|
1128
|
+
skipI18n: false
|
|
1129
|
+
}
|
|
1130
|
+
);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
console.log(
|
|
1134
|
+
pc3.bold(`Moving into ${destRoot}: ${entries.map((e) => e.name).join(", ")}`)
|
|
1135
|
+
);
|
|
1136
|
+
for (const entry of entries) {
|
|
1137
|
+
const copied = await copyEntryToRepo(destRoot, entry, {
|
|
1138
|
+
overwrite: options.overwrite
|
|
1139
|
+
});
|
|
1140
|
+
for (const file of copied) {
|
|
1141
|
+
if (file.skipped) {
|
|
1142
|
+
console.log(pc3.yellow(` skip ${toDisplayPath(destRoot, file.targetPath)} (${file.reason})`));
|
|
1143
|
+
} else {
|
|
1144
|
+
console.log(pc3.green(` add ${toDisplayPath(destRoot, file.targetPath)}`));
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const deps = unionDependencies(entries);
|
|
1149
|
+
const i18n = await mergeI18nIntoRepo(destRoot, deps.i18nKeys);
|
|
1150
|
+
if (i18n.added.length > 0) {
|
|
1151
|
+
console.log(pc3.green(` add messages/ (${i18n.added.length} key(s))`));
|
|
1152
|
+
}
|
|
1153
|
+
console.log(pc3.bold(pc3.green(`
|
|
1154
|
+
Done. Copied ${entries.length} component(s) into ${destRoot}.`)));
|
|
1155
|
+
}
|
|
1156
|
+
|
|
526
1157
|
// src/index.ts
|
|
527
1158
|
var program = new Command();
|
|
528
1159
|
program.name("composed-cli").description(
|
|
529
1160
|
"Install individual composed UI components from the BIM frontend boilerplate into a Next.js + shadcn project."
|
|
530
|
-
).version("0.1.
|
|
531
|
-
program.command("add").description("Add one or more composed components to the current project").argument(
|
|
1161
|
+
).version("0.1.1");
|
|
1162
|
+
program.command("add").description("Add one or more composed components to the current project").argument(
|
|
1163
|
+
"<components...>",
|
|
1164
|
+
"component name(s), slug(s), or path(s) like labeled-select or components/ui"
|
|
1165
|
+
).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
1166
|
await runAdd(components, {
|
|
533
1167
|
cwd: opts.cwd,
|
|
534
1168
|
overwrite: opts.overwrite,
|
|
@@ -537,4 +1171,71 @@ program.command("add").description("Add one or more composed components to the c
|
|
|
537
1171
|
skipI18n: opts.skipI18n
|
|
538
1172
|
});
|
|
539
1173
|
});
|
|
1174
|
+
program.command("move").description(
|
|
1175
|
+
"Copy a composed component and its related files (controller, i18n) into another repository"
|
|
1176
|
+
).argument(
|
|
1177
|
+
"<components...>",
|
|
1178
|
+
"component name(s), slug(s), or path(s) like labeled-select or components/ui"
|
|
1179
|
+
).option("-t, --to <path>", "destination repository path", process.cwd()).option(
|
|
1180
|
+
"-f, --from <path>",
|
|
1181
|
+
"source boilerplate repository (optional; the published package uses its bundled registry)"
|
|
1182
|
+
).option("-o, --overwrite", "overwrite files that already exist", false).action(async (components, opts) => {
|
|
1183
|
+
await runMove(components, {
|
|
1184
|
+
to: opts.to,
|
|
1185
|
+
from: opts.from,
|
|
1186
|
+
overwrite: opts.overwrite
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
var create = program.command("create").description(
|
|
1190
|
+
"Install a feature that already exists in the boilerplate into a target project"
|
|
1191
|
+
).addHelpText(
|
|
1192
|
+
"after",
|
|
1193
|
+
`
|
|
1194
|
+
The boilerplate is the single source of truth for which features exist. This
|
|
1195
|
+
does not generate arbitrary/new features \u2014 it installs an existing, registered
|
|
1196
|
+
boilerplate feature's real files into a target project, in the implementation
|
|
1197
|
+
that matches the target project's own framework.
|
|
1198
|
+
|
|
1199
|
+
Boilerplate -> Feature Registry -> Feature -> Framework Implementation -> Target Project
|
|
1200
|
+
|
|
1201
|
+
See \`composed-cli create feature --help\` for the feature installer.`
|
|
1202
|
+
);
|
|
1203
|
+
create.command("feature").description(
|
|
1204
|
+
"Install an existing boilerplate feature (route, screen, API layer, translations) into a target project, using the implementation registered for that project's detected framework."
|
|
1205
|
+
).argument("<name>", "registered feature name, e.g. login, dashboard, about, contact").option("-c, --cwd <path>", "target project directory", process.cwd()).option("--protected", "require/assert the protected-route implementation").option("--public", "require/assert the public-route implementation").option("-o, --overwrite", "overwrite files that already exist", false).addHelpText(
|
|
1206
|
+
"after",
|
|
1207
|
+
`
|
|
1208
|
+
The target project's framework is detected automatically (package.json
|
|
1209
|
+
dependencies, then project structure) \u2014 it does not need to be a copy of this
|
|
1210
|
+
boilerplate. Only frameworks with a registered implementation for the
|
|
1211
|
+
requested feature are supported; run the command to see the registry's error
|
|
1212
|
+
message list available features or supported frameworks.
|
|
1213
|
+
|
|
1214
|
+
--protected / --public are validated against the resolved implementation's
|
|
1215
|
+
own route scope (e.g. dashboard is always protected, about is always public)
|
|
1216
|
+
rather than choosing it \u2014 pass one only to assert the scope you expect;
|
|
1217
|
+
omit both to just install whatever the registry defines.
|
|
1218
|
+
|
|
1219
|
+
Examples:
|
|
1220
|
+
$ composed-cli create feature login
|
|
1221
|
+
$ composed-cli create feature dashboard
|
|
1222
|
+
$ composed-cli create feature login --cwd ./next-app
|
|
1223
|
+
$ composed-cli create feature dashboard --cwd ./tanstack-app
|
|
1224
|
+
$ composed-cli create feature login --overwrite
|
|
1225
|
+
|
|
1226
|
+
Behavior:
|
|
1227
|
+
- Unknown feature name -> rejected, lists available features.
|
|
1228
|
+
- Feature exists but no implementation for the detected framework -> rejected, lists supported frameworks.
|
|
1229
|
+
- Next.js targets install App Router pages from cli/templates/nextjs/ and rewrite Paraglide to @/lib/composed-strings.
|
|
1230
|
+
- Every target file is checked for conflicts before anything is written; existing files are reported and left untouched unless --overwrite is passed.
|
|
1231
|
+
- Files are copied byte-for-byte from the boilerplate's real source tree \u2014 nothing is templated or invented.
|
|
1232
|
+
- Translation keys are merged into the target's messages/<locale>/*.json when that folder exists; skipped otherwise.`
|
|
1233
|
+
).action(async (name, opts) => {
|
|
1234
|
+
await runCreateFeature(name, {
|
|
1235
|
+
cwd: opts.cwd,
|
|
1236
|
+
protected: opts.protected ?? false,
|
|
1237
|
+
public: opts.public ?? false,
|
|
1238
|
+
overwrite: opts.overwrite
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
540
1241
|
program.parseAsync(process.argv);
|