create-rigline-plugin 1.0.0-alpha.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lionell Pack
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 ADDED
@@ -0,0 +1,45 @@
1
+ # create-rigline-plugin
2
+
3
+ Scaffolds a workspace for [Rigline](https://github.com/Rigline/Rigline) plugins — plugins for the
4
+ Claude Code VS Code extension.
5
+
6
+ npm create rigline-plugin my-plugins
7
+
8
+ ## What you get
9
+
10
+ A pnpm workspace with `plugins/*` and one plugin in it, rather than a single-plugin repository. The
11
+ multi-plugin shape scaffolds correctly for one plugin and a second is then a directory copy, where a
12
+ single-plugin template could not grow into a workspace without a restructure.
13
+
14
+ my-plugins/
15
+ generated.ts the harvested identifiers, shared by every plugin here
16
+ pnpm-workspace.yaml with the supply-chain settings written down rather than inherited
17
+ tsconfig.base.json pulls the root harvest into every plugin's program
18
+ plugins/my-plugin/
19
+ rigline.json what the plugin declares it needs from the extension
20
+ src/index.ts the plugin
21
+ src/index.test.ts
22
+
23
+ Then:
24
+
25
+ pnpm install
26
+ pnpm codegen # harvest your installed extension, and commit the result
27
+ pnpm build
28
+ pnpm rigline add plugins/my-plugin
29
+
30
+ and *Developer: Reload Webviews*.
31
+
32
+ `generated.ts` ships as a placeholder so a fresh scaffold typechecks before `codegen` has ever run.
33
+ Once you run `codegen` it holds the identifiers *your* extension version actually has; commit it,
34
+ and the diff when you run against a newer extension is how you find out what moved.
35
+
36
+ ## Why a workspace root at all
37
+
38
+ The identifiers are harvested once, at the root, and imported by every plugin in the repository,
39
+ because they all compile against the same installed extension. Module augmentation is per-program,
40
+ so each plugin's tsconfig has to pull that harvest in — which the shared base config does, and which
41
+ is the one ordering dependency in the whole arrangement.
42
+
43
+ [Authoring guide](https://github.com/Rigline/Rigline/blob/main/docs/authoring.md)
44
+
45
+ MIT.
package/dist/index.js ADDED
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `create-rigline-plugin`: scaffolds a workspace holding one plugin (decisions.md, D50).
4
+ *
5
+ * A pnpm workspace with `plugins/*` rather than a single-plugin repository, because the multi-plugin
6
+ * shape is a superset: it scaffolds correctly for one plugin, and a second plugin is then a
7
+ * directory copy rather than a restructure.
8
+ *
9
+ * The template is real files under `template/`, copied and substituted, rather than strings in this
10
+ * module. It stays readable and reviewable that way, and the example plugin's own source is a file
11
+ * an editor can check rather than a string literal with its backticks escaped. Two things that
12
+ * takes care of, both of which have caught people out before: `.gitignore` is shipped as
13
+ * `gitignore`, because npm renames a `.gitignore` inside a published tarball and the scaffolded
14
+ * repository would arrive without one; and `generated.ts` is shipped as a placeholder, so the
15
+ * scaffold typechecks before `rigline codegen` has ever run, which is the promise D40 makes and the
16
+ * one moment it is easiest to break.
17
+ */
18
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { basename, dirname, join, relative, resolve } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ /** The manifest's own rule for a name, which is also npm's for an unscoped package. */
22
+ const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{0,213}$/;
23
+ export class ScaffoldError extends Error {
24
+ }
25
+ /** The template shipped with this package. */
26
+ export function defaultTemplateDir() {
27
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "template");
28
+ }
29
+ /**
30
+ * Writes the template into `target`, substituted.
31
+ *
32
+ * It refuses a directory that already has anything in it. A scaffold is not a merge, and a
33
+ * half-overwritten workspace is worse than either a new one or the one that was there.
34
+ */
35
+ export function scaffold(options) {
36
+ const dir = resolve(options.target);
37
+ const name = options.name ?? basename(dir);
38
+ if (!NAME_PATTERN.test(name)) {
39
+ throw new ScaffoldError(`"${name}" is not a usable plugin name. It must start with a letter or digit and hold only ` +
40
+ "lowercase letters, digits, dot, dash and underscore.");
41
+ }
42
+ if (existsSync(dir) && readdirSync(dir).length > 0) {
43
+ throw new ScaffoldError(`${dir} is not empty`);
44
+ }
45
+ const templateDir = options.templateDir ?? defaultTemplateDir();
46
+ if (!existsSync(templateDir)) {
47
+ throw new ScaffoldError(`the template is missing from this package: ${templateDir}`);
48
+ }
49
+ const substitutions = {
50
+ NAME: name,
51
+ DESCRIPTION: options.description ?? `A Rigline plugin called ${name}.`,
52
+ };
53
+ const files = [];
54
+ for (const source of walk(templateDir)) {
55
+ const relativePath = substitute(relative(templateDir, source), substitutions);
56
+ // npm renames a `.gitignore` inside a published tarball, so the template ships it undotted and
57
+ // it is put back here. Without this the scaffolded repository arrives with none at all.
58
+ const written = relativePath === "gitignore" ? ".gitignore" : relativePath;
59
+ const target = join(dir, written);
60
+ mkdirSync(dirname(target), { recursive: true });
61
+ writeFileSync(target, substitute(readFileSync(source, "utf8"), substitutions));
62
+ files.push(written.split("\\").join("/"));
63
+ }
64
+ files.sort();
65
+ return { dir, name, files };
66
+ }
67
+ /** Every file under `root`, recursively, in a stable order. */
68
+ function walk(root) {
69
+ const found = [];
70
+ for (const entry of readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
71
+ const path = join(root, entry.name);
72
+ if (entry.isDirectory())
73
+ found.push(...walk(path));
74
+ else
75
+ found.push(path);
76
+ }
77
+ return found;
78
+ }
79
+ /** `__NAME__` and `__DESCRIPTION__`, in a path or in a file's contents. */
80
+ function substitute(text, substitutions) {
81
+ let out = text;
82
+ for (const [key, value] of Object.entries(substitutions)) {
83
+ out = out.split(`__${key}__`).join(value);
84
+ }
85
+ return out;
86
+ }
87
+ /** Everything this scaffold wants said afterwards, in the order it wants doing. */
88
+ export function nextSteps(result) {
89
+ // The relative path, unless it is the worse of the two: a target several directories up produces
90
+ // a run of `..` nobody wants to read, let alone type.
91
+ const from = relative(process.cwd(), result.dir);
92
+ const here = from === "" ? "." : from.startsWith("..") ? result.dir : from;
93
+ return [
94
+ `Created ${result.files.length} files in ${result.dir}`,
95
+ "",
96
+ "Next:",
97
+ ` cd ${here}`,
98
+ " pnpm install",
99
+ " pnpm codegen # harvest the installed extension, and commit the result",
100
+ " pnpm build",
101
+ ` pnpm rigline add plugins/${result.name}`,
102
+ "",
103
+ "Then reload the webview: Developer: Reload Webviews.",
104
+ "",
105
+ `${join(here, "README.md")} has the rest, including the four rules worth reading first.`,
106
+ ].join("\n");
107
+ }
108
+ /**
109
+ * The command. One positional, the directory; `--name` where it should differ from the directory's
110
+ * own, and `--description` for the sentence that lands in three files.
111
+ */
112
+ export function main(argv) {
113
+ const positionals = [];
114
+ let name;
115
+ let description;
116
+ for (let at = 0; at < argv.length; at++) {
117
+ const argument = argv[at];
118
+ if (argument === "--name")
119
+ name = argv[++at];
120
+ else if (argument === "--description")
121
+ description = argv[++at];
122
+ else if (argument === "-h" || argument === "--help") {
123
+ console.log(USAGE);
124
+ return 0;
125
+ }
126
+ else if (argument.startsWith("-")) {
127
+ console.error(`create-rigline-plugin: unknown option "${argument}"\n\n${USAGE}`);
128
+ return 1;
129
+ }
130
+ else
131
+ positionals.push(argument);
132
+ }
133
+ if (positionals.length !== 1) {
134
+ console.error(`create-rigline-plugin: expected one directory\n\n${USAGE}`);
135
+ return 1;
136
+ }
137
+ try {
138
+ console.log(nextSteps(scaffold({ target: positionals[0], name, description })));
139
+ return 0;
140
+ }
141
+ catch (error) {
142
+ if (error instanceof ScaffoldError) {
143
+ console.error(`create-rigline-plugin: ${error.message}`);
144
+ return 1;
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ const USAGE = `create-rigline-plugin
150
+
151
+ pnpm create rigline-plugin <directory> [--name NAME] [--description TEXT]
152
+
153
+ Scaffolds a pnpm workspace holding one Rigline plugin. The directory's own name is the plugin's
154
+ unless --name says otherwise; a second plugin later is a copy of the first.`;
155
+ // Only when run as the command, so that a test may import `scaffold` without scaffolding anything.
156
+ // Compared as resolved paths rather than by suffix, which a directory named after the bin defeats.
157
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
158
+ process.exitCode = main(process.argv.slice(2));
159
+ }
160
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "create-rigline-plugin",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "Scaffold a workspace for Claude Code VS Code extension plugins",
5
+ "keywords": [
6
+ "rigline",
7
+ "claude-code",
8
+ "vscode",
9
+ "plugin",
10
+ "scaffold",
11
+ "create"
12
+ ],
13
+ "homepage": "https://github.com/Rigline/Rigline#readme",
14
+ "bugs": "https://github.com/Rigline/Rigline/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Rigline/Rigline.git",
18
+ "directory": "packages/create-plugin"
19
+ },
20
+ "author": "Lionell Pack",
21
+ "type": "module",
22
+ "license": "MIT",
23
+ "files": [
24
+ "dist",
25
+ "template"
26
+ ],
27
+ "bin": {
28
+ "create-rigline-plugin": "./dist/index.js"
29
+ },
30
+ "devDependencies": {
31
+ "@rigline/plugin-api": "1.0.0-alpha.0"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "typecheck": "tsc -p tsconfig.json"
36
+ }
37
+ }
@@ -0,0 +1,67 @@
1
+ # __NAME__
2
+
3
+ Plugins for the Claude Code VS Code extension, built with [Rigline](https://github.com/Rigline/Rigline).
4
+
5
+ A plugin is one browser ES module and a `rigline.json` manifest. This workspace holds one to start
6
+ with; adding a second is a directory copy.
7
+
8
+ ## First run
9
+
10
+ pnpm install
11
+ pnpm codegen # harvest the installed extension, and commit the result
12
+ pnpm build
13
+ pnpm rigline add plugins/__NAME__
14
+
15
+ Then reload the webview: **Developer: Reload Webviews** in the command palette. The badge appears
16
+ in the composer footer.
17
+
18
+ `pnpm codegen` writes `generated.ts` at this workspace's root: the identifiers the installed
19
+ extension actually has, as types every plugin here compiles against. Commit it. It is replaced when
20
+ you run against a newer extension, and the diff is how you find out what moved.
21
+
22
+ ## The loop while you work
23
+
24
+ pnpm rigline dev plugins/__NAME__
25
+
26
+ Rebuilds and re-injects on every save. Reload webviews to see each change.
27
+
28
+ ## What a plugin declares
29
+
30
+ Everything in `uses` is a dependency on the installed extension still having something. An update
31
+ that retires one refuses this plugin by name, at install, rather than leaving it subtly broken —
32
+ which is the whole reason to declare rather than to reach.
33
+
34
+ pnpm rigline check # what this extension version would refuse, and why
35
+ pnpm rigline list # every plugin installed, and what each says it can do
36
+
37
+ Put a dependency you can do without under `uses.optional`: it is checked the same way and costs the
38
+ plugin that one decoration rather than the whole plugin.
39
+
40
+ ## Testing
41
+
42
+ `pnpm test` runs the pure half — the functions that do not touch the DOM. Whether a decoration
43
+ lands in the right place, survives a re-render, or costs a row a line of height is a question only
44
+ the app can answer: build, add, reload, look.
45
+
46
+ ## Publishing
47
+
48
+ 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` bundles
50
+ everything the entry imports, so a published plugin has no runtime dependency to install, and
51
+ `@rigline/plugin-api` stays a *devDependency*.
52
+
53
+ pnpm --filter rigline-plugin-__NAME__ publish
54
+
55
+ ## Rules that will cost you if you break them
56
+
57
+ - **Never name a class from the bundle by hand.** They are minifier output and change every build.
58
+ Use `ctx.anchor("footerSpacer")` for curated names, and `ctx.cls(module, local)` — declared in the
59
+ manifest — for anything not curated yet.
60
+ - **Never scope a stylesheet rule to an anchor's bare class.** One class is applied wherever that
61
+ look is wanted, so a rule written against it lands on every control wearing it. Scope to something
62
+ you placed.
63
+ - **Ask what a container does about its children before decorating it.** The composer footer
64
+ measures its own element children and re-measures on any foreign change inside it; footer
65
+ decorations go beside `footerSpacer`, with `ctx.mountBefore`.
66
+ - **Do not poll for an element.** `ctx.watch(name, …)` hands it over when it appears and again when
67
+ the app replaces it.
@@ -0,0 +1,13 @@
1
+ // A placeholder, so that this workspace typechecks before it has ever harvested anything.
2
+ //
3
+ // Replace it by running `pnpm codegen`, which reads the Claude Code extension installed on this
4
+ // machine and writes the real file: a module augmentation naming every CSS module, class, message
5
+ // type and payload field that extension has, plus the harvest the install flow diffs the next
6
+ // version against. Commit the result the way you commit a lockfile.
7
+ //
8
+ // Until you do, `ctx.cls`, `ctx.onMessage` and `ctx.rewrite` accept any string rather than the
9
+ // identifiers that exist. Everything still compiles and everything still runs; what you lose is
10
+ // being told about a typo at your desk instead of at install time. A plugin written entirely
11
+ // against curated anchors needs no harvest at all.
12
+
13
+ export const EXTENSION_VERSION = "not harvested yet";
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "__NAME__-plugins",
3
+ "private": true,
4
+ "type": "module",
5
+ "packageManager": "pnpm@12.3.4",
6
+ "engines": { "node": ">=26" },
7
+ "scripts": {
8
+ "codegen": "rigline codegen",
9
+ "build": "pnpm -r build",
10
+ "typecheck": "pnpm -r typecheck",
11
+ "test": "vitest run"
12
+ },
13
+ "devDependencies": {
14
+ "@rigline/plugin-api": "^1.0.0",
15
+ "rigline": "^1.0.0",
16
+ "typescript": "^7.0.2",
17
+ "vitest": "^5.0.0"
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ # __NAME__
2
+
3
+ __DESCRIPTION__
4
+
5
+ ## Depends on
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
+ - `uses.tools` — `ctx.onToolUse`, every completed tool call the assistant makes.
15
+
16
+ ## Notes
17
+
18
+ `badgeText` is exported and tested because it is the half a plain test run can hold. Everything
19
+ else is a question about the app, and only the app can answer it.
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "rigline-plugin-__NAME__",
3
+ "version": "0.1.0",
4
+ "description": "__DESCRIPTION__",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "files": ["dist", "rigline.json"],
8
+ "scripts": {
9
+ "build": "rigline build",
10
+ "typecheck": "tsc -p tsconfig.json"
11
+ },
12
+ "devDependencies": {
13
+ "@rigline/plugin-api": "^1.0.0",
14
+ "rigline": "^1.0.0"
15
+ }
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "../../node_modules/@rigline/plugin-api/schema/manifest.json",
3
+ "api": 1,
4
+ "name": "__NAME__",
5
+ "description": "__DESCRIPTION__",
6
+ "entry": "dist/index.js",
7
+ "surfaces": ["editor", "sidebar"],
8
+ "uses": {
9
+ "anchors": ["footerSpacer"],
10
+ "mount": true,
11
+ "style": true,
12
+ "tools": true
13
+ }
14
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The pure half of the plugin, under plain `vitest`.
3
+ *
4
+ * Verification splits three ways, and knowing which tier a question belongs to is most of writing
5
+ * a test that is worth having. A pure function is this tier. Whether a decoration lands in the
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, `rigline add`, reload the webview, look.
8
+ */
9
+ import { describe, expect, it } from "vitest";
10
+ import { badgeText } from "./index.ts";
11
+
12
+ describe("badgeText", () => {
13
+ it("says something before anything has happened", () => {
14
+ expect(badgeText(0)).toBe("no tools yet");
15
+ });
16
+
17
+ it("does not make a reader read (s)", () => {
18
+ expect(badgeText(1)).toBe("1 tool call");
19
+ expect(badgeText(2)).toBe("2 tool calls");
20
+ });
21
+ });
@@ -0,0 +1,64 @@
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
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.plugin.json",
3
+ "include": ["src/**/*.ts"],
4
+ "exclude": ["src/**/*.test.ts"]
5
+ }
@@ -0,0 +1,21 @@
1
+ packages:
2
+ - plugins/*
3
+
4
+ # Supply-chain settings, written down rather than inherited.
5
+ #
6
+ # Three of these are pnpm's own defaults today. They are stated anyway, because a default is not a
7
+ # position: a fresh clone on a different pnpm, or a default that moves, would quietly change what
8
+ # this repository is willing to install without anyone deciding to.
9
+ #
10
+ # minimumReleaseAge is the rule rigline applies to plugins, applied here to your own dependencies:
11
+ # a day is where a compromised publish is usually caught, and a day of latency costs nothing.
12
+ minimumReleaseAge: 1440
13
+
14
+ # Only a direct dependency may come from a git repository or a tarball URL. A transitive dependency
15
+ # that resolves outside the registry is the shape of a supply-chain problem rather than of an
16
+ # ordinary package.
17
+ blockExoticSubdeps: true
18
+
19
+ # No dependency may run an install script. Empty because nothing here needs one; the day something
20
+ # does, a person decides about it by name, here.
21
+ allowBuilds: {}
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "moduleDetection": "force",
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "noImplicitOverride": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "isolatedModules": true,
14
+ "verbatimModuleSyntax": true,
15
+ "erasableSyntaxOnly": true,
16
+ "skipLibCheck": true,
17
+ "forceConsistentCasingInFileNames": true
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "//": "The base every plugin here extends. `files` names the committed harvest so that its module augmentation is in each plugin's own program: augmentation is per-program, so a plugin whose tsconfig does not pull generated.ts in compiles with every extension identifier widened to `string`. Relative paths in an extended config resolve against the config that declared them, so this one entry serves every plugin however deep it sits. A plugin's own tsconfig overrides `include` and leaves `files` alone. That augmentation resolves `@rigline/plugin-api` from this directory rather than from the plugin's, which is why the workspace root depends on it too.",
3
+ "extends": "./tsconfig.base.json",
4
+ "compilerOptions": {
5
+ "lib": ["esnext", "dom"],
6
+ "types": [],
7
+ "noEmit": true,
8
+ "allowImportingTsExtensions": true
9
+ },
10
+ "files": ["generated.ts"]
11
+ }