@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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: Rename Avatar named sizes to Icon's abbreviated scale
|
|
5
|
+
* @see https://github.com/facebook/astryx/issues/2672
|
|
6
|
+
*
|
|
7
|
+
* As of v0.1.8, Avatar and AvatarGroup use the same abbreviated size scale as
|
|
8
|
+
* Icon (`xsm`/`sm`/`md`/`lg`/`xl`) instead of full words. Pixel values are
|
|
9
|
+
* unchanged — only the names move:
|
|
10
|
+
*
|
|
11
|
+
* tiny (20px) → xsm
|
|
12
|
+
* xsmall (24px) → sm
|
|
13
|
+
* small (36px) → md ← also the new default (was `small`)
|
|
14
|
+
* medium (48px) → lg
|
|
15
|
+
* large (128px) → xl
|
|
16
|
+
*
|
|
17
|
+
* The default size shifts from `small` to `md`, but that is the SAME 36px, so
|
|
18
|
+
* call sites that relied on the default need no change.
|
|
19
|
+
*
|
|
20
|
+
* ## Precision
|
|
21
|
+
*
|
|
22
|
+
* `small`, `medium`, and `large` are common English words that appear as
|
|
23
|
+
* unrelated string/type literals (priorities, statuses, breakpoints, …). To
|
|
24
|
+
* avoid corrupting those, the three AMBIGUOUS names are renamed ONLY in a
|
|
25
|
+
* precise context: a `size` JSX attribute on a component that resolves to an
|
|
26
|
+
* Avatar/AvatarGroup import (alias-aware).
|
|
27
|
+
*
|
|
28
|
+
* `tiny` and `xsmall` are unique to Avatar's scale, so they are additionally
|
|
29
|
+
* renamed in context-blind positions (object properties keyed `size`/`*Size`,
|
|
30
|
+
* Storybook `options` arrays, and size-typed union literals) within files that
|
|
31
|
+
* import Avatar/AvatarGroup.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const meta = {
|
|
35
|
+
title: 'Rename Avatar named sizes tiny/xsmall/small/medium/large → xsm/sm/md/lg/xl',
|
|
36
|
+
description:
|
|
37
|
+
'Avatar and AvatarGroup adopt Icon\'s abbreviated size scale ' +
|
|
38
|
+
'(xsm/sm/md/lg/xl). Pixel values are unchanged; the default moves from ' +
|
|
39
|
+
'`small` to `md` (both 36px). The ambiguous names small/medium/large are ' +
|
|
40
|
+
'only renamed on Avatar/AvatarGroup `size` JSX attributes; the unique ' +
|
|
41
|
+
'names tiny/xsmall are also renamed in size-keyed props, Storybook ' +
|
|
42
|
+
'options, and union types in files importing Avatar/AvatarGroup.',
|
|
43
|
+
pr: '#2672',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Import sources that provide the Astryx Avatar/AvatarGroup components. */
|
|
47
|
+
const IMPORT_SOURCES = new Set([
|
|
48
|
+
'@astryxdesign/core',
|
|
49
|
+
'@astryxdesign/core/Avatar',
|
|
50
|
+
'@astryxdesign/core/AvatarGroup',
|
|
51
|
+
'@xds/core',
|
|
52
|
+
'@xds/core/Avatar',
|
|
53
|
+
'@xds/core/AvatarGroup',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/** Component names whose `size` prop is renamed. */
|
|
57
|
+
const TARGET_IMPORTED_NAMES = new Set(['Avatar', 'AvatarGroup']);
|
|
58
|
+
|
|
59
|
+
/** Old → new size-name mapping. */
|
|
60
|
+
const RENAMES = new Map([
|
|
61
|
+
['tiny', 'xsm'],
|
|
62
|
+
['xsmall', 'sm'],
|
|
63
|
+
['small', 'md'],
|
|
64
|
+
['medium', 'lg'],
|
|
65
|
+
['large', 'xl'],
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ambiguous names — common words that must ONLY be renamed in a precise
|
|
70
|
+
* context. `tiny`/`xsmall` are omitted because they are unique to Avatar's
|
|
71
|
+
* scale; their presence in a collection proves it is an Avatar size enum.
|
|
72
|
+
*/
|
|
73
|
+
const AMBIGUOUS = new Set(['small', 'medium', 'large']);
|
|
74
|
+
|
|
75
|
+
/** Names that appear only in Avatar's scale — safe signals of Avatar sizing. */
|
|
76
|
+
const UNIQUE = new Set(['tiny', 'xsmall']);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Rename a string-literal node if its value is an old size name.
|
|
80
|
+
* Unwraps `'small' as const`. In non-precise contexts, ambiguous names are
|
|
81
|
+
* skipped. Returns true when a rename occurred.
|
|
82
|
+
*/
|
|
83
|
+
function renameValue(node, {precise = false} = {}) {
|
|
84
|
+
if (!node) return false;
|
|
85
|
+
const target = node.type === 'TSAsExpression' ? node.expression : node;
|
|
86
|
+
const isString =
|
|
87
|
+
target.type === 'StringLiteral' || target.type === 'Literal';
|
|
88
|
+
if (!isString || typeof target.value !== 'string') return false;
|
|
89
|
+
const replacement = RENAMES.get(target.value);
|
|
90
|
+
if (!replacement) return false;
|
|
91
|
+
if (!precise && AMBIGUOUS.has(target.value)) return false;
|
|
92
|
+
target.value = replacement;
|
|
93
|
+
if (target.raw) target.raw = undefined;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Extract a plain string value from a literal-ish node, or null. */
|
|
98
|
+
function literalString(node) {
|
|
99
|
+
const target = node?.type === 'TSAsExpression' ? node.expression : node;
|
|
100
|
+
if (
|
|
101
|
+
target &&
|
|
102
|
+
(target.type === 'StringLiteral' || target.type === 'Literal') &&
|
|
103
|
+
typeof target.value === 'string'
|
|
104
|
+
) {
|
|
105
|
+
return target.value;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default function transformer(file, api) {
|
|
111
|
+
const j = api.jscodeshift;
|
|
112
|
+
const root = j(file.source);
|
|
113
|
+
let hasChanges = false;
|
|
114
|
+
|
|
115
|
+
// Track alias-aware local names for the target components.
|
|
116
|
+
const targetLocals = new Set();
|
|
117
|
+
root.find(j.ImportDeclaration).forEach((path) => {
|
|
118
|
+
if (!IMPORT_SOURCES.has(path.node.source.value)) return;
|
|
119
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
120
|
+
if (
|
|
121
|
+
spec.type === 'ImportSpecifier' &&
|
|
122
|
+
TARGET_IMPORTED_NAMES.has(spec.imported.name)
|
|
123
|
+
) {
|
|
124
|
+
targetLocals.add(spec.local.name);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (targetLocals.size === 0) return undefined;
|
|
130
|
+
|
|
131
|
+
function renameArrayElements(node) {
|
|
132
|
+
let arr = node;
|
|
133
|
+
if (arr.type === 'TSAsExpression') arr = arr.expression;
|
|
134
|
+
if (arr.type !== 'ArrayExpression') return;
|
|
135
|
+
// If the array contains a name unique to Avatar's scale (tiny/xsmall), the
|
|
136
|
+
// whole collection is unambiguously the Avatar size enum, so it is safe to
|
|
137
|
+
// rename the ambiguous members too. Otherwise stay conservative.
|
|
138
|
+
const precise = arr.elements.some((el) => UNIQUE.has(literalString(el)));
|
|
139
|
+
for (const el of arr.elements) {
|
|
140
|
+
if (renameValue(el, {precise})) hasChanges = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 1. `size` JSX attributes on target components — PRECISE (component known),
|
|
145
|
+
// so all five names, including the ambiguous ones, are renamed here.
|
|
146
|
+
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
147
|
+
const name = path.node.name;
|
|
148
|
+
const componentName = name.type === 'JSXIdentifier' ? name.name : null;
|
|
149
|
+
if (!componentName || !targetLocals.has(componentName)) return;
|
|
150
|
+
|
|
151
|
+
for (const attr of path.node.attributes) {
|
|
152
|
+
if (attr.type !== 'JSXAttribute' || attr.name?.name !== 'size') continue;
|
|
153
|
+
const value = attr.value;
|
|
154
|
+
|
|
155
|
+
// size="small"
|
|
156
|
+
if (renameValue(value, {precise: true})) {
|
|
157
|
+
hasChanges = true;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
value &&
|
|
162
|
+
value.type === 'JSXExpressionContainer' &&
|
|
163
|
+
value.expression
|
|
164
|
+
) {
|
|
165
|
+
// size={'small'}
|
|
166
|
+
if (renameValue(value.expression, {precise: true})) {
|
|
167
|
+
hasChanges = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// size={cond ? 'small' : 'large'}
|
|
171
|
+
if (value.expression.type === 'ConditionalExpression') {
|
|
172
|
+
if (renameValue(value.expression.consequent, {precise: true}))
|
|
173
|
+
hasChanges = true;
|
|
174
|
+
if (renameValue(value.expression.alternate, {precise: true}))
|
|
175
|
+
hasChanges = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 2. Object properties keyed `size` (or `*Size`) + Storybook `options`
|
|
182
|
+
// arrays — NON-PRECISE (the component isn't known here), so only the
|
|
183
|
+
// unique names tiny/xsmall are renamed.
|
|
184
|
+
const PropertyType = j.ObjectProperty ?? j.Property;
|
|
185
|
+
root.find(PropertyType).forEach((path) => {
|
|
186
|
+
const key = path.node.key;
|
|
187
|
+
const keyName =
|
|
188
|
+
key.type === 'Identifier'
|
|
189
|
+
? key.name
|
|
190
|
+
: key.type === 'StringLiteral' || key.type === 'Literal'
|
|
191
|
+
? key.value
|
|
192
|
+
: null;
|
|
193
|
+
if (typeof keyName !== 'string') return;
|
|
194
|
+
|
|
195
|
+
const lower = keyName.toLowerCase();
|
|
196
|
+
if (lower !== 'size' && !lower.endsWith('size')) return;
|
|
197
|
+
|
|
198
|
+
const value = path.node.value;
|
|
199
|
+
// size: 'tiny'
|
|
200
|
+
if (renameValue(value)) {
|
|
201
|
+
hasChanges = true;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// size: { control: 'select', options: ['tiny', ...] }
|
|
205
|
+
if (value.type === 'ObjectExpression') {
|
|
206
|
+
const optionsProp = value.properties.find(
|
|
207
|
+
(p) => p.key && (p.key.name === 'options' || p.key.value === 'options'),
|
|
208
|
+
);
|
|
209
|
+
if (optionsProp) renameArrayElements(optionsProp.value);
|
|
210
|
+
}
|
|
211
|
+
// size: ['tiny', 'xsmall', ...]
|
|
212
|
+
renameArrayElements(value);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// 3. Union-type literals — NON-PRECISE, so only the unique names tiny/xsmall
|
|
216
|
+
// are renamed. This avoids corrupting unrelated unions that happen to use
|
|
217
|
+
// common words like 'medium' (e.g. a priority or breakpoint type) in a
|
|
218
|
+
// file that also imports Avatar.
|
|
219
|
+
root.find(j.TSLiteralType).forEach((path) => {
|
|
220
|
+
if (renameValue(path.node.literal)) hasChanges = true;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 4. Standalone array literals whose members are exactly the Avatar size
|
|
224
|
+
// scale (identified by the presence of a unique name like tiny/xsmall) —
|
|
225
|
+
// e.g. `(['tiny', 'xsmall', 'small', 'medium', 'large'] as const).map(...)`
|
|
226
|
+
// used to render every size. The unique name proves the whole array is the
|
|
227
|
+
// Avatar enum, so its ambiguous members can be renamed safely.
|
|
228
|
+
root.find(j.ArrayExpression).forEach((path) => {
|
|
229
|
+
renameArrayElements(path.node);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!hasChanges) return undefined;
|
|
233
|
+
return root.toSource({quote: 'single'});
|
|
234
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - Hermes Agent: .hermes.md or HERMES.md (existing), else AGENTS.md
|
|
13
13
|
*
|
|
14
14
|
* Auto-detect: discovers existing files and updates them in place.
|
|
15
|
-
* Default (no existing files): creates .
|
|
15
|
+
* Default (no existing files): creates AGENTS.md (the tool-agnostic standard).
|
|
16
16
|
*
|
|
17
17
|
* --agent <tool>: target a specific tool preset (claude, cursor, codex, hermes, all)
|
|
18
18
|
* --agent-docs-path <path>: explicit file path(s)
|
|
@@ -22,7 +22,7 @@ import * as fs from 'node:fs';
|
|
|
22
22
|
import * as path from 'node:path';
|
|
23
23
|
import {findCoreDir, CLI_ROOT} from '../utils/paths.mjs';
|
|
24
24
|
import {assertWithin, PathSafetyError} from '../utils/path-safety.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import {getCliInvocation} from '../utils/package-manager.mjs';
|
|
26
26
|
import {discoverComponents} from '../lib/component-discovery.mjs';
|
|
27
27
|
import {humanLog} from '../lib/json.mjs';
|
|
28
28
|
import {cliError} from '../lib/cli-error.mjs';
|
|
@@ -31,6 +31,9 @@ import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
|
31
31
|
const AGENTS_MD = 'AGENTS.md';
|
|
32
32
|
const CLAUDE_MD = 'CLAUDE.md';
|
|
33
33
|
const CLAUDE_DIR_MD = '.claude/CLAUDE.md'; // cross-platform literal
|
|
34
|
+
const CURSOR_RULES = '.cursorrules';
|
|
35
|
+
const HERMES_DOT_MD = '.hermes.md';
|
|
36
|
+
const HERMES_MD = 'HERMES.md';
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
const MARKER_START = '<!-- ASTRYX:START -->';
|
|
@@ -45,20 +48,132 @@ const LEGACY_MARKER_END = '<!-- XDS:END -->';
|
|
|
45
48
|
*/
|
|
46
49
|
const AGENT_PRESETS = {
|
|
47
50
|
claude: [CLAUDE_MD, CLAUDE_DIR_MD],
|
|
48
|
-
cursor: [
|
|
51
|
+
cursor: [CURSOR_RULES, AGENTS_MD],
|
|
49
52
|
codex: [AGENTS_MD],
|
|
50
|
-
hermes: [
|
|
53
|
+
hermes: [HERMES_DOT_MD, HERMES_MD, AGENTS_MD],
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
57
|
+
* The canonical set of EVERY location an --agent preset (or the default) can
|
|
58
|
+
* write the Astryx block. SINGLE SOURCE OF TRUTH: discovery, removal, and the
|
|
59
|
+
* `isAstryxInitialized` predicate all derive from this list, so "where init
|
|
60
|
+
* writes" and "where we look" can never drift. (Explicit --agent-docs-path
|
|
61
|
+
* targets are user-chosen and not enumerable here.)
|
|
62
|
+
*/
|
|
63
|
+
const AGENT_DOC_PATHS = [
|
|
64
|
+
AGENTS_MD, // Codex / ChatGPT / generic
|
|
65
|
+
CLAUDE_MD, // Claude Code (root)
|
|
66
|
+
CLAUDE_DIR_MD, // Claude Code (.claude/CLAUDE.md)
|
|
67
|
+
CURSOR_RULES, // Cursor
|
|
68
|
+
HERMES_DOT_MD, // Hermes
|
|
69
|
+
HERMES_MD, // Hermes
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find all existing agent doc files in a directory, across EVERY location any
|
|
74
|
+
* preset can write (see {@link AGENT_DOC_PATHS}: AGENTS.md, CLAUDE.md,
|
|
75
|
+
* .claude/CLAUDE.md, .cursorrules, .hermes.md, HERMES.md).
|
|
56
76
|
* @param {string} targetDir
|
|
57
77
|
* @returns {string[]} Relative paths of existing agent doc files
|
|
58
78
|
*/
|
|
59
79
|
export function discoverAgentDocs(targetDir) {
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
return AGENT_DOC_PATHS.filter(p => fs.existsSync(path.join(targetDir, p)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Single source of truth for "is Astryx set up in this project?" — true when any
|
|
85
|
+
* agent-doc file already carries the Astryx marker, i.e. `init` / `agent-docs`
|
|
86
|
+
* has run. Reused by the init & upgrade commands, the per-command setup nudge
|
|
87
|
+
* (enforcement layer 3), and the cli postinstall nudge (layer 2). Core's
|
|
88
|
+
* postinstall (separate package, layer 1) mirrors the same marker contract.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} [targetDir=process.cwd()]
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
export function isAstryxInitialized(targetDir = process.cwd()) {
|
|
94
|
+
for (const rel of discoverAgentDocs(targetDir)) {
|
|
95
|
+
try {
|
|
96
|
+
const content = fs.readFileSync(path.join(targetDir, rel), 'utf-8');
|
|
97
|
+
if (content.includes(MARKER_START) || content.includes(LEGACY_MARKER_START)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Unreadable file — ignore and keep checking the others.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse the Astryx version a managed block was generated for, from its header
|
|
109
|
+
* line ("Astryx v1.2.3 · N components"). Returns null when the block predates
|
|
110
|
+
* the versioned header (e.g. a legacy XDS block) or has no header at all.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} content File contents, or just the block text.
|
|
113
|
+
* @returns {string|null}
|
|
114
|
+
*/
|
|
115
|
+
export function parseBlockVersion(content) {
|
|
116
|
+
const match = /Astryx v(\d+\.\d+\.\d+[^\s·]*)/.exec(content ?? '');
|
|
117
|
+
return match ? match[1] : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read-only staleness assessment of the managed agent-docs block(s) against the
|
|
122
|
+
* installed core version. This is the detection half of the `astryx upgrade`
|
|
123
|
+
* agent-docs refresh: it never writes, so `upgrade` can run it on EVERY path —
|
|
124
|
+
* including the up-to-date / no-codemods short-circuits — and decide whether to
|
|
125
|
+
* rewrite a stale block, nudge an uninitialized repo, or stay silent.
|
|
126
|
+
*
|
|
127
|
+
* A managed block is:
|
|
128
|
+
* - `stale` — it carries a legacy XDS marker, has no parseable version, or
|
|
129
|
+
* records a version other than the installed one.
|
|
130
|
+
* - `current` — every managed block already matches the installed version.
|
|
131
|
+
* And the project is `missing` when no managed block exists anywhere (the repo
|
|
132
|
+
* has agent-doc files without our markers, or none at all — i.e. never `init`ed).
|
|
133
|
+
*
|
|
134
|
+
* @param {string} targetDir
|
|
135
|
+
* @param {string} [installedVersion] Defaults to the installed core version.
|
|
136
|
+
* @returns {{
|
|
137
|
+
* installedVersion: string,
|
|
138
|
+
* status: 'missing' | 'stale' | 'current',
|
|
139
|
+
* files: Array<{path: string, blockVersion: string|null, legacy: boolean, stale: boolean}>,
|
|
140
|
+
* staleFiles: string[],
|
|
141
|
+
* blockVersions: string[],
|
|
142
|
+
* }}
|
|
143
|
+
*/
|
|
144
|
+
export function inspectAgentDocs(targetDir, installedVersion) {
|
|
145
|
+
const version = installedVersion ?? getXdsVersion(findCoreDir(targetDir));
|
|
146
|
+
const files = [];
|
|
147
|
+
|
|
148
|
+
for (const rel of discoverAgentDocs(targetDir)) {
|
|
149
|
+
let content;
|
|
150
|
+
try {
|
|
151
|
+
content = fs.readFileSync(path.join(targetDir, rel), 'utf-8');
|
|
152
|
+
} catch {
|
|
153
|
+
continue; // Unreadable — treat as absent.
|
|
154
|
+
}
|
|
155
|
+
const hasNew = content.includes(MARKER_START);
|
|
156
|
+
const hasLegacy = content.includes(LEGACY_MARKER_START);
|
|
157
|
+
if (!hasNew && !hasLegacy) continue; // Not a block we manage.
|
|
158
|
+
|
|
159
|
+
const legacy = !hasNew && hasLegacy;
|
|
160
|
+
const blockVersion = parseBlockVersion(content);
|
|
161
|
+
const stale = legacy || blockVersion == null || blockVersion !== version;
|
|
162
|
+
files.push({path: rel, blockVersion, legacy, stale});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const staleEntries = files.filter(f => f.stale);
|
|
166
|
+
const staleFiles = staleEntries.map(f => f.path);
|
|
167
|
+
const blockVersions = [
|
|
168
|
+
...new Set(staleEntries.map(f => f.blockVersion).filter(Boolean)),
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
let status;
|
|
172
|
+
if (files.length === 0) status = 'missing';
|
|
173
|
+
else if (staleFiles.length > 0) status = 'stale';
|
|
174
|
+
else status = 'current';
|
|
175
|
+
|
|
176
|
+
return {installedVersion: version, status, files, staleFiles, blockVersions};
|
|
62
177
|
}
|
|
63
178
|
|
|
64
179
|
/**
|
|
@@ -144,8 +259,8 @@ export function detectStylingSystem(targetDir) {
|
|
|
144
259
|
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
145
260
|
* styling path that isn't compiled here.
|
|
146
261
|
*/
|
|
147
|
-
export function generateCompressedIndex(version, {coreDir,
|
|
148
|
-
const run =
|
|
262
|
+
export function generateCompressedIndex(version, {coreDir, invocation = getCliInvocation(), stylingSystem = 'css'} = {}) {
|
|
263
|
+
const run = invocation;
|
|
149
264
|
const lines = [MARKER_START];
|
|
150
265
|
|
|
151
266
|
// Component count from live discovery
|
|
@@ -392,11 +507,11 @@ export function removeAgentDocs(targetDir) {
|
|
|
392
507
|
|
|
393
508
|
/**
|
|
394
509
|
* Programmatic entry point for installing agent docs.
|
|
395
|
-
* Used by the init
|
|
510
|
+
* Used by the init command, upgrade command, and agent-docs command.
|
|
396
511
|
*
|
|
397
512
|
* Strategy (when no agent/paths specified):
|
|
398
513
|
* - Discover all existing agent doc files and update them
|
|
399
|
-
* - If nothing found, create .
|
|
514
|
+
* - If nothing found, create AGENTS.md as default (tool-agnostic standard)
|
|
400
515
|
*
|
|
401
516
|
* @param {string} targetDir
|
|
402
517
|
* @param {object} [options]
|
|
@@ -410,9 +525,9 @@ export function removeAgentDocs(targetDir) {
|
|
|
410
525
|
export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onlyReplace = false} = {}) {
|
|
411
526
|
const coreDir = findCoreDir(targetDir);
|
|
412
527
|
const version = getXdsVersion(coreDir);
|
|
413
|
-
const
|
|
528
|
+
const invocation = getCliInvocation(targetDir);
|
|
414
529
|
const stylingSystem = detectStylingSystem(targetDir);
|
|
415
|
-
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang,
|
|
530
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, invocation, stylingSystem});
|
|
416
531
|
const written = [];
|
|
417
532
|
|
|
418
533
|
// Explicit paths override everything
|
|
@@ -471,14 +586,16 @@ export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onl
|
|
|
471
586
|
return written;
|
|
472
587
|
}
|
|
473
588
|
|
|
474
|
-
// Nothing exists — create .
|
|
589
|
+
// Nothing exists — create root AGENTS.md as the default (skip if onlyReplace).
|
|
590
|
+
// AGENTS.md is the tool-agnostic standard (Codex/Copilot, Cursor, and most
|
|
591
|
+
// agents read it), so it's the safe default. Claude-specific output is opt-in
|
|
592
|
+
// via `--agent claude` (→ .claude/CLAUDE.md); `--agent all` writes both.
|
|
475
593
|
if (onlyReplace) return written;
|
|
476
594
|
|
|
477
|
-
const defaultPath =
|
|
478
|
-
fs.mkdirSync(path.join(targetDir, '.claude'), {recursive: true});
|
|
595
|
+
const defaultPath = AGENTS_MD;
|
|
479
596
|
injectXdsBlock(path.join(targetDir, defaultPath), compressedIndex, {
|
|
480
597
|
createIfMissing: true,
|
|
481
|
-
header: `#
|
|
598
|
+
header: `# AGENTS.md\n\nProject-specific guidance for AI coding agents.`,
|
|
482
599
|
});
|
|
483
600
|
written.push(defaultPath);
|
|
484
601
|
return written;
|
|
@@ -538,8 +655,7 @@ export function registerAgentDocs(program) {
|
|
|
538
655
|
throw err;
|
|
539
656
|
}
|
|
540
657
|
|
|
541
|
-
const
|
|
542
|
-
const run = `${runPrefix} astryx`;
|
|
658
|
+
const run = getCliInvocation(targetDir);
|
|
543
659
|
|
|
544
660
|
for (const t of targets) {
|
|
545
661
|
humanLog(`✓ ${t}`);
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
removeXdsBlock,
|
|
17
17
|
discoverAgentDocs,
|
|
18
18
|
resolveAgentPaths,
|
|
19
|
+
parseBlockVersion,
|
|
20
|
+
inspectAgentDocs,
|
|
19
21
|
} from './agent-docs.mjs';
|
|
20
22
|
|
|
21
23
|
let tmpDir;
|
|
@@ -88,17 +90,24 @@ describe('generateCompressedIndex', () => {
|
|
|
88
90
|
expect(result).toMatch(/after any @astryxdesign\/core bump/);
|
|
89
91
|
});
|
|
90
92
|
|
|
91
|
-
it('states the
|
|
92
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
93
|
+
it('states the invocation once in the CLI header (yarn)', () => {
|
|
94
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'yarn astryx'});
|
|
93
95
|
expect(result).toContain('yarn astryx <cmd>');
|
|
94
96
|
expect(result).not.toContain('npx astryx');
|
|
95
97
|
});
|
|
96
98
|
|
|
97
|
-
it('uses pnpm exec
|
|
98
|
-
const result = generateCompressedIndex('1.0.0', {
|
|
99
|
+
it('uses the pnpm exec invocation', () => {
|
|
100
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'pnpm exec astryx'});
|
|
99
101
|
expect(result).toContain('pnpm exec astryx <cmd>');
|
|
100
102
|
expect(result).not.toContain('npx astryx');
|
|
101
103
|
});
|
|
104
|
+
|
|
105
|
+
it('uses the scoped package for one-off (uninstalled) runs so agents never hit the bare name', () => {
|
|
106
|
+
const result = generateCompressedIndex('1.0.0', {invocation: 'npx @astryxdesign/cli'});
|
|
107
|
+
expect(result).toContain('npx @astryxdesign/cli <cmd>');
|
|
108
|
+
// The header defines the mapping; the bare "run every command as `npx astryx`" footgun must be absent.
|
|
109
|
+
expect(result).not.toContain('npx astryx <cmd>');
|
|
110
|
+
});
|
|
102
111
|
});
|
|
103
112
|
|
|
104
113
|
describe('detectStylingSystem', () => {
|
|
@@ -389,18 +398,37 @@ describe('installAgentDocs', () => {
|
|
|
389
398
|
);
|
|
390
399
|
}
|
|
391
400
|
|
|
392
|
-
it('creates .
|
|
401
|
+
it('creates AGENTS.md when no agent docs exist (tool-agnostic default)', () => {
|
|
393
402
|
setupCorePackage(tmpDir);
|
|
394
403
|
|
|
395
404
|
const written = installAgentDocs(tmpDir);
|
|
396
405
|
|
|
397
|
-
expect(written).toEqual(['.
|
|
398
|
-
expect(fs.existsSync(path.join(tmpDir, '.
|
|
399
|
-
|
|
400
|
-
|
|
406
|
+
expect(written).toEqual(['AGENTS.md']);
|
|
407
|
+
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(true);
|
|
408
|
+
// Must NOT create the Claude-specific file by default.
|
|
409
|
+
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
410
|
+
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
|
|
411
|
+
expect(content).toContain('# AGENTS.md');
|
|
401
412
|
expect(content).toContain('<!-- ASTRYX:START -->');
|
|
402
413
|
});
|
|
403
414
|
|
|
415
|
+
it('defaults to AGENTS.md but writes .claude/CLAUDE.md only when --agent claude is explicit', () => {
|
|
416
|
+
// Default (no agent): tool-agnostic AGENTS.md, never the Claude file.
|
|
417
|
+
setupCorePackage(tmpDir);
|
|
418
|
+
expect(installAgentDocs(tmpDir)).toEqual(['AGENTS.md']);
|
|
419
|
+
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
420
|
+
|
|
421
|
+
// Explicit Claude: the Claude-specific file, in a fresh project.
|
|
422
|
+
const claudeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-claude-'));
|
|
423
|
+
setupCorePackage(claudeDir);
|
|
424
|
+
try {
|
|
425
|
+
expect(installAgentDocs(claudeDir, {agent: 'claude'})).toEqual(['.claude/CLAUDE.md']);
|
|
426
|
+
expect(fs.existsSync(path.join(claudeDir, 'AGENTS.md'))).toBe(false);
|
|
427
|
+
} finally {
|
|
428
|
+
fs.rmSync(claudeDir, {recursive: true, force: true});
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
404
432
|
it('injects into CLAUDE.md at root when it exists', () => {
|
|
405
433
|
setupCorePackage(tmpDir);
|
|
406
434
|
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '# Claude\n\nProject rules.\n');
|
|
@@ -517,12 +545,13 @@ describe('installAgentDocs', () => {
|
|
|
517
545
|
expect(content).toContain('Other rules.');
|
|
518
546
|
});
|
|
519
547
|
|
|
520
|
-
it('onlyReplace: does not create default .
|
|
548
|
+
it('onlyReplace: does not create the default AGENTS.md when nothing exists', () => {
|
|
521
549
|
setupCorePackage(tmpDir);
|
|
522
550
|
|
|
523
551
|
const written = installAgentDocs(tmpDir, {onlyReplace: true});
|
|
524
552
|
|
|
525
553
|
expect(written).toEqual([]);
|
|
554
|
+
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(false);
|
|
526
555
|
expect(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md'))).toBe(false);
|
|
527
556
|
});
|
|
528
557
|
});
|
|
@@ -602,3 +631,87 @@ describe('resolveAgentPaths', () => {
|
|
|
602
631
|
expect(result).toEqual({inject: [], create: ['.claude/CLAUDE.md']});
|
|
603
632
|
});
|
|
604
633
|
});
|
|
634
|
+
|
|
635
|
+
describe('parseBlockVersion', () => {
|
|
636
|
+
it('reads the version from the header of a generated block', () => {
|
|
637
|
+
expect(parseBlockVersion(generateCompressedIndex('1.2.3'))).toBe('1.2.3');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('reads prerelease versions', () => {
|
|
641
|
+
expect(parseBlockVersion('Astryx v1.2.3-beta.4 · 10 components')).toBe(
|
|
642
|
+
'1.2.3-beta.4',
|
|
643
|
+
);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('returns null when no versioned header is present', () => {
|
|
647
|
+
expect(parseBlockVersion('<!-- XDS:START -->\nold\n<!-- XDS:END -->')).toBeNull();
|
|
648
|
+
expect(parseBlockVersion('')).toBeNull();
|
|
649
|
+
expect(parseBlockVersion(undefined)).toBeNull();
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
describe('inspectAgentDocs', () => {
|
|
654
|
+
function writeBlock(rel, version) {
|
|
655
|
+
const filePath = path.join(tmpDir, rel);
|
|
656
|
+
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
657
|
+
fs.writeFileSync(filePath, `# Doc\n\n${generateCompressedIndex(version)}\n`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
it('reports missing when no agent docs exist at all', () => {
|
|
661
|
+
const res = inspectAgentDocs(tmpDir, '1.0.0');
|
|
662
|
+
expect(res.status).toBe('missing');
|
|
663
|
+
expect(res.files).toEqual([]);
|
|
664
|
+
expect(res.staleFiles).toEqual([]);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it('reports missing when agent files exist but carry no Astryx marker', () => {
|
|
668
|
+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents\n\nHand-written notes.\n');
|
|
669
|
+
expect(inspectAgentDocs(tmpDir, '1.0.0').status).toBe('missing');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('reports current when the only block matches the installed version', () => {
|
|
673
|
+
writeBlock('AGENTS.md', '1.0.0');
|
|
674
|
+
const res = inspectAgentDocs(tmpDir, '1.0.0');
|
|
675
|
+
expect(res.status).toBe('current');
|
|
676
|
+
expect(res.staleFiles).toEqual([]);
|
|
677
|
+
expect(res.blockVersions).toEqual([]);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('reports stale (with the old version) when the block is behind', () => {
|
|
681
|
+
writeBlock('AGENTS.md', '1.0.0');
|
|
682
|
+
const res = inspectAgentDocs(tmpDir, '2.0.0');
|
|
683
|
+
expect(res.status).toBe('stale');
|
|
684
|
+
expect(res.staleFiles).toEqual(['AGENTS.md']);
|
|
685
|
+
expect(res.blockVersions).toEqual(['1.0.0']);
|
|
686
|
+
expect(res.installedVersion).toBe('2.0.0');
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('treats a legacy XDS block as stale even with no parseable version', () => {
|
|
690
|
+
fs.writeFileSync(
|
|
691
|
+
path.join(tmpDir, 'CLAUDE.md'),
|
|
692
|
+
'# Claude\n\n<!-- XDS:START -->\nlegacy index\n<!-- XDS:END -->\n',
|
|
693
|
+
);
|
|
694
|
+
const res = inspectAgentDocs(tmpDir, '1.0.0');
|
|
695
|
+
expect(res.status).toBe('stale');
|
|
696
|
+
expect(res.files[0].legacy).toBe(true);
|
|
697
|
+
expect(res.blockVersions).toEqual([]);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('is stale if ANY marked file is behind (mixed current + stale)', () => {
|
|
701
|
+
writeBlock('AGENTS.md', '2.0.0');
|
|
702
|
+
writeBlock('CLAUDE.md', '1.0.0');
|
|
703
|
+
const res = inspectAgentDocs(tmpDir, '2.0.0');
|
|
704
|
+
expect(res.status).toBe('stale');
|
|
705
|
+
expect(res.staleFiles).toEqual(['CLAUDE.md']);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('defaults the installed version to the core package when omitted', () => {
|
|
709
|
+
const coreDir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
710
|
+
fs.mkdirSync(coreDir, {recursive: true});
|
|
711
|
+
fs.writeFileSync(path.join(coreDir, 'package.json'), JSON.stringify({version: '3.0.0'}));
|
|
712
|
+
writeBlock('AGENTS.md', '3.0.0');
|
|
713
|
+
const res = inspectAgentDocs(tmpDir);
|
|
714
|
+
expect(res.installedVersion).toBe('3.0.0');
|
|
715
|
+
expect(res.status).toBe('current');
|
|
716
|
+
});
|
|
717
|
+
});
|