@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.
- package/CHANGELOG.md +27 -0
- package/DOCS.md +127 -128
- package/README.md +56 -75
- package/devops/build-binaries.ts +44 -62
- package/devops/devops.xdocs.md +8 -5
- package/devops/install.ps1 +182 -152
- package/devops/install.sh +197 -262
- package/devops/installers.spec.ts +31 -0
- package/devops/verify-release-assets.ts +17 -0
- package/docs/docs.xdocs.md +3 -1
- package/docs/plans/plans.xdocs.md +7 -0
- package/docs/plans/rfc-0034-cli-compliance-migration.md +586 -0
- package/docs/plans/upgrade-reliability-implementation.md +95 -0
- package/docs/reviews/implementation/implementation.xdocs.md +2 -0
- package/docs/reviews/implementation/rfc-0034-cli-compliance-migration-review.md +59 -0
- package/docs/reviews/plans/plans.xdocs.md +6 -0
- package/docs/reviews/plans/rfc-0034-cli-compliance-migration-review.md +80 -0
- package/docs/reviews/plans/upgrade-reliability-implementation-review.md +65 -0
- package/docs/superpowers/specs/2026-07-15-upgrade-reliability-design.md +662 -0
- package/docs/superpowers/specs/specs.xdocs.md +24 -0
- package/docs/superpowers/superpowers.xdocs.md +21 -0
- package/docs/todo/rfc-0034-cli-compliance-migration-implementation.md +58 -0
- package/docs/todo/rfc-0034-cli-compliance-migration.md +151 -0
- package/docs/todo/todo.xdocs.md +6 -2
- package/docs/validation/rfc-0034-cli-compliance-migration.md +67 -0
- package/docs/validation/upgrade-reliability.md +124 -0
- package/docs/validation/validation.xdocs.md +5 -0
- package/library/agents.d.ts +28 -4
- package/library/agents.d.ts.map +1 -1
- package/library/agents.js +143 -41
- package/library/cli.d.ts.map +1 -1
- package/library/cli.js +296 -334
- package/library/configuration.d.ts +57 -0
- package/library/configuration.d.ts.map +1 -0
- package/library/configuration.js +111 -0
- package/library/embedded-resources.d.ts +6 -1
- package/library/embedded-resources.d.ts.map +1 -1
- package/library/embedded-resources.js +10 -4
- package/library/executor.d.ts +5 -1
- package/library/executor.d.ts.map +1 -1
- package/library/executor.js +10 -10
- package/library/help.d.ts +8 -4
- package/library/help.d.ts.map +1 -1
- package/library/help.js +70 -46
- package/library/init.d.ts +8 -27
- package/library/init.d.ts.map +1 -1
- package/library/init.js +37 -159
- package/library/manifest.d.ts +4 -43
- package/library/manifest.d.ts.map +1 -1
- package/library/manifest.js +4 -125
- package/library/path-utils.d.ts +13 -0
- package/library/path-utils.d.ts.map +1 -0
- package/library/path-utils.js +82 -0
- package/library/recovery.d.ts +7 -0
- package/library/recovery.d.ts.map +1 -0
- package/library/recovery.js +23 -0
- package/library/release-catalog.d.ts +32 -0
- package/library/release-catalog.d.ts.map +1 -0
- package/library/release-catalog.js +124 -0
- package/library/self-management.d.ts +24 -4
- package/library/self-management.d.ts.map +1 -1
- package/library/self-management.js +279 -99
- package/library/storage.d.ts +13 -0
- package/library/storage.d.ts.map +1 -0
- package/library/storage.js +38 -0
- package/library/types.d.ts +11 -16
- package/library/types.d.ts.map +1 -1
- package/library/types.js +3 -0
- package/library/update-cache.d.ts +21 -0
- package/library/update-cache.d.ts.map +1 -0
- package/library/update-cache.js +68 -0
- package/library/upgrade-reporting.d.ts +11 -0
- package/library/upgrade-reporting.d.ts.map +1 -0
- package/library/upgrade-reporting.js +67 -0
- package/library/upgrade-types.d.ts +72 -0
- package/library/upgrade-types.d.ts.map +1 -0
- package/library/upgrade-types.js +3 -0
- package/package.json +6 -3
- package/scripts/runx-bin.mjs +49 -0
- package/scripts/runx-bin.spec.ts +62 -0
- package/scripts/scripts.xdocs.md +3 -3
- package/skills/guiho-s-runx/SKILL.md +64 -59
- package/skills/guiho-s-runx/guiho-s-runx.xdocs.md +7 -4
- package/skills/skills.xdocs.md +1 -1
- package/docs/todo/implement-runx-alpha.md +0 -36
- 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
|
-
|
|
2
|
-
|
|
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<
|
|
5
|
-
declare const upgradeSelf: (
|
|
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":"
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
25
|
-
|
|
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 (
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
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
|
|
218
|
+
await defaultFileOperations.remove(executablePath);
|
|
61
219
|
return { executablePath, scheduled: false, dryRun: false };
|
|
62
220
|
};
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
273
|
+
const cleanupBackup = async (backupPath, os, operations) => {
|
|
112
274
|
try {
|
|
113
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
stdin: 'ignore',
|
|
127
|
-
});
|
|
290
|
+
stdout: 'ignore', stderr: 'ignore', stdin: 'ignore',
|
|
291
|
+
}).unref();
|
|
128
292
|
};
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|