@comento/pota-harness 0.0.0-stage → 1.0.0

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 (99) hide show
  1. package/.agents/workflows/pota-doctor.md +16 -0
  2. package/.agents/workflows/pota-install.md +15 -0
  3. package/.agents/workflows/pota-memory.md +17 -0
  4. package/.agents/workflows/pota-plan.md +22 -0
  5. package/.agents/workflows/pota-review.md +16 -0
  6. package/.agents/workflows/pota-toolcheck.md +9 -0
  7. package/.agents/workflows/pota-uninstall.md +9 -0
  8. package/.agents/workflows/pota-upgrade.md +15 -0
  9. package/.agents/workflows/pota-work.md +24 -0
  10. package/.claude/agents/explorer.md +7 -0
  11. package/.claude/agents/worker.md +7 -0
  12. package/.claude/commands/pota-doctor.md +5 -0
  13. package/.claude/commands/pota-install.md +9 -0
  14. package/.claude/commands/pota-memory.md +5 -0
  15. package/.claude/commands/pota-plan.md +5 -0
  16. package/.claude/commands/pota-review.md +5 -0
  17. package/.claude/commands/pota-toolcheck.md +7 -0
  18. package/.claude/commands/pota-uninstall.md +5 -0
  19. package/.claude/commands/pota-upgrade.md +9 -0
  20. package/.claude/commands/pota-work.md +7 -0
  21. package/.codex-plugin/plugin.json +33 -0
  22. package/LICENSE +21 -0
  23. package/README.de.md +166 -0
  24. package/README.es.md +166 -0
  25. package/README.fr.md +166 -0
  26. package/README.ja.md +166 -0
  27. package/README.ko.md +184 -0
  28. package/README.md +191 -2
  29. package/README.pt.md +166 -0
  30. package/README.zh.md +166 -0
  31. package/agents/explorer.md +36 -0
  32. package/agents/worker.md +45 -0
  33. package/commands/pota-doctor.md +28 -0
  34. package/commands/pota-install.md +29 -0
  35. package/commands/pota-memory.md +17 -0
  36. package/commands/pota-plan.md +22 -0
  37. package/commands/pota-review.md +16 -0
  38. package/commands/pota-toolcheck.md +17 -0
  39. package/commands/pota-uninstall.md +27 -0
  40. package/commands/pota-upgrade.md +26 -0
  41. package/commands/pota-work.md +24 -0
  42. package/docs/model-guidance/README.md +67 -0
  43. package/docs/model-guidance/claude.md +49 -0
  44. package/docs/model-guidance/codex.md +68 -0
  45. package/hooks/hooks.json +65 -0
  46. package/package.json +47 -4
  47. package/scripts/pota +14 -0
  48. package/scripts/pota-auto-upgrade.mjs +342 -0
  49. package/scripts/pota-cleanup.mjs +194 -0
  50. package/scripts/pota-goal-cli.mjs +337 -0
  51. package/scripts/pota-goal-evidence.mjs +186 -0
  52. package/scripts/pota-goal-history.mjs +316 -0
  53. package/scripts/pota-goal-operations-evidence.mjs +213 -0
  54. package/scripts/pota-goal-operations-mutations.mjs +97 -0
  55. package/scripts/pota-goal-operations.mjs +281 -0
  56. package/scripts/pota-goal-policy.mjs +62 -0
  57. package/scripts/pota-goal-state-validation.mjs +361 -0
  58. package/scripts/pota-goal-state.mjs +136 -0
  59. package/scripts/pota-goal.mjs +69 -0
  60. package/scripts/pota-index.mjs +1052 -0
  61. package/scripts/pota-lifecycle-claude.mjs +173 -0
  62. package/scripts/pota-lifecycle-commands.mjs +418 -0
  63. package/scripts/pota-lifecycle-config.mjs +153 -0
  64. package/scripts/pota-lifecycle-hosts.mjs +98 -0
  65. package/scripts/pota-lifecycle-integrity.mjs +272 -0
  66. package/scripts/pota-lifecycle-layout.mjs +52 -0
  67. package/scripts/pota-lifecycle-lock.mjs +205 -0
  68. package/scripts/pota-lifecycle-mirror.mjs +83 -0
  69. package/scripts/pota-lifecycle-staging.mjs +156 -0
  70. package/scripts/pota-lifecycle-transaction-journal.mjs +244 -0
  71. package/scripts/pota-lifecycle-transaction-targets.mjs +119 -0
  72. package/scripts/pota-lifecycle-transaction.mjs +12 -0
  73. package/scripts/pota-lifecycle.mjs +244 -0
  74. package/scripts/pota-paths.mjs +190 -0
  75. package/scripts/pota-safe-fs.mjs +277 -0
  76. package/scripts/pota-state-context.mjs +281 -0
  77. package/scripts/pota-state-hook.mjs +26 -0
  78. package/scripts/pota-state-init.mjs +80 -0
  79. package/scripts/pota-state.mjs +245 -0
  80. package/scripts/pota-toolcheck-detect.mjs +229 -0
  81. package/scripts/pota-toolcheck-runtime.mjs +178 -0
  82. package/scripts/pota-toolcheck.mjs +221 -0
  83. package/scripts/pota-update-state.mjs +219 -0
  84. package/scripts/pota.mjs +200 -0
  85. package/skills/pota-memory/SKILL.md +52 -0
  86. package/skills/pota-plan/SKILL.md +66 -0
  87. package/skills/pota-plan/delegation.md +65 -0
  88. package/skills/pota-plan/heavy-path.md +28 -0
  89. package/skills/pota-plan/planning-rules.md +78 -0
  90. package/skills/pota-review/SKILL.md +56 -0
  91. package/skills/pota-work/SKILL.md +71 -0
  92. package/skills/pota-work/delegation.md +65 -0
  93. package/skills/pota-work/heavy-path.md +18 -0
  94. package/skills/pota-work/recovery.md +29 -0
  95. package/skills/pota-work/verification.md +79 -0
  96. package/templates/MEMORY.md +12 -0
  97. package/templates/PLAN.md +9 -0
  98. package/templates/domain-memory.md +22 -0
  99. package/templates/work-plan.md +177 -0
@@ -0,0 +1,229 @@
1
+ import { lstat, readdir } from "node:fs/promises";
2
+ import { join, relative, sep } from "node:path";
3
+ import { commandOk } from "./pota-toolcheck-runtime.mjs";
4
+
5
+ export async function collectEntries(base) {
6
+ const output = [];
7
+ async function walk(dir) {
8
+ for (const entry of await readdir(dir)) {
9
+ if ([".git", ".pota", "node_modules", "vendor", "target", ".venv", "__pycache__"].includes(entry)) continue;
10
+ const path = join(dir, entry);
11
+ const relativePath = normalizePath(relative(base, path));
12
+ if (relativePath === "test/fixtures" || relativePath.startsWith("test/fixtures/")) continue;
13
+ const info = await lstat(path);
14
+ if (info.isSymbolicLink()) continue;
15
+ if (info.isDirectory()) await walk(path);
16
+ else output.push(relativePath);
17
+ }
18
+ }
19
+ await walk(base);
20
+ return output;
21
+ }
22
+
23
+ export function detectStacks(paths) {
24
+ const stacks = new Set();
25
+ if (hasBasename(paths, "package.json")) stacks.add("javascript");
26
+ if (hasBasename(paths, "tsconfig.json") || paths.some((path) => path.endsWith(".ts") || path.endsWith(".tsx"))) stacks.add("typescript");
27
+ if (hasBasename(paths, "pyproject.toml") || paths.some((path) => path.endsWith(".py"))) stacks.add("python");
28
+ if (hasBasename(paths, "composer.json") || paths.some((path) => path.endsWith(".php"))) stacks.add("php");
29
+ if (hasBasename(paths, "pom.xml", "build.gradle", "build.gradle.kts") || paths.some((path) => path.endsWith(".java"))) stacks.add("java");
30
+ if (paths.some((path) => path.endsWith(".kt") || path.endsWith(".kts"))) stacks.add("kotlin");
31
+ if (hasBasename(paths, "Package.swift") || paths.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace") || path.endsWith(".swift"))) stacks.add("swift");
32
+ if (hasBasename(paths, "go.mod") || paths.some((path) => path.endsWith(".go"))) stacks.add("go");
33
+ if (hasBasename(paths, "Cargo.toml") || paths.some((path) => path.endsWith(".rs"))) stacks.add("rust");
34
+ return [...stacks];
35
+ }
36
+
37
+ export function buildRequirements(stacks, entries = [], _commandAvailable = commandOk, platform = process.platform) {
38
+ const requirements = [];
39
+ if (stacks.includes("javascript")) {
40
+ requirements.push(unsafeReq("node", ["--version"], "JavaScript runtime for package scripts and CLI checks", "automatic runtime installation is unavailable"));
41
+ const manager = packageManager(entries);
42
+ requirements.push(unsafeReq(manager, ["--version"], `JavaScript package manager selected from ${packageManagerSource(entries)}`, "automatic package-manager installation is unavailable"));
43
+ }
44
+ if (stacks.includes("typescript")) {
45
+ requirements.push(unsafeReq("typescript-language-server", ["--version"], "TypeScript LSP server", "automatic package installation is disabled without pinned package versions"));
46
+ requirements.push(unsafeReq("tsc", ["--version"], "TypeScript compiler verification", "automatic package installation is disabled without pinned package versions"));
47
+ }
48
+ if (stacks.includes("python")) {
49
+ requirements.push(unsafeReq("basedpyright", ["--version"], "Python LSP/type checker", "automatic Python package installation is disabled without pinned package versions"));
50
+ requirements.push(unsafeReq("ruff", ["--version"], "Python lint and format checker", "automatic Python package installation is disabled without pinned package versions"));
51
+ }
52
+ if (stacks.includes("php")) {
53
+ const composerProject = hasBasename(entries, "composer.json");
54
+ requirements.push(req("php", ["--version"], "PHP runtime for Composer scripts and CLI checks"));
55
+ if (composerProject) requirements.push(req("composer", ["--version"], "PHP package manager for project-local tool installation"));
56
+ requirements.push(unsafeReq("phpactor", ["--version"], "PHP LSP server", "automatic Composer package installation is disabled without pinned package versions"));
57
+ requirements.push(unsafeReq("phpstan", ["--version"], "PHP static analysis verification", "automatic Composer package installation is disabled without pinned package versions"));
58
+ requirements.push(unsafeReq("php-cs-fixer", ["--version"], "PHP formatting verification", "automatic Composer package installation is disabled without pinned package versions"));
59
+ }
60
+ if (stacks.includes("java")) {
61
+ addJvmRequirements(requirements);
62
+ requirements.push(unsafeReq("jdtls", ["--version"], "Java LSP server", "checksum-free JDTLS latest downloads are disabled"));
63
+ addJvmBuildToolRequirements(requirements, entries);
64
+ }
65
+ if (stacks.includes("kotlin")) {
66
+ addJvmRequirements(requirements);
67
+ requirements.push(req("kotlin-lsp.sh", ["--help"], "Kotlin LSP server"));
68
+ requirements.push(req("kotlinc", ["-version"], "Kotlin compiler verification"));
69
+ addJvmBuildToolRequirements(requirements, entries);
70
+ }
71
+ if (stacks.includes("swift")) {
72
+ requirements.push(req("swift", ["--version"], "Swift toolchain for build and test commands"));
73
+ requirements.push(req("sourcekit-lsp", ["--version"], "Swift LSP server"));
74
+ if (entries.some((path) => hasPathSegmentWithSuffix(path, ".xcodeproj", ".xcworkspace"))) {
75
+ requirements.push(req("xcodebuild", ["-version"], "Xcode build verification"));
76
+ }
77
+ }
78
+ if (stacks.includes("go")) {
79
+ requirements.push(unsafeReq("gopls", ["version"], "Go LSP server", "unpinned go install targets are disabled"));
80
+ requirements.push(unsafeReq("golangci-lint", ["--version"], "Go lint verification", "unpinned go install targets are disabled"));
81
+ }
82
+ if (stacks.includes("rust")) {
83
+ requirements.push(unsafeReq("rust-analyzer", ["--version"], "Rust LSP server", "automatic rustup changes are disabled without a pinned toolchain manifest"));
84
+ requirements.push(unsafeReq("cargo", ["clippy", "--version"], "Rust clippy verification", "automatic rustup changes are disabled without a pinned toolchain manifest"));
85
+ }
86
+ return requirements;
87
+ }
88
+
89
+ export function addJvmRequirements(requirements) {
90
+ if (!requirements.some((requirement) => requirement.name === "java")) {
91
+ requirements.push(req("java", ["--version"], "JVM runtime for build and test commands"));
92
+ }
93
+ if (!requirements.some((requirement) => requirement.name === "javac")) {
94
+ requirements.push(req("javac", ["--version"], "JVM compiler verification"));
95
+ }
96
+ }
97
+
98
+ export function addJvmBuildToolRequirements(requirements, entries = []) {
99
+ const gradleWindows = findBasename(entries, "gradlew.bat");
100
+ const gradleUnix = findBasename(entries, "gradlew");
101
+ if (gradleWindows) pushUnique(requirements, req(repositoryCommand(gradleWindows), ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
102
+ else if (gradleUnix) pushUnique(requirements, req(repositoryCommand(gradleUnix), ["--version"], "Gradle wrapper availability", null, { localOnly: true }));
103
+ else if (hasBasename(entries, "build.gradle", "build.gradle.kts")) pushUnique(requirements, req("gradle", ["--version"], "Gradle availability"));
104
+ const mavenWindows = findBasename(entries, "mvnw.cmd", "mvnw.bat");
105
+ const mavenUnix = findBasename(entries, "mvnw");
106
+ if (mavenWindows) pushUnique(requirements, req(repositoryCommand(mavenWindows), ["--version"], "Maven wrapper availability", null, { localOnly: true }));
107
+ else if (mavenUnix) pushUnique(requirements, req(repositoryCommand(mavenUnix), ["--version"], "Maven wrapper availability", null, { localOnly: true }));
108
+ else if (hasBasename(entries, "pom.xml")) pushUnique(requirements, req("mvn", ["--version"], "Maven availability"));
109
+ }
110
+
111
+ export function pushUnique(requirements, requirement) {
112
+ if (!requirements.some((candidate) => candidate.name === requirement.name)) requirements.push(requirement);
113
+ }
114
+
115
+ // Kept for API compatibility. Callers may show these commands, but buildRequirements
116
+ // no longer attaches an unpinned command to an automatically executable requirement.
117
+ export function composerInstall(packages) {
118
+ return { command: "composer", args: ["require", "--dev", ...packages] };
119
+ }
120
+
121
+ export function pythonInstall(uv, project, tool, platform = process.platform) {
122
+ if (uv && project) return { command: "uv", args: ["add", "--dev", tool] };
123
+ if (uv) return { command: "uv", args: ["tool", "install", tool] };
124
+ if (platform === "win32") return { command: "py", args: ["-3", "-m", "pip", "install", "--user", tool] };
125
+ return { command: "python3", args: ["-m", "pip", "install", "--user", tool] };
126
+ }
127
+
128
+ export function jdtlsInstall(_platform = process.platform) {
129
+ return null;
130
+ }
131
+
132
+ export function req(command, args, reason, installCommand = null, options = {}) {
133
+ return {
134
+ name: command,
135
+ command,
136
+ args,
137
+ reason,
138
+ install: installCommand,
139
+ installUnavailableReason: options.installUnavailableReason ?? null,
140
+ localOnly: options.localOnly ?? false,
141
+ };
142
+ }
143
+
144
+ export function unsafeReq(command, args, reason, installUnavailableReason) {
145
+ return req(command, args, reason, null, { installUnavailableReason });
146
+ }
147
+
148
+ export function packageManager(entries = []) {
149
+ return selectPackageManager(entries).manager;
150
+ }
151
+
152
+ export function packageManagerSource(entries = []) {
153
+ const selected = selectPackageManager(entries);
154
+ return selected.path ?? "the npm default (no lockfile found)";
155
+ }
156
+
157
+ export function installDev(pm, packages) {
158
+ if (pm === "bun") return { command: "bun", args: ["add", "-d", ...packages] };
159
+ if (pm === "pnpm") return { command: "pnpm", args: ["add", "-D", ...packages] };
160
+ if (pm === "yarn") return { command: "yarn", args: ["add", "-D", ...packages] };
161
+ return { command: "npm", args: ["install", "--save-dev", ...packages] };
162
+ }
163
+
164
+ function normalizePath(path) {
165
+ return path.split(sep).join("/");
166
+ }
167
+
168
+ function pathBasename(path) {
169
+ const normalized = normalizePath(path);
170
+ return normalized.slice(normalized.lastIndexOf("/") + 1);
171
+ }
172
+
173
+ function hasBasename(paths, ...names) {
174
+ const expected = new Set(names);
175
+ return paths.some((path) => expected.has(pathBasename(path)));
176
+ }
177
+
178
+ function findBasename(paths, ...names) {
179
+ const expected = new Set(names);
180
+ return [...paths]
181
+ .filter((path) => expected.has(pathBasename(path)))
182
+ .sort(compareRepositoryPaths)[0] ?? null;
183
+ }
184
+
185
+ function hasPathSegmentWithSuffix(path, ...suffixes) {
186
+ return normalizePath(path)
187
+ .split("/")
188
+ .some((segment) => suffixes.some((suffix) => segment.endsWith(suffix)));
189
+ }
190
+
191
+ function repositoryCommand(path) {
192
+ const normalized = normalizePath(path);
193
+ return normalized.startsWith("./") ? normalized : `./${normalized}`;
194
+ }
195
+
196
+ function selectPackageManager(entries) {
197
+ const lockfiles = [
198
+ { manager: "bun", names: new Set(["bun.lock", "bun.lockb"]), priority: 0 },
199
+ { manager: "pnpm", names: new Set(["pnpm-lock.yaml"]), priority: 1 },
200
+ { manager: "yarn", names: new Set(["yarn.lock"]), priority: 2 },
201
+ { manager: "npm", names: new Set(["package-lock.json", "npm-shrinkwrap.json"]), priority: 3 },
202
+ ];
203
+ const candidates = [];
204
+
205
+ for (const path of entries) {
206
+ const normalized = normalizePath(path);
207
+ const name = pathBasename(normalized);
208
+ const match = lockfiles.find(({ names }) => names.has(name));
209
+ if (!match) continue;
210
+ candidates.push({
211
+ manager: match.manager,
212
+ path: normalized,
213
+ depth: normalized.split("/").length - 1,
214
+ priority: match.priority,
215
+ });
216
+ }
217
+
218
+ candidates.sort((left, right) => left.depth - right.depth
219
+ || left.priority - right.priority
220
+ || left.path.localeCompare(right.path));
221
+ return candidates[0] ?? { manager: "npm", path: null };
222
+ }
223
+
224
+ function compareRepositoryPaths(left, right) {
225
+ const leftPath = normalizePath(left);
226
+ const rightPath = normalizePath(right);
227
+ const depth = leftPath.split("/").length - rightPath.split("/").length;
228
+ return depth || leftPath.localeCompare(rightPath);
229
+ }
@@ -0,0 +1,178 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { accessSync, constants, statSync } from "node:fs";
3
+ import { delimiter, isAbsolute, join, posix, resolve, win32 } from "node:path";
4
+
5
+ export const DEFAULT_TIMEOUT_MS = 10_000;
6
+
7
+ export function inspectCommand(requirement, {
8
+ root = process.cwd(),
9
+ runner = run,
10
+ timeoutMs = DEFAULT_TIMEOUT_MS,
11
+ localChecker = localExecutableAvailable,
12
+ platform = process.platform,
13
+ } = {}) {
14
+ const candidates = commandCandidates(requirement.command, root, platform);
15
+ const localOnly = requirement.localOnly || isRepositoryCommand(requirement.command, root);
16
+
17
+ if (!localOnly) {
18
+ let diagnostic = null;
19
+ for (const candidate of systemCommandCandidates(requirement.command, platform)) {
20
+ diagnostic = execute(candidate, requirement.args, { root, runner, timeoutMs });
21
+ if (diagnostic.status === 0) {
22
+ return {
23
+ ok: true,
24
+ availability: "verified",
25
+ source: "system",
26
+ candidate,
27
+ diagnostic,
28
+ };
29
+ }
30
+ }
31
+
32
+ for (const candidate of candidates.slice(1)) {
33
+ if (localChecker(candidate)) {
34
+ return {
35
+ ok: true,
36
+ availability: "discovered",
37
+ source: "repository",
38
+ candidate,
39
+ diagnostic,
40
+ };
41
+ }
42
+ }
43
+
44
+ return {
45
+ ok: false,
46
+ availability: diagnostic?.timedOut ? "timeout" : "missing",
47
+ source: null,
48
+ candidate: null,
49
+ diagnostic,
50
+ };
51
+ }
52
+
53
+ for (const candidate of repositoryCandidates(requirement.command, root)) {
54
+ if (localChecker(candidate)) {
55
+ return {
56
+ ok: true,
57
+ availability: "discovered",
58
+ source: "repository",
59
+ candidate,
60
+ diagnostic: null,
61
+ };
62
+ }
63
+ }
64
+
65
+ return {
66
+ ok: false,
67
+ availability: "missing",
68
+ source: null,
69
+ candidate: null,
70
+ diagnostic: null,
71
+ };
72
+ }
73
+
74
+ export function commandOk(command, args, root = process.cwd(), runner = run) {
75
+ return inspectCommand({ name: command, command, args, reason: "tool availability" }, { root, runner }).ok;
76
+ }
77
+
78
+ export function commandCandidates(command, root = process.cwd(), platform = process.platform) {
79
+ const candidates = isRepositoryCommand(command, root)
80
+ ? repositoryCandidates(command, root)
81
+ : [
82
+ command,
83
+ join(root, "node_modules", ".bin", command),
84
+ join(root, "vendor", "bin", command),
85
+ join(root, ".pota", "tools", "bin", command),
86
+ ];
87
+ if (platform !== "win32") return [...new Set(candidates)];
88
+ const extensions = [".cmd", ".bat", ".exe"];
89
+ return [...new Set(candidates.flatMap((candidate) => hasWindowsExecutableExtension(candidate) ? [candidate] : [candidate, ...extensions.map((extension) => `${candidate}${extension}`)]))];
90
+ }
91
+
92
+ export function systemCommandCandidates(command, platform = process.platform) {
93
+ if (platform !== "win32" || hasWindowsExecutableExtension(command) || isAbsolute(command)) return [command];
94
+ return [command, `${command}.cmd`, `${command}.bat`, `${command}.exe`];
95
+ }
96
+
97
+ export function localExecutableAvailable(command, platform = process.platform) {
98
+ try {
99
+ const info = statSync(command);
100
+ if (!info.isFile()) return false;
101
+ accessSync(command, platform === "win32" ? constants.F_OK : constants.X_OK);
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ export function run(command, args, { cwd = process.cwd(), timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
109
+ const startedAt = Date.now();
110
+ const result = spawnSync(command, args, {
111
+ cwd,
112
+ stdio: "ignore",
113
+ encoding: "utf8",
114
+ shell: shouldUseShell(command),
115
+ timeout: timeoutMs,
116
+ env: safeEnvironment(cwd),
117
+ });
118
+ return { ...result, durationMs: Date.now() - startedAt };
119
+ }
120
+
121
+ export function shouldUseShell(command, platform = process.platform) {
122
+ return platform === "win32" && /\.(cmd|bat)$/i.test(command);
123
+ }
124
+
125
+ export function execute(command, args, { root, runner, timeoutMs }) {
126
+ const startedAt = Date.now();
127
+ try {
128
+ const result = runner(command, args, { cwd: root, timeoutMs });
129
+ return {
130
+ status: Number.isInteger(result?.status) ? result.status : null,
131
+ signal: result?.signal ?? null,
132
+ errorCode: result?.error?.code ?? null,
133
+ timedOut: result?.error?.code === "ETIMEDOUT",
134
+ durationMs: Number.isFinite(result?.durationMs) ? result.durationMs : Date.now() - startedAt,
135
+ };
136
+ } catch (error) {
137
+ return {
138
+ status: null,
139
+ signal: null,
140
+ errorCode: error && typeof error === "object" && "code" in error ? String(error.code) : "RUNNER_ERROR",
141
+ timedOut: false,
142
+ durationMs: Date.now() - startedAt,
143
+ };
144
+ }
145
+ }
146
+
147
+ function repositoryCandidates(command, root) {
148
+ const normalizedCommand = command.replace(/^\.\//, "");
149
+ const direct = isAbsolute(command) ? command : resolve(root, normalizedCommand);
150
+ return [...new Set([direct])];
151
+ }
152
+
153
+ function isRepositoryCommand(command, root) {
154
+ if (/^(?:\.\.?[\\/])/.test(command)) return true;
155
+ if (/^(?:gradlew|gradlew\.bat|mvnw|mvnw\.cmd|mvnw\.bat)$/i.test(command)) return true;
156
+ return isAbsolute(command) && isWithin(root, command);
157
+ }
158
+
159
+ export function isWithin(root, candidate, platform = process.platform) {
160
+ const pathApi = platform === "win32" ? win32 : posix;
161
+ const path = pathApi.relative(pathApi.resolve(root), pathApi.resolve(candidate));
162
+ return path === "" || (!pathApi.isAbsolute(path) && !path.startsWith(`..${pathApi.sep}`) && path !== "..");
163
+ }
164
+
165
+ function safeEnvironment(root) {
166
+ const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
167
+ const safePath = (process.env[pathKey] ?? "")
168
+ .split(delimiter)
169
+ .filter(Boolean)
170
+ .filter((entry) => isAbsolute(entry) && !isWithin(root, entry))
171
+ .join(delimiter);
172
+ return { ...process.env, [pathKey]: safePath };
173
+ }
174
+
175
+
176
+ function hasWindowsExecutableExtension(command) {
177
+ return /\.(cmd|bat|exe)$/i.test(command);
178
+ }
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, realpathSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import {
6
+ DEFAULT_TIMEOUT_MS,
7
+ commandOk,
8
+ execute,
9
+ inspectCommand,
10
+ localExecutableAvailable,
11
+ run,
12
+ } from "./pota-toolcheck-runtime.mjs";
13
+ import { buildRequirements, collectEntries, detectStacks } from "./pota-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 "./pota-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 "./pota-toolcheck-detect.mjs";
42
+
43
+ const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
44
+ const usage = "Usage: pota toolcheck [--root PATH] [--json] [--install --yes]";
45
+
46
+ if (isCliEntry()) {
47
+ const args = process.argv.slice(2);
48
+ try {
49
+ const options = parseCliArgs(args);
50
+ if (options.action === "help") {
51
+ console.log(usage);
52
+ } else if (options.action === "version") {
53
+ console.log(`pota ${version}`);
54
+ } else {
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;
59
+ }
60
+ } catch (error) {
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(`Pota toolcheck error: ${message}`);
66
+ }
67
+ process.exitCode = 2;
68
+ }
69
+ }
70
+
71
+ export function parseCliArgs(args, cwd = process.cwd()) {
72
+ let root = cwd;
73
+ let install = false;
74
+ let yes = false;
75
+ let json = false;
76
+ let action = "check";
77
+ const provided = new Set();
78
+
79
+ for (let index = 0; index < args.length; index += 1) {
80
+ const argument = args[index];
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);
93
+ if (!value || value.startsWith("--")) throw new Error("--root requires a path");
94
+ root = value;
95
+ if (argument === "--root") index += 1;
96
+ } else {
97
+ throw new Error(`unknown option: ${argument}`);
98
+ }
99
+ }
100
+
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 };
105
+ }
106
+
107
+ export async function createReport({
108
+ root = process.cwd(),
109
+ install = false,
110
+ yes = false,
111
+ runner = run,
112
+ timeoutMs = DEFAULT_TIMEOUT_MS,
113
+ localChecker = localExecutableAvailable,
114
+ platform = process.platform,
115
+ } = {}) {
116
+ const resolvedRoot = resolve(root);
117
+ const entries = await collectEntries(resolvedRoot);
118
+ const detected = detectStacks(entries);
119
+ const requirements = buildRequirements(detected, entries, commandOk, platform);
120
+ const results = [];
121
+ const installRefused = install && !yes;
122
+
123
+ for (const requirement of requirements) {
124
+ let check = inspectCommand(requirement, {
125
+ root: resolvedRoot,
126
+ runner,
127
+ timeoutMs,
128
+ localChecker,
129
+ platform,
130
+ });
131
+ let installed = false;
132
+ let installDiagnostic = null;
133
+ let installRefusal = null;
134
+
135
+ if (!check.ok && install) {
136
+ if (!yes) {
137
+ installRefusal = "explicit-confirmation-required";
138
+ } else if (!requirement.install) {
139
+ installRefusal = "installer-unavailable";
140
+ } else {
141
+ installDiagnostic = execute(requirement.install.command, requirement.install.args, {
142
+ root: resolvedRoot,
143
+ runner,
144
+ timeoutMs,
145
+ });
146
+ installed = installDiagnostic.status === 0;
147
+ if (installed) {
148
+ check = inspectCommand(requirement, {
149
+ root: resolvedRoot,
150
+ runner,
151
+ timeoutMs,
152
+ localChecker,
153
+ platform,
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ results.push({
160
+ name: requirement.name,
161
+ reason: requirement.reason,
162
+ ok: check.ok,
163
+ availability: check.availability,
164
+ source: check.source,
165
+ candidate: check.candidate,
166
+ check: check.diagnostic,
167
+ installed,
168
+ installCommand: requirement.install ? formatCommand(requirement.install) : null,
169
+ installStatus: installDiagnostic?.status ?? null,
170
+ install: {
171
+ available: Boolean(requirement.install),
172
+ unavailableReason: requirement.installUnavailableReason ?? null,
173
+ requested: install,
174
+ attempted: Boolean(installDiagnostic),
175
+ refusal: installRefusal,
176
+ diagnostic: installDiagnostic,
177
+ },
178
+ });
179
+ }
180
+
181
+ return {
182
+ schemaVersion: 1,
183
+ root: resolvedRoot,
184
+ timeoutMs,
185
+ detected,
186
+ install: {
187
+ requested: install,
188
+ confirmed: install && yes,
189
+ refused: installRefused,
190
+ refusalReason: installRefused ? "--install requires the additional --yes confirmation flag" : null,
191
+ },
192
+ results,
193
+ };
194
+ }
195
+
196
+ function printReport(report) {
197
+ console.log(`Pota toolcheck: ${report.root}`);
198
+ console.log(`Detected stacks: ${report.detected.length > 0 ? report.detected.join(", ") : "none"}`);
199
+ if (report.install.refused) console.log(`INSTALL REFUSED - ${report.install.refusalReason}`);
200
+ for (const result of report.results) {
201
+ const prefix = result.ok ? result.availability === "discovered" ? "DISCOVERED" : "OK" : "MISSING";
202
+ const installedText = result.installed ? " installed" : "";
203
+ console.log(`${prefix}${installedText} ${result.name} - ${result.reason}`);
204
+ if (result.check?.timedOut) console.log(` check timed out after ${result.check.durationMs}ms`);
205
+ if (!result.ok && result.install.unavailableReason) console.log(` install unavailable: ${result.install.unavailableReason}`);
206
+ else if (!result.ok && result.installCommand) console.log(` install: ${result.installCommand}`);
207
+ }
208
+ }
209
+
210
+ function formatCommand(command) {
211
+ return [command.command, ...command.args].join(" ");
212
+ }
213
+
214
+ function isCliEntry() {
215
+ if (!process.argv[1]) return false;
216
+ try {
217
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
218
+ } catch {
219
+ return import.meta.url === pathToFileURL(process.argv[1]).href;
220
+ }
221
+ }