@trineui/cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,8 @@ Notes:
16
16
 
17
17
  - `button` is the only supported component in this public-style baseline
18
18
  - omitting `--target` uses the current directory when it already matches the supported Angular app shape
19
- - when the current directory is not a supported Angular app target, omitting `--target` falls back to `apps/demo` for local Trine repo verification only
19
+ - when the current directory is not a supported Angular app target, the CLI auto-detects a single Angular app target under the current directory and proceeds automatically
20
+ - when multiple Angular app targets are found, the CLI stops and asks for `--target <app-root>`
20
21
  - external targets can run `trine add button` from the app root or pass an explicit app root such as `--target /absolute/path/to/angular-app`
21
22
  - the canonical public package name is `@trineui/cli`
22
23
  - the CLI command exposed through the package bin is still `trine`
@@ -30,6 +31,7 @@ Notes:
30
31
  - for `apps/demo`, `@trine/ui` resolves locally for delivered components while `@trine/ui/*` temporarily bridges non-localized components back to the authoring source
31
32
  - the delivered shared styling baseline is `tokens.css` + `trine-consumer.css`
32
33
  - the current proven target dependency baseline is Angular 21, Tailwind CSS v4, and `class-variance-authority`
34
+ - the current proven target shape accepts a global stylesheet entry such as `src/styles.scss`, `src/styles.css`, or `src/global.scss` when it is resolved from `angular.json`
33
35
  - use a Node LTS line supported by Angular 21 in the target repo; odd-numbered Node releases can build with warnings
34
36
  - when `package.json` is present in the target root, the CLI warns if Tailwind CSS v4 or `class-variance-authority` are missing
35
37
 
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import ts from 'typescript';
5
5
  const STYLE_SOURCE_FILES = ['styles/tokens.css', 'styles/trine-consumer.css'];
6
6
  const TEMPLATE_ROOT = fileURLToPath(new URL('../templates/', import.meta.url));
7
+ const LOCAL_REPO_DEMO_ROOT = path.resolve(TEMPLATE_ROOT, '../../../apps/demo');
7
8
  const STYLE_IMPORT_LINE = "@import './styles/trine-consumer.css';";
8
9
  const DEMO_BRIDGE_COMMENT = '// Temporary demo verification bridge: delivered local components resolve locally; other components still re-export from the authoring source.';
9
10
  const DEMO_LOCAL_COMPONENTS = [
@@ -60,10 +61,7 @@ const DEMO_PROXY_EXPORT_LINES = [
60
61
  ];
61
62
  export function addComponent(manifest, options) {
62
63
  const targetRoot = path.resolve(options.cwd, options.target);
63
- const targetAppDir = path.join(targetRoot, 'src', 'app');
64
- const targetStylesEntry = path.join(targetRoot, 'src', 'styles.scss');
65
- const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
66
- assertTargetShape(manifest.componentName, targetRoot, targetAppDir, targetStylesEntry, targetTsconfig);
64
+ const { targetStylesEntry, targetTsconfig } = resolveTargetShape(manifest.componentName, targetRoot);
67
65
  const componentDestDir = path.join(targetRoot, 'src', 'app', 'components', 'ui', manifest.componentName);
68
66
  const stylesDestDir = path.join(targetRoot, 'src', 'styles');
69
67
  const componentCopyTargets = manifest.sourceFiles.map((source) => ({
@@ -94,7 +92,7 @@ export function addComponent(manifest, options) {
94
92
  updatedFiles.push(toTargetRelativePath(targetRoot, componentBarrelPath));
95
93
  }
96
94
  const uiRootBarrelPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
97
- const uiRootUpdated = isDemoTarget(targetRoot, options.cwd)
95
+ const uiRootUpdated = isDemoTarget(targetRoot)
98
96
  ? rewriteDemoUiRootBarrel(uiRootBarrelPath)
99
97
  : ensureLinesFile(uiRootBarrelPath, [manifest.uiExportLine]);
100
98
  if (uiRootUpdated) {
@@ -110,7 +108,7 @@ export function addComponent(manifest, options) {
110
108
  if (stylesResult.authoringImportStillPresent) {
111
109
  warnings.push(`${toTargetRelativePath(targetRoot, targetStylesEntry)} still imports @trine/ui/styles/trine.css for the broader demo authoring baseline.`);
112
110
  }
113
- if (isDemoTarget(targetRoot, options.cwd)) {
111
+ if (isDemoTarget(targetRoot)) {
114
112
  warnings.push('apps/demo keeps non-localized components on a temporary @trine/ui/* bridge back to packages/ui so the full demo app can still build while delivered components resolve locally.');
115
113
  }
116
114
  else {
@@ -145,7 +143,10 @@ function ensureSharedStyleBaseline(targetRoot, stylesDestDir, componentLabel) {
145
143
  warnings,
146
144
  };
147
145
  }
148
- function assertTargetShape(componentName, targetRoot, targetAppDir, targetStylesEntry, targetTsconfig) {
146
+ function resolveTargetShape(componentName, targetRoot) {
147
+ const targetAppDir = path.join(targetRoot, 'src', 'app');
148
+ const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
149
+ const targetStylesEntry = resolveStylesEntry(targetRoot);
149
150
  const missing = [];
150
151
  if (!existsSync(targetRoot)) {
151
152
  missing.push(targetRoot);
@@ -153,18 +154,25 @@ function assertTargetShape(componentName, targetRoot, targetAppDir, targetStyles
153
154
  if (!existsSync(targetAppDir)) {
154
155
  missing.push(targetAppDir);
155
156
  }
156
- if (!existsSync(targetStylesEntry)) {
157
- missing.push(targetStylesEntry);
157
+ if (!targetStylesEntry) {
158
+ missing.push(`${path.join(targetRoot, 'src', 'styles.scss')} or ${path.join(targetRoot, 'src', 'styles.css')} or the first resolvable build styles entry in ${path.join(targetRoot, 'angular.json')}`);
158
159
  }
159
160
  if (!existsSync(targetTsconfig)) {
160
161
  missing.push(targetTsconfig);
161
162
  }
162
163
  if (missing.length > 0) {
163
164
  throw new Error([
164
- `trine add ${componentName} requires an Angular app target with src/app, src/styles.scss, and tsconfig.app.json.`,
165
+ `trine add ${componentName} requires an Angular app target with src/app, a global stylesheet entry, and tsconfig.app.json.`,
165
166
  ...missing.map((file) => `- ${file}`),
166
167
  ].join('\n'));
167
168
  }
169
+ if (!targetStylesEntry) {
170
+ throw new Error(`trine add ${componentName} could not resolve a global stylesheet entry for ${targetRoot}.`);
171
+ }
172
+ return {
173
+ targetStylesEntry,
174
+ targetTsconfig,
175
+ };
168
176
  }
169
177
  function ensureLinesFile(filePath, lines) {
170
178
  const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
@@ -213,14 +221,14 @@ function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
213
221
  const config = (parsed.config ?? {});
214
222
  config.compilerOptions ??= {};
215
223
  config.compilerOptions.paths ??= {};
216
- const aliasTarget = isDemoTarget(targetRoot, cwd)
217
- ? toPosixPath(path.relative(cwd, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
224
+ const aliasTarget = isDemoTarget(targetRoot)
225
+ ? toPosixPath(path.relative(process.cwd(), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
218
226
  : toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
219
227
  const wildcardTarget = 'packages/ui/*';
220
228
  const currentAlias = config.compilerOptions.paths['@trine/ui'];
221
229
  const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
222
230
  const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
223
- const wildcardIsCurrent = !isDemoTarget(targetRoot, cwd) ||
231
+ const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
224
232
  (Array.isArray(currentWildcardAlias) &&
225
233
  currentWildcardAlias.length === 1 &&
226
234
  currentWildcardAlias[0] === wildcardTarget);
@@ -228,7 +236,7 @@ function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
228
236
  return false;
229
237
  }
230
238
  config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
231
- if (isDemoTarget(targetRoot, cwd)) {
239
+ if (isDemoTarget(targetRoot)) {
232
240
  config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
233
241
  }
234
242
  else if ('@trine/ui/*' in config.compilerOptions.paths) {
@@ -265,8 +273,9 @@ function ensureStylesImport(stylesPath) {
265
273
  authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
266
274
  };
267
275
  }
268
- function isDemoTarget(targetRoot, cwd) {
269
- return path.resolve(targetRoot) === path.resolve(cwd, 'apps/demo');
276
+ function isDemoTarget(targetRoot) {
277
+ return (existsSync(LOCAL_REPO_DEMO_ROOT) &&
278
+ path.resolve(targetRoot) === path.resolve(LOCAL_REPO_DEMO_ROOT));
270
279
  }
271
280
  function toPosixPath(filePath) {
272
281
  return filePath.split(path.sep).join(path.posix.sep);
@@ -313,3 +322,41 @@ function readTargetDependencyWarnings(targetRoot, componentLabel) {
313
322
  function looksLikeTailwindV4(range) {
314
323
  return /(^|[^\d])4(\D|$)/.test(range);
315
324
  }
325
+ export function looksLikeAngularAppRoot(root) {
326
+ return (existsSync(path.join(root, 'src', 'app')) &&
327
+ existsSync(path.join(root, 'tsconfig.app.json')) &&
328
+ resolveStylesEntry(root) !== undefined);
329
+ }
330
+ function resolveStylesEntry(targetRoot) {
331
+ const conventionalStyles = ['src/styles.scss', 'src/styles.css'];
332
+ for (const relativePath of conventionalStyles) {
333
+ const absolutePath = path.join(targetRoot, relativePath);
334
+ if (existsSync(absolutePath)) {
335
+ return absolutePath;
336
+ }
337
+ }
338
+ const angularJsonPath = path.join(targetRoot, 'angular.json');
339
+ if (!existsSync(angularJsonPath)) {
340
+ return undefined;
341
+ }
342
+ try {
343
+ const angularJson = JSON.parse(readFileSync(angularJsonPath, 'utf8'));
344
+ for (const project of Object.values(angularJson.projects ?? {})) {
345
+ const styles = project.architect?.build?.options?.styles ?? project.targets?.build?.options?.styles ?? [];
346
+ for (const styleEntry of styles) {
347
+ const relativePath = typeof styleEntry === 'string' ? styleEntry : (styleEntry.input ?? undefined);
348
+ if (!relativePath) {
349
+ continue;
350
+ }
351
+ const absolutePath = path.join(targetRoot, relativePath);
352
+ if (existsSync(absolutePath)) {
353
+ return absolutePath;
354
+ }
355
+ }
356
+ }
357
+ }
358
+ catch {
359
+ return undefined;
360
+ }
361
+ return undefined;
362
+ }
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from 'node:fs';
2
+ import { readdirSync } from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { looksLikeAngularAppRoot } from "./add-component.js";
4
5
  import { addButton } from "./add-button.js";
5
6
  const HELP_TEXT = `Usage:
6
7
  npx @trineui/cli@latest add button [--target <app-root>]
@@ -8,12 +9,13 @@ const HELP_TEXT = `Usage:
8
9
 
9
10
  Defaults:
10
11
  - current directory when it matches the supported Angular app target shape
11
- - otherwise apps/demo for local Trine repo verification only
12
+ - otherwise auto-detect a single Angular app target under the current directory
13
+ - when multiple Angular app targets are found, re-run with --target <app-root>
12
14
 
13
15
  Notes:
14
16
  - v0 supports Button only
15
17
  - 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
18
+ - the current proven target model is Angular 21 + src/app + tsconfig.app.json + a global stylesheet entry such as src/styles.scss, src/styles.css, or src/global.scss resolved from angular.json
17
19
  - use a Node LTS line supported by Angular 21 in the target repo
18
20
  - the current proven styling/runtime baseline requires Tailwind CSS v4 and class-variance-authority in the target repo
19
21
  - consumer-owned component files fail clearly if they already exist
@@ -29,7 +31,7 @@ function main(argv) {
29
31
  if (!isSupportedComponent(component)) {
30
32
  throw new Error(component ? `Unsupported component: ${component}\n\n${HELP_TEXT}` : HELP_TEXT);
31
33
  }
32
- const target = readTarget(rest) ?? defaultTarget(process.cwd());
34
+ const target = readTarget(rest) ?? autoDetectTarget(process.cwd());
33
35
  const result = addButton({
34
36
  target,
35
37
  cwd: process.cwd(),
@@ -48,6 +50,7 @@ function printSuccess(component, result) {
48
50
  const relativeTarget = path.relative(process.cwd(), result.targetRoot) || '.';
49
51
  const displayTarget = relativeTarget.startsWith('..') ? result.targetRoot : relativeTarget;
50
52
  const componentLabel = capitalize(component);
53
+ const isRepoDemoVerification = result.warnings.some((warning) => warning.includes('temporary @trine/ui/* bridge'));
51
54
  const lines = [
52
55
  `trine add ${component} completed.`,
53
56
  `Target: ${displayTarget}`,
@@ -62,7 +65,7 @@ function printSuccess(component, result) {
62
65
  lines.push('', 'Warnings:', ...result.warnings.map((warning) => `- ${warning}`));
63
66
  }
64
67
  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') {
68
+ if (isRepoDemoVerification) {
66
69
  lines.push('- Open /validation-shell and review the CLI delivery proof section for the temporary demo verification path.');
67
70
  }
68
71
  console.log(lines.join('\n'));
@@ -73,11 +76,52 @@ function isSupportedComponent(value) {
73
76
  function capitalize(value) {
74
77
  return value.charAt(0).toUpperCase() + value.slice(1);
75
78
  }
76
- function defaultTarget(cwd) {
77
- return looksLikeAngularAppRoot(cwd) ? '.' : 'apps/demo';
79
+ function autoDetectTarget(cwd) {
80
+ if (looksLikeAngularAppRoot(cwd)) {
81
+ return '.';
82
+ }
83
+ const matches = findAngularAppTargets(cwd);
84
+ if (matches.length === 1) {
85
+ return matches[0];
86
+ }
87
+ if (matches.length > 1) {
88
+ throw new Error([
89
+ 'Multiple Angular app targets were found under the current directory. Re-run with --target <app-root>:',
90
+ ...matches.map((match) => `- ${match}`),
91
+ ].join('\n'));
92
+ }
93
+ return '.';
94
+ }
95
+ function findAngularAppTargets(root) {
96
+ const matches = new Set();
97
+ walkForAngularApps(root, root, matches);
98
+ return [...matches].sort((left, right) => left.localeCompare(right));
99
+ }
100
+ function walkForAngularApps(root, currentDir, matches) {
101
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
102
+ if (entry.isDirectory()) {
103
+ if (shouldSkipDirectory(entry.name)) {
104
+ continue;
105
+ }
106
+ walkForAngularApps(root, path.join(currentDir, entry.name), matches);
107
+ continue;
108
+ }
109
+ if (!entry.isFile() || entry.name !== 'tsconfig.app.json') {
110
+ continue;
111
+ }
112
+ const candidateRoot = currentDir;
113
+ if (!looksLikeAngularAppRoot(candidateRoot)) {
114
+ continue;
115
+ }
116
+ const relativeRoot = path.relative(root, candidateRoot) || '.';
117
+ matches.add(toPosixPath(relativeRoot));
118
+ }
119
+ }
120
+ function shouldSkipDirectory(name) {
121
+ return ['.angular', '.git', '.playwright-cli', 'dist', 'node_modules', 'output'].includes(name);
78
122
  }
79
- function looksLikeAngularAppRoot(root) {
80
- return ['src/app', 'src/styles.scss', 'tsconfig.app.json'].every((relativePath) => existsSync(path.join(root, relativePath)));
123
+ function toPosixPath(filePath) {
124
+ return filePath.split(path.sep).join(path.posix.sep);
81
125
  }
82
126
  try {
83
127
  main(process.argv.slice(2));
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@trineui/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Copy-paste ownership CLI for Trine UI components.",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {
8
- "trine": "./bin/trine.js"
8
+ "trine": "bin/trine.js"
9
9
  },
10
10
  "scripts": {
11
11
  "build": "tsc -p tsconfig.build.json",