@remodex/rmx 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -10
- package/bin/ocx.mjs +73 -14
- package/gui/dist/assets/index-1SDbgh2-.css +1 -0
- package/gui/dist/assets/{index-CkETtt7P.js → index-D3MaPif3.js} +13 -13
- package/gui/dist/index.html +2 -2
- package/package.json +5 -5
- package/src/android-remote/cloudflare-tunnel.ts +30 -8
- package/src/cli/help.ts +28 -6
- package/src/cli/index.ts +47 -74
- package/src/cli/init.ts +7 -1
- package/src/cli/internal-dispatch.ts +0 -6
- package/src/cli/onboard.ts +599 -0
- package/src/cli/system-command.ts +37 -84
- package/src/codex/inject.ts +8 -1
- package/src/codex/sync.ts +3 -0
- package/src/lib/config-ownership.ts +5 -0
- package/src/lib/remodex-home.ts +5 -0
- package/src/server/management/android-remote-routes.ts +1 -1
- package/src/server/management/config-routes.ts +0 -80
- package/src/server/management/context.ts +0 -4
- package/src/tray/windows-tray.ps1 +109 -66
- package/src/tray/windows.ts +7 -1
- package/src/update/auto-scheduler.ts +1671 -0
- package/src/update/job.ts +72 -13
- package/src/update/notify.ts +2 -2
- package/gui/dist/assets/index-CZqebSPQ.css +0 -1
- package/src/update/desktop-release.ts +0 -1620
package/src/update/job.ts
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
type NpmCachePreflightReason,
|
|
44
44
|
} from "./npm-cache-preflight.mjs";
|
|
45
45
|
|
|
46
|
-
const
|
|
46
|
+
const RELEASES_URL = "https://github.com/ESCANOR-001/remodex-android/releases";
|
|
47
47
|
const UPDATE_JOB_FILENAME = "update-job.json";
|
|
48
48
|
const UPDATE_TIMEOUT_MS = 180_000;
|
|
49
49
|
const RESTART_TIMEOUT_MS = 60_000;
|
|
@@ -65,6 +65,8 @@ export interface UpdateCheckResult {
|
|
|
65
65
|
canUpdate: boolean;
|
|
66
66
|
command: string;
|
|
67
67
|
releaseNotesUrl: string;
|
|
68
|
+
/** Local time recorded after this exact runtime version was installed. */
|
|
69
|
+
lastInstalledAt?: string;
|
|
68
70
|
reason?: string;
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -81,6 +83,10 @@ export interface UpdateJobState {
|
|
|
81
83
|
command: string;
|
|
82
84
|
releaseNotesUrl: string;
|
|
83
85
|
log: string[];
|
|
86
|
+
/** Exact package version whose installer completed successfully. */
|
|
87
|
+
installedVersion?: string;
|
|
88
|
+
/** Local installation event, persisted as an ISO-8601 timestamp. */
|
|
89
|
+
installedAt?: string;
|
|
84
90
|
pid?: number;
|
|
85
91
|
error?: string;
|
|
86
92
|
exitCode?: number | null;
|
|
@@ -98,6 +104,7 @@ export interface UpdateCheckDeps {
|
|
|
98
104
|
currentVersion: () => string;
|
|
99
105
|
detectInstall: () => Installer;
|
|
100
106
|
latestVersion: (tag: Channel) => string | null;
|
|
107
|
+
readUpdateJob?: () => UpdateJobState | null;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
interface UpdateWorkerProcess {
|
|
@@ -212,7 +219,7 @@ async function waitForGhostListenClear(
|
|
|
212
219
|
return { ok: false, accessDenied };
|
|
213
220
|
}
|
|
214
221
|
|
|
215
|
-
function packageLauncherPath(): string {
|
|
222
|
+
export function packageLauncherPath(): string {
|
|
216
223
|
// This module lives at src/update/job.ts — the launcher is <pkg-root>/bin/ocx.mjs.
|
|
217
224
|
// After `npm install -g`, import.meta.url can still point at npm's renamed temp
|
|
218
225
|
// tree (`@remodex/.rmx-*` or the legacy `@bitkyc08/.opencodex-*`). Prefer the
|
|
@@ -280,6 +287,27 @@ function isVersionLike(value: unknown): value is string {
|
|
|
280
287
|
&& /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
|
|
281
288
|
}
|
|
282
289
|
|
|
290
|
+
/** Only timestamps written by this updater may cross the management API boundary. */
|
|
291
|
+
function normalizedUpdateTimestamp(value: unknown): string | null {
|
|
292
|
+
if (typeof value !== "string" || value.length > 40) return null;
|
|
293
|
+
const parsed = Date.parse(value);
|
|
294
|
+
if (!Number.isFinite(parsed)) return null;
|
|
295
|
+
const canonical = new Date(parsed).toISOString();
|
|
296
|
+
return canonical === value ? canonical : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function releaseNotesUrlForVersion(version: unknown): string {
|
|
300
|
+
return isVersionLike(version)
|
|
301
|
+
? `${RELEASES_URL}/tag/v${version}`
|
|
302
|
+
: RELEASES_URL;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isTrustedReleaseNotesUrl(value: unknown): value is string {
|
|
306
|
+
if (value === RELEASES_URL) return true;
|
|
307
|
+
if (typeof value !== "string" || !value.startsWith(`${RELEASES_URL}/tag/v`)) return false;
|
|
308
|
+
return releaseNotesUrlForVersion(value.slice(`${RELEASES_URL}/tag/v`.length)) === value;
|
|
309
|
+
}
|
|
310
|
+
|
|
283
311
|
function withheldSummary(error: unknown): string {
|
|
284
312
|
// `error.name` is writable, so it is external text like the message. A fixed classification
|
|
285
313
|
// is the only part of an unknown error we can state without repeating something we were
|
|
@@ -319,7 +347,7 @@ function withheldSummary(error: unknown): string {
|
|
|
319
347
|
*/
|
|
320
348
|
function brandOwnComposedText(key: string, value: unknown): unknown {
|
|
321
349
|
if (key === "releaseNotesUrl") {
|
|
322
|
-
return value
|
|
350
|
+
return isTrustedReleaseNotesUrl(value) ? value : "";
|
|
323
351
|
}
|
|
324
352
|
if (key === "command") {
|
|
325
353
|
// Render the command shape first, then apply the same path test as every other field. The
|
|
@@ -374,8 +402,8 @@ function renderSafeCommand(value: string): string {
|
|
|
374
402
|
part === "$" || part === "<path>"
|
|
375
403
|
|| /^(?:npm|bun|pnpm|yarn|node)$/.test(part)
|
|
376
404
|
|| /^-{1,2}[\w-]+$/.test(part)
|
|
377
|
-
|| /^(?:install|add|update|i)$/.test(part)
|
|
378
|
-
|| /^opencodex(?:@[\w.\-]+)?$/.test(part)
|
|
405
|
+
|| /^(?:install|add|update|i|__exact-update)$/.test(part)
|
|
406
|
+
|| /^(?:@remodex\/rmx|opencodex)(?:@[\w.\-]+)?$/.test(part)
|
|
379
407
|
|| /^(?:latest|preview|next|beta)$/.test(part)
|
|
380
408
|
|| /^\d[\w.\-]*$/.test(part));
|
|
381
409
|
if (allowed) return rendered.join(" ");
|
|
@@ -412,6 +440,11 @@ export function readUpdateJob(jobId?: string | null): UpdateJobState | null {
|
|
|
412
440
|
const parsed = JSON.parse(readFileSync(updateJobPath(), "utf8")) as UpdateJobState;
|
|
413
441
|
if (jobId && parsed.id !== jobId) return null;
|
|
414
442
|
if (!parsed || typeof parsed.id !== "string" || typeof parsed.status !== "string") return null;
|
|
443
|
+
if (parsed.installedAt !== undefined) {
|
|
444
|
+
const installedAt = normalizedUpdateTimestamp(parsed.installedAt);
|
|
445
|
+
if (installedAt) parsed.installedAt = installedAt;
|
|
446
|
+
else delete parsed.installedAt;
|
|
447
|
+
}
|
|
415
448
|
return parsed;
|
|
416
449
|
} catch {
|
|
417
450
|
return null;
|
|
@@ -445,9 +478,12 @@ export function updateExecutionCommand(
|
|
|
445
478
|
): { bin: string; args: string[]; display: string } {
|
|
446
479
|
if (installer === "npm") {
|
|
447
480
|
const bin = nodeBin();
|
|
448
|
-
const args =
|
|
449
|
-
|
|
450
|
-
|
|
481
|
+
const args = resolvedVersion
|
|
482
|
+
? [launcher, "__exact-update", resolvedVersion]
|
|
483
|
+
: [launcher, "update", "--tag", channel];
|
|
484
|
+
// A scheduled update passes a registry-resolved version through the hidden exact
|
|
485
|
+
// launcher path. Interactive/dashboard updates without a resolved target retain
|
|
486
|
+
// the historical tag-based launcher behavior.
|
|
451
487
|
return { bin, args, display: formatCommand(bin, args) };
|
|
452
488
|
}
|
|
453
489
|
if (installer === "bun") {
|
|
@@ -508,6 +544,12 @@ export function checkForUpdate(
|
|
|
508
544
|
reason = "already_latest";
|
|
509
545
|
}
|
|
510
546
|
|
|
547
|
+
const previousJob = (deps.readUpdateJob ?? readUpdateJob)();
|
|
548
|
+
const installedVersion = previousJob?.installedVersion ?? previousJob?.latestVersion;
|
|
549
|
+
const lastInstalledAt = installedVersion === current
|
|
550
|
+
? normalizedUpdateTimestamp(previousJob?.installedAt)
|
|
551
|
+
: null;
|
|
552
|
+
|
|
511
553
|
return {
|
|
512
554
|
currentVersion: current,
|
|
513
555
|
latestVersion: latest,
|
|
@@ -516,7 +558,8 @@ export function checkForUpdate(
|
|
|
516
558
|
updateAvailable,
|
|
517
559
|
canUpdate: installer !== "source" && updateAvailable,
|
|
518
560
|
command,
|
|
519
|
-
releaseNotesUrl:
|
|
561
|
+
releaseNotesUrl: releaseNotesUrlForVersion(latest ?? current),
|
|
562
|
+
...(lastInstalledAt ? { lastInstalledAt } : {}),
|
|
520
563
|
...(reason ? { reason } : {}),
|
|
521
564
|
};
|
|
522
565
|
}
|
|
@@ -1742,6 +1785,14 @@ export interface GuiUpdateWorkerIo {
|
|
|
1742
1785
|
checkForUpdateFn?: (channel: Channel) => ReturnType<typeof checkForUpdate>;
|
|
1743
1786
|
/** Bypass the registry integrity probe, which runs before the cache gate and needs network. */
|
|
1744
1787
|
integrityFn?: (version: string | null) => ReturnType<typeof checkUpdatePackageIntegrity>;
|
|
1788
|
+
/** Clock seam for deterministic install-event records in tests. */
|
|
1789
|
+
now?: () => string;
|
|
1790
|
+
/**
|
|
1791
|
+
* Immutable target selected by a caller that already resolved a registry version.
|
|
1792
|
+
* npm uses the hidden Node-launcher path so the package manager cannot re-resolve a
|
|
1793
|
+
* moving dist-tag between the check and install.
|
|
1794
|
+
*/
|
|
1795
|
+
exactVersion?: string;
|
|
1745
1796
|
runCommandFn?: (
|
|
1746
1797
|
job: UpdateJobState,
|
|
1747
1798
|
bin: string,
|
|
@@ -1758,7 +1809,7 @@ export async function runGuiUpdateWorker(
|
|
|
1758
1809
|
): Promise<void> {
|
|
1759
1810
|
let job = readUpdateJob(jobId);
|
|
1760
1811
|
const check = (io.checkForUpdateFn ?? checkForUpdate)(channel);
|
|
1761
|
-
const now = new Date().toISOString();
|
|
1812
|
+
const now = io.now ?? (() => new Date().toISOString());
|
|
1762
1813
|
// Capture the live listen target BEFORE the update command runs: the stop-first update
|
|
1763
1814
|
// flow clears pid/runtime state, so this is the last moment the real port is knowable.
|
|
1764
1815
|
// Only trust runtime-port.json when its pid matches the live pidfile process.
|
|
@@ -1780,8 +1831,8 @@ export async function runGuiUpdateWorker(
|
|
|
1780
1831
|
job = {
|
|
1781
1832
|
id: jobId,
|
|
1782
1833
|
status: "running",
|
|
1783
|
-
startedAt: now,
|
|
1784
|
-
updatedAt: now,
|
|
1834
|
+
startedAt: now(),
|
|
1835
|
+
updatedAt: now(),
|
|
1785
1836
|
currentVersion: check.currentVersion,
|
|
1786
1837
|
latestVersion: check.latestVersion,
|
|
1787
1838
|
channel: check.channel,
|
|
@@ -1811,7 +1862,9 @@ export async function runGuiUpdateWorker(
|
|
|
1811
1862
|
? `Integrity pre-flight skipped: ${integrity.reason}. Proceeding best-effort.`
|
|
1812
1863
|
: `Verified ${PKG}@${check.latestVersion} integrity metadata ${integrity.integrity.slice(0, 24)}…`;
|
|
1813
1864
|
|
|
1814
|
-
const cmd =
|
|
1865
|
+
const cmd = io.exactVersion === undefined
|
|
1866
|
+
? updateExecutionCommand(check.installer, channel, undefined, check.latestVersion)
|
|
1867
|
+
: updateExecutionCommand(check.installer, channel, undefined, io.exactVersion);
|
|
1815
1868
|
job = updateJob(job, {
|
|
1816
1869
|
currentVersion: check.currentVersion,
|
|
1817
1870
|
latestVersion: check.latestVersion,
|
|
@@ -1874,6 +1927,12 @@ export async function runGuiUpdateWorker(
|
|
|
1874
1927
|
return;
|
|
1875
1928
|
}
|
|
1876
1929
|
|
|
1930
|
+
const installedVersion = io.exactVersion ?? check.latestVersion;
|
|
1931
|
+
job = updateJob(job, {
|
|
1932
|
+
...(installedVersion ? { installedVersion } : {}),
|
|
1933
|
+
installedAt: now(),
|
|
1934
|
+
}, `Package ${installedVersion ?? "update"} installed successfully.`);
|
|
1935
|
+
|
|
1877
1936
|
if (trayWasInstalled) {
|
|
1878
1937
|
const trayArgs = [process.argv[1], ...planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs];
|
|
1879
1938
|
const tray = runLoggedCommand(job, process.execPath, trayArgs, 20_000);
|
package/src/update/notify.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
|
|
17
17
|
const VERSION_FILENAME = "version.json";
|
|
18
18
|
const REFRESH_INTERVAL_MS = 20 * 60 * 60 * 1000; // 20h, matching codex-rs
|
|
19
|
-
const
|
|
19
|
+
const RELEASES_URL = "https://github.com/ESCANOR-001/remodex-android/releases";
|
|
20
20
|
|
|
21
21
|
export interface VersionCache {
|
|
22
22
|
latest_version: string;
|
|
@@ -210,7 +210,7 @@ function renderPrompt(current: string, latest: string, channel: Channel): string
|
|
|
210
210
|
"",
|
|
211
211
|
` \x1b[38;5;141m✨ Update available!\x1b[0m \x1b[2m${current} -> ${latest}\x1b[0m`,
|
|
212
212
|
"",
|
|
213
|
-
` \x1b[2mRelease notes:\x1b[0m ${
|
|
213
|
+
` \x1b[2mRelease notes:\x1b[0m ${RELEASES_URL}/tag/v${latest}`,
|
|
214
214
|
"",
|
|
215
215
|
` 1) Update now (runs \`${command}\`)`,
|
|
216
216
|
" 2) Skip",
|