@x-otto/workspace 0.0.1-alpha.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/README.md +100 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @x-otto/workspace
|
|
2
|
+
|
|
3
|
+
> Workspace identity primitives: unique IDs, local marker read/write, upward search, and three runtime forms (L/R/V).
|
|
4
|
+
|
|
5
|
+
`@x-otto/workspace` defines how otto identifies and associates a workspace with a directory. A workspace is a first-class logical entity with an immutable `id`, persisted via a local marker file (`.otto/workspace.json`). The package handles id generation and validation, upward directory search for existing markers, and resolution of the three workspace forms (Local/Remote/Virtual). Zero internal dependencies — only Node.js built-ins.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @x-otto/workspace
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import {
|
|
17
|
+
createWorkspaceResolver,
|
|
18
|
+
genWorkspaceId,
|
|
19
|
+
isValidWorkspaceId,
|
|
20
|
+
workspaceKey,
|
|
21
|
+
readMarker,
|
|
22
|
+
findMarker,
|
|
23
|
+
DEFAULT_WORKSPACE_KEY,
|
|
24
|
+
} from '@x-otto/workspace'
|
|
25
|
+
|
|
26
|
+
// Create a resolver with injected I/O
|
|
27
|
+
const io: WorkspaceIO = {
|
|
28
|
+
readFile: async (path) => { /* read from real fs or mock */ return null },
|
|
29
|
+
writeFile: async (path, content) => { /* write to real fs or mock */ },
|
|
30
|
+
genId: () => genWorkspaceId(),
|
|
31
|
+
now: () => new Date().toISOString(),
|
|
32
|
+
}
|
|
33
|
+
const resolver = createWorkspaceResolver(io)
|
|
34
|
+
|
|
35
|
+
// Resolve workspace identity for a directory
|
|
36
|
+
// Finds existing marker via upward search, or creates one
|
|
37
|
+
const ref = await resolver.resolveLocal('/path/to/project')
|
|
38
|
+
// ref.key === 'ws_<id>'
|
|
39
|
+
// ref.backend === 'local'
|
|
40
|
+
// ref.root === '/path/to/project'
|
|
41
|
+
|
|
42
|
+
// Explicit workspace id override (--id mode)
|
|
43
|
+
const devpodRef = await resolver.resolveLocal('/path/to/project', {
|
|
44
|
+
id: 'my-custom-id',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Interactive mode: don't auto-create, let the user decide
|
|
48
|
+
const result = await resolver.resolveLocal('/safe/path', { autoCreate: false })
|
|
49
|
+
if (result.kind === 'uninitialized') {
|
|
50
|
+
// Show trust prompt → user confirms → create explicitly
|
|
51
|
+
const ref = await resolver.createLocal('/safe/path')
|
|
52
|
+
} else if (result.kind === 'parentWorkspaceExisted') {
|
|
53
|
+
// A marker was found in parent directory — choose how to proceed
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Id Utilities
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { genWorkspaceId, isValidWorkspaceId, workspaceKey } from '@x-otto/workspace'
|
|
61
|
+
|
|
62
|
+
const id = genWorkspaceId()
|
|
63
|
+
// → 'a1b2c3d4e5f6789012345678abcdef01'
|
|
64
|
+
|
|
65
|
+
isValidWorkspaceId(id) // → true
|
|
66
|
+
workspaceKey(id) // → 'ws_a1b2c3d4e5f6789012345678abcdef01'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Key Exports
|
|
70
|
+
|
|
71
|
+
### Types
|
|
72
|
+
- `Workspace` — Logical entity: id, name, owner, createdAt
|
|
73
|
+
- `WorkspaceRef` — Runtime reference: key, backend (local/remote), root, remote URL, memorySync
|
|
74
|
+
- `WorkspaceMarker` — `.otto/workspace.json` shape: id, name, remote, memorySyncEnabled
|
|
75
|
+
- `WorkspaceIO` — Injected I/O interface: readFile, writeFile, genId, now, confirmTrust
|
|
76
|
+
- `WorkspaceResolver` — `resolveLocal()` + `createLocal()` interface
|
|
77
|
+
|
|
78
|
+
### Functions
|
|
79
|
+
- `createWorkspaceResolver()` — Create resolver with given IO adapter
|
|
80
|
+
- `genWorkspaceId()` — Generate uuid-based id (32 hex chars)
|
|
81
|
+
- `isValidWorkspaceId()` / `assertValidWorkspaceId()` — Validate charset + length
|
|
82
|
+
- `workspaceKey()` — Build storage key (`ws_<id>`)
|
|
83
|
+
- `readMarker()` / `writeMarker()` — Read/write `.otto/workspace.json`
|
|
84
|
+
- `findMarker()` — Upward directory search for existing marker
|
|
85
|
+
|
|
86
|
+
### Constants
|
|
87
|
+
- `MARKER_DIR` — `.otto`
|
|
88
|
+
- `MARKER_FILE` — `workspace.json`
|
|
89
|
+
- `OTTO_GITIGNORE` — Default gitignore template for `.otto/`
|
|
90
|
+
- `DEFAULT_WORKSPACE_KEY` — `'default'` fallback for unbound sessions
|
|
91
|
+
- `WS_KEY_PREFIX` — `ws_`
|
|
92
|
+
|
|
93
|
+
## Dependencies
|
|
94
|
+
|
|
95
|
+
- **Internal**: None
|
|
96
|
+
- **External**: Node.js built-ins only (`node:crypto`, `node:path`)
|
|
97
|
+
|
|
98
|
+
## Related
|
|
99
|
+
|
|
100
|
+
- [Architecture](./ARCHITECTURE.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* @x-otto/workspace — workspace 身份原语(纯层)。
|
|
4
|
+
*
|
|
5
|
+
* workspace 是一等 DB 逻辑实体(id 不可变、不绑定文件),持久化第一维 = `workspaceKey = ws_<id>`。
|
|
6
|
+
* 两个正交轴:`backend`(持久化在哪)× `root`(有无本地文件)→ L/R/V 三形态。
|
|
7
|
+
* 见 docs/rfc/RFC-034-workspace-mechanism-persistence-dimension.md §4.1(workspace-mechanism-flows.md
|
|
8
|
+
* 设计稿已删,内容并入本 RFC,见 commit 5d6a9f5b)。
|
|
9
|
+
*/
|
|
10
|
+
/** workspace 一等实体(DB 行 / web-ui CRUD 对象)。 */
|
|
11
|
+
interface Workspace {
|
|
12
|
+
/** 不可变;客户端生成(见 id.ts)。 */
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/** 多租户(对接 RFC-009 M29);本地 unmanaged 形态可缺省。 */
|
|
16
|
+
readonly ownerId?: string;
|
|
17
|
+
readonly orgId?: string;
|
|
18
|
+
/** ISO 时间串;由调用方注入(纯层不取系统时钟)。 */
|
|
19
|
+
readonly createdAt: string;
|
|
20
|
+
}
|
|
21
|
+
/** 远端绑定引用(R/V 形态携带)。 */
|
|
22
|
+
interface RemoteRef {
|
|
23
|
+
/** 远端持久化服务 base URL。 */
|
|
24
|
+
readonly url: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 运行时引用:身份永远是 `ws_<id>`。两个正交轴:
|
|
28
|
+
* - `backend`:持久化后端在哪(`local` 本地盘 / `remote` 远端服务)。
|
|
29
|
+
* - `root`:本地文件根;在场 = 有文件工具,缺省 = headless(V)。
|
|
30
|
+
*
|
|
31
|
+
* 三形态:L = local+root · R = remote+root · V = remote+无 root。
|
|
32
|
+
*/
|
|
33
|
+
interface WorkspaceRef {
|
|
34
|
+
/** = `ws_` + id,持久化第一维。 */
|
|
35
|
+
readonly key: string;
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly backend: 'local' | 'remote';
|
|
38
|
+
/** 本地文件根(工具 root);headless 缺省。 */
|
|
39
|
+
readonly root?: string;
|
|
40
|
+
/** backend=remote 时携带。 */
|
|
41
|
+
readonly remote?: RemoteRef;
|
|
42
|
+
/**
|
|
43
|
+
* R 形态 workspace 记忆远端同步 opt-in(RFC-164 M164-2 D2),透传自 `WorkspaceMarker`。
|
|
44
|
+
* 缺省/`false` = 记忆走本地文件(M58-02 现状);`true` = auto-memory/lessons 走远端。
|
|
45
|
+
*/
|
|
46
|
+
readonly memorySyncEnabled?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `.otto/workspace.json` 的形状(本地关联 marker)。
|
|
50
|
+
* 默认 gitignore(RFC 规则 3);携 `remote` 时启动需过信任门(RFC 规则 3/§4.2.1)。
|
|
51
|
+
*/
|
|
52
|
+
interface WorkspaceMarker {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly createdAt: string;
|
|
56
|
+
readonly remote?: RemoteRef;
|
|
57
|
+
/**
|
|
58
|
+
* R 形态 workspace 记忆远端同步 opt-in(RFC-164 M164-2 D2)。缺省/`false` = 记忆走本地文件
|
|
59
|
+
* (M58-02 现状,向后兼容);显式 `true` = auto-memory/lessons 走远端 HttpMemoryStore。
|
|
60
|
+
* 用户需通过 `otto workspace memory-sync enable/disable` 命令显式切换,不隐式跟随
|
|
61
|
+
* session backend 的选择(D2:认知边界,用户须知情同意)。
|
|
62
|
+
*/
|
|
63
|
+
readonly memorySyncEnabled?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 注入式 IO(RFC 规则 9:纯层不直接碰 fs,便于测试)。
|
|
67
|
+
* 路径为绝对路径;实现见 coding 层(node fs 适配)。
|
|
68
|
+
*/
|
|
69
|
+
interface WorkspaceIO {
|
|
70
|
+
/** 读文件 utf-8;不存在返回 null(不抛)。 */
|
|
71
|
+
readFile(path: string): Promise<string | null>;
|
|
72
|
+
/** 写文件 utf-8(父目录由实现保证存在)。 */
|
|
73
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* RFC-303 D7(M2 T303-05):独占创建写——O_EXCL 语义,目标文件已存在则失败
|
|
76
|
+
* (返回 `false`,不覆盖、不抛异常),成功创建返回 `true`。用于并发首启创建互斥
|
|
77
|
+
* (两进程同时对空目录 createLocal 时,只有一个能真正创建 marker,另一个应改为
|
|
78
|
+
* 读取刚创建的 marker 复用其 id,而非各自生成不同 id 互相覆盖)。可选字段——缺省
|
|
79
|
+
* 时 resolver 降级为"先读后写"近似方案(TOCTOU 窗口极小但非零,见 resolver.ts
|
|
80
|
+
* create() 注释)。
|
|
81
|
+
*/
|
|
82
|
+
writeFileExclusive?(path: string, content: string): Promise<boolean>;
|
|
83
|
+
/** 生成不可变 workspace id(默认实现见 id.ts)。 */
|
|
84
|
+
genId(): string;
|
|
85
|
+
/** 当前时间 ISO 串;注入式(纯层不直接取系统时钟,便于测试)。 */
|
|
86
|
+
now(): string;
|
|
87
|
+
/**
|
|
88
|
+
* 信任门:发现带 `remote` 的 marker 时调用,返回是否信任连接。
|
|
89
|
+
* 缺省 = fail-closed(不信任,降级本地);见 RFC 规则 3。
|
|
90
|
+
*/
|
|
91
|
+
confirmTrust?(remote: RemoteRef, markerDir: string): Promise<boolean>;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/id.d.ts
|
|
95
|
+
/** `workspaceKey` 前缀。 */
|
|
96
|
+
declare const WS_KEY_PREFIX = "ws_";
|
|
97
|
+
/**
|
|
98
|
+
* 缺省 workspace key(无 workspace 身份标识时的回退分区键)。
|
|
99
|
+
* 供服务端路由/会话池/持久化层统一引用——定义在此为单一真源,
|
|
100
|
+
* 防止各消费方各自硬编码 `'default'` 字符串。
|
|
101
|
+
*/
|
|
102
|
+
declare const DEFAULT_WORKSPACE_KEY = "default";
|
|
103
|
+
/** id 是否合法(charset `[A-Za-z0-9_-]`,长度 1..64)。 */
|
|
104
|
+
declare function isValidWorkspaceId(id: string): boolean;
|
|
105
|
+
/** 校验 id,不合法即抛(fail-closed;读 marker / bind 入口用)。 */
|
|
106
|
+
declare function assertValidWorkspaceId(id: string): void;
|
|
107
|
+
/**
|
|
108
|
+
* 持久化第一维 key。id 不可变 ⇒ key 稳定。
|
|
109
|
+
* 校验后拼接,杜绝非法 id 进入 storage/URL。
|
|
110
|
+
*/
|
|
111
|
+
declare function workspaceKey(id: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* 默认 id 生成器:客户端生成、全局唯一、不可变(RFC 规则 1)。
|
|
114
|
+
* 用 uuid(去横线 → 32 hex),落在合法字符集内;远端 bind 时直接接受(不重分配)。
|
|
115
|
+
* resolver 接受注入覆盖以便测试。
|
|
116
|
+
*/
|
|
117
|
+
declare function genWorkspaceId(): string;
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/marker.d.ts
|
|
120
|
+
/** workspace 本地足迹目录名。 */
|
|
121
|
+
declare const MARKER_DIR = ".otto";
|
|
122
|
+
/** 关联 marker 文件名。 */
|
|
123
|
+
declare const MARKER_FILE = "workspace.json";
|
|
124
|
+
/** 生成的 gitignore 文件名。 */
|
|
125
|
+
declare const GITIGNORE_FILE = ".gitignore";
|
|
126
|
+
/**
|
|
127
|
+
* `.otto/.gitignore` 模板(RFC-034 规则 3 / 决策 D4):
|
|
128
|
+
* 身份(`workspace.json`)默认私有 + 易变忽略;项目知识(AGENTS.md/MEMORY.md/memory/)默认提交。
|
|
129
|
+
* 团队共享 workspace 实体需显式 `git add -f .otto/workspace.json`。
|
|
130
|
+
*/
|
|
131
|
+
declare const OTTO_GITIGNORE = "# otto workspace \u672C\u5730\u8DB3\u8FF9 \u2014 RFC-034 \u89C4\u5219 3\n# \u8EAB\u4EFD\u79C1\u6709 + \u6613\u53D8\u5FFD\u7565;\u9879\u76EE\u77E5\u8BC6(AGENTS.md / MEMORY.md / memory/)\u9ED8\u8BA4\u63D0\u4EA4\u3002\n# \u56E2\u961F\u5171\u4EAB workspace \u5B9E\u4F53:\u663E\u5F0F `git add -f .otto/workspace.json`\u3002\nworkspace.json\ncache/\nlogs/\n*.tmp\n# Machine-specific memory (RFC-164 M164-1 D3) \u2014 local/ is never synced across devices, never committed.\nlocal/\n";
|
|
132
|
+
/**
|
|
133
|
+
* 按 `memorySyncEnabled` 状态生成 gitignore 内容——opt-in 生效时追加 D4 单一权威切换规则。
|
|
134
|
+
* `writeMarker` 用此替代裸 `OTTO_GITIGNORE` 常量,使 marker 与 gitignore 内容随 opt-in 状态同步。
|
|
135
|
+
*
|
|
136
|
+
* 注意:`writeMarker` 会同时写入 gitignore 以保证 marker 的 `memorySyncEnabled` 字段与
|
|
137
|
+
* gitignore 规则一致——用户自定义 gitignore 规则应写在 `.otto/.gitignore` 文件末尾。
|
|
138
|
+
*/
|
|
139
|
+
declare function buildGitignoreContent(memorySyncEnabled: boolean): string;
|
|
140
|
+
/** `<dir>/.otto/workspace.json` 绝对路径。 */
|
|
141
|
+
declare function markerPath(dir: string): string;
|
|
142
|
+
/** `<dir>/.otto/.gitignore` 绝对路径。 */
|
|
143
|
+
declare function gitignorePath(dir: string): string;
|
|
144
|
+
/** 读取某目录的 marker;不存在返回 null,内容非法则抛(fail-closed)。 */
|
|
145
|
+
declare function readMarker(dir: string, io: WorkspaceIO): Promise<WorkspaceMarker | null>;
|
|
146
|
+
/** 命中结果:marker 所在目录 + 解析内容。 */
|
|
147
|
+
interface MarkerHit {
|
|
148
|
+
readonly dir: string;
|
|
149
|
+
readonly marker: WorkspaceMarker;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 从 `startCwd` 向上 walk 找最近的 `.otto/workspace.json`(类比 git 找 `.git`)。
|
|
153
|
+
* 命中即返回;到文件系统根仍未命中返回 null。
|
|
154
|
+
*/
|
|
155
|
+
declare function findMarker(startCwd: string, io: WorkspaceIO): Promise<MarkerHit | null>;
|
|
156
|
+
/**
|
|
157
|
+
* 在 `<dir>/.otto/` 写入 marker + gitignore(内容随 `marker.memorySyncEnabled` 状态调整,见 D4)。
|
|
158
|
+
*
|
|
159
|
+
* RFC-303 D7(M2 T303-05):`exclusive:true` 时用 `io.writeFileExclusive`(O_EXCL 语义)写
|
|
160
|
+
* marker 本体——若目标已存在则不覆盖、返回 `false`(调用方应改为读取已存在的 marker)。
|
|
161
|
+
* gitignore 始终用普通覆盖写(无并发创建互斥语义需求,其内容由 `memorySyncEnabled` 派生,
|
|
162
|
+
* 覆盖写是安全的幂等操作)。`io` 未实现 `writeFileExclusive` 时退化为普通覆盖写(向后兼容,
|
|
163
|
+
* 老 IO 实现不受影响,但不具备本次修复的互斥保证——调用方应尽快升级 IO 实现)。
|
|
164
|
+
*/
|
|
165
|
+
declare function writeMarker(dir: string, marker: WorkspaceMarker, io: WorkspaceIO, options?: {
|
|
166
|
+
exclusive?: boolean;
|
|
167
|
+
}): Promise<boolean>;
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/resolver.d.ts
|
|
170
|
+
/**
|
|
171
|
+
* `resolveLocal` 在 `autoCreate:false` 模式下,向上递归在**父级目录**命中 marker 时的返回值。
|
|
172
|
+
* 调用方(交互态 CLI)应提示用户选择"使用上级 workspace"还是"在当前目录创建新 workspace",
|
|
173
|
+
* 而非静默采用父级身份(防止子目录 workspace 被意外吞入父 workspace)。
|
|
174
|
+
*/
|
|
175
|
+
interface ParentWorkspaceExisted {
|
|
176
|
+
readonly kind: 'parentWorkspaceExisted';
|
|
177
|
+
/** marker 所在目录(父级 workspace 根)。 */
|
|
178
|
+
readonly parentRoot: string;
|
|
179
|
+
/** 用户当前工作目录(pwd)。 */
|
|
180
|
+
readonly currentRoot: string;
|
|
181
|
+
/** 父级 marker 内容。 */
|
|
182
|
+
readonly marker: WorkspaceMarker;
|
|
183
|
+
}
|
|
184
|
+
/** `resolveLocal` 可选项。 */
|
|
185
|
+
interface ResolveLocalOptions {
|
|
186
|
+
/** `--id` 覆盖:在此目录显式绑定指定 workspace(一目录多 workspace,DevPod 模式)。 */
|
|
187
|
+
id?: string;
|
|
188
|
+
/** 新建时的名字;缺省取目录名。 */
|
|
189
|
+
name?: string;
|
|
190
|
+
/** 新建时的 createdAt;缺省取 `io.now()`。 */
|
|
191
|
+
createdAt?: string;
|
|
192
|
+
/**
|
|
193
|
+
* marker 未命中时是否自动创建。缺省 `true`(旧行为,headless/`--id` 场景零感知)。
|
|
194
|
+
* 传 `false`(交互态新流程):未命中时不写文件,返回 `{kind:'uninitialized', root}`,
|
|
195
|
+
* 调用方需在用户确认(信任门)后调 `createLocal()` 才真正建立身份。对 `opts.id` 显式给定
|
|
196
|
+
* 的场景无效(该分支必然走 create,无歧义可言)。
|
|
197
|
+
*/
|
|
198
|
+
autoCreate?: boolean;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* `resolveLocal` 未命中 marker 时的结果(`autoCreate:false` 专用,见下方 opts)——
|
|
202
|
+
* 调用方须显式 `createLocal()` 才会真正写 marker(本次修复:交互态先过信任门,用户确认后
|
|
203
|
+
* 才建立 workspace 身份,防止 `.otto/workspace.json` 缺失时静默生成新随机 id 致旧会话孤儿化)。
|
|
204
|
+
*/
|
|
205
|
+
interface UninitializedWorkspace {
|
|
206
|
+
readonly kind: 'uninitialized';
|
|
207
|
+
readonly root: string;
|
|
208
|
+
}
|
|
209
|
+
/** workspace 身份解析器(注入式 IO,纯可测)。 */
|
|
210
|
+
interface WorkspaceResolver {
|
|
211
|
+
/**
|
|
212
|
+
* 本地 resolve-or-create:
|
|
213
|
+
* - `opts.id` 在场 → 显式绑定该 id(写/刷新 marker),忽略 autoCreate(DevPod/`--id` 模式无歧义)。
|
|
214
|
+
* - 否则向上 walk 找 marker:
|
|
215
|
+
* - 命中且 `hit.dir === cwd` → 当前目录已有 workspace 身份 → L/R(取决于 remote/trust)。
|
|
216
|
+
* - 命中且 `hit.dir !== cwd` + `autoCreate:false` → 返回 `ParentWorkspaceExisted`,
|
|
217
|
+
* 调用层应提示用户选择"使用上级 workspace"还是"在当前目录创建新 workspace",
|
|
218
|
+
* 防止子目录 workspace 被静默吞入父 workspace。
|
|
219
|
+
* - 命中且 `hit.dir !== cwd` + `autoCreate` 未传/`true`(headless)→ 静默采用父级。
|
|
220
|
+
* - 未命中:`opts.autoCreate !== false`(缺省/headless 等旧行为)→ 在 cwd 生成 id + 写 marker → L;
|
|
221
|
+
* `opts.autoCreate === false`(交互态新流程)→ 返回 `UninitializedWorkspace`,不写文件,
|
|
222
|
+
* 调用方需在用户确认(信任门)后调 `createLocal()` 才真正建立身份。
|
|
223
|
+
*/
|
|
224
|
+
resolveLocal(cwd: string, opts?: ResolveLocalOptions): Promise<WorkspaceRef | UninitializedWorkspace | ParentWorkspaceExisted>;
|
|
225
|
+
/**
|
|
226
|
+
* 显式创建 L 形态 workspace(写 marker)——`resolveLocal({autoCreate:false})` 返回
|
|
227
|
+
* `uninitialized` 后,调用方(信任确认后)调此方法完成创建。与 resolveLocal 内部的
|
|
228
|
+
* 自动创建分支同一实现,只是从"自动触发"变为"显式调用"。
|
|
229
|
+
*/
|
|
230
|
+
createLocal(root: string, opts?: ResolveLocalOptions): Promise<WorkspaceRef>;
|
|
231
|
+
}
|
|
232
|
+
declare function createWorkspaceResolver(io: WorkspaceIO): WorkspaceResolver;
|
|
233
|
+
//#endregion
|
|
234
|
+
export { DEFAULT_WORKSPACE_KEY, GITIGNORE_FILE, MARKER_DIR, MARKER_FILE, MarkerHit, OTTO_GITIGNORE, ParentWorkspaceExisted, RemoteRef, ResolveLocalOptions, UninitializedWorkspace, WS_KEY_PREFIX, Workspace, WorkspaceIO, WorkspaceMarker, WorkspaceRef, WorkspaceResolver, assertValidWorkspaceId, buildGitignoreContent, createWorkspaceResolver, findMarker, genWorkspaceId, gitignorePath, isValidWorkspaceId, markerPath, readMarker, workspaceKey, writeMarker };
|
|
235
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/types.ts","../src/id.ts","../src/marker.ts","../src/resolver.ts"],"mappings":";;AAUA;;;;;;;;UAAiB,SAAA;EAQG;EAAA,SANT,EAAA;EAAA,SACA,IAAA;EASe;EAAA,SAPf,OAAA;EAAA,SACA,KAAA;EAQG;EAAA,SANH,SAAA;AAAA;;UAIM,SAAA;EAcN;EAAA,SAZA,GAAA;AAAA;;;;;;;AA8BX;UApBiB,YAAA;;WAEN,GAAA;EAAA,SACA,EAAA;EAAA,SACA,OAAA;EAmBA;EAAA,SAjBA,IAAA;EAkBS;EAAA,SAhBT,MAAA,GAAS,SAAA;EAuBQ;;AAO5B;;EAP4B,SAlBjB,iBAAA;AAAA;;;;;UAOM,eAAA;EAAA,SACN,EAAA;EAAA,SACA,IAAA;EAAA,SACA,SAAA;EAAA,SACA,MAAA,GAAS,SAAA;EAkBlB;;;;;;EAAA,SAXS,iBAAA;AAAA;;;;;UAOM,WAAA;EAsBkB;EApBjC,QAAA,CAAS,IAAA,WAAe,OAAA;EAoBoC;EAlB5D,SAAA,CAAU,IAAA,UAAc,OAAA,WAAkB,OAAA;;;;ACzE5C;;;;;EDkFE,kBAAA,EAAoB,IAAA,UAAc,OAAA,WAAkB,OAAA;EC3EpB;ED6EhC,KAAA;EC7EgC;ED+EhC,GAAA;ECtEc;;;;ED2Ed,YAAA,EAAc,MAAA,EAAQ,SAAA,EAAW,SAAA,WAAoB,OAAA;AAAA;;;;cC3F1C,aAAA;;;;;;cAOA,qBAAA;;iBASG,kBAAA,CAAmB,EAAA;;iBAKnB,sBAAA,CAAuB,EAAA;ADFvC;;;;AAAA,iBCYgB,YAAA,CAAa,EAAA;ADA7B;;;;;AAAA,iBCUgB,cAAA,CAAA;;;ADlChB;AAAA,cEHa,UAAA;;cAEA,WAAA;;cAEA,cAAA;;;;;;cAOA,cAAA;;;;;AFgBb;;;iBEcgB,qBAAA,CAAsB,iBAAA;;iBAKtB,UAAA,CAAW,GAAA;;iBAKX,aAAA,CAAc,GAAA;;iBAkCR,UAAA,CAAW,GAAA,UAAa,EAAA,EAAI,WAAA,GAAc,OAAA,CAAQ,eAAA;;UAOvD,SAAA;EAAA,SACN,GAAA;EAAA,SACA,MAAA,EAAQ,eAAA;AAAA;;;;;iBAOG,UAAA,CAAW,QAAA,UAAkB,EAAA,EAAI,WAAA,GAAc,OAAA,CAAQ,SAAA;;;;;;AFpC7E;;;;iBEwDsB,WAAA,CACpB,GAAA,UACA,MAAA,EAAQ,eAAA,EACR,EAAA,EAAI,WAAA,EACJ,OAAA;EAAY,SAAA;AAAA,IACX,OAAA;;;AF3HH;;;;;AAAA,UGEiB,sBAAA;EAAA,SACN,IAAA;EHGA;EAAA,SGDA,UAAA;EHGS;EAAA,SGDT,WAAA;EHKM;EAAA,SGHN,MAAA,EAAQ,eAAA;AAAA;;UAIF,mBAAA;EHWA;EGTf,EAAA;;EAEA,IAAA;EHSS;EGPT,SAAA;EHSS;;;;;;EGFT,UAAA;AAAA;;;;;;UA6Be,sBAAA;EAAA,SACN,IAAA;EAAA,SACA,IAAA;AAAA;;UAIM,iBAAA;EHCA;;;;;;;;;;;;;EGaf,YAAA,CACE,GAAA,UACA,IAAA,GAAO,mBAAA,GACN,OAAA,CAAQ,YAAA,GAAe,sBAAA,GAAyB,sBAAA;EHZzC;;;;;EGkBV,WAAA,CAAY,IAAA,UAAc,IAAA,GAAO,mBAAA,GAAsB,OAAA,CAAQ,YAAA;AAAA;AAAA,iBAGjD,uBAAA,CAAwB,EAAA,EAAI,WAAA,GAAc,iBAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import{randomUUID as e}from"node:crypto";import{basename as t,dirname as n,join as r,resolve as i}from"node:path";const a=`ws_`,o=`default`,s=/^[A-Za-z0-9_-]{1,64}$/;function c(e){return s.test(e)}function l(e){if(!c(e))throw Error(`invalid workspace id: ${JSON.stringify(e)} (expect ${s})`)}function u(e){return l(e),`ws_`+e}function d(){return e().replace(/-/g,``)}const f=`.otto`,p=`workspace.json`,m=`.gitignore`,h=`# otto workspace 本地足迹 — RFC-034 规则 3
|
|
2
|
+
# 身份私有 + 易变忽略;项目知识(AGENTS.md / MEMORY.md / memory/)默认提交。
|
|
3
|
+
# 团队共享 workspace 实体:显式 \`git add -f .otto/workspace.json\`。
|
|
4
|
+
workspace.json
|
|
5
|
+
cache/
|
|
6
|
+
logs/
|
|
7
|
+
*.tmp
|
|
8
|
+
# Machine-specific memory (RFC-164 M164-1 D3) — local/ is never synced across devices, never committed.
|
|
9
|
+
local/
|
|
10
|
+
`;function g(e){return e?h+`# Remote memory sync enabled (RFC-164 M164-2 D2/D4) — auto-memory/lessons
|
|
11
|
+
# now have a single source of truth in the remote store; AGENTS.md still tracked by git.
|
|
12
|
+
MEMORY.md
|
|
13
|
+
memory/
|
|
14
|
+
lessons.json
|
|
15
|
+
`:h}function _(e){return r(e,f,p)}function v(e){return r(e,f,m)}function y(e){let t=JSON.parse(e),n=t.id,r=t.name,i=t.createdAt;if(typeof n!=`string`)throw Error(`workspace marker: missing id`);if(l(n),typeof r!=`string`)throw Error(`workspace marker: missing name`);if(typeof i!=`string`)throw Error(`workspace marker: missing createdAt`);let a=t.remote,o;if(a&&typeof a==`object`){let e=a.url;typeof e==`string`&&(o={url:e})}let s=t.memorySyncEnabled===!0?!0:void 0,c={id:n,name:r,createdAt:i};return o&&(c.remote=o),s!==void 0&&(c.memorySyncEnabled=s),c}async function b(e,t){let n=await t.readFile(_(e));return n==null?null:y(n)}async function x(e,t){let r=i(e);for(;;){let e=await b(r,t);if(e)return{dir:r,marker:e};let i=n(r);if(i===r)return null;r=i}}async function S(e,t,n,r){l(t.id);let i=_(e),a=JSON.stringify(t,null,2)+`
|
|
16
|
+
`;if(r?.exclusive&&n.writeFileExclusive){if(!await n.writeFileExclusive(i,a))return!1}else await n.writeFile(i,a);return await n.writeFile(v(e),g(t.memorySyncEnabled===!0)),!0}function C(e,t){return{key:u(e.id),id:e.id,backend:`local`,root:t,...e.memorySyncEnabled?{memorySyncEnabled:!0}:{}}}function w(e,t){return{key:u(e.id),id:e.id,backend:`remote`,root:t,...e.remote?{remote:e.remote}:{},...e.memorySyncEnabled?{memorySyncEnabled:!0}:{}}}function T(e){async function n(n,r,i){let a={id:i,name:r.name??(t(n)||`workspace`),createdAt:r.createdAt??e.now()};if(r.id==null){if(!await S(n,a,e,{exclusive:!0})){let t=await b(n,e);if(t)return C(t,n);await S(n,a,e)}}else await S(n,a,e);return C(a,n)}return{async resolveLocal(t,r={}){let a=i(t);if(r.id!=null)return l(r.id),n(a,r,r.id);let o=await x(a,e);return o?o.dir!==a&&r.autoCreate===!1?{kind:`parentWorkspaceExisted`,parentRoot:o.dir,currentRoot:a,marker:o.marker}:o.marker.remote&&e.confirmTrust&&await e.confirmTrust(o.marker.remote,o.dir)?w(o.marker,o.dir):C(o.marker,o.dir):r.autoCreate===!1?{kind:`uninitialized`,root:a}:n(a,r,e.genId())},async createLocal(t,r={}){return n(i(t),r,r.id??e.genId())}}}export{o as DEFAULT_WORKSPACE_KEY,m as GITIGNORE_FILE,f as MARKER_DIR,p as MARKER_FILE,h as OTTO_GITIGNORE,a as WS_KEY_PREFIX,l as assertValidWorkspaceId,g as buildGitignoreContent,T as createWorkspaceResolver,x as findMarker,d as genWorkspaceId,v as gitignorePath,c as isValidWorkspaceId,_ as markerPath,b as readMarker,u as workspaceKey,S as writeMarker};
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/id.ts","../src/marker.ts","../src/resolver.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\n/** `workspaceKey` 前缀。 */\nexport const WS_KEY_PREFIX = 'ws_'\n\n/**\n * 缺省 workspace key(无 workspace 身份标识时的回退分区键)。\n * 供服务端路由/会话池/持久化层统一引用——定义在此为单一真源,\n * 防止各消费方各自硬编码 `'default'` 字符串。\n */\nexport const DEFAULT_WORKSPACE_KEY = 'default'\n\n/**\n * workspace id 合法字符集与长度上限(RFC 规则 11):\n * id 会进 storage 分区 key + URL 路径段 + SQL,必须可控,防注入/路径穿越。\n */\nconst WS_ID_RE = /^[A-Za-z0-9_-]{1,64}$/\n\n/** id 是否合法(charset `[A-Za-z0-9_-]`,长度 1..64)。 */\nexport function isValidWorkspaceId(id: string): boolean {\n return WS_ID_RE.test(id)\n}\n\n/** 校验 id,不合法即抛(fail-closed;读 marker / bind 入口用)。 */\nexport function assertValidWorkspaceId(id: string): void {\n if (!isValidWorkspaceId(id)) {\n throw new Error(`invalid workspace id: ${JSON.stringify(id)} (expect ${WS_ID_RE})`)\n }\n}\n\n/**\n * 持久化第一维 key。id 不可变 ⇒ key 稳定。\n * 校验后拼接,杜绝非法 id 进入 storage/URL。\n */\nexport function workspaceKey(id: string): string {\n assertValidWorkspaceId(id)\n return WS_KEY_PREFIX + id\n}\n\n/**\n * 默认 id 生成器:客户端生成、全局唯一、不可变(RFC 规则 1)。\n * 用 uuid(去横线 → 32 hex),落在合法字符集内;远端 bind 时直接接受(不重分配)。\n * resolver 接受注入覆盖以便测试。\n */\nexport function genWorkspaceId(): string {\n return randomUUID().replace(/-/g, '')\n}\n","import { dirname, join, resolve } from 'node:path'\n\nimport { assertValidWorkspaceId } from './id'\n\nimport type { WorkspaceIO, WorkspaceMarker } from './types'\n\n/** workspace 本地足迹目录名。 */\nexport const MARKER_DIR = '.otto'\n/** 关联 marker 文件名。 */\nexport const MARKER_FILE = 'workspace.json'\n/** 生成的 gitignore 文件名。 */\nexport const GITIGNORE_FILE = '.gitignore'\n\n/**\n * `.otto/.gitignore` 模板(RFC-034 规则 3 / 决策 D4):\n * 身份(`workspace.json`)默认私有 + 易变忽略;项目知识(AGENTS.md/MEMORY.md/memory/)默认提交。\n * 团队共享 workspace 实体需显式 `git add -f .otto/workspace.json`。\n */\nexport const OTTO_GITIGNORE = `# otto workspace 本地足迹 — RFC-034 规则 3\n# 身份私有 + 易变忽略;项目知识(AGENTS.md / MEMORY.md / memory/)默认提交。\n# 团队共享 workspace 实体:显式 \\`git add -f .otto/workspace.json\\`。\nworkspace.json\ncache/\nlogs/\n*.tmp\n# Machine-specific memory (RFC-164 M164-1 D3) — local/ is never synced across devices, never committed.\nlocal/\n`\n\n/**\n * opt-in 远端记忆同步生效时(RFC-164 M164-2 D2/D4)追加的 gitignore 行——\n * auto-memory(`MEMORY.md`/`memory/`)与 `lessons.json` 改由远端 store 单一权威,\n * 不再随 git 提交;`AGENTS.md`(人工维护的项目知识)不受影响,继续走 git(D4 分离处理)。\n */\nconst MEMORY_SYNC_GITIGNORE_SUFFIX = `# Remote memory sync enabled (RFC-164 M164-2 D2/D4) — auto-memory/lessons\n# now have a single source of truth in the remote store; AGENTS.md still tracked by git.\nMEMORY.md\nmemory/\nlessons.json\n`\n\n/**\n * 按 `memorySyncEnabled` 状态生成 gitignore 内容——opt-in 生效时追加 D4 单一权威切换规则。\n * `writeMarker` 用此替代裸 `OTTO_GITIGNORE` 常量,使 marker 与 gitignore 内容随 opt-in 状态同步。\n *\n * 注意:`writeMarker` 会同时写入 gitignore 以保证 marker 的 `memorySyncEnabled` 字段与\n * gitignore 规则一致——用户自定义 gitignore 规则应写在 `.otto/.gitignore` 文件末尾。\n */\nexport function buildGitignoreContent(memorySyncEnabled: boolean): string {\n return memorySyncEnabled ? OTTO_GITIGNORE + MEMORY_SYNC_GITIGNORE_SUFFIX : OTTO_GITIGNORE\n}\n\n/** `<dir>/.otto/workspace.json` 绝对路径。 */\nexport function markerPath(dir: string): string {\n return join(dir, MARKER_DIR, MARKER_FILE)\n}\n\n/** `<dir>/.otto/.gitignore` 绝对路径。 */\nexport function gitignorePath(dir: string): string {\n return join(dir, MARKER_DIR, GITIGNORE_FILE)\n}\n\nfunction parseMarker(raw: string): WorkspaceMarker {\n const obj = JSON.parse(raw) as Record<string, unknown>\n const id = obj['id']\n const name = obj['name']\n const createdAt = obj['createdAt']\n if (typeof id !== 'string') throw new Error('workspace marker: missing id')\n assertValidWorkspaceId(id)\n if (typeof name !== 'string') throw new Error('workspace marker: missing name')\n if (typeof createdAt !== 'string') throw new Error('workspace marker: missing createdAt')\n const remoteRaw = obj['remote']\n let remote: WorkspaceMarker['remote']\n if (remoteRaw && typeof remoteRaw === 'object') {\n const url = (remoteRaw as Record<string, unknown>)['url']\n if (typeof url === 'string') remote = { url }\n }\n const memorySyncEnabled = obj['memorySyncEnabled'] === true ? true : undefined\n\n const result: {\n id: string\n name: string\n createdAt: string\n remote?: WorkspaceMarker['remote']\n memorySyncEnabled?: boolean\n } = { id, name, createdAt }\n if (remote) result.remote = remote\n if (memorySyncEnabled !== undefined) result.memorySyncEnabled = memorySyncEnabled\n return result\n}\n\n/** 读取某目录的 marker;不存在返回 null,内容非法则抛(fail-closed)。 */\nexport async function readMarker(dir: string, io: WorkspaceIO): Promise<WorkspaceMarker | null> {\n const raw = await io.readFile(markerPath(dir))\n if (raw == null) return null\n return parseMarker(raw)\n}\n\n/** 命中结果:marker 所在目录 + 解析内容。 */\nexport interface MarkerHit {\n readonly dir: string\n readonly marker: WorkspaceMarker\n}\n\n/**\n * 从 `startCwd` 向上 walk 找最近的 `.otto/workspace.json`(类比 git 找 `.git`)。\n * 命中即返回;到文件系统根仍未命中返回 null。\n */\nexport async function findMarker(startCwd: string, io: WorkspaceIO): Promise<MarkerHit | null> {\n let dir = resolve(startCwd)\n for (;;) {\n const marker = await readMarker(dir, io)\n if (marker) return { dir, marker }\n const parent = dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n}\n\n/**\n * 在 `<dir>/.otto/` 写入 marker + gitignore(内容随 `marker.memorySyncEnabled` 状态调整,见 D4)。\n *\n * RFC-303 D7(M2 T303-05):`exclusive:true` 时用 `io.writeFileExclusive`(O_EXCL 语义)写\n * marker 本体——若目标已存在则不覆盖、返回 `false`(调用方应改为读取已存在的 marker)。\n * gitignore 始终用普通覆盖写(无并发创建互斥语义需求,其内容由 `memorySyncEnabled` 派生,\n * 覆盖写是安全的幂等操作)。`io` 未实现 `writeFileExclusive` 时退化为普通覆盖写(向后兼容,\n * 老 IO 实现不受影响,但不具备本次修复的互斥保证——调用方应尽快升级 IO 实现)。\n */\nexport async function writeMarker(\n dir: string,\n marker: WorkspaceMarker,\n io: WorkspaceIO,\n options?: { exclusive?: boolean },\n): Promise<boolean> {\n assertValidWorkspaceId(marker.id)\n const path = markerPath(dir)\n const content = JSON.stringify(marker, null, 2) + '\\n'\n\n if (options?.exclusive && io.writeFileExclusive) {\n const created = await io.writeFileExclusive(path, content)\n if (!created) return false\n } else {\n await io.writeFile(path, content)\n }\n\n await io.writeFile(gitignorePath(dir), buildGitignoreContent(marker.memorySyncEnabled === true))\n return true\n}\n","import { basename, resolve } from 'node:path'\n\nimport { assertValidWorkspaceId, workspaceKey } from './id'\nimport { findMarker, readMarker, writeMarker } from './marker'\n\nimport type { WorkspaceIO, WorkspaceMarker, WorkspaceRef } from './types'\n\n/**\n * `resolveLocal` 在 `autoCreate:false` 模式下,向上递归在**父级目录**命中 marker 时的返回值。\n * 调用方(交互态 CLI)应提示用户选择\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n * 而非静默采用父级身份(防止子目录 workspace 被意外吞入父 workspace)。\n */\nexport interface ParentWorkspaceExisted {\n readonly kind: 'parentWorkspaceExisted'\n /** marker 所在目录(父级 workspace 根)。 */\n readonly parentRoot: string\n /** 用户当前工作目录(pwd)。 */\n readonly currentRoot: string\n /** 父级 marker 内容。 */\n readonly marker: WorkspaceMarker\n}\n\n/** `resolveLocal` 可选项。 */\nexport interface ResolveLocalOptions {\n /** `--id` 覆盖:在此目录显式绑定指定 workspace(一目录多 workspace,DevPod 模式)。 */\n id?: string\n /** 新建时的名字;缺省取目录名。 */\n name?: string\n /** 新建时的 createdAt;缺省取 `io.now()`。 */\n createdAt?: string\n /**\n * marker 未命中时是否自动创建。缺省 `true`(旧行为,headless/`--id` 场景零感知)。\n * 传 `false`(交互态新流程):未命中时不写文件,返回 `{kind:'uninitialized', root}`,\n * 调用方需在用户确认(信任门)后调 `createLocal()` 才真正建立身份。对 `opts.id` 显式给定\n * 的场景无效(该分支必然走 create,无歧义可言)。\n */\n autoCreate?: boolean\n}\n\nfunction localRef(marker: WorkspaceMarker, root: string): WorkspaceRef {\n return {\n key: workspaceKey(marker.id),\n id: marker.id,\n backend: 'local',\n root,\n ...(marker.memorySyncEnabled ? { memorySyncEnabled: true } : {}),\n }\n}\n\nfunction remoteRef(marker: WorkspaceMarker, root: string): WorkspaceRef {\n return {\n key: workspaceKey(marker.id),\n id: marker.id,\n backend: 'remote',\n root,\n ...(marker.remote ? { remote: marker.remote } : {}),\n ...(marker.memorySyncEnabled ? { memorySyncEnabled: true } : {}),\n }\n}\n\n/**\n * `resolveLocal` 未命中 marker 时的结果(`autoCreate:false` 专用,见下方 opts)——\n * 调用方须显式 `createLocal()` 才会真正写 marker(本次修复:交互态先过信任门,用户确认后\n * 才建立 workspace 身份,防止 `.otto/workspace.json` 缺失时静默生成新随机 id 致旧会话孤儿化)。\n */\nexport interface UninitializedWorkspace {\n readonly kind: 'uninitialized'\n readonly root: string\n}\n\n/** workspace 身份解析器(注入式 IO,纯可测)。 */\nexport interface WorkspaceResolver {\n /**\n * 本地 resolve-or-create:\n * - `opts.id` 在场 → 显式绑定该 id(写/刷新 marker),忽略 autoCreate(DevPod/`--id` 模式无歧义)。\n * - 否则向上 walk 找 marker:\n * - 命中且 `hit.dir === cwd` → 当前目录已有 workspace 身份 → L/R(取决于 remote/trust)。\n * - 命中且 `hit.dir !== cwd` + `autoCreate:false` → 返回 `ParentWorkspaceExisted`,\n * 调用层应提示用户选择\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n * 防止子目录 workspace 被静默吞入父 workspace。\n * - 命中且 `hit.dir !== cwd` + `autoCreate` 未传/`true`(headless)→ 静默采用父级。\n * - 未命中:`opts.autoCreate !== false`(缺省/headless 等旧行为)→ 在 cwd 生成 id + 写 marker → L;\n * `opts.autoCreate === false`(交互态新流程)→ 返回 `UninitializedWorkspace`,不写文件,\n * 调用方需在用户确认(信任门)后调 `createLocal()` 才真正建立身份。\n */\n resolveLocal(\n cwd: string,\n opts?: ResolveLocalOptions,\n ): Promise<WorkspaceRef | UninitializedWorkspace | ParentWorkspaceExisted>\n /**\n * 显式创建 L 形态 workspace(写 marker)——`resolveLocal({autoCreate:false})` 返回\n * `uninitialized` 后,调用方(信任确认后)调此方法完成创建。与 resolveLocal 内部的\n * 自动创建分支同一实现,只是从\"自动触发\"变为\"显式调用\"。\n */\n createLocal(root: string, opts?: ResolveLocalOptions): Promise<WorkspaceRef>\n}\n\nexport function createWorkspaceResolver(io: WorkspaceIO): WorkspaceResolver {\n /**\n * RFC-303 D7(M2 T303-05):并发首启创建互斥——此前两个进程在同一空目录首启,\n * `opts.id` 缺省时各自 `io.genId()` 生成不同随机 id、各自 `writeMarker` 覆盖写,\n * 后写者覆盖先写者,两进程持有不同 `workspace_key` 分区(会话列表互相隔离)。\n * 修复:`opts.id` 未显式给定时,用 `writeMarker(..., {exclusive:true})`(O_EXCL\n * 语义)尝试创建——若目标已存在(`writeMarker` 返回 `false`,说明另一进程刚\n * 抢先创建成功),改为读取该 marker 复用其 id,而非用自己生成的新 id 覆盖。\n * `opts.id` 显式给定的路径(DevPod/`--id` 模式)不受影响,语义上本就无歧义\n * (用户明确指定了 id,不存在\"谁先谁后\"的竞态问题)。\n */\n async function create(\n root: string,\n opts: ResolveLocalOptions,\n id: string,\n ): Promise<WorkspaceRef> {\n const marker: WorkspaceMarker = {\n id,\n name: opts.name ?? (basename(root) || 'workspace'),\n createdAt: opts.createdAt ?? io.now(),\n }\n if (opts.id == null) {\n const created = await writeMarker(root, marker, io, { exclusive: true })\n if (!created) {\n // 独占写失败——目标已存在(另一进程抢先创建),读取复用其 id。\n const existing = await readMarker(root, io)\n if (existing) return localRef(existing, root)\n // 理论不可达(写入失败即意味着文件存在,读取应该成功);若发生说明\n // 极端竞态下文件又被删除,退化为覆盖写保底(不阻断用户)。\n await writeMarker(root, marker, io)\n }\n } else {\n await writeMarker(root, marker, io)\n }\n return localRef(marker, root)\n }\n\n return {\n async resolveLocal(cwd, opts = {}) {\n const root = resolve(cwd)\n\n if (opts.id != null) {\n assertValidWorkspaceId(opts.id)\n return create(root, opts, opts.id)\n }\n\n const hit = await findMarker(root, io)\n if (hit) {\n // 父级目录命中 marker(非当前目录)且 autoCreate:false →\n // 返回信号让调用层决定\"使用上级 workspace\"还是\"在当前目录创建新 workspace\",\n // 而非静默采用父级身份(防止子项目被意外吞入父 workspace)。\n // headless 态(autoCreate 缺省 true)不受影响——静默采用父级,保持旧行为。\n if (hit.dir !== root && opts.autoCreate === false) {\n return {\n kind: 'parentWorkspaceExisted' as const,\n parentRoot: hit.dir,\n currentRoot: root,\n marker: hit.marker,\n }\n }\n if (hit.marker.remote) {\n const trusted = io.confirmTrust\n ? await io.confirmTrust(hit.marker.remote, hit.dir)\n : false\n if (trusted) return remoteRef(hit.marker, hit.dir)\n return localRef(hit.marker, hit.dir)\n } else {\n return localRef(hit.marker, hit.dir)\n }\n }\n\n if (opts.autoCreate === false) {\n return { kind: 'uninitialized', root }\n }\n return create(root, opts, io.genId())\n },\n\n async createLocal(root, opts = {}) {\n return create(resolve(root), opts, opts.id ?? io.genId())\n },\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x-otto/workspace",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org",
|
|
19
|
+
"tag": "alpha"
|
|
20
|
+
},
|
|
21
|
+
"private": false,
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsdown",
|
|
24
|
+
"typecheck:project": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"clean": "rm -rf dist"
|
|
27
|
+
}
|
|
28
|
+
}
|