@dsh-plugin/dsh-loader 1.0.0 → 1.1.0-dev.32278873528

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 (73) hide show
  1. package/README.md +251 -226
  2. package/{README.zh-CN.md → README.zh_CN.md} +227 -202
  3. package/bin/dshloader.mjs +42 -42
  4. package/cordis.patch.yml +8 -8
  5. package/dist/adapters/dsh-1-x.d.ts +23 -0
  6. package/dist/adapters/dsh-1-x.js +223 -0
  7. package/dist/adapters/dsh-1-x.js.map +1 -0
  8. package/dist/adapters/index.d.ts +22 -0
  9. package/dist/adapters/index.js +57 -0
  10. package/dist/adapters/index.js.map +1 -0
  11. package/dist/api.d.ts +13 -0
  12. package/dist/api.js +46 -0
  13. package/dist/api.js.map +1 -0
  14. package/dist/client.d.ts +157 -0
  15. package/dist/client.js +295 -0
  16. package/dist/client.js.map +1 -0
  17. package/dist/index.d.ts +52 -0
  18. package/dist/index.js +89 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/registry.d.ts +35 -0
  21. package/dist/registry.js +229 -0
  22. package/dist/registry.js.map +1 -0
  23. package/dist/services/services.d.ts +4 -0
  24. package/dist/services/services.js +28 -0
  25. package/dist/services/services.js.map +1 -0
  26. package/dist/services/settings.d.ts +41 -0
  27. package/dist/services/settings.js +161 -0
  28. package/dist/services/settings.js.map +1 -0
  29. package/dist/services/web.d.ts +10 -0
  30. package/dist/services/web.js +63 -0
  31. package/dist/services/web.js.map +1 -0
  32. package/dist/setup.d.ts +32 -0
  33. package/dist/setup.js +119 -0
  34. package/dist/setup.js.map +1 -0
  35. package/dist/types.d.ts +111 -0
  36. package/dist/types.js +12 -0
  37. package/dist/types.js.map +1 -0
  38. package/dist/version.d.ts +2 -0
  39. package/{src → dist}/version.js +5 -5
  40. package/dist/version.js.map +1 -0
  41. package/lib/client.js +283 -0
  42. package/lib/client.js.map +1 -0
  43. package/package.json +74 -59
  44. package/src/stable/agent.d.ts +1 -1
  45. package/src/stable/agent.js +2 -2
  46. package/src/stable/llm.d.ts +1 -1
  47. package/src/stable/llm.js +2 -2
  48. package/src/stable/runtime.d.ts +1 -1
  49. package/src/stable/runtime.js +5 -5
  50. package/src/stable/schema-form.d.ts +1 -1
  51. package/src/stable/schema-form.js +2 -2
  52. package/src/stable/settings.d.ts +1 -1
  53. package/src/stable/settings.js +2 -2
  54. package/src/stable/tools.d.ts +1 -1
  55. package/src/stable/tools.js +5 -5
  56. package/src/stable/ui-primitives.d.ts +4 -4
  57. package/src/stable/ui-primitives.js +6 -6
  58. package/src/stable/ui-settings.d.ts +6 -6
  59. package/src/stable/ui-settings.js +4 -4
  60. package/src/stable/ui-slots.d.ts +1 -1
  61. package/src/stable/ui-slots.js +2 -2
  62. package/src/stable/web-react.d.ts +1 -1
  63. package/src/stable/web-react.js +2 -2
  64. package/src/adapters/dsh-1-x.js +0 -236
  65. package/src/adapters/index.js +0 -59
  66. package/src/api.js +0 -56
  67. package/src/client.js +0 -330
  68. package/src/index.js +0 -105
  69. package/src/registry.js +0 -242
  70. package/src/services/services.js +0 -28
  71. package/src/services/settings.js +0 -174
  72. package/src/services/web.js +0 -71
  73. package/src/setup.mjs +0 -125
package/src/registry.js DELETED
@@ -1,242 +0,0 @@
1
- /**
2
- * Adapter registry + dsh version detection (design.md §3.1 / §3.2).
3
- *
4
- * Selection rules (mirrors design.md §3.1, five rules):
5
- * 1. exact match — adapter.supports === version
6
- * 2. range match — semver.satisfies(version, supports); when several
7
- * ranges cover the version, pick the narrowest; ties
8
- * broken by last-registered-wins.
9
- * 3. nearest-low fallback — no exact/range hit, but some adapters only cover
10
- * versions below the real one: pick the one whose
11
- * upper bound is closest, mark mode 'fallback', warn.
12
- * 4. version too old — real version is below every adapter's lower bound:
13
- * throw UnsupportedDshVersionError with a "too old"
14
- * message and the lowest supported version.
15
- * 5. version too new / empty registry — throw UnsupportedDshVersionError with
16
- * an "upgrade dshloader" message.
17
- *
18
- * Version detection priority (design.md §3.2):
19
- * 1. DSHLOADER_DSH_VERSION env var (highest; for tests/CI override)
20
- * 2. node_modules/@deepseek-ai/dsh/package.json#version
21
- * 3. ctx.runtime?.version — reserved for the future, NOT consulted in v1.
22
- * child_process `dsh --version` is deliberately NOT used (design.md §3.2).
23
- */
24
- import { readFileSync } from 'node:fs';
25
- import { join, resolve, dirname } from 'node:path';
26
- import { createRequire } from 'node:module';
27
- import semver from 'semver';
28
- import { LOG_PREFIX } from './version.js';
29
-
30
- const require = createRequire(import.meta.url);
31
-
32
- export class UnsupportedDshVersionError extends Error {
33
- constructor(message, { kind, version, minSupported } = {}) {
34
- super(message);
35
- this.name = 'UnsupportedDshVersionError';
36
- this.kind = kind; // 'too-old' | 'too-new'
37
- this.version = version;
38
- this.minSupported = minSupported;
39
- }
40
- }
41
-
42
- export class InvalidVersionError extends Error {
43
- constructor(message, { version } = {}) {
44
- super(message);
45
- this.name = 'InvalidVersionError';
46
- this.version = version;
47
- }
48
- }
49
-
50
- /**
51
- * Resolve the installed dsh version.
52
- *
53
- * @param {{ profileDir?: string, dshPkgPath?: string, env?: NodeJS.ProcessEnv }} [opts]
54
- * @returns {string|undefined} semver version string, or undefined when unreachable
55
- */
56
- export function detectDshVersion(opts = {}) {
57
- const env = opts.env ?? process.env;
58
- const envVal = env.DSHLOADER_DSH_VERSION;
59
- if (typeof envVal === 'string' && envVal.trim() !== '') {
60
- return envVal.trim();
61
- }
62
- const candidates = [];
63
- if (opts.dshPkgPath) candidates.push(opts.dshPkgPath);
64
- if (opts.profileDir) {
65
- candidates.push(join(opts.profileDir, 'node_modules', '@deepseek-ai', 'dsh', 'package.json'));
66
- }
67
- // Resolve from the loader's own location (profile node_modules sits beside it).
68
- try {
69
- candidates.push(require.resolve('@deepseek-ai/dsh/package.json'));
70
- } catch { /* not installed — skip */ }
71
- // Global node_modules — dsh is typically installed globally (the runtime
72
- // that loads profiles, not a profile dependency). Derive from the Node.js
73
- // executable: /opt/homebrew/bin/node → /opt/homebrew/lib/node_modules.
74
- const globalRoot = resolve(dirname(process.execPath), '..', 'lib', 'node_modules');
75
- candidates.push(join(globalRoot, '@deepseek-ai', 'dsh', 'package.json'));
76
- // Walk up from cwd as a last resort.
77
- let dir = process.cwd();
78
- for (let i = 0; i < 8; i += 1) {
79
- candidates.push(join(dir, 'node_modules', '@deepseek-ai', 'dsh', 'package.json'));
80
- const parent = resolve(dir, '..');
81
- if (parent === dir) break;
82
- dir = parent;
83
- }
84
- for (const path of candidates) {
85
- try {
86
- const pkg = JSON.parse(readFileSync(path, 'utf8'));
87
- if (typeof pkg.version === 'string' && pkg.version.trim() !== '') {
88
- return pkg.version.trim();
89
- }
90
- } catch { /* try next */ }
91
- }
92
- return undefined;
93
- }
94
-
95
- /**
96
- * Parse a semver range into { lower, upper } bounds. Each bound is
97
- * { v: SemVer, inc: boolean } or null when unbounded on that side.
98
- */
99
- function rangeBounds(range) {
100
- const r = new semver.Range(range);
101
- let lower = null;
102
- let upper = null;
103
- for (const group of r.set) {
104
- for (const c of group) {
105
- const op = c.operator;
106
- const v = c.semver; // parsed SemVer (c.value is the raw comparator string)
107
- if (op === '' || op === '=') {
108
- lower = { v, inc: true };
109
- upper = { v, inc: true };
110
- } else if (op === '>') {
111
- lower = { v, inc: false };
112
- } else if (op === '>=') {
113
- lower = { v, inc: true };
114
- } else if (op === '<') {
115
- upper = { v, inc: false };
116
- } else if (op === '<=') {
117
- upper = { v, inc: true };
118
- }
119
- }
120
- }
121
- return { lower, upper };
122
- }
123
-
124
- /** True when every version the range covers is strictly below `version`. */
125
- function isLowerCandidate(range, version) {
126
- const { upper } = rangeBounds(range);
127
- if (!upper) return false; // unbounded above → can cover version, not a lower candidate
128
- return upper.inc ? semver.gt(version, upper.v) : semver.gte(version, upper.v);
129
- }
130
-
131
- /** Lowest version that satisfies the range (semver.minVersion). */
132
- function rangeMinVersion(range) {
133
- const min = semver.minVersion(range);
134
- return min ? min.version : null;
135
- }
136
-
137
- export class AdapterRegistry {
138
- constructor() {
139
- /** @type {Array<{supports: string, name: string, create: Function}>} */
140
- this.adapters = [];
141
- }
142
-
143
- register(factory) {
144
- if (!factory || typeof factory.supports !== 'string' || typeof factory.create !== 'function') {
145
- throw new TypeError('AdapterFactory must expose { supports: string, create: function }');
146
- }
147
- this.adapters.push(factory);
148
- return this;
149
- }
150
-
151
- /**
152
- * @param {string} version real dsh version
153
- * @returns {{ factory: object, mode: 'exact'|'range'|'fallback' }}
154
- */
155
- select(version) {
156
- if (!semver.valid(version)) {
157
- throw new InvalidVersionError(
158
- `${LOG_PREFIX} cannot parse dsh version "${version}" as semver`,
159
- { version },
160
- );
161
- }
162
- if (this.adapters.length === 0) {
163
- throw new UnsupportedDshVersionError(
164
- `${LOG_PREFIX} no adapter registered for dsh ${version}; please upgrade @dsh-plugin/dsh-loader`,
165
- { kind: 'too-new', version },
166
- );
167
- }
168
-
169
- // Rule 1 + 2: exact and range matches.
170
- const matches = [];
171
- for (const factory of this.adapters) {
172
- if (factory.supports === version) {
173
- matches.push({ factory, mode: 'exact' });
174
- } else if (semver.satisfies(version, factory.supports)) {
175
- matches.push({ factory, mode: 'range' });
176
- }
177
- }
178
- if (matches.length > 0) {
179
- // Narrowest range wins; tie → last registered.
180
- let best = matches[matches.length - 1];
181
- for (let i = matches.length - 1; i >= 0; i -= 1) {
182
- const candidate = matches[i];
183
- const narrowest = matches.every((other, j) => {
184
- if (i === j) return true;
185
- try {
186
- return semver.subset(candidate.factory.supports, other.factory.supports);
187
- } catch {
188
- return false;
189
- }
190
- });
191
- if (narrowest) {
192
- best = candidate;
193
- break;
194
- }
195
- }
196
- // Exact match always beats a range match on the same version.
197
- const exact = matches.find((m) => m.mode === 'exact');
198
- if (exact) best = exact;
199
- return { factory: best.factory, mode: best.mode };
200
- }
201
-
202
- // Rule 3: nearest-low fallback.
203
- const lowers = this.adapters
204
- .filter((f) => isLowerCandidate(f.supports, version))
205
- .map((f) => {
206
- const { upper } = rangeBounds(f.supports);
207
- return { factory: f, upper };
208
- });
209
- if (lowers.length > 0) {
210
- lowers.sort((a, b) => {
211
- const cmp = semver.compare(b.upper.v, a.upper.v);
212
- if (cmp !== 0) return cmp;
213
- // exclusive upper is "higher" than inclusive at the same version
214
- return (b.upper.inc ? 0 : 1) - (a.upper.inc ? 0 : 1);
215
- });
216
- const chosen = lowers[0];
217
- console.warn(
218
- `${LOG_PREFIX} no exact adapter for dsh ${version}; falling back to "${chosen.factory.name}" (supports ${chosen.factory.supports})`,
219
- );
220
- return { factory: chosen.factory, mode: 'fallback' };
221
- }
222
-
223
- // Rule 4 vs 5: too old vs too new.
224
- const minVersions = this.adapters
225
- .map((f) => rangeMinVersion(f.supports))
226
- .filter(Boolean)
227
- .map((v) => semver.parse(v));
228
- if (minVersions.length > 0) {
229
- const lowest = minVersions.reduce((acc, v) => (semver.lt(v, acc) ? v : acc));
230
- if (semver.lt(version, lowest)) {
231
- throw new UnsupportedDshVersionError(
232
- `${LOG_PREFIX} current dsh version ${version} is too old; dshloader minimum supported is ${lowest.version}. Please upgrade dsh or use an older dshloader release.`,
233
- { kind: 'too-old', version, minSupported: lowest.version },
234
- );
235
- }
236
- }
237
- throw new UnsupportedDshVersionError(
238
- `${LOG_PREFIX} no adapter covers dsh ${version}; please upgrade @dsh-plugin/dsh-loader`,
239
- { kind: 'too-new', version },
240
- );
241
- }
242
- }
@@ -1,28 +0,0 @@
1
- // Services stable API (design.md §3.3 / §4.1 `ctx.dshLoader.services`).
2
- //
3
- // Thin read/alias helpers over the cordis service registry. `alias()` is a
4
- // low-level escape hatch for plugin authors who need a one-hop alias beyond
5
- // what the selected adapter already provides; it never overwrites an existing
6
- // service (mirrors the adapter safety rule in design.md §5.1).
7
-
8
- import { LOG_PREFIX } from '../version.js';
9
-
10
- export function createServicesAPI({ ctx }) {
11
- return {
12
- get(name) {
13
- return ctx.get(name);
14
- },
15
- alias(from, to) {
16
- if (ctx.get(from) !== undefined) {
17
- console.warn(`${LOG_PREFIX} services.alias: "${from}" already exists, skip alias`);
18
- return;
19
- }
20
- const target = ctx.get(to);
21
- if (target === undefined) {
22
- console.warn(`${LOG_PREFIX} services.alias: target "${to}" unavailable, cannot alias "${from}"`);
23
- return;
24
- }
25
- ctx.reflect.provide(from, target);
26
- },
27
- };
28
- }
@@ -1,174 +0,0 @@
1
- // Settings stable API (design.md §3.3.1 / §4.1 / §4.2).
2
- //
3
- // Two concerns are deliberately separated:
4
- // 1. naming/shape differences — handled by proxying to ctx.get('settings').
5
- // 2. access scope (security) — `exposeAllNamespaces` controls whether
6
- // `describe` returns only the official browser whitelist namespaces
7
- // (default, matches official behavior) or every registered namespace.
8
- //
9
- // Host-side writes always proxy through to the real settings service: host
10
- // plugin code is already trusted (it runs in the dsh Node process and could
11
- // call ctx.get('settings').update directly). The whitelist is a browser-side
12
- // default-deny boundary owned by dsh-host-apiproxy; the browser path is
13
- // covered separately by the client fetch interceptor (src/client.js).
14
- import { LOG_PREFIX } from '../version.js';
15
-
16
- /**
17
- * Official browser-writable settings namespaces (dsh-host-apiproxy
18
- * WEB_SETTINGS_NAMESPACES), as documented in dsh-upstream-fixes/README.md.
19
- * Product namespaces and dynamic model-provider namespaces are not enumerable
20
- * without dsh internals; callers may extend this set via `extraWhitelist`.
21
- */
22
- export const DEFAULT_WEB_SETTINGS_NAMESPACES = new Set([
23
- 'agent-loop',
24
- 'shell',
25
- 'locale',
26
- 'permission',
27
- 'ui-conversation',
28
- 'ui-theme',
29
- 'web-search-deepseek',
30
- ]);
31
-
32
- /**
33
- * Shape a raw settings descriptor into the official NamespaceView wire shape
34
- * (mirrors dsh-upstream-fixes/lib/index.js `namespaceView`).
35
- */
36
- export function toNamespaceView(descriptor) {
37
- const view = {
38
- ns: String(descriptor.ns),
39
- schema: descriptor.schema,
40
- value: descriptor.value,
41
- ...(descriptor.base === undefined ? {} : { base: descriptor.base }),
42
- ...(descriptor.user === undefined ? {} : { user: descriptor.user }),
43
- applies: descriptor.applies,
44
- secrets: (descriptor.secrets ?? []).map((s) => ({ path: [...s.path], set: s.set })),
45
- revision: descriptor.revision,
46
- };
47
- return view;
48
- }
49
-
50
- /**
51
- * Map a thrown settings error into a SettingsResult, preserving the official
52
- * `settings-conflict` / `settings-rejected` classification.
53
- */
54
- export function settingsErrorToResult(error, ns, method) {
55
- const isObject = error !== null && typeof error === 'object';
56
- const isConflict = isObject && ('expected' in error || 'actual' in error);
57
- if (isConflict) {
58
- return {
59
- ok: false,
60
- code: 'settings-conflict',
61
- message: error.message ?? String(error),
62
- details: {
63
- ns,
64
- ...(error.expected === undefined ? {} : { expected: error.expected }),
65
- ...(error.actual === undefined ? {} : { actual: error.actual }),
66
- },
67
- };
68
- }
69
- return {
70
- ok: false,
71
- code: 'settings-rejected',
72
- message: error instanceof Error ? error.message : String(error),
73
- details: { ns, method },
74
- };
75
- }
76
-
77
- /**
78
- * Build the `ctx.dshLoader.settings` stable API.
79
- *
80
- * @param {{ ctx: object, exposeAllNamespaces: boolean, whitelist?: Set<string> }} opts
81
- */
82
- export function createSettingsAPI({ ctx, exposeAllNamespaces, whitelist }) {
83
- const allowed = whitelist ?? DEFAULT_WEB_SETTINGS_NAMESPACES;
84
-
85
- function getSettings() {
86
- return ctx.get('settings');
87
- }
88
-
89
- function filterNamespaces(views) {
90
- if (exposeAllNamespaces) return views;
91
- return views.filter((v) => allowed.has(String(v.ns)));
92
- }
93
-
94
- const api = {
95
- exposeAllNamespaces: Boolean(exposeAllNamespaces),
96
-
97
- /**
98
- * Register a settings namespace. Proxies to the real settings service's
99
- * `register(ns, schema, options)` and returns the owner scope
100
- * ({ get, watch }) — the same shape the official service returns.
101
- *
102
- * Unlike describe/update/replace/mutate, register is NOT filtered by the
103
- * whitelist: host plugin code is trusted and registering a namespace is
104
- * a composition-time act, not a browser-facing read/write.
105
- *
106
- * @param {string} ns - unique namespace (lowercase kebab-case)
107
- * @param {object} schema - schemastery schema for this namespace
108
- * @param {{ base?: object, applies?: 'live'|'restart', validate?: (value:any)=>void }} [options]
109
- * @returns {{ get: () => any, watch: (cb: (value:any)=>void) => () => void } | undefined}
110
- */
111
- register(ns, schema, options) {
112
- const settings = getSettings();
113
- if (settings === undefined || typeof settings.register !== 'function') {
114
- console.warn(`${LOG_PREFIX}:settings.register settings service unavailable`);
115
- return undefined;
116
- }
117
- return settings.register(ns, schema, options);
118
- },
119
-
120
- describe(options = {}) {
121
- const settings = getSettings();
122
- if (settings === undefined || typeof settings.describe !== 'function') {
123
- return [];
124
- }
125
- const redactSecrets = options.redactSecrets !== false;
126
- const descriptors = settings.describe({ redactSecrets });
127
- const views = (descriptors ?? []).map(toNamespaceView);
128
- return filterNamespaces(views);
129
- },
130
-
131
- async _write(method, ns, section, expectedRevision) {
132
- const settings = getSettings();
133
- if (settings === undefined) {
134
- return {
135
- ok: false,
136
- code: 'internal',
137
- message: `${LOG_PREFIX}:settings.${method} settings service unavailable`,
138
- details: { ns },
139
- };
140
- }
141
- try {
142
- if (method === 'update') await settings.update(ns, section, expectedRevision);
143
- else if (method === 'replace') await settings.replace(ns, section, expectedRevision);
144
- else await settings.mutate(ns, section, expectedRevision);
145
- } catch (error) {
146
- return settingsErrorToResult(error, ns, method);
147
- }
148
- const descriptor = settings
149
- .describe({ redactSecrets: true })
150
- .find((d) => String(d.ns) === String(ns));
151
- if (descriptor === undefined) {
152
- return {
153
- ok: false,
154
- code: 'internal',
155
- message: `${LOG_PREFIX}:settings.${method} namespace disposed after write`,
156
- details: { ns },
157
- };
158
- }
159
- return { ok: true, value: toNamespaceView(descriptor) };
160
- },
161
-
162
- update(ns, section, expectedRevision) {
163
- return api._write('update', ns, section, expectedRevision);
164
- },
165
- replace(ns, section, expectedRevision) {
166
- return api._write('replace', ns, section, expectedRevision);
167
- },
168
- mutate(ns, ops, expectedRevision) {
169
- return api._write('mutate', ns, ops, expectedRevision);
170
- },
171
- };
172
-
173
- return api;
174
- }
@@ -1,71 +0,0 @@
1
- // Web server stable API (design.md §3.3.2 / §4.1).
2
- //
3
- // Routes every registration through the real `webServer` (or `httpServer`
4
- // alias) `register({ kind, ... })` call shape used by dsh 1.x, mirroring
5
- // dsh-upstream-fixes/lib/index.js `registerRoutes`. Each method returns a
6
- // dispose function that removes the registration when the underlying
7
- // service supports it.
8
- import { LOG_PREFIX } from '../version.js';
9
-
10
- export class DshLoaderWebError extends Error {
11
- constructor(message) {
12
- super(message);
13
- this.name = 'DshLoaderWebError';
14
- }
15
- }
16
-
17
- /** Resolve the active web server service (webServer preferred, httpServer alias next). */
18
- function resolveWebServer(ctx) {
19
- return ctx.get('webServer') ?? ctx.get('httpServer');
20
- }
21
-
22
- /**
23
- * Build the `ctx.dshLoader.web` stable API.
24
- * @param {{ ctx: object }} opts
25
- */
26
- export function createWebAPI({ ctx }) {
27
- function server() {
28
- const web = resolveWebServer(ctx);
29
- if (web === undefined || typeof web.register !== 'function') {
30
- throw new DshLoaderWebError(
31
- `${LOG_PREFIX}:web webServer service unavailable`,
32
- );
33
- }
34
- return web;
35
- }
36
-
37
- return {
38
- register(prefix, handler) {
39
- const web = server();
40
- return web.register({ kind: 'prefix', path: prefix, handler }) ?? (() => {});
41
- },
42
- get(path, handler) {
43
- const web = server();
44
- return web.register({ kind: 'route', method: 'GET', path, handler }) ?? (() => {});
45
- },
46
- post(path, handler) {
47
- const web = server();
48
- return web.register({ kind: 'route', method: 'POST', path, handler }) ?? (() => {});
49
- },
50
- use(middleware) {
51
- const web = server();
52
- return web.register({ kind: 'middleware', handler: middleware }) ?? (() => {});
53
- },
54
- /**
55
- * Register a WebSocket upgrade route for an exact pathname.
56
- * Proxies to `webServer.registerUpgrade({ path, handler })`.
57
- *
58
- * @param {{ path: string, handler: (req: any, socket: any, head: Buffer) => void }} route
59
- * @returns {() => void} dispose function that removes the upgrade route
60
- */
61
- registerUpgrade(route) {
62
- const web = server();
63
- if (typeof web.registerUpgrade !== 'function') {
64
- throw new DshLoaderWebError(
65
- `${LOG_PREFIX}:web.registerUpgrade webServer does not support upgrade routes (registerUpgrade missing)`,
66
- );
67
- }
68
- return web.registerUpgrade(route) ?? (() => {});
69
- },
70
- };
71
- }
package/src/setup.mjs DELETED
@@ -1,125 +0,0 @@
1
- // One-shot profile injection script (design.md §3.6 / §6 / M6).
2
- //
3
- // `dshloader setup <profile>`:
4
- // - adds `@dsh-plugin/dsh-loader` to the profile package.json dependencies
5
- // (if missing);
6
- // - appends the dshloader `insert` entry to cordis.patch.yml (if missing);
7
- // - does NOT reorder the insert list — cordis is reactive DI, so position
8
- // does not affect whether service aliases / module redirects take effect
9
- // (design.md §1.2 / §6.2).
10
- //
11
- // `dshloader dump-config <profile>`: best-effort validation that runs
12
- // `dsh --profile <name> --dump-config` when the dsh CLI is available.
13
- //
14
- // `dshloader info [profile]`: prints dshloader version, detected dsh version,
15
- // selected adapter, and registered aliases (AC-OB-03, P2 — minimal).
16
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
17
- import { join, resolve } from 'node:path';
18
- import { spawnSync } from 'node:child_process';
19
- import { LOADER_VERSION, LOG_PREFIX } from './version.js';
20
- import { detectDshVersion, AdapterRegistry, UnsupportedDshVersionError } from './registry.js';
21
- import { registerHostAdapters } from './adapters/index.js';
22
-
23
- const LOADER_PKG = '@dsh-plugin/dsh-loader';
24
- const PATCH_ENTRY = `- id: dsh-loader\n name: '${LOADER_PKG}'`;
25
-
26
- export function dshHome() {
27
- const env = process.env.DSH_HOME?.trim();
28
- return env ? resolve(env) : join(process.env.HOME ?? '~', '.dsh');
29
- }
30
-
31
- export function profileDir(profileName) {
32
- return join(dshHome(), 'profiles', profileName);
33
- }
34
-
35
- function readJson(path) {
36
- try {
37
- return JSON.parse(readFileSync(path, 'utf8'));
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- function writeJson(path, data) {
44
- writeFileSync(path, JSON.stringify(data, undefined, 2) + '\n');
45
- }
46
-
47
- /** Ensure dshloader is listed in the profile package.json dependencies. */
48
- export function injectDependency(pkgPath) {
49
- const manifest = readJson(pkgPath) ?? { name: '', dependencies: {} };
50
- manifest.dependencies = manifest.dependencies ?? {};
51
- if (manifest.dependencies[LOADER_PKG] === undefined) {
52
- manifest.dependencies[LOADER_PKG] = '^' + LOADER_VERSION;
53
- writeJson(pkgPath, manifest);
54
- return { added: true, manifest };
55
- }
56
- return { added: false, manifest };
57
- }
58
-
59
- /** Ensure the dshloader insert entry exists in cordis.patch.yml (no reorder). */
60
- export function injectPatch(patchPath) {
61
- let text = '';
62
- try {
63
- text = readFileSync(patchPath, 'utf8');
64
- } catch {
65
- text = '';
66
- }
67
- if (text.includes("id: dsh-loader")) {
68
- return { added: false, text };
69
- }
70
- const insertion = `- insert:\n ${PATCH_ENTRY}\n`;
71
- const next = text.length === 0 ? insertion : text.endsWith('\n') ? text + insertion : text + '\n' + insertion;
72
- writeFileSync(patchPath, next);
73
- return { added: true, text: next };
74
- }
75
-
76
- /**
77
- * Run `dshloader setup <profile>`.
78
- * @param {string} profileName
79
- * @returns {{ profileDir: string, dependencyAdded: boolean, patchAdded: boolean }}
80
- */
81
- export function setupProfile(profileName) {
82
- const dir = profileDir(profileName);
83
- if (!existsSync(dir)) {
84
- throw new Error(`${LOG_PREFIX} profile directory not found: ${dir}`);
85
- }
86
- const pkgPath = join(dir, 'package.json');
87
- const patchPath = join(dir, 'cordis.patch.yml');
88
- const dep = injectDependency(pkgPath);
89
- const patch = injectPatch(patchPath);
90
- console.log(`${LOG_PREFIX} setup ${profileName}: dependency ${dep.added ? 'added' : 'already present'}, patch ${patch.added ? 'appended' : 'already present'}`);
91
- console.log(`${LOG_PREFIX} note: insert position is irrelevant — cordis reactive DI resolves aliases regardless of order (design.md §6.2)`);
92
- return { profileDir: dir, dependencyAdded: dep.added, patchAdded: patch.added };
93
- }
94
-
95
- /** Best-effort dump-config validation. Returns { ok, output } (never throws). */
96
- export function dumpConfig(profileName) {
97
- const dir = profileDir(profileName);
98
- if (!existsSync(dir)) {
99
- return { ok: false, output: `${LOG_PREFIX} profile directory not found: ${dir}` };
100
- }
101
- const result = spawnSync('dsh', ['--profile', profileName, '--dump-config'], {
102
- encoding: 'utf8',
103
- timeout: 60_000,
104
- });
105
- const output = (result.stdout ?? '') + (result.stderr ?? '');
106
- return { ok: result.status === 0, output };
107
- }
108
-
109
- /** Print dshloader status for a profile (AC-OB-03, minimal). */
110
- export function info(profileName) {
111
- console.log(`${LOG_PREFIX} version ${LOADER_VERSION}`);
112
- const dir = profileName ? profileDir(profileName) : undefined;
113
- const dshVersion = detectDshVersion(dir ? { profileDir: dir } : {});
114
- console.log(`${LOG_PREFIX} detected dsh version: ${dshVersion ?? '<unknown>'}`);
115
- if (dshVersion) {
116
- try {
117
- const reg = registerHostAdapters(new AdapterRegistry());
118
- const { factory, mode } = reg.select(dshVersion);
119
- console.log(`${LOG_PREFIX} selected adapter: ${factory.name} (supports ${factory.supports}, mode ${mode})`);
120
- } catch (error) {
121
- console.log(`${LOG_PREFIX} adapter selection: ${error.message}`);
122
- }
123
- }
124
- return { loaderVersion: LOADER_VERSION, dshVersion };
125
- }