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.
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/bin/ai-dev-setup.js +7 -0
- package/package.json +35 -0
- package/src/cli/index.js +142 -0
- package/src/cli/logger.js +29 -0
- package/src/cli/prompts.js +267 -0
- package/src/commands/init.js +321 -0
- package/src/commands/update.js +6 -0
- package/src/constants.js +34 -0
- package/src/core/detector.js +164 -0
- package/src/core/git-ref.js +28 -0
- package/src/core/gitignore-vendor.js +126 -0
- package/src/core/renderer.js +97 -0
- package/src/core/vendors.js +354 -0
- package/src/core/writer.js +67 -0
- package/src/platforms/claude-code.js +33 -0
- package/src/platforms/cursor.js +33 -0
- package/src/platforms/platform.js +18 -0
- package/src/platforms/registry.js +56 -0
- package/src/templates/claude-code/claude.md.tmpl +58 -0
- package/src/templates/claude-code/commands/kickoff.md.tmpl +18 -0
- package/src/templates/claude-code/commands/review.md.tmpl +20 -0
- package/src/templates/claude-code/commands/ship.md.tmpl +19 -0
- package/src/templates/claude-code/settings.json.tmpl +17 -0
- package/src/templates/cursor/cursorrules.tmpl +36 -0
- package/src/templates/cursor/rules/agents.mdc.tmpl +12 -0
- package/src/templates/cursor/rules/core-rules.mdc.tmpl +14 -0
- package/src/templates/cursor/rules/review.mdc.tmpl +13 -0
- package/src/templates/cursor/rules/workflow.mdc.tmpl +12 -0
- package/src/templates/ignore/claudeignore.tmpl +25 -0
- package/src/templates/ignore/cursorignore.tmpl +25 -0
- package/src/templates/shared/agents.md.tmpl +41 -0
- package/src/templates/shared/docs/api-patterns.md.tmpl +39 -0
- package/src/templates/shared/docs/architecture.md.tmpl +41 -0
- package/src/templates/shared/docs/conventions.md.tmpl +50 -0
- package/src/templates/shared/docs/error-handling.md.tmpl +32 -0
- package/src/templates/shared/docs/security.md.tmpl +37 -0
- package/src/templates/shared/docs/testing.md.tmpl +34 -0
- package/src/templates/shared/rules.md.tmpl +65 -0
- package/src/templates/shared/workflow.md.tmpl +42 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import readline from 'node:readline/promises';
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
+
import { bold, dim, green } from './logger.js';
|
|
5
|
+
|
|
6
|
+
function restoreTerminal() {
|
|
7
|
+
output.write('\x1b[?25h');
|
|
8
|
+
if (input.isTTY) {
|
|
9
|
+
input.setRawMode(false);
|
|
10
|
+
input.pause();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function canUseInteractiveMode() {
|
|
15
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function itemLabel(item) {
|
|
19
|
+
if (typeof item === 'string') return item;
|
|
20
|
+
return item.desc ? `${item.label} ${dim(item.desc)}` : item.label;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @param {string} question */
|
|
24
|
+
export async function textPrompt(question, defaultValue = '') {
|
|
25
|
+
if (!canUseInteractiveMode()) {
|
|
26
|
+
return defaultValue;
|
|
27
|
+
}
|
|
28
|
+
const rl = readline.createInterface({ input, output });
|
|
29
|
+
try {
|
|
30
|
+
const hint = defaultValue ? dim(` [${defaultValue}]`) : '';
|
|
31
|
+
const line = await rl.question(`${question}${hint}: `);
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
return trimmed === '' ? defaultValue : trimmed;
|
|
34
|
+
} finally {
|
|
35
|
+
rl.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @param {string} question */
|
|
40
|
+
export async function confirm(question, defaultYes = true) {
|
|
41
|
+
if (!canUseInteractiveMode()) {
|
|
42
|
+
return defaultYes;
|
|
43
|
+
}
|
|
44
|
+
const suffix = defaultYes ? 'Y/n' : 'y/N';
|
|
45
|
+
const rl = readline.createInterface({ input, output });
|
|
46
|
+
try {
|
|
47
|
+
const line = await rl.question(`${question} (${suffix}): `);
|
|
48
|
+
const t = line.trim().toLowerCase();
|
|
49
|
+
if (t === '') return defaultYes;
|
|
50
|
+
if (t === 'y' || t === 'yes') return true;
|
|
51
|
+
if (t === 'n' || t === 'no') return false;
|
|
52
|
+
return defaultYes;
|
|
53
|
+
} finally {
|
|
54
|
+
rl.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Single-select: ↑/↓ to navigate, Enter to confirm. */
|
|
59
|
+
export async function selectOne(question, items, defaultIdx = 0) {
|
|
60
|
+
if (!canUseInteractiveMode()) {
|
|
61
|
+
if (items.length === 0) {
|
|
62
|
+
throw new Error('Interactive prompt requires a TTY. Re-run with --yes in non-interactive environments.');
|
|
63
|
+
}
|
|
64
|
+
const safeIdx = Math.min(Math.max(defaultIdx, 0), items.length - 1);
|
|
65
|
+
return items[safeIdx];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let idx = defaultIdx;
|
|
69
|
+
|
|
70
|
+
const render = () => {
|
|
71
|
+
output.write('\x1b[?25l');
|
|
72
|
+
output.write(`\n ${bold(question)}\n`);
|
|
73
|
+
for (let i = 0; i < items.length; i++) {
|
|
74
|
+
const pointer = i === idx ? green('❯') : ' ';
|
|
75
|
+
const label = i === idx ? bold(itemLabel(items[i])) : itemLabel(items[i]);
|
|
76
|
+
output.write(` ${pointer} ${label}\n`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const clear = () => {
|
|
81
|
+
const lines = items.length + 2;
|
|
82
|
+
output.write(`\x1b[${lines}A`);
|
|
83
|
+
for (let i = 0; i < lines; i++) {
|
|
84
|
+
output.write('\x1b[2K\n');
|
|
85
|
+
}
|
|
86
|
+
output.write(`\x1b[${lines}A`);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
render();
|
|
90
|
+
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
input.setRawMode(true);
|
|
93
|
+
input.resume();
|
|
94
|
+
|
|
95
|
+
const onData = (data) => {
|
|
96
|
+
try {
|
|
97
|
+
const bytes = [...data];
|
|
98
|
+
const key = data.toString();
|
|
99
|
+
|
|
100
|
+
const isUp =
|
|
101
|
+
key === '\x1b[A' ||
|
|
102
|
+
(bytes.length === 2 && (bytes[0] === 0xe0 || bytes[0] === 0x00) && bytes[1] === 0x48);
|
|
103
|
+
const isDown =
|
|
104
|
+
key === '\x1b[B' ||
|
|
105
|
+
(bytes.length === 2 && (bytes[0] === 0xe0 || bytes[0] === 0x00) && bytes[1] === 0x50);
|
|
106
|
+
const isEnter = key === '\r' || key === '\n';
|
|
107
|
+
const isCtrlC = key === '\x03';
|
|
108
|
+
|
|
109
|
+
if (isCtrlC) {
|
|
110
|
+
restoreTerminal();
|
|
111
|
+
process.exit(130);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isUp) {
|
|
115
|
+
idx = (idx - 1 + items.length) % items.length;
|
|
116
|
+
clear();
|
|
117
|
+
render();
|
|
118
|
+
} else if (isDown) {
|
|
119
|
+
idx = (idx + 1) % items.length;
|
|
120
|
+
clear();
|
|
121
|
+
render();
|
|
122
|
+
} else if (isEnter) {
|
|
123
|
+
restoreTerminal();
|
|
124
|
+
input.removeListener('data', onData);
|
|
125
|
+
clear();
|
|
126
|
+
const item = items[idx];
|
|
127
|
+
const label = typeof item === 'string' ? item : item.label;
|
|
128
|
+
output.write(` ${bold(question)} ${green(label)}\n`);
|
|
129
|
+
resolve(items[idx]);
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
restoreTerminal();
|
|
133
|
+
input.removeListener('data', onData);
|
|
134
|
+
reject(err);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
input.on('data', onData);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Multi-select: ↑/↓ navigate, Space toggle, A toggle all, Enter confirm.
|
|
144
|
+
* @param {number[]|null} defaultSelected
|
|
145
|
+
*/
|
|
146
|
+
export async function selectMulti(question, items, defaultSelected = null) {
|
|
147
|
+
if (!canUseInteractiveMode()) {
|
|
148
|
+
if (items.length === 0) {
|
|
149
|
+
throw new Error('Interactive prompt requires a TTY. Re-run with --yes in non-interactive environments.');
|
|
150
|
+
}
|
|
151
|
+
if (defaultSelected !== null) {
|
|
152
|
+
const indices = defaultSelected.filter((i) => i >= 0 && i < items.length);
|
|
153
|
+
return indices.map((i) => items[i]);
|
|
154
|
+
}
|
|
155
|
+
if (items.length >= 2) {
|
|
156
|
+
return [items[0], items[1]];
|
|
157
|
+
}
|
|
158
|
+
return [items[0]];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let idx = 0;
|
|
162
|
+
const selected = new Set();
|
|
163
|
+
if (defaultSelected !== null) {
|
|
164
|
+
for (const i of defaultSelected) selected.add(i);
|
|
165
|
+
} else if (items.length >= 2) {
|
|
166
|
+
selected.add(0);
|
|
167
|
+
selected.add(1);
|
|
168
|
+
} else if (items.length === 1) {
|
|
169
|
+
selected.add(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const render = () => {
|
|
173
|
+
output.write('\x1b[?25l');
|
|
174
|
+
output.write(`\n ${bold(question)} ${dim('(Space=toggle, A=all, Enter=confirm)')}\n`);
|
|
175
|
+
for (let i = 0; i < items.length; i++) {
|
|
176
|
+
const pointer = i === idx ? green('❯') : ' ';
|
|
177
|
+
const check = selected.has(i) ? green('◉') : dim('○');
|
|
178
|
+
const label = i === idx ? bold(itemLabel(items[i])) : itemLabel(items[i]);
|
|
179
|
+
output.write(` ${pointer} ${check} ${label}\n`);
|
|
180
|
+
}
|
|
181
|
+
if (selected.size === 0) {
|
|
182
|
+
output.write(dim(' ↑ Select at least one, then press Enter\n'));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const clear = () => {
|
|
187
|
+
const extra = selected.size === 0 ? 1 : 0;
|
|
188
|
+
const lines = items.length + 2 + extra;
|
|
189
|
+
output.write(`\x1b[${lines}A`);
|
|
190
|
+
for (let i = 0; i < lines; i++) {
|
|
191
|
+
output.write('\x1b[2K\n');
|
|
192
|
+
}
|
|
193
|
+
output.write(`\x1b[${lines}A`);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
render();
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
input.setRawMode(true);
|
|
200
|
+
input.resume();
|
|
201
|
+
|
|
202
|
+
const onData = (data) => {
|
|
203
|
+
try {
|
|
204
|
+
const bytes = [...data];
|
|
205
|
+
const key = data.toString();
|
|
206
|
+
|
|
207
|
+
const isUp =
|
|
208
|
+
key === '\x1b[A' ||
|
|
209
|
+
(bytes.length === 2 && (bytes[0] === 0xe0 || bytes[0] === 0x00) && bytes[1] === 0x48);
|
|
210
|
+
const isDown =
|
|
211
|
+
key === '\x1b[B' ||
|
|
212
|
+
(bytes.length === 2 && (bytes[0] === 0xe0 || bytes[0] === 0x00) && bytes[1] === 0x50);
|
|
213
|
+
const isSpace = key === ' ';
|
|
214
|
+
const isEnter = key === '\r' || key === '\n';
|
|
215
|
+
const isCtrlC = key === '\x03';
|
|
216
|
+
const isA = key === 'a' || key === 'A';
|
|
217
|
+
|
|
218
|
+
if (isCtrlC) {
|
|
219
|
+
restoreTerminal();
|
|
220
|
+
process.exit(130);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isUp) {
|
|
224
|
+
idx = (idx - 1 + items.length) % items.length;
|
|
225
|
+
clear();
|
|
226
|
+
render();
|
|
227
|
+
} else if (isDown) {
|
|
228
|
+
idx = (idx + 1) % items.length;
|
|
229
|
+
clear();
|
|
230
|
+
render();
|
|
231
|
+
} else if (isSpace) {
|
|
232
|
+
if (selected.has(idx)) selected.delete(idx);
|
|
233
|
+
else selected.add(idx);
|
|
234
|
+
clear();
|
|
235
|
+
render();
|
|
236
|
+
} else if (isA) {
|
|
237
|
+
if (selected.size === items.length) {
|
|
238
|
+
selected.clear();
|
|
239
|
+
} else {
|
|
240
|
+
for (let i = 0; i < items.length; i++) selected.add(i);
|
|
241
|
+
}
|
|
242
|
+
clear();
|
|
243
|
+
render();
|
|
244
|
+
} else if (isEnter && selected.size > 0) {
|
|
245
|
+
restoreTerminal();
|
|
246
|
+
input.removeListener('data', onData);
|
|
247
|
+
clear();
|
|
248
|
+
const labels = [...selected]
|
|
249
|
+
.sort((a, b) => a - b)
|
|
250
|
+
.map((i) => {
|
|
251
|
+
const it = items[i];
|
|
252
|
+
return typeof it === 'string' ? it : it.label;
|
|
253
|
+
})
|
|
254
|
+
.join(', ');
|
|
255
|
+
output.write(` ${bold(question)} ${green(labels)}\n`);
|
|
256
|
+
resolve([...selected].sort((a, b) => a - b).map((i) => items[i]));
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
restoreTerminal();
|
|
260
|
+
input.removeListener('data', onData);
|
|
261
|
+
reject(err);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
input.on('data', onData);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { DEFAULTS, PLATFORMS, STACKS } from '../constants.js';
|
|
3
|
+
import { detectProject } from '../core/detector.js';
|
|
4
|
+
import { writeFiles } from '../core/writer.js';
|
|
5
|
+
import { getSharedFiles, getPlatform } from '../platforms/registry.js';
|
|
6
|
+
import '../platforms/claude-code.js';
|
|
7
|
+
import '../platforms/cursor.js';
|
|
8
|
+
import { bold, dim, green, red, yellow } from '../cli/logger.js';
|
|
9
|
+
import { selectMulti, textPrompt } from '../cli/prompts.js';
|
|
10
|
+
import { installVendors } from '../core/vendors.js';
|
|
11
|
+
import { ensureVendorDirGitignored } from '../core/gitignore-vendor.js';
|
|
12
|
+
|
|
13
|
+
const STACK_KEYS = new Set(STACKS.map((s) => s.key));
|
|
14
|
+
const PLATFORM_KEYS = new Set(PLATFORMS.map((p) => p.key));
|
|
15
|
+
|
|
16
|
+
/** @param {string|null|undefined} s */
|
|
17
|
+
function parseList(s) {
|
|
18
|
+
if (s == null || s === '') return [];
|
|
19
|
+
return s
|
|
20
|
+
.split(',')
|
|
21
|
+
.map((x) => x.trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {string|null|undefined} flag */
|
|
26
|
+
function parseStacksFlag(flag) {
|
|
27
|
+
return parseList(flag).filter((k) => {
|
|
28
|
+
if (!STACK_KEYS.has(k)) {
|
|
29
|
+
throw new Error(`Unknown stack key: ${k}. Valid: ${[...STACK_KEYS].join(', ')}`);
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {string|null|undefined} flag */
|
|
36
|
+
function parsePlatformsFlag(flag) {
|
|
37
|
+
const keys = parseList(flag);
|
|
38
|
+
if (keys.length === 0) return ['claude', 'cursor'];
|
|
39
|
+
for (const k of keys) {
|
|
40
|
+
if (!PLATFORM_KEYS.has(k)) {
|
|
41
|
+
throw new Error(`Unknown platform: ${k}. Valid: ${[...PLATFORM_KEYS].join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return keys;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @param {string|null} language */
|
|
48
|
+
function fallbackStacks(language) {
|
|
49
|
+
if (language === 'TypeScript' || language === 'JavaScript') return ['ts'];
|
|
50
|
+
if (language === 'Python') return ['python'];
|
|
51
|
+
if (language === 'Go') return ['go'];
|
|
52
|
+
if (language === 'Dart') return ['flutter'];
|
|
53
|
+
return ['ts'];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @param {Awaited<ReturnType<typeof detectProject>>} detected */
|
|
57
|
+
function buildConfig(detected, stacks) {
|
|
58
|
+
return {
|
|
59
|
+
projectName: detected.name ?? DEFAULTS.projectName,
|
|
60
|
+
language: detected.language ?? DEFAULTS.language,
|
|
61
|
+
framework: detected.framework ?? DEFAULTS.framework,
|
|
62
|
+
testCmd: detected.testCmd ?? DEFAULTS.testCmd,
|
|
63
|
+
lintCmd: detected.lintCmd ?? DEFAULTS.lintCmd,
|
|
64
|
+
buildCmd: detected.buildCmd ?? DEFAULTS.buildCmd,
|
|
65
|
+
database: detected.database ?? DEFAULTS.database,
|
|
66
|
+
stacks,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatDetectedLine(detected) {
|
|
71
|
+
const stackStr =
|
|
72
|
+
detected.detectedStack.length > 0 ? detected.detectedStack.join(' + ') : 'unknown stack';
|
|
73
|
+
const name = detected.name ?? DEFAULTS.projectName;
|
|
74
|
+
return `${stackStr} project "${name}"`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {object} InitFlags
|
|
79
|
+
* @property {boolean} [yes]
|
|
80
|
+
* @property {boolean} [force]
|
|
81
|
+
* @property {boolean} [skipVendor]
|
|
82
|
+
* @property {boolean} [vendorOnly]
|
|
83
|
+
* @property {string|null} [stack]
|
|
84
|
+
* @property {string|null} [platforms]
|
|
85
|
+
* @property {string|null} [superpowersRef]
|
|
86
|
+
* @property {string|null} [agencyRef]
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {InitFlags & Record<string, unknown>} flags
|
|
91
|
+
*/
|
|
92
|
+
function resolveVendorRefs(flags) {
|
|
93
|
+
return {
|
|
94
|
+
superpowersRef:
|
|
95
|
+
flags.superpowersRef != null && flags.superpowersRef !== ''
|
|
96
|
+
? String(flags.superpowersRef)
|
|
97
|
+
: 'main',
|
|
98
|
+
agencyRef:
|
|
99
|
+
flags.agencyRef != null && flags.agencyRef !== '' ? String(flags.agencyRef) : 'main',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string} cwd
|
|
105
|
+
* @param {string[]} platformKeys
|
|
106
|
+
* @param {InitFlags & Record<string, unknown>} flags
|
|
107
|
+
*/
|
|
108
|
+
async function executeVendorInstall(cwd, platformKeys, flags) {
|
|
109
|
+
const { superpowersRef, agencyRef } = resolveVendorRefs(flags);
|
|
110
|
+
const vendorLog = await installVendors(cwd, {
|
|
111
|
+
platformKeys,
|
|
112
|
+
force: Boolean(flags.force),
|
|
113
|
+
superpowersRef,
|
|
114
|
+
agencyRef,
|
|
115
|
+
});
|
|
116
|
+
for (const line of vendorLog) {
|
|
117
|
+
console.log(` ${line}`);
|
|
118
|
+
}
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(green('✅ Vendor install complete.'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printAfterFullInit() {
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(bold('Next steps:'));
|
|
126
|
+
console.log(' 1. Review docs/ARCHITECTURE.md — fill in your system design');
|
|
127
|
+
console.log(' 2. Review .ai/rules.md — adjust coding preferences if needed');
|
|
128
|
+
console.log(' 3. Commit scaffold files to your repo (see README for team workflow and vendor/ options)');
|
|
129
|
+
console.log(' 4. Open in Claude Code or Cursor — Superpowers + Agency are wired in-project');
|
|
130
|
+
console.log('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function printAfterVendorOnly() {
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(bold('Next steps:'));
|
|
136
|
+
console.log(' 1. Confirm vendor/superpowers and vendor/agency-agents exist');
|
|
137
|
+
console.log(' 2. Open in Claude Code or Cursor');
|
|
138
|
+
console.log(' 3. Pin refs in your npm script for reproducible clones (see README)');
|
|
139
|
+
console.log('');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} cwd
|
|
144
|
+
*/
|
|
145
|
+
async function syncVendorGitignore(cwd) {
|
|
146
|
+
const r = await ensureVendorDirGitignored(cwd, {
|
|
147
|
+
onWarn: (msg) => console.log(` ${yellow('!')} ${msg}`),
|
|
148
|
+
});
|
|
149
|
+
if (r.action === 'error') return;
|
|
150
|
+
if (r.action === 'noop_already_ignored' || r.action === 'noop_managed_ok') {
|
|
151
|
+
console.log(` ${dim('o')} .gitignore (already ignores vendor/)`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.log(` ${green('+')} .gitignore (vendor/ ignored)`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** @param {InitFlags & Record<string, unknown>} flags */
|
|
158
|
+
export async function initCommand(flags) {
|
|
159
|
+
const cwd = process.cwd();
|
|
160
|
+
|
|
161
|
+
if (flags.vendorOnly && flags.skipVendor) {
|
|
162
|
+
throw new Error('Cannot use --skip-vendor together with --vendor-only');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (flags.vendorOnly) {
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(bold('🚀 AI Dev Setup — vendor only'));
|
|
168
|
+
console.log('');
|
|
169
|
+
const platformKeys = parsePlatformsFlag(flags.platforms ?? null);
|
|
170
|
+
console.log(dim('📦 Vendoring Superpowers + Agency Agents (git + bash required)...'));
|
|
171
|
+
try {
|
|
172
|
+
await executeVendorInstall(cwd, platformKeys, flags);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(red(`Vendor install failed: ${msg}`));
|
|
177
|
+
console.log(dim(' Fix git/bash/network, or clone manually (see README).'));
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(dim('📌 .gitignore'));
|
|
182
|
+
await syncVendorGitignore(cwd);
|
|
183
|
+
printAfterVendorOnly();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const detected = await detectProject(cwd);
|
|
188
|
+
const fromFlag = parseStacksFlag(flags.stack ?? null);
|
|
189
|
+
let stacks = [...new Set([...detected.detectedStack, ...fromFlag])];
|
|
190
|
+
if (stacks.length === 0) {
|
|
191
|
+
stacks = fallbackStacks(detected.language ?? DEFAULTS.language);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log(bold('🚀 AI Dev Setup — Universal Configuration'));
|
|
196
|
+
console.log('');
|
|
197
|
+
|
|
198
|
+
/** @type {Record<string, unknown>} */
|
|
199
|
+
let config;
|
|
200
|
+
|
|
201
|
+
if (flags.yes) {
|
|
202
|
+
const platformKeys = parsePlatformsFlag(flags.platforms ?? null);
|
|
203
|
+
config = { ...buildConfig(detected, stacks) };
|
|
204
|
+
await runGenerate(cwd, config, platformKeys, flags);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(dim(`Detected: ${formatDetectedLine(detected)}`));
|
|
209
|
+
console.log('');
|
|
210
|
+
|
|
211
|
+
const projectName = await textPrompt('? Project name', detected.name ?? DEFAULTS.projectName);
|
|
212
|
+
const language = await textPrompt('? Language', detected.language ?? DEFAULTS.language);
|
|
213
|
+
const framework = await textPrompt('? Framework', detected.framework ?? DEFAULTS.framework);
|
|
214
|
+
const testCmd = await textPrompt('? Test command', detected.testCmd ?? DEFAULTS.testCmd);
|
|
215
|
+
const lintCmd = await textPrompt('? Lint command', detected.lintCmd ?? DEFAULTS.lintCmd);
|
|
216
|
+
const buildCmd = await textPrompt('? Build command', detected.buildCmd ?? DEFAULTS.buildCmd);
|
|
217
|
+
const database = await textPrompt('? Database', detected.database ?? DEFAULTS.database);
|
|
218
|
+
|
|
219
|
+
const selected = await selectMulti(
|
|
220
|
+
'? Select platforms',
|
|
221
|
+
PLATFORMS,
|
|
222
|
+
PLATFORMS.length >= 2 ? [0, 1] : [0],
|
|
223
|
+
);
|
|
224
|
+
const platformKeys = selected.map((p) => p.key);
|
|
225
|
+
|
|
226
|
+
const interactiveStacks =
|
|
227
|
+
stacks.length > 0 ? stacks : fallbackStacks(language || DEFAULTS.language);
|
|
228
|
+
|
|
229
|
+
config = {
|
|
230
|
+
projectName,
|
|
231
|
+
language,
|
|
232
|
+
framework,
|
|
233
|
+
testCmd,
|
|
234
|
+
lintCmd,
|
|
235
|
+
buildCmd,
|
|
236
|
+
database,
|
|
237
|
+
stacks: interactiveStacks,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
await runGenerate(cwd, config, platformKeys, flags);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {string} cwd
|
|
245
|
+
* @param {Record<string, unknown>} config
|
|
246
|
+
* @param {string[]} platformKeys
|
|
247
|
+
* @param {InitFlags & Record<string, unknown>} flags
|
|
248
|
+
*/
|
|
249
|
+
async function runGenerate(cwd, config, platformKeys, flags) {
|
|
250
|
+
const force = Boolean(flags.force);
|
|
251
|
+
const skipVendor = Boolean(flags.skipVendor);
|
|
252
|
+
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(dim('📝 Generating files...'));
|
|
255
|
+
|
|
256
|
+
const files = await collectFiles(config, platformKeys);
|
|
257
|
+
const results = await writeFiles(files, { cwd, force });
|
|
258
|
+
|
|
259
|
+
for (const r of results) {
|
|
260
|
+
if (r.status === 'written') {
|
|
261
|
+
console.log(` ${green('+')} ${r.path}`);
|
|
262
|
+
} else if (r.status === 'skipped') {
|
|
263
|
+
console.log(` ${dim('o')} ${r.path} (skipped)`);
|
|
264
|
+
} else {
|
|
265
|
+
console.log(` ! ${r.path} — ${r.error ?? 'error'}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const written = results.filter((r) => r.status === 'written').length;
|
|
270
|
+
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
271
|
+
const errors = results.filter((r) => r.status === 'error').length;
|
|
272
|
+
|
|
273
|
+
console.log('');
|
|
274
|
+
if (errors === 0) {
|
|
275
|
+
console.log(green(`✅ Templates: ${written} files written, ${skipped} skipped.`));
|
|
276
|
+
} else {
|
|
277
|
+
console.log(`Templates completed with errors: ${written} written, ${skipped} skipped, ${errors} failed.`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log('');
|
|
281
|
+
console.log(dim('📌 .gitignore'));
|
|
282
|
+
await syncVendorGitignore(cwd);
|
|
283
|
+
|
|
284
|
+
if (!skipVendor && errors === 0) {
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(dim('📦 Vendoring Superpowers + Agency Agents (git + bash required)...'));
|
|
287
|
+
try {
|
|
288
|
+
await executeVendorInstall(cwd, platformKeys, flags);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
291
|
+
console.log('');
|
|
292
|
+
console.log(red(`Vendor install failed: ${msg}`));
|
|
293
|
+
console.log(dim(' Fix git/bash/network, or re-run with --skip-vendor and clone manually (see README).'));
|
|
294
|
+
throw e;
|
|
295
|
+
}
|
|
296
|
+
} else if (skipVendor) {
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(dim('⏭️ Skipped vendor install (--skip-vendor). Run `npx ai-dev-setup init --vendor-only` after clone, or clone manually (see README).'));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
printAfterFullInit();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {Record<string, unknown>} config
|
|
306
|
+
* @param {string[]} platformKeys
|
|
307
|
+
*/
|
|
308
|
+
async function collectFiles(config, platformKeys) {
|
|
309
|
+
/** @type {Array<{ path: string, content: string }>} */
|
|
310
|
+
const all = [];
|
|
311
|
+
all.push(...(await getSharedFiles(config)));
|
|
312
|
+
|
|
313
|
+
for (const key of platformKeys) {
|
|
314
|
+
const p = getPlatform(key);
|
|
315
|
+
if (!p) {
|
|
316
|
+
throw new Error(`Platform not registered: ${key}`);
|
|
317
|
+
}
|
|
318
|
+
all.push(...(await p.getFiles(config)));
|
|
319
|
+
}
|
|
320
|
+
return all;
|
|
321
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
6
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
7
|
+
|
|
8
|
+
export const VERSION = pkg.version;
|
|
9
|
+
|
|
10
|
+
export const PLATFORMS = [
|
|
11
|
+
{ key: 'claude', label: 'Claude Code', desc: 'CLAUDE.md + .claude/' },
|
|
12
|
+
{ key: 'cursor', label: 'Cursor', desc: '.cursorrules + .cursor/rules/' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export const STACKS = [
|
|
16
|
+
{ key: 'ts', label: 'TypeScript (generic)' },
|
|
17
|
+
{ key: 'react', label: 'React' },
|
|
18
|
+
{ key: 'nextjs', label: 'Next.js' },
|
|
19
|
+
{ key: 'node', label: 'Node.js API' },
|
|
20
|
+
{ key: 'nestjs', label: 'NestJS' },
|
|
21
|
+
{ key: 'python', label: 'Python' },
|
|
22
|
+
{ key: 'go', label: 'Go (Golang)' },
|
|
23
|
+
{ key: 'flutter', label: 'Flutter (Dart)' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const DEFAULTS = {
|
|
27
|
+
projectName: 'my-project',
|
|
28
|
+
language: 'TypeScript',
|
|
29
|
+
framework: 'Next.js',
|
|
30
|
+
testCmd: 'npm test',
|
|
31
|
+
lintCmd: 'npm run lint',
|
|
32
|
+
buildCmd: 'npm run build',
|
|
33
|
+
database: 'PostgreSQL',
|
|
34
|
+
};
|