koppler 0.0.0-stage → 0.0.1
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 +73 -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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Yanowsky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Koppler
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run the part of your stack you need on your own machine, without learning how each backend is run.
|
|
4
|
+
|
|
5
|
+
Pick apps on a canvas and plug cables between them, then press **Run**.
|
|
6
|
+
Koppler does the wiring, the tunnels, the local queues and the start order, and reverts all of it when you press Stop.
|
|
7
|
+
Everything you leave unplugged keeps calling the deployed environment you chose.
|
|
8
|
+
|
|
9
|
+
Koppler is for local development.
|
|
10
|
+
It helps most when you work on one app (a mobile app, a front end, one API) and need a few of its neighbours running next to it.
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
- **A shared stack.** The team describes its apps once in a `stack.json` in a repo they share: what each app is, how it runs, what it calls, which queues it uses, which secrets it needs.
|
|
15
|
+
- **A board per person.** On the canvas each app is a card. A cable from one card to another means "this caller uses my local copy of that app". Team boards load a usual setup in one click.
|
|
16
|
+
- **Run.** Koppler switches branches, points callers at the local apps, opens tunnels, starts local queues and starts the apps callees first, checking that each one answers.
|
|
17
|
+
- **Live cards.** Each card shows its steps, whether its port answers, and its log.
|
|
18
|
+
- **Settings, not JSON.** Every fact of the stack is edited in the canvas's settings or with `koppler stack set`, checked before it is kept; a lock keeps a field as it is. Nobody edits `stack.json` by hand.
|
|
19
|
+
- **Stop and revert.** One click stops what Koppler started and undoes every temporary change. Apps you started yourself are left alone.
|
|
20
|
+
- **Secrets stay yours.** You store them in the macOS Keychain (on Linux, in files only you can read); Koppler starts an app with them and never shows them.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g koppler
|
|
26
|
+
koppler init
|
|
27
|
+
koppler canvas
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`koppler init` finds your repos and your team's stack (or starts a new one), and `koppler canvas` opens the canvas in your browser.
|
|
31
|
+
[Getting started](https://github.com/evan-mcgeek/koppler/blob/main/docs/getting-started.md) walks through the first run.
|
|
32
|
+
|
|
33
|
+
Koppler is not on npm yet. Until it is, install it from a clone:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/evan-mcgeek/koppler.git
|
|
37
|
+
npm install -g ./koppler/app
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## The agent add-on
|
|
41
|
+
|
|
42
|
+
Koppler works on its own.
|
|
43
|
+
With an AI agent it does more: you describe a setup in chat ("run the Orders API against my local Payments API"), the agent fills in what the stack is missing from the repos, takes notes like "use the test account" into a run, and explains and fixes what fails.
|
|
44
|
+
|
|
45
|
+
In Claude Code, add the plugin:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
/plugin marketplace add evan-mcgeek/koppler
|
|
49
|
+
/plugin install koppler@koppler
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For any other agent (GitHub Copilot CLI, VS Code, OpenCode), or Claude Code without the plugin, run `koppler agent init`.
|
|
53
|
+
It links the [skill](https://github.com/evan-mcgeek/koppler/blob/main/plugin/skills/koppler-run) into each agent it finds and registers `koppler mcp`, its MCP server.
|
|
54
|
+
Either way, the agent drives the `koppler` command, so install the app first.
|
|
55
|
+
|
|
56
|
+
## Requirements
|
|
57
|
+
|
|
58
|
+
- macOS (secrets live in the Keychain) or Linux (secrets live in private files, without the per-read prompt; see [security](https://github.com/evan-mcgeek/koppler/blob/main/docs/security.md)).
|
|
59
|
+
- git and Node 22 or newer.
|
|
60
|
+
|
|
61
|
+
Everything else depends on your stack (the .NET SDK, Flutter, Docker, the AWS CLI, ...).
|
|
62
|
+
`koppler doctor` lists what your stack needs on your machine and what is missing.
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
- [Getting started](https://github.com/evan-mcgeek/koppler/blob/main/docs/getting-started.md): install, the first stack, the first run.
|
|
67
|
+
- [The canvas](https://github.com/evan-mcgeek/koppler/blob/main/docs/canvas.md): boards, cables, queues, environments, Run and Stop.
|
|
68
|
+
- [App kinds](https://github.com/evan-mcgeek/koppler/blob/main/docs/app-kinds.md): what Koppler can run and how.
|
|
69
|
+
- [Secrets and safety](https://github.com/evan-mcgeek/koppler/blob/main/docs/security.md): how secrets are kept, and what an agent may and may not do.
|
|
70
|
+
- [Commands](https://github.com/evan-mcgeek/koppler/blob/main/docs/commands.md): every `koppler` command.
|
|
71
|
+
- [Stack format](https://github.com/evan-mcgeek/koppler/blob/main/plugin/skills/koppler-run/references/stack.md): the `stack.json` reference.
|
|
72
|
+
- [Releasing](https://github.com/evan-mcgeek/koppler/blob/main/docs/releasing.md): how a version tag publishes Koppler to npm.
|
|
73
|
+
|
|
74
|
+
## Repository
|
|
75
|
+
|
|
76
|
+
| Folder | What |
|
|
77
|
+
|---|---|
|
|
78
|
+
| [`app/`](https://github.com/evan-mcgeek/koppler/blob/main/app) | The `koppler` package: the command, the engine, the canvas and its local server |
|
|
79
|
+
| [`plugin/`](https://github.com/evan-mcgeek/koppler/blob/main/plugin) | The agent add-on: a Claude Code plugin with the `koppler-run` skill |
|
|
80
|
+
| [`docs/`](https://github.com/evan-mcgeek/koppler/blob/main/docs) | User documentation |
|
|
81
|
+
|
|
82
|
+
Koppler runs from `app/dist/`, which `npm run build` makes from `app/src/` (`npm install` first).
|
|
83
|
+
While working on Koppler itself, link `app/scripts/koppler-dev.mjs` as `koppler`: it rebuilds when the source is newer, then runs the build.
|
|
84
|
+
Run the tests with `npm test` in `app/`; unit tests sit next to their module as `<name>.test.ts`.
|
|
85
|
+
The canvas has tests of its own that drive it in a browser: `npm install`, `npx playwright install chromium` once, then `npm run test:canvas`.
|
|
86
|
+
`npm run lint`, `npm run types` and `npm run format:check` are what CI checks besides the tests; `npm run format` formats.
|
|
87
|
+
|
|
88
|
+
## Reporting bugs
|
|
89
|
+
|
|
90
|
+
Open an [issue](https://github.com/evan-mcgeek/koppler/issues/new?template=bug_report.md) with the steps to reproduce, the logs and, if you have one, an idea for the fix.
|
|
91
|
+
`koppler status --debug [app...]` gives versions and recent logs with secrets and company names taken out, ready to paste.
|
|
92
|
+
With the add-on, the agent does the same on its own: when it finds a bug in Koppler, it drafts the issue and files it once you agree.
|
|
93
|
+
|
|
94
|
+
## Status
|
|
95
|
+
|
|
96
|
+
Early.
|
|
97
|
+
Used daily on .NET, Flutter, Node and AWS (Lambda, SAM, SQS, Kafka) apps behind Cloudflare tunnels, on macOS.
|
|
98
|
+
See the [project board](https://github.com/users/evan-mcgeek/projects/2) for what is next.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
[MIT](https://github.com/evan-mcgeek/koppler/blob/main/LICENSE)
|
package/bin/run-logged
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Runs a command and copies its output to the app's log file, which the canvas shows.
|
|
3
|
+
# Usage: run-logged <app> -- <command...>
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
app=${1:?usage: run-logged <app> -- <command...>}; shift
|
|
6
|
+
[[ $app =~ ^[a-z0-9-]+$ ]] || { echo "invalid app id: $app" >&2; exit 1; }
|
|
7
|
+
[[ ${1:-} == -- ]] && shift
|
|
8
|
+
(( $# )) || { echo "usage: run-logged <app> -- <command...>" >&2; exit 1; }
|
|
9
|
+
|
|
10
|
+
dir=${KOPPLER_LOGS:-$HOME/.local/state/koppler/logs}
|
|
11
|
+
umask 077; mkdir -p "$dir"
|
|
12
|
+
log=$dir/$app.log
|
|
13
|
+
printf '%s $ %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" > "$log"
|
|
14
|
+
|
|
15
|
+
"$@" 2>&1 | tee -a "$log"
|
|
16
|
+
exit "${PIPESTATUS[0]}"
|
package/bin/secret-list
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Lists stored secret names for an app (or all apps). Never prints values.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
here=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")" && pwd)
|
|
5
|
+
[[ -z ${1:-} || $1 =~ ^[a-z0-9-]+(@[a-z0-9-]+)?$ ]] || { echo "invalid app id: $1" >&2; exit 1; }
|
|
6
|
+
out=$("$here/secret-store" names ${1:+"$1"})
|
|
7
|
+
[[ -n $out ]] && echo "$out" || echo "no secrets stored${1:+ for $1}"
|
package/bin/secret-psql
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Opens psql with a stored database connection string, or adds it to ~/.pgpass so psql and other tools find the password.
|
|
3
|
+
# Run in your own terminal, not through an agent. The password is never printed.
|
|
4
|
+
# Usage: secret-psql <app or app@env> <ENV_NAME> [psql args...] open psql
|
|
5
|
+
# secret-psql <app or app@env> <ENV_NAME> --pgpass add or replace its ~/.pgpass line; never for a protected store
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
app=${1:?usage: secret-psql <app> <ENV_NAME> [--pgpass | psql args...]}; key=${2:?usage: secret-psql <app> <ENV_NAME> [--pgpass | psql args...]}; shift 2
|
|
8
|
+
[[ $app =~ ^[a-z0-9-]+(@[a-z0-9-]+)?$ ]] || { echo "invalid app id: $app" >&2; exit 2; }
|
|
9
|
+
[[ $key =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || { echo "invalid env name: $key" >&2; exit 2; }
|
|
10
|
+
here=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")" && pwd)
|
|
11
|
+
if [[ ${1:-} == --pgpass ]] && "$here/secret-store" protected "$app"; then
|
|
12
|
+
echo "$app is protected: its password is never copied into a plain file. Open psql instead: koppler secret psql $app $key" >&2; exit 2
|
|
13
|
+
fi
|
|
14
|
+
value=$("$here/secret-store" value "$app" "$key") || value=
|
|
15
|
+
[[ -n $value ]] || { echo "$key is not stored for $app" >&2; exit 2; }
|
|
16
|
+
|
|
17
|
+
# ponytail: reads Key=Value;... as .NET writes it; a value containing ";" needs a real parser
|
|
18
|
+
field() { printf '%s' "$value" | tr ';' '\n' | awk -F= -v k="$1" 'tolower($1) ~ "^ *(" k ") *$" { sub(/^[^=]*=/, ""); print; exit }'; }
|
|
19
|
+
host=$(field 'host|server'); port=$(field 'port'); db=$(field 'database|db'); user=$(field 'username|user id|user|uid'); pass=$(field 'password|pwd')
|
|
20
|
+
[[ -n $host && -n $user ]] || { echo "$key is not a database connection string Koppler reads" >&2; exit 2; }
|
|
21
|
+
port=${port:-5432}; db=${db:-postgres}
|
|
22
|
+
|
|
23
|
+
if [[ ${1:-} == --pgpass ]]; then
|
|
24
|
+
pgpass=${PGPASSFILE:-$HOME/.pgpass}
|
|
25
|
+
umask 077; touch "$pgpass"; chmod 600 "$pgpass"
|
|
26
|
+
esc() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/:/\\:/g'; }
|
|
27
|
+
tmp=$(mktemp "$pgpass.XXXXXX")
|
|
28
|
+
# One line per host name people type for a local tunnel.
|
|
29
|
+
hosts=("$host"); if [[ $host == localhost || $host == 127.0.0.1 ]]; then hosts=(localhost 127.0.0.1); fi
|
|
30
|
+
starts=$(for h in "${hosts[@]}"; do printf '%s:%s:%s:%s:\n' "$(esc "$h")" "$port" "$(esc "$db")" "$(esc "$user")"; done)
|
|
31
|
+
# Drops the lines it replaces, matched as plain text.
|
|
32
|
+
STARTS=$starts awk 'BEGIN { n = split(ENVIRON["STARTS"], s, "\n") } { for (i = 1; i <= n; i++) if (index($0, s[i]) == 1) next; print }' "$pgpass" > "$tmp"
|
|
33
|
+
while IFS= read -r start; do printf '%s%s\n' "$start" "$(esc "$pass")" >> "$tmp"; done <<< "$starts"
|
|
34
|
+
mv "$tmp" "$pgpass"
|
|
35
|
+
echo "Added $host:$port, database $db, user $user to $pgpass"
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
psql=$(command -v psql || ls /opt/homebrew/opt/libpq/bin/psql /usr/local/opt/libpq/bin/psql 2>/dev/null | head -1)
|
|
40
|
+
[[ -n $psql ]] || { echo "psql is not installed (brew install libpq)" >&2; exit 2; }
|
|
41
|
+
PGPASSWORD=$pass exec "$psql" -h "$host" -p "$port" -U "$user" -d "$db" "$@"
|
package/bin/secret-set
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stores one secret for an app without echoing it. Run in your own terminal, not through an agent.
|
|
3
|
+
# Usage: secret-set <app> <ENV_NAME> (hidden prompt)
|
|
4
|
+
# <fetch command> | secret-set <app> <ENV_NAME> --stdin (never shown on screen)
|
|
5
|
+
# For an environment other than the stack's first, name the store <app>@<env>, e.g. orders-api@prod.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
here=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")" && pwd)
|
|
8
|
+
app=${1:?usage: secret-set <app> <ENV_NAME>}; key=${2:?usage: secret-set <app> <ENV_NAME>}
|
|
9
|
+
[[ $app =~ ^[a-z0-9-]+(@[a-z0-9-]+)?$ ]] || { echo "invalid app id: $app" >&2; exit 1; }
|
|
10
|
+
[[ $key =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || { echo "invalid env name: $key" >&2; exit 1; }
|
|
11
|
+
from_stdin=false; [[ ${3:-} == --stdin ]] && from_stdin=true
|
|
12
|
+
$from_stdin || [[ -t 0 ]] || { echo "run this in an interactive terminal, or pipe the value in with --stdin" >&2; exit 1; }
|
|
13
|
+
|
|
14
|
+
if $from_stdin; then value=$(cat); value=${value%$'\n'}; else IFS= read -rsp "Value for $key ($app): " value; echo; fi
|
|
15
|
+
[[ -n $value ]] || { echo "empty value, nothing saved" >&2; exit 1; }
|
|
16
|
+
printf '%s' "$value" | "$here/secret-store" set "$app" "$key"
|
|
17
|
+
echo "Saved $key for $app (${#value} chars)"
|
package/bin/secret-store
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Where Koppler keeps secret values: the macOS Keychain, or private files where there is none (or when KOPPLER_SECRETS names a folder).
|
|
3
|
+
# The other secret-* scripts and with-secrets go through this one. A store is <app> or <app>@<env>.
|
|
4
|
+
# Usage: secret-store set <store> <NAME> value on stdin
|
|
5
|
+
# secret-store names [<store>] "<store>: NAME NAME" per store; never values
|
|
6
|
+
# secret-store value <store> <NAME> the value, for with-secrets and the checks only
|
|
7
|
+
# secret-store remove <store> <NAME>
|
|
8
|
+
# secret-store migrate move values from the private files into the Keychain
|
|
9
|
+
# secret-store protected <store> exit 0 when the store is protected
|
|
10
|
+
# In the Keychain, a protected store asks you on every read; the others are readable without asking, by anything running as you.
|
|
11
|
+
set -uo pipefail
|
|
12
|
+
cmd=${1:?usage: secret-store set|names|value|remove|migrate|protected ...}; shift
|
|
13
|
+
store=${1:-}; name=${2:-}
|
|
14
|
+
dir=${KOPPLER_SECRETS:-$HOME/.config/koppler/secrets}
|
|
15
|
+
backend=${KOPPLER_SECRET_BACKEND:-$([[ -z ${KOPPLER_SECRETS:-} && $(uname) == Darwin ]] && echo keychain || echo files)}
|
|
16
|
+
prefix=${KOPPLER_KEYCHAIN_PREFIX:-koppler}
|
|
17
|
+
# A keychain file of its own instead of the user's default one (KOPPLER_KEYCHAIN), e.g. for the tests.
|
|
18
|
+
in_keychain=(); [[ -n ${KOPPLER_KEYCHAIN:-} ]] && in_keychain=("$KOPPLER_KEYCHAIN")
|
|
19
|
+
|
|
20
|
+
check_store() { [[ $store =~ ^[a-z0-9-]+(@[a-z0-9-]+)?$ ]] || { echo "invalid app id: $store" >&2; exit 2; }; }
|
|
21
|
+
check_name() { [[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || { echo "invalid env name: $name" >&2; exit 2; }; }
|
|
22
|
+
# Koppler says which stores are protected (KOPPLER_SECRET_PROTECTED=1 or 0, from the stack's protected environments).
|
|
23
|
+
# Called without it, every store of a named environment (<app>@<env>) is protected: never decided by the environment's name.
|
|
24
|
+
protected() { case ${KOPPLER_SECRET_PROTECTED:-} in 1) return 0 ;; 0) return 1 ;; esac; [[ $1 == *@* ]]; }
|
|
25
|
+
|
|
26
|
+
files_set() {
|
|
27
|
+
local file=$dir/$store.env tmp value; value=$(cat)
|
|
28
|
+
umask 077; mkdir -p "$dir"; chmod 700 "$dir"; touch "$file"; chmod 600 "$file"
|
|
29
|
+
tmp=$(mktemp "$dir/.tmp.XXXXXX")
|
|
30
|
+
grep -v "^export $name=" "$file" > "$tmp" || true
|
|
31
|
+
printf 'export %s=%q\n' "$name" "$value" >> "$tmp"
|
|
32
|
+
mv "$tmp" "$file"
|
|
33
|
+
}
|
|
34
|
+
files_names() {
|
|
35
|
+
shopt -s nullglob
|
|
36
|
+
local f files=("$dir"/${store:-*}.env)
|
|
37
|
+
for f in "${files[@]}"; do [[ -f $f ]] && echo "$(basename "$f" .env): $(sed -nE 's/^export ([A-Za-z_][A-Za-z0-9_]*)=.*/\1/p' "$f" | paste -sd ' ' -)"; done
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
files_value() { local file=$dir/$store.env; [[ -f $file ]] || return 1; (set -a; source "$file"; [[ -n ${!name+x} ]] && printf '%s' "${!name}"); }
|
|
41
|
+
files_remove() { local file=$dir/$store.env tmp; [[ -f $file ]] || return 0; tmp=$(mktemp "$dir/.tmp.XXXXXX"); grep -v "^export $name=" "$file" > "$tmp" || true; mv "$tmp" "$file"; [[ -s $file ]] || rm -f "$file"; }
|
|
42
|
+
|
|
43
|
+
keychain_set() {
|
|
44
|
+
# Kept as hex text, so any value (quotes, accents, several lines) comes back exactly as it went in.
|
|
45
|
+
local hex trust; hex=$(xxd -p | tr -d '\n')
|
|
46
|
+
[[ -n $hex ]] || { echo "empty value, nothing saved" >&2; exit 1; }
|
|
47
|
+
protected "$store" && trust='-T ""' || trust='-T /usr/bin/security'
|
|
48
|
+
security delete-generic-password -s "$prefix:$store" -a "$name" ${in_keychain[@]+"${in_keychain[@]}"} >/dev/null 2>&1
|
|
49
|
+
# Through security's own prompt, so the value is never on a command line other processes can see.
|
|
50
|
+
printf 'add-generic-password -s "%s" -a "%s" -l "%s" %s -X %s%s\n' "$prefix:$store" "$name" "Koppler: $name for $store" "$trust" "$(printf '%s' "$hex" | xxd -p | tr -d '\n')" "${KOPPLER_KEYCHAIN:+ \"$KOPPLER_KEYCHAIN\"}" | security -i >/dev/null
|
|
51
|
+
}
|
|
52
|
+
keychain_names() {
|
|
53
|
+
security dump-keychain ${in_keychain[@]+"${in_keychain[@]}"} 2>/dev/null | awk -v p="\"svce\"<blob>=\"$prefix:" -v want="$store" '
|
|
54
|
+
/^keychain:/ { acct = ""; svce = "" }
|
|
55
|
+
index($0, "\"acct\"<blob>=\"") { v = $0; sub(/.*"acct"<blob>="/, "", v); sub(/"$/, "", v); acct = v }
|
|
56
|
+
index($0, p) { v = $0; sub(/.*"svce"<blob>="[^:]*:/, "", v); sub(/"$/, "", v); svce = v }
|
|
57
|
+
acct != "" && svce != "" { if (want == "" || want == svce) names[svce] = names[svce] " " acct; acct = ""; svce = "" }
|
|
58
|
+
END { for (s in names) print s ":" names[s] }' | sort
|
|
59
|
+
}
|
|
60
|
+
keychain_value() { local hex; hex=$(security find-generic-password -s "$prefix:$store" -a "$name" -w ${in_keychain[@]+"${in_keychain[@]}"} 2>/dev/null) || return 1; printf '%s' "$hex" | xxd -r -p; }
|
|
61
|
+
keychain_remove() { security delete-generic-password -s "$prefix:$store" -a "$name" ${in_keychain[@]+"${in_keychain[@]}"} >/dev/null 2>&1; return 0; }
|
|
62
|
+
|
|
63
|
+
case $cmd in
|
|
64
|
+
set) check_store; check_name; "${backend}_set" ;;
|
|
65
|
+
names) [[ -z $store ]] || check_store; "${backend}_names" ;;
|
|
66
|
+
value) check_store; check_name; "${backend}_value" ;;
|
|
67
|
+
remove) check_store; check_name; "${backend}_remove" ;;
|
|
68
|
+
protected) check_store; protected "$store" ;;
|
|
69
|
+
migrate)
|
|
70
|
+
[[ $backend == keychain ]] || { echo "nothing to migrate: values are kept in files here" >&2; exit 2; }
|
|
71
|
+
shopt -s nullglob
|
|
72
|
+
for f in "$dir"/*.env; do
|
|
73
|
+
store=$(basename "$f" .env); check_store; moved=0
|
|
74
|
+
for name in $(sed -nE 's/^export ([A-Za-z_][A-Za-z0-9_]*)=.*/\1/p' "$f"); do
|
|
75
|
+
files_value | keychain_set
|
|
76
|
+
security find-generic-password -s "$prefix:$store" -a "$name" ${in_keychain[@]+"${in_keychain[@]}"} >/dev/null 2>&1 || { echo "$store: $name could not be moved; its file is kept" >&2; moved=-1; break; }
|
|
77
|
+
moved=$((moved + 1))
|
|
78
|
+
done
|
|
79
|
+
(( moved >= 0 )) && { rm -f "$f"; echo "$store: moved $moved to the Keychain"; }
|
|
80
|
+
done ;;
|
|
81
|
+
*) echo "usage: secret-store set|names|value|remove|migrate|protected ..." >&2; exit 2 ;;
|
|
82
|
+
esac
|
package/bin/secret-test
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Checks that a stored credential really connects (Postgres, MySQL, Redis or Valkey). Prints only the outcome, never the value.
|
|
3
|
+
// Usage: secret-test <app or app@env> <ENV_NAME> [--type postgres|mysql|redis] exit 0 connected, 1 failed, 2 cannot be tested
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const here = path.dirname(fs.realpathSync(fileURLToPath(import.meta.url)));
|
|
9
|
+
const { main } = await import(path.join(here, '..', 'dist', 'secret-check.mjs'));
|
|
10
|
+
await main(process.argv.slice(2), here);
|
package/bin/with-secrets
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Runs a command with an app's stored secrets in its environment only. For a log on the canvas, start the app with `koppler run <app>` instead.
|
|
3
|
+
# Usage: with-secrets <app> -- <command...> (<app>@<env> for an environment other than the stack's first)
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
here=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "${BASH_SOURCE[0]}")")" && pwd)
|
|
6
|
+
app=${1:?usage: with-secrets <app> -- <command...>}; shift
|
|
7
|
+
[[ $app =~ ^[a-z0-9-]+(@[a-z0-9-]+)?$ ]] || { echo "invalid app id: $app" >&2; exit 1; }
|
|
8
|
+
[[ ${1:-} == -- ]] && shift
|
|
9
|
+
(( $# )) || { echo "usage: with-secrets <app> -- <command...>" >&2; exit 1; }
|
|
10
|
+
names=$("$here/secret-store" names "$app" | sed 's/^[^:]*: *//')
|
|
11
|
+
[[ -n $names ]] || echo "no secrets stored for $app, running without" >&2
|
|
12
|
+
for name in $names; do
|
|
13
|
+
[[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || { echo "skipping a stored name that is not a variable name" >&2; continue; }
|
|
14
|
+
# A stored value never becomes a variable that makes a runtime load other code or find its programs elsewhere.
|
|
15
|
+
case $name in
|
|
16
|
+
PATH|NODE_OPTIONS|NODE_PATH|DYLD_*|LD_*|DOTNET_STARTUP_HOOKS|DOTNET_ADDITIONAL_DEPS|DOTNET_SHARED_STORE|CORECLR_*|COR_*|BASH_ENV|ENV|PROMPT_COMMAND|PYTHONSTARTUP|PYTHONPATH|PYTHONHOME|PYTHONINSPECT|RUBYOPT|RUBYLIB|PERL5OPT|PERL5LIB|JAVA_TOOL_OPTIONS|_JAVA_OPTIONS|JDK_JAVA_OPTIONS)
|
|
17
|
+
echo "refusing to start: the stored secret $name for $app would make the app run other code; remove it (koppler secret remove $app $name)" >&2; exit 1 ;;
|
|
18
|
+
esac
|
|
19
|
+
value=$("$here/secret-store" value "$app" "$name") || { echo "could not read $name for $app (was the Keychain prompt denied?)" >&2; exit 1; }
|
|
20
|
+
export "$name=$value"
|
|
21
|
+
done
|
|
22
|
+
unset value
|
|
23
|
+
# Names only, so a runner can keep the values away from the tools it starts.
|
|
24
|
+
export KOPPLER_SECRET_NAMES="${names//$'\n'/ }"
|
|
25
|
+
exec "$@"
|
package/dist/.built
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2026-10-15T14:27:51.009Z
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// `koppler agent init`: links the skill into each agent on this Mac and registers `koppler mcp` with it.
|
|
2
|
+
// Another tool's config is only added to: a file that does not read as plain JSON is left alone, with what to add.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { sh } from "../engine/processes.js";
|
|
8
|
+
import { fail } from "../failure.js";
|
|
9
|
+
import { APP, CODE } from "../paths.js";
|
|
10
|
+
import { flagValue } from "./output.js";
|
|
11
|
+
const SKILL = 'koppler-run';
|
|
12
|
+
const USAGE = 'usage: koppler agent init [--agent claude,copilot,vscode,opencode] [--dry-run]';
|
|
13
|
+
const home = () => os.homedir();
|
|
14
|
+
const exists = (file) => fs.existsSync(file);
|
|
15
|
+
const vscodeDir = () => process.platform === 'darwin'
|
|
16
|
+
? path.join(home(), 'Library', 'Application Support', 'Code', 'User')
|
|
17
|
+
: path.join(home(), '.config', 'Code', 'User');
|
|
18
|
+
const onPath = (program) => !!sh('sh', ['-c', `command -v ${program}`]);
|
|
19
|
+
/** Adds one entry under a key of a JSON config file, creating the file when there is none. */
|
|
20
|
+
function addToJson(file, key, value, dry) {
|
|
21
|
+
let config = {};
|
|
22
|
+
if (exists(file)) {
|
|
23
|
+
try {
|
|
24
|
+
const read = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
25
|
+
if (!read || typeof read !== 'object' || Array.isArray(read))
|
|
26
|
+
throw new Error('not an object');
|
|
27
|
+
config = read;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return `left ${file} alone (not plain JSON); add under "${key}": "koppler": ${JSON.stringify(value)}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const under = (config[key] ?? {});
|
|
34
|
+
if (JSON.stringify(under['koppler']) === JSON.stringify(value))
|
|
35
|
+
return `MCP already in ${file}`;
|
|
36
|
+
if (dry)
|
|
37
|
+
return `would add koppler to ${file}`;
|
|
38
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
39
|
+
fs.writeFileSync(file, `${JSON.stringify({ ...config, [key]: { ...under, koppler: value } }, null, 2)}\n`);
|
|
40
|
+
return `MCP added to ${file}`;
|
|
41
|
+
}
|
|
42
|
+
const AGENTS = {
|
|
43
|
+
claude: {
|
|
44
|
+
name: 'Claude Code',
|
|
45
|
+
here: () => exists(path.join(home(), '.claude')) || onPath('claude'),
|
|
46
|
+
skills: () => path.join(home(), '.claude', 'skills'),
|
|
47
|
+
plugin: () => {
|
|
48
|
+
try {
|
|
49
|
+
return fs
|
|
50
|
+
.readFileSync(path.join(home(), '.claude', 'plugins', 'installed_plugins.json'), 'utf8')
|
|
51
|
+
.includes('"koppler@');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
mcp: (server, dry) => {
|
|
58
|
+
const add = ['mcp', 'add', '--scope', 'user', 'koppler', '--', server.command, ...server.args];
|
|
59
|
+
if (!onPath('claude'))
|
|
60
|
+
return `run: claude ${add.join(' ')}`;
|
|
61
|
+
if (spawnSync('claude', ['mcp', 'get', 'koppler'], { stdio: 'ignore' }).status === 0)
|
|
62
|
+
return 'MCP already added';
|
|
63
|
+
if (dry)
|
|
64
|
+
return `would run: claude ${add.join(' ')}`;
|
|
65
|
+
const added = spawnSync('claude', add, { encoding: 'utf8' });
|
|
66
|
+
return added.status === 0
|
|
67
|
+
? 'MCP added (claude mcp, user scope)'
|
|
68
|
+
: `run: claude ${add.join(' ')} (${added.stderr.trim()})`;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
copilot: {
|
|
72
|
+
name: 'GitHub Copilot CLI',
|
|
73
|
+
here: () => exists(path.join(home(), '.copilot')) || onPath('copilot'),
|
|
74
|
+
skills: () => path.join(home(), '.copilot', 'skills'),
|
|
75
|
+
mcp: (server, dry) => addToJson(path.join(home(), '.copilot', 'mcp-config.json'), 'mcpServers', { type: 'local', ...server, tools: ['*'] }, dry),
|
|
76
|
+
},
|
|
77
|
+
vscode: {
|
|
78
|
+
name: 'VS Code',
|
|
79
|
+
here: () => exists(vscodeDir()),
|
|
80
|
+
// VS Code reads skills from ~/.copilot/skills, which Copilot's own entry links.
|
|
81
|
+
skills: () => (AGENTS['copilot']?.here() ? null : path.join(home(), '.copilot', 'skills')),
|
|
82
|
+
mcp: (server, dry) => addToJson(path.join(vscodeDir(), 'mcp.json'), 'servers', { type: 'stdio', ...server }, dry),
|
|
83
|
+
},
|
|
84
|
+
opencode: {
|
|
85
|
+
name: 'OpenCode',
|
|
86
|
+
here: () => exists(path.join(home(), '.config', 'opencode')) || onPath('opencode'),
|
|
87
|
+
skills: () => path.join(home(), '.config', 'opencode', 'skills'),
|
|
88
|
+
mcp: (server, dry) => addToJson(path.join(home(), '.config', 'opencode', 'opencode.json'), 'mcp', { type: 'local', command: [server.command, ...server.args], enabled: true }, dry),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
/** The skill this Koppler carries: packed into the package, or in the clone it runs from. */
|
|
92
|
+
function skillSource() {
|
|
93
|
+
const found = [path.join(APP, 'skills', SKILL), path.join(APP, '..', 'plugin', 'skills', SKILL)].find(dir => exists(path.join(dir, 'SKILL.md')));
|
|
94
|
+
return found ?? fail(`the ${SKILL} skill is not in this Koppler (${APP})`);
|
|
95
|
+
}
|
|
96
|
+
/** Links the skill into a skills folder; a folder there that is not a link is the user's, and stays. */
|
|
97
|
+
function linkSkill(dir, source, dry) {
|
|
98
|
+
const at = path.join(dir, SKILL);
|
|
99
|
+
let stat = null;
|
|
100
|
+
try {
|
|
101
|
+
stat = fs.lstatSync(at);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Nothing there yet.
|
|
105
|
+
}
|
|
106
|
+
if (stat && !stat.isSymbolicLink())
|
|
107
|
+
return `left ${at} alone (a folder of its own); remove it to link the skill`;
|
|
108
|
+
if (stat && path.resolve(dir, fs.readlinkSync(at)) === source)
|
|
109
|
+
return `skill already linked at ${at}`;
|
|
110
|
+
if (dry)
|
|
111
|
+
return `would link the skill at ${at}`;
|
|
112
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
if (stat)
|
|
114
|
+
fs.unlinkSync(at);
|
|
115
|
+
fs.symlinkSync(source, at);
|
|
116
|
+
return `skill linked at ${at}`;
|
|
117
|
+
}
|
|
118
|
+
/** `koppler agent init`: every agent found here, or the ones named, gets the skill and the MCP server. */
|
|
119
|
+
export function agentCommand(args) {
|
|
120
|
+
if (args[0] !== 'init')
|
|
121
|
+
fail(USAGE);
|
|
122
|
+
const named = flagValue(args, '--agent')?.split(',').filter(Boolean);
|
|
123
|
+
for (const id of named ?? [])
|
|
124
|
+
if (!Object.hasOwn(AGENTS, id))
|
|
125
|
+
fail(`no agent called ${id} (${USAGE})`);
|
|
126
|
+
const dry = args.includes('--dry-run');
|
|
127
|
+
const chosen = Object.entries(AGENTS).filter(([id, agent]) => (named ? named.includes(id) : agent.here()));
|
|
128
|
+
if (!chosen.length)
|
|
129
|
+
fail(`found none of Claude Code, GitHub Copilot CLI, VS Code or OpenCode here; name one with --agent (${USAGE})`);
|
|
130
|
+
const source = fs.realpathSync(skillSource());
|
|
131
|
+
const server = onPath('koppler')
|
|
132
|
+
? { command: 'koppler', args: ['mcp'] }
|
|
133
|
+
: { command: process.execPath, args: [path.join(CODE, 'koppler.mjs'), 'mcp'] };
|
|
134
|
+
for (const [, agent] of chosen) {
|
|
135
|
+
const skills = agent.skills();
|
|
136
|
+
const skill = agent.plugin?.()
|
|
137
|
+
? 'skill comes with the koppler plugin'
|
|
138
|
+
: skills
|
|
139
|
+
? linkSkill(skills, source, dry)
|
|
140
|
+
: 'skill shared with GitHub Copilot CLI';
|
|
141
|
+
console.log(`${agent.name}: ${skill}; ${agent.mcp(server, dry)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/cli/agent.ts"],"names":[],"mappings":"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"}
|