grok-bot-cli 0.8.0 → 0.10.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/CHANGELOG.md +30 -0
- package/README.md +65 -0
- package/dist/.claude-plugin/marketplace.json +1 -1
- package/dist/.claude-plugin/plugin.json +1 -1
- package/dist/.codex-plugin/plugin.json +1 -1
- package/dist/.cursor-plugin/plugin.json +1 -1
- package/dist/.mcp.json +1 -1
- package/dist/AGENTS.md +12 -0
- package/dist/INSTALL.md +47 -39
- package/dist/README.md +65 -0
- package/dist/agent-bundle.compile-evidence.json +1 -1
- package/dist/agent-bundle.manifest.json +1 -1
- package/dist/agent-bundle.package-compile-evidence.json +1 -1
- package/dist/bin/gbot-flight.mjs +25242 -31532
- package/dist/bin/gbot-install.js +71105 -135394
- package/dist/bin/gbot.mjs +43299 -90567
- package/dist/install.mjs +66 -142
- package/dist/mcp/mcp-claude-channel-8029413c.mjs +45557 -0
- package/dist/mcp/mcp-grok-bot-b8c2461e-flight.mjs +23740 -29992
- package/dist/mcp/mcp-grok-bot-b8c2461e.mjs +60333 -111638
- package/dist/package.json +5 -3
- package/dist/plugin.json +1 -1
- package/dist/scripts/gbot-relay.mjs +26991 -60592
- package/dist/skills/talk-to-grok-bot/SKILL.md +22 -81
- package/dist/skills/talk-to-grok-bot/references/bridge-administration.md +84 -0
- package/package.json +5 -3
package/dist/install.mjs
CHANGED
|
@@ -7,14 +7,12 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'nod
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
9
|
const pluginName = "gbot";
|
|
10
|
-
const pluginVersion = "0.
|
|
10
|
+
const pluginVersion = "0.10.0";
|
|
11
11
|
const receiptFile = ".agent-bundle-install.json";
|
|
12
12
|
const receiptFormat = "agent-bundle-install-receipt/2";
|
|
13
|
-
const legacyReceiptFormat = "agent-bundle-install-receipt/1";
|
|
14
13
|
const preservedEntries = ["state"];
|
|
15
14
|
// Runtime roots match case-insensitively: on case-insensitive filesystems State/ is state/.
|
|
16
15
|
const isPreservedRoot = (name) => preservedEntries.includes(String(name).toLowerCase());
|
|
17
|
-
const markerFiles = ["INSTALL.md","install.mjs"];
|
|
18
16
|
const source = resolve(fileURLToPath(new URL('.', import.meta.url)));
|
|
19
17
|
const artifactManifest = await (async () => {
|
|
20
18
|
try {
|
|
@@ -39,7 +37,7 @@ const marketplacePlugin = join(marketplaceRepo, 'plugins', pluginName);
|
|
|
39
37
|
const pluginData = join(cursorRoot, 'agent-bundle', 'plugin-data', pluginName);
|
|
40
38
|
const receiptsRoot = join(cursorRoot, 'agent-bundle', 'receipts');
|
|
41
39
|
const marketplaceReceipt = join(receiptsRoot, `${pluginName}.marketplace.json`);
|
|
42
|
-
const usage = 'Usage: node install.mjs [--mode local|marketplace] [--replace
|
|
40
|
+
const usage = 'Usage: node install.mjs [--mode local|marketplace] [--replace] [--help]\n node install.mjs --uninstall [--mode local|marketplace] [--keep-data | --purge-data --confirm-purge] [--force] [--plan]';
|
|
43
41
|
|
|
44
42
|
let replace = false;
|
|
45
43
|
let force = false;
|
|
@@ -52,7 +50,8 @@ let mode = 'local';
|
|
|
52
50
|
const argv = process.argv.slice(2);
|
|
53
51
|
for (let index = 0; index < argv.length; index += 1) {
|
|
54
52
|
const argument = argv[index];
|
|
55
|
-
if (argument === '--replace'
|
|
53
|
+
if (argument === '--replace') { replace = true; continue; }
|
|
54
|
+
if (argument === '--force') { force = true; continue; }
|
|
56
55
|
if (argument === '--uninstall') { uninstall = true; continue; }
|
|
57
56
|
if (argument === '--plan') { plan = true; continue; }
|
|
58
57
|
if (argument === '--keep-data') { keepData = true; continue; }
|
|
@@ -68,6 +67,8 @@ for (let index = 0; index < argv.length; index += 1) {
|
|
|
68
67
|
console.error(`Unknown installer argument ${JSON.stringify(argument)}.\n${usage}`);
|
|
69
68
|
process.exit(2);
|
|
70
69
|
}
|
|
70
|
+
if (!uninstall && force) { console.error(`--force applies to --uninstall only. Use --replace to replace an install.\n${usage}`); process.exit(2); }
|
|
71
|
+
if (uninstall && replace) { console.error(`--replace applies to installation only. Use --force to override uninstall ownership checks.\n${usage}`); process.exit(2); }
|
|
71
72
|
if (!uninstall && (plan || keepData || purgeData || confirmPurge)) {
|
|
72
73
|
console.error(`--plan, --keep-data, --purge-data, and --confirm-purge apply to --uninstall only.\n${usage}`);
|
|
73
74
|
process.exit(2);
|
|
@@ -252,9 +253,7 @@ const isReceiptState = (value) => value !== null && typeof value === 'object' &&
|
|
|
252
253
|
typeof root.root === 'string' && isAbsolute(root.root) && typeof root.canonicalRoot === 'string' && isAbsolute(root.canonicalRoot) &&
|
|
253
254
|
['declared', 'derived'].includes(root.source) && Array.isArray(root.servers) && root.servers.every((server) => typeof server === 'string') &&
|
|
254
255
|
isStateOwnership(root.ownership));
|
|
255
|
-
// Same shape check as the core reader: a receipt missing any field
|
|
256
|
-
// is read with its lifecycle fields synthesized (local mode, user scope, one cursor-local-plugin registration,
|
|
257
|
-
// no host directories) and `migratedFrom` set; the next replacement rewrites it as the current format.
|
|
256
|
+
// Same shape check as the core reader: a receipt missing any field, or of any other format, reads as absent.
|
|
258
257
|
const readReceiptFile = async (path) => {
|
|
259
258
|
let value;
|
|
260
259
|
try {
|
|
@@ -262,18 +261,13 @@ const readReceiptFile = async (path) => {
|
|
|
262
261
|
value = JSON.parse(await readFile(path, 'utf8'));
|
|
263
262
|
} catch (error) { if (error?.code === 'ENOENT' || error instanceof SyntaxError) return undefined; throw error; }
|
|
264
263
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
265
|
-
if (
|
|
264
|
+
if (value.format !== receiptFormat ||
|
|
266
265
|
typeof value.plugin !== 'string' || typeof value.version !== 'string' ||
|
|
267
266
|
typeof value.host !== 'string' || typeof value.contentHash !== 'string' || typeof value.installedAt !== 'string' ||
|
|
268
267
|
!Array.isArray(value.files) || !value.files.every(safeRelative) ||
|
|
269
268
|
!Array.isArray(value.directories) || !value.directories.every(safeRelative) ||
|
|
270
269
|
(value.state !== undefined && !isReceiptState(value.state)) ||
|
|
271
|
-
(value.stateRoot !== undefined && (value.stateRoot === null || typeof value.stateRoot !== 'object' || Array.isArray(value.stateRoot) || typeof value.stateRoot.root !== 'string' || !['derived', 'native'].includes(value.stateRoot.source))) ||
|
|
272
270
|
(value.webDataRoot !== undefined && typeof value.webDataRoot !== 'string')) return undefined;
|
|
273
|
-
if (value.format === legacyReceiptFormat) {
|
|
274
|
-
return { ...value, format: receiptFormat, hostDirectories: [], migratedFrom: legacyReceiptFormat, mode: 'local',
|
|
275
|
-
registrations: [{ kind: 'cursor-local-plugin' }], scope: 'user', updatedAt: value.installedAt };
|
|
276
|
-
}
|
|
277
271
|
if (!['host-cli', 'local', 'marketplace'].includes(value.mode) || !isScope(value.scope) || typeof value.updatedAt !== 'string' ||
|
|
278
272
|
!Array.isArray(value.hostDirectories) || !value.hostDirectories.every(safeRelative) ||
|
|
279
273
|
!Array.isArray(value.registrations) || !value.registrations.every(isRegistration)) return undefined;
|
|
@@ -312,11 +306,6 @@ const readManifest = async (root) => {
|
|
|
312
306
|
return undefined;
|
|
313
307
|
};
|
|
314
308
|
|
|
315
|
-
const hasMarkers = async (root) => {
|
|
316
|
-
for (const marker of markerFiles) if (!(await exists(join(root, marker)))) return false;
|
|
317
|
-
return true;
|
|
318
|
-
};
|
|
319
|
-
|
|
320
309
|
// Agent Plugins packs (root plugin.json with an agent-plugins.org $schema, no .cursor-plugin/plugin.json).
|
|
321
310
|
// Observed on Cursor 3.18.25 (docs/audits/2026-09-03-agent-plugins-cursor-ide-proof.md): the loader spawns their
|
|
322
311
|
// stdio servers without expanding ${PLUGIN_ROOT}/${PLUGIN_DATA} in args, env values or cwd, without providing
|
|
@@ -408,7 +397,6 @@ const receiptFor = (tree, options = {}) => {
|
|
|
408
397
|
registrations: options.registrations ?? [{ kind: 'cursor-local-plugin' }],
|
|
409
398
|
scope: 'user',
|
|
410
399
|
...(options.state === undefined ? {} : { state: options.state }),
|
|
411
|
-
...(options.stateRoot === undefined ? {} : { stateRoot: options.stateRoot }),
|
|
412
400
|
updatedAt: now,
|
|
413
401
|
version: pluginVersion,
|
|
414
402
|
...(options.webDataRoot === undefined ? {} : { webDataRoot: options.webDataRoot }),
|
|
@@ -585,8 +573,8 @@ const ensureAncestors = async (file, created) => {
|
|
|
585
573
|
}
|
|
586
574
|
};
|
|
587
575
|
|
|
588
|
-
// Unowned entries under root that survive the uninstall, POSIX-relative: files that are
|
|
589
|
-
//
|
|
576
|
+
// Unowned entries under root that survive the uninstall, POSIX-relative: files that are not owned (symlinks listed,
|
|
577
|
+
// never followed) plus unowned directories holding nothing retained (`name/`), which the prune never touches.
|
|
590
578
|
const listRetained = async (root, owned, ownedDirectories) => {
|
|
591
579
|
const retained = [];
|
|
592
580
|
const visit = async (relativePath) => {
|
|
@@ -596,7 +584,7 @@ const listRetained = async (root, owned, ownedDirectories) => {
|
|
|
596
584
|
let kept = 0;
|
|
597
585
|
for (const name of entries) {
|
|
598
586
|
const child = relativePath === '' ? name : `${relativePath}/${name}`;
|
|
599
|
-
if (relativePath === '' &&
|
|
587
|
+
if (relativePath === '' && name === receiptFile) continue;
|
|
600
588
|
const metadata = await lstat(join(root, child));
|
|
601
589
|
if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
|
|
602
590
|
const below = await visit(child);
|
|
@@ -659,7 +647,7 @@ const runtimeStateRoots = async () => {
|
|
|
659
647
|
const webCanonical = resolve(destination);
|
|
660
648
|
const webDigest = createHash('sha256').update(webCanonical).digest('hex').slice(0, 16);
|
|
661
649
|
const webName = /^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$/u.test(basename(webCanonical)) ? basename(webCanonical) : 'plugin';
|
|
662
|
-
return [explicitStateRoot ?? join(stateHome, segment), join(
|
|
650
|
+
return [explicitStateRoot ?? join(stateHome, segment), join(homedir(), '.agent-bundle', 'web-data', `${webName}-${webDigest}`)];
|
|
663
651
|
};
|
|
664
652
|
|
|
665
653
|
if (uninstall && mode === 'local') {
|
|
@@ -668,48 +656,29 @@ if (uninstall && mode === 'local') {
|
|
|
668
656
|
const destinationMetadata = await lstat(destination);
|
|
669
657
|
if (destinationMetadata.isSymbolicLink() || !destinationMetadata.isDirectory()) throw unsupported('.');
|
|
670
658
|
const receipt = await readReceipt(destination);
|
|
671
|
-
let owned;
|
|
672
|
-
let ownedDirectories;
|
|
673
|
-
let hostDirectories;
|
|
674
|
-
let receiptStatus;
|
|
675
659
|
if (receipt === undefined) {
|
|
676
|
-
// No receipt: only a legacy layout (emitted install surface + manifest naming this plugin) may go, under --force.
|
|
677
660
|
const manifest = await readManifest(destination);
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
661
|
+
throw new Error(`Refusing to uninstall foreign directory ${destination}: it carries no install receipt naming ${pluginName}` +
|
|
662
|
+
`${manifest === undefined ? " (no loader manifest)" : ` (manifest names ${JSON.stringify(manifest.name)})`}. ` +
|
|
663
|
+
'Remove it manually if it is stale; --force does not apply to foreign directories.');
|
|
664
|
+
}
|
|
665
|
+
if (receipt.plugin !== pluginName) {
|
|
666
|
+
throw new Error(`Refusing to uninstall ${destination}: its install receipt names plugin ${JSON.stringify(receipt.plugin)}, not ` +
|
|
667
|
+
`${JSON.stringify(pluginName)}. Uninstall that plugin from its own bundle instead; --force does not apply.`);
|
|
668
|
+
}
|
|
669
|
+
const installedHash = await hashOwned(destination, receipt.files);
|
|
670
|
+
let receiptStatus = 'consumed';
|
|
671
|
+
if (installedHash !== receipt.contentHash) {
|
|
684
672
|
if (!force) {
|
|
685
|
-
throw new Error(`Refusing to uninstall ${destination}
|
|
686
|
-
|
|
687
|
-
'
|
|
673
|
+
throw new Error(`Refusing to uninstall ${destination}: the owned files hash ${short(installedHash)} but the receipt recorded ` +
|
|
674
|
+
`${short(receipt.contentHash)}, so the installed copy was modified after installation. Re-run with --force to remove the ` +
|
|
675
|
+
'receipt-owned files anyway (unowned entries are never removed).');
|
|
688
676
|
}
|
|
689
|
-
|
|
690
|
-
owned = tree.files;
|
|
691
|
-
ownedDirectories = directoriesOf(tree.files);
|
|
692
|
-
hostDirectories = [];
|
|
693
|
-
receiptStatus = 'forced-legacy';
|
|
694
|
-
} else {
|
|
695
|
-
if (receipt.plugin !== pluginName) {
|
|
696
|
-
throw new Error(`Refusing to uninstall ${destination}: its install receipt names plugin ${JSON.stringify(receipt.plugin)}, not ` +
|
|
697
|
-
`${JSON.stringify(pluginName)}. Uninstall that plugin from its own bundle instead; --force does not apply.`);
|
|
698
|
-
}
|
|
699
|
-
const installedHash = await hashOwned(destination, receipt.files);
|
|
700
|
-
receiptStatus = receipt.migratedFrom === undefined ? 'consumed' : 'migrated';
|
|
701
|
-
if (installedHash !== receipt.contentHash) {
|
|
702
|
-
if (!force) {
|
|
703
|
-
throw new Error(`Refusing to uninstall ${destination}: the owned files hash ${short(installedHash)} but the receipt recorded ` +
|
|
704
|
-
`${short(receipt.contentHash)}, so the installed copy was modified after installation. Re-run with --force to remove the ` +
|
|
705
|
-
'receipt-owned files anyway (unowned entries are never removed).');
|
|
706
|
-
}
|
|
707
|
-
receiptStatus = 'forced-mismatch';
|
|
708
|
-
}
|
|
709
|
-
owned = receipt.files;
|
|
710
|
-
ownedDirectories = receipt.directories;
|
|
711
|
-
hostDirectories = receipt.hostDirectories;
|
|
677
|
+
receiptStatus = 'forced-mismatch';
|
|
712
678
|
}
|
|
679
|
+
const owned = receipt.files;
|
|
680
|
+
const ownedDirectories = receipt.directories;
|
|
681
|
+
const hostDirectories = receipt.hostDirectories;
|
|
713
682
|
// A symlinked ancestor would let a leaf-only delete reach outside the plugin root: refused before any change.
|
|
714
683
|
await assertRealAncestors(destination, owned);
|
|
715
684
|
const files = [];
|
|
@@ -720,13 +689,13 @@ if (uninstall && mode === 'local') {
|
|
|
720
689
|
if (metadata.isSymbolicLink() || !metadata.isFile()) throw unsupported(file);
|
|
721
690
|
files.push(path);
|
|
722
691
|
}
|
|
723
|
-
|
|
724
|
-
const [resolvedStateDirectory,
|
|
692
|
+
files.push(join(destination, receiptFile));
|
|
693
|
+
const [resolvedStateDirectory, resolvedWebDataDirectory] = await runtimeStateRoots();
|
|
725
694
|
const retainedState = [];
|
|
726
695
|
const ownedStatePaths = [];
|
|
727
696
|
const emptyOwnedStateFiles = [];
|
|
728
697
|
const emptyOwnedStateRoots = [];
|
|
729
|
-
if (receipt
|
|
698
|
+
if (receipt.state !== undefined) {
|
|
730
699
|
for (const root of receipt.state.roots) {
|
|
731
700
|
let metadata;
|
|
732
701
|
try { metadata = await lstat(root.root); } catch (error) { if (error?.code === 'ENOENT') continue; throw error; }
|
|
@@ -751,35 +720,25 @@ if (uninstall && mode === 'local') {
|
|
|
751
720
|
ownedStatePaths.push(root.root);
|
|
752
721
|
}
|
|
753
722
|
} else {
|
|
754
|
-
|
|
755
|
-
|
|
723
|
+
// A receipt without a state block was written between the two receipt writes of an install: the observed root
|
|
724
|
+
// is real but unproven, so it is retained until a reinstall records ownership.
|
|
756
725
|
let metadata;
|
|
757
|
-
try { metadata = await lstat(
|
|
758
|
-
if (metadata?.isDirectory()) {
|
|
759
|
-
if (receipt?.stateRoot?.root === fallbackStateDirectory && fallbackStateSource === 'derived') ownedStatePaths.push(fallbackStateDirectory);
|
|
760
|
-
else retainedState.push({ path: fallbackStateDirectory, reason: 'unproven' });
|
|
761
|
-
}
|
|
726
|
+
try { metadata = await lstat(resolvedStateDirectory); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
727
|
+
if (metadata?.isDirectory()) retainedState.push({ path: resolvedStateDirectory, reason: 'unproven' });
|
|
762
728
|
}
|
|
763
|
-
const webDataDirectory = receipt
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
externalDataPaths.push(path);
|
|
729
|
+
const webDataDirectory = receipt.webDataRoot ?? resolvedWebDataDirectory;
|
|
730
|
+
const dataPaths = [...ownedStatePaths];
|
|
731
|
+
let webDataMetadata;
|
|
732
|
+
try { webDataMetadata = await lstat(webDataDirectory); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
733
|
+
if (webDataMetadata !== undefined) {
|
|
734
|
+
if (webDataMetadata.isSymbolicLink() || !webDataMetadata.isDirectory()) throw unsupported(webDataDirectory);
|
|
735
|
+
dataPaths.push(webDataDirectory);
|
|
771
736
|
}
|
|
772
|
-
|
|
773
|
-
try { stateMetadata = await lstat(stateDirectory); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
774
|
-
if (stateMetadata !== undefined && (stateMetadata.isSymbolicLink() || !stateMetadata.isDirectory())) throw unsupported('state');
|
|
775
|
-
// A state/ holding nothing is not durable state: pruned like an installer-created directory instead of kept as a remnant.
|
|
776
|
-
const emptyState = stateMetadata !== undefined && (await readdir(stateDirectory)).length === 0 ? stateDirectory : undefined;
|
|
777
|
-
const dataPaths = [...externalDataPaths, ...(stateMetadata === undefined || emptyState !== undefined ? [] : [stateDirectory])];
|
|
778
|
-
const dataKinds = [...externalDataPaths.map((path) => ownedStatePaths.includes(path) ? `owned framework state root ${path}` : `web-data directory ${path}`), ...(stateMetadata === undefined || emptyState !== undefined ? [] : ['legacy state/ (state kernel, notices journal)'])];
|
|
737
|
+
const dataKinds = dataPaths.map((path) => ownedStatePaths.includes(path) ? `owned framework state root ${path}` : `web-data directory ${path}`);
|
|
779
738
|
// The receipt's cursorExpansion records the PLUGIN_DATA directory this installer created for the copy (spec 9.1). Only
|
|
780
739
|
// the directory at this home's own plugin-data location is receipt-owned; a written one is durable state (kept or
|
|
781
|
-
// purged like state
|
|
782
|
-
const recordedPluginData = receipt
|
|
740
|
+
// purged like an owned state root), an empty one is an installer-created directory that is pruned, a recorded path elsewhere is left alone.
|
|
741
|
+
const recordedPluginData = receipt.cursorExpansion?.pluginData;
|
|
783
742
|
const pluginDataRecorded = recordedPluginData === pluginData;
|
|
784
743
|
let emptyPluginData;
|
|
785
744
|
let foreignNote = "";
|
|
@@ -803,30 +762,29 @@ if (uninstall && mode === 'local') {
|
|
|
803
762
|
const retainedStateNote = retainedState.length === 0 ? '' : ` Retained ${retainedState.map((entry) => `${entry.path} (${entry.reason})`).join(', ')} because the receipt does not prove exclusive ownership.`;
|
|
804
763
|
const dataOutcome = dataPaths.length === 0 ? retainedState.length === 0 ? 'absent' : 'kept' : purgeData ? 'purged' : 'kept';
|
|
805
764
|
const dataDetail = dataPaths.length === 0 && retainedState.length === 0
|
|
806
|
-
? `No durable runtime state exists
|
|
765
|
+
? `No durable runtime state exists${emptyPluginData === undefined ? '' : ` (the installer-created PLUGIN_DATA directory ${emptyPluginData} is empty and is pruned)`}.${foreignNote}`
|
|
807
766
|
: purgeData
|
|
808
767
|
? `${dataPaths.length === 0 ? 'No owned durable runtime state is removed.' : `Durable runtime state — ${dataKinds.join(' and ')} — is removed (--purge-data --confirm-purge).`}${retainedStateNote}${foreignNote}`
|
|
809
768
|
: `Durable runtime state${dataKinds.length === 0 ? '' : ` — ${dataKinds.join(' and ')}`} — is kept; pass --purge-data --confirm-purge to remove owned roots.${retainedStateNote}${foreignNote}`;
|
|
810
769
|
// External state kept by --keep-data needs the remnant receipt and recorded ownership so a later purge
|
|
811
770
|
// removes the same root even though no plugin content remains.
|
|
812
|
-
const keepRoot = !purgeData &&
|
|
771
|
+
const keepRoot = !purgeData && (dataPaths.length > 0 || retainedState.length > 0);
|
|
813
772
|
const directories = [
|
|
814
773
|
...ownedDirectories.map((directory) => join(destination, directory)),
|
|
815
774
|
...(keepRoot ? [] : [destination]),
|
|
816
775
|
...hostDirectories.map((directory) => join(cursorRoot, directory)),
|
|
817
776
|
...(emptyPluginData === undefined ? [] : [emptyPluginData]),
|
|
818
|
-
...(emptyState === undefined ? [] : [emptyState]),
|
|
819
777
|
...(pluginDataRecorded ? [join(cursorRoot, 'agent-bundle', 'plugin-data'), join(cursorRoot, 'agent-bundle')] : []),
|
|
820
778
|
...emptyOwnedStateRoots,
|
|
821
779
|
].sort((left, right) => right.length - left.length || left.localeCompare(right));
|
|
822
780
|
files.push(...emptyOwnedStateFiles);
|
|
823
781
|
const ownedSet = new Set(owned);
|
|
824
782
|
const ownedDirectorySet = new Set(ownedDirectories);
|
|
825
|
-
const remnantOnly = receipt
|
|
783
|
+
const remnantOnly = receipt.files.length === 0 && receipt.registrations.length === 0;
|
|
826
784
|
const purging = purgeData && dataPaths.length > 0;
|
|
827
785
|
// A keep-data rerun over a remnant whose preserved data (or retained unowned entries) are still there is the documented
|
|
828
|
-
// no-op. Once
|
|
829
|
-
// consumes it (receipt, empty plugin root, the host and plugin-data directories it recorded) like an explicit purge would.
|
|
786
|
+
// no-op. Once the recorded roots and the PLUGIN_DATA directory are gone or emptied by hand the remnant guards nothing, and
|
|
787
|
+
// the rerun consumes it (receipt, empty plugin root, the host and plugin-data directories it recorded) like an explicit purge would.
|
|
830
788
|
const remnantGuards = dataPaths.length > 0 || retainedState.length > 0 || (await listRetained(destination, ownedSet, ownedDirectorySet)).length > 0;
|
|
831
789
|
if (remnantOnly && !purgeData && remnantGuards && files.length === 1 && files[0] === join(destination, receiptFile)) {
|
|
832
790
|
// A rerun over what an earlier --keep-data uninstall left behind, still keeping the data: nothing to remove, so the
|
|
@@ -879,9 +837,9 @@ if (uninstall && mode === 'local') {
|
|
|
879
837
|
// created host directories receipt-owned for a later purge and lets Doctor explain the directory; a reinstall fills it in.
|
|
880
838
|
await writeReceiptFile(join(destination, receiptFile), receiptFor({ files: [], hash: createHash('sha256').digest('hex') }, {
|
|
881
839
|
// A kept PLUGIN_DATA directory stays receipt-owned through the remnant's expansion record.
|
|
882
|
-
...(keepRoot && receipt
|
|
883
|
-
directories: [], hostDirectories, installedAt: receipt
|
|
884
|
-
...(receipt
|
|
840
|
+
...(keepRoot && receipt.cursorExpansion !== undefined ? { cursorExpansion: receipt.cursorExpansion } : {}),
|
|
841
|
+
directories: [], hostDirectories, installedAt: receipt.installedAt, registrations: [],
|
|
842
|
+
...(receipt.state === undefined ? {} : { state: receipt.state }),
|
|
885
843
|
...(keepRoot ? { webDataRoot: webDataDirectory } : {}),
|
|
886
844
|
}));
|
|
887
845
|
console.log(`Remnant receipt: ${join(destination, receiptFile)} — owns no files; keeps the created host directories receipt-owned for a later purge.`);
|
|
@@ -900,7 +858,7 @@ if (uninstall && mode === 'marketplace') {
|
|
|
900
858
|
console.log(`Not installed ${pluginName}@${pluginVersion} for cursor (marketplace mode) at ${marketplaceRepo}`);
|
|
901
859
|
process.exit(0);
|
|
902
860
|
}
|
|
903
|
-
let receiptStatus = receipt === undefined ? 'forced-missing' :
|
|
861
|
+
let receiptStatus = receipt === undefined ? 'forced-missing' : 'consumed';
|
|
904
862
|
const recorded = receipt?.registrations.find((registration) => registration.kind === 'cursor-marketplace-staging');
|
|
905
863
|
if (repoExists) {
|
|
906
864
|
if (receipt === undefined) {
|
|
@@ -1191,18 +1149,15 @@ const receipt = await readReceipt(destination);
|
|
|
1191
1149
|
const manifest = await readManifest(destination);
|
|
1192
1150
|
let ownership;
|
|
1193
1151
|
let installedHash;
|
|
1194
|
-
// --uninstall --keep-data leaves a
|
|
1195
|
-
//
|
|
1196
|
-
const
|
|
1197
|
-
const stateOnlyRemnant = receipt === undefined
|
|
1198
|
-
? remnantEntries.length > 0 && remnantEntries.every(isPreservedRoot)
|
|
1199
|
-
: receipt.plugin === pluginName && receipt.files.length === 0 && receipt.registrations.length === 0; // remnant receipt from --uninstall --keep-data
|
|
1152
|
+
// --uninstall --keep-data leaves a remnant receipt owning no files around the preserved durable state: a reinstall
|
|
1153
|
+
// fills the shell back in instead of refusing it.
|
|
1154
|
+
const remnant = receipt !== undefined && receipt.plugin === pluginName && receipt.files.length === 0 && receipt.registrations.length === 0;
|
|
1200
1155
|
if (receipt !== undefined && receipt.plugin === pluginName) {
|
|
1201
1156
|
ownership = 'receipt';
|
|
1202
1157
|
installedHash = await hashOwned(destination, receipt.files);
|
|
1203
1158
|
} else {
|
|
1204
1159
|
installedHash = (await inventory(destination)).hash;
|
|
1205
|
-
ownership =
|
|
1160
|
+
ownership = 'foreign';
|
|
1206
1161
|
}
|
|
1207
1162
|
const installedVersion = manifest?.version ?? (ownership === 'receipt' ? receipt.version : undefined);
|
|
1208
1163
|
const installedName = manifest?.name ?? (ownership === 'receipt' ? receipt.plugin : pluginName);
|
|
@@ -1223,52 +1178,22 @@ if (ownership === 'foreign') {
|
|
|
1223
1178
|
const inventoryMatches = ownership !== 'receipt' ||
|
|
1224
1179
|
(receipt.files.length === artifact.files.length && receipt.files.every((file, index) => file === artifact.files[index]));
|
|
1225
1180
|
if (installedHash === artifact.hash && inventoryMatches) {
|
|
1226
|
-
if (
|
|
1227
|
-
// Byte-identical pre-receipt copy: adoption only writes the receipt (adoption created no directories),
|
|
1228
|
-
// through an exclusively created random sibling so no existing file or link is followed or overwritten.
|
|
1229
|
-
await writeReceiptFile(join(destination, receiptFile), receiptFor(artifact, { directories: [], hostDirectories: [] }));
|
|
1230
|
-
await attachStateOwnership();
|
|
1231
|
-
console.log(`Adopted ${pluginName}@${pluginVersion} at ${destination} (content ${short(artifact.hash)})`);
|
|
1232
|
-
reportExpansion();
|
|
1233
|
-
process.exit(0);
|
|
1234
|
-
}
|
|
1235
|
-
if (ownership === 'receipt' && receipt.migratedFrom !== undefined) {
|
|
1236
|
-
// An identical receipt-managed copy whose receipt predates the current format is upgraded in place:
|
|
1237
|
-
// lifecycle fields exactly as the reader synthesized them, and nothing else changes.
|
|
1238
|
-
await writeReceiptFile(join(destination, receiptFile), receiptFor(artifact, {
|
|
1239
|
-
directories: receipt.directories, hostDirectories: receipt.hostDirectories, installedAt: receipt.installedAt,
|
|
1240
|
-
}));
|
|
1241
|
-
}
|
|
1242
|
-
if (ownership === 'receipt' && receipt.state === undefined) await attachStateOwnership();
|
|
1181
|
+
if (receipt.state === undefined) await attachStateOwnership();
|
|
1243
1182
|
console.log(`Already installed ${pluginName}@${pluginVersion} at ${destination} (content ${short(artifact.hash)})`);
|
|
1244
1183
|
process.exit(0);
|
|
1245
1184
|
}
|
|
1246
1185
|
if (installedVersion !== undefined && installedVersion !== pluginVersion && !replace) {
|
|
1247
1186
|
throw new Error(`Refusing version collision at ${destination}: ${detail}. Re-run with --replace to replace this agent-bundle install.`);
|
|
1248
1187
|
}
|
|
1249
|
-
if (ownership === 'legacy' && !replace && !stateOnlyRemnant) {
|
|
1250
|
-
throw new Error(`Refusing content collision at ${destination}: ${detail}; this copy predates install receipts. ` +
|
|
1251
|
-
'Re-run with --replace once to adopt it; later same-version rebuilds replace automatically.');
|
|
1252
|
-
}
|
|
1253
1188
|
|
|
1254
1189
|
// Owned-files-only replacement: stale owned files leave first, staged files rename over their
|
|
1255
1190
|
// predecessors, and the receipt lands last as the commit marker. Unowned entries (runtime state) stay.
|
|
1256
1191
|
const staged = await stage(artifact);
|
|
1257
1192
|
try {
|
|
1258
|
-
// A legacy copy has no inventory: only files the new artifact also ships count as owned; everything
|
|
1259
|
-
// else (operator files, stale artifact files, runtime state) stays in place and remains unowned.
|
|
1260
1193
|
const incoming = new Set(staged.inventory.files);
|
|
1261
|
-
|
|
1262
|
-
if (ownership === 'receipt') {
|
|
1263
|
-
owned = receipt.files;
|
|
1264
|
-
} else {
|
|
1265
|
-
owned = [];
|
|
1266
|
-
for (const file of (await inventory(destination)).files) {
|
|
1267
|
-
if (await isOwnedEntry(destination, incoming, file)) owned.push(file);
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1194
|
+
const owned = receipt.files;
|
|
1270
1195
|
const ownedSet = new Set(owned);
|
|
1271
|
-
const ownedDirectories = new Set(
|
|
1196
|
+
const ownedDirectories = new Set(receipt.directories);
|
|
1272
1197
|
await assertRealAncestors(destination, owned);
|
|
1273
1198
|
await assertRealAncestors(destination, staged.inventory.files, ownedSet);
|
|
1274
1199
|
// An existing directory at an incoming file path is fine only when it is wholly owned: it and every
|
|
@@ -1318,13 +1243,12 @@ try {
|
|
|
1318
1243
|
// ones this replacement created; the first install time and the host directories it created carry
|
|
1319
1244
|
// over. Finalised in the private staging copy, then committed by rename.
|
|
1320
1245
|
const directories = sortNames(new Set([...[...ownedDirectories].filter((directory) => !pruned.has(directory)), ...created]));
|
|
1321
|
-
const previous = ownership === 'receipt' ? receipt : undefined;
|
|
1322
1246
|
await writeFile(join(staged.root, receiptFile), receiptFor(staged.inventory, {
|
|
1323
|
-
directories, hostDirectories:
|
|
1247
|
+
directories, hostDirectories: receipt.hostDirectories, installedAt: receipt.installedAt,
|
|
1324
1248
|
}), 'utf8');
|
|
1325
1249
|
await rename(join(staged.root, receiptFile), join(destination, receiptFile));
|
|
1326
|
-
await attachStateOwnership(
|
|
1327
|
-
if (
|
|
1250
|
+
await attachStateOwnership(receipt.state);
|
|
1251
|
+
if (remnant) console.log(`Installed ${pluginName}@${pluginVersion} at ${destination} (content ${short(artifact.hash)})`);
|
|
1328
1252
|
else console.log(`Replaced ${pluginName}@${pluginVersion} at ${destination} (content ${short(installedHash)} -> ${short(artifact.hash)})`);
|
|
1329
1253
|
reportExpansion();
|
|
1330
1254
|
} finally {
|