@cairn-tool/cairn 3.6.0 → 4.1.0
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/dist/agent/conditionals.d.ts +39 -4
- package/dist/agent/conditionals.js +107 -18
- package/dist/agent/conditionals.js.map +1 -1
- package/dist/agent/import/normalize.js +34 -10
- package/dist/agent/import/normalize.js.map +1 -1
- package/dist/agent/install/index.d.ts +21 -1
- package/dist/agent/install/index.js +301 -46
- package/dist/agent/install/index.js.map +1 -1
- package/dist/agent/naming.d.ts +24 -0
- package/dist/agent/naming.js +48 -0
- package/dist/agent/naming.js.map +1 -0
- package/dist/agent/package/index.js +4 -0
- package/dist/agent/package/index.js.map +1 -1
- package/dist/agent/parser.js +88 -12
- package/dist/agent/parser.js.map +1 -1
- package/dist/agent/render.d.ts +9 -4
- package/dist/agent/render.js +90 -29
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/targets/antigravity.js +14 -2
- package/dist/agent/targets/antigravity.js.map +1 -1
- package/dist/agent/targets/claude-code.js +15 -2
- package/dist/agent/targets/claude-code.js.map +1 -1
- package/dist/agent/targets/codex.js +36 -30
- package/dist/agent/targets/codex.js.map +1 -1
- package/dist/agent/targets/cursor.js +14 -2
- package/dist/agent/targets/cursor.js.map +1 -1
- package/dist/agent/targets/opencode.js +14 -2
- package/dist/agent/targets/opencode.js.map +1 -1
- package/dist/agent/targets/schema.d.ts +48 -3
- package/dist/agent/targets/schema.js +54 -3
- package/dist/agent/targets/schema.js.map +1 -1
- package/dist/agent/types.d.ts +9 -0
- package/dist/agent/types.js +9 -0
- package/dist/agent/types.js.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/agent-install.d.ts +2 -2
- package/dist/commands/agent-install.js +6 -4
- package/dist/commands/agent-install.js.map +1 -1
- package/dist/commands/agent-marketplace.js +2 -1
- package/dist/commands/agent-marketplace.js.map +1 -1
- package/dist/contract/registry.js +3 -3
- package/dist/contract/registry.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
@@ -122,6 +123,14 @@ export function expandInstallRoot(root, context = {}) {
|
|
|
122
123
|
return path.resolve(cwd);
|
|
123
124
|
return path.resolve(cwd, root);
|
|
124
125
|
}
|
|
126
|
+
/** Resolves an install location, honoring a host-specific home override first. */
|
|
127
|
+
export function expandInstallLocationRoot(location, context = {}) {
|
|
128
|
+
const configured = location.environmentRoot;
|
|
129
|
+
const value = configured ? (context.env ?? process.env)[configured.variable] : undefined;
|
|
130
|
+
if (configured && value)
|
|
131
|
+
return path.resolve(value, configured.suffix);
|
|
132
|
+
return expandInstallRoot(location.root, context);
|
|
133
|
+
}
|
|
125
134
|
export function locationFor(target, scope) {
|
|
126
135
|
return profileFor(target).install?.[scope] ?? null;
|
|
127
136
|
}
|
|
@@ -241,19 +250,33 @@ function parseRecord(value) {
|
|
|
241
250
|
const keys = Array.isArray(registration?.pluginKeys)
|
|
242
251
|
? registration.pluginKeys.filter((key) => typeof key === "string")
|
|
243
252
|
: undefined;
|
|
244
|
-
const
|
|
253
|
+
const hasRegistrationKey = typeof registration?.pluginKey === "string" || keys !== undefined;
|
|
254
|
+
let parsedRegistration;
|
|
255
|
+
if (registration?.form === "codex-plugin-cli" &&
|
|
256
|
+
typeof registration.command === "string" &&
|
|
257
|
+
typeof registration.marketplaceKey === "string" &&
|
|
258
|
+
hasRegistrationKey)
|
|
259
|
+
parsedRegistration = {
|
|
260
|
+
form: "codex-plugin-cli",
|
|
261
|
+
command: registration.command,
|
|
262
|
+
marketplaceKey: registration.marketplaceKey,
|
|
263
|
+
...(typeof registration.pluginKey === "string" ? { pluginKey: registration.pluginKey } : {}),
|
|
264
|
+
...(keys ? { pluginKeys: keys } : {}),
|
|
265
|
+
};
|
|
266
|
+
else if (registration &&
|
|
267
|
+
(registration.form === undefined || registration.form === "claude-enabled-plugins") &&
|
|
245
268
|
typeof registration.file === "string" &&
|
|
246
269
|
typeof registration.marketplaceKey === "string" &&
|
|
247
|
-
|
|
248
|
-
|
|
270
|
+
hasRegistrationKey)
|
|
271
|
+
parsedRegistration = {
|
|
272
|
+
...(registration.form === "claude-enabled-plugins"
|
|
273
|
+
? { form: "claude-enabled-plugins" }
|
|
274
|
+
: {}),
|
|
249
275
|
file: registration.file,
|
|
250
276
|
marketplaceKey: registration.marketplaceKey,
|
|
251
|
-
...(typeof registration.pluginKey === "string"
|
|
252
|
-
? { pluginKey: registration.pluginKey }
|
|
253
|
-
: {}),
|
|
277
|
+
...(typeof registration.pluginKey === "string" ? { pluginKey: registration.pluginKey } : {}),
|
|
254
278
|
...(keys ? { pluginKeys: keys } : {}),
|
|
255
|
-
}
|
|
256
|
-
: undefined;
|
|
279
|
+
};
|
|
257
280
|
const collection = doc.collection;
|
|
258
281
|
const plugins = collection && Array.isArray(collection.plugins)
|
|
259
282
|
? collection.plugins.flatMap((entry) => {
|
|
@@ -362,13 +385,11 @@ export function resolveInstallDestination(target, scope, name, options = {}) {
|
|
|
362
385
|
if (!location)
|
|
363
386
|
return error("AB800", `No recorded install location for ${target} ${scope} scope`, {
|
|
364
387
|
target,
|
|
365
|
-
remediation: scope
|
|
366
|
-
? "Codex has no user-scope install; use --scope project, or pick another target."
|
|
367
|
-
: "Use a target and scope the profile declares, or pass --into only after one exists.",
|
|
388
|
+
remediation: "Use a target and scope the profile declares, or pass --into only after one exists.",
|
|
368
389
|
});
|
|
369
390
|
const locationRoot = options.into
|
|
370
391
|
? path.resolve(options.into)
|
|
371
|
-
:
|
|
392
|
+
: expandInstallLocationRoot(location, options);
|
|
372
393
|
const destination = location.layout === "merge" ? locationRoot : path.join(locationRoot, name);
|
|
373
394
|
return { location, locationRoot, destination };
|
|
374
395
|
}
|
|
@@ -430,7 +451,102 @@ function readJsonObject(file) {
|
|
|
430
451
|
throw new Error(`Activation file is not a JSON object: ${file}`);
|
|
431
452
|
return parsed;
|
|
432
453
|
}
|
|
433
|
-
function
|
|
454
|
+
function isCodexRegistration(registration) {
|
|
455
|
+
return registration.form === "codex-plugin-cli";
|
|
456
|
+
}
|
|
457
|
+
function commandJson(command, args) {
|
|
458
|
+
const result = spawnSync(command, args, {
|
|
459
|
+
encoding: "utf8",
|
|
460
|
+
env: process.env,
|
|
461
|
+
maxBuffer: 1024 * 1024,
|
|
462
|
+
});
|
|
463
|
+
const invocation = [command, ...args].join(" ");
|
|
464
|
+
if (result.error)
|
|
465
|
+
throw new Error(`Failed to run '${invocation}': ${result.error.message}`);
|
|
466
|
+
if (result.status !== 0) {
|
|
467
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
468
|
+
throw new Error(`Host command '${invocation}' exited ${result.status ?? "without a status"}${detail ? `: ${detail}` : ""}`);
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const parsed = JSON.parse(result.stdout);
|
|
472
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
473
|
+
throw new Error("output is not an object");
|
|
474
|
+
return parsed;
|
|
475
|
+
}
|
|
476
|
+
catch (cause) {
|
|
477
|
+
throw new Error(`Host command '${invocation}' returned invalid JSON`, { cause });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function codexMarketplaces(registration) {
|
|
481
|
+
const value = commandJson(registration.command, [
|
|
482
|
+
"plugin",
|
|
483
|
+
"marketplace",
|
|
484
|
+
"list",
|
|
485
|
+
"--json",
|
|
486
|
+
]).marketplaces;
|
|
487
|
+
if (!Array.isArray(value))
|
|
488
|
+
throw new Error("Codex marketplace list returned no marketplaces");
|
|
489
|
+
return value.flatMap((entry) => {
|
|
490
|
+
const row = entry;
|
|
491
|
+
return row && typeof row.name === "string" && typeof row.root === "string"
|
|
492
|
+
? [{ name: row.name, root: row.root }]
|
|
493
|
+
: [];
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
function codexPlugins(registration) {
|
|
497
|
+
const value = commandJson(registration.command, ["plugin", "list", "--json"]).installed;
|
|
498
|
+
if (!Array.isArray(value))
|
|
499
|
+
throw new Error("Codex plugin list returned no installed plugins");
|
|
500
|
+
return value.flatMap((entry) => {
|
|
501
|
+
const row = entry;
|
|
502
|
+
return row && typeof row.pluginId === "string"
|
|
503
|
+
? [
|
|
504
|
+
{
|
|
505
|
+
pluginId: row.pluginId,
|
|
506
|
+
...(typeof row.version === "string" ? { version: row.version } : {}),
|
|
507
|
+
installed: row.installed === true,
|
|
508
|
+
enabled: row.enabled === true,
|
|
509
|
+
},
|
|
510
|
+
]
|
|
511
|
+
: [];
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function canonicalPath(file) {
|
|
515
|
+
try {
|
|
516
|
+
return fs.realpathSync(file);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return path.resolve(file);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function samePath(left, right) {
|
|
523
|
+
return canonicalPath(left) === canonicalPath(right);
|
|
524
|
+
}
|
|
525
|
+
function codexMarketplace(registration) {
|
|
526
|
+
return codexMarketplaces(registration).find((marketplace) => marketplace.name === registration.marketplaceKey);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Checks host-side conflicts before a batch writes any files.
|
|
530
|
+
*
|
|
531
|
+
* In particular, `--force` never takes over a same-named Codex marketplace
|
|
532
|
+
* that points somewhere else: it is external state Cairn does not own.
|
|
533
|
+
*/
|
|
534
|
+
export function preflightInstallRegistrations(plans) {
|
|
535
|
+
const checked = new Set();
|
|
536
|
+
for (const plan of plans) {
|
|
537
|
+
const registration = plan.settings;
|
|
538
|
+
if (!plan.register || !registration || !isCodexRegistration(registration))
|
|
539
|
+
continue;
|
|
540
|
+
const key = `${registration.command}\0${registration.marketplaceKey}\0${plan.destination}`;
|
|
541
|
+
if (checked.has(key))
|
|
542
|
+
continue;
|
|
543
|
+
checked.add(key);
|
|
544
|
+
const current = codexMarketplace(registration);
|
|
545
|
+
if (current && !samePath(current.root, plan.destination))
|
|
546
|
+
throw new Error(`Codex marketplace '${registration.marketplaceKey}' already points to ${current.root}; refusing to replace it with ${plan.destination}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function applyClaudeRegistration(settings, destination) {
|
|
434
550
|
const current = readJsonObject(settings.file);
|
|
435
551
|
const extra = {
|
|
436
552
|
...(current.extraKnownMarketplaces ?? {}),
|
|
@@ -449,7 +565,55 @@ function applyRegistration(settings, destination) {
|
|
|
449
565
|
enabledPlugins: enabled,
|
|
450
566
|
});
|
|
451
567
|
}
|
|
452
|
-
function
|
|
568
|
+
function applyCodexRegistration(registration, destination) {
|
|
569
|
+
const marketplace = codexMarketplace(registration);
|
|
570
|
+
if (marketplace && !samePath(marketplace.root, destination))
|
|
571
|
+
throw new Error(`Codex marketplace '${registration.marketplaceKey}' already points to ${marketplace.root}; refusing to replace it with ${destination}`);
|
|
572
|
+
const installedBefore = new Set(codexPlugins(registration)
|
|
573
|
+
.filter((plugin) => plugin.installed)
|
|
574
|
+
.map((plugin) => plugin.pluginId));
|
|
575
|
+
const added = [];
|
|
576
|
+
try {
|
|
577
|
+
commandJson(registration.command, ["plugin", "marketplace", "add", destination, "--json"]);
|
|
578
|
+
for (const key of registeredPluginKeys(registration)) {
|
|
579
|
+
commandJson(registration.command, ["plugin", "add", key, "--json"]);
|
|
580
|
+
if (!installedBefore.has(key))
|
|
581
|
+
added.push(key);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch (cause) {
|
|
585
|
+
for (const key of added.reverse()) {
|
|
586
|
+
try {
|
|
587
|
+
commandJson(registration.command, ["plugin", "remove", key, "--json"]);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// Best effort: preserve the original host failure.
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (!marketplace) {
|
|
594
|
+
try {
|
|
595
|
+
commandJson(registration.command, [
|
|
596
|
+
"plugin",
|
|
597
|
+
"marketplace",
|
|
598
|
+
"remove",
|
|
599
|
+
registration.marketplaceKey,
|
|
600
|
+
"--json",
|
|
601
|
+
]);
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// Best effort: preserve the original host failure.
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
throw cause;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function applyRegistration(registration, destination) {
|
|
611
|
+
if (isCodexRegistration(registration))
|
|
612
|
+
applyCodexRegistration(registration, destination);
|
|
613
|
+
else
|
|
614
|
+
applyClaudeRegistration(registration, destination);
|
|
615
|
+
}
|
|
616
|
+
function revertClaudeRegistration(registration, destination) {
|
|
453
617
|
if (!existsAt(registration.file))
|
|
454
618
|
return;
|
|
455
619
|
const current = readJsonObject(registration.file);
|
|
@@ -470,7 +634,54 @@ function revertRegistration(registration, destination) {
|
|
|
470
634
|
enabledPlugins: enabled,
|
|
471
635
|
});
|
|
472
636
|
}
|
|
473
|
-
function
|
|
637
|
+
function revertCodexRegistration(registration, destination) {
|
|
638
|
+
const marketplace = codexMarketplace(registration);
|
|
639
|
+
// The name now belongs to a different root. Removing either its plugins or
|
|
640
|
+
// the marketplace would mutate state Cairn did not install.
|
|
641
|
+
if (marketplace && !samePath(marketplace.root, destination))
|
|
642
|
+
return;
|
|
643
|
+
const installed = new Set(codexPlugins(registration)
|
|
644
|
+
.filter((plugin) => plugin.installed)
|
|
645
|
+
.map((plugin) => plugin.pluginId));
|
|
646
|
+
for (const key of registeredPluginKeys(registration))
|
|
647
|
+
if (installed.has(key))
|
|
648
|
+
commandJson(registration.command, ["plugin", "remove", key, "--json"]);
|
|
649
|
+
if (marketplace)
|
|
650
|
+
commandJson(registration.command, [
|
|
651
|
+
"plugin",
|
|
652
|
+
"marketplace",
|
|
653
|
+
"remove",
|
|
654
|
+
registration.marketplaceKey,
|
|
655
|
+
"--json",
|
|
656
|
+
]);
|
|
657
|
+
}
|
|
658
|
+
function revertRegistration(registration, destination) {
|
|
659
|
+
if (isCodexRegistration(registration))
|
|
660
|
+
revertCodexRegistration(registration, destination);
|
|
661
|
+
else
|
|
662
|
+
revertClaudeRegistration(registration, destination);
|
|
663
|
+
}
|
|
664
|
+
function expectedPluginVersions(registration, record) {
|
|
665
|
+
if (record.kind === "collection" && record.collection)
|
|
666
|
+
return new Map(record.collection.plugins.map((plugin) => [
|
|
667
|
+
`${plugin.name}@${registration.marketplaceKey}`,
|
|
668
|
+
plugin.version,
|
|
669
|
+
]));
|
|
670
|
+
return new Map(registeredPluginKeys(registration).map((key) => [key, record.bundle.version]));
|
|
671
|
+
}
|
|
672
|
+
function codexRegistrationCurrent(registration, destination, record) {
|
|
673
|
+
const marketplace = codexMarketplace(registration);
|
|
674
|
+
if (!marketplace || !samePath(marketplace.root, destination))
|
|
675
|
+
return false;
|
|
676
|
+
const installed = new Map(codexPlugins(registration).map((plugin) => [plugin.pluginId, plugin]));
|
|
677
|
+
for (const [key, version] of expectedPluginVersions(registration, record)) {
|
|
678
|
+
const plugin = installed.get(key);
|
|
679
|
+
if (!plugin?.installed || !plugin.enabled || plugin.version !== version)
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
function claudeRegistrationCurrent(settings, destination) {
|
|
474
685
|
if (!existsAt(settings.file))
|
|
475
686
|
return false;
|
|
476
687
|
try {
|
|
@@ -484,6 +695,11 @@ function registrationCurrent(settings, destination) {
|
|
|
484
695
|
return false;
|
|
485
696
|
}
|
|
486
697
|
}
|
|
698
|
+
function registrationCurrent(settings, destination, record) {
|
|
699
|
+
return isCodexRegistration(settings)
|
|
700
|
+
? codexRegistrationCurrent(settings, destination, record)
|
|
701
|
+
: claudeRegistrationCurrent(settings, destination);
|
|
702
|
+
}
|
|
487
703
|
function pruneEmptyAncestors(root, relative) {
|
|
488
704
|
let current = path.dirname(path.join(root, relative));
|
|
489
705
|
const stop = path.resolve(root);
|
|
@@ -518,7 +734,9 @@ export function installIsCurrent(plan) {
|
|
|
518
734
|
// would leave --check reporting "current" while uninstall reports not-found.
|
|
519
735
|
if (readInstallRecord(plan.destination, installKey(plan.record)) === "missing")
|
|
520
736
|
return false;
|
|
521
|
-
if (plan.register &&
|
|
737
|
+
if (plan.register &&
|
|
738
|
+
plan.settings &&
|
|
739
|
+
!registrationCurrent(plan.settings, plan.destination, plan.record))
|
|
522
740
|
return false;
|
|
523
741
|
return true;
|
|
524
742
|
}
|
|
@@ -581,6 +799,42 @@ function unresolvedPlan(bundle, target, scope, mode, resolved) {
|
|
|
581
799
|
register: false,
|
|
582
800
|
};
|
|
583
801
|
}
|
|
802
|
+
function registrationFor(location, marketplaceKey, pluginKeys, context, single) {
|
|
803
|
+
if (location.layout !== "marketplace" || !location.activation)
|
|
804
|
+
return undefined;
|
|
805
|
+
const keys = single ? { pluginKey: pluginKeys[0] } : { pluginKeys };
|
|
806
|
+
if (location.activation.form === "codex-plugin-cli")
|
|
807
|
+
return {
|
|
808
|
+
form: "codex-plugin-cli",
|
|
809
|
+
command: location.activation.command,
|
|
810
|
+
marketplaceKey,
|
|
811
|
+
...keys,
|
|
812
|
+
};
|
|
813
|
+
return {
|
|
814
|
+
file: expandInstallRoot(location.activation.file, context),
|
|
815
|
+
marketplaceKey,
|
|
816
|
+
...keys,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function activationDiagnostic(registration, destination, target) {
|
|
820
|
+
const keys = registeredPluginKeys(registration);
|
|
821
|
+
if (isCodexRegistration(registration)) {
|
|
822
|
+
const commands = [
|
|
823
|
+
`${registration.command} plugin marketplace add ${destination} --json`,
|
|
824
|
+
...keys.map((key) => `${registration.command} plugin add ${key} --json`),
|
|
825
|
+
];
|
|
826
|
+
return diagnostic("AB805", `Host activation required but --register was not given. Run: ${commands.join("; ")}.`, "unsupported", {
|
|
827
|
+
target,
|
|
828
|
+
path: destination,
|
|
829
|
+
remediation: "Re-run with --register, or run the reported Codex commands yourself.",
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
return diagnostic("AB805", `Host activation edit required but --register was not given. Add extraKnownMarketplaces.${registration.marketplaceKey} (directory ${destination}) and enabledPlugins for ${keys.join(", ")} to ${registration.file}.`, "unsupported", {
|
|
833
|
+
target,
|
|
834
|
+
path: registration.file,
|
|
835
|
+
remediation: "Re-run with --register, or apply the edit yourself.",
|
|
836
|
+
});
|
|
837
|
+
}
|
|
584
838
|
function draftInstall(bundle, target, options) {
|
|
585
839
|
const scope = resolveScope(options.scope, "user");
|
|
586
840
|
const resolved = resolveInstallDestination(target, scope, bundle.name, options);
|
|
@@ -602,23 +856,10 @@ function draftInstall(bundle, target, options) {
|
|
|
602
856
|
target,
|
|
603
857
|
profile: location.profile,
|
|
604
858
|
}));
|
|
605
|
-
const
|
|
606
|
-
? expandInstallRoot(location.activation.file, options)
|
|
607
|
-
: undefined;
|
|
608
|
-
const settings = location.layout === "marketplace" && settingsFile
|
|
609
|
-
? {
|
|
610
|
-
file: settingsFile,
|
|
611
|
-
marketplaceKey: bundle.name,
|
|
612
|
-
pluginKey: `${bundle.name}@${bundle.name}`,
|
|
613
|
-
}
|
|
614
|
-
: undefined;
|
|
859
|
+
const settings = registrationFor(location, bundle.name, [`${bundle.name}@${bundle.name}`], options, true);
|
|
615
860
|
const register = Boolean(options.register) && Boolean(settings);
|
|
616
861
|
if (settings && !options.register)
|
|
617
|
-
diagnostics.push(
|
|
618
|
-
target,
|
|
619
|
-
path: settingsFile,
|
|
620
|
-
remediation: "Re-run with --register, or apply the edit yourself.",
|
|
621
|
-
}));
|
|
862
|
+
diagnostics.push(activationDiagnostic(settings, destination, target));
|
|
622
863
|
if (mode === "link")
|
|
623
864
|
diagnostics.push(diagnostic("AB807", "--link is in use; edits to the materialized tree are live and the host may not follow symlinks", "exact", { target, profile: location.profile, path: destination }));
|
|
624
865
|
const materialized = mode === "link" ? path.join(bundle.root, INSTALL_CACHE, target, location.profile) : undefined;
|
|
@@ -840,20 +1081,11 @@ export function planCollectionInstall(collection, options = {}) {
|
|
|
840
1081
|
target: collection.target,
|
|
841
1082
|
profile: "plugin",
|
|
842
1083
|
}));
|
|
843
|
-
const settingsFile = location.activation
|
|
844
|
-
? expandInstallRoot(location.activation.file, options)
|
|
845
|
-
: undefined;
|
|
846
1084
|
const pluginKeys = collection.plugins.map((plugin) => `${plugin.name}@${collection.name}`);
|
|
847
|
-
const settings = location.
|
|
848
|
-
? { file: settingsFile, marketplaceKey: collection.name, pluginKeys }
|
|
849
|
-
: undefined;
|
|
1085
|
+
const settings = registrationFor(location, collection.name, pluginKeys, options, false);
|
|
850
1086
|
const register = Boolean(options.register) && Boolean(settings);
|
|
851
1087
|
if (settings && !options.register)
|
|
852
|
-
diagnostics.push(
|
|
853
|
-
target: collection.target,
|
|
854
|
-
path: settingsFile,
|
|
855
|
-
remediation: "Re-run with --register, or apply the edit yourself.",
|
|
856
|
-
}));
|
|
1088
|
+
diagnostics.push(activationDiagnostic(settings, destination, collection.target));
|
|
857
1089
|
if (mode === "link")
|
|
858
1090
|
diagnostics.push(diagnostic("AB807", "--link is in use; edits to the materialized tree are live and the host may not follow symlinks", "exact", { target: collection.target, profile: "plugin", path: destination }));
|
|
859
1091
|
const materialized = mode === "link" ? options.materializeInto : undefined;
|
|
@@ -971,6 +1203,19 @@ function retirePrior(plan) {
|
|
|
971
1203
|
if (previous.registration && !plan.record.registration)
|
|
972
1204
|
revertRegistration(previous.registration, plan.destination);
|
|
973
1205
|
}
|
|
1206
|
+
function restoreRegistrationRecordAfterFailure(plan) {
|
|
1207
|
+
const key = installKey(plan.record);
|
|
1208
|
+
const previous = plan.prior === "missing" || plan.prior === "malformed"
|
|
1209
|
+
? undefined
|
|
1210
|
+
: plan.prior.installs.find((record) => installKey(record) === key)?.registration;
|
|
1211
|
+
const installs = plan.document.installs.map((record) => {
|
|
1212
|
+
if (installKey(record) !== key)
|
|
1213
|
+
return record;
|
|
1214
|
+
const { registration: _failed, ...without } = record;
|
|
1215
|
+
return previous ? { ...without, registration: previous } : without;
|
|
1216
|
+
});
|
|
1217
|
+
writeJsonAtomically(path.join(plan.destination, INSTALL_MANIFEST), serializeDocument({ generator: currentGenerator(), installs: sortRecords(installs) }));
|
|
1218
|
+
}
|
|
974
1219
|
/** Writes a planned install. Caller must have already decided the run is not blocked. */
|
|
975
1220
|
export function commitInstall(plan) {
|
|
976
1221
|
retirePrior(plan);
|
|
@@ -979,7 +1224,15 @@ export function commitInstall(plan) {
|
|
|
979
1224
|
else
|
|
980
1225
|
writeCopy(plan);
|
|
981
1226
|
if (plan.register && plan.settings)
|
|
982
|
-
|
|
1227
|
+
try {
|
|
1228
|
+
applyRegistration(plan.settings, plan.destination);
|
|
1229
|
+
}
|
|
1230
|
+
catch (cause) {
|
|
1231
|
+
// The filesystem install remains useful, but its manifest must not claim
|
|
1232
|
+
// host activation that the driver just rolled back.
|
|
1233
|
+
restoreRegistrationRecordAfterFailure(plan);
|
|
1234
|
+
throw cause;
|
|
1235
|
+
}
|
|
983
1236
|
}
|
|
984
1237
|
export function planToEntry(plan) {
|
|
985
1238
|
return toEntry(plan.record);
|
|
@@ -1086,6 +1339,10 @@ export function commitUninstall(plan) {
|
|
|
1086
1339
|
if (!record)
|
|
1087
1340
|
return;
|
|
1088
1341
|
const destination = plan.destination;
|
|
1342
|
+
// Host state is the harder-to-recover half. If its driver fails, leave the
|
|
1343
|
+
// marketplace tree and manifest intact so a retry can still identify it.
|
|
1344
|
+
if (record.registration)
|
|
1345
|
+
revertRegistration(record.registration, destination);
|
|
1089
1346
|
const document = readInstallDocument(destination);
|
|
1090
1347
|
const key = installKey(record);
|
|
1091
1348
|
const remaining = document === "missing" || document === "malformed"
|
|
@@ -1123,8 +1380,6 @@ export function commitUninstall(plan) {
|
|
|
1123
1380
|
}
|
|
1124
1381
|
if (record.materialized)
|
|
1125
1382
|
removePath(record.materialized);
|
|
1126
|
-
if (record.registration)
|
|
1127
|
-
revertRegistration(record.registration, destination);
|
|
1128
1383
|
}
|
|
1129
1384
|
function scanRoot(target, scope, location, locationRoot) {
|
|
1130
1385
|
const entries = [];
|
|
@@ -1164,7 +1419,7 @@ export function listInstalled(targets, options = {}) {
|
|
|
1164
1419
|
continue;
|
|
1165
1420
|
const locationRoot = options.into
|
|
1166
1421
|
? path.resolve(options.into)
|
|
1167
|
-
:
|
|
1422
|
+
: expandInstallLocationRoot(location, options);
|
|
1168
1423
|
entries.push(...scanRoot(target, scope, location, locationRoot));
|
|
1169
1424
|
}
|
|
1170
1425
|
}
|