create-rigline-plugin 1.0.0-alpha.1 → 1.0.0-alpha.11
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/README.md +12 -0
- package/dist/index.js +8 -2
- package/package.json +6 -3
- package/template/.github/workflows/ci.yml +62 -0
- package/template/.github/workflows/release.yml +130 -0
- package/template/README.md +65 -6
- package/template/gitignore +1 -0
- package/template/package.json +5 -3
- package/template/plugins/__NAME__/README.md +8 -7
- package/template/plugins/__NAME__/package.json +7 -2
- package/template/plugins/__NAME__/rigline.json +7 -3
- package/template/plugins/__NAME__/src/index.test.ts +2 -2
- package/template/plugins/__NAME__/src/index.tsx +64 -0
- package/template/plugins/__NAME__/tsconfig.json +1 -1
- package/template/pnpm-workspace.yaml +15 -5
- package/template/tsconfig.plugin.json +2 -1
- package/template/plugins/__NAME__/src/index.ts +0 -64
package/README.md
CHANGED
|
@@ -5,6 +5,10 @@ Claude Code VS Code extension.
|
|
|
5
5
|
|
|
6
6
|
npm create rigline-plugin my-plugins
|
|
7
7
|
|
|
8
|
+
A plugin runs inside a modification of Anthropic's extension, so the
|
|
9
|
+
[plugin policy](https://github.com/Rigline/Rigline/blob/main/docs/plugin-policy.md) applies to it —
|
|
10
|
+
whether or not you ever publish it. One page, and the scaffolded README points at it too.
|
|
11
|
+
|
|
8
12
|
## What you get
|
|
9
13
|
|
|
10
14
|
A pnpm workspace with `plugins/*` and one plugin in it, rather than a single-plugin repository. The
|
|
@@ -29,6 +33,11 @@ Then:
|
|
|
29
33
|
|
|
30
34
|
and *Developer: Reload Webviews*.
|
|
31
35
|
|
|
36
|
+
The workspace declares `@rigline/core` — the engine, which carries the `rigline-engine` command its
|
|
37
|
+
`build` and `codegen` scripts run — and never `rigline`, which is the layer a *user* installs to
|
|
38
|
+
fetch that engine. `pnpm rigline <verb>` here is a script forwarding to the engine in your own
|
|
39
|
+
`node_modules`, so the loop above needs nothing installed globally.
|
|
40
|
+
|
|
32
41
|
`generated.ts` ships as a placeholder so a fresh scaffold typechecks before `codegen` has ever run.
|
|
33
42
|
Once you run `codegen` it holds the identifiers *your* extension version actually has; commit it,
|
|
34
43
|
and the diff when you run against a newer extension is how you find out what moved.
|
|
@@ -42,4 +51,7 @@ is the one ordering dependency in the whole arrangement.
|
|
|
42
51
|
|
|
43
52
|
[Authoring guide](https://github.com/Rigline/Rigline/blob/main/docs/authoring.md)
|
|
44
53
|
|
|
54
|
+
[Changelog](https://github.com/Rigline/Rigline/blob/main/CHANGELOG.md) — every package in this
|
|
55
|
+
workspace shares it, and one version number.
|
|
56
|
+
|
|
45
57
|
MIT.
|
package/dist/index.js
CHANGED
|
@@ -30,10 +30,12 @@ export function scaffold(options) {
|
|
|
30
30
|
if (!existsSync(templateDir)) {
|
|
31
31
|
throw new ScaffoldError(`the template is missing from this package: ${templateDir}`);
|
|
32
32
|
}
|
|
33
|
+
const range = options.riglineRange ?? riglineRange();
|
|
33
34
|
const substitutions = {
|
|
34
35
|
NAME: name,
|
|
35
36
|
DESCRIPTION: options.description ?? `A Rigline plugin called ${name}.`,
|
|
36
|
-
RIGLINE_RANGE:
|
|
37
|
+
RIGLINE_RANGE: range,
|
|
38
|
+
RIGLINE_VERSION: range.replace(/^\^/, ""),
|
|
37
39
|
};
|
|
38
40
|
const files = [];
|
|
39
41
|
for (const source of walk(templateDir)) {
|
|
@@ -80,7 +82,11 @@ export function nextSteps(result) {
|
|
|
80
82
|
"",
|
|
81
83
|
"Then reload the webview: Developer: Reload Webviews.",
|
|
82
84
|
"",
|
|
83
|
-
`${join(here, "README.md")} has the rest, including the
|
|
85
|
+
`${join(here, "README.md")} has the rest, including the rules worth reading first.`,
|
|
86
|
+
"",
|
|
87
|
+
"Read the plugin policy once — what Rigline promises Anthropic, and the part your plugin has",
|
|
88
|
+
"to hold up. It applies whether or not you ever publish this:",
|
|
89
|
+
" https://github.com/Rigline/Rigline/blob/main/docs/plugin-policy.md",
|
|
84
90
|
].join("\n");
|
|
85
91
|
}
|
|
86
92
|
export function main(argv) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-rigline-plugin",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.11",
|
|
4
4
|
"description": "Scaffold a workspace for Claude Code VS Code extension plugins",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rigline",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"author": "Lionell Pack",
|
|
21
21
|
"type": "module",
|
|
22
22
|
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.12.0"
|
|
25
|
+
},
|
|
23
26
|
"files": [
|
|
24
27
|
"dist",
|
|
25
28
|
"!dist/**/*.map",
|
|
@@ -29,10 +32,10 @@
|
|
|
29
32
|
"create-rigline-plugin": "./dist/index.js"
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|
|
32
|
-
"@rigline/plugin-api": "1.0.0-alpha.
|
|
35
|
+
"@rigline/plugin-api": "1.0.0-alpha.11"
|
|
33
36
|
},
|
|
34
37
|
"scripts": {
|
|
35
|
-
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"build": "node ../../scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
|
|
36
39
|
"typecheck": "tsc -p tsconfig.json"
|
|
37
40
|
}
|
|
38
41
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Your plugins, checked on every push and every pull request. The release workflow runs the same
|
|
2
|
+
# steps again before it stages anything, so this is the copy that tells you early — and the copy a
|
|
3
|
+
# contributor's pull request gets, which is the whole reason it is separate.
|
|
4
|
+
#
|
|
5
|
+
# There is no lint step because this workspace ships no linter. Add one here if you add one.
|
|
6
|
+
name: CI
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
# GitHub takes no expression here, so your default branch cannot be filled in for you. These
|
|
11
|
+
# are the two common names; edit if yours is neither.
|
|
12
|
+
branches: [main, master]
|
|
13
|
+
pull_request:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
# A push supersedes the run before it on the same ref, and a pull request supersedes its own last
|
|
19
|
+
# push. Two runs of one ref prove the same thing, and the newer one is what is being asked about.
|
|
20
|
+
concurrency:
|
|
21
|
+
group: ci-${{ github.ref }}
|
|
22
|
+
cancel-in-progress: true
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
check:
|
|
26
|
+
name: Node ${{ matrix.node }}
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
strategy:
|
|
29
|
+
# Every rung reports. A failure on the floor and a failure on the newest runtime are
|
|
30
|
+
# different bugs, and cancelling the rest hides which one this is.
|
|
31
|
+
fail-fast: false
|
|
32
|
+
matrix:
|
|
33
|
+
# The first is the floor this workspace's package.json declares in `engines`, pinned
|
|
34
|
+
# exactly so that the number claimed is a number something ran; the others float to the
|
|
35
|
+
# latest of their major, which says the major is supported rather than that a patch was
|
|
36
|
+
# tested. Cutting this to one rung is a reasonable thing to do — then drop the `engines`
|
|
37
|
+
# claim to match, because a floor nothing runs on is a guess.
|
|
38
|
+
node: ["22.12.0", "24", "26"]
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v5
|
|
41
|
+
|
|
42
|
+
# Pinned exactly rather than to the major: this action's `v6` tag still resolves to the last
|
|
43
|
+
# release before pnpm v12 support, and `v4` runs on a runner runtime GitHub has deprecated.
|
|
44
|
+
- uses: pnpm/action-setup@v6.1.0
|
|
45
|
+
|
|
46
|
+
# After pnpm, never before: `cache: pnpm` asks pnpm where its store is.
|
|
47
|
+
- uses: actions/setup-node@v5
|
|
48
|
+
with:
|
|
49
|
+
node-version: ${{ matrix.node }}
|
|
50
|
+
cache: pnpm
|
|
51
|
+
|
|
52
|
+
# Needs `pnpm-lock.yaml` committed, which is why nothing ignores it. Installing what the
|
|
53
|
+
# lockfile says is the point: a run that resolved its own dependencies would be testing
|
|
54
|
+
# something nobody has.
|
|
55
|
+
- run: pnpm install --frozen-lockfile
|
|
56
|
+
|
|
57
|
+
- run: pnpm typecheck
|
|
58
|
+
|
|
59
|
+
# Before the tests, because a test that imports a built entry should import this build.
|
|
60
|
+
- run: pnpm build
|
|
61
|
+
|
|
62
|
+
- run: pnpm test
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Publish your plugins to npm, without an npm token ever being stored here.
|
|
2
|
+
#
|
|
3
|
+
# GitHub authenticates to npm over OIDC, so the secret this workflow would otherwise need does not
|
|
4
|
+
# exist. What it does is *stage*: a staged version sits in a queue that nobody can install until you
|
|
5
|
+
# approve it from your own machine with 2FA. Approving says you meant to ship it.
|
|
6
|
+
#
|
|
7
|
+
# pnpm stage approve
|
|
8
|
+
#
|
|
9
|
+
# Before this works, once per package, on npmjs.com:
|
|
10
|
+
#
|
|
11
|
+
# 1. Publish the first version by hand. A package that does not exist yet has nothing for a
|
|
12
|
+
# trusted publisher to attach to: `pnpm publish -r --otp <code>` (a one-time password, not a
|
|
13
|
+
# token — there is then nothing to store or revoke).
|
|
14
|
+
# 2. Add a trusted publisher under the package's settings, naming this repository and this file
|
|
15
|
+
# by path. Renaming this file breaks the exchange until the entry is edited to match.
|
|
16
|
+
# Set its permission to stage-only.
|
|
17
|
+
#
|
|
18
|
+
# `-r` stages every package in the workspace whose version is not already on the registry, in
|
|
19
|
+
# dependency order, so bumping one plugin releases one plugin.
|
|
20
|
+
name: Release
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
workflow_dispatch:
|
|
24
|
+
inputs:
|
|
25
|
+
dist_tag:
|
|
26
|
+
description: The tag these versions go to. A prerelease belongs on `next`, not `latest`.
|
|
27
|
+
type: choice
|
|
28
|
+
options: [latest, next]
|
|
29
|
+
default: latest
|
|
30
|
+
dry_run:
|
|
31
|
+
description: Build, test and pack, but stage nothing
|
|
32
|
+
type: boolean
|
|
33
|
+
default: false
|
|
34
|
+
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
|
|
38
|
+
concurrency:
|
|
39
|
+
group: release
|
|
40
|
+
cancel-in-progress: false
|
|
41
|
+
|
|
42
|
+
jobs:
|
|
43
|
+
stage:
|
|
44
|
+
name: Stage to npm
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
permissions:
|
|
47
|
+
contents: read
|
|
48
|
+
# What lets the runner ask GitHub for the token npm exchanges for a publishing one. Without
|
|
49
|
+
# it there is nothing to send, and pnpm falls back to looking for a credential that is not
|
|
50
|
+
# here — reporting that as a warning, not an error, so read the log rather than the tick.
|
|
51
|
+
id-token: write
|
|
52
|
+
steps:
|
|
53
|
+
- name: Refuse anything but the default branch
|
|
54
|
+
if: github.ref_name != github.event.repository.default_branch
|
|
55
|
+
run: |
|
|
56
|
+
echo "::error::Releases go from ${{ github.event.repository.default_branch }}."
|
|
57
|
+
exit 1
|
|
58
|
+
|
|
59
|
+
- uses: actions/checkout@v5
|
|
60
|
+
|
|
61
|
+
# npm binds a provenance attestation to the package's `repository` field, so a package
|
|
62
|
+
# without one cannot be staged with `--provenance` at all. Asked here, before the build, so
|
|
63
|
+
# that the answer is a package name rather than an error about attestations two minutes in.
|
|
64
|
+
# A dry run is checked too: a rehearsal that skips this rehearses a different release.
|
|
65
|
+
- name: Refuse a publishable package with no repository
|
|
66
|
+
run: |
|
|
67
|
+
node -e '
|
|
68
|
+
const fs = require("fs");
|
|
69
|
+
const dirs = fs.existsSync("plugins") ? fs.readdirSync("plugins") : [];
|
|
70
|
+
const missing = [];
|
|
71
|
+
for (const dir of dirs) {
|
|
72
|
+
const path = `plugins/${dir}/package.json`;
|
|
73
|
+
if (!fs.existsSync(path)) continue;
|
|
74
|
+
const manifest = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
75
|
+
if (!manifest.private && !manifest.repository) missing.push(manifest.name || dir);
|
|
76
|
+
}
|
|
77
|
+
if (missing.length === 0) process.exit(0);
|
|
78
|
+
console.log(`::error::No "repository" field in ${missing.join(", ")}. npm binds a provenance attestation to it, so --provenance cannot stage a package without one.`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
'
|
|
81
|
+
|
|
82
|
+
# Pinned exactly rather than to the major: this action's `v6` tag still resolves to the last
|
|
83
|
+
# release before pnpm v12 support, and `v4` runs on a runner runtime GitHub has deprecated.
|
|
84
|
+
- uses: pnpm/action-setup@v6.1.0
|
|
85
|
+
- uses: actions/setup-node@v5
|
|
86
|
+
with:
|
|
87
|
+
node-version: 26
|
|
88
|
+
cache: pnpm
|
|
89
|
+
|
|
90
|
+
- run: pnpm install --frozen-lockfile
|
|
91
|
+
- run: pnpm typecheck
|
|
92
|
+
- run: pnpm build
|
|
93
|
+
- run: pnpm test
|
|
94
|
+
|
|
95
|
+
# `generated.ts` is committed, so this needs no installed extension. `rigline codegen` is
|
|
96
|
+
# something you run locally against your own editor and commit the result of.
|
|
97
|
+
- name: Stage
|
|
98
|
+
if: ${{ !inputs.dry_run }}
|
|
99
|
+
run: pnpm stage publish -r --tag ${{ inputs.dist_tag }} --provenance --report-summary --no-git-checks
|
|
100
|
+
|
|
101
|
+
- name: Pack only
|
|
102
|
+
if: ${{ inputs.dry_run }}
|
|
103
|
+
run: pnpm stage publish -r --tag ${{ inputs.dist_tag }} --dry-run --report-summary --no-git-checks
|
|
104
|
+
|
|
105
|
+
# Nothing notifies you that a stage is waiting, and pnpm's publish output carries no stage id
|
|
106
|
+
# to quote (the registry has one; `npm stage list` shows it). So the run summary names what
|
|
107
|
+
# went up and the one command that finishes the job.
|
|
108
|
+
- name: Summarise
|
|
109
|
+
if: always()
|
|
110
|
+
run: |
|
|
111
|
+
if [ ! -f pnpm-publish-summary.json ]; then
|
|
112
|
+
echo "## Nothing was staged" >> "$GITHUB_STEP_SUMMARY"
|
|
113
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
114
|
+
echo "Either the run stopped before staging, or every version is already on the registry." >> "$GITHUB_STEP_SUMMARY"
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
node -e '
|
|
118
|
+
const s = JSON.parse(require("fs").readFileSync("pnpm-publish-summary.json", "utf8"));
|
|
119
|
+
const out = [];
|
|
120
|
+
const staged = s.publishedPackages ?? [];
|
|
121
|
+
if (staged.length === 0) {
|
|
122
|
+
out.push("## Nothing was staged", "", "Every version is already on the registry. Bump one to release it.");
|
|
123
|
+
} else {
|
|
124
|
+
out.push("## Staged, and awaiting approval", "", "| package | version |", "| --- | --- |");
|
|
125
|
+
for (const p of staged) out.push(`| \`${p.name}\` | \`${p.version}\` |`);
|
|
126
|
+
out.push("", "**Nobody can install these yet.** On a machine with your npm 2FA:", "", "```", "pnpm stage approve", "```", "",
|
|
127
|
+
"It lists what is queued and takes the batch under one one-time password. A version you would rather not ship needs no action — do not approve it, and it expires.");
|
|
128
|
+
}
|
|
129
|
+
require("fs").appendFileSync(process.env.GITHUB_STEP_SUMMARY, out.join("\n") + "\n");
|
|
130
|
+
'
|
package/template/README.md
CHANGED
|
@@ -37,20 +37,77 @@ which is the whole reason to declare rather than to reach.
|
|
|
37
37
|
Put a dependency you can do without under `uses.optional`: it is checked the same way and costs the
|
|
38
38
|
plugin that one decoration rather than the whole plugin.
|
|
39
39
|
|
|
40
|
+
`elements` is the other half: what the plugin contributes, each with the places it may go and where
|
|
41
|
+
it goes by default — or `null` for off. A place this extension version cannot provide costs that
|
|
42
|
+
element and nothing else, and `check` says so.
|
|
43
|
+
|
|
44
|
+
## The plugin policy
|
|
45
|
+
|
|
46
|
+
**This applies whether or not you ever publish.** A plugin you wrote for yourself and will never
|
|
47
|
+
share still runs inside a modification of Anthropic's extension, and Anthropic's terms still apply
|
|
48
|
+
to what it does on your machine. Read it once, at
|
|
49
|
+
[docs/plugin-policy.md](https://github.com/Rigline/Rigline/blob/main/docs/plugin-policy.md); it
|
|
50
|
+
takes a minute.
|
|
51
|
+
|
|
52
|
+
Rigline modifies Anthropic's extension and publishes a compliance position saying what it does and
|
|
53
|
+
does not do. Your plugin runs inside that modification, so the position has to hold for it too.
|
|
54
|
+
|
|
55
|
+
Most of it is not left to you — the webview has no network egress, no filesystem, and no way to run
|
|
56
|
+
code in the extension host, so the usual ways to do harm are absent rather than forbidden. What the policy
|
|
57
|
+
asks is the part the architecture cannot cover: do not deceive the person using it, do not reach for
|
|
58
|
+
credentials, do not carry conversation content off the machine by a path the closed network does not
|
|
59
|
+
cover, and keep any host patch to switching on a capability the extension already has.
|
|
60
|
+
|
|
40
61
|
## Testing
|
|
41
62
|
|
|
42
63
|
`pnpm test` runs the pure half — the functions that do not touch the DOM. Whether a decoration
|
|
43
64
|
lands in the right place, survives a re-render, or costs a row a line of height is a question only
|
|
44
65
|
the app can answer: build, add, reload, look.
|
|
45
66
|
|
|
67
|
+
`.github/workflows/ci.yml` runs typecheck, build and test on every push and every pull request,
|
|
68
|
+
over three Node versions — the same set the release workflow runs, so nothing reaches a release
|
|
69
|
+
that a pull request would not already have failed on. Commit `pnpm-lock.yaml`: CI installs what it
|
|
70
|
+
says rather than resolving its own.
|
|
71
|
+
|
|
46
72
|
## Publishing
|
|
47
73
|
|
|
48
74
|
A plugin is published as an ordinary npm package carrying `rigline.json` and its built entry, and
|
|
49
|
-
installed with `rigline add <name>`. Nothing about publishing is special: `rigline build`
|
|
50
|
-
everything the entry imports, so a published plugin has no runtime dependency to install,
|
|
51
|
-
`@rigline/plugin-api`
|
|
52
|
-
|
|
53
|
-
|
|
75
|
+
installed with `rigline add <name>`. Nothing about publishing is special: `rigline-engine build`
|
|
76
|
+
bundles everything the entry imports, so a published plugin has no runtime dependency to install,
|
|
77
|
+
and both `@rigline/core` and `@rigline/plugin-api` stay *devDependencies*.
|
|
78
|
+
|
|
79
|
+
`.github/workflows/release.yml` does it from CI, with no npm token stored anywhere: GitHub
|
|
80
|
+
authenticates to npm over OIDC, and what the workflow does is *stage* — a version nobody can
|
|
81
|
+
install until you approve it from your own machine with 2FA.
|
|
82
|
+
|
|
83
|
+
pnpm stage approve
|
|
84
|
+
|
|
85
|
+
**One field to fill in before the first publish: `repository`.** npm binds a provenance attestation
|
|
86
|
+
to it, and this workflow stages with provenance, so a package without one cannot be staged at all.
|
|
87
|
+
Nothing can scaffold it for you — a guessed URL would be a wrong one in the registry rather than a
|
|
88
|
+
missing one — so the workflow refuses by name, before it builds anything, until it is there:
|
|
89
|
+
|
|
90
|
+
"repository": {
|
|
91
|
+
"type": "git",
|
|
92
|
+
"url": "git+https://github.com/you/your-repo.git",
|
|
93
|
+
"directory": "plugins/__NAME__"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
`homepage` and `bugs` are worth the same minute; npm shows them on the package page. A `LICENSE`
|
|
97
|
+
file is not scaffolded either, because the copyright line is yours to write — the manifest says
|
|
98
|
+
MIT, and npm ships a licence file whatever `files` says, so adding one is the whole job. The
|
|
99
|
+
`rigline-plugin` keyword is already there: it is how somebody finds a plugin on npm.
|
|
100
|
+
|
|
101
|
+
Two things to set up once per package, the first time. Publish version one by hand, because a
|
|
102
|
+
package that does not exist yet has nothing for a trusted publisher to attach to — `pnpm publish -r
|
|
103
|
+
--otp <code>`, supplying a one-time password rather than creating a token, so there is nothing to
|
|
104
|
+
store or to revoke afterwards. Then add a trusted publisher in the package's settings on npmjs.com,
|
|
105
|
+
naming this repository and `release.yml` by path, with its permission set to stage-only. The
|
|
106
|
+
workflow's own header repeats both, where you will be when you need them.
|
|
107
|
+
|
|
108
|
+
Adding a second plugin later means one more of each: npm's exchange is per package, so a trusted
|
|
109
|
+
publisher is too. That is friction on a second plugin, never on a second release — `-r` stages only
|
|
110
|
+
what you bumped.
|
|
54
111
|
|
|
55
112
|
## Rules that will cost you if you break them
|
|
56
113
|
|
|
@@ -62,6 +119,8 @@ everything the entry imports, so a published plugin has no runtime dependency to
|
|
|
62
119
|
you placed.
|
|
63
120
|
- **Ask what a container does about its children before decorating it.** The composer footer
|
|
64
121
|
measures its own element children and re-measures on any foreign change inside it; footer
|
|
65
|
-
|
|
122
|
+
elements go before `footerSpacer`.
|
|
123
|
+
- **Keep what you place steady.** An element in the footer whose text keeps changing makes the
|
|
124
|
+
footer re-measure each time, and one in `rigRow` whose height keeps changing re-renders the panel.
|
|
66
125
|
- **Do not poll for an element.** `ctx.watch(name, …)` hands it over when it appears and again when
|
|
67
126
|
the app replaces it.
|
package/template/gitignore
CHANGED
package/template/package.json
CHANGED
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@12.3.4",
|
|
6
|
-
"engines": { "node": ">=
|
|
6
|
+
"engines": { "node": ">=22.12.0" },
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
8
|
+
"rigline": "rigline-engine",
|
|
9
|
+
"codegen": "rigline-engine codegen",
|
|
9
10
|
"build": "pnpm -r build",
|
|
10
11
|
"typecheck": "pnpm -r typecheck",
|
|
11
12
|
"test": "vitest run"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
15
|
+
"@rigline/core": "__RIGLINE_RANGE__",
|
|
14
16
|
"@rigline/plugin-api": "__RIGLINE_RANGE__",
|
|
15
|
-
"
|
|
17
|
+
"rolldown": "^1.2.8",
|
|
16
18
|
"typescript": "^7.0.2",
|
|
17
19
|
"vitest": "^5.0.0"
|
|
18
20
|
}
|
|
@@ -4,15 +4,16 @@ __DESCRIPTION__
|
|
|
4
4
|
|
|
5
5
|
## Depends on
|
|
6
6
|
|
|
7
|
-
- `uses.anchors: ["footerSpacer"]` — the flexible gap dividing the composer footer's left cluster
|
|
8
|
-
from its right, and the anchor every footer decoration uses. The footer measures the widths of its
|
|
9
|
-
own element children to pick a fit stage; the spacer renders in every stage, so a decoration
|
|
10
|
-
beside it contributes a constant width and the measurement settles.
|
|
11
|
-
- `uses.mount` — `ctx.watch` and `ctx.mountBefore`, which place the badge and keep it placed across
|
|
12
|
-
a re-render.
|
|
13
|
-
- `uses.style` — one stylesheet, scoped to the class this plugin puts on its own element.
|
|
14
7
|
- `uses.tools` — `ctx.onToolUse`, every completed tool call the assistant makes.
|
|
15
8
|
|
|
9
|
+
## Contributes
|
|
10
|
+
|
|
11
|
+
- `elements.badge` — the tool-call count, before `footerSpacer` by default: the flexible gap
|
|
12
|
+
dividing the composer footer's left cluster from its right, and the place footer elements go. The
|
|
13
|
+
footer measures the widths of its own element children to pick a fit stage; the spacer renders in
|
|
14
|
+
every stage, so an element beside it contributes a constant width and the measurement settles. It
|
|
15
|
+
may also go in `rigRow`, the row under the composer's controls.
|
|
16
|
+
|
|
16
17
|
## Notes
|
|
17
18
|
|
|
18
19
|
`badgeText` is exported and tested because it is the half a plain test run can hold. Everything
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
"name": "rigline-plugin-__NAME__",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"description": "__DESCRIPTION__",
|
|
5
|
+
"keywords": ["rigline-plugin", "rigline", "claude-code", "vscode"],
|
|
5
6
|
"type": "module",
|
|
6
7
|
"license": "MIT",
|
|
8
|
+
"publishConfig": { "access": "public" },
|
|
7
9
|
"files": ["dist", "rigline.json"],
|
|
8
10
|
"scripts": {
|
|
9
|
-
"build": "rigline build",
|
|
11
|
+
"build": "rigline-engine build",
|
|
10
12
|
"typecheck": "tsc -p tsconfig.json"
|
|
11
13
|
},
|
|
12
14
|
"devDependencies": {
|
|
15
|
+
"@rigline/core": "__RIGLINE_RANGE__",
|
|
13
16
|
"@rigline/plugin-api": "__RIGLINE_RANGE__",
|
|
14
|
-
"
|
|
17
|
+
"@types/react": "^19.3.0",
|
|
18
|
+
"react": "^19.3.0",
|
|
19
|
+
"react-dom": "^19.3.0"
|
|
15
20
|
}
|
|
16
21
|
}
|
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
"entry": "dist/index.js",
|
|
7
7
|
"surfaces": ["editor", "sidebar"],
|
|
8
8
|
"uses": {
|
|
9
|
-
"anchors": ["footerSpacer"],
|
|
10
|
-
"mount": true,
|
|
11
|
-
"style": true,
|
|
12
9
|
"tools": true
|
|
10
|
+
},
|
|
11
|
+
"elements": {
|
|
12
|
+
"badge": {
|
|
13
|
+
"title": "Tool calls",
|
|
14
|
+
"placements": [{ "anchor": "footerSpacer", "at": "before" }, "rigRow"],
|
|
15
|
+
"default": { "anchor": "footerSpacer", "at": "before" }
|
|
16
|
+
}
|
|
13
17
|
}
|
|
14
18
|
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Verification splits three ways, and knowing which tier a question belongs to is most of writing
|
|
5
5
|
* a test that is worth having. A pure function is this tier. Whether a decoration lands in the
|
|
6
6
|
* right place, survives a re-render, or costs the row a line of height is a question about the app,
|
|
7
|
-
* and only the app can answer it: build
|
|
7
|
+
* and only the app can answer it: `pnpm build`, `pnpm rigline add`, reload the webview, look.
|
|
8
8
|
*/
|
|
9
9
|
import { describe, expect, it } from "vitest";
|
|
10
|
-
import { badgeText } from "./index.
|
|
10
|
+
import { badgeText } from "./index.tsx";
|
|
11
11
|
|
|
12
12
|
describe("badgeText", () => {
|
|
13
13
|
it("says something before anything has happened", () => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* __DESCRIPTION__
|
|
3
|
+
*
|
|
4
|
+
* A worked example of the loop every plugin is: declare what you contribute and depend on in
|
|
5
|
+
* `rigline.json`, keep what you learn in a store, and render it from a component. Replace the body;
|
|
6
|
+
* keep the shape.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
definePlugin,
|
|
10
|
+
type PluginContext,
|
|
11
|
+
type Store,
|
|
12
|
+
store,
|
|
13
|
+
type Teardown,
|
|
14
|
+
} from "@rigline/plugin-api";
|
|
15
|
+
import { Pill, useStore } from "@rigline/plugin-api/ui";
|
|
16
|
+
import type { ReactNode } from "react";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What the badge reads, for a given number of tool calls.
|
|
20
|
+
*
|
|
21
|
+
* Pure, and exported, because this is the half a plain `vitest` run can hold: anything that renders
|
|
22
|
+
* wants the app itself, and anything that does not should not need it. See `src/index.test.ts`.
|
|
23
|
+
*/
|
|
24
|
+
export function badgeText(calls: number): string {
|
|
25
|
+
if (calls === 0) return "no tools yet";
|
|
26
|
+
return `${calls} tool ${calls === 1 ? "call" : "calls"}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Badge(props: { readonly calls: Store<number> }): ReactNode {
|
|
30
|
+
return <Pill>{badgeText(useStore(props.calls))}</Pill>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default definePlugin({
|
|
34
|
+
setup(ctx: PluginContext): Teardown {
|
|
35
|
+
// A store, made here rather than in the component, because state caught in `setup` is caught
|
|
36
|
+
// from the moment the plugin loads, and the badge and anything else can share it.
|
|
37
|
+
const calls = store(0);
|
|
38
|
+
|
|
39
|
+
// Every completed tool call the assistant makes. `uses.tools` is what makes this fire; without
|
|
40
|
+
// the declaration it throws and disables the plugin, which is the point of declaring.
|
|
41
|
+
const stopTools = ctx.onToolUse(() => calls.set(calls.get() + 1));
|
|
42
|
+
|
|
43
|
+
// `badge` is declared under `elements` in rigline.json, which says where it goes by default:
|
|
44
|
+
// before `footerSpacer`, at the end of the composer footer's left cluster. Rigline places it,
|
|
45
|
+
// keeps it placed, and moves it if the user asks for it somewhere else.
|
|
46
|
+
const stopBadge = ctx.element("badge", () => <Badge calls={calls} />);
|
|
47
|
+
|
|
48
|
+
// One line in Rigline's diagnostics panel, under this plugin's name. Whether the badge is on
|
|
49
|
+
// screen is Rigline's to say, and it does; ask what only your own state can answer, and say
|
|
50
|
+
// `n/a` with a reason when there is nothing to report yet. The host runs this about once a
|
|
51
|
+
// second, so read state you already keep rather than computing anything here.
|
|
52
|
+
const stopCheck = ctx.check("tool calls observed", () =>
|
|
53
|
+
calls.get() === 0
|
|
54
|
+
? { verdict: "n/a", detail: "none yet" }
|
|
55
|
+
: { verdict: "pass", detail: badgeText(calls.get()) },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
stopCheck();
|
|
60
|
+
stopBadge();
|
|
61
|
+
stopTools();
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -3,14 +3,24 @@ packages:
|
|
|
3
3
|
|
|
4
4
|
# Supply-chain settings, written down rather than inherited.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Two of these restate a pnpm default. They are stated anyway, because a default is not a position:
|
|
7
|
+
# a fresh clone on a different pnpm, or a default that moves, would quietly change what this
|
|
8
|
+
# repository is willing to install without anyone deciding to.
|
|
9
9
|
#
|
|
10
|
-
# minimumReleaseAge is the
|
|
11
|
-
#
|
|
10
|
+
# minimumReleaseAge is the third and is not a restatement. pnpm applies the same cutoff by default
|
|
11
|
+
# but merely records anything younger; writing it down is what makes it refuse. That is the rule
|
|
12
|
+
# rigline applies to plugins, applied here to your own dependencies: a day is where a compromised
|
|
13
|
+
# publish is usually caught.
|
|
12
14
|
minimumReleaseAge: 1440
|
|
13
15
|
|
|
16
|
+
# The exception, and the reason for it: pnpm resolves to the newest version in range that is old
|
|
17
|
+
# enough, and these two ranges have no older version in them to fall back on. Scaffolding you ran
|
|
18
|
+
# minutes ago is this same release, so waiting a day for the rest of it protects nothing. Drop these
|
|
19
|
+
# once the versions are a day old; a later Rigline release is gated like anything else.
|
|
20
|
+
minimumReleaseAgeExclude:
|
|
21
|
+
- "@rigline/core@__RIGLINE_VERSION__"
|
|
22
|
+
- "@rigline/plugin-api@__RIGLINE_VERSION__"
|
|
23
|
+
|
|
14
24
|
# Only a direct dependency may come from a git repository or a tarball URL. A transitive dependency
|
|
15
25
|
# that resolves outside the registry is the shape of a supply-chain problem rather than of an
|
|
16
26
|
# ordinary package.
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* __DESCRIPTION__
|
|
3
|
-
*
|
|
4
|
-
* A worked example of the loop every plugin is: declare what you depend on in `rigline.json`, wait
|
|
5
|
-
* to be handed the element you decorate, and put something beside it. Replace the body; keep the
|
|
6
|
-
* shape.
|
|
7
|
-
*/
|
|
8
|
-
import { definePlugin, type PluginContext, type Teardown } from "@rigline/plugin-api";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* What the badge reads, for a given number of tool calls.
|
|
12
|
-
*
|
|
13
|
-
* Pure, and exported, because this is the half a plain `vitest` run can hold: anything that touches
|
|
14
|
-
* the DOM wants the app itself, and anything that does not should not need it. See
|
|
15
|
-
* `src/index.test.ts`.
|
|
16
|
-
*/
|
|
17
|
-
export function badgeText(calls: number): string {
|
|
18
|
-
if (calls === 0) return "no tools yet";
|
|
19
|
-
return `${calls} tool ${calls === 1 ? "call" : "calls"}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export default definePlugin({
|
|
23
|
-
setup(ctx: PluginContext): Teardown {
|
|
24
|
-
let calls = 0;
|
|
25
|
-
const badge = document.createElement("span");
|
|
26
|
-
badge.className = "example-badge";
|
|
27
|
-
badge.textContent = badgeText(calls);
|
|
28
|
-
|
|
29
|
-
// Scoped to a class this plugin put on its own element. Never scope a rule to an anchor's bare
|
|
30
|
-
// class: one class is applied wherever that look is wanted, so a rule written against it lands
|
|
31
|
-
// on every control wearing it. See the anchors guide.
|
|
32
|
-
const stopStyle = ctx.style(`
|
|
33
|
-
.example-badge {
|
|
34
|
-
font-size: 11px;
|
|
35
|
-
opacity: 0.7;
|
|
36
|
-
padding: 0 6px;
|
|
37
|
-
white-space: nowrap;
|
|
38
|
-
}
|
|
39
|
-
`);
|
|
40
|
-
|
|
41
|
-
// Every completed tool call the assistant makes. `uses.tools` is what makes this fire; without
|
|
42
|
-
// the declaration it throws and disables the plugin, which is the point of declaring.
|
|
43
|
-
const stopTools = ctx.onToolUse(() => {
|
|
44
|
-
calls += 1;
|
|
45
|
-
badge.textContent = badgeText(calls);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// `watch` hands over the element for an anchor whenever one is in the document, and again if
|
|
49
|
-
// the app replaces it. No plugin polls for an element.
|
|
50
|
-
//
|
|
51
|
-
// `footerSpacer` and `mountBefore` together, rather than any other footer anchor: the composer
|
|
52
|
-
// footer measures the widths of its own element children to pick a fit stage, and resets that
|
|
53
|
-
// measurement on any foreign change inside it. A decoration whose membership of the footer
|
|
54
|
-
// changes with the stage fights the ladder that moved it. The spacer renders in every stage, so
|
|
55
|
-
// a decoration beside it contributes a constant width and the ladder settles.
|
|
56
|
-
const stopWatch = ctx.watch("footerSpacer", (spacer) => ctx.mountBefore(spacer, () => badge));
|
|
57
|
-
|
|
58
|
-
return () => {
|
|
59
|
-
stopWatch();
|
|
60
|
-
stopTools();
|
|
61
|
-
stopStyle();
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
});
|