fix(ops): operator export scripts honor --help and refuse out-of-repo output paths - #36
fix(ops): operator export scripts honor --help and refuse out-of-repo output paths#36nish3451 wants to merge 4 commits into
Conversation
…-repo output paths Every active operator export script now handles --help/-h before doing any work: it prints usage and exits 0 without writing or overwriting any cockpit or mission artifact. Output paths (--output, --html, --ops, --loom-links) are resolved against the service repository root and refused when they escape it via absolute paths, .. traversal, or symlinks, so an export can no longer write or overwrite files anywhere on the machine. - Add scripts/lib/operator-cli.mjs with handleHelp() and resolveOutputPath() - Wire both into all 21 npm-script-backed export scripts - Route the internal-dashboard parity scratch file inside the repo (runs/) instead of /tmp, still cleaned up after use - Extend test-active-operator-surfaces.mjs: --help surface for every active export script (exit 0 + usage + no overwrite) and escape-refusal probes for absolute, ..-relative, and symlink-escape paths
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
📝 WalkthroughWalkthroughThe PR adds shared CLI help and output-path validation. Export scripts use these helpers for generated files and related inputs. Tests cover help behavior, artifact preservation, traversal, absolute paths, and symlink escapes. ChangesOperator CLI safety
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a568523ca5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let existing = resolved; | ||
| while (!existsSync(existing)) { |
There was a problem hiding this comment.
Reject dangling symlinks before returning the output path
When the requested output itself is a symlink to a nonexistent file outside the repository, existsSync(existing) returns false, so this loop skips the symlink and validates only its in-repository parent. The subsequent writeFileSync follows the dangling symlink and creates the external target; for example, a repository symlink report.md -> /tmp/external-report.md lets --output=report.md write outside the repository while exiting successfully. Inspect symlink components with lstatSync/readlinkSync, including dangling links, rather than relying on existsSync.
Useful? React with 👍 / 👎.
| for (const [script, args, escapePath] of escapeProbes) { | ||
| rmSync(escapePath, {force: true}) |
There was a problem hiding this comment.
Keep escape probes inside test-owned temporary storage
When a developer runs npm test, this unconditional cleanup deletes predictable paths outside the test directory before any assertion runs. Because T is created directly under /tmp, the current probes resolve to locations such as /tmp/escape-metrics.md, /tmp/escape-cockpit.html, and even /escape-mission.html; any pre-existing user or process-owned files at those paths are silently removed. Create a dedicated external temporary directory owned by this test and target all escape probes within it instead.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 12
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/export-followup-cockpit.mjs`:
- Around line 408-411: Update the output-directory handling around
resolvedOutputPath to import dirname from node:path and derive outputDir with
dirname(resolvedOutputPath) instead of splitting on "/". Keep the existing
recursive mkdirSync and writeFileSync behavior unchanged.
In `@scripts/export-growth-cockpit.mjs`:
- Around line 246-249: Replace the manual output-directory extraction in the
export flow with dirname(resolvedOutputPath) from node:path, ensuring Windows
backslash-separated paths are handled before mkdirSync creates the parent
directory.
In `@scripts/export-growth-doctor.mjs`:
- Around line 238-241: Update the output-directory handling around
resolvedOutputPath to import and use dirname from node:path instead of splitting
on "/". Pass dirname(resolvedOutputPath) to mkdirSync with recursive creation
before writeFileSync, preserving the existing resolved output path.
In `@scripts/export-growth-metrics.mjs`:
- Around line 187-190: Update the output-directory calculation around
resolvedOutputPath to import dirname from node:path and call
dirname(resolvedOutputPath) instead of splitting on "/"; retain the existing
recursive mkdirSync and writeFileSync flow.
In `@scripts/export-internal-dashboard.mjs`:
- Around line 59-61: Wrap the parity command and cleanup in a try/finally block
so parityScratchPath is removed even when runJson throws. Keep the existing
runJson invocation and rmSync cleanup behavior, with cleanup executing in the
finally block.
In `@scripts/export-lead-scoring-cockpit.mjs`:
- Around line 300-303: Replace the manual output-directory derivation in the
resolvedOutputPath flow with dirname(resolvedOutputPath) from node:path, and use
that result for the existing mkdirSync call so native Windows and POSIX path
separators are handled correctly.
In `@scripts/export-market-proof-run.mjs`:
- Around line 341-352: Replace the manual slash-based parent-directory
extraction for resolvedOutputPath and resolvedLoomLinksPath with dirname() from
node:path. Use the returned directories in the existing mkdirSync calls so both
output files create their parent directories correctly on all platforms.
In `@scripts/export-proof-library.mjs`:
- Around line 68-71: Update the resolved output directory logic near
resolvedOutputPath to import and use dirname from node:path instead of splitting
on "/". Keep the existing conditional mkdirSync behavior and writeFileSync flow
unchanged.
In `@scripts/export-prospect-outbox.mjs`:
- Around line 647-650: Update the output-directory calculation near
resolvedOutputPath to import and use dirname() from node:path instead of
splitting on "/"; retain the conditional mkdirSync(outputDir, { recursive: true
}) before writeFileSync so Windows paths create their parent directory
correctly.
In `@scripts/export-recording-cockpit.mjs`:
- Around line 584-587: Replace the manual parent-directory extraction after
resolveOutputPath with dirname(resolvedOutputPath) from node:path, ensuring the
import is available. Keep the existing conditional mkdirSync behavior and
writeFileSync flow unchanged.
In `@scripts/test-active-operator-surfaces.mjs`:
- Around line 121-132: Extend the help-surface loop in the active-operator test
to run each script with both --help and -h. Apply the same successful-exit and
Usage: assertions to each flag, and ensure the existing live-metrics and
proof-library preservation checks cover both invocations; likewise validate
export-daily-money-mission.mjs with -h while preserving the daily mission
artifact.
- Around line 134-142: Update the operator export path validation exercised by
escapeProbes to reject any isAbsolute(raw) output path before resolving it,
including absolute paths that fall within the service root. Add an in-repository
absolute-path probe and preserve the existing rejection coverage for
parent-relative and symlink escapes.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0a891ba0-37c6-47dd-8d43-b23953be5f23
📒 Files selected for processing (23)
scripts/export-client-delivery-cockpit.mjsscripts/export-daily-money-mission.mjsscripts/export-followup-cockpit.mjsscripts/export-growth-cockpit.mjsscripts/export-growth-doctor.mjsscripts/export-growth-metrics.mjsscripts/export-internal-dashboard.mjsscripts/export-lead-scoring-cockpit.mjsscripts/export-managed-it-one-pager.mjsscripts/export-market-benchmark.mjsscripts/export-market-learning-review.mjsscripts/export-market-proof-cockpit.mjsscripts/export-market-proof-run.mjsscripts/export-proof-library.mjsscripts/export-prospect-outbox.mjsscripts/export-recording-cockpit.mjsscripts/export-recording-queue.mjsscripts/export-recording-rehearsal-check.mjsscripts/export-recording-teleprompter.mjsscripts/export-sales-cockpit.mjsscripts/export-sender-setup-guide.mjsscripts/lib/operator-cli.mjsscripts/test-active-operator-surfaces.mjs
| const resolvedOutputPath = resolveOutputPath(outputPath); | ||
| const outputDir = resolvedOutputPath.split("/").slice(0, -1).join("/"); | ||
| if (outputDir) mkdirSync(outputDir, { recursive: true }); | ||
| writeFileSync(outputPath, html); | ||
| writeFileSync(resolvedOutputPath, html); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find all resolved paths that derive a parent directory by splitting on "/".
rg -n -C 2 'resolved(Output|LoomLinks)Path\.split\("/"\)' scripts
# Expected: use node:path dirname() for each resolved filesystem path.Repository: nish3451/tinystudio-in
Length of output: 5843
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- imports and path helper references ---'
rg -n -C 5 'from "node:path"|resolveOutputPath|resolvedOutputPath|outputDir' scripts/export-followup-cockpit.mjs
printf '%s\n' '--- helper definitions across scripts ---'
rg -n -C 6 'function resolveOutputPath|const resolveOutputPath|resolveOutputPath =' scripts -g '*.mjs'Repository: nish3451/tinystudio-in
Length of output: 2545
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- operator-cli path imports and helper ---'
cat -n scripts/lib/operator-cli.mjs | sed -n '1,60p'
printf '%s\n' '--- followup path-related imports and output setup ---'
cat -n scripts/export-followup-cockpit.mjs | sed -n '1,25p'
cat -n scripts/export-followup-cockpit.mjs | sed -n '400,415p'Repository: nish3451/tinystudio-in
Length of output: 4901
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const path = require('node:path');
const windowsPath = 'C:\\work\\exports\\followup.html';
const splitDir = windowsPath.split('/').slice(0, -1).join('/');
const dirnameResult = path.win32.dirname(windowsPath);
console.log(JSON.stringify({
windowsPath,
splitDir,
splitDirIsEmpty: splitDir === '',
dirnameResult,
unixSplitDir: '/work/exports/followup.html'.split('/').slice(0, -1).join('/'),
unixDirname: path.posix.dirname('/work/exports/followup.html'),
}, null, 2));
JSRepository: nish3451/tinystudio-in
Length of output: 370
Use dirname() for the resolved output path.
On Windows, split("/") produces an empty outputDir, so mkdirSync() does not create missing parent directories. writeFileSync() then fails with ENOENT. Import dirname from node:path and use dirname(resolvedOutputPath).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/export-followup-cockpit.mjs` around lines 408 - 411, Update the
output-directory handling around resolvedOutputPath to import dirname from
node:path and derive outputDir with dirname(resolvedOutputPath) instead of
splitting on "/". Keep the existing recursive mkdirSync and writeFileSync
behavior unchanged.
| const resolvedOutputPath = resolveOutputPath(outputPath); | ||
| const outputDir = resolvedOutputPath.split("/").slice(0, -1).join("/"); | ||
| if (outputDir) mkdirSync(outputDir, { recursive: true }); | ||
| writeFileSync(outputPath, `${html.replace(/[ \t]+$/gm, "").trimEnd()}\n`); | ||
| writeFileSync(resolvedOutputPath, `${html.replace(/[ \t]+$/gm, "").trimEnd()}\n`); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- file location and size ---'
fd -t f 'export-growth-cockpit\.mjs$' .
wc -l scripts/export-growth-cockpit.mjs
printf '%s\n' '--- imports and path handling ---'
sed -n '1,45p' scripts/export-growth-cockpit.mjs
sed -n '220,260p' scripts/export-growth-cockpit.mjs
printf '%s\n' '--- relevant symbol usages ---'
rg -n 'resolveOutputPath|outputPath|mkdirSync|writeFileSync|dirname|pathToFileURL' scripts/export-growth-cockpit.mjsRepository: nish3451/tinystudio-in
Length of output: 3568
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path, PurePosixPath, PureWindowsPath
source = Path("scripts/export-growth-cockpit.mjs").read_text()
print("--- deterministic split behavior ---")
for value in [
r"C:\reports\growth.html",
r"C:/reports/growth.html",
"/reports/growth.html",
"growth.html",
]:
split_parent = value.split("/")[:-1]
split_dir = "/".join(split_parent)
print(f"{value!r} -> split parent {split_dir!r}")
print("--- Windows path interpretation ---")
for value in [r"C:\reports\growth.html", r"C:/reports/growth.html"]:
p = PureWindowsPath(value)
print(f"{value!r} -> parent {str(p.parent)!r}, name {p.name!r}")
PYRepository: nish3451/tinystudio-in
Length of output: 556
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- operator CLI path resolver ---'
fd -t f 'operator-cli\.mjs$' .
wc -l scripts/lib/operator-cli.mjs
cat -n scripts/lib/operator-cli.mjs
printf '%s\n' '--- related output-path tests and documentation ---'
rg -n -i 'resolveOutputPath|--output=|growth-cockpit|output path' scripts test tests README.md package.json 2>/dev/null || trueRepository: nish3451/tinystudio-in
Length of output: 27332
🏁 Script executed:
#!/bin/bash
set -e
node - <<'JS'
const path = require("node:path");
const root = "C:\\service";
const inputs = [
"runs\\growth-cockpit.html",
"nested\\new\\growth-cockpit.html",
"runs/growth-cockpit.html",
"C:\\service\\nested\\new\\growth-cockpit.html",
];
for (const raw of inputs) {
const resolved = path.win32.resolve(root, raw);
const currentDir = resolved.split("/").slice(0, -1).join("/");
console.log(JSON.stringify({
raw,
resolved,
currentDir,
currentCreatesDirectory: Boolean(currentDir),
dirname: path.win32.dirname(resolved),
}));
}
JSRepository: nish3451/tinystudio-in
Length of output: 878
Use dirname() for the resolved output path.
On Windows, resolveOutputPath() returns backslash-separated paths, so split("/") leaves outputDir empty. Nested output paths then fail when their parent directory does not exist.
Use dirname(resolvedOutputPath) from node:path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/export-growth-cockpit.mjs` around lines 246 - 249, Replace the manual
output-directory extraction in the export flow with dirname(resolvedOutputPath)
from node:path, ensuring Windows backslash-separated paths are handled before
mkdirSync creates the parent directory.
| const resolvedOutputPath = resolveOutputPath(outputPath); | ||
| const outputDir = resolvedOutputPath.split("/").slice(0, -1).join("/"); | ||
| if (outputDir) mkdirSync(outputDir, { recursive: true }); | ||
| writeFileSync(outputPath, markdown); | ||
| writeFileSync(resolvedOutputPath, markdown); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
file="scripts/export-growth-doctor.mjs"
printf '%s\n' "== imports and target code =="
sed -n '1,45p' "$file"
sed -n '220,250p' "$file"
printf '%s\n' "== path-related usage =="
rg -n "resolveOutputPath|dirname|outputDir|mkdirSync|writeFileSync" "$file"Repository: nish3451/tinystudio-in
Length of output: 3462
🏁 Script executed:
#!/bin/bash
set -e
file="scripts/lib/operator-cli.mjs"
printf '%s\n' "== resolver definition =="
rg -n -A35 -B10 "function resolveOutputPath|const resolveOutputPath|export .*resolveOutputPath" "$file"
printf '%s\n' "== resolver callers =="
rg -n "resolveOutputPath\(" scripts
printf '%s\n' "== path module usage =="
rg -n "from \"node:path\"|from 'node:path'|win32|posix|normalize|resolve" scripts/lib/operator-cli.mjs scripts/export-growth-doctor.mjs
printf '%s\n' "== tracked related tests/docs =="
rg -n -i "growth-doctor|resolveOutputPath|output=.*runs|output path" --glob '!node_modules' .Repository: nish3451/tinystudio-in
Length of output: 16993
🏁 Script executed:
node - <<'JS'
import path from "node:path";
for (const [label, resolver] of [
["posix", path.posix.resolve],
["win32", path.win32.resolve]
]) {
const resolved = resolver("service-root", "runs/growth-doctor.md");
const current = resolved.split("/").slice(0, -1).join("/");
const correct = path[label].dirname(resolved);
console.log(JSON.stringify({ label, resolved, current, correct }));
}
JSRepository: nish3451/tinystudio-in
Length of output: 501
Use dirname() for the resolved output path.
On Windows, resolvedOutputPath.split("/") produces an empty outputDir because Windows uses \ separators. The parent directory is not created, and writeFileSync() fails when it does not already exist.
Import dirname from node:path and call mkdirSync(dirname(resolvedOutputPath), { recursive: true }).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/export-growth-doctor.mjs` around lines 238 - 241, Update the
output-directory handling around resolvedOutputPath to import and use dirname
from node:path instead of splitting on "/". Pass dirname(resolvedOutputPath) to
mkdirSync with recursive creation before writeFileSync, preserving the existing
resolved output path.
| const resolvedOutputPath = resolveOutputPath(outputPath); | ||
| const outputDir = resolvedOutputPath.split("/").slice(0, -1).join("/"); | ||
| if (outputDir) mkdirSync(outputDir, { recursive: true }); | ||
| writeFileSync(outputPath, markdown); | ||
| writeFileSync(resolvedOutputPath, markdown); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- imports and path helpers ---'
sed -n '1,80p' scripts/export-growth-metrics.mjs
printf '%s\n' '--- target code ---'
sed -n '160,205p' scripts/export-growth-metrics.mjs
printf '%s\n' '--- path helper references ---'
rg -n -C 4 'resolveOutputPath|outputPath|writeFileSync|mkdirSync' scripts/export-growth-metrics.mjs
printf '%s\n' '--- native path behavior ---'
node - <<'JS'
const path = require('node:path');
const samples = [
path.win32.resolve('metrics.md'),
path.win32.resolve('reports', 'metrics.md'),
path.posix.resolve('reports', 'metrics.md'),
];
for (const resolved of samples) {
const splitDir = resolved.split('/').slice(0, -1).join('/');
console.log(JSON.stringify({
resolved,
splitDir,
dirname: path.dirname(resolved),
win32Dirname: path.win32.dirname(resolved),
}));
}
JSRepository: nish3451/tinystudio-in
Length of output: 7104
🏁 Script executed:
#!/bin/bash
set -eu
file=$(fd -t f -i 'operator-cli\.mjs$' . | head -n 1)
if [ -z "$file" ]; then
printf '%s\n' 'operator-cli.mjs not found'
exit 0
fi
printf '--- %s ---\n' "$file"
rg -n -C 8 'resolveOutputPath|from "node:path"|path\.resolve|resolve\(' "$file"
printf '%s\n' '--- related path tests and callers ---'
rg -n -C 3 'resolveOutputPath' . -g '!node_modules' -g '!dist' -g '!build'Repository: nish3451/tinystudio-in
Length of output: 251
🏁 Script executed:
#!/bin/bash
set -eu
file=$(fd -t f -i 'operator-cli\.mjs$' . | head -n 1)
if [ -z "$file" ]; then
printf '%s\n' 'operator-cli.mjs not found'
exit 0
fi
printf -- '--- %s ---\n' "$file"
rg -n -C 8 'resolveOutputPath|from "node:path"|path\.resolve|resolve\(' "$file"
printf '%s\n' '--- related path tests and callers ---'
rg -n -C 3 'resolveOutputPath' . -g '!node_modules' -g '!dist' -g '!build'Repository: nish3451/tinystudio-in
Length of output: 30818
Use dirname() for the resolved output path.
On Windows, resolveOutputPath() returns backslash-separated paths. split("/") produces an empty outputDir, so mkdirSync() is skipped. writeFileSync() then fails when the parent directory does not exist.
Import dirname from node:path and use dirname(resolvedOutputPath).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/export-growth-metrics.mjs` around lines 187 - 190, Update the
output-directory calculation around resolvedOutputPath to import dirname from
node:path and call dirname(resolvedOutputPath) instead of splitting on "/";
retain the existing recursive mkdirSync and writeFileSync flow.
| const parityScratchPath = "runs/.internal-dashboard-parity.md"; | ||
| const parity = runJson(["scripts/check-market-parity-readiness.mjs", "--skip-kit", `--output=${parityScratchPath}`]); | ||
| try { rmSync(join(serviceRoot, parityScratchPath), { force: true }); } catch {} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Remove the parity scratch file when the parity command fails.
runJson(...) executes before the try statement. If it throws after creating the scratch file, rmSync(...) does not run. Wrap the command and cleanup in try/finally.
Proposed fix
const parityScratchPath = "runs/.internal-dashboard-parity.md";
-const parity = runJson(["scripts/check-market-parity-readiness.mjs", "--skip-kit", `--output=${parityScratchPath}`]);
-try { rmSync(join(serviceRoot, parityScratchPath), { force: true }); } catch {}
+let parity;
+try {
+ parity = runJson(["scripts/check-market-parity-readiness.mjs", "--skip-kit", `--output=${parityScratchPath}`]);
+} finally {
+ rmSync(join(serviceRoot, parityScratchPath), { force: true });
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parityScratchPath = "runs/.internal-dashboard-parity.md"; | |
| const parity = runJson(["scripts/check-market-parity-readiness.mjs", "--skip-kit", `--output=${parityScratchPath}`]); | |
| try { rmSync(join(serviceRoot, parityScratchPath), { force: true }); } catch {} | |
| const parityScratchPath = "runs/.internal-dashboard-parity.md"; | |
| let parity; | |
| try { | |
| parity = runJson(["scripts/check-market-parity-readiness.mjs", "--skip-kit", `--output=${parityScratchPath}`]); | |
| } finally { | |
| rmSync(join(serviceRoot, parityScratchPath), { force: true }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/export-internal-dashboard.mjs` around lines 59 - 61, Wrap the parity
command and cleanup in a try/finally block so parityScratchPath is removed even
when runJson throws. Keep the existing runJson invocation and rmSync cleanup
behavior, with cleanup executing in the finally block.
| const resolvedOutputPath = resolveOutputPath(outputPath); | ||
| const outputDir = resolvedOutputPath.split("/").slice(0, -1).join("/"); | ||
| if (outputDir) mkdirSync(outputDir, { recursive: true }); | ||
| writeFileSync(outputPath, content); | ||
| writeFileSync(resolvedOutputPath, content); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- file context ---'
sed -n '1,110p' scripts/export-proof-library.mjs
printf '%s\n' '--- path helper references ---'
rg -n -C 4 'resolveOutputPath|split\("/"\)|dirname\(' scripts
printf '%s\n' '--- runtime path behavior ---'
node - <<'JS'
const { dirname, win32, posix } = require('node:path');
const samples = [
'/tmp/out/proof.json',
'C:\\work\\out\\proof.json',
'\\\\server\\share\\out\\proof.json',
];
for (const value of samples) {
console.log(JSON.stringify({
value,
splitSlashParent: value.split('/').slice(0, -1).join('/'),
dirname: dirname(value),
win32Dirname: win32.dirname(value),
posixDirname: posix.dirname(value),
}));
}
JSRepository: nish3451/tinystudio-in
Length of output: 50379
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- operator-cli path helper ---'
rg -n -C 12 'function resolveOutputPath|const resolveOutputPath|export .*resolveOutputPath' scripts/lib/operator-cli.mjs
printf '%s\n' '--- relevant imports and call sites ---'
sed -n '1,95p' scripts/lib/operator-cli.mjs
sed -n '1,10p' scripts/export-proof-library.mjs
sed -n '64,75p' scripts/export-proof-library.mjs
printf '%s\n' '--- isolated platform behavior ---'
node - <<'JS'
const { dirname, win32 } = require('node:path');
for (const value of [
'C:\\work\\out\\proof.json',
'\\\\server\\share\\out\\proof.json',
'C:/work/out/proof.json',
]) {
console.log(JSON.stringify({
value,
slashSplit: value.split('/').slice(0, -1).join('/'),
hostDirname: dirname(value),
windowsDirname: win32.dirname(value),
}));
}
JSRepository: nish3451/tinystudio-in
Length of output: 5188
Use dirname() for the resolved output path.
On Windows, resolveOutputPath() returns backslash separators. split("/") produces no parent directory, so mkdirSync() is skipped and writeFileSync() fails when the parent does not exist. Import dirname from node:path and use dirname(resolvedOutputPath).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/export-proof-library.mjs` around lines 68 - 71, Update the resolved
output directory logic near resolvedOutputPath to import and use dirname from
node:path instead of splitting on "/". Keep the existing conditional mkdirSync
behavior and writeFileSync flow unchanged.
| const resolvedOutputPath = resolveOutputPath(outputPath); | ||
| const outputDir = resolvedOutputPath.split("/").slice(0, -1).join("/"); | ||
| if (outputDir) mkdirSync(outputDir, { recursive: true }); | ||
| writeFileSync(outputPath, html); | ||
| writeFileSync(resolvedOutputPath, html); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- imports and target code ---'
sed -n '1,45p' scripts/export-prospect-outbox.mjs
sed -n '620,665p' scripts/export-prospect-outbox.mjs
printf '%s\n' '--- path-related usage ---'
rg -n -C 3 'resolveOutputPath|outputDir|writeFileSync|mkdirSync|dirname|outputPath' scripts/export-prospect-outbox.mjs
printf '%s\n' '--- repository platform assumptions ---'
rg -n -C 2 'export-prospect-outbox|Windows|win32|path separator|dirname' README.md package.json scripts test tests 2>/dev/null || trueRepository: nish3451/tinystudio-in
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '1,45p' scripts/export-prospect-outbox.mjs
sed -n '620,665p' scripts/export-prospect-outbox.mjs
rg -n -C 3 'resolveOutputPath|outputDir|writeFileSync|mkdirSync|dirname|outputPath' scripts/export-prospect-outbox.mjsRepository: nish3451/tinystudio-in
Length of output: 4604
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- resolveOutputPath implementation ---'
sed -n '1,120p' scripts/lib/operator-cli.mjs
printf '%s\n' '--- all resolveOutputPath call sites ---'
rg -n -C 4 'resolveOutputPath' scripts --glob '*.mjs'
printf '%s\n' '--- cross-platform path behavior probe ---'
node - <<'JS'
const path = require("node:path");
for (const p of [
"C:\\repo\\prospects\\outbox.html",
"C:\\repo\\outbox.html",
"prospects/outbox.html",
"/repo/prospects/outbox.html",
"outbox.html"
]) {
console.log(JSON.stringify({
input: p,
posixSplitDir: p.split("/").slice(0, -1).join("/"),
win32Dirname: path.win32.dirname(p),
posixDirname: path.posix.dirname(p)
}));
}
JSRepository: nish3451/tinystudio-in
Length of output: 38115
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '1,120p' scripts/lib/operator-cli.mjs
rg -n -C 4 'resolveOutputPath' scripts --glob '*.mjs'
node - <<'JS'
const path = require("node:path");
for (const p of [
"C:\\repo\\prospects\\outbox.html",
"C:\\repo\\outbox.html",
"prospects/outbox.html",
"/repo/prospects/outbox.html",
"outbox.html"
]) {
console.log(JSON.stringify({
input: p,
posixSplitDir: p.split("/").slice(0, -1).join("/"),
win32Dirname: path.win32.dirname(p),
posixDirname: path.posix.dirname(p)
}));
}
JSRepository: nish3451/tinystudio-in
Length of output: 37990
Use dirname() for the resolved output path.
On Windows, resolveOutputPath() returns backslash-separated paths. split("/") produces no directory component, so mkdirSync() is skipped and writeFileSync() fails when the parent directory is missing. Import dirname() from node:path and use it for outputDir.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/export-prospect-outbox.mjs` around lines 647 - 650, Update the
output-directory calculation near resolvedOutputPath to import and use dirname()
from node:path instead of splitting on "/"; retain the conditional
mkdirSync(outputDir, { recursive: true }) before writeFileSync so Windows paths
create their parent directory correctly.
| const resolvedOutputPath = resolveOutputPath(outputPath); | ||
| const outputDir = resolvedOutputPath.split("/").slice(0, -1).join("/"); | ||
| if (outputDir) mkdirSync(outputDir, { recursive: true }); | ||
| writeFileSync(outputPath, html); | ||
| writeFileSync(resolvedOutputPath, html); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use dirname() for the resolved output path.
When resolvedOutputPath uses Windows separators, split("/") returns no parent directory. mkdirSync() then does not create the required parent directory, and writeFileSync() can fail. Use dirname(resolvedOutputPath) from node:path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/export-recording-cockpit.mjs` around lines 584 - 587, Replace the
manual parent-directory extraction after resolveOutputPath with
dirname(resolvedOutputPath) from node:path, ensuring the import is available.
Keep the existing conditional mkdirSync behavior and writeFileSync flow
unchanged.
| const liveMetricsBeforeHelp = readFileSync(join(T, "growth-brain/ops/live-metrics.md"), "utf8") | ||
| for (const name of helpSurface) { | ||
| const helped = run([`scripts/${name}`, "--help"]) | ||
| eq(helped.status, 0, `${name} --help must exit 0: ${helped.stderr || helped.stdout}`) | ||
| mat(helped.stdout, /Usage:/, `${name} --help must print usage`) | ||
| } | ||
| deq(readFileSync(join(T, "growth-brain/ops/live-metrics.md"), "utf8"), liveMetricsBeforeHelp, "--help must not overwrite live metrics") | ||
| deq(readFileSync(join(T, "growth-brain/ops/proof-library.md")), trackedArtifacts.get("growth-brain/ops/proof-library.md"), "--help must not overwrite the proof library") | ||
| eq(existsSync(join(T, "runs/daily-money-mission.md")), true, "daily mission must exist from regeneration") | ||
| const missionBeforeHelp = readFileSync(join(T, "runs/daily-money-mission.md"), "utf8") | ||
| run(["scripts/export-daily-money-mission.mjs", "--help"]) | ||
| deq(readFileSync(join(T, "runs/daily-money-mission.md"), "utf8"), missionBeforeHelp, "--help must not overwrite the daily money mission") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Test the -h help contract.
The loop tests only --help. A script can regress for -h without failing this active-surface test. Run the same status, usage, and artifact-preservation checks for both help flags.
Proposed fix
for (const name of helpSurface) {
- const helped = run([`scripts/${name}`, "--help"])
- eq(helped.status, 0, `${name} --help must exit 0: ${helped.stderr || helped.stdout}`)
- mat(helped.stdout, /Usage:/, `${name} --help must print usage`)
+ for (const flag of ["--help", "-h"]) {
+ const helped = run([`scripts/${name}`, flag])
+ eq(helped.status, 0, `${name} ${flag} must exit 0: ${helped.stderr || helped.stdout}`)
+ mat(helped.stdout, /Usage:/, `${name} ${flag} must print usage`)
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const liveMetricsBeforeHelp = readFileSync(join(T, "growth-brain/ops/live-metrics.md"), "utf8") | |
| for (const name of helpSurface) { | |
| const helped = run([`scripts/${name}`, "--help"]) | |
| eq(helped.status, 0, `${name} --help must exit 0: ${helped.stderr || helped.stdout}`) | |
| mat(helped.stdout, /Usage:/, `${name} --help must print usage`) | |
| } | |
| deq(readFileSync(join(T, "growth-brain/ops/live-metrics.md"), "utf8"), liveMetricsBeforeHelp, "--help must not overwrite live metrics") | |
| deq(readFileSync(join(T, "growth-brain/ops/proof-library.md")), trackedArtifacts.get("growth-brain/ops/proof-library.md"), "--help must not overwrite the proof library") | |
| eq(existsSync(join(T, "runs/daily-money-mission.md")), true, "daily mission must exist from regeneration") | |
| const missionBeforeHelp = readFileSync(join(T, "runs/daily-money-mission.md"), "utf8") | |
| run(["scripts/export-daily-money-mission.mjs", "--help"]) | |
| deq(readFileSync(join(T, "runs/daily-money-mission.md"), "utf8"), missionBeforeHelp, "--help must not overwrite the daily money mission") | |
| const liveMetricsBeforeHelp = readFileSync(join(T, "growth-brain/ops/live-metrics.md"), "utf8") | |
| for (const name of helpSurface) { | |
| for (const flag of ["--help", "-h"]) { | |
| const helped = run([`scripts/${name}`, flag]) | |
| eq(helped.status, 0, `${name} ${flag} must exit 0: ${helped.stderr || helped.stdout}`) | |
| mat(helped.stdout, /Usage:/, `${name} ${flag} must print usage`) | |
| } | |
| } | |
| deq(readFileSync(join(T, "growth-brain/ops/live-metrics.md"), "utf8"), liveMetricsBeforeHelp, "--help must not overwrite live metrics") | |
| deq(readFileSync(join(T, "growth-brain/ops/proof-library.md")), trackedArtifacts.get("growth-brain/ops/proof-library.md"), "--help must not overwrite the proof library") | |
| eq(existsSync(join(T, "runs/daily-money-mission.md")), true, "daily mission must exist from regeneration") | |
| const missionBeforeHelp = readFileSync(join(T, "runs/daily-money-mission.md"), "utf8") | |
| run(["scripts/export-daily-money-mission.mjs", "--help"]) | |
| deq(readFileSync(join(T, "runs/daily-money-mission.md"), "utf8"), missionBeforeHelp, "--help must not overwrite the daily money mission") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/test-active-operator-surfaces.mjs` around lines 121 - 132, Extend the
help-surface loop in the active-operator test to run each script with both
--help and -h. Apply the same successful-exit and Usage: assertions to each
flag, and ensure the existing live-metrics and proof-library preservation checks
cover both invocations; likewise validate export-daily-money-mission.mjs with -h
while preserving the daily mission artifact.
| // Operator export scripts must refuse output paths that escape the service | ||
| // root, whether absolute, ".."-relative, or through a symlink. | ||
| const escapeProbes = [ | ||
| ["scripts/export-growth-metrics.mjs", ["--output=../escape-metrics.md"], join(T, "../escape-metrics.md")], | ||
| ["scripts/export-growth-cockpit.mjs", ["--output=/tmp/escape-cockpit.html"], "/tmp/escape-cockpit.html"], | ||
| ["scripts/export-daily-money-mission.mjs", ["--html=../../escape-mission.html"], join(T, "../../escape-mission.html")], | ||
| ["scripts/export-market-proof-run.mjs", ["--output=runs/escape-run.md", "--loom-links=../escape-links.txt"], join(T, "../escape-links.txt")], | ||
| ["scripts/export-market-benchmark.mjs", ["--ops=/tmp/escape-ops.md"], "/tmp/escape-ops.md"] | ||
| ] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node --input-type=module <<'NODE'
import { isAbsolute, relative, resolve } from "node:path";
const root = resolve(process.cwd());
const raw = resolve(root, "runs/absolute-path-probe.md");
const resolved = resolve(root, raw);
const rel = relative(root, resolved);
if (!isAbsolute(raw) || rel.startsWith("..")) {
throw new Error("Probe setup failed");
}
console.log({ raw, resolved, rel });
// `rel` is in-root, so a resolver that checks only `rel` accepts this absolute input.
NODERepository: nish3451/tinystudio-in
Length of output: 320
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- candidate files ---'
fd -i 'export|active-operator' . scripts test tests 2>/dev/null | head -80
printf '%s\n' '--- resolver definitions and uses ---'
rg -n -C 5 'resolveOutputPath|escapeProbes|isAbsolute|relative\\(' scripts . --glob '*.mjs' --glob '*.js' --glob '*.ts' --glob '*.json' 2>/dev/null | head -240Repository: nish3451/tinystudio-in
Length of output: 3182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- resolver references ---'
rg -n -C 8 'resolveOutputPath|escapeProbes' . --glob '*.mjs' --glob '*.js' --glob '*.ts' || true
printf '%s\n' '--- test section ---'
sed -n '110,165p' scripts/test-active-operator-surfaces.mjs
printf '%s\n' '--- resolver-like path validation ---'
rg -n -C 6 'relative\\(|resolve\\(|isAbsolute\\(|service.*root|output path|outputPath' scripts --glob '*.mjs' | head -320Repository: nish3451/tinystudio-in
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- operator-cli resolver ---'
sed -n '1,75p' scripts/lib/operator-cli.mjs
printf '%s\n' '--- complete path-probe test ---'
sed -n '134,180p' scripts/test-active-operator-surfaces.mjs
printf '%s\n' '--- service root definition ---'
rg -n -C 4 'serviceRoot|SERVICE_REPO_ROOT' scripts/lib scripts --glob '*.mjs' | head -120Repository: nish3451/tinystudio-in
Length of output: 14793
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- documented output-path policy ---'
rg -n -C 3 'absolute|output paths|stay inside|escape the service|escape.*repository' README.md docs scripts --glob '*.md' --glob '*.mjs' | head -240
printf '%s\n' '--- resolver-focused tests and package commands ---'
rg -n -C 5 'operator-cli|resolveOutputPath|active-operator-surfaces|escapeProbes' . --glob '!scripts/*.mjs' --glob '*.json' --glob '*.md' --glob '*.yml' --glob '*.yaml' || trueRepository: nish3451/tinystudio-in
Length of output: 21356
Reject absolute output paths inside the service repository.
resolve(root, raw) accepts an absolute raw path when it is inside root. Add an in-repository absolute-path probe and reject isAbsolute(raw) before calling resolve(root, raw).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/test-active-operator-surfaces.mjs` around lines 134 - 142, Update the
operator export path validation exercised by escapeProbes to reject any
isAbsolute(raw) output path before resolving it, including absolute paths that
fall within the service root. Add an in-repository absolute-path probe and
preserve the existing rejection coverage for parent-relative and symlink
escapes.
|
Lane item verification: Repro against current origin/main (e0769ca): Against this branch (a568523): both scripts print usage and exit 0 with the tracked artifacts untouched (sentinel preserved). |
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f099aa029
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| write(resolveOutputPath(outputPath), markdown); | ||
| write(resolveOutputPath(opsPath, { flag: "--ops" }), markdown); |
There was a problem hiding this comment.
Validate every output path before writing artifacts
When a secondary path is unsafe—for example, market:benchmark -- --ops=/tmp/escape.md—line 348 overwrites the primary benchmark before line 349 rejects the escaping path and exits. The failed invocation therefore leaves a partially regenerated, potentially inconsistent output set; resolve all three paths before performing the first write.
Useful? React with 👍 / 👎.
| const parityScratchPath = "runs/.internal-dashboard-parity.md"; | ||
| const parity = runJson(["scripts/check-market-parity-readiness.mjs", "--skip-kit", `--output=${parityScratchPath}`]); | ||
| try { rmSync(join(serviceRoot, parityScratchPath), { force: true }); } catch {} |
There was a problem hiding this comment.
Use a unique dashboard scratch file
When runs/.internal-dashboard-parity.md already exists, the parity checker first overwrites that operator-owned file and this cleanup then deletes it unconditionally. Since runs/ is a general ignored output directory, a dashboard export can destroy unrelated local data at this predictable name; create a unique temporary file for each invocation and remove only that file.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 66bb043a13
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| `; | ||
|
|
||
| const outputDir = outputPath.split("/").slice(0, -1).join("/"); | ||
| const resolvedOutputPath = resolveOutputPath(outputPath); |
There was a problem hiding this comment.
Validate the path before running dependency exporters
When growth:cockpit is invoked with an escaping path such as --output=/tmp/cockpit.html, validation does not occur until this line, after lines 32–43 have run eleven child exporters. The command exits nonzero but still overwrites in-repository cockpits, metrics, the proof library, mission, doctor report, and sender guide, so a rejected invocation is not atomic; resolve the requested path before launching any dependency exporters.
AGENTS.md reference: AGENTS.md:L2-L3
Useful? React with 👍 / 👎.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
The fleet keeps dispatching the same finding to multiple lanes, producing duplicate same-fix PR pairs: #36/#44 and #55/#56 are byte-identical or near-identical patches of the operator export --help fix, #39/#49 the offername article fix, #40/#52 the recording exporter --help fix, plus #38/#48/#51, #30/#61/#72, #45/#64, #42/#60, #43/#54 and #46/#74. Add a PR Duplicate Guard: - scripts/check-pr-duplicates.mjs compares a PR's diff against every other open PR (shared changed-file coverage >= 0.8 and patch similarity >= 0.5). Calibrated on all 79 open PRs on 2026-08-11: every pair above the thresholds was a genuine duplicate-cluster member, zero false positives. - .github/workflows/pr-duplicate-guard.yml runs it on every PR event and posts one marker comment naming the duplicate(s) and the canonical PR; the check fails loudly when a duplicate is found. Informational, not required. - scripts/test-pr-duplicates.mjs covers parsing, similarity, detection, and comment upsert with an injected API; wired into npm ci and npm test.
|
Superseded by #56, which implements |
|
Correction to my closing comment above: the keeper for this cluster is #160, not #56. I had wrongly concluded that This PR stays closed either way — its content is superseded by what is already on |
…main by PR #80 (duplicate guard) (#234) The duplicate-PR guard (PR #80, merged 2026-08-19) is now live on main, preventing the same-fix duplication pattern at the CI level. The named duplicate pairs (#36/#44, #39/#49, #40/#52) are functionally superseded: their underlying fixes are on main via PRs #135, #145, and #178. The prior 2026-08-15 lane run had concluded the guard existed but was stuck unmerged; on 2026-08-19 it landed and the item is resolved at the root-cause level. Verification-only run (no product or test files touched): - PR #80 source commit 2091c7a and merge commit 0a9909b are ancestors of origin/main - scripts/test-pr-duplicates.mjs → 'test-pr-duplicates: ok' (exit 0) - Superseding PRs #135 (d4f3ef4), #145 (fc44b42), #178 (77f6922) all on main - Guard workflow runs on pull_request events; not a required status, so existing work never blocks Co-authored-by: minimax-vps <minimax-vps@nish3451.dev>
What
Closes the operator-export CLI safety gap: export scripts ignored
--helpandaccepted
--output/--html/--ops/--loom-linkspaths that could write oroverwrite cockpits, missions, and other files anywhere on the machine.
Changes
scripts/lib/operator-cli.mjshandleHelp(args, usage)— prints usage and exits 0 before any workwhen
--help/-his present.resolveOutputPath(value, {flag, fallback})— resolves operator-suppliedoutput paths against the service repository root and refuses paths that
escape it via absolute paths,
..traversal, or symlink escapes.handleHelpfirst androute every output path through
resolveOutputPath.export-internal-dashboard.mjsalso moves its parity scratch file from/tmpinto gitignoredruns/(cleaned up after use), so the dashboard nolonger writes outside the repository.
test-active-operator-surfaces.mjsextended:--help, print usage, and notoverwrite
live-metrics.md, the proof library, or the daily money mission;--output=../…, absolute/tmp/…,../../HTML path,--loom-links=../…,--ops=/tmp/…, and a symlink escape are all refusedwithout creating the file.
Validation
npm run ci— full gate passes (all suites +node --checkon every script).export-owned-*) are notpart of the active operator surface and were left untouched.
Files changed
scripts/(21 export scripts + extended surface test)scripts/lib/operator-cli.mjsSummary by CodeRabbit
New Features
--helpand-hguidance across export commands.Bug Fixes