@chatcode/cco-market 1.45.1

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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/README.zh.md +143 -0
  4. package/UPDATE-API-V1.md +132 -0
  5. package/client/client.js +11534 -0
  6. package/cordis.patch.yml +5 -0
  7. package/lib/accelerate.js +184 -0
  8. package/lib/agents.js +36 -0
  9. package/lib/backup.js +572 -0
  10. package/lib/catalog-local-match.js +126 -0
  11. package/lib/catalog-npm.js +111 -0
  12. package/lib/changelog.js +242 -0
  13. package/lib/channels.js +64 -0
  14. package/lib/check.js +1067 -0
  15. package/lib/compatibility.js +198 -0
  16. package/lib/diagnostics.js +49 -0
  17. package/lib/discovery-compatibility.js +260 -0
  18. package/lib/dsh-cli.js +949 -0
  19. package/lib/dsh-install.js +108 -0
  20. package/lib/gist.js +352 -0
  21. package/lib/groups.js +97 -0
  22. package/lib/home-paths.js +42 -0
  23. package/lib/hot.js +519 -0
  24. package/lib/http.js +39 -0
  25. package/lib/index.js +97 -0
  26. package/lib/install.js +387 -0
  27. package/lib/log.js +197 -0
  28. package/lib/ndjson.js +154 -0
  29. package/lib/net.js +98 -0
  30. package/lib/order.js +284 -0
  31. package/lib/patch.js +521 -0
  32. package/lib/pnpm-compat.js +490 -0
  33. package/lib/presets.js +278 -0
  34. package/lib/profile.js +911 -0
  35. package/lib/region-probe.js +93 -0
  36. package/lib/regions.js +253 -0
  37. package/lib/registry.js +204 -0
  38. package/lib/restart.js +373 -0
  39. package/lib/routes.js +4372 -0
  40. package/lib/settings.js +109 -0
  41. package/lib/skill-market/api.js +119 -0
  42. package/lib/skill-market/config.js +29 -0
  43. package/lib/skill-market/installer.js +352 -0
  44. package/lib/skill-market/runtime.js +13 -0
  45. package/lib/skill-market/tui.js +119 -0
  46. package/lib/skill-market/types.js +1 -0
  47. package/lib/skill-market/web-routes.js +123 -0
  48. package/lib/snapshot.js +500 -0
  49. package/lib/source-migration.js +54 -0
  50. package/lib/sources.js +596 -0
  51. package/lib/store.js +91 -0
  52. package/lib/themes.js +102 -0
  53. package/lib/trial.js +116 -0
  54. package/lib/types/accelerate.d.ts +85 -0
  55. package/lib/types/agents.d.ts +22 -0
  56. package/lib/types/backup.d.ts +131 -0
  57. package/lib/types/catalog-local-match.d.ts +50 -0
  58. package/lib/types/catalog-npm.d.ts +47 -0
  59. package/lib/types/changelog.d.ts +102 -0
  60. package/lib/types/channels.d.ts +56 -0
  61. package/lib/types/check.d.ts +266 -0
  62. package/lib/types/compatibility.d.ts +64 -0
  63. package/lib/types/diagnostics.d.ts +29 -0
  64. package/lib/types/discovery-compatibility.d.ts +80 -0
  65. package/lib/types/dsh-cli.d.ts +324 -0
  66. package/lib/types/dsh-install.d.ts +46 -0
  67. package/lib/types/gist.d.ts +53 -0
  68. package/lib/types/groups.d.ts +34 -0
  69. package/lib/types/home-paths.d.ts +16 -0
  70. package/lib/types/hot.d.ts +221 -0
  71. package/lib/types/http.d.ts +12 -0
  72. package/lib/types/index.d.ts +14 -0
  73. package/lib/types/install.d.ts +172 -0
  74. package/lib/types/log.d.ts +43 -0
  75. package/lib/types/ndjson.d.ts +52 -0
  76. package/lib/types/net.d.ts +58 -0
  77. package/lib/types/order.d.ts +100 -0
  78. package/lib/types/patch.d.ts +129 -0
  79. package/lib/types/pnpm-compat.d.ts +93 -0
  80. package/lib/types/presets.d.ts +90 -0
  81. package/lib/types/profile.d.ts +253 -0
  82. package/lib/types/region-probe.d.ts +50 -0
  83. package/lib/types/regions.d.ts +122 -0
  84. package/lib/types/registry.d.ts +86 -0
  85. package/lib/types/restart.d.ts +194 -0
  86. package/lib/types/routes.d.ts +67 -0
  87. package/lib/types/settings.d.ts +78 -0
  88. package/lib/types/skill-market/api.d.ts +10 -0
  89. package/lib/types/skill-market/config.d.ts +3 -0
  90. package/lib/types/skill-market/installer.d.ts +23 -0
  91. package/lib/types/skill-market/runtime.d.ts +10 -0
  92. package/lib/types/skill-market/tui.d.ts +4 -0
  93. package/lib/types/skill-market/types.d.ts +71 -0
  94. package/lib/types/skill-market/web-routes.d.ts +19 -0
  95. package/lib/types/snapshot.d.ts +93 -0
  96. package/lib/types/source-migration.d.ts +11 -0
  97. package/lib/types/sources.d.ts +216 -0
  98. package/lib/types/store.d.ts +30 -0
  99. package/lib/types/themes.d.ts +40 -0
  100. package/lib/types/trial.d.ts +61 -0
  101. package/lib/types/update-api-v1.d.ts +66 -0
  102. package/lib/types/updates.d.ts +123 -0
  103. package/lib/types/verify.d.ts +139 -0
  104. package/lib/update-api-v1.js +215 -0
  105. package/lib/updates.js +361 -0
  106. package/lib/verify.js +453 -0
  107. package/package.json +121 -0
  108. package/src/accelerate.ts +213 -0
  109. package/src/agents.ts +43 -0
  110. package/src/backup.ts +583 -0
  111. package/src/catalog-local-match.ts +144 -0
  112. package/src/catalog-npm.ts +120 -0
  113. package/src/changelog.ts +282 -0
  114. package/src/channels.ts +70 -0
  115. package/src/check.ts +1239 -0
  116. package/src/client/CommentsModal.tsx +119 -0
  117. package/src/client/Diagnostics.tsx +907 -0
  118. package/src/client/ErrorBoundary.tsx +111 -0
  119. package/src/client/InstallToast.tsx +31 -0
  120. package/src/client/Market.module.css +830 -0
  121. package/src/client/MarketSection.tsx +5301 -0
  122. package/src/client/OperationsPanel.tsx +365 -0
  123. package/src/client/SettingsCard.tsx +621 -0
  124. package/src/client/SkillMarket.module.css +21 -0
  125. package/src/client/SkillMarketSection.tsx +162 -0
  126. package/src/client/comments.ts +54 -0
  127. package/src/client/globals.d.ts +13 -0
  128. package/src/client/index.ts +180 -0
  129. package/src/client/locales.ts +1112 -0
  130. package/src/client/market-data.ts +1321 -0
  131. package/src/client/operations.ts +201 -0
  132. package/src/client/preset-panel.tsx +263 -0
  133. package/src/client/primitives.d.ts +140 -0
  134. package/src/client/self-check.ts +147 -0
  135. package/src/client/snapshot-panel.tsx +244 -0
  136. package/src/compatibility.ts +237 -0
  137. package/src/diagnostics.ts +84 -0
  138. package/src/discovery-compatibility.ts +315 -0
  139. package/src/dsh-cli.ts +1126 -0
  140. package/src/dsh-install.ts +118 -0
  141. package/src/gist.ts +362 -0
  142. package/src/groups.ts +111 -0
  143. package/src/home-paths.ts +53 -0
  144. package/src/hot.ts +628 -0
  145. package/src/http.ts +41 -0
  146. package/src/index.ts +128 -0
  147. package/src/install.ts +420 -0
  148. package/src/log.ts +206 -0
  149. package/src/ndjson.ts +185 -0
  150. package/src/net.ts +106 -0
  151. package/src/order.ts +303 -0
  152. package/src/patch.ts +522 -0
  153. package/src/pnpm-compat.ts +527 -0
  154. package/src/presets.ts +344 -0
  155. package/src/profile.ts +940 -0
  156. package/src/region-probe.ts +97 -0
  157. package/src/regions.ts +310 -0
  158. package/src/registry.ts +250 -0
  159. package/src/restart.ts +396 -0
  160. package/src/routes.ts +4506 -0
  161. package/src/settings.ts +141 -0
  162. package/src/skill-market/api.ts +130 -0
  163. package/src/skill-market/config.ts +32 -0
  164. package/src/skill-market/installer.ts +337 -0
  165. package/src/skill-market/runtime.ts +14 -0
  166. package/src/skill-market/tui.ts +157 -0
  167. package/src/skill-market/types.ts +80 -0
  168. package/src/skill-market/web-routes.ts +132 -0
  169. package/src/snapshot.ts +532 -0
  170. package/src/source-migration.ts +61 -0
  171. package/src/sources.ts +565 -0
  172. package/src/store.ts +89 -0
  173. package/src/themes.ts +125 -0
  174. package/src/trial.ts +156 -0
  175. package/src/update-api-v1.ts +277 -0
  176. package/src/updates.ts +400 -0
  177. package/src/verify.ts +492 -0
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Update detection: per-plugin comparison of what the profile has against
3
+ * the source of truth — git HEAD for github installs, the npm latest
4
+ * dist-tag for registry installs — with a TTL cache.
5
+ */
6
+ import { type Channel } from './channels.ts';
7
+ export interface UpdateStatus {
8
+ kind: 'github' | 'npm' | 'linked';
9
+ version: string | null;
10
+ current: string | null;
11
+ latest: string | null;
12
+ /**
13
+ * A NEWER version exists. Forwards only, always — every caller reads it
14
+ * as "there is an upgrade" and labels a button accordingly.
15
+ */
16
+ updateAvailable: boolean;
17
+ /** Taking this update replaces a local package source with its matched online release. */
18
+ restoreRequired?: boolean;
19
+ /** Independent, explicit source switch offered for verified legacy Git installs. */
20
+ sourceMigration?: {
21
+ kind: 'git-to-npm';
22
+ repo: string;
23
+ target: string;
24
+ };
25
+ /**
26
+ * The version this package's channel points at, when it differs from what
27
+ * is installed and is NOT newer.
28
+ *
29
+ * A separate field rather than a second meaning for `updateAvailable`,
30
+ * which was tried and leaked immediately: the market page has three
31
+ * consumers of that flag (the header banner, "update all", the row
32
+ * button) and all three announced a DOWNGRADE as "a new version is
33
+ * available". One field, one meaning; a caller that has not been taught
34
+ * about channel switches simply does not offer one.
35
+ *
36
+ * Only a channel-following package can be in this state, and it is the
37
+ * state that used to be unreachable: picking "stable" while a prerelease
38
+ * was installed compared 1.13.1 against 1.14.0-beta.1, found nothing
39
+ * newer, and answered "up to date" — so there was no way back off a
40
+ * channel the user had just left.
41
+ */
42
+ channelSwitch?: string;
43
+ }
44
+ /**
45
+ * Semver precedence: negative / 0 / positive like a comparator, or null when
46
+ * either side isn't a plain semver version. Build metadata is ignored, a
47
+ * release outranks any prerelease of the same core, and prerelease
48
+ * identifiers compare numerically when both are numeric (so `rc.10` > `rc.9`).
49
+ */
50
+ export declare function compareVersions(a: string, b: string): number | null;
51
+ /**
52
+ * True only when the registry's `latest` is semantically HIGHER than what the
53
+ * profile has (#64 by @ZeroOrigin64). A plain `!==` also fires when a
54
+ * package's `latest` dist-tag is left pointing at an OLDER release than the
55
+ * pinned install — clicking "update" then rewrote the exact pin to `@latest`
56
+ * and downgraded the profile until it no longer booted.
57
+ *
58
+ * Undecidable inputs (missing or non-semver versions) report no update:
59
+ * without a direction we cannot promise the "update" isn't a downgrade.
60
+ */
61
+ export declare function isUpgrade(installed: string | null, latest: string | null): boolean;
62
+ /** Drop the cached listing (after a successful install/update/uninstall). */
63
+ export declare function invalidateUpdates(): void;
64
+ /**
65
+ * Point update checks at a registry. Called when the download region
66
+ * resolves and whenever it changes.
67
+ *
68
+ * Dropping the cache is the load-bearing half. A mirror can lag the official
69
+ * registry by minutes, so answers gathered from one are not answers from the
70
+ * other — keeping them across a switch would report a version this registry
71
+ * cannot yet serve.
72
+ */
73
+ export declare function setUpdateRegistry(base: string): void;
74
+ /**
75
+ * Evidence check behind the "wait a day" stale diagnosis (#45): whether the
76
+ * package's CURRENT latest release was published recently enough to sit
77
+ * inside pnpm's default fresh-release window. pnpm's silent hold leaves no
78
+ * trace in its output, so the publish time is the only verifiable signal.
79
+ * @returns true/false when the npm time metadata answers, null when it
80
+ * can't be determined (offline, unpublished, non-npm) — callers must NOT
81
+ * claim the safety wait on null.
82
+ */
83
+ export declare function latestPublishedRecently(name: string, windowMs?: number): Promise<boolean | null>;
84
+ /** The registry's current `latest` version for a package, or null when it can't be read. */
85
+ /**
86
+ * The version a channel subscriber should be offered: the newest build in
87
+ * the set that channel is willing to receive.
88
+ *
89
+ * A channel is a SET, not a tag. Someone on beta has not stopped accepting
90
+ * releases — they accept releases and prereleases — so beta means
91
+ * {latest, beta} and dev means {latest, beta, dev}. Reading it as one tag
92
+ * gets a real case wrong: once 1.14.0 ships, `beta` still points at
93
+ * 1.14.0-beta.1 until the next prerelease is cut, and following that tag
94
+ * literally would walk a subscriber BACKWARDS onto a build their channel
95
+ * has already moved past.
96
+ *
97
+ * The nesting is also what makes a channel leavable. Going backwards is
98
+ * only ever offered when the user narrows the set — picking stable while a
99
+ * prerelease is installed drops `beta` out of it, so the answer becomes
100
+ * `latest` and the market can finally offer the way back. That case used to
101
+ * be unreachable: comparing 1.13.1 against an installed 1.14.0-beta.1 found
102
+ * nothing newer and answered "up to date", so the control the user had just
103
+ * used appeared to do nothing.
104
+ *
105
+ * @param stable - the `latest` version, already fetched by the caller.
106
+ */
107
+ export declare function versionOnChannel(name: string, channel: Channel, stable: string | null): Promise<string | null>;
108
+ export declare function fetchNpmLatest(name: string): Promise<string | null>;
109
+ /** Per-plugin update checks; a failed check reports no update rather than failing the listing. */
110
+ export declare function checkUpdates(profile: string, force?: boolean, explicitDir?: string,
111
+ /**
112
+ * Packages that follow a release channel instead of plain `latest`. Only
113
+ * ever the market itself: opting into early builds is volunteering to try
114
+ * THIS plugin early, not a licence to pull every other author's
115
+ * unreleased work.
116
+ */
117
+ channelFor?: ReadonlyMap<string, Channel>,
118
+ /**
119
+ * Curated npm sources for `file:` installs that were matched to the market
120
+ * catalog. `link:` workspaces remain development sources and are never
121
+ * opted into online updates.
122
+ */
123
+ onlineSourceFor?: ReadonlyMap<string, string>): Promise<Record<string, UpdateStatus>>;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Post-install activation verification (P0-2): what "installed" actually
3
+ * means for a package in a dsh profile.
4
+ *
5
+ * Two sources of truth, in strict order of authority:
6
+ *
7
+ * 1. The LOADER INVENTORY (observed): whatever the loader is running right
8
+ * now is live, full stop. A plain library with no `dsh` field can be
9
+ * loaded by name from someone else's bundle patch — the official
10
+ * dsh-base patch loads `@deepseek-ai/dsh-tools`, which has no `dsh`
11
+ * field at all — so no manifest check may overrule it (#135).
12
+ * 2. The profile manifest (inferred): `<profile>/package.json` →
13
+ * `dsh.profile.bundles`, what the dsh CLI reconciled. This predicts what
14
+ * the NEXT boot will load, and is the only evidence available for a
15
+ * package that is not currently running.
16
+ *
17
+ * State taxonomy (IMPROVEMENT-PLAN P0-2):
18
+ * live – running in the current composition (hot mount or loader entry)
19
+ * restart – installed and will activate on the next boot, but not live now
20
+ * inert – installed but not a profile-layer plugin (plain dependency, or
21
+ * client-only — the market shim-mounts those at boot)
22
+ * broken – would fail to load: listed as a bundle without a dsh surface,
23
+ * or a declared entry artifact that is missing
24
+ * missing – not present in node_modules
25
+ */
26
+ export type ActivationState = 'live' | 'restart' | 'inert' | 'broken' | 'missing' | 'disabled';
27
+ export interface ActivationResult {
28
+ state: ActivationState;
29
+ /** Bilingual, user-facing explanations (zh / en joined with " / "). */
30
+ reasons: string[];
31
+ /** True when the package is in the profile's `dsh.profile.bundles`. */
32
+ bundle: boolean;
33
+ /** True when the package is live in the running composition. */
34
+ hot: boolean;
35
+ }
36
+ export declare function verifyActivation(profile: string, name: string, live?: ReadonlySet<string>, explicitDir?: string, isDisabled?: boolean): ActivationResult;
37
+ /**
38
+ * Correct a post-UPDATE verdict for a plugin that was already running.
39
+ *
40
+ * `verifyActivation` answers "is this name in the live loader inventory".
41
+ * That is the right question after an install and the wrong one after an
42
+ * update: the plugin was already live, so the answer stays "live" while the
43
+ * process keeps serving the module it imported at boot. Replacing files under
44
+ * a running composition does not re-import anything.
45
+ *
46
+ * Measured on a real host rather than reasoned about — updating the market
47
+ * from 1.11.3 to 1.12.2 left `/dsh-market/status` reporting 1.11.3 with an
48
+ * unchanged boot id, while the update route called it hot-loaded in the same
49
+ * response. The browser half genuinely does refresh (the host re-serves the
50
+ * client bundle from disk), which is what makes the wrong verdict credible:
51
+ * the UI visibly becomes the new version while the server half does not.
52
+ *
53
+ * Only a plugin that was ALREADY live is affected. One that was missing,
54
+ * broken or disabled beforehand has nothing loaded to shadow the new build,
55
+ * so its fresh mount really does run the new code.
56
+ *
57
+ * Client-only packages are excluded for the same reason from the other end:
58
+ * they have no host half to go stale, and the browser fetches their bundle
59
+ * from disk on the next page load. Telling their users to restart would be
60
+ * #156 again, in a narrower place — see `hasHostHalf`.
61
+ * @param result the verdict computed from the loader inventory
62
+ * @param hostHalfWasLive whether a HOST half was live BEFORE the replacement
63
+ */
64
+ export declare function activationAfterReplace(result: ActivationResult, hostHalfWasLive: boolean): ActivationResult;
65
+ /**
66
+ * Whether a package has a host (Node) half at all.
67
+ *
68
+ * A `dsh.client`-only package — themes, skins, most pure-UI plugins — runs
69
+ * no server code: the market shim-mounts it so the loader has a live row,
70
+ * and the browser re-fetches its bundle from disk on the next page load. An
71
+ * update to one takes effect on refresh, with no restart to ask for.
72
+ */
73
+ export declare function hasHostHalf(profile: string, name: string, explicitDir?: string): boolean;
74
+ /**
75
+ * The client bundle path a package's `exports["./client"]` names, relative
76
+ * to the package root — or `null` when it cannot be resolved CONFIDENTLY.
77
+ *
78
+ * Returning null is the important half. This feeds a post-install check
79
+ * whose only job is to catch a corrupt bundle, and a resolver that guessed
80
+ * wrong would report a healthy plugin as broken — worse than the silence it
81
+ * replaces. So every shape this does not fully understand resolves to null
82
+ * and the check simply does not run: unresolvable is not evidence of damage.
83
+ *
84
+ * Handles the two shapes real plugins ship: a plain string, and a
85
+ * conditional object. For the object, only `browser` and `default` are
86
+ * consulted — those are the conditions the host's client loader actually
87
+ * activates; `import`/`require` describe a Node resolution this file is not
88
+ * modelling, and picking one of those could name a different artifact.
89
+ * Nested conditions recurse; anything else (arrays, non-relative targets)
90
+ * gives up.
91
+ */
92
+ export declare function clientBundlePath(exportsField: unknown, depth?: number): string | null;
93
+ /** What a bundle check concluded. `ok` covers "fine" AND "could not tell". */
94
+ export interface BundleCheck {
95
+ ok: boolean;
96
+ /** Populated only when the file was found AND failed to parse. */
97
+ reason: string | null;
98
+ }
99
+ /**
100
+ * Every installed plugin whose client bundle will not parse (#222 by
101
+ * @MicroMilo).
102
+ *
103
+ * The per-package check above only ever looked at what an operation added,
104
+ * which misses the failure that was actually reported: pnpm re-extracts the
105
+ * WHOLE tree on any install, so updating one plugin can restore another
106
+ * plugin's pristine — and broken — bundle, or fail to re-apply a patch that
107
+ * was holding it together. The damage then surfaces at the next boot as
108
+ * "failed to load plugins", with nothing connecting it to the install that
109
+ * caused it.
110
+ *
111
+ * Cheap enough to run on every operation: 0.40ms per plugin including the
112
+ * read (measured on a 385KB bundle), so a 30-plugin profile costs ~12ms
113
+ * against an install that takes seconds.
114
+ *
115
+ * Silent on ESM bundles, like the per-package check it calls — see there for
116
+ * why. Widening the sweep is exactly what would have turned that one false
117
+ * "corrupt" into one per ESM plugin in the profile.
118
+ *
119
+ * Callers compare a before-list with an after-list rather than reporting
120
+ * this one directly — a profile can carry a broken bundle indefinitely, and
121
+ * re-reporting a problem the user already had would put them in front of
122
+ * something this operation did not cause and cannot undo.
123
+ */
124
+ export declare function brokenClientBundles(profile: string, explicitDir?: string): {
125
+ name: string;
126
+ reason: string;
127
+ }[];
128
+ /** Bundles broken after an operation that were intact before it. */
129
+ export declare function newlyBrokenBundles(before: readonly {
130
+ name: string;
131
+ reason: string;
132
+ }[], after: readonly {
133
+ name: string;
134
+ reason: string;
135
+ }[]): {
136
+ name: string;
137
+ reason: string;
138
+ }[];
139
+ export declare function checkClientBundle(profile: string, name: string, explicitDir?: string): BundleCheck;
@@ -0,0 +1,215 @@
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
+ export const UPDATE_API_V1_SCHEMA = 'dsh-market/update-api/v1';
9
+ export const MAX_UPDATE_OPERATIONS_V1 = 50;
10
+ function record(value) {
11
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
12
+ ? value
13
+ : null;
14
+ }
15
+ function text(value) {
16
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
17
+ }
18
+ function bool(value) {
19
+ return value === true;
20
+ }
21
+ function failureCode(status, body) {
22
+ const explicit = text(body.failureCode);
23
+ if (explicit === 'DOWNGRADE_DETECTED' || explicit === 'RESOLVED_VERSION_MISMATCH')
24
+ return explicit;
25
+ if (body.agentsBusy === true)
26
+ return 'AGENTS_RUNNING';
27
+ if (body.busy === true || status === 409)
28
+ return 'OPERATION_BUSY';
29
+ if (body.stale === true && body.staleReason === 'release-age')
30
+ return 'RELEASE_TOO_FRESH';
31
+ if (body.stale === true)
32
+ return 'VERSION_UNCHANGED';
33
+ if (body.timedOut === true)
34
+ return 'UPDATE_TIMEOUT';
35
+ if (status === 403)
36
+ return 'UPDATE_FORBIDDEN';
37
+ if (status === 400)
38
+ return 'UPDATE_REJECTED';
39
+ return 'UPDATE_FAILED';
40
+ }
41
+ function failureOf(status, body) {
42
+ const code = failureCode(status, body);
43
+ const message = text(body.error)
44
+ ?? text(body.stderr)
45
+ ?? text(body.stdout)
46
+ ?? `update failed with HTTP ${String(status)}`;
47
+ return {
48
+ code,
49
+ message: message.slice(-1200),
50
+ retryable: code === 'AGENTS_RUNNING'
51
+ || code === 'OPERATION_BUSY'
52
+ || code === 'RELEASE_TOO_FRESH'
53
+ || code === 'VERSION_UNCHANGED'
54
+ || code === 'RESOLVED_VERSION_MISMATCH'
55
+ || code === 'UPDATE_TIMEOUT',
56
+ };
57
+ }
58
+ function activationState(body, packageName) {
59
+ const activations = record(body.activation);
60
+ const activation = activations === null ? null : record(activations[packageName]);
61
+ return activation === null ? null : text(activation.state);
62
+ }
63
+ function rollbackIdOf(body) {
64
+ const compatibility = record(body.compatibility);
65
+ return compatibility === null ? null : text(compatibility.rollbackId);
66
+ }
67
+ function progressView(progress) {
68
+ if (progress === undefined) {
69
+ return {
70
+ phase: null,
71
+ done: 0,
72
+ total: null,
73
+ percent: null,
74
+ currentPackage: null,
75
+ detail: null,
76
+ downloaded: null,
77
+ size: null,
78
+ };
79
+ }
80
+ const percent = progress.total !== null && progress.total > 0
81
+ ? Math.min(100, Math.round(progress.done / progress.total * 100))
82
+ : null;
83
+ return {
84
+ phase: progress.phase,
85
+ done: progress.done,
86
+ total: progress.total,
87
+ percent,
88
+ currentPackage: progress.currentPackage,
89
+ detail: text(progress.lastLine) ?? text(progress.error),
90
+ downloaded: progress.downloaded,
91
+ size: progress.size,
92
+ };
93
+ }
94
+ /** Process-local operation registry. A boot id scopes ids across restarts. */
95
+ export class UpdateOperationStoreV1 {
96
+ bootId;
97
+ now;
98
+ maxOperations;
99
+ sequence = 0;
100
+ operations = new Map();
101
+ activeId = null;
102
+ constructor(bootId, now = Date.now, maxOperations = MAX_UPDATE_OPERATIONS_V1) {
103
+ this.bootId = bootId;
104
+ this.now = now;
105
+ this.maxOperations = maxOperations;
106
+ }
107
+ hasActive() {
108
+ return this.activeId !== null;
109
+ }
110
+ create(packageName, beforeVersion) {
111
+ const operationId = `${this.bootId}-update-${String(++this.sequence)}`;
112
+ const createdAt = this.now();
113
+ const operation = {
114
+ schema: UPDATE_API_V1_SCHEMA,
115
+ operationId,
116
+ kind: 'update',
117
+ packageName,
118
+ state: 'queued',
119
+ createdAt,
120
+ startedAt: null,
121
+ finishedAt: null,
122
+ beforeVersion,
123
+ installedVersion: beforeVersion,
124
+ progress: progressView(),
125
+ outcome: {
126
+ refreshRequired: false,
127
+ restartRequired: false,
128
+ rollback: { available: false, state: 'unavailable', detail: null },
129
+ },
130
+ failure: null,
131
+ legacyRollbackId: null,
132
+ };
133
+ this.operations.set(operationId, operation);
134
+ this.activeId = operationId;
135
+ while (this.operations.size > this.maxOperations) {
136
+ const oldestId = this.operations.keys().next().value;
137
+ if (oldestId === undefined || oldestId === this.activeId)
138
+ break;
139
+ this.operations.delete(oldestId);
140
+ }
141
+ return this.snapshot(operation);
142
+ }
143
+ start(operationId) {
144
+ const operation = this.operations.get(operationId);
145
+ if (operation === undefined)
146
+ return;
147
+ operation.state = 'running';
148
+ operation.startedAt = this.now();
149
+ this.activeId = operationId;
150
+ }
151
+ finish(operationId, status, payload, installedVersion) {
152
+ const operation = this.operations.get(operationId);
153
+ if (operation === undefined)
154
+ return null;
155
+ const body = record(payload) ?? {};
156
+ const cancelled = bool(body.cancelled);
157
+ const ok = status >= 200 && status < 300 && bool(body.ok) && !cancelled;
158
+ operation.state = cancelled ? 'cancelled' : ok ? 'succeeded' : 'failed';
159
+ operation.finishedAt = this.now();
160
+ operation.installedVersion = installedVersion;
161
+ operation.failure = ok || cancelled ? null : failureOf(status, body);
162
+ const activation = activationState(body, operation.packageName);
163
+ operation.outcome.restartRequired = activation === 'restart';
164
+ operation.outcome.refreshRequired = bool(body.needsRefresh)
165
+ || (activation === 'live' && body.hot === true);
166
+ operation.legacyRollbackId = rollbackIdOf(body);
167
+ if (operation.legacyRollbackId !== null) {
168
+ operation.outcome.rollback.available = true;
169
+ operation.outcome.rollback.state = 'available';
170
+ }
171
+ if (this.activeId === operationId)
172
+ this.activeId = null;
173
+ return this.snapshot(operation);
174
+ }
175
+ beginRollback(operationId) {
176
+ const operation = this.operations.get(operationId);
177
+ if (operation?.legacyRollbackId === null || operation?.legacyRollbackId === undefined)
178
+ return null;
179
+ operation.outcome.rollback.state = 'running';
180
+ operation.outcome.rollback.detail = null;
181
+ return operation.legacyRollbackId;
182
+ }
183
+ finishRollback(operationId, status, payload, installedVersion) {
184
+ const operation = this.operations.get(operationId);
185
+ if (operation === undefined)
186
+ return null;
187
+ const body = record(payload) ?? {};
188
+ const ok = status >= 200 && status < 300 && body.rolledBack === true;
189
+ // The rollback endpoint supplies a fresh disk read. Keep the argument
190
+ // optional so existing store consumers retain source compatibility.
191
+ if (installedVersion !== undefined)
192
+ operation.installedVersion = installedVersion;
193
+ operation.outcome.rollback.available = !ok && status !== 400;
194
+ operation.outcome.rollback.state = ok ? 'succeeded' : 'failed';
195
+ operation.outcome.rollback.detail = ok ? null : text(body.error) ?? `rollback failed with HTTP ${String(status)}`;
196
+ if (ok) {
197
+ operation.state = 'rolled-back';
198
+ operation.outcome.restartRequired = true;
199
+ }
200
+ return this.snapshot(operation);
201
+ }
202
+ get(operationId, progress) {
203
+ const operation = this.operations.get(operationId);
204
+ if (operation === undefined)
205
+ return null;
206
+ const view = this.snapshot(operation);
207
+ if (this.activeId === operationId && operation.state === 'running')
208
+ view.progress = progressView(progress);
209
+ return view;
210
+ }
211
+ snapshot(operation) {
212
+ const { legacyRollbackId: _legacyRollbackId, ...view } = operation;
213
+ return structuredClone(view);
214
+ }
215
+ }