@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4
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 +99 -66
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +115 -3
- package/api/build/blocks/blocks.d.mts +42 -0
- package/api/build/blocks/blocks.mjs +169 -0
- package/api/build/blocks/blocks.test.mjs +108 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/kit/answer.d.mts +14 -0
- package/api/build/kit/answer.mjs +85 -0
- package/api/build/kit/answer.test.mjs +38 -0
- package/api/build/kit/kind.d.mts +25 -0
- package/api/build/kit/kind.mjs +170 -0
- package/api/build/kit/kit.d.mts +2 -4
- package/api/build/kit/kit.mjs +8 -398
- package/api/build/kit/rank.d.mts +3 -32
- package/api/build/kit/rank.mjs +25 -342
- package/api/build/kit/rank.test.mjs +2 -1
- package/api/build/kit/signals.d.mts +60 -0
- package/api/build/kit/signals.mjs +284 -0
- package/api/build/kit/start.d.mts +32 -0
- package/api/build/kit/start.mjs +163 -0
- package/api/build/kit/words.d.mts +72 -0
- package/api/build/kit/words.mjs +202 -0
- package/api/build/tree.d.mts +20 -0
- package/api/build/tree.mjs +52 -0
- package/api/build/tree.test.mjs +177 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/component.mjs +16 -2
- package/api/component/component.test.mjs +15 -0
- package/api/discover/_adapter.mjs +36 -9
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/docs/_adapter.mjs +17 -5
- package/api/docs/docs.test.mjs +7 -1
- package/api/doctor/doctor.mjs +86 -12
- package/api/doctor/doctor.test.mjs +28 -3
- package/api/init/init.doc.mjs +20 -6
- package/api/init/init.test.mjs +18 -2
- package/api/init/init.type.d.mts +3 -3
- package/api/init/init.type.mjs +3 -3
- package/api/init/remove/remove.mjs +17 -4
- package/api/init/remove/remove.test.mjs +114 -2
- package/api/init/run/run.mjs +69 -6
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/pack-check.mjs +123 -4
- package/api/integration/pack-check.test.mjs +87 -0
- package/api/integration/validate-integration-case-disk.test.mjs +111 -0
- package/api/integration/validate-integration-gaps.test.mjs +1964 -0
- package/api/integration/validate-integration.d.mts +11 -2
- package/api/integration/validate-integration.mjs +226 -32
- package/api/integration/validate-integration.type.d.mts +58 -1
- package/api/integration/validate-integration.type.mjs +45 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +66 -2
- package/api/search/_adapter.mjs +118 -10
- package/api/search/_candidate-score.d.mts +40 -0
- package/api/search/_candidate-score.mjs +238 -0
- package/api/search/_cards.json +6098 -0
- package/api/search/_compound.d.mts +50 -0
- package/api/search/_compound.mjs +170 -0
- package/api/search/_doc-searcher.d.mts +4 -0
- package/api/search/_doc-searcher.mjs +23 -0
- package/api/search/_doc-searcher.test.mjs +36 -0
- package/api/search/_doc-searcher.weights.json +1 -0
- package/api/search/_doc-text.d.mts +30 -0
- package/api/search/_doc-text.mjs +87 -0
- package/api/search/_entries.d.mts +52 -0
- package/api/search/_entries.mjs +158 -0
- package/api/search/_features.d.mts +35 -0
- package/api/search/_features.mjs +155 -0
- package/api/search/_gather-components.d.mts +26 -0
- package/api/search/_gather-components.mjs +284 -0
- package/api/search/_gather-docs.d.mts +20 -0
- package/api/search/_gather-docs.mjs +254 -0
- package/api/search/_gather-hooks.d.mts +12 -0
- package/api/search/_gather-hooks.mjs +52 -0
- package/api/search/_gather-templates.d.mts +25 -0
- package/api/search/_gather-templates.mjs +107 -0
- package/api/search/_lexical.d.mts +135 -0
- package/api/search/_lexical.mjs +351 -0
- package/api/search/_main-searcher.d.mts +19 -0
- package/api/search/_main-searcher.mjs +115 -0
- package/api/search/_main-searcher.test.mjs +106 -0
- package/api/search/_main-searcher.weights.json +1 -0
- package/api/search/_module-doc.d.mts +32 -0
- package/api/search/_module-doc.mjs +78 -0
- package/api/search/_order.d.mts +32 -0
- package/api/search/_order.mjs +162 -0
- package/api/search/_order.test.mjs +72 -0
- package/api/search/_priority.d.mts +50 -0
- package/api/search/_priority.mjs +139 -0
- package/api/search/_query-words.d.mts +25 -0
- package/api/search/_query-words.mjs +92 -0
- package/api/search/_query.d.mts +65 -0
- package/api/search/_query.mjs +176 -0
- package/api/search/_result.d.mts +97 -0
- package/api/search/_result.mjs +150 -0
- package/api/search/_score.d.mts +60 -0
- package/api/search/_score.mjs +283 -0
- package/api/search/_synonyms.d.mts +21 -0
- package/api/search/_synonyms.mjs +67 -0
- package/api/search/_template-searcher.d.mts +4 -0
- package/api/search/_template-searcher.mjs +23 -0
- package/api/search/_template-searcher.test.mjs +40 -0
- package/api/search/_template-searcher.weights.json +1 -0
- package/api/search/_titles.d.mts +33 -0
- package/api/search/_titles.mjs +65 -0
- package/api/search/_words.d.mts +64 -0
- package/api/search/_words.mjs +157 -0
- package/api/search/search.d.mts +22 -305
- package/api/search/search.doc.mjs +7 -5
- package/api/search/search.mjs +53 -2483
- package/api/search/search.type.d.mts +6 -1
- package/api/search/search.type.mjs +6 -1
- package/api/search/tree.d.mts +15 -0
- package/api/search/tree.mjs +40 -0
- package/api/search/tree.test.mjs +66 -0
- package/api/search/tree.weights.json +1 -0
- package/api/theme/build/build.packed-old-core.test.mjs +1 -0
- package/api/upgrade/_adapter.d.mts +57 -9
- package/api/upgrade/_adapter.mjs +198 -28
- package/api/upgrade/registry/registry.d.mts +10 -4
- package/api/upgrade/registry/registry.mjs +33 -16
- package/api/upgrade/registry/registry.test.mjs +85 -0
- package/api/upgrade/run/run.d.mts +4 -2
- package/api/upgrade/run/run.mjs +13 -2
- package/api/upgrade/run/run.test.mjs +417 -3
- package/api/upgrade/upgrade.doc.mjs +10 -5
- package/api/upgrade/upgrade.mjs +13 -2
- package/api/upgrade/upgrade.test.mjs +194 -0
- package/api/upgrade/upgrade.type.d.mts +5 -3
- package/api/upgrade/upgrade.type.mjs +8 -6
- package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
- package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +6 -2
- package/assets/templates/themes/butter/butterTheme.ts +4 -2
- package/assets/templates/themes/stone/stoneTheme.ts +4 -2
- package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
- package/authoring/debug/parse.d.mts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/commands/doctor.mjs +30 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/init.behavior.test.mjs +230 -1
- package/clients/cli/commands/init.doc.mjs +28 -9
- package/clients/cli/commands/init.flag-help.test.mjs +1 -0
- package/clients/cli/commands/init.mjs +2 -2
- package/clients/cli/commands/outside-project.test.mjs +2 -1
- package/clients/cli/commands/setup-nudge.test.mjs +61 -0
- package/clients/cli/commands/upgrade.doc.mjs +4 -2
- package/clients/cli/commands/upgrade.mjs +9 -2
- package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
- package/clients/cli/index.mjs +19 -1
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
- package/foundation/agent-docs/agent-doc-state.mjs +35 -8
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1010 -99
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +678 -35
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/component-discovery.d.mts +37 -0
- package/foundation/discovery/component-discovery.mjs +80 -5
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/docs-discovery.mjs +14 -1
- package/foundation/discovery/module-exports.d.mts +41 -0
- package/foundation/discovery/module-exports.mjs +253 -0
- package/foundation/discovery/template-adapter.mjs +18 -1
- package/foundation/discovery/template-needs.mjs +1 -44
- package/foundation/discovery/theme-discovery.mjs +147 -3
- package/foundation/doc-compiler/doc-loads.test.mjs +8 -0
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/fs/paths.d.mts +70 -8
- package/foundation/fs/paths.mjs +556 -38
- package/foundation/fs/paths.test.mjs +430 -54
- package/foundation/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- package/foundation/integrations/computer-integrations.d.mts +6 -3
- package/foundation/integrations/computer-integrations.mjs +38 -9
- package/foundation/integrations/contribution-inventory.d.mts +15 -20
- package/foundation/integrations/contribution-inventory.mjs +41 -5
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/run-context.mjs +14 -0
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/route/route.d.mts +285 -0
- package/foundation/route/route.mjs +324 -0
- package/foundation/route/route.test.mjs +153 -0
- package/foundation/runtime/hosted-runtime.test.mjs +403 -0
- package/foundation/runtime/locked-versions.d.mts +32 -0
- package/foundation/runtime/locked-versions.mjs +65 -0
- package/foundation/runtime/package-root.d.mts +91 -0
- package/foundation/runtime/package-root.mjs +174 -0
- package/foundation/runtime/package-set.d.mts +62 -0
- package/foundation/runtime/package-set.mjs +130 -0
- package/foundation/runtime/package-set.test.mjs +432 -0
- package/host/door-conformance.test.mjs +274 -0
- package/host/index.d.ts +116 -0
- package/host/index.mjs +295 -0
- package/host/index.test.mjs +302 -0
- package/package.json +14 -9
|
@@ -70,10 +70,12 @@ describe('astryx init — default (no flags)', () => {
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
describe('astryx init --features', () => {
|
|
73
|
-
it('--features agents writes AGENTS.md
|
|
73
|
+
it('--features agents writes AGENTS.md and the GEMINI.md import of it', async () => {
|
|
74
74
|
const {status} = await runCli(['init', '--features', 'agents'], {cwd: tmpDir});
|
|
75
75
|
expect(status).toBe(0);
|
|
76
76
|
expect(exists('AGENTS.md')).toBe(true);
|
|
77
|
+
expect(read('GEMINI.md')).toBe(`${MARKER_START}\n@./AGENTS.md\n<!-- ASTRYX:END -->\n`);
|
|
78
|
+
expect(exists('theme.template.ts')).toBe(false);
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
it('--features theme writes the annotated theme template', async () => {
|
|
@@ -126,7 +128,234 @@ describe('astryx init --agent <preset>', () => {
|
|
|
126
128
|
});
|
|
127
129
|
});
|
|
128
130
|
|
|
131
|
+
describe('astryx init from a subfolder', () => {
|
|
132
|
+
/** @param {string} dir */
|
|
133
|
+
const astryxProject = dir =>
|
|
134
|
+
fs.writeFileSync(
|
|
135
|
+
path.join(dir, 'package.json'),
|
|
136
|
+
JSON.stringify({name: 'app', dependencies: {'@astryxdesign/core': '0.7.0'}}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
it('writes the agent docs at the project root and reports them from where it ran', async () => {
|
|
140
|
+
astryxProject(tmpDir);
|
|
141
|
+
const sub = path.join(tmpDir, 'src', 'app');
|
|
142
|
+
fs.mkdirSync(sub, {recursive: true});
|
|
143
|
+
|
|
144
|
+
const json = await runCli(['init', '--json'], {cwd: sub});
|
|
145
|
+
expect(json.status).toBe(0);
|
|
146
|
+
expect(JSON.parse(json.stdout).data.docsWritten).toEqual([
|
|
147
|
+
'../../AGENTS.md',
|
|
148
|
+
'../../GEMINI.md',
|
|
149
|
+
]);
|
|
150
|
+
expect(read('AGENTS.md')).toContain(MARKER_START);
|
|
151
|
+
expect(fs.readdirSync(sub)).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('keeps --agent-docs-path relative to where it ran', async () => {
|
|
155
|
+
astryxProject(tmpDir);
|
|
156
|
+
const sub = path.join(tmpDir, 'docs');
|
|
157
|
+
fs.mkdirSync(sub);
|
|
158
|
+
|
|
159
|
+
const json = await runCli(['init', '--agent-docs-path', 'AI.md', '--json'], {cwd: sub});
|
|
160
|
+
expect(JSON.parse(json.stdout).data.docsWritten).toEqual(['AI.md']);
|
|
161
|
+
expect(fs.readFileSync(path.join(sub, 'AI.md'), 'utf8')).toContain(MARKER_START);
|
|
162
|
+
expect(exists('AGENTS.md')).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('astryx init in the home folder', () => {
|
|
167
|
+
const saved = {HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE};
|
|
168
|
+
const personal = '# My notes\n\nAlways use tabs.\n';
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
process.env.HOME = tmpDir;
|
|
172
|
+
process.env.USERPROFILE = tmpDir;
|
|
173
|
+
fs.mkdirSync(path.join(tmpDir, '.claude'), {recursive: true});
|
|
174
|
+
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), personal);
|
|
175
|
+
});
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
178
|
+
if (value === undefined) delete process.env[key];
|
|
179
|
+
else process.env[key] = value;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('writes no agent docs, says why, and keeps the soft-failure exit code', async () => {
|
|
184
|
+
const json = await runCli(['init', '--json'], {cwd: tmpDir});
|
|
185
|
+
expect(json.status).toBe(0);
|
|
186
|
+
const env = JSON.parse(json.stdout);
|
|
187
|
+
expect(env.data.docsWritten).toEqual([]);
|
|
188
|
+
expect(env.data.docsError).toEqual({
|
|
189
|
+
kind: 'install-failed',
|
|
190
|
+
message: expect.stringMatching(/not written into the home folder/),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const text = await runCli(['init'], {cwd: tmpDir});
|
|
194
|
+
expect(text.status).toBe(0);
|
|
195
|
+
expect(text.stderr).toMatch(/not written into the home folder/);
|
|
196
|
+
expect(text.stderr).not.toMatch(/Error:/);
|
|
197
|
+
|
|
198
|
+
expect(read('.claude/CLAUDE.md')).toBe(personal);
|
|
199
|
+
expect(fs.readdirSync(tmpDir)).toEqual(['.claude']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('does not let a package.json in the home folder pull the docs there', async () => {
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
path.join(tmpDir, 'package.json'),
|
|
205
|
+
JSON.stringify({dependencies: {'@astryxdesign/core': '0.7.0'}}),
|
|
206
|
+
);
|
|
207
|
+
const scratch = path.join(tmpDir, 'scratch');
|
|
208
|
+
fs.mkdirSync(scratch);
|
|
209
|
+
|
|
210
|
+
const json = await runCli(['init', '--json'], {cwd: scratch});
|
|
211
|
+
expect(json.status).toBe(0);
|
|
212
|
+
expect(JSON.parse(json.stdout).data.docsWritten).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
213
|
+
expect(fs.existsSync(path.join(scratch, 'AGENTS.md'))).toBe(true);
|
|
214
|
+
expect(exists('AGENTS.md')).toBe(false);
|
|
215
|
+
expect(read('.claude/CLAUDE.md')).toBe(personal);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('--remove-agents in a folder in home never reaches the home folder', async () => {
|
|
219
|
+
fs.writeFileSync(
|
|
220
|
+
path.join(tmpDir, '.claude', 'CLAUDE.md'),
|
|
221
|
+
`${personal}\n<!-- ASTRYX:START -->\nAstryx v0.7.0\n<!-- ASTRYX:END -->\n`,
|
|
222
|
+
);
|
|
223
|
+
fs.writeFileSync(
|
|
224
|
+
path.join(tmpDir, 'package.json'),
|
|
225
|
+
JSON.stringify({dependencies: {'@astryxdesign/core': '0.7.0'}}),
|
|
226
|
+
);
|
|
227
|
+
const scratch = path.join(tmpDir, 'scratch');
|
|
228
|
+
fs.mkdirSync(scratch);
|
|
229
|
+
|
|
230
|
+
const json = await runCli(['init', '--remove-agents', '--json'], {cwd: scratch});
|
|
231
|
+
expect(JSON.parse(json.stdout).data.removed).toBe(false);
|
|
232
|
+
expect(read('.claude/CLAUDE.md')).toContain('<!-- ASTRYX:START -->');
|
|
233
|
+
|
|
234
|
+
// Run in the home folder itself, it still removes the block there.
|
|
235
|
+
const home = await runCli(['init', '--remove-agents', '--json'], {cwd: tmpDir});
|
|
236
|
+
expect(JSON.parse(home.stdout).data.removed).toBe(true);
|
|
237
|
+
expect(read('.claude/CLAUDE.md')).toBe(personal);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it.each(['.gemini', '.claude'])('refuses the settings folder ~/%s', async folder => {
|
|
241
|
+
const dir = path.join(tmpDir, folder);
|
|
242
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
243
|
+
const before = fs.readdirSync(dir);
|
|
244
|
+
|
|
245
|
+
const json = await runCli(['init', '--json'], {cwd: dir});
|
|
246
|
+
expect(json.status).toBe(0);
|
|
247
|
+
const env = JSON.parse(json.stdout);
|
|
248
|
+
expect(env.data.docsWritten).toEqual([]);
|
|
249
|
+
expect(env.data.docsError.message).toMatch(/settings folder in it outside a project/);
|
|
250
|
+
expect(fs.readdirSync(dir)).toEqual(before);
|
|
251
|
+
expect(read('.claude/CLAUDE.md')).toBe(personal);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('astryx init in an agent worktree inside a home settings folder', () => {
|
|
256
|
+
const saved = {HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE};
|
|
257
|
+
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
process.env.HOME = tmpDir;
|
|
260
|
+
process.env.USERPROFILE = tmpDir;
|
|
261
|
+
});
|
|
262
|
+
afterEach(() => {
|
|
263
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
264
|
+
if (value === undefined) delete process.env[key];
|
|
265
|
+
else process.env[key] = value;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it.each([
|
|
270
|
+
['Codex', path.join('.codex', 'worktrees', 'a1b2c3', 'web')],
|
|
271
|
+
['Cursor', path.join('.cursor', 'worktrees', 'web', 'x9y8')],
|
|
272
|
+
])('writes at the project root of a %s worktree', async (_tool, rel) => {
|
|
273
|
+
const project = path.join(tmpDir, rel);
|
|
274
|
+
fs.mkdirSync(path.join(project, 'src'), {recursive: true});
|
|
275
|
+
fs.writeFileSync(path.join(project, '.git'), 'gitdir: /elsewhere/.git/worktrees/web\n');
|
|
276
|
+
fs.writeFileSync(
|
|
277
|
+
path.join(project, 'package.json'),
|
|
278
|
+
JSON.stringify({name: 'web', dependencies: {'@astryxdesign/core': '0.7.0'}}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const json = await runCli(['init', '--json'], {cwd: path.join(project, 'src')});
|
|
282
|
+
expect(json.status).toBe(0);
|
|
283
|
+
const env = JSON.parse(json.stdout);
|
|
284
|
+
expect(env.data.docsWritten).toEqual(['../AGENTS.md', '../GEMINI.md']);
|
|
285
|
+
expect(env.data.docsError).toBeNull();
|
|
286
|
+
|
|
287
|
+
const explicit = await runCli(['init', '--agent-docs-path', 'AI.md', '--json'], {
|
|
288
|
+
cwd: path.join(project, 'src'),
|
|
289
|
+
});
|
|
290
|
+
expect(JSON.parse(explicit.stdout).data.docsWritten).toEqual(['AI.md']);
|
|
291
|
+
|
|
292
|
+
// Nothing reaches the settings folder itself.
|
|
293
|
+
const settings = path.join(tmpDir, rel.split(path.sep)[0]);
|
|
294
|
+
expect(fs.readdirSync(settings)).toEqual(['worktrees']);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('astryx init and the project root', () => {
|
|
299
|
+
it('writes where it runs when no folder above has a package.json, as before', async () => {
|
|
300
|
+
const notes = path.join(tmpDir, 'notes');
|
|
301
|
+
fs.mkdirSync(notes);
|
|
302
|
+
|
|
303
|
+
const json = await runCli(['init', '--json'], {cwd: notes});
|
|
304
|
+
expect(json.status).toBe(0);
|
|
305
|
+
const env = JSON.parse(json.stdout);
|
|
306
|
+
expect(env.data.docsWritten).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
307
|
+
expect(env.data.docsError).toBeNull();
|
|
308
|
+
expect(fs.readdirSync(notes).sort()).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
309
|
+
expect(fs.readdirSync(tmpDir)).toEqual(['notes']);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('writes at the project root that declares Astryx, past a nested package that does not', async () => {
|
|
313
|
+
fs.writeFileSync(
|
|
314
|
+
path.join(tmpDir, 'package.json'),
|
|
315
|
+
JSON.stringify({name: 'app', dependencies: {'@astryxdesign/core': '0.7.0'}}),
|
|
316
|
+
);
|
|
317
|
+
const functions = path.join(tmpDir, 'functions');
|
|
318
|
+
fs.mkdirSync(path.join(functions, 'src'), {recursive: true});
|
|
319
|
+
fs.writeFileSync(path.join(functions, 'package.json'), '{"name":"functions"}\n');
|
|
320
|
+
|
|
321
|
+
const json = await runCli(['init', '--json'], {cwd: path.join(functions, 'src')});
|
|
322
|
+
expect(JSON.parse(json.stdout).data.docsWritten).toEqual([
|
|
323
|
+
'../../AGENTS.md',
|
|
324
|
+
'../../GEMINI.md',
|
|
325
|
+
]);
|
|
326
|
+
expect(fs.readdirSync(functions).sort()).toEqual(['package.json', 'src']);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
129
330
|
describe('astryx init --remove-agents', () => {
|
|
331
|
+
it('run in a subfolder, removes what init wrote at the project root, with the same JSON shape', async () => {
|
|
332
|
+
fs.writeFileSync(
|
|
333
|
+
path.join(tmpDir, 'package.json'),
|
|
334
|
+
JSON.stringify({name: 'app', dependencies: {'@astryxdesign/core': '0.7.0'}}),
|
|
335
|
+
);
|
|
336
|
+
const sub = path.join(tmpDir, 'src', 'app');
|
|
337
|
+
fs.mkdirSync(sub, {recursive: true});
|
|
338
|
+
await runCli(['init'], {cwd: sub});
|
|
339
|
+
expect(exists('AGENTS.md')).toBe(true);
|
|
340
|
+
|
|
341
|
+
const text = await runCli(['init', '--remove-agents'], {cwd: sub});
|
|
342
|
+
expect(text.status).toBe(0);
|
|
343
|
+
expect(text.stdout).toMatch(/Removed empty \.\.\/\.\.\/AGENTS\.md/);
|
|
344
|
+
expect(exists('AGENTS.md')).toBe(false);
|
|
345
|
+
expect(exists('GEMINI.md')).toBe(false);
|
|
346
|
+
|
|
347
|
+
await runCli(['init'], {cwd: sub});
|
|
348
|
+
const json = await runCli(['init', '--remove-agents', '--json'], {cwd: sub});
|
|
349
|
+
expect(json.status).toBe(0);
|
|
350
|
+
// meta.answeredBy names the release that answered (spec AST-081).
|
|
351
|
+
expect(JSON.parse(json.stdout)).toEqual({
|
|
352
|
+
apiVersion: 1,
|
|
353
|
+
type: 'init.remove',
|
|
354
|
+
data: {removed: true},
|
|
355
|
+
meta: {answeredBy: expect.any(Object)},
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
130
359
|
it('removes the Astryx section it previously installed', async () => {
|
|
131
360
|
await runCli(['init', '--features', 'agents'], {cwd: tmpDir});
|
|
132
361
|
expect(exists('AGENTS.md')).toBe(true);
|
|
@@ -16,10 +16,24 @@ export const doc = {
|
|
|
16
16
|
summary: 'Initialize the design system in your project',
|
|
17
17
|
description:
|
|
18
18
|
'Non-interactive project setup (no prompts, so it behaves the same for humans, ' +
|
|
19
|
-
'agents, and CI). By default it
|
|
20
|
-
'
|
|
21
|
-
|
|
22
|
-
'
|
|
19
|
+
'agents, and CI). By default it writes the agent-docs block, including guidance ' +
|
|
20
|
+
'from configured integrations, to AGENTS.md and to the Claude Code file the project ' +
|
|
21
|
+
'has (CLAUDE.md, else .claude/CLAUDE.md; when CLAUDE.md exists, a block in ' +
|
|
22
|
+
'.claude/CLAUDE.md is removed, unless the project ignores CLAUDE.md, in which case a .claude/CLAUDE.md the ' +
|
|
23
|
+
'project commits keeps a copy), and a one-line import of AGENTS.md to GEMINI.md ' +
|
|
24
|
+
'unless GEMINI.md already imports it. It then prints getting-started steps. Agent ' +
|
|
25
|
+
'docs go to the project root: the nearest folder at or above this one whose ' +
|
|
26
|
+
'package.json declares an @astryxdesign package, else the nearest with any ' +
|
|
27
|
+
'package.json, else this folder. The search stops at a repository root and never ' +
|
|
28
|
+
'counts the home folder. In the home folder, or in a settings folder in it ' +
|
|
29
|
+
'such as ~/.gemini outside a project, no agent docs are written. Every folder of the ' +
|
|
30
|
+
'home folder whose name starts with a dot is a settings folder, and such a folder ' +
|
|
31
|
+
'itself never counts as a project, even with a package.json; a project inside one, ' +
|
|
32
|
+
'such as an agent worktree with its own package.json, works. The managed block is ' +
|
|
33
|
+
'written whatever ignore rules or generated markers say; the text around it is never changed. ' +
|
|
34
|
+
'--features and --all print only ' +
|
|
35
|
+
"the chosen features' " +
|
|
36
|
+
'guidance (no getting-started steps); the theme feature also writes an annotated theme template. ' +
|
|
23
37
|
'Outside a project it writes the same files but prints no step that sets up a project: ' +
|
|
24
38
|
'no getting-started steps, and no step that adds a theme to an app. ' +
|
|
25
39
|
'--user writes the computer-level agent text instead (see the flag), in any folder, and touches nothing in the folder. ' +
|
|
@@ -44,8 +58,10 @@ export const doc = {
|
|
|
44
58
|
flag: '--remove-agents',
|
|
45
59
|
param: 'options.removeAgents',
|
|
46
60
|
description:
|
|
47
|
-
'Remove the managed block from AGENTS.md, CLAUDE.md, .claude/CLAUDE.md, .cursorrules, .hermes.md and HERMES.md ' +
|
|
48
|
-
|
|
61
|
+
'Remove the managed block from AGENTS.md, CLAUDE.md, .claude/CLAUDE.md, GEMINI.md, .cursorrules, .hermes.md and HERMES.md, ' +
|
|
62
|
+
'at the project root (where init writes them) and in this folder ' +
|
|
63
|
+
"(deleting AGENTS.md or .claude/CLAUDE.md when only init's heading is left, and GEMINI.md when nothing is left) and do nothing else. " +
|
|
64
|
+
'Below the project root, a project-root agent file whose block resolves outside the project is left as it is, with a note. ' +
|
|
49
65
|
'--features, --all, --agent and --agent-docs-path are refused; a file written with --agent-docs-path keeps its block',
|
|
50
66
|
},
|
|
51
67
|
{
|
|
@@ -66,7 +82,9 @@ export const doc = {
|
|
|
66
82
|
choices: ['claude', 'cursor', 'codex', 'hermes', 'muse', 'all'],
|
|
67
83
|
description:
|
|
68
84
|
'Target AI tool for agent docs: claude (CLAUDE.md or .claude/CLAUDE.md, else creates .claude/CLAUDE.md), cursor (.cursorrules if present, else AGENTS.md), ' +
|
|
69
|
-
'codex and muse (AGENTS.md), hermes (.hermes.md or HERMES.md if present, else AGENTS.md), all (every existing agent doc
|
|
85
|
+
'codex and muse (AGENTS.md), hermes (.hermes.md or HERMES.md if present, else AGENTS.md), all (every existing agent doc except GEMINI.md, which keeps its ' +
|
|
86
|
+
'one-line import of AGENTS.md, else AGENTS.md and .claude/CLAUDE.md). With claude and all, when CLAUDE.md gets the block, .claude/CLAUDE.md gets none and ' +
|
|
87
|
+
'a block already there is removed, unless the project ignores CLAUDE.md: then a .claude/CLAUDE.md the project commits gets a copy too, with claude and all alike. ' +
|
|
70
88
|
'An unknown tool exits 1 with ERR_UNKNOWN_AGENT. ' +
|
|
71
89
|
'Used only when agent docs are installed (the default, --all, or --features agents); --agent-docs-path takes precedence.',
|
|
72
90
|
},
|
|
@@ -76,6 +94,7 @@ export const doc = {
|
|
|
76
94
|
description:
|
|
77
95
|
'Explicit file path(s) for agent docs, inside the project; takes precedence over --agent. ' +
|
|
78
96
|
'If any path is outside the project, no agent docs are written and the command exits 1. ' +
|
|
97
|
+
'A path in the home folder itself, or in a settings folder in it outside a project (such as ~/.claude or ~/.claude/hooks), is refused with a note (exit 0), and nothing is written. ' +
|
|
79
98
|
'Used only when agent docs are installed',
|
|
80
99
|
},
|
|
81
100
|
],
|
|
@@ -90,11 +109,11 @@ export const doc = {
|
|
|
90
109
|
exitCodes: [
|
|
91
110
|
{
|
|
92
111
|
code: 0,
|
|
93
|
-
when:
|
|
112
|
+
when: "success, including agent docs left unwritten in five soft cases, each reported as docsError with one note: running in the home folder, or in a settings folder in it outside a project; a GEMINI.md that resolves outside the project; when init runs below the project root, a project-root agent file that resolves outside the project; the project's Claude Code file (root CLAUDE.md, or .claude/CLAUDE.md when there is no root one) when it resolves outside the project and is a block-less import of an existing AGENTS.md, which 0.7.1 left alone; and a .claude/CLAUDE.md that resolves outside the project and that 0.7.1 did not write: beside a CLAUDE.md the project ignores, the one --agent claude adds or a block-less import of an existing AGENTS.md; with --agent claude beside a CLAUDE.md, one whose block it would remove",
|
|
94
113
|
},
|
|
95
114
|
{
|
|
96
115
|
code: 1,
|
|
97
|
-
|
|
116
|
+
when: 'an unknown --agent (ERR_UNKNOWN_AGENT) or feature, an --agent-docs-path outside the project, --remove-agents combined with install options, --user combined with --features, --all, --agent or --agent-docs-path, or --all with --features (ERR_INVALID_ARGUMENT); or, when init runs at the project root, an agent file 0.7.1 also wrote there that resolves outside the project (ERR_PATH_TRAVERSAL, docsError path-safety, nothing written)',
|
|
98
117
|
},
|
|
99
118
|
],
|
|
100
119
|
related: ['doctor', 'upgrade', 'build', 'theme'],
|
|
@@ -67,6 +67,7 @@ describe('astryx init flag interactions', () => {
|
|
|
67
67
|
expect(help).toContain(file);
|
|
68
68
|
}
|
|
69
69
|
expect(help).toMatch(/--agent-docs-path are refused/);
|
|
70
|
+
expect(help).toMatch(/at the project root \(where init writes them\) and in this folder/);
|
|
70
71
|
|
|
71
72
|
await initJson(['--agent-docs-path', 'docs/AI.md']);
|
|
72
73
|
await initJson([]);
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file init command — thin wrapper around the init API.
|
|
5
5
|
*
|
|
6
|
-
* `astryx init` is non-interactive by default: it installs the
|
|
7
|
-
*
|
|
6
|
+
* `astryx init` is non-interactive by default: it installs the agent-docs
|
|
7
|
+
* cheat sheet with NO prompts, so it behaves identically for humans,
|
|
8
8
|
* AI agents, CI, and piped I/O — it never hangs or errors on a missing TTY.
|
|
9
9
|
* Non-interactive feature install: `astryx init --features agents,theme,template`
|
|
10
10
|
* Re-runnable: safe to run multiple times, idempotent.
|
|
@@ -541,7 +541,8 @@ describe('init outside a project (own)', () => {
|
|
|
541
541
|
const dir = tmpDir('init');
|
|
542
542
|
const json = await answer(['--json', 'init'], dir);
|
|
543
543
|
expect(json.status).toBe(0);
|
|
544
|
-
|
|
544
|
+
// GEMINI.md gets its one-line import of AGENTS.md (spec cli-surface INV31).
|
|
545
|
+
expect(json.written).toEqual(['AGENTS.md', 'GEMINI.md']);
|
|
545
546
|
expect(json.envelope.data.nextSteps).toBe(false);
|
|
546
547
|
const text = await runCli(['init'], tmpDir('init-text'));
|
|
547
548
|
expect(text.stdout).not.toContain('Next steps');
|
|
@@ -121,6 +121,25 @@ describe('enforcement layer 3 — per-command setup nudge', () => {
|
|
|
121
121
|
expect(r.stderr).toMatch(NUDGE);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it.each([
|
|
125
|
+
['in the home folder', ''],
|
|
126
|
+
['at the top of a settings folder in it', '.dotfiles'],
|
|
127
|
+
])('stays quiet %s, where init writes no agent docs', async (_label, rel) => {
|
|
128
|
+
const saved = {HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE};
|
|
129
|
+
process.env.HOME = tmp;
|
|
130
|
+
process.env.USERPROFILE = tmp;
|
|
131
|
+
try {
|
|
132
|
+
write(path.join(rel, 'package.json'), '{"name":"t"}');
|
|
133
|
+
const r = await runCli(['docs', 'tokens'], path.join(tmp, rel));
|
|
134
|
+
expect(r.stderr).not.toMatch(NUDGE);
|
|
135
|
+
} finally {
|
|
136
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
137
|
+
if (value === undefined) delete process.env[key];
|
|
138
|
+
else process.env[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
124
143
|
it("in a project with only Core of another release, names adding the CLI at Core's version first, as the daily hint does", async () => {
|
|
125
144
|
write(
|
|
126
145
|
'package.json',
|
|
@@ -167,6 +186,48 @@ describe('enforcement layer 3 — per-command setup nudge', () => {
|
|
|
167
186
|
expect((await runCli(['docs', 'tokens'], tmp)).stderr).not.toMatch(NUDGE);
|
|
168
187
|
});
|
|
169
188
|
|
|
189
|
+
it('is quiet in a nested package once init has written the docs at the project root', async () => {
|
|
190
|
+
// init run in functions/ writes at the root that declares Astryx.
|
|
191
|
+
write('package.json', JSON.stringify({name: 'app', dependencies: {'@astryxdesign/core': '0.7.0'}}));
|
|
192
|
+
write('AGENTS.md', MARKER);
|
|
193
|
+
write('functions/package.json', '{"name":"functions"}');
|
|
194
|
+
const r = await runCli(['docs', 'tokens'], path.join(tmp, 'functions'));
|
|
195
|
+
expect(r.stderr).not.toMatch(NUDGE);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('nudges in a nested package while the project root has no docs', async () => {
|
|
199
|
+
write('package.json', JSON.stringify({name: 'app', dependencies: {'@astryxdesign/core': '0.7.0'}}));
|
|
200
|
+
write('functions/package.json', '{"name":"functions"}');
|
|
201
|
+
const r = await runCli(['docs', 'tokens'], path.join(tmp, 'functions'));
|
|
202
|
+
expect(r.stderr).toMatch(NUDGE);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Inside a git repository the project-root check above adds nothing to the
|
|
206
|
+
// walk up to the repository root: the same outcomes as without it.
|
|
207
|
+
it('inside a repository, is quiet in a nested package once init has written the docs at the project root', async () => {
|
|
208
|
+
fs.mkdirSync(path.join(tmp, '.git'));
|
|
209
|
+
write('package.json', JSON.stringify({name: 'app', dependencies: {'@astryxdesign/core': '0.7.0'}}));
|
|
210
|
+
write('AGENTS.md', MARKER);
|
|
211
|
+
write('functions/package.json', '{"name":"functions"}');
|
|
212
|
+
const r = await runCli(['docs', 'tokens'], path.join(tmp, 'functions'));
|
|
213
|
+
expect(r.stderr).not.toMatch(NUDGE);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('inside a repository, still nudges when the only docs lie above the repository root', async () => {
|
|
217
|
+
write('package.json', JSON.stringify({name: 'outer', dependencies: {'@astryxdesign/core': '0.7.0'}}));
|
|
218
|
+
write('AGENTS.md', MARKER);
|
|
219
|
+
fs.mkdirSync(path.join(tmp, 'repo', '.git'), {recursive: true});
|
|
220
|
+
write('repo/package.json', '{"name":"repo"}');
|
|
221
|
+
// It declares Astryx: a folder that declares nothing is outside a
|
|
222
|
+
// project, where no reminder prints (spec AST-081 FR3).
|
|
223
|
+
write(
|
|
224
|
+
'repo/functions/package.json',
|
|
225
|
+
JSON.stringify({name: 'functions', dependencies: {'@astryxdesign/core': '0.7.0'}}),
|
|
226
|
+
);
|
|
227
|
+
const r = await runCli(['docs', 'tokens'], path.join(tmp, 'repo', 'functions'));
|
|
228
|
+
expect(r.stderr).toMatch(NUDGE);
|
|
229
|
+
});
|
|
230
|
+
|
|
170
231
|
it('does not nudge for the installer command itself', async () => {
|
|
171
232
|
asProject();
|
|
172
233
|
expect((await runCli(['init'], tmp)).stderr).not.toMatch(NUDGE);
|
|
@@ -21,6 +21,7 @@ export const doc = {
|
|
|
21
21
|
'agent-docs block when Core or configured integration guidance changes. ' +
|
|
22
22
|
'Dry-run by default. --apply writes codemod and receipt changes, runs hooks, then refreshes agent docs. ' +
|
|
23
23
|
'Anything a post-codemod hook prints goes to stderr, so stdout carries only the result. ' +
|
|
24
|
+
'Run from a folder inside a project, it upgrades that project and names files in the result relative to the folder you ran it in. ' +
|
|
24
25
|
'ShadCN-copied compositions are checked automatically during a normal upgrade, or alone with --registry. ' +
|
|
25
26
|
'Outside a project, --list lists the codemods this release ships and --registry finds no copied compositions to update; ' +
|
|
26
27
|
'a migration writes nothing and fails with ERR_VERSION_DETECT, because no installed Core names its target.',
|
|
@@ -69,8 +70,9 @@ export const doc = {
|
|
|
69
70
|
{
|
|
70
71
|
flag: '--path <dir>',
|
|
71
72
|
param: 'options.path',
|
|
72
|
-
description:
|
|
73
|
-
|
|
73
|
+
description:
|
|
74
|
+
"Source directory to scan, relative to where you run the command, however you spell it. Without --path, the project's src folder, from any folder in the project",
|
|
75
|
+
default: './src at the project root',
|
|
74
76
|
},
|
|
75
77
|
{
|
|
76
78
|
flag: '--install-deps',
|
|
@@ -31,8 +31,9 @@ export function registerUpgrade(program) {
|
|
|
31
31
|
action:
|
|
32
32
|
/**
|
|
33
33
|
* @param {import('../../../api/upgrade/upgrade.mjs').UpgradeOptions} options
|
|
34
|
+
* @param {import('commander').Command} command
|
|
34
35
|
*/
|
|
35
|
-
async options => {
|
|
36
|
+
async (options, command) => {
|
|
36
37
|
const json = program.opts().json || false;
|
|
37
38
|
const isList = Boolean(options.list);
|
|
38
39
|
// Silence the logger for --list so the handler renders from the
|
|
@@ -42,8 +43,14 @@ export function registerUpgrade(program) {
|
|
|
42
43
|
|
|
43
44
|
/** @type {import('../../../api/upgrade/upgrade.type.mjs').UpgradeListResponse | import('../../../api/upgrade/upgrade.type.mjs').UpgradeRegistryResponse | import('../../../api/upgrade/upgrade.type.mjs').UpgradeStatusResponse | import('../../../api/upgrade/upgrade.type.mjs').UpgradeRunResponse} */
|
|
44
45
|
let result;
|
|
46
|
+
// `--path` shows its default in help, but a path nobody typed is the
|
|
47
|
+
// project's src, not one read from this folder: leave it unset.
|
|
48
|
+
const apiOptions =
|
|
49
|
+
command.getOptionValueSource('path') === 'default'
|
|
50
|
+
? {...options, path: undefined}
|
|
51
|
+
: options;
|
|
45
52
|
try {
|
|
46
|
-
result = await upgradeApi(
|
|
53
|
+
result = await upgradeApi(apiOptions, {cwd: process.cwd()});
|
|
47
54
|
} catch (e) {
|
|
48
55
|
// Re-enable the logger so the error is visible in text mode.
|
|
49
56
|
// For --list the logger was silent; for other modes the API
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file `astryx upgrade` from a folder inside a project: a `--path` you type
|
|
5
|
+
* is read from that folder, however you spell it, and without `--path` the
|
|
6
|
+
* upgrade scans the project's own src. The option's default must not read as
|
|
7
|
+
* a typed `./src`. The `--json` result names files relative to the folder the
|
|
8
|
+
* command ran in.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as os from 'node:os';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
+
|
|
17
|
+
/** Core 0.7.0 renamed this prop; its codemod renames it on Typeahead. */
|
|
18
|
+
const OLD_PROP_SOURCE =
|
|
19
|
+
"import {Typeahead} from '@astryxdesign/core';\n" +
|
|
20
|
+
'export const App = () => <Typeahead label="Fruit" emptySearchResultsText="None" />;\n';
|
|
21
|
+
|
|
22
|
+
let root;
|
|
23
|
+
|
|
24
|
+
/** @param {string} rel @param {string} body */
|
|
25
|
+
function write(rel, body) {
|
|
26
|
+
const file = path.join(root, rel);
|
|
27
|
+
fs.mkdirSync(path.dirname(file), {recursive: true});
|
|
28
|
+
fs.writeFileSync(file, body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {string} rel */
|
|
32
|
+
const renamed = rel =>
|
|
33
|
+
fs
|
|
34
|
+
.readFileSync(path.join(root, rel), 'utf-8')
|
|
35
|
+
.includes('emptySearchText="None"');
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-upgrade-subfolder-'));
|
|
39
|
+
write(
|
|
40
|
+
'package.json',
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
name: 'app',
|
|
43
|
+
private: true,
|
|
44
|
+
dependencies: {'@astryxdesign/core': '0.7.0'},
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
write(
|
|
48
|
+
'node_modules/@astryxdesign/core/package.json',
|
|
49
|
+
JSON.stringify({name: '@astryxdesign/core', version: '0.7.0'}),
|
|
50
|
+
);
|
|
51
|
+
write('src/App.tsx', OLD_PROP_SOURCE);
|
|
52
|
+
// A folder inside the project with source of its own and no package.json.
|
|
53
|
+
write('frontend/src/App.tsx', OLD_PROP_SOURCE);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
fs.rmSync(root, {recursive: true, force: true});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** @param {string[]} args @param {string} [folder] */
|
|
61
|
+
async function upgradeFrom(args, folder = 'frontend') {
|
|
62
|
+
const r = await runCli(
|
|
63
|
+
['upgrade', '--from', '0.6.8', ...args, '--apply', '--json'],
|
|
64
|
+
{
|
|
65
|
+
cwd: path.join(root, folder),
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
expect(r.status).toBe(0);
|
|
69
|
+
return JSON.parse(r.stdout);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('upgrade --path from a folder inside the project', () => {
|
|
73
|
+
it.each([['./src'], ['src'], ['src/'], ['./src/']])(
|
|
74
|
+
'reads --path %s from the folder you ran in',
|
|
75
|
+
async spelling => {
|
|
76
|
+
const result = await upgradeFrom(['--path', spelling]);
|
|
77
|
+
|
|
78
|
+
expect(result.data.modifiedFiles).toEqual(['src/App.tsx']);
|
|
79
|
+
expect(renamed('frontend/src/App.tsx')).toBe(true);
|
|
80
|
+
expect(renamed('src/App.tsx')).toBe(false);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
it("scans the project's src without --path", async () => {
|
|
85
|
+
const result = await upgradeFrom([]);
|
|
86
|
+
|
|
87
|
+
expect(result.data.modifiedFiles).toEqual(['../src/App.tsx']);
|
|
88
|
+
expect(renamed('src/App.tsx')).toBe(true);
|
|
89
|
+
expect(renamed('frontend/src/App.tsx')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('names files from the project root when run there', async () => {
|
|
93
|
+
const result = await upgradeFrom([], '.');
|
|
94
|
+
|
|
95
|
+
expect(result.data.modifiedFiles).toEqual(['src/App.tsx']);
|
|
96
|
+
});
|
|
97
|
+
});
|
package/clients/cli/index.mjs
CHANGED
|
@@ -32,7 +32,12 @@ import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
|
|
|
32
32
|
import {installJsonShim} from './lib/json-shim.mjs';
|
|
33
33
|
import {addDocHelp, markReportsResult} from './lib/define-command.mjs';
|
|
34
34
|
import {doc as manifestDoc} from './commands/manifest.doc.mjs';
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
agentDocsTargetDir,
|
|
37
|
+
assertAgentDocsTarget,
|
|
38
|
+
isAstryxInitialized,
|
|
39
|
+
isAstryxPromptInReach,
|
|
40
|
+
} from '../../foundation/agent-docs/agent-docs.mjs';
|
|
36
41
|
import {CLI_ROOT} from '../../foundation/fs/paths.mjs';
|
|
37
42
|
import {setupNudge} from '../../foundation/agent-docs/agent-doc-state.mjs';
|
|
38
43
|
import * as debug from '../../foundation/debug/index.mjs';
|
|
@@ -635,6 +640,19 @@ export async function createProgram() {
|
|
|
635
640
|
if (INTEGRATION_MANIFEST_FILES.some(file => fs.existsSync(path.join(cwd, file)))) return;
|
|
636
641
|
if (inOwnSourceCheckout(cwd)) return; // developing Astryx itself, not an app
|
|
637
642
|
if (isAstryxPromptInReach(cwd)) return; // already set up — stay quiet
|
|
643
|
+
// Where init writes the docs, by init's own rule: the project root, which
|
|
644
|
+
// can sit above a nested package such as functions/. Inside a git
|
|
645
|
+
// repository the walk above already reaches it (the root never lies
|
|
646
|
+
// above the repository root), so this changes nothing there; outside a
|
|
647
|
+
// repository the walk checks only cwd, and this finds the root.
|
|
648
|
+
if (isAstryxInitialized(agentDocsTargetDir(cwd))) return;
|
|
649
|
+
// In the home folder, or at the top of a settings folder in it, init
|
|
650
|
+
// writes no agent docs, so naming it would repeat on every command.
|
|
651
|
+
try {
|
|
652
|
+
assertAgentDocsTarget(agentDocsTargetDir(cwd));
|
|
653
|
+
} catch {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
638
656
|
// Same wording as the core/cli postinstall nudges. getCliInvocation()
|
|
639
657
|
// renders the correct form for THIS project — an integration's cliCommand,
|
|
640
658
|
// scoped `npx @astryxdesign/cli` one-off, or `<pm> astryx` when installed —
|