okstra 0.1.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/README.md +36 -0
- package/bin/okstra +62 -0
- package/package.json +30 -0
- package/runtime/.gitkeep +0 -0
- package/runtime/BUILD.json +5 -0
- package/runtime/agents/SKILL.md +243 -0
- package/runtime/agents/TODO.md +168 -0
- package/runtime/agents/workers/claude-worker.md +106 -0
- package/runtime/agents/workers/codex-worker.md +179 -0
- package/runtime/agents/workers/gemini-worker.md +179 -0
- package/runtime/agents/workers/report-writer-worker.md +116 -0
- package/runtime/bin/okstra-central.sh +152 -0
- package/runtime/bin/okstra-codex-exec.sh +53 -0
- package/runtime/bin/okstra-error-log.py +295 -0
- package/runtime/bin/okstra-gemini-exec.sh +55 -0
- package/runtime/bin/okstra-token-usage.py +46 -0
- package/runtime/bin/okstra.sh +162 -0
- package/runtime/prompts/launch.template.md +52 -0
- package/runtime/prompts/profiles/error-analysis.md +43 -0
- package/runtime/prompts/profiles/final-verification.md +37 -0
- package/runtime/prompts/profiles/implementation-planning.md +85 -0
- package/runtime/prompts/profiles/implementation.md +71 -0
- package/runtime/prompts/profiles/requirements-discovery.md +43 -0
- package/runtime/python/lib/okstra/cli.sh +227 -0
- package/runtime/python/lib/okstra/globals.sh +157 -0
- package/runtime/python/lib/okstra/interactive.sh +411 -0
- package/runtime/python/lib/okstra/project-resolver.sh +57 -0
- package/runtime/python/lib/okstra/usage.sh +98 -0
- package/runtime/python/lib/okstra-ctl/cmd-batch.sh +59 -0
- package/runtime/python/lib/okstra-ctl/cmd-list.sh +35 -0
- package/runtime/python/lib/okstra-ctl/cmd-open.sh +36 -0
- package/runtime/python/lib/okstra-ctl/cmd-projects.sh +26 -0
- package/runtime/python/lib/okstra-ctl/cmd-reconcile.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-reindex.sh +38 -0
- package/runtime/python/lib/okstra-ctl/cmd-rerun.sh +326 -0
- package/runtime/python/lib/okstra-ctl/cmd-show.sh +27 -0
- package/runtime/python/lib/okstra-ctl/cmd-tail.sh +76 -0
- package/runtime/python/lib/okstra-ctl/main.sh +41 -0
- package/runtime/python/lib/okstra-ctl/prepare.sh +29 -0
- package/runtime/python/lib/okstra-ctl/usage.sh +23 -0
- package/runtime/python/okstra_ctl/__init__.py +125 -0
- package/runtime/python/okstra_ctl/backfill.py +253 -0
- package/runtime/python/okstra_ctl/batch.py +62 -0
- package/runtime/python/okstra_ctl/ids.py +84 -0
- package/runtime/python/okstra_ctl/index.py +216 -0
- package/runtime/python/okstra_ctl/invocation.py +49 -0
- package/runtime/python/okstra_ctl/jsonl.py +84 -0
- package/runtime/python/okstra_ctl/listing.py +156 -0
- package/runtime/python/okstra_ctl/locks.py +42 -0
- package/runtime/python/okstra_ctl/material.py +62 -0
- package/runtime/python/okstra_ctl/models.py +63 -0
- package/runtime/python/okstra_ctl/path_resolve.py +40 -0
- package/runtime/python/okstra_ctl/paths.py +251 -0
- package/runtime/python/okstra_ctl/project_meta.py +51 -0
- package/runtime/python/okstra_ctl/reconcile.py +166 -0
- package/runtime/python/okstra_ctl/render.py +1065 -0
- package/runtime/python/okstra_ctl/resolver.py +54 -0
- package/runtime/python/okstra_ctl/run.py +674 -0
- package/runtime/python/okstra_ctl/run_context.py +166 -0
- package/runtime/python/okstra_ctl/seeding.py +97 -0
- package/runtime/python/okstra_ctl/sequence.py +53 -0
- package/runtime/python/okstra_ctl/session.py +33 -0
- package/runtime/python/okstra_ctl/tmux.py +27 -0
- package/runtime/python/okstra_ctl/workers.py +64 -0
- package/runtime/python/okstra_ctl/workflow.py +182 -0
- package/runtime/python/okstra_project/__init__.py +41 -0
- package/runtime/python/okstra_project/resolver.py +126 -0
- package/runtime/python/okstra_project/state.py +170 -0
- package/runtime/python/okstra_token_usage/__init__.py +26 -0
- package/runtime/python/okstra_token_usage/blocks.py +62 -0
- package/runtime/python/okstra_token_usage/claude.py +97 -0
- package/runtime/python/okstra_token_usage/cli.py +84 -0
- package/runtime/python/okstra_token_usage/codex.py +80 -0
- package/runtime/python/okstra_token_usage/collect.py +161 -0
- package/runtime/python/okstra_token_usage/gemini.py +77 -0
- package/runtime/python/okstra_token_usage/jsonl_io.py +18 -0
- package/runtime/python/okstra_token_usage/paths.py +22 -0
- package/runtime/python/okstra_token_usage/pricing.py +71 -0
- package/runtime/python/okstra_token_usage/report.py +64 -0
- package/runtime/templates/prd/brief.template.md +273 -0
- package/runtime/templates/project-docs/task-index.template.md +65 -0
- package/runtime/templates/reports/error-analysis-input.template.md +80 -0
- package/runtime/templates/reports/final-report.template.md +167 -0
- package/runtime/templates/reports/final-verification-input.template.md +67 -0
- package/runtime/templates/reports/implementation-input.template.md +81 -0
- package/runtime/templates/reports/implementation-planning-input.template.md +93 -0
- package/runtime/templates/reports/quick-input.template.md +64 -0
- package/runtime/templates/reports/schedule.template.md +168 -0
- package/runtime/templates/reports/settings.template.json +101 -0
- package/runtime/templates/reports/task-brief.template.md +165 -0
- package/runtime/validators/lib/common.sh +44 -0
- package/runtime/validators/lib/fixtures.sh +322 -0
- package/runtime/validators/lib/paths.sh +44 -0
- package/runtime/validators/lib/runners.sh +140 -0
- package/runtime/validators/lib/summary.sh +15 -0
- package/runtime/validators/lib/validate-assets.sh +44 -0
- package/runtime/validators/lib/validate-prompt-metadata.sh +267 -0
- package/runtime/validators/lib/validate-tasks.sh +335 -0
- package/runtime/validators/validate-run.py +568 -0
- package/runtime/validators/validate-schedule.py +665 -0
- package/runtime/validators/validate-workflow.sh +190 -0
- package/src/doctor.mjs +127 -0
- package/src/install.mjs +355 -0
- package/src/paths.mjs +132 -0
- package/src/uninstall.mjs +122 -0
- package/src/version.mjs +20 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SOURCE_PATH="${BASH_SOURCE[0]}"
|
|
6
|
+
while [[ -L "$SOURCE_PATH" ]]; do
|
|
7
|
+
SOURCE_DIR="$(cd -P "$(dirname "$SOURCE_PATH")" && pwd)"
|
|
8
|
+
SOURCE_PATH="$(readlink "$SOURCE_PATH")"
|
|
9
|
+
[[ "$SOURCE_PATH" != /* ]] && SOURCE_PATH="$SOURCE_DIR/$SOURCE_PATH"
|
|
10
|
+
done
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE_PATH")" && pwd)"
|
|
13
|
+
WORKSPACE_ROOT="$(cd -P "$SCRIPT_DIR/.." && pwd)"
|
|
14
|
+
PROJECT_ID="okstra-validation"
|
|
15
|
+
PROJECT_ROOT="${OKSTRA_VALIDATION_PROJECT_ROOT:-/tmp/okstra-validate.workflow}"
|
|
16
|
+
WORKSPACE_APP_PATH="$PROJECT_ROOT"
|
|
17
|
+
OKSTRA_SCRIPT="$WORKSPACE_ROOT/scripts/okstra.sh"
|
|
18
|
+
RUN_VALIDATOR_SCRIPT="$WORKSPACE_ROOT/validators/validate-run.py"
|
|
19
|
+
SOURCE_ASSET_ROOT="$WORKSPACE_ROOT/agents"
|
|
20
|
+
TASK_TYPE="final-verification"
|
|
21
|
+
MARKER="MANUAL-VALIDATION-MARKER"
|
|
22
|
+
PRIMARY_TASK_GROUP="validation"
|
|
23
|
+
PRIMARY_TASK_ID="asset-refresh-and-reference-expectations"
|
|
24
|
+
PRIMARY_BRIEF_FILENAME="validation-brief-primary.md"
|
|
25
|
+
SECONDARY_TASK_GROUP="discovery"
|
|
26
|
+
SECONDARY_TASK_ID="task-catalog"
|
|
27
|
+
SECONDARY_BRIEF_FILENAME="validation-brief-secondary.md"
|
|
28
|
+
LATEST_TASK_RELATIVE_PATH=".project-docs/okstra/discovery/latest-task.json"
|
|
29
|
+
TASK_CATALOG_RELATIVE_PATH=".project-docs/okstra/discovery/task-catalog.json"
|
|
30
|
+
|
|
31
|
+
# shellcheck source=lib/common.sh
|
|
32
|
+
source "$SCRIPT_DIR/lib/common.sh"
|
|
33
|
+
# shellcheck source=lib/paths.sh
|
|
34
|
+
source "$SCRIPT_DIR/lib/paths.sh"
|
|
35
|
+
# shellcheck source=lib/fixtures.sh
|
|
36
|
+
source "$SCRIPT_DIR/lib/fixtures.sh"
|
|
37
|
+
# shellcheck source=lib/runners.sh
|
|
38
|
+
source "$SCRIPT_DIR/lib/runners.sh"
|
|
39
|
+
# shellcheck source=lib/validate-assets.sh
|
|
40
|
+
source "$SCRIPT_DIR/lib/validate-assets.sh"
|
|
41
|
+
# shellcheck source=lib/validate-prompt-metadata.sh
|
|
42
|
+
source "$SCRIPT_DIR/lib/validate-prompt-metadata.sh"
|
|
43
|
+
# shellcheck source=lib/validate-tasks.sh
|
|
44
|
+
source "$SCRIPT_DIR/lib/validate-tasks.sh"
|
|
45
|
+
# shellcheck source=lib/summary.sh
|
|
46
|
+
source "$SCRIPT_DIR/lib/summary.sh"
|
|
47
|
+
|
|
48
|
+
trap 'on_error "$LINENO"' ERR
|
|
49
|
+
|
|
50
|
+
require_file "$OKSTRA_SCRIPT"
|
|
51
|
+
require_file "$RUN_VALIDATOR_SCRIPT"
|
|
52
|
+
|
|
53
|
+
validate_project_root_safety
|
|
54
|
+
|
|
55
|
+
PRIMARY_BRIEF_PATH="$PROJECT_ROOT/$PRIMARY_BRIEF_FILENAME"
|
|
56
|
+
SECONDARY_BRIEF_PATH="$PROJECT_ROOT/$SECONDARY_BRIEF_FILENAME"
|
|
57
|
+
DISCOVERY_FILE="$PROJECT_ROOT/$LATEST_TASK_RELATIVE_PATH"
|
|
58
|
+
CATALOG_FILE="$PROJECT_ROOT/$TASK_CATALOG_RELATIVE_PATH"
|
|
59
|
+
MARKER_FILE="$PROJECT_ROOT/.claude/agents/codex-worker.md"
|
|
60
|
+
PRIMARY_TASK_KEY="$(task_key "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID")"
|
|
61
|
+
SECONDARY_TASK_KEY="$(task_key "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID")"
|
|
62
|
+
PRIMARY_REFERENCE_EXPECTATIONS_FILE="$(task_root "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID")/instruction-set/reference-expectations.md"
|
|
63
|
+
SECONDARY_REFERENCE_EXPECTATIONS_FILE="$(task_root "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID")/instruction-set/reference-expectations.md"
|
|
64
|
+
|
|
65
|
+
step "Resetting the validation root"
|
|
66
|
+
reset_validation_root
|
|
67
|
+
pass "Validation root reset: $PROJECT_ROOT"
|
|
68
|
+
|
|
69
|
+
step "Writing validation briefs for multiple tasks"
|
|
70
|
+
write_validation_brief \
|
|
71
|
+
"$PRIMARY_BRIEF_PATH" \
|
|
72
|
+
"Primary validation brief for reference expectations" \
|
|
73
|
+
"$PRIMARY_TASK_GROUP" \
|
|
74
|
+
"$PRIMARY_TASK_ID" \
|
|
75
|
+
"verify the initial task bundle and discovery pointer"
|
|
76
|
+
write_validation_brief \
|
|
77
|
+
"$SECONDARY_BRIEF_PATH" \
|
|
78
|
+
"Secondary validation brief for discovery catalog" \
|
|
79
|
+
"$SECONDARY_TASK_GROUP" \
|
|
80
|
+
"$SECONDARY_TASK_ID" \
|
|
81
|
+
"verify task catalog retention across multiple prepared tasks"
|
|
82
|
+
require_file "$PRIMARY_BRIEF_PATH"
|
|
83
|
+
require_file "$SECONDARY_BRIEF_PATH"
|
|
84
|
+
pass "Validation briefs created for primary and secondary tasks"
|
|
85
|
+
|
|
86
|
+
step "Running the initial render-only okstra validation for the primary task"
|
|
87
|
+
if ! run_okstra "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID" "$PRIMARY_BRIEF_FILENAME"; then
|
|
88
|
+
fail "Initial okstra render-only validation for the primary task failed"
|
|
89
|
+
fi
|
|
90
|
+
pass "Primary task render-only validation completed"
|
|
91
|
+
|
|
92
|
+
step "Checking seeded assets and primary task discovery artifacts"
|
|
93
|
+
if ! validate_seeded_assets match; then
|
|
94
|
+
fail "Seeded project-local okstra assets are missing or do not match the source files"
|
|
95
|
+
fi
|
|
96
|
+
if ! validate_reference_expectations "$PRIMARY_BRIEF_PATH" "$PRIMARY_REFERENCE_EXPECTATIONS_FILE" "$PRIMARY_TASK_KEY"; then
|
|
97
|
+
fail "Primary reference-expectations.md does not preserve the expected brief content"
|
|
98
|
+
fi
|
|
99
|
+
if ! validate_task_artifacts "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID"; then
|
|
100
|
+
fail "Primary task manifest or run manifest validation failed"
|
|
101
|
+
fi
|
|
102
|
+
if ! validate_worker_prompt_metadata "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID"; then
|
|
103
|
+
fail "Primary worker prompt metadata validation failed"
|
|
104
|
+
fi
|
|
105
|
+
if ! validate_latest_task_pointer "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID"; then
|
|
106
|
+
fail "latest-task.json did not point to the primary task after the first run"
|
|
107
|
+
fi
|
|
108
|
+
if ! validate_task_catalog "$PRIMARY_TASK_KEY" "$PRIMARY_TASK_KEY"; then
|
|
109
|
+
fail "task-catalog.json did not preserve the expected primary task entry"
|
|
110
|
+
fi
|
|
111
|
+
pass "Primary task discovery artifacts are valid"
|
|
112
|
+
|
|
113
|
+
step "Preparing validator fixture artifacts for the primary task"
|
|
114
|
+
if ! prepare_run_validator_fixture "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID" codex; then
|
|
115
|
+
fail "Failed to prepare validator fixture artifacts for the primary task"
|
|
116
|
+
fi
|
|
117
|
+
pass "Primary task validator fixture prepared with an intentionally missing Codex prompt history file"
|
|
118
|
+
|
|
119
|
+
step "Ensuring the run validator rejects attempted workers without prompt history"
|
|
120
|
+
if ! run_validator_expectation "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID" failed "Codex worker with status \`timeout\` is missing worker prompt history file"; then
|
|
121
|
+
fail "Run validator did not reject the missing worker prompt history artifact"
|
|
122
|
+
fi
|
|
123
|
+
pass "Run validator rejected the missing worker prompt history artifact as expected"
|
|
124
|
+
|
|
125
|
+
step "Restoring the missing worker prompt history file and rerunning the validator"
|
|
126
|
+
if ! write_worker_prompt_history_fixture "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID" codex; then
|
|
127
|
+
fail "Failed to write the missing worker prompt history fixture"
|
|
128
|
+
fi
|
|
129
|
+
if ! run_validator_expectation "$PRIMARY_TASK_GROUP" "$PRIMARY_TASK_ID" passed; then
|
|
130
|
+
fail "Run validator did not pass after restoring the worker prompt history artifact"
|
|
131
|
+
fi
|
|
132
|
+
pass "Run validator passed after the missing worker prompt history artifact was restored"
|
|
133
|
+
|
|
134
|
+
step "Running a second render-only okstra validation for the secondary task"
|
|
135
|
+
if ! run_okstra "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID" "$SECONDARY_BRIEF_FILENAME"; then
|
|
136
|
+
fail "Render-only validation for the secondary task failed"
|
|
137
|
+
fi
|
|
138
|
+
pass "Secondary task render-only validation completed"
|
|
139
|
+
|
|
140
|
+
step "Checking that latest-task.json moves while task-catalog.json retains both tasks"
|
|
141
|
+
if ! validate_reference_expectations "$SECONDARY_BRIEF_PATH" "$SECONDARY_REFERENCE_EXPECTATIONS_FILE" "$SECONDARY_TASK_KEY"; then
|
|
142
|
+
fail "Secondary reference-expectations.md does not preserve the expected brief content"
|
|
143
|
+
fi
|
|
144
|
+
if ! validate_task_artifacts "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID"; then
|
|
145
|
+
fail "Secondary task manifest or run manifest validation failed"
|
|
146
|
+
fi
|
|
147
|
+
if ! validate_worker_prompt_metadata "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID"; then
|
|
148
|
+
fail "Secondary worker prompt metadata validation failed"
|
|
149
|
+
fi
|
|
150
|
+
if ! validate_latest_task_pointer "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID"; then
|
|
151
|
+
fail "latest-task.json did not point to the secondary task after the second run"
|
|
152
|
+
fi
|
|
153
|
+
if ! validate_task_catalog "$SECONDARY_TASK_KEY" "$PRIMARY_TASK_KEY" "$SECONDARY_TASK_KEY"; then
|
|
154
|
+
fail "task-catalog.json did not preserve both prepared tasks"
|
|
155
|
+
fi
|
|
156
|
+
pass "latest-task.json and task-catalog.json now reflect distinct primary and secondary tasks"
|
|
157
|
+
|
|
158
|
+
step "Verifying that rerun without refresh preserves project-local assets"
|
|
159
|
+
require_file "$MARKER_FILE"
|
|
160
|
+
printf '\n%s\n' "$MARKER" >>"$MARKER_FILE"
|
|
161
|
+
assert_contains "$MARKER_FILE" "$MARKER"
|
|
162
|
+
if ! run_okstra "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID" "$SECONDARY_BRIEF_FILENAME"; then
|
|
163
|
+
fail "Secondary task rerun without refresh failed"
|
|
164
|
+
fi
|
|
165
|
+
assert_contains "$MARKER_FILE" "$MARKER"
|
|
166
|
+
if ! validate_latest_task_pointer "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID"; then
|
|
167
|
+
fail "latest-task.json changed unexpectedly during secondary rerun without refresh"
|
|
168
|
+
fi
|
|
169
|
+
if ! validate_task_catalog "$SECONDARY_TASK_KEY" "$PRIMARY_TASK_KEY" "$SECONDARY_TASK_KEY"; then
|
|
170
|
+
fail "task-catalog.json changed unexpectedly during secondary rerun without refresh"
|
|
171
|
+
fi
|
|
172
|
+
pass "Rerun without refresh preserved the modified project-local asset and retained both catalog entries"
|
|
173
|
+
|
|
174
|
+
step "Verifying that rerun with refresh regenerates project-local assets"
|
|
175
|
+
if ! run_okstra "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID" "$SECONDARY_BRIEF_FILENAME" --refresh-assets; then
|
|
176
|
+
fail "Secondary task rerun with --refresh-assets failed"
|
|
177
|
+
fi
|
|
178
|
+
assert_not_contains "$MARKER_FILE" "$MARKER"
|
|
179
|
+
if ! validate_seeded_assets match; then
|
|
180
|
+
fail "Refreshed project-local okstra assets do not match the source files"
|
|
181
|
+
fi
|
|
182
|
+
if ! validate_latest_task_pointer "$SECONDARY_TASK_GROUP" "$SECONDARY_TASK_ID"; then
|
|
183
|
+
fail "latest-task.json became invalid after refresh"
|
|
184
|
+
fi
|
|
185
|
+
if ! validate_task_catalog "$SECONDARY_TASK_KEY" "$PRIMARY_TASK_KEY" "$SECONDARY_TASK_KEY"; then
|
|
186
|
+
fail "task-catalog.json became invalid after refresh"
|
|
187
|
+
fi
|
|
188
|
+
pass "Refresh regenerated the mapped project-local okstra assets while preserving both catalog entries"
|
|
189
|
+
|
|
190
|
+
print_summary
|
package/src/doctor.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolvePaths } from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
const USAGE = `okstra doctor — diagnose the installed runtime
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
okstra doctor Run all checks and print a summary
|
|
10
|
+
okstra doctor --json Machine-readable result
|
|
11
|
+
|
|
12
|
+
Checks:
|
|
13
|
+
- python3 binary and version (>= 3.10)
|
|
14
|
+
- okstra_project module importable from $HOME/.okstra/lib/python
|
|
15
|
+
- okstra_ctl module importable
|
|
16
|
+
- bash entrypoints present and executable in $HOME/.okstra/bin
|
|
17
|
+
- agents/ directory exists inside the okstra package
|
|
18
|
+
- version stamp matches package version
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
async function pathExists(p) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(p);
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function runProcess(cmd, args, opts = {}) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], ...opts });
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
36
|
+
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
37
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
38
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function checkPython3() {
|
|
43
|
+
const r = await runProcess("python3", ["--version"]);
|
|
44
|
+
if (r.code !== 0) return { ok: false, detail: `python3 not found: ${r.stderr.trim() || "missing binary"}` };
|
|
45
|
+
const out = (r.stdout + r.stderr).trim();
|
|
46
|
+
const m = out.match(/Python\s+(\d+)\.(\d+)/);
|
|
47
|
+
if (!m) return { ok: false, detail: `unparsable python version: ${out}` };
|
|
48
|
+
const [_, maj, min] = m.map(Number);
|
|
49
|
+
if (maj < 3 || (maj === 3 && min < 10)) {
|
|
50
|
+
return { ok: false, detail: `python ${maj}.${min} is too old (need 3.10+)` };
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, detail: out };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function checkPythonImport(moduleName, pythonpath) {
|
|
56
|
+
const r = await runProcess("python3", ["-c", `import ${moduleName}; print(${moduleName}.__name__)`], {
|
|
57
|
+
env: { ...process.env, PYTHONPATH: pythonpath },
|
|
58
|
+
});
|
|
59
|
+
if (r.code !== 0) {
|
|
60
|
+
return { ok: false, detail: `import ${moduleName} failed: ${r.stderr.trim().split("\n").pop()}` };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, detail: `${moduleName} importable` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function checkBashEntry(binDir, name) {
|
|
66
|
+
const p = join(binDir, name);
|
|
67
|
+
if (!(await pathExists(p))) return { ok: false, detail: `missing ${p}` };
|
|
68
|
+
try {
|
|
69
|
+
const st = await fs.stat(p);
|
|
70
|
+
if (!(st.mode & 0o111)) return { ok: false, detail: `${name} not executable` };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { ok: false, detail: `stat ${name} failed: ${err.message}` };
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, detail: `${name} OK` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function check(name, fn) {
|
|
78
|
+
try {
|
|
79
|
+
const result = await fn();
|
|
80
|
+
return { name, ...result };
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return { name, ok: false, detail: `unexpected error: ${err.message}` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function run(args) {
|
|
87
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
88
|
+
process.stdout.write(USAGE);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const jsonMode = args.includes("--json");
|
|
92
|
+
const paths = await resolvePaths();
|
|
93
|
+
|
|
94
|
+
const results = [
|
|
95
|
+
await check("python3", checkPython3),
|
|
96
|
+
await check("okstra_project import", () => checkPythonImport("okstra_project", paths.pythonpath)),
|
|
97
|
+
await check("okstra_ctl import", () => checkPythonImport("okstra_ctl", paths.pythonpath)),
|
|
98
|
+
await check("agents dir", async () =>
|
|
99
|
+
(await pathExists(paths.agents))
|
|
100
|
+
? { ok: true, detail: paths.agents }
|
|
101
|
+
: { ok: false, detail: `not found: ${paths.agents}` },
|
|
102
|
+
),
|
|
103
|
+
await check("bin: okstra.sh", () => checkBashEntry(paths.bin, "okstra.sh")),
|
|
104
|
+
await check("bin: okstra-codex-exec.sh", () => checkBashEntry(paths.bin, "okstra-codex-exec.sh")),
|
|
105
|
+
await check("bin: okstra-gemini-exec.sh", () => checkBashEntry(paths.bin, "okstra-gemini-exec.sh")),
|
|
106
|
+
await check("bin: okstra-central.sh", () => checkBashEntry(paths.bin, "okstra-central.sh")),
|
|
107
|
+
await check("version stamp", async () =>
|
|
108
|
+
paths.version === paths.package
|
|
109
|
+
? { ok: true, detail: paths.version }
|
|
110
|
+
: { ok: false, detail: `stamp=${paths.version || "(none)"} package=${paths.package}` },
|
|
111
|
+
),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const allOk = results.every((r) => r.ok);
|
|
115
|
+
|
|
116
|
+
if (jsonMode) {
|
|
117
|
+
process.stdout.write(`${JSON.stringify({ ok: allOk, paths, checks: results }, null, 2)}\n`);
|
|
118
|
+
return allOk ? 0 : 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
const mark = r.ok ? "OK " : "FAIL";
|
|
123
|
+
process.stdout.write(` [${mark}] ${r.name}: ${r.detail}\n`);
|
|
124
|
+
}
|
|
125
|
+
process.stdout.write(`\nresult: ${allOk ? "OK" : "FAIL"}\n`);
|
|
126
|
+
return allOk ? 0 : 1;
|
|
127
|
+
}
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { join, relative, resolve as resolveAbs } from "node:path";
|
|
4
|
+
import { getPackageRoot } from "./version.mjs";
|
|
5
|
+
import { resolvePaths } from "./paths.mjs";
|
|
6
|
+
|
|
7
|
+
const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "lib"];
|
|
8
|
+
const BIN_ENTRYPOINTS = [
|
|
9
|
+
"okstra.sh",
|
|
10
|
+
"okstra-codex-exec.sh",
|
|
11
|
+
"okstra-gemini-exec.sh",
|
|
12
|
+
"okstra-central.sh",
|
|
13
|
+
"okstra-token-usage.py",
|
|
14
|
+
"okstra-error-log.py",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const INSTALL_USAGE = `okstra install — install runtime into ~/.okstra
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
okstra install Install/update runtime (copy mode)
|
|
21
|
+
okstra install --link <p> Dev only: symlink ~/.okstra/{lib/python,bin}
|
|
22
|
+
to the source repo at <p>. agents/ resolves
|
|
23
|
+
to <p>/agents. Not advertised to end users.
|
|
24
|
+
okstra install --dry-run Print the plan without touching disk
|
|
25
|
+
okstra install --refresh Re-copy even files that match by hash
|
|
26
|
+
|
|
27
|
+
Effect (copy mode):
|
|
28
|
+
${"$HOME"}/.okstra/lib/python <- packages/okstra/runtime/python
|
|
29
|
+
${"$HOME"}/.okstra/bin <- packages/okstra/runtime/bin
|
|
30
|
+
${"$HOME"}/.okstra/version <- installed package version stamp
|
|
31
|
+
|
|
32
|
+
Effect (link mode):
|
|
33
|
+
${"$HOME"}/.okstra/lib/python/<pkg> -> <repo>/scripts/<pkg> (symlink)
|
|
34
|
+
${"$HOME"}/.okstra/bin/<name>.sh -> <repo>/scripts/<name>.sh
|
|
35
|
+
${"$HOME"}/.okstra/dev-link <- <repo> path stamp
|
|
36
|
+
${"$HOME"}/.okstra/version <- installed package version stamp
|
|
37
|
+
|
|
38
|
+
agents/ is NOT copied — it stays inside the package (copy mode) or is
|
|
39
|
+
resolved to <repo>/agents (link mode) via 'okstra paths --field agents'.
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const ENSURE_USAGE = `okstra ensure-installed — idempotent install check
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
okstra ensure-installed Verify install; reinstall if version/files drift
|
|
46
|
+
okstra ensure-installed -q Same, but suppress success output
|
|
47
|
+
|
|
48
|
+
Skills call this on every run. Returns 0 quickly when the install is fresh.
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
async function hashFile(path) {
|
|
52
|
+
const buf = await fs.readFile(path);
|
|
53
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function dirExists(path) {
|
|
57
|
+
try {
|
|
58
|
+
const st = await fs.stat(path);
|
|
59
|
+
return st.isDirectory();
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function* walkFiles(root) {
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = await fs.readdir(root, { withFileTypes: true });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err.code === "ENOENT") return;
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
for (const ent of entries) {
|
|
74
|
+
if (ent.name === "__pycache__" || ent.name === ".DS_Store") continue;
|
|
75
|
+
if (ent.name.endsWith(".pyc")) continue;
|
|
76
|
+
const abs = join(root, ent.name);
|
|
77
|
+
if (ent.isDirectory()) {
|
|
78
|
+
yield* walkFiles(abs);
|
|
79
|
+
} else if (ent.isFile()) {
|
|
80
|
+
yield abs;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function writeFileAtomic(target, data, mode) {
|
|
86
|
+
await fs.mkdir(join(target, ".."), { recursive: true });
|
|
87
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
88
|
+
await fs.writeFile(tmp, data, { mode });
|
|
89
|
+
await fs.rename(tmp, target);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function copyTreeIfChanged(srcRoot, dstRoot, opts) {
|
|
93
|
+
const { refresh = false, dryRun = false, mode } = opts ?? {};
|
|
94
|
+
let copied = 0;
|
|
95
|
+
let skipped = 0;
|
|
96
|
+
let missingSource = false;
|
|
97
|
+
|
|
98
|
+
if (!(await dirExists(srcRoot))) {
|
|
99
|
+
missingSource = true;
|
|
100
|
+
return { copied, skipped, missingSource };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for await (const srcPath of walkFiles(srcRoot)) {
|
|
104
|
+
const rel = relative(srcRoot, srcPath);
|
|
105
|
+
const dstPath = join(dstRoot, rel);
|
|
106
|
+
|
|
107
|
+
let needsCopy = refresh;
|
|
108
|
+
if (!needsCopy) {
|
|
109
|
+
try {
|
|
110
|
+
await fs.access(dstPath);
|
|
111
|
+
const [srcHash, dstHash] = await Promise.all([hashFile(srcPath), hashFile(dstPath)]);
|
|
112
|
+
needsCopy = srcHash !== dstHash;
|
|
113
|
+
} catch {
|
|
114
|
+
needsCopy = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!needsCopy) {
|
|
119
|
+
skipped++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (dryRun) {
|
|
124
|
+
process.stdout.write(`[dry-run] copy ${srcPath} -> ${dstPath}\n`);
|
|
125
|
+
} else {
|
|
126
|
+
const buf = await fs.readFile(srcPath);
|
|
127
|
+
await writeFileAtomic(dstPath, buf, mode);
|
|
128
|
+
}
|
|
129
|
+
copied++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { copied, skipped, missingSource };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function ensureSymlink(target, linkPath, opts) {
|
|
136
|
+
const { dryRun = false } = opts ?? {};
|
|
137
|
+
try {
|
|
138
|
+
const current = await fs.readlink(linkPath);
|
|
139
|
+
if (current === target) return "skipped";
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err.code !== "ENOENT" && err.code !== "EINVAL") throw err;
|
|
142
|
+
}
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
process.stdout.write(`[dry-run] symlink ${linkPath} -> ${target}\n`);
|
|
145
|
+
return "linked";
|
|
146
|
+
}
|
|
147
|
+
await fs.mkdir(join(linkPath, ".."), { recursive: true });
|
|
148
|
+
try {
|
|
149
|
+
await fs.unlink(linkPath);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err.code !== "ENOENT") {
|
|
152
|
+
// Existing directory or regular file — refuse to clobber automatically.
|
|
153
|
+
throw new Error(
|
|
154
|
+
`refuse to replace ${linkPath} (not a symlink). Remove it manually and retry, or run 'okstra uninstall' first.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await fs.symlink(target, linkPath);
|
|
159
|
+
return "linked";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function installLinkMode(repoPath, paths, opts) {
|
|
163
|
+
const { dryRun, quiet } = opts;
|
|
164
|
+
const repoAbs = resolveAbs(repoPath);
|
|
165
|
+
|
|
166
|
+
if (!(await dirExists(repoAbs))) {
|
|
167
|
+
throw new Error(`repo path not found: ${repoAbs}`);
|
|
168
|
+
}
|
|
169
|
+
const scriptsDir = join(repoAbs, "scripts");
|
|
170
|
+
if (!(await dirExists(scriptsDir))) {
|
|
171
|
+
throw new Error(`<repo>/scripts not found: ${scriptsDir}`);
|
|
172
|
+
}
|
|
173
|
+
const agentsDir = join(repoAbs, "agents");
|
|
174
|
+
if (!(await dirExists(agentsDir))) {
|
|
175
|
+
throw new Error(`<repo>/agents not found: ${agentsDir}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!quiet) {
|
|
179
|
+
process.stdout.write(`installing okstra runtime in LINK mode (package ${paths.package})\n`);
|
|
180
|
+
process.stdout.write(` repo: ${repoAbs}\n`);
|
|
181
|
+
process.stdout.write(` home: ${paths.home}\n`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!dryRun) {
|
|
185
|
+
await fs.mkdir(paths.pythonpath, { recursive: true });
|
|
186
|
+
await fs.mkdir(paths.bin, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const pkg of PYTHON_PACKAGES) {
|
|
190
|
+
const src = join(scriptsDir, pkg);
|
|
191
|
+
const dst = join(paths.pythonpath, pkg);
|
|
192
|
+
if (!(await dirExists(src))) {
|
|
193
|
+
if (!quiet) process.stdout.write(` python/${pkg}: missing in repo — skipped\n`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const action = await ensureSymlink(src, dst, { dryRun });
|
|
197
|
+
if (!quiet) process.stdout.write(` python/${pkg}: ${action}\n`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const name of BIN_ENTRYPOINTS) {
|
|
201
|
+
const src = join(scriptsDir, name);
|
|
202
|
+
const dst = join(paths.bin, name);
|
|
203
|
+
if (!(await fileExists(src))) {
|
|
204
|
+
if (!quiet) process.stdout.write(` bin/${name}: missing in repo — skipped\n`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const action = await ensureSymlink(src, dst, { dryRun });
|
|
208
|
+
if (!quiet) process.stdout.write(` bin/${name}: ${action}\n`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!dryRun) {
|
|
212
|
+
await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
|
|
213
|
+
await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
|
|
214
|
+
}
|
|
215
|
+
if (!quiet) {
|
|
216
|
+
process.stdout.write(` dev-link stamp: ${repoAbs}\n`);
|
|
217
|
+
process.stdout.write(` version stamp: ${paths.package}\n`);
|
|
218
|
+
process.stdout.write("done.\n");
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function fileExists(p) {
|
|
224
|
+
try {
|
|
225
|
+
await fs.access(p);
|
|
226
|
+
return true;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseInstallArgs(args) {
|
|
233
|
+
const result = {
|
|
234
|
+
dryRun: false,
|
|
235
|
+
refresh: false,
|
|
236
|
+
quiet: false,
|
|
237
|
+
linkRepo: null,
|
|
238
|
+
};
|
|
239
|
+
for (let i = 0; i < args.length; i++) {
|
|
240
|
+
const a = args[i];
|
|
241
|
+
if (a === "--dry-run") result.dryRun = true;
|
|
242
|
+
else if (a === "--refresh") result.refresh = true;
|
|
243
|
+
else if (a === "-q" || a === "--quiet") result.quiet = true;
|
|
244
|
+
else if (a === "--link") {
|
|
245
|
+
const next = args[i + 1];
|
|
246
|
+
if (!next || next.startsWith("--")) {
|
|
247
|
+
throw new Error("--link requires a repo path");
|
|
248
|
+
}
|
|
249
|
+
result.linkRepo = next;
|
|
250
|
+
i++;
|
|
251
|
+
} else {
|
|
252
|
+
throw new Error(`unknown argument '${a}'`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function runInstall(args) {
|
|
259
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
260
|
+
process.stdout.write(INSTALL_USAGE);
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const opts = parseInstallArgs(args);
|
|
265
|
+
const pkgRoot = getPackageRoot();
|
|
266
|
+
const runtimeRoot = join(pkgRoot, "runtime");
|
|
267
|
+
const paths = await resolvePaths();
|
|
268
|
+
|
|
269
|
+
if (!opts.dryRun) {
|
|
270
|
+
await fs.mkdir(paths.home, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (opts.linkRepo) {
|
|
274
|
+
return await installLinkMode(opts.linkRepo, paths, opts);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// copy mode — if a prior dev-link is present, refuse rather than silently overwriting.
|
|
278
|
+
if (paths["dev-link"]) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`~/.okstra/dev-link is set to ${paths["dev-link"]}. Run 'okstra uninstall' first, then 'okstra install' in copy mode.`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!opts.quiet) {
|
|
285
|
+
process.stdout.write(`installing okstra runtime (package ${paths.package})\n`);
|
|
286
|
+
process.stdout.write(` source: ${runtimeRoot}\n`);
|
|
287
|
+
process.stdout.write(` home: ${paths.home}\n`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const pythonResult = await copyTreeIfChanged(
|
|
291
|
+
join(runtimeRoot, "python"),
|
|
292
|
+
paths.pythonpath,
|
|
293
|
+
{ refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o644 },
|
|
294
|
+
);
|
|
295
|
+
const binResult = await copyTreeIfChanged(
|
|
296
|
+
join(runtimeRoot, "bin"),
|
|
297
|
+
paths.bin,
|
|
298
|
+
{ refresh: opts.refresh, dryRun: opts.dryRun, mode: 0o755 },
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (!opts.quiet) {
|
|
302
|
+
summarise("python", pythonResult, paths.pythonpath);
|
|
303
|
+
summarise("bin", binResult, paths.bin);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (pythonResult.missingSource && binResult.missingSource) {
|
|
307
|
+
process.stderr.write(
|
|
308
|
+
"warning: runtime/{python,bin} are both empty. Runtime sync (build step) has not been performed.\n",
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!opts.dryRun) {
|
|
313
|
+
await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
|
|
314
|
+
}
|
|
315
|
+
if (!opts.quiet) {
|
|
316
|
+
process.stdout.write(` version stamp: ${paths.package}\n`);
|
|
317
|
+
process.stdout.write("done.\n");
|
|
318
|
+
}
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function summarise(label, result, target) {
|
|
323
|
+
if (result.missingSource) {
|
|
324
|
+
process.stdout.write(` ${label}: source directory missing — skipped\n`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
process.stdout.write(
|
|
328
|
+
` ${label}: copied=${result.copied} skipped=${result.skipped} -> ${target}\n`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function runEnsureInstalled(args) {
|
|
333
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
334
|
+
process.stdout.write(ENSURE_USAGE);
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
const quiet = args.includes("-q") || args.includes("--quiet");
|
|
338
|
+
const paths = await resolvePaths();
|
|
339
|
+
|
|
340
|
+
const reasons = [];
|
|
341
|
+
if (!paths.version) reasons.push("no version stamp");
|
|
342
|
+
else if (paths.version !== paths.package) {
|
|
343
|
+
reasons.push(`version drift (stamp=${paths.version}, package=${paths.package})`);
|
|
344
|
+
}
|
|
345
|
+
if (!(await dirExists(paths.pythonpath))) reasons.push(`missing ${paths.pythonpath}`);
|
|
346
|
+
if (!(await dirExists(paths.agents))) reasons.push(`missing agents dir ${paths.agents}`);
|
|
347
|
+
|
|
348
|
+
if (reasons.length === 0) {
|
|
349
|
+
if (!quiet) process.stdout.write(`okstra runtime OK (package ${paths.package})\n`);
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
process.stderr.write(`okstra runtime needs install: ${reasons.join("; ")}\n`);
|
|
354
|
+
return await runInstall(quiet ? ["-q"] : []);
|
|
355
|
+
}
|