@astryxdesign/cli 0.1.7 → 0.1.8-canary.0c4d8d7
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 +40 -0
- package/README.md +115 -19
- package/docs/cli-integrations.doc.mjs +150 -0
- package/docs/elevation.doc.mjs +79 -1
- 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 +19 -19
- package/package.json +13 -11
- package/scripts/postinstall.mjs +74 -0
- package/src/api/doctor.mjs +3 -3
- package/src/api/error.mjs +2 -2
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/ensure-jscodeshift.mjs +11 -27
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +42 -7
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +43 -0
- package/src/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +102 -3
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +72 -6
- package/src/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +161 -0
- package/src/codemods/transforms/v0.1.8/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +234 -0
- package/src/commands/agent-docs.mjs +136 -20
- package/src/commands/agent-docs.test.mjs +123 -10
- package/src/commands/build-theme.mjs +10 -71
- package/src/commands/build.mjs +15 -15
- package/src/commands/cli-postinstall.test.mjs +42 -0
- 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 +2 -2
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/setup-nudge.test.mjs +108 -0
- package/src/commands/swizzle.mjs +12 -35
- package/src/commands/template.mjs +11 -31
- package/src/commands/upgrade.mjs +138 -32
- package/src/commands/upgrade.test.mjs +155 -1
- package/src/index.mjs +40 -6
- package/src/lib/cli-error.mjs +2 -2
- package/src/lib/component-format.mjs +2 -1
- package/src/lib/json-shim.mjs +1 -1
- package/src/lib/json.mjs +3 -3
- package/src/lib/term-log.mjs +48 -0
- package/src/types/api.d.ts +4 -9
- package/src/types/base.d.ts +24 -3
- package/src/types/build.d.ts +23 -0
- package/src/types/index.d.ts +1 -0
- package/src/types/swizzle.d.ts +4 -0
- package/src/types/template-api.d.ts +4 -1
- package/src/types/upgrade.d.ts +39 -2
- package/src/utils/package-manager.mjs +108 -4
- package/src/utils/package-manager.test.mjs +108 -1
- package/src/utils/path-safety.mjs +0 -18
- package/src/utils/update-check.mjs +5 -4
- package/src/utils/update-check.test.mjs +3 -3
- package/templates/blocks/components/Avatar/AvatarFallbackChain.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarGroup.tsx +2 -2
- package/templates/blocks/components/Avatar/AvatarInitialsFallback.tsx +1 -1
- package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarUserCard.tsx +1 -1
- package/templates/blocks/components/Avatar/AvatarWithImage.tsx +4 -4
- package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -3
- package/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.tsx +2 -2
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowCustomText.tsx +1 -1
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowDefault.tsx +1 -1
- package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowShowcase.tsx +2 -2
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotShowcase.tsx +3 -3
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +3 -3
- package/templates/blocks/components/Banner/BannerFloating.doc.mjs +14 -0
- package/templates/blocks/components/Banner/BannerFloating.tsx +16 -0
- package/templates/blocks/components/Button/ButtonFloating.doc.mjs +14 -0
- package/templates/blocks/components/Button/ButtonFloating.tsx +37 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupFloating.doc.mjs +14 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupFloating.tsx +23 -0
- package/templates/blocks/components/Card/CardElevations.doc.mjs +14 -0
- package/templates/blocks/components/Card/CardElevations.tsx +32 -0
- package/templates/blocks/components/Card/ClickableCardElevated.doc.mjs +14 -0
- package/templates/blocks/components/Card/ClickableCardElevated.tsx +21 -0
- package/templates/blocks/components/Card/SelectableCardElevated.doc.mjs +14 -0
- package/templates/blocks/components/Card/SelectableCardElevated.tsx +37 -0
- package/templates/blocks/components/Carousel/CarouselSnap.tsx +1 -1
- package/templates/blocks/components/ChatComposer/ChatComposerFlat.doc.mjs +14 -0
- package/templates/blocks/components/ChatComposer/ChatComposerFlat.tsx +75 -0
- package/templates/blocks/components/ChatMessage/ChatMessageAvatarName.tsx +2 -2
- package/templates/blocks/components/ChatMessage/ChatMessageMultiBubble.tsx +1 -1
- package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleGrouping.tsx +1 -1
- package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleMetadata.tsx +1 -1
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +5 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockTerminal.tsx +1 -1
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -1
- package/templates/blocks/components/IconButton/IconButtonFloating.doc.mjs +14 -0
- package/templates/blocks/components/IconButton/IconButtonFloating.tsx +37 -0
- package/templates/blocks/components/Item/ItemWithMedia.tsx +2 -2
- package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
- package/templates/blocks/components/OverflowList/OverflowListCappedToolbar.doc.mjs +14 -0
- package/templates/blocks/components/OverflowList/OverflowListCappedToolbar.tsx +39 -0
- package/templates/blocks/components/OverflowList/OverflowListMultiRowTags.doc.mjs +14 -0
- package/templates/blocks/components/OverflowList/OverflowListMultiRowTags.tsx +44 -0
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -6
- package/templates/blocks/components/Stack/StackFillItem.tsx +2 -6
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
- package/templates/blocks/components/Table/TableRowStatusTable.doc.mjs +14 -0
- package/templates/blocks/components/Table/TableRowStatusTable.tsx +59 -0
- package/templates/blocks/components/Thumbnail/ThumbnailElevated.doc.mjs +14 -0
- package/templates/blocks/components/Thumbnail/ThumbnailElevated.tsx +31 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemShowcase.tsx +1 -1
- package/templates/pages/ai-chat/page.tsx +4 -4
- package/templates/pages/dashboard-portfolio/page.tsx +3 -11
- package/templates/pages/detail-page/page.tsx +5 -12
- package/templates/pages/documentation-design/page.tsx +1 -1
- package/templates/pages/messaging-shell/page.tsx +6 -8
- package/templates/pages/table-grouped/page.tsx +9 -14
- package/templates/pages/table-page/page.tsx +7 -9
- package/templates/pages/table-page-heatmap-status/page.tsx +5 -13
- package/docs/integration-authoring.md +0 -105
- package/src/utils/interactive.mjs +0 -76
- package/src/utils/interactive.test.mjs +0 -70
package/src/lib/json.mjs
CHANGED
|
@@ -112,10 +112,10 @@ export function jsonOut(type, data, meta) {
|
|
|
112
112
|
* consumers can branch on it unconditionally.
|
|
113
113
|
*
|
|
114
114
|
* @param {unknown} err
|
|
115
|
-
* @param {
|
|
115
|
+
* @param {import('../types/base').Suggestion[]} [suggestions]
|
|
116
116
|
* @param {string} [code] - Explicit stable error code. Overrides any code
|
|
117
117
|
* carried on a thrown Error.
|
|
118
|
-
* @returns {{apiVersion: number, error: string, code: string, suggestions?:
|
|
118
|
+
* @returns {{apiVersion: number, error: string, code: string, suggestions?: import('../types/base').Suggestion[]}}
|
|
119
119
|
*/
|
|
120
120
|
export function toErrorEnvelope(err, suggestions, code) {
|
|
121
121
|
const message =
|
|
@@ -136,7 +136,7 @@ export function toErrorEnvelope(err, suggestions, code) {
|
|
|
136
136
|
/**
|
|
137
137
|
* Output a structured JSON error and exit.
|
|
138
138
|
* @param {string} message
|
|
139
|
-
* @param {
|
|
139
|
+
* @param {import('../types/base').Suggestion[]} [suggestions]
|
|
140
140
|
* @param {string} [code] - Stable machine-readable error code (error-codes.mjs).
|
|
141
141
|
*/
|
|
142
142
|
export function jsonError(message, suggestions, code) {
|
|
@@ -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
|
+
}
|
package/src/types/api.d.ts
CHANGED
|
@@ -44,17 +44,14 @@ import type {
|
|
|
44
44
|
import type {SearchResponse, SearchDomain} from './search';
|
|
45
45
|
import type {ErrorCode} from './error-codes';
|
|
46
46
|
import type {DoctorResponse} from './doctor';
|
|
47
|
+
import type {Suggestion} from './base';
|
|
47
48
|
|
|
48
49
|
/** Structured API error with a stable machine-readable code. */
|
|
49
50
|
export declare class AstryxError extends Error {
|
|
50
51
|
/** Stable error code; consumers branch on this, never the message. */
|
|
51
52
|
code: ErrorCode;
|
|
52
|
-
suggestions?:
|
|
53
|
-
constructor(
|
|
54
|
-
message: string,
|
|
55
|
-
suggestions?: Array<{name: string; reason: string}>,
|
|
56
|
-
code?: ErrorCode,
|
|
57
|
-
);
|
|
53
|
+
suggestions?: Suggestion[];
|
|
54
|
+
constructor(message: string, suggestions?: Suggestion[], code?: ErrorCode);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
// ── Component ────────────────────────────────────────────────────────
|
|
@@ -100,9 +97,7 @@ export interface DocsOptions {
|
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
type DocsResult =
|
|
103
|
-
|
|
|
104
|
-
| DocsDetailResponse
|
|
105
|
-
| DocsDetailSectionResponse;
|
|
100
|
+
DocsListResponse | DocsDetailResponse | DocsDetailSectionResponse;
|
|
106
101
|
|
|
107
102
|
export declare function docs(
|
|
108
103
|
topic?: string,
|
package/src/types/base.d.ts
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
import type {
|
|
12
12
|
ComponentListResponse,
|
|
13
13
|
ComponentBriefResponse,
|
|
14
|
+
ComponentFullResponse,
|
|
14
15
|
ComponentDetailResponse,
|
|
15
16
|
ComponentDetailPropsResponse,
|
|
16
17
|
ComponentDetailSourceResponse,
|
|
17
18
|
ComponentDetailShowcaseResponse,
|
|
19
|
+
ComponentDetailBlocksResponse,
|
|
18
20
|
} from './component';
|
|
19
21
|
import type {
|
|
20
22
|
DiscoverListResponse,
|
|
@@ -42,13 +44,28 @@ import type {
|
|
|
42
44
|
} from './hook';
|
|
43
45
|
import type {SwizzleListResponse, SwizzleCopyResponse} from './swizzle';
|
|
44
46
|
import type {ThemeBuildResponse} from './theme';
|
|
45
|
-
import type {
|
|
47
|
+
import type {
|
|
48
|
+
UpgradeListResponse,
|
|
49
|
+
UpgradeRunResponse,
|
|
50
|
+
UpgradeStatusResponse,
|
|
51
|
+
} from './upgrade';
|
|
46
52
|
import type {SearchResponse} from './search';
|
|
53
|
+
import type {BuildHelpResponse} from './build';
|
|
47
54
|
import type {ErrorCode} from './error-codes';
|
|
48
55
|
import type {ManifestResponse} from './manifest';
|
|
49
56
|
import type {DoctorResponse} from './doctor';
|
|
50
57
|
import type {ValidateIntegrationResponse} from './validate-integration';
|
|
51
58
|
|
|
59
|
+
/**
|
|
60
|
+
* A "did you mean…" suggestion attached to an error. `reason` is optional:
|
|
61
|
+
* some call sites emit bare `{name}` (e.g. a list of candidate component names)
|
|
62
|
+
* with no per-item explanation.
|
|
63
|
+
*/
|
|
64
|
+
export interface Suggestion {
|
|
65
|
+
name: string;
|
|
66
|
+
reason?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
/**
|
|
53
70
|
* Structured error. Check `'error' in result` to discriminate.
|
|
54
71
|
*
|
|
@@ -58,7 +75,7 @@ import type {ValidateIntegrationResponse} from './validate-integration';
|
|
|
58
75
|
export interface CLIError {
|
|
59
76
|
error: string;
|
|
60
77
|
code: ErrorCode;
|
|
61
|
-
suggestions?:
|
|
78
|
+
suggestions?: Suggestion[];
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
/** Returned by the fallback hook for commands without --json support. */
|
|
@@ -74,10 +91,12 @@ export type CLIResult<T> = T | CLIError | CLIUnsupportedError;
|
|
|
74
91
|
export type CLIAnyResponse =
|
|
75
92
|
| ComponentListResponse
|
|
76
93
|
| ComponentBriefResponse
|
|
94
|
+
| ComponentFullResponse
|
|
77
95
|
| ComponentDetailResponse
|
|
78
96
|
| ComponentDetailPropsResponse
|
|
79
97
|
| ComponentDetailSourceResponse
|
|
80
98
|
| ComponentDetailShowcaseResponse
|
|
99
|
+
| ComponentDetailBlocksResponse
|
|
81
100
|
| DiscoverListResponse
|
|
82
101
|
| DiscoverDetailResponse
|
|
83
102
|
| DiscoverDetailDocResponse
|
|
@@ -99,7 +118,9 @@ export type CLIAnyResponse =
|
|
|
99
118
|
| ThemeBuildResponse
|
|
100
119
|
| UpgradeListResponse
|
|
101
120
|
| UpgradeRunResponse
|
|
121
|
+
| UpgradeStatusResponse
|
|
102
122
|
| SearchResponse
|
|
123
|
+
| BuildHelpResponse
|
|
103
124
|
| ManifestResponse
|
|
104
125
|
| DoctorResponse
|
|
105
126
|
| ValidateIntegrationResponse;
|
|
@@ -129,7 +150,7 @@ export function jsonOut<T extends CLIResponseType>(
|
|
|
129
150
|
*/
|
|
130
151
|
export function jsonError(
|
|
131
152
|
message: string,
|
|
132
|
-
suggestions?:
|
|
153
|
+
suggestions?: Suggestion[],
|
|
133
154
|
code?: ErrorCode,
|
|
134
155
|
): never;
|
|
135
156
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build command JSON responses.
|
|
5
|
+
*
|
|
6
|
+
* `astryx build` is the "assemble a page" verb. With no query it emits the
|
|
7
|
+
* workflow playbook; with a query it delegates to the search pipeline and
|
|
8
|
+
* emits a `search` response (see search.d.ts).
|
|
9
|
+
*
|
|
10
|
+
* Invocation -> type discriminator
|
|
11
|
+
* ---------------------------------------------------
|
|
12
|
+
* xds --json build -> build.help
|
|
13
|
+
* xds --json build "<idea>" -> search
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** xds --json build (no query) — the "how to build a page" playbook signal. */
|
|
17
|
+
export interface BuildHelpResponse {
|
|
18
|
+
type: 'build.help';
|
|
19
|
+
data: {
|
|
20
|
+
/** Always true; marks this envelope as the playbook rather than a result set. */
|
|
21
|
+
playbook: true;
|
|
22
|
+
};
|
|
23
|
+
}
|
package/src/types/index.d.ts
CHANGED
package/src/types/swizzle.d.ts
CHANGED
|
@@ -29,9 +29,13 @@ export interface SwizzleCopyResponse {
|
|
|
29
29
|
type: 'swizzle.copy';
|
|
30
30
|
data: {
|
|
31
31
|
component: string;
|
|
32
|
+
/** Owner package the component source was copied from. */
|
|
33
|
+
package: string;
|
|
32
34
|
outputDir: string;
|
|
33
35
|
filesCopied: number;
|
|
34
36
|
files: string[];
|
|
37
|
+
/** Whether any copied file uses StyleX (requires build-time setup). */
|
|
38
|
+
usesStyleX: boolean;
|
|
35
39
|
feedback?: SwizzleFeedback;
|
|
36
40
|
};
|
|
37
41
|
}
|
|
@@ -15,4 +15,7 @@ export type {
|
|
|
15
15
|
AstryxTemplate,
|
|
16
16
|
} from '@astryxdesign/core/authoring';
|
|
17
17
|
|
|
18
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
createPageTemplate,
|
|
20
|
+
createBlockTemplate,
|
|
21
|
+
} from '@astryxdesign/core/authoring';
|
package/src/types/upgrade.d.ts
CHANGED
|
@@ -23,6 +23,31 @@ export interface UpgradeListEntry {
|
|
|
23
23
|
version: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* State of the managed agent-docs block (`<!-- ASTRYX:START --> … END -->`)
|
|
28
|
+
* relative to the installed core version, plus what `upgrade` did about it.
|
|
29
|
+
* Present on every upgrade response (run, status, and the codemod/config error
|
|
30
|
+
* envelopes) because the block is refreshed independently of codemods.
|
|
31
|
+
*
|
|
32
|
+
* - `refreshed` — a stale block was rewritten (`--apply` only).
|
|
33
|
+
* - `would-refresh` — a stale block was detected in dry-run; nothing written.
|
|
34
|
+
* - `nudge-init` — no managed block exists; user should run `init`.
|
|
35
|
+
* - `error` — refresh was attempted but writing failed.
|
|
36
|
+
* - `none` — nothing to do (block already current).
|
|
37
|
+
*/
|
|
38
|
+
export interface AgentDocsSummary {
|
|
39
|
+
status: 'missing' | 'stale' | 'current';
|
|
40
|
+
/** Installed core version the block should reflect. */
|
|
41
|
+
installedVersion: string;
|
|
42
|
+
/** Distinct stale block versions found (the "from" side of the refresh). */
|
|
43
|
+
fromVersions: string[];
|
|
44
|
+
/** Files rewritten (apply) or that would be rewritten (dry-run). */
|
|
45
|
+
files: string[];
|
|
46
|
+
/** True only when a block was actually rewritten (apply mode). */
|
|
47
|
+
refreshed: boolean;
|
|
48
|
+
action: 'refreshed' | 'would-refresh' | 'nudge-init' | 'error' | 'none';
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
/** xds --json upgrade [--apply] */
|
|
27
52
|
export interface UpgradeRunResponse {
|
|
28
53
|
type: 'upgrade.run';
|
|
@@ -32,6 +57,7 @@ export interface UpgradeRunResponse {
|
|
|
32
57
|
codemods: number;
|
|
33
58
|
depsUpdated: string[];
|
|
34
59
|
agentDocsRefreshed: boolean;
|
|
60
|
+
agentDocs: AgentDocsSummary;
|
|
35
61
|
};
|
|
36
62
|
}
|
|
37
63
|
|
|
@@ -49,8 +75,18 @@ export interface UpgradeRunResponse {
|
|
|
49
75
|
export interface UpgradeStatusResponse {
|
|
50
76
|
type: 'upgrade.status';
|
|
51
77
|
data:
|
|
52
|
-
| {
|
|
53
|
-
|
|
78
|
+
| {
|
|
79
|
+
status: 'up_to_date';
|
|
80
|
+
from: string;
|
|
81
|
+
to: string;
|
|
82
|
+
agentDocs: AgentDocsSummary;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
status: 'no_codemods';
|
|
86
|
+
from: string;
|
|
87
|
+
to: string;
|
|
88
|
+
agentDocs: AgentDocsSummary;
|
|
89
|
+
}
|
|
54
90
|
| {
|
|
55
91
|
status: 'config_fixable';
|
|
56
92
|
from: string;
|
|
@@ -60,5 +96,6 @@ export interface UpgradeStatusResponse {
|
|
|
60
96
|
suggestedCommand: string;
|
|
61
97
|
message: string;
|
|
62
98
|
note: string;
|
|
99
|
+
agentDocs: AgentDocsSummary;
|
|
63
100
|
};
|
|
64
101
|
}
|
|
@@ -10,15 +10,41 @@
|
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A package manager we can install with and run binaries through.
|
|
15
|
+
* @typedef {'yarn' | 'pnpm' | 'bun' | 'npm'} PackageManager
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Result of package-manager detection. `'npx'` is the sentinel for "nothing
|
|
20
|
+
* detected" — no lockfile, no `packageManager` field, no runner user-agent —
|
|
21
|
+
* so callers fall back to npm/npx. It is a distinct value from {@link PackageManager}
|
|
22
|
+
* because it means "undetected", not "npm was chosen".
|
|
23
|
+
* @typedef {PackageManager | 'npx'} DetectedPackageManager
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** @type {readonly PackageManager[]} */
|
|
27
|
+
const KNOWN_PMS = ['yarn', 'pnpm', 'bun', 'npm'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Narrow an arbitrary string to a known {@link PackageManager}.
|
|
31
|
+
* @param {string} name
|
|
32
|
+
* @returns {name is PackageManager}
|
|
33
|
+
*/
|
|
34
|
+
function isKnownPackageManager(name) {
|
|
35
|
+
return /** @type {readonly string[]} */ (KNOWN_PMS).includes(name);
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
/**
|
|
14
39
|
* Detect the package manager used in a project directory.
|
|
15
40
|
* Walks up from targetDir looking for lockfiles.
|
|
16
41
|
*
|
|
42
|
+
* Returns `'npx'` when nothing can be detected — see {@link DetectedPackageManager}.
|
|
43
|
+
*
|
|
17
44
|
* @param {string} [targetDir=process.cwd()]
|
|
18
|
-
* @returns {
|
|
45
|
+
* @returns {DetectedPackageManager}
|
|
19
46
|
*/
|
|
20
47
|
export function detectPackageManager(targetDir = process.cwd()) {
|
|
21
|
-
const KNOWN_PMS = new Set(['yarn', 'pnpm', 'bun', 'npm']);
|
|
22
48
|
let dir = path.resolve(targetDir);
|
|
23
49
|
const root = path.parse(dir).root;
|
|
24
50
|
|
|
@@ -36,7 +62,7 @@ export function detectPackageManager(targetDir = process.cwd()) {
|
|
|
36
62
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
37
63
|
if (pkg.packageManager) {
|
|
38
64
|
const name = pkg.packageManager.split('@')[0];
|
|
39
|
-
if (
|
|
65
|
+
if (isKnownPackageManager(name)) return name;
|
|
40
66
|
}
|
|
41
67
|
} catch {
|
|
42
68
|
// Best-effort: unreadable/invalid package.json — keep walking up.
|
|
@@ -50,7 +76,7 @@ export function detectPackageManager(targetDir = process.cwd()) {
|
|
|
50
76
|
const ua = process.env.npm_config_user_agent;
|
|
51
77
|
if (ua) {
|
|
52
78
|
const name = ua.split('/')[0];
|
|
53
|
-
if (
|
|
79
|
+
if (isKnownPackageManager(name)) return name;
|
|
54
80
|
}
|
|
55
81
|
|
|
56
82
|
return 'npx';
|
|
@@ -72,3 +98,81 @@ export function getRunPrefix(targetDir) {
|
|
|
72
98
|
default: return 'npx';
|
|
73
99
|
}
|
|
74
100
|
}
|
|
101
|
+
|
|
102
|
+
/** The published CLI package name — used for one-off (uninstalled) invocations. */
|
|
103
|
+
export const CLI_PACKAGE = '@astryxdesign/cli';
|
|
104
|
+
|
|
105
|
+
/** The CLI binary name — only resolves once the CLI is installed (or run via CLI_PACKAGE). */
|
|
106
|
+
export const CLI_BIN = 'astryx';
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the one-off ("dlx") runner for the detected package manager.
|
|
110
|
+
*
|
|
111
|
+
* Unlike {@link getRunPrefix} (which runs an *installed* binary), this fetches
|
|
112
|
+
* and runs a package on demand — so it is always paired with the scoped
|
|
113
|
+
* {@link CLI_PACKAGE}, never the bare `astryx` bin. Running bare `npx astryx`
|
|
114
|
+
* without the CLI installed resolves to an unrelated package on the registry.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} [targetDir]
|
|
117
|
+
* @returns {string} e.g. 'npx', 'pnpm dlx', 'yarn dlx', 'bunx'
|
|
118
|
+
*/
|
|
119
|
+
export function getDlxPrefix(targetDir) {
|
|
120
|
+
const pm = detectPackageManager(targetDir);
|
|
121
|
+
switch (pm) {
|
|
122
|
+
case 'yarn': return 'yarn dlx';
|
|
123
|
+
case 'pnpm': return 'pnpm dlx';
|
|
124
|
+
case 'bun': return 'bunx';
|
|
125
|
+
case 'npm':
|
|
126
|
+
default: return 'npx';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Heuristic: was the running CLI launched one-off via a package runner
|
|
132
|
+
* (npx / pnpm dlx / yarn dlx / bunx) rather than from an installed dependency?
|
|
133
|
+
*
|
|
134
|
+
* We sniff the entry path (`process.argv[1]`) for well-known runner-cache
|
|
135
|
+
* markers. This errs safe in both directions: a false negative falls back to
|
|
136
|
+
* the installed form (`<prefix> astryx`, the historical behavior), and a false
|
|
137
|
+
* positive emits the always-valid scoped form (`<dlx> @astryxdesign/cli`).
|
|
138
|
+
*
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
export function isCliOneOff() {
|
|
142
|
+
const entry = String(process.argv[1] || '').replace(/\\/g, '/');
|
|
143
|
+
return /\/_npx\/|\/dlx[-/]|\/\.bun\/install\/cache\/|\/bunx-/.test(entry);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The safe, install-aware CLI invocation stem to suggest to users.
|
|
148
|
+
*
|
|
149
|
+
* - Installed / global / dev: `<run-prefix> astryx` (e.g. `pnpm exec astryx`).
|
|
150
|
+
* Bare `astryx` resolves to the local (or global) binary.
|
|
151
|
+
* - One-off (npx/dlx cache): `<dlx-prefix> @astryxdesign/cli` — the bare
|
|
152
|
+
* `astryx` name isn't on disk, so npm would fetch an unrelated registry
|
|
153
|
+
* package; the scoped package always resolves to us.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} [targetDir]
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
export function getCliInvocation(targetDir) {
|
|
159
|
+
if (isCliOneOff()) return `${getDlxPrefix(targetDir)} ${CLI_PACKAGE}`;
|
|
160
|
+
return `${getRunPrefix(targetDir)} ${CLI_BIN}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format a full, runnable CLI command from a subcommand string.
|
|
165
|
+
*
|
|
166
|
+
* Accepts either `astryx component Button` or `component Button` (a leading
|
|
167
|
+
* `astryx` token is stripped) and prepends the install-aware invocation stem
|
|
168
|
+
* from {@link getCliInvocation}.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} command e.g. 'astryx component Button' | 'docs tokens'
|
|
171
|
+
* @param {string} [targetDir]
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
export function formatCliCommand(command, targetDir) {
|
|
175
|
+
const sub = String(command).replace(/^\s*astryx\b\s*/, '').trim();
|
|
176
|
+
const stem = getCliInvocation(targetDir);
|
|
177
|
+
return sub ? `${stem} ${sub}` : stem;
|
|
178
|
+
}
|
|
@@ -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,12 +14,13 @@
|
|
|
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.
|
|
20
|
-
* No network calls — purely
|
|
21
|
+
* No network calls — purely an env-var check ($ASTRYX_LATEST_VERSION),
|
|
22
|
+
* so it takes no arguments.
|
|
21
23
|
*
|
|
22
|
-
* @param {string} [cwd] - Project directory (default: process.cwd())
|
|
23
24
|
* @returns {string|null} Latest version string, or null if unknown
|
|
24
25
|
*/
|
|
25
26
|
export function getLatestVersion() {
|
|
@@ -63,7 +64,7 @@ export function getInstalledVersion(cwd = process.cwd()) {
|
|
|
63
64
|
* @returns {string|null} FYI hint string, or null if up to date / unknown
|
|
64
65
|
*/
|
|
65
66
|
export function checkForUpdate(cwd = process.cwd()) {
|
|
66
|
-
const latest = getLatestVersion(
|
|
67
|
+
const latest = getLatestVersion();
|
|
67
68
|
if (!latest) return null;
|
|
68
69
|
|
|
69
70
|
// Persist for subsequent commands in this shell session
|
|
@@ -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;
|
|
@@ -33,12 +33,12 @@ afterEach(() => {
|
|
|
33
33
|
describe('getLatestVersion', () => {
|
|
34
34
|
it('reads from ASTRYX_LATEST_VERSION env var', () => {
|
|
35
35
|
process.env.ASTRYX_LATEST_VERSION = '0.0.8';
|
|
36
|
-
expect(getLatestVersion(
|
|
36
|
+
expect(getLatestVersion()).toBe('0.0.8');
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it('ignores invalid env var values', () => {
|
|
40
40
|
process.env.ASTRYX_LATEST_VERSION = 'not-a-version';
|
|
41
|
-
expect(getLatestVersion(
|
|
41
|
+
expect(getLatestVersion()).toBeNull();
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
it('returns null when no signals exist', () => {
|
|
@@ -46,7 +46,7 @@ describe('getLatestVersion', () => {
|
|
|
46
46
|
path.join(tmpDir, 'package.json'),
|
|
47
47
|
JSON.stringify({name: 'test'}),
|
|
48
48
|
);
|
|
49
|
-
expect(getLatestVersion(
|
|
49
|
+
expect(getLatestVersion()).toBeNull();
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|