dlw-machine-setup 0.8.1 → 0.8.2

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 (2) hide show
  1. package/bin/installer.js +237 -62
  2. package/package.json +1 -1
package/bin/installer.js CHANGED
@@ -3244,9 +3244,9 @@ ${page}${helpTipBottom}${choiceDescription}${import_ansi_escapes3.default.cursor
3244
3244
  });
3245
3245
 
3246
3246
  // src/index.ts
3247
- var import_fs11 = require("fs");
3247
+ var import_fs12 = require("fs");
3248
3248
  var import_readline = require("readline");
3249
- var import_path12 = require("path");
3249
+ var import_path13 = require("path");
3250
3250
 
3251
3251
  // src/utils/fetch.ts
3252
3252
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -3324,6 +3324,25 @@ async function fetchLatestRelease(token, repo) {
3324
3324
  const data = await res.json();
3325
3325
  return { tagName: data.tag_name ?? "unknown", assets: data.assets ?? [] };
3326
3326
  }
3327
+ async function fetchLatestReleaseByTagPrefix(token, repo, tagPrefix) {
3328
+ const headers = {
3329
+ "Accept": "application/vnd.github+json",
3330
+ "Authorization": `Bearer ${token}`
3331
+ };
3332
+ const res = await fetchWithRetry(
3333
+ `https://api.github.com/repos/${repo}/releases?per_page=100`,
3334
+ { headers }
3335
+ );
3336
+ if (!res.ok) {
3337
+ throw new Error(`GitHub API error (${res.status}): ${getReadableError(res.status)}`);
3338
+ }
3339
+ const releases = await res.json();
3340
+ const match = releases.find((r) => typeof r.tag_name === "string" && r.tag_name.startsWith(tagPrefix));
3341
+ if (!match) {
3342
+ throw new Error(`No release with tag prefix "${tagPrefix}" found in ${repo}`);
3343
+ }
3344
+ return { tagName: match.tag_name ?? "unknown", assets: match.assets ?? [] };
3345
+ }
3327
3346
  async function downloadAndExtractAsset(token, asset, projectPath, tempDirName) {
3328
3347
  const tarCheck = (0, import_child_process.spawnSync)("tar", ["--version"], { stdio: "ignore" });
3329
3348
  if (tarCheck.status !== 0) {
@@ -3738,6 +3757,7 @@ async function runPipeline(steps, ctx) {
3738
3757
  // src/steps/index.ts
3739
3758
  var steps_exports = {};
3740
3759
  __export(steps_exports, {
3760
+ fetchAbapHooks: () => fetch_abap_hooks_default,
3741
3761
  fetchContexts: () => fetch_contexts_default,
3742
3762
  fetchFactory: () => fetch_factory_default,
3743
3763
  runMcpInstallCommands: () => run_mcp_install_commands_default,
@@ -4330,9 +4350,140 @@ async function installViaBundle(manifestPath, extractedPath, targetDir, agent, r
4330
4350
  result.filesInstalled = runResult.filesTouched;
4331
4351
  }
4332
4352
 
4333
- // src/steps/setup/write-instructions.ts
4353
+ // src/steps/resources/fetch-abap-hooks.ts
4334
4354
  var import_fs8 = require("fs");
4335
4355
  var import_path8 = require("path");
4356
+ var HOOKS_ASSET_NAME = "sap-hooks.tar.gz";
4357
+ var HOOKS_TAG_PREFIX = "hooks-v";
4358
+ var HOOKS_ROOT_FOLDER = "hooks";
4359
+ var BUNDLE_NAME = "abap-mcp-hooks";
4360
+ var HOOK_DEFINITIONS = [
4361
+ { event: "PreToolUse", script: "sap-safety-gate.mjs" },
4362
+ { event: "PostToolUse", script: "telemetry-post-tool.mjs" },
4363
+ { event: "PostToolUseFailure", script: "telemetry-post-tool-failure.mjs" },
4364
+ { event: "SessionStart", script: "telemetry-session-start.mjs" },
4365
+ { event: "SessionEnd", script: "telemetry-session-end.mjs" }
4366
+ ];
4367
+ var INSTRUCTIONS_SNIPPET = [
4368
+ `## SAP ABAP MCP Hooks`,
4369
+ ``,
4370
+ `Installed by One-Shot Installer. Hook scripts live in \`.claude/hooks/\` and are wired into \`.claude/settings.json\`.`,
4371
+ ``,
4372
+ `**What they do:**`,
4373
+ `- \`sap-safety-gate.mjs\` (PreToolUse) \u2014 blocks \`delete\` (any tool) and transport \`release\`.`,
4374
+ `- \`telemetry-*.mjs\` (PostToolUse, PostToolUseFailure, SessionStart, SessionEnd) \u2014 emit usage events.`,
4375
+ ``,
4376
+ `**Optional environment variables:**`,
4377
+ `- \`SAP_MCP_TELEMETRY_URL\` \u2014 base URL of the MCP telemetry endpoint (e.g. \`https://mcp.<cluster>.kyma.ondemand.com\`). When unset, telemetry is silently skipped; the safety gate still blocks.`,
4378
+ `- \`SAP_MCP_TELEMETRY_TOKEN\` \u2014 Bearer token for the telemetry endpoint.`,
4379
+ ``,
4380
+ `On network/HTTP failure, telemetry events fall back to \`~/.claude/telemetry/events.jsonl\`.`,
4381
+ ``
4382
+ ].join("\n");
4383
+ var fetch_abap_hooks_default = defineStep({
4384
+ name: "fetch-abap-hooks",
4385
+ label: "Installing ABAP MCP hooks",
4386
+ /* All eligibility (Claude-only + ABAP selected + repo discovered) is
4387
+ * resolved upfront in collectInputs() and frozen on InstallConfig.
4388
+ * Reading the single flag keeps the guard, the preview, and the
4389
+ * step in sync with no scattered conditionals. */
4390
+ when: (ctx) => ctx.config.installAbapHooks,
4391
+ execute: async (ctx) => {
4392
+ const result = await fetchAbapHooks(
4393
+ ctx.token,
4394
+ ctx.abapHooksRepo,
4395
+ ctx.config.projectPath,
4396
+ ctx.config.agent
4397
+ );
4398
+ ctx.installed.abapHooksInstalled = result.success;
4399
+ if (result.instructionsSnippet) {
4400
+ ctx.installed.abapHooksInstructionsSnippet = result.instructionsSnippet;
4401
+ }
4402
+ if (!result.success) {
4403
+ return { status: "failed", detail: result.failureReason };
4404
+ }
4405
+ return { status: "success", message: result.filesInstalled.join(", ") };
4406
+ }
4407
+ });
4408
+ async function fetchAbapHooks(token, repo, targetDir, agent) {
4409
+ const result = { success: false, filesInstalled: [] };
4410
+ let release;
4411
+ try {
4412
+ release = await fetchLatestReleaseByTagPrefix(token, repo, HOOKS_TAG_PREFIX);
4413
+ } catch (err) {
4414
+ result.failureReason = err instanceof Error ? err.message : String(err);
4415
+ return result;
4416
+ }
4417
+ const asset = release.assets.find((a) => a.name === HOOKS_ASSET_NAME);
4418
+ if (!asset) {
4419
+ result.failureReason = `${HOOKS_ASSET_NAME} not found in release ${release.tagName}`;
4420
+ return result;
4421
+ }
4422
+ let archive = null;
4423
+ try {
4424
+ archive = await downloadAndExtractAsset(token, asset, targetDir, ".temp-abap-hooks-download");
4425
+ const extractedEntries = (0, import_fs8.readdirSync)(archive.extractedRoot);
4426
+ const innerFolder = extractedEntries.find(
4427
+ (e) => e.toLowerCase() === HOOKS_ROOT_FOLDER.toLowerCase()
4428
+ );
4429
+ if (!innerFolder) {
4430
+ result.failureReason = `No ${HOOKS_ROOT_FOLDER}/ folder in archive`;
4431
+ return result;
4432
+ }
4433
+ const bundleRoot = (0, import_path8.join)(archive.extractedRoot, innerFolder);
4434
+ if (!(0, import_fs8.statSync)(bundleRoot).isDirectory()) {
4435
+ result.failureReason = `${HOOKS_ROOT_FOLDER}/ entry is not a directory`;
4436
+ return result;
4437
+ }
4438
+ const presentDefs = HOOK_DEFINITIONS.filter((d) => (0, import_fs8.existsSync)((0, import_path8.join)(bundleRoot, d.script)));
4439
+ if (presentDefs.length === 0) {
4440
+ result.failureReason = "Archive contains no recognized hook scripts";
4441
+ return result;
4442
+ }
4443
+ const assets = [
4444
+ ...presentDefs.map((d) => ({
4445
+ type: "hook",
4446
+ event: d.event,
4447
+ /* {hookDir} is substituted by run-bundle.ts with the agent
4448
+ * profile's scriptDir (.claude/hooks for Claude Code), so the
4449
+ * resulting command is exactly `node .claude/hooks/<script>`,
4450
+ * matching the form Claude Code already uses for Factory hooks. */
4451
+ command: `node {hookDir}/${d.script}`,
4452
+ script: d.script
4453
+ })),
4454
+ { type: "instructions-snippet", content: INSTRUCTIONS_SNIPPET }
4455
+ ];
4456
+ const manifest = {
4457
+ schemaVersion: 2,
4458
+ name: BUNDLE_NAME,
4459
+ assets
4460
+ };
4461
+ const target = getAgentTarget(agent);
4462
+ const runResult = await runBundle(manifest, {
4463
+ bundleRoot,
4464
+ projectPath: targetDir,
4465
+ agent,
4466
+ instructionsFile: target.instructions,
4467
+ /* write-instructions.ts owns the ordering of marker blocks in
4468
+ * CLAUDE.md (one-shot-installer:start/end first, bundle blocks
4469
+ * after). Skipping here means we hand the snippet back; it gets
4470
+ * committed when write-instructions.ts runs in the setup phase. */
4471
+ skipInstructions: true
4472
+ });
4473
+ result.filesInstalled = runResult.filesTouched;
4474
+ result.instructionsSnippet = runResult.instructionsSnippet;
4475
+ result.success = true;
4476
+ } catch (error) {
4477
+ result.failureReason = error instanceof Error ? error.message : String(error);
4478
+ } finally {
4479
+ archive?.cleanup();
4480
+ }
4481
+ return result;
4482
+ }
4483
+
4484
+ // src/steps/setup/write-instructions.ts
4485
+ var import_fs9 = require("fs");
4486
+ var import_path9 = require("path");
4336
4487
 
4337
4488
  // src/steps/shared.ts
4338
4489
  var cached = null;
@@ -4359,7 +4510,7 @@ var red = (text) => `\x1B[31m${text}\x1B[0m`;
4359
4510
  var write_instructions_default = defineStep({
4360
4511
  name: "write-instructions",
4361
4512
  label: "Writing instruction file",
4362
- when: (ctx) => (ctx.installed.domainsInstalled?.length ?? 0) > 0 || Object.keys(ctx.config.mcpConfig).length > 0 || !!ctx.installed.factoryInstructionsSnippet,
4513
+ when: (ctx) => (ctx.installed.domainsInstalled?.length ?? 0) > 0 || Object.keys(ctx.config.mcpConfig).length > 0 || !!ctx.installed.factoryInstructionsSnippet || !!ctx.installed.abapHooksInstructionsSnippet,
4363
4514
  execute: async (ctx) => {
4364
4515
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4365
4516
  const domains = ctx.installed.domainsInstalled ?? [];
@@ -4367,11 +4518,11 @@ var write_instructions_default = defineStep({
4367
4518
  const projectPath = ctx.config.projectPath;
4368
4519
  const content = buildCombinedInstructions(domains, filteredMcpConfig, agent, projectPath);
4369
4520
  const target = getAgentTarget(agent);
4370
- const filePath = (0, import_path8.join)(projectPath, target.instructions);
4371
- const fileDir = (0, import_path8.dirname)(filePath);
4372
- if (!(0, import_fs8.existsSync)(fileDir)) (0, import_fs8.mkdirSync)(fileDir, { recursive: true });
4373
- if (agent === "cursor" && !(0, import_fs8.existsSync)(filePath)) {
4374
- (0, import_fs8.writeFileSync)(filePath, `---
4521
+ const filePath = (0, import_path9.join)(projectPath, target.instructions);
4522
+ const fileDir = (0, import_path9.dirname)(filePath);
4523
+ if (!(0, import_fs9.existsSync)(fileDir)) (0, import_fs9.mkdirSync)(fileDir, { recursive: true });
4524
+ if (agent === "cursor" && !(0, import_fs9.existsSync)(filePath)) {
4525
+ (0, import_fs9.writeFileSync)(filePath, `---
4375
4526
  description: AI development instructions from One-Shot Installer
4376
4527
  alwaysApply: true
4377
4528
  ---
@@ -4406,20 +4557,28 @@ alwaysApply: true
4406
4557
  ctx.installed.factoryInstructionsSnippet
4407
4558
  );
4408
4559
  }
4560
+ if (ctx.installed.abapHooksInstructionsSnippet) {
4561
+ upsertMarkerBlock(
4562
+ filePath,
4563
+ "<!-- abap-mcp-hooks:start -->",
4564
+ "<!-- abap-mcp-hooks:end -->",
4565
+ ctx.installed.abapHooksInstructionsSnippet
4566
+ );
4567
+ }
4409
4568
  return { status: "success", message: target.instructions };
4410
4569
  }
4411
4570
  });
4412
4571
  function collectMdFiles(dir) {
4413
- if (!(0, import_fs8.existsSync)(dir)) return [];
4414
- const entries = (0, import_fs8.readdirSync)(dir, { recursive: true });
4572
+ if (!(0, import_fs9.existsSync)(dir)) return [];
4573
+ const entries = (0, import_fs9.readdirSync)(dir, { recursive: true });
4415
4574
  return entries.filter((entry) => {
4416
- const fullPath = (0, import_path8.join)(dir, entry);
4417
- return entry.endsWith(".md") && (0, import_fs8.statSync)(fullPath).isFile();
4575
+ const fullPath = (0, import_path9.join)(dir, entry);
4576
+ return entry.endsWith(".md") && (0, import_fs9.statSync)(fullPath).isFile();
4418
4577
  }).map((entry) => entry.replace(/\\/g, "/")).sort();
4419
4578
  }
4420
4579
  function extractFirstHeading(filePath) {
4421
4580
  try {
4422
- const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
4581
+ const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
4423
4582
  const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, "");
4424
4583
  const match = withoutFrontmatter.match(/^#\s+(.+)/m);
4425
4584
  if (match) {
@@ -4437,16 +4596,16 @@ function formatPathRef(contextPath, description, agent) {
4437
4596
  return `- \`${contextPath}\` \u2014 ${description}`;
4438
4597
  }
4439
4598
  function resolveDomainFolder(domain, contextsDir) {
4440
- if ((0, import_fs8.existsSync)(contextsDir)) {
4599
+ if ((0, import_fs9.existsSync)(contextsDir)) {
4441
4600
  try {
4442
- const entries = (0, import_fs8.readdirSync)(contextsDir);
4601
+ const entries = (0, import_fs9.readdirSync)(contextsDir);
4443
4602
  const match = entries.find((e) => e.toLowerCase() === domain.toLowerCase());
4444
- if (match) return { folderName: match, folderPath: (0, import_path8.join)(contextsDir, match) };
4603
+ if (match) return { folderName: match, folderPath: (0, import_path9.join)(contextsDir, match) };
4445
4604
  } catch {
4446
4605
  }
4447
4606
  }
4448
4607
  const fallback = domain.toUpperCase();
4449
- return { folderName: fallback, folderPath: (0, import_path8.join)(contextsDir, fallback) };
4608
+ return { folderName: fallback, folderPath: (0, import_path9.join)(contextsDir, fallback) };
4450
4609
  }
4451
4610
  function buildContextRefsSection(domains, agent, contextsDir) {
4452
4611
  const lines2 = [
@@ -4458,34 +4617,34 @@ function buildContextRefsSection(domains, agent, contextsDir) {
4458
4617
  let hasAnyFiles = false;
4459
4618
  for (const domain of domains) {
4460
4619
  const { folderName, folderPath: domainPath } = resolveDomainFolder(domain, contextsDir);
4461
- if (!(0, import_fs8.existsSync)(domainPath)) continue;
4620
+ if (!(0, import_fs9.existsSync)(domainPath)) continue;
4462
4621
  const domainFiles = [];
4463
- const ctxInstructions = (0, import_path8.join)(domainPath, "context-instructions.md");
4464
- if ((0, import_fs8.existsSync)(ctxInstructions)) {
4622
+ const ctxInstructions = (0, import_path9.join)(domainPath, "context-instructions.md");
4623
+ if ((0, import_fs9.existsSync)(ctxInstructions)) {
4465
4624
  const desc = extractFirstHeading(ctxInstructions);
4466
4625
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/context-instructions.md`, desc, agent));
4467
4626
  }
4468
- const instructionsMd = (0, import_path8.join)(domainPath, "core", "instructions.md");
4469
- if ((0, import_fs8.existsSync)(instructionsMd)) {
4627
+ const instructionsMd = (0, import_path9.join)(domainPath, "core", "instructions.md");
4628
+ if ((0, import_fs9.existsSync)(instructionsMd)) {
4470
4629
  const desc = extractFirstHeading(instructionsMd);
4471
4630
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/instructions.md`, `${desc} (start here)`, agent));
4472
4631
  }
4473
- const coreDir = (0, import_path8.join)(domainPath, "core");
4474
- if ((0, import_fs8.existsSync)(coreDir)) {
4632
+ const coreDir = (0, import_path9.join)(domainPath, "core");
4633
+ if ((0, import_fs9.existsSync)(coreDir)) {
4475
4634
  const coreFiles = collectMdFiles(coreDir).filter((f) => f !== "instructions.md" && !f.startsWith("instructions/"));
4476
4635
  for (const file of coreFiles) {
4477
- const desc = extractFirstHeading((0, import_path8.join)(coreDir, file));
4636
+ const desc = extractFirstHeading((0, import_path9.join)(coreDir, file));
4478
4637
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/${file}`, desc, agent));
4479
4638
  }
4480
4639
  }
4481
- const refDir = (0, import_path8.join)(domainPath, "reference");
4482
- if ((0, import_fs8.existsSync)(refDir)) {
4640
+ const refDir = (0, import_path9.join)(domainPath, "reference");
4641
+ if ((0, import_fs9.existsSync)(refDir)) {
4483
4642
  const refFiles = collectMdFiles(refDir);
4484
4643
  if (refFiles.length > 0) {
4485
4644
  domainFiles.push(``);
4486
4645
  domainFiles.push(`**Reference & cheat sheets:**`);
4487
4646
  for (const file of refFiles) {
4488
- const desc = extractFirstHeading((0, import_path8.join)(refDir, file));
4647
+ const desc = extractFirstHeading((0, import_path9.join)(refDir, file));
4489
4648
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/reference/${file}`, desc, agent));
4490
4649
  }
4491
4650
  }
@@ -4516,7 +4675,7 @@ function buildMCPSection(mcpConfig) {
4516
4675
  return lines2.join("\n");
4517
4676
  }
4518
4677
  function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4519
- const contextsDir = (0, import_path8.join)(projectPath, "_ai-context");
4678
+ const contextsDir = (0, import_path9.join)(projectPath, "_ai-context");
4520
4679
  const lines2 = [`# AI Development Instructions`, ``, `> Generated by One-Shot Installer`, ``];
4521
4680
  lines2.push(buildContextRefsSection(domains, agent, contextsDir));
4522
4681
  if (Object.keys(mcpConfig).length > 0) lines2.push(buildMCPSection(mcpConfig));
@@ -4524,8 +4683,8 @@ function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4524
4683
  }
4525
4684
 
4526
4685
  // src/steps/setup/write-mcp-config.ts
4527
- var import_fs9 = require("fs");
4528
- var import_path9 = require("path");
4686
+ var import_fs10 = require("fs");
4687
+ var import_path10 = require("path");
4529
4688
  var red2 = (text) => `\x1B[31m${text}\x1B[0m`;
4530
4689
  var write_mcp_config_default = defineStep({
4531
4690
  name: "write-mcp-config",
@@ -4534,15 +4693,15 @@ var write_mcp_config_default = defineStep({
4534
4693
  execute: async (ctx) => {
4535
4694
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4536
4695
  const target = getAgentTarget(ctx.config.agent);
4537
- const mcpJsonPath = (0, import_path9.join)(ctx.config.projectPath, target.mcpConfig);
4696
+ const mcpJsonPath = (0, import_path10.join)(ctx.config.projectPath, target.mcpConfig);
4538
4697
  if (target.mcpDir) {
4539
- const dir = (0, import_path9.join)(ctx.config.projectPath, target.mcpDir);
4540
- if (!(0, import_fs9.existsSync)(dir)) (0, import_fs9.mkdirSync)(dir, { recursive: true });
4698
+ const dir = (0, import_path10.join)(ctx.config.projectPath, target.mcpDir);
4699
+ if (!(0, import_fs10.existsSync)(dir)) (0, import_fs10.mkdirSync)(dir, { recursive: true });
4541
4700
  }
4542
4701
  let existingFile = {};
4543
- if ((0, import_fs9.existsSync)(mcpJsonPath)) {
4702
+ if ((0, import_fs10.existsSync)(mcpJsonPath)) {
4544
4703
  try {
4545
- existingFile = JSON.parse((0, import_fs9.readFileSync)(mcpJsonPath, "utf-8"));
4704
+ existingFile = JSON.parse((0, import_fs10.readFileSync)(mcpJsonPath, "utf-8"));
4546
4705
  } catch {
4547
4706
  const box = [
4548
4707
  "",
@@ -4568,7 +4727,7 @@ var write_mcp_config_default = defineStep({
4568
4727
  }
4569
4728
  const mergedServers = { ...existingServers, ...newServers };
4570
4729
  const outputFile = { ...existingFile, [target.mcpRootKey]: mergedServers };
4571
- (0, import_fs9.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
4730
+ (0, import_fs10.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
4572
4731
  ctx.installed.mcpServersAdded = addedServers;
4573
4732
  return {
4574
4733
  status: "success",
@@ -4637,7 +4796,7 @@ function isClaudeCliAvailable() {
4637
4796
  }
4638
4797
 
4639
4798
  // src/steps/setup/update-gitignore.ts
4640
- var import_path10 = require("path");
4799
+ var import_path11 = require("path");
4641
4800
  var MARKER_START2 = "# one-shot-installer:start";
4642
4801
  var MARKER_END2 = "# one-shot-installer:end";
4643
4802
  var CORE_GITIGNORE_ENTRIES = [
@@ -4658,15 +4817,15 @@ var update_gitignore_default = defineStep({
4658
4817
  name: "update-gitignore",
4659
4818
  label: "Updating .gitignore",
4660
4819
  execute: async (ctx) => {
4661
- const gitignorePath = (0, import_path10.join)(ctx.config.projectPath, ".gitignore");
4820
+ const gitignorePath = (0, import_path11.join)(ctx.config.projectPath, ".gitignore");
4662
4821
  upsertMarkerBlock(gitignorePath, MARKER_START2, MARKER_END2, CORE_GITIGNORE_ENTRIES.join("\n"));
4663
4822
  return { status: "success" };
4664
4823
  }
4665
4824
  });
4666
4825
 
4667
4826
  // src/steps/setup/write-state.ts
4668
- var import_fs10 = require("fs");
4669
- var import_path11 = require("path");
4827
+ var import_fs11 = require("fs");
4828
+ var import_path12 = require("path");
4670
4829
  var import_os2 = require("os");
4671
4830
 
4672
4831
  // package.json
@@ -4700,7 +4859,7 @@ var write_state_default = defineStep({
4700
4859
  name: "write-state",
4701
4860
  label: "Saving installation state",
4702
4861
  execute: async (ctx) => {
4703
- const statePath = (0, import_path11.join)(ctx.config.projectPath, ".one-shot-state.json");
4862
+ const statePath = (0, import_path12.join)(ctx.config.projectPath, ".one-shot-state.json");
4704
4863
  const mcpServersAdded = ctx.installed.mcpServersAdded ?? [];
4705
4864
  const filteredMcpConfig = Object.fromEntries(
4706
4865
  Object.entries(ctx.config.mcpConfig).filter(([name]) => mcpServersAdded.includes(name))
@@ -4718,15 +4877,17 @@ var write_state_default = defineStep({
4718
4877
  mcpServers: mcpServersAdded,
4719
4878
  mcpConfigs: filteredMcpConfig,
4720
4879
  factoryInstalled: ctx.installed.factoryInstalled ?? false,
4880
+ abapHooksInstalled: ctx.installed.abapHooksInstalled ?? false,
4721
4881
  files: {
4722
4882
  instructions: target.instructions,
4723
4883
  mcpConfig: target.mcpConfig,
4724
4884
  contexts: "_ai-context/",
4725
4885
  factory: ctx.installed.factoryInstalled ? "factory/" : null,
4726
- globalConfig: (0, import_path11.join)((0, import_os2.homedir)(), ".one-shot-installer")
4886
+ abapHooks: ctx.installed.abapHooksInstalled ? ".claude/hooks/" : null,
4887
+ globalConfig: (0, import_path12.join)((0, import_os2.homedir)(), ".one-shot-installer")
4727
4888
  }
4728
4889
  };
4729
- (0, import_fs10.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4890
+ (0, import_fs11.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4730
4891
  return { status: "success" };
4731
4892
  }
4732
4893
  });
@@ -4741,19 +4902,19 @@ var dim = (text) => `\x1B[2m${text}\x1B[0m`;
4741
4902
  var yellow = (text) => `\x1B[33m${text}\x1B[0m`;
4742
4903
  var green = (text) => `\x1B[32m${text}\x1B[0m`;
4743
4904
  function detectMarkerFileMode(filePath, markerStart) {
4744
- if (!(0, import_fs11.existsSync)(filePath)) return green("create");
4745
- const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
4905
+ if (!(0, import_fs12.existsSync)(filePath)) return green("create");
4906
+ const content = (0, import_fs12.readFileSync)(filePath, "utf-8");
4746
4907
  if (content.includes(markerStart)) return yellow("update");
4747
4908
  return yellow("append");
4748
4909
  }
4749
4910
  function detectMCPFileMode(filePath) {
4750
- if (!(0, import_fs11.existsSync)(filePath)) return green("create");
4911
+ if (!(0, import_fs12.existsSync)(filePath)) return green("create");
4751
4912
  return yellow("merge");
4752
4913
  }
4753
4914
  function detectContextMode(projectPath, domain) {
4754
- const contextDir = (0, import_path12.join)(projectPath, "_ai-context", domain.toUpperCase());
4755
- const contextDirLower = (0, import_path12.join)(projectPath, "_ai-context", domain);
4756
- if ((0, import_fs11.existsSync)(contextDir) || (0, import_fs11.existsSync)(contextDirLower)) return yellow("overwrite");
4915
+ const contextDir = (0, import_path13.join)(projectPath, "_ai-context", domain.toUpperCase());
4916
+ const contextDirLower = (0, import_path13.join)(projectPath, "_ai-context", domain);
4917
+ if ((0, import_fs12.existsSync)(contextDir) || (0, import_fs12.existsSync)(contextDirLower)) return yellow("overwrite");
4757
4918
  return green("create");
4758
4919
  }
4759
4920
  function waitForEnter() {
@@ -4771,10 +4932,11 @@ async function main() {
4771
4932
  try {
4772
4933
  const token = await getGitHubToken();
4773
4934
  console.log(" Locating repositories...");
4774
- const [repo, contextRepo, factoryRepo] = await Promise.all([
4935
+ const [repo, contextRepo, factoryRepo, abapHooksRepo] = await Promise.all([
4775
4936
  discoverRepo(token),
4776
4937
  discoverRepo(token, "context-data").catch(() => null),
4777
- discoverRepo(token, "factory-data").catch(() => null)
4938
+ discoverRepo(token, "factory-data").catch(() => null),
4939
+ discoverRepo(token, "abap-mcp-hooks").catch(() => null)
4778
4940
  ]);
4779
4941
  if (!contextRepo) {
4780
4942
  console.log(" \u26A0 Context repo not found \u2014 AI contexts will not be installed.");
@@ -4782,9 +4944,12 @@ async function main() {
4782
4944
  if (!factoryRepo) {
4783
4945
  console.log(" \u26A0 Factory repo not found \u2014 Factory will not be installed.");
4784
4946
  }
4947
+ if (!abapHooksRepo) {
4948
+ console.log(" \u26A0 ABAP MCP hooks repo not found \u2014 hooks will not be installed.");
4949
+ }
4785
4950
  console.log(" Loading configuration...");
4786
4951
  const options = await loadWizardOptions(token, repo);
4787
- const config = await collectInputs(options, options.releaseVersion, !!factoryRepo);
4952
+ const config = await collectInputs(options, options.releaseVersion, !!factoryRepo, !!abapHooksRepo);
4788
4953
  if (!config) {
4789
4954
  await waitForEnter();
4790
4955
  return;
@@ -4801,6 +4966,7 @@ async function main() {
4801
4966
  repo,
4802
4967
  contextRepo,
4803
4968
  factoryRepo,
4969
+ abapHooksRepo,
4804
4970
  installed: {}
4805
4971
  };
4806
4972
  const stepList = Object.values(steps_exports);
@@ -4813,7 +4979,7 @@ async function main() {
4813
4979
  await waitForEnter();
4814
4980
  }
4815
4981
  }
4816
- async function collectInputs(options, releaseVersion, factoryAvailable = false) {
4982
+ async function collectInputs(options, releaseVersion, factoryAvailable = false, abapHooksAvailable = false) {
4817
4983
  const selectedIds = await esm_default2({
4818
4984
  message: "Technology:",
4819
4985
  instructions: " Space to select \xB7 Enter to confirm",
@@ -4851,17 +5017,20 @@ async function collectInputs(options, releaseVersion, factoryAvailable = false)
4851
5017
  }
4852
5018
  const projectInput = await esm_default4({
4853
5019
  message: "Project directory:",
4854
- default: (0, import_path12.resolve)(process.cwd())
5020
+ default: (0, import_path13.resolve)(process.cwd())
4855
5021
  });
5022
+ const isAbapSelected = selectedTechnologies.some((t) => t.domains.includes("ABAP"));
5023
+ const installAbapHooks = abapHooksAvailable && agent === "claude-code" && isAbapSelected;
4856
5024
  return {
4857
5025
  technologies: selectedTechnologies,
4858
5026
  agent,
4859
5027
  azureDevOpsOrg,
4860
- projectPath: (0, import_path12.resolve)(projectInput),
5028
+ projectPath: (0, import_path13.resolve)(projectInput),
4861
5029
  baseMcpServers: options.baseMcpServers,
4862
5030
  mcpConfig,
4863
5031
  releaseVersion,
4864
- installFactory: factoryAvailable
5032
+ installFactory: factoryAvailable,
5033
+ installAbapHooks
4865
5034
  };
4866
5035
  }
4867
5036
  async function previewAndConfirm(config, options) {
@@ -4871,9 +5040,9 @@ async function previewAndConfirm(config, options) {
4871
5040
  const instructionFile = target.instructions;
4872
5041
  const mcpConfigFile = target.mcpConfig;
4873
5042
  const serverEntries = Object.entries(config.mcpConfig);
4874
- const instructionFilePath = (0, import_path12.join)(config.projectPath, instructionFile);
4875
- const mcpConfigFilePath = (0, import_path12.join)(config.projectPath, mcpConfigFile);
4876
- const gitignorePath = (0, import_path12.join)(config.projectPath, ".gitignore");
5043
+ const instructionFilePath = (0, import_path13.join)(config.projectPath, instructionFile);
5044
+ const mcpConfigFilePath = (0, import_path13.join)(config.projectPath, mcpConfigFile);
5045
+ const gitignorePath = (0, import_path13.join)(config.projectPath, ".gitignore");
4877
5046
  const instructionMode = detectMarkerFileMode(instructionFilePath, "<!-- one-shot-installer:start -->");
4878
5047
  const mcpMode = detectMCPFileMode(mcpConfigFilePath);
4879
5048
  const gitignoreMode = detectMarkerFileMode(gitignorePath, "# one-shot-installer:start");
@@ -4888,6 +5057,7 @@ async function previewAndConfirm(config, options) {
4888
5057
  console.log(` Technology ${technologyNames}`);
4889
5058
  console.log(` Tool ${agentDisplay}`);
4890
5059
  console.log(` Factory ${config.installFactory ? "yes" : "no"}`);
5060
+ console.log(` ABAP hooks ${config.installAbapHooks ? "yes" : "no"}`);
4891
5061
  console.log(` Directory ${config.projectPath}`);
4892
5062
  console.log("");
4893
5063
  console.log(` ${dim("File actions:")}`);
@@ -4899,10 +5069,15 @@ async function previewAndConfirm(config, options) {
4899
5069
  console.log(` ${mcpConfigFile.padEnd(domainColWidth + 14)}${mcpMode}`);
4900
5070
  console.log(` ${".gitignore".padEnd(domainColWidth + 14)}${gitignoreMode}`);
4901
5071
  if (config.installFactory) {
4902
- const factoryExists = (0, import_fs11.existsSync)((0, import_path12.join)(config.projectPath, "factory"));
5072
+ const factoryExists = (0, import_fs12.existsSync)((0, import_path13.join)(config.projectPath, "factory"));
4903
5073
  const factoryMode = factoryExists ? yellow("overwrite") : green("create");
4904
5074
  console.log(` ${"factory/".padEnd(domainColWidth + 14)}${factoryMode}`);
4905
5075
  }
5076
+ if (config.installAbapHooks) {
5077
+ const hooksDir = (0, import_path13.join)(config.projectPath, ".claude", "hooks");
5078
+ const hooksMode = (0, import_fs12.existsSync)(hooksDir) ? yellow("merge") : green("create");
5079
+ console.log(` ${".claude/hooks/".padEnd(domainColWidth + 14)}${hooksMode}`);
5080
+ }
4906
5081
  if (serverEntries.length > 0) {
4907
5082
  console.log("");
4908
5083
  console.log(` ${dim("MCP servers:")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dlw-machine-setup",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "One-shot installer for The Machine toolchain",
5
5
  "bin": {
6
6
  "dlw-machine-setup": "bin/installer.js"