minecodex 0.2.2 → 1.0.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.
@@ -4,6 +4,12 @@
4
4
  --color-token-main-surface-primary: #fff;
5
5
  --color-token-bg-primary: #f9f9f9;
6
6
  --color-token-bg-secondary: color-mix(in srgb, #f9f9f9 92%, transparent);
7
+ --color-surface-tertiary: #f0f0f0;
8
+ --color-surface-elevated-secondary: #fff;
9
+ --color-border-subtle: color-mix(in oklab, #1a1c1f 4%, transparent);
10
+ --color-background-primary-ghost-hover: color-mix(in oklab, #1a1c1f 5%, transparent);
11
+ --color-chart-blue: #339cff;
12
+ --color-text-inverse: #fff;
7
13
  --color-background-elevated-primary-opaque: #fff;
8
14
  --color-background-panel: #f5f5f5;
9
15
  --color-background-primary-solid: #1a1c1f;
@@ -48,6 +54,12 @@
48
54
  --color-token-main-surface-primary: #181818;
49
55
  --color-token-bg-primary: #141414;
50
56
  --color-token-bg-secondary: color-mix(in srgb, #141414 92%, transparent);
57
+ --color-surface-tertiary: rgb(40 40 40);
58
+ --color-surface-elevated-secondary: rgb(45 45 45);
59
+ --color-border-subtle: rgb(255 255 255 / 4.2%);
60
+ --color-background-primary-ghost-hover: rgb(255 255 255 / 7.8%);
61
+ --color-chart-blue: #339cff;
62
+ --color-text-inverse: rgb(13 13 13);
51
63
  --color-background-elevated-primary-opaque: #363636;
52
64
  --color-background-panel: #232323;
53
65
  --color-background-primary-solid: #fff;
@@ -76,6 +88,12 @@
76
88
  --color-token-main-surface-primary: #181818;
77
89
  --color-token-bg-primary: #141414;
78
90
  --color-token-bg-secondary: color-mix(in srgb, #141414 92%, transparent);
91
+ --color-surface-tertiary: rgb(40 40 40);
92
+ --color-surface-elevated-secondary: rgb(45 45 45);
93
+ --color-border-subtle: rgb(255 255 255 / 4.2%);
94
+ --color-background-primary-ghost-hover: rgb(255 255 255 / 7.8%);
95
+ --color-chart-blue: #339cff;
96
+ --color-text-inverse: rgb(13 13 13);
79
97
  --color-background-elevated-primary-opaque: #363636;
80
98
  --color-background-panel: #232323;
81
99
  --color-background-primary-solid: #fff;
@@ -810,6 +828,68 @@ h1,
810
828
  background: var(--color-token-bg-primary);
811
829
  }
812
830
 
831
+ /* Native-token image editor: tokens arrive from RuntimeHost with the active Codex theme. */
832
+ .image-editor { position: relative; background: var(--color-token-bg-primary); }
833
+ .image-editor [hidden] { display: none !important; }
834
+ .image-editor-controls { position: sticky; z-index: 6; top: 0; background: var(--color-token-bg-primary); }
835
+ .image-editor-toolbar { position: relative; display: flex; justify-content: center; min-height: 48px; padding: 8px 16px; }
836
+ .image-edit-actions, .image-edit-mode { display: flex; min-width: 0; align-items: center; gap: 2px; padding: 2px; overflow: hidden; border-radius: 9999px; background: color-mix(in srgb, var(--color-surface-tertiary) 95%, transparent); box-shadow: 0 0 0 1px var(--color-border-subtle), var(--shadow-md); backdrop-filter: blur(4px); }
837
+ .image-edit-action, .image-edit-mode button { display: inline-flex; height: 28px; align-items: center; justify-content: center; gap: 4px; border: 1px solid transparent; border-radius: 999px; padding: 0 6px; background: transparent; color: var(--color-text-foreground); font-family: var(--font-sans-default); font-size: var(--text-base); font-weight: var(--font-weight-normal); line-height: 18px; white-space: nowrap; cursor: pointer; }
838
+ .image-edit-action:hover, .image-edit-action[aria-expanded="true"], .image-edit-mode button:hover { background: var(--color-background-primary-ghost-hover); }
839
+ .image-edit-action:focus-visible, .image-edit-mode button:focus-visible, .image-zoom-trigger:focus-visible, .image-edit-menu button:focus-visible { outline: 1px solid var(--color-border-focus); outline-offset: 1px; }
840
+ .image-edit-mode button:disabled { color: var(--color-text-foreground-tertiary); cursor: default; }
841
+ .image-edit-mode > span { min-width: 0; overflow: hidden; padding: 0 6px; color: var(--color-text-foreground-tertiary); font-size: var(--text-base); line-height: 18px; text-overflow: ellipsis; white-space: nowrap; }
842
+ .image-edit-mode button { width: 28px; padding: 0; flex: 0 0 auto; }
843
+ .image-edit-mode button > svg { width: 16px; height: 16px; fill: currentColor; }
844
+ .image-edit-mode button[aria-label="Redo"] > svg { transform: scaleX(-1); }
845
+ .image-edit-mode [data-edit-cancel] { color: var(--color-text-foreground-tertiary); }
846
+ .image-edit-mode [data-edit-cancel] > svg { width: 14px; height: 14px; }
847
+ .image-edit-mode [data-edit-send] { width: auto; padding: 0 6px; background: var(--color-chart-blue); color: var(--color-text-inverse); }
848
+ .image-edit-mode [data-edit-send]:hover { background: color-mix(in srgb, var(--color-chart-blue) 90%, transparent); }
849
+ .image-edit-mode [data-edit-send]:disabled { color: var(--color-text-inverse); opacity: .4; }
850
+ .image-zoom-trigger { position: absolute; top: 8px; right: 10px; height: 28px; display: inline-flex; align-items: center; gap: 2px; border: 1px solid transparent; border-radius: var(--radius-lg); padding: 0 8px; background: transparent; color: var(--color-text-foreground-tertiary); cursor: pointer; font-family: var(--font-sans-default); font-size: var(--text-sm); font-weight: var(--font-weight-normal); line-height: 20px; font-variant-numeric: tabular-nums; }
851
+ .image-zoom-trigger:hover, .image-zoom-trigger[aria-expanded="true"] { background: var(--color-background-primary-ghost-hover); }
852
+ .image-zoom-trigger [data-zoom-chevron] { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.5; opacity: .5; }
853
+ .image-edit-menu { position: absolute; z-index: 8; width: 160px; padding: 4px; border: 0; border-radius: var(--radius-xl); background: color-mix(in srgb, var(--color-surface-elevated-secondary) 90%, transparent); box-shadow: 0 0 0 .5px var(--color-border), var(--shadow-xl); backdrop-filter: blur(4px); }
854
+ .image-edit-menu button { display: flex; width: 100%; align-items: center; gap: 6px; border: 0; border-radius: var(--radius-lg); padding: 5px 8px; background: transparent; color: var(--color-text-foreground); font-family: var(--font-sans-default); font-size: var(--text-sm); font-weight: var(--font-weight-normal); line-height: var(--text-sm--line-height); text-align: left; cursor: pointer; }
855
+ .image-edit-menu button:hover { background: var(--color-background-primary-ghost-hover); }
856
+ .image-ratio-label { min-width: 0; flex: 1; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
857
+ .image-ratio-label small { margin: 0; color: var(--color-text-foreground-tertiary); font-size: inherit; }
858
+ .image-ratio-icon { display: block; width: 16px; height: 16px; flex: 0 0 auto; fill: currentColor; opacity: .75; }
859
+ .image-menu-check { width: 17px; height: 17px; flex: 0 0 auto; fill: currentColor; opacity: .75; }
860
+ .image-menu-check.is-hidden { visibility: hidden; }
861
+ .image-menu-separator { height: 1px; margin: 0 8px; background: var(--color-border); }
862
+ .image-zoom-menu { width: 136px; padding: 6px; border-radius: 10px; }
863
+ .image-zoom-menu button { border-radius: 6px; padding: 5px 5px 5px 8px; line-height: 20px; }
864
+ .image-editor-stage { position: relative; overflow: auto; touch-action: none; }
865
+ .image-editor-stage img { transform-origin: center; transition: transform 120ms ease-out; }
866
+ .image-edit-mask, .image-comment-layer { position: absolute; inset: 0; width: 100%; height: 100%; }
867
+ .image-edit-mask { cursor: crosshair; opacity: .5; color: var(--color-text-accent); }
868
+ .image-comment-layer { pointer-events: none; }
869
+ .image-comment-marker { position: absolute; display: flex; width: 30px; height: 30px; align-items: center; justify-content: center; border: 0; padding: 0; background: transparent; color: var(--color-text-accent); font-family: var(--font-sans-default); transform: translate(-50%, -50%); }
870
+ button.image-comment-marker { pointer-events: auto; cursor: pointer; }
871
+ .image-comment-marker > svg { position: absolute; inset: 0; width: 30px; height: 30px; fill: currentColor; overflow: visible; stroke: white; stroke-width: 1.65; }
872
+ .image-comment-marker > span { position: relative; z-index: 1; color: white; font-size: 10px; font-weight: 700; line-height: 10px; transform: translate(-1px, -1px); }
873
+ .image-comment-popover { position: absolute; z-index: 4; width: min(294px, calc(100% - 16px)); height: 44px; padding: 8px 8px 8px 16px; border: 0; border-radius: 22px; background: var(--color-surface-elevated-secondary); box-shadow: 0 0 0 1px var(--color-border-subtle), var(--shadow-md); }
874
+ .image-comment-popover input { width: 100%; height: 28px; border: 0; padding: 0; outline: 0; background: transparent; color: var(--color-token-input-foreground); font-family: var(--font-sans-default); font-size: var(--text-base); font-weight: var(--font-weight-normal); line-height: 21px; }
875
+ .image-comment-popover.is-existing-comment { height: 120px; padding: 8px 12px 8px 16px; }
876
+ .image-comment-popover textarea { display: block; width: 100%; height: 60px; resize: none; border: 0; padding: 2px 0 0; outline: 0; background: transparent; color: var(--color-token-input-foreground); font-family: var(--font-sans-default); font-size: var(--text-base); font-weight: var(--font-weight-normal); line-height: 24px; }
877
+ .image-comment-editor-footer { height: 44px; display: flex; align-items: end; justify-content: space-between; }
878
+ .image-comment-editor-actions { display: flex; gap: 6px; }
879
+ .image-comment-editor-footer button { height: 28px; border: 1px solid var(--color-border); border-radius: 999px; padding: 0 8px; background: color-mix(in srgb, var(--color-token-main-surface-primary) 96%, transparent); color: var(--color-text-foreground); font-family: var(--font-sans-default); font-size: var(--text-sm); font-weight: var(--font-weight-normal); line-height: 18px; cursor: pointer; }
880
+ .image-comment-editor-footer button:hover { background: var(--color-background-primary-ghost-hover); }
881
+ .image-comment-editor-footer .image-comment-delete { width: 28px; padding: 0; border-color: transparent; background: transparent; }
882
+ .image-comment-delete > svg { width: 20px; height: 20px; fill: currentColor; }
883
+ .image-comment-editor-footer .image-comment-save { border-color: transparent; background: var(--color-background-primary-solid); color: var(--color-text-button-primary); }
884
+ .image-comment-editor-footer button:disabled { cursor: default; opacity: .4; }
885
+ .image-brush-slider { position: absolute; z-index: 3; top: 50%; left: 16px; transform: translateY(-50%) rotate(-90deg); transform-origin: center; }
886
+ .image-brush-slider input { accent-color: var(--color-text-accent); }
887
+ .image-edit-menu[data-resize-menu] { top: 48px; left: 50%; transform: translateX(-50%); }
888
+ .image-zoom-menu { top: 48px; right: 10px; }
889
+ .image-edit-action > svg { width: 16px; height: 16px; flex: 0 0 auto; fill: currentColor; }
890
+ .image-edit-action > [data-native-image-icon="resize"] { width: 18px; height: 18px; }
891
+ @media (max-width: 520px) { .image-edit-action span { display: none; } .image-edit-action { width: 28px; padding: 0; } }
892
+
813
893
  .drawer-figure img {
814
894
  max-width: 100%;
815
895
  max-height: min(68vh, 720px);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "0.2.2",
3
+ "version": "1.0.1",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,7 +4,7 @@ import { createInterface } from "node:readline/promises";
4
4
  import { assertNodeSupported, MacPlatformAdapter } from "./platform.mjs";
5
5
  import { ensureConfig, ensureControlToken, readConfig } from "./config.mjs";
6
6
  import { createControlServer } from "./control-server.mjs";
7
- import { createInstallReporter } from "./install-progress.mjs";
7
+ import { createProgressReporter } from "./progress.mjs";
8
8
  import { createNpmAdapter } from "./npm-adapter.mjs";
9
9
  import { resolvePaths } from "./paths.mjs";
10
10
  import { RuntimeManager, writeServicePid } from "./runtime-manager.mjs";
@@ -119,7 +119,7 @@ export async function confirmRestartNow({ input = process.stdin, terminalOutput
119
119
  }
120
120
 
121
121
  async function installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion = process.versions.node }) {
122
- const reporter = createInstallReporter({ output });
122
+ const reporter = createProgressReporter({ output });
123
123
  reporter.heading("Installing MineCodex");
124
124
  let previousService = null;
125
125
  try {
@@ -167,21 +167,30 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
167
167
  }
168
168
  }
169
169
 
170
- async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
170
+ async function openCommand({ paths, platform, output, serviceProbe, fetchImpl }) {
171
171
  const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
172
+ let reporter = null;
172
173
  if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl, timeoutMs: 750 }))) {
173
174
  if (!(await serviceInstalled(platform, paths))) throw new Error("MineCodex is not installed. Run `mcx install` first.");
174
- await platform.startService(paths);
175
- if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) throw new Error("MineCodex service could not be started.");
175
+ reporter = createProgressReporter({ output });
176
+ reporter.heading("Opening MineCodex");
177
+ await reporter.step(1, 2, "Starting the background helper", async () => {
178
+ await platform.startService(paths);
179
+ if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) throw new Error("MineCodex service could not be started.");
180
+ });
176
181
  }
177
- const exchangeResponse = await authorizedFetch(paths, "/api/auth/exchange", {
178
- method: "POST",
179
- body: "{}",
180
- }, { fetchImpl });
181
- if (!exchangeResponse.ok) throw new Error(`MineCodex browser exchange failed: HTTP ${exchangeResponse.status}`);
182
- const { exchange } = await exchangeResponse.json();
183
- if (!exchange) throw new Error("MineCodex browser exchange did not return a short-lived value.");
184
- await platform.openBrowser(`${controlOrigin()}/auth?exchange=${encodeURIComponent(exchange)}`);
182
+ const openControlPanel = async () => {
183
+ const exchangeResponse = await authorizedFetch(paths, "/api/auth/exchange", {
184
+ method: "POST",
185
+ body: "{}",
186
+ }, { fetchImpl });
187
+ if (!exchangeResponse.ok) throw new Error(`MineCodex browser exchange failed: HTTP ${exchangeResponse.status}`);
188
+ const { exchange } = await exchangeResponse.json();
189
+ if (!exchange) throw new Error("MineCodex browser exchange did not return a short-lived value.");
190
+ await platform.openBrowser(`${controlOrigin()}/auth?exchange=${encodeURIComponent(exchange)}`);
191
+ };
192
+ if (reporter) await reporter.step(2, 2, "Opening the control panel", openControlPanel);
193
+ else await openControlPanel();
185
194
  }
186
195
 
187
196
  function featureStatusText(feature) {
@@ -239,95 +248,130 @@ async function statusCommand({ paths, platform, jsonOutput, output, fetchImpl })
239
248
  }
240
249
 
241
250
  async function restartCommand({ paths, output, fetchImpl }) {
242
- const response = await authorizedFetch(paths, "/api/restart", { method: "POST", body: "{}" }, { fetchImpl });
243
- if (!response.ok) throw new Error(`MineCodex restart failed: HTTP ${response.status}`);
244
- output("MineCodex restarted and saved plugin switches are now applied.");
251
+ const reporter = createProgressReporter({ output });
252
+ reporter.heading("Restarting MineCodex");
253
+ await reporter.step(1, 1, "Restarting Codex and applying plugin changes", async () => {
254
+ const response = await authorizedFetch(paths, "/api/restart", { method: "POST", body: "{}" }, { fetchImpl });
255
+ if (!response.ok) throw new Error(`MineCodex restart failed: HTTP ${response.status}`);
256
+ });
257
+ reporter.heading("MineCodex restarted and saved plugin switches are now applied.");
245
258
  }
246
259
 
247
260
  async function uninstallCommand({ paths, platform, purge, output, cliPath }) {
248
- platform.assertInstallSupported();
249
- const previousService = await serviceSnapshot(platform, paths);
261
+ const reporter = createProgressReporter({ output });
262
+ reporter.heading("Uninstalling MineCodex");
263
+ const previousService = await reporter.step(1, 2, "Checking the current installation", async () => {
264
+ platform.assertInstallSupported();
265
+ return serviceSnapshot(platform, paths);
266
+ });
250
267
  let wasManaged = false;
251
268
  try {
252
- await platform.removeService(paths);
253
- wasManaged = await platform.terminateOwnedCodex(paths);
254
- await platform.assertServiceStopped?.();
255
- await platform.assertOwnedCodexStopped?.(paths);
256
- if (purge) {
257
- const targets = [paths.supportDir, paths.notesDataDir, paths.imagesDataDir];
258
- for (const target of targets) {
259
- if (!path.isAbsolute(target) || target === paths.homeDir || target.length <= paths.homeDir.length + 2) {
260
- throw new Error(`Refusing unsafe purge target: ${target}`);
269
+ await reporter.step(2, 2, purge ? "Removing MineCodex and its data" : "Removing the MineCodex integration", async () => {
270
+ await platform.removeService(paths);
271
+ wasManaged = await platform.terminateOwnedCodex(paths);
272
+ await platform.assertServiceStopped?.();
273
+ await platform.assertOwnedCodexStopped?.(paths);
274
+ if (purge) {
275
+ const targets = [paths.supportDir, paths.notesDataDir, paths.imagesDataDir];
276
+ for (const target of targets) {
277
+ if (!path.isAbsolute(target) || target === paths.homeDir || target.length <= paths.homeDir.length + 2) {
278
+ throw new Error(`Refusing unsafe purge target: ${target}`);
279
+ }
261
280
  }
281
+ for (const target of targets) await rm(target, { recursive: true, force: true });
262
282
  }
263
- for (const target of targets) await rm(target, { recursive: true, force: true });
264
- output("MineCodex integration and MineCodex-owned data were removed.");
265
- } else {
266
- output("MineCodex integration was removed. Plugin data was preserved.");
267
- }
283
+ });
268
284
  } catch (error) {
269
285
  let rollbackError = null;
286
+ reporter.heading("Uninstall failed. Restoring the background helper.");
270
287
  try {
271
- await restoreService(platform, previousService, { cliPath, paths });
288
+ await reporter.step(1, 1, "Restoring the background helper", async () => {
289
+ await restoreService(platform, previousService, { cliPath, paths });
290
+ });
272
291
  } catch (failure) {
273
292
  rollbackError = failure;
274
293
  }
275
294
  throw appendRollbackError(error, rollbackError);
276
295
  }
277
- if (wasManaged) await platform.openNativeCodex();
278
- output("Codex generated images and files referenced by Notes were not modified.");
296
+ reporter.heading(purge
297
+ ? "MineCodex integration and MineCodex-owned data were removed."
298
+ : "MineCodex integration was removed. Plugin data was preserved.");
299
+ if (wasManaged) {
300
+ await reporter.step(1, 1, "Reopening Codex", async () => platform.openNativeCodex());
301
+ }
302
+ reporter.heading("Codex generated images and files referenced by Notes were not modified.");
279
303
  }
280
304
 
281
305
  async function updateCommand({ paths, platform, cliPath, output, npm = createNpmAdapter(), serviceProbe, fetchImpl }) {
282
- platform.assertInstallSupported();
283
- const latest = await npm.viewLatestVersion();
284
- if (!latest) throw new Error("Could not resolve the latest MineCodex version.");
306
+ const reporter = createProgressReporter({ output });
307
+ reporter.heading("Updating MineCodex");
308
+ const latest = await reporter.step(1, 5, "Checking for updates", async () => {
309
+ platform.assertInstallSupported();
310
+ const version = await npm.viewLatestVersion();
311
+ if (!version) throw new Error("Could not resolve the latest MineCodex version.");
312
+ return version;
313
+ });
285
314
  if (latest === packageMetadata.version) {
286
- output(`MineCodex ${packageMetadata.version} is already up to date.`);
315
+ reporter.heading(`MineCodex ${packageMetadata.version} is already up to date.`);
287
316
  return;
288
317
  }
289
- const wasInstalled = await serviceInstalled(platform, paths);
290
- const previousService = wasInstalled
291
- ? (await serviceSnapshot(platform, paths) ?? { running: true, plist: { exists: true } })
292
- : null;
293
- const rollback = await npm.createRollbackArchive(paths.packageRoot);
318
+ let wasInstalled;
319
+ let previousService;
320
+ const rollback = await reporter.step(2, 5, "Backing up the current version", async () => {
321
+ wasInstalled = await serviceInstalled(platform, paths);
322
+ previousService = wasInstalled
323
+ ? (await serviceSnapshot(platform, paths) ?? { running: true, plist: { exists: true } })
324
+ : null;
325
+ return npm.createRollbackArchive(paths.packageRoot);
326
+ });
294
327
  const rollbackPath = typeof rollback === "string" ? rollback : rollback.archivePath;
295
328
  if (!rollbackPath) throw new Error("Could not create a rollback archive for the installed MineCodex version.");
296
329
  try {
297
- if (wasInstalled && previousService?.running) {
298
- await platform.stopService(paths, { ignoreErrors: false });
299
- await platform.assertServiceStopped?.();
300
- }
301
- await npm.installGlobal("minecodex@latest");
302
- if (wasInstalled) {
303
- await platform.installService({ cliPath, paths, start: previousService.running });
304
- if (previousService.running) {
305
- const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
306
- const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
307
- if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
308
- || !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
309
- throw new Error("MineCodex service did not become ready after update.");
310
- }
311
- } else {
330
+ await reporter.step(3, 5, "Pausing the background helper", async () => {
331
+ if (wasInstalled && previousService?.running) {
332
+ await platform.stopService(paths, { ignoreErrors: false });
312
333
  await platform.assertServiceStopped?.();
313
334
  }
314
- }
315
- output(`MineCodex updated from ${packageMetadata.version} to ${latest}.`);
335
+ });
336
+ await reporter.step(4, 5, `Installing MineCodex ${latest}`, async () => {
337
+ await npm.installGlobal("minecodex@latest");
338
+ });
339
+ await reporter.step(5, 5, "Restoring the background helper", async () => {
340
+ if (wasInstalled) {
341
+ await platform.installService({ cliPath, paths, start: previousService.running });
342
+ if (previousService.running) {
343
+ const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
344
+ const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
345
+ if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
346
+ || !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
347
+ throw new Error("MineCodex service did not become ready after update.");
348
+ }
349
+ } else {
350
+ await platform.assertServiceStopped?.();
351
+ }
352
+ }
353
+ });
354
+ reporter.heading(`MineCodex updated from ${packageMetadata.version} to ${latest}.`);
316
355
  } catch (error) {
317
356
  let rollbackError = null;
357
+ reporter.heading("Update failed. Restoring the previous version.");
318
358
  try {
319
- await npm.installGlobal(rollbackPath);
320
- await restoreService(platform, previousService, { cliPath, paths });
321
- if (wasInstalled && previousService?.running) {
322
- const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
323
- const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
324
- if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
325
- || !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
326
- throw new Error("Rolled-back MineCodex service did not become ready.");
359
+ await reporter.step(1, 2, "Restoring the previous version", async () => {
360
+ await npm.installGlobal(rollbackPath);
361
+ });
362
+ await reporter.step(2, 2, "Restoring the background helper", async () => {
363
+ await restoreService(platform, previousService, { cliPath, paths });
364
+ if (wasInstalled && previousService?.running) {
365
+ const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
366
+ const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
367
+ if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
368
+ || !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
369
+ throw new Error("Rolled-back MineCodex service did not become ready.");
370
+ }
371
+ } else if (wasInstalled && !previousService?.running) {
372
+ await platform.assertServiceStopped?.();
327
373
  }
328
- } else if (wasInstalled && !previousService?.running) {
329
- await platform.assertServiceStopped?.();
330
- }
374
+ });
331
375
  } catch (failure) {
332
376
  rollbackError = failure;
333
377
  }
@@ -397,7 +441,7 @@ export async function runCli(argv, {
397
441
  if (["--help", "-h", "help"].includes(command)) return printHelp(output);
398
442
  if (["--version", "-v"].includes(command)) return output(packageMetadata.version);
399
443
  if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion });
400
- if (command === "open" || command === "gui") return openCommand({ paths, platform, serviceProbe, fetchImpl });
444
+ if (command === "open" || command === "gui") return openCommand({ paths, platform, output, serviceProbe, fetchImpl });
401
445
  if (command === "status") return statusCommand({ paths, platform, jsonOutput: args.includes("--json"), output, fetchImpl });
402
446
  if (command === "restart") return restartCommand({ paths, output, fetchImpl });
403
447
  if (command === "uninstall" || command === "remove") {
@@ -1,6 +1,6 @@
1
1
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
2
 
3
- function createInstallReporter({ output = console.log, terminalOutput = process.stdout } = {}) {
3
+ function createProgressReporter({ output = console.log, terminalOutput = process.stdout } = {}) {
4
4
  // 只有真实终端才显示 spinner;测试或脚本环境保持普通逐行文本。
5
5
  const interactive = output === console.log && Boolean(terminalOutput?.isTTY);
6
6
  let timer = null;
@@ -53,4 +53,4 @@ function createInstallReporter({ output = console.log, terminalOutput = process.
53
53
  };
54
54
  }
55
55
 
56
- export { createInstallReporter };
56
+ export { createProgressReporter };