dlw-machine-setup 0.8.10 → 0.9.1

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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/bin/installer.js +961 -182
  3. package/package.json +1 -1
package/bin/installer.js CHANGED
@@ -2753,13 +2753,13 @@ var PromisePolyfill = class extends Promise {
2753
2753
  // Available starting from Node 22
2754
2754
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
2755
2755
  static withResolver() {
2756
- let resolve4;
2756
+ let resolve6;
2757
2757
  let reject;
2758
2758
  const promise = new Promise((res, rej) => {
2759
- resolve4 = res;
2759
+ resolve6 = res;
2760
2760
  reject = rej;
2761
2761
  });
2762
- return { promise, resolve: resolve4, reject };
2762
+ return { promise, resolve: resolve6, reject };
2763
2763
  }
2764
2764
  };
2765
2765
 
@@ -2776,7 +2776,7 @@ function createPrompt(view) {
2776
2776
  output
2777
2777
  });
2778
2778
  const screen = new ScreenManager(rl);
2779
- const { promise, resolve: resolve4, reject } = PromisePolyfill.withResolver();
2779
+ const { promise, resolve: resolve6, reject } = PromisePolyfill.withResolver();
2780
2780
  const cancel = () => reject(new CancelPromptError());
2781
2781
  if (signal) {
2782
2782
  const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
@@ -2800,7 +2800,7 @@ function createPrompt(view) {
2800
2800
  cycle(() => {
2801
2801
  try {
2802
2802
  const nextView = view(config, (value) => {
2803
- setImmediate(() => resolve4(value));
2803
+ setImmediate(() => resolve6(value));
2804
2804
  });
2805
2805
  const [content, bottomContent] = typeof nextView === "string" ? [nextView] : nextView;
2806
2806
  screen.render(content, bottomContent);
@@ -3244,9 +3244,9 @@ ${page}${helpTipBottom}${choiceDescription}${import_ansi_escapes3.default.cursor
3244
3244
  });
3245
3245
 
3246
3246
  // src/index.ts
3247
- var import_fs12 = require("fs");
3248
- var import_readline = require("readline");
3249
- var import_path13 = require("path");
3247
+ var import_fs16 = require("fs");
3248
+ var import_readline2 = require("readline");
3249
+ var import_path16 = require("path");
3250
3250
 
3251
3251
  // src/utils/fetch.ts
3252
3252
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -3301,7 +3301,7 @@ async function fetchWithRetry(url, options = {}, maxRetries = DEFAULT_MAX_RETRIE
3301
3301
  }
3302
3302
  if (attempt < maxRetries) {
3303
3303
  const delay = retryDelayMs * Math.pow(2, attempt - 1);
3304
- await new Promise((resolve4) => setTimeout(resolve4, delay));
3304
+ await new Promise((resolve6) => setTimeout(resolve6, delay));
3305
3305
  }
3306
3306
  }
3307
3307
  throw lastError || new Error("Failed after maximum retries");
@@ -3548,7 +3548,7 @@ async function pollForToken(deviceCode, interval) {
3548
3548
  const maxAttempts = 60;
3549
3549
  let attempts = 0;
3550
3550
  while (attempts < maxAttempts) {
3551
- await new Promise((resolve4) => setTimeout(resolve4, interval * 1e3));
3551
+ await new Promise((resolve6) => setTimeout(resolve6, interval * 1e3));
3552
3552
  const response = await fetchWithTimeout(GITHUB_TOKEN_URL, {
3553
3553
  method: "POST",
3554
3554
  headers: {
@@ -3734,6 +3734,7 @@ async function runPipeline(steps, ctx) {
3734
3734
  entries.push({ name: step.name, label: step.label, result });
3735
3735
  if (result.status === "success") {
3736
3736
  console.log("\u2713");
3737
+ ctx.journal?.recordStep(step.name, result.record);
3737
3738
  } else {
3738
3739
  console.log("\u2717");
3739
3740
  }
@@ -3753,6 +3754,148 @@ async function runPipeline(steps, ctx) {
3753
3754
  }
3754
3755
  return { entries };
3755
3756
  }
3757
+ async function runRollback(steps, ctx) {
3758
+ const entries = [];
3759
+ if (!ctx.journal) {
3760
+ return { entries };
3761
+ }
3762
+ const stepsByName = new Map(steps.map((s) => [s.name, s]));
3763
+ for (const journalEntry of ctx.journal.getEntriesReversed()) {
3764
+ const step = stepsByName.get(journalEntry.step);
3765
+ if (!step) {
3766
+ entries.push({
3767
+ name: journalEntry.step,
3768
+ label: journalEntry.step,
3769
+ result: {
3770
+ status: "skipped",
3771
+ detail: "step not found in this installer build \u2014 installed by a different version?"
3772
+ }
3773
+ });
3774
+ continue;
3775
+ }
3776
+ if (!step.inverse) {
3777
+ entries.push({
3778
+ name: step.name,
3779
+ label: step.label,
3780
+ result: { status: "skipped", detail: "no inverse defined" }
3781
+ });
3782
+ continue;
3783
+ }
3784
+ const label = step.inverse.label ?? `Reversing: ${step.label}`;
3785
+ process.stdout.write(` ${label}... `);
3786
+ try {
3787
+ const result = await step.inverse.execute(journalEntry.record, ctx);
3788
+ entries.push({ name: step.name, label, result });
3789
+ if (result.status === "success") {
3790
+ console.log("\u2713");
3791
+ } else if (result.status === "failed") {
3792
+ console.log("\u2717");
3793
+ } else {
3794
+ console.log("\u2014");
3795
+ }
3796
+ if (result.detail) {
3797
+ console.log(` ${result.detail}`);
3798
+ }
3799
+ } catch (err) {
3800
+ const detail = err instanceof Error ? err.message : String(err);
3801
+ entries.push({
3802
+ name: step.name,
3803
+ label,
3804
+ result: { status: "failed", detail }
3805
+ });
3806
+ console.log("\u2717");
3807
+ console.log(` ${detail}`);
3808
+ }
3809
+ }
3810
+ return { entries };
3811
+ }
3812
+
3813
+ // src/journal.ts
3814
+ var import_fs3 = require("fs");
3815
+ var import_path3 = require("path");
3816
+ var JOURNAL_SCHEMA_VERSION = 1;
3817
+ var Journal = class {
3818
+ filePath;
3819
+ entries = [];
3820
+ /** Top-level summary fields (everything outside `rollback`). Owned by
3821
+ * write-state.ts; the journal just carries them so the on-disk file
3822
+ * stays one coherent JSON document instead of two side-by-side files. */
3823
+ summary = {};
3824
+ constructor(filePath) {
3825
+ this.filePath = filePath;
3826
+ }
3827
+ /** Load existing state from disk. Used by uninstall. Tolerates a missing
3828
+ * or corrupt file — both surface as "no entries to roll back." */
3829
+ load() {
3830
+ this.entries = [];
3831
+ this.summary = {};
3832
+ if (!(0, import_fs3.existsSync)(this.filePath)) return;
3833
+ try {
3834
+ const raw = JSON.parse((0, import_fs3.readFileSync)(this.filePath, "utf-8"));
3835
+ const { rollback, ...summary } = raw;
3836
+ this.summary = summary;
3837
+ if (rollback && typeof rollback === "object") {
3838
+ const section = rollback;
3839
+ if (Array.isArray(section.entries)) {
3840
+ this.entries = section.entries.filter(
3841
+ (e) => !!e && typeof e === "object" && typeof e.step === "string"
3842
+ );
3843
+ }
3844
+ }
3845
+ } catch {
3846
+ }
3847
+ }
3848
+ /** Clear in-memory state and flush an empty file to disk. Used by install
3849
+ * at the start of a run so a previous install's journal never leaks
3850
+ * into a fresh run's rollback list. */
3851
+ startFresh() {
3852
+ this.entries = [];
3853
+ this.summary = {};
3854
+ this.flush();
3855
+ }
3856
+ /** Append a completed step to the journal and flush immediately. The
3857
+ * flush-per-step is intentional: an install that crashes between step 4
3858
+ * and step 5 still leaves a journal that ends at step 4. */
3859
+ recordStep(name, record) {
3860
+ this.entries.push({
3861
+ step: name,
3862
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
3863
+ record
3864
+ });
3865
+ this.flush();
3866
+ }
3867
+ /** Replace the top-level summary fields and flush. Called by
3868
+ * write-state.ts so the existing on-disk shape is preserved. */
3869
+ setSummary(summary) {
3870
+ this.summary = summary;
3871
+ this.flush();
3872
+ }
3873
+ /** Entries in install order (oldest first). */
3874
+ getEntries() {
3875
+ return this.entries;
3876
+ }
3877
+ /** Entries in rollback order (most-recent first). LIFO so each step's
3878
+ * inverse undoes its own changes before any earlier step's state is
3879
+ * touched — matters when later steps depend on earlier ones (e.g.
3880
+ * write-instructions reads _ai-context/ that fetch-contexts populated). */
3881
+ getEntriesReversed() {
3882
+ return [...this.entries].reverse();
3883
+ }
3884
+ /** Persist the current summary + entries to disk as one JSON document.
3885
+ * Summary fields come first so a human opening the file sees the
3886
+ * familiar shape; `rollback` is the last key. */
3887
+ flush() {
3888
+ (0, import_fs3.mkdirSync)((0, import_path3.dirname)(this.filePath), { recursive: true });
3889
+ const out = {
3890
+ ...this.summary,
3891
+ rollback: {
3892
+ schemaVersion: JOURNAL_SCHEMA_VERSION,
3893
+ entries: this.entries
3894
+ }
3895
+ };
3896
+ (0, import_fs3.writeFileSync)(this.filePath, JSON.stringify(out, null, 2), "utf-8");
3897
+ }
3898
+ };
3756
3899
 
3757
3900
  // src/steps/index.ts
3758
3901
  var steps_exports = {};
@@ -3768,19 +3911,149 @@ __export(steps_exports, {
3768
3911
  });
3769
3912
 
3770
3913
  // src/steps/resources/fetch-contexts.ts
3771
- var import_fs4 = require("fs");
3772
- var import_path4 = require("path");
3914
+ var import_fs6 = require("fs");
3915
+ var import_path6 = require("path");
3773
3916
 
3774
3917
  // src/utils/fs-copy.ts
3775
- var import_fs3 = require("fs");
3776
- var import_path3 = require("path");
3918
+ var import_fs4 = require("fs");
3919
+ var import_path4 = require("path");
3777
3920
  function copyDirectory(source, target) {
3778
- if (!(0, import_fs3.existsSync)(target)) (0, import_fs3.mkdirSync)(target, { recursive: true });
3779
- for (const entry of (0, import_fs3.readdirSync)(source, { withFileTypes: true })) {
3780
- const s = (0, import_path3.join)(source, entry.name);
3781
- const t = (0, import_path3.join)(target, entry.name);
3921
+ if (!(0, import_fs4.existsSync)(target)) (0, import_fs4.mkdirSync)(target, { recursive: true });
3922
+ for (const entry of (0, import_fs4.readdirSync)(source, { withFileTypes: true })) {
3923
+ const s = (0, import_path4.join)(source, entry.name);
3924
+ const t = (0, import_path4.join)(target, entry.name);
3782
3925
  if (entry.isDirectory()) copyDirectory(s, t);
3783
- else (0, import_fs3.copyFileSync)(s, t);
3926
+ else (0, import_fs4.copyFileSync)(s, t);
3927
+ }
3928
+ }
3929
+
3930
+ // src/inverse/primitives.ts
3931
+ var import_fs5 = require("fs");
3932
+ var import_path5 = require("path");
3933
+ function deleteFileIfExists(absPath) {
3934
+ if (!(0, import_fs5.existsSync)(absPath)) return false;
3935
+ try {
3936
+ (0, import_fs5.unlinkSync)(absPath);
3937
+ return true;
3938
+ } catch {
3939
+ return false;
3940
+ }
3941
+ }
3942
+ function deleteDirIfExists(absPath) {
3943
+ if (!(0, import_fs5.existsSync)(absPath)) return false;
3944
+ try {
3945
+ (0, import_fs5.rmSync)(absPath, { recursive: true, force: true });
3946
+ return true;
3947
+ } catch {
3948
+ return false;
3949
+ }
3950
+ }
3951
+ function pruneEmptyAncestors(relPaths, projectPath) {
3952
+ const root = (0, import_path5.resolve)(projectPath);
3953
+ const ancestors = /* @__PURE__ */ new Set();
3954
+ for (const rel of relPaths) {
3955
+ let cur = (0, import_path5.resolve)((0, import_path5.join)(root, rel));
3956
+ while (true) {
3957
+ cur = (0, import_path5.dirname)(cur);
3958
+ if (cur === root) break;
3959
+ if (!cur.startsWith(root + import_path5.sep)) break;
3960
+ ancestors.add(cur);
3961
+ }
3962
+ }
3963
+ const sorted = [...ancestors].sort((a, b) => b.length - a.length);
3964
+ const removed = [];
3965
+ for (const abs of sorted) {
3966
+ if (!(0, import_fs5.existsSync)(abs)) continue;
3967
+ try {
3968
+ const entries = (0, import_fs5.readdirSync)(abs);
3969
+ if (entries.length > 0) continue;
3970
+ (0, import_fs5.rmdirSync)(abs);
3971
+ removed.push(abs);
3972
+ } catch {
3973
+ }
3974
+ }
3975
+ return removed;
3976
+ }
3977
+ function readJsonOrNull(absPath) {
3978
+ if (!(0, import_fs5.existsSync)(absPath)) return null;
3979
+ try {
3980
+ return JSON.parse((0, import_fs5.readFileSync)(absPath, "utf-8"));
3981
+ } catch {
3982
+ return null;
3983
+ }
3984
+ }
3985
+ function isPlainObject2(v) {
3986
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3987
+ }
3988
+ var OWNER_KEY = "_bundleOwner";
3989
+ function stripOwnedEntries(value, owner) {
3990
+ if (Array.isArray(value)) {
3991
+ return value.filter((item) => !(isPlainObject2(item) && item[OWNER_KEY] === owner)).map((item) => stripOwnedEntries(item, owner));
3992
+ }
3993
+ if (isPlainObject2(value)) {
3994
+ return Object.fromEntries(
3995
+ Object.entries(value).map(([k, v]) => [k, stripOwnedEntries(v, owner)])
3996
+ );
3997
+ }
3998
+ return value;
3999
+ }
4000
+ function deleteJsonIfEffectivelyEmpty(absPath) {
4001
+ if (!(0, import_fs5.existsSync)(absPath)) return false;
4002
+ const raw = readJsonOrNull(absPath);
4003
+ if (!isPlainObject2(raw)) return false;
4004
+ const keys = Object.keys(raw);
4005
+ if (keys.length === 0) {
4006
+ return deleteFileIfExists(absPath);
4007
+ }
4008
+ if (keys.length === 1) {
4009
+ const v = raw[keys[0]];
4010
+ if (isPlainObject2(v) && Object.keys(v).length === 0) return deleteFileIfExists(absPath);
4011
+ if (Array.isArray(v) && v.length === 0) return deleteFileIfExists(absPath);
4012
+ }
4013
+ return false;
4014
+ }
4015
+ function stripBundleOwnerFromJsonFile(absPath, owner) {
4016
+ const raw = readJsonOrNull(absPath);
4017
+ if (!isPlainObject2(raw)) return false;
4018
+ const stripped = stripOwnedEntries(raw, owner);
4019
+ const before = JSON.stringify(raw);
4020
+ const after = JSON.stringify(stripped);
4021
+ if (before === after) return false;
4022
+ (0, import_fs5.writeFileSync)(absPath, JSON.stringify(stripped, null, 2) + "\n", "utf-8");
4023
+ return true;
4024
+ }
4025
+ function stripMcpServers(absPath, rootKey, serverNames) {
4026
+ const raw = readJsonOrNull(absPath);
4027
+ if (!isPlainObject2(raw)) return { changed: false, rootEmpty: false };
4028
+ const servers = raw[rootKey];
4029
+ if (!isPlainObject2(servers)) return { changed: false, rootEmpty: false };
4030
+ let changed = false;
4031
+ for (const name of serverNames) {
4032
+ if (name in servers) {
4033
+ delete servers[name];
4034
+ changed = true;
4035
+ }
4036
+ }
4037
+ if (!changed) {
4038
+ return { changed: false, rootEmpty: Object.keys(servers).length === 0 };
4039
+ }
4040
+ raw[rootKey] = servers;
4041
+ (0, import_fs5.writeFileSync)(absPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
4042
+ return { changed: true, rootEmpty: Object.keys(servers).length === 0 };
4043
+ }
4044
+ function resolveProjectPath(rel, projectPath) {
4045
+ const base = (0, import_path5.resolve)(projectPath);
4046
+ const full = (0, import_path5.resolve)((0, import_path5.join)(base, rel));
4047
+ if (full !== base && !full.startsWith(base + import_path5.sep)) {
4048
+ throw new Error(`Inverse target escapes project path: ${rel}`);
4049
+ }
4050
+ return full;
4051
+ }
4052
+ function isDirectorySafe(absPath) {
4053
+ try {
4054
+ return (0, import_fs5.statSync)(absPath).isDirectory();
4055
+ } catch {
4056
+ return false;
3784
4057
  }
3785
4058
  }
3786
4059
 
@@ -3798,6 +4071,8 @@ var fetch_contexts_default = defineStep({
3798
4071
  detail: "Context repo not found (topic: context-data)"
3799
4072
  };
3800
4073
  }
4074
+ const contextsDir = (0, import_path6.join)(ctx.config.projectPath, "_ai-context");
4075
+ const contextsDirExistedBefore = (0, import_fs6.existsSync)(contextsDir);
3801
4076
  const downloadResult = await fetchContexts(domainValues, ctx.token, ctx.contextRepo, ctx.config.projectPath);
3802
4077
  ctx.installed.domainsInstalled = downloadResult.successful;
3803
4078
  ctx.installed.contextReleaseVersion = downloadResult.releaseVersion;
@@ -3809,16 +4084,42 @@ var fetch_contexts_default = defineStep({
3809
4084
  const status = ok ? "\u2713" : `\u2717 ${reason ?? "Unknown error"}`;
3810
4085
  console.log(` ${domain.padEnd(domainColWidth)}${status}`);
3811
4086
  }
4087
+ const record = {
4088
+ domains: downloadResult.successful,
4089
+ contextsDirCreated: !contextsDirExistedBefore
4090
+ };
3812
4091
  if (downloadResult.failed.length > 0) {
3813
4092
  return {
3814
4093
  status: "failed",
3815
- message: `${downloadResult.successful.join(", ")} succeeded; ${downloadResult.failed.join(", ")} failed`
4094
+ message: `${downloadResult.successful.join(", ")} succeeded; ${downloadResult.failed.join(", ")} failed`,
4095
+ record
3816
4096
  };
3817
4097
  }
3818
4098
  return {
3819
4099
  status: "success",
3820
- message: downloadResult.successful.join(", ")
4100
+ message: downloadResult.successful.join(", "),
4101
+ record
3821
4102
  };
4103
+ },
4104
+ inverse: {
4105
+ label: "Removing contexts",
4106
+ execute: async (raw, ctx) => {
4107
+ const rec = raw ?? {};
4108
+ const domains = rec.domains ?? [];
4109
+ const removed = [];
4110
+ const relPaths = [];
4111
+ for (const domain of domains) {
4112
+ const rel = (0, import_path6.join)("_ai-context", domain);
4113
+ relPaths.push(rel);
4114
+ const abs = resolveProjectPath(rel, ctx.config.projectPath);
4115
+ if (deleteDirIfExists(abs)) removed.push(domain);
4116
+ }
4117
+ pruneEmptyAncestors(relPaths, ctx.config.projectPath);
4118
+ return {
4119
+ status: "success",
4120
+ message: removed.length > 0 ? removed.join(", ") : "nothing to remove"
4121
+ };
4122
+ }
3822
4123
  }
3823
4124
  });
3824
4125
  async function fetchContexts(domains, token, repo, targetDir) {
@@ -3834,11 +4135,11 @@ async function fetchContexts(domains, token, repo, targetDir) {
3834
4135
  }
3835
4136
  return result;
3836
4137
  }
3837
- const contextsDir = (0, import_path4.join)(targetDir, "_ai-context");
3838
- if (!(0, import_fs4.existsSync)(contextsDir)) (0, import_fs4.mkdirSync)(contextsDir, { recursive: true });
4138
+ const contextsDir = (0, import_path6.join)(targetDir, "_ai-context");
4139
+ if (!(0, import_fs6.existsSync)(contextsDir)) (0, import_fs6.mkdirSync)(contextsDir, { recursive: true });
3839
4140
  const archive = await downloadAndExtractAsset(token, asset, targetDir, ".temp-download");
3840
4141
  try {
3841
- const extractedFolders = (0, import_fs4.readdirSync)(archive.extractedRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4142
+ const extractedFolders = (0, import_fs6.readdirSync)(archive.extractedRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3842
4143
  for (const domain of domains) {
3843
4144
  try {
3844
4145
  const match = extractedFolders.find((f) => f.toLowerCase() === domain.toLowerCase());
@@ -3847,8 +4148,8 @@ async function fetchContexts(domains, token, repo, targetDir) {
3847
4148
  result.failureReasons[domain] = "Not found in archive";
3848
4149
  continue;
3849
4150
  }
3850
- const domainPath = (0, import_path4.join)(contextsDir, domain);
3851
- copyDirectory((0, import_path4.join)(archive.extractedRoot, match), domainPath);
4151
+ const domainPath = (0, import_path6.join)(contextsDir, domain);
4152
+ copyDirectory((0, import_path6.join)(archive.extractedRoot, match), domainPath);
3852
4153
  result.successful.push(domain);
3853
4154
  } catch (error) {
3854
4155
  result.failed.push(domain);
@@ -3862,29 +4163,29 @@ async function fetchContexts(domains, token, repo, targetDir) {
3862
4163
  }
3863
4164
 
3864
4165
  // src/steps/resources/fetch-factory.ts
3865
- var import_fs7 = require("fs");
3866
- var import_path7 = require("path");
4166
+ var import_fs9 = require("fs");
4167
+ var import_path9 = require("path");
3867
4168
 
3868
4169
  // src/bundles/run-bundle.ts
3869
- var import_fs6 = require("fs");
3870
- var import_path6 = require("path");
4170
+ var import_fs8 = require("fs");
4171
+ var import_path8 = require("path");
3871
4172
 
3872
4173
  // src/bundles/types.ts
3873
4174
  var SUPPORTED_SCHEMA_VERSIONS = [1, 2];
3874
4175
 
3875
4176
  // src/utils/marker-block.ts
3876
- var import_fs5 = require("fs");
3877
- var import_path5 = require("path");
4177
+ var import_fs7 = require("fs");
4178
+ var import_path7 = require("path");
3878
4179
  function upsertMarkerBlock(filePath, startMarker, endMarker, body, opts = {}) {
3879
4180
  const block = `${startMarker}
3880
4181
  ${body}
3881
4182
  ${endMarker}`;
3882
- (0, import_fs5.mkdirSync)((0, import_path5.dirname)(filePath), { recursive: true });
3883
- if (!(0, import_fs5.existsSync)(filePath)) {
3884
- (0, import_fs5.writeFileSync)(filePath, block + "\n", "utf-8");
4183
+ (0, import_fs7.mkdirSync)((0, import_path7.dirname)(filePath), { recursive: true });
4184
+ if (!(0, import_fs7.existsSync)(filePath)) {
4185
+ (0, import_fs7.writeFileSync)(filePath, block + "\n", "utf-8");
3885
4186
  return;
3886
4187
  }
3887
- const existing = (0, import_fs5.readFileSync)(filePath, "utf-8");
4188
+ const existing = (0, import_fs7.readFileSync)(filePath, "utf-8");
3888
4189
  const s = existing.indexOf(startMarker);
3889
4190
  const e = existing.indexOf(endMarker);
3890
4191
  const hasS = s !== -1;
@@ -3894,12 +4195,43 @@ ${endMarker}`;
3894
4195
  }
3895
4196
  if (hasS && hasE && e > s) {
3896
4197
  const updated = existing.slice(0, s) + block + existing.slice(e + endMarker.length);
3897
- (0, import_fs5.writeFileSync)(filePath, updated, "utf-8");
4198
+ (0, import_fs7.writeFileSync)(filePath, updated, "utf-8");
3898
4199
  } else {
3899
- const sep3 = existing.endsWith("\n") ? "\n" : "\n\n";
3900
- (0, import_fs5.writeFileSync)(filePath, existing + sep3 + block + "\n", "utf-8");
4200
+ const sep4 = existing.endsWith("\n") ? "\n" : "\n\n";
4201
+ (0, import_fs7.writeFileSync)(filePath, existing + sep4 + block + "\n", "utf-8");
3901
4202
  }
3902
4203
  }
4204
+ function removeMarkerBlock(filePath, startMarker, endMarker, opts = {}) {
4205
+ if (!(0, import_fs7.existsSync)(filePath)) return "absent";
4206
+ const existing = (0, import_fs7.readFileSync)(filePath, "utf-8");
4207
+ const s = existing.indexOf(startMarker);
4208
+ const e = existing.indexOf(endMarker);
4209
+ if (s === -1 && e === -1) {
4210
+ } else if (s === -1 || e === -1 || e < s) {
4211
+ return "corrupt";
4212
+ }
4213
+ let updated = existing;
4214
+ if (s !== -1 && e !== -1) {
4215
+ const blockEnd = e + endMarker.length;
4216
+ let cutStart = s;
4217
+ let cutEnd = blockEnd;
4218
+ if (cutStart > 0 && existing[cutStart - 1] === "\n") cutStart -= 1;
4219
+ if (existing[cutEnd] === "\n") cutEnd += 1;
4220
+ updated = existing.slice(0, cutStart) + existing.slice(cutEnd);
4221
+ }
4222
+ if (opts.deleteFileIfEmpty) {
4223
+ const stripped = opts.emptyIgnorePattern ? updated.replace(opts.emptyIgnorePattern, "") : updated;
4224
+ if (stripped.trim() === "") {
4225
+ (0, import_fs7.unlinkSync)(filePath);
4226
+ return "deleted";
4227
+ }
4228
+ }
4229
+ if (updated !== existing) {
4230
+ (0, import_fs7.writeFileSync)(filePath, updated, "utf-8");
4231
+ return "removed";
4232
+ }
4233
+ return "absent";
4234
+ }
3903
4235
 
3904
4236
  // src/profiles/claude-code.ts
3905
4237
  var claudeCodeProfile = {
@@ -4038,7 +4370,7 @@ function getProfile(agent) {
4038
4370
  }
4039
4371
 
4040
4372
  // src/bundles/run-bundle.ts
4041
- var OWNER_KEY = "_bundleOwner";
4373
+ var OWNER_KEY2 = "_bundleOwner";
4042
4374
  async function runBundle(manifest, ctx) {
4043
4375
  assertSchemaCompatible(manifest);
4044
4376
  assertNameValid(manifest);
@@ -4054,7 +4386,10 @@ function runBundleV1(manifest, ctx) {
4054
4386
  const result = {
4055
4387
  name: manifest.name,
4056
4388
  opsExecuted: 0,
4057
- filesTouched: []
4389
+ filesTouched: [],
4390
+ filesCreated: [],
4391
+ filesPatched: [],
4392
+ markerBlockFiles: []
4058
4393
  };
4059
4394
  const ops = manifest.targets?.[ctx.agent] ?? [];
4060
4395
  for (const op of ops) {
@@ -4074,7 +4409,10 @@ function runBundleV2(manifest, ctx) {
4074
4409
  const result = {
4075
4410
  name: manifest.name,
4076
4411
  opsExecuted: 0,
4077
- filesTouched: []
4412
+ filesTouched: [],
4413
+ filesCreated: [],
4414
+ filesPatched: [],
4415
+ markerBlockFiles: []
4078
4416
  };
4079
4417
  const hookPatches = /* @__PURE__ */ new Map();
4080
4418
  for (const asset of manifest.assets ?? []) {
@@ -4122,17 +4460,18 @@ function runAsset(asset, profile, hookPatches, ctx, result) {
4122
4460
  function runAssetCopy(asset, handler, ctx, result) {
4123
4461
  if (!handler.supported || !handler.destination) return;
4124
4462
  const source = resolveBundlePath(asset.source, ctx);
4125
- if (!(0, import_fs6.existsSync)(source)) return;
4126
- const isDirectory = (0, import_fs6.statSync)(source).isDirectory();
4463
+ if (!(0, import_fs8.existsSync)(source)) return;
4464
+ const isDirectory = (0, import_fs8.statSync)(source).isDirectory();
4127
4465
  const targetRel = handler.destination(asset.name, isDirectory);
4128
- const target = resolveProjectPath(targetRel, ctx);
4466
+ const target = resolveProjectPath2(targetRel, ctx);
4129
4467
  if (isDirectory) {
4130
4468
  copyDirectory(source, target);
4131
4469
  } else {
4132
- (0, import_fs6.mkdirSync)((0, import_path6.dirname)(target), { recursive: true });
4133
- (0, import_fs6.copyFileSync)(source, target);
4470
+ (0, import_fs8.mkdirSync)((0, import_path8.dirname)(target), { recursive: true });
4471
+ (0, import_fs8.copyFileSync)(source, target);
4134
4472
  }
4135
4473
  result.filesTouched.push(targetRel);
4474
+ result.filesCreated.push(targetRel);
4136
4475
  }
4137
4476
  function accumulateHook(asset, handler, hookPatches, ctx, result) {
4138
4477
  if (!handler.supported) return;
@@ -4141,13 +4480,14 @@ function accumulateHook(asset, handler, hookPatches, ctx, result) {
4141
4480
  if (eventName === null) return;
4142
4481
  if (asset.script && handler.scriptDir) {
4143
4482
  const scriptSource = resolveBundlePath(asset.script, ctx);
4144
- if ((0, import_fs6.existsSync)(scriptSource)) {
4145
- const scriptName = (0, import_path6.basename)(asset.script);
4483
+ if ((0, import_fs8.existsSync)(scriptSource)) {
4484
+ const scriptName = (0, import_path8.basename)(asset.script);
4146
4485
  const scriptRelDest = `${handler.scriptDir}/${scriptName}`;
4147
- const scriptDest = resolveProjectPath(scriptRelDest, ctx);
4148
- (0, import_fs6.mkdirSync)((0, import_path6.dirname)(scriptDest), { recursive: true });
4149
- (0, import_fs6.copyFileSync)(scriptSource, scriptDest);
4486
+ const scriptDest = resolveProjectPath2(scriptRelDest, ctx);
4487
+ (0, import_fs8.mkdirSync)((0, import_path8.dirname)(scriptDest), { recursive: true });
4488
+ (0, import_fs8.copyFileSync)(scriptSource, scriptDest);
4150
4489
  result.filesTouched.push(scriptRelDest);
4490
+ result.filesCreated.push(scriptRelDest);
4151
4491
  }
4152
4492
  }
4153
4493
  const command = handler.scriptDir ? asset.command.replace(/\{hookDir\}/g, handler.scriptDir) : asset.command;
@@ -4181,22 +4521,24 @@ function substituteHookDir(op, profile) {
4181
4521
  function writeGitignoreBlock(manifest, ctx, result) {
4182
4522
  if (!manifest.gitignore?.length) return;
4183
4523
  upsertMarkerBlock(
4184
- (0, import_path6.join)(ctx.projectPath, ".gitignore"),
4524
+ (0, import_path8.join)(ctx.projectPath, ".gitignore"),
4185
4525
  `# ${manifest.name}:start`,
4186
4526
  `# ${manifest.name}:end`,
4187
4527
  manifest.gitignore.join("\n")
4188
4528
  );
4189
4529
  result.filesTouched.push(".gitignore");
4530
+ result.markerBlockFiles.push(".gitignore");
4190
4531
  }
4191
4532
  function writeInstructionsBlock(bundleName, snippet, instructionsFile, ctx, result) {
4192
4533
  if (ctx.skipInstructions) return;
4193
4534
  upsertMarkerBlock(
4194
- (0, import_path6.join)(ctx.projectPath, instructionsFile),
4535
+ (0, import_path8.join)(ctx.projectPath, instructionsFile),
4195
4536
  `<!-- ${bundleName}:start -->`,
4196
4537
  `<!-- ${bundleName}:end -->`,
4197
4538
  snippet
4198
4539
  );
4199
4540
  result.filesTouched.push(instructionsFile);
4541
+ result.markerBlockFiles.push(instructionsFile);
4200
4542
  }
4201
4543
  function executeOp(op, owner, ctx, result) {
4202
4544
  switch (op.op) {
@@ -4214,63 +4556,65 @@ function executeOp(op, owner, ctx, result) {
4214
4556
  }
4215
4557
  function runCopy(op, ctx, result) {
4216
4558
  const source = resolveBundlePath(op.from, ctx);
4217
- const target = resolveProjectPath(op.to, ctx);
4218
- if (!(0, import_fs6.existsSync)(source)) return;
4219
- if ((0, import_fs6.statSync)(source).isDirectory()) {
4559
+ const target = resolveProjectPath2(op.to, ctx);
4560
+ if (!(0, import_fs8.existsSync)(source)) return;
4561
+ if ((0, import_fs8.statSync)(source).isDirectory()) {
4220
4562
  copyDirectory(source, target);
4221
4563
  } else {
4222
- (0, import_fs6.mkdirSync)((0, import_path6.dirname)(target), { recursive: true });
4223
- (0, import_fs6.copyFileSync)(source, target);
4564
+ (0, import_fs8.mkdirSync)((0, import_path8.dirname)(target), { recursive: true });
4565
+ (0, import_fs8.copyFileSync)(source, target);
4224
4566
  }
4225
4567
  result.filesTouched.push(op.to);
4568
+ result.filesCreated.push(op.to);
4226
4569
  }
4227
4570
  function runMergeJson(op, owner, ctx, result) {
4228
- const filePath = resolveProjectPath(op.file, ctx);
4571
+ const filePath = resolveProjectPath2(op.file, ctx);
4229
4572
  const existing = readJsonOrEmpty(filePath);
4230
- const stripped = stripOwnedEntries(existing, owner);
4573
+ const stripped = stripOwnedEntries2(existing, owner);
4231
4574
  const tagged = tagEntries(op.patch, owner);
4232
4575
  const merged = deepMerge2(stripped, tagged);
4233
- (0, import_fs6.mkdirSync)((0, import_path6.dirname)(filePath), { recursive: true });
4234
- (0, import_fs6.writeFileSync)(filePath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
4576
+ (0, import_fs8.mkdirSync)((0, import_path8.dirname)(filePath), { recursive: true });
4577
+ (0, import_fs8.writeFileSync)(filePath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
4235
4578
  result.filesTouched.push(op.file);
4579
+ result.filesPatched.push(op.file);
4236
4580
  }
4237
4581
  function readJsonOrEmpty(path) {
4238
- if (!(0, import_fs6.existsSync)(path)) return {};
4582
+ if (!(0, import_fs8.existsSync)(path)) return {};
4239
4583
  try {
4240
- return JSON.parse((0, import_fs6.readFileSync)(path, "utf-8"));
4584
+ return JSON.parse((0, import_fs8.readFileSync)(path, "utf-8"));
4241
4585
  } catch {
4242
4586
  return {};
4243
4587
  }
4244
4588
  }
4245
- function isPlainObject2(v) {
4589
+ function isPlainObject3(v) {
4246
4590
  return typeof v === "object" && v !== null && !Array.isArray(v);
4247
4591
  }
4248
4592
  function tagEntries(value, owner) {
4249
4593
  if (Array.isArray(value)) {
4250
4594
  return value.map(
4251
- (item) => isPlainObject2(item) ? { ...item, [OWNER_KEY]: owner } : item
4595
+ (item) => isPlainObject3(item) ? { ...item, [OWNER_KEY2]: owner } : item
4252
4596
  );
4253
4597
  }
4254
- if (isPlainObject2(value)) {
4598
+ if (isPlainObject3(value)) {
4255
4599
  return Object.fromEntries(
4256
4600
  Object.entries(value).map(([k, v]) => [k, tagEntries(v, owner)])
4257
4601
  );
4258
4602
  }
4259
4603
  return value;
4260
4604
  }
4261
- function stripOwnedEntries(value, owner) {
4605
+ function stripOwnedEntries2(value, owner) {
4262
4606
  if (Array.isArray(value)) {
4263
- return value.filter((item) => !(isPlainObject2(item) && item[OWNER_KEY] === owner)).map((item) => stripOwnedEntries(item, owner));
4607
+ return value.filter((item) => !(isPlainObject3(item) && item[OWNER_KEY2] === owner)).map((item) => stripOwnedEntries2(item, owner));
4264
4608
  }
4265
- if (isPlainObject2(value)) {
4609
+ if (isPlainObject3(value)) {
4266
4610
  return Object.fromEntries(
4267
- Object.entries(value).map(([k, v]) => [k, stripOwnedEntries(v, owner)])
4611
+ Object.entries(value).map(([k, v]) => [k, stripOwnedEntries2(v, owner)])
4268
4612
  );
4269
4613
  }
4270
4614
  return value;
4271
4615
  }
4272
4616
  function deepMerge2(a, b) {
4273
- if (isPlainObject2(a) && isPlainObject2(b)) {
4617
+ if (isPlainObject3(a) && isPlainObject3(b)) {
4274
4618
  const out = { ...a };
4275
4619
  for (const [k, v] of Object.entries(b)) {
4276
4620
  out[k] = k in a ? deepMerge2(a[k], v) : v;
@@ -4283,17 +4627,17 @@ function deepMerge2(a, b) {
4283
4627
  return b;
4284
4628
  }
4285
4629
  function resolveBundlePath(rel, ctx) {
4286
- const base = (0, import_path6.resolve)(ctx.bundleRoot);
4287
- const full = (0, import_path6.resolve)((0, import_path6.join)(base, rel));
4288
- if (full !== base && !full.startsWith(base + import_path6.sep)) {
4630
+ const base = (0, import_path8.resolve)(ctx.bundleRoot);
4631
+ const full = (0, import_path8.resolve)((0, import_path8.join)(base, rel));
4632
+ if (full !== base && !full.startsWith(base + import_path8.sep)) {
4289
4633
  throw new Error(`Bundle op "from" escapes bundle root: ${rel}`);
4290
4634
  }
4291
4635
  return full;
4292
4636
  }
4293
- function resolveProjectPath(rel, ctx) {
4294
- const base = (0, import_path6.resolve)(ctx.projectPath);
4295
- const full = (0, import_path6.resolve)((0, import_path6.join)(base, rel));
4296
- if (full !== base && !full.startsWith(base + import_path6.sep)) {
4637
+ function resolveProjectPath2(rel, ctx) {
4638
+ const base = (0, import_path8.resolve)(ctx.projectPath);
4639
+ const full = (0, import_path8.resolve)((0, import_path8.join)(base, rel));
4640
+ if (full !== base && !full.startsWith(base + import_path8.sep)) {
4297
4641
  throw new Error(`Bundle op "to" escapes project path: ${rel}`);
4298
4642
  }
4299
4643
  return full;
@@ -4314,7 +4658,7 @@ function assertNameValid(manifest) {
4314
4658
  }
4315
4659
  }
4316
4660
  function assertBundleRootExists(ctx) {
4317
- if (!(0, import_fs6.existsSync)(ctx.bundleRoot) || !(0, import_fs6.statSync)(ctx.bundleRoot).isDirectory()) {
4661
+ if (!(0, import_fs8.existsSync)(ctx.bundleRoot) || !(0, import_fs8.statSync)(ctx.bundleRoot).isDirectory()) {
4318
4662
  throw new Error(`Bundle root not found or not a directory: ${ctx.bundleRoot}`);
4319
4663
  }
4320
4664
  }
@@ -4332,14 +4676,67 @@ var fetch_factory_default = defineStep({
4332
4676
  if (result.instructionsSnippet) {
4333
4677
  ctx.installed.factoryInstructionsSnippet = result.instructionsSnippet;
4334
4678
  }
4679
+ const record = {
4680
+ bundleName: "factory",
4681
+ filesCreated: result.filesCreated,
4682
+ filesPatched: result.filesPatched,
4683
+ markerBlockFiles: result.markerBlockFiles
4684
+ };
4335
4685
  if (!result.success) {
4336
- return { status: "failed", detail: result.failureReason };
4686
+ return { status: "failed", detail: result.failureReason, record };
4687
+ }
4688
+ return { status: "success", message: result.filesInstalled.join(", "), record };
4689
+ },
4690
+ inverse: {
4691
+ label: "Removing Factory framework",
4692
+ execute: async (raw, ctx) => {
4693
+ const rec = raw ?? {};
4694
+ const bundle = rec.bundleName ?? "factory";
4695
+ const projectPath = ctx.config.projectPath;
4696
+ const summary = [];
4697
+ for (const rel of rec.filesPatched ?? []) {
4698
+ const abs = resolveProjectPath(rel, projectPath);
4699
+ if (stripBundleOwnerFromJsonFile(abs, bundle)) summary.push(`stripped ${rel}`);
4700
+ deleteJsonIfEffectivelyEmpty(abs);
4701
+ }
4702
+ for (const rel of rec.markerBlockFiles ?? []) {
4703
+ const abs = resolveProjectPath(rel, projectPath);
4704
+ if (rel === ".gitignore") {
4705
+ removeMarkerBlock(abs, `# ${bundle}:start`, `# ${bundle}:end`);
4706
+ } else {
4707
+ removeMarkerBlock(abs, `<!-- ${bundle}:start -->`, `<!-- ${bundle}:end -->`);
4708
+ }
4709
+ }
4710
+ const created = rec.filesCreated ?? [];
4711
+ const sortedCreated = [...created].sort((a, b) => a.length - b.length);
4712
+ for (const rel of sortedCreated) {
4713
+ const abs = resolveProjectPath(rel, projectPath);
4714
+ if (!(0, import_fs9.existsSync)(abs)) continue;
4715
+ if (isDirectorySafe(abs)) {
4716
+ deleteDirIfExists(abs);
4717
+ summary.push(`removed ${rel}`);
4718
+ } else {
4719
+ deleteFileIfExists(abs);
4720
+ summary.push(`removed ${rel}`);
4721
+ }
4722
+ }
4723
+ const pruned = pruneEmptyAncestors(created, projectPath);
4724
+ if (pruned.length > 0) summary.push(`pruned ${pruned.length} empty dir(s)`);
4725
+ return {
4726
+ status: "success",
4727
+ message: summary.length > 0 ? `${summary.length} action(s)` : "nothing to remove"
4728
+ };
4337
4729
  }
4338
- return { status: "success", message: result.filesInstalled.join(", ") };
4339
4730
  }
4340
4731
  });
4341
4732
  async function fetchFactory(token, repo, targetDir, agent) {
4342
- const result = { success: false, filesInstalled: [] };
4733
+ const result = {
4734
+ success: false,
4735
+ filesInstalled: [],
4736
+ filesCreated: [],
4737
+ filesPatched: [],
4738
+ markerBlockFiles: []
4739
+ };
4343
4740
  let release;
4344
4741
  try {
4345
4742
  release = await fetchLatestRelease(token, repo);
@@ -4355,15 +4752,15 @@ async function fetchFactory(token, repo, targetDir, agent) {
4355
4752
  let archive = null;
4356
4753
  try {
4357
4754
  archive = await downloadAndExtractAsset(token, asset, targetDir, ".temp-factory-download");
4358
- const extractedEntries = (0, import_fs7.readdirSync)(archive.extractedRoot);
4755
+ const extractedEntries = (0, import_fs9.readdirSync)(archive.extractedRoot);
4359
4756
  const extractedFolder = extractedEntries.find((e) => e.toLowerCase() === FACTORY_ROOT_FOLDER.toLowerCase());
4360
4757
  if (!extractedFolder) {
4361
4758
  result.failureReason = `No ${FACTORY_ROOT_FOLDER}/ folder in archive`;
4362
4759
  return result;
4363
4760
  }
4364
- const extractedPath = (0, import_path7.join)(archive.extractedRoot, extractedFolder);
4365
- const manifestPath = (0, import_path7.join)(extractedPath, "install.json");
4366
- if (!(0, import_fs7.existsSync)(manifestPath)) {
4761
+ const extractedPath = (0, import_path9.join)(archive.extractedRoot, extractedFolder);
4762
+ const manifestPath = (0, import_path9.join)(extractedPath, "install.json");
4763
+ if (!(0, import_fs9.existsSync)(manifestPath)) {
4367
4764
  result.failureReason = "Factory archive has no install.json \u2014 upgrade Factory or downgrade installer";
4368
4765
  return result;
4369
4766
  }
@@ -4379,7 +4776,7 @@ async function fetchFactory(token, repo, targetDir, agent) {
4379
4776
  async function installViaBundle(manifestPath, extractedPath, targetDir, agent, result) {
4380
4777
  let manifest;
4381
4778
  try {
4382
- manifest = JSON.parse((0, import_fs7.readFileSync)(manifestPath, "utf-8"));
4779
+ manifest = JSON.parse((0, import_fs9.readFileSync)(manifestPath, "utf-8"));
4383
4780
  } catch (e) {
4384
4781
  throw new Error(`install.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
4385
4782
  }
@@ -4394,11 +4791,14 @@ async function installViaBundle(manifestPath, extractedPath, targetDir, agent, r
4394
4791
  });
4395
4792
  result.instructionsSnippet = runResult.instructionsSnippet;
4396
4793
  result.filesInstalled = runResult.filesTouched;
4794
+ result.filesCreated = runResult.filesCreated;
4795
+ result.filesPatched = runResult.filesPatched;
4796
+ result.markerBlockFiles = runResult.markerBlockFiles;
4397
4797
  }
4398
4798
 
4399
4799
  // src/steps/resources/fetch-abap-hooks.ts
4400
- var import_fs8 = require("fs");
4401
- var import_path8 = require("path");
4800
+ var import_fs10 = require("fs");
4801
+ var import_path10 = require("path");
4402
4802
  var HOOKS_ASSET_NAME = "sap-hooks.tar.gz";
4403
4803
  var HOOKS_TAG_PREFIX = "hooks-v";
4404
4804
  var HOOKS_ROOT_FOLDER = "hooks";
@@ -4466,14 +4866,55 @@ var fetch_abap_hooks_default = defineStep({
4466
4866
  if (result.instructionsSnippet) {
4467
4867
  ctx.installed.abapHooksInstructionsSnippet = result.instructionsSnippet;
4468
4868
  }
4869
+ const record = {
4870
+ bundleName: "abap-mcp-hooks",
4871
+ filesCreated: result.filesCreated,
4872
+ filesPatched: result.filesPatched,
4873
+ markerBlockFiles: result.markerBlockFiles
4874
+ };
4469
4875
  if (!result.success) {
4470
- return { status: "failed", detail: result.failureReason };
4876
+ return { status: "failed", detail: result.failureReason, record };
4877
+ }
4878
+ return { status: "success", message: result.filesInstalled.join(", "), record };
4879
+ },
4880
+ inverse: {
4881
+ label: "Removing ABAP MCP hooks",
4882
+ execute: async (raw, ctx) => {
4883
+ const rec = raw ?? {};
4884
+ const bundle = rec.bundleName ?? "abap-mcp-hooks";
4885
+ const projectPath = ctx.config.projectPath;
4886
+ for (const rel of rec.filesPatched ?? []) {
4887
+ const abs = resolveProjectPath(rel, projectPath);
4888
+ stripBundleOwnerFromJsonFile(abs, bundle);
4889
+ deleteJsonIfEffectivelyEmpty(abs);
4890
+ }
4891
+ for (const rel of rec.markerBlockFiles ?? []) {
4892
+ const abs = resolveProjectPath(rel, projectPath);
4893
+ if (rel === ".gitignore") {
4894
+ removeMarkerBlock(abs, `# ${bundle}:start`, `# ${bundle}:end`);
4895
+ } else {
4896
+ removeMarkerBlock(abs, `<!-- ${bundle}:start -->`, `<!-- ${bundle}:end -->`);
4897
+ }
4898
+ }
4899
+ const created = rec.filesCreated ?? [];
4900
+ for (const rel of created) {
4901
+ const abs = resolveProjectPath(rel, projectPath);
4902
+ if (isDirectorySafe(abs)) deleteDirIfExists(abs);
4903
+ else deleteFileIfExists(abs);
4904
+ }
4905
+ pruneEmptyAncestors(created, projectPath);
4906
+ return { status: "success" };
4471
4907
  }
4472
- return { status: "success", message: result.filesInstalled.join(", ") };
4473
4908
  }
4474
4909
  });
4475
4910
  async function fetchAbapHooks(token, repo, targetDir, agent) {
4476
- const result = { success: false, filesInstalled: [] };
4911
+ const result = {
4912
+ success: false,
4913
+ filesInstalled: [],
4914
+ filesCreated: [],
4915
+ filesPatched: [],
4916
+ markerBlockFiles: []
4917
+ };
4477
4918
  let release;
4478
4919
  try {
4479
4920
  release = await fetchLatestReleaseByTagPrefix(token, repo, HOOKS_TAG_PREFIX);
@@ -4489,7 +4930,7 @@ async function fetchAbapHooks(token, repo, targetDir, agent) {
4489
4930
  let archive = null;
4490
4931
  try {
4491
4932
  archive = await downloadAndExtractAsset(token, asset, targetDir, ".temp-abap-hooks-download");
4492
- const extractedEntries = (0, import_fs8.readdirSync)(archive.extractedRoot);
4933
+ const extractedEntries = (0, import_fs10.readdirSync)(archive.extractedRoot);
4493
4934
  const innerFolder = extractedEntries.find(
4494
4935
  (e) => e.toLowerCase() === HOOKS_ROOT_FOLDER.toLowerCase()
4495
4936
  );
@@ -4497,12 +4938,12 @@ async function fetchAbapHooks(token, repo, targetDir, agent) {
4497
4938
  result.failureReason = `No ${HOOKS_ROOT_FOLDER}/ folder in archive`;
4498
4939
  return result;
4499
4940
  }
4500
- const bundleRoot = (0, import_path8.join)(archive.extractedRoot, innerFolder);
4501
- if (!(0, import_fs8.statSync)(bundleRoot).isDirectory()) {
4941
+ const bundleRoot = (0, import_path10.join)(archive.extractedRoot, innerFolder);
4942
+ if (!(0, import_fs10.statSync)(bundleRoot).isDirectory()) {
4502
4943
  result.failureReason = `${HOOKS_ROOT_FOLDER}/ entry is not a directory`;
4503
4944
  return result;
4504
4945
  }
4505
- const presentDefs = HOOK_DEFINITIONS.filter((d) => (0, import_fs8.existsSync)((0, import_path8.join)(bundleRoot, d.script)));
4946
+ const presentDefs = HOOK_DEFINITIONS.filter((d) => (0, import_fs10.existsSync)((0, import_path10.join)(bundleRoot, d.script)));
4506
4947
  if (presentDefs.length === 0) {
4507
4948
  result.failureReason = "Archive contains no recognized hook scripts";
4508
4949
  return result;
@@ -4542,6 +4983,9 @@ async function fetchAbapHooks(token, repo, targetDir, agent) {
4542
4983
  skipInstructions: true
4543
4984
  });
4544
4985
  result.filesInstalled = runResult.filesTouched;
4986
+ result.filesCreated = runResult.filesCreated;
4987
+ result.filesPatched = runResult.filesPatched;
4988
+ result.markerBlockFiles = runResult.markerBlockFiles;
4545
4989
  result.instructionsSnippet = runResult.instructionsSnippet;
4546
4990
  result.success = true;
4547
4991
  } catch (error) {
@@ -4553,8 +4997,8 @@ async function fetchAbapHooks(token, repo, targetDir, agent) {
4553
4997
  }
4554
4998
 
4555
4999
  // src/steps/setup/write-instructions.ts
4556
- var import_fs9 = require("fs");
4557
- var import_path9 = require("path");
5000
+ var import_fs11 = require("fs");
5001
+ var import_path11 = require("path");
4558
5002
 
4559
5003
  // src/steps/shared.ts
4560
5004
  var cached = null;
@@ -4589,17 +5033,23 @@ var write_instructions_default = defineStep({
4589
5033
  const projectPath = ctx.config.projectPath;
4590
5034
  const content = buildCombinedInstructions(domains, filteredMcpConfig, agent, projectPath);
4591
5035
  const target = getAgentTarget(agent);
4592
- const filePath = (0, import_path9.join)(projectPath, target.instructions);
4593
- const fileDir = (0, import_path9.dirname)(filePath);
4594
- if (!(0, import_fs9.existsSync)(fileDir)) (0, import_fs9.mkdirSync)(fileDir, { recursive: true });
4595
- if (agent === "cursor" && !(0, import_fs9.existsSync)(filePath)) {
4596
- (0, import_fs9.writeFileSync)(filePath, `---
5036
+ const filePath = (0, import_path11.join)(projectPath, target.instructions);
5037
+ const fileDir = (0, import_path11.dirname)(filePath);
5038
+ if (!(0, import_fs11.existsSync)(fileDir)) (0, import_fs11.mkdirSync)(fileDir, { recursive: true });
5039
+ const fileExistedBefore = (0, import_fs11.existsSync)(filePath);
5040
+ let cursorFrontmatterWritten = false;
5041
+ if (agent === "cursor" && !fileExistedBefore) {
5042
+ (0, import_fs11.writeFileSync)(filePath, `---
4597
5043
  description: AI development instructions from One-Shot Installer
4598
5044
  alwaysApply: true
4599
5045
  ---
4600
5046
 
4601
5047
  `, "utf-8");
5048
+ cursorFrontmatterWritten = true;
4602
5049
  }
5050
+ const markerBlocks = [
5051
+ { start: MARKER_START, end: MARKER_END }
5052
+ ];
4603
5053
  upsertMarkerBlock(filePath, MARKER_START, MARKER_END, content, {
4604
5054
  onCorrupt: (file) => {
4605
5055
  const fileName = file.split(/[/\\]/).pop() ?? file;
@@ -4627,6 +5077,7 @@ alwaysApply: true
4627
5077
  "<!-- factory:end -->",
4628
5078
  ctx.installed.factoryInstructionsSnippet
4629
5079
  );
5080
+ markerBlocks.push({ start: "<!-- factory:start -->", end: "<!-- factory:end -->" });
4630
5081
  }
4631
5082
  if (ctx.installed.abapHooksInstructionsSnippet) {
4632
5083
  upsertMarkerBlock(
@@ -4635,21 +5086,50 @@ alwaysApply: true
4635
5086
  "<!-- abap-mcp-hooks:end -->",
4636
5087
  ctx.installed.abapHooksInstructionsSnippet
4637
5088
  );
5089
+ markerBlocks.push({ start: "<!-- abap-mcp-hooks:start -->", end: "<!-- abap-mcp-hooks:end -->" });
5090
+ }
5091
+ const record = {
5092
+ filePath: target.instructions,
5093
+ markerBlocks,
5094
+ fileExistedBefore,
5095
+ cursorFrontmatterWritten
5096
+ };
5097
+ return { status: "success", message: target.instructions, record };
5098
+ },
5099
+ inverse: {
5100
+ label: "Restoring instructions file",
5101
+ execute: async (raw, ctx) => {
5102
+ const rec = raw ?? {};
5103
+ if (!rec.filePath) return { status: "skipped", detail: "no file recorded" };
5104
+ const abs = resolveProjectPath(rec.filePath, ctx.config.projectPath);
5105
+ const blocks = rec.markerBlocks ?? [];
5106
+ const cursorPreambleRe = /^---[\s\S]*?---\s*/;
5107
+ for (let i = 0; i < blocks.length; i++) {
5108
+ const b = blocks[i];
5109
+ const isLast = i === blocks.length - 1;
5110
+ removeMarkerBlock(abs, b.start, b.end, {
5111
+ /* Only attempt to delete the file after the LAST block is
5112
+ * removed — if we delete on the first pass we'd lose later
5113
+ * blocks waiting in the same file. */
5114
+ deleteFileIfEmpty: isLast && !rec.fileExistedBefore,
5115
+ emptyIgnorePattern: rec.cursorFrontmatterWritten ? cursorPreambleRe : void 0
5116
+ });
5117
+ }
5118
+ return { status: "success", message: rec.filePath };
4638
5119
  }
4639
- return { status: "success", message: target.instructions };
4640
5120
  }
4641
5121
  });
4642
5122
  function collectMdFiles(dir) {
4643
- if (!(0, import_fs9.existsSync)(dir)) return [];
4644
- const entries = (0, import_fs9.readdirSync)(dir, { recursive: true });
5123
+ if (!(0, import_fs11.existsSync)(dir)) return [];
5124
+ const entries = (0, import_fs11.readdirSync)(dir, { recursive: true });
4645
5125
  return entries.filter((entry) => {
4646
- const fullPath = (0, import_path9.join)(dir, entry);
4647
- return entry.endsWith(".md") && (0, import_fs9.statSync)(fullPath).isFile();
5126
+ const fullPath = (0, import_path11.join)(dir, entry);
5127
+ return entry.endsWith(".md") && (0, import_fs11.statSync)(fullPath).isFile();
4648
5128
  }).map((entry) => entry.replace(/\\/g, "/")).sort();
4649
5129
  }
4650
5130
  function extractFirstHeading(filePath) {
4651
5131
  try {
4652
- const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
5132
+ const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
4653
5133
  const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, "");
4654
5134
  const match = withoutFrontmatter.match(/^#\s+(.+)/m);
4655
5135
  if (match) {
@@ -4667,16 +5147,16 @@ function formatPathRef(contextPath, description, agent) {
4667
5147
  return `- \`${contextPath}\` \u2014 ${description}`;
4668
5148
  }
4669
5149
  function resolveDomainFolder(domain, contextsDir) {
4670
- if ((0, import_fs9.existsSync)(contextsDir)) {
5150
+ if ((0, import_fs11.existsSync)(contextsDir)) {
4671
5151
  try {
4672
- const entries = (0, import_fs9.readdirSync)(contextsDir);
5152
+ const entries = (0, import_fs11.readdirSync)(contextsDir);
4673
5153
  const match = entries.find((e) => e.toLowerCase() === domain.toLowerCase());
4674
- if (match) return { folderName: match, folderPath: (0, import_path9.join)(contextsDir, match) };
5154
+ if (match) return { folderName: match, folderPath: (0, import_path11.join)(contextsDir, match) };
4675
5155
  } catch {
4676
5156
  }
4677
5157
  }
4678
5158
  const fallback = domain.toUpperCase();
4679
- return { folderName: fallback, folderPath: (0, import_path9.join)(contextsDir, fallback) };
5159
+ return { folderName: fallback, folderPath: (0, import_path11.join)(contextsDir, fallback) };
4680
5160
  }
4681
5161
  function buildContextRefsSection(domains, agent, contextsDir) {
4682
5162
  const lines2 = [
@@ -4688,34 +5168,34 @@ function buildContextRefsSection(domains, agent, contextsDir) {
4688
5168
  let hasAnyFiles = false;
4689
5169
  for (const domain of domains) {
4690
5170
  const { folderName, folderPath: domainPath } = resolveDomainFolder(domain, contextsDir);
4691
- if (!(0, import_fs9.existsSync)(domainPath)) continue;
5171
+ if (!(0, import_fs11.existsSync)(domainPath)) continue;
4692
5172
  const domainFiles = [];
4693
- const ctxInstructions = (0, import_path9.join)(domainPath, "context-instructions.md");
4694
- if ((0, import_fs9.existsSync)(ctxInstructions)) {
5173
+ const ctxInstructions = (0, import_path11.join)(domainPath, "context-instructions.md");
5174
+ if ((0, import_fs11.existsSync)(ctxInstructions)) {
4695
5175
  const desc = extractFirstHeading(ctxInstructions);
4696
5176
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/context-instructions.md`, desc, agent));
4697
5177
  }
4698
- const instructionsMd = (0, import_path9.join)(domainPath, "core", "instructions.md");
4699
- if ((0, import_fs9.existsSync)(instructionsMd)) {
5178
+ const instructionsMd = (0, import_path11.join)(domainPath, "core", "instructions.md");
5179
+ if ((0, import_fs11.existsSync)(instructionsMd)) {
4700
5180
  const desc = extractFirstHeading(instructionsMd);
4701
5181
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/instructions.md`, `${desc} (start here)`, agent));
4702
5182
  }
4703
- const coreDir = (0, import_path9.join)(domainPath, "core");
4704
- if ((0, import_fs9.existsSync)(coreDir)) {
5183
+ const coreDir = (0, import_path11.join)(domainPath, "core");
5184
+ if ((0, import_fs11.existsSync)(coreDir)) {
4705
5185
  const coreFiles = collectMdFiles(coreDir).filter((f) => f !== "instructions.md" && !f.startsWith("instructions/"));
4706
5186
  for (const file of coreFiles) {
4707
- const desc = extractFirstHeading((0, import_path9.join)(coreDir, file));
5187
+ const desc = extractFirstHeading((0, import_path11.join)(coreDir, file));
4708
5188
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/${file}`, desc, agent));
4709
5189
  }
4710
5190
  }
4711
- const refDir = (0, import_path9.join)(domainPath, "reference");
4712
- if ((0, import_fs9.existsSync)(refDir)) {
5191
+ const refDir = (0, import_path11.join)(domainPath, "reference");
5192
+ if ((0, import_fs11.existsSync)(refDir)) {
4713
5193
  const refFiles = collectMdFiles(refDir);
4714
5194
  if (refFiles.length > 0) {
4715
5195
  domainFiles.push(``);
4716
5196
  domainFiles.push(`**Reference & cheat sheets:**`);
4717
5197
  for (const file of refFiles) {
4718
- const desc = extractFirstHeading((0, import_path9.join)(refDir, file));
5198
+ const desc = extractFirstHeading((0, import_path11.join)(refDir, file));
4719
5199
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/reference/${file}`, desc, agent));
4720
5200
  }
4721
5201
  }
@@ -4746,7 +5226,7 @@ function buildMCPSection(mcpConfig) {
4746
5226
  return lines2.join("\n");
4747
5227
  }
4748
5228
  function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4749
- const contextsDir = (0, import_path9.join)(projectPath, "_ai-context");
5229
+ const contextsDir = (0, import_path11.join)(projectPath, "_ai-context");
4750
5230
  const lines2 = [`# AI Development Instructions`, ``, `> Generated by One-Shot Installer`, ``];
4751
5231
  lines2.push(buildContextRefsSection(domains, agent, contextsDir));
4752
5232
  if (Object.keys(mcpConfig).length > 0) lines2.push(buildMCPSection(mcpConfig));
@@ -4754,8 +5234,8 @@ function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4754
5234
  }
4755
5235
 
4756
5236
  // src/steps/setup/write-mcp-config.ts
4757
- var import_fs10 = require("fs");
4758
- var import_path10 = require("path");
5237
+ var import_fs12 = require("fs");
5238
+ var import_path12 = require("path");
4759
5239
  var red2 = (text) => `\x1B[31m${text}\x1B[0m`;
4760
5240
  var write_mcp_config_default = defineStep({
4761
5241
  name: "write-mcp-config",
@@ -4764,15 +5244,16 @@ var write_mcp_config_default = defineStep({
4764
5244
  execute: async (ctx) => {
4765
5245
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4766
5246
  const target = getAgentTarget(ctx.config.agent);
4767
- const mcpJsonPath = (0, import_path10.join)(ctx.config.projectPath, target.mcpConfig);
5247
+ const mcpJsonPath = (0, import_path12.join)(ctx.config.projectPath, target.mcpConfig);
5248
+ const fileExistedBefore = (0, import_fs12.existsSync)(mcpJsonPath);
4768
5249
  if (target.mcpDir) {
4769
- const dir = (0, import_path10.join)(ctx.config.projectPath, target.mcpDir);
4770
- if (!(0, import_fs10.existsSync)(dir)) (0, import_fs10.mkdirSync)(dir, { recursive: true });
5250
+ const dir = (0, import_path12.join)(ctx.config.projectPath, target.mcpDir);
5251
+ if (!(0, import_fs12.existsSync)(dir)) (0, import_fs12.mkdirSync)(dir, { recursive: true });
4771
5252
  }
4772
5253
  let existingFile = {};
4773
- if ((0, import_fs10.existsSync)(mcpJsonPath)) {
5254
+ if ((0, import_fs12.existsSync)(mcpJsonPath)) {
4774
5255
  try {
4775
- existingFile = JSON.parse((0, import_fs10.readFileSync)(mcpJsonPath, "utf-8"));
5256
+ existingFile = JSON.parse((0, import_fs12.readFileSync)(mcpJsonPath, "utf-8"));
4776
5257
  } catch {
4777
5258
  const box = [
4778
5259
  "",
@@ -4798,12 +5279,35 @@ var write_mcp_config_default = defineStep({
4798
5279
  }
4799
5280
  const mergedServers = { ...existingServers, ...newServers };
4800
5281
  const outputFile = { ...existingFile, [target.mcpRootKey]: mergedServers };
4801
- (0, import_fs10.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
5282
+ (0, import_fs12.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
4802
5283
  ctx.installed.mcpServersAdded = addedServers;
5284
+ const record = {
5285
+ filePath: target.mcpConfig,
5286
+ rootKey: target.mcpRootKey,
5287
+ serversAdded: addedServers,
5288
+ fileExistedBefore
5289
+ };
4803
5290
  return {
4804
5291
  status: "success",
4805
- message: addedServers.length > 0 ? addedServers.join(", ") : void 0
5292
+ message: addedServers.length > 0 ? addedServers.join(", ") : void 0,
5293
+ record
4806
5294
  };
5295
+ },
5296
+ inverse: {
5297
+ label: "Restoring MCP configuration",
5298
+ execute: async (raw, ctx) => {
5299
+ const rec = raw ?? {};
5300
+ if (!rec.filePath || !rec.rootKey) return { status: "skipped" };
5301
+ const abs = resolveProjectPath(rec.filePath, ctx.config.projectPath);
5302
+ const { changed, rootEmpty } = stripMcpServers(abs, rec.rootKey, rec.serversAdded ?? []);
5303
+ if (rootEmpty && !rec.fileExistedBefore) {
5304
+ deleteFileIfExists(abs);
5305
+ }
5306
+ return {
5307
+ status: "success",
5308
+ message: changed ? rec.filePath : "no changes needed"
5309
+ };
5310
+ }
4807
5311
  }
4808
5312
  });
4809
5313
 
@@ -4844,21 +5348,54 @@ var run_mcp_install_commands_default = defineStep({
4844
5348
  failed.push({ name, reason });
4845
5349
  }
4846
5350
  }
5351
+ const record = { registeredServers: succeeded };
4847
5352
  if (failed.length === 0) {
4848
5353
  return {
4849
5354
  status: "success",
4850
- message: succeeded.length > 0 ? succeeded.join(", ") : void 0
5355
+ message: succeeded.length > 0 ? succeeded.join(", ") : void 0,
5356
+ record
4851
5357
  };
4852
5358
  }
4853
5359
  const failedSummary = failed.map((f) => `${f.name} (${f.reason})`).join("; ");
4854
5360
  if (succeeded.length === 0) {
4855
- return { status: "failed", detail: failedSummary };
5361
+ return { status: "failed", detail: failedSummary, record };
4856
5362
  }
4857
5363
  return {
4858
5364
  status: "success",
4859
5365
  message: `${succeeded.join(", ")}; failed: ${failed.map((f) => f.name).join(", ")}`,
4860
- detail: `Some registrations failed: ${failedSummary}`
5366
+ detail: `Some registrations failed: ${failedSummary}`,
5367
+ record
4861
5368
  };
5369
+ },
5370
+ inverse: {
5371
+ label: "Unregistering MCP servers from Claude Code",
5372
+ execute: async (raw) => {
5373
+ const rec = raw ?? {};
5374
+ const servers = rec.registeredServers ?? [];
5375
+ if (servers.length === 0) return { status: "skipped", detail: "nothing was registered" };
5376
+ if (!isClaudeCliAvailable()) {
5377
+ return {
5378
+ status: "skipped",
5379
+ detail: "`claude` CLI not found \u2014 leftover global registrations: " + servers.join(", ")
5380
+ };
5381
+ }
5382
+ const removed = [];
5383
+ const failed = [];
5384
+ for (const name of servers) {
5385
+ const r = (0, import_child_process3.spawnSync)(`claude mcp remove ${name}`, {
5386
+ shell: true,
5387
+ stdio: "pipe",
5388
+ encoding: "utf-8"
5389
+ });
5390
+ if (r.status === 0) removed.push(name);
5391
+ else failed.push(name);
5392
+ }
5393
+ return {
5394
+ status: "success",
5395
+ message: removed.length > 0 ? removed.join(", ") : "none",
5396
+ ...failed.length > 0 ? { detail: `also tried (already removed?): ${failed.join(", ")}` } : {}
5397
+ };
5398
+ }
4862
5399
  }
4863
5400
  });
4864
5401
  function isClaudeCliAvailable() {
@@ -4867,7 +5404,8 @@ function isClaudeCliAvailable() {
4867
5404
  }
4868
5405
 
4869
5406
  // src/steps/setup/update-gitignore.ts
4870
- var import_path11 = require("path");
5407
+ var import_fs13 = require("fs");
5408
+ var import_path13 = require("path");
4871
5409
  var MARKER_START2 = "# one-shot-installer:start";
4872
5410
  var MARKER_END2 = "# one-shot-installer:end";
4873
5411
  var CORE_GITIGNORE_ENTRIES = [
@@ -4888,15 +5426,37 @@ var update_gitignore_default = defineStep({
4888
5426
  name: "update-gitignore",
4889
5427
  label: "Updating .gitignore",
4890
5428
  execute: async (ctx) => {
4891
- const gitignorePath = (0, import_path11.join)(ctx.config.projectPath, ".gitignore");
5429
+ const gitignorePath = (0, import_path13.join)(ctx.config.projectPath, ".gitignore");
5430
+ const fileExistedBefore = (0, import_fs13.existsSync)(gitignorePath);
4892
5431
  upsertMarkerBlock(gitignorePath, MARKER_START2, MARKER_END2, CORE_GITIGNORE_ENTRIES.join("\n"));
4893
- return { status: "success" };
5432
+ const record = {
5433
+ filePath: ".gitignore",
5434
+ start: MARKER_START2,
5435
+ end: MARKER_END2,
5436
+ fileExistedBefore
5437
+ };
5438
+ return { status: "success", record };
5439
+ },
5440
+ inverse: {
5441
+ label: "Restoring .gitignore",
5442
+ execute: async (raw, ctx) => {
5443
+ const rec = raw ?? {};
5444
+ const filePath = rec.filePath ?? ".gitignore";
5445
+ const abs = resolveProjectPath(filePath, ctx.config.projectPath);
5446
+ removeMarkerBlock(
5447
+ abs,
5448
+ rec.start ?? MARKER_START2,
5449
+ rec.end ?? MARKER_END2,
5450
+ { deleteFileIfEmpty: !rec.fileExistedBefore }
5451
+ );
5452
+ return { status: "success" };
5453
+ }
4894
5454
  }
4895
5455
  });
4896
5456
 
4897
5457
  // src/steps/setup/write-state.ts
4898
- var import_fs11 = require("fs");
4899
- var import_path12 = require("path");
5458
+ var import_fs14 = require("fs");
5459
+ var import_path14 = require("path");
4900
5460
  var import_os2 = require("os");
4901
5461
 
4902
5462
  // package.json
@@ -4930,7 +5490,7 @@ var write_state_default = defineStep({
4930
5490
  name: "write-state",
4931
5491
  label: "Saving installation state",
4932
5492
  execute: async (ctx) => {
4933
- const statePath = (0, import_path12.join)(ctx.config.projectPath, ".one-shot-state.json");
5493
+ const statePath = (0, import_path14.join)(ctx.config.projectPath, ".one-shot-state.json");
4934
5494
  const mcpServersAdded = ctx.installed.mcpServersAdded ?? [];
4935
5495
  const filteredMcpConfig = Object.fromEntries(
4936
5496
  Object.entries(ctx.config.mcpConfig).filter(([name]) => mcpServersAdded.includes(name))
@@ -4955,45 +5515,233 @@ var write_state_default = defineStep({
4955
5515
  contexts: "_ai-context/",
4956
5516
  factory: ctx.installed.factoryInstalled ? "factory/" : null,
4957
5517
  abapHooks: ctx.installed.abapHooksInstalled ? ".claude/hooks/" : null,
4958
- globalConfig: (0, import_path12.join)((0, import_os2.homedir)(), ".one-shot-installer")
5518
+ globalConfig: (0, import_path14.join)((0, import_os2.homedir)(), ".one-shot-installer")
4959
5519
  }
4960
5520
  };
4961
- (0, import_fs11.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
5521
+ if (ctx.journal) {
5522
+ ctx.journal.setSummary(state);
5523
+ } else {
5524
+ (0, import_fs14.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
5525
+ }
4962
5526
  return { status: "success" };
5527
+ },
5528
+ inverse: {
5529
+ label: "Clearing installation state",
5530
+ /* No-op during journal walk. The journal IS the state file, so
5531
+ * deleting it here would yank the rug from under runRollback's
5532
+ * own iteration. The uninstall orchestrator deletes the file as
5533
+ * the FINAL action, after all other inverses have completed. */
5534
+ execute: async () => ({ status: "skipped", detail: "handled by uninstall orchestrator" })
4963
5535
  }
4964
5536
  });
4965
5537
 
5538
+ // src/uninstall.ts
5539
+ var import_fs15 = require("fs");
5540
+ var import_readline = require("readline");
5541
+ var import_path15 = require("path");
5542
+ var dim = (text) => `\x1B[2m${text}\x1B[0m`;
5543
+ var yellow = (text) => `\x1B[33m${text}\x1B[0m`;
5544
+ var red3 = (text) => `\x1B[31m${text}\x1B[0m`;
5545
+ var green = (text) => `\x1B[32m${text}\x1B[0m`;
5546
+ function waitForEnter() {
5547
+ const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
5548
+ return new Promise((resolve6) => {
5549
+ rl.question(" Press Enter to close...", () => {
5550
+ rl.close();
5551
+ resolve6();
5552
+ });
5553
+ });
5554
+ }
5555
+ async function uninstall() {
5556
+ console.clear();
5557
+ console.log("One-Shot Setup Uninstaller\n");
5558
+ try {
5559
+ const projectInput = await esm_default4({
5560
+ message: "Project directory to uninstall from:",
5561
+ default: (0, import_path15.resolve)(process.cwd())
5562
+ });
5563
+ const projectPath = (0, import_path15.resolve)(projectInput);
5564
+ const statePath = (0, import_path15.join)(projectPath, ".one-shot-state.json");
5565
+ if (!(0, import_fs15.existsSync)(statePath)) {
5566
+ console.log("");
5567
+ console.log(red3(" No .one-shot-state.json found at:"));
5568
+ console.log(` ${statePath}`);
5569
+ console.log("");
5570
+ console.log(dim(" Nothing to uninstall \u2014 either this directory is not an installer"));
5571
+ console.log(dim(" target, or the state file has already been removed."));
5572
+ console.log("");
5573
+ await waitForEnter();
5574
+ return;
5575
+ }
5576
+ const journal = new Journal(statePath);
5577
+ journal.load();
5578
+ const entries = journal.getEntries();
5579
+ if (entries.length === 0) {
5580
+ console.log("");
5581
+ console.log(yellow(" State file found, but its rollback journal is empty."));
5582
+ console.log(dim(" This file was likely written by an older installer version that"));
5583
+ console.log(dim(" did not yet emit a journal. Manual cleanup is required."));
5584
+ console.log("");
5585
+ const proceed2 = await esm_default3({
5586
+ message: "Delete the state file anyway?",
5587
+ default: false
5588
+ });
5589
+ if (proceed2) {
5590
+ (0, import_fs15.unlinkSync)(statePath);
5591
+ console.log(" Removed .one-shot-state.json");
5592
+ }
5593
+ await waitForEnter();
5594
+ return;
5595
+ }
5596
+ const summary = readSummary(statePath);
5597
+ printPreview(projectPath, summary, entries.length);
5598
+ const proceed = await esm_default3({
5599
+ message: "Proceed with uninstall?",
5600
+ default: false
5601
+ });
5602
+ if (!proceed) {
5603
+ console.log("\nNo changes made.");
5604
+ await waitForEnter();
5605
+ return;
5606
+ }
5607
+ const ctx = {
5608
+ config: {
5609
+ technologies: [],
5610
+ agent: typeof summary?.agent === "string" ? summary.agent : "claude-code",
5611
+ azureDevOpsOrg: "",
5612
+ projectPath,
5613
+ baseMcpServers: [],
5614
+ mcpConfig: {},
5615
+ releaseVersion: "",
5616
+ installFactory: false,
5617
+ installAbapHooks: false
5618
+ },
5619
+ token: "",
5620
+ repo: "",
5621
+ contextRepo: null,
5622
+ factoryRepo: null,
5623
+ abapHooksRepo: null,
5624
+ installed: {},
5625
+ journal
5626
+ };
5627
+ const stepList = Object.values(steps_exports);
5628
+ console.log("");
5629
+ const result = await runRollback(stepList, ctx);
5630
+ try {
5631
+ if ((0, import_fs15.existsSync)(statePath)) (0, import_fs15.unlinkSync)(statePath);
5632
+ } catch {
5633
+ }
5634
+ printSummary(result);
5635
+ await waitForEnter();
5636
+ } catch (error) {
5637
+ console.error("\n[ERROR]", error instanceof Error ? error.message : String(error));
5638
+ await waitForEnter();
5639
+ }
5640
+ }
5641
+ function readSummary(statePath) {
5642
+ try {
5643
+ const raw = JSON.parse((0, import_fs15.readFileSync)(statePath, "utf-8"));
5644
+ return raw;
5645
+ } catch {
5646
+ return {};
5647
+ }
5648
+ }
5649
+ function asArray(v) {
5650
+ return Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
5651
+ }
5652
+ function printPreview(projectPath, summary, entryCount) {
5653
+ const agent = typeof summary.agent === "string" ? summary.agent : "unknown";
5654
+ const domains = asArray(summary.domains);
5655
+ const mcpServers = asArray(summary.mcpServers);
5656
+ const factoryInstalled = summary.factoryInstalled === true;
5657
+ const abapHooksInstalled = summary.abapHooksInstalled === true;
5658
+ const files = summary.files ?? {};
5659
+ console.log("\n" + "\u2500".repeat(48));
5660
+ console.log(" Ready to uninstall");
5661
+ console.log("\u2500".repeat(48) + "\n");
5662
+ console.log(` Directory ${projectPath}`);
5663
+ console.log(` Agent ${agent}`);
5664
+ console.log(` Journal ${entryCount} recorded step(s)`);
5665
+ console.log("");
5666
+ console.log(` ${dim("Will remove:")}`);
5667
+ if (domains.length > 0) {
5668
+ console.log(` _ai-context/ ${yellow(domains.join(", "))}`);
5669
+ }
5670
+ if (factoryInstalled && typeof files.factory === "string") {
5671
+ console.log(` ${files.factory.padEnd(16)}${yellow("remove")}`);
5672
+ }
5673
+ if (abapHooksInstalled && typeof files.abapHooks === "string") {
5674
+ console.log(` ${files.abapHooks.padEnd(16)}${yellow("strip our scripts")}`);
5675
+ }
5676
+ if (typeof files.instructions === "string") {
5677
+ console.log(` ${files.instructions.padEnd(16)}${yellow("remove our markers")}`);
5678
+ }
5679
+ if (typeof files.mcpConfig === "string" && mcpServers.length > 0) {
5680
+ console.log(` ${files.mcpConfig.padEnd(16)}${yellow(`remove ${mcpServers.length} server(s)`)}`);
5681
+ }
5682
+ console.log(` ${".gitignore".padEnd(16)}${yellow("remove our markers")}`);
5683
+ console.log(` ${".one-shot-state.json".padEnd(16)}${yellow("delete")}`);
5684
+ if (mcpServers.length > 0) {
5685
+ console.log("");
5686
+ console.log(` ${dim("MCP servers being removed:")}`);
5687
+ console.log(` ${mcpServers.join(", ")}`);
5688
+ }
5689
+ console.log("\n" + dim(" User-edited files outside our marker blocks will be preserved."));
5690
+ console.log(dim(" Files we created from scratch will be deleted; files we modified"));
5691
+ console.log(dim(" will have only our changes stripped."));
5692
+ console.log("\n" + "\u2500".repeat(48));
5693
+ }
5694
+ function printSummary(result) {
5695
+ const failed = result.entries.filter((e) => e.result.status === "failed");
5696
+ const succeeded = result.entries.filter((e) => e.result.status === "success");
5697
+ const skipped = result.entries.filter((e) => e.result.status === "skipped");
5698
+ console.log("\u2500".repeat(48));
5699
+ console.log(failed.length > 0 ? " Done (with errors).\n" : " Done.\n");
5700
+ for (const entry of succeeded) {
5701
+ const msg = entry.result.message ? ` ${dim(entry.result.message)}` : "";
5702
+ console.log(` ${green("\u2713")} ${entry.label}${msg}`);
5703
+ }
5704
+ for (const entry of skipped) {
5705
+ const detail = entry.result.detail ? ` ${dim(entry.result.detail)}` : "";
5706
+ console.log(` ${dim("\u2014")} ${entry.label}${detail}`);
5707
+ }
5708
+ for (const entry of failed) {
5709
+ console.log(` ${red3("\u2717")} ${entry.label}: ${entry.result.detail ?? "unknown"}`);
5710
+ }
5711
+ console.log("\n" + "\u2500".repeat(48) + "\n");
5712
+ }
5713
+
4966
5714
  // src/index.ts
4967
5715
  function formatMCPCommand(server) {
4968
5716
  if (server.url) return server.url;
4969
5717
  if (server.command) return `${server.command} ${(server.args ?? []).join(" ")}`.trimEnd();
4970
5718
  return "";
4971
5719
  }
4972
- var dim = (text) => `\x1B[2m${text}\x1B[0m`;
4973
- var yellow = (text) => `\x1B[33m${text}\x1B[0m`;
4974
- var green = (text) => `\x1B[32m${text}\x1B[0m`;
5720
+ var dim2 = (text) => `\x1B[2m${text}\x1B[0m`;
5721
+ var yellow2 = (text) => `\x1B[33m${text}\x1B[0m`;
5722
+ var green2 = (text) => `\x1B[32m${text}\x1B[0m`;
4975
5723
  function detectMarkerFileMode(filePath, markerStart) {
4976
- if (!(0, import_fs12.existsSync)(filePath)) return green("create");
4977
- const content = (0, import_fs12.readFileSync)(filePath, "utf-8");
4978
- if (content.includes(markerStart)) return yellow("update");
4979
- return yellow("append");
5724
+ if (!(0, import_fs16.existsSync)(filePath)) return green2("create");
5725
+ const content = (0, import_fs16.readFileSync)(filePath, "utf-8");
5726
+ if (content.includes(markerStart)) return yellow2("update");
5727
+ return yellow2("append");
4980
5728
  }
4981
5729
  function detectMCPFileMode(filePath) {
4982
- if (!(0, import_fs12.existsSync)(filePath)) return green("create");
4983
- return yellow("merge");
5730
+ if (!(0, import_fs16.existsSync)(filePath)) return green2("create");
5731
+ return yellow2("merge");
4984
5732
  }
4985
5733
  function detectContextMode(projectPath, domain) {
4986
- const contextDir = (0, import_path13.join)(projectPath, "_ai-context", domain.toUpperCase());
4987
- const contextDirLower = (0, import_path13.join)(projectPath, "_ai-context", domain);
4988
- if ((0, import_fs12.existsSync)(contextDir) || (0, import_fs12.existsSync)(contextDirLower)) return yellow("overwrite");
4989
- return green("create");
5734
+ const contextDir = (0, import_path16.join)(projectPath, "_ai-context", domain.toUpperCase());
5735
+ const contextDirLower = (0, import_path16.join)(projectPath, "_ai-context", domain);
5736
+ if ((0, import_fs16.existsSync)(contextDir) || (0, import_fs16.existsSync)(contextDirLower)) return yellow2("overwrite");
5737
+ return green2("create");
4990
5738
  }
4991
- function waitForEnter() {
4992
- const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
4993
- return new Promise((resolve4) => {
5739
+ function waitForEnter2() {
5740
+ const rl = (0, import_readline2.createInterface)({ input: process.stdin, output: process.stdout });
5741
+ return new Promise((resolve6) => {
4994
5742
  rl.question(" Press Enter to close...", () => {
4995
5743
  rl.close();
4996
- resolve4();
5744
+ resolve6();
4997
5745
  });
4998
5746
  });
4999
5747
  }
@@ -5022,15 +5770,18 @@ async function main() {
5022
5770
  const options = await loadWizardOptions(token, repo);
5023
5771
  const config = await collectInputs(options, options.releaseVersion, !!factoryRepo, !!abapHooksRepo);
5024
5772
  if (!config) {
5025
- await waitForEnter();
5773
+ await waitForEnter2();
5026
5774
  return;
5027
5775
  }
5028
5776
  const proceed = await previewAndConfirm(config, options);
5029
5777
  if (!proceed) {
5030
5778
  console.log("\nNo changes made.");
5031
- await waitForEnter();
5779
+ await waitForEnter2();
5032
5780
  return;
5033
5781
  }
5782
+ const statePath = (0, import_path16.join)(config.projectPath, ".one-shot-state.json");
5783
+ const journal = new Journal(statePath);
5784
+ journal.startFresh();
5034
5785
  const ctx = {
5035
5786
  config,
5036
5787
  token,
@@ -5038,16 +5789,17 @@ async function main() {
5038
5789
  contextRepo,
5039
5790
  factoryRepo,
5040
5791
  abapHooksRepo,
5041
- installed: {}
5792
+ installed: {},
5793
+ journal
5042
5794
  };
5043
5795
  const stepList = Object.values(steps_exports);
5044
5796
  console.log("");
5045
5797
  const result = await runPipeline(stepList, ctx);
5046
- printSummary(result, config);
5798
+ printSummary2(result, config);
5047
5799
  return;
5048
5800
  } catch (error) {
5049
5801
  console.error("\n[ERROR]", error instanceof Error ? error.message : String(error));
5050
- await waitForEnter();
5802
+ await waitForEnter2();
5051
5803
  }
5052
5804
  }
5053
5805
  async function collectInputs(options, releaseVersion, factoryAvailable = false, abapHooksAvailable = false) {
@@ -5090,7 +5842,7 @@ async function collectInputs(options, releaseVersion, factoryAvailable = false,
5090
5842
  }
5091
5843
  const projectInput = await esm_default4({
5092
5844
  message: "Project directory:",
5093
- default: (0, import_path13.resolve)(process.cwd())
5845
+ default: (0, import_path16.resolve)(process.cwd())
5094
5846
  });
5095
5847
  const isAbapSelected = selectedTechnologies.some((t) => t.domains.includes("ABAP"));
5096
5848
  const installAbapHooks = abapHooksAvailable && agent === "claude-code" && isAbapSelected;
@@ -5098,7 +5850,7 @@ async function collectInputs(options, releaseVersion, factoryAvailable = false,
5098
5850
  technologies: selectedTechnologies,
5099
5851
  agent,
5100
5852
  azureDevOpsOrg,
5101
- projectPath: (0, import_path13.resolve)(projectInput),
5853
+ projectPath: (0, import_path16.resolve)(projectInput),
5102
5854
  baseMcpServers: options.baseMcpServers,
5103
5855
  mcpConfig,
5104
5856
  releaseVersion,
@@ -5113,9 +5865,9 @@ async function previewAndConfirm(config, options) {
5113
5865
  const instructionFile = target.instructions;
5114
5866
  const mcpConfigFile = target.mcpConfig;
5115
5867
  const serverEntries = Object.entries(config.mcpConfig);
5116
- const instructionFilePath = (0, import_path13.join)(config.projectPath, instructionFile);
5117
- const mcpConfigFilePath = (0, import_path13.join)(config.projectPath, mcpConfigFile);
5118
- const gitignorePath = (0, import_path13.join)(config.projectPath, ".gitignore");
5868
+ const instructionFilePath = (0, import_path16.join)(config.projectPath, instructionFile);
5869
+ const mcpConfigFilePath = (0, import_path16.join)(config.projectPath, mcpConfigFile);
5870
+ const gitignorePath = (0, import_path16.join)(config.projectPath, ".gitignore");
5119
5871
  const instructionMode = detectMarkerFileMode(instructionFilePath, "<!-- one-shot-installer:start -->");
5120
5872
  const mcpMode = detectMCPFileMode(mcpConfigFilePath);
5121
5873
  const gitignoreMode = detectMarkerFileMode(gitignorePath, "# one-shot-installer:start");
@@ -5133,7 +5885,7 @@ async function previewAndConfirm(config, options) {
5133
5885
  console.log(` ABAP hooks ${config.installAbapHooks ? "yes" : "no"}`);
5134
5886
  console.log(` Directory ${config.projectPath}`);
5135
5887
  console.log("");
5136
- console.log(` ${dim("File actions:")}`);
5888
+ console.log(` ${dim2("File actions:")}`);
5137
5889
  const domainColWidth = Math.max(...domainModes.map((d) => d.domain.length), 6) + 2;
5138
5890
  for (const { domain, mode } of domainModes) {
5139
5891
  console.log(` _ai-context/${domain.padEnd(domainColWidth)}${mode}`);
@@ -5142,18 +5894,18 @@ async function previewAndConfirm(config, options) {
5142
5894
  console.log(` ${mcpConfigFile.padEnd(domainColWidth + 14)}${mcpMode}`);
5143
5895
  console.log(` ${".gitignore".padEnd(domainColWidth + 14)}${gitignoreMode}`);
5144
5896
  if (config.installFactory) {
5145
- const factoryExists = (0, import_fs12.existsSync)((0, import_path13.join)(config.projectPath, "factory"));
5146
- const factoryMode = factoryExists ? yellow("overwrite") : green("create");
5897
+ const factoryExists = (0, import_fs16.existsSync)((0, import_path16.join)(config.projectPath, "factory"));
5898
+ const factoryMode = factoryExists ? yellow2("overwrite") : green2("create");
5147
5899
  console.log(` ${"factory/".padEnd(domainColWidth + 14)}${factoryMode}`);
5148
5900
  }
5149
5901
  if (config.installAbapHooks) {
5150
- const hooksDir = (0, import_path13.join)(config.projectPath, ".claude", "hooks");
5151
- const hooksMode = (0, import_fs12.existsSync)(hooksDir) ? yellow("merge") : green("create");
5902
+ const hooksDir = (0, import_path16.join)(config.projectPath, ".claude", "hooks");
5903
+ const hooksMode = (0, import_fs16.existsSync)(hooksDir) ? yellow2("merge") : green2("create");
5152
5904
  console.log(` ${".claude/hooks/".padEnd(domainColWidth + 14)}${hooksMode}`);
5153
5905
  }
5154
5906
  if (serverEntries.length > 0) {
5155
5907
  console.log("");
5156
- console.log(` ${dim("MCP servers:")}`);
5908
+ console.log(` ${dim2("MCP servers:")}`);
5157
5909
  const maxLen = Math.max(...serverEntries.map(([name]) => name.length));
5158
5910
  for (const [name, server] of serverEntries) {
5159
5911
  const cmd = formatMCPCommand(server);
@@ -5163,7 +5915,7 @@ async function previewAndConfirm(config, options) {
5163
5915
  console.log("\n" + "\u2500".repeat(48));
5164
5916
  return esm_default3({ message: "Proceed?", default: true });
5165
5917
  }
5166
- function printSummary(result, config) {
5918
+ function printSummary2(result, config) {
5167
5919
  const failed = result.entries.filter((e) => e.result.status === "failed");
5168
5920
  const succeeded = result.entries.filter((e) => e.result.status === "success");
5169
5921
  const skipped = result.entries.filter((e) => e.result.status === "skipped");
@@ -5191,7 +5943,34 @@ function printSummary(result, config) {
5191
5943
  }
5192
5944
  console.log("\n" + "\u2500".repeat(48) + "\n");
5193
5945
  }
5194
- main().catch((error) => {
5946
+ function printHelp() {
5947
+ console.log("");
5948
+ console.log("Usage:");
5949
+ console.log(" npx dlw-machine-setup Install the toolchain (wizard).");
5950
+ console.log(" npx dlw-machine-setup uninstall Remove a previous install (wizard).");
5951
+ console.log(" npx dlw-machine-setup --help Show this help.");
5952
+ console.log("");
5953
+ }
5954
+ async function dispatch() {
5955
+ const subcommand = process.argv[2];
5956
+ switch (subcommand) {
5957
+ case void 0:
5958
+ case "":
5959
+ return main();
5960
+ case "uninstall":
5961
+ return uninstall();
5962
+ case "help":
5963
+ case "--help":
5964
+ case "-h":
5965
+ printHelp();
5966
+ return;
5967
+ default:
5968
+ console.error(`Unknown subcommand: ${subcommand}`);
5969
+ printHelp();
5970
+ process.exit(1);
5971
+ }
5972
+ }
5973
+ dispatch().catch((error) => {
5195
5974
  console.error("\n[ERROR]", error instanceof Error ? error.message : String(error));
5196
5975
  process.exit(1);
5197
5976
  });