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
41 changes: 41 additions & 0 deletions .agents/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Agent Skills for the Sawala CLI

Each subdirectory here is an [Agent Skill](https://agentskills.io) — a
`SKILL.md` with `name` + `description` frontmatter that an agent loads on
demand when a task matches the description.

## Why `.agents/skills/` and not `.claude/skills/`

The `SKILL.md` format is a cross-vendor standard, but the directory each tool
looks in is not:

| Tool | Reads |
|---|---|
| OpenAI Codex | `.agents/skills/` **only** — not `.codex/skills`, not `.claude/skills` |
| GitHub Copilot | `.agents/skills/`, `.github/skills/`, or `.claude/skills/` |
| Claude Code | `.claude/skills/` |

`.agents/skills/` is the only directory Codex and Copilot both read, so it is
canonical here. `.claude/skills` is a relative symlink to it, which covers
Claude Code without a second copy. There is no `.github/skills/` — Copilot
already reads `.agents/skills/`.

On Windows without Developer Mode, Git may materialise the symlink as a text
file. If that happens, copy `.agents/skills/` to `.claude/skills/` and add the
copy to `.gitignore`.

## What ships to customers

Every skill here except `sawala-cli-dev` is published inside the `@sawala/cli`
npm package and installed by `sawala skills install`. `sawala-cli-dev`
documents how to contribute to *this monorepo* and is meaningless to a
customer, so it stays here only.

## If you add a command or an MCP tool

`scripts/check-skills-coverage.mjs` fails the build when a CLI command group or
an MCP tool name appears nowhere in these files. It also validates each skill
against the Agent Skills spec (name pattern, `name` matching the directory,
description length, body under 500 lines, no dangling relative links).

Run it with `npm run check:skills`.
1 change: 1 addition & 0 deletions .agents/skills/kodena-deploy
1 change: 1 addition & 0 deletions .agents/skills/sawala-ajena
1 change: 1 addition & 0 deletions .agents/skills/sawala-akuna
1 change: 1 addition & 0 deletions .agents/skills/sawala-cli
105 changes: 105 additions & 0 deletions .agents/skills/sawala-cli-dev/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
name: sawala-cli-dev
description: Contribute to the Sawala CLI monorepo — add a command to @sawala/cli, mirror it as an MCP tool, follow the house conventions, and ship it. Use when editing anything in the sawala-tech/cli repository, adding or changing a CLI command or MCP tool, writing command tests, or preparing a release.
metadata:
sawala-cli-version: "0.13.0"
---

# Contributing to the Sawala CLI monorepo

npm workspaces under `packages/*`. Node >= 20, but **use v22 for anything
touching `wrangler`**: `export PATH=~/.nvm/versions/node/v22.19.0/bin:$PATH`.

## A PR without a changeset ships nothing

This is the single most consequential fact in this repository. Published
packages are versioned by [changesets](https://github.com/changesets/changesets).
**A PR that changes a package's behaviour without a changeset never bumps the
version, so no user ever receives the change.**

Add one file per change at `.changeset/<kebab-slug>.md`:

---
"@sawala/cli": minor
---

One paragraph, user-facing: what someone can now do that they could not
before, and anything surprising about it. This becomes the CHANGELOG entry,
so write it for a user of the CLI, not a reviewer of the diff.

`patch` = bug fix, no new surface. `minor` = new command, flag, or behaviour.
`major` = a break (removed/renamed command, changed default). List **every**
package affected, each with its own bump — a change spanning `@sawala/cli` and
`@sawala/mcp` names both.

Skip a changeset only for changes with no published effect: tests, docs, CI,
`AGENTS.md`, or the skills in `.agents/`.

Never hand-edit the generated "Version Packages" PR.

## Which package

| Path | Package | Holds |
|---|---|---|
| `packages/sawala` | `@sawala/cli` | the multi-service platform CLI (`sawala …`) |
| `packages/kodena` | `@sawala/kodena` | the deploy CLI (`kodena …`) |
| `packages/sawala-auth` | `@sawala/auth` | shared credentials, context, `apiFetch` |
| `packages/sawala-mcp` | `@sawala/mcp` | MCP server mirroring `@sawala/cli` |
| `packages/kodena-mcp` | `@sawala/kodena-mcp` | MCP server mirroring `@sawala/kodena` |

New platform CRUD goes in **`packages/sawala`**, next to the `datana`/`kontena`
precedent. `sawala-cloud-core`'s `AGENTS.md` says to build it in
`packages/kodena` — **that wording is stale.** `packages/kodena` is the deploy
CLI only.

## Adding a command

`src/commands/datana.ts` is the reference implementation. Copy its shape.

- A new group is `src/commands/<service>.ts` exporting
`create<Service>Command()`, registered in `src/cli.ts`.
- Use `apiFetch(ctx, '/cli/<service>/<path>')`, `loadContext`,
`requireActiveOrg`, and `requireActiveProject` — plus
`requireActiveProjectId` when the service takes a project ULID in the path.
- Bodies via `-f/--file` (`-` = stdin) or `-d/--data`, resolved by
`resolveInputPayload` from `src/lib/io`.
- `--dry-run` prints `{ wouldSend: { method, body } }` and writes nothing.
- Destructive verbs take `-y/--yes`, else `confirmOrThrow`, which refuses
outright with no TTY.
- Reads print pretty JSON; lists print terse padded columns.
- Errors: `throw new Error(msg)`. `src/cli.ts` maps a throw to stderr + exit 1 —
that is what makes `validate`-style commands usable as CI gates.

Check the service before copying a path shape: Kontena and Datana resolve a
project ULID into the path, while Ajena derives scope from the CLI token and
takes none.

Note that `@sawala/cli` does **not** wire the `--org`/`--project`/`--token`
flags that `CliOptions` supports — every call site is `loadContext(SAWALA_BRAND)`
with no options, even though `requireActiveProject`'s message advertises
`--project`. `kodena deploy` is the one command that does pass them. Wiring
them in `@sawala/cli` would be a genuine improvement.

## Tests

Command tests live in `packages/<pkg>/test/`, stub `fetch`, and assert the
exact method, path, and body the CLI would send — see `test/datana.test.ts`
and `test/ajena.test.ts`. A new command should assert its path, its
`--dry-run`, and its confirmation and exit-code behaviour.

## Before opening a PR

npm run typecheck && npm run test && npm run check:skills

`check:skills` runs `scripts/check-skills-coverage.mjs`, which fails when a CLI
command group or MCP tool name appears in no skill under `.agents/skills/`, when
a skill breaks the Agent Skills spec, or when a skill contains something
credential-shaped. **A new command or MCP tool must also be documented in a
skill** — see `.agents/skills/README.md` for the layout and why it is
`.agents/` rather than `.claude/`.

The upstream reference validator (`skills-ref validate <dir>`, a Python package
from the Agent Skills project) is a useful occasional cross-check, but CI does
not depend on it — its authors describe it as demonstration-quality.

Then check: **did you add a changeset?**
1 change: 1 addition & 0 deletions .agents/skills/sawala-datana
1 change: 1 addition & 0 deletions .agents/skills/sawala-kontena
1 change: 1 addition & 0 deletions .agents/skills/sawala-sebar
1 change: 1 addition & 0 deletions .agents/skills/sawala-tugasna
9 changes: 9 additions & 0 deletions .changeset/agent-skills-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@sawala/cli": minor
---

New `sawala skills` command: install the Sawala Agent Skills into your AI coding agent so it knows how to drive these tools correctly.

`sawala skills install` copies eight skills — orientation plus one per product (Kontena, Datana, Tugasna, Sebar, Akuna, Ajena, Kodena) — into your project. They carry the things that are not in `--help`: that a collection or schema `update` is a PUT replacement so adding a field means pull-append-push, that a Datana boolean filter silently returns zero rows, that Tugasna dates are epoch-millisecond numbers, that `sebar broadcast create` sends immediately with no undo, and that `akuna isolate` is effectively one-way.

Skills are an open cross-vendor format, so one install serves Claude Code, GitHub Copilot, and OpenAI Codex. The default target is `.agents/skills/`, which Codex and Copilot both read; pass `--target all` to also write `.claude/skills/` and `.github/skills/`, or `--global` to install into your home directory. `sawala skills list` shows what is bundled and `sawala skills uninstall` removes them again. Installing never overwrites an existing skill folder without `--force`, and `--dry-run` prints what it would write.
1 change: 1 addition & 0 deletions .claude/skills
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ jobs:
- run: npm run typecheck
- run: npm run test
- run: npm run build
- run: npm run check:skills
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ dist/
coverage/
*.tsbuildinfo
*.tgz

# Local agent worktrees (see .claude/worktrees/)
.claude/worktrees/
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"test": "npm --workspaces --if-present run test",
"typecheck": "npm --workspaces --if-present run typecheck",
"lint": "npm --workspaces --if-present run lint",
"release": "changeset publish"
"release": "changeset publish",
"check:skills": "node scripts/check-skills-coverage.mjs"
},
"devDependencies": {
"@changesets/cli": "^2.27.0",
Expand Down
1 change: 1 addition & 0 deletions packages/sawala/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"files": [
"dist",
"skills",
"LICENSE",
"README.md"
],
Expand Down
137 changes: 137 additions & 0 deletions packages/sawala/skills/kodena-deploy/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
name: kodena-deploy
description: Deploy and operate sites and workers on Kodena with the kodena CLI — static sites, worker bundles, OpenNext builds of Next.js, and single-file code deploys — plus scripts, assets, env vars, secrets, custom domains, and logs. Use when asked to deploy, redeploy, point a domain at a site, read deploy logs, or debug a site that deployed successfully but serves the wrong thing.
metadata:
sawala-cli-version: "0.11.0"
---

# Kodena — deploying and operating

Kodena is the deploy target. Its CLI is `kodena`, a **separate binary** from
`sawala` with its own credentials under `~/.kodena/` (`KODENA_API_TOKEN`,
`KODENA_ORG`, `KODENA_PROJECT`). `@sawala/kodena-mcp` mirrors it with 21 tools.

Unlike `sawala`, `kodena deploy` **does** accept per-command scope overrides:
`--org`, `--project`, `--token`, `--api-base`. Prefer those over changing
global state.

## `kodena.json` is mandatory

Every deploy needs a `kodena.json` in the current directory or a parent —
`--code` deploys included. Minimum content:

{ "slug": "<your-script-slug>" }

Without it the command fails before any network call. `kodena.json` may also
carry `project`, which sits between `KODENA_PROJECT` and `~/.kodena/config` in
the project-resolution chain, and a `build` block (`command`, `runByDefault`,
`static`).

## Pick the deploy shape first

Choosing wrong is the most expensive mistake here. There are three:

**Static site — `--static`.** Uploads a build output directory as
`kind:assets`, no worker of your own. Kodena installs a small shim worker and
pins compatibility date `2025-01-24`. Use for pure static output.

**Worker bundle — the default.** Uploads your built worker. This is the shape
a Next.js app takes when built with OpenNext; add `--build` to run
`kodena.json`'s `build.command`, which defaults to
`npx @opennextjs/cloudflare build`.

**Single module — `--code <file>`.** Deploys one directly-authored Worker
source file as `kind:code`, skipping build and auto-detection. The source
round-trips, so it is readable back via `kodena env` and the dashboard editor.

`--no-build` and `--no-static` force off whatever `kodena.json` turns on.
`--dry-run` runs everything up to the network call and prints a summary.

## Static-deploy behaviour

Kodena sets `html_handling: "auto-trailing-slash"` on the asset config. That
is what makes `/page.html` redirect to `/page` — a redirect, not a rewrite, so
anything asserting on the exact URL must expect the hop. A root `index.html`
is what makes `/` resolve; without one the site has no entry point.

You do **not** need to set `run_worker_first` yourself. Kodena already sets it
for asset deploys — it is required for the shim's `Cache-Control` rewriting to
run at all, and the service handles it.

## The blank `Content-Type` trap — worker bundles only

Some assets are served from R2 with no `Content-Type`, which breaks favicons,
SVGs, and fonts in the browser.

**Static (`--static`) deploys are already protected**: Kodena's default shim
carries a safety net that infers the type from the file extension for `ico`,
`png`, `jpg`, `jpeg`, `webp`, `svg`, `css`, `js`, `json`, `woff2`, `xml`, and
`txt`.

**Worker-bundle deploys — including every OpenNext build — are not.** When you
supply your own worker, Kodena uses it instead of the shim and the safety net
goes with it. If a deployed Next.js site renders but its icons, fonts, or SVGs
do not load, this is the cause. The fix is to set the header in your own
worker's asset response path, mirroring the shim's extension map.

## Debugging a deploy that "worked"

Two first moves when the deploy succeeded but the site misbehaves:

kodena logs <slug> # runtime logs
kodena script get <slug> # what is actually deployed

**Scripts are project-scoped.** `kodena script list` and the MCP
`kodena_list_scripts` reflect the active project only, so "the script does not
exist" often means "wrong project", not "missing". Check with
`kodena project list` before concluding anything is gone.

## The rest of the surface

kodena script list|get|rename|rehydrate|rm
kodena asset list|get|patch|rebuild
kodena env list|set|unset
kodena secret put|list|rm
kodena domain set|status|rm
kodena logs / kodena logging
kodena template list
kodena slug check
kodena org list|use|handle kodena project list|use

Prefer `kodena secret put` over `--secret KEY=value` at deploy time: it rotates
a secret with no rebuild. `--var` and `--secret` are both repeatable and take
`KEY=value`.

`--compat-flag` accepts only `nodejs_compat` or `nodejs_als` and is repeatable —
pass the flag twice rather than a comma-separated string. `--compat-date` takes
`YYYY-MM-DD`.

## MCP equivalents

`@sawala/kodena-mcp` exposes 21 tools. Prefer them for reads and for scripted
mutation; the CLI still owns `deploy` itself.

| Area | MCP tools |
|---|---|
| identity, scope | `kodena_whoami`, `kodena_list_orgs`, `kodena_list_projects`, `kodena_get_org_handle`, `kodena_set_org_handle` |
| scripts | `kodena_list_scripts`, `kodena_get_script`, `kodena_create_script`, `kodena_update_script`, `kodena_deploy_script`, `kodena_rehydrate_script`, `kodena_delete_script` |
| assets | `kodena_get_asset`, `kodena_patch_assets`, `kodena_rebuild_assets_manifest` |
| domains | `kodena_set_custom_domain`, `kodena_get_custom_domain_status`, `kodena_remove_custom_domain` |
| logs, secrets, slugs | `kodena_get_script_logs`, `kodena_list_secrets`, `kodena_check_slug_available` |

`kodena_list_secrets` returns names only — secret values are never readable
back, by design. `kodena_check_slug_available` before `kodena_create_script`
saves a round-trip on a taken slug.

Session commands (`kodena login`, `kodena logout`) have no MCP equivalent:
authentication is a terminal action by design.

## Environment traps

- **Wrangler needs Node v22.** The repo's `engines` allows v20 and the default
`node` often is v20. Fix: `export PATH=~/.nvm/versions/node/v22.19.0/bin:$PATH`.
- A Cloudflare **`Authentication error`** during deploy is usually an API-token
**scope gap**, not a wrong or expired token. Check the token's permissions
before reissuing it.
- `.id` domains are registered through the reseller **liqu.id**, not Cloudflare,
so a `.id` custom domain has a registrar step outside `kodena domain set`.
Loading
Loading