dsh-session-ops 0.1.4 → 0.2.1
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 +16 -11
- package/README.zh.md +28 -40
- package/cordis.patch.yml +1 -1
- package/docs/DETAIL.md +9 -7
- package/docs/DETAIL.zh.md +20 -14
- package/lib/admin.js +723 -774
- package/lib/client.js +1767 -2065
- package/lib/engine.js +181 -182
- package/lib/index.js +125 -166
- package/lib/memory.js +137 -132
- package/lib/move.js +630 -644
- package/lib/sessionlog.js +2 -18
- package/lib/store/frames.js +183 -170
- package/lib/store/host.js +41 -42
- package/lib/store/http.js +64 -59
- package/lib/store/index.js +5 -6
- package/lib/store/paths.js +79 -76
- package/lib/store/registry.js +100 -89
- package/lib/store/util.js +19 -0
- package/lib/types/admin.d.ts +187 -0
- package/lib/types/engine.d.ts +136 -0
- package/lib/types/index.d.ts +281 -0
- package/lib/types/memory.d.ts +106 -0
- package/lib/types/move.d.ts +272 -0
- package/lib/types/sessionlog.d.ts +14 -0
- package/lib/types/store/frames.d.ts +128 -0
- package/lib/types/store/host.d.ts +88 -0
- package/lib/types/store/http.d.ts +105 -0
- package/lib/types/store/index.d.ts +30 -0
- package/lib/types/store/paths.d.ts +100 -0
- package/lib/types/store/registry.d.ts +206 -0
- package/lib/types/store/util.d.ts +16 -0
- package/package.json +13 -3
package/lib/memory.js
CHANGED
|
@@ -17,22 +17,20 @@
|
|
|
17
17
|
*
|
|
18
18
|
* @module dsh-session-ops/memory
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
20
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { basename, join, resolve } from 'node:path';
|
|
22
|
+
import { messageOf } from './store/util.js';
|
|
24
23
|
/** 与 dsh-cross-memory/lib/storage.js 的 slugify 逐字一致。 */
|
|
25
24
|
export function slugify(value) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
const slug = String(value)
|
|
26
|
+
.normalize('NFKD')
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[\u0300-\u036f]/gu, '')
|
|
29
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/gu, '-')
|
|
30
|
+
.replace(/^-+|-+$/gu, '')
|
|
31
|
+
.slice(0, 64);
|
|
32
|
+
return slug.length > 0 ? slug : 'memory';
|
|
34
33
|
}
|
|
35
|
-
|
|
36
34
|
/**
|
|
37
35
|
* 与 dsh-cross-memory/lib/storage.js 的 projectSlug 逐字一致:**只取项目名**,
|
|
38
36
|
* 不再拼 `sha1(resolve(root))` 后缀。dsh-cross-memory 已把项目 slug 改成
|
|
@@ -40,21 +38,18 @@ export function slugify(value) {
|
|
|
40
38
|
* 项目按机器拆成多个目录,迁移逻辑见其 storage.ts 的 LEGACY_PROJECT_SLUG)。
|
|
41
39
|
*/
|
|
42
40
|
export function projectSlug(root) {
|
|
43
|
-
|
|
41
|
+
return slugify(root.replace(/[\\/]+$/u, '').split(/[\\/]/u).pop() ?? 'workspace');
|
|
44
42
|
}
|
|
45
|
-
|
|
46
43
|
/** `$DSH_HOME/memories/projects`。 */
|
|
47
44
|
export function projectsDirOf(dshHome) {
|
|
48
|
-
|
|
45
|
+
return join(dshHome, 'memories', 'projects');
|
|
49
46
|
}
|
|
50
|
-
|
|
51
47
|
/** Windows 上路径比较不区分大小写(其余平台按字面比较)。 */
|
|
52
48
|
function samePath(left, right) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
const a = resolve(left);
|
|
50
|
+
const b = resolve(right);
|
|
51
|
+
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
56
52
|
}
|
|
57
|
-
|
|
58
53
|
/**
|
|
59
54
|
* 在 projects 目录里按 root 找已存在的项目(优先命中已有 slug,避免算出第二个目录)。
|
|
60
55
|
* @param projectsDir - `…/memories/projects`。
|
|
@@ -62,44 +57,49 @@ function samePath(left, right) {
|
|
|
62
57
|
* @returns `{slug, root, descriptorFile, dir}`,没有时 `undefined`。
|
|
63
58
|
*/
|
|
64
59
|
export function findProjectByRoot(projectsDir, root) {
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
entries = readdirSync(projectsDir, { withFileTypes: true })
|
|
68
|
-
} catch {
|
|
69
|
-
return undefined
|
|
70
|
-
}
|
|
71
|
-
for (const entry of entries) {
|
|
72
|
-
if (!entry.isDirectory()) continue
|
|
73
|
-
const descriptorFile = join(projectsDir, entry.name, 'project.json')
|
|
74
|
-
if (!existsSync(descriptorFile)) continue
|
|
60
|
+
let entries;
|
|
75
61
|
try {
|
|
76
|
-
|
|
77
|
-
if (typeof descriptor?.root === 'string' && samePath(descriptor.root, root)) {
|
|
78
|
-
return { slug: entry.name, root: descriptor.root, descriptorFile, dir: join(projectsDir, entry.name) }
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
/* 坏的 project.json 跳过 */
|
|
62
|
+
entries = readdirSync(projectsDir, { withFileTypes: true });
|
|
82
63
|
}
|
|
83
|
-
|
|
84
|
-
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (!entry.isDirectory())
|
|
69
|
+
continue;
|
|
70
|
+
const descriptorFile = join(projectsDir, entry.name, 'project.json');
|
|
71
|
+
if (!existsSync(descriptorFile))
|
|
72
|
+
continue;
|
|
73
|
+
try {
|
|
74
|
+
const descriptor = JSON.parse(readFileSync(descriptorFile, 'utf8'));
|
|
75
|
+
if (typeof descriptor?.root === 'string' && samePath(descriptor.root, root)) {
|
|
76
|
+
return { slug: entry.name, root: descriptor.root, descriptorFile, dir: join(projectsDir, entry.name) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* 坏的 project.json 跳过 */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
85
84
|
}
|
|
86
|
-
|
|
87
85
|
/** 极简 frontmatter 解析:只认 `key: value` 单行(dsh-cross-memory 的条目就是这种形状)。 */
|
|
88
86
|
export function parseFrontmatter(text) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
const out = {};
|
|
88
|
+
if (!text.startsWith('---'))
|
|
89
|
+
return out;
|
|
90
|
+
const end = text.indexOf('\n---', 3);
|
|
91
|
+
const head = end === -1 ? text.slice(3) : text.slice(3, end);
|
|
92
|
+
for (const line of head.split('\n')) {
|
|
93
|
+
const at = line.indexOf(':');
|
|
94
|
+
if (at <= 0)
|
|
95
|
+
continue;
|
|
96
|
+
const key = line.slice(0, at).trim();
|
|
97
|
+
if (key.length === 0)
|
|
98
|
+
continue;
|
|
99
|
+
out[key] = line.slice(at + 1).trim().replace(/^"(.*)"$/u, '$1');
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
101
102
|
}
|
|
102
|
-
|
|
103
103
|
/**
|
|
104
104
|
* 计算一次「记忆随会话走」的计划。**只读**。
|
|
105
105
|
*
|
|
@@ -107,71 +107,74 @@ export function parseFrontmatter(text) {
|
|
|
107
107
|
* @returns 计划:源/目标项目、匹配到的条目(按 frontmatter 的 `session:` 归属过滤)。
|
|
108
108
|
*/
|
|
109
109
|
export function planMemoryFollow(options) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
files = readdirSync(entriesDir)
|
|
134
|
-
} catch {
|
|
135
|
-
return plan
|
|
136
|
-
}
|
|
137
|
-
const wanted = String(sessionId).replace(/^session-/u, '')
|
|
138
|
-
for (const name of files) {
|
|
139
|
-
if (!name.endsWith('.md')) continue
|
|
140
|
-
const file = join(entriesDir, name)
|
|
141
|
-
let front
|
|
110
|
+
const { dshHome, sessionId, fromCwd, toCwd } = options;
|
|
111
|
+
const projectsDir = projectsDirOf(dshHome);
|
|
112
|
+
const source = findProjectByRoot(projectsDir, fromCwd);
|
|
113
|
+
const targetFound = findProjectByRoot(projectsDir, toCwd);
|
|
114
|
+
const target = targetFound ?? {
|
|
115
|
+
slug: projectSlug(toCwd),
|
|
116
|
+
root: resolve(toCwd),
|
|
117
|
+
descriptorFile: join(projectsDir, projectSlug(toCwd), 'project.json'),
|
|
118
|
+
dir: join(projectsDir, projectSlug(toCwd)),
|
|
119
|
+
planned: true,
|
|
120
|
+
};
|
|
121
|
+
const plan = {
|
|
122
|
+
projectsDir,
|
|
123
|
+
source: source === undefined ? null : { slug: source.slug, root: source.root, dir: source.dir },
|
|
124
|
+
target: { slug: target.slug, root: target.root, dir: target.dir, descriptorFile: target.descriptorFile, existed: targetFound !== undefined },
|
|
125
|
+
sessionId,
|
|
126
|
+
totalInSource: 0,
|
|
127
|
+
matched: [],
|
|
128
|
+
};
|
|
129
|
+
if (source === undefined)
|
|
130
|
+
return plan;
|
|
131
|
+
const entriesDir = join(source.dir, 'entries');
|
|
132
|
+
let files;
|
|
142
133
|
try {
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
134
|
+
files = readdirSync(entriesDir);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return plan;
|
|
146
138
|
}
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
139
|
+
const wanted = String(sessionId).replace(/^session-/u, '');
|
|
140
|
+
for (const name of files) {
|
|
141
|
+
if (!name.endsWith('.md'))
|
|
142
|
+
continue;
|
|
143
|
+
const file = join(entriesDir, name);
|
|
144
|
+
let front;
|
|
145
|
+
try {
|
|
146
|
+
front = parseFrontmatter(readFileSync(file, 'utf8'));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
plan.totalInSource += 1;
|
|
152
|
+
const owner = typeof front.session === 'string' ? front.session.replace(/^session-/u, '') : undefined;
|
|
153
|
+
if (owner !== wanted)
|
|
154
|
+
continue;
|
|
155
|
+
const targetFile = join(target.dir, 'entries', name);
|
|
156
|
+
plan.matched.push({
|
|
157
|
+
id: front.id ?? name.replace(/\.md$/u, ''),
|
|
158
|
+
title: front.title ?? '',
|
|
159
|
+
sourceFile: file,
|
|
160
|
+
targetFile,
|
|
161
|
+
existsInTarget: existsSync(targetFile),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return plan;
|
|
160
165
|
}
|
|
161
|
-
|
|
162
166
|
/** 建立目标项目目录(含 project.json 与 entries/)。 */
|
|
163
167
|
function ensureProject(target) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
mkdirSync(join(target.dir, 'entries'), { recursive: true });
|
|
169
|
+
if (!existsSync(target.descriptorFile)) {
|
|
170
|
+
writeFileSync(target.descriptorFile, `${JSON.stringify({
|
|
171
|
+
root: target.root,
|
|
172
|
+
name: basename(target.root),
|
|
173
|
+
slug: target.slug,
|
|
174
|
+
updatedAt: Date.now(),
|
|
175
|
+
}, null, 2)}\n`);
|
|
176
|
+
}
|
|
173
177
|
}
|
|
174
|
-
|
|
175
178
|
/**
|
|
176
179
|
* 执行计划。`copy` 只加不删;`move` 复制后从源项目移除(= 纠正错放)。
|
|
177
180
|
* 目标已有同名条目时跳过(不覆盖、按名去重)。
|
|
@@ -181,25 +184,27 @@ function ensureProject(target) {
|
|
|
181
184
|
* @returns `{mode, copied, skipped, removed, files}`。
|
|
182
185
|
*/
|
|
183
186
|
export function applyMemoryFollow(plan, mode) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
187
|
+
const outcome = { mode, copied: 0, skipped: 0, removed: 0, files: [] };
|
|
188
|
+
if (plan.source === null || plan.matched.length === 0)
|
|
189
|
+
return outcome;
|
|
190
|
+
ensureProject(plan.target);
|
|
191
|
+
for (const entry of plan.matched) {
|
|
192
|
+
if (entry.existsInTarget) {
|
|
193
|
+
outcome.skipped += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
copyFileSync(entry.sourceFile, entry.targetFile);
|
|
197
|
+
outcome.copied += 1;
|
|
198
|
+
outcome.files.push(entry.id);
|
|
199
|
+
if (mode === 'move') {
|
|
200
|
+
try {
|
|
201
|
+
unlinkSync(entry.sourceFile);
|
|
202
|
+
outcome.removed += 1;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
outcome.removeError = messageOf(error);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
202
208
|
}
|
|
203
|
-
|
|
204
|
-
return outcome
|
|
209
|
+
return outcome;
|
|
205
210
|
}
|