dsh-home-hosted 0.2.0 → 0.2.2

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/lib/index.js CHANGED
@@ -39,6 +39,8 @@ var DEFAULT_SETTINGS = {
39
39
  // Every tool on by default; the session's own permission mode is what gates them.
40
40
  agentTools: { enabled: true, allow: [...AGENT_TOOL_NAMES] },
41
41
  panel: { port: null },
42
+ authNotice: true,
43
+ uiStyle: "detailed",
42
44
  cli: { prefer: "pinned" }
43
45
  };
44
46
 
@@ -2179,18 +2181,20 @@ function defaultIntent(id, platform = process.platform) {
2179
2181
  id,
2180
2182
  autostart: true,
2181
2183
  onPortConflict: defaultOnPortConflict(platform),
2182
- stopKillPortHolders: true
2184
+ stopKillPortHolders: true,
2185
+ persistent: true
2183
2186
  };
2184
2187
  }
2185
- function ownedPatch(intent, live) {
2188
+ function ownedPatch(intent, live, options = {}) {
2186
2189
  const stop = live?.stop ?? {};
2187
2190
  return {
2188
2191
  autostart: intent.autostart,
2189
2192
  onPortConflict: intent.onPortConflict,
2193
+ ...options.persistent === false ? {} : { persistent: intent.persistent },
2190
2194
  stop: { ...stop, killPortHolders: intent.stopKillPortHolders }
2191
2195
  };
2192
2196
  }
2193
- function ownedDrift(live, intent) {
2197
+ function ownedDrift(live, intent, options = {}) {
2194
2198
  if (live === null)
2195
2199
  return ["missing entry"];
2196
2200
  const drift = [];
@@ -2198,6 +2202,8 @@ function ownedDrift(live, intent) {
2198
2202
  drift.push("autostart");
2199
2203
  if (live.onPortConflict !== intent.onPortConflict)
2200
2204
  drift.push("onPortConflict");
2205
+ if (options.persistent !== false && (live.persistent ?? false) !== intent.persistent)
2206
+ drift.push("persistent");
2201
2207
  const killPortHolders = live.stop?.killPortHolders;
2202
2208
  if (killPortHolders !== intent.stopKillPortHolders)
2203
2209
  drift.push("stop.killPortHolders");
@@ -2209,6 +2215,7 @@ function snapshotOwned(live) {
2209
2215
  id: live.id,
2210
2216
  autostart: typeof live.autostart === "boolean" ? live.autostart : false,
2211
2217
  onPortConflict: isOnPortConflict(live.onPortConflict) ? live.onPortConflict : "block",
2218
+ persistent: live.persistent === true,
2212
2219
  stop: { killPortHolders: typeof killPortHolders === "boolean" ? killPortHolders : false }
2213
2220
  };
2214
2221
  }
@@ -2218,6 +2225,7 @@ function restorePatch(live, snapshot) {
2218
2225
  return {
2219
2226
  autostart: snapshot?.autostart ?? false,
2220
2227
  onPortConflict: snapshot?.onPortConflict ?? "block",
2228
+ persistent: snapshot?.persistent === true,
2221
2229
  stop: {
2222
2230
  ...stop,
2223
2231
  killPortHolders: typeof previous.killPortHolders === "boolean" ? previous.killPortHolders : false
@@ -2230,9 +2238,10 @@ import { createRequire } from "node:module";
2230
2238
  import fs9 from "node:fs";
2231
2239
  import path9 from "node:path";
2232
2240
  import process6 from "node:process";
2233
- var EXPECTED_RANGE = "^0.6.1";
2241
+ var EXPECTED_RANGE = "^0.6.4";
2234
2242
  var MIN_SUPPORTED_VERSION = "0.4.1";
2235
2243
  var MIN_KILL_VERSION = "0.6.0";
2244
+ var MIN_PERSISTENT_VERSION = "0.6.3";
2236
2245
  function pinnedManifestPath() {
2237
2246
  try {
2238
2247
  return createRequire(import.meta.url).resolve("home-hosted/package.json");
@@ -2988,6 +2997,13 @@ var HomeHostedService = class extends Service {
2988
2997
  // -------------------------------------------------------------------------
2989
2998
  // Panel
2990
2999
  // -------------------------------------------------------------------------
3000
+ /** The panel page showing this session's log, where its tokenised URL is. */
3001
+ panelLogUrl() {
3002
+ const url = this.runtime()?.url;
3003
+ if (url === void 0 || url.length === 0)
3004
+ return null;
3005
+ return `${url.replace(/\/+$/, "")}/logs?server=${encodeURIComponent(this.options.defaultEntryId)}`;
3006
+ }
2991
3007
  /** The port the panel's config holds, without touching a running panel. */
2992
3008
  configuredPort() {
2993
3009
  const control = readConfig(this.options.home).raw?.control;
@@ -3101,7 +3117,7 @@ var HomeHostedService = class extends Service {
3101
3117
  snapshots[intent.id] = live === null ? { id: intent.id } : snapshotOwned(live.config);
3102
3118
  this.saveSnapshots(snapshots);
3103
3119
  }
3104
- const patch = ownedPatch(intent, live?.config ?? null);
3120
+ const patch = ownedPatch(intent, live?.config ?? null, { persistent: await this.supportsPersistent() });
3105
3121
  const client = await this.tryClient();
3106
3122
  if (client !== null) {
3107
3123
  if (live !== null) {
@@ -3153,6 +3169,11 @@ var HomeHostedService = class extends Service {
3153
3169
  const { resolution } = await this.cli();
3154
3170
  return resolution.status.version;
3155
3171
  }
3172
+ /** Whether the panel whose schema parses our write knows `persistent`. */
3173
+ async supportsPersistent() {
3174
+ const version = await this.configVersion();
3175
+ return version !== null && compareVersions(version, MIN_PERSISTENT_VERSION) >= 0;
3176
+ }
3156
3177
  async assertPolicySupported(intent) {
3157
3178
  if (intent.onPortConflict !== "kill")
3158
3179
  return;
@@ -3180,7 +3201,8 @@ var HomeHostedService = class extends Service {
3180
3201
  id: raw.id,
3181
3202
  autostart: raw.autostart === true,
3182
3203
  onPortConflict: isOnPortConflict(requested) ? requested : fallback.onPortConflict,
3183
- stopKillPortHolders: raw.stopKillPortHolders ?? fallback.stopKillPortHolders
3204
+ stopKillPortHolders: raw.stopKillPortHolders ?? fallback.stopKillPortHolders,
3205
+ persistent: raw.persistent ?? fallback.persistent
3184
3206
  };
3185
3207
  await this.assertPolicySupported(intent);
3186
3208
  await this.writeOwned(intent);
@@ -3318,6 +3340,7 @@ var HomeHostedService = class extends Service {
3318
3340
  const settings = this.options.settings.get();
3319
3341
  const live = await this.liveEntries();
3320
3342
  const snapshots = this.snapshots();
3343
+ const persistent = await this.supportsPersistent();
3321
3344
  const ids = /* @__PURE__ */ new Set([this.options.defaultEntryId, ...settings.entries.map((entry) => entry.id)]);
3322
3345
  return [...ids].map((id) => {
3323
3346
  const intent = this.options.settings.intentFor(id);
@@ -3326,7 +3349,7 @@ var HomeHostedService = class extends Service {
3326
3349
  intent,
3327
3350
  exists: entry !== null,
3328
3351
  managed: snapshots[id] !== void 0,
3329
- drift: entry === null ? ["missing entry"] : ownedDrift(entry.config, intent),
3352
+ drift: entry === null ? ["missing entry"] : ownedDrift(entry.config, intent, { persistent }),
3330
3353
  live: entry?.view ?? null,
3331
3354
  snapshot: snapshots[id] ?? null
3332
3355
  };
@@ -3680,6 +3703,76 @@ function registerRpc(ctx, service) {
3680
3703
  });
3681
3704
  }
3682
3705
 
3706
+ // src/home-hosted/web-notice.ts
3707
+ var STOCK_UNAUTHORIZED = "dsh web authentication required; reopen the URL printed by dsh web.";
3708
+ function authNoticeBody(logUrl) {
3709
+ if (logUrl === null)
3710
+ return `${STOCK_UNAUTHORIZED}
3711
+ `;
3712
+ return `${STOCK_UNAUTHORIZED}
3713
+ home-hosted: the URL with its token is in the dsh log at ${logUrl}. An authentication plugin can sign you in seamlessly instead.
3714
+ `;
3715
+ }
3716
+ function wrap(auth, logUrlOf) {
3717
+ const original = auth.writeUnauthorized.bind(auth);
3718
+ auth.writeUnauthorized = (req, res) => {
3719
+ const response = res;
3720
+ const end = response.end;
3721
+ if (typeof end !== "function" || response.writableEnded === true) {
3722
+ original(req, res);
3723
+ return;
3724
+ }
3725
+ let restored = false;
3726
+ const restore = () => {
3727
+ if (restored) return;
3728
+ restored = true;
3729
+ try {
3730
+ response.end = end;
3731
+ } catch {
3732
+ }
3733
+ };
3734
+ try {
3735
+ response.end = (...args) => {
3736
+ restore();
3737
+ const [chunk, ...rest] = args;
3738
+ let body = chunk;
3739
+ if (typeof chunk === "string" && chunk.trim() === STOCK_UNAUTHORIZED) {
3740
+ try {
3741
+ body = authNoticeBody(logUrlOf());
3742
+ } catch {
3743
+ body = chunk;
3744
+ }
3745
+ }
3746
+ return end.call(response, body, ...rest);
3747
+ };
3748
+ } catch {
3749
+ original(req, res);
3750
+ return;
3751
+ }
3752
+ try {
3753
+ response.once?.("finish", restore);
3754
+ response.once?.("close", restore);
3755
+ } catch {
3756
+ }
3757
+ original(req, res);
3758
+ };
3759
+ return () => {
3760
+ auth.writeUnauthorized = original;
3761
+ };
3762
+ }
3763
+ function registerAuthNotice(ctx, logUrlOf) {
3764
+ ctx.inject(["connection"], (scoped) => {
3765
+ scoped.effect(() => {
3766
+ const connection = scoped.get("connection");
3767
+ const auth = connection?.browserAuth;
3768
+ if (typeof auth?.writeUnauthorized !== "function")
3769
+ return () => {
3770
+ };
3771
+ return wrap(auth, logUrlOf);
3772
+ }, "dsh-home-hosted: sign-in notice");
3773
+ });
3774
+ }
3775
+
3683
3776
  // src/settings.ts
3684
3777
  function knownTool(value) {
3685
3778
  return typeof value === "string" && AGENT_TOOL_NAMES.includes(value);
@@ -3689,6 +3782,8 @@ function normalizeIntent(value, fallbackId) {
3689
3782
  return {
3690
3783
  id: base.id,
3691
3784
  autostart: typeof value?.autostart === "boolean" ? value.autostart : base.autostart,
3785
+ // Absent means a stored intent from before persistence existed: it wants it.
3786
+ persistent: typeof value?.persistent === "boolean" ? value.persistent : base.persistent,
3692
3787
  // Only a policy the panel's own schema parses may reach the config it boots from.
3693
3788
  onPortConflict: isOnPortConflict(value?.onPortConflict) ? value.onPortConflict : base.onPortConflict,
3694
3789
  stopKillPortHolders: typeof value?.stopKillPortHolders === "boolean" ? value.stopKillPortHolders : base.stopKillPortHolders
@@ -3735,6 +3830,8 @@ function normalize(raw, fallbackEntryId) {
3735
3830
  // and a value it cannot parse stops it booting at all.
3736
3831
  port: typeof raw?.panel?.port === "number" && Number.isInteger(raw.panel.port) && raw.panel.port >= 1 && raw.panel.port <= 65535 ? raw.panel.port : null
3737
3832
  },
3833
+ authNotice: raw?.authNotice === void 0 ? DEFAULT_SETTINGS.authNotice : raw.authNotice === true,
3834
+ uiStyle: raw?.uiStyle === "compact" ? "compact" : "detailed",
3738
3835
  agentTools: {
3739
3836
  // Absent means the default (on), and so does the pair the previous
3740
3837
  // release wrote for it; any other explicit false stays false.
@@ -3770,6 +3867,8 @@ var SettingsStore = class {
3770
3867
  manageDsh: patch.manageDsh ?? this.current.manageDsh,
3771
3868
  entries: patch.entries ?? this.current.entries,
3772
3869
  panel: { ...this.current.panel, ...patch.panel ?? {} },
3870
+ authNotice: patch.authNotice ?? this.current.authNotice,
3871
+ uiStyle: patch.uiStyle ?? this.current.uiStyle,
3773
3872
  agentTools: { ...this.current.agentTools, ...patch.agentTools ?? {} },
3774
3873
  cli: { ...this.current.cli, ...patch.cli ?? {} }
3775
3874
  }, this.fallbackEntryId);
@@ -3989,6 +4088,7 @@ function apply(ctx, config) {
3989
4088
  });
3990
4089
  registerRpc(ctx, service);
3991
4090
  registerAgentTools(ctx, service, settings);
4091
+ registerAuthNotice(ctx, () => settings.get().authNotice ? service.panelLogUrl() : null);
3992
4092
  ctx.effect(() => {
3993
4093
  const timer = setTimeout(() => {
3994
4094
  void service.reconcile().catch(() => {