@trineui/cli 0.1.0-beta.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # @trine/cli Changelog
2
+
3
+ ## 0.1.0-beta.1 (2026-03-22)
4
+
5
+ Initial beta release.
6
+
7
+ ### Commands
8
+
9
+ - `trine add <component>` — Generate blueprint + skin files from templates
10
+ - `trine diff <component>` — Show local and upstream status (read-only)
11
+ - `trine sync <component>` — Auto-overwrite untouched blueprint with upstream changes
12
+ - `trine sync <component> --interactive` — 3-way merge TUI for reviewing modified blueprints
13
+ - `trine eject <component>` — Merge blueprint + skin into standalone component, one-way exit from lifecycle
14
+ - `trine init` — Reconstruct manifest.json from existing blueprint files
15
+
16
+ ### Sync Model
17
+
18
+ - Normalized text hash v0.1 for change detection
19
+ - manifest.json (`.trine/manifest.json`) is source of truth for sync state
20
+ - Gate G1 validated: Prettier reformat does not trigger false-positive dirty signal
21
+
22
+ ### Installation
23
+
24
+ ```bash
25
+ npm install -g @trine/cli@beta
26
+ ```
27
+
28
+ ### Requirements
29
+
30
+ - Node.js >=18.0.0
package/dist/index.js ADDED
@@ -0,0 +1,662 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command6 } from "commander";
5
+
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;
69
+ }
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)");
219
+ }
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)}`);
266
+ });
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"
285
+ }
286
+ ]
287
+ });
288
+ if (choice === "upstream") {
289
+ applyHunk(lines, hunk, upstreamContent.split("\n"));
290
+ }
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";
299
+ }
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];
415
+ 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
+ ""
420
+ ];
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());
457
+ }
458
+ if (moduleName.includes("./") && moduleName.includes(".skin")) {
459
+ imports.push(imp.getText());
460
+ }
461
+ });
462
+ return imports;
463
+ }
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;
490
+ }
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");
504
+ }
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);
516
+ }
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);
530
+ }
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;
553
+ }
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`);
610
+ }
611
+ }
612
+ }
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();