propr-cli 0.8.3 → 0.8.5
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 +182 -59
- 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 +3 -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/stackCommands.js +14 -2
- package/dist/commands/systemCommands.js +49 -2
- package/dist/commands/tunnelCommand.js +562 -0
- package/dist/config/ConfigManager.js +22 -0
- package/dist/config/types.js +1 -0
- package/dist/index.js +14 -45
- package/dist/orchestrator/format.js +46 -0
- package/dist/orchestrator/index.js +7 -2
- package/dist/orchestrator/manifest.json +12 -11
- package/dist/orchestrator/orchestrator.mjs +872 -73
- 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 +28 -2
- package/dist/utils/envFile.js +45 -0
- package/dist/vendor/shared/githubEventIntakeMode.js +41 -0
- package/dist/vendor/shared/index.js +17 -0
- package/dist/vendor/shared/intakeModePrerequisites.js +76 -0
- package/dist/vendor/shared/modelDefinitions.js +4 -4
- package/dist/vendor/shared/proprCompatibility.js +70 -0
- package/dist/vendor/shared/proprServiceUrls.js +124 -0
- package/dist/vendor/shared/statusKeys.js +14 -0
- package/dist/vendor/shared/validateRoutingUrl.js +46 -0
- package/package.json +3 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker image maintenance commands for the local ProPR stack.
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { createConfigManager } from "../config/index.js";
|
|
6
|
+
import { getHostConfig } from "../orchestrator/index.js";
|
|
7
|
+
async function pullImages(options) {
|
|
8
|
+
const configManager = await createConfigManager();
|
|
9
|
+
const { orch, cfg, rootDir } = await getHostConfig({ configManager, root: options.root });
|
|
10
|
+
if (!orch.dockerAvailable()) {
|
|
11
|
+
console.error("Error: cannot reach the Docker daemon. Run 'propr check' for diagnostics.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
console.log(`Pulling ProPR images (root: ${rootDir})`);
|
|
15
|
+
const env = options.skipRemoteImageCheck
|
|
16
|
+
? { ...process.env, PROPR_SKIP_REMOTE_IMAGE_CHECK: "1" }
|
|
17
|
+
: process.env;
|
|
18
|
+
const { failedAgentImages, strictAgentPull } = orch.pullImages(cfg, {
|
|
19
|
+
env,
|
|
20
|
+
onLog: (line) => console.log(line),
|
|
21
|
+
});
|
|
22
|
+
if (failedAgentImages.length > 0) {
|
|
23
|
+
console.warn(`\nwarning: ${failedAgentImages.length} agent image(s) could not be pulled:`);
|
|
24
|
+
for (const tag of failedAgentImages)
|
|
25
|
+
console.warn(` - ${tag}`);
|
|
26
|
+
console.warn(" Jobs using those agents will fail until the images are available.");
|
|
27
|
+
if (strictAgentPull)
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function createImagesCommand() {
|
|
32
|
+
const images = new Command("images")
|
|
33
|
+
.description("Manage local ProPR Docker images");
|
|
34
|
+
images
|
|
35
|
+
.command("pull")
|
|
36
|
+
.description("Pull missing or stale ProPR Docker images without starting the stack")
|
|
37
|
+
.option("--root <dir>", "Stack root directory (where .env/data/logs/repos live)")
|
|
38
|
+
.option("--skip-remote-image-check", "Skip registry freshness checks before deciding what to pull")
|
|
39
|
+
.addHelpText("after", `
|
|
40
|
+
Examples:
|
|
41
|
+
$ propr images pull
|
|
42
|
+
$ propr images pull --skip-remote-image-check
|
|
43
|
+
$ propr images pull --root ~/propr
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
--skip-remote-image-check is for offline runs. It still pulls missing images,
|
|
47
|
+
but local images are treated as acceptable because stale tags cannot be
|
|
48
|
+
detected without the registry freshness check.
|
|
49
|
+
`)
|
|
50
|
+
.action(async (options) => {
|
|
51
|
+
try {
|
|
52
|
+
await pullImages(options);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(`Error pulling images: ${error.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return images;
|
|
60
|
+
}
|
package/dist/commands/index.js
CHANGED
|
@@ -13,10 +13,13 @@ export { createLogCommand } from "./logCommands.js";
|
|
|
13
13
|
export { createTodoCommand } from "./todoCommands.js";
|
|
14
14
|
export { createRemoteStatusCommand, createQueueCommand } from "./systemCommands.js";
|
|
15
15
|
export { createInitCommand } from "./initCommands.js";
|
|
16
|
+
export { createSetupCommand } from "./setupCommand.js";
|
|
16
17
|
// Control-plane commands (local Docker stack)
|
|
17
18
|
export { createCheckCommand, runChecks, printChecks, STACK_CONFIG_CHECK_NAME } from "./checkCommands.js";
|
|
19
|
+
export { createImagesCommand } from "./imageCommands.js";
|
|
18
20
|
export { createStartCommand } from "./startCommand.js";
|
|
19
21
|
export { createStackStatusCommand, createStopCommand } from "./stackCommands.js";
|
|
20
22
|
export { createUiCommand, createDocsCommand } from "./uiDocsCommands.js";
|
|
23
|
+
export { createTunnelCommand } from "./tunnelCommand.js";
|
|
21
24
|
export { createTankCommand } from "./tankCommands.js";
|
|
22
25
|
export { createRelayCommand } from "./relayCommands.js";
|
|
@@ -9,9 +9,42 @@
|
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import { existsSync, copyFileSync, chmodSync, mkdirSync, readFileSync, appendFileSync } from "node:fs";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { createConfigManager } from "../config/index.js";
|
|
15
|
+
// Mirrors the launcher's HOST_VIBE_PROMPT_CACHE_DIR default in
|
|
16
|
+
// docker/launcher/orchestrator.mjs. Keep it per-user and private because prompt
|
|
17
|
+
// files can contain task/repository context.
|
|
18
|
+
function defaultVibePromptCacheDir() {
|
|
19
|
+
return `/tmp/propr-vibe-prompts-${process.getuid?.() ?? "user"}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pre-create the host Vibe prompt-cache directory owned by the invoking user.
|
|
23
|
+
*
|
|
24
|
+
* Spawned Vibe agent containers bind-mount this path. If it does not exist when
|
|
25
|
+
* Docker first mounts it, the daemon auto-creates it as root, leaving a
|
|
26
|
+
* root-owned directory the user can no longer write to — which then trips
|
|
27
|
+
* `validateEnv`'s writability check and blocks `propr start` on every
|
|
28
|
+
* subsequent run. Creating it here (owned by the user, 0700) avoids that.
|
|
29
|
+
*
|
|
30
|
+
* Returns the directory if it was created, otherwise `undefined`. Safe to call
|
|
31
|
+
* repeatedly: an existing directory (regardless of owner) is left untouched.
|
|
32
|
+
*/
|
|
33
|
+
export function ensureVibePromptCacheDir(cacheDir) {
|
|
34
|
+
if (!cacheDir)
|
|
35
|
+
return undefined;
|
|
36
|
+
// Skip container-only / Docker-special paths (a ':' would be a volume spec).
|
|
37
|
+
if (!isAbsolute(cacheDir) || cacheDir.includes(":"))
|
|
38
|
+
return undefined;
|
|
39
|
+
if (existsSync(cacheDir))
|
|
40
|
+
return undefined;
|
|
41
|
+
mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
|
|
42
|
+
try {
|
|
43
|
+
chmodSync(cacheDir, 0o700);
|
|
44
|
+
}
|
|
45
|
+
catch { /* best-effort: keep prompt material private */ }
|
|
46
|
+
return cacheDir;
|
|
47
|
+
}
|
|
15
48
|
/** Resolve the bundled .env.example, falling back to a repo checkout. */
|
|
16
49
|
function resolveEnvExample() {
|
|
17
50
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -119,6 +152,22 @@ export async function scaffoldStack(options = {}) {
|
|
|
119
152
|
}
|
|
120
153
|
result.detected = detected;
|
|
121
154
|
result.pendingCredentials = toAppend;
|
|
155
|
+
// 3b. When Vibe is in play, pre-create its prompt-cache dir so spawned Vibe
|
|
156
|
+
// agent containers can bind-mount a writable host directory. Creating it
|
|
157
|
+
// here (owned by the invoking user) avoids Docker auto-creating it as
|
|
158
|
+
// root. Defaults to a per-user /tmp/propr-vibe-prompts-$uid path
|
|
159
|
+
// (matching the launcher default); an explicit
|
|
160
|
+
// HOST_VIBE_PROMPT_CACHE_DIR is honored.
|
|
161
|
+
const vibeConfigured = detected.some((c) => c.envKey === "HOST_VIBE_DIR") ||
|
|
162
|
+
/^\s*(?:export\s+)?(?:HOST_VIBE_DIR|MISTRAL_API_KEY)\s*=\s*\S/m.test(envContent);
|
|
163
|
+
if (vibeConfigured) {
|
|
164
|
+
const match = envContent.match(/^\s*(?:export\s+)?HOST_VIBE_PROMPT_CACHE_DIR\s*=\s*(.+)\s*$/m);
|
|
165
|
+
const configured = match?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
166
|
+
const cacheDir = configured || defaultVibePromptCacheDir();
|
|
167
|
+
const created = ensureVibePromptCacheDir(cacheDir);
|
|
168
|
+
if (created)
|
|
169
|
+
result.dirsCreated.push(created);
|
|
170
|
+
}
|
|
122
171
|
// 4. Persist the stack root so other commands can find it.
|
|
123
172
|
const configManager = await createConfigManager();
|
|
124
173
|
await configManager.setStackRoot(rootDir);
|
|
@@ -10,41 +10,67 @@
|
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { hostname } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { validateRelayUrl } from "../vendor/shared/index.js";
|
|
13
|
+
import { validateRelayUrl, DEFAULT_PROPR_GH_RELAY_URL } from "../vendor/shared/index.js";
|
|
14
14
|
import { createConfigManager } from "../config/index.js";
|
|
15
15
|
import { loadOrchestrator, resolveStackRoot } from "../orchestrator/index.js";
|
|
16
16
|
import { upsertEnvVars } from "../utils/envFile.js";
|
|
17
|
-
import { enrollRelayToken, listRelayTokens, revokeRelayToken, } from "../api/relay.js";
|
|
17
|
+
import { enrollRelayToken, fetchAuthenticatedUser, listRelayTokens, revokeRelayToken, } from "../api/relay.js";
|
|
18
18
|
async function resolveContext(options) {
|
|
19
19
|
const configManager = await createConfigManager();
|
|
20
20
|
const rootDir = resolveStackRoot(configManager, options.root);
|
|
21
21
|
const envPath = join(rootDir, ".env");
|
|
22
22
|
const orch = await loadOrchestrator();
|
|
23
23
|
const fileEnv = orch.readEnvFile(envPath);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Falls back to the hosted relay (webhook.propr.dev) so `propr relay enroll`
|
|
25
|
+
// works out of the box; an explicit --url or PROPR_GH_RELAY_URL overrides it
|
|
26
|
+
// for self-hosted relays.
|
|
27
|
+
const relayBaseUrl = options.url ??
|
|
28
|
+
process.env.PROPR_GH_RELAY_URL ??
|
|
29
|
+
fileEnv.PROPR_GH_RELAY_URL ??
|
|
30
|
+
DEFAULT_PROPR_GH_RELAY_URL;
|
|
28
31
|
const urlError = validateRelayUrl(relayBaseUrl);
|
|
29
32
|
if (urlError) {
|
|
30
33
|
throw new Error(urlError);
|
|
31
34
|
}
|
|
32
|
-
const installationId = options.installation ?? process.env.GH_INSTALLATION_ID ?? fileEnv.GH_INSTALLATION_ID;
|
|
33
|
-
if (!installationId) {
|
|
34
|
-
throw new Error("No installation id. Pass --installation <id> or set GH_INSTALLATION_ID in .env.");
|
|
35
|
-
}
|
|
36
35
|
const githubToken = configManager.getGithubToken();
|
|
37
36
|
if (!githubToken) {
|
|
38
37
|
throw new Error("Not logged in to GitHub. Run `propr login` first.");
|
|
39
38
|
}
|
|
39
|
+
const client = { baseUrl: relayBaseUrl, githubToken };
|
|
40
|
+
// Explicit flag / env / .env win; otherwise ask the relay which installations
|
|
41
|
+
// this GitHub identity can access and auto-select when there's exactly one.
|
|
42
|
+
const installationId = options.installation ??
|
|
43
|
+
process.env.GH_INSTALLATION_ID ??
|
|
44
|
+
fileEnv.GH_INSTALLATION_ID ??
|
|
45
|
+
(await discoverInstallationId(client));
|
|
40
46
|
return {
|
|
41
47
|
rootDir,
|
|
42
48
|
envPath,
|
|
43
49
|
relayBaseUrl,
|
|
44
50
|
installationId,
|
|
45
|
-
client
|
|
51
|
+
client,
|
|
46
52
|
};
|
|
47
53
|
}
|
|
54
|
+
// Discovery fallback when no installation id was supplied: query the relay for
|
|
55
|
+
// the installations this GitHub identity can access. Auto-select the only one;
|
|
56
|
+
// otherwise fail with an actionable message (zero installs vs. ambiguous choice).
|
|
57
|
+
export async function discoverInstallationId(client) {
|
|
58
|
+
const { installations } = await fetchAuthenticatedUser(client);
|
|
59
|
+
if (installations.length === 1) {
|
|
60
|
+
const only = installations[0];
|
|
61
|
+
// To stderr, not stdout: `relay list --json` must emit only the JSON body,
|
|
62
|
+
// so this informational notice must stay out of the data stream.
|
|
63
|
+
console.error(`Using installation ${only.installation_id} (${only.account_login}) — the only one available to you.`);
|
|
64
|
+
return String(only.installation_id);
|
|
65
|
+
}
|
|
66
|
+
if (installations.length === 0) {
|
|
67
|
+
throw new Error("No GitHub App installation is available for your account. Install the shared ProPR GitHub App, then retry — or pass --installation <id>.");
|
|
68
|
+
}
|
|
69
|
+
const options = installations
|
|
70
|
+
.map((i) => ` ${i.installation_id} ${i.account_login} (${i.account_type})`)
|
|
71
|
+
.join("\n");
|
|
72
|
+
throw new Error(`Multiple installations are available; pass --installation <id> to choose one:\n${options}`);
|
|
73
|
+
}
|
|
48
74
|
export function createRelayCommand() {
|
|
49
75
|
const relay = new Command("relay")
|
|
50
76
|
.description("Manage GitHub token relay enrollment (shared-app auth path)")
|
|
@@ -52,8 +78,15 @@ export function createRelayCommand() {
|
|
|
52
78
|
The relay lets a shared-app stack obtain GitHub installation tokens without
|
|
53
79
|
holding the App's private key. Enroll once; the token is saved to your .env.
|
|
54
80
|
|
|
81
|
+
The relay URL defaults to the hosted service (${DEFAULT_PROPR_GH_RELAY_URL});
|
|
82
|
+
pass --url only when running a self-hosted relay.
|
|
83
|
+
|
|
84
|
+
The installation id is discovered automatically when you have exactly one;
|
|
85
|
+
pass --installation <id> to disambiguate or override it.
|
|
86
|
+
|
|
55
87
|
Examples:
|
|
56
|
-
$ propr relay enroll
|
|
88
|
+
$ propr relay enroll
|
|
89
|
+
$ propr relay enroll --url https://relay.example.com/v1
|
|
57
90
|
$ propr relay list
|
|
58
91
|
$ propr relay revoke <token-id>
|
|
59
92
|
`);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent enablement + image-based authentication for `propr setup`.
|
|
3
|
+
*
|
|
4
|
+
* This runs as a setup step *after the stack is up* (the backend must be
|
|
5
|
+
* reachable to read and write agent configuration). It does three things, each
|
|
6
|
+
* non-destructively:
|
|
7
|
+
*
|
|
8
|
+
* 1. Reads the agents already configured in the running backend.
|
|
9
|
+
* 2. Adds any *selected* agent whose type is not yet configured, seeding it
|
|
10
|
+
* from the shared {@link AGENT_DEFAULTS} metadata (alias + supported
|
|
11
|
+
* models). Existing agents are never disabled, deleted, or re-aliased — a
|
|
12
|
+
* re-run only fills in what is missing.
|
|
13
|
+
* 3. For selected agents that support an interactive image login (see
|
|
14
|
+
* {@link planAgentLogin}), offers to authenticate through the agent's
|
|
15
|
+
* Docker image and runs the login only for the ones the user confirms.
|
|
16
|
+
*
|
|
17
|
+
* Like the engine, this module is UI-agnostic: the side effects live behind the
|
|
18
|
+
* injectable {@link AgentSetupActions} seam (tests pass mocks so the flow runs
|
|
19
|
+
* without Docker, the network, or a TTY) and the single user decision is
|
|
20
|
+
* collected through the optional {@link AgentSetupParams.confirmLogin} callback
|
|
21
|
+
* (a missing callback means "authenticate nothing", the safe default).
|
|
22
|
+
*/
|
|
23
|
+
import { AGENT_DEFAULTS } from "../../vendor/shared/index.js";
|
|
24
|
+
/**
|
|
25
|
+
* Enable the selected agents in the running backend and, on confirmation,
|
|
26
|
+
* authenticate the ones that support an image login. Never throws for expected
|
|
27
|
+
* conditions — every failure is captured in {@link AgentSetupOutcome.errors} so
|
|
28
|
+
* the caller can settle the step as a warning rather than aborting setup.
|
|
29
|
+
*/
|
|
30
|
+
export async function runAgentSetup(params) {
|
|
31
|
+
const { rootDir, selectedAgents, actions, confirmLogin, onLog } = params;
|
|
32
|
+
const outcome = {
|
|
33
|
+
added: [],
|
|
34
|
+
alreadyConfigured: [],
|
|
35
|
+
authenticated: [],
|
|
36
|
+
authFailed: [],
|
|
37
|
+
errors: [],
|
|
38
|
+
};
|
|
39
|
+
if (selectedAgents.length === 0)
|
|
40
|
+
return outcome;
|
|
41
|
+
// 1. Read the current backend configuration. Without it we cannot safely tell
|
|
42
|
+
// which agents are new, so a read failure stops here (nothing was changed).
|
|
43
|
+
let existing;
|
|
44
|
+
try {
|
|
45
|
+
existing = await actions.listAgents(rootDir);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
outcome.errors.push(`could not read backend agents: ${error.message}`);
|
|
49
|
+
return outcome;
|
|
50
|
+
}
|
|
51
|
+
// 2. Add the selected agents that are not yet configured. Match by type so we
|
|
52
|
+
// never add a second agent for a type the user already runs — existing
|
|
53
|
+
// agents (enabled or not) are left exactly as they are.
|
|
54
|
+
const configuredTypes = new Set(existing.map((agent) => agent.type));
|
|
55
|
+
for (const type of selectedAgents) {
|
|
56
|
+
if (configuredTypes.has(type)) {
|
|
57
|
+
outcome.alreadyConfigured.push(type);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const defaults = AGENT_DEFAULTS[type];
|
|
61
|
+
if (!defaults)
|
|
62
|
+
continue; // unknown type — guarded, but never trust the input
|
|
63
|
+
try {
|
|
64
|
+
onLog?.(`enabling agent ${type}…`);
|
|
65
|
+
// Seed from shared metadata: alias + the full supported-model set. The
|
|
66
|
+
// backend resolves the default docker image and host config path, so we
|
|
67
|
+
// don't pass them (a literal "~" path would otherwise reach the backend).
|
|
68
|
+
await actions.addAgent(rootDir, {
|
|
69
|
+
alias: defaults.defaultAlias,
|
|
70
|
+
type: type,
|
|
71
|
+
models: defaults.defaultModels,
|
|
72
|
+
enabled: true,
|
|
73
|
+
});
|
|
74
|
+
outcome.added.push(type);
|
|
75
|
+
configuredTypes.add(type);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
outcome.errors.push(`could not enable ${type}: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 3. Image-based authentication — only for selected agents that actually have
|
|
82
|
+
// a login plan, and only for the ones the user confirms.
|
|
83
|
+
let loginable;
|
|
84
|
+
try {
|
|
85
|
+
loginable = new Set(await actions.loginableAgents());
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
outcome.errors.push(`could not determine which agents support image login: ${error.message}`);
|
|
89
|
+
return outcome;
|
|
90
|
+
}
|
|
91
|
+
const candidates = selectedAgents.filter((type) => loginable.has(type));
|
|
92
|
+
if (candidates.length === 0 || !confirmLogin)
|
|
93
|
+
return outcome;
|
|
94
|
+
let chosen;
|
|
95
|
+
try {
|
|
96
|
+
chosen = await confirmLogin({ candidates, rootDir });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// A failed/cancelled prompt must not abort the whole run — just skip login.
|
|
100
|
+
outcome.errors.push(`agent login prompt failed: ${error.message}`);
|
|
101
|
+
return outcome;
|
|
102
|
+
}
|
|
103
|
+
const chosenSet = new Set(chosen.filter((type) => loginable.has(type)));
|
|
104
|
+
// Iterate the candidate order (not the user's), so logins run in a stable order.
|
|
105
|
+
for (const type of candidates) {
|
|
106
|
+
if (!chosenSet.has(type))
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
onLog?.(`authenticating ${type} through its image…`);
|
|
110
|
+
const result = await actions.loginAgent(rootDir, type);
|
|
111
|
+
if (result.detail)
|
|
112
|
+
onLog?.(result.detail);
|
|
113
|
+
if (result.available && result.success)
|
|
114
|
+
outcome.authenticated.push(type);
|
|
115
|
+
else
|
|
116
|
+
outcome.authFailed.push(type);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
outcome.authFailed.push(type);
|
|
120
|
+
outcome.errors.push(`login for ${type} failed: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return outcome;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build the production {@link AgentSetupActions}, lazily importing the heavy
|
|
127
|
+
* orchestrator/API/validation modules only when an action runs — keeping the
|
|
128
|
+
* engine import cheap and Docker-free for tests, which replace these anyway.
|
|
129
|
+
*/
|
|
130
|
+
export function createDefaultAgentSetupActions(configManager) {
|
|
131
|
+
/** A client pointed at the local stack's API port (not the saved remote URL). */
|
|
132
|
+
const localApiClient = async (rootDir) => {
|
|
133
|
+
const { getHostConfig } = await import("../../orchestrator/index.js");
|
|
134
|
+
const { cfg } = await getHostConfig({ configManager, root: rootDir });
|
|
135
|
+
const { createApiClient } = await import("../../api/client.js");
|
|
136
|
+
return createApiClient({ baseUrl: `http://localhost:${cfg.apiPort}` });
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
async listAgents(rootDir) {
|
|
140
|
+
const { listAgents } = await import("../../api/agents.js");
|
|
141
|
+
const client = await localApiClient(rootDir);
|
|
142
|
+
const response = await listAgents(client);
|
|
143
|
+
return response.agents;
|
|
144
|
+
},
|
|
145
|
+
async addAgent(rootDir, options) {
|
|
146
|
+
const { addAgent } = await import("../../api/agents.js");
|
|
147
|
+
const client = await localApiClient(rootDir);
|
|
148
|
+
await addAgent(options, client);
|
|
149
|
+
},
|
|
150
|
+
async loginableAgents() {
|
|
151
|
+
const { loginableAgents } = await import("../agentValidation.js");
|
|
152
|
+
return loginableAgents();
|
|
153
|
+
},
|
|
154
|
+
async loginAgent(rootDir, type) {
|
|
155
|
+
const { mkdirSync, mkdtempSync, rmSync } = await import("node:fs");
|
|
156
|
+
const { tmpdir } = await import("node:os");
|
|
157
|
+
const { join } = await import("node:path");
|
|
158
|
+
const { spawnSync } = await import("node:child_process");
|
|
159
|
+
const { getHostConfig } = await import("../../orchestrator/index.js");
|
|
160
|
+
const { planAgentLogin } = await import("../agentValidation.js");
|
|
161
|
+
const { orch, cfg } = await getHostConfig({ configManager, root: rootDir });
|
|
162
|
+
const tmp = mkdtempSync(join(tmpdir(), "propr-setup-login-"));
|
|
163
|
+
const workspaceDir = join(tmp, "workspace");
|
|
164
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
165
|
+
try {
|
|
166
|
+
const { plan, error } = planAgentLogin(type, cfg, workspaceDir, orch.validateDockerBindPath);
|
|
167
|
+
if (error || !plan)
|
|
168
|
+
return { available: false, success: false, detail: error };
|
|
169
|
+
// The image must be present locally; setup pulls selected agent images
|
|
170
|
+
// earlier, but a failed pull would leave it absent.
|
|
171
|
+
if (orch.docker(["images", "-q", plan.image], { capture: true }).stdout.trim().length === 0) {
|
|
172
|
+
return { available: true, success: false, detail: `image ${plan.image} not present locally — run \`propr images pull\`` };
|
|
173
|
+
}
|
|
174
|
+
mkdirSync(plan.hostDir, { recursive: true, mode: 0o700 });
|
|
175
|
+
const res = spawnSync("docker", plan.dockerArgs, { stdio: "inherit" });
|
|
176
|
+
return res.status === 0
|
|
177
|
+
? { available: true, success: true, detail: `${type} login finished — credentials written to ${plan.hostDir}` }
|
|
178
|
+
: { available: true, success: false, detail: `${type} login exited with code ${res.status ?? "?"}` };
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|