dshmarket 1.32.0 → 1.33.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/README.md CHANGED
@@ -37,6 +37,7 @@ its own dsh: it may be older than the one `npm` would give you (#139).
37
37
  - **One-click install** — confirm the source, watch live progress; most plugins go live after a page refresh, no restart
38
38
  - **Backup & restore** — export your profile's plugin list and configuration as readable JSON, import it on another machine, store it on WebDAV with daily auto-backup, or sync through a private GitHub Gist; restores **merge** (plugins installed after the backup are kept), validate before writing, and roll back on failure
39
39
  - **Updates** — per-plugin update checks (npm version or pinned commit vs HEAD), one-click update, or update everything at once; the market updates itself the same way
40
+ - **Public update API** — plugin-owned settings pages can use the versioned, capability-gated [update API v1](UPDATE-API-V1.md) (beta) instead of copying package-manager logic or depending on private Market UI responses
40
41
  - **Uninstall** — two-step confirm; plugins installed this session are removed live
41
42
  - **Hot disable / enable** — toggles write `- id: …` + `disabled: true|false` into the profile's `cordis.patch.yml` (the official patch layer, mechanism ported from [dsh-plugin-hub](https://github.com/Noob-stupid/dsh-plugin-hub)): DSH's HMR re-composes within ~1s, no restart, and the loader re-applies the choice on every boot; hand-edited patch rows show as badges, host-infrastructure plugins are protected from toggling, and a malformed patch file is never made worse
42
43
  - **Restart when needed** — changes that cannot hot-load show a one-click restart beside the pending-change banner; the action is restricted to same-origin loopback requests
package/README.zh.md CHANGED
@@ -35,6 +35,7 @@ dsh plugin --profile web add dshmarket
35
35
  - **一键安装**——确认来源,实时进度;多数插件刷新页面即可用,无需重启
36
36
  - **备份与恢复**——把 profile 的插件清单与配置导出为可读 JSON,换机导入,存到 WebDAV 并每日自动备份,或通过私有 GitHub Gist 跨机器同步;恢复采用**合并**方式(备份之后新装的插件会保留),写入前校验、失败自动回滚
37
37
  - **更新**——逐插件检测(npm 版本或锁定 commit 对比 HEAD),一键更新或全部更新;市场自己也走同一通道升级
38
+ - **公共更新接口**——插件自己的设置页可调用带版本号、能力探测和回滚状态的[更新 API v1](UPDATE-API-V1.md)(beta),无需复制包管理逻辑,也不依赖市场 UI 的私有响应字段
38
39
  - **卸载**——两步确认防误触;本次会话装的插件即点即卸
39
40
  - **热禁用 / 启用**——开关会往 profile 的 `cordis.patch.yml`(官方补丁层,机制移植自 [dsh-plugin-hub](https://github.com/Noob-stupid/dsh-plugin-hub))写入 `- id: …` + `disabled: true|false`:DSH 的 HMR 约 1 秒内重新组合,无需重启,loader 每次启动都会重新应用这个选择;手工改过的补丁行会显示成徽标,宿主基础设施插件禁止开关,补丁文件格式不对时绝不会被写得更糟
40
41
  - **按需重启**——无法热加载的变更会在待重启提示旁显示一键重启;操作仅接受本机同源请求
@@ -0,0 +1,115 @@
1
+ # Public plugin update API v1
2
+
3
+ > **Status: beta.** The shape described here may still change between
4
+ > releases. `GET /dsh-market/api/v1/capabilities` reports
5
+ > `"stability": "beta"` while that is true, and `"stable"` once it stops
6
+ > moving — read that field rather than assuming from the `v1` in the path.
7
+ >
8
+ > Nothing here is going away; what is not yet promised is that field names
9
+ > and response shapes will survive untouched. If you ship against it now,
10
+ > say so in an issue: a shape somebody depends on is a much stronger reason
11
+ > not to move it, and it is how this reaches `stable`.
12
+
13
+
14
+ `dshmarket` exposes a small, versioned, same-origin JSON API for plugin-owned
15
+ update surfaces. It lets a plugin show its own update button without spawning a
16
+ package manager, copying the Market installation algorithm, or binding to the
17
+ Market UI's private response fields.
18
+
19
+ All responses carry:
20
+
21
+ ```json
22
+ { "schema": "dsh-market/update-api/v1" }
23
+ ```
24
+
25
+ Clients must discover the API before enabling mutation controls:
26
+
27
+ ```http
28
+ GET /dsh-market/api/v1/capabilities
29
+ ```
30
+
31
+ The response names the Market version, profile, runtime (`web` or `desktop`),
32
+ supported features, restart owner and endpoint paths. A client must hide its
33
+ restart button when `restart.supported` is false. Desktop and supervised hosts
34
+ normally delegate restart to their owning shell or operator.
35
+
36
+ ## Check one installed package
37
+
38
+ ```http
39
+ GET /dsh-market/api/v1/updates?name=dsh-mcp-connector&force=1
40
+ ```
41
+
42
+ The response includes the installed version, target version, source kind and
43
+ whether the target is a forward update. Omitting `force=1` allows the Market's
44
+ short update-check cache.
45
+
46
+ ## Start and observe an update
47
+
48
+ Mutation requests require the same-origin protection used by the Market UI.
49
+
50
+ ```http
51
+ POST /dsh-market/api/v1/updates
52
+ Content-Type: application/json
53
+
54
+ { "packageName": "dsh-mcp-connector" }
55
+ ```
56
+
57
+ An accepted request returns HTTP `202` immediately with an `operationId`.
58
+ Passing `"force": true` opts this one operation out of the registry release-age
59
+ wait; clients should offer it only after the normal operation reports
60
+ `RELEASE_TOO_FRESH` or `VERSION_UNCHANGED`.
61
+
62
+ Poll the operation by id:
63
+
64
+ ```http
65
+ GET /dsh-market/api/v1/operations?operationId=<id>
66
+ ```
67
+
68
+ States are `queued`, `running`, `succeeded`, `failed`, `cancelled` and
69
+ `rolled-back`. Running operations include structured package progress when
70
+ pnpm provides it. Terminal operations include:
71
+
72
+ - the before and actually installed versions;
73
+ - `refreshRequired` and `restartRequired` outcomes;
74
+ - a stable failure code, bounded user-facing message and retryability;
75
+ - whether a compatibility rollback is currently available.
76
+
77
+ Up to 50 operation records live in the current Host process. A boot id is
78
+ embedded in every operation id, so a client never mistakes a stale browser
79
+ record for a task belonging to the replacement process.
80
+
81
+ ## Roll back
82
+
83
+ ```http
84
+ POST /dsh-market/api/v1/rollback
85
+ Content-Type: application/json
86
+
87
+ { "operationId": "<id>" }
88
+ ```
89
+
90
+ Rollback is intentionally capability- and operation-scoped. It is available
91
+ only when the Market's compatibility verification retained a recovery point;
92
+ a later mutation may supersede it. The normalized result is written back to
93
+ the same operation record.
94
+
95
+ ## Restart
96
+
97
+ ```http
98
+ POST /dsh-market/api/v1/restart
99
+ Content-Type: application/json
100
+
101
+ {}
102
+ ```
103
+
104
+ This preserves the Market's stricter restart guard: direct loopback,
105
+ same-origin, no forwarding headers, no package mutation in progress, and a Host
106
+ whose lifecycle is not owned by Desktop or a supervisor. Clients must feature
107
+ detect it; they must not invent an alternative process-control path.
108
+
109
+ ## Compatibility policy
110
+
111
+ - New optional response fields may be added within v1.
112
+ - Existing v1 fields and meanings are not repurposed.
113
+ - A breaking change uses a new path and schema version.
114
+ - When discovery is unavailable, plugin UIs should fall back to opening the
115
+ Market rather than calling legacy `/dsh-market/*` mutation routes directly.
package/lib/check.js CHANGED
@@ -221,7 +221,7 @@ function readNodeModulesVersion(base, name) {
221
221
  * hoisting (`<profiles>/node_modules/…` when the profile lives under
222
222
  * `<profiles>/<name>`) and mirrors the Loader's package search roots.
223
223
  */
224
- function resolvePackageDir(anchorPackageJson, name) {
224
+ function resolvePackageDir(anchorPackageJson, name, ignoredPackageDirectory) {
225
225
  let paths = [];
226
226
  try {
227
227
  paths = createRequire(anchorPackageJson).resolve.paths(name) ?? [];
@@ -229,8 +229,19 @@ function resolvePackageDir(anchorPackageJson, name) {
229
229
  catch {
230
230
  return null;
231
231
  }
232
+ const ignored = ignoredPackageDirectory === undefined
233
+ ? null
234
+ : resolve(ignoredPackageDirectory);
232
235
  for (const searchPath of paths) {
233
236
  const candidate = join(searchPath, name);
237
+ if (ignored !== null) {
238
+ const resolvedCandidate = resolve(candidate);
239
+ const matchesIgnored = process.platform === 'win32'
240
+ ? resolvedCandidate.toLowerCase() === ignored.toLowerCase()
241
+ : resolvedCandidate === ignored;
242
+ if (matchesIgnored)
243
+ continue;
244
+ }
234
245
  if (existsSync(join(candidate, 'package.json')))
235
246
  return candidate;
236
247
  }
@@ -709,15 +720,27 @@ function lockfileCoreVersions(profileDir) {
709
720
  */
710
721
  export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInstallDir) {
711
722
  const bundles = bundleNames.map((name) => {
723
+ // The real loader gives the DSH installation first refusal for in-box
724
+ // bundles. Desktop keeps that installation private from plugins, so a
725
+ // DIRECT profile-local copy with the same official name is only a stale
726
+ // shadow, never evidence for the layer the running host loaded (#371).
727
+ // Keep walking the profile anchor's parent search paths: Desktop heals an
728
+ // authoritative host fallback at <profiles>/node_modules.
729
+ const ignoredProfilePackage = dshInstallDir === null && INBOX_BUNDLES.has(name)
730
+ ? join(profileDirectory, 'node_modules', name)
731
+ : undefined;
712
732
  const anchors = [
713
- dshInstallDir !== null ? join(dshInstallDir, 'package.json') : null,
714
- join(profileDirectory, 'package.json'),
733
+ { anchor: dshInstallDir !== null ? join(dshInstallDir, 'package.json') : null },
734
+ {
735
+ anchor: join(profileDirectory, 'package.json'),
736
+ ignoredPackageDirectory: ignoredProfilePackage,
737
+ },
715
738
  ];
716
739
  let directory = null;
717
- for (const anchor of anchors) {
740
+ for (const { anchor, ignoredPackageDirectory } of anchors) {
718
741
  if (anchor === null)
719
742
  continue;
720
- directory = resolvePackageDir(anchor, name);
743
+ directory = resolvePackageDir(anchor, name, ignoredPackageDirectory);
721
744
  if (directory !== null)
722
745
  break;
723
746
  }
package/lib/routes.js CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
+ import { Readable } from 'node:stream';
12
13
  import { forgetCatalog, loadRegistry, pluginCategories } from './registry.js';
13
14
  import { cleanHotDir, hotMount, hotUnmount, listHotMounts, MAX_NOTE, mountClientOnlyDeps, purgeMarketState, readMarketState, writeMarketState, } from './hot.js';
14
15
  import { createGroup, deleteGroup, removeFromGroups, renameGroup, setGroupMembers } from './groups.js';
@@ -38,6 +39,7 @@ import { activationAfterReplace, brokenClientBundles, checkClientBundle, hasHost
38
39
  import { carrierDisableIds, disableRow, enableRow, findUserPatchPath, isProtectedModule, packagePatchFlags, readUserPatchState, removeRowBlocks, rowIdsForPackage, userPatchPackageReferences, } from './patch.js';
39
40
  import { createProfileBackup, downloadWebdav, MAX_BACKUP_BYTES, mergeRestoreManifest, restoreProfileBackup, unportableDeps, uploadWebdav, } from './backup.js';
40
41
  import { createGist, fitsGistLimit, GistError, gistErrorCode, parseGistId, readGist, resolveGistTokenSource, updateGist, verifyGistToken, } from './gist.js';
42
+ import { MAX_UPDATE_OPERATIONS_V1, UpdateOperationStoreV1, UPDATE_API_V1_SCHEMA } from './update-api-v1.js';
41
43
  /**
42
44
  * The market's own version, read once from its installed package.json.
43
45
  *
@@ -658,7 +660,281 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
658
660
  throw error;
659
661
  }
660
662
  }
663
+ const legacyHandlers = new Map();
664
+ const captureLegacy = (path, route) => {
665
+ legacyHandlers.set(path, route.handler);
666
+ return route;
667
+ };
668
+ const operationsV1 = new UpdateOperationStoreV1(BOOT_ID);
669
+ /** Invoke one existing route in memory so v1 reuses the battle-tested executor. */
670
+ async function invokeLegacy(path, source, method, body, url = path) {
671
+ const handler = legacyHandlers.get(path);
672
+ if (handler === undefined)
673
+ throw new Error(`legacy route is unavailable: ${path}`);
674
+ const chunks = body === undefined ? [] : [Buffer.from(JSON.stringify(body))];
675
+ const replay = Readable.from(chunks);
676
+ Object.assign(replay, {
677
+ method,
678
+ url,
679
+ headers: { ...source.headers },
680
+ socket: source.socket,
681
+ });
682
+ let status = 200;
683
+ let text = '';
684
+ const captured = {
685
+ writeHead(code) { status = code; return this; },
686
+ end(chunk) {
687
+ if (chunk !== undefined)
688
+ text += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
689
+ return this;
690
+ },
691
+ };
692
+ await handler(replay, captured);
693
+ let payload = null;
694
+ try {
695
+ payload = text === '' ? null : JSON.parse(text);
696
+ }
697
+ catch {
698
+ payload = { error: text };
699
+ }
700
+ return { status, payload };
701
+ }
702
+ const packageNameFrom = (request) => {
703
+ try {
704
+ return new URL(request.url ?? '', 'http://localhost').searchParams.get('name') ?? '';
705
+ }
706
+ catch {
707
+ return '';
708
+ }
709
+ };
710
+ const operationIdFrom = (request) => {
711
+ try {
712
+ return new URL(request.url ?? '', 'http://localhost').searchParams.get('operationId') ?? '';
713
+ }
714
+ catch {
715
+ return '';
716
+ }
717
+ };
718
+ const forceCheckFrom = (request) => {
719
+ try {
720
+ return new URL(request.url ?? '', 'http://localhost').searchParams.get('force') === '1';
721
+ }
722
+ catch {
723
+ return false;
724
+ }
725
+ };
661
726
  const disposers = [
727
+ host.webServer.register({
728
+ kind: 'exact',
729
+ path: '/dsh-market/api/v1/capabilities',
730
+ handler: (request, response) => {
731
+ if (request.method !== 'GET') {
732
+ response.writeHead(405, { allow: 'GET' });
733
+ response.end();
734
+ return;
735
+ }
736
+ const canRestart = restartAllowed(config);
737
+ sendJson(response, 200, {
738
+ schema: UPDATE_API_V1_SCHEMA,
739
+ apiVersion: 1,
740
+ // Machine-readable, because a policy that lives only in a markdown
741
+ // file is one a client never reads. `beta` says the shape may still
742
+ // change; it becomes `stable` once a release stops moving it, and
743
+ // that is the point at which the compatibility promise starts.
744
+ stability: 'beta',
745
+ marketVersion: marketVersion(),
746
+ profile: config.profile,
747
+ bootId: BOOT_ID,
748
+ runtime: config.profileDirectory === undefined ? 'web' : 'desktop',
749
+ features: {
750
+ check: true,
751
+ update: true,
752
+ progress: true,
753
+ rollback: true,
754
+ restart: canRestart,
755
+ },
756
+ restart: {
757
+ supported: canRestart,
758
+ managedBy: canRestart ? 'market' : config.profileDirectory === undefined ? 'operator' : 'desktop-host',
759
+ supervisor: detectedSupervisor(),
760
+ },
761
+ operationRetention: 'current-process',
762
+ operationLimit: MAX_UPDATE_OPERATIONS_V1,
763
+ endpoints: {
764
+ updates: '/dsh-market/api/v1/updates',
765
+ operations: '/dsh-market/api/v1/operations',
766
+ rollback: '/dsh-market/api/v1/rollback',
767
+ restart: '/dsh-market/api/v1/restart',
768
+ },
769
+ });
770
+ },
771
+ }),
772
+ host.webServer.register({
773
+ kind: 'exact',
774
+ path: '/dsh-market/api/v1/updates',
775
+ handler: async (request, response) => {
776
+ if (request.method === 'GET') {
777
+ const name = packageNameFrom(request);
778
+ if (!NPM_NAME_RE.test(name)) {
779
+ sendJson(response, 400, { schema: UPDATE_API_V1_SCHEMA, error: 'a valid package name is required' });
780
+ return;
781
+ }
782
+ try {
783
+ const force = forceCheckFrom(request);
784
+ const channel = activeChannel();
785
+ const channelFor = SELF_NAMES.has(name) ? new Map([[name, channel]]) : undefined;
786
+ const update = (await checkUpdates(config.profile, force, activeProfileDir, channelFor))[name];
787
+ if (update === undefined) {
788
+ sendJson(response, 404, { schema: UPDATE_API_V1_SCHEMA, error: 'plugin is not installed' });
789
+ return;
790
+ }
791
+ sendJson(response, 200, {
792
+ schema: UPDATE_API_V1_SCHEMA,
793
+ package: {
794
+ name,
795
+ source: update.kind,
796
+ installedVersion: update.current ?? update.version,
797
+ latestVersion: update.latest,
798
+ updateAvailable: update.updateAvailable,
799
+ channelSwitch: update.channelSwitch ?? null,
800
+ },
801
+ });
802
+ }
803
+ catch (error) {
804
+ sendJson(response, 500, {
805
+ schema: UPDATE_API_V1_SCHEMA,
806
+ error: error instanceof Error ? error.message : String(error),
807
+ });
808
+ }
809
+ return;
810
+ }
811
+ if (request.method !== 'POST') {
812
+ response.writeHead(405, { allow: 'GET, POST' });
813
+ response.end();
814
+ return;
815
+ }
816
+ if (!sameOrigin(request)) {
817
+ sendJson(response, 403, { schema: UPDATE_API_V1_SCHEMA, error: 'untrusted origin' });
818
+ return;
819
+ }
820
+ try {
821
+ const body = (await readJsonBody(request));
822
+ const packageName = typeof body.packageName === 'string' ? body.packageName : '';
823
+ if (!NPM_NAME_RE.test(packageName)) {
824
+ sendJson(response, 400, { schema: UPDATE_API_V1_SCHEMA, error: 'a valid package name is required' });
825
+ return;
826
+ }
827
+ if (operationsV1.hasActive()) {
828
+ sendJson(response, 409, {
829
+ schema: UPDATE_API_V1_SCHEMA,
830
+ error: 'another public update operation is already running',
831
+ failure: {
832
+ code: 'OPERATION_BUSY',
833
+ message: 'another public update operation is already running',
834
+ retryable: true,
835
+ },
836
+ });
837
+ return;
838
+ }
839
+ const installedVersion = readInstalledVersion(config.profile, packageName, activeProfileDir);
840
+ if (installedVersion === null) {
841
+ sendJson(response, 404, {
842
+ schema: UPDATE_API_V1_SCHEMA,
843
+ error: 'plugin is not installed',
844
+ failure: {
845
+ code: 'PLUGIN_NOT_INSTALLED',
846
+ message: 'plugin is not installed in this profile',
847
+ retryable: false,
848
+ },
849
+ });
850
+ return;
851
+ }
852
+ const operation = operationsV1.create(packageName, installedVersion);
853
+ operationsV1.start(operation.operationId);
854
+ void invokeLegacy('/dsh-market/update', request, 'POST', {
855
+ name: packageName,
856
+ ...(body.force === true ? { force: true } : {}),
857
+ }).then(({ status, payload }) => {
858
+ operationsV1.finish(operation.operationId, status, payload, readInstalledVersion(config.profile, packageName, activeProfileDir));
859
+ }).catch((error) => {
860
+ operationsV1.finish(operation.operationId, 500, { error: error instanceof Error ? error.message : String(error) }, readInstalledVersion(config.profile, packageName, activeProfileDir));
861
+ });
862
+ sendJson(response, 202, {
863
+ schema: UPDATE_API_V1_SCHEMA,
864
+ operation: operationsV1.get(operation.operationId),
865
+ });
866
+ }
867
+ catch (error) {
868
+ sendJson(response, 400, {
869
+ schema: UPDATE_API_V1_SCHEMA,
870
+ error: error instanceof Error ? error.message : String(error),
871
+ });
872
+ }
873
+ },
874
+ }),
875
+ host.webServer.register({
876
+ kind: 'exact',
877
+ path: '/dsh-market/api/v1/operations',
878
+ handler: (request, response) => {
879
+ if (request.method !== 'GET') {
880
+ response.writeHead(405, { allow: 'GET' });
881
+ response.end();
882
+ return;
883
+ }
884
+ const operation = operationsV1.get(operationIdFrom(request), progress);
885
+ if (operation === null) {
886
+ sendJson(response, 404, { schema: UPDATE_API_V1_SCHEMA, error: 'operation not found in this host process' });
887
+ return;
888
+ }
889
+ sendJson(response, 200, { schema: UPDATE_API_V1_SCHEMA, operation });
890
+ },
891
+ }),
892
+ host.webServer.register({
893
+ kind: 'exact',
894
+ path: '/dsh-market/api/v1/rollback',
895
+ handler: async (request, response) => {
896
+ if (request.method !== 'POST') {
897
+ response.writeHead(405, { allow: 'POST' });
898
+ response.end();
899
+ return;
900
+ }
901
+ if (!sameOrigin(request)) {
902
+ sendJson(response, 403, { schema: UPDATE_API_V1_SCHEMA, error: 'untrusted origin' });
903
+ return;
904
+ }
905
+ try {
906
+ const body = (await readJsonBody(request));
907
+ const operationId = typeof body.operationId === 'string' ? body.operationId : '';
908
+ const legacyRollbackId = operationsV1.beginRollback(operationId);
909
+ if (legacyRollbackId === null) {
910
+ sendJson(response, 409, { schema: UPDATE_API_V1_SCHEMA, error: 'rollback is not available for this operation' });
911
+ return;
912
+ }
913
+ const result = await invokeLegacy('/dsh-market/rollback', request, 'POST', { rollbackId: legacyRollbackId });
914
+ const operation = operationsV1.finishRollback(operationId, result.status, result.payload);
915
+ sendJson(response, 200, { schema: UPDATE_API_V1_SCHEMA, operation });
916
+ }
917
+ catch (error) {
918
+ sendJson(response, 400, {
919
+ schema: UPDATE_API_V1_SCHEMA,
920
+ error: error instanceof Error ? error.message : String(error),
921
+ });
922
+ }
923
+ },
924
+ }),
925
+ host.webServer.register({
926
+ kind: 'exact',
927
+ path: '/dsh-market/api/v1/restart',
928
+ handler: async (request, response) => {
929
+ if (request.method !== 'POST') {
930
+ response.writeHead(405, { allow: 'POST' });
931
+ response.end();
932
+ return;
933
+ }
934
+ const result = await invokeLegacy('/dsh-market/restart', request, 'POST', {});
935
+ sendJson(response, result.status, { schema: UPDATE_API_V1_SCHEMA, result: result.payload });
936
+ },
937
+ }),
662
938
  host.webServer.register({
663
939
  kind: 'exact',
664
940
  path: '/dsh-market/backup',
@@ -1633,7 +1909,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
1633
1909
  }
1634
1910
  },
1635
1911
  }),
1636
- host.webServer.register({
1912
+ host.webServer.register(captureLegacy('/dsh-market/update', {
1637
1913
  kind: 'exact',
1638
1914
  path: '/dsh-market/update',
1639
1915
  handler: async (request, response) => {
@@ -1997,7 +2273,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
1997
2273
  sendJson(response, 500, { error: message });
1998
2274
  }
1999
2275
  },
2000
- }),
2276
+ })),
2001
2277
  host.webServer.register({
2002
2278
  kind: 'exact',
2003
2279
  path: '/dsh-market/setup-pnpm',
@@ -2227,7 +2503,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2227
2503
  }
2228
2504
  },
2229
2505
  }),
2230
- host.webServer.register({
2506
+ host.webServer.register(captureLegacy('/dsh-market/restart', {
2231
2507
  kind: 'exact',
2232
2508
  path: '/dsh-market/restart',
2233
2509
  handler: (request, response) => {
@@ -2266,7 +2542,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2266
2542
  sendJson(response, 500, { error: message });
2267
2543
  }
2268
2544
  },
2269
- }),
2545
+ })),
2270
2546
  host.webServer.register({
2271
2547
  kind: 'exact',
2272
2548
  path: '/dsh-market/approve-builds',
@@ -2562,7 +2838,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2562
2838
  }
2563
2839
  },
2564
2840
  }),
2565
- host.webServer.register({
2841
+ host.webServer.register(captureLegacy('/dsh-market/rollback', {
2566
2842
  kind: 'exact',
2567
2843
  path: '/dsh-market/rollback',
2568
2844
  handler: async (request, response) => {
@@ -2630,7 +2906,7 @@ export function mountMarketRoutes(host, config, commandRuntime, agentsLookup) {
2630
2906
  sendJson(response, 500, { error: message });
2631
2907
  }
2632
2908
  },
2633
- }),
2909
+ })),
2634
2910
  host.webServer.register({
2635
2911
  kind: 'exact',
2636
2912
  path: '/dsh-market/install',
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Stable, versioned contract for plugin-owned update surfaces.
3
+ *
4
+ * The market UI has richer internal response shapes that evolve with its UI.
5
+ * Third-party plugins must not depend on those shapes, so this module owns the
6
+ * small JSON envelope exposed under `/dsh-market/api/v1`.
7
+ */
8
+ import type { InstallProgress } from './dsh-cli.ts';
9
+ export declare const UPDATE_API_V1_SCHEMA: 'dsh-market/update-api/v1';
10
+ export declare const MAX_UPDATE_OPERATIONS_V1 = 50;
11
+ export type UpdateOperationState = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'rolled-back';
12
+ export interface UpdateFailureV1 {
13
+ code: string;
14
+ message: string;
15
+ retryable: boolean;
16
+ }
17
+ export interface UpdateOperationV1 {
18
+ schema: typeof UPDATE_API_V1_SCHEMA;
19
+ operationId: string;
20
+ kind: 'update';
21
+ packageName: string;
22
+ state: UpdateOperationState;
23
+ createdAt: number;
24
+ startedAt: number | null;
25
+ finishedAt: number | null;
26
+ beforeVersion: string | null;
27
+ installedVersion: string | null;
28
+ progress: {
29
+ phase: string | null;
30
+ done: number;
31
+ total: number | null;
32
+ percent: number | null;
33
+ currentPackage: string | null;
34
+ detail: string | null;
35
+ downloaded: number | null;
36
+ size: number | null;
37
+ };
38
+ outcome: {
39
+ refreshRequired: boolean;
40
+ restartRequired: boolean;
41
+ rollback: {
42
+ available: boolean;
43
+ state: 'unavailable' | 'available' | 'running' | 'succeeded' | 'failed';
44
+ detail: string | null;
45
+ };
46
+ };
47
+ failure: UpdateFailureV1 | null;
48
+ }
49
+ /** Process-local operation registry. A boot id scopes ids across restarts. */
50
+ export declare class UpdateOperationStoreV1 {
51
+ private readonly bootId;
52
+ private readonly now;
53
+ private readonly maxOperations;
54
+ private sequence;
55
+ private readonly operations;
56
+ private activeId;
57
+ constructor(bootId: string, now?: () => number, maxOperations?: number);
58
+ hasActive(): boolean;
59
+ create(packageName: string, beforeVersion: string | null): UpdateOperationV1;
60
+ start(operationId: string): void;
61
+ finish(operationId: string, status: number, payload: unknown, installedVersion: string | null): UpdateOperationV1 | null;
62
+ beginRollback(operationId: string): string | null;
63
+ finishRollback(operationId: string, status: number, payload: unknown): UpdateOperationV1 | null;
64
+ get(operationId: string, progress?: InstallProgress): UpdateOperationV1 | null;
65
+ private snapshot;
66
+ }