ai-sprint-kit 2.0.4 ā 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -14
- package/bin/ai-sprint.js +439 -8
- package/lib/ci-generator.js +248 -0
- package/lib/interactive.js +249 -0
- package/lib/recovery.js +279 -0
- package/lib/updater.js +340 -0
- package/lib/validator.js +264 -0
- package/package.json +3 -2
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const fs = require('fs/promises');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate GitHub Actions workflow
|
|
6
|
+
*/
|
|
7
|
+
async function generateGitHubActions(targetDir) {
|
|
8
|
+
const workflowDir = path.join(targetDir, '.github/workflows');
|
|
9
|
+
const workflowPath = path.join(workflowDir, 'ai-sprint-validate.yml');
|
|
10
|
+
|
|
11
|
+
const workflow = `name: AI Sprint Validation
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
pull_request:
|
|
15
|
+
branches: [main, develop]
|
|
16
|
+
push:
|
|
17
|
+
branches: [main, develop]
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
validate:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout code
|
|
25
|
+
uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: Setup Node.js
|
|
28
|
+
uses: actions/setup-node@v4
|
|
29
|
+
with:
|
|
30
|
+
node-version: '18'
|
|
31
|
+
|
|
32
|
+
- name: Install AI Sprint Kit CLI
|
|
33
|
+
run: npm install -g ai-sprint-kit
|
|
34
|
+
|
|
35
|
+
- name: Validate Installation
|
|
36
|
+
run: ai-sprint validate
|
|
37
|
+
|
|
38
|
+
- name: Run Diagnostics
|
|
39
|
+
run: ai-sprint doctor
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
await fs.mkdir(workflowDir, { recursive: true });
|
|
43
|
+
await fs.writeFile(workflowPath, workflow, 'utf8');
|
|
44
|
+
|
|
45
|
+
return workflowPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate GitLab CI configuration
|
|
50
|
+
*/
|
|
51
|
+
async function generateGitLabCI(targetDir) {
|
|
52
|
+
const gitlabCIPath = path.join(targetDir, '.gitlab-ci.yml');
|
|
53
|
+
|
|
54
|
+
const gitlabCI = `stages:
|
|
55
|
+
- validate
|
|
56
|
+
|
|
57
|
+
variables:
|
|
58
|
+
NODE_VERSION: "18"
|
|
59
|
+
|
|
60
|
+
ai-sprint-validate:
|
|
61
|
+
stage: validate
|
|
62
|
+
image: node:18
|
|
63
|
+
|
|
64
|
+
before_script:
|
|
65
|
+
- npm install -g ai-sprint-kit
|
|
66
|
+
|
|
67
|
+
script:
|
|
68
|
+
- ai-sprint validate
|
|
69
|
+
- ai-sprint doctor
|
|
70
|
+
|
|
71
|
+
only:
|
|
72
|
+
- merge_requests
|
|
73
|
+
- main
|
|
74
|
+
- develop
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
await fs.writeFile(gitlabCIPath, gitlabCI, 'utf8');
|
|
78
|
+
|
|
79
|
+
return gitlabCIPath;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate Bitbucket Pipelines configuration
|
|
84
|
+
*/
|
|
85
|
+
async function generateBitbucketPipelines(targetDir) {
|
|
86
|
+
const pipelinesPath = path.join(targetDir, 'bitbucket-pipelines.yml');
|
|
87
|
+
|
|
88
|
+
const pipelines = `image: node:18
|
|
89
|
+
|
|
90
|
+
pipelines:
|
|
91
|
+
pull-requests:
|
|
92
|
+
'**':
|
|
93
|
+
- step:
|
|
94
|
+
name: AI Sprint Validation
|
|
95
|
+
script:
|
|
96
|
+
- npm install -g ai-sprint-kit
|
|
97
|
+
- ai-sprint validate
|
|
98
|
+
- ai-sprint doctor
|
|
99
|
+
|
|
100
|
+
branches:
|
|
101
|
+
main:
|
|
102
|
+
- step:
|
|
103
|
+
name: AI Sprint Validation
|
|
104
|
+
script:
|
|
105
|
+
- npm install -g ai-sprint-kit
|
|
106
|
+
- ai-sprint validate
|
|
107
|
+
- ai-sprint doctor
|
|
108
|
+
develop:
|
|
109
|
+
- step:
|
|
110
|
+
name: AI Sprint Validation
|
|
111
|
+
script:
|
|
112
|
+
- npm install -g ai-sprint-kit
|
|
113
|
+
- ai-sprint validate
|
|
114
|
+
- ai-sprint doctor
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
await fs.writeFile(pipelinesPath, pipelines, 'utf8');
|
|
118
|
+
|
|
119
|
+
return pipelinesPath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect which CI platform is being used
|
|
124
|
+
*/
|
|
125
|
+
async function detectCIPlatform(targetDir) {
|
|
126
|
+
// Check for GitHub Actions
|
|
127
|
+
const githubDir = path.join(targetDir, '.github');
|
|
128
|
+
try {
|
|
129
|
+
await fs.access(githubDir);
|
|
130
|
+
return 'github';
|
|
131
|
+
} catch {}
|
|
132
|
+
|
|
133
|
+
// Check for GitLab CI
|
|
134
|
+
const gitlabCI = path.join(targetDir, '.gitlab-ci.yml');
|
|
135
|
+
try {
|
|
136
|
+
await fs.access(gitlabCI);
|
|
137
|
+
return 'gitlab';
|
|
138
|
+
} catch {}
|
|
139
|
+
|
|
140
|
+
// Check for Bitbucket Pipelines
|
|
141
|
+
const bitbucketCI = path.join(targetDir, 'bitbucket-pipelines.yml');
|
|
142
|
+
try {
|
|
143
|
+
await fs.access(bitbucketCI);
|
|
144
|
+
return 'bitbucket';
|
|
145
|
+
} catch {}
|
|
146
|
+
|
|
147
|
+
// No CI detected
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Initialize CI configuration
|
|
153
|
+
*/
|
|
154
|
+
async function initCI(targetDir, platform) {
|
|
155
|
+
const detected = await detectCIPlatform(targetDir);
|
|
156
|
+
const selectedPlatform = platform || detected || 'github';
|
|
157
|
+
|
|
158
|
+
let generatedPath;
|
|
159
|
+
|
|
160
|
+
switch (selectedPlatform) {
|
|
161
|
+
case 'github':
|
|
162
|
+
generatedPath = await generateGitHubActions(targetDir);
|
|
163
|
+
break;
|
|
164
|
+
case 'gitlab':
|
|
165
|
+
generatedPath = await generateGitLabCI(targetDir);
|
|
166
|
+
break;
|
|
167
|
+
case 'bitbucket':
|
|
168
|
+
generatedPath = await generateBitbucketPipelines(targetDir);
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
throw new Error(`Unsupported platform: ${selectedPlatform}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
platform: selectedPlatform,
|
|
176
|
+
path: generatedPath,
|
|
177
|
+
detected: detected || 'none'
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Run CI validation (headless mode for CI systems)
|
|
183
|
+
*/
|
|
184
|
+
async function runCIValidation(targetDir) {
|
|
185
|
+
const { validateInstallation } = require('./validator');
|
|
186
|
+
|
|
187
|
+
const result = await validateInstallation(targetDir);
|
|
188
|
+
|
|
189
|
+
// Return CI-friendly output
|
|
190
|
+
return {
|
|
191
|
+
success: result.isValid(),
|
|
192
|
+
summary: result.summary(),
|
|
193
|
+
passed: result.passed,
|
|
194
|
+
failed: result.failed,
|
|
195
|
+
warnings: result.warnings,
|
|
196
|
+
// Exit code for CI
|
|
197
|
+
exitCode: result.isValid() ? 0 : 1
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Format CI validation output for logging
|
|
203
|
+
*/
|
|
204
|
+
function formatCIOutput(ciResult) {
|
|
205
|
+
const lines = [];
|
|
206
|
+
|
|
207
|
+
lines.push('=== AI Sprint CI Validation ===\n');
|
|
208
|
+
|
|
209
|
+
if (ciResult.success) {
|
|
210
|
+
lines.push(`ā
PASSED: ${ciResult.summary.passed} checks passed`);
|
|
211
|
+
} else {
|
|
212
|
+
lines.push(`ā FAILED: ${ciResult.summary.failed} checks failed`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (ciResult.summary.warnings > 0) {
|
|
216
|
+
lines.push(`ā ļø WARNINGS: ${ciResult.summary.warnings} warnings`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
lines.push(`\nTotal: ${ciResult.summary.total} checks`);
|
|
220
|
+
|
|
221
|
+
if (!ciResult.success && ciResult.failed.length > 0) {
|
|
222
|
+
lines.push('\nFailed checks:');
|
|
223
|
+
ciResult.failed.forEach(({ check, suggestion }) => {
|
|
224
|
+
lines.push(` ā ${check}`);
|
|
225
|
+
lines.push(` ā ${suggestion}`);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (ciResult.warnings.length > 0) {
|
|
230
|
+
lines.push('\nWarnings:');
|
|
231
|
+
ciResult.warnings.forEach(({ check, message }) => {
|
|
232
|
+
lines.push(` ! ${check}`);
|
|
233
|
+
lines.push(` ā ${message}`);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return lines.join('\n');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
generateGitHubActions,
|
|
242
|
+
generateGitLabCI,
|
|
243
|
+
generateBitbucketPipelines,
|
|
244
|
+
detectCIPlatform,
|
|
245
|
+
initCI,
|
|
246
|
+
runCIValidation,
|
|
247
|
+
formatCIOutput
|
|
248
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create readline interface for interactive prompts
|
|
6
|
+
*/
|
|
7
|
+
function createReadline() {
|
|
8
|
+
return readline.createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Prompt user with a yes/no question
|
|
16
|
+
*/
|
|
17
|
+
function promptQuestion(rl, question) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
rl.question(`${question} (y/n): `, (answer) => {
|
|
20
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Prompt user to select from options
|
|
27
|
+
*/
|
|
28
|
+
function promptChoice(rl, question, options) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
console.log(chalk.cyan(`\n${question}`));
|
|
31
|
+
options.forEach((opt, i) => {
|
|
32
|
+
console.log(chalk.gray(` ${i + 1}. ${opt.label}`));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
rl.question(chalk.cyan('\nSelect option (number): '), (answer) => {
|
|
36
|
+
const index = parseInt(answer) - 1;
|
|
37
|
+
if (index >= 0 && index < options.length) {
|
|
38
|
+
resolve(options[index].value);
|
|
39
|
+
} else {
|
|
40
|
+
resolve(options[0].value); // Default to first option
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Interactive installation mode
|
|
48
|
+
*/
|
|
49
|
+
async function interactiveInstall() {
|
|
50
|
+
const rl = createReadline();
|
|
51
|
+
|
|
52
|
+
console.log(chalk.blue.bold('\nšÆ Interactive Installation\n'));
|
|
53
|
+
console.log(chalk.gray('Customize your AI Sprint Kit setup:\n'));
|
|
54
|
+
|
|
55
|
+
const preferences = {
|
|
56
|
+
includeHooks: true,
|
|
57
|
+
includeBrowserTesting: false,
|
|
58
|
+
ciPlatform: 'none'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Ask about hooks
|
|
62
|
+
preferences.includeHooks = await promptQuestion(
|
|
63
|
+
rl,
|
|
64
|
+
chalk.yellow('Include automation hooks? (auto-format, lint-check)')
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Ask about browser testing
|
|
68
|
+
preferences.includeBrowserTesting = await promptQuestion(
|
|
69
|
+
rl,
|
|
70
|
+
chalk.yellow('Include browser testing infrastructure?')
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Ask about CI platform
|
|
74
|
+
console.log(chalk.cyan('\nCI/CD Platform:'));
|
|
75
|
+
console.log(chalk.gray(' Choose a platform for automated validation\n'));
|
|
76
|
+
|
|
77
|
+
const ciOptions = [
|
|
78
|
+
{ label: 'None (skip CI setup)', value: 'none' },
|
|
79
|
+
{ label: 'GitHub Actions', value: 'github' },
|
|
80
|
+
{ label: 'GitLab CI', value: 'gitlab' },
|
|
81
|
+
{ label: 'Bitbucket Pipelines', value: 'bitbucket' }
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
preferences.ciPlatform = await promptChoice(
|
|
85
|
+
rl,
|
|
86
|
+
'Which CI/CD platform do you use?',
|
|
87
|
+
ciOptions
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
rl.close();
|
|
91
|
+
|
|
92
|
+
return preferences;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Show dry-run preview of what will be installed
|
|
97
|
+
*/
|
|
98
|
+
async function dryRunPreview(targetDir) {
|
|
99
|
+
const path = require('path');
|
|
100
|
+
const fs = require('fs/promises');
|
|
101
|
+
|
|
102
|
+
console.log(chalk.blue.bold('\nš Dry Run - Installation Preview\n'));
|
|
103
|
+
console.log(chalk.cyan('Target directory:'));
|
|
104
|
+
console.log(chalk.gray(` ${targetDir}\n`));
|
|
105
|
+
|
|
106
|
+
console.log(chalk.cyan('Files that will be created:\n'));
|
|
107
|
+
|
|
108
|
+
const filesToCreate = [
|
|
109
|
+
{ path: '.claude/settings.json', desc: 'Claude Code configuration' },
|
|
110
|
+
{ path: '.claude/.env.example', desc: 'Environment variables template' },
|
|
111
|
+
{ path: '.claude/agents/', desc: '9 agent files (planner, implementer, tester, etc.)' },
|
|
112
|
+
{ path: '.claude/commands/', desc: '13 command files (/plan, /code, /test, etc.)' },
|
|
113
|
+
{ path: '.claude/workflows/', desc: 'Development workflow rules' },
|
|
114
|
+
{ path: '.claude/hooks/', desc: 'Automation hooks (optional)' },
|
|
115
|
+
{ path: '.claude/skills/', desc: 'Python scripts for advanced features' },
|
|
116
|
+
{ path: 'ai_context/', desc: 'Context memory directories' },
|
|
117
|
+
{ path: 'CLAUDE.md', desc: 'Framework documentation' },
|
|
118
|
+
{ path: '.claude/docs/', desc: 'Additional documentation' }
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
filesToCreate.forEach(file => {
|
|
122
|
+
const exists = file.path.endsWith('/')
|
|
123
|
+
? checkDirExists(targetDir, file.path)
|
|
124
|
+
: checkFileExists(targetDir, file.path);
|
|
125
|
+
|
|
126
|
+
const status = exists
|
|
127
|
+
? chalk.yellow('ā OVERWRITE')
|
|
128
|
+
: chalk.green('ā CREATE');
|
|
129
|
+
|
|
130
|
+
console.log(` ${status} ${chalk.cyan(file.path.padEnd(30))} ${chalk.gray(file.desc)}`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log(chalk.cyan('\nSummary:'));
|
|
134
|
+
console.log(chalk.gray(` ⢠${filesToCreate.length} locations will be created/modified`));
|
|
135
|
+
console.log(chalk.gray(` ⢠Target: ${targetDir}`));
|
|
136
|
+
console.log(chalk.gray(` ⢠Run without --dry-run to proceed\n`));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if directory exists
|
|
141
|
+
*/
|
|
142
|
+
async function checkDirExists(targetDir, dirPath) {
|
|
143
|
+
const fs = require('fs/promises');
|
|
144
|
+
const path = require('path');
|
|
145
|
+
try {
|
|
146
|
+
await fs.access(path.join(targetDir, dirPath));
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if file exists
|
|
155
|
+
*/
|
|
156
|
+
async function checkFileExists(targetDir, filePath) {
|
|
157
|
+
const fs = require('fs/promises');
|
|
158
|
+
const path = require('path');
|
|
159
|
+
try {
|
|
160
|
+
await fs.access(path.join(targetDir, filePath));
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Show installation progress with detailed steps
|
|
169
|
+
*/
|
|
170
|
+
class InstallProgress {
|
|
171
|
+
constructor() {
|
|
172
|
+
this.steps = [];
|
|
173
|
+
this.currentStep = 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
addStep(name, description) {
|
|
177
|
+
this.steps.push({ name, description, status: 'pending' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
startStep(stepIndex) {
|
|
181
|
+
this.currentStep = stepIndex;
|
|
182
|
+
this.steps[stepIndex].status = 'running';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
completeStep(stepIndex) {
|
|
186
|
+
this.steps[stepIndex].status = 'complete';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
failStep(stepIndex, error) {
|
|
190
|
+
this.steps[stepIndex].status = 'failed';
|
|
191
|
+
this.steps[stepIndex].error = error;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
render() {
|
|
195
|
+
const lines = [];
|
|
196
|
+
lines.push(chalk.cyan('\nš¦ Installation Progress:\n'));
|
|
197
|
+
|
|
198
|
+
this.steps.forEach((step, index) => {
|
|
199
|
+
const isCurrent = index === this.currentStep;
|
|
200
|
+
|
|
201
|
+
let status;
|
|
202
|
+
switch (step.status) {
|
|
203
|
+
case 'complete':
|
|
204
|
+
status = chalk.green('ā');
|
|
205
|
+
break;
|
|
206
|
+
case 'failed':
|
|
207
|
+
status = chalk.red('ā');
|
|
208
|
+
break;
|
|
209
|
+
case 'running':
|
|
210
|
+
status = chalk.yellow('ā³');
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
status = chalk.gray('ā');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const name = isCurrent ? chalk.bold(step.name) : step.name;
|
|
217
|
+
lines.push(` ${status} ${name}`);
|
|
218
|
+
|
|
219
|
+
if (step.description && step.status !== 'complete') {
|
|
220
|
+
lines.push(chalk.gray(` ${step.description}`));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (step.error) {
|
|
224
|
+
lines.push(chalk.red(` Error: ${step.error}`));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
lines.push('');
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getProgress() {
|
|
233
|
+
const complete = this.steps.filter(s => s.status === 'complete').length;
|
|
234
|
+
return {
|
|
235
|
+
current: complete,
|
|
236
|
+
total: this.steps.length,
|
|
237
|
+
percent: Math.round((complete / this.steps.length) * 100)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
createReadline,
|
|
244
|
+
promptQuestion,
|
|
245
|
+
promptChoice,
|
|
246
|
+
interactiveInstall,
|
|
247
|
+
dryRunPreview,
|
|
248
|
+
InstallProgress
|
|
249
|
+
};
|