dsh-config-manager 0.1.77 → 0.1.79
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.d.ts +16 -0
- package/lib/adapters/plugins.js +234 -22
- 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 +400 -112
- package/lib/client.js +2849 -1528
- package/lib/core/analyzer.js +3 -1
- package/lib/core/backup.d.ts +9 -0
- package/lib/core/backup.js +36 -1
- 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 +30 -0
- package/lib/core/messages.js +64 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +48 -2
- package/lib/core/plugin-cli.js +84 -5
- 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 +22 -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 +61 -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 +15 -1
- package/lib/sync/gist/gist-transport.js +66 -5
- package/lib/sync/gist/messages.d.ts +3 -0
- package/lib/sync/gist/messages.js +7 -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/sync-engine.d.ts +193 -4
- package/lib/sync/sync-engine.js +450 -37
- 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 +8 -0
- package/lib/ui/i18n.js +20 -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 +338 -2
- package/src/adapters/plugins.ts +243 -23
- 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/routes.ts +9 -0
- 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/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 +219 -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 +234 -0
- package/src/client/sync/merge-view.ts +371 -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.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/analyzer.ts +3 -1
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +36 -1
- 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 +66 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.fs.test.ts +65 -3
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +85 -6
- 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 +21 -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 +59 -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 +223 -0
- package/src/sync/gist/gist-transport.ts +73 -7
- package/src/sync/gist/messages.ts +7 -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.ts +87 -9
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +379 -0
- package/src/sync/sync-engine.ts +535 -36
- 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 +20 -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,16 +13,16 @@
|
|
|
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';
|
|
20
20
|
import { stringifyJsonSafe } from '../utils/json.js';
|
|
21
21
|
import { isPathSafe, normalizePath } from '../utils/paths.js';
|
|
22
22
|
import { isENOENT } from '../utils/guards.js';
|
|
23
|
-
import { PLUGIN_PATCH_REF_PREFIX } from '../core/backup.js';
|
|
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;
|
|
@@ -351,6 +367,9 @@ export class PluginsAdapter {
|
|
|
351
367
|
isBundle: p.isBundle ?? false,
|
|
352
368
|
inBundles: p.inBundles ?? [],
|
|
353
369
|
enabled: p.enabled,
|
|
370
|
+
// issue #95:只在 dsh.profile.bundles 里声明的层(源机无依赖声明)必须带上这个标记,
|
|
371
|
+
// 导入端据此走「只声明」而不是 npm 安装。缺省不写,普通依赖的字节与改造前逐字一致。
|
|
372
|
+
...(p.bundlesOnly === true ? { bundlesOnly: true } : {}),
|
|
354
373
|
});
|
|
355
374
|
}
|
|
356
375
|
}
|
|
@@ -372,7 +391,16 @@ export class PluginsAdapter {
|
|
|
372
391
|
try {
|
|
373
392
|
const rel = PNPM_WORKSPACE_REL(ctx.profile);
|
|
374
393
|
if (await ctx.fs.exists(rel)) {
|
|
375
|
-
|
|
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
|
+
}
|
|
376
404
|
}
|
|
377
405
|
}
|
|
378
406
|
catch (err) {
|
|
@@ -543,6 +571,83 @@ export class PluginsAdapter {
|
|
|
543
571
|
}
|
|
544
572
|
return set;
|
|
545
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* 读目标机 profile 的 `dsh.profile.bundles`(issue #95)。
|
|
576
|
+
*
|
|
577
|
+
* 相对路径与写入目标同源(`profiles/<profile>/package.json`),走 `ctx.target.fs` 而不是
|
|
578
|
+
* 宿主私有通道:与 pnpm-workspace.yaml / patch 文件同一条 I/O 纪律(受限、可被 MemFs 测试)。
|
|
579
|
+
*/
|
|
580
|
+
async targetBundles(ctx) {
|
|
581
|
+
const rel = `${PNPM_PROFILE_DIR(ctx.target.profile)}/package.json`;
|
|
582
|
+
try {
|
|
583
|
+
if (!(await ctx.target.fs.exists(rel)))
|
|
584
|
+
return { state: 'absent' };
|
|
585
|
+
const text = new TextDecoder().decode(await ctx.target.fs.readFile(rel));
|
|
586
|
+
let parsed;
|
|
587
|
+
try {
|
|
588
|
+
parsed = JSON.parse(text);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
return { state: 'unreadable', reason: ctx.msg('adapter.pluginHostBundleBadJson') };
|
|
592
|
+
}
|
|
593
|
+
const bundles = parsed?.dsh?.profile?.bundles;
|
|
594
|
+
if (!Array.isArray(bundles) || bundles.some((entry) => typeof entry !== 'string')) {
|
|
595
|
+
return { state: 'unreadable', reason: ctx.msg('adapter.pluginHostBundleBadShape') };
|
|
596
|
+
}
|
|
597
|
+
return { state: 'ok', names: new Set(bundles), text };
|
|
598
|
+
}
|
|
599
|
+
catch (err) {
|
|
600
|
+
return { state: 'unreadable', reason: err instanceof Error ? err.message : String(err) };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* 把 host 自带的层声明进目标机 profile 的 `dsh.profile.bundles`(issue #95)。
|
|
605
|
+
*
|
|
606
|
+
* 为什么**不走安装通道**:这类层的包由 DSH 从自己的安装位置解析;按裸包名 `dsh plugin add`
|
|
607
|
+
* 会把 npm `dist-tags.latest` 的副本装进 profile,与 host 的版本线脱节 —— 真机故障:
|
|
608
|
+
* host 0.2.1-rc.2 + 包 0.2.1-alpha.2 的隔离组契约对不上 → `session-controller` 一直 pending
|
|
609
|
+
* → 新建会话 service-unavailable。声明不了时只报非致命 warning(§34.17),不拖垮其余配置。
|
|
610
|
+
*/
|
|
611
|
+
async declareHostBundle(name, ctx) {
|
|
612
|
+
const msg = ctx.msg;
|
|
613
|
+
const rel = `${PNPM_PROFILE_DIR(ctx.target.profile)}/package.json`;
|
|
614
|
+
const view = await this.targetBundles(ctx);
|
|
615
|
+
if (view.state === 'absent') {
|
|
616
|
+
return { ok: false, warning: true, message: msg('adapter.pluginHostBundleNoManifest', { name }) };
|
|
617
|
+
}
|
|
618
|
+
if (view.state === 'unreadable') {
|
|
619
|
+
return {
|
|
620
|
+
ok: false,
|
|
621
|
+
warning: true,
|
|
622
|
+
message: msg('adapter.pluginHostBundleManifestUnreadable', { name, reason: view.reason }),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
if (view.names.has(name)) {
|
|
626
|
+
return { ok: true, needsRestart: false, message: msg('adapter.pluginHostBundleSame', { name }) };
|
|
627
|
+
}
|
|
628
|
+
const merged = mergeBundleDeclaration(view.text, name);
|
|
629
|
+
if (merged === null) {
|
|
630
|
+
return {
|
|
631
|
+
ok: false,
|
|
632
|
+
warning: true,
|
|
633
|
+
message: msg('adapter.pluginHostBundleManifestUnreadable', {
|
|
634
|
+
name,
|
|
635
|
+
reason: msg('adapter.pluginHostBundleBadShape'),
|
|
636
|
+
}),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
await ctx.target.fs.writeFile(rel, new TextEncoder().encode(merged.text));
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
return {
|
|
644
|
+
ok: false,
|
|
645
|
+
warning: true,
|
|
646
|
+
message: msg('adapter.pluginHostBundleWriteFailed', { name, reason: err instanceof Error ? err.message : String(err) }),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return { ok: true, needsRestart: true, message: msg('adapter.pluginHostBundleDeclared', { name }) };
|
|
650
|
+
}
|
|
546
651
|
async analyzeImport(data, ctx) {
|
|
547
652
|
const msg = ctx.msg;
|
|
548
653
|
const items = [];
|
|
@@ -587,7 +692,10 @@ export class PluginsAdapter {
|
|
|
587
692
|
// issue #35:目标机满足不了的 patchedDependencies 声明必须剔除——否则写进去之后,
|
|
588
693
|
// 目标机 pnpm 会因为「patch 文件读不到」拒绝**一切** add(含本次要装的插件)。
|
|
589
694
|
const available = await this.patchAvailability(data, ctx);
|
|
590
|
-
|
|
695
|
+
// 可移植性净化(写回侧·纵深防御):老版本备份 / 第三方工具产出的载荷里可能仍带着
|
|
696
|
+
// 来源机的机器路径键(本版本收集侧已剥离,正常路径这里就是 no-op)。
|
|
697
|
+
const stripped = stripPnpmMachinePaths(data.pnpmWorkspace);
|
|
698
|
+
const clean = sanitizePnpmWorkspacePatches(stripped.text, (rel) => available.has(rel));
|
|
591
699
|
const droppedNames = clean.dropped.map((d) => d.name).join(', ');
|
|
592
700
|
let current = null;
|
|
593
701
|
try {
|
|
@@ -599,15 +707,27 @@ export class PluginsAdapter {
|
|
|
599
707
|
catch {
|
|
600
708
|
current = null;
|
|
601
709
|
}
|
|
602
|
-
|
|
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
|
+
}
|
|
603
724
|
items.push({
|
|
604
725
|
id: 'plugins:pnpm-workspace',
|
|
605
726
|
kind: current === null ? 'Create' : 'Update',
|
|
606
727
|
adapter: 'plugins',
|
|
607
728
|
description: current === null ? msg('adapter.pwCreate') : msg('adapter.pwUpdate'),
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
: {}),
|
|
729
|
+
// detail 非空 ⇒ isToolchainChangeItem ⇒ 进确认列表(可见、可取消)——净化绝不静默
|
|
730
|
+
...(details.length > 0 ? { detail: details.join(msg('adapter.listSeparator')) } : {}),
|
|
611
731
|
severity: 'info',
|
|
612
732
|
target: { adapter: 'plugins', ref: 'pnpm-workspace.yaml' },
|
|
613
733
|
});
|
|
@@ -634,8 +754,44 @@ export class PluginsAdapter {
|
|
|
634
754
|
}
|
|
635
755
|
// 插件:包名唯一键;同版本 Skip / 未装 Install / 版本不同 Conflict
|
|
636
756
|
const targetInstalled = await ctx.target.plugins.listInstalled();
|
|
757
|
+
// issue #95:只在 dsh.profile.bundles 里声明的层(源机没有依赖声明)**不装 npm 包** ——
|
|
758
|
+
// 它的名字只声明进目标机的 bundles,由 DSH 从自己的安装位置解析。只在真出现这类条目时才读
|
|
759
|
+
// 目标清单(其余场景零额外 I/O,行为与改造前逐字一致)。
|
|
760
|
+
const bundleView = data.plugins.some((p) => isBundlesOnlyEntry(p)) ? await this.targetBundles(ctx) : null;
|
|
637
761
|
for (const p of data.plugins) {
|
|
638
762
|
const id = `plugin:${p.name}`;
|
|
763
|
+
if (isBundlesOnlyEntry(p)) {
|
|
764
|
+
if (bundleView !== null && bundleView.state === 'ok' && bundleView.names.has(p.name)) {
|
|
765
|
+
items.push({
|
|
766
|
+
id, kind: 'Skip', adapter: 'plugins',
|
|
767
|
+
description: msg('adapter.pluginHostBundleSame', { name: p.name }),
|
|
768
|
+
detail: msg('adapter.pluginHostBundleDetail'),
|
|
769
|
+
severity: 'info',
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
else if (bundleView !== null && bundleView.state === 'ok') {
|
|
773
|
+
items.push({
|
|
774
|
+
id, kind: 'Create', adapter: 'plugins',
|
|
775
|
+
description: msg('adapter.pluginHostBundleDeclare', { name: p.name }),
|
|
776
|
+
detail: msg('adapter.pluginHostBundleDetail'),
|
|
777
|
+
severity: 'info',
|
|
778
|
+
target: { adapter: 'plugins', ref: PLUGIN_PROFILE_MANIFEST_REF },
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
// 清单缺 / 读不到 / 形态不符:**不猜也不重建**,如实告警(非致命,其余分区照常导入)
|
|
783
|
+
items.push({
|
|
784
|
+
id,
|
|
785
|
+
kind: 'Warning',
|
|
786
|
+
adapter: 'plugins',
|
|
787
|
+
description: bundleView === null || bundleView.state === 'absent'
|
|
788
|
+
? msg('adapter.pluginHostBundleNoManifest', { name: p.name })
|
|
789
|
+
: msg('adapter.pluginHostBundleManifestUnreadable', { name: p.name, reason: bundleView.reason }),
|
|
790
|
+
severity: 'warning',
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
639
795
|
const tp = targetInstalled.find((t) => t.name === p.name);
|
|
640
796
|
// T1:该插件在备份内自带 tarball(本地源 link:/file:)→ 目标机无需原始路径即可安装。
|
|
641
797
|
const hasTarball = tarballOf(data, p.name) !== undefined;
|
|
@@ -748,9 +904,32 @@ export class PluginsAdapter {
|
|
|
748
904
|
throw err;
|
|
749
905
|
}
|
|
750
906
|
}
|
|
751
|
-
|
|
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;
|
|
913
|
+
try {
|
|
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);
|
|
752
931
|
try {
|
|
753
|
-
await ctx.target.fs.writeFile(
|
|
932
|
+
await ctx.target.fs.writeFile(wsRel, new TextEncoder().encode(merged.text));
|
|
754
933
|
const notes = [msg('adapter.pwWritten')];
|
|
755
934
|
if (clean.dropped.length > 0) {
|
|
756
935
|
notes.push(msg('adapter.pwWrittenPatchesDropped', {
|
|
@@ -758,6 +937,7 @@ export class PluginsAdapter {
|
|
|
758
937
|
names: clean.dropped.map((d) => d.name).join(', '),
|
|
759
938
|
}));
|
|
760
939
|
}
|
|
940
|
+
notes.push(...machinePathNotes(msg, stripped.stripped, merged.restored));
|
|
761
941
|
return { ok: true, needsRestart: true, message: notes.join(msg('adapter.listSeparator')) };
|
|
762
942
|
}
|
|
763
943
|
catch (err) {
|
|
@@ -777,29 +957,56 @@ export class PluginsAdapter {
|
|
|
777
957
|
// 拖垮已成功导入的其余配置——与 workspaces 的「目标不可达 → warning」同款语义。
|
|
778
958
|
if (item.id.startsWith('plugin:')) {
|
|
779
959
|
const name = item.id.replace(/^plugin:/, '');
|
|
960
|
+
const data = ctx.sections.get('plugins');
|
|
961
|
+
// issue #95:host 自带的层(源机只在 dsh.profile.bundles 声明、没有依赖声明)走「只声明」,
|
|
962
|
+
// 绝不进 npm 安装通道(按裸包名装 = 把 dist-tag latest 的副本塞进 profile)。
|
|
963
|
+
const entry = data?.plugins.find((p) => p.name === name);
|
|
964
|
+
if (entry !== undefined && isBundlesOnlyEntry(entry)) {
|
|
965
|
+
return await this.declareHostBundle(name, ctx);
|
|
966
|
+
}
|
|
780
967
|
try {
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
// T1:备份内自带 tarball → 解包到 homeDir 内的本地插件缓存目录,把 spec 重写为绝对路径。
|
|
968
|
+
// 安装目标由 pluginInstallTarget 统一决策(issue #101):备份里有具体版本时精确安装,
|
|
969
|
+
// 其余 registry 包带「包名@spec」;非 registry 来源原样。
|
|
970
|
+
// T1:备份内自带 tarball → 解包到 homeDir 内的本地插件缓存目录,改以 file:<abs> 安装。
|
|
785
971
|
// 这是「换机后本地插件不再丢失」的关键:原始绝对路径在目标机不存在,解包出的 tgz 一定存在。
|
|
972
|
+
let tarballPath;
|
|
786
973
|
const tarball = data !== undefined ? tarballOf(data, name) : undefined;
|
|
787
974
|
if (tarball !== undefined) {
|
|
788
975
|
try {
|
|
789
|
-
|
|
790
|
-
spec = `file:${abs}`;
|
|
976
|
+
tarballPath = await writeLocalTarball(ctx, tarball);
|
|
791
977
|
}
|
|
792
978
|
catch (err) {
|
|
793
979
|
// 解包失败不阻断安装尝试:退回原 spec(可能失败,但会如实报错而非静默丢失)
|
|
794
980
|
ctx.onLog?.(msg('adapter.localTarballUnpackFailed', { name, err: err instanceof Error ? err.message : String(err) }));
|
|
795
981
|
}
|
|
796
982
|
}
|
|
983
|
+
const target = pluginInstallTarget(name, { version: entry?.version, spec: entry?.spec, tarballPath });
|
|
797
984
|
// 执行日志:记录实际将发起的子进程命令行(与宿主 DshPluginsFacade 的
|
|
798
|
-
// dsh plugin --profile <p> add <
|
|
799
|
-
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}`);
|
|
800
987
|
// 透传中止信号:用户「跳过当前插件」→ 宿主 kill 子进程 + 清半装状态 → 抛 ImportUserSkippedError
|
|
801
|
-
const result = await ctx.target.plugins.install(name,
|
|
988
|
+
const result = await ctx.target.plugins.install(name, target, ctx.signal);
|
|
802
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
|
+
}
|
|
803
1010
|
return {
|
|
804
1011
|
ok: true,
|
|
805
1012
|
needsRestart: true,
|
|
@@ -860,6 +1067,11 @@ export class PluginsAdapter {
|
|
|
860
1067
|
if (typeof name !== 'string' || name === '') {
|
|
861
1068
|
issues.push({ path: `plugins[${i}].name`, message: msg('adapter.validate.requiredField', { subject: 'plugins[].name' }), severity: 'error' });
|
|
862
1069
|
}
|
|
1070
|
+
// issue #95:`bundlesOnly` 决定走不走 npm 安装通道,写错类型必须拦下(缺省合法)
|
|
1071
|
+
const bundlesOnly = p.bundlesOnly;
|
|
1072
|
+
if (bundlesOnly !== undefined && typeof bundlesOnly !== 'boolean') {
|
|
1073
|
+
issues.push({ path: `plugins[${i}].bundlesOnly`, message: msg('adapter.validate.boolean', { subject: 'plugins[].bundlesOnly' }), severity: 'error' });
|
|
1074
|
+
}
|
|
863
1075
|
}
|
|
864
1076
|
}
|
|
865
1077
|
if (section.patch !== undefined && !Array.isArray(section.patch)) {
|
|
@@ -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>;
|