ima2-gen 3.26.0 → 3.26.1
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 +10 -0
- package/bin/lib/platform.js +36 -15
- package/docs/API.md +1 -1
- package/docs/migration/runtime-test-inventory.md +5 -1
- package/package.json +2 -2
- package/routes/admin.js +9 -17
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.26.1] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Desktop update checks find their manifests** — installed apps ask the release GitHub marks Latest, which is the npm release `vX.Y.Z`, for `latest-mac.yml`, but the manifests lived only in `desktop-vX.Y.Z`, so every check ended in a 404 ("Unable to check for updates"). The desktop workflow now copies `latest*.yml` into `vX.Y.Z` with file paths that point at the desktop release assets (`scripts/mirror-desktop-update-manifests.mjs`), which also lets apps from 3.23–3.26 update without a new build.
|
|
15
|
+
- **Start hidden applies only at login** — the desktop app used to hide its window on every launch when Start hidden was on. Now only a login launch stays in the menu bar or tray; opening ima2 yourself always shows the window, and a login relaunch of a running app no longer raises it. (#365)
|
|
16
|
+
- **Closing the window keeps the server and frees the renderer** — with Keep running on close, closing the window now destroys it instead of hiding it; the server keeps running in the tray and *Open ima2* recreates a window on the live server. *Open ima2* stays available when the server is stopped or failed, because the window offers Start, Restart and the server log. A crashed or killed window reloads itself up to three times a minute. (#365)
|
|
17
|
+
- **A failed update install no longer leaves ima2 without a server** — if the installer does not start (an error from the updater, a thrown install call, or, on Windows and Linux, no exit within 15 seconds), the app restarts the server it stopped and shows a retryable error. A quit that was cancelled during update preparation no longer disables the updater. (#365)
|
|
18
|
+
- **Stopping the server on Windows** — the desktop app ends the whole server process tree (`taskkill /T /F`) and a stop that cannot end the server reports an error after a bounded wait instead of hanging quit or an update. `ima2 stop` and the desktop app now run the server's own shutdown on Windows too (OAuth helper, MCP connections, database), which a self-sent SIGTERM skipped. (#365)
|
|
19
|
+
|
|
10
20
|
## [3.26.0] - 2026-09-30
|
|
11
21
|
|
|
12
22
|
### Added
|
package/bin/lib/platform.js
CHANGED
|
@@ -89,27 +89,48 @@ export function openUrl(url) {
|
|
|
89
89
|
* before forceful exit, giving file handles and sockets time to close cleanly.
|
|
90
90
|
*/
|
|
91
91
|
const SHUTDOWN_GRACE_MS = 3_000;
|
|
92
|
-
|
|
92
|
+
export function createShutdownCoordinator({ exit = (code) => process.exit(code), graceMs = SHUTDOWN_GRACE_MS, } = {}) {
|
|
93
|
+
let handler = null;
|
|
94
|
+
let run = null;
|
|
95
|
+
return {
|
|
96
|
+
setHandler(next) { handler = next; },
|
|
97
|
+
request(reason) {
|
|
98
|
+
if (run)
|
|
99
|
+
return run;
|
|
100
|
+
let exited = false;
|
|
101
|
+
let force;
|
|
102
|
+
const finish = () => {
|
|
103
|
+
if (exited)
|
|
104
|
+
return;
|
|
105
|
+
exited = true;
|
|
106
|
+
clearTimeout(force);
|
|
107
|
+
exit(0);
|
|
108
|
+
};
|
|
109
|
+
force = setTimeout(finish, graceMs);
|
|
110
|
+
force.unref?.();
|
|
111
|
+
run = (async () => {
|
|
112
|
+
try {
|
|
113
|
+
await handler?.(reason);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
logWarn("shutdown", "handler_failed", { signal: reason, error: err });
|
|
117
|
+
}
|
|
118
|
+
finish();
|
|
119
|
+
})();
|
|
120
|
+
return run;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const shutdown = createShutdownCoordinator();
|
|
125
|
+
export const requestShutdown = (reason) => shutdown.request(reason);
|
|
93
126
|
export function onShutdown(handler) {
|
|
127
|
+
shutdown.setHandler(handler);
|
|
94
128
|
const signals = isWin
|
|
95
129
|
? ["SIGINT", "SIGTERM", "SIGBREAK"]
|
|
96
130
|
: ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
97
131
|
for (const sig of signals) {
|
|
98
132
|
try {
|
|
99
|
-
process.on(sig,
|
|
100
|
-
if (shutdownStarted)
|
|
101
|
-
return;
|
|
102
|
-
shutdownStarted = true;
|
|
103
|
-
const forceExit = setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS);
|
|
104
|
-
forceExit.unref?.();
|
|
105
|
-
try {
|
|
106
|
-
await handler(sig);
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
logWarn("shutdown", "handler_failed", { signal: sig, error: err });
|
|
110
|
-
}
|
|
111
|
-
process.exit(0);
|
|
112
|
-
});
|
|
133
|
+
process.on(sig, () => { void requestShutdown(sig); });
|
|
113
134
|
}
|
|
114
135
|
catch {
|
|
115
136
|
// Some signals aren't installable on certain platforms; ignore.
|
package/docs/API.md
CHANGED
|
@@ -149,7 +149,7 @@ inventing a runtime `lanes` availability result.
|
|
|
149
149
|
| Method | Path | Notes |
|
|
150
150
|
|---|---|---|
|
|
151
151
|
| `GET` | `/api/health` | Server health, version, paths, provider policy; includes `grok: { auth: "oauth" \| "none" }` and the runtime identity `bootId`, `launcher` (`foreground` \| `background` \| `service` \| `desktop`) and `root`, mirrored in `~/.ima2/server.json` |
|
|
152
|
-
| `POST` | `/api/admin/stop` | Clean shutdown (local admin only): requires the boot-generated `X-Ima2-Admin-Nonce` from `~/.ima2/server.json`; any request with an `Origin` header is refused (browser drive-by protection). Responds `202`, prints `IMA2_STOP_INTENT <bootId>` on the server's stdout (the desktop app reads it to tell a requested stop from a crash), then self-
|
|
152
|
+
| `POST` | `/api/admin/stop` | Clean shutdown (local admin only): requires the boot-generated `X-Ima2-Admin-Nonce` from `~/.ima2/server.json`; any request with an `Origin` header is refused (browser drive-by protection). Responds `202`, prints `IMA2_STOP_INTENT <bootId>` on the server's stdout (the desktop app reads it to tell a requested stop from a crash), then runs the server's shutdown routine directly (a self-SIGTERM would skip it on Windows) |
|
|
153
153
|
| `GET` | `/api/providers` | Provider availability and runtime ports |
|
|
154
154
|
| `GET` | `/api/oauth/status` | GPT OAuth status and visible models, plus `auth` / `grokAuth` login verdicts: `{ provider, loggedIn, health: healthy\|warning\|reauth_required\|not_logged_in, reason?, source?, email?, plan?, accountId (masked), expiresAt?, refreshable, action?, note? }`. A proxy that exited because no session file exists reports `auth_required`. |
|
|
155
155
|
| `GET` | `/api/grok/status` | xAI OAuth session state and visible xAI image models: `ready`, `no_image_model`, `error`, or `offline` with `reason: "login_required"` when no session is stored |
|
|
@@ -4,7 +4,7 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
|
|
|
4
4
|
|
|
5
5
|
_Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
|
|
6
6
|
|
|
7
|
-
Total:
|
|
7
|
+
Total: 545 (runtime: 247, contract: 298)
|
|
8
8
|
|
|
9
9
|
## Runtime-importing tests
|
|
10
10
|
- `tests/advertise-payload-contract.test.ts`
|
|
@@ -169,6 +169,7 @@ Total: 541 (runtime: 246, contract: 295)
|
|
|
169
169
|
- `tests/open-directory.test.ts`
|
|
170
170
|
- `tests/openai-execution-parity.test.ts`
|
|
171
171
|
- `tests/openai-transport-parity.test.ts`
|
|
172
|
+
- `tests/platform-shutdown.test.ts`
|
|
172
173
|
- `tests/preset-compiler.test.ts`
|
|
173
174
|
- `tests/preset-restore-contract.test.ts`
|
|
174
175
|
- `tests/preset-xmp-roundtrip.test.ts`
|
|
@@ -358,6 +359,7 @@ Total: 541 (runtime: 246, contract: 295)
|
|
|
358
359
|
- `tests/desktop-context-menu.test.ts`
|
|
359
360
|
- `tests/desktop-edit-menu.test.ts`
|
|
360
361
|
- `tests/desktop-mac-verification.test.ts`
|
|
362
|
+
- `tests/desktop-quit-cleanup.test.ts`
|
|
361
363
|
- `tests/desktop-release-assets.test.ts`
|
|
362
364
|
- `tests/desktop-release-contract.test.ts`
|
|
363
365
|
- `tests/desktop-release-workflow-shell.test.ts`
|
|
@@ -369,8 +371,10 @@ Total: 541 (runtime: 246, contract: 295)
|
|
|
369
371
|
- `tests/desktop-takeover.test.ts`
|
|
370
372
|
- `tests/desktop-titlebar-contract.test.ts`
|
|
371
373
|
- `tests/desktop-tray.test.ts`
|
|
374
|
+
- `tests/desktop-update-manifest-mirror.test.ts`
|
|
372
375
|
- `tests/desktop-update-state.test.ts`
|
|
373
376
|
- `tests/desktop-updater.test.ts`
|
|
377
|
+
- `tests/desktop-window-lifecycle.test.ts`
|
|
374
378
|
- `tests/desktop-window-open.test.ts`
|
|
375
379
|
- `tests/direct-mode-visual-contract.test.ts`
|
|
376
380
|
- `tests/dropped-media-sorting-contract.test.ts`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "3.26.
|
|
3
|
+
"version": "3.26.1",
|
|
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": "8c27479c140b1a8db59b866cf41df099e287dd9e"
|
|
135
135
|
}
|
package/routes/admin.js
CHANGED
|
@@ -2,6 +2,7 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
3
3
|
import { logEvent } from "../lib/logger.js";
|
|
4
4
|
import { stopIntentLine } from "../lib/runtimeIdentity.js";
|
|
5
|
+
import { requestShutdown } from "../bin/lib/platform.js";
|
|
5
6
|
/**
|
|
6
7
|
* Local admin surface. POST /api/admin/stop shuts the server down cleanly.
|
|
7
8
|
*
|
|
@@ -17,18 +18,17 @@ import { stopIntentLine } from "../lib/runtimeIdentity.js";
|
|
|
17
18
|
* 2. Any request carrying an Origin header is refused outright. Browser-issued
|
|
18
19
|
* cross-origin fetches always carry Origin; the ima2 CLI never does.
|
|
19
20
|
*
|
|
20
|
-
* Shutdown
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* proxy children and leave a stale advertise file (audit blocker 2).
|
|
21
|
+
* Shutdown goes through the coordinator installed by onShutdown(), which owns
|
|
22
|
+
* the complete teardown (unadvertise, proxy children, agent queue, timers, DB
|
|
23
|
+
* close, exit). A Windows self-SIGTERM terminates the process without running
|
|
24
|
+
* that handler, so the route requests shutdown directly instead.
|
|
25
25
|
*
|
|
26
|
-
* Before
|
|
27
|
-
*
|
|
26
|
+
* Before requesting shutdown the route prints the stop-intent line on stdout
|
|
27
|
+
* and proceeds only once that write has flushed. A supervisor that owns this
|
|
28
28
|
* process's pipe (the desktop shell) reads it to tell a requested stop from a
|
|
29
29
|
* crash, so it does not restart a server the user just stopped.
|
|
30
30
|
*/
|
|
31
|
-
export function registerAdminRoutes(app, ctxRaw) {
|
|
31
|
+
export function registerAdminRoutes(app, ctxRaw, { shutdown = requestShutdown } = {}) {
|
|
32
32
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
33
33
|
app.post("/api/admin/stop", (req, res) => {
|
|
34
34
|
if (typeof req.headers.origin === "string" && req.headers.origin.length > 0) {
|
|
@@ -45,16 +45,8 @@ export function registerAdminRoutes(app, ctxRaw) {
|
|
|
45
45
|
}
|
|
46
46
|
logEvent("admin", "stop_requested", { pid: process.pid });
|
|
47
47
|
res.status(202).json({ ok: true, pid: process.pid, stopping: true });
|
|
48
|
-
const selfSignal = () => {
|
|
49
|
-
try {
|
|
50
|
-
process.kill(process.pid, "SIGTERM");
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
process.exit(0);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
48
|
setImmediate(() => {
|
|
57
|
-
process.stdout.write(stopIntentLine(ctx.bootId), () =>
|
|
49
|
+
process.stdout.write(stopIntentLine(ctx.bootId), () => { void shutdown("admin"); });
|
|
58
50
|
});
|
|
59
51
|
});
|
|
60
52
|
}
|