@h-rig/cli 0.0.6-alpha.0
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 +30 -0
- package/dist/bin/build-rig-binaries.js +107 -0
- package/dist/bin/rig.js +9330 -0
- package/dist/src/commands/_authority-runs.js +110 -0
- package/dist/src/commands/_connection-state.js +123 -0
- package/dist/src/commands/_doctor-checks.js +501 -0
- package/dist/src/commands/_operator-view.js +322 -0
- package/dist/src/commands/_parsers.js +107 -0
- package/dist/src/commands/_paths.js +50 -0
- package/dist/src/commands/_pi-install.js +184 -0
- package/dist/src/commands/_policy.js +79 -0
- package/dist/src/commands/_preflight.js +460 -0
- package/dist/src/commands/_probes.js +13 -0
- package/dist/src/commands/_run-driver-helpers.js +289 -0
- package/dist/src/commands/_server-client.js +364 -0
- package/dist/src/commands/_snapshot-upload.js +313 -0
- package/dist/src/commands/_task-picker.js +48 -0
- package/dist/src/commands/agent.js +497 -0
- package/dist/src/commands/browser.js +890 -0
- package/dist/src/commands/connect.js +180 -0
- package/dist/src/commands/dist.js +402 -0
- package/dist/src/commands/doctor.js +511 -0
- package/dist/src/commands/github.js +276 -0
- package/dist/src/commands/inbox.js +160 -0
- package/dist/src/commands/init.js +1254 -0
- package/dist/src/commands/inspect.js +174 -0
- package/dist/src/commands/inspector.js +256 -0
- package/dist/src/commands/plugin.js +167 -0
- package/dist/src/commands/profile-and-review.js +178 -0
- package/dist/src/commands/queue.js +197 -0
- package/dist/src/commands/remote.js +507 -0
- package/dist/src/commands/repo-git-harness.js +221 -0
- package/dist/src/commands/run.js +753 -0
- package/dist/src/commands/server.js +368 -0
- package/dist/src/commands/setup.js +681 -0
- package/dist/src/commands/task-report-bug.js +1083 -0
- package/dist/src/commands/task-run-driver.js +1933 -0
- package/dist/src/commands/task.js +1325 -0
- package/dist/src/commands/test.js +39 -0
- package/dist/src/commands/workspace.js +123 -0
- package/dist/src/commands.js +9012 -0
- package/dist/src/index.js +9348 -0
- package/dist/src/launcher.js +131 -0
- package/dist/src/report-bug.js +260 -0
- package/dist/src/runner.js +272 -0
- package/dist/src/withMutedConsole.js +42 -0
- package/package.json +31 -0
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/cli/src/commands/init.ts
|
|
5
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
6
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
7
|
+
import { resolve as resolve5 } from "path";
|
|
8
|
+
|
|
9
|
+
// packages/cli/src/runner.ts
|
|
10
|
+
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
11
|
+
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
12
|
+
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
13
|
+
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
14
|
+
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
15
|
+
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
16
|
+
import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
|
|
17
|
+
function takeFlag(args, flag) {
|
|
18
|
+
const rest = [];
|
|
19
|
+
let value = false;
|
|
20
|
+
for (const arg of args) {
|
|
21
|
+
if (arg === flag) {
|
|
22
|
+
value = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
rest.push(arg);
|
|
26
|
+
}
|
|
27
|
+
return { value, rest };
|
|
28
|
+
}
|
|
29
|
+
function takeOption(args, option) {
|
|
30
|
+
const rest = [];
|
|
31
|
+
let value;
|
|
32
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
33
|
+
const current = args[index];
|
|
34
|
+
if (current === option) {
|
|
35
|
+
const next = args[index + 1];
|
|
36
|
+
if (!next || next.startsWith("-")) {
|
|
37
|
+
throw new CliError(`Missing value for ${option}`);
|
|
38
|
+
}
|
|
39
|
+
value = next;
|
|
40
|
+
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (current !== undefined) {
|
|
44
|
+
rest.push(current);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { value, rest };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// packages/cli/src/commands/init.ts
|
|
51
|
+
import { buildRigInitConfigSource } from "@rig/core";
|
|
52
|
+
|
|
53
|
+
// packages/cli/src/commands/_connection-state.ts
|
|
54
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
55
|
+
import { homedir } from "os";
|
|
56
|
+
import { dirname, resolve } from "path";
|
|
57
|
+
function resolveGlobalConnectionsPath(env = process.env) {
|
|
58
|
+
const explicit = env.RIG_CONNECTIONS_FILE?.trim();
|
|
59
|
+
if (explicit)
|
|
60
|
+
return resolve(explicit);
|
|
61
|
+
const stateDir = env.RIG_GLOBAL_STATE_DIR?.trim();
|
|
62
|
+
if (stateDir)
|
|
63
|
+
return resolve(stateDir, "connections.json");
|
|
64
|
+
return resolve(homedir(), ".rig", "connections.json");
|
|
65
|
+
}
|
|
66
|
+
function resolveRepoConnectionPath(projectRoot) {
|
|
67
|
+
return resolve(projectRoot, ".rig", "state", "connection.json");
|
|
68
|
+
}
|
|
69
|
+
function readJsonFile(path) {
|
|
70
|
+
if (!existsSync(path))
|
|
71
|
+
return null;
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function writeJsonFile(path, value) {
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
80
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
81
|
+
`, "utf8");
|
|
82
|
+
}
|
|
83
|
+
function normalizeConnection(value) {
|
|
84
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
85
|
+
return null;
|
|
86
|
+
const record = value;
|
|
87
|
+
if (record.kind === "local")
|
|
88
|
+
return { kind: "local", mode: "auto" };
|
|
89
|
+
if (record.kind === "remote" && typeof record.baseUrl === "string" && record.baseUrl.trim()) {
|
|
90
|
+
const baseUrl = record.baseUrl.trim().replace(/\/+$/, "");
|
|
91
|
+
return { kind: "remote", baseUrl };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function readGlobalConnections(options = {}) {
|
|
96
|
+
const path = resolveGlobalConnectionsPath(options.env ?? process.env);
|
|
97
|
+
const payload = readJsonFile(path);
|
|
98
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
99
|
+
return { connections: {} };
|
|
100
|
+
}
|
|
101
|
+
const rawConnections = payload.connections;
|
|
102
|
+
const connections = {};
|
|
103
|
+
if (rawConnections && typeof rawConnections === "object" && !Array.isArray(rawConnections)) {
|
|
104
|
+
for (const [alias, raw] of Object.entries(rawConnections)) {
|
|
105
|
+
const connection = normalizeConnection(raw);
|
|
106
|
+
if (connection)
|
|
107
|
+
connections[alias] = connection;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { connections };
|
|
111
|
+
}
|
|
112
|
+
function writeGlobalConnections(state, options = {}) {
|
|
113
|
+
writeJsonFile(resolveGlobalConnectionsPath(options.env ?? process.env), state);
|
|
114
|
+
}
|
|
115
|
+
function upsertGlobalConnection(alias, connection, options = {}) {
|
|
116
|
+
const cleanAlias = alias.trim();
|
|
117
|
+
if (!cleanAlias)
|
|
118
|
+
throw new CliError2("Connection alias is required.", 1);
|
|
119
|
+
const state = readGlobalConnections(options);
|
|
120
|
+
state.connections[cleanAlias] = connection;
|
|
121
|
+
writeGlobalConnections(state, options);
|
|
122
|
+
return state;
|
|
123
|
+
}
|
|
124
|
+
function readRepoConnection(projectRoot) {
|
|
125
|
+
const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
|
|
126
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
127
|
+
return null;
|
|
128
|
+
const record = payload;
|
|
129
|
+
const selected = typeof record.selected === "string" ? record.selected.trim() : "";
|
|
130
|
+
if (!selected)
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
selected,
|
|
134
|
+
project: typeof record.project === "string" ? record.project : undefined,
|
|
135
|
+
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function writeRepoConnection(projectRoot, state) {
|
|
139
|
+
writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
|
|
140
|
+
}
|
|
141
|
+
function resolveSelectedConnection(projectRoot, options = {}) {
|
|
142
|
+
const repo = readRepoConnection(projectRoot);
|
|
143
|
+
if (!repo)
|
|
144
|
+
return null;
|
|
145
|
+
if (repo.selected === "local")
|
|
146
|
+
return { alias: "local", connection: { kind: "local", mode: "auto" } };
|
|
147
|
+
const global = readGlobalConnections(options);
|
|
148
|
+
const connection = global.connections[repo.selected];
|
|
149
|
+
if (!connection) {
|
|
150
|
+
throw new CliError2(`Selected Rig connection "${repo.selected}" was not found. Run \`rig connect list\` or \`rig connect use local\`.`, 1);
|
|
151
|
+
}
|
|
152
|
+
return { alias: repo.selected, connection };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// packages/cli/src/commands/_server-client.ts
|
|
156
|
+
import { spawnSync } from "child_process";
|
|
157
|
+
import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
|
|
158
|
+
var cachedGitHubBearerToken;
|
|
159
|
+
function cleanToken(value) {
|
|
160
|
+
const trimmed = value?.trim();
|
|
161
|
+
return trimmed ? trimmed : null;
|
|
162
|
+
}
|
|
163
|
+
function setGitHubBearerTokenForCurrentProcess(token) {
|
|
164
|
+
cachedGitHubBearerToken = cleanToken(token ?? undefined);
|
|
165
|
+
}
|
|
166
|
+
function readGitHubBearerTokenForRemote() {
|
|
167
|
+
if (cachedGitHubBearerToken !== undefined)
|
|
168
|
+
return cachedGitHubBearerToken;
|
|
169
|
+
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
170
|
+
if (envToken) {
|
|
171
|
+
cachedGitHubBearerToken = envToken;
|
|
172
|
+
return cachedGitHubBearerToken;
|
|
173
|
+
}
|
|
174
|
+
const result = spawnSync("gh", ["auth", "token"], {
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
timeout: 5000,
|
|
177
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
178
|
+
});
|
|
179
|
+
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
180
|
+
return cachedGitHubBearerToken;
|
|
181
|
+
}
|
|
182
|
+
async function ensureServerForCli(projectRoot) {
|
|
183
|
+
try {
|
|
184
|
+
const selected = resolveSelectedConnection(projectRoot);
|
|
185
|
+
if (selected?.connection.kind === "remote") {
|
|
186
|
+
return {
|
|
187
|
+
baseUrl: selected.connection.baseUrl,
|
|
188
|
+
authToken: readGitHubBearerTokenForRemote(),
|
|
189
|
+
connectionKind: "remote"
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const connection = await ensureLocalRigServerConnection(projectRoot);
|
|
193
|
+
return {
|
|
194
|
+
baseUrl: connection.baseUrl,
|
|
195
|
+
authToken: connection.authToken,
|
|
196
|
+
connectionKind: "local"
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error instanceof Error) {
|
|
200
|
+
throw new CliError2(error.message, 1);
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function mergeHeaders(headers, authToken) {
|
|
206
|
+
const merged = new Headers(headers);
|
|
207
|
+
if (authToken) {
|
|
208
|
+
merged.set("authorization", `Bearer ${authToken}`);
|
|
209
|
+
}
|
|
210
|
+
return merged;
|
|
211
|
+
}
|
|
212
|
+
function diagnosticMessage(payload) {
|
|
213
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
214
|
+
return null;
|
|
215
|
+
const record = payload;
|
|
216
|
+
const diagnostics = Array.isArray(record.diagnostics) ? record.diagnostics : [];
|
|
217
|
+
const messages = diagnostics.flatMap((entry) => {
|
|
218
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
219
|
+
return [];
|
|
220
|
+
const diagnostic = entry;
|
|
221
|
+
const kind = typeof diagnostic.kind === "string" ? diagnostic.kind : "task-source";
|
|
222
|
+
const message = typeof diagnostic.message === "string" ? diagnostic.message : null;
|
|
223
|
+
return message ? [`${kind}: ${message}`] : [];
|
|
224
|
+
});
|
|
225
|
+
return messages.length > 0 ? messages.join("; ") : null;
|
|
226
|
+
}
|
|
227
|
+
async function requestServerJson(context, pathname, init = {}) {
|
|
228
|
+
const server = await ensureServerForCli(context.projectRoot);
|
|
229
|
+
const response = await fetch(`${server.baseUrl}${pathname}`, {
|
|
230
|
+
...init,
|
|
231
|
+
headers: mergeHeaders(init.headers, server.authToken)
|
|
232
|
+
});
|
|
233
|
+
const text = await response.text();
|
|
234
|
+
const payload = text.trim().length > 0 ? (() => {
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(text);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
})() : null;
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
const diagnostics = diagnosticMessage(payload);
|
|
243
|
+
const detail = diagnostics ?? (text || response.statusText);
|
|
244
|
+
throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
|
|
245
|
+
}
|
|
246
|
+
return payload;
|
|
247
|
+
}
|
|
248
|
+
async function postGitHubTokenViaServer(context, token, options = {}) {
|
|
249
|
+
const payload = await requestServerJson(context, "/api/github/auth/token", {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { "content-type": "application/json" },
|
|
252
|
+
body: JSON.stringify({ token, selectedRepo: options.selectedRepo })
|
|
253
|
+
});
|
|
254
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
255
|
+
}
|
|
256
|
+
async function prepareRemoteCheckoutViaServer(context, input) {
|
|
257
|
+
const payload = await requestServerJson(context, `/api/projects/${encodeURIComponent(input.repoSlug)}/prepare-checkout`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: { "content-type": "application/json" },
|
|
260
|
+
body: JSON.stringify({ checkout: input.checkout, repoUrl: input.repoUrl, baseDir: input.baseDir })
|
|
261
|
+
});
|
|
262
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
263
|
+
}
|
|
264
|
+
async function registerProjectViaServer(context, input) {
|
|
265
|
+
const payload = await requestServerJson(context, "/api/projects", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: { "content-type": "application/json" },
|
|
268
|
+
body: JSON.stringify(input)
|
|
269
|
+
});
|
|
270
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
271
|
+
}
|
|
272
|
+
function sleep(ms) {
|
|
273
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
274
|
+
}
|
|
275
|
+
async function switchServerProjectRootViaServer(context, projectRoot, options = {}) {
|
|
276
|
+
const switched = await requestServerJson(context, "/api/server/project-root", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: { "content-type": "application/json" },
|
|
279
|
+
body: JSON.stringify({ projectRoot })
|
|
280
|
+
});
|
|
281
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
282
|
+
const pollMs = options.pollMs ?? 1000;
|
|
283
|
+
const deadline = Date.now() + timeoutMs;
|
|
284
|
+
let lastError;
|
|
285
|
+
while (Date.now() < deadline) {
|
|
286
|
+
try {
|
|
287
|
+
const status = await requestServerJson(context, "/api/server/status");
|
|
288
|
+
if (status && typeof status === "object" && !Array.isArray(status)) {
|
|
289
|
+
const record = status;
|
|
290
|
+
if (record.projectRoot === projectRoot) {
|
|
291
|
+
return { ok: true, switched, status: record };
|
|
292
|
+
}
|
|
293
|
+
lastError = `server projectRoot=${String(record.projectRoot ?? "unknown")}`;
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
lastError = error;
|
|
297
|
+
}
|
|
298
|
+
await sleep(pollMs);
|
|
299
|
+
}
|
|
300
|
+
throw new CliError2(`Rig server did not switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no status")}).`, 1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// packages/cli/src/commands/_pi-install.ts
|
|
304
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
|
|
305
|
+
import { homedir as homedir2 } from "os";
|
|
306
|
+
import { resolve as resolve2 } from "path";
|
|
307
|
+
var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
308
|
+
var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
|
|
309
|
+
export { default } from '@rig/pi-rig';
|
|
310
|
+
`;
|
|
311
|
+
async function defaultCommandRunner(command, options = {}) {
|
|
312
|
+
const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
|
|
313
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
314
|
+
new Response(proc.stdout).text(),
|
|
315
|
+
new Response(proc.stderr).text(),
|
|
316
|
+
proc.exited
|
|
317
|
+
]);
|
|
318
|
+
return { exitCode, stdout, stderr };
|
|
319
|
+
}
|
|
320
|
+
function resolvePiRigExtensionPath(homeDir) {
|
|
321
|
+
return resolve2(homeDir, ".pi", "agent", "extensions", "pi-rig");
|
|
322
|
+
}
|
|
323
|
+
function resolvePiRigPackageSource(projectRoot, exists = existsSync2) {
|
|
324
|
+
const localPackage = resolve2(projectRoot, "packages", "pi-rig");
|
|
325
|
+
if (exists(resolve2(localPackage, "package.json")))
|
|
326
|
+
return localPackage;
|
|
327
|
+
return `npm:${PI_RIG_PACKAGE_NAME}`;
|
|
328
|
+
}
|
|
329
|
+
function resolvePiHomeDir(inputHomeDir) {
|
|
330
|
+
return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir2();
|
|
331
|
+
}
|
|
332
|
+
function piListContainsPiRig(output) {
|
|
333
|
+
return output.split(/\r?\n/).some((line) => {
|
|
334
|
+
const normalized = line.trim();
|
|
335
|
+
return normalized.includes(PI_RIG_PACKAGE_NAME) || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(normalized);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
async function safeRun(runner, command, options) {
|
|
339
|
+
try {
|
|
340
|
+
return await runner(command, options);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return { exitCode: 1, stdout: "", stderr: error instanceof Error ? error.message : String(error) };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function splitInstallCommand(value) {
|
|
346
|
+
return value.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
347
|
+
}
|
|
348
|
+
async function ensurePiBinaryAvailable(input) {
|
|
349
|
+
const current = await safeRun(input.runner, ["pi", "--version"]);
|
|
350
|
+
if (current.exitCode === 0) {
|
|
351
|
+
const updateCommand = process.env.RIG_PI_UPDATE_COMMAND?.trim();
|
|
352
|
+
if (updateCommand) {
|
|
353
|
+
const parts2 = splitInstallCommand(updateCommand);
|
|
354
|
+
if (parts2.length > 0)
|
|
355
|
+
await safeRun(input.runner, parts2, input.projectRoot ? { cwd: input.projectRoot } : undefined);
|
|
356
|
+
}
|
|
357
|
+
return { ok: true, detail: (current.stdout || current.stderr).trim() || undefined };
|
|
358
|
+
}
|
|
359
|
+
const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim() || "bunx @earendil-works/pi@latest install";
|
|
360
|
+
const parts = splitInstallCommand(installCommand);
|
|
361
|
+
if (parts.length === 0) {
|
|
362
|
+
return { ok: false, error: (current.stderr || current.stdout).trim() || "pi --version failed" };
|
|
363
|
+
}
|
|
364
|
+
const install = await safeRun(input.runner, parts, input.projectRoot ? { cwd: input.projectRoot } : undefined);
|
|
365
|
+
if (install.exitCode !== 0) {
|
|
366
|
+
return { ok: false, installedOrUpdated: true, error: (install.stderr || install.stdout).trim() || `Pi install command failed (${install.exitCode})` };
|
|
367
|
+
}
|
|
368
|
+
const next = await safeRun(input.runner, ["pi", "--version"]);
|
|
369
|
+
return {
|
|
370
|
+
ok: next.exitCode === 0,
|
|
371
|
+
installedOrUpdated: true,
|
|
372
|
+
detail: (next.stdout || next.stderr).trim() || undefined,
|
|
373
|
+
...next.exitCode === 0 ? {} : { error: (next.stderr || next.stdout).trim() || "pi --version failed after install" }
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function removeManagedLegacyPiRigBridge(homeDir, exists = existsSync2) {
|
|
377
|
+
const extensionPath = resolvePiRigExtensionPath(homeDir);
|
|
378
|
+
const indexPath = resolve2(extensionPath, "index.ts");
|
|
379
|
+
if (!exists(indexPath))
|
|
380
|
+
return;
|
|
381
|
+
try {
|
|
382
|
+
const content = readFileSync2(indexPath, "utf8");
|
|
383
|
+
if (content === LEGACY_PI_RIG_MARKER || content.includes("Managed by Rig. Source package: @rig/pi-rig")) {
|
|
384
|
+
rmSync(extensionPath, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
async function checkPiRigInstall(input = {}) {
|
|
389
|
+
const home = resolvePiHomeDir(input.homeDir);
|
|
390
|
+
const extensionPath = resolvePiRigExtensionPath(home);
|
|
391
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
392
|
+
return {
|
|
393
|
+
extensionPath,
|
|
394
|
+
pi: { ok: true, label: "pi", detail: "fake-pi" },
|
|
395
|
+
piRig: { ok: true, label: "pi-rig global extension", detail: extensionPath }
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const exists = input.exists ?? existsSync2;
|
|
399
|
+
const runner = input.commandRunner ?? defaultCommandRunner;
|
|
400
|
+
const piResult = await safeRun(runner, ["pi", "--version"]);
|
|
401
|
+
const piListResult = piResult.exitCode === 0 ? await safeRun(runner, ["pi", "list"]) : { exitCode: 1, stdout: "", stderr: "" };
|
|
402
|
+
const listedPiRig = piListResult.exitCode === 0 && piListContainsPiRig(`${piListResult.stdout}
|
|
403
|
+
${piListResult.stderr}`);
|
|
404
|
+
const legacyBridge = exists(resolve2(extensionPath, "index.ts"));
|
|
405
|
+
const hasPiRig = listedPiRig;
|
|
406
|
+
return {
|
|
407
|
+
extensionPath,
|
|
408
|
+
pi: {
|
|
409
|
+
ok: piResult.exitCode === 0,
|
|
410
|
+
label: "pi",
|
|
411
|
+
detail: (piResult.stdout || piResult.stderr).trim() || undefined,
|
|
412
|
+
hint: piResult.exitCode === 0 ? undefined : "Install Pi or run `rig init --yes` to install/update the Pi runtime."
|
|
413
|
+
},
|
|
414
|
+
piRig: {
|
|
415
|
+
ok: hasPiRig,
|
|
416
|
+
label: "pi-rig global extension",
|
|
417
|
+
detail: hasPiRig ? piListResult.stdout.trim() || PI_RIG_PACKAGE_NAME : legacyBridge ? `${extensionPath} (legacy bridge; reinstall required)` : undefined,
|
|
418
|
+
hint: hasPiRig ? undefined : "Run `rig init --yes` to install/enable the global pi-rig package with `pi install`."
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
async function ensurePiRigInstalled(input) {
|
|
423
|
+
const home = resolvePiHomeDir(input.homeDir);
|
|
424
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
425
|
+
const status2 = await checkPiRigInstall({ homeDir: home, commandRunner: input.commandRunner });
|
|
426
|
+
return { ...status2, installedPath: status2.extensionPath };
|
|
427
|
+
}
|
|
428
|
+
const runner = input.commandRunner ?? defaultCommandRunner;
|
|
429
|
+
const piAvailable = await ensurePiBinaryAvailable({ runner, projectRoot: input.projectRoot });
|
|
430
|
+
const status = piAvailable.ok ? await checkPiRigInstall({ homeDir: home, commandRunner: runner }) : {
|
|
431
|
+
extensionPath: resolvePiRigExtensionPath(home),
|
|
432
|
+
pi: { ok: false, label: "pi", detail: piAvailable.error, hint: "Install/update Pi with RIG_PI_INSTALL_COMMAND or install Pi manually." },
|
|
433
|
+
piRig: { ok: false, label: "pi-rig global extension", hint: "Pi is required before pi-rig can be installed." }
|
|
434
|
+
};
|
|
435
|
+
if (!piAvailable.ok) {
|
|
436
|
+
throw new Error(`Pi install/update failed: ${piAvailable.error ?? "pi unavailable"}`);
|
|
437
|
+
}
|
|
438
|
+
const packageSource = resolvePiRigPackageSource(input.projectRoot);
|
|
439
|
+
removeManagedLegacyPiRigBridge(home);
|
|
440
|
+
const install = await runner(["pi", "install", packageSource], { cwd: input.projectRoot });
|
|
441
|
+
if (install.exitCode !== 0) {
|
|
442
|
+
throw new Error(`pi-rig install failed: ${(install.stderr || install.stdout).trim() || `exit ${install.exitCode}`}`);
|
|
443
|
+
}
|
|
444
|
+
const next = await checkPiRigInstall({ homeDir: home, commandRunner: runner });
|
|
445
|
+
return { ...next, installedPath: packageSource };
|
|
446
|
+
}
|
|
447
|
+
async function ensureRemotePiRigInstalled(input) {
|
|
448
|
+
const payload = await input.requestJson("/api/pi-rig/install", {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: { "content-type": "application/json" },
|
|
451
|
+
body: JSON.stringify({ package: "@rig/pi-rig", scope: "global" })
|
|
452
|
+
});
|
|
453
|
+
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
454
|
+
const piOk = record.piOk === true || record.ok === true;
|
|
455
|
+
const piRigOk = record.piRigOk === true || record.installed === true || record.ok === true;
|
|
456
|
+
const extensionPath = typeof record.extensionPath === "string" ? record.extensionPath : "remote:~/.pi/agent/extensions/pi-rig";
|
|
457
|
+
return {
|
|
458
|
+
remote: true,
|
|
459
|
+
extensionPath,
|
|
460
|
+
pi: {
|
|
461
|
+
ok: piOk,
|
|
462
|
+
label: "pi",
|
|
463
|
+
detail: typeof record.piVersion === "string" ? record.piVersion : undefined,
|
|
464
|
+
hint: piOk ? undefined : "Install/update Pi on the selected remote Rig server."
|
|
465
|
+
},
|
|
466
|
+
piRig: {
|
|
467
|
+
ok: piRigOk,
|
|
468
|
+
label: "pi-rig global extension",
|
|
469
|
+
detail: extensionPath,
|
|
470
|
+
hint: piRigOk ? undefined : "Install/enable pi-rig on the selected remote Rig server."
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
async function buildPiSetupChecks(input = {}) {
|
|
475
|
+
const status = await checkPiRigInstall(input);
|
|
476
|
+
return [status.pi, status.piRig];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// packages/cli/src/commands/_snapshot-upload.ts
|
|
480
|
+
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
481
|
+
import { dirname as dirname2, resolve as resolve3, relative, sep } from "path";
|
|
482
|
+
var SNAPSHOT_ARCHIVE_VERSION = 1;
|
|
483
|
+
var SNAPSHOT_ARCHIVE_CONTENT_TYPE = "application/vnd.rig.snapshot+json";
|
|
484
|
+
var DEFAULT_EXCLUDED_DIRECTORIES = new Set([
|
|
485
|
+
".git",
|
|
486
|
+
".rig",
|
|
487
|
+
"node_modules",
|
|
488
|
+
".turbo",
|
|
489
|
+
".next",
|
|
490
|
+
".cache",
|
|
491
|
+
"coverage",
|
|
492
|
+
"dist",
|
|
493
|
+
"build",
|
|
494
|
+
"out"
|
|
495
|
+
]);
|
|
496
|
+
function toPosixPath(path) {
|
|
497
|
+
return path.split(sep).join("/");
|
|
498
|
+
}
|
|
499
|
+
function assertManifestPath(root, relativePath) {
|
|
500
|
+
if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\x00")) {
|
|
501
|
+
throw new Error(`Invalid snapshot path: ${relativePath}`);
|
|
502
|
+
}
|
|
503
|
+
const resolved = resolve3(root, relativePath);
|
|
504
|
+
const relativeToRoot = relative(root, resolved);
|
|
505
|
+
if (relativeToRoot.startsWith("..") || relativeToRoot === ".." || resolve3(relativeToRoot) === resolved) {
|
|
506
|
+
throw new Error(`Snapshot path escapes project root: ${relativePath}`);
|
|
507
|
+
}
|
|
508
|
+
return resolved;
|
|
509
|
+
}
|
|
510
|
+
async function buildSnapshotUploadManifest(projectRoot, options = {}) {
|
|
511
|
+
const root = resolve3(projectRoot);
|
|
512
|
+
const excludedDirectories = [...new Set([
|
|
513
|
+
...DEFAULT_EXCLUDED_DIRECTORIES,
|
|
514
|
+
...options.excludedDirectories ?? []
|
|
515
|
+
])];
|
|
516
|
+
const excludedSet = new Set(excludedDirectories);
|
|
517
|
+
const files = [];
|
|
518
|
+
async function visit(dir) {
|
|
519
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
520
|
+
for (const entry of entries) {
|
|
521
|
+
if (entry.isDirectory() && excludedSet.has(entry.name))
|
|
522
|
+
continue;
|
|
523
|
+
const fullPath = resolve3(dir, entry.name);
|
|
524
|
+
if (entry.isDirectory()) {
|
|
525
|
+
await visit(fullPath);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (!entry.isFile())
|
|
529
|
+
continue;
|
|
530
|
+
files.push(toPosixPath(relative(root, fullPath)));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
await visit(root);
|
|
534
|
+
files.sort();
|
|
535
|
+
return { root, files, excludedDirectories };
|
|
536
|
+
}
|
|
537
|
+
async function createSnapshotUploadArchive(projectRoot, options = {}) {
|
|
538
|
+
const manifest = await buildSnapshotUploadManifest(projectRoot, options);
|
|
539
|
+
const files = await Promise.all(manifest.files.map(async (path) => {
|
|
540
|
+
const fullPath = assertManifestPath(manifest.root, path);
|
|
541
|
+
return {
|
|
542
|
+
path,
|
|
543
|
+
contentBase64: (await readFile(fullPath)).toString("base64")
|
|
544
|
+
};
|
|
545
|
+
}));
|
|
546
|
+
return {
|
|
547
|
+
version: SNAPSHOT_ARCHIVE_VERSION,
|
|
548
|
+
root: manifest.root,
|
|
549
|
+
files,
|
|
550
|
+
excludedDirectories: manifest.excludedDirectories,
|
|
551
|
+
createdAt: (options.now?.() ?? new Date).toISOString()
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function encodeSnapshotUploadArchive(archive) {
|
|
555
|
+
return Buffer.from(JSON.stringify(archive), "utf8").toString("base64");
|
|
556
|
+
}
|
|
557
|
+
async function uploadSnapshotArchiveViaServer(context, input) {
|
|
558
|
+
const payload = await requestServerJson(context, `/api/projects/${encodeURIComponent(input.repoSlug)}/upload-snapshot`, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers: { "content-type": "application/json" },
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
archiveContentBase64: encodeSnapshotUploadArchive(input.archive),
|
|
563
|
+
contentType: SNAPSHOT_ARCHIVE_CONTENT_TYPE,
|
|
564
|
+
baseDir: input.baseDir
|
|
565
|
+
})
|
|
566
|
+
});
|
|
567
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// packages/cli/src/commands/_doctor-checks.ts
|
|
571
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
572
|
+
import { resolve as resolve4 } from "path";
|
|
573
|
+
import { isSupportedBunVersion, MIN_SUPPORTED_BUN_VERSION } from "@rig/runtime/control-plane/setup-version";
|
|
574
|
+
|
|
575
|
+
// packages/cli/src/commands/_parsers.ts
|
|
576
|
+
async function loadRigConfigOrNull(projectRoot) {
|
|
577
|
+
try {
|
|
578
|
+
const { loadConfig } = await import("@rig/core/load-config");
|
|
579
|
+
return await loadConfig(projectRoot);
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// packages/cli/src/commands/_doctor-checks.ts
|
|
586
|
+
function check(id, label, status, detail, remediation) {
|
|
587
|
+
return {
|
|
588
|
+
id,
|
|
589
|
+
label,
|
|
590
|
+
status,
|
|
591
|
+
...detail ? { detail } : {},
|
|
592
|
+
...remediation ? { remediation } : {}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function errorMessage(error) {
|
|
596
|
+
return error instanceof Error ? error.message : String(error);
|
|
597
|
+
}
|
|
598
|
+
function isAuthenticated(payload) {
|
|
599
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
600
|
+
return false;
|
|
601
|
+
const record = payload;
|
|
602
|
+
return record.signedIn === true || record.authenticated === true || record.status === "authenticated" || record.ok === true && typeof record.login === "string" && record.login.trim().length > 0;
|
|
603
|
+
}
|
|
604
|
+
function repoSlugFromConfig(config) {
|
|
605
|
+
const project = config?.project;
|
|
606
|
+
if (project && typeof project === "object" && !Array.isArray(project)) {
|
|
607
|
+
const record = project;
|
|
608
|
+
if (typeof record.repo === "string" && /^([^/\s]+)\/([^/\s]+)$/.test(record.repo))
|
|
609
|
+
return record.repo;
|
|
610
|
+
if (typeof record.name === "string" && /^([^/\s]+)\/([^/\s]+)$/.test(record.name))
|
|
611
|
+
return record.name;
|
|
612
|
+
}
|
|
613
|
+
const taskSource = config?.taskSource;
|
|
614
|
+
if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
|
|
615
|
+
const source = taskSource;
|
|
616
|
+
if (typeof source.owner === "string" && typeof source.repo === "string")
|
|
617
|
+
return `${source.owner}/${source.repo}`;
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
function loadFallbackConfig(projectRoot) {
|
|
622
|
+
const candidates = ["rig.config.ts", "rig.config.mts", "rig.config.json"];
|
|
623
|
+
for (const name of candidates) {
|
|
624
|
+
const path = resolve4(projectRoot, name);
|
|
625
|
+
if (!existsSync3(path))
|
|
626
|
+
continue;
|
|
627
|
+
try {
|
|
628
|
+
const source = readFileSync3(path, "utf8");
|
|
629
|
+
if (name.endsWith(".json"))
|
|
630
|
+
return JSON.parse(source);
|
|
631
|
+
const owner = source.match(/owner\s*:\s*["']([^"']+)["']/)?.[1];
|
|
632
|
+
const repo = source.match(/repo\s*:\s*["']([^"']+)["']/)?.[1];
|
|
633
|
+
const projectRepo = source.match(/project\s*:\s*\{[^}]*repo\s*:\s*["']([^"']+)["']/s)?.[1] ?? (owner && repo ? `${owner}/${repo}` : undefined);
|
|
634
|
+
const taskKind = source.match(/taskSource\s*:\s*\{[^}]*kind\s*:\s*["']([^"']+)["']/s)?.[1];
|
|
635
|
+
if (projectRepo || taskKind) {
|
|
636
|
+
return {
|
|
637
|
+
...projectRepo ? { project: { name: projectRepo, repo: projectRepo } } : {},
|
|
638
|
+
...taskKind ? { taskSource: { kind: taskKind, ...owner ? { owner } : {}, ...repo ? { repo } : {} } } : {}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
function projectStatusSlug(projectRoot, config) {
|
|
648
|
+
return readRepoConnection(projectRoot)?.project ?? repoSlugFromConfig(config);
|
|
649
|
+
}
|
|
650
|
+
function githubProjectsCheck(config) {
|
|
651
|
+
const github = config?.github;
|
|
652
|
+
const projects = github?.projects;
|
|
653
|
+
if (!projects?.enabled) {
|
|
654
|
+
return check("github-projects", "GitHub Projects status sync", "warn", "disabled or not configured", "Run `rig init --github-project <project>` or configure github.projects when Project status sync should be authoritative.");
|
|
655
|
+
}
|
|
656
|
+
if (projects.projectId && projects.statusFieldId) {
|
|
657
|
+
return check("github-projects", "GitHub Projects status sync", "pass", `project ${projects.projectId}`);
|
|
658
|
+
}
|
|
659
|
+
return check("github-projects", "GitHub Projects status sync", "fail", "enabled but projectId/statusFieldId is incomplete", "Configure github.projects.projectId and github.projects.statusFieldId, or disable github.projects.enabled.");
|
|
660
|
+
}
|
|
661
|
+
function permissionAllowsPr(payload) {
|
|
662
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
663
|
+
return null;
|
|
664
|
+
const record = payload;
|
|
665
|
+
if (record.canOpenPullRequest === true || record.pullRequests === true || record.push === true || record.maintain === true || record.admin === true)
|
|
666
|
+
return true;
|
|
667
|
+
if (record.canOpenPullRequest === false || record.pullRequests === false || record.push === false)
|
|
668
|
+
return false;
|
|
669
|
+
const permissions = record.permissions;
|
|
670
|
+
if (permissions && typeof permissions === "object" && !Array.isArray(permissions)) {
|
|
671
|
+
const p = permissions;
|
|
672
|
+
if (p.push === true || p.maintain === true || p.admin === true)
|
|
673
|
+
return true;
|
|
674
|
+
if (p.push === false && p.maintain !== true && p.admin !== true)
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
function labelsReady(payload) {
|
|
680
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
681
|
+
return null;
|
|
682
|
+
const record = payload;
|
|
683
|
+
if (record.ok === true || record.ready === true || record.labelsReady === true)
|
|
684
|
+
return true;
|
|
685
|
+
if (record.ok === false || record.ready === false || record.labelsReady === false)
|
|
686
|
+
return false;
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
function prMergeCheck(config) {
|
|
690
|
+
const pr = config?.pr;
|
|
691
|
+
const merge = config?.merge;
|
|
692
|
+
if (pr?.mode === "off" || merge?.mode === "off") {
|
|
693
|
+
return check("pr-merge", "PR/merge automation", "warn", "automatic PR or merge is disabled", "Set pr.mode and merge.mode to auto for autonomous YOLO runs.");
|
|
694
|
+
}
|
|
695
|
+
return check("pr-merge", "PR/merge automation", "pass", `pr=${pr?.mode ?? "auto"}, merge=${merge?.mode ?? "auto"}, method=${merge?.method ?? "repo-default"}`);
|
|
696
|
+
}
|
|
697
|
+
async function runRigDoctorChecks(options) {
|
|
698
|
+
const projectRoot = options.projectRoot;
|
|
699
|
+
const checks = [];
|
|
700
|
+
const which = options.which ?? ((binary) => Bun.which(binary));
|
|
701
|
+
const bunVersion = options.bunVersion ?? Bun.version;
|
|
702
|
+
const request = options.requestJson ?? ((pathname, init) => requestServerJson({ projectRoot }, pathname, init));
|
|
703
|
+
const loadConfig = options.loadConfig ?? loadRigConfigOrNull;
|
|
704
|
+
checks.push(check("bun", `bun >= ${MIN_SUPPORTED_BUN_VERSION}`, isSupportedBunVersion(bunVersion) ? "pass" : "fail", `found ${bunVersion}`, `Install Bun ${MIN_SUPPORTED_BUN_VERSION} or newer.`), check("git", "git", which("git") ? "pass" : "fail", which("git") ?? undefined, "Install git and ensure it is on PATH."), check("jq", "jq", which("jq") ? "pass" : "warn", which("jq") ?? undefined, "Install jq (for example `brew install jq`)."));
|
|
705
|
+
const loadedConfig = await loadConfig(projectRoot).catch(() => null);
|
|
706
|
+
const config = loadedConfig ?? loadFallbackConfig(projectRoot);
|
|
707
|
+
const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync3(resolve4(projectRoot, name)));
|
|
708
|
+
checks.push(config ? check("config", "rig.config loadable", "pass") : check("config", "rig.config loadable", hasConfigFile ? "fail" : "fail", hasConfigFile ? "config file exists but failed to load" : "missing rig.config.ts/json", "Run `rig init` or fix the config error."));
|
|
709
|
+
const taskSourceKind = config?.taskSource?.kind;
|
|
710
|
+
checks.push(taskSourceKind ? check("task-source", "task source configured", "pass", taskSourceKind) : check("task-source", "task source configured", "fail", "missing taskSource", "Configure taskSource in rig.config.ts."));
|
|
711
|
+
const repo = readRepoConnection(projectRoot);
|
|
712
|
+
checks.push(repo ? check("project-link", "repo selected Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to link this checkout to a GitHub repo slug.") : check("project-link", "repo selected Rig connection", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
|
|
713
|
+
const selected = (() => {
|
|
714
|
+
try {
|
|
715
|
+
return resolveSelectedConnection(projectRoot);
|
|
716
|
+
} catch {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
})();
|
|
720
|
+
checks.push(selected ? check("connection", "selected server connection", "pass", selected.connection.kind === "remote" ? selected.connection.baseUrl : "local auto") : check("connection", "selected server connection", repo ? "fail" : "warn", repo ? "selected alias is missing" : "will auto-start local server", repo ? "Run `rig connect list` and `rig connect use <alias|local>`." : undefined));
|
|
721
|
+
let server = null;
|
|
722
|
+
try {
|
|
723
|
+
server = await (options.resolveServer ?? ensureServerForCli)(projectRoot);
|
|
724
|
+
checks.push(check("server", "Rig server reachable", "pass", `${server.connectionKind} ${server.baseUrl}`));
|
|
725
|
+
} catch (error) {
|
|
726
|
+
checks.push(check("server", "Rig server reachable", "fail", errorMessage(error), "Start the local Rig server or fix the selected remote connection."));
|
|
727
|
+
}
|
|
728
|
+
if (server || options.requestJson) {
|
|
729
|
+
try {
|
|
730
|
+
const status = await request("/api/server/status");
|
|
731
|
+
checks.push(check("server-status", "server project status", "pass", JSON.stringify(status).slice(0, 180)));
|
|
732
|
+
} catch (error) {
|
|
733
|
+
checks.push(check("server-status", "server project status", "fail", errorMessage(error), "Run `rig doctor` after the selected server is reachable."));
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
const auth = await request("/api/github/auth/status");
|
|
737
|
+
checks.push(isAuthenticated(auth) ? check("github-auth", "GitHub auth", "pass") : check("github-auth", "GitHub auth", "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
|
|
738
|
+
} catch (error) {
|
|
739
|
+
checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub through Rig and ensure the server exposes auth status."));
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const permissions = await request("/api/github/repo/permissions");
|
|
743
|
+
const allowed = permissionAllowsPr(permissions);
|
|
744
|
+
checks.push(allowed === true ? check("github-repo-permissions", "GitHub repo PR permissions", "pass", JSON.stringify(permissions).slice(0, 180)) : allowed === false ? check("github-repo-permissions", "GitHub repo PR permissions", "fail", JSON.stringify(permissions).slice(0, 180), "Grant the selected GitHub token permission to push branches, open PRs, and merge according to repo rules.") : check("github-repo-permissions", "GitHub repo PR permissions", "warn", JSON.stringify(permissions).slice(0, 180), "Confirm the selected token can push branches and open PRs."));
|
|
745
|
+
} catch (error) {
|
|
746
|
+
checks.push(check("github-repo-permissions", "GitHub repo PR permissions", "warn", errorMessage(error), "Ensure the server exposes repo permission checks and the token can open PRs."));
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const labels = await request("/api/workspace/task-labels");
|
|
750
|
+
const ready = labelsReady(labels);
|
|
751
|
+
checks.push(ready === false ? check("task-labels", "GitHub issue labels", "fail", JSON.stringify(labels).slice(0, 180), "Let Rig create required labels or create the configured lifecycle labels manually.") : check("task-labels", "GitHub issue labels", ready === true ? "pass" : "warn", JSON.stringify(labels).slice(0, 180), "Confirm required Rig lifecycle labels exist."));
|
|
752
|
+
} catch (error) {
|
|
753
|
+
checks.push(check("task-labels", "GitHub issue labels", "warn", errorMessage(error), "Run `rig init`/`rig doctor` after label setup is wired on the server."));
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
const projection = await request("/api/workspace/task-projection");
|
|
757
|
+
checks.push(check("task-projection", "task projection", "pass", JSON.stringify(projection).slice(0, 180)));
|
|
758
|
+
} catch (error) {
|
|
759
|
+
checks.push(check("task-projection", "task projection", "warn", errorMessage(error), "Refresh task projection with `rig task list` or fix the task source."));
|
|
760
|
+
}
|
|
761
|
+
const slug = projectStatusSlug(projectRoot, config);
|
|
762
|
+
if (slug) {
|
|
763
|
+
try {
|
|
764
|
+
const project = await request(`/api/projects/${encodeURIComponent(slug)}`);
|
|
765
|
+
checks.push(check("remote-checkout", "server project checkout", "pass", JSON.stringify(project).slice(0, 180)));
|
|
766
|
+
} catch (error) {
|
|
767
|
+
checks.push(check("remote-checkout", "server project checkout", "warn", errorMessage(error), "Run `rig init --yes --repo owner/repo` to register/link the server project checkout."));
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
checks.push(check("remote-checkout", "server project checkout", "warn", "repo slug unknown", "Set project.repo or run `rig init --repo owner/repo`."));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (taskSourceKind === "github-issues") {
|
|
774
|
+
checks.push(check("gh", "gh CLI fallback", which("gh") ? "pass" : "warn", which("gh") ?? undefined, "Install gh for local/dev GitHub fallback operations."));
|
|
775
|
+
}
|
|
776
|
+
checks.push(githubProjectsCheck(config));
|
|
777
|
+
checks.push(prMergeCheck(config));
|
|
778
|
+
const piChecks = await (options.piChecks ?? (() => buildPiSetupChecks()))().catch((error) => [{
|
|
779
|
+
ok: false,
|
|
780
|
+
label: "pi/pi-rig checks",
|
|
781
|
+
hint: errorMessage(error)
|
|
782
|
+
}]);
|
|
783
|
+
for (const pi of piChecks) {
|
|
784
|
+
checks.push(check(pi.label === "pi" ? "pi" : "pi-rig", pi.label, pi.ok ? "pass" : "warn", pi.detail, pi.hint ?? (pi.ok ? undefined : "Run `rig init --yes` to install/update Pi and enable pi-rig.")));
|
|
785
|
+
}
|
|
786
|
+
return checks;
|
|
787
|
+
}
|
|
788
|
+
function countDoctorFailures(checks) {
|
|
789
|
+
return checks.filter((entry) => entry.status === "fail").length;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// packages/cli/src/commands/init.ts
|
|
793
|
+
var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.0";
|
|
794
|
+
var RIG_CONFIG_DEV_DEPENDENCIES = {
|
|
795
|
+
"@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
|
|
796
|
+
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
|
|
797
|
+
};
|
|
798
|
+
function parseRepoSlugFromRemote(remoteUrl) {
|
|
799
|
+
const trimmed = remoteUrl.trim();
|
|
800
|
+
const gitHubMatch = trimmed.match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?$/i);
|
|
801
|
+
return gitHubMatch ? `${gitHubMatch[1]}/${gitHubMatch[2]}` : null;
|
|
802
|
+
}
|
|
803
|
+
function detectOriginRepoSlug(projectRoot) {
|
|
804
|
+
const result = spawnSync2("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
|
|
805
|
+
if (result.status !== 0)
|
|
806
|
+
return null;
|
|
807
|
+
return parseRepoSlugFromRemote(result.stdout.trim());
|
|
808
|
+
}
|
|
809
|
+
function parseRepoSlug(value) {
|
|
810
|
+
const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
811
|
+
if (!match)
|
|
812
|
+
throw new CliError2(`Invalid GitHub repo slug: ${value}. Expected owner/repo.`, 1);
|
|
813
|
+
return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
|
|
814
|
+
}
|
|
815
|
+
function ensureRigPrivateDirs(projectRoot) {
|
|
816
|
+
const rigDir = resolve5(projectRoot, ".rig");
|
|
817
|
+
mkdirSync2(resolve5(rigDir, "state"), { recursive: true });
|
|
818
|
+
mkdirSync2(resolve5(rigDir, "logs"), { recursive: true });
|
|
819
|
+
mkdirSync2(resolve5(rigDir, "runs"), { recursive: true });
|
|
820
|
+
mkdirSync2(resolve5(rigDir, "tmp"), { recursive: true });
|
|
821
|
+
mkdirSync2(resolve5(projectRoot, "artifacts"), { recursive: true });
|
|
822
|
+
const taskConfigPath = resolve5(rigDir, "task-config.json");
|
|
823
|
+
if (!existsSync4(taskConfigPath))
|
|
824
|
+
writeFileSync2(taskConfigPath, `{}
|
|
825
|
+
`, "utf-8");
|
|
826
|
+
}
|
|
827
|
+
function ensureGitignoreEntries(projectRoot) {
|
|
828
|
+
const path = resolve5(projectRoot, ".gitignore");
|
|
829
|
+
const existing = existsSync4(path) ? readFileSync4(path, "utf8") : "";
|
|
830
|
+
const entries = [".rig/state/", ".rig/logs/", ".rig/runs/", ".rig/tmp/"];
|
|
831
|
+
const missing = entries.filter((entry) => !existing.split(/\r?\n/).includes(entry));
|
|
832
|
+
if (missing.length === 0)
|
|
833
|
+
return;
|
|
834
|
+
const prefix = existing.length > 0 && !existing.endsWith(`
|
|
835
|
+
`) ? `
|
|
836
|
+
` : "";
|
|
837
|
+
appendFileSync(path, `${prefix}${missing.join(`
|
|
838
|
+
`)}
|
|
839
|
+
`, "utf8");
|
|
840
|
+
}
|
|
841
|
+
function ensureRigConfigPackageDependencies(projectRoot) {
|
|
842
|
+
const path = resolve5(projectRoot, "package.json");
|
|
843
|
+
const existing = existsSync4(path) ? JSON.parse(readFileSync4(path, "utf8")) : {};
|
|
844
|
+
const devDependencies = existing.devDependencies && typeof existing.devDependencies === "object" && !Array.isArray(existing.devDependencies) ? { ...existing.devDependencies } : {};
|
|
845
|
+
for (const [name, spec] of Object.entries(RIG_CONFIG_DEV_DEPENDENCIES)) {
|
|
846
|
+
devDependencies[name] = spec;
|
|
847
|
+
}
|
|
848
|
+
const next = {
|
|
849
|
+
...existsSync4(path) ? existing : { name: "rig-project", private: true },
|
|
850
|
+
devDependencies
|
|
851
|
+
};
|
|
852
|
+
writeFileSync2(path, `${JSON.stringify(next, null, 2)}
|
|
853
|
+
`, "utf8");
|
|
854
|
+
}
|
|
855
|
+
function applyGitHubProjectConfig(source, options) {
|
|
856
|
+
if (!options.githubProject || options.githubProject === "off")
|
|
857
|
+
return source;
|
|
858
|
+
const projectId = JSON.stringify(options.githubProject);
|
|
859
|
+
const statusFieldId = JSON.stringify(options.githubProjectStatusField ?? "Status");
|
|
860
|
+
return source.replace(` projects: { enabled: false },`, [
|
|
861
|
+
` projects: {`,
|
|
862
|
+
` enabled: true,`,
|
|
863
|
+
` projectId: ${projectId},`,
|
|
864
|
+
` statusFieldId: ${statusFieldId},`,
|
|
865
|
+
` },`
|
|
866
|
+
].join(`
|
|
867
|
+
`));
|
|
868
|
+
}
|
|
869
|
+
function checkoutForInit(projectRoot, serverKind, strategy) {
|
|
870
|
+
if (serverKind === "local")
|
|
871
|
+
return { kind: "local", path: projectRoot };
|
|
872
|
+
const selected = strategy ?? { kind: "managed-clone" };
|
|
873
|
+
switch (selected.kind) {
|
|
874
|
+
case "managed-clone":
|
|
875
|
+
return { kind: "managed-clone", path: projectRoot };
|
|
876
|
+
case "current-ref":
|
|
877
|
+
return { kind: "current-ref", path: projectRoot, ...selected.ref ? { ref: selected.ref } : {} };
|
|
878
|
+
case "uploaded-snapshot":
|
|
879
|
+
return { kind: "uploaded-snapshot", path: projectRoot, source: "local-working-tree" };
|
|
880
|
+
case "existing-path":
|
|
881
|
+
return { kind: "existing-path", path: selected.path };
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function detectGhLogin() {
|
|
885
|
+
const result = spawnSync2("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
|
|
886
|
+
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
|
|
887
|
+
}
|
|
888
|
+
function readGhAuthToken() {
|
|
889
|
+
const result = spawnSync2("gh", ["auth", "token"], { encoding: "utf8" });
|
|
890
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
891
|
+
throw new CliError2(result.stderr.trim() || "Could not read GitHub token from `gh auth token`.", result.status || 1);
|
|
892
|
+
}
|
|
893
|
+
return result.stdout.trim();
|
|
894
|
+
}
|
|
895
|
+
async function loadClackPrompts() {
|
|
896
|
+
return await import("@clack/prompts");
|
|
897
|
+
}
|
|
898
|
+
async function promptRequiredText(prompts, options) {
|
|
899
|
+
const value = await prompts.text(options);
|
|
900
|
+
if (prompts.isCancel(value))
|
|
901
|
+
throw new CliError2("Init cancelled.", 1);
|
|
902
|
+
const text = String(value ?? "").trim();
|
|
903
|
+
if (!text)
|
|
904
|
+
throw new CliError2(`${options.message} is required.`, 1);
|
|
905
|
+
return text;
|
|
906
|
+
}
|
|
907
|
+
async function promptOptionalText(prompts, options) {
|
|
908
|
+
const value = await prompts.text(options);
|
|
909
|
+
if (prompts.isCancel(value))
|
|
910
|
+
throw new CliError2("Init cancelled.", 1);
|
|
911
|
+
return String(value ?? "").trim();
|
|
912
|
+
}
|
|
913
|
+
async function promptSelect(prompts, options) {
|
|
914
|
+
const value = await prompts.select(options);
|
|
915
|
+
if (prompts.isCancel(value))
|
|
916
|
+
throw new CliError2("Init cancelled.", 1);
|
|
917
|
+
return String(value);
|
|
918
|
+
}
|
|
919
|
+
async function pollDeviceAuthOnce(context, pollId) {
|
|
920
|
+
if (typeof pollId !== "string" || !pollId.trim())
|
|
921
|
+
return null;
|
|
922
|
+
const payload = await requestServerJson(context, "/api/github/auth/device/poll", {
|
|
923
|
+
method: "POST",
|
|
924
|
+
headers: { "content-type": "application/json" },
|
|
925
|
+
body: JSON.stringify({ pollId })
|
|
926
|
+
}).catch(() => null);
|
|
927
|
+
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
|
|
928
|
+
}
|
|
929
|
+
async function runControlPlaneInit(context, options) {
|
|
930
|
+
const projectRoot = context.projectRoot;
|
|
931
|
+
const detectedSlug = options.repoSlug ?? detectOriginRepoSlug(projectRoot);
|
|
932
|
+
if (!detectedSlug) {
|
|
933
|
+
throw new CliError2("Could not detect GitHub repo slug from origin. Pass --repo owner/repo.", 1);
|
|
934
|
+
}
|
|
935
|
+
const repo = parseRepoSlug(detectedSlug);
|
|
936
|
+
const serverKind = options.server ?? "local";
|
|
937
|
+
const connectionAlias = options.connectionAlias ?? (serverKind === "local" ? "local" : "remote");
|
|
938
|
+
if (serverKind === "remote") {
|
|
939
|
+
if (!options.remoteUrl)
|
|
940
|
+
throw new CliError2("Missing --remote-url for --server remote.", 1);
|
|
941
|
+
upsertGlobalConnection(connectionAlias, { kind: "remote", baseUrl: options.remoteUrl });
|
|
942
|
+
}
|
|
943
|
+
writeRepoConnection(projectRoot, {
|
|
944
|
+
selected: connectionAlias,
|
|
945
|
+
project: repo.slug,
|
|
946
|
+
linkedAt: new Date().toISOString()
|
|
947
|
+
});
|
|
948
|
+
ensureRigPrivateDirs(projectRoot);
|
|
949
|
+
ensureGitignoreEntries(projectRoot);
|
|
950
|
+
const configTsPath = resolve5(projectRoot, "rig.config.ts");
|
|
951
|
+
const configJsonPath = resolve5(projectRoot, "rig.config.json");
|
|
952
|
+
const configExists = existsSync4(configTsPath) || existsSync4(configJsonPath);
|
|
953
|
+
if (!options.privateStateOnly) {
|
|
954
|
+
if (configExists && !options.repair) {
|
|
955
|
+
if (context.outputMode !== "json")
|
|
956
|
+
console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it.");
|
|
957
|
+
} else {
|
|
958
|
+
const source = applyGitHubProjectConfig(buildRigInitConfigSource({
|
|
959
|
+
projectName: repo.slug,
|
|
960
|
+
projectRepo: repo.slug,
|
|
961
|
+
taskSource: { kind: "github-issues", owner: repo.owner, repo: repo.repo },
|
|
962
|
+
useStandardPlugin: true
|
|
963
|
+
}), options);
|
|
964
|
+
writeFileSync2(configTsPath, source, "utf-8");
|
|
965
|
+
}
|
|
966
|
+
ensureRigConfigPackageDependencies(projectRoot);
|
|
967
|
+
}
|
|
968
|
+
writeFileSync2(resolve5(projectRoot, ".rig", "state", "project-link.json"), `${JSON.stringify({ repoSlug: repo.slug, connection: connectionAlias, linkedAt: new Date().toISOString() }, null, 2)}
|
|
969
|
+
`, "utf8");
|
|
970
|
+
const checkout = checkoutForInit(projectRoot, serverKind, options.remoteCheckout);
|
|
971
|
+
let uploadedSnapshot = null;
|
|
972
|
+
if (serverKind === "remote" && options.remoteCheckout?.kind === "uploaded-snapshot") {
|
|
973
|
+
const archive = await createSnapshotUploadArchive(projectRoot);
|
|
974
|
+
uploadedSnapshot = await uploadSnapshotArchiveViaServer(context, { repoSlug: repo.slug, archive });
|
|
975
|
+
const uploadedCheckout = uploadedSnapshot.checkout;
|
|
976
|
+
if (uploadedCheckout && typeof uploadedCheckout === "object" && !Array.isArray(uploadedCheckout)) {
|
|
977
|
+
Object.assign(checkout, uploadedCheckout);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
let githubAuth = null;
|
|
981
|
+
let deviceAuth = null;
|
|
982
|
+
const authMethod = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
|
|
983
|
+
const remoteGhTokenWarning = serverKind === "remote" && authMethod === "gh" ? `This sends a GitHub token from this machine to ${options.remoteUrl ?? "the remote Rig server"}.` : null;
|
|
984
|
+
if (remoteGhTokenWarning && !options.yes) {
|
|
985
|
+
throw new CliError2(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
|
|
986
|
+
}
|
|
987
|
+
const token = authMethod === "gh" && !options.githubToken ? readGhAuthToken() : options.githubToken?.trim();
|
|
988
|
+
if (token) {
|
|
989
|
+
githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug });
|
|
990
|
+
setGitHubBearerTokenForCurrentProcess(token);
|
|
991
|
+
if (serverKind === "remote") {
|
|
992
|
+
writeFileSync2(resolve5(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({ authenticated: true, source: authMethod === "gh" ? "gh" : "init-token", storedOnServer: true, selectedRepo: repo.slug, updatedAt: new Date().toISOString() }, null, 2)}
|
|
993
|
+
`, "utf8");
|
|
994
|
+
}
|
|
995
|
+
} else if (authMethod === "device") {
|
|
996
|
+
const payload = await requestServerJson(context, "/api/github/auth/device/start", {
|
|
997
|
+
method: "POST",
|
|
998
|
+
headers: { "content-type": "application/json" },
|
|
999
|
+
body: JSON.stringify({ repoSlug: repo.slug })
|
|
1000
|
+
});
|
|
1001
|
+
deviceAuth = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
1002
|
+
const completed = await pollDeviceAuthOnce(context, deviceAuth.pollId);
|
|
1003
|
+
if (completed)
|
|
1004
|
+
deviceAuth = { ...deviceAuth, poll: completed, completed: completed.status === "signed-in" };
|
|
1005
|
+
}
|
|
1006
|
+
let remoteCheckoutPreparation = null;
|
|
1007
|
+
if (serverKind === "remote" && options.remoteCheckout?.kind !== "uploaded-snapshot") {
|
|
1008
|
+
remoteCheckoutPreparation = await prepareRemoteCheckoutViaServer(context, {
|
|
1009
|
+
repoSlug: repo.slug,
|
|
1010
|
+
checkout,
|
|
1011
|
+
repoUrl: `https://github.com/${repo.slug}.git`
|
|
1012
|
+
});
|
|
1013
|
+
const preparedCheckout = remoteCheckoutPreparation.checkout;
|
|
1014
|
+
if (preparedCheckout && typeof preparedCheckout === "object" && !Array.isArray(preparedCheckout)) {
|
|
1015
|
+
Object.assign(checkout, preparedCheckout);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const registered = await registerProjectViaServer(context, {
|
|
1019
|
+
repoSlug: repo.slug,
|
|
1020
|
+
checkout
|
|
1021
|
+
});
|
|
1022
|
+
const checkoutPath = typeof checkout.path === "string" ? checkout.path : null;
|
|
1023
|
+
const serverRootSwitch = serverKind === "remote" && checkoutPath ? await switchServerProjectRootViaServer(context, checkoutPath) : null;
|
|
1024
|
+
const activeProjectRegistration = serverRootSwitch ? await registerProjectViaServer(context, { repoSlug: repo.slug, checkout }) : null;
|
|
1025
|
+
const pi = serverKind === "remote" ? await ensureRemotePiRigInstalled({ requestJson: (pathname, init) => requestServerJson(context, pathname, init) }).catch((error) => ({
|
|
1026
|
+
remote: true,
|
|
1027
|
+
pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
|
|
1028
|
+
piRig: { ok: false, label: "pi-rig global extension", hint: "Remote server did not complete pi-rig installation." },
|
|
1029
|
+
extensionPath: "remote:~/.pi/agent/extensions/pi-rig"
|
|
1030
|
+
})) : await ensurePiRigInstalled({ projectRoot, homeDir: process.env.RIG_PI_HOME_DIR }).catch((error) => ({
|
|
1031
|
+
pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
|
|
1032
|
+
piRig: { ok: false, label: "pi-rig global extension", hint: "Local pi-rig installation failed." },
|
|
1033
|
+
extensionPath: null,
|
|
1034
|
+
installedPath: null
|
|
1035
|
+
}));
|
|
1036
|
+
const doctor = await runRigDoctorChecks({ projectRoot }).then((checks) => ({
|
|
1037
|
+
ok: countDoctorFailures(checks) === 0,
|
|
1038
|
+
failures: countDoctorFailures(checks),
|
|
1039
|
+
checks
|
|
1040
|
+
}));
|
|
1041
|
+
const details = {
|
|
1042
|
+
repoSlug: repo.slug,
|
|
1043
|
+
server: serverKind,
|
|
1044
|
+
connection: connectionAlias,
|
|
1045
|
+
githubProject: options.githubProject ?? "off",
|
|
1046
|
+
checkout,
|
|
1047
|
+
remoteCheckoutPreparation,
|
|
1048
|
+
uploadedSnapshot,
|
|
1049
|
+
projectRegistration: registered,
|
|
1050
|
+
activeProjectRegistration,
|
|
1051
|
+
serverRootSwitch,
|
|
1052
|
+
githubAuth,
|
|
1053
|
+
deviceAuth,
|
|
1054
|
+
githubAuthWarning: remoteGhTokenWarning,
|
|
1055
|
+
pi,
|
|
1056
|
+
doctor
|
|
1057
|
+
};
|
|
1058
|
+
if (context.outputMode === "json")
|
|
1059
|
+
console.log(JSON.stringify(details, null, 2));
|
|
1060
|
+
else
|
|
1061
|
+
console.log(`Initialized Rig control-plane project ${repo.slug}. Next: rig doctor && rig task list`);
|
|
1062
|
+
return { ok: true, group: "init", command: "init", details };
|
|
1063
|
+
}
|
|
1064
|
+
function parseInitOptions(args) {
|
|
1065
|
+
let rest = [...args];
|
|
1066
|
+
const yes = takeFlag(rest, "--yes");
|
|
1067
|
+
rest = yes.rest;
|
|
1068
|
+
const repair = takeFlag(rest, "--repair");
|
|
1069
|
+
rest = repair.rest;
|
|
1070
|
+
const privateStateOnly = takeFlag(rest, "--private-state-only");
|
|
1071
|
+
rest = privateStateOnly.rest;
|
|
1072
|
+
const server = takeOption(rest, "--server");
|
|
1073
|
+
rest = server.rest;
|
|
1074
|
+
const remoteUrl = takeOption(rest, "--remote-url");
|
|
1075
|
+
rest = remoteUrl.rest;
|
|
1076
|
+
const connectionAlias = takeOption(rest, "--connection");
|
|
1077
|
+
rest = connectionAlias.rest;
|
|
1078
|
+
const repoSlug = takeOption(rest, "--repo");
|
|
1079
|
+
rest = repoSlug.rest;
|
|
1080
|
+
const githubToken = takeOption(rest, "--github-token");
|
|
1081
|
+
rest = githubToken.rest;
|
|
1082
|
+
const githubProject = takeOption(rest, "--github-project");
|
|
1083
|
+
rest = githubProject.rest;
|
|
1084
|
+
const githubProjectStatusField = takeOption(rest, "--github-project-status-field");
|
|
1085
|
+
rest = githubProjectStatusField.rest;
|
|
1086
|
+
const githubAuth = takeOption(rest, "--github-auth");
|
|
1087
|
+
rest = githubAuth.rest;
|
|
1088
|
+
const remoteCheckout = takeOption(rest, "--remote-checkout");
|
|
1089
|
+
rest = remoteCheckout.rest;
|
|
1090
|
+
const existingPath = takeOption(rest, "--existing-path");
|
|
1091
|
+
rest = existingPath.rest;
|
|
1092
|
+
const ref = takeOption(rest, "--ref");
|
|
1093
|
+
rest = ref.rest;
|
|
1094
|
+
const options = {
|
|
1095
|
+
yes: yes.value,
|
|
1096
|
+
repair: repair.value,
|
|
1097
|
+
privateStateOnly: privateStateOnly.value,
|
|
1098
|
+
server: server.value === "remote" ? "remote" : server.value === "local" ? "local" : undefined,
|
|
1099
|
+
remoteUrl: remoteUrl.value,
|
|
1100
|
+
connectionAlias: connectionAlias.value,
|
|
1101
|
+
repoSlug: repoSlug.value,
|
|
1102
|
+
githubToken: githubToken.value,
|
|
1103
|
+
githubAuthMethod: githubAuth.value,
|
|
1104
|
+
githubProject: githubProject.value,
|
|
1105
|
+
githubProjectStatusField: githubProjectStatusField.value
|
|
1106
|
+
};
|
|
1107
|
+
if (server.value && options.server === undefined) {
|
|
1108
|
+
throw new CliError2("--server must be local or remote.", 1);
|
|
1109
|
+
}
|
|
1110
|
+
if (githubAuth.value && !["gh", "token", "device", "skip"].includes(githubAuth.value)) {
|
|
1111
|
+
throw new CliError2("--github-auth must be gh, token, device, or skip.", 1);
|
|
1112
|
+
}
|
|
1113
|
+
if (remoteCheckout.value) {
|
|
1114
|
+
if (remoteCheckout.value === "managed-clone")
|
|
1115
|
+
options.remoteCheckout = { kind: "managed-clone" };
|
|
1116
|
+
else if (remoteCheckout.value === "current-ref")
|
|
1117
|
+
options.remoteCheckout = { kind: "current-ref", ref: ref.value };
|
|
1118
|
+
else if (remoteCheckout.value === "uploaded-snapshot")
|
|
1119
|
+
options.remoteCheckout = { kind: "uploaded-snapshot" };
|
|
1120
|
+
else if (remoteCheckout.value === "existing-path") {
|
|
1121
|
+
if (!existingPath.value)
|
|
1122
|
+
throw new CliError2("--remote-checkout existing-path requires --existing-path <path>.", 1);
|
|
1123
|
+
options.remoteCheckout = { kind: "existing-path", path: existingPath.value };
|
|
1124
|
+
} else {
|
|
1125
|
+
throw new CliError2("--remote-checkout must be managed-clone, current-ref, uploaded-snapshot, or existing-path.", 1);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return { options, rest };
|
|
1129
|
+
}
|
|
1130
|
+
async function runInteractiveControlPlaneInit(context, prompts) {
|
|
1131
|
+
prompts.intro?.("Initialize a Rig control-plane project");
|
|
1132
|
+
const projectRoot = context.projectRoot;
|
|
1133
|
+
const existingConfig = existsSync4(resolve5(projectRoot, "rig.config.ts")) || existsSync4(resolve5(projectRoot, "rig.config.json"));
|
|
1134
|
+
let repair = false;
|
|
1135
|
+
let privateStateOnly = false;
|
|
1136
|
+
if (existingConfig) {
|
|
1137
|
+
const action = await promptSelect(prompts, {
|
|
1138
|
+
message: "rig.config already exists. What should rig init do?",
|
|
1139
|
+
options: [
|
|
1140
|
+
{ value: "repair", label: "Verify/repair generated config" },
|
|
1141
|
+
{ value: "reconfigure", label: "Reconfigure project and rewrite config" },
|
|
1142
|
+
{ value: "private-state-only", label: "Leave config unchanged; update private connection/auth state only" },
|
|
1143
|
+
{ value: "cancel", label: "Cancel" }
|
|
1144
|
+
]
|
|
1145
|
+
});
|
|
1146
|
+
if (action === "cancel") {
|
|
1147
|
+
prompts.cancel?.("Init cancelled.");
|
|
1148
|
+
return { ok: false, group: "init", command: "init", details: { cancelled: true } };
|
|
1149
|
+
}
|
|
1150
|
+
repair = action === "repair" || action === "reconfigure";
|
|
1151
|
+
privateStateOnly = action === "private-state-only";
|
|
1152
|
+
}
|
|
1153
|
+
const detectedRepo = detectOriginRepoSlug(projectRoot) ?? undefined;
|
|
1154
|
+
const repoSlug = await promptRequiredText(prompts, {
|
|
1155
|
+
message: "GitHub repo slug",
|
|
1156
|
+
placeholder: "owner/repo",
|
|
1157
|
+
defaultValue: detectedRepo
|
|
1158
|
+
});
|
|
1159
|
+
const serverChoice = await promptSelect(prompts, {
|
|
1160
|
+
message: "Rig server",
|
|
1161
|
+
options: [
|
|
1162
|
+
{ value: "local", label: "Local server", hint: "run on this machine" },
|
|
1163
|
+
{ value: "remote", label: "Remote server", hint: "connect to an HTTPS Rig server" }
|
|
1164
|
+
]
|
|
1165
|
+
});
|
|
1166
|
+
const remoteUrl = serverChoice === "remote" ? await promptRequiredText(prompts, { message: "Remote Rig server URL", placeholder: "https://rig.example.com" }) : undefined;
|
|
1167
|
+
let remoteCheckout;
|
|
1168
|
+
if (serverChoice === "remote") {
|
|
1169
|
+
const checkout = await promptSelect(prompts, {
|
|
1170
|
+
message: "Remote checkout strategy",
|
|
1171
|
+
options: [
|
|
1172
|
+
{ value: "managed-clone", label: "Server-managed clone (recommended)" },
|
|
1173
|
+
{ value: "current-ref", label: "Clone current branch/ref" },
|
|
1174
|
+
{ value: "uploaded-snapshot", label: "Upload current working-tree snapshot" },
|
|
1175
|
+
{ value: "existing-path", label: "Use existing server path" }
|
|
1176
|
+
]
|
|
1177
|
+
});
|
|
1178
|
+
if (checkout === "existing-path") {
|
|
1179
|
+
remoteCheckout = { kind: "existing-path", path: await promptRequiredText(prompts, { message: "Existing server checkout path", placeholder: "/srv/rig/checkouts/repo" }) };
|
|
1180
|
+
} else if (checkout === "current-ref") {
|
|
1181
|
+
remoteCheckout = { kind: "current-ref", ref: await promptOptionalText(prompts, { message: "Branch/ref to clone (blank for current HEAD)", placeholder: "main" }) || undefined };
|
|
1182
|
+
} else if (checkout === "uploaded-snapshot") {
|
|
1183
|
+
remoteCheckout = { kind: "uploaded-snapshot" };
|
|
1184
|
+
} else {
|
|
1185
|
+
remoteCheckout = { kind: "managed-clone" };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const detectedGhLogin = detectGhLogin();
|
|
1189
|
+
const authMethod = await promptSelect(prompts, {
|
|
1190
|
+
message: `GitHub auth method${detectedGhLogin ? ` (detected gh login: ${detectedGhLogin})` : ""}`,
|
|
1191
|
+
options: [
|
|
1192
|
+
{ value: "gh", label: "Import token from gh auth token", hint: serverChoice === "local" ? "recommended for local" : "sends this machine's token to the remote server" },
|
|
1193
|
+
{ value: "device", label: "Start server GitHub device flow", hint: serverChoice === "remote" ? "recommended for remote" : undefined },
|
|
1194
|
+
{ value: "token", label: "Paste token" },
|
|
1195
|
+
{ value: "skip", label: "Skip for now" }
|
|
1196
|
+
]
|
|
1197
|
+
});
|
|
1198
|
+
if (serverChoice === "remote" && authMethod === "gh") {
|
|
1199
|
+
if (!prompts.confirm)
|
|
1200
|
+
throw new CliError2("Remote gh-token import requires explicit confirmation.", 1);
|
|
1201
|
+
const confirmed = await prompts.confirm({
|
|
1202
|
+
message: `This sends a GitHub token from this machine to ${remoteUrl}. Continue?`,
|
|
1203
|
+
initialValue: false
|
|
1204
|
+
});
|
|
1205
|
+
if (prompts.isCancel(confirmed) || confirmed !== true) {
|
|
1206
|
+
throw new CliError2("Remote gh-token import cancelled.", 1);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : undefined;
|
|
1210
|
+
const projectChoice = await promptSelect(prompts, {
|
|
1211
|
+
message: "GitHub Projects status sync",
|
|
1212
|
+
options: [
|
|
1213
|
+
{ value: "off", label: "Off" },
|
|
1214
|
+
{ value: "configure", label: "Configure ProjectV2 status field" }
|
|
1215
|
+
]
|
|
1216
|
+
});
|
|
1217
|
+
const githubProject = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : "off";
|
|
1218
|
+
const githubProjectStatusField = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }) : undefined;
|
|
1219
|
+
const result = await runControlPlaneInit(context, {
|
|
1220
|
+
server: serverChoice,
|
|
1221
|
+
remoteUrl,
|
|
1222
|
+
repoSlug,
|
|
1223
|
+
githubToken,
|
|
1224
|
+
githubAuthMethod: authMethod,
|
|
1225
|
+
githubProject,
|
|
1226
|
+
githubProjectStatusField,
|
|
1227
|
+
remoteCheckout,
|
|
1228
|
+
repair,
|
|
1229
|
+
privateStateOnly
|
|
1230
|
+
});
|
|
1231
|
+
const details = result.details && typeof result.details === "object" && !Array.isArray(result.details) ? result.details : {};
|
|
1232
|
+
const deviceAuth = details.deviceAuth && typeof details.deviceAuth === "object" && !Array.isArray(details.deviceAuth) ? details.deviceAuth : null;
|
|
1233
|
+
const deviceMessage = deviceAuth ? ` GitHub device flow: open ${String(deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by the server")} and enter ${String(deviceAuth.user_code ?? "the returned user code")}.` : "";
|
|
1234
|
+
prompts.outro?.(`Rig project initialized.${deviceMessage} Next: rig doctor && rig task list`);
|
|
1235
|
+
return result;
|
|
1236
|
+
}
|
|
1237
|
+
async function executeInit(context, args) {
|
|
1238
|
+
const parsed = parseInitOptions(args);
|
|
1239
|
+
if (parsed.options.yes || parsed.options.server || parsed.options.repoSlug || parsed.options.githubToken || parsed.options.privateStateOnly || parsed.options.repair || parsed.options.githubAuthMethod || parsed.options.remoteCheckout) {
|
|
1240
|
+
if (parsed.rest.length > 0)
|
|
1241
|
+
throw new CliError2(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1242
|
+
Usage: rig init [--server local|remote] [--remote-url <url>] [--repo owner/repo] [--github-auth gh|token|device|skip] [--github-token <token>] [--github-project off|<project-id>] [--remote-checkout managed-clone|current-ref|uploaded-snapshot|existing-path] [--yes]`, 1);
|
|
1243
|
+
return runControlPlaneInit(context, parsed.options);
|
|
1244
|
+
}
|
|
1245
|
+
if (parsed.rest.length > 0)
|
|
1246
|
+
throw new CliError2(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1247
|
+
Usage: rig init`, 1);
|
|
1248
|
+
return runInteractiveControlPlaneInit(context, await loadClackPrompts());
|
|
1249
|
+
}
|
|
1250
|
+
export {
|
|
1251
|
+
runInteractiveControlPlaneInit,
|
|
1252
|
+
executeInit,
|
|
1253
|
+
buildRigInitConfigSource
|
|
1254
|
+
};
|