zerg-status 1.1.0 → 2.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/dist/__tests__/commands.test.d.ts +4 -0
- package/dist/__tests__/commands.test.js +258 -0
- package/dist/commands/ask.d.ts +8 -0
- package/dist/commands/ask.js +13 -0
- package/dist/commands/done.d.ts +7 -0
- package/dist/commands/done.js +12 -0
- package/dist/commands/notify.d.ts +1 -0
- package/dist/commands/notify.js +8 -0
- package/dist/index.d.ts +8 -11
- package/dist/index.js +67 -52
- package/dist/state.d.ts +11 -0
- package/dist/state.js +46 -1
- package/dist/types.d.ts +13 -1
- package/dist/types.js +0 -1
- package/dist/validate.d.ts +25 -1
- package/dist/validate.js +70 -0
- package/package.json +1 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for zerg-status CLI commands
|
|
3
|
+
*/
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
10
|
+
import assert from 'node:assert';
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const STATE_DIR = path.join(os.homedir(), '.zerg-status');
|
|
13
|
+
const BIN_PATH = path.join(__dirname, '../../dist/index.js');
|
|
14
|
+
// Helper to run zerg-status commands
|
|
15
|
+
function runCommand(args) {
|
|
16
|
+
try {
|
|
17
|
+
const stdout = execSync(`node ${BIN_PATH} ${args}`, {
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
timeout: 5000,
|
|
20
|
+
});
|
|
21
|
+
return { stdout, stderr: '', exitCode: 0 };
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const error = err;
|
|
25
|
+
return {
|
|
26
|
+
stdout: error.stdout || '',
|
|
27
|
+
stderr: error.stderr || '',
|
|
28
|
+
exitCode: error.status || 1,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Helper to parse ZERG_SESSION output
|
|
33
|
+
function parseSessionOutput(output) {
|
|
34
|
+
const match = output.match(/ZERG_SESSION:([a-f0-9]+)(?::(.+))?/);
|
|
35
|
+
if (!match)
|
|
36
|
+
return null;
|
|
37
|
+
return { sessionId: match[1], name: match[2] };
|
|
38
|
+
}
|
|
39
|
+
// Helper to parse ZERG_STATUS output
|
|
40
|
+
function parseStatusOutput(output) {
|
|
41
|
+
const match = output.match(/ZERG_STATUS:(\{[\s\S]*\})/);
|
|
42
|
+
if (!match)
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(match[1]);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Clean up test sessions
|
|
52
|
+
function cleanupSession(sessionId) {
|
|
53
|
+
const statePath = path.join(STATE_DIR, `${sessionId}.json`);
|
|
54
|
+
if (fs.existsSync(statePath)) {
|
|
55
|
+
fs.unlinkSync(statePath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Track test session for cleanup
|
|
59
|
+
let testSessionId = '';
|
|
60
|
+
describe('zerg-status CLI', () => {
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
// Clean up test session if created
|
|
63
|
+
if (testSessionId) {
|
|
64
|
+
cleanupSession(testSessionId);
|
|
65
|
+
testSessionId = '';
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
describe('init command', () => {
|
|
69
|
+
test('should create a new session with name', () => {
|
|
70
|
+
const result = runCommand('init "Fix auth bug"');
|
|
71
|
+
assert.strictEqual(result.exitCode, 0);
|
|
72
|
+
const parsed = parseSessionOutput(result.stdout);
|
|
73
|
+
assert.ok(parsed, 'Should parse session output');
|
|
74
|
+
assert.match(parsed.sessionId, /^[a-f0-9]{8}$/);
|
|
75
|
+
assert.strictEqual(parsed.name, 'Fix auth bug');
|
|
76
|
+
testSessionId = parsed.sessionId;
|
|
77
|
+
});
|
|
78
|
+
test('should create a new session without name', () => {
|
|
79
|
+
const result = runCommand('init');
|
|
80
|
+
assert.strictEqual(result.exitCode, 0);
|
|
81
|
+
const parsed = parseSessionOutput(result.stdout);
|
|
82
|
+
assert.ok(parsed, 'Should parse session output');
|
|
83
|
+
assert.match(parsed.sessionId, /^[a-f0-9]{8}$/);
|
|
84
|
+
testSessionId = parsed.sessionId;
|
|
85
|
+
});
|
|
86
|
+
test('should reject names that are too long', () => {
|
|
87
|
+
const result = runCommand('init "This is a very long session name that has more than six words"');
|
|
88
|
+
assert.strictEqual(result.exitCode, 1);
|
|
89
|
+
assert.ok(result.stderr.includes('too long') || result.stderr.includes('Max'), 'Should reject names that are too long');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('ask command', () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
const initResult = runCommand('init "Test session"');
|
|
95
|
+
const parsed = parseSessionOutput(initResult.stdout);
|
|
96
|
+
testSessionId = parsed.sessionId;
|
|
97
|
+
});
|
|
98
|
+
test('should set state to ask with question', () => {
|
|
99
|
+
const result = runCommand(`-s ${testSessionId} ask "Which database to use?"`);
|
|
100
|
+
assert.strictEqual(result.exitCode, 0);
|
|
101
|
+
const status = parseStatusOutput(result.stdout);
|
|
102
|
+
assert.ok(status, 'Should parse status output');
|
|
103
|
+
assert.strictEqual(status.state, 'ask');
|
|
104
|
+
assert.strictEqual(status.ask, 'Which database to use?');
|
|
105
|
+
});
|
|
106
|
+
test('should set ask with clickable options', () => {
|
|
107
|
+
const result = runCommand(`-s ${testSessionId} ask "Which database?" -- "Redis: Fast caching" "PostgreSQL: Relational DB"`);
|
|
108
|
+
assert.strictEqual(result.exitCode, 0);
|
|
109
|
+
const status = parseStatusOutput(result.stdout);
|
|
110
|
+
assert.ok(status, 'Should parse status output');
|
|
111
|
+
assert.strictEqual(status.state, 'ask');
|
|
112
|
+
assert.strictEqual(status.ask, 'Which database?');
|
|
113
|
+
assert.deepStrictEqual(status.askOptions, [
|
|
114
|
+
{ name: 'Redis', desc: 'Fast caching' },
|
|
115
|
+
{ name: 'PostgreSQL', desc: 'Relational DB' },
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
test('should parse options without descriptions', () => {
|
|
119
|
+
const result = runCommand(`-s ${testSessionId} ask "Yes or no?" -- "Yes" "No"`);
|
|
120
|
+
assert.strictEqual(result.exitCode, 0);
|
|
121
|
+
const status = parseStatusOutput(result.stdout);
|
|
122
|
+
assert.deepStrictEqual(status?.askOptions, [
|
|
123
|
+
{ name: 'Yes' },
|
|
124
|
+
{ name: 'No' },
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
test('should reject too few options', () => {
|
|
128
|
+
const result = runCommand(`-s ${testSessionId} ask "Question?" -- "OnlyOne"`);
|
|
129
|
+
assert.strictEqual(result.exitCode, 1);
|
|
130
|
+
assert.ok(result.stderr.includes('Too few options'), 'Should reject too few options');
|
|
131
|
+
});
|
|
132
|
+
test('should reject too many options', () => {
|
|
133
|
+
const result = runCommand(`-s ${testSessionId} ask "Question?" -- "A" "B" "C" "D" "E" "F"`);
|
|
134
|
+
assert.strictEqual(result.exitCode, 1);
|
|
135
|
+
assert.ok(result.stderr.includes('Too many options'), 'Should reject too many options');
|
|
136
|
+
});
|
|
137
|
+
test('should require a question', () => {
|
|
138
|
+
const result = runCommand(`-s ${testSessionId} ask`);
|
|
139
|
+
assert.strictEqual(result.exitCode, 1);
|
|
140
|
+
assert.ok(result.stderr.includes('Question text required'), 'Should require question text');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('done command', () => {
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
const initResult = runCommand('init "Test session"');
|
|
146
|
+
const parsed = parseSessionOutput(initResult.stdout);
|
|
147
|
+
testSessionId = parsed.sessionId;
|
|
148
|
+
});
|
|
149
|
+
test('should set state to done with summary', () => {
|
|
150
|
+
const result = runCommand(`-s ${testSessionId} done "Fixed auth bug. All tests pass."`);
|
|
151
|
+
assert.strictEqual(result.exitCode, 0);
|
|
152
|
+
const status = parseStatusOutput(result.stdout);
|
|
153
|
+
assert.ok(status, 'Should parse status output');
|
|
154
|
+
assert.strictEqual(status.state, 'done');
|
|
155
|
+
assert.strictEqual(status.done, 'Fixed auth bug. All tests pass.');
|
|
156
|
+
});
|
|
157
|
+
test('should clear ask fields when done', () => {
|
|
158
|
+
// First set to ask
|
|
159
|
+
runCommand(`-s ${testSessionId} ask "Question?" -- "A" "B"`);
|
|
160
|
+
// Then set to done
|
|
161
|
+
const result = runCommand(`-s ${testSessionId} done "Completed the task."`);
|
|
162
|
+
const status = parseStatusOutput(result.stdout);
|
|
163
|
+
assert.strictEqual(status?.state, 'done');
|
|
164
|
+
assert.strictEqual(status?.ask, undefined);
|
|
165
|
+
assert.strictEqual(status?.askOptions, undefined);
|
|
166
|
+
});
|
|
167
|
+
test('should require a summary', () => {
|
|
168
|
+
const result = runCommand(`-s ${testSessionId} done`);
|
|
169
|
+
assert.strictEqual(result.exitCode, 1);
|
|
170
|
+
assert.ok(result.stderr.includes('Result summary required'), 'Should require summary');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('now command', () => {
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
const initResult = runCommand('init "Test session"');
|
|
176
|
+
const parsed = parseSessionOutput(initResult.stdout);
|
|
177
|
+
testSessionId = parsed.sessionId;
|
|
178
|
+
});
|
|
179
|
+
test('should set current activity', () => {
|
|
180
|
+
const result = runCommand(`-s ${testSessionId} now "Analyzing auth flow"`);
|
|
181
|
+
assert.strictEqual(result.exitCode, 0);
|
|
182
|
+
const status = parseStatusOutput(result.stdout);
|
|
183
|
+
assert.strictEqual(status?.now, 'Analyzing auth flow');
|
|
184
|
+
});
|
|
185
|
+
test('should reject text that is too long', () => {
|
|
186
|
+
const longText = 'A'.repeat(60);
|
|
187
|
+
const result = runCommand(`-s ${testSessionId} now "${longText}"`);
|
|
188
|
+
assert.strictEqual(result.exitCode, 1);
|
|
189
|
+
assert.ok(result.stderr.includes('too long'), 'Should reject long text');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('tasks commands', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
const initResult = runCommand('init "Test session"');
|
|
195
|
+
const parsed = parseSessionOutput(initResult.stdout);
|
|
196
|
+
testSessionId = parsed.sessionId;
|
|
197
|
+
});
|
|
198
|
+
test('should set task list', () => {
|
|
199
|
+
const result = runCommand(`-s ${testSessionId} tasks set "Analyze" "Implement" "Test"`);
|
|
200
|
+
assert.strictEqual(result.exitCode, 0);
|
|
201
|
+
const status = parseStatusOutput(result.stdout);
|
|
202
|
+
assert.deepStrictEqual(status?.tasks, [
|
|
203
|
+
{ task: 'Analyze', status: 'todo' },
|
|
204
|
+
{ task: 'Implement', status: 'todo' },
|
|
205
|
+
{ task: 'Test', status: 'todo' },
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
test('should mark task as doing', () => {
|
|
209
|
+
runCommand(`-s ${testSessionId} tasks set "Analyze" "Implement" "Test"`);
|
|
210
|
+
const result = runCommand(`-s ${testSessionId} task doing "Analyze"`);
|
|
211
|
+
assert.strictEqual(result.exitCode, 0);
|
|
212
|
+
const status = parseStatusOutput(result.stdout);
|
|
213
|
+
const tasks = status?.tasks;
|
|
214
|
+
const analyzeTask = tasks.find(t => t.task === 'Analyze');
|
|
215
|
+
assert.strictEqual(analyzeTask?.status, 'doing');
|
|
216
|
+
});
|
|
217
|
+
test('should mark task as done', () => {
|
|
218
|
+
runCommand(`-s ${testSessionId} tasks set "Analyze" "Implement" "Test"`);
|
|
219
|
+
const result = runCommand(`-s ${testSessionId} task done "Analyze"`);
|
|
220
|
+
assert.strictEqual(result.exitCode, 0);
|
|
221
|
+
const status = parseStatusOutput(result.stdout);
|
|
222
|
+
const tasks = status?.tasks;
|
|
223
|
+
const analyzeTask = tasks.find(t => t.task === 'Analyze');
|
|
224
|
+
assert.strictEqual(analyzeTask?.status, 'done');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
describe('full workflow', () => {
|
|
228
|
+
test('should handle a complete session workflow', () => {
|
|
229
|
+
// 1. Initialize session
|
|
230
|
+
const initResult = runCommand('init "Fix auth bug"');
|
|
231
|
+
const parsed = parseSessionOutput(initResult.stdout);
|
|
232
|
+
testSessionId = parsed.sessionId;
|
|
233
|
+
// 2. Set up tasks
|
|
234
|
+
runCommand(`-s ${testSessionId} tasks set "Investigate" "Fix" "Test"`);
|
|
235
|
+
runCommand(`-s ${testSessionId} task doing "Investigate"`);
|
|
236
|
+
runCommand(`-s ${testSessionId} now "Analyzing auth module"`);
|
|
237
|
+
// 3. Ask a question with options
|
|
238
|
+
const askResult = runCommand(`-s ${testSessionId} ask "Use JWT or sessions?" -- "JWT: Stateless" "Sessions: Simpler"`);
|
|
239
|
+
let status = parseStatusOutput(askResult.stdout);
|
|
240
|
+
assert.strictEqual(status?.state, 'ask');
|
|
241
|
+
assert.strictEqual(status?.ask, 'Use JWT or sessions?');
|
|
242
|
+
const askOptions = status?.askOptions;
|
|
243
|
+
assert.strictEqual(askOptions.length, 2);
|
|
244
|
+
// 4. Resume work after getting answer
|
|
245
|
+
runCommand(`-s ${testSessionId} task done "Investigate"`);
|
|
246
|
+
runCommand(`-s ${testSessionId} task doing "Fix"`);
|
|
247
|
+
runCommand(`-s ${testSessionId} now "Implementing JWT auth"`);
|
|
248
|
+
// 5. Finish
|
|
249
|
+
const doneResult = runCommand(`-s ${testSessionId} done "Fixed auth bug. JWT tokens working, 12 tests added."`);
|
|
250
|
+
status = parseStatusOutput(doneResult.stdout);
|
|
251
|
+
assert.strictEqual(status?.state, 'done');
|
|
252
|
+
assert.strictEqual(status?.done, 'Fixed auth bug. JWT tokens working, 12 tests added.');
|
|
253
|
+
// ask fields should be cleared
|
|
254
|
+
assert.strictEqual(status?.ask, undefined);
|
|
255
|
+
assert.strictEqual(status?.askOptions, undefined);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask command - Set state to blocked with a question and optional clickable options.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* zerg-status -s <id> ask "Which database?"
|
|
6
|
+
* zerg-status -s <id> ask "Which database?" -- "Redis: Fast" "Postgres: Simple"
|
|
7
|
+
*/
|
|
8
|
+
export declare function askCommand(sessionId: string, question: string, options?: string[]): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { setAsk } from '../state.js';
|
|
2
|
+
import { formatStatusOutput } from '../output.js';
|
|
3
|
+
/**
|
|
4
|
+
* ask command - Set state to blocked with a question and optional clickable options.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* zerg-status -s <id> ask "Which database?"
|
|
8
|
+
* zerg-status -s <id> ask "Which database?" -- "Redis: Fast" "Postgres: Simple"
|
|
9
|
+
*/
|
|
10
|
+
export function askCommand(sessionId, question, options) {
|
|
11
|
+
const state = setAsk(sessionId, question, options);
|
|
12
|
+
console.log(formatStatusOutput(state));
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { setDone } from '../state.js';
|
|
2
|
+
import { formatStatusOutput } from '../output.js';
|
|
3
|
+
/**
|
|
4
|
+
* done command - Set state to finished with a result summary.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* zerg-status -s <id> done "Fixed auth bug. JWT refresh working, 12 tests pass."
|
|
8
|
+
*/
|
|
9
|
+
export function doneCommand(sessionId, summary) {
|
|
10
|
+
const state = setDone(sessionId, summary);
|
|
11
|
+
console.log(formatStatusOutput(state));
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function notifyCommand(sessionId: string, text: string): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { setNotify } from '../state.js';
|
|
2
|
+
import { validateNotify } from '../validate.js';
|
|
3
|
+
import { formatStatusOutput } from '../output.js';
|
|
4
|
+
export function notifyCommand(sessionId, text) {
|
|
5
|
+
const validText = validateNotify(text);
|
|
6
|
+
const state = setNotify(sessionId, validText);
|
|
7
|
+
console.log(formatStatusOutput(state));
|
|
8
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* zerg-status - Progress reporting CLI for Zerg dashboard
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* COMMANDS:
|
|
6
6
|
* npx zerg-status init "Session Name" # Initialize session with name
|
|
7
|
-
* npx zerg-status
|
|
8
|
-
* npx zerg-status -s <id>
|
|
9
|
-
* npx zerg-status -s <id>
|
|
10
|
-
* npx zerg-status -s <id>
|
|
11
|
-
* npx zerg-status -s <id>
|
|
12
|
-
* npx zerg-status -s <id> task
|
|
13
|
-
* npx zerg-status -s <id> task
|
|
14
|
-
* npx zerg-status -s <id> task done "Task" # Mark task as complete
|
|
15
|
-
* npx zerg-status -s <id> task clear # Clear all tasks
|
|
16
|
-
* npx zerg-status -s <id> tasks set "A" "B" "C" # Replace all tasks
|
|
7
|
+
* npx zerg-status -s <id> now "Activity" # Set current activity (while working)
|
|
8
|
+
* npx zerg-status -s <id> ask "Question?" # Set blocked state with question
|
|
9
|
+
* npx zerg-status -s <id> ask "Q?" -- "A: desc" "B: desc" # With clickable options
|
|
10
|
+
* npx zerg-status -s <id> done "Result summary" # Set done state with summary
|
|
11
|
+
* npx zerg-status -s <id> tasks set "A" "B" "C" # Set task list
|
|
12
|
+
* npx zerg-status -s <id> task doing "A" # Mark task in progress
|
|
13
|
+
* npx zerg-status -s <id> task done "A" # Mark task complete
|
|
17
14
|
*/
|
|
18
15
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -2,23 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* zerg-status - Progress reporting CLI for Zerg dashboard
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* COMMANDS:
|
|
6
6
|
* npx zerg-status init "Session Name" # Initialize session with name
|
|
7
|
-
* npx zerg-status
|
|
8
|
-
* npx zerg-status -s <id>
|
|
9
|
-
* npx zerg-status -s <id>
|
|
10
|
-
* npx zerg-status -s <id>
|
|
11
|
-
* npx zerg-status -s <id>
|
|
12
|
-
* npx zerg-status -s <id> task
|
|
13
|
-
* npx zerg-status -s <id> task
|
|
14
|
-
* npx zerg-status -s <id> task done "Task" # Mark task as complete
|
|
15
|
-
* npx zerg-status -s <id> task clear # Clear all tasks
|
|
16
|
-
* npx zerg-status -s <id> tasks set "A" "B" "C" # Replace all tasks
|
|
7
|
+
* npx zerg-status -s <id> now "Activity" # Set current activity (while working)
|
|
8
|
+
* npx zerg-status -s <id> ask "Question?" # Set blocked state with question
|
|
9
|
+
* npx zerg-status -s <id> ask "Q?" -- "A: desc" "B: desc" # With clickable options
|
|
10
|
+
* npx zerg-status -s <id> done "Result summary" # Set done state with summary
|
|
11
|
+
* npx zerg-status -s <id> tasks set "A" "B" "C" # Set task list
|
|
12
|
+
* npx zerg-status -s <id> task doing "A" # Mark task in progress
|
|
13
|
+
* npx zerg-status -s <id> task done "A" # Mark task complete
|
|
17
14
|
*/
|
|
18
15
|
import { init } from './commands/init.js';
|
|
19
|
-
import { stateCommand } from './commands/state.js';
|
|
20
16
|
import { nowCommand } from './commands/now.js';
|
|
21
|
-
import {
|
|
17
|
+
import { askCommand } from './commands/ask.js';
|
|
18
|
+
import { doneCommand } from './commands/done.js';
|
|
22
19
|
import { taskCommand } from './commands/task.js';
|
|
23
20
|
import { tasksCommand } from './commands/tasks.js';
|
|
24
21
|
import { validateSessionId, ValidationError } from './validate.js';
|
|
@@ -27,9 +24,23 @@ function parseArgs(argv) {
|
|
|
27
24
|
let sessionId;
|
|
28
25
|
let command = 'help';
|
|
29
26
|
const commandArgs = [];
|
|
27
|
+
const optionsAfterSeparator = [];
|
|
28
|
+
let foundSeparator = false;
|
|
30
29
|
let i = 0;
|
|
31
30
|
while (i < args.length) {
|
|
32
31
|
const arg = args[i];
|
|
32
|
+
// Handle -- separator for options
|
|
33
|
+
if (arg === '--') {
|
|
34
|
+
foundSeparator = true;
|
|
35
|
+
i++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// Everything after -- goes to optionsAfterSeparator
|
|
39
|
+
if (foundSeparator) {
|
|
40
|
+
optionsAfterSeparator.push(arg);
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
33
44
|
if (arg === '-s' || arg === '--session') {
|
|
34
45
|
sessionId = args[i + 1];
|
|
35
46
|
i += 2;
|
|
@@ -43,82 +54,86 @@ function parseArgs(argv) {
|
|
|
43
54
|
commandArgs.push(arg);
|
|
44
55
|
i++;
|
|
45
56
|
}
|
|
46
|
-
return { sessionId, command, args: commandArgs };
|
|
57
|
+
return { sessionId, command, args: commandArgs, optionsAfterSeparator };
|
|
47
58
|
}
|
|
48
59
|
function printHelp() {
|
|
49
60
|
console.log(`
|
|
50
61
|
zerg-status - Progress reporting CLI for Zerg dashboard
|
|
51
62
|
|
|
52
63
|
COMMANDS:
|
|
53
|
-
init "Session Name"
|
|
54
|
-
init Initialize new session (outputs ZERG_SESSION:<id>)
|
|
55
|
-
|
|
56
|
-
-s <id> state <work|ask|done> Set session state
|
|
57
|
-
-s <id> state ask "Question?" Set state to ask with a question
|
|
58
|
-
-s <id> state done Set state to done
|
|
64
|
+
init "Session Name" Initialize session (outputs ZERG_SESSION:<id>:<name>)
|
|
59
65
|
|
|
60
|
-
-s <id> now "
|
|
61
|
-
-s <id>
|
|
66
|
+
-s <id> now "Activity" Set current activity (max 50 chars, while working)
|
|
67
|
+
-s <id> ask "Question?" Set blocked state with question
|
|
68
|
+
-s <id> ask "Q?" -- "A" "B" Set blocked with clickable options (2-5 options)
|
|
69
|
+
-s <id> done "Result summary" Set done state with result summary
|
|
62
70
|
|
|
63
|
-
-s <id>
|
|
64
|
-
-s <id> task doing "Task"
|
|
65
|
-
-s <id> task done "Task"
|
|
66
|
-
-s <id> task todo "Task" Mark task as pending
|
|
67
|
-
-s <id> task clear Clear all tasks
|
|
71
|
+
-s <id> tasks set "A" "B" "C" Set task list (shows progress bar)
|
|
72
|
+
-s <id> task doing "Task" Mark task as in progress
|
|
73
|
+
-s <id> task done "Task" Mark task as complete
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
OPTIONS FORMAT:
|
|
76
|
+
Options use "Name: Description" format. Description is optional.
|
|
77
|
+
Example: "Redis: Faster, needs separate server"
|
|
70
78
|
|
|
71
79
|
EXAMPLES:
|
|
80
|
+
# Initialize session
|
|
72
81
|
npx zerg-status init "Fix auth bug"
|
|
73
|
-
# ZERG_SESSION:f7a3b2c1:Fix auth bug
|
|
82
|
+
# Output: ZERG_SESSION:f7a3b2c1:Fix auth bug
|
|
83
|
+
|
|
84
|
+
# Set up tasks and start working
|
|
85
|
+
npx zerg-status -s f7a3b2c1 tasks set "Investigate" "Fix" "Test"
|
|
86
|
+
npx zerg-status -s f7a3b2c1 task doing "Investigate"
|
|
87
|
+
npx zerg-status -s f7a3b2c1 now "Analyzing auth module"
|
|
88
|
+
|
|
89
|
+
# When blocked with options
|
|
90
|
+
npx zerg-status -s f7a3b2c1 ask "Which database?" -- \\
|
|
91
|
+
"Redis: Faster, requires separate server" \\
|
|
92
|
+
"PostgreSQL: Simpler, uses existing DB"
|
|
74
93
|
|
|
75
|
-
|
|
76
|
-
npx zerg-status -s f7a3b2c1
|
|
77
|
-
npx zerg-status -s f7a3b2c1 tasks set "Task 1" "Task 2" "Task 3"
|
|
78
|
-
npx zerg-status -s f7a3b2c1 task doing "Task 1"
|
|
79
|
-
npx zerg-status -s f7a3b2c1 task done "Task 1"
|
|
80
|
-
npx zerg-status -s f7a3b2c1 state done
|
|
94
|
+
# When finished
|
|
95
|
+
npx zerg-status -s f7a3b2c1 done "Fixed auth bug. JWT refresh working, 8 tests added."
|
|
81
96
|
|
|
82
97
|
OUTPUT:
|
|
83
|
-
init
|
|
84
|
-
|
|
98
|
+
init: ZERG_SESSION:<id>[:<name>]
|
|
99
|
+
others: ZERG_STATUS:<json_state>
|
|
85
100
|
`);
|
|
86
101
|
}
|
|
87
102
|
function main() {
|
|
88
103
|
try {
|
|
89
|
-
const { sessionId, command, args } = parseArgs(process.argv);
|
|
104
|
+
const { sessionId, command, args, optionsAfterSeparator } = parseArgs(process.argv);
|
|
90
105
|
switch (command) {
|
|
91
106
|
case 'init': {
|
|
92
107
|
const name = args.join(' ') || undefined;
|
|
93
108
|
init(name);
|
|
94
109
|
break;
|
|
95
110
|
}
|
|
96
|
-
case '
|
|
111
|
+
case 'ask': {
|
|
97
112
|
const validSessionId = validateSessionId(sessionId);
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
100
|
-
throw new ValidationError('
|
|
113
|
+
const question = args.join(' ');
|
|
114
|
+
if (!question) {
|
|
115
|
+
throw new ValidationError('Question text required. Example: ask "Which database?"');
|
|
101
116
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
117
|
+
const options = optionsAfterSeparator.length > 0 ? optionsAfterSeparator : undefined;
|
|
118
|
+
askCommand(validSessionId, question, options);
|
|
104
119
|
break;
|
|
105
120
|
}
|
|
106
|
-
case '
|
|
121
|
+
case 'done': {
|
|
107
122
|
const validSessionId = validateSessionId(sessionId);
|
|
108
|
-
const
|
|
109
|
-
if (!
|
|
110
|
-
throw new ValidationError('
|
|
123
|
+
const summary = args.join(' ');
|
|
124
|
+
if (!summary) {
|
|
125
|
+
throw new ValidationError('Result summary required. Example: done "Fixed bug, 8 tests pass."');
|
|
111
126
|
}
|
|
112
|
-
|
|
127
|
+
doneCommand(validSessionId, summary);
|
|
113
128
|
break;
|
|
114
129
|
}
|
|
115
|
-
case '
|
|
130
|
+
case 'now': {
|
|
116
131
|
const validSessionId = validateSessionId(sessionId);
|
|
117
132
|
const text = args.join(' ');
|
|
118
133
|
if (!text) {
|
|
119
|
-
throw new ValidationError('
|
|
134
|
+
throw new ValidationError('Now text required');
|
|
120
135
|
}
|
|
121
|
-
|
|
136
|
+
nowCommand(validSessionId, text);
|
|
122
137
|
break;
|
|
123
138
|
}
|
|
124
139
|
case 'task': {
|
package/dist/state.d.ts
CHANGED
|
@@ -10,7 +10,18 @@ export declare function updateState(sessionId: string, updates: Partial<ZergStat
|
|
|
10
10
|
export declare function setState(sessionId: string, newState: State, question?: string): ZergState;
|
|
11
11
|
export declare function setNow(sessionId: string, now: string): ZergState;
|
|
12
12
|
export declare function setSummary(sessionId: string, summary: string): ZergState;
|
|
13
|
+
export declare function setNotify(sessionId: string, notify: string): ZergState;
|
|
13
14
|
export declare function addTask(sessionId: string, taskDescription: string, status?: TaskStatus): ZergState;
|
|
14
15
|
export declare function updateTaskStatus(sessionId: string, taskDescription: string, status: TaskStatus): ZergState;
|
|
15
16
|
export declare function clearTasks(sessionId: string): ZergState;
|
|
16
17
|
export declare function setTasks(sessionId: string, taskDescriptions: string[]): ZergState;
|
|
18
|
+
/**
|
|
19
|
+
* Set ask state with question and optional clickable options.
|
|
20
|
+
* This is the new unified command that sets state=ask AND stores the question.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setAsk(sessionId: string, question: string, optionStrings?: string[]): ZergState;
|
|
23
|
+
/**
|
|
24
|
+
* Set done state with result summary.
|
|
25
|
+
* This is the new unified command that sets state=done AND stores the summary.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setDone(sessionId: string, summary: string): ZergState;
|
package/dist/state.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { DEFAULT_STATE } from './types.js';
|
|
6
|
-
import { validateTaskCount, validateTask } from './validate.js';
|
|
6
|
+
import { validateTaskCount, validateTask, validateAsk, validateDone, validateAskOptions } from './validate.js';
|
|
7
7
|
const STATE_DIR = path.join(os.homedir(), '.zerg-status');
|
|
8
8
|
function ensureStateDir() {
|
|
9
9
|
if (!fs.existsSync(STATE_DIR)) {
|
|
@@ -74,6 +74,9 @@ export function setNow(sessionId, now) {
|
|
|
74
74
|
export function setSummary(sessionId, summary) {
|
|
75
75
|
return updateState(sessionId, { summary });
|
|
76
76
|
}
|
|
77
|
+
export function setNotify(sessionId, notify) {
|
|
78
|
+
return updateState(sessionId, { notify });
|
|
79
|
+
}
|
|
77
80
|
export function addTask(sessionId, taskDescription, status = 'todo') {
|
|
78
81
|
const state = loadState(sessionId);
|
|
79
82
|
validateTask(taskDescription);
|
|
@@ -113,3 +116,45 @@ export function setTasks(sessionId, taskDescriptions) {
|
|
|
113
116
|
});
|
|
114
117
|
return updateState(sessionId, { tasks });
|
|
115
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Set ask state with question and optional clickable options.
|
|
121
|
+
* This is the new unified command that sets state=ask AND stores the question.
|
|
122
|
+
*/
|
|
123
|
+
export function setAsk(sessionId, question, optionStrings) {
|
|
124
|
+
const state = loadState(sessionId);
|
|
125
|
+
// Validate inputs
|
|
126
|
+
validateAsk(question);
|
|
127
|
+
const options = optionStrings ? validateAskOptions(optionStrings) : undefined;
|
|
128
|
+
// Set state and question
|
|
129
|
+
state.state = 'ask';
|
|
130
|
+
state.ask = question;
|
|
131
|
+
state.askOptions = options;
|
|
132
|
+
// Also set legacy fields for backwards compatibility
|
|
133
|
+
state.question = question;
|
|
134
|
+
state.notify = question;
|
|
135
|
+
// Clear done field since we're back to asking
|
|
136
|
+
delete state.done;
|
|
137
|
+
saveState(state);
|
|
138
|
+
return state;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Set done state with result summary.
|
|
142
|
+
* This is the new unified command that sets state=done AND stores the summary.
|
|
143
|
+
*/
|
|
144
|
+
export function setDone(sessionId, summary) {
|
|
145
|
+
const state = loadState(sessionId);
|
|
146
|
+
// Validate input
|
|
147
|
+
validateDone(summary);
|
|
148
|
+
// Set state and summary
|
|
149
|
+
state.state = 'done';
|
|
150
|
+
state.done = summary;
|
|
151
|
+
// Also set legacy fields for backwards compatibility
|
|
152
|
+
state.summary = summary;
|
|
153
|
+
state.notify = summary;
|
|
154
|
+
// Clear ask fields since we're done
|
|
155
|
+
delete state.ask;
|
|
156
|
+
delete state.askOptions;
|
|
157
|
+
delete state.question;
|
|
158
|
+
saveState(state);
|
|
159
|
+
return state;
|
|
160
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,13 +4,25 @@ export interface Task {
|
|
|
4
4
|
task: string;
|
|
5
5
|
status: TaskStatus;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* An option for ask questions with clickable choices
|
|
9
|
+
* Format in CLI: "Name: Description" or just "Name"
|
|
10
|
+
*/
|
|
11
|
+
export interface AskOption {
|
|
12
|
+
name: string;
|
|
13
|
+
desc?: string;
|
|
14
|
+
}
|
|
7
15
|
export interface ZergState {
|
|
8
16
|
session: string;
|
|
9
17
|
name?: string;
|
|
10
18
|
state: State;
|
|
11
19
|
now: string;
|
|
12
|
-
|
|
20
|
+
ask?: string;
|
|
21
|
+
askOptions?: AskOption[];
|
|
22
|
+
done?: string;
|
|
13
23
|
tasks: Task[];
|
|
24
|
+
summary?: string;
|
|
14
25
|
question?: string;
|
|
26
|
+
notify?: string;
|
|
15
27
|
}
|
|
16
28
|
export declare const DEFAULT_STATE: Omit<ZergState, 'session'>;
|
package/dist/types.js
CHANGED
package/dist/validate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { State, TaskStatus } from './types.js';
|
|
1
|
+
import type { State, TaskStatus, AskOption } from './types.js';
|
|
2
2
|
export declare const VALID_STATES: State[];
|
|
3
3
|
export declare const VALID_TASK_STATUSES: TaskStatus[];
|
|
4
4
|
export declare const MAX_NOW_LENGTH = 50;
|
|
@@ -7,6 +7,13 @@ export declare const MAX_TASK_LENGTH = 200;
|
|
|
7
7
|
export declare const MAX_TASKS = 20;
|
|
8
8
|
export declare const MAX_NAME_LENGTH = 60;
|
|
9
9
|
export declare const MAX_NAME_WORDS = 6;
|
|
10
|
+
export declare const MAX_NOTIFY_LENGTH = 150;
|
|
11
|
+
export declare const MAX_ASK_LENGTH = 150;
|
|
12
|
+
export declare const MAX_DONE_LENGTH = 200;
|
|
13
|
+
export declare const MIN_OPTIONS = 2;
|
|
14
|
+
export declare const MAX_OPTIONS = 5;
|
|
15
|
+
export declare const MAX_OPTION_NAME_LENGTH = 30;
|
|
16
|
+
export declare const MAX_OPTION_DESC_LENGTH = 50;
|
|
10
17
|
export declare class ValidationError extends Error {
|
|
11
18
|
constructor(message: string);
|
|
12
19
|
}
|
|
@@ -14,7 +21,24 @@ export declare function validateState(state: string): State;
|
|
|
14
21
|
export declare function validateTaskStatus(status: string): TaskStatus;
|
|
15
22
|
export declare function validateNow(now: string): string;
|
|
16
23
|
export declare function validateSummary(summary: string): string;
|
|
24
|
+
export declare function validateNotify(notify: string): string;
|
|
17
25
|
export declare function validateTask(task: string): string;
|
|
18
26
|
export declare function validateTaskCount(count: number): void;
|
|
19
27
|
export declare function validateSessionId(sessionId: string | undefined): string;
|
|
20
28
|
export declare function validateName(name: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Validates the ask question text
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateAsk(ask: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Validates the done summary text
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateDone(done: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Parses an option string in the format "Name: Description" or just "Name"
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseOption(optionStr: string): AskOption;
|
|
41
|
+
/**
|
|
42
|
+
* Validates and parses ask options
|
|
43
|
+
*/
|
|
44
|
+
export declare function validateAskOptions(optionStrings: string[]): AskOption[];
|
package/dist/validate.js
CHANGED
|
@@ -6,6 +6,14 @@ export const MAX_TASK_LENGTH = 200;
|
|
|
6
6
|
export const MAX_TASKS = 20;
|
|
7
7
|
export const MAX_NAME_LENGTH = 60;
|
|
8
8
|
export const MAX_NAME_WORDS = 6;
|
|
9
|
+
export const MAX_NOTIFY_LENGTH = 150;
|
|
10
|
+
// Ask/Done validation
|
|
11
|
+
export const MAX_ASK_LENGTH = 150;
|
|
12
|
+
export const MAX_DONE_LENGTH = 200;
|
|
13
|
+
export const MIN_OPTIONS = 2;
|
|
14
|
+
export const MAX_OPTIONS = 5;
|
|
15
|
+
export const MAX_OPTION_NAME_LENGTH = 30;
|
|
16
|
+
export const MAX_OPTION_DESC_LENGTH = 50;
|
|
9
17
|
export class ValidationError extends Error {
|
|
10
18
|
constructor(message) {
|
|
11
19
|
super(message);
|
|
@@ -36,6 +44,12 @@ export function validateSummary(summary) {
|
|
|
36
44
|
}
|
|
37
45
|
return summary;
|
|
38
46
|
}
|
|
47
|
+
export function validateNotify(notify) {
|
|
48
|
+
if (notify.length > MAX_NOTIFY_LENGTH) {
|
|
49
|
+
throw new ValidationError(`Notify text too long (${notify.length} chars). Max ${MAX_NOTIFY_LENGTH} chars.`);
|
|
50
|
+
}
|
|
51
|
+
return notify;
|
|
52
|
+
}
|
|
39
53
|
export function validateTask(task) {
|
|
40
54
|
if (task.length > MAX_TASK_LENGTH) {
|
|
41
55
|
throw new ValidationError(`Task too long (${task.length} chars). Max ${MAX_TASK_LENGTH} chars.`);
|
|
@@ -63,3 +77,59 @@ export function validateName(name) {
|
|
|
63
77
|
}
|
|
64
78
|
return name;
|
|
65
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Validates the ask question text
|
|
82
|
+
*/
|
|
83
|
+
export function validateAsk(ask) {
|
|
84
|
+
if (ask.length > MAX_ASK_LENGTH) {
|
|
85
|
+
throw new ValidationError(`Ask text too long (${ask.length} chars). Max ${MAX_ASK_LENGTH} chars.`);
|
|
86
|
+
}
|
|
87
|
+
return ask;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validates the done summary text
|
|
91
|
+
*/
|
|
92
|
+
export function validateDone(done) {
|
|
93
|
+
if (done.length > MAX_DONE_LENGTH) {
|
|
94
|
+
throw new ValidationError(`Done text too long (${done.length} chars). Max ${MAX_DONE_LENGTH} chars.`);
|
|
95
|
+
}
|
|
96
|
+
return done;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parses an option string in the format "Name: Description" or just "Name"
|
|
100
|
+
*/
|
|
101
|
+
export function parseOption(optionStr) {
|
|
102
|
+
const colonIndex = optionStr.indexOf(':');
|
|
103
|
+
if (colonIndex === -1) {
|
|
104
|
+
// Just a name, no description
|
|
105
|
+
const name = optionStr.trim();
|
|
106
|
+
if (name.length > MAX_OPTION_NAME_LENGTH) {
|
|
107
|
+
throw new ValidationError(`Option name too long (${name.length} chars). Max ${MAX_OPTION_NAME_LENGTH} chars.`);
|
|
108
|
+
}
|
|
109
|
+
return { name };
|
|
110
|
+
}
|
|
111
|
+
const name = optionStr.slice(0, colonIndex).trim();
|
|
112
|
+
const desc = optionStr.slice(colonIndex + 1).trim();
|
|
113
|
+
if (name.length > MAX_OPTION_NAME_LENGTH) {
|
|
114
|
+
throw new ValidationError(`Option name "${name}" too long (${name.length} chars). Max ${MAX_OPTION_NAME_LENGTH} chars.`);
|
|
115
|
+
}
|
|
116
|
+
if (desc.length > MAX_OPTION_DESC_LENGTH) {
|
|
117
|
+
throw new ValidationError(`Option description too long (${desc.length} chars). Max ${MAX_OPTION_DESC_LENGTH} chars.`);
|
|
118
|
+
}
|
|
119
|
+
return { name, desc: desc || undefined };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Validates and parses ask options
|
|
123
|
+
*/
|
|
124
|
+
export function validateAskOptions(optionStrings) {
|
|
125
|
+
if (optionStrings.length === 0) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
if (optionStrings.length < MIN_OPTIONS) {
|
|
129
|
+
throw new ValidationError(`Too few options (${optionStrings.length}). Min ${MIN_OPTIONS} options.`);
|
|
130
|
+
}
|
|
131
|
+
if (optionStrings.length > MAX_OPTIONS) {
|
|
132
|
+
throw new ValidationError(`Too many options (${optionStrings.length}). Max ${MAX_OPTIONS} options.`);
|
|
133
|
+
}
|
|
134
|
+
return optionStrings.map(parseOption);
|
|
135
|
+
}
|