Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cross-platform-final-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@prover-coder-ai/docker-git": patch
"@prover-coder-ai/docker-git-session-sync": patch
---

Add portable launch/build scripts and CI final-build verification across Linux, macOS, and Windows.
1 change: 1 addition & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ runs:
with:
node-version: ${{ inputs.node-version }}
- name: Install OpenSSH client
if: runner.os == 'Linux'
shell: bash
run: |
if command -v ssh >/dev/null 2>&1 && command -v ssh-keygen >/dev/null 2>&1; then
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/final-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Final Build

on:
workflow_dispatch:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
final-build:
name: Final build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- name: Install dependencies
uses: ./.github/actions/setup
with:
bun-version: 1.3.11
node-version: 24.14.0
- name: Build final workspace packages
run: bun run build
- name: Verify docker-git CLI starts
run: bun ./packages/app/dist/src/docker-git/main.js --help
- name: Verify session sync CLI starts
run: bun ./packages/docker-git-session-sync/dist/docker-git-session-sync.js --help
- name: Prepare package artifacts directory
Comment on lines +27 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Final Build is missing the requested browser/UI clone smoke check.

The workflow currently validates CLI startup only. It does not cover the reviewer-requested runtime path: browser launch and cloning via UI/menu, so the PR acceptance criteria in discussion remain partially unverified.

🤖 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 @.github/workflows/final-build.yml around lines 27 - 33, The workflow
.github/workflows/final-build.yml currently only checks CLIs ("Verify docker-git
CLI starts" and "Verify session sync CLI starts"); add a new step after those
that performs the requested browser/UI clone smoke check by launching a headless
browser and exercising the UI clone path (open app served from the build,
navigate to the clone UI/menu, trigger a clone, and assert success). Name the
step clearly (e.g., "Verify browser UI clone smoke check") and implement it
using your existing test runner or a lightweight headless tool
(Playwright/puppeteer or an npm script) so it starts the built web app, runs the
UI clone flow, and fails the job on error.

run: |
node -e "require('node:fs').mkdirSync('artifacts', { recursive: true })"
- name: Pack docker-git package
working-directory: packages/app
run: bun pm pack --quiet --ignore-scripts --destination ../../artifacts
- name: Pack session sync package
working-directory: packages/docker-git-session-sync
run: bun pm pack --quiet --ignore-scripts --destination ../../artifacts
- name: Upload final build artifacts
uses: actions/upload-artifact@v7
with:
name: final-build-${{ matrix.os }}
path: artifacts/*.tgz
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
"changeset": "changeset",
"changeset-publish": "bun -e \"if (!process.env.NPM_TOKEN) { console.log('Skipping publish: NPM_TOKEN is not set'); process.exit(0); }\" && changeset publish",
"changeset-version": "changeset version",
"clone": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js clone \"$@\"' --",
"open": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js open \"$@\"' --",
"docker-git": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --",
"clone": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js clone",
"open": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js open",
"docker-git": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js",
"skiller:init": "git submodule update --init --checkout third_party/skiller-desktop-skills-manager && bun scripts/skiller-apply-docker-git-patches.mjs",
"skiller:install": "bun install --cwd third_party/skiller-desktop-skills-manager --frozen-lockfile",
"skiller:dev": "bun run --cwd third_party/skiller-desktop-skills-manager dev",
Expand All @@ -36,7 +36,7 @@
"e2e:login-context": "bash scripts/e2e/login-context.sh",
"e2e:runtime-volumes-ssh": "bash scripts/e2e/runtime-volumes-ssh.sh",
"e2e:opencode-autoconnect": "bash scripts/e2e/opencode-autoconnect.sh",
"list": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js ps \"$@\"' --",
"list": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js ps",
"dev": "bun run --cwd packages/app dev",
"web:dev": "bun run --cwd packages/app dev:web",
"web:build": "bun run --cwd packages/app build:web",
Expand All @@ -47,7 +47,7 @@
"lint:effect": "bun run --filter @prover-coder-ai/docker-git lint:effect && bun run --filter @effect-template/lib lint:effect",
"test": "bun run --filter @prover-coder-ai/docker-git-session-sync test && bun run --filter @prover-coder-ai/docker-git test && bun run --filter @effect-template/lib test",
"typecheck": "bun run --filter @prover-coder-ai/docker-git-session-sync typecheck && bun run --filter @prover-coder-ai/docker-git typecheck && bun run --filter @effect-template/lib typecheck",
"start": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --"
"start": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js"
},
"devDependencies": {
"@changesets/changelog-github": "^0.7.0",
Expand Down
10 changes: 5 additions & 5 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
"prebuild:docker-git": "bun install --cwd ../.. && bun run --cwd ../docker-git-session-sync build && bun run --cwd ../lib build",
"build:docker-git": "vite build --config vite.docker-git.config.ts",
"check": "bun run typecheck",
"clone": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js clone \"$@\"' --",
"open": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js open \"$@\"' --",
"docker-git": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js \"$@\"' --",
"list": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js ps \"$@\"' --",
"clone": "bun run build:docker-git && bun dist/src/docker-git/main.js clone",
"open": "bun run build:docker-git && bun dist/src/docker-git/main.js open",
"docker-git": "bun run build:docker-git && bun dist/src/docker-git/main.js",
"list": "bun run build:docker-git && bun dist/src/docker-git/main.js ps",
"preview:web": "vite preview --config vite.web.config.ts",
"start": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js \"$@\"' --",
"start": "bun run build:docker-git && bun dist/src/docker-git/main.js",
"pretest": "bun run --cwd ../docker-git-session-sync build && bun run --cwd ../lib build",
"test": "bun run lint:tests && vitest run",
"pretypecheck": "bun run --cwd ../docker-git-session-sync build && bun run --cwd ../lib build",
Expand Down
13 changes: 12 additions & 1 deletion packages/app/src/lib/core/templates-entrypoint/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ const renderCloneAuthRepoUrl = (): string =>
AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${RESOLVED_GIT_AUTH_USER}:\${RESOLVED_GIT_AUTH_TOKEN}@#")"
fi`

// CHANGE: restrict clone-cache mirror refresh to branch and tag refs
// WHY: broad refs include hosted forge PR refs and make cache reuse proportional to every remote ref
// QUOTE(ТЗ): "Для тестов можно реализовать CI/CD workflow для Linux, MAC, Windows"
// REF: issue-278-ci-check-clone-cache
// SOURCE: n/a
// FORMAT THEOREM: forall r in refreshedRefs: r in refs/heads/* union refs/tags/*
// PURITY: CORE
// INVARIANT: clone-cache refresh never requests refs/pull/* or refs/merge-requests/*
// COMPLEXITY: O(|heads| + |tags|)
const cloneCacheRefreshRefspecs = "'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'"

const renderCloneCacheInit = (config: TemplateConfig): string =>
` CLONE_CACHE_ARGS=""
CACHE_REPO_DIR=""
Expand All @@ -135,7 +146,7 @@ const renderCloneCacheInit = (config: TemplateConfig): string =>
chown 1000:1000 "$CACHE_ROOT" || true
if [[ -d "$CACHE_REPO_DIR" ]]; then
if su - ${config.sshUser} -c "git --git-dir '$CACHE_REPO_DIR' rev-parse --is-bare-repository >/dev/null 2>&1"; then
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' '+refs/*:refs/*'"; then
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' ${cloneCacheRefreshRefspecs}"; then
echo "[clone-cache] mirror refresh failed for $REPO_URL"
fi
CLONE_CACHE_ARGS="--reference-if-able '$CACHE_REPO_DIR' --dissociate"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from "@effect/vitest"

import rootPackage from "../../../../package.json" with { type: "json" }
import sessionSyncPackage from "../../../docker-git-session-sync/package.json" with { type: "json" }
import appPackage from "../../package.json" with { type: "json" }

const launchScripts: ReadonlyArray<Readonly<{ packageName: string; scriptName: string; script: string }>> = [
{ packageName: "workspace", scriptName: "clone", script: rootPackage.scripts.clone },
{ packageName: "workspace", scriptName: "open", script: rootPackage.scripts.open },
{ packageName: "workspace", scriptName: "docker-git", script: rootPackage.scripts["docker-git"] },
{ packageName: "workspace", scriptName: "list", script: rootPackage.scripts.list },
{ packageName: "workspace", scriptName: "start", script: rootPackage.scripts.start },
{ packageName: "@prover-coder-ai/docker-git", scriptName: "clone", script: appPackage.scripts.clone },
{ packageName: "@prover-coder-ai/docker-git", scriptName: "open", script: appPackage.scripts.open },
{
packageName: "@prover-coder-ai/docker-git",
scriptName: "docker-git",
script: appPackage.scripts["docker-git"]
},
{ packageName: "@prover-coder-ai/docker-git", scriptName: "list", script: appPackage.scripts.list },
{ packageName: "@prover-coder-ai/docker-git", scriptName: "start", script: appPackage.scripts.start }
]

describe("package scripts cross-platform contract", () => {
it("keeps user-facing launch scripts independent from bash", () => {
for (const entry of launchScripts) {
expect(entry.script, `${entry.packageName}:${entry.scriptName}`).not.toMatch(/\bbash(?:\.exe)?\b/u)
}
})

it("keeps final package build independent from raw chmod", () => {
expect(sessionSyncPackage.scripts.build).not.toMatch(/\bchmod\s+/u)
})
})
Comment on lines +24 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add property-based assertions for script invariants.

This test is table-driven only; please add fast-check property tests for the bash/chmod invariants to satisfy the test policy for .test.ts files.

Proposed direction
+import fc from "fast-check"
...
+  it("bash-free launch scripts hold for all declared launch entries", () => {
+    fc.assert(
+      fc.property(fc.constantFrom(...launchScripts), (entry) => {
+        expect(entry.script).not.toMatch(/\bbash(?:\.exe)?\b/u)
+      })
+    )
+  })

As per coding guidelines "**/*.test.{ts,tsx}: Implement property-based testing using fast-check for mathematical properties and invariants."

🤖 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 `@packages/app/tests/docker-git/package-scripts-cross-platform.test.ts` around
lines 24 - 34, Add fast-check property-based assertions covering the same
invariants currently tested by the table-driven checks: the "no bash" invariant
for each launch script and the "no chmod" invariant for the package build
script. Use fast-check's fc.assert with an fc.property that generates indices or
script entries from the existing launchScripts array and validates that the
selected entry.script does not match /\bbash(?:\.exe)?\b/u (referencing
launchScripts and entry.scriptName/entry.packageName for contextual failure
messages), and another property that checks sessionSyncPackage.scripts.build
does not match /\bchmod\s+/u. Replace or augment the existing for-loop and
single expect with these fc.assert(fc.property(...)) calls so the test file
package-scripts-cross-platform.test.ts uses property-based testing for the
invariants.

2 changes: 1 addition & 1 deletion packages/docker-git-session-sync/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"dist"
],
"scripts": {
"build": "vite build && chmod +x dist/docker-git-session-sync.js",
"build": "vite build && bun ../../scripts/mark-executable.mjs dist/docker-git-session-sync.js",
"check": "bun run typecheck",
"prepack": "bun run build",
"test": "vitest run --passWithNoTests",
Expand Down
13 changes: 12 additions & 1 deletion packages/lib/src/core/templates-entrypoint/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ const renderCloneAuthRepoUrl = (): string =>
AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${RESOLVED_GIT_AUTH_USER}:\${RESOLVED_GIT_AUTH_TOKEN}@#")"
fi`

// CHANGE: restrict clone-cache mirror refresh to branch and tag refs
// WHY: broad refs include hosted forge PR refs and make cache reuse proportional to every remote ref
// QUOTE(ТЗ): "Для тестов можно реализовать CI/CD workflow для Linux, MAC, Windows"
// REF: issue-278-ci-check-clone-cache
// SOURCE: n/a
// FORMAT THEOREM: forall r in refreshedRefs: r in refs/heads/* union refs/tags/*
// PURITY: CORE
// INVARIANT: clone-cache refresh never requests refs/pull/* or refs/merge-requests/*
// COMPLEXITY: O(|heads| + |tags|)
const cloneCacheRefreshRefspecs = "'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'"

const renderCloneCacheInit = (config: TemplateConfig): string =>
` CLONE_CACHE_ARGS=""
CACHE_REPO_DIR=""
Expand All @@ -135,7 +146,7 @@ const renderCloneCacheInit = (config: TemplateConfig): string =>
chown 1000:1000 "$CACHE_ROOT" || true
if [[ -d "$CACHE_REPO_DIR" ]]; then
if su - ${config.sshUser} -c "git --git-dir '$CACHE_REPO_DIR' rev-parse --is-bare-repository >/dev/null 2>&1"; then
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' '+refs/*:refs/*'"; then
if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' ${cloneCacheRefreshRefspecs}"; then
echo "[clone-cache] mirror refresh failed for $REPO_URL"
fi
CLONE_CACHE_ARGS="--reference-if-able '$CACHE_REPO_DIR' --dissociate"
Expand Down
11 changes: 11 additions & 0 deletions packages/lib/tests/core/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ describe("renderDockerfile", () => {
})
})

describe("renderEntrypoint clone cache", () => {
it("refreshes mirrors without broad remote refs", () => {
const entrypoint = renderEntrypoint(makeTemplateConfig())

expect(entrypoint).toContain("git --git-dir '$CACHE_REPO_DIR' fetch")
expect(entrypoint).toContain("'+refs/heads/*:refs/heads/*'")
expect(entrypoint).toContain("'+refs/tags/*:refs/tags/*'")
expect(entrypoint).not.toContain("'+refs/*:refs/*'")
})
})
Comment on lines +77 to +86
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the recommended way to run fast-check property-based tests in Vitest 4.1.5 (including TypeScript examples)?

💡 Result:

Recommended way: use fast-check through the dedicated Vitest connector package @fast-check/vitest, and run property-based “prop” tests via the connector’s enriched test/it APIs (e.g., test.prop / it.prop). This is the safest/simplest integration because the connector wires up Vitest-specific concerns like timeouts and lifecycle hooks. [1][2] Setup (TypeScript) 1) Install: - npm i -D vitest fast-check @fast-check/vitest [1] 2) Write property-based tests using @fast-check/vitest instead of importing test/it directly from vitest. The connector provides.prop variants for property inputs. Example: tuple (positional) notation ts // your-test.spec.ts import { test, expect, fc } from '@fast-check/vitest'; a function isSubstring(text: string, pattern: string) { return text.includes(pattern); } test.prop([fc.string(), fc.string(), fc.string()])( 'should detect substring', (a, b, c) => { const text = a + b + c; return expect(isSubstring(text, b)).toBe(true); }, ); Property inputs can be provided positionally with test.prop([arb1, arb2,...]). [1][2] Example: record (named) notation ts import { test, expect, fc } from '@fast-check/vitest'; function isSubstring(text: string, pattern: string) { return text.includes(pattern); } test.prop({ a: fc.string(), b: fc.string(), c: fc.string() })( 'should detect substring', ({ a, b, c }) => { const text = a + b + c; expect(isSubstring(text, b)).toBe(true); }, ); Record notation passes inputs as a named object to the predicate. [1][2] Reproducibility / replaying failures (seed) You can pass fast-check parameters (including a seed) to test.prop so failures are replayable. [2] Example (seeded run) ts import { test, expect, fc } from '@fast-check/vitest'; test.prop([fc.nat(), fc.nat()], { seed: 4242 })( 'should replay the test for the seed 4242', (a, b) => { expect(a + b === b + a).toBe(true); }, ); [2] How this relates to “fast” execution in Vitest 4.1.5 - The recommended approach is to delegate property-test orchestration to @fast-check/vitest; it takes care of Vitest integration details (timeouts and beforeEach/afterEach lifecycle wiring) so property predicates run correctly under Vitest’s execution model. [1][2] - For more extensive property testing, @fast-check/vitest supports a full property-based mode that repeatedly generates inputs to find edge cases. [2] Alternative (not recommended for Vitest): using fast-check directly fast-check itself works with any runner, but fast-check’s Vitest docs specifically recommend the connector as the “safest and simplest path” for Vitest integration, rather than using Vitest’s test/it directly. [1]

Citations:


🏁 Script executed:

# First, locate the test file and examine its content
find . -path "*/packages/lib/tests/core/templates.test.ts" -type f

Repository: ProverCoderAI/docker-git

Length of output: 112


🏁 Script executed:

# Check if file exists using git ls-files as fallback
git ls-files | grep -E "templates\.test\.ts"

Repository: ProverCoderAI/docker-git

Length of output: 110


🏁 Script executed:

# Search for templates test file more broadly
fd "templates.test.ts" --type f

Repository: ProverCoderAI/docker-git

Length of output: 110


🏁 Script executed:

# Look for the renderEntrypoint function
rg "renderEntrypoint" -t ts -t tsx -A 5 --max-count 20

Repository: ProverCoderAI/docker-git

Length of output: 96


🏁 Script executed:

# Check project dependencies for Vitest and fast-check
cat package.json | jq '.devDependencies | to_entries[] | select(.key | test("vitest|fast-check")) | "\(.key): \(.value)"'

Repository: ProverCoderAI/docker-git

Length of output: 50


🏁 Script executed:

# Search for existing fast-check usage in test files
fd ".test.ts" -o ".spec.ts" --type f -x rg "fast-check|fc\." {} --max-count 5

Repository: ProverCoderAI/docker-git

Length of output: 204


🏁 Script executed:

# Read the test file
cat -n packages/lib/tests/core/templates.test.ts | head -100

Repository: ProverCoderAI/docker-git

Length of output: 5439


🏁 Script executed:

# Search for renderEntrypoint function definition
rg "function renderEntrypoint|const renderEntrypoint|export.*renderEntrypoint" -A 10

Repository: ProverCoderAI/docker-git

Length of output: 50380


🏁 Script executed:

# Check package.json for dependencies
cat package.json | head -100

Repository: ProverCoderAI/docker-git

Length of output: 4503


🏁 Script executed:

# Look for fast-check imports or usage
rg "from.*fast-check|import.*fc\." --type ts

Repository: ProverCoderAI/docker-git

Length of output: 50


🏁 Script executed:

# Find renderCloneBody and git ref generation logic
rg "renderCloneBody|git.*fetch.*refs|refs/heads|refs/tags" -A 3 -B 2

Repository: ProverCoderAI/docker-git

Length of output: 17230


🏁 Script executed:

# Check if fast-check is mentioned anywhere in the codebase (even if not installed)
rg "fast-check|fc\." --type ts --max-count 10

Repository: ProverCoderAI/docker-git

Length of output: 50


🏁 Script executed:

# Look at the package.json for lib package specifically
cat packages/lib/package.json | head -150

Repository: ProverCoderAI/docker-git

Length of output: 3283


Add explicit negative assertions for merge-request and pull-request refs.

The test validates the clone-cache refspec invariant (documented in tasks.ts) but is incomplete. Add expect(entrypoint).not.toContain("'+refs/pull/*:refs/pull/*'") and expect(entrypoint).not.toContain("'+refs/merge-requests/*:refs/merge-requests/*'") to prevent regression if those refs are accidentally reintroduced.

🤖 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 `@packages/lib/tests/core/templates.test.ts` around lines 77 - 86, The test for
"renderEntrypoint clone cache" is missing negative assertions for pull-request
and merge-request refspecs; update the spec for the entrypoint returned by
renderEntrypoint(makeTemplateConfig()) to assert it does not contain the
undesired refs by adding checks that entrypoint does not contain
"'+refs/pull/*:refs/pull/*'" and
"'+refs/merge-requests/*:refs/merge-requests/*'". Ensure you add these two
expect(...).not.toContain(...) assertions alongside the existing negative check
so the test prevents regressions.


describe("renderEntrypointGitHooks", () => {
it("installs pre-push protection checks and a global git post-push runtime", () => {
const hooks = renderEntrypointGitHooks()
Expand Down
23 changes: 23 additions & 0 deletions scripts/mark-executable.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bun

import { chmodSync } from "node:fs"
import { resolve } from "node:path"

// CHANGE: centralize executable-bit handling for generated CLI files.
// WHY: POSIX chmod is not available on Windows, while Linux/macOS package builds require executable bins.
// QUOTE(TZ): "run conveniently on Windows and Linux"
// REF: issue-278
// SOURCE: n/a
// FORMAT THEOREM: forall p in Paths: platform=win32 -> no_posix_chmod(p), platform!=win32 -> executable(p)
// PURITY: SHELL
// EFFECT: filesystem metadata update
// INVARIANT: missing target argument exits non-zero; Windows builds do not invoke POSIX chmod.
// COMPLEXITY: O(1)/O(1)
const target = process.argv[2]

if (target === undefined || target.length === 0) {
process.stderr.write("Usage: mark-executable <path>\n")
process.exitCode = 1
} else if (process.platform !== "win32") {
chmodSync(resolve(process.cwd(), target), 0o755)
}
Loading