diff --git a/docs/brainstorms/2026-04-22-demo-reel-local-save-requirements.md b/docs/brainstorms/2026-04-22-demo-reel-local-save-requirements.md new file mode 100644 index 0000000..0191a24 --- /dev/null +++ b/docs/brainstorms/2026-04-22-demo-reel-local-save-requirements.md @@ -0,0 +1,53 @@ +--- +date: 2026-04-22 +topic: demo-reel-local-save +--- + +# Demo Reel: Local Evidence Save + +## Problem Frame + +When `ce-demo-reel` captures evidence (GIFs, screenshots, terminal recordings), the local artifacts are deleted after uploading to catbox.moe. Users who want to keep evidence locally — for offline access, committing to the repo, or archival — have no way to do so without manually copying files from the temp directory before cleanup runs. + +--- + +## Requirements + +**Destination choice** +- R1. After capture completes, ask the user whether to upload to catbox (existing behavior) or save locally. +- R2. The question must present the captured artifact(s) and clearly describe both options. + +**Local save behavior** +- R3. When the user chooses local save, copy the final artifact(s) (GIF, PNG, or recording) to a stable OS-temp path (`$TMPDIR/compound-engineering/ce-demo-reel/`). Do not upload to catbox. +- R4. Create the destination directory if it does not exist. +- R5. Use a descriptive filename that includes the branch name or PR identifier and a timestamp to avoid collisions across runs. +- R6. After saving, display the local file path(s) to the user for easy reference. + +--- + +## Success Criteria + +- A user running `ce-demo-reel` can keep captured evidence on disk without manual intervention. +- The saved artifacts are discoverable in a predictable, stable OS-temp location. + +--- + +## Scope Boundaries + +- Catbox upload logic itself is unchanged — only the routing (local vs. upload) is new. +- No automatic git-add or commit of saved artifacts. +- No configurable save path — `$TMPDIR/compound-engineering/ce-demo-reel/` is the fixed default for now. +- No retroactive save of previously captured evidence. + +--- + +## Key Decisions + +- **Local save as an alternative to upload, not an addition**: The user chooses one destination per capture — either catbox or local. This keeps the flow simple and avoids redundant artifacts. +- **OS-temp as the local target**: Uses `$TMPDIR/compound-engineering/ce-demo-reel/` per the repo's cross-invocation scratch-space convention. Stable prefix makes files findable without polluting the repo tree. + +--- + +## Next Steps + +-> `/ce-plan` for structured implementation planning, or proceed directly to implementation given the small scope. diff --git a/docs/plans/2026-04-22-001-feat-demo-reel-local-save-plan.md b/docs/plans/2026-04-22-001-feat-demo-reel-local-save-plan.md new file mode 100644 index 0000000..3f7f372 --- /dev/null +++ b/docs/plans/2026-04-22-001-feat-demo-reel-local-save-plan.md @@ -0,0 +1,210 @@ +--- +title: "feat(ce-demo-reel): Add local save as alternative to catbox upload" +type: feat +status: active +date: 2026-04-22 +origin: docs/brainstorms/2026-04-22-demo-reel-local-save-requirements.md +--- + +# feat(ce-demo-reel): Add local save as alternative to catbox upload + +## Overview + +Add a destination choice to the ce-demo-reel upload flow: after capture, the user picks either "upload to catbox" (existing behavior) or "save locally" (new). Local save copies the final artifact to a stable OS-temp path with a descriptive filename. The catbox upload path is unchanged. + +--- + +## Problem Frame + +When ce-demo-reel captures evidence, local artifacts are deleted after uploading to catbox.moe. Users who want to keep evidence locally have no way to do so. (See origin: `docs/brainstorms/2026-04-22-demo-reel-local-save-requirements.md`) + +--- + +## Requirements Trace + +- R1. After capture completes, ask the user whether to upload to catbox or save locally +- R2. The question must present the captured artifact(s) and clearly describe both options +- R3. When the user chooses local save, copy artifacts to `$TMPDIR/compound-engineering/ce-demo-reel/`; do not upload to catbox +- R4. Create the destination directory if it does not exist +- R5. Use a descriptive filename with branch name and timestamp to avoid collisions +- R6. After saving, display the local file path(s) to the user + +--- + +## Scope Boundaries + +- Catbox upload logic itself is unchanged — only the routing is new +- No automatic git-add or commit of saved artifacts +- No configurable save path — `$TMPDIR/compound-engineering/ce-demo-reel/` is the fixed default +- No retroactive save of previously captured evidence + +--- + +## Context & Research + +### Relevant Code and Patterns + +- `plugins/compound-engineering/skills/ce-demo-reel/references/upload-and-approval.md` — the 5-step upload flow where the destination choice will be inserted +- `plugins/compound-engineering/skills/ce-demo-reel/scripts/capture-demo.py` — pipeline script with `preview` and `upload` subcommands; will get a new `save-local` subcommand +- `plugins/compound-engineering/skills/ce-demo-reel/SKILL.md` — Step 8 delegates to `upload-and-approval.md`; Output section defines the return format + +### Institutional Learnings + +- **Script-first architecture** (`docs/solutions/skill-design/script-first-skill-architecture.md`): File manipulation (mkdir, copy, path generation) belongs in the Python script, not inline in SKILL.md +- **Prefer Python over bash** (`docs/solutions/best-practices/prefer-python-over-bash-for-pipeline-scripts-2026-04-09.md`): The `save-local` subcommand should be Python, consistent with the existing script + +--- + +## Key Technical Decisions + +- **Destination choice replaces approval gate, not adds to it**: The existing Step 2 approval gate asks "use this / recapture / skip". The new flow asks "upload to catbox / save locally / recapture / skip" — a single merged question, not two sequential prompts. +- **`save-local` as a script subcommand**: Per script-first architecture, the Python script handles directory creation, filename generation, and file copying. The SKILL.md orchestrates the choice and calls the script. +- **Filename format**: `-.` — branch provides context, timestamp prevents collisions. Branch name is sanitized (slashes to dashes, truncated to 60 chars). +- **Output format for local save**: The existing output uses `URL: [public URL]`. For local saves, use `Path: [local path]` instead, so the caller can distinguish between the two. + +--- + +## Open Questions + +### Resolved During Planning + +- **Should preview upload still happen before the choice?** Yes — the user needs to see the artifact to decide. The preview is temporary (1h) and costs nothing if they choose local save. + +### Deferred to Implementation + +- **Exact branch-name sanitization regex**: Implementation detail; follow Python `re.sub` conventions. + +--- + +## Implementation Units + +- [ ] U1. **Add `save-local` subcommand to capture-demo.py** + +**Goal:** Add a script subcommand that copies an artifact to a target directory with a descriptive filename. + +**Requirements:** R3, R4, R5, R6 + +**Dependencies:** None + +**Files:** +- Modify: `plugins/compound-engineering/skills/ce-demo-reel/scripts/capture-demo.py` + +**Approach:** +- Add `save-local` subcommand accepting `--file` (artifact path), `--branch` (branch name), and `--output-dir` (target directory, defaults to `$TMPDIR/compound-engineering/ce-demo-reel/`) +- Create output directory with `os.makedirs(exist_ok=True)` +- Sanitize branch name: replace `/` with `-`, strip non-alphanumeric chars except `-`, truncate to 60 chars +- Generate filename: `-.` where ext comes from the source file +- Copy file with `shutil.copy2` +- Print the final absolute path as the last line of output (matching the convention of `preview` and `upload` which print the URL as last line) +- Register the subcommand in the argparse `main()` block + +**Patterns to follow:** +- `cmd_preview` and `cmd_upload` in the same file — same structure, same error handling with `die()` +- Argparse registration pattern at bottom of file + +**Test scenarios:** +- Happy path: `save-local --file /tmp/demo.gif --branch feat/add-login` creates `$TMPDIR/compound-engineering/ce-demo-reel/feat-add-login-.gif` and prints the path +- Happy path: `save-local --file /tmp/screenshot.png --branch main` creates `$TMPDIR/compound-engineering/ce-demo-reel/main-.png` +- Edge case: branch with deep nesting `feat/team/subsystem/thing` sanitizes to `feat-team-subsystem-thing` +- Edge case: branch name exceeding 60 chars is truncated +- Edge case: output directory does not exist — created automatically +- Error path: source file does not exist — exits with error message + +**Verification:** +- `python3 scripts/capture-demo.py save-local --file --branch test-branch` copies the file and prints the destination path + +--- + +- [ ] U2. **Update upload-and-approval.md to add destination choice** + +**Goal:** Replace the current approval gate with a combined destination-choice question that includes the local save option. + +**Requirements:** R1, R2 + +**Dependencies:** U1 + +**Files:** +- Modify: `plugins/compound-engineering/skills/ce-demo-reel/references/upload-and-approval.md` + +**Approach:** +- Step 1 (preview upload) stays unchanged — user still sees a preview +- Step 2 becomes "Destination Choice" instead of "Approval Gate" +- The blocking question now offers 4 options: + 1. **Upload to catbox (public URL)** — proceeds to Step 3 (promote to permanent, unchanged) + 2. **Save locally** — runs `save-local` subcommand, skips Step 3, goes to cleanup + 3. **Recapture** — unchanged behavior + 4. **Proceed without evidence** — unchanged behavior +- Add a new section "Step 3b: Local Save" that calls `python3 scripts/capture-demo.py save-local --file [ARTIFACT_PATH] --branch [BRANCH]` +- Step 3b captures the printed path and uses it in the output +- Step 5 (cleanup) remains the same — `[RUN_DIR]` is always removed since the artifact has been copied out + +**Patterns to follow:** +- Existing Step 2 approval gate structure (question wording, option format, platform blocking tool instructions) +- Existing Step 3 promote structure (script invocation, output capture) + +**Test scenarios:** +- Happy path: user selects "Save locally" -> `save-local` runs, local path displayed, `[RUN_DIR]` cleaned up +- Happy path: user selects "Upload to catbox" -> existing promote flow runs unchanged +- Happy path: user selects "Recapture" -> returns to tier execution as before +- Integration: multiple static screenshots — each file is saved locally with the same branch prefix but unique timestamps + +**Verification:** +- The approval gate question includes all 4 options with clear descriptions +- "Save locally" branch calls the script and does not invoke catbox upload +- "Upload to catbox" branch is functionally identical to the current behavior + +--- + +- [ ] U3. **Update SKILL.md output format for local saves** + +**Goal:** Extend the output contract to support local file paths alongside URLs. + +**Requirements:** R6 + +**Dependencies:** U2 + +**Files:** +- Modify: `plugins/compound-engineering/skills/ce-demo-reel/SKILL.md` + +**Approach:** +- In the Output section, add `Path` as an alternative to `URL`: + - `URL: [public URL]` when uploaded to catbox (unchanged) + - `Path: [local file path]` when saved locally + - One of the two is present, never both +- Update the note about `URL: "none"` to cover the local case: when saved locally, `URL` is `"none"` but `Path` is populated + +**Patterns to follow:** +- Existing output block format in SKILL.md + +**Test scenarios:** +- Happy path: local save produces output with `Path:` field and `URL: "none"` +- Happy path: catbox upload produces output with `URL:` field and no `Path:` field (unchanged) + +**Verification:** +- Output contract is clear about when `Path` vs `URL` is present +- Callers (e.g., ce-commit-push-pr) can distinguish local from remote evidence + +--- + +## System-Wide Impact + +- **Interaction graph:** ce-commit-push-pr is the primary caller of ce-demo-reel. It currently expects a `URL` in the output to embed in PR descriptions. With local saves, it will receive `Path` instead — it should handle this gracefully (e.g., skip embedding or note that evidence is local-only). +- **Error propagation:** If `save-local` fails (disk full, permission denied), the artifact still exists in `[RUN_DIR]`. The skill should report the error and offer to retry or fall back to catbox upload. +- **Unchanged invariants:** The catbox preview/upload pipeline, tier selection, and capture logic are entirely untouched. + +--- + +## Risks & Dependencies + +| Risk | Mitigation | +|------|------------| +| ce-commit-push-pr doesn't handle `Path` output | Check how ce-commit-push-pr consumes demo-reel output; update if needed (but scoped out of this plan per scope boundaries) | +| OS-temp files cleaned by system reboot | Acceptable — demo reel artifacts are transient; users can `mv` to repo if they want to commit | + +--- + +## Sources & References + +- **Origin document:** [docs/brainstorms/2026-04-22-demo-reel-local-save-requirements.md](docs/brainstorms/2026-04-22-demo-reel-local-save-requirements.md) +- Related code: `plugins/compound-engineering/skills/ce-demo-reel/` +- Learnings: `docs/solutions/skill-design/script-first-skill-architecture.md`, `docs/solutions/best-practices/prefer-python-over-bash-for-pipeline-scripts-2026-04-09.md` diff --git a/plugins/compound-engineering/skills/ce-commit-push-pr/SKILL.md b/plugins/compound-engineering/skills/ce-commit-push-pr/SKILL.md index 8908dd9..3036e8f 100644 --- a/plugins/compound-engineering/skills/ce-commit-push-pr/SKILL.md +++ b/plugins/compound-engineering/skills/ce-commit-push-pr/SKILL.md @@ -194,7 +194,7 @@ Use this branch diff (not the working-tree diff) for the evidence decision. If t **Evidence decision (before delegation).** If the branch diff changes observable behavior (UI, CLI output, API behavior with runnable code, generated artifacts, workflow output) and evidence is not otherwise blocked (unavailable credentials, paid services, deploy-only infrastructure, hardware), ask: "This PR has observable behavior. Capture evidence for the PR description?" -- **Capture now** -- load the `ce-demo-reel` skill with a target description inferred from the branch diff. ce-demo-reel returns `Tier`, `Description`, and `URL`. Note the captured evidence so it can be passed as free-text steering to `ce-pr-description` (e.g., "include the captured demo: as a `## Demo` section") or spliced into the returned body before apply. If capture returns `Tier: skipped` or `URL: "none"`, proceed with no evidence. +- **Capture now** -- load the `ce-demo-reel` skill with a target description inferred from the branch diff. ce-demo-reel returns `Tier`, `Description`, `URL`, and `Path`. Exactly one of `URL` or `Path` contains a real value; the other is `"none"`. If capture returns a public URL, pass it as steering to `ce-pr-description` (e.g., "include the captured demo: as a `## Demo` section") or splice into the returned body before apply. If capture returns a local `Path` instead (user chose local save), pass steering that notes evidence was captured but is local-only (e.g., "evidence was captured locally — note in the PR that a demo was recorded but is not embedded because the user chose local save"). If capture returns `Tier: skipped` or both `URL` and `Path` are `"none"`, proceed with no evidence. - **Use existing evidence** -- ask for the URL or markdown embed, then pass it as free-text steering to `ce-pr-description` or splice in before apply. - **Skip** -- proceed with no evidence section. @@ -202,7 +202,7 @@ When evidence is not possible (docs-only, markdown-only, changelog-only, release **Delegate title and body generation to `ce-pr-description`.** Load the `ce-pr-description` skill: -- **For a new PR** (no existing PR found in Step 3): invoke with `base:/` using the already-resolved base from earlier in this step, so `ce-pr-description` describes the correct commit range even when the branch targets a non-default base (e.g., `develop`, `release/*`). Append any captured-evidence context or user focus as free-text steering (e.g., "include the captured demo: as a `## Demo` section"). +- **For a new PR** (no existing PR found in Step 3): invoke with `base:/` using the already-resolved base from earlier in this step, so `ce-pr-description` describes the correct commit range even when the branch targets a non-default base (e.g., `develop`, `release/*`). Append any captured-evidence context or user focus as free-text steering (e.g., "include the captured demo: as a `## Demo` section", or "evidence captured locally — not embedded" for local saves). - **For an existing PR** (found in Step 3): invoke with the full PR URL from the Step 3 context (e.g., `https://github.com/owner/repo/pull/123`). The URL preserves repo/PR identity even when invoked from a worktree or subdirectory; the skill reads the PR's own `baseRefName` so no `base:` override is needed. Append any focus steering as free text after the URL. **Steering discipline.** Pass only what the diff cannot reveal: a user focus ("emphasize the performance win"), a specific framing concern ("this needs to read as a migration not a feature"), or a pointer to institutional knowledge. Do NOT dump an exhaustive scope summary or a numbered list of every change — `ce-pr-description` reads the diff itself. Over-specified steering encourages the downstream skill to cover everything passed in, producing verbose output. Cap steering at roughly 100 words; if a longer framing feels necessary, trust the diff and cut. diff --git a/plugins/compound-engineering/skills/ce-demo-reel/SKILL.md b/plugins/compound-engineering/skills/ce-demo-reel/SKILL.md index 474c4f2..a8e1963 100644 --- a/plugins/compound-engineering/skills/ce-demo-reel/SKILL.md +++ b/plugins/compound-engineering/skills/ce-demo-reel/SKILL.md @@ -154,12 +154,16 @@ Return these values to the caller (e.g., ce-commit-push-pr): Tier: [browser-reel / terminal-recording / screenshot-reel / static / skipped] Description: [1 sentence describing what the evidence shows] URL: [public URL or "none" (multiple URLs comma-separated for static screenshots)] +Path: [local file path or "none" (multiple paths comma-separated for static screenshots)] === End Evidence === ``` The `Description` is a 1-line summary derived from the capture hypothesis in Step 0 (e.g., "CLI detect command classifying 3 project types and recommending capture tiers"). The caller decides how to format the URL(s) into the PR description. -- `Tier: skipped` or `URL: "none"` means no evidence was captured. +- `Tier: skipped` means no evidence was captured; both `URL` and `Path` are `"none"`. +- When uploaded to catbox: `URL` has the public URL, `Path` is `"none"`. +- When saved locally: `Path` has the local file path, `URL` is `"none"`. +- For all non-skipped tiers, exactly one of `URL` or `Path` contains a real value; the other is `"none"`. **Label convention:** - Browser reel, terminal recording, screenshot reel: label as "Demo" diff --git a/plugins/compound-engineering/skills/ce-demo-reel/references/upload-and-approval.md b/plugins/compound-engineering/skills/ce-demo-reel/references/upload-and-approval.md index 53abc81..6d561cf 100644 --- a/plugins/compound-engineering/skills/ce-demo-reel/references/upload-and-approval.md +++ b/plugins/compound-engineering/skills/ce-demo-reel/references/upload-and-approval.md @@ -1,6 +1,6 @@ # Upload and Approval -Upload a temporary preview for the user to review, then promote to permanent hosting on approval. +Upload a temporary preview for the user to review, then either promote to permanent hosting or save locally based on user choice. ## Step 1: Preview Upload (Temporary) @@ -14,24 +14,33 @@ The last line of output is the preview URL (e.g., `https://litter.catbox.moe/abc For multiple files (static screenshots tier), upload each file separately. -**If upload fails** after retry, fall back to opening the local file with the platform file-opener (`open` on macOS, `xdg-open` on Linux) so the user can still review it. Include the local path in the approval question instead of a URL. +**If upload fails** after retry, fall back to opening the local file with the platform file-opener (`open` on macOS, `xdg-open` on Linux) so the user can still review it. Include the local path in the destination choice question instead of a URL. -## Step 2: Approval Gate +## Step 2: Destination Choice -Present the preview URL to the user for approval. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. +Present the preview URL to the user and ask how to handle the evidence. Use the platform's blocking question tool: `AskUserQuestion` in Claude Code (call `ToolSearch` with `select:AskUserQuestion` first if its schema isn't loaded), `request_user_input` in Codex, `ask_user` in Gemini, `ask_user` in Pi (requires the `pi-ask-user` extension). Fall back to presenting options in chat only when no blocking tool exists in the harness or the call errors (e.g., Codex edit modes) — not because a schema load is required. Never silently skip the question. -**Question:** "Evidence preview (1h link): [PREVIEW_URL]" +**Question:** "Evidence preview (1h link): [PREVIEW_URL]. Where should the evidence go?" **Options:** -1. **Use this in the PR** -- promote to permanent hosting -2. **Recapture** -- provide instructions on what to change -3. **Proceed without evidence** -- set evidence to null and proceed +1. **Upload to catbox (public URL)** -- promote to permanent hosting for PR embedding +2. **Save locally** -- save to a stable OS-temp path ($TMPDIR/compound-engineering/ce-demo-reel/) +3. **Recapture** -- provide instructions on what to change +4. **Proceed without evidence** -- set evidence to null and proceed If the question tool is unavailable (headless/background mode), present the numbered options and wait for the user's reply before proceeding. +### On "Upload to catbox (public URL)" + +Proceed to Step 3: Promote to Permanent Hosting. + +### On "Save locally" + +Proceed to Step 3b: Local Save. + ### On "Recapture" -Return to the tier execution step. The user's instructions guide what to change in the next capture attempt. After recapture, upload a new preview and repeat the approval gate. +Return to the tier execution step. The user's instructions guide what to change in the next capture attempt. After recapture, upload a new preview and repeat the destination choice. ### On "Proceed without evidence" @@ -39,7 +48,7 @@ Set evidence to null and proceed. The preview link expires on its own. ## Step 3: Promote to Permanent Hosting -After the user approves, upload to permanent catbox hosting. The command accepts either the preview URL (preferred) or the local file path (fallback): +After the user selects "Upload to catbox", upload to permanent catbox hosting. The command accepts either the preview URL (preferred) or the local file path (fallback): ```bash python3 scripts/capture-demo.py upload [PREVIEW_URL or ARTIFACT_PATH] @@ -51,10 +60,26 @@ The last line of output is the permanent URL (e.g., `https://files.catbox.moe/ab For multiple files, promote each separately. +## Step 3b: Local Save + +After the user selects "Save locally", save the artifact to the default OS-temp path using the pipeline script: + +```bash +python3 scripts/capture-demo.py save-local --file [ARTIFACT_PATH] --branch [BRANCH_NAME] +``` + +Determine `[BRANCH_NAME]` from `git branch --show-current` or the PR context discovered in Step 0 of the SKILL.md. + +The last line of output is the absolute path of the saved file. Use this path in the output. + +For multiple files (static screenshots tier), save each file separately. + +**If save fails** (permission denied, disk full), report the error and offer to retry or fall back to catbox upload (Step 3). + ## Step 4: Return Output -Return the structured output defined in the SKILL.md Output section: `Tier`, `Description`, and `URL` (the permanent catbox URL). The caller formats the evidence into the PR description. ce-demo-reel does not generate markdown. +Return the structured output defined in the SKILL.md Output section: `Tier`, `Description`, and either `URL` (permanent catbox URL) or `Path` (local file path). The caller formats the evidence into the PR description. ce-demo-reel does not generate markdown. ## Step 5: Cleanup -Remove the `[RUN_DIR]` scratch directory and all temporary files. Preserve nothing -- the evidence lives at the permanent URL now. +Remove the `[RUN_DIR]` scratch directory and all temporary files. Preserve nothing -- the evidence lives at the permanent URL or has been copied to the local save path. diff --git a/plugins/compound-engineering/skills/ce-demo-reel/scripts/capture-demo.py b/plugins/compound-engineering/skills/ce-demo-reel/scripts/capture-demo.py index 95a0acf..1928964 100755 --- a/plugins/compound-engineering/skills/ce-demo-reel/scripts/capture-demo.py +++ b/plugins/compound-engineering/skills/ce-demo-reel/scripts/capture-demo.py @@ -11,15 +11,18 @@ Subcommands: terminal-recording --output OUT --tape TAPE Run VHS tape file preview FILE Upload to litterbox (1h expiry) for preview upload FILE_OR_URL Upload/promote to catbox.moe (permanent) + save-local --file F --branch B Save artifact locally instead of uploading """ import argparse import json import os +import re import shutil import subprocess import sys import tempfile import time +from datetime import datetime, timezone from pathlib import Path @@ -640,6 +643,35 @@ def cmd_upload(args): _upload_with_retry(CATBOX_API, source, "catbox.moe") +# --- Save local --- + +def _sanitize_branch(branch): + sanitized = branch.replace("/", "-") + sanitized = re.sub(r"[^a-zA-Z0-9_-]", "", sanitized) + sanitized = re.sub(r"-+", "-", sanitized).strip("-") + return sanitized[:60] + + +def cmd_save_local(args): + src = Path(args.file) + if not src.exists(): + die(f"File not found: {src}") + + output_dir = Path(args.output_dir) + os.makedirs(output_dir, exist_ok=True) + + branch_part = _sanitize_branch(args.branch) if args.branch else "unknown" + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f") + stem = re.sub(r"[^a-zA-Z0-9_-]", "", src.stem)[:40] or "artifact" + filename = f"{branch_part}-{timestamp}-{stem}{src.suffix}" + dest = output_dir / filename + + shutil.copy2(src, dest) + dest_abs = str(dest.resolve()) + print(f"Saved: {dest_abs}") + print(dest_abs) + + # --- Main --- def main(): @@ -656,6 +688,7 @@ Commands: terminal-recording --output O --tape T Run VHS tape preview FILE Upload to litterbox (1h expiry) upload FILE_OR_URL Upload/promote to catbox.moe (permanent) + save-local --file F --branch B Save artifact locally instead of uploading """, ) sub = parser.add_subparsers(dest="command") @@ -702,6 +735,13 @@ Commands: p_upload = sub.add_parser("upload", help="Upload or promote to catbox.moe (permanent)") p_upload.add_argument("source", help="Local file path or URL to promote") + # save-local + p_save = sub.add_parser("save-local", help="Save artifact locally instead of uploading") + p_save.add_argument("--file", required=True, help="Artifact file to save") + p_save.add_argument("--branch", default="", help="Branch name for filename") + default_dir = str(Path(os.environ.get("TMPDIR", "/tmp")) / "compound-engineering" / "ce-demo-reel") + p_save.add_argument("--output-dir", default=default_dir, help="Target directory") + args = parser.parse_args() if not args.command: @@ -717,6 +757,7 @@ Commands: "terminal-recording": cmd_terminal_recording, "preview": cmd_preview, "upload": cmd_upload, + "save-local": cmd_save_local, } dispatch[args.command](args)