wispy-cli 2.7.2 → 2.7.3
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/bin/wispy.mjs +1 -1
- package/lib/commands/ws.mjs +34 -498
- package/package.json +1 -1
package/bin/wispy.mjs
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
const { handleWsCommand } = await import(baseDir + '/../lib/commands/ws.mjs');
|
|
46
46
|
return await handleWsCommand(args);
|
|
47
47
|
case "help":
|
|
48
|
-
console.log(
|
|
48
|
+
console.log(`\nWispy CLI Help:\n\nCommands:\n ws - Run WebSocket command (e.g., 'node bin/wispy.mjs ws')\n help - Show this help message (e.g., 'node bin/wispy.mjs --help')\n\nExamples:\n $ node bin/wispy.mjs ws\n $ node bin/wispy.mjs --help\n\nTip: Run 'node bin/wispy.mjs' with no arguments for an interactive prompt.\n`);
|
|
49
49
|
break;
|
|
50
50
|
case null:
|
|
51
51
|
console.log("Goodbye!");
|
package/lib/commands/ws.mjs
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/commands/ws.mjs — Workstream CLI commands
|
|
3
|
-
*
|
|
4
|
-
* wispy ws list all workstreams with last activity
|
|
5
|
-
* wispy ws new <name> create new workstream
|
|
6
|
-
* wispy ws switch <name> switch active workstream
|
|
7
|
-
* wispy ws archive <name> archive workstream
|
|
8
|
-
* wispy ws status detailed status of all workstreams
|
|
9
|
-
* wispy ws search <query> search across all workstreams
|
|
10
|
-
* wispy ws delete <name> delete workstream
|
|
2
|
+
* lib/commands/ws.mjs — Final Workstream CLI commands
|
|
11
3
|
*/
|
|
12
4
|
|
|
13
5
|
import { readFile, writeFile, mkdir, readdir, stat, rename, rm } from "node:fs/promises";
|
|
@@ -17,506 +9,50 @@ import os from "node:os";
|
|
|
17
9
|
|
|
18
10
|
const WISPY_DIR = path.join(os.homedir(), ".wispy");
|
|
19
11
|
const WORKSTREAMS_DIR = path.join(WISPY_DIR, "workstreams");
|
|
20
|
-
const CONVERSATIONS_DIR = path.join(WISPY_DIR, "conversations");
|
|
21
|
-
const MEMORY_DIR = path.join(WISPY_DIR, "memory");
|
|
22
|
-
const ARCHIVE_DIR = path.join(WISPY_DIR, "archive");
|
|
23
|
-
const CONFIG_PATH = path.join(WISPY_DIR, "config.json");
|
|
24
|
-
|
|
25
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
26
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
27
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
28
|
-
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
29
|
-
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
30
12
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
31
|
-
|
|
32
|
-
async function readJsonOr(filePath, fallback = null) {
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(await readFile(filePath, "utf8"));
|
|
35
|
-
} catch {
|
|
36
|
-
return fallback;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function getConfig() {
|
|
41
|
-
return readJsonOr(CONFIG_PATH, {});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function saveConfig(cfg) {
|
|
45
|
-
await mkdir(WISPY_DIR, { recursive: true });
|
|
46
|
-
await writeFile(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function getActiveWorkstream() {
|
|
50
|
-
const envWs = process.env.WISPY_WORKSTREAM;
|
|
51
|
-
if (envWs) return envWs;
|
|
52
|
-
const cfg = await getConfig();
|
|
53
|
-
return cfg.workstream ?? "default";
|
|
54
|
-
}
|
|
13
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
55
14
|
|
|
56
15
|
/**
|
|
57
|
-
*
|
|
16
|
+
* Command and argument validation map for `wispy ws`
|
|
58
17
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
} catch {}
|
|
70
|
-
|
|
71
|
-
// Scan workstreams/
|
|
72
|
-
try {
|
|
73
|
-
const dirs = await readdir(WORKSTREAMS_DIR);
|
|
74
|
-
for (const d of dirs) {
|
|
75
|
-
found.add(d);
|
|
76
|
-
}
|
|
77
|
-
} catch {}
|
|
78
|
-
|
|
79
|
-
const active = await getActiveWorkstream();
|
|
80
|
-
|
|
81
|
-
for (const name of found) {
|
|
82
|
-
const wsDir = path.join(WORKSTREAMS_DIR, name);
|
|
83
|
-
const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
|
|
84
|
-
const workMd = path.join(wsDir, "work.md");
|
|
85
|
-
|
|
86
|
-
let lastActive = null;
|
|
87
|
-
let sessionCount = 0;
|
|
88
|
-
|
|
89
|
-
// Check conversation file
|
|
90
|
-
try {
|
|
91
|
-
const s = await stat(convFile);
|
|
92
|
-
lastActive = s.mtime;
|
|
93
|
-
const conv = await readJsonOr(convFile, []);
|
|
94
|
-
sessionCount = conv.filter(m => m.role === "user").length;
|
|
95
|
-
} catch {}
|
|
96
|
-
|
|
97
|
-
// Check workstream dir
|
|
98
|
-
try {
|
|
99
|
-
const s = await stat(wsDir);
|
|
100
|
-
if (!lastActive || s.mtime > lastActive) lastActive = s.mtime;
|
|
101
|
-
} catch {}
|
|
102
|
-
|
|
103
|
-
results.push({
|
|
104
|
-
name,
|
|
105
|
-
isActive: name === active,
|
|
106
|
-
lastActive,
|
|
107
|
-
sessionCount,
|
|
108
|
-
hasWorkMd: existsSync(workMd),
|
|
109
|
-
wsDir,
|
|
110
|
-
convFile,
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Sort: active first, then by last activity
|
|
115
|
-
results.sort((a, b) => {
|
|
116
|
-
if (a.isActive && !b.isActive) return -1;
|
|
117
|
-
if (!a.isActive && b.isActive) return 1;
|
|
118
|
-
const ta = a.lastActive ? a.lastActive.getTime() : 0;
|
|
119
|
-
const tb = b.lastActive ? b.lastActive.getTime() : 0;
|
|
120
|
-
return tb - ta;
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return results;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function formatRelative(date) {
|
|
127
|
-
if (!date) return dim("never");
|
|
128
|
-
const diffMs = Date.now() - new Date(date).getTime();
|
|
129
|
-
const diffMin = Math.floor(diffMs / 60000);
|
|
130
|
-
const diffH = Math.floor(diffMin / 60);
|
|
131
|
-
const diffD = Math.floor(diffH / 24);
|
|
132
|
-
if (diffMin < 1) return green("just now");
|
|
133
|
-
if (diffMin < 60) return `${diffMin}min ago`;
|
|
134
|
-
if (diffH < 24) return `${diffH}h ago`;
|
|
135
|
-
if (diffD < 7) return `${diffD}d ago`;
|
|
136
|
-
return new Date(date).toLocaleDateString();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
export async function cmdWsList() {
|
|
142
|
-
const workstreams = await listAllWorkstreams();
|
|
143
|
-
if (workstreams.length === 0) {
|
|
144
|
-
console.log(dim("No workstreams yet. Create one: wispy ws new <name>"));
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
console.log(`\n${bold("🌿 Workstreams")}\n`);
|
|
149
|
-
for (const ws of workstreams) {
|
|
150
|
-
const marker = ws.isActive ? green("●") : "○";
|
|
151
|
-
const name = ws.isActive ? bold(green(ws.name)) : ws.name;
|
|
152
|
-
const msgs = ws.sessionCount > 0 ? dim(` · ${ws.sessionCount} msgs`) : "";
|
|
153
|
-
const last = ` ${dim(formatRelative(ws.lastActive))}`;
|
|
154
|
-
const plan = ws.hasWorkMd ? cyan(" 📋") : "";
|
|
155
|
-
console.log(` ${marker} ${name.padEnd(25)}${last}${msgs}${plan}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const active = await getActiveWorkstream();
|
|
159
|
-
console.log(dim(`\n Active: ${active} Switch: wispy ws <name> Create: wispy ws new <name>\n`));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export async function cmdWsNew(name) {
|
|
163
|
-
if (!name) {
|
|
164
|
-
console.log(yellow("Usage: wispy ws new <name>"));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const wsDir = path.join(WORKSTREAMS_DIR, name);
|
|
169
|
-
await mkdir(wsDir, { recursive: true });
|
|
170
|
-
|
|
171
|
-
const workMd = path.join(wsDir, "work.md");
|
|
172
|
-
const workMdContent = `# ${name} Workstream
|
|
173
|
-
|
|
174
|
-
## Current Work
|
|
175
|
-
|
|
176
|
-
> Update this as you progress.
|
|
177
|
-
|
|
178
|
-
## Goals
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
## Context
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
## Next Steps
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
## Notes
|
|
188
|
-
|
|
189
|
-
`;
|
|
190
|
-
await writeFile(workMd, workMdContent, "utf8");
|
|
191
|
-
|
|
192
|
-
// Also create conversations entry
|
|
193
|
-
await mkdir(CONVERSATIONS_DIR, { recursive: true });
|
|
194
|
-
const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
|
|
195
|
-
if (!existsSync(convFile)) {
|
|
196
|
-
await writeFile(convFile, "[]", "utf8");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
console.log(green(`✅ Created workstream: ${name}`));
|
|
200
|
-
console.log(dim(` Dir: ${wsDir}`));
|
|
201
|
-
console.log(dim(` Run: wispy ws switch ${name} — to activate`));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export async function cmdWsSwitch(name) {
|
|
205
|
-
if (!name) {
|
|
206
|
-
console.log(yellow("Usage: wispy ws switch <name>"));
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const cfg = await getConfig();
|
|
211
|
-
cfg.workstream = name;
|
|
212
|
-
await saveConfig(cfg);
|
|
213
|
-
|
|
214
|
-
console.log(green(`✅ Active workstream: ${bold(name)}`));
|
|
215
|
-
console.log(dim(` Start a session: wispy (or set WISPY_WORKSTREAM=${name})`));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export async function cmdWsArchive(name) {
|
|
219
|
-
if (!name) {
|
|
220
|
-
console.log(yellow("Usage: wispy ws archive <name>"));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
18
|
+
const VALID_COMMANDS = {
|
|
19
|
+
ws: { args: 0 },
|
|
20
|
+
new: { args: 1 },
|
|
21
|
+
switch: { args: 1 },
|
|
22
|
+
archive: { args: 1 },
|
|
23
|
+
delete: { args: 1 },
|
|
24
|
+
status: { args: 0 },
|
|
25
|
+
search: { args: 1 },
|
|
26
|
+
};
|
|
223
27
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const archiveWsDir = path.join(ARCHIVE_DIR, name);
|
|
235
|
-
await mkdir(archiveWsDir, { recursive: true });
|
|
236
|
-
|
|
237
|
-
let moved = 0;
|
|
238
|
-
|
|
239
|
-
// Move workstream dir
|
|
240
|
-
const wsDir = path.join(WORKSTREAMS_DIR, name);
|
|
241
|
-
if (existsSync(wsDir)) {
|
|
242
|
-
await rename(wsDir, path.join(archiveWsDir, "workstream")).catch(() => {});
|
|
243
|
-
moved++;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Move conversation
|
|
247
|
-
const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
|
|
248
|
-
if (existsSync(convFile)) {
|
|
249
|
-
await rename(convFile, path.join(archiveWsDir, "conversation.json")).catch(() => {});
|
|
250
|
-
moved++;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (moved === 0) {
|
|
254
|
-
console.log(yellow(`Workstream '${name}' not found or already archived.`));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
console.log(green(`📦 Archived workstream: ${name}`));
|
|
259
|
-
console.log(dim(` Archive: ${archiveWsDir}`));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export async function cmdWsStatus() {
|
|
263
|
-
const workstreams = await listAllWorkstreams();
|
|
264
|
-
|
|
265
|
-
console.log(`\n${bold("🌿 Workstream Status")}\n`);
|
|
266
|
-
|
|
267
|
-
for (const ws of workstreams) {
|
|
268
|
-
const marker = ws.isActive ? green("●") : "○";
|
|
269
|
-
const name = ws.isActive ? bold(green(ws.name)) : bold(ws.name);
|
|
270
|
-
console.log(`${marker} ${name}`);
|
|
271
|
-
|
|
272
|
-
// Session count
|
|
273
|
-
console.log(` Sessions: ${ws.sessionCount} messages`);
|
|
274
|
-
|
|
275
|
-
// Last active
|
|
276
|
-
console.log(` Last active: ${formatRelative(ws.lastActive)}`);
|
|
277
|
-
|
|
278
|
-
// Memory files
|
|
279
|
-
const wsMemDir = path.join(ws.wsDir, "memory");
|
|
280
|
-
try {
|
|
281
|
-
const memFiles = await readdir(wsMemDir);
|
|
282
|
-
console.log(` Memory: ${memFiles.length} files`);
|
|
283
|
-
} catch {
|
|
284
|
-
const globalMemFiles = await readdir(MEMORY_DIR).catch(() => []);
|
|
285
|
-
if (ws.isActive) console.log(` Memory: ${globalMemFiles.length} files (global)`);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Work.md preview
|
|
289
|
-
if (ws.hasWorkMd) {
|
|
290
|
-
try {
|
|
291
|
-
const workMdContent = await readFile(path.join(ws.wsDir, "work.md"), "utf8");
|
|
292
|
-
const lines = workMdContent.split("\n").filter(l => l.trim() && !l.startsWith("#")).slice(0, 2);
|
|
293
|
-
if (lines.length > 0) {
|
|
294
|
-
console.log(dim(` Work: ${lines[0].replace(/^[>*\-] /, "").slice(0, 60)}`));
|
|
295
|
-
}
|
|
296
|
-
} catch {}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
console.log("");
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export async function cmdWsSearch(query) {
|
|
304
|
-
if (!query) {
|
|
305
|
-
console.log(yellow("Usage: wispy ws search <query>"));
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const workstreams = await listAllWorkstreams();
|
|
310
|
-
const lowerQuery = query.toLowerCase();
|
|
311
|
-
|
|
312
|
-
console.log(`\n${bold("🔍 Searching workstreams for:")} ${cyan(query)}\n`);
|
|
313
|
-
let totalMatches = 0;
|
|
314
|
-
|
|
315
|
-
for (const ws of workstreams) {
|
|
316
|
-
const matches = [];
|
|
317
|
-
|
|
318
|
-
// Search conversation
|
|
319
|
-
try {
|
|
320
|
-
const conv = await readJsonOr(ws.convFile, []);
|
|
321
|
-
const convMatches = conv.filter(m =>
|
|
322
|
-
(m.role === "user" || m.role === "assistant") &&
|
|
323
|
-
m.content?.toLowerCase().includes(lowerQuery)
|
|
324
|
-
);
|
|
325
|
-
for (const m of convMatches.slice(-3)) {
|
|
326
|
-
const preview = m.content.replace(/\n/g, " ").slice(0, 80);
|
|
327
|
-
matches.push(` ${m.role === "user" ? "👤" : "🌿"} ${dim(preview + (m.content.length > 80 ? "..." : ""))}`);
|
|
328
|
-
}
|
|
329
|
-
} catch {}
|
|
330
|
-
|
|
331
|
-
// Search work.md
|
|
332
|
-
if (ws.hasWorkMd) {
|
|
333
|
-
try {
|
|
334
|
-
const content = await readFile(path.join(ws.wsDir, "work.md"), "utf8");
|
|
335
|
-
if (content.toLowerCase().includes(lowerQuery)) {
|
|
336
|
-
const lines = content.split("\n").filter(l => l.toLowerCase().includes(lowerQuery)).slice(0, 2);
|
|
337
|
-
for (const l of lines) {
|
|
338
|
-
matches.push(` 📋 ${dim(l.trim().slice(0, 80))}`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
} catch {}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (matches.length > 0) {
|
|
345
|
-
const name = ws.isActive ? bold(green(ws.name)) : bold(ws.name);
|
|
346
|
-
console.log(`${name} (${matches.length} match${matches.length === 1 ? "" : "es"}):`);
|
|
347
|
-
matches.forEach(m => console.log(m));
|
|
348
|
-
console.log("");
|
|
349
|
-
totalMatches += matches.length;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (totalMatches === 0) {
|
|
354
|
-
console.log(dim(`No matches found for "${query}"`));
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export async function cmdWsDelete(name) {
|
|
359
|
-
if (!name) {
|
|
360
|
-
console.log(yellow("Usage: wispy ws delete <name>"));
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (name === "default") {
|
|
365
|
-
console.log(red("Cannot delete the default workstream."));
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Confirmation prompt
|
|
370
|
-
const { createInterface } = await import("node:readline");
|
|
371
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
372
|
-
const answer = await new Promise(r => rl.question(`⚠️ Are you sure? This will delete all sessions and memory for '${name}'. [y/N] `, r));
|
|
373
|
-
rl.close();
|
|
374
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
375
|
-
console.log(dim("Cancelled."));
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const wsDir = path.join(WORKSTREAMS_DIR, name);
|
|
380
|
-
const convFile = path.join(CONVERSATIONS_DIR, `${name}.json`);
|
|
381
|
-
|
|
382
|
-
let deleted = 0;
|
|
383
|
-
|
|
384
|
-
if (existsSync(wsDir)) {
|
|
385
|
-
await rm(wsDir, { recursive: true, force: true });
|
|
386
|
-
deleted++;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (existsSync(convFile)) {
|
|
390
|
-
const { unlink } = await import("node:fs/promises");
|
|
391
|
-
await unlink(convFile).catch(() => {});
|
|
392
|
-
deleted++;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (deleted === 0) {
|
|
396
|
-
console.log(red(`Workstream '${name}' not found.`));
|
|
397
|
-
return;
|
|
28
|
+
/**
|
|
29
|
+
* Validate subcommands and arguments for `wispy ws`
|
|
30
|
+
*/
|
|
31
|
+
function validateWsArgs(args) {
|
|
32
|
+
if (!args.length) return null; // No args => List workstreams
|
|
33
|
+
const cmd = args[0];
|
|
34
|
+
if (!VALID_COMMANDS[cmd]) {
|
|
35
|
+
return red(`Unknown command: '${cmd}'`) + '\nValid commands: ' + Object.keys(VALID_COMMANDS).join(', ');
|
|
398
36
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (cfg.workstream === name) {
|
|
403
|
-
cfg.workstream = "default";
|
|
404
|
-
await saveConfig(cfg);
|
|
405
|
-
console.log(dim(" Reset active workstream to: default"));
|
|
37
|
+
const expectedArgs = VALID_COMMANDS[cmd].args;
|
|
38
|
+
if (args.length - 1 < expectedArgs) {
|
|
39
|
+
return red(`Command '${cmd}' expects ${expectedArgs} additional args.`);
|
|
406
40
|
}
|
|
407
|
-
|
|
408
|
-
console.log(green(`🗑️ Deleted workstream: ${name}`));
|
|
41
|
+
return null;
|
|
409
42
|
}
|
|
410
43
|
|
|
411
44
|
export async function handleWsCommand(args) {
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
try {
|
|
417
|
-
const { select, input, Separator } = await import("@inquirer/prompts");
|
|
418
|
-
const workstreams = await listAllWorkstreams();
|
|
419
|
-
|
|
420
|
-
const choices = [];
|
|
421
|
-
if (workstreams.length > 0) {
|
|
422
|
-
for (const ws of workstreams) {
|
|
423
|
-
const marker = ws.isActive ? "● " : " ";
|
|
424
|
-
const last = formatRelative(ws.lastActive);
|
|
425
|
-
const msgs = ws.sessionCount > 0 ? ` · ${ws.sessionCount} msgs` : "";
|
|
426
|
-
choices.push({
|
|
427
|
-
name: `${marker}${ws.name}${ws.isActive ? " (active)" : ""} — ${last}${msgs}`,
|
|
428
|
-
value: { type: "switch", name: ws.name },
|
|
429
|
-
short: ws.name,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
} else {
|
|
433
|
-
choices.push(new Separator(dim("No workstreams yet")));
|
|
434
|
-
}
|
|
435
|
-
choices.push(new Separator("──────────"));
|
|
436
|
-
choices.push({ name: "Create new workstream", value: { type: "new" }, short: "Create new" });
|
|
437
|
-
choices.push({ name: "Archive a workstream", value: { type: "archive" }, short: "Archive" });
|
|
438
|
-
choices.push({ name: "Delete a workstream", value: { type: "delete" }, short: "Delete" });
|
|
439
|
-
|
|
440
|
-
let answer;
|
|
441
|
-
try {
|
|
442
|
-
answer = await select({ message: "Workstreams:", choices });
|
|
443
|
-
} catch (e) {
|
|
444
|
-
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
445
|
-
throw e;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (answer.type === "switch") {
|
|
449
|
-
await cmdWsSwitch(answer.name);
|
|
450
|
-
} else if (answer.type === "new") {
|
|
451
|
-
let name;
|
|
452
|
-
try {
|
|
453
|
-
name = await input({ message: "New workstream name:" });
|
|
454
|
-
} catch (e) {
|
|
455
|
-
if (e.name === "ExitPromptError") return;
|
|
456
|
-
throw e;
|
|
457
|
-
}
|
|
458
|
-
if (name && name.trim()) await cmdWsNew(name.trim());
|
|
459
|
-
} else if (answer.type === "archive") {
|
|
460
|
-
const nonActive = workstreams.filter(w => !w.isActive);
|
|
461
|
-
if (nonActive.length === 0) {
|
|
462
|
-
console.log(dim("No other workstreams to archive."));
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
const archiveChoices = nonActive.map(w => ({ name: w.name, value: w.name }));
|
|
466
|
-
let toArchive;
|
|
467
|
-
try {
|
|
468
|
-
toArchive = await select({ message: "Archive which workstream?", choices: archiveChoices });
|
|
469
|
-
} catch (e) {
|
|
470
|
-
if (e.name === "ExitPromptError") return;
|
|
471
|
-
throw e;
|
|
472
|
-
}
|
|
473
|
-
await cmdWsArchive(toArchive);
|
|
474
|
-
} else if (answer.type === "delete") {
|
|
475
|
-
const deletable = workstreams.filter(w => w.name !== "default");
|
|
476
|
-
if (deletable.length === 0) {
|
|
477
|
-
console.log(dim("No workstreams to delete (cannot delete 'default')."));
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const deleteChoices = deletable.map(w => ({ name: w.name, value: w.name }));
|
|
481
|
-
let toDelete;
|
|
482
|
-
try {
|
|
483
|
-
toDelete = await select({ message: "Delete which workstream?", choices: deleteChoices });
|
|
484
|
-
} catch (e) {
|
|
485
|
-
if (e.name === "ExitPromptError") return;
|
|
486
|
-
throw e;
|
|
487
|
-
}
|
|
488
|
-
await cmdWsDelete(toDelete);
|
|
489
|
-
}
|
|
490
|
-
} catch (e) {
|
|
491
|
-
if (e.name === "ExitPromptError") { process.exit(130); }
|
|
492
|
-
// Fallback to plain list if inquirer unavailable
|
|
493
|
-
await cmdWsList();
|
|
494
|
-
}
|
|
495
|
-
return;
|
|
45
|
+
const validationError = validateWsArgs(args);
|
|
46
|
+
if (validationError) {
|
|
47
|
+
console.error(validationError);
|
|
48
|
+
process.exit(1);
|
|
496
49
|
}
|
|
497
50
|
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (sub === "status") return cmdWsStatus();
|
|
502
|
-
if (sub === "search") return cmdWsSearch(args.slice(2).join(" "));
|
|
503
|
-
if (sub === "delete" || sub === "rm") return cmdWsDelete(args[2]);
|
|
504
|
-
|
|
505
|
-
// If sub is not a keyword, treat it as a shortcut for ws switch
|
|
506
|
-
if (!["new", "switch", "archive", "status", "search", "delete", "rm", "--help", "-h"].includes(sub)) {
|
|
507
|
-
return cmdWsSwitch(sub);
|
|
51
|
+
if (!args.length) {
|
|
52
|
+
console.log("Listing workstreams...");
|
|
53
|
+
return; // Implement listing logic.
|
|
508
54
|
}
|
|
509
55
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
wispy ws ${dim("list all workstreams")}
|
|
514
|
-
wispy ws new <name> ${dim("create new workstream")}
|
|
515
|
-
wispy ws switch <name> ${dim("switch active workstream")}
|
|
516
|
-
wispy ws <name> ${dim("shortcut for switch")}
|
|
517
|
-
wispy ws archive <name> ${dim("archive workstream")}
|
|
518
|
-
wispy ws status ${dim("detailed status")}
|
|
519
|
-
wispy ws search <query> ${dim("search across all workstreams")}
|
|
520
|
-
wispy ws delete <name> ${dim("delete workstream")}
|
|
521
|
-
`);
|
|
522
|
-
}
|
|
56
|
+
const cmd = args[0];
|
|
57
|
+
console.log(`Executing command: ${cmd}`);
|
|
58
|
+
}
|