@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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -0
  3. package/dist/build.d.ts +16 -0
  4. package/dist/build.js +32 -0
  5. package/dist/build.js.map +1 -0
  6. package/dist/check.d.ts +7 -0
  7. package/dist/check.js +49 -0
  8. package/dist/check.js.map +1 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +104 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/create.d.ts +2 -0
  13. package/dist/create.js +56 -0
  14. package/dist/create.js.map +1 -0
  15. package/dist/files.d.ts +12 -0
  16. package/dist/files.js +42 -0
  17. package/dist/files.js.map +1 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.js +8 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/prompts.d.ts +46 -0
  22. package/dist/prompts.js +112 -0
  23. package/dist/prompts.js.map +1 -0
  24. package/dist/repo-root.d.ts +10 -0
  25. package/dist/repo-root.js +47 -0
  26. package/dist/repo-root.js.map +1 -0
  27. package/dist/repo.d.ts +68 -0
  28. package/dist/repo.js +222 -0
  29. package/dist/repo.js.map +1 -0
  30. package/dist/scaffold.d.ts +68 -0
  31. package/dist/scaffold.js +296 -0
  32. package/dist/scaffold.js.map +1 -0
  33. package/dist/vitest.d.ts +12 -0
  34. package/dist/vitest.js +17 -0
  35. package/dist/vitest.js.map +1 -0
  36. package/package.json +56 -0
  37. package/templates/plugin/blank/plugin.yaml +22 -0
  38. package/templates/plugin/blank/src/plugin.ts +74 -0
  39. package/templates/plugin/blank/test/plugin.test.ts +33 -0
  40. package/templates/plugin/common/README.md +23 -0
  41. package/templates/plugin/common/RUNBOOK.md +10 -0
  42. package/templates/plugin/common/src/index.ts +4 -0
  43. package/templates/plugin/common/tsconfig.build.json +9 -0
  44. package/templates/plugin/common/tsconfig.json +7 -0
  45. package/templates/plugin/common/vitest.config.ts +1 -0
  46. package/templates/plugin/openapi-rest/plugin.yaml +49 -0
  47. package/templates/plugin/openapi-rest/src/plugin.ts +89 -0
  48. package/templates/plugin/openapi-rest/test/e2e.test.ts +49 -0
  49. package/templates/plugin/openapi-rest/test/fake-upstream.ts +63 -0
  50. package/templates/plugin/openapi-rest/test/plugin.test.ts +66 -0
  51. package/templates/plugin/static-rest/plugin.yaml +61 -0
  52. package/templates/plugin/static-rest/src/plugin.ts +70 -0
  53. package/templates/plugin/static-rest/test/e2e.test.ts +46 -0
  54. package/templates/plugin/static-rest/test/fake-upstream.ts +39 -0
  55. package/templates/plugin/static-rest/test/plugin.test.ts +67 -0
  56. package/templates/plugin/websocket-rpc/plugin.yaml +45 -0
  57. package/templates/plugin/websocket-rpc/src/client.ts +96 -0
  58. package/templates/plugin/websocket-rpc/src/plugin.ts +98 -0
  59. package/templates/plugin/websocket-rpc/test/e2e.test.ts +46 -0
  60. package/templates/plugin/websocket-rpc/test/fake-upstream.ts +63 -0
  61. package/templates/plugin/websocket-rpc/test/plugin.test.ts +67 -0
  62. package/templates/repo/base/README.md +27 -0
  63. package/templates/repo/base/_github/workflows/ci.yml +27 -0
  64. package/templates/repo/base/_gitignore +6 -0
  65. package/templates/repo/base/_nvmrc +1 -0
  66. package/templates/repo/base/_prettierignore +3 -0
  67. package/templates/repo/base/_prettierrc +5 -0
  68. package/templates/repo/base/eslint.config.js +17 -0
  69. package/templates/repo/base/package.json +34 -0
  70. package/templates/repo/base/plugin-files.d.ts +10 -0
  71. package/templates/repo/base/pnpm-workspace.yaml +11 -0
  72. package/templates/repo/base/tsconfig.base.json +21 -0
  73. package/templates/repo/base/vitest.shared.ts +9 -0
  74. package/templates/repo/claude/CLAUDE.md +38 -0
  75. package/templates/repo/claude/_claude/agents/security-reviewer.md +33 -0
  76. package/templates/repo/claude/_claude/hooks/format.mjs +20 -0
  77. package/templates/repo/claude/_claude/hooks/guard-paths.mjs +43 -0
  78. package/templates/repo/claude/_claude/hooks/session-start.sh +13 -0
  79. package/templates/repo/claude/_claude/settings.json +21 -0
  80. package/templates/repo/claude/_claude/skills/add-plugin/SKILL.md +39 -0
  81. package/templates/repo/claude/_claude/skills/release-plugin/SKILL.md +18 -0
  82. package/templates/repo/release/RELEASING.md +27 -0
  83. 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