dlw-machine-setup 0.8.0 → 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 +279 -70
  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,
@@ -3888,7 +3908,9 @@ var claudeCodeProfile = {
3888
3908
  handlers: {
3889
3909
  agent: {
3890
3910
  supported: true,
3891
- destination: (name) => `.claude/agents/${name}.md`
3911
+ // File agents land at `.claude/agents/<name>.md`; folder agents
3912
+ // (persona bundles like jay/, monty/) land at `.claude/agents/<name>/`.
3913
+ destination: (name, isDir) => `.claude/agents/${name}${isDir ? "" : ".md"}`
3892
3914
  },
3893
3915
  skill: {
3894
3916
  supported: true,
@@ -3903,6 +3925,13 @@ var claudeCodeProfile = {
3903
3925
  },
3904
3926
  "instructions-snippet": {
3905
3927
  supported: true
3928
+ },
3929
+ workspace: {
3930
+ supported: true,
3931
+ // Bundle workspace payload (workflow scripts, shared definitions,
3932
+ // README) lands under `.claude/factory/`. Matches the path pattern
3933
+ // used historically by Factory's v1 ops and gitignored entries.
3934
+ destination: (name) => `.claude/factory/${name}`
3906
3935
  }
3907
3936
  }
3908
3937
  };
@@ -3914,19 +3943,37 @@ var githubCopilotProfile = {
3914
3943
  handlers: {
3915
3944
  agent: {
3916
3945
  supported: true,
3917
- destination: (name) => `.github/agents/${name}.agent.md`
3946
+ // Copilot only registers agents that match `.agent.md` literally.
3947
+ // File agents → `.github/agents/<name>.agent.md` (registered).
3948
+ // Folder agents (persona bundles like jay/, monty/, shared/) →
3949
+ // `.github/agents/<name>/` (placed alongside, but Copilot ignores
3950
+ // them because they don't match `.agent.md`). That's intentional:
3951
+ // these are Claude-specific personas, but shipping the files
3952
+ // means a user who switches agents later still has them on disk.
3953
+ destination: (name, isDir) => `.github/agents/${name}${isDir ? "" : ".agent.md"}`
3918
3954
  },
3919
3955
  skill: {
3920
3956
  supported: true,
3921
3957
  destination: (name) => `.github/skills/${name}/SKILL.md`
3922
3958
  },
3923
3959
  hook: {
3924
- // See header docblock for the three HookHandler extensions needed
3925
- // to flip this on. Until then, hook assets are skipped on Copilot.
3960
+ // Hooks are Claude-only by policy (see plan doc). The bundle's
3961
+ // hook-related ops are gated with `when: 'hooks-supported'`, so
3962
+ // Copilot installs ship no hook events, no hook scripts, and no
3963
+ // statusLine setting. Phase 4 (Copilot hook implementation) was
3964
+ // canceled — this is the permanent final state.
3926
3965
  supported: false
3927
3966
  },
3928
3967
  "instructions-snippet": {
3929
3968
  supported: true
3969
+ },
3970
+ workspace: {
3971
+ supported: true,
3972
+ // Bundle workspace payload (workflow scripts, shared definitions,
3973
+ // README) lands under `.github/factory/`. Keeps Factory's
3974
+ // supporting material under the namespaced .github/ tree where
3975
+ // Copilot already reads everything else from.
3976
+ destination: (name) => `.github/factory/${name}`
3930
3977
  }
3931
3978
  }
3932
3979
  };
@@ -3939,7 +3986,8 @@ var cursorProfile = {
3939
3986
  agent: { supported: false },
3940
3987
  skill: { supported: false },
3941
3988
  hook: { supported: false },
3942
- "instructions-snippet": { supported: true }
3989
+ "instructions-snippet": { supported: true },
3990
+ workspace: { supported: false }
3943
3991
  }
3944
3992
  };
3945
3993
 
@@ -4008,6 +4056,7 @@ function runBundleV2(manifest, ctx) {
4008
4056
  runMergeJson({ op: "merge-json", file, patch }, manifest.name, ctx, result);
4009
4057
  }
4010
4058
  for (const op of manifest.ops ?? []) {
4059
+ if (op.when === "hooks-supported" && !profile.handlers.hook.supported) continue;
4011
4060
  executeOp(op, manifest.name, ctx, result);
4012
4061
  result.opsExecuted++;
4013
4062
  }
@@ -4035,20 +4084,25 @@ function runAsset(asset, profile, hookPatches, ctx, result) {
4035
4084
  result.instructionsSnippet = asset.content;
4036
4085
  }
4037
4086
  return;
4087
+ case "workspace":
4088
+ runAssetCopy(asset, profile.handlers.workspace, ctx, result);
4089
+ return;
4038
4090
  }
4039
4091
  }
4040
4092
  function runAssetCopy(asset, handler, ctx, result) {
4041
4093
  if (!handler.supported || !handler.destination) return;
4042
4094
  const source = resolveBundlePath(asset.source, ctx);
4043
4095
  if (!(0, import_fs6.existsSync)(source)) return;
4044
- const target = resolveProjectPath(handler.destination(asset.name), ctx);
4045
- if ((0, import_fs6.statSync)(source).isDirectory()) {
4096
+ const isDirectory = (0, import_fs6.statSync)(source).isDirectory();
4097
+ const targetRel = handler.destination(asset.name, isDirectory);
4098
+ const target = resolveProjectPath(targetRel, ctx);
4099
+ if (isDirectory) {
4046
4100
  copyDirectory(source, target);
4047
4101
  } else {
4048
4102
  (0, import_fs6.mkdirSync)((0, import_path6.dirname)(target), { recursive: true });
4049
4103
  (0, import_fs6.copyFileSync)(source, target);
4050
4104
  }
4051
- result.filesTouched.push(handler.destination(asset.name));
4105
+ result.filesTouched.push(targetRel);
4052
4106
  }
4053
4107
  function accumulateHook(asset, handler, hookPatches, ctx, result) {
4054
4108
  if (!handler.supported) return;
@@ -4296,9 +4350,140 @@ async function installViaBundle(manifestPath, extractedPath, targetDir, agent, r
4296
4350
  result.filesInstalled = runResult.filesTouched;
4297
4351
  }
4298
4352
 
4299
- // src/steps/setup/write-instructions.ts
4353
+ // src/steps/resources/fetch-abap-hooks.ts
4300
4354
  var import_fs8 = require("fs");
4301
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");
4302
4487
 
4303
4488
  // src/steps/shared.ts
4304
4489
  var cached = null;
@@ -4325,7 +4510,7 @@ var red = (text) => `\x1B[31m${text}\x1B[0m`;
4325
4510
  var write_instructions_default = defineStep({
4326
4511
  name: "write-instructions",
4327
4512
  label: "Writing instruction file",
4328
- 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,
4329
4514
  execute: async (ctx) => {
4330
4515
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4331
4516
  const domains = ctx.installed.domainsInstalled ?? [];
@@ -4333,11 +4518,11 @@ var write_instructions_default = defineStep({
4333
4518
  const projectPath = ctx.config.projectPath;
4334
4519
  const content = buildCombinedInstructions(domains, filteredMcpConfig, agent, projectPath);
4335
4520
  const target = getAgentTarget(agent);
4336
- const filePath = (0, import_path8.join)(projectPath, target.instructions);
4337
- const fileDir = (0, import_path8.dirname)(filePath);
4338
- if (!(0, import_fs8.existsSync)(fileDir)) (0, import_fs8.mkdirSync)(fileDir, { recursive: true });
4339
- if (agent === "cursor" && !(0, import_fs8.existsSync)(filePath)) {
4340
- (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, `---
4341
4526
  description: AI development instructions from One-Shot Installer
4342
4527
  alwaysApply: true
4343
4528
  ---
@@ -4372,20 +4557,28 @@ alwaysApply: true
4372
4557
  ctx.installed.factoryInstructionsSnippet
4373
4558
  );
4374
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
+ }
4375
4568
  return { status: "success", message: target.instructions };
4376
4569
  }
4377
4570
  });
4378
4571
  function collectMdFiles(dir) {
4379
- if (!(0, import_fs8.existsSync)(dir)) return [];
4380
- 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 });
4381
4574
  return entries.filter((entry) => {
4382
- const fullPath = (0, import_path8.join)(dir, entry);
4383
- 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();
4384
4577
  }).map((entry) => entry.replace(/\\/g, "/")).sort();
4385
4578
  }
4386
4579
  function extractFirstHeading(filePath) {
4387
4580
  try {
4388
- const content = (0, import_fs8.readFileSync)(filePath, "utf-8");
4581
+ const content = (0, import_fs9.readFileSync)(filePath, "utf-8");
4389
4582
  const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, "");
4390
4583
  const match = withoutFrontmatter.match(/^#\s+(.+)/m);
4391
4584
  if (match) {
@@ -4403,16 +4596,16 @@ function formatPathRef(contextPath, description, agent) {
4403
4596
  return `- \`${contextPath}\` \u2014 ${description}`;
4404
4597
  }
4405
4598
  function resolveDomainFolder(domain, contextsDir) {
4406
- if ((0, import_fs8.existsSync)(contextsDir)) {
4599
+ if ((0, import_fs9.existsSync)(contextsDir)) {
4407
4600
  try {
4408
- const entries = (0, import_fs8.readdirSync)(contextsDir);
4601
+ const entries = (0, import_fs9.readdirSync)(contextsDir);
4409
4602
  const match = entries.find((e) => e.toLowerCase() === domain.toLowerCase());
4410
- 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) };
4411
4604
  } catch {
4412
4605
  }
4413
4606
  }
4414
4607
  const fallback = domain.toUpperCase();
4415
- return { folderName: fallback, folderPath: (0, import_path8.join)(contextsDir, fallback) };
4608
+ return { folderName: fallback, folderPath: (0, import_path9.join)(contextsDir, fallback) };
4416
4609
  }
4417
4610
  function buildContextRefsSection(domains, agent, contextsDir) {
4418
4611
  const lines2 = [
@@ -4424,34 +4617,34 @@ function buildContextRefsSection(domains, agent, contextsDir) {
4424
4617
  let hasAnyFiles = false;
4425
4618
  for (const domain of domains) {
4426
4619
  const { folderName, folderPath: domainPath } = resolveDomainFolder(domain, contextsDir);
4427
- if (!(0, import_fs8.existsSync)(domainPath)) continue;
4620
+ if (!(0, import_fs9.existsSync)(domainPath)) continue;
4428
4621
  const domainFiles = [];
4429
- const ctxInstructions = (0, import_path8.join)(domainPath, "context-instructions.md");
4430
- if ((0, import_fs8.existsSync)(ctxInstructions)) {
4622
+ const ctxInstructions = (0, import_path9.join)(domainPath, "context-instructions.md");
4623
+ if ((0, import_fs9.existsSync)(ctxInstructions)) {
4431
4624
  const desc = extractFirstHeading(ctxInstructions);
4432
4625
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/context-instructions.md`, desc, agent));
4433
4626
  }
4434
- const instructionsMd = (0, import_path8.join)(domainPath, "core", "instructions.md");
4435
- if ((0, import_fs8.existsSync)(instructionsMd)) {
4627
+ const instructionsMd = (0, import_path9.join)(domainPath, "core", "instructions.md");
4628
+ if ((0, import_fs9.existsSync)(instructionsMd)) {
4436
4629
  const desc = extractFirstHeading(instructionsMd);
4437
4630
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/instructions.md`, `${desc} (start here)`, agent));
4438
4631
  }
4439
- const coreDir = (0, import_path8.join)(domainPath, "core");
4440
- if ((0, import_fs8.existsSync)(coreDir)) {
4632
+ const coreDir = (0, import_path9.join)(domainPath, "core");
4633
+ if ((0, import_fs9.existsSync)(coreDir)) {
4441
4634
  const coreFiles = collectMdFiles(coreDir).filter((f) => f !== "instructions.md" && !f.startsWith("instructions/"));
4442
4635
  for (const file of coreFiles) {
4443
- const desc = extractFirstHeading((0, import_path8.join)(coreDir, file));
4636
+ const desc = extractFirstHeading((0, import_path9.join)(coreDir, file));
4444
4637
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/core/${file}`, desc, agent));
4445
4638
  }
4446
4639
  }
4447
- const refDir = (0, import_path8.join)(domainPath, "reference");
4448
- if ((0, import_fs8.existsSync)(refDir)) {
4640
+ const refDir = (0, import_path9.join)(domainPath, "reference");
4641
+ if ((0, import_fs9.existsSync)(refDir)) {
4449
4642
  const refFiles = collectMdFiles(refDir);
4450
4643
  if (refFiles.length > 0) {
4451
4644
  domainFiles.push(``);
4452
4645
  domainFiles.push(`**Reference & cheat sheets:**`);
4453
4646
  for (const file of refFiles) {
4454
- const desc = extractFirstHeading((0, import_path8.join)(refDir, file));
4647
+ const desc = extractFirstHeading((0, import_path9.join)(refDir, file));
4455
4648
  domainFiles.push(formatPathRef(`_ai-context/${folderName}/reference/${file}`, desc, agent));
4456
4649
  }
4457
4650
  }
@@ -4482,7 +4675,7 @@ function buildMCPSection(mcpConfig) {
4482
4675
  return lines2.join("\n");
4483
4676
  }
4484
4677
  function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4485
- const contextsDir = (0, import_path8.join)(projectPath, "_ai-context");
4678
+ const contextsDir = (0, import_path9.join)(projectPath, "_ai-context");
4486
4679
  const lines2 = [`# AI Development Instructions`, ``, `> Generated by One-Shot Installer`, ``];
4487
4680
  lines2.push(buildContextRefsSection(domains, agent, contextsDir));
4488
4681
  if (Object.keys(mcpConfig).length > 0) lines2.push(buildMCPSection(mcpConfig));
@@ -4490,8 +4683,8 @@ function buildCombinedInstructions(domains, mcpConfig, agent, projectPath) {
4490
4683
  }
4491
4684
 
4492
4685
  // src/steps/setup/write-mcp-config.ts
4493
- var import_fs9 = require("fs");
4494
- var import_path9 = require("path");
4686
+ var import_fs10 = require("fs");
4687
+ var import_path10 = require("path");
4495
4688
  var red2 = (text) => `\x1B[31m${text}\x1B[0m`;
4496
4689
  var write_mcp_config_default = defineStep({
4497
4690
  name: "write-mcp-config",
@@ -4500,15 +4693,15 @@ var write_mcp_config_default = defineStep({
4500
4693
  execute: async (ctx) => {
4501
4694
  const filteredMcpConfig = getFilteredMcpConfig(ctx);
4502
4695
  const target = getAgentTarget(ctx.config.agent);
4503
- const mcpJsonPath = (0, import_path9.join)(ctx.config.projectPath, target.mcpConfig);
4696
+ const mcpJsonPath = (0, import_path10.join)(ctx.config.projectPath, target.mcpConfig);
4504
4697
  if (target.mcpDir) {
4505
- const dir = (0, import_path9.join)(ctx.config.projectPath, target.mcpDir);
4506
- 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 });
4507
4700
  }
4508
4701
  let existingFile = {};
4509
- if ((0, import_fs9.existsSync)(mcpJsonPath)) {
4702
+ if ((0, import_fs10.existsSync)(mcpJsonPath)) {
4510
4703
  try {
4511
- existingFile = JSON.parse((0, import_fs9.readFileSync)(mcpJsonPath, "utf-8"));
4704
+ existingFile = JSON.parse((0, import_fs10.readFileSync)(mcpJsonPath, "utf-8"));
4512
4705
  } catch {
4513
4706
  const box = [
4514
4707
  "",
@@ -4534,7 +4727,7 @@ var write_mcp_config_default = defineStep({
4534
4727
  }
4535
4728
  const mergedServers = { ...existingServers, ...newServers };
4536
4729
  const outputFile = { ...existingFile, [target.mcpRootKey]: mergedServers };
4537
- (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");
4538
4731
  ctx.installed.mcpServersAdded = addedServers;
4539
4732
  return {
4540
4733
  status: "success",
@@ -4603,7 +4796,7 @@ function isClaudeCliAvailable() {
4603
4796
  }
4604
4797
 
4605
4798
  // src/steps/setup/update-gitignore.ts
4606
- var import_path10 = require("path");
4799
+ var import_path11 = require("path");
4607
4800
  var MARKER_START2 = "# one-shot-installer:start";
4608
4801
  var MARKER_END2 = "# one-shot-installer:end";
4609
4802
  var CORE_GITIGNORE_ENTRIES = [
@@ -4624,15 +4817,15 @@ var update_gitignore_default = defineStep({
4624
4817
  name: "update-gitignore",
4625
4818
  label: "Updating .gitignore",
4626
4819
  execute: async (ctx) => {
4627
- const gitignorePath = (0, import_path10.join)(ctx.config.projectPath, ".gitignore");
4820
+ const gitignorePath = (0, import_path11.join)(ctx.config.projectPath, ".gitignore");
4628
4821
  upsertMarkerBlock(gitignorePath, MARKER_START2, MARKER_END2, CORE_GITIGNORE_ENTRIES.join("\n"));
4629
4822
  return { status: "success" };
4630
4823
  }
4631
4824
  });
4632
4825
 
4633
4826
  // src/steps/setup/write-state.ts
4634
- var import_fs10 = require("fs");
4635
- var import_path11 = require("path");
4827
+ var import_fs11 = require("fs");
4828
+ var import_path12 = require("path");
4636
4829
  var import_os2 = require("os");
4637
4830
 
4638
4831
  // package.json
@@ -4666,7 +4859,7 @@ var write_state_default = defineStep({
4666
4859
  name: "write-state",
4667
4860
  label: "Saving installation state",
4668
4861
  execute: async (ctx) => {
4669
- 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");
4670
4863
  const mcpServersAdded = ctx.installed.mcpServersAdded ?? [];
4671
4864
  const filteredMcpConfig = Object.fromEntries(
4672
4865
  Object.entries(ctx.config.mcpConfig).filter(([name]) => mcpServersAdded.includes(name))
@@ -4684,15 +4877,17 @@ var write_state_default = defineStep({
4684
4877
  mcpServers: mcpServersAdded,
4685
4878
  mcpConfigs: filteredMcpConfig,
4686
4879
  factoryInstalled: ctx.installed.factoryInstalled ?? false,
4880
+ abapHooksInstalled: ctx.installed.abapHooksInstalled ?? false,
4687
4881
  files: {
4688
4882
  instructions: target.instructions,
4689
4883
  mcpConfig: target.mcpConfig,
4690
4884
  contexts: "_ai-context/",
4691
4885
  factory: ctx.installed.factoryInstalled ? "factory/" : null,
4692
- 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")
4693
4888
  }
4694
4889
  };
4695
- (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");
4696
4891
  return { status: "success" };
4697
4892
  }
4698
4893
  });
@@ -4707,19 +4902,19 @@ var dim = (text) => `\x1B[2m${text}\x1B[0m`;
4707
4902
  var yellow = (text) => `\x1B[33m${text}\x1B[0m`;
4708
4903
  var green = (text) => `\x1B[32m${text}\x1B[0m`;
4709
4904
  function detectMarkerFileMode(filePath, markerStart) {
4710
- if (!(0, import_fs11.existsSync)(filePath)) return green("create");
4711
- 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");
4712
4907
  if (content.includes(markerStart)) return yellow("update");
4713
4908
  return yellow("append");
4714
4909
  }
4715
4910
  function detectMCPFileMode(filePath) {
4716
- if (!(0, import_fs11.existsSync)(filePath)) return green("create");
4911
+ if (!(0, import_fs12.existsSync)(filePath)) return green("create");
4717
4912
  return yellow("merge");
4718
4913
  }
4719
4914
  function detectContextMode(projectPath, domain) {
4720
- const contextDir = (0, import_path12.join)(projectPath, "_ai-context", domain.toUpperCase());
4721
- const contextDirLower = (0, import_path12.join)(projectPath, "_ai-context", domain);
4722
- 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");
4723
4918
  return green("create");
4724
4919
  }
4725
4920
  function waitForEnter() {
@@ -4737,10 +4932,11 @@ async function main() {
4737
4932
  try {
4738
4933
  const token = await getGitHubToken();
4739
4934
  console.log(" Locating repositories...");
4740
- const [repo, contextRepo, factoryRepo] = await Promise.all([
4935
+ const [repo, contextRepo, factoryRepo, abapHooksRepo] = await Promise.all([
4741
4936
  discoverRepo(token),
4742
4937
  discoverRepo(token, "context-data").catch(() => null),
4743
- discoverRepo(token, "factory-data").catch(() => null)
4938
+ discoverRepo(token, "factory-data").catch(() => null),
4939
+ discoverRepo(token, "abap-mcp-hooks").catch(() => null)
4744
4940
  ]);
4745
4941
  if (!contextRepo) {
4746
4942
  console.log(" \u26A0 Context repo not found \u2014 AI contexts will not be installed.");
@@ -4748,9 +4944,12 @@ async function main() {
4748
4944
  if (!factoryRepo) {
4749
4945
  console.log(" \u26A0 Factory repo not found \u2014 Factory will not be installed.");
4750
4946
  }
4947
+ if (!abapHooksRepo) {
4948
+ console.log(" \u26A0 ABAP MCP hooks repo not found \u2014 hooks will not be installed.");
4949
+ }
4751
4950
  console.log(" Loading configuration...");
4752
4951
  const options = await loadWizardOptions(token, repo);
4753
- const config = await collectInputs(options, options.releaseVersion, !!factoryRepo);
4952
+ const config = await collectInputs(options, options.releaseVersion, !!factoryRepo, !!abapHooksRepo);
4754
4953
  if (!config) {
4755
4954
  await waitForEnter();
4756
4955
  return;
@@ -4767,6 +4966,7 @@ async function main() {
4767
4966
  repo,
4768
4967
  contextRepo,
4769
4968
  factoryRepo,
4969
+ abapHooksRepo,
4770
4970
  installed: {}
4771
4971
  };
4772
4972
  const stepList = Object.values(steps_exports);
@@ -4779,7 +4979,7 @@ async function main() {
4779
4979
  await waitForEnter();
4780
4980
  }
4781
4981
  }
4782
- async function collectInputs(options, releaseVersion, factoryAvailable = false) {
4982
+ async function collectInputs(options, releaseVersion, factoryAvailable = false, abapHooksAvailable = false) {
4783
4983
  const selectedIds = await esm_default2({
4784
4984
  message: "Technology:",
4785
4985
  instructions: " Space to select \xB7 Enter to confirm",
@@ -4817,17 +5017,20 @@ async function collectInputs(options, releaseVersion, factoryAvailable = false)
4817
5017
  }
4818
5018
  const projectInput = await esm_default4({
4819
5019
  message: "Project directory:",
4820
- default: (0, import_path12.resolve)(process.cwd())
5020
+ default: (0, import_path13.resolve)(process.cwd())
4821
5021
  });
5022
+ const isAbapSelected = selectedTechnologies.some((t) => t.domains.includes("ABAP"));
5023
+ const installAbapHooks = abapHooksAvailable && agent === "claude-code" && isAbapSelected;
4822
5024
  return {
4823
5025
  technologies: selectedTechnologies,
4824
5026
  agent,
4825
5027
  azureDevOpsOrg,
4826
- projectPath: (0, import_path12.resolve)(projectInput),
5028
+ projectPath: (0, import_path13.resolve)(projectInput),
4827
5029
  baseMcpServers: options.baseMcpServers,
4828
5030
  mcpConfig,
4829
5031
  releaseVersion,
4830
- installFactory: factoryAvailable
5032
+ installFactory: factoryAvailable,
5033
+ installAbapHooks
4831
5034
  };
4832
5035
  }
4833
5036
  async function previewAndConfirm(config, options) {
@@ -4837,9 +5040,9 @@ async function previewAndConfirm(config, options) {
4837
5040
  const instructionFile = target.instructions;
4838
5041
  const mcpConfigFile = target.mcpConfig;
4839
5042
  const serverEntries = Object.entries(config.mcpConfig);
4840
- const instructionFilePath = (0, import_path12.join)(config.projectPath, instructionFile);
4841
- const mcpConfigFilePath = (0, import_path12.join)(config.projectPath, mcpConfigFile);
4842
- 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");
4843
5046
  const instructionMode = detectMarkerFileMode(instructionFilePath, "<!-- one-shot-installer:start -->");
4844
5047
  const mcpMode = detectMCPFileMode(mcpConfigFilePath);
4845
5048
  const gitignoreMode = detectMarkerFileMode(gitignorePath, "# one-shot-installer:start");
@@ -4854,6 +5057,7 @@ async function previewAndConfirm(config, options) {
4854
5057
  console.log(` Technology ${technologyNames}`);
4855
5058
  console.log(` Tool ${agentDisplay}`);
4856
5059
  console.log(` Factory ${config.installFactory ? "yes" : "no"}`);
5060
+ console.log(` ABAP hooks ${config.installAbapHooks ? "yes" : "no"}`);
4857
5061
  console.log(` Directory ${config.projectPath}`);
4858
5062
  console.log("");
4859
5063
  console.log(` ${dim("File actions:")}`);
@@ -4865,10 +5069,15 @@ async function previewAndConfirm(config, options) {
4865
5069
  console.log(` ${mcpConfigFile.padEnd(domainColWidth + 14)}${mcpMode}`);
4866
5070
  console.log(` ${".gitignore".padEnd(domainColWidth + 14)}${gitignoreMode}`);
4867
5071
  if (config.installFactory) {
4868
- 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"));
4869
5073
  const factoryMode = factoryExists ? yellow("overwrite") : green("create");
4870
5074
  console.log(` ${"factory/".padEnd(domainColWidth + 14)}${factoryMode}`);
4871
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
+ }
4872
5081
  if (serverEntries.length > 0) {
4873
5082
  console.log("");
4874
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.0",
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"