ai-dev-setup 1.0.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/bin/ai-dev-setup.js +7 -0
  4. package/package.json +35 -0
  5. package/src/cli/index.js +142 -0
  6. package/src/cli/logger.js +29 -0
  7. package/src/cli/prompts.js +267 -0
  8. package/src/commands/init.js +321 -0
  9. package/src/commands/update.js +6 -0
  10. package/src/constants.js +34 -0
  11. package/src/core/detector.js +164 -0
  12. package/src/core/git-ref.js +28 -0
  13. package/src/core/gitignore-vendor.js +126 -0
  14. package/src/core/renderer.js +97 -0
  15. package/src/core/vendors.js +354 -0
  16. package/src/core/writer.js +67 -0
  17. package/src/platforms/claude-code.js +33 -0
  18. package/src/platforms/cursor.js +33 -0
  19. package/src/platforms/platform.js +18 -0
  20. package/src/platforms/registry.js +56 -0
  21. package/src/templates/claude-code/claude.md.tmpl +58 -0
  22. package/src/templates/claude-code/commands/kickoff.md.tmpl +18 -0
  23. package/src/templates/claude-code/commands/review.md.tmpl +20 -0
  24. package/src/templates/claude-code/commands/ship.md.tmpl +19 -0
  25. package/src/templates/claude-code/settings.json.tmpl +17 -0
  26. package/src/templates/cursor/cursorrules.tmpl +36 -0
  27. package/src/templates/cursor/rules/agents.mdc.tmpl +12 -0
  28. package/src/templates/cursor/rules/core-rules.mdc.tmpl +14 -0
  29. package/src/templates/cursor/rules/review.mdc.tmpl +13 -0
  30. package/src/templates/cursor/rules/workflow.mdc.tmpl +12 -0
  31. package/src/templates/ignore/claudeignore.tmpl +25 -0
  32. package/src/templates/ignore/cursorignore.tmpl +25 -0
  33. package/src/templates/shared/agents.md.tmpl +41 -0
  34. package/src/templates/shared/docs/api-patterns.md.tmpl +39 -0
  35. package/src/templates/shared/docs/architecture.md.tmpl +41 -0
  36. package/src/templates/shared/docs/conventions.md.tmpl +50 -0
  37. package/src/templates/shared/docs/error-handling.md.tmpl +32 -0
  38. package/src/templates/shared/docs/security.md.tmpl +37 -0
  39. package/src/templates/shared/docs/testing.md.tmpl +34 -0
  40. package/src/templates/shared/rules.md.tmpl +65 -0
  41. package/src/templates/shared/workflow.md.tmpl +42 -0
@@ -0,0 +1,164 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ async function readJsonIfExists(cwd, name) {
5
+ try {
6
+ const p = path.join(cwd, name);
7
+ const raw = await fs.readFile(p, 'utf8');
8
+ return JSON.parse(raw);
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ async function fileExists(cwd, name) {
15
+ try {
16
+ await fs.access(path.join(cwd, name));
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ async function globFrameworkConfig(cwd, prefix) {
24
+ try {
25
+ const entries = await fs.readdir(cwd, { withFileTypes: true });
26
+ return entries.some((e) => e.isFile() && e.name.startsWith(prefix));
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ function pickScript(scripts, keys) {
33
+ if (!scripts || typeof scripts !== 'object') return null;
34
+ for (const k of keys) {
35
+ if (typeof scripts[k] === 'string' && scripts[k].trim()) return scripts[k].trim();
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function depsFromPkg(pkg) {
41
+ const d = pkg?.dependencies ?? {};
42
+ const dev = pkg?.devDependencies ?? {};
43
+ return { ...dev, ...d };
44
+ }
45
+
46
+ /**
47
+ * Auto-detect project stack from the file system.
48
+ * @param {string} cwd
49
+ */
50
+ export async function detectProject(cwd) {
51
+ const result = {
52
+ name: null,
53
+ language: null,
54
+ framework: null,
55
+ testCmd: null,
56
+ lintCmd: null,
57
+ buildCmd: null,
58
+ database: null,
59
+ detectedStack: [],
60
+ };
61
+
62
+ const pkg = await readJsonIfExists(cwd, 'package.json');
63
+ if (pkg) {
64
+ if (typeof pkg.name === 'string' && pkg.name.trim()) result.name = pkg.name.trim();
65
+ const scripts = pkg.scripts;
66
+ result.testCmd = pickScript(scripts, ['test', 'test:unit', 'test:ci']);
67
+ result.lintCmd = pickScript(scripts, ['lint', 'eslint', 'check:lint']);
68
+ result.buildCmd = pickScript(scripts, ['build', 'compile', 'build:prod']);
69
+
70
+ const deps = depsFromPkg(pkg);
71
+ if (deps.react) {
72
+ result.detectedStack.push('react');
73
+ if (!result.framework) result.framework = 'React';
74
+ }
75
+ if (deps.next) {
76
+ result.detectedStack.push('nextjs');
77
+ result.framework = 'Next.js';
78
+ }
79
+ if (deps['@nestjs/core']) {
80
+ result.detectedStack.push('nestjs');
81
+ result.framework = 'NestJS';
82
+ }
83
+ if (deps.prisma) {
84
+ const schemaPath = path.join(cwd, 'prisma', 'schema.prisma');
85
+ try {
86
+ const schema = await fs.readFile(schemaPath, 'utf8');
87
+ if (/provider\s*=\s*"postgresql"/i.test(schema)) result.database = 'PostgreSQL';
88
+ else if (/provider\s*=\s*"mysql"/i.test(schema)) result.database = 'MySQL';
89
+ else if (/provider\s*=\s*"sqlite"/i.test(schema)) result.database = 'SQLite';
90
+ } catch {
91
+ result.database = result.database ?? 'PostgreSQL';
92
+ }
93
+ }
94
+ if (deps.typeorm) {
95
+ result.database = result.database ?? 'SQL (TypeORM)';
96
+ }
97
+ if (deps.pg || deps['pg-promise']) {
98
+ result.database = result.database ?? 'PostgreSQL';
99
+ }
100
+ if (deps.mysql2) {
101
+ result.database = result.database ?? 'MySQL';
102
+ }
103
+ }
104
+
105
+ const hasTsConfig = await fileExists(cwd, 'tsconfig.json');
106
+ if (hasTsConfig) {
107
+ result.detectedStack.push('ts');
108
+ result.language = 'TypeScript';
109
+ }
110
+
111
+ if (pkg && !hasTsConfig && !result.language) {
112
+ result.language = 'JavaScript';
113
+ }
114
+
115
+ if (await globFrameworkConfig(cwd, 'next.config')) {
116
+ if (!result.detectedStack.includes('nextjs')) result.detectedStack.push('nextjs');
117
+ result.framework = 'Next.js';
118
+ }
119
+
120
+ if (await readJsonIfExists(cwd, 'nest-cli.json')) {
121
+ if (!result.detectedStack.includes('nestjs')) result.detectedStack.push('nestjs');
122
+ result.framework = 'NestJS';
123
+ }
124
+
125
+ if (pkg) {
126
+ const deps = depsFromPkg(pkg);
127
+ const isNext = result.detectedStack.includes('nextjs');
128
+ if ((deps.express || deps.fastify || deps.koa || deps.hapi) && !isNext) {
129
+ result.detectedStack.push('node');
130
+ if (!result.framework || result.framework === 'React') {
131
+ result.framework = 'Node.js API';
132
+ }
133
+ }
134
+ }
135
+
136
+ if (await fileExists(cwd, 'pyproject.toml') || (await fileExists(cwd, 'requirements.txt'))) {
137
+ result.detectedStack.push('python');
138
+ result.language = 'Python';
139
+ if (!result.testCmd) result.testCmd = 'pytest';
140
+ if (!result.lintCmd) result.lintCmd = 'ruff check .';
141
+ if (!result.buildCmd) result.buildCmd = 'python -m build';
142
+ }
143
+
144
+ if (await fileExists(cwd, 'go.mod')) {
145
+ result.detectedStack.push('go');
146
+ result.language = 'Go';
147
+ if (!result.testCmd) result.testCmd = 'go test ./...';
148
+ if (!result.lintCmd) result.lintCmd = 'golangci-lint run';
149
+ if (!result.buildCmd) result.buildCmd = 'go build ./...';
150
+ }
151
+
152
+ if (await fileExists(cwd, 'pubspec.yaml')) {
153
+ result.detectedStack.push('flutter');
154
+ result.language = 'Dart';
155
+ result.framework = 'Flutter';
156
+ if (!result.testCmd) result.testCmd = 'flutter test';
157
+ if (!result.lintCmd) result.lintCmd = 'dart analyze';
158
+ if (!result.buildCmd) result.buildCmd = 'flutter build apk';
159
+ }
160
+
161
+ result.detectedStack = [...new Set(result.detectedStack)];
162
+
163
+ return result;
164
+ }
@@ -0,0 +1,28 @@
1
+ const MAX_LEN = 256;
2
+ /** Branch/tag names: alphanumerics, separators common in semver and paths */
3
+ const SAFE_REF = /^[a-zA-Z0-9._/-]+$/;
4
+
5
+ /**
6
+ * Validate a git ref before passing to `git clone -b`.
7
+ * @param {unknown} ref
8
+ * @returns {string}
9
+ */
10
+ export function assertSafeGitRef(ref) {
11
+ if (ref == null || ref === '') {
12
+ throw new Error('Git ref must be non-empty');
13
+ }
14
+ const s = String(ref).trim();
15
+ if (s.length > MAX_LEN) {
16
+ throw new Error(`Git ref exceeds maximum length (${MAX_LEN})`);
17
+ }
18
+ if (s.includes('..')) {
19
+ throw new Error('Invalid git ref');
20
+ }
21
+ if (s.startsWith('-')) {
22
+ throw new Error('Invalid git ref');
23
+ }
24
+ if (!SAFE_REF.test(s)) {
25
+ throw new Error('Invalid git ref: use only letters, digits, and . _ / -');
26
+ }
27
+ return s;
28
+ }
@@ -0,0 +1,126 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ /** @see ensureVendorDirGitignored — stable section for merges */
5
+ export const GITIGNORE_VENDOR_BEGIN = '# --- ai-dev-setup: vendor (managed) ---';
6
+ export const GITIGNORE_VENDOR_END = '# --- end ai-dev-setup vendor ---';
7
+ const VENDOR_LINE = '/vendor/';
8
+
9
+ const HEADER =
10
+ '# ai-dev-setup: ignore cloned Superpowers + Agency (remove this block if you commit vendor/)\n';
11
+
12
+ /**
13
+ * True if the line is a gitignore rule that ignores a root `vendor/` directory
14
+ * (or any path segment `vendor`, which is stronger).
15
+ * @param {string} line
16
+ */
17
+ export function lineIgnoresRootVendor(line) {
18
+ const t = line.trim();
19
+ if (!t || t.startsWith('#')) return false;
20
+ const core = t.startsWith('!') ? t.slice(1).trim() : t;
21
+ // Root-only style: vendor, vendor/, /vendor, /vendor/, **/vendor, **/vendor/
22
+ if (/^(?:\*\*\/)?\/?vendor\/?$/.test(core)) return true;
23
+ return false;
24
+ }
25
+
26
+ /**
27
+ * @param {string} content
28
+ */
29
+ export function fileAlreadyIgnoresVendorDir(content) {
30
+ const lines = content.split(/\r?\n/);
31
+ return lines.some((line) => lineIgnoresRootVendor(line));
32
+ }
33
+
34
+ function managedBlockPresent(content) {
35
+ return content.includes(GITIGNORE_VENDOR_BEGIN) && content.includes(GITIGNORE_VENDOR_END);
36
+ }
37
+
38
+ function blockInnerHasVendorLine(inner) {
39
+ return inner.split(/\r?\n/).some((l) => l.trim() === VENDOR_LINE);
40
+ }
41
+
42
+ function buildManagedBlock() {
43
+ return `${GITIGNORE_VENDOR_BEGIN}\n${VENDOR_LINE}\n${GITIGNORE_VENDOR_END}\n`;
44
+ }
45
+
46
+ /**
47
+ * @param {string} content normalized with \n
48
+ */
49
+ function repairManagedBlock(content) {
50
+ const start = content.indexOf(GITIGNORE_VENDOR_BEGIN);
51
+ const end = content.indexOf(GITIGNORE_VENDOR_END, start);
52
+ if (start === -1 || end === -1) return null;
53
+ const afterBegin = start + GITIGNORE_VENDOR_BEGIN.length;
54
+ const inner = content.slice(afterBegin, end);
55
+ if (blockInnerHasVendorLine(inner)) return null;
56
+ return content.slice(0, afterBegin) + `\n${VENDOR_LINE}` + content.slice(afterBegin);
57
+ }
58
+
59
+ /**
60
+ * Ensure `.gitignore` ignores repo-root `vendor/` without clobbering user rules.
61
+ *
62
+ * @param {string} cwd
63
+ * @param {{ onWarn?: (msg: string) => void }} [opts]
64
+ * @returns {Promise<{ action: 'created' | 'appended_block' | 'repaired_block' | 'noop_already_ignored' | 'noop_managed_ok' | 'error', error?: string }>}
65
+ */
66
+ export async function ensureVendorDirGitignored(cwd, opts = {}) {
67
+ const { onWarn } = opts;
68
+ const target = path.join(cwd, '.gitignore');
69
+
70
+ let raw = '';
71
+ try {
72
+ raw = await fs.readFile(target, 'utf8');
73
+ } catch (e) {
74
+ if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
75
+ raw = '';
76
+ } else {
77
+ const msg = e instanceof Error ? e.message : String(e);
78
+ onWarn?.(`.gitignore: could not read (${msg})`);
79
+ return { action: 'error', error: msg };
80
+ }
81
+ }
82
+
83
+ const content = raw.replace(/\r\n/g, '\n');
84
+
85
+ if (managedBlockPresent(content)) {
86
+ const repaired = repairManagedBlock(content);
87
+ if (repaired != null) {
88
+ try {
89
+ await fs.writeFile(target, ensureTrailingNewline(repaired), 'utf8');
90
+ return { action: 'repaired_block' };
91
+ } catch (e) {
92
+ const msg = e instanceof Error ? e.message : String(e);
93
+ onWarn?.(`.gitignore: could not write (${msg})`);
94
+ return { action: 'error', error: msg };
95
+ }
96
+ }
97
+ return { action: 'noop_managed_ok' };
98
+ }
99
+
100
+ if (fileAlreadyIgnoresVendorDir(content)) {
101
+ return { action: 'noop_already_ignored' };
102
+ }
103
+
104
+ const block = buildManagedBlock();
105
+ let next;
106
+ if (content.length === 0) {
107
+ next = HEADER + block;
108
+ } else {
109
+ const sep = content.endsWith('\n') ? '\n' : '\n\n';
110
+ next = content + sep + block;
111
+ }
112
+
113
+ try {
114
+ await fs.writeFile(target, ensureTrailingNewline(next), 'utf8');
115
+ return { action: content.length === 0 ? 'created' : 'appended_block' };
116
+ } catch (e) {
117
+ const msg = e instanceof Error ? e.message : String(e);
118
+ onWarn?.(`.gitignore: could not write (${msg})`);
119
+ return { action: 'error', error: msg };
120
+ }
121
+ }
122
+
123
+ /** @param {string} s */
124
+ function ensureTrailingNewline(s) {
125
+ return s.endsWith('\n') ? s : `${s}\n`;
126
+ }
@@ -0,0 +1,97 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ /**
4
+ * @param {string} key
5
+ * @param {Record<string, unknown>} config
6
+ */
7
+ function evalConditional(key, config) {
8
+ const stacks = Array.isArray(config.stacks) ? config.stacks : [];
9
+ const lang = config.language;
10
+ switch (key) {
11
+ case 'TYPESCRIPT':
12
+ return lang === 'TypeScript';
13
+ case 'PYTHON':
14
+ return lang === 'Python';
15
+ case 'GO':
16
+ return lang === 'Go';
17
+ case 'JAVASCRIPT':
18
+ return lang === 'JavaScript';
19
+ case 'DART':
20
+ return lang === 'Dart';
21
+ case 'FLUTTER':
22
+ return stacks.includes('flutter');
23
+ case 'REACT':
24
+ return stacks.includes('react');
25
+ case 'NEXTJS':
26
+ return stacks.includes('nextjs');
27
+ case 'NESTJS':
28
+ return stacks.includes('nestjs');
29
+ case 'NODE':
30
+ return stacks.includes('node');
31
+ default:
32
+ return false;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Process {{#IF_KEY}}...{{/IF_KEY}} blocks.
38
+ * @param {string} template
39
+ * @param {Record<string, unknown>} config
40
+ */
41
+ function processConditionals(template, config) {
42
+ const re = /\{\{#IF_([A-Z0-9_]+)\}\}([\s\S]*?)\{\{\/IF_\1\}\}/g;
43
+ return template.replace(re, (_, key, inner) => {
44
+ return evalConditional(key, config) ? inner : '';
45
+ });
46
+ }
47
+
48
+ const PLACEHOLDER_KEYS = [
49
+ 'PROJECT_NAME',
50
+ 'LANGUAGE',
51
+ 'FRAMEWORK',
52
+ 'TEST_CMD',
53
+ 'LINT_CMD',
54
+ 'BUILD_CMD',
55
+ 'DATABASE',
56
+ ];
57
+
58
+ /**
59
+ * @param {Record<string, unknown>} config
60
+ */
61
+ function placeholderValue(key, config) {
62
+ const map = {
63
+ PROJECT_NAME: config.projectName,
64
+ LANGUAGE: config.language,
65
+ FRAMEWORK: config.framework,
66
+ TEST_CMD: config.testCmd,
67
+ LINT_CMD: config.lintCmd,
68
+ BUILD_CMD: config.buildCmd,
69
+ DATABASE: config.database,
70
+ };
71
+ const v = map[key];
72
+ return v == null ? '' : String(v);
73
+ }
74
+
75
+ /**
76
+ * Replaces {{PLACEHOLDER}} tokens; unknown tokens stay unchanged.
77
+ * @param {string} template
78
+ * @param {Record<string, unknown>} config
79
+ */
80
+ export function render(template, config) {
81
+ let out = processConditionals(template, config);
82
+ for (const key of PLACEHOLDER_KEYS) {
83
+ const token = `{{${key}}}`;
84
+ const val = placeholderValue(key, config);
85
+ out = out.split(token).join(val);
86
+ }
87
+ return out;
88
+ }
89
+
90
+ /**
91
+ * @param {string} templatePath
92
+ * @param {Record<string, unknown>} config
93
+ */
94
+ export async function renderFile(templatePath, config) {
95
+ const raw = await readFile(templatePath, 'utf8');
96
+ return render(raw, config);
97
+ }