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/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.8.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|--force] [--help]\n node install.mjs --uninstall [--mode local|marketplace] [--keep-data | --purge-data --confirm-purge] [--force] [--plan]';
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' || argument === '--force') { replace = true; force = argument === '--force'; continue; }
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 reads as absent. A format/1 receipt (#420)
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 ((value.format !== receiptFormat && value.format !== legacyReceiptFormat) ||
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 neither owned nor runtime state
589
- // (symlinks listed, never followed) plus unowned directories holding nothing retained (`name/`), which the prune never touches.
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 === '' && (name === receiptFile || isPreservedRoot(name))) continue;
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(canonical, 'state'), join(homedir(), '.agent-bundle', 'web-data', `${webName}-${webDigest}`), explicitStateRoot === undefined ? 'derived' : 'native'];
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
- const legacy = manifest?.name === pluginName && await hasMarkers(destination);
679
- if (!legacy) {
680
- throw new Error(`Refusing to uninstall foreign directory ${destination}: it carries no install receipt and is not a ` +
681
- `recognizable agent-bundle install of ${pluginName}${manifest === undefined ? " (no loader manifest)" : ` (manifest names ${JSON.stringify(manifest.name)})`}. ` +
682
- 'Remove it manually if it is stale; --force does not apply to foreign directories.');
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} without an install receipt: this copy predates install receipts, so ownership ` +
686
- 'cannot be proven. Re-run with --force to remove its inventoried plugin files (runtime state under state/ is kept unless ' +
687
- '--purge-data --confirm-purge is passed), or reinstall with --replace first to adopt it.');
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
- const tree = await inventory(destination);
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
- if (await exists(join(destination, receiptFile))) files.push(join(destination, receiptFile));
724
- const [resolvedStateDirectory, stateDirectory, resolvedWebDataDirectory, resolvedStateSource] = await runtimeStateRoots();
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?.state !== undefined) {
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
- const fallbackStateDirectory = receipt?.stateRoot?.root ?? resolvedStateDirectory;
755
- const fallbackStateSource = receipt?.stateRoot?.source ?? resolvedStateSource;
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(fallbackStateDirectory); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
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?.webDataRoot ?? resolvedWebDataDirectory;
764
- const externalDataPaths = [...ownedStatePaths];
765
- for (const path of [webDataDirectory]) {
766
- if (path === stateDirectory) continue;
767
- let metadata;
768
- try { metadata = await lstat(path); } catch (error) { if (error?.code === 'ENOENT') continue; throw error; }
769
- if (metadata.isSymbolicLink() || !metadata.isDirectory()) throw unsupported(path);
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
- let stateMetadata;
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/), an empty one is an installer-created directory that is pruned, a recorded path elsewhere is left alone.
782
- const recordedPluginData = receipt?.cursorExpansion?.pluginData;
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 (${emptyState === undefined ? 'no state/ under the installed plugin root' : 'state/ under the installed plugin root is empty and is pruned'}${emptyPluginData === undefined ? '' : `; the installer-created PLUGIN_DATA directory ${emptyPluginData} is empty and is pruned`}).${foreignNote}`
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 && [...dataPaths, ...retainedState.map((entry) => entry.path)].some((path) => path !== stateDirectory);
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 !== undefined && receipt.files.length === 0 && receipt.registrations.length === 0;
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 state/ and the PLUGIN_DATA directory are gone or emptied by hand the remnant guards nothing, and the rerun
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?.cursorExpansion !== undefined ? { cursorExpansion: receipt.cursorExpansion } : {}),
883
- directories: [], hostDirectories, installedAt: receipt?.installedAt, registrations: [],
884
- ...(receipt?.state === undefined ? {} : { state: receipt.state }),
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' : receipt.migratedFrom === undefined ? 'consumed' : 'migrated';
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 shell holding only state/: a reinstall fills it back in around the preserved
1195
- // durable state instead of refusing it as foreign (nothing in it is anyone's plugin content).
1196
- const remnantEntries = (await readdir(destination)).filter((name) => name !== receiptFile);
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 = stateOnlyRemnant || (receipt === undefined && manifest?.name === pluginName && await hasMarkers(destination)) ? 'legacy' : 'foreign';
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 (ownership === 'legacy' && replace) {
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
- let owned;
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(ownership === 'receipt' ? receipt.directories : []);
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: previous?.hostDirectories ?? [], installedAt: previous?.installedAt,
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(previous?.state);
1327
- if (stateOnlyRemnant) console.log(`Installed ${pluginName}@${pluginVersion} at ${destination} (content ${short(artifact.hash)})`);
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 {