@termix-ssh/plugin-sdk 1.0.20 → 1.0.21

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 (36) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/PLUGIN-GUIDE.md +5 -2
  3. package/dist/host/{PluginsTab-ZSMTJ5C5.js → PluginsTab-HQGSDSHS.js} +7 -5
  4. package/dist/host/{chunk-J3ZQPBDK.js → chunk-A5CULDWV.js} +10 -5
  5. package/dist/host/{chunk-2MU6WNSX.js → chunk-FGX27MAA.js} +1 -1
  6. package/dist/host/{chunk-EYONBBRJ.js → chunk-GTHINW3L.js} +1 -1
  7. package/dist/host/{chunk-7LEJ3HYF.js → chunk-JFQGI4OD.js} +5 -3
  8. package/dist/host/{chunk-CITUENSU.js → chunk-WWFRBU2E.js} +3 -2
  9. package/dist/host/{linked-server-OUF3POT7.js → linked-server-UDSFY3N7.js} +1 -1
  10. package/dist/host/{rbac-api-LZVENREG.js → rbac-api-AAAPMF52.js} +1 -1
  11. package/dist/host/testing-host.js +4 -4
  12. package/dist/host/types/src/ui/api/look-api.d.ts +2 -0
  13. package/dist/host/types/src/ui/api/plugins-api.d.ts +1 -1
  14. package/dist/host/types/src/ui/lib/app-return.d.ts +13 -0
  15. package/dist/host/types/src/ui/plugins/plugin-model.d.ts +1 -1
  16. package/dist/host/ui.js +67 -24
  17. package/dist/host/{use-permissions-BYCDS5PD.js → use-permissions-VAND5GEG.js} +1 -1
  18. package/dist/host-commands.d.ts +2 -6
  19. package/dist/host-commands.js +3 -6
  20. package/dist/host-scripts/docker.d.ts +137 -0
  21. package/dist/host-scripts/docker.js +345 -0
  22. package/dist/host-scripts/index.d.ts +15 -0
  23. package/dist/host-scripts/index.js +15 -0
  24. package/dist/host-scripts/journal.d.ts +45 -0
  25. package/dist/host-scripts/journal.js +194 -0
  26. package/dist/host-scripts/metrics.d.ts +134 -0
  27. package/dist/host-scripts/metrics.js +320 -0
  28. package/dist/host-scripts/openrc.d.ts +6 -0
  29. package/dist/host-scripts/openrc.js +90 -0
  30. package/dist/host-scripts/shell.d.ts +43 -0
  31. package/dist/host-scripts/shell.js +95 -0
  32. package/dist/host-scripts/systemd.d.ts +73 -0
  33. package/dist/host-scripts/systemd.js +320 -0
  34. package/dist/host-scripts/text.d.ts +23 -0
  35. package/dist/host-scripts/text.js +111 -0
  36. package/package.json +5 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.21
4
+
5
+ Plugin API 1.12.0.
6
+
7
+ ### Added
8
+
9
+ - `@termix-ssh/plugin-sdk/host-scripts`: Docker, Linux metrics, systemd, journal and OpenRC command builders and parsers, plus `scriptShell()` and `passwordSudoScript()` to send scripts to `bash -s` on stdin. It has no Node or ssh2 imports, so the Termix mobile app runs the same code
10
+
3
11
  ## 1.0.20
4
12
 
5
13
  Plugin API 1.12.0.
package/PLUGIN-GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Termix plugin guide
2
2
 
3
- SDK 1.0.20 | Plugin API 1.12.0
3
+ SDK 1.0.21 | Plugin API 1.12.0
4
4
 
5
5
  This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
6
6
  `@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
@@ -335,7 +335,9 @@ try {
335
335
 
336
336
  `ctx.ssh.withConnection(hostId, { pool, purpose }, fn)` borrows a pooled connection for frequent short
337
337
  commands. `@termix-ssh/plugin-sdk/host-commands` has helpers to detect the OS and run package manager and
338
- sudo commands. Hosts from `ctx.hosts` never carry secrets. Pass the host or its id to `ctx.ssh`.
338
+ sudo commands. `@termix-ssh/plugin-sdk/host-scripts` builds Docker, metrics, systemd and journal
339
+ commands and parses their output; send a script on stdin to the command `scriptShell()` returns. Hosts from
340
+ `ctx.hosts` never carry secrets. Pass the host or its id to `ctx.ssh`.
339
341
 
340
342
  ### The desktop app
341
343
 
@@ -905,6 +907,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
905
907
  | `@termix-ssh/plugin-sdk/testing` | `createMockCtx`, `createTestDb`, `renderWithApp`, `createFakeContext` |
906
908
  | `@termix-ssh/plugin-sdk/vitest-preset` | `pluginVitestConfig` |
907
909
  | `@termix-ssh/plugin-sdk/host-commands` | OS detection, package manager and sudo helpers over ssh2 |
910
+ | `@termix-ssh/plugin-sdk/host-scripts` | Docker, metrics, systemd and journal command builders and parsers, scripts sent to `bash -s` |
908
911
  | `@termix-ssh/plugin-sdk/ssh-certs` | `applyCertificateAuth()` for certificate based SSH auth |
909
912
  | `@termix-ssh/plugin-sdk/ssh-keys` | `generateSshKeyPair()`, `detectSshKeyType()`, `publicKeyFromPrivate()`, `validateSshKeyPair()` |
910
913
  | `@termix-ssh/plugin-sdk/desktop` | The desktop app bridge, typed. Check it exists before use |
@@ -29,7 +29,7 @@ import {
29
29
  sortPlugins,
30
30
  uploadEntry,
31
31
  youtubeEmbedUrl
32
- } from "./chunk-CITUENSU.js";
32
+ } from "./chunk-WWFRBU2E.js";
33
33
  import {
34
34
  ArrowDownWideNarrow,
35
35
  BookOpen,
@@ -107,8 +107,8 @@ import {
107
107
  updatePlugin,
108
108
  uploadPlugin,
109
109
  useConfirm
110
- } from "./chunk-EYONBBRJ.js";
111
- import "./chunk-7LEJ3HYF.js";
110
+ } from "./chunk-GTHINW3L.js";
111
+ import "./chunk-JFQGI4OD.js";
112
112
  import {
113
113
  __toESM,
114
114
  define_import_meta_env_default,
@@ -2681,7 +2681,7 @@ function InstalledCard({
2681
2681
  )
2682
2682
  ] }) : !running ? /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 border-t border-border px-3 py-2", children: [
2683
2683
  /* @__PURE__ */ jsx6("span", { className: "size-1.5 shrink-0 bg-muted-foreground/40" }),
2684
- /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: plugin.status === "server" ? t("plugins.manager.status.server") : t("plugins.manager.status.disabled") })
2684
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: plugin.status === "server" ? t("plugins.manager.status.server") : plugin.status === "safeMode" ? t("plugins.manager.status.safeMode") : t("plugins.manager.status.disabled") })
2685
2685
  ] }) : /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 border-t border-border px-3 py-2", children: [
2686
2686
  /* @__PURE__ */ jsx6("span", { className: "size-1.5 shrink-0 bg-green-500" }),
2687
2687
  /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: plugin.pinnedVersion ? t("plugins.manager.pinnedTo", {
@@ -2759,7 +2759,9 @@ function StatusCell({ plugin }) {
2759
2759
  const { t } = useTranslation7();
2760
2760
  const running = plugin.status === "running";
2761
2761
  const failed = plugin.status === "failed" || plugin.status === "blocked";
2762
- const label = plugin.status === "blocked" ? t("plugins.manager.status.blocked") : failed ? t("plugins.manager.status.failed") : plugin.status === "server" ? t("plugins.manager.status.server") : !running ? t("plugins.manager.status.disabled") : plugin.pinnedVersion ? t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion }) : t("plugins.manager.status.running");
2762
+ const label = plugin.status === "blocked" ? t("plugins.manager.status.blocked") : failed ? t("plugins.manager.status.failed") : plugin.status === "server" ? t("plugins.manager.status.server") : plugin.status === "safeMode" ? t("plugins.manager.status.safeMode") : !running ? t("plugins.manager.status.disabled") : plugin.pinnedVersion ? t("plugins.manager.pinnedTo", {
2763
+ version: plugin.pinnedVersion
2764
+ }) : t("plugins.manager.status.running");
2763
2765
  return /* @__PURE__ */ jsxs6(
2764
2766
  "span",
2765
2767
  {
@@ -3,7 +3,7 @@ import {
3
3
  hasPermission,
4
4
  resetPermissionsCache,
5
5
  usePermissions
6
- } from "./chunk-2MU6WNSX.js";
6
+ } from "./chunk-FGX27MAA.js";
7
7
  import {
8
8
  ArrowDownToLine,
9
9
  ArrowLeftToLine,
@@ -83,7 +83,7 @@ import {
83
83
  withIconBoundary,
84
84
  withPluginContext,
85
85
  withPluginScope
86
- } from "./chunk-EYONBBRJ.js";
86
+ } from "./chunk-GTHINW3L.js";
87
87
  import {
88
88
  SYNC_CHANGED_EVENT,
89
89
  TRUST_TOKEN_KEY,
@@ -132,7 +132,7 @@ import {
132
132
  usePluginStore,
133
133
  useTabTypes,
134
134
  websocketAuthProtocols
135
- } from "./chunk-7LEJ3HYF.js";
135
+ } from "./chunk-JFQGI4OD.js";
136
136
  import {
137
137
  define_import_meta_env_default,
138
138
  init_define_import_meta_env
@@ -1144,6 +1144,10 @@ var en_default = {
1144
1144
  ready: "Termix {{version}} is ready to install",
1145
1145
  readyHint: "It installs the next time Termix restarts."
1146
1146
  },
1147
+ safeMode: {
1148
+ title: "Safe mode",
1149
+ description: "Only layout and theme plugins are running. Restart the server without TERMIX_SAFE_MODE to turn the rest back on."
1150
+ },
1147
1151
  companion: {
1148
1152
  close: "Close panel",
1149
1153
  moveSide: "Move panel",
@@ -2220,7 +2224,8 @@ var en_default = {
2220
2224
  disabled: "Disabled",
2221
2225
  failed: "Stopped unexpectedly",
2222
2226
  blocked: "Waiting on another plugin",
2223
- server: "Runs on your server"
2227
+ server: "Runs on your server",
2228
+ safeMode: "Off in safe mode"
2224
2229
  },
2225
2230
  groups: {
2226
2231
  running: "Running",
@@ -10263,7 +10268,7 @@ function CompanionLayout({
10263
10268
  // src/ui/shell/tabUtils.tsx
10264
10269
  import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
10265
10270
  var PluginsTab = lazy(
10266
- () => import("./PluginsTab-ZSMTJ5C5.js").then((m) => ({ default: m.PluginsTab }))
10271
+ () => import("./PluginsTab-HQGSDSHS.js").then((m) => ({ default: m.PluginsTab }))
10267
10272
  );
10268
10273
  function preloadTabSurface(type) {
10269
10274
  const loader = getTabType(type)?.preload;
@@ -26,7 +26,7 @@ var inFlight = null;
26
26
  function load() {
27
27
  if (cache) return Promise.resolve(cache);
28
28
  if (inFlight) return inFlight;
29
- inFlight = import("./rbac-api-LZVENREG.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
29
+ inFlight = import("./rbac-api-AAAPMF52.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
30
30
  cache = {
31
31
  permissions: result.permissions ?? [],
32
32
  isAdmin: !!result.isAdmin
@@ -4,7 +4,7 @@ import {
4
4
  getPluginRecord,
5
5
  rbacApi,
6
6
  usePluginStore
7
- } from "./chunk-7LEJ3HYF.js";
7
+ } from "./chunk-JFQGI4OD.js";
8
8
  import {
9
9
  __export,
10
10
  init_define_import_meta_env
@@ -4087,7 +4087,9 @@ function unregisteredViewStatus(kind, id) {
4087
4087
  return { status: getPluginStoreState().loaded ? "missing" : "loading" };
4088
4088
  }
4089
4089
  const serverState = owner.summary.state;
4090
- if (!owner.summary.enabled) return { status: "disabled", owner };
4090
+ if (!owner.summary.enabled || serverState === "safe-mode") {
4091
+ return { status: "disabled", owner };
4092
+ }
4091
4093
  if (owner.frontend === "failed" || owner.frontend === "blocked" || serverState === "failed" || serverState === "blocked") {
4092
4094
  return { status: "failed", owner };
4093
4095
  }
@@ -6316,7 +6318,7 @@ async function updateRole(roleId, roleData) {
6316
6318
  try {
6317
6319
  const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
6318
6320
  if (roleData.permissions) {
6319
- const { notifyPermissionsChanged } = await import("./use-permissions-BYCDS5PD.js");
6321
+ const { notifyPermissionsChanged } = await import("./use-permissions-VAND5GEG.js");
6320
6322
  notifyPermissionsChanged();
6321
6323
  }
6322
6324
  return response.data;
@@ -6898,7 +6900,7 @@ function createRemoteOriginApiInstance(path) {
6898
6900
  });
6899
6901
  instance.interceptors.request.use(
6900
6902
  async (config) => {
6901
- const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-OUF3POT7.js");
6903
+ const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-UDSFY3N7.js");
6902
6904
  const linked = await getLinkedSession2();
6903
6905
  config.baseURL = linked ? `${linked.serverUrl}${path}` : "http://no-server-configured";
6904
6906
  if (config.headers.set) {
@@ -8,10 +8,10 @@ import {
8
8
  cn,
9
9
  gridSizeStyle,
10
10
  useAreaPreferences
11
- } from "./chunk-EYONBBRJ.js";
11
+ } from "./chunk-GTHINW3L.js";
12
12
  import {
13
13
  isElectron
14
- } from "./chunk-7LEJ3HYF.js";
14
+ } from "./chunk-JFQGI4OD.js";
15
15
  import {
16
16
  __commonJS,
17
17
  __toESM,
@@ -2486,6 +2486,7 @@ function pluginStatus(summary) {
2486
2486
  if (summary.state === "failed") return "failed";
2487
2487
  if (summary.state === "blocked") return "blocked";
2488
2488
  if (!summary.enabled) return "disabled";
2489
+ if (summary.state === "safe-mode") return "safeMode";
2489
2490
  return "running";
2490
2491
  }
2491
2492
  function mergePlugins(installed, registry) {
@@ -4,7 +4,7 @@ import {
4
4
  getLinkedSession,
5
5
  notifySyncChanged,
6
6
  useDesktopLinked
7
- } from "./chunk-7LEJ3HYF.js";
7
+ } from "./chunk-JFQGI4OD.js";
8
8
  import "./chunk-YGYBM7SQ.js";
9
9
  export {
10
10
  SYNC_CHANGED_EVENT,
@@ -23,7 +23,7 @@ import {
23
23
  shareHost,
24
24
  updateHostAccess,
25
25
  updateRole
26
- } from "./chunk-7LEJ3HYF.js";
26
+ } from "./chunk-JFQGI4OD.js";
27
27
  import "./chunk-YGYBM7SQ.js";
28
28
  export {
29
29
  assignRoleToUser,
@@ -59,10 +59,10 @@ import {
59
59
  trackTabHandle,
60
60
  useShellController,
61
61
  useTabStageStore
62
- } from "./chunk-J3ZQPBDK.js";
62
+ } from "./chunk-A5CULDWV.js";
63
63
  import {
64
64
  setPermissionsForTesting
65
- } from "./chunk-2MU6WNSX.js";
65
+ } from "./chunk-FGX27MAA.js";
66
66
  import {
67
67
  SurfaceScope,
68
68
  notifyShellReady,
@@ -70,14 +70,14 @@ import {
70
70
  setShellCallbacks,
71
71
  setShellHosts,
72
72
  setShellLayoutProvider
73
- } from "./chunk-EYONBBRJ.js";
73
+ } from "./chunk-GTHINW3L.js";
74
74
  import {
75
75
  getTabType,
76
76
  listTabTypes,
77
77
  resetPluginStore,
78
78
  setFrontendState,
79
79
  setPluginSummaries
80
- } from "./chunk-7LEJ3HYF.js";
80
+ } from "./chunk-JFQGI4OD.js";
81
81
  import {
82
82
  init_define_import_meta_env
83
83
  } from "./chunk-YGYBM7SQ.js";
@@ -22,6 +22,8 @@ export interface LookResponse {
22
22
  };
23
23
  /** TERMIX_SAFE_LAYOUT is set on the server. */
24
24
  safeLayout: boolean;
25
+ /** TERMIX_SAFE_MODE is set: only layout and theme plugins run. */
26
+ safeMode?: boolean;
25
27
  unavailable: UnavailableLook[];
26
28
  }
27
29
  export declare function getLook(): Promise<LookResponse>;
@@ -120,7 +120,7 @@ export interface PluginSummary {
120
120
  name: string;
121
121
  version: string;
122
122
  enabled: boolean;
123
- /** enabled | disabled | blocked | failed, or the loader's live state. */
123
+ /** enabled | disabled | blocked | failed | safe-mode, or the loader's live state. */
124
124
  state: string;
125
125
  /** A linked desktop leaves this plugin to its server. */
126
126
  runsOnServer?: boolean;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sign in for the Termix mobile app in a real browser. The app opens the
3
+ * login page with ?return_to=termix-mobile://..., and after any login method
4
+ * the page sends the token back to that address. Only the app's own scheme
5
+ * is accepted, so the value can never send a token to a website.
6
+ */
7
+ export declare function isAppReturn(value: unknown): value is string;
8
+ /** Reads return_to from the address bar once and keeps it for this tab. */
9
+ export declare function captureAppReturn(): string | null;
10
+ export declare function pendingAppReturn(): string | null;
11
+ export declare function appReturnUrl(base: string, params: Record<string, string>): string;
12
+ /** Sends the result to the app. False when no app asked for it. */
13
+ export declare function handOffToApp(params: Record<string, string>): boolean;
@@ -2,7 +2,7 @@ import { type CapabilityRisk } from "@termix-ssh/plugin-sdk/capabilities";
2
2
  import type { PluginEnvVar } from "@termix-ssh/plugin-sdk/docs";
3
3
  import { type ChangeType, type ChangelogChange, type ChangelogRelease } from "@termix-ssh/plugin-sdk/changelog";
4
4
  import type { PluginChannel, PluginSummary, PluginUploadPreview, RegistryPluginEntry, RegistryPluginVersion } from "../api/plugins-api";
5
- export type PluginStatus = "running" | "disabled" | "failed" | "blocked" | "server";
5
+ export type PluginStatus = "running" | "disabled" | "failed" | "blocked" | "server" | "safeMode";
6
6
  /** One row of the Plugins tab: what is installed merged with the registry. */
7
7
  export interface PluginEntry {
8
8
  id: string;
package/dist/host/ui.js CHANGED
@@ -160,10 +160,10 @@ import {
160
160
  useThemes,
161
161
  verifySecondFactor,
162
162
  withProtocolSettings
163
- } from "./chunk-J3ZQPBDK.js";
163
+ } from "./chunk-A5CULDWV.js";
164
164
  import {
165
165
  usePermissions
166
- } from "./chunk-2MU6WNSX.js";
166
+ } from "./chunk-FGX27MAA.js";
167
167
  import {
168
168
  AddButton,
169
169
  Card,
@@ -184,7 +184,7 @@ import {
184
184
  isPrerelease,
185
185
  reportBetaFeedbackUrl,
186
186
  reportCoreIssueUrl
187
- } from "./chunk-CITUENSU.js";
187
+ } from "./chunk-WWFRBU2E.js";
188
188
  import {
189
189
  Action,
190
190
  Activity,
@@ -374,7 +374,7 @@ import {
374
374
  useSurfaceKind,
375
375
  useUiPreference,
376
376
  useUiPreferencesContext
377
- } from "./chunk-EYONBBRJ.js";
377
+ } from "./chunk-GTHINW3L.js";
378
378
  import {
379
379
  CREDENTIAL_ROW_FIELD_KEYS,
380
380
  PRESETS,
@@ -535,7 +535,7 @@ import {
535
535
  useTabs,
536
536
  useTabsSafe,
537
537
  verifyPasswordResetCode
538
- } from "./chunk-7LEJ3HYF.js";
538
+ } from "./chunk-JFQGI4OD.js";
539
539
  import {
540
540
  define_import_meta_env_default,
541
541
  init_define_import_meta_env
@@ -3327,6 +3327,39 @@ function isInElectronWebView() {
3327
3327
  }
3328
3328
  return false;
3329
3329
  }
3330
+ function isEmbeddedAuthFrame() {
3331
+ return isInMobileWebView() || isInElectronWebView();
3332
+ }
3333
+
3334
+ // src/ui/lib/app-return.ts
3335
+ init_define_import_meta_env();
3336
+ var STORAGE_KEY = "termix-app-return";
3337
+ var APP_RETURN = /^termix-mobile:\/\/[A-Za-z0-9._~/-]*$/;
3338
+ function isAppReturn(value) {
3339
+ return typeof value === "string" && APP_RETURN.test(value);
3340
+ }
3341
+ function pendingAppReturn() {
3342
+ try {
3343
+ const value = sessionStorage.getItem(STORAGE_KEY);
3344
+ return isAppReturn(value) ? value : null;
3345
+ } catch {
3346
+ return null;
3347
+ }
3348
+ }
3349
+ function appReturnUrl(base2, params) {
3350
+ const query = new URLSearchParams(params).toString();
3351
+ return query ? `${base2}?${query}` : base2;
3352
+ }
3353
+ function handOffToApp(params) {
3354
+ const base2 = pendingAppReturn();
3355
+ if (!base2) return false;
3356
+ try {
3357
+ sessionStorage.removeItem(STORAGE_KEY);
3358
+ } catch {
3359
+ }
3360
+ window.location.replace(appReturnUrl(base2, params));
3361
+ return true;
3362
+ }
3330
3363
 
3331
3364
  // src/ui/auth/Auth.tsx
3332
3365
  import { useState as useState7, useEffect as useEffect6, useCallback as useCallback5, useRef as useRef4 } from "react";
@@ -3878,7 +3911,7 @@ function currentDeps() {
3878
3911
  return deps ?? defaultDeps;
3879
3912
  }
3880
3913
  function serverAllowsFrontend(summary) {
3881
- return summary.enabled && summary.state !== "failed" && summary.state !== "blocked" && !!summary.frontend;
3914
+ return summary.enabled && summary.state !== "failed" && summary.state !== "blocked" && summary.state !== "safe-mode" && !!summary.frontend;
3882
3915
  }
3883
3916
  function orderForActivation(summaries) {
3884
3917
  const byId = new Map(summaries.map((summary) => [summary.id, summary]));
@@ -4252,7 +4285,7 @@ function LanguageRow({
4252
4285
  )
4253
4286
  ] });
4254
4287
  }
4255
- var STORAGE_KEY = "termix_auth";
4288
+ var STORAGE_KEY2 = "termix_auth";
4256
4289
  function parseSecondFactorIds(value) {
4257
4290
  const ids = (value ?? "").split(",").filter(Boolean);
4258
4291
  return ids.map((id) => ({ id, pluginId: "", labelKey: "" }));
@@ -4266,10 +4299,15 @@ function hasDesktopManualLogout() {
4266
4299
  }
4267
4300
  function storeAuth(username) {
4268
4301
  localStorage.setItem(
4269
- STORAGE_KEY,
4302
+ STORAGE_KEY2,
4270
4303
  JSON.stringify({ loggedIn: true, username })
4271
4304
  );
4272
4305
  }
4306
+ async function handOffLogin(token) {
4307
+ if (!pendingAppReturn() || isEmbeddedAuthFrame()) return false;
4308
+ const value = token || await getCurrentToken();
4309
+ return handOffToApp(value ? { token: value } : { error: "no_token" });
4310
+ }
4273
4311
  function PasswordInput2({
4274
4312
  value,
4275
4313
  onChange,
@@ -4407,8 +4445,9 @@ function Auth({ onLogin }) {
4407
4445
  useEffect6(() => {
4408
4446
  if (proxySigninHandledRef.current || isElectron()) return;
4409
4447
  proxySigninHandledRef.current = true;
4410
- requestTrustedProxyLogin().then((result) => {
4448
+ requestTrustedProxyLogin().then(async (result) => {
4411
4449
  if (!result.enabled || !result.success) return;
4450
+ if (await handOffLogin()) return;
4412
4451
  storeAuth(result.username || "");
4413
4452
  onLogin(
4414
4453
  result.username || "",
@@ -4625,21 +4664,24 @@ function Auth({ onLogin }) {
4625
4664
  }
4626
4665
  return;
4627
4666
  }
4628
- getUserInfo().then((meRes) => {
4629
- storeAuth(meRes.username || "");
4630
- clearDesktopManualLogout();
4631
- onLogin(
4632
- meRes.username || "",
4633
- meRes.userId || void 0,
4634
- !!meRes.is_admin
4635
- );
4636
- window.history.replaceState(
4637
- {},
4638
- document.title,
4639
- window.location.pathname
4640
- );
4641
- }).catch(() => {
4642
- toast2.error(t3("errors.failedUserInfo"));
4667
+ void handOffLogin(urlParams.get("token")).then((handedOff) => {
4668
+ if (handedOff) return;
4669
+ getUserInfo().then((meRes) => {
4670
+ storeAuth(meRes.username || "");
4671
+ clearDesktopManualLogout();
4672
+ onLogin(
4673
+ meRes.username || "",
4674
+ meRes.userId || void 0,
4675
+ !!meRes.is_admin
4676
+ );
4677
+ window.history.replaceState(
4678
+ {},
4679
+ document.title,
4680
+ window.location.pathname
4681
+ );
4682
+ }).catch(() => {
4683
+ toast2.error(t3("errors.failedUserInfo"));
4684
+ });
4643
4685
  });
4644
4686
  }
4645
4687
  }, [onLogin, t3]);
@@ -4721,6 +4763,7 @@ function Auth({ onLogin }) {
4721
4763
  setWebviewAuthSuccess(true);
4722
4764
  return;
4723
4765
  }
4766
+ if (await handOffLogin(res?.token)) return;
4724
4767
  const meRes = await getUserInfo();
4725
4768
  const name2 = meRes.username || res.username || options.fallbackUsername || "";
4726
4769
  storeAuth(name2);
@@ -7,7 +7,7 @@ import {
7
7
  resetPermissionsCache,
8
8
  setPermissionsForTesting,
9
9
  usePermissions
10
- } from "./chunk-2MU6WNSX.js";
10
+ } from "./chunk-FGX27MAA.js";
11
11
  import "./chunk-YGYBM7SQ.js";
12
12
  export {
13
13
  hasPermission,
@@ -10,6 +10,8 @@
10
10
  */
11
11
  export type { Client, ClientChannel } from "ssh2";
12
12
  import type { Client } from "ssh2";
13
+ import { EXTRA_PATH_DIRS } from "./host-scripts/docker.js";
14
+ export { EXTRA_PATH_DIRS };
13
15
  export interface ExecResult {
14
16
  stdout: string;
15
17
  stderr: string;
@@ -26,12 +28,6 @@ export interface PlatformInfo {
26
28
  hasDocker: boolean;
27
29
  osPrettyName: string | null;
28
30
  }
29
- /**
30
- * Non-interactive SSH shells don't source ~/.zprofile or ~/.bash_profile,
31
- * so PATH additions from installers like Homebrew or OrbStack are missing.
32
- * Extend PATH with their common install locations before probing.
33
- */
34
- export declare const EXTRA_PATH_DIRS = "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:\"$HOME/.orbstack/bin\"";
35
31
  /**
36
32
  * Single probe that reports which tooling is available. Each line is
37
33
  * "key=value" so the parser is trivial and order-independent.
@@ -8,6 +8,9 @@
8
8
  *
9
9
  * A plugin passes the `Client` it got from ctx.ssh.connect/withConnection.
10
10
  */
11
+ // Homebrew and OrbStack paths a non-interactive shell misses.
12
+ import { EXTRA_PATH_DIRS } from "./host-scripts/docker.js";
13
+ export { EXTRA_PATH_DIRS };
11
14
  /** Runs one non-interactive command over an existing ssh2 client. */
12
15
  export function execCommand(client, command, timeoutMs = 30000) {
13
16
  return new Promise((resolve, reject) => {
@@ -83,12 +86,6 @@ export function execCommand(client, command, timeoutMs = 30000) {
83
86
  });
84
87
  });
85
88
  }
86
- /**
87
- * Non-interactive SSH shells don't source ~/.zprofile or ~/.bash_profile,
88
- * so PATH additions from installers like Homebrew or OrbStack are missing.
89
- * Extend PATH with their common install locations before probing.
90
- */
91
- export const EXTRA_PATH_DIRS = '/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:"$HOME/.orbstack/bin"';
92
89
  const DOCKER_PROBE = `command -v docker >/dev/null 2>&1` +
93
90
  ` || [ -S /var/run/docker.sock ]` +
94
91
  ` || [ -S "$HOME/.orbstack/run/docker.sock" ]`;