@h-rig/doctor-plugin 0.0.6-alpha.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # @h-rig/doctor-plugin
@@ -0,0 +1 @@
1
+ export * from "./plugin";
@@ -0,0 +1,217 @@
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);
17
+
18
+ // packages/doctor-plugin/src/service.ts
19
+ var exports_service = {};
20
+ __export(exports_service, {
21
+ runDoctorChecks: () => runDoctorChecks,
22
+ doctorRunner: () => doctorRunner
23
+ });
24
+ import { existsSync, readFileSync } from "fs";
25
+ import { resolve } from "path";
26
+ import { isSupportedBunVersion, MIN_SUPPORTED_BUN_VERSION } from "@rig/runtime/control-plane/setup-version";
27
+ import { checkGitHubRepoPermissions, resolveGitHubAuthStatus } from "@rig/github-provider-plugin";
28
+ import { loadConfig } from "@rig/core/load-config";
29
+ import { resolveRigIdentity, resolveRigIdentityFilter } from "@rig/runtime/control-plane/identity";
30
+ import { readSelection, listPlacements } from "@rig/runtime/control-plane/placement";
31
+ import { listRuns } from "@rig/run-worker/runs";
32
+ function check(id, label, status, detail, remediation) {
33
+ return { id, label, status, level: status === "pass" ? "ok" : status, ...detail ? { detail } : {}, ...remediation ? { remediation } : {} };
34
+ }
35
+ function errorMessage(error) {
36
+ return error instanceof Error ? error.message : String(error);
37
+ }
38
+ function isAuthenticated(payload) {
39
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
40
+ return false;
41
+ const record = payload;
42
+ return record.signedIn === true || record.authenticated === true || record.status === "authenticated" || record.ok === true && typeof record.login === "string" && record.login.trim().length > 0;
43
+ }
44
+ function repoSlugFromConfig(config) {
45
+ const record = config && typeof config === "object" && !Array.isArray(config) ? config : null;
46
+ const project = record?.project && typeof record.project === "object" && !Array.isArray(record.project) ? record.project : null;
47
+ if (typeof project?.repo === "string" && /^[^/\s]+\/[^/\s]+$/.test(project.repo))
48
+ return project.repo;
49
+ if (typeof project?.name === "string" && /^[^/\s]+\/[^/\s]+$/.test(project.name))
50
+ return project.name;
51
+ const taskSource = record?.taskSource && typeof record.taskSource === "object" && !Array.isArray(record.taskSource) ? record.taskSource : null;
52
+ if (typeof taskSource?.owner === "string" && typeof taskSource?.repo === "string")
53
+ return `${taskSource.owner}/${taskSource.repo}`;
54
+ return null;
55
+ }
56
+ function loadFallbackConfig(projectRoot) {
57
+ for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
58
+ const path = resolve(projectRoot, name);
59
+ if (!existsSync(path))
60
+ continue;
61
+ try {
62
+ const source = readFileSync(path, "utf8");
63
+ if (name.endsWith(".json"))
64
+ return JSON.parse(source);
65
+ const owner = source.match(/owner\s*:\s*["']([^"']+)["']/)?.[1];
66
+ const repo = source.match(/repo\s*:\s*["']([^"']+)["']/)?.[1];
67
+ const projectRepo = source.match(/project\s*:\s*{[^}]*repo\s*:\s*["']([^"']+)["']/s)?.[1];
68
+ const taskKind = source.match(/taskSource\s*:\s*{[^}]*kind\s*:\s*["']([^"']+)["']/s)?.[1];
69
+ return { ...projectRepo ? { project: { name: projectRepo, repo: projectRepo } } : {}, ...taskKind ? { taskSource: { kind: taskKind, ...owner ? { owner } : {}, ...repo ? { repo } : {} } } : {} };
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ function githubProjectsCheck(config) {
77
+ const github = config?.github;
78
+ const projects = github?.projects;
79
+ if (!projects?.enabled)
80
+ return check("github-projects", "GitHub Projects status sync", "warn", "disabled or not configured", "Current OMP flow does not require GitHub Projects status sync.");
81
+ if (projects.projectId && projects.statusFieldId)
82
+ return check("github-projects", "GitHub Projects status sync", "pass", `project ${projects.projectId}`);
83
+ return check("github-projects", "GitHub Projects status sync", "fail", "enabled but projectId/statusFieldId is incomplete", "Configure github.projects.projectId and github.projects.statusFieldId, or disable github.projects.enabled.");
84
+ }
85
+ function permissionAllowsPr(payload) {
86
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
87
+ return null;
88
+ const record = payload;
89
+ if (record.canOpenPullRequest === true || record.pullRequests === true || record.push === true || record.maintain === true || record.admin === true)
90
+ return true;
91
+ if (record.canOpenPullRequest === false || record.pullRequests === false || record.push === false)
92
+ return false;
93
+ const permissions = record.permissions;
94
+ if (permissions && typeof permissions === "object" && !Array.isArray(permissions)) {
95
+ const p = permissions;
96
+ if (p.push === true || p.maintain === true || p.admin === true)
97
+ return true;
98
+ if (p.push === false && p.admin !== true)
99
+ return false;
100
+ }
101
+ return null;
102
+ }
103
+ function prMergeCheck(config) {
104
+ const record = config && typeof config === "object" && !Array.isArray(config) ? config : null;
105
+ const pr = record?.pr;
106
+ const merge = record?.merge;
107
+ if (pr?.mode === "off" || merge?.mode === "off")
108
+ return check("pr-merge", "PR/merge automation", "warn", "automatic PR or merge is disabled", "Set pr.mode and merge.mode to auto for autonomous YOLO runs.");
109
+ return check("pr-merge", "PR/merge automation", "pass", `pr=${pr?.mode ?? "auto"}, merge=${merge?.mode ?? "auto"}, method=${merge?.method ?? "repo-default"}`);
110
+ }
111
+ async function defaultPiChecks() {
112
+ const pi = Bun.which("pi");
113
+ return [{ ok: Boolean(pi), label: "pi", detail: pi ?? undefined, hint: pi ? undefined : "Install Pi/OMP and the Rig extension." }];
114
+ }
115
+ async function defaultRequestJson(projectRoot, pathname, _init) {
116
+ if (pathname === "/api/github/auth/status")
117
+ return { ok: true, ...resolveGitHubAuthStatus({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) };
118
+ if (pathname === "/api/github/repo/permissions")
119
+ return checkGitHubRepoPermissions({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
120
+ return { ok: false, error: `No in-process handler for ${pathname}` };
121
+ }
122
+ async function runDoctorChecks(options) {
123
+ const projectRoot = options.projectRoot;
124
+ const checks = [];
125
+ const which = options.which ?? ((binary) => Bun.which(binary));
126
+ const bunVersion = options.bunVersion ?? Bun.version;
127
+ const request = options.requestJson ?? ((pathname, init) => defaultRequestJson(projectRoot, pathname, init));
128
+ const progress = options.onProgress ?? (() => {});
129
+ const load = options.loadConfig ?? ((root) => loadConfig(root).catch(() => null));
130
+ progress("Checking local toolchain\u2026");
131
+ checks.push(check("bun", "bun", isSupportedBunVersion(bunVersion) ? "pass" : "fail", `found ${bunVersion}`, `Install Bun ${MIN_SUPPORTED_BUN_VERSION} or newer.`));
132
+ checks.push(check("git", "git", which("git") ? "pass" : "fail", which("git") ?? undefined, "Install git and ensure it is on PATH."));
133
+ checks.push(check("jq", "jq", which("jq") ? "pass" : "warn", which("jq") ?? undefined, "Install jq (for example `brew install jq`)."));
134
+ progress("Loading rig.config\u2026");
135
+ const loadedConfig = await load(projectRoot);
136
+ const config = loadedConfig ?? loadFallbackConfig(projectRoot);
137
+ const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync(resolve(projectRoot, name)));
138
+ 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 or fix rig.config."));
139
+ const taskSourceKind = config?.taskSource?.kind;
140
+ 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."));
141
+ const repo = readSelection(projectRoot);
142
+ const configuredRepo = repoSlugFromConfig(config);
143
+ checks.push(repo ? check("project-link", "repo selected for local setup", repo.project ? "pass" : "warn", repo.selected + (repo.project ? ` -> ${repo.project}` : ""), repo.project ? undefined : `Run rig init --repo ${configuredRepo ?? "owner/repo"} to repair the local project link.`) : check("project-link", "repo selected for local setup", "warn", "missing .rig/state/connection.json", "Current UX can run without this compatibility file; run setup from the cockpit if GitHub Issues integration is needed."));
144
+ progress("Checking GitHub auth\u2026");
145
+ try {
146
+ const auth = await request("/api/github/auth/status");
147
+ checks.push(isAuthenticated(auth) ? check("github-auth", "GitHub auth", "pass") : check("github-auth", "GitHub auth", "fail", "not authenticated", "Run Rig GitHub auth setup or import a token."));
148
+ } catch (error) {
149
+ checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub access."));
150
+ }
151
+ progress("Checking GitHub repo permissions\u2026");
152
+ try {
153
+ const permissions = await request("/api/github/repo/permissions");
154
+ const allowed = permissionAllowsPr(permissions);
155
+ 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."));
156
+ } catch (error) {
157
+ checks.push(check("github-repo-permissions", "GitHub repo PR permissions", "warn", errorMessage(error), "Ensure the token can open PRs."));
158
+ }
159
+ if (taskSourceKind === "github-issues")
160
+ checks.push(check("gh", "gh CLI fallback", which("gh") ? "pass" : "warn", which("gh") ?? undefined, "Install gh locally for fallback operations."));
161
+ if (config)
162
+ checks.push(githubProjectsCheck(config), prMergeCheck(config));
163
+ progress("Checking Pi installation\u2026");
164
+ const piChecks = await (options.piChecks ?? defaultPiChecks)().catch((error) => [{ ok: false, label: "pi/pi-rig checks", detail: undefined, hint: errorMessage(error) }]);
165
+ for (const pi of piChecks)
166
+ 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.")));
167
+ const identity = options.identityContext ? resolveRigIdentity(options.identityContext) : null;
168
+ if (options.identityContext) {
169
+ const filter = resolveRigIdentityFilter(options.identityContext);
170
+ checks.push({ label: "identity", status: identity ? "pass" : "warn", level: identity ? "ok" : "warn", detail: identity?.source ?? "GitHub identity unavailable" });
171
+ checks.push({ label: "github-repo", status: filter?.selectedRepo ? "pass" : "warn", level: filter?.selectedRepo ? "ok" : "warn", detail: filter?.selectedRepo ?? "repo not selected" });
172
+ }
173
+ try {
174
+ const runs = await (options.listRuns ?? listRuns)(projectRoot);
175
+ checks.push({ label: "run-discovery", status: "pass", level: "ok", detail: `${runs.length} run session(s)` });
176
+ } catch (error) {
177
+ checks.push({ label: "run-discovery", status: "fail", level: "fail", detail: errorMessage(error) });
178
+ }
179
+ try {
180
+ const targets = (options.listPlacements ?? listPlacements)(projectRoot);
181
+ checks.push({ label: "relay-targets", status: targets.length > 0 ? "pass" : "warn", level: targets.length > 0 ? "ok" : "warn", detail: `${targets.length} target(s)` });
182
+ } catch (error) {
183
+ checks.push({ label: "relay-targets", status: "warn", level: "warn", detail: errorMessage(error) });
184
+ }
185
+ return checks;
186
+ }
187
+ var doctorRunner;
188
+ var init_service = __esm(() => {
189
+ doctorRunner = { runDoctorChecks };
190
+ });
191
+
192
+ // packages/doctor-plugin/src/plugin.ts
193
+ import { definePlugin } from "@rig/core/config";
194
+ import { DOCTOR_RUNNER_CAPABILITY_ID } from "@rig/contracts";
195
+ var DOCTOR_PLUGIN_NAME = "@rig/doctor-plugin";
196
+ var doctorPlugin = definePlugin({
197
+ name: DOCTOR_PLUGIN_NAME,
198
+ version: "0.0.0-alpha.1",
199
+ contributes: {
200
+ capabilities: [
201
+ {
202
+ id: DOCTOR_RUNNER_CAPABILITY_ID,
203
+ title: "Diagnostic doctor checks",
204
+ description: "Local toolchain, rig.config loadability, GitHub auth/permissions, Pi, and run/relay diagnostics with remediation copy.",
205
+ run: async () => (await Promise.resolve().then(() => (init_service(), exports_service))).doctorRunner
206
+ }
207
+ ]
208
+ }
209
+ });
210
+ function createDoctorPlugin() {
211
+ return doctorPlugin;
212
+ }
213
+ export {
214
+ doctorPlugin,
215
+ createDoctorPlugin,
216
+ DOCTOR_PLUGIN_NAME
217
+ };
@@ -0,0 +1,4 @@
1
+ export declare const DOCTOR_PLUGIN_NAME = "@rig/doctor-plugin";
2
+ export declare const doctorPlugin: import("@rig/core").RigPlugin;
3
+ export declare function createDoctorPlugin(): import("@rig/core").RigPlugin;
4
+ export default doctorPlugin;
@@ -0,0 +1,219 @@
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);
17
+
18
+ // packages/doctor-plugin/src/service.ts
19
+ var exports_service = {};
20
+ __export(exports_service, {
21
+ runDoctorChecks: () => runDoctorChecks,
22
+ doctorRunner: () => doctorRunner
23
+ });
24
+ import { existsSync, readFileSync } from "fs";
25
+ import { resolve } from "path";
26
+ import { isSupportedBunVersion, MIN_SUPPORTED_BUN_VERSION } from "@rig/runtime/control-plane/setup-version";
27
+ import { checkGitHubRepoPermissions, resolveGitHubAuthStatus } from "@rig/github-provider-plugin";
28
+ import { loadConfig } from "@rig/core/load-config";
29
+ import { resolveRigIdentity, resolveRigIdentityFilter } from "@rig/runtime/control-plane/identity";
30
+ import { readSelection, listPlacements } from "@rig/runtime/control-plane/placement";
31
+ import { listRuns } from "@rig/run-worker/runs";
32
+ function check(id, label, status, detail, remediation) {
33
+ return { id, label, status, level: status === "pass" ? "ok" : status, ...detail ? { detail } : {}, ...remediation ? { remediation } : {} };
34
+ }
35
+ function errorMessage(error) {
36
+ return error instanceof Error ? error.message : String(error);
37
+ }
38
+ function isAuthenticated(payload) {
39
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
40
+ return false;
41
+ const record = payload;
42
+ return record.signedIn === true || record.authenticated === true || record.status === "authenticated" || record.ok === true && typeof record.login === "string" && record.login.trim().length > 0;
43
+ }
44
+ function repoSlugFromConfig(config) {
45
+ const record = config && typeof config === "object" && !Array.isArray(config) ? config : null;
46
+ const project = record?.project && typeof record.project === "object" && !Array.isArray(record.project) ? record.project : null;
47
+ if (typeof project?.repo === "string" && /^[^/\s]+\/[^/\s]+$/.test(project.repo))
48
+ return project.repo;
49
+ if (typeof project?.name === "string" && /^[^/\s]+\/[^/\s]+$/.test(project.name))
50
+ return project.name;
51
+ const taskSource = record?.taskSource && typeof record.taskSource === "object" && !Array.isArray(record.taskSource) ? record.taskSource : null;
52
+ if (typeof taskSource?.owner === "string" && typeof taskSource?.repo === "string")
53
+ return `${taskSource.owner}/${taskSource.repo}`;
54
+ return null;
55
+ }
56
+ function loadFallbackConfig(projectRoot) {
57
+ for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
58
+ const path = resolve(projectRoot, name);
59
+ if (!existsSync(path))
60
+ continue;
61
+ try {
62
+ const source = readFileSync(path, "utf8");
63
+ if (name.endsWith(".json"))
64
+ return JSON.parse(source);
65
+ const owner = source.match(/owner\s*:\s*["']([^"']+)["']/)?.[1];
66
+ const repo = source.match(/repo\s*:\s*["']([^"']+)["']/)?.[1];
67
+ const projectRepo = source.match(/project\s*:\s*{[^}]*repo\s*:\s*["']([^"']+)["']/s)?.[1];
68
+ const taskKind = source.match(/taskSource\s*:\s*{[^}]*kind\s*:\s*["']([^"']+)["']/s)?.[1];
69
+ return { ...projectRepo ? { project: { name: projectRepo, repo: projectRepo } } : {}, ...taskKind ? { taskSource: { kind: taskKind, ...owner ? { owner } : {}, ...repo ? { repo } : {} } } : {} };
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ function githubProjectsCheck(config) {
77
+ const github = config?.github;
78
+ const projects = github?.projects;
79
+ if (!projects?.enabled)
80
+ return check("github-projects", "GitHub Projects status sync", "warn", "disabled or not configured", "Current OMP flow does not require GitHub Projects status sync.");
81
+ if (projects.projectId && projects.statusFieldId)
82
+ return check("github-projects", "GitHub Projects status sync", "pass", `project ${projects.projectId}`);
83
+ return check("github-projects", "GitHub Projects status sync", "fail", "enabled but projectId/statusFieldId is incomplete", "Configure github.projects.projectId and github.projects.statusFieldId, or disable github.projects.enabled.");
84
+ }
85
+ function permissionAllowsPr(payload) {
86
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
87
+ return null;
88
+ const record = payload;
89
+ if (record.canOpenPullRequest === true || record.pullRequests === true || record.push === true || record.maintain === true || record.admin === true)
90
+ return true;
91
+ if (record.canOpenPullRequest === false || record.pullRequests === false || record.push === false)
92
+ return false;
93
+ const permissions = record.permissions;
94
+ if (permissions && typeof permissions === "object" && !Array.isArray(permissions)) {
95
+ const p = permissions;
96
+ if (p.push === true || p.maintain === true || p.admin === true)
97
+ return true;
98
+ if (p.push === false && p.admin !== true)
99
+ return false;
100
+ }
101
+ return null;
102
+ }
103
+ function prMergeCheck(config) {
104
+ const record = config && typeof config === "object" && !Array.isArray(config) ? config : null;
105
+ const pr = record?.pr;
106
+ const merge = record?.merge;
107
+ if (pr?.mode === "off" || merge?.mode === "off")
108
+ return check("pr-merge", "PR/merge automation", "warn", "automatic PR or merge is disabled", "Set pr.mode and merge.mode to auto for autonomous YOLO runs.");
109
+ return check("pr-merge", "PR/merge automation", "pass", `pr=${pr?.mode ?? "auto"}, merge=${merge?.mode ?? "auto"}, method=${merge?.method ?? "repo-default"}`);
110
+ }
111
+ async function defaultPiChecks() {
112
+ const pi = Bun.which("pi");
113
+ return [{ ok: Boolean(pi), label: "pi", detail: pi ?? undefined, hint: pi ? undefined : "Install Pi/OMP and the Rig extension." }];
114
+ }
115
+ async function defaultRequestJson(projectRoot, pathname, _init) {
116
+ if (pathname === "/api/github/auth/status")
117
+ return { ok: true, ...resolveGitHubAuthStatus({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) };
118
+ if (pathname === "/api/github/repo/permissions")
119
+ return checkGitHubRepoPermissions({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
120
+ return { ok: false, error: `No in-process handler for ${pathname}` };
121
+ }
122
+ async function runDoctorChecks(options) {
123
+ const projectRoot = options.projectRoot;
124
+ const checks = [];
125
+ const which = options.which ?? ((binary) => Bun.which(binary));
126
+ const bunVersion = options.bunVersion ?? Bun.version;
127
+ const request = options.requestJson ?? ((pathname, init) => defaultRequestJson(projectRoot, pathname, init));
128
+ const progress = options.onProgress ?? (() => {});
129
+ const load = options.loadConfig ?? ((root) => loadConfig(root).catch(() => null));
130
+ progress("Checking local toolchain\u2026");
131
+ checks.push(check("bun", "bun", isSupportedBunVersion(bunVersion) ? "pass" : "fail", `found ${bunVersion}`, `Install Bun ${MIN_SUPPORTED_BUN_VERSION} or newer.`));
132
+ checks.push(check("git", "git", which("git") ? "pass" : "fail", which("git") ?? undefined, "Install git and ensure it is on PATH."));
133
+ checks.push(check("jq", "jq", which("jq") ? "pass" : "warn", which("jq") ?? undefined, "Install jq (for example `brew install jq`)."));
134
+ progress("Loading rig.config\u2026");
135
+ const loadedConfig = await load(projectRoot);
136
+ const config = loadedConfig ?? loadFallbackConfig(projectRoot);
137
+ const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync(resolve(projectRoot, name)));
138
+ 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 or fix rig.config."));
139
+ const taskSourceKind = config?.taskSource?.kind;
140
+ 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."));
141
+ const repo = readSelection(projectRoot);
142
+ const configuredRepo = repoSlugFromConfig(config);
143
+ checks.push(repo ? check("project-link", "repo selected for local setup", repo.project ? "pass" : "warn", repo.selected + (repo.project ? ` -> ${repo.project}` : ""), repo.project ? undefined : `Run rig init --repo ${configuredRepo ?? "owner/repo"} to repair the local project link.`) : check("project-link", "repo selected for local setup", "warn", "missing .rig/state/connection.json", "Current UX can run without this compatibility file; run setup from the cockpit if GitHub Issues integration is needed."));
144
+ progress("Checking GitHub auth\u2026");
145
+ try {
146
+ const auth = await request("/api/github/auth/status");
147
+ checks.push(isAuthenticated(auth) ? check("github-auth", "GitHub auth", "pass") : check("github-auth", "GitHub auth", "fail", "not authenticated", "Run Rig GitHub auth setup or import a token."));
148
+ } catch (error) {
149
+ checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub access."));
150
+ }
151
+ progress("Checking GitHub repo permissions\u2026");
152
+ try {
153
+ const permissions = await request("/api/github/repo/permissions");
154
+ const allowed = permissionAllowsPr(permissions);
155
+ 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."));
156
+ } catch (error) {
157
+ checks.push(check("github-repo-permissions", "GitHub repo PR permissions", "warn", errorMessage(error), "Ensure the token can open PRs."));
158
+ }
159
+ if (taskSourceKind === "github-issues")
160
+ checks.push(check("gh", "gh CLI fallback", which("gh") ? "pass" : "warn", which("gh") ?? undefined, "Install gh locally for fallback operations."));
161
+ if (config)
162
+ checks.push(githubProjectsCheck(config), prMergeCheck(config));
163
+ progress("Checking Pi installation\u2026");
164
+ const piChecks = await (options.piChecks ?? defaultPiChecks)().catch((error) => [{ ok: false, label: "pi/pi-rig checks", detail: undefined, hint: errorMessage(error) }]);
165
+ for (const pi of piChecks)
166
+ 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.")));
167
+ const identity = options.identityContext ? resolveRigIdentity(options.identityContext) : null;
168
+ if (options.identityContext) {
169
+ const filter = resolveRigIdentityFilter(options.identityContext);
170
+ checks.push({ label: "identity", status: identity ? "pass" : "warn", level: identity ? "ok" : "warn", detail: identity?.source ?? "GitHub identity unavailable" });
171
+ checks.push({ label: "github-repo", status: filter?.selectedRepo ? "pass" : "warn", level: filter?.selectedRepo ? "ok" : "warn", detail: filter?.selectedRepo ?? "repo not selected" });
172
+ }
173
+ try {
174
+ const runs = await (options.listRuns ?? listRuns)(projectRoot);
175
+ checks.push({ label: "run-discovery", status: "pass", level: "ok", detail: `${runs.length} run session(s)` });
176
+ } catch (error) {
177
+ checks.push({ label: "run-discovery", status: "fail", level: "fail", detail: errorMessage(error) });
178
+ }
179
+ try {
180
+ const targets = (options.listPlacements ?? listPlacements)(projectRoot);
181
+ checks.push({ label: "relay-targets", status: targets.length > 0 ? "pass" : "warn", level: targets.length > 0 ? "ok" : "warn", detail: `${targets.length} target(s)` });
182
+ } catch (error) {
183
+ checks.push({ label: "relay-targets", status: "warn", level: "warn", detail: errorMessage(error) });
184
+ }
185
+ return checks;
186
+ }
187
+ var doctorRunner;
188
+ var init_service = __esm(() => {
189
+ doctorRunner = { runDoctorChecks };
190
+ });
191
+
192
+ // packages/doctor-plugin/src/plugin.ts
193
+ import { definePlugin } from "@rig/core/config";
194
+ import { DOCTOR_RUNNER_CAPABILITY_ID } from "@rig/contracts";
195
+ var DOCTOR_PLUGIN_NAME = "@rig/doctor-plugin";
196
+ var doctorPlugin = definePlugin({
197
+ name: DOCTOR_PLUGIN_NAME,
198
+ version: "0.0.0-alpha.1",
199
+ contributes: {
200
+ capabilities: [
201
+ {
202
+ id: DOCTOR_RUNNER_CAPABILITY_ID,
203
+ title: "Diagnostic doctor checks",
204
+ description: "Local toolchain, rig.config loadability, GitHub auth/permissions, Pi, and run/relay diagnostics with remediation copy.",
205
+ run: async () => (await Promise.resolve().then(() => (init_service(), exports_service))).doctorRunner
206
+ }
207
+ ]
208
+ }
209
+ });
210
+ function createDoctorPlugin() {
211
+ return doctorPlugin;
212
+ }
213
+ var plugin_default = doctorPlugin;
214
+ export {
215
+ doctorPlugin,
216
+ plugin_default as default,
217
+ createDoctorPlugin,
218
+ DOCTOR_PLUGIN_NAME
219
+ };
@@ -0,0 +1,5 @@
1
+ import type { DoctorCheck } from "@rig/contracts";
2
+ import type { DoctorService, RunDoctorChecksOptions } from "@rig/runtime/control-plane/doctor-service-port";
3
+ export declare function runDoctorChecks(options: RunDoctorChecksOptions): Promise<DoctorCheck[]>;
4
+ /** The concrete diagnostic service the runtime port resolves and consumes. */
5
+ export declare const doctorRunner: DoctorService;
@@ -0,0 +1,170 @@
1
+ // @bun
2
+ // packages/doctor-plugin/src/service.ts
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { resolve } from "path";
5
+ import { isSupportedBunVersion, MIN_SUPPORTED_BUN_VERSION } from "@rig/runtime/control-plane/setup-version";
6
+ import { checkGitHubRepoPermissions, resolveGitHubAuthStatus } from "@rig/github-provider-plugin";
7
+ import { loadConfig } from "@rig/core/load-config";
8
+ import { resolveRigIdentity, resolveRigIdentityFilter } from "@rig/runtime/control-plane/identity";
9
+ import { readSelection, listPlacements } from "@rig/runtime/control-plane/placement";
10
+ import { listRuns } from "@rig/run-worker/runs";
11
+ function check(id, label, status, detail, remediation) {
12
+ return { id, label, status, level: status === "pass" ? "ok" : status, ...detail ? { detail } : {}, ...remediation ? { remediation } : {} };
13
+ }
14
+ function errorMessage(error) {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ function isAuthenticated(payload) {
18
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
19
+ return false;
20
+ const record = payload;
21
+ return record.signedIn === true || record.authenticated === true || record.status === "authenticated" || record.ok === true && typeof record.login === "string" && record.login.trim().length > 0;
22
+ }
23
+ function repoSlugFromConfig(config) {
24
+ const record = config && typeof config === "object" && !Array.isArray(config) ? config : null;
25
+ const project = record?.project && typeof record.project === "object" && !Array.isArray(record.project) ? record.project : null;
26
+ if (typeof project?.repo === "string" && /^[^/\s]+\/[^/\s]+$/.test(project.repo))
27
+ return project.repo;
28
+ if (typeof project?.name === "string" && /^[^/\s]+\/[^/\s]+$/.test(project.name))
29
+ return project.name;
30
+ const taskSource = record?.taskSource && typeof record.taskSource === "object" && !Array.isArray(record.taskSource) ? record.taskSource : null;
31
+ if (typeof taskSource?.owner === "string" && typeof taskSource?.repo === "string")
32
+ return `${taskSource.owner}/${taskSource.repo}`;
33
+ return null;
34
+ }
35
+ function loadFallbackConfig(projectRoot) {
36
+ for (const name of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
37
+ const path = resolve(projectRoot, name);
38
+ if (!existsSync(path))
39
+ continue;
40
+ try {
41
+ const source = readFileSync(path, "utf8");
42
+ if (name.endsWith(".json"))
43
+ return JSON.parse(source);
44
+ const owner = source.match(/owner\s*:\s*["']([^"']+)["']/)?.[1];
45
+ const repo = source.match(/repo\s*:\s*["']([^"']+)["']/)?.[1];
46
+ const projectRepo = source.match(/project\s*:\s*{[^}]*repo\s*:\s*["']([^"']+)["']/s)?.[1];
47
+ const taskKind = source.match(/taskSource\s*:\s*{[^}]*kind\s*:\s*["']([^"']+)["']/s)?.[1];
48
+ return { ...projectRepo ? { project: { name: projectRepo, repo: projectRepo } } : {}, ...taskKind ? { taskSource: { kind: taskKind, ...owner ? { owner } : {}, ...repo ? { repo } : {} } } : {} };
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ function githubProjectsCheck(config) {
56
+ const github = config?.github;
57
+ const projects = github?.projects;
58
+ if (!projects?.enabled)
59
+ return check("github-projects", "GitHub Projects status sync", "warn", "disabled or not configured", "Current OMP flow does not require GitHub Projects status sync.");
60
+ if (projects.projectId && projects.statusFieldId)
61
+ return check("github-projects", "GitHub Projects status sync", "pass", `project ${projects.projectId}`);
62
+ return check("github-projects", "GitHub Projects status sync", "fail", "enabled but projectId/statusFieldId is incomplete", "Configure github.projects.projectId and github.projects.statusFieldId, or disable github.projects.enabled.");
63
+ }
64
+ function permissionAllowsPr(payload) {
65
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
66
+ return null;
67
+ const record = payload;
68
+ if (record.canOpenPullRequest === true || record.pullRequests === true || record.push === true || record.maintain === true || record.admin === true)
69
+ return true;
70
+ if (record.canOpenPullRequest === false || record.pullRequests === false || record.push === false)
71
+ return false;
72
+ const permissions = record.permissions;
73
+ if (permissions && typeof permissions === "object" && !Array.isArray(permissions)) {
74
+ const p = permissions;
75
+ if (p.push === true || p.maintain === true || p.admin === true)
76
+ return true;
77
+ if (p.push === false && p.admin !== true)
78
+ return false;
79
+ }
80
+ return null;
81
+ }
82
+ function prMergeCheck(config) {
83
+ const record = config && typeof config === "object" && !Array.isArray(config) ? config : null;
84
+ const pr = record?.pr;
85
+ const merge = record?.merge;
86
+ if (pr?.mode === "off" || merge?.mode === "off")
87
+ return check("pr-merge", "PR/merge automation", "warn", "automatic PR or merge is disabled", "Set pr.mode and merge.mode to auto for autonomous YOLO runs.");
88
+ return check("pr-merge", "PR/merge automation", "pass", `pr=${pr?.mode ?? "auto"}, merge=${merge?.mode ?? "auto"}, method=${merge?.method ?? "repo-default"}`);
89
+ }
90
+ async function defaultPiChecks() {
91
+ const pi = Bun.which("pi");
92
+ return [{ ok: Boolean(pi), label: "pi", detail: pi ?? undefined, hint: pi ? undefined : "Install Pi/OMP and the Rig extension." }];
93
+ }
94
+ async function defaultRequestJson(projectRoot, pathname, _init) {
95
+ if (pathname === "/api/github/auth/status")
96
+ return { ok: true, ...resolveGitHubAuthStatus({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) }) };
97
+ if (pathname === "/api/github/repo/permissions")
98
+ return checkGitHubRepoPermissions({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
99
+ return { ok: false, error: `No in-process handler for ${pathname}` };
100
+ }
101
+ async function runDoctorChecks(options) {
102
+ const projectRoot = options.projectRoot;
103
+ const checks = [];
104
+ const which = options.which ?? ((binary) => Bun.which(binary));
105
+ const bunVersion = options.bunVersion ?? Bun.version;
106
+ const request = options.requestJson ?? ((pathname, init) => defaultRequestJson(projectRoot, pathname, init));
107
+ const progress = options.onProgress ?? (() => {});
108
+ const load = options.loadConfig ?? ((root) => loadConfig(root).catch(() => null));
109
+ progress("Checking local toolchain\u2026");
110
+ checks.push(check("bun", "bun", isSupportedBunVersion(bunVersion) ? "pass" : "fail", `found ${bunVersion}`, `Install Bun ${MIN_SUPPORTED_BUN_VERSION} or newer.`));
111
+ checks.push(check("git", "git", which("git") ? "pass" : "fail", which("git") ?? undefined, "Install git and ensure it is on PATH."));
112
+ checks.push(check("jq", "jq", which("jq") ? "pass" : "warn", which("jq") ?? undefined, "Install jq (for example `brew install jq`)."));
113
+ progress("Loading rig.config\u2026");
114
+ const loadedConfig = await load(projectRoot);
115
+ const config = loadedConfig ?? loadFallbackConfig(projectRoot);
116
+ const hasConfigFile = ["rig.config.ts", "rig.config.mts", "rig.config.json"].some((name) => existsSync(resolve(projectRoot, name)));
117
+ 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 or fix rig.config."));
118
+ const taskSourceKind = config?.taskSource?.kind;
119
+ 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."));
120
+ const repo = readSelection(projectRoot);
121
+ const configuredRepo = repoSlugFromConfig(config);
122
+ checks.push(repo ? check("project-link", "repo selected for local setup", repo.project ? "pass" : "warn", repo.selected + (repo.project ? ` -> ${repo.project}` : ""), repo.project ? undefined : `Run rig init --repo ${configuredRepo ?? "owner/repo"} to repair the local project link.`) : check("project-link", "repo selected for local setup", "warn", "missing .rig/state/connection.json", "Current UX can run without this compatibility file; run setup from the cockpit if GitHub Issues integration is needed."));
123
+ progress("Checking GitHub auth\u2026");
124
+ try {
125
+ const auth = await request("/api/github/auth/status");
126
+ checks.push(isAuthenticated(auth) ? check("github-auth", "GitHub auth", "pass") : check("github-auth", "GitHub auth", "fail", "not authenticated", "Run Rig GitHub auth setup or import a token."));
127
+ } catch (error) {
128
+ checks.push(check("github-auth", "GitHub auth", "fail", errorMessage(error), "Authenticate GitHub access."));
129
+ }
130
+ progress("Checking GitHub repo permissions\u2026");
131
+ try {
132
+ const permissions = await request("/api/github/repo/permissions");
133
+ const allowed = permissionAllowsPr(permissions);
134
+ 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."));
135
+ } catch (error) {
136
+ checks.push(check("github-repo-permissions", "GitHub repo PR permissions", "warn", errorMessage(error), "Ensure the token can open PRs."));
137
+ }
138
+ if (taskSourceKind === "github-issues")
139
+ checks.push(check("gh", "gh CLI fallback", which("gh") ? "pass" : "warn", which("gh") ?? undefined, "Install gh locally for fallback operations."));
140
+ if (config)
141
+ checks.push(githubProjectsCheck(config), prMergeCheck(config));
142
+ progress("Checking Pi installation\u2026");
143
+ const piChecks = await (options.piChecks ?? defaultPiChecks)().catch((error) => [{ ok: false, label: "pi/pi-rig checks", detail: undefined, hint: errorMessage(error) }]);
144
+ for (const pi of piChecks)
145
+ 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.")));
146
+ const identity = options.identityContext ? resolveRigIdentity(options.identityContext) : null;
147
+ if (options.identityContext) {
148
+ const filter = resolveRigIdentityFilter(options.identityContext);
149
+ checks.push({ label: "identity", status: identity ? "pass" : "warn", level: identity ? "ok" : "warn", detail: identity?.source ?? "GitHub identity unavailable" });
150
+ checks.push({ label: "github-repo", status: filter?.selectedRepo ? "pass" : "warn", level: filter?.selectedRepo ? "ok" : "warn", detail: filter?.selectedRepo ?? "repo not selected" });
151
+ }
152
+ try {
153
+ const runs = await (options.listRuns ?? listRuns)(projectRoot);
154
+ checks.push({ label: "run-discovery", status: "pass", level: "ok", detail: `${runs.length} run session(s)` });
155
+ } catch (error) {
156
+ checks.push({ label: "run-discovery", status: "fail", level: "fail", detail: errorMessage(error) });
157
+ }
158
+ try {
159
+ const targets = (options.listPlacements ?? listPlacements)(projectRoot);
160
+ checks.push({ label: "relay-targets", status: targets.length > 0 ? "pass" : "warn", level: targets.length > 0 ? "ok" : "warn", detail: `${targets.length} target(s)` });
161
+ } catch (error) {
162
+ checks.push({ label: "relay-targets", status: "warn", level: "warn", detail: errorMessage(error) });
163
+ }
164
+ return checks;
165
+ }
166
+ var doctorRunner = { runDoctorChecks };
167
+ export {
168
+ runDoctorChecks,
169
+ doctorRunner
170
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@h-rig/doctor-plugin",
3
+ "version": "0.0.6-alpha.156",
4
+ "type": "module",
5
+ "description": "First-party diagnostic doctor capability plugin for Rig.",
6
+ "license": "UNLICENSED",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/src/plugin.d.ts",
14
+ "import": "./dist/src/plugin.js"
15
+ },
16
+ "./plugin": {
17
+ "types": "./dist/src/plugin.d.ts",
18
+ "import": "./dist/src/plugin.js"
19
+ },
20
+ "./index": {
21
+ "types": "./dist/src/index.d.ts",
22
+ "import": "./dist/src/index.js"
23
+ }
24
+ },
25
+ "engines": {
26
+ "bun": ">=1.3.11"
27
+ },
28
+ "main": "./dist/src/index.js",
29
+ "module": "./dist/src/index.js",
30
+ "types": "./dist/src/index.d.ts",
31
+ "dependencies": {
32
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.156",
33
+ "@rig/run-worker": "npm:@h-rig/run-worker@0.0.6-alpha.156",
34
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.156",
35
+ "@rig/github-provider-plugin": "npm:@h-rig/github-provider-plugin@0.0.6-alpha.156",
36
+ "@rig/runtime": "npm:@h-rig/runtime@0.0.6-alpha.156"
37
+ }
38
+ }