@dsh-workbench/core 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +13 -9
  2. package/bin/dsh-workbench +12 -43
  3. package/client.js +914 -350
  4. package/dist/client/client.body.js +914 -350
  5. package/dist/client/index.js +915 -351
  6. package/dist/host/index.js +1314 -667
  7. package/dist/types/app/client.d.ts +216 -6
  8. package/dist/types/app/layouts/config-sync/ConfigSyncSection.d.ts +5 -0
  9. package/dist/types/app/layouts/rightbar/__tests__/market-order.test.d.ts +1 -0
  10. package/dist/types/app/layouts/rightbar/__tests__/market-running-display.test.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/__tests__/market-workbench-act.test.d.ts +1 -0
  12. package/dist/types/app/layouts/rightbar/market-act.d.ts +2 -1
  13. package/dist/types/app/layouts/rightbar/market-rows.d.ts +3 -0
  14. package/dist/types/app/layouts/rightbar/market-settle-hold.d.ts +13 -0
  15. package/dist/types/app/layouts/rightbar/market-settle.d.ts +22 -0
  16. package/dist/types/app/layouts/rightbar/market-transient-state.d.ts +22 -0
  17. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +26 -1
  18. package/dist/types/app/layouts/sidebar/workspace-groups.d.ts +4 -1
  19. package/dist/types/modules/panels/__tests__/market-tools-harness.d.ts +3 -0
  20. package/dist/types/modules/panels/model/market-workbench-update.d.ts +19 -0
  21. package/dist/types/modules/panels/model/market.d.ts +4 -0
  22. package/dist/types/modules/sessions/index.d.ts +1 -1
  23. package/dist/types/modules/sessions/model/session-node.d.ts +1 -0
  24. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  25. package/dist/types/shared/api/config-sync.d.ts +35 -0
  26. package/dist/types/shared/api/config-sync.test.d.ts +1 -0
  27. package/dist/types/shared/api/host/__tests__/config-sync.test.d.ts +1 -0
  28. package/dist/types/shared/api/host/__tests__/post-channels.test.d.ts +1 -0
  29. package/dist/types/shared/api/host/__tests__/self-update.test.d.ts +1 -0
  30. package/dist/types/shared/api/host/bundle-manager.d.ts +1 -0
  31. package/dist/types/shared/api/host/bundle-routes.d.ts +1 -1
  32. package/dist/types/shared/api/host/config-sync.d.ts +46 -0
  33. package/dist/types/shared/api/host/post-channels.d.ts +18 -0
  34. package/dist/types/shared/api/host/self-update.d.ts +45 -0
  35. package/docs/HANDOFF.md +19 -14
  36. package/docs/OPERATIONS.md +40 -8
  37. package/package.json +4 -4
  38. package/scripts/seed-profile.sh +21 -85
@@ -26,9 +26,11 @@ export declare function withClientRefusals(modules: readonly InstalledWorkbenchM
26
26
  }[]): readonly InstalledWorkbenchModule[];
27
27
  export declare function refusalReasons(host: readonly string[] | undefined, client: readonly string[] | undefined): readonly string[];
28
28
  export declare function mergeTitles(known: ReadonlyMap<string, string>, manifests: readonly UnitManifest[]): ReadonlyMap<string, string>;
29
+ export declare function installedIdSet(installed: readonly InstalledWorkbenchModule[] | undefined): ReadonlySet<string> | null;
29
30
  export declare function installedRows(modules: readonly InstalledWorkbenchModule[], manifests: readonly UnitManifest[], listed: ReadonlySet<string>, known?: ReadonlyMap<string, string> | undefined, defaults?: ReadonlySet<string>, changed?: Readonly<Record<string, number>>): MarketRow[];
30
31
  export declare function marketSwitchLocked(row: MarketRow): boolean;
31
32
  export declare function marketActions(row: MarketRow): MarketRowAction[];
33
+ export declare function capabilityOutdated(capabilities: CapabilityMap | null, unit: string): boolean;
32
34
  export declare function marketCatalog(manifests: readonly UnitManifest[], installable: ReadonlySet<string> | null | undefined): readonly UnitManifest[];
33
35
  export declare function liveManifests(manifests: readonly UnitManifest[], capabilities: CapabilityMap | null, installed: ReadonlySet<string> | null): readonly UnitManifest[];
34
36
  export declare function marketHasUpdates(rows: readonly MarketRow[], shared?: readonly SharedRow[]): boolean;
@@ -39,5 +41,6 @@ export interface MarketSection {
39
41
  readonly titleKey: string;
40
42
  readonly rows: readonly MarketRow[];
41
43
  }
44
+ export declare function marketOrder(rows: readonly MarketRow[], titleOf: (row: MarketRow) => string): MarketRow[];
42
45
  export declare function marketSections(rows: readonly MarketRow[]): MarketSection[];
43
46
  export declare function marketRows(manifests: readonly UnitManifest[], capabilities: CapabilityMap | null, updates?: ModuleUpdateMap, defaults?: ReadonlySet<string>, changed?: Readonly<Record<string, number>>): MarketRow[];
@@ -0,0 +1,13 @@
1
+ import type { Translate } from '@dsh-workbench/shared';
2
+ import type { Pending } from './market-view-text';
3
+ export declare function useSettleHold(input: {
4
+ readonly pending: Pending | null;
5
+ readonly landed: boolean;
6
+ readonly t: Translate;
7
+ readonly onChanged: () => void;
8
+ readonly setPending: (next: Pending | null) => void;
9
+ readonly setFailed: (next: {
10
+ readonly id: string;
11
+ readonly message: string;
12
+ } | null) => void;
13
+ }): void;
@@ -1,2 +1,24 @@
1
+ import type { MarketRowAction, ModuleUpdate } from './market-row-types';
1
2
  export declare const MODULE_SETTLE_MS = 15000;
2
3
  export declare function holdBack(installed: readonly string[], roster: ReadonlySet<string>, active: ReadonlySet<string>, previous: ReadonlySet<string> | null): readonly string[];
4
+ export declare const WORKBENCH_SWAP_MS = 180000;
5
+ export declare const WORKBENCH_SWAP_POLL_MS = 800;
6
+ export declare function swapLanded(until: string | undefined, installed: string | undefined): boolean;
7
+ export declare function swapUntil(action: string, application: string | undefined, latest: string | undefined): string | undefined;
8
+ export declare function awaitsReadBack(action: MarketRowAction, application: string | undefined, version: string | undefined): boolean;
9
+ export declare function readBackDone(input: {
10
+ readonly action: MarketRowAction;
11
+ readonly entry: ModuleUpdate | undefined;
12
+ readonly clicked: string | undefined;
13
+ readonly outdated: boolean;
14
+ }): boolean;
15
+ export declare function settleLanded(input: {
16
+ readonly action: MarketRowAction;
17
+ readonly until: string | undefined;
18
+ readonly readBack: boolean | undefined;
19
+ readonly installed: string | undefined;
20
+ readonly entry: ModuleUpdate | undefined;
21
+ readonly clicked: string | undefined;
22
+ readonly outdated: boolean;
23
+ }): boolean;
24
+ export declare function holdWindowMs(until: string | undefined): number;
@@ -0,0 +1,22 @@
1
+ import type { MarketMock } from './market-mock';
2
+ import type { Pending } from './market-view-text';
3
+ export interface MarketTransient {
4
+ readonly pending: Pending | null;
5
+ readonly setPending: (next: Pending | null) => void;
6
+ readonly confirming: string | null;
7
+ readonly setConfirming: (next: string | null) => void;
8
+ readonly failed: {
9
+ readonly id: string;
10
+ readonly message: string;
11
+ } | null;
12
+ readonly setFailed: (next: {
13
+ readonly id: string;
14
+ readonly message: string;
15
+ } | null) => void;
16
+ readonly notice: string | null;
17
+ readonly setNotice: (next: string | null) => void;
18
+ }
19
+ export declare function useMarketTransient(input: {
20
+ readonly mock: MarketMock | undefined;
21
+ readonly onPending?: ((pending: boolean) => void) | undefined;
22
+ }): MarketTransient;
@@ -1,13 +1,38 @@
1
- import type { MarketRow, MarketRowAction } from './market-rows';
1
+ import type { UnitManifest } from '@dsh-workbench/shared';
2
+ import type { CapabilityMap } from '../../../shared/hooks/use-capabilities';
3
+ import type { MarketRow, MarketRowAction, ModuleUpdateMap } from './market-rows';
2
4
  export declare const PROGRESS_KEY: Readonly<Record<MarketRowAction, string>>;
3
5
  export declare const ACTION_LABEL_KEY: Readonly<Record<MarketRowAction, string>>;
6
+ export interface HeldRow {
7
+ readonly id: string;
8
+ readonly row: MarketRow;
9
+ }
4
10
  export interface Pending {
5
11
  readonly id: string;
6
12
  readonly action: MarketRowAction;
7
13
  readonly titleKey?: string | undefined;
8
14
  readonly pkg?: string | undefined;
9
15
  readonly version?: string | undefined;
16
+ readonly row?: MarketRow | undefined;
17
+ readonly until?: string | undefined;
18
+ readonly readBack?: boolean | undefined;
10
19
  }
20
+ export declare function holdOf(pending: Pending | null, application: string | undefined): HeldRow | undefined;
21
+ export declare function pendingLanded(pending: Pending | null, updates: ModuleUpdateMap, capabilities: CapabilityMap | null, installed: string | undefined): boolean;
22
+ export declare function holdExpired(held: HeldRow | undefined, backInRoster: boolean, elapsedMs: number): boolean;
23
+ export declare function heldIdOf(pending: Pending | null, held: HeldRow | undefined): string | null;
24
+ export declare function pinnedRowOf(pending: Pending | null, held: HeldRow | undefined): MarketRow | undefined;
25
+ export declare function withPinnedRow(rows: readonly MarketRow[], pinned: MarketRow | undefined): MarketRow[];
26
+ export declare function marketFootVisible(shown: readonly MarketRow[], installed: readonly MarketRow[]): boolean;
27
+ export declare function useMarketTitles(manifests: readonly UnitManifest[], mocked: boolean): ReadonlyMap<string, string>;
28
+ export interface HeldRowControl {
29
+ readonly held: HeldRow | undefined;
30
+ hold(next: HeldRow | undefined): void;
31
+ }
32
+ export declare function useHeldRow(pending: Pending | null, backInRoster: (id: string) => boolean): HeldRowControl;
33
+ export declare function removingIdOf(pending: Pending | null, failed: {
34
+ readonly id: string;
35
+ } | null): string | null;
11
36
  export declare function pendingRowOf(input: {
12
37
  readonly pending: Pending | null;
13
38
  readonly removing: string | null;
@@ -1,13 +1,16 @@
1
+ export type WorkspaceSectionId = 'workspaces' | 'defaultWorkspace' | 'workshop';
1
2
  export interface WorkspaceHeadings {
2
3
  readonly workspaces: string;
4
+ readonly defaultWorkspace: string;
3
5
  readonly workshop: string;
4
6
  }
5
7
  export interface WorkspaceSectionPlan<T> {
6
- readonly id: 'workspaces' | 'workshop';
8
+ readonly id: WorkspaceSectionId;
7
9
  readonly heading: string | null;
8
10
  readonly items: readonly T[];
9
11
  }
10
12
  export declare function workspaceSections<T extends {
11
13
  readonly path: string;
14
+ readonly title: string;
12
15
  }>(items: readonly T[], headings: WorkspaceHeadings): readonly WorkspaceSectionPlan<T>[];
13
16
  export declare function flattenSections<T>(sections: readonly WorkspaceSectionPlan<T>[]): readonly T[];
@@ -2,6 +2,7 @@ import type { UnitCapability } from '../../../shared/api/host/probe-units';
2
2
  import type { BundleManager } from '../../../shared/api/host/bundle-manager';
3
3
  import type { InstalledModule, ProfileModule, ProfilePackage } from '../../../shared/api/host/installed-module';
4
4
  import type { PluginSource, UnitManifest } from '@dsh-workbench/shared';
5
+ import { type SelfUpdateRequest } from '../../../shared/api/host/self-update';
5
6
  import { type PluginLedger } from '../model/plugin-ledger';
6
7
  export declare function unit(id: string, plugin: string, source?: PluginSource): UnitManifest;
7
8
  export declare function libUnit(id: string, libName: string): UnitManifest;
@@ -25,6 +26,7 @@ export declare function harness(options: {
25
26
  };
26
27
  npmLatest?: string;
27
28
  npmMissing?: readonly string[];
29
+ profileName?: string | null;
28
30
  }): {
29
31
  registered: {
30
32
  name: string;
@@ -37,6 +39,7 @@ export declare function harness(options: {
37
39
  commands: () => string[];
38
40
  };
39
41
  disposers: (() => void)[];
42
+ selfUpdates: SelfUpdateRequest[];
40
43
  };
41
44
  export declare function fakeBundles(over?: Partial<BundleManager>): BundleManager;
42
45
  export declare function call(h: ReturnType<typeof harness>, args: unknown): Promise<Record<string, unknown>>;
@@ -0,0 +1,19 @@
1
+ import type { DshShell } from '@dsh-workbench/shared';
2
+ import type { InstalledModule, ProfilePackage } from '../../../shared/api/host/installed-module';
3
+ import { type SelfUpdateRequest, type SelfUpdateStart } from '../../../shared/api/host/self-update';
4
+ import type { BundleInstallOutcome } from './market-types';
5
+ export interface WorkbenchUpdateDeps {
6
+ readonly asked: string;
7
+ readonly workbench?: ((pkg: string) => ProfilePackage | undefined) | undefined;
8
+ readonly shell?: DshShell | undefined;
9
+ readonly installed?: ((pkg: string) => InstalledModule) | undefined;
10
+ readonly dshHome?: (() => string) | undefined;
11
+ readonly profileRegistry?: (() => Promise<string | undefined>) | undefined;
12
+ readonly profileName?: (() => string | null) | undefined;
13
+ readonly selfUpdate?: ((request: SelfUpdateRequest, onError: (message: string) => void) => SelfUpdateStart) | undefined;
14
+ readonly logger?: {
15
+ info?(message: string): void;
16
+ warn?(message: string): void;
17
+ } | undefined;
18
+ }
19
+ export declare function updateWorkbenchOnce(deps: WorkbenchUpdateDeps): Promise<BundleInstallOutcome>;
@@ -4,6 +4,7 @@ import type { BundleManager } from '../../../shared/api/host/bundle-manager';
4
4
  import type { InstalledModule, ProfileModule, ProfilePackage } from '../../../shared/api/host/installed-module';
5
5
  import type { RegistryNotebook } from '../../../shared/api/host/profile-registries';
6
6
  import type { UnitMountDeps } from '@dsh-workbench/shared/host';
7
+ import type { SelfUpdateRequest, SelfUpdateStart } from '../../../shared/api/host/self-update';
7
8
  import { type PluginReferences } from './plugin-actions';
8
9
  import type { PluginLedger } from './plugin-ledger';
9
10
  import type { MarketService } from './market-types';
@@ -25,6 +26,9 @@ export declare function createMarketService(options: {
25
26
  registries?: RegistryNotebook | undefined;
26
27
  ledger?: PluginLedger | undefined;
27
28
  references?: (() => PluginReferences) | undefined;
29
+ dshHome?: (() => string) | undefined;
30
+ profileName?: (() => string | null) | undefined;
31
+ selfUpdate?: ((request: SelfUpdateRequest, onError: (message: string) => void) => SelfUpdateStart) | undefined;
28
32
  logger?: {
29
33
  info?(message: string): void;
30
34
  warn?(message: string): void;
@@ -1,2 +1,2 @@
1
- export { currentSessionId, sessionNode, orderSessionRows, sessionRows, workspaceOfSession, sessionStatuses, visiblePendingKind, type SessionNode, type SessionStatusDot, } from './model/session-node';
1
+ export { currentSessionId, orderByRecency, sessionNode, orderSessionRows, sessionRows, workspaceOfSession, sessionStatuses, visiblePendingKind, type SessionNode, type SessionStatusDot, } from './model/session-node';
2
2
  export { scrollEdgesOf, type ScrollEdges } from './model/scroll-edges';
@@ -19,6 +19,7 @@ export declare function visiblePendingKind(kind: string | undefined): string | u
19
19
  export declare function sessionNode(summary: SessionSummary, statusById: SessionStatusSnapshot, list: SessionsSnapshot, pinnedIds: ReadonlySet<string>, archivedIds: ReadonlySet<string>): SessionNode;
20
20
  export declare function sessionStatuses(node: SessionNode, t: Translate): SessionStatusDot[];
21
21
  export declare function currentSessionId(list: SessionsSnapshot, panelActive: boolean): string | undefined;
22
+ export declare function orderByRecency(rows: readonly SessionSummary[]): SessionSummary[];
22
23
  export declare function orderSessionRows(rows: readonly SessionSummary[], pinnedIds: ReadonlySet<string>, archivedIds: ReadonlySet<string>): SessionSummary[];
23
24
  export declare function workspaceOfSession<T extends {
24
25
  readonly sessionIds: readonly string[];
@@ -111,6 +111,7 @@ export interface WorkbenchVersion {
111
111
  readonly pkg: string;
112
112
  readonly installed: string;
113
113
  readonly latest?: string;
114
+ readonly running?: string;
114
115
  readonly local?: boolean;
115
116
  }
116
117
  export interface ModuleVersionsPayload {
@@ -0,0 +1,35 @@
1
+ export declare const CONFIG_SYNC_ROUTE = "config-sync";
2
+ export declare const CONFIG_SYNC_RESOURCE = "configSync";
3
+ export type ConfigSyncState = 'new' | 'differs';
4
+ export interface ConfigSyncRow {
5
+ readonly key: string;
6
+ readonly id: string;
7
+ readonly insert: boolean;
8
+ readonly state: ConfigSyncState;
9
+ readonly body: string;
10
+ readonly ownBody: string | null;
11
+ readonly missing: readonly string[];
12
+ }
13
+ export interface ConfigSyncProfile {
14
+ readonly name: string;
15
+ readonly readable: boolean;
16
+ readonly candidates: number;
17
+ }
18
+ export interface ConfigSyncSnapshot {
19
+ readonly self: string;
20
+ readonly home: string;
21
+ readonly source: string;
22
+ readonly profiles: readonly ConfigSyncProfile[];
23
+ readonly rows: readonly ConfigSyncRow[];
24
+ readonly emptyProviders: boolean;
25
+ readonly reason?: string;
26
+ }
27
+ export interface ConfigSyncApplied {
28
+ readonly written: readonly string[];
29
+ readonly skipped: readonly string[];
30
+ }
31
+ export interface ConfigSyncRequest {
32
+ readonly source: string;
33
+ readonly keys: readonly string[];
34
+ readonly overwrite: readonly string[];
35
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -5,6 +5,7 @@ export interface BundleManager {
5
5
  inspect(spec: string, registry?: BundleRegistry): Promise<BundleInspection | null>;
6
6
  install(request: BundleInstallRequest): Promise<BundleChange | null>;
7
7
  remove(name: string): Promise<BundleChange | null>;
8
+ setEnabled?(name: string, enabled: boolean): Promise<BundleChange | null>;
8
9
  cancel(requestId: string): Promise<BundleCancellation | null>;
9
10
  }
10
11
  export declare function readBundleManager(ctx: DshHostContext): BundleManager | undefined;
@@ -3,5 +3,5 @@ import { type RouteHandler } from '@dsh-workbench/shared/host';
3
3
  export declare function bundleRegistriesHandler(ctx: DshHostContext): RouteHandler;
4
4
  export declare function resetRefusalLog(): void;
5
5
  export declare function resetHoldLog(): void;
6
- export declare function moduleVersionsHandler(ctx: DshHostContext, shell: DshShell | undefined): RouteHandler;
6
+ export declare function moduleVersionsHandler(ctx: DshHostContext, shell: DshShell | undefined, running?: string | undefined): RouteHandler;
7
7
  export declare function bundleInspectHandler(ctx: DshHostContext): RouteHandler;
@@ -0,0 +1,46 @@
1
+ import type { PostResource, RouteHandler } from '@dsh-workbench/shared/host';
2
+ import type { ConfigSyncRow, ConfigSyncSnapshot } from '../config-sync.js';
3
+ export interface PatchBlock {
4
+ readonly key: string;
5
+ readonly id: string;
6
+ readonly insert: boolean;
7
+ readonly body: string;
8
+ readonly names: readonly string[];
9
+ }
10
+ export declare function patchBlocks(text: string): PatchBlock[];
11
+ export declare function normalizeBlock(body: string): string;
12
+ export declare function syncRows(source: readonly PatchBlock[], own: readonly PatchBlock[], missing: (block: PatchBlock) => readonly string[]): ConfigSyncRow[];
13
+ export declare function mergeBlocks(ownText: string, incoming: readonly PatchBlock[], overwrite: ReadonlySet<string>): {
14
+ ok: true;
15
+ text: string;
16
+ written: string[];
17
+ skipped: string[];
18
+ } | {
19
+ ok: false;
20
+ reason: string;
21
+ };
22
+ export declare function readPatchBlocks(path: string): {
23
+ ok: true;
24
+ text: string;
25
+ blocks: PatchBlock[];
26
+ } | {
27
+ ok: false;
28
+ reason: string;
29
+ };
30
+ export declare function writePatchFile(path: string, text: string): void;
31
+ export interface ConfigSyncDeps {
32
+ readonly home: string;
33
+ readonly self: string;
34
+ readonly profileDir: string | null;
35
+ }
36
+ export declare function configSyncHandler(deps: ConfigSyncDeps): RouteHandler;
37
+ export declare function configSyncSnapshot(deps: ConfigSyncDeps, asked?: string): ConfigSyncSnapshot;
38
+ export declare function configSyncApply(deps: ConfigSyncDeps, payload: unknown): {
39
+ ok: true;
40
+ written: string[];
41
+ skipped: string[];
42
+ } | {
43
+ ok: false;
44
+ text: string;
45
+ };
46
+ export declare function configSyncAction(deps: ConfigSyncDeps): PostResource;
@@ -0,0 +1,18 @@
1
+ import type { DshHostContext } from '@dsh-workbench/shared';
2
+ import type { PostResource, PostWrite } from '@dsh-workbench/shared/host';
3
+ import { type UnitsPlan } from './mount-units.js';
4
+ export interface PostChannelDeps {
5
+ readonly ctx: DshHostContext;
6
+ readonly mounted: ReadonlyMap<string, {
7
+ readonly plan: UnitsPlan;
8
+ }>;
9
+ readonly facts: {
10
+ bump(names: readonly string[]): unknown;
11
+ };
12
+ readonly shellActions: readonly PostResource[];
13
+ }
14
+ export interface PostChannels {
15
+ rebind(wrote: (resource: string, write: PostWrite) => void): void;
16
+ dispose(): void;
17
+ }
18
+ export declare function createPostChannels(deps: PostChannelDeps): PostChannels;
@@ -0,0 +1,45 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { type SeedCommand } from './runtime-seed';
3
+ export declare const SELF_UPDATE_STATES: readonly ["running", "ok", "rolled-back", "failed"];
4
+ export type SelfUpdateState = (typeof SELF_UPDATE_STATES)[number];
5
+ export interface SelfUpdatePaths {
6
+ readonly dir: string;
7
+ readonly script: string;
8
+ readonly marker: string;
9
+ readonly log: string;
10
+ }
11
+ export interface SelfUpdateRequest {
12
+ readonly dshHome: string;
13
+ readonly profile: string;
14
+ readonly pkg: string;
15
+ readonly from: string;
16
+ readonly to: string;
17
+ readonly pinned: boolean;
18
+ }
19
+ export interface SelfUpdateMarker extends SelfUpdateRequest {
20
+ readonly state: SelfUpdateState;
21
+ readonly version: string;
22
+ readonly at: string;
23
+ }
24
+ export interface SelfUpdatePlan {
25
+ readonly paths: SelfUpdatePaths;
26
+ readonly script: string;
27
+ readonly marker: SelfUpdateMarker;
28
+ readonly command: SeedCommand;
29
+ }
30
+ export interface SelfUpdateStart {
31
+ readonly started: boolean;
32
+ readonly paths: SelfUpdatePaths;
33
+ readonly forward: string;
34
+ }
35
+ export declare function runningVersionOf(moduleUrl: string): string | undefined;
36
+ export declare function selfUpdatePaths(dshHome: string): SelfUpdatePaths;
37
+ export declare function recoveryCommand(input: {
38
+ readonly profile: string;
39
+ readonly pkg: string;
40
+ readonly version: string;
41
+ }): string;
42
+ export declare function selfUpdateMarker(request: SelfUpdateRequest, state: SelfUpdateState, version: string, at: string): SelfUpdateMarker;
43
+ export declare function selfUpdateScript(request: SelfUpdateRequest, paths: SelfUpdatePaths): string;
44
+ export declare function planSelfUpdate(request: SelfUpdateRequest, now?: Date): SelfUpdatePlan;
45
+ export declare function startSelfUpdate(request: SelfUpdateRequest, onError: (message: string) => void, spawnImpl?: typeof spawn): SelfUpdateStart;
package/docs/HANDOFF.md CHANGED
@@ -58,16 +58,19 @@ npx @dsh-workbench/core
58
58
  > `chmod +x ~/.npm/_npx/*/node_modules/@dsh-workbench/core/{scripts/seed-profile.sh,bin/opencli-isolated}`。
59
59
  > 0.1.7 起启动器已不吃 x 位,重装那个版本即可根治。
60
60
 
61
- ### 设置从哪来:**两边都要的在 `dsh web` 里配**(单向、实时)
61
+ ### 设置从哪来:**工作台自己那份说了算**,想要 web 的行就同步一次(ADR-0044)
62
62
 
63
- 工作台启动时会用 `--patch` 把 `profiles/web/cordis.patch.yml` 叠在自己的差异层之上
64
- (`bin/dsh-workbench` 里就在做这件事)。所以:
63
+ 工作台**不再**把 `profiles/web/cordis.patch.yml` 叠在自己上面 —— 叠层是最高层,
64
+ 它盖住的行在 `:3081` 的 Settings 里**改不动**(dsh 的 config-editor 会被那句
65
+ `overridden by a home patch or command-line overlay` 挡在写文件之前)。
66
+ 现在的工作方式:
65
67
 
66
- - **模型供应商 / API key / 搜索源 / MCP** 这类"机器级、两边都要"的设置 ⇒ **在 `:3080` 的 Settings 里配一次**,工作台自动同源;
67
- - **只有工作台要的**(面板显示哪些模块、会话隔离)⇒ 在 `:3081` 里配,落在它自己的 profile patch,web 看不到;
68
- - 被 web 声明过的行,在工作台的 Settings 页里会显示 `overridden by a home patch or command-line overlay` —— 那是"这行由 web 管"的**提示**,不是故障。
69
-
70
- (这是 `--patch` 单向叠加,不是双向同步:工作台里配的**不会**流回 web。)
68
+ - **要配什么就在 `:3081` 配**,落在 `profiles/workbench/cordis.patch.yml` —— 那是它唯一的那份设置文档;
69
+ - **想用 web 那边已经配好的行**(模型供应商 / API key 引用 / 搜索源 / MCP)⇒ `:3081` →
70
+ `Meta+,` → 「配置同步」→ 选来源 `web` → 勾行 → 同步(即生效,不用重启)。
71
+ 它读的是那个 profile 的 `cordis.patch.yml`,按行搬,只搬**显式配过**的行;
72
+ 同 id 两边内容不同时**默认不覆盖**,要覆盖得自己勾;行点名的包没装会当场说出来。
73
+ - **web 后来改了不会自动过来** —— 再打开这一页,那几行会重新以「差异」标签出现在这一页,拉一次即可。
71
74
 
72
75
  ### 一条"看起来坏了、其实没坏"的情况
73
76
 
@@ -85,14 +88,16 @@ Settings → 插件里把 `@dsh-workbench/core` 关掉后,工作台会**退化
85
88
  ### `setup` 到底做了什么(幂等,可以重复跑)
86
89
 
87
90
  1. 拿 `~/.dsh/profiles/web` 当模板建 `workbench` profile;
88
- 2. 继承 web 的 bundle 清单 + 设置快照 + patch 点名的依赖;
91
+ 2. 继承 web 的 **bundle 清单**(装没装东西 —— 安装范畴,走 `dsh plugin` 通路);
89
92
  3. 追加**数据隔离层**(会话 / 存储 / 附件各走 `~/.dsh/workbench/**`);
90
93
  4. `dsh plugin --profile workbench add @dsh-workbench/core` —— 走 **dsh 自己的**
91
94
  安装通路(兼容性预检、profile 写锁、装完自动把 bundle 登记进 profile 清单)。
92
95
 
93
- 继承是**一次性快照**,不是订阅:web 之后改设置不会流过来(这正是"互不影响"的实现方式),
94
- 要重新同步得重跑 `setup`。有意共享的只有两样:**API 凭据**(`~/.dsh/.credentials.yaml`,
95
- 一机一把 key)和 `$DSH_HOME/cordis.patch.yml`(机器公共层,对所有 profile 实时生效)。
96
+ **设置不在这一步继承**(这里以前快照 web 的 patch、并继承被它点名的依赖):新机器上
97
+ `setup` 完的工作台**没有模型供应商**,第一次打开去 设置 → 「配置同步」拉一次即可 ——
98
+ 这样每一行都归工作台自己,改得动。有意共享的只有两样:**API 凭据**
99
+ (`~/.dsh/.credentials.yaml`,一机一把 key)和 `$DSH_HOME/cordis.patch.yml`(机器公共层,
100
+ 对所有 profile 实时生效)。
96
101
 
97
102
  ## 四、不想用 npx?仓库副本也可以
98
103
 
@@ -183,7 +188,7 @@ manifest;彩排结束、以及「零上传」的失败,都会自动回滚。
183
188
  `pnpm publish` 的人兜底的(外壳那份 = 整仓门禁);不关掉它们,pnpm 会在**彩排与真发各再跑
184
189
  一遍**整门禁 —— 一次发布就是 3 遍门禁 + 3 遍 vitest + 每个包多两轮构建(实测 pnpm 10.32.1:
185
190
  `--dry-run` 一样触发)。来历见
186
- [`.scratch/dsh-ui-alignment/spec.md`](../../../.scratch/dsh-ui-alignment/spec.md) §18.45。
191
+ [`../../../.scratch/dsh-ui-alignment/issues/12-s18-pduiv2.md`](../../../.scratch/dsh-ui-alignment/issues/12-s18-pduiv2.md) §18.45。
187
192
 
188
193
  脚本把这几件事变成**硬失败**(都在任何上传之前):
189
194
 
@@ -203,7 +208,7 @@ manifest;彩排结束、以及「零上传」的失败,都会自动回滚。
203
208
  自己拥有的转发脚本。这条由外壳 `selfcheck` 的判据钉着(门禁里跑)。
204
209
  这类错**本机复现不出来**,只在别人机器上炸
205
210
  (0.1.6 就是这么坏在 `seed-profile.sh: Permission denied` 上的,见
206
- [`.scratch/dsh-ui-alignment/spec.md`](../../../.scratch/dsh-ui-alignment/spec.md) §18.41)。
211
+ [`.scratch/dsh-ui-alignment/issues/53-18-41-permission-denied-pnpm.md`](../../../.scratch/dsh-ui-alignment/issues/53-18-41-permission-denied-pnpm.md) §18.41)。
207
212
 
208
213
  ## 八、版本与兼容(实话)
209
214
 
@@ -26,7 +26,8 @@
26
26
  `OPENCLI_PIN` 锁),但它连的 **daemon 是整机一个** —— 见下面「隔离边界」。
27
27
 
28
28
  前提:`dsh` 和 `pnpm` 在 PATH 上(`dsh plugin` 是转发给 pnpm 的),
29
- 且 `~/.dsh/profiles/web` 已存在(跑过一次 `dsh web` 即可)。
29
+ 且 `~/.dsh/profiles/web` 已存在(跑过一次 `dsh web` 即可)—— seed 拿它当**模板**
30
+ (bundle 清单),之后也是「配置同步」的默认来源。
30
31
 
31
32
  **给别人用**(两条路:插件页装进他自己的 web / 独立实例 `setup`;包在公网 npm 上)见 [`HANDOFF.md`](HANDOFF.md)。
32
33
 
@@ -58,9 +59,9 @@
58
59
  | 开工作台 | `./bin/dsh-workbench` |
59
60
  | 开发(监听改动 + 起工作台) | `pnpm run dev` —— profile 不对会先 `setup`;`:3081` 已在跑就只监听 |
60
61
  | 换端口 | `WORKBENCH_PORT=3091 ./bin/dsh-workbench` |
61
- | 改工作台设置 | 在 `:3081` 的 Settings 页改,落在 `profiles/workbench/cordis.patch.yml` |
62
- | 改 web 设置 | 在 `:3080` 改,落在 `profiles/web/cordis.patch.yml`。**两边都要的设置(模型供应商、搜索源、MCP)就在这一份里配** —— 工作台启动时会用 `--patch` 把它叠上来(web → 工作台,单向、实时),所以不必配两遍 |
63
- | 改只有工作台用的设置 | 在 `:3081` 改,落在 `profiles/workbench/cordis.patch.yml`。这个文件**只剩工作台自己的差异**(会话/存储/附件隔离 + 沙箱);被 web 覆盖过的行在这里改不动(编辑器会写 `overridden`,那就是"这行由 web 管"的提示) |
62
+ | 改工作台设置 | 在 `:3081` 的 Settings 页改,落在 `profiles/workbench/cordis.patch.yml`。**这是工作台唯一的那份设置文档**(没有别处叠在它上面),所以模型供应商 / 搜索源 / MCP 每一行都改得动 |
63
+ | 用 web 那边已经配好的行 | `:3081` → `Meta+,` → 「配置同步」→ 选来源(默认 `web`)→ 勾行 → 同步(`dsh-hmr` 会重组,**不用重启**)。只搬你在那个 profile 里**显式配过**的行;行点名的包没装时页面会说出来(ADR-0044) |
64
+ | 改 web 设置 | 在 `:3080` 改,落在 `profiles/web/cordis.patch.yml`。工作台**不会**再自动跟着变 —— 要就跟上面那条同步一次;不想跟就不动 |
64
65
  | 重建 profile | `mv ~/.dsh/profiles/workbench ~/.dsh/profiles/workbench.bak` 后重跑 seed |
65
66
  | **别在设置里关掉工作台** | Settings → 插件里 `@dsh-workbench/core` 那个开关**不要关**。关掉=把它从 profile 的 `bundles` 里移除 → 我们那份 bundle patch(含"禁用出厂左栏")**整份不再应用** → 页面退化成**原版 dsh web**(左栏回来、右侧面板消失),**数据一个都不丢**。恢复:插件页把开关打开(左栏的「插件」入口也在;进不去设置就按 `Meta+,`)。实测过这条降级路径,不是推断 |
66
67
 
@@ -85,7 +86,8 @@ WORKBENCH_PORT=3091 pnpm run dev # 换端口(监听与实例同一个端口
85
86
  | 浏览器 | Vite 重建 `client.js` → `dsh-client-hmr` 每 500ms stat-poll 到 mtime/大小变化 → `/plugins/events` 发 `rebuilt` → 浏览器作废旧模块、预取新包、换 fiber | 插件内的 React 状态会丢;会话 / 工作区 / 连接状态保留 |
86
87
  | 宿主 | Vite 重建 `dist/host/index.js` → `dsh-hmr` 按模块 realpath 匹配并重载插件 | 插件重建,进程与其它插件不受影响 |
87
88
 
88
- **只有这两种改动要重启**:`cordis.patch.yml`(组合层)与 `package.json` 的 `dsh.profile.bundles`。
89
+ `cordis.patch.yml`(组合层)与 `package.json` 的 `dsh.profile.bundles` **也在监听里**:`dsh-hmr` 变了就 `recomposeProfile`,
90
+ 不用重启(它报 `restartReason` 时才按提示重启)。仍然要重启的是改 `vite.config.ts`。
89
91
 
90
92
  宿主侧监听为什么要环境变量:出厂 `hmr` 的 `root` 默认是 `[]`(不监听),
91
93
  而相对路径以 profile 目录为基准 —— 我们的包在 profile 里只是一个指向本仓库的符号链接,
@@ -102,8 +104,9 @@ WORKBENCH_PORT=3091 pnpm run dev # 换端口(监听与实例同一个端口
102
104
 
103
105
  0. **代码接线** —— 槽位名 / key / id / locale 是否对得上(语法检查抓不到这类错)
104
106
  1. **web 是否被动过** —— 与 `--record` 的哈希逐一比对
105
- 2. **组合树差异** —— `dsh web --dump-config` vs `dsh workbench --dump-config`,
106
- 差异应恰好是隔离行 + 工作台行 + `ui-sidebar` 被禁用
107
+ 2. **组合树** —— `dsh workbench --dump-config`:工作台行在位、出厂 `ui-sidebar` 被禁用、
108
+ 启动器里没有 web 设置的叠层(脚本直接判,见 ADR-0044)。两棵树**本来就不该相同**
109
+ (工作台不再继承 web 的设置),差异只打出来给人看
107
110
  3. **数据是否分家** —— `~/.dsh/sessions` 与 `~/.dsh/workbench/sessions` 各自独立
108
111
  4. **隔离行是否在位** —— 三个根行都在 `profiles/workbench/cordis.patch.yml` 里
109
112
 
@@ -126,6 +129,35 @@ WORKBENCH_PORT=3091 pnpm run dev # 换端口(监听与实例同一个端口
126
129
  | opencli **daemon** | ❌ **整机一个,隔离不了** | 端口 **19825 被扩展写死**(`background.js` 里 `DAEMON_PORT = 19825`;CLI 侧 `OPENCLI_DAEMON_PORT` 设非默认值直接报错)。所以本机的全局 opencli 与工作台的隔离运行时**抢同一个 daemon**:两边版本不一致时,谁的命令后跑谁就把 daemon 重启成自己那版 |
127
130
  | profile 的 pnpm 源(私有 scope) | ⚠️ **由工作台写进去** | `<profile>/.npmrc` 里的 `@scope:registry=…`,前面带一行 `# dsh-workbench 写的…` 标记。**为什么在工作台这一侧写**:dsh 的卸载没有源参数(`runPnpm(['remove', name])`),pnpm 只能从 profile 的 `.npmrc` 链知道源;不写它,装私有源的模块能成、卸不了。规则与边界见 [ADR-0030](../../../docs/adr/0030-private-registries-live-in-the-profile-pnpm-config.md):只加不改、只写 scope 行、不碰 `~/.npmrc`。**删了会怎样**:下次从这个源装会再写回来;已经装着的那种,点卸载时撞 404 会自愈补回再重试一次 |
128
131
 
132
+ ## 工作台自己怎么升级、出事怎么救
133
+
134
+ 工作台那一行的「更新」**不会把自己摘掉**(见 [ADR-0045](../../docs/adr/0045-workbench-self-update-runs-outside-the-workbench.md)):
135
+ 点下去之后工作台把现场写到 `$DSH_HOME/workbench/self-update.sh|json|log`,然后起一个**独立进程**去做
136
+ `dsh plugin --profile <profile> add @dsh-workbench/core@<版本>` —— 就地换版本,一行都不摘,
137
+ 所以它换的就是工作台自己也没关系(换的是外头那个进程)。
138
+
139
+ - 成功:`self-update.json` 里 `state: "ok"`,`version` 是新版;**重启一次 dsh** 才用得上(面板会这么说)。
140
+ - 换版没成:脚本自己把旧版本装回去,`state: "rolled-back"` —— 工作台不会因此消失。
141
+ - 回滚也没成:`state: "failed"`,日志最后一行给出救法。终端敲这一条即可:
142
+ `dsh plugin --profile <profile> add @dsh-workbench/core@<版本>`(`<版本>` 取源上最新,或 `latest`)。
143
+
144
+ 两种安装形态的差别:
145
+
146
+ | 怎么装的 | profile 里那一条 | 市场自更新 |
147
+ |---|---|---|
148
+ | `npm i -g @dsh-workbench/core` 然后 `dsh-workbench setup` | `@dsh-workbench/core@^x.y.z`(源上) | 能(就是上面那一条) |
149
+ | 仓库副本(`pnpm run dev` / `link:` 本目录) | `link:…` | 拒:不把开发副本换成源上那份 |
150
+
151
+ 全局那一份**启动器**不会跟着升级(它只是启动器:真正加载的是 profile 里那份)。换 profile 或重建之前,
152
+ 先 `npm i -g @dsh-workbench/core@latest`;已经在跑的 profile 走市场那一颗「更新」。
153
+
154
+ ### 起不来:`failed to parse overlay …/cordis.patch.yml`(YAMLException: end of the stream …)
155
+
156
+ 早期版本 `seed-profile.sh` 会把隔离层直接接到 dsh 新模板的 patch 后面,而那份模板的内容是「横幅注释 +
157
+ 一个空数组 `[]`」—— 空数组本身是一个**完整的 YAML 文档**,后面再接序列就解析不了,profile 一条都组不起来。
158
+ 现在的脚本追加前会先把那行单独的 `[]` 让位(`selfcheck` 有判据钉着)。如果你手上那份 profile 是旧脚本建出来的,
159
+ 手工把 `cordis.patch.yml` 里**单独一行**的 `[]` 删掉再起即可(其余内容不用动)。
160
+
129
161
  ## 排障
130
162
 
131
163
  | 症状 | 原因 |
@@ -134,7 +166,7 @@ WORKBENCH_PORT=3091 pnpm run dev # 换端口(监听与实例同一个端口
134
166
  | 面板一直空着、`/workbench/api/issues` 返回 `coding-not-logged-in` | opencli 登录态失效:在终端跑 `opencli coding team-current` 完成登录 |
135
167
  | 面板显示"这个项目不是 Coding 仓库" | 该工作区的 git `origin` 不指向 `e.coding.net`(或没有 origin) |
136
168
  | 打开面板/按刷新要等 5–8 秒 | 正常:opencli 每次调用有**固定约 5 秒**开销(起浏览器会话;接口本身只要 ~260ms)。**没有缓存**——数据一律实时,列表 = 缺陷 + 状态目录两次调用(限并发 2,实测约 7 秒) |
137
- | 改了源码没变化 | 忘了 `pnpm run build`,或没重启进程(宿主缓存已加载的包) |
169
+ | 改了源码没变化 | 忘了 `pnpm run build`,或没刷新页面;改过 `vite.config.ts` 后没重启(正在跑的 watch 用的还是旧配置) |
138
170
  | 市场页点「卸载」报 `没成功:… ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/@你的私有 scope/…` 之类 | 这个 profile 里的私有包不在 pnpm 默认源上,而 dsh 的卸载**不带源**。工作台会自己补上 `<profile>/.npmrc` 的 `@scope:registry=` 再重试一次([ADR-0030](../../../docs/adr/0030-private-registries-live-in-the-profile-pnpm-config.md));**还失败**说明那个包没有 scope(一行 scope 行表达不了),这时才需要人/agent 上手:`dsh plugin --profile <p> remove '<包名>' --config.registry=<那个源>`(`pnpm remove` 不认 `--registry=`),或者给 profile 自己配一行默认 `registry=` |
139
171
  | 体检那屏「检测浏览器通道 · 没连上」,可面板照样能用 | 本机全局 opencli 与隔离运行时版本不一致 ⇒ daemon 被顶成对方那版(`Stale daemon detected: daemon vX != CLI vY`)。下一条浏览器命令会自动把它重启回来,体检也会复检后改回「已连接」([ADR-0024](../../../docs/adr/0024-warm-the-sites-before-the-panel.md) §3.2.1)。**要彻底不抖**:让两边同版本 —— 把 `OPENCLI_PIN`(`src/shared/config/constants.ts`)对齐到 `opencli --version`,再 `npm i --prefix ~/.dsh/workbench/opencli/toolchain @jackwener/opencli@<那个版本> --cache ~/.dsh/workbench/opencli/npm-cache` |
140
172
  | Settings 页报 `overridden by a home patch` | 想在 Settings 里改的行被 `$DSH_HOME/cordis.patch.yml` 或 `--patch` 覆盖了 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -42,7 +42,7 @@
42
42
  "title": "Workbench",
43
43
  "description": "个人工作台"
44
44
  },
45
- "//scripts": "包内不跑 check:docs —— 那份文档管理块住在仓库根的 docs/ARCHITECTURE.md,由根 `pnpm run check` 用 --config 指到本包的 arch.config.mjs 校验。prepublishOnly 走 -w 跑根那一整套。",
45
+ "//scripts": "包内不跑 check:docs —— 那份文档管理块住在仓库根的 docs/ARCHITECTURE.md,由根 `pnpm run check` 用 --config 指到本包的 arch.config.mjs 校验。check 与 prepublishOnly 都走 -w 跑根那一整套:包内自己串一份会漏掉 lint(react-hooks 那几条规则只在这里跑)、check:docs 与模块工具包的十一条判据 —— 2026-10-16 实测踩到(渲染期写 ref,包内全绿、根上一跑就红)。",
46
46
  "//dsh.bundle.patch": "一份 preset 就是一个补丁层:它 insert 一行 @deepseek-ai/dsh-agent-preset,config.plugins 是整表插件清单。diy.patch.yml 是**生成的**(scripts/gen-diy-preset.mjs,从 dsh 出厂那份 standard.patch.yml 派生),selfcheck 有一条判据盯着它有没有过期。",
47
47
  "dsh": {
48
48
  "bundle": {
@@ -87,7 +87,7 @@
87
87
  "access": "public"
88
88
  },
89
89
  "dependencies": {
90
- "@dsh-workbench/shared": "0.2.5"
90
+ "@dsh-workbench/shared": "0.2.7"
91
91
  },
92
92
  "scripts": {
93
93
  "build": "node scripts/build.mjs && node scripts/build-types.mjs",
@@ -97,7 +97,7 @@
97
97
  "arch": "arch-guard",
98
98
  "selfcheck": "node scripts/selfcheck.mjs",
99
99
  "check:runtimes": "node scripts/check-runtimes.mjs",
100
- "check": "npm run typecheck && npm run coverage && npm run arch && npm run check:runtimes && npm run selfcheck",
100
+ "check": "pnpm -w run check",
101
101
  "test": "vitest run",
102
102
  "coverage": "vitest run --coverage",
103
103
  "dev": "node scripts/dev.mjs",