@reed-io/dsh-workspace-move 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.
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Registry-level primitives shared by plan, execute and undo: entity snapshots,
3
+ * session-header index updates, and the two structural writes the public
4
+ * registry API cannot express (retargeting one workspace's path, merging a
5
+ * moved session group back into its old registration).
6
+ *
7
+ * These helpers operate on the registry's own serialized operation queue so they
8
+ * never interleave with a concurrent create/delete/attach from other plugins.
9
+ * @module @reed-io/dsh-workspace-move
10
+ */
11
+ import { samePath } from "./fsx.js";
12
+
13
+ /**
14
+ * Serialize one operation behind the registry's own write queue.
15
+ * @param registry - workspace registry service.
16
+ * @param operation - work to run inside the queue.
17
+ * @returns the operation result.
18
+ */
19
+ /**
20
+ * Reach the registry's durable record table.
21
+ * @param registry - workspace registry service.
22
+ * @returns the storage-domain table, or undefined when the registry is not ready.
23
+ */
24
+ export function tableOf(registry) {
25
+ const table = registry?.table;
26
+ return typeof table === "function" ? table.call(registry) : table;
27
+ }
28
+
29
+ export function enqueue(registry, operation) {
30
+ if (typeof registry.enqueueOperation === "function") return registry.enqueueOperation(operation);
31
+ return operation();
32
+ }
33
+
34
+ /**
35
+ * Read one workspace's stored record without touching live entity state.
36
+ * @param registry - workspace registry service.
37
+ * @param id - workspace id.
38
+ * @returns the stored record, or undefined.
39
+ */
40
+ export function recordOf(registry, id) {
41
+ const table = tableOf(registry);
42
+ return table?.get(id);
43
+ }
44
+
45
+ /**
46
+ * Snapshot one workspace registration for rollback and review.
47
+ * @param registry - workspace registry service.
48
+ * @param id - workspace id.
49
+ * @returns a plain snapshot.
50
+ */
51
+ export function snapshotWorkspace(registry, id) {
52
+ const record = recordOf(registry, id);
53
+ if (record === undefined) return undefined;
54
+ return {
55
+ id,
56
+ path: record.path,
57
+ title: record.title,
58
+ sessionIds: [...record.sessionIds],
59
+ createdAt: record.createdAt,
60
+ updatedAt: record.updatedAt
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Every session id the registry knows about, live or stored.
66
+ * @param registry - workspace registry service.
67
+ * @returns session ids.
68
+ */
69
+ export function knownSessionIds(registry) {
70
+ return [...(registry.headers?.keys() ?? [])];
71
+ }
72
+
73
+ /**
74
+ * Make the live header index agree with a session whose header now carries `cwd`.
75
+ * @param registry - workspace registry service.
76
+ * @param id - session id.
77
+ * @param cwd - the working directory the stored header now records.
78
+ * @returns nothing.
79
+ */
80
+ export function indexSessionAt(registry, id, cwd) {
81
+ const headers = registry.headers;
82
+ if (headers !== undefined) {
83
+ const current = headers.get(id);
84
+ if (current !== undefined) headers.set(id, { ...current, cwd });
85
+ }
86
+ registry.sessionPaths?.set(id, cwd);
87
+ registry.missingSessionPaths?.delete(id);
88
+ registry.invalidSessionPaths?.delete(id);
89
+ }
90
+
91
+ /**
92
+ * Drop one session from the live header index, mirroring an unmounted session.
93
+ * @param registry - workspace registry service.
94
+ * @param id - session id.
95
+ * @returns nothing.
96
+ */
97
+ export function forgetSession(registry, id) {
98
+ registry.headers?.delete(id);
99
+ registry.sessionPaths?.delete(id);
100
+ registry.missingSessionPaths?.delete(id);
101
+ registry.invalidSessionPaths?.delete(id);
102
+ }
103
+
104
+ /**
105
+ * Retarget one existing registration to a new directory without recreating it:
106
+ * the record keeps its id, title, creation stamp and session order, and the
107
+ * entity cache is rebuilt in the registry's durable order.
108
+ * @param registry - workspace registry service.
109
+ * @param id - workspace id.
110
+ * @param path - canonical new path.
111
+ * @param options - pass `{ rebuild: false }` to skip the entity rebuild.
112
+ * @returns nothing.
113
+ */
114
+ export async function retargetWorkspace(registry, id, path, options = {}) {
115
+ const write = async () => {
116
+ const record = recordOf(registry, id);
117
+ if (record === undefined) throw new Error(`workspace '${id}' is no longer registered`);
118
+ if (record.path !== path) {
119
+ await tableOf(registry).update(id, (current) => ({ ...current, path, updatedAt: new Date().toISOString() }));
120
+ }
121
+ for (const sessionId of record.sessionIds) indexSessionAt(registry, sessionId, path);
122
+ if (options.rebuild !== false) registry.rebuildEntities?.();
123
+ };
124
+ return write();
125
+ }
126
+
127
+ /**
128
+ * Detach one session from every other registration that still claims it.
129
+ * @param registry - workspace registry service.
130
+ * @param sessionId - session id.
131
+ * @param keepId - the registration that should keep it.
132
+ * @returns nothing.
133
+ */
134
+ export async function detachFromOthers(registry, sessionId, keepId) {
135
+ for (const entity of registry.list()) {
136
+ if (entity.id === keepId) continue;
137
+ const ids = recordOf(registry, entity.id)?.sessionIds ?? [];
138
+ if (!ids.includes(sessionId)) continue;
139
+ await entity.detachSession(sessionId).catch(() => {});
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Attach sessions to one registration, preserving their relative order.
145
+ * @param registry - workspace registry service.
146
+ * @param entity - target workspace entity.
147
+ * @param sessionIds - desired session order.
148
+ * @returns nothing.
149
+ */
150
+ export async function attachOrdered(registry, entity, sessionIds) {
151
+ const existing = recordOf(registry, entity.id)?.sessionIds ?? [];
152
+ const missing = sessionIds.filter((id) => !existing.includes(id));
153
+ for (const sessionId of missing) await detachFromOthers(registry, sessionId, entity.id);
154
+ for (const sessionId of [...missing].reverse()) await entity.attachSession(sessionId);
155
+ const order = recordOf(registry, entity.id)?.sessionIds ?? [];
156
+ let previous;
157
+ for (const sessionId of order) {
158
+ if (!sessionIds.includes(sessionId)) continue;
159
+ if (previous !== undefined) await entity.insertSessionBefore(sessionId, previous);
160
+ previous = sessionId;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Report the workspace whose registration already owns a path, if any.
166
+ * @param registry - workspace registry service.
167
+ * @param path - canonical path.
168
+ * @param exceptId - registration to ignore.
169
+ * @returns the owning workspace id, or undefined.
170
+ */
171
+ export function ownerOfPath(registry, path, exceptId) {
172
+ for (const entity of registry.list()) {
173
+ if (entity.id === exceptId) continue;
174
+ if (samePath(entity.path, path)) return entity.id;
175
+ }
176
+ return undefined;
177
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Per-session work of a move: locate a session's artifact, take it down before
3
+ * the workspace folder travels, and write it back at the path its new working
4
+ * directory implies.
5
+ *
6
+ * Taking every session down first is what makes a half-moved session impossible
7
+ * to observe: no live handle points at a folder that is being renamed, and no
8
+ * index entry can claim a path that has not been written yet.
9
+ * @module @reed-io/dsh-workspace-move
10
+ */
11
+ import { copyFile, mkdir, readdir, rename, rm } from "node:fs/promises";
12
+ import { basename, dirname, join } from "node:path";
13
+ import { copyArtifactWithCwd, isZstdFramed, readArtifactHeader, scanFrames, transcodeArtifactWithCwd } from "./artifact.js";
14
+ import { exists } from "./fsx.js";
15
+ import { logPath, projectDir, sessionDir } from "./paths.js";
16
+
17
+ /**
18
+ * Resolve the persistence root and compression the backend uses.
19
+ * @param persistence - session persistence service.
20
+ * @returns the root directory and the raw compression setting.
21
+ */
22
+ export function persistenceInfo(persistence) {
23
+ const root = persistence.root ?? persistence.config?.root;
24
+ if (typeof root !== "string" || root === "") throw new Error("session persistence root is unknown");
25
+ const raw = persistence.compression ?? persistence.config?.compression ?? "zstd";
26
+ return { root, compression: raw === "none" || raw === undefined ? "none" : "zstd" };
27
+ }
28
+
29
+ /**
30
+ * Compare two paths the way the running platform does.
31
+ * @param value - path to normalize.
32
+ * @returns a comparison key.
33
+ */
34
+ function comparisonKey(value) {
35
+ const text = value.replace(/[\\/]+$/u, "");
36
+ return process.platform === "win32" ? text.toLowerCase() : text;
37
+ }
38
+
39
+ /**
40
+ * Whether one path is the parent itself or lives underneath it.
41
+ * @param child - candidate path.
42
+ * @param parent - containing path.
43
+ * @returns true when child is inside parent.
44
+ */
45
+ export function isWithin(child, parent) {
46
+ const inner = comparisonKey(child);
47
+ const outer = comparisonKey(parent);
48
+ if (inner === outer) return true;
49
+ if (inner.startsWith(`${outer}\\`)) return true;
50
+ return inner.startsWith(`${outer}/`);
51
+ }
52
+
53
+ /**
54
+ * Locate one stored session: its artifact, the folder holding it, and its header.
55
+ * @param ctx - Host context.
56
+ * @param id - session id.
57
+ * @returns the location, or undefined when the session is not stored.
58
+ */
59
+ export async function locateSession(ctx, id) {
60
+ const persistence = ctx.sessionPersistence;
61
+ if (persistence === undefined) return undefined;
62
+ const { root, compression } = persistenceInfo(persistence);
63
+ const snapshot = await persistence.stat(id).catch(() => undefined);
64
+ if (snapshot === undefined || snapshot.location === undefined) return undefined;
65
+ const header = snapshot.header;
66
+ const path = typeof snapshot.location.path === "string" ? snapshot.location.path : logPath(root, header.cwd, id, header.version ?? 4, compression);
67
+ if (!isWithin(path, root)) return undefined;
68
+ return { id, path, dir: dirname(path), header, root, compression };
69
+ }
70
+
71
+ /**
72
+ * Collect the sessions one move carries: the ones the workspace lists, plus any
73
+ * stored session whose header records a working directory inside the folder.
74
+ * @param ctx - Host context.
75
+ * @param workspace - the registry record being moved.
76
+ * @returns located sessions, listed ones first.
77
+ */
78
+ export async function collectSessions(ctx, workspace) {
79
+ const located = [];
80
+ const seen = new Set();
81
+ for (const id of workspace.sessionIds) {
82
+ const found = await locateSession(ctx, id).catch(() => undefined);
83
+ if (found === undefined) continue;
84
+ located.push({ ...found, listed: true });
85
+ seen.add(id);
86
+ }
87
+ const persistence = ctx.sessionPersistence;
88
+ if (persistence !== undefined) {
89
+ const { root, compression } = persistenceInfo(persistence);
90
+ const snapshots = await persistence.list().catch(() => []);
91
+ for (const snapshot of snapshots) {
92
+ const header = snapshot.header;
93
+ const id = header?.id;
94
+ const cwd = header?.cwd;
95
+ if (typeof id !== "string" || typeof cwd !== "string" || seen.has(id)) continue;
96
+ if (!isWithin(cwd, workspace.path)) continue;
97
+ const path = logPath(root, cwd, id, header.version ?? 4, compression);
98
+ located.push({ id, path, dir: dirname(path), header, root, compression, listed: false });
99
+ seen.add(id);
100
+ }
101
+ }
102
+ return located;
103
+ }
104
+
105
+ /**
106
+ * Take one session down for the duration of the folder move: its folder is
107
+ * renamed into the plugin's backup area, so nothing can read a half-written log.
108
+ * @param backupRoot - plugin-owned backup directory.
109
+ * @param tag - unique label for this move.
110
+ * @param entry - located session.
111
+ * @returns where the archived folder now lives.
112
+ */
113
+ export async function takeDownSession(backupRoot, tag, entry) {
114
+ const target = join(backupRoot, tag, basename(entry.dir));
115
+ await mkdir(dirname(target), { recursive: true });
116
+ await rename(entry.dir, target);
117
+ return target;
118
+ }
119
+
120
+ /**
121
+ * Delete the backup area of one move.
122
+ * @param backupRoot - plugin-owned backup directory.
123
+ * @param tag - unique label for this move.
124
+ * @returns nothing.
125
+ */
126
+ export async function dropBackup(backupRoot, tag) {
127
+ await rm(join(backupRoot, tag), { recursive: true, force: true }).catch(() => {});
128
+ }
129
+
130
+ /**
131
+ * Find the log inside one archived session folder.
132
+ * @param folder - archived session folder.
133
+ * @returns the log path and the log filename.
134
+ */
135
+ async function logIn(folder) {
136
+ const entries = await readdir(folder, { withFileTypes: true });
137
+ const log = entries.find((entry) => entry.isFile() && entry.name.startsWith("session.") && entry.name.includes(".jsonl"));
138
+ if (log === undefined) throw new Error(`archived session '${folder}' holds no log`);
139
+ return { path: join(folder, log.name), name: log.name };
140
+ }
141
+
142
+ /**
143
+ * Write one taken-down session at the path its new working directory implies,
144
+ * keeping every payload byte, then move any companion files next to it.
145
+ * @param entry - located session (original location).
146
+ * @param archived - archived session folder.
147
+ * @param cwd - the new working directory.
148
+ * @returns the log path at the new location.
149
+ */
150
+ export async function writeBackSession(entry, archived, cwd) {
151
+ const log = await logIn(archived);
152
+ const targetDir = sessionDir(entry.root, cwd, entry.id);
153
+ await mkdir(targetDir, { recursive: true });
154
+ const target = join(targetDir, log.name);
155
+ await rm(target, { force: true }).catch(() => {});
156
+ await copyArtifactWithCwd(log.path, target, cwd, entry.id);
157
+ const companions = await readdir(archived, { withFileTypes: true });
158
+ for (const companion of companions) {
159
+ if (companion.name === log.name) continue;
160
+ const from = join(archived, companion.name);
161
+ const to = join(targetDir, companion.name);
162
+ await rm(to, { recursive: true, force: true }).catch(() => {});
163
+ await renameOrCopy(from, to);
164
+ }
165
+ const header = await readArtifactHeader(target);
166
+ if (header.cwd !== cwd) throw new Error(`session '${entry.id}' did not adopt cwd '${cwd}'`);
167
+ return target;
168
+ }
169
+
170
+ /**
171
+ * Rename a companion file or folder, falling back to a copy across devices.
172
+ * @param from - source path.
173
+ * @param to - destination path.
174
+ * @returns nothing.
175
+ */
176
+ async function renameOrCopy(from, to) {
177
+ try {
178
+ await rename(from, to);
179
+ } catch (error) {
180
+ if (error?.code !== "EXDEV") throw error;
181
+ await mkdir(dirname(to), { recursive: true });
182
+ await moveAcrossDevices(from, to);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Copy one path tree across devices and drop the source.
188
+ * @param from - source path.
189
+ * @param to - destination path.
190
+ * @returns nothing.
191
+ */
192
+ async function moveAcrossDevices(from, to) {
193
+ const stat = await (await import("node:fs/promises")).stat(from);
194
+ if (!stat.isDirectory()) {
195
+ await copyFile(from, to);
196
+ await rm(from, { force: true });
197
+ return;
198
+ }
199
+ await mkdir(to, { recursive: true });
200
+ for (const child of await readdir(from)) await moveAcrossDevices(join(from, child), join(to, child));
201
+ await rm(from, { recursive: true, force: true });
202
+ }
203
+
204
+ /**
205
+ * Put one session back exactly where it was, with its original working directory.
206
+ * Accepts both the original folder and the backup copy as the source.
207
+ * @param entry - located session describing the original location.
208
+ * @param cwd - the working directory to record in the header.
209
+ * @param fallbackFolder - backup folder holding the intact original, if any.
210
+ * @returns nothing.
211
+ */
212
+ export async function restoreSession(entry, cwd, fallbackFolder) {
213
+ const usable = fallbackFolder !== undefined && (await exists(fallbackFolder)) && (await logIn(fallbackFolder).then(() => true).catch(() => false));
214
+ if (!usable) fallbackFolder = undefined;
215
+ if (fallbackFolder !== undefined) {
216
+ await rm(entry.dir, { recursive: true, force: true }).catch(() => {});
217
+ await mkdir(dirname(entry.dir), { recursive: true });
218
+ await renameOrCopy(fallbackFolder, entry.dir);
219
+ }
220
+ const log = await logIn(entry.dir);
221
+ await normalizeLog(log.path, cwd, entry.id);
222
+ return;
223
+ }
224
+
225
+ /**
226
+ * Rewrite one log's header in place, re-encoding every frame only when the log
227
+ * cannot be copied verbatim.
228
+ * @param path - log path.
229
+ * @param cwd - working directory to record.
230
+ * @param id - expected session id.
231
+ * @returns nothing.
232
+ */
233
+ async function normalizeLog(path, cwd, id) {
234
+ const header = await readArtifactHeader(path);
235
+ if (header.cwd === cwd) return;
236
+ const bytes = await (await import("node:fs/promises")).readFile(path);
237
+ const frames = isZstdFramed(bytes) ? scanFrames(bytes).frames.length : 1;
238
+ const stage = join(dirname(path), `.dshwm-transcode-${process.pid}-${Date.now()}`);
239
+ if (frames <= 2) {
240
+ await copyArtifactWithCwd(path, stage, cwd, id);
241
+ } else {
242
+ await transcodeArtifactWithCwd(path, stage, cwd, id);
243
+ }
244
+ await rm(path, { force: true });
245
+ await rename(stage, path);
246
+ }
247
+
248
+ /**
249
+ * Every stored session folder whose working directory lives under one path.
250
+ * @param ctx - Host context.
251
+ * @param path - the folder about to move.
252
+ * @returns located sessions.
253
+ */
254
+ export async function sessionsUnder(ctx, path) {
255
+ const persistence = ctx.sessionPersistence;
256
+ if (persistence === undefined) return [];
257
+ const snapshots = await persistence.list().catch(() => []);
258
+ const { root, compression } = persistenceInfo(persistence);
259
+ const found = [];
260
+ for (const snapshot of snapshots) {
261
+ const header = snapshot.header;
262
+ if (typeof header?.cwd !== "string" || !isWithin(header.cwd, path)) continue;
263
+ found.push({ id: header.id, header, cwd: header.cwd, root, compression, project: projectDir(root, header.cwd) });
264
+ }
265
+ return found;
266
+ }
package/locale/en.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "meta": {
3
+ "meta": {
4
+ "title": "Workspace Move",
5
+ "description": "Move a workspace directory — its subfolders, nested workspaces and sessions — to a new location, with rename, rollback and undo."
6
+ }
7
+ },
8
+ "meta.title": "Workspace Move",
9
+ "meta.description": "Move a workspace directory — its subfolders, nested workspaces and sessions — to a new location, with rename, rollback and undo.",
10
+ "menu.move": "Move workspace…",
11
+ "menu.undo": "Undo last move",
12
+ "dialog.title": "Move workspace",
13
+ "dialog.source": "Current location",
14
+ "dialog.target": "Destination parent folder",
15
+ "dialog.folderName": "Folder name",
16
+ "dialog.folderHint": "The workspace lands in the chosen folder under this name. Changing the name renames the workspace.",
17
+ "dialog.destination": "New location",
18
+ "dialog.up": "Up",
19
+ "dialog.home": "Home",
20
+ "dialog.newFolder": "New folder",
21
+ "dialog.newFolderPrompt": "New folder name",
22
+ "dialog.pickHere": "Use this folder",
23
+ "dialog.confirm": "Move",
24
+ "dialog.cancel": "Cancel",
25
+ "dialog.moving": "Moving…",
26
+ "dialog.loading": "Loading…",
27
+ "dialog.emptyFolder": "No subfolders here",
28
+ "dialog.invalidName": "Enter a folder name without slashes.",
29
+ "dialog.targetExists": "That name already exists there and is not empty. Pick another name.",
30
+ "dialog.sameLocation": "The workspace is already there. Pick another place or change the name.",
31
+ "dialog.insideSelf": "A workspace cannot be moved inside its own folder.",
32
+ "dialog.ancestor": "That place is above the workspace's current path. Pick another one.",
33
+ "dialog.missingFolder": "That folder does not exist. Refresh and try again.",
34
+ "dialog.unknownWorkspace": "That workspace is no longer listed. Reopen the menu.",
35
+ "toast.moved": "Workspace moved.",
36
+ "toast.undone": "Move undone.",
37
+ "toast.failed": "The action failed.",
38
+ "running.title": "Stop these sessions first",
39
+ "running.hint": "Sessions with running work cannot be moved. Stop them and try again.",
40
+ "undo.title": "Undo last move",
41
+ "undo.body": "Restores the workspace and its sessions to the previous location.",
42
+ "undo.confirm": "Undo",
43
+ "undo.none": "Nothing to undo."
44
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "meta": {
3
+ "meta": {
4
+ "title": "移动工作区",
5
+ "description": "把工作区目录、其子目录、其下的子工作区和其中的会话整体搬到新位置,支持重命名、失败回滚与撤销。"
6
+ }
7
+ },
8
+ "meta.title": "移动工作区",
9
+ "meta.description": "把工作区目录、其子目录、其下的子工作区和其中的会话整体搬到新位置,支持重命名、失败回滚与撤销。",
10
+ "menu.move": "移动工作区…",
11
+ "menu.undo": "撤销上次移动",
12
+ "dialog.title": "移动工作区",
13
+ "dialog.source": "当前位置",
14
+ "dialog.target": "目标上级文件夹",
15
+ "dialog.folderName": "文件夹名称",
16
+ "dialog.folderHint": "工作区会以此名称放进所选文件夹;改名即重命名工作区。",
17
+ "dialog.destination": "移动后位置",
18
+ "dialog.up": "上一层",
19
+ "dialog.home": "主目录",
20
+ "dialog.newFolder": "新建文件夹",
21
+ "dialog.newFolderPrompt": "新文件夹名称",
22
+ "dialog.pickHere": "选择此文件夹",
23
+ "dialog.confirm": "移动",
24
+ "dialog.cancel": "取消",
25
+ "dialog.moving": "正在移动…",
26
+ "dialog.loading": "加载中…",
27
+ "dialog.emptyFolder": "这里没有子文件夹",
28
+ "dialog.invalidName": "请填写不含斜杠的文件夹名称。",
29
+ "dialog.targetExists": "目标文件夹里已有同名项且非空,请换个名字。",
30
+ "dialog.sameLocation": "工作区已经在这个位置了,换个目标或改个名字。",
31
+ "dialog.insideSelf": "不能把工作区搬到它自己的文件夹里面。",
32
+ "dialog.ancestor": "目标在工作区当前路径的上层,请换一个位置。",
33
+ "dialog.missingFolder": "这个文件夹不存在,请刷新后重试。",
34
+ "dialog.unknownWorkspace": "这个工作区已经不在列表里了,请重新打开菜单。",
35
+ "toast.moved": "工作区已移动。",
36
+ "toast.undone": "已撤销移动。",
37
+ "toast.failed": "操作失败。",
38
+ "running.title": "请先停止这些会话",
39
+ "running.hint": "有正在运行的会话不能移动,停止后再试。",
40
+ "undo.title": "撤销上次移动",
41
+ "undo.body": "将把工作区及其会话恢复到原来的位置。",
42
+ "undo.confirm": "撤销",
43
+ "undo.none": "没有可撤销的移动。"
44
+ }
package/package.json CHANGED
@@ -1,6 +1,93 @@
1
1
  {
2
2
  "name": "@reed-io/dsh-workspace-move",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Move a DSH workspace — its folder tree, nested workspaces and sessions — to a new location from the sidebar, with rename, rollback and undo.",
5
+ "author": "reed-io",
6
+ "homepage": "https://github.com/ame-code/dsh-workspace-move#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ame-code/dsh-workspace-move.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ame-code/dsh-workspace-move/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "./lib/index.js",
16
+ "exports": {
17
+ ".": "./lib/index.js",
18
+ "./client": "./lib/client.js",
19
+ "./icon": "./icon/icon.svg",
20
+ "./icon/icon.svg": "./icon/icon.svg",
21
+ "./locale/*.json": "./locale/*.json",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib",
26
+ "src",
27
+ "locale",
28
+ "icon",
29
+ "cordis.patch.yml",
30
+ "CHANGELOG.md",
31
+ "README.md",
32
+ "README.zh-CN.md",
33
+ "LICENSE"
34
+ ],
35
+ "keywords": [
36
+ "dsh",
37
+ "deepseek-harness",
38
+ "dsh-plugin",
39
+ "workspace",
40
+ "move",
41
+ "session"
42
+ ],
43
+ "license": "MIT",
44
+ "engines": {
45
+ "node": "^22.19.0 || >=24.0.0",
46
+ "dsh": ">=0.2.0-rc.1 <0.3.0-0"
47
+ },
48
+ "peerDependencies": {
49
+ "@deepseek-ai/cordis": ">=4.0.1 <5.0.0",
50
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.1 <0.3.0-0",
51
+ "@deepseek-ai/dsh-workspace": ">=0.2.0-rc.1 <0.3.0-0",
52
+ "@deepseek-ai/dsh-session-persistence": ">=0.2.0-rc.1 <0.3.0-0",
53
+ "@deepseek-ai/dsh-host-webserver": ">=0.2.0-rc.1 <0.3.0-0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@deepseek-ai/cordis": {
57
+ "optional": true
58
+ },
59
+ "@deepseek-ai/dsh-tools": {
60
+ "optional": true
61
+ },
62
+ "@deepseek-ai/dsh-workspace": {
63
+ "optional": true
64
+ },
65
+ "@deepseek-ai/dsh-session-persistence": {
66
+ "optional": true
67
+ },
68
+ "@deepseek-ai/dsh-host-webserver": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "dsh": {
73
+ "bundle": {
74
+ "patch": "./cordis.patch.yml"
75
+ },
76
+ "client": {
77
+ "platform": "web",
78
+ "immediately": false,
79
+ "inject": [
80
+ "@deepseek-ai/dsh-client-ui-workspace"
81
+ ]
82
+ }
83
+ },
84
+ "scripts": {
85
+ "build": "node scripts/build-client.mjs",
86
+ "check": "node scripts/build-client.mjs && node scripts/check-client.mjs",
87
+ "test": "node test/unit.mjs",
88
+ "prepack": "node scripts/build-client.mjs"
89
+ },
90
+ "publishConfig": {
91
+ "access": "public"
92
+ }
93
+ }