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,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回收站服务的**契约**:依赖注入形状、目标/结果类型、索引状态。
|
|
3
|
+
*
|
|
4
|
+
* 从 `recycle-bin-service.ts` 抽出(审计 0076)。抽出的直接原因不是"文件太长",而是
|
|
5
|
+
* 世界原语模块需要这些类型:留在 service 里会形成 `world → service`(type)与
|
|
6
|
+
* `service → world`(value)的环,架构测试会红。契约独立后方向单一。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-over-ssh/app/recycle-bin-contract
|
|
9
|
+
*/
|
|
10
|
+
import type { BackendEntry, BackendStat } from '../backend.ts';
|
|
11
|
+
import { type RecycleBinEntry, type RecycleBinEntryId } from '../../domain/transfer/recycle-bin.ts';
|
|
12
|
+
import { type RootRef } from '../../domain/roots/root.ts';
|
|
13
|
+
export interface RecycleBinRemovalOptions {
|
|
14
|
+
/** Whether the target is a directory that must be removed with its content. */
|
|
15
|
+
readonly recursive: boolean;
|
|
16
|
+
/** Caller cancellation. */
|
|
17
|
+
readonly signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
/** The mutating primitives of one execution world, shaped like the remote group. */
|
|
20
|
+
/**
|
|
21
|
+
* Serialize every read-modify-write of the bin index.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this is a seam and not a mutex inside the service
|
|
24
|
+
*
|
|
25
|
+
* Two separate processes can drive the same bin: the plugin runs inside DSH, but the user may also have a
|
|
26
|
+
* second DSH (another port, a headless run) pointed at the same state directory. An in-process promise chain
|
|
27
|
+
* would serialize one process and still let the other interleave — that is exactly the hole
|
|
28
|
+
* `session-bin-lock.ts` was written to close for the **session** bin, and this bin has the same shape.
|
|
29
|
+
*
|
|
30
|
+
* The implementation is a cross-process file lock (see `createSessionBinLock`). Keeping it behind this seam
|
|
31
|
+
* means the domain service never imports `node:fs`, and tests can inject a no-op or a recording stub.
|
|
32
|
+
*
|
|
33
|
+
* @param key - the root being mutated; implementations may use it to pick a per-root lock file.
|
|
34
|
+
* @param operation - the critical section; must perform its own read **and** write inside it.
|
|
35
|
+
*/
|
|
36
|
+
export type WithIndexLock = <T>(key: string, operation: () => Promise<T>) => Promise<T>;
|
|
37
|
+
export interface RecycleBinWorldIo {
|
|
38
|
+
/**
|
|
39
|
+
* Move (rename) a path.
|
|
40
|
+
*
|
|
41
|
+
* Contract: the implementation must create a missing destination parent directory (equivalent to
|
|
42
|
+
* `mkdir -p` + `rename`). A cross-device move that cannot be done by rename must **fail loudly**; silently
|
|
43
|
+
* degrading to copy-then-delete would break the "don't delete until it is in the bin" guarantee.
|
|
44
|
+
*/
|
|
45
|
+
readonly move: (from: string, to: string, signal?: AbortSignal) => Promise<void>;
|
|
46
|
+
/** Physically delete a path. */
|
|
47
|
+
readonly remove: (path: string, options: RecycleBinRemovalOptions) => Promise<void>;
|
|
48
|
+
/** List one directory. */
|
|
49
|
+
readonly list: (dir: string, signal?: AbortSignal) => Promise<readonly BackendEntry[]>;
|
|
50
|
+
/** Read a text file (the index). */
|
|
51
|
+
readonly readText: (path: string, signal?: AbortSignal) => Promise<string>;
|
|
52
|
+
/** Write a text file as UTF-8 **without BOM** (the index). */
|
|
53
|
+
readonly writeText: (path: string, text: string, signal?: AbortSignal) => Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/** Everything {@link createRecycleBinService} needs; each field is injected so tests need no host. */
|
|
56
|
+
export interface RecycleBinServiceDeps {
|
|
57
|
+
/** Bin root directory for one root; a deployment setting, never hardcoded here. */
|
|
58
|
+
readonly binRootFor: (root: RootRef) => string;
|
|
59
|
+
/** Find a root by its `rootKey()`; `undefined` means the root is no longer registered. */
|
|
60
|
+
readonly resolveRoot: (key: string) => RootRef | undefined;
|
|
61
|
+
/** `stat` in the local world. */
|
|
62
|
+
readonly statLocal: (path: string, signal?: AbortSignal) => Promise<BackendStat>;
|
|
63
|
+
/** `stat` in the remote world. */
|
|
64
|
+
readonly statRemote: (path: string, signal?: AbortSignal) => Promise<BackendStat>;
|
|
65
|
+
/** Move primitive of the remote world. */
|
|
66
|
+
readonly moveRemote: (from: string, to: string, signal?: AbortSignal) => Promise<void>;
|
|
67
|
+
/** Remove primitive of the remote world. */
|
|
68
|
+
readonly removeRemote: (path: string, options: RecycleBinRemovalOptions) => Promise<void>;
|
|
69
|
+
/** List primitive of the remote world. */
|
|
70
|
+
readonly listRemote: (dir: string, signal?: AbortSignal) => Promise<readonly BackendEntry[]>;
|
|
71
|
+
/** Index read primitive of the remote world. */
|
|
72
|
+
readonly readTextRemote: (path: string, signal?: AbortSignal) => Promise<string>;
|
|
73
|
+
/** Index write primitive of the remote world (UTF-8, no BOM). */
|
|
74
|
+
readonly writeTextRemote: (path: string, text: string, signal?: AbortSignal) => Promise<void>;
|
|
75
|
+
/** Clock, injected so a test can pin "now". */
|
|
76
|
+
readonly now: () => number;
|
|
77
|
+
/** Undo depth from configuration (a non-positive value keeps no history). */
|
|
78
|
+
readonly undoDepth: number;
|
|
79
|
+
/** Roots whose bins exist, so `sweep()` can walk them all. */
|
|
80
|
+
readonly knownRoots: () => readonly RootRef[];
|
|
81
|
+
/**
|
|
82
|
+
* Cross-process lock around every index read-modify-write.
|
|
83
|
+
*
|
|
84
|
+
* Optional so the domain service stays testable without touching the filesystem; when absent the service
|
|
85
|
+
* falls back to an in-process chain, which is **weaker** (it cannot see a second DSH process) and is
|
|
86
|
+
* therefore only acceptable in tests. Production wiring always injects the real lock.
|
|
87
|
+
*/
|
|
88
|
+
readonly withIndexLock?: WithIndexLock;
|
|
89
|
+
/**
|
|
90
|
+
* Local-world mutators.
|
|
91
|
+
*
|
|
92
|
+
* Optional: a service built for remote work omits it, and touching a local target then fails with
|
|
93
|
+
* `REMOTE_UNSUPPORTED` instead of misapplying the remote primitives to a local path.
|
|
94
|
+
*/
|
|
95
|
+
readonly localIo?: RecycleBinWorldIo;
|
|
96
|
+
}
|
|
97
|
+
/** One deletion target: a root plus a path inside it. */
|
|
98
|
+
export interface RecycleBinTarget {
|
|
99
|
+
readonly root: RootRef;
|
|
100
|
+
/** Absolute path in the root's own spelling. */
|
|
101
|
+
readonly path: string;
|
|
102
|
+
}
|
|
103
|
+
/** A bin view scope: one root, optionally narrowed to targets deleted under one directory. */
|
|
104
|
+
export interface RecycleBinScope {
|
|
105
|
+
readonly root: RootRef;
|
|
106
|
+
/** Optional directory prefix, matched per path segment. */
|
|
107
|
+
readonly path?: string;
|
|
108
|
+
}
|
|
109
|
+
/** How {@link RecycleBinService.discard} removes a target. */
|
|
110
|
+
export type DiscardMode = 'permanent' | 'recycle';
|
|
111
|
+
/** Options for {@link RecycleBinService.discard}. */
|
|
112
|
+
export interface DiscardOptions {
|
|
113
|
+
readonly mode: DiscardMode;
|
|
114
|
+
readonly signal?: AbortSignal;
|
|
115
|
+
}
|
|
116
|
+
/** Result of a discard. */
|
|
117
|
+
export interface DiscardResult {
|
|
118
|
+
readonly mode: DiscardMode;
|
|
119
|
+
/** The bin entry in `recycle` mode; `undefined` for `permanent`. */
|
|
120
|
+
readonly entry: RecycleBinEntry | undefined;
|
|
121
|
+
/** Whether the content can still be recovered. **Always `false` for `permanent`.** */
|
|
122
|
+
readonly recoverable: boolean;
|
|
123
|
+
/** Post-operation confirmation: the original path is gone. */
|
|
124
|
+
readonly verified: boolean;
|
|
125
|
+
/** Human-facing note, mandatory for an irreversible delete. */
|
|
126
|
+
readonly warning: string | undefined;
|
|
127
|
+
}
|
|
128
|
+
/** What a bin actually holds for one root. */
|
|
129
|
+
export interface RecycleBinListing {
|
|
130
|
+
readonly entries: readonly RecycleBinEntry[];
|
|
131
|
+
/** Ids the index knows but the bin directory no longer contains. */
|
|
132
|
+
readonly missing: readonly RecycleBinEntryId[];
|
|
133
|
+
/** Directory names in the bin that no index record claims. */
|
|
134
|
+
readonly orphans: readonly string[];
|
|
135
|
+
/** Index records that were unusable and skipped. */
|
|
136
|
+
readonly skipped: number;
|
|
137
|
+
}
|
|
138
|
+
/** Result of a restore. */
|
|
139
|
+
export interface RestoreResult {
|
|
140
|
+
readonly entry: RecycleBinEntry;
|
|
141
|
+
readonly restoredPath: string;
|
|
142
|
+
readonly verified: boolean;
|
|
143
|
+
}
|
|
144
|
+
/** Result of a physical delete from the bin. */
|
|
145
|
+
export interface PurgeResult {
|
|
146
|
+
readonly id: RecycleBinEntryId;
|
|
147
|
+
readonly sizeBytes: number;
|
|
148
|
+
/** Always true when the call returns; a failed delete throws instead. */
|
|
149
|
+
readonly purged: boolean;
|
|
150
|
+
readonly warning: string;
|
|
151
|
+
}
|
|
152
|
+
/** Options for `list`, `restore`, `purge`, `undo` and `redo`. */
|
|
153
|
+
export interface OperationOptions {
|
|
154
|
+
readonly signal?: AbortSignal;
|
|
155
|
+
}
|
|
156
|
+
/** Result of an undo or redo step. */
|
|
157
|
+
export interface UndoResult {
|
|
158
|
+
/** The action that was executed. */
|
|
159
|
+
readonly action: 'discard' | 'restore';
|
|
160
|
+
readonly entry: RecycleBinEntry;
|
|
161
|
+
/** Human-facing description of what happened. */
|
|
162
|
+
readonly message: string;
|
|
163
|
+
}
|
|
164
|
+
/** Retention limits for one sweep; all optional. */
|
|
165
|
+
export interface SweepOptions {
|
|
166
|
+
/** Keep at most this many entries per bin; non-positive/absent = unlimited. */
|
|
167
|
+
readonly maxEntries?: number;
|
|
168
|
+
/** Keep at most this many bytes per bin; non-positive/absent = unlimited. */
|
|
169
|
+
readonly maxBytes?: number;
|
|
170
|
+
/** Keep entries at most this old; non-positive/absent = unlimited. */
|
|
171
|
+
readonly maxAgeMs?: number;
|
|
172
|
+
readonly signal?: AbortSignal;
|
|
173
|
+
}
|
|
174
|
+
/** One entry physically removed by a sweep. */
|
|
175
|
+
export interface SweepPurge {
|
|
176
|
+
readonly rootKey: string;
|
|
177
|
+
readonly id: RecycleBinEntryId;
|
|
178
|
+
readonly sizeBytes: number;
|
|
179
|
+
readonly dimension: 'age' | 'count' | 'bytes';
|
|
180
|
+
}
|
|
181
|
+
/** One entry a sweep could not delete; the sweep continues with the rest. */
|
|
182
|
+
export interface SweepFailure {
|
|
183
|
+
readonly rootKey: string;
|
|
184
|
+
readonly id?: RecycleBinEntryId;
|
|
185
|
+
readonly message: string;
|
|
186
|
+
}
|
|
187
|
+
/** Per-root summary of a sweep. */
|
|
188
|
+
export interface SweepRootReport {
|
|
189
|
+
readonly rootKey: string;
|
|
190
|
+
readonly considered: number;
|
|
191
|
+
readonly purged: readonly RecycleBinEntryId[];
|
|
192
|
+
readonly kept: readonly RecycleBinEntryId[];
|
|
193
|
+
readonly reasons: readonly {
|
|
194
|
+
readonly id: RecycleBinEntryId;
|
|
195
|
+
readonly dimension: 'age' | 'count' | 'bytes';
|
|
196
|
+
}[];
|
|
197
|
+
}
|
|
198
|
+
/** Outcome of a sweep. */
|
|
199
|
+
export interface SweepReport {
|
|
200
|
+
readonly purged: readonly SweepPurge[];
|
|
201
|
+
readonly roots: readonly SweepRootReport[];
|
|
202
|
+
readonly failures: readonly SweepFailure[];
|
|
203
|
+
/** `false` when no dimension was limited: no retention decision was made (an unconfigured 0 must not empty a bin). */
|
|
204
|
+
readonly policyApplied: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Empty entry directories removed because no index record claimed them.
|
|
207
|
+
*
|
|
208
|
+
* Reported separately from `purged` because these are garbage, not retention: they are pruned even when no
|
|
209
|
+
* limit is configured, and they were never recoverable (audit 0098).
|
|
210
|
+
*/
|
|
211
|
+
readonly prunedOrphans: number;
|
|
212
|
+
}
|
|
213
|
+
/** Read-only undo state, for a UI that needs to enable or disable the buttons. */
|
|
214
|
+
export interface RecycleBinHistory {
|
|
215
|
+
readonly canUndo: boolean;
|
|
216
|
+
readonly canRedo: boolean;
|
|
217
|
+
readonly undo: readonly {
|
|
218
|
+
readonly kind: string;
|
|
219
|
+
readonly id: string;
|
|
220
|
+
}[];
|
|
221
|
+
readonly redo: readonly {
|
|
222
|
+
readonly kind: string;
|
|
223
|
+
readonly id: string;
|
|
224
|
+
}[];
|
|
225
|
+
readonly dropped: number;
|
|
226
|
+
readonly undoDepth: number;
|
|
227
|
+
}
|
|
228
|
+
/** The service surface. */
|
|
229
|
+
export interface RecycleBinService {
|
|
230
|
+
/** Delete a target permanently or into the recycle bin. */
|
|
231
|
+
discard(target: RecycleBinTarget, options: DiscardOptions): Promise<DiscardResult>;
|
|
232
|
+
/** List one bin, reconciled against the directory that actually exists. */
|
|
233
|
+
list(scope: RecycleBinScope, options?: OperationOptions): Promise<RecycleBinListing>;
|
|
234
|
+
/** Restore one entry to its original path. */
|
|
235
|
+
restore(id: RecycleBinEntryId, options?: OperationOptions): Promise<RestoreResult>;
|
|
236
|
+
/** Physically delete one entry from the bin. */
|
|
237
|
+
purge(id: RecycleBinEntryId, options?: OperationOptions): Promise<PurgeResult>;
|
|
238
|
+
/** Apply retention limits to every known bin. */
|
|
239
|
+
sweep(options: SweepOptions): Promise<SweepReport>;
|
|
240
|
+
/** Undo the newest operation. */
|
|
241
|
+
undo(options?: OperationOptions): Promise<UndoResult>;
|
|
242
|
+
/** Re-apply the newest undone operation. */
|
|
243
|
+
redo(options?: OperationOptions): Promise<UndoResult>;
|
|
244
|
+
/** Current undo/redo state. */
|
|
245
|
+
history(): RecycleBinHistory;
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=recycle-bin-contract.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回收站服务的**移动/恢复族**:把条目搬进 bin、搬回原处,含"移动后确认"与"索引写失败回滚"。
|
|
3
|
+
*
|
|
4
|
+
* 从 `recycle-bin-service.ts` 的工厂里抽出(审计 0076)。只依赖世界原语,不碰撤销栈。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-over-ssh/app/recycle-bin-move
|
|
7
|
+
*/
|
|
8
|
+
import { type RecycleBinEntry } from '../../domain/transfer/recycle-bin.ts';
|
|
9
|
+
import { type RootRef } from '../../domain/roots/root.ts';
|
|
10
|
+
import type { BinWorld } from './recycle-bin-world.ts';
|
|
11
|
+
export declare function createBinMove(world: BinWorld): {
|
|
12
|
+
tryMove: (root: RootRef, from: string, to: string, signal: AbortSignal | undefined) => Promise<boolean>;
|
|
13
|
+
moveOrReport: (root: RootRef, from: string, to: string, signal: AbortSignal | undefined, reason: string) => Promise<void>;
|
|
14
|
+
confirmInBin: (root: RootRef, entry: RecycleBinEntry, signal: AbortSignal | undefined) => Promise<void>;
|
|
15
|
+
moveIntoBin: (root: RootRef, entry: RecycleBinEntry, signal: AbortSignal | undefined) => Promise<void>;
|
|
16
|
+
restoreEntry: (root: RootRef, entry: RecycleBinEntry, signal: AbortSignal | undefined) => Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=recycle-bin-move.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recycle-bin service: the domain's pure decisions wired to injected I/O primitives.
|
|
3
|
+
*
|
|
4
|
+
* The seam is deliberate (AGENTS.md 红线 3): every path that touches a real filesystem or a real SSH
|
|
5
|
+
* connection arrives as a function, so this module is testable offline and a test can inject a move that fails.
|
|
6
|
+
*
|
|
7
|
+
* Two contracts matter more than the rest:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Move first, then confirm.** In `recycle` mode a success is reported only after `stat` shows the entry
|
|
10
|
+
* inside the bin **and** the original path gone. A move that throws is confirmed before anything is
|
|
11
|
+
* claimed: if the move actually landed (a lost response) it counts as success, otherwise the call fails
|
|
12
|
+
* and nothing is recorded — "deleted but not in the bin" must never be reported as success.
|
|
13
|
+
* 2. **The index write is never swallowed.** If the index cannot be written after a successful move, the move
|
|
14
|
+
* is rolled back and the failure is reported; a physical delete has no rollback, so that case is reported
|
|
15
|
+
* with `purged: true, indexStale: true` instead of being hidden.
|
|
16
|
+
*
|
|
17
|
+
* Other decisions worth knowing:
|
|
18
|
+
*
|
|
19
|
+
* - A `permanent` discard is irreversible by construction and says so in its result (`recoverable: false`),
|
|
20
|
+
* and its undo record is refused as `irreversible`.
|
|
21
|
+
* - Restoring onto an occupied original path is **refused** (`TRANSFER_CONFLICT`, with the informational
|
|
22
|
+
* `suggestedPath` in `details`); this module never silently restores to a different name.
|
|
23
|
+
* - `*Remote` primitives are the **remote world's** mutators. A local root additionally needs `localIo`; when it
|
|
24
|
+
* is missing the call fails with `REMOTE_UNSUPPORTED` rather than reusing remote primitives on local paths.
|
|
25
|
+
* - Every destructive call takes an `AbortSignal`, propagated to every injected primitive.
|
|
26
|
+
* - Error codes are reused from the existing table (no new code is registered): `TRASH_NOT_FOUND` (missing
|
|
27
|
+
* entry, unreadable index), `REMOTE_NOT_FOUND` (target gone), `TRANSFER_CONFLICT` (occupied restore path),
|
|
28
|
+
* `ROOT_OUTSIDE` (symlink target refused), `ROOT_AMBIGUOUS` (one id in two bins), `REMOTE_UNSUPPORTED`
|
|
29
|
+
* (local root without local primitives), `REMOTE_COMMAND_FAILED` (an operation that could not be confirmed),
|
|
30
|
+
* `ROOT_INVALID` (unusable configuration/input), `CANCELLED` (aborted signal).
|
|
31
|
+
*
|
|
32
|
+
* @module dsh-over-ssh/integration/recycle-bin-service
|
|
33
|
+
*/
|
|
34
|
+
import type { RecycleBinServiceDeps, RecycleBinService } from './recycle-bin-contract.ts';
|
|
35
|
+
export * from './recycle-bin-contract.ts';
|
|
36
|
+
/** Removal options for one physical delete. */
|
|
37
|
+
export * from './recycle-bin-contract.ts';
|
|
38
|
+
/**
|
|
39
|
+
* Build the service.
|
|
40
|
+
*
|
|
41
|
+
* @param deps - injected I/O, clock, configuration and root lookup.
|
|
42
|
+
* @returns a {@link RecycleBinService}; its undo stack is per-instance, in memory and not persisted.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createRecycleBinService(deps: RecycleBinServiceDeps): RecycleBinService;
|
|
45
|
+
//# sourceMappingURL=recycle-bin-service.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回收站服务的**世界原语**:索引读写、按根取 IO、stat、根查找。
|
|
3
|
+
*
|
|
4
|
+
* 从 `recycle-bin-service.ts` 的工厂里抽出(审计 0076):只依赖 deps,不碰撤销栈与业务判断。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-over-ssh/app/recycle-bin-world
|
|
7
|
+
*/
|
|
8
|
+
import type { BackendStat } from '../backend.ts';
|
|
9
|
+
import { type RecycleBinEntry, type RecycleBinEntryId } from '../../domain/transfer/recycle-bin.ts';
|
|
10
|
+
import { type RootRef } from '../../domain/roots/root.ts';
|
|
11
|
+
import type { RecycleBinServiceDeps, RecycleBinWorldIo, RecycleBinRemovalOptions } from './recycle-bin-contract.ts';
|
|
12
|
+
export type IndexState = {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly entries: readonly RecycleBinEntry[];
|
|
15
|
+
readonly skipped: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function createBinWorld(deps: RecycleBinServiceDeps): {
|
|
18
|
+
describe: (error: unknown) => string;
|
|
19
|
+
abortIfRequested: (signal: AbortSignal | undefined) => void;
|
|
20
|
+
ioFor: (root: RootRef) => RecycleBinWorldIo;
|
|
21
|
+
statFor: (root: RootRef, path: string, signal: AbortSignal | undefined) => Promise<BackendStat>;
|
|
22
|
+
binRootOf: (root: RootRef) => string;
|
|
23
|
+
readIndex: (root: RootRef, signal: AbortSignal | undefined) => Promise<IndexState>;
|
|
24
|
+
writeIndex: (root: RootRef, path: string, entries: readonly RecycleBinEntry[], signal: AbortSignal | undefined) => Promise<void>;
|
|
25
|
+
withIndexLock: <T>(key: string, operation: () => Promise<T>) => Promise<T>;
|
|
26
|
+
removalOptions: (recursive: boolean, signal: AbortSignal | undefined) => RecycleBinRemovalOptions;
|
|
27
|
+
knownRoots: () => readonly RootRef[];
|
|
28
|
+
resolveRootOf: (key: string) => RootRef;
|
|
29
|
+
findEntry: (id: RecycleBinEntryId, signal: AbortSignal | undefined) => Promise<{
|
|
30
|
+
readonly root: RootRef;
|
|
31
|
+
readonly entry: RecycleBinEntry;
|
|
32
|
+
} | undefined>;
|
|
33
|
+
contains: (root: RootRef, base: string, candidate: string) => boolean;
|
|
34
|
+
entryDirOf: (entry: RecycleBinEntry) => string;
|
|
35
|
+
pruneEntryDir: (root: RootRef, dir: string, signal: AbortSignal | undefined) => Promise<boolean>;
|
|
36
|
+
};
|
|
37
|
+
export type BinWorld = ReturnType<typeof createBinWorld>;
|
|
38
|
+
//# sourceMappingURL=recycle-bin-world.d.ts.map
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回收站服务的**契约**:依赖注入形状、目标/结果类型、索引状态。
|
|
3
|
+
*
|
|
4
|
+
* 从 `recycle-bin-service.ts` 抽出(审计 0076)。抽出的直接原因不是"文件太长",而是
|
|
5
|
+
* 世界原语模块需要这些类型:留在 service 里会形成 `world → service`(type)与
|
|
6
|
+
* `service → world`(value)的环,架构测试会红。契约独立后方向单一。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-remote-workspace/app/recycle-bin-contract
|
|
9
|
+
*/
|
|
10
|
+
import type { BackendEntry, BackendStat } from './backend.ts';
|
|
11
|
+
import { type RecycleBinEntry, type RecycleBinEntryId } from '../domain/recycle-bin.ts';
|
|
12
|
+
import { type RootRef } from '../domain/root.ts';
|
|
13
|
+
export interface RecycleBinRemovalOptions {
|
|
14
|
+
/** Whether the target is a directory that must be removed with its content. */
|
|
15
|
+
readonly recursive: boolean;
|
|
16
|
+
/** Caller cancellation. */
|
|
17
|
+
readonly signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
/** The mutating primitives of one execution world, shaped like the remote group. */
|
|
20
|
+
export interface RecycleBinWorldIo {
|
|
21
|
+
/**
|
|
22
|
+
* Move (rename) a path.
|
|
23
|
+
*
|
|
24
|
+
* Contract: the implementation must create a missing destination parent directory (equivalent to
|
|
25
|
+
* `mkdir -p` + `rename`). A cross-device move that cannot be done by rename must **fail loudly**; silently
|
|
26
|
+
* degrading to copy-then-delete would break the "don't delete until it is in the bin" guarantee.
|
|
27
|
+
*/
|
|
28
|
+
readonly move: (from: string, to: string, signal?: AbortSignal) => Promise<void>;
|
|
29
|
+
/** Physically delete a path. */
|
|
30
|
+
readonly remove: (path: string, options: RecycleBinRemovalOptions) => Promise<void>;
|
|
31
|
+
/** List one directory. */
|
|
32
|
+
readonly list: (dir: string, signal?: AbortSignal) => Promise<readonly BackendEntry[]>;
|
|
33
|
+
/** Read a text file (the index). */
|
|
34
|
+
readonly readText: (path: string, signal?: AbortSignal) => Promise<string>;
|
|
35
|
+
/** Write a text file as UTF-8 **without BOM** (the index). */
|
|
36
|
+
readonly writeText: (path: string, text: string, signal?: AbortSignal) => Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/** Everything {@link createRecycleBinService} needs; each field is injected so tests need no host. */
|
|
39
|
+
export interface RecycleBinServiceDeps {
|
|
40
|
+
/** Bin root directory for one root; a deployment setting, never hardcoded here. */
|
|
41
|
+
readonly binRootFor: (root: RootRef) => string;
|
|
42
|
+
/** Find a root by its `rootKey()`; `undefined` means the root is no longer registered. */
|
|
43
|
+
readonly resolveRoot: (key: string) => RootRef | undefined;
|
|
44
|
+
/** `stat` in the local world. */
|
|
45
|
+
readonly statLocal: (path: string, signal?: AbortSignal) => Promise<BackendStat>;
|
|
46
|
+
/** `stat` in the remote world. */
|
|
47
|
+
readonly statRemote: (path: string, signal?: AbortSignal) => Promise<BackendStat>;
|
|
48
|
+
/** Move primitive of the remote world. */
|
|
49
|
+
readonly moveRemote: (from: string, to: string, signal?: AbortSignal) => Promise<void>;
|
|
50
|
+
/** Remove primitive of the remote world. */
|
|
51
|
+
readonly removeRemote: (path: string, options: RecycleBinRemovalOptions) => Promise<void>;
|
|
52
|
+
/** List primitive of the remote world. */
|
|
53
|
+
readonly listRemote: (dir: string, signal?: AbortSignal) => Promise<readonly BackendEntry[]>;
|
|
54
|
+
/** Index read primitive of the remote world. */
|
|
55
|
+
readonly readTextRemote: (path: string, signal?: AbortSignal) => Promise<string>;
|
|
56
|
+
/** Index write primitive of the remote world (UTF-8, no BOM). */
|
|
57
|
+
readonly writeTextRemote: (path: string, text: string, signal?: AbortSignal) => Promise<void>;
|
|
58
|
+
/** Clock, injected so a test can pin "now". */
|
|
59
|
+
readonly now: () => number;
|
|
60
|
+
/** Undo depth from configuration (a non-positive value keeps no history). */
|
|
61
|
+
readonly undoDepth: number;
|
|
62
|
+
/** Roots whose bins exist, so `sweep()` can walk them all. */
|
|
63
|
+
readonly knownRoots: () => readonly RootRef[];
|
|
64
|
+
/**
|
|
65
|
+
* Local-world mutators.
|
|
66
|
+
*
|
|
67
|
+
* Optional: a service built for remote work omits it, and touching a local target then fails with
|
|
68
|
+
* `REMOTE_UNSUPPORTED` instead of misapplying the remote primitives to a local path.
|
|
69
|
+
*/
|
|
70
|
+
readonly localIo?: RecycleBinWorldIo;
|
|
71
|
+
}
|
|
72
|
+
/** One deletion target: a root plus a path inside it. */
|
|
73
|
+
export interface RecycleBinTarget {
|
|
74
|
+
readonly root: RootRef;
|
|
75
|
+
/** Absolute path in the root's own spelling. */
|
|
76
|
+
readonly path: string;
|
|
77
|
+
}
|
|
78
|
+
/** A bin view scope: one root, optionally narrowed to targets deleted under one directory. */
|
|
79
|
+
export interface RecycleBinScope {
|
|
80
|
+
readonly root: RootRef;
|
|
81
|
+
/** Optional directory prefix, matched per path segment. */
|
|
82
|
+
readonly path?: string;
|
|
83
|
+
}
|
|
84
|
+
/** How {@link RecycleBinService.discard} removes a target. */
|
|
85
|
+
export type DiscardMode = 'permanent' | 'recycle';
|
|
86
|
+
/** Options for {@link RecycleBinService.discard}. */
|
|
87
|
+
export interface DiscardOptions {
|
|
88
|
+
readonly mode: DiscardMode;
|
|
89
|
+
readonly signal?: AbortSignal;
|
|
90
|
+
}
|
|
91
|
+
/** Result of a discard. */
|
|
92
|
+
export interface DiscardResult {
|
|
93
|
+
readonly mode: DiscardMode;
|
|
94
|
+
/** The bin entry in `recycle` mode; `undefined` for `permanent`. */
|
|
95
|
+
readonly entry: RecycleBinEntry | undefined;
|
|
96
|
+
/** Whether the content can still be recovered. **Always `false` for `permanent`.** */
|
|
97
|
+
readonly recoverable: boolean;
|
|
98
|
+
/** Post-operation confirmation: the original path is gone. */
|
|
99
|
+
readonly verified: boolean;
|
|
100
|
+
/** Human-facing note, mandatory for an irreversible delete. */
|
|
101
|
+
readonly warning: string | undefined;
|
|
102
|
+
}
|
|
103
|
+
/** What a bin actually holds for one root. */
|
|
104
|
+
export interface RecycleBinListing {
|
|
105
|
+
readonly entries: readonly RecycleBinEntry[];
|
|
106
|
+
/** Ids the index knows but the bin directory no longer contains. */
|
|
107
|
+
readonly missing: readonly RecycleBinEntryId[];
|
|
108
|
+
/** Directory names in the bin that no index record claims. */
|
|
109
|
+
readonly orphans: readonly string[];
|
|
110
|
+
/** Index records that were unusable and skipped. */
|
|
111
|
+
readonly skipped: number;
|
|
112
|
+
}
|
|
113
|
+
/** Result of a restore. */
|
|
114
|
+
export interface RestoreResult {
|
|
115
|
+
readonly entry: RecycleBinEntry;
|
|
116
|
+
readonly restoredPath: string;
|
|
117
|
+
readonly verified: boolean;
|
|
118
|
+
}
|
|
119
|
+
/** Result of a physical delete from the bin. */
|
|
120
|
+
export interface PurgeResult {
|
|
121
|
+
readonly id: RecycleBinEntryId;
|
|
122
|
+
readonly sizeBytes: number;
|
|
123
|
+
/** Always true when the call returns; a failed delete throws instead. */
|
|
124
|
+
readonly purged: boolean;
|
|
125
|
+
readonly warning: string;
|
|
126
|
+
}
|
|
127
|
+
/** Options for `list`, `restore`, `purge`, `undo` and `redo`. */
|
|
128
|
+
export interface OperationOptions {
|
|
129
|
+
readonly signal?: AbortSignal;
|
|
130
|
+
}
|
|
131
|
+
/** Result of an undo or redo step. */
|
|
132
|
+
export interface UndoResult {
|
|
133
|
+
/** The action that was executed. */
|
|
134
|
+
readonly action: 'discard' | 'restore';
|
|
135
|
+
readonly entry: RecycleBinEntry;
|
|
136
|
+
/** Human-facing description of what happened. */
|
|
137
|
+
readonly message: string;
|
|
138
|
+
}
|
|
139
|
+
/** Retention limits for one sweep; all optional. */
|
|
140
|
+
export interface SweepOptions {
|
|
141
|
+
/** Keep at most this many entries per bin; non-positive/absent = unlimited. */
|
|
142
|
+
readonly maxEntries?: number;
|
|
143
|
+
/** Keep at most this many bytes per bin; non-positive/absent = unlimited. */
|
|
144
|
+
readonly maxBytes?: number;
|
|
145
|
+
/** Keep entries at most this old; non-positive/absent = unlimited. */
|
|
146
|
+
readonly maxAgeMs?: number;
|
|
147
|
+
readonly signal?: AbortSignal;
|
|
148
|
+
}
|
|
149
|
+
/** One entry physically removed by a sweep. */
|
|
150
|
+
export interface SweepPurge {
|
|
151
|
+
readonly rootKey: string;
|
|
152
|
+
readonly id: RecycleBinEntryId;
|
|
153
|
+
readonly sizeBytes: number;
|
|
154
|
+
readonly dimension: 'age' | 'count' | 'bytes';
|
|
155
|
+
}
|
|
156
|
+
/** One entry a sweep could not delete; the sweep continues with the rest. */
|
|
157
|
+
export interface SweepFailure {
|
|
158
|
+
readonly rootKey: string;
|
|
159
|
+
readonly id?: RecycleBinEntryId;
|
|
160
|
+
readonly message: string;
|
|
161
|
+
}
|
|
162
|
+
/** Per-root summary of a sweep. */
|
|
163
|
+
export interface SweepRootReport {
|
|
164
|
+
readonly rootKey: string;
|
|
165
|
+
readonly considered: number;
|
|
166
|
+
readonly purged: readonly RecycleBinEntryId[];
|
|
167
|
+
readonly kept: readonly RecycleBinEntryId[];
|
|
168
|
+
readonly reasons: readonly {
|
|
169
|
+
readonly id: RecycleBinEntryId;
|
|
170
|
+
readonly dimension: 'age' | 'count' | 'bytes';
|
|
171
|
+
}[];
|
|
172
|
+
}
|
|
173
|
+
/** Outcome of a sweep. */
|
|
174
|
+
export interface SweepReport {
|
|
175
|
+
readonly purged: readonly SweepPurge[];
|
|
176
|
+
readonly roots: readonly SweepRootReport[];
|
|
177
|
+
readonly failures: readonly SweepFailure[];
|
|
178
|
+
/** `false` when no dimension was limited: nothing was touched (an unconfigured 0 must not empty a bin). */
|
|
179
|
+
readonly policyApplied: boolean;
|
|
180
|
+
}
|
|
181
|
+
/** Read-only undo state, for a UI that needs to enable or disable the buttons. */
|
|
182
|
+
export interface RecycleBinHistory {
|
|
183
|
+
readonly canUndo: boolean;
|
|
184
|
+
readonly canRedo: boolean;
|
|
185
|
+
readonly undo: readonly {
|
|
186
|
+
readonly kind: string;
|
|
187
|
+
readonly id: string;
|
|
188
|
+
}[];
|
|
189
|
+
readonly redo: readonly {
|
|
190
|
+
readonly kind: string;
|
|
191
|
+
readonly id: string;
|
|
192
|
+
}[];
|
|
193
|
+
readonly dropped: number;
|
|
194
|
+
readonly undoDepth: number;
|
|
195
|
+
}
|
|
196
|
+
/** The service surface. */
|
|
197
|
+
export interface RecycleBinService {
|
|
198
|
+
/** Delete a target permanently or into the recycle bin. */
|
|
199
|
+
discard(target: RecycleBinTarget, options: DiscardOptions): Promise<DiscardResult>;
|
|
200
|
+
/** List one bin, reconciled against the directory that actually exists. */
|
|
201
|
+
list(scope: RecycleBinScope, options?: OperationOptions): Promise<RecycleBinListing>;
|
|
202
|
+
/** Restore one entry to its original path. */
|
|
203
|
+
restore(id: RecycleBinEntryId, options?: OperationOptions): Promise<RestoreResult>;
|
|
204
|
+
/** Physically delete one entry from the bin. */
|
|
205
|
+
purge(id: RecycleBinEntryId, options?: OperationOptions): Promise<PurgeResult>;
|
|
206
|
+
/** Apply retention limits to every known bin. */
|
|
207
|
+
sweep(options: SweepOptions): Promise<SweepReport>;
|
|
208
|
+
/** Undo the newest operation. */
|
|
209
|
+
undo(options?: OperationOptions): Promise<UndoResult>;
|
|
210
|
+
/** Re-apply the newest undone operation. */
|
|
211
|
+
redo(options?: OperationOptions): Promise<UndoResult>;
|
|
212
|
+
/** Current undo/redo state. */
|
|
213
|
+
history(): RecycleBinHistory;
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=recycle-bin-contract.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回收站服务的**移动/恢复族**:把条目搬进 bin、搬回原处,含"移动后确认"与"索引写失败回滚"。
|
|
3
|
+
*
|
|
4
|
+
* 从 `recycle-bin-service.ts` 的工厂里抽出(审计 0076)。只依赖世界原语,不碰撤销栈。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-remote-workspace/app/recycle-bin-move
|
|
7
|
+
*/
|
|
8
|
+
import { type RecycleBinEntry } from '../domain/recycle-bin.ts';
|
|
9
|
+
import { type RootRef } from '../domain/root.ts';
|
|
10
|
+
import type { BinWorld } from './recycle-bin-world.ts';
|
|
11
|
+
export declare function createBinMove(world: BinWorld): {
|
|
12
|
+
tryMove: (root: RootRef, from: string, to: string, signal: AbortSignal | undefined) => Promise<boolean>;
|
|
13
|
+
moveOrReport: (root: RootRef, from: string, to: string, signal: AbortSignal | undefined, reason: string) => Promise<void>;
|
|
14
|
+
confirmInBin: (root: RootRef, entry: RecycleBinEntry, signal: AbortSignal | undefined) => Promise<void>;
|
|
15
|
+
moveIntoBin: (root: RootRef, entry: RecycleBinEntry, signal: AbortSignal | undefined) => Promise<void>;
|
|
16
|
+
restoreEntry: (root: RootRef, entry: RecycleBinEntry, signal: AbortSignal | undefined) => Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=recycle-bin-move.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recycle-bin service: the domain's pure decisions wired to injected I/O primitives.
|
|
3
|
+
*
|
|
4
|
+
* The seam is deliberate (AGENTS.md 红线 3): every path that touches a real filesystem or a real SSH
|
|
5
|
+
* connection arrives as a function, so this module is testable offline and a test can inject a move that fails.
|
|
6
|
+
*
|
|
7
|
+
* Two contracts matter more than the rest:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Move first, then confirm.** In `recycle` mode a success is reported only after `stat` shows the entry
|
|
10
|
+
* inside the bin **and** the original path gone. A move that throws is confirmed before anything is
|
|
11
|
+
* claimed: if the move actually landed (a lost response) it counts as success, otherwise the call fails
|
|
12
|
+
* and nothing is recorded — "deleted but not in the bin" must never be reported as success.
|
|
13
|
+
* 2. **The index write is never swallowed.** If the index cannot be written after a successful move, the move
|
|
14
|
+
* is rolled back and the failure is reported; a physical delete has no rollback, so that case is reported
|
|
15
|
+
* with `purged: true, indexStale: true` instead of being hidden.
|
|
16
|
+
*
|
|
17
|
+
* Other decisions worth knowing:
|
|
18
|
+
*
|
|
19
|
+
* - A `permanent` discard is irreversible by construction and says so in its result (`recoverable: false`),
|
|
20
|
+
* and its undo record is refused as `irreversible`.
|
|
21
|
+
* - Restoring onto an occupied original path is **refused** (`TRANSFER_CONFLICT`, with the informational
|
|
22
|
+
* `suggestedPath` in `details`); this module never silently restores to a different name.
|
|
23
|
+
* - `*Remote` primitives are the **remote world's** mutators. A local root additionally needs `localIo`; when it
|
|
24
|
+
* is missing the call fails with `REMOTE_UNSUPPORTED` rather than reusing remote primitives on local paths.
|
|
25
|
+
* - Every destructive call takes an `AbortSignal`, propagated to every injected primitive.
|
|
26
|
+
* - Error codes are reused from the existing table (no new code is registered): `TRASH_NOT_FOUND` (missing
|
|
27
|
+
* entry, unreadable index), `REMOTE_NOT_FOUND` (target gone), `TRANSFER_CONFLICT` (occupied restore path),
|
|
28
|
+
* `ROOT_OUTSIDE` (symlink target refused), `ROOT_AMBIGUOUS` (one id in two bins), `REMOTE_UNSUPPORTED`
|
|
29
|
+
* (local root without local primitives), `REMOTE_COMMAND_FAILED` (an operation that could not be confirmed),
|
|
30
|
+
* `ROOT_INVALID` (unusable configuration/input), `CANCELLED` (aborted signal).
|
|
31
|
+
*
|
|
32
|
+
* @module dsh-remote-workspace/integration/recycle-bin-service
|
|
33
|
+
*/
|
|
34
|
+
import type { RecycleBinServiceDeps, RecycleBinService } from './recycle-bin-contract.ts';
|
|
35
|
+
export * from './recycle-bin-contract.ts';
|
|
36
|
+
/** Removal options for one physical delete. */
|
|
37
|
+
export * from './recycle-bin-contract.ts';
|
|
38
|
+
/**
|
|
39
|
+
* Build the service.
|
|
40
|
+
*
|
|
41
|
+
* @param deps - injected I/O, clock, configuration and root lookup.
|
|
42
|
+
* @returns a {@link RecycleBinService}; its undo stack is per-instance, in memory and not persisted.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createRecycleBinService(deps: RecycleBinServiceDeps): RecycleBinService;
|
|
45
|
+
//# sourceMappingURL=recycle-bin-service.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回收站服务的**世界原语**:索引读写、按根取 IO、stat、根查找。
|
|
3
|
+
*
|
|
4
|
+
* 从 `recycle-bin-service.ts` 的工厂里抽出(审计 0076):只依赖 deps,不碰撤销栈与业务判断。
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-remote-workspace/app/recycle-bin-world
|
|
7
|
+
*/
|
|
8
|
+
import type { BackendStat } from './backend.ts';
|
|
9
|
+
import { type RecycleBinEntry, type RecycleBinEntryId } from '../domain/recycle-bin.ts';
|
|
10
|
+
import { type RootRef } from '../domain/root.ts';
|
|
11
|
+
import type { RecycleBinServiceDeps, RecycleBinWorldIo, RecycleBinRemovalOptions } from './recycle-bin-contract.ts';
|
|
12
|
+
export type IndexState = {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly entries: readonly RecycleBinEntry[];
|
|
15
|
+
readonly skipped: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function createBinWorld(deps: RecycleBinServiceDeps): {
|
|
18
|
+
describe: (error: unknown) => string;
|
|
19
|
+
abortIfRequested: (signal: AbortSignal | undefined) => void;
|
|
20
|
+
ioFor: (root: RootRef) => RecycleBinWorldIo;
|
|
21
|
+
statFor: (root: RootRef, path: string, signal: AbortSignal | undefined) => Promise<BackendStat>;
|
|
22
|
+
binRootOf: (root: RootRef) => string;
|
|
23
|
+
readIndex: (root: RootRef, signal: AbortSignal | undefined) => Promise<IndexState>;
|
|
24
|
+
writeIndex: (root: RootRef, path: string, entries: readonly RecycleBinEntry[], signal: AbortSignal | undefined) => Promise<void>;
|
|
25
|
+
removalOptions: (recursive: boolean, signal: AbortSignal | undefined) => RecycleBinRemovalOptions;
|
|
26
|
+
knownRoots: () => readonly RootRef[];
|
|
27
|
+
resolveRootOf: (key: string) => RootRef;
|
|
28
|
+
findEntry: (id: RecycleBinEntryId, signal: AbortSignal | undefined) => Promise<{
|
|
29
|
+
readonly root: RootRef;
|
|
30
|
+
readonly entry: RecycleBinEntry;
|
|
31
|
+
} | undefined>;
|
|
32
|
+
contains: (root: RootRef, base: string, candidate: string) => boolean;
|
|
33
|
+
};
|
|
34
|
+
export type BinWorld = ReturnType<typeof createBinWorld>;
|
|
35
|
+
//# sourceMappingURL=recycle-bin-world.d.ts.map
|