@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 ADDED
@@ -0,0 +1 @@
1
+ # @h-rig/init-lib
@@ -0,0 +1,3 @@
1
+ import type { RigConfigDependencyPreflightInput, RigConfigDependencyPreflightResult } from "@rig/contracts";
2
+ /** Implements `RigConfigDependencyPreflightService["ensureInstalled"]`. */
3
+ export declare function ensureRigConfigDependenciesInstalled(input: RigConfigDependencyPreflightInput): RigConfigDependencyPreflightResult;
@@ -0,0 +1,229 @@
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
+ export {
228
+ ensureRigConfigDependenciesInstalled
229
+ };
@@ -0,0 +1,44 @@
1
+ import type { ConfigDefaultsContext, RigPlugin } from "@rig/core/config";
2
+ /** Absolute path to a project's declarative config. */
3
+ export declare function rigfigConfigPath(projectRoot: string): string;
4
+ /**
5
+ * Compose the declarative config from a plugin collection's contributed
6
+ * defaults, in plugin order (later plugins win on conflict).
7
+ */
8
+ export declare function composeDeclarativeConfig(plugins: readonly RigPlugin[], context: ConfigDefaultsContext): Record<string, unknown>;
9
+ /** THE single config writer: serialize a composed config object to `.rig/rigfig.toml`. */
10
+ export declare function writeRigfigConfig(projectRoot: string, config: Record<string, unknown>): string;
11
+ /**
12
+ * Compose the declarative config from the embedded standard plugins (threading
13
+ * an explicit `repoSlug` when the caller has one) and write `.rig/rigfig.toml`.
14
+ *
15
+ * This is the SINGLE entry the init wizard / setup flows use to materialize a
16
+ * config: it asks the embedded plugins for their fragments (the seed/composer
17
+ * holds no per-domain knowledge), applies the caller's wizard-choice `overlay`
18
+ * (deep-merged; later wins), and serializes via the one writer
19
+ * (`writeRigfigConfig`). When the overlay supplies a `taskSource`, it REPLACES
20
+ * the composed one wholesale (a kind switch — e.g. github-issues → files — must
21
+ * not leave stale owner/repo keys behind).
22
+ */
23
+ export declare function composeAndWriteRigfig(projectRoot: string, options?: {
24
+ readonly repoSlug?: string;
25
+ readonly overlay?: Record<string, unknown>;
26
+ }): string;
27
+ export interface EnsureDeclarativeConfigResult {
28
+ /** True when this call wrote a new `.rig/rigfig.toml`. */
29
+ readonly created: boolean;
30
+ /** The rigfig path when one now exists (created or pre-existing), else null. */
31
+ readonly path: string | null;
32
+ /** Why nothing was created (code config present, no plugin produced a config, …). */
33
+ readonly reason?: string;
34
+ }
35
+ /**
36
+ * Ensure a project has a config, autocreating `.rig/rigfig.toml` from the
37
+ * embedded plugins' composed defaults when it has none. A pre-existing
38
+ * rig.config.ts (the power path) or rigfig.toml short-circuits. `repoSlug`, when
39
+ * supplied (init wizard / setup), is threaded to plugins that need the repo
40
+ * identity instead of having them derive it.
41
+ */
42
+ export declare function ensureDeclarativeConfig(projectRoot: string, options?: {
43
+ readonly repoSlug?: string;
44
+ }): EnsureDeclarativeConfigResult;
@@ -0,0 +1,88 @@
1
+ // @bun
2
+ // packages/init-lib/src/ensure-config.ts
3
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
4
+ import { resolve } from "path";
5
+ import { stringify as stringifyToml } from "smol-toml";
6
+ import { getStandardPluginsResolver } from "@rig/core/embedded-plugins";
7
+ function rigfigConfigPath(projectRoot) {
8
+ return resolve(projectRoot, ".rig", "rigfig.toml");
9
+ }
10
+ function isPlainObject(value) {
11
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ function deepMerge(base, overlay) {
14
+ const out = { ...base };
15
+ for (const [key, value] of Object.entries(overlay)) {
16
+ const prev = out[key];
17
+ out[key] = isPlainObject(prev) && isPlainObject(value) ? deepMerge(prev, value) : value;
18
+ }
19
+ return out;
20
+ }
21
+ function composeDeclarativeConfig(plugins, context) {
22
+ let merged = {};
23
+ for (const plugin of plugins) {
24
+ const fragment = plugin.contributes?.config?.defaults?.(context);
25
+ if (isPlainObject(fragment))
26
+ merged = deepMerge(merged, fragment);
27
+ }
28
+ return merged;
29
+ }
30
+ var HEADER = [
31
+ "# Declarative rig configuration \u2014 the happy path.",
32
+ "# Plain DATA, composed from the standard plugins' defaults. No top-level",
33
+ "# rig.config.ts and no @h-rig/* install: the global rig binary resolves",
34
+ "# plugins from its embedded standard collection. rig created this for you;",
35
+ "# edit it freely (each plugin owns the section it needs).",
36
+ "",
37
+ ""
38
+ ].join(`
39
+ `);
40
+ function writeRigfigConfig(projectRoot, config) {
41
+ mkdirSync(resolve(projectRoot, ".rig"), { recursive: true });
42
+ const path = rigfigConfigPath(projectRoot);
43
+ writeFileSync(path, `${HEADER}${stringifyToml(config)}
44
+ `, "utf-8");
45
+ return path;
46
+ }
47
+ function composeAndWriteRigfig(projectRoot, options = {}) {
48
+ const resolver = getStandardPluginsResolver();
49
+ if (!resolver) {
50
+ throw new Error("Cannot write rig config: embedded standard plugins are not registered (seed wiring error).");
51
+ }
52
+ const context = options.repoSlug ? { projectRoot, repoSlug: options.repoSlug } : { projectRoot };
53
+ let composed = composeDeclarativeConfig(resolver(), context);
54
+ if (options.overlay) {
55
+ if (isPlainObject(options.overlay.taskSource) && "taskSource" in composed)
56
+ delete composed.taskSource;
57
+ composed = deepMerge(composed, options.overlay);
58
+ }
59
+ return writeRigfigConfig(projectRoot, composed);
60
+ }
61
+ function ensureDeclarativeConfig(projectRoot, options = {}) {
62
+ const rigfig = rigfigConfigPath(projectRoot);
63
+ if (existsSync(rigfig))
64
+ return { created: false, path: rigfig };
65
+ for (const codeConfig of ["rig.config.ts", "rig.config.mts", "rig.config.json"]) {
66
+ if (existsSync(resolve(projectRoot, codeConfig))) {
67
+ return { created: false, path: null, reason: `${codeConfig} present` };
68
+ }
69
+ }
70
+ const resolver = getStandardPluginsResolver();
71
+ if (!resolver) {
72
+ return { created: false, path: null, reason: "no embedded standard plugins registered (seed wiring error)" };
73
+ }
74
+ const context = options.repoSlug ? { projectRoot, repoSlug: options.repoSlug } : { projectRoot };
75
+ const composed = composeDeclarativeConfig(resolver(), context);
76
+ if (!isPlainObject(composed.taskSource)) {
77
+ return { created: false, path: null, reason: "no plugin produced a task source (no GitHub remote / explicit repo)" };
78
+ }
79
+ const path = writeRigfigConfig(projectRoot, composed);
80
+ return { created: true, path };
81
+ }
82
+ export {
83
+ writeRigfigConfig,
84
+ rigfigConfigPath,
85
+ ensureDeclarativeConfig,
86
+ composeDeclarativeConfig,
87
+ composeAndWriteRigfig
88
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./dependency-preflight";
2
+ export * from "./ensure-config";
3
+ export * from "./setup";