@remodex/rmx 1.0.5 → 1.0.6
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/gui/dist/assets/{index-B-jlbgno.js → index-BOZwc-1w.js} +13 -13
- package/gui/dist/assets/{index-1SDbgh2-.css → index-s2Il3gAr.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/cli/index.ts +0 -2
- package/src/server/management/config-routes.ts +9 -1
- package/src/service.ts +0 -6
- package/src/update/notify.ts +2 -5
- package/src/update/release-notes.ts +111 -0
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BOZwc-1w.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-s2Il3gAr.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -53,7 +53,6 @@ import { installShellHook, uninstallShellHook } from "../server/system-env";
|
|
|
53
53
|
import { startTokenGuardian } from "../oauth/token-guardian";
|
|
54
54
|
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
|
|
55
55
|
import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
|
|
56
|
-
import { maybeShowStarPrompt } from "./star-prompt";
|
|
57
56
|
import { scheduleCatalogPrewarm } from "./catalog-prewarm";
|
|
58
57
|
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
59
58
|
import { syncModelsToCodex } from "../codex/sync";
|
|
@@ -399,7 +398,6 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
399
398
|
// Auto-install .zshrc hook (idempotent — skips if already present).
|
|
400
399
|
installShellHook();
|
|
401
400
|
|
|
402
|
-
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
403
401
|
// Post-startup sync drives the readiness gate AND the #1046 stale app-server
|
|
404
402
|
// warning. `syncCodexOnStartIfEnabled` respects the Codex integration toggle
|
|
405
403
|
// (OFF → no sync) and reports whether anything was written; the readiness gate
|
|
@@ -343,11 +343,19 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
343
343
|
|
|
344
344
|
if (url.pathname === "/api/update/check" && req.method === "GET") {
|
|
345
345
|
const { checkForUpdate, normalizeUpdateChannel } = await import("../../update/job");
|
|
346
|
+
const { fetchReleaseNotesForVersion } = await import("../../update/release-notes");
|
|
346
347
|
const rawTag = url.searchParams.get("tag");
|
|
347
348
|
if (rawTag && rawTag !== "latest" && rawTag !== "preview") {
|
|
348
349
|
return jsonResponse({ error: "tag must be latest or preview" }, 400);
|
|
349
350
|
}
|
|
350
|
-
|
|
351
|
+
const check = checkForUpdate(normalizeUpdateChannel(rawTag));
|
|
352
|
+
const releaseNotesVersion = check.latestVersion ?? check.currentVersion;
|
|
353
|
+
const releaseNotes = await fetchReleaseNotesForVersion(releaseNotesVersion);
|
|
354
|
+
return jsonResponse({
|
|
355
|
+
...check,
|
|
356
|
+
releaseNotesVersion,
|
|
357
|
+
...(releaseNotes ? { releaseNotes } : {}),
|
|
358
|
+
});
|
|
351
359
|
}
|
|
352
360
|
|
|
353
361
|
if (url.pathname === "/api/update/run" && req.method === "POST") {
|
package/src/service.ts
CHANGED
|
@@ -53,7 +53,6 @@ import { defaultWinswEntry, installWinswService, startWinswService, stopWinswSer
|
|
|
53
53
|
import { hardenSecretDir, hardenSecretPath } from "./lib/windows-secret-acl";
|
|
54
54
|
import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
|
|
55
55
|
import { rebaseConfigOwnershipRoot, recordOwnedConfigPath } from "./lib/config-ownership";
|
|
56
|
-
import { maybeShowStarPrompt } from "./cli/star-prompt";
|
|
57
56
|
import {
|
|
58
57
|
type DefaultRemodexHomeResolution,
|
|
59
58
|
LEGACY_REMODEX_HOME_DIRNAME,
|
|
@@ -2916,11 +2915,6 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
2916
2915
|
// installed artifact instead.
|
|
2917
2916
|
await reportServiceServing("installed", { port: resolveServiceListenPort() });
|
|
2918
2917
|
if (process.platform === "linux") console.log(" For auto-start on boot: loginctl enable-linger $USER");
|
|
2919
|
-
// Service users never reach the `rmx start` prompt: the proxy they run is the
|
|
2920
|
-
// supervised child, which always carries OCX_SERVICE=1. This command, though, is
|
|
2921
|
-
// hand-typed in a real terminal, so it is the one interactive moment they get.
|
|
2922
|
-
// Same one-time marker and same guards (TTY, gh auth, agent deferral) apply.
|
|
2923
|
-
await maybeShowStarPrompt();
|
|
2924
2918
|
break;
|
|
2925
2919
|
case "start":
|
|
2926
2920
|
prepareServiceProviderEnvironment();
|
package/src/update/notify.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
6
|
-
import { hasStarPromptRun } from "../cli/star-prompt";
|
|
7
6
|
import {
|
|
8
7
|
type Channel,
|
|
9
8
|
currentVersion,
|
|
@@ -120,7 +119,7 @@ export function isSourceBuildVersion(v: string): boolean {
|
|
|
120
119
|
return v.trim() === "0.0.0";
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
/** The interactive/TTY + install-method gate
|
|
122
|
+
/** The interactive/TTY + install-method gate for update notifications. */
|
|
124
123
|
function interactiveGuardOk(): boolean {
|
|
125
124
|
return !(process.env.OCX_SERVICE || !process.stdin.isTTY || !process.stdout.isTTY);
|
|
126
125
|
}
|
|
@@ -128,15 +127,13 @@ function interactiveGuardOk(): boolean {
|
|
|
128
127
|
/**
|
|
129
128
|
* Decide whether this run should even consider showing the prompt. Returns the
|
|
130
129
|
* channel + current version when eligible, else null. Eligibility requires a
|
|
131
|
-
* real global install, a non-source version, the interactive guard
|
|
132
|
-
* the one-time star prompt has already run (first-run yield, O1).
|
|
130
|
+
* real global install, a non-source version, and the interactive guard.
|
|
133
131
|
*/
|
|
134
132
|
export function shouldConsider(): { channel: Channel; current: string } | null {
|
|
135
133
|
if (detectInstall() === "source") return null;
|
|
136
134
|
const current = currentVersion();
|
|
137
135
|
if (current === "?" || isSourceBuildVersion(current)) return null;
|
|
138
136
|
if (!interactiveGuardOk()) return null;
|
|
139
|
-
if (!hasStarPromptRun()) return null; // yield on the very first run
|
|
140
137
|
return { channel: updateTag(current), current };
|
|
141
138
|
}
|
|
142
139
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
2
|
+
|
|
3
|
+
/** Public repository that carries the release notes for the npm package. */
|
|
4
|
+
export const RELEASE_NOTES_REPOSITORY = "ESCANOR-001/remodex-android";
|
|
5
|
+
const RELEASE_NOTES_API_URL = `https://api.github.com/repos/${RELEASE_NOTES_REPOSITORY}/releases/tags`;
|
|
6
|
+
export const RELEASE_NOTES_MAX_BYTES = 32 * 1024;
|
|
7
|
+
const RELEASE_RESPONSE_MAX_BYTES = 64 * 1024;
|
|
8
|
+
const FETCH_TIMEOUT_MS = 6_000;
|
|
9
|
+
const BODY_TIMEOUT_MS = 5_000;
|
|
10
|
+
const CACHE_TTL_MS = 10 * 60_000;
|
|
11
|
+
|
|
12
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
13
|
+
|
|
14
|
+
interface CacheEntry {
|
|
15
|
+
body: string;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const notesCache = new Map<string, CacheEntry>();
|
|
20
|
+
|
|
21
|
+
function isReleaseVersion(value: string): boolean {
|
|
22
|
+
return value.length <= 64 && VERSION_PATTERN.test(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeReleaseNotes(value: string): string {
|
|
30
|
+
const normalized = value
|
|
31
|
+
.replace(/\r\n?/g, "\n")
|
|
32
|
+
// Keep markdown and whitespace, but never let terminal/control bytes reach the GUI.
|
|
33
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
|
|
34
|
+
.trim();
|
|
35
|
+
if (Buffer.byteLength(normalized, "utf8") <= RELEASE_NOTES_MAX_BYTES) return normalized;
|
|
36
|
+
|
|
37
|
+
// The response reader already enforces a byte ceiling. This second guard accounts for the
|
|
38
|
+
// JSON envelope and makes the field safe even when this function is called directly in tests.
|
|
39
|
+
const clipped = Buffer.from(normalized, "utf8")
|
|
40
|
+
.subarray(0, RELEASE_NOTES_MAX_BYTES)
|
|
41
|
+
.toString("utf8")
|
|
42
|
+
.replace(/\uFFFD$/u, "")
|
|
43
|
+
.trimEnd();
|
|
44
|
+
return `${clipped}\n…`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Clear the in-memory release-note cache. Intended for focused tests and development reloads. */
|
|
48
|
+
export function clearReleaseNotesCacheForTests(): void {
|
|
49
|
+
notesCache.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fetch the exact GitHub Release body for a version.
|
|
54
|
+
*
|
|
55
|
+
* Release notes are display-only metadata. A registry/API failure therefore returns `null` and
|
|
56
|
+
* leaves the npm version check usable. The response is bounded, rendered as text by React, and
|
|
57
|
+
* accepted only when GitHub confirms the requested tag.
|
|
58
|
+
*/
|
|
59
|
+
export async function fetchReleaseNotesForVersion(
|
|
60
|
+
version: string,
|
|
61
|
+
fetchFn: typeof fetch = fetch,
|
|
62
|
+
): Promise<string | null> {
|
|
63
|
+
if (!isReleaseVersion(version)) return null;
|
|
64
|
+
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const cached = notesCache.get(version);
|
|
67
|
+
if (cached && cached.expiresAt > now) return cached.body;
|
|
68
|
+
if (cached) notesCache.delete(version);
|
|
69
|
+
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetchFn(`${RELEASE_NOTES_API_URL}/v${encodeURIComponent(version)}`, {
|
|
74
|
+
headers: {
|
|
75
|
+
Accept: "application/vnd.github+json",
|
|
76
|
+
"User-Agent": "Remodex-update-check",
|
|
77
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
78
|
+
},
|
|
79
|
+
redirect: "error",
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) return null;
|
|
83
|
+
|
|
84
|
+
const bounded = await readBoundedResponseBody(response, {
|
|
85
|
+
// The JSON envelope and escaping can be larger than the final note body.
|
|
86
|
+
maxBytes: RELEASE_RESPONSE_MAX_BYTES,
|
|
87
|
+
totalTimeoutMs: BODY_TIMEOUT_MS,
|
|
88
|
+
inactivityTimeoutMs: BODY_TIMEOUT_MS,
|
|
89
|
+
});
|
|
90
|
+
if (!bounded.displaySafe || bounded.oversized || bounded.timedOut) return null;
|
|
91
|
+
|
|
92
|
+
let parsed: unknown;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(bounded.text);
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (!isRecord(parsed) || parsed.tag_name !== `v${version}` || typeof parsed.body !== "string") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const body = normalizeReleaseNotes(parsed.body);
|
|
103
|
+
if (!body) return null;
|
|
104
|
+
notesCache.set(version, { body, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
105
|
+
return body;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
} finally {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
}
|
|
111
|
+
}
|