@astryxdesign/cli 0.1.7-canary.fb9c68c → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -115
- package/docs/getting-started.doc.mjs +9 -9
- package/docs/integration-authoring.md +105 -0
- package/docs/migration.doc.mjs +18 -18
- package/docs/principles.doc.dense.mjs +1 -1
- package/docs/principles.doc.mjs +6 -6
- package/docs/principles.doc.zh.mjs +1 -1
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +4 -4
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +7 -7
- package/docs/theme.doc.zh.mjs +1 -1
- package/docs/tokens.doc.mjs +1 -1
- package/docs/working-with-ai.doc.mjs +18 -18
- package/package.json +10 -11
- package/src/api/doctor.mjs +3 -3
- package/src/codemods/ensure-jscodeshift.mjs +27 -11
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/commands/agent-docs.mjs +14 -56
- package/src/commands/agent-docs.test.mjs +4 -11
- package/src/commands/build-theme.mjs +71 -10
- package/src/commands/build.mjs +15 -15
- package/src/commands/component/index.mjs +4 -4
- package/src/commands/discover.mjs +5 -7
- package/src/commands/docs.mjs +4 -4
- package/src/commands/hook/index.mjs +4 -4
- package/src/commands/init.mjs +152 -48
- package/src/commands/init.next-steps.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +22 -19
- package/src/commands/json-contract.test.mjs +1 -1
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/swizzle.mjs +34 -11
- package/src/commands/template.mjs +31 -11
- package/src/commands/upgrade.mjs +6 -9
- package/src/commands/upgrade.test.mjs +1 -1
- package/src/index.mjs +6 -40
- package/src/lib/component-format.mjs +1 -2
- package/src/utils/interactive.mjs +76 -0
- package/src/utils/interactive.test.mjs +70 -0
- package/src/utils/package-manager.mjs +0 -78
- package/src/utils/package-manager.test.mjs +1 -108
- package/src/utils/path-safety.mjs +18 -0
- package/src/utils/update-check.mjs +1 -2
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +7 -2
- package/docs/cli-integrations.doc.mjs +0 -150
- package/scripts/postinstall.mjs +0 -74
- package/src/commands/cli-postinstall.test.mjs +0 -42
- package/src/commands/setup-nudge.test.mjs +0 -108
- package/src/lib/term-log.mjs +0 -48
|
@@ -4,16 +4,9 @@ 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 {
|
|
8
|
-
detectPackageManager,
|
|
9
|
-
getDlxPrefix,
|
|
10
|
-
isCliOneOff,
|
|
11
|
-
getCliInvocation,
|
|
12
|
-
formatCliCommand,
|
|
13
|
-
} from './package-manager.mjs';
|
|
7
|
+
import {detectPackageManager} from './package-manager.mjs';
|
|
14
8
|
|
|
15
9
|
let tmpDir;
|
|
16
|
-
const ORIGINAL_ARGV1 = process.argv[1];
|
|
17
10
|
|
|
18
11
|
afterEach(() => {
|
|
19
12
|
if (tmpDir) {
|
|
@@ -22,7 +15,6 @@ afterEach(() => {
|
|
|
22
15
|
}
|
|
23
16
|
vi.restoreAllMocks();
|
|
24
17
|
delete process.env.npm_config_user_agent;
|
|
25
|
-
process.argv[1] = ORIGINAL_ARGV1;
|
|
26
18
|
});
|
|
27
19
|
|
|
28
20
|
function makeTmpDir() {
|
|
@@ -119,102 +111,3 @@ describe('detectPackageManager', () => {
|
|
|
119
111
|
expect(detectPackageManager(dir)).toBe('bun');
|
|
120
112
|
});
|
|
121
113
|
});
|
|
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,3 +165,21 @@ 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,7 +14,6 @@
|
|
|
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';
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Read the latest available version from local signals.
|
|
@@ -76,7 +75,7 @@ export function checkForUpdate(cwd = process.cwd()) {
|
|
|
76
75
|
// Use semver-aware comparison so '0.0.20' is correctly treated as greater
|
|
77
76
|
// than '0.0.5' (lexicographic compare gets that backwards).
|
|
78
77
|
if (semverGt(latest, installed)) {
|
|
79
|
-
return `FYI: A newer version of @astryxdesign/core (${latest}) is available. Install the new package version, then run:
|
|
78
|
+
return `FYI: A newer version of @astryxdesign/core (${latest}) is available. Install the new package version, then run: astryx upgrade --from <old-version> --apply`;
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
return null;
|
|
@@ -7,7 +7,7 @@ export const doc = {
|
|
|
7
7
|
name: 'TabList — With Actions',
|
|
8
8
|
displayName: 'TabList — With Actions',
|
|
9
9
|
description:
|
|
10
|
-
'Page header pattern with tabs on the left and action buttons pushed to the right. When hasDivider is true,
|
|
10
|
+
'Page header pattern with tabs on the left and action buttons pushed to the right. When hasDivider is true, pair with a smaller button size (sm) so actions don\'t overpower the tab row.',
|
|
11
11
|
isReady: true,
|
|
12
12
|
aspectRatio: 4 / 3,
|
|
13
13
|
componentsUsed: ['TabList', 'Tab', 'Button'],
|
|
@@ -55,11 +55,16 @@ export default function TabListTabsWithActions() {
|
|
|
55
55
|
<Button
|
|
56
56
|
label="Filter"
|
|
57
57
|
variant="ghost"
|
|
58
|
-
size="
|
|
58
|
+
size="sm"
|
|
59
59
|
icon={FilterIcon}
|
|
60
60
|
isIconOnly
|
|
61
61
|
/>
|
|
62
|
-
<Button
|
|
62
|
+
<Button
|
|
63
|
+
label="New item"
|
|
64
|
+
variant="primary"
|
|
65
|
+
size="sm"
|
|
66
|
+
icon={PlusIcon}
|
|
67
|
+
/>
|
|
63
68
|
</div>
|
|
64
69
|
</TabList>
|
|
65
70
|
);
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/** @type {import('../../core/src/docs-types').ReferenceDoc} */
|
|
4
|
-
|
|
5
|
-
export const docs = {
|
|
6
|
-
name: 'cli-integrations',
|
|
7
|
-
title: 'CLI Integrations',
|
|
8
|
-
category: 'guide',
|
|
9
|
-
description:
|
|
10
|
-
'Author an npm package that contributes components, templates, and upgrade codemods to Astryx.',
|
|
11
|
-
|
|
12
|
-
sections: [
|
|
13
|
-
{
|
|
14
|
-
title: 'Overview',
|
|
15
|
-
category: 'guide',
|
|
16
|
-
content: [
|
|
17
|
-
{
|
|
18
|
-
type: 'prose',
|
|
19
|
-
text: 'An integration is an npm package that contributes components, templates, and/or upgrade codemods to a consumer\u2019s design-system workflow. Consumers install the package and add it to their `astryx.config`; from then on the integration\u2019s contributions show up alongside core\u2019s in the same CLI commands.',
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
type: 'prose',
|
|
23
|
-
text: 'The system runs on two files. The consumer writes `astryx.config.{ts,mjs,js}` at their project root to list which packages to load. The author writes `astryx.integration.{ts,mjs,js}` at the package root to declare what the package contributes. This page is the author\u2019s guide. For the consumer side, run `npx astryx docs getting-started`.',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
type: 'prose',
|
|
27
|
-
text: 'On the consumer side, adding your package is one line:',
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
type: 'code',
|
|
31
|
-
lang: 'typescript',
|
|
32
|
-
code: "import {createConfig} from '@astryxdesign/core/config';\n\nexport default createConfig({\n integrations: ['@acme/astryx-widgets'],\n});",
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
type: 'prose',
|
|
36
|
-
text: 'Your components and templates then appear next to core\u2019s:',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
type: 'code',
|
|
40
|
-
lang: 'bash',
|
|
41
|
-
code: 'astryx component --list --package @acme/astryx-widgets\nastryx component AcmeCarousel --props',
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
title: 'The Integration File',
|
|
47
|
-
category: 'guide',
|
|
48
|
-
content: [
|
|
49
|
-
{
|
|
50
|
-
type: 'prose',
|
|
51
|
-
text: 'To register your package as an integration, add an `astryx.integration.{ts,mjs,js}` file as a sibling of your `package.json`. It tells the CLI where to find your components, templates, and codemods. Identity (name, version) comes from your `package.json`, not this file.',
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
type: 'code',
|
|
55
|
-
lang: 'typescript',
|
|
56
|
-
code: "// astryx.integration.ts\nimport {createIntegration} from '@astryxdesign/core/authoring';\n\nexport default createIntegration({\n components: './components',\n templates: './templates',\n codemods: './codemods',\n issuesUrl: 'https://github.com/acme/widgets/issues',\n});",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
type: 'prose',
|
|
60
|
-
text: 'Every field is optional. Declare only the contribution roots your package ships. `createIntegration` is a type-preserving helper for editor autocomplete and type-checking. It lives in `@astryxdesign/core/authoring` and is also re-exported from `@astryxdesign/cli/integration` for back-compat.',
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
title: 'Components',
|
|
66
|
-
category: 'guide',
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'prose',
|
|
70
|
-
text: 'Export your components from your library however you like, and consumers still import them from your package. For each component the CLI should document, ship a `.doc.{ts,mjs,js}` file with the same stem, for example `AcmeCarousel.tsx` alongside `AcmeCarousel.doc.ts`.',
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
type: 'code',
|
|
74
|
-
lang: 'typescript',
|
|
75
|
-
code: "// AcmeCarousel.doc.ts\nimport {createComponentDoc} from '@astryxdesign/core/authoring';\n\nexport default createComponentDoc({\n name: 'AcmeCarousel',\n description: 'A carousel that cycles through slides.',\n // props, usage, examples, ...\n});",
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
title: 'Templates',
|
|
81
|
-
category: 'guide',
|
|
82
|
-
content: [
|
|
83
|
-
{
|
|
84
|
-
type: 'prose',
|
|
85
|
-
text: 'Templates are usually not exported from the package directly. Instead, consumers browse them through the CLI and materialize them into their app. Define a template with `createPageTemplate` (full pages) or `createBlockTemplate` (smaller chunks) in a `.template.{ts,mjs,js}` file next to the source, for example `AcmeLandingPage.tsx` and `AcmeLandingPage.template.ts`.',
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
type: 'code',
|
|
89
|
-
lang: 'typescript',
|
|
90
|
-
code: "// AcmeLandingPage.template.ts\nimport {createPageTemplate} from '@astryxdesign/core/authoring';\n\nexport default createPageTemplate({\n // name, description, preview, ...\n});",
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
type: 'prose',
|
|
94
|
-
text: 'The CLI needs the template source at consume time, so make sure it is included in your published package. This is typically done via the `exports` key in `package.json`. It also lets the docsite render template previews in the future.',
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
type: 'code',
|
|
98
|
-
lang: 'jsonc',
|
|
99
|
-
code: '{\n "exports": {\n // ...\n "./templates/*.tsx": "./templates/*.tsx"\n }\n}',
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
type: 'prose',
|
|
103
|
-
text: 'To verify it resolves, try importing the template component with its `.tsx` extension. An extensionless specifier will not resolve under `moduleResolution: bundler`, and the extensionful export above is what lets this type-check without consumers enabling `allowImportingTsExtensions`.',
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
type: 'code',
|
|
107
|
-
lang: 'typescript',
|
|
108
|
-
code: "import('@acme/astryx-widgets/templates/AcmeLandingPage.tsx');",
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
title: 'Codemods',
|
|
114
|
-
category: 'guide',
|
|
115
|
-
content: [
|
|
116
|
-
{
|
|
117
|
-
type: 'prose',
|
|
118
|
-
text: 'Ship codemods so `astryx upgrade` can migrate consumers across breaking changes in your package. Point the integration file\u2019s `codemods` field at your codemods root, and author each one with `createCodemod` (transforms source files) or `createConfigCodemod` (rewrites the consumer\u2019s `astryx.config`).',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
type: 'code',
|
|
122
|
-
lang: 'typescript',
|
|
123
|
-
code: "// codemods/v2-rename-prop.ts\nimport {createCodemod} from '@astryxdesign/cli/codemod';\n\nexport default createCodemod({\n // version, description, transform, ...\n});",
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
type: 'prose',
|
|
127
|
-
text: 'The codemod helpers live in `@astryxdesign/cli/codemod`, not `@astryxdesign/core/authoring` like the doc, integration, and template helpers. Consumers can also run their own post-codemod hooks, such as a reinstall or rebuild, via `hooks.postCodemod` in their `astryx.config`.',
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
title: 'How It Works',
|
|
133
|
-
category: 'guide',
|
|
134
|
-
content: [
|
|
135
|
-
{
|
|
136
|
-
type: 'prose',
|
|
137
|
-
text: 'Every CLI command loads the consumer\u2019s `astryx.config`, resolves each listed integration\u2019s manifest from `node_modules`, and discovers its contributions. Everything is validated against one strict schema at the load boundary. The `create*` helpers do not validate. They are identity functions whose value is their TypeScript surface, so validation happens when the CLI loads the file, not when you author it.',
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
type: 'prose',
|
|
141
|
-
text: 'Discovery is resilient. A broken or misconfigured integration is skipped with a single non-blocking warning on stderr instead of crashing the CLI, and it never corrupts a `--json` stdout envelope. Everyday commands keep working with the remaining valid contributions.',
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
type: 'prose',
|
|
145
|
-
text: 'To inspect problems, run `astryx validate-integration <package>` for a detailed report on one package, or `astryx doctor` for an overall health check of the setup.',
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
};
|
package/scripts/postinstall.mjs
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file @astryxdesign/cli postinstall — nudge to run `astryx init`.
|
|
5
|
-
*
|
|
6
|
-
* Enforcement layer 2 of making `astryx init` foolproof: when the CLI is
|
|
7
|
-
* installed as a project dependency and the project hasn't run init yet, print a
|
|
8
|
-
* one-line next-step so agents/humans discover it.
|
|
9
|
-
*
|
|
10
|
-
* Reuses the ONE setup check (isAstryxInitialized from agent-docs.mjs — a
|
|
11
|
-
* dep-free import chain, safe at install time). Non-interactive, never fails the
|
|
12
|
-
* install, and stays quiet:
|
|
13
|
-
* - in the monorepo/source build (not under node_modules),
|
|
14
|
-
* - during npx's transient fetch (npx runs the bin — likely `init` — right
|
|
15
|
-
* after, so nudging here would double up), and
|
|
16
|
-
* - once setup is already done.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
20
|
-
|
|
21
|
-
const HERE = fileURLToPath(import.meta.url);
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Pure decision: should the postinstall print the setup nudge? Split out so the
|
|
25
|
-
* matrix is unit-testable without an actual npm install.
|
|
26
|
-
*
|
|
27
|
-
* @param {object} opts
|
|
28
|
-
* @param {string} opts.scriptPath - Absolute path of this script (location tells
|
|
29
|
-
* us dependency vs monorepo vs npx-cache).
|
|
30
|
-
* @param {string} [opts.npmCommand] - process.env.npm_command ('install', 'exec', …).
|
|
31
|
-
* @param {boolean} opts.isSetUp - Whether the project already ran init.
|
|
32
|
-
* @returns {boolean}
|
|
33
|
-
*/
|
|
34
|
-
export function shouldNudge({scriptPath, npmCommand, isSetUp} = {}) {
|
|
35
|
-
if (!scriptPath || !scriptPath.includes('node_modules')) return false; // monorepo/source build
|
|
36
|
-
if (scriptPath.includes('_npx') || npmCommand === 'exec') return false; // npx transient — init runs next
|
|
37
|
-
if (isSetUp) return false; // already set up — stay quiet
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** @param {string} root @returns {Promise<boolean>} */
|
|
42
|
-
async function projectIsSetUp(root) {
|
|
43
|
-
try {
|
|
44
|
-
const {isAstryxInitialized} = await import('../src/commands/agent-docs.mjs');
|
|
45
|
-
return isAstryxInitialized(root);
|
|
46
|
-
} catch {
|
|
47
|
-
return false; // best-effort — if the check can't load, fall through and nudge
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function main() {
|
|
52
|
-
const root = process.env.INIT_CWD || process.cwd();
|
|
53
|
-
const nudge = shouldNudge({
|
|
54
|
-
scriptPath: HERE,
|
|
55
|
-
npmCommand: process.env.npm_command,
|
|
56
|
-
isSetUp: await projectIsSetUp(root),
|
|
57
|
-
});
|
|
58
|
-
if (nudge) {
|
|
59
|
-
// Scoped package form (`@astryxdesign/cli`) — always resolves to us. Bare
|
|
60
|
-
// `npx astryx` would fetch an unrelated look-alike package (see PR #4151).
|
|
61
|
-
// After that lands, switch this to its getCliInvocation() single source of truth.
|
|
62
|
-
process.stdout.write(
|
|
63
|
-
'\nNext step: run `npx @astryxdesign/cli init` to finish setup and install the Astryx agent prompt.\n\n',
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Run only when executed directly (`node scripts/postinstall.mjs`), never when
|
|
69
|
-
// imported by tests.
|
|
70
|
-
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
|
71
|
-
main()
|
|
72
|
-
.catch(() => {})
|
|
73
|
-
.finally(() => process.exit(0));
|
|
74
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file Guardrail tests for the @astryxdesign/cli postinstall nudge (layer 2).
|
|
5
|
-
*
|
|
6
|
-
* Tests the pure decision matrix (shouldNudge) — nudges for a real dependency
|
|
7
|
-
* install when not set up; stays quiet in the monorepo/source, during npx's
|
|
8
|
-
* transient fetch (path _npx or npm_command=exec), and once set up.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {describe, it, expect} from 'vitest';
|
|
12
|
-
import {shouldNudge} from '../../scripts/postinstall.mjs';
|
|
13
|
-
|
|
14
|
-
const DEP = '/proj/node_modules/@astryxdesign/cli/scripts/postinstall.mjs'; // real dep install
|
|
15
|
-
const NPX = '/Users/x/.npm/_npx/a1b2/node_modules/@astryxdesign/cli/scripts/postinstall.mjs';
|
|
16
|
-
const REPO = '/repo/packages/cli/scripts/postinstall.mjs'; // monorepo/source
|
|
17
|
-
|
|
18
|
-
describe('cli postinstall — shouldNudge', () => {
|
|
19
|
-
it('nudges for a real dependency install when not set up', () => {
|
|
20
|
-
expect(shouldNudge({scriptPath: DEP, npmCommand: 'install', isSetUp: false})).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('quiet in the monorepo/source (not under node_modules)', () => {
|
|
24
|
-
expect(shouldNudge({scriptPath: REPO, npmCommand: 'install', isSetUp: false})).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('quiet during npx transient install (path contains _npx)', () => {
|
|
28
|
-
expect(shouldNudge({scriptPath: NPX, npmCommand: 'install', isSetUp: false})).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('quiet during npx (npm_command=exec) — avoids double-nudge before init', () => {
|
|
32
|
-
expect(shouldNudge({scriptPath: DEP, npmCommand: 'exec', isSetUp: false})).toBe(false);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('quiet once the project is already set up', () => {
|
|
36
|
-
expect(shouldNudge({scriptPath: DEP, npmCommand: 'install', isSetUp: true})).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('quiet with no script path (defensive)', () => {
|
|
40
|
-
expect(shouldNudge({})).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
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/lib/term-log.mjs
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
}
|