@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.
- package/README.md +1 -0
- package/dist/src/dependency-preflight.d.ts +3 -0
- package/dist/src/dependency-preflight.js +229 -0
- package/dist/src/ensure-config.d.ts +44 -0
- package/dist/src/ensure-config.js +88 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +704 -0
- package/dist/src/setup.d.ts +121 -0
- package/dist/src/setup.js +455 -0
- package/package.json +36 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { type GitHubAuthStatus, type GitHubUserFetcher } from "@rig/github-lib";
|
|
3
|
+
import { type PlacementKind } from "@rig/core/placement";
|
|
4
|
+
export type RigConfigStatus = {
|
|
5
|
+
readonly exists: boolean;
|
|
6
|
+
readonly valid: boolean;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly slug: string | null;
|
|
9
|
+
readonly reason?: string;
|
|
10
|
+
};
|
|
11
|
+
export type RigStateStatus = {
|
|
12
|
+
readonly valid: boolean;
|
|
13
|
+
readonly selected: string | null;
|
|
14
|
+
readonly project: string | null;
|
|
15
|
+
readonly reason?: string;
|
|
16
|
+
};
|
|
17
|
+
export type RigAuthValidation = {
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly source: "stored-token" | "gh" | "missing";
|
|
20
|
+
readonly login?: string | null;
|
|
21
|
+
readonly detail: string;
|
|
22
|
+
readonly status?: GitHubAuthStatus;
|
|
23
|
+
};
|
|
24
|
+
export type RigSetupStatus = {
|
|
25
|
+
readonly configured: boolean;
|
|
26
|
+
readonly projectRoot: string;
|
|
27
|
+
readonly slug: string | null;
|
|
28
|
+
readonly config: RigConfigStatus;
|
|
29
|
+
readonly state: RigStateStatus;
|
|
30
|
+
readonly auth: RigAuthValidation;
|
|
31
|
+
readonly reasons: readonly string[];
|
|
32
|
+
};
|
|
33
|
+
export type RigSetupPlacement = {
|
|
34
|
+
readonly alias: string;
|
|
35
|
+
readonly kind: PlacementKind;
|
|
36
|
+
readonly host?: string;
|
|
37
|
+
readonly port?: number;
|
|
38
|
+
readonly token?: string | null;
|
|
39
|
+
};
|
|
40
|
+
export type RigSetupDeps = {
|
|
41
|
+
readonly spawn?: typeof spawnSync;
|
|
42
|
+
readonly fetch?: typeof fetch;
|
|
43
|
+
readonly fetchUser?: GitHubUserFetcher;
|
|
44
|
+
};
|
|
45
|
+
export type RunSetupInput = {
|
|
46
|
+
readonly projectRoot: string;
|
|
47
|
+
readonly slug: string;
|
|
48
|
+
readonly placement: RigSetupPlacement;
|
|
49
|
+
readonly rewriteConfig?: boolean | undefined;
|
|
50
|
+
readonly ensurePi?: boolean | undefined;
|
|
51
|
+
readonly githubToken?: string | null | undefined;
|
|
52
|
+
readonly importGhToken?: boolean | undefined;
|
|
53
|
+
readonly ensureLabels?: boolean | undefined;
|
|
54
|
+
readonly deps?: RigSetupDeps | undefined;
|
|
55
|
+
};
|
|
56
|
+
export declare function parseRepoSlugFromRemote(remoteUrl: string): string | null;
|
|
57
|
+
export declare function parseRepoSlug(value: string): {
|
|
58
|
+
owner: string;
|
|
59
|
+
repo: string;
|
|
60
|
+
slug: string;
|
|
61
|
+
};
|
|
62
|
+
export declare function detectOriginRepoSlug(projectRoot: string, deps?: RigSetupDeps): string | null;
|
|
63
|
+
export declare function ensureRigPrivateDirs(projectRoot: string): void;
|
|
64
|
+
export declare function ensureGitignoreEntries(projectRoot: string): void;
|
|
65
|
+
export declare function writeRigConnectionState(projectRoot: string, slug: string, placement: RigSetupPlacement): void;
|
|
66
|
+
export declare function writeRigConfig(projectRoot: string, slug: string): void;
|
|
67
|
+
export declare function readRigConfigStatus(projectRoot: string): RigConfigStatus;
|
|
68
|
+
export declare function readRigConnectionStatus(projectRoot: string): RigStateStatus;
|
|
69
|
+
export declare function detectGhAuth(projectRoot: string, slug: string, deps?: RigSetupDeps): RigAuthValidation | null;
|
|
70
|
+
export declare function validateGitHubAuth(projectRoot: string, slug: string | null, deps?: RigSetupDeps): Promise<RigAuthValidation>;
|
|
71
|
+
export declare function saveGitHubTokenLocally(projectRoot: string, token: string, slug: string, deps?: RigSetupDeps): Promise<void>;
|
|
72
|
+
export declare function readGhAuthToken(projectRoot: string, deps?: RigSetupDeps): string;
|
|
73
|
+
export declare function ensureGitHubAuth(input: {
|
|
74
|
+
projectRoot: string;
|
|
75
|
+
slug: string;
|
|
76
|
+
token?: string | null | undefined;
|
|
77
|
+
importGhToken?: boolean | undefined;
|
|
78
|
+
deps?: RigSetupDeps | undefined;
|
|
79
|
+
}): Promise<RigAuthValidation>;
|
|
80
|
+
export declare function ensureGitHubLabels(input: {
|
|
81
|
+
projectRoot: string;
|
|
82
|
+
slug: string;
|
|
83
|
+
token?: string | null;
|
|
84
|
+
deps?: RigSetupDeps | undefined;
|
|
85
|
+
}): Promise<{
|
|
86
|
+
ok: true;
|
|
87
|
+
method: "api" | "gh";
|
|
88
|
+
labels: readonly string[];
|
|
89
|
+
}>;
|
|
90
|
+
export declare function ensurePiRigInstalledForSetup(projectRoot: string, deps?: RigSetupDeps): {
|
|
91
|
+
ok: true;
|
|
92
|
+
detail: string;
|
|
93
|
+
};
|
|
94
|
+
export declare function detectRigStartupStatus(input: {
|
|
95
|
+
projectRoot: string;
|
|
96
|
+
deps?: RigSetupDeps | undefined;
|
|
97
|
+
}): Promise<RigSetupStatus>;
|
|
98
|
+
export declare const detectStartupStatus: typeof detectRigStartupStatus;
|
|
99
|
+
export declare function applyRigSetupProject(input: {
|
|
100
|
+
projectRoot: string;
|
|
101
|
+
slug: string;
|
|
102
|
+
placement: RigSetupPlacement;
|
|
103
|
+
rewriteConfig: boolean;
|
|
104
|
+
ensurePi?: boolean | undefined;
|
|
105
|
+
ensureLabels?: boolean | undefined;
|
|
106
|
+
deps?: RigSetupDeps | undefined;
|
|
107
|
+
}): Promise<{
|
|
108
|
+
repoSlug: string;
|
|
109
|
+
placement: string;
|
|
110
|
+
configWritten: boolean;
|
|
111
|
+
labels: unknown;
|
|
112
|
+
pi: unknown;
|
|
113
|
+
}>;
|
|
114
|
+
export declare function runSetup(input: RunSetupInput): Promise<{
|
|
115
|
+
repoSlug: string;
|
|
116
|
+
placement: string;
|
|
117
|
+
configWritten: boolean;
|
|
118
|
+
labels: unknown;
|
|
119
|
+
pi: unknown;
|
|
120
|
+
status: RigSetupStatus;
|
|
121
|
+
}>;
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/init-lib/src/setup.ts
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
5
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
6
|
+
import {
|
|
7
|
+
createGitHubAuthStore,
|
|
8
|
+
probeGitHubRepository,
|
|
9
|
+
resolveGitHubAuthStatus,
|
|
10
|
+
saveGitHubTokenForProject
|
|
11
|
+
} from "@rig/github-lib";
|
|
12
|
+
import { addPlacement, selectPlacement } from "@rig/core/placement";
|
|
13
|
+
|
|
14
|
+
// packages/init-lib/src/ensure-config.ts
|
|
15
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
16
|
+
import { resolve } from "path";
|
|
17
|
+
import { stringify as stringifyToml } from "smol-toml";
|
|
18
|
+
import { getStandardPluginsResolver } from "@rig/core/embedded-plugins";
|
|
19
|
+
function rigfigConfigPath(projectRoot) {
|
|
20
|
+
return resolve(projectRoot, ".rig", "rigfig.toml");
|
|
21
|
+
}
|
|
22
|
+
function isPlainObject(value) {
|
|
23
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function deepMerge(base, overlay) {
|
|
26
|
+
const out = { ...base };
|
|
27
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
28
|
+
const prev = out[key];
|
|
29
|
+
out[key] = isPlainObject(prev) && isPlainObject(value) ? deepMerge(prev, value) : value;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function composeDeclarativeConfig(plugins, context) {
|
|
34
|
+
let merged = {};
|
|
35
|
+
for (const plugin of plugins) {
|
|
36
|
+
const fragment = plugin.contributes?.config?.defaults?.(context);
|
|
37
|
+
if (isPlainObject(fragment))
|
|
38
|
+
merged = deepMerge(merged, fragment);
|
|
39
|
+
}
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
var HEADER = [
|
|
43
|
+
"# Declarative rig configuration \u2014 the happy path.",
|
|
44
|
+
"# Plain DATA, composed from the standard plugins' defaults. No top-level",
|
|
45
|
+
"# rig.config.ts and no @h-rig/* install: the global rig binary resolves",
|
|
46
|
+
"# plugins from its embedded standard collection. rig created this for you;",
|
|
47
|
+
"# edit it freely (each plugin owns the section it needs).",
|
|
48
|
+
"",
|
|
49
|
+
""
|
|
50
|
+
].join(`
|
|
51
|
+
`);
|
|
52
|
+
function writeRigfigConfig(projectRoot, config) {
|
|
53
|
+
mkdirSync(resolve(projectRoot, ".rig"), { recursive: true });
|
|
54
|
+
const path = rigfigConfigPath(projectRoot);
|
|
55
|
+
writeFileSync(path, `${HEADER}${stringifyToml(config)}
|
|
56
|
+
`, "utf-8");
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
59
|
+
function composeAndWriteRigfig(projectRoot, options = {}) {
|
|
60
|
+
const resolver = getStandardPluginsResolver();
|
|
61
|
+
if (!resolver) {
|
|
62
|
+
throw new Error("Cannot write rig config: embedded standard plugins are not registered (seed wiring error).");
|
|
63
|
+
}
|
|
64
|
+
const context = options.repoSlug ? { projectRoot, repoSlug: options.repoSlug } : { projectRoot };
|
|
65
|
+
let composed = composeDeclarativeConfig(resolver(), context);
|
|
66
|
+
if (options.overlay) {
|
|
67
|
+
if (isPlainObject(options.overlay.taskSource) && "taskSource" in composed)
|
|
68
|
+
delete composed.taskSource;
|
|
69
|
+
composed = deepMerge(composed, options.overlay);
|
|
70
|
+
}
|
|
71
|
+
return writeRigfigConfig(projectRoot, composed);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// packages/init-lib/src/setup.ts
|
|
75
|
+
var RIG_LABELS_TO_ENSURE = [
|
|
76
|
+
"rig:running",
|
|
77
|
+
"rig:pr-open",
|
|
78
|
+
"rig:ci-fixing",
|
|
79
|
+
"rig:merging",
|
|
80
|
+
"rig:done",
|
|
81
|
+
"rig:needs-attention",
|
|
82
|
+
"rig:ready",
|
|
83
|
+
"rig:blocked",
|
|
84
|
+
"rig:generated"
|
|
85
|
+
];
|
|
86
|
+
var RIG_LABEL_METADATA = {
|
|
87
|
+
"rig:running": { color: "1d76db", description: "Rig is actively working on this issue." },
|
|
88
|
+
"rig:pr-open": { color: "5319e7", description: "Rig opened a pull request for this issue." },
|
|
89
|
+
"rig:ci-fixing": { color: "fbca04", description: "Rig is fixing CI or review feedback for this issue." },
|
|
90
|
+
"rig:merging": { color: "0052cc", description: "Rig is merging the completed change for this issue." },
|
|
91
|
+
"rig:done": { color: "0e8a16", description: "Rig completed this issue." },
|
|
92
|
+
"rig:needs-attention": { color: "d93f0b", description: "Rig needs operator attention for this issue." },
|
|
93
|
+
"rig:ready": { color: "0e8a16", description: "Rig issue analysis marked this issue ready." },
|
|
94
|
+
"rig:blocked": { color: "d93f0b", description: "Rig issue analysis found blockers for this issue." },
|
|
95
|
+
"rig:generated": { color: "c5def5", description: "Rig generated this follow-up issue." }
|
|
96
|
+
};
|
|
97
|
+
function cleanString(value) {
|
|
98
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
99
|
+
}
|
|
100
|
+
function parseRepoSlugFromRemote(remoteUrl) {
|
|
101
|
+
const trimmed = remoteUrl.trim();
|
|
102
|
+
const match = trimmed.match(/github\.com[:/]([^/\s]+)\/([^/\s.]+)(?:\.git)?$/i);
|
|
103
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
104
|
+
}
|
|
105
|
+
function parseRepoSlug(value) {
|
|
106
|
+
const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
107
|
+
if (!match)
|
|
108
|
+
throw new Error(`Invalid GitHub repo slug "${value}". Expected owner/repo.`);
|
|
109
|
+
return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
|
|
110
|
+
}
|
|
111
|
+
function runSyncCommand(command, input = {}) {
|
|
112
|
+
const executable = command[0];
|
|
113
|
+
if (!executable)
|
|
114
|
+
throw new Error("command is required");
|
|
115
|
+
return (input.spawn ?? spawnSync)(executable, [...command.slice(1)], {
|
|
116
|
+
cwd: input.cwd,
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
timeout: input.timeoutMs ?? 1e4,
|
|
119
|
+
env: input.env ?? process.env
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function detectOriginRepoSlug(projectRoot, deps = {}) {
|
|
123
|
+
const result = runSyncCommand(["git", "-C", projectRoot, "remote", "get-url", "origin"], { timeoutMs: 5000, spawn: deps.spawn });
|
|
124
|
+
if (result.status !== 0 || result.error)
|
|
125
|
+
return null;
|
|
126
|
+
return parseRepoSlugFromRemote(result.stdout.trim());
|
|
127
|
+
}
|
|
128
|
+
function connectionStatePath(projectRoot) {
|
|
129
|
+
return resolve2(projectRoot, ".rig", "state", "connection.json");
|
|
130
|
+
}
|
|
131
|
+
function projectLinkStatePath(projectRoot) {
|
|
132
|
+
return resolve2(projectRoot, ".rig", "state", "project-link.json");
|
|
133
|
+
}
|
|
134
|
+
function readJsonRecord(path) {
|
|
135
|
+
if (!existsSync2(path))
|
|
136
|
+
return null;
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
139
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function writeJsonFile(path, value) {
|
|
145
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
146
|
+
writeFileSync2(path, `${JSON.stringify(value, null, 2)}
|
|
147
|
+
`, "utf-8");
|
|
148
|
+
}
|
|
149
|
+
function ensureRigPrivateDirs(projectRoot) {
|
|
150
|
+
mkdirSync2(resolve2(projectRoot, ".rig", "state"), { recursive: true });
|
|
151
|
+
mkdirSync2(resolve2(projectRoot, ".rig", "logs"), { recursive: true });
|
|
152
|
+
mkdirSync2(resolve2(projectRoot, ".rig", "runs"), { recursive: true });
|
|
153
|
+
mkdirSync2(resolve2(projectRoot, ".rig", "tmp"), { recursive: true });
|
|
154
|
+
mkdirSync2(resolve2(projectRoot, "artifacts"), { recursive: true });
|
|
155
|
+
const taskConfigPath = resolve2(projectRoot, ".rig", "task-config.json");
|
|
156
|
+
if (!existsSync2(taskConfigPath))
|
|
157
|
+
writeFileSync2(taskConfigPath, `{}
|
|
158
|
+
`, "utf-8");
|
|
159
|
+
}
|
|
160
|
+
function ensureGitignoreEntries(projectRoot) {
|
|
161
|
+
const path = resolve2(projectRoot, ".gitignore");
|
|
162
|
+
const existing = existsSync2(path) ? readFileSync(path, "utf-8") : "";
|
|
163
|
+
const lines = new Set(existing.split(/\r?\n/));
|
|
164
|
+
const missing = [".rig/state/", ".rig/logs/", ".rig/runs/", ".rig/tmp/"].filter((entry) => !lines.has(entry));
|
|
165
|
+
if (missing.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
const prefix = existing.length > 0 && !existing.endsWith(`
|
|
168
|
+
`) ? `
|
|
169
|
+
` : "";
|
|
170
|
+
appendFileSync(path, `${prefix}${missing.join(`
|
|
171
|
+
`)}
|
|
172
|
+
`, "utf-8");
|
|
173
|
+
}
|
|
174
|
+
function writeRigConnectionState(projectRoot, slug, placement) {
|
|
175
|
+
const previous = readJsonRecord(connectionStatePath(projectRoot)) ?? {};
|
|
176
|
+
writeJsonFile(connectionStatePath(projectRoot), {
|
|
177
|
+
...previous,
|
|
178
|
+
selected: placement.alias,
|
|
179
|
+
project: slug,
|
|
180
|
+
linkedAt: new Date().toISOString()
|
|
181
|
+
});
|
|
182
|
+
writeJsonFile(projectLinkStatePath(projectRoot), {
|
|
183
|
+
repoSlug: slug,
|
|
184
|
+
connection: placement.alias,
|
|
185
|
+
linkedAt: new Date().toISOString()
|
|
186
|
+
});
|
|
187
|
+
if (placement.alias === "local")
|
|
188
|
+
delete process.env.RIG_REMOTE_ALIAS;
|
|
189
|
+
else
|
|
190
|
+
process.env.RIG_REMOTE_ALIAS = placement.alias;
|
|
191
|
+
}
|
|
192
|
+
function writeRigConfig(projectRoot, slug) {
|
|
193
|
+
parseRepoSlug(slug);
|
|
194
|
+
composeAndWriteRigfig(projectRoot, { repoSlug: slug });
|
|
195
|
+
}
|
|
196
|
+
function readRigConfigStatus(projectRoot) {
|
|
197
|
+
const path = rigfigConfigPath(projectRoot);
|
|
198
|
+
if (!existsSync2(path))
|
|
199
|
+
return { exists: false, valid: false, path, slug: null, reason: "missing .rig/rigfig.toml" };
|
|
200
|
+
try {
|
|
201
|
+
const source = readFileSync(path, "utf-8");
|
|
202
|
+
const owner = source.match(/^\s*owner\s*=\s*["']([^"']+)["']/m)?.[1] ?? null;
|
|
203
|
+
const repoValues = [...source.matchAll(/^\s*repo\s*=\s*["']([^"']+)["']/gm)].map((match) => match[1]).filter(Boolean);
|
|
204
|
+
const taskRepo = repoValues.find((value) => !value.includes("/")) ?? null;
|
|
205
|
+
const projectRepo = repoValues.find((value) => value.includes("/")) ?? null;
|
|
206
|
+
const githubIssues = /^\s*kind\s*=\s*["']github-issues["']/m.test(source);
|
|
207
|
+
const slug = owner && taskRepo ? `${owner}/${taskRepo}` : projectRepo;
|
|
208
|
+
if (!githubIssues || !slug)
|
|
209
|
+
return { exists: true, valid: false, path, slug: slug ?? null, reason: ".rig/rigfig.toml is not a GitHub Issues Rig config" };
|
|
210
|
+
parseRepoSlug(slug);
|
|
211
|
+
return { exists: true, valid: true, path, slug };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return { exists: true, valid: false, path, slug: null, reason: error instanceof Error ? error.message : String(error) };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function readRigConnectionStatus(projectRoot) {
|
|
217
|
+
const stateDir = resolve2(projectRoot, ".rig", "state");
|
|
218
|
+
if (!existsSync2(stateDir))
|
|
219
|
+
return { valid: false, selected: null, project: null, reason: "missing .rig/state" };
|
|
220
|
+
const connection = readJsonRecord(connectionStatePath(projectRoot));
|
|
221
|
+
if (!connection)
|
|
222
|
+
return { valid: false, selected: null, project: null, reason: "missing or invalid .rig/state/connection.json" };
|
|
223
|
+
const selected = cleanString(connection.selected);
|
|
224
|
+
const project = cleanString(connection.project);
|
|
225
|
+
if (!selected)
|
|
226
|
+
return { valid: false, selected: null, project, reason: "connection.json is missing selected placement" };
|
|
227
|
+
if (!project)
|
|
228
|
+
return { valid: false, selected, project: null, reason: "connection.json is missing project slug" };
|
|
229
|
+
try {
|
|
230
|
+
parseRepoSlug(project);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return { valid: false, selected, project, reason: error instanceof Error ? error.message : String(error) };
|
|
233
|
+
}
|
|
234
|
+
return { valid: true, selected, project };
|
|
235
|
+
}
|
|
236
|
+
function detectGhAuth(projectRoot, slug, deps = {}) {
|
|
237
|
+
const user = runSyncCommand(["gh", "api", "user", "--jq", ".login"], { cwd: projectRoot, timeoutMs: 5000, spawn: deps.spawn });
|
|
238
|
+
if (user.status !== 0 || user.error || !user.stdout.trim())
|
|
239
|
+
return null;
|
|
240
|
+
const repo = runSyncCommand(["gh", "repo", "view", slug, "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { cwd: projectRoot, timeoutMs: 5000, spawn: deps.spawn });
|
|
241
|
+
if (repo.status !== 0 || repo.error)
|
|
242
|
+
return { ok: false, source: "gh", login: user.stdout.trim(), detail: (repo.stderr || repo.stdout || "gh cannot access the selected repository").trim() };
|
|
243
|
+
return { ok: true, source: "gh", login: user.stdout.trim(), detail: "gh CLI authentication can access the selected repository" };
|
|
244
|
+
}
|
|
245
|
+
async function validateGitHubAuth(projectRoot, slug, deps = {}) {
|
|
246
|
+
if (!slug)
|
|
247
|
+
return { ok: false, source: "missing", detail: "GitHub repo slug is unknown" };
|
|
248
|
+
const status = resolveGitHubAuthStatus({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
249
|
+
if (status.signedIn) {
|
|
250
|
+
const store = createGitHubAuthStore(projectRoot);
|
|
251
|
+
if (!status.selectedRepo) {
|
|
252
|
+
store.saveSelectedRepo(slug);
|
|
253
|
+
return { ok: true, source: "stored-token", login: status.login, detail: "stored Rig GitHub token selected for this repo", status: store.status({ oauthConfigured: status.oauthConfigured }) };
|
|
254
|
+
}
|
|
255
|
+
if (status.selectedRepo !== slug)
|
|
256
|
+
return { ok: false, source: "stored-token", login: status.login, detail: `stored GitHub token is scoped to ${status.selectedRepo}, not ${slug}`, status };
|
|
257
|
+
return { ok: true, source: "stored-token", login: status.login, detail: "stored Rig GitHub token is present", status };
|
|
258
|
+
}
|
|
259
|
+
const gh = detectGhAuth(projectRoot, slug, deps);
|
|
260
|
+
if (gh)
|
|
261
|
+
return gh;
|
|
262
|
+
return { ok: false, source: "missing", detail: "Sign in with `gh auth login`, choose Setup \u2192 GitHub auth, or paste a token." };
|
|
263
|
+
}
|
|
264
|
+
async function saveGitHubTokenLocally(projectRoot, token, slug, deps = {}) {
|
|
265
|
+
ensureRigPrivateDirs(projectRoot);
|
|
266
|
+
await saveGitHubTokenForProject({
|
|
267
|
+
projectRoot,
|
|
268
|
+
token,
|
|
269
|
+
tokenSource: "manual-token",
|
|
270
|
+
selectedRepo: slug,
|
|
271
|
+
...deps.fetchUser ? { fetchUser: deps.fetchUser } : {}
|
|
272
|
+
});
|
|
273
|
+
createGitHubAuthStore(projectRoot).copyToLocalProjectRoot(projectRoot);
|
|
274
|
+
}
|
|
275
|
+
function readGhAuthToken(projectRoot, deps = {}) {
|
|
276
|
+
const result = runSyncCommand(["gh", "auth", "token"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
277
|
+
if (result.status !== 0 || result.error || !result.stdout.trim())
|
|
278
|
+
throw new Error((result.stderr || result.stdout || "Could not read GitHub token from `gh auth token`.").trim());
|
|
279
|
+
return result.stdout.trim();
|
|
280
|
+
}
|
|
281
|
+
async function ensureGitHubAuth(input) {
|
|
282
|
+
const current = await validateGitHubAuth(input.projectRoot, input.slug, input.deps);
|
|
283
|
+
if (current.ok && !input.token && !input.importGhToken)
|
|
284
|
+
return current;
|
|
285
|
+
if (input.token?.trim())
|
|
286
|
+
await saveGitHubTokenLocally(input.projectRoot, input.token.trim(), input.slug, input.deps);
|
|
287
|
+
else if (input.importGhToken)
|
|
288
|
+
await saveGitHubTokenLocally(input.projectRoot, readGhAuthToken(input.projectRoot, input.deps), input.slug, input.deps);
|
|
289
|
+
return validateGitHubAuth(input.projectRoot, input.slug, input.deps);
|
|
290
|
+
}
|
|
291
|
+
async function ensureGitHubLabels(input) {
|
|
292
|
+
const repo = parseRepoSlug(input.slug);
|
|
293
|
+
const token = input.token?.trim() || createGitHubAuthStore(input.projectRoot).readToken();
|
|
294
|
+
if (token) {
|
|
295
|
+
const fetchLabels = input.deps?.fetch ?? fetch;
|
|
296
|
+
for (const name of RIG_LABELS_TO_ENSURE) {
|
|
297
|
+
const metadata = RIG_LABEL_METADATA[name];
|
|
298
|
+
const response = await fetchLabels(`https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/labels`, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: {
|
|
301
|
+
accept: "application/vnd.github+json",
|
|
302
|
+
authorization: `Bearer ${token}`,
|
|
303
|
+
"content-type": "application/json",
|
|
304
|
+
"user-agent": "rig"
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify({ name, color: metadata.color, description: metadata.description })
|
|
307
|
+
});
|
|
308
|
+
if (response.ok)
|
|
309
|
+
continue;
|
|
310
|
+
const text = await response.text().catch(() => "");
|
|
311
|
+
if (response.status === 422 && /already_exists|already exists|exists/i.test(text))
|
|
312
|
+
continue;
|
|
313
|
+
throw new Error(`Could not create GitHub label ${name}: ${response.status} ${text || response.statusText}`);
|
|
314
|
+
}
|
|
315
|
+
return { ok: true, method: "api", labels: RIG_LABELS_TO_ENSURE };
|
|
316
|
+
}
|
|
317
|
+
const gh = detectGhAuth(input.projectRoot, input.slug, input.deps);
|
|
318
|
+
if (!gh?.ok)
|
|
319
|
+
throw new Error("GitHub labels require a stored Rig token or gh auth.");
|
|
320
|
+
for (const name of RIG_LABELS_TO_ENSURE) {
|
|
321
|
+
const metadata = RIG_LABEL_METADATA[name];
|
|
322
|
+
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 });
|
|
323
|
+
if (result.status !== 0 || result.error)
|
|
324
|
+
throw new Error(`gh label create ${name} failed: ${(result.stderr || result.stdout || result.error?.message || "unknown error").trim()}`);
|
|
325
|
+
}
|
|
326
|
+
return { ok: true, method: "gh", labels: RIG_LABELS_TO_ENSURE };
|
|
327
|
+
}
|
|
328
|
+
function piListContainsRigExtension(output) {
|
|
329
|
+
return output.split(/\r?\n/).some((line) => line.includes("@h-rig/pi-rig") || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(line));
|
|
330
|
+
}
|
|
331
|
+
function splitInstallCommand(value) {
|
|
332
|
+
return value.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^["']|["']$/g, "")) ?? [];
|
|
333
|
+
}
|
|
334
|
+
function ensurePiRigInstalledForSetup(projectRoot, deps = {}) {
|
|
335
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1")
|
|
336
|
+
return { ok: true, detail: "fake-pi" };
|
|
337
|
+
let version = runSyncCommand(["pi", "--version"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
338
|
+
if (version.status !== 0 || version.error) {
|
|
339
|
+
const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim();
|
|
340
|
+
if (!installCommand)
|
|
341
|
+
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.`);
|
|
342
|
+
const parts = splitInstallCommand(installCommand);
|
|
343
|
+
if (parts.length === 0)
|
|
344
|
+
throw new Error("RIG_PI_INSTALL_COMMAND is empty.");
|
|
345
|
+
const install = runSyncCommand(parts, { cwd: projectRoot, timeoutMs: 120000, spawn: deps.spawn });
|
|
346
|
+
if (install.status !== 0 || install.error)
|
|
347
|
+
throw new Error(`Pi/OMP install command failed: ${(install.stderr || install.stdout || install.error?.message || "unknown error").trim()}`);
|
|
348
|
+
version = runSyncCommand(["pi", "--version"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
349
|
+
if (version.status !== 0 || version.error)
|
|
350
|
+
throw new Error(`Pi/OMP is still unavailable after install: ${(version.stderr || version.stdout || version.error?.message || "pi --version failed").trim()}`);
|
|
351
|
+
}
|
|
352
|
+
let list = runSyncCommand(["pi", "list"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
353
|
+
if (!piListContainsRigExtension(`${list.stdout}
|
|
354
|
+
${list.stderr}`)) {
|
|
355
|
+
const packageSource = existsSync2(resolve2(projectRoot, "packages", "pi-rig", "package.json")) ? resolve2(projectRoot, "packages", "pi-rig") : "npm:@h-rig/pi-rig";
|
|
356
|
+
const install = runSyncCommand(["pi", "install", packageSource], { cwd: projectRoot, timeoutMs: 120000, spawn: deps.spawn });
|
|
357
|
+
if (install.status !== 0 || install.error)
|
|
358
|
+
throw new Error(`Could not install/register the Rig OMP extension: ${(install.stderr || install.stdout || install.error?.message || "pi install failed").trim()}`);
|
|
359
|
+
list = runSyncCommand(["pi", "list"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
360
|
+
if (!piListContainsRigExtension(`${list.stdout}
|
|
361
|
+
${list.stderr}`))
|
|
362
|
+
throw new Error("Pi/OMP is installed, but `pi list` does not show the Rig extension.");
|
|
363
|
+
}
|
|
364
|
+
return { ok: true, detail: (version.stdout || version.stderr).trim() || "pi available; rig extension registered" };
|
|
365
|
+
}
|
|
366
|
+
async function detectRigStartupStatus(input) {
|
|
367
|
+
const projectRoot = input.projectRoot;
|
|
368
|
+
const config = readRigConfigStatus(projectRoot);
|
|
369
|
+
const state = readRigConnectionStatus(projectRoot);
|
|
370
|
+
const detectedSlug = detectOriginRepoSlug(projectRoot, input.deps);
|
|
371
|
+
const slug = config.slug ?? state.project ?? detectedSlug;
|
|
372
|
+
const reasons = [];
|
|
373
|
+
if (!detectedSlug)
|
|
374
|
+
reasons.push("git origin does not point at a GitHub owner/repo remote");
|
|
375
|
+
if (!config.exists || !config.valid)
|
|
376
|
+
reasons.push(config.reason ?? "rig.config.ts is invalid");
|
|
377
|
+
if (!state.valid)
|
|
378
|
+
reasons.push(state.reason ?? ".rig/state/connection.json is invalid");
|
|
379
|
+
if (config.slug && state.project && config.slug !== state.project)
|
|
380
|
+
reasons.push(`rig.config.ts repo ${config.slug} does not match connection project ${state.project}`);
|
|
381
|
+
if (slug && detectedSlug && slug !== detectedSlug)
|
|
382
|
+
reasons.push(`configured repo ${slug} does not match git origin ${detectedSlug}`);
|
|
383
|
+
const auth = await validateGitHubAuth(projectRoot, slug, input.deps);
|
|
384
|
+
if (!auth.ok)
|
|
385
|
+
reasons.push(auth.detail);
|
|
386
|
+
return { configured: reasons.length === 0, projectRoot, slug, config, state, auth, reasons };
|
|
387
|
+
}
|
|
388
|
+
var detectStartupStatus = detectRigStartupStatus;
|
|
389
|
+
async function applyRigSetupProject(input) {
|
|
390
|
+
const repo = parseRepoSlug(input.slug);
|
|
391
|
+
ensureRigPrivateDirs(input.projectRoot);
|
|
392
|
+
ensureGitignoreEntries(input.projectRoot);
|
|
393
|
+
if (input.placement.alias !== "local" && input.placement.host) {
|
|
394
|
+
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 });
|
|
395
|
+
} else {
|
|
396
|
+
selectPlacement(input.projectRoot, input.placement.alias);
|
|
397
|
+
}
|
|
398
|
+
writeRigConnectionState(input.projectRoot, repo.slug, input.placement);
|
|
399
|
+
if (input.rewriteConfig)
|
|
400
|
+
writeRigConfig(input.projectRoot, repo.slug);
|
|
401
|
+
const labels = input.ensureLabels === false ? { skipped: true } : await ensureGitHubLabels({ projectRoot: input.projectRoot, slug: repo.slug, deps: input.deps });
|
|
402
|
+
const pi = input.ensurePi === false ? { skipped: true } : ensurePiRigInstalledForSetup(input.projectRoot, input.deps);
|
|
403
|
+
return { repoSlug: repo.slug, placement: input.placement.alias, configWritten: input.rewriteConfig, labels, pi };
|
|
404
|
+
}
|
|
405
|
+
async function runSetup(input) {
|
|
406
|
+
const repo = parseRepoSlug(input.slug);
|
|
407
|
+
const auth = await ensureGitHubAuth({ projectRoot: input.projectRoot, slug: repo.slug, token: input.githubToken, importGhToken: input.importGhToken, deps: input.deps });
|
|
408
|
+
if (!auth.ok)
|
|
409
|
+
throw new Error(auth.detail);
|
|
410
|
+
const token = createGitHubAuthStore(input.projectRoot).readToken();
|
|
411
|
+
const probe = await probeGitHubRepository({
|
|
412
|
+
owner: repo.owner,
|
|
413
|
+
repo: repo.repo,
|
|
414
|
+
token,
|
|
415
|
+
scopes: auth.status?.scopes ?? [],
|
|
416
|
+
...input.deps?.fetch ? { fetchRepository: input.deps.fetch } : {}
|
|
417
|
+
});
|
|
418
|
+
if (!probe.ok)
|
|
419
|
+
throw new Error(probe.message);
|
|
420
|
+
const result = await applyRigSetupProject({
|
|
421
|
+
projectRoot: input.projectRoot,
|
|
422
|
+
slug: repo.slug,
|
|
423
|
+
placement: input.placement,
|
|
424
|
+
rewriteConfig: input.rewriteConfig ?? true,
|
|
425
|
+
ensurePi: input.ensurePi,
|
|
426
|
+
ensureLabels: input.ensureLabels,
|
|
427
|
+
deps: input.deps
|
|
428
|
+
});
|
|
429
|
+
const status = await detectRigStartupStatus({ projectRoot: input.projectRoot, deps: input.deps });
|
|
430
|
+
if (!status.configured)
|
|
431
|
+
throw new Error(`Setup wrote state but doctor still reports: ${status.reasons.join("; ")}`);
|
|
432
|
+
return { ...result, status };
|
|
433
|
+
}
|
|
434
|
+
export {
|
|
435
|
+
writeRigConnectionState,
|
|
436
|
+
writeRigConfig,
|
|
437
|
+
validateGitHubAuth,
|
|
438
|
+
saveGitHubTokenLocally,
|
|
439
|
+
runSetup,
|
|
440
|
+
readRigConnectionStatus,
|
|
441
|
+
readRigConfigStatus,
|
|
442
|
+
readGhAuthToken,
|
|
443
|
+
parseRepoSlugFromRemote,
|
|
444
|
+
parseRepoSlug,
|
|
445
|
+
ensureRigPrivateDirs,
|
|
446
|
+
ensurePiRigInstalledForSetup,
|
|
447
|
+
ensureGitignoreEntries,
|
|
448
|
+
ensureGitHubLabels,
|
|
449
|
+
ensureGitHubAuth,
|
|
450
|
+
detectStartupStatus,
|
|
451
|
+
detectRigStartupStatus,
|
|
452
|
+
detectOriginRepoSlug,
|
|
453
|
+
detectGhAuth,
|
|
454
|
+
applyRigSetupProject
|
|
455
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@h-rig/init-lib",
|
|
3
|
+
"version": "0.0.6-alpha.158",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Rig init/onboarding library (config-source codegen, setup runner, GitHub-auth + startup detection) — non-plugin domain library shared by the cli surface, the rig extension, and the init plugin.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/src/index.d.ts",
|
|
14
|
+
"import": "./dist/src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./index": {
|
|
17
|
+
"types": "./dist/src/index.d.ts",
|
|
18
|
+
"import": "./dist/src/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./ensure-config": {
|
|
21
|
+
"types": "./dist/src/ensure-config.d.ts",
|
|
22
|
+
"import": "./dist/src/ensure-config.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.158",
|
|
33
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.158",
|
|
34
|
+
"@rig/github-lib": "npm:@h-rig/github-lib@0.0.6-alpha.158"
|
|
35
|
+
}
|
|
36
|
+
}
|