@crewx/wbs 0.1.6 → 0.1.8
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 +7 -7
- package/SKILL.md +1198 -1198
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +718 -0
- package/dist/cli.js.map +1 -0
- package/dist/src/engine.d.ts +54 -0
- package/dist/src/engine.d.ts.map +1 -0
- package/dist/src/engine.js +551 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/types.d.ts +60 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +55 -54
package/dist/cli.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.main = main;
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const engine_1 = require("./src/engine");
|
|
42
|
+
let PKG_VERSION = 'unknown';
|
|
43
|
+
try {
|
|
44
|
+
const _pkgPath = fs.existsSync(path.join(__dirname, 'package.json'))
|
|
45
|
+
? path.join(__dirname, 'package.json')
|
|
46
|
+
: path.join(__dirname, '..', 'package.json');
|
|
47
|
+
const _pkg = JSON.parse(fs.readFileSync(_pkgPath, 'utf-8'));
|
|
48
|
+
if (_pkg?.version)
|
|
49
|
+
PKG_VERSION = String(_pkg.version);
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
async function loadTracer() {
|
|
53
|
+
try {
|
|
54
|
+
return await Promise.resolve().then(() => __importStar(require('@crewx/shared/skill-tracer')));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const STATUS_ICONS = {
|
|
61
|
+
pending: '⬜️',
|
|
62
|
+
running: '🟡',
|
|
63
|
+
completed: '✅',
|
|
64
|
+
failed: '❌',
|
|
65
|
+
on_hold: '⏸️',
|
|
66
|
+
planning: '📝',
|
|
67
|
+
partial: '⚠️',
|
|
68
|
+
};
|
|
69
|
+
function icon(status) {
|
|
70
|
+
return STATUS_ICONS[status] ?? '❓';
|
|
71
|
+
}
|
|
72
|
+
function getDisplayWidth(str) {
|
|
73
|
+
let width = 0;
|
|
74
|
+
for (const char of String(str)) {
|
|
75
|
+
const code = char.codePointAt(0) ?? 0;
|
|
76
|
+
if ((code >= 0x1100 && code <= 0x11FF) || (code >= 0x3000 && code <= 0x303F) ||
|
|
77
|
+
(code >= 0x3040 && code <= 0x309F) || (code >= 0x30A0 && code <= 0x30FF) ||
|
|
78
|
+
(code >= 0x3130 && code <= 0x318F) || (code >= 0x3200 && code <= 0x32FF) ||
|
|
79
|
+
(code >= 0x4E00 && code <= 0x9FFF) || (code >= 0xAC00 && code <= 0xD7AF) ||
|
|
80
|
+
(code >= 0xF900 && code <= 0xFAFF) || (code >= 0xFF00 && code <= 0xFFEF) ||
|
|
81
|
+
(code >= 0x1F300 && code <= 0x1F9FF) || (code >= 0x2600 && code <= 0x26FF) ||
|
|
82
|
+
(code >= 0x2700 && code <= 0x27BF)) {
|
|
83
|
+
width += 2;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
width += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return width;
|
|
90
|
+
}
|
|
91
|
+
function pad(str, targetWidth) {
|
|
92
|
+
const s = String(str);
|
|
93
|
+
const diff = targetWidth - getDisplayWidth(s);
|
|
94
|
+
return diff > 0 ? s + ' '.repeat(diff) : s;
|
|
95
|
+
}
|
|
96
|
+
function colWidths(headers, rows) {
|
|
97
|
+
const widths = headers.map(h => getDisplayWidth(h));
|
|
98
|
+
rows.forEach(row => row.forEach((cell, i) => {
|
|
99
|
+
const w = getDisplayWidth(String(cell));
|
|
100
|
+
if (w > (widths[i] ?? 0))
|
|
101
|
+
widths[i] = w;
|
|
102
|
+
}));
|
|
103
|
+
return widths;
|
|
104
|
+
}
|
|
105
|
+
function fmtRow(cells, widths) {
|
|
106
|
+
return '| ' + cells.map((c, i) => pad(String(c), widths[i] ?? 0)).join(' | ') + ' |';
|
|
107
|
+
}
|
|
108
|
+
function fmtSep(widths) {
|
|
109
|
+
return '|' + widths.map(w => '-'.repeat(w + 2)).join('|') + '|';
|
|
110
|
+
}
|
|
111
|
+
function fmtTable(headers, rows) {
|
|
112
|
+
const w = colWidths(headers, rows);
|
|
113
|
+
return [fmtRow(headers, w), fmtSep(w), ...rows.map(r => fmtRow(r, w))].join('\n');
|
|
114
|
+
}
|
|
115
|
+
function fmtProjectList(projects) {
|
|
116
|
+
if (!projects || projects.length === 0)
|
|
117
|
+
return '📋 WBS Projects (0)\n\nNo projects found.';
|
|
118
|
+
const rows = projects.map(p => {
|
|
119
|
+
const jobs = engine_1.engine.listJobs(p.id);
|
|
120
|
+
const done = jobs.filter(j => j.status === 'completed').length;
|
|
121
|
+
const total = jobs.length;
|
|
122
|
+
const progress = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
123
|
+
const statusIcon = p.status === 'completed' ? icon('completed')
|
|
124
|
+
: (p.status === 'running' || jobs.some(j => j.status === 'running')) ? icon('running')
|
|
125
|
+
: done > 0 ? icon('running') : icon('pending');
|
|
126
|
+
return [statusIcon, p.id, p.title, String(total), `${progress}%`];
|
|
127
|
+
});
|
|
128
|
+
return `📋 WBS Projects (${projects.length})\n\n${fmtTable(['상태', 'ID', '프로젝트명', 'Jobs', 'Progress'], rows)}`;
|
|
129
|
+
}
|
|
130
|
+
function fmtStatus(result) {
|
|
131
|
+
if (!result.project)
|
|
132
|
+
return '❌ Project not found';
|
|
133
|
+
const { project, jobs, summary } = result;
|
|
134
|
+
const lines = [];
|
|
135
|
+
lines.push(`📋 ${project.title} (${project.id})`);
|
|
136
|
+
if (summary)
|
|
137
|
+
lines.push(` Status: ${project.status} | Progress: ${summary.progress}% (${summary.completed}/${summary.total})`);
|
|
138
|
+
if (project.detail_path)
|
|
139
|
+
lines.push(` Detail: ${project.detail_path}`);
|
|
140
|
+
lines.push('');
|
|
141
|
+
if (jobs.length === 0) {
|
|
142
|
+
lines.push(' Jobs: None');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
lines.push(' Jobs:');
|
|
146
|
+
const table = fmtTable(['상태', '#', 'Slug', 'ID', '작업명', '담당'], jobs.map((j, i) => [icon(j.status), String(i + 1), (0, engine_1.getJobSlug)(j), j.id, j.title, j.agent]));
|
|
147
|
+
table.split('\n').forEach(l => lines.push(' ' + l));
|
|
148
|
+
}
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
function fmtJobList(jobs, wbsId) {
|
|
152
|
+
if (!jobs || jobs.length === 0)
|
|
153
|
+
return `📋 Jobs for ${wbsId} (0)\n\nNo jobs found.`;
|
|
154
|
+
const done = jobs.filter(j => j.status === 'completed').length;
|
|
155
|
+
const progress = jobs.length > 0 ? Math.round((done / jobs.length) * 100) : 0;
|
|
156
|
+
const table = fmtTable(['상태', '#', 'Slug', 'ID', '작업명', '담당', 'Status'], jobs.map((j, i) => [icon(j.status), String(i + 1), (0, engine_1.getJobSlug)(j), j.id, j.title, j.agent, j.status]));
|
|
157
|
+
return `📋 Jobs for ${wbsId} (${jobs.length})\n Progress: ${progress}% (${done}/${jobs.length})\n\n${table}`;
|
|
158
|
+
}
|
|
159
|
+
function fmtExecList(execs, jobId) {
|
|
160
|
+
if (execs.length === 0)
|
|
161
|
+
return `No executions found for job: ${jobId}`;
|
|
162
|
+
const headers = ['상태', 'ID', 'PID', 'Task ID', '시작', '종료', '코드'];
|
|
163
|
+
const rows = execs.map(e => [
|
|
164
|
+
icon(e.status), e.id, String(e.pid ?? '-'), e.task_id ?? '-',
|
|
165
|
+
e.started_at ? new Date(e.started_at).toLocaleString('ko-KR') : '-',
|
|
166
|
+
e.ended_at ? new Date(e.ended_at).toLocaleString('ko-KR') : '-',
|
|
167
|
+
e.exit_code !== null ? String(e.exit_code) : '-',
|
|
168
|
+
]);
|
|
169
|
+
return `📋 Executions for ${jobId}\n\n${fmtTable(headers, rows)}`;
|
|
170
|
+
}
|
|
171
|
+
function fmtExecStatus(exec) {
|
|
172
|
+
return `\n📋 Execution: ${exec.id}\n\nStatus: ${icon(exec.status)} ${exec.status}\nJob ID: ${exec.job_id}\nPID: ${exec.pid ?? '-'}\nTask ID: ${exec.task_id ?? '-'}\nStarted: ${exec.started_at ? new Date(exec.started_at).toLocaleString('ko-KR') : '-'}\nEnded: ${exec.ended_at ? new Date(exec.ended_at).toLocaleString('ko-KR') : '-'}\nExit Code: ${exec.exit_code !== null ? exec.exit_code : '-'}\nError: ${exec.error ?? '-'}\n`;
|
|
173
|
+
}
|
|
174
|
+
function fmtRunning(execs) {
|
|
175
|
+
if (execs.length === 0)
|
|
176
|
+
return 'No running executions';
|
|
177
|
+
const headers = ['상태', 'Exec ID', 'Job ID', 'PID', 'Task ID', '시작'];
|
|
178
|
+
const rows = execs.map(e => [
|
|
179
|
+
icon(e.status), e.id, e.job_id, String(e.pid ?? '-'), e.task_id ?? '-',
|
|
180
|
+
e.started_at ? new Date(e.started_at).toLocaleString('ko-KR') : '-',
|
|
181
|
+
]);
|
|
182
|
+
return `🟡 Running Executions (${execs.length})\n\n${fmtTable(headers, rows)}`;
|
|
183
|
+
}
|
|
184
|
+
function hasJson(args) { return args.includes('--json'); }
|
|
185
|
+
function hasFlag(args, name) { return args.includes(`--${name}`); }
|
|
186
|
+
function getBooleanOpt(args, opts, name) {
|
|
187
|
+
const inlinePrefix = `--${name}=`;
|
|
188
|
+
const inlineArg = args.find(arg => arg.startsWith(inlinePrefix));
|
|
189
|
+
if (inlineArg) {
|
|
190
|
+
const rawInline = inlineArg.slice(inlinePrefix.length);
|
|
191
|
+
if (rawInline === 'true' || rawInline === '1')
|
|
192
|
+
return true;
|
|
193
|
+
if (rawInline === 'false' || rawInline === '0')
|
|
194
|
+
return false;
|
|
195
|
+
console.error(`Invalid boolean for --${name}: ${rawInline} (use true/false)`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
if (hasFlag(args, name))
|
|
199
|
+
return true;
|
|
200
|
+
const raw = opts[name];
|
|
201
|
+
if (raw === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
if (raw === 'true' || raw === '1')
|
|
204
|
+
return true;
|
|
205
|
+
if (raw === 'false' || raw === '0')
|
|
206
|
+
return false;
|
|
207
|
+
console.error(`Invalid boolean for --${name}: ${raw} (use true/false)`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
function parseOpts(args, start = 0) {
|
|
211
|
+
const opts = {};
|
|
212
|
+
for (let i = start; i < args.length; i++) {
|
|
213
|
+
const current = args[i];
|
|
214
|
+
if (!current)
|
|
215
|
+
continue;
|
|
216
|
+
const next = args[i + 1];
|
|
217
|
+
if (current.startsWith('--') && next && !next.startsWith('--')) {
|
|
218
|
+
opts[current.slice(2)] = next;
|
|
219
|
+
i++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return opts;
|
|
223
|
+
}
|
|
224
|
+
function handleJob(args) {
|
|
225
|
+
const sub = args[0];
|
|
226
|
+
switch (sub) {
|
|
227
|
+
case 'list': {
|
|
228
|
+
const wbsId = args[1];
|
|
229
|
+
if (!wbsId) {
|
|
230
|
+
console.error('Usage: wbs job list <wbs-id> [--json]');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
const jobs = engine_1.engine.listJobs(wbsId);
|
|
234
|
+
console.log(hasJson(args) ? JSON.stringify(jobs, null, 2) : fmtJobList(jobs, wbsId));
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case 'add': {
|
|
238
|
+
const wbsId = args[1];
|
|
239
|
+
const opts = parseOpts(args, 2);
|
|
240
|
+
const { title, agent } = opts;
|
|
241
|
+
const description = opts.desc || opts.description || null;
|
|
242
|
+
const seq = opts.seq ? parseInt(opts.seq, 10) : 0;
|
|
243
|
+
const issue_number = opts.issue ? parseInt(opts.issue, 10) : null;
|
|
244
|
+
const no_git = getBooleanOpt(args, opts, 'no-git') ?? false;
|
|
245
|
+
if (!wbsId || !title || !agent) {
|
|
246
|
+
console.error('Usage: wbs job add <wbs-id> --title "Job Title" --agent "@agent" [--desc "..."] [--seq N] [--issue N] [--no-git]');
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const jobId = engine_1.engine.addJob(wbsId, { title, description, agent, seq, issue_number, no_git });
|
|
250
|
+
(0, engine_1.generateDetailFile)(wbsId);
|
|
251
|
+
console.log(JSON.stringify({ jobId, wbsId, title, description, agent, seq, issue_number, no_git }, null, 2));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 'update': {
|
|
255
|
+
const jobIdArg = args[1];
|
|
256
|
+
if (!jobIdArg) {
|
|
257
|
+
console.error('Usage: wbs job update <job-id|slug> [--status ...] [--title ...] [--agent ...] [--seq N] [--desc ...] [--issue N] [--no-git[=true|false]]');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const opts = parseOpts(args, 2);
|
|
261
|
+
const job = (0, engine_1.resolveJobId)(jobIdArg);
|
|
262
|
+
if (!job) {
|
|
263
|
+
console.error(`Job not found: ${jobIdArg}`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
const jobId = job.id;
|
|
267
|
+
const fields = {};
|
|
268
|
+
if (opts.status) {
|
|
269
|
+
if (!['pending', 'running', 'completed', 'failed'].includes(opts.status)) {
|
|
270
|
+
console.error('Invalid status. Must be: pending, running, completed, or failed');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
fields.status = opts.status;
|
|
274
|
+
}
|
|
275
|
+
if (opts.title)
|
|
276
|
+
fields.title = opts.title;
|
|
277
|
+
if (opts.agent)
|
|
278
|
+
fields.agent = opts.agent;
|
|
279
|
+
if (opts.seq !== undefined)
|
|
280
|
+
fields.seq = parseInt(opts.seq, 10);
|
|
281
|
+
if (opts.desc || opts.description)
|
|
282
|
+
fields.description = opts.desc || opts.description;
|
|
283
|
+
if (opts.issue !== undefined)
|
|
284
|
+
fields.issue_number = opts.issue ? parseInt(opts.issue, 10) : null;
|
|
285
|
+
const noGitOpt = getBooleanOpt(args, opts, 'no-git');
|
|
286
|
+
if (noGitOpt !== undefined)
|
|
287
|
+
fields.no_git = noGitOpt;
|
|
288
|
+
if (Object.keys(fields).length === 0) {
|
|
289
|
+
console.error('No update options provided.');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
engine_1.engine.updateJob(jobId, fields);
|
|
293
|
+
const updated = engine_1.engine.getJob(jobId);
|
|
294
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case 'next': {
|
|
298
|
+
const wbsId = args[1];
|
|
299
|
+
if (!wbsId) {
|
|
300
|
+
console.error('Usage: wbs job next <wbs-id>');
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
const job = engine_1.engine.getNextJob(wbsId);
|
|
304
|
+
if (job) {
|
|
305
|
+
engine_1.engine.runWorker(job).then(r => console.log(JSON.stringify(r, null, 2)));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
console.log('No pending jobs');
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case 'run': {
|
|
313
|
+
const wbsId = args[1];
|
|
314
|
+
if (!wbsId) {
|
|
315
|
+
console.error('Usage: wbs job run <wbs-id>');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
engine_1.engine.run(wbsId);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case 'retry': {
|
|
322
|
+
const jobIdArg = args[1];
|
|
323
|
+
if (!jobIdArg) {
|
|
324
|
+
console.error('Usage: wbs job retry <job-id|slug>');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const job = (0, engine_1.resolveJobId)(jobIdArg);
|
|
328
|
+
if (!job) {
|
|
329
|
+
console.error(`Job not found: ${jobIdArg}`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
engine_1.engine.updateJob(job.id, { status: 'pending' });
|
|
333
|
+
console.log(`[WBS] Retrying job: ${job.title}`);
|
|
334
|
+
engine_1.engine.runWorker(job).then(r => console.log(JSON.stringify(r, null, 2)));
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
default:
|
|
338
|
+
console.log(`
|
|
339
|
+
Job Commands - Manage jobs within a WBS project
|
|
340
|
+
|
|
341
|
+
Usage:
|
|
342
|
+
wbs job list <wbs-id> [--json] List all jobs in a project
|
|
343
|
+
wbs job add <wbs-id> [options] Add a new job to the project
|
|
344
|
+
wbs job update <job-id> [options] Update an existing job
|
|
345
|
+
wbs job next <wbs-id> Run the next pending job
|
|
346
|
+
wbs job run <wbs-id> [--parallel] Run all pending jobs (default: sequential)
|
|
347
|
+
wbs job retry <job-id> Retry a failed/completed job
|
|
348
|
+
|
|
349
|
+
Options for 'job add':
|
|
350
|
+
--title "Job Title" Job title (required)
|
|
351
|
+
--agent "@agent" Worker agent (required)
|
|
352
|
+
--seq N Execution sequence (default: 0)
|
|
353
|
+
--desc "..." Detailed instructions
|
|
354
|
+
--issue N Related GitHub issue number
|
|
355
|
+
--no-git Skip git worktree/PR template for non-code jobs
|
|
356
|
+
|
|
357
|
+
Options for 'job update':
|
|
358
|
+
--no-git[=true|false] Set/clear no_git flag
|
|
359
|
+
|
|
360
|
+
Options for 'job run':
|
|
361
|
+
--parallel Run same-seq pending jobs concurrently (job run only)
|
|
362
|
+
`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function handleExec(args) {
|
|
366
|
+
const sub = args[0];
|
|
367
|
+
switch (sub) {
|
|
368
|
+
case 'list': {
|
|
369
|
+
const jobIdArg = args[1];
|
|
370
|
+
if (!jobIdArg) {
|
|
371
|
+
console.error('Usage: wbs exec list <job-id|slug> [--json]');
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
const resolvedJob = (0, engine_1.resolveJobId)(jobIdArg);
|
|
375
|
+
const jobId = resolvedJob?.id ?? jobIdArg;
|
|
376
|
+
const execs = engine_1.engine.listExecutions(jobId);
|
|
377
|
+
console.log(hasJson(args) ? JSON.stringify(execs, null, 2) : fmtExecList(execs, jobId));
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case 'status': {
|
|
381
|
+
const execId = args[1];
|
|
382
|
+
if (!execId) {
|
|
383
|
+
console.error('Usage: wbs exec status <exec-id> [--json]');
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
const exec = engine_1.engine.getExecution(execId);
|
|
387
|
+
if (!exec) {
|
|
388
|
+
console.error(`Execution not found: ${execId}`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
console.log(hasJson(args) ? JSON.stringify(exec, null, 2) : fmtExecStatus(exec));
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case 'kill': {
|
|
395
|
+
const execId = args[1];
|
|
396
|
+
if (!execId) {
|
|
397
|
+
console.error('Usage: wbs exec kill <exec-id>');
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
const ok = engine_1.engine.killExecution(execId);
|
|
401
|
+
if (ok) {
|
|
402
|
+
console.log(`Killed execution: ${execId}`);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
console.error(`Could not kill execution: ${execId} (not running or not found)`);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case 'running': {
|
|
411
|
+
const execs = engine_1.engine.getRunningExecutions();
|
|
412
|
+
console.log(hasJson(args) ? JSON.stringify(execs, null, 2) : fmtRunning(execs));
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
default:
|
|
416
|
+
console.log(`
|
|
417
|
+
Execution Commands - Manage job executions
|
|
418
|
+
|
|
419
|
+
Usage:
|
|
420
|
+
wbs exec list <job-id> [--json] List execution history for a job
|
|
421
|
+
wbs exec status <exec-id> [--json] Get execution details
|
|
422
|
+
wbs exec kill <exec-id> Kill a running execution
|
|
423
|
+
wbs exec running [--json] List all running executions
|
|
424
|
+
`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function getLocalTimestamp() {
|
|
428
|
+
const now = new Date();
|
|
429
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
430
|
+
}
|
|
431
|
+
function daemonTick() {
|
|
432
|
+
const ts = getLocalTimestamp();
|
|
433
|
+
console.log(`[${ts}] Daemon tick`);
|
|
434
|
+
const zombies = engine_1.engine.checkAndFixZombies();
|
|
435
|
+
if (zombies.length > 0) {
|
|
436
|
+
console.log(`[${ts}] Fixed ${zombies.length} zombie(s):`);
|
|
437
|
+
zombies.forEach(z => console.log(` - ${z.jobId}: ${z.reason}`));
|
|
438
|
+
}
|
|
439
|
+
const activeProjects = engine_1.engine.getActiveProjects();
|
|
440
|
+
if (activeProjects.length === 0) {
|
|
441
|
+
console.log(`[${ts}] No active projects`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
for (const project of activeProjects) {
|
|
445
|
+
const nextSeq = engine_1.engine.getNextSeq(project.id);
|
|
446
|
+
if (nextSeq === null) {
|
|
447
|
+
console.log(`[${ts}] ${project.id}: No pending jobs`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const running = engine_1.engine.getJobsBySeq(project.id, nextSeq, 'running');
|
|
451
|
+
const pending = engine_1.engine.getJobsBySeq(project.id, nextSeq, 'pending');
|
|
452
|
+
if (running.length > 0) {
|
|
453
|
+
console.log(`[${ts}] ${project.id}: ${running.length} job(s) running in seq ${nextSeq}`);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (pending.length > 0) {
|
|
457
|
+
console.log(`[${ts}] ${project.id}: Starting next seq ${nextSeq} (${pending.length} pending)`);
|
|
458
|
+
try {
|
|
459
|
+
(0, child_process_1.execSync)(`crewx x "@wbs_coordinator ${project.id} 다음 작업 진행해줘"`, { stdio: 'inherit', cwd: process.cwd() });
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
console.error(`[${ts}] Failed to call coordinator: ${e.message}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function startDaemon() {
|
|
468
|
+
const pidFile = engine_1.engine.daemonPidFile;
|
|
469
|
+
if (fs.existsSync(pidFile)) {
|
|
470
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
471
|
+
if (engine_1.engine.isProcessAlive(pid)) {
|
|
472
|
+
console.error(`[WBS Daemon] Already running (PID: ${pid})`);
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
fs.unlinkSync(pidFile);
|
|
476
|
+
}
|
|
477
|
+
const logFile = engine_1.engine.daemonLogFile;
|
|
478
|
+
const out = fs.openSync(logFile, 'a');
|
|
479
|
+
const child = (0, child_process_1.spawn)(process.execPath, [__filename, 'daemon', '--foreground'], {
|
|
480
|
+
detached: true,
|
|
481
|
+
stdio: ['ignore', out, out],
|
|
482
|
+
cwd: process.cwd(),
|
|
483
|
+
});
|
|
484
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
485
|
+
child.unref();
|
|
486
|
+
console.log(`[WBS Daemon] Started (PID: ${child.pid})`);
|
|
487
|
+
console.log(`[WBS Daemon] Log: ${logFile}`);
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
function stopDaemon() {
|
|
491
|
+
const pidFile = engine_1.engine.daemonPidFile;
|
|
492
|
+
if (!fs.existsSync(pidFile)) {
|
|
493
|
+
console.error('[WBS Daemon] Not running (no PID file)');
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
497
|
+
if (!engine_1.engine.isProcessAlive(pid)) {
|
|
498
|
+
console.log('[WBS Daemon] Process not found, cleaning up PID file');
|
|
499
|
+
fs.unlinkSync(pidFile);
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
process.kill(pid, 'SIGTERM');
|
|
504
|
+
fs.unlinkSync(pidFile);
|
|
505
|
+
console.log(`[WBS Daemon] Stopped (PID: ${pid})`);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
console.error(`[WBS Daemon] Failed to stop: ${e.message}`);
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function handleDaemon(args) {
|
|
514
|
+
const sub = args[0];
|
|
515
|
+
switch (sub) {
|
|
516
|
+
case 'start':
|
|
517
|
+
startDaemon();
|
|
518
|
+
break;
|
|
519
|
+
case 'stop':
|
|
520
|
+
stopDaemon();
|
|
521
|
+
break;
|
|
522
|
+
case 'status': {
|
|
523
|
+
const pidFile = engine_1.engine.daemonPidFile;
|
|
524
|
+
if (!fs.existsSync(pidFile)) {
|
|
525
|
+
const msg = { running: false, message: 'Not running' };
|
|
526
|
+
console.log(hasJson(args) ? JSON.stringify(msg) : `[WBS Daemon] Not running`);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
530
|
+
const alive = engine_1.engine.isProcessAlive(pid);
|
|
531
|
+
const msg = alive ? { running: true, pid, message: `Running (PID: ${pid})` } : { running: false, pid, message: 'Stale PID file (process dead)' };
|
|
532
|
+
console.log(hasJson(args) ? JSON.stringify(msg, null, 2) : `[WBS Daemon] ${msg.message}`);
|
|
533
|
+
}
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case 'restart':
|
|
537
|
+
stopDaemon();
|
|
538
|
+
setTimeout(() => startDaemon(), 1000);
|
|
539
|
+
break;
|
|
540
|
+
case '--foreground': {
|
|
541
|
+
console.log(`[WBS Daemon] Starting in foreground mode`);
|
|
542
|
+
console.log(`[WBS Daemon] Interval: ${engine_1.DAEMON_INTERVAL_MS / 1000 / 60} minutes`);
|
|
543
|
+
console.log(`[WBS Daemon] Zombie timeout: ${engine_1.ZOMBIE_TIMEOUT_MS / 1000 / 60} minutes`);
|
|
544
|
+
console.log(`[WBS Daemon] Press Ctrl+C to stop\n`);
|
|
545
|
+
daemonTick();
|
|
546
|
+
setInterval(daemonTick, engine_1.DAEMON_INTERVAL_MS);
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
case 'tick':
|
|
550
|
+
daemonTick();
|
|
551
|
+
break;
|
|
552
|
+
default:
|
|
553
|
+
console.log(`
|
|
554
|
+
Daemon Commands - Background scheduler for WBS
|
|
555
|
+
|
|
556
|
+
Usage:
|
|
557
|
+
wbs daemon start Start daemon in background
|
|
558
|
+
wbs daemon stop Stop running daemon
|
|
559
|
+
wbs daemon status Check daemon status
|
|
560
|
+
wbs daemon restart Restart daemon
|
|
561
|
+
wbs daemon tick Manual tick (for testing)
|
|
562
|
+
`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function showDetailedUsage() {
|
|
566
|
+
const skillMd = path.join(__dirname, '..', 'SKILL.md');
|
|
567
|
+
if (fs.existsSync(skillMd)) {
|
|
568
|
+
process.stdout.write(fs.readFileSync(skillMd, 'utf-8'));
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
showHelp();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function showHelp() {
|
|
575
|
+
console.log(`
|
|
576
|
+
@crewx/wbs@${PKG_VERSION} - Work Breakdown Structure for AI agent teams
|
|
577
|
+
|
|
578
|
+
Usage:
|
|
579
|
+
wbs <command> [options]
|
|
580
|
+
|
|
581
|
+
Project Commands:
|
|
582
|
+
wbs create "Title" [--detail "path"] Create a new WBS project
|
|
583
|
+
wbs list [--json] List all projects
|
|
584
|
+
wbs status <wbs-id> [--json] Get project status with jobs summary
|
|
585
|
+
wbs delete <wbs-id> Delete a project and its jobs
|
|
586
|
+
|
|
587
|
+
Job Commands:
|
|
588
|
+
wbs job list <wbs-id> [--json] List all jobs in a project
|
|
589
|
+
wbs job add <wbs-id> [opts] Add a job to the project
|
|
590
|
+
wbs job update <job-id> [opts] Update a job
|
|
591
|
+
wbs job next <wbs-id> Run the next pending job
|
|
592
|
+
wbs job run <wbs-id> Run all pending jobs
|
|
593
|
+
wbs job retry <job-id> Retry a failed/completed job
|
|
594
|
+
|
|
595
|
+
Execution Commands:
|
|
596
|
+
wbs exec list <job-id> List execution history for a job
|
|
597
|
+
wbs exec status <exec-id> Get execution details
|
|
598
|
+
wbs exec kill <exec-id> Kill a running execution
|
|
599
|
+
wbs exec running List all running executions
|
|
600
|
+
|
|
601
|
+
Daemon Commands:
|
|
602
|
+
wbs daemon start Start background scheduler
|
|
603
|
+
wbs daemon stop Stop daemon
|
|
604
|
+
wbs daemon status Check daemon status
|
|
605
|
+
|
|
606
|
+
wbs usage Show detailed guide
|
|
607
|
+
|
|
608
|
+
Options:
|
|
609
|
+
--json Output raw JSON
|
|
610
|
+
--version Show version
|
|
611
|
+
--help, -h Show this help
|
|
612
|
+
|
|
613
|
+
Examples:
|
|
614
|
+
wbs create "User Authentication"
|
|
615
|
+
wbs job add wbs-1 --title "Design API" --agent "@backend" --seq 1
|
|
616
|
+
wbs status wbs-1
|
|
617
|
+
wbs job run wbs-1
|
|
618
|
+
|
|
619
|
+
For detailed usage guide: wbs usage
|
|
620
|
+
`);
|
|
621
|
+
}
|
|
622
|
+
async function main(inputArgs) {
|
|
623
|
+
const args = inputArgs ?? process.argv.slice(2);
|
|
624
|
+
const command = args[0];
|
|
625
|
+
if (command === '--version') {
|
|
626
|
+
console.log(PKG_VERSION);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
switch (command) {
|
|
630
|
+
case 'create': {
|
|
631
|
+
const title = args[1];
|
|
632
|
+
if (!title) {
|
|
633
|
+
console.error('Usage: wbs create "Project Title" [--detail "path"] [--source "branch"] [--target "branch"]');
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
const opts = parseOpts(args, 2);
|
|
637
|
+
const detailOpt = opts.detail ?? null;
|
|
638
|
+
const sourceBranchOpt = opts.source || undefined;
|
|
639
|
+
const targetBranchOpt = opts.target || undefined;
|
|
640
|
+
const id = engine_1.engine.create(title, detailOpt, sourceBranchOpt, targetBranchOpt);
|
|
641
|
+
let detailPath = detailOpt;
|
|
642
|
+
if (!detailPath) {
|
|
643
|
+
const detailsDir = path.join(process.cwd(), 'wbs-details');
|
|
644
|
+
if (!fs.existsSync(detailsDir))
|
|
645
|
+
fs.mkdirSync(detailsDir, { recursive: true });
|
|
646
|
+
detailPath = `wbs-details/${id}-detail.md`;
|
|
647
|
+
const detailFile = path.join(process.cwd(), detailPath);
|
|
648
|
+
fs.writeFileSync(detailFile, `# ${title}\n\n## 개요\n\n(프로젝트 개요를 작성하세요)\n\n## 요구사항\n\n(요구사항을 작성하세요)\n\n## 참고 자료\n\n(참고 자료 링크나 설명을 추가하세요)\n`);
|
|
649
|
+
engine_1.engine.updateDetailPath(id, detailPath);
|
|
650
|
+
}
|
|
651
|
+
console.log(JSON.stringify({ id, title, detailPath }, null, 2));
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
case 'list': {
|
|
655
|
+
const projects = engine_1.engine.list();
|
|
656
|
+
console.log(hasJson(args) ? JSON.stringify(projects, null, 2) : fmtProjectList(projects));
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case 'status': {
|
|
660
|
+
const wbsId = args[1];
|
|
661
|
+
if (!wbsId) {
|
|
662
|
+
console.error('Usage: wbs status <wbs-id> [--json]');
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
const result = engine_1.engine.status(wbsId);
|
|
666
|
+
if (result.project) {
|
|
667
|
+
const pending = result.jobs.filter(j => j.status === 'pending').length;
|
|
668
|
+
const running = result.jobs.filter(j => j.status === 'running').length;
|
|
669
|
+
const completed = result.jobs.filter(j => j.status === 'completed').length;
|
|
670
|
+
const failed = result.jobs.filter(j => j.status === 'failed').length;
|
|
671
|
+
result.summary = {
|
|
672
|
+
total: result.jobs.length, pending, running, completed, failed,
|
|
673
|
+
progress: result.jobs.length > 0 ? Math.round((completed / result.jobs.length) * 100) : 0,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
console.log(hasJson(args) ? JSON.stringify(result, null, 2) : fmtStatus(result));
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
case 'delete': {
|
|
680
|
+
const wbsId = args[1];
|
|
681
|
+
if (!wbsId) {
|
|
682
|
+
console.error('Usage: wbs delete <wbs-id>');
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
engine_1.engine.delete(wbsId);
|
|
686
|
+
console.log(`Deleted: ${wbsId}`);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case 'help':
|
|
690
|
+
showHelp();
|
|
691
|
+
break;
|
|
692
|
+
case 'usage':
|
|
693
|
+
showDetailedUsage();
|
|
694
|
+
break;
|
|
695
|
+
case 'job':
|
|
696
|
+
handleJob(args.slice(1));
|
|
697
|
+
break;
|
|
698
|
+
case 'exec':
|
|
699
|
+
handleExec(args.slice(1));
|
|
700
|
+
break;
|
|
701
|
+
case 'daemon':
|
|
702
|
+
handleDaemon(args.slice(1));
|
|
703
|
+
break;
|
|
704
|
+
default: showHelp();
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (require.main === module) {
|
|
708
|
+
const _args = process.argv.slice(2);
|
|
709
|
+
const _isDaemonForeground = _args[0] === 'daemon' && _args.includes('--foreground');
|
|
710
|
+
(_isDaemonForeground ? Promise.resolve(null) : loadTracer()).then(tracer => {
|
|
711
|
+
const opts = tracer ? { skillVersion: tracer.getOwnSkillVersion(__dirname) } : {};
|
|
712
|
+
return tracer ? tracer.run('crewx/wbs', () => main(), opts) : main();
|
|
713
|
+
}).catch((e) => {
|
|
714
|
+
console.error(e.message);
|
|
715
|
+
process.exit(1);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
//# sourceMappingURL=cli.js.map
|