ima2-gen 3.25.0 → 3.26.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 +7 -0
- package/README.md +10 -2
- package/bin/commands/update.js +161 -0
- package/bin/ima2.js +11 -8
- package/bin/lib/helpText.js +1 -0
- package/bin/lib/npmUpdate.js +149 -0
- package/bin/lib/updateNotice.js +30 -0
- package/config.js +10 -0
- package/docs/API.md +13 -0
- package/docs/migration/runtime-test-inventory.md +6 -1
- package/lib/updateCache.js +128 -0
- package/lib/updateCheck.js +118 -0
- package/lib/updateVersion.js +59 -0
- package/package.json +2 -2
- package/routes/events.js +8 -3
- package/routes/index.js +2 -0
- package/routes/update.js +67 -0
- package/server.js +7 -0
- package/skills/ima2/SKILL.md +4 -0
- package/ui/dist/.vite/manifest.json +71 -64
- package/ui/dist/assets/{AgentWorkspace-DXPJup3F.js → AgentWorkspace-DGEWNErN.js} +1 -1
- package/ui/dist/assets/App-CZZNsSpb.css +1 -0
- package/ui/dist/assets/App-b7KJO01r.js +9 -0
- package/ui/dist/assets/AssetGenWorkspace-woj0orEW.js +2 -0
- package/ui/dist/assets/{AssetMediaLightbox-CV9ME5oR.js → AssetMediaLightbox-39eeVLgg.js} +1 -1
- package/ui/dist/assets/{AssetsWorkspace-DWzOEvQE.js → AssetsWorkspace-feT-v8BY.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-B1qS17V6.js → CardNewsWorkspace-Bce7Qq0s.js} +1 -1
- package/ui/dist/assets/{GenerationRequestLogPanel-DyFi62oC.js → GenerationRequestLogPanel-D6jO_fXj.js} +1 -1
- package/ui/dist/assets/{HomeWorkspace-BHdt4qCg.js → HomeWorkspace-C7SABT2o.js} +1 -1
- package/ui/dist/assets/{KeyingPanel-CyycINzW.js → KeyingPanel-DWaV9fRG.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-BdKhUDjc.js → NodeCanvas-ybOBBYto.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-CMXjpe3Y.js → PromptBuilderPanel-C1DLHD9Q.js} +1 -1
- package/ui/dist/assets/PromptImportDialog-OGyYU5K5.js +2 -0
- package/ui/dist/assets/{PromptImportDiscoverySection-C0WBOpMu.js → PromptImportDiscoverySection-Cwigr40k.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-DqAoW1I_.js → PromptImportFolderSection-B1XqjRbl.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-B6a4fySv.js → PromptLibraryPanel-v7hIoYy-.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-BF_e8kUk.js → SettingsWorkspace-CGZXJLgN.js} +1 -1
- package/ui/dist/assets/{SpriteRecipeWorkspace-BbkGaNby.js → SpriteRecipeWorkspace-Cm5wizr5.js} +1 -1
- package/ui/dist/assets/{index-CDZTiRZg.js → index-CJ-sBrSx.js} +9 -8
- package/ui/dist/assets/{index-BrM_ZMPs.js → index-CUMOJ4Vx.js} +3 -3
- package/ui/dist/assets/{pptxgen.es-DWAWViwz.js → pptxgen.es-XHiluGIr.js} +1 -1
- package/ui/dist/assets/{promptBuilderStore-CCMVetZf.js → promptBuilderStore-BKNNtgtk.js} +1 -1
- package/ui/dist/assets/useAgentDialogFocus-CZabQdET.js +1 -0
- package/ui/dist/index.html +1 -1
- package/ui/dist/assets/App-BdynRa0c.js +0 -9
- package/ui/dist/assets/AssetGenWorkspace-CFjZEEJn.js +0 -2
- package/ui/dist/assets/PromptImportDialog-Dd1sw7H6.js +0 -2
- package/ui/dist/assets/useAgentDialogFocus-Cxfj1Zq7.js +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { errInfo } from "./errInfo.js";
|
|
3
|
+
import { readVersionCache, updateVersionCache } from "./updateCache.js";
|
|
4
|
+
import { PACKAGE_NAME, parseVersion } from "./updateVersion.js";
|
|
5
|
+
export async function fetchLatestVersion(tag, deps = {}) {
|
|
6
|
+
const registry = (deps.registry ?? config.update.registry).replace(/\/+$/, "");
|
|
7
|
+
const timeout = AbortSignal.timeout(deps.timeoutMs ?? config.update.timeoutMs);
|
|
8
|
+
const signal = deps.signal ? AbortSignal.any([timeout, deps.signal]) : timeout;
|
|
9
|
+
const response = await (deps.fetchImpl ?? fetch)(`${registry}/${PACKAGE_NAME}/${tag}`, {
|
|
10
|
+
headers: { accept: "application/json" }, signal,
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok)
|
|
13
|
+
throw new Error(`registry returned HTTP ${response.status}`);
|
|
14
|
+
const body = await response.json().catch((error) => {
|
|
15
|
+
throw new Error("registry returned invalid JSON", { cause: error });
|
|
16
|
+
});
|
|
17
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !("version" in body)
|
|
18
|
+
|| typeof body.version !== "string" || !parseVersion(body.version)) {
|
|
19
|
+
throw new Error("registry returned an invalid version");
|
|
20
|
+
}
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
return body.version;
|
|
23
|
+
}
|
|
24
|
+
const inFlight = new Map();
|
|
25
|
+
export function checkForUpdate(tag, deps = {}) {
|
|
26
|
+
const existing = inFlight.get(tag);
|
|
27
|
+
if (existing)
|
|
28
|
+
return existing;
|
|
29
|
+
const pending = fetchLatestVersion(tag, deps).then((latest_version) => {
|
|
30
|
+
deps.signal?.throwIfAborted();
|
|
31
|
+
return updateVersionCache({ latest_version, last_checked_at: (deps.now ?? Date.now)(), tag }, deps.cachePath);
|
|
32
|
+
}).finally(() => { inFlight.delete(tag); });
|
|
33
|
+
inFlight.set(tag, pending);
|
|
34
|
+
return pending;
|
|
35
|
+
}
|
|
36
|
+
export function isCacheFresh(cache, tag, now, maxAgeMs = config.update.freshMs) {
|
|
37
|
+
return cache.tag === tag && cache.last_checked_at !== null && now - cache.last_checked_at < maxAgeMs;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Automatic checks run only for real npm-launched servers: never for the desktop's bundled
|
|
41
|
+
* server (the app updates itself) and never under node --test, whose NODE_TEST_CONTEXT is
|
|
42
|
+
* inherited by servers that tests start, so suites stay offline and away from ~/.ima2.
|
|
43
|
+
*/
|
|
44
|
+
export function updateChecksEnabled(env, launcher) {
|
|
45
|
+
return env.IMA2_DISABLE_UPDATE_CHECK !== "1" && launcher !== "desktop" && !env.NODE_TEST_CONTEXT;
|
|
46
|
+
}
|
|
47
|
+
class UpdateScheduler {
|
|
48
|
+
opts;
|
|
49
|
+
controller = new AbortController();
|
|
50
|
+
deps;
|
|
51
|
+
setTimer;
|
|
52
|
+
clearTimer;
|
|
53
|
+
timer;
|
|
54
|
+
stopped = false;
|
|
55
|
+
pending = null;
|
|
56
|
+
delay = 0;
|
|
57
|
+
backoff;
|
|
58
|
+
constructor(opts) {
|
|
59
|
+
this.opts = opts;
|
|
60
|
+
this.deps = { ...opts.deps, signal: opts.deps?.signal
|
|
61
|
+
? AbortSignal.any([this.controller.signal, opts.deps.signal]) : this.controller.signal };
|
|
62
|
+
this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
63
|
+
this.clearTimer = opts.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
64
|
+
this.backoff = opts.backoffMinMs ?? config.update.backoffMinMs;
|
|
65
|
+
this.schedule(0);
|
|
66
|
+
}
|
|
67
|
+
checkNow() {
|
|
68
|
+
if (this.stopped)
|
|
69
|
+
return Promise.resolve(null);
|
|
70
|
+
if (this.pending)
|
|
71
|
+
return this.pending;
|
|
72
|
+
this.pending = checkForUpdate(this.opts.tag, this.deps).catch((error) => {
|
|
73
|
+
if (!this.stopped)
|
|
74
|
+
(this.opts.log ?? console.log)(`[update] check failed: ${errInfo(error).message}`);
|
|
75
|
+
return null;
|
|
76
|
+
}).finally(() => { this.pending = null; });
|
|
77
|
+
return this.pending;
|
|
78
|
+
}
|
|
79
|
+
async run() {
|
|
80
|
+
if (this.stopped)
|
|
81
|
+
return;
|
|
82
|
+
const cache = readVersionCache(this.deps.cachePath);
|
|
83
|
+
const fresh = isCacheFresh(cache, this.opts.tag, (this.deps.now ?? Date.now)(), this.opts.freshMs);
|
|
84
|
+
const succeeded = fresh || await this.checkNow() !== null;
|
|
85
|
+
if (this.stopped)
|
|
86
|
+
return;
|
|
87
|
+
if (succeeded) {
|
|
88
|
+
this.backoff = this.opts.backoffMinMs ?? config.update.backoffMinMs;
|
|
89
|
+
this.schedule(this.opts.tickMs ?? config.update.tickMs);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.schedule(this.backoff);
|
|
93
|
+
this.backoff = Math.min(this.backoff * 2, this.opts.backoffMaxMs ?? config.update.backoffMaxMs);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
schedule(ms) {
|
|
97
|
+
if (this.stopped)
|
|
98
|
+
return;
|
|
99
|
+
this.delay = ms;
|
|
100
|
+
this.timer = this.setTimer(() => { void this.run(); }, ms);
|
|
101
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer && typeof this.timer.unref === "function") {
|
|
102
|
+
this.timer.unref();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
stop() {
|
|
106
|
+
this.stopped = true;
|
|
107
|
+
if (this.timer !== undefined)
|
|
108
|
+
this.clearTimer(this.timer);
|
|
109
|
+
this.controller.abort();
|
|
110
|
+
}
|
|
111
|
+
nextDelayMs() { return this.delay; }
|
|
112
|
+
}
|
|
113
|
+
export function startUpdateScheduler(opts) {
|
|
114
|
+
const scheduler = new UpdateScheduler(opts);
|
|
115
|
+
return {
|
|
116
|
+
stop: () => scheduler.stop(), checkNow: () => scheduler.checkNow(), nextDelayMs: () => scheduler.nextDelayMs(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const PACKAGE_NAME = "ima2-gen";
|
|
2
|
+
const VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
3
|
+
export function parseVersion(v) {
|
|
4
|
+
const match = VERSION_RE.exec(v);
|
|
5
|
+
if (!match)
|
|
6
|
+
return null;
|
|
7
|
+
const core = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
8
|
+
if (!core.every(Number.isSafeInteger))
|
|
9
|
+
return null;
|
|
10
|
+
const pre = match[4]?.split(".") ?? [];
|
|
11
|
+
if (pre.some((part) => /^\d+$/.test(part) && part.length > 1 && part.startsWith("0")))
|
|
12
|
+
return null;
|
|
13
|
+
return { core, pre };
|
|
14
|
+
}
|
|
15
|
+
function compareIdentifier(a, b) {
|
|
16
|
+
if (a === b)
|
|
17
|
+
return 0;
|
|
18
|
+
const aNumeric = /^\d+$/.test(a);
|
|
19
|
+
const bNumeric = /^\d+$/.test(b);
|
|
20
|
+
if (aNumeric && bNumeric && a.length !== b.length)
|
|
21
|
+
return a.length > b.length ? 1 : -1;
|
|
22
|
+
if (aNumeric !== bNumeric)
|
|
23
|
+
return aNumeric ? -1 : 1;
|
|
24
|
+
return a > b ? 1 : -1;
|
|
25
|
+
}
|
|
26
|
+
export function compareVersions(a, b) {
|
|
27
|
+
const left = parseVersion(a);
|
|
28
|
+
const right = parseVersion(b);
|
|
29
|
+
if (!left || !right)
|
|
30
|
+
return left ? 1 : right ? -1 : 0;
|
|
31
|
+
for (const index of [0, 1, 2]) {
|
|
32
|
+
if (left.core[index] !== right.core[index])
|
|
33
|
+
return left.core[index] > right.core[index] ? 1 : -1;
|
|
34
|
+
}
|
|
35
|
+
if (!left.pre.length || !right.pre.length)
|
|
36
|
+
return left.pre.length ? -1 : right.pre.length ? 1 : 0;
|
|
37
|
+
for (let index = 0; index < Math.max(left.pre.length, right.pre.length); index++) {
|
|
38
|
+
const l = left.pre[index];
|
|
39
|
+
const r = right.pre[index];
|
|
40
|
+
if (l === undefined || r === undefined)
|
|
41
|
+
return l === undefined ? -1 : 1;
|
|
42
|
+
const order = compareIdentifier(l, r);
|
|
43
|
+
if (order)
|
|
44
|
+
return order;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
export function isNewer(candidate, current) {
|
|
49
|
+
return typeof candidate === "string" && parseVersion(candidate) !== null && compareVersions(candidate, current) > 0;
|
|
50
|
+
}
|
|
51
|
+
export function defaultTag(current) {
|
|
52
|
+
return /-preview/.test(current) ? "preview" : "latest";
|
|
53
|
+
}
|
|
54
|
+
export function isUpdateTag(v) {
|
|
55
|
+
return v === "latest" || v === "preview";
|
|
56
|
+
}
|
|
57
|
+
export function releaseUrl(version) {
|
|
58
|
+
return `https://github.com/lidge-ai/ima2-gen/releases/tag/v${version}`;
|
|
59
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.26.0",
|
|
4
4
|
"packageManager": "npm@11.18.0",
|
|
5
5
|
"description": "Local-first visual generation runtime and studio for people and coding agents, with reproducible image and video workflows across multiple providers.",
|
|
6
6
|
"type": "module",
|
|
@@ -131,5 +131,5 @@
|
|
|
131
131
|
"typescript-eslint": "8.70.0",
|
|
132
132
|
"yaml": "2.9.1"
|
|
133
133
|
},
|
|
134
|
-
"gitHead": "
|
|
134
|
+
"gitHead": "f7857a52d057ce628f04d3402ac460fd029f92f9"
|
|
135
135
|
}
|
package/routes/events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { subscribe, replaySince, hasReplayGap, replayOldestId, latestEventId, MAX_SSE_LISTENERS } from "../lib/eventBus.js";
|
|
2
2
|
import { SSE_STREAM_POLICY } from "../lib/eventsPolicy.js";
|
|
3
|
+
import { updateHintFrame } from "./update.js";
|
|
3
4
|
let activeConnections = 0;
|
|
4
5
|
const HEARTBEAT_MS = 15_000;
|
|
5
6
|
function formatSse(ev) {
|
|
@@ -20,16 +21,18 @@ class EventConnection {
|
|
|
20
21
|
req;
|
|
21
22
|
res;
|
|
22
23
|
cursor;
|
|
24
|
+
hint;
|
|
23
25
|
closed = false;
|
|
24
26
|
pumping = false;
|
|
25
27
|
blocked = false;
|
|
26
28
|
unsubscribe = () => { };
|
|
27
29
|
heartbeat;
|
|
28
30
|
deadline;
|
|
29
|
-
constructor(req, res, cursor) {
|
|
31
|
+
constructor(req, res, cursor, hint = null) {
|
|
30
32
|
this.req = req;
|
|
31
33
|
this.res = res;
|
|
32
34
|
this.cursor = cursor;
|
|
35
|
+
this.hint = hint;
|
|
33
36
|
}
|
|
34
37
|
start() {
|
|
35
38
|
activeConnections++;
|
|
@@ -46,6 +49,8 @@ class EventConnection {
|
|
|
46
49
|
this.res.flushHeaders?.();
|
|
47
50
|
if (this.closed)
|
|
48
51
|
return;
|
|
52
|
+
if (this.hint)
|
|
53
|
+
this.write(this.hint);
|
|
49
54
|
this.unsubscribe = subscribe(this.live);
|
|
50
55
|
this.pump();
|
|
51
56
|
if (!this.closed)
|
|
@@ -150,7 +155,7 @@ class EventConnection {
|
|
|
150
155
|
this.res.destroy();
|
|
151
156
|
};
|
|
152
157
|
}
|
|
153
|
-
export function registerEventsRoute(app,
|
|
158
|
+
export function registerEventsRoute(app, ctx) {
|
|
154
159
|
app.get("/api/events", (req, res) => {
|
|
155
160
|
if (activeConnections >= MAX_SSE_LISTENERS) {
|
|
156
161
|
return res.status(503).json({
|
|
@@ -161,6 +166,6 @@ export function registerEventsRoute(app, _ctx) {
|
|
|
161
166
|
const queryLastId = parseInt(String(req.query.lastEventId ?? ""), 10);
|
|
162
167
|
const lastId = Number.isSafeInteger(headerLastId) ? headerLastId : queryLastId;
|
|
163
168
|
const cursor = Number.isSafeInteger(lastId) ? Math.max(0, lastId) : latestEventId();
|
|
164
|
-
new EventConnection(req, res, cursor).start();
|
|
169
|
+
new EventConnection(req, res, cursor, updateHintFrame(ctx)).start();
|
|
165
170
|
});
|
|
166
171
|
}
|
package/routes/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { registerCapabilitiesRoutes } from "./capabilities.js";
|
|
|
2
2
|
import { registerModelsRoutes } from "./models.js";
|
|
3
3
|
import { registerEventsRoute } from "./events.js";
|
|
4
4
|
import { registerHealthRoutes } from "./health.js";
|
|
5
|
+
import { registerUpdateRoutes } from "./update.js";
|
|
5
6
|
import { registerGithubRoutes } from "./github.js";
|
|
6
7
|
import { registerAdminRoutes } from "./admin.js";
|
|
7
8
|
import { registerHistoryRoutes } from "./history.js";
|
|
@@ -47,6 +48,7 @@ export function configureRoutes(app, ctxRaw) {
|
|
|
47
48
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
48
49
|
registerEventsRoute(app, ctx);
|
|
49
50
|
registerHealthRoutes(app, ctx);
|
|
51
|
+
registerUpdateRoutes(app, ctx);
|
|
50
52
|
registerGithubRoutes(app);
|
|
51
53
|
registerAdminRoutes(app, ctx);
|
|
52
54
|
registerCapabilitiesRoutes(app, ctx);
|
package/routes/update.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { errInfo } from "../lib/errInfo.js";
|
|
2
|
+
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
3
|
+
import { buildBadge, claimUpdatedNotice, readVersionCache, updateVersionCache, versionCachePath } from "../lib/updateCache.js";
|
|
4
|
+
import { checkForUpdate, updateChecksEnabled } from "../lib/updateCheck.js";
|
|
5
|
+
import { defaultTag, parseVersion } from "../lib/updateVersion.js";
|
|
6
|
+
/**
|
|
7
|
+
* One SSE frame with the cached badge, written when a browser opens GET /api/events. It is the
|
|
8
|
+
* web UI's only cue that this server has update routes: the UI makes no update request until it
|
|
9
|
+
* sees it, so fixture servers and e2e route harnesses never receive one. No id line, so replay
|
|
10
|
+
* cursors are unchanged. Null for desktop-launched servers and for contexts without a version.
|
|
11
|
+
*/
|
|
12
|
+
export function updateHintFrame(ctxRaw, deps = {}) {
|
|
13
|
+
const raw = ctxRaw;
|
|
14
|
+
if (typeof raw?.packageVersion !== "string" || raw.launcher === "desktop")
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const ctx = requireRuntimeContext(ctxRaw);
|
|
18
|
+
const badge = buildBadge({
|
|
19
|
+
cache: readVersionCache(deps.cachePath ?? versionCachePath(ctx.config.storage.configDir)),
|
|
20
|
+
current: ctx.packageVersion, tag: defaultTag(ctx.packageVersion), surface: "npm",
|
|
21
|
+
enabled: updateChecksEnabled(deps.env ?? process.env, ctx.launcher),
|
|
22
|
+
now: (deps.now ?? Date.now)(), staleMs: ctx.config.update.staleMs,
|
|
23
|
+
});
|
|
24
|
+
return `event: update\ndata: ${JSON.stringify(badge)}\n\n`;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null; // The hint is optional; an unreadable cache must not break the event stream.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function registerUpdateRoutes(app, ctxRaw, deps = {}) {
|
|
31
|
+
const ctx = requireRuntimeContext(ctxRaw);
|
|
32
|
+
const surface = ctx.launcher === "desktop" ? "desktop" : "npm";
|
|
33
|
+
const tag = defaultTag(ctx.packageVersion);
|
|
34
|
+
const path = deps.cachePath ?? versionCachePath(ctx.config.storage.configDir);
|
|
35
|
+
const badge = () => buildBadge({
|
|
36
|
+
cache: readVersionCache(path), current: ctx.packageVersion, tag, surface,
|
|
37
|
+
enabled: updateChecksEnabled(deps.env ?? process.env, ctx.launcher),
|
|
38
|
+
now: (deps.now ?? Date.now)(), staleMs: ctx.config.update.staleMs,
|
|
39
|
+
});
|
|
40
|
+
app.get("/api/update/badge", (_req, res) => { res.json(badge()); });
|
|
41
|
+
app.post("/api/update/check", async (_req, res) => {
|
|
42
|
+
if (surface === "desktop") {
|
|
43
|
+
res.json(badge());
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await (deps.check ?? checkForUpdate)(tag, { cachePath: path, now: deps.now ?? Date.now });
|
|
48
|
+
res.json(badge());
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
res.status(503).json({ ok: false, code: "UPDATE_CHECK_FAILED", message: errInfo(error).message, badge: badge() });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
app.post("/api/update/dismiss", (req, res) => {
|
|
55
|
+
const body = req.body;
|
|
56
|
+
const version = body && typeof body === "object" && "version" in body ? body.version : undefined;
|
|
57
|
+
if (typeof version !== "string" || !parseVersion(version)) {
|
|
58
|
+
res.status(400).json({ ok: false, code: "INVALID_VERSION" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
updateVersionCache({ dismissed_version: version }, path);
|
|
62
|
+
res.json(badge());
|
|
63
|
+
});
|
|
64
|
+
app.post("/api/update/notice", (_req, res) => {
|
|
65
|
+
res.json({ updatedTo: surface === "desktop" ? null : claimUpdatedNotice(ctx.packageVersion, path) });
|
|
66
|
+
});
|
|
67
|
+
}
|
package/server.js
CHANGED
|
@@ -14,6 +14,8 @@ import { configureLogger, logError, logWarn } from "./lib/logger.js";
|
|
|
14
14
|
import { createRequestLogger } from "./lib/requestLogger.js";
|
|
15
15
|
import { configureApiCachePolicy } from "./lib/apiCachePolicy.js";
|
|
16
16
|
import { configureRoutes } from "./routes/index.js";
|
|
17
|
+
import { startUpdateScheduler, updateChecksEnabled } from "./lib/updateCheck.js";
|
|
18
|
+
import { defaultTag } from "./lib/updateVersion.js";
|
|
17
19
|
import { API_REQUEST_POLICY, config } from "./config.js";
|
|
18
20
|
import { createApiRequestBudget } from "./lib/apiRequestBudget.js";
|
|
19
21
|
import { createLocalLanAccess, createLanApiGuard as tokenOnlyLanApiGuard } from "./lib/localLanAccess.js";
|
|
@@ -496,8 +498,10 @@ export async function startServer(overrides = {}) {
|
|
|
496
498
|
ctx.markOAuthReady({ url: ctx.oauthUrl, port: ctx.oauthPort });
|
|
497
499
|
let server;
|
|
498
500
|
let reapTimer;
|
|
501
|
+
let updateScheduler = null;
|
|
499
502
|
let tempReferenceReapTimer;
|
|
500
503
|
onShutdown(async () => {
|
|
504
|
+
updateScheduler?.stop();
|
|
501
505
|
unadvertise(ctx);
|
|
502
506
|
try {
|
|
503
507
|
oauthChild?.stop?.();
|
|
@@ -543,6 +547,9 @@ export async function startServer(overrides = {}) {
|
|
|
543
547
|
console.log(`Image Gen running at ${ctx.serverUrl}`);
|
|
544
548
|
console.log(`Provider policy: GPT OAuth, API-key Responses, and Grok (xAI OAuth or API key) providers. GPT OAuth ${ctx.oauthTransport === "native" ? "calls ChatGPT directly" : `uses ${ctx.oauthUrl}`}.`);
|
|
545
549
|
advertise(ctx);
|
|
550
|
+
if (updateChecksEnabled(process.env, ctx.launcher)) {
|
|
551
|
+
updateScheduler = startUpdateScheduler({ tag: defaultTag(ctx.packageVersion), log: (m) => console.log(m) });
|
|
552
|
+
}
|
|
546
553
|
try {
|
|
547
554
|
const s = ensureDefaultSession();
|
|
548
555
|
if (s)
|
package/skills/ima2/SKILL.md
CHANGED
|
@@ -48,6 +48,10 @@ needs `ima2 stop --service`; a server started by the desktop app is restarted fr
|
|
|
48
48
|
|
|
49
49
|
Use `ima2 doctor` when setup, GPT OAuth, storage, or package integrity is unclear.
|
|
50
50
|
|
|
51
|
+
Update a global install with `ima2 update --check` (report only) or `ima2 update --yes`
|
|
52
|
+
(install, restart the service or background server, verify the new version). Add `--json`
|
|
53
|
+
for one machine-readable result. It refuses source checkouts and npx runs.
|
|
54
|
+
|
|
51
55
|
## Generate Images
|
|
52
56
|
|
|
53
57
|
List ready image lanes, choose a persistent CLI target, then generate:
|