delegation-layer 0.1.28 → 0.1.29

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 (35) hide show
  1. package/README.md +58 -14
  2. package/docs/assets/delegation-layer-hero.jpg +0 -0
  3. package/package.json +3 -2
  4. package/plugins/delegation-layer/.claude-plugin/plugin.json +1 -1
  5. package/plugins/delegation-layer/.codex-plugin/plugin.json +1 -1
  6. package/plugins/delegation-layer/skills/agent-integration/SKILL.md +27 -8
  7. package/plugins/delegation-layer/skills/agent-integration/examples/json-results.md +6 -7
  8. package/plugins/delegation-layer/skills/agent-integration/examples/managed-workspace.md +68 -0
  9. package/plugins/delegation-layer/skills/agent-integration/references/collaboration.md +22 -6
  10. package/plugins/delegation-layer/skills/agent-integration/references/command-reference.md +53 -20
  11. package/plugins/delegation-layer/skills/agent-integration/references/continuation.md +36 -0
  12. package/plugins/delegation-layer/skills/agent-integration/references/result-handling.md +46 -2
  13. package/plugins/delegation-layer/skills/agent-integration/references/workspaces.md +197 -0
  14. package/scripts/install-agent-integration.mjs +15 -829
  15. package/scripts/install-cli.mjs +26 -66
  16. package/scripts/installer/application.mjs +250 -0
  17. package/scripts/installer/arguments.mjs +123 -0
  18. package/scripts/installer/catalog.mjs +95 -0
  19. package/scripts/installer/detection.mjs +41 -0
  20. package/scripts/installer/filesystem.mjs +97 -0
  21. package/scripts/installer/paths.mjs +14 -0
  22. package/scripts/installer/picker-state.mjs +46 -0
  23. package/scripts/installer/routing-rules.mjs +99 -0
  24. package/scripts/installer/routing-store.mjs +151 -0
  25. package/scripts/installer/selection.mjs +34 -0
  26. package/scripts/installer/skill-store.mjs +33 -0
  27. package/scripts/installer/terminal.mjs +104 -0
  28. package/skills/agent-integration/SKILL.md +27 -8
  29. package/skills/agent-integration/examples/json-results.md +6 -7
  30. package/skills/agent-integration/examples/managed-workspace.md +68 -0
  31. package/skills/agent-integration/references/collaboration.md +22 -6
  32. package/skills/agent-integration/references/command-reference.md +53 -20
  33. package/skills/agent-integration/references/continuation.md +36 -0
  34. package/skills/agent-integration/references/result-handling.md +46 -2
  35. package/skills/agent-integration/references/workspaces.md +197 -0
@@ -1,77 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "node:child_process";
4
- import { dirname, isAbsolute, join, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
-
7
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
- const installerPath = join(packageRoot, "install.sh");
9
-
10
- function usage(message) {
11
- if (message) {
12
- console.error(`Error: ${message}`);
13
- }
14
- console.error(
15
- "Usage: delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]",
16
- );
17
- }
18
-
19
- function parseArguments(argumentsList) {
20
- let version;
21
- let installDir;
22
-
23
- for (let index = 0; index < argumentsList.length; index += 1) {
24
- const argument = argumentsList[index];
25
- if (argument === "--version") {
26
- version = argumentsList[index + 1];
27
- index += 1;
28
- if (!version) {
29
- throw new Error("--version requires a value");
30
- }
31
- continue;
32
- }
33
- if (argument === "--install-dir") {
34
- installDir = argumentsList[index + 1];
35
- index += 1;
36
- if (!installDir) {
37
- throw new Error("--install-dir requires a value");
38
- }
39
- if (!isAbsolute(installDir)) {
40
- throw new Error("--install-dir must be an absolute path");
41
- }
42
- continue;
43
- }
44
- if (argument === "--help" || argument === "-h") {
45
- usage();
46
- return null;
47
- }
48
- throw new Error(`unknown argument ${argument}`);
49
- }
50
-
51
- if (process.platform === "win32") {
52
- throw new Error("the release installer supports Linux and macOS only");
53
- }
54
-
55
- return { installDir: installDir ? resolve(installDir) : undefined, version };
56
- }
57
-
58
- export function main(argumentsList) {
59
- const options = parseArguments(argumentsList);
60
- if (!options) {
61
- return Promise.resolve();
4
+ import { join } from "node:path";
5
+ import { parseInstallCliArguments } from "./installer/arguments.mjs";
6
+
7
+ const usageText = "Usage: delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]";
8
+
9
+ export async function main(argumentsList, {
10
+ packageRoot,
11
+ environment = process.env,
12
+ platform = process.platform,
13
+ spawnProcess = spawn,
14
+ errorOutput = process.stderr,
15
+ } = {}) {
16
+ const parsed = parseInstallCliArguments(argumentsList, { platform });
17
+ if (parsed.kind === "error") throw new Error(parsed.message);
18
+ if (parsed.kind === "help") {
19
+ errorOutput.write(`${usageText}\n`);
20
+ return;
62
21
  }
22
+ if (!packageRoot) throw new Error("install-cli requires a package root");
63
23
 
64
- const environment = { ...process.env };
65
- if (options.version) {
66
- environment.DELEGATION_LAYER_VERSION = options.version;
24
+ const childEnvironment = { ...environment };
25
+ if (parsed.options.version) {
26
+ childEnvironment.DELEGATION_LAYER_VERSION = parsed.options.version;
67
27
  }
68
- if (options.installDir) {
69
- environment.DELEGATION_LAYER_INSTALL_DIR = options.installDir;
28
+ if (parsed.options.installDir) {
29
+ childEnvironment.DELEGATION_LAYER_INSTALL_DIR = parsed.options.installDir;
70
30
  }
71
31
 
72
- return new Promise((resolvePromise, reject) => {
73
- const child = spawn("sh", [installerPath], {
74
- env: environment,
32
+ await new Promise((resolvePromise, reject) => {
33
+ const child = spawnProcess("sh", [join(packageRoot, "install.sh")], {
34
+ env: childEnvironment,
75
35
  stdio: "inherit",
76
36
  });
77
37
  child.on("error", reject);
@@ -0,0 +1,250 @@
1
+ import { join } from "node:path";
2
+ import { harnesses } from "./catalog.mjs";
3
+ import { parseInstallArguments, parseLegacyArguments } from "./arguments.mjs";
4
+ import { detectHarnesses } from "./detection.mjs";
5
+ import { validateSkillSource } from "./filesystem.mjs";
6
+ import { installSkill } from "./skill-store.mjs";
7
+ import { defaultHarnessNames, formatDetected, resolveInstallSelection, targetFor } from "./selection.mjs";
8
+ import { pickOptions } from "./terminal.mjs";
9
+ import { applyRouting, prepareRouting, printRouting } from "./routing-store.mjs";
10
+
11
+ const usageText = "Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force] [--routing default|skill-only]\n delegation-layer routing status|disable [--scope project|global] [--harness NAME[,NAME...]]\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]";
12
+ const helpText = `${usageText}\n\nThe install command detects supported agent harnesses and installs the skill for a project or user.\nGlobal scope is the default; use --scope project for a project-local install.\nUse --harness universal when no specific harness is detected.`;
13
+
14
+ function isFailure(value) {
15
+ return value.kind === "error";
16
+ }
17
+
18
+ function printUsage(errorOutput, message) {
19
+ if (message) errorOutput.write(`Error: ${message}\n`);
20
+ errorOutput.write(`${usageText}\n`);
21
+ }
22
+
23
+ function printDetections(detections, report) {
24
+ report("Detected harnesses:");
25
+ detections.forEach(({ harness, reasons }, index) => {
26
+ report(` ${index + 1}. ${harness.label}${formatDetected(reasons)}`);
27
+ });
28
+ }
29
+
30
+ async function chooseInstallOptions({
31
+ force,
32
+ harnessNames,
33
+ scope,
34
+ yes,
35
+ routing,
36
+ cwd,
37
+ home,
38
+ environment,
39
+ platform,
40
+ input,
41
+ output,
42
+ report,
43
+ }) {
44
+ const detections = await detectHarnesses({ cwd, home, path: environment.PATH ?? "", platform });
45
+ const defaults = defaultHarnessNames(detections);
46
+ const canPrompt = Boolean(input.isTTY && output.isTTY);
47
+ const needsPrompt = !yes && !harnessNames && !scope && canPrompt;
48
+
49
+ if (!harnessNames && !needsPrompt) printDetections(detections, report);
50
+ if (!canPrompt && !yes && !harnessNames && !scope) {
51
+ report("No interactive terminal detected; using global scope and detected harnesses.");
52
+ }
53
+
54
+ if (needsPrompt) {
55
+ scope = await pickOptions({
56
+ initialValues: ["global"],
57
+ options: [
58
+ { value: "global", label: "Global", description: `available to your agents from ${home}` },
59
+ { value: "project", label: "Project", description: `available to this project from ${cwd}` },
60
+ ],
61
+ title: "Where should the agent skill be installed?",
62
+ input,
63
+ output,
64
+ });
65
+ if (!scope) return null;
66
+ harnessNames = await pickOptions({
67
+ initialValues: defaults,
68
+ multiple: true,
69
+ options: detections.map(({ harness, reasons }) => ({
70
+ value: harness.name,
71
+ label: `${harness.label}${formatDetected(reasons)}`,
72
+ description: targetFor(harness, scope, cwd, home, environment),
73
+ })),
74
+ title: "Which harnesses should use the skill?",
75
+ input,
76
+ output,
77
+ });
78
+ if (!harnessNames) return null;
79
+ }
80
+
81
+ if (needsPrompt && !routing) {
82
+ routing = await pickOptions({
83
+ initialValues: ["default"],
84
+ options: [
85
+ {
86
+ value: "default",
87
+ label: "Prefer Delegate",
88
+ description: "Install collaboration context hooks for Claude and Codex; native hook trust still applies",
89
+ },
90
+ {
91
+ value: "skill-only",
92
+ label: "Skill only",
93
+ description: "Install guidance without adding hooks; keep existing routing settings",
94
+ },
95
+ ],
96
+ title: "How should external collaboration be routed?",
97
+ input,
98
+ output,
99
+ });
100
+ if (!routing) return null;
101
+ }
102
+
103
+ return resolveInstallSelection({ force, harnessNames, scope, routing, cwd, home, detections });
104
+ }
105
+
106
+ function parseResult(parsed, errorOutput, output) {
107
+ if (parsed.kind === "help") {
108
+ output.write(`${helpText}\n`);
109
+ return 0;
110
+ }
111
+ if (isFailure(parsed)) {
112
+ printUsage(errorOutput, parsed.message);
113
+ return 2;
114
+ }
115
+ return null;
116
+ }
117
+
118
+ export async function runInstaller({
119
+ argumentsList,
120
+ packageRoot,
121
+ cwd,
122
+ home,
123
+ resolveHome,
124
+ environment,
125
+ platform,
126
+ input,
127
+ output,
128
+ errorOutput,
129
+ installCliMain,
130
+ }) {
131
+ const report = (message) => output.write(`${message}\n`);
132
+ const reportError = (error) => {
133
+ const message = error instanceof Error ? error.message : String(error);
134
+ errorOutput.write(`Error: ${message}\n`);
135
+ };
136
+ const sourceDirectory = join(packageRoot, "skills", "agent-integration");
137
+
138
+ try {
139
+ await validateSkillSource(sourceDirectory);
140
+ } catch (error) {
141
+ reportError(error);
142
+ return 1;
143
+ }
144
+
145
+ if (argumentsList[0] === "install-cli") {
146
+ try {
147
+ await installCliMain(argumentsList.slice(1), { packageRoot, environment, platform });
148
+ return 0;
149
+ } catch (error) {
150
+ reportError(error);
151
+ return 1;
152
+ }
153
+ }
154
+
155
+ if (argumentsList[0] === "routing") {
156
+ const action = argumentsList[1];
157
+ const parsed = parseInstallArguments(argumentsList.slice(2));
158
+ const parseExit = parseResult(parsed, errorOutput, output);
159
+ if (parseExit !== null) return parseExit;
160
+ if (!["status", "disable"].includes(action)) {
161
+ printUsage(errorOutput, "routing requires status or disable");
162
+ return 2;
163
+ }
164
+ const options = parsed.options;
165
+ try {
166
+ if (options.routing || options.force) {
167
+ throw new Error("routing status/disable do not accept --routing or --force");
168
+ }
169
+ const selected = await chooseInstallOptions({
170
+ ...options,
171
+ yes: true,
172
+ harnessNames: options.harnessNames ?? ["claude", "codex"],
173
+ cwd,
174
+ home: home ?? resolveHome(),
175
+ environment,
176
+ platform,
177
+ input,
178
+ output,
179
+ report,
180
+ });
181
+ const plans = await prepareRouting(selected, action, { environment, platform, sourceDirectory });
182
+ if (action === "disable") await applyRouting(plans);
183
+ for (const plan of plans) printRouting(plan, action, report);
184
+ return 0;
185
+ } catch (error) {
186
+ reportError(error);
187
+ return 1;
188
+ }
189
+ }
190
+
191
+ if (argumentsList[0] === "install") {
192
+ const parsed = parseInstallArguments(argumentsList.slice(1));
193
+ const parseExit = parseResult(parsed, errorOutput, output);
194
+ if (parseExit !== null) return parseExit;
195
+ try {
196
+ const selected = await chooseInstallOptions({
197
+ ...parsed.options,
198
+ cwd,
199
+ home: home ?? resolveHome(),
200
+ environment,
201
+ platform,
202
+ input,
203
+ output,
204
+ report,
205
+ });
206
+ if (!selected) {
207
+ report("Installation cancelled.");
208
+ return 0;
209
+ }
210
+ const routingPlans = selected.routing === "default"
211
+ ? await prepareRouting(selected, "enable", { environment, platform, sourceDirectory })
212
+ : [];
213
+ const targets = selected.harnessNames.map((name) => {
214
+ const harness = harnesses.find((candidate) => candidate.name === name);
215
+ if (!harness) throw new Error(`cannot install for unknown harness: ${name}`);
216
+ return targetFor(harness, selected.scope, selected.cwd, selected.home, environment);
217
+ });
218
+ const uniqueTargets = [...new Set(targets)];
219
+ report(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
220
+ for (const target of uniqueTargets) {
221
+ await installSkill({ force: selected.force, sourceDirectory, target, report });
222
+ }
223
+ await applyRouting(routingPlans);
224
+ for (const plan of routingPlans) printRouting(plan, "enable", report);
225
+ if (selected.routing === "skill-only") {
226
+ report("Skill-only installation; existing routing settings were not changed.");
227
+ }
228
+ return 0;
229
+ } catch (error) {
230
+ reportError(error);
231
+ return 1;
232
+ }
233
+ }
234
+
235
+ const parsed = parseLegacyArguments(argumentsList);
236
+ const parseExit = parseResult(parsed, errorOutput, output);
237
+ if (parseExit !== null) return parseExit;
238
+ try {
239
+ await installSkill({
240
+ force: parsed.options.force,
241
+ sourceDirectory,
242
+ target: parsed.options.target,
243
+ report,
244
+ });
245
+ return 0;
246
+ } catch (error) {
247
+ reportError(error);
248
+ return 1;
249
+ }
250
+ }
@@ -0,0 +1,123 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { harnesses, isHarnessName } from "./catalog.mjs";
3
+ import { normalizeAbsolute } from "./paths.mjs";
4
+
5
+ function error(message) {
6
+ return { kind: "error", message };
7
+ }
8
+
9
+ function requiredValue(argumentsList, index) {
10
+ const value = argumentsList[index + 1];
11
+ return value && !value.startsWith("--") ? value : null;
12
+ }
13
+
14
+ export function parseLegacyArguments(argumentsList) {
15
+ let target;
16
+ let force = false;
17
+
18
+ for (let index = 0; index < argumentsList.length; index += 1) {
19
+ const argument = argumentsList[index];
20
+ if (argument === "--force") {
21
+ force = true;
22
+ continue;
23
+ }
24
+ if (argument === "--target") {
25
+ target = requiredValue(argumentsList, index);
26
+ if (!target) return error("--target requires a value");
27
+ index += 1;
28
+ continue;
29
+ }
30
+ if (argument === "--help" || argument === "-h") {
31
+ return { kind: "help" };
32
+ }
33
+ return error(`unknown argument ${argument}`);
34
+ }
35
+
36
+ if (!target) return error("--target is required");
37
+ if (!isAbsolute(target)) return error("--target must be an absolute path");
38
+ return { kind: "options", options: { force, target: normalizeAbsolute(target) } };
39
+ }
40
+
41
+ export function parseInstallArguments(argumentsList) {
42
+ let force = false;
43
+ let harnessNames;
44
+ let scope;
45
+ let yes = false;
46
+ let routing;
47
+
48
+ for (let index = 0; index < argumentsList.length; index += 1) {
49
+ const argument = argumentsList[index];
50
+ if (argument === "--force") {
51
+ force = true;
52
+ continue;
53
+ }
54
+ if (argument === "--yes" || argument === "-y") {
55
+ yes = true;
56
+ continue;
57
+ }
58
+ if (["--routing", "--scope", "--harness"].includes(argument)) {
59
+ const value = requiredValue(argumentsList, index);
60
+ if (!value) return error(`${argument} requires a value`);
61
+ if (argument === "--routing") routing = value;
62
+ if (argument === "--scope") scope = value;
63
+ if (argument === "--harness") {
64
+ harnessNames = value.split(",").map((name) => name.trim().toLowerCase());
65
+ }
66
+ index += 1;
67
+ continue;
68
+ }
69
+ if (argument === "--help" || argument === "-h") {
70
+ return { kind: "help" };
71
+ }
72
+ return error(`unknown argument ${argument}`);
73
+ }
74
+
75
+ if (scope && scope !== "project" && scope !== "global") {
76
+ return error("--scope must be project or global");
77
+ }
78
+ if (harnessNames?.length === 0 || harnessNames?.some((name) => !name)) {
79
+ return error("--harness requires at least one harness name");
80
+ }
81
+ const unknownHarness = harnessNames?.find((name) => !isHarnessName(name));
82
+ if (unknownHarness) {
83
+ return error(`unknown harness ${unknownHarness}; choose from ${harnesses.map((harness) => harness.name).join(", ")}`);
84
+ }
85
+ if (routing && !["default", "skill-only"].includes(routing)) {
86
+ return error("--routing must be default or skill-only");
87
+ }
88
+ return { kind: "options", options: { force, harnessNames, scope, yes, routing } };
89
+ }
90
+
91
+ export function parseInstallCliArguments(argumentsList, { platform }) {
92
+ let version;
93
+ let installDir;
94
+
95
+ for (let index = 0; index < argumentsList.length; index += 1) {
96
+ const argument = argumentsList[index];
97
+ if (argument === "--version") {
98
+ version = argumentsList[index + 1];
99
+ index += 1;
100
+ if (!version) return error("--version requires a value");
101
+ continue;
102
+ }
103
+ if (argument === "--install-dir") {
104
+ installDir = argumentsList[index + 1];
105
+ index += 1;
106
+ if (!installDir) return error("--install-dir requires a value");
107
+ if (!isAbsolute(installDir)) return error("--install-dir must be an absolute path");
108
+ continue;
109
+ }
110
+ if (argument === "--help" || argument === "-h") {
111
+ return { kind: "help" };
112
+ }
113
+ return error(`unknown argument ${argument}`);
114
+ }
115
+
116
+ if (platform === "win32") {
117
+ return error("the release installer supports Linux and macOS only");
118
+ }
119
+ return {
120
+ kind: "options",
121
+ options: { installDir: installDir ? normalizeAbsolute(installDir) : undefined, version },
122
+ };
123
+ }
@@ -0,0 +1,95 @@
1
+ function freezeHarness(harness) {
2
+ return Object.freeze({
3
+ ...harness,
4
+ projectPath: Object.freeze(harness.projectPath),
5
+ globalPath: Object.freeze(harness.globalPath),
6
+ projectMarkers: Object.freeze(harness.projectMarkers.map((marker) => Object.freeze(marker))),
7
+ globalMarkers: Object.freeze(harness.globalMarkers.map((marker) => Object.freeze(marker))),
8
+ commands: Object.freeze(harness.commands),
9
+ });
10
+ }
11
+
12
+ export const harnesses = Object.freeze([
13
+ freezeHarness({
14
+ name: "universal",
15
+ label: "Universal Agent Skills",
16
+ projectPath: [".agents", "skills"],
17
+ globalPath: [".agents", "skills"],
18
+ projectMarkers: [[".agents"]],
19
+ globalMarkers: [[".agents"]],
20
+ commands: [],
21
+ }),
22
+ freezeHarness({
23
+ name: "codex",
24
+ label: "Codex",
25
+ projectPath: [".agents", "skills"],
26
+ globalPath: [".agents", "skills"],
27
+ projectMarkers: [[".agents"], [".codex"]],
28
+ globalMarkers: [[".agents"], [".codex"]],
29
+ commands: ["codex"],
30
+ }),
31
+ freezeHarness({
32
+ name: "claude",
33
+ label: "Claude Code",
34
+ projectPath: [".claude", "skills"],
35
+ globalPath: [".claude", "skills"],
36
+ projectMarkers: [[".claude"]],
37
+ globalMarkers: [[".claude"]],
38
+ commands: ["claude"],
39
+ }),
40
+ freezeHarness({
41
+ name: "cursor",
42
+ label: "Cursor",
43
+ projectPath: [".cursor", "skills"],
44
+ globalPath: [".cursor", "skills"],
45
+ projectMarkers: [[".cursor"]],
46
+ globalMarkers: [[".cursor"]],
47
+ commands: ["cursor"],
48
+ }),
49
+ freezeHarness({
50
+ name: "gemini",
51
+ label: "Gemini CLI",
52
+ projectPath: [".gemini", "skills"],
53
+ globalPath: [".gemini", "skills"],
54
+ projectMarkers: [[".gemini"]],
55
+ globalMarkers: [[".gemini"]],
56
+ commands: ["gemini"],
57
+ }),
58
+ freezeHarness({
59
+ name: "opencode",
60
+ label: "OpenCode",
61
+ projectPath: [".opencode", "skills"],
62
+ globalPath: [".config", "opencode", "skills"],
63
+ projectMarkers: [[".opencode"]],
64
+ globalMarkers: [[".config", "opencode"]],
65
+ commands: ["opencode"],
66
+ }),
67
+ freezeHarness({
68
+ name: "pi",
69
+ label: "Pi",
70
+ projectPath: [".pi", "skills"],
71
+ globalPath: [".pi", "agent", "skills"],
72
+ projectMarkers: [[".pi"]],
73
+ globalMarkers: [[".pi", "agent"]],
74
+ commands: ["pi"],
75
+ }),
76
+ freezeHarness({
77
+ name: "hermes",
78
+ label: "Hermes",
79
+ projectPath: [".hermes", "skills"],
80
+ globalPath: [".hermes", "skills"],
81
+ projectMarkers: [[".hermes"]],
82
+ globalMarkers: [[".hermes"]],
83
+ commands: ["hermes"],
84
+ }),
85
+ ]);
86
+
87
+ const harnessesByName = new Map(harnesses.map((harness) => [harness.name, harness]));
88
+
89
+ export function getHarness(name) {
90
+ return harnessesByName.get(name) ?? null;
91
+ }
92
+
93
+ export function isHarnessName(name) {
94
+ return harnessesByName.has(name);
95
+ }
@@ -0,0 +1,41 @@
1
+ import { accessSync, constants } from "node:fs";
2
+ import { delimiter, join } from "node:path";
3
+ import { harnesses } from "./catalog.mjs";
4
+ import { existingPath } from "./filesystem.mjs";
5
+
6
+ function executableOnPath(command, path, platform) {
7
+ const candidates = platform === "win32" ? [command, `${command}.exe`, `${command}.cmd`] : [command];
8
+ return path.split(delimiter).filter(Boolean).some((pathEntry) => candidates.some((candidate) => {
9
+ try {
10
+ accessSync(join(pathEntry, candidate), constants.X_OK);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }));
16
+ }
17
+
18
+ export async function detectHarnesses({ cwd, home, path = "", platform = process.platform }) {
19
+ return Promise.all(harnesses.map(async (harness) => {
20
+ const reasons = [];
21
+ for (const marker of harness.projectMarkers) {
22
+ if ((await existingPath(join(cwd, ...marker)))?.isDirectory()) {
23
+ reasons.push(`project ${join(...marker)}`);
24
+ break;
25
+ }
26
+ }
27
+ for (const marker of harness.globalMarkers) {
28
+ if ((await existingPath(join(home, ...marker)))?.isDirectory()) {
29
+ reasons.push(`global ${join(...marker)}`);
30
+ break;
31
+ }
32
+ }
33
+ for (const command of harness.commands) {
34
+ if (executableOnPath(command, path, platform)) {
35
+ reasons.push(`${command} on PATH`);
36
+ break;
37
+ }
38
+ }
39
+ return { harness, reasons };
40
+ }));
41
+ }