create-principles-disciple 1.111.0 → 1.112.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.
@@ -14,6 +14,13 @@ import { migrateLegacyExtensionBackups, reservePdBackupDestination, resolvePdBac
14
14
  // installer package (create-principles-disciple). These are independently
15
15
  // versioned — comparing them caused a permanent false "update available".
16
16
  const NPM_REGISTRY_LATEST = 'https://registry.npmjs.org/principles-disciple/latest';
17
+ // Full update installs the bundled plugin from the INSTALLER package. The
18
+ // installer captures the plugin version at build time (bundle-plugin.mjs
19
+ // records it as `pd.bundledPluginVersion`). `/check` must compare the
20
+ // installed version against what the installer can ACTUALLY deliver, not the
21
+ // raw plugin registry latest, otherwise it promises a version the full update
22
+ // can never install → permanent false "update available" and no-op updates.
23
+ const NPM_REGISTRY_INSTALLER = 'https://registry.npmjs.org/create-principles-disciple/latest';
17
24
  const WORKSPACE_FILES = ['AGENTS.md', 'SOUL.md', 'USER.md', 'CLAUDE.md'];
18
25
  // Directories to skip during backup and diff. node_modules contains native
19
26
  // .node addons locked by the gateway/console processes (EPERM on copyfile),
@@ -287,45 +294,84 @@ function detectCodexInstall() {
287
294
  const pdCodexDir = path.join(os.homedir(), '.pd', 'codex');
288
295
  return fs.existsSync(codexHooks) || fs.existsSync(pdCodexDir);
289
296
  }
297
+ /**
298
+ * Fetch a registry `/latest` document.
299
+ *
300
+ * Returns parsed fields with runtime guards (rc-1/rc-2): never trusts unknown
301
+ * JSON. `bundledPluginVersion` is read from `pd.bundledPluginVersion` and
302
+ * only returned when it is a valid semver string.
303
+ */
304
+ async function fetchRegistryMetadata(url, label) {
305
+ const controller = new AbortController();
306
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
307
+ let response;
308
+ try {
309
+ response = await fetch(url, { signal: controller.signal });
310
+ if (!response.ok)
311
+ throw new Error(`${label}: HTTP ${response.status}`);
312
+ const rawData = await response.json();
313
+ if (!isRecord(rawData))
314
+ throw new Error(`Invalid registry response (${label})`);
315
+ if (typeof rawData.version !== 'string')
316
+ throw new Error(`Missing version (${label})`);
317
+ const out = { version: rawData.version };
318
+ if (isRecord(rawData.pd) && typeof rawData.pd.bundledPluginVersion === 'string') {
319
+ const bundled = rawData.pd.bundledPluginVersion;
320
+ if (semver.valid(bundled) !== null)
321
+ out.bundledPluginVersion = bundled;
322
+ }
323
+ return out;
324
+ }
325
+ finally {
326
+ clearTimeout(timeoutId);
327
+ }
328
+ }
290
329
  async function doCheckForUpdates(currentVersion) {
291
330
  try {
292
- const controller = new AbortController();
293
- const timeoutId = setTimeout(() => controller.abort(), 5000);
294
- let latestVersion = '';
295
- try {
296
- const response = await fetch(NPM_REGISTRY_LATEST, {
297
- signal: controller.signal,
298
- });
299
- if (!response.ok)
300
- throw new Error(`HTTP ${response.status}`);
301
- const rawData = await response.json();
302
- if (!isRecord(rawData))
303
- throw new Error('Invalid registry response');
304
- if (typeof rawData.version !== 'string')
305
- throw new Error('Invalid registry response: missing version');
306
- latestVersion = rawData.version;
307
- }
308
- finally {
309
- clearTimeout(timeoutId);
310
- }
311
- // Fetch release notes from GitHub for the latest version (best-effort,
312
- // non-blocking — if it fails, the UI still shows version info without notes).
331
+ // Fetch BOTH the plugin registry latest (for changelog + sync detection)
332
+ // and the installer's declared bundled plugin version (what a full update
333
+ // can actually deliver). rc-4: validate array/element shapes with guards.
334
+ const pluginResult = await fetchRegistryMetadata(NPM_REGISTRY_LATEST, 'Plugin registry check');
335
+ const pluginLatest = pluginResult.version;
336
+ const installerResult = await fetchRegistryMetadata(NPM_REGISTRY_INSTALLER, 'Installer registry check');
337
+ // The installer stamps the exact plugin version it bundles in
338
+ // `pd.bundledPluginVersion` (bundle-plugin.mjs). This is the version the
339
+ // full update ACTUALLY installs. Fall back to the plugin registry latest
340
+ // for old installers without the stamp.
341
+ const deliverableVersion = installerResult.bundledPluginVersion ?? pluginLatest;
342
+ // hasUpdate must compare against what we can actually install. If the
343
+ // installer is stale (its bundled plugin < plugin registry latest), we
344
+ // report the stale deliverable and surface the sync gap so the UI does
345
+ // not offer a version the update never installs.
346
+ const hasUpdate = semver.gt(deliverableVersion, currentVersion);
347
+ const syncPending = Boolean(pluginLatest) &&
348
+ Boolean(deliverableVersion) &&
349
+ semver.gt(pluginLatest, deliverableVersion);
350
+ // Fetch release notes from GitHub (best-effort, non-blocking).
313
351
  let changelog = '';
314
- try {
315
- const ghResponse = await fetch(`https://api.github.com/repos/csuzngjh/principles/releases/tags/v${latestVersion}`, { signal: AbortSignal.timeout(5000), headers: { Accept: 'application/vnd.github.v3+json' } });
316
- if (ghResponse.ok) {
317
- const ghData = await ghResponse.json();
318
- if (isRecord(ghData) && typeof ghData.body === 'string') {
319
- changelog = ghData.body;
352
+ const notesVersion = syncPending ? pluginLatest : deliverableVersion;
353
+ if (notesVersion) {
354
+ try {
355
+ const ghResponse = await fetch(`https://api.github.com/repos/csuzngjh/principles/releases/tags/v${notesVersion}`, { signal: AbortSignal.timeout(5000), headers: { Accept: 'application/vnd.github.v3+json' } });
356
+ if (ghResponse.ok) {
357
+ const ghData = await ghResponse.json();
358
+ if (isRecord(ghData) && typeof ghData.body === 'string') {
359
+ changelog = ghData.body;
360
+ }
320
361
  }
321
362
  }
363
+ catch { /* best-effort — changelog is optional */ }
322
364
  }
323
- catch { /* best-effort — changelog is optional */ }
324
365
  return {
325
- hasUpdate: semver.gt(latestVersion, currentVersion),
366
+ hasUpdate,
326
367
  currentVersion,
327
- latestVersion,
368
+ latestVersion: deliverableVersion,
328
369
  changelog,
370
+ // Newer plugin is published but the installer has not been republished
371
+ // to bundle it. UI shows this as an honest "sync in progress" notice
372
+ // instead of offering an uninstallable version.
373
+ pluginLatestVersion: pluginLatest,
374
+ syncPending,
329
375
  };
330
376
  }
331
377
  catch (error) {
@@ -560,7 +606,6 @@ async function doRollbackUpdate(options, workspaceDir) {
560
606
  // pd-cli). We download it directly and copy the pre-built dist/ directories.
561
607
  // This is seconds-fast (no npm install) and requires no CLI/npx — the entire
562
608
  // operation happens inside the console HTTP handler.
563
- const NPM_REGISTRY_INSTALLER = 'https://registry.npmjs.org/create-principles-disciple/latest';
564
609
  /**
565
610
  * Compare two dependency maps for meaningful differences.
566
611
  * Ignores `@principles/core` (always `file:./core` in bundled packages).
@@ -680,8 +725,36 @@ async function doInlineFullUpdate(workspaceDir) {
680
725
  if (tempDir && fs.existsSync(tempDir)) {
681
726
  fs.rmSync(tempDir, { recursive: true, force: true });
682
727
  }
683
- // 7. Record history
728
+ // 7. Version-advance check (drift guard). The full update installs the
729
+ // plugin bundled inside the installer. If the installer is stale (its
730
+ // bundled plugin is NOT newer than what is installed), the "update" is a
731
+ // no-op that merely rewrites the same version — recording it as success
732
+ // produced the confusing `1.209.0 → 1.209.0` history and a permanent
733
+ // false "update available". Detect that and fail loud (rc-9) instead of
734
+ // reporting a false success.
684
735
  const newVersion = readCurrentVersion(extDir) ?? toVersion ?? 'unknown';
736
+ const progressed = fromVersion !== 'unknown' &&
737
+ semver.valid(fromVersion) !== null &&
738
+ semver.valid(newVersion) !== null &&
739
+ semver.gt(newVersion, fromVersion);
740
+ if (!progressed) {
741
+ // Files were already rewritten to the same (or lower) version. Record a
742
+ // FAILED history entry so the operator sees why, and return a structured
743
+ // error with nextAction.
744
+ appendUpdateHistory(workspaceDir, {
745
+ fromVersion,
746
+ toVersion: newVersion,
747
+ success: false,
748
+ });
749
+ return {
750
+ success: false,
751
+ message: 'Installed version did not advance — the update source is stale (it bundles the same or an older plugin).',
752
+ reason: 'installer_bundle_stale',
753
+ nextAction: 'The published installer mirrors an older plugin. Contact the maintainer to republish the installer, or try again later when a newer installer is available.',
754
+ requiresRestart: false,
755
+ };
756
+ }
757
+ // 8. Record history (only for genuine version advancement)
685
758
  appendUpdateHistory(workspaceDir, {
686
759
  fromVersion,
687
760
  toVersion: newVersion,
@@ -886,6 +886,7 @@
886
886
  "version": "Version",
887
887
  "failed": "Failed",
888
888
  "checkError": "Check error",
889
+ "syncPending": "Version {{pluginVersion}} is published, but the installer is syncing (you can update to the version shown above for now). Please try again later.",
889
890
  "loadError": "Unable to load update data. You can try refreshing the page.",
890
891
  "checkFailed": "Update check failed. Please try again later.",
891
892
  "applyUpdate": "Update Now",
@@ -886,6 +886,7 @@
886
886
  "version": "版本",
887
887
  "failed": "失败",
888
888
  "checkError": "检查出错",
889
+ "syncPending": "最新版 {{pluginVersion}} 已发布,安装器同步中(当前可更新至上方列出的版本)。请稍后再试。",
889
890
  "loadError": "无法加载更新数据。你可以尝试刷新页面。",
890
891
  "checkFailed": "更新检查失败,请稍后重试。",
891
892
  "applyUpdate": "立即更新",
@@ -197,7 +197,7 @@ export function UpdatePage() {
197
197
  }
198
198
  // ── Loaded state ─────────────────────────────────────────────────────────
199
199
  const isUpToDate = statusData ? !statusData.hasUpdate : true;
200
- return (_jsxs(PageShell, { children: [_jsxs("div", { className: "animate-[pdFadeIn_400ms_ease-out]", children: [_jsx("div", { className: "font-mono text-[12px] tracking-[0.14em] text-ink-3 uppercase mb-3", children: t("pages.update.eyebrow") }), _jsx("h1", { className: "text-[29px] font-semibold tracking-tight text-ink mb-2", children: t("pages.update.title") }), _jsx("p", { className: "text-ink-3 text-[14px] max-w-[760px] leading-relaxed mb-7", children: t("pages.update.subtitle") }), _jsxs("section", { className: "mb-8", "aria-labelledby": "section-update-status", children: [_jsx(SectionTitle, { id: "section-update-status", children: t("pages.update.currentVersion") }), _jsxs("div", { className: "bg-panel border border-line rounded-[6px] p-5", children: [_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-ink-3 text-[13px]", children: t("pages.update.currentVersion") }), _jsx("span", { className: "font-mono text-[13px] text-ink", children: statusData?.currentVersion ?? "—" })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-ink-3 text-[13px]", children: t("pages.update.latestVersion") }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-mono text-[13px] text-ink", children: statusData?.latestVersion ?? "—" }), isUpToDate ? (_jsx("span", { className: "inline-flex items-center border border-green/35 text-green rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: t("pages.update.upToDate") })) : (_jsx("span", { className: "inline-flex items-center border border-amber/35 text-amber rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: t("pages.update.updateAvailable") }))] })] })] }), statusData?.error && (_jsxs("div", { className: "mt-4 pt-3 border-t border-line text-[12px] text-ink-4 font-mono", children: [t("pages.update.checkError"), ": ", statusData.error] })), statusData?.codexInstalled && (_jsx("div", { className: "mt-4 p-3 rounded-[4px] border border-amber/30 bg-amber/5", children: _jsx("p", { className: "text-[12px] text-amber leading-relaxed", children: t("pages.update.codexWarning") }) })), statusData?.changelog && statusData.hasUpdate && (_jsxs("div", { className: "mt-4 p-4 rounded-[4px] border border-line bg-surface/50", children: [_jsx("p", { className: "text-[12px] font-mono text-ink-3 uppercase tracking-wide mb-2", children: t("pages.update.whatsNew") }), _jsx("div", { className: "text-[13px] text-ink-2 leading-relaxed max-h-[300px] overflow-y-auto whitespace-pre-wrap", children: statusData.changelog })] })), _jsxs("div", { className: "mt-5 pt-4 border-t border-line flex items-center gap-3 flex-wrap", children: [_jsx("button", { type: "button", onClick: handleCheckForUpdates, disabled: checking || fullUpdating, className: "border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: checking ? t("pages.update.checking") : t("pages.update.checkForUpdates") }), !isUpToDate && (_jsx("button", { type: "button", onClick: () => setShowFullUpdateDialog(true), disabled: checking || fullUpdating, className: "bg-gov text-white rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-gov/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: fullUpdating ? (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), t("pages.update.updating")] })) : (t("pages.update.applyUpdate")) }))] }), updateResult && (_jsx("div", { className: `mt-4 p-4 rounded-[6px] border animate-[pdFadeIn_400ms_ease-out] ${updateResult.success ? 'bg-green/5 border-green/20' : 'bg-red/5 border-red/20'}`, children: _jsxs("div", { className: "flex items-start gap-3", children: [updateResult.success ? (_jsx(CheckCircle2, { className: "h-6 w-6 text-green shrink-0 mt-0.5 animate-[pdFadeIn_600ms_ease-out]", style: { transform: 'scale(1)', animationFillMode: 'both' } })) : (_jsx(XCircle, { className: "h-6 w-6 text-red shrink-0 mt-0.5" })), _jsxs("div", { className: "flex-1 min-w-0", children: [updateResult.success && updateResult.fromVersion && updateResult.newVersion && (_jsxs("div", { className: "flex items-center gap-2 mb-1.5", children: [_jsx("span", { className: "font-mono text-[13px] text-ink-3 line-through", children: updateResult.fromVersion }), _jsx(ArrowRight, { className: "h-3.5 w-3.5 text-green" }), _jsx("span", { className: "font-mono text-[13px] text-green font-medium", children: updateResult.newVersion })] })), _jsx("p", { className: `text-[13px] ${updateResult.success ? 'text-green' : 'text-red'}`, children: updateResult.message }), updateResult.success && updateResult.updatedFiles && updateResult.updatedFiles.length > 0 && (_jsx("p", { className: "mt-1 text-[12px] text-ink-4 font-mono", children: t("pages.update.filesUpdated", { count: updateResult.updatedFiles.length }) })), updateResult.success && !updateResult.requiresRestart && (_jsx("p", { className: "mt-2 text-[12px] font-mono text-ink-4", children: t("pages.update.restartHint") })), updateResult.success && updateResult.requiresRestart && (_jsx("p", { className: "mt-2 p-2 rounded-[3px] bg-amber/5 border border-amber/20 text-[12px] text-amber leading-relaxed", children: t("pages.update.fullUpdateRestartPrompt") })), updateResult.success && updateResult.partialUpdate && (_jsx("p", { className: "mt-1 text-[12px] text-amber leading-relaxed", children: t("pages.update.partialUpdateHint") })), !updateResult.success && updateResult.nextAction && (_jsx("p", { className: "mt-1 text-[12px] text-ink-4 font-mono leading-relaxed", children: updateResult.nextAction })), !updateResult.success && (_jsxs("div", { className: "mt-2 flex items-center gap-2", children: [/network|fetch|timeout|ECONNREFUSED|ENOTFOUND/i.test(updateResult.message) && (_jsx("p", { className: "text-[12px] text-ink-4 font-mono", children: t("pages.update.networkErrorHint") })), _jsx("button", { type: "button", onClick: handleApplyUpdate, disabled: updating, className: "text-[12px] text-red underline hover:text-red/80 disabled:opacity-50", children: t("pages.update.retry") })] }))] })] }) }))] })] }), _jsxs("section", { "aria-labelledby": "section-update-history", children: [_jsx(SectionTitle, { id: "section-update-history", children: t("pages.update.history") }), historyErrorReason ? (_jsxs("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: [t("pages.update.loadError"), " (", historyErrorReason, ")"] })) : historyData && historyData.updates.length > 0 ? (_jsx("div", { className: "space-y-[10px]", children: historyData.updates.map((entry) => (_jsx("article", { className: "bg-panel border border-line rounded-[6px] px-5 py-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "inline-flex items-center border border-line rounded-[2px] px-[7px] py-1 font-mono text-[11px] text-ink-3 bg-surface/80 uppercase", children: t("pages.update.version") }), _jsxs("span", { className: "font-mono text-[13px] text-ink", children: [entry.fromVersion, " \u2192 ", entry.toVersion] }), !entry.success && (_jsx("span", { className: "inline-flex items-center border border-red/35 text-red rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: t("pages.update.failed") }))] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-ink-3 text-[13px]", children: formatDate(entry.timestamp, locale) }), entry.backupPath && (_jsxs("button", { type: "button", onClick: () => setShowRollbackDialog(entry.backupPath), disabled: rollingBack || updating, className: "flex items-center gap-1 border border-line bg-surface text-ink-3 rounded-[3px] px-[8px] py-[4px] text-[11px] hover:text-ink hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", title: t("pages.update.rollback"), children: [_jsx(RotateCcw, { className: "h-3 w-3" }), t("pages.update.rollback")] }))] })] }) }, entry.id))) })) : (_jsx("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: t("pages.update.noHistory") }))] })] }), _jsx(AlertDialog, { open: showFullUpdateDialog, onOpenChange: setShowFullUpdateDialog, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t("pages.update.confirmFullUpdateTitle") }), _jsx(AlertDialogDescription, { children: t("pages.update.confirmFullUpdateDesc") })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t("common.cancel") }), _jsx(AlertDialogAction, { onClick: handleApplyFullUpdate, children: t("pages.update.applyFullUpdate") })] })] }) }), _jsx(AlertDialog, { open: showRollbackDialog !== null, onOpenChange: (open) => { if (!open)
200
+ return (_jsxs(PageShell, { children: [_jsxs("div", { className: "animate-[pdFadeIn_400ms_ease-out]", children: [_jsx("div", { className: "font-mono text-[12px] tracking-[0.14em] text-ink-3 uppercase mb-3", children: t("pages.update.eyebrow") }), _jsx("h1", { className: "text-[29px] font-semibold tracking-tight text-ink mb-2", children: t("pages.update.title") }), _jsx("p", { className: "text-ink-3 text-[14px] max-w-[760px] leading-relaxed mb-7", children: t("pages.update.subtitle") }), _jsxs("section", { className: "mb-8", "aria-labelledby": "section-update-status", children: [_jsx(SectionTitle, { id: "section-update-status", children: t("pages.update.currentVersion") }), _jsxs("div", { className: "bg-panel border border-line rounded-[6px] p-5", children: [_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-ink-3 text-[13px]", children: t("pages.update.currentVersion") }), _jsx("span", { className: "font-mono text-[13px] text-ink", children: statusData?.currentVersion ?? "—" })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-ink-3 text-[13px]", children: t("pages.update.latestVersion") }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-mono text-[13px] text-ink", children: statusData?.latestVersion ?? "—" }), isUpToDate ? (_jsx("span", { className: "inline-flex items-center border border-green/35 text-green rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: t("pages.update.upToDate") })) : (_jsx("span", { className: "inline-flex items-center border border-amber/35 text-amber rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: t("pages.update.updateAvailable") }))] })] })] }), statusData?.error && (_jsxs("div", { className: "mt-4 pt-3 border-t border-line text-[12px] text-ink-4 font-mono", children: [t("pages.update.checkError"), ": ", statusData.error] })), statusData?.syncPending && (_jsx("div", { className: "mt-4 p-3 rounded-[4px] border border-amber/30 bg-amber/5", children: _jsx("p", { className: "text-[12px] text-amber leading-relaxed", children: t("pages.update.syncPending", { pluginVersion: statusData.pluginLatestVersion ?? "" }) }) })), statusData?.codexInstalled && (_jsx("div", { className: "mt-4 p-3 rounded-[4px] border border-amber/30 bg-amber/5", children: _jsx("p", { className: "text-[12px] text-amber leading-relaxed", children: t("pages.update.codexWarning") }) })), statusData?.changelog && statusData.hasUpdate && (_jsxs("div", { className: "mt-4 p-4 rounded-[4px] border border-line bg-surface/50", children: [_jsx("p", { className: "text-[12px] font-mono text-ink-3 uppercase tracking-wide mb-2", children: t("pages.update.whatsNew") }), _jsx("div", { className: "text-[13px] text-ink-2 leading-relaxed max-h-[300px] overflow-y-auto whitespace-pre-wrap", children: statusData.changelog })] })), _jsxs("div", { className: "mt-5 pt-4 border-t border-line flex items-center gap-3 flex-wrap", children: [_jsx("button", { type: "button", onClick: handleCheckForUpdates, disabled: checking || fullUpdating, className: "border border-line bg-surface text-ink rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: checking ? t("pages.update.checking") : t("pages.update.checkForUpdates") }), !isUpToDate && (_jsx("button", { type: "button", onClick: () => setShowFullUpdateDialog(true), disabled: checking || fullUpdating, className: "bg-gov text-white rounded-[3px] px-[14px] py-[6px] text-[12.5px] hover:bg-gov/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-gov focus-visible:outline-offset-2", children: fullUpdating ? (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), t("pages.update.updating")] })) : (t("pages.update.applyUpdate")) }))] }), updateResult && (_jsx("div", { className: `mt-4 p-4 rounded-[6px] border animate-[pdFadeIn_400ms_ease-out] ${updateResult.success ? 'bg-green/5 border-green/20' : 'bg-red/5 border-red/20'}`, children: _jsxs("div", { className: "flex items-start gap-3", children: [updateResult.success ? (_jsx(CheckCircle2, { className: "h-6 w-6 text-green shrink-0 mt-0.5 animate-[pdFadeIn_600ms_ease-out]", style: { transform: 'scale(1)', animationFillMode: 'both' } })) : (_jsx(XCircle, { className: "h-6 w-6 text-red shrink-0 mt-0.5" })), _jsxs("div", { className: "flex-1 min-w-0", children: [updateResult.success && updateResult.fromVersion && updateResult.newVersion && (_jsxs("div", { className: "flex items-center gap-2 mb-1.5", children: [_jsx("span", { className: "font-mono text-[13px] text-ink-3 line-through", children: updateResult.fromVersion }), _jsx(ArrowRight, { className: "h-3.5 w-3.5 text-green" }), _jsx("span", { className: "font-mono text-[13px] text-green font-medium", children: updateResult.newVersion })] })), _jsx("p", { className: `text-[13px] ${updateResult.success ? 'text-green' : 'text-red'}`, children: updateResult.message }), updateResult.success && updateResult.updatedFiles && updateResult.updatedFiles.length > 0 && (_jsx("p", { className: "mt-1 text-[12px] text-ink-4 font-mono", children: t("pages.update.filesUpdated", { count: updateResult.updatedFiles.length }) })), updateResult.success && !updateResult.requiresRestart && (_jsx("p", { className: "mt-2 text-[12px] font-mono text-ink-4", children: t("pages.update.restartHint") })), updateResult.success && updateResult.requiresRestart && (_jsx("p", { className: "mt-2 p-2 rounded-[3px] bg-amber/5 border border-amber/20 text-[12px] text-amber leading-relaxed", children: t("pages.update.fullUpdateRestartPrompt") })), updateResult.success && updateResult.partialUpdate && (_jsx("p", { className: "mt-1 text-[12px] text-amber leading-relaxed", children: t("pages.update.partialUpdateHint") })), !updateResult.success && updateResult.nextAction && (_jsx("p", { className: "mt-1 text-[12px] text-ink-4 font-mono leading-relaxed", children: updateResult.nextAction })), !updateResult.success && (_jsxs("div", { className: "mt-2 flex items-center gap-2", children: [/network|fetch|timeout|ECONNREFUSED|ENOTFOUND/i.test(updateResult.message) && (_jsx("p", { className: "text-[12px] text-ink-4 font-mono", children: t("pages.update.networkErrorHint") })), _jsx("button", { type: "button", onClick: handleApplyUpdate, disabled: updating, className: "text-[12px] text-red underline hover:text-red/80 disabled:opacity-50", children: t("pages.update.retry") })] }))] })] }) }))] })] }), _jsxs("section", { "aria-labelledby": "section-update-history", children: [_jsx(SectionTitle, { id: "section-update-history", children: t("pages.update.history") }), historyErrorReason ? (_jsxs("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: [t("pages.update.loadError"), " (", historyErrorReason, ")"] })) : historyData && historyData.updates.length > 0 ? (_jsx("div", { className: "space-y-[10px]", children: historyData.updates.map((entry) => (_jsx("article", { className: "bg-panel border border-line rounded-[6px] px-5 py-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "inline-flex items-center border border-line rounded-[2px] px-[7px] py-1 font-mono text-[11px] text-ink-3 bg-surface/80 uppercase", children: t("pages.update.version") }), _jsxs("span", { className: "font-mono text-[13px] text-ink", children: [entry.fromVersion, " \u2192 ", entry.toVersion] }), !entry.success && (_jsx("span", { className: "inline-flex items-center border border-red/35 text-red rounded-[2px] px-[7px] py-1 font-mono text-[11px] uppercase", children: t("pages.update.failed") }))] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-ink-3 text-[13px]", children: formatDate(entry.timestamp, locale) }), entry.backupPath && (_jsxs("button", { type: "button", onClick: () => setShowRollbackDialog(entry.backupPath), disabled: rollingBack || updating, className: "flex items-center gap-1 border border-line bg-surface text-ink-3 rounded-[3px] px-[8px] py-[4px] text-[11px] hover:text-ink hover:border-line-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed", title: t("pages.update.rollback"), children: [_jsx(RotateCcw, { className: "h-3 w-3" }), t("pages.update.rollback")] }))] })] }) }, entry.id))) })) : (_jsx("div", { className: "text-ink-3 text-[13px] leading-relaxed py-3", children: t("pages.update.noHistory") }))] })] }), _jsx(AlertDialog, { open: showFullUpdateDialog, onOpenChange: setShowFullUpdateDialog, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t("pages.update.confirmFullUpdateTitle") }), _jsx(AlertDialogDescription, { children: t("pages.update.confirmFullUpdateDesc") })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t("common.cancel") }), _jsx(AlertDialogAction, { onClick: handleApplyFullUpdate, children: t("pages.update.applyFullUpdate") })] })] }) }), _jsx(AlertDialog, { open: showRollbackDialog !== null, onOpenChange: (open) => { if (!open)
201
201
  setShowRollbackDialog(null); }, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t("pages.update.confirmRollbackTitle") }), _jsx(AlertDialogDescription, { children: t("pages.update.confirmRollbackDesc") })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t("common.cancel") }), _jsx(AlertDialogAction, { onClick: () => { if (showRollbackDialog)
202
202
  handleRollback(showRollbackDialog); }, children: t("pages.update.rollback") })] })] }) }), rollbackResult && (_jsxs("div", { className: `fixed bottom-4 right-4 max-w-md p-3 rounded-[4px] text-[13px] z-50 ${rollbackResult.success ? 'bg-green/10 border border-green/20 text-green' : 'bg-red/10 border border-red/20 text-red'}`, children: [_jsx("p", { children: rollbackResult.message }), rollbackResult.success && (_jsx("p", { className: "mt-1 text-[12px] font-mono opacity-80", children: t("pages.update.restartHintAfterRollback") }))] }))] }));
203
203
  }
@@ -278,6 +278,10 @@ export interface UpdateStatusData {
278
278
  codexInstalled?: boolean;
279
279
  /** Release notes for the latest version (markdown, from GitHub Releases). */
280
280
  changelog?: string;
281
+ /** Newest plugin version published to npm (may exceed what the installer can deliver). */
282
+ pluginLatestVersion?: string;
283
+ /** True when a newer plugin is published but the installer has not been republished to bundle it. */
284
+ syncPending?: boolean;
281
285
  }
282
286
  export declare function validateUpdateStatus(v: unknown): UpdateStatusData | null;
283
287
  export interface UpdateHistoryEntryData {
@@ -759,6 +759,12 @@ export function validateUpdateStatus(v) {
759
759
  if (Object.hasOwn(v, 'changelog') && isString(v.changelog)) {
760
760
  result.changelog = v.changelog;
761
761
  }
762
+ if (Object.hasOwn(v, 'pluginLatestVersion') && isString(v.pluginLatestVersion)) {
763
+ result.pluginLatestVersion = v.pluginLatestVersion;
764
+ }
765
+ if (Object.hasOwn(v, 'syncPending') && typeof v.syncPending === 'boolean') {
766
+ result.syncPending = v.syncPending;
767
+ }
762
768
  return result;
763
769
  }
764
770
  export function validateUpdateHistoryEntry(v) {