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,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";
|