claude-cli-advanced-starter-pack 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/OVERVIEW.md +597 -0
- package/README.md +439 -0
- package/bin/gtask.js +282 -0
- package/bin/postinstall.js +53 -0
- package/package.json +69 -0
- package/src/agents/phase-dev-templates.js +1011 -0
- package/src/agents/templates.js +668 -0
- package/src/analysis/checklist-parser.js +414 -0
- package/src/analysis/codebase.js +481 -0
- package/src/cli/menu.js +958 -0
- package/src/commands/claude-audit.js +1482 -0
- package/src/commands/claude-settings.js +2243 -0
- package/src/commands/create-agent.js +681 -0
- package/src/commands/create-command.js +337 -0
- package/src/commands/create-hook.js +262 -0
- package/src/commands/create-phase-dev/codebase-analyzer.js +813 -0
- package/src/commands/create-phase-dev/documentation-generator.js +352 -0
- package/src/commands/create-phase-dev/post-completion.js +404 -0
- package/src/commands/create-phase-dev/scale-calculator.js +344 -0
- package/src/commands/create-phase-dev/wizard.js +492 -0
- package/src/commands/create-phase-dev.js +481 -0
- package/src/commands/create-skill.js +313 -0
- package/src/commands/create.js +446 -0
- package/src/commands/decompose.js +392 -0
- package/src/commands/detect-tech-stack.js +768 -0
- package/src/commands/explore-mcp/claude-md-updater.js +252 -0
- package/src/commands/explore-mcp/mcp-installer.js +346 -0
- package/src/commands/explore-mcp/mcp-registry.js +438 -0
- package/src/commands/explore-mcp.js +638 -0
- package/src/commands/gtask-init.js +641 -0
- package/src/commands/help.js +128 -0
- package/src/commands/init.js +1890 -0
- package/src/commands/install.js +250 -0
- package/src/commands/list.js +116 -0
- package/src/commands/roadmap.js +750 -0
- package/src/commands/setup-wizard.js +482 -0
- package/src/commands/setup.js +351 -0
- package/src/commands/sync.js +534 -0
- package/src/commands/test-run.js +456 -0
- package/src/commands/test-setup.js +456 -0
- package/src/commands/validate.js +67 -0
- package/src/config/tech-stack.defaults.json +182 -0
- package/src/config/tech-stack.schema.json +502 -0
- package/src/github/client.js +359 -0
- package/src/index.js +84 -0
- package/src/templates/claude-command.js +244 -0
- package/src/templates/issue-body.js +284 -0
- package/src/testing/config.js +411 -0
- package/src/utils/template-engine.js +398 -0
- package/src/utils/validate-templates.js +223 -0
- package/src/utils.js +396 -0
- package/templates/commands/ccasp-setup.template.md +113 -0
- package/templates/commands/context-audit.template.md +97 -0
- package/templates/commands/create-task-list.template.md +382 -0
- package/templates/commands/deploy-full.template.md +261 -0
- package/templates/commands/github-task-start.template.md +99 -0
- package/templates/commands/github-update.template.md +69 -0
- package/templates/commands/happy-start.template.md +117 -0
- package/templates/commands/phase-track.template.md +142 -0
- package/templates/commands/tunnel-start.template.md +127 -0
- package/templates/commands/tunnel-stop.template.md +106 -0
- package/templates/hooks/context-guardian.template.js +173 -0
- package/templates/hooks/deployment-orchestrator.template.js +219 -0
- package/templates/hooks/github-progress-hook.template.js +197 -0
- package/templates/hooks/happy-checkpoint-manager.template.js +222 -0
- package/templates/hooks/phase-dev-enforcer.template.js +183 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Command
|
|
3
|
+
*
|
|
4
|
+
* Synchronize task progress between local state and GitHub issues
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { showHeader, showSuccess, showError, showWarning, showInfo } from '../cli/menu.js';
|
|
13
|
+
import { loadConfigSync, execCommand } from '../utils.js';
|
|
14
|
+
import { getIssue, addIssueComment, updateProjectItemField, getProjectItemId } from '../github/client.js';
|
|
15
|
+
import { parseIssueBody, toClaudeTaskList, toMarkdownChecklist, getCompletionStats } from '../analysis/checklist-parser.js';
|
|
16
|
+
|
|
17
|
+
// State directory
|
|
18
|
+
const STATE_DIR = join(process.cwd(), '.gtask');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run the sync command
|
|
22
|
+
*/
|
|
23
|
+
export async function runSync(options) {
|
|
24
|
+
const subcommand = options.subcommand || 'status';
|
|
25
|
+
const issueNumber = options.issue ? parseInt(options.issue, 10) : null;
|
|
26
|
+
|
|
27
|
+
switch (subcommand) {
|
|
28
|
+
case 'pull':
|
|
29
|
+
return await syncPull(issueNumber, options);
|
|
30
|
+
case 'push':
|
|
31
|
+
return await syncPush(issueNumber, options);
|
|
32
|
+
case 'watch':
|
|
33
|
+
return await syncWatch(issueNumber, options);
|
|
34
|
+
case 'status':
|
|
35
|
+
default:
|
|
36
|
+
return await syncStatus(issueNumber, options);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Show sync status
|
|
42
|
+
*/
|
|
43
|
+
async function syncStatus(issueNumber, options) {
|
|
44
|
+
showHeader('Task Sync Status');
|
|
45
|
+
|
|
46
|
+
const states = loadAllTaskStates();
|
|
47
|
+
|
|
48
|
+
if (states.length === 0) {
|
|
49
|
+
showWarning('No tracked issues found.');
|
|
50
|
+
console.log(chalk.dim('Run "gtask decompose <issue>" to start tracking.'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.bold('Tracked Issues:\n'));
|
|
55
|
+
console.log(
|
|
56
|
+
chalk.dim(
|
|
57
|
+
`${'#'.padEnd(8)} ${'Title'.padEnd(40)} ${'Progress'.padEnd(12)} ${'Last Sync'}`
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
console.log(chalk.dim('ā'.repeat(80)));
|
|
61
|
+
|
|
62
|
+
for (const state of states) {
|
|
63
|
+
const stats = getCompletionStats(state.tasks);
|
|
64
|
+
const progress = `${stats.completed}/${stats.total} (${stats.percentage}%)`;
|
|
65
|
+
const lastSync = state.lastSyncedAt
|
|
66
|
+
? new Date(state.lastSyncedAt).toLocaleDateString()
|
|
67
|
+
: 'never';
|
|
68
|
+
const title = (state.issueTitle || '').slice(0, 38);
|
|
69
|
+
|
|
70
|
+
const progressColor =
|
|
71
|
+
stats.percentage === 100
|
|
72
|
+
? chalk.green
|
|
73
|
+
: stats.percentage > 50
|
|
74
|
+
? chalk.yellow
|
|
75
|
+
: chalk.white;
|
|
76
|
+
|
|
77
|
+
console.log(
|
|
78
|
+
`${chalk.cyan(`#${state.issueNumber}`.padEnd(8))} ${title.padEnd(40)} ${progressColor(
|
|
79
|
+
progress.padEnd(12)
|
|
80
|
+
)} ${chalk.dim(lastSync)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(chalk.dim('Commands:'));
|
|
86
|
+
console.log(chalk.dim(' gtask sync pull <issue> - Pull latest from GitHub'));
|
|
87
|
+
console.log(chalk.dim(' gtask sync push <issue> - Push progress to GitHub'));
|
|
88
|
+
console.log(chalk.dim(' gtask sync watch <issue> - Auto-sync on changes'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Pull task state from GitHub issue
|
|
93
|
+
*/
|
|
94
|
+
async function syncPull(issueNumber, options) {
|
|
95
|
+
showHeader('Pull from GitHub');
|
|
96
|
+
|
|
97
|
+
if (!issueNumber) {
|
|
98
|
+
const { num } = await inquirer.prompt([
|
|
99
|
+
{
|
|
100
|
+
type: 'input',
|
|
101
|
+
name: 'num',
|
|
102
|
+
message: 'Issue number:',
|
|
103
|
+
validate: (input) => parseInt(input, 10) > 0 || 'Enter valid number',
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
issueNumber = parseInt(num, 10);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { config } = loadConfigSync();
|
|
110
|
+
if (!config?.project_board) {
|
|
111
|
+
showError('Not configured');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { owner, repo } = config.project_board;
|
|
116
|
+
|
|
117
|
+
const spinner = ora(`Fetching issue #${issueNumber}...`).start();
|
|
118
|
+
const issue = getIssue(owner, repo, issueNumber);
|
|
119
|
+
|
|
120
|
+
if (!issue) {
|
|
121
|
+
spinner.fail('Issue not found');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
spinner.text = 'Parsing issue...';
|
|
126
|
+
const parsed = parseIssueBody(issue.body);
|
|
127
|
+
const tasks = toClaudeTaskList(parsed);
|
|
128
|
+
|
|
129
|
+
// Load existing state
|
|
130
|
+
const existingState = loadTaskState(issueNumber);
|
|
131
|
+
const stats = getCompletionStats(tasks);
|
|
132
|
+
|
|
133
|
+
spinner.succeed(`Pulled ${tasks.length} tasks from #${issueNumber}`);
|
|
134
|
+
|
|
135
|
+
// Check for conflicts
|
|
136
|
+
if (existingState && existingState.tasks) {
|
|
137
|
+
const localStats = getCompletionStats(existingState.tasks);
|
|
138
|
+
|
|
139
|
+
if (localStats.completed > stats.completed) {
|
|
140
|
+
showWarning(
|
|
141
|
+
`Local has more progress (${localStats.completed}/${localStats.total}) than GitHub (${stats.completed}/${stats.total})`
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const { resolve } = await inquirer.prompt([
|
|
145
|
+
{
|
|
146
|
+
type: 'list',
|
|
147
|
+
name: 'resolve',
|
|
148
|
+
message: 'How to resolve?',
|
|
149
|
+
choices: [
|
|
150
|
+
{ name: 'Keep local (more progress)', value: 'local' },
|
|
151
|
+
{ name: 'Use GitHub (overwrite local)', value: 'github' },
|
|
152
|
+
{ name: 'Merge (keep completed from both)', value: 'merge' },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
if (resolve === 'local') {
|
|
158
|
+
console.log(chalk.green('ā Keeping local state'));
|
|
159
|
+
return existingState;
|
|
160
|
+
} else if (resolve === 'merge') {
|
|
161
|
+
// Merge: mark as completed if either has it completed
|
|
162
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
163
|
+
if (existingState.tasks[i]?.status === 'completed') {
|
|
164
|
+
tasks[i].status = 'completed';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
console.log(chalk.green('ā Merged states'));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Save state
|
|
173
|
+
const state = {
|
|
174
|
+
issueNumber,
|
|
175
|
+
issueTitle: issue.title,
|
|
176
|
+
owner,
|
|
177
|
+
repo,
|
|
178
|
+
tasks,
|
|
179
|
+
createdAt: existingState?.createdAt || new Date().toISOString(),
|
|
180
|
+
lastSyncedAt: new Date().toISOString(),
|
|
181
|
+
pullSource: 'github',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
saveTaskState(state);
|
|
185
|
+
|
|
186
|
+
showSuccess('Pull Complete', [
|
|
187
|
+
`Issue: #${issueNumber} - ${issue.title}`,
|
|
188
|
+
`Tasks: ${stats.total} (${stats.completed} completed)`,
|
|
189
|
+
`Progress: ${stats.percentage}%`,
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
return state;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Push task progress to GitHub
|
|
197
|
+
*/
|
|
198
|
+
async function syncPush(issueNumber, options) {
|
|
199
|
+
showHeader('Push to GitHub');
|
|
200
|
+
|
|
201
|
+
if (!issueNumber) {
|
|
202
|
+
// Show available issues
|
|
203
|
+
const states = loadAllTaskStates();
|
|
204
|
+
if (states.length === 0) {
|
|
205
|
+
showError('No tracked issues', 'Run "gtask decompose <issue>" first.');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { selectedIssue } = await inquirer.prompt([
|
|
210
|
+
{
|
|
211
|
+
type: 'list',
|
|
212
|
+
name: 'selectedIssue',
|
|
213
|
+
message: 'Select issue to push:',
|
|
214
|
+
choices: states.map((s) => ({
|
|
215
|
+
name: `#${s.issueNumber} - ${s.issueTitle} (${getCompletionStats(s.tasks).percentage}%)`,
|
|
216
|
+
value: s.issueNumber,
|
|
217
|
+
})),
|
|
218
|
+
},
|
|
219
|
+
]);
|
|
220
|
+
issueNumber = selectedIssue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const state = loadTaskState(issueNumber);
|
|
224
|
+
if (!state) {
|
|
225
|
+
showError(`No local state for issue #${issueNumber}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { config } = loadConfigSync();
|
|
230
|
+
const { owner, repo, project_number: projectNumber, project_id: projectId } =
|
|
231
|
+
config.project_board;
|
|
232
|
+
|
|
233
|
+
const stats = getCompletionStats(state.tasks);
|
|
234
|
+
|
|
235
|
+
// Generate update comment
|
|
236
|
+
const spinner = ora('Generating progress update...').start();
|
|
237
|
+
|
|
238
|
+
const completedTasks = state.tasks
|
|
239
|
+
.filter((t) => t.status === 'completed')
|
|
240
|
+
.map((t, i) => ` ā
${t.content}`)
|
|
241
|
+
.join('\n');
|
|
242
|
+
|
|
243
|
+
const pendingTasks = state.tasks
|
|
244
|
+
.filter((t) => t.status !== 'completed')
|
|
245
|
+
.map((t) => ` ⬠${t.content}`)
|
|
246
|
+
.join('\n');
|
|
247
|
+
|
|
248
|
+
// Get recent commits
|
|
249
|
+
const commitResult = execCommand('git log --oneline -5 2>/dev/null');
|
|
250
|
+
const recentCommits = commitResult.success
|
|
251
|
+
? commitResult.output
|
|
252
|
+
.split('\n')
|
|
253
|
+
.slice(0, 3)
|
|
254
|
+
.map((c) => ` - ${c}`)
|
|
255
|
+
.join('\n')
|
|
256
|
+
: ' (no recent commits)';
|
|
257
|
+
|
|
258
|
+
const commentBody = `## Progress Update
|
|
259
|
+
|
|
260
|
+
**Status**: ${stats.percentage}% complete (${stats.completed}/${stats.total} tasks)
|
|
261
|
+
**Updated**: ${new Date().toISOString()}
|
|
262
|
+
|
|
263
|
+
### Completed Tasks
|
|
264
|
+
${completedTasks || ' (none yet)'}
|
|
265
|
+
|
|
266
|
+
### Remaining Tasks
|
|
267
|
+
${pendingTasks || ' (all done!)'}
|
|
268
|
+
|
|
269
|
+
### Recent Commits
|
|
270
|
+
${recentCommits}
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
*Synced by GitHub Task Kit*`;
|
|
274
|
+
|
|
275
|
+
spinner.text = 'Posting to GitHub...';
|
|
276
|
+
const success = addIssueComment(owner, repo, issueNumber, commentBody);
|
|
277
|
+
|
|
278
|
+
if (!success) {
|
|
279
|
+
spinner.fail('Failed to post comment');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Update project board status if 100%
|
|
284
|
+
if (stats.percentage === 100 && projectId && config.field_ids?.status) {
|
|
285
|
+
spinner.text = 'Updating project board...';
|
|
286
|
+
const itemId = getProjectItemId(owner, projectNumber, issueNumber);
|
|
287
|
+
|
|
288
|
+
if (itemId && config.status_options?.done) {
|
|
289
|
+
updateProjectItemField(
|
|
290
|
+
projectId,
|
|
291
|
+
itemId,
|
|
292
|
+
config.field_ids.status,
|
|
293
|
+
config.status_options.done,
|
|
294
|
+
'single-select'
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Update local state
|
|
300
|
+
state.lastSyncedAt = new Date().toISOString();
|
|
301
|
+
saveTaskState(state);
|
|
302
|
+
|
|
303
|
+
spinner.succeed('Pushed to GitHub');
|
|
304
|
+
|
|
305
|
+
showSuccess('Push Complete', [
|
|
306
|
+
`Issue: #${issueNumber}`,
|
|
307
|
+
`Progress: ${stats.percentage}%`,
|
|
308
|
+
`Comment posted with ${stats.completed} completed tasks`,
|
|
309
|
+
stats.percentage === 100 ? 'Project board status updated to Done' : '',
|
|
310
|
+
].filter(Boolean));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Watch for changes and auto-sync
|
|
315
|
+
*/
|
|
316
|
+
async function syncWatch(issueNumber, options) {
|
|
317
|
+
showHeader('Watch Mode');
|
|
318
|
+
|
|
319
|
+
if (!issueNumber) {
|
|
320
|
+
const states = loadAllTaskStates();
|
|
321
|
+
if (states.length === 0) {
|
|
322
|
+
showError('No tracked issues');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const { selectedIssue } = await inquirer.prompt([
|
|
327
|
+
{
|
|
328
|
+
type: 'list',
|
|
329
|
+
name: 'selectedIssue',
|
|
330
|
+
message: 'Select issue to watch:',
|
|
331
|
+
choices: states.map((s) => ({
|
|
332
|
+
name: `#${s.issueNumber} - ${s.issueTitle}`,
|
|
333
|
+
value: s.issueNumber,
|
|
334
|
+
})),
|
|
335
|
+
},
|
|
336
|
+
]);
|
|
337
|
+
issueNumber = selectedIssue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const state = loadTaskState(issueNumber);
|
|
341
|
+
if (!state) {
|
|
342
|
+
showError(`No local state for issue #${issueNumber}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log(chalk.cyan(`Watching issue #${issueNumber}: ${state.issueTitle}`));
|
|
347
|
+
console.log(chalk.dim('Press Ctrl+C to stop\n'));
|
|
348
|
+
|
|
349
|
+
// Display interactive task list
|
|
350
|
+
let tasks = state.tasks;
|
|
351
|
+
let lastStats = getCompletionStats(tasks);
|
|
352
|
+
|
|
353
|
+
const displayTasks = () => {
|
|
354
|
+
console.clear();
|
|
355
|
+
console.log(chalk.cyan.bold(`\n Issue #${issueNumber}: ${state.issueTitle}\n`));
|
|
356
|
+
|
|
357
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
358
|
+
const task = tasks[i];
|
|
359
|
+
const status =
|
|
360
|
+
task.status === 'completed'
|
|
361
|
+
? chalk.green('ā')
|
|
362
|
+
: task.status === 'in_progress'
|
|
363
|
+
? chalk.yellow('ā')
|
|
364
|
+
: chalk.dim('ā');
|
|
365
|
+
const num = chalk.dim(`${i + 1}.`);
|
|
366
|
+
console.log(` ${status} ${num} ${task.content}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const stats = getCompletionStats(tasks);
|
|
370
|
+
console.log('');
|
|
371
|
+
console.log(
|
|
372
|
+
chalk.dim(` Progress: ${stats.completed}/${stats.total} (${stats.percentage}%)`)
|
|
373
|
+
);
|
|
374
|
+
console.log('');
|
|
375
|
+
console.log(chalk.dim(' Commands:'));
|
|
376
|
+
console.log(chalk.dim(' c <num> - Complete task'));
|
|
377
|
+
console.log(chalk.dim(' s <num> - Start task (in progress)'));
|
|
378
|
+
console.log(chalk.dim(' r <num> - Reset task (pending)'));
|
|
379
|
+
console.log(chalk.dim(' p - Push to GitHub'));
|
|
380
|
+
console.log(chalk.dim(' q - Quit'));
|
|
381
|
+
console.log('');
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
displayTasks();
|
|
385
|
+
|
|
386
|
+
// Interactive loop
|
|
387
|
+
const readline = await import('readline');
|
|
388
|
+
const rl = readline.createInterface({
|
|
389
|
+
input: process.stdin,
|
|
390
|
+
output: process.stdout,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const { config } = loadConfigSync();
|
|
394
|
+
|
|
395
|
+
const processCommand = async (line) => {
|
|
396
|
+
const [cmd, arg] = line.trim().split(/\s+/);
|
|
397
|
+
const taskNum = parseInt(arg, 10) - 1;
|
|
398
|
+
|
|
399
|
+
switch (cmd?.toLowerCase()) {
|
|
400
|
+
case 'c':
|
|
401
|
+
case 'complete':
|
|
402
|
+
if (taskNum >= 0 && taskNum < tasks.length) {
|
|
403
|
+
tasks[taskNum].status = 'completed';
|
|
404
|
+
state.tasks = tasks;
|
|
405
|
+
saveTaskState(state);
|
|
406
|
+
console.log(chalk.green(`ā Task ${taskNum + 1} completed`));
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
|
|
410
|
+
case 's':
|
|
411
|
+
case 'start':
|
|
412
|
+
if (taskNum >= 0 && taskNum < tasks.length) {
|
|
413
|
+
tasks[taskNum].status = 'in_progress';
|
|
414
|
+
state.tasks = tasks;
|
|
415
|
+
saveTaskState(state);
|
|
416
|
+
console.log(chalk.yellow(`ā Task ${taskNum + 1} in progress`));
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
case 'r':
|
|
421
|
+
case 'reset':
|
|
422
|
+
if (taskNum >= 0 && taskNum < tasks.length) {
|
|
423
|
+
tasks[taskNum].status = 'pending';
|
|
424
|
+
state.tasks = tasks;
|
|
425
|
+
saveTaskState(state);
|
|
426
|
+
console.log(chalk.dim(`ā Task ${taskNum + 1} reset`));
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
|
|
430
|
+
case 'p':
|
|
431
|
+
case 'push':
|
|
432
|
+
await syncPush(issueNumber, {});
|
|
433
|
+
break;
|
|
434
|
+
|
|
435
|
+
case 'q':
|
|
436
|
+
case 'quit':
|
|
437
|
+
rl.close();
|
|
438
|
+
process.exit(0);
|
|
439
|
+
break;
|
|
440
|
+
|
|
441
|
+
default:
|
|
442
|
+
if (cmd) {
|
|
443
|
+
console.log(chalk.red('Unknown command'));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Check if we should auto-sync
|
|
448
|
+
const newStats = getCompletionStats(tasks);
|
|
449
|
+
if (newStats.completed !== lastStats.completed) {
|
|
450
|
+
// Progress changed, maybe auto-push
|
|
451
|
+
if (newStats.percentage === 100) {
|
|
452
|
+
console.log(chalk.green('\nš All tasks complete! Pushing to GitHub...'));
|
|
453
|
+
await syncPush(issueNumber, {});
|
|
454
|
+
}
|
|
455
|
+
lastStats = newStats;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
setTimeout(displayTasks, 500);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
rl.on('line', processCommand);
|
|
462
|
+
|
|
463
|
+
// Handle Ctrl+C
|
|
464
|
+
rl.on('close', () => {
|
|
465
|
+
console.log(chalk.dim('\nStopped watching.'));
|
|
466
|
+
process.exit(0);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Save task state to disk
|
|
472
|
+
*/
|
|
473
|
+
export function saveTaskState(state) {
|
|
474
|
+
if (!existsSync(STATE_DIR)) {
|
|
475
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const filePath = join(STATE_DIR, `issue-${state.issueNumber}.json`);
|
|
479
|
+
writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf8');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Load task state from disk
|
|
484
|
+
*/
|
|
485
|
+
export function loadTaskState(issueNumber) {
|
|
486
|
+
const filePath = join(STATE_DIR, `issue-${issueNumber}.json`);
|
|
487
|
+
|
|
488
|
+
if (!existsSync(filePath)) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Load all task states
|
|
501
|
+
*/
|
|
502
|
+
export function loadAllTaskStates() {
|
|
503
|
+
if (!existsSync(STATE_DIR)) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const files = readdirSync(STATE_DIR).filter((f) => f.startsWith('issue-') && f.endsWith('.json'));
|
|
508
|
+
const states = [];
|
|
509
|
+
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
try {
|
|
512
|
+
const content = readFileSync(join(STATE_DIR, file), 'utf8');
|
|
513
|
+
states.push(JSON.parse(content));
|
|
514
|
+
} catch {
|
|
515
|
+
// Skip invalid files
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return states.sort((a, b) => b.issueNumber - a.issueNumber);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Update a specific task's status
|
|
524
|
+
*/
|
|
525
|
+
export function updateTaskStatus(issueNumber, taskIndex, status) {
|
|
526
|
+
const state = loadTaskState(issueNumber);
|
|
527
|
+
if (!state || !state.tasks[taskIndex]) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
state.tasks[taskIndex].status = status;
|
|
532
|
+
saveTaskState(state);
|
|
533
|
+
return true;
|
|
534
|
+
}
|