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.
- package/bin/installer.js +279 -70
- 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
|
|
3247
|
+
var import_fs12 = require("fs");
|
|
3248
3248
|
var import_readline = require("readline");
|
|
3249
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
3925
|
-
//
|
|
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
|
|
4045
|
-
|
|
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(
|
|
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/
|
|
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,
|
|
4337
|
-
const fileDir = (0,
|
|
4338
|
-
if (!(0,
|
|
4339
|
-
if (agent === "cursor" && !(0,
|
|
4340
|
-
(0,
|
|
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,
|
|
4380
|
-
const entries = (0,
|
|
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,
|
|
4383
|
-
return entry.endsWith(".md") && (0,
|
|
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,
|
|
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,
|
|
4599
|
+
if ((0, import_fs9.existsSync)(contextsDir)) {
|
|
4407
4600
|
try {
|
|
4408
|
-
const entries = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
4620
|
+
if (!(0, import_fs9.existsSync)(domainPath)) continue;
|
|
4428
4621
|
const domainFiles = [];
|
|
4429
|
-
const ctxInstructions = (0,
|
|
4430
|
-
if ((0,
|
|
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,
|
|
4435
|
-
if ((0,
|
|
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,
|
|
4440
|
-
if ((0,
|
|
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,
|
|
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,
|
|
4448
|
-
if ((0,
|
|
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,
|
|
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,
|
|
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
|
|
4494
|
-
var
|
|
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,
|
|
4696
|
+
const mcpJsonPath = (0, import_path10.join)(ctx.config.projectPath, target.mcpConfig);
|
|
4504
4697
|
if (target.mcpDir) {
|
|
4505
|
-
const dir = (0,
|
|
4506
|
-
if (!(0,
|
|
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,
|
|
4702
|
+
if ((0, import_fs10.existsSync)(mcpJsonPath)) {
|
|
4510
4703
|
try {
|
|
4511
|
-
existingFile = JSON.parse((0,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
4635
|
-
var
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
4711
|
-
const content = (0,
|
|
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,
|
|
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,
|
|
4721
|
-
const contextDirLower = (0,
|
|
4722
|
-
if ((0,
|
|
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,
|
|
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,
|
|
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,
|
|
4841
|
-
const mcpConfigFilePath = (0,
|
|
4842
|
-
const gitignorePath = (0,
|
|
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,
|
|
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:")}`);
|