@christang/keel 5.1.1 → 5.2.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.
- package/LICENSE +21 -21
- package/README.md +114 -158
- package/README.zh-CN.md +118 -197
- package/assets/bootstrap/AGENTS.md +1 -1
- package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/hardware-dsl.md +4 -2
- package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/hardware.md +4 -2
- package/{plugins/keel/skills/keel-align-expectations/references → assets/lenses}/web.md +4 -2
- package/assets/openspec/schemas/keel-spec-driven/schema.yaml +172 -166
- package/assets/openspec/schemas/keel-spec-driven/templates/tasks.md +13 -4
- package/bin/keel.js +218 -15
- package/package.json +1 -1
- package/plugins/keel/.claude-plugin/plugin.json +1 -1
- package/plugins/keel/.codex-plugin/plugin.json +1 -1
- package/plugins/keel/hooks/hooks.json +30 -30
- package/plugins/keel/scripts/pretooluse-guard.js +156 -156
- package/plugins/keel/scripts/session-start.js +182 -182
- package/plugins/keel/skills/keel-align-expectations/SKILL.md +2 -6
- package/plugins/keel/skills/keel-debug-failure/SKILL.md +2 -2
- package/plugins/keel/skills/keel-review-checklist/SKILL.md +2 -2
- package/plugins/keel/skills/keel-tdd-or-test-first/SKILL.md +2 -2
- package/scripts/bump_version.js +140 -0
- package/scripts/install_to_repo.py +0 -70
- package/scripts/run_python.js +63 -63
- package/scripts/validate_plugin.py +408 -96
- package/src/core/capabilities.js +291 -291
- package/src/core/context.js +521 -514
- package/src/core/gates.js +664 -643
- package/src/core/goal.js +230 -230
- package/src/core/guard.js +295 -295
- package/src/core/helper.js +319 -319
- package/src/core/projection.js +195 -195
- package/src/core/task-contract.js +757 -736
- package/src/core/tasksview.js +123 -123
|
@@ -40,13 +40,9 @@ Accepted alignment routes to existing OpenSpec owners; create no separate alignm
|
|
|
40
40
|
- specs own observable requirements and positive/negative/edge/failure scenarios.
|
|
41
41
|
- tasks.md owns Covers, verification strategy and checks, scope, and stop boundaries that reference the accepted authority instead of duplicating chat prose.
|
|
42
42
|
|
|
43
|
-
## Domain
|
|
43
|
+
## Domain lenses
|
|
44
44
|
|
|
45
|
-
When the change
|
|
46
|
-
|
|
47
|
-
- references/web.md for UI, API, routing, auth/session, persistence, or backend integration work.
|
|
48
|
-
- references/hardware.md for Verilog/SystemVerilog interface, protocol, reset, or verification work.
|
|
49
|
-
- references/hardware-dsl.md for hardware modeling DSL, generated RTL, or golden/equivalence work.
|
|
45
|
+
When the change signals a specific domain, look in `keel/lenses/` for a lens whose `Applies when:` header matches, and read only that lens before asking domain questions; do not load unrelated lenses. When no lens matches, or the repo defines none, proceed on the domain-agnostic path. Lenses are user-authored; scaffold the bundled starting points with `keel lenses add` (web, hardware, hardware-dsl).
|
|
50
46
|
|
|
51
47
|
## Boundaries
|
|
52
48
|
|
|
@@ -12,9 +12,9 @@ Use this skill when a command or validation path fails. Keep the investigation t
|
|
|
12
12
|
|
|
13
13
|
Read the selected OpenSpec task's Commands, Acceptance, Coupling, Candidate Boundary, Autonomy boundary, Stop Rules, Evidence, Stop if, Read, Touch, and Mode fields. When `Coupling: required`, also read design.md's Coupled Iteration Contract. Read the failed command output and any repository files needed to reproduce or explain the failure.
|
|
14
14
|
|
|
15
|
-
## Domain
|
|
15
|
+
## Domain lenses
|
|
16
16
|
|
|
17
|
-
When the change's artifacts or the failing surface signal a
|
|
17
|
+
When the change's artifacts or the failing surface signal a domain, consult the matching lens's `Execution and review checks` section from `keel/lenses/` — the lens whose `Applies when:` header matches — before locking a root-cause hypothesis, and load only that one. When no lens matches, load nothing.
|
|
18
18
|
|
|
19
19
|
## Fuse
|
|
20
20
|
|
|
@@ -35,9 +35,9 @@ Record the current agent's judgment inside the selected task Evidence:
|
|
|
35
35
|
|
|
36
36
|
The Review remains in tasks.md. A user-facing Report summarizes delivery but is not hidden gate state. Do not let Core or this checklist write evidence automatically.
|
|
37
37
|
|
|
38
|
-
## Domain
|
|
38
|
+
## Domain lenses
|
|
39
39
|
|
|
40
|
-
When the change's artifacts or Touch extensions signal a
|
|
40
|
+
When the change's artifacts or Touch extensions signal a domain, consult the matching lens's `Execution and review checks` section from `keel/lenses/` — the lens whose `Applies when:` header matches — before concluding the review, and load only that one. When no lens matches, load nothing.
|
|
41
41
|
|
|
42
42
|
## Expectation and follow-up ownership
|
|
43
43
|
|
|
@@ -24,9 +24,9 @@ Read the selected task's compiled capsule: resolved Acceptance, Verify strategy
|
|
|
24
24
|
|
|
25
25
|
Red-green strategies (`vertical-tdd`, `regression-first`) must record concrete per-label `.red` and `.green` Evidence entries for the same check; `keel gate task-complete` rejects absent or pending entries.
|
|
26
26
|
|
|
27
|
-
## Domain
|
|
27
|
+
## Domain lenses
|
|
28
28
|
|
|
29
|
-
When the change's proposal/design/specs or the task's Touch extensions signal a
|
|
29
|
+
When the change's proposal/design/specs or the task's Touch extensions signal a domain, consult the matching lens's `Execution and review checks` section from `keel/lenses/` — the lens whose `Applies when:` header matches — before finalizing the strategy and the first check, and load only that one. When no lens matches, load nothing.
|
|
30
30
|
|
|
31
31
|
## Coupled-task preflight
|
|
32
32
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// One-shot version bump across every place Keel pins its version.
|
|
5
|
+
//
|
|
6
|
+
// The Keel validation suite requires the same version in package.json,
|
|
7
|
+
// package-lock.json, both native plugin manifests, the validator constants,
|
|
8
|
+
// the protocol docs, and the changelog. This script updates all of them
|
|
9
|
+
// together so a release never ships half-aligned.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node scripts/bump_version.js <patch|minor|major|explicit-version>
|
|
13
|
+
//
|
|
14
|
+
// After running: fill in the CHANGELOG entry, then `npm test`, commit,
|
|
15
|
+
// tag `vX.Y.Z`, push, and publish a GitHub Release.
|
|
16
|
+
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
21
|
+
const PKG_PATH = path.join(ROOT, "package.json");
|
|
22
|
+
const LOCK_PATH = path.join(ROOT, "package-lock.json");
|
|
23
|
+
const CHANGELOG_PATH = path.join(ROOT, "keel", "CHANGELOG.md");
|
|
24
|
+
const CHANGELOG_HEADER = "# Keel Changelog\n\n";
|
|
25
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
26
|
+
|
|
27
|
+
function fail(message) {
|
|
28
|
+
process.stderr.write(`bump-version: ${message}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveNewVersion(current, target) {
|
|
33
|
+
if (target === "patch" || target === "minor" || target === "major") {
|
|
34
|
+
const [, major, minor, patch] = current.match(SEMVER_RE).map(Number);
|
|
35
|
+
if (target === "major") return `${major + 1}.0.0`;
|
|
36
|
+
if (target === "minor") return `${major}.${minor + 1}.0`;
|
|
37
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
38
|
+
}
|
|
39
|
+
if (!SEMVER_RE.test(target)) {
|
|
40
|
+
fail(`not a patch|minor|major keyword or an X.Y.Z version: ${target}`);
|
|
41
|
+
}
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeJson(filePath, data) {
|
|
46
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bumpPackageFiles(newVersion) {
|
|
50
|
+
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, "utf8"));
|
|
51
|
+
pkg.version = newVersion;
|
|
52
|
+
writeJson(PKG_PATH, pkg);
|
|
53
|
+
process.stdout.write(" updated package.json\n");
|
|
54
|
+
|
|
55
|
+
const lock = JSON.parse(fs.readFileSync(LOCK_PATH, "utf8"));
|
|
56
|
+
lock.version = newVersion;
|
|
57
|
+
if (lock.packages && lock.packages[""]) {
|
|
58
|
+
lock.packages[""].version = newVersion;
|
|
59
|
+
}
|
|
60
|
+
writeJson(LOCK_PATH, lock);
|
|
61
|
+
process.stdout.write(" updated package-lock.json\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function replaceInFile(relPath, replacements) {
|
|
65
|
+
const filePath = path.join(ROOT, relPath);
|
|
66
|
+
let content = fs.readFileSync(filePath, "utf8");
|
|
67
|
+
for (const [from, to] of replacements) {
|
|
68
|
+
if (!content.includes(from)) {
|
|
69
|
+
fail(`expected to find ${JSON.stringify(from)} in ${relPath}`);
|
|
70
|
+
}
|
|
71
|
+
content = content.split(from).join(to);
|
|
72
|
+
}
|
|
73
|
+
fs.writeFileSync(filePath, content);
|
|
74
|
+
process.stdout.write(` updated ${relPath}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function prependChangelogEntry(newVersion) {
|
|
78
|
+
let content = fs.readFileSync(CHANGELOG_PATH, "utf8");
|
|
79
|
+
if (content.includes(`## ${newVersion} `) || content.includes(`## ${newVersion}\n`)) {
|
|
80
|
+
process.stdout.write(` keel/CHANGELOG.md already has a ${newVersion} entry\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!content.startsWith(CHANGELOG_HEADER)) {
|
|
84
|
+
fail("keel/CHANGELOG.md does not start with the expected header");
|
|
85
|
+
}
|
|
86
|
+
const entry =
|
|
87
|
+
`## ${newVersion} - TODO: summarize this release\n\n` +
|
|
88
|
+
"- TODO: describe the change.\n" +
|
|
89
|
+
"- Version alignment: the npm package, both native plugin manifests, " +
|
|
90
|
+
`protocol docs, and this changelog share Keel ${newVersion}; the OpenSpec ` +
|
|
91
|
+
"dependency pin stays `^1.4.1`.\n\n";
|
|
92
|
+
content = CHANGELOG_HEADER + entry + content.slice(CHANGELOG_HEADER.length);
|
|
93
|
+
fs.writeFileSync(CHANGELOG_PATH, content);
|
|
94
|
+
process.stdout.write(" updated keel/CHANGELOG.md (fill in the TODO lines)\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function main() {
|
|
98
|
+
const target = process.argv[2];
|
|
99
|
+
if (!target) {
|
|
100
|
+
fail("usage: node scripts/bump_version.js <patch|minor|major|explicit-version>");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const oldVersion = JSON.parse(fs.readFileSync(PKG_PATH, "utf8")).version;
|
|
104
|
+
if (!SEMVER_RE.test(oldVersion)) {
|
|
105
|
+
fail(`current package.json version is not X.Y.Z: ${oldVersion}`);
|
|
106
|
+
}
|
|
107
|
+
const newVersion = resolveNewVersion(oldVersion, target);
|
|
108
|
+
process.stdout.write(`Bumping ${oldVersion} -> ${newVersion}\n`);
|
|
109
|
+
|
|
110
|
+
bumpPackageFiles(newVersion);
|
|
111
|
+
replaceInFile("plugins/keel/.claude-plugin/plugin.json", [
|
|
112
|
+
[`"version": "${oldVersion}"`, `"version": "${newVersion}"`],
|
|
113
|
+
]);
|
|
114
|
+
replaceInFile("plugins/keel/.codex-plugin/plugin.json", [
|
|
115
|
+
[`"version": "${oldVersion}"`, `"version": "${newVersion}"`],
|
|
116
|
+
]);
|
|
117
|
+
replaceInFile("scripts/validate_plugin.py", [
|
|
118
|
+
[`PACKAGE_VERSION = "${oldVersion}"`, `PACKAGE_VERSION = "${newVersion}"`],
|
|
119
|
+
[`PROTOCOL_VERSION = "${oldVersion}"`, `PROTOCOL_VERSION = "${newVersion}"`],
|
|
120
|
+
]);
|
|
121
|
+
replaceInFile("AGENTS.md", [
|
|
122
|
+
[`v${oldVersion}`, `v${newVersion}`],
|
|
123
|
+
[`version=${oldVersion}`, `version=${newVersion}`],
|
|
124
|
+
]);
|
|
125
|
+
replaceInFile("assets/bootstrap/AGENTS.md", [
|
|
126
|
+
[`version=${oldVersion}`, `version=${newVersion}`],
|
|
127
|
+
]);
|
|
128
|
+
prependChangelogEntry(newVersion);
|
|
129
|
+
|
|
130
|
+
process.stdout.write(
|
|
131
|
+
`\nDone. Next:\n` +
|
|
132
|
+
` 1. Edit keel/CHANGELOG.md ${newVersion} entry.\n` +
|
|
133
|
+
` 2. npm test\n` +
|
|
134
|
+
` 3. git commit -am "${newVersion}"\n` +
|
|
135
|
+
` 4. git tag v${newVersion} && git push --follow-tags && git push origin v${newVersion}\n` +
|
|
136
|
+
` 5. gh release create v${newVersion} --title v${newVersion} --notes "..."\n`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
main();
|
|
@@ -51,23 +51,11 @@ KEEL_HOOK_NAME = "keel-gate"
|
|
|
51
51
|
KEEL_HOOK_ROOT = Path(".claude") / "hooks" / KEEL_HOOK_NAME
|
|
52
52
|
SUPPORTED_TARGETS = ("claude", "codex", "opencode")
|
|
53
53
|
AGENT_PROTOCOL_TARGETS = {"codex", "opencode"}
|
|
54
|
-
TARGET_SKILL_ROOTS = {
|
|
55
|
-
"claude": Path(".claude") / "skills",
|
|
56
|
-
"codex": Path(".agents") / "skills",
|
|
57
|
-
"opencode": Path(".opencode") / "skills",
|
|
58
|
-
}
|
|
59
54
|
TARGET_ADAPTER_PATHS = {
|
|
60
55
|
"claude": Path(".claude") / "keel" / "keel-adapter.js",
|
|
61
56
|
"codex": Path(".agents") / "keel" / "keel-adapter.js",
|
|
62
57
|
"opencode": Path(".opencode") / "keel" / "keel-adapter.js",
|
|
63
58
|
}
|
|
64
|
-
CORE_KEEL_SKILLS = {
|
|
65
|
-
"keel-align-expectations",
|
|
66
|
-
"keel-debug-failure",
|
|
67
|
-
"keel-handoff",
|
|
68
|
-
"keel-review-checklist",
|
|
69
|
-
"keel-tdd-or-test-first",
|
|
70
|
-
}
|
|
71
59
|
HANDOFF_FIELDS = {"schema", "owner", "action", "reason"}
|
|
72
60
|
HANDOFF_ACTIONS = {
|
|
73
61
|
"discuss",
|
|
@@ -158,10 +146,6 @@ def target_set(target: str) -> set[str]:
|
|
|
158
146
|
return set(target_names(target))
|
|
159
147
|
|
|
160
148
|
|
|
161
|
-
def core_skill_names() -> set[str]:
|
|
162
|
-
return set(CORE_KEEL_SKILLS)
|
|
163
|
-
|
|
164
|
-
|
|
165
149
|
def file_action(relative_path: str, source_path: Path) -> InstallAction:
|
|
166
150
|
if not source_path.is_file():
|
|
167
151
|
raise ValueError(f"missing packaged asset: {source_path}")
|
|
@@ -233,31 +217,6 @@ def openspec_schema_actions() -> list[InstallAction]:
|
|
|
233
217
|
return actions
|
|
234
218
|
|
|
235
219
|
|
|
236
|
-
def skill_actions(target: str) -> list[InstallAction]:
|
|
237
|
-
actions: list[InstallAction] = []
|
|
238
|
-
selected_skill_names = core_skill_names()
|
|
239
|
-
for target_name in target_names(target):
|
|
240
|
-
skill_destination_root = TARGET_SKILL_ROOTS[target_name]
|
|
241
|
-
skills_root = dist_asset(target_name, "skills")
|
|
242
|
-
if skills_root.is_dir():
|
|
243
|
-
for skill in sorted(skills_root.iterdir()):
|
|
244
|
-
if not skill.is_dir():
|
|
245
|
-
continue
|
|
246
|
-
if skill.name not in selected_skill_names:
|
|
247
|
-
continue
|
|
248
|
-
for skill_file in sorted(skill.rglob("*")):
|
|
249
|
-
if not skill_file.is_file():
|
|
250
|
-
continue
|
|
251
|
-
relative_file = skill_file.relative_to(skill).as_posix()
|
|
252
|
-
actions.append(
|
|
253
|
-
file_action(
|
|
254
|
-
(skill_destination_root / skill.name / relative_file).as_posix(),
|
|
255
|
-
skill_file,
|
|
256
|
-
)
|
|
257
|
-
)
|
|
258
|
-
return actions
|
|
259
|
-
|
|
260
|
-
|
|
261
220
|
def agent_actions(target: str) -> list[InstallAction]:
|
|
262
221
|
actions: list[InstallAction] = []
|
|
263
222
|
if "claude" not in target_set(target):
|
|
@@ -945,35 +904,6 @@ def rmdir_if_empty_action(relative_dir: str) -> PlannedAction:
|
|
|
945
904
|
return PlannedAction("rmdir", Path(relative_dir))
|
|
946
905
|
|
|
947
906
|
|
|
948
|
-
def plan_uninstall_skill_actions(repo: Path, target: str) -> list[PlannedAction]:
|
|
949
|
-
actions: list[PlannedAction] = []
|
|
950
|
-
for target_name in target_names(target):
|
|
951
|
-
skill_root = TARGET_SKILL_ROOTS[target_name]
|
|
952
|
-
target_actions = skill_actions(target_name)
|
|
953
|
-
for action in target_actions:
|
|
954
|
-
if action.source_path is None:
|
|
955
|
-
continue
|
|
956
|
-
actions.append(
|
|
957
|
-
plan_uninstall_packaged_file(
|
|
958
|
-
repo,
|
|
959
|
-
action.relative_path.as_posix(),
|
|
960
|
-
action.source_path,
|
|
961
|
-
)
|
|
962
|
-
)
|
|
963
|
-
|
|
964
|
-
skill_dirs: set[Path] = set()
|
|
965
|
-
for action in target_actions:
|
|
966
|
-
parent = action.relative_path.parent
|
|
967
|
-
while parent != skill_root:
|
|
968
|
-
skill_dirs.add(parent)
|
|
969
|
-
parent = parent.parent
|
|
970
|
-
for skill_dir in sorted(skill_dirs, key=lambda path: len(path.parts), reverse=True):
|
|
971
|
-
actions.append(rmdir_if_empty_action(skill_dir.as_posix()))
|
|
972
|
-
actions.append(rmdir_if_empty_action(skill_root.as_posix()))
|
|
973
|
-
actions.append(rmdir_if_empty_action(skill_root.parent.as_posix()))
|
|
974
|
-
return actions
|
|
975
|
-
|
|
976
|
-
|
|
977
907
|
def plan_uninstall_agent_actions(repo: Path, target: str) -> list[PlannedAction]:
|
|
978
908
|
actions: list[PlannedAction] = []
|
|
979
909
|
if "claude" not in target_set(target):
|
package/scripts/run_python.js
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
|
-
|
|
4
|
-
const { spawnSync } = require("child_process");
|
|
5
|
-
|
|
6
|
-
function pythonCandidates() {
|
|
7
|
-
if (process.env.KEEL_PYTHON) {
|
|
8
|
-
return [{ command: process.env.KEEL_PYTHON, prefixArgs: [] }];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
if (process.platform === "win32") {
|
|
12
|
-
return [
|
|
13
|
-
{ command: "py", prefixArgs: ["-3"] },
|
|
14
|
-
{ command: "python", prefixArgs: [] },
|
|
15
|
-
{ command: "python3", prefixArgs: [] },
|
|
16
|
-
];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return [
|
|
20
|
-
{ command: "python3", prefixArgs: [] },
|
|
21
|
-
{ command: "python", prefixArgs: [] },
|
|
22
|
-
];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function commandExists(candidate) {
|
|
26
|
-
const result = spawnSync(
|
|
27
|
-
candidate.command,
|
|
28
|
-
[...candidate.prefixArgs, "--version"],
|
|
29
|
-
{ encoding: "utf8" }
|
|
30
|
-
);
|
|
31
|
-
return !result.error && result.status === 0;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function main() {
|
|
35
|
-
const args = process.argv.slice(2);
|
|
36
|
-
if (args.length === 0) {
|
|
37
|
-
process.stderr.write("run_python: expected a Python script path.\n");
|
|
38
|
-
return 2;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const candidate = pythonCandidates().find(commandExists);
|
|
42
|
-
if (!candidate) {
|
|
43
|
-
process.stderr.write(
|
|
44
|
-
"run_python: could not find Python. Install python3/python or set KEEL_PYTHON.\n"
|
|
45
|
-
);
|
|
46
|
-
return 127;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const result = spawnSync(
|
|
50
|
-
candidate.command,
|
|
51
|
-
[...candidate.prefixArgs, ...args],
|
|
52
|
-
{ stdio: "inherit" }
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
if (result.error) {
|
|
56
|
-
process.stderr.write(`run_python: ${result.error.message}\n`);
|
|
57
|
-
return 127;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return result.status ?? 1;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
process.exit(main());
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
5
|
+
|
|
6
|
+
function pythonCandidates() {
|
|
7
|
+
if (process.env.KEEL_PYTHON) {
|
|
8
|
+
return [{ command: process.env.KEEL_PYTHON, prefixArgs: [] }];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (process.platform === "win32") {
|
|
12
|
+
return [
|
|
13
|
+
{ command: "py", prefixArgs: ["-3"] },
|
|
14
|
+
{ command: "python", prefixArgs: [] },
|
|
15
|
+
{ command: "python3", prefixArgs: [] },
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return [
|
|
20
|
+
{ command: "python3", prefixArgs: [] },
|
|
21
|
+
{ command: "python", prefixArgs: [] },
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function commandExists(candidate) {
|
|
26
|
+
const result = spawnSync(
|
|
27
|
+
candidate.command,
|
|
28
|
+
[...candidate.prefixArgs, "--version"],
|
|
29
|
+
{ encoding: "utf8" }
|
|
30
|
+
);
|
|
31
|
+
return !result.error && result.status === 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function main() {
|
|
35
|
+
const args = process.argv.slice(2);
|
|
36
|
+
if (args.length === 0) {
|
|
37
|
+
process.stderr.write("run_python: expected a Python script path.\n");
|
|
38
|
+
return 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const candidate = pythonCandidates().find(commandExists);
|
|
42
|
+
if (!candidate) {
|
|
43
|
+
process.stderr.write(
|
|
44
|
+
"run_python: could not find Python. Install python3/python or set KEEL_PYTHON.\n"
|
|
45
|
+
);
|
|
46
|
+
return 127;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const result = spawnSync(
|
|
50
|
+
candidate.command,
|
|
51
|
+
[...candidate.prefixArgs, ...args],
|
|
52
|
+
{ stdio: "inherit" }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (result.error) {
|
|
56
|
+
process.stderr.write(`run_python: ${result.error.message}\n`);
|
|
57
|
+
return 127;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return result.status ?? 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
process.exit(main());
|