dsh-config-manager 0.2.0 → 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/lib/adapters/self.d.ts +28 -0
- package/lib/adapters/self.js +14 -3
- package/lib/client.js +522 -522
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/src/adapters/self.test.ts +54 -0
- package/src/adapters/self.ts +40 -3
- package/src/index.ts +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export declare const name = "config-manager";
|
|
|
77
77
|
/** Services required before the engine can mount (present in every profile). */
|
|
78
78
|
export declare const inject: string[];
|
|
79
79
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
80
|
-
export declare const PLUGIN_VERSION = "0.2.
|
|
80
|
+
export declare const PLUGIN_VERSION = "0.2.1";
|
|
81
81
|
/**
|
|
82
82
|
* Star 引导弹窗指向的 GitHub 仓库(用户引导点 Star 的目标)。
|
|
83
83
|
* 与 package.json 的 repository 字段保持一致;界面不可改(硬编码,参照
|
package/lib/index.js
CHANGED
|
@@ -136,7 +136,7 @@ export const name = 'config-manager';
|
|
|
136
136
|
/** Services required before the engine can mount (present in every profile). */
|
|
137
137
|
export const inject = ['settings', 'credentials'];
|
|
138
138
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
139
|
-
export const PLUGIN_VERSION = '0.2.
|
|
139
|
+
export const PLUGIN_VERSION = '0.2.1';
|
|
140
140
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
141
141
|
const PLUGIN_NAME = 'dsh-config-manager';
|
|
142
142
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-config-manager",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backup, restore, export, import, migrate and sync your DeepSeek Harness (DSH) configuration: settings, plugins, MCP servers, skills, agent presets and workspaces. One-click migration to another machine. DSH 配置一键备份、恢复、导出、导入、迁移与同步插件。",
|
|
6
6
|
"icon": "./icon.svg",
|
|
@@ -284,4 +284,58 @@ test('self: 运行态改写不改变分区 hash(自动同步不再每轮误判
|
|
|
284
284
|
await src.fs.writeFile(rel, Buffer.from(config({ lastRunAt: '2026-10-25T12:02:00.000Z' }, 'https://y/repo.git'), 'utf8'));
|
|
285
285
|
const third = await adapter.export(src, { includeSecrets: false });
|
|
286
286
|
assert.notEqual(hashSection(third.data), hashSection(second.data), '真实配置变化必须让分区 hash 变化');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* issue #119 回归:跳过码翻转不得改变分区 hash。
|
|
291
|
+
*
|
|
292
|
+
* 现场(0.2.0 / macOS desktop / git 通道 + 自动同步 60s 远端轮询):配置一字未改,远端仓库
|
|
293
|
+
* 每分钟多 1~2 个 commit,15 分钟 24 个。根因 = 调度器「跳过」轮写
|
|
294
|
+
* lastRunSkipReason='upToDate'(冲突轮写 'conflict' + lastRunSkipCount),而「真推」轮收尾
|
|
295
|
+
* 又把它们清空(toChannelPayload 空值不写);两个字段不在剔除表里 → self 分区 hash 每轮翻新
|
|
296
|
+
* → hasLocalChanges() 恒真 → 每轮都真推一个新快照。
|
|
297
|
+
*/
|
|
298
|
+
test('self: 跳过码翻转不改变分区 hash(issue #119:自动同步不再每轮真推快照)', async () => {
|
|
299
|
+
const rel = 'dsh-config-manager/sync/sync-autosync.json';
|
|
300
|
+
const src = makeContext('win32', 'C:\\Users\\alice');
|
|
301
|
+
const adapter = new SelfAdapter();
|
|
302
|
+
const config = (runState: Record<string, unknown>, repoUrl = 'https://x/repo.git') => JSON.stringify({
|
|
303
|
+
schemaVersion: 2,
|
|
304
|
+
channels: {
|
|
305
|
+
git: { enabled: true, intervalMinutes: 30, realtime: true, remoteCheckMs: 60000, repoUrl, branch: 'main', ...runState },
|
|
306
|
+
webdav: { enabled: false },
|
|
307
|
+
},
|
|
308
|
+
}, null, 2);
|
|
309
|
+
|
|
310
|
+
// 轮次 A:跳过(无本地改动)
|
|
311
|
+
await src.fs.writeFile(rel, Buffer.from(config({ lastRunAt: '2026-10-29T09:00:00.000Z', lastRunStatus: 'success', lastRunSkipReason: 'upToDate', consecutiveFailures: 0 }), 'utf8'));
|
|
312
|
+
const skipRound = await adapter.export(src, { includeSecrets: false });
|
|
313
|
+
|
|
314
|
+
// 轮次 B:真推一次 —— 跳过码被清空,其余与 A 相同
|
|
315
|
+
await src.fs.writeFile(rel, Buffer.from(config({ lastRunAt: '2026-10-29T09:01:00.000Z', lastRunStatus: 'success', consecutiveFailures: 0 }), 'utf8'));
|
|
316
|
+
const pushRound = await adapter.export(src, { includeSecrets: false });
|
|
317
|
+
|
|
318
|
+
// 轮次 C:跳过(冲突,带待确认分区数)
|
|
319
|
+
await src.fs.writeFile(rel, Buffer.from(config({ lastRunAt: '2026-10-29T09:02:00.000Z', lastRunStatus: 'success', lastRunSkipReason: 'conflict', lastRunSkipCount: 3, consecutiveFailures: 0 }), 'utf8'));
|
|
320
|
+
const conflictRound = await adapter.export(src, { includeSecrets: false });
|
|
321
|
+
|
|
322
|
+
const base = hashSection(skipRound.data);
|
|
323
|
+
assert.equal(hashSection(pushRound.data), base, '跳过码被清空不得改变分区 hash(否则每轮都判「本地有改动」)');
|
|
324
|
+
assert.equal(hashSection(conflictRound.data), base, '跳过码换值 / 带分区数不得改变分区 hash');
|
|
325
|
+
|
|
326
|
+
const payload = Buffer.from(pushRound.data.files.find((f) => f.relativePath === 'sync/sync-autosync.json')!.data).toString('utf8');
|
|
327
|
+
assert.ok(!payload.includes('lastRunSkip'), '导出载荷不含跳过码:远端不该接收本机执行记录');
|
|
328
|
+
assert.ok(payload.includes('"repoUrl":"https://x/repo.git"'), '配置本体仍在载荷里');
|
|
329
|
+
|
|
330
|
+
// 同一份远端载荷回灌本机(本机多出跳过码)→ Skip,不是 Conflict
|
|
331
|
+
const dst = makeContext('linux', '/home/bob');
|
|
332
|
+
await dst.fs.writeFile(rel, Buffer.from(config({ lastRunAt: '2026-10-29T09:05:00.000Z', lastRunStatus: 'success', lastRunSkipReason: 'upToDate' }), 'utf8'));
|
|
333
|
+
const sections = new Map([['self', pushRound.data]]);
|
|
334
|
+
let items = await adapter.analyzeImport(pushRound.data, makeImportContext(dst, sections));
|
|
335
|
+
assert.equal(items[0]?.kind, 'Skip', '仅跳过码不同 → Skip(此前每轮判 Conflict 并重复推送)');
|
|
336
|
+
|
|
337
|
+
// 配置本体变化仍必须被识别(不能把剔除做成「什么都判不变」)
|
|
338
|
+
await dst.fs.writeFile(rel, Buffer.from(config({ lastRunSkipReason: 'upToDate' }, 'https://y/repo.git'), 'utf8'));
|
|
339
|
+
items = await adapter.analyzeImport(pushRound.data, makeImportContext(dst, sections));
|
|
340
|
+
assert.equal(items[0]?.kind, 'Conflict', '真实配置变化必须仍判 Conflict');
|
|
287
341
|
});
|
package/src/adapters/self.ts
CHANGED
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
* 且文件类分区不进 SecretScanner(与 pluginFiles/skills 同语义)。
|
|
46
46
|
*/
|
|
47
47
|
import type { HostContext } from '../core/types.ts';
|
|
48
|
+
import type { AutosyncConfig } from '../sync/autosync-config.ts';
|
|
49
|
+
import type { BackupScheduleConfig } from '../sync/backup-schedule-config.ts';
|
|
48
50
|
import type { RecursiveListing } from '../utils/recursive-walk.ts';
|
|
49
51
|
import { sectionMeta } from '../schema/section-registry.ts';
|
|
50
52
|
import { FileCollectionAdapter } from './file-collection.ts';
|
|
@@ -62,13 +64,48 @@ export const SELF_CONFIG_FILES: readonly string[] = [
|
|
|
62
64
|
'exports/.backup-notes.json',
|
|
63
65
|
];
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* 调度配置里「运行态字段」的**命名口径**:调度器每轮收尾都会改写的字段名形状
|
|
69
|
+
* (`lastRun*` 一族 + `consecutiveFailures`)。剔除表必须覆盖两个调度配置的并集,
|
|
70
|
+
* 少一个都会让 self 分区的 hashSection 每轮翻新(issue #119)。
|
|
71
|
+
*/
|
|
72
|
+
type RunStateKeyOf<T> = {
|
|
73
|
+
[K in keyof T]-?: K extends 'consecutiveFailures' | `lastRun${string}` ? K : never;
|
|
74
|
+
}[keyof T];
|
|
75
|
+
|
|
76
|
+
type SchedulerRunStateKey = RunStateKeyOf<AutosyncConfig> | RunStateKeyOf<BackupScheduleConfig>;
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* 调度配置里的**运行态字段**:每次执行收尾都会被调度器改写,不属于「配置身份」。
|
|
67
80
|
* 仅比对时剔除(见 comparableContent);写盘仍写完整文件。
|
|
81
|
+
*
|
|
82
|
+
* 逐项列全而不是靠前缀过滤:前缀过滤会把**将来**新增的运行态字段静默吞掉(看不见的假 Skip),
|
|
83
|
+
* 漏列则让 hash 每轮翻新(issue #119)。所以列全 + 下面的穷尽断言,让编译器守住漂移。
|
|
68
84
|
*/
|
|
69
|
-
const
|
|
70
|
-
'lastRunAt',
|
|
71
|
-
|
|
85
|
+
const VOLATILE_RUN_STATE_KEY_LIST = [
|
|
86
|
+
'lastRunAt',
|
|
87
|
+
'lastRunStatus',
|
|
88
|
+
'lastRunMessage',
|
|
89
|
+
'lastRunHistoryId',
|
|
90
|
+
'consecutiveFailures',
|
|
91
|
+
// issue #119:跳过码每轮在「写入 / 清空」之间翻转,与 lastRunAt 同性质,漏剔即每轮真推快照
|
|
92
|
+
'lastRunSkipReason',
|
|
93
|
+
'lastRunSkipCount',
|
|
94
|
+
] as const satisfies readonly SchedulerRunStateKey[];
|
|
95
|
+
|
|
96
|
+
const VOLATILE_RUN_STATE_KEYS: ReadonlySet<string> = new Set(VOLATILE_RUN_STATE_KEY_LIST);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 编译期穷尽断言(零运行时开销):`AutosyncConfig` / `BackupScheduleConfig` 将来新增任何
|
|
100
|
+
* `lastRun*` 字段而没进上表时,`Exclude` 不再是 `never`,**这一行 typecheck 报错**。
|
|
101
|
+
* 只写 `satisfies keyof T` 挡不住「新增字段忘了加」——那正是 issue #119 的失效模式。
|
|
102
|
+
*/
|
|
103
|
+
type AssertExhaustive<T extends never> = T;
|
|
104
|
+
|
|
105
|
+
/** 见 AssertExhaustive 的注释;导出只为让断言参与类型检查(无需被引用)。 */
|
|
106
|
+
export type SchedulerRunStateKeysExhaustive = AssertExhaustive<
|
|
107
|
+
Exclude<SchedulerRunStateKey, (typeof VOLATILE_RUN_STATE_KEY_LIST)[number]>
|
|
108
|
+
>;
|
|
72
109
|
|
|
73
110
|
/** 含运行态字段的调度配置(相对 baseDir)。 */
|
|
74
111
|
const SCHEDULER_CONFIG_RELS: ReadonlySet<string> = new Set([
|
package/src/index.ts
CHANGED
|
@@ -180,7 +180,7 @@ export const name = 'config-manager'
|
|
|
180
180
|
export const inject = ['settings', 'credentials']
|
|
181
181
|
|
|
182
182
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
183
|
-
export const PLUGIN_VERSION = '0.2.
|
|
183
|
+
export const PLUGIN_VERSION = '0.2.1'
|
|
184
184
|
|
|
185
185
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
186
186
|
const PLUGIN_NAME = 'dsh-config-manager'
|