create-principles-disciple 1.145.6 → 1.145.8
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/_release/product-identity.json +2 -2
- package/codex-adapter/dist/ingestion/explicit-owner-statement.d.ts +1 -17
- package/codex-adapter/dist/ingestion/explicit-owner-statement.js +1 -21
- package/console/dist/server/models/CodexGovernanceHealthModel.d.ts +39 -2
- package/console/dist/server/models/CodexGovernanceHealthModel.js +228 -29
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js +28 -9
- package/core/dist/runtime-v2/adapter/artificer-l2-adapter.js.map +1 -1
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js +29 -9
- package/core/dist/runtime-v2/adapter/l2-agent-loop-adapter.js.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts +6 -0
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +105 -1
- package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
- package/core/dist/runtime-v2/context-payload.d.ts +4 -4
- package/core/dist/runtime-v2/context-payload.d.ts.map +1 -1
- package/core/dist/runtime-v2/context-payload.js +1 -0
- package/core/dist/runtime-v2/context-payload.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +7 -2
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +4 -1
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts +154 -0
- package/core/dist/runtime-v2/internalization/artificer-intent-preflight.d.ts.map +1 -0
- package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js +686 -0
- package/core/dist/runtime-v2/internalization/artificer-intent-preflight.js.map +1 -0
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts +21 -3
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js +48 -5
- package/core/dist/runtime-v2/internalization/artificer-prompt-builder.js.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-runner.d.ts +17 -0
- package/core/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/artificer-runner.js +45 -2
- package/core/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
- package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts +0 -19
- package/core/dist/runtime-v2/internalization/behavior-example-pack.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/behavior-example-pack.js +31 -0
- package/core/dist/runtime-v2/internalization/behavior-example-pack.js.map +1 -1
- package/core/dist/runtime-v2/internalization/intent-contract.d.ts +31 -0
- package/core/dist/runtime-v2/internalization/intent-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/intent-contract.js +42 -0
- package/core/dist/runtime-v2/internalization/intent-contract.js.map +1 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +14 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
- package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts +64 -0
- package/core/dist/runtime-v2/internalization/pitask-metadata.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/pitask-metadata.js +84 -0
- package/core/dist/runtime-v2/internalization/pitask-metadata.js.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-output.d.ts +13 -1
- package/core/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-output.js +13 -1
- package/core/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts +15 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.d.ts.map +1 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js +29 -1
- package/core/dist/runtime-v2/internalization/scribe-prompt-builder.js.map +1 -1
- package/core/dist/runtime-v2/task-status.d.ts +1 -1
- package/core/dist/runtime-v2/tools/agent-tool-contract.d.ts +2 -1
- package/core/dist/runtime-v2/tools/agent-tool-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/tools/agent-tool-contract.js +1 -1
- package/core/dist/runtime-v2/tools/agent-tool-contract.js.map +1 -1
- package/core/dist/runtime-v2/tools/artificer-l2-tool-contract.d.ts +1 -1
- package/core/dist/telemetry-event.d.ts +4 -2
- package/core/dist/telemetry-event.d.ts.map +1 -1
- package/core/dist/telemetry-event.js +12 -0
- package/core/dist/telemetry-event.js.map +1 -1
- package/core/package.json +3 -3
- package/dist/installers/workbuddy-host-installer.d.ts +10 -3
- package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/dist/installers/workbuddy-host-installer.js +400 -39
- package/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/host-runtime/dist/explicit-owner-statement-prefix.d.ts +17 -0
- package/host-runtime/dist/explicit-owner-statement-prefix.js +21 -0
- package/host-runtime/dist/governance-signal-admission.d.ts +2 -2
- package/host-runtime/dist/governance-signal-admission.js +15 -8
- package/host-runtime/dist/index.d.ts +1 -0
- package/host-runtime/dist/index.js +1 -0
- package/host-runtime/dist/internalization-consumer-cycle.d.ts +10 -1
- package/host-runtime/dist/internalization-consumer-cycle.js +85 -4
- package/host-runtime/dist/production-rulehost-gate.js +40 -3
- package/host-runtime/package.json +1 -1
- package/package.json +2 -2
- package/plugin/dist/bundle.js +655 -781
- package/plugin/dist/openclaw.plugin.json +1 -1
- package/plugin/dist/rulehost-evidence.js +27 -27
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +1 -1
- package/release-manager/dist/installers/workbuddy-host-installer.d.ts +10 -3
- package/release-manager/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/release-manager/dist/installers/workbuddy-host-installer.js +400 -39
- package/release-manager/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/release-manager/package.json +2 -2
- package/workbuddy-adapter/dist/explicit-owner-statement.d.ts +3 -0
- package/workbuddy-adapter/dist/explicit-owner-statement.js +52 -0
- package/workbuddy-adapter/dist/pd-hook.js +31 -7
- package/workbuddy-adapter/plugin/scripts/pd-control.cjs +45 -0
- package/workbuddy-adapter/plugin/skills/pd-disable/SKILL.md +18 -0
- package/workbuddy-adapter/plugin/skills/pd-record-pain/SKILL.md +21 -0
- package/workbuddy-adapter/plugin/skills/pd-review/SKILL.md +16 -0
- package/workbuddy-adapter/plugin/skills/pd-status/SKILL.md +17 -0
|
@@ -42,11 +42,12 @@
|
|
|
42
42
|
* result reason; every result carries reason + nextAction.
|
|
43
43
|
*/
|
|
44
44
|
import { spawnSync } from 'node:child_process';
|
|
45
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, cpSync, writeFileSync } from 'node:fs';
|
|
45
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, cpSync, writeFileSync } from 'node:fs';
|
|
46
46
|
import * as path from 'node:path';
|
|
47
47
|
import * as yaml from 'js-yaml';
|
|
48
|
+
import semver from 'semver';
|
|
48
49
|
import { getInstallLayoutPaths, getWorkbuddyWorkspaceDir, installManifestHostsForPersist, mergeInstallManifestWorkspaces, parseInstallManifest, INSTALL_LAYOUT_VERSION, } from '@principles/install-layout';
|
|
49
|
-
import { atomicReplaceTextFile } from '../utils/config-file-io.js';
|
|
50
|
+
import { atomicReplaceTextFile, errnoCode } from '../utils/config-file-io.js';
|
|
50
51
|
import { getHomeDir, generateConfigYamlContent, getConfigYamlPath, resolveDefaultModelForProvider, isValidHttpBaseUrl } from '../mvp-config.js';
|
|
51
52
|
// Type-only import from @principles/core — erased at compile time; this
|
|
52
53
|
// package ships self-contained without @principles/core (PRI-625/PRI-669).
|
|
@@ -57,6 +58,19 @@ export const WORKBUDDY_PLUGIN_NAME = 'principles-disciple';
|
|
|
57
58
|
export const WORKBUDDY_MARKETPLACE_NAME = 'principles-disciple-market';
|
|
58
59
|
/** Registry/settings key: "principles-disciple@principles-disciple-market". */
|
|
59
60
|
export const WORKBUDDY_PLUGIN_KEY = `${WORKBUDDY_PLUGIN_NAME}@${WORKBUDDY_MARKETPLACE_NAME}`;
|
|
61
|
+
const WORKBUDDY_REQUIRED_SKILLS = ['pd-record-pain', 'pd-status', 'pd-review', 'pd-disable'];
|
|
62
|
+
/**
|
|
63
|
+
* Files the host needs to load this plugin. One list serves both the
|
|
64
|
+
* "is the cached payload current?" comparison and the "did the staged copy
|
|
65
|
+
* finish?" proof — two copies of this set would drift apart silently.
|
|
66
|
+
*/
|
|
67
|
+
const WORKBUDDY_PAYLOAD_FILES = [
|
|
68
|
+
path.join('.codebuddy-plugin', 'plugin.json'),
|
|
69
|
+
path.join('hooks', 'hooks.json'),
|
|
70
|
+
path.join('scripts', 'wrapper.cjs'),
|
|
71
|
+
path.join('scripts', 'pd-control.cjs'),
|
|
72
|
+
...WORKBUDDY_REQUIRED_SKILLS.map((skill) => path.join('skills', skill, 'SKILL.md')),
|
|
73
|
+
];
|
|
60
74
|
/** Engine CLI shipped with WorkBuddy Desktop (probe P01 frozen path). */
|
|
61
75
|
export const WORKBUDDY_ENGINE_RELATIVE = ['resources', 'app.asar.unpacked', 'cli', 'dist', 'codebuddy-lite-wb.mjs'];
|
|
62
76
|
/** Engine command success markers (frozen by probe P01 regexes). */
|
|
@@ -70,6 +84,47 @@ const GOVERNANCE_INIT_TIMEOUT_MS = 180_000;
|
|
|
70
84
|
function isRecord(value) {
|
|
71
85
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
72
86
|
}
|
|
87
|
+
/** Native v2 stores scope records as an array; old PD installs used one object. */
|
|
88
|
+
function userInstalledPluginRecord(value) {
|
|
89
|
+
const records = Array.isArray(value) ? value : [value];
|
|
90
|
+
return records.find((record) => isRecord(record) && record.scope === 'user' && record.projectPath === undefined);
|
|
91
|
+
}
|
|
92
|
+
function isSafePluginVersion(version) {
|
|
93
|
+
return typeof version === 'string'
|
|
94
|
+
&& /^[0-9][0-9A-Za-z.+-]*$/.test(version)
|
|
95
|
+
&& semver.valid(version) !== null;
|
|
96
|
+
}
|
|
97
|
+
/** Deletion/writes may target only a strict child of our fixed cache root. */
|
|
98
|
+
function workbuddyCacheVersionDir(cacheRoot, version) {
|
|
99
|
+
if (!isSafePluginVersion(version))
|
|
100
|
+
return undefined;
|
|
101
|
+
const root = path.resolve(cacheRoot);
|
|
102
|
+
const target = path.resolve(root, version);
|
|
103
|
+
const relative = path.relative(root, target);
|
|
104
|
+
if (relative.length === 0 || path.isAbsolute(relative) || relative === '..' || relative.startsWith(`..${path.sep}`))
|
|
105
|
+
return undefined;
|
|
106
|
+
return target;
|
|
107
|
+
}
|
|
108
|
+
/** Every scope record PD may have written for its key (native array or legacy object). */
|
|
109
|
+
function installedPluginRecords(value) {
|
|
110
|
+
return (Array.isArray(value) ? value : [value]).filter(isRecord);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Proof that a *staged* cache payload is complete before it replaces the live
|
|
114
|
+
* one. Returns the first missing/empty required file, or undefined when the
|
|
115
|
+
* payload is loadable. Never trusts `cpSync` returning without throwing.
|
|
116
|
+
*/
|
|
117
|
+
function missingWorkbuddyPayloadFile(root) {
|
|
118
|
+
for (const file of WORKBUDDY_PAYLOAD_FILES) {
|
|
119
|
+
try {
|
|
120
|
+
if (readFileSync(path.join(root, file)).length > 0)
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
catch { /* unreachable payload — reported below, never guessed */ }
|
|
124
|
+
return file;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
73
128
|
export function resolveWorkbuddyPaths(homeDir) {
|
|
74
129
|
const layout = getInstallLayoutPaths(homeDir);
|
|
75
130
|
// The frozen governance dir (~/.workbuddy/pd-workspace) defines the host
|
|
@@ -175,8 +230,14 @@ export function readHostJsonFile(filePath) {
|
|
|
175
230
|
try {
|
|
176
231
|
raw = readFileSync(filePath, 'utf-8');
|
|
177
232
|
}
|
|
178
|
-
catch {
|
|
179
|
-
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (errnoCode(error) === 'ENOENT')
|
|
235
|
+
return { ok: true, value: undefined, existed: false };
|
|
236
|
+
return {
|
|
237
|
+
ok: false, path: filePath,
|
|
238
|
+
reason: `workbuddy_settings_unreadable: cannot read ${filePath} (${error instanceof Error ? error.message : String(error)}). No PD write was performed.`,
|
|
239
|
+
nextAction: 'Restore host file read access before installing or uninstalling; do not delete caches while registry references are unknown.',
|
|
240
|
+
};
|
|
180
241
|
}
|
|
181
242
|
let parsed;
|
|
182
243
|
try {
|
|
@@ -273,6 +334,9 @@ export function findWorkbuddyPluginPayload(pluginDir, runtimeAdapterDir) {
|
|
|
273
334
|
if (!isRecord(manifest) || manifest.name !== WORKBUDDY_PLUGIN_NAME) {
|
|
274
335
|
return { reason: `workbuddy_plugin_manifest_invalid: ${manifestPath} must declare name "${WORKBUDDY_PLUGIN_NAME}" (got ${isRecord(manifest) ? JSON.stringify(manifest.name) : 'unparsable manifest'}).` };
|
|
275
336
|
}
|
|
337
|
+
if (!isSafePluginVersion(manifest.version)) {
|
|
338
|
+
return { reason: `workbuddy_plugin_version_invalid: ${manifestPath} must declare a safe single-segment semantic version.` };
|
|
339
|
+
}
|
|
276
340
|
let hooks;
|
|
277
341
|
try {
|
|
278
342
|
hooks = JSON.parse(readFileSync(hooksPath, 'utf-8'));
|
|
@@ -283,7 +347,21 @@ export function findWorkbuddyPluginPayload(pluginDir, runtimeAdapterDir) {
|
|
|
283
347
|
if (!isRecord(hooks) || !isRecord(hooks.hooks)) {
|
|
284
348
|
return { reason: `workbuddy_plugin_hooks_invalid: ${hooksPath} must contain a "hooks" object (frozen manifest-facts shape).` };
|
|
285
349
|
}
|
|
286
|
-
|
|
350
|
+
// Plugin-root discovery is the WorkBuddy skill delivery boundary. Do not
|
|
351
|
+
// register an old hook-only payload as a complete install.
|
|
352
|
+
for (const skill of WORKBUDDY_REQUIRED_SKILLS) {
|
|
353
|
+
const skillPath = path.join(root, 'skills', skill, 'SKILL.md');
|
|
354
|
+
try {
|
|
355
|
+
if (readFileSync(skillPath, 'utf-8').trim().length > 0)
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
catch { /* Missing/unreadable required skills fail before registration. */ }
|
|
359
|
+
return { reason: `workbuddy_plugin_skill_missing: required nonempty skill ${skillPath}. Reinstall with a complete WorkBuddy payload.` };
|
|
360
|
+
}
|
|
361
|
+
if (!existsSync(path.join(root, 'scripts', 'pd-control.cjs'))) {
|
|
362
|
+
return { reason: `workbuddy_plugin_control_missing: ${root} must ship scripts/pd-control.cjs for the pd-disable skill.` };
|
|
363
|
+
}
|
|
364
|
+
const { version } = manifest;
|
|
287
365
|
return { payload: { pluginDir: root, version } };
|
|
288
366
|
}
|
|
289
367
|
return {
|
|
@@ -1139,6 +1217,12 @@ export class WorkBuddyHostInstaller {
|
|
|
1139
1217
|
}
|
|
1140
1218
|
registerPlugin(details, version) {
|
|
1141
1219
|
const p = this.paths();
|
|
1220
|
+
// A registered old plugin can still be hook-only, even at the same
|
|
1221
|
+
// manifest version. Reuse the installer-owned registration/cache writer
|
|
1222
|
+
// rather than reporting a stale cache as an idempotent successful install.
|
|
1223
|
+
if (details.registered && !this.pluginCacheMatchesPayload(version)) {
|
|
1224
|
+
return this.directWriteRegistration(details, version, 'registered plugin cache differs from the current bundled payload; refreshed by installer');
|
|
1225
|
+
}
|
|
1142
1226
|
// Idempotent fast path: already registered AND enabled AND cached.
|
|
1143
1227
|
if (details.registered && details.pluginEnabled && existsSync(p.cachePluginRoot)) {
|
|
1144
1228
|
return { ok: true, mode: 'already-registered', settingsAction: 'preserved' };
|
|
@@ -1202,6 +1286,28 @@ export class WorkBuddyHostInstaller {
|
|
|
1202
1286
|
return direct;
|
|
1203
1287
|
return { ok: false, reason: direct.reason, nextAction: direct.nextAction };
|
|
1204
1288
|
}
|
|
1289
|
+
pluginCacheMatchesPayload(version) {
|
|
1290
|
+
const p = this.paths();
|
|
1291
|
+
const registry = readHostJsonFile(p.registryPath);
|
|
1292
|
+
const rawEntry = registry.ok && isRecord(registry.value?.plugins)
|
|
1293
|
+
? registry.value.plugins[WORKBUDDY_PLUGIN_KEY] : undefined;
|
|
1294
|
+
const entry = userInstalledPluginRecord(rawEntry);
|
|
1295
|
+
const cacheDir = workbuddyCacheVersionDir(p.cachePluginRoot, version);
|
|
1296
|
+
if (cacheDir === undefined)
|
|
1297
|
+
return false;
|
|
1298
|
+
// The host may write the same install path in a different (but equivalent)
|
|
1299
|
+
// textual form; comparing raw strings would re-register on every run.
|
|
1300
|
+
const entryInstallPath = typeof entry?.installPath === 'string' ? path.resolve(entry.installPath) : undefined;
|
|
1301
|
+
if (!isRecord(entry) || entry.version !== version || entryInstallPath !== path.resolve(cacheDir))
|
|
1302
|
+
return false;
|
|
1303
|
+
const sourceDir = path.join(p.marketplaceRoot, WORKBUDDY_PLUGIN_NAME);
|
|
1304
|
+
try {
|
|
1305
|
+
return WORKBUDDY_PAYLOAD_FILES.every((file) => readFileSync(path.join(sourceDir, file)).equals(readFileSync(path.join(cacheDir, file))));
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1205
1311
|
/**
|
|
1206
1312
|
* Set enabledPlugins[OUR_KEY]=true in settings.json, preserving every other
|
|
1207
1313
|
* key (same protected write discipline as the direct-write registration).
|
|
@@ -1225,55 +1331,187 @@ export class WorkBuddyHostInstaller {
|
|
|
1225
1331
|
/**
|
|
1226
1332
|
* Direct-write registration for engine-unreachable machines, aligned with the
|
|
1227
1333
|
* frozen P01 formats: enabledPlugins[key]=true in settings.json (other keys
|
|
1228
|
-
* and other plugins preserved), installed_plugins.json v2
|
|
1229
|
-
*
|
|
1334
|
+
* and other plugins preserved), installed_plugins.json v2 user registration
|
|
1335
|
+
* (native scope array or legacy object), and the versioned cache
|
|
1230
1336
|
* copy under plugins/cache/<marketplace>/<plugin>/<version>/.
|
|
1231
1337
|
*/
|
|
1232
1338
|
directWriteRegistration(details, version, engineNote) {
|
|
1233
1339
|
const p = this.paths();
|
|
1340
|
+
const cacheVersionDir = workbuddyCacheVersionDir(p.cachePluginRoot, version);
|
|
1341
|
+
if (cacheVersionDir === undefined) {
|
|
1342
|
+
return { ok: false, reason: 'workbuddy_cache_path_invalid: version must resolve strictly inside the PD plugin cache root. No registration/cache write was performed.', nextAction: 'Reinstall with a valid WorkBuddy plugin payload.' };
|
|
1343
|
+
}
|
|
1234
1344
|
const settings = readHostJsonFile(p.settingsPath);
|
|
1235
1345
|
if (!settings.ok)
|
|
1236
1346
|
return { ok: false, reason: settings.reason, nextAction: settings.nextAction };
|
|
1237
1347
|
const registry = readHostJsonFile(p.registryPath);
|
|
1238
1348
|
if (!registry.ok)
|
|
1239
1349
|
return { ok: false, reason: registry.reason, nextAction: registry.nextAction };
|
|
1240
|
-
//
|
|
1350
|
+
// Capture exact pre-install bytes so a later JSON commit failure can undo
|
|
1351
|
+
// *both* host settings and the cache swap, including original formatting.
|
|
1352
|
+
let originalSettingsBytes;
|
|
1353
|
+
let originalRegistryBytes;
|
|
1354
|
+
try {
|
|
1355
|
+
if (settings.existed)
|
|
1356
|
+
originalSettingsBytes = readFileSync(p.settingsPath, 'utf-8');
|
|
1357
|
+
if (registry.existed)
|
|
1358
|
+
originalRegistryBytes = readFileSync(p.registryPath, 'utf-8');
|
|
1359
|
+
}
|
|
1360
|
+
catch (error) {
|
|
1361
|
+
return { ok: false, reason: `workbuddy_registration_snapshot_failed: ${error instanceof Error ? error.message : String(error)}. No change was made.`, nextAction: 'Check host registry/settings permissions, then retry.' };
|
|
1362
|
+
}
|
|
1363
|
+
// settings.json merge — computed here, WRITTEN after the cache lands (see
|
|
1364
|
+
// below): a failed payload must leave no registration state behind.
|
|
1241
1365
|
const nextSettings = { ...(settings.value ?? {}) };
|
|
1242
1366
|
const enabledPlugins = isRecord(nextSettings.enabledPlugins) ? { ...nextSettings.enabledPlugins } : {};
|
|
1243
1367
|
enabledPlugins[WORKBUDDY_PLUGIN_KEY] = true;
|
|
1244
1368
|
nextSettings.enabledPlugins = enabledPlugins;
|
|
1245
|
-
const settingsWrite = writeHostJsonFileProtected(p.settingsPath, JSON.stringify(nextSettings, null, 2) + '\n');
|
|
1246
|
-
if (!settingsWrite.ok)
|
|
1247
|
-
return { ok: false, reason: settingsWrite.reason, nextAction: settingsWrite.nextAction };
|
|
1248
1369
|
const settingsAction = settings.existed ? 'updated' : 'created';
|
|
1249
1370
|
// installed_plugins.json v2 merge (create with version 2 when absent)
|
|
1250
1371
|
const nextRegistry = { ...(registry.value ?? {}) };
|
|
1251
1372
|
if (typeof nextRegistry.version !== 'number')
|
|
1252
1373
|
nextRegistry.version = 2;
|
|
1253
1374
|
const plugins = isRecord(nextRegistry.plugins) ? { ...nextRegistry.plugins } : {};
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1375
|
+
const existingEntry = plugins[WORKBUDDY_PLUGIN_KEY];
|
|
1376
|
+
const previousUser = userInstalledPluginRecord(existingEntry);
|
|
1377
|
+
const now = new Date().toISOString();
|
|
1378
|
+
const userEntry = {
|
|
1379
|
+
...previousUser,
|
|
1256
1380
|
scope: 'user',
|
|
1257
1381
|
installPath: cacheVersionDir,
|
|
1258
1382
|
version,
|
|
1259
|
-
installedAt:
|
|
1383
|
+
installedAt: typeof previousUser?.installedAt === 'string' ? previousUser.installedAt : now,
|
|
1384
|
+
lastUpdated: now,
|
|
1260
1385
|
};
|
|
1386
|
+
// Update only the user registration. Project/local/managed records and
|
|
1387
|
+
// unknown fields remain owned by the host, even under the same plugin key.
|
|
1388
|
+
plugins[WORKBUDDY_PLUGIN_KEY] = Array.isArray(existingEntry)
|
|
1389
|
+
? previousUser !== undefined
|
|
1390
|
+
? existingEntry.map((record) => record === previousUser ? userEntry : record)
|
|
1391
|
+
: [...existingEntry, userEntry]
|
|
1392
|
+
: isRecord(existingEntry) && previousUser === undefined
|
|
1393
|
+
? [existingEntry, userEntry]
|
|
1394
|
+
: userEntry;
|
|
1261
1395
|
nextRegistry.plugins = plugins;
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1396
|
+
// The registry write is DEFERRED until the cache payload is proven in place:
|
|
1397
|
+
// pointing the host at a version whose directory was never materialized
|
|
1398
|
+
// would turn a failed upgrade into an unloadable plugin.
|
|
1265
1399
|
// Versioned cache copy from our marketplace source.
|
|
1400
|
+
//
|
|
1401
|
+
// A same-version refresh REPLACES the payload the host is currently loading,
|
|
1402
|
+
// so the copy never targets the live directory: build it in a staging
|
|
1403
|
+
// sibling, prove it complete, and only then swap. A failure (ENOSPC, EPERM
|
|
1404
|
+
// on a locked file, a partial cpSync) must leave the previous cache intact —
|
|
1405
|
+
// otherwise a failed upgrade trades a working old plugin for a broken one.
|
|
1406
|
+
const previousCacheExisted = existsSync(cacheVersionDir);
|
|
1407
|
+
const cacheParent = path.resolve(path.dirname(p.cachePluginRoot));
|
|
1408
|
+
const stagingDir = path.join(cacheParent, `.pd-staging-${process.pid}-${Date.now()}`);
|
|
1409
|
+
const backupDir = path.join(cacheParent, `.pd-backup-${process.pid}-${Date.now()}`);
|
|
1410
|
+
const swapFailedReason = (error) => `workbuddy_cache_write_failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1411
|
+
let restoreFailure;
|
|
1266
1412
|
try {
|
|
1267
|
-
|
|
1268
|
-
|
|
1413
|
+
if (workbuddyCacheVersionDir(p.cachePluginRoot, version) !== cacheVersionDir)
|
|
1414
|
+
throw new Error('workbuddy_cache_path_invalid');
|
|
1415
|
+
// Both temp paths are derived locally, but a future refactor must not be
|
|
1416
|
+
// able to point them outside the marketplace cache parent.
|
|
1417
|
+
if (path.dirname(path.resolve(stagingDir)) !== cacheParent || path.dirname(path.resolve(backupDir)) !== cacheParent) {
|
|
1418
|
+
throw new Error('workbuddy_cache_path_invalid');
|
|
1419
|
+
}
|
|
1420
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
1421
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
1422
|
+
cpSync(path.join(p.marketplaceRoot, WORKBUDDY_PLUGIN_NAME), stagingDir, { recursive: true });
|
|
1423
|
+
const missing = missingWorkbuddyPayloadFile(stagingDir);
|
|
1424
|
+
if (missing !== undefined)
|
|
1425
|
+
throw new Error(`staged payload is incomplete (missing or empty ${missing})`);
|
|
1426
|
+
// Swap: move the live dir aside, move the verified payload in, drop the backup.
|
|
1427
|
+
if (existsSync(cacheVersionDir))
|
|
1428
|
+
renameSync(cacheVersionDir, backupDir);
|
|
1429
|
+
try {
|
|
1430
|
+
mkdirSync(path.dirname(cacheVersionDir), { recursive: true });
|
|
1431
|
+
renameSync(stagingDir, cacheVersionDir);
|
|
1432
|
+
}
|
|
1433
|
+
catch (swapError) {
|
|
1434
|
+
// Never leave the host without a payload: put the previous one back.
|
|
1435
|
+
try {
|
|
1436
|
+
if (existsSync(backupDir))
|
|
1437
|
+
renameSync(backupDir, cacheVersionDir);
|
|
1438
|
+
}
|
|
1439
|
+
catch (restoreError) {
|
|
1440
|
+
restoreFailure = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
1441
|
+
throw new Error(`workbuddy_cache_swap_failed: ${swapError instanceof Error ? swapError.message : String(swapError)}; restoring the previous cache also failed`, { cause: restoreError });
|
|
1442
|
+
}
|
|
1443
|
+
throw swapError;
|
|
1444
|
+
}
|
|
1445
|
+
// Do not discard the old cache until *both* JSON files are committed.
|
|
1446
|
+
// A registry/settings write can still fail after a successful swap.
|
|
1269
1447
|
}
|
|
1270
1448
|
catch (error) {
|
|
1449
|
+
// The live cache is untouched (or restored); only our temp dir may remain.
|
|
1450
|
+
try {
|
|
1451
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
1452
|
+
}
|
|
1453
|
+
catch { /* reported below */ }
|
|
1454
|
+
const cacheState = restoreFailure === undefined
|
|
1455
|
+
? 'the previous cache payload was left in place'
|
|
1456
|
+
: `the previous cache payload is preserved at ${backupDir} but could NOT be restored in place (${restoreFailure})`;
|
|
1271
1457
|
return {
|
|
1272
1458
|
ok: false,
|
|
1273
|
-
reason:
|
|
1459
|
+
reason: `${swapFailedReason(error)}. No registration state was changed; ${cacheState}.`,
|
|
1274
1460
|
nextAction: `Re-run the installer (idempotent), or run the engine command manually: node <engine> plugin install ${WORKBUDDY_PLUGIN_KEY}.`,
|
|
1275
1461
|
};
|
|
1276
1462
|
}
|
|
1463
|
+
// Cache is verified and in place. Commit the two host files, retaining
|
|
1464
|
+
// the old cache until they both succeed; failures restore all three.
|
|
1465
|
+
const rollbackCommit = (failedWrite) => {
|
|
1466
|
+
const rollbackErrors = [];
|
|
1467
|
+
try {
|
|
1468
|
+
if (previousCacheExisted) {
|
|
1469
|
+
if (!existsSync(backupDir))
|
|
1470
|
+
throw new Error('previous cache backup missing');
|
|
1471
|
+
rmSync(cacheVersionDir, { recursive: true, force: true });
|
|
1472
|
+
renameSync(backupDir, cacheVersionDir);
|
|
1473
|
+
}
|
|
1474
|
+
else {
|
|
1475
|
+
rmSync(cacheVersionDir, { recursive: true, force: true });
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
catch (error) {
|
|
1479
|
+
rollbackErrors.push(`cache: ${error instanceof Error ? error.message : String(error)} (backup: ${backupDir})`);
|
|
1480
|
+
}
|
|
1481
|
+
const restoreJson = (file, original) => {
|
|
1482
|
+
try {
|
|
1483
|
+
if (original === undefined) {
|
|
1484
|
+
rmSync(file, { force: true });
|
|
1485
|
+
}
|
|
1486
|
+
else if (readFileSync(file, 'utf-8') !== original) {
|
|
1487
|
+
const restored = writeHostJsonFileProtected(file, original);
|
|
1488
|
+
if (!restored.ok)
|
|
1489
|
+
throw new Error(restored.reason);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
catch (error) {
|
|
1493
|
+
rollbackErrors.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
restoreJson(p.settingsPath, originalSettingsBytes);
|
|
1497
|
+
restoreJson(p.registryPath, originalRegistryBytes);
|
|
1498
|
+
return {
|
|
1499
|
+
ok: false,
|
|
1500
|
+
reason: `workbuddy_registration_commit_failed: ${failedWrite.reason}. ${rollbackErrors.length === 0 ? 'Previous cache and registration state restored.' : `Rollback incomplete: ${rollbackErrors.join('; ')}`}`,
|
|
1501
|
+
nextAction: rollbackErrors.length === 0 ? failedWrite.nextAction : 'Preserve the reported backup and repair the host registration before retrying.',
|
|
1502
|
+
};
|
|
1503
|
+
};
|
|
1504
|
+
const settingsWrite = writeHostJsonFileProtected(p.settingsPath, JSON.stringify(nextSettings, null, 2) + '\n');
|
|
1505
|
+
if (!settingsWrite.ok)
|
|
1506
|
+
return rollbackCommit(settingsWrite);
|
|
1507
|
+
const registryWrite = writeHostJsonFileProtected(p.registryPath, JSON.stringify(nextRegistry, null, 2) + '\n');
|
|
1508
|
+
if (!registryWrite.ok)
|
|
1509
|
+
return rollbackCommit(registryWrite);
|
|
1510
|
+
// The new installation is now committed; cleanup must not invalidate it.
|
|
1511
|
+
try {
|
|
1512
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
1513
|
+
}
|
|
1514
|
+
catch { /* orphan backup is recoverable, not a failed registration */ }
|
|
1277
1515
|
return { ok: true, mode: 'direct-write', settingsAction, engineNote };
|
|
1278
1516
|
}
|
|
1279
1517
|
// ─── enable / disable (explicit, user-visible — OD-03) ─────────────────────
|
|
@@ -1305,8 +1543,36 @@ export class WorkBuddyHostInstaller {
|
|
|
1305
1543
|
const preservedPaths = [];
|
|
1306
1544
|
const failedPaths = [];
|
|
1307
1545
|
const notes = [];
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1546
|
+
// Fail before ANY destructive operation when the native registry cannot be
|
|
1547
|
+
// read. Treating an unreadable registry as "no references" risks deleting
|
|
1548
|
+
// caches still owned by another scope.
|
|
1549
|
+
const registryPreflight = readHostJsonFile(p.registryPath);
|
|
1550
|
+
if (!registryPreflight.ok) {
|
|
1551
|
+
return {
|
|
1552
|
+
success: false, hostId: this.hostId, removedPaths,
|
|
1553
|
+
preservedPaths: [p.registryPath, p.cachePluginRoot, p.marketplaceRoot],
|
|
1554
|
+
reason: registryPreflight.reason, nextAction: registryPreflight.nextAction,
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
const settingsPreflight = readHostJsonFile(p.settingsPath);
|
|
1558
|
+
if (!settingsPreflight.ok) {
|
|
1559
|
+
return {
|
|
1560
|
+
success: false, hostId: this.hostId, removedPaths,
|
|
1561
|
+
preservedPaths: [p.settingsPath, p.registryPath, p.cachePluginRoot],
|
|
1562
|
+
reason: settingsPreflight.reason, nextAction: settingsPreflight.nextAction,
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
const registeredEntry = isRecord(registryPreflight.value?.plugins)
|
|
1566
|
+
? registryPreflight.value.plugins[WORKBUDDY_PLUGIN_KEY] : undefined;
|
|
1567
|
+
const otherScopesPresent = installedPluginRecords(registeredEntry)
|
|
1568
|
+
.some((record) => !(record.scope === 'user' && record.projectPath === undefined));
|
|
1569
|
+
// The native engine's disable/uninstall commands affect the entire plugin
|
|
1570
|
+
// key (all scopes). They must not be used for a USER-only uninstall when
|
|
1571
|
+
// project/local/managed registrations coexist.
|
|
1572
|
+
if (otherScopesPresent)
|
|
1573
|
+
notes.push('other scopes remain: bypassed host-wide engine commands and preserved their registrations');
|
|
1574
|
+
// 1. Disable first only if this key has no other scope owners.
|
|
1575
|
+
if (details.pluginEnabled && !otherScopesPresent) {
|
|
1310
1576
|
const disable = details.engineFound
|
|
1311
1577
|
? this.runEngine(['plugin', 'disable', WORKBUDDY_PLUGIN_KEY])
|
|
1312
1578
|
: undefined;
|
|
@@ -1315,7 +1581,7 @@ export class WorkBuddyHostInstaller {
|
|
|
1315
1581
|
}
|
|
1316
1582
|
}
|
|
1317
1583
|
// 2. Unregister via the engine when possible.
|
|
1318
|
-
if (details.registered && details.engineFound) {
|
|
1584
|
+
if (details.registered && details.engineFound && !otherScopesPresent) {
|
|
1319
1585
|
const uninstall = this.runEngine(['plugin', 'uninstall', WORKBUDDY_PLUGIN_KEY]);
|
|
1320
1586
|
const engineUninstalled = uninstall !== undefined && uninstall.status === 0 && PLUGIN_UNINSTALL_OK.test(uninstall.stdout + uninstall.stderr);
|
|
1321
1587
|
if (!engineUninstalled && uninstall !== undefined) {
|
|
@@ -1323,7 +1589,9 @@ export class WorkBuddyHostInstaller {
|
|
|
1323
1589
|
}
|
|
1324
1590
|
}
|
|
1325
1591
|
// 3. Direct-write removal of the frozen-format entries when the engine did
|
|
1326
|
-
// not (or could not) remove them. Only OUR key is touched
|
|
1592
|
+
// not (or could not) remove them. Only OUR key is touched, and only the
|
|
1593
|
+
// USER scope: a project/local/managed record under the same key owns its
|
|
1594
|
+
// own installPath and survives this uninstall.
|
|
1327
1595
|
const afterEngine = this.detectDetails();
|
|
1328
1596
|
if (afterEngine.registered || afterEngine.pluginEnabled) {
|
|
1329
1597
|
const removal = this.directWriteRemoval();
|
|
@@ -1339,22 +1607,76 @@ export class WorkBuddyHostInstaller {
|
|
|
1339
1607
|
nextAction: removal.nextAction,
|
|
1340
1608
|
};
|
|
1341
1609
|
}
|
|
1342
|
-
|
|
1610
|
+
if (removal.wroteSettings)
|
|
1611
|
+
removedPaths.push(p.settingsPath);
|
|
1612
|
+
if (removal.wroteRegistry)
|
|
1613
|
+
removedPaths.push(p.registryPath);
|
|
1614
|
+
if (removal.preservedScopes.length > 0) {
|
|
1615
|
+
preservedPaths.push(p.registryPath);
|
|
1616
|
+
notes.push(`preserved ${removal.preservedScopes.join('/')} scope registration(s) under ${WORKBUDDY_PLUGIN_KEY}; only the user scope was uninstalled.`);
|
|
1617
|
+
}
|
|
1343
1618
|
}
|
|
1344
|
-
// 4. Remove
|
|
1619
|
+
// 4. Remove only cache versions that no surviving scope record references.
|
|
1620
|
+
// Another scope may still load an older version from this same root, so
|
|
1621
|
+
// the version dirs are swept individually instead of deleting the root.
|
|
1345
1622
|
if (existsSync(p.cachePluginRoot)) {
|
|
1623
|
+
const references = this.referencedCacheDirs();
|
|
1624
|
+
if (!references.ok) {
|
|
1625
|
+
return {
|
|
1626
|
+
success: false, hostId: this.hostId, removedPaths,
|
|
1627
|
+
preservedPaths: [...preservedPaths, p.cachePluginRoot, p.marketplaceRoot],
|
|
1628
|
+
reason: references.reason, nextAction: references.nextAction,
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
const referenced = references.directories;
|
|
1632
|
+
let entries = [];
|
|
1346
1633
|
try {
|
|
1347
|
-
|
|
1348
|
-
removedPaths.push(p.cachePluginRoot);
|
|
1634
|
+
entries = readdirSync(p.cachePluginRoot);
|
|
1349
1635
|
}
|
|
1350
1636
|
catch (error) {
|
|
1351
1637
|
failedPaths.push(p.cachePluginRoot);
|
|
1352
1638
|
preservedPaths.push(p.cachePluginRoot);
|
|
1353
|
-
notes.push(`cache
|
|
1639
|
+
notes.push(`cache listing failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1640
|
+
}
|
|
1641
|
+
for (const name of entries) {
|
|
1642
|
+
const versionDir = workbuddyCacheVersionDir(p.cachePluginRoot, name);
|
|
1643
|
+
// Preserve unexpected/unknown cache children; only validated version
|
|
1644
|
+
// directories may be removed by a user-scope uninstall.
|
|
1645
|
+
if (versionDir === undefined) {
|
|
1646
|
+
preservedPaths.push(path.join(p.cachePluginRoot, name));
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
if (referenced.has(path.resolve(versionDir))) {
|
|
1650
|
+
preservedPaths.push(versionDir);
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
1655
|
+
removedPaths.push(versionDir);
|
|
1656
|
+
}
|
|
1657
|
+
catch (error) {
|
|
1658
|
+
failedPaths.push(versionDir);
|
|
1659
|
+
preservedPaths.push(versionDir);
|
|
1660
|
+
notes.push(`cache removal failed for ${versionDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
// Drop the root itself only once nothing survives inside it.
|
|
1664
|
+
try {
|
|
1665
|
+
if (existsSync(p.cachePluginRoot) && readdirSync(p.cachePluginRoot).length === 0) {
|
|
1666
|
+
rmSync(p.cachePluginRoot, { recursive: true, force: true });
|
|
1667
|
+
removedPaths.push(p.cachePluginRoot);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
catch {
|
|
1671
|
+
notes.push(`cache root cleanup skipped: ${p.cachePluginRoot} is not removable yet.`);
|
|
1354
1672
|
}
|
|
1355
1673
|
}
|
|
1356
1674
|
// 5. Remove the PD-owned marketplace dir.
|
|
1357
|
-
if (
|
|
1675
|
+
if (otherScopesPresent) {
|
|
1676
|
+
preservedPaths.push(p.marketplaceRoot);
|
|
1677
|
+
notes.push('preserved marketplace and host manifest for remaining non-user scopes');
|
|
1678
|
+
}
|
|
1679
|
+
else if (existsSync(p.marketplaceRoot)) {
|
|
1358
1680
|
try {
|
|
1359
1681
|
rmSync(p.marketplaceRoot, { recursive: true, force: true });
|
|
1360
1682
|
removedPaths.push(p.marketplaceRoot);
|
|
@@ -1367,7 +1689,8 @@ export class WorkBuddyHostInstaller {
|
|
|
1367
1689
|
}
|
|
1368
1690
|
// 6. Manifest bookkeeping: drop 'workbuddy' from hosts. The governance
|
|
1369
1691
|
// workspace entry and ALL governance data are preserved (§18 default).
|
|
1370
|
-
|
|
1692
|
+
if (!otherScopesPresent)
|
|
1693
|
+
removeWorkbuddyFromInstallManifestHosts(p.installManifestPath);
|
|
1371
1694
|
const governancePreservedByDesign = existsSync(p.governanceDir);
|
|
1372
1695
|
if (governancePreservedByDesign) {
|
|
1373
1696
|
preservedPaths.push(p.governanceDir);
|
|
@@ -1390,13 +1713,32 @@ export class WorkBuddyHostInstaller {
|
|
|
1390
1713
|
].filter(Boolean).join(' '),
|
|
1391
1714
|
};
|
|
1392
1715
|
}
|
|
1393
|
-
/**
|
|
1716
|
+
/**
|
|
1717
|
+
* Remove the USER-scope registration only (frozen formats). Other scopes of
|
|
1718
|
+
* the same key are host-owned: dropping the whole key would delete a
|
|
1719
|
+
* project/local/managed install PD never owned.
|
|
1720
|
+
*/
|
|
1394
1721
|
directWriteRemoval() {
|
|
1395
1722
|
const p = this.paths();
|
|
1396
1723
|
const settings = readHostJsonFile(p.settingsPath);
|
|
1397
1724
|
if (!settings.ok)
|
|
1398
1725
|
return { ok: false, reason: settings.reason, nextAction: settings.nextAction };
|
|
1399
|
-
|
|
1726
|
+
const registry = readHostJsonFile(p.registryPath);
|
|
1727
|
+
if (!registry.ok)
|
|
1728
|
+
return { ok: false, reason: registry.reason, nextAction: registry.nextAction };
|
|
1729
|
+
const registered = registry.existed && isRecord(registry.value?.plugins)
|
|
1730
|
+
? registry.value.plugins[WORKBUDDY_PLUGIN_KEY] : undefined;
|
|
1731
|
+
const allRecords = installedPluginRecords(registered);
|
|
1732
|
+
const remaining = allRecords.filter((record) => !(record.scope === 'user' && record.projectPath === undefined));
|
|
1733
|
+
const preservedScopes = [...new Set(remaining
|
|
1734
|
+
.map((record) => (typeof record.scope === 'string' && record.scope.length > 0 ? record.scope : 'unknown')))];
|
|
1735
|
+
const removedUserRecord = remaining.length !== allRecords.length;
|
|
1736
|
+
// enabledPlugins is a single host-level value for the key: clear it only
|
|
1737
|
+
// when no scope remains installed.
|
|
1738
|
+
let wroteSettings = false;
|
|
1739
|
+
if (remaining.length === 0
|
|
1740
|
+
&& settings.existed && isRecord(settings.value?.enabledPlugins)
|
|
1741
|
+
&& Object.hasOwn(settings.value.enabledPlugins, WORKBUDDY_PLUGIN_KEY)) {
|
|
1400
1742
|
const nextSettings = { ...settings.value };
|
|
1401
1743
|
const enabledPlugins = { ...nextSettings.enabledPlugins };
|
|
1402
1744
|
delete enabledPlugins[WORKBUDDY_PLUGIN_KEY];
|
|
@@ -1404,20 +1746,39 @@ export class WorkBuddyHostInstaller {
|
|
|
1404
1746
|
const write = writeHostJsonFileProtected(p.settingsPath, JSON.stringify(nextSettings, null, 2) + '\n');
|
|
1405
1747
|
if (!write.ok)
|
|
1406
1748
|
return { ok: false, reason: write.reason, nextAction: write.nextAction };
|
|
1749
|
+
wroteSettings = true;
|
|
1407
1750
|
}
|
|
1408
|
-
|
|
1409
|
-
if (
|
|
1410
|
-
return { ok: false, reason: registry.reason, nextAction: registry.nextAction };
|
|
1411
|
-
if (registry.existed && isRecord(registry.value?.plugins) && Object.hasOwn(registry.value.plugins, WORKBUDDY_PLUGIN_KEY)) {
|
|
1751
|
+
let wroteRegistry = false;
|
|
1752
|
+
if (removedUserRecord) {
|
|
1412
1753
|
const nextRegistry = { ...registry.value };
|
|
1413
1754
|
const plugins = { ...nextRegistry.plugins };
|
|
1414
|
-
|
|
1755
|
+
if (remaining.length === 0)
|
|
1756
|
+
delete plugins[WORKBUDDY_PLUGIN_KEY];
|
|
1757
|
+
else
|
|
1758
|
+
plugins[WORKBUDDY_PLUGIN_KEY] = Array.isArray(registered) ? remaining : remaining[0];
|
|
1415
1759
|
nextRegistry.plugins = plugins;
|
|
1416
1760
|
const write = writeHostJsonFileProtected(p.registryPath, JSON.stringify(nextRegistry, null, 2) + '\n');
|
|
1417
1761
|
if (!write.ok)
|
|
1418
1762
|
return { ok: false, reason: write.reason, nextAction: write.nextAction };
|
|
1763
|
+
wroteRegistry = true;
|
|
1419
1764
|
}
|
|
1420
|
-
return { ok: true };
|
|
1765
|
+
return { ok: true, wroteSettings, wroteRegistry, preservedScopes };
|
|
1766
|
+
}
|
|
1767
|
+
/** installPaths still referenced by ANY surviving scope record of our key. */
|
|
1768
|
+
referencedCacheDirs() {
|
|
1769
|
+
const p = this.paths();
|
|
1770
|
+
const registry = readHostJsonFile(p.registryPath);
|
|
1771
|
+
if (!registry.ok)
|
|
1772
|
+
return { ok: false, reason: registry.reason, nextAction: registry.nextAction };
|
|
1773
|
+
const referenced = new Set();
|
|
1774
|
+
if (isRecord(registry.value?.plugins)) {
|
|
1775
|
+
for (const record of installedPluginRecords(registry.value.plugins[WORKBUDDY_PLUGIN_KEY])) {
|
|
1776
|
+
if (typeof record.installPath === 'string' && record.installPath.length > 0) {
|
|
1777
|
+
referenced.add(path.resolve(record.installPath));
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return { ok: true, directories: referenced };
|
|
1421
1782
|
}
|
|
1422
1783
|
// ─── detect ───────────────────────────────────────────────────────────────
|
|
1423
1784
|
detect() {
|