koppler 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +101 -2
- package/bin/run-logged +16 -0
- package/bin/secret-list +7 -0
- package/bin/secret-psql +41 -0
- package/bin/secret-set +17 -0
- package/bin/secret-store +82 -0
- package/bin/secret-test +10 -0
- package/bin/with-secrets +25 -0
- package/dist/.built +1 -0
- package/dist/cli/agent.js +144 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/doctor.js +173 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/mcp.js +204 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/output.js +37 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/records.js +121 -0
- package/dist/cli/records.js.map +1 -0
- package/dist/cli/running.js +270 -0
- package/dist/cli/running.js.map +1 -0
- package/dist/cli/shell.js +43 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/wait.js +59 -0
- package/dist/cli/wait.js.map +1 -0
- package/dist/core/addresses.js +55 -0
- package/dist/core/addresses.js.map +1 -0
- package/dist/core/arrange.js +329 -0
- package/dist/core/arrange.js.map +1 -0
- package/dist/core/cables.js +158 -0
- package/dist/core/cables.js.map +1 -0
- package/dist/core/expressions.js +72 -0
- package/dist/core/expressions.js.map +1 -0
- package/dist/core/frameworks.js +13 -0
- package/dist/core/frameworks.js.map +1 -0
- package/dist/core/kinds.js +244 -0
- package/dist/core/kinds.js.map +1 -0
- package/dist/core/launch.js +181 -0
- package/dist/core/launch.js.map +1 -0
- package/dist/core/logfmt.js +144 -0
- package/dist/core/logfmt.js.map +1 -0
- package/dist/core/mock.js +27 -0
- package/dist/core/mock.js.map +1 -0
- package/dist/core/plan.js +147 -0
- package/dist/core/plan.js.map +1 -0
- package/dist/core/ports.js +155 -0
- package/dist/core/ports.js.map +1 -0
- package/dist/core/presets.js +186 -0
- package/dist/core/presets.js.map +1 -0
- package/dist/core/secret-types.js +3 -0
- package/dist/core/secret-types.js.map +1 -0
- package/dist/core/sign-in.js +30 -0
- package/dist/core/sign-in.js.map +1 -0
- package/dist/core/stack-path.js +64 -0
- package/dist/core/stack-path.js.map +1 -0
- package/dist/core/stack.js +509 -0
- package/dist/core/stack.js.map +1 -0
- package/dist/core/terraform.js +393 -0
- package/dist/core/terraform.js.map +1 -0
- package/dist/data.js +37 -0
- package/dist/data.js.map +1 -0
- package/dist/engine/actions.js +99 -0
- package/dist/engine/actions.js.map +1 -0
- package/dist/engine/app-commands.js +106 -0
- package/dist/engine/app-commands.js.map +1 -0
- package/dist/engine/app-start.js +167 -0
- package/dist/engine/app-start.js.map +1 -0
- package/dist/engine/app-state.js +43 -0
- package/dist/engine/app-state.js.map +1 -0
- package/dist/engine/boards.js +219 -0
- package/dist/engine/boards.js.map +1 -0
- package/dist/engine/branch.js +194 -0
- package/dist/engine/branch.js.map +1 -0
- package/dist/engine/browser-console.js +45 -0
- package/dist/engine/browser-console.js.map +1 -0
- package/dist/engine/calls.js +97 -0
- package/dist/engine/calls.js.map +1 -0
- package/dist/engine/checks.js +270 -0
- package/dist/engine/checks.js.map +1 -0
- package/dist/engine/clones.js +67 -0
- package/dist/engine/clones.js.map +1 -0
- package/dist/engine/config.js +26 -0
- package/dist/engine/config.js.map +1 -0
- package/dist/engine/deployed.js +488 -0
- package/dist/engine/deployed.js.map +1 -0
- package/dist/engine/devices.js +81 -0
- package/dist/engine/devices.js.map +1 -0
- package/dist/engine/discover.js +236 -0
- package/dist/engine/discover.js.map +1 -0
- package/dist/engine/down.js +253 -0
- package/dist/engine/down.js.map +1 -0
- package/dist/engine/history.js +215 -0
- package/dist/engine/history.js.map +1 -0
- package/dist/engine/investigate.js +278 -0
- package/dist/engine/investigate.js.map +1 -0
- package/dist/engine/libraries.js +129 -0
- package/dist/engine/libraries.js.map +1 -0
- package/dist/engine/linked.js +146 -0
- package/dist/engine/linked.js.map +1 -0
- package/dist/engine/listen.js +223 -0
- package/dist/engine/listen.js.map +1 -0
- package/dist/engine/locks.js +38 -0
- package/dist/engine/locks.js.map +1 -0
- package/dist/engine/messages.js +177 -0
- package/dist/engine/messages.js.map +1 -0
- package/dist/engine/needs.js +363 -0
- package/dist/engine/needs.js.map +1 -0
- package/dist/engine/processes.js +350 -0
- package/dist/engine/processes.js.map +1 -0
- package/dist/engine/query.js +241 -0
- package/dist/engine/query.js.map +1 -0
- package/dist/engine/queues.js +100 -0
- package/dist/engine/queues.js.map +1 -0
- package/dist/engine/recipes.js +248 -0
- package/dist/engine/recipes.js.map +1 -0
- package/dist/engine/reload.js +32 -0
- package/dist/engine/reload.js.map +1 -0
- package/dist/engine/repos.js +214 -0
- package/dist/engine/repos.js.map +1 -0
- package/dist/engine/research.js +134 -0
- package/dist/engine/research.js.map +1 -0
- package/dist/engine/run-logs.js +79 -0
- package/dist/engine/run-logs.js.map +1 -0
- package/dist/engine/run.js +54 -0
- package/dist/engine/run.js.map +1 -0
- package/dist/engine/runs.js +223 -0
- package/dist/engine/runs.js.map +1 -0
- package/dist/engine/session.js +130 -0
- package/dist/engine/session.js.map +1 -0
- package/dist/engine/share.js +124 -0
- package/dist/engine/share.js.map +1 -0
- package/dist/engine/stack-file.js +206 -0
- package/dist/engine/stack-file.js.map +1 -0
- package/dist/engine/stack-share.js +86 -0
- package/dist/engine/stack-share.js.map +1 -0
- package/dist/engine/stacks.js +143 -0
- package/dist/engine/stacks.js.map +1 -0
- package/dist/engine/sync.js +214 -0
- package/dist/engine/sync.js.map +1 -0
- package/dist/engine/traces.js +65 -0
- package/dist/engine/traces.js.map +1 -0
- package/dist/engine/up.js +621 -0
- package/dist/engine/up.js.map +1 -0
- package/dist/engine/watch.js +71 -0
- package/dist/engine/watch.js.map +1 -0
- package/dist/engine/wire.js +120 -0
- package/dist/engine/wire.js.map +1 -0
- package/dist/failure.js +16 -0
- package/dist/failure.js.map +1 -0
- package/dist/github.js +95 -0
- package/dist/github.js.map +1 -0
- package/dist/ids.js +6 -0
- package/dist/ids.js.map +1 -0
- package/dist/json-files.js +36 -0
- package/dist/json-files.js.map +1 -0
- package/dist/koppler.mjs +840 -0
- package/dist/koppler.mjs.map +1 -0
- package/dist/lambda-local.mjs +250 -0
- package/dist/lambda-local.mjs.map +1 -0
- package/dist/lambda-secrets-proxy.go +31 -0
- package/dist/mock-server.mjs +23 -0
- package/dist/mock-server.mjs.map +1 -0
- package/dist/page/assets/JetBrainsMono-cyrillic-D2jMJ_bk.woff2 +0 -0
- package/dist/page/assets/JetBrainsMono-greek-D2nYnAec.woff2 +0 -0
- package/dist/page/assets/JetBrainsMono-latin-Db4Uuiha.woff2 +0 -0
- package/dist/page/assets/JetBrainsMono-latin-ext-Bnd2h4Br.woff2 +0 -0
- package/dist/page/assets/JetBrainsMono-vietnamese-CNlsqbWb.woff2 +0 -0
- package/dist/page/assets/SchibstedGrotesk-latin-DxefFeRw.woff2 +0 -0
- package/dist/page/assets/SchibstedGrotesk-latin-ext-CnpJE18X.woff2 +0 -0
- package/dist/page/assets/index-Bkj_pX35.js +42 -0
- package/dist/page/assets/index-Dq45jswH.css +1 -0
- package/dist/page/index.html +14 -0
- package/dist/page/theme.js +8 -0
- package/dist/paths.js +19 -0
- package/dist/paths.js.map +1 -0
- package/dist/safe-env.js +73 -0
- package/dist/safe-env.js.map +1 -0
- package/dist/sam-local.mjs +135 -0
- package/dist/sam-local.mjs.map +1 -0
- package/dist/scrub.js +87 -0
- package/dist/scrub.js.map +1 -0
- package/dist/secret-check.mjs +262 -0
- package/dist/secret-check.mjs.map +1 -0
- package/dist/server/browser-console.js +117 -0
- package/dist/server/browser-console.js.map +1 -0
- package/dist/server/cable-test.js +365 -0
- package/dist/server/cable-test.js.map +1 -0
- package/dist/server/choices.js +51 -0
- package/dist/server/choices.js.map +1 -0
- package/dist/server/files.js +107 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/gate.js +128 -0
- package/dist/server/gate.js.map +1 -0
- package/dist/server/logs.js +76 -0
- package/dist/server/logs.js.map +1 -0
- package/dist/server/mobile.js +113 -0
- package/dist/server/mobile.js.map +1 -0
- package/dist/server/routes.js +326 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/server/runs.js +111 -0
- package/dist/server/runs.js.map +1 -0
- package/dist/server/sanitize.js +242 -0
- package/dist/server/sanitize.js.map +1 -0
- package/dist/server/secrets.js +111 -0
- package/dist/server/secrets.js.map +1 -0
- package/dist/server/status.js +321 -0
- package/dist/server/status.js.map +1 -0
- package/dist/server/token.js +38 -0
- package/dist/server/token.js.map +1 -0
- package/dist/server/waiting.js +79 -0
- package/dist/server/waiting.js.map +1 -0
- package/dist/status-watch.mjs +63 -0
- package/dist/status-watch.mjs.map +1 -0
- package/package.json +74 -4
- package/skills/koppler-run/SKILL.md +169 -0
- package/skills/koppler-run/VERSION +1 -0
- package/skills/koppler-run/references/action-log.md +50 -0
- package/skills/koppler-run/references/permissions.md +25 -0
- package/skills/koppler-run/references/preparing-nodes.md +62 -0
- package/skills/koppler-run/references/reporting-bugs.md +76 -0
- package/skills/koppler-run/references/research.md +40 -0
- package/skills/koppler-run/references/secrets.md +56 -0
- package/skills/koppler-run/references/stack.md +197 -0
- package/skills/koppler-run/references/status.md +92 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Shared stack config: `stack.json`
|
|
2
|
+
|
|
3
|
+
Nobody writes this file directly. Change any field with `koppler stack set <path> <value>` or `koppler stack unset <path>` (read one with `koppler stack get <path>`); paths look like `apps.orders.url`, `apps.orders.prereqs[0].port` or `apps.orders.envs.prod.start`. Each change is checked against the rules below before it is kept.
|
|
4
|
+
|
|
5
|
+
One file per stack, in the team's repo, reviewed through PRs.
|
|
6
|
+
It holds facts about the apps, never secret values.
|
|
7
|
+
`koppler sync` validates it (`envs`, and `name` / `kind` / `slug` per app, `consumes` pointing at known apps) and copies it to `<state>/registry.json`.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"name": "Orders",
|
|
12
|
+
"envs": ["qa", "prod"],
|
|
13
|
+
"notes": ["Stack-wide facts the agent reads first, e.g. which tunnel can move to another port."],
|
|
14
|
+
"checks": [
|
|
15
|
+
{ "name": "nuget-feed", "kinds": ["dotnet"], "run": "dotnet nuget list source | grep -q acme", "ok": "feed configured", "fix": "see the README setup" }
|
|
16
|
+
],
|
|
17
|
+
"apps": {
|
|
18
|
+
"orders-api": {
|
|
19
|
+
"name": "Orders API",
|
|
20
|
+
"kind": "dotnet",
|
|
21
|
+
"slug": "acme/orders-api",
|
|
22
|
+
"project": "src/Orders.Api",
|
|
23
|
+
"launchSettings": "src/Orders.Api/Properties/launchSettings.json",
|
|
24
|
+
"baseProfile": "QA",
|
|
25
|
+
"url": "http://localhost:5001",
|
|
26
|
+
"deployedUrl": "https://orders.qa.acme.dev",
|
|
27
|
+
"run": { "cli": "dotnet run --project src/Orders.Api --launch-profile <profile>", "ide": "Rider: launch profile <profile>" },
|
|
28
|
+
"consumes": {
|
|
29
|
+
"payments-api": { "how": "env", "env": ["Payments__BaseUrl"], "deployed": "https://payments.qa.acme.dev" }
|
|
30
|
+
},
|
|
31
|
+
"prereqs": [
|
|
32
|
+
{ "sso": "acme-qa", "note": "AWS login for config and secrets" },
|
|
33
|
+
{ "tunnel": "db-qa.internal.acme.dev", "port": 5432, "cmd": "cloudflared access tcp --hostname db-qa.internal.acme.dev --url localhost:5432" }
|
|
34
|
+
],
|
|
35
|
+
"secrets": [
|
|
36
|
+
{ "env": "ConnectionStrings__DB", "when": "always", "fetch": { "cli": "<command> | koppler secret set orders-api ConnectionStrings__DB --stdin", "ui": "<where to copy it from>" } }
|
|
37
|
+
],
|
|
38
|
+
"envs": { "prod": { "baseProfile": "PROD", "deployedUrl": "https://orders.acme.dev" } }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Field | Meaning |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `envs` | Deployed environments, first one is the default; app fields apply to it, `apps[id].envs.<env>` overrides them for the others. |
|
|
47
|
+
| `protected` | The environments where a mistake costs something (production, usually): a list of names from `envs`. Without it, every environment except the first. They get the red frame and the blocking review on the canvas, and their secrets ask on every read. |
|
|
48
|
+
| `envAliases` | Other names the repos use for an environment, e.g. `{ "qa": ["dev"] }`; `koppler discover --deployed` reads Terraform written for `dev` as QA. |
|
|
49
|
+
| `notes` | Stack-wide facts. |
|
|
50
|
+
| `checks` | The stack's own health checks, run when it loads (`koppler doctor`): `name`, `run` (a shell command), `ok` and `fix` texts, `kinds` to limit it to stacks with those app kinds. At most 50, each `run` up to 2000 characters. They are someone's commands, so they run only after the user approved them with `koppler doctor --approve`, and again after one changes. The same approval covers everything else the stack would run or write: an app's `start` (also per environment), a custom kind's `start` and `stop`, and `localChanges`. |
|
|
51
|
+
| `apps.<id>.outside` | The app does not run on this Mac, or has nothing to run (Salesforce, a SaaS package, a managed service, an S3 bucket, a library or config repo), with a short why. It is never planned, sent or started (`koppler up` refuses it by name), needs no `slug`, and its card shows the address its `deployedUrl` gives for the board's environment (the Salesforce sandbox on QA, production on PROD). `koppler discover --deployed` drafts known ones (Salesforce) from each app's addresses per environment, with the calls to them (`outsideSuggestions`). It stays in the stack so the flow is complete: it never runs or takes a cable, and shows as a dashed read-only card next to the board apps that call it (`consumes`) or share a queue with it. |
|
|
52
|
+
| `queues` | Top-level: `{ "<id>": { "name", "type", "queue"? } }`, the queues, topics and event buses between apps. `type` is `sqs`, `sns`, `eventbridge`, `kafka` or `other`; `queue` is its real name (default the id), per environment under `envs.<env>.queue`. |
|
|
53
|
+
| `apps.<id>.publishes`, `apps.<id>.reads` | `koppler discover --deployed` drafts them, with `queues`, from the Terraform (who sends and reads, the settings carrying each address, what feeds a queue from outside); confirm before adding. The queues the app sends to or takes from: `{ "<queue>": { "env"?: [settings holding the queue's URL] } }`. On the canvas an app is off its queues until its card's Queues switch is on (`queues: true` in a preset); then they draw a queue card between its apps, with a Local switch. Switched on, `koppler up` runs it locally. SQS: one AWS emulator in Docker on port 4566 holds the queue, and every app on it starts with `AWS_ENDPOINT_URL_SQS` (which current AWS SDKs read) and its `env` settings pointed at the local queue. Kafka: one broker in Docker (`apache/kafka`) on `localhost:9092` gets the topic (`queue`), and every app on it starts with its `env` settings (its broker list, e.g. `Kafka__BootstrapServers`) set to `localhost:9092`; a broker setup that needs sign-in (SASL, MSK IAM) has to be turned off for the local run, e.g. by a remembered change. SNS and EventBridge stay deployed. Off, a local reader takes messages off the deployed queue, so the deployed reader misses those; the card says so. |
|
|
54
|
+
| `install` | How to install a program the stack needs, by name: `{ "cloudflared": "brew install cloudflared" }`. Which programs are needed is worked out from the stack itself: each app kind's own, and the first word of every prerequisite's `cmd`. |
|
|
55
|
+
| `kind` | What the app is built with; decides how it is recognized, started and shown. See "Kinds" below. |
|
|
56
|
+
| `start` | The shell command that starts the app in its repo. Needed for `other`, optional for the recipe kinds, where it replaces the usual command. |
|
|
57
|
+
| `kinds` | Top-level: the stack's own kinds, `{ "<id>": { "label", "detect": [files that must all exist], "start": "<command>", "port" } }`, for a technology Koppler has no recipe for. |
|
|
58
|
+
| `slug` | `owner/repo` for GitHub, or `host/path` for any other git host (`gitlab.com/group/sub/repo`, `dev.azure.com/org/project/repo`); `koppler init` matches it against the remotes of the user's clones. |
|
|
59
|
+
| `ready` | Optional. When the app is ready, for an app whose port answers before it can serve: `{ "path": "/health", "status"?: 200, "timeout"?: seconds }`. A start then also waits until a GET of that path on the app's `url` answers (with that status, or any below 400), and apps that call it start after that. Without it, ready means the port answers. |
|
|
60
|
+
| `once` | Optional. The app runs to completion instead of staying up, such as a migration or seed data: `{ "exitCode"?: 0, "timeout"?: seconds }` (600 by default). A start waits for it to end with that exit code; any other code, or the timeout, stops the run with its last log lines. |
|
|
61
|
+
| `browserConsole` | Optional, for a web front end: `{ "file": "index.html" }`, its HTML entry file inside the clone. While it runs, one script line in that file sends what the page writes to its console, and its uncaught errors, to the app's log on its card. The line is a temporary change: Stop takes it out. |
|
|
62
|
+
| `watch` | Optional. For Auto-restart on the card, which restarts the app when a file in its clone changes: `{ "ignore": ["generated", "docs"] }`, paths whose changes leave it running. Tools' own folders (`.git`, `node_modules`, `bin`, `obj`, `build`, `dist`, `.dart_tool`, `.next`) never restart it. |
|
|
63
|
+
| `commands` | Optional. Buttons such as Seed or Migrate on the app's card, also run with `koppler do <app> <command>`: `{ "seed": { "label": "Seed", "run": "npm run seed", "icon"?: "sprout", "confirm"?: "Add test orders?", "args"?: [{ "name": "COUNT", "type"?: "number", "required"?: true }], "show"?: "canvas" \| "cli" \| "both" } }`. It runs in the app's clone with its secrets, each argument as a setting of its name, and only after `koppler doctor --approve`. Icons: play, database, sprout, refresh, trash, terminal, upload, download. |
|
|
64
|
+
| `mock` | Optional. Canned responses that answer in the app's place when its card is set to Mock (Started by: Mock), for a dependency that is flaky or not there yet: `[{ "method"?: "GET", "path": "/orders/*", "status"?: 200, "body"?: { "id": 7 }, "headers"?: {} }]`. A path ending in `*` answers every path under it; a JSON body is sent as JSON, a text one as text; a call no response fits gets a 404 that lists them. Koppler answers on the app's own `url` (http only), so every cable into it works as it would to the app; a mock needs no clone, branch, tunnels or secrets. Also `koppler run <app> --mock`. |
|
|
65
|
+
| `after` | Optional. App ids that must finish (a `once` app) or start before this one, when they are in the same run: `["orders-db-migrate"]`. |
|
|
66
|
+
| `consumes.<app>` | How this app calls another: `how: env` (env vars in a `-LOCAL` launch profile), `how: env-file` (lines in a git-ignored local env file, `file` default `.env.local`, for front ends whose variables need the framework's prefix such as `VITE_` or `NEXT_PUBLIC_`), `how: edit` (a marked temporary code edit at `file` / `key`; `value` is optional, `koppler plan --wire` works out the address for each device, and a `value` per `ios` / `android` pins it), `how: secret` (a queue-triggered Lambda that reads the address from its secret: `key` is the secret's key), or `how: unknown` (the call is known from the repo, `found` says where, but how a local run repoints it is not: work it out from the repo on first use and write the real `how` back). When the caller's setting holds more than the callee's address (it posts to `<address>/some/route` as it is), give that route as `path`: a local cable then sets `<local address><path>`, where it would otherwise drop the route. `ifDown` says what a run does when the local app it calls does not start: `skip` (the default) leaves this app unstarted too, `start` starts it anyway, for an app that retries its calls. Either way the rest of the run goes on. |
|
|
67
|
+
| `prereqs` | `sso` (an AWS profile) or `signIn` (the name of a sign-in under the stack's `signIns`, for anything else), `tunnel` (+ `port`, `cmd`; optional `what` such as `"Database (Aurora)"`, else guessed from the host name; optional `env`, the app setting that holds the tunnel's `host:port`, which lets Koppler move the tunnel to a free port when another one needs the same port; optional `connectsAs`, the host name the app actually dials when it is not the tunnel host, e.g. a database endpoint from a secret mapped to the tunnel by a `hosts` entry), `hosts`, `devCerts`, `file`, `nuget`, each with an optional `note`. |
|
|
68
|
+
| `secrets` | What the user must store (`env` or `file`), `when` it is needed, `fetch.cli` / `fetch.ui` steps, and for a database or cache connection an optional `type` (`postgres`, `mysql`, `redis`) so it can be tested. See `secrets.md`. |
|
|
69
|
+
| `mobile` | `iosBundleId` / `androidPackage`, so the watcher can tell whether the app is running, and `koppler stop` can close it on the device. |
|
|
70
|
+
| `flutter` | For `kind: flutter`: `fvm` (run through fvm), `flavor` (pass the env as `--flavor`), `defineFile` (e.g. `.env_{env}.json` for `--dart-define-from-file`). |
|
|
71
|
+
| `script` | For `kind: node` or `web`: the npm script that starts the app locally (default `dev`, else `start`). |
|
|
72
|
+
| `cors` | For an API called from browser front ends: `env`, the setting(s) holding allowed origins, e.g. `"Cors__AllowedOrigins__{n}"`; `{n}` gives each front end its own slot. Without it, `koppler plan --wire` warns that the browser may block calls. |
|
|
73
|
+
| `pending` | The app is listed but its run facts are not collected yet. |
|
|
74
|
+
|
|
75
|
+
Local libraries need nothing in `stack.json`: `koppler sync` matches each app's dependencies (`pubspec.yaml`, `package.json`) with the packages your clones publish, and the card offers a Libraries switch for each. Switched on, `koppler up` points the app at your clone (a `pubspec_overrides.yaml` for Dart, `npm install --no-save` for npm) and Stop reverts it; .NET libraries are not covered yet.
|
|
76
|
+
|
|
77
|
+
Top-level `presets` are the team boards: `[{ "name": "Mobile App + Mobile API", "version": 1, "nodes": { "<app>": { "run": "cli", "platform"?, "branch"?, "note"? } }, "edges": [{ "from", "to" }] }]`. The canvas lays them out by who calls whom; `koppler sync` checks every app and cable exists. Every board a user builds is kept as their own, saved as they change it; Share copies it as JSON to add here.
|
|
78
|
+
A single preset in its own `.json` file (the JSON that Copy in the board selector copies; Share makes a board a team board of the stack), a list of them, or a whole `stack.json` can be opened from the canvas's board selector, as a file or as a GitHub link.
|
|
79
|
+
|
|
80
|
+
Every preset carries the `version` of the preset format it was saved in (`VERSION` in `core/presets`, currently 1); `koppler sync` requires it on shared presets.
|
|
81
|
+
Before any preset opens, its version is compared with the one this Koppler reads:
|
|
82
|
+
- older: it is brought up to date one step at a time (`MIGRATIONS[n]` turns version n into n + 1; a file without a version is read as version 0);
|
|
83
|
+
- newer: it is refused, with a message to update Koppler.
|
|
84
|
+
|
|
85
|
+
It is then checked against the stack and your clones: apps the stack does not have or you have not cloned are left out, a run mode or platform the app does not offer falls back to one it does, a cable between apps that do not call each other is dropped, and a branch missing from your clone is pointed out.
|
|
86
|
+
Each change is listed when the preset loads; a preset with nothing left to open is refused and the board stays as it was.
|
|
87
|
+
To change the format: raise `VERSION`, add the `MIGRATIONS` step from the previous version, and cover it in `app/src/core/presets.test.ts`.
|
|
88
|
+
|
|
89
|
+
Other fields (`config`, `overrides`, `note`) are free-form facts for the agent to read.
|
|
90
|
+
|
|
91
|
+
## Expressions
|
|
92
|
+
|
|
93
|
+
A value can name another app's fact instead of repeating it: `"url": "http://localhost:{{orders-api.port}}"`, `"local": "http://{{orders-api.host}}:{{orders-api.port}}/v1"`.
|
|
94
|
+
`{{<app>.<field>}}` is any field of that app (its top-level value), and `port` and `host` also come from its `url`.
|
|
95
|
+
A value that is only an expression keeps the type of what it names, so `"port": "{{db-api.dbPort}}"` is a number.
|
|
96
|
+
`stack.json` keeps the expressions, and Settings shows and saves them as written; Koppler fills them in where it uses the stack (runs, the board, checks).
|
|
97
|
+
One that names nothing, or only itself, is a problem `koppler stack check` reports, and the stack is not saved with it.
|
|
98
|
+
|
|
99
|
+
## What the repos say
|
|
100
|
+
|
|
101
|
+
Right after repos are added on the canvas, and later from "Look through the repos again" in the stack menu (`koppler discover [--apply]`), Koppler reads the clones and offers, for the user to tick:
|
|
102
|
+
- where each app runs locally (launch settings, `.env`, the framework's default port), as its `url`;
|
|
103
|
+
- each app's deployed address per environment, from its Terraform, as `deployedUrl` (and `envs.<env>.deployedUrl`);
|
|
104
|
+
- an environment the repos name differently (QA deployed as `dev`), as `envAliases`, with the addresses that follow from it;
|
|
105
|
+
- who calls whom: another app's address in a caller's code, as `consumes.<callee>` with `how: unknown` and where it was found.
|
|
106
|
+
Only ticked findings are written; the agent works out `how` the first time that cable is used.
|
|
107
|
+
|
|
108
|
+
## Shared stacks from GitHub
|
|
109
|
+
|
|
110
|
+
A stack can be used straight from a link to its `stack.json` on GitHub (`koppler link <url>`, or "Use" next to the link field on the canvas), with no clone.
|
|
111
|
+
Koppler reads it through the user's `gh` login and keeps a copy in `~/.config/koppler/stacks/<owner>-<repo>-<name>/`, with `.source.json` holding the link and the file as last fetched.
|
|
112
|
+
- What Koppler learns goes into the copy and shows as "Changes not shared yet" in the stack menu (`koppler link`).
|
|
113
|
+
- "Check for updates" (`koppler link update`) brings in GitHub's changes; when both sides changed it refuses rather than lose either.
|
|
114
|
+
- "Open pull request" (`koppler link pr`) pushes a branch `koppler/<stack>-<id>` with the copy and opens a pull request listing the changes, after the user confirms. Once it is merged, the next update finds the two in line.
|
|
115
|
+
- "Discard my changes" (`koppler link discard`) puts the copy back to the shared file as last fetched.
|
|
116
|
+
|
|
117
|
+
## Kinds
|
|
118
|
+
|
|
119
|
+
| Kind | Recognized by | Started with | Usual port |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| `dotnet` | a `.csproj` | `dotnet run` with the app's `-LOCAL` launch profile | from the profile |
|
|
122
|
+
| `flutter` | `pubspec.yaml` | `flutter run` on the chosen device, then `flutter logs` | none |
|
|
123
|
+
| `reactnative` | `expo` or `react-native` in `package.json` | `npx expo start --ios` / `npx react-native run-ios` (or android) | none |
|
|
124
|
+
| `web` | Vite, Next.js, Create React App or Angular in `package.json` | `npm run` its `script` (else `dev`, `start`) | the framework's |
|
|
125
|
+
| `node` | any other `package.json` | `npm run` its `script` (else `dev`, `start`) | from `.env` or scripts |
|
|
126
|
+
| `go` | `go.mod` | `go run .`, or `go run ./cmd/<name>` when there is one | none |
|
|
127
|
+
| `rust` | `Cargo.toml` | `cargo run` | none |
|
|
128
|
+
| `java` | `pom.xml`, `build.gradle(.kts)` | Spring Boot: `./mvnw spring-boot:run` or `./gradlew bootRun`; other Gradle: `run` | 8080 for Spring Boot |
|
|
129
|
+
| `python` | `manage.py`, `pyproject.toml`, `requirements.txt`, `Pipfile` | Django `manage.py runserver`, FastAPI `uvicorn <module>:app --reload`, Flask `flask run`, through uv, Poetry, Pipenv or `.venv` when present | 8000, Flask 5000 |
|
|
130
|
+
| `ruby` | `Gemfile` | Rails: `bin/rails server` | 3000 |
|
|
131
|
+
| `php` | `artisan`, `composer.json` | Laravel: `php artisan serve` | 8000 |
|
|
132
|
+
| `compose` | a Compose file, when nothing above matches | `docker compose up`, and `docker compose down` on stop | the first published port |
|
|
133
|
+
| `lambda` | chosen by hand: a queue-triggered AWS Lambda | `lambda-local.mjs`: an AWS emulator in Docker (Floci) with the queue, the function and its secret; needs Docker and the AWS CLI | from `url` (the next port up is the emulator's) |
|
|
134
|
+
| `sam` | `template.yaml` with `AWS::Serverless` and no `dev` / `start` script | `sam-local.mjs`: `sam build`, then `sam local start-api` for the API-triggered functions, any runtime; deploy parameters from `samconfig.toml` for the env (or `sam.envs.<env>`), the template's declared settings from stored secrets and cable addresses (`localhost` becomes `host.docker.internal`); functions with other triggers are listed, not run. Built under `~/.local/state/koppler/sam`, never in the repo. Needs Docker and the SAM CLI | from `url`, default 3000 |
|
|
135
|
+
| `other` | chosen by hand | the app's `start` | from `url` |
|
|
136
|
+
|
|
137
|
+
A `lambda` app says how to build and trigger it: `"lambda": { "project", "handler", "runtime", "timeout", "routes": ["retry_request"], "secret": { "id": "<the Secrets Manager name the function asks for>", "from": "<the stored secret holding its JSON>" } }`.
|
|
138
|
+
Callers post to `url` plus a route, as they post to API Gateway when deployed.
|
|
139
|
+
A callee whose address lives inside that secret is a call with `how: secret` and `key`, the secret's key that holds the address: `koppler stack set apps.<lambda>.consumes.<callee> '{"how":"secret","key":"WalletApiUrl"}'`. A cable to the local callee then sets that key over the secret when the Lambda starts; Stop takes it back.
|
|
140
|
+
|
|
141
|
+
When a recipe cannot tell how the app starts (several Go commands, a Python repo with no known framework, plain Maven), `koppler run` says so and asks for `start`.
|
|
142
|
+
The port in the app's `url` wins over the usual one. The recipes live in the app's `core/kinds`; only .NET, Flutter, Node and the web frameworks have been run for real so far.
|
|
143
|
+
|
|
144
|
+
## Adding apps
|
|
145
|
+
|
|
146
|
+
Apps join the stack through the repo picker on the canvas: `koppler init --pick` (it opens by itself on a stack with no apps) or "Add repos" above the Apps list.
|
|
147
|
+
It lists the clones found on this machine by folder, with the kind each looks like (.NET, Flutter, Node, web; ask when it cannot tell) and a name taken from the repo, which can be edited.
|
|
148
|
+
Only the ticked ones are written to `stack.json`, as `{ name, kind, slug, pending }`: listed, but not set up yet. Their run facts are collected the first time they are used.
|
|
149
|
+
|
|
150
|
+
## Who calls whom
|
|
151
|
+
|
|
152
|
+
Map every app's outgoing calls, not just the app you started from: a missing or empty `consumes` means "not mapped yet", never "calls nothing".
|
|
153
|
+
`koppler sync` helps: it searches each cloned app's repo for the other apps' deployed hosts (`deployedUrl`, and each `consumes.<app>.deployed`) and prints:
|
|
154
|
+
|
|
155
|
+
- `FOUND a -> b`: a call in `a`'s repo that `stack.json` does not list. The canvas lets you cable it; confirm how a local run repoints it, then add it with `koppler stack set apps.<a>.consumes.<b> '{...}'`.
|
|
156
|
+
- `UNKNOWN app`: no deployed address is known for it, so calls to it cannot be found. Add its `deployedUrl`.
|
|
157
|
+
- `SHARED host`: two apps use the same address (e.g. one app's QA URL pointing at another app), so calls to it are not attributed. Check those by hand.
|
|
158
|
+
|
|
159
|
+
Tests, docs, lock files and migration tools are not searched.
|
|
160
|
+
|
|
161
|
+
## Drafting from Terraform
|
|
162
|
+
|
|
163
|
+
`koppler discover --deployed [app...]` reads the Terraform in each clone (no cloud access) and drafts, per environment:
|
|
164
|
+
|
|
165
|
+
- the app's deployed address (API Gateway domain, CDN alias or DNS record), marked `NEW`, `matches stack.json` or `OTHER than stack.json`;
|
|
166
|
+
- the names of its settings, and which of them hold or point to a secret;
|
|
167
|
+
- Secrets Manager secret names and Parameter Store paths;
|
|
168
|
+
- calls: a setting whose address belongs to another app.
|
|
169
|
+
|
|
170
|
+
It reads out names and plain addresses only (a host, or an http(s) URL with no credentials or query); every other value stays in the repo.
|
|
171
|
+
Environments come from variable files named after them (`deployment-dev.tfvars`) or, without those, from trying each stack environment and alias as the Terraform workspace.
|
|
172
|
+
What it cannot work out (a domain passed in by the deploy pipeline, a name built from a random suffix) is reported as not found or counted.
|
|
173
|
+
The draft, with a `suggestions` block shaped like `stack.json`, goes to `<state>/stack.deployed.json`. Review it, then keep what holds with `koppler discover --apply` or `koppler stack set`.
|
|
174
|
+
|
|
175
|
+
## Sign-ins that are not AWS
|
|
176
|
+
|
|
177
|
+
`sso` is shorthand for an AWS profile.
|
|
178
|
+
Add `account` (the 12-digit id) next to it when the profile must be one account: signed in to another one then counts as not signed in, and nothing starts.
|
|
179
|
+
A declared sign-in does the same inside its own `check` command.
|
|
180
|
+
Any other sign-in (Azure, Google Cloud, Firebase, a company tool) is declared once at the top of the stack and named by the apps that need it:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
"signIns": {
|
|
184
|
+
"acme-dev": {
|
|
185
|
+
"what": "Azure",
|
|
186
|
+
"check": "az account get-access-token --tenant acme",
|
|
187
|
+
"login": "az login --tenant acme",
|
|
188
|
+
"env": { "AZURE_TENANT_ID": "acme" }
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"apps": { "orders": { "prereqs": [{ "signIn": "acme-dev" }] } }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- `check` succeeds while the sign-in holds. Koppler runs it before a run starts anything, and it is a command of the stack, so it runs only after `koppler doctor --approve`.
|
|
195
|
+
- `login` is what the user runs to sign in. Koppler shows it and never runs it.
|
|
196
|
+
- `env` is given to the app when Koppler starts it.
|
|
197
|
+
- Like `sso`, a sign-in belongs to one environment: each environment that needs one lists its own.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Live status on the canvas
|
|
2
|
+
|
|
3
|
+
The canvas shows each app's checklist, whether its port answers, and the tail of its log.
|
|
4
|
+
The page asks the canvas server for the status every 1.5 seconds.
|
|
5
|
+
|
|
6
|
+
## What the agent maintains: each card's steps
|
|
7
|
+
|
|
8
|
+
Set one step at a time; never write the status file yourself:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
koppler status step orders-api "Start" user --detail "Run in your own terminal." --copy "<exact command>" --port 5001
|
|
12
|
+
koppler status step orders-api "Start" done # the same label replaces the step
|
|
13
|
+
koppler status step orders-api --clear # take your steps off the card
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
What the card then holds:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"stack": { "env": "qa", "mode": "auto" },
|
|
21
|
+
"apps": {
|
|
22
|
+
"<app id>": {
|
|
23
|
+
"port": 5001,
|
|
24
|
+
"log": "/absolute/path/to/app.log",
|
|
25
|
+
"steps": [
|
|
26
|
+
{ "label": "Changes", "state": "done", "detail": "QA-LOCAL points Orders API at http://localhost:5001." },
|
|
27
|
+
{ "label": "Prerequisites", "state": "done", "detail": "AWS login, Redis tunnel on :6379." },
|
|
28
|
+
{ "label": "Start", "state": "user", "detail": "Run in your own terminal.", "copy": "<exact command>" },
|
|
29
|
+
{ "label": "Check", "state": "todo", "detail": "http://localhost:5001 answers." }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `state`: `done`, `todo`, `user` (waiting on the user) or `failed`. Keep the four steps in this order, matching the output format in `SKILL.md`.
|
|
37
|
+
- `copy`: only on a step the user must run; the card shows a "Copy command" button. Take the title, why and command from `koppler doctor --json`, so the wording matches the terminal.
|
|
38
|
+
- `--port`: leave out for apps with no port (the mobile app). `--log <file>`: only when the app logs somewhere else than the default file below; it must be a `.log`, `.out` or `.txt` file.
|
|
39
|
+
- Set a step again whenever it changes. Secret values are masked.
|
|
40
|
+
- A run Koppler applies itself (`koppler up --run`) writes its own steps per app to `<state>/progress.json` (branch, remembered change, tunnels, local queue, start), and the step it stopped on is marked failed. Cards show them when you wrote no steps for that app; a Stop clears them.
|
|
41
|
+
|
|
42
|
+
## What publishes it: `status-watch.mjs`
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
koppler canvas # while the canvas is open
|
|
46
|
+
node <app>/dist/status-watch.mjs --once # publish a single snapshot
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Every 1.5 seconds it puts the status together from those steps, adding `listening` (a TCP connect to the port) and `logTail` (the end of the log).
|
|
50
|
+
Log lines are masked for passwords, tokens, JWTs, AWS keys and credentials inside addresses before they reach the page, `koppler logs`, a failed start's output or a run handed to the agent.
|
|
51
|
+
The files in the logs folder are not masked: read a log with `koppler logs <name>`, never the file itself.
|
|
52
|
+
Started through `koppler`, it keeps running across agent sessions.
|
|
53
|
+
|
|
54
|
+
## Where logs come from
|
|
55
|
+
|
|
56
|
+
| App started by | Log |
|
|
57
|
+
|---|---|
|
|
58
|
+
| The agent or the user, through `koppler run <app> ...` | written by `koppler` |
|
|
59
|
+
| The user, in the foreground of their own terminal | `bin/run-logged <app> -- <command>` |
|
|
60
|
+
| The user, from an IDE: a Flutter app | `koppler run <app> --cwd <repo> --restart -- fvm flutter logs -d <device id>` copies the app's output from the simulator, emulator or phone (`fvm flutter devices` lists ids). It follows the app across restarts and shows lines only when the app prints |
|
|
61
|
+
| The user, from an IDE: anything else | none; the output stays in the IDE. The card still shows the port state and checklist |
|
|
62
|
+
| A web front end's browser console | not captured yet; its dev server's output is, when started through `koppler run` |
|
|
63
|
+
|
|
64
|
+
Both write `~/.local/state/koppler/logs/<app>.log` (private to the user) and restart the file on each run, so the `koppler run` name must be the app id for the card to find it.
|
|
65
|
+
|
|
66
|
+
## Layout autosave
|
|
67
|
+
|
|
68
|
+
The board is kept by the canvas server as the user changes it, so a reload brings it back.
|
|
69
|
+
While `status-watch.mjs` runs, it listens on `127.0.0.1` (port 4747 or `KOPPLER_PORT`; the token is kept in `~/.local/state/koppler/canvas-token`, so the page address survives restarts) and handles:
|
|
70
|
+
|
|
71
|
+
| Request | What it does |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `GET /s/<token>/` and `/s/<token>/data/<name>.json` | serves the canvas and its data; the URL is in `<state>/canvas-url` |
|
|
74
|
+
| `POST /state?token=…` | keeps only known layout fields and writes `<state>/state.json`, which the page loads on open |
|
|
75
|
+
| `POST /preset?token=…` | body `{ "url" }`: reads a preset file from an https GitHub link (through the user's `gh` login, else as a public file, 300 KB at most) and returns its presets with only known fields; any other address is refused |
|
|
76
|
+
| `POST /secret?token=…`, `/secret-remove`, `/secret-test` | body `{ app, env, name }` (+ `value` to set): store, remove or test one secret the stack declares for that app and environment; nothing returns a value, and the test answers only `{ ok, message }` |
|
|
77
|
+
| `POST /apps?token=…` | body `{ apps: [{ slug, name?, kind? }] }`: adds clones the user picked to `stack.json` as apps not set up yet (only clones the scan found, never one already there), then reloads the stack |
|
|
78
|
+
| `POST /prompt?token=…` | a run/Stop from the standalone page (`koppler` or `koppler-stop` only); appended to `<state>/prompts.jsonl`, which `koppler listen` prints and consumes, and kept as a run in `<state>/history.jsonl` |
|
|
79
|
+
|
|
80
|
+
The full list of routes is in `app/src/server/routes.ts`.
|
|
81
|
+
Everything else gets 404, 400 or 413.
|
|
82
|
+
The page saves 400 ms after each change, and tries again when the server did not take it. The sidebar says whether the layout is being saved.
|
|
83
|
+
|
|
84
|
+
## Mobile app detection
|
|
85
|
+
|
|
86
|
+
The mobile app has no port, so for registry apps with a `mobile` field (`iosBundleId`, `androidPackage`) the watcher checks every 5 seconds:
|
|
87
|
+
- iOS: each booted simulator's `launchctl list` for `UIKitApplication:<bundle id>[`
|
|
88
|
+
- Android: `adb shell pidof <package>` on each connected device, with the model from `getprop ro.product.model`
|
|
89
|
+
- the matching `flutter run` process for `--flavor` and `--machine` (started from an IDE) or not (CLI)
|
|
90
|
+
|
|
91
|
+
It publishes `running: [{ platform, device, deviceId, pid, flavor, source }]` (empty when not running); the card shows it next to the status dot.
|
|
92
|
+
QA and PROD builds share the same id, so the flavor is only known while `flutter run` is attached; a build launched from the home screen shows no flavor.
|