dsh-biocore 0.1.6 → 0.1.7

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
@@ -28,7 +28,7 @@ import { TRACE_ROUTE_PATH, defaultStoreDir, handleTraceRoute } from './modules/t
28
28
  import { queueSessionTurn } from './modules/turn-queue.js'
29
29
  import { anySwitchOn } from './modules/expression-inject.js'
30
30
  import { makeCoreSessionListener, registerCoreTurn } from './modules/core-turn.js'
31
- import { PLUGIN_DATA_DIR, resolveDataDir } from './modules/data-dir.js'
31
+ import { migrateLegacyPluginData, PLUGIN_DATA_DIR, resolveDataDir } from './modules/data-dir.js'
32
32
  import {
33
33
  PANEL_MOUNT_PATH,
34
34
  PANEL_ROUTE_PREFIX,
@@ -761,6 +761,7 @@ export async function handleState(req, res, stateDir, options) {
761
761
 
762
762
  export function apply(ctx, config) {
763
763
  const dataDir = resolveDataDir(config)
764
+ try { migrateLegacyPluginData(dataDir) } catch { /* best-effort data migration */ }
764
765
  try { migratePersonaToShared(dataDir) } catch { /* best-effort migration */ }
765
766
  const disposers = []
766
767
  const keep = (dispose) => {
@@ -4,21 +4,24 @@
4
4
  * <dataDir>/sessions/<session_id>.json (per-session BioCore state)
5
5
  * <dataDir>/flags.json (four switches)
6
6
  *
7
- * Resolution order (task-201):
7
+ * Resolution order:
8
8
  * 1. --profile <name> / --profile=<name> from process.argv
9
9
  * -> <homedir>/.dsh/profiles/<name>/data, only when that profile dir exists
10
10
  * 2. config.dataDir (cordis insert config)
11
11
  * 3. BIOCORE_UI_DATA_DIR
12
12
  * 4. <DSH_PROFILE_DIR>/data -- only when it is a real DSH profile directory
13
13
  * (contains package.json or dsh.yml)
14
- * 5. <plugin>/data (repo/deployed-copy fallback; never cwd)
14
+ * 5. <home>/.dsh/profiles/web/data -- when profiles/web/package.json exists
15
+ * 6. <home>/.dsh/profiles/<only>/data -- exactly one profile under profiles/
16
+ * 7. <plugin>/data -- last resort, logs __bioDataDirFallback
17
+ * (never cwd; previous node_modules data is migrated, not deleted)
15
18
  *
16
19
  * Why argv first: DSH passes the serving process its own --profile flag, but
17
20
  * DSH_PROFILE_DIR in the environment can point at the *default* profile (web)
18
21
  * even when the instance runs --profile web-bio. Trusting the env first wrote
19
22
  * session state into the wrong profile's data dir.
20
23
  */
21
- import { existsSync } from 'node:fs'
24
+ import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs'
22
25
  import { homedir } from 'node:os'
23
26
  import { fileURLToPath } from 'node:url'
24
27
  import { dirname, join, resolve } from 'node:path'
@@ -28,6 +31,10 @@ export const PROFILE_DIR_ENV = 'DSH_PROFILE_DIR'
28
31
  export const PROFILE_MARKERS = Object.freeze(['package.json', 'dsh.yml'])
29
32
  /** Fallback: the plugin checkout's own data/ directory. */
30
33
  export const PLUGIN_DATA_DIR = fileURLToPath(new URL('../../data', import.meta.url))
34
+ export const FALLBACK_WARN_PREFIX = '__bioDataDirFallback'
35
+ export const MIGRATE_LOG_PREFIX = '__bioDataMigrated'
36
+
37
+ let warnedFallback = false
31
38
 
32
39
  function text(value) {
33
40
  return typeof value === 'string' ? value.trim() : ''
@@ -69,6 +76,71 @@ export function isProfileDir(dir) {
69
76
  }
70
77
  }
71
78
 
79
+ /** <home>/.dsh/profiles */
80
+ export function profilesRootDir(homeDir) {
81
+ const home = text(homeDir) || homedir()
82
+ return join(resolve(home), '.dsh', 'profiles')
83
+ }
84
+
85
+ /** All real profile names directly under <home>/.dsh/profiles. */
86
+ export function listProfiles(homeDir) {
87
+ const root = profilesRootDir(homeDir)
88
+ let names = []
89
+ try {
90
+ names = readdirSync(root, { withFileTypes: true })
91
+ .filter((entry) => entry && entry.isDirectory())
92
+ .map((entry) => entry.name)
93
+ } catch { return [] }
94
+ return names.filter((name) => isProfileDir(join(root, name)))
95
+ }
96
+
97
+ function warnPluginFallback(path) {
98
+ if (warnedFallback) return
99
+ warnedFallback = true
100
+ try {
101
+ console.error(FALLBACK_WARN_PREFIX + ' using plugin data dir=' + path
102
+ + ' reason=no-profile-dir (writes will live inside the plugin package)')
103
+ } catch { /* logging must not throw */ }
104
+ }
105
+
106
+ /** Copy existing legacy plugin data into the resolved profile dataDir.
107
+ * Missing files are copied; existing files are never overwritten; the old
108
+ * plugin copy stays in place for rollback. */
109
+ export function migrateLegacyPluginData(dataDir, legacyDir) {
110
+ const target = text(dataDir)
111
+ const source = text(legacyDir) || PLUGIN_DATA_DIR
112
+ const result = { migrated: false, copied: 0, target, source }
113
+ if (!target || resolve(target) === resolve(source)) return result
114
+ // Only migrate into <profile>/data when the parent is a real profile root.
115
+ const parent = dirname(resolve(target))
116
+ if (!isProfileDir(parent)) return result
117
+ const copyIfMissing = (from, to) => {
118
+ try {
119
+ if (!existsSync(from) || existsSync(to)) return false
120
+ mkdirSync(dirname(to), { recursive: true })
121
+ copyFileSync(from, to)
122
+ result.copied += 1
123
+ return true
124
+ } catch { return false }
125
+ }
126
+ const files = ['flags.json', 'persona_profile.json', 'persona.md', 'config.json']
127
+ for (const name of files) copyIfMissing(join(source, name), join(target, name))
128
+ try {
129
+ const sessions = readdirSync(join(source, 'sessions'))
130
+ for (const name of sessions) {
131
+ copyIfMissing(join(source, 'sessions', name), join(target, 'sessions', name))
132
+ }
133
+ } catch { /* no legacy sessions */ }
134
+ result.migrated = result.copied > 0
135
+ if (result.migrated) {
136
+ try {
137
+ console.error(MIGRATE_LOG_PREFIX + ' from=' + source + ' to=' + target
138
+ + ' copied=' + result.copied)
139
+ } catch { /* logging must not throw */ }
140
+ }
141
+ return result
142
+ }
143
+
72
144
  /**
73
145
  * Resolve the one directory that owns flags.json and sessions/.
74
146
  * @param {object} [config] plugin config (config.dataDir)
@@ -103,5 +175,18 @@ export function resolveDataDir(config, env, argv, homeDir) {
103
175
  if (isProfileDir(candidate)) return join(candidate, 'data')
104
176
  }
105
177
 
106
- return resolve(PLUGIN_DATA_DIR)
107
- }
178
+ // 5) default web profile, when it is a real profile.
179
+ const webProfile = join(profilesRootDir(homeDir), 'web')
180
+ if (isProfileDir(webProfile)) return join(webProfile, 'data')
181
+
182
+ // 6) exactly one profile -> deterministic; multiple profiles are ambiguous.
183
+ const profiles = listProfiles(homeDir)
184
+ if (profiles.length === 1) {
185
+ return join(profilesRootDir(homeDir), profiles[0], 'data')
186
+ }
187
+
188
+ // 7) last resort: the plugin package itself.
189
+ const fallback = resolve(PLUGIN_DATA_DIR)
190
+ warnPluginFallback(fallback)
191
+ return fallback
192
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-biocore",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "BioCore DSH UI: per-session state tab, standalone instrument panel, /api/biocore/state, session/event one_turn updates, /bio sync.",
6
6
  "main": "lib/index.js",