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,10 @@
1
+ /**
2
+ * Type definitions for the shared stack orchestrator
3
+ * (docker/launcher/orchestrator.mjs).
4
+ *
5
+ * The orchestrator core is a dependency-free `.mjs` module shared with the
6
+ * production launcher image. The CLI imports it at runtime via `loadOrchestrator`
7
+ * (see ./index.ts) and types it with the `OrchestratorModule` interface below.
8
+ * Keep this in sync with orchestrator.mjs's exports.
9
+ */
10
+ export {};
@@ -0,0 +1,175 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Live stack dashboard (ink).
4
+ *
5
+ * Polls `getStackStatus` on an interval and renders a service table. The stack
6
+ * containers run detached, so:
7
+ * b / Ctrl-C → leave the stack running, exit the viewer ("background")
8
+ * q → stop + remove the stack, then exit ("stopped")
9
+ * l → toggle a follow-logs pane for the selected service
10
+ * ↑/↓ → select a service
11
+ * u → toggle the UI service
12
+ * r → refresh now
13
+ * ? → toggle help
14
+ */
15
+ import { useEffect, useRef, useState } from "react";
16
+ import { Box, Text, useApp, useInput } from "ink";
17
+ import { stateGlyph } from "../orchestrator/format.js";
18
+ const POLL_INTERVAL_MS = 1500;
19
+ const LOG_LINES = 14;
20
+ function stateColor(s) {
21
+ if (!s.exists)
22
+ return "gray";
23
+ if (s.running)
24
+ return "green";
25
+ return "yellow";
26
+ }
27
+ const glyph = stateGlyph;
28
+ export function StartApp({ orch, cfg, configManager, onResult }) {
29
+ const { exit } = useApp();
30
+ const [status, setStatus] = useState(() => orch.getStackStatus(cfg));
31
+ const [selected, setSelected] = useState(0);
32
+ const [showLogs, setShowLogs] = useState(false);
33
+ const [logLines, setLogLines] = useState([]);
34
+ const [showHelp, setShowHelp] = useState(false);
35
+ const [message, setMessage] = useState("");
36
+ const [busy, setBusy] = useState(false);
37
+ const logProcRef = useRef(null);
38
+ const services = status.services;
39
+ const current = services[selected];
40
+ // Poll stack status.
41
+ useEffect(() => {
42
+ const timer = setInterval(() => {
43
+ try {
44
+ setStatus(orch.getStackStatus(cfg));
45
+ }
46
+ catch {
47
+ /* transient docker error — keep last status */
48
+ }
49
+ }, POLL_INTERVAL_MS);
50
+ return () => clearInterval(timer);
51
+ }, [orch, cfg]);
52
+ // Follow logs for the selected service when the pane is open.
53
+ const currentService = current?.service;
54
+ const currentExists = current?.exists;
55
+ useEffect(() => {
56
+ if (!showLogs)
57
+ return;
58
+ if (!currentService || !currentExists) {
59
+ setLogLines(["(service not running)"]);
60
+ return;
61
+ }
62
+ setLogLines([]);
63
+ const proc = orch.getServiceLogs(cfg, currentService, {
64
+ follow: true,
65
+ tail: LOG_LINES,
66
+ stdio: ["ignore", "pipe", "pipe"],
67
+ });
68
+ logProcRef.current = proc;
69
+ const onData = (buf) => {
70
+ const incoming = buf.toString().split("\n").filter((l) => l.length > 0);
71
+ setLogLines((prev) => [...prev, ...incoming].slice(-LOG_LINES));
72
+ };
73
+ proc.stdout?.on("data", onData);
74
+ proc.stderr?.on("data", onData);
75
+ return () => {
76
+ proc.kill();
77
+ logProcRef.current = null;
78
+ };
79
+ }, [showLogs, currentService, currentExists, orch, cfg]);
80
+ const background = () => {
81
+ logProcRef.current?.kill();
82
+ onResult("background");
83
+ exit();
84
+ };
85
+ const stop = () => {
86
+ logProcRef.current?.kill();
87
+ setMessage("Stopping stack…");
88
+ // Let Ink flush the status message before the synchronous Docker calls block.
89
+ setTimeout(() => {
90
+ try {
91
+ const { failed } = orch.stopStack(cfg, { remove: true, removeNetwork: true });
92
+ if (failed.length > 0) {
93
+ // The app exits right after, so log directly — Ink is unmounting anyway.
94
+ console.error(`warning: ${failed.length} container(s) could not be stopped: ${failed.join(", ")}`);
95
+ }
96
+ }
97
+ catch {
98
+ /* ignore — report outcome regardless */
99
+ }
100
+ onResult("stopped");
101
+ exit();
102
+ }, 50);
103
+ };
104
+ useInput((input, key) => {
105
+ if (key.ctrl && input === "c") {
106
+ background();
107
+ return;
108
+ }
109
+ if (input === "b") {
110
+ background();
111
+ return;
112
+ }
113
+ if (input === "q") {
114
+ stop();
115
+ return;
116
+ }
117
+ if (input === "l") {
118
+ setShowLogs((v) => !v);
119
+ return;
120
+ }
121
+ if (input === "?") {
122
+ setShowHelp((v) => !v);
123
+ return;
124
+ }
125
+ if (input === "r") {
126
+ try {
127
+ setStatus(orch.getStackStatus(cfg));
128
+ }
129
+ catch {
130
+ /* ignore */
131
+ }
132
+ return;
133
+ }
134
+ if (input === "u") {
135
+ if (busy)
136
+ return;
137
+ const ui = services.find((s) => s.service === "ui");
138
+ const shouldStop = Boolean(ui?.running);
139
+ setBusy(true);
140
+ setMessage(shouldStop ? "Stopping UI..." : "Starting UI...");
141
+ // Let Ink flush the status message before the synchronous Docker calls block.
142
+ setTimeout(() => {
143
+ try {
144
+ if (shouldStop) {
145
+ orch.stopService(cfg, "ui", { remove: true });
146
+ configManager?.setUiEnabled(false).catch((e) => setMessage(`UI stopped (config save failed: ${e.message})`));
147
+ setMessage("UI stopped");
148
+ }
149
+ else {
150
+ orch.startService(cfg, "ui", { pull: false });
151
+ configManager?.setUiEnabled(true).catch((e) => setMessage(`UI started (config save failed: ${e.message})`));
152
+ setMessage("UI started");
153
+ }
154
+ setStatus(orch.getStackStatus(cfg));
155
+ }
156
+ catch (e) {
157
+ setMessage(`UI toggle failed: ${e.message}`);
158
+ }
159
+ finally {
160
+ setBusy(false);
161
+ }
162
+ }, 50);
163
+ return;
164
+ }
165
+ if (key.upArrow) {
166
+ setSelected((i) => (i - 1 + services.length) % services.length);
167
+ return;
168
+ }
169
+ if (key.downArrow) {
170
+ setSelected((i) => (i + 1) % services.length);
171
+ }
172
+ });
173
+ const nameWidth = Math.max(...services.map((s) => s.service.length), 8);
174
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "ProPR " }), _jsx(Text, { color: "cyan", children: status.stack }), _jsxs(Text, { dimColor: true, children: [" \u00B7 network ", status.network, " \u00B7 "] }), _jsx(Text, { color: status.running ? "green" : "yellow", children: status.running ? "running" : "stopped" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: services.map((s, i) => (_jsx(Box, { children: _jsxs(Text, { inverse: i === selected, children: [_jsxs(Text, { color: stateColor(s), children: [glyph(s), " "] }), _jsxs(Text, { children: [s.service.padEnd(nameWidth), " "] }), _jsxs(Text, { dimColor: true, children: [(s.exists ? s.state : "absent").padEnd(10), " "] }), _jsx(Text, { children: s.exists ? s.status : "not created" }), s.ports ? _jsxs(Text, { dimColor: true, children: [" ", s.ports] }) : null] }) }, s.service))) }), showLogs && current ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { dimColor: true, children: ["logs: ", current.service, " (l to close)"] }), logLines.length === 0 ? (_jsx(Text, { dimColor: true, children: "(waiting for output\u2026)" })) : (logLines.map((line, i) => (_jsx(Text, { children: line }, i))))] })) : null, showHelp ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "b = background (keep running) q = stop stack l = logs" }), _jsx(Text, { dimColor: true, children: "\u2191/\u2193 = select u = toggle UI r = refresh ? = help" })] })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "b background \u00B7 q stop \u00B7 l logs \u00B7 \u2191/\u2193 select \u00B7 u UI \u00B7 r refresh \u00B7 ? help" }) })), message ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "magenta", children: message }) })) : null] }));
175
+ }
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from "ink";
3
+ import { StartApp } from "./StartApp.js";
4
+ export async function renderDashboard(props) {
5
+ const result = { outcome: "background" };
6
+ const instance = render(_jsx(StartApp, { orch: props.orch, cfg: props.cfg, configManager: props.configManager, onResult: (o) => { result.outcome = o; } }), { exitOnCtrlC: false });
7
+ await instance.waitUntilExit();
8
+ return result.outcome;
9
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `propr start` entry point.
3
+ *
4
+ * Pulls images, starts the stack, then either renders the live ink dashboard
5
+ * (TTY) or prints a one-shot status snapshot and exits (non-TTY / --no-tui).
6
+ * In all cases the containers run detached, so the stack outlives this process.
7
+ */
8
+ import { getHostConfig } from "../orchestrator/index.js";
9
+ import { renderStatusTable } from "../orchestrator/format.js";
10
+ import { createInterface } from "node:readline/promises";
11
+ async function confirmRestart() {
12
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
13
+ return false;
14
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
15
+ try {
16
+ const answer = await rl.question("Stack is already running. Restart all services? [y/N] ");
17
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
18
+ }
19
+ finally {
20
+ rl.close();
21
+ }
22
+ }
23
+ export async function runStart(configManager, options) {
24
+ const { orch, cfg, rootDir } = await getHostConfig({ configManager, root: options.root });
25
+ if (!orch.dockerAvailable()) {
26
+ console.error("Error: cannot reach the Docker daemon. Run 'propr check' for diagnostics.");
27
+ process.exit(1);
28
+ }
29
+ const validation = orch.validateEnv(cfg);
30
+ for (const w of validation.warnings)
31
+ console.warn(`warning: ${w}`);
32
+ if (!validation.ok) {
33
+ console.error("\nCannot start — the stack environment is not ready:");
34
+ for (const e of validation.errors)
35
+ console.error(` ✗ ${e}`);
36
+ console.error("\nRun `propr init stack` and edit .env, then `propr check`.");
37
+ process.exit(1);
38
+ }
39
+ console.log(`Starting ProPR stack (root: ${rootDir})`);
40
+ const running = orch.isStackRunning(cfg);
41
+ if (running) {
42
+ if (!options.restart && !(await confirmRestart())) {
43
+ console.error("\nStack is already running. Use `propr start --restart` to recreate all services.");
44
+ process.exit(1);
45
+ }
46
+ console.log("\nRestarting all services…");
47
+ orch.stopStack(cfg, { remove: true, onLog: (l) => console.log(l) });
48
+ }
49
+ else {
50
+ console.log("\nStarting containers…");
51
+ }
52
+ if (options.pull !== false) {
53
+ const { failedAgentImages } = orch.pullImages(cfg, { onLog: (l) => console.log(l) });
54
+ if (failedAgentImages.length > 0) {
55
+ console.warn(`\nwarning: ${failedAgentImages.length} agent image(s) could not be pulled:`);
56
+ for (const t of failedAgentImages)
57
+ console.warn(` - ${t}`);
58
+ console.warn(" Jobs using those agents will fail until the images are available.\n");
59
+ }
60
+ }
61
+ const ui = configManager.getUiEnabled();
62
+ const docs = cfg.docsEnabled;
63
+ orch.ensureNetwork(cfg, (l) => console.log(l));
64
+ const status = orch.startStack(cfg, { ui, docs, onLog: (l) => console.log(l) });
65
+ const interactive = options.tui !== false && Boolean(process.stdout.isTTY);
66
+ if (!interactive) {
67
+ console.log("");
68
+ console.log(renderStatusTable(status));
69
+ console.log("");
70
+ console.log("Stack running in the background.");
71
+ console.log(" propr status # check status");
72
+ console.log(" propr stop # stop the stack");
73
+ return;
74
+ }
75
+ // Hand off to the live dashboard. Loaded dynamically so the non-TTY path never
76
+ // pulls in ink/react.
77
+ const { renderDashboard } = await import("./app.js");
78
+ const outcome = await renderDashboard({ orch, cfg, configManager });
79
+ if (outcome === "stopped") {
80
+ console.log("Stack stopped.");
81
+ }
82
+ else {
83
+ console.log("\nStack still running in the background.");
84
+ console.log(" propr status # check status");
85
+ console.log(" propr stop # stop the stack");
86
+ }
87
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Minimal .env upsert helper.
3
+ *
4
+ * Sets each KEY to a value in a Docker --env-file-compatible dotenv file: replaces the first
5
+ * uncommented `KEY=` assignment if present, otherwise appends it. Other lines
6
+ * (comments, blank lines, commented examples) are preserved.
7
+ *
8
+ * Docker does not strip quotes in --env-file values, so values are written
9
+ * literally and must fit on one line.
10
+ */
11
+ import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
12
+ function escapeRegExp(value) {
13
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
+ }
15
+ export function upsertEnvVars(envPath, vars) {
16
+ for (const [key, value] of Object.entries(vars)) {
17
+ if (/[\r\n]/.test(value)) {
18
+ throw new Error(`${key} cannot contain newlines; Docker --env-file only supports one KEY=VALUE assignment per line.`);
19
+ }
20
+ if (/^\s|\s$/.test(value)) {
21
+ throw new Error(`${key} cannot contain leading or trailing whitespace in ${envPath}; Docker --env-file does not strip quotes.`);
22
+ }
23
+ if (/\s#/.test(value)) {
24
+ // The orchestrator's env-file reader strips a trailing " #comment" from
25
+ // unquoted values, so such a value would not survive a read-back round trip.
26
+ throw new Error(`${key} cannot contain whitespace followed by '#' in ${envPath}; it would be read back as a truncated value (inline-comment syntax).`);
27
+ }
28
+ }
29
+ const raw = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
30
+ const lines = raw.split(/\r?\n/);
31
+ // Drop trailing blank lines so appends stay tidy; we re-add one newline at the end.
32
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
33
+ lines.pop();
34
+ }
35
+ for (const [key, value] of Object.entries(vars)) {
36
+ const pattern = new RegExp(`^\\s*(export\\s+)?${escapeRegExp(key)}\\s*=`);
37
+ const index = lines.findIndex((line) => pattern.test(line));
38
+ const preserveExport = index >= 0 && /^\s*export\s+/.test(lines[index]);
39
+ const assignment = `${preserveExport ? "export " : ""}${key}=${value}`;
40
+ if (index >= 0) {
41
+ lines[index] = assignment;
42
+ }
43
+ else {
44
+ lines.push(assignment);
45
+ }
46
+ }
47
+ const isNew = !existsSync(envPath);
48
+ let tightenedFrom = null;
49
+ if (!isNew) {
50
+ try {
51
+ const before = statSync(envPath).mode & 0o777;
52
+ if (before !== 0o600) {
53
+ chmodSync(envPath, 0o600);
54
+ tightenedFrom = before;
55
+ }
56
+ }
57
+ catch {
58
+ // Best-effort — may fail on Windows or non-owned files.
59
+ }
60
+ }
61
+ writeFileSync(envPath, `${lines.join("\n")}\n`, { encoding: "utf-8", mode: isNew ? 0o600 : undefined });
62
+ if (tightenedFrom !== null) {
63
+ console.warn(`Note: tightened ${envPath} permissions from ${tightenedFrom.toString(8)} to 600 (secrets file).`);
64
+ }
65
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * CLI Utilities Module
3
+ *
4
+ * Exports utility functions for common CLI operations.
5
+ */
6
+ export { resolveProject, ProjectResolutionError, } from "./resolveProject.js";
7
+ export { formatOutput, printOutput, readJsonInput, validateJsonFields, isPlainObject, JsonInputError, } from "./io.js";
8
+ export { parseOnOffState, ParseStateError } from "./parseState.js";
@@ -0,0 +1,186 @@
1
+ /**
2
+ * I/O Utilities Module
3
+ *
4
+ * Provides utilities for JSON input/output operations to support
5
+ * programmatic integrations in CI/CD pipelines and scripts.
6
+ */
7
+ import { existsSync } from "fs";
8
+ import { readFile } from "fs/promises";
9
+ import { createInterface } from "readline";
10
+ /**
11
+ * Formats data for output based on the specified options.
12
+ * When JSON mode is enabled, outputs raw JSON.stringify result.
13
+ * Otherwise, returns null to indicate human-readable formatting should be used.
14
+ *
15
+ * @param data - The data to format.
16
+ * @param options - Formatting options.
17
+ * @returns The formatted JSON string if json mode is enabled, or null if human-readable formatting should be used.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const result = formatOutput(data, { json: options.json });
22
+ * if (result !== null) {
23
+ * console.log(result);
24
+ * return;
25
+ * }
26
+ * // Continue with human-readable formatting...
27
+ * ```
28
+ */
29
+ export function formatOutput(data, options = {}) {
30
+ const { json = false, indent = 2 } = options;
31
+ if (!json) {
32
+ return null;
33
+ }
34
+ return JSON.stringify(data, null, indent);
35
+ }
36
+ /**
37
+ * Prints data to stdout, using JSON format if specified.
38
+ * This is a convenience wrapper that handles the output directly.
39
+ *
40
+ * @param data - The data to output.
41
+ * @param isJson - Whether to output as JSON.
42
+ * @returns True if JSON was output (caller should return early), false otherwise.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * if (printOutput(data, options.json)) {
47
+ * return;
48
+ * }
49
+ * // Continue with human-readable formatting...
50
+ * ```
51
+ */
52
+ export function printOutput(data, isJson) {
53
+ if (isJson) {
54
+ console.log(JSON.stringify(data, null, 2));
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+ /**
60
+ * Error thrown when JSON input operations fail.
61
+ */
62
+ export class JsonInputError extends Error {
63
+ cause;
64
+ constructor(message, cause) {
65
+ super(message);
66
+ this.cause = cause;
67
+ this.name = "JsonInputError";
68
+ }
69
+ }
70
+ /**
71
+ * Reads and parses JSON from a file or stdin.
72
+ *
73
+ * @param filePath - Path to the JSON file, or "-" to read from stdin.
74
+ * @returns A promise resolving to the parsed JSON data.
75
+ * @throws {JsonInputError} If the file doesn't exist, can't be read, or contains invalid JSON.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * // Read from file
80
+ * const config = await readJsonInput("./config.json");
81
+ *
82
+ * // Read from stdin
83
+ * const data = await readJsonInput("-");
84
+ * ```
85
+ */
86
+ export async function readJsonInput(filePath) {
87
+ try {
88
+ let content;
89
+ if (filePath === "-") {
90
+ // Read from stdin
91
+ content = await readFromStdin();
92
+ }
93
+ else {
94
+ // Read from file
95
+ if (!existsSync(filePath)) {
96
+ throw new JsonInputError(`File not found: ${filePath}`);
97
+ }
98
+ content = await readFile(filePath, "utf-8");
99
+ }
100
+ // Trim whitespace
101
+ content = content.trim();
102
+ if (!content) {
103
+ throw new JsonInputError("Empty input: no JSON data provided");
104
+ }
105
+ try {
106
+ return JSON.parse(content);
107
+ }
108
+ catch (parseError) {
109
+ throw new JsonInputError(`Invalid JSON: ${parseError.message}`, parseError);
110
+ }
111
+ }
112
+ catch (error) {
113
+ if (error instanceof JsonInputError) {
114
+ throw error;
115
+ }
116
+ throw new JsonInputError(`Failed to read input: ${error.message}`, error);
117
+ }
118
+ }
119
+ /**
120
+ * Reads all data from stdin until EOF.
121
+ *
122
+ * @returns A promise resolving to the stdin content as a string.
123
+ */
124
+ async function readFromStdin() {
125
+ return new Promise((resolve, reject) => {
126
+ // Check if stdin is a TTY (interactive terminal)
127
+ if (process.stdin.isTTY) {
128
+ reject(new JsonInputError("No input provided. Use a file path or pipe JSON data to stdin."));
129
+ return;
130
+ }
131
+ const chunks = [];
132
+ const rl = createInterface({
133
+ input: process.stdin,
134
+ terminal: false,
135
+ });
136
+ rl.on("line", (line) => {
137
+ chunks.push(line);
138
+ });
139
+ rl.on("close", () => {
140
+ resolve(chunks.join("\n"));
141
+ });
142
+ rl.on("error", (error) => {
143
+ reject(new JsonInputError(`Failed to read stdin: ${error.message}`, error));
144
+ });
145
+ // Set a timeout for stdin reading (10 seconds)
146
+ const timeout = setTimeout(() => {
147
+ rl.close();
148
+ reject(new JsonInputError("Timeout reading from stdin"));
149
+ }, 10000);
150
+ rl.on("close", () => {
151
+ clearTimeout(timeout);
152
+ });
153
+ });
154
+ }
155
+ /**
156
+ * Validates that an object has the required fields.
157
+ *
158
+ * @param data - The object to validate.
159
+ * @param requiredFields - Array of required field names.
160
+ * @throws {JsonInputError} If any required field is missing.
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * const input = await readJsonInput("./agent.json");
165
+ * validateJsonFields(input, ["alias", "type", "models"]);
166
+ * ```
167
+ */
168
+ export function validateJsonFields(data, requiredFields) {
169
+ if (typeof data !== "object" || data === null) {
170
+ throw new JsonInputError("Input must be a JSON object");
171
+ }
172
+ const obj = data;
173
+ const missingFields = requiredFields.filter((field) => !(field in obj) || obj[field] === undefined);
174
+ if (missingFields.length > 0) {
175
+ throw new JsonInputError(`Missing required fields: ${missingFields.join(", ")}`);
176
+ }
177
+ }
178
+ /**
179
+ * Type guard to check if a value is a plain object.
180
+ *
181
+ * @param value - The value to check.
182
+ * @returns True if the value is a plain object.
183
+ */
184
+ export function isPlainObject(value) {
185
+ return typeof value === "object" && value !== null && !Array.isArray(value);
186
+ }
@@ -0,0 +1,14 @@
1
+ export class ParseStateError extends Error {
2
+ constructor(value) {
3
+ super(`expected on/off (or enable/disable, true/false, yes/no, 1/0), got '${value}'`);
4
+ this.name = "ParseStateError";
5
+ }
6
+ }
7
+ export function parseOnOffState(value) {
8
+ const v = value.trim().toLowerCase();
9
+ if (v === "on" || v === "enable" || v === "true" || v === "1" || v === "yes")
10
+ return true;
11
+ if (v === "off" || v === "disable" || v === "false" || v === "0" || v === "no")
12
+ return false;
13
+ throw new ParseStateError(value);
14
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Project Resolution Utility
3
+ *
4
+ * Provides a utility function to resolve the target project by checking
5
+ * command options first, then falling back to the configured default project.
6
+ */
7
+ /**
8
+ * Error thrown when no project can be resolved.
9
+ */
10
+ export class ProjectResolutionError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "ProjectResolutionError";
14
+ }
15
+ }
16
+ /**
17
+ * Resolves the target project by checking command options first,
18
+ * then falling back to the configured default project.
19
+ *
20
+ * @param options - The command options that may contain a project flag.
21
+ * @param configManager - The ConfigManager instance to retrieve the default project.
22
+ * @returns The resolved project name (owner/repo format).
23
+ * @throws {ProjectResolutionError} If no project is specified and no default is configured.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const configManager = await createConfigManager();
28
+ * const project = resolveProject({ project: "owner/repo" }, configManager);
29
+ * ```
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // Falls back to default project from config
34
+ * const configManager = await createConfigManager();
35
+ * const project = resolveProject({}, configManager);
36
+ * ```
37
+ */
38
+ export function resolveProject(options, configManager) {
39
+ // First, check if a project was provided via the command options
40
+ if (options.project) {
41
+ return options.project;
42
+ }
43
+ // Fall back to the configured default project
44
+ const defaultProject = configManager.getDefaultProject();
45
+ if (defaultProject) {
46
+ return defaultProject;
47
+ }
48
+ // No project could be resolved - throw a helpful error
49
+ throw new ProjectResolutionError("No project specified. Use the -p/--project flag or set a default project with 'propr use <project>'.");
50
+ }
@@ -0,0 +1,6 @@
1
+ export const DEMO_MODE_READ_ONLY_CODE = 'DEMO_MODE_READ_ONLY';
2
+ export function parseTruthyEnvValue(value) {
3
+ const normalized = value?.trim().toLowerCase();
4
+ return normalized === 'true' || normalized === '1';
5
+ }
6
+ //# sourceMappingURL=demoMode.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Event names for real-time updates via WebSocket
3
+ * These events are published to Redis and broadcast to WebSocket clients
4
+ */
5
+ /** Event fired when a task's state changes (e.g., pending -> processing -> completed) */
6
+ export const TASK_UPDATE = 'task:update';
7
+ /** Event fired when draft generation progress changes (relevance, context, llm steps) */
8
+ export const DRAFT_UPDATE = 'draft:update';
9
+ /** Event fired when a plan generation step completes */
10
+ export const PLAN_STEP_UPDATE = 'plan:step:update';
11
+ /** Event fired when indexing progress changes */
12
+ export const INDEXING_UPDATE = 'indexing:update';
13
+ /** Event fired when live task details (Claude log) changes */
14
+ export const TASK_LIVE_UPDATE = 'task:live:update';
15
+ /** Event fired when queue statistics change */
16
+ export const QUEUE_STATS_UPDATE = 'queue:stats:update';
17
+ /** Redis channel names for pub/sub */
18
+ export const REDIS_CHANNELS = {
19
+ /** Channel for all task-related events */
20
+ TASKS: 'propr:events:tasks',
21
+ /** Channel for draft/plan generation events */
22
+ DRAFTS: 'propr:events:drafts',
23
+ /** Channel for indexing events */
24
+ INDEXING: 'propr:events:indexing',
25
+ /** Channel for live task details (Claude log updates) */
26
+ LIVE_DETAILS: 'propr:events:live',
27
+ /** Channel for queue statistics updates */
28
+ QUEUE_STATS: 'propr:events:queue'
29
+ };
30
+ //# sourceMappingURL=events.js.map