dsh-config-manager 0.1.78 → 0.1.80
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/credentials.d.ts +6 -2
- package/lib/adapters/credentials.js +19 -3
- package/lib/adapters/file-collection.js +2 -1
- package/lib/adapters/mcp.d.ts +48 -2
- package/lib/adapters/mcp.js +265 -3
- package/lib/adapters/messages.d.ts +3 -0
- package/lib/adapters/messages.js +7 -0
- package/lib/adapters/plugin-files.js +2 -1
- package/lib/adapters/plugins.js +105 -20
- package/lib/adapters/pnpm-workspace.d.ts +32 -0
- package/lib/adapters/pnpm-workspace.js +113 -2
- package/lib/adapters/prompts.d.ts +6 -2
- package/lib/adapters/prompts.js +17 -2
- package/lib/adapters/providers.d.ts +19 -2
- package/lib/adapters/providers.js +59 -3
- package/lib/adapters/settings.d.ts +6 -2
- package/lib/adapters/settings.js +13 -2
- package/lib/adapters/skills.d.ts +25 -1
- package/lib/adapters/skills.js +60 -0
- package/lib/adapters/ui.d.ts +6 -2
- package/lib/adapters/ui.js +13 -2
- package/lib/adapters/units.d.ts +18 -0
- package/lib/adapters/units.js +39 -0
- package/lib/adapters/workspaces.js +2 -1
- package/lib/client.d.ts +401 -112
- package/lib/client.js +2899 -1525
- package/lib/core/backup.js +20 -0
- package/lib/core/mcp-connector-store.d.ts +159 -0
- package/lib/core/mcp-connector-store.js +445 -0
- package/lib/core/messages-core.d.ts +2 -0
- package/lib/core/messages-core.js +4 -0
- package/lib/core/messages.d.ts +20 -0
- package/lib/core/messages.js +44 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +22 -2
- package/lib/core/plugin-cli.js +22 -3
- package/lib/core/recovery-orchestrator.d.ts +3 -1
- package/lib/core/recovery-orchestrator.js +75 -16
- package/lib/core/rollback-activity.d.ts +89 -0
- package/lib/core/rollback-activity.js +350 -0
- package/lib/core/rollback-ledger.d.ts +0 -9
- package/lib/core/rollback-ledger.js +37 -0
- package/lib/core/rollback-merge.d.ts +22 -2
- package/lib/core/rollback-merge.js +63 -13
- package/lib/core/rollback-point.d.ts +93 -0
- package/lib/core/rollback-point.js +420 -101
- package/lib/core/rollback-undo-journal.d.ts +123 -0
- package/lib/core/rollback-undo-journal.js +517 -0
- package/lib/core/self-update.js +2 -4
- package/lib/core/text-fidelity.d.ts +89 -0
- package/lib/core/text-fidelity.js +176 -0
- package/lib/core/types.d.ts +12 -3
- package/lib/core/update-check.js +2 -4
- package/lib/core/validator.d.ts +9 -0
- package/lib/core/validator.js +12 -0
- package/lib/foreign/bundle.js +42 -3
- package/lib/foreign/claude-code.js +358 -26
- package/lib/foreign/claude-sessions.d.ts +51 -8
- package/lib/foreign/claude-sessions.js +111 -13
- package/lib/foreign/read-claude-code.d.ts +33 -4
- package/lib/foreign/read-claude-code.js +446 -65
- package/lib/foreign/read-claude-plugins.d.ts +36 -0
- package/lib/foreign/read-claude-plugins.js +403 -0
- package/lib/foreign/registry.js +25 -2
- package/lib/foreign/truth-table.js +6 -4
- package/lib/foreign/types.d.ts +170 -2
- package/lib/index.d.ts +14 -5
- package/lib/index.js +60 -16
- package/lib/market/git-file-writer.js +7 -1
- package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
- package/lib/profiles/dsh-profile-launcher.js +58 -1
- package/lib/profiles/dsh-profile-shared.d.ts +8 -2
- package/lib/profiles/dsh-profile-shared.js +8 -2
- package/lib/routes/foreign.js +8 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/sync.js +173 -9
- package/lib/schema/manifest.d.ts +6 -1
- package/lib/schema/manifest.js +55 -0
- package/lib/schema/types.d.ts +47 -2
- package/lib/sync/ancestor.d.ts +22 -0
- package/lib/sync/ancestor.js +66 -2
- package/lib/sync/autosync-config.d.ts +73 -0
- package/lib/sync/autosync-config.js +89 -0
- package/lib/sync/autosync-scheduler.d.ts +80 -6
- package/lib/sync/autosync-scheduler.js +263 -40
- package/lib/sync/gist/gist-transport.d.ts +18 -1
- package/lib/sync/gist/gist-transport.js +42 -3
- package/lib/sync/gist/messages.d.ts +2 -0
- package/lib/sync/gist/messages.js +5 -0
- package/lib/sync/git/git-transport.d.ts +42 -3
- package/lib/sync/git/git-transport.js +146 -3
- package/lib/sync/merge.d.ts +47 -14
- package/lib/sync/merge.js +242 -222
- package/lib/sync/messages.d.ts +8 -0
- package/lib/sync/messages.js +19 -0
- package/lib/sync/risk.d.ts +19 -2
- package/lib/sync/risk.js +10 -3
- package/lib/sync/s3/messages.d.ts +6 -0
- package/lib/sync/s3/messages.js +13 -0
- package/lib/sync/s3/s3-transport.d.ts +9 -1
- package/lib/sync/s3/s3-transport.js +78 -6
- package/lib/sync/s3/sigv4.js +11 -2
- package/lib/sync/sync-engine.d.ts +212 -8
- package/lib/sync/sync-engine.js +514 -58
- package/lib/sync/sync-history.d.ts +11 -0
- package/lib/sync/sync-history.js +20 -0
- package/lib/sync/sync-state.d.ts +43 -0
- package/lib/sync/sync-state.js +44 -0
- package/lib/sync/sync-watcher.d.ts +76 -0
- package/lib/sync/sync-watcher.js +202 -0
- package/lib/sync/text-merge.d.ts +85 -0
- package/lib/sync/text-merge.js +513 -0
- package/lib/sync/transport.d.ts +48 -0
- package/lib/sync/transport.js +12 -0
- package/lib/sync/watch-scope.d.ts +38 -0
- package/lib/sync/watch-scope.js +95 -0
- package/lib/sync/webdav/webdav-transport.d.ts +16 -2
- package/lib/sync/webdav/webdav-transport.js +128 -11
- package/lib/ui/foreign-view.js +10 -0
- package/lib/ui/i18n.d.ts +9 -0
- package/lib/ui/i18n.js +22 -0
- package/lib/ui/rollback-view.d.ts +12 -0
- package/lib/ui/rollback-view.js +2 -0
- package/lib/ui/sync-settings-view.d.ts +47 -0
- package/lib/ui/sync-settings-view.js +24 -0
- package/lib/ui/types.d.ts +21 -0
- package/lib/utils/env-lock.d.ts +1 -0
- package/lib/utils/env-lock.js +7 -4
- package/package.json +6 -6
- package/src/adapters/credentials.ts +22 -4
- package/src/adapters/file-collection.ts +2 -1
- package/src/adapters/mcp.test.ts +235 -1
- package/src/adapters/mcp.ts +276 -5
- package/src/adapters/messages.ts +7 -0
- package/src/adapters/plugin-files.ts +2 -1
- package/src/adapters/plugins.test.ts +176 -2
- package/src/adapters/plugins.ts +107 -21
- package/src/adapters/pnpm-workspace.test.ts +89 -1
- package/src/adapters/pnpm-workspace.ts +127 -2
- package/src/adapters/prompts.ts +20 -3
- package/src/adapters/providers.ts +62 -4
- package/src/adapters/settings.ts +16 -3
- package/src/adapters/skills.test.ts +141 -0
- package/src/adapters/skills.ts +58 -2
- package/src/adapters/ui.ts +16 -3
- package/src/adapters/units-entries.test.ts +190 -0
- package/src/adapters/units.ts +50 -0
- package/src/adapters/workspaces.ts +2 -1
- package/src/client/about/PluginUpdateDialog.tsx +4 -2
- package/src/client/about/ReleaseNotesDialog.tsx +4 -2
- package/src/client/chat/RollbackReviewDialog.tsx +3 -2
- package/src/client/common/Skeleton.tsx +32 -0
- package/src/client/common/api-base.test.ts +36 -0
- package/src/client/common/http.test.ts +12 -1
- package/src/client/common/http.ts +8 -2
- package/src/client/common/routes.ts +46 -3
- package/src/client/config-manager.module.css +321 -11
- package/src/client/home/HomePanel.tsx +14 -6
- package/src/client/import/ForeignImportView.tsx +3 -1
- package/src/client/import/import-wizard-steps.tsx +2 -1
- package/src/client/library/ArtifactRow.tsx +12 -2
- package/src/client/library/LibraryActions.tsx +6 -3
- package/src/client/library/LibraryPanel.tsx +21 -3
- package/src/client/library/RestorePlanView.tsx +4 -2
- package/src/client/locales.ts +3 -0
- package/src/client/market/market-api.test.ts +3 -2
- package/src/client/recovery/RecoveryPanel.tsx +54 -7
- package/src/client/recovery/RollbackCard.tsx +7 -2
- package/src/client/recovery/RollbackDiffPane.tsx +3 -2
- package/src/client/recovery/recovery-locales.ts +20 -2
- package/src/client/recovery/recovery-view.test.ts +39 -1
- package/src/client/recovery/recovery-view.ts +23 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/run-store.ts +4 -0
- package/src/client/sync/AutosyncCard.tsx +49 -6
- package/src/client/sync/MergeModal.tsx +220 -0
- package/src/client/sync/MergePreviewView.tsx +306 -0
- package/src/client/sync/SyncActivityBar.tsx +31 -0
- package/src/client/sync/SyncConfirmView.tsx +2 -1
- package/src/client/sync/SyncPanel.tsx +130 -87
- package/src/client/sync/SyncProbeRow.tsx +162 -0
- package/src/client/sync/SyncSettingsView.test.ts +27 -2
- package/src/client/sync/WorkDirCard.tsx +44 -6
- package/src/client/sync/history-model.ts +4 -1
- package/src/client/sync/merge-view.test.ts +248 -0
- package/src/client/sync/merge-view.ts +398 -0
- package/src/client/sync/probe-view.test.ts +90 -0
- package/src/client/sync/probe-view.ts +63 -0
- package/src/client/sync/remote-device-view.test.ts +35 -0
- package/src/client/sync/remote-device-view.ts +25 -0
- package/src/client/sync/sync-api.test.ts +5 -3
- package/src/client/sync/sync-api.ts +212 -2
- package/src/client/sync/sync-entry.test.ts +78 -0
- package/src/client/sync/sync-entry.ts +53 -0
- package/src/client/sync/sync-locales.ts +234 -2
- package/src/client/sync/sync-view.test.ts +18 -1
- package/src/client/sync/sync-view.ts +20 -0
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +20 -0
- package/src/core/mcp-connector-store.test.ts +265 -0
- package/src/core/mcp-connector-store.ts +516 -0
- package/src/core/messages-core.ts +4 -0
- package/src/core/messages.ts +46 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +30 -4
- package/src/core/recovery-orchestrator.test.ts +104 -5
- package/src/core/recovery-orchestrator.ts +74 -17
- package/src/core/rollback-activity.test.ts +311 -0
- package/src/core/rollback-activity.ts +367 -0
- package/src/core/rollback-cross-session.test.ts +694 -0
- package/src/core/rollback-interrupted.test.ts +137 -0
- package/src/core/rollback-ledger.ts +32 -0
- package/src/core/rollback-merge.test.ts +130 -0
- package/src/core/rollback-merge.ts +78 -14
- package/src/core/rollback-point.ts +468 -91
- package/src/core/rollback-undo-journal.test.ts +508 -0
- package/src/core/rollback-undo-journal.ts +571 -0
- package/src/core/self-update.ts +2 -5
- package/src/core/text-fidelity.test.ts +189 -0
- package/src/core/text-fidelity.ts +197 -0
- package/src/core/types.ts +11 -3
- package/src/core/update-check.ts +2 -5
- package/src/core/validator.ts +14 -0
- package/src/foreign/bundle-origin.test.ts +265 -0
- package/src/foreign/bundle.ts +45 -5
- package/src/foreign/claude-code.test.ts +268 -0
- package/src/foreign/claude-code.ts +406 -25
- package/src/foreign/claude-plugins.test.ts +234 -0
- package/src/foreign/claude-sessions.test.ts +141 -0
- package/src/foreign/claude-sessions.ts +179 -23
- package/src/foreign/file-budget.test.ts +4 -1
- package/src/foreign/read-claude-code.test.ts +320 -0
- package/src/foreign/read-claude-code.ts +490 -71
- package/src/foreign/read-claude-plugins.ts +465 -0
- package/src/foreign/registry.ts +25 -2
- package/src/foreign/session-ir.test.ts +65 -0
- package/src/foreign/truth-table.test.ts +81 -0
- package/src/foreign/truth-table.ts +6 -3
- package/src/foreign/types.ts +162 -2
- package/src/index.facade.test.ts +26 -4
- package/src/index.ts +76 -19
- package/src/market/git-file-writer.test.ts +4 -0
- package/src/market/git-file-writer.ts +7 -1
- package/src/profiles/dsh-profile-launcher.test.ts +68 -3
- package/src/profiles/dsh-profile-launcher.ts +56 -1
- package/src/profiles/dsh-profile-shared.ts +8 -2
- package/src/routes/foreign.ts +8 -1
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/sync.ts +176 -9
- package/src/schema/manifest-schema.test.ts +87 -2
- package/src/schema/manifest.ts +57 -1
- package/src/schema/types.ts +45 -2
- package/src/sync/ancestor.ts +76 -2
- package/src/sync/autosync-config.test.ts +73 -1
- package/src/sync/autosync-config.ts +124 -0
- package/src/sync/autosync-risk-tier.test.ts +38 -2
- package/src/sync/autosync-scheduler.test.ts +264 -12
- package/src/sync/autosync-scheduler.ts +296 -44
- package/src/sync/gist/gist-conditional-write.test.ts +203 -0
- package/src/sync/gist/gist-transport.ts +53 -5
- package/src/sync/gist/messages.ts +5 -0
- package/src/sync/git/git-probe.test.ts +80 -0
- package/src/sync/git/git-transport.test.ts +185 -0
- package/src/sync/git/git-transport.ts +178 -5
- package/src/sync/merge.test.ts +188 -55
- package/src/sync/merge.ts +282 -224
- package/src/sync/messages.ts +19 -0
- package/src/sync/risk.ts +29 -5
- package/src/sync/s3/messages.ts +13 -0
- package/src/sync/s3/s3-conditional-write.test.ts +330 -0
- package/src/sync/s3/s3-transport.test.ts +2 -1
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/s3/sigv4.test.ts +4 -1
- package/src/sync/s3/sigv4.ts +11 -2
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +410 -19
- package/src/sync/sync-engine.ts +602 -57
- package/src/sync/sync-history.test.ts +16 -1
- package/src/sync/sync-history.ts +26 -0
- package/src/sync/sync-state.test.ts +64 -0
- package/src/sync/sync-state.ts +79 -0
- package/src/sync/sync-watcher.test.ts +197 -0
- package/src/sync/sync-watcher.ts +246 -0
- package/src/sync/text-merge.test.ts +358 -0
- package/src/sync/text-merge.ts +554 -0
- package/src/sync/transport.ts +57 -0
- package/src/sync/watch-scope.test.ts +122 -0
- package/src/sync/watch-scope.ts +120 -0
- package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
- package/src/sync/webdav/webdav-transport.ts +137 -14
- package/src/ui/foreign-view.test.ts +34 -0
- package/src/ui/foreign-view.ts +10 -0
- package/src/ui/i18n.ts +22 -0
- package/src/ui/rollback-view.ts +14 -0
- package/src/ui/sync-settings-view.test.ts +28 -1
- package/src/ui/sync-settings-view.ts +43 -0
- package/src/ui/types.ts +22 -0
- package/src/utils/env-lock.ts +8 -5
package/lib/adapters/plugins.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { isDeepStrictEqual } from 'node:util';
|
|
15
15
|
import { sha256Hex } from '../utils/hashing.js';
|
|
16
|
-
import {
|
|
16
|
+
import { isBundlesOnlyEntry, mergeBundleDeclaration, pluginInstallTarget, resolveProcessProfileName } from '../core/plugin-cli.js';
|
|
17
17
|
import { classifyPluginSpec, LOCAL_PLUGIN_DIR, resolveLocalPluginPath } from '../core/local-plugin-pack.js';
|
|
18
18
|
import { msgOf, zhMsg } from '../core/messages.js';
|
|
19
19
|
import { engineMsg } from './messages.js';
|
|
@@ -22,7 +22,7 @@ import { isPathSafe, normalizePath } from '../utils/paths.js';
|
|
|
22
22
|
import { isENOENT } from '../utils/guards.js';
|
|
23
23
|
import { PLUGIN_PATCH_REF_PREFIX, PLUGIN_PROFILE_MANIFEST_REF } from '../core/backup.js';
|
|
24
24
|
import { readEffectivePatchLines, resolveWriteLayer, USER_PATCH_FILE } from '../core/patch-layers.js';
|
|
25
|
-
import { parsePnpmPatchedDependencies, sanitizePnpmWorkspacePatches } from './pnpm-workspace.js';
|
|
25
|
+
import { parsePnpmPatchedDependencies, restorePnpmMachinePaths, sanitizePnpmWorkspacePatches, stripPnpmMachinePaths, } from './pnpm-workspace.js';
|
|
26
26
|
import { BUILTIN_PLUGIN_DATA_SOURCES, areasForSection } from '../core/plugin-data-registry.js';
|
|
27
27
|
import { declaredProfileName, scanDeclaredDataSources } from './plugin-data-declared.js';
|
|
28
28
|
import { sectionMeta } from '../schema/section-registry.js';
|
|
@@ -85,6 +85,20 @@ async function writeLocalTarball(ctx, tarball) {
|
|
|
85
85
|
export const PNPM_PROFILE_DIR = (profile) => `profiles/${profile !== undefined && profile !== '' ? profile : 'web'}`;
|
|
86
86
|
/** pnpm-workspace.yaml 相对 $DSH_HOME 的路径(plugins 分区内按「插件安装配置」管理)。 */
|
|
87
87
|
export const PNPM_WORKSPACE_REL = (profile) => `${PNPM_PROFILE_DIR(profile)}/pnpm-workspace.yaml`;
|
|
88
|
+
/**
|
|
89
|
+
* pnpm 配置可移植净化的可见文案(导出告警 / 导入计划项 detail / 写入结果三处**同一句**,
|
|
90
|
+
* 不另造通道):来源机的机器路径键被剥离、本机原有的机器路径键被保留,都必须说出来。
|
|
91
|
+
*/
|
|
92
|
+
function machinePathNotes(msg, stripped, restored) {
|
|
93
|
+
const notes = [];
|
|
94
|
+
if (stripped.length > 0) {
|
|
95
|
+
notes.push(msg('adapter.pwMachinePathStripped', { count: String(stripped.length), keys: stripped.join(', ') }));
|
|
96
|
+
}
|
|
97
|
+
if (restored.length > 0) {
|
|
98
|
+
notes.push(msg('adapter.pwMachinePathKept', { count: String(restored.length), keys: restored.join(', ') }));
|
|
99
|
+
}
|
|
100
|
+
return notes;
|
|
101
|
+
}
|
|
88
102
|
/** 单个 patch 文件随备份迁移的体积上限(issue #35;patch 是纯文本,超过说明放错了东西)。 */
|
|
89
103
|
export const MAX_PATCH_FILE_BYTES = 2 * 1024 * 1024;
|
|
90
104
|
/** patch 行 raw 是否由其他 adapter 管理(mcp-client 行 / systemPrompt / planMode 行)。
|
|
@@ -246,7 +260,9 @@ function unitsOf(data, tarballBytes, related) {
|
|
|
246
260
|
const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
|
|
247
261
|
const group = [wsId, ...patchFileIds];
|
|
248
262
|
const units = [];
|
|
249
|
-
|
|
263
|
+
// data.plugins 可能缺省(空分区 / 手写 section):按空表处理,不抛错 ——
|
|
264
|
+
// 选择器对任何分区都会调 listUnits,抛错会让整行退化成不可展开。
|
|
265
|
+
for (const pl of data.plugins ?? []) {
|
|
250
266
|
const tarball = tarballOf(data, pl.name);
|
|
251
267
|
const measured = tarballBytes?.get(pl.name);
|
|
252
268
|
const areas = related?.get(pl.name);
|
|
@@ -261,7 +277,7 @@ function unitsOf(data, tarballBytes, related) {
|
|
|
261
277
|
...(areas !== undefined && areas.length > 0 ? { relatedAreas: areas } : {}),
|
|
262
278
|
});
|
|
263
279
|
}
|
|
264
|
-
for (const line of data.patch) {
|
|
280
|
+
for (const line of data.patch ?? []) {
|
|
265
281
|
// 由 mcp / prompts adapter 管理的行不属本分区(applyItem 同样跳过),列出来只会误导用户
|
|
266
282
|
if (isManagedElsewhere(line.raw))
|
|
267
283
|
continue;
|
|
@@ -375,7 +391,16 @@ export class PluginsAdapter {
|
|
|
375
391
|
try {
|
|
376
392
|
const rel = PNPM_WORKSPACE_REL(ctx.profile);
|
|
377
393
|
if (await ctx.fs.exists(rel)) {
|
|
378
|
-
|
|
394
|
+
const raw = new TextDecoder().decode(await ctx.fs.readFile(rel));
|
|
395
|
+
// 可移植性净化(收集侧):storeDir / cacheDir … 的值是本机绝对路径,跟着快照跑到
|
|
396
|
+
// 别的机器上必然指向不存在的目录 → 只带「可移植」的那部分(逐行删除,其余逐字不变)。
|
|
397
|
+
const strip = stripPnpmMachinePaths(raw);
|
|
398
|
+
pnpmWorkspace = strip.text;
|
|
399
|
+
if (strip.stripped.length > 0) {
|
|
400
|
+
warnings.push(msgOf(ctx)('adapter.pwMachinePathStripped', {
|
|
401
|
+
count: String(strip.stripped.length), keys: strip.stripped.join(', '),
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
379
404
|
}
|
|
380
405
|
}
|
|
381
406
|
catch (err) {
|
|
@@ -667,7 +692,10 @@ export class PluginsAdapter {
|
|
|
667
692
|
// issue #35:目标机满足不了的 patchedDependencies 声明必须剔除——否则写进去之后,
|
|
668
693
|
// 目标机 pnpm 会因为「patch 文件读不到」拒绝**一切** add(含本次要装的插件)。
|
|
669
694
|
const available = await this.patchAvailability(data, ctx);
|
|
670
|
-
|
|
695
|
+
// 可移植性净化(写回侧·纵深防御):老版本备份 / 第三方工具产出的载荷里可能仍带着
|
|
696
|
+
// 来源机的机器路径键(本版本收集侧已剥离,正常路径这里就是 no-op)。
|
|
697
|
+
const stripped = stripPnpmMachinePaths(data.pnpmWorkspace);
|
|
698
|
+
const clean = sanitizePnpmWorkspacePatches(stripped.text, (rel) => available.has(rel));
|
|
671
699
|
const droppedNames = clean.dropped.map((d) => d.name).join(', ');
|
|
672
700
|
let current = null;
|
|
673
701
|
try {
|
|
@@ -679,15 +707,27 @@ export class PluginsAdapter {
|
|
|
679
707
|
catch {
|
|
680
708
|
current = null;
|
|
681
709
|
}
|
|
682
|
-
|
|
710
|
+
// 写回侧:不搬运来源机的机器路径,也不清掉**本机**自己的设置 → 接回目标机原有的那几行。
|
|
711
|
+
const merged = restorePnpmMachinePaths(clean.text, current);
|
|
712
|
+
// 「只差机器路径行」= 目标机现文本剥掉它自己的机器路径行后与将要写入的内容逐字一致。
|
|
713
|
+
// 此时写回只会把本机那几行挪到末尾(YAML 映射顺序无语义),没有意义 → 不生成计划项:
|
|
714
|
+
// 零写入、本机设置原地不动,也不给用户造一条无意义的 Update 噪音。
|
|
715
|
+
const onlyMachinePaths = current !== null && stripPnpmMachinePaths(current).text === clean.text;
|
|
716
|
+
// 剥完机器路径键后**没有可移植内容**(来源机的配置只剩机器路径)→ 不产出空文件项:
|
|
717
|
+
// 「无内容形态不得交给 importer」与 collect() 的口径一致。
|
|
718
|
+
if (merged.text !== '' && !onlyMachinePaths && current !== merged.text) {
|
|
719
|
+
const details = machinePathNotes(msg, stripped.stripped, merged.restored);
|
|
720
|
+
if (clean.dropped.length > 0) {
|
|
721
|
+
// 既有口径不变:剔除声明的那句话排在最前
|
|
722
|
+
details.unshift(msg('adapter.pwPatchesDroppedDetail', { count: String(clean.dropped.length), names: droppedNames }));
|
|
723
|
+
}
|
|
683
724
|
items.push({
|
|
684
725
|
id: 'plugins:pnpm-workspace',
|
|
685
726
|
kind: current === null ? 'Create' : 'Update',
|
|
686
727
|
adapter: 'plugins',
|
|
687
728
|
description: current === null ? msg('adapter.pwCreate') : msg('adapter.pwUpdate'),
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
: {}),
|
|
729
|
+
// detail 非空 ⇒ isToolchainChangeItem ⇒ 进确认列表(可见、可取消)——净化绝不静默
|
|
730
|
+
...(details.length > 0 ? { detail: details.join(msg('adapter.listSeparator')) } : {}),
|
|
691
731
|
severity: 'info',
|
|
692
732
|
target: { adapter: 'plugins', ref: 'pnpm-workspace.yaml' },
|
|
693
733
|
});
|
|
@@ -864,9 +904,32 @@ export class PluginsAdapter {
|
|
|
864
904
|
throw err;
|
|
865
905
|
}
|
|
866
906
|
}
|
|
867
|
-
|
|
907
|
+
// 可移植性净化(写回侧):先剥掉载荷里的机器路径键(纵深防御),再把目标机**原有的**
|
|
908
|
+
// 那几行接回去 —— 既不搬运来源机的路径,也不清掉本机自己的 store / 缓存位置。
|
|
909
|
+
const stripped = stripPnpmMachinePaths(text);
|
|
910
|
+
const clean = sanitizePnpmWorkspacePatches(stripped.text, (rel) => available.has(rel));
|
|
911
|
+
const wsRel = PNPM_WORKSPACE_REL(ctx.target.profile);
|
|
912
|
+
let local = null;
|
|
868
913
|
try {
|
|
869
|
-
await ctx.target.fs.
|
|
914
|
+
local = (await ctx.target.fs.exists(wsRel))
|
|
915
|
+
? new TextDecoder().decode(await ctx.target.fs.readFile(wsRel))
|
|
916
|
+
: null;
|
|
917
|
+
}
|
|
918
|
+
catch (err) {
|
|
919
|
+
// 「存在但读不到」必须显式失败(只有 ENOENT 才算本来就没有):读不到就无法保证本机的
|
|
920
|
+
// 路径设置不被清掉 —— 宁可不写,也不静默丢掉本机的 store / 缓存位置。
|
|
921
|
+
if (!isENOENT(err)) {
|
|
922
|
+
return {
|
|
923
|
+
ok: false,
|
|
924
|
+
warning: true,
|
|
925
|
+
message: msg('adapter.pwLocalReadFailed', { reason: err instanceof Error ? err.message : String(err) }),
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
local = null;
|
|
929
|
+
}
|
|
930
|
+
const merged = restorePnpmMachinePaths(clean.text, local);
|
|
931
|
+
try {
|
|
932
|
+
await ctx.target.fs.writeFile(wsRel, new TextEncoder().encode(merged.text));
|
|
870
933
|
const notes = [msg('adapter.pwWritten')];
|
|
871
934
|
if (clean.dropped.length > 0) {
|
|
872
935
|
notes.push(msg('adapter.pwWrittenPatchesDropped', {
|
|
@@ -874,6 +937,7 @@ export class PluginsAdapter {
|
|
|
874
937
|
names: clean.dropped.map((d) => d.name).join(', '),
|
|
875
938
|
}));
|
|
876
939
|
}
|
|
940
|
+
notes.push(...machinePathNotes(msg, stripped.stripped, merged.restored));
|
|
877
941
|
return { ok: true, needsRestart: true, message: notes.join(msg('adapter.listSeparator')) };
|
|
878
942
|
}
|
|
879
943
|
catch (err) {
|
|
@@ -901,27 +965,48 @@ export class PluginsAdapter {
|
|
|
901
965
|
return await this.declareHostBundle(name, ctx);
|
|
902
966
|
}
|
|
903
967
|
try {
|
|
904
|
-
//
|
|
905
|
-
|
|
906
|
-
// T1:备份内自带 tarball → 解包到 homeDir
|
|
968
|
+
// 安装目标由 pluginInstallTarget 统一决策(issue #101):备份里有具体版本时精确安装,
|
|
969
|
+
// 其余 registry 包带「包名@spec」;非 registry 来源原样。
|
|
970
|
+
// T1:备份内自带 tarball → 解包到 homeDir 内的本地插件缓存目录,改以 file:<abs> 安装。
|
|
907
971
|
// 这是「换机后本地插件不再丢失」的关键:原始绝对路径在目标机不存在,解包出的 tgz 一定存在。
|
|
972
|
+
let tarballPath;
|
|
908
973
|
const tarball = data !== undefined ? tarballOf(data, name) : undefined;
|
|
909
974
|
if (tarball !== undefined) {
|
|
910
975
|
try {
|
|
911
|
-
|
|
912
|
-
spec = `file:${abs}`;
|
|
976
|
+
tarballPath = await writeLocalTarball(ctx, tarball);
|
|
913
977
|
}
|
|
914
978
|
catch (err) {
|
|
915
979
|
// 解包失败不阻断安装尝试:退回原 spec(可能失败,但会如实报错而非静默丢失)
|
|
916
980
|
ctx.onLog?.(msg('adapter.localTarballUnpackFailed', { name, err: err instanceof Error ? err.message : String(err) }));
|
|
917
981
|
}
|
|
918
982
|
}
|
|
983
|
+
const target = pluginInstallTarget(name, { version: entry?.version, spec: entry?.spec, tarballPath });
|
|
919
984
|
// 执行日志:记录实际将发起的子进程命令行(与宿主 DshPluginsFacade 的
|
|
920
|
-
// dsh plugin --profile <p> add <
|
|
921
|
-
ctx.onLog?.(`$ dsh plugin --profile ${ctx.target.profile ?? resolveProcessProfileName()} add ${
|
|
985
|
+
// dsh plugin --profile <p> add <target> 一致);仅非敏感文本,渲染前 UI 再 redact 兜底
|
|
986
|
+
ctx.onLog?.(`$ dsh plugin --profile ${ctx.target.profile ?? resolveProcessProfileName()} add ${target}`);
|
|
922
987
|
// 透传中止信号:用户「跳过当前插件」→ 宿主 kill 子进程 + 清半装状态 → 抛 ImportUserSkippedError
|
|
923
|
-
const result = await ctx.target.plugins.install(name,
|
|
988
|
+
const result = await ctx.target.plugins.install(name, target, ctx.signal);
|
|
924
989
|
const suffix = result.needsRestart ? msg('adapter.pluginRestartSuffix') : '';
|
|
990
|
+
// 落地校验(issue #101):官方 CLI 退出 0 不等于版本变了 —— pnpm 会尊重目标机既有
|
|
991
|
+
// package.json spec,固定版已装、区间已满足时「什么都没装」同样返回 0。回读实际版本再下结论。
|
|
992
|
+
const expected = entry?.version;
|
|
993
|
+
const actual = result.installedVersion ?? null;
|
|
994
|
+
if (expected !== undefined && expected !== '' && actual !== null && actual !== expected) {
|
|
995
|
+
return {
|
|
996
|
+
ok: false,
|
|
997
|
+
warning: true,
|
|
998
|
+
needsRestart: true,
|
|
999
|
+
message: msg('adapter.pluginVersionMismatch', {
|
|
1000
|
+
name,
|
|
1001
|
+
expected,
|
|
1002
|
+
actual,
|
|
1003
|
+
command: `dsh plugin --profile ${ctx.target.profile ?? resolveProcessProfileName()} add ${name}@${expected}`,
|
|
1004
|
+
}),
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
if (actual !== null) {
|
|
1008
|
+
return { ok: true, needsRestart: true, message: msg('adapter.pluginInstalledAtVersion', { name, version: actual, suffix }) };
|
|
1009
|
+
}
|
|
925
1010
|
return {
|
|
926
1011
|
ok: true,
|
|
927
1012
|
needsRestart: true,
|
|
@@ -24,3 +24,35 @@ export interface SanitizeResult {
|
|
|
24
24
|
* @param available 判断某相对路径(相对 profile 目录)在**导入后**是否存在
|
|
25
25
|
*/
|
|
26
26
|
export declare function sanitizePnpmWorkspacePatches(text: string, available: (relPath: string) => boolean): SanitizeResult;
|
|
27
|
+
/**
|
|
28
|
+
* 机器相关 pnpm 设置键(根层)。camelCase 是 pnpm-workspace.yaml 的权威拼写;
|
|
29
|
+
* kebab 变体是同一组设置的另一方言(.npmrc / 部分版本),根层出现同样是本机路径绑定。
|
|
30
|
+
*/
|
|
31
|
+
export declare const PNPM_MACHINE_PATH_KEYS: readonly string[];
|
|
32
|
+
export interface MachinePathStripResult {
|
|
33
|
+
/** 剥离后的文本(无需改动时与入参**逐字节相同**;只做整行删除,EOL 原样保留) */
|
|
34
|
+
text: string;
|
|
35
|
+
/** 被剥离的键(按首次出现顺序去重) */
|
|
36
|
+
stripped: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 剥掉机器相关路径键(行级过滤)。
|
|
40
|
+
*
|
|
41
|
+
* 值写在同行 → 只删这一行;值写在缩进块里(`storeDir:` / `storeDir: |`)→ 连它的缩进续行
|
|
42
|
+
* 一起删,避免留下孤儿缩进行把文件变成坏 YAML。其余内容(含注释、空行、行尾缺换行)逐字不变。
|
|
43
|
+
*/
|
|
44
|
+
export declare function stripPnpmMachinePaths(text: string): MachinePathStripResult;
|
|
45
|
+
export interface MachinePathRestoreResult {
|
|
46
|
+
/** 接回本机路径行后的文本(无需恢复时与入参逐字相同) */
|
|
47
|
+
text: string;
|
|
48
|
+
/** 接回的键(按首次出现顺序去重) */
|
|
49
|
+
restored: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 把**目标机自己原有的**机器路径行接回将要写入的文本(写回侧)。
|
|
53
|
+
*
|
|
54
|
+
* 来源机的路径已在收集侧剥离(这里对老载荷再剥一次是纵深防御),所以本函数接回的只可能是
|
|
55
|
+
* 目标机原本就有的设置 —— 既不搬运来源机的路径,也不让一次导入清掉本机自己的 store 位置。
|
|
56
|
+
* `local` 为 null(目标机本来就没有该文件)时原样返回。接回行统一用 incoming 的 EOL。
|
|
57
|
+
*/
|
|
58
|
+
export declare function restorePnpmMachinePaths(incoming: string, local: string | null | undefined): MachinePathRestoreResult;
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* 组合,此后**任何** `pnpm add`(含 dsh plugin add)都失败于
|
|
7
7
|
* `Failed to read patch file ... (os error 2)` —— 用户实测 13/13 插件安装全灭。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 本模块做三件事,且**不做整文件重写**(保留用户注释与格式):
|
|
10
10
|
* 1. `parsePnpmPatchedDependencies`:按行解析声明(供导出侧收集 patch 文件);
|
|
11
11
|
* 2. `sanitizePnpmWorkspacePatches`:把「目标机没有对应文件」的条目整行删掉
|
|
12
12
|
* (含其上方注释),全部被删时连键一起删 —— 宁可少迁移一条 patch 声明,
|
|
13
|
-
* 也不让目标机的 pnpm
|
|
13
|
+
* 也不让目标机的 pnpm 从此拒绝一切安装;
|
|
14
|
+
* 3. `stripPnpmMachinePaths` / `restorePnpmMachinePaths`:机器相关路径键的
|
|
15
|
+
* 可移植性净化(见文末「可移植性净化」一节)。
|
|
14
16
|
*/
|
|
15
17
|
import { isPathSafe, normalizePath } from '../utils/paths.js';
|
|
16
18
|
const KEY_RE = /^patchedDependencies\s*:(.*)$/;
|
|
@@ -144,4 +146,113 @@ export function sanitizePnpmWorkspacePatches(text, available) {
|
|
|
144
146
|
unsupported: null,
|
|
145
147
|
};
|
|
146
148
|
}
|
|
149
|
+
/* ------------------------------------------------------------------ *
|
|
150
|
+
* 可移植性净化(跨机同步):机器相关路径键
|
|
151
|
+
*
|
|
152
|
+
* `storeDir` / `cacheDir` … 的值是**本机绝对路径**。它们跟着快照跑到另一台机器后必然指向
|
|
153
|
+
* 不存在的目录(pnpm 会去那里建 store / 缓存)。纪律只有两条:
|
|
154
|
+
* 1. 收集(导出 / push)与写回(导入 / pull)两侧都**整行删除**这些键 —— 其余内容逐字不变,
|
|
155
|
+
* EOL 原样保留(CRLF 输入绝不引入 LF 漂移);只删「根层 `key:`」形态,
|
|
156
|
+
* 缩进块里的同名键(如插件的自定义配置)一律不碰;
|
|
157
|
+
* 2. 写回时把**目标机自己原有的**那几行接回去:不搬运来源机的路径,也不清掉本机自己的设置。
|
|
158
|
+
*
|
|
159
|
+
* `.npmrc` 刻意**不进同步面**(见 plugins.ts 的说明):纳入它会改变 bundle 载荷形态
|
|
160
|
+
* (docs/spec/bundle-format-v1.md + conformance),而本机 .npmrc 从来就不由本插件写 ——
|
|
161
|
+
* 「不碰 = 本机保留」。
|
|
162
|
+
* ------------------------------------------------------------------ */
|
|
163
|
+
/**
|
|
164
|
+
* 机器相关 pnpm 设置键(根层)。camelCase 是 pnpm-workspace.yaml 的权威拼写;
|
|
165
|
+
* kebab 变体是同一组设置的另一方言(.npmrc / 部分版本),根层出现同样是本机路径绑定。
|
|
166
|
+
*/
|
|
167
|
+
export const PNPM_MACHINE_PATH_KEYS = [
|
|
168
|
+
'storeDir', 'store-dir',
|
|
169
|
+
'globalDir', 'global-dir',
|
|
170
|
+
'globalBinDir', 'global-bin-dir',
|
|
171
|
+
'cacheDir', 'cache-dir',
|
|
172
|
+
'stateDir', 'state-dir',
|
|
173
|
+
'configDir', 'config-dir',
|
|
174
|
+
];
|
|
175
|
+
const MACHINE_PATH_KEY_SET = new Set(PNPM_MACHINE_PATH_KEYS);
|
|
176
|
+
/** 根层的 `key: value` 形态;缩进行、注释行、块标量内容都不会命中(它们不从行首开始)。 */
|
|
177
|
+
const ROOT_KEY_RE = /^([A-Za-z_][A-Za-z0-9_-]*)[ \t]*:(.*)$/;
|
|
178
|
+
/** 空值或 YAML 块标量(`|` / `>` 及其修饰符)⇒ 值写在其后的缩进行里 */
|
|
179
|
+
function isBlockValue(raw) {
|
|
180
|
+
const v = raw.trim();
|
|
181
|
+
return v === '' || /^[|>][+-]?\d*$/.test(v);
|
|
182
|
+
}
|
|
183
|
+
/** 行首若是机器相关键 → 返回键名与冒号后的原文;否则 null(`\r` 不算值的一部分) */
|
|
184
|
+
function rootMachineKey(line) {
|
|
185
|
+
const m = ROOT_KEY_RE.exec(line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
186
|
+
if (m === null)
|
|
187
|
+
return null;
|
|
188
|
+
const key = m[1] ?? '';
|
|
189
|
+
if (!MACHINE_PATH_KEY_SET.has(key))
|
|
190
|
+
return null;
|
|
191
|
+
return { key, value: m[2] ?? '' };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 剥掉机器相关路径键(行级过滤)。
|
|
195
|
+
*
|
|
196
|
+
* 值写在同行 → 只删这一行;值写在缩进块里(`storeDir:` / `storeDir: |`)→ 连它的缩进续行
|
|
197
|
+
* 一起删,避免留下孤儿缩进行把文件变成坏 YAML。其余内容(含注释、空行、行尾缺换行)逐字不变。
|
|
198
|
+
*/
|
|
199
|
+
export function stripPnpmMachinePaths(text) {
|
|
200
|
+
if (text === '')
|
|
201
|
+
return { text, stripped: [] };
|
|
202
|
+
const lines = text.split('\n');
|
|
203
|
+
const out = [];
|
|
204
|
+
const stripped = [];
|
|
205
|
+
for (let i = 0; i < lines.length; i++) {
|
|
206
|
+
const line = lines[i] ?? '';
|
|
207
|
+
const hit = rootMachineKey(line);
|
|
208
|
+
if (hit === null) {
|
|
209
|
+
out.push(line);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!stripped.includes(hit.key))
|
|
213
|
+
stripped.push(hit.key);
|
|
214
|
+
if (isBlockValue(hit.value)) {
|
|
215
|
+
while (i + 1 < lines.length && /^[ \t]/.test(lines[i + 1] ?? ''))
|
|
216
|
+
i += 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { text: out.join('\n'), stripped };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 把**目标机自己原有的**机器路径行接回将要写入的文本(写回侧)。
|
|
223
|
+
*
|
|
224
|
+
* 来源机的路径已在收集侧剥离(这里对老载荷再剥一次是纵深防御),所以本函数接回的只可能是
|
|
225
|
+
* 目标机原本就有的设置 —— 既不搬运来源机的路径,也不让一次导入清掉本机自己的 store 位置。
|
|
226
|
+
* `local` 为 null(目标机本来就没有该文件)时原样返回。接回行统一用 incoming 的 EOL。
|
|
227
|
+
*/
|
|
228
|
+
export function restorePnpmMachinePaths(incoming, local) {
|
|
229
|
+
if (incoming === '' || local === null || local === undefined || local === '') {
|
|
230
|
+
return { text: incoming, restored: [] };
|
|
231
|
+
}
|
|
232
|
+
const lines = local.split('\n');
|
|
233
|
+
const kept = [];
|
|
234
|
+
const restored = [];
|
|
235
|
+
for (let i = 0; i < lines.length; i++) {
|
|
236
|
+
const raw = lines[i] ?? '';
|
|
237
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
238
|
+
const hit = rootMachineKey(line);
|
|
239
|
+
if (hit === null)
|
|
240
|
+
continue;
|
|
241
|
+
if (!restored.includes(hit.key))
|
|
242
|
+
restored.push(hit.key);
|
|
243
|
+
kept.push(line);
|
|
244
|
+
if (isBlockValue(hit.value)) {
|
|
245
|
+
while (i + 1 < lines.length && /^[ \t]/.test(lines[i + 1] ?? '')) {
|
|
246
|
+
i += 1;
|
|
247
|
+
const cont = lines[i] ?? '';
|
|
248
|
+
kept.push(cont.endsWith('\r') ? cont.slice(0, -1) : cont);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (kept.length === 0)
|
|
253
|
+
return { text: incoming, restored: [] };
|
|
254
|
+
const eol = incoming.includes('\r\n') ? '\r\n' : '\n';
|
|
255
|
+
const head = incoming.endsWith('\n') ? incoming : incoming + eol;
|
|
256
|
+
return { text: head + kept.map((l) => l + eol).join(''), restored };
|
|
257
|
+
}
|
|
147
258
|
//# sourceMappingURL=pnpm-workspace.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MsgFunc } from '../core/messages.ts';
|
|
2
2
|
import './messages.ts';
|
|
3
3
|
import type { PromptEntry } from '../schema/types.ts';
|
|
4
|
-
import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
|
|
4
|
+
import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
|
|
5
5
|
/** 导出记录:PromptEntry 之外记录来源行名与所在层(导入重建 / 写回定位用) */
|
|
6
6
|
export interface PromptExportEntry extends PromptEntry {
|
|
7
7
|
sourceLineName?: string;
|
|
@@ -34,7 +34,11 @@ export declare class PromptsAdapter implements ConfigAdapter<PromptsExportSectio
|
|
|
34
34
|
readonly displayName: string;
|
|
35
35
|
readonly defaultIncluded: boolean;
|
|
36
36
|
readonly portability: import("../schema/section-registry.ts").SectionPortability;
|
|
37
|
-
export(ctx: HostContext,
|
|
37
|
+
export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<PromptsExportSection>>;
|
|
38
|
+
/** 单元 id:`prompts:<条目 id>`(条目 id 已含来源行与 persona/section 名,天然唯一) */
|
|
39
|
+
private unitId;
|
|
40
|
+
/** 条目级单元清单(零 I/O);label = 条目名,detail = 类型(数据,不是 UI 文案) */
|
|
41
|
+
listUnits(section: ExportSection<PromptsExportSection>): ExportUnit[];
|
|
38
42
|
analyzeImport(data: PromptsExportSection, ctx: ImportContext): Promise<PlanItem[]>;
|
|
39
43
|
applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
|
|
40
44
|
validate(data: PromptsExportSection, msg?: MsgFunc): Promise<ValidationResult>;
|
package/lib/adapters/prompts.js
CHANGED
|
@@ -18,6 +18,7 @@ import './messages.js';
|
|
|
18
18
|
import { locatePatchLineLayer, readEffectivePatchLines, resolveWriteLayer } from '../core/patch-layers.js';
|
|
19
19
|
import { sectionMeta } from '../schema/section-registry.js';
|
|
20
20
|
import { validateJsonSection } from './json-section.js';
|
|
21
|
+
import { entryByteSize, filterByUnits } from './units.js';
|
|
21
22
|
/**
|
|
22
23
|
* 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text)。
|
|
23
24
|
*
|
|
@@ -115,14 +116,15 @@ export class PromptsAdapter {
|
|
|
115
116
|
displayName = sectionMeta('prompts').displayName;
|
|
116
117
|
defaultIncluded = sectionMeta('prompts').defaultIncluded;
|
|
117
118
|
portability = sectionMeta('prompts').portability;
|
|
118
|
-
async export(ctx,
|
|
119
|
+
async export(ctx, options) {
|
|
119
120
|
const warnings = [];
|
|
120
121
|
// issue #71:两层都读(home 层优先),否则 profile 层里的 persona / planMode 全部丢失
|
|
121
122
|
const read = await readEffectivePatchLines(ctx.patchFile, ctx.profile);
|
|
122
123
|
for (const f of read.failures) {
|
|
123
124
|
warnings.push(msgOf(ctx)('adapter.patchReadFailedPrompts', { reason: `${f.file}: ${f.reason}` }));
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
+
// 条目级白名单:未勾选的 persona / plan-mode section 不进包
|
|
127
|
+
const prompts = filterByUnits(extractPrompts(read.lines), options.includeItems?.[this.id], (p) => this.unitId(p));
|
|
126
128
|
return {
|
|
127
129
|
sectionId: 'prompts',
|
|
128
130
|
data: { version: 1, prompts },
|
|
@@ -130,6 +132,19 @@ export class PromptsAdapter {
|
|
|
130
132
|
warnings,
|
|
131
133
|
};
|
|
132
134
|
}
|
|
135
|
+
/** 单元 id:`prompts:<条目 id>`(条目 id 已含来源行与 persona/section 名,天然唯一) */
|
|
136
|
+
unitId(prompt) {
|
|
137
|
+
return this.id + ':' + prompt.id;
|
|
138
|
+
}
|
|
139
|
+
/** 条目级单元清单(零 I/O);label = 条目名,detail = 类型(数据,不是 UI 文案) */
|
|
140
|
+
listUnits(section) {
|
|
141
|
+
return (section.data.prompts ?? []).map((p) => ({
|
|
142
|
+
id: this.unitId(p),
|
|
143
|
+
label: p.name,
|
|
144
|
+
detail: p.kind,
|
|
145
|
+
sizeBytes: entryByteSize(p),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
133
148
|
async analyzeImport(data, ctx) {
|
|
134
149
|
const msg = ctx.msg;
|
|
135
150
|
const items = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MsgFunc } from '../core/messages.ts';
|
|
2
2
|
import type { ProviderEntry } from '../schema/types.ts';
|
|
3
|
-
import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
|
|
3
|
+
import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, PlanItem, ValidationResult } from '../core/types.ts';
|
|
4
4
|
import { type NamespaceProvider } from './settings.ts';
|
|
5
5
|
/** 导出记录:ProviderEntry 之外附加 namespace 级元数据(导入写回用),满足 ProvidersSection 形状 */
|
|
6
6
|
export interface ProviderExportEntry extends ProviderEntry {
|
|
@@ -27,7 +27,24 @@ export declare class ProvidersAdapter implements ConfigAdapter<ProviderExportSec
|
|
|
27
27
|
readonly portability: import("../schema/section-registry.ts").SectionPortability;
|
|
28
28
|
private readonly namespaces;
|
|
29
29
|
constructor(namespaces?: string[] | NamespaceProvider);
|
|
30
|
-
export(ctx: HostContext,
|
|
30
|
+
export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<ProviderExportSection>>;
|
|
31
|
+
/** 单元 id:`providers:<route>`(route 是 providers 记录的键,天然唯一) */
|
|
32
|
+
private unitId;
|
|
33
|
+
/**
|
|
34
|
+
* 条目级单元清单(零 I/O):一台 provider(记录里的一个 route)= 一个单元。
|
|
35
|
+
*
|
|
36
|
+
* detail 给来源 namespace —— 用户靠它区分「同一个模型走哪个 ns」,不放任何 key 值。
|
|
37
|
+
*/
|
|
38
|
+
listUnits(section: ExportSection<ProviderExportSection>): ExportUnit[];
|
|
39
|
+
/**
|
|
40
|
+
* 白名单过滤 + raw 重建。
|
|
41
|
+
*
|
|
42
|
+
* 为什么必须重建 raw:每个 entry.raw 存的是**整个 namespace** 的快照(导入按 namespace 整体替换)。
|
|
43
|
+
* 只勾 llm-pi-ai 下的一条 route 时若原样带走 raw,未勾的 route 会随 raw 一起进包、
|
|
44
|
+
* 导入时又被整 namespace 写回目标机 —— 用户看到「没勾却没生效」,选择器等于撒谎。
|
|
45
|
+
* 因此这里把每个 namespace 的 raw.providers 收窄成「该 ns 里被勾选的 route」集合。
|
|
46
|
+
*/
|
|
47
|
+
private pickProviders;
|
|
31
48
|
analyzeImport(data: ProviderExportSection, ctx: ImportContext): Promise<PlanItem[]>;
|
|
32
49
|
applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
|
|
33
50
|
validate(data: ProviderExportSection, msg?: MsgFunc): Promise<ValidationResult>;
|
|
@@ -10,6 +10,7 @@ import { msgOf, zhMsg } from '../core/messages.js';
|
|
|
10
10
|
import { resolveNamespaces, isEmptyValue } from './settings.js';
|
|
11
11
|
import { sectionMeta } from '../schema/section-registry.js';
|
|
12
12
|
import { validateJsonSection } from './json-section.js';
|
|
13
|
+
import { entryByteSize, filterByUnits } from './units.js';
|
|
13
14
|
export const DEFAULT_PROVIDER_NAMESPACES = ['llm-deepseek', 'llm-pi-ai'];
|
|
14
15
|
/** 无 raw 时的兜底重建:只取 ProviderEntry 已知字段 */
|
|
15
16
|
function stripEntry(entry) {
|
|
@@ -30,7 +31,7 @@ export class ProvidersAdapter {
|
|
|
30
31
|
constructor(namespaces = DEFAULT_PROVIDER_NAMESPACES) {
|
|
31
32
|
this.namespaces = namespaces;
|
|
32
33
|
}
|
|
33
|
-
async export(ctx,
|
|
34
|
+
async export(ctx, options) {
|
|
34
35
|
const providers = {};
|
|
35
36
|
const warnings = [];
|
|
36
37
|
for (const ns of await resolveNamespaces(this.namespaces, ctx)) {
|
|
@@ -60,13 +61,68 @@ export class ProvidersAdapter {
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
const picked = this.pickProviders(providers, options.includeItems?.[this.id]);
|
|
63
65
|
return {
|
|
64
66
|
sectionId: 'providers',
|
|
65
|
-
data: { version: 1, providers },
|
|
66
|
-
counts: { providers: Object.keys(
|
|
67
|
+
data: { version: 1, providers: picked },
|
|
68
|
+
counts: { providers: Object.keys(picked).length },
|
|
67
69
|
warnings,
|
|
68
70
|
};
|
|
69
71
|
}
|
|
72
|
+
/** 单元 id:`providers:<route>`(route 是 providers 记录的键,天然唯一) */
|
|
73
|
+
unitId(route) {
|
|
74
|
+
return this.id + ':' + route;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 条目级单元清单(零 I/O):一台 provider(记录里的一个 route)= 一个单元。
|
|
78
|
+
*
|
|
79
|
+
* detail 给来源 namespace —— 用户靠它区分「同一个模型走哪个 ns」,不放任何 key 值。
|
|
80
|
+
*/
|
|
81
|
+
listUnits(section) {
|
|
82
|
+
return Object.entries(section.data.providers ?? {}).map(([route, entry]) => ({
|
|
83
|
+
id: this.unitId(route),
|
|
84
|
+
label: route,
|
|
85
|
+
detail: entry.namespace,
|
|
86
|
+
sizeBytes: entryByteSize(entry),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 白名单过滤 + raw 重建。
|
|
91
|
+
*
|
|
92
|
+
* 为什么必须重建 raw:每个 entry.raw 存的是**整个 namespace** 的快照(导入按 namespace 整体替换)。
|
|
93
|
+
* 只勾 llm-pi-ai 下的一条 route 时若原样带走 raw,未勾的 route 会随 raw 一起进包、
|
|
94
|
+
* 导入时又被整 namespace 写回目标机 —— 用户看到「没勾却没生效」,选择器等于撒谎。
|
|
95
|
+
* 因此这里把每个 namespace 的 raw.providers 收窄成「该 ns 里被勾选的 route」集合。
|
|
96
|
+
*/
|
|
97
|
+
pickProviders(providers, allow) {
|
|
98
|
+
const picked = filterByUnits(Object.entries(providers), allow, ([route]) => this.unitId(route));
|
|
99
|
+
const out = {};
|
|
100
|
+
const keepByNs = new Map();
|
|
101
|
+
for (const [route, entry] of picked) {
|
|
102
|
+
out[route] = entry;
|
|
103
|
+
const keep = keepByNs.get(entry.namespace) ?? new Set();
|
|
104
|
+
keep.add(route);
|
|
105
|
+
keepByNs.set(entry.namespace, keep);
|
|
106
|
+
}
|
|
107
|
+
if (allow === undefined)
|
|
108
|
+
return out; // 全量导出不动 raw(保持与旧备份逐字节一致)
|
|
109
|
+
for (const entry of Object.values(out)) {
|
|
110
|
+
const raw = entry.raw;
|
|
111
|
+
if (raw === null || typeof raw !== 'object')
|
|
112
|
+
continue;
|
|
113
|
+
const pv = raw['providers'];
|
|
114
|
+
if (pv === null || typeof pv !== 'object' || Array.isArray(pv))
|
|
115
|
+
continue;
|
|
116
|
+
const keep = keepByNs.get(entry.namespace) ?? new Set();
|
|
117
|
+
const narrowed = {};
|
|
118
|
+
for (const [route, value] of Object.entries(pv)) {
|
|
119
|
+
if (keep.has(route))
|
|
120
|
+
narrowed[route] = value;
|
|
121
|
+
}
|
|
122
|
+
entry.raw = { ...raw, providers: narrowed };
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
70
126
|
async analyzeImport(data, ctx) {
|
|
71
127
|
const msg = ctx.msg;
|
|
72
128
|
const items = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MsgFunc } from '../core/messages.ts';
|
|
2
2
|
import type { NamespaceRecord, SettingsSection } from '../schema/types.ts';
|
|
3
|
-
import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext, ImportContext, NamespaceInfo, PlanItem, ValidationResult } from '../core/types.ts';
|
|
3
|
+
import type { ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext, ImportContext, NamespaceInfo, PlanItem, ValidationResult } from '../core/types.ts';
|
|
4
4
|
/** namespace 清单提供者:宿主从 settings.yaml 顶层 key / 插件注册表获得 */
|
|
5
5
|
export type NamespaceProvider = (ctx: HostContext) => Promise<string[]> | string[];
|
|
6
6
|
export declare function resolveNamespaces(namespaces: string[] | NamespaceProvider, ctx: HostContext): Promise<string[]>;
|
|
@@ -39,7 +39,11 @@ export declare class SettingsAdapter implements ConfigAdapter<SettingsSection> {
|
|
|
39
39
|
readonly portability: import("../schema/section-registry.ts").SectionPortability;
|
|
40
40
|
private readonly namespaces;
|
|
41
41
|
constructor(namespaces?: string[] | NamespaceProvider);
|
|
42
|
-
export(ctx: HostContext,
|
|
42
|
+
export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<SettingsSection>>;
|
|
43
|
+
/** 单元 id:`settings:<namespace>`(同一命名空间下的唯一键) */
|
|
44
|
+
private unitId;
|
|
45
|
+
/** 条目级单元清单(零 I/O:输入即 export 产物);一个 namespace = 一个单元 */
|
|
46
|
+
listUnits(section: ExportSection<SettingsSection>): ExportUnit[];
|
|
43
47
|
analyzeImport(data: SettingsSection, ctx: ImportContext): Promise<PlanItem[]>;
|
|
44
48
|
applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult>;
|
|
45
49
|
validate(data: SettingsSection, msg?: MsgFunc): Promise<ValidationResult>;
|
package/lib/adapters/settings.js
CHANGED
|
@@ -11,6 +11,7 @@ import { msgOf, zhMsg } from '../core/messages.js';
|
|
|
11
11
|
import { isUiNamespace } from './ui.js';
|
|
12
12
|
import { sectionMeta } from '../schema/section-registry.js';
|
|
13
13
|
import { validateJsonSection } from './json-section.js';
|
|
14
|
+
import { pickRecord, recordUnits } from './units.js';
|
|
14
15
|
export async function resolveNamespaces(namespaces, ctx) {
|
|
15
16
|
return typeof namespaces === 'function' ? namespaces(ctx) : namespaces;
|
|
16
17
|
}
|
|
@@ -154,13 +155,15 @@ export class SettingsAdapter {
|
|
|
154
155
|
constructor(namespaces = []) {
|
|
155
156
|
this.namespaces = namespaces;
|
|
156
157
|
}
|
|
157
|
-
async export(ctx,
|
|
158
|
+
async export(ctx, options) {
|
|
158
159
|
const warnings = [];
|
|
159
160
|
const all = await resolveNamespaces(this.namespaces, ctx);
|
|
160
161
|
const nonUi = all.filter((n) => !isUiNamespace(n));
|
|
161
162
|
if (nonUi.length === 0)
|
|
162
163
|
warnings.push(msgOf(ctx)('adapter.noSettingsNamespaces'));
|
|
163
|
-
|
|
164
|
+
// 条目级白名单:未勾选的 namespace 整条不进包(勾了却全量搬 = 选择器在撒谎)
|
|
165
|
+
const collected = await collectNamespaceRecords(ctx, nonUi, warnings);
|
|
166
|
+
const namespaces = pickRecord(collected, options.includeItems?.[this.id], (ns) => this.unitId(ns));
|
|
164
167
|
return {
|
|
165
168
|
sectionId: 'settings',
|
|
166
169
|
data: { version: 1, namespaces },
|
|
@@ -168,6 +171,14 @@ export class SettingsAdapter {
|
|
|
168
171
|
warnings,
|
|
169
172
|
};
|
|
170
173
|
}
|
|
174
|
+
/** 单元 id:`settings:<namespace>`(同一命名空间下的唯一键) */
|
|
175
|
+
unitId(namespace) {
|
|
176
|
+
return this.id + ':' + namespace;
|
|
177
|
+
}
|
|
178
|
+
/** 条目级单元清单(零 I/O:输入即 export 产物);一个 namespace = 一个单元 */
|
|
179
|
+
listUnits(section) {
|
|
180
|
+
return recordUnits(this.id, section.data.namespaces ?? {});
|
|
181
|
+
}
|
|
171
182
|
async analyzeImport(data, ctx) {
|
|
172
183
|
return planNamespaceItems(data.namespaces, 'settings', ctx);
|
|
173
184
|
}
|