vike 0.4.264 → 0.4.266
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/dist/node/vite/plugins/pluginDev/logSkillHint.d.ts +4 -0
- package/dist/node/vite/plugins/pluginDev/logSkillHint.js +276 -0
- package/dist/node/vite/plugins/pluginDev.js +4 -10
- package/dist/node/vite/shared/cache.d.ts +5 -0
- package/dist/node/vite/shared/cache.js +50 -0
- package/dist/node/vite/shared/resolveVikeConfigInternal/crawlPlusFiles.js +1 -1
- package/dist/types/Config.d.ts +2 -6
- package/dist/utils/PROJECT_VERSION.d.ts +1 -1
- package/dist/utils/PROJECT_VERSION.js +1 -1
- package/dist/utils/crawlFiles.d.ts +10 -2
- package/dist/utils/crawlFiles.js +6 -5
- package/package.json +4 -3
- package/skills/vike/SKILL.md +6 -0
- package/dist/node/vite/plugins/pluginDev/autoAddVikeSkill.d.ts +0 -3
- package/dist/node/vite/plugins/pluginDev/autoAddVikeSkill.js +0 -178
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
export { logSkillHint };
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import pc from '@brillout/picocolors';
|
|
8
|
+
import { assert, assertInfo, assertUsage } from '../../../../utils/assert.js';
|
|
9
|
+
import { assertKeys } from '../../../../utils/assertKeys.js';
|
|
10
|
+
import { checkType } from '../../../../utils/checkType.js';
|
|
11
|
+
import { crawlFiles } from '../../../../utils/crawlFiles.js';
|
|
12
|
+
import { getGlobalObject } from '../../../../utils/getGlobalObject.js';
|
|
13
|
+
import { getVikeConfigError } from '../../../../shared-server-node/getVikeConfigError.js';
|
|
14
|
+
import { requireResolveOptional } from '../../../../utils/requireResolve.js';
|
|
15
|
+
import { setTimeoutUnref } from '../../../../utils/setTimeoutUnref.js';
|
|
16
|
+
import { isObject } from '../../../../utils/isObject.js';
|
|
17
|
+
import { toPosixPath } from '../../../../utils/path.js';
|
|
18
|
+
import { unique } from '../../../../utils/unique.js';
|
|
19
|
+
import { getVikeConfigInternal } from '../../shared/resolveVikeConfigInternal.js';
|
|
20
|
+
import { getCacheValue, setCacheValue } from '../../shared/cache.js';
|
|
21
|
+
import '../../assertEnvVite.js';
|
|
22
|
+
const execFileA = promisify(execFile);
|
|
23
|
+
const importMetaUrl = import.meta.url;
|
|
24
|
+
const globalObject = getGlobalObject('logSkillHint.ts', {
|
|
25
|
+
alreadyChecked: false,
|
|
26
|
+
});
|
|
27
|
+
const docsUrl = 'https://vike.dev/ai#skill';
|
|
28
|
+
const seeDocs = `— see ${pc.underline(docsUrl)}`;
|
|
29
|
+
const suppressHint = `set ${pc.cyan('+ai.skill')} to ${pc.cyan('false')} to suppress this log.`;
|
|
30
|
+
const logMissing = `Add Vike's skill for AI agents (Claude Code, Codex, Cursor, ...) ${seeDocs}, or ${suppressHint}`;
|
|
31
|
+
const logOutdated = (skillFilePaths) => {
|
|
32
|
+
const isPlural = skillFilePaths.length > 1;
|
|
33
|
+
const files = skillFilePaths.map((f) => pc.cyan(f)).join(', ');
|
|
34
|
+
return `Your Vike skill${isPlural ? 's' : ''} ${files} ${isPlural ? "don't" : "doesn't"} match the official ${pc.cyan('vike/SKILL.md')}, update ${isPlural ? 'them' : 'it'} ${seeDocs}, or, if you maintain your own version, ${suppressHint}`;
|
|
35
|
+
};
|
|
36
|
+
const skillName = 'vike';
|
|
37
|
+
// The skill file shipped by the vike npm package: node_modules/vike/skills/vike/SKILL.md (see packages/vike/scripts/copySkill.mjs)
|
|
38
|
+
const skillFilePathInsidePackage = 'skills/vike/SKILL.md';
|
|
39
|
+
// Cache entry at node_modules/.vike/cache.json, see cache.ts
|
|
40
|
+
// - `false` => the check didn't log anything last time => skip the check (forever, until node_modules/ is removed)
|
|
41
|
+
// - Nothing is written as long as the hint is logged => the check is re-run upon every dev start
|
|
42
|
+
const cacheKey = 'logSkillHint';
|
|
43
|
+
// Log a hint if the user didn't install Vike's skill for AI agents (vike/SKILL.md), or if it differs from the official one — https://vike.dev/ai#skill
|
|
44
|
+
function logSkillHint(server, userRootDir) {
|
|
45
|
+
applyLate(server, () => checkSkill(userRootDir));
|
|
46
|
+
}
|
|
47
|
+
// Apply late — 5 seconds after the first request, or at most 10 seconds after the dev server started — so that it doesn't slow down dev start nor the first page requests.
|
|
48
|
+
function applyLate(server, callback) {
|
|
49
|
+
let isDone = false;
|
|
50
|
+
const runAfter = (milliseconds) => {
|
|
51
|
+
setTimeoutUnref(() => {
|
|
52
|
+
if (isDone)
|
|
53
|
+
return;
|
|
54
|
+
isDone = true;
|
|
55
|
+
callback();
|
|
56
|
+
}, milliseconds);
|
|
57
|
+
};
|
|
58
|
+
if (server.httpServer) {
|
|
59
|
+
server.httpServer.once('listening', () => runAfter(10 * 1000));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Middleware mode: the HTTP server is owned by the user
|
|
63
|
+
runAfter(10 * 1000);
|
|
64
|
+
}
|
|
65
|
+
let isFirstRequest = true;
|
|
66
|
+
server.middlewares.use((_req, _res, next) => {
|
|
67
|
+
if (isFirstRequest) {
|
|
68
|
+
isFirstRequest = false;
|
|
69
|
+
runAfter(5 * 1000);
|
|
70
|
+
}
|
|
71
|
+
next();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async function checkSkill(userRootDir) {
|
|
75
|
+
try {
|
|
76
|
+
await checkSkillUnsafe(userRootDir);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
// The check runs in a timer (see applyLate()): a thrown error would be an unhandled rejection that kills the dev server => log it instead.
|
|
80
|
+
// - Environmental failures (Git missing, unreadable files, ...) are handled gracefully and don't throw.
|
|
81
|
+
// - What can throw: usage errors (e.g. an invalid +ai.skill value) and bugs.
|
|
82
|
+
console.error(err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function checkSkillUnsafe(userRootDir) {
|
|
86
|
+
if (globalObject.alreadyChecked)
|
|
87
|
+
return;
|
|
88
|
+
// Skip CI environments: the hint is meant for the machine of an app developer.
|
|
89
|
+
if (process.env.CI)
|
|
90
|
+
return;
|
|
91
|
+
const vikeConfig = await getVikeConfigInternal();
|
|
92
|
+
// Maybe the user disabled the check in a config file that currently has an error => retry later (Vite restarts upon config changes).
|
|
93
|
+
if (getVikeConfigError())
|
|
94
|
+
return;
|
|
95
|
+
if (!getConfigValueAiSkill(vikeConfig))
|
|
96
|
+
return;
|
|
97
|
+
globalObject.alreadyChecked = true;
|
|
98
|
+
// The check is skipped forever once it didn't log anything (until node_modules/ is removed) — see cacheKey
|
|
99
|
+
if ((await getCacheValue(userRootDir, cacheKey)) === false)
|
|
100
|
+
return;
|
|
101
|
+
const skillState = await getSkillState(userRootDir);
|
|
102
|
+
if (skillState.state === 'missing') {
|
|
103
|
+
assertInfo(false, logMissing, { onlyOnce: true });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (skillState.state === 'outdated') {
|
|
107
|
+
assertInfo(false, logOutdated(skillState.skillFilePaths), { onlyOnce: true });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (skillState.state === 'installed' ||
|
|
111
|
+
skillState.state === 'not-using-ai-agents' ||
|
|
112
|
+
skillState.state === 'vike-not-from-npm') {
|
|
113
|
+
await setCacheValue(userRootDir, cacheKey, false);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
checkType(skillState);
|
|
117
|
+
assert(false);
|
|
118
|
+
}
|
|
119
|
+
// Determine the state of the user's skill — without side effects: the caller checkSkillUnsafe() performs exactly one action per state.
|
|
120
|
+
async function getSkillState(userRootDir) {
|
|
121
|
+
const repoRootDir = await getRepoRootDir(userRootDir);
|
|
122
|
+
const skillsDirs = await findSkillsDirs(repoRootDir, userRootDir);
|
|
123
|
+
const isUsingAiAgents = skillsDirs.length > 0 || (await hasAgentMarker(userRootDir, repoRootDir));
|
|
124
|
+
if (!isUsingAiAgents)
|
|
125
|
+
return { state: 'not-using-ai-agents' };
|
|
126
|
+
const skillContentExpected = await getSkillContentExpected();
|
|
127
|
+
if (skillContentExpected === null)
|
|
128
|
+
return { state: 'vike-not-from-npm' };
|
|
129
|
+
const skillFiles = await findSkillFiles(repoRootDir, skillsDirs, skillContentExpected);
|
|
130
|
+
if (skillFiles.length === 0)
|
|
131
|
+
return { state: 'missing' };
|
|
132
|
+
const skillFilesOutdated = skillFiles.filter((f) => f.isOutdated);
|
|
133
|
+
if (skillFilesOutdated.length > 0) {
|
|
134
|
+
const skillFilePaths = skillFilesOutdated.map((f) => toPosixPath(path.relative(userRootDir, f.filePathAbsolute)));
|
|
135
|
+
return { state: 'outdated', skillFilePaths };
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
return { state: 'installed' };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// https://vike.dev/ai#settings
|
|
142
|
+
function getConfigValueAiSkill(vikeConfig) {
|
|
143
|
+
const configAi = vikeConfig.config.ai;
|
|
144
|
+
if (configAi === undefined)
|
|
145
|
+
return true;
|
|
146
|
+
assertUsage(isObject(configAi), `Setting ${pc.cyan('ai')} should be an object`);
|
|
147
|
+
assertKeys(configAi, ['skill'], `Setting ${pc.cyan('ai')}:`);
|
|
148
|
+
const skill = configAi.skill;
|
|
149
|
+
if (skill === undefined)
|
|
150
|
+
return true;
|
|
151
|
+
assertUsage(typeof skill === 'boolean', `${pc.cyan('+ai.skill')} should be a boolean, see ${pc.underline(docsUrl)}`);
|
|
152
|
+
return skill;
|
|
153
|
+
}
|
|
154
|
+
// The root directory of the user's Git repository — skills directories usually live at the repository root (e.g. monorepos).
|
|
155
|
+
async function getRepoRootDir(userRootDir) {
|
|
156
|
+
try {
|
|
157
|
+
const { stdout } = await execFileA('git', ['rev-parse', '--show-toplevel'], { cwd: userRootDir });
|
|
158
|
+
const gitRootDir = stdout.toString().trim();
|
|
159
|
+
if (gitRootDir)
|
|
160
|
+
return gitRootDir;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Git isn't installed, or the app isn't inside a Git repository
|
|
164
|
+
}
|
|
165
|
+
return userRootDir;
|
|
166
|
+
}
|
|
167
|
+
// Discover the skills directories of the user's repository, following the Agent Skills convention `**/skills/*/SKILL.md` (https://agentskills.io) — e.g. .claude/skills/ (Claude Code) and .agents/skills/ (Codex, Gemini CLI, Cursor, ...).
|
|
168
|
+
async function findSkillsDirs(repoRootDir, userRootDir) {
|
|
169
|
+
// - skills-npm installs skills as gitignored symlinks (`**/skills/npm-*`) that `$ git ls-files` cannot see => tinyglobby (which finds gitignored files and follows symlinks)
|
|
170
|
+
// - Otherwise `$ git ls-files` only: we don't want to crawl the entire directory tree of the user's repository upon every dev start
|
|
171
|
+
const useGlob = isUsingSkillsNpm(userRootDir);
|
|
172
|
+
const files = await crawlFiles({
|
|
173
|
+
filePattern: '**/skills/*/SKILL',
|
|
174
|
+
fileExtension: ['md'],
|
|
175
|
+
cwd: repoRootDir,
|
|
176
|
+
// Skills directories usually live inside dot directories (e.g. .claude/ and .agents/)
|
|
177
|
+
dot: true,
|
|
178
|
+
crawler: { git: !useGlob, glob: useGlob },
|
|
179
|
+
});
|
|
180
|
+
const skillsDirs = unique(files.map((filePath) => path.posix.dirname(path.posix.dirname(filePath)))).sort();
|
|
181
|
+
return skillsDirs;
|
|
182
|
+
}
|
|
183
|
+
// Whether the user installed skills-npm (https://github.com/antfu/skills-npm)
|
|
184
|
+
function isUsingSkillsNpm(userRootDir) {
|
|
185
|
+
return requireResolveOptional({ importPath: 'skills-npm', importerFilePath: null, userRootDir }) !== null;
|
|
186
|
+
}
|
|
187
|
+
// Whether the app seems to use AI agents, even without any skills directory: instruction files and config directories of AI agents, at the app's root directory and at the repository's root directory.
|
|
188
|
+
const agentMarkers = [
|
|
189
|
+
// Instruction files
|
|
190
|
+
'AGENTS.md', // Codex, Cursor, Gemini CLI, GitHub Copilot, Amp, Zed, OpenCode, Jules, Devin, ...
|
|
191
|
+
'CLAUDE.md', // Claude Code
|
|
192
|
+
'GEMINI.md', // Gemini CLI
|
|
193
|
+
'.cursorrules', // Cursor (legacy)
|
|
194
|
+
'.clinerules', // Cline (file or directory)
|
|
195
|
+
'.windsurfrules', // Windsurf (legacy)
|
|
196
|
+
'.github/copilot-instructions.md', // GitHub Copilot
|
|
197
|
+
'.mcp.json', // Claude Code (project MCP servers)
|
|
198
|
+
// Config directories
|
|
199
|
+
'.claude', // Claude Code
|
|
200
|
+
'.agents', // Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode, Cline, Amp, Zed, ...
|
|
201
|
+
'.cursor', // Cursor
|
|
202
|
+
'.gemini', // Gemini CLI
|
|
203
|
+
'.windsurf', // Windsurf
|
|
204
|
+
'.junie', // JetBrains Junie
|
|
205
|
+
'.kiro', // Kiro
|
|
206
|
+
'.roo', // Roo Code
|
|
207
|
+
'.continue', // Continue
|
|
208
|
+
'.github/instructions', // GitHub Copilot
|
|
209
|
+
];
|
|
210
|
+
async function hasAgentMarker(userRootDir, repoRootDir) {
|
|
211
|
+
const dirs = unique([userRootDir, repoRootDir]);
|
|
212
|
+
const results = await Promise.all(dirs.flatMap((dir) => agentMarkers.map((marker) => isReadable(path.join(dir, ...marker.split('/'))))));
|
|
213
|
+
return results.some(Boolean);
|
|
214
|
+
}
|
|
215
|
+
// Find the installed copies of Vike's skill: `vike/SKILL.md` (manual and skills.sh installs) as well as `npm-vike-vike/SKILL.md` (skills-npm installs).
|
|
216
|
+
async function findSkillFiles(repoRootDir, skillsDirs, skillContentExpected) {
|
|
217
|
+
const skillFiles = [];
|
|
218
|
+
for (const skillsDir of skillsDirs) {
|
|
219
|
+
const skillsDirAbsolute = path.join(repoRootDir, ...skillsDir.split('/'));
|
|
220
|
+
const entries = await fs.readdir(skillsDirAbsolute).catch(() => []);
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
if (!entry.toLowerCase().includes(skillName))
|
|
223
|
+
continue;
|
|
224
|
+
const filePathAbsolute = path.join(skillsDirAbsolute, entry, 'SKILL.md');
|
|
225
|
+
// fs.readFile() follows symlinks — a dangling symlink (e.g. after removing node_modules/) is treated as missing
|
|
226
|
+
const content = await fs.readFile(filePathAbsolute, 'utf8').catch(() => null);
|
|
227
|
+
if (content === null)
|
|
228
|
+
continue;
|
|
229
|
+
// Skip other skills, e.g. `vike-react/SKILL.md`
|
|
230
|
+
if (getSkillName(content) !== skillName)
|
|
231
|
+
continue;
|
|
232
|
+
const isOutdated = normalizeContent(content) !== normalizeContent(skillContentExpected);
|
|
233
|
+
skillFiles.push({ filePathAbsolute, isOutdated });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return skillFiles;
|
|
237
|
+
}
|
|
238
|
+
// The `name` field of the YAML frontmatter
|
|
239
|
+
function getSkillName(skillFileContent) {
|
|
240
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillFileContent)?.[1];
|
|
241
|
+
if (!frontmatter)
|
|
242
|
+
return null;
|
|
243
|
+
const name = /^name:[ \t]*["']?([^"'\s]+)["']?[ \t]*$/m.exec(frontmatter)?.[1];
|
|
244
|
+
return name ?? null;
|
|
245
|
+
}
|
|
246
|
+
// Ignore line endings (e.g. Git's autocrlf on Windows) and trailing whitespace
|
|
247
|
+
function normalizeContent(content) {
|
|
248
|
+
return content
|
|
249
|
+
.split(/\r?\n/)
|
|
250
|
+
.map((line) => line.trimEnd())
|
|
251
|
+
.join('\n')
|
|
252
|
+
.trim();
|
|
253
|
+
}
|
|
254
|
+
async function getSkillContentExpected() {
|
|
255
|
+
// [RELATIVE_PATH_FROM_DIST] Current file: node_modules/vike/dist/node/vite/plugins/pluginDev/logSkillHint.js
|
|
256
|
+
assert(importMetaUrl.includes('/dist/node/vite/plugins/pluginDev/'));
|
|
257
|
+
const filePath = fileURLToPath(new URL(`../../../../../${skillFilePathInsidePackage}`, importMetaUrl));
|
|
258
|
+
let fileContent;
|
|
259
|
+
try {
|
|
260
|
+
fileContent = await fs.readFile(filePath, 'utf8');
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// The file is added upon publishing (`$ pnpm publish` => `prepack` script) => it's missing when Vike is linked (e.g. when running an example of the Vike monorepo)
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return fileContent;
|
|
267
|
+
}
|
|
268
|
+
async function isReadable(filePath) {
|
|
269
|
+
try {
|
|
270
|
+
await fs.access(filePath);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -2,10 +2,9 @@ export { pluginDev };
|
|
|
2
2
|
export { logDockerHint };
|
|
3
3
|
import { optimizeDeps, resolveOptimizeDeps } from './pluginDev/optimizeDeps.js';
|
|
4
4
|
import { determineFsAllowList } from './pluginDev/determineFsAllowList.js';
|
|
5
|
-
|
|
5
|
+
import { logSkillHint } from './pluginDev/logSkillHint.js';
|
|
6
6
|
import { addSsrMiddleware } from '../shared/addSsrMiddleware.js';
|
|
7
7
|
import { isDebugError } from '../../../utils/debug.js';
|
|
8
|
-
// import { setTimeoutUnref } from '../../../utils/setTimeoutUnref.js'
|
|
9
8
|
import { applyDev } from '../../../utils/isDev.js';
|
|
10
9
|
import { isDocker } from '../../../utils/isDocker.js';
|
|
11
10
|
import { assertWarning } from '../../../utils/assert.js';
|
|
@@ -36,16 +35,11 @@ function pluginDev() {
|
|
|
36
35
|
logDockerHint(config.server.host);
|
|
37
36
|
},
|
|
38
37
|
},
|
|
39
|
-
// TODO/soon: re-enable automatically adding vike/SKILL.md (https://vike.dev/ai#skill).
|
|
40
|
-
// Temporarily disabled — https://github.com/vikejs/vike/pull/3465 got some backlash, let's find a better solution first.
|
|
41
|
-
/*
|
|
42
38
|
configureServer: {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
39
|
+
handler(server) {
|
|
40
|
+
logSkillHint(server, config.root);
|
|
41
|
+
},
|
|
47
42
|
},
|
|
48
|
-
*/
|
|
49
43
|
},
|
|
50
44
|
{
|
|
51
45
|
name: 'vike:pluginDev:post',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export { getCacheValue };
|
|
2
|
+
export { setCacheValue };
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { isObject } from '../../../utils/isObject.js';
|
|
6
|
+
import { findFile } from '../../../utils/findFile.js';
|
|
7
|
+
import { toPosixPath } from '../../../utils/path.js';
|
|
8
|
+
import '../assertEnvVite.js';
|
|
9
|
+
// Persistent key-value cache at node_modules/.vike/cache.json — e.g. to remember that a check was already done.
|
|
10
|
+
// - Best-effort: a missing or invalid file reads as empty, and write errors (e.g. read-only file system) are swallowed.
|
|
11
|
+
// - One top-level key per feature: entries are merged into the existing file.
|
|
12
|
+
// - Values must be JSON-serializable.
|
|
13
|
+
const cacheFileName = 'cache.json';
|
|
14
|
+
async function getCacheValue(userRootDir, key) {
|
|
15
|
+
const cache = await readCache(userRootDir);
|
|
16
|
+
return cache[key];
|
|
17
|
+
}
|
|
18
|
+
async function setCacheValue(userRootDir, key, value) {
|
|
19
|
+
const filePath = getCacheFilePath(userRootDir);
|
|
20
|
+
try {
|
|
21
|
+
const cache = await readCache(userRootDir);
|
|
22
|
+
cache[key] = value;
|
|
23
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
24
|
+
await fs.writeFile(filePath, `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// E.g. read-only file system
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function readCache(userRootDir) {
|
|
31
|
+
const filePath = getCacheFilePath(userRootDir);
|
|
32
|
+
try {
|
|
33
|
+
const cache = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
34
|
+
if (isObject(cache))
|
|
35
|
+
return cache;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Missing or invalid cache file
|
|
39
|
+
}
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
// Same location logic as Vite's default `cacheDir` (node_modules/.vite/): the node_modules/ directory of the nearest package.json (searching upwards from the user's root directory), falling back to the user's root directory if there isn't any package.json.
|
|
43
|
+
function getCacheFilePath(userRootDir) {
|
|
44
|
+
userRootDir = toPosixPath(userRootDir);
|
|
45
|
+
const packageJsonPath = findFile('package.json', userRootDir);
|
|
46
|
+
const cacheDir = packageJsonPath
|
|
47
|
+
? path.posix.join(path.posix.dirname(packageJsonPath), 'node_modules', '.vike')
|
|
48
|
+
: path.posix.join(userRootDir, '.vike');
|
|
49
|
+
return path.posix.join(cacheDir, cacheFileName);
|
|
50
|
+
}
|
|
@@ -20,7 +20,7 @@ async function crawlPlusFiles(userRootDir) {
|
|
|
20
20
|
cwd: userRootDir,
|
|
21
21
|
dot: false,
|
|
22
22
|
// Fallback to tinyglobby for users that dynamically generate plus files (and `.gitignore`s them)
|
|
23
|
-
|
|
23
|
+
crawler: { git: true, glob: true },
|
|
24
24
|
});
|
|
25
25
|
// Filter build files
|
|
26
26
|
files = files.filter((filePath) => !isTemporaryBuildFile(filePath));
|
package/dist/types/Config.d.ts
CHANGED
|
@@ -592,17 +592,13 @@ type ConfigBuiltIn = {
|
|
|
592
592
|
};
|
|
593
593
|
type ConfigAi = {
|
|
594
594
|
/**
|
|
595
|
-
* Whether Vike
|
|
596
|
-
*
|
|
597
|
-
* - `true`: add `vike/SKILL.md` to every existing `skills/` directory
|
|
598
|
-
* - `false`: never add `vike/SKILL.md`
|
|
599
|
-
* - `string[]`: add `vike/SKILL.md` to exactly these directories
|
|
595
|
+
* Whether Vike checks that your app has Vike's skill for AI agents (`vike/SKILL.md`), logging a hint upon `$ vike dev` if it's missing or outdated.
|
|
600
596
|
*
|
|
601
597
|
* @default true
|
|
602
598
|
*
|
|
603
599
|
* https://vike.dev/ai#skill
|
|
604
600
|
*/
|
|
605
|
-
skill?: boolean
|
|
601
|
+
skill?: boolean;
|
|
606
602
|
};
|
|
607
603
|
type Vercel = {
|
|
608
604
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PROJECT_VERSION: '0.4.
|
|
1
|
+
export declare const PROJECT_VERSION: '0.4.266';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Automatically updated by @brillout/release-me
|
|
2
|
-
export const PROJECT_VERSION = '0.4.
|
|
2
|
+
export const PROJECT_VERSION = '0.4.266';
|
|
@@ -15,7 +15,15 @@ declare function crawlFiles(options: {
|
|
|
15
15
|
*/
|
|
16
16
|
dot: boolean;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Which crawlers to use.
|
|
19
|
+
* - `git` => `$ git ls-files` (fast, but skips gitignored files)
|
|
20
|
+
* - `glob` => tinyglobby (finds gitignored files)
|
|
21
|
+
*
|
|
22
|
+
* If both are enabled, tinyglobby is only used as a fallback when `$ git ls-files` doesn't find any file.
|
|
23
|
+
* If Git isn't usable (Git isn't installed, or `cwd` isn't inside a Git repository), tinyglobby is always used.
|
|
19
24
|
*/
|
|
20
|
-
|
|
25
|
+
crawler: {
|
|
26
|
+
git: boolean;
|
|
27
|
+
glob: boolean;
|
|
28
|
+
};
|
|
21
29
|
}): Promise<string[]>;
|
package/dist/utils/crawlFiles.js
CHANGED
|
@@ -26,19 +26,20 @@ const globstar = '**/';
|
|
|
26
26
|
* Crawl the files matching `filePattern`, using `$ git ls-files` and, as a fallback, [tinyglobby](https://github.com/SuperchupuDev/tinyglobby).
|
|
27
27
|
*/
|
|
28
28
|
async function crawlFiles(options) {
|
|
29
|
-
const { filePattern, fileExtension, cwd, dot,
|
|
29
|
+
const { filePattern, fileExtension, cwd, dot, crawler } = options;
|
|
30
|
+
assert(crawler.git || crawler.glob);
|
|
30
31
|
const userSettings = getUserSettings();
|
|
31
32
|
const globOptions = { cwd, dot, nocase: false, ignore: getIgnorePatterns(userSettings) };
|
|
32
33
|
// One pattern per file extension (the `filePattern` skips the file extension)
|
|
33
34
|
assert(!path.posix.basename(filePattern).includes('.'));
|
|
34
35
|
const patterns = fileExtension.map((ext) => `${filePattern}.${ext}`);
|
|
35
36
|
// Crawl
|
|
36
|
-
const filesGit = userSettings.git !== false && (await crawlGit(patterns, cwd, globOptions));
|
|
37
|
+
const filesGit = crawler.git && userSettings.git !== false && (await crawlGit(patterns, cwd, globOptions));
|
|
37
38
|
const useGlob =
|
|
38
|
-
// `!filesGit` => Git isn't usable => we *have* to use tinyglobby
|
|
39
|
+
// `!filesGit` => Git isn't used or isn't usable => we *have* to use tinyglobby
|
|
39
40
|
!filesGit ||
|
|
40
|
-
// `filesGit.length === 0` => fallback to tinyglobby if
|
|
41
|
-
(filesGit.length === 0 &&
|
|
41
|
+
// `filesGit.length === 0` => fallback to tinyglobby if enabled
|
|
42
|
+
(filesGit.length === 0 && crawler.glob);
|
|
42
43
|
const filesGlob = (useGlob || debug.isActivated) && (await crawlGlob(patterns, globOptions));
|
|
43
44
|
const files = useGlob ? filesGlob : filesGit;
|
|
44
45
|
assert(files);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vike",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.266",
|
|
4
4
|
"repository": "https://github.com/vikejs/vike",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./server": {
|
|
@@ -137,8 +137,8 @@
|
|
|
137
137
|
"es-module-lexer": "^1.0.0",
|
|
138
138
|
"esbuild": ">=0.19.0",
|
|
139
139
|
"json5": "^2.0.0",
|
|
140
|
-
"magic-string": "^1.2.
|
|
141
|
-
"picomatch": "^4.0.
|
|
140
|
+
"magic-string": "^1.2.3",
|
|
141
|
+
"picomatch": "^4.0.7",
|
|
142
142
|
"semver": "^7.8.5",
|
|
143
143
|
"sirv": "^3.0.2",
|
|
144
144
|
"source-map-support": "^0.5.0",
|
|
@@ -240,6 +240,7 @@
|
|
|
240
240
|
"./bin.js",
|
|
241
241
|
"./AGENTS.md",
|
|
242
242
|
"./dist/",
|
|
243
|
+
"./skills/",
|
|
243
244
|
"./abort.js",
|
|
244
245
|
"./api.js",
|
|
245
246
|
"./client.js",
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
export { autoAddVikeSkill };
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { execFile } from 'node:child_process';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
|
-
import pc from '@brillout/picocolors';
|
|
7
|
-
import { assertInfo, assertUsage } from '../../../../utils/assert.js';
|
|
8
|
-
import { assertKeys } from '../../../../utils/assertKeys.js';
|
|
9
|
-
import { crawlFiles } from '../../../../utils/crawlFiles.js';
|
|
10
|
-
import { getGlobalObject } from '../../../../utils/getGlobalObject.js';
|
|
11
|
-
import { getVikeConfigError } from '../../../../shared-server-node/getVikeConfigError.js';
|
|
12
|
-
import { isArrayOfStrings } from '../../../../utils/isArrayOfStrings.js';
|
|
13
|
-
import { isFilePathAbsoluteFilesystem } from '../../../../utils/isFilePathAbsoluteFilesystem.js';
|
|
14
|
-
import { isObject } from '../../../../utils/isObject.js';
|
|
15
|
-
import { unique } from '../../../../utils/unique.js';
|
|
16
|
-
import { getVikeConfigInternal } from '../../shared/resolveVikeConfigInternal.js';
|
|
17
|
-
import '../../assertEnvVite.js';
|
|
18
|
-
const execFileA = promisify(execFile);
|
|
19
|
-
const importMetaUrl = import.meta.url;
|
|
20
|
-
const globalObject = getGlobalObject('autoAddVikeSkill.ts', {
|
|
21
|
-
alreadyDone: false,
|
|
22
|
-
});
|
|
23
|
-
const skillPathInsideSkillsDir = 'vike/SKILL.md';
|
|
24
|
-
const skillFileContent = `---
|
|
25
|
-
name: "vike"
|
|
26
|
-
description: "Vike documentation index — a compact overview of Vike's docs. Consider consulting it, e.g. when using uncommon Vike APIs or when stuck on a Vike problem."
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
See https://vike.dev/llms.txt
|
|
30
|
-
`;
|
|
31
|
-
const commitMessage = (isUpdate) => `${isUpdate ? 'Update' : 'Add'} Vike skill — ${pc.underline('https://vike.dev/ai#skill')}`;
|
|
32
|
-
// Automatically add vike/SKILL.md to skills/ directories (e.g. .claude/skills/ and .agents/skills/) of the user's Git repository (and Git-commit it) — so that AI agents (Claude Code, Codex, Cursor, Gemini CLI, ...) automatically pick it up.
|
|
33
|
-
// https://vike.dev/ai#skill
|
|
34
|
-
async function autoAddVikeSkill(userRootDir) {
|
|
35
|
-
try {
|
|
36
|
-
await autoAddVikeSkillUnsafe(userRootDir);
|
|
37
|
-
}
|
|
38
|
-
catch (err) {
|
|
39
|
-
// Show the error without breaking the dev server. (Expected situations don't throw — e.g. Git missing is handled gracefully.)
|
|
40
|
-
console.error(err);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function autoAddVikeSkillUnsafe(userRootDir) {
|
|
44
|
-
if (globalObject.alreadyDone)
|
|
45
|
-
return;
|
|
46
|
-
// Skip CI environments: the skill file is meant to be added from the machine of an app developer.
|
|
47
|
-
if (process.env.CI)
|
|
48
|
-
return;
|
|
49
|
-
// Skip if Vike isn't inside node_modules/ (e.g. when Vike is linked, such as when running an example of the Vike monorepo).
|
|
50
|
-
if (!importMetaUrl.includes('node_modules/'))
|
|
51
|
-
return;
|
|
52
|
-
const vikeConfig = await getVikeConfigInternal();
|
|
53
|
-
// Maybe the user disabled the feature in a config file that currently has an error => retry later (Vite restarts upon config changes).
|
|
54
|
-
if (getVikeConfigError())
|
|
55
|
-
return;
|
|
56
|
-
const configValue = getConfigValueAiSkill(vikeConfig);
|
|
57
|
-
if (configValue === false)
|
|
58
|
-
return;
|
|
59
|
-
globalObject.alreadyDone = true;
|
|
60
|
-
const res = await addAiSkill(userRootDir, {
|
|
61
|
-
// true (default) => add the skill file to every existing skills directory (`**/skills/*/SKILL.md`)
|
|
62
|
-
skillsDirs: configValue === true ? undefined : configValue,
|
|
63
|
-
});
|
|
64
|
-
if (!res)
|
|
65
|
-
return;
|
|
66
|
-
assertInfo(false, `${res.isUpdate ? 'Updated' : 'Created'}${res.isCommitted ? ' and Git-committed' : ''} ${res.files
|
|
67
|
-
.map((f) => pc.cyan(f.filePathRelative))
|
|
68
|
-
.join(', ')} — ${pc.underline('https://vike.dev/ai#skill')}`, { onlyOnce: false });
|
|
69
|
-
}
|
|
70
|
-
// https://vike.dev/ai#skill
|
|
71
|
-
function getConfigValueAiSkill(vikeConfig) {
|
|
72
|
-
const configAi = vikeConfig.config.ai;
|
|
73
|
-
if (configAi === undefined)
|
|
74
|
-
return true;
|
|
75
|
-
assertUsage(isObject(configAi), `Setting ${pc.cyan('ai')} should be an object`);
|
|
76
|
-
assertKeys(configAi, ['skill'], `Setting ${pc.cyan('ai')}:`);
|
|
77
|
-
const skill = configAi.skill;
|
|
78
|
-
if (skill === undefined || skill === true)
|
|
79
|
-
return true;
|
|
80
|
-
if (skill === false)
|
|
81
|
-
return false;
|
|
82
|
-
assertUsage(isArrayOfStrings(skill), `Setting ${pc.cyan('ai.skill')} should be a boolean or a list of skills directories (e.g. ${pc.cyan("['.claude/skills', '.agents/skills']")})`);
|
|
83
|
-
return skill.map((skillsDir) => {
|
|
84
|
-
assertUsage(skillsDir !== '' && !isFilePathAbsoluteFilesystem(skillsDir), `Setting ${pc.cyan('ai.skill')} entries should be paths relative to the root directory of your app's Git repository (e.g. ${pc.cyan("'.claude/skills'")})`);
|
|
85
|
-
// Normalize: remove trailing slashes
|
|
86
|
-
return skillsDir.replace(/\/+$/, '');
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
async function addAiSkill(userRootDir, { skillsDirs }) {
|
|
90
|
-
// Skip if Git isn't installed
|
|
91
|
-
if ('err' in (await runGitCommand(['--version'], userRootDir)))
|
|
92
|
-
return null;
|
|
93
|
-
// Skip if the app isn't inside a Git repository
|
|
94
|
-
const resGitRootDir = await runGitCommand(['rev-parse', '--show-toplevel'], userRootDir);
|
|
95
|
-
if ('err' in resGitRootDir)
|
|
96
|
-
return null;
|
|
97
|
-
const gitRootDir = resGitRootDir.stdout.trim();
|
|
98
|
-
if (!gitRootDir)
|
|
99
|
-
return null;
|
|
100
|
-
// By default, the skill file is only added to the skills directories that already exist — we don't want to add files to the repositories of users who don't use skills.
|
|
101
|
-
skillsDirs ?? (skillsDirs = await discoverSkillsDirs(gitRootDir));
|
|
102
|
-
if (skillsDirs.length === 0)
|
|
103
|
-
return null;
|
|
104
|
-
const files = [];
|
|
105
|
-
for (const skillsDir of skillsDirs) {
|
|
106
|
-
const filePathRelative = `${skillsDir}/${skillPathInsideSkillsDir}`;
|
|
107
|
-
const filePathAbsolute = path.join(gitRootDir, ...filePathRelative.split('/'));
|
|
108
|
-
// Skip if the skill file is already up-to-date
|
|
109
|
-
const contentCurrent = await fs.readFile(filePathAbsolute, 'utf8').catch(() => null);
|
|
110
|
-
if (contentCurrent === skillFileContent)
|
|
111
|
-
continue;
|
|
112
|
-
const isUpdate = contentCurrent !== null;
|
|
113
|
-
await fs.mkdir(path.dirname(filePathAbsolute), { recursive: true });
|
|
114
|
-
await fs.writeFile(filePathAbsolute, skillFileContent, 'utf8');
|
|
115
|
-
files.push({ filePathAbsolute, filePathRelative, isUpdate });
|
|
116
|
-
}
|
|
117
|
-
if (files.length === 0)
|
|
118
|
-
return null;
|
|
119
|
-
const isCommitted = await gitCommit(gitRootDir, files);
|
|
120
|
-
return { files, isUpdate: files.some((f) => f.isUpdate), isCommitted };
|
|
121
|
-
}
|
|
122
|
-
// Discover the skills directories of the user's Git repository, following the Agent Skills convention `**/skills/*/SKILL.md` (https://agentskills.io) — e.g. .claude/skills/ (Claude Code) and .agents/skills/ (Codex, Gemini CLI, Cursor, ...).
|
|
123
|
-
async function discoverSkillsDirs(gitRootDir) {
|
|
124
|
-
const files = await crawlFiles({
|
|
125
|
-
filePattern: '**/skills/*/SKILL',
|
|
126
|
-
fileExtension: ['md'],
|
|
127
|
-
cwd: gitRootDir,
|
|
128
|
-
// Skills directories usually live inside dot directories (e.g. .claude/ and .agents/)
|
|
129
|
-
dot: true,
|
|
130
|
-
// Most apps don't contain any skills directory — when Git doesn't know about any skill file, we don't want to crawl the app's entire directory tree upon every dev start.
|
|
131
|
-
globFallback: false,
|
|
132
|
-
});
|
|
133
|
-
const skillsDirs = unique(files.map((filePath) => path.posix.dirname(path.posix.dirname(filePath)))).sort();
|
|
134
|
-
return skillsDirs;
|
|
135
|
-
}
|
|
136
|
-
async function gitCommit(gitRootDir, files) {
|
|
137
|
-
// Don't Git-commit the skill files that the user chose to .gitignore — `$ git check-ignore` succeeds if the file is ignored
|
|
138
|
-
const filesToCommit = [];
|
|
139
|
-
for (const file of files) {
|
|
140
|
-
const resCheckIgnore = await runGitCommand(['check-ignore', '--quiet', '--', file.filePathRelative], gitRootDir);
|
|
141
|
-
if ('err' in resCheckIgnore)
|
|
142
|
-
filesToCommit.push(file);
|
|
143
|
-
}
|
|
144
|
-
if (filesToCommit.length === 0)
|
|
145
|
-
return false;
|
|
146
|
-
const filePaths = filesToCommit.map((f) => f.filePathRelative);
|
|
147
|
-
const resAdd = await runGitCommand(['add', '--', ...filePaths], gitRootDir);
|
|
148
|
-
if ('err' in resAdd)
|
|
149
|
-
return false;
|
|
150
|
-
const isUpdate = filesToCommit.some((f) => f.isUpdate);
|
|
151
|
-
const resCommit = await runGitCommand([
|
|
152
|
-
'-c',
|
|
153
|
-
'user.name=Vike',
|
|
154
|
-
'-c',
|
|
155
|
-
'user.email=no-reply@vike.dev',
|
|
156
|
-
'commit',
|
|
157
|
-
// Skip Git hooks (e.g. slow or failing pre-commit hooks)
|
|
158
|
-
'--no-verify',
|
|
159
|
-
'--message',
|
|
160
|
-
commitMessage(isUpdate),
|
|
161
|
-
// Only commit the skill files — never commit files staged by the user
|
|
162
|
-
'--',
|
|
163
|
-
...filePaths,
|
|
164
|
-
], gitRootDir);
|
|
165
|
-
return !('err' in resCommit);
|
|
166
|
-
}
|
|
167
|
-
// Run a Git command — doesn't throw: it returns `{ err }` upon failure.
|
|
168
|
-
async function runGitCommand(args, cwd) {
|
|
169
|
-
let stdout;
|
|
170
|
-
try {
|
|
171
|
-
const res = await execFileA('git', args, { cwd });
|
|
172
|
-
stdout = res.stdout.toString();
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
return { err };
|
|
176
|
-
}
|
|
177
|
-
return { stdout };
|
|
178
|
-
}
|