dsh-session-manager 0.7.2 → 0.7.4

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.
@@ -8,18 +8,50 @@
8
8
  * @module dsh-session-manager/compat/dsh-adapter
9
9
  */
10
10
  import { createRequire } from "node:module";
11
- import { readFileSync } from "node:fs";
12
- import { join } from "node:path";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { join, resolve } from "node:path";
13
14
 
14
15
  const require = createRequire(import.meta.url);
15
16
 
17
+ /** Expand a leading `~` / `~/` / `~\` against the OS home, matching
18
+ * `@deepseek-ai/dsh-home-paths` `expandHomePath`. */
19
+ function expandHomePath(path) {
20
+ if (path === "~") return homedir();
21
+ if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
22
+ return path;
23
+ }
24
+
25
+ /**
26
+ * Resolve the DSH home without the `dshHomePath` service. DSH 0.2.x resolves
27
+ * the harness home exclusively through `resolveDshHome()`: `$DSH_HOME` when
28
+ * set and non-blank, otherwise `~/.dsh`; the value is tilde-expanded and
29
+ * resolved to an absolute path. Mirror that order so a plugin scope that
30
+ * cannot see the root-context service still lands on the same directory.
31
+ */
32
+ function fallbackDshHome() {
33
+ const raw = typeof process.env.DSH_HOME === "string" ? process.env.DSH_HOME.trim() : "";
34
+ const selected = raw.length > 0 ? expandHomePath(raw) : join(homedir(), ".dsh");
35
+ return resolve(selected);
36
+ }
37
+
38
+ /** Same calling convention as the official `dshHomePath(...segments)`. */
39
+ function fallbackHomePath(...segments) {
40
+ return join(fallbackDshHome(), ...segments);
41
+ }
16
42
  /** Pull one service out of the Cordis ctx, tolerating direct property fallbacks. */
17
- function getService(ctx, name) {
43
+ export function getService(ctx, name) {
18
44
  try {
19
- const value = typeof ctx?.get === "function" ? ctx.get(name) : undefined;
20
- if (value !== undefined) return value;
21
- } catch { /* fall through to the legacy direct property */ }
22
- return ctx && ctx[name];
45
+ if (typeof ctx?.get === "function") {
46
+ const value = ctx.get(name);
47
+ if (value !== undefined) return value;
48
+ }
49
+ } catch { /* ignore */ }
50
+ try {
51
+ return ctx && ctx[name];
52
+ } catch {
53
+ return undefined;
54
+ }
23
55
  }
24
56
 
25
57
  /** Read one package's version from its package.json if resolvable from this plugin. */
@@ -151,7 +183,15 @@ export function createDshAdapter(ctx) {
151
183
  get persistence() { return getService(ctx, "sessionPersistence"); },
152
184
  get agentPresets() { return getService(ctx, "agentPresets"); },
153
185
  get projectionCache() { return getService(ctx, "sessionProjectionCache"); },
154
- get homePath() { return getService(ctx, "dshHomePath"); },
186
+ get homePath() {
187
+ // DSH 0.2.x provides `dshHomePath` on the root context (app-boot), but a
188
+ // plugin scope may not resolve it; without a fallback every disk-backed
189
+ // path (sessions scan, annotation store, workspace archive store) is
190
+ // lost. Fall back to the official home resolution instead.
191
+ const served = getService(ctx, "dshHomePath");
192
+ if (typeof served === "function") return served;
193
+ return fallbackHomePath;
194
+ },
155
195
  get pluginManager() { return getService(ctx, "pluginManager"); },
156
196
 
157
197
  getSession(id) {
@@ -268,8 +308,172 @@ export function createDshAdapter(ctx) {
268
308
  }
269
309
  },
270
310
  pluginVersion() {
271
- return packageVersion("dsh-session-manager") || "unknown";
311
+ const resolved = packageVersion("dsh-session-manager");
312
+ if (resolved) return resolved;
313
+ try {
314
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
315
+ if (typeof pkg.version === "string") return pkg.version;
316
+ } catch { /* best-effort */ }
317
+ return "unknown";
272
318
  },
273
319
  };
274
320
  return adapter;
275
321
  }
322
+
323
+ /**
324
+ * Resolve the DSH profile directory from adapter or ctx safely without
325
+ * triggering un-injected property access errors on Cordis context proxies.
326
+ */
327
+ export function resolveProfileDir(adapter, ctx) {
328
+ const pm = adapter?.pluginManager || getService(ctx, "pluginManager");
329
+ try {
330
+ if (typeof pm?.profile?.dir === "string") return pm.profile.dir;
331
+ } catch { /* ignore */ }
332
+ try {
333
+ const pCtx = getService(ctx, "profileContext");
334
+ if (typeof pCtx?.dir === "string") return pCtx.dir;
335
+ } catch { /* ignore */ }
336
+ try {
337
+ if (typeof adapter?.homePath === "string") return adapter.homePath;
338
+ } catch { /* ignore */ }
339
+ return undefined;
340
+ }
341
+
342
+ /**
343
+ * Classify an installation specification string into a recognized source:
344
+ * "npm" | "github" | "linked" | "unknown"
345
+ *
346
+ * Implements the specification rules from DSH install-spec.ts & dependencySpec():
347
+ * - Local filesystem paths: file:, link:, absolute paths, or relative path segments.
348
+ * - Git/GitHub repositories: github:, gitlab:, bitbucket:, gist:, git+, git://, git@, or hosted repo URLs.
349
+ * - Registry (npm):
350
+ * - Bare semver version, range, or dist-tag: "^0.7.3", "~0.7.3", "0.7.3", "latest", "beta"
351
+ * - Package-prefixed spec from dependencySpec(): "dsh-session-manager@^0.7.3", "dsh-session-manager@0.7.3", "dsh-session-manager@latest"
352
+ * - npm alias: "npm:dsh-session-manager@^0.7.3" or "dsh-session-manager@npm:..."
353
+ * - Any unrecognized URL, arbitrary protocol, or malformed string falls back to "unknown".
354
+ */
355
+ export function classifyInstallSource(spec, packageName = "dsh-session-manager") {
356
+ if (typeof spec !== "string") return "unknown";
357
+ const s = spec.trim();
358
+ if (!s) return "unknown";
359
+
360
+ // 1. Local link / filesystem path
361
+ if (/^(?:file|link):/i.test(s)) return "linked";
362
+ if (/^(?:\/|[a-zA-Z]:[\\/]|\\\\|\.{1,2}(?:[\\/]|$))/.test(s)) return "linked";
363
+
364
+ // 2. Git / GitHub
365
+ if (/^(?:github|gitlab|bitbucket|gist):/i.test(s)) return "github";
366
+ if (/^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i.test(s)) return "github";
367
+ if (/^https?:\/\/(?:www\.)?(?:github\.com|gitlab\.com|bitbucket\.org)\/[^/]+\/[^/#]+(?:\.git)?(?:#.*)?$/i.test(s)) return "github";
368
+ if (/^git\+https?:\/\//i.test(s)) return "github";
369
+
370
+ // 3. Registry (npm)
371
+ // Strip package name prefix if present (e.g. "dsh-session-manager@...")
372
+ let target = s;
373
+ const prefix = packageName.toLowerCase() + "@";
374
+ if (target.toLowerCase().startsWith(prefix)) {
375
+ target = target.slice(prefix.length).trim();
376
+ }
377
+
378
+ // npm alias or registry protocol (e.g. "registry:npm", "npm:dsh-session-manager@^0.7.3", "npm:...")
379
+ if (
380
+ target.toLowerCase().startsWith("registry:") ||
381
+ target.toLowerCase().startsWith("npm:") ||
382
+ /^npm:(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*@/i.test(target)
383
+ ) {
384
+ return "npm";
385
+ }
386
+
387
+ // After stripping package prefix, check if remainder is a local path or git URL (e.g. "pkg@link:...")
388
+ if (/^(?:file|link):/i.test(target) || /^(?:\/|[a-zA-Z]:[\\/]|\\\\|\.{1,2}(?:[\\/]|$))/.test(target)) {
389
+ return "linked";
390
+ }
391
+ if (/^(?:github|gitlab|bitbucket|gist):/i.test(target) || /^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i.test(target)) {
392
+ return "github";
393
+ }
394
+ if (/^https?:\/\//i.test(target)) {
395
+ // Arbitrary HTTP URLs are NOT standard npm registry ranges
396
+ return "unknown";
397
+ }
398
+
399
+ // Semver versions & ranges:
400
+ // e.g. "0.7.3", "0.7.4-test.1", "^0.7.3", "~0.7.3", ">=0.7.0", "*", "1.x", "=0.7.3"
401
+ if (/^[~^>=<*]/.test(target) || /^\d+\.\d+/.test(target) || /^\d+x/i.test(target)) {
402
+ return "npm";
403
+ }
404
+
405
+ // npm dist-tags: lowercase URL-safe tag names like "latest", "beta", "next", "canary", "dev"
406
+ if (/^[a-zA-Z0-9_.-]+$/.test(target)) {
407
+ return "npm";
408
+ }
409
+
410
+ return "unknown";
411
+ }
412
+
413
+ /**
414
+ * Detect the installation source of dsh-session-manager ("npm" | "github" | "linked" | "installation" | "unknown").
415
+ * Inspects explicit test/config overrides first, then bundle records from PluginManager,
416
+ * then the host profile package.json dependencies. Never defaults to "npm".
417
+ */
418
+ export function detectPluginSource(adapter, ctx, bundles) {
419
+ try {
420
+ if (typeof adapter?.pluginSourceOverride === "string") {
421
+ return adapter.pluginSourceOverride;
422
+ }
423
+ } catch { /* best-effort */ }
424
+ const override = getService(ctx, "pluginSourceOverride");
425
+ if (typeof override === "string") {
426
+ return override;
427
+ }
428
+
429
+ // 1. Inspect bundles from PluginManager.listBundles() if provided
430
+ if (Array.isArray(bundles)) {
431
+ const b = bundles.find(item => item?.name === "dsh-session-manager");
432
+ if (b) {
433
+ if (b.availability === "installation") {
434
+ return "installation";
435
+ }
436
+ if (typeof b.source === "string") {
437
+ const source = classifyInstallSource(b.source, "dsh-session-manager");
438
+ if (source !== "unknown") return source;
439
+ }
440
+ }
441
+ }
442
+
443
+ // 2. Inspect host profile package.json dependencies
444
+ const profileDir = resolveProfileDir(adapter, ctx);
445
+ if (profileDir) {
446
+ try {
447
+ const pkgFile = join(profileDir, "package.json");
448
+ if (existsSync(pkgFile)) {
449
+ const pkg = JSON.parse(readFileSync(pkgFile, "utf8"));
450
+ const spec = pkg?.dependencies?.["dsh-session-manager"]
451
+ || pkg?.devDependencies?.["dsh-session-manager"];
452
+ if (typeof spec === "string") {
453
+ return classifyInstallSource(spec, "dsh-session-manager");
454
+ }
455
+ }
456
+ } catch { /* best-effort */ }
457
+ }
458
+ return "unknown";
459
+ }
460
+
461
+ /**
462
+ * Check if the runtime is currently executing in HMR (Hot Module Replacement) development mode.
463
+ */
464
+ export function isHmrActive(adapter, ctx) {
465
+ try {
466
+ if (typeof ctx?.get === "function" && ctx.get("hmr") !== undefined) return true;
467
+ } catch { /* ignore */ }
468
+ try {
469
+ const pm = adapter?.pluginManager || getService(ctx, "pluginManager");
470
+ if (pm?.hot === true || pm?.hmr !== undefined) return true;
471
+ if (pm?.ownerContext && typeof pm.ownerContext.get === "function" && pm.ownerContext.get("hmr") !== undefined) {
472
+ return true;
473
+ }
474
+ } catch { /* ignore */ }
475
+ try {
476
+ if (ctx && (ctx.hmr !== undefined || ctx.hot === true || ctx.isHmr === true)) return true;
477
+ } catch { /* ignore */ }
478
+ return false;
479
+ }