okstra 0.96.1 → 0.97.1
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.kr.md +1 -1
- package/README.md +1 -1
- package/bin/okstra +1 -1
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/kr/architecture/storage-model.md +273 -0
- package/docs/kr/architecture.md +6 -277
- package/docs/kr/cli.md +1 -1
- package/docs/project-structure-overview.md +26 -22
- package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +1019 -0
- package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +583 -0
- package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +819 -0
- package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +348 -0
- package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +2 -2
- package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +171 -0
- package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +112 -0
- package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +145 -0
- package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +113 -0
- package/docs/task-process/release-handoff.md +3 -3
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/cli.sh +1 -1
- package/runtime/bin/lib/okstra/interactive.sh +30 -4
- package/runtime/bin/lib/okstra-ctl/cmd-tail.sh +18 -5
- package/runtime/bin/okstra-render-final-report.py +4 -15
- package/runtime/bin/okstra.sh +5 -2
- package/runtime/prompts/launch.template.md +1 -0
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/lead/team-contract.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -0
- package/runtime/prompts/profiles/forbidden-actions.json +1 -1
- package/runtime/prompts/profiles/release-handoff.md +10 -4
- package/runtime/python/okstra_ctl/__init__.py +3 -2
- package/runtime/python/okstra_ctl/backfill.py +12 -21
- package/runtime/python/okstra_ctl/codex_dispatch.py +7 -2
- package/runtime/python/okstra_ctl/conformance.py +21 -3
- package/runtime/python/okstra_ctl/consumers.py +7 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +10 -2
- package/runtime/python/okstra_ctl/doctor.py +29 -12
- package/runtime/python/okstra_ctl/error_log_core.py +72 -0
- package/runtime/python/okstra_ctl/error_report.py +8 -58
- package/runtime/python/okstra_ctl/error_zip.py +319 -0
- package/runtime/python/okstra_ctl/fanout.py +5 -2
- package/runtime/python/okstra_ctl/fix_cycles.py +14 -2
- package/runtime/python/okstra_ctl/git_reconcile.py +7 -1
- package/runtime/python/okstra_ctl/handoff.py +125 -26
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -2
- package/runtime/python/okstra_ctl/improvement_lenses.py +2 -0
- package/runtime/python/okstra_ctl/index.py +34 -91
- package/runtime/python/okstra_ctl/jsonl.py +29 -18
- package/runtime/python/okstra_ctl/listing.py +30 -5
- package/runtime/python/okstra_ctl/md_table.py +1 -1
- package/runtime/python/okstra_ctl/migrate.py +17 -7
- package/runtime/python/okstra_ctl/paths.py +1 -1
- package/runtime/python/okstra_ctl/recap.py +12 -3
- package/runtime/python/okstra_ctl/reconcile.py +13 -11
- package/runtime/python/okstra_ctl/render.py +49 -12
- package/runtime/python/okstra_ctl/render_final_report.py +4 -0
- package/runtime/python/okstra_ctl/report_views.py +47 -6
- package/runtime/python/okstra_ctl/run.py +128 -100
- package/runtime/python/okstra_ctl/run_index_row.py +118 -0
- package/runtime/python/okstra_ctl/schema_excerpt.py +8 -2
- package/runtime/python/okstra_ctl/sequence.py +2 -2
- package/runtime/python/okstra_ctl/stage_integrate.py +193 -0
- package/runtime/python/okstra_ctl/stage_targets.py +2 -8
- package/runtime/python/okstra_ctl/task_target.py +4 -1
- package/runtime/python/okstra_ctl/team_reconcile.py +6 -1
- package/runtime/python/okstra_ctl/wizard.py +27 -12
- package/runtime/python/okstra_ctl/workflow.py +9 -1
- package/runtime/python/okstra_ctl/worktree.py +93 -4
- package/runtime/python/okstra_ctl/worktree_registry.py +57 -8
- package/runtime/python/okstra_token_usage/collect.py +43 -38
- package/runtime/skills/okstra-brief/SKILL.md +43 -12
- package/runtime/skills/okstra-inspect/SKILL.md +44 -1
- package/runtime/templates/reports/final-report.template.md +2 -2
- package/runtime/templates/reports/release-handoff-input.template.md +1 -1
- package/runtime/validators/forbidden_actions.py +8 -1
- package/runtime/validators/validate-brief.py +13 -2
- package/runtime/validators/validate-run.py +27 -6
- package/runtime/validators/validate-schedule.py +1 -2
- package/runtime/validators/validate_improvement_report.py +14 -0
- package/src/cli-registry.mjs +45 -31
- package/src/{codex-dispatch.mjs → commands/execute/codex-dispatch.mjs} +3 -3
- package/src/{codex-run.mjs → commands/execute/codex-run.mjs} +3 -3
- package/src/{error-log.mjs → commands/execute/error-log.mjs} +1 -1
- package/src/{git-reconcile.mjs → commands/execute/git-reconcile.mjs} +1 -1
- package/src/{handoff.mjs → commands/execute/handoff.mjs} +3 -1
- package/src/commands/execute/integrate-stages.mjs +25 -0
- package/src/{plan-validate.mjs → commands/execute/plan-validate.mjs} +1 -1
- package/src/{render-bundle.mjs → commands/execute/render-bundle.mjs} +4 -4
- package/src/{run.mjs → commands/execute/run.mjs} +4 -4
- package/src/{spawn-followups.mjs → commands/execute/spawn-followups.mjs} +1 -1
- package/src/{team.mjs → commands/execute/team.mjs} +3 -3
- package/src/{token-usage.mjs → commands/execute/token-usage.mjs} +1 -1
- package/src/{wizard.mjs → commands/execute/wizard.mjs} +15 -6
- package/src/{worktree-lookup.mjs → commands/execute/worktree-lookup.mjs} +1 -1
- package/src/{context-cost.mjs → commands/inspect/context-cost.mjs} +2 -2
- package/src/{error-report.mjs → commands/inspect/error-report.mjs} +2 -2
- package/src/commands/inspect/error-zip.mjs +25 -0
- package/src/{recap.mjs → commands/inspect/recap.mjs} +2 -2
- package/src/{task-list.mjs → commands/inspect/task-list.mjs} +1 -1
- package/src/{task-show.mjs → commands/inspect/task-show.mjs} +1 -1
- package/src/{check-project.mjs → commands/lifecycle/check-project.mjs} +2 -2
- package/src/{config.mjs → commands/lifecycle/config.mjs} +3 -3
- package/src/{doctor.mjs → commands/lifecycle/doctor.mjs} +4 -4
- package/src/{install.mjs → commands/lifecycle/install.mjs} +18 -13
- package/src/{migrate.mjs → commands/lifecycle/migrate.mjs} +1 -1
- package/src/{paths.mjs → commands/lifecycle/paths.mjs} +1 -60
- package/src/{setup.mjs → commands/lifecycle/setup.mjs} +4 -4
- package/src/{uninstall.mjs → commands/lifecycle/uninstall.mjs} +20 -32
- package/src/{memory.mjs → commands/memory/memory.mjs} +32 -7
- package/src/{inject-report-index.mjs → commands/report/inject-report-index.mjs} +1 -1
- package/src/{render-final-report.mjs → commands/report/render-final-report.mjs} +1 -1
- package/src/{render-views.mjs → commands/report/render-views.mjs} +1 -1
- package/src/lib/paths.mjs +60 -0
- package/src/{_python-helper.mjs → lib/python-helper.mjs} +35 -13
- package/src/{version.mjs → lib/version.mjs} +2 -2
- /package/src/{okstra-dirs.mjs → lib/okstra-dirs.mjs} +0 -0
- /package/src/{_proc.mjs → lib/proc.mjs} +0 -0
- /package/src/{runtime-manifest.mjs → lib/runtime-manifest.mjs} +0 -0
- /package/src/{runtime-resolver.mjs → lib/runtime-resolver.mjs} +0 -0
- /package/src/{skill-catalog.mjs → lib/skill-catalog.mjs} +0 -0
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { getPackageRoot, getPackageVersion } from "./version.mjs";
|
|
1
|
+
import { resolvePaths } from "../../lib/paths.mjs";
|
|
5
2
|
|
|
6
3
|
const USAGE = `okstra paths — print runtime paths
|
|
7
4
|
|
|
@@ -52,62 +49,6 @@ const SHELL_EXPORTS = [
|
|
|
52
49
|
["OKSTRA_HOME", "home"],
|
|
53
50
|
];
|
|
54
51
|
|
|
55
|
-
export async function resolvePaths() {
|
|
56
|
-
const home = process.env.OKSTRA_HOME?.trim() || join(homedir(), ".okstra");
|
|
57
|
-
const pkgRoot = getPackageRoot();
|
|
58
|
-
const devLink = await readSimpleFile(join(home, "dev-link"));
|
|
59
|
-
const runtimeRoot = devLink || join(pkgRoot, "runtime");
|
|
60
|
-
return {
|
|
61
|
-
agents: join(runtimeRoot, "agents"),
|
|
62
|
-
workspace: runtimeRoot,
|
|
63
|
-
pythonpath: join(home, "lib", "python"),
|
|
64
|
-
bin: join(home, "bin"),
|
|
65
|
-
home,
|
|
66
|
-
package: await getPackageVersion(),
|
|
67
|
-
version: await readSimpleFile(join(home, "version")),
|
|
68
|
-
"dev-link": devLink || "",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function workspacePythonpath(paths) {
|
|
73
|
-
const candidates = [
|
|
74
|
-
join(paths.workspace, "python"),
|
|
75
|
-
join(paths.workspace, "scripts"),
|
|
76
|
-
];
|
|
77
|
-
return candidates.find((candidate) => existsSync(candidate)) || "";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Compose the PYTHONPATH spawned python3 subprocesses should see.
|
|
81
|
-
//
|
|
82
|
-
// Precedence (first wins):
|
|
83
|
-
// 1. process.env.PYTHONPATH (user override — e.g. in-repo `PYTHONPATH=scripts`
|
|
84
|
-
// for dev work without re-running `okstra install`).
|
|
85
|
-
// 2. The package/dev-link workspace runtime selected by `okstra paths`.
|
|
86
|
-
// 3. The installed user-runtime path (~/.okstra/lib/python).
|
|
87
|
-
//
|
|
88
|
-
// This is a colon-joined string suitable for direct assignment into the child
|
|
89
|
-
// process env. Empty segments are dropped so a missing env var does not produce
|
|
90
|
-
// a leading ':' (which Python treats as 'current directory').
|
|
91
|
-
export function buildPythonpath(paths) {
|
|
92
|
-
const fragments = [
|
|
93
|
-
process.env.PYTHONPATH,
|
|
94
|
-
workspacePythonpath(paths),
|
|
95
|
-
paths.pythonpath,
|
|
96
|
-
].filter(
|
|
97
|
-
(s) => typeof s === "string" && s.length > 0,
|
|
98
|
-
);
|
|
99
|
-
return fragments.join(":");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function readSimpleFile(path) {
|
|
103
|
-
try {
|
|
104
|
-
const { readFile } = await import("node:fs/promises");
|
|
105
|
-
return (await readFile(path, "utf8")).trim();
|
|
106
|
-
} catch {
|
|
107
|
-
return "";
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
52
|
export async function run(args) {
|
|
112
53
|
if (args.includes("--help") || args.includes("-h")) {
|
|
113
54
|
process.stdout.write(USAGE);
|
|
@@ -2,8 +2,8 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve as resolvePath } from "node:path";
|
|
5
|
-
import { resolvePaths } from "
|
|
6
|
-
import { fileExists, runProcess } from "
|
|
5
|
+
import { buildPythonpath, resolvePaths } from "../../lib/paths.mjs";
|
|
6
|
+
import { fileExists, runProcess } from "../../lib/proc.mjs";
|
|
7
7
|
|
|
8
8
|
const USAGE = `okstra setup — register the current project with okstra
|
|
9
9
|
|
|
@@ -95,7 +95,7 @@ async function resolveProjectRoot(paths, explicit) {
|
|
|
95
95
|
explicit || "",
|
|
96
96
|
process.cwd(),
|
|
97
97
|
],
|
|
98
|
-
{ PYTHONPATH: paths
|
|
98
|
+
{ PYTHONPATH: buildPythonpath(paths) },
|
|
99
99
|
);
|
|
100
100
|
if (probe.code !== 0) {
|
|
101
101
|
throw new Error(`python invocation failed: ${probe.stderr.trim() || probe.stdout.trim()}`);
|
|
@@ -136,7 +136,7 @@ async function upsert(paths, projectRoot, projectId) {
|
|
|
136
136
|
projectRoot,
|
|
137
137
|
projectId,
|
|
138
138
|
],
|
|
139
|
-
{ PYTHONPATH: paths
|
|
139
|
+
{ PYTHONPATH: buildPythonpath(paths) },
|
|
140
140
|
);
|
|
141
141
|
if (probe.code !== 0) {
|
|
142
142
|
throw new Error(`python invocation failed: ${probe.stderr.trim() || probe.stdout.trim()}`);
|
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { resolvePaths } from "
|
|
4
|
+
import { resolvePaths } from "../../lib/paths.mjs";
|
|
5
5
|
import {
|
|
6
6
|
RUNTIMES_MANIFEST_REL,
|
|
7
7
|
runtimeManifestIncludesClaudeAssets,
|
|
8
|
-
} from "
|
|
9
|
-
|
|
10
|
-
const BIN_ENTRYPOINTS = [
|
|
11
|
-
"okstra.sh",
|
|
12
|
-
"okstra-codex-exec.sh",
|
|
13
|
-
"okstra-claude-exec.sh",
|
|
14
|
-
"okstra-antigravity-exec.sh",
|
|
15
|
-
"okstra-trace-cleanup.sh",
|
|
16
|
-
"okstra-team-reconcile.sh",
|
|
17
|
-
"okstra-central.sh",
|
|
18
|
-
"okstra-token-usage.py",
|
|
19
|
-
"okstra-error-log.py",
|
|
20
|
-
"okstra-render-report-views.py",
|
|
21
|
-
"okstra-render-final-report.py",
|
|
22
|
-
"okstra-wrapper-status.py",
|
|
23
|
-
];
|
|
8
|
+
} from "../../lib/runtime-manifest.mjs";
|
|
24
9
|
|
|
25
10
|
const FALLBACK_SKILL_NAMES = [
|
|
26
11
|
"okstra",
|
|
@@ -71,7 +56,7 @@ const USAGE = `okstra uninstall — remove installed runtime and okstra skills
|
|
|
71
56
|
|
|
72
57
|
Usage:
|
|
73
58
|
okstra uninstall Remove ~/.okstra/{lib (python + validators),
|
|
74
|
-
bin
|
|
59
|
+
bin, schemas, templates, version,
|
|
75
60
|
dev-link, installed-runtimes.json,
|
|
76
61
|
installed-skills.json, installed-agents.json} AND
|
|
77
62
|
every skill target recorded in
|
|
@@ -184,21 +169,24 @@ export async function runUninstall(args) {
|
|
|
184
169
|
// template's section schema (observed: a pre-renumber `## 4.5 Stage Map`
|
|
185
170
|
// validator outliving every later install).
|
|
186
171
|
await removePath(join(paths.home, "lib", "validators"), opts);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
172
|
+
// bin/ tree — install copies the whole runtime/bin payload (entrypoints +
|
|
173
|
+
// bin/lib helpers) in copy mode, so a per-name list always drifts behind
|
|
174
|
+
// additions (e.g. okstra-inject-report-index.py, okstra-spawn-followups.sh).
|
|
175
|
+
// Remove the directory wholesale, matching how install populates it; nothing
|
|
176
|
+
// under ~/.okstra/bin is user data.
|
|
177
|
+
await removePath(paths.bin, opts);
|
|
178
|
+
// Clean now-empty lib parent (best-effort) — pythonpath + validators were the
|
|
179
|
+
// only okstra-owned children.
|
|
180
|
+
const libDir = join(paths.home, "lib");
|
|
181
|
+
if (await pathExists(libDir)) {
|
|
182
|
+
try {
|
|
183
|
+
const entries = await fs.readdir(libDir);
|
|
184
|
+
if (entries.length === 0) {
|
|
185
|
+
if (!opts.dryRun) await fs.rmdir(libDir);
|
|
186
|
+
if (!opts.quiet) process.stdout.write(` removed empty: ${libDir}\n`);
|
|
201
187
|
}
|
|
188
|
+
} catch {
|
|
189
|
+
/* ignore */
|
|
202
190
|
}
|
|
203
191
|
}
|
|
204
192
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
3
4
|
import { promises as fs } from "node:fs";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
@@ -227,12 +228,15 @@ function buildEntry(opts, content, now) {
|
|
|
227
228
|
const title = inferTitle(content, opts.title, opts.file);
|
|
228
229
|
const compact = now.toISOString().replace(/\D/g, "").slice(0, 17);
|
|
229
230
|
const slug = slugify(title);
|
|
230
|
-
|
|
231
|
+
// Random suffix disambiguates two same-title adds within one millisecond,
|
|
232
|
+
// which would otherwise collide on both id and on-disk path (silent overwrite).
|
|
233
|
+
const nonce = randomBytes(3).toString("hex");
|
|
234
|
+
const id = `mem_${compact}_${slug}_${nonce}`;
|
|
231
235
|
const relativePath = join(
|
|
232
236
|
"entries",
|
|
233
237
|
String(now.getUTCFullYear()),
|
|
234
238
|
String(now.getUTCMonth() + 1).padStart(2, "0"),
|
|
235
|
-
`${now.toISOString().replace(/[:.]/g, "")}-${slug}.md`,
|
|
239
|
+
`${now.toISOString().replace(/[:.]/g, "")}-${slug}-${nonce}.md`,
|
|
236
240
|
);
|
|
237
241
|
return {
|
|
238
242
|
id,
|
|
@@ -298,10 +302,19 @@ async function readIndex() {
|
|
|
298
302
|
if (err.code === "ENOENT") return [];
|
|
299
303
|
throw err;
|
|
300
304
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
// Skip lines that fail to parse so one truncated row (e.g. an interrupted
|
|
306
|
+
// appendIndex) cannot make the whole Memory Book unreadable; other JSONL
|
|
307
|
+
// readers in okstra tolerate corruption the same way.
|
|
308
|
+
const entries = [];
|
|
309
|
+
for (const line of text.split(/\r?\n/)) {
|
|
310
|
+
if (!line) continue;
|
|
311
|
+
try {
|
|
312
|
+
entries.push(JSON.parse(line));
|
|
313
|
+
} catch {
|
|
314
|
+
// Drop the unparseable row; intact entries remain visible.
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return entries;
|
|
305
318
|
}
|
|
306
319
|
|
|
307
320
|
async function writeIndex(entries) {
|
|
@@ -447,7 +460,19 @@ async function opArchive(args) {
|
|
|
447
460
|
const updated = entries.map((candidate) =>
|
|
448
461
|
candidate.id === entry.id ? archivedEntry : candidate,
|
|
449
462
|
);
|
|
450
|
-
|
|
463
|
+
try {
|
|
464
|
+
await writeIndex(updated);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
// Index rewrite failed after the file was already moved — move it back so
|
|
467
|
+
// the index keeps pointing at the file's real location (mirrors opAdd).
|
|
468
|
+
if (archivedEntry.path !== entry.path) {
|
|
469
|
+
await fs.rename(
|
|
470
|
+
join(memoryRoot(), archivedEntry.path),
|
|
471
|
+
join(memoryRoot(), entry.path),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
throw err;
|
|
475
|
+
}
|
|
451
476
|
if (json) emitJson({ ok: true, entry: archivedEntry });
|
|
452
477
|
else process.stdout.write(`archived ${entry.id}\n`);
|
|
453
478
|
return 0;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { getPackageRoot, getPackageVersion } from "./version.mjs";
|
|
5
|
+
|
|
6
|
+
export async function resolvePaths() {
|
|
7
|
+
const home = process.env.OKSTRA_HOME?.trim() || join(homedir(), ".okstra");
|
|
8
|
+
const pkgRoot = getPackageRoot();
|
|
9
|
+
const devLink = await readSimpleFile(join(home, "dev-link"));
|
|
10
|
+
const runtimeRoot = devLink || join(pkgRoot, "runtime");
|
|
11
|
+
return {
|
|
12
|
+
agents: join(runtimeRoot, "agents"),
|
|
13
|
+
workspace: runtimeRoot,
|
|
14
|
+
pythonpath: join(home, "lib", "python"),
|
|
15
|
+
bin: join(home, "bin"),
|
|
16
|
+
home,
|
|
17
|
+
package: await getPackageVersion(),
|
|
18
|
+
version: await readSimpleFile(join(home, "version")),
|
|
19
|
+
"dev-link": devLink || "",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function workspacePythonpath(paths) {
|
|
24
|
+
const candidates = [
|
|
25
|
+
join(paths.workspace, "python"),
|
|
26
|
+
join(paths.workspace, "scripts"),
|
|
27
|
+
];
|
|
28
|
+
return candidates.find((candidate) => existsSync(candidate)) || "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Compose the PYTHONPATH spawned python3 subprocesses should see.
|
|
32
|
+
//
|
|
33
|
+
// Precedence (first wins):
|
|
34
|
+
// 1. process.env.PYTHONPATH (user override — e.g. in-repo `PYTHONPATH=scripts`
|
|
35
|
+
// for dev work without re-running `okstra install`).
|
|
36
|
+
// 2. The package/dev-link workspace runtime selected by `okstra paths`.
|
|
37
|
+
// 3. The installed user-runtime path (~/.okstra/lib/python).
|
|
38
|
+
//
|
|
39
|
+
// This is a colon-joined string suitable for direct assignment into the child
|
|
40
|
+
// process env. Empty segments are dropped so a missing env var does not produce
|
|
41
|
+
// a leading ':' (which Python treats as 'current directory').
|
|
42
|
+
export function buildPythonpath(paths) {
|
|
43
|
+
const fragments = [
|
|
44
|
+
process.env.PYTHONPATH,
|
|
45
|
+
workspacePythonpath(paths),
|
|
46
|
+
paths.pythonpath,
|
|
47
|
+
].filter(
|
|
48
|
+
(s) => typeof s === "string" && s.length > 0,
|
|
49
|
+
);
|
|
50
|
+
return fragments.join(":");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readSimpleFile(path) {
|
|
54
|
+
try {
|
|
55
|
+
const { readFile } = await import("node:fs/promises");
|
|
56
|
+
return (await readFile(path, "utf8")).trim();
|
|
57
|
+
} catch {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -10,7 +10,7 @@ function resolveInstalledScript(paths, scriptName) {
|
|
|
10
10
|
// been installed (dev / CI).
|
|
11
11
|
const installed = join(paths.bin, scriptName);
|
|
12
12
|
if (existsSync(installed)) return installed;
|
|
13
|
-
const repoRoot = fileURLToPath(new URL("
|
|
13
|
+
const repoRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
14
14
|
const dev = resolvePath(repoRoot, "scripts", scriptName);
|
|
15
15
|
return existsSync(dev) ? dev : null;
|
|
16
16
|
}
|
|
@@ -60,12 +60,22 @@ export async function runPythonSnippet({ script, args = [], extraEnv = {} }) {
|
|
|
60
60
|
stdio: ["ignore", "pipe", "pipe"],
|
|
61
61
|
env: { ...process.env, PYTHONPATH: buildPythonpath(paths), ...extraEnv },
|
|
62
62
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
child.on("
|
|
68
|
-
child.on("
|
|
63
|
+
// Collect chunks and join once: repeated `s += chunk` is O(n^2) for the
|
|
64
|
+
// large rendered prompts some capture-mode callers stream through here.
|
|
65
|
+
const outChunks = [];
|
|
66
|
+
const errChunks = [];
|
|
67
|
+
child.stdout.on("data", (b) => outChunks.push(b));
|
|
68
|
+
child.stderr.on("data", (b) => errChunks.push(b));
|
|
69
|
+
child.on("error", (err) =>
|
|
70
|
+
resolve({ code: -1, stdout: Buffer.concat(outChunks).toString(), stderr: err.message }),
|
|
71
|
+
);
|
|
72
|
+
child.on("close", (code) =>
|
|
73
|
+
resolve({
|
|
74
|
+
code,
|
|
75
|
+
stdout: Buffer.concat(outChunks).toString(),
|
|
76
|
+
stderr: Buffer.concat(errChunks).toString(),
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
69
79
|
});
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -76,14 +86,26 @@ export async function runPythonModule({ module, args = [], extraEnv = {}, stdio
|
|
|
76
86
|
stdio: stdio === "capture" ? ["ignore", "pipe", "pipe"] : ["ignore", "inherit", "inherit"],
|
|
77
87
|
env: { ...process.env, PYTHONPATH: buildPythonpath(paths), ...extraEnv },
|
|
78
88
|
});
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
// Collect chunks and join once: repeated `s += chunk` is O(n^2) for the
|
|
90
|
+
// large rendered prompts some capture-mode callers stream through here.
|
|
91
|
+
const outChunks = [];
|
|
92
|
+
const errChunks = [];
|
|
81
93
|
if (stdio === "capture") {
|
|
82
|
-
child.stdout.on("data", (b) => (
|
|
83
|
-
child.stderr.on("data", (b) => (
|
|
94
|
+
child.stdout.on("data", (b) => outChunks.push(b));
|
|
95
|
+
child.stderr.on("data", (b) => errChunks.push(b));
|
|
84
96
|
}
|
|
85
|
-
child.on("error", (err) =>
|
|
86
|
-
|
|
97
|
+
child.on("error", (err) =>
|
|
98
|
+
resolve({ code: -1, stdout: Buffer.concat(outChunks).toString(), stderr: err.message }),
|
|
99
|
+
);
|
|
100
|
+
// A signal-killed child reports code === null; normalize to a non-zero exit
|
|
101
|
+
// here so every consumer inherits correct failure semantics from one place.
|
|
102
|
+
child.on("close", (code) =>
|
|
103
|
+
resolve({
|
|
104
|
+
code: typeof code === "number" ? code : 1,
|
|
105
|
+
stdout: Buffer.concat(outChunks).toString(),
|
|
106
|
+
stderr: Buffer.concat(errChunks).toString(),
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
87
109
|
});
|
|
88
110
|
}
|
|
89
111
|
|
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const packageJsonPath = join(here, "..", "package.json");
|
|
6
|
+
const packageJsonPath = join(here, "..", "..", "package.json");
|
|
7
7
|
|
|
8
8
|
let cachedVersion = null;
|
|
9
9
|
|
|
@@ -16,5 +16,5 @@ export async function getPackageVersion() {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function getPackageRoot() {
|
|
19
|
-
return join(here, "..");
|
|
19
|
+
return join(here, "..", "..");
|
|
20
20
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|