@monotykamary/localterm-server 2.56.3 → 2.58.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.
Files changed (39) hide show
  1. package/dist/constants.d.ts +10 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +22 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/font-store.d.ts +36 -0
  6. package/dist/font-store.d.ts.map +1 -0
  7. package/dist/font-store.js +126 -0
  8. package/dist/font-store.js.map +1 -0
  9. package/dist/index.d.ts +19 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +110 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/protocol.d.ts +3 -2
  14. package/dist/protocol.d.ts.map +1 -1
  15. package/dist/protocol.js +1 -1
  16. package/dist/protocol.js.map +1 -1
  17. package/dist/schemas.d.ts +39 -0
  18. package/dist/schemas.d.ts.map +1 -1
  19. package/dist/schemas.js +83 -1
  20. package/dist/schemas.js.map +1 -1
  21. package/dist/terminal-fonts.d.ts +13 -0
  22. package/dist/terminal-fonts.d.ts.map +1 -0
  23. package/dist/terminal-fonts.js +60 -0
  24. package/dist/terminal-fonts.js.map +1 -0
  25. package/dist/types.d.ts +4 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/update-check-store.d.ts +54 -0
  28. package/dist/update-check-store.d.ts.map +1 -0
  29. package/dist/update-check-store.js +208 -0
  30. package/dist/update-check-store.js.map +1 -0
  31. package/dist/utils/read-server-version.d.ts +2 -0
  32. package/dist/utils/read-server-version.d.ts.map +1 -0
  33. package/dist/utils/read-server-version.js +17 -0
  34. package/dist/utils/read-server-version.js.map +1 -0
  35. package/dist/utils/semver-compare.d.ts +2 -0
  36. package/dist/utils/semver-compare.d.ts.map +1 -0
  37. package/dist/utils/semver-compare.js +29 -0
  38. package/dist/utils/semver-compare.js.map +1 -0
  39. package/package.json +5 -1
package/dist/index.js CHANGED
@@ -21,11 +21,13 @@ import { detectWithExplicitPort } from "./cdp/discover-explicit-endpoint.js";
21
21
  import { DaemonConfigStore } from "./daemon-config-store.js";
22
22
  import { formatCandidates, resolveCandidates, resolveCompletionContext, } from "./completion/index.js";
23
23
  import { z } from "zod";
24
- import { ACTIVITY_DIRNAME, ACTIVITY_REFRESH_DEBOUNCE_MS, ACTIVITY_WATCHED_PROGRAMS, AUTOMATION_EVENT_DEBOUNCE_MS, AUTOMATION_RECONCILE_MIN_DOWNTIME_MS, AUTOMATION_RUN_QUERY_PARAM, AUTOMATION_WATCH_DEBOUNCE_MS, AUTOMATION_WATCH_POST_RUN_GRACE_MS, AUTOMATION_WEBHOOK_DEBOUNCE_MS, DEFAULT_HOST, DEFAULT_PORT, FRIENDLY_HOSTNAME, GIT_DIRTY_THROTTLE_MS, GIT_MAX_REF_LENGTH, HTTP_STATUS_ACCEPTED, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_CONFLICT, HTTP_STATUS_CREATED, HTTP_STATUS_NOT_FOUND, MAX_AUTOMATIONS, MAX_PROCESSES, MAX_SECRETS, MS_PER_MINUTE, PROCESSES_FILENAME, SECRETS_FILENAME, THEMES_FILENAME, SECRETS_SHIMS_DIRNAME, SECRET_EXPORT_SCRYPT_WORK_FACTOR, AUTOMATION_AGENT_SESSIONS_DIRNAME, SERVER_STOP_GRACE_MS, SESSION_ID_QUERY_PARAM, SESSION_ACTIVITY_WINDOW_MS, WINDOW_ID_QUERY_PARAM, WAIT_DEFAULT_TIMEOUT_MS, WS_BACKPRESSURE_THRESHOLD_BYTES, WS_CLOSE_BACKPRESSURE, WS_CLOSE_CAPACITY_REACHED, WS_CLOSE_POLICY_VIOLATION, WS_HEARTBEAT_GRACE_MS, WS_HEARTBEAT_INTERVAL_MS, WS_HEARTBEAT_TIMEOUT_MS, WS_READY_STATE_OPEN, AUTH_SECRET_FILENAME, AUTH_COOKIE_NAME, } from "./constants.js";
24
+ import { ACTIVITY_DIRNAME, ACTIVITY_REFRESH_DEBOUNCE_MS, ACTIVITY_WATCHED_PROGRAMS, AUTOMATION_EVENT_DEBOUNCE_MS, AUTOMATION_RECONCILE_MIN_DOWNTIME_MS, AUTOMATION_RUN_QUERY_PARAM, AUTOMATION_WATCH_DEBOUNCE_MS, AUTOMATION_WATCH_POST_RUN_GRACE_MS, AUTOMATION_WEBHOOK_DEBOUNCE_MS, DEFAULT_HOST, DEFAULT_PORT, FRIENDLY_HOSTNAME, GIT_DIRTY_THROTTLE_MS, GIT_MAX_REF_LENGTH, HTTP_STATUS_ACCEPTED, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_CONFLICT, HTTP_STATUS_CREATED, HTTP_STATUS_NOT_FOUND, MAX_AUTOMATIONS, MAX_PROCESSES, MAX_SECRETS, MS_PER_MINUTE, PROCESSES_FILENAME, SECRETS_FILENAME, THEMES_FILENAME, FONTS_FILENAME, SECRETS_SHIMS_DIRNAME, SECRET_EXPORT_SCRYPT_WORK_FACTOR, AUTOMATION_AGENT_SESSIONS_DIRNAME, SERVER_STOP_GRACE_MS, SESSION_ID_QUERY_PARAM, SESSION_ACTIVITY_WINDOW_MS, WINDOW_ID_QUERY_PARAM, WAIT_DEFAULT_TIMEOUT_MS, WS_BACKPRESSURE_THRESHOLD_BYTES, WS_CLOSE_BACKPRESSURE, WS_CLOSE_CAPACITY_REACHED, WS_CLOSE_POLICY_VIOLATION, WS_HEARTBEAT_GRACE_MS, WS_HEARTBEAT_INTERVAL_MS, WS_HEARTBEAT_TIMEOUT_MS, WS_READY_STATE_OPEN, AUTH_SECRET_FILENAME, AUTH_COOKIE_NAME, UPDATE_CHECK_FILENAME, } from "./constants.js";
25
25
  import { getDefaultShell, listKnownShells, resolveShellOverride } from "./default-shell.js";
26
26
  import { resolveWindowId } from "./utils/resolve-window-id.js";
27
27
  import { shellPathForUserShell } from "./utils/shell-path.js";
28
28
  import { openChromeInspect } from "./utils/open-chrome-inspect.js";
29
+ import { readServerVersion } from "./utils/read-server-version.js";
30
+ import { UpdateCheckStore } from "./update-check-store.js";
29
31
  import { ServerErrorException, serverError } from "./errors.js";
30
32
  import { AutomationGitWatcher } from "./automation-git-watcher.js";
31
33
  import { FolderWatchManager } from "./folder-watch-manager.js";
@@ -37,15 +39,17 @@ import { createDefaultSecretBackend } from "./secret-backend.js";
37
39
  import { SecretStore } from "./secret-store.js";
38
40
  import { ProcessStore } from "./process-store.js";
39
41
  import { ThemeStore } from "./theme-store.js";
42
+ import { FontStore } from "./font-store.js";
40
43
  import { parseImportedTheme } from "./theme-parser.js";
41
44
  import { isBuiltinThemeId, BUILTIN_THEME_IDS } from "./terminal-themes.js";
45
+ import { isBuiltinFontId } from "./terminal-fonts.js";
42
46
  import { regenerateShims } from "./secret-shims.js";
43
47
  import { encryptSecretExport, decryptSecretExport } from "./secret-export.js";
44
48
  import { ProcessActivityWatcher } from "./process-activity-watcher.js";
45
49
  import { parseCronExpression } from "./cron-expression.js";
46
50
  import { createGitWorktree, listGitWorktrees, removeGitWorktree } from "./git-worktrees.js";
47
51
  import { defaultSnapshotListeners, isSessionDescendantPid, listSessionListeningPorts, } from "./listening-ports.js";
48
- import { clientToServerMessageSchema, createAutomationInputSchema, createSessionInputSchema, createWorktreeInputSchema, execInputSchema, execOneShotInputSchema, launchInputSchema, resetAutomationInputSchema, secretEntrySchema, secretSetInputSchema, secretExportRequestSchema, secretImportRequestSchema, sessionInputSchema, sessionResizeSchema, processNameSchema, processSetInputSchema, importThemeInputSchema, setActiveThemeInputSchema, migrateThemesInputSchema, updateAutomationInputSchema, updateDaemonConfigInputSchema, updateSessionInputSchema, updateWorktreeConfigInputSchema, worktreeIncludeFileInputSchema, waitInputSchema, mouseInputSchema, } from "./schemas.js";
52
+ import { clientToServerMessageSchema, createAutomationInputSchema, createSessionInputSchema, createWorktreeInputSchema, execInputSchema, execOneShotInputSchema, launchInputSchema, resetAutomationInputSchema, secretEntrySchema, secretSetInputSchema, secretExportRequestSchema, secretImportRequestSchema, sessionInputSchema, sessionResizeSchema, processNameSchema, processSetInputSchema, importThemeInputSchema, setActiveThemeInputSchema, migrateThemesInputSchema, updateFontsInputSchema, migrateFontsInputSchema, updateAutomationInputSchema, updateDaemonConfigInputSchema, updateSessionInputSchema, updateWorktreeConfigInputSchema, worktreeIncludeFileInputSchema, waitInputSchema, mouseInputSchema, updateStatusSchema, } from "./schemas.js";
49
53
  import { createNetworkPolicyMiddleware, isAllowedSourceIp, isLoopbackHost } from "./security.js";
50
54
  import { createIdentityProvider } from "./identity/factory.js";
51
55
  import { loadOrCreateAuthSecret, signSessionToken } from "./identity/session-cookie.js";
@@ -180,7 +184,7 @@ const normalizeMouseAction = (input) => {
180
184
  };
181
185
  const buildApiRoutes = (ctx) => {
182
186
  const api = new Hono();
183
- const { registry, ownerFor, cdpClient, secretBackend, secretExportScryptWorkFactor, secretStore, shimsDir, processStore, themeStore, syncSecretShims, automationStore, broadcastAutomations, syncFolderWatchers, syncSessionEventListeners, webhookTriggerManager, worktreeConfigStore, portsSnapshotProcesses, portsSnapshotListeners, toAutomationWithNextRun, listAutomationsWithNextRun, tryLaunch, getCdpPort, applyCdpPort, getGraceSeconds, applyGraceSeconds, connectCdpNow, buildTabUrl, mintViewerCookie, stateDirectory, broadcastThemes, } = ctx;
187
+ const { registry, ownerFor, cdpClient, secretBackend, secretExportScryptWorkFactor, secretStore, shimsDir, processStore, themeStore, fontStore, syncSecretShims, automationStore, broadcastAutomations, syncFolderWatchers, syncSessionEventListeners, webhookTriggerManager, worktreeConfigStore, portsSnapshotProcesses, portsSnapshotListeners, toAutomationWithNextRun, listAutomationsWithNextRun, tryLaunch, getCdpPort, applyCdpPort, getGraceSeconds, applyGraceSeconds, connectCdpNow, buildTabUrl, mintViewerCookie, stateDirectory, broadcastThemes, broadcastFonts, updateCheckStore, } = ctx;
184
188
  // Headless SGR-1006 fallback for `mouse` when no CDP tab is reachable:
185
189
  // encode the gesture as SGR bytes and write them straight to the PTY. Closes
186
190
  // over `registry` so the automation layer stays CDP-agnostic. Coords arrive
@@ -211,6 +215,20 @@ const buildApiRoutes = (ctx) => {
211
215
  }
212
216
  : null,
213
217
  }));
218
+ // The daemon's cached npm update check. By default returns the non-blocking
219
+ // cache and kicks a background refresh when stale — so a poll from every open
220
+ // tab can't wedge on the registry. `?wait=1` (the CLI banner path) awaits a
221
+ // fresh fetch when the cache is stale so `localterm start`/`status` can print
222
+ // an accurate line (bounded by UPDATE_CHECK_HTTP_TIMEOUT_MS). Auth-gated like
223
+ // the rest of /api/*; the CLI injects the operator bearer token, the browser
224
+ // tab carries its session cookie.
225
+ api.get("/update-status", async (context) => {
226
+ if (context.req.query("wait") === "1") {
227
+ return context.json(updateStatusSchema.parse(await updateCheckStore.getFreshStatus()));
228
+ }
229
+ updateCheckStore.refreshIfStaleBackground();
230
+ return context.json(updateStatusSchema.parse(updateCheckStore.getStatus()));
231
+ });
214
232
  // Completion fast path: the shell scripts curl this with the command line
215
233
  // (repeated `argv=` query params) so the daemon resolves candidates
216
234
  // in-process — no Node startup per <Tab>. Auth-gated like the rest of /api/*
@@ -779,6 +797,58 @@ const buildApiRoutes = (ctx) => {
779
797
  broadcastThemes();
780
798
  return context.json({ activeThemeId });
781
799
  });
800
+ // Terminal fonts: the active font id + the user-entered custom family + the
801
+ // Nerd Font / ligatures toggles, stored in ~/.localterm/fonts.json so the
802
+ // `localterm font` CLI and every browser tab share one source of truth
803
+ // (replacing the per-browser localStorage the UI used to keep). GET returns
804
+ // the full state in one read so the client reconciles its localStorage cache
805
+ // in a single round-trip.
806
+ api.get("/fonts", (context) => context.json({
807
+ activeFontId: fontStore.getActive(),
808
+ customFontFamily: fontStore.getCustomFontFamily(),
809
+ nerdFontEnabled: fontStore.getNerdFontEnabled(),
810
+ ligaturesEnabled: fontStore.getLigaturesEnabled(),
811
+ initialized: fontStore.isInitialized(),
812
+ }));
813
+ // One-time migration of the browser's legacy localStorage font state. No-ops
814
+ // once the store is initialized (the CLI or another tab wrote first) and
815
+ // returns the current state so the caller can reconcile regardless.
816
+ api.post("/fonts/migrate", async (context) => {
817
+ const parsed = migrateFontsInputSchema.safeParse(await readJsonBody(context));
818
+ if (!parsed.success)
819
+ return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
820
+ fontStore.migrate(parsed.data);
821
+ broadcastFonts();
822
+ return context.json({
823
+ activeFontId: fontStore.getActive(),
824
+ customFontFamily: fontStore.getCustomFontFamily(),
825
+ nerdFontEnabled: fontStore.getNerdFontEnabled(),
826
+ ligaturesEnabled: fontStore.getLigaturesEnabled(),
827
+ initialized: fontStore.isInitialized(),
828
+ });
829
+ });
830
+ // Update any subset of the font settings. The client pushes only the field
831
+ // that changed; `activeFontId` is validated against the built-ins (incl.
832
+ // "custom") so a typo can't select a font that won't resolve (the client
833
+ // would silently fall back to the default family otherwise). Returns the
834
+ // full state so the caller reconciles in one read.
835
+ api.put("/fonts", async (context) => {
836
+ const parsed = updateFontsInputSchema.safeParse(await readJsonBody(context));
837
+ if (!parsed.success)
838
+ return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
839
+ if (parsed.data.activeFontId !== undefined && !isBuiltinFontId(parsed.data.activeFontId)) {
840
+ return context.json({ error: "not_found" }, HTTP_STATUS_NOT_FOUND);
841
+ }
842
+ fontStore.update(parsed.data);
843
+ broadcastFonts();
844
+ return context.json({
845
+ activeFontId: fontStore.getActive(),
846
+ customFontFamily: fontStore.getCustomFontFamily(),
847
+ nerdFontEnabled: fontStore.getNerdFontEnabled(),
848
+ ligaturesEnabled: fontStore.getLigaturesEnabled(),
849
+ initialized: fontStore.isInitialized(),
850
+ });
851
+ });
782
852
  // Open dev ports: TCP listening sockets owned by processes descended from a
783
853
  // localterm session shell (a dev server run inside a tab). The ports modal
784
854
  // polls this while open so a dev server starting/stopping shows up live. The
@@ -1583,6 +1653,7 @@ export const createServer = async (options = {}) => {
1583
1653
  });
1584
1654
  const processStore = new ProcessStore(path.join(stateDirectory, PROCESSES_FILENAME));
1585
1655
  const themeStore = new ThemeStore({ filePath: path.join(stateDirectory, THEMES_FILENAME) });
1656
+ const fontStore = new FontStore({ filePath: path.join(stateDirectory, FONTS_FILENAME) });
1586
1657
  const activityDir = path.join(stateDirectory, ACTIVITY_DIRNAME);
1587
1658
  const syncSecretShims = () => regenerateShims(processStore.list(), secretStore.envVarByName(), shimsDir, secretBackend, activityDir);
1588
1659
  syncSecretShims();
@@ -1803,6 +1874,22 @@ export const createServer = async (options = {}) => {
1803
1874
  safeSend(clientSocket, payload);
1804
1875
  }
1805
1876
  };
1877
+ // Push the full font state to every tab on any mutation (set/family/toggle/
1878
+ // migrate) so open terminals reflect a CLI or other-tab change instantly —
1879
+ // the browser applies the {type:"fonts"} WS message directly, no polling.
1880
+ const broadcastFonts = () => {
1881
+ const payload = {
1882
+ type: "fonts",
1883
+ activeFontId: fontStore.getActive(),
1884
+ customFontFamily: fontStore.getCustomFontFamily(),
1885
+ nerdFontEnabled: fontStore.getNerdFontEnabled(),
1886
+ ligaturesEnabled: fontStore.getLigaturesEnabled(),
1887
+ initialized: fontStore.isInitialized(),
1888
+ };
1889
+ for (const clientSocket of clientSockets) {
1890
+ safeSend(clientSocket, payload);
1891
+ }
1892
+ };
1806
1893
  const caffeinateStatePayload = () => ({
1807
1894
  type: "caffeinate",
1808
1895
  supported: caffeinateManager.supported,
@@ -2073,6 +2160,13 @@ export const createServer = async (options = {}) => {
2073
2160
  };
2074
2161
  }
2075
2162
  };
2163
+ const updateCheckStore = new UpdateCheckStore({
2164
+ filePath: path.join(stateDirectory, UPDATE_CHECK_FILENAME),
2165
+ currentVersion: options.currentVersion ?? readServerVersion(),
2166
+ fetcher: options.updateCheckFetcher,
2167
+ enabled: process.env.LOCALTERM_SKIP_UPDATE_CHECK !== "1" &&
2168
+ process.env.LOCALTERM_SKIP_UPDATE_CHECK !== "true",
2169
+ });
2076
2170
  const ctx = {
2077
2171
  registry,
2078
2172
  resolveIdentity,
@@ -2084,6 +2178,7 @@ export const createServer = async (options = {}) => {
2084
2178
  shimsDir,
2085
2179
  processStore,
2086
2180
  themeStore,
2181
+ fontStore,
2087
2182
  syncSecretShims,
2088
2183
  automationStore,
2089
2184
  broadcastAutomations,
@@ -2101,6 +2196,7 @@ export const createServer = async (options = {}) => {
2101
2196
  getGraceSeconds,
2102
2197
  applyGraceSeconds,
2103
2198
  broadcastThemes,
2199
+ broadcastFonts,
2104
2200
  connectCdpNow,
2105
2201
  buildTabUrl: (sessionId) => {
2106
2202
  const url = new URL(localOrigin ?? publicOrigin ?? `http://${FRIENDLY_HOSTNAME}:${actualPort}`);
@@ -2109,6 +2205,7 @@ export const createServer = async (options = {}) => {
2109
2205
  },
2110
2206
  mintViewerCookie,
2111
2207
  stateDirectory,
2208
+ updateCheckStore,
2112
2209
  };
2113
2210
  const api = buildApiRoutes(ctx);
2114
2211
  app.route("/api", api);
@@ -2493,6 +2590,12 @@ export const createServer = async (options = {}) => {
2493
2590
  // Arm folder-watch triggers for the automations loaded at boot.
2494
2591
  syncFolderWatchers();
2495
2592
  syncSessionEventListeners();
2593
+ // Kick the first npm update check + arm the daily refresh so the cache stays
2594
+ // warm for the CLI banner and the browser indicator even while no tab is
2595
+ // open. Fire-and-forget on startup; the `?wait=1` banner path awaits the
2596
+ // in-flight fetch, and here it just primes the cache without blocking boot.
2597
+ updateCheckStore.start();
2598
+ updateCheckStore.refreshIfStaleBackground();
2496
2599
  // Open the persistent CDP connection now, while the user is present at
2497
2600
  // `start` to clear any one-time remote-debugging prompt. Fire-and-forget:
2498
2601
  // failure just means runs fall back to the OS opener.
@@ -2512,6 +2615,7 @@ export const createServer = async (options = {}) => {
2512
2615
  automationScheduler.dispose();
2513
2616
  folderWatchManager.dispose();
2514
2617
  automationGitWatcher.dispose();
2618
+ updateCheckStore.dispose();
2515
2619
  webhookTriggerManager.dispose();
2516
2620
  caffeinateManager.dispose();
2517
2621
  processActivityWatcher?.dispose();
@@ -2563,7 +2667,9 @@ export const createServer = async (options = {}) => {
2563
2667
  export { CaffeinateController } from "./caffeinate-controller.js";
2564
2668
  export { DEFAULT_HOST, DEFAULT_PORT, WS_CLOSE_BACKPRESSURE } from "./constants.js";
2565
2669
  export { isLoopbackHost, isPrivateHost, isAllowedSourceIp } from "./security.js";
2566
- export { healthSchema, cdpHealthSchema, daemonConfigSchema, identityConfigSchema, oidcConfigSchema, passkeyConfigSchema, updateDaemonConfigInputSchema, } from "./schemas.js";
2670
+ export { healthSchema, cdpHealthSchema, daemonConfigSchema, identityConfigSchema, oidcConfigSchema, passkeyConfigSchema, updateDaemonConfigInputSchema, updateStatusSchema, } from "./schemas.js";
2671
+ export { UpdateCheckStore } from "./update-check-store.js";
2672
+ export { NPM_PACKAGE_NAME } from "./constants.js";
2567
2673
  export { createSessionInputSchema, sessionResponseSchema, updateSessionInputSchema, sessionInputSchema, sessionResizeSchema, execInputSchema, execOneShotInputSchema, execResultSchema, capturePaneResponseSchema, sessionsListResponseSchema, } from "./schemas.js";
2568
2674
  export { createDefaultSecretBackend } from "./secret-backend.js";
2569
2675
  export { detectChromiumBrowsers } from "./cdp/detect-chromium.js";