tmux-ide 2.6.0 → 2.6.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/bin/cli.js +13 -5
- package/bin/cli.ts +1 -1
- package/bunfig.toml +4 -0
- package/package.json +11 -3
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/src/agent-explain.ts +270 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +336 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +195 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +428 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +113 -0
- package/packages/daemon/src/tui/detect/classify.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
- package/packages/daemon/src/tui/detect/manifest.ts +184 -0
- package/packages/daemon/src/tui/detect/manifests.ts +226 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +197 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +70 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +11 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +73 -0
- package/packages/daemon/src/tui/team/sessions.ts +344 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/postinstall.js +26 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed sync of the bundled Claude Code skill → `~/.claude/skills/tmux-ide`.
|
|
3
|
+
*
|
|
4
|
+
* `skill/SKILL.md` is the manual a coding agent loads to drive tmux-ide. It ships
|
|
5
|
+
* IN the package, but the copy that agents actually read lives in the user's
|
|
6
|
+
* `~/.claude/skills/` — so every install AND every update has to keep that copy
|
|
7
|
+
* fresh, or agents drive an old CLI. This module is that refresh: it copies the
|
|
8
|
+
* bundled SKILL.md into the tmux-ide skill dir with the version marker rewritten
|
|
9
|
+
* to the installed package version, and reports whether that was an install, an
|
|
10
|
+
* update, or a no-op.
|
|
11
|
+
*
|
|
12
|
+
* The dir is FULLY MANAGED: overwriting our own `~/.claude/skills/tmux-ide` is
|
|
13
|
+
* correct, and this NEVER touches anything outside it (user-authored skills in
|
|
14
|
+
* sibling dirs are safe). The version marker is line 2 of SKILL.md, an HTML
|
|
15
|
+
* comment ({@link VERSION_MARKER_RE}); {@link parseSkillVersion} reads it and
|
|
16
|
+
* {@link rewriteVersionMarker} substitutes it at copy time.
|
|
17
|
+
*
|
|
18
|
+
* The parse/render helpers are PURE and tested against fixtures; {@link syncSkill}
|
|
19
|
+
* is the thin io wrapper (tested with tmp dirs). Wired from three places — the
|
|
20
|
+
* npm `postinstall` (which re-implements the marker rewrite in plain JS since it
|
|
21
|
+
* can't import TS — see scripts/postinstall.js), `tmux-ide skill-sync` / the dev
|
|
22
|
+
* path of `tmux-ide update`, and `tmux-ide integration install claude`.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { getCurrentVersion } from "./update-check.ts";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Paths
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Claude Code's config dir: `TMUX_IDE_CLAUDE_DIR` when set (tests / per-run
|
|
36
|
+
* overrides), else `~/.claude`. Mirrors the {@link ../tui/integrations/claude.ts}
|
|
37
|
+
* settings override so both halves of the agent setup honor the same scratch dir.
|
|
38
|
+
*/
|
|
39
|
+
export function claudeDir(): string {
|
|
40
|
+
return process.env.TMUX_IDE_CLAUDE_DIR ?? join(homedir(), ".claude");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The fully-managed tmux-ide skill dir: `<claudeDir>/skills/tmux-ide`. */
|
|
44
|
+
export function skillTargetDir(): string {
|
|
45
|
+
return join(claudeDir(), "skills", "tmux-ide");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The installed SKILL.md path (inside {@link skillTargetDir}). */
|
|
49
|
+
export function skillTargetFile(): string {
|
|
50
|
+
return join(skillTargetDir(), "SKILL.md");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* io — the bundled SKILL.md that ships in the package. Resolved relative to this
|
|
55
|
+
* module, handling BOTH the esbuild-bundled CLI (this file inlines into
|
|
56
|
+
* `bin/cli.js`, so `here` is `bin/` and the skill is one level up) AND the dev
|
|
57
|
+
* source tree (`packages/daemon/src/lib/`, four levels below the repo root) — the
|
|
58
|
+
* same dual-candidate trick as {@link getCurrentVersion}. Returns the first that
|
|
59
|
+
* exists, else the bundled-layout guess.
|
|
60
|
+
*/
|
|
61
|
+
export function defaultSkillSource(): string {
|
|
62
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
const candidates = [
|
|
64
|
+
join(here, "../skill/SKILL.md"), // bundled bin/cli.js → repo root
|
|
65
|
+
join(here, "../../../../skill/SKILL.md"), // dev src/lib → repo root
|
|
66
|
+
];
|
|
67
|
+
return candidates.find((c) => existsSync(c)) ?? candidates[0]!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Pure — version marker
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The version marker line (an HTML comment on line 2 of SKILL.md). The sync
|
|
76
|
+
* machinery reads it to know which version an installed copy is, and rewrites it
|
|
77
|
+
* to the installed package version at copy time.
|
|
78
|
+
*/
|
|
79
|
+
export const VERSION_MARKER_RE = /<!--\s*tmux-ide-skill-version:\s*([^\s]+)\s*-->/;
|
|
80
|
+
|
|
81
|
+
/** PURE — render the marker line for a given version. */
|
|
82
|
+
export function versionMarker(version: string): string {
|
|
83
|
+
return `<!-- tmux-ide-skill-version: ${version} -->`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** PURE — the version in a SKILL.md body, or null when the marker is absent. */
|
|
87
|
+
export function parseSkillVersion(content: string): string | null {
|
|
88
|
+
const match = content.match(VERSION_MARKER_RE);
|
|
89
|
+
return match ? match[1]! : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* PURE — return `content` with its version marker rewritten to `version`. When no
|
|
94
|
+
* marker is present the content is returned unchanged (the bundled SKILL.md
|
|
95
|
+
* always carries one; this just never corrupts a hand-stripped file).
|
|
96
|
+
*/
|
|
97
|
+
export function rewriteVersionMarker(content: string, version: string): string {
|
|
98
|
+
if (!VERSION_MARKER_RE.test(content)) return content;
|
|
99
|
+
return content.replace(VERSION_MARKER_RE, versionMarker(version));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// io
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/** The version marker read off an installed SKILL.md, or null if absent/unreadable. */
|
|
107
|
+
export function installedSkillVersion(dir: string = skillTargetDir()): string | null {
|
|
108
|
+
const file = join(dir, "SKILL.md");
|
|
109
|
+
if (!existsSync(file)) return null;
|
|
110
|
+
try {
|
|
111
|
+
return parseSkillVersion(readFileSync(file, "utf-8"));
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What a {@link syncSkill} did. */
|
|
118
|
+
export interface SyncResult {
|
|
119
|
+
/** `installed` = target was absent; `updated` = content changed; `unchanged` = no-op. */
|
|
120
|
+
action: "installed" | "updated" | "unchanged";
|
|
121
|
+
/** The written SKILL.md path. */
|
|
122
|
+
path: string;
|
|
123
|
+
/** The installed version before an `updated` (the marker we replaced), if any. */
|
|
124
|
+
from?: string | null;
|
|
125
|
+
/** The version written into the copy's marker. */
|
|
126
|
+
to: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* io — copy the bundled SKILL.md into the managed skill dir with its version
|
|
131
|
+
* marker rewritten to `version`. Content-driven and idempotent: the RENDERED
|
|
132
|
+
* source (marker substituted) is compared byte-for-byte against the installed
|
|
133
|
+
* copy — identical → `unchanged` (no write), absent → `installed`, otherwise
|
|
134
|
+
* `updated`. Only ever writes inside {@link skillTargetDir}; nothing outside it
|
|
135
|
+
* is read or touched.
|
|
136
|
+
*/
|
|
137
|
+
export function syncSkill({
|
|
138
|
+
source = defaultSkillSource(),
|
|
139
|
+
version = getCurrentVersion(),
|
|
140
|
+
}: { source?: string; version?: string } = {}): SyncResult {
|
|
141
|
+
const rendered = rewriteVersionMarker(readFileSync(source, "utf-8"), version);
|
|
142
|
+
const dir = skillTargetDir();
|
|
143
|
+
const target = join(dir, "SKILL.md");
|
|
144
|
+
|
|
145
|
+
const existing = existsSync(target) ? readFileSync(target, "utf-8") : null;
|
|
146
|
+
if (existing === rendered) {
|
|
147
|
+
return { action: "unchanged", path: target, to: version };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
mkdirSync(dir, { recursive: true });
|
|
151
|
+
writeFileSync(target, rendered, "utf-8");
|
|
152
|
+
|
|
153
|
+
if (existing === null) return { action: "installed", path: target, to: version };
|
|
154
|
+
return { action: "updated", path: target, from: parseSkillVersion(existing), to: version };
|
|
155
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminals registry (G20-P1).
|
|
3
|
+
*
|
|
4
|
+
* JSON-backed persistence for tab-strip metadata — names, scopes, and
|
|
5
|
+
* kind. The actual PTY process lives in `PtyBridgeRegistry`; this
|
|
6
|
+
* store only persists what the UI needs to restore tab labels across
|
|
7
|
+
* reloads. Atomic writes (temp + rename) so a crash mid-write leaves
|
|
8
|
+
* the previous list intact.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import type { Terminal, TerminalKind } from "@tmux-ide/contracts";
|
|
14
|
+
|
|
15
|
+
const TERMINALS_FILE = ".tmux-ide/terminals.json";
|
|
16
|
+
const SAFE_ID = /^[A-Za-z0-9_-]+$/u;
|
|
17
|
+
|
|
18
|
+
function path(dir: string): string {
|
|
19
|
+
return join(dir, TERMINALS_FILE);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ensureDir(dir: string): void {
|
|
23
|
+
mkdirSync(dirname(path(dir)), { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Load the full list for a session. Missing file → empty list. */
|
|
27
|
+
export function loadTerminals(dir: string): Terminal[] {
|
|
28
|
+
const file = path(dir);
|
|
29
|
+
if (!existsSync(file)) return [];
|
|
30
|
+
try {
|
|
31
|
+
const body = readFileSync(file, "utf-8");
|
|
32
|
+
const parsed = JSON.parse(body) as { terminals?: unknown };
|
|
33
|
+
if (!parsed.terminals || !Array.isArray(parsed.terminals)) return [];
|
|
34
|
+
return parsed.terminals.filter((t): t is Terminal => isTerminal(t)).map((t) => ({ ...t }));
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTerminal(value: unknown): value is Terminal {
|
|
41
|
+
if (!value || typeof value !== "object") return false;
|
|
42
|
+
const v = value as Record<string, unknown>;
|
|
43
|
+
return (
|
|
44
|
+
typeof v.id === "string" &&
|
|
45
|
+
SAFE_ID.test(v.id) &&
|
|
46
|
+
typeof v.projectId === "string" &&
|
|
47
|
+
typeof v.scopeId === "string" &&
|
|
48
|
+
typeof v.name === "string" &&
|
|
49
|
+
(v.kind === "shell" || v.kind === "setup" || v.kind === "run" || v.kind === "teardown") &&
|
|
50
|
+
typeof v.createdAt === "string" &&
|
|
51
|
+
typeof v.updatedAt === "string"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeAtomic(dir: string, terminals: Terminal[]): void {
|
|
56
|
+
ensureDir(dir);
|
|
57
|
+
const file = path(dir);
|
|
58
|
+
const tmp = `${file}.tmp`;
|
|
59
|
+
writeFileSync(tmp, JSON.stringify({ terminals }, null, 2) + "\n");
|
|
60
|
+
renameSync(tmp, file);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CreateInput {
|
|
64
|
+
id: string;
|
|
65
|
+
projectId: string;
|
|
66
|
+
scopeId: string;
|
|
67
|
+
name: string;
|
|
68
|
+
kind: TerminalKind;
|
|
69
|
+
scripted?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Insert OR replace a terminal record by id. Idempotent — calling
|
|
73
|
+
* `createTerminal` twice with the same deterministic id is a no-op
|
|
74
|
+
* on the canonical fields and updates `updatedAt`. */
|
|
75
|
+
export function upsertTerminal(dir: string, input: CreateInput): Terminal {
|
|
76
|
+
if (!SAFE_ID.test(input.id)) {
|
|
77
|
+
throw new Error(`invalid terminal id "${input.id}"`);
|
|
78
|
+
}
|
|
79
|
+
const existing = loadTerminals(dir);
|
|
80
|
+
const idx = existing.findIndex((t) => t.id === input.id);
|
|
81
|
+
const now = new Date().toISOString();
|
|
82
|
+
const next: Terminal = {
|
|
83
|
+
id: input.id,
|
|
84
|
+
projectId: input.projectId,
|
|
85
|
+
scopeId: input.scopeId,
|
|
86
|
+
name: input.name,
|
|
87
|
+
kind: input.kind,
|
|
88
|
+
createdAt: existing[idx]?.createdAt ?? now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
...(input.scripted ? { scripted: true } : {}),
|
|
91
|
+
};
|
|
92
|
+
if (idx === -1) existing.push(next);
|
|
93
|
+
else existing[idx] = next;
|
|
94
|
+
writeAtomic(dir, existing);
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function renameTerminal(dir: string, id: string, name: string): Terminal | null {
|
|
99
|
+
const all = loadTerminals(dir);
|
|
100
|
+
const idx = all.findIndex((t) => t.id === id);
|
|
101
|
+
if (idx === -1) return null;
|
|
102
|
+
const trimmed = name.trim();
|
|
103
|
+
if (!trimmed) throw new Error("name required");
|
|
104
|
+
const next: Terminal = {
|
|
105
|
+
...all[idx]!,
|
|
106
|
+
name: trimmed,
|
|
107
|
+
updatedAt: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
all[idx] = next;
|
|
110
|
+
writeAtomic(dir, all);
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function deleteTerminal(dir: string, id: string): boolean {
|
|
115
|
+
const all = loadTerminals(dir);
|
|
116
|
+
const next = all.filter((t) => t.id !== id);
|
|
117
|
+
if (next.length === all.length) return false;
|
|
118
|
+
writeAtomic(dir, next);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function findTerminal(dir: string, id: string): Terminal | null {
|
|
123
|
+
const all = loadTerminals(dir);
|
|
124
|
+
return all.find((t) => t.id === id) ?? null;
|
|
125
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in update check — "you're on an old tmux-ide" surfaced where
|
|
3
|
+
* everyone already looks: the dock.
|
|
4
|
+
*
|
|
5
|
+
* Once a day the chrome updater asks npm for the latest published version, caches
|
|
6
|
+
* the answer in `~/.tmux-ide/update-check.json` (overridable via `TMUX_IDE_HOME`,
|
|
7
|
+
* consistent with {@link ../tui/chrome/welcome.ts}), and — when the cached latest
|
|
8
|
+
* is newer than what's running — every surface reads that ONE cache to decide
|
|
9
|
+
* whether to nudge: the dock's `⬆ v<latest>` segment, a one-time toast per
|
|
10
|
+
* version, `tmux-ide doctor`, the actions menu, and the `tmux-ide` start hint.
|
|
11
|
+
*
|
|
12
|
+
* The split, as elsewhere: {@link compareSemver} / {@link isNewer} /
|
|
13
|
+
* {@link shouldCheck} / {@link parseRegistryResponse} are PURE (unit-tested, never
|
|
14
|
+
* throw); {@link fetchLatestVersion}, the cache read/write, and the throttled
|
|
15
|
+
* {@link runUpdateCheck} are the thin — and strictly OFFLINE-SAFE — io wrappers.
|
|
16
|
+
* Nothing here ever throws into the updater loop: a failed fetch, an unreadable
|
|
17
|
+
* cache, or a garbage registry body all degrade to "no update known".
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
/** The npm dist-tag endpoint that returns `{ "version": "x.y.z", … }`. */
|
|
25
|
+
export const REGISTRY_URL = "https://registry.npmjs.org/tmux-ide/latest";
|
|
26
|
+
|
|
27
|
+
/** Re-check no more than once per this window (24h). */
|
|
28
|
+
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
/** The cheap, cache-derived answer every surface reads. */
|
|
31
|
+
export interface UpdateStatus {
|
|
32
|
+
/** The latest published version the last check saw, or null when unknown. */
|
|
33
|
+
latest: string | null;
|
|
34
|
+
/** True when {@link latest} is strictly newer than the running version. */
|
|
35
|
+
updateAvailable: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The on-disk cache shape (`~/.tmux-ide/update-check.json`). */
|
|
39
|
+
export interface UpdateCache {
|
|
40
|
+
/** Epoch ms of the last network check (throttles {@link shouldCheck}). */
|
|
41
|
+
lastCheckedAt: number | null;
|
|
42
|
+
/** The latest version that check saw, or null. */
|
|
43
|
+
latest: string | null;
|
|
44
|
+
/** Versions we've already toasted about (one-time-per-version debounce). */
|
|
45
|
+
notified?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Pure
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
interface ParsedSemver {
|
|
53
|
+
nums: [number, number, number];
|
|
54
|
+
/** The prerelease tag (`1.0.0-rc.1` → `rc.1`), or "" for a release. */
|
|
55
|
+
pre: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* PURE — split `[v]MAJOR.MINOR.PATCH[-prerelease][+build]` into its numeric core
|
|
60
|
+
* and prerelease tag. Lenient: a leading `v`, build metadata (`+…`), and missing
|
|
61
|
+
* or non-numeric core parts are all tolerated (coerced to 0), so a malformed
|
|
62
|
+
* version compares as `0.0.0` instead of throwing.
|
|
63
|
+
*/
|
|
64
|
+
function parseSemver(version: string): ParsedSemver {
|
|
65
|
+
const core = version.trim().replace(/^v/i, "").split("+")[0] ?? "";
|
|
66
|
+
const dash = core.indexOf("-");
|
|
67
|
+
const main = dash === -1 ? core : core.slice(0, dash);
|
|
68
|
+
const pre = dash === -1 ? "" : core.slice(dash + 1);
|
|
69
|
+
const parts = main.split(".");
|
|
70
|
+
const num = (i: number): number => {
|
|
71
|
+
const n = Number.parseInt(parts[i] ?? "", 10);
|
|
72
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
73
|
+
};
|
|
74
|
+
return { nums: [num(0), num(1), num(2)], pre };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* PURE — order two semver strings: `-1` if `a < b`, `1` if `a > b`, else `0`.
|
|
79
|
+
*
|
|
80
|
+
* Numeric core (major, minor, patch) compares field-by-field, so `2.10.0` sorts
|
|
81
|
+
* ABOVE `2.6.0` (numeric, not lexical). Prerelease follows semver's rule: a
|
|
82
|
+
* release outranks a prerelease of the same core (`1.0.0` > `1.0.0-rc`); two
|
|
83
|
+
* prereleases fall back to a plain lexical compare of their tags (a simple, good-
|
|
84
|
+
* enough ordering — we never need to rank `rc.2` vs `rc.10` precisely).
|
|
85
|
+
*/
|
|
86
|
+
export function compareSemver(a: string, b: string): -1 | 0 | 1 {
|
|
87
|
+
const pa = parseSemver(a);
|
|
88
|
+
const pb = parseSemver(b);
|
|
89
|
+
for (let i = 0; i < 3; i++) {
|
|
90
|
+
if (pa.nums[i]! !== pb.nums[i]!) return pa.nums[i]! < pb.nums[i]! ? -1 : 1;
|
|
91
|
+
}
|
|
92
|
+
if (pa.pre === pb.pre) return 0;
|
|
93
|
+
if (pa.pre === "") return 1; // release > prerelease of same core
|
|
94
|
+
if (pb.pre === "") return -1;
|
|
95
|
+
return pa.pre < pb.pre ? -1 : 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** PURE — is `latest` strictly newer than `current`? */
|
|
99
|
+
export function isNewer(latest: string, current: string): boolean {
|
|
100
|
+
return compareSemver(latest, current) === 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* PURE — should we hit the network again? True when we've never checked
|
|
105
|
+
* (`lastCheckedAt` null) or the last check was at least {@link CHECK_INTERVAL_MS}
|
|
106
|
+
* ago. A future `lastCheckedAt` (clock skew) reads as "recent" and skips — the
|
|
107
|
+
* daily tick self-heals once the clock passes it.
|
|
108
|
+
*/
|
|
109
|
+
export function shouldCheck(lastCheckedAt: number | null, nowMs: number): boolean {
|
|
110
|
+
if (lastCheckedAt === null) return true;
|
|
111
|
+
return nowMs - lastCheckedAt >= CHECK_INTERVAL_MS;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* PURE — pull the `version` string out of a raw npm dist-tag response body.
|
|
116
|
+
* Returns null (never throws) for malformed JSON, a non-object, or a
|
|
117
|
+
* missing/empty/non-string `version`.
|
|
118
|
+
*/
|
|
119
|
+
export function parseRegistryResponse(json: string): string | null {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(json) as unknown;
|
|
122
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
123
|
+
const version = (parsed as { version?: unknown }).version;
|
|
124
|
+
return typeof version === "string" && version.length > 0 ? version : null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* PURE — derive the {@link UpdateStatus} for a running version from a (possibly
|
|
132
|
+
* null) cached latest. The single place "is there an update?" is decided, so
|
|
133
|
+
* every surface agrees.
|
|
134
|
+
*/
|
|
135
|
+
export function deriveStatus(latest: string | null, currentVersion: string): UpdateStatus {
|
|
136
|
+
return {
|
|
137
|
+
latest,
|
|
138
|
+
updateAvailable: latest !== null && isNewer(latest, currentVersion),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// io — cache + network (all offline-safe, never throw)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Absolute path to the cache file: `<home>/update-check.json`, where `<home>` is
|
|
148
|
+
* `TMUX_IDE_HOME` when set (tests / per-run overrides), else `~/.tmux-ide` — the
|
|
149
|
+
* same home resolution the welcome marker uses.
|
|
150
|
+
*/
|
|
151
|
+
export function updateCachePath(): string {
|
|
152
|
+
const home = process.env.TMUX_IDE_HOME ?? join(homedir(), ".tmux-ide");
|
|
153
|
+
return join(home, "update-check.json");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** io — read + parse the cache, or null when absent/unreadable/malformed. */
|
|
157
|
+
export function readUpdateCache(): UpdateCache | null {
|
|
158
|
+
const path = updateCachePath();
|
|
159
|
+
if (!existsSync(path)) return null;
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
162
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
163
|
+
const obj = parsed as Record<string, unknown>;
|
|
164
|
+
const lastCheckedAt = typeof obj.lastCheckedAt === "number" ? obj.lastCheckedAt : null;
|
|
165
|
+
const latest = typeof obj.latest === "string" && obj.latest.length > 0 ? obj.latest : null;
|
|
166
|
+
const notified = Array.isArray(obj.notified)
|
|
167
|
+
? obj.notified.filter((v): v is string => typeof v === "string")
|
|
168
|
+
: undefined;
|
|
169
|
+
return { lastCheckedAt, latest, ...(notified ? { notified } : {}) };
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** io — write the cache (creating `<home>` if needed). Best-effort, never throws. */
|
|
176
|
+
export function writeUpdateCache(cache: UpdateCache): void {
|
|
177
|
+
const path = updateCachePath();
|
|
178
|
+
try {
|
|
179
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
180
|
+
writeFileSync(path, JSON.stringify(cache));
|
|
181
|
+
} catch {
|
|
182
|
+
// an unwritable cache just means we re-check next tick — never fatal
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* io — ask npm for the latest published version, or null on ANY failure (offline,
|
|
188
|
+
* timeout, non-200, garbage body). Aborts after `timeoutMs` so a slow registry
|
|
189
|
+
* can never stall the updater tick. This is the ONLY network call in the module.
|
|
190
|
+
*/
|
|
191
|
+
export async function fetchLatestVersion(timeoutMs = 3000): Promise<string | null> {
|
|
192
|
+
const controller = new AbortController();
|
|
193
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(REGISTRY_URL, { signal: controller.signal });
|
|
196
|
+
if (!res.ok) return null;
|
|
197
|
+
return parseRegistryResponse(await res.text());
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The running tmux-ide version, read from the nearest `package.json`. Handles
|
|
207
|
+
* BOTH the bundled CLI (`bin/cli.js`, so the repo-root package.json is one level
|
|
208
|
+
* up) AND the dev source tree (this file at `packages/daemon/src/lib/`, so the
|
|
209
|
+
* root is four levels up). The workspace `packages/daemon/package.json` is
|
|
210
|
+
* deliberately NOT a candidate — it carries a placeholder version. Falls back to
|
|
211
|
+
* `"0.0.0"` when nothing is readable (so an unknown version simply never shows an
|
|
212
|
+
* update rather than crashing).
|
|
213
|
+
*/
|
|
214
|
+
export function getCurrentVersion(): string {
|
|
215
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
216
|
+
const candidates = [
|
|
217
|
+
join(here, "../package.json"), // bundled bin/cli.js → repo root
|
|
218
|
+
join(here, "../../../../package.json"), // dev src/lib → repo root
|
|
219
|
+
];
|
|
220
|
+
for (const candidate of candidates) {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf-8")) as { version?: unknown };
|
|
223
|
+
if (typeof parsed.version === "string" && parsed.version.length > 0) return parsed.version;
|
|
224
|
+
} catch {
|
|
225
|
+
// try the next candidate
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return "0.0.0";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The cheap, network-FREE status read every surface uses: derive
|
|
233
|
+
* {@link UpdateStatus} from the cache against the current version. `now`/
|
|
234
|
+
* `currentVersion` are injectable for tests; the defaults read the live clock +
|
|
235
|
+
* package version.
|
|
236
|
+
*/
|
|
237
|
+
export function getUpdateStatus({
|
|
238
|
+
currentVersion = getCurrentVersion(),
|
|
239
|
+
}: { now?: number; currentVersion?: string } = {}): UpdateStatus {
|
|
240
|
+
const cache = readUpdateCache();
|
|
241
|
+
return deriveStatus(cache?.latest ?? null, currentVersion);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* io — the throttled network refresh (called fire-and-forget from the updater
|
|
246
|
+
* tick). Does nothing if the last check was inside {@link CHECK_INTERVAL_MS};
|
|
247
|
+
* otherwise fetches the latest version and rewrites the cache with a fresh
|
|
248
|
+
* `lastCheckedAt`. On a failed fetch it still stamps `lastCheckedAt` (so we don't
|
|
249
|
+
* hammer a flaky/offline registry every tick) while KEEPING the previously known
|
|
250
|
+
* `latest`. Never throws.
|
|
251
|
+
*/
|
|
252
|
+
export async function runUpdateCheck({ now = Date.now() }: { now?: number } = {}): Promise<void> {
|
|
253
|
+
const cache = readUpdateCache();
|
|
254
|
+
if (!shouldCheck(cache?.lastCheckedAt ?? null, now)) return;
|
|
255
|
+
const fetched = await fetchLatestVersion();
|
|
256
|
+
writeUpdateCache({
|
|
257
|
+
lastCheckedAt: now,
|
|
258
|
+
latest: fetched ?? cache?.latest ?? null,
|
|
259
|
+
...(cache?.notified ? { notified: cache.notified } : {}),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The tick's entry point: return the cheap cache-derived {@link UpdateStatus} NOW,
|
|
265
|
+
* and (when `enabled`) kick off the throttled network refresh in the background so
|
|
266
|
+
* the NEXT tick sees a fresh answer. Disabled (`updates.check: false`) → always
|
|
267
|
+
* "no update", no network. The background refresh is deliberately un-awaited: the
|
|
268
|
+
* tick must never block on the registry.
|
|
269
|
+
*/
|
|
270
|
+
export function maybeCheckForUpdate({
|
|
271
|
+
enabled,
|
|
272
|
+
now = Date.now(),
|
|
273
|
+
currentVersion = getCurrentVersion(),
|
|
274
|
+
}: {
|
|
275
|
+
enabled: boolean;
|
|
276
|
+
now?: number;
|
|
277
|
+
currentVersion?: string;
|
|
278
|
+
}): UpdateStatus {
|
|
279
|
+
if (!enabled) return { latest: null, updateAvailable: false };
|
|
280
|
+
const status = getUpdateStatus({ now, currentVersion });
|
|
281
|
+
void runUpdateCheck({ now }).catch(() => {});
|
|
282
|
+
return status;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* io — record that we've toasted about `version` and report whether this is the
|
|
287
|
+
* FIRST time (so the caller should actually toast). Persisted in the cache file's
|
|
288
|
+
* `notified` list so the one-time guarantee survives updater restarts — the old
|
|
289
|
+
* in-memory notify debounce would re-fire on every respawn. Best-effort: if the
|
|
290
|
+
* cache can't be written we still return the in-memory verdict.
|
|
291
|
+
*/
|
|
292
|
+
export function markUpdateNotified(version: string): boolean {
|
|
293
|
+
const cache = readUpdateCache() ?? { lastCheckedAt: null, latest: null };
|
|
294
|
+
const notified = cache.notified ?? [];
|
|
295
|
+
if (notified.includes(version)) return false;
|
|
296
|
+
writeUpdateCache({ ...cache, notified: [...notified, version] });
|
|
297
|
+
return true;
|
|
298
|
+
}
|