@antomanc/aginit 0.1.0

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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/THIRD_PARTY_NOTICES.md +36 -0
  4. package/bin/aginit.js +8 -0
  5. package/dist/adapters/agents-md.d.ts +8 -0
  6. package/dist/adapters/agents-md.d.ts.map +1 -0
  7. package/dist/adapters/agents-md.js +121 -0
  8. package/dist/adapters/agents-md.js.map +1 -0
  9. package/dist/adapters/browser.d.ts +5 -0
  10. package/dist/adapters/browser.d.ts.map +1 -0
  11. package/dist/adapters/browser.js +42 -0
  12. package/dist/adapters/browser.js.map +1 -0
  13. package/dist/adapters/git.d.ts +5 -0
  14. package/dist/adapters/git.d.ts.map +1 -0
  15. package/dist/adapters/git.js +90 -0
  16. package/dist/adapters/git.js.map +1 -0
  17. package/dist/adapters/graft.d.ts +14 -0
  18. package/dist/adapters/graft.d.ts.map +1 -0
  19. package/dist/adapters/graft.js +56 -0
  20. package/dist/adapters/graft.js.map +1 -0
  21. package/dist/adapters/package-manager.d.ts +20 -0
  22. package/dist/adapters/package-manager.d.ts.map +1 -0
  23. package/dist/adapters/package-manager.js +89 -0
  24. package/dist/adapters/package-manager.js.map +1 -0
  25. package/dist/adapters/skills.d.ts +29 -0
  26. package/dist/adapters/skills.d.ts.map +1 -0
  27. package/dist/adapters/skills.js +80 -0
  28. package/dist/adapters/skills.js.map +1 -0
  29. package/dist/cli/doctor.d.ts +2 -0
  30. package/dist/cli/doctor.d.ts.map +1 -0
  31. package/dist/cli/doctor.js +212 -0
  32. package/dist/cli/doctor.js.map +1 -0
  33. package/dist/cli/init.d.ts +3 -0
  34. package/dist/cli/init.d.ts.map +1 -0
  35. package/dist/cli/init.js +32 -0
  36. package/dist/cli/init.js.map +1 -0
  37. package/dist/cli/main.d.ts +3 -0
  38. package/dist/cli/main.d.ts.map +1 -0
  39. package/dist/cli/main.js +162 -0
  40. package/dist/cli/main.js.map +1 -0
  41. package/dist/cli/new.d.ts +15 -0
  42. package/dist/cli/new.d.ts.map +1 -0
  43. package/dist/cli/new.js +161 -0
  44. package/dist/cli/new.js.map +1 -0
  45. package/dist/cli/wizard.d.ts +31 -0
  46. package/dist/cli/wizard.d.ts.map +1 -0
  47. package/dist/cli/wizard.js +260 -0
  48. package/dist/cli/wizard.js.map +1 -0
  49. package/dist/config/defaults.d.ts +8 -0
  50. package/dist/config/defaults.d.ts.map +1 -0
  51. package/dist/config/defaults.js +122 -0
  52. package/dist/config/defaults.js.map +1 -0
  53. package/dist/config/schema.d.ts +40 -0
  54. package/dist/config/schema.d.ts.map +1 -0
  55. package/dist/config/schema.js +4 -0
  56. package/dist/config/schema.js.map +1 -0
  57. package/dist/config/validation.d.ts +11 -0
  58. package/dist/config/validation.d.ts.map +1 -0
  59. package/dist/config/validation.js +99 -0
  60. package/dist/config/validation.js.map +1 -0
  61. package/dist/index.d.ts +16 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +16 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/presets/adr.d.ts +5 -0
  66. package/dist/presets/adr.d.ts.map +1 -0
  67. package/dist/presets/adr.js +33 -0
  68. package/dist/presets/adr.js.map +1 -0
  69. package/dist/presets/cli.d.ts +3 -0
  70. package/dist/presets/cli.d.ts.map +1 -0
  71. package/dist/presets/cli.js +126 -0
  72. package/dist/presets/cli.js.map +1 -0
  73. package/dist/presets/generic.d.ts +3 -0
  74. package/dist/presets/generic.d.ts.map +1 -0
  75. package/dist/presets/generic.js +29 -0
  76. package/dist/presets/generic.js.map +1 -0
  77. package/dist/presets/registry.d.ts +6 -0
  78. package/dist/presets/registry.d.ts.map +1 -0
  79. package/dist/presets/registry.js +20 -0
  80. package/dist/presets/registry.js.map +1 -0
  81. package/dist/presets/testing.d.ts +6 -0
  82. package/dist/presets/testing.d.ts.map +1 -0
  83. package/dist/presets/testing.js +46 -0
  84. package/dist/presets/testing.js.map +1 -0
  85. package/dist/presets/types.d.ts +14 -0
  86. package/dist/presets/types.d.ts.map +1 -0
  87. package/dist/presets/types.js +2 -0
  88. package/dist/presets/types.js.map +1 -0
  89. package/dist/presets/web.d.ts +3 -0
  90. package/dist/presets/web.d.ts.map +1 -0
  91. package/dist/presets/web.js +84 -0
  92. package/dist/presets/web.js.map +1 -0
  93. package/dist/utils/fs.d.ts +13 -0
  94. package/dist/utils/fs.d.ts.map +1 -0
  95. package/dist/utils/fs.js +60 -0
  96. package/dist/utils/fs.js.map +1 -0
  97. package/dist/utils/logger.d.ts +10 -0
  98. package/dist/utils/logger.d.ts.map +1 -0
  99. package/dist/utils/logger.js +20 -0
  100. package/dist/utils/logger.js.map +1 -0
  101. package/dist/utils/shell.d.ts +17 -0
  102. package/dist/utils/shell.d.ts.map +1 -0
  103. package/dist/utils/shell.js +87 -0
  104. package/dist/utils/shell.js.map +1 -0
  105. package/package.json +71 -0
  106. package/src/adapters/agents-md.ts +143 -0
  107. package/src/adapters/browser.ts +49 -0
  108. package/src/adapters/git.ts +100 -0
  109. package/src/adapters/graft.ts +80 -0
  110. package/src/adapters/package-manager.ts +116 -0
  111. package/src/adapters/skills.ts +117 -0
  112. package/src/cli/doctor.ts +244 -0
  113. package/src/cli/init.ts +34 -0
  114. package/src/cli/main.ts +191 -0
  115. package/src/cli/new.ts +200 -0
  116. package/src/cli/wizard.ts +300 -0
  117. package/src/config/defaults.ts +137 -0
  118. package/src/config/schema.ts +44 -0
  119. package/src/config/validation.ts +96 -0
  120. package/src/index.ts +15 -0
  121. package/src/presets/adr.ts +37 -0
  122. package/src/presets/cli.ts +136 -0
  123. package/src/presets/generic.ts +32 -0
  124. package/src/presets/registry.ts +24 -0
  125. package/src/presets/testing.ts +56 -0
  126. package/src/presets/types.ts +15 -0
  127. package/src/presets/web.ts +99 -0
  128. package/src/utils/fs.ts +79 -0
  129. package/src/utils/logger.ts +20 -0
  130. package/src/utils/shell.ts +116 -0
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import { fileExists, readJsonFile } from '../utils/fs.js';
3
+ import { AginitConfig, AGINIT_CONFIG_FILENAME, LEGACY_CONFIG_FILENAME } from './schema.js';
4
+
5
+ export const PRESETS = ['web', 'cli', 'generic'] as const;
6
+ export const FRAMEWORKS = ['none', 'vite', 'next', 'existing'] as const;
7
+ export const SUPPORTED_PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn', 'bun'] as const;
8
+ export function isRecord(value: unknown): value is Record<string, any> {
9
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ }
11
+ export function validateProjectName(name: string): void {
12
+ if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name) || name.length > 214) {
13
+ throw new Error(
14
+ 'Invalid project name. Use a lowercase npm package name (letters, numbers, dots, dashes, underscores; optional @scope/name).'
15
+ );
16
+ }
17
+ }
18
+ export function validateUpstreamArgument(value: unknown, label: string): asserts value is string {
19
+ if (
20
+ typeof value !== 'string' ||
21
+ !value.trim() ||
22
+ value.startsWith('-') ||
23
+ /[\x00-\x1f\x7f]/.test(value)
24
+ ) {
25
+ throw new Error(
26
+ `Invalid ${label}: expected a nonempty value that does not begin with an option.`
27
+ );
28
+ }
29
+ }
30
+ export function validateConfig(value: unknown): AginitConfig {
31
+ const fail = (field: string): never => {
32
+ throw new Error(`Invalid project configuration: ${field}`);
33
+ };
34
+ if (!isRecord(value)) return fail('expected a JSON object');
35
+ if (value.schemaVersion !== undefined && value.schemaVersion !== '1.0.0')
36
+ fail('unsupported schemaVersion');
37
+ if (typeof value.name !== 'string' || !value.name) fail('name');
38
+ if (!PRESETS.includes(value.preset)) fail('preset');
39
+ if (value.framework !== undefined && !FRAMEWORKS.includes(value.framework)) fail('framework');
40
+ if (
41
+ value.packageManager !== undefined &&
42
+ !SUPPORTED_PACKAGE_MANAGERS.includes(value.packageManager)
43
+ )
44
+ fail('packageManager');
45
+ if (!isRecord(value.agents)) fail('agents');
46
+ for (const key of ['primary', 'secondary'])
47
+ validateUpstreamArgument(value.agents[key], `agent ${key}`);
48
+ if (!isRecord(value.skills) || !Array.isArray(value.skills.sources)) fail('skills.sources');
49
+ if (value.skills.workflow !== undefined && !['minimal', 'spec'].includes(value.skills.workflow))
50
+ fail('skills.workflow');
51
+ for (const source of value.skills.sources) {
52
+ if (!isRecord(source) || !Array.isArray(source.skills)) fail('skill source');
53
+ validateUpstreamArgument(source.package, 'skill package');
54
+ for (const skill of source.skills) validateUpstreamArgument(skill, 'skill name');
55
+ }
56
+ for (const [section, keys] of Object.entries({
57
+ codebase: ['graft'],
58
+ browser: ['visualAgent', 'playwright'],
59
+ docs: ['adr'],
60
+ github: ['ci']
61
+ })) {
62
+ if (!isRecord(value[section])) fail(section);
63
+ for (const key of keys) if (typeof value[section][key] !== 'boolean') fail(`${section}.${key}`);
64
+ }
65
+ return { ...value, schemaVersion: '1.0.0' } as AginitConfig;
66
+ }
67
+ export function readProjectConfig(dir: string): AginitConfig | null {
68
+ const file = [AGINIT_CONFIG_FILENAME, LEGACY_CONFIG_FILENAME]
69
+ .map((name) => path.join(dir, name))
70
+ .find(fileExists);
71
+ if (!file) return null;
72
+ try {
73
+ return validateConfig(readJsonFile<unknown>(file));
74
+ } catch (error) {
75
+ throw new Error(
76
+ `${path.basename(file)}: ${error instanceof Error ? error.message : String(error)}`
77
+ );
78
+ }
79
+ }
80
+ export function readPackageManifest(dir: string): Record<string, any> | null {
81
+ const file = path.join(dir, 'package.json');
82
+ if (!fileExists(file)) return null;
83
+ const pkg = readJsonFile<unknown>(file);
84
+ if (!isRecord(pkg)) throw new Error('Invalid package.json: expected a JSON object.');
85
+ for (const key of ['scripts', 'dependencies', 'devDependencies']) {
86
+ if (
87
+ pkg[key] !== undefined &&
88
+ (!isRecord(pkg[key]) || Object.values(pkg[key]).some((v) => typeof v !== 'string'))
89
+ ) {
90
+ throw new Error(`Invalid package.json: ${key} must contain string values.`);
91
+ }
92
+ }
93
+ if (pkg.name !== undefined && typeof pkg.name !== 'string')
94
+ throw new Error('Invalid package.json: name must be a string.');
95
+ return pkg;
96
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from './config/schema.js';
2
+ export * from './config/defaults.js';
3
+ export * from './config/validation.js';
4
+ export * from './presets/registry.js';
5
+ export * from './presets/types.js';
6
+ export * from './adapters/package-manager.js';
7
+ export * from './adapters/git.js';
8
+ export * from './adapters/skills.js';
9
+ export * from './adapters/graft.js';
10
+ export * from './adapters/agents-md.js';
11
+ export * from './adapters/browser.js';
12
+ export * from './cli/new.js';
13
+ export * from './cli/init.js';
14
+ export * from './cli/doctor.js';
15
+ export * from './cli/wizard.js';
@@ -0,0 +1,37 @@
1
+ import path from 'node:path';
2
+ import { safeWriteFile } from '../utils/fs.js';
3
+
4
+ const ADR_0001_CONTENT = `# 1. Record Architecture Decisions
5
+
6
+ Date: 2026-10-04
7
+
8
+ ## Status
9
+ Accepted
10
+
11
+ ## Context
12
+ We need to record significant architectural, structural, and tooling decisions
13
+ made throughout the lifecycle of this project so humans and AI agents understand
14
+ the rationale behind key choices.
15
+
16
+ ## Decision
17
+ We will use Architecture Decision Records (ADRs) stored in \`docs/adr/\`.
18
+ Each record describes:
19
+ - **Status**: Proposed, Accepted, Rejected, Superseded.
20
+ - **Context**: The problem or requirement prompting the decision.
21
+ - **Decision**: The selected option and why it was chosen.
22
+ - **Consequences**: Positive and negative outcomes or trade-offs.
23
+
24
+ ## Consequences
25
+ - Every major architectural shift is tracked in version control.
26
+ - Agents reading this repository can inspect historical decisions to avoid
27
+ re-proposing already evaluated or superseded designs.
28
+ `;
29
+
30
+ export function setupAdr(
31
+ targetDir: string,
32
+ options: { dryRun?: boolean; silent?: boolean } = {}
33
+ ): void {
34
+ const { dryRun = false, silent = false } = options;
35
+ const adrFile = path.join(targetDir, 'docs', 'adr', '0001-record-architecture-decisions.md');
36
+ safeWriteFile(adrFile, ADR_0001_CONTENT, { dryRun, silent });
37
+ }
@@ -0,0 +1,136 @@
1
+ import path from 'node:path';
2
+ import { fileExists, safeWriteFile, writeJsonFile } from '../utils/fs.js';
3
+ import { PresetContext, PresetHandler } from './types.js';
4
+ import { readPackageManifest } from '../config/validation.js';
5
+ import { addTestingDependencies, setupTesting, VITEST_VERSION } from './testing.js';
6
+ import { setupAdr } from './adr.js';
7
+
8
+ export const cliPreset: PresetHandler = {
9
+ name: 'cli',
10
+ description: 'AI-first TypeScript CLI application with Commander, Picocolors, and Vitest',
11
+ async scaffold(ctx: PresetContext): Promise<void> {
12
+ const { targetDir, projectName, dryRun, silent } = ctx;
13
+ const pm = ctx.config.packageManager || 'pnpm';
14
+
15
+ const existing = readPackageManifest(targetDir);
16
+ if (existing) {
17
+ const hadVitestScript = Object.values(existing.scripts || {}).some(
18
+ (script) => typeof script === 'string' && /^vitest(?:\s|$)/.test(script)
19
+ );
20
+ addTestingDependencies(existing);
21
+ writeJsonFile(path.join(targetDir, 'package.json'), existing, {
22
+ overwrite: true,
23
+ dryRun,
24
+ silent
25
+ });
26
+ setupTesting(ctx, hadVitestScript);
27
+ if (ctx.config.docs.adr) setupAdr(targetDir, { dryRun, silent });
28
+ return;
29
+ }
30
+
31
+ // 1. package.json
32
+ const pkg = {
33
+ name: projectName,
34
+ version: '0.1.0',
35
+ type: 'module',
36
+ bin: {
37
+ [projectName.split('/').pop()!]: './bin/cli.js'
38
+ },
39
+ scripts: {
40
+ build: 'tsc',
41
+ test: 'vitest run',
42
+ 'test:watch': 'vitest',
43
+ typecheck: 'tsc --noEmit',
44
+ prepack: 'tsc'
45
+ },
46
+ dependencies: {
47
+ commander: '^12.1.0',
48
+ picocolors: '^1.1.1'
49
+ },
50
+ devDependencies: {
51
+ '@types/node': '^22.8.0',
52
+ typescript: '^5.6.3',
53
+ vitest: VITEST_VERSION
54
+ }
55
+ };
56
+ writeJsonFile(path.join(targetDir, 'package.json'), pkg, { dryRun, silent });
57
+
58
+ // pnpm-workspace.yaml generated ONLY for pnpm!
59
+ if (pm === 'pnpm') {
60
+ const workspaceYamlPath = path.join(targetDir, 'pnpm-workspace.yaml');
61
+ if (!fileExists(workspaceYamlPath)) {
62
+ const workspaceYaml = `allowBuilds:\n esbuild: true\n`;
63
+ safeWriteFile(workspaceYamlPath, workspaceYaml, { dryRun, silent });
64
+ }
65
+ }
66
+
67
+ // 2. tsconfig.json
68
+ const tsconfig = {
69
+ compilerOptions: {
70
+ target: 'ES2022',
71
+ module: 'NodeNext',
72
+ moduleResolution: 'NodeNext',
73
+ lib: ['ES2022'],
74
+ strict: true,
75
+ skipLibCheck: true,
76
+ outDir: 'dist',
77
+ rootDir: 'src',
78
+ declaration: true
79
+ },
80
+ include: ['src/**/*']
81
+ };
82
+ writeJsonFile(path.join(targetDir, 'tsconfig.json'), tsconfig, { dryRun, silent });
83
+
84
+ // 3. vitest.config.ts
85
+ const vitestConfig = `import { defineConfig } from 'vitest/config';
86
+
87
+ export default defineConfig({
88
+ test: {
89
+ include: ['tests/**/*.test.{js,ts}']
90
+ }
91
+ });
92
+ `;
93
+ safeWriteFile(path.join(targetDir, 'vitest.config.ts'), vitestConfig, { dryRun, silent });
94
+
95
+ // 4. Executable CLI entrypoint
96
+ const binCli = `#!/usr/bin/env node
97
+ import { run } from '../dist/index.js';
98
+
99
+ run(process.argv.slice(2)).catch((err) => {
100
+ console.error(err);
101
+ process.exit(1);
102
+ });
103
+ `;
104
+ safeWriteFile(path.join(targetDir, 'bin', 'cli.js'), binCli, {
105
+ dryRun,
106
+ silent,
107
+ mode: 0o755
108
+ });
109
+
110
+ // 5. Source code
111
+ const srcIndex = `export async function run(args: string[] = []): Promise<void> {\n console.log(${JSON.stringify(projectName + ' ready. Args:')}, args);\n}\n`;
112
+ const createdSource = safeWriteFile(path.join(targetDir, 'src', 'index.ts'), srcIndex, {
113
+ dryRun,
114
+ silent
115
+ });
116
+
117
+ // 6. Unit test
118
+ const unitTest = `import { describe, it, expect } from 'vitest';
119
+ import { run } from '../src/index.js';
120
+
121
+ describe('run', () => {
122
+ it('runs with the supplied arguments', async () => {
123
+ await expect(run(['--help'])).resolves.toBeUndefined();
124
+ });
125
+ });
126
+ `;
127
+ if (createdSource)
128
+ safeWriteFile(path.join(targetDir, 'tests', 'index.test.ts'), unitTest, { dryRun, silent });
129
+ else setupTesting(ctx);
130
+
131
+ // 7. Docs ADR
132
+ if (ctx.config.docs.adr) {
133
+ setupAdr(targetDir, { dryRun, silent });
134
+ }
135
+ }
136
+ };
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { fileExists, writeJsonFile } from '../utils/fs.js';
3
+ import { PresetContext, PresetHandler } from './types.js';
4
+ import { addTestingDependencies, setupTesting } from './testing.js';
5
+ import { setupAdr } from './adr.js';
6
+
7
+ export const genericPreset: PresetHandler = {
8
+ name: 'generic',
9
+ description: 'Minimal, unopinionated project skeleton with AI skills, Git, and ADR structure',
10
+ async scaffold(ctx: PresetContext): Promise<void> {
11
+ const { targetDir, projectName, dryRun, silent } = ctx;
12
+
13
+ const pkgPath = path.join(targetDir, 'package.json');
14
+ if (!fileExists(pkgPath)) {
15
+ const pkg = {
16
+ name: projectName,
17
+ version: '0.1.0',
18
+ type: 'module',
19
+ scripts: {
20
+ test: 'vitest run'
21
+ }
22
+ };
23
+ addTestingDependencies(pkg);
24
+ writeJsonFile(pkgPath, pkg, { dryRun, silent });
25
+ setupTesting(ctx);
26
+ }
27
+
28
+ if (ctx.config.docs.adr) {
29
+ setupAdr(targetDir, { dryRun, silent });
30
+ }
31
+ }
32
+ };
@@ -0,0 +1,24 @@
1
+ import { PresetHandler } from './types.js';
2
+ import { webPreset } from './web.js';
3
+ import { cliPreset } from './cli.js';
4
+ import { genericPreset } from './generic.js';
5
+ import { PresetType } from '../config/schema.js';
6
+
7
+ export const presets: Record<PresetType, PresetHandler> = {
8
+ web: webPreset,
9
+ cli: cliPreset,
10
+ generic: genericPreset
11
+ };
12
+
13
+ export function getPreset(name: string): PresetHandler {
14
+ const handler = presets[name as PresetType];
15
+ if (!handler) {
16
+ const valid = Object.keys(presets).join(', ');
17
+ throw new Error(`Unknown preset: "${name}". Valid presets are: ${valid}`);
18
+ }
19
+ return handler;
20
+ }
21
+
22
+ export function listPresets(): PresetHandler[] {
23
+ return Object.values(presets);
24
+ }
@@ -0,0 +1,56 @@
1
+ import path from 'node:path';
2
+ import { safeWriteFile, fileExists } from '../utils/fs.js';
3
+ import { PresetContext } from './types.js';
4
+
5
+ export const VITEST_VERSION = '^4.1.11';
6
+ export function addTestingDependencies(pkg: Record<string, any>, playwright = false): void {
7
+ pkg.scripts ??= {};
8
+ pkg.devDependencies ??= {};
9
+ if (!pkg.dependencies?.vitest && !pkg.devDependencies.vitest)
10
+ pkg.devDependencies.vitest = VITEST_VERSION;
11
+ const customTest = pkg.scripts.test && !/^vitest(?:\s|$)/.test(pkg.scripts.test);
12
+ const testScript = customTest ? 'test:unit' : 'test';
13
+ pkg.scripts[testScript] ??= 'vitest run';
14
+ pkg.scripts[customTest ? 'test:unit:watch' : 'test:watch'] ??= 'vitest';
15
+ if (playwright) {
16
+ pkg.scripts['test:e2e'] ??= 'playwright test';
17
+ if (!pkg.dependencies?.['@playwright/test'] && !pkg.devDependencies['@playwright/test'])
18
+ pkg.devDependencies['@playwright/test'] = '^1.63.0';
19
+ }
20
+ }
21
+
22
+ /** A starter harness checks package metadata without assuming application exports. */
23
+ export function setupTesting(ctx: PresetContext, preserveViteTestConfig = false): void {
24
+ const { targetDir, dryRun, silent } = ctx;
25
+ const hasConfig = [
26
+ 'vitest.config.ts',
27
+ 'vitest.config.js',
28
+ 'vitest.config.mts',
29
+ 'vitest.config.mjs',
30
+ 'vitest.config.cts',
31
+ 'vitest.config.cjs',
32
+ ...(preserveViteTestConfig
33
+ ? ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs']
34
+ : [])
35
+ ].some((name) => fileExists(path.join(targetDir, name)));
36
+ if (!hasConfig)
37
+ safeWriteFile(
38
+ path.join(targetDir, 'vitest.config.mts'),
39
+ `import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n test: { include: ['tests/**/*.test.{js,ts}'] }\n});\n`,
40
+ { dryRun, silent }
41
+ );
42
+ safeWriteFile(
43
+ path.join(targetDir, 'tests', 'aginit.test.js'),
44
+ `import { it, expect } from 'vitest';
45
+ import { readFileSync } from 'node:fs';
46
+
47
+ // Starter harness: add application behavior tests as the project takes shape.
48
+ it('has a valid project manifest', () => {
49
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
50
+ expect(typeof pkg.name).toBe('string');
51
+ expect(pkg.name.length).toBeGreaterThan(0);
52
+ });
53
+ `,
54
+ { dryRun, silent }
55
+ );
56
+ }
@@ -0,0 +1,15 @@
1
+ import { AIProjectConfig, PresetType } from '../config/schema.js';
2
+
3
+ export interface PresetContext {
4
+ targetDir: string;
5
+ projectName: string;
6
+ config: AIProjectConfig;
7
+ dryRun?: boolean;
8
+ silent?: boolean;
9
+ }
10
+
11
+ export interface PresetHandler {
12
+ name: PresetType;
13
+ description: string;
14
+ scaffold: (ctx: PresetContext) => Promise<void>;
15
+ }
@@ -0,0 +1,99 @@
1
+ import path from 'node:path';
2
+ import { fileExists, safeWriteFile, writeJsonFile } from '../utils/fs.js';
3
+ import { readPackageManifest } from '../config/validation.js';
4
+ import { runExecutable } from '../utils/shell.js';
5
+ import { PresetContext, PresetHandler } from './types.js';
6
+ import { setupBrowserTesting } from '../adapters/browser.js';
7
+ import { setupAdr } from './adr.js';
8
+ import { scaffoldCommand } from '../adapters/package-manager.js';
9
+ import { addTestingDependencies, setupTesting } from './testing.js';
10
+
11
+ export const webPreset: PresetHandler = {
12
+ name: 'web',
13
+ description:
14
+ 'Framework-agnostic web project with upstream scaffolding, Vitest and optional Playwright',
15
+ async scaffold(ctx: PresetContext): Promise<void> {
16
+ const { targetDir, projectName, dryRun, silent, config } = ctx;
17
+ const framework = config.framework || 'none';
18
+ const pkgPath = path.join(targetDir, 'package.json');
19
+ const pm = config.packageManager || 'pnpm';
20
+ let pkg = readPackageManifest(targetDir);
21
+ const fresh = pkg === null;
22
+ if (fresh && framework === 'existing')
23
+ throw new Error('An existing framework requires an existing package.json.');
24
+ if (fresh && (framework === 'vite' || framework === 'next')) {
25
+ const command = scaffoldCommand(pm, framework);
26
+ const result = await runExecutable(command.executable, command.args, {
27
+ cwd: targetDir,
28
+ dryRun,
29
+ silent: true
30
+ });
31
+ if (!result.ok)
32
+ throw new Error(`${framework} scaffolding failed: ${result.stderr || result.stdout}`);
33
+ if (!dryRun) {
34
+ pkg = readPackageManifest(targetDir);
35
+ const expected = framework === 'vite' ? 'index.html' : 'src/app/page.tsx';
36
+ if (!pkg || !fileExists(path.join(targetDir, expected)))
37
+ throw new Error(
38
+ `${framework} scaffolding did not create a complete project; it may have been cancelled.`
39
+ );
40
+ }
41
+ }
42
+ if (!pkg) {
43
+ // In a framework dry run the upstream command has only been displayed.
44
+ if (dryRun && (framework === 'vite' || framework === 'next')) return;
45
+ pkg = {
46
+ name: projectName,
47
+ version: '0.1.0',
48
+ type: 'module',
49
+ scripts: { build: 'tsc', typecheck: 'tsc --noEmit' },
50
+ devDependencies: { '@types/node': '^22.8.0', typescript: '^5.6.3' }
51
+ };
52
+ }
53
+ const hadVitestScript = Object.values(pkg.scripts || {}).some(
54
+ (script) => typeof script === 'string' && /^vitest(?:\s|$)/.test(script)
55
+ );
56
+ addTestingDependencies(pkg, config.browser.playwright);
57
+ if (fileExists(path.join(targetDir, 'tsconfig.json'))) {
58
+ pkg.scripts.typecheck ??=
59
+ fresh && framework === 'next' ? 'next typegen && tsc --noEmit' : 'tsc --noEmit';
60
+ if (!pkg.dependencies?.typescript && !pkg.devDependencies.typescript)
61
+ pkg.devDependencies.typescript = '^5.6.3';
62
+ }
63
+ // Only scaffold application source and compiler settings for a fresh agnostic project.
64
+ if (fresh && framework === 'none') {
65
+ writeJsonFile(
66
+ path.join(targetDir, 'tsconfig.json'),
67
+ {
68
+ compilerOptions: {
69
+ target: 'ES2022',
70
+ module: 'NodeNext',
71
+ moduleResolution: 'NodeNext',
72
+ lib: ['ES2022', 'DOM'],
73
+ strict: true,
74
+ skipLibCheck: true,
75
+ outDir: 'dist',
76
+ rootDir: 'src'
77
+ },
78
+ include: ['src/**/*']
79
+ },
80
+ { dryRun, silent }
81
+ );
82
+ safeWriteFile(
83
+ path.join(targetDir, 'src', 'index.ts'),
84
+ `export function createApp() {\n return { name: ${JSON.stringify(projectName)}, status: 'ready' };\n}\n`,
85
+ { dryRun, silent }
86
+ );
87
+ }
88
+ writeJsonFile(pkgPath, pkg, { overwrite: true, dryRun, silent });
89
+ if (pm === 'pnpm')
90
+ safeWriteFile(
91
+ path.join(targetDir, 'pnpm-workspace.yaml'),
92
+ 'allowBuilds:\n esbuild: true\n',
93
+ { dryRun, silent }
94
+ );
95
+ setupTesting(ctx, hadVitestScript);
96
+ if (config.browser.playwright) setupBrowserTesting(targetDir, { dryRun, silent });
97
+ if (config.docs.adr) setupAdr(targetDir, { dryRun, silent });
98
+ }
99
+ };
@@ -0,0 +1,79 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { logger } from './logger.js';
4
+
5
+ export interface WriteFileOptions {
6
+ overwrite?: boolean;
7
+ dryRun?: boolean;
8
+ silent?: boolean;
9
+ mode?: number;
10
+ }
11
+
12
+ export function ensureDir(dirPath: string, dryRun = false): void {
13
+ if (dryRun) {
14
+ logger.dim(`[dry-run] Would create directory: ${dirPath}`);
15
+ return;
16
+ }
17
+ if (!fs.existsSync(dirPath)) {
18
+ fs.mkdirSync(dirPath, { recursive: true });
19
+ }
20
+ }
21
+
22
+ export function fileExists(filePath: string): boolean {
23
+ return fs.existsSync(filePath);
24
+ }
25
+
26
+ export function safeWriteFile(
27
+ filePath: string,
28
+ content: string,
29
+ options: WriteFileOptions = {}
30
+ ): boolean {
31
+ const { overwrite = false, dryRun = false, silent = false, mode } = options;
32
+
33
+ if (fileExists(filePath) && !overwrite) {
34
+ if (!silent) {
35
+ logger.dim(`Skipping existing file: ${path.basename(filePath)}`);
36
+ }
37
+ return false;
38
+ }
39
+
40
+ if (dryRun) {
41
+ logger.dim(`[dry-run] Would write file: ${filePath}`);
42
+ return true;
43
+ }
44
+
45
+ ensureDir(path.dirname(filePath), false);
46
+ fs.writeFileSync(filePath, content, { encoding: 'utf-8', mode });
47
+ if (mode !== undefined && process.platform !== 'win32') {
48
+ try {
49
+ fs.chmodSync(filePath, mode);
50
+ } catch {
51
+ // ignore in environments that do not permit chmod
52
+ }
53
+ }
54
+ return true;
55
+ }
56
+
57
+ export function readTextFile(filePath: string): string | null {
58
+ if (!fs.existsSync(filePath)) return null;
59
+ return fs.readFileSync(filePath, 'utf-8');
60
+ }
61
+
62
+ export function readJsonFile<T>(filePath: string): T | null {
63
+ const text = readTextFile(filePath);
64
+ if (!text) return null;
65
+ try {
66
+ return JSON.parse(text) as T;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ export function writeJsonFile(
73
+ filePath: string,
74
+ data: unknown,
75
+ options: WriteFileOptions = {}
76
+ ): boolean {
77
+ const content = JSON.stringify(data, null, 2) + '\n';
78
+ return safeWriteFile(filePath, content, options);
79
+ }
@@ -0,0 +1,20 @@
1
+ import pc from 'picocolors';
2
+
3
+ export const logger = {
4
+ info: (msg: string) => console.log(pc.cyan('ℹ ') + msg),
5
+ success: (msg: string) => console.log(pc.green('✓ ') + msg),
6
+ warn: (msg: string) => console.log(pc.yellow('⚠ ') + msg),
7
+ error: (msg: string) => console.error(pc.red('✖ ') + msg),
8
+ dim: (msg: string) => console.log(pc.dim(msg)),
9
+ step: (step: number, total: number, msg: string) => {
10
+ console.log(pc.bold(pc.magenta(`[${step}/${total}] `)) + pc.bold(msg));
11
+ },
12
+ banner: (title: string, subtitle?: string) => {
13
+ console.log();
14
+ console.log(pc.bold(pc.cyan(title)));
15
+ if (subtitle) {
16
+ console.log(pc.dim(subtitle));
17
+ }
18
+ console.log();
19
+ }
20
+ };