@guiho/runx 0.2.6 → 0.4.0

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/DOCS.md +128 -109
  3. package/README.md +58 -58
  4. package/devops/build-binaries.ts +44 -62
  5. package/devops/devops.xdocs.md +8 -5
  6. package/devops/install.ps1 +182 -152
  7. package/devops/install.sh +197 -262
  8. package/devops/installers.spec.ts +31 -0
  9. package/devops/verify-release-assets.ts +17 -0
  10. package/docs/decisions/decisions.xdocs.md +4 -0
  11. package/docs/decisions/interactive-init-manifest.md +187 -0
  12. package/docs/docs.xdocs.md +3 -1
  13. package/docs/plans/interactive-init-manifest.md +177 -0
  14. package/docs/plans/plans.xdocs.md +11 -0
  15. package/docs/plans/rfc-0034-cli-compliance-migration.md +586 -0
  16. package/docs/plans/upgrade-reliability-implementation.md +95 -0
  17. package/docs/reviews/implementation/implementation.xdocs.md +6 -0
  18. package/docs/reviews/implementation/interactive-init-manifest-review.md +69 -0
  19. package/docs/reviews/implementation/rfc-0034-cli-compliance-migration-review.md +59 -0
  20. package/docs/reviews/plans/interactive-init-manifest-review.md +82 -0
  21. package/docs/reviews/plans/plans.xdocs.md +10 -0
  22. package/docs/reviews/plans/rfc-0034-cli-compliance-migration-review.md +80 -0
  23. package/docs/reviews/plans/upgrade-reliability-implementation-review.md +65 -0
  24. package/docs/superpowers/specs/2026-07-15-upgrade-reliability-design.md +662 -0
  25. package/docs/superpowers/specs/specs.xdocs.md +24 -0
  26. package/docs/superpowers/superpowers.xdocs.md +21 -0
  27. package/docs/todo/rfc-0034-cli-compliance-migration-implementation.md +58 -0
  28. package/docs/todo/rfc-0034-cli-compliance-migration.md +151 -0
  29. package/docs/todo/todo.xdocs.md +6 -2
  30. package/docs/validation/interactive-init-manifest.md +79 -0
  31. package/docs/validation/rfc-0034-cli-compliance-migration.md +67 -0
  32. package/docs/validation/upgrade-reliability.md +124 -0
  33. package/docs/validation/validation.xdocs.md +9 -0
  34. package/library/agents.d.ts +28 -4
  35. package/library/agents.d.ts.map +1 -1
  36. package/library/agents.js +143 -41
  37. package/library/cli.d.ts.map +1 -1
  38. package/library/cli.js +297 -317
  39. package/library/configuration.d.ts +57 -0
  40. package/library/configuration.d.ts.map +1 -0
  41. package/library/configuration.js +111 -0
  42. package/library/embedded-resources.d.ts +6 -1
  43. package/library/embedded-resources.d.ts.map +1 -1
  44. package/library/embedded-resources.js +10 -4
  45. package/library/executor.d.ts +5 -1
  46. package/library/executor.d.ts.map +1 -1
  47. package/library/executor.js +10 -10
  48. package/library/help.d.ts +8 -4
  49. package/library/help.d.ts.map +1 -1
  50. package/library/help.js +70 -42
  51. package/library/init.d.ts +18 -0
  52. package/library/init.d.ts.map +1 -0
  53. package/library/init.js +43 -0
  54. package/library/manifest.d.ts +4 -40
  55. package/library/manifest.d.ts.map +1 -1
  56. package/library/manifest.js +4 -112
  57. package/library/path-utils.d.ts +13 -0
  58. package/library/path-utils.d.ts.map +1 -0
  59. package/library/path-utils.js +82 -0
  60. package/library/recovery.d.ts +7 -0
  61. package/library/recovery.d.ts.map +1 -0
  62. package/library/recovery.js +23 -0
  63. package/library/release-catalog.d.ts +32 -0
  64. package/library/release-catalog.d.ts.map +1 -0
  65. package/library/release-catalog.js +124 -0
  66. package/library/self-management.d.ts +24 -4
  67. package/library/self-management.d.ts.map +1 -1
  68. package/library/self-management.js +279 -99
  69. package/library/storage.d.ts +13 -0
  70. package/library/storage.d.ts.map +1 -0
  71. package/library/storage.js +38 -0
  72. package/library/types.d.ts +11 -16
  73. package/library/types.d.ts.map +1 -1
  74. package/library/types.js +3 -0
  75. package/library/update-cache.d.ts +21 -0
  76. package/library/update-cache.d.ts.map +1 -0
  77. package/library/update-cache.js +68 -0
  78. package/library/upgrade-reporting.d.ts +11 -0
  79. package/library/upgrade-reporting.d.ts.map +1 -0
  80. package/library/upgrade-reporting.js +67 -0
  81. package/library/upgrade-types.d.ts +72 -0
  82. package/library/upgrade-types.d.ts.map +1 -0
  83. package/library/upgrade-types.js +3 -0
  84. package/package.json +6 -3
  85. package/scripts/runx-bin.mjs +49 -0
  86. package/scripts/runx-bin.spec.ts +62 -0
  87. package/scripts/scripts.xdocs.md +3 -3
  88. package/skills/guiho-s-runx/SKILL.md +64 -59
  89. package/skills/guiho-s-runx/guiho-s-runx.xdocs.md +7 -4
  90. package/skills/skills.xdocs.md +1 -1
  91. package/docs/todo/implement-runx-alpha.md +0 -36
  92. package/scripts/runx-bin.ts +0 -24
@@ -1,55 +1,213 @@
1
- import { Buffer } from 'node:buffer';
2
- import { chmod, rename, rm } from 'node:fs/promises';
3
- import { basename } from 'node:path';
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+ import { $ } from 'bun';
5
+ import { compare, gt, valid } from 'semver';
4
6
  import { RunXError } from './errors.js';
7
+ import { applyAgentInstructions, updateAgentSkill } from './agents.js';
5
8
  import { readVersion } from './help.js';
6
- export { checkForLatestVersion, listAvailableVersions, uninstallSelf, upgradeSelf, };
7
- const repositoryApi = 'https://api.github.com/repos/CGuiho/runx/releases';
8
- const checkForLatestVersion = async () => {
9
- const currentVersion = readVersion();
10
- try {
11
- const response = await fetch(`${repositoryApi}/latest`, { headers: { accept: 'application/vnd.github+json' } });
12
- if (!response.ok)
13
- return { currentVersion, latestVersion: currentVersion, updateAvailable: false };
14
- const release = await response.json();
15
- const latestVersion = release.tag_name.replace(/^@guiho\/runx@/, '').replace(/^v/, '');
16
- const asset = findAsset(release);
17
- return { currentVersion, latestVersion, updateAvailable: compareVersions(latestVersion, currentVersion) > 0, url: asset?.browser_download_url };
18
- }
19
- catch {
20
- return { currentVersion, latestVersion: currentVersion, updateAvailable: false };
9
+ import { baseName } from './path-utils.js';
10
+ import { fetchReleaseCatalog, resolveUpgradePlatform } from './release-catalog.js';
11
+ import { createRecoveryInstructions } from './recovery.js';
12
+ export { checkForLatestVersion, listAvailableVersions, uninstallSelf, upgradeSelf, validateNativeBinary };
13
+ class UpgradeOperationError extends Error {
14
+ code;
15
+ constructor(code, message) {
16
+ super(message);
17
+ this.name = 'UpgradeOperationError';
18
+ this.code = code;
21
19
  }
20
+ }
21
+ const defaultFileOperations = {
22
+ rename: async (from, to) => {
23
+ if (process.platform === 'win32') {
24
+ const child = Bun.spawn(['cmd.exe', '/d', '/s', '/c', 'move', '/y', from, to], { stdout: 'pipe', stderr: 'pipe' });
25
+ const [code, error] = await Promise.all([child.exited, new Response(child.stderr).text()]);
26
+ if (code !== 0)
27
+ throw new Error(error.trim() || `Could not move ${from} to ${to}.`);
28
+ return;
29
+ }
30
+ await $ `mv ${from} ${to}`.quiet();
31
+ },
32
+ remove: async (path) => {
33
+ if (process.platform === 'win32') {
34
+ const child = Bun.spawn(['cmd.exe', '/d', '/s', '/c', 'del', '/f', '/q', path], { stdout: 'ignore', stderr: 'pipe' });
35
+ const [code, error] = await Promise.all([child.exited, new Response(child.stderr).text()]);
36
+ if (code !== 0 && await Bun.file(path).exists())
37
+ throw new Error(error.trim() || `Could not remove ${path}.`);
38
+ return;
39
+ }
40
+ await $ `rm -f ${path}`.quiet();
41
+ },
42
+ makeExecutable: async (path) => {
43
+ if (process.platform === 'win32')
44
+ return;
45
+ await $ `chmod 755 ${path}`.quiet();
46
+ },
47
+ };
48
+ const checkForLatestVersion = async () => {
49
+ const catalog = await listAvailableVersions();
50
+ const latest = catalog.releases.find((release) => release.latestStable);
51
+ return {
52
+ currentVersion: catalog.currentVersion,
53
+ latestVersion: catalog.latestStableVersion ?? catalog.currentVersion,
54
+ updateAvailable: Boolean(latest && valid(latest.version) && valid(catalog.currentVersion) && gt(latest.version, catalog.currentVersion)),
55
+ url: latest?.compatibleAsset?.url,
56
+ };
22
57
  };
23
58
  const listAvailableVersions = async () => {
24
- const response = await fetch(`${repositoryApi}?per_page=20`, { headers: { accept: 'application/vnd.github+json' } });
25
- if (!response.ok)
26
- throw new RunXError(`Could not retrieve RunX releases: HTTP ${response.status}`);
27
- const releases = await response.json();
28
- return releases.map((release) => release.tag_name.replace(/^@guiho\/runx@/, '').replace(/^v/, ''));
59
+ const platform = resolveUpgradePlatform();
60
+ return fetchReleaseCatalog({ ...platform, currentVersion: readVersion() });
29
61
  };
30
- const upgradeSelf = async (dryRun) => {
31
- const executablePath = requireNativeExecutable();
32
- const update = await checkForLatestVersion();
33
- const upToDate = !update.updateAvailable && update.latestVersion === update.currentVersion && Boolean(update.url);
34
- if (!update.updateAvailable || !update.url)
35
- return { ...update, executablePath, scheduled: false, upToDate };
36
- if (dryRun)
37
- return { ...update, executablePath, scheduled: false, upToDate: false };
38
- const response = await fetch(update.url);
39
- if (!response.ok)
40
- throw new RunXError(`Could not download RunX update: HTTP ${response.status}`);
41
- const temporaryPath = `${executablePath}.new`;
42
- await Bun.write(temporaryPath, await response.arrayBuffer());
43
- if (process.platform !== 'win32') {
44
- await chmod(temporaryPath, 0o755);
45
- await rename(temporaryPath, executablePath);
46
- return { ...update, executablePath, scheduled: false, upToDate: false };
62
+ const upgradeSelf = async (input) => {
63
+ const options = typeof input === 'boolean' ? { dryRun: input } : input;
64
+ const fileOperations = options.fileOperations ?? defaultFileOperations;
65
+ const currentVersion = options.currentVersion ?? readVersion();
66
+ const detectedPlatform = resolveUpgradePlatform(process.platform, options.arch ?? process.arch);
67
+ const platform = { ...detectedPlatform, variant: options.variant ?? detectedPlatform.variant };
68
+ const executablePath = resolveSelfExecutable();
69
+ const events = [];
70
+ let phase = 'plan';
71
+ let plan = null;
72
+ let recovery = createRecoveryInstructions(currentVersion, platform.os, 'fallback-current');
73
+ let temporaryPath = null;
74
+ let replacement = null;
75
+ let mutationCode = null;
76
+ const emit = (eventPhase, status, message) => {
77
+ phase = eventPhase;
78
+ const event = { sequence: events.length + 1, phase: eventPhase, status, ...(message ? { message } : {}) };
79
+ events.push(event);
80
+ options.onEvent?.(event);
81
+ };
82
+ emit('plan', 'started');
83
+ try {
84
+ let catalog;
85
+ try {
86
+ catalog = await fetchReleaseCatalog({ ...platform, currentVersion });
87
+ }
88
+ catch (error) {
89
+ throw new UpgradeOperationError(classifyPlanError(error), errorMessage(error));
90
+ }
91
+ const requestedVersion = options.requestedVersion?.replace(/^@guiho\/runx@/, '').replace(/^v/, '');
92
+ const stableTarget = requestedVersion
93
+ ? catalog.releases.find((release) => release.version === requestedVersion)
94
+ : catalog.releases.find((release) => release.latestStable);
95
+ if (!stableTarget || !catalog.latestStableVersion)
96
+ throw new UpgradeOperationError('release_lookup_failed', 'No stable RunX release is available for upgrade.');
97
+ const target = preventDowngrade(currentVersion, stableTarget, catalog.releases);
98
+ const targetIsCurrent = target.version === currentVersion;
99
+ recovery = createRecoveryInstructions(target.version, platform.os, 'resolved');
100
+ if (!targetIsCurrent && !target.compatibleAsset) {
101
+ throw new UpgradeOperationError('no_compatible_asset', `RunX ${target.version} has no compatible ${platform.os} ${platform.arch} asset.`);
102
+ }
103
+ plan = {
104
+ currentVersion,
105
+ targetVersion: target.version,
106
+ os: platform.os,
107
+ arch: platform.arch,
108
+ assetName: target.compatibleAsset?.name ?? '',
109
+ assetUrl: target.compatibleAsset?.url ?? '',
110
+ executablePath,
111
+ };
112
+ emit('plan', 'succeeded');
113
+ options.onPlan?.(plan);
114
+ if (baseName(executablePath).toLowerCase().startsWith('bun')) {
115
+ throw new UpgradeOperationError('verification_failed', 'Self-management requires a native RunX executable installed from a GitHub release.');
116
+ }
117
+ if (targetIsCurrent) {
118
+ emitSkippedMutation(events, options.onEvent);
119
+ return envelope('up-to-date', plan, events, { installedVersion: currentVersion, cleanupDeferred: false }, recovery);
120
+ }
121
+ if (options.dryRun) {
122
+ emitSkippedMutation(events, options.onEvent);
123
+ return envelope('dry-run', plan, events, null, recovery);
124
+ }
125
+ emit('download', 'started');
126
+ const response = await fetch(plan.assetUrl);
127
+ if (!response.ok)
128
+ throw new UpgradeOperationError('download_failed', `Could not download RunX ${plan.targetVersion}: HTTP ${response.status}`);
129
+ const bytes = new Uint8Array(await response.arrayBuffer());
130
+ if (bytes.length === 0)
131
+ throw new UpgradeOperationError('download_invalid', 'Downloaded RunX executable is empty.');
132
+ emit('download', 'succeeded');
133
+ emit('validate', 'started');
134
+ try {
135
+ validateNativeBinary(bytes, platform.os);
136
+ temporaryPath = `${executablePath}.new-${process.pid}-${crypto.randomUUID()}`;
137
+ await Bun.write(temporaryPath, bytes);
138
+ if (platform.os !== 'windows')
139
+ await fileOperations.makeExecutable(temporaryPath);
140
+ }
141
+ catch (error) {
142
+ throw asOperationError('download_invalid', error);
143
+ }
144
+ emit('validate', 'succeeded');
145
+ emit('replace', 'started');
146
+ replacement = { backupPath: `${executablePath}.old-${process.pid}-${crypto.randomUUID()}`, originalMoved: false };
147
+ mutationCode = 'backup_failed';
148
+ try {
149
+ await fileOperations.rename(executablePath, replacement.backupPath);
150
+ replacement.originalMoved = true;
151
+ mutationCode = 'replace_failed';
152
+ await fileOperations.rename(temporaryPath, executablePath);
153
+ temporaryPath = null;
154
+ }
155
+ catch (error) {
156
+ throw asOperationError(mutationCode, error);
157
+ }
158
+ emit('replace', 'succeeded');
159
+ emit('verify', 'started');
160
+ try {
161
+ await verifyExecutableVersion(executablePath, plan.targetVersion);
162
+ }
163
+ catch (error) {
164
+ throw asOperationError('verification_failed', error);
165
+ }
166
+ emit('verify', 'succeeded');
167
+ emit('cache', 'started');
168
+ if (Bun.env.NODE_ENV === 'test') {
169
+ emit('cache', 'skipped', 'Agent reconciliation is isolated during tests.');
170
+ }
171
+ else {
172
+ await updateAgentSkill('global', process.cwd());
173
+ await applyAgentInstructions(process.cwd());
174
+ emit('cache', 'succeeded', 'Agent skill and project instructions reconciled.');
175
+ }
176
+ emit('cleanup', 'started');
177
+ const cleanupDeferred = await cleanupBackup(replacement.backupPath, platform.os, fileOperations);
178
+ emit('cleanup', cleanupDeferred ? 'skipped' : 'succeeded', cleanupDeferred ? 'Old executable deletion was deferred; replacement already succeeded.' : undefined);
179
+ return envelope('upgraded', plan, events, { installedVersion: plan.targetVersion, cleanupDeferred }, recovery);
180
+ }
181
+ catch (error) {
182
+ const primary = asOperationError(classifyFailure(phase, mutationCode), error);
183
+ if (!events.some((event) => event.phase === phase && event.status === 'failed'))
184
+ emit(phase, 'failed', primary.message);
185
+ if (temporaryPath)
186
+ await fileOperations.remove(temporaryPath).catch(() => undefined);
187
+ if (replacement?.originalMoved) {
188
+ try {
189
+ await rollbackReplacement(replacement.backupPath, executablePath, fileOperations);
190
+ return envelope('rolled-back', plan, events, { installedVersion: currentVersion, cleanupDeferred: false }, recovery, {
191
+ code: primary.code,
192
+ phase,
193
+ message: primary.message,
194
+ });
195
+ }
196
+ catch (rollbackError) {
197
+ return envelope('failed', plan, events, null, recovery, {
198
+ code: 'rollback_failed',
199
+ phase,
200
+ message: `${primary.message}. Automatic rollback failed: ${errorMessage(rollbackError)}. Canonical path: ${executablePath}. Backup path: ${replacement.backupPath}.`,
201
+ });
202
+ }
203
+ }
204
+ return envelope('failed', plan, events, null, recovery, { code: primary.code, phase, message: primary.message });
47
205
  }
48
- await replaceWindowsExecutable(temporaryPath, executablePath, update.latestVersion);
49
- return { ...update, executablePath, scheduled: false, upToDate: false };
50
206
  };
51
207
  const uninstallSelf = async (dryRun) => {
52
- const executablePath = requireNativeExecutable();
208
+ const executablePath = resolveSelfExecutable();
209
+ if (baseName(executablePath).toLowerCase().startsWith('bun'))
210
+ throw new RunXError('Self-management requires a native RunX executable installed from a GitHub release.');
53
211
  if (dryRun)
54
212
  return { executablePath, scheduled: false, dryRun: true };
55
213
  if (process.platform === 'win32') {
@@ -57,84 +215,106 @@ const uninstallSelf = async (dryRun) => {
57
215
  Bun.spawn(['cmd.exe', '/d', '/s', '/c', command], { detached: true, stdout: 'ignore', stderr: 'ignore', stdin: 'ignore' });
58
216
  return { executablePath, scheduled: true, dryRun: false };
59
217
  }
60
- await rm(executablePath);
218
+ await defaultFileOperations.remove(executablePath);
61
219
  return { executablePath, scheduled: false, dryRun: false };
62
220
  };
63
- const findAsset = (release) => release.assets.find((asset) => asset.name === assetName());
64
- const assetName = () => `runx-${process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'darwin' : 'linux'}-${process.arch}${process.platform === 'win32' ? '.exe' : ''}`;
65
- const requireNativeExecutable = () => {
66
- const executablePath = process.env['RUNX_SELF_PATH'] ?? process.execPath;
67
- if (basename(executablePath).toLowerCase().startsWith('bun'))
68
- throw new RunXError('Self-management requires a native RunX executable installed from a GitHub release.');
69
- return executablePath;
221
+ const envelope = (outcome, plan, events, result, recovery, error = null) => ({ schemaVersion: 1, command: 'runx upgrade', outcome, plan, events, result, recovery, error });
222
+ const preventDowngrade = (currentVersion, stableTarget, releases) => {
223
+ if (!valid(currentVersion) || !valid(stableTarget.version) || compare(currentVersion, stableTarget.version) < 0)
224
+ return stableTarget;
225
+ return releases.find((release) => release.version === currentVersion) ?? {
226
+ tag: currentVersion,
227
+ version: currentVersion,
228
+ channel: valid(currentVersion)?.includes('-') ? 'prerelease' : 'stable',
229
+ prerelease: Boolean(valid(currentVersion)?.includes('-')),
230
+ publishedAt: null,
231
+ current: true,
232
+ latestStable: false,
233
+ compatibleAsset: null,
234
+ };
70
235
  };
71
- const replaceWindowsExecutable = async (temporaryPath, executablePath, expectedVersion) => {
72
- const backupPath = `${executablePath}.old`;
73
- let originalMoved = false;
74
- try {
75
- await rm(backupPath, { force: true });
76
- await rename(executablePath, backupPath);
77
- originalMoved = true;
78
- await rename(temporaryPath, executablePath);
79
- await verifyExecutableVersion(executablePath, expectedVersion);
80
- await cleanupWindowsBackup(backupPath);
81
- }
82
- catch (error) {
83
- const replacementFailure = errorMessage(error);
84
- await rm(temporaryPath, { force: true }).catch(() => undefined);
85
- if (!originalMoved) {
86
- throw new RunXError(`Could not replace the Windows RunX executable at ${executablePath}: ${replacementFailure}`);
87
- }
88
- try {
89
- await rm(executablePath, { force: true });
90
- await rename(backupPath, executablePath);
91
- }
92
- catch (rollbackError) {
93
- throw new RunXError(`Windows RunX upgrade failed at ${executablePath}: ${replacementFailure}. Automatic rollback also failed: ${errorMessage(rollbackError)}`);
94
- }
95
- throw new RunXError(`Windows RunX upgrade failed; the previous executable was restored at ${executablePath}: ${replacementFailure}`);
236
+ const emitSkippedMutation = (events, onEvent) => {
237
+ for (const phase of ['download', 'validate', 'replace', 'verify', 'cache', 'cleanup']) {
238
+ const event = { sequence: events.length + 1, phase, status: 'skipped' };
239
+ events.push(event);
240
+ onEvent?.(event);
96
241
  }
97
242
  };
243
+ const rollbackReplacement = async (backupPath, executablePath, operations) => {
244
+ await operations.remove(executablePath);
245
+ await operations.rename(backupPath, executablePath);
246
+ };
98
247
  const verifyExecutableVersion = async (executablePath, expectedVersion) => {
99
248
  const verification = Bun.spawn([executablePath, '--version'], { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
249
+ let timeout;
250
+ const exited = Promise.race([
251
+ verification.exited,
252
+ new Promise((_, reject) => {
253
+ timeout = setTimeout(() => {
254
+ verification.kill();
255
+ reject(new Error('replacement version check timed out after 10 seconds'));
256
+ }, 10_000);
257
+ }),
258
+ ]);
100
259
  const [stdout, stderr, exitCode] = await Promise.all([
101
260
  new Response(verification.stdout).text(),
102
261
  new Response(verification.stderr).text(),
103
- verification.exited,
104
- ]);
262
+ exited,
263
+ ]).finally(() => {
264
+ if (timeout !== undefined)
265
+ clearTimeout(timeout);
266
+ });
105
267
  const actualVersion = stdout.trim();
106
268
  if (exitCode !== 0)
107
269
  throw new Error(`replacement exited with code ${exitCode}${stderr.trim() ? `: ${stderr.trim()}` : ''}`);
108
270
  if (actualVersion !== expectedVersion)
109
271
  throw new Error(`replacement reported version ${actualVersion || '<empty>'}; expected ${expectedVersion}`);
110
272
  };
111
- const cleanupWindowsBackup = async (backupPath) => {
273
+ const cleanupBackup = async (backupPath, os, operations) => {
112
274
  try {
113
- await rm(backupPath, { force: true });
275
+ await operations.remove(backupPath);
276
+ return false;
277
+ }
278
+ catch {
279
+ if (os !== 'windows')
280
+ throw new UpgradeOperationError('replace_failed', `Could not delete old RunX executable: ${backupPath}`);
281
+ scheduleWindowsBackupCleanup(backupPath);
282
+ return true;
114
283
  }
115
- catch { }
116
- scheduleWindowsBackupCleanup(backupPath);
117
284
  };
118
285
  const scheduleWindowsBackupCleanup = (backupPath) => {
119
286
  const command = 'for ($attempt = 0; $attempt -lt 300; $attempt += 1) { if (-not (Test-Path -LiteralPath $env:RUNX_BACKUP_PATH)) { exit 0 }; try { Remove-Item -LiteralPath $env:RUNX_BACKUP_PATH -Force -ErrorAction Stop; exit 0 } catch { Start-Sleep -Milliseconds 100 } }; exit 1';
120
- const encodedCommand = Buffer.from(command, 'utf16le').toString('base64');
121
- Bun.spawn(['cmd.exe', '/d', '/s', '/c', `powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${encodedCommand}`], {
287
+ Bun.spawn(['powershell.exe', '-NoLogo', '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', command], {
122
288
  detached: true,
123
289
  env: { ...process.env, RUNX_BACKUP_PATH: backupPath },
124
- stdout: 'ignore',
125
- stderr: 'ignore',
126
- stdin: 'ignore',
127
- });
290
+ stdout: 'ignore', stderr: 'ignore', stdin: 'ignore',
291
+ }).unref();
128
292
  };
129
- const errorMessage = (error) => error instanceof Error ? error.message : String(error);
130
- const compareVersions = (left, right) => {
131
- const parse = (value) => value.split(/[.+-]/).map((part) => Number.parseInt(part, 10) || 0);
132
- const a = parse(left);
133
- const b = parse(right);
134
- for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
135
- const difference = (a[index] ?? 0) - (b[index] ?? 0);
136
- if (difference !== 0)
137
- return difference;
138
- }
139
- return 0;
293
+ const validateNativeBinary = (bytes, os) => {
294
+ const native = os === 'windows'
295
+ ? bytes[0] === 0x4d && bytes[1] === 0x5a
296
+ : os === 'linux'
297
+ ? bytes[0] === 0x7f && bytes[1] === 0x45 && bytes[2] === 0x4c && bytes[3] === 0x46
298
+ : isMachO(bytes);
299
+ if (!native)
300
+ throw new RunXError(`Downloaded file is not a native ${os} executable.`);
140
301
  };
302
+ const isMachO = (bytes) => {
303
+ const magic = Array.from(bytes.slice(0, 4)).map((value) => value.toString(16).padStart(2, '0')).join('');
304
+ return ['feedface', 'feedfacf', 'cefaedfe', 'cffaedfe', 'cafebabe', 'bebafeca'].includes(magic);
305
+ };
306
+ const classifyPlanError = (error) => /malformed/.test(errorMessage(error)) ? 'release_payload_invalid' : 'release_lookup_failed';
307
+ const classifyFailure = (phase, mutationCode) => {
308
+ if (phase === 'download')
309
+ return 'download_failed';
310
+ if (phase === 'validate')
311
+ return 'download_invalid';
312
+ if (phase === 'replace')
313
+ return mutationCode ?? 'replace_failed';
314
+ if (phase === 'verify')
315
+ return 'verification_failed';
316
+ return 'release_lookup_failed';
317
+ };
318
+ const asOperationError = (code, error) => error instanceof UpgradeOperationError ? error : new UpgradeOperationError(code, errorMessage(error));
319
+ const resolveSelfExecutable = () => process.env['RUNX_SELF_PATH'] ?? process.execPath;
320
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+ export { copyTextFile, ensureDirectory, globalRunXDirectory, movePath, pathExists, readTextIfExists, removePath, writeTextFile, };
5
+ declare function globalRunXDirectory(): string;
6
+ declare function pathExists(path: string): Promise<boolean>;
7
+ declare function ensureDirectory(path: string): Promise<void>;
8
+ declare function removePath(path: string): Promise<void>;
9
+ declare function movePath(from: string, to: string): Promise<void>;
10
+ declare function readTextIfExists(path: string): Promise<string | null>;
11
+ declare function writeTextFile(path: string, value: string): Promise<void>;
12
+ declare function copyTextFile(source: string, target: string): Promise<void>;
13
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../source/storage.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,EACL,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,UAAU,EACV,aAAa,GACd,CAAA;AAED,iBAAS,mBAAmB,IAAI,MAAM,CAErC;AAED,iBAAe,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAExD;AAED,iBAAe,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1D;AAED,iBAAe,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErD;AAED,iBAAe,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/D;AAED,iBAAe,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEpE;AAED,iBAAe,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvE;AAED,iBAAe,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzE"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+ import { $ } from 'bun';
5
+ import { directoryName, homeDirectory, joinPath } from './path-utils.js';
6
+ export { copyTextFile, ensureDirectory, globalRunXDirectory, movePath, pathExists, readTextIfExists, removePath, writeTextFile, };
7
+ function globalRunXDirectory() {
8
+ return joinPath(homeDirectory(), '.guiho', 'runx');
9
+ }
10
+ async function pathExists(path) {
11
+ return Bun.file(path).exists();
12
+ }
13
+ async function ensureDirectory(path) {
14
+ await $ `mkdir -p ${path}`.quiet();
15
+ }
16
+ async function removePath(path) {
17
+ await $ `rm -rf ${path}`.quiet();
18
+ }
19
+ async function movePath(from, to) {
20
+ if (process.platform === 'win32') {
21
+ const child = Bun.spawn(['cmd.exe', '/d', '/s', '/c', 'move', '/y', from, to], { stdout: 'ignore', stderr: 'pipe' });
22
+ const [code, error] = await Promise.all([child.exited, new Response(child.stderr).text()]);
23
+ if (code !== 0)
24
+ throw new Error(error.trim() || `Could not move ${from} to ${to}.`);
25
+ return;
26
+ }
27
+ await $ `mv ${from} ${to}`.quiet();
28
+ }
29
+ async function readTextIfExists(path) {
30
+ return await pathExists(path) ? Bun.file(path).text() : null;
31
+ }
32
+ async function writeTextFile(path, value) {
33
+ await ensureDirectory(directoryName(path));
34
+ await Bun.write(path, value);
35
+ }
36
+ async function copyTextFile(source, target) {
37
+ await writeTextFile(target, await Bun.file(source).text());
38
+ }
@@ -1,31 +1,26 @@
1
- import type { Static } from '@sinclair/typebox';
2
- import type { CommandSchema, ManifestSchema } from './manifest.js';
3
- export type RunXManifest = Static<typeof ManifestSchema>;
4
- export type RunXCommand = Static<typeof CommandSchema>;
5
- export type ResolvedCommand = RunXCommand & {
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+ import type { RunXCommand, RunXManifest } from './configuration.js';
5
+ export type { AgentScope, CliOptions, OutputFormat, ResolvedCommand, RunXCommand, RunXManifest, UpdateResult, };
6
+ type ResolvedCommand = RunXCommand & {
6
7
  index: number;
7
8
  selector: string;
8
9
  manifestPath: string;
9
10
  cwd: string;
10
11
  };
11
- export type OutputFormat = 'text' | 'json';
12
- export type CliOptions = {
12
+ type OutputFormat = 'text' | 'json';
13
+ type CliOptions = {
13
14
  cwd: string;
14
- file?: string;
15
+ config?: string;
15
16
  format: OutputFormat;
16
17
  verbose: boolean;
17
18
  };
18
- export type AgentTool = 'agents' | 'claude' | 'all';
19
- export type AgentScope = 'local' | 'global';
20
- export type UpdateResult = {
19
+ type AgentScope = 'local' | 'global';
20
+ type UpdateResult = {
21
21
  currentVersion: string;
22
22
  latestVersion: string;
23
23
  updateAvailable: boolean;
24
24
  url?: string;
25
25
  };
26
- export type RunXUpgradeResult = UpdateResult & {
27
- executablePath: string;
28
- scheduled: boolean;
29
- upToDate: boolean;
30
- };
31
26
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../source/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAElE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,cAAc,CAAC,CAAA;AACxD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,aAAa,CAAC,CAAA;AAEtD,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAA;AAE1C,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAA;AAEnD,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE3C,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,OAAO,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;CAClB,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../source/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEnE,YAAY,EACV,UAAU,EACV,UAAU,EACV,YAAY,EACZ,eAAe,EACf,WAAW,EACX,YAAY,EACZ,YAAY,GACb,CAAA;AAED,KAAK,eAAe,GAAG,WAAW,GAAG;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,CAAA;AAEnC,KAAK,UAAU,GAAG;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,YAAY,CAAA;IACpB,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,KAAK,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEpC,KAAK,YAAY,GAAG;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,OAAO,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA"}
package/library/types.js CHANGED
@@ -0,0 +1,3 @@
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+ import type { Static } from '@sinclair/typebox';
5
+ export { cacheSchema, readCachedUpdateNotice, runUpdateWorker, spawnUpdateWorker, updateCachePath, };
6
+ export type { UpdateCache, };
7
+ declare const cacheSchema: import("@sinclair/typebox").TObject<{
8
+ newVersionAvailable: import("@sinclair/typebox").TBoolean;
9
+ latestVersion: import("@sinclair/typebox").TString;
10
+ upgradeCommand: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
11
+ lastCheck: import("@sinclair/typebox").TString;
12
+ }>;
13
+ type UpdateCache = Static<typeof cacheSchema>;
14
+ declare function updateCachePath(): string;
15
+ declare function readCachedUpdateNotice(verbose?: boolean): Promise<string | null>;
16
+ declare function spawnUpdateWorker(): void;
17
+ declare function runUpdateWorker(options?: {
18
+ fetchImpl?: typeof fetch;
19
+ now?: () => Date;
20
+ }): Promise<UpdateCache>;
21
+ //# sourceMappingURL=update-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-cache.d.ts","sourceRoot":"","sources":["../source/update-cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EACL,WAAW,EACX,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,eAAe,GAChB,CAAA;AACD,YAAY,EACV,WAAW,GACZ,CAAA;AAED,QAAA,MAAM,WAAW;;;;;EAKkB,CAAA;AAEnC,KAAK,WAAW,GAAG,MAAM,CAAC,OAAO,WAAW,CAAC,CAAA;AAE7C,iBAAS,eAAe,IAAI,MAAM,CAEjC;AAED,iBAAe,sBAAsB,CAAC,OAAO,UAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAY7E;AAED,iBAAS,iBAAiB,IAAI,IAAI,CAYjC;AAED,iBAAe,eAAe,CAAC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAiBjH"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+ import { Type } from '@sinclair/typebox';
5
+ import { Value } from '@sinclair/typebox/value';
6
+ import { gt, valid } from 'semver';
7
+ import { readVersion } from './help.js';
8
+ import { fetchReleaseCatalog, resolveUpgradePlatform } from './release-catalog.js';
9
+ import { globalRunXDirectory, movePath, readTextIfExists, writeTextFile } from './storage.js';
10
+ import { joinPath } from './path-utils.js';
11
+ export { cacheSchema, readCachedUpdateNotice, runUpdateWorker, spawnUpdateWorker, updateCachePath, };
12
+ const cacheSchema = Type.Object({
13
+ newVersionAvailable: Type.Boolean(),
14
+ latestVersion: Type.String(),
15
+ upgradeCommand: Type.Optional(Type.String()),
16
+ lastCheck: Type.String(),
17
+ }, { additionalProperties: false });
18
+ function updateCachePath() {
19
+ return joinPath(globalRunXDirectory(), 'cache.json');
20
+ }
21
+ async function readCachedUpdateNotice(verbose = false) {
22
+ const raw = await readTextIfExists(updateCachePath());
23
+ if (raw === null)
24
+ return null;
25
+ try {
26
+ const cache = Value.Decode(cacheSchema, JSON.parse(raw));
27
+ return cache.newVersionAvailable
28
+ ? 'New version available. Run this command to upgrade: runx upgrade'
29
+ : null;
30
+ }
31
+ catch (error) {
32
+ if (verbose)
33
+ process.stderr.write(`warning: ignored invalid RunX update cache: ${error instanceof Error ? error.message : String(error)}\n`);
34
+ return null;
35
+ }
36
+ }
37
+ function spawnUpdateWorker() {
38
+ if (Bun.env.RUNX_DISABLE_UPDATE_WORKER === '1')
39
+ return;
40
+ const command = process.execPath.toLowerCase().includes('bun')
41
+ ? [process.execPath, Bun.main, '--check-updates-worker']
42
+ : [process.execPath, '--check-updates-worker'];
43
+ Bun.spawn(command, {
44
+ detached: true,
45
+ stdout: 'ignore',
46
+ stderr: 'ignore',
47
+ stdin: 'ignore',
48
+ env: process.env,
49
+ }).unref();
50
+ }
51
+ async function runUpdateWorker(options = {}) {
52
+ const currentVersion = readVersion();
53
+ const platform = resolveUpgradePlatform();
54
+ const catalog = await fetchReleaseCatalog({ ...platform, currentVersion, fetchImpl: options.fetchImpl });
55
+ const latestVersion = catalog.latestStableVersion ?? currentVersion;
56
+ const newVersionAvailable = Boolean(valid(currentVersion) && valid(latestVersion) && gt(latestVersion, currentVersion));
57
+ const cache = {
58
+ newVersionAvailable,
59
+ latestVersion,
60
+ ...(newVersionAvailable ? { upgradeCommand: 'runx upgrade' } : {}),
61
+ lastCheck: (options.now?.() ?? new Date()).toISOString(),
62
+ };
63
+ const path = updateCachePath();
64
+ const temporary = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
65
+ await writeTextFile(temporary, `${JSON.stringify(cache, null, 2)}\n`);
66
+ await movePath(temporary, path);
67
+ return cache;
68
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+ import type { ReleaseCatalog, UpgradeEnvelope, UpgradeEvent, UpgradePlan } from './upgrade-types.js';
5
+ export { renderReleaseCatalog, renderUpgradeEvent, renderUpgradeHeading, renderUpgradePlan, renderUpgradeResult, };
6
+ declare const renderUpgradeHeading: () => string;
7
+ declare const renderUpgradePlan: (plan: UpgradePlan) => string;
8
+ declare const renderUpgradeEvent: (event: UpgradeEvent) => string;
9
+ declare const renderUpgradeResult: (result: UpgradeEnvelope) => string;
10
+ declare const renderReleaseCatalog: (catalog: ReleaseCatalog) => string;
11
+ //# sourceMappingURL=upgrade-reporting.d.ts.map