@rightkit/release 0.2.106 → 0.2.108

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.
@@ -26,6 +26,12 @@ const commands = new Map([
26
26
 
27
27
  const args = process.argv.slice(2);
28
28
  const first = args[0];
29
+ if (first === "dev") {
30
+ import("../development.mjs").then(({ main }) => main(args.slice(1))).catch((error) => {
31
+ console.error(`right-release dev: ${error.message}`);
32
+ process.exitCode = 1;
33
+ });
34
+ } else {
29
35
  const githubBuildCommands = new Set(["release", "build", "package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target"]);
30
36
 
31
37
  try {
@@ -143,6 +149,8 @@ function printHelp() {
143
149
  Commands:
144
150
  build [--platform mac|win] [--skip-checks] [--resume-from <phase>]
145
151
  Resume macOS at prepackage, sign, package, post-sign, or harden
152
+ dev --platform win|mac [--config <path>] [--dry-run] [--build-only]
153
+ Incremental unsigned build & optional local install
146
154
  upload --release <id> --platform mac|win --tier patch|update
147
155
  Upload one sealed release; never builds
148
156
  release Compatibility alias for build
@@ -169,5 +177,6 @@ Commands:
169
177
  Attach sealed signed artifacts to a verified GitHub Release
170
178
 
171
179
  Direct flags are treated as: right-release build <flags>.
172
- Build is tier-neutral. Upload requires an explicit tier. Unsigned/local smoke builds stay app-local.`);
180
+ Build is tier-neutral. Upload requires an explicit tier. Dev builds are incremental and unsigned.`);
181
+ }
173
182
  }
@@ -0,0 +1,230 @@
1
+ import { spawnSync } from "./background-process.mjs";
2
+ import { createHash } from "node:crypto";
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
7
+ import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
8
+ import { managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
9
+ import { installPortableDevelopment } from "./portable-development.mjs";
10
+
11
+ export const DEVELOPMENT_PROFILE = "release-iterate";
12
+
13
+ export function developmentEnvironment(env = process.env) {
14
+ const result = {
15
+ ...env,
16
+ RIGHTKIT_BUILD_MODE: "dev",
17
+ RIGHTKIT_CARGO_PROFILE: DEVELOPMENT_PROFILE,
18
+ TAURI_SIGNING_PRIVATE_KEY: "",
19
+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "",
20
+ APPLE_SIGNING_IDENTITY: "-",
21
+ APPLE_ID: "",
22
+ APPLE_PASSWORD: "",
23
+ APPLE_TEAM_ID: "",
24
+ CSC_IDENTITY_AUTO_DISCOVERY: "false",
25
+ };
26
+ delete result.CARGO_INCREMENTAL;
27
+ const client = managedRuntimeClient(result);
28
+ if (client) {
29
+ const managedRoot = result.RIGHTKIT_MANAGED_ROOT ?? result.RIGHTKIT_EXTERNAL_ROOT
30
+ ?? (process.platform === "win32" ? "D:\\.rightkit-managed" : process.platform === "darwin" ? "/Volumes/D/.rightkit-managed" : undefined);
31
+ const agentBin = managedRoot && path.join(managedRoot, "agent-bin");
32
+ if (agentBin && existsSync(agentBin)) {
33
+ const pathKey = Object.keys(result).find((key) => key.toLowerCase() === "path") ?? "PATH";
34
+ result[pathKey] = [agentBin, result[pathKey]].filter(Boolean).join(path.delimiter);
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+
40
+ export function validateDevelopmentConfig(config, platform) {
41
+ const target = config?.development?.targets?.[platform];
42
+ if (!target || !Array.isArray(target.build) || target.build.length === 0) throw new Error(`development.targets.${platform}.build must be a non-empty command array`);
43
+ for (const [index, command] of target.build.entries()) validateCommand(command, `development.targets.${platform}.build[${index}]`);
44
+ if (target.install?.kind === "command") validateCommand(target.install, `development.targets.${platform}.install`);
45
+ else if (target.install?.kind === "tauri") {
46
+ if (!target.install.productName || !target.install.targetTriple) throw new Error(`development.targets.${platform}.install requires productName and targetTriple`);
47
+ if (target.install.manifestPath && path.isAbsolute(target.install.manifestPath)) throw new Error("install.manifestPath must be relative to app root");
48
+ } else if (target.install?.kind === "portable") {
49
+ const descriptor = target.install;
50
+ if (descriptor.activation?.kind !== "current" || !path.isAbsolute(descriptor.installRoot ?? "")) throw new Error("portable development installer requires absolute installRoot and current activation");
51
+ for (const key of ["sourceDirectory", "entrypoint"]) {
52
+ if (typeof descriptor[key] !== "string" || !descriptor[key] || path.isAbsolute(descriptor[key]) || path.win32.isAbsolute(descriptor[key]) || descriptor[key].split(/[\\/]/).includes("..")) throw new Error(`portable development ${key} must be a contained relative path`);
53
+ }
54
+ } else if (target.install) throw new Error(`unsupported development installer kind: ${target.install.kind}`);
55
+ return target;
56
+ }
57
+
58
+ export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync }) {
59
+ if (!new Set(["win", "mac"]).has(platform)) throw new Error("--platform must be win or mac");
60
+ assertNativeDevelopmentHost(platform);
61
+ const target = validateDevelopmentConfig(config, platform);
62
+ const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
63
+ assertPrimaryReleaseCheckout(repoRoot, env);
64
+ assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
65
+ const childEnv = developmentEnvironment(env);
66
+ if (dryRun) return { built: false, installed: false, plan: { build: target.build, install: buildOnly ? null : target.install ?? null, profile: DEVELOPMENT_PROFILE } };
67
+ for (const command of target.build) execute(command, { root, env: childEnv, dryRun, run, label: "build" });
68
+ if (buildOnly || !target.install) return { built: true, installed: false };
69
+ install(target.install, { root, platform, env: childEnv, run });
70
+ return { built: true, installed: true };
71
+ }
72
+
73
+ export function assertNativeDevelopmentHost(platform, host = process.platform) {
74
+ if ((platform === "win" && host !== "win32") || (platform === "mac" && host !== "darwin")) throw new Error(`development target ${platform} requires its native host`);
75
+ }
76
+
77
+ function install(descriptor, { root, platform, env, run }) {
78
+ if (descriptor.kind === "command") return execute(descriptor, { root, env, run, label: "install" });
79
+ if (descriptor.kind === "portable") return installPortableDevelopment(descriptor, { root, platform: platform === "win" ? "win32" : "darwin", env });
80
+ if (platform === "win") {
81
+ if (!descriptor.expectedInstalledPath) throw new Error("Windows Tauri installer requires expectedInstalledPath for installed verification");
82
+ const { bundleRoot, installer: app } = resolveTauriBundleDirectory(descriptor, { root, platform, env });
83
+ const built = path.resolve(bundleRoot, "..", "..", path.basename(descriptor.expectedInstalledPath));
84
+ assertNonEmptyExecutable(built);
85
+ const result = run(app, ["/S"], { cwd: root, env, stdio: "inherit", windowsHide: true, shell: false });
86
+ if (result.status !== 0) throw new Error(`development installer failed (${result.status}): ${app}`);
87
+ const installed = path.resolve(root, descriptor.expectedInstalledPath);
88
+ assertInstalledExecutableMatches(built, installed);
89
+ return;
90
+ }
91
+ const app = resolveTauriArtifact(descriptor, { root, platform, env });
92
+ const destination = path.resolve(descriptor.destination ?? path.join("/Applications", path.basename(app)));
93
+ const destinationRoot = path.dirname(destination);
94
+ mkdirSync(destinationRoot, { recursive: true });
95
+ const staging = `${destination}.rightkit-dev-${process.pid}`;
96
+ const backup = `${destination}.rightkit-backup-${process.pid}`;
97
+ rmSync(staging, { recursive: true, force: true });
98
+ rmSync(backup, { recursive: true, force: true });
99
+ cpSync(app, staging, { recursive: true });
100
+ const signature = run("codesign", ["--force", "--deep", "--preserve-metadata=entitlements", "--sign", "-", staging], { cwd: root, env, stdio: "inherit", windowsHide: true, shell: false });
101
+ if (signature.status !== 0) { rmSync(staging, { recursive: true, force: true }); throw new Error(`ad-hoc signing failed (${signature.status}): ${staging}`); }
102
+ const verified = run("codesign", ["--verify", "--deep", "--strict", staging], { cwd: root, env, stdio: "inherit", windowsHide: true, shell: false });
103
+ if (verified.status !== 0) { rmSync(staging, { recursive: true, force: true }); throw new Error(`ad-hoc signature verification failed (${verified.status}): ${staging}`); }
104
+ let backedUp = false;
105
+ try {
106
+ if (existsSync(destination)) { renameSync(destination, backup); backedUp = true; }
107
+ renameSync(staging, destination);
108
+ if (!existsSync(path.join(destination, "Contents", "Info.plist"))) throw new Error(`installed app verification failed: ${destination}`);
109
+ if (backedUp) rmSync(backup, { recursive: true, force: true });
110
+ } catch (error) {
111
+ rmSync(destination, { recursive: true, force: true });
112
+ if (backedUp && existsSync(backup)) renameSync(backup, destination);
113
+ rmSync(staging, { recursive: true, force: true });
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ export function resolveTauriArtifact(descriptor, { root, platform, env = process.env, runCargo = runCargoSync }) {
119
+ return resolveTauriBundleDirectory(descriptor, { root, platform, env, runCargo }).installer;
120
+ }
121
+
122
+ export function resolveTauriBundleDirectory(descriptor, { root, platform, env = process.env, runCargo = runCargoSync }) {
123
+ const bundleRoot = descriptor.bundleDirectory
124
+ ? path.resolve(root, descriptor.bundleDirectory)
125
+ : resolveBundleRoot(descriptor, { root, env, runCargo });
126
+ if (descriptor.bundleDirectory && !inside(root, bundleRoot)) throw new Error("Tauri bundleDirectory must stay inside app root");
127
+ const entries = readdirSync(bundleRoot, { withFileTypes: true });
128
+ const candidates = platform === "mac"
129
+ ? entries.filter((entry) => entry.isDirectory() && entry.name === `${descriptor.productName}.app`).map((entry) => path.join(bundleRoot, entry.name))
130
+ : entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".exe") && entry.name.toLowerCase().includes("setup")).map((entry) => path.join(bundleRoot, entry.name));
131
+ if (candidates.length !== 1) throw new Error(`expected exactly one ${platform} Tauri installer for ${descriptor.productName} in ${bundleRoot}; found ${candidates.length}`);
132
+ if (!statSync(candidates[0]).isFile() && platform === "win") throw new Error(`installer is not a file: ${candidates[0]}`);
133
+ return { bundleRoot, installer: candidates[0] };
134
+ }
135
+
136
+ export function assertInstalledExecutableMatches(built, installed, read = readFileSync) {
137
+ assertNonEmptyExecutable(built);
138
+ assertNonEmptyExecutable(installed);
139
+ const digest = (file) => createHash("sha256").update(read(file)).digest("hex");
140
+ if (digest(built) !== digest(installed)) throw new Error(`installed executable does not match this dev build: ${installed}`);
141
+ }
142
+ function assertNonEmptyExecutable(file) { if (!existsSync(file) || !statSync(file).isFile() || statSync(file).size === 0) throw new Error(`installed executable verification failed: missing or empty file ${file}`); }
143
+
144
+ function resolveBundleRoot(descriptor, { root, env, runCargo }) {
145
+ if (!descriptor.manifestPath) throw new Error("Tauri installer requires manifestPath when bundleDirectory is omitted");
146
+ const manifest = path.resolve(root, descriptor.manifestPath);
147
+ const cwd = process.cwd();
148
+ let metadata;
149
+ try {
150
+ process.chdir(root);
151
+ metadata = runCargo(["metadata", "--offline", "--no-deps", "--format-version", "1", "--manifest-path", manifest], { cwd: root, env, encoding: "utf8", windowsHide: true, shell: false });
152
+ } finally { process.chdir(cwd); }
153
+ if (metadata.error || metadata.status !== 0) throw new Error(`RightKit Cargo metadata failed (${metadata.status ?? "unavailable"})${metadata.error ? `: ${metadata.error.message}` : ""}`);
154
+ let targetDirectory;
155
+ try { targetDirectory = JSON.parse(metadata.stdout).target_directory; } catch { throw new Error("RightKit Cargo metadata returned invalid JSON"); }
156
+ if (!targetDirectory || !path.isAbsolute(targetDirectory)) throw new Error("Cargo metadata omitted absolute target_directory");
157
+ return path.join(targetDirectory, descriptor.targetTriple, DEVELOPMENT_PROFILE, "bundle", platformBundleKind(descriptor));
158
+ }
159
+
160
+ function platformBundleKind(descriptor) { return descriptor.targetTriple.includes("apple") ? "macos" : "nsis"; }
161
+ function validateCommand(command, label) { if (!command || typeof command.cmd !== "string" || !command.cmd.trim() || (command.args !== undefined && !Array.isArray(command.args))) throw new Error(`${label} must declare cmd and args`); }
162
+ function execute(command, { root, env, dryRun, run, label }) {
163
+ validateCommand(command, `development ${label}`);
164
+ const cwd = path.resolve(root, command.cwd ?? ".");
165
+ if (!inside(root, cwd)) throw new Error(`development ${label} cwd must stay inside app root`);
166
+ const args = (command.args ?? []).map(String);
167
+ if (dryRun) return { cmd: command.cmd, args, cwd };
168
+ const launch = resolveCommand(command.cmd, args, { root, env });
169
+ const result = run(launch.command, launch.args, { cwd, env, stdio: "inherit", windowsHide: true, shell: false, timeout: command.timeoutMs });
170
+ if (result.status !== 0) throw new Error(`development ${label} command failed (${result.status}): ${command.name ?? command.cmd}`);
171
+ }
172
+ export function resolveCommand(command, args, { root, env = process.env } = {}) {
173
+ if (command === "node") return { command: process.execPath, args };
174
+ if (command === "pnpm") return { command: process.execPath, args: [resolvePnpmCli({ root, env }), ...args] };
175
+ if (command === "rightkit") {
176
+ const client = managedRuntimeClient(env);
177
+ if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
178
+ return { command: process.execPath, args: [client, ...args] };
179
+ }
180
+ return { command, args };
181
+ }
182
+ export function resolvePnpmCli({ root, env = process.env } = {}) {
183
+ const candidates = [];
184
+ if (env.npm_execpath && /pnpm/i.test(path.basename(env.npm_execpath))) candidates.push(env.npm_execpath);
185
+ const nodeDir = path.dirname(process.execPath);
186
+ candidates.push(path.join(nodeDir, "node_modules", "corepack", "dist", "pnpm.js"));
187
+ candidates.push(path.join(nodeDir, "node_modules", "pnpm", "bin", "pnpm.cjs"));
188
+ for (let dir = path.resolve(root ?? process.cwd()); ; dir = path.dirname(dir)) {
189
+ const store = path.join(dir, "node_modules", ".pnpm");
190
+ if (existsSync(store)) {
191
+ for (const entry of readdirSync(store).filter((name) => /^pnpm@/.test(name)).sort().reverse()) {
192
+ candidates.push(path.join(store, entry, "node_modules", "pnpm", "bin", "pnpm.cjs"));
193
+ }
194
+ }
195
+ const parent = path.dirname(dir);
196
+ if (parent === dir) break;
197
+ }
198
+ const selected = candidates.find((candidate) => existsSync(candidate));
199
+ if (!selected) throw new Error("could not resolve pnpm JavaScript CLI entrypoint; install/pin pnpm through Corepack");
200
+ return selected;
201
+ }
202
+ function git(cwd, args) { const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true }); if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed`); return result.stdout.trim(); }
203
+ function inside(root, child) { const rel = path.relative(path.resolve(root), path.resolve(child)); return rel === "" || (rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel)); }
204
+ export async function main(args = process.argv.slice(2)) {
205
+ let platform, configPath = "right-release.config.mjs", dryRun = false, buildOnly = false;
206
+ for (let i = 0; i < args.length; i++) {
207
+ if (args[i] === "-h" || args[i] === "--help") { console.log("usage: right-release dev --platform win|mac [--config right-release.config.mjs] [--dry-run] [--build-only]"); return; }
208
+ if (args[i] === "--platform") platform = args[++i];
209
+ else if (args[i] === "--config") configPath = args[++i];
210
+ else if (args[i] === "--dry-run") dryRun = true;
211
+ else if (args[i] === "--build-only") buildOnly = true;
212
+ else throw new Error(`unknown dev option: ${args[i]}`);
213
+ }
214
+ if (!platform) throw new Error("right-release dev requires --platform win|mac");
215
+ const absoluteConfig = path.resolve(configPath);
216
+ const safeEnv = developmentEnvironment(process.env);
217
+ for (const [key, value] of Object.entries(safeEnv)) process.env[key] = value;
218
+ delete process.env.CARGO_INCREMENTAL;
219
+ const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path");
220
+ if (pathKey) process.env[pathKey] = safeEnv[pathKey] ?? process.env[pathKey];
221
+ const loaded = await import(pathToFileURL(absoluteConfig));
222
+ const config = loaded.default ?? loaded.config ?? loaded;
223
+ const result = runDevelopment({ root: path.dirname(absoluteConfig), platform, config, dryRun, buildOnly });
224
+ if (dryRun) console.log(JSON.stringify(result.plan, null, 2));
225
+ return result;
226
+ }
227
+
228
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
229
+ main().catch((error) => { console.error(`right-release dev: ${error.message}`); process.exitCode = 1; });
230
+ }
package/notary-auth.mjs CHANGED
@@ -1,10 +1,18 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
 
4
- export function notarytoolAuthArgs({ profile = "apple-dev-notary" } = {}) {
5
- const keyPath = process.env.APPLE_API_KEY_PATH?.trim();
6
- const keyId = process.env.APPLE_API_KEY?.trim();
7
- const issuer = process.env.APPLE_API_ISSUER?.trim();
4
+ export function notarytoolAuthArgs({ profile, env = process.env } = {}) {
5
+ // A configured profile explicitly selects Keychain authentication. Inherited
6
+ // API settings belong to automatic selection only.
7
+ if (profile !== undefined) {
8
+ if (typeof profile !== "string" || !profile.trim()) {
9
+ throw new Error("Notarization keychain profile must be a non-empty string");
10
+ }
11
+ return ["--keychain-profile", profile.trim()];
12
+ }
13
+ const keyPath = env.APPLE_API_KEY_PATH?.trim();
14
+ const keyId = env.APPLE_API_KEY?.trim();
15
+ const issuer = env.APPLE_API_ISSUER?.trim();
8
16
  const apiValues = [keyPath, keyId, issuer];
9
17
 
10
18
  if (apiValues.every(Boolean)) {
@@ -13,7 +21,7 @@ export function notarytoolAuthArgs({ profile = "apple-dev-notary" } = {}) {
13
21
  if (apiValues.some(Boolean)) {
14
22
  throw new Error("Notarization API auth requires APPLE_API_KEY_PATH, APPLE_API_KEY, and APPLE_API_ISSUER together");
15
23
  }
16
- return ["--keychain-profile", profile];
24
+ return ["--keychain-profile", "apple-dev-notary"];
17
25
  }
18
26
 
19
27
  function expandHome(value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.106",
3
+ "version": "0.2.108",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,181 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+
5
+ const CHUNK_BYTES = 1024 * 1024;
6
+
7
+ /** Install an unsigned development payload through an owned, versioned `current` pointer. */
8
+ export function installPortableDevelopment(descriptor, { root, platform = process.platform, env = process.env } = {}) {
9
+ const emit = (event, fields = {}) => process.stdout.write(`${JSON.stringify({ event: `DEV_INSTALL_${event}`, ...fields })}\n`);
10
+ let backup = null;
11
+ let switched = false;
12
+ let versionRoot;
13
+ let digest;
14
+ let current;
15
+ try {
16
+ validateDescriptor(descriptor, root, platform);
17
+ const installRoot = path.resolve(descriptor.installRoot);
18
+ if (!path.isAbsolute(descriptor.installRoot) || isFilesystemRoot(installRoot, platform)) {
19
+ throw new Error("development installRoot must be an absolute non-filesystem-root path");
20
+ }
21
+ const source = resolveContained(root, descriptor.sourceDirectory, "sourceDirectory");
22
+ const sourceReal = fs.realpathSync(source);
23
+ if (!isWithin(fs.realpathSync(root), sourceReal, platform)) throw new Error("development sourceDirectory resolves outside repository root");
24
+ const sourceInfo = fs.lstatSync(source);
25
+ if (!sourceInfo.isDirectory() || sourceInfo.isSymbolicLink()) throw new Error("development sourceDirectory must be a real directory");
26
+ const sourceEntrypoint = resolveContained(source, descriptor.entrypoint, "entrypoint");
27
+ const sourceEntrypointInfo = fs.lstatSync(sourceEntrypoint);
28
+ if (!sourceEntrypointInfo.isFile() || sourceEntrypointInfo.isSymbolicLink()) throw new Error("development entrypoint must be a regular file inside sourceDirectory");
29
+ const sourceDigest = digestTree(source);
30
+ digest = sourceDigest;
31
+ versionRoot = path.join(installRoot, "versions", `dev-${digest}`);
32
+ current = path.join(installRoot, "current");
33
+ emit("STAGE", { sourceDirectory: source, versionRoot, digest });
34
+
35
+ fs.mkdirSync(path.dirname(versionRoot), { recursive: true });
36
+ if (exists(versionRoot)) {
37
+ if (digestTree(versionRoot) !== digest) throw new Error(`development version digest collision: ${versionRoot}`);
38
+ } else {
39
+ const staging = path.join(path.dirname(versionRoot), `.stage-${process.pid}-${randomSuffix()}`);
40
+ try {
41
+ copyTree(source, staging);
42
+ if (digestTree(staging) !== digest) throw new Error("staged development payload did not match source digest");
43
+ fs.renameSync(staging, versionRoot);
44
+ } catch (error) {
45
+ removeIfExists(staging);
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ validateCurrent(current, installRoot, platform);
51
+ if (exists(current)) {
52
+ backup = path.join(installRoot, `.current-backup-${process.pid}-${randomSuffix()}`);
53
+ fs.renameSync(current, backup);
54
+ }
55
+ try {
56
+ fs.symlinkSync(versionRoot, current, platform === "win32" ? "junction" : "dir");
57
+ switched = true;
58
+ emit("ACTIVATE", { current, versionRoot });
59
+ const activeRoot = fs.realpathSync(current);
60
+ if (!samePath(activeRoot, fs.realpathSync(versionRoot), platform)) throw new Error("development current pointer did not resolve to staged version");
61
+ const entrypoint = resolveContained(versionRoot, descriptor.entrypoint, "entrypoint");
62
+ const entryDigest = hashFile(entrypoint);
63
+ if (entryDigest !== hashFile(path.join(source, descriptor.entrypoint))) throw new Error("development entrypoint readback did not match source");
64
+ if (backup) removeIfExists(backup);
65
+ emit("SUCCEEDED", { installRoot, current, versionRoot, digest, entrypoint: descriptor.entrypoint, entrypointDigest: entryDigest });
66
+ return { installRoot, current, versionRoot, digest, entrypoint: path.join(current, descriptor.entrypoint), entrypointDigest: entryDigest };
67
+ } catch (error) {
68
+ if (switched) removeIfExists(current);
69
+ if (backup && exists(backup)) fs.renameSync(backup, current);
70
+ throw error;
71
+ }
72
+ } catch (error) {
73
+ emit("FAILED", { installRoot: descriptor?.installRoot, versionRoot, digest, message: error?.message ?? String(error) });
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ function validateDescriptor(descriptor, root, platform) {
79
+ if (!descriptor || descriptor.kind !== "portable" || descriptor.activation?.kind !== "current") throw new Error("unsupported portable development descriptor");
80
+ if (typeof root !== "string" || !path.isAbsolute(root)) throw new Error("repository root must be absolute");
81
+ if (typeof descriptor.installRoot !== "string" || !path.isAbsolute(descriptor.installRoot)) throw new Error("development installRoot must be absolute");
82
+ resolveContained(root, descriptor.sourceDirectory, "sourceDirectory");
83
+ resolveContained(root, descriptor.entrypoint, "entrypoint");
84
+ if (!isRelativePath(descriptor.sourceDirectory) || !isRelativePath(descriptor.entrypoint)) throw new Error("development sourceDirectory and entrypoint must be relative paths");
85
+ if (platform !== "win32" && platform !== "darwin" && platform !== "linux") throw new Error(`unsupported development install platform: ${platform}`);
86
+ }
87
+
88
+ function isRelativePath(value) {
89
+ return typeof value === "string" && value.length > 0 && !path.isAbsolute(value) && !path.win32.isAbsolute(value) && !value.split(/[\\/]/).includes("..");
90
+ }
91
+
92
+ function resolveContained(base, relative, label) {
93
+ if (!isRelativePath(relative)) throw new Error(`${label} must be a contained relative path`);
94
+ const resolved = path.resolve(base, relative);
95
+ if (!isWithin(path.resolve(base), resolved, process.platform)) throw new Error(`${label} escapes its parent directory`);
96
+ return resolved;
97
+ }
98
+
99
+ function isFilesystemRoot(value, platform) {
100
+ const parsed = path.parse(value);
101
+ return samePath(parsed.root, value, platform);
102
+ }
103
+
104
+ function isWithin(parent, child, platform) {
105
+ const relative = path.relative(parent, child);
106
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
107
+ }
108
+
109
+ function samePath(left, right, platform) {
110
+ const a = path.normalize(left);
111
+ const b = path.normalize(right);
112
+ return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
113
+ }
114
+
115
+ function exists(file) {
116
+ try { fs.lstatSync(file); return true; } catch (error) { if (error?.code === "ENOENT") return false; throw error; }
117
+ }
118
+
119
+ function digestTree(directory) {
120
+ const hash = createHash("sha256");
121
+ const visit = (dir, relative = "") => {
122
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
123
+ for (const entry of entries) {
124
+ const rel = relative ? `${relative}/${entry.name}` : entry.name;
125
+ const absolute = path.join(dir, entry.name);
126
+ const stat = fs.lstatSync(absolute);
127
+ if (stat.isSymbolicLink()) throw new Error(`development payload contains a symbolic link: ${rel}`);
128
+ if (stat.isDirectory()) { hash.update(`D\0${rel}\0`); visit(absolute, rel); }
129
+ else if (stat.isFile()) hash.update(`F\0${rel}\0${stat.size}\0${hashFile(absolute)}\0`);
130
+ else throw new Error(`development payload contains a non-regular entry: ${rel}`);
131
+ }
132
+ };
133
+ visit(directory);
134
+ return hash.digest("hex");
135
+ }
136
+
137
+ function hashFile(file) {
138
+ const fd = fs.openSync(file, "r");
139
+ const hash = createHash("sha256");
140
+ const buffer = Buffer.allocUnsafe(CHUNK_BYTES);
141
+ try {
142
+ let bytes;
143
+ while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
144
+ } finally { fs.closeSync(fd); }
145
+ return hash.digest("hex");
146
+ }
147
+
148
+ function copyTree(source, target) {
149
+ fs.mkdirSync(target, { recursive: true });
150
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
151
+ const from = path.join(source, entry.name);
152
+ const to = path.join(target, entry.name);
153
+ const stat = fs.lstatSync(from);
154
+ if (stat.isSymbolicLink()) throw new Error(`development payload contains a symbolic link: ${from}`);
155
+ if (stat.isDirectory()) copyTree(from, to);
156
+ else if (stat.isFile()) fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL);
157
+ else throw new Error(`development payload contains a non-regular entry: ${from}`);
158
+ }
159
+ }
160
+
161
+ function validateCurrent(current, installRoot, platform) {
162
+ if (!exists(current)) return;
163
+ const stat = fs.lstatSync(current);
164
+ if (!stat.isSymbolicLink()) throw new Error(`development current pointer is not a symlink or junction; refusing to replace: ${current}`);
165
+ let target;
166
+ try { target = fs.realpathSync(current); } catch (error) { throw new Error(`development current pointer is dangling or unreadable: ${current}`, { cause: error }); }
167
+ const versionsRoot = path.join(installRoot, "versions");
168
+ const versionName = path.basename(target);
169
+ if (!isWithin(fs.realpathSync(installRoot), target, platform)
170
+ || !samePath(path.dirname(target), fs.realpathSync(versionsRoot), platform)
171
+ || !versionName || versionName === "." || versionName === "..") {
172
+ throw new Error(`development current pointer does not resolve to an owned version: ${current}`);
173
+ }
174
+ }
175
+
176
+ function removeIfExists(target) {
177
+ if (!exists(target)) return;
178
+ fs.rmSync(target, { recursive: true, force: true });
179
+ }
180
+
181
+ function randomSuffix() { return `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
@@ -1,141 +1,143 @@
1
- {
2
- "schema": 2,
3
- "packageManager": "pnpm@11.24.0",
4
- "npm": {
5
- "@rightkit/git": "0.2.0",
6
- "@rightkit/legal": "0.2.0",
7
- "@rightkit/license": "0.1.5",
8
- "@rightkit/logs": "0.1.3",
9
- "@rightkit/platform-ui": "0.1.0",
10
- "@rightkit/qa": "0.1.0",
11
- "@rightkit/release": "0.2.47",
12
- "@rightkit/tauri": "0.1.0",
13
- "@rightkit/updates": "0.2.3"
14
- },
15
- "stagedNpm": {
16
- "@rightkit/ax": "0.2.4",
17
- "@rightkit/git": "0.2.25",
18
- "@rightkit/hooks": "0.1.1",
19
- "@rightkit/legal": "0.3.1",
20
- "@rightkit/legal-ui": "0.1.1",
21
- "@rightkit/license": "0.1.7",
22
- "@rightkit/logs": "0.1.4",
23
- "@rightkit/platform-ui": "0.1.1",
24
- "@rightkit/qa": "0.2.1",
25
- "@rightkit/release": "0.2.106",
26
- "@rightkit/tauri": "0.1.1",
27
- "@rightkit/updates": "0.2.4"
28
- },
29
- "legacyNpm": {
30
- "@rightkit/ax": [
31
- "0.2.0",
32
- "0.2.1",
33
- "0.2.2",
34
- "0.2.3"
35
- ],
36
- "@rightkit/git": [
37
- "0.2.1",
38
- "0.2.2",
39
- "0.2.3",
40
- "0.2.4",
41
- "0.2.5",
42
- "0.2.6",
43
- "0.2.7",
44
- "0.2.8",
45
- "0.2.9",
46
- "0.2.10",
47
- "0.2.11",
48
- "0.2.12",
49
- "0.2.13",
50
- "0.2.14",
51
- "0.2.15",
52
- "0.2.16",
53
- "0.2.18",
54
- "0.2.19",
55
- "0.2.20",
56
- "0.2.21",
57
- "0.2.22",
58
- "0.2.23",
59
- "0.2.24"
60
- ],
61
- "@rightkit/legal-ui": [
62
- "0.1.0"
63
- ],
64
- "@rightkit/hooks": [
65
- "0.1.0"
66
- ],
67
- "@rightkit/legal": [
68
- "0.3.0"
69
- ],
70
- "@rightkit/license": [
71
- "0.1.6"
72
- ],
73
- "@rightkit/release": [
74
- "0.2.22",
75
- "0.2.29",
76
- "0.2.30",
77
- "0.2.31",
78
- "0.2.41",
79
- "0.2.42",
80
- "0.2.43",
81
- "0.2.44",
82
- "0.2.45",
83
- "0.2.46",
84
- "0.2.49",
85
- "0.2.50",
86
- "0.2.51",
87
- "0.2.53",
88
- "0.2.54",
89
- "0.2.55",
90
- "0.2.56",
91
- "0.2.61",
92
- "0.2.62",
93
- "0.2.63",
94
- "0.2.64",
95
- "0.2.65",
96
- "0.2.66",
97
- "0.2.67",
98
- "0.2.68",
99
- "0.2.69",
100
- "0.2.70",
101
- "0.2.71",
102
- "0.2.72",
103
- "0.2.73",
104
- "0.2.74",
105
- "0.2.75",
106
- "0.2.76",
107
- "0.2.82",
108
- "0.2.83",
109
- "0.2.91",
110
- "0.2.92",
111
- "0.2.94",
112
- "0.2.95",
113
- "0.2.96",
114
- "0.2.97",
115
- "0.2.98",
116
- "0.2.99",
117
- "0.2.104",
118
- "0.2.105"
119
- ],
120
- "@rightkit/qa": [
121
- "0.1.0",
122
- "0.2.0"
123
- ]
124
- },
125
- "cargo": {
126
- "rightkit-license": "0.1.2",
127
- "rightkit-logs": "0.1.0",
128
- "rightkit-process": "0.1.0",
129
- "rightkit-tauri": "0.1.0"
130
- },
131
- "stagedCargo": {
132
- "rightkit-license": "0.1.3",
133
- "rightkit-tauri": "0.1.1"
134
- },
135
- "swift": {
136
- "rightkit-swift": {
137
- "url": "https://github.com/bogusyogi/rightkit-swift.git",
138
- "version": "0.1.1"
139
- }
140
- }
141
- }
1
+ {
2
+ "schema": 2,
3
+ "packageManager": "pnpm@11.24.0",
4
+ "npm": {
5
+ "@rightkit/git": "0.2.0",
6
+ "@rightkit/legal": "0.2.0",
7
+ "@rightkit/license": "0.1.5",
8
+ "@rightkit/logs": "0.1.3",
9
+ "@rightkit/platform-ui": "0.1.0",
10
+ "@rightkit/qa": "0.1.0",
11
+ "@rightkit/release": "0.2.47",
12
+ "@rightkit/tauri": "0.1.0",
13
+ "@rightkit/updates": "0.2.3"
14
+ },
15
+ "stagedNpm": {
16
+ "@rightkit/ax": "0.2.4",
17
+ "@rightkit/git": "0.2.25",
18
+ "@rightkit/hooks": "0.1.1",
19
+ "@rightkit/legal": "0.3.1",
20
+ "@rightkit/legal-ui": "0.1.1",
21
+ "@rightkit/license": "0.1.7",
22
+ "@rightkit/logs": "0.1.4",
23
+ "@rightkit/platform-ui": "0.1.1",
24
+ "@rightkit/qa": "0.2.1",
25
+ "@rightkit/release": "0.2.108",
26
+ "@rightkit/tauri": "0.1.1",
27
+ "@rightkit/updates": "0.2.4"
28
+ },
29
+ "legacyNpm": {
30
+ "@rightkit/ax": [
31
+ "0.2.0",
32
+ "0.2.1",
33
+ "0.2.2",
34
+ "0.2.3"
35
+ ],
36
+ "@rightkit/git": [
37
+ "0.2.1",
38
+ "0.2.2",
39
+ "0.2.3",
40
+ "0.2.4",
41
+ "0.2.5",
42
+ "0.2.6",
43
+ "0.2.7",
44
+ "0.2.8",
45
+ "0.2.9",
46
+ "0.2.10",
47
+ "0.2.11",
48
+ "0.2.12",
49
+ "0.2.13",
50
+ "0.2.14",
51
+ "0.2.15",
52
+ "0.2.16",
53
+ "0.2.18",
54
+ "0.2.19",
55
+ "0.2.20",
56
+ "0.2.21",
57
+ "0.2.22",
58
+ "0.2.23",
59
+ "0.2.24"
60
+ ],
61
+ "@rightkit/legal-ui": [
62
+ "0.1.0"
63
+ ],
64
+ "@rightkit/hooks": [
65
+ "0.1.0"
66
+ ],
67
+ "@rightkit/legal": [
68
+ "0.3.0"
69
+ ],
70
+ "@rightkit/license": [
71
+ "0.1.6"
72
+ ],
73
+ "@rightkit/release": [
74
+ "0.2.22",
75
+ "0.2.29",
76
+ "0.2.30",
77
+ "0.2.31",
78
+ "0.2.41",
79
+ "0.2.42",
80
+ "0.2.43",
81
+ "0.2.44",
82
+ "0.2.45",
83
+ "0.2.46",
84
+ "0.2.49",
85
+ "0.2.50",
86
+ "0.2.51",
87
+ "0.2.53",
88
+ "0.2.54",
89
+ "0.2.55",
90
+ "0.2.56",
91
+ "0.2.61",
92
+ "0.2.62",
93
+ "0.2.63",
94
+ "0.2.64",
95
+ "0.2.65",
96
+ "0.2.66",
97
+ "0.2.67",
98
+ "0.2.68",
99
+ "0.2.69",
100
+ "0.2.70",
101
+ "0.2.71",
102
+ "0.2.72",
103
+ "0.2.73",
104
+ "0.2.74",
105
+ "0.2.75",
106
+ "0.2.76",
107
+ "0.2.82",
108
+ "0.2.83",
109
+ "0.2.91",
110
+ "0.2.92",
111
+ "0.2.94",
112
+ "0.2.95",
113
+ "0.2.96",
114
+ "0.2.97",
115
+ "0.2.98",
116
+ "0.2.99",
117
+ "0.2.104",
118
+ "0.2.105",
119
+ "0.2.106",
120
+ "0.2.107"
121
+ ],
122
+ "@rightkit/qa": [
123
+ "0.1.0",
124
+ "0.2.0"
125
+ ]
126
+ },
127
+ "cargo": {
128
+ "rightkit-license": "0.1.2",
129
+ "rightkit-logs": "0.1.0",
130
+ "rightkit-process": "0.1.0",
131
+ "rightkit-tauri": "0.1.0"
132
+ },
133
+ "stagedCargo": {
134
+ "rightkit-license": "0.1.3",
135
+ "rightkit-tauri": "0.1.1"
136
+ },
137
+ "swift": {
138
+ "rightkit-swift": {
139
+ "url": "https://github.com/bogusyogi/rightkit-swift.git",
140
+ "version": "0.1.1"
141
+ }
142
+ }
143
+ }