@synoikia/create-plugin 0.1.0
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 +20 -0
- package/dist/build.d.ts +16 -0
- package/dist/build.js +32 -0
- package/dist/build.js.map +1 -0
- package/dist/check.d.ts +7 -0
- package/dist/check.js +49 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +1 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.js +56 -0
- package/dist/create.js.map +1 -0
- package/dist/files.d.ts +12 -0
- package/dist/files.js +42 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +46 -0
- package/dist/prompts.js +112 -0
- package/dist/prompts.js.map +1 -0
- package/dist/repo-root.d.ts +10 -0
- package/dist/repo-root.js +47 -0
- package/dist/repo-root.js.map +1 -0
- package/dist/repo.d.ts +68 -0
- package/dist/repo.js +222 -0
- package/dist/repo.js.map +1 -0
- package/dist/scaffold.d.ts +68 -0
- package/dist/scaffold.js +296 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/vitest.d.ts +12 -0
- package/dist/vitest.js +17 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +56 -0
- package/templates/plugin/blank/plugin.yaml +22 -0
- package/templates/plugin/blank/src/plugin.ts +74 -0
- package/templates/plugin/blank/test/plugin.test.ts +33 -0
- package/templates/plugin/common/README.md +23 -0
- package/templates/plugin/common/RUNBOOK.md +10 -0
- package/templates/plugin/common/src/index.ts +4 -0
- package/templates/plugin/common/tsconfig.build.json +9 -0
- package/templates/plugin/common/tsconfig.json +7 -0
- package/templates/plugin/common/vitest.config.ts +1 -0
- package/templates/plugin/openapi-rest/plugin.yaml +49 -0
- package/templates/plugin/openapi-rest/src/plugin.ts +89 -0
- package/templates/plugin/openapi-rest/test/e2e.test.ts +49 -0
- package/templates/plugin/openapi-rest/test/fake-upstream.ts +63 -0
- package/templates/plugin/openapi-rest/test/plugin.test.ts +66 -0
- package/templates/plugin/static-rest/plugin.yaml +61 -0
- package/templates/plugin/static-rest/src/plugin.ts +70 -0
- package/templates/plugin/static-rest/test/e2e.test.ts +46 -0
- package/templates/plugin/static-rest/test/fake-upstream.ts +39 -0
- package/templates/plugin/static-rest/test/plugin.test.ts +67 -0
- package/templates/plugin/websocket-rpc/plugin.yaml +45 -0
- package/templates/plugin/websocket-rpc/src/client.ts +96 -0
- package/templates/plugin/websocket-rpc/src/plugin.ts +98 -0
- package/templates/plugin/websocket-rpc/test/e2e.test.ts +46 -0
- package/templates/plugin/websocket-rpc/test/fake-upstream.ts +63 -0
- package/templates/plugin/websocket-rpc/test/plugin.test.ts +67 -0
- package/templates/repo/base/README.md +27 -0
- package/templates/repo/base/_github/workflows/ci.yml +27 -0
- package/templates/repo/base/_gitignore +6 -0
- package/templates/repo/base/_nvmrc +1 -0
- package/templates/repo/base/_prettierignore +3 -0
- package/templates/repo/base/_prettierrc +5 -0
- package/templates/repo/base/eslint.config.js +17 -0
- package/templates/repo/base/package.json +34 -0
- package/templates/repo/base/plugin-files.d.ts +10 -0
- package/templates/repo/base/pnpm-workspace.yaml +11 -0
- package/templates/repo/base/tsconfig.base.json +21 -0
- package/templates/repo/base/vitest.shared.ts +9 -0
- package/templates/repo/claude/CLAUDE.md +38 -0
- package/templates/repo/claude/_claude/agents/security-reviewer.md +33 -0
- package/templates/repo/claude/_claude/hooks/format.mjs +20 -0
- package/templates/repo/claude/_claude/hooks/guard-paths.mjs +43 -0
- package/templates/repo/claude/_claude/hooks/session-start.sh +13 -0
- package/templates/repo/claude/_claude/settings.json +21 -0
- package/templates/repo/claude/_claude/skills/add-plugin/SKILL.md +39 -0
- package/templates/repo/claude/_claude/skills/release-plugin/SKILL.md +18 -0
- package/templates/repo/release/RELEASING.md +27 -0
- package/templates/repo/release/_github/workflows/release.yml +89 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// PreToolUse hook: stops edits to generated files and asks before edits to the signing key.
|
|
2
|
+
// Reads the tool call from stdin; prints a permission decision for matching paths, nothing otherwise.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const RULES = [
|
|
6
|
+
{
|
|
7
|
+
match: /(^|\/)pnpm-lock\.yaml$/,
|
|
8
|
+
decision: 'deny',
|
|
9
|
+
reason: 'pnpm-lock.yaml is generated. Change package.json and run `pnpm install` instead.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
match: /^plugins\/[^/]+\/dist\//,
|
|
13
|
+
decision: 'deny',
|
|
14
|
+
reason: 'dist/ is the esbuild bundle. Edit src/ and run `pnpm --filter ./plugins/<id> build`.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
match: /^minisign\.pub$/,
|
|
18
|
+
decision: 'ask',
|
|
19
|
+
reason:
|
|
20
|
+
'minisign.pub is the key every Synoikia install pins for this repository. Changing it blocks installs until each admin confirms the new key; only for a compromised key.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
let input = '';
|
|
25
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
26
|
+
const { tool_input: toolInput = {}, cwd } = JSON.parse(input || '{}');
|
|
27
|
+
const file = toolInput.file_path ?? toolInput.notebook_path;
|
|
28
|
+
if (!file) process.exit(0);
|
|
29
|
+
|
|
30
|
+
const root = process.env.CLAUDE_PROJECT_DIR ?? cwd ?? process.cwd();
|
|
31
|
+
const rel = path.relative(root, path.resolve(root, file)).split(path.sep).join('/');
|
|
32
|
+
const rule = RULES.find((r) => r.match.test(rel));
|
|
33
|
+
if (!rule) process.exit(0);
|
|
34
|
+
|
|
35
|
+
console.log(
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
hookSpecificOutput: {
|
|
38
|
+
hookEventName: 'PreToolUse',
|
|
39
|
+
permissionDecision: rule.decision,
|
|
40
|
+
permissionDecisionReason: `${rel}: ${rule.reason}`,
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SessionStart hook: installs workspace dependencies in Claude Code cloud sessions, so lint, typecheck
|
|
3
|
+
# and tests work straight away. Local sessions are left alone.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
cd "$CLAUDE_PROJECT_DIR"
|
|
11
|
+
command -v pnpm >/dev/null 2>&1 || corepack enable
|
|
12
|
+
# stdout becomes session context; keep the install log out of it.
|
|
13
|
+
pnpm install --frozen-lockfile >&2
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-start.sh" }]
|
|
6
|
+
}
|
|
7
|
+
],
|
|
8
|
+
"PreToolUse": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
|
11
|
+
"hooks": [{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-paths.mjs" }]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"PostToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
17
|
+
"hooks": [{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.mjs" }]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-plugin
|
|
3
|
+
description: Scaffold a new Synoikia plugin under plugins/<id> with `pnpm new`, then fill in its plugin.yaml rules, discovery and tests. Use when asked to add, create or start a plugin for a new upstream service.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a plugin
|
|
7
|
+
|
|
8
|
+
## 1. Settle the basics
|
|
9
|
+
|
|
10
|
+
- **`<id>`**: lowercase, the directory name and manifest `id`; the sandbox namespace defaults to it in camelCase.
|
|
11
|
+
- **Archetype**: `openapi-rest` (the upstream serves an OpenAPI spec), `static-rest` (a REST API without one; operations are declared in `plugin.yaml`), `websocket-rpc` (JSON-RPC over WebSocket), or `blank`.
|
|
12
|
+
- **Auth**: `bearer`, `api-key` (with its header), `basic` or `none`.
|
|
13
|
+
- **Destructive operations**: which ones are `locked` (always a human who types a confirmation literal), and what that literal is.
|
|
14
|
+
- **Secrets**: which connection fields are credentials, and which fields the upstream returns hold secrets.
|
|
15
|
+
|
|
16
|
+
## 2. Scaffold
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm new --id <id> --name "<Name>" --archetype <archetype> --auth <auth> --yes
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This writes `plugins/<id>/` (manifest, `plugin.yaml`, `src/`, a fake upstream, unit and e2e tests) and runs `pnpm install`. The generated plugin already builds and passes its tests.
|
|
23
|
+
|
|
24
|
+
## 3. Describe the upstream
|
|
25
|
+
|
|
26
|
+
- `manifest.json`: connection fields and help, `sensitiveKeys` (add every secret field the upstream returns), `network.hosts`.
|
|
27
|
+
- `plugin.yaml`: `rules` for locks, splits, classification overrides, `sensitiveParams`, `sensitiveResult`, `confirm` literals and summary notes; `exclude` for plumbing the model has no business calling. A rule's `classification` can never unlock a locked operation, and anything unclassified is a write.
|
|
28
|
+
- `src/plugin.ts`: only what needs logic, such as a `splitWhen` predicate or a custom confirm source.
|
|
29
|
+
|
|
30
|
+
## 4. Tests
|
|
31
|
+
|
|
32
|
+
Extend `test/fake-upstream.ts` to answer what the plugin calls, including responses with secrets. Keep `checkPluginContract` in `test/e2e.test.ts` passing, and add unit tests for every rule that locks or masks something.
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pnpm --filter ./plugins/<id> test
|
|
36
|
+
pnpm typecheck && pnpm lint && pnpm format:check
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then run the `security-reviewer` subagent on the diff.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: release-plugin
|
|
3
|
+
description: Prepare a plugin release by bumping its version in manifest.json and package.json together, and optionally its minimum Synoikia version. Use when asked to release, publish, ship or bump the version of a plugin.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Release a plugin
|
|
7
|
+
|
|
8
|
+
A release is a version bump merged to `main`; the Release workflow packs, signs, verifies and publishes every plugin whose version isn't in the published index yet (see `RELEASING.md`).
|
|
9
|
+
|
|
10
|
+
1. **Pick the version** with semver against the last release (`git log --oneline -- plugins/<id>`):
|
|
11
|
+
- patch: fixes, catalog corrections, wording;
|
|
12
|
+
- minor: new operations, connection fields or capabilities; also any `plugin.yaml` change that unlocks an operation, drops a `sensitiveParams`/`sensitiveResult` rule or overrides a classification to read (call it out in the PR);
|
|
13
|
+
- major: a change that breaks existing instances, such as a renamed operation key or a removed connection field.
|
|
14
|
+
2. **Bump both files to the same version**: `plugins/<id>/manifest.json` and `plugins/<id>/package.json`.
|
|
15
|
+
3. **Minimum Synoikia version**, only when the change needs a newer core: `"synoikia": { "minCoreVersion": "x.y.z" }` in `package.json`. A newer SDK means bumping the manifest `sdk` range together with the `@synoikia/plugin-sdk` range, then `pnpm install`.
|
|
16
|
+
4. **Verify**: `pnpm --filter ./plugins/<id> test`, then `pnpm typecheck && pnpm lint && pnpm format:check`.
|
|
17
|
+
|
|
18
|
+
Released versions are immutable. Never touch `minisign.pub` or the signing steps of the workflow.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
This repository publishes a **signed plugin repository** that Synoikia installs from. Add it on Synoikia's Plugins → Repositories page as a signed repository:
|
|
4
|
+
|
|
5
|
+
- Index: `https://github.com/{{repository}}/releases/download/index/index.json`
|
|
6
|
+
- Public key: the key in `minisign.pub`
|
|
7
|
+
|
|
8
|
+
## One-time setup
|
|
9
|
+
|
|
10
|
+
1. Generate a password-less minisign key pair on a trusted machine: `minisign -G -W -p minisign.pub -s minisign.key`.
|
|
11
|
+
2. Commit `minisign.pub`. Store the whole `minisign.key` file (both lines) as the `MINISIGN_SECRET_KEY` Actions secret, then delete the local copy or keep it offline.
|
|
12
|
+
3. Check that `package.json` `synoikia.repository` is `{{repository}}`.
|
|
13
|
+
|
|
14
|
+
Never commit the secret key. Changing the key later makes every Synoikia install block this repository until its admin confirms the new key, so a new key is for a compromised key only.
|
|
15
|
+
|
|
16
|
+
## Releasing a plugin
|
|
17
|
+
|
|
18
|
+
Bump `version` in both the plugin's `manifest.json` and `package.json` (they must match) and merge to `main`. The Release workflow then:
|
|
19
|
+
|
|
20
|
+
1. runs every plugin's tests;
|
|
21
|
+
2. packs each plugin whose version isn't in the published index yet, as a deterministic tarball (`synoikia-plugin repo pack`);
|
|
22
|
+
3. signs each tarball and checks it against `minisign.pub`;
|
|
23
|
+
4. merges the new versions into the index (`repo index`);
|
|
24
|
+
5. installs each new version with Synoikia's own repository service, with `minisign.pub` pinned (`repo verify`);
|
|
25
|
+
6. only then creates the `<id>-v<version>` releases and replaces `index.json` on the `index` release (`repo publish`).
|
|
26
|
+
|
|
27
|
+
A released version is never changed: to ship a fix, bump the version again. A plugin can require a minimum Synoikia version with `"synoikia": { "minCoreVersion": "x.y.z" }` in its `package.json`.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes the signed plugin repository Synoikia installs from (RELEASING.md). A plugin version is
|
|
4
|
+
# released when its manifest version isn't in the published index yet: each gets a `<id>-v<version>`
|
|
5
|
+
# release with its tarball and minisign signature, and index.json on the `index` release is replaced.
|
|
6
|
+
# Released versions are never rebuilt or removed.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [main]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: release
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
release:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
with:
|
|
26
|
+
# Nothing here pushes with git; keep the token out of .git/config while dev dependencies run.
|
|
27
|
+
persist-credentials: false
|
|
28
|
+
- uses: pnpm/action-setup@v4
|
|
29
|
+
- uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version-file: .nvmrc
|
|
32
|
+
cache: pnpm
|
|
33
|
+
- run: pnpm install --frozen-lockfile
|
|
34
|
+
- run: pnpm test
|
|
35
|
+
|
|
36
|
+
- name: Download the published index
|
|
37
|
+
env:
|
|
38
|
+
GH_TOKEN: ${{ github.token }}
|
|
39
|
+
run: |
|
|
40
|
+
# Start a new index only when there really is none: on any other error (rate limit, network)
|
|
41
|
+
# stop, rather than rebuild every released version.
|
|
42
|
+
if out=$(gh release view index 2>&1); then
|
|
43
|
+
gh release download index --pattern index.json --dir .repo-current
|
|
44
|
+
elif grep -qi 'release not found' <<<"$out"; then
|
|
45
|
+
echo "No index release yet: starting a new index."
|
|
46
|
+
else
|
|
47
|
+
echo "::error::Could not read the index release: $out"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
- name: Pack new plugin versions
|
|
52
|
+
id: pack
|
|
53
|
+
run: |
|
|
54
|
+
pnpm exec synoikia-plugin repo pack
|
|
55
|
+
echo "count=$(node -p "require('./.repo/releases.json').length")" >> "$GITHUB_OUTPUT"
|
|
56
|
+
|
|
57
|
+
- name: Sign
|
|
58
|
+
if: steps.pack.outputs.count != '0'
|
|
59
|
+
env:
|
|
60
|
+
MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }}
|
|
61
|
+
run: |
|
|
62
|
+
if [ -z "$MINISIGN_SECRET_KEY" ]; then
|
|
63
|
+
echo "::error::The MINISIGN_SECRET_KEY secret is not set."
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
sudo apt-get update -qq && sudo apt-get install -y -qq minisign
|
|
67
|
+
key="$RUNNER_TEMP/minisign.key"
|
|
68
|
+
(umask 077; printf '%s\n' "$MINISIGN_SECRET_KEY" > "$key")
|
|
69
|
+
trap 'rm -f "$key"' EXIT
|
|
70
|
+
for f in .repo/*.tgz; do
|
|
71
|
+
minisign -S -s "$key" -m "$f" -t "$(basename "$f")"
|
|
72
|
+
minisign -V -p minisign.pub -m "$f"
|
|
73
|
+
done
|
|
74
|
+
|
|
75
|
+
- name: Build the index
|
|
76
|
+
if: steps.pack.outputs.count != '0'
|
|
77
|
+
run: pnpm exec synoikia-plugin repo index
|
|
78
|
+
|
|
79
|
+
# Synoikia's own repository service installs every new version with minisign.pub pinned, before
|
|
80
|
+
# anything is uploaded.
|
|
81
|
+
- name: Verify with Synoikia
|
|
82
|
+
if: steps.pack.outputs.count != '0'
|
|
83
|
+
run: pnpm exec synoikia-plugin repo verify
|
|
84
|
+
|
|
85
|
+
- name: Publish
|
|
86
|
+
if: steps.pack.outputs.count != '0'
|
|
87
|
+
env:
|
|
88
|
+
GH_TOKEN: ${{ github.token }}
|
|
89
|
+
run: pnpm exec synoikia-plugin repo publish
|