propr-cli 0.8.3 → 0.8.4
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 +4 -4
- package/dist/api/relay.js +10 -0
- package/dist/assets/env.example.txt +93 -57
- package/dist/auth/githubLogin.js +66 -0
- package/dist/commands/agentCommands.js +74 -0
- package/dist/commands/agentValidation.js +548 -0
- package/dist/commands/checkCommands.js +981 -76
- package/dist/commands/imageCommands.js +60 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/initStack.js +50 -1
- package/dist/commands/relayCommands.js +45 -12
- package/dist/commands/setup/agents.js +185 -0
- package/dist/commands/setup/engine.js +956 -0
- package/dist/commands/setup/github.js +181 -0
- package/dist/commands/setup/sequential.js +501 -0
- package/dist/commands/setup/state.js +242 -0
- package/dist/commands/setup/types.js +85 -0
- package/dist/commands/setupCommand.js +85 -0
- package/dist/commands/systemCommands.js +49 -2
- package/dist/index.js +13 -45
- package/dist/orchestrator/manifest.json +10 -10
- package/dist/orchestrator/orchestrator.mjs +513 -61
- package/dist/tui/AgentTableApp.js +86 -0
- package/dist/tui/CheckApp.js +202 -0
- package/dist/tui/SetupApp.js +586 -0
- package/dist/tui/SetupApp.test.js +172 -0
- package/dist/tui/app.js +84 -0
- package/dist/tui/render.js +11 -0
- package/dist/utils/envFile.js +45 -0
- package/dist/vendor/shared/githubEventIntakeMode.js +41 -0
- package/dist/vendor/shared/index.js +16 -0
- package/dist/vendor/shared/intakeModePrerequisites.js +76 -0
- package/dist/vendor/shared/modelDefinitions.js +4 -4
- package/dist/vendor/shared/proprServiceUrls.js +27 -0
- package/dist/vendor/shared/statusKeys.js +14 -0
- package/dist/vendor/shared/validateRoutingUrl.js +46 -0
- package/package.json +2 -2
- package/dist/assets/.env.example +0 -183
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup wizard domain helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure, side-effect-light helpers that the `propr setup` driver and both
|
|
5
|
+
* renderers (Ink TUI and readline fallback) build on:
|
|
6
|
+
* - resolving the stack root (reusing the orchestrator's precedence rules),
|
|
7
|
+
* - inspecting whether the stack is already initialized,
|
|
8
|
+
* - reading and *safely* editing .env (non-destructive by default),
|
|
9
|
+
* - constructing and transitioning the {@link SetupState} step model.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here loads the orchestrator's Docker core or renders UI, so the
|
|
12
|
+
* module can be imported and unit-tested without Docker, Ink, or readline.
|
|
13
|
+
* `resolveStackRoot` lives in ../../orchestrator/index.js but only reads config
|
|
14
|
+
* and env — it does not start Docker.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, statSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { resolveGithubAuthMode } from "../../vendor/shared/index.js";
|
|
19
|
+
import { resolveStackRoot } from "../../orchestrator/index.js";
|
|
20
|
+
import { clearEnvKeys as clearEnvFileKeys, upsertEnvVars } from "../../utils/envFile.js";
|
|
21
|
+
import { SETUP_STEP_DEFINITIONS, } from "./types.js";
|
|
22
|
+
/**
|
|
23
|
+
* Sub-directories scaffoldStack creates under the stack root. Exported so the
|
|
24
|
+
* setup driver and tests can create/check the same scaffold shape without
|
|
25
|
+
* duplicating these names.
|
|
26
|
+
*/
|
|
27
|
+
export const STACK_SUBDIRS = ["data", "logs", "repos"];
|
|
28
|
+
/** True only when `path` exists and is a directory. Missing paths read false. */
|
|
29
|
+
function isDirectory(path) {
|
|
30
|
+
try {
|
|
31
|
+
return statSync(path).isDirectory();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** True only when `path` exists and is a regular file. Missing paths read false. */
|
|
38
|
+
function isFile(path) {
|
|
39
|
+
try {
|
|
40
|
+
return statSync(path).isFile();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** True when a value is missing or contains only whitespace. */
|
|
47
|
+
function isBlank(value) {
|
|
48
|
+
return value === undefined || value.trim() === "";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the stack root for setup, reusing the orchestrator's precedence:
|
|
52
|
+
* explicit flag → PROPR_ROOT env → saved config stackRoot → cwd. Does not load
|
|
53
|
+
* Docker.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveSetupRoot(configManager, flagRoot) {
|
|
56
|
+
return resolveStackRoot(configManager, flagRoot);
|
|
57
|
+
}
|
|
58
|
+
/** Absolute path to the .env file for a given stack root. */
|
|
59
|
+
export function envPathFor(rootDir) {
|
|
60
|
+
return join(rootDir, ".env");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Inspect whether the stack at `rootDir` looks initialized. Read-only — never
|
|
64
|
+
* creates anything — so callers can decide whether to skip or re-run
|
|
65
|
+
* scaffolding. A plain file standing in for an expected directory (or vice
|
|
66
|
+
* versa) counts as *not* initialized, matching what the runtime requires.
|
|
67
|
+
*/
|
|
68
|
+
export function inspectStackInit(rootDir) {
|
|
69
|
+
const envExists = isFile(envPathFor(rootDir));
|
|
70
|
+
const dirs = {};
|
|
71
|
+
for (const sub of STACK_SUBDIRS) {
|
|
72
|
+
dirs[sub] = isDirectory(join(rootDir, sub));
|
|
73
|
+
}
|
|
74
|
+
const initialized = envExists && STACK_SUBDIRS.every((sub) => dirs[sub]);
|
|
75
|
+
return { rootDir, envExists, dirs, initialized };
|
|
76
|
+
}
|
|
77
|
+
/** Convenience predicate over {@link inspectStackInit}. */
|
|
78
|
+
export function isStackInitialized(rootDir) {
|
|
79
|
+
return inspectStackInit(rootDir).initialized;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse the .env at `rootDir` into a flat map. Returns `{}` when the file is
|
|
83
|
+
* absent. Mirrors the assignment shape the rest of the stack relies on:
|
|
84
|
+
* `KEY=value`, optionally `export `-prefixed, ignoring blanks and comments.
|
|
85
|
+
* For unquoted values a trailing ` # comment` is stripped, matching the
|
|
86
|
+
* orchestrator's env-file reader (and the round-trip that {@link upsertEnvVars}
|
|
87
|
+
* guards against); surrounding quotes on quoted values are stripped and their
|
|
88
|
+
* contents kept verbatim. This is intentionally a lightweight reader, not a
|
|
89
|
+
* full dotenv implementation — it does not handle escaped quotes or multiline
|
|
90
|
+
* values.
|
|
91
|
+
*/
|
|
92
|
+
export function readEnvVars(rootDir) {
|
|
93
|
+
const envPath = envPathFor(rootDir);
|
|
94
|
+
// Treat anything that is not a regular file (absent, a directory, a broken
|
|
95
|
+
// symlink) as "no vars", matching inspectStackInit's `isFile` guard, so a
|
|
96
|
+
// malformed stack surfaces as not-initialized instead of crashing the read.
|
|
97
|
+
if (!isFile(envPath))
|
|
98
|
+
return {};
|
|
99
|
+
const vars = {};
|
|
100
|
+
for (const line of readFileSync(envPath, "utf-8").split(/\r?\n/)) {
|
|
101
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/);
|
|
102
|
+
if (!match)
|
|
103
|
+
continue;
|
|
104
|
+
const [, key, rawValue] = match;
|
|
105
|
+
const trimmed = rawValue.trim();
|
|
106
|
+
const quoted = trimmed.match(/^(["'])(.*)\1$/);
|
|
107
|
+
// Quoted values keep their contents verbatim; unquoted values drop a
|
|
108
|
+
// trailing inline comment so reads agree with what upsertEnvVars allows.
|
|
109
|
+
vars[key] = quoted ? quoted[2] : trimmed.replace(/\s+#.*$/, "");
|
|
110
|
+
}
|
|
111
|
+
return vars;
|
|
112
|
+
}
|
|
113
|
+
/** True when `key` is present in .env with a non-blank value. */
|
|
114
|
+
export function hasEnvValue(rootDir, key) {
|
|
115
|
+
return !isBlank(readEnvVars(rootDir)[key]);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Safely edit .env for a setup step.
|
|
119
|
+
*
|
|
120
|
+
* Non-destructive by default: a key is only written when it is currently
|
|
121
|
+
* absent/empty, so re-running `propr setup` never clobbers values the user
|
|
122
|
+
* already set. Pass `{ overwrite: true }` for steps where the user explicitly
|
|
123
|
+
* selected a new value and intends to replace whatever is there.
|
|
124
|
+
*
|
|
125
|
+
* Blank selections (empty or whitespace-only) are ignored entirely — a step
|
|
126
|
+
* that has nothing to write must not blank out an existing value. Writes go
|
|
127
|
+
* through
|
|
128
|
+
* {@link upsertEnvVars}, which preserves unrelated lines and tightens the
|
|
129
|
+
* file's permissions.
|
|
130
|
+
*/
|
|
131
|
+
export function applyEnvSelection(rootDir, vars, opts = {}) {
|
|
132
|
+
const existing = readEnvVars(rootDir);
|
|
133
|
+
const toWrite = {};
|
|
134
|
+
const written = [];
|
|
135
|
+
const skipped = [];
|
|
136
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
137
|
+
if (isBlank(value))
|
|
138
|
+
continue; // never blank out an existing value
|
|
139
|
+
const alreadySet = !isBlank(existing[key]);
|
|
140
|
+
if (alreadySet && !opts.overwrite) {
|
|
141
|
+
skipped.push(key);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
toWrite[key] = value;
|
|
145
|
+
written.push(key);
|
|
146
|
+
}
|
|
147
|
+
if (written.length > 0) {
|
|
148
|
+
upsertEnvVars(envPathFor(rootDir), toWrite);
|
|
149
|
+
}
|
|
150
|
+
return { written, skipped };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Remove `keys` from the stack's `.env` entirely.
|
|
154
|
+
*
|
|
155
|
+
* {@link applyEnvSelection} can only set keys (and deliberately ignores blank
|
|
156
|
+
* values so it never clobbers a value the user set), so it cannot *clear* a key:
|
|
157
|
+
* writing `KEY=` would leave an empty assignment that reads back as a set-but-
|
|
158
|
+
* empty value. Setup steps that must genuinely drop a stale key — clearing the
|
|
159
|
+
* user whitelist back to "none", removing a key when switching modes — call this
|
|
160
|
+
* instead. A missing `.env` or absent keys are no-ops.
|
|
161
|
+
*/
|
|
162
|
+
export function clearEnvKeys(rootDir, keys) {
|
|
163
|
+
clearEnvFileKeys(envPathFor(rootDir), keys);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Infer the current GitHub auth mode from the stack's .env, so the github-auth
|
|
167
|
+
* step can show what is already configured (and skip prompting when valid).
|
|
168
|
+
* Reuses the shared resolver the backend uses, so the two can't drift.
|
|
169
|
+
*/
|
|
170
|
+
export function detectGithubAuthMode(rootDir) {
|
|
171
|
+
const env = readEnvVars(rootDir);
|
|
172
|
+
const truthy = /^(1|true|yes|on)$/i;
|
|
173
|
+
return resolveGithubAuthMode({
|
|
174
|
+
demoMode: truthy.test(env.PROPR_DEMO_MODE ?? ""),
|
|
175
|
+
ghAuthMode: env.GH_AUTH_MODE,
|
|
176
|
+
relayUrl: env.PROPR_GH_RELAY_URL,
|
|
177
|
+
relayToken: env.PROPR_GH_RELAY_TOKEN,
|
|
178
|
+
appId: env.GH_APP_ID,
|
|
179
|
+
// The CLI stack records the App key as HOST_GH_PRIVATE_KEY (the orchestrator
|
|
180
|
+
// bind-mounts it and sets the in-container GH_PRIVATE_KEY_PATH to that path),
|
|
181
|
+
// so accept either when inferring app mode — otherwise a stack configured by
|
|
182
|
+
// `propr setup` would resolve as "none" despite being fully set up.
|
|
183
|
+
privateKeyPath: env.GH_PRIVATE_KEY_PATH ?? env.HOST_GH_PRIVATE_KEY,
|
|
184
|
+
installationId: env.GH_INSTALLATION_ID,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** Build the initial, all-`pending` setup state for a resolved stack root. */
|
|
188
|
+
export function createSetupState(rootDir) {
|
|
189
|
+
return {
|
|
190
|
+
rootDir,
|
|
191
|
+
steps: SETUP_STEP_DEFINITIONS.map((def) => ({ ...def, status: "pending" })),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** Look up a step by id. */
|
|
195
|
+
export function getStep(state, id) {
|
|
196
|
+
return state.steps.find((step) => step.id === id);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Return a new state with `id`'s step patched. Immutable so renderers can diff
|
|
200
|
+
* by reference; unknown ids return the state unchanged.
|
|
201
|
+
*/
|
|
202
|
+
export function updateStep(state, id, patch) {
|
|
203
|
+
let changed = false;
|
|
204
|
+
const steps = state.steps.map((step) => {
|
|
205
|
+
if (step.id !== id)
|
|
206
|
+
return step;
|
|
207
|
+
changed = true;
|
|
208
|
+
return { ...step, ...patch };
|
|
209
|
+
});
|
|
210
|
+
return changed ? { ...state, steps } : state;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The next step the wizard should act on: the first one still `pending`. Used
|
|
214
|
+
* by the sequential renderer to drive the flow and by the TUI to highlight the
|
|
215
|
+
* current step.
|
|
216
|
+
*
|
|
217
|
+
* A failed required step blocks everything after it (see the `failed` status in
|
|
218
|
+
* ./types.ts), so once one is encountered there is no next step until it is
|
|
219
|
+
* retried — `undefined` is returned. Failed *optional* steps don't block.
|
|
220
|
+
*/
|
|
221
|
+
export function nextPendingStep(state) {
|
|
222
|
+
// Scan for a blocking failure first so the "a failed required step blocks
|
|
223
|
+
// everything after it" contract holds even if state was patched out of
|
|
224
|
+
// order (e.g. a later step failed before an earlier one finished).
|
|
225
|
+
if (state.steps.some((step) => !step.optional && step.status === "failed")) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
return state.steps.find((step) => step.status === "pending");
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* True once every required step has reached a terminal, non-failed state.
|
|
232
|
+
* Optional steps never block completion; a single failed required step does.
|
|
233
|
+
*/
|
|
234
|
+
export function isSetupComplete(state) {
|
|
235
|
+
return state.steps.every((step) => {
|
|
236
|
+
if (step.status === "failed")
|
|
237
|
+
return false;
|
|
238
|
+
if (step.optional)
|
|
239
|
+
return true;
|
|
240
|
+
return step.status === "done" || step.status === "skipped" || step.status === "warning";
|
|
241
|
+
});
|
|
242
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup wizard domain types.
|
|
3
|
+
*
|
|
4
|
+
* `propr setup` walks a new user through getting a local control-plane stack
|
|
5
|
+
* running end to end. The flow coordinates several existing commands
|
|
6
|
+
* (environment checks, stack scaffolding, image pulls, agent + GitHub
|
|
7
|
+
* configuration, stack startup, whitelist + repo setup, and UI launch).
|
|
8
|
+
*
|
|
9
|
+
* These types are intentionally free of any rendering concern so the same
|
|
10
|
+
* step/status model can drive an Ink TUI and a plain readline fallback. They
|
|
11
|
+
* carry no Docker, Ink, or readline imports — see ./state.ts for the pure
|
|
12
|
+
* helpers that compute and transition this state.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Canonical, ordered step definitions. All start `pending`; renderers and the
|
|
16
|
+
* command driver transition them via the helpers in ./state.ts.
|
|
17
|
+
*/
|
|
18
|
+
export const SETUP_STEP_DEFINITIONS = [
|
|
19
|
+
{
|
|
20
|
+
id: "check",
|
|
21
|
+
title: "Environment checks",
|
|
22
|
+
description: "Verify Docker, images, and agent credentials are ready.",
|
|
23
|
+
optional: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "init-stack",
|
|
27
|
+
title: "Initialize stack",
|
|
28
|
+
description: "Scaffold the stack root (.env, data/, logs/, repos/).",
|
|
29
|
+
optional: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "pull-images",
|
|
33
|
+
title: "Pull images",
|
|
34
|
+
description: "Download the ProPR service and agent container images.",
|
|
35
|
+
optional: false,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "configure-agents",
|
|
39
|
+
title: "Configure agents",
|
|
40
|
+
description: "Record detected host agent-credential directories in .env.",
|
|
41
|
+
optional: false,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "github-auth",
|
|
45
|
+
title: "GitHub authentication",
|
|
46
|
+
description: "Choose how the backend authenticates to GitHub.",
|
|
47
|
+
optional: false,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "intake",
|
|
51
|
+
title: "GitHub intake",
|
|
52
|
+
description: "Choose how the backend ingests GitHub events (routing WebSocket, polling, or direct webhooks).",
|
|
53
|
+
optional: false,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "start-stack",
|
|
57
|
+
title: "Start stack",
|
|
58
|
+
description: "Launch the local control-plane services.",
|
|
59
|
+
optional: false,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "enable-agents",
|
|
63
|
+
title: "Enable agents",
|
|
64
|
+
description: "Enable the selected agents in the backend and authenticate through their images.",
|
|
65
|
+
optional: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "whitelist",
|
|
69
|
+
title: "Whitelist setup",
|
|
70
|
+
description: "Restrict which GitHub users may trigger ProPR.",
|
|
71
|
+
optional: false,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "repo",
|
|
75
|
+
title: "Repository setup",
|
|
76
|
+
description: "Optionally connect a first repository to work on.",
|
|
77
|
+
optional: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "launch-ui",
|
|
81
|
+
title: "Launch UI",
|
|
82
|
+
description: "Open the ProPR web UI.",
|
|
83
|
+
optional: true,
|
|
84
|
+
},
|
|
85
|
+
];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `propr setup` — guided one-time setup for the local ProPR stack.
|
|
3
|
+
*
|
|
4
|
+
* The flow itself lives in the UI-agnostic engine (./setup/engine.ts). This
|
|
5
|
+
* command only chooses how to render it:
|
|
6
|
+
*
|
|
7
|
+
* • The full-screen Ink wizard (../tui/app.tsx) when stdin and stdout are
|
|
8
|
+
* interactive TTYs that support raw mode — the keyboard-driven view.
|
|
9
|
+
* • The sequential readline wizard (./setup/sequential.ts) otherwise: an
|
|
10
|
+
* explicit `--no-tui`, or an interactive terminal that can't enter raw mode
|
|
11
|
+
* (some SSH sessions, minimal/embedded terminals). It prompts line by line.
|
|
12
|
+
*
|
|
13
|
+
* These two are distinct from the no-stdin case. When stdin is not a TTY at all
|
|
14
|
+
* (piped, redirected, CI), nobody can answer a prompt, so the sequential wizard
|
|
15
|
+
* fails fast with actionable guidance rather than hanging — see
|
|
16
|
+
* {@link runSequentialSetup}.
|
|
17
|
+
*/
|
|
18
|
+
import { Command } from "commander";
|
|
19
|
+
import { createConfigManager } from "../config/index.js";
|
|
20
|
+
import { runSequentialSetup, SequentialSetupUnavailableError } from "./setup/sequential.js";
|
|
21
|
+
/**
|
|
22
|
+
* Whether the full-screen Ink wizard can run. It needs interactive input *and*
|
|
23
|
+
* output plus raw-mode support for its keyboard handling; anything short of
|
|
24
|
+
* that drops to the sequential readline wizard. Kept as a pure function of the
|
|
25
|
+
* streams so the decision is unit-testable without a real terminal.
|
|
26
|
+
*/
|
|
27
|
+
export function canRenderInkSetup(stdin = process.stdin, stdout = process.stdout) {
|
|
28
|
+
return Boolean(stdin.isTTY) && Boolean(stdout.isTTY) && typeof stdin.setRawMode === "function";
|
|
29
|
+
}
|
|
30
|
+
export function createSetupCommand() {
|
|
31
|
+
return new Command("setup")
|
|
32
|
+
.description("Guided one-time setup for the local ProPR stack")
|
|
33
|
+
.option("--root <dir>", "Stack root directory (where .env/data/logs/repos live)")
|
|
34
|
+
.option("--no-tui", "Skip the full-screen wizard; prompt line-by-line instead")
|
|
35
|
+
.option("--skip-remote-image-check", "Skip the slow registry round-trip when checking that stack images exist")
|
|
36
|
+
.addHelpText("after", `
|
|
37
|
+
Examples:
|
|
38
|
+
$ propr setup
|
|
39
|
+
$ propr setup --no-tui
|
|
40
|
+
$ propr setup --root ~/propr
|
|
41
|
+
$ propr setup --skip-remote-image-check
|
|
42
|
+
|
|
43
|
+
Setup is safe to re-run at any time: it re-discovers your environment and skips
|
|
44
|
+
steps that are already satisfied, so running it again only fills in what is
|
|
45
|
+
missing — it never undoes existing configuration.
|
|
46
|
+
|
|
47
|
+
The full-screen wizard runs in an interactive terminal. Over SSH, in shells
|
|
48
|
+
without raw-mode support, or with --no-tui, setup falls back to line-by-line
|
|
49
|
+
prompts. When stdin is not a terminal at all (piped, redirected, CI), setup
|
|
50
|
+
cannot prompt and exits with guidance — scaffold non-interactively instead with
|
|
51
|
+
\`propr init stack\`, then edit <root>/.env and run \`propr start\`.
|
|
52
|
+
`)
|
|
53
|
+
.action(async (options) => {
|
|
54
|
+
try {
|
|
55
|
+
const configManager = await createConfigManager();
|
|
56
|
+
const { skipRemoteImageCheck } = options;
|
|
57
|
+
const useInk = options.tui !== false && canRenderInkSetup();
|
|
58
|
+
if (useInk) {
|
|
59
|
+
// Loaded dynamically so the sequential path never pulls in ink/react.
|
|
60
|
+
const { renderSetupWizard } = await import("../tui/app.js");
|
|
61
|
+
const result = await renderSetupWizard({
|
|
62
|
+
configManager,
|
|
63
|
+
root: options.root,
|
|
64
|
+
skipRemoteImageCheck,
|
|
65
|
+
});
|
|
66
|
+
process.exit(result.completed ? 0 : 1);
|
|
67
|
+
}
|
|
68
|
+
const result = await runSequentialSetup({
|
|
69
|
+
configManager,
|
|
70
|
+
root: options.root,
|
|
71
|
+
skipRemoteImageCheck,
|
|
72
|
+
});
|
|
73
|
+
process.exit(result.completed ? 0 : 1);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof SequentialSetupUnavailableError) {
|
|
77
|
+
// Already actionable guidance — print it verbatim, no "Error:" prefix.
|
|
78
|
+
console.error(error.message);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
console.error(`Error during setup: ${error.message}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -40,7 +40,13 @@ function displaySystemStatus(status) {
|
|
|
40
40
|
"Worker",
|
|
41
41
|
"Workers Active",
|
|
42
42
|
"GitHub Auth",
|
|
43
|
+
"GitHub Auth Mode",
|
|
44
|
+
"GitHub Event Intake",
|
|
43
45
|
"Claude Auth",
|
|
46
|
+
"Routing URL",
|
|
47
|
+
"Routing WebSocket",
|
|
48
|
+
"Last Delivery ID",
|
|
49
|
+
"Last ACK",
|
|
44
50
|
"Timestamp",
|
|
45
51
|
];
|
|
46
52
|
const maxLabelWidth = Math.max(...labels.map((l) => l.length));
|
|
@@ -52,16 +58,49 @@ function displaySystemStatus(status) {
|
|
|
52
58
|
console.log(`${"Workers Active".padEnd(maxLabelWidth)} ${status.workerCount}`);
|
|
53
59
|
}
|
|
54
60
|
console.log(`${"GitHub Auth".padEnd(maxLabelWidth)} ${formatStatusIndicator(status.githubAuth)}`);
|
|
61
|
+
if (status.githubAuthMode) {
|
|
62
|
+
console.log(`${"GitHub Auth Mode".padEnd(maxLabelWidth)} ${status.githubAuthMode}`);
|
|
63
|
+
}
|
|
64
|
+
if (status.githubEventIntake) {
|
|
65
|
+
console.log(`${"GitHub Event Intake".padEnd(maxLabelWidth)} ${status.githubEventIntake}`);
|
|
66
|
+
}
|
|
55
67
|
console.log(`${"Claude Auth".padEnd(maxLabelWidth)} ${formatStatusIndicator(status.claudeAuth)}`);
|
|
68
|
+
// Routing WebSocket diagnostics for default (routing_websocket) deployments.
|
|
69
|
+
const routingIntakeActive = status.githubEventIntake === "routing_websocket";
|
|
70
|
+
if (status.routing) {
|
|
71
|
+
const routing = status.routing;
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(`${"Routing URL".padEnd(maxLabelWidth)} ${routing.routingUrl || "(not set)"}`);
|
|
74
|
+
console.log(`${"Routing WebSocket".padEnd(maxLabelWidth)} ${formatStatusIndicator(routing.connected ? "connected" : "disconnected")}`);
|
|
75
|
+
console.log(`${"Last Delivery ID".padEnd(maxLabelWidth)} ${routing.lastDeliveryId ?? "(none yet)"}`);
|
|
76
|
+
console.log(`${"Last ACK".padEnd(maxLabelWidth)} ${routing.lastAckAt ? new Date(routing.lastAckAt).toLocaleString() : "(none yet)"}`);
|
|
77
|
+
}
|
|
78
|
+
else if (routingIntakeActive) {
|
|
79
|
+
// routing_websocket is the active intake mode but the daemon published no
|
|
80
|
+
// routing state — the default event path is not diagnosable (publisher down
|
|
81
|
+
// or daemon not running). Surface it explicitly rather than rendering nothing.
|
|
82
|
+
console.log("");
|
|
83
|
+
console.log(`${"Routing WebSocket".padEnd(maxLabelWidth)} ${formatStatusIndicator("unknown")} (no routing state published)`);
|
|
84
|
+
}
|
|
56
85
|
console.log("");
|
|
57
86
|
console.log(`${"Timestamp".padEnd(maxLabelWidth)} ${new Date(status.timestamp).toLocaleString()}`);
|
|
58
87
|
console.log("");
|
|
59
88
|
console.log("=".repeat(50));
|
|
89
|
+
// Routing health counts against overall health whenever routing_websocket is the
|
|
90
|
+
// active intake path: a published-but-disconnected state is unhealthy, and so is
|
|
91
|
+
// a *missing* state (the daemon publisher is not running), since both mean the
|
|
92
|
+
// default event path is not delivering. When routing is not the active mode, an
|
|
93
|
+
// absent routing record is expected and does not affect health.
|
|
94
|
+
const routingStateMissing = routingIntakeActive && !status.routing;
|
|
95
|
+
const routingHealthy = status.routing
|
|
96
|
+
? status.routing.connected === true
|
|
97
|
+
: !routingIntakeActive;
|
|
60
98
|
const allHealthy = status.api === "healthy" &&
|
|
61
99
|
status.redis === "connected" &&
|
|
62
100
|
status.daemon === "running" &&
|
|
63
101
|
status.worker === "running" &&
|
|
64
|
-
status.githubAuth === "connected"
|
|
102
|
+
status.githubAuth === "connected" &&
|
|
103
|
+
routingHealthy;
|
|
65
104
|
console.log("");
|
|
66
105
|
if (allHealthy) {
|
|
67
106
|
console.log("All systems operational.");
|
|
@@ -78,11 +117,19 @@ function displaySystemStatus(status) {
|
|
|
78
117
|
console.log(" - No workers active. Check worker processes.");
|
|
79
118
|
}
|
|
80
119
|
if (status.githubAuth !== "connected") {
|
|
81
|
-
|
|
120
|
+
// githubAuth is derived from the resolved auth mode, so a relay deployment
|
|
121
|
+
// only lands here when nothing valid is configured — mention both paths.
|
|
122
|
+
console.log(` - GitHub auth not configured (mode: ${status.githubAuthMode ?? "unknown"}). Set GH_APP_ID, GH_PRIVATE_KEY_PATH, and GH_INSTALLATION_ID for app auth, or PROPR_GH_RELAY_URL and PROPR_GH_RELAY_TOKEN for relay auth.`);
|
|
82
123
|
}
|
|
83
124
|
if (status.claudeAuth !== "connected") {
|
|
84
125
|
console.log(" - Claude auth status unknown or no recent activity.");
|
|
85
126
|
}
|
|
127
|
+
if (routingStateMissing) {
|
|
128
|
+
console.log(" - Routing WebSocket state unavailable. routing_websocket is the active intake mode but the daemon published no routing state; ensure the daemon is running and check its logs.");
|
|
129
|
+
}
|
|
130
|
+
else if (!routingHealthy) {
|
|
131
|
+
console.log(" - Routing WebSocket disconnected. The daemon is not connected to the routing relay; check PROPR_ROUTING_URL / PROPR_GH_RELAY_TOKEN and the daemon logs.");
|
|
132
|
+
}
|
|
86
133
|
}
|
|
87
134
|
}
|
|
88
135
|
/**
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readFileSync } from "fs";
|
|
|
5
5
|
import { dirname, join } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { createConfigManager } from "./config/index.js";
|
|
8
|
-
import { createIssueCommand, createPlanCommand, createTaskCommand, createRepoCommand, createAgentCommand, createSettingCommand, createLogCommand, createTodoCommand, createRemoteStatusCommand, createQueueCommand, createInitCommand, createCheckCommand, createStartCommand, createStackStatusCommand, createStopCommand, createUiCommand, createDocsCommand, createTankCommand, createRelayCommand, runChecks, printChecks, STACK_CONFIG_CHECK_NAME, } from "./commands/index.js";
|
|
8
|
+
import { createIssueCommand, createPlanCommand, createTaskCommand, createRepoCommand, createAgentCommand, createSettingCommand, createLogCommand, createTodoCommand, createRemoteStatusCommand, createQueueCommand, createInitCommand, createSetupCommand, createCheckCommand, createImagesCommand, createStartCommand, createStackStatusCommand, createStopCommand, createUiCommand, createDocsCommand, createTankCommand, createRelayCommand, runChecks, printChecks, STACK_CONFIG_CHECK_NAME, } from "./commands/index.js";
|
|
9
9
|
// Re-export configuration module for programmatic use
|
|
10
10
|
export { ConfigManager, createConfigManager, DEFAULT_CONFIG, } from "./config/index.js";
|
|
11
11
|
// Re-export API module for programmatic use
|
|
@@ -31,6 +31,7 @@ drive the backend (plans, issues, tasks, repos, agents).
|
|
|
31
31
|
Quick Start (local stack):
|
|
32
32
|
$ propr Verify the environment (same as 'propr check')
|
|
33
33
|
$ propr init stack Scaffold .env + data/logs/repos, detect agents
|
|
34
|
+
$ propr images pull Pull stack images without starting
|
|
34
35
|
$ propr start Start the stack with a live dashboard
|
|
35
36
|
$ propr status Show local stack status
|
|
36
37
|
$ propr stop Stop the stack
|
|
@@ -57,7 +58,7 @@ Examples:
|
|
|
57
58
|
$ propr remote-status
|
|
58
59
|
|
|
59
60
|
Command Groups:
|
|
60
|
-
Control Plane: check, init [repo|stack], start, status, stop, ui, docs, tank
|
|
61
|
+
Control Plane: check, images, init [repo|stack], start, status, stop, ui, docs, tank
|
|
61
62
|
GitHub Relay: relay [enroll|list|revoke]
|
|
62
63
|
Configuration: remote, use, login, logout
|
|
63
64
|
Plans: plan [create|list|get|delete|abort]
|
|
@@ -149,53 +150,18 @@ Examples:
|
|
|
149
150
|
console.log(`Token saved to: ${configManager.getConfigFilePath()}`);
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
|
-
// Interactive flow via gh CLI
|
|
153
|
-
const {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
console.error("Error: GitHub CLI (gh) is not installed.");
|
|
160
|
-
console.log("");
|
|
161
|
-
console.log("Install it from: https://cli.github.com");
|
|
162
|
-
console.log("");
|
|
163
|
-
console.log("Or provide a token directly:");
|
|
164
|
-
console.log(" $ propr login <token>");
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
// Try to get an existing token
|
|
168
|
-
try {
|
|
169
|
-
const existingToken = execSync("gh auth token", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
170
|
-
if (existingToken) {
|
|
171
|
-
await configManager.setGithubToken(existingToken);
|
|
172
|
-
console.log("Authenticated using existing gh CLI session.");
|
|
173
|
-
console.log(`Token saved to: ${configManager.getConfigFilePath()}`);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
// Not logged in yet — proceed to interactive login
|
|
179
|
-
}
|
|
180
|
-
// Launch interactive gh auth login
|
|
181
|
-
console.log("No existing gh session found. Starting interactive login...");
|
|
182
|
-
console.log("");
|
|
183
|
-
const result = spawnSync("gh", ["auth", "login", "-s", "repo,read:org"], {
|
|
184
|
-
stdio: "inherit",
|
|
153
|
+
// Interactive flow via the gh CLI (shared with `propr setup`).
|
|
154
|
+
const { loginWithGithubCli } = await import("./auth/githubLogin.js");
|
|
155
|
+
const result = await loginWithGithubCli(configManager, {
|
|
156
|
+
interactive: true,
|
|
157
|
+
onLog: (line) => console.log(line),
|
|
185
158
|
});
|
|
186
|
-
if (result.
|
|
187
|
-
console.error(
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
|
-
// Grab the token after successful login
|
|
191
|
-
const ghToken = execSync("gh auth token", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
|
|
192
|
-
if (!ghToken) {
|
|
193
|
-
console.error("Error: Could not retrieve token after login.");
|
|
159
|
+
if (!result.ok) {
|
|
160
|
+
console.error(`Error: ${result.message}`);
|
|
194
161
|
process.exit(1);
|
|
195
162
|
}
|
|
196
|
-
await configManager.setGithubToken(ghToken);
|
|
197
163
|
console.log("");
|
|
198
|
-
console.log(
|
|
164
|
+
console.log(result.message);
|
|
199
165
|
console.log(`Token saved to: ${configManager.getConfigFilePath()}`);
|
|
200
166
|
}
|
|
201
167
|
catch (error) {
|
|
@@ -230,6 +196,7 @@ Example:
|
|
|
230
196
|
});
|
|
231
197
|
// Control-plane commands (local Docker stack)
|
|
232
198
|
program.addCommand(createCheckCommand());
|
|
199
|
+
program.addCommand(createImagesCommand());
|
|
233
200
|
program.addCommand(createStartCommand());
|
|
234
201
|
program.addCommand(createStackStatusCommand());
|
|
235
202
|
program.addCommand(createStopCommand());
|
|
@@ -239,6 +206,7 @@ program.addCommand(createTankCommand());
|
|
|
239
206
|
program.addCommand(createRelayCommand());
|
|
240
207
|
// Setup + backend client command groups
|
|
241
208
|
program.addCommand(createInitCommand());
|
|
209
|
+
program.addCommand(createSetupCommand());
|
|
242
210
|
program.addCommand(createPlanCommand());
|
|
243
211
|
program.addCommand(createIssueCommand());
|
|
244
212
|
program.addCommand(createTaskCommand());
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.8.
|
|
3
|
-
"git_sha": "
|
|
2
|
+
"version": "0.8.4",
|
|
3
|
+
"git_sha": "b31f23cb",
|
|
4
4
|
"registry": "propr",
|
|
5
5
|
"images": {
|
|
6
|
-
"app": "propr/app:0.8.
|
|
7
|
-
"ui": "propr/ui:0.8.
|
|
8
|
-
"docs": "propr/docs:0.8.
|
|
9
|
-
"agent-claude": "propr/agent-claude:0.8.
|
|
10
|
-
"agent-codex": "propr/agent-codex:0.8.
|
|
11
|
-
"agent-antigravity": "propr/agent-antigravity:0.8.
|
|
12
|
-
"agent-opencode": "propr/agent-opencode:0.8.
|
|
13
|
-
"agent-vibe": "propr/agent-vibe:0.8.
|
|
6
|
+
"app": "propr/app:0.8.4",
|
|
7
|
+
"ui": "propr/ui:0.8.4",
|
|
8
|
+
"docs": "propr/docs:0.8.4",
|
|
9
|
+
"agent-claude": "propr/agent-claude:0.8.4",
|
|
10
|
+
"agent-codex": "propr/agent-codex:0.8.4",
|
|
11
|
+
"agent-antigravity": "propr/agent-antigravity:0.8.4",
|
|
12
|
+
"agent-opencode": "propr/agent-opencode:0.8.4",
|
|
13
|
+
"agent-vibe": "propr/agent-vibe:0.8.4",
|
|
14
14
|
"redis": "redis:7-alpine"
|
|
15
15
|
}
|
|
16
16
|
}
|