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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,22 @@ jobs:

- name: Run npm-install smoke test
run: bash scripts/e2e-smoke.sh

docker-e2e:
name: Docker E2E (clean env gate)
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run Docker E2E test
run: bash scripts/e2e-docker.sh
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:docker": "bash scripts/e2e-docker.sh",
"prepublishOnly": "npm run build"
},
"keywords": [
Expand Down
92 changes: 92 additions & 0 deletions scripts/e2e-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# E2E Docker test: builds a clean Ubuntu container, installs squads-cli from
# tarball (like a real user), and runs smoke tests inside it.
#
# Catches issues that local tests miss:
# - Missing files in npm package
# - Broken bin entry / shebang
# - Node version incompatibilities
# - Permission issues (non-root user)
# - Missing runtime dependencies
#
# Usage: bash scripts/e2e-docker.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DOCKERFILE="$REPO_ROOT/test/e2e/Dockerfile.first-run"
IMAGE_NAME="squads-cli-e2e"

echo "▶ Building package..."
cd "$REPO_ROOT"
npm run build

echo "▶ Packing tarball..."
TARBALL=$(npm pack --quiet)
TARBALL_PATH="$REPO_ROOT/$TARBALL"

cleanup() {
echo "▶ Cleaning up..."
rm -f "$TARBALL_PATH"
docker rmi "$IMAGE_NAME" 2>/dev/null || true
}
trap cleanup EXIT

echo "▶ Building Docker image (clean Ubuntu + squads-cli from tarball)..."
docker build \
-f "$DOCKERFILE" \
--build-arg "TARBALL=$TARBALL" \
-t "$IMAGE_NAME" \
"$REPO_ROOT"

echo "▶ Running tests inside container..."
docker run --rm "$IMAGE_NAME" bash -c '
set -euo pipefail

step() { echo ""; echo "=== STEP: $1 ==="; }

step "squads --version"
squads --version

step "squads --help"
squads --help | head -20

step "squads init --yes --force"
mkdir -p /tmp/test-project && cd /tmp/test-project
git init -q && git commit --allow-empty -q -m "init"
squads init --yes --force

step "squads status"
squads status

step "squads run --status (daemon status)"
squads run --status

step "squads run --dry-run --once (autopilot preview)"
squads run --dry-run --once || true

step "squads run company --dry-run (single squad preview)"
squads run company --dry-run || true

step "squads run --pause \"e2e test\""
squads run --pause "e2e test"

step "squads run --status (should show paused)"
squads run --status

step "squads run --resume"
squads run --resume

step "squads run --status (should show not running)"
squads run --status

step "squads autonomous status (deprecated alias)"
squads autonomous status || true

step "squads doctor"
squads doctor || true

Choose a reason for hiding this comment

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

medium

The use of || true for squads doctor can mask unexpected failures in the E2E test. The doctor command is intended to diagnose environment issues, and it should ideally pass. If it fails for an unexpected reason, || true will prevent the CI from catching the problem.

If a non-zero exit code is expected in this test environment, it would be more robust to check for that specific exit code rather than ignoring all errors. If it's expected to succeed, || true should be removed.

For example, if a specific failure is expected:

# If doctor is expected to fail with a specific code, e.g., 1
set +e
squads doctor
exit_code=$?
set -e
if [[ $exit_code -ne 1 ]]; then # Assuming 1 is the expected error code
  echo "squads doctor exited with $exit_code, but expected 1"
  exit 1
fi

This makes the test's intent clearer and more reliable.


echo ""
echo "✅ All Docker E2E tests passed"
'
97 changes: 97 additions & 0 deletions scripts/scrub-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────
# Git history secret scrub for squads-cli (PUBLIC REPO)
#
# Removes leaked secrets from git history using git-filter-repo.
# This rewrites history — all collaborators must re-clone after.
#
# Prerequisites:
# brew install git-filter-repo (or pip install git-filter-repo)
#
# What gets scrubbed:
# 1. Telemetry API key (base64-encoded)
# 2. Telemetry endpoint URL (base64-encoded)
# 3. Local DB credentials (two connection strings)
#
# Usage:
# 1. Review this script
# 2. Run: bash scripts/scrub-secrets.sh
# 3. Verify: git log -p --all -S '<pattern>' should return nothing
# 4. Force push: git push --force --all && git push --force --tags
# ─────────────────────────────────────────────────────────────────────

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"

# Preflight
if ! command -v git-filter-repo &>/dev/null; then
echo "ERROR: git-filter-repo not found"
echo "Install: brew install git-filter-repo"
exit 1
fi

# Safety: must be on a clean working tree
if [ -n "$(git status --porcelain)" ]; then
echo "ERROR: Working tree not clean. Commit or stash changes first."
exit 1
fi

echo "=== Strings to scrub from git history ==="
echo ""
echo "1. Telemetry API key (base64): c3FfdGVsX3YxXzdmOGE5YjJjM2Q0ZTVmNmE="
echo "2. Telemetry endpoint (base64): aHR0cHM6Ly9zcXVhZHMtdGVsZW1ldHJ5LTk3ODg3MTgxNzYxMC51cy1jZW50cmFsMS5ydW4uYXBwL3Bpbmc="
echo "3. DB credential 1: postgresql://user:password@localhost:5432/squads"
echo "4. DB credential 2: postgresql://squads:squads_local_dev@localhost:5433/squads"
echo ""
echo "This will REWRITE git history. All collaborators must re-clone."
echo ""
read -p "Continue? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Aborted."
exit 0
fi

# Create the replacements file
REPLACEMENTS=$(mktemp)
cat > "$REPLACEMENTS" <<'REPLACE'
c3FfdGVsX3YxXzdmOGE5YjJjM2Q0ZTVmNmE===>REDACTED_TELEMETRY_KEY
aHR0cHM6Ly9zcXVhZHMtdGVsZW1ldHJ5LTk3ODg3MTgxNzYxMC51cy1jZW50cmFsMS5ydW4uYXBwL3Bpbmc===>REDACTED_TELEMETRY_ENDPOINT
postgresql://user:password@localhost:5432/squads==>REDACTED_DB_URL
postgresql://squads:squads_local_dev@localhost:5433/squads==>REDACTED_DB_URL
REPLACE

echo "▶ Running git-filter-repo with blob replacements..."
git filter-repo --replace-text "$REPLACEMENTS" --force

rm -f "$REPLACEMENTS"

echo ""
echo "=== Verification ==="
echo ""

# Verify scrub worked
FOUND=0
for pattern in "c3FfdGVsX3YxXzdm" "aHR0cHM6Ly9zcXVhZHMtdGVsZW1ldHJ5" "user:password@localhost" "squads_local_dev"; do
HITS=$(git log --all -p | grep -c "$pattern" 2>/dev/null || true)
if [ "$HITS" -gt 0 ]; then
echo "FAIL: '$pattern' still found $HITS time(s)"
FOUND=1
else
echo "OK: '$pattern' scrubbed"
fi
done

echo ""
if [ "$FOUND" -eq 0 ]; then
echo "All secrets scrubbed from git history."
echo ""
echo "Next steps:"
echo " 1. git push --force --all"
echo " 2. git push --force --tags"
echo " 3. Tell collaborators to re-clone (git pull won't work after rewrite)"
echo " 4. Rotate the telemetry API key server-side (if not already done)"
else
echo "WARNING: Some secrets still found. Manual investigation needed."
fi
57 changes: 49 additions & 8 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { applyStackConfig } from './lib/stack-config.js';
// Register-pattern commands (must define subcommand structure before parseAsync)
import { registerOrchestrateCommand } from './commands/orchestrate.js';
import { registerTriggerCommand } from './commands/trigger.js';
import { registerAutonomousCommand } from './commands/autonomous.js';
// autonomous.ts removed — daemon lifecycle consolidated into run-modes.ts
import { registerApprovalCommand } from './commands/approval.js';
import { registerDeployCommand } from './commands/deploy.js';
import { registerEvalCommand } from './commands/eval.js';
Expand Down Expand Up @@ -303,6 +303,10 @@ program
.option('--once', 'Autopilot: run one cycle then exit')
.option('--phased', 'Autopilot: use dependency-based phase ordering (from SQUAD.md depends_on)')
.option('--no-eval', 'Skip post-run COO evaluation')
.option('--stop', 'Stop running daemon')
.option('--status', 'Show daemon status, running agents, next routines')
.option('--pause [reason]', 'Pause daemon without stopping')
.option('--resume', 'Resume daemon after pause')
.addHelpText('after', `
Examples:
$ squads run engineering Run squad conversation (lead → scan → work → review)
Expand All @@ -316,9 +320,13 @@ Examples:
$ squads run engineering -w Run in background but tail logs
$ squads run research --provider=google Use Gemini CLI instead of Claude
$ squads run engineering/issue-solver --cloud Dispatch to cloud worker
$ squads run Autopilot mode (watch → decide → dispatch → learn)
$ squads run --once --dry-run Preview one autopilot cycle
$ squads run -i 15 --budget 50 Autopilot: 15min cycles, $50/day cap
$ squads run Daemon mode (cron routines + scoring + dispatch)
$ squads run --once --dry-run Preview one cycle then exit
$ squads run -i 15 --budget 50 Custom: 15min cycles, $50/day cap
$ squads run --status Show daemon status and next routines
$ squads run --stop Stop running daemon
$ squads run --pause "quota" Pause daemon without stopping
$ squads run --resume Resume after pause
`)
.action(async (target, options) => {
const { runCommand } = await import('./commands/run.js');
Expand Down Expand Up @@ -711,7 +719,7 @@ program
program
.command('autopilot')
.alias('daemon')
.description('[deprecated] Use "squads run" instead — autopilot mode when no target given')
.description('[deprecated] Use "squads run" instead — unified daemon mode')
.option('-i, --interval <minutes>', 'Minutes between cycles', '30')
.option('-p, --parallel <count>', 'Max parallel agent runs', '2')
.option('-b, --budget <dollars>', 'Max daily spend in dollars (0 = unlimited/subscription)', '0')
Expand Down Expand Up @@ -898,8 +906,41 @@ registerTriggerCommand(program);
// Approval command group - human-in-the-loop for agents
registerApprovalCommand(program);

// Autonomous command group - scheduled routines
registerAutonomousCommand(program);
// Autonomous command — deprecated, now `squads run --status/--stop/--pause/--resume`
program
.command('autonomous')
.alias('auto')
.description('[deprecated] Daemon lifecycle moved to squads run flags')
.argument('[action]', 'start|stop|status|pause|resume')
.argument('[reason]', 'Pause reason (optional)')
.action(async (action?: string, reason?: string) => {
const colors = termColors;
const mapping: Record<string, string> = {
start: 'squads run',
stop: 'squads run --stop',
status: 'squads run --status',
pause: 'squads run --pause',
resume: 'squads run --resume',
};
const newCmd = mapping[action || 'status'] || 'squads run --status';
writeLine(` ${colors.yellow}Note: "squads autonomous ${action || ''}" is now "${newCmd}"${termReset}`);
writeLine();

const { runCommand } = await import('./commands/run.js');
switch (action) {
case 'start':
return runCommand(null, {});
case 'stop':
return runCommand(null, { stop: true });
case 'pause':
return runCommand(null, { pause: reason || 'Manual pause' });
case 'resume':
return runCommand(null, { resume: true });
case 'status':
default:
return runCommand(null, { status: true });
}
});

// ─── System ──────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -1076,7 +1117,7 @@ program

program.command('stack', { hidden: true }).description('[removed]').action(removedCommand('stack', 'Infrastructure is managed via the cloud. Use: squads login'));
program.command('cron', { hidden: true }).description('[removed]').action(removedCommand('cron', 'Use platform scheduler: squads trigger list'));
program.command('tonight', { hidden: true }).description('[removed]').action(removedCommand('tonight', 'Use platform scheduler for overnight runs: squads autonomous start'));
program.command('tonight', { hidden: true }).description('[removed]').action(removedCommand('tonight', 'Use: squads run (daemon mode, no arguments)'));
program.command('live', { hidden: true }).description('[removed]').action(removedCommand('live', 'Use: squads dash'));
program.command('top', { hidden: true }).description('[removed]').action(removedCommand('top', 'Use: squads sessions'));
program.command('watch', { hidden: true }).description('[removed]').action(removedCommand('watch', 'Use: watch -n 2 squads status'));
Expand Down
Loading
Loading