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.
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Integration tests for zerg-status CLI commands
3
+ */
4
+ export {};
@@ -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,7 @@
1
+ /**
2
+ * done command - Set state to finished with a result summary.
3
+ *
4
+ * Usage:
5
+ * zerg-status -s <id> done "Fixed auth bug. JWT refresh working, 12 tests pass."
6
+ */
7
+ export declare function doneCommand(sessionId: string, summary: string): void;
@@ -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
- * Usage:
5
+ * COMMANDS:
6
6
  * npx zerg-status init "Session Name" # Initialize session with name
7
- * npx zerg-status init # Initialize session, outputs ZERG_SESSION:<id>
8
- * npx zerg-status -s <id> state work # Set state to work/ask/done
9
- * npx zerg-status -s <id> state ask "Question?" # Set state to ask with a question
10
- * npx zerg-status -s <id> now "Headline" # Set current activity headline
11
- * npx zerg-status -s <id> summary "Details" # Set detailed summary
12
- * npx zerg-status -s <id> task add "Task" # Add a task
13
- * npx zerg-status -s <id> task doing "Task" # Mark task as in progress
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
- * Usage:
5
+ * COMMANDS:
6
6
  * npx zerg-status init "Session Name" # Initialize session with name
7
- * npx zerg-status init # Initialize session, outputs ZERG_SESSION:<id>
8
- * npx zerg-status -s <id> state work # Set state to work/ask/done
9
- * npx zerg-status -s <id> state ask "Question?" # Set state to ask with a question
10
- * npx zerg-status -s <id> now "Headline" # Set current activity headline
11
- * npx zerg-status -s <id> summary "Details" # Set detailed summary
12
- * npx zerg-status -s <id> task add "Task" # Add a task
13
- * npx zerg-status -s <id> task doing "Task" # Mark task as in progress
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 { summaryCommand } from './commands/summary.js';
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" Initialize new session with name (outputs ZERG_SESSION:<id>:<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 "Headline" Set current activity (max 50 chars)
61
- -s <id> summary "Details" Set detailed summary (max 500 chars)
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> task add "Task" Add a task (status: todo)
64
- -s <id> task doing "Task" Mark task as in progress
65
- -s <id> task done "Task" Mark task as complete
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
- -s <id> tasks set "A" "B" "C" Replace all tasks (max 20 tasks)
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
- npx zerg-status -s f7a3b2c1 state work
76
- npx zerg-status -s f7a3b2c1 now "Writing unit tests"
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 outputs: ZERG_SESSION:<session_id>[:<name>]
84
- all others: ZERG_STATUS:<full_json_state>
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 'state': {
111
+ case 'ask': {
97
112
  const validSessionId = validateSessionId(sessionId);
98
- const [stateValue, ...rest] = args;
99
- if (!stateValue) {
100
- throw new ValidationError('State value required. Use: work, ask, or done');
113
+ const question = args.join(' ');
114
+ if (!question) {
115
+ throw new ValidationError('Question text required. Example: ask "Which database?"');
101
116
  }
102
- const question = stateValue === 'ask' ? rest.join(' ') : undefined;
103
- stateCommand(validSessionId, stateValue, question);
117
+ const options = optionsAfterSeparator.length > 0 ? optionsAfterSeparator : undefined;
118
+ askCommand(validSessionId, question, options);
104
119
  break;
105
120
  }
106
- case 'now': {
121
+ case 'done': {
107
122
  const validSessionId = validateSessionId(sessionId);
108
- const text = args.join(' ');
109
- if (!text) {
110
- throw new ValidationError('Now text required');
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
- nowCommand(validSessionId, text);
127
+ doneCommand(validSessionId, summary);
113
128
  break;
114
129
  }
115
- case 'summary': {
130
+ case 'now': {
116
131
  const validSessionId = validateSessionId(sessionId);
117
132
  const text = args.join(' ');
118
133
  if (!text) {
119
- throw new ValidationError('Summary text required');
134
+ throw new ValidationError('Now text required');
120
135
  }
121
- summaryCommand(validSessionId, text);
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
- summary: string;
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
@@ -1,6 +1,5 @@
1
1
  export const DEFAULT_STATE = {
2
2
  state: 'work',
3
3
  now: '',
4
- summary: '',
5
4
  tasks: [],
6
5
  };
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zerg-status",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Progress reporting CLI for Zerg dashboard",
5
5
  "type": "module",
6
6
  "bin": {