@astryxdesign/cli 0.1.6-canary.ff5dfca → 0.1.7-canary.04cd8f7
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 +33 -0
- package/README.md +115 -19
- package/docs/cli-integrations.doc.mjs +150 -0
- package/docs/getting-started.doc.mjs +9 -9
- package/docs/migration.doc.mjs +18 -18
- package/docs/principles.doc.dense.mjs +1 -1
- package/docs/principles.doc.mjs +6 -6
- package/docs/principles.doc.zh.mjs +1 -1
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +4 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +7 -7
- package/docs/theme.doc.zh.mjs +1 -1
- package/docs/tokens.doc.mjs +1 -1
- package/docs/working-with-ai.doc.mjs +18 -18
- package/package.json +9 -10
- package/src/api/doctor.mjs +3 -3
- package/src/codemods/ensure-jscodeshift.mjs +11 -27
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/commands/agent-docs.mjs +7 -8
- package/src/commands/agent-docs.test.mjs +11 -4
- package/src/commands/build-theme.mjs +10 -71
- package/src/commands/build.mjs +15 -15
- package/src/commands/component/index.mjs +4 -4
- package/src/commands/discover.mjs +7 -5
- package/src/commands/docs.mjs +4 -4
- package/src/commands/hook/index.mjs +4 -4
- package/src/commands/init.mjs +48 -152
- package/src/commands/init.next-steps.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +19 -22
- package/src/commands/json-contract.test.mjs +1 -1
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/swizzle.mjs +11 -34
- package/src/commands/template.mjs +11 -31
- package/src/commands/upgrade.mjs +9 -6
- package/src/commands/upgrade.test.mjs +1 -1
- package/src/index.mjs +5 -6
- package/src/lib/component-format.mjs +2 -1
- package/src/lib/term-log.mjs +48 -0
- package/src/utils/package-manager.mjs +78 -0
- package/src/utils/package-manager.test.mjs +108 -1
- package/src/utils/path-safety.mjs +0 -18
- package/src/utils/update-check.mjs +2 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
- package/docs/integration-authoring.md +0 -105
- package/src/utils/interactive.mjs +0 -76
- package/src/utils/interactive.test.mjs +0 -70
package/src/commands/swizzle.mjs
CHANGED
|
@@ -15,18 +15,16 @@
|
|
|
15
15
|
|
|
16
16
|
import * as fs from 'node:fs';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
|
-
import * as p from '@clack/prompts';
|
|
19
18
|
import {findCoreDir, listComponents} from '../utils/paths.mjs';
|
|
20
19
|
import {
|
|
21
20
|
assertWithin,
|
|
22
21
|
PathSafetyError,
|
|
23
|
-
isNonInteractive,
|
|
24
22
|
} from '../utils/path-safety.mjs';
|
|
25
23
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
26
24
|
import {cliError} from '../lib/cli-error.mjs';
|
|
27
25
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
28
26
|
import {checkGhCli} from '../utils/github.mjs';
|
|
29
|
-
import {
|
|
27
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
30
28
|
import {Project} from '../lib/project.mjs';
|
|
31
29
|
import {
|
|
32
30
|
CORE_PACKAGE,
|
|
@@ -99,14 +97,6 @@ function buildFeedback(component, issuesUrl) {
|
|
|
99
97
|
return feedback;
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
function isCancel(value) {
|
|
103
|
-
if (p.isCancel(value)) {
|
|
104
|
-
p.cancel('Cancelled.');
|
|
105
|
-
process.exit(0);
|
|
106
|
-
}
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
100
|
/**
|
|
111
101
|
* Load the configured integrations + core issues URL for `cwd`, swallowing any
|
|
112
102
|
* config errors so swizzle never hard-fails on a malformed/absent config. An
|
|
@@ -190,6 +180,7 @@ export function registerSwizzle(program) {
|
|
|
190
180
|
.action(async (component, options) => {
|
|
191
181
|
const coreDir = findCoreDir(process.cwd());
|
|
192
182
|
const json = program.opts().json || false;
|
|
183
|
+
const run = getCliInvocation();
|
|
193
184
|
|
|
194
185
|
if (!coreDir) {
|
|
195
186
|
cliError(
|
|
@@ -207,10 +198,10 @@ export function registerSwizzle(program) {
|
|
|
207
198
|
for (const name of components) {
|
|
208
199
|
humanLog(` ${name}`);
|
|
209
200
|
}
|
|
210
|
-
humanLog(`\nUsage:
|
|
211
|
-
humanLog(
|
|
201
|
+
humanLog(`\nUsage: ${run} swizzle <component>\n`);
|
|
202
|
+
humanLog(`Example: ${run} swizzle Button`);
|
|
212
203
|
humanLog(
|
|
213
|
-
|
|
204
|
+
` ${run} swizzle XDSButton (XDS prefix also works)\n`,
|
|
214
205
|
);
|
|
215
206
|
return;
|
|
216
207
|
}
|
|
@@ -314,25 +305,11 @@ export function registerSwizzle(program) {
|
|
|
314
305
|
|
|
315
306
|
if (existingFiles.length > 0 && !options.overwrite) {
|
|
316
307
|
const relOutputForMsg = path.relative(process.cwd(), outputDir) || '.';
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const confirmed = isCancel(
|
|
325
|
-
await p.confirm({
|
|
326
|
-
message:
|
|
327
|
-
`Overwrite ${existingFiles.length} existing file(s) in ${relOutputForMsg}/? ` +
|
|
328
|
-
`(${existingFiles.slice(0, 3).join(', ')}${existingFiles.length > 3 ? ', …' : ''})`,
|
|
329
|
-
initialValue: false,
|
|
330
|
-
}),
|
|
331
|
-
);
|
|
332
|
-
if (!confirmed) {
|
|
333
|
-
humanLog('Aborted. Re-run with --overwrite to replace files.');
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
308
|
+
const msg =
|
|
309
|
+
`Refusing to overwrite ${existingFiles.length} existing file(s) in ${relOutputForMsg}/. ` +
|
|
310
|
+
`Re-run with --overwrite (or -f) to replace them.`;
|
|
311
|
+
cliError(msg, {code: ERROR_CODES.ERR_FILE_EXISTS});
|
|
312
|
+
return;
|
|
336
313
|
}
|
|
337
314
|
|
|
338
315
|
fs.mkdirSync(outputDir, {recursive: true});
|
|
@@ -411,7 +388,7 @@ export function registerSwizzle(program) {
|
|
|
411
388
|
humanLog(
|
|
412
389
|
' Without one they render unstyled (no error). See setup per framework:',
|
|
413
390
|
);
|
|
414
|
-
humanLog(` ${
|
|
391
|
+
humanLog(` ${run} docs styling`);
|
|
415
392
|
humanLog(
|
|
416
393
|
' Next.js note: the StyleX Babel plugin disables SWC and breaks next/font —',
|
|
417
394
|
);
|
|
@@ -6,25 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
import * as path from 'node:path';
|
|
8
8
|
import * as fs from 'node:fs';
|
|
9
|
-
import * as p from '@clack/prompts';
|
|
10
|
-
import {isNonInteractive} from '../utils/path-safety.mjs';
|
|
11
9
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
12
10
|
import {cliError} from '../lib/cli-error.mjs';
|
|
13
11
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
14
12
|
import {template as templateApi} from '../api/template.mjs';
|
|
15
13
|
import {Project} from '../lib/project.mjs';
|
|
16
14
|
import {warnOnIntegrationIssues} from '../lib/integration-warnings.mjs';
|
|
15
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
17
16
|
|
|
18
17
|
export {discoverTemplates, listTemplates} from '../api/template.mjs';
|
|
19
18
|
|
|
20
|
-
function isCancel(value) {
|
|
21
|
-
if (p.isCancel(value)) {
|
|
22
|
-
p.cancel('Cancelled.');
|
|
23
|
-
process.exit(0);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
19
|
export function registerTemplate(program) {
|
|
29
20
|
program
|
|
30
21
|
.command('template [name] [path]')
|
|
@@ -36,6 +27,7 @@ export function registerTemplate(program) {
|
|
|
36
27
|
.option('-f, --overwrite', 'Overwrite existing files without prompting')
|
|
37
28
|
.action(async (name, targetPath, options) => {
|
|
38
29
|
const json = program.opts().json || false;
|
|
30
|
+
const run = getCliInvocation();
|
|
39
31
|
|
|
40
32
|
// Non-blocking nudge: if any configured integration has validation
|
|
41
33
|
// issues, print one compact line to stderr pointing at
|
|
@@ -59,23 +51,11 @@ export function registerTemplate(program) {
|
|
|
59
51
|
const collision = await detectTemplateCollision(name, targetPath);
|
|
60
52
|
if (collision && !options.overwrite) {
|
|
61
53
|
const rel = path.relative(process.cwd(), collision) || collision;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const confirmed = isCancel(
|
|
70
|
-
await p.confirm({
|
|
71
|
-
message: `Overwrite existing file ${rel}?`,
|
|
72
|
-
initialValue: false,
|
|
73
|
-
}),
|
|
74
|
-
);
|
|
75
|
-
if (!confirmed) {
|
|
76
|
-
humanLog('Aborted. Re-run with --overwrite to replace the file.');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
54
|
+
const msg =
|
|
55
|
+
`Refusing to overwrite existing file ${rel}. ` +
|
|
56
|
+
`Re-run with --overwrite (or -f) to replace it.`;
|
|
57
|
+
cliError(msg, {code: ERROR_CODES.ERR_FILE_EXISTS});
|
|
58
|
+
return;
|
|
79
59
|
}
|
|
80
60
|
}
|
|
81
61
|
|
|
@@ -120,10 +100,10 @@ export function registerTemplate(program) {
|
|
|
120
100
|
for (const t of blocks) renderEntry(t);
|
|
121
101
|
}
|
|
122
102
|
humanLog('\nUsage:');
|
|
123
|
-
humanLog(
|
|
124
|
-
humanLog(
|
|
125
|
-
humanLog(
|
|
126
|
-
humanLog(
|
|
103
|
+
humanLog(` ${run} template <id> [target-path] Scaffold page or block`);
|
|
104
|
+
humanLog(` ${run} template <id> --skeleton Layout reference`);
|
|
105
|
+
humanLog(` ${run} template --list --type block List only blocks`);
|
|
106
|
+
humanLog(` ${run} template --list --package <pkg> List from one package\n`);
|
|
127
107
|
break;
|
|
128
108
|
}
|
|
129
109
|
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import * as fs from 'node:fs';
|
|
|
37
37
|
import * as path from 'node:path';
|
|
38
38
|
import {execFile} from 'node:child_process';
|
|
39
39
|
import {promisify} from 'node:util';
|
|
40
|
-
import * as p from '
|
|
40
|
+
import * as p from '../lib/term-log.mjs';
|
|
41
41
|
import {ensureJscodeshift} from '../codemods/ensure-jscodeshift.mjs';
|
|
42
42
|
import {getTransformsBetween, latestVersion} from '../codemods/registry.mjs';
|
|
43
43
|
import {runCodemods} from '../codemods/runner.mjs';
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
} from '../codemods/integration-discovery.mjs';
|
|
48
48
|
import {runIntegrationCodemods} from '../codemods/integration-runner.mjs';
|
|
49
49
|
import {installAgentDocs, discoverAgentDocs} from './agent-docs.mjs';
|
|
50
|
-
import {
|
|
50
|
+
import {getCliInvocation, formatCliCommand} from '../utils/package-manager.mjs';
|
|
51
51
|
import {isValidSemver, semverGte} from '../utils/semver.mjs';
|
|
52
52
|
import {jsonOut, jsonError} from '../lib/json.mjs';
|
|
53
53
|
import {Project} from '../lib/project.mjs';
|
|
@@ -176,7 +176,7 @@ export function registerUpgrade(program) {
|
|
|
176
176
|
|
|
177
177
|
if (!options.list && !options.from) {
|
|
178
178
|
const msg =
|
|
179
|
-
|
|
179
|
+
`Missing required --from. Install the target version first, then run \`${getCliInvocation()} upgrade --from <old-version>\`.`;
|
|
180
180
|
if (json)
|
|
181
181
|
return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_ARGUMENT);
|
|
182
182
|
p.log.error(msg);
|
|
@@ -237,7 +237,7 @@ export function registerUpgrade(program) {
|
|
|
237
237
|
const installed = detectInstalledTargetVersion();
|
|
238
238
|
if (!installed) {
|
|
239
239
|
const msg =
|
|
240
|
-
|
|
240
|
+
`Could not find installed @astryxdesign/core (or legacy @xds/core). Install the target version first, then rerun \`${getCliInvocation()} upgrade --from <old-version>\`.`;
|
|
241
241
|
if (json)
|
|
242
242
|
return jsonError(msg, undefined, ERROR_CODES.ERR_VERSION_DETECT);
|
|
243
243
|
p.log.error(msg);
|
|
@@ -390,6 +390,9 @@ export function registerUpgrade(program) {
|
|
|
390
390
|
const codemodFlags = coreConfigCodemodNames
|
|
391
391
|
.map(name => `--codemod ${name}`)
|
|
392
392
|
.join(' ');
|
|
393
|
+
// Canonical (bare) form — this is a structured, machine-executable
|
|
394
|
+
// field in the --json envelope. The human print below is made
|
|
395
|
+
// install-aware via formatCliCommand.
|
|
393
396
|
const suggestedCommand = `astryx upgrade --from ${currentVersion} ${codemodFlags} --apply`;
|
|
394
397
|
const guidance =
|
|
395
398
|
'Your astryx.config currently fails strict validation, but a pending ' +
|
|
@@ -409,7 +412,7 @@ export function registerUpgrade(program) {
|
|
|
409
412
|
});
|
|
410
413
|
}
|
|
411
414
|
p.log.warn(guidance);
|
|
412
|
-
p.log.info(` ${suggestedCommand}`);
|
|
415
|
+
p.log.info(` ${formatCliCommand(suggestedCommand)}`);
|
|
413
416
|
p.log.info(
|
|
414
417
|
'Integrations are skipped in this preview; they will be processed on the --apply run.',
|
|
415
418
|
);
|
|
@@ -619,7 +622,7 @@ export function registerUpgrade(program) {
|
|
|
619
622
|
} catch {
|
|
620
623
|
if (!json) {
|
|
621
624
|
p.log.warn(
|
|
622
|
-
`Could not update agent docs. Run \`${
|
|
625
|
+
`Could not update agent docs. Run \`${getCliInvocation()} init --features agents\` to update manually.`,
|
|
623
626
|
);
|
|
624
627
|
}
|
|
625
628
|
}
|
|
@@ -24,7 +24,7 @@ beforeEach(() => {
|
|
|
24
24
|
logCalls.push(args.join(' '));
|
|
25
25
|
});
|
|
26
26
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
27
|
-
//
|
|
27
|
+
// Some human logs are written straight to process.stdout — capture that too.
|
|
28
28
|
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
29
29
|
stdoutCalls.push(typeof chunk === 'string' ? chunk : chunk.toString());
|
|
30
30
|
return true;
|
package/src/index.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import {fileURLToPath} from 'node:url';
|
|
|
12
12
|
import * as fs from 'node:fs';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import {checkForUpdate} from './utils/update-check.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import {getCliInvocation} from './utils/package-manager.mjs';
|
|
16
16
|
import {API_VERSION, setJsonMode} from './lib/json.mjs';
|
|
17
17
|
import {buildManifest} from './lib/manifest.mjs';
|
|
18
18
|
import {cliError} from './lib/cli-error.mjs';
|
|
@@ -174,7 +174,7 @@ function fullCommandName(actionCommand) {
|
|
|
174
174
|
*
|
|
175
175
|
* If --json is set on a command that is not on the JSON_SUPPORTED allowlist,
|
|
176
176
|
* emit a structured error envelope and exit 1 — without running the command's
|
|
177
|
-
* action (so no filesystem mutations, no
|
|
177
|
+
* action (so no filesystem mutations, no interactive prompts, no spawned processes).
|
|
178
178
|
*
|
|
179
179
|
* This is the single source of truth for "command does not support --json".
|
|
180
180
|
* Individual commands should NOT re-check this; they may assume that if their
|
|
@@ -305,15 +305,14 @@ program
|
|
|
305
305
|
console.log(` ${c.name}${tag}`);
|
|
306
306
|
if (c.description) console.log(` ${c.description}`);
|
|
307
307
|
}
|
|
308
|
-
console.log(`\nRun
|
|
308
|
+
console.log(`\nRun \`${getCliInvocation()} manifest --json\` for the full structured manifest.\n`);
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
// Hidden command used by package.json postinstall scripts
|
|
312
312
|
program
|
|
313
313
|
.command('postinstall', {hidden: true})
|
|
314
314
|
.action(() => {
|
|
315
|
-
const
|
|
316
|
-
const r = `${run} xds`;
|
|
315
|
+
const r = getCliInvocation();
|
|
317
316
|
const pad = (s, len) => s + ' '.repeat(Math.max(0, len - s.length));
|
|
318
317
|
const W = 49; // inner width of the box
|
|
319
318
|
const line = (s) => ` │ ${pad(s, W)}│`;
|
|
@@ -323,7 +322,7 @@ ${line('')}
|
|
|
323
322
|
${line(' Design system installed!')}
|
|
324
323
|
${line('')}
|
|
325
324
|
${line(' Get started:')}
|
|
326
|
-
${line(` ${r} init
|
|
325
|
+
${line(` ${r} init Setup + AI agent docs`)}
|
|
327
326
|
${line(` ${r} --help See all commands`)}
|
|
328
327
|
${line('')}
|
|
329
328
|
${line(' Or run directly:')}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {discoverComponents, findComponentReadme, resolveImportPath} from './component-discovery.mjs';
|
|
8
8
|
import {loadDocs} from './component-loader.mjs';
|
|
9
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Derive the `defineTheme` component-override key from a theming target.
|
|
@@ -81,7 +82,7 @@ function formatSubComponent(comp) {
|
|
|
81
82
|
if (table) {
|
|
82
83
|
out.push(table + '\n');
|
|
83
84
|
} else {
|
|
84
|
-
out.push(`See
|
|
85
|
+
out.push(`See \`${getCliInvocation()} component ${comp.name}\` for props and usage.\n`);
|
|
85
86
|
}
|
|
86
87
|
return out;
|
|
87
88
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Minimal non-interactive terminal logger.
|
|
5
|
+
*
|
|
6
|
+
* @input message strings from CLI commands/codemods
|
|
7
|
+
* @output plain lines on stdout via humanLog (suppressed in --json mode)
|
|
8
|
+
* @position src/lib — shared output helper, no side effects on import
|
|
9
|
+
*
|
|
10
|
+
* The CLI is fully non-interactive: it never prompts, so it only needs plain,
|
|
11
|
+
* unbuffered output. This provides the *output-only* surface (`log.*`, `intro`,
|
|
12
|
+
* `outro`) the CLI needs, so it has no dependency on any prompt library.
|
|
13
|
+
*
|
|
14
|
+
* All output is routed through `humanLog`, the CLI's stdout-discipline
|
|
15
|
+
* primitive, which is a no-op in `--json` mode — so these human logs can never
|
|
16
|
+
* corrupt a JSON envelope.
|
|
17
|
+
*
|
|
18
|
+
* Call sites use it as `import * as p from '../lib/term-log.mjs'` and call
|
|
19
|
+
* `p.log.info(...)`, `p.intro(...)`, `p.outro(...)`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {humanLog} from './json.mjs';
|
|
23
|
+
|
|
24
|
+
const toStr = (msg) => (msg === undefined || msg === null ? '' : String(msg));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Human-facing log surface (the small `log` API the CLI uses). All lines go to
|
|
28
|
+
* stdout via humanLog; the level prefixes are cosmetic. `--json` mode suppresses
|
|
29
|
+
* every one of these, keeping machine-readable stdout clean.
|
|
30
|
+
*/
|
|
31
|
+
export const log = {
|
|
32
|
+
message: (msg) => humanLog(toStr(msg)),
|
|
33
|
+
info: (msg) => humanLog(toStr(msg)),
|
|
34
|
+
step: (msg) => humanLog(toStr(msg)),
|
|
35
|
+
success: (msg) => humanLog(`✓ ${toStr(msg)}`),
|
|
36
|
+
warn: (msg) => humanLog(`⚠ ${toStr(msg)}`),
|
|
37
|
+
error: (msg) => humanLog(`✗ ${toStr(msg)}`),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Banner printed at the start of a multi-step command. */
|
|
41
|
+
export function intro(title) {
|
|
42
|
+
humanLog(`\n${toStr(title)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Footer printed at the end of a multi-step command. */
|
|
46
|
+
export function outro(message) {
|
|
47
|
+
humanLog(`${toStr(message)}\n`);
|
|
48
|
+
}
|
|
@@ -72,3 +72,81 @@ export function getRunPrefix(targetDir) {
|
|
|
72
72
|
default: return 'npx';
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/** The published CLI package name — used for one-off (uninstalled) invocations. */
|
|
77
|
+
export const CLI_PACKAGE = '@astryxdesign/cli';
|
|
78
|
+
|
|
79
|
+
/** The CLI binary name — only resolves once the CLI is installed (or run via CLI_PACKAGE). */
|
|
80
|
+
export const CLI_BIN = 'astryx';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the one-off ("dlx") runner for the detected package manager.
|
|
84
|
+
*
|
|
85
|
+
* Unlike {@link getRunPrefix} (which runs an *installed* binary), this fetches
|
|
86
|
+
* and runs a package on demand — so it is always paired with the scoped
|
|
87
|
+
* {@link CLI_PACKAGE}, never the bare `astryx` bin. Running bare `npx astryx`
|
|
88
|
+
* without the CLI installed resolves to an unrelated package on the registry.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} [targetDir]
|
|
91
|
+
* @returns {string} e.g. 'npx', 'pnpm dlx', 'yarn dlx', 'bunx'
|
|
92
|
+
*/
|
|
93
|
+
export function getDlxPrefix(targetDir) {
|
|
94
|
+
const pm = detectPackageManager(targetDir);
|
|
95
|
+
switch (pm) {
|
|
96
|
+
case 'yarn': return 'yarn dlx';
|
|
97
|
+
case 'pnpm': return 'pnpm dlx';
|
|
98
|
+
case 'bun': return 'bunx';
|
|
99
|
+
case 'npm':
|
|
100
|
+
default: return 'npx';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Heuristic: was the running CLI launched one-off via a package runner
|
|
106
|
+
* (npx / pnpm dlx / yarn dlx / bunx) rather than from an installed dependency?
|
|
107
|
+
*
|
|
108
|
+
* We sniff the entry path (`process.argv[1]`) for well-known runner-cache
|
|
109
|
+
* markers. This errs safe in both directions: a false negative falls back to
|
|
110
|
+
* the installed form (`<prefix> astryx`, the historical behavior), and a false
|
|
111
|
+
* positive emits the always-valid scoped form (`<dlx> @astryxdesign/cli`).
|
|
112
|
+
*
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
export function isCliOneOff() {
|
|
116
|
+
const entry = String(process.argv[1] || '').replace(/\\/g, '/');
|
|
117
|
+
return /\/_npx\/|\/dlx[-/]|\/\.bun\/install\/cache\/|\/bunx-/.test(entry);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The safe, install-aware CLI invocation stem to suggest to users.
|
|
122
|
+
*
|
|
123
|
+
* - Installed / global / dev: `<run-prefix> astryx` (e.g. `pnpm exec astryx`).
|
|
124
|
+
* Bare `astryx` resolves to the local (or global) binary.
|
|
125
|
+
* - One-off (npx/dlx cache): `<dlx-prefix> @astryxdesign/cli` — the bare
|
|
126
|
+
* `astryx` name isn't on disk, so npm would fetch an unrelated registry
|
|
127
|
+
* package; the scoped package always resolves to us.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} [targetDir]
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
export function getCliInvocation(targetDir) {
|
|
133
|
+
if (isCliOneOff()) return `${getDlxPrefix(targetDir)} ${CLI_PACKAGE}`;
|
|
134
|
+
return `${getRunPrefix(targetDir)} ${CLI_BIN}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format a full, runnable CLI command from a subcommand string.
|
|
139
|
+
*
|
|
140
|
+
* Accepts either `astryx component Button` or `component Button` (a leading
|
|
141
|
+
* `astryx` token is stripped) and prepends the install-aware invocation stem
|
|
142
|
+
* from {@link getCliInvocation}.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} command e.g. 'astryx component Button' | 'docs tokens'
|
|
145
|
+
* @param {string} [targetDir]
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function formatCliCommand(command, targetDir) {
|
|
149
|
+
const sub = String(command).replace(/^\s*astryx\b\s*/, '').trim();
|
|
150
|
+
const stem = getCliInvocation(targetDir);
|
|
151
|
+
return sub ? `${stem} ${sub}` : stem;
|
|
152
|
+
}
|
|
@@ -4,9 +4,16 @@ import {describe, it, expect, afterEach, vi} from 'vitest';
|
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import * as os from 'node:os';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
detectPackageManager,
|
|
9
|
+
getDlxPrefix,
|
|
10
|
+
isCliOneOff,
|
|
11
|
+
getCliInvocation,
|
|
12
|
+
formatCliCommand,
|
|
13
|
+
} from './package-manager.mjs';
|
|
8
14
|
|
|
9
15
|
let tmpDir;
|
|
16
|
+
const ORIGINAL_ARGV1 = process.argv[1];
|
|
10
17
|
|
|
11
18
|
afterEach(() => {
|
|
12
19
|
if (tmpDir) {
|
|
@@ -15,6 +22,7 @@ afterEach(() => {
|
|
|
15
22
|
}
|
|
16
23
|
vi.restoreAllMocks();
|
|
17
24
|
delete process.env.npm_config_user_agent;
|
|
25
|
+
process.argv[1] = ORIGINAL_ARGV1;
|
|
18
26
|
});
|
|
19
27
|
|
|
20
28
|
function makeTmpDir() {
|
|
@@ -111,3 +119,102 @@ describe('detectPackageManager', () => {
|
|
|
111
119
|
expect(detectPackageManager(dir)).toBe('bun');
|
|
112
120
|
});
|
|
113
121
|
});
|
|
122
|
+
|
|
123
|
+
describe('getDlxPrefix', () => {
|
|
124
|
+
it('returns "pnpm dlx" for pnpm projects', () => {
|
|
125
|
+
const dir = makeTmpDir();
|
|
126
|
+
fs.writeFileSync(path.join(dir, 'pnpm-lock.yaml'), '');
|
|
127
|
+
expect(getDlxPrefix(dir)).toBe('pnpm dlx');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns "yarn dlx" for yarn projects', () => {
|
|
131
|
+
const dir = makeTmpDir();
|
|
132
|
+
fs.writeFileSync(path.join(dir, 'yarn.lock'), '');
|
|
133
|
+
expect(getDlxPrefix(dir)).toBe('yarn dlx');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns "bunx" for bun projects', () => {
|
|
137
|
+
const dir = makeTmpDir();
|
|
138
|
+
fs.writeFileSync(path.join(dir, 'bun.lockb'), '');
|
|
139
|
+
expect(getDlxPrefix(dir)).toBe('bunx');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('falls back to "npx" with no signals', () => {
|
|
143
|
+
const dir = makeTmpDir();
|
|
144
|
+
delete process.env.npm_config_user_agent;
|
|
145
|
+
expect(getDlxPrefix(dir)).toBe('npx');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('isCliOneOff', () => {
|
|
150
|
+
it('detects an npm npx cache entry', () => {
|
|
151
|
+
process.argv[1] = '/home/u/.npm/_npx/a1b2/node_modules/.bin/astryx';
|
|
152
|
+
expect(isCliOneOff()).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('detects a pnpm dlx cache entry', () => {
|
|
156
|
+
process.argv[1] = '/home/u/.cache/pnpm/dlx/9f/node_modules/@astryxdesign/cli/bin/astryx.mjs';
|
|
157
|
+
expect(isCliOneOff()).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('detects a bunx cache entry', () => {
|
|
161
|
+
process.argv[1] = '/home/u/.bun/install/cache/@astryxdesign/cli/bin/astryx.mjs';
|
|
162
|
+
expect(isCliOneOff()).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('is false for an installed node_modules entry', () => {
|
|
166
|
+
process.argv[1] = '/proj/node_modules/@astryxdesign/cli/bin/astryx.mjs';
|
|
167
|
+
expect(isCliOneOff()).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('is false for a source checkout (dev) entry', () => {
|
|
171
|
+
process.argv[1] = '/repo/packages/cli/bin/astryx.mjs';
|
|
172
|
+
expect(isCliOneOff()).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('getCliInvocation', () => {
|
|
177
|
+
it('uses the run-prefix + bare bin when installed (not one-off)', () => {
|
|
178
|
+
process.argv[1] = '/proj/node_modules/@astryxdesign/cli/bin/astryx.mjs';
|
|
179
|
+
const dir = makeTmpDir();
|
|
180
|
+
fs.writeFileSync(path.join(dir, 'pnpm-lock.yaml'), '');
|
|
181
|
+
expect(getCliInvocation(dir)).toBe('pnpm exec astryx');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('uses the dlx runner + scoped package when run one-off', () => {
|
|
185
|
+
process.argv[1] = '/home/u/.npm/_npx/a1b2/node_modules/.bin/astryx';
|
|
186
|
+
const dir = makeTmpDir();
|
|
187
|
+
delete process.env.npm_config_user_agent;
|
|
188
|
+
expect(getCliInvocation(dir)).toBe('npx @astryxdesign/cli');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('pairs the dlx runner with the scoped package for pnpm one-off', () => {
|
|
192
|
+
process.argv[1] = '/home/u/.cache/pnpm/dlx/9f/node_modules/@astryxdesign/cli/bin/astryx.mjs';
|
|
193
|
+
const dir = makeTmpDir();
|
|
194
|
+
fs.writeFileSync(path.join(dir, 'pnpm-lock.yaml'), '');
|
|
195
|
+
expect(getCliInvocation(dir)).toBe('pnpm dlx @astryxdesign/cli');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('formatCliCommand', () => {
|
|
200
|
+
it('strips a leading "astryx" token and prepends the invocation stem', () => {
|
|
201
|
+
process.argv[1] = '/proj/node_modules/@astryxdesign/cli/bin/astryx.mjs';
|
|
202
|
+
const dir = makeTmpDir();
|
|
203
|
+
fs.writeFileSync(path.join(dir, 'pnpm-lock.yaml'), '');
|
|
204
|
+
expect(formatCliCommand('astryx component Button', dir)).toBe('pnpm exec astryx component Button');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('accepts a bare subcommand (no leading astryx)', () => {
|
|
208
|
+
process.argv[1] = '/proj/node_modules/@astryxdesign/cli/bin/astryx.mjs';
|
|
209
|
+
const dir = makeTmpDir();
|
|
210
|
+
fs.writeFileSync(path.join(dir, 'package-lock.json'), '{}');
|
|
211
|
+
expect(formatCliCommand('docs tokens', dir)).toBe('npx astryx docs tokens');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('rewrites to the scoped package for one-off invocations', () => {
|
|
215
|
+
process.argv[1] = '/home/u/.npm/_npx/a1b2/node_modules/.bin/astryx';
|
|
216
|
+
const dir = makeTmpDir();
|
|
217
|
+
fs.writeFileSync(path.join(dir, 'package-lock.json'), '{}');
|
|
218
|
+
expect(formatCliCommand('astryx component Button', dir)).toBe('npx @astryxdesign/cli component Button');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -165,21 +165,3 @@ export function isFilePathArg(pathArg) {
|
|
|
165
165
|
const ext = path.extname(base).toLowerCase();
|
|
166
166
|
return ext.length > 0 && FILE_EXTENSIONS.has(ext);
|
|
167
167
|
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* True when the process is running non-interactively (no TTY) or when the
|
|
171
|
-
* caller has signaled JSON / scripted use. Commands consult this before
|
|
172
|
-
* prompting for confirmation; in scripted mode they require an explicit
|
|
173
|
-
* `--overwrite` flag instead.
|
|
174
|
-
*
|
|
175
|
-
* @param {object} [options]
|
|
176
|
-
* @param {boolean} [options.json] - Caller's --json flag.
|
|
177
|
-
* @returns {boolean}
|
|
178
|
-
*/
|
|
179
|
-
export function isNonInteractive({json = false} = {}) {
|
|
180
|
-
if (json) return true;
|
|
181
|
-
// stdin not a TTY means piped input or scripted execution.
|
|
182
|
-
if (process.stdin && process.stdin.isTTY === false) return true;
|
|
183
|
-
if (process.stdout && process.stdout.isTTY === false) return true;
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
15
|
import * as path from 'node:path';
|
|
16
16
|
import {semverGt} from './semver.mjs';
|
|
17
|
+
import {getCliInvocation} from './package-manager.mjs';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Read the latest available version from local signals.
|
|
@@ -75,7 +76,7 @@ export function checkForUpdate(cwd = process.cwd()) {
|
|
|
75
76
|
// Use semver-aware comparison so '0.0.20' is correctly treated as greater
|
|
76
77
|
// than '0.0.5' (lexicographic compare gets that backwards).
|
|
77
78
|
if (semverGt(latest, installed)) {
|
|
78
|
-
return `FYI: A newer version of @astryxdesign/core (${latest}) is available. Install the new package version, then run:
|
|
79
|
+
return `FYI: A newer version of @astryxdesign/core (${latest}) is available. Install the new package version, then run: ${getCliInvocation()} upgrade --from <old-version> --apply`;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
return null;
|
|
@@ -7,7 +7,7 @@ export const doc = {
|
|
|
7
7
|
name: 'TabList — With Actions',
|
|
8
8
|
displayName: 'TabList — With Actions',
|
|
9
9
|
description:
|
|
10
|
-
'Page header pattern with tabs on the left and action buttons pushed to the right. When hasDivider is true,
|
|
10
|
+
'Page header pattern with tabs on the left and action buttons pushed to the right. When hasDivider is true, match the Button size to the TabList size so the tabs and actions align to a shared baseline above the divider.',
|
|
11
11
|
isReady: true,
|
|
12
12
|
aspectRatio: 4 / 3,
|
|
13
13
|
componentsUsed: ['TabList', 'Tab', 'Button'],
|
|
@@ -55,16 +55,11 @@ export default function TabListTabsWithActions() {
|
|
|
55
55
|
<Button
|
|
56
56
|
label="Filter"
|
|
57
57
|
variant="ghost"
|
|
58
|
-
size="
|
|
58
|
+
size="lg"
|
|
59
59
|
icon={FilterIcon}
|
|
60
60
|
isIconOnly
|
|
61
61
|
/>
|
|
62
|
-
<Button
|
|
63
|
-
label="New item"
|
|
64
|
-
variant="primary"
|
|
65
|
-
size="sm"
|
|
66
|
-
icon={PlusIcon}
|
|
67
|
-
/>
|
|
62
|
+
<Button label="New item" variant="primary" size="lg" icon={PlusIcon} />
|
|
68
63
|
</div>
|
|
69
64
|
</TabList>
|
|
70
65
|
);
|