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/src/adapters/plugins.ts
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.ts';
|
|
16
|
-
import {
|
|
16
|
+
import { isBundlesOnlyEntry, mergeBundleDeclaration, pluginInstallTarget, resolveProcessProfileName } from '../core/plugin-cli.ts';
|
|
17
17
|
import { classifyPluginSpec, LOCAL_PLUGIN_DIR, resolveLocalPluginPath } from '../core/local-plugin-pack.ts';
|
|
18
18
|
import type { PackLocalPluginsResult } from '../core/local-plugin-pack.ts';
|
|
19
19
|
import { msgOf, zhMsg } from '../core/messages.ts';
|
|
@@ -24,7 +24,9 @@ import { isPathSafe, normalizePath } from '../utils/paths.ts';
|
|
|
24
24
|
import { isENOENT } from '../utils/guards.ts';
|
|
25
25
|
import { PLUGIN_PATCH_REF_PREFIX, PLUGIN_PROFILE_MANIFEST_REF } from '../core/backup.ts';
|
|
26
26
|
import { readEffectivePatchLines, resolveWriteLayer, USER_PATCH_FILE } from '../core/patch-layers.ts';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
parsePnpmPatchedDependencies, restorePnpmMachinePaths, sanitizePnpmWorkspacePatches, stripPnpmMachinePaths,
|
|
29
|
+
} from './pnpm-workspace.ts';
|
|
28
30
|
import type { LocalPluginTarball, PatchLine, PluginEntry, PluginsSection, PnpmPatchFile } from '../schema/types.ts';
|
|
29
31
|
import type {
|
|
30
32
|
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext,
|
|
@@ -117,6 +119,21 @@ export const PNPM_PROFILE_DIR = (profile: string | undefined): string =>
|
|
|
117
119
|
export const PNPM_WORKSPACE_REL = (profile: string | undefined): string =>
|
|
118
120
|
`${PNPM_PROFILE_DIR(profile)}/pnpm-workspace.yaml`;
|
|
119
121
|
|
|
122
|
+
/**
|
|
123
|
+
* pnpm 配置可移植净化的可见文案(导出告警 / 导入计划项 detail / 写入结果三处**同一句**,
|
|
124
|
+
* 不另造通道):来源机的机器路径键被剥离、本机原有的机器路径键被保留,都必须说出来。
|
|
125
|
+
*/
|
|
126
|
+
function machinePathNotes(msg: MsgFunc, stripped: string[], restored: string[]): string[] {
|
|
127
|
+
const notes: string[] = [];
|
|
128
|
+
if (stripped.length > 0) {
|
|
129
|
+
notes.push(msg('adapter.pwMachinePathStripped', { count: String(stripped.length), keys: stripped.join(', ') }));
|
|
130
|
+
}
|
|
131
|
+
if (restored.length > 0) {
|
|
132
|
+
notes.push(msg('adapter.pwMachinePathKept', { count: String(restored.length), keys: restored.join(', ') }));
|
|
133
|
+
}
|
|
134
|
+
return notes;
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
/** 单个 patch 文件随备份迁移的体积上限(issue #35;patch 是纯文本,超过说明放错了东西)。 */
|
|
121
138
|
export const MAX_PATCH_FILE_BYTES = 2 * 1024 * 1024;
|
|
122
139
|
|
|
@@ -282,7 +299,9 @@ function unitsOf(
|
|
|
282
299
|
const patchFileIds = (data.patchFiles ?? []).map((pf) => `plugins:patch:${normalizePath(pf.relativePath)}`);
|
|
283
300
|
const group = [wsId, ...patchFileIds];
|
|
284
301
|
const units: ExportUnit[] = [];
|
|
285
|
-
|
|
302
|
+
// data.plugins 可能缺省(空分区 / 手写 section):按空表处理,不抛错 ——
|
|
303
|
+
// 选择器对任何分区都会调 listUnits,抛错会让整行退化成不可展开。
|
|
304
|
+
for (const pl of data.plugins ?? []) {
|
|
286
305
|
const tarball = tarballOf(data, pl.name);
|
|
287
306
|
const measured = tarballBytes?.get(pl.name);
|
|
288
307
|
const areas = related?.get(pl.name);
|
|
@@ -297,7 +316,7 @@ function unitsOf(
|
|
|
297
316
|
...(areas !== undefined && areas.length > 0 ? { relatedAreas: areas } : {}),
|
|
298
317
|
});
|
|
299
318
|
}
|
|
300
|
-
for (const line of data.patch) {
|
|
319
|
+
for (const line of data.patch ?? []) {
|
|
301
320
|
// 由 mcp / prompts adapter 管理的行不属本分区(applyItem 同样跳过),列出来只会误导用户
|
|
302
321
|
if (isManagedElsewhere(line.raw)) continue;
|
|
303
322
|
units.push({ id: `patch:${line.lineId}`, label: line.lineId, sizeBytes: 0 });
|
|
@@ -422,7 +441,16 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
422
441
|
try {
|
|
423
442
|
const rel = PNPM_WORKSPACE_REL(ctx.profile);
|
|
424
443
|
if (await ctx.fs.exists(rel)) {
|
|
425
|
-
|
|
444
|
+
const raw = new TextDecoder().decode(await ctx.fs.readFile(rel));
|
|
445
|
+
// 可移植性净化(收集侧):storeDir / cacheDir … 的值是本机绝对路径,跟着快照跑到
|
|
446
|
+
// 别的机器上必然指向不存在的目录 → 只带「可移植」的那部分(逐行删除,其余逐字不变)。
|
|
447
|
+
const strip = stripPnpmMachinePaths(raw);
|
|
448
|
+
pnpmWorkspace = strip.text;
|
|
449
|
+
if (strip.stripped.length > 0) {
|
|
450
|
+
warnings.push(msgOf(ctx)('adapter.pwMachinePathStripped', {
|
|
451
|
+
count: String(strip.stripped.length), keys: strip.stripped.join(', '),
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
426
454
|
}
|
|
427
455
|
} catch (err) {
|
|
428
456
|
warnings.push(msgOf(ctx)('adapter.pnpmReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
|
|
@@ -708,7 +736,10 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
708
736
|
// issue #35:目标机满足不了的 patchedDependencies 声明必须剔除——否则写进去之后,
|
|
709
737
|
// 目标机 pnpm 会因为「patch 文件读不到」拒绝**一切** add(含本次要装的插件)。
|
|
710
738
|
const available = await this.patchAvailability(data, ctx);
|
|
711
|
-
|
|
739
|
+
// 可移植性净化(写回侧·纵深防御):老版本备份 / 第三方工具产出的载荷里可能仍带着
|
|
740
|
+
// 来源机的机器路径键(本版本收集侧已剥离,正常路径这里就是 no-op)。
|
|
741
|
+
const stripped = stripPnpmMachinePaths(data.pnpmWorkspace);
|
|
742
|
+
const clean = sanitizePnpmWorkspacePatches(stripped.text, (rel) => available.has(rel));
|
|
712
743
|
const droppedNames = clean.dropped.map((d) => d.name).join(', ');
|
|
713
744
|
let current: string | null = null;
|
|
714
745
|
try {
|
|
@@ -719,15 +750,27 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
719
750
|
} catch {
|
|
720
751
|
current = null;
|
|
721
752
|
}
|
|
722
|
-
|
|
753
|
+
// 写回侧:不搬运来源机的机器路径,也不清掉**本机**自己的设置 → 接回目标机原有的那几行。
|
|
754
|
+
const merged = restorePnpmMachinePaths(clean.text, current);
|
|
755
|
+
// 「只差机器路径行」= 目标机现文本剥掉它自己的机器路径行后与将要写入的内容逐字一致。
|
|
756
|
+
// 此时写回只会把本机那几行挪到末尾(YAML 映射顺序无语义),没有意义 → 不生成计划项:
|
|
757
|
+
// 零写入、本机设置原地不动,也不给用户造一条无意义的 Update 噪音。
|
|
758
|
+
const onlyMachinePaths = current !== null && stripPnpmMachinePaths(current).text === clean.text;
|
|
759
|
+
// 剥完机器路径键后**没有可移植内容**(来源机的配置只剩机器路径)→ 不产出空文件项:
|
|
760
|
+
// 「无内容形态不得交给 importer」与 collect() 的口径一致。
|
|
761
|
+
if (merged.text !== '' && !onlyMachinePaths && current !== merged.text) {
|
|
762
|
+
const details = machinePathNotes(msg, stripped.stripped, merged.restored);
|
|
763
|
+
if (clean.dropped.length > 0) {
|
|
764
|
+
// 既有口径不变:剔除声明的那句话排在最前
|
|
765
|
+
details.unshift(msg('adapter.pwPatchesDroppedDetail', { count: String(clean.dropped.length), names: droppedNames }));
|
|
766
|
+
}
|
|
723
767
|
items.push({
|
|
724
768
|
id: 'plugins:pnpm-workspace',
|
|
725
769
|
kind: current === null ? 'Create' : 'Update',
|
|
726
770
|
adapter: 'plugins',
|
|
727
771
|
description: current === null ? msg('adapter.pwCreate') : msg('adapter.pwUpdate'),
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
: {}),
|
|
772
|
+
// detail 非空 ⇒ isToolchainChangeItem ⇒ 进确认列表(可见、可取消)——净化绝不静默
|
|
773
|
+
...(details.length > 0 ? { detail: details.join(msg('adapter.listSeparator')) } : {}),
|
|
731
774
|
severity: 'info',
|
|
732
775
|
target: { adapter: 'plugins', ref: 'pnpm-workspace.yaml' },
|
|
733
776
|
});
|
|
@@ -896,9 +939,31 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
896
939
|
if (!isENOENT(err)) throw err;
|
|
897
940
|
}
|
|
898
941
|
}
|
|
899
|
-
|
|
942
|
+
// 可移植性净化(写回侧):先剥掉载荷里的机器路径键(纵深防御),再把目标机**原有的**
|
|
943
|
+
// 那几行接回去 —— 既不搬运来源机的路径,也不清掉本机自己的 store / 缓存位置。
|
|
944
|
+
const stripped = stripPnpmMachinePaths(text);
|
|
945
|
+
const clean = sanitizePnpmWorkspacePatches(stripped.text, (rel) => available.has(rel));
|
|
946
|
+
const wsRel = PNPM_WORKSPACE_REL(ctx.target.profile);
|
|
947
|
+
let local: string | null = null;
|
|
900
948
|
try {
|
|
901
|
-
await ctx.target.fs.
|
|
949
|
+
local = (await ctx.target.fs.exists(wsRel))
|
|
950
|
+
? new TextDecoder().decode(await ctx.target.fs.readFile(wsRel))
|
|
951
|
+
: null;
|
|
952
|
+
} catch (err) {
|
|
953
|
+
// 「存在但读不到」必须显式失败(只有 ENOENT 才算本来就没有):读不到就无法保证本机的
|
|
954
|
+
// 路径设置不被清掉 —— 宁可不写,也不静默丢掉本机的 store / 缓存位置。
|
|
955
|
+
if (!isENOENT(err)) {
|
|
956
|
+
return {
|
|
957
|
+
ok: false,
|
|
958
|
+
warning: true,
|
|
959
|
+
message: msg('adapter.pwLocalReadFailed', { reason: err instanceof Error ? err.message : String(err) }),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
local = null;
|
|
963
|
+
}
|
|
964
|
+
const merged = restorePnpmMachinePaths(clean.text, local);
|
|
965
|
+
try {
|
|
966
|
+
await ctx.target.fs.writeFile(wsRel, new TextEncoder().encode(merged.text));
|
|
902
967
|
const notes: string[] = [msg('adapter.pwWritten')];
|
|
903
968
|
if (clean.dropped.length > 0) {
|
|
904
969
|
notes.push(msg('adapter.pwWrittenPatchesDropped', {
|
|
@@ -906,6 +971,7 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
906
971
|
names: clean.dropped.map((d) => d.name).join(', '),
|
|
907
972
|
}));
|
|
908
973
|
}
|
|
974
|
+
notes.push(...machinePathNotes(msg, stripped.stripped, merged.restored));
|
|
909
975
|
return { ok: true, needsRestart: true, message: notes.join(msg('adapter.listSeparator')) };
|
|
910
976
|
} catch (err) {
|
|
911
977
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -933,27 +999,47 @@ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
|
|
|
933
999
|
return await this.declareHostBundle(name, ctx);
|
|
934
1000
|
}
|
|
935
1001
|
try {
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
// T1:备份内自带 tarball → 解包到 homeDir 内的本地插件缓存目录,把 spec 重写为绝对路径。
|
|
1002
|
+
// 安装目标由 pluginInstallTarget 统一决策(issue #101):备份里有具体版本时精确安装,
|
|
1003
|
+
// 其余 registry 包带「包名@spec」;非 registry 来源原样。
|
|
1004
|
+
// T1:备份内自带 tarball → 解包到 homeDir 内的本地插件缓存目录,改以 file:<abs> 安装。
|
|
940
1005
|
// 这是「换机后本地插件不再丢失」的关键:原始绝对路径在目标机不存在,解包出的 tgz 一定存在。
|
|
1006
|
+
let tarballPath: string | undefined;
|
|
941
1007
|
const tarball = data !== undefined ? tarballOf(data, name) : undefined;
|
|
942
1008
|
if (tarball !== undefined) {
|
|
943
1009
|
try {
|
|
944
|
-
|
|
945
|
-
spec = `file:${abs}`;
|
|
1010
|
+
tarballPath = await writeLocalTarball(ctx, tarball);
|
|
946
1011
|
} catch (err) {
|
|
947
1012
|
// 解包失败不阻断安装尝试:退回原 spec(可能失败,但会如实报错而非静默丢失)
|
|
948
1013
|
ctx.onLog?.(msg('adapter.localTarballUnpackFailed', { name, err: err instanceof Error ? err.message : String(err) }));
|
|
949
1014
|
}
|
|
950
1015
|
}
|
|
1016
|
+
const target = pluginInstallTarget(name, { version: entry?.version, spec: entry?.spec, tarballPath });
|
|
951
1017
|
// 执行日志:记录实际将发起的子进程命令行(与宿主 DshPluginsFacade 的
|
|
952
|
-
// dsh plugin --profile <p> add <
|
|
953
|
-
ctx.onLog?.(`$ dsh plugin --profile ${ctx.target.profile ?? resolveProcessProfileName()} add ${
|
|
1018
|
+
// dsh plugin --profile <p> add <target> 一致);仅非敏感文本,渲染前 UI 再 redact 兜底
|
|
1019
|
+
ctx.onLog?.(`$ dsh plugin --profile ${ctx.target.profile ?? resolveProcessProfileName()} add ${target}`);
|
|
954
1020
|
// 透传中止信号:用户「跳过当前插件」→ 宿主 kill 子进程 + 清半装状态 → 抛 ImportUserSkippedError
|
|
955
|
-
const result = await ctx.target.plugins.install(name,
|
|
1021
|
+
const result = await ctx.target.plugins.install(name, target, ctx.signal);
|
|
956
1022
|
const suffix = result.needsRestart ? msg('adapter.pluginRestartSuffix') : '';
|
|
1023
|
+
// 落地校验(issue #101):官方 CLI 退出 0 不等于版本变了 —— pnpm 会尊重目标机既有
|
|
1024
|
+
// package.json spec,固定版已装、区间已满足时「什么都没装」同样返回 0。回读实际版本再下结论。
|
|
1025
|
+
const expected = entry?.version;
|
|
1026
|
+
const actual = result.installedVersion ?? null;
|
|
1027
|
+
if (expected !== undefined && expected !== '' && actual !== null && actual !== expected) {
|
|
1028
|
+
return {
|
|
1029
|
+
ok: false,
|
|
1030
|
+
warning: true,
|
|
1031
|
+
needsRestart: true,
|
|
1032
|
+
message: msg('adapter.pluginVersionMismatch', {
|
|
1033
|
+
name,
|
|
1034
|
+
expected,
|
|
1035
|
+
actual,
|
|
1036
|
+
command: `dsh plugin --profile ${ctx.target.profile ?? resolveProcessProfileName()} add ${name}@${expected}`,
|
|
1037
|
+
}),
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (actual !== null) {
|
|
1041
|
+
return { ok: true, needsRestart: true, message: msg('adapter.pluginInstalledAtVersion', { name, version: actual, suffix }) };
|
|
1042
|
+
}
|
|
957
1043
|
return {
|
|
958
1044
|
ok: true,
|
|
959
1045
|
needsRestart: true,
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* issue #35 回归:patchedDependencies 的解析与「目标机没有 patch 文件」条目的剔除。
|
|
3
3
|
* 目标:绝不把「声明存在、文件不存在」的组合写到目标机(否则 pnpm 拒绝一切 add)。
|
|
4
|
+
*
|
|
5
|
+
* 跨机可移植性回归(④):机器相关 pnpm 路径键的行级剥离与「本机设置保留」的回接。
|
|
4
6
|
*/
|
|
5
7
|
import test from 'node:test';
|
|
6
8
|
import assert from 'node:assert/strict';
|
|
7
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
parsePnpmPatchedDependencies, restorePnpmMachinePaths, sanitizePnpmWorkspacePatches, stripPnpmMachinePaths,
|
|
11
|
+
} from './pnpm-workspace.ts';
|
|
8
12
|
|
|
9
13
|
const SOURCE = [
|
|
10
14
|
'packages:',
|
|
@@ -85,3 +89,87 @@ test('sanitize:没有 patchedDependencies 时不产生任何改动', () => {
|
|
|
85
89
|
assert.deepEqual(r.declared, []);
|
|
86
90
|
assert.equal(r.unsupported, null);
|
|
87
91
|
});
|
|
92
|
+
|
|
93
|
+
/* ---------------- 可移植性净化:机器相关路径键(④) ---------------- */
|
|
94
|
+
|
|
95
|
+
test('strip:剥离根层机器路径键(含 kebab 方言),其余内容与注释逐字不变', () => {
|
|
96
|
+
const text = [
|
|
97
|
+
'storeDir: D:/pnpm-store',
|
|
98
|
+
'packages:',
|
|
99
|
+
" - 'plugins/*'",
|
|
100
|
+
'cache-dir: C:/pnpm-cache # 注释也一起走',
|
|
101
|
+
'allowBuilds:',
|
|
102
|
+
' ssh2: true',
|
|
103
|
+
'minimumReleaseAge: 1440',
|
|
104
|
+
'',
|
|
105
|
+
].join('\n');
|
|
106
|
+
const r = stripPnpmMachinePaths(text);
|
|
107
|
+
assert.deepEqual(r.stripped, ['storeDir', 'cache-dir']);
|
|
108
|
+
assert.equal(r.text, [
|
|
109
|
+
'packages:',
|
|
110
|
+
" - 'plugins/*'",
|
|
111
|
+
'allowBuilds:',
|
|
112
|
+
' ssh2: true',
|
|
113
|
+
'minimumReleaseAge: 1440',
|
|
114
|
+
'',
|
|
115
|
+
].join('\n'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('strip:不含机器路径键 → 逐字节不变(含缩进块 / 行尾注释 / 行尾缺换行)', () => {
|
|
119
|
+
const text = "# 本机配置\nallowBuilds:\n ssh2: true # 说明\npatchedDependencies:\n a: patches/a.patch";
|
|
120
|
+
const r = stripPnpmMachinePaths(text);
|
|
121
|
+
assert.equal(r.text, text);
|
|
122
|
+
assert.deepEqual(r.stripped, []);
|
|
123
|
+
const empty = stripPnpmMachinePaths('');
|
|
124
|
+
assert.equal(empty.text, '');
|
|
125
|
+
assert.deepEqual(empty.stripped, []);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('strip:只有根层才算设置 —— 缩进块里的同名键一律不碰', () => {
|
|
129
|
+
const text = 'pluginFoo:\n storeDir: /nested/path\nstoreDir: /root/path\n';
|
|
130
|
+
const r = stripPnpmMachinePaths(text);
|
|
131
|
+
assert.deepEqual(r.stripped, ['storeDir']);
|
|
132
|
+
assert.equal(r.text, 'pluginFoo:\n storeDir: /nested/path\n');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('strip:CRLF 输入不引入 LF 漂移(被删行之外逐字保留 CRLF)', () => {
|
|
136
|
+
const text = 'storeDir: D:/pnpm-store\r\nallowBuilds:\r\n ssh2: true\r\n';
|
|
137
|
+
const r = stripPnpmMachinePaths(text);
|
|
138
|
+
assert.equal(r.text, 'allowBuilds:\r\n ssh2: true\r\n');
|
|
139
|
+
assert.ok(!r.text.includes('\n\n'), '不得产生空行');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('strip:块标量形态连缩进续行一起删(不留孤儿缩进行 → 不产出坏 YAML)', () => {
|
|
143
|
+
const text = 'storeDir: |\n D:/pnpm-store\nallowBuilds:\n ssh2: true\n';
|
|
144
|
+
const r = stripPnpmMachinePaths(text);
|
|
145
|
+
assert.equal(r.text, 'allowBuilds:\n ssh2: true\n');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('strip:整份文件只有机器路径键 → 结果为空(上层据此不再携带该单元)', () => {
|
|
149
|
+
const r = stripPnpmMachinePaths('storeDir: D:/pnpm-store\n');
|
|
150
|
+
assert.equal(r.text, '');
|
|
151
|
+
assert.deepEqual(r.stripped, ['storeDir']);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('restore:接回本机原有的机器路径行(来源机的路径早已被剥离)', () => {
|
|
155
|
+
const local = 'storeDir: D:/pnpm-store\nallowBuilds:\n ssh2: true\n';
|
|
156
|
+
const incoming = 'allowBuilds:\n ssh2: false\n';
|
|
157
|
+
const r = restorePnpmMachinePaths(incoming, local);
|
|
158
|
+
assert.deepEqual(r.restored, ['storeDir']);
|
|
159
|
+
assert.equal(r.text, 'allowBuilds:\n ssh2: false\nstoreDir: D:/pnpm-store\n');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('restore:本机没有这些键 / 本机没有该文件 / incoming 为空 → 原样返回', () => {
|
|
163
|
+
const incoming = 'allowBuilds:\n ssh2: false\n';
|
|
164
|
+
assert.deepEqual(restorePnpmMachinePaths(incoming, null), { text: incoming, restored: [] });
|
|
165
|
+
assert.deepEqual(restorePnpmMachinePaths(incoming, ''), { text: incoming, restored: [] });
|
|
166
|
+
assert.deepEqual(restorePnpmMachinePaths(incoming, 'packages:\n - "*"\n'), { text: incoming, restored: [] });
|
|
167
|
+
assert.deepEqual(restorePnpmMachinePaths('', 'storeDir: /x\n'), { text: '', restored: [] });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('restore:接回行沿用 incoming 的 EOL;incoming 无尾换行时先补齐', () => {
|
|
171
|
+
const crlf = restorePnpmMachinePaths('allowBuilds:\r\n ssh2: false\r\n', 'storeDir: D:/s\r\n');
|
|
172
|
+
assert.equal(crlf.text, 'allowBuilds:\r\n ssh2: false\r\nstoreDir: D:/s\r\n');
|
|
173
|
+
const noTail = restorePnpmMachinePaths('allowBuilds:', 'storeDir: D:/s\n');
|
|
174
|
+
assert.equal(noTail.text, 'allowBuilds:\nstoreDir: D:/s\n');
|
|
175
|
+
});
|
|
@@ -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.ts';
|
|
16
18
|
|
|
@@ -148,3 +150,126 @@ export function sanitizePnpmWorkspacePatches(
|
|
|
148
150
|
unsupported: null,
|
|
149
151
|
};
|
|
150
152
|
}
|
|
153
|
+
|
|
154
|
+
/* ------------------------------------------------------------------ *
|
|
155
|
+
* 可移植性净化(跨机同步):机器相关路径键
|
|
156
|
+
*
|
|
157
|
+
* `storeDir` / `cacheDir` … 的值是**本机绝对路径**。它们跟着快照跑到另一台机器后必然指向
|
|
158
|
+
* 不存在的目录(pnpm 会去那里建 store / 缓存)。纪律只有两条:
|
|
159
|
+
* 1. 收集(导出 / push)与写回(导入 / pull)两侧都**整行删除**这些键 —— 其余内容逐字不变,
|
|
160
|
+
* EOL 原样保留(CRLF 输入绝不引入 LF 漂移);只删「根层 `key:`」形态,
|
|
161
|
+
* 缩进块里的同名键(如插件的自定义配置)一律不碰;
|
|
162
|
+
* 2. 写回时把**目标机自己原有的**那几行接回去:不搬运来源机的路径,也不清掉本机自己的设置。
|
|
163
|
+
*
|
|
164
|
+
* `.npmrc` 刻意**不进同步面**(见 plugins.ts 的说明):纳入它会改变 bundle 载荷形态
|
|
165
|
+
* (docs/spec/bundle-format-v1.md + conformance),而本机 .npmrc 从来就不由本插件写 ——
|
|
166
|
+
* 「不碰 = 本机保留」。
|
|
167
|
+
* ------------------------------------------------------------------ */
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 机器相关 pnpm 设置键(根层)。camelCase 是 pnpm-workspace.yaml 的权威拼写;
|
|
171
|
+
* kebab 变体是同一组设置的另一方言(.npmrc / 部分版本),根层出现同样是本机路径绑定。
|
|
172
|
+
*/
|
|
173
|
+
export const PNPM_MACHINE_PATH_KEYS: readonly string[] = [
|
|
174
|
+
'storeDir', 'store-dir',
|
|
175
|
+
'globalDir', 'global-dir',
|
|
176
|
+
'globalBinDir', 'global-bin-dir',
|
|
177
|
+
'cacheDir', 'cache-dir',
|
|
178
|
+
'stateDir', 'state-dir',
|
|
179
|
+
'configDir', 'config-dir',
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const MACHINE_PATH_KEY_SET: ReadonlySet<string> = new Set(PNPM_MACHINE_PATH_KEYS);
|
|
183
|
+
|
|
184
|
+
/** 根层的 `key: value` 形态;缩进行、注释行、块标量内容都不会命中(它们不从行首开始)。 */
|
|
185
|
+
const ROOT_KEY_RE = /^([A-Za-z_][A-Za-z0-9_-]*)[ \t]*:(.*)$/;
|
|
186
|
+
|
|
187
|
+
/** 空值或 YAML 块标量(`|` / `>` 及其修饰符)⇒ 值写在其后的缩进行里 */
|
|
188
|
+
function isBlockValue(raw: string): boolean {
|
|
189
|
+
const v = raw.trim();
|
|
190
|
+
return v === '' || /^[|>][+-]?\d*$/.test(v);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 行首若是机器相关键 → 返回键名与冒号后的原文;否则 null(`\r` 不算值的一部分) */
|
|
194
|
+
function rootMachineKey(line: string): { key: string; value: string } | null {
|
|
195
|
+
const m = ROOT_KEY_RE.exec(line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
196
|
+
if (m === null) return null;
|
|
197
|
+
const key = m[1] ?? '';
|
|
198
|
+
if (!MACHINE_PATH_KEY_SET.has(key)) return null;
|
|
199
|
+
return { key, value: m[2] ?? '' };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface MachinePathStripResult {
|
|
203
|
+
/** 剥离后的文本(无需改动时与入参**逐字节相同**;只做整行删除,EOL 原样保留) */
|
|
204
|
+
text: string;
|
|
205
|
+
/** 被剥离的键(按首次出现顺序去重) */
|
|
206
|
+
stripped: string[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 剥掉机器相关路径键(行级过滤)。
|
|
211
|
+
*
|
|
212
|
+
* 值写在同行 → 只删这一行;值写在缩进块里(`storeDir:` / `storeDir: |`)→ 连它的缩进续行
|
|
213
|
+
* 一起删,避免留下孤儿缩进行把文件变成坏 YAML。其余内容(含注释、空行、行尾缺换行)逐字不变。
|
|
214
|
+
*/
|
|
215
|
+
export function stripPnpmMachinePaths(text: string): MachinePathStripResult {
|
|
216
|
+
if (text === '') return { text, stripped: [] };
|
|
217
|
+
const lines = text.split('\n');
|
|
218
|
+
const out: string[] = [];
|
|
219
|
+
const stripped: string[] = [];
|
|
220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
221
|
+
const line = lines[i] ?? '';
|
|
222
|
+
const hit = rootMachineKey(line);
|
|
223
|
+
if (hit === null) { out.push(line); continue; }
|
|
224
|
+
if (!stripped.includes(hit.key)) stripped.push(hit.key);
|
|
225
|
+
if (isBlockValue(hit.value)) {
|
|
226
|
+
while (i + 1 < lines.length && /^[ \t]/.test(lines[i + 1] ?? '')) i += 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { text: out.join('\n'), stripped };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface MachinePathRestoreResult {
|
|
233
|
+
/** 接回本机路径行后的文本(无需恢复时与入参逐字相同) */
|
|
234
|
+
text: string;
|
|
235
|
+
/** 接回的键(按首次出现顺序去重) */
|
|
236
|
+
restored: string[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 把**目标机自己原有的**机器路径行接回将要写入的文本(写回侧)。
|
|
241
|
+
*
|
|
242
|
+
* 来源机的路径已在收集侧剥离(这里对老载荷再剥一次是纵深防御),所以本函数接回的只可能是
|
|
243
|
+
* 目标机原本就有的设置 —— 既不搬运来源机的路径,也不让一次导入清掉本机自己的 store 位置。
|
|
244
|
+
* `local` 为 null(目标机本来就没有该文件)时原样返回。接回行统一用 incoming 的 EOL。
|
|
245
|
+
*/
|
|
246
|
+
export function restorePnpmMachinePaths(
|
|
247
|
+
incoming: string,
|
|
248
|
+
local: string | null | undefined,
|
|
249
|
+
): MachinePathRestoreResult {
|
|
250
|
+
if (incoming === '' || local === null || local === undefined || local === '') {
|
|
251
|
+
return { text: incoming, restored: [] };
|
|
252
|
+
}
|
|
253
|
+
const lines = local.split('\n');
|
|
254
|
+
const kept: string[] = [];
|
|
255
|
+
const restored: string[] = [];
|
|
256
|
+
for (let i = 0; i < lines.length; i++) {
|
|
257
|
+
const raw = lines[i] ?? '';
|
|
258
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
259
|
+
const hit = rootMachineKey(line);
|
|
260
|
+
if (hit === null) continue;
|
|
261
|
+
if (!restored.includes(hit.key)) restored.push(hit.key);
|
|
262
|
+
kept.push(line);
|
|
263
|
+
if (isBlockValue(hit.value)) {
|
|
264
|
+
while (i + 1 < lines.length && /^[ \t]/.test(lines[i + 1] ?? '')) {
|
|
265
|
+
i += 1;
|
|
266
|
+
const cont = lines[i] ?? '';
|
|
267
|
+
kept.push(cont.endsWith('\r') ? cont.slice(0, -1) : cont);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (kept.length === 0) return { text: incoming, restored: [] };
|
|
272
|
+
const eol = incoming.includes('\r\n') ? '\r\n' : '\n';
|
|
273
|
+
const head = incoming.endsWith('\n') ? incoming : incoming + eol;
|
|
274
|
+
return { text: head + kept.map((l) => l + eol).join(''), restored };
|
|
275
|
+
}
|
package/src/adapters/prompts.ts
CHANGED
|
@@ -18,12 +18,13 @@ import type { MsgFunc } from '../core/messages.ts';
|
|
|
18
18
|
import './messages.ts';
|
|
19
19
|
import type { PromptEntry } from '../schema/types.ts';
|
|
20
20
|
import type {
|
|
21
|
-
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
|
|
21
|
+
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext,
|
|
22
22
|
ImportContext, PlanItem, ValidationResult,
|
|
23
23
|
} from '../core/types.ts';
|
|
24
24
|
import { locatePatchLineLayer, readEffectivePatchLines, resolveWriteLayer } from '../core/patch-layers.ts';
|
|
25
25
|
import { sectionMeta } from '../schema/section-registry.ts';
|
|
26
26
|
import { validateJsonSection } from './json-section.ts';
|
|
27
|
+
import { entryByteSize, filterByUnits } from './units.ts';
|
|
27
28
|
|
|
28
29
|
/** 导出记录:PromptEntry 之外记录来源行名与所在层(导入重建 / 写回定位用) */
|
|
29
30
|
export interface PromptExportEntry extends PromptEntry {
|
|
@@ -128,14 +129,15 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
|
|
|
128
129
|
readonly defaultIncluded = sectionMeta('prompts').defaultIncluded;
|
|
129
130
|
readonly portability = sectionMeta('prompts').portability;
|
|
130
131
|
|
|
131
|
-
async export(ctx: HostContext,
|
|
132
|
+
async export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<PromptsExportSection>> {
|
|
132
133
|
const warnings: string[] = [];
|
|
133
134
|
// issue #71:两层都读(home 层优先),否则 profile 层里的 persona / planMode 全部丢失
|
|
134
135
|
const read = await readEffectivePatchLines(ctx.patchFile, ctx.profile);
|
|
135
136
|
for (const f of read.failures) {
|
|
136
137
|
warnings.push(msgOf(ctx)('adapter.patchReadFailedPrompts', { reason: `${f.file}: ${f.reason}` }));
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
+
// 条目级白名单:未勾选的 persona / plan-mode section 不进包
|
|
140
|
+
const prompts = filterByUnits(extractPrompts(read.lines), options.includeItems?.[this.id], (p) => this.unitId(p));
|
|
139
141
|
return {
|
|
140
142
|
sectionId: 'prompts',
|
|
141
143
|
data: { version: 1, prompts },
|
|
@@ -144,6 +146,21 @@ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
|
|
|
144
146
|
};
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
/** 单元 id:`prompts:<条目 id>`(条目 id 已含来源行与 persona/section 名,天然唯一) */
|
|
150
|
+
private unitId(prompt: PromptExportEntry): string {
|
|
151
|
+
return this.id + ':' + prompt.id;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 条目级单元清单(零 I/O);label = 条目名,detail = 类型(数据,不是 UI 文案) */
|
|
155
|
+
listUnits(section: ExportSection<PromptsExportSection>): ExportUnit[] {
|
|
156
|
+
return (section.data.prompts ?? []).map((p) => ({
|
|
157
|
+
id: this.unitId(p),
|
|
158
|
+
label: p.name,
|
|
159
|
+
detail: p.kind,
|
|
160
|
+
sizeBytes: entryByteSize(p),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
147
164
|
async analyzeImport(data: PromptsExportSection, ctx: ImportContext): Promise<PlanItem[]> {
|
|
148
165
|
const msg = ctx.msg;
|
|
149
166
|
const items: PlanItem[] = [];
|
|
@@ -10,12 +10,13 @@ import { msgOf, zhMsg } from '../core/messages.ts';
|
|
|
10
10
|
import type { MsgFunc } from '../core/messages.ts';
|
|
11
11
|
import type { ProviderEntry } from '../schema/types.ts';
|
|
12
12
|
import type {
|
|
13
|
-
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
|
|
13
|
+
ApplyResult, ConfigAdapter, ExportOptions, ExportSection, ExportUnit, HostContext,
|
|
14
14
|
ImportContext, NamespaceInfo, PlanItem, ValidationResult,
|
|
15
15
|
} from '../core/types.ts';
|
|
16
16
|
import { resolveNamespaces, isEmptyValue, type NamespaceProvider } from './settings.ts';
|
|
17
17
|
import { sectionMeta } from '../schema/section-registry.ts';
|
|
18
18
|
import { validateJsonSection } from './json-section.ts';
|
|
19
|
+
import { entryByteSize, filterByUnits } from './units.ts';
|
|
19
20
|
|
|
20
21
|
/** 导出记录:ProviderEntry 之外附加 namespace 级元数据(导入写回用),满足 ProvidersSection 形状 */
|
|
21
22
|
export interface ProviderExportEntry extends ProviderEntry {
|
|
@@ -53,7 +54,7 @@ export class ProvidersAdapter implements ConfigAdapter<ProviderExportSection> {
|
|
|
53
54
|
this.namespaces = namespaces;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
async export(ctx: HostContext,
|
|
57
|
+
async export(ctx: HostContext, options: ExportOptions): Promise<ExportSection<ProviderExportSection>> {
|
|
57
58
|
const providers: Record<string, ProviderExportEntry> = {};
|
|
58
59
|
const warnings: string[] = [];
|
|
59
60
|
for (const ns of await resolveNamespaces(this.namespaces, ctx)) {
|
|
@@ -83,14 +84,71 @@ export class ProvidersAdapter implements ConfigAdapter<ProviderExportSection> {
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
}
|
|
87
|
+
const picked = this.pickProviders(providers, options.includeItems?.[this.id]);
|
|
86
88
|
return {
|
|
87
89
|
sectionId: 'providers',
|
|
88
|
-
data: { version: 1, providers },
|
|
89
|
-
counts: { providers: Object.keys(
|
|
90
|
+
data: { version: 1, providers: picked },
|
|
91
|
+
counts: { providers: Object.keys(picked).length },
|
|
90
92
|
warnings,
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/** 单元 id:`providers:<route>`(route 是 providers 记录的键,天然唯一) */
|
|
97
|
+
private unitId(route: string): string {
|
|
98
|
+
return this.id + ':' + route;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 条目级单元清单(零 I/O):一台 provider(记录里的一个 route)= 一个单元。
|
|
103
|
+
*
|
|
104
|
+
* detail 给来源 namespace —— 用户靠它区分「同一个模型走哪个 ns」,不放任何 key 值。
|
|
105
|
+
*/
|
|
106
|
+
listUnits(section: ExportSection<ProviderExportSection>): ExportUnit[] {
|
|
107
|
+
return Object.entries(section.data.providers ?? {}).map(([route, entry]) => ({
|
|
108
|
+
id: this.unitId(route),
|
|
109
|
+
label: route,
|
|
110
|
+
detail: entry.namespace,
|
|
111
|
+
sizeBytes: entryByteSize(entry),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 白名单过滤 + raw 重建。
|
|
117
|
+
*
|
|
118
|
+
* 为什么必须重建 raw:每个 entry.raw 存的是**整个 namespace** 的快照(导入按 namespace 整体替换)。
|
|
119
|
+
* 只勾 llm-pi-ai 下的一条 route 时若原样带走 raw,未勾的 route 会随 raw 一起进包、
|
|
120
|
+
* 导入时又被整 namespace 写回目标机 —— 用户看到「没勾却没生效」,选择器等于撒谎。
|
|
121
|
+
* 因此这里把每个 namespace 的 raw.providers 收窄成「该 ns 里被勾选的 route」集合。
|
|
122
|
+
*/
|
|
123
|
+
private pickProviders(
|
|
124
|
+
providers: Record<string, ProviderExportEntry>,
|
|
125
|
+
allow: readonly string[] | undefined,
|
|
126
|
+
): Record<string, ProviderExportEntry> {
|
|
127
|
+
const picked = filterByUnits(Object.entries(providers), allow, ([route]) => this.unitId(route));
|
|
128
|
+
const out: Record<string, ProviderExportEntry> = {};
|
|
129
|
+
const keepByNs = new Map<string, Set<string>>();
|
|
130
|
+
for (const [route, entry] of picked) {
|
|
131
|
+
out[route] = entry;
|
|
132
|
+
const keep = keepByNs.get(entry.namespace) ?? new Set<string>();
|
|
133
|
+
keep.add(route);
|
|
134
|
+
keepByNs.set(entry.namespace, keep);
|
|
135
|
+
}
|
|
136
|
+
if (allow === undefined) return out; // 全量导出不动 raw(保持与旧备份逐字节一致)
|
|
137
|
+
for (const entry of Object.values(out)) {
|
|
138
|
+
const raw = entry.raw;
|
|
139
|
+
if (raw === null || typeof raw !== 'object') continue;
|
|
140
|
+
const pv = raw['providers'];
|
|
141
|
+
if (pv === null || typeof pv !== 'object' || Array.isArray(pv)) continue;
|
|
142
|
+
const keep = keepByNs.get(entry.namespace) ?? new Set<string>();
|
|
143
|
+
const narrowed: Record<string, unknown> = {};
|
|
144
|
+
for (const [route, value] of Object.entries(pv as Record<string, unknown>)) {
|
|
145
|
+
if (keep.has(route)) narrowed[route] = value;
|
|
146
|
+
}
|
|
147
|
+
entry.raw = { ...raw, providers: narrowed };
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
94
152
|
async analyzeImport(data: ProviderExportSection, ctx: ImportContext): Promise<PlanItem[]> {
|
|
95
153
|
const msg = ctx.msg;
|
|
96
154
|
const items: PlanItem[] = [];
|