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,384 @@
1
+ /**
2
+ * Environment Check (doctor)
3
+ *
4
+ * `propr check` verifies the host is ready to run a local ProPR stack: Docker is
5
+ * installed and running, the stack images are available, and agent credentials
6
+ * are present. It is also what bare `propr` runs.
7
+ */
8
+ import { Command } from "commander";
9
+ import { spawnSync } from "node:child_process";
10
+ import { existsSync, accessSync, readFileSync, constants as fsConstants } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { resolveGithubAuthMode, validateRelayUrl } from "../vendor/shared/index.js";
14
+ import { createConfigManager } from "../config/index.js";
15
+ import { getHostConfig } from "../orchestrator/index.js";
16
+ import { printOutput } from "../utils/index.js";
17
+ function agentDescriptors() {
18
+ const home = homedir();
19
+ return [
20
+ { type: "claude", hostDirKey: "hostClaudeDir", envKey: "HOST_CLAUDE_DIR", defaultDir: join(home, ".claude"), imageKey: "agent-claude", bin: "claude" },
21
+ { type: "codex", hostDirKey: "hostCodexDir", envKey: "HOST_CODEX_DIR", defaultDir: join(home, ".codex"), imageKey: "agent-codex", bin: "codex" },
22
+ { type: "antigravity", hostDirKey: "hostAntigravityDir", envKey: "HOST_ANTIGRAVITY_DIR", defaultDir: join(home, ".gemini"), imageKey: "agent-antigravity", bin: "gemini" },
23
+ { type: "opencode", hostDirKey: "hostOpencodeXdgDir", envKey: "HOST_OPENCODE_XDG_DIR", defaultDir: join(home, ".config", "opencode"), imageKey: "agent-opencode", bin: "opencode" },
24
+ { type: "opencode-legacy", hostDirKey: "hostOpencodeLegacyDir", envKey: "HOST_OPENCODE_LEGACY_DIR", defaultDir: join(home, ".opencode"), imageKey: "agent-opencode", bin: "opencode" },
25
+ { type: "opencode-data", hostDirKey: "hostOpencodeDataDir", envKey: "HOST_OPENCODE_DATA_DIR", defaultDir: join(home, ".local", "share", "opencode"), imageKey: "agent-opencode", bin: "opencode" },
26
+ { type: "vibe", hostDirKey: "hostVibeDir", envKey: "HOST_VIBE_DIR", defaultDir: join(home, ".vibe"), imageKey: "agent-vibe", bin: "vibe" },
27
+ ];
28
+ }
29
+ export const STACK_CONFIG_CHECK_NAME = "Stack config (.env)";
30
+ const STATUS_GLYPH = { ok: "✓", warn: "!", fail: "✗" };
31
+ /** Run all checks and return the structured outcome (no printing). */
32
+ export async function runChecks(options = {}) {
33
+ const results = [];
34
+ const configManager = await createConfigManager();
35
+ // 1. Docker installed
36
+ const dockerVersion = spawnSync("docker", ["--version"], { encoding: "utf-8" });
37
+ if (dockerVersion.status === 0) {
38
+ results.push({ name: "Docker installed", status: "ok", detail: dockerVersion.stdout.trim() });
39
+ }
40
+ else {
41
+ results.push({
42
+ name: "Docker installed",
43
+ status: "fail",
44
+ detail: "`docker` command not found",
45
+ fix: "Install Docker: https://docs.docker.com/get-docker/",
46
+ });
47
+ }
48
+ const { orch, cfg, rootDir } = await getHostConfig({ configManager, root: options.root });
49
+ // 2. Docker daemon running
50
+ const daemonUp = orch.dockerAvailable();
51
+ results.push(daemonUp
52
+ ? { name: "Docker daemon", status: "ok", detail: "daemon is reachable" }
53
+ : {
54
+ name: "Docker daemon",
55
+ status: "fail",
56
+ detail: "cannot reach the Docker daemon (`docker info` failed)",
57
+ fix: "Start Docker (e.g. `sudo systemctl start docker`) and ensure your user can access it.",
58
+ });
59
+ // 3. Docker socket (informational — only relevant for the default socket setup)
60
+ const socketPath = "/var/run/docker.sock";
61
+ if (existsSync(socketPath)) {
62
+ let accessible = true;
63
+ try {
64
+ accessSync(socketPath, fsConstants.R_OK | fsConstants.W_OK);
65
+ }
66
+ catch {
67
+ accessible = false;
68
+ }
69
+ results.push(accessible
70
+ ? { name: "Docker socket", status: "ok", detail: socketPath }
71
+ : {
72
+ name: "Docker socket",
73
+ status: "warn",
74
+ detail: `${socketPath} is not read/write for the current user`,
75
+ fix: "Add your user to the `docker` group, or run with sufficient privileges.",
76
+ });
77
+ }
78
+ // 4. Stack root + .env
79
+ const envPath = join(rootDir, ".env");
80
+ if (existsSync(envPath)) {
81
+ results.push({ name: STACK_CONFIG_CHECK_NAME, status: "ok", detail: envPath });
82
+ }
83
+ else {
84
+ results.push({
85
+ name: STACK_CONFIG_CHECK_NAME,
86
+ status: "warn",
87
+ detail: `no .env found at ${rootDir}`,
88
+ fix: "Run `propr init stack` to scaffold .env, data/, logs/ and repos/.",
89
+ });
90
+ }
91
+ // 5. Stack images present locally
92
+ if (daemonUp) {
93
+ for (const [key, tag] of Object.entries(cfg.images)) {
94
+ if (key === "docs" && !cfg.docsEnabled)
95
+ continue;
96
+ const present = imagePresent(orch, tag);
97
+ if (present) {
98
+ results.push({ name: `Image ${key}`, status: "ok", detail: tag });
99
+ }
100
+ else {
101
+ const isAgent = key.startsWith("agent-");
102
+ results.push({
103
+ name: `Image ${key}`,
104
+ status: "warn",
105
+ detail: `${tag} not present locally`,
106
+ fix: isAgent
107
+ ? "Jobs using this agent fail until the image is pulled. `propr start` pulls it, or build with scripts/build-images.sh."
108
+ : "Will be pulled automatically on `propr start`.",
109
+ });
110
+ }
111
+ }
112
+ }
113
+ // 6. Agent credential dirs
114
+ for (const agent of agentDescriptors()) {
115
+ const configured = cfg[agent.hostDirKey];
116
+ const dir = configured || agent.defaultDir;
117
+ if (existsSync(dir)) {
118
+ results.push({ name: `Agent creds: ${agent.type}`, status: "ok", detail: dir });
119
+ }
120
+ else {
121
+ results.push({
122
+ name: `Agent creds: ${agent.type}`,
123
+ status: "warn",
124
+ detail: `${dir} not found — ${agent.type} will not authenticate`,
125
+ fix: `Log in with the ${agent.type} CLI on this host, or set ${agent.envKey} in .env.`,
126
+ });
127
+ }
128
+ }
129
+ // 7. GitHub credentials (the backend hard-exits without a valid auth mode)
130
+ const fileEnv = existsSync(envPath) ? orch.readEnvFile(envPath) : {};
131
+ for (const r of checkGithubAuth(fileEnv, cfg))
132
+ results.push(r);
133
+ // 8. User whitelist — warn when no whitelist is configured for non-demo stacks
134
+ const whitelistRaw = process.env.GITHUB_USER_WHITELIST ?? fileEnv.GITHUB_USER_WHITELIST;
135
+ const whitelistEntries = (whitelistRaw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
136
+ const authMode = (process.env.GH_AUTH_MODE ?? fileEnv.GH_AUTH_MODE ?? "").trim().toLowerCase();
137
+ const isDemo = isTruthy(process.env.PROPR_DEMO_MODE ?? fileEnv.PROPR_DEMO_MODE) || authMode === "demo";
138
+ if (whitelistEntries.length === 0 && !isDemo) {
139
+ results.push({
140
+ name: "User whitelist",
141
+ status: "warn",
142
+ detail: "GITHUB_USER_WHITELIST is not set — any GitHub user who can authenticate to this instance may trigger processing and use the API (within the App's repository access)",
143
+ fix: "Set GITHUB_USER_WHITELIST to a comma-separated list of allowed GitHub usernames in .env.",
144
+ });
145
+ }
146
+ else if (whitelistEntries.length > 0) {
147
+ results.push({ name: "User whitelist", status: "ok", detail: `${whitelistEntries.length} user(s) allowed` });
148
+ }
149
+ // 9. Config validation from the orchestrator (bind paths, vibe dirs, etc.)
150
+ const validation = orch.validateEnv(cfg);
151
+ for (const warn of validation.warnings) {
152
+ results.push({ name: "Config warning", status: "warn", detail: warn });
153
+ }
154
+ for (const err of validation.errors) {
155
+ // env file / data dir absence is already surfaced by steps 4–6 above; skip duplicates.
156
+ if (/env file path is not set/i.test(err))
157
+ continue;
158
+ if (/data directory.*is not set/i.test(err))
159
+ continue;
160
+ results.push({ name: "Config error", status: "fail", detail: err });
161
+ }
162
+ // 10. Deep verify (opt-in): image/CLI smoke test per selected agent
163
+ if (options.verify && daemonUp) {
164
+ const selected = options.agents && options.agents.length
165
+ ? agentDescriptors().filter((a) => options.agents.includes(a.type))
166
+ : agentDescriptors();
167
+ for (const agent of selected) {
168
+ const tag = cfg.images[agent.imageKey];
169
+ if (!tag || !imagePresent(orch, tag)) {
170
+ results.push({
171
+ name: `Verify: ${agent.type}`,
172
+ status: "warn",
173
+ detail: `image ${tag ?? agent.imageKey} not present — skipped`,
174
+ });
175
+ continue;
176
+ }
177
+ const run = spawnSync("docker", ["run", "--rm", "--network=none", "--memory=512m", tag, agent.bin, "--version"], { encoding: "utf-8", timeout: 60000 });
178
+ if (run.status === 0) {
179
+ results.push({ name: `Verify: ${agent.type}`, status: "ok", detail: `image runs (${(run.stdout || "").trim().split("\n")[0]})` });
180
+ }
181
+ else {
182
+ results.push({
183
+ name: `Verify: ${agent.type}`,
184
+ status: "warn",
185
+ detail: `image/CLI smoke test failed: ${(run.stderr || run.stdout || "").trim().split("\n")[0]}`,
186
+ });
187
+ }
188
+ }
189
+ }
190
+ const anyFail = results.some((r) => r.status === "fail");
191
+ return { results, cfg, rootDir, anyFail };
192
+ }
193
+ function imagePresent(orch, tag) {
194
+ const res = orch.docker(["images", "-q", tag], { capture: true });
195
+ return res.stdout.trim().length > 0;
196
+ }
197
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
198
+ function isTruthy(value) {
199
+ return value !== undefined && TRUTHY.has(value.trim().toLowerCase());
200
+ }
201
+ // Matches the unedited .env.example placeholders (your_app_id, path/to/..., etc.).
202
+ // Every alternative is anchored so a real value that merely contains a
203
+ // placeholder-looking substring is not misflagged.
204
+ function isPlaceholder(value) {
205
+ if (!value || value.trim() === "")
206
+ return true;
207
+ return /^your_|^\.?\/path\/to|^changeme$|^x{4,}$|^example\.com$/i.test(value.trim());
208
+ }
209
+ // Forward-compatible relay env names (see the token-relay plan). Presence of the
210
+ // relay URL selects relay mode.
211
+ const RELAY_URL_KEY = "PROPR_GH_RELAY_URL";
212
+ const RELAY_TOKEN_KEY = "PROPR_GH_RELAY_TOKEN";
213
+ /**
214
+ * Verify the GitHub credentials the backend needs to boot. The daemon/worker/api
215
+ * import @propr/core's githubAuth, which hard-exits unless one of these is true:
216
+ * demo mode, a token relay, or a configured GitHub App + readable key.
217
+ *
218
+ * The mode itself comes from @propr/shared's resolveGithubAuthMode — the same
219
+ * function the backend uses — so this check cannot drift from boot behavior.
220
+ */
221
+ function checkGithubAuth(env, cfg) {
222
+ const val = (k) => process.env[k] ?? env[k];
223
+ const out = [];
224
+ const relayUrl = val(RELAY_URL_KEY);
225
+ const relayToken = val(RELAY_TOKEN_KEY);
226
+ const { mode, warnings } = resolveGithubAuthMode({
227
+ demoMode: isTruthy(val("PROPR_DEMO_MODE")),
228
+ ghAuthMode: val("GH_AUTH_MODE"),
229
+ relayUrl,
230
+ relayToken,
231
+ appId: val("GH_APP_ID"),
232
+ privateKeyPath: val("GH_PRIVATE_KEY_PATH"),
233
+ installationId: val("GH_INSTALLATION_ID"),
234
+ });
235
+ for (const warning of warnings) {
236
+ out.push({ name: "GitHub auth", status: "warn", detail: warning });
237
+ }
238
+ if (mode === "demo") {
239
+ out.push({ name: "GitHub auth", status: "ok", detail: "demo mode — GitHub access disabled" });
240
+ return out;
241
+ }
242
+ if (mode === "none") {
243
+ out.push({
244
+ name: "GitHub auth mode",
245
+ status: "fail",
246
+ detail: "no GitHub auth configured — the backend will exit at startup",
247
+ fix: "Set GH_APP_ID + GH_INSTALLATION_ID + a private key (own App), or PROPR_GH_RELAY_URL + PROPR_GH_RELAY_TOKEN (token relay), or PROPR_DEMO_MODE=true.",
248
+ });
249
+ return out;
250
+ }
251
+ if (mode === "relay") {
252
+ const urlError = relayUrl ? validateRelayUrl(relayUrl) : `${RELAY_URL_KEY} must be set for relay mode`;
253
+ out.push(urlError
254
+ ? { name: "GitHub auth mode", status: "fail", detail: urlError, fix: "Use an https:// relay URL (http only for localhost)." }
255
+ : { name: "GitHub auth mode", status: "ok", detail: `token relay (${relayUrl})` });
256
+ if (!relayToken) {
257
+ out.push({
258
+ name: "Relay credential",
259
+ status: "fail",
260
+ detail: `${RELAY_TOKEN_KEY} is not set`,
261
+ fix: `Set ${RELAY_TOKEN_KEY} to the relay credential issued for your installation.`,
262
+ });
263
+ }
264
+ else {
265
+ out.push({ name: "Relay credential", status: "ok", detail: `${RELAY_TOKEN_KEY} is set` });
266
+ }
267
+ return out;
268
+ }
269
+ // App mode (default).
270
+ out.push({ name: "GitHub auth mode", status: "ok", detail: "GitHub App (own/shared app)" });
271
+ const appId = val("GH_APP_ID");
272
+ const installationId = val("GH_INSTALLATION_ID");
273
+ out.push(isPlaceholder(appId)
274
+ ? { name: "GH_APP_ID", status: "fail", detail: "missing or placeholder", fix: "Set GH_APP_ID from your GitHub App settings." }
275
+ : { name: "GH_APP_ID", status: "ok", detail: appId });
276
+ out.push(isPlaceholder(installationId)
277
+ ? { name: "GH_INSTALLATION_ID", status: "fail", detail: "missing or placeholder", fix: "Set GH_INSTALLATION_ID for the App's installation on your account/org." }
278
+ : { name: "GH_INSTALLATION_ID", status: "ok", detail: installationId });
279
+ // Private key reachability. Prefer the explicit host mount (HOST_GH_PRIVATE_KEY).
280
+ const hostKey = cfg.hostGhPrivateKey;
281
+ const keyPath = val("GH_PRIVATE_KEY_PATH");
282
+ if (hostKey) {
283
+ if (!existsSync(hostKey)) {
284
+ out.push({ name: "GitHub App key", status: "fail", detail: `HOST_GH_PRIVATE_KEY (${hostKey}) does not exist` });
285
+ }
286
+ else {
287
+ let readable = true;
288
+ try {
289
+ accessSync(hostKey, fsConstants.R_OK);
290
+ }
291
+ catch {
292
+ readable = false;
293
+ }
294
+ const looksLikePem = readable && /-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(safeRead(hostKey));
295
+ out.push(readable && looksLikePem
296
+ ? { name: "GitHub App key", status: "ok", detail: `${hostKey} (mounted read-only)` }
297
+ : {
298
+ name: "GitHub App key",
299
+ status: "fail",
300
+ detail: readable ? `${hostKey} does not look like a PEM private key` : `${hostKey} is not readable`,
301
+ });
302
+ }
303
+ }
304
+ else if (isPlaceholder(keyPath)) {
305
+ out.push({
306
+ name: "GitHub App key",
307
+ status: "fail",
308
+ detail: "no private key configured",
309
+ fix: "Set HOST_GH_PRIVATE_KEY to your .pem host path (recommended), or stage the key under data/ and set GH_PRIVATE_KEY_PATH.",
310
+ });
311
+ }
312
+ else {
313
+ out.push({
314
+ name: "GitHub App key",
315
+ status: "warn",
316
+ detail: `GH_PRIVATE_KEY_PATH=${keyPath} — reachability inside the container not verified`,
317
+ fix: "Prefer HOST_GH_PRIVATE_KEY (bind-mounts the key), or ensure this path resolves inside the container (e.g. under data/).",
318
+ });
319
+ }
320
+ return out;
321
+ }
322
+ function safeRead(path) {
323
+ try {
324
+ return readFileSync(path, "utf-8").slice(0, 200);
325
+ }
326
+ catch {
327
+ return "";
328
+ }
329
+ }
330
+ /** Print a human-readable check table. */
331
+ export function printChecks(outcome) {
332
+ console.log("");
333
+ console.log(`ProPR environment check (stack root: ${outcome.rootDir})`);
334
+ console.log("─".repeat(60));
335
+ for (const r of outcome.results) {
336
+ const glyph = STATUS_GLYPH[r.status];
337
+ console.log(`${glyph} ${r.name.padEnd(24)} ${r.detail}`);
338
+ if (r.fix && r.status !== "ok") {
339
+ console.log(` ↳ ${r.fix}`);
340
+ }
341
+ }
342
+ console.log("─".repeat(60));
343
+ const counts = { ok: 0, warn: 0, fail: 0 };
344
+ for (const r of outcome.results)
345
+ counts[r.status]++;
346
+ console.log(`${counts.ok} ok, ${counts.warn} warning(s), ${counts.fail} failure(s)`);
347
+ }
348
+ /** Creates the `check` command. */
349
+ export function createCheckCommand() {
350
+ return new Command("check")
351
+ .description("Verify the host is ready to run a local ProPR stack (Docker, images, agents)")
352
+ .option("--root <dir>", "Stack root directory (where .env/data/logs/repos live)")
353
+ .option("--verify", "Also run an image/CLI smoke test for each agent (slower)")
354
+ .option("--agents <list>", "Comma-separated agent types to --verify (default: all)")
355
+ .option("--json", "Output raw JSON")
356
+ .addHelpText("after", `
357
+ Examples:
358
+ $ propr check
359
+ $ propr check --verify
360
+ $ propr check --verify --agents claude,codex
361
+ $ propr check --json
362
+ `)
363
+ .action(async (options) => {
364
+ try {
365
+ const outcome = await runChecks({
366
+ root: options.root,
367
+ verify: options.verify,
368
+ agents: options.agents ? options.agents.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
369
+ });
370
+ if (options.json) {
371
+ printOutput({ rootDir: outcome.rootDir, results: outcome.results }, true);
372
+ }
373
+ else {
374
+ printChecks(outcome);
375
+ }
376
+ if (outcome.anyFail)
377
+ process.exit(1);
378
+ }
379
+ catch (error) {
380
+ console.error(`Error running checks: ${error.message}`);
381
+ process.exit(1);
382
+ }
383
+ });
384
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Implementation Commands
3
+ *
4
+ * CLI commands for implementing issues using the ProPR backend.
5
+ * Provides the `issue` command group with the `implement` subcommand.
6
+ */
7
+ import { Command } from "commander";
8
+ import { ProjectResolutionError } from "../utils/index.js";
9
+ import { implementIssue, getTaskStatus, } from "../api/index.js";
10
+ /**
11
+ * Terminal states that indicate a task has finished processing.
12
+ */
13
+ const TERMINAL_STATES = ["completed", "failed", "cancelled"];
14
+ /**
15
+ * Poll interval in milliseconds.
16
+ */
17
+ const POLL_INTERVAL_MS = 3000;
18
+ /**
19
+ * Maximum wait time in milliseconds (10 minutes).
20
+ */
21
+ const MAX_WAIT_MS = 600000;
22
+ /**
23
+ * Parses an issue identifier in the format "draft-id/issue-number" or "draft-id:issue-number".
24
+ */
25
+ function parseIssueId(issueId) {
26
+ const separatorMatch = issueId.match(/^(.+)[\/:](\d+)$/);
27
+ if (separatorMatch) {
28
+ const draftId = separatorMatch[1];
29
+ const issueNumber = parseInt(separatorMatch[2], 10);
30
+ if (!isNaN(issueNumber) && issueNumber > 0) {
31
+ return { draftId, issueNumber };
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ /**
37
+ * Formats the current task state for display.
38
+ */
39
+ function formatState(state) {
40
+ const stateMap = {
41
+ pending: "Pending",
42
+ queued: "Queued",
43
+ processing: "Processing",
44
+ claude_execution: "Executing",
45
+ post_processing: "Post-processing",
46
+ completed: "Completed",
47
+ failed: "Failed",
48
+ cancelled: "Cancelled",
49
+ };
50
+ return stateMap[state] || state;
51
+ }
52
+ /**
53
+ * Polls the task status until it reaches a terminal state.
54
+ */
55
+ async function pollTaskStatus(taskId) {
56
+ const startTime = Date.now();
57
+ let lastState = "";
58
+ while (Date.now() - startTime < MAX_WAIT_MS) {
59
+ const status = await getTaskStatus(taskId);
60
+ if (status.currentState !== lastState) {
61
+ console.log(`Status: ${formatState(status.currentState)}`);
62
+ lastState = status.currentState;
63
+ }
64
+ if (TERMINAL_STATES.includes(status.currentState)) {
65
+ return status;
66
+ }
67
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
68
+ }
69
+ const finalStatus = await getTaskStatus(taskId);
70
+ return finalStatus;
71
+ }
72
+ /**
73
+ * Creates the `issue` command group.
74
+ */
75
+ export function createIssueCommand() {
76
+ const issue = new Command("issue")
77
+ .description("Manage GitHub issue implementation")
78
+ .addHelpText("after", `
79
+ Examples:
80
+ $ propr issue implement abc123/1
81
+ $ propr issue implement abc123:42 --wait
82
+ $ propr issue implement abc123/1 -a claude --wait --auto-merge
83
+ `);
84
+ issue
85
+ .command("implement <issue-id>")
86
+ .description("Implement a GitHub issue from a plan using AI agents")
87
+ .option("-p, --project <project>", "Target project (owner/repo)")
88
+ .option("-w, --wait", "Wait for the implementation to complete")
89
+ .option("-a, --agent <agent>", "Agent alias to use for implementation")
90
+ .option("-m, --model <model>", "Model name to use for implementation")
91
+ .option("--epic", "Create an Epic PR to collect all related PRs")
92
+ .option("--auto-merge", "Enable auto-merge for the created PR")
93
+ .addHelpText("after", `
94
+ Argument:
95
+ issue-id Format: <draft-id>/<issue-number> or <draft-id>:<issue-number>
96
+
97
+ Examples:
98
+ $ propr issue implement abc123/1
99
+ $ propr issue implement abc123:42 --wait
100
+ $ propr issue implement abc123/1 -a claude -m claude-sonnet-4-20250514 --wait
101
+ $ propr issue implement abc123/1 --epic --auto-merge
102
+ `)
103
+ .action(async (issueId, options) => {
104
+ try {
105
+ const parsed = parseIssueId(issueId);
106
+ if (!parsed) {
107
+ console.error("Error: Invalid issue ID format. Expected: <draft-id>/<issue-number> or <draft-id>:<issue-number>");
108
+ console.error("");
109
+ console.error("Examples:");
110
+ console.error(" propr issue implement abc123/1");
111
+ console.error(" propr issue implement draft-uuid-here:42");
112
+ process.exit(1);
113
+ }
114
+ const { draftId, issueNumber } = parsed;
115
+ console.log(`Implementing issue #${issueNumber} from draft ${draftId}...`);
116
+ const result = await implementIssue(draftId, issueNumber, {
117
+ agent_alias: options.agent,
118
+ model_name: options.model,
119
+ useEpic: options.epic,
120
+ autoMerge: options.autoMerge,
121
+ });
122
+ if (!result.success) {
123
+ console.error(`Error: ${result.message}`);
124
+ process.exit(1);
125
+ }
126
+ console.log(result.message);
127
+ if (result.taskId) {
128
+ console.log(`Task ID: ${result.taskId}`);
129
+ }
130
+ if (!options.wait) {
131
+ if (result.taskId) {
132
+ console.log("");
133
+ console.log("Use 'propr issue implement <issue-id> --wait' to wait for completion.");
134
+ }
135
+ return;
136
+ }
137
+ if (!result.taskId) {
138
+ console.log("");
139
+ console.log("Note: No task ID returned. The implementation may be triggered asynchronously.");
140
+ return;
141
+ }
142
+ console.log("");
143
+ console.log("Waiting for implementation to complete...");
144
+ const finalStatus = await pollTaskStatus(result.taskId);
145
+ console.log("");
146
+ if (finalStatus.isCompleted) {
147
+ console.log("Implementation completed successfully!");
148
+ if (finalStatus.prNumber) {
149
+ console.log(`Pull Request: #${finalStatus.prNumber}`);
150
+ }
151
+ if (finalStatus.prUrl) {
152
+ console.log(`PR URL: ${finalStatus.prUrl}`);
153
+ }
154
+ }
155
+ else if (finalStatus.isFailed) {
156
+ console.error("Implementation failed.");
157
+ if (finalStatus.failureReason) {
158
+ console.error(`Reason: ${finalStatus.failureReason}`);
159
+ }
160
+ process.exit(1);
161
+ }
162
+ else {
163
+ console.log(`Implementation is still ${formatState(finalStatus.currentState)}.`);
164
+ console.log(`Task ID: ${result.taskId}`);
165
+ console.log("You can check the status later using the ProPR dashboard.");
166
+ }
167
+ }
168
+ catch (error) {
169
+ if (error instanceof ProjectResolutionError) {
170
+ console.error(`Error: ${error.message}`);
171
+ process.exit(1);
172
+ }
173
+ console.error(`Error implementing issue: ${error.message}`);
174
+ process.exit(1);
175
+ }
176
+ });
177
+ return issue;
178
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * CLI Commands Module
3
+ *
4
+ * Exports command creation functions for the ProPR CLI.
5
+ */
6
+ export { createIssueCommand } from "./implementCommands.js";
7
+ export { createPlanCommand } from "./planCommands.js";
8
+ export { createTaskCommand } from "./taskCommands.js";
9
+ export { createRepoCommand } from "./repoCommands.js";
10
+ export { createAgentCommand } from "./agentCommands.js";
11
+ export { createSettingCommand } from "./settingCommands.js";
12
+ export { createLogCommand } from "./logCommands.js";
13
+ export { createTodoCommand } from "./todoCommands.js";
14
+ export { createRemoteStatusCommand, createQueueCommand } from "./systemCommands.js";
15
+ export { createInitCommand } from "./initCommands.js";
16
+ // Control-plane commands (local Docker stack)
17
+ export { createCheckCommand, runChecks, printChecks, STACK_CONFIG_CHECK_NAME } from "./checkCommands.js";
18
+ export { createStartCommand } from "./startCommand.js";
19
+ export { createStackStatusCommand, createStopCommand } from "./stackCommands.js";
20
+ export { createUiCommand, createDocsCommand } from "./uiDocsCommands.js";
21
+ export { createTankCommand } from "./tankCommands.js";
22
+ export { createRelayCommand } from "./relayCommands.js";