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,360 @@
1
+ /**
2
+ * CLI Configuration Manager
3
+ *
4
+ * Manages persistent configuration for the CLI, storing settings in a JSON file
5
+ * located in the user's home directory (~/.propr/config.json).
6
+ *
7
+ * Features:
8
+ * - Reads and writes configuration to a JSON file
9
+ * - Handles missing configuration files gracefully
10
+ * - Handles corrupted configuration files with fallback behavior
11
+ * - Provides type-safe getter and setter methods
12
+ */
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import os from "node:os";
16
+ import { DEFAULT_CONFIG } from "./types.js";
17
+ /**
18
+ * Default configuration directory name.
19
+ */
20
+ const CONFIG_DIR_NAME = ".propr";
21
+ /**
22
+ * Default configuration file name.
23
+ */
24
+ const CONFIG_FILE_NAME = "config.json";
25
+ /**
26
+ * ConfigManager handles persistent CLI configuration.
27
+ */
28
+ export class ConfigManager {
29
+ configDir;
30
+ configFilePath;
31
+ config;
32
+ initialized = false;
33
+ /**
34
+ * Creates a new ConfigManager instance.
35
+ *
36
+ * @param customConfigDir - Optional custom configuration directory path.
37
+ * Defaults to ~/.propr
38
+ */
39
+ constructor(customConfigDir) {
40
+ this.configDir = customConfigDir ?? path.join(os.homedir(), CONFIG_DIR_NAME);
41
+ this.configFilePath = path.join(this.configDir, CONFIG_FILE_NAME);
42
+ this.config = { ...DEFAULT_CONFIG };
43
+ }
44
+ /**
45
+ * Initializes the ConfigManager by loading the configuration file.
46
+ * This method is idempotent and safe to call multiple times.
47
+ *
48
+ * @returns A promise that resolves when initialization is complete.
49
+ */
50
+ async init() {
51
+ if (this.initialized) {
52
+ return;
53
+ }
54
+ await this.load();
55
+ this.initialized = true;
56
+ }
57
+ /**
58
+ * Ensures the configuration directory exists.
59
+ *
60
+ * @returns A promise that resolves when the directory exists.
61
+ */
62
+ async ensureConfigDir() {
63
+ try {
64
+ await fs.promises.mkdir(this.configDir, { recursive: true });
65
+ }
66
+ catch (error) {
67
+ // Directory already exists or other error
68
+ if (error.code !== "EEXIST") {
69
+ throw error;
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Loads the configuration from the file.
75
+ * If the file doesn't exist, uses default values.
76
+ * If the file is corrupted, resets to defaults and warns the user.
77
+ *
78
+ * @returns A promise that resolves when the configuration is loaded.
79
+ */
80
+ async load() {
81
+ try {
82
+ const data = await fs.promises.readFile(this.configFilePath, "utf-8");
83
+ const parsed = JSON.parse(data);
84
+ // Validate that parsed data is an object
85
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
86
+ console.warn(`Warning: Configuration file at ${this.configFilePath} contains invalid data. Using defaults.`);
87
+ this.config = { ...DEFAULT_CONFIG };
88
+ return this.config;
89
+ }
90
+ // Merge with defaults to ensure all keys are present
91
+ this.config = {
92
+ ...DEFAULT_CONFIG,
93
+ ...this.sanitizeConfig(parsed),
94
+ };
95
+ return this.config;
96
+ }
97
+ catch (error) {
98
+ const err = error;
99
+ if (err.code === "ENOENT") {
100
+ // File doesn't exist - use defaults (this is normal on first run)
101
+ this.config = { ...DEFAULT_CONFIG };
102
+ return this.config;
103
+ }
104
+ if (err instanceof SyntaxError) {
105
+ // JSON parsing error - corrupted file
106
+ console.warn(`Warning: Configuration file at ${this.configFilePath} is corrupted (invalid JSON). Using defaults.`);
107
+ this.config = { ...DEFAULT_CONFIG };
108
+ return this.config;
109
+ }
110
+ // Other errors (permission issues, etc.)
111
+ console.warn(`Warning: Could not read configuration file at ${this.configFilePath}: ${err.message}. Using defaults.`);
112
+ this.config = { ...DEFAULT_CONFIG };
113
+ return this.config;
114
+ }
115
+ }
116
+ /**
117
+ * Sanitizes the loaded configuration to ensure only valid keys are included.
118
+ *
119
+ * @param data - The raw parsed configuration data.
120
+ * @returns A sanitized configuration object.
121
+ */
122
+ sanitizeConfig(data) {
123
+ const sanitized = {};
124
+ if (typeof data.githubToken === "string") {
125
+ sanitized.githubToken = data.githubToken;
126
+ }
127
+ if (typeof data.remoteUrl === "string") {
128
+ sanitized.remoteUrl = data.remoteUrl;
129
+ }
130
+ if (typeof data.defaultProject === "string") {
131
+ sanitized.defaultProject = data.defaultProject;
132
+ }
133
+ if (typeof data.stackRoot === "string") {
134
+ sanitized.stackRoot = data.stackRoot;
135
+ }
136
+ if (typeof data.uiEnabled === "boolean") {
137
+ sanitized.uiEnabled = data.uiEnabled;
138
+ }
139
+ if (typeof data.docsEnabled === "boolean") {
140
+ sanitized.docsEnabled = data.docsEnabled;
141
+ }
142
+ return sanitized;
143
+ }
144
+ /**
145
+ * Saves the current configuration to the file.
146
+ *
147
+ * @returns A promise that resolves when the configuration is saved.
148
+ */
149
+ async save() {
150
+ await this.ensureConfigDir();
151
+ // Only write non-undefined values
152
+ const dataToWrite = {};
153
+ for (const [key, value] of Object.entries(this.config)) {
154
+ if (value !== undefined) {
155
+ dataToWrite[key] = value;
156
+ }
157
+ }
158
+ const content = JSON.stringify(dataToWrite, null, 2);
159
+ await fs.promises.writeFile(this.configFilePath, content, "utf-8");
160
+ }
161
+ /**
162
+ * Gets a configuration value by key.
163
+ *
164
+ * @param key - The configuration key to retrieve.
165
+ * @returns The configuration value, or undefined if not set.
166
+ */
167
+ get(key) {
168
+ return this.config[key];
169
+ }
170
+ /**
171
+ * Sets a configuration value by key.
172
+ *
173
+ * @param key - The configuration key to set.
174
+ * @param value - The value to set.
175
+ * @returns A promise that resolves when the value is saved.
176
+ */
177
+ async set(key, value) {
178
+ this.config[key] = value;
179
+ await this.save();
180
+ }
181
+ /**
182
+ * Gets the GitHub token.
183
+ *
184
+ * @returns The GitHub token, or undefined if not set.
185
+ */
186
+ getGithubToken() {
187
+ return this.get("githubToken");
188
+ }
189
+ /**
190
+ * Sets the GitHub token.
191
+ *
192
+ * @param token - The GitHub token to set.
193
+ * @returns A promise that resolves when the token is saved.
194
+ */
195
+ async setGithubToken(token) {
196
+ await this.set("githubToken", token);
197
+ }
198
+ /**
199
+ * Clears the GitHub token.
200
+ *
201
+ * @returns A promise that resolves when the token is cleared.
202
+ */
203
+ async clearGithubToken() {
204
+ await this.set("githubToken", undefined);
205
+ }
206
+ /**
207
+ * Gets the remote API URL.
208
+ *
209
+ * @returns The remote URL, or undefined if not set.
210
+ */
211
+ getRemoteUrl() {
212
+ return this.get("remoteUrl");
213
+ }
214
+ /**
215
+ * Sets the remote API URL.
216
+ *
217
+ * @param url - The remote URL to set.
218
+ * @returns A promise that resolves when the URL is saved.
219
+ */
220
+ async setRemoteUrl(url) {
221
+ await this.set("remoteUrl", url);
222
+ }
223
+ /**
224
+ * Gets the default project.
225
+ *
226
+ * @returns The default project (owner/repo format), or undefined if not set.
227
+ */
228
+ getDefaultProject() {
229
+ return this.get("defaultProject");
230
+ }
231
+ /**
232
+ * Sets the default project.
233
+ *
234
+ * @param project - The default project to set (owner/repo format).
235
+ * @returns A promise that resolves when the project is saved.
236
+ */
237
+ async setDefaultProject(project) {
238
+ await this.set("defaultProject", project);
239
+ }
240
+ /**
241
+ * Gets the local stack root directory (where .env, data/, logs/, repos/ live).
242
+ *
243
+ * @returns The stack root path, or undefined if not set.
244
+ */
245
+ getStackRoot() {
246
+ return this.get("stackRoot");
247
+ }
248
+ /**
249
+ * Sets the local stack root directory.
250
+ *
251
+ * @param root - Absolute path to the stack root.
252
+ */
253
+ async setStackRoot(root) {
254
+ await this.set("stackRoot", root);
255
+ }
256
+ /**
257
+ * Gets the desired UI service state. Defaults to true when unset.
258
+ */
259
+ getUiEnabled() {
260
+ return this.get("uiEnabled") ?? true;
261
+ }
262
+ /**
263
+ * Sets the desired UI service state.
264
+ */
265
+ async setUiEnabled(enabled) {
266
+ await this.set("uiEnabled", enabled);
267
+ }
268
+ /**
269
+ * Gets the desired docs service state. Defaults to false when unset.
270
+ */
271
+ getDocsEnabled() {
272
+ return this.get("docsEnabled") ?? false;
273
+ }
274
+ /**
275
+ * Sets the desired docs service state.
276
+ */
277
+ async setDocsEnabled(enabled) {
278
+ await this.set("docsEnabled", enabled);
279
+ }
280
+ /**
281
+ * Gets all configuration values.
282
+ *
283
+ * @returns A copy of the current configuration.
284
+ */
285
+ getAll() {
286
+ return { ...this.config };
287
+ }
288
+ /**
289
+ * Resets the configuration to default values.
290
+ *
291
+ * @param persist - Whether to persist the reset to the file. Defaults to true.
292
+ * @returns A promise that resolves when the reset is complete.
293
+ */
294
+ async reset(persist = true) {
295
+ this.config = { ...DEFAULT_CONFIG };
296
+ if (persist) {
297
+ await this.save();
298
+ }
299
+ }
300
+ /**
301
+ * Deletes the configuration file.
302
+ *
303
+ * @returns A promise that resolves when the file is deleted.
304
+ */
305
+ async deleteConfigFile() {
306
+ try {
307
+ await fs.promises.unlink(this.configFilePath);
308
+ }
309
+ catch (error) {
310
+ const err = error;
311
+ if (err.code !== "ENOENT") {
312
+ throw error;
313
+ }
314
+ // File doesn't exist - nothing to delete
315
+ }
316
+ this.config = { ...DEFAULT_CONFIG };
317
+ }
318
+ /**
319
+ * Gets the path to the configuration file.
320
+ *
321
+ * @returns The configuration file path.
322
+ */
323
+ getConfigFilePath() {
324
+ return this.configFilePath;
325
+ }
326
+ /**
327
+ * Gets the path to the configuration directory.
328
+ *
329
+ * @returns The configuration directory path.
330
+ */
331
+ getConfigDir() {
332
+ return this.configDir;
333
+ }
334
+ /**
335
+ * Checks if the configuration file exists.
336
+ *
337
+ * @returns A promise that resolves to true if the file exists.
338
+ */
339
+ async configFileExists() {
340
+ try {
341
+ await fs.promises.access(this.configFilePath, fs.constants.F_OK);
342
+ return true;
343
+ }
344
+ catch {
345
+ return false;
346
+ }
347
+ }
348
+ }
349
+ /**
350
+ * Creates and initializes a ConfigManager instance.
351
+ * This is a convenience function for one-off usage.
352
+ *
353
+ * @param customConfigDir - Optional custom configuration directory path.
354
+ * @returns A promise that resolves to an initialized ConfigManager.
355
+ */
356
+ export async function createConfigManager(customConfigDir) {
357
+ const manager = new ConfigManager(customConfigDir);
358
+ await manager.init();
359
+ return manager;
360
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * CLI Configuration Module
3
+ *
4
+ * Exports the ConfigManager and related types for managing
5
+ * persistent CLI configuration.
6
+ */
7
+ export { ConfigManager, createConfigManager } from "./ConfigManager.js";
8
+ export { DEFAULT_CONFIG } from "./types.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * CLI Configuration Types
3
+ *
4
+ * These types define the configuration schema for the CLI.
5
+ */
6
+ /**
7
+ * Default configuration values.
8
+ */
9
+ export const DEFAULT_CONFIG = {
10
+ githubToken: undefined,
11
+ remoteUrl: undefined,
12
+ defaultProject: undefined,
13
+ stackRoot: undefined,
14
+ uiEnabled: undefined,
15
+ docsEnabled: undefined,
16
+ };
package/dist/index.js ADDED
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { config } from "dotenv";
4
+ import { readFileSync } from "fs";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "url";
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";
9
+ // Re-export configuration module for programmatic use
10
+ export { ConfigManager, createConfigManager, DEFAULT_CONFIG, } from "./config/index.js";
11
+ // Re-export API module for programmatic use
12
+ export { ApiClient, createApiClient, createApiClientWithConfig, ApiError, UnauthorizedError, ForbiddenError, NotFoundError, BadRequestError, InternalServerError, NetworkError, TimeoutError, createApiError, } from "./api/index.js";
13
+ // Re-export utilities module for programmatic use
14
+ export { resolveProject, ProjectResolutionError, formatOutput, printOutput, readJsonInput, validateJsonFields, isPlainObject, JsonInputError, } from "./utils/index.js";
15
+ // Load environment variables
16
+ config();
17
+ const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
18
+ const program = new Command();
19
+ program
20
+ .name("propr")
21
+ .description("ProPR control plane + backend client - run a local stack and implement GitHub issues with AI agents")
22
+ .version(packageJson.version ?? "0.0.0")
23
+ .option("-p, --project <project>", "Specify the target project (owner/repo)")
24
+ .addHelpText("before", `
25
+ ProPR CLI - AI-Powered GitHub Issue Implementation
26
+
27
+ Run a local ProPR Docker stack (check / init / start / status / stop) and
28
+ drive the backend (plans, issues, tasks, repos, agents).
29
+ `)
30
+ .addHelpText("after", `
31
+ Quick Start (local stack):
32
+ $ propr Verify the environment (same as 'propr check')
33
+ $ propr init stack Scaffold .env + data/logs/repos, detect agents
34
+ $ propr start Start the stack with a live dashboard
35
+ $ propr status Show local stack status
36
+ $ propr stop Stop the stack
37
+
38
+ Quick Start (backend client):
39
+ $ propr remote <url> Set the backend API URL
40
+ $ propr login <token> Authenticate with GitHub
41
+ $ propr use <owner/repo> Set default project
42
+ $ propr plan list View available implementation plans
43
+ $ propr issue implement <id> Implement a GitHub issue
44
+
45
+ JSON Output:
46
+ Most commands support --json (-j) for machine-readable output:
47
+ $ propr plan list --json
48
+ $ propr agent list -j
49
+
50
+ Examples:
51
+ $ propr remote https://api.propr.example.com
52
+ $ propr login ghp_xxxxxxxxxxxx
53
+ $ propr use myorg/myrepo
54
+ $ propr plan create "Add dark mode toggle" --wait
55
+ $ propr issue implement abc123/1 --wait --auto-merge
56
+ $ propr task list -s processing
57
+ $ propr remote-status
58
+
59
+ Command Groups:
60
+ Control Plane: check, init [repo|stack], start, status, stop, ui, docs, tank
61
+ GitHub Relay: relay [enroll|list|revoke]
62
+ Configuration: remote, use, login, logout
63
+ Plans: plan [create|list|get|delete|abort]
64
+ Implementation: issue [implement]
65
+ Tasks: task [list|get|stop|delete|revert]
66
+ Repositories: repo [list|add|remove|toggle|index|status]
67
+ Agents: agent [list|add|enable|disable|delete]
68
+ Settings: setting [get|update]
69
+ To-Dos: todo [list|get|add|complete|delete]
70
+ Logs: log [list]
71
+ Backend: remote-status, queue
72
+
73
+ For more information on a command, run:
74
+ $ propr <command> --help
75
+ `);
76
+ // Remote command - set the API base URL
77
+ program
78
+ .command("remote <url>")
79
+ .description("Set the remote API base URL for ProPR backend")
80
+ .addHelpText("after", `
81
+ Example:
82
+ $ propr remote https://api.propr.example.com
83
+ `)
84
+ .action(async (url) => {
85
+ try {
86
+ const configManager = await createConfigManager();
87
+ await configManager.setRemoteUrl(url);
88
+ console.log(`Remote URL set to: ${url}`);
89
+ console.log(`Configuration saved to: ${configManager.getConfigFilePath()}`);
90
+ }
91
+ catch (error) {
92
+ console.error(`Error setting remote URL: ${error.message}`);
93
+ process.exit(1);
94
+ }
95
+ });
96
+ // Use command - set the default project
97
+ program
98
+ .command("use <project>")
99
+ .description("Set the default project (repository) for subsequent commands")
100
+ .addHelpText("after", `
101
+ Argument:
102
+ project Repository in owner/repo format (e.g., myorg/myrepo)
103
+
104
+ Example:
105
+ $ propr use myorg/myrepo
106
+ `)
107
+ .action(async (project) => {
108
+ try {
109
+ const configManager = await createConfigManager();
110
+ await configManager.setDefaultProject(project);
111
+ console.log(`Default project set to: ${project}`);
112
+ console.log(`Configuration saved to: ${configManager.getConfigFilePath()}`);
113
+ }
114
+ catch (error) {
115
+ console.error(`Error setting default project: ${error.message}`);
116
+ process.exit(1);
117
+ }
118
+ });
119
+ // Login command - authenticate with GitHub
120
+ program
121
+ .command("login [token]")
122
+ .description("Authenticate with GitHub (interactive via gh CLI, or provide a PAT)")
123
+ .addHelpText("after", `
124
+ Argument:
125
+ token GitHub Personal Access Token (optional)
126
+
127
+ When no token is provided, the CLI uses 'gh' (GitHub CLI) to authenticate:
128
+ - If you're already logged in to gh, your token is used automatically
129
+ - If not, 'gh auth login' is launched interactively
130
+
131
+ Examples:
132
+ $ propr login # Interactive login via gh CLI
133
+ $ propr login ghp_xxxxxxxxxxxx # Use a PAT directly
134
+ `)
135
+ .action(async (token) => {
136
+ try {
137
+ const configManager = await createConfigManager();
138
+ if (token) {
139
+ // Direct PAT flow
140
+ const validPrefixes = ["ghp_", "gho_", "ghu_", "ghs_", "ghr_"];
141
+ const hasValidPrefix = validPrefixes.some((prefix) => token.startsWith(prefix));
142
+ if (!hasValidPrefix && token.length < 40) {
143
+ console.warn("Warning: The provided token does not appear to be a valid GitHub token format.");
144
+ console.warn("GitHub personal access tokens typically start with 'ghp_'.");
145
+ console.log("");
146
+ }
147
+ await configManager.setGithubToken(token);
148
+ console.log("Authentication successful!");
149
+ console.log(`Token saved to: ${configManager.getConfigFilePath()}`);
150
+ return;
151
+ }
152
+ // Interactive flow via gh CLI
153
+ const { execSync, spawnSync } = await import("child_process");
154
+ // Check if gh is installed
155
+ try {
156
+ execSync("gh --version", { stdio: "ignore" });
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",
185
+ });
186
+ if (result.status !== 0) {
187
+ console.error("Error: GitHub login failed or was cancelled.");
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.");
194
+ process.exit(1);
195
+ }
196
+ await configManager.setGithubToken(ghToken);
197
+ console.log("");
198
+ console.log("Authentication successful!");
199
+ console.log(`Token saved to: ${configManager.getConfigFilePath()}`);
200
+ }
201
+ catch (error) {
202
+ console.error(`Error during login: ${error.message}`);
203
+ process.exit(1);
204
+ }
205
+ });
206
+ // Logout command - clear the GitHub token
207
+ program
208
+ .command("logout")
209
+ .description("Clear the stored GitHub token from configuration")
210
+ .addHelpText("after", `
211
+ Example:
212
+ $ propr logout
213
+ `)
214
+ .action(async () => {
215
+ try {
216
+ const configManager = await createConfigManager();
217
+ const existingToken = configManager.getGithubToken();
218
+ if (!existingToken) {
219
+ console.log("No token is currently configured.");
220
+ return;
221
+ }
222
+ await configManager.clearGithubToken();
223
+ console.log("Successfully logged out.");
224
+ console.log("GitHub token has been removed from configuration.");
225
+ }
226
+ catch (error) {
227
+ console.error(`Error clearing token: ${error.message}`);
228
+ process.exit(1);
229
+ }
230
+ });
231
+ // Control-plane commands (local Docker stack)
232
+ program.addCommand(createCheckCommand());
233
+ program.addCommand(createStartCommand());
234
+ program.addCommand(createStackStatusCommand());
235
+ program.addCommand(createStopCommand());
236
+ program.addCommand(createUiCommand());
237
+ program.addCommand(createDocsCommand());
238
+ program.addCommand(createTankCommand());
239
+ program.addCommand(createRelayCommand());
240
+ // Setup + backend client command groups
241
+ program.addCommand(createInitCommand());
242
+ program.addCommand(createPlanCommand());
243
+ program.addCommand(createIssueCommand());
244
+ program.addCommand(createTaskCommand());
245
+ program.addCommand(createRepoCommand());
246
+ program.addCommand(createAgentCommand());
247
+ program.addCommand(createSettingCommand());
248
+ program.addCommand(createLogCommand());
249
+ program.addCommand(createTodoCommand());
250
+ program.addCommand(createRemoteStatusCommand());
251
+ program.addCommand(createQueueCommand());
252
+ // Bare `propr` (no args): run the environment check, then hint at next steps.
253
+ if (!process.argv.slice(2).length) {
254
+ void (async () => {
255
+ try {
256
+ const outcome = await runChecks();
257
+ printChecks(outcome);
258
+ console.log("");
259
+ if (outcome.results.some((r) => r.name === STACK_CONFIG_CHECK_NAME && r.status !== "ok")) {
260
+ console.log("Next: `propr init stack` to scaffold a stack, then `propr start`.");
261
+ }
262
+ else {
263
+ console.log("Next: `propr start` to launch the stack · `propr --help` for all commands.");
264
+ }
265
+ process.exit(outcome.anyFail ? 1 : 0);
266
+ }
267
+ catch (error) {
268
+ console.error(`Error: ${error.message}`);
269
+ console.log("Run 'propr --help' for usage information.");
270
+ process.exit(1);
271
+ }
272
+ })();
273
+ }
274
+ else {
275
+ program.parse();
276
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared rendering for stack status — used by `propr status` (one-shot) and the
3
+ * `propr start` TUI's non-TTY fallback.
4
+ */
5
+ export function stateGlyph(s) {
6
+ if (!s.exists)
7
+ return "·";
8
+ if (s.running)
9
+ return "●";
10
+ return "○";
11
+ }
12
+ /** A single padded status row, e.g. "● api running Up 3 minutes". */
13
+ export function formatServiceRow(s, nameWidth) {
14
+ const glyph = stateGlyph(s);
15
+ const name = s.service.padEnd(nameWidth);
16
+ const state = (s.exists ? s.state : "absent").padEnd(10);
17
+ const detail = s.exists ? s.status : "not created";
18
+ const ports = s.ports ? ` ${s.ports}` : "";
19
+ return `${glyph} ${name} ${state} ${detail}${ports}`;
20
+ }
21
+ /** Full status table as a string. */
22
+ export function renderStatusTable(status) {
23
+ const nameWidth = Math.max(...status.services.map((s) => s.service.length), 8);
24
+ const lines = [];
25
+ lines.push(`Stack: ${status.stack} network: ${status.network} ${status.running ? "running" : "stopped"}`);
26
+ lines.push("─".repeat(60));
27
+ for (const s of status.services) {
28
+ lines.push(formatServiceRow(s, nameWidth));
29
+ }
30
+ return lines.join("\n");
31
+ }