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.
- package/README.md +549 -0
- package/dist/api/agentTank.js +27 -0
- package/dist/api/agents.js +201 -0
- package/dist/api/client.js +284 -0
- package/dist/api/errors.js +145 -0
- package/dist/api/implement.js +147 -0
- package/dist/api/index.js +26 -0
- package/dist/api/logs.js +59 -0
- package/dist/api/plans.js +160 -0
- package/dist/api/relay.js +73 -0
- package/dist/api/repos.js +243 -0
- package/dist/api/settings.js +219 -0
- package/dist/api/system.js +53 -0
- package/dist/api/tasks.js +140 -0
- package/dist/api/todos.js +77 -0
- package/dist/api/types.js +6 -0
- package/dist/assets/.env.example +183 -0
- package/dist/assets/env.example.txt +198 -0
- package/dist/commands/agentCommands.js +405 -0
- package/dist/commands/checkCommands.js +384 -0
- package/dist/commands/implementCommands.js +178 -0
- package/dist/commands/index.js +22 -0
- package/dist/commands/initCommands.js +167 -0
- package/dist/commands/initStack.js +193 -0
- package/dist/commands/logCommands.js +170 -0
- package/dist/commands/planCommands.js +552 -0
- package/dist/commands/relayCommands.js +149 -0
- package/dist/commands/repoCommands.js +526 -0
- package/dist/commands/settingCommands.js +237 -0
- package/dist/commands/stackCommands.js +86 -0
- package/dist/commands/startCommand.js +36 -0
- package/dist/commands/systemCommands.js +221 -0
- package/dist/commands/tankCommands.js +55 -0
- package/dist/commands/taskCommands.js +554 -0
- package/dist/commands/todoCommands.js +620 -0
- package/dist/commands/uiDocsCommands.js +69 -0
- package/dist/config/ConfigManager.js +360 -0
- package/dist/config/index.js +8 -0
- package/dist/config/types.js +16 -0
- package/dist/index.js +276 -0
- package/dist/orchestrator/format.js +31 -0
- package/dist/orchestrator/index.js +102 -0
- package/dist/orchestrator/manifest.json +16 -0
- package/dist/orchestrator/orchestrator.mjs +798 -0
- package/dist/orchestrator/types.js +10 -0
- package/dist/tui/StartApp.js +175 -0
- package/dist/tui/app.js +9 -0
- package/dist/tui/render.js +87 -0
- package/dist/utils/envFile.js +65 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/io.js +186 -0
- package/dist/utils/parseState.js +14 -0
- package/dist/utils/resolveProject.js +50 -0
- package/dist/vendor/shared/demoMode.js +6 -0
- package/dist/vendor/shared/events.js +30 -0
- package/dist/vendor/shared/githubAuthMode.js +35 -0
- package/dist/vendor/shared/index.js +15 -0
- package/dist/vendor/shared/labelUtils.js +32 -0
- package/dist/vendor/shared/modelDefinitions.js +146 -0
- package/dist/vendor/shared/reviewPrompt.js +18 -0
- package/dist/vendor/shared/usageTypes.js +13 -0
- package/dist/vendor/shared/userWhitelist.js +30 -0
- package/dist/vendor/shared/validateRelayUrl.js +21 -0
- 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
|
+
}
|