Skip to content

Commit 2c8ded6

Browse files
authored
refactor: remove old versioned marker system from agent instructions injection (#802)
Replace the `<!--injected-by-vite-plus-v...-->` marker system with the `<!--VITE PLUS START-->` / `<!--VITE PLUS END-->` markers already used by `utils/agent.ts`. This removes `getOwnVersion()`, version comparison logic, and the old regex constants, reusing `replaceMarkedAgentInstructionsSection()` from the shared utility instead.
1 parent 127711c commit 2c8ded6

File tree

4 files changed

+168
-51
lines changed

4 files changed

+168
-51
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { join } from 'node:path';
2+
3+
import * as prompts from '@voidzero-dev/vite-plus-prompts';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
6+
import { pkgRoot } from '../../utils/path.js';
7+
8+
const AGENT_TEMPLATE = ['<!--VITE PLUS START-->', 'template block', '<!--VITE PLUS END-->'].join(
9+
'\n',
10+
);
11+
12+
const { files, fsMock } = vi.hoisted(() => {
13+
const files = new Map<string, string>();
14+
const fsMock = {
15+
existsSync: (p: string) => files.has(p),
16+
readFileSync: (p: string) => {
17+
const content = files.get(p);
18+
if (content === undefined) {
19+
throw new Error(`ENOENT: no such file "${p}"`);
20+
}
21+
return content;
22+
},
23+
writeFileSync: (p: string, data: string) => {
24+
files.set(p, data);
25+
},
26+
};
27+
return { files, fsMock };
28+
});
29+
30+
vi.mock('node:fs', () => ({
31+
...fsMock,
32+
default: fsMock,
33+
}));
34+
35+
import { injectAgentBlock } from '../agent.js';
36+
37+
beforeEach(() => {
38+
files.clear();
39+
files.set(join(pkgRoot, 'AGENTS.md'), AGENT_TEMPLATE);
40+
vi.spyOn(prompts.log, 'info').mockImplementation(() => {});
41+
vi.spyOn(prompts.log, 'success').mockImplementation(() => {});
42+
});
43+
44+
afterEach(() => {
45+
vi.restoreAllMocks();
46+
});
47+
48+
describe('injectAgentBlock', () => {
49+
it('creates file with template when file does not exist', () => {
50+
injectAgentBlock('/project', 'AGENTS.md');
51+
expect(files.get(join('/project', 'AGENTS.md'))).toBe(AGENT_TEMPLATE);
52+
});
53+
54+
it('updates marked section when file has markers', () => {
55+
const existing = [
56+
'# Header',
57+
'<!--VITE PLUS START-->',
58+
'old content',
59+
'<!--VITE PLUS END-->',
60+
'# Footer',
61+
].join('\n');
62+
files.set(join('/project', 'CLAUDE.md'), existing);
63+
64+
injectAgentBlock('/project', 'CLAUDE.md');
65+
66+
expect(files.get(join('/project', 'CLAUDE.md'))).toBe(
67+
[
68+
'# Header',
69+
'<!--VITE PLUS START-->',
70+
'template block',
71+
'<!--VITE PLUS END-->',
72+
'# Footer',
73+
].join('\n'),
74+
);
75+
});
76+
77+
it('does not write when content is already up-to-date', () => {
78+
files.set(join('/project', 'AGENTS.md'), AGENT_TEMPLATE);
79+
const infoSpy = vi.spyOn(prompts.log, 'info');
80+
81+
injectAgentBlock('/project', 'AGENTS.md');
82+
83+
expect(infoSpy).toHaveBeenCalledWith('AGENTS.md already has up-to-date Vite+ instructions');
84+
});
85+
86+
it('appends template when file exists without markers', () => {
87+
files.set(join('/project', 'AGENTS.md'), '# Existing content\n');
88+
89+
injectAgentBlock('/project', 'AGENTS.md');
90+
91+
expect(files.get(join('/project', 'AGENTS.md'))).toBe(
92+
`# Existing content\n\n${AGENT_TEMPLATE}`,
93+
);
94+
});
95+
});

packages/cli/src/config/agent.ts

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts';
66
import {
77
detectAgents,
88
getAgentById,
9+
hasExistingAgentInstructions,
10+
replaceMarkedAgentInstructionsSection,
911
type AgentConfig,
1012
type McpConfigTarget,
1113
} from '../utils/agent.js';
@@ -81,76 +83,38 @@ export async function resolveAgentSetup(
8183
return pickAgentWhenUndetected();
8284
}
8385

84-
// --- Version and template reading ---
85-
86-
function getOwnVersion(): string {
87-
const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf-8'));
88-
if (typeof pkg.version !== 'string') {
89-
throw new Error('vite-plus package.json is missing a "version" field');
90-
}
91-
return pkg.version;
92-
}
86+
// --- Template reading ---
9387

9488
function readAgentPrompt(): string {
9589
return readFileSync(join(pkgRoot, 'AGENTS.md'), 'utf-8');
9690
}
9791

98-
// --- Versioned injection ---
92+
// --- Agent instructions injection ---
9993

100-
const MARKER_OPEN_RE = /<!--injected-by-vite-plus-v([\w.+-]+)-->/;
101-
const MARKER_CLOSE = '<!--/injected-by-vite-plus-->';
102-
const MARKER_BLOCK_RE =
103-
/<!--injected-by-vite-plus-v[\w.+-]+-->\n[\s\S]*?<!--\/injected-by-vite-plus-->/;
104-
105-
export function hasExistingAgentInstructions(root: string): boolean {
106-
for (const file of ['AGENTS.md', 'CLAUDE.md']) {
107-
const fullPath = join(root, file);
108-
if (existsSync(fullPath)) {
109-
const content = readFileSync(fullPath, 'utf-8');
110-
if (MARKER_OPEN_RE.test(content)) {
111-
return true;
112-
}
113-
}
114-
}
115-
return false;
116-
}
94+
export { hasExistingAgentInstructions };
11795

11896
export function injectAgentBlock(root: string, filePath: string): void {
11997
const fullPath = join(root, filePath);
120-
const version = getOwnVersion();
121-
const promptContent = readAgentPrompt();
122-
const openMarker = `<!--injected-by-vite-plus-v${version}-->`;
123-
const block = `${openMarker}\n${promptContent}\n${MARKER_CLOSE}`;
98+
const template = readAgentPrompt();
12499

125100
if (existsSync(fullPath)) {
126101
const existing = readFileSync(fullPath, 'utf-8');
127-
const match = existing.match(MARKER_OPEN_RE);
128-
if (match) {
129-
if (match[1] === version) {
130-
prompts.log.info(`${filePath} already has Vite+ instructions (v${version})`);
131-
return;
132-
}
133-
// Replace existing block with updated version
134-
const updated = existing.replace(MARKER_BLOCK_RE, block);
135-
if (updated === existing) {
136-
// Closing marker is missing or malformed — append fresh block
137-
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
138-
writeFileSync(fullPath, existing + separator + block + '\n');
139-
prompts.log.warn(`Existing Vite+ block in ${filePath} was malformed; appended fresh block`);
140-
} else {
102+
const updated = replaceMarkedAgentInstructionsSection(existing, template);
103+
if (updated !== undefined) {
104+
if (updated !== existing) {
141105
writeFileSync(fullPath, updated);
142-
prompts.log.success(
143-
`Updated Vite+ instructions in ${filePath} (v${match[1]} → v${version})`,
144-
);
106+
prompts.log.success(`Updated Vite+ instructions in ${filePath}`);
107+
} else {
108+
prompts.log.info(`${filePath} already has up-to-date Vite+ instructions`);
145109
}
146110
} else {
147-
// Append block to end of file
111+
// No markers found — append template
148112
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
149-
writeFileSync(fullPath, existing + separator + block + '\n');
113+
writeFileSync(fullPath, existing + separator + template);
150114
prompts.log.success(`Added Vite+ instructions to ${filePath}`);
151115
}
152116
} else {
153-
writeFileSync(fullPath, block + '\n');
117+
writeFileSync(fullPath, template);
154118
prompts.log.success(`Created ${filePath} with Vite+ instructions`);
155119
}
156120
}

packages/cli/src/utils/__tests__/agent.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
88
import {
99
detectExistingAgentTargetPaths,
1010
detectExistingAgentTargetPath,
11+
hasExistingAgentInstructions,
1112
replaceMarkedAgentInstructionsSection,
1213
resolveAgentTargetPaths,
1314
writeAgentInstructions,
@@ -103,6 +104,15 @@ class InMemoryFs {
103104
this.nodes.delete(normalizedPath);
104105
}
105106

107+
readFileSync(filePath: fs.PathLike): string {
108+
const resolvedPath = this.resolvePath(filePath);
109+
const node = this.nodes.get(resolvedPath);
110+
if (!node || node.kind !== 'file') {
111+
throw new Error(`ENOENT: no such file "${String(filePath)}"`);
112+
}
113+
return node.content;
114+
}
115+
106116
isSymlink(filePath: string): boolean {
107117
return this.lstatSync(filePath).isSymbolicLink();
108118
}
@@ -197,6 +207,9 @@ beforeEach(async () => {
197207

198208
vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => mockFs.existsSync(filePath));
199209
vi.spyOn(fs, 'lstatSync').mockImplementation((filePath) => mockFs.lstatSync(filePath));
210+
vi.spyOn(fs, 'readFileSync').mockImplementation((filePath) =>
211+
mockFs.readFileSync(filePath as fs.PathLike),
212+
);
200213

201214
vi.spyOn(fsPromises, 'appendFile').mockImplementation(async (filePath, data) =>
202215
mockFs.appendFile(filePath as fs.PathLike, String(data)),
@@ -375,3 +388,34 @@ describe('writeAgentInstructions symlink behavior', () => {
375388
expect(successSpy).not.toHaveBeenCalledWith('Updated agent instructions in AGENTS.md');
376389
});
377390
});
391+
392+
describe('hasExistingAgentInstructions', () => {
393+
it('returns true when an agent file has start marker', async () => {
394+
const dir = await createProjectDir();
395+
await mockFs.writeFile(
396+
path.join(dir, 'AGENTS.md'),
397+
'<!--VITE PLUS START-->\ncontent\n<!--VITE PLUS END-->',
398+
);
399+
expect(hasExistingAgentInstructions(dir)).toBe(true);
400+
});
401+
402+
it('returns true when CLAUDE.md has start marker', async () => {
403+
const dir = await createProjectDir();
404+
await mockFs.writeFile(
405+
path.join(dir, 'CLAUDE.md'),
406+
'<!--VITE PLUS START-->\ncontent\n<!--VITE PLUS END-->',
407+
);
408+
expect(hasExistingAgentInstructions(dir)).toBe(true);
409+
});
410+
411+
it('returns false when files exist without markers', async () => {
412+
const dir = await createProjectDir();
413+
await mockFs.writeFile(path.join(dir, 'AGENTS.md'), '# No markers here');
414+
expect(hasExistingAgentInstructions(dir)).toBe(false);
415+
});
416+
417+
it('returns false when no files exist', async () => {
418+
const dir = await createProjectDir();
419+
expect(hasExistingAgentInstructions(dir)).toBe(false);
420+
});
421+
});

packages/cli/src/utils/agent.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,20 @@ export function detectExistingAgentTargetPath(projectRoot: string) {
265265
return detectExistingAgentTargetPaths(projectRoot)?.[0];
266266
}
267267

268+
export function hasExistingAgentInstructions(projectRoot: string): boolean {
269+
const targetPaths = detectExistingAgentTargetPaths(projectRoot);
270+
if (!targetPaths) {
271+
return false;
272+
}
273+
for (const targetPath of targetPaths) {
274+
const content = fs.readFileSync(path.join(projectRoot, targetPath), 'utf-8');
275+
if (content.includes(AGENT_INSTRUCTIONS_START_MARKER)) {
276+
return true;
277+
}
278+
}
279+
return false;
280+
}
281+
268282
export function resolveAgentTargetPaths(agent?: string | string[]) {
269283
const agentNames = parseAgentNames(agent);
270284
const resolvedAgentNames = agentNames.length > 0 ? agentNames : ['other'];

0 commit comments

Comments
 (0)