@antomanc/aginit 0.3.0 → 0.4.1

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -3
  3. package/dist/adapters/agents-md.d.ts +12 -0
  4. package/dist/adapters/agents-md.d.ts.map +1 -1
  5. package/dist/adapters/agents-md.js +25 -1
  6. package/dist/adapters/agents-md.js.map +1 -1
  7. package/dist/adapters/graft.d.ts +1 -0
  8. package/dist/adapters/graft.d.ts.map +1 -1
  9. package/dist/adapters/graft.js +5 -3
  10. package/dist/adapters/graft.js.map +1 -1
  11. package/dist/adapters/skills.d.ts +20 -1
  12. package/dist/adapters/skills.d.ts.map +1 -1
  13. package/dist/adapters/skills.js +38 -1
  14. package/dist/adapters/skills.js.map +1 -1
  15. package/dist/adapters/tool-ignores.d.ts +9 -0
  16. package/dist/adapters/tool-ignores.d.ts.map +1 -0
  17. package/dist/adapters/tool-ignores.js +121 -0
  18. package/dist/adapters/tool-ignores.js.map +1 -0
  19. package/dist/cli/doctor.d.ts.map +1 -1
  20. package/dist/cli/doctor.js +59 -2
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/main.d.ts.map +1 -1
  23. package/dist/cli/main.js +13 -3
  24. package/dist/cli/main.js.map +1 -1
  25. package/dist/cli/new.d.ts.map +1 -1
  26. package/dist/cli/new.js +17 -4
  27. package/dist/cli/new.js.map +1 -1
  28. package/dist/config/defaults.d.ts.map +1 -1
  29. package/dist/config/defaults.js +6 -3
  30. package/dist/config/defaults.js.map +1 -1
  31. package/dist/config/schema.d.ts +5 -0
  32. package/dist/config/schema.d.ts.map +1 -1
  33. package/dist/config/schema.js.map +1 -1
  34. package/dist/config/validation.d.ts.map +1 -1
  35. package/dist/config/validation.js +2 -0
  36. package/dist/config/validation.js.map +1 -1
  37. package/dist/presets/web.d.ts.map +1 -1
  38. package/dist/presets/web.js +34 -3
  39. package/dist/presets/web.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/adapters/agents-md.ts +30 -1
  42. package/src/adapters/graft.ts +6 -4
  43. package/src/adapters/skills.ts +46 -2
  44. package/src/adapters/tool-ignores.ts +158 -0
  45. package/src/cli/doctor.ts +72 -2
  46. package/src/cli/main.ts +12 -3
  47. package/src/cli/new.ts +14 -4
  48. package/src/config/defaults.ts +6 -3
  49. package/src/config/schema.ts +5 -0
  50. package/src/config/validation.ts +2 -0
  51. package/src/presets/web.ts +35 -3
@@ -0,0 +1,158 @@
1
+ import path from 'node:path';
2
+ import { fileExists, readTextFile, safeWriteFile } from '../utils/fs.js';
3
+ import { readPackageManifest } from '../config/validation.js';
4
+
5
+ // Vendored skill sources ship third-party JS and Markdown that the project's own
6
+ // linters and formatters must not check. No ignore convention is shared across tools,
7
+ // so each tool below says where its ignores live and how to add to them.
8
+ const VENDORED_DIRS = ['.agents', '.claude'];
9
+
10
+ interface Project {
11
+ manifest: Record<string, any> | null;
12
+ has(dependency: string): boolean;
13
+ exists(file: string): boolean;
14
+ }
15
+
16
+ interface ToolIgnore {
17
+ /** The file holding this tool's ignores, when the project uses the tool. */
18
+ file(project: Project): string | undefined;
19
+ /** Vendored directories the file does not ignore yet. */
20
+ missing(text: string): string[];
21
+ /** The file with those directories ignored, or null when it cannot be edited safely. */
22
+ patch(text: string, dirs: string[]): string | null;
23
+ advice(file: string, dirs: string[]): string;
24
+ }
25
+
26
+ const configs = (base: string, extensions: string[]) => extensions.map((ext) => `${base}${ext}`);
27
+
28
+ /** Gitignore-syntax files: append the directories. */
29
+ function ignoreFile(name: string, used: (project: Project) => boolean): ToolIgnore {
30
+ return {
31
+ file: (project) => (used(project) ? name : undefined),
32
+ missing: (text) => {
33
+ const lines = text.split(/\r?\n/).map((line) => line.trim().replace(/^\/|\/(\*\*)?$/g, ''));
34
+ return VENDORED_DIRS.filter((dir) => !lines.includes(dir));
35
+ },
36
+ patch: (text, dirs) => {
37
+ const block = `# Vendored agent skills (aginit)\n${dirs.map((dir) => `${dir}/`).join('\n')}\n`;
38
+ return text.trim() ? `${text.trimEnd()}\n\n${block}` : block;
39
+ },
40
+ advice: (file, dirs) => `add ${dirs.map((dir) => `${dir}/`).join(' and ')} to ${file}`
41
+ };
42
+ }
43
+
44
+ const prettier = ignoreFile(
45
+ '.prettierignore',
46
+ (project) =>
47
+ project.has('prettier') ||
48
+ Boolean(project.manifest?.prettier) ||
49
+ [
50
+ ...configs('.prettierrc', ['', '.json', '.json5', '.yaml', '.yml', '.toml']),
51
+ ...configs('.prettierrc', ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts']),
52
+ ...configs('prettier.config', ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts'])
53
+ ].some(project.exists)
54
+ );
55
+
56
+ // Legacy ESLint and oxlint read .eslintignore; flat-config ESLint does not.
57
+ const eslintIgnore = ignoreFile(
58
+ '.eslintignore',
59
+ (project) =>
60
+ project.has('oxlint') ||
61
+ Boolean(project.manifest?.eslintConfig) ||
62
+ configs('.eslintrc', ['', '.js', '.cjs', '.json', '.yaml', '.yml']).some(project.exists)
63
+ );
64
+
65
+ /** Flat ESLint config: extend a global ignores array if there is an unambiguous one,
66
+ * `globalIgnores([...])` or an object holding only `ignores`. */
67
+ const eslintFlat: ToolIgnore = {
68
+ file: (project) =>
69
+ configs('eslint.config', ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']).find(project.exists),
70
+ missing: (text) => VENDORED_DIRS.filter((dir) => !text.includes(`${dir}/`)),
71
+ patch: (text, dirs) => {
72
+ const match =
73
+ /globalIgnores\(\s*\[/.exec(text) ??
74
+ /\{\s*ignores\s*:\s*\[(?=[^[\]]*\]\s*,?\s*\})/.exec(text);
75
+ if (!match) return null;
76
+ const at = match.index + match[0].length;
77
+ const rest = text.slice(at);
78
+ const quote =
79
+ /^\s*(['"`])/.exec(rest)?.[1] ??
80
+ ((text.match(/"/g)?.length ?? 0) > (text.match(/'/g)?.length ?? 0) ? '"' : "'");
81
+ const entries = dirs.map((dir) => `${quote}${dir}/**${quote}`);
82
+ const indent = /^(\s*\n[ \t]*)/.exec(rest)?.[1];
83
+ const insertion = indent
84
+ ? entries.map((entry) => `${indent}${entry},`).join('')
85
+ : entries.join(', ') + (/^\s*\]/.test(rest) ? '' : ', ');
86
+ return text.slice(0, at) + insertion + rest;
87
+ },
88
+ advice: (file, dirs) =>
89
+ `add { ignores: [${dirs.map((dir) => `'${dir}/**'`).join(', ')}] } to the exported array in ${file}`
90
+ };
91
+
92
+ /** Biome 2.2+: force-ignore the folders through files.includes. */
93
+ const biome: ToolIgnore = {
94
+ file: (project) => ['biome.json', 'biome.jsonc'].find(project.exists),
95
+ missing: (text) => VENDORED_DIRS.filter((dir) => !text.includes(`**/${dir}`)),
96
+ patch: (text, dirs) => {
97
+ let config: Record<string, any>;
98
+ try {
99
+ config = JSON.parse(text); // a biome.jsonc with comments gets advice instead
100
+ } catch {
101
+ return null;
102
+ }
103
+ const includes = config.files?.includes ?? ['**'];
104
+ if (!Array.isArray(includes)) return null;
105
+ config.files = {
106
+ ...config.files,
107
+ includes: [...includes, ...dirs.map((dir) => `!!**/${dir}`)]
108
+ };
109
+ const json = JSON.stringify(config, null, /\n([ \t]+)"/.exec(text)?.[1] ?? 2) + '\n';
110
+ // Like Biome's formatter, keep the array on one line when it fits in 80 columns.
111
+ return json.replace(/^([ \t]*"includes": )\[[^\]]*\]/m, (block, key: string) => {
112
+ const inline = `${key}[${config.files.includes.map((p: string) => JSON.stringify(p)).join(', ')}]`;
113
+ return inline.replace(/\t/g, ' ').length <= 80 ? inline : block;
114
+ });
115
+ },
116
+ advice: (file, dirs) =>
117
+ `add ${dirs.map((dir) => `"!!**/${dir}"`).join(', ')} to files.includes in ${file}`
118
+ };
119
+
120
+ const TOOLS = [prettier, eslintIgnore, eslintFlat, biome];
121
+
122
+ function pendingIgnores(targetDir: string) {
123
+ const manifest = readPackageManifest(targetDir);
124
+ const project: Project = {
125
+ manifest,
126
+ has: (name) => Boolean(manifest?.dependencies?.[name] || manifest?.devDependencies?.[name]),
127
+ exists: (file) => fileExists(path.join(targetDir, file))
128
+ };
129
+ return TOOLS.flatMap((tool) => {
130
+ const file = tool.file(project);
131
+ if (!file) return [];
132
+ const text = readTextFile(path.join(targetDir, file)) ?? '';
133
+ const missing = tool.missing(text);
134
+ return missing.length > 0 ? [{ tool, file, text, missing }] : [];
135
+ });
136
+ }
137
+
138
+ /** Keeps the project's linters and formatters off vendored skills. Returns the
139
+ * advice for configs that could not be edited safely. */
140
+ export function ignoreVendoredSkills(
141
+ targetDir: string,
142
+ options: { dryRun?: boolean; silent?: boolean } = {}
143
+ ): string[] {
144
+ const { dryRun = false, silent = false } = options;
145
+ const advice: string[] = [];
146
+ for (const { tool, file, text, missing } of pendingIgnores(targetDir)) {
147
+ const patched = tool.patch(text, missing);
148
+ if (patched === null)
149
+ advice.push(`Linters check vendored skills: ${tool.advice(file, missing)}`);
150
+ else safeWriteFile(path.join(targetDir, file), patched, { overwrite: true, dryRun, silent });
151
+ }
152
+ return advice;
153
+ }
154
+
155
+ /** Doctor view: every config that still lets a linter or formatter see vendored skills. */
156
+ export function vendoredSkillsExposure(targetDir: string): string[] {
157
+ return pendingIgnores(targetDir).map(({ tool, file, missing }) => tool.advice(file, missing));
158
+ }
package/src/cli/doctor.ts CHANGED
@@ -9,7 +9,13 @@ import {
9
9
  PackageManager
10
10
  } from '../config/schema.js';
11
11
  import { detectPackageManager } from '../adapters/package-manager.js';
12
- import { getInstalledSkills } from '../adapters/skills.js';
12
+ import fs from 'node:fs';
13
+ import {
14
+ claudeCodeEnabled,
15
+ getInstalledSkills,
16
+ getSkillsMissingForClaude
17
+ } from '../adapters/skills.js';
18
+ import { claudeMarkdownImportsAgents } from '../adapters/agents-md.js';
13
19
  import {
14
20
  agentBrowserConfigured,
15
21
  agentBrowserStubInstalled,
@@ -17,6 +23,7 @@ import {
17
23
  runAgentBrowserDiagnostics
18
24
  } from '../adapters/agent-browser.js';
19
25
  import { readProjectConfig } from '../config/validation.js';
26
+ import { vendoredSkillsExposure } from '../adapters/tool-ignores.js';
20
27
  import { logger } from '../utils/logger.js';
21
28
 
22
29
  interface CheckItem {
@@ -169,7 +176,7 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
169
176
  category: 'Project Config',
170
177
  ok: !!projectConfig,
171
178
  detail: projectConfig
172
- ? `Preset: "${projectConfig.preset}" (v${projectConfig.schemaVersion || '1.0.0'}), pm: ${activePm}, agents: ${projectConfig.agents.primary}/${projectConfig.agents.secondary}`
179
+ ? `Preset: "${projectConfig.preset}" (v${projectConfig.schemaVersion || '1.0.0'}), pm: ${activePm}, agents: ${projectConfig.agents.primary}/${projectConfig.agents.secondary}${claudeCodeEnabled(projectConfig) ? '/claude-code' : ''}`
173
180
  : configError
174
181
  ? `Invalid configuration: ${configError}`
175
182
  : 'No aginit.config.json found in current directory',
@@ -220,6 +227,69 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
220
227
  : 'Run `aginit init` or `npx skills add ...` to install skills'
221
228
  });
222
229
 
230
+ // 9a. Vendored skills must stay out of the project's linters and formatters
231
+ if (installedSkills.length > 0) {
232
+ const exposure = vendoredSkillsExposure(targetDir);
233
+ checks.push({
234
+ name: 'Lint/format ignores for vendored skills',
235
+ category: 'Project Config',
236
+ ok: exposure.length === 0,
237
+ detail:
238
+ exposure.length === 0
239
+ ? 'Detected linters and formatters skip .agents/ and .claude/'
240
+ : 'Project linters or formatters will check vendored skill files',
241
+ suggestion:
242
+ exposure.length === 0 ? undefined : `Run \`aginit update\`, or ${exposure.join('; ')}`
243
+ });
244
+ }
245
+
246
+ // 9b. Claude Code: skills linked into .claude/skills/ and AGENTS.md reachable
247
+ if (projectConfig !== null && claudeCodeEnabled(projectConfig)) {
248
+ if (!skillsDisabled && installedSkills.length > 0) {
249
+ const missing = getSkillsMissingForClaude(targetDir);
250
+ checks.push({
251
+ name: 'Claude Code Skills (.claude/skills/)',
252
+ category: 'Project Config',
253
+ ok: missing.length === 0,
254
+ detail:
255
+ missing.length === 0
256
+ ? `All ${installedSkills.length} skills visible to Claude Code`
257
+ : `${missing.length} skills not linked for Claude Code: ${missing.join(', ')}`,
258
+ suggestion:
259
+ missing.length === 0
260
+ ? undefined
261
+ : 'Run `aginit update` or `aginit init` to link skills into .claude/skills/ (or set agents.claudeCode: false)'
262
+ });
263
+ }
264
+
265
+ const claudePath = path.join(targetDir, 'CLAUDE.md');
266
+ const claudeExists = fileExists(claudePath);
267
+ // A CLAUDE.md symlinked to AGENTS.md is as good as the import.
268
+ const claudeIsAgentsLink =
269
+ claudeExists &&
270
+ agentsExists &&
271
+ fs.realpathSync(claudePath) === fs.realpathSync(agentsPath);
272
+ const claudeOk =
273
+ !claudeExists ||
274
+ claudeIsAgentsLink ||
275
+ claudeMarkdownImportsAgents(readTextFile(claudePath) || '');
276
+ checks.push({
277
+ name: 'CLAUDE.md → AGENTS.md',
278
+ category: 'Project Config',
279
+ ok: claudeExists && claudeOk,
280
+ detail: !claudeExists
281
+ ? 'No CLAUDE.md; only Claude Code v2.1.277+ reads AGENTS.md on its own'
282
+ : claudeOk
283
+ ? 'CLAUDE.md imports AGENTS.md'
284
+ : 'CLAUDE.md exists without `@AGENTS.md`, so Claude Code ignores AGENTS.md',
285
+ suggestion: !claudeExists
286
+ ? 'Run `aginit update` or `aginit init` to add a CLAUDE.md that imports AGENTS.md'
287
+ : claudeOk
288
+ ? undefined
289
+ : 'Add a line `@AGENTS.md` to CLAUDE.md'
290
+ });
291
+ }
292
+
223
293
  // 10. agent-browser runtime (only when the project ships the skill)
224
294
  const agentBrowserWanted =
225
295
  projectConfig !== null &&
package/src/cli/main.ts CHANGED
@@ -3,7 +3,9 @@ import { readFileSync } from 'node:fs';
3
3
  import { createProject } from './new.js';
4
4
  import { initCurrentDirectory } from './init.js';
5
5
  import { runDoctor } from './doctor.js';
6
- import { updateSkills } from '../adapters/skills.js';
6
+ import { claudeCodeEnabled, ensureClaudeSkillLinks, updateSkills } from '../adapters/skills.js';
7
+ import { ignoreVendoredSkills } from '../adapters/tool-ignores.js';
8
+ import { setupClaudeMarkdown } from '../adapters/agents-md.js';
7
9
  import { buildGraft } from '../adapters/graft.js';
8
10
  import { readProjectConfig } from '../config/validation.js';
9
11
  import { logger } from '../utils/logger.js';
@@ -161,9 +163,16 @@ export function createCliProgram(): Command {
161
163
  const targetDir = process.cwd();
162
164
  const config = readProjectConfig(targetDir);
163
165
  const skillsOk =
164
- config?.skills.sources.length === 0
166
+ (config?.skills.sources.length === 0
165
167
  ? true
166
- : await updateSkills(targetDir, { dryRun: options.dryRun });
168
+ : await updateSkills(targetDir, { dryRun: options.dryRun })) &&
169
+ (config ? await ensureClaudeSkillLinks(targetDir, config, { dryRun: options.dryRun }) : true);
170
+ if (config && claudeCodeEnabled(config))
171
+ setupClaudeMarkdown(targetDir, { dryRun: options.dryRun });
172
+ if (config?.skills.sources.length !== 0) {
173
+ const advice = ignoreVendoredSkills(targetDir, { dryRun: options.dryRun, silent: true });
174
+ for (const line of advice) logger.warn(line);
175
+ }
167
176
  const graftOk =
168
177
  config?.codebase.graft === false
169
178
  ? true
package/src/cli/new.ts CHANGED
@@ -9,9 +9,10 @@ import {
9
9
  } from '../config/schema.js';
10
10
  import { getPreset } from '../presets/registry.js';
11
11
  import { setupGit } from '../adapters/git.js';
12
- import { setupAgentsMarkdown } from '../adapters/agents-md.js';
13
- import { installAllSkills } from '../adapters/skills.js';
12
+ import { setupAgentsMarkdown, setupClaudeMarkdown } from '../adapters/agents-md.js';
13
+ import { claudeCodeEnabled, installAllSkills, skillAgents } from '../adapters/skills.js';
14
14
  import { initGraft } from '../adapters/graft.js';
15
+ import { ignoreVendoredSkills } from '../adapters/tool-ignores.js';
15
16
  import {
16
17
  agentBrowserConfigured,
17
18
  agentBrowserStubInstalled,
@@ -137,13 +138,14 @@ export async function createProject(
137
138
  // Step 4: Write or update AGENTS.md intake contract
138
139
  if (!silent) logger.step(currentStep++, totalSteps, 'Generating AGENTS.md intake guidance');
139
140
  setupAgentsMarkdown(targetDir, config, { dryRun, silent });
141
+ if (claudeCodeEnabled(config)) setupClaudeMarkdown(targetDir, { dryRun, silent });
140
142
  if (!silent) logger.success('Configured AGENTS.md');
141
143
 
142
144
  // Step 5: Install skills via skills.sh engine
143
145
  if (!silent) logger.step(currentStep++, totalSteps, 'Installing upstream skills via skills.sh');
144
146
  if (options.skills !== false && config.skills.sources.length > 0) {
145
147
  const { installed, failed } = await installAllSkills(targetDir, config.skills.sources, {
146
- agents: [config.agents.primary, config.agents.secondary],
148
+ agents: skillAgents(config),
147
149
  dryRun,
148
150
  silent
149
151
  });
@@ -159,6 +161,10 @@ export async function createProject(
159
161
  } else if (!silent) {
160
162
  logger.dim('Skills installation skipped (--no-skills or empty configuration)');
161
163
  }
164
+ if ((options.skills !== false && config.skills.sources.length > 0) || agentBrowserWanted) {
165
+ const advice = ignoreVendoredSkills(targetDir, { dryRun, silent: true });
166
+ if (!silent) for (const line of advice) logger.warn(line);
167
+ }
162
168
 
163
169
  // Provision the agent-browser runtime the skill stub drives
164
170
  if (agentBrowserWanted) {
@@ -171,7 +177,11 @@ export async function createProject(
171
177
  // Codebase intelligence (Graft)
172
178
  if (!silent) logger.step(currentStep++, totalSteps, 'Configuring codebase intelligence');
173
179
  if (config.codebase.graft && options.graft !== false) {
174
- const graftOk = await initGraft(targetDir, { dryRun, silent });
180
+ const graftOk = await initGraft(targetDir, {
181
+ dryRun,
182
+ silent,
183
+ claude: claudeCodeEnabled(config)
184
+ });
175
185
  complete &&= graftOk;
176
186
  if (graftOk && !silent) {
177
187
  logger.success('Graft context graph initialized');
@@ -70,7 +70,8 @@ export function getDefaultConfig(
70
70
  packageManager,
71
71
  agents: {
72
72
  primary: 'antigravity',
73
- secondary: 'codex'
73
+ secondary: 'codex',
74
+ claudeCode: true
74
75
  },
75
76
  skills: {
76
77
  sources: [
@@ -120,7 +121,8 @@ export function getDefaultConfig(
120
121
  packageManager,
121
122
  agents: {
122
123
  primary: 'antigravity',
123
- secondary: 'codex'
124
+ secondary: 'codex',
125
+ claudeCode: true
124
126
  },
125
127
  skills: {
126
128
  sources: [
@@ -162,7 +164,8 @@ export function getDefaultConfig(
162
164
  packageManager,
163
165
  agents: {
164
166
  primary: 'antigravity',
165
- secondary: 'codex'
167
+ secondary: 'codex',
168
+ claudeCode: true
166
169
  },
167
170
  skills: {
168
171
  sources: [
@@ -16,6 +16,11 @@ export interface AginitConfig {
16
16
  agents: {
17
17
  primary: string;
18
18
  secondary: string;
19
+ /**
20
+ * Also install skills, Graft wiring and a CLAUDE.md import for Claude Code.
21
+ * Missing in configs written before 0.4.0; treated as enabled.
22
+ */
23
+ claudeCode?: boolean;
19
24
  };
20
25
  skills: {
21
26
  /**
@@ -45,6 +45,8 @@ export function validateConfig(value: unknown): AginitConfig {
45
45
  if (!isRecord(value.agents)) fail('agents');
46
46
  for (const key of ['primary', 'secondary'])
47
47
  validateUpstreamArgument(value.agents[key], `agent ${key}`);
48
+ if (value.agents.claudeCode !== undefined && typeof value.agents.claudeCode !== 'boolean')
49
+ fail('agents.claudeCode');
48
50
  if (!isRecord(value.skills) || !Array.isArray(value.skills.sources)) fail('skills.sources');
49
51
  if (value.skills.workflow !== undefined && !['minimal', 'spec'].includes(value.skills.workflow))
50
52
  fail('skills.workflow');
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { fileExists, safeWriteFile, writeJsonFile } from '../utils/fs.js';
2
+ import { fileExists, readTextFile, safeWriteFile, writeJsonFile } from '../utils/fs.js';
3
3
  import { readPackageManifest } from '../config/validation.js';
4
4
  import { runExecutable } from '../utils/shell.js';
5
5
  import { PresetContext, PresetHandler } from './types.js';
@@ -8,6 +8,34 @@ import { setupAdr } from './adr.js';
8
8
  import { scaffoldCommand } from '../adapters/package-manager.js';
9
9
  import { addTestingDependencies, setupTesting } from './testing.js';
10
10
 
11
+ /** Plain tsc cannot resolve framework virtual modules or single-file components,
12
+ * so those frameworks get their own checker (and the package that provides it). */
13
+ function typecheckFor(
14
+ pkg: Record<string, any>,
15
+ targetDir: string,
16
+ freshNext: boolean
17
+ ): { script: string; checker?: [string, string] } {
18
+ const has = (name: string) => Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
19
+ if (freshNext) return { script: 'next typegen && tsc --noEmit' };
20
+ if (has('astro')) return { script: 'astro check', checker: ['@astrojs/check', '^0.9.10'] };
21
+ if (has('nuxt')) return { script: 'nuxi typecheck', checker: ['vue-tsc', '^3.3.13'] };
22
+ if (has('@sveltejs/kit'))
23
+ return {
24
+ script: 'svelte-kit sync && svelte-check --tsconfig ./tsconfig.json',
25
+ checker: ['svelte-check', '^4.7.6']
26
+ };
27
+ if (has('svelte')) return { script: 'svelte-check', checker: ['svelte-check', '^4.7.6'] };
28
+ // A solution-style tsconfig (Vite templates) has no files of its own: only build mode checks
29
+ // the referenced projects.
30
+ const build = /"references"\s*:/.test(readTextFile(path.join(targetDir, 'tsconfig.json')) || '');
31
+ if (has('vue'))
32
+ return {
33
+ script: build ? 'vue-tsc -b --noEmit' : 'vue-tsc --noEmit',
34
+ checker: ['vue-tsc', '^3.3.13']
35
+ };
36
+ return { script: build ? 'tsc -b --noEmit' : 'tsc --noEmit' };
37
+ }
38
+
11
39
  export const webPreset: PresetHandler = {
12
40
  name: 'web',
13
41
  description:
@@ -55,8 +83,12 @@ export const webPreset: PresetHandler = {
55
83
  );
56
84
  addTestingDependencies(pkg, config.browser.playwright);
57
85
  if (fileExists(path.join(targetDir, 'tsconfig.json'))) {
58
- pkg.scripts.typecheck ??=
59
- fresh && framework === 'next' ? 'next typegen && tsc --noEmit' : 'tsc --noEmit';
86
+ if (pkg.scripts.typecheck === undefined) {
87
+ const { script, checker } = typecheckFor(pkg, targetDir, fresh && framework === 'next');
88
+ pkg.scripts.typecheck = script;
89
+ if (checker && !pkg.dependencies?.[checker[0]] && !pkg.devDependencies[checker[0]])
90
+ pkg.devDependencies[checker[0]] = checker[1];
91
+ }
60
92
  if (!pkg.dependencies?.typescript && !pkg.devDependencies.typescript)
61
93
  pkg.devDependencies.typescript = '^5.6.3';
62
94
  }