@fairfox/polly 0.20.1 → 0.22.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/README.md +83 -3
- package/dist/cli/polly.js +21 -1
- package/dist/cli/polly.js.map +3 -3
- package/dist/src/background/index.js.map +7 -7
- package/dist/src/background/message-router.js.map +7 -7
- package/dist/src/elysia/index.d.ts +2 -0
- package/dist/src/elysia/index.js +177 -17
- package/dist/src/elysia/index.js.map +8 -5
- package/dist/src/elysia/peer-repo-plugin.d.ts +79 -0
- package/dist/src/elysia/signaling-server-plugin.d.ts +121 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +90 -1
- package/dist/src/index.js.map +15 -13
- package/dist/src/mesh.d.ts +29 -0
- package/dist/src/mesh.js +1502 -0
- package/dist/src/mesh.js.map +22 -0
- package/dist/src/peer.d.ts +29 -0
- package/dist/src/peer.js +928 -0
- package/dist/src/peer.js.map +20 -0
- package/dist/src/shared/adapters/index.js.map +6 -6
- package/dist/src/shared/lib/_client-only.d.ts +38 -0
- package/dist/src/shared/lib/access.d.ts +124 -0
- package/dist/src/shared/lib/blob-ref.d.ts +72 -0
- package/dist/src/shared/lib/context-helpers.js.map +7 -7
- package/dist/src/shared/lib/crdt-specialised.d.ts +129 -0
- package/dist/src/shared/lib/crdt-state.d.ts +86 -0
- package/dist/src/shared/lib/encryption.d.ts +117 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +130 -0
- package/dist/src/shared/lib/mesh-signaling-client.d.ts +85 -0
- package/dist/src/shared/lib/mesh-state.d.ts +102 -0
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +132 -0
- package/dist/src/shared/lib/message-bus.js.map +7 -7
- package/dist/src/shared/lib/migrate-primitive.d.ts +100 -0
- package/dist/src/shared/lib/pairing.d.ts +170 -0
- package/dist/src/shared/lib/peer-relay-adapter.d.ts +80 -0
- package/dist/src/shared/lib/peer-repo-server.d.ts +83 -0
- package/dist/src/shared/lib/peer-state.d.ts +117 -0
- package/dist/src/shared/lib/primitive-registry.d.ts +88 -0
- package/dist/src/shared/lib/resource.js.map +4 -4
- package/dist/src/shared/lib/revocation.d.ts +126 -0
- package/dist/src/shared/lib/schema-version.d.ts +129 -0
- package/dist/src/shared/lib/signing.d.ts +118 -0
- package/dist/src/shared/lib/state.js.map +4 -4
- package/dist/src/shared/state/app-state.js.map +5 -5
- package/dist/tools/init/src/cli.js.map +1 -1
- package/dist/tools/quality/src/cli.js +162 -0
- package/dist/tools/quality/src/cli.js.map +11 -0
- package/dist/tools/test/src/adapters/index.js.map +2 -2
- package/dist/tools/test/src/browser/harness.d.ts +80 -0
- package/dist/tools/test/src/browser/index.d.ts +32 -0
- package/dist/tools/test/src/browser/index.js +243 -0
- package/dist/tools/test/src/browser/index.js.map +10 -0
- package/dist/tools/test/src/browser/run.d.ts +26 -0
- package/dist/tools/test/src/index.js.map +2 -2
- package/dist/tools/verify/specs/tla/MeshState.cfg +21 -0
- package/dist/tools/verify/specs/tla/MeshState.tla +247 -0
- package/dist/tools/verify/specs/tla/PeerState.cfg +27 -0
- package/dist/tools/verify/specs/tla/PeerState.tla +238 -0
- package/dist/tools/verify/specs/tla/README.md +27 -3
- package/dist/tools/verify/src/cli.js.map +8 -8
- package/dist/tools/visualize/src/cli.js.map +7 -7
- package/package.json +51 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @fairfox/polly
|
|
2
2
|
|
|
3
|
-
Reactive state for multi-context apps
|
|
3
|
+
Reactive state for multi-context apps — from single-process signals to peer-to-peer encrypted mesh — with formal verification.
|
|
4
4
|
|
|
5
5
|
## The pitch
|
|
6
6
|
|
|
@@ -86,6 +86,45 @@ todos.status; // Signal<"idle" | "loading" | "success" | "error">
|
|
|
86
86
|
todos.refetch();
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
## Peer-first state (v0.21.0)
|
|
90
|
+
|
|
91
|
+
The four primitives above keep state consistent inside a single deployment. But some applications need state that survives the server going away, or state that the server never sees at all. Polly now offers three resilience tiers, each a distinct primitive family:
|
|
92
|
+
|
|
93
|
+
| Tier | Primitive | Server's role | Resilience |
|
|
94
|
+
|------|-----------|---------------|------------|
|
|
95
|
+
| Weakest | `$sharedState` | Source of truth | Server backups |
|
|
96
|
+
| Middle | `$peerState` | Full peer on the data path | Any device can rehydrate the server |
|
|
97
|
+
| Strongest | `$meshState` | Not on the data path | App works with zero server uptime |
|
|
98
|
+
|
|
99
|
+
**`$peerState`** — every device holds a full [Automerge](https://automerge.org/) CRDT replica. The server holds one too, so cron and HTTP handlers can read and mutate documents. If the server loses its storage, any reconnecting client repopulates it through the normal sync protocol.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { createPeerStateClient, configurePeerState, $peerState } from "@fairfox/polly";
|
|
103
|
+
|
|
104
|
+
const client = createPeerStateClient({ url: "wss://yourapp.com/polly/peer" });
|
|
105
|
+
configurePeerState(client.repo);
|
|
106
|
+
|
|
107
|
+
const settings = $peerState("settings", { theme: "dark" });
|
|
108
|
+
await settings.loaded;
|
|
109
|
+
settings.value = { theme: "light" }; // syncs to every peer
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**`$meshState`** — peers exchange operations directly over WebRTC data channels, signed with Ed25519 and encrypted with XSalsa20-Poly1305. No server sees the data. A small stateless signalling server helps peers find each other; removing it does not affect running connections.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { configureMeshState, $meshState, MeshNetworkAdapter, MeshWebRTCAdapter } from "@fairfox/polly";
|
|
116
|
+
|
|
117
|
+
const repo = new Repo({ network: [new MeshNetworkAdapter({ base: webrtcAdapter, keyring })] });
|
|
118
|
+
configureMeshState(repo);
|
|
119
|
+
|
|
120
|
+
const notes = $meshState("notes", { entries: [] });
|
|
121
|
+
// Operations flow peer-to-peer, signed and encrypted
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
First-time key exchange between devices uses a pairing token displayed as a QR code. Compromised devices are revoked via signed revocation records that propagate to every peer.
|
|
125
|
+
|
|
126
|
+
The three tiers coexist in one application — public settings in `$sharedState`, collaborative documents in `$peerState`, private notes in `$meshState`. See [docs/STATE.md](docs/STATE.md) for the full decision tree and [docs/RFC-041-choosing.md](docs/RFC-041-choosing.md) for the design rationale.
|
|
127
|
+
|
|
89
128
|
## Verification that plugs in
|
|
90
129
|
|
|
91
130
|
A popup and a background script both write to the same state. A content script reads it mid-update. Tests miss these bugs because they depend on timing.
|
|
@@ -206,16 +245,57 @@ polly verify Run formal verification
|
|
|
206
245
|
polly visualize Generate architecture diagrams (Structurizr DSL)
|
|
207
246
|
```
|
|
208
247
|
|
|
248
|
+
## Quality tooling
|
|
249
|
+
|
|
250
|
+
Polly ships conformance checks and a browser test harness that consuming applications can adopt directly.
|
|
251
|
+
|
|
252
|
+
**No-as-casting check.** Bans TypeScript `as` type assertions codebase-wide (only `as const` and the explicit escape hatch `as unknown as` are allowed). Violations include pattern-specific fix advice. Import it programmatically:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { checkNoAsCasting } from "@fairfox/polly/quality";
|
|
256
|
+
|
|
257
|
+
const result = await checkNoAsCasting({ rootDir: process.cwd() });
|
|
258
|
+
if (result.violations.length > 0) {
|
|
259
|
+
result.print();
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Browser test harness.** A Puppeteer-based harness for testing browser-only code (WebRTC adapters, Preact components, anything needing real DOM). Bundles each test file with Bun.build, serves on an ephemeral port, and collects results:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { describe, test, expect, done, flush, cleanup } from "@fairfox/polly/test/browser";
|
|
268
|
+
|
|
269
|
+
describe("my component", () => {
|
|
270
|
+
test("renders", async () => {
|
|
271
|
+
render(<MyComponent />, app);
|
|
272
|
+
await flush();
|
|
273
|
+
expect(app.querySelector("h1")).toHaveTextContent("Hello");
|
|
274
|
+
cleanup(app);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
done();
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Run with `bun tools/test/src/browser/run.ts tests/browser`.
|
|
282
|
+
|
|
209
283
|
## Use with Claude Code
|
|
210
284
|
|
|
211
|
-
This repo ships with a [Claude Code skill](.claude/skills) that
|
|
285
|
+
This repo ships with a [Claude Code skill](.claude/skills) that covers the full Polly API — state primitives, peer-first and mesh state, verification, quality tooling, and the browser test harness. Install it in your project so Claude can help you integrate Polly with full context:
|
|
212
286
|
|
|
213
287
|
```bash
|
|
214
288
|
# From your project directory
|
|
215
289
|
claude install-skill /path/to/polly/.claude/skills
|
|
216
290
|
```
|
|
217
291
|
|
|
218
|
-
Then ask Claude things like
|
|
292
|
+
Then ask Claude things like:
|
|
293
|
+
- "How would Polly fit into this project?"
|
|
294
|
+
- "Add verification to my handlers"
|
|
295
|
+
- "Set up $peerState with an Elysia server"
|
|
296
|
+
- "Wire up the no-as-casting conformance check in CI"
|
|
297
|
+
|
|
298
|
+
If you maintain your own Claude Code skills, consider adding Polly's state-tier decision tree and verification patterns to your project-specific skill so Claude can recommend the right primitive for each piece of state.
|
|
219
299
|
|
|
220
300
|
## Licence
|
|
221
301
|
|
package/dist/cli/polly.js
CHANGED
|
@@ -89,6 +89,22 @@ async function visualize() {
|
|
|
89
89
|
throw new Error(`Visualization failed with exit code ${exitCode}`);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
async function quality() {
|
|
93
|
+
const bundledCli = `${__dirname2}/../tools/quality/src/cli.js`;
|
|
94
|
+
const monorepoCli = `${__dirname2}/../tools/quality/src/cli.ts`;
|
|
95
|
+
const qualityCli = await Bun.file(bundledCli).exists() ? bundledCli : monorepoCli;
|
|
96
|
+
const proc = Bun.spawn(["bun", qualityCli, ...commandArgs], {
|
|
97
|
+
cwd,
|
|
98
|
+
stdout: "inherit",
|
|
99
|
+
stderr: "inherit",
|
|
100
|
+
stdin: "inherit",
|
|
101
|
+
env: process.env
|
|
102
|
+
});
|
|
103
|
+
const exitCode = await proc.exited;
|
|
104
|
+
if (exitCode !== 0) {
|
|
105
|
+
throw new Error(`Quality checks failed with exit code ${exitCode}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
92
108
|
async function typecheck() {
|
|
93
109
|
const proc = Bun.spawn(["bunx", "tsc", "--noEmit"], {
|
|
94
110
|
cwd,
|
|
@@ -191,6 +207,7 @@ Usage:
|
|
|
191
207
|
polly test [args] Run tests
|
|
192
208
|
polly verify [args] Run formal verification
|
|
193
209
|
polly visualize [args] Generate architecture diagrams
|
|
210
|
+
polly quality [args] Run quality conformance checks
|
|
194
211
|
polly help Show this help
|
|
195
212
|
|
|
196
213
|
Options:
|
|
@@ -232,6 +249,9 @@ async function main() {
|
|
|
232
249
|
case "visualize":
|
|
233
250
|
await visualize();
|
|
234
251
|
break;
|
|
252
|
+
case "quality":
|
|
253
|
+
await quality();
|
|
254
|
+
break;
|
|
235
255
|
case "help":
|
|
236
256
|
case "--help":
|
|
237
257
|
case "-h":
|
|
@@ -252,4 +272,4 @@ async function main() {
|
|
|
252
272
|
}
|
|
253
273
|
main();
|
|
254
274
|
|
|
255
|
-
//# debugId=
|
|
275
|
+
//# debugId=B9B6663515E3E39364756E2164756E21
|
package/dist/cli/polly.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../cli/polly.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"#!/usr/bin/env bun\n/**\n * Polly CLI\n *\n * Command-line tool for building multi-execution-context applications\n * with reactive state and cross-context messaging.\n *\n * Supports: Chrome extensions, PWAs, Node/Bun/Deno apps with workers\n *\n * Usage:\n * polly init [name] [--type=TYPE] Create a new project\n * polly check Run all checks (typecheck, lint, test, build)\n * polly build [options] Build the project\n * polly dev Build with watch mode\n * polly typecheck Type check your code\n * polly lint [--fix] Lint your code\n * polly format Format your code\n * polly test [args] Run tests (requires bun test)\n * polly verify [args] Run formal verification\n * polly visualize [args] Generate architecture diagrams\n * polly help Show help\n *\n * Project Types (init --type):\n * extension Chrome/Firefox extension (default)\n * pwa Progressive Web App with workers\n * websocket WebSocket server application\n * generic Generic TypeScript project\n *\n * Options:\n * --prod Build for production (minified)\n * --config <path> Path to config file (default: polly.config.ts)\n * --fix Auto-fix lint/format issues\n * --type=TYPE Project type for init command\n */\n\n// Use Bun built-ins instead of Node.js APIs\nconst __dirname = import.meta.dir;\n\nconst command = process.argv[2];\nconst commandArgs = process.argv.slice(3);\nconst cwd = process.cwd();\n\n// Parse arguments\nconst args = {\n prod: process.argv.includes(\"--prod\"),\n config: process.argv.includes(\"--config\")\n ? process.argv[process.argv.indexOf(\"--config\") + 1]\n : undefined,\n};\n\n/**\n * Load user's configuration\n */\nasync function loadConfig() {\n const configPaths = [\n args.config,\n `${cwd}/polly.config.ts`,\n `${cwd}/polly.config.js`,\n `${cwd}/polly.config.mjs`,\n ].filter(Boolean) as string[];\n\n for (const configPath of configPaths) {\n // Use Bun.file().exists() instead of existsSync\n if (await Bun.file(configPath).exists()) {\n try {\n const config = await import(configPath);\n return config.default || config;\n } catch (error) {\n console.log(`❌ Failed to load config: ${configPath}`);\n throw error;\n }\n }\n }\n return {\n srcDir: \"src\",\n distDir: \"dist\",\n manifest: \"manifest.json\",\n };\n}\n\n/**\n * Build command - build the extension\n */\nasync function build() {\n const config = await loadConfig();\n\n // Check if bundled (published) or in monorepo\n const bundledScript = `${__dirname}/../scripts/build-extension.js`;\n const monorepoScript = `${__dirname}/../scripts/build-extension.ts`;\n const buildScriptPath = (await Bun.file(bundledScript).exists()) ? bundledScript : monorepoScript;\n\n // Pass config via environment\n process.env[\"WEB_EXT_SRC\"] = `${cwd}/${config.srcDir || \"src\"}`;\n process.env[\"WEB_EXT_DIST\"] = `${cwd}/${config.distDir || \"dist\"}`;\n process.env[\"WEB_EXT_MANIFEST\"] = `${cwd}/${config.manifest || \"manifest.json\"}`;\n process.env[\"WEB_EXT_CWD\"] = cwd;\n process.env[\"WEB_EXT_PROD\"] = args.prod ? \"true\" : \"false\";\n\n // Run build\n const proc = Bun.spawn([\"bun\", buildScriptPath], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n process.exit(exitCode);\n }\n}\n\n/**\n * Dev command - build with watch mode\n */\nasync function dev() {\n await build();\n}\n\n/**\n * Verify command - delegate to @fairfox/web-ext-verify\n */\nasync function verify() {\n // Check if bundled (published) or in monorepo\n const bundledCli = `${__dirname}/../tools/verify/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/verify/src/cli.ts`;\n const verifyCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", verifyCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n env: process.env,\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Verification failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Visualize command - delegate to @fairfox/polly-visualize\n */\nasync function visualize() {\n // Check if bundled (published) or in monorepo\n const bundledCli = `${__dirname}/../tools/visualize/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/visualize/src/cli.ts`;\n const visualizeCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", visualizeCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n env: process.env,\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Visualization failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Typecheck command - run TypeScript type checking\n */\nasync function typecheck() {\n const proc = Bun.spawn([\"bunx\", \"tsc\", \"--noEmit\"], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Type checking failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Lint command - run Biome linter\n */\nasync function lint() {\n const fix = commandArgs.includes(\"--fix\");\n const lintArgs = fix ? [\"check\", \"--write\", \".\"] : [\"check\", \".\"];\n\n const proc = Bun.spawn([\"bunx\", \"@biomejs/biome\", ...lintArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Linting failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Format command - run Biome formatter\n */\nasync function format() {\n const proc = Bun.spawn([\"bunx\", \"@biomejs/biome\", \"format\", \"--write\", \".\"], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Formatting failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Test command - delegate to @fairfox/polly-test\n */\nasync function test() {\n // Check if bundled (published) or in monorepo\n const bundledCli = `${__dirname}/../tools/test/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/test/src/cli.ts`;\n const testCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", testCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Tests failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Check command - run all quality checks in sequence\n */\nasync function check() {\n const checks = [\n { name: \"Type checking\", fn: typecheck },\n { name: \"Linting\", fn: lint },\n { name: \"Testing\", fn: test },\n { name: \"Building\", fn: build },\n { name: \"Verification\", fn: verify, optional: true },\n { name: \"Visualization\", fn: visualize, optional: true },\n ];\n\n for (const { name, fn, optional } of checks) {\n try {\n await fn();\n } catch (_error) {\n if (optional) {\n continue;\n }\n console.log(`\\n\\x1b[31m✗ ${name} failed\\x1b[0m\\n`);\n process.exit(1);\n }\n }\n}\n\n/**\n * Init command - delegate to @fairfox/polly-init\n */\nasync function init() {\n // Check if bundled (published) or in monorepo\n const bundledCli = `${__dirname}/../tools/init/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/init/src/cli.ts`;\n const initCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", initCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Initialization failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Help command — prints usage from the docstring at the top of this file\n */\nfunction help() {\n console.log(`Polly CLI — multi-execution-context framework\n\nUsage:\n polly init [name] [--type=TYPE] Create a new project\n polly check Run all checks (typecheck, lint, test, build)\n polly build [options] Build the project\n polly dev Build with watch mode\n polly typecheck Type check your code\n polly lint [--fix] Lint your code\n polly format Format your code\n polly test [args] Run tests\n polly verify [args] Run formal verification\n polly visualize [args] Generate architecture diagrams\n polly help Show this help\n\nOptions:\n --prod Build for production (minified)\n --config <path> Path to config file (default: polly.config.ts)\n --fix Auto-fix lint/format issues\n --type=TYPE Project type for init command (pwa, extension, websocket, generic)`);\n}\n\n/**\n * Main entry point\n */\nasync function main() {\n try {\n switch (command) {\n case \"init\":\n await init();\n break;\n case \"check\":\n await check();\n break;\n case \"build\":\n await build();\n break;\n case \"dev\":\n await dev();\n break;\n case \"typecheck\":\n await typecheck();\n break;\n case \"lint\":\n await lint();\n break;\n case \"format\":\n await format();\n break;\n case \"test\":\n await test();\n break;\n case \"verify\":\n await verify();\n break;\n case \"visualize\":\n await visualize();\n break;\n case \"help\":\n case \"--help\":\n case \"-h\":\n case undefined:\n help();\n break;\n default:\n console.log(`❌ Unknown command: ${command}\\n`);\n help();\n process.exit(1);\n }\n } catch (error) {\n console.log(\"\\n❌ Command failed:\", error);\n process.exit(1);\n }\n}\n\nmain();\n"
|
|
5
|
+
"#!/usr/bin/env bun\n/**\n * Polly CLI\n *\n * Command-line tool for building multi-execution-context applications\n * with reactive state and cross-context messaging.\n *\n * Supports: Chrome extensions, PWAs, Node/Bun/Deno apps with workers\n *\n * Usage:\n * polly init [name] [--type=TYPE] Create a new project\n * polly check Run all checks (typecheck, lint, test, build)\n * polly build [options] Build the project\n * polly dev Build with watch mode\n * polly typecheck Type check your code\n * polly lint [--fix] Lint your code\n * polly format Format your code\n * polly test [args] Run tests (requires bun test)\n * polly verify [args] Run formal verification\n * polly visualize [args] Generate architecture diagrams\n * polly quality [args] Run quality conformance checks\n * polly help Show help\n *\n * Project Types (init --type):\n * extension Chrome/Firefox extension (default)\n * pwa Progressive Web App with workers\n * websocket WebSocket server application\n * generic Generic TypeScript project\n *\n * Options:\n * --prod Build for production (minified)\n * --config <path> Path to config file (default: polly.config.ts)\n * --fix Auto-fix lint/format issues\n * --type=TYPE Project type for init command\n */\n\n// Use Bun built-ins instead of Node.js APIs\nconst __dirname = import.meta.dir;\n\nconst command = process.argv[2];\nconst commandArgs = process.argv.slice(3);\nconst cwd = process.cwd();\n\n// Parse arguments\nconst args = {\n prod: process.argv.includes(\"--prod\"),\n config: process.argv.includes(\"--config\")\n ? process.argv[process.argv.indexOf(\"--config\") + 1]\n : undefined,\n};\n\n/**\n * Load user's configuration\n */\nasync function loadConfig() {\n const configPaths = [\n args.config,\n `${cwd}/polly.config.ts`,\n `${cwd}/polly.config.js`,\n `${cwd}/polly.config.mjs`,\n ].filter(Boolean) as string[];\n\n for (const configPath of configPaths) {\n // Use Bun.file().exists() instead of existsSync\n if (await Bun.file(configPath).exists()) {\n try {\n const config = await import(configPath);\n return config.default || config;\n } catch (error) {\n console.log(`❌ Failed to load config: ${configPath}`);\n throw error;\n }\n }\n }\n return {\n srcDir: \"src\",\n distDir: \"dist\",\n manifest: \"manifest.json\",\n };\n}\n\n/**\n * Build command - build the extension\n */\nasync function build() {\n const config = await loadConfig();\n\n // Check if bundled (published) or in monorepo\n const bundledScript = `${__dirname}/../scripts/build-extension.js`;\n const monorepoScript = `${__dirname}/../scripts/build-extension.ts`;\n const buildScriptPath = (await Bun.file(bundledScript).exists()) ? bundledScript : monorepoScript;\n\n // Pass config via environment\n process.env[\"WEB_EXT_SRC\"] = `${cwd}/${config.srcDir || \"src\"}`;\n process.env[\"WEB_EXT_DIST\"] = `${cwd}/${config.distDir || \"dist\"}`;\n process.env[\"WEB_EXT_MANIFEST\"] = `${cwd}/${config.manifest || \"manifest.json\"}`;\n process.env[\"WEB_EXT_CWD\"] = cwd;\n process.env[\"WEB_EXT_PROD\"] = args.prod ? \"true\" : \"false\";\n\n // Run build\n const proc = Bun.spawn([\"bun\", buildScriptPath], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n process.exit(exitCode);\n }\n}\n\n/**\n * Dev command - build with watch mode\n */\nasync function dev() {\n await build();\n}\n\n/**\n * Verify command - delegate to @fairfox/web-ext-verify\n */\nasync function verify() {\n // Check if bundled (published) or in monorepo\n const bundledCli = `${__dirname}/../tools/verify/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/verify/src/cli.ts`;\n const verifyCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", verifyCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n env: process.env,\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Verification failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Visualize command - delegate to @fairfox/polly-visualize\n */\nasync function visualize() {\n // Check if bundled (published) or in monorepo\n const bundledCli = `${__dirname}/../tools/visualize/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/visualize/src/cli.ts`;\n const visualizeCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", visualizeCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n env: process.env,\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Visualization failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Quality command - delegate to @fairfox/polly-quality\n */\nasync function quality() {\n const bundledCli = `${__dirname}/../tools/quality/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/quality/src/cli.ts`;\n const qualityCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", qualityCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n env: process.env,\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Quality checks failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Typecheck command - run TypeScript type checking\n */\nasync function typecheck() {\n const proc = Bun.spawn([\"bunx\", \"tsc\", \"--noEmit\"], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Type checking failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Lint command - run Biome linter\n */\nasync function lint() {\n const fix = commandArgs.includes(\"--fix\");\n const lintArgs = fix ? [\"check\", \"--write\", \".\"] : [\"check\", \".\"];\n\n const proc = Bun.spawn([\"bunx\", \"@biomejs/biome\", ...lintArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Linting failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Format command - run Biome formatter\n */\nasync function format() {\n const proc = Bun.spawn([\"bunx\", \"@biomejs/biome\", \"format\", \"--write\", \".\"], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Formatting failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Test command - delegate to @fairfox/polly-test\n */\nasync function test() {\n // Check if bundled (published) or in monorepo\n const bundledCli = `${__dirname}/../tools/test/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/test/src/cli.ts`;\n const testCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", testCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Tests failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Check command - run all quality checks in sequence\n */\nasync function check() {\n const checks = [\n { name: \"Type checking\", fn: typecheck },\n { name: \"Linting\", fn: lint },\n { name: \"Testing\", fn: test },\n { name: \"Building\", fn: build },\n { name: \"Verification\", fn: verify, optional: true },\n { name: \"Visualization\", fn: visualize, optional: true },\n ];\n\n for (const { name, fn, optional } of checks) {\n try {\n await fn();\n } catch (_error) {\n if (optional) {\n continue;\n }\n console.log(`\\n\\x1b[31m✗ ${name} failed\\x1b[0m\\n`);\n process.exit(1);\n }\n }\n}\n\n/**\n * Init command - delegate to @fairfox/polly-init\n */\nasync function init() {\n // Check if bundled (published) or in monorepo\n const bundledCli = `${__dirname}/../tools/init/src/cli.js`;\n const monorepoCli = `${__dirname}/../tools/init/src/cli.ts`;\n const initCli = (await Bun.file(bundledCli).exists()) ? bundledCli : monorepoCli;\n\n const proc = Bun.spawn([\"bun\", initCli, ...commandArgs], {\n cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n });\n\n const exitCode = await proc.exited;\n if (exitCode !== 0) {\n throw new Error(`Initialization failed with exit code ${exitCode}`);\n }\n}\n\n/**\n * Help command — prints usage from the docstring at the top of this file\n */\nfunction help() {\n console.log(`Polly CLI — multi-execution-context framework\n\nUsage:\n polly init [name] [--type=TYPE] Create a new project\n polly check Run all checks (typecheck, lint, test, build)\n polly build [options] Build the project\n polly dev Build with watch mode\n polly typecheck Type check your code\n polly lint [--fix] Lint your code\n polly format Format your code\n polly test [args] Run tests\n polly verify [args] Run formal verification\n polly visualize [args] Generate architecture diagrams\n polly quality [args] Run quality conformance checks\n polly help Show this help\n\nOptions:\n --prod Build for production (minified)\n --config <path> Path to config file (default: polly.config.ts)\n --fix Auto-fix lint/format issues\n --type=TYPE Project type for init command (pwa, extension, websocket, generic)`);\n}\n\n/**\n * Main entry point\n */\nasync function main() {\n try {\n switch (command) {\n case \"init\":\n await init();\n break;\n case \"check\":\n await check();\n break;\n case \"build\":\n await build();\n break;\n case \"dev\":\n await dev();\n break;\n case \"typecheck\":\n await typecheck();\n break;\n case \"lint\":\n await lint();\n break;\n case \"format\":\n await format();\n break;\n case \"test\":\n await test();\n break;\n case \"verify\":\n await verify();\n break;\n case \"visualize\":\n await visualize();\n break;\n case \"quality\":\n await quality();\n break;\n case \"help\":\n case \"--help\":\n case \"-h\":\n case undefined:\n help();\n break;\n default:\n console.log(`❌ Unknown command: ${command}\\n`);\n help();\n process.exit(1);\n }\n } catch (error) {\n console.log(\"\\n❌ Command failed:\", error);\n process.exit(1);\n }\n}\n\nmain();\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "B9B6663515E3E39364756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|