@trineui/cli 0.1.0-beta.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/bin/trine.js +24 -0
- package/dist/add-button.js +15 -0
- package/dist/add-component.js +315 -0
- package/dist/index.js +73 -646
- package/package.json +23 -30
- package/templates/button/button.html +11 -0
- package/templates/button/button.skin.ts +128 -0
- package/templates/button/button.ts +39 -0
- package/templates/styles/tokens.css +102 -0
- package/templates/styles/trine-consumer.css +58 -0
- package/CHANGELOG.md +0 -30
- package/src/commands/add.ts +0 -101
- package/src/commands/diff.test.ts +0 -55
- package/src/commands/diff.ts +0 -104
- package/src/commands/eject.ts +0 -95
- package/src/commands/init.ts +0 -92
- package/src/commands/sync-interactive.ts +0 -108
- package/src/commands/sync.test.ts +0 -35
- package/src/commands/sync.ts +0 -113
- package/src/index.ts +0 -18
- package/src/types/manifest.ts +0 -14
- package/src/utils/__tests__/hash.test.ts +0 -35
- package/src/utils/__tests__/template.test.ts +0 -47
- package/src/utils/eject-merger.ts +0 -149
- package/src/utils/hash.ts +0 -43
- package/src/utils/manifest.ts +0 -43
- package/src/utils/template.ts +0 -26
- package/templates/button.blueprint.ts.hbs +0 -41
- package/templates/button.skin.ts.hbs +0 -35
- package/templates/checkbox.blueprint.ts.hbs +0 -57
- package/templates/checkbox.skin.ts.hbs +0 -44
- package/templates/dialog.blueprint.ts.hbs +0 -61
- package/templates/dialog.skin.ts.hbs +0 -27
- package/templates/input.blueprint.ts.hbs +0 -83
- package/templates/input.skin.ts.hbs +0 -29
- package/templates/select.blueprint.ts.hbs +0 -86
- package/templates/select.skin.ts.hbs +0 -53
- package/tsconfig.json +0 -10
package/dist/index.js
CHANGED
|
@@ -1,662 +1,89 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } 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
|
-
|
|
4
|
-
|
|
9
|
+
Defaults:
|
|
10
|
+
- current directory when it matches the supported Angular app target shape
|
|
11
|
+
- otherwise apps/demo for local Trine repo verification only
|
|
5
12
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
13
|
+
Notes:
|
|
14
|
+
- v0 supports Button only
|
|
15
|
+
- external targets can run trine add button from the app root or pass --target /absolute/path/to/angular-app
|
|
16
|
+
- the current proven target model is Angular 21 + src/app + src/styles.scss + tsconfig.app.json
|
|
17
|
+
- use a Node LTS line supported by Angular 21 in the target repo
|
|
18
|
+
- the current proven styling/runtime baseline requires Tailwind CSS v4 and class-variance-authority in the target repo
|
|
19
|
+
- consumer-owned component files fail clearly if they already exist
|
|
20
|
+
- shared styling baseline files are copied when missing and preserved when they already exist
|
|
21
|
+
- @trine/ui is configured as a consumer-local alias inside the target app
|
|
22
|
+
- apps/demo keeps a temporary @trine/ui/* bridge for non-localized components during local repo verification`;
|
|
23
|
+
const SUPPORTED_COMPONENTS = ['button'];
|
|
24
|
+
function main(argv) {
|
|
25
|
+
const [command, component, ...rest] = argv;
|
|
26
|
+
if (command !== 'add') {
|
|
27
|
+
throw new Error(command ? `Unsupported command: ${command}\n\n${HELP_TEXT}` : HELP_TEXT);
|
|
69
28
|
}
|
|
70
|
-
|
|
71
|
-
|
|
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)");
|
|
29
|
+
if (!isSupportedComponent(component)) {
|
|
30
|
+
throw new Error(component ? `Unsupported component: ${component}\n\n${HELP_TEXT}` : HELP_TEXT);
|
|
219
31
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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)}`);
|
|
32
|
+
const target = readTarget(rest) ?? defaultTarget(process.cwd());
|
|
33
|
+
const result = addButton({
|
|
34
|
+
target,
|
|
35
|
+
cwd: process.cwd(),
|
|
266
36
|
});
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
{
|
|
272
|
-
|
|
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"
|
|
37
|
+
printSuccess('button', result);
|
|
38
|
+
}
|
|
39
|
+
function readTarget(argv) {
|
|
40
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
41
|
+
if (argv[index] === '--target') {
|
|
42
|
+
return argv[index + 1];
|
|
285
43
|
}
|
|
286
|
-
]
|
|
287
|
-
});
|
|
288
|
-
if (choice === "upstream") {
|
|
289
|
-
applyHunk(lines, hunk, upstreamContent.split("\n"));
|
|
290
44
|
}
|
|
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";
|
|
45
|
+
return undefined;
|
|
299
46
|
}
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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];
|
|
47
|
+
function printSuccess(component, result) {
|
|
48
|
+
const relativeTarget = path.relative(process.cwd(), result.targetRoot) || '.';
|
|
49
|
+
const displayTarget = relativeTarget.startsWith('..') ? result.targetRoot : relativeTarget;
|
|
50
|
+
const componentLabel = capitalize(component);
|
|
415
51
|
const lines = [
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
52
|
+
`trine add ${component} completed.`,
|
|
53
|
+
`Target: ${displayTarget}`,
|
|
54
|
+
'',
|
|
55
|
+
'Copied files:',
|
|
56
|
+
...result.copiedFiles.map((file) => `- ${file}`),
|
|
57
|
+
'',
|
|
58
|
+
'Created or updated:',
|
|
59
|
+
...result.updatedFiles.map((file) => `- ${file}`),
|
|
420
60
|
];
|
|
421
|
-
|
|
422
|
-
|
|
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());
|
|
61
|
+
if (result.warnings.length > 0) {
|
|
62
|
+
lines.push('', 'Warnings:', ...result.warnings.map((warning) => `- ${warning}`));
|
|
457
63
|
}
|
|
458
|
-
|
|
459
|
-
|
|
64
|
+
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.`);
|
|
65
|
+
if (relativeTarget === 'apps/demo') {
|
|
66
|
+
lines.push('- Open /validation-shell and review the CLI delivery proof section for the temporary demo verification path.');
|
|
460
67
|
}
|
|
461
|
-
|
|
462
|
-
return imports;
|
|
68
|
+
console.log(lines.join('\n'));
|
|
463
69
|
}
|
|
464
|
-
function
|
|
465
|
-
|
|
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;
|
|
70
|
+
function isSupportedComponent(value) {
|
|
71
|
+
return SUPPORTED_COMPONENTS.some((component) => component === value);
|
|
490
72
|
}
|
|
491
|
-
function
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
73
|
+
function capitalize(value) {
|
|
74
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
75
|
+
}
|
|
76
|
+
function defaultTarget(cwd) {
|
|
77
|
+
return looksLikeAngularAppRoot(cwd) ? '.' : 'apps/demo';
|
|
78
|
+
}
|
|
79
|
+
function looksLikeAngularAppRoot(root) {
|
|
80
|
+
return ['src/app', 'src/styles.scss', 'tsconfig.app.json'].every((relativePath) => existsSync(path.join(root, relativePath)));
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
main(process.argv.slice(2));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
console.error(message);
|
|
88
|
+
process.exitCode = 1;
|
|
504
89
|
}
|
|
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();
|