@h-rig/cli 0.0.6-alpha.9 → 0.0.6-alpha.91
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 +18 -19
- package/dist/bin/build-rig-binaries.d.ts +2 -0
- package/dist/bin/build-rig-binaries.js +22 -10
- package/dist/bin/rig.d.ts +72 -0
- package/dist/bin/rig.js +15637 -7344
- package/dist/config/rig-default-config.yml +5 -0
- package/dist/src/app/drone-ui.d.ts +34 -0
- package/dist/src/app/drone-ui.js +278 -0
- package/dist/src/commands/_async-ui.d.ts +10 -0
- package/dist/src/commands/_async-ui.js +121 -0
- package/dist/src/commands/_cli-format.d.ts +56 -0
- package/dist/src/commands/_cli-format.js +319 -0
- package/dist/src/commands/_connection-state.d.ts +54 -0
- package/dist/src/commands/_connection-state.js +75 -11
- package/dist/src/commands/_doctor-checks.d.ts +46 -0
- package/dist/src/commands/_doctor-checks.js +100 -267
- package/dist/src/commands/_help-catalog.d.ts +51 -0
- package/dist/src/commands/_help-catalog.js +502 -0
- package/dist/src/commands/_inprocess-services.d.ts +33 -0
- package/dist/src/commands/_inprocess-services.js +102 -0
- package/dist/src/commands/_json-output.d.ts +11 -0
- package/dist/src/commands/_json-output.js +60 -0
- package/dist/src/commands/_lazy-reconcile.d.ts +34 -0
- package/dist/src/commands/_lazy-reconcile.js +102 -0
- package/dist/src/commands/_parsers.d.ts +15 -0
- package/dist/src/commands/_parsers.js +18 -11
- package/dist/src/commands/_paths.d.ts +11 -0
- package/dist/src/commands/_paths.js +1 -1
- package/dist/src/commands/_pi-frontend.d.ts +35 -0
- package/dist/src/commands/_pi-frontend.js +64 -0
- package/dist/src/commands/_pi-install.d.ts +42 -0
- package/dist/src/commands/_pi-install.js +19 -36
- package/dist/src/commands/_policy.d.ts +8 -0
- package/dist/src/commands/_policy.js +67 -19
- package/dist/src/commands/_probes.d.ts +1 -0
- package/dist/src/commands/_run-bridge.d.ts +114 -0
- package/dist/src/commands/_run-bridge.js +387 -0
- package/dist/src/commands/_run-diagnostics.d.ts +9 -0
- package/dist/src/commands/_run-diagnostics.js +51 -0
- package/dist/src/commands/_run-driver-helpers.d.ts +26 -0
- package/dist/src/commands/_run-driver-helpers.js +79 -230
- package/dist/src/commands/_run-projection.d.ts +50 -0
- package/dist/src/commands/_run-projection.js +349 -0
- package/dist/src/commands/_run-subcommands.d.ts +3 -0
- package/dist/src/commands/_run-subcommands.js +31 -0
- package/dist/src/commands/_spinner.d.ts +25 -0
- package/dist/src/commands/_spinner.js +65 -0
- package/dist/src/commands/agent.d.ts +3 -0
- package/dist/src/commands/agent.js +8575 -236
- package/dist/src/commands/dist.d.ts +28 -0
- package/dist/src/commands/dist.js +44 -29
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +219 -268
- package/dist/src/commands/github.d.ts +3 -0
- package/dist/src/commands/github.js +236 -188
- package/dist/src/commands/inbox.d.ts +28 -0
- package/dist/src/commands/inbox.js +530 -111
- package/dist/src/commands/init.d.ts +64 -0
- package/dist/src/commands/init.js +912 -705
- package/dist/src/commands/inspect.d.ts +20 -0
- package/dist/src/commands/inspect.js +709 -119
- package/dist/src/commands/pi.d.ts +3 -0
- package/dist/src/commands/pi.js +177 -0
- package/dist/src/commands/plugin.d.ts +16 -0
- package/dist/src/commands/plugin.js +576 -29
- package/dist/src/commands/profile-and-review.d.ts +4 -0
- package/dist/src/commands/profile-and-review.js +112 -67
- package/dist/src/commands/queue.d.ts +3 -0
- package/dist/src/commands/queue.js +20 -20
- package/dist/src/commands/remote.d.ts +3 -0
- package/dist/src/commands/remote.js +880 -50
- package/dist/src/commands/repo-git-harness.d.ts +5 -0
- package/dist/src/commands/repo-git-harness.js +78 -28
- package/dist/src/commands/run.d.ts +21 -0
- package/dist/src/commands/run.js +17701 -661
- package/dist/src/commands/server.d.ts +3 -0
- package/dist/src/commands/server.js +166 -303
- package/dist/src/commands/setup.d.ts +16 -0
- package/dist/src/commands/setup.js +244 -299
- package/dist/src/commands/stats.d.ts +15 -0
- package/dist/src/commands/stats.js +979 -0
- package/dist/src/commands/task-run-driver.d.ts +94 -0
- package/dist/src/commands/task-run-driver.js +116 -1961
- package/dist/src/commands/task.d.ts +35 -0
- package/dist/src/commands/task.js +676 -1201
- package/dist/src/commands/test.d.ts +3 -0
- package/dist/src/commands/test.js +15 -8
- package/dist/src/commands/triage.d.ts +11 -0
- package/dist/src/commands/triage.js +227 -0
- package/dist/src/commands/workspace.d.ts +3 -0
- package/dist/src/commands/workspace.js +18 -11
- package/dist/src/commands.d.ts +13 -0
- package/dist/src/commands.js +16792 -8055
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +16862 -8450
- package/dist/src/launcher.d.ts +61 -0
- package/dist/src/launcher.js +81 -13
- package/dist/src/operator-cli.d.ts +2 -0
- package/dist/src/operator-cli.js +17837 -0
- package/dist/src/operator-entry.d.ts +1 -0
- package/dist/src/operator-entry.js +3 -0
- package/dist/src/runner.d.ts +47 -0
- package/dist/src/runner.js +16 -22
- package/dist/src/withMutedConsole.d.ts +2 -0
- package/package.json +21 -8
- package/dist/src/commands/_authority-runs.js +0 -111
- package/dist/src/commands/_operator-view.js +0 -340
- package/dist/src/commands/_preflight.js +0 -478
- package/dist/src/commands/_server-client.js +0 -382
- package/dist/src/commands/_snapshot-upload.js +0 -331
- package/dist/src/commands/_task-picker.js +0 -48
- package/dist/src/commands/browser.js +0 -890
- package/dist/src/commands/connect.js +0 -180
- package/dist/src/commands/inspector.js +0 -256
- package/dist/src/commands/task-report-bug.js +0 -1083
- package/dist/src/report-bug.js +0 -260
|
@@ -1,19 +1,325 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
2
17
|
var __require = import.meta.require;
|
|
3
18
|
|
|
19
|
+
// packages/cli/src/commands/_spinner.ts
|
|
20
|
+
function createTtySpinner(input) {
|
|
21
|
+
const output = input.output ?? process.stdout;
|
|
22
|
+
const isTty = output.isTTY === true;
|
|
23
|
+
const frames = input.frames && input.frames.length > 0 ? input.frames : SPINNER_FRAMES;
|
|
24
|
+
let label = input.label;
|
|
25
|
+
let frame = 0;
|
|
26
|
+
let paused = false;
|
|
27
|
+
let stopped = false;
|
|
28
|
+
let lastPrintedLabel = "";
|
|
29
|
+
const render = () => {
|
|
30
|
+
if (stopped || paused)
|
|
31
|
+
return;
|
|
32
|
+
if (!isTty) {
|
|
33
|
+
if (label !== lastPrintedLabel) {
|
|
34
|
+
output.write(`${label}
|
|
35
|
+
`);
|
|
36
|
+
lastPrintedLabel = label;
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
frame = (frame + 1) % frames.length;
|
|
41
|
+
const glyph = frames[frame] ?? frames[0] ?? "";
|
|
42
|
+
output.write(`\r\x1B[2K${input.styleFrame ? input.styleFrame(glyph) : glyph} ${label}`);
|
|
43
|
+
};
|
|
44
|
+
const clearLine = () => {
|
|
45
|
+
if (isTty)
|
|
46
|
+
output.write("\r\x1B[2K");
|
|
47
|
+
};
|
|
48
|
+
render();
|
|
49
|
+
const timer = isTty ? setInterval(render, input.intervalMs ?? 16) : null;
|
|
50
|
+
return {
|
|
51
|
+
setLabel(next) {
|
|
52
|
+
label = next;
|
|
53
|
+
render();
|
|
54
|
+
},
|
|
55
|
+
pause() {
|
|
56
|
+
paused = true;
|
|
57
|
+
clearLine();
|
|
58
|
+
},
|
|
59
|
+
resume() {
|
|
60
|
+
if (stopped)
|
|
61
|
+
return;
|
|
62
|
+
paused = false;
|
|
63
|
+
render();
|
|
64
|
+
},
|
|
65
|
+
stop(finalLine) {
|
|
66
|
+
if (stopped)
|
|
67
|
+
return;
|
|
68
|
+
stopped = true;
|
|
69
|
+
if (timer)
|
|
70
|
+
clearInterval(timer);
|
|
71
|
+
clearLine();
|
|
72
|
+
if (finalLine)
|
|
73
|
+
output.write(`${finalLine}
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var SPINNER_FRAMES;
|
|
79
|
+
var init__spinner = __esm(() => {
|
|
80
|
+
SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// packages/cli/src/app/drone-ui.ts
|
|
84
|
+
var exports_drone_ui = {};
|
|
85
|
+
__export(exports_drone_ui, {
|
|
86
|
+
droneWarn: () => droneWarn,
|
|
87
|
+
droneText: () => droneText,
|
|
88
|
+
droneStep: () => droneStep,
|
|
89
|
+
droneSpinner: () => droneSpinner,
|
|
90
|
+
droneSelect: () => droneSelect,
|
|
91
|
+
droneOutro: () => droneOutro,
|
|
92
|
+
droneNote: () => droneNote,
|
|
93
|
+
droneIntro: () => droneIntro,
|
|
94
|
+
droneInfo: () => droneInfo,
|
|
95
|
+
droneError: () => droneError,
|
|
96
|
+
droneConfirm: () => droneConfirm,
|
|
97
|
+
droneCancel: () => droneCancel
|
|
98
|
+
});
|
|
99
|
+
import { Input, matchesKey, ProcessTerminal, SelectList, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
100
|
+
function hexToRgb(hex) {
|
|
101
|
+
const value = hex.charCodeAt(0) === 35 ? hex.slice(1) : hex;
|
|
102
|
+
return [
|
|
103
|
+
Number.parseInt(value.slice(0, 2), 16),
|
|
104
|
+
Number.parseInt(value.slice(2, 4), 16),
|
|
105
|
+
Number.parseInt(value.slice(4, 6), 16)
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
function fg(hex) {
|
|
109
|
+
const [r, g, b] = hexToRgb(hex);
|
|
110
|
+
return (text) => `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
|
|
111
|
+
}
|
|
112
|
+
function bold(text) {
|
|
113
|
+
return `\x1B[1m${text}\x1B[22m`;
|
|
114
|
+
}
|
|
115
|
+
function renderMicroDroneFrame(frame) {
|
|
116
|
+
const tick = Math.max(0, MICRO_DRONE_FRAMES.indexOf(frame));
|
|
117
|
+
const blade = MICRO_BLADES[tick % MICRO_BLADES.length];
|
|
118
|
+
const eye = EYE_FRAMES[Math.floor(tick / 2) % EYE_FRAMES.length];
|
|
119
|
+
return `${ink4("(")}${cyan(blade)}${ink4(")")}${bold(accent(eye))}${ink4("(")}${cyan(blade)}${ink4(")")}`;
|
|
120
|
+
}
|
|
121
|
+
function hairline(width = Math.min(process.stdout.columns ?? 80, 100)) {
|
|
122
|
+
return ink4("\u2500".repeat(Math.max(10, width)));
|
|
123
|
+
}
|
|
124
|
+
function droneIntro(title, subtitle) {
|
|
125
|
+
console.log("");
|
|
126
|
+
console.log(` ${accent("\u258D")}${bold(ink(title))}${subtitle ? ink3(` \u2014 ${subtitle}`) : ""}`);
|
|
127
|
+
console.log(hairline());
|
|
128
|
+
}
|
|
129
|
+
function droneOutro(text) {
|
|
130
|
+
console.log(hairline());
|
|
131
|
+
console.log(` ${accent("\u25C6")} ${ink2(text)}`);
|
|
132
|
+
console.log("");
|
|
133
|
+
}
|
|
134
|
+
function droneNote(message, title) {
|
|
135
|
+
if (title)
|
|
136
|
+
console.log(` ${accentDim("\u25C7")} ${bold(ink2(title))}`);
|
|
137
|
+
for (const line of message.split(`
|
|
138
|
+
`))
|
|
139
|
+
console.log(` ${ink4("\u2502")} ${line}`);
|
|
140
|
+
}
|
|
141
|
+
function droneStep(text) {
|
|
142
|
+
console.log(` ${accent("\u203A")} ${ink(text)}`);
|
|
143
|
+
}
|
|
144
|
+
function droneInfo(text) {
|
|
145
|
+
console.log(` ${cyan("\xB7")} ${ink2(text)}`);
|
|
146
|
+
}
|
|
147
|
+
function droneWarn(text) {
|
|
148
|
+
console.log(` ${yellow("\u25B2")} ${ink2(text)}`);
|
|
149
|
+
}
|
|
150
|
+
function droneError(text) {
|
|
151
|
+
console.log(` ${red("\u2716")} ${ink2(text)}`);
|
|
152
|
+
}
|
|
153
|
+
function droneCancel(text) {
|
|
154
|
+
console.log(` ${red("\u2716")} ${ink3(text)}`);
|
|
155
|
+
}
|
|
156
|
+
function droneSpinner() {
|
|
157
|
+
let active = null;
|
|
158
|
+
return {
|
|
159
|
+
start(message) {
|
|
160
|
+
active = createTtySpinner({
|
|
161
|
+
label: message,
|
|
162
|
+
frames: MICRO_DRONE_FRAMES,
|
|
163
|
+
styleFrame: renderMicroDroneFrame
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
stop(message) {
|
|
167
|
+
active?.stop(message ? ` ${accent("\u25C6")} ${ink2(message)}` : undefined);
|
|
168
|
+
active = null;
|
|
169
|
+
},
|
|
170
|
+
error(message) {
|
|
171
|
+
active?.stop(message ? ` ${red("\u2716")} ${ink2(message)}` : undefined);
|
|
172
|
+
active = null;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function runMiniTui(build) {
|
|
177
|
+
const tui = new TUI(new ProcessTerminal);
|
|
178
|
+
let settled = false;
|
|
179
|
+
return await new Promise((resolve5) => {
|
|
180
|
+
const finish = (result) => {
|
|
181
|
+
if (settled)
|
|
182
|
+
return;
|
|
183
|
+
settled = true;
|
|
184
|
+
tui.stop();
|
|
185
|
+
resolve5(result);
|
|
186
|
+
};
|
|
187
|
+
build(tui, finish);
|
|
188
|
+
tui.start();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async function droneSelect(input) {
|
|
192
|
+
if (!process.stdout.isTTY || input.options.length === 0)
|
|
193
|
+
return input.initialValue ?? input.options[0]?.value ?? null;
|
|
194
|
+
return runMiniTui((tui, finish) => {
|
|
195
|
+
tui.addChild(new Text(` ${accent("\u258D")}${bold(ink(input.message))}`));
|
|
196
|
+
const items = input.options.map((option) => ({
|
|
197
|
+
value: option.value,
|
|
198
|
+
label: option.label,
|
|
199
|
+
...option.hint ? { description: option.hint } : {}
|
|
200
|
+
}));
|
|
201
|
+
const list = new SelectList(items, Math.min(items.length, 12), DRONE_SELECT_THEME);
|
|
202
|
+
const initialIndex = input.initialValue ? items.findIndex((item) => item.value === input.initialValue) : -1;
|
|
203
|
+
if (initialIndex > 0)
|
|
204
|
+
list.setSelectedIndex(initialIndex);
|
|
205
|
+
list.onSelect = (item) => finish(item.value);
|
|
206
|
+
list.onCancel = () => finish(null);
|
|
207
|
+
tui.addChild(list);
|
|
208
|
+
tui.addChild(new Text(ink4(" \u2191\u2193 navigate \xB7 enter select \xB7 esc cancel")));
|
|
209
|
+
tui.setFocus(list);
|
|
210
|
+
tui.addInputListener((data) => {
|
|
211
|
+
if (matchesKey(data, "ctrl+c") || matchesKey(data, "escape")) {
|
|
212
|
+
finish(null);
|
|
213
|
+
return { consume: true };
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
async function droneText(input) {
|
|
220
|
+
if (!process.stdout.isTTY)
|
|
221
|
+
return input.initialValue ?? null;
|
|
222
|
+
return runMiniTui((tui, finish) => {
|
|
223
|
+
tui.addChild(new Text(` ${accent("\u258D")}${bold(ink(input.message))}${input.placeholder ? ink4(` (${input.placeholder})`) : ""}`));
|
|
224
|
+
const field = new Input;
|
|
225
|
+
if (input.initialValue)
|
|
226
|
+
field.setValue(input.initialValue);
|
|
227
|
+
field.onSubmit = (value) => finish(value);
|
|
228
|
+
field.onEscape = () => finish(null);
|
|
229
|
+
tui.addChild(field);
|
|
230
|
+
tui.addChild(new Text(ink4(" enter submit \xB7 esc cancel")));
|
|
231
|
+
tui.setFocus(field);
|
|
232
|
+
tui.addInputListener((data) => {
|
|
233
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
234
|
+
finish(null);
|
|
235
|
+
return { consume: true };
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async function droneConfirm(input) {
|
|
242
|
+
const answer = await droneSelect({
|
|
243
|
+
message: input.message,
|
|
244
|
+
options: [
|
|
245
|
+
{ value: "yes", label: "Yes" },
|
|
246
|
+
{ value: "no", label: "No" }
|
|
247
|
+
],
|
|
248
|
+
initialValue: input.initialValue === false ? "no" : "yes"
|
|
249
|
+
});
|
|
250
|
+
return answer === null ? null : answer === "yes";
|
|
251
|
+
}
|
|
252
|
+
var PALETTE, ink, ink2, ink3, ink4, accent, accentDim, cyan, red, yellow, MICRO_BLADES, EYE_FRAMES, MICRO_DRONE_FRAMES, DRONE_SYMBOLS, DRONE_SELECT_THEME;
|
|
253
|
+
var init_drone_ui = __esm(() => {
|
|
254
|
+
init__spinner();
|
|
255
|
+
PALETTE = {
|
|
256
|
+
ink: "#f2f3f6",
|
|
257
|
+
ink2: "#aeb0ba",
|
|
258
|
+
ink3: "#6c6e79",
|
|
259
|
+
ink4: "#44464f",
|
|
260
|
+
accent: "#ccff4d",
|
|
261
|
+
accentDim: "#a9d63f",
|
|
262
|
+
cyan: "#56d8ff",
|
|
263
|
+
red: "#ff5d5d",
|
|
264
|
+
yellow: "#ffd24d"
|
|
265
|
+
};
|
|
266
|
+
ink = fg(PALETTE.ink);
|
|
267
|
+
ink2 = fg(PALETTE.ink2);
|
|
268
|
+
ink3 = fg(PALETTE.ink3);
|
|
269
|
+
ink4 = fg(PALETTE.ink4);
|
|
270
|
+
accent = fg(PALETTE.accent);
|
|
271
|
+
accentDim = fg(PALETTE.accentDim);
|
|
272
|
+
cyan = fg(PALETTE.cyan);
|
|
273
|
+
red = fg(PALETTE.red);
|
|
274
|
+
yellow = fg(PALETTE.yellow);
|
|
275
|
+
MICRO_BLADES = ["---", "\\\\\\", "|||", "///"];
|
|
276
|
+
EYE_FRAMES = ["@", "o", "."];
|
|
277
|
+
MICRO_DRONE_FRAMES = Array.from({ length: 12 }, (_, index) => {
|
|
278
|
+
const blade = MICRO_BLADES[index % MICRO_BLADES.length];
|
|
279
|
+
const eye = EYE_FRAMES[Math.floor(index / 2) % EYE_FRAMES.length];
|
|
280
|
+
return `(${blade})${eye}(${blade})`;
|
|
281
|
+
});
|
|
282
|
+
DRONE_SYMBOLS = {
|
|
283
|
+
cursor: "\u2192",
|
|
284
|
+
inputCursor: "\u258F",
|
|
285
|
+
boxRound: { topLeft: "\u256D", topRight: "\u256E", bottomLeft: "\u2570", bottomRight: "\u256F", horizontal: "\u2500", vertical: "\u2502" },
|
|
286
|
+
boxSharp: { topLeft: "\u250C", topRight: "\u2510", bottomLeft: "\u2514", bottomRight: "\u2518", horizontal: "\u2500", vertical: "\u2502", teeDown: "\u252C", teeUp: "\u2534", teeLeft: "\u2524", teeRight: "\u251C", cross: "\u253C" },
|
|
287
|
+
table: { topLeft: "\u250C", topRight: "\u2510", bottomLeft: "\u2514", bottomRight: "\u2518", horizontal: "\u2500", vertical: "\u2502", teeDown: "\u252C", teeUp: "\u2534", teeLeft: "\u2524", teeRight: "\u251C", cross: "\u253C" },
|
|
288
|
+
quoteBorder: "\u2502",
|
|
289
|
+
hrChar: "\u2500",
|
|
290
|
+
spinnerFrames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
|
|
291
|
+
};
|
|
292
|
+
DRONE_SELECT_THEME = {
|
|
293
|
+
selectedPrefix: (text) => accent(text),
|
|
294
|
+
selectedText: (text) => bold(ink(text)),
|
|
295
|
+
description: (text) => ink3(text),
|
|
296
|
+
scrollInfo: (text) => ink4(text),
|
|
297
|
+
noMatch: (text) => ink3(text),
|
|
298
|
+
symbols: DRONE_SYMBOLS,
|
|
299
|
+
hovered: (text) => bold(ink(text))
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
|
|
4
303
|
// packages/cli/src/commands/init.ts
|
|
5
|
-
import { appendFileSync, existsSync as
|
|
6
|
-
import { spawnSync
|
|
7
|
-
import { resolve as
|
|
304
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
305
|
+
import { spawnSync } from "child_process";
|
|
306
|
+
import { basename, resolve as resolve5 } from "path";
|
|
8
307
|
|
|
9
308
|
// packages/cli/src/runner.ts
|
|
10
309
|
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
11
|
-
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
310
|
+
import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
|
|
12
311
|
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
312
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
16
|
-
|
|
313
|
+
|
|
314
|
+
class CliError extends RuntimeCliError {
|
|
315
|
+
hint;
|
|
316
|
+
constructor(message, exitCode = 1, options = {}) {
|
|
317
|
+
super(message, exitCode);
|
|
318
|
+
if (options.hint?.trim()) {
|
|
319
|
+
this.hint = options.hint.trim();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
17
323
|
function takeFlag(args, flag) {
|
|
18
324
|
const rest = [];
|
|
19
325
|
let value = false;
|
|
@@ -34,7 +340,7 @@ function takeOption(args, option) {
|
|
|
34
340
|
if (current === option) {
|
|
35
341
|
const next = args[index + 1];
|
|
36
342
|
if (!next || next.startsWith("-")) {
|
|
37
|
-
throw new CliError(`Missing value for ${option}`);
|
|
343
|
+
throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
|
|
38
344
|
}
|
|
39
345
|
value = next;
|
|
40
346
|
index += 1;
|
|
@@ -52,17 +358,7 @@ import { buildRigInitConfigSource } from "@rig/core";
|
|
|
52
358
|
|
|
53
359
|
// packages/cli/src/commands/_connection-state.ts
|
|
54
360
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
55
|
-
import { homedir } from "os";
|
|
56
361
|
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
362
|
function resolveRepoConnectionPath(projectRoot) {
|
|
67
363
|
return resolve(projectRoot, ".rig", "state", "connection.json");
|
|
68
364
|
}
|
|
@@ -72,7 +368,7 @@ function readJsonFile(path) {
|
|
|
72
368
|
try {
|
|
73
369
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
74
370
|
} catch (error) {
|
|
75
|
-
throw new
|
|
371
|
+
throw new CliError(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1, { hint: "Fix or delete that file, then re-select a server with `rig server use <alias|local>`." });
|
|
76
372
|
}
|
|
77
373
|
}
|
|
78
374
|
function writeJsonFile(path, value) {
|
|
@@ -80,47 +376,6 @@ function writeJsonFile(path, value) {
|
|
|
80
376
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
81
377
|
`, "utf8");
|
|
82
378
|
}
|
|
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
379
|
function readRepoConnection(projectRoot) {
|
|
125
380
|
const payload = readJsonFile(resolveRepoConnectionPath(projectRoot));
|
|
126
381
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
@@ -132,200 +387,118 @@ function readRepoConnection(projectRoot) {
|
|
|
132
387
|
return {
|
|
133
388
|
selected,
|
|
134
389
|
project: typeof record.project === "string" ? record.project : undefined,
|
|
135
|
-
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined
|
|
390
|
+
linkedAt: typeof record.linkedAt === "string" ? record.linkedAt : undefined,
|
|
391
|
+
serverProjectRoot: typeof record.serverProjectRoot === "string" && record.serverProjectRoot.trim() ? record.serverProjectRoot.trim() : undefined,
|
|
392
|
+
serverProjectRootAlias: typeof record.serverProjectRootAlias === "string" && record.serverProjectRootAlias.trim() ? record.serverProjectRootAlias.trim() : undefined,
|
|
393
|
+
serverProjectRootBaseUrl: typeof record.serverProjectRootBaseUrl === "string" && record.serverProjectRootBaseUrl.trim() ? record.serverProjectRootBaseUrl.trim().replace(/\/+$/, "") : undefined
|
|
136
394
|
};
|
|
137
395
|
}
|
|
138
396
|
function writeRepoConnection(projectRoot, state) {
|
|
139
397
|
writeJsonFile(resolveRepoConnectionPath(projectRoot), state);
|
|
140
398
|
}
|
|
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
399
|
|
|
155
|
-
// packages/cli/src/commands/
|
|
156
|
-
import {
|
|
157
|
-
|
|
400
|
+
// packages/cli/src/commands/init.ts
|
|
401
|
+
import { upsertManagedRemoteEndpoint } from "@rig/runtime/control-plane/remote";
|
|
402
|
+
|
|
403
|
+
// packages/cli/src/commands/_inprocess-services.ts
|
|
158
404
|
import { resolve as resolve2 } from "path";
|
|
159
|
-
import {
|
|
160
|
-
|
|
405
|
+
import {
|
|
406
|
+
beginGitHubDeviceFlow,
|
|
407
|
+
checkGitHubRepoPermissions,
|
|
408
|
+
createGitHubAuthStore,
|
|
409
|
+
listGitHubProjects,
|
|
410
|
+
pollGitHubDeviceFlow,
|
|
411
|
+
resolveGitHubAuthStatus,
|
|
412
|
+
resolveProjectStatusField,
|
|
413
|
+
saveGitHubTokenForProject
|
|
414
|
+
} from "@rig/runtime/control-plane/github/index";
|
|
415
|
+
var scopedGitHubBearerTokens = new Map;
|
|
161
416
|
function cleanToken(value) {
|
|
162
417
|
const trimmed = value?.trim();
|
|
163
418
|
return trimmed ? trimmed : null;
|
|
164
419
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
420
|
+
function cleanString(value) {
|
|
421
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
167
422
|
}
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
if (!existsSync2(path))
|
|
171
|
-
return null;
|
|
172
|
-
try {
|
|
173
|
-
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
174
|
-
return cleanToken(typeof parsed.apiSessionToken === "string" ? parsed.apiSessionToken : typeof parsed.sessionToken === "string" ? parsed.sessionToken : undefined);
|
|
175
|
-
} catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
423
|
+
function oauthClientId() {
|
|
424
|
+
return cleanToken(process.env.RIG_GITHUB_OAUTH_CLIENT_ID);
|
|
178
425
|
}
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const privateSession = readPrivateRemoteSessionToken(projectRoot);
|
|
183
|
-
if (privateSession) {
|
|
184
|
-
cachedGitHubBearerToken = privateSession;
|
|
185
|
-
return cachedGitHubBearerToken;
|
|
186
|
-
}
|
|
187
|
-
const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
|
|
188
|
-
if (envToken) {
|
|
189
|
-
cachedGitHubBearerToken = envToken;
|
|
190
|
-
return cachedGitHubBearerToken;
|
|
191
|
-
}
|
|
192
|
-
const result = spawnSync("gh", ["auth", "token"], {
|
|
193
|
-
encoding: "utf8",
|
|
194
|
-
timeout: 5000,
|
|
195
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
196
|
-
});
|
|
197
|
-
cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
|
|
198
|
-
return cachedGitHubBearerToken;
|
|
426
|
+
function tokenForProject(projectRoot, override) {
|
|
427
|
+
const scoped = scopedGitHubBearerTokens.get(resolve2(projectRoot));
|
|
428
|
+
return cleanToken(override) ?? scoped ?? createGitHubAuthStore(projectRoot).readToken() ?? cleanToken(process.env.RIG_GITHUB_TOKEN);
|
|
199
429
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const selected = resolveSelectedConnection(projectRoot);
|
|
203
|
-
if (selected?.connection.kind === "remote") {
|
|
204
|
-
return {
|
|
205
|
-
baseUrl: selected.connection.baseUrl,
|
|
206
|
-
authToken: readGitHubBearerTokenForRemote(projectRoot),
|
|
207
|
-
connectionKind: "remote"
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
const connection = await ensureLocalRigServerConnection(projectRoot);
|
|
211
|
-
return {
|
|
212
|
-
baseUrl: connection.baseUrl,
|
|
213
|
-
authToken: connection.authToken,
|
|
214
|
-
connectionKind: "local"
|
|
215
|
-
};
|
|
216
|
-
} catch (error) {
|
|
217
|
-
if (error instanceof Error) {
|
|
218
|
-
throw new CliError2(error.message, 1);
|
|
219
|
-
}
|
|
220
|
-
throw error;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
function mergeHeaders(headers, authToken) {
|
|
224
|
-
const merged = new Headers(headers);
|
|
225
|
-
if (authToken) {
|
|
226
|
-
merged.set("authorization", `Bearer ${authToken}`);
|
|
227
|
-
}
|
|
228
|
-
return merged;
|
|
430
|
+
function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
|
|
431
|
+
scopedGitHubBearerTokens.set(resolve2(projectRoot ?? process.cwd()), cleanToken(token));
|
|
229
432
|
}
|
|
230
|
-
function
|
|
231
|
-
|
|
232
|
-
return null;
|
|
233
|
-
const record = payload;
|
|
234
|
-
const diagnostics = Array.isArray(record.diagnostics) ? record.diagnostics : [];
|
|
235
|
-
const messages = diagnostics.flatMap((entry) => {
|
|
236
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
237
|
-
return [];
|
|
238
|
-
const diagnostic = entry;
|
|
239
|
-
const kind = typeof diagnostic.kind === "string" ? diagnostic.kind : "task-source";
|
|
240
|
-
const message = typeof diagnostic.message === "string" ? diagnostic.message : null;
|
|
241
|
-
return message ? [`${kind}: ${message}`] : [];
|
|
242
|
-
});
|
|
243
|
-
return messages.length > 0 ? messages.join("; ") : null;
|
|
433
|
+
async function getGitHubAuthStatusInProcess(context) {
|
|
434
|
+
return { ok: true, ...resolveGitHubAuthStatus({ projectRoot: context.projectRoot, oauthConfigured: Boolean(oauthClientId()) }) };
|
|
244
435
|
}
|
|
245
|
-
async function
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
436
|
+
async function postGitHubTokenInProcess(context, token, options = {}) {
|
|
437
|
+
const targetRoot = options.projectRoot?.trim() || context.projectRoot;
|
|
438
|
+
const result = await saveGitHubTokenForProject({
|
|
439
|
+
projectRoot: targetRoot,
|
|
440
|
+
token,
|
|
441
|
+
tokenSource: "manual-token",
|
|
442
|
+
selectedRepo: options.selectedRepo ?? null
|
|
250
443
|
});
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
} catch {
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
})() : null;
|
|
259
|
-
if (!response.ok) {
|
|
260
|
-
const diagnostics = diagnosticMessage(payload);
|
|
261
|
-
const detail = diagnostics ?? (text || response.statusText);
|
|
262
|
-
throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
|
|
444
|
+
const store = createGitHubAuthStore(targetRoot);
|
|
445
|
+
const session = store.createApiSession();
|
|
446
|
+
if (targetRoot !== context.projectRoot) {
|
|
447
|
+
store.copyToLocalProjectRoot(context.projectRoot);
|
|
263
448
|
}
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
async function
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
});
|
|
299
|
-
const timeoutMs = options.timeoutMs ?? 30000;
|
|
300
|
-
const pollMs = options.pollMs ?? 1000;
|
|
301
|
-
const deadline = Date.now() + timeoutMs;
|
|
302
|
-
let lastError;
|
|
303
|
-
while (Date.now() < deadline) {
|
|
304
|
-
try {
|
|
305
|
-
const status = await requestServerJson(context, "/api/server/status");
|
|
306
|
-
if (status && typeof status === "object" && !Array.isArray(status)) {
|
|
307
|
-
const record = status;
|
|
308
|
-
if (record.projectRoot === projectRoot) {
|
|
309
|
-
return { ok: true, switched, status: record };
|
|
310
|
-
}
|
|
311
|
-
lastError = `server projectRoot=${String(record.projectRoot ?? "unknown")}`;
|
|
312
|
-
}
|
|
313
|
-
} catch (error) {
|
|
314
|
-
lastError = error;
|
|
315
|
-
}
|
|
316
|
-
await sleep(pollMs);
|
|
449
|
+
return { ...result, authenticated: result.signedIn, apiSessionToken: session.token };
|
|
450
|
+
}
|
|
451
|
+
async function listGitHubProjectsInProcess(context, owner, options = {}) {
|
|
452
|
+
const token = tokenForProject(context.projectRoot, options.authToken);
|
|
453
|
+
if (!token)
|
|
454
|
+
return { ok: false, error: "missing-token", projects: [] };
|
|
455
|
+
const projects = await listGitHubProjects({ owner, token });
|
|
456
|
+
return { ok: true, projects };
|
|
457
|
+
}
|
|
458
|
+
async function getGitHubProjectStatusFieldInProcess(context, projectId, options = {}) {
|
|
459
|
+
const token = tokenForProject(context.projectRoot, options.authToken);
|
|
460
|
+
if (!token)
|
|
461
|
+
return { ok: false, error: "missing-token" };
|
|
462
|
+
const field = await resolveProjectStatusField({ projectId, token });
|
|
463
|
+
return { ok: true, field };
|
|
464
|
+
}
|
|
465
|
+
async function requestGitHubAuthJsonInProcess(context, pathname, init = {}) {
|
|
466
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
467
|
+
if (pathname === "/api/github/auth/status")
|
|
468
|
+
return getGitHubAuthStatusInProcess(context);
|
|
469
|
+
if (pathname === "/api/github/repo/permissions") {
|
|
470
|
+
return checkGitHubRepoPermissions({ projectRoot: context.projectRoot, oauthConfigured: Boolean(oauthClientId()) });
|
|
471
|
+
}
|
|
472
|
+
if (pathname === "/api/github/auth/device/start" && method === "POST") {
|
|
473
|
+
const clientId = oauthClientId();
|
|
474
|
+
if (!clientId)
|
|
475
|
+
return { ok: false, oauthConfigured: false, error: "RIG_GITHUB_OAUTH_CLIENT_ID is not configured." };
|
|
476
|
+
const body = init.body ? JSON.parse(String(init.body)) : {};
|
|
477
|
+
return beginGitHubDeviceFlow({
|
|
478
|
+
projectRoot: context.projectRoot,
|
|
479
|
+
clientId,
|
|
480
|
+
scope: cleanString(body.scope) ?? undefined,
|
|
481
|
+
selectedRepo: cleanString(body.repoSlug)
|
|
482
|
+
});
|
|
317
483
|
}
|
|
318
|
-
|
|
484
|
+
if (pathname === "/api/github/auth/device/poll" && method === "POST") {
|
|
485
|
+
const clientId = oauthClientId();
|
|
486
|
+
if (!clientId)
|
|
487
|
+
return { ok: false, oauthConfigured: false, error: "RIG_GITHUB_OAUTH_CLIENT_ID is not configured." };
|
|
488
|
+
const body = init.body ? JSON.parse(String(init.body)) : {};
|
|
489
|
+
const pollId = cleanString(body.pollId);
|
|
490
|
+
if (!pollId)
|
|
491
|
+
return { ok: false, status: "error", error: "pollId is required" };
|
|
492
|
+
return pollGitHubDeviceFlow({ projectRoot: context.projectRoot, clientId, pollId, selectedRepo: cleanString(body.repoSlug) });
|
|
493
|
+
}
|
|
494
|
+
throw new Error(`No in-process GitHub auth API for ${method} ${pathname}`);
|
|
319
495
|
}
|
|
320
496
|
|
|
321
497
|
// packages/cli/src/commands/_pi-install.ts
|
|
322
|
-
import { existsSync as
|
|
323
|
-
import { homedir
|
|
498
|
+
import { existsSync as existsSync2 } from "fs";
|
|
499
|
+
import { homedir } from "os";
|
|
324
500
|
import { resolve as resolve3 } from "path";
|
|
325
|
-
var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
|
|
326
|
-
var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
|
|
327
|
-
export { default } from '@rig/pi-rig';
|
|
328
|
-
`;
|
|
501
|
+
var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
|
|
329
502
|
async function defaultCommandRunner(command, options = {}) {
|
|
330
503
|
const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
|
|
331
504
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
@@ -338,14 +511,14 @@ async function defaultCommandRunner(command, options = {}) {
|
|
|
338
511
|
function resolvePiRigExtensionPath(homeDir) {
|
|
339
512
|
return resolve3(homeDir, ".pi", "agent", "extensions", "pi-rig");
|
|
340
513
|
}
|
|
341
|
-
function resolvePiRigPackageSource(projectRoot, exists =
|
|
514
|
+
function resolvePiRigPackageSource(projectRoot, exists = existsSync2) {
|
|
342
515
|
const localPackage = resolve3(projectRoot, "packages", "pi-rig");
|
|
343
516
|
if (exists(resolve3(localPackage, "package.json")))
|
|
344
517
|
return localPackage;
|
|
345
518
|
return `npm:${PI_RIG_PACKAGE_NAME}`;
|
|
346
519
|
}
|
|
347
520
|
function resolvePiHomeDir(inputHomeDir) {
|
|
348
|
-
return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ??
|
|
521
|
+
return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir();
|
|
349
522
|
}
|
|
350
523
|
function piListContainsPiRig(output) {
|
|
351
524
|
return output.split(/\r?\n/).some((line) => {
|
|
@@ -374,7 +547,13 @@ async function ensurePiBinaryAvailable(input) {
|
|
|
374
547
|
}
|
|
375
548
|
return { ok: true, detail: (current.stdout || current.stderr).trim() || undefined };
|
|
376
549
|
}
|
|
377
|
-
const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim()
|
|
550
|
+
const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim();
|
|
551
|
+
if (!installCommand) {
|
|
552
|
+
return {
|
|
553
|
+
ok: false,
|
|
554
|
+
error: `${(current.stderr || current.stdout).trim() || "pi --version failed"}. Set RIG_PI_INSTALL_COMMAND to a supported installer or install Pi manually.`
|
|
555
|
+
};
|
|
556
|
+
}
|
|
378
557
|
const parts = splitInstallCommand(installCommand);
|
|
379
558
|
if (parts.length === 0) {
|
|
380
559
|
return { ok: false, error: (current.stderr || current.stdout).trim() || "pi --version failed" };
|
|
@@ -391,18 +570,6 @@ async function ensurePiBinaryAvailable(input) {
|
|
|
391
570
|
...next.exitCode === 0 ? {} : { error: (next.stderr || next.stdout).trim() || "pi --version failed after install" }
|
|
392
571
|
};
|
|
393
572
|
}
|
|
394
|
-
function removeManagedLegacyPiRigBridge(homeDir, exists = existsSync3) {
|
|
395
|
-
const extensionPath = resolvePiRigExtensionPath(homeDir);
|
|
396
|
-
const indexPath = resolve3(extensionPath, "index.ts");
|
|
397
|
-
if (!exists(indexPath))
|
|
398
|
-
return;
|
|
399
|
-
try {
|
|
400
|
-
const content = readFileSync3(indexPath, "utf8");
|
|
401
|
-
if (content === LEGACY_PI_RIG_MARKER || content.includes("Managed by Rig. Source package: @rig/pi-rig")) {
|
|
402
|
-
rmSync(extensionPath, { recursive: true, force: true });
|
|
403
|
-
}
|
|
404
|
-
} catch {}
|
|
405
|
-
}
|
|
406
573
|
async function checkPiRigInstall(input = {}) {
|
|
407
574
|
const home = resolvePiHomeDir(input.homeDir);
|
|
408
575
|
const extensionPath = resolvePiRigExtensionPath(home);
|
|
@@ -413,48 +580,40 @@ async function checkPiRigInstall(input = {}) {
|
|
|
413
580
|
piRig: { ok: true, label: "pi-rig global extension", detail: extensionPath }
|
|
414
581
|
};
|
|
415
582
|
}
|
|
416
|
-
const exists = input.exists ?? existsSync3;
|
|
417
583
|
const runner = input.commandRunner ?? defaultCommandRunner;
|
|
418
584
|
const piResult = await safeRun(runner, ["pi", "--version"]);
|
|
419
585
|
const piListResult = piResult.exitCode === 0 ? await safeRun(runner, ["pi", "list"]) : { exitCode: 1, stdout: "", stderr: "" };
|
|
420
|
-
const
|
|
586
|
+
const hasPiRig = piListResult.exitCode === 0 && piListContainsPiRig(`${piListResult.stdout}
|
|
421
587
|
${piListResult.stderr}`);
|
|
422
|
-
const
|
|
423
|
-
const hasPiRig = listedPiRig;
|
|
588
|
+
const hasLegacyBridgeScaffold = !hasPiRig && (input.exists ?? existsSync2)(extensionPath);
|
|
424
589
|
return {
|
|
425
590
|
extensionPath,
|
|
426
591
|
pi: {
|
|
427
592
|
ok: piResult.exitCode === 0,
|
|
428
593
|
label: "pi",
|
|
429
594
|
detail: (piResult.stdout || piResult.stderr).trim() || undefined,
|
|
430
|
-
hint: piResult.exitCode === 0 ? undefined : "Install Pi or
|
|
595
|
+
hint: piResult.exitCode === 0 ? undefined : "Install Pi/OMP manually or set RIG_PI_INSTALL_COMMAND before verifying with bare `rig` / Cockpit \u2192 Doctor."
|
|
431
596
|
},
|
|
432
597
|
piRig: {
|
|
433
598
|
ok: hasPiRig,
|
|
434
599
|
label: "pi-rig global extension",
|
|
435
|
-
detail: hasPiRig ? piListResult.stdout.trim() || PI_RIG_PACKAGE_NAME :
|
|
436
|
-
hint: hasPiRig ? undefined :
|
|
600
|
+
detail: hasPiRig ? piListResult.stdout.trim() || PI_RIG_PACKAGE_NAME : hasLegacyBridgeScaffold ? `legacy bridge scaffold at ${extensionPath}` : undefined,
|
|
601
|
+
hint: hasPiRig ? undefined : `Install the Rig OMP extension with \`pi install ${PI_RIG_PACKAGE_NAME}\`, then verify with bare \`rig\` / Cockpit \u2192 Doctor.`
|
|
437
602
|
}
|
|
438
603
|
};
|
|
439
604
|
}
|
|
440
605
|
async function ensurePiRigInstalled(input) {
|
|
441
606
|
const home = resolvePiHomeDir(input.homeDir);
|
|
442
607
|
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
|
|
443
|
-
const
|
|
444
|
-
return { ...
|
|
608
|
+
const status = await checkPiRigInstall({ homeDir: home, commandRunner: input.commandRunner });
|
|
609
|
+
return { ...status, installedPath: status.extensionPath };
|
|
445
610
|
}
|
|
446
611
|
const runner = input.commandRunner ?? defaultCommandRunner;
|
|
447
612
|
const piAvailable = await ensurePiBinaryAvailable({ runner, projectRoot: input.projectRoot });
|
|
448
|
-
const status = piAvailable.ok ? await checkPiRigInstall({ homeDir: home, commandRunner: runner }) : {
|
|
449
|
-
extensionPath: resolvePiRigExtensionPath(home),
|
|
450
|
-
pi: { ok: false, label: "pi", detail: piAvailable.error, hint: "Install/update Pi with RIG_PI_INSTALL_COMMAND or install Pi manually." },
|
|
451
|
-
piRig: { ok: false, label: "pi-rig global extension", hint: "Pi is required before pi-rig can be installed." }
|
|
452
|
-
};
|
|
453
613
|
if (!piAvailable.ok) {
|
|
454
614
|
throw new Error(`Pi install/update failed: ${piAvailable.error ?? "pi unavailable"}`);
|
|
455
615
|
}
|
|
456
616
|
const packageSource = resolvePiRigPackageSource(input.projectRoot);
|
|
457
|
-
removeManagedLegacyPiRigBridge(home);
|
|
458
617
|
const install = await runner(["pi", "install", packageSource], { cwd: input.projectRoot });
|
|
459
618
|
if (install.exitCode !== 0) {
|
|
460
619
|
throw new Error(`pi-rig install failed: ${(install.stderr || install.stdout).trim() || `exit ${install.exitCode}`}`);
|
|
@@ -462,132 +621,14 @@ async function ensurePiRigInstalled(input) {
|
|
|
462
621
|
const next = await checkPiRigInstall({ homeDir: home, commandRunner: runner });
|
|
463
622
|
return { ...next, installedPath: packageSource };
|
|
464
623
|
}
|
|
465
|
-
async function ensureRemotePiRigInstalled(input) {
|
|
466
|
-
const payload = await input.requestJson("/api/pi-rig/install", {
|
|
467
|
-
method: "POST",
|
|
468
|
-
headers: { "content-type": "application/json" },
|
|
469
|
-
body: JSON.stringify({ package: "@rig/pi-rig", scope: "global" })
|
|
470
|
-
});
|
|
471
|
-
const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
472
|
-
const piOk = record.piOk === true || record.ok === true;
|
|
473
|
-
const piRigOk = record.piRigOk === true || record.installed === true || record.ok === true;
|
|
474
|
-
const extensionPath = typeof record.extensionPath === "string" ? record.extensionPath : "remote:~/.pi/agent/extensions/pi-rig";
|
|
475
|
-
return {
|
|
476
|
-
remote: true,
|
|
477
|
-
extensionPath,
|
|
478
|
-
pi: {
|
|
479
|
-
ok: piOk,
|
|
480
|
-
label: "pi",
|
|
481
|
-
detail: typeof record.piVersion === "string" ? record.piVersion : undefined,
|
|
482
|
-
hint: piOk ? undefined : "Install/update Pi on the selected remote Rig server."
|
|
483
|
-
},
|
|
484
|
-
piRig: {
|
|
485
|
-
ok: piRigOk,
|
|
486
|
-
label: "pi-rig global extension",
|
|
487
|
-
detail: extensionPath,
|
|
488
|
-
hint: piRigOk ? undefined : "Install/enable pi-rig on the selected remote Rig server."
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
624
|
async function buildPiSetupChecks(input = {}) {
|
|
493
625
|
const status = await checkPiRigInstall(input);
|
|
494
626
|
return [status.pi, status.piRig];
|
|
495
627
|
}
|
|
496
628
|
|
|
497
|
-
// packages/cli/src/commands/_snapshot-upload.ts
|
|
498
|
-
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
499
|
-
import { dirname as dirname2, resolve as resolve4, relative, sep } from "path";
|
|
500
|
-
var SNAPSHOT_ARCHIVE_VERSION = 1;
|
|
501
|
-
var SNAPSHOT_ARCHIVE_CONTENT_TYPE = "application/vnd.rig.snapshot+json";
|
|
502
|
-
var DEFAULT_EXCLUDED_DIRECTORIES = new Set([
|
|
503
|
-
".git",
|
|
504
|
-
".rig",
|
|
505
|
-
"node_modules",
|
|
506
|
-
".turbo",
|
|
507
|
-
".next",
|
|
508
|
-
".cache",
|
|
509
|
-
"coverage",
|
|
510
|
-
"dist",
|
|
511
|
-
"build",
|
|
512
|
-
"out"
|
|
513
|
-
]);
|
|
514
|
-
function toPosixPath(path) {
|
|
515
|
-
return path.split(sep).join("/");
|
|
516
|
-
}
|
|
517
|
-
function assertManifestPath(root, relativePath) {
|
|
518
|
-
if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\x00")) {
|
|
519
|
-
throw new Error(`Invalid snapshot path: ${relativePath}`);
|
|
520
|
-
}
|
|
521
|
-
const resolved = resolve4(root, relativePath);
|
|
522
|
-
const relativeToRoot = relative(root, resolved);
|
|
523
|
-
if (relativeToRoot.startsWith("..") || relativeToRoot === ".." || resolve4(relativeToRoot) === resolved) {
|
|
524
|
-
throw new Error(`Snapshot path escapes project root: ${relativePath}`);
|
|
525
|
-
}
|
|
526
|
-
return resolved;
|
|
527
|
-
}
|
|
528
|
-
async function buildSnapshotUploadManifest(projectRoot, options = {}) {
|
|
529
|
-
const root = resolve4(projectRoot);
|
|
530
|
-
const excludedDirectories = [...new Set([
|
|
531
|
-
...DEFAULT_EXCLUDED_DIRECTORIES,
|
|
532
|
-
...options.excludedDirectories ?? []
|
|
533
|
-
])];
|
|
534
|
-
const excludedSet = new Set(excludedDirectories);
|
|
535
|
-
const files = [];
|
|
536
|
-
async function visit(dir) {
|
|
537
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
538
|
-
for (const entry of entries) {
|
|
539
|
-
if (entry.isDirectory() && excludedSet.has(entry.name))
|
|
540
|
-
continue;
|
|
541
|
-
const fullPath = resolve4(dir, entry.name);
|
|
542
|
-
if (entry.isDirectory()) {
|
|
543
|
-
await visit(fullPath);
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
if (!entry.isFile())
|
|
547
|
-
continue;
|
|
548
|
-
files.push(toPosixPath(relative(root, fullPath)));
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
await visit(root);
|
|
552
|
-
files.sort();
|
|
553
|
-
return { root, files, excludedDirectories };
|
|
554
|
-
}
|
|
555
|
-
async function createSnapshotUploadArchive(projectRoot, options = {}) {
|
|
556
|
-
const manifest = await buildSnapshotUploadManifest(projectRoot, options);
|
|
557
|
-
const files = await Promise.all(manifest.files.map(async (path) => {
|
|
558
|
-
const fullPath = assertManifestPath(manifest.root, path);
|
|
559
|
-
return {
|
|
560
|
-
path,
|
|
561
|
-
contentBase64: (await readFile(fullPath)).toString("base64")
|
|
562
|
-
};
|
|
563
|
-
}));
|
|
564
|
-
return {
|
|
565
|
-
version: SNAPSHOT_ARCHIVE_VERSION,
|
|
566
|
-
root: manifest.root,
|
|
567
|
-
files,
|
|
568
|
-
excludedDirectories: manifest.excludedDirectories,
|
|
569
|
-
createdAt: (options.now?.() ?? new Date).toISOString()
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
function encodeSnapshotUploadArchive(archive) {
|
|
573
|
-
return Buffer.from(JSON.stringify(archive), "utf8").toString("base64");
|
|
574
|
-
}
|
|
575
|
-
async function uploadSnapshotArchiveViaServer(context, input) {
|
|
576
|
-
const payload = await requestServerJson(context, `/api/projects/${encodeURIComponent(input.repoSlug)}/upload-snapshot`, {
|
|
577
|
-
method: "POST",
|
|
578
|
-
headers: { "content-type": "application/json" },
|
|
579
|
-
body: JSON.stringify({
|
|
580
|
-
archiveContentBase64: encodeSnapshotUploadArchive(input.archive),
|
|
581
|
-
contentType: SNAPSHOT_ARCHIVE_CONTENT_TYPE,
|
|
582
|
-
baseDir: input.baseDir
|
|
583
|
-
})
|
|
584
|
-
});
|
|
585
|
-
return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
629
|
// packages/cli/src/commands/_doctor-checks.ts
|
|
589
|
-
import { existsSync as
|
|
590
|
-
import { resolve as
|
|
630
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
631
|
+
import { resolve as resolve4 } from "path";
|
|
591
632
|
import { isSupportedBunVersion, MIN_SUPPORTED_BUN_VERSION } from "@rig/runtime/control-plane/setup-version";
|
|
592
633
|
|
|
593
634
|
// packages/cli/src/commands/_parsers.ts
|
|
@@ -619,31 +660,14 @@ function isAuthenticated(payload) {
|
|
|
619
660
|
const record = payload;
|
|
620
661
|
return record.signedIn === true || record.authenticated === true || record.status === "authenticated" || record.ok === true && typeof record.login === "string" && record.login.trim().length > 0;
|
|
621
662
|
}
|
|
622
|
-
function repoSlugFromConfig(config) {
|
|
623
|
-
const project = config?.project;
|
|
624
|
-
if (project && typeof project === "object" && !Array.isArray(project)) {
|
|
625
|
-
const record = project;
|
|
626
|
-
if (typeof record.repo === "string" && /^([^/\s]+)\/([^/\s]+)$/.test(record.repo))
|
|
627
|
-
return record.repo;
|
|
628
|
-
if (typeof record.name === "string" && /^([^/\s]+)\/([^/\s]+)$/.test(record.name))
|
|
629
|
-
return record.name;
|
|
630
|
-
}
|
|
631
|
-
const taskSource = config?.taskSource;
|
|
632
|
-
if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
|
|
633
|
-
const source = taskSource;
|
|
634
|
-
if (typeof source.owner === "string" && typeof source.repo === "string")
|
|
635
|
-
return `${source.owner}/${source.repo}`;
|
|
636
|
-
}
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
663
|
function loadFallbackConfig(projectRoot) {
|
|
640
664
|
const candidates = ["rig.config.ts", "rig.config.mts", "rig.config.json"];
|
|
641
665
|
for (const name of candidates) {
|
|
642
|
-
const path =
|
|
643
|
-
if (!
|
|
666
|
+
const path = resolve4(projectRoot, name);
|
|
667
|
+
if (!existsSync3(path))
|
|
644
668
|
continue;
|
|
645
669
|
try {
|
|
646
|
-
const source =
|
|
670
|
+
const source = readFileSync2(path, "utf8");
|
|
647
671
|
if (name.endsWith(".json"))
|
|
648
672
|
return JSON.parse(source);
|
|
649
673
|
const owner = source.match(/owner\s*:\s*["']([^"']+)["']/)?.[1];
|
|
@@ -662,14 +686,11 @@ function loadFallbackConfig(projectRoot) {
|
|
|
662
686
|
}
|
|
663
687
|
return null;
|
|
664
688
|
}
|
|
665
|
-
function projectStatusSlug(projectRoot, config) {
|
|
666
|
-
return readRepoConnection(projectRoot)?.project ?? repoSlugFromConfig(config);
|
|
667
|
-
}
|
|
668
689
|
function githubProjectsCheck(config) {
|
|
669
690
|
const github = config?.github;
|
|
670
691
|
const projects = github?.projects;
|
|
671
692
|
if (!projects?.enabled) {
|
|
672
|
-
return check("github-projects", "GitHub Projects status sync", "warn", "disabled or not configured", "
|
|
693
|
+
return check("github-projects", "GitHub Projects status sync", "warn", "disabled or not configured", "Current OMP flow does not require GitHub Projects status sync; configure github.projects manually when Project status sync should be authoritative.");
|
|
673
694
|
}
|
|
674
695
|
if (projects.projectId && projects.statusFieldId) {
|
|
675
696
|
return check("github-projects", "GitHub Projects status sync", "pass", `project ${projects.projectId}`);
|
|
@@ -694,16 +715,6 @@ function permissionAllowsPr(payload) {
|
|
|
694
715
|
}
|
|
695
716
|
return null;
|
|
696
717
|
}
|
|
697
|
-
function labelsReady(payload) {
|
|
698
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
699
|
-
return null;
|
|
700
|
-
const record = payload;
|
|
701
|
-
if (record.ok === true || record.ready === true || record.labelsReady === true)
|
|
702
|
-
return true;
|
|
703
|
-
if (record.ok === false || record.ready === false || record.labelsReady === false)
|
|
704
|
-
return false;
|
|
705
|
-
return null;
|
|
706
|
-
}
|
|
707
718
|
function prMergeCheck(config) {
|
|
708
719
|
const pr = config?.pr;
|
|
709
720
|
const merge = config?.merge;
|
|
@@ -717,89 +728,48 @@ async function runRigDoctorChecks(options) {
|
|
|
717
728
|
const checks = [];
|
|
718
729
|
const which = options.which ?? ((binary) => Bun.which(binary));
|
|
719
730
|
const bunVersion = options.bunVersion ?? Bun.version;
|
|
720
|
-
const request = options.requestJson ?? ((pathname, init) =>
|
|
731
|
+
const request = options.requestJson ?? ((pathname, init) => requestGitHubAuthJsonInProcess({ projectRoot }, pathname, init));
|
|
721
732
|
const loadConfig = options.loadConfig ?? loadRigConfigOrNull;
|
|
733
|
+
const progress = options.onProgress ?? (() => {});
|
|
734
|
+
progress("Checking local toolchain\u2026");
|
|
722
735
|
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`)."));
|
|
736
|
+
progress("Loading rig.config\u2026");
|
|
723
737
|
const loadedConfig = await loadConfig(projectRoot).catch(() => null);
|
|
724
738
|
const config = loadedConfig ?? loadFallbackConfig(projectRoot);
|
|
725
|
-
const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) =>
|
|
726
|
-
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", "
|
|
739
|
+
const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync3(resolve4(projectRoot, name)));
|
|
740
|
+
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", "Create/fix rig.config.ts or use the OMP cockpit Start tab to scaffold one."));
|
|
727
741
|
const taskSourceKind = config?.taskSource?.kind;
|
|
728
742
|
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."));
|
|
729
743
|
const repo = readRepoConnection(projectRoot);
|
|
730
|
-
checks.push(repo ? check("project-link", "repo selected
|
|
731
|
-
const selected = (() => {
|
|
732
|
-
try {
|
|
733
|
-
return resolveSelectedConnection(projectRoot);
|
|
734
|
-
} catch {
|
|
735
|
-
return null;
|
|
736
|
-
}
|
|
737
|
-
})();
|
|
738
|
-
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));
|
|
739
|
-
let server = null;
|
|
744
|
+
checks.push(repo ? check("project-link", "repo selected for local setup", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --repo owner/repo` if this project should use GitHub Issues.") : check("project-link", "repo selected for local setup", "warn", "missing .rig/state/connection.json", "Current UX can run without this compatibility file; `rig init` recreates it for GitHub-backed projects."));
|
|
740
745
|
try {
|
|
741
|
-
|
|
742
|
-
|
|
746
|
+
progress("Checking GitHub auth\u2026");
|
|
747
|
+
const auth = await request("/api/github/auth/status");
|
|
748
|
+
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>`."));
|
|
743
749
|
} catch (error) {
|
|
744
|
-
checks.push(check("
|
|
750
|
+
checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub through Rig."));
|
|
745
751
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
try {
|
|
754
|
-
const auth = await request("/api/github/auth/status");
|
|
755
|
-
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>`."));
|
|
756
|
-
} catch (error) {
|
|
757
|
-
checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub through Rig and ensure the server exposes auth status."));
|
|
758
|
-
}
|
|
759
|
-
try {
|
|
760
|
-
const permissions = await request("/api/github/repo/permissions");
|
|
761
|
-
const allowed = permissionAllowsPr(permissions);
|
|
762
|
-
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."));
|
|
763
|
-
} catch (error) {
|
|
764
|
-
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."));
|
|
765
|
-
}
|
|
766
|
-
try {
|
|
767
|
-
const labels = await request("/api/workspace/task-labels");
|
|
768
|
-
const ready = labelsReady(labels);
|
|
769
|
-
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."));
|
|
770
|
-
} catch (error) {
|
|
771
|
-
checks.push(check("task-labels", "GitHub issue labels", "warn", errorMessage(error), "Run `rig init`/`rig doctor` after label setup is wired on the server."));
|
|
772
|
-
}
|
|
773
|
-
try {
|
|
774
|
-
const projection = await request("/api/workspace/task-projection");
|
|
775
|
-
checks.push(check("task-projection", "task projection", "pass", JSON.stringify(projection).slice(0, 180)));
|
|
776
|
-
} catch (error) {
|
|
777
|
-
checks.push(check("task-projection", "task projection", "warn", errorMessage(error), "Refresh task projection with `rig task list` or fix the task source."));
|
|
778
|
-
}
|
|
779
|
-
const slug = projectStatusSlug(projectRoot, config);
|
|
780
|
-
if (slug) {
|
|
781
|
-
try {
|
|
782
|
-
const project = await request(`/api/projects/${encodeURIComponent(slug)}`);
|
|
783
|
-
checks.push(check("remote-checkout", "server project checkout", "pass", JSON.stringify(project).slice(0, 180)));
|
|
784
|
-
} catch (error) {
|
|
785
|
-
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."));
|
|
786
|
-
}
|
|
787
|
-
} else {
|
|
788
|
-
checks.push(check("remote-checkout", "server project checkout", "warn", "repo slug unknown", "Set project.repo or run `rig init --repo owner/repo`."));
|
|
789
|
-
}
|
|
752
|
+
try {
|
|
753
|
+
progress("Checking GitHub repo permissions\u2026");
|
|
754
|
+
const permissions = await request("/api/github/repo/permissions");
|
|
755
|
+
const allowed = permissionAllowsPr(permissions);
|
|
756
|
+
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."));
|
|
757
|
+
} catch (error) {
|
|
758
|
+
checks.push(check("github-repo-permissions", "GitHub repo PR permissions", "warn", errorMessage(error), "Ensure the token can open PRs."));
|
|
790
759
|
}
|
|
791
760
|
if (taskSourceKind === "github-issues") {
|
|
792
761
|
checks.push(check("gh", "gh CLI fallback", which("gh") ? "pass" : "warn", which("gh") ?? undefined, "Install gh for local/dev GitHub fallback operations."));
|
|
793
762
|
}
|
|
794
763
|
checks.push(githubProjectsCheck(config));
|
|
795
764
|
checks.push(prMergeCheck(config));
|
|
765
|
+
progress("Checking Pi installation\u2026");
|
|
796
766
|
const piChecks = await (options.piChecks ?? (() => buildPiSetupChecks()))().catch((error) => [{
|
|
797
767
|
ok: false,
|
|
798
768
|
label: "pi/pi-rig checks",
|
|
799
769
|
hint: errorMessage(error)
|
|
800
770
|
}]);
|
|
801
771
|
for (const pi of piChecks) {
|
|
802
|
-
checks.push(check(pi.label === "pi" ? "pi" : "pi-rig", pi.label, pi.ok ? "pass" : "warn", pi.detail, pi.hint ?? (pi.ok ? undefined : "
|
|
772
|
+
checks.push(check(pi.label === "pi" ? "pi" : "pi-rig", pi.label, pi.ok ? "pass" : "warn", pi.detail, pi.hint ?? (pi.ok ? undefined : "Install Pi/OMP and the Rig extension, then use bare `rig` or Cockpit \u2192 Doctor to verify.")));
|
|
803
773
|
}
|
|
804
774
|
return checks;
|
|
805
775
|
}
|
|
@@ -819,7 +789,7 @@ function parseRepoSlugFromRemote(remoteUrl) {
|
|
|
819
789
|
return gitHubMatch ? `${gitHubMatch[1]}/${gitHubMatch[2]}` : null;
|
|
820
790
|
}
|
|
821
791
|
function detectOriginRepoSlug(projectRoot) {
|
|
822
|
-
const result =
|
|
792
|
+
const result = spawnSync("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
|
|
823
793
|
if (result.status !== 0)
|
|
824
794
|
return null;
|
|
825
795
|
return parseRepoSlugFromRemote(result.stdout.trim());
|
|
@@ -827,24 +797,24 @@ function detectOriginRepoSlug(projectRoot) {
|
|
|
827
797
|
function parseRepoSlug(value) {
|
|
828
798
|
const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
829
799
|
if (!match)
|
|
830
|
-
throw new
|
|
800
|
+
throw new CliError(`Invalid GitHub repo slug: ${value}. Expected owner/repo.`, 1, { hint: "Pass the slug as owner/repo, e.g. `rig init --repo acme/widgets`." });
|
|
831
801
|
return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
|
|
832
802
|
}
|
|
833
803
|
function ensureRigPrivateDirs(projectRoot) {
|
|
834
|
-
const rigDir =
|
|
835
|
-
mkdirSync2(
|
|
836
|
-
mkdirSync2(
|
|
837
|
-
mkdirSync2(
|
|
838
|
-
mkdirSync2(
|
|
839
|
-
mkdirSync2(
|
|
840
|
-
const taskConfigPath =
|
|
841
|
-
if (!
|
|
804
|
+
const rigDir = resolve5(projectRoot, ".rig");
|
|
805
|
+
mkdirSync2(resolve5(rigDir, "state"), { recursive: true });
|
|
806
|
+
mkdirSync2(resolve5(rigDir, "logs"), { recursive: true });
|
|
807
|
+
mkdirSync2(resolve5(rigDir, "runs"), { recursive: true });
|
|
808
|
+
mkdirSync2(resolve5(rigDir, "tmp"), { recursive: true });
|
|
809
|
+
mkdirSync2(resolve5(projectRoot, "artifacts"), { recursive: true });
|
|
810
|
+
const taskConfigPath = resolve5(rigDir, "task-config.json");
|
|
811
|
+
if (!existsSync4(taskConfigPath))
|
|
842
812
|
writeFileSync2(taskConfigPath, `{}
|
|
843
813
|
`, "utf-8");
|
|
844
814
|
}
|
|
845
815
|
function ensureGitignoreEntries(projectRoot) {
|
|
846
|
-
const path =
|
|
847
|
-
const existing =
|
|
816
|
+
const path = resolve5(projectRoot, ".gitignore");
|
|
817
|
+
const existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
|
|
848
818
|
const entries = [".rig/state/", ".rig/logs/", ".rig/runs/", ".rig/tmp/"];
|
|
849
819
|
const missing = entries.filter((entry) => !existing.split(/\r?\n/).includes(entry));
|
|
850
820
|
if (missing.length === 0)
|
|
@@ -857,14 +827,14 @@ function ensureGitignoreEntries(projectRoot) {
|
|
|
857
827
|
`, "utf8");
|
|
858
828
|
}
|
|
859
829
|
function ensureRigConfigPackageDependencies(projectRoot) {
|
|
860
|
-
const path =
|
|
861
|
-
const existing =
|
|
830
|
+
const path = resolve5(projectRoot, "package.json");
|
|
831
|
+
const existing = existsSync4(path) ? JSON.parse(readFileSync3(path, "utf8")) : {};
|
|
862
832
|
const devDependencies = existing.devDependencies && typeof existing.devDependencies === "object" && !Array.isArray(existing.devDependencies) ? { ...existing.devDependencies } : {};
|
|
863
833
|
for (const [name, spec] of Object.entries(RIG_CONFIG_DEV_DEPENDENCIES)) {
|
|
864
834
|
devDependencies[name] = spec;
|
|
865
835
|
}
|
|
866
836
|
const next = {
|
|
867
|
-
...
|
|
837
|
+
...existsSync4(path) ? existing : { name: "rig-project", private: true },
|
|
868
838
|
devDependencies
|
|
869
839
|
};
|
|
870
840
|
writeFileSync2(path, `${JSON.stringify(next, null, 2)}
|
|
@@ -875,67 +845,243 @@ function applyGitHubProjectConfig(source, options) {
|
|
|
875
845
|
return source;
|
|
876
846
|
const projectId = JSON.stringify(options.githubProject);
|
|
877
847
|
const statusFieldId = JSON.stringify(options.githubProjectStatusField ?? "Status");
|
|
848
|
+
const statuses = options.githubProjectStatuses && Object.keys(options.githubProjectStatuses).length > 0 ? `
|
|
849
|
+
statuses: ${JSON.stringify(options.githubProjectStatuses, null, 8).replace(/\n/g, `
|
|
850
|
+
`)},` : "";
|
|
878
851
|
return source.replace(` projects: { enabled: false },`, [
|
|
879
852
|
` projects: {`,
|
|
880
853
|
` enabled: true,`,
|
|
881
854
|
` projectId: ${projectId},`,
|
|
882
|
-
` statusFieldId: ${statusFieldId}
|
|
855
|
+
` statusFieldId: ${statusFieldId},${statuses}`,
|
|
883
856
|
` },`
|
|
884
857
|
].join(`
|
|
885
858
|
`));
|
|
886
859
|
}
|
|
887
|
-
function checkoutForInit(projectRoot,
|
|
888
|
-
if (
|
|
889
|
-
return {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
return { kind: "current-ref", path: projectRoot, ...selected.ref ? { ref: selected.ref } : {} };
|
|
896
|
-
case "uploaded-snapshot":
|
|
897
|
-
return { kind: "uploaded-snapshot", path: projectRoot, source: "local-working-tree" };
|
|
898
|
-
case "existing-path":
|
|
899
|
-
return { kind: "existing-path", path: selected.path };
|
|
860
|
+
function checkoutForInit(projectRoot, options) {
|
|
861
|
+
if (options.server === "remote") {
|
|
862
|
+
return {
|
|
863
|
+
kind: "remote",
|
|
864
|
+
strategy: "existing-path",
|
|
865
|
+
path: options.remoteCheckout?.trim() || null,
|
|
866
|
+
alias: options.remoteAlias ?? options.remoteHost ?? null
|
|
867
|
+
};
|
|
900
868
|
}
|
|
869
|
+
return { kind: "local", path: projectRoot };
|
|
901
870
|
}
|
|
902
871
|
function detectGhLogin() {
|
|
903
|
-
const result =
|
|
872
|
+
const result = spawnSync("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
|
|
904
873
|
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
|
|
905
874
|
}
|
|
906
875
|
function readGhAuthToken() {
|
|
907
|
-
const result =
|
|
876
|
+
const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
|
|
908
877
|
if (result.status !== 0 || !result.stdout.trim()) {
|
|
909
|
-
throw new
|
|
878
|
+
throw new CliError(result.stderr.trim() || "Could not read GitHub token from `gh auth token`.", result.status || 1, { hint: "Sign in with `gh auth login`, or pass a token directly: `rig init --github-auth token --github-token <token>`." });
|
|
910
879
|
}
|
|
911
880
|
return result.stdout.trim();
|
|
912
881
|
}
|
|
882
|
+
function refreshGhProjectScopesAndReadToken() {
|
|
883
|
+
const result = spawnSync("gh", ["auth", "refresh", "--scopes", "read:project"], {
|
|
884
|
+
encoding: "utf8",
|
|
885
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
886
|
+
});
|
|
887
|
+
if (result.status !== 0)
|
|
888
|
+
return null;
|
|
889
|
+
try {
|
|
890
|
+
return readGhAuthToken();
|
|
891
|
+
} catch {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
var DRONE_CANCEL = Symbol("drone-cancel");
|
|
913
896
|
async function loadClackPrompts() {
|
|
914
|
-
|
|
897
|
+
const drone = await Promise.resolve().then(() => (init_drone_ui(), exports_drone_ui));
|
|
898
|
+
return {
|
|
899
|
+
intro: (message) => drone.droneIntro(message),
|
|
900
|
+
outro: (message) => drone.droneOutro(message),
|
|
901
|
+
cancel: (message) => drone.droneCancel(message),
|
|
902
|
+
isCancel: (value) => value === DRONE_CANCEL,
|
|
903
|
+
text: async (options) => {
|
|
904
|
+
const value = await drone.droneText({
|
|
905
|
+
message: options.message,
|
|
906
|
+
...options.placeholder ? { placeholder: options.placeholder } : {},
|
|
907
|
+
...(options.initialValue ?? options.defaultValue)?.trim() ? { initialValue: (options.initialValue ?? options.defaultValue).trim() } : {}
|
|
908
|
+
});
|
|
909
|
+
return value === null ? DRONE_CANCEL : value;
|
|
910
|
+
},
|
|
911
|
+
select: async (options) => {
|
|
912
|
+
const value = await drone.droneSelect({
|
|
913
|
+
message: options.message,
|
|
914
|
+
options: options.options,
|
|
915
|
+
...options.initialValue ? { initialValue: options.initialValue } : {}
|
|
916
|
+
});
|
|
917
|
+
return value === null ? DRONE_CANCEL : value;
|
|
918
|
+
},
|
|
919
|
+
confirm: async (options) => {
|
|
920
|
+
const value = await drone.droneConfirm(options);
|
|
921
|
+
return value === null ? DRONE_CANCEL : value;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
915
924
|
}
|
|
916
925
|
async function promptRequiredText(prompts, options) {
|
|
917
926
|
const value = await prompts.text(options);
|
|
918
927
|
if (prompts.isCancel(value))
|
|
919
|
-
throw new
|
|
928
|
+
throw new CliError("Init cancelled.", 1);
|
|
920
929
|
const text = String(value ?? "").trim();
|
|
921
930
|
if (!text)
|
|
922
|
-
throw new
|
|
931
|
+
throw new CliError(`${options.message} is required.`, 1);
|
|
923
932
|
return text;
|
|
924
933
|
}
|
|
925
934
|
async function promptOptionalText(prompts, options) {
|
|
926
935
|
const value = await prompts.text(options);
|
|
927
936
|
if (prompts.isCancel(value))
|
|
928
|
-
throw new
|
|
937
|
+
throw new CliError("Init cancelled.", 1);
|
|
929
938
|
return String(value ?? "").trim();
|
|
930
939
|
}
|
|
931
940
|
async function promptSelect(prompts, options) {
|
|
932
941
|
const value = await prompts.select(options);
|
|
933
942
|
if (prompts.isCancel(value))
|
|
934
|
-
throw new
|
|
943
|
+
throw new CliError("Init cancelled.", 1);
|
|
935
944
|
return String(value);
|
|
936
945
|
}
|
|
937
|
-
function
|
|
938
|
-
return
|
|
946
|
+
function repoOwnerFromSlug(repoSlug) {
|
|
947
|
+
return repoSlug.trim().match(/^([^/]+)\/[^/]+$/)?.[1] ?? null;
|
|
948
|
+
}
|
|
949
|
+
function recordArray(value, key) {
|
|
950
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
951
|
+
return [];
|
|
952
|
+
const raw = value[key];
|
|
953
|
+
return Array.isArray(raw) ? raw.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
954
|
+
}
|
|
955
|
+
async function listGitHubProjectsForInit(context, owner, token, serverBaseUrl) {
|
|
956
|
+
return listGitHubProjectsInProcess(context, owner, { authToken: token, baseUrl: serverBaseUrl });
|
|
957
|
+
}
|
|
958
|
+
async function getGitHubProjectStatusFieldForInit(context, projectId, token, serverBaseUrl) {
|
|
959
|
+
return getGitHubProjectStatusFieldInProcess(context, projectId, { authToken: token, baseUrl: serverBaseUrl });
|
|
960
|
+
}
|
|
961
|
+
var PROJECT_STATUS_PROMPTS = {
|
|
962
|
+
running: "Running/In progress",
|
|
963
|
+
prOpen: "PR open/review",
|
|
964
|
+
ciFixing: "CI/review fixing",
|
|
965
|
+
merging: "Merging",
|
|
966
|
+
done: "Done",
|
|
967
|
+
needsAttention: "Needs attention"
|
|
968
|
+
};
|
|
969
|
+
var DEFAULT_PROJECT_STATUS_OPTIONS = {
|
|
970
|
+
running: "In Progress",
|
|
971
|
+
prOpen: "In Review",
|
|
972
|
+
ciFixing: "In Review",
|
|
973
|
+
merging: "Merging",
|
|
974
|
+
done: "Done",
|
|
975
|
+
needsAttention: "Needs Attention"
|
|
976
|
+
};
|
|
977
|
+
async function promptManualProjectStatusMapping(prompts) {
|
|
978
|
+
const statuses = {};
|
|
979
|
+
for (const [key, label] of Object.entries(PROJECT_STATUS_PROMPTS)) {
|
|
980
|
+
const defaultLabel = DEFAULT_PROJECT_STATUS_OPTIONS[key] ?? label;
|
|
981
|
+
const value = await promptOptionalText(prompts, {
|
|
982
|
+
message: `Project status option id/name for ${label} (blank for ${defaultLabel})`,
|
|
983
|
+
placeholder: defaultLabel
|
|
984
|
+
});
|
|
985
|
+
statuses[key] = value || defaultLabel;
|
|
986
|
+
}
|
|
987
|
+
return statuses;
|
|
988
|
+
}
|
|
989
|
+
function projectScopeError(value) {
|
|
990
|
+
const text = typeof value === "string" ? value : JSON.stringify(value ?? "");
|
|
991
|
+
return /INSUFFICIENT_SCOPES|read:project|required scopes/i.test(text);
|
|
992
|
+
}
|
|
993
|
+
function optionName(option) {
|
|
994
|
+
return String(option.name ?? option.label ?? option.id ?? "").trim();
|
|
995
|
+
}
|
|
996
|
+
function autoProjectStatusValue(options, key, label) {
|
|
997
|
+
const candidates = [DEFAULT_PROJECT_STATUS_OPTIONS[key], label].filter((value) => Boolean(value)).map((value) => value.trim().toLowerCase());
|
|
998
|
+
const match = options.find((option) => candidates.includes(optionName(option).toLowerCase()));
|
|
999
|
+
if (!match)
|
|
1000
|
+
return null;
|
|
1001
|
+
return String(match.id ?? match.name);
|
|
1002
|
+
}
|
|
1003
|
+
async function promptGitHubProjectConfig(context, prompts, repoSlug, githubToken, refreshProjectToken, serverBaseUrl) {
|
|
1004
|
+
const projectChoice = await promptSelect(prompts, {
|
|
1005
|
+
message: "GitHub Projects status sync",
|
|
1006
|
+
initialValue: "select",
|
|
1007
|
+
options: [
|
|
1008
|
+
{ value: "select", label: "Select accessible ProjectV2" },
|
|
1009
|
+
{ value: "off", label: "Off" },
|
|
1010
|
+
{ value: "manual", label: "Enter ProjectV2 ids manually" }
|
|
1011
|
+
]
|
|
1012
|
+
});
|
|
1013
|
+
if (projectChoice === "off")
|
|
1014
|
+
return { githubProject: "off" };
|
|
1015
|
+
if (projectChoice === "manual") {
|
|
1016
|
+
return {
|
|
1017
|
+
githubProject: await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }),
|
|
1018
|
+
githubProjectStatusField: await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }),
|
|
1019
|
+
githubProjectStatuses: await promptManualProjectStatusMapping(prompts)
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const owner = repoOwnerFromSlug(repoSlug);
|
|
1023
|
+
if (!owner)
|
|
1024
|
+
throw new CliError(`Cannot derive GitHub owner from repo slug ${repoSlug}.`, 1, { hint: "Pass the slug as owner/repo, e.g. `rig init --repo acme/widgets`." });
|
|
1025
|
+
let activeToken = githubToken?.trim() || null;
|
|
1026
|
+
let projectsPayload = await listGitHubProjectsForInit(context, owner, activeToken, serverBaseUrl).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
|
|
1027
|
+
let projects = recordArray(projectsPayload, "projects");
|
|
1028
|
+
if (projects.length === 0 && projectScopeError(projectsPayload.error) && refreshProjectToken) {
|
|
1029
|
+
prompts.outro?.("GitHub token is missing read:project; refreshing gh auth scopes and retrying Projects.");
|
|
1030
|
+
const refreshedToken = refreshProjectToken();
|
|
1031
|
+
if (refreshedToken) {
|
|
1032
|
+
activeToken = refreshedToken;
|
|
1033
|
+
projectsPayload = await listGitHubProjectsForInit(context, owner, activeToken, serverBaseUrl).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), projects: [] }));
|
|
1034
|
+
projects = recordArray(projectsPayload, "projects");
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (projects.length === 0) {
|
|
1038
|
+
const error = typeof projectsPayload.error === "string" ? ` (${String(projectsPayload.error).replace(/\s+/g, " ").slice(0, 240)})` : "";
|
|
1039
|
+
prompts.outro?.(`No accessible GitHub Projects were returned${error}; continuing with GitHub Projects status sync off.`);
|
|
1040
|
+
return { githubProject: "off", ...activeToken ? { githubToken: activeToken } : {} };
|
|
1041
|
+
}
|
|
1042
|
+
const selectedProjectId = await promptSelect(prompts, {
|
|
1043
|
+
message: "GitHub ProjectV2 project",
|
|
1044
|
+
options: [
|
|
1045
|
+
...projects.map((project) => ({
|
|
1046
|
+
value: String(project.id),
|
|
1047
|
+
label: `${String(project.title ?? "Untitled project")} (#${String(project.number ?? "?")})`,
|
|
1048
|
+
hint: typeof project.url === "string" ? project.url : undefined
|
|
1049
|
+
})),
|
|
1050
|
+
{ value: "manual", label: "Enter ProjectV2 id manually" }
|
|
1051
|
+
]
|
|
1052
|
+
});
|
|
1053
|
+
const projectId = selectedProjectId === "manual" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : selectedProjectId;
|
|
1054
|
+
const fieldPayload = await getGitHubProjectStatusFieldForInit(context, projectId, activeToken, serverBaseUrl).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }));
|
|
1055
|
+
const fieldPayloadRecord = fieldPayload && typeof fieldPayload === "object" && !Array.isArray(fieldPayload) ? fieldPayload : {};
|
|
1056
|
+
const rawField = fieldPayloadRecord.field;
|
|
1057
|
+
const field = rawField && typeof rawField === "object" && !Array.isArray(rawField) ? rawField : null;
|
|
1058
|
+
const fieldId = typeof field?.id === "string" && field.id.trim() ? field.id : await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" });
|
|
1059
|
+
const options = Array.isArray(field?.options) ? field.options.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) : [];
|
|
1060
|
+
if (options.length === 0) {
|
|
1061
|
+
return {
|
|
1062
|
+
githubProject: projectId,
|
|
1063
|
+
githubProjectStatusField: fieldId,
|
|
1064
|
+
githubProjectStatuses: await promptManualProjectStatusMapping(prompts),
|
|
1065
|
+
...activeToken ? { githubToken: activeToken } : {}
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const statuses = {};
|
|
1069
|
+
for (const [key, label] of Object.entries(PROJECT_STATUS_PROMPTS)) {
|
|
1070
|
+
const auto = autoProjectStatusValue(options, key, label);
|
|
1071
|
+
statuses[key] = auto ?? await promptSelect(prompts, {
|
|
1072
|
+
message: `Project status option for ${label}`,
|
|
1073
|
+
options: options.map((option) => ({ value: String(option.id ?? option.name), label: optionName(option) }))
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
githubProject: projectId,
|
|
1078
|
+
githubProjectStatusField: fieldId,
|
|
1079
|
+
githubProjectStatuses: Object.keys(statuses).length > 0 ? statuses : undefined,
|
|
1080
|
+
...activeToken ? { githubToken: activeToken } : {}
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function sleep(ms) {
|
|
1084
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
939
1085
|
}
|
|
940
1086
|
function positiveIntFromEnv(name, fallback) {
|
|
941
1087
|
const value = Number.parseInt(process.env[name] ?? "", 10);
|
|
@@ -947,17 +1093,6 @@ function apiSessionTokenFrom(payload) {
|
|
|
947
1093
|
const token = payload.apiSessionToken;
|
|
948
1094
|
return typeof token === "string" && token.trim() ? token.trim() : null;
|
|
949
1095
|
}
|
|
950
|
-
function writeRemoteGitHubAuthState(projectRoot, input) {
|
|
951
|
-
writeFileSync2(resolve6(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({
|
|
952
|
-
authenticated: true,
|
|
953
|
-
source: input.source,
|
|
954
|
-
storedOnServer: true,
|
|
955
|
-
selectedRepo: input.selectedRepo,
|
|
956
|
-
...input.apiSessionToken ? { apiSessionToken: input.apiSessionToken } : {},
|
|
957
|
-
updatedAt: new Date().toISOString()
|
|
958
|
-
}, null, 2)}
|
|
959
|
-
`, "utf8");
|
|
960
|
-
}
|
|
961
1096
|
async function pollDeviceAuthUntilComplete(context, pollId, firstPayload) {
|
|
962
1097
|
if (typeof pollId !== "string" || !pollId.trim())
|
|
963
1098
|
return null;
|
|
@@ -967,7 +1102,7 @@ async function pollDeviceAuthUntilComplete(context, pollId, firstPayload) {
|
|
|
967
1102
|
const deadline = Date.now() + timeoutMs;
|
|
968
1103
|
let last = null;
|
|
969
1104
|
do {
|
|
970
|
-
const payload = await
|
|
1105
|
+
const payload = await requestGitHubAuthJsonInProcess(context, "/api/github/auth/device/poll", {
|
|
971
1106
|
method: "POST",
|
|
972
1107
|
headers: { "content-type": "application/json" },
|
|
973
1108
|
body: JSON.stringify({ pollId })
|
|
@@ -979,34 +1114,174 @@ async function pollDeviceAuthUntilComplete(context, pollId, firstPayload) {
|
|
|
979
1114
|
}
|
|
980
1115
|
if (timeoutMs <= 0)
|
|
981
1116
|
return last;
|
|
982
|
-
await
|
|
1117
|
+
await sleep(intervalMs);
|
|
983
1118
|
} while (Date.now() < deadline);
|
|
984
1119
|
return last;
|
|
985
1120
|
}
|
|
1121
|
+
function runLocalFilesInit(context, options) {
|
|
1122
|
+
const projectRoot = context.projectRoot;
|
|
1123
|
+
ensureRigPrivateDirs(projectRoot);
|
|
1124
|
+
ensureGitignoreEntries(projectRoot);
|
|
1125
|
+
writeRepoConnection(projectRoot, { selected: "local", linkedAt: new Date().toISOString() });
|
|
1126
|
+
const configTsPath = resolve5(projectRoot, "rig.config.ts");
|
|
1127
|
+
const configExists = existsSync4(configTsPath) || existsSync4(resolve5(projectRoot, "rig.config.json"));
|
|
1128
|
+
if (configExists && !options.repair) {
|
|
1129
|
+
if (context.outputMode !== "json")
|
|
1130
|
+
console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it.");
|
|
1131
|
+
} else {
|
|
1132
|
+
const projectName = basename(projectRoot) || "rig-project";
|
|
1133
|
+
writeFileSync2(configTsPath, buildRigInitConfigSource({
|
|
1134
|
+
projectName,
|
|
1135
|
+
taskSource: { kind: "files", path: "tasks" },
|
|
1136
|
+
useStandardPlugin: true
|
|
1137
|
+
}), "utf-8");
|
|
1138
|
+
}
|
|
1139
|
+
ensureRigConfigPackageDependencies(projectRoot);
|
|
1140
|
+
const tasksDir = resolve5(projectRoot, "tasks");
|
|
1141
|
+
if (!existsSync4(tasksDir)) {
|
|
1142
|
+
mkdirSync2(tasksDir, { recursive: true });
|
|
1143
|
+
writeFileSync2(resolve5(tasksDir, "T-1.json"), `${JSON.stringify({ id: "T-1", title: "My first Rig task", body: "Describe the change you want an agent to make." }, null, 2)}
|
|
1144
|
+
`, "utf-8");
|
|
1145
|
+
}
|
|
1146
|
+
if (context.outputMode !== "json") {
|
|
1147
|
+
console.log("Initialized a local files-source Rig project (no GitHub).");
|
|
1148
|
+
console.log(" tasks live in tasks/*.json \xB7 server: local");
|
|
1149
|
+
console.log("Next: `rig task list`, then `rig task run T-1`.");
|
|
1150
|
+
console.log("To wire GitHub later: `rig init --repair --repo owner/repo --github-auth gh`.");
|
|
1151
|
+
}
|
|
1152
|
+
return { ok: true, group: "init", command: "init", details: { mode: "local-files", projectRoot, taskSourcePath: "tasks" } };
|
|
1153
|
+
}
|
|
1154
|
+
var DEMO_TASKS_RELATIVE_DIR = ".rig/demo-tasks";
|
|
1155
|
+
var DEMO_TASKS = [
|
|
1156
|
+
{
|
|
1157
|
+
id: "demo-1",
|
|
1158
|
+
title: "Add a hello CLI script",
|
|
1159
|
+
body: [
|
|
1160
|
+
"Create `scripts/hello.ts` that prints `Hello from Rig!` plus the current date,",
|
|
1161
|
+
"and add a `hello` script entry to package.json that runs it with bun.",
|
|
1162
|
+
"Keep it dependency-free."
|
|
1163
|
+
].join(`
|
|
1164
|
+
`),
|
|
1165
|
+
status: "ready",
|
|
1166
|
+
labels: ["demo"]
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
id: "demo-2",
|
|
1170
|
+
title: "Write a README section about this project",
|
|
1171
|
+
body: [
|
|
1172
|
+
"Add (or extend) README.md with a short `## What this is` section:",
|
|
1173
|
+
"two or three sentences describing the repository and how to run it.",
|
|
1174
|
+
"Plain prose, no badges."
|
|
1175
|
+
].join(`
|
|
1176
|
+
`),
|
|
1177
|
+
status: "ready",
|
|
1178
|
+
labels: ["demo"]
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
id: "demo-3",
|
|
1182
|
+
title: "Add a unit test for the hello script",
|
|
1183
|
+
body: [
|
|
1184
|
+
"Add `scripts/hello.test.ts` with a bun test that imports the greeting",
|
|
1185
|
+
"helper from the hello script and asserts it contains `Hello from Rig!`.",
|
|
1186
|
+
"Refactor the script to export that helper if needed."
|
|
1187
|
+
].join(`
|
|
1188
|
+
`),
|
|
1189
|
+
status: "ready",
|
|
1190
|
+
labels: ["demo"]
|
|
1191
|
+
}
|
|
1192
|
+
];
|
|
1193
|
+
function runDemoInit(context, options) {
|
|
1194
|
+
const projectRoot = context.projectRoot;
|
|
1195
|
+
ensureRigPrivateDirs(projectRoot);
|
|
1196
|
+
ensureGitignoreEntries(projectRoot);
|
|
1197
|
+
writeRepoConnection(projectRoot, { selected: "local", linkedAt: new Date().toISOString() });
|
|
1198
|
+
const configTsPath = resolve5(projectRoot, "rig.config.ts");
|
|
1199
|
+
const configExists = existsSync4(configTsPath) || existsSync4(resolve5(projectRoot, "rig.config.json"));
|
|
1200
|
+
let configWritten = false;
|
|
1201
|
+
if (configExists && !options.repair) {
|
|
1202
|
+
if (context.outputMode !== "json") {
|
|
1203
|
+
console.log("rig.config already exists; leaving it unchanged. Pass --repair to rewrite it for the demo.");
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
writeFileSync2(configTsPath, buildRigInitConfigSource({
|
|
1207
|
+
projectName: basename(projectRoot) || "rig-demo",
|
|
1208
|
+
taskSource: { kind: "files", path: DEMO_TASKS_RELATIVE_DIR },
|
|
1209
|
+
useStandardPlugin: true
|
|
1210
|
+
}), "utf-8");
|
|
1211
|
+
configWritten = true;
|
|
1212
|
+
}
|
|
1213
|
+
ensureRigConfigPackageDependencies(projectRoot);
|
|
1214
|
+
const demoTasksDir = resolve5(projectRoot, DEMO_TASKS_RELATIVE_DIR);
|
|
1215
|
+
mkdirSync2(demoTasksDir, { recursive: true });
|
|
1216
|
+
const taskIds = [];
|
|
1217
|
+
for (const task of DEMO_TASKS) {
|
|
1218
|
+
const id = String(task.id);
|
|
1219
|
+
taskIds.push(id);
|
|
1220
|
+
const taskPath = resolve5(demoTasksDir, `${id}.json`);
|
|
1221
|
+
if (!existsSync4(taskPath)) {
|
|
1222
|
+
writeFileSync2(taskPath, `${JSON.stringify(task, null, 2)}
|
|
1223
|
+
`, "utf-8");
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (context.outputMode !== "json") {
|
|
1227
|
+
console.log(`Demo Rig project ready (offline, no GitHub).`);
|
|
1228
|
+
console.log(` config: rig.config.ts (files task source -> ${DEMO_TASKS_RELATIVE_DIR}/)`);
|
|
1229
|
+
console.log(` tasks: ${taskIds.join(", ")}`);
|
|
1230
|
+
console.log("Next steps:");
|
|
1231
|
+
console.log(" 1. run bare `rig`");
|
|
1232
|
+
console.log(" 2. use the OMP cockpit Start screen");
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
ok: true,
|
|
1236
|
+
group: "init",
|
|
1237
|
+
command: "init",
|
|
1238
|
+
details: {
|
|
1239
|
+
mode: "demo",
|
|
1240
|
+
projectRoot,
|
|
1241
|
+
taskSourcePath: DEMO_TASKS_RELATIVE_DIR,
|
|
1242
|
+
demoTasksDir,
|
|
1243
|
+
tasks: taskIds,
|
|
1244
|
+
configWritten
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
986
1248
|
async function runControlPlaneInit(context, options) {
|
|
987
1249
|
const projectRoot = context.projectRoot;
|
|
988
|
-
const
|
|
1250
|
+
const existingRepoConnection = readRepoConnection(projectRoot);
|
|
1251
|
+
const detectedSlug = options.repoSlug ?? existingRepoConnection?.project ?? detectOriginRepoSlug(projectRoot);
|
|
989
1252
|
if (!detectedSlug) {
|
|
990
|
-
|
|
1253
|
+
const authMethod2 = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
|
|
1254
|
+
if (authMethod2 === "skip") {
|
|
1255
|
+
return runLocalFilesInit(context, options);
|
|
1256
|
+
}
|
|
1257
|
+
throw new CliError("Could not detect GitHub repo slug from origin. Current UX starts from bare `rig`; pass --repo owner/repo or use `rig init --yes --github-auth skip` for a local files-source project without GitHub.", 1);
|
|
991
1258
|
}
|
|
992
1259
|
const repo = parseRepoSlug(detectedSlug);
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
if (
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1260
|
+
const placement = options.server ?? "local";
|
|
1261
|
+
let connectionAlias = "local";
|
|
1262
|
+
if (placement === "remote") {
|
|
1263
|
+
const host = (options.remoteHost ?? "").trim();
|
|
1264
|
+
const alias = (options.remoteAlias ?? host).trim();
|
|
1265
|
+
if (!host || !alias) {
|
|
1266
|
+
throw new CliError("Remote placement needs --remote-host <host>.", 1, {
|
|
1267
|
+
hint: "rig init --server remote --remote-host rig.example.com [--remote-alias prod] [--remote-port 22] [--remote-checkout /srv/repo]"
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
upsertManagedRemoteEndpoint({ alias, host, port: options.remotePort ?? 22 }, undefined, projectRoot);
|
|
1271
|
+
connectionAlias = alias;
|
|
999
1272
|
}
|
|
1273
|
+
const remoteCheckoutPath = placement === "remote" ? options.remoteCheckout?.trim() || null : null;
|
|
1000
1274
|
writeRepoConnection(projectRoot, {
|
|
1001
1275
|
selected: connectionAlias,
|
|
1002
1276
|
project: repo.slug,
|
|
1003
|
-
linkedAt: new Date().toISOString()
|
|
1277
|
+
linkedAt: new Date().toISOString(),
|
|
1278
|
+
...remoteCheckoutPath ? { serverProjectRoot: remoteCheckoutPath, serverProjectRootAlias: connectionAlias } : {}
|
|
1004
1279
|
});
|
|
1005
1280
|
ensureRigPrivateDirs(projectRoot);
|
|
1006
1281
|
ensureGitignoreEntries(projectRoot);
|
|
1007
|
-
const configTsPath =
|
|
1008
|
-
const configJsonPath =
|
|
1009
|
-
const configExists =
|
|
1282
|
+
const configTsPath = resolve5(projectRoot, "rig.config.ts");
|
|
1283
|
+
const configJsonPath = resolve5(projectRoot, "rig.config.json");
|
|
1284
|
+
const configExists = existsSync4(configTsPath) || existsSync4(configJsonPath);
|
|
1010
1285
|
if (!options.privateStateOnly) {
|
|
1011
1286
|
if (configExists && !options.repair) {
|
|
1012
1287
|
if (context.outputMode !== "json")
|
|
@@ -1022,46 +1297,26 @@ async function runControlPlaneInit(context, options) {
|
|
|
1022
1297
|
}
|
|
1023
1298
|
ensureRigConfigPackageDependencies(projectRoot);
|
|
1024
1299
|
}
|
|
1025
|
-
writeFileSync2(
|
|
1300
|
+
writeFileSync2(resolve5(projectRoot, ".rig", "state", "project-link.json"), `${JSON.stringify({ repoSlug: repo.slug, connection: connectionAlias, linkedAt: new Date().toISOString() }, null, 2)}
|
|
1026
1301
|
`, "utf8");
|
|
1027
|
-
const checkout = checkoutForInit(projectRoot,
|
|
1028
|
-
let uploadedSnapshot = null;
|
|
1029
|
-
if (serverKind === "remote" && options.remoteCheckout?.kind === "uploaded-snapshot") {
|
|
1030
|
-
const archive = await createSnapshotUploadArchive(projectRoot);
|
|
1031
|
-
uploadedSnapshot = await uploadSnapshotArchiveViaServer(context, { repoSlug: repo.slug, archive });
|
|
1032
|
-
const uploadedCheckout = uploadedSnapshot.checkout;
|
|
1033
|
-
if (uploadedCheckout && typeof uploadedCheckout === "object" && !Array.isArray(uploadedCheckout)) {
|
|
1034
|
-
Object.assign(checkout, uploadedCheckout);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1302
|
+
const checkout = checkoutForInit(projectRoot, options);
|
|
1037
1303
|
let githubAuth = null;
|
|
1038
1304
|
let deviceAuth = null;
|
|
1039
1305
|
const authMethod = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
|
|
1040
|
-
const remoteGhTokenWarning = serverKind === "remote" && authMethod === "gh" ? `This sends a GitHub token from this machine to ${options.remoteUrl ?? "the remote Rig server"}.` : null;
|
|
1041
|
-
if (remoteGhTokenWarning && !options.yes) {
|
|
1042
|
-
throw new CliError2(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
|
|
1043
|
-
}
|
|
1044
1306
|
const token = authMethod === "gh" && !options.githubToken ? readGhAuthToken() : options.githubToken?.trim();
|
|
1045
1307
|
if (token) {
|
|
1046
|
-
githubAuth = await
|
|
1308
|
+
githubAuth = await postGitHubTokenInProcess(context, token, { selectedRepo: repo.slug });
|
|
1047
1309
|
const apiSessionToken = apiSessionTokenFrom(githubAuth);
|
|
1048
|
-
setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
|
|
1049
|
-
if (serverKind === "remote") {
|
|
1050
|
-
writeRemoteGitHubAuthState(projectRoot, {
|
|
1051
|
-
source: authMethod === "gh" ? "gh" : "init-token",
|
|
1052
|
-
selectedRepo: repo.slug,
|
|
1053
|
-
apiSessionToken
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1310
|
+
setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token, projectRoot);
|
|
1056
1311
|
} else if (authMethod === "device") {
|
|
1057
|
-
const payload = await
|
|
1312
|
+
const payload = await requestGitHubAuthJsonInProcess(context, "/api/github/auth/device/start", {
|
|
1058
1313
|
method: "POST",
|
|
1059
1314
|
headers: { "content-type": "application/json" },
|
|
1060
1315
|
body: JSON.stringify({ repoSlug: repo.slug })
|
|
1061
1316
|
});
|
|
1062
1317
|
deviceAuth = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
1063
1318
|
if (context.outputMode !== "json") {
|
|
1064
|
-
const verificationUri = String(deviceAuth.verificationUri ?? deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by
|
|
1319
|
+
const verificationUri = String(deviceAuth.verificationUri ?? deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by GitHub");
|
|
1065
1320
|
const userCode = String(deviceAuth.userCode ?? deviceAuth.user_code ?? "the returned user code");
|
|
1066
1321
|
console.log(`GitHub device flow: open ${verificationUri} and enter ${userCode}. Waiting for authorization...`);
|
|
1067
1322
|
}
|
|
@@ -1069,45 +1324,19 @@ async function runControlPlaneInit(context, options) {
|
|
|
1069
1324
|
if (completed) {
|
|
1070
1325
|
const apiSessionToken = apiSessionTokenFrom(completed);
|
|
1071
1326
|
if (apiSessionToken) {
|
|
1072
|
-
setGitHubBearerTokenForCurrentProcess(apiSessionToken);
|
|
1073
|
-
if (serverKind === "remote") {
|
|
1074
|
-
writeRemoteGitHubAuthState(projectRoot, { source: "device", selectedRepo: repo.slug, apiSessionToken });
|
|
1075
|
-
}
|
|
1327
|
+
setGitHubBearerTokenForCurrentProcess(apiSessionToken, projectRoot);
|
|
1076
1328
|
}
|
|
1077
1329
|
deviceAuth = { ...deviceAuth, poll: completed, completed: completed.status === "signed-in" };
|
|
1078
1330
|
}
|
|
1079
1331
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
if (preparedCheckout && typeof preparedCheckout === "object" && !Array.isArray(preparedCheckout)) {
|
|
1089
|
-
Object.assign(checkout, preparedCheckout);
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
const registered = await registerProjectViaServer(context, {
|
|
1093
|
-
repoSlug: repo.slug,
|
|
1094
|
-
checkout
|
|
1095
|
-
});
|
|
1096
|
-
const checkoutPath = typeof checkout.path === "string" ? checkout.path : null;
|
|
1097
|
-
const serverRootSwitch = serverKind === "remote" && checkoutPath ? await switchServerProjectRootViaServer(context, checkoutPath) : null;
|
|
1098
|
-
if (serverRootSwitch && token) {
|
|
1099
|
-
githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug, projectRoot: checkoutPath ?? undefined });
|
|
1100
|
-
const apiSessionToken = apiSessionTokenFrom(githubAuth);
|
|
1101
|
-
setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token);
|
|
1102
|
-
writeRemoteGitHubAuthState(projectRoot, { source: authMethod === "gh" ? "gh" : "init-token", selectedRepo: repo.slug, apiSessionToken });
|
|
1103
|
-
}
|
|
1104
|
-
const activeProjectRegistration = serverRootSwitch ? await registerProjectViaServer(context, { repoSlug: repo.slug, checkout }) : null;
|
|
1105
|
-
const pi = serverKind === "remote" ? await ensureRemotePiRigInstalled({ requestJson: (pathname, init) => requestServerJson(context, pathname, init) }).catch((error) => ({
|
|
1106
|
-
remote: true,
|
|
1107
|
-
pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
|
|
1108
|
-
piRig: { ok: false, label: "pi-rig global extension", hint: "Remote server did not complete pi-rig installation." },
|
|
1109
|
-
extensionPath: "remote:~/.pi/agent/extensions/pi-rig"
|
|
1110
|
-
})) : await ensurePiRigInstalled({ projectRoot, homeDir: process.env.RIG_PI_HOME_DIR }).catch((error) => ({
|
|
1332
|
+
const labelSetup = {
|
|
1333
|
+
ok: false,
|
|
1334
|
+
ready: false,
|
|
1335
|
+
labelsReady: false,
|
|
1336
|
+
skipped: true,
|
|
1337
|
+
reason: "retired-server-label-bootstrap"
|
|
1338
|
+
};
|
|
1339
|
+
const pi = await ensurePiRigInstalled({ projectRoot, homeDir: process.env.RIG_PI_HOME_DIR }).catch((error) => ({
|
|
1111
1340
|
pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
|
|
1112
1341
|
piRig: { ok: false, label: "pi-rig global extension", hint: "Local pi-rig installation failed." },
|
|
1113
1342
|
extensionPath: null,
|
|
@@ -1120,41 +1349,32 @@ async function runControlPlaneInit(context, options) {
|
|
|
1120
1349
|
}));
|
|
1121
1350
|
const details = {
|
|
1122
1351
|
repoSlug: repo.slug,
|
|
1123
|
-
server:
|
|
1352
|
+
server: placement,
|
|
1124
1353
|
connection: connectionAlias,
|
|
1125
1354
|
githubProject: options.githubProject ?? "off",
|
|
1126
1355
|
checkout,
|
|
1127
|
-
remoteCheckoutPreparation,
|
|
1128
|
-
uploadedSnapshot,
|
|
1129
|
-
projectRegistration: registered,
|
|
1130
|
-
activeProjectRegistration,
|
|
1131
|
-
serverRootSwitch,
|
|
1132
1356
|
githubAuth,
|
|
1133
1357
|
deviceAuth,
|
|
1134
|
-
|
|
1358
|
+
labelSetup,
|
|
1135
1359
|
pi,
|
|
1136
1360
|
doctor
|
|
1137
1361
|
};
|
|
1138
1362
|
if (context.outputMode === "json")
|
|
1139
1363
|
console.log(JSON.stringify(details, null, 2));
|
|
1140
1364
|
else
|
|
1141
|
-
console.log(`Initialized Rig control-plane project ${repo.slug}. Next: rig doctor && rig task list`);
|
|
1365
|
+
console.log(`Initialized Rig control-plane project ${repo.slug} (${placement} placement). Next: rig doctor && rig task list`);
|
|
1142
1366
|
return { ok: true, group: "init", command: "init", details };
|
|
1143
1367
|
}
|
|
1144
1368
|
function parseInitOptions(args) {
|
|
1145
1369
|
let rest = [...args];
|
|
1370
|
+
const demo = takeFlag(rest, "--demo");
|
|
1371
|
+
rest = demo.rest;
|
|
1146
1372
|
const yes = takeFlag(rest, "--yes");
|
|
1147
1373
|
rest = yes.rest;
|
|
1148
1374
|
const repair = takeFlag(rest, "--repair");
|
|
1149
1375
|
rest = repair.rest;
|
|
1150
1376
|
const privateStateOnly = takeFlag(rest, "--private-state-only");
|
|
1151
1377
|
rest = privateStateOnly.rest;
|
|
1152
|
-
const server = takeOption(rest, "--server");
|
|
1153
|
-
rest = server.rest;
|
|
1154
|
-
const remoteUrl = takeOption(rest, "--remote-url");
|
|
1155
|
-
rest = remoteUrl.rest;
|
|
1156
|
-
const connectionAlias = takeOption(rest, "--connection");
|
|
1157
|
-
rest = connectionAlias.rest;
|
|
1158
1378
|
const repoSlug = takeOption(rest, "--repo");
|
|
1159
1379
|
rest = repoSlug.rest;
|
|
1160
1380
|
const githubToken = takeOption(rest, "--github-token");
|
|
@@ -1165,52 +1385,51 @@ function parseInitOptions(args) {
|
|
|
1165
1385
|
rest = githubProjectStatusField.rest;
|
|
1166
1386
|
const githubAuth = takeOption(rest, "--github-auth");
|
|
1167
1387
|
rest = githubAuth.rest;
|
|
1388
|
+
const server = takeOption(rest, "--server");
|
|
1389
|
+
rest = server.rest;
|
|
1390
|
+
const remoteHost = takeOption(rest, "--remote-host");
|
|
1391
|
+
rest = remoteHost.rest;
|
|
1392
|
+
const remoteUrl = takeOption(rest, "--remote-url");
|
|
1393
|
+
rest = remoteUrl.rest;
|
|
1394
|
+
const remoteAlias = takeOption(rest, "--remote-alias");
|
|
1395
|
+
rest = remoteAlias.rest;
|
|
1396
|
+
const remotePort = takeOption(rest, "--remote-port");
|
|
1397
|
+
rest = remotePort.rest;
|
|
1168
1398
|
const remoteCheckout = takeOption(rest, "--remote-checkout");
|
|
1169
1399
|
rest = remoteCheckout.rest;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1400
|
+
if (server.value && !["local", "remote"].includes(server.value)) {
|
|
1401
|
+
throw new CliError("--server must be local or remote.", 1);
|
|
1402
|
+
}
|
|
1403
|
+
if (githubAuth.value && !["gh", "token", "device", "skip"].includes(githubAuth.value)) {
|
|
1404
|
+
throw new CliError("--github-auth must be gh, token, device, or skip.", 1);
|
|
1405
|
+
}
|
|
1406
|
+
const portValue = remotePort.value ? Number(remotePort.value) : undefined;
|
|
1407
|
+
if (portValue !== undefined && !Number.isFinite(portValue)) {
|
|
1408
|
+
throw new CliError("--remote-port must be a number.", 1);
|
|
1409
|
+
}
|
|
1410
|
+
const hostFromUrl = remoteUrl.value ? remoteUrl.value.replace(/^[a-z]+:\/\//i, "").replace(/[/:].*$/, "").trim() : undefined;
|
|
1174
1411
|
const options = {
|
|
1412
|
+
demo: demo.value,
|
|
1175
1413
|
yes: yes.value,
|
|
1176
1414
|
repair: repair.value,
|
|
1177
1415
|
privateStateOnly: privateStateOnly.value,
|
|
1178
|
-
server: server.value === "remote" ? "remote" : server.value === "local" ? "local" : undefined,
|
|
1179
|
-
remoteUrl: remoteUrl.value,
|
|
1180
|
-
connectionAlias: connectionAlias.value,
|
|
1181
1416
|
repoSlug: repoSlug.value,
|
|
1182
1417
|
githubToken: githubToken.value,
|
|
1183
1418
|
githubAuthMethod: githubAuth.value,
|
|
1184
1419
|
githubProject: githubProject.value,
|
|
1185
|
-
githubProjectStatusField: githubProjectStatusField.value
|
|
1420
|
+
githubProjectStatusField: githubProjectStatusField.value,
|
|
1421
|
+
server: server.value === "remote" ? "remote" : server.value === "local" ? "local" : undefined,
|
|
1422
|
+
remoteAlias: remoteAlias.value,
|
|
1423
|
+
remoteHost: remoteHost.value ?? hostFromUrl,
|
|
1424
|
+
remotePort: portValue,
|
|
1425
|
+
remoteCheckout: remoteCheckout.value
|
|
1186
1426
|
};
|
|
1187
|
-
if (server.value && options.server === undefined) {
|
|
1188
|
-
throw new CliError2("--server must be local or remote.", 1);
|
|
1189
|
-
}
|
|
1190
|
-
if (githubAuth.value && !["gh", "token", "device", "skip"].includes(githubAuth.value)) {
|
|
1191
|
-
throw new CliError2("--github-auth must be gh, token, device, or skip.", 1);
|
|
1192
|
-
}
|
|
1193
|
-
if (remoteCheckout.value) {
|
|
1194
|
-
if (remoteCheckout.value === "managed-clone")
|
|
1195
|
-
options.remoteCheckout = { kind: "managed-clone" };
|
|
1196
|
-
else if (remoteCheckout.value === "current-ref")
|
|
1197
|
-
options.remoteCheckout = { kind: "current-ref", ref: ref.value };
|
|
1198
|
-
else if (remoteCheckout.value === "uploaded-snapshot")
|
|
1199
|
-
options.remoteCheckout = { kind: "uploaded-snapshot" };
|
|
1200
|
-
else if (remoteCheckout.value === "existing-path") {
|
|
1201
|
-
if (!existingPath.value)
|
|
1202
|
-
throw new CliError2("--remote-checkout existing-path requires --existing-path <path>.", 1);
|
|
1203
|
-
options.remoteCheckout = { kind: "existing-path", path: existingPath.value };
|
|
1204
|
-
} else {
|
|
1205
|
-
throw new CliError2("--remote-checkout must be managed-clone, current-ref, uploaded-snapshot, or existing-path.", 1);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
1427
|
return { options, rest };
|
|
1209
1428
|
}
|
|
1210
1429
|
async function runInteractiveControlPlaneInit(context, prompts) {
|
|
1211
1430
|
prompts.intro?.("Initialize a Rig control-plane project");
|
|
1212
1431
|
const projectRoot = context.projectRoot;
|
|
1213
|
-
const existingConfig =
|
|
1432
|
+
const existingConfig = existsSync4(resolve5(projectRoot, "rig.config.ts")) || existsSync4(resolve5(projectRoot, "rig.config.json"));
|
|
1214
1433
|
let repair = false;
|
|
1215
1434
|
let privateStateOnly = false;
|
|
1216
1435
|
if (existingConfig) {
|
|
@@ -1236,99 +1455,87 @@ async function runInteractiveControlPlaneInit(context, prompts) {
|
|
|
1236
1455
|
placeholder: "owner/repo",
|
|
1237
1456
|
defaultValue: detectedRepo
|
|
1238
1457
|
});
|
|
1239
|
-
const
|
|
1240
|
-
message: "
|
|
1458
|
+
const placement = await promptSelect(prompts, {
|
|
1459
|
+
message: "Execution placement",
|
|
1460
|
+
initialValue: "local",
|
|
1241
1461
|
options: [
|
|
1242
|
-
{ value: "local", label: "Local
|
|
1243
|
-
{ value: "remote", label: "Remote
|
|
1462
|
+
{ value: "local", label: "Local", hint: "runs execute on this machine (tmux)" },
|
|
1463
|
+
{ value: "remote", label: "Remote", hint: "runs execute on an SSH host" }
|
|
1244
1464
|
]
|
|
1245
1465
|
});
|
|
1246
|
-
|
|
1466
|
+
let remoteAlias;
|
|
1467
|
+
let remoteHost;
|
|
1468
|
+
let remotePort;
|
|
1247
1469
|
let remoteCheckout;
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
{ value: "uploaded-snapshot", label: "Upload current working-tree snapshot" },
|
|
1255
|
-
{ value: "existing-path", label: "Use existing server path" }
|
|
1256
|
-
]
|
|
1257
|
-
});
|
|
1258
|
-
if (checkout === "existing-path") {
|
|
1259
|
-
remoteCheckout = { kind: "existing-path", path: await promptRequiredText(prompts, { message: "Existing server checkout path", placeholder: "/srv/rig/checkouts/repo" }) };
|
|
1260
|
-
} else if (checkout === "current-ref") {
|
|
1261
|
-
remoteCheckout = { kind: "current-ref", ref: await promptOptionalText(prompts, { message: "Branch/ref to clone (blank for current HEAD)", placeholder: "main" }) || undefined };
|
|
1262
|
-
} else if (checkout === "uploaded-snapshot") {
|
|
1263
|
-
remoteCheckout = { kind: "uploaded-snapshot" };
|
|
1264
|
-
} else {
|
|
1265
|
-
remoteCheckout = { kind: "managed-clone" };
|
|
1266
|
-
}
|
|
1470
|
+
if (placement === "remote") {
|
|
1471
|
+
remoteHost = await promptRequiredText(prompts, { message: "Remote host (ssh)", placeholder: "rig.example.com" });
|
|
1472
|
+
remoteAlias = await promptOptionalText(prompts, { message: "Alias for this remote", placeholder: "prod", initialValue: remoteHost }) || remoteHost;
|
|
1473
|
+
const portText = await promptOptionalText(prompts, { message: "SSH port", placeholder: "22", initialValue: "22" });
|
|
1474
|
+
remotePort = portText && Number.isFinite(Number(portText)) ? Number(portText) : 22;
|
|
1475
|
+
remoteCheckout = await promptOptionalText(prompts, { message: "Remote working directory (existing checkout path)", placeholder: "/srv/rig/<repo>" }) || undefined;
|
|
1267
1476
|
}
|
|
1268
1477
|
const detectedGhLogin = detectGhLogin();
|
|
1269
1478
|
const authMethod = await promptSelect(prompts, {
|
|
1270
1479
|
message: `GitHub auth method${detectedGhLogin ? ` (detected gh login: ${detectedGhLogin})` : ""}`,
|
|
1271
1480
|
options: [
|
|
1272
|
-
{ value: "gh", label: "Import token from gh auth token", hint:
|
|
1273
|
-
{ value: "device", label: "Start
|
|
1481
|
+
{ value: "gh", label: "Import token from gh auth token", hint: "recommended for local" },
|
|
1482
|
+
{ value: "device", label: "Start GitHub device flow" },
|
|
1274
1483
|
{ value: "token", label: "Paste token" },
|
|
1275
1484
|
{ value: "skip", label: "Skip for now" }
|
|
1276
1485
|
]
|
|
1277
1486
|
});
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
const confirmed = await prompts.confirm({
|
|
1282
|
-
message: `This sends a GitHub token from this machine to ${remoteUrl}. Continue?`,
|
|
1283
|
-
initialValue: false
|
|
1284
|
-
});
|
|
1285
|
-
if (prompts.isCancel(confirmed) || confirmed !== true) {
|
|
1286
|
-
throw new CliError2("Remote gh-token import cancelled.", 1);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : undefined;
|
|
1290
|
-
const projectChoice = await promptSelect(prompts, {
|
|
1291
|
-
message: "GitHub Projects status sync",
|
|
1292
|
-
options: [
|
|
1293
|
-
{ value: "off", label: "Off" },
|
|
1294
|
-
{ value: "configure", label: "Configure ProjectV2 status field" }
|
|
1295
|
-
]
|
|
1296
|
-
});
|
|
1297
|
-
const githubProject = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : "off";
|
|
1298
|
-
const githubProjectStatusField = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }) : undefined;
|
|
1487
|
+
const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : authMethod === "gh" ? readGhAuthToken() : undefined;
|
|
1488
|
+
const projectConfig = await promptGitHubProjectConfig(context, prompts, repoSlug, githubToken, authMethod === "gh" ? refreshGhProjectScopesAndReadToken : undefined);
|
|
1489
|
+
const effectiveGithubToken = projectConfig.githubToken ?? githubToken;
|
|
1299
1490
|
const result = await runControlPlaneInit(context, {
|
|
1300
|
-
server: serverChoice,
|
|
1301
|
-
remoteUrl,
|
|
1302
1491
|
repoSlug,
|
|
1303
|
-
githubToken,
|
|
1492
|
+
githubToken: effectiveGithubToken,
|
|
1304
1493
|
githubAuthMethod: authMethod,
|
|
1305
|
-
githubProject,
|
|
1306
|
-
githubProjectStatusField,
|
|
1307
|
-
|
|
1494
|
+
githubProject: projectConfig.githubProject,
|
|
1495
|
+
githubProjectStatusField: projectConfig.githubProjectStatusField,
|
|
1496
|
+
githubProjectStatuses: projectConfig.githubProjectStatuses,
|
|
1308
1497
|
repair,
|
|
1309
|
-
privateStateOnly
|
|
1498
|
+
privateStateOnly,
|
|
1499
|
+
server: placement,
|
|
1500
|
+
...remoteAlias ? { remoteAlias } : {},
|
|
1501
|
+
...remoteHost ? { remoteHost } : {},
|
|
1502
|
+
...remotePort !== undefined ? { remotePort } : {},
|
|
1503
|
+
...remoteCheckout ? { remoteCheckout } : {}
|
|
1310
1504
|
});
|
|
1311
1505
|
const details = result.details && typeof result.details === "object" && !Array.isArray(result.details) ? result.details : {};
|
|
1312
1506
|
const deviceAuth = details.deviceAuth && typeof details.deviceAuth === "object" && !Array.isArray(details.deviceAuth) ? details.deviceAuth : null;
|
|
1313
1507
|
const deviceMessage = deviceAuth ? ` GitHub device flow: open ${String(deviceAuth.verificationUri ?? deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by the server")} and enter ${String(deviceAuth.userCode ?? deviceAuth.user_code ?? "the returned user code")}.` : "";
|
|
1314
|
-
prompts.outro?.(`Rig project initialized.${deviceMessage} Next: rig doctor && rig task list`);
|
|
1508
|
+
prompts.outro?.(`Rig project initialized (${placement} placement).${deviceMessage} Next: rig doctor && rig task list`);
|
|
1315
1509
|
return result;
|
|
1316
1510
|
}
|
|
1317
1511
|
async function executeInit(context, args) {
|
|
1318
1512
|
const parsed = parseInitOptions(args);
|
|
1319
|
-
if (parsed.options.
|
|
1513
|
+
if (parsed.options.demo) {
|
|
1514
|
+
if (parsed.rest.length > 0) {
|
|
1515
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1516
|
+
Usage: rig init --demo [--yes] [--repair]`, 1, { hint: "Run `rig init --demo` (optionally with --repair to rewrite an existing rig.config)." });
|
|
1517
|
+
}
|
|
1518
|
+
return runDemoInit(context, parsed.options);
|
|
1519
|
+
}
|
|
1520
|
+
if (parsed.options.yes || parsed.options.repoSlug || parsed.options.githubToken || parsed.options.privateStateOnly || parsed.options.repair || parsed.options.githubAuthMethod || parsed.options.server || parsed.options.remoteHost) {
|
|
1320
1521
|
if (parsed.rest.length > 0)
|
|
1321
|
-
throw new
|
|
1322
|
-
Usage: rig init [--server local|remote] [--remote-
|
|
1522
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1523
|
+
Usage: rig init [--demo] [--repo owner/repo] [--server local|remote] [--remote-host <host>] [--remote-alias <a>] [--remote-port <n>] [--remote-checkout <path>] [--github-auth gh|token|device|skip] [--github-token <token>] [--github-project off|<project-id>] [--yes]`, 1);
|
|
1323
1524
|
return runControlPlaneInit(context, parsed.options);
|
|
1324
1525
|
}
|
|
1325
1526
|
if (parsed.rest.length > 0)
|
|
1326
|
-
throw new
|
|
1527
|
+
throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
|
|
1327
1528
|
Usage: rig init`, 1);
|
|
1529
|
+
if (!process.stdin.isTTY) {
|
|
1530
|
+
throw new CliError("rig init needs an interactive terminal. For scripts, pass flags: rig init --yes --repo owner/repo [--server local|remote --remote-host <host>] [--github-auth gh|token|device|skip].", 1);
|
|
1531
|
+
}
|
|
1328
1532
|
return runInteractiveControlPlaneInit(context, await loadClackPrompts());
|
|
1329
1533
|
}
|
|
1330
1534
|
export {
|
|
1331
1535
|
runInteractiveControlPlaneInit,
|
|
1536
|
+
runDemoInit,
|
|
1332
1537
|
executeInit,
|
|
1333
|
-
buildRigInitConfigSource
|
|
1538
|
+
buildRigInitConfigSource,
|
|
1539
|
+
DEMO_TASKS_RELATIVE_DIR,
|
|
1540
|
+
DEMO_TASKS
|
|
1334
1541
|
};
|