dsh-over-ssh 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +119 -2
- package/agents/agent-manifest.json +20 -0
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/cordis.patch.yml +84 -0
- package/lib/client.js +6611 -0
- package/lib/file-references.js +366 -0
- package/lib/fs.js +1368 -0
- package/lib/index.js +19034 -0
- package/lib/local-bridge-refs.js +16 -0
- package/lib/local-bridge.js +16 -0
- package/lib/terminal-wrapper.js +294 -0
- package/lib/types/src/client/controls.d.ts +131 -0
- package/lib/types/src/client/core/card-focus-trap.d.ts +45 -0
- package/lib/types/src/client/core/controls.d.ts +164 -0
- package/lib/types/src/client/core/latest.d.ts +48 -0
- package/lib/types/src/client/core/locale.d.ts +38 -0
- package/lib/types/src/client/core/message.d.ts +37 -0
- package/lib/types/src/client/core/panel-model.d.ts +134 -0
- package/lib/types/src/client/core/portal.d.ts +266 -0
- package/lib/types/src/client/core/remote.d.ts +84 -0
- package/lib/types/src/client/core/styles.d.ts +458 -0
- package/lib/types/src/client/file-tree-io.d.ts +19 -0
- package/lib/types/src/client/file-tree.d.ts +271 -0
- package/lib/types/src/client/flows/directory-browser.d.ts +587 -0
- package/lib/types/src/client/flows/session-directory.d.ts +415 -0
- package/lib/types/src/client/flows/workspace-directory-flow.d.ts +480 -0
- package/lib/types/src/client/index.d.ts +49 -0
- package/lib/types/src/client/locale.d.ts +31 -0
- package/lib/types/src/client/panel-model.d.ts +37 -0
- package/lib/types/src/client/panel.d.ts +77 -0
- package/lib/types/src/client/roots/connection-test.d.ts +69 -0
- package/lib/types/src/client/roots/roots-panel-state.d.ts +307 -0
- package/lib/types/src/client/roots/roots-panel-view.d.ts +280 -0
- package/lib/types/src/client/roots/roots-panel.d.ts +364 -0
- package/lib/types/src/client/roots/transfer-panel-view.d.ts +35 -0
- package/lib/types/src/client/roots/transfer-panel.d.ts +241 -0
- package/lib/types/src/client/roots/transfer-progress.d.ts +67 -0
- package/lib/types/src/client/roots-panel-view.d.ts +236 -0
- package/lib/types/src/client/roots-panel.d.ts +347 -0
- package/lib/types/src/client/session/session-panel-view.d.ts +336 -0
- package/lib/types/src/client/session/session-panel.d.ts +322 -0
- package/lib/types/src/client/session/session-remote-mark.d.ts +110 -0
- package/lib/types/src/client/session/session-row-actions.d.ts +398 -0
- package/lib/types/src/client/session-panel.d.ts +152 -0
- package/lib/types/src/client/styles.d.ts +252 -0
- package/lib/types/src/client/workspace-directory-flow.d.ts +342 -0
- package/lib/types/src/config.d.ts +312 -0
- package/lib/types/src/file-references.d.ts +25 -0
- package/lib/types/src/fs.d.ts +46 -0
- package/lib/types/src/host/app/backend.d.ts +170 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +247 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +18 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +45 -0
- package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +38 -0
- package/lib/types/src/host/app/recycle-bin-contract.d.ts +215 -0
- package/lib/types/src/host/app/recycle-bin-move.d.ts +18 -0
- package/lib/types/src/host/app/recycle-bin-service.d.ts +45 -0
- package/lib/types/src/host/app/recycle-bin-world.d.ts +35 -0
- package/lib/types/src/host/app/roots/roots-api-connections.d.ts +49 -0
- package/lib/types/src/host/app/roots/roots-api-contract.d.ts +369 -0
- package/lib/types/src/host/app/roots/roots-api-directories.d.ts +13 -0
- package/lib/types/src/host/app/roots/roots-api-http.d.ts +77 -0
- package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +130 -0
- package/lib/types/src/host/app/roots/roots-api-projections.d.ts +102 -0
- package/lib/types/src/host/app/roots/roots-api-roots.d.ts +13 -0
- package/lib/types/src/host/app/roots/roots-api-state.d.ts +22 -0
- package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +26 -0
- package/lib/types/src/host/app/roots/roots-api.d.ts +55 -0
- package/lib/types/src/host/app/roots/user-roots.d.ts +552 -0
- package/lib/types/src/host/app/roots-api-connections.d.ts +49 -0
- package/lib/types/src/host/app/roots-api-contract.d.ts +339 -0
- package/lib/types/src/host/app/roots-api-directories.d.ts +13 -0
- package/lib/types/src/host/app/roots-api-http.d.ts +77 -0
- package/lib/types/src/host/app/roots-api-projections.d.ts +102 -0
- package/lib/types/src/host/app/roots-api-roots.d.ts +13 -0
- package/lib/types/src/host/app/roots-api-state.d.ts +22 -0
- package/lib/types/src/host/app/roots-api-support.d.ts +182 -0
- package/lib/types/src/host/app/roots-api-transfers.d.ts +13 -0
- package/lib/types/src/host/app/roots-api.d.ts +55 -0
- package/lib/types/src/host/app/routed-fs.d.ts +239 -0
- package/lib/types/src/host/app/serialize.d.ts +37 -0
- package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +36 -0
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +221 -0
- package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +34 -0
- package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +40 -0
- package/lib/types/src/host/app/session-admin-context.d.ts +36 -0
- package/lib/types/src/host/app/session-admin-contract.d.ts +214 -0
- package/lib/types/src/host/app/session-admin-delete.d.ts +34 -0
- package/lib/types/src/host/app/session-admin-service.d.ts +38 -0
- package/lib/types/src/host/app/takeover/takeover-plan.d.ts +227 -0
- package/lib/types/src/host/app/takeover/takeover-rows.d.ts +55 -0
- package/lib/types/src/host/app/transport/transport-selector.d.ts +98 -0
- package/lib/types/src/host/app/transport/transport-service.d.ts +82 -0
- package/lib/types/src/host/app/transport-selector.d.ts +98 -0
- package/lib/types/src/host/app/transport-service.d.ts +75 -0
- package/lib/types/src/host/app/user-roots.d.ts +461 -0
- package/lib/types/src/host/domain/bin-location.d.ts +79 -0
- package/lib/types/src/host/domain/capability.d.ts +78 -0
- package/lib/types/src/host/domain/connection.d.ts +95 -0
- package/lib/types/src/host/domain/errors.d.ts +55 -0
- package/lib/types/src/host/domain/glob-pattern.d.ts +62 -0
- package/lib/types/src/host/domain/literal-text.d.ts +63 -0
- package/lib/types/src/host/domain/path-grammar.d.ts +84 -0
- package/lib/types/src/host/domain/recycle-bin.d.ts +337 -0
- package/lib/types/src/host/domain/root.d.ts +74 -0
- package/lib/types/src/host/domain/roots/bin-location.d.ts +91 -0
- package/lib/types/src/host/domain/roots/connection.d.ts +106 -0
- package/lib/types/src/host/domain/roots/root.d.ts +108 -0
- package/lib/types/src/host/domain/roots/route-table.d.ts +102 -0
- package/lib/types/src/host/domain/roots/session-roots.d.ts +151 -0
- package/lib/types/src/host/domain/roots/ssh-config.d.ts +73 -0
- package/lib/types/src/host/domain/route-table.d.ts +80 -0
- package/lib/types/src/host/domain/session/session-admin.d.ts +345 -0
- package/lib/types/src/host/domain/session/terminal-target.d.ts +136 -0
- package/lib/types/src/host/domain/session-admin.d.ts +330 -0
- package/lib/types/src/host/domain/shell.d.ts +47 -0
- package/lib/types/src/host/domain/ssh-config.d.ts +73 -0
- package/lib/types/src/host/domain/terminal-target.d.ts +132 -0
- package/lib/types/src/host/domain/tool-requests.d.ts +369 -0
- package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +337 -0
- package/lib/types/src/host/domain/transfer/transfer.d.ts +260 -0
- package/lib/types/src/host/domain/transfer.d.ts +260 -0
- package/lib/types/src/host/infra/backend/local-backend.d.ts +84 -0
- package/lib/types/src/host/infra/backend/local-version.d.ts +43 -0
- package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +89 -0
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +239 -0
- package/lib/types/src/host/infra/cache/lru.d.ts +52 -0
- package/lib/types/src/host/infra/cache/versioned.d.ts +77 -0
- package/lib/types/src/host/infra/temp-name.d.ts +19 -0
- package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +121 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +132 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +140 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +41 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +197 -0
- package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +564 -0
- package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +418 -0
- package/lib/types/src/host/infra/transport/agent-installer.d.ts +121 -0
- package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +123 -0
- package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +131 -0
- package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +41 -0
- package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +197 -0
- package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +509 -0
- package/lib/types/src/host/infra/transport/agent-transport.d.ts +353 -0
- package/lib/types/src/host/infra/transport/alias-transport.d.ts +206 -0
- package/lib/types/src/host/infra/transport/askpass-fs.d.ts +20 -0
- package/lib/types/src/host/infra/transport/askpass.d.ts +133 -0
- package/lib/types/src/host/infra/transport/command-pool.d.ts +61 -0
- package/lib/types/src/host/infra/transport/fs-commands.d.ts +140 -0
- package/lib/types/src/host/infra/transport/git-status-route.d.ts +63 -0
- package/lib/types/src/host/infra/transport/git-status.d.ts +101 -0
- package/lib/types/src/host/infra/transport/node-spawn.d.ts +26 -0
- package/lib/types/src/host/infra/transport/session-window.d.ts +51 -0
- package/lib/types/src/host/infra/transport/shell-quote.d.ts +59 -0
- package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +206 -0
- package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +20 -0
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +133 -0
- package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +62 -0
- package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +43 -0
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +328 -0
- package/lib/types/src/host/infra/transport/ssh-argv.d.ts +43 -0
- package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +302 -0
- package/lib/types/src/host/infra/transport/transport-base.d.ts +34 -0
- package/lib/types/src/host/infra/transport/transport.d.ts +527 -0
- package/lib/types/src/host/integration/agent/agent-install-io.d.ts +33 -0
- package/lib/types/src/host/integration/agent/agent-preflight.d.ts +56 -0
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +66 -0
- package/lib/types/src/host/integration/agent-install-io.d.ts +33 -0
- package/lib/types/src/host/integration/agent-preflight.d.ts +56 -0
- package/lib/types/src/host/integration/anchor-directory.d.ts +56 -0
- package/lib/types/src/host/integration/askpass-registry.d.ts +59 -0
- package/lib/types/src/host/integration/bin-root-resolver.d.ts +81 -0
- package/lib/types/src/host/integration/connection-test.d.ts +73 -0
- package/lib/types/src/host/integration/file-reference-provider.d.ts +115 -0
- package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +142 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +368 -0
- package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +21 -0
- package/lib/types/src/host/integration/fs/platform-errors.d.ts +94 -0
- package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +369 -0
- package/lib/types/src/host/integration/fs/remote-index.d.ts +177 -0
- package/lib/types/src/host/integration/fs-provider.d.ts +226 -0
- package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +38 -0
- package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +67 -0
- package/lib/types/src/host/integration/lsp/lsp-session.d.ts +130 -0
- package/lib/types/src/host/integration/management-tools.d.ts +64 -0
- package/lib/types/src/host/integration/recycle-bin-service.d.ts +247 -0
- package/lib/types/src/host/integration/remote/connection-test.d.ts +73 -0
- package/lib/types/src/host/integration/remote/remote-face.d.ts +127 -0
- package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +108 -0
- package/lib/types/src/host/integration/remote-face.d.ts +127 -0
- package/lib/types/src/host/integration/remote-file-tools.d.ts +241 -0
- package/lib/types/src/host/integration/remote-home.d.ts +69 -0
- package/lib/types/src/host/integration/remote-index.d.ts +165 -0
- package/lib/types/src/host/integration/remote-job-tools.d.ts +295 -0
- package/lib/types/src/host/integration/remote-tool-backend.d.ts +97 -0
- package/lib/types/src/host/integration/remote-tools.d.ts +206 -0
- package/lib/types/src/host/integration/root-registry.d.ts +115 -0
- package/lib/types/src/host/integration/roots/anchor-directory.d.ts +56 -0
- package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +81 -0
- package/lib/types/src/host/integration/roots/remote-home.d.ts +69 -0
- package/lib/types/src/host/integration/roots/root-registry.d.ts +144 -0
- package/lib/types/src/host/integration/roots/roots-wiring.d.ts +219 -0
- package/lib/types/src/host/integration/roots/session-root-index.d.ts +52 -0
- package/lib/types/src/host/integration/roots-api.d.ts +407 -0
- package/lib/types/src/host/integration/roots-wiring.d.ts +211 -0
- package/lib/types/src/host/integration/session/session-api.d.ts +88 -0
- package/lib/types/src/host/integration/session/session-bin-lock.d.ts +80 -0
- package/lib/types/src/host/integration/session/session-header.d.ts +151 -0
- package/lib/types/src/host/integration/session/session-labels.d.ts +56 -0
- package/lib/types/src/host/integration/session/session-route-wiring.d.ts +50 -0
- package/lib/types/src/host/integration/session-admin-service.d.ts +236 -0
- package/lib/types/src/host/integration/session-api.d.ts +81 -0
- package/lib/types/src/host/integration/session-bin-lock.d.ts +80 -0
- package/lib/types/src/host/integration/session-header.d.ts +118 -0
- package/lib/types/src/host/integration/session-labels.d.ts +56 -0
- package/lib/types/src/host/integration/session-route-wiring.d.ts +50 -0
- package/lib/types/src/host/integration/session-scope.d.ts +113 -0
- package/lib/types/src/host/integration/session-tools.d.ts +44 -0
- package/lib/types/src/host/integration/strict-arguments.d.ts +79 -0
- package/lib/types/src/host/integration/takeover/takeover-api.d.ts +62 -0
- package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +60 -0
- package/lib/types/src/host/integration/takeover/takeover-service.d.ts +167 -0
- package/lib/types/src/host/integration/terminal-manifest.d.ts +94 -0
- package/lib/types/src/host/integration/tools/management-tools.d.ts +245 -0
- package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +316 -0
- package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +43 -0
- package/lib/types/src/host/integration/tools/remote-tools.d.ts +236 -0
- package/lib/types/src/host/integration/tools/session-target.d.ts +52 -0
- package/lib/types/src/host/integration/tools/session-tools.d.ts +46 -0
- package/lib/types/src/host/integration/tools/strict-arguments.d.ts +79 -0
- package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +94 -0
- package/lib/types/src/host/integration/transfer/remote-copy.d.ts +66 -0
- package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +48 -0
- package/lib/types/src/host/integration/transfer/transfer-service.d.ts +212 -0
- package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +106 -0
- package/lib/types/src/host/integration/transfer-adapters.d.ts +44 -0
- package/lib/types/src/host/integration/transfer-service.d.ts +164 -0
- package/lib/types/src/host/integration/transfer-tracker.d.ts +106 -0
- package/lib/types/src/host/integration/user-roots.d.ts +461 -0
- package/lib/types/src/index.d.ts +66 -0
- package/lib/types/src/local-bridge-refs.d.ts +26 -0
- package/lib/types/src/local-bridge.d.ts +36 -0
- package/lib/types/src/shared/format.d.ts +13 -0
- package/lib/types/src/wiring/connections.d.ts +189 -0
- package/lib/types/src/wiring/fs.d.ts +13 -0
- package/lib/types/src/wiring/helpers.d.ts +144 -0
- package/lib/types/src/wiring/install-memo.d.ts +32 -0
- package/lib/types/src/wiring/legacy-dirs.d.ts +62 -0
- package/lib/types/src/wiring/package-paths.d.ts +35 -0
- package/lib/types/src/wiring/roots.d.ts +21 -0
- package/lib/types/src/wiring/state-dir.d.ts +29 -0
- package/lib/types/src/wiring/tools.d.ts +35 -0
- package/package.json +120 -4
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话管理的**契约**:依赖注入形状、会话头、删除/恢复/清理结果与服务接口。
|
|
3
|
+
*
|
|
4
|
+
* 从 `session-admin-service.ts` 抽出(审计 0076):上下文模块需要这些类型,留在 service 里会形成环。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-over-ssh/app/session-admin-contract
|
|
7
|
+
*/
|
|
8
|
+
import { type DeleteDecision, type SessionRecord, type SessionTrashEntry, type SessionTrashEntryId } from '../../domain/session/session-admin.ts';
|
|
9
|
+
export interface SessionDirEntry {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly isDirectory: boolean;
|
|
12
|
+
readonly bytes: number;
|
|
13
|
+
readonly mtimeMs: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Everything the service needs from the outside world.
|
|
17
|
+
*
|
|
18
|
+
* `readHeader` is deliberately the **only** way to learn about a session: the directory name cannot be decoded
|
|
19
|
+
* (see ADR-0009), so classification must come from the header.
|
|
20
|
+
*/
|
|
21
|
+
export interface SessionAdminDeps {
|
|
22
|
+
/** The platform's sessions root (e.g. `~/.dsh/sessions`). */
|
|
23
|
+
readonly sessionsRoot: string;
|
|
24
|
+
/** The plugin's own bin root; must **not** be inside {@link sessionsRoot}. */
|
|
25
|
+
readonly binRoot: string;
|
|
26
|
+
/** List one directory's entries; a missing directory yields an empty list, not a failure. */
|
|
27
|
+
readonly listDir: (path: string) => Promise<readonly SessionDirEntry[]>;
|
|
28
|
+
/** Read a session header. `undefined` means the log is unreadable (corrupt/unsupported). */
|
|
29
|
+
readonly readHeader: (sessionDir: string) => Promise<SessionHeader | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Display title of the workspace that owns a session directory, or `undefined` when none does.
|
|
32
|
+
*
|
|
33
|
+
* Injected because the mapping lives in the platform's workspace registry, which the domain layer must not
|
|
34
|
+
* import (see the repository's red lines). A listing without this resolver still answers: every row keeps
|
|
35
|
+
* its `cwd`, so a reader falls back to the directory's own leaf name.
|
|
36
|
+
*/
|
|
37
|
+
readonly resolveWorkspaceTitle?: ((cwd: string | undefined) => string | undefined) | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The session's generated title, or `undefined` when it has none.
|
|
40
|
+
*
|
|
41
|
+
* The header carries no title (it is a projection, not header metadata), so this is the only way a listing
|
|
42
|
+
* can label a session by name. Injected for the same reason as {@link resolveWorkspaceTitle}; without it
|
|
43
|
+
* every row falls back to its id.
|
|
44
|
+
*/
|
|
45
|
+
readonly resolveSessionTitle?: ((header: SessionHeader) => string | undefined) | undefined;
|
|
46
|
+
/** `stat`; `undefined` when the path does not exist. */
|
|
47
|
+
readonly stat: (path: string) => Promise<{
|
|
48
|
+
readonly isDirectory: boolean;
|
|
49
|
+
readonly bytes: number;
|
|
50
|
+
} | undefined>;
|
|
51
|
+
/** Read a text file. */
|
|
52
|
+
readonly readText: (path: string) => Promise<string>;
|
|
53
|
+
/** Write a text file (UTF-8, no BOM). */
|
|
54
|
+
readonly writeText: (path: string, text: string) => Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Replace a text file **atomically** (write a temp file, then rename over the target).
|
|
57
|
+
*
|
|
58
|
+
* Optional, and worth supplying: without it a process killed mid-write leaves a truncated JSON index,
|
|
59
|
+
* which the parser then rejects as a hard failure. Atomic replace makes the index either the old or the
|
|
60
|
+
* new document, never a fragment.
|
|
61
|
+
*/
|
|
62
|
+
readonly writeTextAtomic?: (path: string, text: string) => Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Run `operation` while holding a **cross-process** lock for `key`.
|
|
65
|
+
*
|
|
66
|
+
* The in-process chain cannot see another DSH process; without this, two processes deleting at the same
|
|
67
|
+
* time can each read the old index and overwrite the other's entry — leaving bytes in the bin that no
|
|
68
|
+
* index mentions (invisible and unrestorable).
|
|
69
|
+
*
|
|
70
|
+
* A caller-supplied implementation must be **exclusive** and must not deadlock when the previous holder
|
|
71
|
+
* died (see `createSessionBinLock`, which reclaims a stale lock by age).
|
|
72
|
+
*/
|
|
73
|
+
readonly withLock?: <T>(key: string, operation: () => Promise<T>) => Promise<T>;
|
|
74
|
+
/**
|
|
75
|
+
* Move a directory. Must be atomic on one filesystem; the caller supplies the cross-device fallback.
|
|
76
|
+
*
|
|
77
|
+
* Returning `'copy'` means the implementation copied (cross-device) and the source is already gone.
|
|
78
|
+
*/
|
|
79
|
+
readonly moveDir: (from: string, to: string) => Promise<'rename' | 'copy'>;
|
|
80
|
+
/** Recursively remove a directory (used for purge and for rolling back a failed record). */
|
|
81
|
+
readonly removeDir: (path: string) => Promise<void>;
|
|
82
|
+
/** Copy a directory recursively (the cross-device fallback, when the deps implement it themselves). */
|
|
83
|
+
readonly copyDir?: (from: string, to: string) => Promise<void>;
|
|
84
|
+
/** Whether the host currently holds this session in its store (it was opened in this process). */
|
|
85
|
+
readonly isLive: (sessionId: string) => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Whether this session's agent is running a turn right now.
|
|
88
|
+
*
|
|
89
|
+
* The platform's own "运行中" signal (`agents.get(id)?.status === 'running'`, the same one the session
|
|
90
|
+
* list uses for its status dot). This — not {@link isLive} — is what a deletion refuses (FR-3.6).
|
|
91
|
+
*/
|
|
92
|
+
readonly isRunning: (sessionId: string) => boolean;
|
|
93
|
+
/** Remove a session from workspace bookkeeping (`detachSession`); optional where unavailable. */
|
|
94
|
+
readonly detachSession?: (sessionId: string) => Promise<void>;
|
|
95
|
+
/** Announce the removal so clients drop the row (`api-session/removed`). */
|
|
96
|
+
readonly emitRemoved?: (sessionId: string) => void;
|
|
97
|
+
/** Injected clock. */
|
|
98
|
+
readonly now: () => number;
|
|
99
|
+
/** Who confirmed, for the audit trail (FR-3.10); absent when the caller did not say. */
|
|
100
|
+
readonly confirmedBy?: string;
|
|
101
|
+
}
|
|
102
|
+
/** The parts of a session header this feature needs. */
|
|
103
|
+
export interface SessionHeader {
|
|
104
|
+
readonly id: string;
|
|
105
|
+
/** `cwd` from the header; `undefined` for the platform's no-cwd bucket. */
|
|
106
|
+
readonly cwd: string | undefined;
|
|
107
|
+
readonly title: string | undefined;
|
|
108
|
+
readonly parentSessionId: string | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* Session format generation, as stored in the log's first line.
|
|
111
|
+
*
|
|
112
|
+
* Together with {@link createdAt} and {@link isSeeded} this is the **lifecycle identity** a header alone
|
|
113
|
+
* witnesses, and it is what the platform's projection cache validates a stored title row against. A
|
|
114
|
+
* resolver that skipped it could serve a title from a different lifecycle that reused the same id.
|
|
115
|
+
*/
|
|
116
|
+
readonly version?: number | undefined;
|
|
117
|
+
readonly createdAt?: number | undefined;
|
|
118
|
+
readonly isSeeded?: boolean | undefined;
|
|
119
|
+
}
|
|
120
|
+
/** Options shared by the destructive calls. */
|
|
121
|
+
export interface SessionAdminOptions {
|
|
122
|
+
readonly signal?: AbortSignal;
|
|
123
|
+
}
|
|
124
|
+
/** Result of a session deletion. */
|
|
125
|
+
export interface DeleteSessionResult {
|
|
126
|
+
readonly mode: 'recycle' | 'permanent';
|
|
127
|
+
/** The bin entry in `recycle` mode; `undefined` for `permanent`. */
|
|
128
|
+
readonly entry: SessionTrashEntry | undefined;
|
|
129
|
+
/** Whether the content can still be recovered. **Always `false` for `permanent`.** */
|
|
130
|
+
readonly recoverable: boolean;
|
|
131
|
+
/** True when the move was observed to have landed (store gone + bin present). */
|
|
132
|
+
readonly verified: boolean;
|
|
133
|
+
/** True when the bytes crossed a filesystem boundary and were copied. */
|
|
134
|
+
readonly copied: boolean;
|
|
135
|
+
/** Ids that were refused, with the reason (a partial deletion is reported, never hidden). */
|
|
136
|
+
readonly refusals: DeleteDecision['refusals'];
|
|
137
|
+
readonly bytes: number;
|
|
138
|
+
}
|
|
139
|
+
/** Result of a restore. */
|
|
140
|
+
export interface RestoreSessionResult {
|
|
141
|
+
readonly entry: SessionTrashEntry;
|
|
142
|
+
/** Absolute path the session directory was moved back to. */
|
|
143
|
+
readonly restoredPath: string;
|
|
144
|
+
readonly movedBy: 'rename' | 'copy';
|
|
145
|
+
}
|
|
146
|
+
/** One purge outcome. */
|
|
147
|
+
export interface PurgeSessionResult {
|
|
148
|
+
readonly id: SessionTrashEntryId;
|
|
149
|
+
readonly sessionId: string;
|
|
150
|
+
readonly purged: boolean;
|
|
151
|
+
/** True when the entry was removed physically but the index could not be updated afterwards. */
|
|
152
|
+
readonly indexStale: boolean;
|
|
153
|
+
}
|
|
154
|
+
/** A retention sweep outcome for the session bin. */
|
|
155
|
+
export interface SweepSessionResult {
|
|
156
|
+
readonly purged: readonly PurgeSessionResult[];
|
|
157
|
+
readonly failed: readonly {
|
|
158
|
+
readonly id: SessionTrashEntryId;
|
|
159
|
+
readonly reason: string;
|
|
160
|
+
}[];
|
|
161
|
+
readonly remainingEntries: number;
|
|
162
|
+
readonly remainingBytes: number;
|
|
163
|
+
}
|
|
164
|
+
/** The session-bin retention policy (same three dimensions as the file-level bin). */
|
|
165
|
+
export interface SessionBinPolicy {
|
|
166
|
+
readonly maxEntries: number;
|
|
167
|
+
readonly maxBytes: number;
|
|
168
|
+
readonly maxAgeMs: number;
|
|
169
|
+
}
|
|
170
|
+
export declare function joinPath(base: string, ...segments: readonly string[]): string;
|
|
171
|
+
/** The service surface. */
|
|
172
|
+
export interface SessionAdminService {
|
|
173
|
+
/** Inventory every session in the store (FR-3.3). */
|
|
174
|
+
inventory(options?: SessionAdminOptions): Promise<readonly SessionRecord[]>;
|
|
175
|
+
/** Preview a deletion without touching anything (FR-3.7). */
|
|
176
|
+
previewDelete(ids: readonly string[], options?: SessionAdminOptions & {
|
|
177
|
+
readonly keep?: readonly string[];
|
|
178
|
+
}): Promise<DeleteDecision>;
|
|
179
|
+
/** Delete sessions: into the bin (`recycle`) or irreversibly (`permanent`). */
|
|
180
|
+
deleteSession(ids: readonly string[], options?: SessionAdminOptions & {
|
|
181
|
+
readonly mode?: 'recycle' | 'permanent';
|
|
182
|
+
readonly keep?: readonly string[];
|
|
183
|
+
}): Promise<DeleteSessionResult>;
|
|
184
|
+
/**
|
|
185
|
+
* Delete every session that belongs to one workspace directory, and nothing else.
|
|
186
|
+
*
|
|
187
|
+
* The workspace itself is **left registered**: the user asked to clear the workspace's sessions, not to
|
|
188
|
+
* unregister the workspace (and the platform's `workspaceRegistry.delete` would also be a different,
|
|
189
|
+
* separately-confirmed action). Sessions are matched by their header's `cwd` against the given directory,
|
|
190
|
+
* folded the same way the label resolver folds paths, so a Windows case/separator difference still matches.
|
|
191
|
+
*
|
|
192
|
+
* Live sessions are refused by the same rules as {@link deleteSession} (they appear in `refusals`), so a
|
|
193
|
+
* workspace containing a running session is deleted **except** for that session rather than failing whole.
|
|
194
|
+
*
|
|
195
|
+
* @param cwd - the workspace directory whose sessions should go.
|
|
196
|
+
* @param options - deletion mode plus cancellation.
|
|
197
|
+
* @returns the ids deleted, the per-id refusals, and the bytes freed.
|
|
198
|
+
*/
|
|
199
|
+
deleteWorkspaceSessions(cwd: string, options?: SessionAdminOptions & {
|
|
200
|
+
readonly mode?: 'recycle' | 'permanent';
|
|
201
|
+
}): Promise<DeleteSessionResult & {
|
|
202
|
+
readonly matched: number;
|
|
203
|
+
}>;
|
|
204
|
+
/** List the bin. */
|
|
205
|
+
list(options?: SessionAdminOptions): Promise<readonly SessionTrashEntry[]>;
|
|
206
|
+
/** Move one entry back to where it came from. */
|
|
207
|
+
restore(entryId: string, options?: SessionAdminOptions): Promise<RestoreSessionResult>;
|
|
208
|
+
/** Delete one entry physically (the irreversible step, after the retention period). */
|
|
209
|
+
purge(entryId: string, options?: SessionAdminOptions): Promise<PurgeSessionResult>;
|
|
210
|
+
/** Apply the retention policy. */
|
|
211
|
+
sweep(policy: SessionBinPolicy, options?: SessionAdminOptions): Promise<SweepSessionResult>;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Build the session-administration service.
|
|
215
|
+
*
|
|
216
|
+
* @param deps - injected I/O.
|
|
217
|
+
* @returns the service.
|
|
218
|
+
* @throws SshDevError `ROOT_INVALID` when the bin root is inside the sessions root (that placement breaks the
|
|
219
|
+
* platform's session listing — see the module note and ADR-0009).
|
|
220
|
+
*/
|
|
221
|
+
//# sourceMappingURL=session-admin-contract.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话管理的**删除族**:预览、物理删除、移入回收站、批量清理、按工作区删除。
|
|
3
|
+
*
|
|
4
|
+
* 从 `session-admin-service.ts` 的工厂里抽出(审计 0076):只依赖上下文,不依赖列表/恢复/清扫。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-over-ssh/app/session-admin-delete
|
|
7
|
+
*/
|
|
8
|
+
import { type SessionTrashEntry, type SessionTrashEntryId } from '../../domain/session/session-admin.ts';
|
|
9
|
+
import { type SessionAdminContext } from './session-admin-context.ts';
|
|
10
|
+
import type { PurgeSessionResult, DeleteSessionResult } from './session-admin-contract.ts';
|
|
11
|
+
import type { DeleteDecision } from '../../domain/session/session-admin.ts';
|
|
12
|
+
export declare function createSessionAdminDelete(ctx: SessionAdminContext): {
|
|
13
|
+
previewDelete: (ids: readonly string[], options?: import("./session-admin-contract.ts").SessionAdminOptions & {
|
|
14
|
+
readonly keep?: readonly string[];
|
|
15
|
+
}) => Promise<DeleteDecision>;
|
|
16
|
+
deleteSession: (ids: readonly string[], options?: import("./session-admin-contract.ts").SessionAdminOptions & {
|
|
17
|
+
readonly mode?: "recycle" | "permanent";
|
|
18
|
+
readonly keep?: readonly string[];
|
|
19
|
+
}) => Promise<DeleteSessionResult>;
|
|
20
|
+
deleteWorkspaceSessions: (cwd: string, options?: import("./session-admin-contract.ts").SessionAdminOptions & {
|
|
21
|
+
readonly mode?: "recycle" | "permanent";
|
|
22
|
+
}) => Promise<DeleteSessionResult & {
|
|
23
|
+
readonly matched: number;
|
|
24
|
+
}>;
|
|
25
|
+
purgeEntry: (entryId: string) => Promise<PurgeSessionResult>;
|
|
26
|
+
purgeEntries: (doomed: readonly SessionTrashEntry[], known: readonly SessionTrashEntry[]) => Promise<{
|
|
27
|
+
readonly purged: readonly PurgeSessionResult[];
|
|
28
|
+
readonly failed: readonly {
|
|
29
|
+
readonly id: SessionTrashEntryId;
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
}[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=session-admin-delete.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session administration service: inventory, deletion into the session bin, restore, purge, retention.
|
|
3
|
+
*
|
|
4
|
+
* The domain decisions live in [`domain/session-admin.ts`](../domain/session-admin.ts); this module wires them to
|
|
5
|
+
* injected I/O. Every filesystem and platform call arrives as a function (AGENTS.md 红线 3), so this module is
|
|
6
|
+
* testable offline and a test can make a move fail.
|
|
7
|
+
*
|
|
8
|
+
* ## Three contracts matter more than the rest
|
|
9
|
+
*
|
|
10
|
+
* 1. **Move first, confirm, then record.** `deleteSession` reports success only after the session directory is
|
|
11
|
+
* observed inside the bin **and** gone from the store. A move that throws is confirmed before anything is
|
|
12
|
+
* claimed: if it actually landed (a lost response), that counts as success; otherwise the call fails and
|
|
13
|
+
* **nothing** is recorded. "Deleted but not in the bin" must never be reported as success.
|
|
14
|
+
* 2. **The index write is never swallowed.** If the index cannot be written after a successful move, the move is
|
|
15
|
+
* rolled back and the failure is reported. A physical purge has no rollback, so that case is reported with
|
|
16
|
+
* `purged: true, indexStale: true` instead of being hidden.
|
|
17
|
+
* 3. **Running sessions are never touched.** The decision layer refuses them; this layer re-checks before
|
|
18
|
+
* moving, so a session that started running between preview and confirm is refused rather than killed.
|
|
19
|
+
* "Running" is the platform's `agents.get(id)?.status === 'running'`, not "loaded in the host": the latter
|
|
20
|
+
* is true for every session opened since the process started and would make almost nothing deletable.
|
|
21
|
+
*
|
|
22
|
+
* ## Where the bin lives (and why not under `sessions/`)
|
|
23
|
+
*
|
|
24
|
+
* The platform enumerates **every** directory under the sessions root (`listProjectDirs` has no dot-directory
|
|
25
|
+
* filter) and fails the whole listing when one session id appears twice. A bin inside `sessions/` that kept the
|
|
26
|
+
* original log filename would therefore make deleted sessions reappear *and* break the session list. The bin
|
|
27
|
+
* lives in the plugin's own storage instead — see ADR-0009.
|
|
28
|
+
*
|
|
29
|
+
* @module dsh-over-ssh/integration/session-admin-service
|
|
30
|
+
*/
|
|
31
|
+
import type { SessionAdminDeps, SessionAdminService } from './session-admin-contract.ts';
|
|
32
|
+
export * from './session-admin-contract.ts';
|
|
33
|
+
/**
|
|
34
|
+
* Build the service.
|
|
35
|
+
*
|
|
36
|
+
* @param deps - injected platform services.
|
|
37
|
+
* @returns the {@link SessionAdminService}.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createSessionAdminService(deps: SessionAdminDeps): SessionAdminService;
|
|
40
|
+
//# sourceMappingURL=session-admin-service.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话管理的**上下文**:bin 根、索引读写、跨进程串行化、会话清单。
|
|
3
|
+
*
|
|
4
|
+
* 从 `session-admin-service.ts` 的工厂里抽出(审计 0076):所有方法族的公共底座。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-remote-workspace/app/session-admin-context
|
|
7
|
+
*/
|
|
8
|
+
import { type SessionRecord, type SessionTrashEntry } from '../domain/session-admin.ts';
|
|
9
|
+
import type { SessionAdminDeps, SessionAdminOptions } from './session-admin-contract.ts';
|
|
10
|
+
/** Whether a thrown error means "the path does not exist". */
|
|
11
|
+
export declare function isMissing(error: unknown): boolean;
|
|
12
|
+
export declare function createSessionAdminContext(deps: SessionAdminDeps): {
|
|
13
|
+
deps: SessionAdminDeps;
|
|
14
|
+
sessionsRoot: string;
|
|
15
|
+
binRoot: string;
|
|
16
|
+
indexePath: () => string;
|
|
17
|
+
serialized: <T>(operation: () => Promise<T>) => Promise<T>;
|
|
18
|
+
entryDir: (segment: string) => string;
|
|
19
|
+
readIndex: () => Promise<readonly SessionTrashEntry[]>;
|
|
20
|
+
writeIndex: (entries: readonly SessionTrashEntry[]) => Promise<void>;
|
|
21
|
+
inventory: (options?: SessionAdminOptions) => Promise<readonly SessionRecord[]>;
|
|
22
|
+
};
|
|
23
|
+
export type SessionAdminContext = ReturnType<typeof createSessionAdminContext>;
|
|
24
|
+
/**
|
|
25
|
+
* Fold a path to a comparison key: forward slashes, no trailing separator, lower case.
|
|
26
|
+
*
|
|
27
|
+
* The same fold the label resolver uses (`session-labels.ts`), duplicated here because this module must not
|
|
28
|
+
* import the resolver: the resolver reads platform services, this one is pure. Windows paths are
|
|
29
|
+
* case-insensitive and a header's `cwd` is the raw spawn directory while a workspace record's path is
|
|
30
|
+
* canonicalized, so an exact compare would miss real matches.
|
|
31
|
+
*
|
|
32
|
+
* @param path - a path in either spelling.
|
|
33
|
+
* @returns the comparison key.
|
|
34
|
+
*/
|
|
35
|
+
export declare function normalizePathKey(path: string): string;
|
|
36
|
+
//# sourceMappingURL=session-admin-context.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话管理的**契约**:依赖注入形状、会话头、删除/恢复/清理结果与服务接口。
|
|
3
|
+
*
|
|
4
|
+
* 从 `session-admin-service.ts` 抽出(审计 0076):上下文模块需要这些类型,留在 service 里会形成环。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-remote-workspace/app/session-admin-contract
|
|
7
|
+
*/
|
|
8
|
+
import { type DeleteDecision, type SessionRecord, type SessionTrashEntry, type SessionTrashEntryId } from '../domain/session-admin.ts';
|
|
9
|
+
export interface SessionDirEntry {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly isDirectory: boolean;
|
|
12
|
+
readonly bytes: number;
|
|
13
|
+
readonly mtimeMs: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Everything the service needs from the outside world.
|
|
17
|
+
*
|
|
18
|
+
* `readHeader` is deliberately the **only** way to learn about a session: the directory name cannot be decoded
|
|
19
|
+
* (see ADR-0009), so classification must come from the header.
|
|
20
|
+
*/
|
|
21
|
+
export interface SessionAdminDeps {
|
|
22
|
+
/** The platform's sessions root (e.g. `~/.dsh/sessions`). */
|
|
23
|
+
readonly sessionsRoot: string;
|
|
24
|
+
/** The plugin's own bin root; must **not** be inside {@link sessionsRoot}. */
|
|
25
|
+
readonly binRoot: string;
|
|
26
|
+
/** List one directory's entries; a missing directory yields an empty list, not a failure. */
|
|
27
|
+
readonly listDir: (path: string) => Promise<readonly SessionDirEntry[]>;
|
|
28
|
+
/** Read a session header. `undefined` means the log is unreadable (corrupt/unsupported). */
|
|
29
|
+
readonly readHeader: (sessionDir: string) => Promise<SessionHeader | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Display title of the workspace that owns a session directory, or `undefined` when none does.
|
|
32
|
+
*
|
|
33
|
+
* Injected because the mapping lives in the platform's workspace registry, which the domain layer must not
|
|
34
|
+
* import (see the repository's red lines). A listing without this resolver still answers: every row keeps
|
|
35
|
+
* its `cwd`, so a reader falls back to the directory's own leaf name.
|
|
36
|
+
*/
|
|
37
|
+
readonly resolveWorkspaceTitle?: ((cwd: string | undefined) => string | undefined) | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The session's generated title, or `undefined` when it has none.
|
|
40
|
+
*
|
|
41
|
+
* The header carries no title (it is a projection, not header metadata), so this is the only way a listing
|
|
42
|
+
* can label a session by name. Injected for the same reason as {@link resolveWorkspaceTitle}; without it
|
|
43
|
+
* every row falls back to its id.
|
|
44
|
+
*/
|
|
45
|
+
readonly resolveSessionTitle?: ((header: SessionHeader) => string | undefined) | undefined;
|
|
46
|
+
/** `stat`; `undefined` when the path does not exist. */
|
|
47
|
+
readonly stat: (path: string) => Promise<{
|
|
48
|
+
readonly isDirectory: boolean;
|
|
49
|
+
readonly bytes: number;
|
|
50
|
+
} | undefined>;
|
|
51
|
+
/** Read a text file. */
|
|
52
|
+
readonly readText: (path: string) => Promise<string>;
|
|
53
|
+
/** Write a text file (UTF-8, no BOM). */
|
|
54
|
+
readonly writeText: (path: string, text: string) => Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Replace a text file **atomically** (write a temp file, then rename over the target).
|
|
57
|
+
*
|
|
58
|
+
* Optional, and worth supplying: without it a process killed mid-write leaves a truncated JSON index,
|
|
59
|
+
* which the parser then rejects as a hard failure. Atomic replace makes the index either the old or the
|
|
60
|
+
* new document, never a fragment.
|
|
61
|
+
*/
|
|
62
|
+
readonly writeTextAtomic?: (path: string, text: string) => Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Run `operation` while holding a **cross-process** lock for `key`.
|
|
65
|
+
*
|
|
66
|
+
* The in-process chain cannot see another DSH process; without this, two processes deleting at the same
|
|
67
|
+
* time can each read the old index and overwrite the other's entry — leaving bytes in the bin that no
|
|
68
|
+
* index mentions (invisible and unrestorable).
|
|
69
|
+
*
|
|
70
|
+
* A caller-supplied implementation must be **exclusive** and must not deadlock when the previous holder
|
|
71
|
+
* died (see `createSessionBinLock`, which reclaims a stale lock by age).
|
|
72
|
+
*/
|
|
73
|
+
readonly withLock?: <T>(key: string, operation: () => Promise<T>) => Promise<T>;
|
|
74
|
+
/**
|
|
75
|
+
* Move a directory. Must be atomic on one filesystem; the caller supplies the cross-device fallback.
|
|
76
|
+
*
|
|
77
|
+
* Returning `'copy'` means the implementation copied (cross-device) and the source is already gone.
|
|
78
|
+
*/
|
|
79
|
+
readonly moveDir: (from: string, to: string) => Promise<'rename' | 'copy'>;
|
|
80
|
+
/** Recursively remove a directory (used for purge and for rolling back a failed record). */
|
|
81
|
+
readonly removeDir: (path: string) => Promise<void>;
|
|
82
|
+
/** Copy a directory recursively (the cross-device fallback, when the deps implement it themselves). */
|
|
83
|
+
readonly copyDir?: (from: string, to: string) => Promise<void>;
|
|
84
|
+
/** Whether a session is currently held by a live agent; never deleted. */
|
|
85
|
+
readonly isLive: (sessionId: string) => boolean;
|
|
86
|
+
/** Remove a session from workspace bookkeeping (`detachSession`); optional where unavailable. */
|
|
87
|
+
readonly detachSession?: (sessionId: string) => Promise<void>;
|
|
88
|
+
/** Announce the removal so clients drop the row (`api-session/removed`). */
|
|
89
|
+
readonly emitRemoved?: (sessionId: string) => void;
|
|
90
|
+
/** Injected clock. */
|
|
91
|
+
readonly now: () => number;
|
|
92
|
+
/** Who confirmed, for the audit trail (FR-3.10); absent when the caller did not say. */
|
|
93
|
+
readonly confirmedBy?: string;
|
|
94
|
+
}
|
|
95
|
+
/** The parts of a session header this feature needs. */
|
|
96
|
+
export interface SessionHeader {
|
|
97
|
+
readonly id: string;
|
|
98
|
+
/** `cwd` from the header; `undefined` for the platform's no-cwd bucket. */
|
|
99
|
+
readonly cwd: string | undefined;
|
|
100
|
+
readonly title: string | undefined;
|
|
101
|
+
readonly parentSessionId: string | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Session format generation, as stored in the log's first line.
|
|
104
|
+
*
|
|
105
|
+
* Together with {@link createdAt} and {@link isSeeded} this is the **lifecycle identity** a header alone
|
|
106
|
+
* witnesses, and it is what the platform's projection cache validates a stored title row against. A
|
|
107
|
+
* resolver that skipped it could serve a title from a different lifecycle that reused the same id.
|
|
108
|
+
*/
|
|
109
|
+
readonly version?: number | undefined;
|
|
110
|
+
readonly createdAt?: number | undefined;
|
|
111
|
+
readonly isSeeded?: boolean | undefined;
|
|
112
|
+
}
|
|
113
|
+
/** Options shared by the destructive calls. */
|
|
114
|
+
export interface SessionAdminOptions {
|
|
115
|
+
readonly signal?: AbortSignal;
|
|
116
|
+
}
|
|
117
|
+
/** Result of a session deletion. */
|
|
118
|
+
export interface DeleteSessionResult {
|
|
119
|
+
readonly mode: 'recycle' | 'permanent';
|
|
120
|
+
/** The bin entry in `recycle` mode; `undefined` for `permanent`. */
|
|
121
|
+
readonly entry: SessionTrashEntry | undefined;
|
|
122
|
+
/** Whether the content can still be recovered. **Always `false` for `permanent`.** */
|
|
123
|
+
readonly recoverable: boolean;
|
|
124
|
+
/** True when the move was observed to have landed (store gone + bin present). */
|
|
125
|
+
readonly verified: boolean;
|
|
126
|
+
/** True when the bytes crossed a filesystem boundary and were copied. */
|
|
127
|
+
readonly copied: boolean;
|
|
128
|
+
/** Ids that were refused, with the reason (a partial deletion is reported, never hidden). */
|
|
129
|
+
readonly refusals: DeleteDecision['refusals'];
|
|
130
|
+
readonly bytes: number;
|
|
131
|
+
}
|
|
132
|
+
/** Result of a restore. */
|
|
133
|
+
export interface RestoreSessionResult {
|
|
134
|
+
readonly entry: SessionTrashEntry;
|
|
135
|
+
/** Absolute path the session directory was moved back to. */
|
|
136
|
+
readonly restoredPath: string;
|
|
137
|
+
readonly movedBy: 'rename' | 'copy';
|
|
138
|
+
}
|
|
139
|
+
/** One purge outcome. */
|
|
140
|
+
export interface PurgeSessionResult {
|
|
141
|
+
readonly id: SessionTrashEntryId;
|
|
142
|
+
readonly sessionId: string;
|
|
143
|
+
readonly purged: boolean;
|
|
144
|
+
/** True when the entry was removed physically but the index could not be updated afterwards. */
|
|
145
|
+
readonly indexStale: boolean;
|
|
146
|
+
}
|
|
147
|
+
/** A retention sweep outcome for the session bin. */
|
|
148
|
+
export interface SweepSessionResult {
|
|
149
|
+
readonly purged: readonly PurgeSessionResult[];
|
|
150
|
+
readonly failed: readonly {
|
|
151
|
+
readonly id: SessionTrashEntryId;
|
|
152
|
+
readonly reason: string;
|
|
153
|
+
}[];
|
|
154
|
+
readonly remainingEntries: number;
|
|
155
|
+
readonly remainingBytes: number;
|
|
156
|
+
}
|
|
157
|
+
/** The session-bin retention policy (same three dimensions as the file-level bin). */
|
|
158
|
+
export interface SessionBinPolicy {
|
|
159
|
+
readonly maxEntries: number;
|
|
160
|
+
readonly maxBytes: number;
|
|
161
|
+
readonly maxAgeMs: number;
|
|
162
|
+
}
|
|
163
|
+
export declare function joinPath(base: string, ...segments: readonly string[]): string;
|
|
164
|
+
/** The service surface. */
|
|
165
|
+
export interface SessionAdminService {
|
|
166
|
+
/** Inventory every session in the store (FR-3.3). */
|
|
167
|
+
inventory(options?: SessionAdminOptions): Promise<readonly SessionRecord[]>;
|
|
168
|
+
/** Preview a deletion without touching anything (FR-3.7). */
|
|
169
|
+
previewDelete(ids: readonly string[], options?: SessionAdminOptions & {
|
|
170
|
+
readonly keep?: readonly string[];
|
|
171
|
+
}): Promise<DeleteDecision>;
|
|
172
|
+
/** Delete sessions: into the bin (`recycle`) or irreversibly (`permanent`). */
|
|
173
|
+
deleteSession(ids: readonly string[], options?: SessionAdminOptions & {
|
|
174
|
+
readonly mode?: 'recycle' | 'permanent';
|
|
175
|
+
readonly keep?: readonly string[];
|
|
176
|
+
}): Promise<DeleteSessionResult>;
|
|
177
|
+
/**
|
|
178
|
+
* Delete every session that belongs to one workspace directory, and nothing else.
|
|
179
|
+
*
|
|
180
|
+
* The workspace itself is **left registered**: the user asked to clear the workspace's sessions, not to
|
|
181
|
+
* unregister the workspace (and the platform's `workspaceRegistry.delete` would also be a different,
|
|
182
|
+
* separately-confirmed action). Sessions are matched by their header's `cwd` against the given directory,
|
|
183
|
+
* folded the same way the label resolver folds paths, so a Windows case/separator difference still matches.
|
|
184
|
+
*
|
|
185
|
+
* Live sessions are refused by the same rules as {@link deleteSession} (they appear in `refusals`), so a
|
|
186
|
+
* workspace containing a running session is deleted **except** for that session rather than failing whole.
|
|
187
|
+
*
|
|
188
|
+
* @param cwd - the workspace directory whose sessions should go.
|
|
189
|
+
* @param options - deletion mode plus cancellation.
|
|
190
|
+
* @returns the ids deleted, the per-id refusals, and the bytes freed.
|
|
191
|
+
*/
|
|
192
|
+
deleteWorkspaceSessions(cwd: string, options?: SessionAdminOptions & {
|
|
193
|
+
readonly mode?: 'recycle' | 'permanent';
|
|
194
|
+
}): Promise<DeleteSessionResult & {
|
|
195
|
+
readonly matched: number;
|
|
196
|
+
}>;
|
|
197
|
+
/** List the bin. */
|
|
198
|
+
list(options?: SessionAdminOptions): Promise<readonly SessionTrashEntry[]>;
|
|
199
|
+
/** Move one entry back to where it came from. */
|
|
200
|
+
restore(entryId: string, options?: SessionAdminOptions): Promise<RestoreSessionResult>;
|
|
201
|
+
/** Delete one entry physically (the irreversible step, after the retention period). */
|
|
202
|
+
purge(entryId: string, options?: SessionAdminOptions): Promise<PurgeSessionResult>;
|
|
203
|
+
/** Apply the retention policy. */
|
|
204
|
+
sweep(policy: SessionBinPolicy, options?: SessionAdminOptions): Promise<SweepSessionResult>;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Build the session-administration service.
|
|
208
|
+
*
|
|
209
|
+
* @param deps - injected I/O.
|
|
210
|
+
* @returns the service.
|
|
211
|
+
* @throws SshDevError `ROOT_INVALID` when the bin root is inside the sessions root (that placement breaks the
|
|
212
|
+
* platform's session listing — see the module note and ADR-0009).
|
|
213
|
+
*/
|
|
214
|
+
//# sourceMappingURL=session-admin-contract.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话管理的**删除族**:预览、物理删除、移入回收站、批量清理、按工作区删除。
|
|
3
|
+
*
|
|
4
|
+
* 从 `session-admin-service.ts` 的工厂里抽出(审计 0076):只依赖上下文,不依赖列表/恢复/清扫。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-remote-workspace/app/session-admin-delete
|
|
7
|
+
*/
|
|
8
|
+
import { type SessionTrashEntry, type SessionTrashEntryId } from '../domain/session-admin.ts';
|
|
9
|
+
import { type SessionAdminContext } from './session-admin-context.ts';
|
|
10
|
+
import type { PurgeSessionResult, DeleteSessionResult } from './session-admin-contract.ts';
|
|
11
|
+
import type { DeleteDecision } from '../domain/session-admin.ts';
|
|
12
|
+
export declare function createSessionAdminDelete(ctx: SessionAdminContext): {
|
|
13
|
+
previewDelete: (ids: readonly string[], options?: import("./session-admin-contract.ts").SessionAdminOptions & {
|
|
14
|
+
readonly keep?: readonly string[];
|
|
15
|
+
}) => Promise<DeleteDecision>;
|
|
16
|
+
deleteSession: (ids: readonly string[], options?: import("./session-admin-contract.ts").SessionAdminOptions & {
|
|
17
|
+
readonly mode?: "recycle" | "permanent";
|
|
18
|
+
readonly keep?: readonly string[];
|
|
19
|
+
}) => Promise<DeleteSessionResult>;
|
|
20
|
+
deleteWorkspaceSessions: (cwd: string, options?: import("./session-admin-contract.ts").SessionAdminOptions & {
|
|
21
|
+
readonly mode?: "recycle" | "permanent";
|
|
22
|
+
}) => Promise<DeleteSessionResult & {
|
|
23
|
+
readonly matched: number;
|
|
24
|
+
}>;
|
|
25
|
+
purgeEntry: (entryId: string) => Promise<PurgeSessionResult>;
|
|
26
|
+
purgeEntries: (doomed: readonly SessionTrashEntry[], known: readonly SessionTrashEntry[]) => Promise<{
|
|
27
|
+
readonly purged: readonly PurgeSessionResult[];
|
|
28
|
+
readonly failed: readonly {
|
|
29
|
+
readonly id: SessionTrashEntryId;
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
}[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=session-admin-delete.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session administration service: inventory, deletion into the session bin, restore, purge, retention.
|
|
3
|
+
*
|
|
4
|
+
* The domain decisions live in [`domain/session-admin.ts`](../domain/session-admin.ts); this module wires them to
|
|
5
|
+
* injected I/O. Every filesystem and platform call arrives as a function (AGENTS.md 红线 3), so this module is
|
|
6
|
+
* testable offline and a test can make a move fail.
|
|
7
|
+
*
|
|
8
|
+
* ## Three contracts matter more than the rest
|
|
9
|
+
*
|
|
10
|
+
* 1. **Move first, confirm, then record.** `deleteSession` reports success only after the session directory is
|
|
11
|
+
* observed inside the bin **and** gone from the store. A move that throws is confirmed before anything is
|
|
12
|
+
* claimed: if it actually landed (a lost response), that counts as success; otherwise the call fails and
|
|
13
|
+
* **nothing** is recorded. "Deleted but not in the bin" must never be reported as success.
|
|
14
|
+
* 2. **The index write is never swallowed.** If the index cannot be written after a successful move, the move is
|
|
15
|
+
* rolled back and the failure is reported. A physical purge has no rollback, so that case is reported with
|
|
16
|
+
* `purged: true, indexStale: true` instead of being hidden.
|
|
17
|
+
* 3. **Live sessions are never touched.** The decision layer refuses them; this layer re-checks before moving, so
|
|
18
|
+
* a session that went live between preview and confirm is refused rather than killed.
|
|
19
|
+
*
|
|
20
|
+
* ## Where the bin lives (and why not under `sessions/`)
|
|
21
|
+
*
|
|
22
|
+
* The platform enumerates **every** directory under the sessions root (`listProjectDirs` has no dot-directory
|
|
23
|
+
* filter) and fails the whole listing when one session id appears twice. A bin inside `sessions/` that kept the
|
|
24
|
+
* original log filename would therefore make deleted sessions reappear *and* break the session list. The bin
|
|
25
|
+
* lives in the plugin's own storage instead — see ADR-0009.
|
|
26
|
+
*
|
|
27
|
+
* @module dsh-remote-workspace/integration/session-admin-service
|
|
28
|
+
*/
|
|
29
|
+
import type { SessionAdminDeps, SessionAdminService } from './session-admin-contract.ts';
|
|
30
|
+
export * from './session-admin-contract.ts';
|
|
31
|
+
/**
|
|
32
|
+
* Build the service.
|
|
33
|
+
*
|
|
34
|
+
* @param deps - injected platform services.
|
|
35
|
+
* @returns the {@link SessionAdminService}.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createSessionAdminService(deps: SessionAdminDeps): SessionAdminService;
|
|
38
|
+
//# sourceMappingURL=session-admin-service.d.ts.map
|