@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.
- package/LICENSE +21 -0
- package/README.md +245 -0
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/bin/aginit.js +8 -0
- package/dist/adapters/agents-md.d.ts +8 -0
- package/dist/adapters/agents-md.d.ts.map +1 -0
- package/dist/adapters/agents-md.js +121 -0
- package/dist/adapters/agents-md.js.map +1 -0
- package/dist/adapters/browser.d.ts +5 -0
- package/dist/adapters/browser.d.ts.map +1 -0
- package/dist/adapters/browser.js +42 -0
- package/dist/adapters/browser.js.map +1 -0
- package/dist/adapters/git.d.ts +5 -0
- package/dist/adapters/git.d.ts.map +1 -0
- package/dist/adapters/git.js +90 -0
- package/dist/adapters/git.js.map +1 -0
- package/dist/adapters/graft.d.ts +14 -0
- package/dist/adapters/graft.d.ts.map +1 -0
- package/dist/adapters/graft.js +56 -0
- package/dist/adapters/graft.js.map +1 -0
- package/dist/adapters/package-manager.d.ts +20 -0
- package/dist/adapters/package-manager.d.ts.map +1 -0
- package/dist/adapters/package-manager.js +89 -0
- package/dist/adapters/package-manager.js.map +1 -0
- package/dist/adapters/skills.d.ts +29 -0
- package/dist/adapters/skills.d.ts.map +1 -0
- package/dist/adapters/skills.js +80 -0
- package/dist/adapters/skills.js.map +1 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +212 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +32 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +162 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/new.d.ts +15 -0
- package/dist/cli/new.d.ts.map +1 -0
- package/dist/cli/new.js +161 -0
- package/dist/cli/new.js.map +1 -0
- package/dist/cli/wizard.d.ts +31 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +260 -0
- package/dist/cli/wizard.js.map +1 -0
- package/dist/config/defaults.d.ts +8 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +122 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/schema.d.ts +40 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +4 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/validation.d.ts +11 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +99 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/adr.d.ts +5 -0
- package/dist/presets/adr.d.ts.map +1 -0
- package/dist/presets/adr.js +33 -0
- package/dist/presets/adr.js.map +1 -0
- package/dist/presets/cli.d.ts +3 -0
- package/dist/presets/cli.d.ts.map +1 -0
- package/dist/presets/cli.js +126 -0
- package/dist/presets/cli.js.map +1 -0
- package/dist/presets/generic.d.ts +3 -0
- package/dist/presets/generic.d.ts.map +1 -0
- package/dist/presets/generic.js +29 -0
- package/dist/presets/generic.js.map +1 -0
- package/dist/presets/registry.d.ts +6 -0
- package/dist/presets/registry.d.ts.map +1 -0
- package/dist/presets/registry.js +20 -0
- package/dist/presets/registry.js.map +1 -0
- package/dist/presets/testing.d.ts +6 -0
- package/dist/presets/testing.d.ts.map +1 -0
- package/dist/presets/testing.js +46 -0
- package/dist/presets/testing.js.map +1 -0
- package/dist/presets/types.d.ts +14 -0
- package/dist/presets/types.d.ts.map +1 -0
- package/dist/presets/types.js +2 -0
- package/dist/presets/types.js.map +1 -0
- package/dist/presets/web.d.ts +3 -0
- package/dist/presets/web.d.ts.map +1 -0
- package/dist/presets/web.js +84 -0
- package/dist/presets/web.js.map +1 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +60 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +20 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/shell.d.ts +17 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +87 -0
- package/dist/utils/shell.js.map +1 -0
- package/package.json +71 -0
- package/src/adapters/agents-md.ts +143 -0
- package/src/adapters/browser.ts +49 -0
- package/src/adapters/git.ts +100 -0
- package/src/adapters/graft.ts +80 -0
- package/src/adapters/package-manager.ts +116 -0
- package/src/adapters/skills.ts +117 -0
- package/src/cli/doctor.ts +244 -0
- package/src/cli/init.ts +34 -0
- package/src/cli/main.ts +191 -0
- package/src/cli/new.ts +200 -0
- package/src/cli/wizard.ts +300 -0
- package/src/config/defaults.ts +137 -0
- package/src/config/schema.ts +44 -0
- package/src/config/validation.ts +96 -0
- package/src/index.ts +15 -0
- package/src/presets/adr.ts +37 -0
- package/src/presets/cli.ts +136 -0
- package/src/presets/generic.ts +32 -0
- package/src/presets/registry.ts +24 -0
- package/src/presets/testing.ts +56 -0
- package/src/presets/types.ts +15 -0
- package/src/presets/web.ts +99 -0
- package/src/utils/fs.ts +79 -0
- package/src/utils/logger.ts +20 -0
- 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
|
+
};
|
package/src/utils/fs.ts
ADDED
|
@@ -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
|
+
};
|