cairn-ai 0.2.5 → 0.2.7
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/.agents/workflows/cairn-memory.md +2 -2
- package/.agents/workflows/cairn-plan.md +7 -3
- package/.agents/workflows/cairn-review.md +2 -2
- package/.agents/workflows/cairn-work.md +7 -3
- package/.claude/agents/explorer.md +2 -2
- package/.claude/agents/worker.md +2 -2
- package/.claude/commands/cairn-memory.md +1 -1
- package/.claude/commands/cairn-plan.md +1 -1
- package/.claude/commands/cairn-review.md +1 -1
- package/.claude/commands/cairn-work.md +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/README.de.md +3 -2
- package/README.es.md +3 -2
- package/README.fr.md +3 -2
- package/README.ja.md +3 -2
- package/README.ko.md +8 -5
- package/README.md +10 -7
- package/README.pt.md +3 -2
- package/README.zh.md +3 -2
- package/agents/explorer.md +2 -2
- package/agents/worker.md +6 -2
- package/commands/cairn-install.md +2 -1
- package/commands/cairn-memory.md +2 -2
- package/commands/cairn-plan.md +8 -4
- package/commands/cairn-review.md +2 -2
- package/commands/cairn-uninstall.md +1 -1
- package/commands/cairn-work.md +7 -3
- package/docs/model-guidance/README.md +16 -4
- package/docs/model-guidance/claude.md +2 -0
- package/docs/model-guidance/codex.md +4 -2
- package/package.json +5 -3
- package/scripts/cairn-cleanup.mjs +67 -0
- package/scripts/cairn-goal-cli.mjs +233 -0
- package/scripts/cairn-goal-evidence.mjs +179 -0
- package/scripts/cairn-goal-operations-evidence.mjs +124 -0
- package/scripts/cairn-goal-operations-mutations.mjs +63 -0
- package/scripts/cairn-goal-operations.mjs +177 -0
- package/scripts/cairn-goal-policy.mjs +33 -0
- package/scripts/cairn-goal-state-validation.mjs +199 -0
- package/scripts/cairn-goal-state.mjs +89 -0
- package/scripts/cairn-goal.mjs +34 -961
- package/scripts/cairn-lifecycle-commands.mjs +200 -0
- package/scripts/cairn-lifecycle-config.mjs +149 -0
- package/scripts/cairn-lifecycle-integrity.mjs +218 -0
- package/scripts/cairn-lifecycle-legacy.mjs +142 -0
- package/scripts/cairn-lifecycle-lock.mjs +205 -0
- package/scripts/cairn-lifecycle-mirror.mjs +91 -0
- package/scripts/cairn-lifecycle-staging.mjs +152 -0
- package/scripts/cairn-lifecycle-transaction-journal.mjs +210 -0
- package/scripts/cairn-lifecycle-transaction-targets.mjs +119 -0
- package/scripts/cairn-lifecycle-transaction.mjs +12 -0
- package/scripts/cairn-lifecycle.mjs +103 -1185
- package/scripts/cairn-paths.mjs +82 -6
- package/scripts/cairn-safe-fs.mjs +61 -35
- package/scripts/cairn-state-context.mjs +148 -0
- package/scripts/cairn-state-hook.mjs +24 -0
- package/scripts/cairn-state-init.mjs +137 -0
- package/scripts/cairn-state.mjs +15 -269
- package/scripts/cairn-toolcheck-detect.mjs +229 -0
- package/scripts/cairn-toolcheck-runtime.mjs +178 -0
- package/scripts/cairn-toolcheck.mjs +79 -424
- package/scripts/cairn.mjs +49 -27
- package/skills/cairn-memory/SKILL.md +4 -4
- package/skills/cairn-plan/SKILL.md +29 -10
- package/skills/cairn-review/SKILL.md +5 -5
- package/skills/cairn-work/SKILL.md +35 -22
- package/templates/PLAN.md +3 -1
- package/templates/work-plan.md +21 -0
- package/scripts/cairn-state.sh +0 -167
|
@@ -1,26 +1,69 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { delimiter, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
6
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_TIMEOUT_MS,
|
|
7
|
+
commandOk,
|
|
8
|
+
execute,
|
|
9
|
+
inspectCommand,
|
|
10
|
+
localExecutableAvailable,
|
|
11
|
+
run,
|
|
12
|
+
} from "./cairn-toolcheck-runtime.mjs";
|
|
13
|
+
import { buildRequirements, collectEntries, detectStacks } from "./cairn-toolcheck-detect.mjs";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
commandCandidates,
|
|
17
|
+
commandOk,
|
|
18
|
+
DEFAULT_TIMEOUT_MS,
|
|
19
|
+
inspectCommand,
|
|
20
|
+
isWithin,
|
|
21
|
+
localExecutableAvailable,
|
|
22
|
+
run,
|
|
23
|
+
shouldUseShell,
|
|
24
|
+
systemCommandCandidates,
|
|
25
|
+
} from "./cairn-toolcheck-runtime.mjs";
|
|
26
|
+
export {
|
|
27
|
+
addJvmBuildToolRequirements,
|
|
28
|
+
addJvmRequirements,
|
|
29
|
+
buildRequirements,
|
|
30
|
+
collectEntries,
|
|
31
|
+
composerInstall,
|
|
32
|
+
detectStacks,
|
|
33
|
+
installDev,
|
|
34
|
+
jdtlsInstall,
|
|
35
|
+
packageManager,
|
|
36
|
+
packageManagerSource,
|
|
37
|
+
pushUnique,
|
|
38
|
+
pythonInstall,
|
|
39
|
+
req,
|
|
40
|
+
unsafeReq,
|
|
41
|
+
} from "./cairn-toolcheck-detect.mjs";
|
|
42
|
+
|
|
43
|
+
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
44
|
+
const usage = "Usage: cairn toolcheck [--root PATH] [--json] [--install --yes]";
|
|
9
45
|
|
|
10
46
|
if (isCliEntry()) {
|
|
47
|
+
const args = process.argv.slice(2);
|
|
11
48
|
try {
|
|
12
|
-
const options = parseCliArgs(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (options.
|
|
16
|
-
console.log(
|
|
49
|
+
const options = parseCliArgs(args);
|
|
50
|
+
if (options.action === "help") {
|
|
51
|
+
console.log(usage);
|
|
52
|
+
} else if (options.action === "version") {
|
|
53
|
+
console.log(`cairn ${version}`);
|
|
17
54
|
} else {
|
|
18
|
-
|
|
55
|
+
const report = await createReport(options);
|
|
56
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
57
|
+
else printReport(report);
|
|
58
|
+
if (report.install.refused || report.results.some((result) => !result.ok)) process.exitCode = 1;
|
|
19
59
|
}
|
|
20
|
-
|
|
21
|
-
if (report.install.refused || report.results.some((result) => !result.ok)) process.exitCode = 1;
|
|
22
60
|
} catch (error) {
|
|
23
|
-
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
if (args.includes("--json")) {
|
|
63
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: { code: "CLI_USAGE", message } }, null, 2));
|
|
64
|
+
} else {
|
|
65
|
+
console.error(`Cairn toolcheck error: ${message}`);
|
|
66
|
+
}
|
|
24
67
|
process.exitCode = 2;
|
|
25
68
|
}
|
|
26
69
|
}
|
|
@@ -30,25 +73,35 @@ export function parseCliArgs(args, cwd = process.cwd()) {
|
|
|
30
73
|
let install = false;
|
|
31
74
|
let yes = false;
|
|
32
75
|
let json = false;
|
|
76
|
+
let action = "check";
|
|
77
|
+
const provided = new Set();
|
|
33
78
|
|
|
34
79
|
for (let index = 0; index < args.length; index += 1) {
|
|
35
80
|
const argument = args[index];
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
81
|
+
if (["--help", "--version", "--install", "--yes", "--json"].includes(argument)) {
|
|
82
|
+
if (provided.has(argument)) throw new Error(`${argument} may only be provided once`);
|
|
83
|
+
provided.add(argument);
|
|
84
|
+
if (argument === "--help") action = "help";
|
|
85
|
+
else if (argument === "--version") action = "version";
|
|
86
|
+
else if (argument === "--install") install = true;
|
|
87
|
+
else if (argument === "--yes") yes = true;
|
|
88
|
+
else json = true;
|
|
89
|
+
} else if (argument === "--root" || argument.startsWith("--root=")) {
|
|
90
|
+
if (provided.has("--root")) throw new Error("--root may only be provided once");
|
|
91
|
+
provided.add("--root");
|
|
92
|
+
const value = argument === "--root" ? args[index + 1] : argument.slice("--root=".length);
|
|
41
93
|
if (!value || value.startsWith("--")) throw new Error("--root requires a path");
|
|
42
94
|
root = value;
|
|
43
|
-
index += 1;
|
|
44
|
-
} else
|
|
45
|
-
|
|
46
|
-
if (!value) throw new Error("--root requires a path");
|
|
47
|
-
root = value;
|
|
95
|
+
if (argument === "--root") index += 1;
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(`unknown option: ${argument}`);
|
|
48
98
|
}
|
|
49
99
|
}
|
|
50
100
|
|
|
51
|
-
|
|
101
|
+
if (provided.has("--help") && provided.has("--version")) {
|
|
102
|
+
throw new Error("--help and --version cannot be used together");
|
|
103
|
+
}
|
|
104
|
+
return { root: resolve(cwd, root), install, yes, json, action };
|
|
52
105
|
}
|
|
53
106
|
|
|
54
107
|
export async function createReport({
|
|
@@ -140,333 +193,6 @@ export async function createReport({
|
|
|
140
193
|
};
|
|
141
194
|
}
|
|
142
195
|
|
|
143
|
-
export async function collectEntries(base) {
|
|
144
|
-
const output = [];
|
|
145
|
-
async function walk(dir) {
|
|
146
|
-
for (const entry of await readdir(dir)) {
|
|
147
|
-
if ([".git", ".cairn", "node_modules", "vendor", "target", ".venv", "__pycache__"].includes(entry)) continue;
|
|
148
|
-
const path = join(dir, entry);
|
|
149
|
-
const relativePath = normalizePath(relative(base, path));
|
|
150
|
-
if (relativePath === "test/fixtures" || relativePath.startsWith("test/fixtures/")) continue;
|
|
151
|
-
const info = await lstat(path);
|
|
152
|
-
if (info.isSymbolicLink()) continue;
|
|
153
|
-
if (info.isDirectory()) await walk(path);
|
|
154
|
-
else output.push(relativePath);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
await walk(base);
|
|
158
|
-
return output;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function detectStacks(paths) {
|
|
162
|
-
const stacks = new Set();
|
|
163
|
-
if (hasBasename(paths, "package.json")) stacks.add("javascript");
|
|
164
|
-
if (hasBasename(paths, "tsconfig.json") || paths.some((path) => path.endsWith(".ts") || path.endsWith(".tsx"))) stacks.add("typescript");
|
|
165
|
-
if (hasBasename(paths, "pyproject.toml") || paths.some((path) => path.endsWith(".py"))) stacks.add("python");
|
|
166
|
-
if (hasBasename(paths, "composer.json") || paths.some((path) => path.endsWith(".php"))) stacks.add("php");
|
|
167
|
-
if (hasBasename(paths, "pom.xml", "build.gradle", "build.gradle.kts") || paths.some((path) => path.endsWith(".java"))) stacks.add("java");
|
|
168
|
-
if (paths.some((path) => path.endsWith(".kt") || path.endsWith(".kts"))) stacks.add("kotlin");
|
|
169
|
-
if (hasBasename(paths, "Package.swift") || paths.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace") || path.endsWith(".swift"))) stacks.add("swift");
|
|
170
|
-
if (hasBasename(paths, "go.mod") || paths.some((path) => path.endsWith(".go"))) stacks.add("go");
|
|
171
|
-
if (hasBasename(paths, "Cargo.toml") || paths.some((path) => path.endsWith(".rs"))) stacks.add("rust");
|
|
172
|
-
return [...stacks];
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function buildRequirements(stacks, entries = [], _commandAvailable = commandOk, platform = process.platform) {
|
|
176
|
-
const requirements = [];
|
|
177
|
-
if (stacks.includes("javascript")) {
|
|
178
|
-
requirements.push(unsafeReq("node", ["--version"], "JavaScript runtime for package scripts and CLI checks", "automatic runtime installation is unavailable"));
|
|
179
|
-
const manager = packageManager(entries);
|
|
180
|
-
requirements.push(unsafeReq(manager, ["--version"], `JavaScript package manager selected from ${packageManagerSource(entries)}`, "automatic package-manager installation is unavailable"));
|
|
181
|
-
}
|
|
182
|
-
if (stacks.includes("typescript")) {
|
|
183
|
-
requirements.push(unsafeReq("typescript-language-server", ["--version"], "TypeScript LSP server", "automatic package installation is disabled without pinned package versions"));
|
|
184
|
-
requirements.push(unsafeReq("tsc", ["--version"], "TypeScript compiler verification", "automatic package installation is disabled without pinned package versions"));
|
|
185
|
-
}
|
|
186
|
-
if (stacks.includes("python")) {
|
|
187
|
-
requirements.push(unsafeReq("basedpyright", ["--version"], "Python LSP/type checker", "automatic Python package installation is disabled without pinned package versions"));
|
|
188
|
-
requirements.push(unsafeReq("ruff", ["--version"], "Python lint and format checker", "automatic Python package installation is disabled without pinned package versions"));
|
|
189
|
-
}
|
|
190
|
-
if (stacks.includes("php")) {
|
|
191
|
-
const composerProject = hasBasename(entries, "composer.json");
|
|
192
|
-
requirements.push(req("php", ["--version"], "PHP runtime for Composer scripts and CLI checks"));
|
|
193
|
-
if (composerProject) requirements.push(req("composer", ["--version"], "PHP package manager for project-local tool installation"));
|
|
194
|
-
requirements.push(unsafeReq("phpactor", ["--version"], "PHP LSP server", "automatic Composer package installation is disabled without pinned package versions"));
|
|
195
|
-
requirements.push(unsafeReq("phpstan", ["--version"], "PHP static analysis verification", "automatic Composer package installation is disabled without pinned package versions"));
|
|
196
|
-
requirements.push(unsafeReq("php-cs-fixer", ["--version"], "PHP formatting verification", "automatic Composer package installation is disabled without pinned package versions"));
|
|
197
|
-
}
|
|
198
|
-
if (stacks.includes("java")) {
|
|
199
|
-
addJvmRequirements(requirements);
|
|
200
|
-
requirements.push(unsafeReq("jdtls", ["--version"], "Java LSP server", "checksum-free JDTLS latest downloads are disabled"));
|
|
201
|
-
addJvmBuildToolRequirements(requirements, entries);
|
|
202
|
-
}
|
|
203
|
-
if (stacks.includes("kotlin")) {
|
|
204
|
-
addJvmRequirements(requirements);
|
|
205
|
-
requirements.push(req("kotlin-lsp.sh", ["--help"], "Kotlin LSP server"));
|
|
206
|
-
requirements.push(req("kotlinc", ["-version"], "Kotlin compiler verification"));
|
|
207
|
-
addJvmBuildToolRequirements(requirements, entries);
|
|
208
|
-
}
|
|
209
|
-
if (stacks.includes("swift")) {
|
|
210
|
-
requirements.push(req("swift", ["--version"], "Swift toolchain for build and test commands"));
|
|
211
|
-
requirements.push(req("sourcekit-lsp", ["--version"], "Swift LSP server"));
|
|
212
|
-
if (entries.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace"))) {
|
|
213
|
-
requirements.push(req("xcodebuild", ["-version"], "Xcode build verification"));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (stacks.includes("go")) {
|
|
217
|
-
requirements.push(unsafeReq("gopls", ["version"], "Go LSP server", "unpinned go install targets are disabled"));
|
|
218
|
-
requirements.push(unsafeReq("golangci-lint", ["--version"], "Go lint verification", "unpinned go install targets are disabled"));
|
|
219
|
-
}
|
|
220
|
-
if (stacks.includes("rust")) {
|
|
221
|
-
requirements.push(unsafeReq("rust-analyzer", ["--version"], "Rust LSP server", "automatic rustup changes are disabled without a pinned toolchain manifest"));
|
|
222
|
-
requirements.push(unsafeReq("cargo", ["clippy", "--version"], "Rust clippy verification", "automatic rustup changes are disabled without a pinned toolchain manifest"));
|
|
223
|
-
}
|
|
224
|
-
return requirements;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function addJvmRequirements(requirements) {
|
|
228
|
-
if (!requirements.some((requirement) => requirement.name === "java")) {
|
|
229
|
-
requirements.push(req("java", ["--version"], "JVM runtime for build and test commands"));
|
|
230
|
-
}
|
|
231
|
-
if (!requirements.some((requirement) => requirement.name === "javac")) {
|
|
232
|
-
requirements.push(req("javac", ["--version"], "JVM compiler verification"));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function addJvmBuildToolRequirements(requirements, entries = []) {
|
|
237
|
-
const gradleWindows = findBasename(entries, "gradlew.bat");
|
|
238
|
-
const gradleUnix = findBasename(entries, "gradlew");
|
|
239
|
-
if (gradleWindows) pushUnique(requirements, req(repositoryCommand(gradleWindows), ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
|
|
240
|
-
else if (gradleUnix) pushUnique(requirements, req(repositoryCommand(gradleUnix), ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
|
|
241
|
-
else if (hasBasename(entries, "build.gradle", "build.gradle.kts")) pushUnique(requirements, req("gradle", ["--version"], "Gradle availability"));
|
|
242
|
-
const mavenWindows = findBasename(entries, "mvnw.cmd", "mvnw.bat");
|
|
243
|
-
const mavenUnix = findBasename(entries, "mvnw");
|
|
244
|
-
if (mavenWindows) pushUnique(requirements, req(repositoryCommand(mavenWindows), ["--version"], "Maven wrapper availability", null, { localOnly: true }));
|
|
245
|
-
else if (mavenUnix) pushUnique(requirements, req(repositoryCommand(mavenUnix), ["--version"], "Maven wrapper availability", null, { localOnly: true }));
|
|
246
|
-
else if (hasBasename(entries, "pom.xml")) pushUnique(requirements, req("mvn", ["--version"], "Maven availability"));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function pushUnique(requirements, requirement) {
|
|
250
|
-
if (!requirements.some((candidate) => candidate.name === requirement.name)) requirements.push(requirement);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Kept for API compatibility. Callers may show these commands, but buildRequirements
|
|
254
|
-
// no longer attaches an unpinned command to an automatically executable requirement.
|
|
255
|
-
export function composerInstall(packages) {
|
|
256
|
-
return { command: "composer", args: ["require", "--dev", ...packages] };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function pythonInstall(uv, project, tool, platform = process.platform) {
|
|
260
|
-
if (uv && project) return { command: "uv", args: ["add", "--dev", tool] };
|
|
261
|
-
if (uv) return { command: "uv", args: ["tool", "install", tool] };
|
|
262
|
-
if (platform === "win32") return { command: "py", args: ["-3", "-m", "pip", "install", "--user", tool] };
|
|
263
|
-
return { command: "python3", args: ["-m", "pip", "install", "--user", tool] };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function jdtlsInstall(_platform = process.platform) {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function req(command, args, reason, installCommand = null, options = {}) {
|
|
271
|
-
return {
|
|
272
|
-
name: command,
|
|
273
|
-
command,
|
|
274
|
-
args,
|
|
275
|
-
reason,
|
|
276
|
-
install: installCommand,
|
|
277
|
-
installUnavailableReason: options.installUnavailableReason ?? null,
|
|
278
|
-
localOnly: options.localOnly ?? false,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function unsafeReq(command, args, reason, installUnavailableReason) {
|
|
283
|
-
return req(command, args, reason, null, { installUnavailableReason });
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function packageManager(entries = []) {
|
|
287
|
-
return selectPackageManager(entries).manager;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function packageManagerSource(entries = []) {
|
|
291
|
-
const selected = selectPackageManager(entries);
|
|
292
|
-
return selected.path ?? "the npm default (no lockfile found)";
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function installDev(pm, packages) {
|
|
296
|
-
if (pm === "bun") return { command: "bun", args: ["add", "-d", ...packages] };
|
|
297
|
-
if (pm === "pnpm") return { command: "pnpm", args: ["add", "-D", ...packages] };
|
|
298
|
-
if (pm === "yarn") return { command: "yarn", args: ["add", "-D", ...packages] };
|
|
299
|
-
return { command: "npm", args: ["install", "--save-dev", ...packages] };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function inspectCommand(requirement, {
|
|
303
|
-
root = process.cwd(),
|
|
304
|
-
runner = run,
|
|
305
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
306
|
-
localChecker = localExecutableAvailable,
|
|
307
|
-
platform = process.platform,
|
|
308
|
-
} = {}) {
|
|
309
|
-
const candidates = commandCandidates(requirement.command, root, platform);
|
|
310
|
-
const localOnly = requirement.localOnly || isRepositoryCommand(requirement.command, root);
|
|
311
|
-
|
|
312
|
-
if (!localOnly) {
|
|
313
|
-
let diagnostic = null;
|
|
314
|
-
for (const candidate of systemCommandCandidates(requirement.command, platform)) {
|
|
315
|
-
diagnostic = execute(candidate, requirement.args, { root, runner, timeoutMs });
|
|
316
|
-
if (diagnostic.status === 0) {
|
|
317
|
-
return {
|
|
318
|
-
ok: true,
|
|
319
|
-
availability: "verified",
|
|
320
|
-
source: "system",
|
|
321
|
-
candidate,
|
|
322
|
-
diagnostic,
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
for (const candidate of candidates.slice(1)) {
|
|
328
|
-
if (localChecker(candidate)) {
|
|
329
|
-
return {
|
|
330
|
-
ok: true,
|
|
331
|
-
availability: "discovered",
|
|
332
|
-
source: "repository",
|
|
333
|
-
candidate,
|
|
334
|
-
diagnostic,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
ok: false,
|
|
341
|
-
availability: diagnostic?.timedOut ? "timeout" : "missing",
|
|
342
|
-
source: null,
|
|
343
|
-
candidate: null,
|
|
344
|
-
diagnostic,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
for (const candidate of repositoryCandidates(requirement.command, root)) {
|
|
349
|
-
if (localChecker(candidate)) {
|
|
350
|
-
return {
|
|
351
|
-
ok: true,
|
|
352
|
-
availability: "discovered",
|
|
353
|
-
source: "repository",
|
|
354
|
-
candidate,
|
|
355
|
-
diagnostic: null,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return {
|
|
361
|
-
ok: false,
|
|
362
|
-
availability: "missing",
|
|
363
|
-
source: null,
|
|
364
|
-
candidate: null,
|
|
365
|
-
diagnostic: null,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export function commandOk(command, args, root = process.cwd(), runner = run) {
|
|
370
|
-
return inspectCommand(req(command, args, "tool availability"), { root, runner }).ok;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export function commandCandidates(command, root = process.cwd(), platform = process.platform) {
|
|
374
|
-
const candidates = isRepositoryCommand(command, root)
|
|
375
|
-
? repositoryCandidates(command, root)
|
|
376
|
-
: [
|
|
377
|
-
command,
|
|
378
|
-
join(root, "node_modules", ".bin", command),
|
|
379
|
-
join(root, "vendor", "bin", command),
|
|
380
|
-
join(root, ".cairn", "tools", "bin", command),
|
|
381
|
-
];
|
|
382
|
-
if (platform !== "win32") return [...new Set(candidates)];
|
|
383
|
-
const extensions = [".cmd", ".bat", ".exe"];
|
|
384
|
-
return [...new Set(candidates.flatMap((candidate) => hasWindowsExecutableExtension(candidate) ? [candidate] : [candidate, ...extensions.map((extension) => `${candidate}${extension}`)]))];
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
export function systemCommandCandidates(command, platform = process.platform) {
|
|
388
|
-
if (platform !== "win32" || hasWindowsExecutableExtension(command) || isAbsolute(command)) return [command];
|
|
389
|
-
return [command, `${command}.cmd`, `${command}.bat`, `${command}.exe`];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export function localExecutableAvailable(command, platform = process.platform) {
|
|
393
|
-
try {
|
|
394
|
-
const info = statSync(command);
|
|
395
|
-
if (!info.isFile()) return false;
|
|
396
|
-
accessSync(command, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
397
|
-
return true;
|
|
398
|
-
} catch {
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
export function run(command, args, { cwd = process.cwd(), timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
404
|
-
const startedAt = Date.now();
|
|
405
|
-
const result = spawnSync(command, args, {
|
|
406
|
-
cwd,
|
|
407
|
-
stdio: "ignore",
|
|
408
|
-
encoding: "utf8",
|
|
409
|
-
shell: shouldUseShell(command),
|
|
410
|
-
timeout: timeoutMs,
|
|
411
|
-
env: safeEnvironment(cwd),
|
|
412
|
-
});
|
|
413
|
-
return { ...result, durationMs: Date.now() - startedAt };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
export function shouldUseShell(command, platform = process.platform) {
|
|
417
|
-
return platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function execute(command, args, { root, runner, timeoutMs }) {
|
|
421
|
-
const startedAt = Date.now();
|
|
422
|
-
try {
|
|
423
|
-
const result = runner(command, args, { cwd: root, timeoutMs });
|
|
424
|
-
return {
|
|
425
|
-
status: Number.isInteger(result?.status) ? result.status : null,
|
|
426
|
-
signal: result?.signal ?? null,
|
|
427
|
-
errorCode: result?.error?.code ?? null,
|
|
428
|
-
timedOut: result?.error?.code === "ETIMEDOUT",
|
|
429
|
-
durationMs: Number.isFinite(result?.durationMs) ? result.durationMs : Date.now() - startedAt,
|
|
430
|
-
};
|
|
431
|
-
} catch (error) {
|
|
432
|
-
return {
|
|
433
|
-
status: null,
|
|
434
|
-
signal: null,
|
|
435
|
-
errorCode: error && typeof error === "object" && "code" in error ? String(error.code) : "RUNNER_ERROR",
|
|
436
|
-
timedOut: false,
|
|
437
|
-
durationMs: Date.now() - startedAt,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function repositoryCandidates(command, root) {
|
|
443
|
-
const normalizedCommand = command.replace(/^\.\//, "");
|
|
444
|
-
const direct = isAbsolute(command) ? command : resolve(root, normalizedCommand);
|
|
445
|
-
return [...new Set([direct])];
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function isRepositoryCommand(command, root) {
|
|
449
|
-
if (/^(?:\.\.?[\\/])/.test(command)) return true;
|
|
450
|
-
if (/^(?:gradlew|gradlew\.bat|mvnw|mvnw\.cmd|mvnw\.bat)$/i.test(command)) return true;
|
|
451
|
-
return isAbsolute(command) && isWithin(root, command);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export function isWithin(root, candidate, platform = process.platform) {
|
|
455
|
-
const pathApi = platform === "win32" ? win32 : posix;
|
|
456
|
-
const path = pathApi.relative(pathApi.resolve(root), pathApi.resolve(candidate));
|
|
457
|
-
return path === "" || (!pathApi.isAbsolute(path) && !path.startsWith(`..${pathApi.sep}`) && path !== "..");
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function safeEnvironment(root) {
|
|
461
|
-
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
462
|
-
const safePath = (process.env[pathKey] ?? "")
|
|
463
|
-
.split(delimiter)
|
|
464
|
-
.filter(Boolean)
|
|
465
|
-
.filter((entry) => isAbsolute(entry) && !isWithin(root, entry))
|
|
466
|
-
.join(delimiter);
|
|
467
|
-
return { ...process.env, [pathKey]: safePath };
|
|
468
|
-
}
|
|
469
|
-
|
|
470
196
|
function printReport(report) {
|
|
471
197
|
console.log(`Cairn toolcheck: ${report.root}`);
|
|
472
198
|
console.log(`Detected stacks: ${report.detected.length > 0 ? report.detected.join(", ") : "none"}`);
|
|
@@ -493,74 +219,3 @@ function isCliEntry() {
|
|
|
493
219
|
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
494
220
|
}
|
|
495
221
|
}
|
|
496
|
-
|
|
497
|
-
function normalizePath(path) {
|
|
498
|
-
return path.split(sep).join("/");
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function pathBasename(path) {
|
|
502
|
-
const normalized = normalizePath(path);
|
|
503
|
-
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function hasBasename(paths, ...names) {
|
|
507
|
-
const expected = new Set(names);
|
|
508
|
-
return paths.some((path) => expected.has(pathBasename(path)));
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function findBasename(paths, ...names) {
|
|
512
|
-
const expected = new Set(names);
|
|
513
|
-
return [...paths]
|
|
514
|
-
.filter((path) => expected.has(pathBasename(path)))
|
|
515
|
-
.sort(compareRepositoryPaths)[0] ?? null;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function hasPathSegmentWithSuffix(path, ...suffixes) {
|
|
519
|
-
return normalizePath(path)
|
|
520
|
-
.split("/")
|
|
521
|
-
.some((segment) => suffixes.some((suffix) => segment.endsWith(suffix)));
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function repositoryCommand(path) {
|
|
525
|
-
const normalized = normalizePath(path);
|
|
526
|
-
return normalized.startsWith("./") ? normalized : `./${normalized}`;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function selectPackageManager(entries) {
|
|
530
|
-
const lockfiles = [
|
|
531
|
-
{ manager: "bun", names: new Set(["bun.lock", "bun.lockb"]), priority: 0 },
|
|
532
|
-
{ manager: "pnpm", names: new Set(["pnpm-lock.yaml"]), priority: 1 },
|
|
533
|
-
{ manager: "yarn", names: new Set(["yarn.lock"]), priority: 2 },
|
|
534
|
-
{ manager: "npm", names: new Set(["package-lock.json", "npm-shrinkwrap.json"]), priority: 3 },
|
|
535
|
-
];
|
|
536
|
-
const candidates = [];
|
|
537
|
-
|
|
538
|
-
for (const path of entries) {
|
|
539
|
-
const normalized = normalizePath(path);
|
|
540
|
-
const name = pathBasename(normalized);
|
|
541
|
-
const match = lockfiles.find(({ names }) => names.has(name));
|
|
542
|
-
if (!match) continue;
|
|
543
|
-
candidates.push({
|
|
544
|
-
manager: match.manager,
|
|
545
|
-
path: normalized,
|
|
546
|
-
depth: normalized.split("/").length - 1,
|
|
547
|
-
priority: match.priority,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
candidates.sort((left, right) => left.depth - right.depth
|
|
552
|
-
|| left.priority - right.priority
|
|
553
|
-
|| left.path.localeCompare(right.path));
|
|
554
|
-
return candidates[0] ?? { manager: "npm", path: null };
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function compareRepositoryPaths(left, right) {
|
|
558
|
-
const leftPath = normalizePath(left);
|
|
559
|
-
const rightPath = normalizePath(right);
|
|
560
|
-
const depth = leftPath.split("/").length - rightPath.split("/").length;
|
|
561
|
-
return depth || leftPath.localeCompare(rightPath);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function hasWindowsExecutableExtension(command) {
|
|
565
|
-
return /\.(cmd|bat|exe)$/i.test(command);
|
|
566
|
-
}
|