@h-rig/init-lib 0.0.6-alpha.158

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.
@@ -0,0 +1,704 @@
1
+ // @bun
2
+ // packages/init-lib/src/dependency-preflight.ts
3
+ import { spawnSync } from "child_process";
4
+ import { existsSync, readFileSync, writeFileSync } from "fs";
5
+ import { basename, join, resolve } from "path";
6
+ var REQUIRED_RIG_CONFIG_PACKAGE_NAMES = ["core", "standard-plugin"];
7
+ var RIG_CONFIG_FILENAMES = ["rig.config.ts", "rig.config.mts", "rig.config.json"];
8
+ var DEPENDENCY_SECTION_NAMES = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
9
+ var INSTALL_CHECK_SECTION_NAMES = ["dependencies", "devDependencies", "optionalDependencies"];
10
+ function hasRigConfig(projectRoot) {
11
+ const root = resolve(projectRoot);
12
+ return RIG_CONFIG_FILENAMES.some((name) => existsSync(join(root, name)));
13
+ }
14
+ function resolveRigConfigPackageVersion(input) {
15
+ const explicit = process.env.RIG_CONFIG_PACKAGE_VERSION?.trim();
16
+ if (explicit)
17
+ return explicit;
18
+ return input.isInstalledCliVersionPublished ? input.installedCliVersion : "latest";
19
+ }
20
+ function readPackageManifest(path) {
21
+ if (!existsSync(path))
22
+ return null;
23
+ return JSON.parse(readFileSync(path, "utf8"));
24
+ }
25
+ function mutableDeps(manifest, key) {
26
+ const source = manifest[key];
27
+ return source && typeof source === "object" && !Array.isArray(source) ? { ...source } : {};
28
+ }
29
+ function rigPackageName(name) {
30
+ const match = name.match(/^@rig\/([^/]+)$/);
31
+ return match?.[1] ?? null;
32
+ }
33
+ function rigPackageNameFromSpecifier(specifier) {
34
+ const match = specifier.match(/^@rig\/([^/]+)/);
35
+ return match?.[1] ?? null;
36
+ }
37
+ function rigPackageSpec(projectRoot, packageName, version) {
38
+ const localManifestPath = join(projectRoot, "packages", packageName, "package.json");
39
+ if (existsSync(localManifestPath)) {
40
+ try {
41
+ const parsed = JSON.parse(readFileSync(localManifestPath, "utf8"));
42
+ if (parsed.name === `@rig/${packageName}`)
43
+ return "workspace:*";
44
+ } catch {}
45
+ }
46
+ return `npm:@h-rig/${packageName}@${version}`;
47
+ }
48
+ function mutableDependencySections(manifest) {
49
+ return Object.fromEntries(DEPENDENCY_SECTION_NAMES.map((section) => [section, mutableDeps(manifest, section)]));
50
+ }
51
+ function discoveredConfigRigSpecs(projectRoot, sections, packageNames, version) {
52
+ const expected = {};
53
+ for (const section of DEPENDENCY_SECTION_NAMES) {
54
+ for (const name of Object.keys(sections[section])) {
55
+ const packageName = rigPackageName(name);
56
+ if (packageName && packageNames.has(packageName)) {
57
+ expected[name] = rigPackageSpec(projectRoot, packageName, version);
58
+ }
59
+ }
60
+ }
61
+ return expected;
62
+ }
63
+ function installedConfigDependencySpecs(sections, packageNames) {
64
+ const expected = {};
65
+ for (const section of INSTALL_CHECK_SECTION_NAMES) {
66
+ for (const [name, spec] of Object.entries(sections[section])) {
67
+ const packageName = rigPackageName(name);
68
+ if (packageName && packageNames.has(packageName) && typeof spec === "string")
69
+ expected[name] = spec;
70
+ }
71
+ }
72
+ return expected;
73
+ }
74
+ function hasFirstPartyRigDependency(manifest) {
75
+ const sections = mutableDependencySections(manifest);
76
+ return DEPENDENCY_SECTION_NAMES.some((section) => Object.keys(sections[section]).some((name) => rigPackageName(name) !== null));
77
+ }
78
+ function rigConfigImportPackageNames(projectRoot) {
79
+ const names = new Set;
80
+ for (const filename of RIG_CONFIG_FILENAMES) {
81
+ const path = join(projectRoot, filename);
82
+ if (!existsSync(path))
83
+ continue;
84
+ const source = readFileSync(path, "utf8");
85
+ const importPattern = /(?:from\s*|import\s*\(\s*|import\s*)["'](@rig\/[^"']+)["']/g;
86
+ for (const match of source.matchAll(importPattern)) {
87
+ const packageName = rigPackageNameFromSpecifier(match[1] ?? "");
88
+ if (packageName)
89
+ names.add(packageName);
90
+ }
91
+ }
92
+ return [...names];
93
+ }
94
+ function withRigConfigDeps(projectRoot, manifest, options) {
95
+ const configPackageNames = new Set(options.requiredConfigPackageNames);
96
+ const sections = mutableDependencySections(manifest);
97
+ let changed = false;
98
+ for (const [name, spec] of Object.entries(discoveredConfigRigSpecs(projectRoot, sections, configPackageNames, options.version))) {
99
+ for (const section of DEPENDENCY_SECTION_NAMES) {
100
+ if (!Object.prototype.hasOwnProperty.call(sections[section], name))
101
+ continue;
102
+ if (sections[section][name] !== spec) {
103
+ sections[section][name] = spec;
104
+ changed = true;
105
+ }
106
+ }
107
+ }
108
+ for (const packageName of options.requiredConfigPackageNames) {
109
+ const name = `@rig/${packageName}`;
110
+ const existsInInstallableSection = INSTALL_CHECK_SECTION_NAMES.some((section) => Object.prototype.hasOwnProperty.call(sections[section], name));
111
+ if (existsInInstallableSection)
112
+ continue;
113
+ sections.devDependencies[name] = rigPackageSpec(projectRoot, packageName, options.version);
114
+ changed = true;
115
+ }
116
+ const next = {
117
+ ...Object.keys(manifest).length > 0 ? manifest : { name: basename(projectRoot) || "rig-project", private: true }
118
+ };
119
+ for (const section of DEPENDENCY_SECTION_NAMES) {
120
+ if (Object.keys(sections[section]).length > 0)
121
+ next[section] = sections[section];
122
+ }
123
+ return { next, changed, expectedInstalled: installedConfigDependencySpecs(sections, configPackageNames) };
124
+ }
125
+ function expectedPublishedVersion(spec) {
126
+ const match = spec.match(/^npm:@h-rig\/[^@]+@(.+)$/);
127
+ if (!match)
128
+ return null;
129
+ return match[1] === "latest" ? null : match[1] ?? null;
130
+ }
131
+ function installedRigPackageHealthy(projectRoot, packageName, expectedSpec) {
132
+ const rigName = rigPackageName(packageName);
133
+ if (!rigName)
134
+ return true;
135
+ const manifest = readPackageManifest(join(projectRoot, "node_modules", "@rig", rigName, "package.json"));
136
+ if (!manifest)
137
+ return false;
138
+ const installedVersion = typeof manifest.version === "string" ? manifest.version.trim() : "";
139
+ const expectedVersion = expectedPublishedVersion(expectedSpec);
140
+ if (expectedVersion && installedVersion !== expectedVersion)
141
+ return false;
142
+ if (!installedVersion)
143
+ return false;
144
+ if (rigName === "core" || rigName === "standard-plugin") {
145
+ const exportsField = manifest.exports;
146
+ if (!exportsField || typeof exportsField !== "object" || Array.isArray(exportsField))
147
+ return false;
148
+ const exportsRecord = exportsField;
149
+ if (rigName === "core" && !("./config" in exportsRecord))
150
+ return false;
151
+ if (rigName === "standard-plugin" && !("./bundle" in exportsRecord))
152
+ return false;
153
+ }
154
+ return true;
155
+ }
156
+ function resolvePackageManagerBinary(manager) {
157
+ if (manager !== "bun")
158
+ return manager;
159
+ const candidates = [
160
+ Bun.which("bun") ?? "",
161
+ process.env.RIG_BUN_PATH?.trim() ?? "",
162
+ process.env.HOME ? join(process.env.HOME, ".bun", "bin", "bun") : "",
163
+ "/opt/homebrew/bin/bun",
164
+ "/usr/local/bin/bun",
165
+ "/usr/bin/bun"
166
+ ];
167
+ for (const candidate of candidates) {
168
+ if (candidate && existsSync(candidate))
169
+ return candidate;
170
+ }
171
+ return "bun";
172
+ }
173
+ function installCommandFor(manifest) {
174
+ const packageManager = typeof manifest.packageManager === "string" ? manifest.packageManager.trim() : "";
175
+ const manager = packageManager.split("@")[0] || "bun";
176
+ switch (manager) {
177
+ case "npm":
178
+ return ["npm", "install", "--ignore-scripts"];
179
+ case "pnpm":
180
+ return ["pnpm", "install", "--ignore-scripts"];
181
+ case "yarn":
182
+ return ["yarn", "install", "--ignore-scripts"];
183
+ default:
184
+ return [resolvePackageManagerBinary("bun"), "install", "--ignore-scripts"];
185
+ }
186
+ }
187
+ function ensureRigConfigDependenciesInstalled(input) {
188
+ const root = resolve(input.projectRoot);
189
+ const version = resolveRigConfigPackageVersion(input);
190
+ const manifestPath = join(root, "package.json");
191
+ const current = readPackageManifest(manifestPath);
192
+ const hasConfig = hasRigConfig(root);
193
+ if (!hasConfig && (!current || !hasFirstPartyRigDependency(current))) {
194
+ return { rewroteManifest: false, ranInstall: false, command: null, version };
195
+ }
196
+ const requiredConfigPackageNames = hasConfig ? [...new Set([...REQUIRED_RIG_CONFIG_PACKAGE_NAMES, ...rigConfigImportPackageNames(root)])] : [];
197
+ const { next, changed, expectedInstalled } = withRigConfigDeps(root, current ?? {}, { requiredConfigPackageNames, version });
198
+ const installNeeded = changed || Object.entries(expectedInstalled).some(([name, spec]) => !installedRigPackageHealthy(root, name, spec));
199
+ if (changed) {
200
+ writeFileSync(manifestPath, `${JSON.stringify(next, null, 2)}
201
+ `, "utf8");
202
+ }
203
+ if (!installNeeded) {
204
+ return { rewroteManifest: changed, ranInstall: false, command: null, version };
205
+ }
206
+ const command = installCommandFor(next);
207
+ const commandBin = command[0];
208
+ if (!commandBin)
209
+ throw new Error("install command resolved to an empty argv");
210
+ const result = spawnSync(commandBin, command.slice(1), {
211
+ cwd: root,
212
+ encoding: "utf8",
213
+ env: process.env
214
+ });
215
+ if (result.status !== 0) {
216
+ const detail = [
217
+ result.error ? String(result.error) : "",
218
+ `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
219
+ ].filter(Boolean).join(`
220
+ `);
221
+ throw new Error(`Rig dependency preflight failed in ${root}: ${command.join(" ")}${detail ? `
222
+ ${detail}` : ""}
223
+ Rig does not delete node_modules or lockfiles automatically; fix the repo/package-manager state explicitly if install recovery is needed.`.trim());
224
+ }
225
+ return { rewroteManifest: changed, ranInstall: true, command: command.join(" "), version };
226
+ }
227
+ // packages/init-lib/src/ensure-config.ts
228
+ import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
229
+ import { resolve as resolve2 } from "path";
230
+ import { stringify as stringifyToml } from "smol-toml";
231
+ import { getStandardPluginsResolver } from "@rig/core/embedded-plugins";
232
+ function rigfigConfigPath(projectRoot) {
233
+ return resolve2(projectRoot, ".rig", "rigfig.toml");
234
+ }
235
+ function isPlainObject(value) {
236
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
237
+ }
238
+ function deepMerge(base, overlay) {
239
+ const out = { ...base };
240
+ for (const [key, value] of Object.entries(overlay)) {
241
+ const prev = out[key];
242
+ out[key] = isPlainObject(prev) && isPlainObject(value) ? deepMerge(prev, value) : value;
243
+ }
244
+ return out;
245
+ }
246
+ function composeDeclarativeConfig(plugins, context) {
247
+ let merged = {};
248
+ for (const plugin of plugins) {
249
+ const fragment = plugin.contributes?.config?.defaults?.(context);
250
+ if (isPlainObject(fragment))
251
+ merged = deepMerge(merged, fragment);
252
+ }
253
+ return merged;
254
+ }
255
+ var HEADER = [
256
+ "# Declarative rig configuration \u2014 the happy path.",
257
+ "# Plain DATA, composed from the standard plugins' defaults. No top-level",
258
+ "# rig.config.ts and no @h-rig/* install: the global rig binary resolves",
259
+ "# plugins from its embedded standard collection. rig created this for you;",
260
+ "# edit it freely (each plugin owns the section it needs).",
261
+ "",
262
+ ""
263
+ ].join(`
264
+ `);
265
+ function writeRigfigConfig(projectRoot, config) {
266
+ mkdirSync(resolve2(projectRoot, ".rig"), { recursive: true });
267
+ const path = rigfigConfigPath(projectRoot);
268
+ writeFileSync2(path, `${HEADER}${stringifyToml(config)}
269
+ `, "utf-8");
270
+ return path;
271
+ }
272
+ function composeAndWriteRigfig(projectRoot, options = {}) {
273
+ const resolver = getStandardPluginsResolver();
274
+ if (!resolver) {
275
+ throw new Error("Cannot write rig config: embedded standard plugins are not registered (seed wiring error).");
276
+ }
277
+ const context = options.repoSlug ? { projectRoot, repoSlug: options.repoSlug } : { projectRoot };
278
+ let composed = composeDeclarativeConfig(resolver(), context);
279
+ if (options.overlay) {
280
+ if (isPlainObject(options.overlay.taskSource) && "taskSource" in composed)
281
+ delete composed.taskSource;
282
+ composed = deepMerge(composed, options.overlay);
283
+ }
284
+ return writeRigfigConfig(projectRoot, composed);
285
+ }
286
+ function ensureDeclarativeConfig(projectRoot, options = {}) {
287
+ const rigfig = rigfigConfigPath(projectRoot);
288
+ if (existsSync2(rigfig))
289
+ return { created: false, path: rigfig };
290
+ for (const codeConfig of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
291
+ if (existsSync2(resolve2(projectRoot, codeConfig))) {
292
+ return { created: false, path: null, reason: `${codeConfig} present` };
293
+ }
294
+ }
295
+ const resolver = getStandardPluginsResolver();
296
+ if (!resolver) {
297
+ return { created: false, path: null, reason: "no embedded standard plugins registered (seed wiring error)" };
298
+ }
299
+ const context = options.repoSlug ? { projectRoot, repoSlug: options.repoSlug } : { projectRoot };
300
+ const composed = composeDeclarativeConfig(resolver(), context);
301
+ if (!isPlainObject(composed.taskSource)) {
302
+ return { created: false, path: null, reason: "no plugin produced a task source (no GitHub remote / explicit repo)" };
303
+ }
304
+ const path = writeRigfigConfig(projectRoot, composed);
305
+ return { created: true, path };
306
+ }
307
+ // packages/init-lib/src/setup.ts
308
+ import { spawnSync as spawnSync2 } from "child_process";
309
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
310
+ import { dirname, resolve as resolve3 } from "path";
311
+ import {
312
+ createGitHubAuthStore,
313
+ probeGitHubRepository,
314
+ resolveGitHubAuthStatus,
315
+ saveGitHubTokenForProject
316
+ } from "@rig/github-lib";
317
+ import { addPlacement, selectPlacement } from "@rig/core/placement";
318
+ var RIG_LABELS_TO_ENSURE = [
319
+ "rig:running",
320
+ "rig:pr-open",
321
+ "rig:ci-fixing",
322
+ "rig:merging",
323
+ "rig:done",
324
+ "rig:needs-attention",
325
+ "rig:ready",
326
+ "rig:blocked",
327
+ "rig:generated"
328
+ ];
329
+ var RIG_LABEL_METADATA = {
330
+ "rig:running": { color: "1d76db", description: "Rig is actively working on this issue." },
331
+ "rig:pr-open": { color: "5319e7", description: "Rig opened a pull request for this issue." },
332
+ "rig:ci-fixing": { color: "fbca04", description: "Rig is fixing CI or review feedback for this issue." },
333
+ "rig:merging": { color: "0052cc", description: "Rig is merging the completed change for this issue." },
334
+ "rig:done": { color: "0e8a16", description: "Rig completed this issue." },
335
+ "rig:needs-attention": { color: "d93f0b", description: "Rig needs operator attention for this issue." },
336
+ "rig:ready": { color: "0e8a16", description: "Rig issue analysis marked this issue ready." },
337
+ "rig:blocked": { color: "d93f0b", description: "Rig issue analysis found blockers for this issue." },
338
+ "rig:generated": { color: "c5def5", description: "Rig generated this follow-up issue." }
339
+ };
340
+ function cleanString(value) {
341
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
342
+ }
343
+ function parseRepoSlugFromRemote(remoteUrl) {
344
+ const trimmed = remoteUrl.trim();
345
+ const match = trimmed.match(/github\.com[:/]([^/\s]+)\/([^/\s.]+)(?:\.git)?$/i);
346
+ return match ? `${match[1]}/${match[2]}` : null;
347
+ }
348
+ function parseRepoSlug(value) {
349
+ const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
350
+ if (!match)
351
+ throw new Error(`Invalid GitHub repo slug "${value}". Expected owner/repo.`);
352
+ return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
353
+ }
354
+ function runSyncCommand(command, input = {}) {
355
+ const executable = command[0];
356
+ if (!executable)
357
+ throw new Error("command is required");
358
+ return (input.spawn ?? spawnSync2)(executable, [...command.slice(1)], {
359
+ cwd: input.cwd,
360
+ encoding: "utf8",
361
+ timeout: input.timeoutMs ?? 1e4,
362
+ env: input.env ?? process.env
363
+ });
364
+ }
365
+ function detectOriginRepoSlug(projectRoot, deps = {}) {
366
+ const result = runSyncCommand(["git", "-C", projectRoot, "remote", "get-url", "origin"], { timeoutMs: 5000, spawn: deps.spawn });
367
+ if (result.status !== 0 || result.error)
368
+ return null;
369
+ return parseRepoSlugFromRemote(result.stdout.trim());
370
+ }
371
+ function connectionStatePath(projectRoot) {
372
+ return resolve3(projectRoot, ".rig", "state", "connection.json");
373
+ }
374
+ function projectLinkStatePath(projectRoot) {
375
+ return resolve3(projectRoot, ".rig", "state", "project-link.json");
376
+ }
377
+ function readJsonRecord(path) {
378
+ if (!existsSync3(path))
379
+ return null;
380
+ try {
381
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
382
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
387
+ function writeJsonFile(path, value) {
388
+ mkdirSync2(dirname(path), { recursive: true });
389
+ writeFileSync3(path, `${JSON.stringify(value, null, 2)}
390
+ `, "utf-8");
391
+ }
392
+ function ensureRigPrivateDirs(projectRoot) {
393
+ mkdirSync2(resolve3(projectRoot, ".rig", "state"), { recursive: true });
394
+ mkdirSync2(resolve3(projectRoot, ".rig", "logs"), { recursive: true });
395
+ mkdirSync2(resolve3(projectRoot, ".rig", "runs"), { recursive: true });
396
+ mkdirSync2(resolve3(projectRoot, ".rig", "tmp"), { recursive: true });
397
+ mkdirSync2(resolve3(projectRoot, "artifacts"), { recursive: true });
398
+ const taskConfigPath = resolve3(projectRoot, ".rig", "task-config.json");
399
+ if (!existsSync3(taskConfigPath))
400
+ writeFileSync3(taskConfigPath, `{}
401
+ `, "utf-8");
402
+ }
403
+ function ensureGitignoreEntries(projectRoot) {
404
+ const path = resolve3(projectRoot, ".gitignore");
405
+ const existing = existsSync3(path) ? readFileSync2(path, "utf-8") : "";
406
+ const lines = new Set(existing.split(/\r?\n/));
407
+ const missing = [".rig/state/", ".rig/logs/", ".rig/runs/", ".rig/tmp/"].filter((entry) => !lines.has(entry));
408
+ if (missing.length === 0)
409
+ return;
410
+ const prefix = existing.length > 0 && !existing.endsWith(`
411
+ `) ? `
412
+ ` : "";
413
+ appendFileSync(path, `${prefix}${missing.join(`
414
+ `)}
415
+ `, "utf-8");
416
+ }
417
+ function writeRigConnectionState(projectRoot, slug, placement) {
418
+ const previous = readJsonRecord(connectionStatePath(projectRoot)) ?? {};
419
+ writeJsonFile(connectionStatePath(projectRoot), {
420
+ ...previous,
421
+ selected: placement.alias,
422
+ project: slug,
423
+ linkedAt: new Date().toISOString()
424
+ });
425
+ writeJsonFile(projectLinkStatePath(projectRoot), {
426
+ repoSlug: slug,
427
+ connection: placement.alias,
428
+ linkedAt: new Date().toISOString()
429
+ });
430
+ if (placement.alias === "local")
431
+ delete process.env.RIG_REMOTE_ALIAS;
432
+ else
433
+ process.env.RIG_REMOTE_ALIAS = placement.alias;
434
+ }
435
+ function writeRigConfig(projectRoot, slug) {
436
+ parseRepoSlug(slug);
437
+ composeAndWriteRigfig(projectRoot, { repoSlug: slug });
438
+ }
439
+ function readRigConfigStatus(projectRoot) {
440
+ const path = rigfigConfigPath(projectRoot);
441
+ if (!existsSync3(path))
442
+ return { exists: false, valid: false, path, slug: null, reason: "missing .rig/rigfig.toml" };
443
+ try {
444
+ const source = readFileSync2(path, "utf-8");
445
+ const owner = source.match(/^\s*owner\s*=\s*["']([^"']+)["']/m)?.[1] ?? null;
446
+ const repoValues = [...source.matchAll(/^\s*repo\s*=\s*["']([^"']+)["']/gm)].map((match) => match[1]).filter(Boolean);
447
+ const taskRepo = repoValues.find((value) => !value.includes("/")) ?? null;
448
+ const projectRepo = repoValues.find((value) => value.includes("/")) ?? null;
449
+ const githubIssues = /^\s*kind\s*=\s*["']github-issues["']/m.test(source);
450
+ const slug = owner && taskRepo ? `${owner}/${taskRepo}` : projectRepo;
451
+ if (!githubIssues || !slug)
452
+ return { exists: true, valid: false, path, slug: slug ?? null, reason: ".rig/rigfig.toml is not a GitHub Issues Rig config" };
453
+ parseRepoSlug(slug);
454
+ return { exists: true, valid: true, path, slug };
455
+ } catch (error) {
456
+ return { exists: true, valid: false, path, slug: null, reason: error instanceof Error ? error.message : String(error) };
457
+ }
458
+ }
459
+ function readRigConnectionStatus(projectRoot) {
460
+ const stateDir = resolve3(projectRoot, ".rig", "state");
461
+ if (!existsSync3(stateDir))
462
+ return { valid: false, selected: null, project: null, reason: "missing .rig/state" };
463
+ const connection = readJsonRecord(connectionStatePath(projectRoot));
464
+ if (!connection)
465
+ return { valid: false, selected: null, project: null, reason: "missing or invalid .rig/state/connection.json" };
466
+ const selected = cleanString(connection.selected);
467
+ const project = cleanString(connection.project);
468
+ if (!selected)
469
+ return { valid: false, selected: null, project, reason: "connection.json is missing selected placement" };
470
+ if (!project)
471
+ return { valid: false, selected, project: null, reason: "connection.json is missing project slug" };
472
+ try {
473
+ parseRepoSlug(project);
474
+ } catch (error) {
475
+ return { valid: false, selected, project, reason: error instanceof Error ? error.message : String(error) };
476
+ }
477
+ return { valid: true, selected, project };
478
+ }
479
+ function detectGhAuth(projectRoot, slug, deps = {}) {
480
+ const user = runSyncCommand(["gh", "api", "user", "--jq", ".login"], { cwd: projectRoot, timeoutMs: 5000, spawn: deps.spawn });
481
+ if (user.status !== 0 || user.error || !user.stdout.trim())
482
+ return null;
483
+ const repo = runSyncCommand(["gh", "repo", "view", slug, "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { cwd: projectRoot, timeoutMs: 5000, spawn: deps.spawn });
484
+ if (repo.status !== 0 || repo.error)
485
+ return { ok: false, source: "gh", login: user.stdout.trim(), detail: (repo.stderr || repo.stdout || "gh cannot access the selected repository").trim() };
486
+ return { ok: true, source: "gh", login: user.stdout.trim(), detail: "gh CLI authentication can access the selected repository" };
487
+ }
488
+ async function validateGitHubAuth(projectRoot, slug, deps = {}) {
489
+ if (!slug)
490
+ return { ok: false, source: "missing", detail: "GitHub repo slug is unknown" };
491
+ const status = resolveGitHubAuthStatus({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
492
+ if (status.signedIn) {
493
+ const store = createGitHubAuthStore(projectRoot);
494
+ if (!status.selectedRepo) {
495
+ store.saveSelectedRepo(slug);
496
+ return { ok: true, source: "stored-token", login: status.login, detail: "stored Rig GitHub token selected for this repo", status: store.status({ oauthConfigured: status.oauthConfigured }) };
497
+ }
498
+ if (status.selectedRepo !== slug)
499
+ return { ok: false, source: "stored-token", login: status.login, detail: `stored GitHub token is scoped to ${status.selectedRepo}, not ${slug}`, status };
500
+ return { ok: true, source: "stored-token", login: status.login, detail: "stored Rig GitHub token is present", status };
501
+ }
502
+ const gh = detectGhAuth(projectRoot, slug, deps);
503
+ if (gh)
504
+ return gh;
505
+ return { ok: false, source: "missing", detail: "Sign in with `gh auth login`, choose Setup \u2192 GitHub auth, or paste a token." };
506
+ }
507
+ async function saveGitHubTokenLocally(projectRoot, token, slug, deps = {}) {
508
+ ensureRigPrivateDirs(projectRoot);
509
+ await saveGitHubTokenForProject({
510
+ projectRoot,
511
+ token,
512
+ tokenSource: "manual-token",
513
+ selectedRepo: slug,
514
+ ...deps.fetchUser ? { fetchUser: deps.fetchUser } : {}
515
+ });
516
+ createGitHubAuthStore(projectRoot).copyToLocalProjectRoot(projectRoot);
517
+ }
518
+ function readGhAuthToken(projectRoot, deps = {}) {
519
+ const result = runSyncCommand(["gh", "auth", "token"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
520
+ if (result.status !== 0 || result.error || !result.stdout.trim())
521
+ throw new Error((result.stderr || result.stdout || "Could not read GitHub token from `gh auth token`.").trim());
522
+ return result.stdout.trim();
523
+ }
524
+ async function ensureGitHubAuth(input) {
525
+ const current = await validateGitHubAuth(input.projectRoot, input.slug, input.deps);
526
+ if (current.ok && !input.token && !input.importGhToken)
527
+ return current;
528
+ if (input.token?.trim())
529
+ await saveGitHubTokenLocally(input.projectRoot, input.token.trim(), input.slug, input.deps);
530
+ else if (input.importGhToken)
531
+ await saveGitHubTokenLocally(input.projectRoot, readGhAuthToken(input.projectRoot, input.deps), input.slug, input.deps);
532
+ return validateGitHubAuth(input.projectRoot, input.slug, input.deps);
533
+ }
534
+ async function ensureGitHubLabels(input) {
535
+ const repo = parseRepoSlug(input.slug);
536
+ const token = input.token?.trim() || createGitHubAuthStore(input.projectRoot).readToken();
537
+ if (token) {
538
+ const fetchLabels = input.deps?.fetch ?? fetch;
539
+ for (const name of RIG_LABELS_TO_ENSURE) {
540
+ const metadata = RIG_LABEL_METADATA[name];
541
+ const response = await fetchLabels(`https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/labels`, {
542
+ method: "POST",
543
+ headers: {
544
+ accept: "application/vnd.github+json",
545
+ authorization: `Bearer ${token}`,
546
+ "content-type": "application/json",
547
+ "user-agent": "rig"
548
+ },
549
+ body: JSON.stringify({ name, color: metadata.color, description: metadata.description })
550
+ });
551
+ if (response.ok)
552
+ continue;
553
+ const text = await response.text().catch(() => "");
554
+ if (response.status === 422 && /already_exists|already exists|exists/i.test(text))
555
+ continue;
556
+ throw new Error(`Could not create GitHub label ${name}: ${response.status} ${text || response.statusText}`);
557
+ }
558
+ return { ok: true, method: "api", labels: RIG_LABELS_TO_ENSURE };
559
+ }
560
+ const gh = detectGhAuth(input.projectRoot, input.slug, input.deps);
561
+ if (!gh?.ok)
562
+ throw new Error("GitHub labels require a stored Rig token or gh auth.");
563
+ for (const name of RIG_LABELS_TO_ENSURE) {
564
+ const metadata = RIG_LABEL_METADATA[name];
565
+ const result = runSyncCommand(["gh", "label", "create", name, "--repo", input.slug, "--color", metadata.color, "--description", metadata.description, "--force"], { cwd: input.projectRoot, timeoutMs: 1e4, spawn: input.deps?.spawn });
566
+ if (result.status !== 0 || result.error)
567
+ throw new Error(`gh label create ${name} failed: ${(result.stderr || result.stdout || result.error?.message || "unknown error").trim()}`);
568
+ }
569
+ return { ok: true, method: "gh", labels: RIG_LABELS_TO_ENSURE };
570
+ }
571
+ function piListContainsRigExtension(output) {
572
+ return output.split(/\r?\n/).some((line) => line.includes("@h-rig/pi-rig") || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(line));
573
+ }
574
+ function splitInstallCommand(value) {
575
+ return value.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^["']|["']$/g, "")) ?? [];
576
+ }
577
+ function ensurePiRigInstalledForSetup(projectRoot, deps = {}) {
578
+ if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1")
579
+ return { ok: true, detail: "fake-pi" };
580
+ let version = runSyncCommand(["pi", "--version"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
581
+ if (version.status !== 0 || version.error) {
582
+ const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim();
583
+ if (!installCommand)
584
+ throw new Error(`Pi/OMP is not available: ${(version.stderr || version.stdout || version.error?.message || "pi --version failed").trim()}. Install Pi/OMP or set RIG_PI_INSTALL_COMMAND.`);
585
+ const parts = splitInstallCommand(installCommand);
586
+ if (parts.length === 0)
587
+ throw new Error("RIG_PI_INSTALL_COMMAND is empty.");
588
+ const install = runSyncCommand(parts, { cwd: projectRoot, timeoutMs: 120000, spawn: deps.spawn });
589
+ if (install.status !== 0 || install.error)
590
+ throw new Error(`Pi/OMP install command failed: ${(install.stderr || install.stdout || install.error?.message || "unknown error").trim()}`);
591
+ version = runSyncCommand(["pi", "--version"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
592
+ if (version.status !== 0 || version.error)
593
+ throw new Error(`Pi/OMP is still unavailable after install: ${(version.stderr || version.stdout || version.error?.message || "pi --version failed").trim()}`);
594
+ }
595
+ let list = runSyncCommand(["pi", "list"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
596
+ if (!piListContainsRigExtension(`${list.stdout}
597
+ ${list.stderr}`)) {
598
+ const packageSource = existsSync3(resolve3(projectRoot, "packages", "pi-rig", "package.json")) ? resolve3(projectRoot, "packages", "pi-rig") : "npm:@h-rig/pi-rig";
599
+ const install = runSyncCommand(["pi", "install", packageSource], { cwd: projectRoot, timeoutMs: 120000, spawn: deps.spawn });
600
+ if (install.status !== 0 || install.error)
601
+ throw new Error(`Could not install/register the Rig OMP extension: ${(install.stderr || install.stdout || install.error?.message || "pi install failed").trim()}`);
602
+ list = runSyncCommand(["pi", "list"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
603
+ if (!piListContainsRigExtension(`${list.stdout}
604
+ ${list.stderr}`))
605
+ throw new Error("Pi/OMP is installed, but `pi list` does not show the Rig extension.");
606
+ }
607
+ return { ok: true, detail: (version.stdout || version.stderr).trim() || "pi available; rig extension registered" };
608
+ }
609
+ async function detectRigStartupStatus(input) {
610
+ const projectRoot = input.projectRoot;
611
+ const config = readRigConfigStatus(projectRoot);
612
+ const state = readRigConnectionStatus(projectRoot);
613
+ const detectedSlug = detectOriginRepoSlug(projectRoot, input.deps);
614
+ const slug = config.slug ?? state.project ?? detectedSlug;
615
+ const reasons = [];
616
+ if (!detectedSlug)
617
+ reasons.push("git origin does not point at a GitHub owner/repo remote");
618
+ if (!config.exists || !config.valid)
619
+ reasons.push(config.reason ?? "rig.config.ts is invalid");
620
+ if (!state.valid)
621
+ reasons.push(state.reason ?? ".rig/state/connection.json is invalid");
622
+ if (config.slug && state.project && config.slug !== state.project)
623
+ reasons.push(`rig.config.ts repo ${config.slug} does not match connection project ${state.project}`);
624
+ if (slug && detectedSlug && slug !== detectedSlug)
625
+ reasons.push(`configured repo ${slug} does not match git origin ${detectedSlug}`);
626
+ const auth = await validateGitHubAuth(projectRoot, slug, input.deps);
627
+ if (!auth.ok)
628
+ reasons.push(auth.detail);
629
+ return { configured: reasons.length === 0, projectRoot, slug, config, state, auth, reasons };
630
+ }
631
+ var detectStartupStatus = detectRigStartupStatus;
632
+ async function applyRigSetupProject(input) {
633
+ const repo = parseRepoSlug(input.slug);
634
+ ensureRigPrivateDirs(input.projectRoot);
635
+ ensureGitignoreEntries(input.projectRoot);
636
+ if (input.placement.alias !== "local" && input.placement.host) {
637
+ addPlacement(input.projectRoot, { alias: input.placement.alias, host: input.placement.host, ...input.placement.port !== undefined ? { port: input.placement.port } : {}, ...input.placement.token !== undefined ? { token: input.placement.token } : {}, select: true });
638
+ } else {
639
+ selectPlacement(input.projectRoot, input.placement.alias);
640
+ }
641
+ writeRigConnectionState(input.projectRoot, repo.slug, input.placement);
642
+ if (input.rewriteConfig)
643
+ writeRigConfig(input.projectRoot, repo.slug);
644
+ const labels = input.ensureLabels === false ? { skipped: true } : await ensureGitHubLabels({ projectRoot: input.projectRoot, slug: repo.slug, deps: input.deps });
645
+ const pi = input.ensurePi === false ? { skipped: true } : ensurePiRigInstalledForSetup(input.projectRoot, input.deps);
646
+ return { repoSlug: repo.slug, placement: input.placement.alias, configWritten: input.rewriteConfig, labels, pi };
647
+ }
648
+ async function runSetup(input) {
649
+ const repo = parseRepoSlug(input.slug);
650
+ const auth = await ensureGitHubAuth({ projectRoot: input.projectRoot, slug: repo.slug, token: input.githubToken, importGhToken: input.importGhToken, deps: input.deps });
651
+ if (!auth.ok)
652
+ throw new Error(auth.detail);
653
+ const token = createGitHubAuthStore(input.projectRoot).readToken();
654
+ const probe = await probeGitHubRepository({
655
+ owner: repo.owner,
656
+ repo: repo.repo,
657
+ token,
658
+ scopes: auth.status?.scopes ?? [],
659
+ ...input.deps?.fetch ? { fetchRepository: input.deps.fetch } : {}
660
+ });
661
+ if (!probe.ok)
662
+ throw new Error(probe.message);
663
+ const result = await applyRigSetupProject({
664
+ projectRoot: input.projectRoot,
665
+ slug: repo.slug,
666
+ placement: input.placement,
667
+ rewriteConfig: input.rewriteConfig ?? true,
668
+ ensurePi: input.ensurePi,
669
+ ensureLabels: input.ensureLabels,
670
+ deps: input.deps
671
+ });
672
+ const status = await detectRigStartupStatus({ projectRoot: input.projectRoot, deps: input.deps });
673
+ if (!status.configured)
674
+ throw new Error(`Setup wrote state but doctor still reports: ${status.reasons.join("; ")}`);
675
+ return { ...result, status };
676
+ }
677
+ export {
678
+ writeRigfigConfig,
679
+ writeRigConnectionState,
680
+ writeRigConfig,
681
+ validateGitHubAuth,
682
+ saveGitHubTokenLocally,
683
+ runSetup,
684
+ rigfigConfigPath,
685
+ readRigConnectionStatus,
686
+ readRigConfigStatus,
687
+ readGhAuthToken,
688
+ parseRepoSlugFromRemote,
689
+ parseRepoSlug,
690
+ ensureRigPrivateDirs,
691
+ ensureRigConfigDependenciesInstalled,
692
+ ensurePiRigInstalledForSetup,
693
+ ensureGitignoreEntries,
694
+ ensureGitHubLabels,
695
+ ensureGitHubAuth,
696
+ ensureDeclarativeConfig,
697
+ detectStartupStatus,
698
+ detectRigStartupStatus,
699
+ detectOriginRepoSlug,
700
+ detectGhAuth,
701
+ composeDeclarativeConfig,
702
+ composeAndWriteRigfig,
703
+ applyRigSetupProject
704
+ };