@guiho/runx 0.2.7 → 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 (86) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/DOCS.md +127 -128
  3. package/README.md +56 -75
  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/docs.xdocs.md +3 -1
  11. package/docs/plans/plans.xdocs.md +7 -0
  12. package/docs/plans/rfc-0034-cli-compliance-migration.md +586 -0
  13. package/docs/plans/upgrade-reliability-implementation.md +95 -0
  14. package/docs/reviews/implementation/implementation.xdocs.md +2 -0
  15. package/docs/reviews/implementation/rfc-0034-cli-compliance-migration-review.md +59 -0
  16. package/docs/reviews/plans/plans.xdocs.md +6 -0
  17. package/docs/reviews/plans/rfc-0034-cli-compliance-migration-review.md +80 -0
  18. package/docs/reviews/plans/upgrade-reliability-implementation-review.md +65 -0
  19. package/docs/superpowers/specs/2026-07-15-upgrade-reliability-design.md +662 -0
  20. package/docs/superpowers/specs/specs.xdocs.md +24 -0
  21. package/docs/superpowers/superpowers.xdocs.md +21 -0
  22. package/docs/todo/rfc-0034-cli-compliance-migration-implementation.md +58 -0
  23. package/docs/todo/rfc-0034-cli-compliance-migration.md +151 -0
  24. package/docs/todo/todo.xdocs.md +6 -2
  25. package/docs/validation/rfc-0034-cli-compliance-migration.md +67 -0
  26. package/docs/validation/upgrade-reliability.md +124 -0
  27. package/docs/validation/validation.xdocs.md +5 -0
  28. package/library/agents.d.ts +28 -4
  29. package/library/agents.d.ts.map +1 -1
  30. package/library/agents.js +143 -41
  31. package/library/cli.d.ts.map +1 -1
  32. package/library/cli.js +296 -334
  33. package/library/configuration.d.ts +57 -0
  34. package/library/configuration.d.ts.map +1 -0
  35. package/library/configuration.js +111 -0
  36. package/library/embedded-resources.d.ts +6 -1
  37. package/library/embedded-resources.d.ts.map +1 -1
  38. package/library/embedded-resources.js +10 -4
  39. package/library/executor.d.ts +5 -1
  40. package/library/executor.d.ts.map +1 -1
  41. package/library/executor.js +10 -10
  42. package/library/help.d.ts +8 -4
  43. package/library/help.d.ts.map +1 -1
  44. package/library/help.js +70 -46
  45. package/library/init.d.ts +8 -27
  46. package/library/init.d.ts.map +1 -1
  47. package/library/init.js +37 -159
  48. package/library/manifest.d.ts +4 -43
  49. package/library/manifest.d.ts.map +1 -1
  50. package/library/manifest.js +4 -125
  51. package/library/path-utils.d.ts +13 -0
  52. package/library/path-utils.d.ts.map +1 -0
  53. package/library/path-utils.js +82 -0
  54. package/library/recovery.d.ts +7 -0
  55. package/library/recovery.d.ts.map +1 -0
  56. package/library/recovery.js +23 -0
  57. package/library/release-catalog.d.ts +32 -0
  58. package/library/release-catalog.d.ts.map +1 -0
  59. package/library/release-catalog.js +124 -0
  60. package/library/self-management.d.ts +24 -4
  61. package/library/self-management.d.ts.map +1 -1
  62. package/library/self-management.js +279 -99
  63. package/library/storage.d.ts +13 -0
  64. package/library/storage.d.ts.map +1 -0
  65. package/library/storage.js +38 -0
  66. package/library/types.d.ts +11 -16
  67. package/library/types.d.ts.map +1 -1
  68. package/library/types.js +3 -0
  69. package/library/update-cache.d.ts +21 -0
  70. package/library/update-cache.d.ts.map +1 -0
  71. package/library/update-cache.js +68 -0
  72. package/library/upgrade-reporting.d.ts +11 -0
  73. package/library/upgrade-reporting.d.ts.map +1 -0
  74. package/library/upgrade-reporting.js +67 -0
  75. package/library/upgrade-types.d.ts +72 -0
  76. package/library/upgrade-types.d.ts.map +1 -0
  77. package/library/upgrade-types.js +3 -0
  78. package/package.json +6 -3
  79. package/scripts/runx-bin.mjs +49 -0
  80. package/scripts/runx-bin.spec.ts +62 -0
  81. package/scripts/scripts.xdocs.md +3 -3
  82. package/skills/guiho-s-runx/SKILL.md +64 -59
  83. package/skills/guiho-s-runx/guiho-s-runx.xdocs.md +7 -4
  84. package/skills/skills.xdocs.md +1 -1
  85. package/docs/todo/implement-runx-alpha.md +0 -36
  86. package/scripts/runx-bin.ts +0 -24
@@ -0,0 +1,124 @@
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 { compare, parse, valid } from 'semver';
7
+ import { RunXError } from './errors.js';
8
+ export { assetCandidates, fetchReleaseCatalog, findCompatibleAsset, normalizeReleaseVersion, resolveUpgradePlatform, };
9
+ const githubReleaseSchema = Type.Object({
10
+ tag_name: Type.String(),
11
+ draft: Type.Optional(Type.Boolean()),
12
+ prerelease: Type.Optional(Type.Boolean()),
13
+ published_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
14
+ created_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
15
+ assets: Type.Array(Type.Object({
16
+ name: Type.String(),
17
+ browser_download_url: Type.String(),
18
+ }, { additionalProperties: true })),
19
+ }, { additionalProperties: true });
20
+ const githubReleasesSchema = Type.Array(githubReleaseSchema);
21
+ const repositoryApi = 'https://api.github.com/repos/CGuiho/runx/releases';
22
+ const fetchReleaseCatalog = async (options) => {
23
+ const fetchImpl = options.fetchImpl ?? fetch;
24
+ const releases = [];
25
+ let page = 1;
26
+ let next = true;
27
+ while (next) {
28
+ const response = await fetchImpl(`${repositoryApi}?per_page=100&page=${page}`, {
29
+ headers: { accept: 'application/vnd.github+json' },
30
+ });
31
+ if (!response.ok)
32
+ throw new RunXError(`Could not retrieve RunX releases page ${page}: HTTP ${response.status}`, 4);
33
+ const payload = await response.json();
34
+ let pageReleases;
35
+ try {
36
+ pageReleases = Value.Decode(githubReleasesSchema, payload);
37
+ }
38
+ catch {
39
+ throw new RunXError(`Could not retrieve RunX releases page ${page}: malformed release`, 4);
40
+ }
41
+ releases.push(...pageReleases.filter((release) => !release.draft));
42
+ next = hasNextPage(response.headers.get('link'));
43
+ page += 1;
44
+ }
45
+ const normalized = releases.map((release) => normalizeRelease(release, options));
46
+ normalized.sort(compareCatalogEntries);
47
+ const latestStableVersion = normalized.find((entry) => valid(entry.version) && !entry.prerelease)?.version ?? null;
48
+ for (const entry of normalized)
49
+ entry.latestStable = entry.version === latestStableVersion;
50
+ return {
51
+ schemaVersion: 1,
52
+ command: 'runx upgrade list',
53
+ currentVersion: options.currentVersion,
54
+ latestStableVersion,
55
+ releases: normalized,
56
+ };
57
+ };
58
+ const normalizeRelease = (release, platform) => {
59
+ const version = normalizeReleaseVersion(release.tag_name);
60
+ const parsed = parse(version);
61
+ const prerelease = parsed ? parsed.prerelease.length > 0 : Boolean(release.prerelease);
62
+ return {
63
+ tag: release.tag_name,
64
+ version,
65
+ channel: releaseChannel(version, prerelease),
66
+ prerelease,
67
+ publishedAt: release.published_at ?? release.created_at ?? null,
68
+ current: version === platform.currentVersion,
69
+ latestStable: false,
70
+ compatibleAsset: findCompatibleAsset(release, platform),
71
+ };
72
+ };
73
+ const findCompatibleAsset = (release, platform) => {
74
+ for (const candidate of assetCandidates(platform)) {
75
+ const asset = release.assets.find((item) => item.name === candidate);
76
+ if (asset)
77
+ return { name: asset.name, url: asset.browser_download_url };
78
+ }
79
+ return null;
80
+ };
81
+ const assetCandidates = ({ os, arch, variant }) => {
82
+ const suffix = os === 'windows' ? '.exe' : '';
83
+ if (arch === 'arm64')
84
+ return [`runx-${os}-arm64${suffix}`];
85
+ const prefix = `runx-${os}-x64`;
86
+ const names = {
87
+ baseline: [`${prefix}-baseline${suffix}`, `${prefix}${suffix}`, `${prefix}-modern${suffix}`],
88
+ default: [`${prefix}${suffix}`, `${prefix}-baseline${suffix}`, `${prefix}-modern${suffix}`],
89
+ modern: [`${prefix}-modern${suffix}`, `${prefix}${suffix}`, `${prefix}-baseline${suffix}`],
90
+ };
91
+ return names[variant];
92
+ };
93
+ const resolveUpgradePlatform = (platform = process.platform, architecture = process.arch) => {
94
+ const os = platform === 'win32' ? 'windows' : platform === 'darwin' ? 'darwin' : platform === 'linux' ? 'linux' : null;
95
+ const arch = architecture === 'x64' ? 'x64' : architecture === 'arm64' ? 'arm64' : null;
96
+ if (!os)
97
+ throw new RunXError(`Self-upgrade is not supported on ${platform}.`);
98
+ if (!arch)
99
+ throw new RunXError(`Self-upgrade is not supported on architecture ${architecture}.`);
100
+ return { os, arch, variant: 'baseline' };
101
+ };
102
+ const normalizeReleaseVersion = (tag) => tag.replace(/^@guiho\/runx@/, '').replace(/^v/, '');
103
+ const releaseChannel = (version, prerelease) => {
104
+ const parsed = parse(version);
105
+ if (!parsed)
106
+ return 'other';
107
+ if (!prerelease)
108
+ return 'stable';
109
+ const identifier = parsed.prerelease[0];
110
+ return typeof identifier === 'string' ? identifier : 'prerelease';
111
+ };
112
+ const compareCatalogEntries = (left, right) => {
113
+ const leftValid = valid(left.version);
114
+ const rightValid = valid(right.version);
115
+ if (leftValid && rightValid)
116
+ return compare(rightValid, leftValid);
117
+ if (leftValid)
118
+ return -1;
119
+ if (rightValid)
120
+ return 1;
121
+ return timestamp(right.publishedAt) - timestamp(left.publishedAt);
122
+ };
123
+ const timestamp = (value) => value ? Date.parse(value) || 0 : 0;
124
+ const hasNextPage = (link) => Boolean(link?.split(',').some((part) => /rel="next"/.test(part)));
@@ -1,11 +1,31 @@
1
- import type { RunXUpgradeResult, UpdateResult } from './types.js';
2
- export { checkForLatestVersion, listAvailableVersions, uninstallSelf, upgradeSelf, };
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
+ import type { UpdateResult } from './types.js';
6
+ export { checkForLatestVersion, listAvailableVersions, uninstallSelf, upgradeSelf, validateNativeBinary };
7
+ export type UpgradeFileOperations = {
8
+ rename: (from: string, to: string) => Promise<void>;
9
+ remove: (path: string) => Promise<void>;
10
+ makeExecutable: (path: string) => Promise<void>;
11
+ };
12
+ export type UpgradeSelfOptions = {
13
+ dryRun: boolean;
14
+ currentVersion?: string;
15
+ requestedVersion?: string;
16
+ arch?: UpgradePlan['arch'];
17
+ variant?: import('./upgrade-types.js').UpgradeVariant;
18
+ onPlan?: (plan: UpgradePlan) => void;
19
+ onEvent?: (event: UpgradeEvent) => void;
20
+ fileOperations?: UpgradeFileOperations;
21
+ };
3
22
  declare const checkForLatestVersion: () => Promise<UpdateResult>;
4
- declare const listAvailableVersions: () => Promise<string[]>;
5
- declare const upgradeSelf: (dryRun: boolean) => Promise<RunXUpgradeResult>;
23
+ declare const listAvailableVersions: () => Promise<ReleaseCatalog>;
24
+ declare const upgradeSelf: (input: boolean | UpgradeSelfOptions) => Promise<UpgradeEnvelope>;
6
25
  declare const uninstallSelf: (dryRun: boolean) => Promise<{
7
26
  executablePath: string;
8
27
  scheduled: boolean;
9
28
  dryRun: boolean;
10
29
  }>;
30
+ declare const validateNativeBinary: (bytes: Uint8Array, os: UpgradePlan['os']) => void;
11
31
  //# sourceMappingURL=self-management.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"self-management.d.ts","sourceRoot":"","sources":["../source/self-management.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEjE,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,aAAa,EACb,WAAW,GACZ,CAAA;AAMD,QAAA,MAAM,qBAAqB,QAAa,OAAO,CAAC,YAAY,CAY3D,CAAA;AAED,QAAA,MAAM,qBAAqB,QAAa,OAAO,CAAC,MAAM,EAAE,CAKvD,CAAA;AAED,QAAA,MAAM,WAAW,WAAkB,OAAO,KAAG,OAAO,CAAC,iBAAiB,CAmBrE,CAAA;AAED,QAAA,MAAM,aAAa,WAAkB,OAAO,KAAG,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAWrH,CAAA"}
1
+ {"version":3,"file":"self-management.d.ts","sourceRoot":"","sources":["../source/self-management.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,OAAO,KAAK,EAAE,cAAc,EAA6C,eAAe,EAAkC,YAAY,EAAgB,WAAW,EAAiB,MAAM,oBAAoB,CAAA;AAC5M,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,aAAa,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAEzG,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC1B,OAAO,CAAC,EAAE,OAAO,oBAAoB,EAAE,cAAc,CAAA;IACrD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAA;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IACvC,cAAc,CAAC,EAAE,qBAAqB,CAAA;CACvC,CAAA;AAuCD,QAAA,MAAM,qBAAqB,QAAa,OAAO,CAAC,YAAY,CAS3D,CAAA;AAED,QAAA,MAAM,qBAAqB,QAAa,OAAO,CAAC,cAAc,CAG7D,CAAA;AAED,QAAA,MAAM,WAAW,UAAiB,OAAO,GAAG,kBAAkB,KAAG,OAAO,CAAC,eAAe,CA4IvF,CAAA;AAED,QAAA,MAAM,aAAa,WAAkB,OAAO,KAAG,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAWrH,CAAA;AAkFD,QAAA,MAAM,oBAAoB,UAAW,UAAU,MAAM,WAAW,CAAC,IAAI,CAAC,KAAG,IAOxE,CAAA"}
@@ -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
+ }