@astryxdesign/cli 0.1.7 → 0.1.8-canary.4942fe4
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/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 +11 -10
- package/scripts/postinstall.mjs +74 -0
- package/src/api/doctor.mjs +3 -3
- 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 +11 -34
- 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/component-format.mjs +2 -1
- package/src/lib/term-log.mjs +48 -0
- package/src/types/upgrade.d.ts +39 -2
- 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/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/Carousel/CarouselSnap.tsx +1 -1
- 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/Item/ItemWithMedia.tsx +2 -2
- package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
- 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/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,108 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Guardrail tests — the centralized setup check + enforcement layer 3.
|
|
5
|
+
*
|
|
6
|
+
* Unit: isAstryxInitialized() detects the Astryx marker across EVERY agent-doc
|
|
7
|
+
* location (including the previously-missed Hermes files) and legacy XDS blocks.
|
|
8
|
+
*
|
|
9
|
+
* Integration: the CLI nudges (stderr) before a command when the project hasn't
|
|
10
|
+
* run init — INCLUDING in --json mode (agents pass --json, and stderr never
|
|
11
|
+
* corrupts the stdout JSON envelope). Quiet once set up, outside a project, and
|
|
12
|
+
* for the installer command itself.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
16
|
+
import {spawnSync} from 'node:child_process';
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import * as os from 'node:os';
|
|
20
|
+
import {fileURLToPath} from 'node:url';
|
|
21
|
+
import {isAstryxInitialized} from './agent-docs.mjs';
|
|
22
|
+
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const CLI = path.resolve(__dirname, '..', '..', 'bin', 'astryx.mjs');
|
|
25
|
+
const MARKER = '<!-- ASTRYX:START -->';
|
|
26
|
+
const NUDGE = /finish setup and install the Astryx agent prompt/;
|
|
27
|
+
|
|
28
|
+
let tmp;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-setup-nudge-'));
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
fs.rmSync(tmp, {recursive: true, force: true});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function write(rel, body) {
|
|
37
|
+
const p = path.join(tmp, rel);
|
|
38
|
+
fs.mkdirSync(path.dirname(p), {recursive: true});
|
|
39
|
+
fs.writeFileSync(p, body);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function run(args, cwd = tmp) {
|
|
43
|
+
return spawnSync(process.execPath, [CLI, ...args], {
|
|
44
|
+
cwd,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
timeout: 30_000,
|
|
47
|
+
env: {...process.env, FORCE_COLOR: '0'},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('isAstryxInitialized — centralized setup check (one place)', () => {
|
|
52
|
+
it('is false in an empty project', () => {
|
|
53
|
+
expect(isAstryxInitialized(tmp)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Covers EVERY location a preset can write — .hermes.md/HERMES.md were the gap.
|
|
57
|
+
it.each(['AGENTS.md', 'CLAUDE.md', '.claude/CLAUDE.md', '.cursorrules', '.hermes.md', 'HERMES.md'])(
|
|
58
|
+
'detects the marker in %s',
|
|
59
|
+
file => {
|
|
60
|
+
write(file, `# doc\n${MARKER}\nbody`);
|
|
61
|
+
expect(isAstryxInitialized(tmp)).toBe(true);
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
it('detects the legacy XDS marker for back-compat', () => {
|
|
66
|
+
write('AGENTS.md', '<!-- XDS:START -->');
|
|
67
|
+
expect(isAstryxInitialized(tmp)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('is false when a doc file exists WITHOUT the marker', () => {
|
|
71
|
+
write('AGENTS.md', 'project notes, no astryx block');
|
|
72
|
+
expect(isAstryxInitialized(tmp)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('enforcement layer 3 — per-command setup nudge', () => {
|
|
77
|
+
const asProject = () => write('package.json', '{"name":"t"}');
|
|
78
|
+
|
|
79
|
+
it('nudges on stderr after a command when not set up', () => {
|
|
80
|
+
asProject();
|
|
81
|
+
const r = run(['docs', 'tokens']);
|
|
82
|
+
expect(r.stderr).toMatch(NUDGE);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is suppressed in --json (machine mode stays clean), stdout valid JSON', () => {
|
|
86
|
+
asProject();
|
|
87
|
+
const r = run(['--json', 'docs', 'tokens']);
|
|
88
|
+
// --json is machine output with a clean stdout+stderr contract; the human
|
|
89
|
+
// nudge must NOT leak into it (json-shim: error envelopes have empty stderr).
|
|
90
|
+
expect(r.stderr).not.toMatch(NUDGE);
|
|
91
|
+
expect(() => JSON.parse(r.stdout)).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('is quiet once set up (marker present)', () => {
|
|
95
|
+
asProject();
|
|
96
|
+
write('AGENTS.md', MARKER);
|
|
97
|
+
expect(run(['docs', 'tokens']).stderr).not.toMatch(NUDGE);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('is quiet outside a project (no package.json)', () => {
|
|
101
|
+
expect(run(['docs', 'tokens']).stderr).not.toMatch(NUDGE);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does not nudge for the installer command itself', () => {
|
|
105
|
+
asProject();
|
|
106
|
+
expect(run(['init']).stderr).not.toMatch(NUDGE);
|
|
107
|
+
});
|
|
108
|
+
});
|
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
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Pipeline (--apply):
|
|
13
13
|
* 1. Read installed @astryxdesign/core (or legacy @xds/core) version
|
|
14
|
-
* 2.
|
|
15
|
-
*
|
|
14
|
+
* 2. Refresh the managed agent-docs block (AGENTS.md / CLAUDE.md) to the
|
|
15
|
+
* installed version — runs on EVERY path (including the up-to-date /
|
|
16
|
+
* no-codemods short-circuits), because the block documents the installed
|
|
17
|
+
* library, not the codemod outcome. Dry-run reports without writing.
|
|
18
|
+
* 3. Run codemods for --from → installed version
|
|
16
19
|
*
|
|
17
20
|
* Options:
|
|
18
21
|
* --from <version> Previous version before the dependency upgrade
|
|
@@ -37,7 +40,7 @@ import * as fs from 'node:fs';
|
|
|
37
40
|
import * as path from 'node:path';
|
|
38
41
|
import {execFile} from 'node:child_process';
|
|
39
42
|
import {promisify} from 'node:util';
|
|
40
|
-
import * as p from '
|
|
43
|
+
import * as p from '../lib/term-log.mjs';
|
|
41
44
|
import {ensureJscodeshift} from '../codemods/ensure-jscodeshift.mjs';
|
|
42
45
|
import {getTransformsBetween, latestVersion} from '../codemods/registry.mjs';
|
|
43
46
|
import {runCodemods} from '../codemods/runner.mjs';
|
|
@@ -46,8 +49,8 @@ import {
|
|
|
46
49
|
selectIntegrationCodemods,
|
|
47
50
|
} from '../codemods/integration-discovery.mjs';
|
|
48
51
|
import {runIntegrationCodemods} from '../codemods/integration-runner.mjs';
|
|
49
|
-
import {installAgentDocs,
|
|
50
|
-
import {
|
|
52
|
+
import {installAgentDocs, inspectAgentDocs} from './agent-docs.mjs';
|
|
53
|
+
import {getCliInvocation, formatCliCommand} from '../utils/package-manager.mjs';
|
|
51
54
|
import {isValidSemver, semverGte} from '../utils/semver.mjs';
|
|
52
55
|
import {jsonOut, jsonError} from '../lib/json.mjs';
|
|
53
56
|
import {Project} from '../lib/project.mjs';
|
|
@@ -135,6 +138,103 @@ async function runPostCodemodHooks(hooks, context, silent) {
|
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Refresh (or, in dry-run, report) the managed agent-docs block after a version
|
|
143
|
+
* bump. The block (`<!-- ASTRYX:START --> … <!-- ASTRYX:END -->`) documents the
|
|
144
|
+
* INSTALLED library — its version, component index, and agent rules — so it must
|
|
145
|
+
* be re-synced on EVERY upgrade path, including the up-to-date / no-codemods
|
|
146
|
+
* short-circuits where no source file changes. That was the gap in #4168: an
|
|
147
|
+
* agent reading a stale index queries missing components and follows superseded
|
|
148
|
+
* rules. Three cases, one detection pass:
|
|
149
|
+
*
|
|
150
|
+
* - `stale` — a managed block records an older version (or a legacy XDS
|
|
151
|
+
* marker). `--apply` rewrites it; dry-run reports the pending
|
|
152
|
+
* refresh as a loud next step and writes nothing.
|
|
153
|
+
* - `missing` — core is installed but no managed block exists anywhere (the repo
|
|
154
|
+
* never ran `init`, or its agent docs were never marked). We never
|
|
155
|
+
* silently create docs mid-upgrade; we nudge to run `init`.
|
|
156
|
+
* - `current` — every block already matches the installed version: stay silent.
|
|
157
|
+
*
|
|
158
|
+
* @param {{cwd: string, installedVersion: string, apply: boolean, json: boolean}} ctx
|
|
159
|
+
* @returns {import('../types/upgrade').AgentDocsSummary}
|
|
160
|
+
*/
|
|
161
|
+
export function refreshAgentDocs({cwd, installedVersion, apply, json}) {
|
|
162
|
+
const inspection = inspectAgentDocs(cwd, installedVersion);
|
|
163
|
+
/** @type {import('../types/upgrade').AgentDocsSummary} */
|
|
164
|
+
const summary = {
|
|
165
|
+
status: inspection.status,
|
|
166
|
+
installedVersion,
|
|
167
|
+
fromVersions: inspection.blockVersions,
|
|
168
|
+
files: [],
|
|
169
|
+
refreshed: false,
|
|
170
|
+
action: 'none',
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Never initialized — don't silently create docs during an upgrade; nudge.
|
|
174
|
+
if (inspection.status === 'missing') {
|
|
175
|
+
summary.action = 'nudge-init';
|
|
176
|
+
if (!json) {
|
|
177
|
+
p.log.warn(
|
|
178
|
+
`No Astryx agent-docs block found — AI agents have no component index. Run \`${formatCliCommand('astryx init --features agents')}\` to install it.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return summary;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (inspection.status === 'current') return summary;
|
|
185
|
+
|
|
186
|
+
// Stale.
|
|
187
|
+
summary.files = inspection.staleFiles;
|
|
188
|
+
const fromLabel = summary.fromVersions.length
|
|
189
|
+
? `v${summary.fromVersions.join(', v')}`
|
|
190
|
+
: 'an unknown version';
|
|
191
|
+
|
|
192
|
+
if (!apply) {
|
|
193
|
+
// Dry-run: report the pending change, never write.
|
|
194
|
+
summary.action = 'would-refresh';
|
|
195
|
+
if (!json) {
|
|
196
|
+
p.log.warn(
|
|
197
|
+
`Agent docs are stale: block is at ${fromLabel}, installed is v${installedVersion}. Re-run with --apply to refresh (${summary.files.join(', ')}).`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return summary;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Apply: rewrite only files that already carry a marker (onlyReplace).
|
|
204
|
+
try {
|
|
205
|
+
const written = installAgentDocs(cwd, {onlyReplace: true});
|
|
206
|
+
summary.refreshed = written.length > 0;
|
|
207
|
+
summary.files = written;
|
|
208
|
+
if (summary.refreshed) {
|
|
209
|
+
summary.action = 'refreshed';
|
|
210
|
+
if (!json) {
|
|
211
|
+
p.log.success(
|
|
212
|
+
`Agent docs refreshed → v${installedVersion} (from ${fromLabel}): ${written.join(', ')}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// We detected a stale marked block but rewrote nothing, and
|
|
217
|
+
// installAgentDocs did not throw — the block markers are malformed (e.g. a
|
|
218
|
+
// START with no matching END, so the writer can't safely splice it). Don't
|
|
219
|
+
// fail silently: the block is exactly the artifact agents rely on.
|
|
220
|
+
summary.action = 'error';
|
|
221
|
+
if (!json) {
|
|
222
|
+
p.log.warn(
|
|
223
|
+
`Agent docs look stale but couldn't be refreshed — the <!-- ASTRYX:START -->/<!-- ASTRYX:END --> markers may be malformed. Run \`${formatCliCommand('astryx init --features agents')}\` to reinstall the block.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
summary.action = 'error';
|
|
229
|
+
if (!json) {
|
|
230
|
+
p.log.warn(
|
|
231
|
+
`Could not refresh agent docs. Run \`${formatCliCommand('astryx init --features agents')}\` to update them manually.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return summary;
|
|
236
|
+
}
|
|
237
|
+
|
|
138
238
|
/**
|
|
139
239
|
* Register the `upgrade` command (codemod-driven version migration).
|
|
140
240
|
*/
|
|
@@ -176,7 +276,7 @@ export function registerUpgrade(program) {
|
|
|
176
276
|
|
|
177
277
|
if (!options.list && !options.from) {
|
|
178
278
|
const msg =
|
|
179
|
-
|
|
279
|
+
`Missing required --from. Install the target version first, then run \`${getCliInvocation()} upgrade --from <old-version>\`.`;
|
|
180
280
|
if (json)
|
|
181
281
|
return jsonError(msg, undefined, ERROR_CODES.ERR_INVALID_ARGUMENT);
|
|
182
282
|
p.log.error(msg);
|
|
@@ -237,7 +337,7 @@ export function registerUpgrade(program) {
|
|
|
237
337
|
const installed = detectInstalledTargetVersion();
|
|
238
338
|
if (!installed) {
|
|
239
339
|
const msg =
|
|
240
|
-
|
|
340
|
+
`Could not find installed @astryxdesign/core (or legacy @xds/core). Install the target version first, then rerun \`${getCliInvocation()} upgrade --from <old-version>\`.`;
|
|
241
341
|
if (json)
|
|
242
342
|
return jsonError(msg, undefined, ERROR_CODES.ERR_VERSION_DETECT);
|
|
243
343
|
p.log.error(msg);
|
|
@@ -254,6 +354,19 @@ export function registerUpgrade(program) {
|
|
|
254
354
|
);
|
|
255
355
|
}
|
|
256
356
|
|
|
357
|
+
// Sync the managed agent-docs block to the installed version FIRST. It
|
|
358
|
+
// documents the installed library (version + component index + rules),
|
|
359
|
+
// independent of any source codemods, so it must be refreshed on every
|
|
360
|
+
// path below — including the up-to-date / no-codemods short-circuits that
|
|
361
|
+
// return before codemods ever run (issue #4168). Dry-run reports without
|
|
362
|
+
// writing. Folded into every terminal payload as `agentDocs`.
|
|
363
|
+
const agentDocs = refreshAgentDocs({
|
|
364
|
+
cwd: process.cwd(),
|
|
365
|
+
installedVersion: targetVersion,
|
|
366
|
+
apply: options.apply,
|
|
367
|
+
json,
|
|
368
|
+
});
|
|
369
|
+
|
|
257
370
|
// ───────────────────────────────────────────────────────────────────
|
|
258
371
|
// PIPELINE ORDERING
|
|
259
372
|
//
|
|
@@ -275,6 +388,7 @@ export function registerUpgrade(program) {
|
|
|
275
388
|
status: 'up_to_date',
|
|
276
389
|
from: currentVersion,
|
|
277
390
|
to: targetVersion,
|
|
391
|
+
agentDocs,
|
|
278
392
|
});
|
|
279
393
|
}
|
|
280
394
|
p.log.success('Already up to date — no codemods to run.');
|
|
@@ -390,6 +504,9 @@ export function registerUpgrade(program) {
|
|
|
390
504
|
const codemodFlags = coreConfigCodemodNames
|
|
391
505
|
.map(name => `--codemod ${name}`)
|
|
392
506
|
.join(' ');
|
|
507
|
+
// Canonical (bare) form — this is a structured, machine-executable
|
|
508
|
+
// field in the --json envelope. The human print below is made
|
|
509
|
+
// install-aware via formatCliCommand.
|
|
393
510
|
const suggestedCommand = `astryx upgrade --from ${currentVersion} ${codemodFlags} --apply`;
|
|
394
511
|
const guidance =
|
|
395
512
|
'Your astryx.config currently fails strict validation, but a pending ' +
|
|
@@ -406,10 +523,11 @@ export function registerUpgrade(program) {
|
|
|
406
523
|
suggestedCommand,
|
|
407
524
|
message: guidance,
|
|
408
525
|
note: 'Integrations are skipped in this preview; they will be processed on the --apply run.',
|
|
526
|
+
agentDocs,
|
|
409
527
|
});
|
|
410
528
|
}
|
|
411
529
|
p.log.warn(guidance);
|
|
412
|
-
p.log.info(` ${suggestedCommand}`);
|
|
530
|
+
p.log.info(` ${formatCliCommand(suggestedCommand)}`);
|
|
413
531
|
p.log.info(
|
|
414
532
|
'Integrations are skipped in this preview; they will be processed on the --apply run.',
|
|
415
533
|
);
|
|
@@ -417,11 +535,12 @@ export function registerUpgrade(program) {
|
|
|
417
535
|
return;
|
|
418
536
|
}
|
|
419
537
|
// Genuine config error (apply mode, OR dry-run with no pending core
|
|
420
|
-
// config codemod that would fix it): abort as before.
|
|
538
|
+
// config codemod that would fix it): abort as before. The agent-docs
|
|
539
|
+
// refresh already ran (it's independent of config), so surface it.
|
|
421
540
|
if (json)
|
|
422
541
|
return jsonError(
|
|
423
542
|
err.message,
|
|
424
|
-
|
|
543
|
+
{agentDocs},
|
|
425
544
|
ERROR_CODES.ERR_INVALID_ARGUMENT,
|
|
426
545
|
);
|
|
427
546
|
p.log.error(err.message);
|
|
@@ -499,6 +618,7 @@ export function registerUpgrade(program) {
|
|
|
499
618
|
status: 'no_codemods',
|
|
500
619
|
from: currentVersion,
|
|
501
620
|
to: targetVersion,
|
|
621
|
+
agentDocs,
|
|
502
622
|
});
|
|
503
623
|
}
|
|
504
624
|
p.log.success('No codemods available for this version range.');
|
|
@@ -510,7 +630,7 @@ export function registerUpgrade(program) {
|
|
|
510
630
|
if (totalTransforms === 0 && totalOptional === 0) {
|
|
511
631
|
const msg = `Codemod "${options.codemod}" not found. Use --list to see available codemods.`;
|
|
512
632
|
if (json)
|
|
513
|
-
return jsonError(msg,
|
|
633
|
+
return jsonError(msg, {agentDocs}, ERROR_CODES.ERR_UNKNOWN_CODEMOD);
|
|
514
634
|
p.log.error(msg);
|
|
515
635
|
p.outro('Aborted');
|
|
516
636
|
process.exitCode = 1;
|
|
@@ -532,7 +652,10 @@ export function registerUpgrade(program) {
|
|
|
532
652
|
to: targetVersion,
|
|
533
653
|
codemods: totalTransforms,
|
|
534
654
|
integrations: integrations.map(i => i.name ?? i.__spec),
|
|
535
|
-
|
|
655
|
+
// Refreshed up front (before the gates above), so the receipt just
|
|
656
|
+
// reports what happened. `agentDocsRefreshed` kept for back-compat.
|
|
657
|
+
agentDocsRefreshed: agentDocs.refreshed,
|
|
658
|
+
agentDocs,
|
|
536
659
|
};
|
|
537
660
|
|
|
538
661
|
// Run file-based integration codemods alongside the core registry
|
|
@@ -604,26 +727,9 @@ export function registerUpgrade(program) {
|
|
|
604
727
|
}
|
|
605
728
|
}
|
|
606
729
|
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
const existingDocs = discoverAgentDocs(process.cwd());
|
|
611
|
-
if (existingDocs.length > 0) {
|
|
612
|
-
try {
|
|
613
|
-
// onlyReplace: only update files that already have Astryx markers.
|
|
614
|
-
// Don't inject into files that never had Astryx content.
|
|
615
|
-
const written = installAgentDocs(process.cwd(), {onlyReplace: true});
|
|
616
|
-
receipt.agentDocsRefreshed = written.length > 0;
|
|
617
|
-
if (!json && written.length > 0)
|
|
618
|
-
p.log.success(`Agent docs updated: ${written.join(', ')}`);
|
|
619
|
-
} catch {
|
|
620
|
-
if (!json) {
|
|
621
|
-
p.log.warn(
|
|
622
|
-
`Could not update agent docs. Run \`${getRunPrefix()} astryx init --features agents\` to update manually.`,
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
730
|
+
// NOTE: the managed agent-docs block was already refreshed up front (see
|
|
731
|
+
// refreshAgentDocs after installed-version detection), so it stays in sync
|
|
732
|
+
// even on the short-circuit paths that return before this point.
|
|
627
733
|
|
|
628
734
|
receipt.filesChanged = mergedFilesChanged;
|
|
629
735
|
receipt.transformsApplied = mergedTransformsApplied;
|