vike 0.4.264 → 0.4.265

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.
@@ -0,0 +1,4 @@
1
+ export { logSkillHint };
2
+ import type { ViteDevServer } from 'vite';
3
+ import '../../assertEnvVite.js';
4
+ declare function logSkillHint(server: ViteDevServer, userRootDir: string): void;
@@ -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 tellAgent = `by telling your agent "${pc.cyan(`Install skill ${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, ...) ${tellAgent}, 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'} ${tellAgent}, 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('https://vike.dev/ai#skill')}`);
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
- // import { autoAddVikeSkill } from './pluginDev/autoAddVikeSkill.js'
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
- handler() {
44
- // Apply late — after the dev server is up and running — so that it doesn't slow down dev start
45
- setTimeoutUnref(() => autoAddVikeSkill(config.root), 15 * 1000)
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,5 @@
1
+ export { getCacheValue };
2
+ export { setCacheValue };
3
+ import '../assertEnvVite.js';
4
+ declare function getCacheValue(userRootDir: string, key: string): Promise<unknown>;
5
+ declare function setCacheValue(userRootDir: string, key: string, value: unknown): Promise<void>;
@@ -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
- globFallback: true,
23
+ crawler: { git: true, glob: true },
24
24
  });
25
25
  // Filter build files
26
26
  files = files.filter((filePath) => !isTemporaryBuildFile(filePath));
@@ -592,17 +592,13 @@ type ConfigBuiltIn = {
592
592
  };
593
593
  type ConfigAi = {
594
594
  /**
595
- * Whether Vike automatically adds and updates `vike/SKILL.md` inside skills directories (e.g. `.agents/skills/` and `.claude/skills/`).
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 | string[];
601
+ skill?: boolean;
606
602
  };
607
603
  type Vercel = {
608
604
  /**
@@ -1 +1 @@
1
- export declare const PROJECT_VERSION: '0.4.264';
1
+ export declare const PROJECT_VERSION: '0.4.265';
@@ -1,2 +1,2 @@
1
1
  // Automatically updated by @brillout/release-me
2
- export const PROJECT_VERSION = '0.4.264';
2
+ export const PROJECT_VERSION = '0.4.265';
@@ -15,7 +15,15 @@ declare function crawlFiles(options: {
15
15
  */
16
16
  dot: boolean;
17
17
  /**
18
- * Whether to fallback to tinyglobby if `$ git ls-files` doesn't find any file.
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
- globFallback: boolean;
25
+ crawler: {
26
+ git: boolean;
27
+ glob: boolean;
28
+ };
21
29
  }): Promise<string[]>;
@@ -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, globFallback } = options;
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 globFallback is true
41
- (filesGit.length === 0 && globFallback);
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.264",
3
+ "version": "0.4.265",
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.2",
141
- "picomatch": "^4.0.5",
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",
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: "vike"
3
+ 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."
4
+ ---
5
+
6
+ See https://vike.dev/llms.txt
@@ -1,3 +0,0 @@
1
- export { autoAddVikeSkill };
2
- import '../../assertEnvVite.js';
3
- declare function autoAddVikeSkill(userRootDir: string): Promise<void>;
@@ -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
- }