propr-cli 0.8.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.
Files changed (64) hide show
  1. package/README.md +549 -0
  2. package/dist/api/agentTank.js +27 -0
  3. package/dist/api/agents.js +201 -0
  4. package/dist/api/client.js +284 -0
  5. package/dist/api/errors.js +145 -0
  6. package/dist/api/implement.js +147 -0
  7. package/dist/api/index.js +26 -0
  8. package/dist/api/logs.js +59 -0
  9. package/dist/api/plans.js +160 -0
  10. package/dist/api/relay.js +73 -0
  11. package/dist/api/repos.js +243 -0
  12. package/dist/api/settings.js +219 -0
  13. package/dist/api/system.js +53 -0
  14. package/dist/api/tasks.js +140 -0
  15. package/dist/api/todos.js +77 -0
  16. package/dist/api/types.js +6 -0
  17. package/dist/assets/.env.example +183 -0
  18. package/dist/assets/env.example.txt +198 -0
  19. package/dist/commands/agentCommands.js +405 -0
  20. package/dist/commands/checkCommands.js +384 -0
  21. package/dist/commands/implementCommands.js +178 -0
  22. package/dist/commands/index.js +22 -0
  23. package/dist/commands/initCommands.js +167 -0
  24. package/dist/commands/initStack.js +193 -0
  25. package/dist/commands/logCommands.js +170 -0
  26. package/dist/commands/planCommands.js +552 -0
  27. package/dist/commands/relayCommands.js +149 -0
  28. package/dist/commands/repoCommands.js +526 -0
  29. package/dist/commands/settingCommands.js +237 -0
  30. package/dist/commands/stackCommands.js +86 -0
  31. package/dist/commands/startCommand.js +36 -0
  32. package/dist/commands/systemCommands.js +221 -0
  33. package/dist/commands/tankCommands.js +55 -0
  34. package/dist/commands/taskCommands.js +554 -0
  35. package/dist/commands/todoCommands.js +620 -0
  36. package/dist/commands/uiDocsCommands.js +69 -0
  37. package/dist/config/ConfigManager.js +360 -0
  38. package/dist/config/index.js +8 -0
  39. package/dist/config/types.js +16 -0
  40. package/dist/index.js +276 -0
  41. package/dist/orchestrator/format.js +31 -0
  42. package/dist/orchestrator/index.js +102 -0
  43. package/dist/orchestrator/manifest.json +16 -0
  44. package/dist/orchestrator/orchestrator.mjs +798 -0
  45. package/dist/orchestrator/types.js +10 -0
  46. package/dist/tui/StartApp.js +175 -0
  47. package/dist/tui/app.js +9 -0
  48. package/dist/tui/render.js +87 -0
  49. package/dist/utils/envFile.js +65 -0
  50. package/dist/utils/index.js +8 -0
  51. package/dist/utils/io.js +186 -0
  52. package/dist/utils/parseState.js +14 -0
  53. package/dist/utils/resolveProject.js +50 -0
  54. package/dist/vendor/shared/demoMode.js +6 -0
  55. package/dist/vendor/shared/events.js +30 -0
  56. package/dist/vendor/shared/githubAuthMode.js +35 -0
  57. package/dist/vendor/shared/index.js +15 -0
  58. package/dist/vendor/shared/labelUtils.js +32 -0
  59. package/dist/vendor/shared/modelDefinitions.js +146 -0
  60. package/dist/vendor/shared/reviewPrompt.js +18 -0
  61. package/dist/vendor/shared/usageTypes.js +13 -0
  62. package/dist/vendor/shared/userWhitelist.js +30 -0
  63. package/dist/vendor/shared/validateRelayUrl.js +21 -0
  64. package/package.json +31 -0
@@ -0,0 +1,620 @@
1
+ /**
2
+ * Repository To-Do Commands
3
+ *
4
+ * CLI commands for managing repository-level to-dos.
5
+ * Provides the `todo` command group with `list`, `get`, `add`, `complete`, `delete` subcommands.
6
+ */
7
+ import { Command } from "commander";
8
+ import { createConfigManager } from "../config/index.js";
9
+ import { resolveProject, ProjectResolutionError, printOutput } from "../utils/index.js";
10
+ import { listTodos, getTodo, createTodo, updateTodo, deleteTodo, listCategories, createCategory, updateCategory, deleteCategory, reorderTodos, reorderCategories, } from "../api/index.js";
11
+ /**
12
+ * Truncates a string to a maximum length.
13
+ */
14
+ function truncate(str, maxLen) {
15
+ if (!str)
16
+ return "-";
17
+ if (str.length <= maxLen)
18
+ return str;
19
+ return str.substring(0, maxLen - 3) + "...";
20
+ }
21
+ /**
22
+ * Displays a table of todos grouped by category.
23
+ */
24
+ function displayTodosTable(todos, categories) {
25
+ const categoryMap = new Map();
26
+ for (const cat of categories) {
27
+ categoryMap.set(cat.categoryId, cat.name);
28
+ }
29
+ const idWidth = Math.max("ID".length, ...todos.map((t) => t.todoId.length));
30
+ const contentWidth = Math.max("Content".length, ...todos.map((t) => truncate(t.content, 50).length));
31
+ const categoryWidth = Math.max("Category".length, ...todos.map((t) => truncate(t.categoryId ? categoryMap.get(t.categoryId) || t.categoryId : "(none)", 20).length));
32
+ const statusWidth = "Status".length;
33
+ const header = [
34
+ "ID".padEnd(idWidth),
35
+ "Content".padEnd(contentWidth),
36
+ "Category".padEnd(categoryWidth),
37
+ "Status".padEnd(statusWidth),
38
+ ].join(" ");
39
+ console.log(header);
40
+ console.log("-".repeat(header.length));
41
+ for (const todo of todos) {
42
+ const categoryName = todo.categoryId
43
+ ? categoryMap.get(todo.categoryId) || todo.categoryId
44
+ : "(none)";
45
+ const status = todo.isCompleted ? "Done" : "Open";
46
+ const row = [
47
+ todo.todoId.padEnd(idWidth),
48
+ truncate(todo.content, 50).padEnd(contentWidth),
49
+ truncate(categoryName, 20).padEnd(categoryWidth),
50
+ status.padEnd(statusWidth),
51
+ ].join(" ");
52
+ console.log(row);
53
+ }
54
+ }
55
+ /**
56
+ * Displays detailed todo information.
57
+ */
58
+ function displayTodoDetails(todo) {
59
+ console.log("");
60
+ console.log("=".repeat(60));
61
+ console.log("To-Do Details");
62
+ console.log("=".repeat(60));
63
+ console.log("");
64
+ console.log(`ID: ${todo.todoId}`);
65
+ console.log(`Content: ${todo.content}`);
66
+ console.log(`Status: ${todo.isCompleted ? "Completed" : "Open"}`);
67
+ console.log(`Category: ${todo.categoryId || "(none)"}`);
68
+ console.log(`Created: ${new Date(todo.createdAt).toLocaleString()}`);
69
+ console.log(`Updated: ${new Date(todo.updatedAt).toLocaleString()}`);
70
+ if (todo.linkedDraftId) {
71
+ console.log(`Linked Plan: ${todo.linkedDraftId}`);
72
+ }
73
+ console.log("");
74
+ console.log("=".repeat(60));
75
+ }
76
+ /**
77
+ * Prompts the user for confirmation.
78
+ */
79
+ async function confirm(message) {
80
+ const readline = await import("readline");
81
+ const rl = readline.createInterface({
82
+ input: process.stdin,
83
+ output: process.stdout,
84
+ });
85
+ return new Promise((resolve) => {
86
+ rl.question(`${message} (y/N): `, (answer) => {
87
+ rl.close();
88
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
89
+ });
90
+ });
91
+ }
92
+ /**
93
+ * Creates the `todo` command group.
94
+ */
95
+ export function createTodoCommand() {
96
+ const todo = new Command("todo")
97
+ .description("Manage repository to-dos")
98
+ .addHelpText("after", `
99
+ Examples:
100
+ $ propr todo list # List todos for default project
101
+ $ propr todo get <todo-id> # View todo details
102
+ $ propr todo add "Fix login page" # Create a todo
103
+ $ propr todo complete <todo-id> # Mark as completed
104
+ $ propr todo delete <todo-id> # Delete a todo
105
+ `);
106
+ // todo list
107
+ todo
108
+ .command("list")
109
+ .description("List to-dos for a repository")
110
+ .option("-p, --project <project>", "Target project (owner/repo)")
111
+ .option("-a, --all", "Show all todos (open and completed)")
112
+ .option("-d, --done", "Show only completed todos")
113
+ .option("-j, --json", "Output as JSON for programmatic use")
114
+ .addHelpText("after", `
115
+ Examples:
116
+ $ propr todo list # Open todos for default project
117
+ $ propr todo list -a # All todos (open + completed)
118
+ $ propr todo list -d # Completed todos only
119
+ $ propr todo list -p myorg/myrepo # Specify project
120
+ $ propr todo list --json # JSON output
121
+ `)
122
+ .action(async (options) => {
123
+ try {
124
+ const configManager = await createConfigManager();
125
+ const project = resolveProject(options, configManager);
126
+ const [todosResult, categoriesResult] = await Promise.all([
127
+ listTodos(project),
128
+ listCategories(project),
129
+ ]);
130
+ let todos = todosResult.todos || [];
131
+ const categories = categoriesResult.categories || [];
132
+ if (options.done) {
133
+ todos = todos.filter((t) => t.isCompleted);
134
+ }
135
+ else if (!options.all) {
136
+ todos = todos.filter((t) => !t.isCompleted);
137
+ }
138
+ if (printOutput({ todos, categories }, options.json ?? false)) {
139
+ return;
140
+ }
141
+ const filterLabel = options.done ? "completed " : options.all ? "" : "open ";
142
+ if (todos.length === 0) {
143
+ console.log(`No ${filterLabel}to-dos found for project: ${project}`);
144
+ console.log("");
145
+ console.log("To add a to-do, use:");
146
+ console.log(" propr todo add \"<content>\"");
147
+ return;
148
+ }
149
+ console.log(`To-dos for ${project}:`);
150
+ console.log("");
151
+ displayTodosTable(todos, categories);
152
+ console.log("");
153
+ if (options.all) {
154
+ const openCount = todos.filter((t) => !t.isCompleted).length;
155
+ const doneCount = todos.filter((t) => t.isCompleted).length;
156
+ console.log(`Total: ${todos.length} to-do(s) (${openCount} open, ${doneCount} completed)`);
157
+ }
158
+ else {
159
+ console.log(`Total: ${todos.length} ${filterLabel}to-do(s)`);
160
+ }
161
+ }
162
+ catch (error) {
163
+ if (error instanceof ProjectResolutionError) {
164
+ console.error(`Error: ${error.message}`);
165
+ process.exit(1);
166
+ }
167
+ console.error(`Error listing to-dos: ${error.message}`);
168
+ process.exit(1);
169
+ }
170
+ });
171
+ // todo get
172
+ todo
173
+ .command("get <todo-id>")
174
+ .description("Get detailed information about a specific to-do")
175
+ .option("-j, --json", "Output as JSON for programmatic use")
176
+ .addHelpText("after", `
177
+ Argument:
178
+ todo-id The unique identifier of the to-do
179
+
180
+ Examples:
181
+ $ propr todo get abc123-def456
182
+ $ propr todo get abc123-def456 --json
183
+ `)
184
+ .action(async (todoId, options) => {
185
+ try {
186
+ const result = await getTodo(todoId);
187
+ if (printOutput(result, options.json ?? false)) {
188
+ return;
189
+ }
190
+ displayTodoDetails(result);
191
+ }
192
+ catch (error) {
193
+ const errorMessage = error.message;
194
+ if (errorMessage.includes("404") || errorMessage.includes("not found")) {
195
+ console.error(`Error: To-do not found: ${todoId}`);
196
+ }
197
+ else if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
198
+ console.error("Error: Unauthorized. Please run 'propr login' first.");
199
+ }
200
+ else {
201
+ console.error(`Error fetching to-do: ${errorMessage}`);
202
+ }
203
+ process.exit(1);
204
+ }
205
+ });
206
+ // todo add
207
+ todo
208
+ .command("add <content>")
209
+ .description("Create a new to-do for a repository")
210
+ .option("-p, --project <project>", "Target project (owner/repo)")
211
+ .option("-c, --category <categoryId>", "Category ID to assign the to-do to")
212
+ .option("-j, --json", "Output as JSON for programmatic use")
213
+ .addHelpText("after", `
214
+ Argument:
215
+ content The to-do content/description
216
+
217
+ Examples:
218
+ $ propr todo add "Fix login page styling"
219
+ $ propr todo add "Add unit tests" -p myorg/myrepo
220
+ $ propr todo add "Refactor auth" -c category-uuid --json
221
+ `)
222
+ .action(async (content, options) => {
223
+ try {
224
+ const configManager = await createConfigManager();
225
+ const project = resolveProject(options, configManager);
226
+ const result = await createTodo({
227
+ repository: project,
228
+ content,
229
+ categoryId: options.category || null,
230
+ });
231
+ if (printOutput(result, options.json ?? false)) {
232
+ return;
233
+ }
234
+ console.log("To-do created successfully!");
235
+ console.log(` ID: ${result.todoId}`);
236
+ console.log(` Content: ${result.content}`);
237
+ }
238
+ catch (error) {
239
+ if (error instanceof ProjectResolutionError) {
240
+ console.error(`Error: ${error.message}`);
241
+ process.exit(1);
242
+ }
243
+ console.error(`Error creating to-do: ${error.message}`);
244
+ process.exit(1);
245
+ }
246
+ });
247
+ // todo complete
248
+ todo
249
+ .command("complete <todo-id>")
250
+ .description("Mark a to-do as completed")
251
+ .option("--undo", "Mark a completed to-do as open again")
252
+ .option("-j, --json", "Output as JSON for programmatic use")
253
+ .addHelpText("after", `
254
+ Argument:
255
+ todo-id The unique identifier of the to-do
256
+
257
+ Examples:
258
+ $ propr todo complete abc123 # Mark as done
259
+ $ propr todo complete abc123 --undo # Mark as open again
260
+ `)
261
+ .action(async (todoId, options) => {
262
+ try {
263
+ const isCompleted = !options.undo;
264
+ const result = await updateTodo(todoId, { isCompleted });
265
+ if (printOutput(result, options.json ?? false)) {
266
+ return;
267
+ }
268
+ const action = isCompleted ? "completed" : "reopened";
269
+ console.log(`To-do ${action}: ${result.content}`);
270
+ }
271
+ catch (error) {
272
+ const errorMessage = error.message;
273
+ if (errorMessage.includes("404") || errorMessage.includes("not found")) {
274
+ console.error(`Error: To-do not found: ${todoId}`);
275
+ }
276
+ else if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
277
+ console.error("Error: Unauthorized. Please run 'propr login' first.");
278
+ }
279
+ else {
280
+ console.error(`Error updating to-do: ${errorMessage}`);
281
+ }
282
+ process.exit(1);
283
+ }
284
+ });
285
+ // todo delete
286
+ todo
287
+ .command("delete <todo-id>")
288
+ .description("Delete a to-do permanently")
289
+ .option("-f, --force", "Skip confirmation prompt")
290
+ .addHelpText("after", `
291
+ Argument:
292
+ todo-id The unique identifier of the to-do to delete
293
+
294
+ Examples:
295
+ $ propr todo delete abc123 # With confirmation
296
+ $ propr todo delete abc123 --force # Skip confirmation
297
+ `)
298
+ .action(async (todoId, options) => {
299
+ try {
300
+ // Fetch the todo first to show what will be deleted
301
+ let todoContent = todoId;
302
+ try {
303
+ const existing = await getTodo(todoId);
304
+ todoContent = existing.content;
305
+ console.log(`To-do: ${existing.content}`);
306
+ console.log(`Status: ${existing.isCompleted ? "Completed" : "Open"}`);
307
+ console.log("");
308
+ }
309
+ catch {
310
+ console.log(`To-do ID: ${todoId}`);
311
+ console.log("");
312
+ }
313
+ if (!options.force) {
314
+ const confirmed = await confirm("Are you sure you want to delete this to-do?");
315
+ if (!confirmed) {
316
+ console.log("Deletion cancelled.");
317
+ return;
318
+ }
319
+ }
320
+ await deleteTodo(todoId);
321
+ console.log("To-do deleted successfully.");
322
+ }
323
+ catch (error) {
324
+ const errorMessage = error.message;
325
+ if (errorMessage.includes("404") || errorMessage.includes("not found")) {
326
+ console.error(`Error: To-do not found: ${todoId}`);
327
+ }
328
+ else if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
329
+ console.error("Error: Unauthorized. Please run 'propr login' first.");
330
+ }
331
+ else {
332
+ console.error(`Error deleting to-do: ${errorMessage}`);
333
+ }
334
+ process.exit(1);
335
+ }
336
+ });
337
+ // todo move
338
+ todo
339
+ .command("move <todo-id> <position>")
340
+ .description("Move a to-do to a different position or category")
341
+ .option("-p, --project <project>", "Target project (owner/repo)")
342
+ .option("-c, --category <categoryId>", "Move to a different category (use 'none' for uncategorized)")
343
+ .addHelpText("after", `
344
+ Arguments:
345
+ todo-id The to-do ID to move
346
+ position Target position (1-based)
347
+
348
+ Examples:
349
+ $ propr todo move abc123 1 # Move to top
350
+ $ propr todo move abc123 3 # Move to position 3
351
+ $ propr todo move abc123 1 -c category-uuid # Move to top of another category
352
+ $ propr todo move abc123 1 -c none # Move to uncategorized
353
+ `)
354
+ .action(async (todoId, positionStr, options) => {
355
+ try {
356
+ const position = parseInt(positionStr, 10);
357
+ if (isNaN(position) || position < 1) {
358
+ console.error("Error: Position must be a positive integer (1-based).");
359
+ process.exit(1);
360
+ }
361
+ const configManager = await createConfigManager();
362
+ const project = resolveProject(options, configManager);
363
+ // Fetch current todo to know its category
364
+ const currentTodo = await getTodo(todoId);
365
+ const targetCategoryId = options.category === "none"
366
+ ? null
367
+ : options.category ?? currentTodo.categoryId;
368
+ // Fetch all todos to compute new order
369
+ const todosResult = await listTodos(project);
370
+ const allTodos = todosResult.todos || [];
371
+ // Get todos in the target category, excluding the one being moved
372
+ const categoryTodos = allTodos
373
+ .filter((t) => t.categoryId === targetCategoryId && t.todoId !== todoId)
374
+ .sort((a, b) => a.orderIndex - b.orderIndex);
375
+ // Clamp position
376
+ const clampedPos = Math.min(position, categoryTodos.length + 1);
377
+ // Insert at the target position (1-based -> 0-based index)
378
+ categoryTodos.splice(clampedPos - 1, 0, currentTodo);
379
+ // Build reorder items
380
+ const items = categoryTodos.map((t, i) => ({
381
+ id: t.todoId,
382
+ orderIndex: i,
383
+ categoryId: targetCategoryId,
384
+ }));
385
+ await reorderTodos(project, items);
386
+ const categoryLabel = targetCategoryId ?? "uncategorized";
387
+ console.log(`Moved "${truncate(currentTodo.content, 40)}" to position ${clampedPos} in ${options.category ? categoryLabel : "its category"}.`);
388
+ }
389
+ catch (error) {
390
+ if (error instanceof ProjectResolutionError) {
391
+ console.error(`Error: ${error.message}`);
392
+ process.exit(1);
393
+ }
394
+ const errorMessage = error.message;
395
+ if (errorMessage.includes("404") || errorMessage.includes("not found")) {
396
+ console.error(`Error: To-do not found: ${todoId}`);
397
+ }
398
+ else {
399
+ console.error(`Error moving to-do: ${errorMessage}`);
400
+ }
401
+ process.exit(1);
402
+ }
403
+ });
404
+ // todo category (nested subcommand group)
405
+ const category = new Command("category")
406
+ .description("Manage to-do categories")
407
+ .addHelpText("after", `
408
+ Examples:
409
+ $ propr todo category list # List categories
410
+ $ propr todo category add "Bug fixes" # Create a category
411
+ $ propr todo category rename <id> "New name" # Rename a category
412
+ $ propr todo category delete <id> # Delete a category
413
+ `);
414
+ // todo category list
415
+ category
416
+ .command("list")
417
+ .description("List to-do categories for a repository")
418
+ .option("-p, --project <project>", "Target project (owner/repo)")
419
+ .option("-j, --json", "Output as JSON for programmatic use")
420
+ .addHelpText("after", `
421
+ Examples:
422
+ $ propr todo category list
423
+ $ propr todo category list --json
424
+ `)
425
+ .action(async (options) => {
426
+ try {
427
+ const configManager = await createConfigManager();
428
+ const project = resolveProject(options, configManager);
429
+ const result = await listCategories(project);
430
+ const categories = result.categories || [];
431
+ if (printOutput({ categories }, options.json ?? false)) {
432
+ return;
433
+ }
434
+ if (categories.length === 0) {
435
+ console.log(`No categories found for project: ${project}`);
436
+ console.log("");
437
+ console.log("To create a category, use:");
438
+ console.log(" propr todo category add \"<name>\"");
439
+ return;
440
+ }
441
+ console.log(`Categories for ${project}:`);
442
+ console.log("");
443
+ const idWidth = Math.max("ID".length, ...categories.map((c) => c.categoryId.length));
444
+ const nameWidth = Math.max("Name".length, ...categories.map((c) => c.name.length));
445
+ const header = `${"ID".padEnd(idWidth)} ${"Name".padEnd(nameWidth)}`;
446
+ console.log(header);
447
+ console.log("-".repeat(header.length));
448
+ for (const cat of categories) {
449
+ console.log(`${cat.categoryId.padEnd(idWidth)} ${cat.name.padEnd(nameWidth)}`);
450
+ }
451
+ console.log("");
452
+ console.log(`Total: ${categories.length} category(ies)`);
453
+ }
454
+ catch (error) {
455
+ if (error instanceof ProjectResolutionError) {
456
+ console.error(`Error: ${error.message}`);
457
+ process.exit(1);
458
+ }
459
+ console.error(`Error listing categories: ${error.message}`);
460
+ process.exit(1);
461
+ }
462
+ });
463
+ // todo category add
464
+ category
465
+ .command("add <name>")
466
+ .description("Create a new to-do category")
467
+ .option("-p, --project <project>", "Target project (owner/repo)")
468
+ .option("-j, --json", "Output as JSON for programmatic use")
469
+ .addHelpText("after", `
470
+ Argument:
471
+ name The category name
472
+
473
+ Examples:
474
+ $ propr todo category add "Bug fixes"
475
+ $ propr todo category add "Features" -p myorg/myrepo
476
+ `)
477
+ .action(async (name, options) => {
478
+ try {
479
+ const configManager = await createConfigManager();
480
+ const project = resolveProject(options, configManager);
481
+ const result = await createCategory({ repository: project, name });
482
+ if (printOutput(result, options.json ?? false)) {
483
+ return;
484
+ }
485
+ console.log("Category created successfully!");
486
+ console.log(` ID: ${result.categoryId}`);
487
+ console.log(` Name: ${result.name}`);
488
+ }
489
+ catch (error) {
490
+ if (error instanceof ProjectResolutionError) {
491
+ console.error(`Error: ${error.message}`);
492
+ process.exit(1);
493
+ }
494
+ console.error(`Error creating category: ${error.message}`);
495
+ process.exit(1);
496
+ }
497
+ });
498
+ // todo category rename
499
+ category
500
+ .command("rename <category-id> <name>")
501
+ .description("Rename a to-do category")
502
+ .option("-j, --json", "Output as JSON for programmatic use")
503
+ .addHelpText("after", `
504
+ Arguments:
505
+ category-id The category ID to rename
506
+ name The new name
507
+
508
+ Example:
509
+ $ propr todo category rename abc123 "New name"
510
+ `)
511
+ .action(async (categoryId, name, options) => {
512
+ try {
513
+ const result = await updateCategory(categoryId, { name });
514
+ if (printOutput(result, options.json ?? false)) {
515
+ return;
516
+ }
517
+ console.log(`Category renamed to: ${result.name}`);
518
+ }
519
+ catch (error) {
520
+ const errorMessage = error.message;
521
+ if (errorMessage.includes("404") || errorMessage.includes("not found")) {
522
+ console.error(`Error: Category not found: ${categoryId}`);
523
+ }
524
+ else {
525
+ console.error(`Error renaming category: ${errorMessage}`);
526
+ }
527
+ process.exit(1);
528
+ }
529
+ });
530
+ // todo category delete
531
+ category
532
+ .command("delete <category-id>")
533
+ .description("Delete a to-do category (todos are moved to uncategorized)")
534
+ .option("-f, --force", "Skip confirmation prompt")
535
+ .addHelpText("after", `
536
+ Argument:
537
+ category-id The category ID to delete
538
+
539
+ Note:
540
+ Todos in this category will be moved to uncategorized.
541
+
542
+ Examples:
543
+ $ propr todo category delete abc123
544
+ $ propr todo category delete abc123 --force
545
+ `)
546
+ .action(async (categoryId, options) => {
547
+ try {
548
+ if (!options.force) {
549
+ const confirmed = await confirm("Are you sure you want to delete this category? Todos will be moved to uncategorized.");
550
+ if (!confirmed) {
551
+ console.log("Deletion cancelled.");
552
+ return;
553
+ }
554
+ }
555
+ await deleteCategory(categoryId);
556
+ console.log("Category deleted successfully. Todos moved to uncategorized.");
557
+ }
558
+ catch (error) {
559
+ const errorMessage = error.message;
560
+ if (errorMessage.includes("404") || errorMessage.includes("not found")) {
561
+ console.error(`Error: Category not found: ${categoryId}`);
562
+ }
563
+ else {
564
+ console.error(`Error deleting category: ${errorMessage}`);
565
+ }
566
+ process.exit(1);
567
+ }
568
+ });
569
+ // todo category move
570
+ category
571
+ .command("move <category-id> <position>")
572
+ .description("Move a category to a different position")
573
+ .option("-p, --project <project>", "Target project (owner/repo)")
574
+ .addHelpText("after", `
575
+ Arguments:
576
+ category-id The category ID to move
577
+ position Target position (1-based)
578
+
579
+ Examples:
580
+ $ propr todo category move abc123 1 # Move to top
581
+ $ propr todo category move abc123 3 # Move to position 3
582
+ `)
583
+ .action(async (categoryId, positionStr, options) => {
584
+ try {
585
+ const position = parseInt(positionStr, 10);
586
+ if (isNaN(position) || position < 1) {
587
+ console.error("Error: Position must be a positive integer (1-based).");
588
+ process.exit(1);
589
+ }
590
+ const configManager = await createConfigManager();
591
+ const project = resolveProject(options, configManager);
592
+ const result = await listCategories(project);
593
+ const categories = (result.categories || []).sort((a, b) => a.orderIndex - b.orderIndex);
594
+ const targetIndex = categories.findIndex((c) => c.categoryId === categoryId);
595
+ if (targetIndex === -1) {
596
+ console.error(`Error: Category not found: ${categoryId}`);
597
+ process.exit(1);
598
+ }
599
+ const [moved] = categories.splice(targetIndex, 1);
600
+ const clampedPos = Math.min(position, categories.length + 1);
601
+ categories.splice(clampedPos - 1, 0, moved);
602
+ const items = categories.map((c, i) => ({
603
+ id: c.categoryId,
604
+ orderIndex: i,
605
+ }));
606
+ await reorderCategories(project, items);
607
+ console.log(`Moved category "${moved.name}" to position ${clampedPos}.`);
608
+ }
609
+ catch (error) {
610
+ if (error instanceof ProjectResolutionError) {
611
+ console.error(`Error: ${error.message}`);
612
+ process.exit(1);
613
+ }
614
+ console.error(`Error moving category: ${error.message}`);
615
+ process.exit(1);
616
+ }
617
+ });
618
+ todo.addCommand(category);
619
+ return todo;
620
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Service toggle commands: `propr ui on|off` and `propr docs on|off`.
3
+ *
4
+ * The UI and docs services are plain containers, so toggling them is just
5
+ * starting/stopping the container. The desired state is persisted in the CLI
6
+ * config so `propr start` and restarts honor it.
7
+ */
8
+ import { Command } from "commander";
9
+ import { createConfigManager } from "../config/index.js";
10
+ import { getHostConfig } from "../orchestrator/index.js";
11
+ import { parseOnOffState, ParseStateError } from "../utils/index.js";
12
+ async function toggleService(service, stateArg, root) {
13
+ const enable = parseOnOffState(stateArg);
14
+ const configManager = await createConfigManager();
15
+ const { orch, cfg } = await getHostConfig({ configManager, root });
16
+ if (!orch.dockerAvailable()) {
17
+ console.error("Error: cannot reach the Docker daemon. Run 'propr check'.");
18
+ process.exit(1);
19
+ }
20
+ if (enable) {
21
+ console.log(`Starting ${service}…`);
22
+ orch.ensureNetwork(cfg, (l) => console.log(l));
23
+ orch.startService(cfg, service, { onLog: (l) => console.log(l) });
24
+ const port = service === "ui" ? cfg.uiPort : cfg.docsPort;
25
+ console.log(`${service} is up on http://localhost:${port}`);
26
+ }
27
+ else {
28
+ console.log(`Stopping ${service}…`);
29
+ orch.stopService(cfg, service, { remove: true, onLog: (l) => console.log(l) });
30
+ console.log(`${service} stopped.`);
31
+ }
32
+ // Persist desired state after the action succeeds so it survives restarts.
33
+ if (service === "ui") {
34
+ await configManager.setUiEnabled(enable);
35
+ }
36
+ else {
37
+ await configManager.setDocsEnabled(enable);
38
+ }
39
+ }
40
+ function makeToggleCommand(service, description) {
41
+ return new Command(service)
42
+ .description(description)
43
+ .argument("<state>", "on or off")
44
+ .option("--root <dir>", "Stack root directory")
45
+ .addHelpText("after", `
46
+ Examples:
47
+ $ propr ${service} on
48
+ $ propr ${service} off
49
+ `)
50
+ .action(async (state, options) => {
51
+ try {
52
+ await toggleService(service, state, options.root);
53
+ }
54
+ catch (error) {
55
+ if (error instanceof ParseStateError) {
56
+ console.error(`Error: ${error.message}`);
57
+ process.exit(1);
58
+ }
59
+ console.error(`Error toggling ${service}: ${error.message}`);
60
+ process.exit(1);
61
+ }
62
+ });
63
+ }
64
+ export function createUiCommand() {
65
+ return makeToggleCommand("ui", "Start or stop the web UI service");
66
+ }
67
+ export function createDocsCommand() {
68
+ return makeToggleCommand("docs", "Start or stop the docs service");
69
+ }