@kisev/agentomatic 11.0.2-dev.101.gae2702b7284e → 11.0.2-dev.103.g258eb838ddb7

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.
@@ -6,6 +6,11 @@ export declare const CONFIG_TARGETS: readonly ["opencode", "kilo", "mimo"];
6
6
  export type ConfigTargetName = (typeof CONFIG_TARGETS)[number];
7
7
  export type TargetFileKind = "main" | "tui";
8
8
  export declare const CONFIG_FRAGMENTS: readonly [{
9
+ readonly name: "core-disable";
10
+ readonly description: "Disconnect only the agentomatic plugin, preserving unrelated configuration";
11
+ readonly targets: readonly ["opencode"];
12
+ readonly file: "main";
13
+ }, {
9
14
  readonly name: "core-plugin";
10
15
  readonly description: "Register @kisev/agentomatic in the plugin array (OpenCode only)";
11
16
  readonly targets: readonly ["opencode"];
@@ -72,9 +77,15 @@ export declare function recoverConfigSetup(scope: Scope, dryRun: boolean, cwd?:
72
77
  }>;
73
78
  export declare function normalizeConfigSelection(scope: Scope, value?: Partial<ConfigSetupSelection>): ConfigSetupSelection;
74
79
  export declare function defaultConfigSelection(scope: Scope, cwd?: string, home?: string): Promise<ConfigSetupSelection>;
75
- export declare function previewConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string, provisionDependency?: boolean): Promise<ConfigSetupPlan>;
80
+ export declare function inspectIntegration(scope: Scope, cwd?: string, home?: string): Promise<{
81
+ path: string;
82
+ connected: boolean;
83
+ problem?: string;
84
+ }>;
85
+ export declare function previewConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string, provisionDependency?: boolean, syncSelection?: boolean): Promise<ConfigSetupPlan>;
76
86
  export declare function applyConfigSetup(selection: ConfigSetupSelection, scope: Scope, cwd?: string, home?: string, options?: {
77
87
  dependencyRunner?: DependencyRunner;
78
88
  provisionDependency?: boolean;
79
89
  receipt?: string;
90
+ syncSelection?: boolean;
80
91
  }): Promise<ConfigSetupPlan>;
@@ -2,13 +2,12 @@ import { randomUUID } from "node:crypto";
2
2
  import { lstat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
- import { archiveMutations } from "./installer.js";
5
+ import { archiveMutations, coreSelectionMutation } from "./installer.js";
6
6
  import { requirePackageVersion } from "./package-metadata.js";
7
7
  import { ensureDependency, planDependency } from "./self-install.js";
8
8
  import { applyTransaction, archiveRoot, deploymentRoot, LifecycleError, lifecycleRoot, inspectTransaction, readRegular, recoverTransaction, sha256, withLifecycleLock, } from "./lifecycle.js";
9
9
  import { applyJsoncEdits, parseJsonc } from "./jsonc.js";
10
- import { corePluginEdits, permissionEdits } from "./opencode-config.js";
11
- const PACKAGE_NAME = "@kisev/agentomatic";
10
+ import { corePluginEdits, corePluginRemovalEdits, permissionEdits, } from "./opencode-config.js";
12
11
  const XDG_STATE_GLOB = "~/.local/state/agent-skills/**";
13
12
  const OPENCODE_SKILLS_GLOB = "~/.config/opencode/skills/**";
14
13
  const AGENTS_SKILLS_GLOB = "~/.agents/skills/**";
@@ -38,6 +37,12 @@ export class ConfigSetupError extends LifecycleError {
38
37
  }
39
38
  export const CONFIG_TARGETS = ["opencode", "kilo", "mimo"];
40
39
  export const CONFIG_FRAGMENTS = [
40
+ {
41
+ name: "core-disable",
42
+ description: "Disconnect only the agentomatic plugin, preserving unrelated configuration",
43
+ targets: ["opencode"],
44
+ file: "main",
45
+ },
41
46
  {
42
47
  name: "core-plugin",
43
48
  description: "Register @kisev/agentomatic in the plugin array (OpenCode only)",
@@ -86,16 +91,43 @@ async function configSnapshot(built) {
86
91
  }
87
92
  async function requireNoRecovery(stateRoot) {
88
93
  if (await readRegular(join(stateRoot, "transaction-journal.json")))
89
- throw new ConfigSetupError("recovery_required", "An interrupted transaction requires config recover --dry-run, then confirmed config recover before a fresh preview");
94
+ throw new ConfigSetupError("recovery_required", "An interrupted transaction requires maintenance recover --dry-run, then confirmed maintenance recover before a fresh preview");
90
95
  }
91
96
  export async function recoverConfigSetup(scope, dryRun, cwd = process.cwd(), home = homedir(), expectedDigest) {
92
97
  const root = deploymentRoot(scope, cwd, home);
93
98
  const state = lifecycleRoot(scope, cwd, home);
94
- const built = await build({ targets: [...CONFIG_TARGETS], fragments: [...FRAGMENT_NAMES] }, scope, cwd, home);
95
- const allowed = [...built.allowedRoots, archiveRoot(scope, cwd, home)];
96
- const paths = await inspectTransaction(root, state, allowed);
97
99
  const journalPath = join(state, "transaction-journal.json");
98
100
  const journal = await readRegular(journalPath);
101
+ const scopeRoot = resolve(scope === "global" ? home : cwd);
102
+ let recoveryRoot = root;
103
+ if (journal) {
104
+ let candidate;
105
+ try {
106
+ candidate = JSON.parse(journal.toString("utf8"));
107
+ }
108
+ catch {
109
+ throw new ConfigSetupError("invalid_journal", "Transaction journal is not valid JSON");
110
+ }
111
+ if (candidate.root === scopeRoot && scopeRoot !== root) {
112
+ const prefix = scope === "global" ? ".config/opencode/" : ".opencode/";
113
+ if (!Array.isArray(candidate.operations) ||
114
+ candidate.operations.some((operation) => !operation.root && !operation.path?.startsWith(prefix)))
115
+ throw new ConfigSetupError("invalid_journal", "Cleanup journal contains paths outside the deployment");
116
+ recoveryRoot = scopeRoot;
117
+ }
118
+ }
119
+ const allowed = [
120
+ root,
121
+ archiveRoot(scope, cwd, home),
122
+ ...(scope === "project"
123
+ ? [resolve(cwd)]
124
+ : [
125
+ join(home, ".config", "opencode"),
126
+ join(home, ".config", "kilo"),
127
+ join(home, ".config", "mimocode"),
128
+ ]),
129
+ ];
130
+ const paths = await inspectTransaction(recoveryRoot, state, allowed);
99
131
  const digest = journal ? sha256(journal) : null;
100
132
  if (dryRun || !paths.length)
101
133
  return { paths, recovered: false, digest };
@@ -103,7 +135,7 @@ export async function recoverConfigSetup(scope, dryRun, cwd = process.cwd(), hom
103
135
  const current = await readRegular(journalPath);
104
136
  if (!current || !expectedDigest || sha256(current) !== expectedDigest)
105
137
  throw new ConfigSetupError("stale_receipt", "Recovery journal changed; preview recovery again");
106
- return recoverTransaction(root, state, allowed);
138
+ return recoverTransaction(recoveryRoot, state, allowed);
107
139
  });
108
140
  return { paths, recovered, digest };
109
141
  }
@@ -168,6 +200,8 @@ function fragmentEdits(fragment, target, current) {
168
200
  if (fragment === "core-plugin") {
169
201
  return corePluginEdits(value);
170
202
  }
203
+ if (fragment === "core-disable")
204
+ return corePluginRemovalEdits(value);
171
205
  if (fragment === "skills-state-permissions") {
172
206
  const statePaths = target === "opencode"
173
207
  ? [XDG_STATE_GLOB, OPENCODE_SKILLS_GLOB, AGENTS_SKILLS_GLOB]
@@ -269,6 +303,8 @@ export function normalizeConfigSelection(scope, value = {}) {
269
303
  if (scope === "project" && (targets.length !== 1 || targets[0] !== "opencode"))
270
304
  throw new ConfigSetupError("invalid_selection", "Project scope supports only the opencode target");
271
305
  const requestedFragments = value.fragments ?? [];
306
+ if (requestedFragments.includes("core-plugin") && requestedFragments.includes("core-disable"))
307
+ throw new ConfigSetupError("invalid_selection", "Choose either core-plugin or core-disable");
272
308
  const fragments = FRAGMENT_NAMES.filter((fragment) => requestedFragments.includes(fragment));
273
309
  if (fragments.length !== requestedFragments.length)
274
310
  throw new ConfigSetupError("invalid_selection", "Unknown config fragment selection");
@@ -289,9 +325,26 @@ export async function defaultConfigSelection(scope, cwd = process.cwd(), home =
289
325
  }
290
326
  return normalizeConfigSelection(scope, {
291
327
  targets: [...new Set(targets)],
292
- fragments: [...FRAGMENT_NAMES],
328
+ fragments: FRAGMENT_NAMES.filter((name) => name !== "core-disable"),
293
329
  });
294
330
  }
331
+ export async function inspectIntegration(scope, cwd = process.cwd(), home = homedir()) {
332
+ const file = await resolveTargetFile("opencode", scope, cwd, home);
333
+ const raw = await readRegular(file.absolute);
334
+ if (!raw)
335
+ return { path: file.absolute, connected: false };
336
+ try {
337
+ const current = parseJsonc(raw.toString("utf8"));
338
+ return { path: file.absolute, connected: corePluginRemovalEdits(current).length > 0 };
339
+ }
340
+ catch (error) {
341
+ return {
342
+ path: file.absolute,
343
+ connected: false,
344
+ problem: error instanceof Error ? error.message : "Invalid configuration",
345
+ };
346
+ }
347
+ }
295
348
  async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
296
349
  const existing = async (root, path) => Boolean(await readRegular(join(root, path)).catch(() => undefined));
297
350
  const globalRoot = deploymentRoot("global", cwd, home);
@@ -376,7 +429,7 @@ async function resolveTargetFile(target, scope, cwd, home, kind = "main") {
376
429
  exists: false,
377
430
  };
378
431
  }
379
- async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
432
+ async function build(selection, scope, cwd = process.cwd(), home = homedir(), syncSelection = true) {
380
433
  const root = deploymentRoot(scope, cwd, home);
381
434
  const operations = [];
382
435
  const mutations = [];
@@ -492,6 +545,23 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
492
545
  ? "TUI files are global-only"
493
546
  : "no selected target supports this fragment",
494
547
  }));
548
+ const core = selection.fragments.find((fragment) => fragment === "core-plugin" || fragment === "core-disable");
549
+ if (syncSelection &&
550
+ core &&
551
+ selection.targets.includes("opencode") &&
552
+ !operations.some((operation) => operation.fragment === core && operation.operation === "conflict")) {
553
+ const mutation = await coreSelectionMutation(root, core === "core-plugin");
554
+ if (mutation) {
555
+ mutations.push(mutation);
556
+ operations.push({
557
+ target: "opencode",
558
+ path: join(root, mutation.path),
559
+ fragment: core,
560
+ operation: "update",
561
+ reason: "Save connection choice for repeat install and repair",
562
+ });
563
+ }
564
+ }
495
565
  const sorted = operations.sort((left, right) => CONFIG_TARGETS.indexOf(left.target) - CONFIG_TARGETS.indexOf(right.target) ||
496
566
  FRAGMENT_NAMES.indexOf(left.fragment) -
497
567
  FRAGMENT_NAMES.indexOf(right.fragment) ||
@@ -519,11 +589,11 @@ async function build(selection, scope, cwd = process.cwd(), home = homedir()) {
519
589
  allowedRoots: [...allowedRoots],
520
590
  };
521
591
  }
522
- export async function previewConfigSetup(selection, scope, cwd = process.cwd(), home = homedir(), provisionDependency = true) {
592
+ export async function previewConfigSetup(selection, scope, cwd = process.cwd(), home = homedir(), provisionDependency = true, syncSelection = true) {
523
593
  const stateRoot = lifecycleRoot(scope, cwd, home);
524
594
  try {
525
595
  await requireNoRecovery(stateRoot);
526
- const built = await build(selection, scope, cwd, home);
596
+ const built = await build(selection, scope, cwd, home, syncSelection);
527
597
  if (provisionDependency &&
528
598
  selection.fragments.includes("core-plugin") &&
529
599
  selection.targets.includes("opencode"))
@@ -535,6 +605,7 @@ export async function previewConfigSetup(selection, scope, cwd = process.cwd(),
535
605
  resolve(cwd),
536
606
  resolve(home),
537
607
  provisionDependency,
608
+ syncSelection,
538
609
  ]);
539
610
  for (const [id, prior] of configReceipts)
540
611
  if (prior.key === key || prior.expires < Date.now())
@@ -560,7 +631,8 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
560
631
  try {
561
632
  return await withLifecycleLock(stateRoot, async () => {
562
633
  await requireNoRecovery(stateRoot);
563
- const built = await build(selection, scope, cwd, home);
634
+ const syncSelection = options.syncSelection !== false;
635
+ const built = await build(selection, scope, cwd, home, syncSelection);
564
636
  const provisionDependency = options.provisionDependency !== false;
565
637
  if (provisionDependency &&
566
638
  selection.fragments.includes("core-plugin") &&
@@ -572,6 +644,7 @@ export async function applyConfigSetup(selection, scope, cwd = process.cwd(), ho
572
644
  resolve(cwd),
573
645
  resolve(home),
574
646
  provisionDependency,
647
+ syncSelection,
575
648
  ]);
576
649
  const id = options.receipt ?? [...configReceipts].find(([, entry]) => entry.key === key)?.[0];
577
650
  const receipt = id ? configReceipts.get(id) : undefined;
package/dist/doctor.js CHANGED
@@ -447,7 +447,9 @@ export async function collectDoctorFacts(scope, cwd = process.cwd(), home = home
447
447
  collisions: inventory?.collisions.length ?? 0,
448
448
  drift: inventory?.drift.length ?? 0,
449
449
  user_owned: inventory?.user_owned.length ?? 0,
450
- }, ["Review collisions and run agent reconcile only after explicit confirmation."]));
450
+ }, [
451
+ "Resolve ownership conflicts and restore modified bytes; preview maintenance repair for missing selected files.",
452
+ ]));
451
453
  const config = await localConfig(scope, project, homeRoot);
452
454
  if (config.incomplete)
453
455
  partial.push("config.local");
@@ -1,3 +1,4 @@
1
+ import { type AgentProfileRequest } from "./agent-profiles.js";
1
2
  import { LifecycleError, type FileMutation, type Scope, type TransactionOptions } from "./lifecycle.js";
2
3
  import { CATALOG } from "./catalog.js";
3
4
  import { type FixedAgentRole } from "./agent-profiles.js";
@@ -30,6 +31,7 @@ export type Plan = {
30
31
  applied?: "changed" | "unchanged" | "skipped";
31
32
  };
32
33
  requires_restart: boolean;
34
+ digest: string;
33
35
  };
34
36
  export type ArchiveCandidate = {
35
37
  path: string;
@@ -44,12 +46,15 @@ export type ArchiveCandidate = {
44
46
  export declare const SELECTABLE_PLUGINS: SelectablePlugin[];
45
47
  export declare const SKILL_COMMANDS: string[];
46
48
  export declare function defaultSelection(): InstallerSelection;
49
+ export declare function installedSelection(scope: Scope, cwd?: string, home?: string): Promise<InstallerSelection | null>;
50
+ export declare function coreSelectionMutation(root: string, enabled: boolean): Promise<FileMutation | undefined>;
47
51
  export declare function normalizeSelection(value?: Partial<InstallerSelection>): InstallerSelection;
48
52
  export declare class InstallerError extends LifecycleError {
49
53
  }
50
54
  export declare function archiveMutations(candidates: readonly ArchiveCandidate[], scope: Scope, cwd: string, home: string, sourceVersion: string, timestamp?: string): Promise<FileMutation[]>;
51
- export declare function preview(action: Action, scope: Scope, cwd?: string, home?: string, selection?: Partial<InstallerSelection>, provisionDependency?: boolean): Promise<Plan>;
55
+ export declare function preview(action: Action, scope: Scope, cwd?: string, home?: string, selection?: Partial<InstallerSelection>, provisionDependency?: boolean, profileChanges?: AgentProfileRequest[]): Promise<Plan>;
52
56
  export declare function apply(action: Action, scope: Scope, cwd?: string, home?: string, options?: TransactionOptions & {
53
57
  dependencyRunner?: DependencyRunner;
54
- }, selection?: Partial<InstallerSelection>, provisionDependency?: boolean): Promise<Plan>;
58
+ expectedDigest?: string;
59
+ }, selection?: Partial<InstallerSelection>, provisionDependency?: boolean, profileChanges?: AgentProfileRequest[]): Promise<Plan>;
55
60
  export declare function result(plan: Plan, applied: boolean): string;
package/dist/installer.js CHANGED
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { buildAgentProfilePlan, listAgentProfiles, validateBuiltAgentProfilePlan, } from "./agent-profiles.js";
7
- import { applyTransaction, archiveRoot, deploymentRoot, destination, LifecycleError, lifecycleRoot, migrateLegacyDeploymentNamespace, migrateLegacyNamespaces, readRegular, recoverTransaction, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
7
+ import { applyTransaction, archiveRoot, deploymentRoot, destination, LifecycleError, lifecycleRoot, migrateLegacyDeploymentNamespace, migrateLegacyNamespaces, readRegular, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
8
8
  import { CATALOG } from "./catalog.js";
9
9
  import { FIXED_AGENT_ROLES } from "./agent-profiles.js";
10
10
  import { readPackageVersion } from "./package-metadata.js";
@@ -33,6 +33,29 @@ export function defaultSelection() {
33
33
  core_activation: true,
34
34
  };
35
35
  }
36
+ export async function installedSelection(scope, cwd = process.cwd(), home = homedir()) {
37
+ const { manifest } = await currentManifest(deploymentRoot(scope, cwd, home));
38
+ return manifest
39
+ ? normalizeSelection({
40
+ commands: manifest.commands,
41
+ agents: manifest.agents,
42
+ plugins: manifest.plugins,
43
+ core_activation: manifest.core_activation,
44
+ })
45
+ : null;
46
+ }
47
+ export async function coreSelectionMutation(root, enabled) {
48
+ const owned = await currentManifest(root);
49
+ if (!owned.manifest || !owned.raw || owned.manifest.core_activation === enabled)
50
+ return undefined;
51
+ return {
52
+ path: MANIFEST_NAME,
53
+ operation: "write",
54
+ content: Buffer.from(`${stable({ ...owned.manifest, core_activation: enabled })}\n`),
55
+ mode: 0o600,
56
+ expected: { sha256: sha256(owned.raw) },
57
+ };
58
+ }
36
59
  export function normalizeSelection(value = {}) {
37
60
  const allCommands = new Set(SKILL_COMMANDS);
38
61
  const commands = [...new Set(value.commands ?? defaultSelection().commands)].sort();
@@ -325,7 +348,7 @@ function asLegacy(manifest) {
325
348
  ? manifest
326
349
  : undefined;
327
350
  }
328
- async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection, provisionDependency = true) {
351
+ async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection, provisionDependency = true, profileChanges = []) {
329
352
  const root = deploymentRoot(scope, cwd, home);
330
353
  const owned = await currentManifest(root);
331
354
  const selection = normalizeSelection(requestedSelection ??
@@ -346,7 +369,7 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
346
369
  manifestSha256: sha256(owned.raw),
347
370
  }
348
371
  : undefined;
349
- const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord, selection.agents);
372
+ const profiles = await buildAgentProfilePlan({ action, changes: profileChanges }, scope, cwd, home, legacyRecord, selection.agents);
350
373
  const dependency = action === "install" && provisionDependency && selection.core_activation
351
374
  ? planDependency(scope, cwd, home)
352
375
  : undefined;
@@ -628,24 +651,23 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
628
651
  item.path.startsWith("plugins/")),
629
652
  };
630
653
  return {
631
- plan: { ...base },
654
+ plan: {
655
+ ...base,
656
+ digest: sha256(stable({ base, mutations, manifest: owned.raw, profiles: profiles.plan.digest })),
657
+ },
632
658
  mutations,
633
659
  expectedManifest: manifestContent,
634
660
  profiles,
635
661
  };
636
662
  }
637
- export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection, provisionDependency = true) {
638
- // Infrastructure normalization like stale-lock reclamation: idempotent,
639
- // preserves every legacy byte, and never touches user configuration.
640
- await migrateLegacyNamespaces(home);
663
+ export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection, provisionDependency = true, profileChanges = []) {
664
+ // Preview never migrates namespaces, creates locks, or recovers transactions.
641
665
  const stateRoot = lifecycleRoot(scope, cwd, home);
642
- const root = deploymentRoot(scope, cwd, home);
643
666
  try {
644
- return await withLifecycleLock(stateRoot, async () => {
645
- if (await recoverTransaction(root, stateRoot, [archiveRoot(scope, cwd, home)]))
646
- throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
647
- return (await build(action, scope, cwd, home, selection, provisionDependency)).plan;
648
- });
667
+ if (await readRegular(join(stateRoot, "transaction-journal.json")))
668
+ throw new InstallerError("recovery_required", "Run maintenance recover before a fresh preview");
669
+ return (await build(action, scope, cwd, home, selection, provisionDependency, profileChanges))
670
+ .plan;
649
671
  }
650
672
  catch (error) {
651
673
  if (error instanceof InstallerError)
@@ -655,7 +677,7 @@ export async function preview(action, scope, cwd = process.cwd(), home = homedir
655
677
  throw error;
656
678
  }
657
679
  }
658
- export async function apply(action, scope, cwd = process.cwd(), home = homedir(), options = {}, selection, provisionDependency = true) {
680
+ export async function apply(action, scope, cwd = process.cwd(), home = homedir(), options = {}, selection, provisionDependency = true, profileChanges = []) {
659
681
  const { dependencyRunner, ...transactionOptions } = options;
660
682
  const stateRoot = lifecycleRoot(scope, cwd, home);
661
683
  const root = deploymentRoot(scope, cwd, home);
@@ -663,9 +685,11 @@ export async function apply(action, scope, cwd = process.cwd(), home = homedir()
663
685
  try {
664
686
  return await withLifecycleLock(stateRoot, async () => {
665
687
  await migrateLegacyDeploymentNamespace(root);
666
- if (await recoverTransaction(root, stateRoot, [archiveRoot(scope, cwd, home)]))
667
- throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
668
- const built = await build(action, scope, cwd, home, selection, provisionDependency);
688
+ if (await readRegular(join(stateRoot, "transaction-journal.json")))
689
+ throw new InstallerError("recovery_required", "Run maintenance recover before a fresh preview");
690
+ const built = await build(action, scope, cwd, home, selection, provisionDependency, profileChanges);
691
+ if (options.expectedDigest && built.plan.digest !== options.expectedDigest)
692
+ throw new InstallerError("stale_plan", "Installation changed; request a fresh preview");
669
693
  if (built.plan.operations.some((item) => item.operation === "conflict" &&
670
694
  (item.reason === "unmanaged_file" ||
671
695
  item.reason === "v1.0.0_agent_ownership_mismatch" ||
@@ -689,7 +713,7 @@ export async function apply(action, scope, cwd = process.cwd(), home = homedir()
689
713
  inventory.drift.length ||
690
714
  inventory.profiles
691
715
  .filter((item) => item.ownership !== "user-owned")
692
- .some((item) => item.state !== "current"))) {
716
+ .some((item) => item.state !== "current" && item.state !== "not-installed"))) {
693
717
  throw new InstallerError("final_validation_failed", "Final installed agent inventory is invalid");
694
718
  }
695
719
  },
@@ -6,4 +6,5 @@ export type PermissionRule = {
6
6
  };
7
7
  export declare function permissionEdits(config: Record<string, unknown>, additions: PermissionRule[]): JsoncEdit[];
8
8
  export declare function matchesWildcard(pattern: string, value: string): boolean;
9
+ export declare function corePluginRemovalEdits(config: Record<string, unknown>): JsoncEdit[];
9
10
  export declare function corePluginEdits(config: Record<string, unknown>): JsoncEdit[];
@@ -122,6 +122,18 @@ function ourEntry(entry) {
122
122
  return OUR_PACKAGE.test(entry);
123
123
  return object(entry) && typeof entry.package === "string" && OUR_PACKAGE.test(entry.package);
124
124
  }
125
+ export function corePluginRemovalEdits(config) {
126
+ return ["plugin", "plugins"].flatMap((key) => {
127
+ if (!(key in config))
128
+ return [];
129
+ plugins(config[key], key === "plugin");
130
+ const entries = config[key];
131
+ const remaining = entries.filter((entry) => !ourEntry(Array.isArray(entry) ? entry[0] : entry));
132
+ return remaining.length === entries.length
133
+ ? []
134
+ : [{ kind: "set-value", path: [key], value: remaining }];
135
+ });
136
+ }
125
137
  export function corePluginEdits(config) {
126
138
  const edits = [
127
139
  { kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/config.json" },
@@ -27,13 +27,16 @@ export type ReconcilePlan = {
27
27
  sha256?: string;
28
28
  }>;
29
29
  confirmable: boolean;
30
+ digest: string;
30
31
  };
31
32
  export type ReconcileResult = {
32
33
  status: "ok";
33
34
  applied: true;
34
35
  plan: ReconcilePlan;
35
36
  };
36
- export type ReconcileOptions = TransactionOptions;
37
+ export type ReconcileOptions = TransactionOptions & {
38
+ expectedDigest?: string;
39
+ };
37
40
  export declare class ReconcileError extends LifecycleError {
38
41
  }
39
42
  export declare function previewReconcile(scope: Scope, cwd?: string, home?: string): Promise<ReconcilePlan>;
package/dist/reconcile.js CHANGED
@@ -3,7 +3,7 @@ import { lstat, readdir, readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { applyTransaction, archiveRoot, assertSafePath, destination, LifecycleError, lifecycleRoot, recoverTransaction, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
6
+ import { applyTransaction, assertSafePath, destination, LifecycleError, lifecycleRoot, readRegular, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
7
7
  import { archiveMutations } from "./installer.js";
8
8
  const PACKAGE_NAME = "@kisev/agentomatic";
9
9
  const GENERIC_MANIFEST = ".agentomatic-manifest.json";
@@ -35,9 +35,6 @@ function loadInventory() {
35
35
  function scopeRoot(scope, cwd, home) {
36
36
  return scope === "global" ? resolve(home) : resolve(cwd);
37
37
  }
38
- function reconcileAllowedRoots(scope, cwd, home) {
39
- return [archiveRoot(scope, cwd, home)];
40
- }
41
38
  function relativePath(root, target) {
42
39
  const value = relative(resolve(root), resolve(target));
43
40
  if (!value || value.startsWith(`..${sep}`) || value === "..")
@@ -398,22 +395,19 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
398
395
  groups["modified-managed"].length === 0 &&
399
396
  groups.conflict.length === 0;
400
397
  return {
401
- plan: { ...base, confirmable },
398
+ plan: { ...base, confirmable, digest: sha256(stable({ base, mutations })) },
402
399
  mutations,
403
400
  };
404
401
  }
405
402
  export async function previewReconcile(scope, cwd = process.cwd(), home = homedir()) {
406
403
  const stateRoot = lifecycleRoot(scope, cwd, home);
407
- const root = scopeRoot(scope, cwd, home);
408
404
  try {
409
- return await withLifecycleLock(stateRoot, async () => {
410
- if (await recoverTransaction(root, stateRoot, reconcileAllowedRoots(scope, cwd, home)))
411
- throw new ReconcileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
412
- const built = await build(scope, cwd, home);
413
- const blocked = built.plan.modified_managed.length > 0 || built.plan.conflicts.length > 0;
414
- const actionable = built.plan.operations.length > 0;
415
- return { ...built.plan, confirmable: !blocked && actionable };
416
- });
405
+ if (await readRegular(join(stateRoot, "transaction-journal.json")))
406
+ throw new ReconcileError("recovery_required", "Run maintenance recover before a fresh preview");
407
+ const built = await build(scope, cwd, home);
408
+ const blocked = built.plan.modified_managed.length > 0 || built.plan.conflicts.length > 0;
409
+ const actionable = built.plan.operations.length > 0;
410
+ return { ...built.plan, confirmable: !blocked && actionable };
417
411
  }
418
412
  catch (error) {
419
413
  if (error instanceof ReconcileError)
@@ -432,9 +426,11 @@ export async function applyReconcile(scope, cwd = process.cwd(), home = homedir(
432
426
  const root = scopeRoot(scope, cwd, home);
433
427
  try {
434
428
  return await withLifecycleLock(stateRoot, async () => {
435
- if (await recoverTransaction(root, stateRoot, reconcileAllowedRoots(scope, cwd, home)))
436
- throw new ReconcileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
429
+ if (await readRegular(join(stateRoot, "transaction-journal.json")))
430
+ throw new ReconcileError("recovery_required", "Run maintenance recover before a fresh preview");
437
431
  const built = await build(scope, cwd, home);
432
+ if (options.expectedDigest && options.expectedDigest !== built.plan.digest)
433
+ throw new ReconcileError("stale_plan", "Cleanup sources changed; request a fresh preview");
438
434
  if (built.plan.conflicts.length || built.plan.modified_managed.length)
439
435
  throw new ReconcileError("conflict", "Reconcile contains unsafe ownership conflicts");
440
436
  await applyTransaction(root, stateRoot, built.mutations, {
@@ -24,3 +24,10 @@ export declare function planDependency(scope: "project" | "global", cwd?: string
24
24
  export declare function ensureDependency(plan: DependencyPlan, runner?: DependencyRunner): Promise<DependencyPlan & {
25
25
  applied: "changed" | "unchanged" | "skipped";
26
26
  }>;
27
+ export type DependencyRemovalPlan = {
28
+ dir: string | null;
29
+ names: string[];
30
+ sources: Record<string, string | null>;
31
+ };
32
+ export declare function previewDependencyRemoval(scope: "project" | "global", cwd?: string, home?: string): Promise<DependencyRemovalPlan>;
33
+ export declare function removeDependency(plan: DependencyRemovalPlan, runner?: DependencyRunner): Promise<void>;
@@ -3,7 +3,8 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
- import { LifecycleError, writeAtomic } from "@kisev/safe-fs";
6
+ import { LifecycleError, writeAtomic, readRegular } from "@kisev/safe-fs";
7
+ import { sha256 } from "./lifecycle.js";
7
8
  import { requirePackageVersion } from "./package-metadata.js";
8
9
  const execFileAsync = promisify(execFile);
9
10
  export const SELF_PACKAGE_NAME = "@kisev/agentomatic";
@@ -127,3 +128,53 @@ export async function ensureDependency(plan, runner = execFileAsync) {
127
128
  }
128
129
  return { ...plan, applied: "changed", ...(removedLegacy ? { removed_legacy: true } : {}) };
129
130
  }
131
+ export async function previewDependencyRemoval(scope, cwd = process.cwd(), home = homedir()) {
132
+ const dir = owningProjectDir(scope, cwd, home);
133
+ if (!dir)
134
+ return { dir, names: [], sources: {} };
135
+ const sources = {};
136
+ for (const name of ["package.json", "package-lock.json"]) {
137
+ const raw = await readRegular(join(dir, name));
138
+ sources[name] = raw ? sha256(raw) : null;
139
+ }
140
+ const raw = await readRegular(join(dir, "package.json"));
141
+ if (!raw)
142
+ return { dir, names: [], sources };
143
+ let parsed;
144
+ try {
145
+ parsed = JSON.parse(raw.toString("utf8"));
146
+ }
147
+ catch {
148
+ throw new SelfInstallError("invalid_configuration", "Cannot inspect dependency removal: invalid package.json");
149
+ }
150
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
151
+ throw new SelfInstallError("invalid_configuration", "Cannot inspect dependency removal: package.json must be an object");
152
+ const names = [SELF_PACKAGE_NAME, LEGACY_SELF_PACKAGE_NAME].filter((name) => ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].some((key) => {
153
+ const section = parsed[key];
154
+ return section && typeof section === "object" && name in section;
155
+ }));
156
+ return { dir, names, sources };
157
+ }
158
+ export async function removeDependency(plan, runner = execFileAsync) {
159
+ if (!plan.dir || !plan.names.length)
160
+ return;
161
+ for (const [name, expected] of Object.entries(plan.sources)) {
162
+ const raw = await readRegular(join(plan.dir, name));
163
+ if ((raw ? sha256(raw) : null) !== expected)
164
+ throw new SelfInstallError("stale_plan", "npm files changed; preview uninstall again");
165
+ }
166
+ try {
167
+ await runner("npm", ["rm", ...plan.names, "--no-audit", "--no-fund"], {
168
+ cwd: plan.dir,
169
+ timeout: NPM_INSTALL_TIMEOUT_MS,
170
+ });
171
+ const raw = await readRegular(join(plan.dir, "package.json"));
172
+ const parsed = raw ? JSON.parse(raw.toString("utf8")) : undefined;
173
+ if (!parsed ||
174
+ plan.names.some((name) => ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].some((key) => parsed[key] && name in parsed[key])))
175
+ throw new Error("npm did not remove the selected dependencies");
176
+ }
177
+ catch {
178
+ throw new SelfInstallError("npm_dependency_failed", "npm removal failed after local stages; inspect package.json and retry uninstall --remove-dependency");
179
+ }
180
+ }
@@ -2,4 +2,4 @@ import type { Readable, Writable } from "node:stream";
2
2
  export declare function selectOption(label: string, options: readonly string[], stdin?: Readable, stderr?: Writable, initial?: number): Promise<number | null>;
3
3
  export declare function selectOptions(label: string, options: readonly string[], initialSelected?: readonly string[], stdin?: Readable, stderr?: Writable): Promise<string[] | null>;
4
4
  export declare function promptText(label: string, stdin?: Readable, stderr?: Writable): Promise<string | null>;
5
- export declare function confirmQuestion(label: string, stdin?: Readable, stderr?: Writable): Promise<boolean | null>;
5
+ export declare function confirmQuestion(label: string, stdin?: Readable, stderr?: Writable, initialValue?: boolean): Promise<boolean | null>;
@@ -55,9 +55,9 @@ export async function promptText(label, stdin = process.stdin, stderr = process.
55
55
  const trimmed = value.trim();
56
56
  return trimmed.length ? trimmed : null;
57
57
  }
58
- export async function confirmQuestion(label, stdin = process.stdin, stderr = process.stderr) {
58
+ export async function confirmQuestion(label, stdin = process.stdin, stderr = process.stderr, initialValue = true) {
59
59
  interactiveStreams(stdin, stderr);
60
- const value = await clack.confirm({ message: label, input: stdin, output: stderr });
60
+ const value = await clack.confirm({ message: label, input: stdin, output: stderr, initialValue });
61
61
  if (clack.isCancel(value))
62
62
  return null;
63
63
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kisev/agentomatic",
3
- "version": "11.0.2-dev.101.gae2702b7284e",
3
+ "version": "11.0.2-dev.103.g258eb838ddb7",
4
4
  "description": "OpenCode integration and opt-in installer for portable Agent Skills.",
5
5
  "homepage": "https://github.com/kisev/skills#readme",
6
6
  "bugs": {
@@ -40,13 +40,13 @@
40
40
  },
41
41
  "scripts": {
42
42
  "build": "node scripts/clean-dist.mjs && tsc --project tsconfig.json && node dist/generate-assets.js",
43
- "test": "node --test test/agent-profiles.test.mjs test/config-setup.test.mjs test/legacy-migration.test.mjs test/doctor.test.mjs test/package.test.mjs test/rtk.test.mjs test/self-install.test.mjs test/stage19.test.mjs test/surface-coverage.test.mjs test/opencode-compatibility.test.mjs",
43
+ "test": "node --test test/agent-profiles.test.mjs test/command-cli.test.mjs test/config-setup.test.mjs test/legacy-migration.test.mjs test/doctor.test.mjs test/package.test.mjs test/rtk.test.mjs test/self-install.test.mjs test/stage19.test.mjs test/surface-coverage.test.mjs test/opencode-compatibility.test.mjs",
44
44
  "smoke": "node test/smoke.mjs",
45
45
  "pack:check": "node test/pack-allowlist.mjs"
46
46
  },
47
47
  "dependencies": {
48
48
  "@clack/prompts": "^1.8.1",
49
- "@kisev/safe-fs": "1.0.0-dev.101.gae2702b7284e",
49
+ "@kisev/safe-fs": "1.0.0-dev.103.g258eb838ddb7",
50
50
  "commander": "15.0.0",
51
51
  "zod": "4.1.8"
52
52
  },