dsh-session-ops 0.1.4 → 0.2.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/lib/memory.js CHANGED
@@ -17,22 +17,20 @@
17
17
  *
18
18
  * @module dsh-session-ops/memory
19
19
  */
20
-
21
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
22
- import { basename, join, resolve } from 'node:path'
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
- const slug = String(value)
27
- .normalize('NFKD')
28
- .toLowerCase()
29
- .replace(/[\u0300-\u036f]/gu, '')
30
- .replace(/[^a-z0-9\u4e00-\u9fff]+/gu, '-')
31
- .replace(/^-+|-+$/gu, '')
32
- .slice(0, 64)
33
- return slug.length > 0 ? slug : 'memory'
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
- return slugify(root.replace(/[\\/]+$/u, '').split(/[\\/]/u).pop() ?? 'workspace')
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
- return join(dshHome, 'memories', 'projects')
45
+ return join(dshHome, 'memories', 'projects');
49
46
  }
50
-
51
47
  /** Windows 上路径比较不区分大小写(其余平台按字面比较)。 */
52
48
  function samePath(left, right) {
53
- const a = resolve(left)
54
- const b = resolve(right)
55
- return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b
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
- let entries
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
- const descriptor = JSON.parse(readFileSync(descriptorFile, 'utf8'))
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
- return undefined
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
- const out = {}
90
- if (!text.startsWith('---')) return out
91
- const end = text.indexOf('\n---', 3)
92
- const head = end === -1 ? text.slice(3) : text.slice(3, end)
93
- for (const line of head.split('\n')) {
94
- const at = line.indexOf(':')
95
- if (at <= 0) continue
96
- const key = line.slice(0, at).trim()
97
- if (key.length === 0) continue
98
- out[key] = line.slice(at + 1).trim().replace(/^"(.*)"$/u, '$1')
99
- }
100
- return out
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
- 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) return plan
130
- const entriesDir = join(source.dir, 'entries')
131
- let files
132
- try {
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
- front = parseFrontmatter(readFileSync(file, 'utf8'))
144
- } catch {
145
- continue
134
+ files = readdirSync(entriesDir);
135
+ }
136
+ catch {
137
+ return plan;
146
138
  }
147
- plan.totalInSource += 1
148
- const owner = typeof front.session === 'string' ? front.session.replace(/^session-/u, '') : undefined
149
- if (owner !== wanted) continue
150
- const targetFile = join(target.dir, 'entries', name)
151
- plan.matched.push({
152
- id: front.id ?? name.replace(/\.md$/u, ''),
153
- title: front.title ?? '',
154
- sourceFile: file,
155
- targetFile,
156
- existsInTarget: existsSync(targetFile),
157
- })
158
- }
159
- return plan
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
- mkdirSync(join(target.dir, 'entries'), { recursive: true })
165
- if (!existsSync(target.descriptorFile)) {
166
- writeFileSync(target.descriptorFile, `${JSON.stringify({
167
- root: target.root,
168
- name: basename(target.root),
169
- slug: target.slug,
170
- updatedAt: Date.now(),
171
- }, null, 2)}\n`)
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
- const outcome = { mode, copied: 0, skipped: 0, removed: 0, files: [] }
185
- if (plan.source === null || plan.matched.length === 0) return outcome
186
- ensureProject(plan.target)
187
- for (const entry of plan.matched) {
188
- if (entry.existsInTarget) {
189
- outcome.skipped += 1
190
- continue
191
- }
192
- copyFileSync(entry.sourceFile, entry.targetFile)
193
- outcome.copied += 1
194
- outcome.files.push(entry.id)
195
- if (mode === 'move') {
196
- try {
197
- unlinkSync(entry.sourceFile)
198
- outcome.removed += 1
199
- } catch (error) {
200
- outcome.removeError = String(error?.message ?? error)
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
  }