@trineui/cli 0.1.0-beta.1 → 0.1.1

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.
Files changed (39) hide show
  1. package/README.md +41 -0
  2. package/bin/trine.js +24 -0
  3. package/dist/add-button.js +15 -0
  4. package/dist/add-component.js +317 -0
  5. package/dist/index.js +114 -641
  6. package/package.json +23 -30
  7. package/templates/button/button.html +11 -0
  8. package/templates/button/button.skin.ts +128 -0
  9. package/templates/button/button.ts +39 -0
  10. package/templates/styles/tokens.css +102 -0
  11. package/templates/styles/trine-consumer.css +58 -0
  12. package/CHANGELOG.md +0 -30
  13. package/src/commands/add.ts +0 -101
  14. package/src/commands/diff.test.ts +0 -55
  15. package/src/commands/diff.ts +0 -104
  16. package/src/commands/eject.ts +0 -95
  17. package/src/commands/init.ts +0 -92
  18. package/src/commands/sync-interactive.ts +0 -108
  19. package/src/commands/sync.test.ts +0 -35
  20. package/src/commands/sync.ts +0 -113
  21. package/src/index.ts +0 -18
  22. package/src/types/manifest.ts +0 -14
  23. package/src/utils/__tests__/hash.test.ts +0 -35
  24. package/src/utils/__tests__/template.test.ts +0 -47
  25. package/src/utils/eject-merger.ts +0 -149
  26. package/src/utils/hash.ts +0 -43
  27. package/src/utils/manifest.ts +0 -43
  28. package/src/utils/template.ts +0 -26
  29. package/templates/button.blueprint.ts.hbs +0 -41
  30. package/templates/button.skin.ts.hbs +0 -35
  31. package/templates/checkbox.blueprint.ts.hbs +0 -57
  32. package/templates/checkbox.skin.ts.hbs +0 -44
  33. package/templates/dialog.blueprint.ts.hbs +0 -61
  34. package/templates/dialog.skin.ts.hbs +0 -27
  35. package/templates/input.blueprint.ts.hbs +0 -83
  36. package/templates/input.skin.ts.hbs +0 -29
  37. package/templates/select.blueprint.ts.hbs +0 -86
  38. package/templates/select.skin.ts.hbs +0 -53
  39. package/tsconfig.json +0 -10
package/dist/index.js CHANGED
@@ -1,662 +1,135 @@
1
1
  #!/usr/bin/env node
2
+ import { existsSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { addButton } from "./add-button.js";
5
+ const HELP_TEXT = `Usage:
6
+ npx @trineui/cli@latest add button [--target <app-root>]
7
+ trine add button [--target <app-root>]
2
8
 
3
- // src/index.ts
4
- import { Command as Command6 } from "commander";
9
+ Defaults:
10
+ - current directory when it matches the supported Angular app target shape
11
+ - otherwise auto-detect a single Angular app target under the current directory
12
+ - when multiple Angular app targets are found, re-run with --target <app-root>
5
13
 
6
- // src/commands/add.ts
7
- import { Command } from "commander";
8
- import {
9
- writeFileSync as writeFileSync2,
10
- existsSync as existsSync3,
11
- mkdirSync as mkdirSync2
12
- } from "fs";
13
- import { resolve as resolve3, join } from "path";
14
-
15
- // src/utils/hash.ts
16
- import { createHash } from "crypto";
17
- function normalizeContent(content) {
18
- let normalized = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/'([^']*)'/g, '"$1"').replace(/`[^`]*`/g, "``").replace(/\s+/g, " ");
19
- normalized = normalized.replace(/,\s*}/g, "}").replace(/,\s*]/g, "]").replace(/,\s*\)/g, ")").replace(/\[\s*/g, "[").replace(/\s*\]/g, "]").replace(/\(\s*/g, "(").replace(/\s+\)/g, ")").replace(/{\s*/g, "{").replace(/\s*}/g, "}").replace(/{\s+}/g, "{}").replace(/\[\s+\]/g, "[]").replace(/\(\s+\)/g, "()").replace(/,\s*/g, ",").replace(/:\s*/g, ":");
20
- const statements = normalized.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
21
- const importStatements = statements.filter((s) => s.startsWith("import"));
22
- const otherStatements = statements.filter((s) => !s.startsWith("import"));
23
- importStatements.sort((a, b) => a.localeCompare(b));
24
- return [...importStatements, ...otherStatements].join("; ") + ";";
25
- }
26
- function computeNormalizedHash(content) {
27
- const normalized = normalizeContent(content);
28
- return "sha256-normalized:" + createHash("sha256").update(normalized).digest("hex");
29
- }
30
-
31
- // src/utils/manifest.ts
32
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
33
- import { resolve, dirname } from "path";
34
- var TRINE_DIR = ".trine";
35
- var MANIFEST_FILE = "manifest.json";
36
- function getManifestPath(projectRoot) {
37
- return resolve(projectRoot, TRINE_DIR, MANIFEST_FILE);
38
- }
39
- function readManifest(projectRoot) {
40
- const manifestPath = getManifestPath(projectRoot);
41
- if (!existsSync(manifestPath)) {
42
- return null;
43
- }
44
- try {
45
- const content = readFileSync(manifestPath, "utf-8");
46
- return JSON.parse(content);
47
- } catch {
48
- return null;
49
- }
50
- }
51
- function writeManifest(manifest, projectRoot) {
52
- const manifestPath = getManifestPath(projectRoot);
53
- const dir = dirname(manifestPath);
54
- if (!existsSync(dir)) {
55
- mkdirSync(dir, { recursive: true });
56
- }
57
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
58
- }
59
-
60
- // src/utils/template.ts
61
- import Handlebars from "handlebars";
62
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
63
- import { resolve as resolve2 } from "path";
64
- function findMonorepoRoot(cwd) {
65
- let dir = cwd;
66
- for (; ; ) {
67
- if (existsSync2(resolve2(dir, "pnpm-workspace.yaml"))) {
68
- return dir;
14
+ Notes:
15
+ - v0 supports Button only
16
+ - external targets can run trine add button from the app root or pass --target /absolute/path/to/angular-app
17
+ - the current proven target model is Angular 21 + src/app + src/styles.scss + tsconfig.app.json
18
+ - use a Node LTS line supported by Angular 21 in the target repo
19
+ - the current proven styling/runtime baseline requires Tailwind CSS v4 and class-variance-authority in the target repo
20
+ - consumer-owned component files fail clearly if they already exist
21
+ - shared styling baseline files are copied when missing and preserved when they already exist
22
+ - @trine/ui is configured as a consumer-local alias inside the target app
23
+ - apps/demo keeps a temporary @trine/ui/* bridge for non-localized components during local repo verification`;
24
+ const SUPPORTED_COMPONENTS = ['button'];
25
+ function main(argv) {
26
+ const [command, component, ...rest] = argv;
27
+ if (command !== 'add') {
28
+ throw new Error(command ? `Unsupported command: ${command}\n\n${HELP_TEXT}` : HELP_TEXT);
69
29
  }
70
- const parent = resolve2(dir, "..");
71
- if (parent === dir) break;
72
- dir = parent;
73
- }
74
- return cwd;
75
- }
76
- var monorepoRoot = findMonorepoRoot(process.cwd());
77
- var templatesDir = resolve2(monorepoRoot, "packages/cli/templates");
78
- function renderTemplate(templateName, context) {
79
- const templatePath = resolve2(templatesDir, templateName);
80
- const templateSource = readFileSync2(templatePath, "utf-8");
81
- const template = Handlebars.compile(templateSource);
82
- return template(context);
83
- }
84
-
85
- // src/commands/add.ts
86
- var ENGINE_VERSION = "0.1.0";
87
- var BLUEPRINT_SCHEMA_VERSION = "1.0.0";
88
- var addCommand = new Command("add").argument("<component>", "Component name (e.g. button)").option("--force", "Overwrite existing blueprint (skin is NEVER overwritten)").option(
89
- "--dir <path>",
90
- "Output directory relative to project root",
91
- "src/components/ui"
92
- ).description("Add a component blueprint + skin to your project").action(async (component, options) => {
93
- const projectRoot = process.cwd();
94
- const componentDir = resolve3(projectRoot, options.dir, component);
95
- const blueprintPath = join(componentDir, `${component}.blueprint.ts`);
96
- const skinPath = join(componentDir, `${component}.skin.ts`);
97
- if (!isSupportedComponent(component)) {
98
- console.error(`\u2717 Unknown component: "${component}"`);
99
- console.error(` Supported: button, input, dialog, checkbox, select`);
100
- process.exit(1);
101
- }
102
- if (existsSync3(blueprintPath) && !options.force) {
103
- console.error(`\u2717 ${component}.blueprint.ts already exists`);
104
- console.error("");
105
- console.error(` To overwrite blueprint (skin will NOT be touched):`);
106
- console.error(` trine add ${component} --force`);
107
- process.exit(1);
108
- }
109
- const ctx = {
110
- engineVersion: ENGINE_VERSION,
111
- blueprintSchemaVersion: BLUEPRINT_SCHEMA_VERSION
112
- };
113
- const blueprintContent = renderTemplate(`${component}.blueprint.ts.hbs`, ctx);
114
- const skinContent = renderTemplate(`${component}.skin.ts.hbs`, ctx);
115
- mkdirSync2(componentDir, { recursive: true });
116
- writeFileSync2(blueprintPath, blueprintContent, "utf-8");
117
- console.log(` \u2713 ${options.dir}/${component}/${component}.blueprint.ts`);
118
- if (!existsSync3(skinPath)) {
119
- writeFileSync2(skinPath, skinContent, "utf-8");
120
- console.log(` \u2713 ${options.dir}/${component}/${component}.skin.ts`);
121
- } else {
122
- console.log(` ~ ${options.dir}/${component}/${component}.skin.ts (kept \u2014 user-owned)`);
123
- }
124
- const existingManifest = readManifest(projectRoot);
125
- const manifest = existingManifest ?? {
126
- "trine-spec": "1.0",
127
- components: {}
128
- };
129
- const blueprintHash = computeNormalizedHash(blueprintContent);
130
- const entry = {
131
- "engine-version": ENGINE_VERSION,
132
- "blueprint-schema-version": BLUEPRINT_SCHEMA_VERSION,
133
- "blueprint-hash": blueprintHash,
134
- "blueprint-modified": false,
135
- "synced-at": (/* @__PURE__ */ new Date()).toISOString(),
136
- "sync-model": "normalized-text-v0.1",
137
- "output-dir": options.dir
138
- };
139
- manifest.components[component] = entry;
140
- writeManifest(manifest, projectRoot);
141
- console.log(` \u2713 .trine/manifest.json`);
142
- console.log("");
143
- console.log(`\u2713 trine add ${component} complete`);
144
- console.log("");
145
- console.log(" Next steps:");
146
- console.log(` Import ${capitalize(component)}Component in your module/component`);
147
- console.log(` Run: trine diff ${component} \u2014 check for upstream changes`);
148
- });
149
- function capitalize(s) {
150
- return s.charAt(0).toUpperCase() + s.slice(1);
151
- }
152
- function isSupportedComponent(name) {
153
- return ["button", "input", "dialog", "checkbox", "select"].includes(name);
154
- }
155
-
156
- // src/commands/diff.ts
157
- import { Command as Command2 } from "commander";
158
- import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
159
- import { resolve as resolve4 } from "path";
160
- import { createTwoFilesPatch } from "diff";
161
- var diffCommand = new Command2("diff").argument("<component>", "Component name (e.g. button)").option("--no-color", "Disable colored output").description("Show upstream changes since last sync (read-only, no files changed)").action(async (component, _options) => {
162
- const projectRoot = process.cwd();
163
- const manifest = readManifest(projectRoot);
164
- if (!manifest?.components[component]) {
165
- console.error(`\u2717 "${component}" not found in manifest`);
166
- console.error(` Run: trine add ${component}`);
167
- process.exit(1);
168
- }
169
- const entry = manifest.components[component];
170
- const blueprintPath = resolve4(
171
- projectRoot,
172
- entry["output-dir"],
173
- component,
174
- `${component}.blueprint.ts`
175
- );
176
- if (!existsSync4(blueprintPath)) {
177
- console.error(`\u2717 Blueprint file not found: ${blueprintPath}`);
178
- console.error(` Run: trine add ${component}`);
179
- process.exit(1);
180
- }
181
- const currentContent = readFileSync3(blueprintPath, "utf-8");
182
- const currentHash = computeNormalizedHash(currentContent);
183
- const manifestHash = entry["blueprint-hash"];
184
- const isLocalModified = currentHash !== manifestHash;
185
- const upstreamContent = renderTemplate(`${component}.blueprint.ts.hbs`, {
186
- engineVersion: entry["engine-version"],
187
- blueprintSchemaVersion: entry["blueprint-schema-version"]
188
- });
189
- const upstreamHash = computeNormalizedHash(upstreamContent);
190
- const hasUpstream = upstreamHash !== manifestHash;
191
- const safeToSync = !isLocalModified && hasUpstream;
192
- console.log("");
193
- console.log(`${component} blueprint:`);
194
- console.log(
195
- ` local status: ${isLocalModified ? "\u26A0 modified (local changes detected)" : "\u2713 clean (unmodified)"}`
196
- );
197
- console.log(
198
- ` upstream status: ${hasUpstream ? `updates available (engine: ${entry["engine-version"]} \u2192 check changelog)` : "\u2713 up to date"}`
199
- );
200
- console.log(
201
- ` safe to sync: ${safeToSync ? "yes \u2014 run: trine sync " + component : isLocalModified ? "no \u2014 run: trine sync " + component + " --interactive" : "nothing to sync"}`
202
- );
203
- if (hasUpstream) {
204
- console.log("");
205
- console.log("--- upstream changes ---");
206
- const patch = createTwoFilesPatch(
207
- `${component}.blueprint.ts (current)`,
208
- `${component}.blueprint.ts (upstream)`,
209
- currentContent,
210
- upstreamContent,
211
- "",
212
- "",
213
- { context: 3 }
214
- );
215
- const lines = patch.split("\n").slice(0, 50);
216
- console.log(lines.join("\n"));
217
- if (patch.split("\n").length > 50) {
218
- console.log(" ... (truncated, use trine sync --interactive for full diff)");
30
+ if (!isSupportedComponent(component)) {
31
+ throw new Error(component ? `Unsupported component: ${component}\n\n${HELP_TEXT}` : HELP_TEXT);
219
32
  }
220
- }
221
- console.log("");
222
- console.log(`${component} skin:`);
223
- console.log(` (no upstream \u2014 user-owned, never synced)`);
224
- console.log("");
225
- });
226
-
227
- // src/commands/sync.ts
228
- import { Command as Command3 } from "commander";
229
- import { writeFileSync as writeFileSync4, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
230
- import { resolve as resolve5 } from "path";
231
-
232
- // src/commands/sync-interactive.ts
233
- import { createTwoFilesPatch as createTwoFilesPatch2, parsePatch } from "diff";
234
- import { select } from "@inquirer/prompts";
235
- import { writeFileSync as writeFileSync3 } from "fs";
236
- async function runInteractiveSync(component, currentContent, upstreamContent, blueprintPath, manifest) {
237
- const patch = createTwoFilesPatch2(
238
- `${component}.blueprint.ts (yours)`,
239
- `${component}.blueprint.ts (upstream)`,
240
- currentContent,
241
- upstreamContent,
242
- "",
243
- "",
244
- { context: 3 }
245
- );
246
- const parsed = parsePatch(patch);
247
- const hunks = parsed.length > 0 && "hunks" in parsed[0] ? parsed[0].hunks : [];
248
- if (hunks.length === 0) {
249
- console.log("\u2713 No differences found.");
250
- return "merged";
251
- }
252
- console.log(
253
- `
254
- Blueprint has local changes. Upstream has ${hunks.length} changed hunk(s).
255
- `
256
- );
257
- const lines = currentContent.split("\n");
258
- for (let i = 0; i < hunks.length; i++) {
259
- const hunk = hunks[i];
260
- console.log(`--- Hunk ${i + 1}/${hunks.length} ---`);
261
- hunk.lines.forEach((line) => {
262
- if (line.startsWith("+"))
263
- console.log(` upstream: ${line.slice(1)}`);
264
- else if (line.startsWith("-"))
265
- console.log(` yours: ${line.slice(1)}`);
33
+ const target = readTarget(rest) ?? autoDetectTarget(process.cwd());
34
+ const result = addButton({
35
+ target,
36
+ cwd: process.cwd(),
266
37
  });
267
- console.log("");
268
- const choice = await select({
269
- message: "What do you want to do with this hunk?",
270
- choices: [
271
- {
272
- name: "(k) Keep mine",
273
- value: "keep",
274
- description: "Keep your local version of this hunk"
275
- },
276
- {
277
- name: "(u) Take upstream",
278
- value: "upstream",
279
- description: "Replace with upstream version"
280
- },
281
- {
282
- name: "(s) Skip (keep mine)",
283
- value: "skip",
284
- description: "Skip this hunk, keep your version"
38
+ printSuccess('button', result);
39
+ }
40
+ function readTarget(argv) {
41
+ for (let index = 0; index < argv.length; index += 1) {
42
+ if (argv[index] === '--target') {
43
+ return argv[index + 1];
285
44
  }
286
- ]
287
- });
288
- if (choice === "upstream") {
289
- applyHunk(lines, hunk, upstreamContent.split("\n"));
290
45
  }
291
- }
292
- const mergedContent = lines.join("\n");
293
- writeFileSync3(blueprintPath, mergedContent, "utf-8");
294
- manifest.components[component]["blueprint-hash"] = computeNormalizedHash(mergedContent);
295
- manifest.components[component]["blueprint-modified"] = false;
296
- manifest.components[component]["synced-at"] = (/* @__PURE__ */ new Date()).toISOString();
297
- console.log("\n\u2713 Merged. Writing blueprint...");
298
- return "merged";
46
+ return undefined;
299
47
  }
300
- function applyHunk(lines, hunk, upstreamLines) {
301
- const start = hunk.oldStart - 1;
302
- const end = start + hunk.oldLines;
303
- const upstreamSection = upstreamLines.slice(start, end);
304
- lines.splice(start, hunk.oldLines, ...upstreamSection);
305
- }
306
-
307
- // src/commands/sync.ts
308
- var syncCommand = new Command3("sync").argument("<component>", "Component name (e.g. button)").option("--interactive", "Interactive 3-way merge for resolving conflicts").option("--dry-run", "Show what would be synced without making changes").description("Sync blueprint with latest upstream version").action(async (component, options) => {
309
- const projectRoot = process.cwd();
310
- const manifest = readManifest(projectRoot);
311
- if (!manifest?.components[component]) {
312
- console.error(`\u2717 "${component}" not found in manifest`);
313
- console.error(` Run: trine add ${component}`);
314
- process.exit(1);
315
- }
316
- const entry = manifest.components[component];
317
- const blueprintPath = resolve5(
318
- projectRoot,
319
- entry["output-dir"],
320
- component,
321
- `${component}.blueprint.ts`
322
- );
323
- if (!existsSync5(blueprintPath)) {
324
- console.error(`\u2717 Blueprint file not found: ${blueprintPath}`);
325
- console.error(` Run: trine add ${component}`);
326
- process.exit(1);
327
- }
328
- const currentContent = readFileSync4(blueprintPath, "utf-8");
329
- const currentHash = computeNormalizedHash(currentContent);
330
- const manifestHash = entry["blueprint-hash"];
331
- const upstreamContent = renderTemplate(`${component}.blueprint.ts.hbs`, {
332
- engineVersion: entry["engine-version"],
333
- blueprintSchemaVersion: entry["blueprint-schema-version"]
334
- });
335
- const upstreamHash = computeNormalizedHash(upstreamContent);
336
- if (options.interactive) {
337
- if (currentHash === manifestHash) {
338
- console.log("\u2713 No local changes to merge.");
339
- return;
340
- }
341
- const result = await runInteractiveSync(
342
- component,
343
- currentContent,
344
- upstreamContent,
345
- blueprintPath,
346
- manifest
347
- );
348
- if (result === "merged") {
349
- writeManifest(manifest, projectRoot);
350
- console.log("\u2713 manifest.json updated.");
351
- }
352
- return;
353
- }
354
- if (currentHash !== manifestHash) {
355
- console.error("\u2717 Cannot auto-sync: blueprint has local modifications");
356
- console.error("");
357
- console.error(` Local changes detected in: ${component}.blueprint.ts`);
358
- console.error("");
359
- console.error(" Options:");
360
- console.error(` trine sync ${component} --interactive Resolve conflicts interactively`);
361
- console.error(` trine diff ${component} See what changed upstream`);
362
- console.error(` trine add ${component} --force Overwrite (loses your changes)`);
363
- console.error("");
364
- console.error(' Tip: Run "trine diff" first to understand the changes.');
365
- process.exit(1);
366
- }
367
- if (upstreamHash === manifestHash) {
368
- console.log(`\u2713 ${component} blueprint already up to date`);
369
- console.log("");
370
- console.log(" Nothing to sync.");
371
- return;
372
- }
373
- if (options.dryRun) {
374
- console.log(`[dry-run] Would sync ${component} blueprint`);
375
- console.log(`[dry-run] Blueprint: ${entry["output-dir"]}/${component}/${component}.blueprint.ts`);
376
- console.log("[dry-run] Skin: (not touched - user-owned)");
377
- return;
378
- }
379
- writeFileSync4(blueprintPath, upstreamContent, "utf-8");
380
- const newHash = computeNormalizedHash(upstreamContent);
381
- entry["blueprint-hash"] = newHash;
382
- entry["synced-at"] = (/* @__PURE__ */ new Date()).toISOString();
383
- writeManifest(manifest, projectRoot);
384
- console.log(`\u2713 ${component} blueprint synced successfully`);
385
- console.log("");
386
- console.log(" Updates applied:");
387
- console.log(" blueprint: updated");
388
- console.log(" skin: unchanged (user-owned)");
389
- console.log("");
390
- console.log(` Run: trine diff ${component} \u2014 check for more upstream changes`);
391
- });
392
-
393
- // src/commands/eject.ts
394
- import { confirm } from "@inquirer/prompts";
395
- import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
396
- import { resolve as resolve6 } from "path";
397
- import { Command as Command4 } from "commander";
398
-
399
- // src/utils/eject-merger.ts
400
- import { Project } from "ts-morph";
401
- function mergeForEject(component, blueprintContent, skinContent) {
402
- try {
403
- const project = new Project({ useInMemoryFileSystem: true });
404
- const blueprintFile = project.createSourceFile(
405
- "blueprint.ts",
406
- blueprintContent
407
- );
408
- const componentClass = blueprintFile.getClass(
409
- `${capitalize2(component)}Component`
410
- );
411
- if (!componentClass) {
412
- return structuredFallback(component, blueprintContent, skinContent);
413
- }
414
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
48
+ function printSuccess(component, result) {
49
+ const relativeTarget = path.relative(process.cwd(), result.targetRoot) || '.';
50
+ const displayTarget = relativeTarget.startsWith('..') ? result.targetRoot : relativeTarget;
51
+ const componentLabel = capitalize(component);
52
+ const isRepoDemoVerification = result.warnings.some((warning) => warning.includes('temporary @trine/ui/* bridge'));
415
53
  const lines = [
416
- `// Ejected from Trine on ${date}.`,
417
- `// This component is no longer managed by trine sync.`,
418
- `// @trine/engine is still required as a dependency.`,
419
- ""
54
+ `trine add ${component} completed.`,
55
+ `Target: ${displayTarget}`,
56
+ '',
57
+ 'Copied files:',
58
+ ...result.copiedFiles.map((file) => `- ${file}`),
59
+ '',
60
+ 'Created or updated:',
61
+ ...result.updatedFiles.map((file) => `- ${file}`),
420
62
  ];
421
- const imports = collectImports(blueprintFile);
422
- lines.push(...imports);
423
- lines.push("");
424
- lines.push("@Component({");
425
- const decoratorProps = extractDecoratorProps(componentClass, component);
426
- lines.push(...decoratorProps);
427
- lines.push("})");
428
- lines.push(`export class ${capitalize2(component)}Component {`);
429
- lines.push("");
430
- lines.push(` protected engine = inject(${capitalize2(component)}Engine);`);
431
- lines.push(` protected skin = inject(${capitalize2(component)}Skin);`);
432
- lines.push("}");
433
- lines.push("");
434
- return lines.join("\n");
435
- } catch {
436
- return structuredFallback(component, blueprintContent, skinContent);
437
- }
438
- }
439
- function capitalize2(s) {
440
- return s.charAt(0).toUpperCase() + s.slice(1);
441
- }
442
- function collectImports(blueprintFile) {
443
- const imports = [];
444
- imports.push(
445
- `import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';`
446
- );
447
- const engineImport = blueprintFile.getImportDeclarations().find(
448
- (imp) => imp.getModuleSpecifier().getText().match(/@trine(ui)?\/engine/)
449
- );
450
- if (engineImport) {
451
- imports.push(engineImport.getText().replace(/"/g, "'"));
452
- }
453
- blueprintFile.getImportDeclarations().forEach((imp) => {
454
- const moduleName = imp.getModuleSpecifier().getText();
455
- if (moduleName.includes("class-variance-authority")) {
456
- imports.push(imp.getText());
63
+ if (result.warnings.length > 0) {
64
+ lines.push('', 'Warnings:', ...result.warnings.map((warning) => `- ${warning}`));
457
65
  }
458
- if (moduleName.includes("./") && moduleName.includes(".skin")) {
459
- imports.push(imp.getText());
66
+ lines.push('', 'Manual next steps:', `- Ensure the target repo has Tailwind CSS v4 and class-variance-authority installed before building the delivered ${componentLabel}.`, `- Build the target app and confirm ${componentLabel} imports resolve through the local @trine/ui alias.`, `- Review the copied ${component}.skin.ts and tokens.css if you want local consumer customization.`);
67
+ if (isRepoDemoVerification) {
68
+ lines.push('- Open /validation-shell and review the CLI delivery proof section for the temporary demo verification path.');
460
69
  }
461
- });
462
- return imports;
70
+ console.log(lines.join('\n'));
463
71
  }
464
- function extractDecoratorProps(componentClass, component) {
465
- const props = [];
466
- props.push(` selector: "ui-${component}",`);
467
- props.push(" standalone: true,");
468
- const decoratorSource = componentClass.getText();
469
- const templateMatch = decoratorSource.match(/template:\s*`[^`]*`/s);
470
- if (templateMatch) {
471
- props.push(` ${templateMatch[0]},`);
472
- }
473
- const cdMatch = decoratorSource.match(/changeDetection:\s*ChangeDetectionStrategy\.\w+/);
474
- if (cdMatch) {
475
- props.push(` ${cdMatch[0]},`);
476
- }
477
- const hostMatch = decoratorSource.match(/host:\s*\{[\s\S]*?\n\s*\}/);
478
- if (hostMatch) {
479
- props.push(` ${hostMatch[0]},`);
480
- }
481
- const hostDirectivesMatch = decoratorSource.match(/hostDirectives:\s*\[[\s\S]*?\n\s*\]/);
482
- if (hostDirectivesMatch) {
483
- props.push(` ${hostDirectivesMatch[0]},`);
484
- }
485
- const providersMatch = decoratorSource.match(/providers:\s*\[[\s\S]*?\n\s*\]/);
486
- if (providersMatch) {
487
- props.push(` ${providersMatch[0]},`);
488
- }
489
- return props;
72
+ function isSupportedComponent(value) {
73
+ return SUPPORTED_COMPONENTS.some((component) => component === value);
490
74
  }
491
- function structuredFallback(_component, blueprintContent, skinContent) {
492
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
493
- return [
494
- `// Ejected from Trine on ${date}.`,
495
- `// ts-morph merge failed \u2014 using structured fallback.`,
496
- `// Merge manually using the sections below.`,
497
- "",
498
- `// ===== BLUEPRINT =====`,
499
- blueprintContent,
500
- "",
501
- `// ===== SKIN =====`,
502
- skinContent
503
- ].join("\n\n");
75
+ function capitalize(value) {
76
+ return value.charAt(0).toUpperCase() + value.slice(1);
504
77
  }
505
-
506
- // src/commands/eject.ts
507
- var ejectCommand = new Command4("eject").argument("<component>", "Component name to eject (e.g. button)").option("--yes", "Skip confirmation prompt").description(
508
- "Merge blueprint + skin into a standalone component. One-way \u2014 no re-attach."
509
- ).action(
510
- async (component, options) => {
511
- const projectRoot = process.cwd();
512
- const manifest = readManifest(projectRoot);
513
- if (!manifest?.components[component]) {
514
- console.error(`\u2717 "${component}" not found in manifest`);
515
- process.exit(1);
78
+ function looksLikeAngularAppRoot(root) {
79
+ return ['src/app', 'src/styles.scss', 'tsconfig.app.json'].every((relativePath) => existsSync(path.join(root, relativePath)));
80
+ }
81
+ function autoDetectTarget(cwd) {
82
+ if (looksLikeAngularAppRoot(cwd)) {
83
+ return '.';
516
84
  }
517
- const entry = manifest.components[component];
518
- const outputDir = resolve6(projectRoot, entry["output-dir"], component);
519
- const blueprintPath = resolve6(
520
- outputDir,
521
- `${component}.blueprint.ts`
522
- );
523
- const skinPath = resolve6(outputDir, `${component}.skin.ts`);
524
- const outputPath = resolve6(outputDir, `${component}.component.ts`);
525
- if (!existsSync6(blueprintPath) || !existsSync6(skinPath)) {
526
- console.error(
527
- `\u2717 Blueprint or skin file not found for "${component}"`
528
- );
529
- process.exit(1);
85
+ const matches = findAngularAppTargets(cwd);
86
+ if (matches.length === 1) {
87
+ return matches[0];
530
88
  }
531
- console.log("");
532
- console.log("\u26A0\uFE0F trine eject \u2014 THIS IS ONE-WAY");
533
- console.log("");
534
- console.log(` Component: ${component}`);
535
- console.log(` Output: ${component}.component.ts`);
536
- console.log(
537
- ` Removes: ${component}.blueprint.ts + ${component}.skin.ts`
538
- );
539
- console.log(` Manifest: ${component} entry will be deleted`);
540
- console.log("");
541
- console.log(" Warning: This cannot be undone.");
542
- console.log(
543
- " The component will no longer receive upstream updates."
544
- );
545
- console.log("");
546
- const confirmed = options.yes || await confirm({
547
- message: `Eject "${component}"? This is permanent.`,
548
- default: false
549
- });
550
- if (!confirmed) {
551
- console.log("Aborted.");
552
- return;
89
+ if (matches.length > 1) {
90
+ throw new Error([
91
+ 'Multiple Angular app targets were found under the current directory. Re-run with --target <app-root>:',
92
+ ...matches.map((match) => `- ${match}`),
93
+ ].join('\n'));
553
94
  }
554
- const blueprintContent = readFileSync5(blueprintPath, "utf-8");
555
- const skinContent = readFileSync5(skinPath, "utf-8");
556
- const merged = mergeForEject(
557
- component,
558
- blueprintContent,
559
- skinContent
560
- );
561
- writeFileSync5(outputPath, merged, "utf-8");
562
- console.log(` \u2713 ${component}.component.ts written`);
563
- unlinkSync(blueprintPath);
564
- unlinkSync(skinPath);
565
- console.log(` \u2713 ${component}.blueprint.ts removed`);
566
- console.log(` \u2713 ${component}.skin.ts removed`);
567
- delete manifest.components[component];
568
- writeManifest(manifest, projectRoot);
569
- console.log(` \u2713 manifest.json updated`);
570
- console.log("");
571
- console.log(`\u2713 ${component} ejected.`);
572
- console.log(
573
- ` Edit ${component}.component.ts directly going forward.`
574
- );
575
- }
576
- );
577
-
578
- // src/commands/init.ts
579
- import { Command as Command5 } from "commander";
580
- import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
581
- import { resolve as resolve7, join as join2 } from "path";
582
- var SUPPORTED_COMPONENTS = ["button", "input", "dialog", "checkbox", "select"];
583
- var initCommand = new Command5("init").description("Scan project and reconstruct manifest.json from existing blueprint files").action(async () => {
584
- const projectRoot = process.cwd();
585
- const manifest = readManifest(projectRoot);
586
- if (manifest) {
587
- console.log("\u2713 .trine/manifest.json already exists");
588
- console.log(" No need to run trine init.");
589
- console.log("");
590
- console.log(" To check sync status, run: trine diff <component>");
591
- return;
592
- }
593
- console.log("Scanning for blueprint files...");
594
- console.log("");
595
- const foundComponents = [];
596
- const scanDirs = ["src/components/ui", "components/ui"];
597
- for (const dir of scanDirs) {
598
- const basePath = resolve7(projectRoot, dir);
599
- if (!existsSync7(basePath)) continue;
600
- for (const component of SUPPORTED_COMPONENTS) {
601
- const blueprintPath = join2(basePath, component, `${component}.blueprint.ts`);
602
- if (existsSync7(blueprintPath)) {
603
- try {
604
- const content = readFileSync6(blueprintPath, "utf-8");
605
- const hash = computeNormalizedHash(content);
606
- foundComponents.push({ name: component, path: blueprintPath, hash });
607
- console.log(` \u2713 Found: ${dir}/${component}/${component}.blueprint.ts`);
608
- } catch {
609
- console.log(` \u2717 Error reading: ${dir}/${component}/${component}.blueprint.ts`);
95
+ return '.';
96
+ }
97
+ function findAngularAppTargets(root) {
98
+ const matches = new Set();
99
+ walkForAngularApps(root, root, matches);
100
+ return [...matches].sort((left, right) => left.localeCompare(right));
101
+ }
102
+ function walkForAngularApps(root, currentDir, matches) {
103
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
104
+ if (entry.isDirectory()) {
105
+ if (shouldSkipDirectory(entry.name)) {
106
+ continue;
107
+ }
108
+ walkForAngularApps(root, path.join(currentDir, entry.name), matches);
109
+ continue;
110
+ }
111
+ if (!entry.isFile() || entry.name !== 'tsconfig.app.json') {
112
+ continue;
113
+ }
114
+ const candidateRoot = currentDir;
115
+ if (!looksLikeAngularAppRoot(candidateRoot)) {
116
+ continue;
610
117
  }
611
- }
118
+ const relativeRoot = path.relative(root, candidateRoot) || '.';
119
+ matches.add(toPosixPath(relativeRoot));
612
120
  }
613
- }
614
- if (foundComponents.length === 0) {
615
- console.log("No blueprint files found.");
616
- console.log("");
617
- console.log(" To add a component, run:");
618
- console.log(" trine add <component>");
619
- console.log("");
620
- console.log(" Supported components: button, input, dialog, checkbox, select");
621
- return;
622
- }
623
- console.log("");
624
- console.log(`Found ${foundComponents.length} blueprint file(s)`);
625
- console.log("");
626
- const newManifest = {
627
- "trine-spec": "1.0",
628
- components: {}
629
- };
630
- for (const { name, hash } of foundComponents) {
631
- const entry = {
632
- "engine-version": "0.1.0",
633
- "blueprint-schema-version": "1.0.0",
634
- "blueprint-hash": hash,
635
- "blueprint-modified": true,
636
- "synced-at": (/* @__PURE__ */ new Date()).toISOString(),
637
- "sync-model": "normalized-text-v0.1",
638
- "output-dir": "src/components/ui"
639
- };
640
- newManifest.components[name] = entry;
641
- }
642
- writeManifest(newManifest, projectRoot);
643
- console.log("\u2713 Created .trine/manifest.json");
644
- console.log("");
645
- console.log("\u26A0 Warning: blueprint-modified is set to true for all components.");
646
- console.log(" This is conservative \u2014 run trine diff <component> to verify sync state.");
647
- console.log("");
648
- console.log(" Next steps:");
649
- console.log(" trine diff button \u2014 check button sync status");
650
- console.log(" trine diff input \u2014 check input sync status");
651
- console.log(" ...");
652
- });
653
-
654
- // src/index.ts
655
- var program = new Command6();
656
- program.name("trine").description("Trine CLI").version("0.1.0");
657
- program.addCommand(addCommand);
658
- program.addCommand(diffCommand);
659
- program.addCommand(syncCommand);
660
- program.addCommand(ejectCommand);
661
- program.addCommand(initCommand);
662
- program.parse();
121
+ }
122
+ function shouldSkipDirectory(name) {
123
+ return ['.angular', '.git', '.playwright-cli', 'dist', 'node_modules', 'output'].includes(name);
124
+ }
125
+ function toPosixPath(filePath) {
126
+ return filePath.split(path.sep).join(path.posix.sep);
127
+ }
128
+ try {
129
+ main(process.argv.slice(2));
130
+ }
131
+ catch (error) {
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ console.error(message);
134
+ process.exitCode = 1;
135
+ }