@deepseek-ai/dsh-workspace 0.1.2-alpha.5 → 0.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/lib/index.js +49 -21
- package/lib/invariant.js +1 -1
- package/lib/types/index.d.ts +8 -6
- package/lib/types/index.js +19 -15
- package/lib/types/paths.d.ts +24 -8
- package/lib/types/paths.js +36 -8
- package/lib/types/types.d.ts +1 -1
- package/package.json +14 -14
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/workspace/workspace/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: b9512b7f0c52760d7d51305dda24151e8464947d
|
|
6
|
+
README.zh.md: d578182c1fbff2ca2131cd1970090c3ece9b0808
|
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ With these rows mounted, creating a project shows up in the list immediately and
|
|
|
50
50
|
|
|
51
51
|
### Creating and ordering projects
|
|
52
52
|
|
|
53
|
-
Create a project from any directory that exists:
|
|
53
|
+
Create a project from any fully qualified directory that exists: filesystem roots such as `C:\` and ordinary directories are valid. Relative paths, Windows drive-relative paths such as `C:work`, missing paths, and files are rejected without creating a project; creating a project for a directory that already has one returns the existing project unchanged. Rename a project at any time, and move it to any position in the list:
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
56
|
// Host consumer code, after the composition above is loaded:
|
|
@@ -128,7 +128,7 @@ Read these pages when this package's view is not enough: the subsystem reference
|
|
|
128
128
|
- [Workspace subsystem](../../../docs/subsystems/workspace.md) — the feature contract for projects and their sessions, and the generated API for the workspace service.
|
|
129
129
|
- [Workspace package map](../README.md) — the group's single package and its repository position.
|
|
130
130
|
- [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md) — why project records use the domain data form.
|
|
131
|
-
- [Workspace UI product-flow Agent Note](../../../.agents/notes/
|
|
131
|
+
- [Workspace UI product-flow Agent Note](../../../.agents/notes/archived/feature/2026-07-25-workspace-ui-product-flow.md) — how the first start builds projects from session history and how the GUI orders them.
|
|
132
132
|
- [Workspace registration deletion decision](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md) — why removing a project never deletes its folder or sessions.
|
|
133
133
|
|
|
134
134
|
-----
|
|
@@ -173,6 +173,6 @@ This Dev Note is working context for maintainers: open questions and directions
|
|
|
173
173
|
|
|
174
174
|
#### Open: the `create(path, title?)` title parameter
|
|
175
175
|
|
|
176
|
-
The `title` parameter has no production caller since the gateway's create-by-name branch was removed; a code TODO proposes dropping the parameter and its `@param` clause together ([note](../../../.agents/notes/
|
|
176
|
+
The `title` parameter has no production caller since the gateway's create-by-name branch was removed; a code TODO proposes dropping the parameter and its `@param` clause together ([note](../../../.agents/notes/archived/simplification/2026-07-31-one-route-to-add-a-workspace.md)).
|
|
177
177
|
|
|
178
178
|
</details>
|
package/README.zh.md
CHANGED
|
@@ -50,7 +50,7 @@ kind: "package-reference"
|
|
|
50
50
|
|
|
51
51
|
### 创建与排序项目
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
从任何存在且完整限定的目录创建项目:`C:\` 等文件系统根目录和普通目录都有效。相对路径、`C:work` 等 Windows 盘符相对路径、不存在的路径和文件都会被拒绝,且不会创建项目;为已有项目的目录再次创建会原样返回现有项目。你可以随时重命名项目,并把它移动到列表中的任意位置:
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
56
|
// Host consumer code, after the composition above is loaded:
|
|
@@ -128,7 +128,7 @@ ctx.workspaceRegistry.list() // shows the project, newest first
|
|
|
128
128
|
- [Workspace 子系统](../../../docs/subsystems/workspace.zh.md)——项目及其会话的功能约定,以及 workspace 服务的生成 API。
|
|
129
129
|
- [Workspace 包映射](../README.zh.md)——本组唯一的包及其仓库位置。
|
|
130
130
|
- [领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)——为什么项目记录使用领域数据形式。
|
|
131
|
-
- [Workspace UI 产品流 Agent Note](../../../.agents/notes/
|
|
131
|
+
- [Workspace UI 产品流 Agent Note](../../../.agents/notes/archived/feature/2026-07-25-workspace-ui-product-flow.md)——首次启动如何从会话历史构建项目,以及 GUI 如何排序。
|
|
132
132
|
- [删除 Workspace 注册记录决策](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.zh.md)——为什么移除项目绝不会删除其文件夹或会话。
|
|
133
133
|
|
|
134
134
|
-----
|
|
@@ -173,6 +173,6 @@ ctx.workspaceRegistry.list() // shows the project, newest first
|
|
|
173
173
|
|
|
174
174
|
#### 开放:`create(path, title?)` 的 title 参数
|
|
175
175
|
|
|
176
|
-
网关的按名称创建分支移除后,`title` 参数已无生产调用方;代码中的 TODO 提议把该参数与其 `@param` 子句一并移除(参见[笔记](../../../.agents/notes/
|
|
176
|
+
网关的按名称创建分支移除后,`title` 参数已无生产调用方;代码中的 TODO 提议把该参数与其 `@param` 子句一并移除(参见[笔记](../../../.agents/notes/archived/simplification/2026-07-31-one-route-to-add-a-workspace.md))。
|
|
177
177
|
|
|
178
178
|
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { realpath, stat } from "node:fs/promises";
|
|
3
|
-
import { basename } from "node:path";
|
|
4
3
|
import { Service } from "@deepseek-ai/cordis";
|
|
4
|
+
import { posix, win32 } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
7
7
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
@@ -11,18 +11,42 @@ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
|
11
11
|
* @module @deepseek-ai/dsh-workspace/src/paths
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
* Check whether a path names one fixed Host location without process cwd or
|
|
15
|
+
* current-drive resolution.
|
|
16
|
+
* @param path - Candidate Workspace path.
|
|
17
|
+
* @param platform - Host platform; injectable for deterministic path tests.
|
|
18
|
+
* @returns Whether the path is fully qualified on that platform.
|
|
19
|
+
*/
|
|
20
|
+
function fullyQualifiedWorkspacePath(path, platform = process.platform) {
|
|
21
|
+
if (platform !== "win32") return posix.isAbsolute(path);
|
|
22
|
+
const root = win32.parse(path).root;
|
|
23
|
+
return win32.isAbsolute(path) && root !== "\\" && root !== "/";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Derive a non-empty default title from a canonical Workspace path.
|
|
27
|
+
* @param path - Canonical Workspace path.
|
|
28
|
+
* @param platform - Host platform; injectable for deterministic path tests.
|
|
29
|
+
* @returns The final segment when present, otherwise the complete root spelling.
|
|
30
|
+
*/
|
|
31
|
+
function defaultWorkspaceTitle(path, platform = process.platform) {
|
|
32
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
33
|
+
return pathApi.basename(path) || pathApi.parse(path).root;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Canonicalize a fully qualified directory path via `fs.realpath`: trailing
|
|
37
|
+
* slashes, `..` segments, and symlinks are all resolved. This is the ONE
|
|
38
|
+
* uniqueness canon of the package — workspace paths are stored canonicalized,
|
|
39
|
+
* uniqueness is string equality of canonicalized paths (a symlink to an
|
|
40
|
+
* existing workspace's directory collides), and attach-time session `cwd`
|
|
41
|
+
* checks go through the same canon. Relative paths reject before `realpath` can
|
|
42
|
+
* resolve them from the Host cwd or current Windows drive. A path that does not
|
|
43
|
+
* exist rejects with the original `ENOENT` — this is `create`'s reject path (a
|
|
44
|
+
* workspace must point at an existing directory).
|
|
22
45
|
* @param path - The path to canonicalize.
|
|
23
46
|
* @returns the canonical absolute path.
|
|
24
47
|
*/
|
|
25
48
|
async function realpathNormalize(path) {
|
|
49
|
+
if (!fullyQualifiedWorkspacePath(path)) throw new TypeError(`Workspace path is not fully qualified: '${path}'`);
|
|
26
50
|
return await realpath(path);
|
|
27
51
|
}
|
|
28
52
|
//#endregion
|
|
@@ -318,23 +342,23 @@ var WorkspaceRegistry = class extends Service {
|
|
|
318
342
|
await this.recoverPendingMutation();
|
|
319
343
|
this.validateStoredState(this.state);
|
|
320
344
|
if (!this.state.initialized) {
|
|
321
|
-
const headers = await this.
|
|
345
|
+
const headers = await this.listStoredHeaders();
|
|
322
346
|
await this.replaceHeaderIndex(headers);
|
|
323
347
|
await this.bootstrap(headers);
|
|
324
|
-
} else if (this.table.size > 0) await this.replaceHeaderIndex(await this.
|
|
348
|
+
} else if (this.table.size > 0) await this.replaceHeaderIndex(await this.listStoredHeaders());
|
|
325
349
|
await this.indexLiveSessions();
|
|
326
350
|
this.validateStoredState(this.requireState());
|
|
327
351
|
this.rebuildEntities();
|
|
328
352
|
this.reportFilteredCandidates();
|
|
329
353
|
}
|
|
330
354
|
/**
|
|
331
|
-
* Create or reuse a workspace for an existing directory. The
|
|
332
|
-
* canonicalized through `fs.realpath`; a nonexistent
|
|
333
|
-
*
|
|
334
|
-
*
|
|
355
|
+
* Create or reuse a workspace for an existing directory. The fully qualified
|
|
356
|
+
* path is canonicalized through `fs.realpath`; a relative, nonexistent, or
|
|
357
|
+
* non-directory path rejects. Repeated calls for the same canonical path
|
|
358
|
+
* return the existing entity without changing its title.
|
|
335
359
|
* A newly created workspace is prepended to the durable registry order.
|
|
336
360
|
* Different canonical paths may share a display title.
|
|
337
|
-
* @param path - Existing directory to own, in
|
|
361
|
+
* @param path - Existing directory to own, in a fully qualified path spelling.
|
|
338
362
|
* @param title - Display title used only when a new record is created.
|
|
339
363
|
* @returns the existing or newly durable workspace.
|
|
340
364
|
*/
|
|
@@ -439,14 +463,14 @@ var WorkspaceRegistry = class extends Service {
|
|
|
439
463
|
async sessionKnown(id) {
|
|
440
464
|
if (this.ctx.get("sessions")?.get(id) !== void 0) return true;
|
|
441
465
|
if (this.headers.has(id)) return true;
|
|
442
|
-
await this.indexHeaders(await this.
|
|
466
|
+
await this.indexHeaders(await this.listStoredHeaders());
|
|
443
467
|
return this.headers.has(id);
|
|
444
468
|
}
|
|
445
469
|
/**
|
|
446
470
|
* Resolve by canonical directory path without creating or mutating a
|
|
447
471
|
* workspace. A missing path rejects during `realpath`; an existing unowned
|
|
448
472
|
* directory returns `undefined`.
|
|
449
|
-
* @param path - Existing directory path in
|
|
473
|
+
* @param path - Existing directory path in a fully qualified spelling.
|
|
450
474
|
* @returns the workspace owning the canonical path, when one exists.
|
|
451
475
|
*/
|
|
452
476
|
async resolveByPath(path) {
|
|
@@ -455,7 +479,7 @@ var WorkspaceRegistry = class extends Service {
|
|
|
455
479
|
}
|
|
456
480
|
async createCanonical(canonical, title) {
|
|
457
481
|
for (const entity of this.entities.values()) if (entity.path === canonical) return entity;
|
|
458
|
-
const workspaceName = title ??
|
|
482
|
+
const workspaceName = title ?? defaultWorkspaceTitle(canonical);
|
|
459
483
|
const table = this.requireTable();
|
|
460
484
|
const state = this.requireState();
|
|
461
485
|
const id = WorkspaceId(randomUUID());
|
|
@@ -602,7 +626,7 @@ var WorkspaceRegistry = class extends Service {
|
|
|
602
626
|
const createdAt = new Date(group.newestAt).toISOString();
|
|
603
627
|
const record = {
|
|
604
628
|
path: group.path,
|
|
605
|
-
title:
|
|
629
|
+
title: defaultWorkspaceTitle(group.path),
|
|
606
630
|
sessionIds,
|
|
607
631
|
createdAt,
|
|
608
632
|
updatedAt: createdAt
|
|
@@ -701,6 +725,10 @@ var WorkspaceRegistry = class extends Service {
|
|
|
701
725
|
this.invalidSessionPaths.set(header.id, `cwd '${header.cwd}' does not resolve`);
|
|
702
726
|
}
|
|
703
727
|
}
|
|
728
|
+
/** Every stored session's header, projected from the persistence snapshot listing. */
|
|
729
|
+
async listStoredHeaders() {
|
|
730
|
+
return (await this.ctx.sessionPersistence.list()).map((snapshot) => snapshot.header);
|
|
731
|
+
}
|
|
704
732
|
async indexLiveSessions() {
|
|
705
733
|
const sessions = this.ctx.get("sessions");
|
|
706
734
|
if (sessions === void 0) return;
|
|
@@ -725,7 +753,7 @@ var WorkspaceRegistry = class extends Service {
|
|
|
725
753
|
}
|
|
726
754
|
const cached = this.headers.get(id);
|
|
727
755
|
if (cached !== void 0) return cached;
|
|
728
|
-
const headers = await this.
|
|
756
|
+
const headers = await this.listStoredHeaders();
|
|
729
757
|
await this.indexHeaders(headers);
|
|
730
758
|
const header = this.headers.get(id);
|
|
731
759
|
if (header === void 0) throw new Error(`cannot validate session '${id}': session persistence holds no such session`);
|
package/lib/invariant.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:crypto";
|
|
2
2
|
import "node:fs/promises";
|
|
3
|
-
import "node:path";
|
|
4
3
|
import { Service } from "@deepseek-ai/cordis";
|
|
4
|
+
import "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
7
7
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
package/lib/types/index.d.ts
CHANGED
|
@@ -66,13 +66,13 @@ export declare class WorkspaceRegistry extends Service {
|
|
|
66
66
|
/** Open the domain, finish bootstrap when required, and rebuild the ordered cache. */
|
|
67
67
|
protected [Service.init](): Promise<void>;
|
|
68
68
|
/**
|
|
69
|
-
* Create or reuse a workspace for an existing directory. The
|
|
70
|
-
* canonicalized through `fs.realpath`; a nonexistent
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* Create or reuse a workspace for an existing directory. The fully qualified
|
|
70
|
+
* path is canonicalized through `fs.realpath`; a relative, nonexistent, or
|
|
71
|
+
* non-directory path rejects. Repeated calls for the same canonical path
|
|
72
|
+
* return the existing entity without changing its title.
|
|
73
73
|
* A newly created workspace is prepended to the durable registry order.
|
|
74
74
|
* Different canonical paths may share a display title.
|
|
75
|
-
* @param path - Existing directory to own, in
|
|
75
|
+
* @param path - Existing directory to own, in a fully qualified path spelling.
|
|
76
76
|
* @param title - Display title used only when a new record is created.
|
|
77
77
|
* @returns the existing or newly durable workspace.
|
|
78
78
|
*/
|
|
@@ -133,7 +133,7 @@ export declare class WorkspaceRegistry extends Service {
|
|
|
133
133
|
* Resolve by canonical directory path without creating or mutating a
|
|
134
134
|
* workspace. A missing path rejects during `realpath`; an existing unowned
|
|
135
135
|
* directory returns `undefined`.
|
|
136
|
-
* @param path - Existing directory path in
|
|
136
|
+
* @param path - Existing directory path in a fully qualified spelling.
|
|
137
137
|
* @returns the workspace owning the canonical path, when one exists.
|
|
138
138
|
*/
|
|
139
139
|
resolveByPath(path: string): Promise<Workspace | undefined>;
|
|
@@ -151,6 +151,8 @@ export declare class WorkspaceRegistry extends Service {
|
|
|
151
151
|
private replaceHeaderIndex;
|
|
152
152
|
private indexHeaders;
|
|
153
153
|
private indexHeader;
|
|
154
|
+
/** Every stored session's header, projected from the persistence snapshot listing. */
|
|
155
|
+
private listStoredHeaders;
|
|
154
156
|
private indexLiveSessions;
|
|
155
157
|
private reportFilteredCandidates;
|
|
156
158
|
private readSessionHeader;
|
package/lib/types/index.js
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { stat } from 'node:fs/promises';
|
|
9
|
-
import { basename } from 'node:path';
|
|
10
9
|
import { Service } from '@deepseek-ai/cordis';
|
|
11
10
|
import { WorkspaceEntity } from "./entity.js";
|
|
12
11
|
export { WorkspaceMoveInvalidError } from "./entity.js";
|
|
13
|
-
import { realpathNormalize } from "./paths.js";
|
|
12
|
+
import { defaultWorkspaceTitle, realpathNormalize } from "./paths.js";
|
|
14
13
|
import { workspaceDomainSpec } from "./spec.js";
|
|
15
14
|
export { workspaceDomainState, workspaceRecord, workspaceDomainSpec } from "./spec.js";
|
|
16
15
|
export { realpathNormalize } from "./paths.js";
|
|
@@ -90,12 +89,12 @@ export class WorkspaceRegistry extends Service {
|
|
|
90
89
|
await this.recoverPendingMutation();
|
|
91
90
|
this.validateStoredState(this.state);
|
|
92
91
|
if (!this.state.initialized) {
|
|
93
|
-
const headers = await this.
|
|
92
|
+
const headers = await this.listStoredHeaders();
|
|
94
93
|
await this.replaceHeaderIndex(headers);
|
|
95
94
|
await this.bootstrap(headers);
|
|
96
95
|
}
|
|
97
96
|
else if (this.table.size > 0) {
|
|
98
|
-
await this.replaceHeaderIndex(await this.
|
|
97
|
+
await this.replaceHeaderIndex(await this.listStoredHeaders());
|
|
99
98
|
}
|
|
100
99
|
await this.indexLiveSessions();
|
|
101
100
|
this.validateStoredState(this.requireState());
|
|
@@ -103,19 +102,19 @@ export class WorkspaceRegistry extends Service {
|
|
|
103
102
|
this.reportFilteredCandidates();
|
|
104
103
|
}
|
|
105
104
|
/**
|
|
106
|
-
* Create or reuse a workspace for an existing directory. The
|
|
107
|
-
* canonicalized through `fs.realpath`; a nonexistent
|
|
108
|
-
*
|
|
109
|
-
*
|
|
105
|
+
* Create or reuse a workspace for an existing directory. The fully qualified
|
|
106
|
+
* path is canonicalized through `fs.realpath`; a relative, nonexistent, or
|
|
107
|
+
* non-directory path rejects. Repeated calls for the same canonical path
|
|
108
|
+
* return the existing entity without changing its title.
|
|
110
109
|
* A newly created workspace is prepended to the durable registry order.
|
|
111
110
|
* Different canonical paths may share a display title.
|
|
112
|
-
* @param path - Existing directory to own, in
|
|
111
|
+
* @param path - Existing directory to own, in a fully qualified path spelling.
|
|
113
112
|
* @param title - Display title used only when a new record is created.
|
|
114
113
|
* @returns the existing or newly durable workspace.
|
|
115
114
|
*/
|
|
116
115
|
// TODO: `title` lost its last production caller when the gateway's
|
|
117
116
|
// create-by-name branch was deleted
|
|
118
|
-
// (.agents/notes/
|
|
117
|
+
// (.agents/notes/archived/simplification/2026-07-31-one-route-to-add-a-workspace.md);
|
|
119
118
|
// drop the parameter with its @param clause and the `create(path, title?)`
|
|
120
119
|
// lines in this package's README pair.
|
|
121
120
|
async create(path, title) {
|
|
@@ -225,14 +224,14 @@ export class WorkspaceRegistry extends Service {
|
|
|
225
224
|
return true;
|
|
226
225
|
if (this.headers.has(id))
|
|
227
226
|
return true;
|
|
228
|
-
await this.indexHeaders(await this.
|
|
227
|
+
await this.indexHeaders(await this.listStoredHeaders());
|
|
229
228
|
return this.headers.has(id);
|
|
230
229
|
}
|
|
231
230
|
/**
|
|
232
231
|
* Resolve by canonical directory path without creating or mutating a
|
|
233
232
|
* workspace. A missing path rejects during `realpath`; an existing unowned
|
|
234
233
|
* directory returns `undefined`.
|
|
235
|
-
* @param path - Existing directory path in
|
|
234
|
+
* @param path - Existing directory path in a fully qualified spelling.
|
|
236
235
|
* @returns the workspace owning the canonical path, when one exists.
|
|
237
236
|
*/
|
|
238
237
|
async resolveByPath(path) {
|
|
@@ -248,7 +247,7 @@ export class WorkspaceRegistry extends Service {
|
|
|
248
247
|
if (entity.path === canonical)
|
|
249
248
|
return entity;
|
|
250
249
|
}
|
|
251
|
-
const workspaceName = title ??
|
|
250
|
+
const workspaceName = title ?? defaultWorkspaceTitle(canonical);
|
|
252
251
|
const table = this.requireTable();
|
|
253
252
|
const state = this.requireState();
|
|
254
253
|
const id = WorkspaceId(randomUUID());
|
|
@@ -413,7 +412,7 @@ export class WorkspaceRegistry extends Service {
|
|
|
413
412
|
const createdAt = new Date(group.newestAt).toISOString();
|
|
414
413
|
const record = {
|
|
415
414
|
path: group.path,
|
|
416
|
-
title:
|
|
415
|
+
title: defaultWorkspaceTitle(group.path),
|
|
417
416
|
sessionIds,
|
|
418
417
|
createdAt,
|
|
419
418
|
updatedAt: createdAt,
|
|
@@ -532,6 +531,11 @@ export class WorkspaceRegistry extends Service {
|
|
|
532
531
|
this.invalidSessionPaths.set(header.id, `cwd '${header.cwd}' does not resolve`);
|
|
533
532
|
}
|
|
534
533
|
}
|
|
534
|
+
/** Every stored session's header, projected from the persistence snapshot listing. */
|
|
535
|
+
async listStoredHeaders() {
|
|
536
|
+
const snapshots = await this.ctx.sessionPersistence.list();
|
|
537
|
+
return snapshots.map(snapshot => snapshot.header);
|
|
538
|
+
}
|
|
535
539
|
async indexLiveSessions() {
|
|
536
540
|
const sessions = this.ctx.get('sessions');
|
|
537
541
|
if (sessions === undefined)
|
|
@@ -562,7 +566,7 @@ export class WorkspaceRegistry extends Service {
|
|
|
562
566
|
const cached = this.headers.get(id);
|
|
563
567
|
if (cached !== undefined)
|
|
564
568
|
return cached;
|
|
565
|
-
const headers = await this.
|
|
569
|
+
const headers = await this.listStoredHeaders();
|
|
566
570
|
await this.indexHeaders(headers);
|
|
567
571
|
const header = this.headers.get(id);
|
|
568
572
|
if (header === undefined) {
|
package/lib/types/paths.d.ts
CHANGED
|
@@ -3,14 +3,30 @@
|
|
|
3
3
|
* @module @deepseek-ai/dsh-workspace/src/paths
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
* Check whether a path names one fixed Host location without process cwd or
|
|
7
|
+
* current-drive resolution.
|
|
8
|
+
* @param path - Candidate Workspace path.
|
|
9
|
+
* @param platform - Host platform; injectable for deterministic path tests.
|
|
10
|
+
* @returns Whether the path is fully qualified on that platform.
|
|
11
|
+
*/
|
|
12
|
+
export declare function fullyQualifiedWorkspacePath(path: string, platform?: NodeJS.Platform): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Derive a non-empty default title from a canonical Workspace path.
|
|
15
|
+
* @param path - Canonical Workspace path.
|
|
16
|
+
* @param platform - Host platform; injectable for deterministic path tests.
|
|
17
|
+
* @returns The final segment when present, otherwise the complete root spelling.
|
|
18
|
+
*/
|
|
19
|
+
export declare function defaultWorkspaceTitle(path: string, platform?: NodeJS.Platform): string;
|
|
20
|
+
/**
|
|
21
|
+
* Canonicalize a fully qualified directory path via `fs.realpath`: trailing
|
|
22
|
+
* slashes, `..` segments, and symlinks are all resolved. This is the ONE
|
|
23
|
+
* uniqueness canon of the package — workspace paths are stored canonicalized,
|
|
24
|
+
* uniqueness is string equality of canonicalized paths (a symlink to an
|
|
25
|
+
* existing workspace's directory collides), and attach-time session `cwd`
|
|
26
|
+
* checks go through the same canon. Relative paths reject before `realpath` can
|
|
27
|
+
* resolve them from the Host cwd or current Windows drive. A path that does not
|
|
28
|
+
* exist rejects with the original `ENOENT` — this is `create`'s reject path (a
|
|
29
|
+
* workspace must point at an existing directory).
|
|
14
30
|
* @param path - The path to canonicalize.
|
|
15
31
|
* @returns the canonical absolute path.
|
|
16
32
|
*/
|
package/lib/types/paths.js
CHANGED
|
@@ -3,19 +3,47 @@
|
|
|
3
3
|
* @module @deepseek-ai/dsh-workspace/src/paths
|
|
4
4
|
*/
|
|
5
5
|
import { realpath } from 'node:fs/promises';
|
|
6
|
+
import { posix, win32 } from 'node:path';
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
* Check whether a path names one fixed Host location without process cwd or
|
|
9
|
+
* current-drive resolution.
|
|
10
|
+
* @param path - Candidate Workspace path.
|
|
11
|
+
* @param platform - Host platform; injectable for deterministic path tests.
|
|
12
|
+
* @returns Whether the path is fully qualified on that platform.
|
|
13
|
+
*/
|
|
14
|
+
export function fullyQualifiedWorkspacePath(path, platform = process.platform) {
|
|
15
|
+
if (platform !== 'win32')
|
|
16
|
+
return posix.isAbsolute(path);
|
|
17
|
+
const root = win32.parse(path).root;
|
|
18
|
+
return win32.isAbsolute(path) && root !== '\\' && root !== '/';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Derive a non-empty default title from a canonical Workspace path.
|
|
22
|
+
* @param path - Canonical Workspace path.
|
|
23
|
+
* @param platform - Host platform; injectable for deterministic path tests.
|
|
24
|
+
* @returns The final segment when present, otherwise the complete root spelling.
|
|
25
|
+
*/
|
|
26
|
+
export function defaultWorkspaceTitle(path, platform = process.platform) {
|
|
27
|
+
const pathApi = platform === 'win32' ? win32 : posix;
|
|
28
|
+
return pathApi.basename(path) || pathApi.parse(path).root;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Canonicalize a fully qualified directory path via `fs.realpath`: trailing
|
|
32
|
+
* slashes, `..` segments, and symlinks are all resolved. This is the ONE
|
|
33
|
+
* uniqueness canon of the package — workspace paths are stored canonicalized,
|
|
34
|
+
* uniqueness is string equality of canonicalized paths (a symlink to an
|
|
35
|
+
* existing workspace's directory collides), and attach-time session `cwd`
|
|
36
|
+
* checks go through the same canon. Relative paths reject before `realpath` can
|
|
37
|
+
* resolve them from the Host cwd or current Windows drive. A path that does not
|
|
38
|
+
* exist rejects with the original `ENOENT` — this is `create`'s reject path (a
|
|
39
|
+
* workspace must point at an existing directory).
|
|
15
40
|
* @param path - The path to canonicalize.
|
|
16
41
|
* @returns the canonical absolute path.
|
|
17
42
|
*/
|
|
18
43
|
export async function realpathNormalize(path) {
|
|
44
|
+
if (!fullyQualifiedWorkspacePath(path)) {
|
|
45
|
+
throw new TypeError(`Workspace path is not fully qualified: '${path}'`);
|
|
46
|
+
}
|
|
19
47
|
return await realpath(path);
|
|
20
48
|
}
|
|
21
49
|
//# sourceMappingURL=paths.js.map
|
package/lib/types/types.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface Workspace {
|
|
|
34
34
|
* afterwards, even when the directory disappears (see {@link status}).
|
|
35
35
|
*/
|
|
36
36
|
readonly path: string;
|
|
37
|
-
/** Display title. Defaults to
|
|
37
|
+
/** Display title. Defaults to the final path segment, or a filesystem root's own spelling; duplicates are allowed. */
|
|
38
38
|
readonly title: string;
|
|
39
39
|
/** ISO-8601 creation instant, stamped at create and never rewritten. */
|
|
40
40
|
readonly createdAt: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-workspace",
|
|
3
3
|
"description": "Workspace entity registry (ctx.workspaceRegistry): durable workspace records with validated session attachment over the domain data form for the DeepSeek Harness",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -38,24 +38,24 @@
|
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
41
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
42
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
43
|
-
"@deepseek-ai/dsh-session-persistence": "^0.1.
|
|
44
|
-
"@deepseek-ai/dsh-storage": "^0.1.
|
|
45
|
-
"@deepseek-ai/dsh-storage-domain": "^0.1.
|
|
46
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.
|
|
41
|
+
"@deepseek-ai/dsh-invariants": "^0.1.3-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.3-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-storage": "^0.1.3-alpha.2",
|
|
45
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.3-alpha.2",
|
|
46
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.3-alpha.2"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"zod": "^4.4.3",
|
|
50
|
-
"@deepseek-ai/dsh-brand": "^0.1.
|
|
50
|
+
"@deepseek-ai/dsh-brand": "^0.1.3-alpha.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
54
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.
|
|
59
|
-
"@deepseek-ai/dsh-storage-domain": "^0.1.
|
|
54
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.3-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-storage": "^0.1.3-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-invariants": "^0.1.3-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.3-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.3-alpha.2"
|
|
60
60
|
}
|
|
61
61
|
}
|