@h-rig/cli 0.0.6-alpha.1 → 0.0.6-alpha.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +18 -19
  2. package/dist/bin/build-rig-binaries.d.ts +2 -0
  3. package/dist/bin/build-rig-binaries.js +22 -10
  4. package/dist/bin/rig.d.ts +72 -0
  5. package/dist/bin/rig.js +15742 -7203
  6. package/dist/config/rig-default-config.yml +5 -0
  7. package/dist/src/app/drone-ui.d.ts +34 -0
  8. package/dist/src/app/drone-ui.js +278 -0
  9. package/dist/src/commands/_async-ui.d.ts +10 -0
  10. package/dist/src/commands/_async-ui.js +121 -0
  11. package/dist/src/commands/_cli-format.d.ts +56 -0
  12. package/dist/src/commands/_cli-format.js +319 -0
  13. package/dist/src/commands/_config-env.d.ts +18 -0
  14. package/dist/src/commands/_config-env.js +22 -0
  15. package/dist/src/commands/_connection-state.d.ts +54 -0
  16. package/dist/src/commands/_connection-state.js +75 -11
  17. package/dist/src/commands/_doctor-checks.d.ts +46 -0
  18. package/dist/src/commands/_doctor-checks.js +95 -244
  19. package/dist/src/commands/_help-catalog.d.ts +29 -0
  20. package/dist/src/commands/_help-catalog.js +146 -0
  21. package/dist/src/commands/_inprocess-services.d.ts +33 -0
  22. package/dist/src/commands/_inprocess-services.js +102 -0
  23. package/dist/src/commands/_json-output.d.ts +11 -0
  24. package/dist/src/commands/_json-output.js +60 -0
  25. package/dist/src/commands/_lazy-reconcile.d.ts +34 -0
  26. package/dist/src/commands/_lazy-reconcile.js +102 -0
  27. package/dist/src/commands/_parsers.d.ts +15 -0
  28. package/dist/src/commands/_parsers.js +18 -11
  29. package/dist/src/commands/_paths.d.ts +11 -0
  30. package/dist/src/commands/_paths.js +1 -1
  31. package/dist/src/commands/_pi-frontend.d.ts +35 -0
  32. package/dist/src/commands/_pi-frontend.js +64 -0
  33. package/dist/src/commands/_pi-install.d.ts +42 -0
  34. package/dist/src/commands/_pi-install.js +19 -36
  35. package/dist/src/commands/_policy.d.ts +8 -0
  36. package/dist/src/commands/_policy.js +67 -19
  37. package/dist/src/commands/_probes.d.ts +1 -0
  38. package/dist/src/commands/_run-bridge.d.ts +114 -0
  39. package/dist/src/commands/_run-bridge.js +387 -0
  40. package/dist/src/commands/_run-diagnostics.d.ts +9 -0
  41. package/dist/src/commands/_run-diagnostics.js +51 -0
  42. package/dist/src/commands/_run-driver-helpers.d.ts +26 -0
  43. package/dist/src/commands/_run-driver-helpers.js +79 -230
  44. package/dist/src/commands/_run-projection.d.ts +50 -0
  45. package/dist/src/commands/_run-projection.js +349 -0
  46. package/dist/src/commands/_run-subcommands.d.ts +3 -0
  47. package/dist/src/commands/_run-subcommands.js +31 -0
  48. package/dist/src/commands/_spinner.d.ts +25 -0
  49. package/dist/src/commands/_spinner.js +65 -0
  50. package/dist/src/commands/agent.d.ts +3 -0
  51. package/dist/src/commands/agent.js +8690 -235
  52. package/dist/src/commands/config.d.ts +3 -0
  53. package/dist/src/commands/config.js +181 -0
  54. package/dist/src/commands/dist.d.ts +28 -0
  55. package/dist/src/commands/dist.js +44 -29
  56. package/dist/src/commands/doctor.d.ts +3 -0
  57. package/dist/src/commands/doctor.js +214 -245
  58. package/dist/src/commands/github.d.ts +3 -0
  59. package/dist/src/commands/github.js +237 -171
  60. package/dist/src/commands/inbox.d.ts +28 -0
  61. package/dist/src/commands/inbox.js +530 -111
  62. package/dist/src/commands/init.d.ts +64 -0
  63. package/dist/src/commands/init.js +941 -654
  64. package/dist/src/commands/inspect.d.ts +20 -0
  65. package/dist/src/commands/inspect.js +709 -119
  66. package/dist/src/commands/pi.d.ts +3 -0
  67. package/dist/src/commands/pi.js +177 -0
  68. package/dist/src/commands/plugin.d.ts +16 -0
  69. package/dist/src/commands/plugin.js +576 -29
  70. package/dist/src/commands/profile-and-review.d.ts +4 -0
  71. package/dist/src/commands/profile-and-review.js +112 -67
  72. package/dist/src/commands/queue.d.ts +3 -0
  73. package/dist/src/commands/queue.js +20 -20
  74. package/dist/src/commands/remote.d.ts +3 -0
  75. package/dist/src/commands/remote.js +883 -50
  76. package/dist/src/commands/repo-git-harness.d.ts +5 -0
  77. package/dist/src/commands/repo-git-harness.js +78 -28
  78. package/dist/src/commands/run.d.ts +21 -0
  79. package/dist/src/commands/run.js +17732 -618
  80. package/dist/src/commands/server.d.ts +3 -0
  81. package/dist/src/commands/server.js +166 -285
  82. package/dist/src/commands/setup.d.ts +16 -0
  83. package/dist/src/commands/setup.js +230 -267
  84. package/dist/src/commands/stats.d.ts +15 -0
  85. package/dist/src/commands/stats.js +632 -0
  86. package/dist/src/commands/task-run-driver.d.ts +94 -0
  87. package/dist/src/commands/task-run-driver.js +120 -1856
  88. package/dist/src/commands/task.d.ts +35 -0
  89. package/dist/src/commands/task.js +676 -1183
  90. package/dist/src/commands/test.d.ts +3 -0
  91. package/dist/src/commands/test.js +15 -8
  92. package/dist/src/commands/triage.d.ts +11 -0
  93. package/dist/src/commands/triage.js +227 -0
  94. package/dist/src/commands/workspace.d.ts +3 -0
  95. package/dist/src/commands/workspace.js +18 -11
  96. package/dist/src/commands.d.ts +13 -0
  97. package/dist/src/commands.js +16760 -7779
  98. package/dist/src/index.d.ts +4 -0
  99. package/dist/src/index.js +16880 -8223
  100. package/dist/src/launcher.d.ts +61 -0
  101. package/dist/src/launcher.js +81 -13
  102. package/dist/src/operator-cli.d.ts +2 -0
  103. package/dist/src/operator-cli.js +17867 -0
  104. package/dist/src/operator-entry.d.ts +1 -0
  105. package/dist/src/operator-entry.js +3 -0
  106. package/dist/src/runner.d.ts +47 -0
  107. package/dist/src/runner.js +19 -24
  108. package/dist/src/withMutedConsole.d.ts +2 -0
  109. package/package.json +21 -7
  110. package/dist/src/commands/_authority-runs.js +0 -110
  111. package/dist/src/commands/_operator-view.js +0 -322
  112. package/dist/src/commands/_preflight.js +0 -460
  113. package/dist/src/commands/_server-client.js +0 -364
  114. package/dist/src/commands/_snapshot-upload.js +0 -313
  115. package/dist/src/commands/_task-picker.js +0 -48
  116. package/dist/src/commands/browser.js +0 -890
  117. package/dist/src/commands/connect.js +0 -180
  118. package/dist/src/commands/inspector.js +0 -256
  119. package/dist/src/commands/task-report-bug.js +0 -1083
  120. 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 existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
6
- import { spawnSync as spawnSync2 } from "child_process";
7
- import { resolve as resolve5 } from "path";
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
- import { CliError as CliError2 } from "@rig/runtime/control-plane/errors";
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 CliError2(`Invalid Rig connection state at ${path}: ${error instanceof Error ? error.message : String(error)}`, 1);
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,182 +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/_server-client.ts
156
- import { spawnSync } from "child_process";
157
- import { ensureLocalRigServerConnection } from "@rig/runtime/local-server";
158
- var cachedGitHubBearerToken;
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
404
+ import { resolve as resolve2 } from "path";
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;
159
416
  function cleanToken(value) {
160
417
  const trimmed = value?.trim();
161
418
  return trimmed ? trimmed : null;
162
419
  }
163
- function setGitHubBearerTokenForCurrentProcess(token) {
164
- cachedGitHubBearerToken = cleanToken(token ?? undefined);
420
+ function cleanString(value) {
421
+ return typeof value === "string" && value.trim() ? value.trim() : null;
165
422
  }
166
- function readGitHubBearerTokenForRemote() {
167
- if (cachedGitHubBearerToken !== undefined)
168
- return cachedGitHubBearerToken;
169
- const envToken = cleanToken(process.env.RIG_GITHUB_TOKEN) ?? cleanToken(process.env.GITHUB_TOKEN) ?? cleanToken(process.env.GH_TOKEN);
170
- if (envToken) {
171
- cachedGitHubBearerToken = envToken;
172
- return cachedGitHubBearerToken;
173
- }
174
- const result = spawnSync("gh", ["auth", "token"], {
175
- encoding: "utf8",
176
- timeout: 5000,
177
- stdio: ["ignore", "pipe", "ignore"]
178
- });
179
- cachedGitHubBearerToken = result.status === 0 ? cleanToken(result.stdout) : null;
180
- return cachedGitHubBearerToken;
423
+ function oauthClientId() {
424
+ return cleanToken(process.env.RIG_GITHUB_OAUTH_CLIENT_ID);
181
425
  }
182
- async function ensureServerForCli(projectRoot) {
183
- try {
184
- const selected = resolveSelectedConnection(projectRoot);
185
- if (selected?.connection.kind === "remote") {
186
- return {
187
- baseUrl: selected.connection.baseUrl,
188
- authToken: readGitHubBearerTokenForRemote(),
189
- connectionKind: "remote"
190
- };
191
- }
192
- const connection = await ensureLocalRigServerConnection(projectRoot);
193
- return {
194
- baseUrl: connection.baseUrl,
195
- authToken: connection.authToken,
196
- connectionKind: "local"
197
- };
198
- } catch (error) {
199
- if (error instanceof Error) {
200
- throw new CliError2(error.message, 1);
201
- }
202
- throw error;
203
- }
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);
204
429
  }
205
- function mergeHeaders(headers, authToken) {
206
- const merged = new Headers(headers);
207
- if (authToken) {
208
- merged.set("authorization", `Bearer ${authToken}`);
209
- }
210
- return merged;
430
+ function setGitHubBearerTokenForCurrentProcess(token, projectRoot) {
431
+ scopedGitHubBearerTokens.set(resolve2(projectRoot ?? process.cwd()), cleanToken(token));
211
432
  }
212
- function diagnosticMessage(payload) {
213
- if (!payload || typeof payload !== "object" || Array.isArray(payload))
214
- return null;
215
- const record = payload;
216
- const diagnostics = Array.isArray(record.diagnostics) ? record.diagnostics : [];
217
- const messages = diagnostics.flatMap((entry) => {
218
- if (!entry || typeof entry !== "object" || Array.isArray(entry))
219
- return [];
220
- const diagnostic = entry;
221
- const kind = typeof diagnostic.kind === "string" ? diagnostic.kind : "task-source";
222
- const message = typeof diagnostic.message === "string" ? diagnostic.message : null;
223
- return message ? [`${kind}: ${message}`] : [];
224
- });
225
- return messages.length > 0 ? messages.join("; ") : null;
433
+ async function getGitHubAuthStatusInProcess(context) {
434
+ return { ok: true, ...resolveGitHubAuthStatus({ projectRoot: context.projectRoot, oauthConfigured: Boolean(oauthClientId()) }) };
226
435
  }
227
- async function requestServerJson(context, pathname, init = {}) {
228
- const server = await ensureServerForCli(context.projectRoot);
229
- const response = await fetch(`${server.baseUrl}${pathname}`, {
230
- ...init,
231
- headers: mergeHeaders(init.headers, server.authToken)
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
232
443
  });
233
- const text = await response.text();
234
- const payload = text.trim().length > 0 ? (() => {
235
- try {
236
- return JSON.parse(text);
237
- } catch {
238
- return null;
239
- }
240
- })() : null;
241
- if (!response.ok) {
242
- const diagnostics = diagnosticMessage(payload);
243
- const detail = diagnostics ?? (text || response.statusText);
244
- throw new CliError2(`Rig server request failed (${response.status}): ${detail}`, 1);
444
+ const store = createGitHubAuthStore(targetRoot);
445
+ const session = store.createApiSession();
446
+ if (targetRoot !== context.projectRoot) {
447
+ store.copyToLocalProjectRoot(context.projectRoot);
245
448
  }
246
- return payload;
247
- }
248
- async function postGitHubTokenViaServer(context, token, options = {}) {
249
- const payload = await requestServerJson(context, "/api/github/auth/token", {
250
- method: "POST",
251
- headers: { "content-type": "application/json" },
252
- body: JSON.stringify({ token, selectedRepo: options.selectedRepo })
253
- });
254
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
255
- }
256
- async function prepareRemoteCheckoutViaServer(context, input) {
257
- const payload = await requestServerJson(context, `/api/projects/${encodeURIComponent(input.repoSlug)}/prepare-checkout`, {
258
- method: "POST",
259
- headers: { "content-type": "application/json" },
260
- body: JSON.stringify({ checkout: input.checkout, repoUrl: input.repoUrl, baseDir: input.baseDir })
261
- });
262
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
263
- }
264
- async function registerProjectViaServer(context, input) {
265
- const payload = await requestServerJson(context, "/api/projects", {
266
- method: "POST",
267
- headers: { "content-type": "application/json" },
268
- body: JSON.stringify(input)
269
- });
270
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
271
- }
272
- function sleep(ms) {
273
- return new Promise((resolve2) => setTimeout(resolve2, ms));
274
- }
275
- async function switchServerProjectRootViaServer(context, projectRoot, options = {}) {
276
- const switched = await requestServerJson(context, "/api/server/project-root", {
277
- method: "POST",
278
- headers: { "content-type": "application/json" },
279
- body: JSON.stringify({ projectRoot })
280
- });
281
- const timeoutMs = options.timeoutMs ?? 30000;
282
- const pollMs = options.pollMs ?? 1000;
283
- const deadline = Date.now() + timeoutMs;
284
- let lastError;
285
- while (Date.now() < deadline) {
286
- try {
287
- const status = await requestServerJson(context, "/api/server/status");
288
- if (status && typeof status === "object" && !Array.isArray(status)) {
289
- const record = status;
290
- if (record.projectRoot === projectRoot) {
291
- return { ok: true, switched, status: record };
292
- }
293
- lastError = `server projectRoot=${String(record.projectRoot ?? "unknown")}`;
294
- }
295
- } catch (error) {
296
- lastError = error;
297
- }
298
- await sleep(pollMs);
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
+ });
483
+ }
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) });
299
493
  }
300
- throw new CliError2(`Rig server did not switch to ${projectRoot} before timeout (${lastError instanceof Error ? lastError.message : String(lastError ?? "no status")}).`, 1);
494
+ throw new Error(`No in-process GitHub auth API for ${method} ${pathname}`);
301
495
  }
302
496
 
303
497
  // packages/cli/src/commands/_pi-install.ts
304
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
305
- import { homedir as homedir2 } from "os";
306
- import { resolve as resolve2 } from "path";
307
- var PI_RIG_PACKAGE_NAME = "@rig/pi-rig";
308
- var LEGACY_PI_RIG_MARKER = `// Managed by Rig. Source package: @rig/pi-rig.
309
- export { default } from '@rig/pi-rig';
310
- `;
498
+ import { existsSync as existsSync2 } from "fs";
499
+ import { homedir } from "os";
500
+ import { resolve as resolve3 } from "path";
501
+ var PI_RIG_PACKAGE_NAME = "@h-rig/pi-rig";
311
502
  async function defaultCommandRunner(command, options = {}) {
312
503
  const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
313
504
  const [stdout, stderr, exitCode] = await Promise.all([
@@ -318,16 +509,16 @@ async function defaultCommandRunner(command, options = {}) {
318
509
  return { exitCode, stdout, stderr };
319
510
  }
320
511
  function resolvePiRigExtensionPath(homeDir) {
321
- return resolve2(homeDir, ".pi", "agent", "extensions", "pi-rig");
512
+ return resolve3(homeDir, ".pi", "agent", "extensions", "pi-rig");
322
513
  }
323
514
  function resolvePiRigPackageSource(projectRoot, exists = existsSync2) {
324
- const localPackage = resolve2(projectRoot, "packages", "pi-rig");
325
- if (exists(resolve2(localPackage, "package.json")))
515
+ const localPackage = resolve3(projectRoot, "packages", "pi-rig");
516
+ if (exists(resolve3(localPackage, "package.json")))
326
517
  return localPackage;
327
518
  return `npm:${PI_RIG_PACKAGE_NAME}`;
328
519
  }
329
520
  function resolvePiHomeDir(inputHomeDir) {
330
- return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir2();
521
+ return inputHomeDir ?? process.env.RIG_PI_HOME_DIR?.trim() ?? homedir();
331
522
  }
332
523
  function piListContainsPiRig(output) {
333
524
  return output.split(/\r?\n/).some((line) => {
@@ -356,7 +547,13 @@ async function ensurePiBinaryAvailable(input) {
356
547
  }
357
548
  return { ok: true, detail: (current.stdout || current.stderr).trim() || undefined };
358
549
  }
359
- const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim() || "bunx @earendil-works/pi@latest install";
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
+ }
360
557
  const parts = splitInstallCommand(installCommand);
361
558
  if (parts.length === 0) {
362
559
  return { ok: false, error: (current.stderr || current.stdout).trim() || "pi --version failed" };
@@ -373,18 +570,6 @@ async function ensurePiBinaryAvailable(input) {
373
570
  ...next.exitCode === 0 ? {} : { error: (next.stderr || next.stdout).trim() || "pi --version failed after install" }
374
571
  };
375
572
  }
376
- function removeManagedLegacyPiRigBridge(homeDir, exists = existsSync2) {
377
- const extensionPath = resolvePiRigExtensionPath(homeDir);
378
- const indexPath = resolve2(extensionPath, "index.ts");
379
- if (!exists(indexPath))
380
- return;
381
- try {
382
- const content = readFileSync2(indexPath, "utf8");
383
- if (content === LEGACY_PI_RIG_MARKER || content.includes("Managed by Rig. Source package: @rig/pi-rig")) {
384
- rmSync(extensionPath, { recursive: true, force: true });
385
- }
386
- } catch {}
387
- }
388
573
  async function checkPiRigInstall(input = {}) {
389
574
  const home = resolvePiHomeDir(input.homeDir);
390
575
  const extensionPath = resolvePiRigExtensionPath(home);
@@ -395,48 +580,40 @@ async function checkPiRigInstall(input = {}) {
395
580
  piRig: { ok: true, label: "pi-rig global extension", detail: extensionPath }
396
581
  };
397
582
  }
398
- const exists = input.exists ?? existsSync2;
399
583
  const runner = input.commandRunner ?? defaultCommandRunner;
400
584
  const piResult = await safeRun(runner, ["pi", "--version"]);
401
585
  const piListResult = piResult.exitCode === 0 ? await safeRun(runner, ["pi", "list"]) : { exitCode: 1, stdout: "", stderr: "" };
402
- const listedPiRig = piListResult.exitCode === 0 && piListContainsPiRig(`${piListResult.stdout}
586
+ const hasPiRig = piListResult.exitCode === 0 && piListContainsPiRig(`${piListResult.stdout}
403
587
  ${piListResult.stderr}`);
404
- const legacyBridge = exists(resolve2(extensionPath, "index.ts"));
405
- const hasPiRig = listedPiRig;
588
+ const hasLegacyBridgeScaffold = !hasPiRig && (input.exists ?? existsSync2)(extensionPath);
406
589
  return {
407
590
  extensionPath,
408
591
  pi: {
409
592
  ok: piResult.exitCode === 0,
410
593
  label: "pi",
411
594
  detail: (piResult.stdout || piResult.stderr).trim() || undefined,
412
- hint: piResult.exitCode === 0 ? undefined : "Install Pi or run `rig init --yes` to install/update the Pi runtime."
595
+ hint: piResult.exitCode === 0 ? undefined : "Install Pi/OMP manually or set RIG_PI_INSTALL_COMMAND before verifying with bare `rig` / Cockpit \u2192 Doctor."
413
596
  },
414
597
  piRig: {
415
598
  ok: hasPiRig,
416
599
  label: "pi-rig global extension",
417
- detail: hasPiRig ? piListResult.stdout.trim() || PI_RIG_PACKAGE_NAME : legacyBridge ? `${extensionPath} (legacy bridge; reinstall required)` : undefined,
418
- hint: hasPiRig ? undefined : "Run `rig init --yes` to install/enable the global pi-rig package with `pi install`."
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.`
419
602
  }
420
603
  };
421
604
  }
422
605
  async function ensurePiRigInstalled(input) {
423
606
  const home = resolvePiHomeDir(input.homeDir);
424
607
  if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1") {
425
- const status2 = await checkPiRigInstall({ homeDir: home, commandRunner: input.commandRunner });
426
- return { ...status2, installedPath: status2.extensionPath };
608
+ const status = await checkPiRigInstall({ homeDir: home, commandRunner: input.commandRunner });
609
+ return { ...status, installedPath: status.extensionPath };
427
610
  }
428
611
  const runner = input.commandRunner ?? defaultCommandRunner;
429
612
  const piAvailable = await ensurePiBinaryAvailable({ runner, projectRoot: input.projectRoot });
430
- const status = piAvailable.ok ? await checkPiRigInstall({ homeDir: home, commandRunner: runner }) : {
431
- extensionPath: resolvePiRigExtensionPath(home),
432
- pi: { ok: false, label: "pi", detail: piAvailable.error, hint: "Install/update Pi with RIG_PI_INSTALL_COMMAND or install Pi manually." },
433
- piRig: { ok: false, label: "pi-rig global extension", hint: "Pi is required before pi-rig can be installed." }
434
- };
435
613
  if (!piAvailable.ok) {
436
614
  throw new Error(`Pi install/update failed: ${piAvailable.error ?? "pi unavailable"}`);
437
615
  }
438
616
  const packageSource = resolvePiRigPackageSource(input.projectRoot);
439
- removeManagedLegacyPiRigBridge(home);
440
617
  const install = await runner(["pi", "install", packageSource], { cwd: input.projectRoot });
441
618
  if (install.exitCode !== 0) {
442
619
  throw new Error(`pi-rig install failed: ${(install.stderr || install.stdout).trim() || `exit ${install.exitCode}`}`);
@@ -444,131 +621,13 @@ async function ensurePiRigInstalled(input) {
444
621
  const next = await checkPiRigInstall({ homeDir: home, commandRunner: runner });
445
622
  return { ...next, installedPath: packageSource };
446
623
  }
447
- async function ensureRemotePiRigInstalled(input) {
448
- const payload = await input.requestJson("/api/pi-rig/install", {
449
- method: "POST",
450
- headers: { "content-type": "application/json" },
451
- body: JSON.stringify({ package: "@rig/pi-rig", scope: "global" })
452
- });
453
- const record = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
454
- const piOk = record.piOk === true || record.ok === true;
455
- const piRigOk = record.piRigOk === true || record.installed === true || record.ok === true;
456
- const extensionPath = typeof record.extensionPath === "string" ? record.extensionPath : "remote:~/.pi/agent/extensions/pi-rig";
457
- return {
458
- remote: true,
459
- extensionPath,
460
- pi: {
461
- ok: piOk,
462
- label: "pi",
463
- detail: typeof record.piVersion === "string" ? record.piVersion : undefined,
464
- hint: piOk ? undefined : "Install/update Pi on the selected remote Rig server."
465
- },
466
- piRig: {
467
- ok: piRigOk,
468
- label: "pi-rig global extension",
469
- detail: extensionPath,
470
- hint: piRigOk ? undefined : "Install/enable pi-rig on the selected remote Rig server."
471
- }
472
- };
473
- }
474
624
  async function buildPiSetupChecks(input = {}) {
475
625
  const status = await checkPiRigInstall(input);
476
626
  return [status.pi, status.piRig];
477
627
  }
478
628
 
479
- // packages/cli/src/commands/_snapshot-upload.ts
480
- import { mkdir, readdir, readFile, writeFile } from "fs/promises";
481
- import { dirname as dirname2, resolve as resolve3, relative, sep } from "path";
482
- var SNAPSHOT_ARCHIVE_VERSION = 1;
483
- var SNAPSHOT_ARCHIVE_CONTENT_TYPE = "application/vnd.rig.snapshot+json";
484
- var DEFAULT_EXCLUDED_DIRECTORIES = new Set([
485
- ".git",
486
- ".rig",
487
- "node_modules",
488
- ".turbo",
489
- ".next",
490
- ".cache",
491
- "coverage",
492
- "dist",
493
- "build",
494
- "out"
495
- ]);
496
- function toPosixPath(path) {
497
- return path.split(sep).join("/");
498
- }
499
- function assertManifestPath(root, relativePath) {
500
- if (!relativePath || relativePath.startsWith("/") || relativePath.includes("\x00")) {
501
- throw new Error(`Invalid snapshot path: ${relativePath}`);
502
- }
503
- const resolved = resolve3(root, relativePath);
504
- const relativeToRoot = relative(root, resolved);
505
- if (relativeToRoot.startsWith("..") || relativeToRoot === ".." || resolve3(relativeToRoot) === resolved) {
506
- throw new Error(`Snapshot path escapes project root: ${relativePath}`);
507
- }
508
- return resolved;
509
- }
510
- async function buildSnapshotUploadManifest(projectRoot, options = {}) {
511
- const root = resolve3(projectRoot);
512
- const excludedDirectories = [...new Set([
513
- ...DEFAULT_EXCLUDED_DIRECTORIES,
514
- ...options.excludedDirectories ?? []
515
- ])];
516
- const excludedSet = new Set(excludedDirectories);
517
- const files = [];
518
- async function visit(dir) {
519
- const entries = await readdir(dir, { withFileTypes: true });
520
- for (const entry of entries) {
521
- if (entry.isDirectory() && excludedSet.has(entry.name))
522
- continue;
523
- const fullPath = resolve3(dir, entry.name);
524
- if (entry.isDirectory()) {
525
- await visit(fullPath);
526
- continue;
527
- }
528
- if (!entry.isFile())
529
- continue;
530
- files.push(toPosixPath(relative(root, fullPath)));
531
- }
532
- }
533
- await visit(root);
534
- files.sort();
535
- return { root, files, excludedDirectories };
536
- }
537
- async function createSnapshotUploadArchive(projectRoot, options = {}) {
538
- const manifest = await buildSnapshotUploadManifest(projectRoot, options);
539
- const files = await Promise.all(manifest.files.map(async (path) => {
540
- const fullPath = assertManifestPath(manifest.root, path);
541
- return {
542
- path,
543
- contentBase64: (await readFile(fullPath)).toString("base64")
544
- };
545
- }));
546
- return {
547
- version: SNAPSHOT_ARCHIVE_VERSION,
548
- root: manifest.root,
549
- files,
550
- excludedDirectories: manifest.excludedDirectories,
551
- createdAt: (options.now?.() ?? new Date).toISOString()
552
- };
553
- }
554
- function encodeSnapshotUploadArchive(archive) {
555
- return Buffer.from(JSON.stringify(archive), "utf8").toString("base64");
556
- }
557
- async function uploadSnapshotArchiveViaServer(context, input) {
558
- const payload = await requestServerJson(context, `/api/projects/${encodeURIComponent(input.repoSlug)}/upload-snapshot`, {
559
- method: "POST",
560
- headers: { "content-type": "application/json" },
561
- body: JSON.stringify({
562
- archiveContentBase64: encodeSnapshotUploadArchive(input.archive),
563
- contentType: SNAPSHOT_ARCHIVE_CONTENT_TYPE,
564
- baseDir: input.baseDir
565
- })
566
- });
567
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
568
- }
569
-
570
629
  // packages/cli/src/commands/_doctor-checks.ts
571
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
630
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
572
631
  import { resolve as resolve4 } from "path";
573
632
  import { isSupportedBunVersion, MIN_SUPPORTED_BUN_VERSION } from "@rig/runtime/control-plane/setup-version";
574
633
 
@@ -601,23 +660,6 @@ function isAuthenticated(payload) {
601
660
  const record = payload;
602
661
  return record.signedIn === true || record.authenticated === true || record.status === "authenticated" || record.ok === true && typeof record.login === "string" && record.login.trim().length > 0;
603
662
  }
604
- function repoSlugFromConfig(config) {
605
- const project = config?.project;
606
- if (project && typeof project === "object" && !Array.isArray(project)) {
607
- const record = project;
608
- if (typeof record.repo === "string" && /^([^/\s]+)\/([^/\s]+)$/.test(record.repo))
609
- return record.repo;
610
- if (typeof record.name === "string" && /^([^/\s]+)\/([^/\s]+)$/.test(record.name))
611
- return record.name;
612
- }
613
- const taskSource = config?.taskSource;
614
- if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
615
- const source = taskSource;
616
- if (typeof source.owner === "string" && typeof source.repo === "string")
617
- return `${source.owner}/${source.repo}`;
618
- }
619
- return null;
620
- }
621
663
  function loadFallbackConfig(projectRoot) {
622
664
  const candidates = ["rig.config.ts", "rig.config.mts", "rig.config.json"];
623
665
  for (const name of candidates) {
@@ -625,7 +667,7 @@ function loadFallbackConfig(projectRoot) {
625
667
  if (!existsSync3(path))
626
668
  continue;
627
669
  try {
628
- const source = readFileSync3(path, "utf8");
670
+ const source = readFileSync2(path, "utf8");
629
671
  if (name.endsWith(".json"))
630
672
  return JSON.parse(source);
631
673
  const owner = source.match(/owner\s*:\s*["']([^"']+)["']/)?.[1];
@@ -644,14 +686,11 @@ function loadFallbackConfig(projectRoot) {
644
686
  }
645
687
  return null;
646
688
  }
647
- function projectStatusSlug(projectRoot, config) {
648
- return readRepoConnection(projectRoot)?.project ?? repoSlugFromConfig(config);
649
- }
650
689
  function githubProjectsCheck(config) {
651
690
  const github = config?.github;
652
691
  const projects = github?.projects;
653
692
  if (!projects?.enabled) {
654
- return check("github-projects", "GitHub Projects status sync", "warn", "disabled or not configured", "Run `rig init --github-project <project>` or configure github.projects when Project status sync should be authoritative.");
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.");
655
694
  }
656
695
  if (projects.projectId && projects.statusFieldId) {
657
696
  return check("github-projects", "GitHub Projects status sync", "pass", `project ${projects.projectId}`);
@@ -676,16 +715,6 @@ function permissionAllowsPr(payload) {
676
715
  }
677
716
  return null;
678
717
  }
679
- function labelsReady(payload) {
680
- if (!payload || typeof payload !== "object" || Array.isArray(payload))
681
- return null;
682
- const record = payload;
683
- if (record.ok === true || record.ready === true || record.labelsReady === true)
684
- return true;
685
- if (record.ok === false || record.ready === false || record.labelsReady === false)
686
- return false;
687
- return null;
688
- }
689
718
  function prMergeCheck(config) {
690
719
  const pr = config?.pr;
691
720
  const merge = config?.merge;
@@ -699,89 +728,48 @@ async function runRigDoctorChecks(options) {
699
728
  const checks = [];
700
729
  const which = options.which ?? ((binary) => Bun.which(binary));
701
730
  const bunVersion = options.bunVersion ?? Bun.version;
702
- const request = options.requestJson ?? ((pathname, init) => requestServerJson({ projectRoot }, pathname, init));
731
+ const request = options.requestJson ?? ((pathname, init) => requestGitHubAuthJsonInProcess({ projectRoot }, pathname, init));
703
732
  const loadConfig = options.loadConfig ?? loadRigConfigOrNull;
733
+ const progress = options.onProgress ?? (() => {});
734
+ progress("Checking local toolchain\u2026");
704
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");
705
737
  const loadedConfig = await loadConfig(projectRoot).catch(() => null);
706
738
  const config = loadedConfig ?? loadFallbackConfig(projectRoot);
707
739
  const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync3(resolve4(projectRoot, name)));
708
- checks.push(config ? check("config", "rig.config loadable", "pass") : check("config", "rig.config loadable", hasConfigFile ? "fail" : "fail", hasConfigFile ? "config file exists but failed to load" : "missing rig.config.ts/json", "Run `rig init` or fix the config error."));
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."));
709
741
  const taskSourceKind = config?.taskSource?.kind;
710
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."));
711
743
  const repo = readRepoConnection(projectRoot);
712
- checks.push(repo ? check("project-link", "repo selected Rig connection", repo.project ? "pass" : "warn", `${repo.selected}${repo.project ? ` -> ${repo.project}` : ""}`, "Run `rig init --yes --repo owner/repo` to link this checkout to a GitHub repo slug.") : check("project-link", "repo selected Rig connection", "fail", "missing .rig/state/connection.json", "Run `rig init` or `rig connect use <alias|local>`."));
713
- const selected = (() => {
714
- try {
715
- return resolveSelectedConnection(projectRoot);
716
- } catch {
717
- return null;
718
- }
719
- })();
720
- checks.push(selected ? check("connection", "selected server connection", "pass", selected.connection.kind === "remote" ? selected.connection.baseUrl : "local auto") : check("connection", "selected server connection", repo ? "fail" : "warn", repo ? "selected alias is missing" : "will auto-start local server", repo ? "Run `rig connect list` and `rig connect use <alias|local>`." : undefined));
721
- let server = null;
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."));
722
745
  try {
723
- server = await (options.resolveServer ?? ensureServerForCli)(projectRoot);
724
- checks.push(check("server", "Rig server reachable", "pass", `${server.connectionKind} ${server.baseUrl}`));
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>`."));
725
749
  } catch (error) {
726
- checks.push(check("server", "Rig server reachable", "fail", errorMessage(error), "Start the local Rig server or fix the selected remote connection."));
750
+ checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub through Rig."));
727
751
  }
728
- if (server || options.requestJson) {
729
- try {
730
- const status = await request("/api/server/status");
731
- checks.push(check("server-status", "server project status", "pass", JSON.stringify(status).slice(0, 180)));
732
- } catch (error) {
733
- checks.push(check("server-status", "server project status", "fail", errorMessage(error), "Run `rig doctor` after the selected server is reachable."));
734
- }
735
- try {
736
- const auth = await request("/api/github/auth/status");
737
- checks.push(isAuthenticated(auth) ? check("github-auth", "GitHub auth", "pass") : check("github-auth", "GitHub auth", "fail", "not authenticated", "Run `rig github auth import-gh` or `rig github auth token --token <token>`."));
738
- } catch (error) {
739
- checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub through Rig and ensure the server exposes auth status."));
740
- }
741
- try {
742
- const permissions = await request("/api/github/repo/permissions");
743
- const allowed = permissionAllowsPr(permissions);
744
- checks.push(allowed === true ? check("github-repo-permissions", "GitHub repo PR permissions", "pass", JSON.stringify(permissions).slice(0, 180)) : allowed === false ? check("github-repo-permissions", "GitHub repo PR permissions", "fail", JSON.stringify(permissions).slice(0, 180), "Grant the selected GitHub token permission to push branches, open PRs, and merge according to repo rules.") : check("github-repo-permissions", "GitHub repo PR permissions", "warn", JSON.stringify(permissions).slice(0, 180), "Confirm the selected token can push branches and open PRs."));
745
- } catch (error) {
746
- checks.push(check("github-repo-permissions", "GitHub repo PR permissions", "warn", errorMessage(error), "Ensure the server exposes repo permission checks and the token can open PRs."));
747
- }
748
- try {
749
- const labels = await request("/api/workspace/task-labels");
750
- const ready = labelsReady(labels);
751
- checks.push(ready === false ? check("task-labels", "GitHub issue labels", "fail", JSON.stringify(labels).slice(0, 180), "Let Rig create required labels or create the configured lifecycle labels manually.") : check("task-labels", "GitHub issue labels", ready === true ? "pass" : "warn", JSON.stringify(labels).slice(0, 180), "Confirm required Rig lifecycle labels exist."));
752
- } catch (error) {
753
- checks.push(check("task-labels", "GitHub issue labels", "warn", errorMessage(error), "Run `rig init`/`rig doctor` after label setup is wired on the server."));
754
- }
755
- try {
756
- const projection = await request("/api/workspace/task-projection");
757
- checks.push(check("task-projection", "task projection", "pass", JSON.stringify(projection).slice(0, 180)));
758
- } catch (error) {
759
- checks.push(check("task-projection", "task projection", "warn", errorMessage(error), "Refresh task projection with `rig task list` or fix the task source."));
760
- }
761
- const slug = projectStatusSlug(projectRoot, config);
762
- if (slug) {
763
- try {
764
- const project = await request(`/api/projects/${encodeURIComponent(slug)}`);
765
- checks.push(check("remote-checkout", "server project checkout", "pass", JSON.stringify(project).slice(0, 180)));
766
- } catch (error) {
767
- checks.push(check("remote-checkout", "server project checkout", "warn", errorMessage(error), "Run `rig init --yes --repo owner/repo` to register/link the server project checkout."));
768
- }
769
- } else {
770
- checks.push(check("remote-checkout", "server project checkout", "warn", "repo slug unknown", "Set project.repo or run `rig init --repo owner/repo`."));
771
- }
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."));
772
759
  }
773
760
  if (taskSourceKind === "github-issues") {
774
761
  checks.push(check("gh", "gh CLI fallback", which("gh") ? "pass" : "warn", which("gh") ?? undefined, "Install gh for local/dev GitHub fallback operations."));
775
762
  }
776
763
  checks.push(githubProjectsCheck(config));
777
764
  checks.push(prMergeCheck(config));
765
+ progress("Checking Pi installation\u2026");
778
766
  const piChecks = await (options.piChecks ?? (() => buildPiSetupChecks()))().catch((error) => [{
779
767
  ok: false,
780
768
  label: "pi/pi-rig checks",
781
769
  hint: errorMessage(error)
782
770
  }]);
783
771
  for (const pi of piChecks) {
784
- checks.push(check(pi.label === "pi" ? "pi" : "pi-rig", pi.label, pi.ok ? "pass" : "warn", pi.detail, pi.hint ?? (pi.ok ? undefined : "Run `rig init --yes` to install/update Pi and enable pi-rig.")));
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.")));
785
773
  }
786
774
  return checks;
787
775
  }
@@ -790,10 +778,10 @@ function countDoctorFailures(checks) {
790
778
  }
791
779
 
792
780
  // packages/cli/src/commands/init.ts
793
- var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.1";
781
+ var RIG_CONFIG_PACKAGE_DIST_TAG = "latest";
794
782
  var RIG_CONFIG_DEV_DEPENDENCIES = {
795
- "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
796
- "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
783
+ "@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_DIST_TAG}`,
784
+ "@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_DIST_TAG}`
797
785
  };
798
786
  function parseRepoSlugFromRemote(remoteUrl) {
799
787
  const trimmed = remoteUrl.trim();
@@ -801,7 +789,7 @@ function parseRepoSlugFromRemote(remoteUrl) {
801
789
  return gitHubMatch ? `${gitHubMatch[1]}/${gitHubMatch[2]}` : null;
802
790
  }
803
791
  function detectOriginRepoSlug(projectRoot) {
804
- const result = spawnSync2("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
792
+ const result = spawnSync("git", ["-C", projectRoot, "remote", "get-url", "origin"], { encoding: "utf8" });
805
793
  if (result.status !== 0)
806
794
  return null;
807
795
  return parseRepoSlugFromRemote(result.stdout.trim());
@@ -809,7 +797,7 @@ function detectOriginRepoSlug(projectRoot) {
809
797
  function parseRepoSlug(value) {
810
798
  const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
811
799
  if (!match)
812
- throw new CliError2(`Invalid GitHub repo slug: ${value}. Expected owner/repo.`, 1);
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`." });
813
801
  return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
814
802
  }
815
803
  function ensureRigPrivateDirs(projectRoot) {
@@ -826,7 +814,7 @@ function ensureRigPrivateDirs(projectRoot) {
826
814
  }
827
815
  function ensureGitignoreEntries(projectRoot) {
828
816
  const path = resolve5(projectRoot, ".gitignore");
829
- const existing = existsSync4(path) ? readFileSync4(path, "utf8") : "";
817
+ const existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
830
818
  const entries = [".rig/state/", ".rig/logs/", ".rig/runs/", ".rig/tmp/"];
831
819
  const missing = entries.filter((entry) => !existing.split(/\r?\n/).includes(entry));
832
820
  if (missing.length === 0)
@@ -840,7 +828,7 @@ function ensureGitignoreEntries(projectRoot) {
840
828
  }
841
829
  function ensureRigConfigPackageDependencies(projectRoot) {
842
830
  const path = resolve5(projectRoot, "package.json");
843
- const existing = existsSync4(path) ? JSON.parse(readFileSync4(path, "utf8")) : {};
831
+ const existing = existsSync4(path) ? JSON.parse(readFileSync3(path, "utf8")) : {};
844
832
  const devDependencies = existing.devDependencies && typeof existing.devDependencies === "object" && !Array.isArray(existing.devDependencies) ? { ...existing.devDependencies } : {};
845
833
  for (const [name, spec] of Object.entries(RIG_CONFIG_DEV_DEPENDENCIES)) {
846
834
  devDependencies[name] = spec;
@@ -857,93 +845,437 @@ function applyGitHubProjectConfig(source, options) {
857
845
  return source;
858
846
  const projectId = JSON.stringify(options.githubProject);
859
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
+ `)},` : "";
860
851
  return source.replace(` projects: { enabled: false },`, [
861
852
  ` projects: {`,
862
853
  ` enabled: true,`,
863
854
  ` projectId: ${projectId},`,
864
- ` statusFieldId: ${statusFieldId},`,
855
+ ` statusFieldId: ${statusFieldId},${statuses}`,
865
856
  ` },`
866
857
  ].join(`
867
858
  `));
868
859
  }
869
- function checkoutForInit(projectRoot, serverKind, strategy) {
870
- if (serverKind === "local")
871
- return { kind: "local", path: projectRoot };
872
- const selected = strategy ?? { kind: "managed-clone" };
873
- switch (selected.kind) {
874
- case "managed-clone":
875
- return { kind: "managed-clone", path: projectRoot };
876
- case "current-ref":
877
- return { kind: "current-ref", path: projectRoot, ...selected.ref ? { ref: selected.ref } : {} };
878
- case "uploaded-snapshot":
879
- return { kind: "uploaded-snapshot", path: projectRoot, source: "local-working-tree" };
880
- case "existing-path":
881
- return { kind: "existing-path", path: selected.path };
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
+ };
882
868
  }
869
+ return { kind: "local", path: projectRoot };
883
870
  }
884
871
  function detectGhLogin() {
885
- const result = spawnSync2("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
872
+ const result = spawnSync("gh", ["api", "user", "--jq", ".login"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] });
886
873
  return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
887
874
  }
888
875
  function readGhAuthToken() {
889
- const result = spawnSync2("gh", ["auth", "token"], { encoding: "utf8" });
876
+ const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
890
877
  if (result.status !== 0 || !result.stdout.trim()) {
891
- throw new CliError2(result.stderr.trim() || "Could not read GitHub token from `gh auth token`.", result.status || 1);
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>`." });
892
879
  }
893
880
  return result.stdout.trim();
894
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");
895
896
  async function loadClackPrompts() {
896
- return await import("@clack/prompts");
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
+ };
897
924
  }
898
925
  async function promptRequiredText(prompts, options) {
899
926
  const value = await prompts.text(options);
900
927
  if (prompts.isCancel(value))
901
- throw new CliError2("Init cancelled.", 1);
928
+ throw new CliError("Init cancelled.", 1);
902
929
  const text = String(value ?? "").trim();
903
930
  if (!text)
904
- throw new CliError2(`${options.message} is required.`, 1);
931
+ throw new CliError(`${options.message} is required.`, 1);
905
932
  return text;
906
933
  }
907
934
  async function promptOptionalText(prompts, options) {
908
935
  const value = await prompts.text(options);
909
936
  if (prompts.isCancel(value))
910
- throw new CliError2("Init cancelled.", 1);
937
+ throw new CliError("Init cancelled.", 1);
911
938
  return String(value ?? "").trim();
912
939
  }
913
940
  async function promptSelect(prompts, options) {
914
941
  const value = await prompts.select(options);
915
942
  if (prompts.isCancel(value))
916
- throw new CliError2("Init cancelled.", 1);
943
+ throw new CliError("Init cancelled.", 1);
917
944
  return String(value);
918
945
  }
919
- async function pollDeviceAuthOnce(context, pollId) {
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));
1085
+ }
1086
+ function positiveIntFromEnv(name, fallback) {
1087
+ const value = Number.parseInt(process.env[name] ?? "", 10);
1088
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
1089
+ }
1090
+ function apiSessionTokenFrom(payload) {
1091
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
1092
+ return null;
1093
+ const token = payload.apiSessionToken;
1094
+ return typeof token === "string" && token.trim() ? token.trim() : null;
1095
+ }
1096
+ async function pollDeviceAuthUntilComplete(context, pollId, firstPayload) {
920
1097
  if (typeof pollId !== "string" || !pollId.trim())
921
1098
  return null;
922
- const payload = await requestServerJson(context, "/api/github/auth/device/poll", {
923
- method: "POST",
924
- headers: { "content-type": "application/json" },
925
- body: JSON.stringify({ pollId })
926
- }).catch(() => null);
927
- return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
1099
+ const intervalSeconds = typeof firstPayload.interval === "number" && Number.isFinite(firstPayload.interval) && firstPayload.interval > 0 ? firstPayload.interval : 5;
1100
+ const timeoutMs = positiveIntFromEnv("RIG_DEVICE_AUTH_POLL_TIMEOUT_MS", 300000);
1101
+ const intervalMs = positiveIntFromEnv("RIG_DEVICE_AUTH_POLL_INTERVAL_MS", Math.max(1000, intervalSeconds * 1000));
1102
+ const deadline = Date.now() + timeoutMs;
1103
+ let last = null;
1104
+ do {
1105
+ const payload = await requestGitHubAuthJsonInProcess(context, "/api/github/auth/device/poll", {
1106
+ method: "POST",
1107
+ headers: { "content-type": "application/json" },
1108
+ body: JSON.stringify({ pollId })
1109
+ }).catch(() => null);
1110
+ last = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
1111
+ const status = typeof last?.status === "string" ? last.status : null;
1112
+ if (status === "signed-in" || status === "expired" || status === "cancelled" || status === "failed") {
1113
+ return last;
1114
+ }
1115
+ if (timeoutMs <= 0)
1116
+ return last;
1117
+ await sleep(intervalMs);
1118
+ } while (Date.now() < deadline);
1119
+ return last;
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
+ };
928
1247
  }
929
1248
  async function runControlPlaneInit(context, options) {
930
1249
  const projectRoot = context.projectRoot;
931
- const detectedSlug = options.repoSlug ?? detectOriginRepoSlug(projectRoot);
1250
+ const existingRepoConnection = readRepoConnection(projectRoot);
1251
+ const detectedSlug = options.repoSlug ?? existingRepoConnection?.project ?? detectOriginRepoSlug(projectRoot);
932
1252
  if (!detectedSlug) {
933
- throw new CliError2("Could not detect GitHub repo slug from origin. Pass --repo owner/repo.", 1);
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);
934
1258
  }
935
1259
  const repo = parseRepoSlug(detectedSlug);
936
- const serverKind = options.server ?? "local";
937
- const connectionAlias = options.connectionAlias ?? (serverKind === "local" ? "local" : "remote");
938
- if (serverKind === "remote") {
939
- if (!options.remoteUrl)
940
- throw new CliError2("Missing --remote-url for --server remote.", 1);
941
- upsertGlobalConnection(connectionAlias, { kind: "remote", baseUrl: options.remoteUrl });
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;
942
1272
  }
1273
+ const remoteCheckoutPath = placement === "remote" ? options.remoteCheckout?.trim() || null : null;
943
1274
  writeRepoConnection(projectRoot, {
944
1275
  selected: connectionAlias,
945
1276
  project: repo.slug,
946
- linkedAt: new Date().toISOString()
1277
+ linkedAt: new Date().toISOString(),
1278
+ ...remoteCheckoutPath ? { serverProjectRoot: remoteCheckoutPath, serverProjectRootAlias: connectionAlias } : {}
947
1279
  });
948
1280
  ensureRigPrivateDirs(projectRoot);
949
1281
  ensureGitignoreEntries(projectRoot);
@@ -967,67 +1299,44 @@ async function runControlPlaneInit(context, options) {
967
1299
  }
968
1300
  writeFileSync2(resolve5(projectRoot, ".rig", "state", "project-link.json"), `${JSON.stringify({ repoSlug: repo.slug, connection: connectionAlias, linkedAt: new Date().toISOString() }, null, 2)}
969
1301
  `, "utf8");
970
- const checkout = checkoutForInit(projectRoot, serverKind, options.remoteCheckout);
971
- let uploadedSnapshot = null;
972
- if (serverKind === "remote" && options.remoteCheckout?.kind === "uploaded-snapshot") {
973
- const archive = await createSnapshotUploadArchive(projectRoot);
974
- uploadedSnapshot = await uploadSnapshotArchiveViaServer(context, { repoSlug: repo.slug, archive });
975
- const uploadedCheckout = uploadedSnapshot.checkout;
976
- if (uploadedCheckout && typeof uploadedCheckout === "object" && !Array.isArray(uploadedCheckout)) {
977
- Object.assign(checkout, uploadedCheckout);
978
- }
979
- }
1302
+ const checkout = checkoutForInit(projectRoot, options);
980
1303
  let githubAuth = null;
981
1304
  let deviceAuth = null;
982
1305
  const authMethod = options.githubAuthMethod ?? (options.githubToken ? "token" : "skip");
983
- const remoteGhTokenWarning = serverKind === "remote" && authMethod === "gh" ? `This sends a GitHub token from this machine to ${options.remoteUrl ?? "the remote Rig server"}.` : null;
984
- if (remoteGhTokenWarning && !options.yes) {
985
- throw new CliError2(`${remoteGhTokenWarning} Re-run with --yes to confirm this explicit token transfer.`, 1);
986
- }
987
1306
  const token = authMethod === "gh" && !options.githubToken ? readGhAuthToken() : options.githubToken?.trim();
988
1307
  if (token) {
989
- githubAuth = await postGitHubTokenViaServer(context, token, { selectedRepo: repo.slug });
990
- setGitHubBearerTokenForCurrentProcess(token);
991
- if (serverKind === "remote") {
992
- writeFileSync2(resolve5(projectRoot, ".rig", "state", "github-auth.json"), `${JSON.stringify({ authenticated: true, source: authMethod === "gh" ? "gh" : "init-token", storedOnServer: true, selectedRepo: repo.slug, updatedAt: new Date().toISOString() }, null, 2)}
993
- `, "utf8");
994
- }
1308
+ githubAuth = await postGitHubTokenInProcess(context, token, { selectedRepo: repo.slug });
1309
+ const apiSessionToken = apiSessionTokenFrom(githubAuth);
1310
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken ?? token, projectRoot);
995
1311
  } else if (authMethod === "device") {
996
- const payload = await requestServerJson(context, "/api/github/auth/device/start", {
1312
+ const payload = await requestGitHubAuthJsonInProcess(context, "/api/github/auth/device/start", {
997
1313
  method: "POST",
998
1314
  headers: { "content-type": "application/json" },
999
1315
  body: JSON.stringify({ repoSlug: repo.slug })
1000
1316
  });
1001
1317
  deviceAuth = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
1002
- const completed = await pollDeviceAuthOnce(context, deviceAuth.pollId);
1003
- if (completed)
1318
+ if (context.outputMode !== "json") {
1319
+ const verificationUri = String(deviceAuth.verificationUri ?? deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by GitHub");
1320
+ const userCode = String(deviceAuth.userCode ?? deviceAuth.user_code ?? "the returned user code");
1321
+ console.log(`GitHub device flow: open ${verificationUri} and enter ${userCode}. Waiting for authorization...`);
1322
+ }
1323
+ const completed = await pollDeviceAuthUntilComplete(context, deviceAuth.pollId, deviceAuth);
1324
+ if (completed) {
1325
+ const apiSessionToken = apiSessionTokenFrom(completed);
1326
+ if (apiSessionToken) {
1327
+ setGitHubBearerTokenForCurrentProcess(apiSessionToken, projectRoot);
1328
+ }
1004
1329
  deviceAuth = { ...deviceAuth, poll: completed, completed: completed.status === "signed-in" };
1005
- }
1006
- let remoteCheckoutPreparation = null;
1007
- if (serverKind === "remote" && options.remoteCheckout?.kind !== "uploaded-snapshot") {
1008
- remoteCheckoutPreparation = await prepareRemoteCheckoutViaServer(context, {
1009
- repoSlug: repo.slug,
1010
- checkout,
1011
- repoUrl: `https://github.com/${repo.slug}.git`
1012
- });
1013
- const preparedCheckout = remoteCheckoutPreparation.checkout;
1014
- if (preparedCheckout && typeof preparedCheckout === "object" && !Array.isArray(preparedCheckout)) {
1015
- Object.assign(checkout, preparedCheckout);
1016
1330
  }
1017
1331
  }
1018
- const registered = await registerProjectViaServer(context, {
1019
- repoSlug: repo.slug,
1020
- checkout
1021
- });
1022
- const checkoutPath = typeof checkout.path === "string" ? checkout.path : null;
1023
- const serverRootSwitch = serverKind === "remote" && checkoutPath ? await switchServerProjectRootViaServer(context, checkoutPath) : null;
1024
- const activeProjectRegistration = serverRootSwitch ? await registerProjectViaServer(context, { repoSlug: repo.slug, checkout }) : null;
1025
- const pi = serverKind === "remote" ? await ensureRemotePiRigInstalled({ requestJson: (pathname, init) => requestServerJson(context, pathname, init) }).catch((error) => ({
1026
- remote: true,
1027
- pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
1028
- piRig: { ok: false, label: "pi-rig global extension", hint: "Remote server did not complete pi-rig installation." },
1029
- extensionPath: "remote:~/.pi/agent/extensions/pi-rig"
1030
- })) : await ensurePiRigInstalled({ projectRoot, homeDir: process.env.RIG_PI_HOME_DIR }).catch((error) => ({
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) => ({
1031
1340
  pi: { ok: false, label: "pi", hint: error instanceof Error ? error.message : String(error) },
1032
1341
  piRig: { ok: false, label: "pi-rig global extension", hint: "Local pi-rig installation failed." },
1033
1342
  extensionPath: null,
@@ -1040,41 +1349,32 @@ async function runControlPlaneInit(context, options) {
1040
1349
  }));
1041
1350
  const details = {
1042
1351
  repoSlug: repo.slug,
1043
- server: serverKind,
1352
+ server: placement,
1044
1353
  connection: connectionAlias,
1045
1354
  githubProject: options.githubProject ?? "off",
1046
1355
  checkout,
1047
- remoteCheckoutPreparation,
1048
- uploadedSnapshot,
1049
- projectRegistration: registered,
1050
- activeProjectRegistration,
1051
- serverRootSwitch,
1052
1356
  githubAuth,
1053
1357
  deviceAuth,
1054
- githubAuthWarning: remoteGhTokenWarning,
1358
+ labelSetup,
1055
1359
  pi,
1056
1360
  doctor
1057
1361
  };
1058
1362
  if (context.outputMode === "json")
1059
1363
  console.log(JSON.stringify(details, null, 2));
1060
1364
  else
1061
- 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`);
1062
1366
  return { ok: true, group: "init", command: "init", details };
1063
1367
  }
1064
1368
  function parseInitOptions(args) {
1065
1369
  let rest = [...args];
1370
+ const demo = takeFlag(rest, "--demo");
1371
+ rest = demo.rest;
1066
1372
  const yes = takeFlag(rest, "--yes");
1067
1373
  rest = yes.rest;
1068
1374
  const repair = takeFlag(rest, "--repair");
1069
1375
  rest = repair.rest;
1070
1376
  const privateStateOnly = takeFlag(rest, "--private-state-only");
1071
1377
  rest = privateStateOnly.rest;
1072
- const server = takeOption(rest, "--server");
1073
- rest = server.rest;
1074
- const remoteUrl = takeOption(rest, "--remote-url");
1075
- rest = remoteUrl.rest;
1076
- const connectionAlias = takeOption(rest, "--connection");
1077
- rest = connectionAlias.rest;
1078
1378
  const repoSlug = takeOption(rest, "--repo");
1079
1379
  rest = repoSlug.rest;
1080
1380
  const githubToken = takeOption(rest, "--github-token");
@@ -1085,46 +1385,45 @@ function parseInitOptions(args) {
1085
1385
  rest = githubProjectStatusField.rest;
1086
1386
  const githubAuth = takeOption(rest, "--github-auth");
1087
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;
1088
1398
  const remoteCheckout = takeOption(rest, "--remote-checkout");
1089
1399
  rest = remoteCheckout.rest;
1090
- const existingPath = takeOption(rest, "--existing-path");
1091
- rest = existingPath.rest;
1092
- const ref = takeOption(rest, "--ref");
1093
- rest = ref.rest;
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;
1094
1411
  const options = {
1412
+ demo: demo.value,
1095
1413
  yes: yes.value,
1096
1414
  repair: repair.value,
1097
1415
  privateStateOnly: privateStateOnly.value,
1098
- server: server.value === "remote" ? "remote" : server.value === "local" ? "local" : undefined,
1099
- remoteUrl: remoteUrl.value,
1100
- connectionAlias: connectionAlias.value,
1101
1416
  repoSlug: repoSlug.value,
1102
1417
  githubToken: githubToken.value,
1103
1418
  githubAuthMethod: githubAuth.value,
1104
1419
  githubProject: githubProject.value,
1105
- 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
1106
1426
  };
1107
- if (server.value && options.server === undefined) {
1108
- throw new CliError2("--server must be local or remote.", 1);
1109
- }
1110
- if (githubAuth.value && !["gh", "token", "device", "skip"].includes(githubAuth.value)) {
1111
- throw new CliError2("--github-auth must be gh, token, device, or skip.", 1);
1112
- }
1113
- if (remoteCheckout.value) {
1114
- if (remoteCheckout.value === "managed-clone")
1115
- options.remoteCheckout = { kind: "managed-clone" };
1116
- else if (remoteCheckout.value === "current-ref")
1117
- options.remoteCheckout = { kind: "current-ref", ref: ref.value };
1118
- else if (remoteCheckout.value === "uploaded-snapshot")
1119
- options.remoteCheckout = { kind: "uploaded-snapshot" };
1120
- else if (remoteCheckout.value === "existing-path") {
1121
- if (!existingPath.value)
1122
- throw new CliError2("--remote-checkout existing-path requires --existing-path <path>.", 1);
1123
- options.remoteCheckout = { kind: "existing-path", path: existingPath.value };
1124
- } else {
1125
- throw new CliError2("--remote-checkout must be managed-clone, current-ref, uploaded-snapshot, or existing-path.", 1);
1126
- }
1127
- }
1128
1427
  return { options, rest };
1129
1428
  }
1130
1429
  async function runInteractiveControlPlaneInit(context, prompts) {
@@ -1156,99 +1455,87 @@ async function runInteractiveControlPlaneInit(context, prompts) {
1156
1455
  placeholder: "owner/repo",
1157
1456
  defaultValue: detectedRepo
1158
1457
  });
1159
- const serverChoice = await promptSelect(prompts, {
1160
- message: "Rig server",
1458
+ const placement = await promptSelect(prompts, {
1459
+ message: "Execution placement",
1460
+ initialValue: "local",
1161
1461
  options: [
1162
- { value: "local", label: "Local server", hint: "run on this machine" },
1163
- { value: "remote", label: "Remote server", hint: "connect to an HTTPS Rig server" }
1462
+ { value: "local", label: "Local", hint: "runs execute on this machine (tmux)" },
1463
+ { value: "remote", label: "Remote", hint: "runs execute on an SSH host" }
1164
1464
  ]
1165
1465
  });
1166
- const remoteUrl = serverChoice === "remote" ? await promptRequiredText(prompts, { message: "Remote Rig server URL", placeholder: "https://rig.example.com" }) : undefined;
1466
+ let remoteAlias;
1467
+ let remoteHost;
1468
+ let remotePort;
1167
1469
  let remoteCheckout;
1168
- if (serverChoice === "remote") {
1169
- const checkout = await promptSelect(prompts, {
1170
- message: "Remote checkout strategy",
1171
- options: [
1172
- { value: "managed-clone", label: "Server-managed clone (recommended)" },
1173
- { value: "current-ref", label: "Clone current branch/ref" },
1174
- { value: "uploaded-snapshot", label: "Upload current working-tree snapshot" },
1175
- { value: "existing-path", label: "Use existing server path" }
1176
- ]
1177
- });
1178
- if (checkout === "existing-path") {
1179
- remoteCheckout = { kind: "existing-path", path: await promptRequiredText(prompts, { message: "Existing server checkout path", placeholder: "/srv/rig/checkouts/repo" }) };
1180
- } else if (checkout === "current-ref") {
1181
- remoteCheckout = { kind: "current-ref", ref: await promptOptionalText(prompts, { message: "Branch/ref to clone (blank for current HEAD)", placeholder: "main" }) || undefined };
1182
- } else if (checkout === "uploaded-snapshot") {
1183
- remoteCheckout = { kind: "uploaded-snapshot" };
1184
- } else {
1185
- remoteCheckout = { kind: "managed-clone" };
1186
- }
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;
1187
1476
  }
1188
1477
  const detectedGhLogin = detectGhLogin();
1189
1478
  const authMethod = await promptSelect(prompts, {
1190
1479
  message: `GitHub auth method${detectedGhLogin ? ` (detected gh login: ${detectedGhLogin})` : ""}`,
1191
1480
  options: [
1192
- { value: "gh", label: "Import token from gh auth token", hint: serverChoice === "local" ? "recommended for local" : "sends this machine's token to the remote server" },
1193
- { value: "device", label: "Start server GitHub device flow", hint: serverChoice === "remote" ? "recommended for remote" : undefined },
1481
+ { value: "gh", label: "Import token from gh auth token", hint: "recommended for local" },
1482
+ { value: "device", label: "Start GitHub device flow" },
1194
1483
  { value: "token", label: "Paste token" },
1195
1484
  { value: "skip", label: "Skip for now" }
1196
1485
  ]
1197
1486
  });
1198
- if (serverChoice === "remote" && authMethod === "gh") {
1199
- if (!prompts.confirm)
1200
- throw new CliError2("Remote gh-token import requires explicit confirmation.", 1);
1201
- const confirmed = await prompts.confirm({
1202
- message: `This sends a GitHub token from this machine to ${remoteUrl}. Continue?`,
1203
- initialValue: false
1204
- });
1205
- if (prompts.isCancel(confirmed) || confirmed !== true) {
1206
- throw new CliError2("Remote gh-token import cancelled.", 1);
1207
- }
1208
- }
1209
- const githubToken = authMethod === "token" ? await promptRequiredText(prompts, { message: "GitHub token", placeholder: "ghp_..." }) : undefined;
1210
- const projectChoice = await promptSelect(prompts, {
1211
- message: "GitHub Projects status sync",
1212
- options: [
1213
- { value: "off", label: "Off" },
1214
- { value: "configure", label: "Configure ProjectV2 status field" }
1215
- ]
1216
- });
1217
- const githubProject = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "GitHub ProjectV2 id", placeholder: "PVT_..." }) : "off";
1218
- const githubProjectStatusField = projectChoice === "configure" ? await promptRequiredText(prompts, { message: "Project Status field id", placeholder: "field_status" }) : undefined;
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;
1219
1490
  const result = await runControlPlaneInit(context, {
1220
- server: serverChoice,
1221
- remoteUrl,
1222
1491
  repoSlug,
1223
- githubToken,
1492
+ githubToken: effectiveGithubToken,
1224
1493
  githubAuthMethod: authMethod,
1225
- githubProject,
1226
- githubProjectStatusField,
1227
- remoteCheckout,
1494
+ githubProject: projectConfig.githubProject,
1495
+ githubProjectStatusField: projectConfig.githubProjectStatusField,
1496
+ githubProjectStatuses: projectConfig.githubProjectStatuses,
1228
1497
  repair,
1229
- privateStateOnly
1498
+ privateStateOnly,
1499
+ server: placement,
1500
+ ...remoteAlias ? { remoteAlias } : {},
1501
+ ...remoteHost ? { remoteHost } : {},
1502
+ ...remotePort !== undefined ? { remotePort } : {},
1503
+ ...remoteCheckout ? { remoteCheckout } : {}
1230
1504
  });
1231
1505
  const details = result.details && typeof result.details === "object" && !Array.isArray(result.details) ? result.details : {};
1232
1506
  const deviceAuth = details.deviceAuth && typeof details.deviceAuth === "object" && !Array.isArray(details.deviceAuth) ? details.deviceAuth : null;
1233
- const deviceMessage = deviceAuth ? ` GitHub device flow: open ${String(deviceAuth.verification_uri ?? deviceAuth.verification_uri_complete ?? "the verification URL returned by the server")} and enter ${String(deviceAuth.user_code ?? "the returned user code")}.` : "";
1234
- prompts.outro?.(`Rig project initialized.${deviceMessage} Next: rig doctor && rig task list`);
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")}.` : "";
1508
+ prompts.outro?.(`Rig project initialized (${placement} placement).${deviceMessage} Next: rig doctor && rig task list`);
1235
1509
  return result;
1236
1510
  }
1237
1511
  async function executeInit(context, args) {
1238
1512
  const parsed = parseInitOptions(args);
1239
- if (parsed.options.yes || parsed.options.server || parsed.options.repoSlug || parsed.options.githubToken || parsed.options.privateStateOnly || parsed.options.repair || parsed.options.githubAuthMethod || parsed.options.remoteCheckout) {
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) {
1240
1521
  if (parsed.rest.length > 0)
1241
- throw new CliError2(`Unexpected arguments: ${parsed.rest.join(" ")}
1242
- Usage: rig init [--server local|remote] [--remote-url <url>] [--repo owner/repo] [--github-auth gh|token|device|skip] [--github-token <token>] [--github-project off|<project-id>] [--remote-checkout managed-clone|current-ref|uploaded-snapshot|existing-path] [--yes]`, 1);
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);
1243
1524
  return runControlPlaneInit(context, parsed.options);
1244
1525
  }
1245
1526
  if (parsed.rest.length > 0)
1246
- throw new CliError2(`Unexpected arguments: ${parsed.rest.join(" ")}
1527
+ throw new CliError(`Unexpected arguments: ${parsed.rest.join(" ")}
1247
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
+ }
1248
1532
  return runInteractiveControlPlaneInit(context, await loadClackPrompts());
1249
1533
  }
1250
1534
  export {
1251
1535
  runInteractiveControlPlaneInit,
1536
+ runDemoInit,
1252
1537
  executeInit,
1253
- buildRigInitConfigSource
1538
+ buildRigInitConfigSource,
1539
+ DEMO_TASKS_RELATIVE_DIR,
1540
+ DEMO_TASKS
1254
1541
  };