dlw-machine-setup 0.8.10 → 0.9.0

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