dlw-machine-setup 0.8.1 → 0.8.3

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 +257 -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,160 @@ 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
+ {
4362
+ event: "PreToolUse",
4363
+ script: "sap-safety-gate.mjs",
4364
+ /* Fires on lifecycle/preview tools regardless of which MCP
4365
+ * server exposes them — the safety gate's job is to block
4366
+ * delete + transport release operations wherever they originate. */
4367
+ matcher: "mcp__.*__sap_(object_lifecycle|transport_lifecycle|data_preview)"
4368
+ },
4369
+ {
4370
+ event: "PostToolUse",
4371
+ script: "telemetry-post-tool.mjs",
4372
+ /* Telemetry only meaningful for the SAP ABAP MCP server itself. */
4373
+ matcher: "mcp__sap-abap-mcp__.*"
4374
+ },
4375
+ {
4376
+ event: "PostToolUseFailure",
4377
+ script: "telemetry-post-tool-failure.mjs",
4378
+ matcher: "mcp__sap-abap-mcp__.*"
4379
+ },
4380
+ { event: "SessionStart", script: "telemetry-session-start.mjs" },
4381
+ { event: "SessionEnd", script: "telemetry-session-end.mjs" }
4382
+ ];
4383
+ var INSTRUCTIONS_SNIPPET = [
4384
+ `## SAP ABAP MCP Hooks`,
4385
+ ``,
4386
+ `Installed by One-Shot Installer. Hook scripts live in \`.claude/hooks/\` and are wired into \`.claude/settings.json\`.`,
4387
+ ``,
4388
+ `**What they do:**`,
4389
+ `- \`sap-safety-gate.mjs\` (PreToolUse) \u2014 blocks \`delete\` (any tool) and transport \`release\`.`,
4390
+ `- \`telemetry-*.mjs\` (PostToolUse, PostToolUseFailure, SessionStart, SessionEnd) \u2014 emit usage events.`,
4391
+ ``,
4392
+ `**Optional environment variables:**`,
4393
+ `- \`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.`,
4394
+ `- \`SAP_MCP_TELEMETRY_TOKEN\` \u2014 Bearer token for the telemetry endpoint.`,
4395
+ ``,
4396
+ `On network/HTTP failure, telemetry events fall back to \`~/.claude/telemetry/events.jsonl\`.`,
4397
+ ``
4398
+ ].join("\n");
4399
+ var fetch_abap_hooks_default = defineStep({
4400
+ name: "fetch-abap-hooks",
4401
+ label: "Installing ABAP MCP hooks",
4402
+ /* All eligibility (Claude-only + ABAP selected + repo discovered) is
4403
+ * resolved upfront in collectInputs() and frozen on InstallConfig.
4404
+ * Reading the single flag keeps the guard, the preview, and the
4405
+ * step in sync with no scattered conditionals. */
4406
+ when: (ctx) => ctx.config.installAbapHooks,
4407
+ execute: async (ctx) => {
4408
+ const result = await fetchAbapHooks(
4409
+ ctx.token,
4410
+ ctx.abapHooksRepo,
4411
+ ctx.config.projectPath,
4412
+ ctx.config.agent
4413
+ );
4414
+ ctx.installed.abapHooksInstalled = result.success;
4415
+ if (result.instructionsSnippet) {
4416
+ ctx.installed.abapHooksInstructionsSnippet = result.instructionsSnippet;
4417
+ }
4418
+ if (!result.success) {
4419
+ return { status: "failed", detail: result.failureReason };
4420
+ }
4421
+ return { status: "success", message: result.filesInstalled.join(", ") };
4422
+ }
4423
+ });
4424
+ async function fetchAbapHooks(token, repo, targetDir, agent) {
4425
+ const result = { success: false, filesInstalled: [] };
4426
+ let release;
4427
+ try {
4428
+ release = await fetchLatestReleaseByTagPrefix(token, repo, HOOKS_TAG_PREFIX);
4429
+ } catch (err) {
4430
+ result.failureReason = err instanceof Error ? err.message : String(err);
4431
+ return result;
4432
+ }
4433
+ const asset = release.assets.find((a) => a.name === HOOKS_ASSET_NAME);
4434
+ if (!asset) {
4435
+ result.failureReason = `${HOOKS_ASSET_NAME} not found in release ${release.tagName}`;
4436
+ return result;
4437
+ }
4438
+ let archive = null;
4439
+ try {
4440
+ archive = await downloadAndExtractAsset(token, asset, targetDir, ".temp-abap-hooks-download");
4441
+ const extractedEntries = (0, import_fs8.readdirSync)(archive.extractedRoot);
4442
+ const innerFolder = extractedEntries.find(
4443
+ (e) => e.toLowerCase() === HOOKS_ROOT_FOLDER.toLowerCase()
4444
+ );
4445
+ if (!innerFolder) {
4446
+ result.failureReason = `No ${HOOKS_ROOT_FOLDER}/ folder in archive`;
4447
+ return result;
4448
+ }
4449
+ const bundleRoot = (0, import_path8.join)(archive.extractedRoot, innerFolder);
4450
+ if (!(0, import_fs8.statSync)(bundleRoot).isDirectory()) {
4451
+ result.failureReason = `${HOOKS_ROOT_FOLDER}/ entry is not a directory`;
4452
+ return result;
4453
+ }
4454
+ const presentDefs = HOOK_DEFINITIONS.filter((d) => (0, import_fs8.existsSync)((0, import_path8.join)(bundleRoot, d.script)));
4455
+ if (presentDefs.length === 0) {
4456
+ result.failureReason = "Archive contains no recognized hook scripts";
4457
+ return result;
4458
+ }
4459
+ const assets = [
4460
+ ...presentDefs.map((d) => ({
4461
+ type: "hook",
4462
+ event: d.event,
4463
+ /* {hookDir} is substituted by run-bundle.ts with the agent
4464
+ * profile's scriptDir (.claude/hooks for Claude Code), so the
4465
+ * resulting command is exactly `node .claude/hooks/<script>`,
4466
+ * matching the form Claude Code already uses for Factory hooks. */
4467
+ command: `node {hookDir}/${d.script}`,
4468
+ script: d.script,
4469
+ /* The runner copies the matcher straight into the merged JSON
4470
+ * entry. Omitted when undefined so session-level hooks don't
4471
+ * carry a useless `matcher` field. */
4472
+ ...d.matcher ? { matcher: d.matcher } : {}
4473
+ })),
4474
+ { type: "instructions-snippet", content: INSTRUCTIONS_SNIPPET }
4475
+ ];
4476
+ const manifest = {
4477
+ schemaVersion: 2,
4478
+ name: BUNDLE_NAME,
4479
+ assets
4480
+ };
4481
+ const target = getAgentTarget(agent);
4482
+ const runResult = await runBundle(manifest, {
4483
+ bundleRoot,
4484
+ projectPath: targetDir,
4485
+ agent,
4486
+ instructionsFile: target.instructions,
4487
+ /* write-instructions.ts owns the ordering of marker blocks in
4488
+ * CLAUDE.md (one-shot-installer:start/end first, bundle blocks
4489
+ * after). Skipping here means we hand the snippet back; it gets
4490
+ * committed when write-instructions.ts runs in the setup phase. */
4491
+ skipInstructions: true
4492
+ });
4493
+ result.filesInstalled = runResult.filesTouched;
4494
+ result.instructionsSnippet = runResult.instructionsSnippet;
4495
+ result.success = true;
4496
+ } catch (error) {
4497
+ result.failureReason = error instanceof Error ? error.message : String(error);
4498
+ } finally {
4499
+ archive?.cleanup();
4500
+ }
4501
+ return result;
4502
+ }
4503
+
4504
+ // src/steps/setup/write-instructions.ts
4505
+ var import_fs9 = require("fs");
4506
+ var import_path9 = require("path");
4336
4507
 
4337
4508
  // src/steps/shared.ts
4338
4509
  var cached = null;
@@ -4359,7 +4530,7 @@ var red = (text) => `\x1B[31m${text}\x1B[0m`;
4359
4530
  var write_instructions_default = defineStep({
4360
4531
  name: "write-instructions",
4361
4532
  label: "Writing instruction file",
4362
- when: (ctx) => (ctx.installed.domainsInstalled?.length ?? 0) > 0 || Object.keys(ctx.config.mcpConfig).length > 0 || !!ctx.installed.factoryInstructionsSnippet,
4533
+ when: (ctx) => (ctx.installed.domainsInstalled?.length ?? 0) > 0 || Object.keys(ctx.config.mcpConfig).length > 0 || !!ctx.installed.factoryInstructionsSnippet || !!ctx.installed.abapHooksInstructionsSnippet,
4363
4534
  execute: async (ctx) => {
4364
4535
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4365
4536
  const domains = ctx.installed.domainsInstalled ?? [];
@@ -4367,11 +4538,11 @@ var write_instructions_default = defineStep({
4367
4538
  const projectPath = ctx.config.projectPath;
4368
4539
  const content = buildCombinedInstructions(domains, filteredMcpConfig, agent, projectPath);
4369
4540
  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, `---
4541
+ const filePath = (0, import_path9.join)(projectPath, target.instructions);
4542
+ const fileDir = (0, import_path9.dirname)(filePath);
4543
+ if (!(0, import_fs9.existsSync)(fileDir)) (0, import_fs9.mkdirSync)(fileDir, { recursive: true });
4544
+ if (agent === "cursor" && !(0, import_fs9.existsSync)(filePath)) {
4545
+ (0, import_fs9.writeFileSync)(filePath, `---
4375
4546
  description: AI development instructions from One-Shot Installer
4376
4547
  alwaysApply: true
4377
4548
  ---
@@ -4406,20 +4577,28 @@ alwaysApply: true
4406
4577
  ctx.installed.factoryInstructionsSnippet
4407
4578
  );
4408
4579
  }
4580
+ if (ctx.installed.abapHooksInstructionsSnippet) {
4581
+ upsertMarkerBlock(
4582
+ filePath,
4583
+ "<!-- abap-mcp-hooks:start -->",
4584
+ "<!-- abap-mcp-hooks:end -->",
4585
+ ctx.installed.abapHooksInstructionsSnippet
4586
+ );
4587
+ }
4409
4588
  return { status: "success", message: target.instructions };
4410
4589
  }
4411
4590
  });
4412
4591
  function collectMdFiles(dir) {
4413
- if (!(0, import_fs8.existsSync)(dir)) return [];
4414
- const entries = (0, import_fs8.readdirSync)(dir, { recursive: true });
4592
+ if (!(0, import_fs9.existsSync)(dir)) return [];
4593
+ const entries = (0, import_fs9.readdirSync)(dir, { recursive: true });
4415
4594
  return entries.filter((entry) => {
4416
- const fullPath = (0, import_path8.join)(dir, entry);
4417
- return entry.endsWith(".md") && (0, import_fs8.statSync)(fullPath).isFile();
4595
+ const fullPath = (0, import_path9.join)(dir, entry);
4596
+ return entry.endsWith(".md") && (0, import_fs9.statSync)(fullPath).isFile();
4418
4597
  }).map((entry) => entry.replace(/\\/g, "/")).sort();
4419
4598
  }
4420
4599
  function extractFirstHeading(filePath) {
4421
4600
  try {
4422
- const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
4601
+ const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
4423
4602
  const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, "");
4424
4603
  const match = withoutFrontmatter.match(/^#\s+(.+)/m);
4425
4604
  if (match) {
@@ -4437,16 +4616,16 @@ function formatPathRef(contextPath, description, agent) {
4437
4616
  return `- \`${contextPath}\` \u2014 ${description}`;
4438
4617
  }
4439
4618
  function resolveDomainFolder(domain, contextsDir) {
4440
- if ((0, import_fs8.existsSync)(contextsDir)) {
4619
+ if ((0, import_fs9.existsSync)(contextsDir)) {
4441
4620
  try {
4442
- const entries = (0, import_fs8.readdirSync)(contextsDir);
4621
+ const entries = (0, import_fs9.readdirSync)(contextsDir);
4443
4622
  const match = entries.find((e) => e.toLowerCase() === domain.toLowerCase());
4444
- if (match) return { folderName: match, folderPath: (0, import_path8.join)(contextsDir, match) };
4623
+ if (match) return { folderName: match, folderPath: (0, import_path9.join)(contextsDir, match) };
4445
4624
  } catch {
4446
4625
  }
4447
4626
  }
4448
4627
  const fallback = domain.toUpperCase();
4449
- return { folderName: fallback, folderPath: (0, import_path8.join)(contextsDir, fallback) };
4628
+ return { folderName: fallback, folderPath: (0, import_path9.join)(contextsDir, fallback) };
4450
4629
  }
4451
4630
  function buildContextRefsSection(domains, agent, contextsDir) {
4452
4631
  const lines2 = [
@@ -4458,34 +4637,34 @@ function buildContextRefsSection(domains, agent, contextsDir) {
4458
4637
  let hasAnyFiles = false;
4459
4638
  for (const domain of domains) {
4460
4639
  const { folderName, folderPath: domainPath } = resolveDomainFolder(domain, contextsDir);
4461
- if (!(0, import_fs8.existsSync)(domainPath)) continue;
4640
+ if (!(0, import_fs9.existsSync)(domainPath)) continue;
4462
4641
  const domainFiles = [];
4463
- const ctxInstructions = (0, import_path8.join)(domainPath, "context-instructions.md");
4464
- if ((0, import_fs8.existsSync)(ctxInstructions)) {
4642
+ const ctxInstructions = (0, import_path9.join)(domainPath, "context-instructions.md");
4643
+ if ((0, import_fs9.existsSync)(ctxInstructions)) {
4465
4644
  const desc = extractFirstHeading(ctxInstructions);
4466
4645
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/context-instructions.md`, desc, agent));
4467
4646
  }
4468
- const instructionsMd = (0, import_path8.join)(domainPath, "core", "instructions.md");
4469
- if ((0, import_fs8.existsSync)(instructionsMd)) {
4647
+ const instructionsMd = (0, import_path9.join)(domainPath, "core", "instructions.md");
4648
+ if ((0, import_fs9.existsSync)(instructionsMd)) {
4470
4649
  const desc = extractFirstHeading(instructionsMd);
4471
4650
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/instructions.md`, `${desc} (start here)`, agent));
4472
4651
  }
4473
- const coreDir = (0, import_path8.join)(domainPath, "core");
4474
- if ((0, import_fs8.existsSync)(coreDir)) {
4652
+ const coreDir = (0, import_path9.join)(domainPath, "core");
4653
+ if ((0, import_fs9.existsSync)(coreDir)) {
4475
4654
  const coreFiles = collectMdFiles(coreDir).filter((f) => f !== "instructions.md" && !f.startsWith("instructions/"));
4476
4655
  for (const file of coreFiles) {
4477
- const desc = extractFirstHeading((0, import_path8.join)(coreDir, file));
4656
+ const desc = extractFirstHeading((0, import_path9.join)(coreDir, file));
4478
4657
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/${file}`, desc, agent));
4479
4658
  }
4480
4659
  }
4481
- const refDir = (0, import_path8.join)(domainPath, "reference");
4482
- if ((0, import_fs8.existsSync)(refDir)) {
4660
+ const refDir = (0, import_path9.join)(domainPath, "reference");
4661
+ if ((0, import_fs9.existsSync)(refDir)) {
4483
4662
  const refFiles = collectMdFiles(refDir);
4484
4663
  if (refFiles.length > 0) {
4485
4664
  domainFiles.push(``);
4486
4665
  domainFiles.push(`**Reference & cheat sheets:**`);
4487
4666
  for (const file of refFiles) {
4488
- const desc = extractFirstHeading((0, import_path8.join)(refDir, file));
4667
+ const desc = extractFirstHeading((0, import_path9.join)(refDir, file));
4489
4668
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/reference/${file}`, desc, agent));
4490
4669
  }
4491
4670
  }
@@ -4516,7 +4695,7 @@ function buildMCPSection(mcpConfig) {
4516
4695
  return lines2.join("\n");
4517
4696
  }
4518
4697
  function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4519
- const contextsDir = (0, import_path8.join)(projectPath, "_ai-context");
4698
+ const contextsDir = (0, import_path9.join)(projectPath, "_ai-context");
4520
4699
  const lines2 = [`# AI Development Instructions`, ``, `> Generated by One-Shot Installer`, ``];
4521
4700
  lines2.push(buildContextRefsSection(domains, agent, contextsDir));
4522
4701
  if (Object.keys(mcpConfig).length > 0) lines2.push(buildMCPSection(mcpConfig));
@@ -4524,8 +4703,8 @@ function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4524
4703
  }
4525
4704
 
4526
4705
  // src/steps/setup/write-mcp-config.ts
4527
- var import_fs9 = require("fs");
4528
- var import_path9 = require("path");
4706
+ var import_fs10 = require("fs");
4707
+ var import_path10 = require("path");
4529
4708
  var red2 = (text) => `\x1B[31m${text}\x1B[0m`;
4530
4709
  var write_mcp_config_default = defineStep({
4531
4710
  name: "write-mcp-config",
@@ -4534,15 +4713,15 @@ var write_mcp_config_default = defineStep({
4534
4713
  execute: async (ctx) => {
4535
4714
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4536
4715
  const target = getAgentTarget(ctx.config.agent);
4537
- const mcpJsonPath = (0, import_path9.join)(ctx.config.projectPath, target.mcpConfig);
4716
+ const mcpJsonPath = (0, import_path10.join)(ctx.config.projectPath, target.mcpConfig);
4538
4717
  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 });
4718
+ const dir = (0, import_path10.join)(ctx.config.projectPath, target.mcpDir);
4719
+ if (!(0, import_fs10.existsSync)(dir)) (0, import_fs10.mkdirSync)(dir, { recursive: true });
4541
4720
  }
4542
4721
  let existingFile = {};
4543
- if ((0, import_fs9.existsSync)(mcpJsonPath)) {
4722
+ if ((0, import_fs10.existsSync)(mcpJsonPath)) {
4544
4723
  try {
4545
- existingFile = JSON.parse((0, import_fs9.readFileSync)(mcpJsonPath, "utf-8"));
4724
+ existingFile = JSON.parse((0, import_fs10.readFileSync)(mcpJsonPath, "utf-8"));
4546
4725
  } catch {
4547
4726
  const box = [
4548
4727
  "",
@@ -4568,7 +4747,7 @@ var write_mcp_config_default = defineStep({
4568
4747
  }
4569
4748
  const mergedServers = { ...existingServers, ...newServers };
4570
4749
  const outputFile = { ...existingFile, [target.mcpRootKey]: mergedServers };
4571
- (0, import_fs9.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
4750
+ (0, import_fs10.writeFileSync)(mcpJsonPath, JSON.stringify(outputFile, null, 2), "utf-8");
4572
4751
  ctx.installed.mcpServersAdded = addedServers;
4573
4752
  return {
4574
4753
  status: "success",
@@ -4637,7 +4816,7 @@ function isClaudeCliAvailable() {
4637
4816
  }
4638
4817
 
4639
4818
  // src/steps/setup/update-gitignore.ts
4640
- var import_path10 = require("path");
4819
+ var import_path11 = require("path");
4641
4820
  var MARKER_START2 = "# one-shot-installer:start";
4642
4821
  var MARKER_END2 = "# one-shot-installer:end";
4643
4822
  var CORE_GITIGNORE_ENTRIES = [
@@ -4658,15 +4837,15 @@ var update_gitignore_default = defineStep({
4658
4837
  name: "update-gitignore",
4659
4838
  label: "Updating .gitignore",
4660
4839
  execute: async (ctx) => {
4661
- const gitignorePath = (0, import_path10.join)(ctx.config.projectPath, ".gitignore");
4840
+ const gitignorePath = (0, import_path11.join)(ctx.config.projectPath, ".gitignore");
4662
4841
  upsertMarkerBlock(gitignorePath, MARKER_START2, MARKER_END2, CORE_GITIGNORE_ENTRIES.join("\n"));
4663
4842
  return { status: "success" };
4664
4843
  }
4665
4844
  });
4666
4845
 
4667
4846
  // src/steps/setup/write-state.ts
4668
- var import_fs10 = require("fs");
4669
- var import_path11 = require("path");
4847
+ var import_fs11 = require("fs");
4848
+ var import_path12 = require("path");
4670
4849
  var import_os2 = require("os");
4671
4850
 
4672
4851
  // package.json
@@ -4700,7 +4879,7 @@ var write_state_default = defineStep({
4700
4879
  name: "write-state",
4701
4880
  label: "Saving installation state",
4702
4881
  execute: async (ctx) => {
4703
- const statePath = (0, import_path11.join)(ctx.config.projectPath, ".one-shot-state.json");
4882
+ const statePath = (0, import_path12.join)(ctx.config.projectPath, ".one-shot-state.json");
4704
4883
  const mcpServersAdded = ctx.installed.mcpServersAdded ?? [];
4705
4884
  const filteredMcpConfig = Object.fromEntries(
4706
4885
  Object.entries(ctx.config.mcpConfig).filter(([name]) => mcpServersAdded.includes(name))
@@ -4718,15 +4897,17 @@ var write_state_default = defineStep({
4718
4897
  mcpServers: mcpServersAdded,
4719
4898
  mcpConfigs: filteredMcpConfig,
4720
4899
  factoryInstalled: ctx.installed.factoryInstalled ?? false,
4900
+ abapHooksInstalled: ctx.installed.abapHooksInstalled ?? false,
4721
4901
  files: {
4722
4902
  instructions: target.instructions,
4723
4903
  mcpConfig: target.mcpConfig,
4724
4904
  contexts: "_ai-context/",
4725
4905
  factory: ctx.installed.factoryInstalled ? "factory/" : null,
4726
- globalConfig: (0, import_path11.join)((0, import_os2.homedir)(), ".one-shot-installer")
4906
+ abapHooks: ctx.installed.abapHooksInstalled ? ".claude/hooks/" : null,
4907
+ globalConfig: (0, import_path12.join)((0, import_os2.homedir)(), ".one-shot-installer")
4727
4908
  }
4728
4909
  };
4729
- (0, import_fs10.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4910
+ (0, import_fs11.writeFileSync)(statePath, JSON.stringify(state, null, 2), "utf-8");
4730
4911
  return { status: "success" };
4731
4912
  }
4732
4913
  });
@@ -4741,19 +4922,19 @@ var dim = (text) => `\x1B[2m${text}\x1B[0m`;
4741
4922
  var yellow = (text) => `\x1B[33m${text}\x1B[0m`;
4742
4923
  var green = (text) => `\x1B[32m${text}\x1B[0m`;
4743
4924
  function detectMarkerFileMode(filePath, markerStart) {
4744
- if (!(0, import_fs11.existsSync)(filePath)) return green("create");
4745
- const content = (0, import_fs11.readFileSync)(filePath, "utf-8");
4925
+ if (!(0, import_fs12.existsSync)(filePath)) return green("create");
4926
+ const content = (0, import_fs12.readFileSync)(filePath, "utf-8");
4746
4927
  if (content.includes(markerStart)) return yellow("update");
4747
4928
  return yellow("append");
4748
4929
  }
4749
4930
  function detectMCPFileMode(filePath) {
4750
- if (!(0, import_fs11.existsSync)(filePath)) return green("create");
4931
+ if (!(0, import_fs12.existsSync)(filePath)) return green("create");
4751
4932
  return yellow("merge");
4752
4933
  }
4753
4934
  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");
4935
+ const contextDir = (0, import_path13.join)(projectPath, "_ai-context", domain.toUpperCase());
4936
+ const contextDirLower = (0, import_path13.join)(projectPath, "_ai-context", domain);
4937
+ if ((0, import_fs12.existsSync)(contextDir) || (0, import_fs12.existsSync)(contextDirLower)) return yellow("overwrite");
4757
4938
  return green("create");
4758
4939
  }
4759
4940
  function waitForEnter() {
@@ -4771,10 +4952,11 @@ async function main() {
4771
4952
  try {
4772
4953
  const token = await getGitHubToken();
4773
4954
  console.log(" Locating repositories...");
4774
- const [repo, contextRepo, factoryRepo] = await Promise.all([
4955
+ const [repo, contextRepo, factoryRepo, abapHooksRepo] = await Promise.all([
4775
4956
  discoverRepo(token),
4776
4957
  discoverRepo(token, "context-data").catch(() => null),
4777
- discoverRepo(token, "factory-data").catch(() => null)
4958
+ discoverRepo(token, "factory-data").catch(() => null),
4959
+ discoverRepo(token, "abap-mcp-hooks").catch(() => null)
4778
4960
  ]);
4779
4961
  if (!contextRepo) {
4780
4962
  console.log(" \u26A0 Context repo not found \u2014 AI contexts will not be installed.");
@@ -4782,9 +4964,12 @@ async function main() {
4782
4964
  if (!factoryRepo) {
4783
4965
  console.log(" \u26A0 Factory repo not found \u2014 Factory will not be installed.");
4784
4966
  }
4967
+ if (!abapHooksRepo) {
4968
+ console.log(" \u26A0 ABAP MCP hooks repo not found \u2014 hooks will not be installed.");
4969
+ }
4785
4970
  console.log(" Loading configuration...");
4786
4971
  const options = await loadWizardOptions(token, repo);
4787
- const config = await collectInputs(options, options.releaseVersion, !!factoryRepo);
4972
+ const config = await collectInputs(options, options.releaseVersion, !!factoryRepo, !!abapHooksRepo);
4788
4973
  if (!config) {
4789
4974
  await waitForEnter();
4790
4975
  return;
@@ -4801,6 +4986,7 @@ async function main() {
4801
4986
  repo,
4802
4987
  contextRepo,
4803
4988
  factoryRepo,
4989
+ abapHooksRepo,
4804
4990
  installed: {}
4805
4991
  };
4806
4992
  const stepList = Object.values(steps_exports);
@@ -4813,7 +4999,7 @@ async function main() {
4813
4999
  await waitForEnter();
4814
5000
  }
4815
5001
  }
4816
- async function collectInputs(options, releaseVersion, factoryAvailable = false) {
5002
+ async function collectInputs(options, releaseVersion, factoryAvailable = false, abapHooksAvailable = false) {
4817
5003
  const selectedIds = await esm_default2({
4818
5004
  message: "Technology:",
4819
5005
  instructions: " Space to select \xB7 Enter to confirm",
@@ -4851,17 +5037,20 @@ async function collectInputs(options, releaseVersion, factoryAvailable = false)
4851
5037
  }
4852
5038
  const projectInput = await esm_default4({
4853
5039
  message: "Project directory:",
4854
- default: (0, import_path12.resolve)(process.cwd())
5040
+ default: (0, import_path13.resolve)(process.cwd())
4855
5041
  });
5042
+ const isAbapSelected = selectedTechnologies.some((t) => t.domains.includes("ABAP"));
5043
+ const installAbapHooks = abapHooksAvailable && agent === "claude-code" && isAbapSelected;
4856
5044
  return {
4857
5045
  technologies: selectedTechnologies,
4858
5046
  agent,
4859
5047
  azureDevOpsOrg,
4860
- projectPath: (0, import_path12.resolve)(projectInput),
5048
+ projectPath: (0, import_path13.resolve)(projectInput),
4861
5049
  baseMcpServers: options.baseMcpServers,
4862
5050
  mcpConfig,
4863
5051
  releaseVersion,
4864
- installFactory: factoryAvailable
5052
+ installFactory: factoryAvailable,
5053
+ installAbapHooks
4865
5054
  };
4866
5055
  }
4867
5056
  async function previewAndConfirm(config, options) {
@@ -4871,9 +5060,9 @@ async function previewAndConfirm(config, options) {
4871
5060
  const instructionFile = target.instructions;
4872
5061
  const mcpConfigFile = target.mcpConfig;
4873
5062
  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");
5063
+ const instructionFilePath = (0, import_path13.join)(config.projectPath, instructionFile);
5064
+ const mcpConfigFilePath = (0, import_path13.join)(config.projectPath, mcpConfigFile);
5065
+ const gitignorePath = (0, import_path13.join)(config.projectPath, ".gitignore");
4877
5066
  const instructionMode = detectMarkerFileMode(instructionFilePath, "<!-- one-shot-installer:start -->");
4878
5067
  const mcpMode = detectMCPFileMode(mcpConfigFilePath);
4879
5068
  const gitignoreMode = detectMarkerFileMode(gitignorePath, "# one-shot-installer:start");
@@ -4888,6 +5077,7 @@ async function previewAndConfirm(config, options) {
4888
5077
  console.log(` Technology ${technologyNames}`);
4889
5078
  console.log(` Tool ${agentDisplay}`);
4890
5079
  console.log(` Factory ${config.installFactory ? "yes" : "no"}`);
5080
+ console.log(` ABAP hooks ${config.installAbapHooks ? "yes" : "no"}`);
4891
5081
  console.log(` Directory ${config.projectPath}`);
4892
5082
  console.log("");
4893
5083
  console.log(` ${dim("File actions:")}`);
@@ -4899,10 +5089,15 @@ async function previewAndConfirm(config, options) {
4899
5089
  console.log(` ${mcpConfigFile.padEnd(domainColWidth + 14)}${mcpMode}`);
4900
5090
  console.log(` ${".gitignore".padEnd(domainColWidth + 14)}${gitignoreMode}`);
4901
5091
  if (config.installFactory) {
4902
- const factoryExists = (0, import_fs11.existsSync)((0, import_path12.join)(config.projectPath, "factory"));
5092
+ const factoryExists = (0, import_fs12.existsSync)((0, import_path13.join)(config.projectPath, "factory"));
4903
5093
  const factoryMode = factoryExists ? yellow("overwrite") : green("create");
4904
5094
  console.log(` ${"factory/".padEnd(domainColWidth + 14)}${factoryMode}`);
4905
5095
  }
5096
+ if (config.installAbapHooks) {
5097
+ const hooksDir = (0, import_path13.join)(config.projectPath, ".claude", "hooks");
5098
+ const hooksMode = (0, import_fs12.existsSync)(hooksDir) ? yellow("merge") : green("create");
5099
+ console.log(` ${".claude/hooks/".padEnd(domainColWidth + 14)}${hooksMode}`);
5100
+ }
4906
5101
  if (serverEntries.length > 0) {
4907
5102
  console.log("");
4908
5103
  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.3",
4
4
  "description": "One-shot installer for The Machine toolchain",
5
5
  "bin": {
6
6
  "dlw-machine-setup": "bin/installer.js"