@trineui/cli 0.2.0 → 0.3.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 CHANGED
@@ -16,17 +16,22 @@ Registry status note:
16
16
  Repository-local current command surface:
17
17
 
18
18
  ```bash
19
- trine init --target <app-root>
19
+ trine init [--target <app-root>] [--yes]
20
20
  trine add button --target <app-root>
21
21
  ```
22
22
 
23
23
  Notes:
24
24
 
25
25
  - `init` is now the preferred first step for project readiness; `add button` still works without it for backward compatibility
26
+ - `init` is guided by default: it detects the target, surfaces warnings, previews file changes, and asks for confirmation before mutation
27
+ - `init --yes` keeps a non-interactive fast path for automation and scripting
26
28
  - `button` is the only supported component in this public-style baseline
27
29
  - omitting `--target` uses the current directory when it already matches the supported Angular app shape
28
30
  - 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
29
- - when multiple Angular app targets are found, the CLI stops and asks for `--target <app-root>`
31
+ - when multiple Angular app targets are found, guided `init` lets the user choose one interactively
32
+ - when multiple Angular app targets are found, `init --yes` and `add button` fail clearly and ask for `--target <app-root>`
33
+ - when multiple plausible stylesheet entries are found, guided `init` lets the user choose one interactively
34
+ - when multiple plausible stylesheet entries are found, `init --yes` fails clearly and asks the user to rerun without `--yes`
30
35
  - 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`
31
36
  - `init` detects `angular` and `ionic-angular` targets conservatively; unsupported targets fail clearly
32
37
  - the canonical public package name is `@trineui/cli`
@@ -36,6 +41,7 @@ Notes:
36
41
  - packaged/public-style proof uses a packed local tarball to simulate `npx @trineui/cli@latest add button`
37
42
  - the packaged CLI ships compiled runtime files plus Button templates so it can execute from `node_modules` in a real `npx`-style flow
38
43
  - `init` ensures `src/styles/tokens.css`, `src/styles/trine-consumer.css`, `src/app/components/ui/index.ts`, the local `@trine/ui` alias, and local stylesheet wiring
44
+ - rerunning `init` on an already prepared target exits cleanly with a no-op summary instead of prompting again
39
45
  - consumer-owned component destination files cause a clear failure
40
46
  - existing shared baseline files (`tokens.css` and `trine-consumer.css`) are preserved so a second component can be added into the same target repo
41
47
  - the command copies consumer-owned source instead of wiring runtime back to `packages/ui`
package/dist/index.js CHANGED
@@ -1,23 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
3
  import { addButton } from "./add-button.js";
4
- import { initProject } from "./init.js";
5
- import { findAngularAppTargets, looksLikeAngularAppRoot } from "./project.js";
4
+ import { applyInitPlan, planInitProject } from "./init.js";
5
+ import { chooseFromList, confirmAction } from "./prompt.js";
6
+ import { findAngularAppTargets, inspectProjectTarget, looksLikeAngularAppRoot } from "./project.js";
6
7
  const HELP_TEXT = `Usage:
7
- npx @trineui/cli@latest init [--target <app-root>]
8
+ npx @trineui/cli@latest init [--target <app-root>] [--yes]
8
9
  npx @trineui/cli@latest add button [--target <app-root>]
9
- trine init [--target <app-root>]
10
+ trine init [--target <app-root>] [--yes]
10
11
  trine add button [--target <app-root>]
11
12
 
12
13
  Defaults:
13
- - current directory when it already matches the supported Trine app target shape
14
- - otherwise auto-detect a single supported Angular app target under the current directory
15
- - when multiple supported app targets are found, re-run with --target <app-root>
14
+ - init is guided by default and asks for confirmation before mutating project files
15
+ - init --yes keeps a non-interactive fast path for automation and scripting
16
+ - current directory is used when it already matches the supported Trine app target shape
17
+ - otherwise init/add auto-detect a single supported Angular app target under the current directory
18
+ - when multiple supported app targets are found, guided init lets you choose and add still asks for --target
16
19
 
17
20
  Notes:
18
21
  - v0 supports init plus add button only
19
22
  - init owns target detection, framework detection, stylesheet resolution, baseline files, local @trine/ui alias setup, and local stylesheet wiring
20
- - add button still works without init for backward compatibility, but init is now the preferred first step
23
+ - add button still works without init for backward compatibility, but init is the preferred first step
21
24
  - 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 directly or from angular.json
22
25
  - v0 distinguishes angular and ionic-angular targets; unsupported frameworks fail clearly
23
26
  - the current proven styling/runtime baseline requires Tailwind CSS v4 and class-variance-authority in the target repo
@@ -26,13 +29,32 @@ Notes:
26
29
  - @trine/ui is configured as a consumer-local alias inside the target app
27
30
  - apps/demo keeps a temporary @trine/ui/* bridge for non-localized components during local repo verification`;
28
31
  const SUPPORTED_COMPONENTS = ['button'];
29
- function main(argv) {
32
+ async function main(argv) {
30
33
  const [command, secondArg, ...rest] = argv;
31
34
  if (command === 'init') {
32
- const selection = resolveTargetSelection(process.cwd(), argv.slice(1), 'trine init');
33
- const result = initProject({
35
+ const flags = parseFlags(argv.slice(1), ['--target', '--yes']);
36
+ const selection = flags.yes
37
+ ? resolveNonInteractiveInitSelection(process.cwd(), flags.target)
38
+ : await resolveGuidedInitSelection(process.cwd(), flags.target);
39
+ const plan = planInitProject({
34
40
  target: selection.target,
35
41
  cwd: process.cwd(),
42
+ targetStylesEntry: selection.targetStylesEntry,
43
+ });
44
+ if (!hasInitMutations(plan)) {
45
+ printInitNoop(plan, selection);
46
+ return;
47
+ }
48
+ if (!flags.yes) {
49
+ printInitPreview(plan, selection);
50
+ const shouldProceed = await confirmAction('Proceed? (Y/n)');
51
+ if (!shouldProceed) {
52
+ console.log('trine init cancelled. No changes were made.');
53
+ return;
54
+ }
55
+ }
56
+ const result = applyInitPlan(plan, {
57
+ cwd: process.cwd(),
36
58
  });
37
59
  printInitSuccess(result, selection);
38
60
  return;
@@ -43,23 +65,54 @@ function main(argv) {
43
65
  if (!isSupportedComponent(secondArg)) {
44
66
  throw new Error(secondArg ? `Unsupported component: ${secondArg}\n\n${HELP_TEXT}` : HELP_TEXT);
45
67
  }
46
- const selection = resolveTargetSelection(process.cwd(), rest, `trine add ${secondArg}`);
68
+ const flags = parseFlags(rest, ['--target']);
69
+ const selection = resolveTargetSelection(process.cwd(), flags.target, `trine add ${secondArg}`);
47
70
  const result = addButton({
48
71
  target: selection.target,
49
72
  cwd: process.cwd(),
50
73
  });
51
74
  printAddSuccess(secondArg, result, selection);
52
75
  }
53
- function readTarget(argv) {
76
+ function resolveNonInteractiveInitSelection(cwd, explicitTarget) {
77
+ const selection = resolveTargetSelection(cwd, explicitTarget, 'trine init');
78
+ const targetRoot = path.resolve(cwd, selection.target);
79
+ const inspection = inspectProjectTarget(targetRoot);
80
+ if (inspection.framework !== 'unsupported' &&
81
+ inspection.targetStylesEntryCandidates.length > 1 &&
82
+ inspection.targetStylesEntryResolution === 'default') {
83
+ const displayTarget = selection.mode === 'cwd' ? '.' : toDisplayTarget(cwd, targetRoot);
84
+ throw new Error([
85
+ `Multiple plausible global stylesheet entries were found for target (${displayTarget}). Re-run without --yes to choose one interactively:`,
86
+ ...inspection.targetStylesEntryCandidates.map((candidate) => `- ${toDisplayFilePath(targetRoot, candidate)}`),
87
+ ].join('\n'));
88
+ }
89
+ return selection;
90
+ }
91
+ function parseFlags(argv, allowedFlags) {
92
+ const flags = {
93
+ yes: false,
94
+ };
54
95
  for (let index = 0; index < argv.length; index += 1) {
55
- if (argv[index] === '--target') {
56
- return argv[index + 1];
96
+ const token = argv[index];
97
+ if (!allowedFlags.includes(token)) {
98
+ throw new Error(`Unsupported flag: ${token}\n\n${HELP_TEXT}`);
99
+ }
100
+ if (token === '--yes') {
101
+ flags.yes = true;
102
+ continue;
103
+ }
104
+ if (token === '--target') {
105
+ const target = argv[index + 1];
106
+ if (!target || target.startsWith('--')) {
107
+ throw new Error(`--target requires a value.\n\n${HELP_TEXT}`);
108
+ }
109
+ flags.target = target;
110
+ index += 1;
57
111
  }
58
112
  }
59
- return undefined;
113
+ return flags;
60
114
  }
61
- function resolveTargetSelection(cwd, argv, commandLabel) {
62
- const explicitTarget = readTarget(argv);
115
+ function resolveTargetSelection(cwd, explicitTarget, commandLabel) {
63
116
  if (explicitTarget) {
64
117
  return {
65
118
  target: explicitTarget,
@@ -90,6 +143,92 @@ function resolveTargetSelection(cwd, argv, commandLabel) {
90
143
  mode: 'cwd',
91
144
  };
92
145
  }
146
+ async function resolveGuidedInitSelection(cwd, explicitTarget) {
147
+ const targetSelection = explicitTarget
148
+ ? {
149
+ target: explicitTarget,
150
+ mode: 'explicit',
151
+ }
152
+ : await chooseInitTarget(cwd);
153
+ const targetRoot = path.resolve(cwd, targetSelection.target);
154
+ const inspection = inspectProjectTarget(targetRoot);
155
+ if (inspection.framework === 'unsupported') {
156
+ return targetSelection;
157
+ }
158
+ if (inspection.targetStylesEntryCandidates.length > 1 &&
159
+ inspection.targetStylesEntryResolution === 'default') {
160
+ const targetStylesEntry = await chooseFromList('Multiple plausible global stylesheet entries were found:', inspection.targetStylesEntryCandidates, {
161
+ renderItem: (item, index) => `${String(index + 1)}. ${toDisplayFilePath(targetRoot, item)}`,
162
+ });
163
+ return {
164
+ ...targetSelection,
165
+ targetStylesEntry,
166
+ };
167
+ }
168
+ return targetSelection;
169
+ }
170
+ async function chooseInitTarget(cwd) {
171
+ if (looksLikeAngularAppRoot(cwd)) {
172
+ return {
173
+ target: '.',
174
+ mode: 'cwd',
175
+ };
176
+ }
177
+ const matches = findAngularAppTargets(cwd);
178
+ if (matches.length === 1) {
179
+ return {
180
+ target: matches[0],
181
+ mode: 'auto-detected',
182
+ };
183
+ }
184
+ if (matches.length > 1) {
185
+ const selected = await chooseFromList('Multiple supported apps found:', matches);
186
+ return {
187
+ target: selected,
188
+ mode: 'selected',
189
+ };
190
+ }
191
+ return {
192
+ target: '.',
193
+ mode: 'cwd',
194
+ };
195
+ }
196
+ function printInitPreview(plan, selection) {
197
+ const displayTarget = toDisplayTarget(process.cwd(), plan.targetRoot);
198
+ const stylesheetDisplay = toDisplayFilePath(plan.targetRoot, plan.targetStylesEntry);
199
+ const lines = [
200
+ `Detected target: ${displayTarget}`,
201
+ `Detected framework: ${plan.framework}`,
202
+ `Detected stylesheet entry: ${stylesheetDisplay}`,
203
+ ];
204
+ if (selection.mode === 'auto-detected' || selection.mode === 'selected') {
205
+ lines.push(`Selected app: ${displayTarget}`);
206
+ }
207
+ lines.push('', 'Trine will:', ...renderPlannedInitActions(plan));
208
+ if (plan.warnings.length > 0) {
209
+ lines.push('', 'Warnings:', ...plan.warnings.map((warning) => `- ${warning}`));
210
+ }
211
+ console.log(lines.join('\n'));
212
+ }
213
+ function printInitNoop(plan, selection) {
214
+ const displayTarget = toDisplayTarget(process.cwd(), plan.targetRoot);
215
+ const stylesheetDisplay = toDisplayFilePath(plan.targetRoot, plan.targetStylesEntry);
216
+ const lines = [
217
+ 'trine init found the Trine baseline already in place.',
218
+ `Target: ${displayTarget}`,
219
+ `Framework: ${plan.framework}`,
220
+ `Resolved stylesheet entry: ${stylesheetDisplay}`,
221
+ ];
222
+ if (selection.mode === 'auto-detected' || selection.mode === 'selected') {
223
+ lines.push(`Selected app: ${displayTarget}`);
224
+ }
225
+ lines.push('', 'Already in place:', ...renderFileGroup(plan.reusedFiles, '- nothing to report'));
226
+ if (plan.warnings.length > 0) {
227
+ lines.push('', 'Warnings:', ...plan.warnings.map((warning) => `- ${warning}`));
228
+ }
229
+ lines.push('', 'No changes were made.');
230
+ console.log(lines.join('\n'));
231
+ }
93
232
  function printInitSuccess(result, selection) {
94
233
  const displayTarget = toDisplayTarget(process.cwd(), result.targetRoot);
95
234
  const stylesheetDisplay = toDisplayFilePath(result.targetRoot, result.targetStylesEntry);
@@ -99,7 +238,7 @@ function printInitSuccess(result, selection) {
99
238
  `Framework: ${result.framework}`,
100
239
  `Resolved stylesheet entry: ${stylesheetDisplay}`,
101
240
  ];
102
- if (selection.mode === 'auto-detected') {
241
+ if (selection.mode === 'auto-detected' || selection.mode === 'selected') {
103
242
  lines.push(`Selected app: ${displayTarget}`);
104
243
  }
105
244
  lines.push('', 'Created:', ...renderFileGroup(result.createdFiles, '- nothing created'), '', 'Reused:', ...renderFileGroup(result.reusedFiles, '- nothing reused'), '', 'Updated:', ...renderFileGroup(result.updatedFiles, '- nothing updated'));
@@ -130,6 +269,16 @@ function printAddSuccess(component, result, selection) {
130
269
  function renderFileGroup(files, emptyMessage) {
131
270
  return files.length > 0 ? files.map((file) => `- ${file}`) : [emptyMessage];
132
271
  }
272
+ function renderPlannedInitActions(plan) {
273
+ return [
274
+ ...plan.createdFiles.map((file) => `- create ${file}`),
275
+ ...plan.updatedFiles.map((file) => `- update ${file}`),
276
+ ...plan.reusedFiles.map((file) => `- keep ${file}`),
277
+ ];
278
+ }
279
+ function hasInitMutations(plan) {
280
+ return plan.createdFiles.length > 0 || plan.updatedFiles.length > 0;
281
+ }
133
282
  function isSupportedComponent(value) {
134
283
  return SUPPORTED_COMPONENTS.some((component) => component === value);
135
284
  }
@@ -143,11 +292,8 @@ function toDisplayTarget(cwd, targetRoot) {
143
292
  function toDisplayFilePath(targetRoot, filePath) {
144
293
  return path.relative(targetRoot, filePath) || '.';
145
294
  }
146
- try {
147
- main(process.argv.slice(2));
148
- }
149
- catch (error) {
295
+ await main(process.argv.slice(2)).catch((error) => {
150
296
  const message = error instanceof Error ? error.message : String(error);
151
297
  console.error(message);
152
298
  process.exitCode = 1;
153
- }
299
+ });
package/dist/init.js CHANGED
@@ -1,17 +1,19 @@
1
1
  import path from 'node:path';
2
- import { ensureSharedStyleBaseline, ensureStylesImport, ensureTsconfigAlias, ensureUiRootBarrel, isDemoTarget, readTargetDependencyWarnings, resolveSupportedProjectTarget, toTargetRelativePath, } from "./project.js";
3
- export function initProject(options) {
2
+ import { ensureSharedStyleBaseline, ensureStylesImport, ensureTsconfigAlias, ensureUiRootBarrel, inspectSharedStyleBaseline, inspectStylesImport, inspectTsconfigAlias, inspectUiRootBarrel, isDemoTarget, readTargetDependencyWarnings, resolveSupportedProjectTarget, toTargetRelativePath, } from "./project.js";
3
+ export function planInitProject(options) {
4
4
  const targetRoot = path.resolve(options.cwd, options.target);
5
- const { framework, targetStylesEntry, targetTsconfig } = resolveSupportedProjectTarget('trine init', targetRoot);
5
+ const { framework, targetStylesEntry, targetTsconfig } = resolveSupportedProjectTarget('trine init', targetRoot, {
6
+ preferredStylesEntry: options.targetStylesEntry,
7
+ });
6
8
  const createdFiles = [];
7
9
  const updatedFiles = [];
8
10
  const reusedFiles = [];
9
11
  const warnings = [];
10
- const sharedStylesResult = ensureSharedStyleBaseline(targetRoot, 'Trine components');
12
+ const sharedStylesResult = inspectSharedStyleBaseline(targetRoot, 'Trine components');
11
13
  createdFiles.push(...sharedStylesResult.createdFiles);
12
14
  reusedFiles.push(...sharedStylesResult.reusedFiles);
13
15
  warnings.push(...sharedStylesResult.warnings);
14
- const uiRootResult = ensureUiRootBarrel(targetRoot);
16
+ const uiRootResult = inspectUiRootBarrel(targetRoot);
15
17
  if (uiRootResult.created) {
16
18
  createdFiles.push(uiRootResult.file);
17
19
  }
@@ -19,15 +21,15 @@ export function initProject(options) {
19
21
  reusedFiles.push(uiRootResult.file);
20
22
  }
21
23
  const tsconfigDisplayPath = toTargetRelativePath(targetRoot, targetTsconfig);
22
- if (ensureTsconfigAlias(targetTsconfig, targetRoot, options.cwd)) {
24
+ if (inspectTsconfigAlias(targetTsconfig, targetRoot, options.cwd).needsUpdate) {
23
25
  updatedFiles.push(tsconfigDisplayPath);
24
26
  }
25
27
  else {
26
28
  reusedFiles.push(tsconfigDisplayPath);
27
29
  }
28
30
  const stylesDisplayPath = toTargetRelativePath(targetRoot, targetStylesEntry);
29
- const stylesResult = ensureStylesImport(targetStylesEntry);
30
- if (stylesResult.updated) {
31
+ const stylesResult = inspectStylesImport(targetStylesEntry);
32
+ if (stylesResult.needsUpdate) {
31
33
  updatedFiles.push(stylesDisplayPath);
32
34
  }
33
35
  else {
@@ -44,6 +46,7 @@ export function initProject(options) {
44
46
  }
45
47
  return {
46
48
  targetRoot,
49
+ targetTsconfig,
47
50
  framework,
48
51
  targetStylesEntry,
49
52
  createdFiles,
@@ -52,3 +55,15 @@ export function initProject(options) {
52
55
  warnings,
53
56
  };
54
57
  }
58
+ export function applyInitPlan(plan, options) {
59
+ ensureSharedStyleBaseline(plan.targetRoot, 'Trine components');
60
+ ensureUiRootBarrel(plan.targetRoot);
61
+ ensureTsconfigAlias(plan.targetTsconfig, plan.targetRoot, options.cwd);
62
+ ensureStylesImport(plan.targetStylesEntry);
63
+ return plan;
64
+ }
65
+ export function initProject(options) {
66
+ return applyInitPlan(planInitProject(options), {
67
+ cwd: options.cwd,
68
+ });
69
+ }
package/dist/project.js CHANGED
@@ -8,10 +8,11 @@ const CONVENTIONAL_STYLE_ENTRY_PATHS = ['src/styles.scss', 'src/styles.css', 'sr
8
8
  export const TEMPLATE_ROOT = fileURLToPath(new URL('../templates/', import.meta.url));
9
9
  const LOCAL_REPO_ROOT = path.resolve(TEMPLATE_ROOT, '../../..');
10
10
  export const LOCAL_REPO_DEMO_ROOT = path.resolve(TEMPLATE_ROOT, '../../../apps/demo');
11
- export function inspectProjectTarget(targetRoot) {
11
+ export function inspectProjectTarget(targetRoot, options = {}) {
12
12
  const targetAppDir = path.join(targetRoot, 'src', 'app');
13
13
  const targetTsconfig = path.join(targetRoot, 'tsconfig.app.json');
14
- const targetStylesEntry = resolveStylesEntry(targetRoot);
14
+ const targetStylesEntryCandidates = resolveStylesEntryCandidates(targetRoot);
15
+ const { targetStylesEntry, targetStylesEntryResolution } = resolveStylesEntrySelection(targetRoot, targetStylesEntryCandidates, options.preferredStylesEntry);
15
16
  const missingPaths = [];
16
17
  if (!existsSync(targetRoot)) {
17
18
  missingPaths.push(targetRoot);
@@ -30,13 +31,15 @@ export function inspectProjectTarget(targetRoot) {
30
31
  targetRoot,
31
32
  targetAppDir,
32
33
  targetTsconfig,
34
+ targetStylesEntryCandidates,
33
35
  targetStylesEntry,
36
+ targetStylesEntryResolution,
34
37
  framework,
35
38
  missingPaths,
36
39
  };
37
40
  }
38
- export function resolveSupportedProjectTarget(commandLabel, targetRoot) {
39
- const inspection = inspectProjectTarget(targetRoot);
41
+ export function resolveSupportedProjectTarget(commandLabel, targetRoot, options = {}) {
42
+ const inspection = inspectProjectTarget(targetRoot, options);
40
43
  if (inspection.framework === 'unsupported' || !inspection.targetStylesEntry) {
41
44
  throw new Error([
42
45
  `${commandLabel} requires a supported Angular or Ionic Angular app target with src/app, a global stylesheet entry, and tsconfig.app.json.`,
@@ -60,17 +63,31 @@ export function looksLikeAngularAppRoot(root) {
60
63
  return inspectProjectTarget(root).framework !== 'unsupported';
61
64
  }
62
65
  export function ensureSharedStyleBaseline(targetRoot, componentLabel) {
66
+ const inspection = inspectSharedStyleBaseline(targetRoot, componentLabel);
67
+ const stylesDestDir = path.join(targetRoot, 'src', 'styles');
68
+ mkdirSync(stylesDestDir, { recursive: true });
69
+ for (const sourceFile of STYLE_SOURCE_FILES) {
70
+ const templatePath = path.join(TEMPLATE_ROOT, sourceFile);
71
+ const destinationPath = path.join(stylesDestDir, path.basename(sourceFile));
72
+ if (!inspection.createdFiles.includes(toTargetRelativePath(targetRoot, destinationPath))) {
73
+ continue;
74
+ }
75
+ if (!existsSync(destinationPath)) {
76
+ copyFileSync(templatePath, destinationPath);
77
+ }
78
+ }
79
+ return inspection;
80
+ }
81
+ export function inspectSharedStyleBaseline(targetRoot, componentLabel) {
63
82
  const stylesDestDir = path.join(targetRoot, 'src', 'styles');
64
83
  const createdFiles = [];
65
84
  const reusedFiles = [];
66
85
  const warnings = [];
67
- mkdirSync(stylesDestDir, { recursive: true });
68
86
  for (const sourceFile of STYLE_SOURCE_FILES) {
69
87
  const templatePath = path.join(TEMPLATE_ROOT, sourceFile);
70
88
  const destinationPath = path.join(stylesDestDir, path.basename(sourceFile));
71
89
  const relativeDestination = toTargetRelativePath(targetRoot, destinationPath);
72
90
  if (!existsSync(destinationPath)) {
73
- copyFileSync(templatePath, destinationPath);
74
91
  createdFiles.push(relativeDestination);
75
92
  continue;
76
93
  }
@@ -88,16 +105,18 @@ export function ensureSharedStyleBaseline(targetRoot, componentLabel) {
88
105
  }
89
106
  export function ensureUiRootBarrel(targetRoot) {
90
107
  const uiRootPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
91
- if (existsSync(uiRootPath)) {
92
- return {
93
- created: false,
94
- file: toTargetRelativePath(targetRoot, uiRootPath),
95
- };
108
+ const inspection = inspectUiRootBarrel(targetRoot);
109
+ if (!inspection.created) {
110
+ return inspection;
96
111
  }
97
112
  mkdirSync(path.dirname(uiRootPath), { recursive: true });
98
113
  writeFileSync(uiRootPath, '\n');
114
+ return inspection;
115
+ }
116
+ export function inspectUiRootBarrel(targetRoot) {
117
+ const uiRootPath = path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts');
99
118
  return {
100
- created: true,
119
+ created: !existsSync(uiRootPath),
101
120
  file: toTargetRelativePath(targetRoot, uiRootPath),
102
121
  };
103
122
  }
@@ -122,6 +141,10 @@ export function ensureLinesFile(filePath, lines) {
122
141
  return changed;
123
142
  }
124
143
  export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
144
+ const inspection = inspectTsconfigAlias(tsconfigPath, targetRoot, cwd);
145
+ if (!inspection.needsUpdate) {
146
+ return false;
147
+ }
125
148
  const currentText = readFileSync(tsconfigPath, 'utf8');
126
149
  const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
127
150
  if (parsed.error) {
@@ -130,20 +153,7 @@ export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
130
153
  const config = (parsed.config ?? {});
131
154
  config.compilerOptions ??= {};
132
155
  config.compilerOptions.paths ??= {};
133
- const aliasTarget = isDemoTarget(targetRoot)
134
- ? toPosixPath(path.relative(LOCAL_REPO_ROOT, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
135
- : toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
136
- const wildcardTarget = 'packages/ui/*';
137
- const currentAlias = config.compilerOptions.paths['@trine/ui'];
138
- const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
139
- const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
140
- const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
141
- (Array.isArray(currentWildcardAlias) &&
142
- currentWildcardAlias.length === 1 &&
143
- currentWildcardAlias[0] === wildcardTarget);
144
- if (aliasIsCurrent && wildcardIsCurrent) {
145
- return false;
146
- }
156
+ const { aliasTarget, wildcardTarget } = buildAliasTargets(tsconfigPath, targetRoot);
147
157
  config.compilerOptions.paths['@trine/ui'] = [aliasTarget];
148
158
  if (isDemoTarget(targetRoot)) {
149
159
  config.compilerOptions.paths['@trine/ui/*'] = [wildcardTarget];
@@ -154,14 +164,41 @@ export function ensureTsconfigAlias(tsconfigPath, targetRoot, cwd) {
154
164
  writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
155
165
  return true;
156
166
  }
167
+ export function inspectTsconfigAlias(tsconfigPath, targetRoot, cwd) {
168
+ const currentText = readFileSync(tsconfigPath, 'utf8');
169
+ const parsed = ts.parseConfigFileTextToJson(tsconfigPath, currentText);
170
+ if (parsed.error) {
171
+ throw new Error(`Unable to parse ${path.relative(cwd, tsconfigPath)} as JSONC.`);
172
+ }
173
+ const config = (parsed.config ?? {});
174
+ config.compilerOptions ??= {};
175
+ config.compilerOptions.paths ??= {};
176
+ const { aliasTarget, wildcardTarget } = buildAliasTargets(tsconfigPath, targetRoot);
177
+ const currentAlias = config.compilerOptions.paths['@trine/ui'];
178
+ const currentWildcardAlias = config.compilerOptions.paths['@trine/ui/*'];
179
+ const aliasIsCurrent = Array.isArray(currentAlias) && currentAlias.length === 1 && currentAlias[0] === aliasTarget;
180
+ const wildcardIsCurrent = !isDemoTarget(targetRoot) ||
181
+ (Array.isArray(currentWildcardAlias) &&
182
+ currentWildcardAlias.length === 1 &&
183
+ currentWildcardAlias[0] === wildcardTarget);
184
+ if (aliasIsCurrent && wildcardIsCurrent) {
185
+ return {
186
+ needsUpdate: false,
187
+ };
188
+ }
189
+ return {
190
+ needsUpdate: true,
191
+ };
192
+ }
157
193
  export function ensureStylesImport(stylesPath) {
158
- const current = readFileSync(stylesPath, 'utf8');
159
- if (current.includes(STYLE_IMPORT_LINE)) {
194
+ const inspection = inspectStylesImport(stylesPath);
195
+ if (!inspection.needsUpdate) {
160
196
  return {
161
197
  updated: false,
162
- authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
198
+ authoringImportStillPresent: inspection.authoringImportStillPresent,
163
199
  };
164
200
  }
201
+ const current = readFileSync(stylesPath, 'utf8');
165
202
  const lines = current.split('\n');
166
203
  let insertAt = -1;
167
204
  for (let index = 0; index < lines.length; index += 1) {
@@ -179,6 +216,13 @@ export function ensureStylesImport(stylesPath) {
179
216
  writeFileSync(stylesPath, lines.join('\n'));
180
217
  return {
181
218
  updated: true,
219
+ authoringImportStillPresent: inspection.authoringImportStillPresent,
220
+ };
221
+ }
222
+ export function inspectStylesImport(stylesPath) {
223
+ const current = readFileSync(stylesPath, 'utf8');
224
+ return {
225
+ needsUpdate: !current.includes(STYLE_IMPORT_LINE),
182
226
  authoringImportStillPresent: current.includes("@import '@trine/ui/styles/trine.css';"),
183
227
  };
184
228
  }
@@ -215,20 +259,23 @@ export function readTargetDependencyWarnings(targetRoot, componentLabel) {
215
259
  }
216
260
  }
217
261
  export function resolveStylesEntry(targetRoot) {
262
+ return inspectProjectTarget(targetRoot).targetStylesEntry;
263
+ }
264
+ export function resolveStylesEntryCandidates(targetRoot) {
265
+ const candidates = new Set();
218
266
  for (const relativePath of CONVENTIONAL_STYLE_ENTRY_PATHS) {
219
267
  const absolutePath = path.join(targetRoot, relativePath);
220
268
  if (existsSync(absolutePath)) {
221
- return absolutePath;
269
+ candidates.add(absolutePath);
222
270
  }
223
271
  }
224
272
  const angularJsonPaths = findFilesUpward(targetRoot, 'angular.json');
225
273
  for (const angularJsonPath of angularJsonPaths) {
226
- const resolvedFromWorkspace = resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot);
227
- if (resolvedFromWorkspace) {
228
- return resolvedFromWorkspace;
274
+ for (const resolvedFromWorkspace of resolveStylesEntriesFromAngularWorkspace(angularJsonPath, targetRoot)) {
275
+ candidates.add(resolvedFromWorkspace);
229
276
  }
230
277
  }
231
- return undefined;
278
+ return [...candidates];
232
279
  }
233
280
  export function isDemoTarget(targetRoot) {
234
281
  return (existsSync(LOCAL_REPO_DEMO_ROOT) &&
@@ -287,7 +334,7 @@ function packageJsonUsesIonicAngular(packageJsonPath) {
287
334
  return false;
288
335
  }
289
336
  }
290
- function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
337
+ function resolveStylesEntriesFromAngularWorkspace(angularJsonPath, targetRoot) {
291
338
  try {
292
339
  const angularJson = JSON.parse(readFileSync(angularJsonPath, 'utf8'));
293
340
  const workspaceRoot = path.dirname(angularJsonPath);
@@ -298,6 +345,7 @@ function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
298
345
  : path.resolve(targetRoot) === path.resolve(workspaceRoot)
299
346
  ? projects
300
347
  : [];
348
+ const matches = [];
301
349
  for (const project of candidateProjects) {
302
350
  const styles = project.architect?.build?.options?.styles ?? project.targets?.build?.options?.styles ?? [];
303
351
  for (const style of styles) {
@@ -307,15 +355,15 @@ function resolveStylesEntryFromAngularWorkspace(angularJsonPath, targetRoot) {
307
355
  }
308
356
  const absolutePath = path.resolve(workspaceRoot, input);
309
357
  if (existsSync(absolutePath)) {
310
- return absolutePath;
358
+ matches.push(absolutePath);
311
359
  }
312
360
  }
313
361
  }
362
+ return matches;
314
363
  }
315
364
  catch {
316
- return undefined;
365
+ return [];
317
366
  }
318
- return undefined;
319
367
  }
320
368
  function projectMatchesTargetRoot(project, workspaceRoot, targetRoot) {
321
369
  const resolvedTargetRoot = path.resolve(targetRoot);
@@ -359,3 +407,48 @@ function looksLikeTailwindV4(range) {
359
407
  const versionTokenPattern = /(?:^|[<>=~^|\s:])v?(\d+)(?:(?:\.\d+){0,2})/g;
360
408
  return [...range.matchAll(versionTokenPattern)].some((match) => Number(match[1]) === 4);
361
409
  }
410
+ function buildAliasTargets(tsconfigPath, targetRoot) {
411
+ const aliasTarget = isDemoTarget(targetRoot)
412
+ ? toPosixPath(path.relative(LOCAL_REPO_ROOT, path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')))
413
+ : toConfigRelativePath(path.relative(path.dirname(tsconfigPath), path.join(targetRoot, 'src', 'app', 'components', 'ui', 'index.ts')));
414
+ const wildcardTarget = 'packages/ui/*';
415
+ return {
416
+ aliasTarget,
417
+ wildcardTarget,
418
+ };
419
+ }
420
+ function resolveStylesEntrySelection(targetRoot, candidates, preferredStylesEntry) {
421
+ if (preferredStylesEntry) {
422
+ const preferredAbsolutePath = path.resolve(targetRoot, preferredStylesEntry);
423
+ if (candidates.includes(preferredAbsolutePath)) {
424
+ return {
425
+ targetStylesEntry: preferredAbsolutePath,
426
+ targetStylesEntryResolution: 'preferred',
427
+ };
428
+ }
429
+ }
430
+ const trineOwnedCandidates = candidates.filter((candidate) => {
431
+ try {
432
+ return readFileSync(candidate, 'utf8').includes(STYLE_IMPORT_LINE);
433
+ }
434
+ catch {
435
+ return false;
436
+ }
437
+ });
438
+ if (trineOwnedCandidates.length === 1) {
439
+ return {
440
+ targetStylesEntry: trineOwnedCandidates[0],
441
+ targetStylesEntryResolution: 'trine-import',
442
+ };
443
+ }
444
+ if (candidates.length > 0) {
445
+ return {
446
+ targetStylesEntry: candidates[0],
447
+ targetStylesEntryResolution: 'default',
448
+ };
449
+ }
450
+ return {
451
+ targetStylesEntry: undefined,
452
+ targetStylesEntryResolution: 'none',
453
+ };
454
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,59 @@
1
+ import process from 'node:process';
2
+ import { createInterface } from 'node:readline/promises';
3
+ export async function chooseFromList(message, items, options = {}) {
4
+ assertInteractiveTerminal();
5
+ if (items.length === 0) {
6
+ throw new Error('Cannot choose from an empty list.');
7
+ }
8
+ if (items.length === 1) {
9
+ return items[0];
10
+ }
11
+ const renderItem = options.renderItem ?? ((item, index) => `${String(index + 1)}. ${item}`);
12
+ console.log(message);
13
+ for (const [index, item] of items.entries()) {
14
+ console.log(renderItem(item, index));
15
+ }
16
+ const prompt = createPrompt();
17
+ try {
18
+ for (;;) {
19
+ const answer = (await prompt.question(`Choose an option [1-${String(items.length)}]: `)).trim();
20
+ const selectedIndex = Number(answer);
21
+ if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= items.length) {
22
+ return items[selectedIndex - 1];
23
+ }
24
+ console.log(`Enter a number between 1 and ${String(items.length)}.`);
25
+ }
26
+ }
27
+ finally {
28
+ prompt.close();
29
+ }
30
+ }
31
+ export async function confirmAction(message) {
32
+ assertInteractiveTerminal();
33
+ const prompt = createPrompt();
34
+ try {
35
+ const answer = (await prompt.question(`${message} `)).trim().toLowerCase();
36
+ if (answer === '' || answer === 'y' || answer === 'yes') {
37
+ return true;
38
+ }
39
+ if (answer === 'n' || answer === 'no') {
40
+ return false;
41
+ }
42
+ console.log('Enter Y, yes, N, or no.');
43
+ return await confirmAction(message);
44
+ }
45
+ finally {
46
+ prompt.close();
47
+ }
48
+ }
49
+ export function assertInteractiveTerminal() {
50
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
51
+ throw new Error('trine init default mode is guided and needs an interactive terminal. Re-run with --yes for non-interactive mode.');
52
+ }
53
+ }
54
+ function createPrompt() {
55
+ return createInterface({
56
+ input: process.stdin,
57
+ output: process.stdout,
58
+ });
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trineui/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Copy-paste ownership CLI for Trine UI components.",
6
6
  "main": "./dist/index.js",