dsh-as-aistudio 0.2.13 → 0.3.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.
package/src/components.js CHANGED
@@ -26,7 +26,7 @@
26
26
  export const STUDIO_ID = 'dsh-as-aistudio'
27
27
 
28
28
  /** Keep in sync with package.json and src/client.js. */
29
- export const STUDIO_VERSION = '0.2.13'
29
+ export const STUDIO_VERSION = '0.3.0'
30
30
 
31
31
  /**
32
32
  * The AI Studio action strip this studio composes, in the order the user meets
package/src/index.js CHANGED
@@ -116,6 +116,27 @@ function safeGet(ctx, service) {
116
116
  }
117
117
  }
118
118
 
119
+ /**
120
+ * The version of the code this studio actually runs.
121
+ *
122
+ * Both halves of every component are vendored here (tools/vendor.mjs writes
123
+ * src/vendor/versions.json as it copies them), so a component package does not
124
+ * even have to be installed. Reading the version off the installed package would
125
+ * therefore answer "unknown" on a perfectly working studio - the exact report
126
+ * this function replaces.
127
+ *
128
+ * @returns {Record<string, string>} component id -> vendored version.
129
+ */
130
+ function vendoredVersions() {
131
+ try {
132
+ const raw = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), 'vendor', 'versions.json'), 'utf8')
133
+ const parsed = JSON.parse(raw)
134
+ return parsed !== null && typeof parsed === 'object' ? parsed : {}
135
+ } catch {
136
+ return {}
137
+ }
138
+ }
139
+
119
140
  /**
120
141
  * Resolve one component package from the profile.
121
142
  * @param pkg - the bare package name.
@@ -251,6 +272,7 @@ export function hostStateOf(component, entries, installed) {
251
272
  export function statusOf(ctx) {
252
273
  const entries = loaderEntries(ctx)
253
274
  const anchors = resolveAnchors(ctx)
275
+ const vendored = vendoredVersions()
254
276
  const components = COMPONENTS.map((component) => {
255
277
  const found = inspectPackage(component.package, anchors)
256
278
  const host = hostStateOf(component, entries, found.installed)
@@ -265,7 +287,10 @@ export function statusOf(ctx) {
265
287
  kind: component.kind,
266
288
  repo: component.repo,
267
289
  installed: found.installed,
268
- version: found.version,
290
+ // The version of the copy that is RUNNING. `installedVersion` is the
291
+ // package in the profile, which may be absent or a different build.
292
+ version: vendored[component.id] ?? found.version,
293
+ installedVersion: found.version,
269
294
  hostMounted: host.hostMounted,
270
295
  hostReason: host.hostReason,
271
296
  hostRow: host.hostRow,
package/src/shell.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // file - and carries the real component package in `config.plugin`. The shell
7
7
  // is what turns that name into a mount:
8
8
  //
9
- // const namespace = await import(config.plugin)
9
+ // const namespace = await import('./vendor/host/' + id + '.js')
10
10
  // await ctx.plugin(namespace.default ?? namespace.plugin ?? namespace)
11
11
  //
12
12
  // Why the row is a subpath of this package and not the component's own name:
@@ -28,7 +28,13 @@
28
28
  //
29
29
  // The row config is read once per activation. Editing it in the plugin manager
30
30
  // restarts the entry, which runs this apply again with the new value.
31
- import { STUDIO_ID } from './components.js'
31
+ //
32
+ // The component's HOST half is vendored into this package (tools/vendor.mjs),
33
+ // exactly like its browser half: `config.plugin` names which component the row
34
+ // is, and the vendored file beside this one is what gets mounted. That is why
35
+ // installing the studio installs nothing else - and why a component's fix
36
+ // reaches the studio only through a re-vendor and a studio release.
37
+ import { COMPONENTS, STUDIO_ID } from './components.js'
32
38
  import { pluginOf } from './index.js'
33
39
 
34
40
  /** Loader display name of this shell (all four rows share it). */
@@ -64,6 +70,14 @@ export const Config = {
64
70
  },
65
71
  }
66
72
 
73
+ /** The component id a row's `plugin` names, or null when it names none of ours. */
74
+ function componentIdOf(spec) {
75
+ for (const component of COMPONENTS) {
76
+ if (component.package === spec) return component.id
77
+ }
78
+ return null
79
+ }
80
+
67
81
  /** The package name a row config names, or null when it names none. */
68
82
  function pluginSpecOf(config) {
69
83
  if (config === null || config === undefined || typeof config !== 'object') return null
@@ -103,11 +117,22 @@ export async function apply(ctx, config) {
103
117
  STUDIO_ID + ': this component row names no plugin; set config.plugin to the component package (row config: ' + describeConfig(config) + ')',
104
118
  )
105
119
  }
120
+ const id = componentIdOf(spec)
121
+ if (id === null) {
122
+ throw new Error(
123
+ STUDIO_ID +
124
+ ': component ' +
125
+ JSON.stringify(spec) +
126
+ ' is not one of this studio\'s components (' +
127
+ COMPONENTS.map((component) => component.package).join(', ') +
128
+ ')',
129
+ )
130
+ }
106
131
  let namespace
107
132
  try {
108
- // The component is a dependency of this package, so the bare specifier
109
- // resolves from the profile that installed the studio.
110
- namespace = await import(spec)
133
+ // The host half is vendored beside this file (tools/vendor.mjs), so the
134
+ // component package does not have to be installed at all.
135
+ namespace = await import('./vendor/host/' + id + '.js')
111
136
  } catch (error) {
112
137
  throw new Error(
113
138
  STUDIO_ID + ': component ' + JSON.stringify(spec) + ' could not be imported: ' + messageOf(error),
@@ -0,0 +1,132 @@
1
+ // The platform packages a vendored component host half may import.
2
+ //
3
+ // A component copied into this package must NOT get its own copy of
4
+ // `@deepseek-ai/dsh-session`: two module instances mean two registries, and the
5
+ // second copy's `appendPluginRecord` cannot touch a Session the host created
6
+ // (the brand check inside it throws). The same goes for `dsh-tools`, whose
7
+ // `defineTool` builds validators against schemas the host's own tool registry
8
+ // expects.
9
+ //
10
+ // So the copies import THIS file, and this file resolves the two packages from
11
+ // the running harness. The harness lives in the `dsh` installation, so the base
12
+ // is found by walking up from this file to the nearest node_modules that has it,
13
+ // with the profile's own tree as the second try. A failure here is a failure of
14
+ // the whole component row and says exactly which package was missing - silence
15
+ // would report a component that never started.
16
+
17
+ import { createRequire } from 'node:module'
18
+ import { existsSync } from 'node:fs'
19
+ import { dirname, join } from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const here = dirname(fileURLToPath(import.meta.url))
23
+
24
+ /**
25
+ * Candidate base directories, nearest-first, for resolving a platform package.
26
+ *
27
+ * Two roots, because a plugin is loaded from one tree and the harness runs from
28
+ * another: this file's own ancestors (a profile that installed the packages
29
+ * beside it), then the RUNNING process's entry point - `process.argv[1]` is the
30
+ * `dsh` binary, and the platform packages sit in that installation's
31
+ * `node_modules`. ESM resolves relative to the importing FILE, so the second
32
+ * root has to be built by hand.
33
+ *
34
+ * @returns directory list to try with createRequire.
35
+ */
36
+ function bases() {
37
+ const out = []
38
+ const seen = new Set()
39
+ const add = (start) => {
40
+ if (typeof start !== 'string' || start === '') return
41
+ let node = start
42
+ for (let up = 0; up < 10; up += 1) {
43
+ const candidate = join(node, 'node_modules')
44
+ if (!seen.has(candidate) && existsSync(candidate)) {
45
+ seen.add(candidate)
46
+ out.push(candidate)
47
+ }
48
+ const parent = dirname(node)
49
+ if (parent === node) break
50
+ node = parent
51
+ }
52
+ }
53
+ add(here)
54
+ add(typeof process !== 'undefined' && process.argv ? process.argv[1] : '')
55
+ add(typeof process !== 'undefined' ? process.cwd() : '')
56
+ return out
57
+ }
58
+
59
+ /**
60
+ * Resolve one platform package and import it as ESM.
61
+ *
62
+ * @param spec - the platform package name.
63
+ * @returns its module namespace.
64
+ * @throws when no candidate base can resolve it, naming the specifier.
65
+ */
66
+ async function importPlatform(spec) {
67
+ for (const base of bases()) {
68
+ try {
69
+ const url = createRequire(join(base, 'noop.js')).resolve(spec)
70
+ return await import(url)
71
+ } catch {
72
+ continue
73
+ }
74
+ }
75
+ // Last resort: the bare specifier, which works when the caller (the profile)
76
+ // can already resolve it.
77
+ try {
78
+ return await import(spec)
79
+ } catch (error) {
80
+ throw new Error(
81
+ 'dsh-as-aistudio: the platform package ' +
82
+ JSON.stringify(spec) +
83
+ ' could not be resolved from ' +
84
+ here +
85
+ '; it ships with the dsh installation, so this copy is being loaded outside a harness (' +
86
+ String((error && error.message) || error) +
87
+ ')',
88
+ { cause: error },
89
+ )
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Resolve one platform package, or fail with a message that names it.
95
+ *
96
+ * Eager on purpose: a component whose platform package is missing must fail its
97
+ * row loudly (the Loader records the reason and /status reports hostMounted:
98
+ * false), never mount half a feature and answer "unsupported" forever after.
99
+ *
100
+ * @param spec - the platform package name.
101
+ * @returns its module namespace.
102
+ */
103
+ async function importPlatformOrThrow(spec) {
104
+ for (const base of bases()) {
105
+ try {
106
+ const url = createRequire(join(base, 'noop.js')).resolve(spec)
107
+ return await import(url)
108
+ } catch {
109
+ continue
110
+ }
111
+ }
112
+ try {
113
+ return await import(spec)
114
+ } catch (error) {
115
+ throw new Error(
116
+ 'dsh-as-aistudio: the platform package ' +
117
+ JSON.stringify(spec) +
118
+ ' could not be resolved from ' +
119
+ here +
120
+ '; it ships with the dsh installation, so this copy is being loaded outside a harness (' +
121
+ String((error && error.message) || error) +
122
+ ')',
123
+ { cause: error },
124
+ )
125
+ }
126
+ }
127
+
128
+ /** `@deepseek-ai/dsh-tools`, resolved from the running harness. */
129
+ export const dshTools = await importPlatformOrThrow('@deepseek-ai/dsh-tools')
130
+
131
+ /** `@deepseek-ai/dsh-session`, resolved from the running harness. */
132
+ export const dshSession = await importPlatformOrThrow('@deepseek-ai/dsh-session')