Skip to content
Merged
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
13 changes: 12 additions & 1 deletion .cursor/rules/sandman.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,19 @@ sandman status <name> [--json]
sandman connect <name> [--json]
sandman destroy <name> [-y] [--json]
sandman providers [--json]
sandman doctor [--reap] [--json]
sandman doctor [--reap] [--harness <name>] [--json]
sandman whoami [--json]
sandman up [--provider <provider>] [--name <name>] [--template <id>] [--json]
sandman handoff <name> --to <cursor|codex|opencode> [--json]
sandman connect-agent --harness <cursor|codex|opencode> [--json]
sandman templates [--json]
sandman mcp serve
sandman view [--port 9420]
sandman link <name> --combie [--json]
sandman verify <name> [--json]
sandman checkpoint <name> [message] [--json]
sandman rollback <name> --last [--json]
sandman seed --reset [--json]
```

## Workflow
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ For AWS, enable services with `sandman enable ec2 s3 lambda`.
| `sandman connect <name>` | Output environment credentials |
| `sandman destroy <name>` | Delete the environment and all resources |
| `sandman providers` | Show the live provider capability matrix |
| `sandman doctor` | Check init state, auth, and reap expired environments (`--reap`) |
| `sandman doctor` | Check init state, auth, and reap expired environments (`--reap`, `--harness`) |
| `sandman up` | Guided first environment: init, create, enable, connect |
| `sandman handoff <name> --to <harness>` | Write a continuation package for another agent |
| `sandman templates` | List built-in environment templates |
| `sandman mcp serve` | Start the Sandman MCP server on stdio |
| `sandman view` | Read-only loopback environment viewer |
| `sandman link <name> --combie` | Register resources with Combie |
| `sandman verify <name>` | Evaluate basic environment health claims |
| `sandman seed --reset` | Load deterministic local state for evals |

---

Expand Down
43 changes: 43 additions & 0 deletions docs/FIRST-RUN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Sandman first run

## Install

```bash
npm install -g @itssergio91/sandman
# or
curl -fsSL https://boringinfra.company/sandman/install.sh | sh
```

## Record yourself

```bash
sandman
```

## Guided environment

```bash
sandman init gcp --use-gcloud --billing-account <id>
sandman up --provider gcp --name demo --template web-app
```

## Agent setup

```bash
sandman connect-agent --harness cursor
sandman mcp serve
```

## Inspect and hand off

```bash
sandman doctor --harness cursor --json
sandman handoff demo --to cursor --json
sandman view
```

## Clean up

```bash
sandman destroy demo -y --json
```
55 changes: 55 additions & 0 deletions docs/MACHINE-CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Sandman CLI machine contract (`sandman.cli.v1`)

This document specifies the stable machine interface for the Sandman CLI.

## Invocation model

- `--json` is supported on all primary commands.
- JSON mode writes exactly one JSON document to stdout per invocation.
- Process exit code `0` means success; `1` means failure.
- stderr remains reserved for human diagnostics in text mode.

## Envelope

```jsonc
// success
{ "schema": "sandman.cli.v1", "command": "list", "success": true, "code": "OK", ... }

// failure
{ "schema": "sandman.cli.v1", "command": "create", "success": false, "code": "NOT_FOUND", "error": "...", "hint": "...", "next": ["..."] }
```

## Determinism

- Persisted timestamps (`createdAt`, `updatedAt`, `expiresAt`) are stored values.
- CLI-generated envelopes do not add random fields.
- Large `resources` and `warnings` arrays may include a `budget` object describing truncation.

## Error codes

| Code | Meaning |
| --- | --- |
| `OK` | Success |
| `NOT_FOUND` | Environment or checkpoint missing |
| `ALREADY_EXISTS` | Environment name already active |
| `NO_PROVIDER` | Provider not initialized |
| `INVALID_PROVIDER` | Unknown provider |
| `INVALID_NAME` | Invalid environment name |
| `INVALID_SERVICE` | Unknown service for provider |
| `AUTH_REQUIRED` | Missing or invalid cloud credentials |
| `CONFIRMATION_REQUIRED` | Destructive action needs `-y` in JSON mode |
| `PARTIAL` | Create finished with partial/failed resources |
| `EXPIRED` | Environment TTL elapsed |
| `INVALID_TTL` | Unparseable TTL duration |
| `INVALID_INPUT` | Argument present but invalid |
| `UNSUPPORTED` | Valid syntax but not implemented |
| `STATE_CORRUPT` | Local state file invalid |
| `STATE_LOCKED` | Another Sandman process holds the lock |
| `PROVIDER_ERROR` | Cloud API failure |
| `INTERNAL` | Unexpected failure |

## Compatibility rules

1. Adding fields is not a breaking change.
2. Removing or renaming fields requires bumping `schema` to `sandman.cli.v2`.
3. Error codes are only added to, never removed.
40 changes: 40 additions & 0 deletions docs/OPERATOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Sandman operator runbook

## Diagnostics

```bash
sandman doctor --json
sandman doctor --reap --json
sandman doctor --harness cursor --json
```

## Deterministic fixture

```bash
sandman seed --reset --json
```

## Portfolio integrations

```bash
sandman link demo --combie --json
sandman verify demo --json
```

## MCP capabilities

Create `~/.sandman/mcp.toml`:

```toml
allow = ["read", "provision"]
```

Omit `destroy` to block destructive MCP tools.

## Viewer

```bash
sandman view --host 127.0.0.1 --port 9420
```

Loopback only unless `--allow-remote` is passed explicitly.
34 changes: 34 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Sandman architecture

Sandman is a CLI-first orchestration layer over cloud provider APIs with local JSON state.

## Layers

1. **CLI** (`src/cli/`) — Commander commands, JSON contract, reap, secrets, handoff, MCP, viewer
2. **Core** (`src/core/`) — `StateStore`, checkpoints, seed fixtures
3. **Providers** (`src/providers/`) — AWS, GCP, Cloudflare, Vercel adapters
4. **Templates** (`src/templates/`) — Built-in environment recipes
5. **MCP** (`src/mcp/`) — stdio tool surface for agents

## State

- Primary config: `~/.sandman/config.json`
- Checkpoints: `~/.sandman/checkpoints/<env>.json`
- Handoffs: `~/.sandman/handoffs/`
- Combie links: `~/.combie/sandman-links/`
- MCP capabilities: `~/.sandman/mcp.toml`

## Safety model

- Zod-validated state with file locking and atomic writes
- Corrupt state refuses overwrite
- Destroy requires confirmation (or `-y` in JSON mode)
- TTL lazy-reap on status/connect/enable/doctor
- Provider destroy guards (GCP `sandman-` prefix, AWS ownership tags)

## Agent interfaces

- `--json` on all commands (`sandman.cli.v1`)
- `sandman mcp serve` for tool discovery
- `sandman handoff` for cross-harness continuation packages
- `sandman doctor --harness` for readiness checks
18 changes: 18 additions & 0 deletions evals/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,23 @@
"query": "Add Terraform support to provision infrastructure as code",
"should_trigger": false,
"notes": "IaC tooling request — not Sandman CLI orchestration"
},
{
"id": "trigger-11",
"query": "Hand off my sandman demo environment to another Cursor agent",
"should_trigger": true,
"notes": "Handoff workflow for cross-harness continuation"
},
{
"id": "trigger-12",
"query": "Start the Sandman MCP server so my agent can list environments",
"should_trigger": true,
"notes": "MCP tooling for agent-native Sandman access"
},
{
"id": "trigger-13",
"query": "Create a web-app template sandbox on GCP with Sandman",
"should_trigger": true,
"notes": "Template-based environment creation"
}
]
23 changes: 23 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env sh
set -eu

VERSION="${SANDMAN_VERSION:-latest}"
PREFIX="${PREFIX:-$HOME/.local}"
BIN_DIR="$PREFIX/bin"
TMP_DIR="${TMPDIR:-/tmp}"

OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64) ARCH="x64" ;;
aarch64|arm64) ARCH="arm64" ;;
esac

if command -v npm >/dev/null 2>&1; then
npm install -g "@itssergio91/sandman@${VERSION}"
echo "Installed Sandman via npm."
exit 0
fi

echo "npm not found. Install Node.js 18+ or use: npm install -g @itssergio91/sandman"
exit 1
41 changes: 41 additions & 0 deletions src/cli/commands/checkpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import chalk from "chalk";
import { StateStore } from "../../core/state-store.js";
import { saveCheckpoint } from "../../core/checkpoints.js";
import { emitErr, emitOk } from "../output.js";

interface CheckpointOptions {
json?: boolean;
message?: string;
}

export async function checkpointEnvironment(
name: string,
store: StateStore,
options: CheckpointOptions = {},
): Promise<void> {
const env = await store.getEnvironment(name);
if (!env || env.status === "destroyed") {
emitErr(options.json, {
code: "NOT_FOUND",
error: `Environment "${name}" not found.`,
next: ["sandman list --json"],
});
}

const record = await saveCheckpoint(
store.getConfigPath(),
env,
options.message,
);

emitOk(
options.json,
{ checkpoint: record },
() => {
console.log(chalk.green(`Checkpoint ${record.id} saved.`));
if (record.message) {
console.log(chalk.gray(record.message));
}
},
);
}
2 changes: 1 addition & 1 deletion src/cli/commands/cli-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe("CLI commands", () => {
const parsed = JSON.parse(String(log.mock.calls[0][0]));
expect(parsed.success).toBe(true);
const cf = parsed.providers.find((p: { id: string }) => p.id === "cloudflare");
expect(cf.maturity).toBe("experimental");
expect(cf.maturity).toBe("supported");
const aws = parsed.providers.find((p: { id: string }) => p.id === "aws");
expect(aws.maturity).toBe("supported");
});
Expand Down
Loading
Loading