claude-toolkit 0.1.20 → 0.1.24

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.
@@ -0,0 +1,75 @@
1
+ # Live Updates & OTA Strategy
2
+
3
+ > OTA is a delivery mechanism for the **web layer**, not a way around the app stores. The strategy is mostly about discipline: knowing what you can ship over the air, keeping bundles small, and rolling out so a mistake is recoverable.
4
+
5
+ ## What You Can — and Cannot — Ship Over the Air
6
+
7
+ | Change | OTA? | Why |
8
+ | ----------------------------------------------- | ---- | ----------------------------------------------------------- |
9
+ | Bug fix in JS/TS | ✅ | Pure web layer |
10
+ | Copy, translations, styling | ✅ | Static assets in the bundle |
11
+ | New feature built on existing native APIs | ✅ | No new native contract |
12
+ | Web-only dependency upgrade | ✅ | Compiles into the bundle |
13
+ | New Capacitor **plugin** (native code) | ❌ | Native shell lacks the implementation → crash |
14
+ | Native dependency / SDK upgrade | ❌ | Native binary change |
15
+ | Capacitor **major** version bump | ❌ | Native runtime change |
16
+ | Permission / entitlement / manifest change | ❌ | Declared in the native binary |
17
+
18
+ The failure mode is concrete: a bundle that calls `SomePlugin.doThing()` on a shell where that plugin isn't compiled in throws at runtime. **Match every bundle to the native version that can run it.**
19
+
20
+ ## Store Compliance
21
+
22
+ OTA of interpreted code is explicitly permitted:
23
+
24
+ - **Apple** — Developer Agreement **§3.3.2** allows OTA of JavaScript and assets executed by the system WebView (since iOS 4.3).
25
+ - **Google Play** — the restriction on non-Play code updates **does not apply** to code running in a VM with limited API access, i.e. JavaScript in a WebView.
26
+
27
+ What gets apps pulled is shipping *native* executable code OTA. Stay in the web layer and you stay compliant.
28
+
29
+ ## Performance: Keep Bundles Small
30
+
31
+ Smaller bundles download faster, fail less, and adopt quicker.
32
+
33
+ - **Delta / differential updates.** Capgo transfers only the files that changed between versions (auto-enabled with direct/instant updates). The win depends on file-level stability — see below.
34
+ - **Deterministic builds.** Strip build timestamps and pin your build tooling so unchanged source produces byte-identical output. Otherwise every file looks "changed" and the delta degrades to a full download.
35
+ - **Compression.** Capgo applies ZSTD compression to full-image updates; you don't configure it, but deterministic builds are what let delta + compression actually shrink the payload.
36
+ - **Prioritize the critical path.** Load auth and main navigation eagerly; lazy-load analytics, settings, animations, and heavy media after first paint. A small critical bundle reaches `notifyAppReady()` faster, which matters because of the rollback timeout.
37
+
38
+ ## Update Timing & Scheduling
39
+
40
+ - **Off-peak rollouts.** Schedule heavy rollouts for **1–4 AM local time** and prefer Wi-Fi so you're not competing with active use or burning cellular data.
41
+ - **Background application.** The default `atBackground` mode downloads silently and applies when the app backgrounds, so users never watch a spinner. Reserve `onLaunch` / `always` for updates that must take effect immediately (e.g. a hotfix).
42
+ - **Respect `periodCheckDelay`.** Minimum 600s (10 min). Don't try to poll faster — it's clamped.
43
+
44
+ ## Roll Out So You Can Roll Back
45
+
46
+ The whole point of OTA is that mistakes are cheap to undo. Treat every release as staged:
47
+
48
+ 1. **Canary** to ~10% of users.
49
+ 2. **Watch** the success signal (`appReady` vs `downloadFailed`, Capgo stats) for 1–2 hours.
50
+ 3. **Widen** to 100% over ~24h if healthy.
51
+ 4. **Roll back instantly** from the channel dashboard if not — devices pick up the previous bundle on next check-in.
52
+
53
+ Healthy benchmarks: **~95% adoption within 24h**, **~82% global success rate**. Below that, investigate before widening.
54
+
55
+ ## The notifyAppReady() Safety Net
56
+
57
+ This is what makes the above safe. The plugin starts a timer (`appReadyTimeout`, default 10s) when a new bundle loads. If your JS calls `notifyAppReady()` before it fires, the bundle is "confirmed." If it doesn't — because the bundle white-screened or threw on boot — the plugin **automatically reverts** to the last good bundle. A broken OTA release self-heals on the next launch even before you notice.
58
+
59
+ Call it as early as possible, before heavy async work, so a slow boot doesn't trip the timeout on an otherwise-fine bundle.
60
+
61
+ ## Anti-Patterns
62
+
63
+ | Anti-pattern | Consequence |
64
+ | ------------------------------------------- | ----------------------------------------------------------------- |
65
+ | OTA-ing a native change | Runtime crash on the mismatched shell |
66
+ | Non-deterministic builds | Delta updates degrade to full downloads |
67
+ | 100% rollout, no canary | A bad bundle hits the entire user base at once |
68
+ | Eager-loading everything | Slow boot risks tripping `appReadyTimeout` → false rollback |
69
+ | Polling faster than 600s | Silently clamped; wasted assumption |
70
+
71
+ ## See Also
72
+
73
+ - [Capacitor 8 Platform](capacitor-8.md) — keeping native and OTA in lockstep
74
+ - [Capgo Setup & API](capgo-setup.md) — config and the manual update lifecycle
75
+ - [Channels & Staged Rollouts](channels-and-rollouts.md) — the mechanics of canarying and rollback
@@ -0,0 +1,68 @@
1
+ # Security & Encryption
2
+
3
+ > An OTA bundle travels over the network and lands on devices you don't control. End-to-end encryption with code signing makes each update unreadable in transit and verifiable on arrival — only your users can decrypt it, and only your signed bundles are accepted.
4
+
5
+ ## Why Encrypt
6
+
7
+ A channel controls *who is eligible* for an update; it does **not** keep the bundle secret. An unencrypted bundle should be treated as a public asset. If your web layer contains anything you don't want readable — proprietary logic, embedded config — encryption is the only thing that keeps it private end to end, including from Capgo itself.
8
+
9
+ ## How v2 Encryption Works (Hybrid RSA + AES)
10
+
11
+ RSA can't efficiently encrypt large payloads, so Capgo uses a hybrid scheme:
12
+
13
+ 1. A **random AES-256 key** is generated for every upload and encrypts the bundle.
14
+ 2. Your **private RSA-2048 key** encrypts (signs) the AES key and the bundle checksum.
15
+ 3. The app holds your **public RSA key** (injected at build) and decrypts the AES key + signature.
16
+ 4. The app decrypts the bundle with the AES key, recomputes the checksum, and compares it against the decrypted signature.
17
+
18
+ The result: the bundle is confidential (AES) **and** authenticated (RSA signature over the checksum). Tampering breaks the checksum match; a bundle not signed by your private key won't validate.
19
+
20
+ > Industry baseline this matches: **AES-256** for payloads, **RSA-2048** for key exchange, **SHA-256** for integrity.
21
+
22
+ ## Setup
23
+
24
+ ```bash
25
+ npx @capgo/cli@latest key create # generate the RSA key pair
26
+ npx @capgo/cli@latest bundle upload --key-v2 --channel=production
27
+ ```
28
+
29
+ `key create` writes two files and injects the public key into your Capacitor config:
30
+
31
+ | File | Sensitivity | Rule |
32
+ | --------------------- | ----------- | ---------------------------------------------------------- |
33
+ | `.capgo_key_v2` | **Private** | **Never commit.** Store as a CI secret. Loss = can't sign. |
34
+ | `.capgo_key_v2.pub` | Public | Safe to commit — it's a backup of the embedded public key. |
35
+
36
+ Add `.capgo_key_v2` to `.gitignore` immediately. The private key only ever lives on trusted dev machines and in CI secret storage; it's used at upload time to sign, never shipped to devices.
37
+
38
+ ## Strict Enforcement
39
+
40
+ Once encryption is configured, the plugin **requires** a valid signature on every update — this is enforced, not advisory. An unsigned or tampered bundle is **rejected**, not applied. Practical consequences:
41
+
42
+ - Every CI upload must run with `--key-v2` and access to the private key, or devices will reject the bundle.
43
+ - Rotating keys requires shipping a new native binary with the new public key; plan it as a store release, not an OTA.
44
+ - `--no-key` (unencrypted) and encrypted uploads don't mix on the same channel for the same audience — pick one posture per channel.
45
+
46
+ ## Integrity Beyond Encryption
47
+
48
+ - **Checksums** verify the decrypted bundle matches what you signed — corruption or tampering fails closed.
49
+ - **`notifyAppReady()`** is the runtime backstop: even a validly-signed bundle that crashes on boot rolls back automatically (see [Capgo Setup](capgo-setup.md)). Signing proves *authenticity*; `notifyAppReady` proves *it actually runs*. You want both.
50
+
51
+ ## Store Compliance
52
+
53
+ Encryption doesn't change what you're allowed to ship — it just protects the payload. The compliance rule is unchanged: OTA of **JavaScript/HTML/CSS** is permitted (Apple Developer Agreement §3.3.2; Google Play's webview/JS-VM exemption). Encrypting that web bundle is fine. Encrypting and shipping *native* executable code OTA is still not allowed — encryption is not a loophole around the native/web boundary.
54
+
55
+ ## Checklist
56
+
57
+ - [ ] `npx @capgo/cli key create` run; `.capgo_key_v2` added to `.gitignore`.
58
+ - [ ] Private key stored as a CI secret; `--key-v2` used on every production upload.
59
+ - [ ] `.capgo_key_v2.pub` committed as a backup.
60
+ - [ ] `notifyAppReady()` present so signed-but-broken bundles still self-heal.
61
+ - [ ] Key rotation planned as a native binary release, not an OTA.
62
+ - [ ] Only web-layer code shipped OTA — never native code, encrypted or not.
63
+
64
+ ## See Also
65
+
66
+ - [Capgo Setup & API](capgo-setup.md) — the upload flow and `publicKey` config
67
+ - [Channels & Staged Rollouts](channels-and-rollouts.md) — per-channel encryption posture
68
+ - [Live Updates & OTA Strategy](live-updates-ota.md) — what's legal to ship over the air
@@ -0,0 +1,145 @@
1
+ # Capacitor OTA (Capgo) Patterns
2
+
3
+ > Capacitor 8 native runtime and Capgo over-the-air live updates: channels, encryption, staged rollouts, and store-safe delivery.
4
+
5
+ **Type:** Stack Skill (requires `capacitor` stack)
6
+ **Source:** [`stacks/capacitor/skills/ct-capacitor-ota/SKILL.md`](../../stacks/capacitor/skills/ct-capacitor-ota/SKILL.md)
7
+ **Directory Mappings:** `capacitor.config.ts`, `capacitor.config.json`, `ios/`, `android/`
8
+ **File Extensions:** _(none — keyed on config + content)_
9
+
10
+ ## Overview
11
+
12
+ Capacitor wraps a web app in a native iOS/Android shell. Capgo's `@capgo/capacitor-updater` lets you replace the web bundle that shell loads, shipping JS/HTML/CSS fixes in minutes instead of waiting on store review.
13
+
14
+ **Golden rule:** OTA updates the **web layer only**. Native code changes (new plugin, native dependency, Capacitor major bump) require a new store binary. Never OTA a bundle whose native contract differs from the installed shell.
15
+
16
+ ## Versions (as of June 2026)
17
+
18
+ | Package | Version | Notes |
19
+ | -------------------------- | ------- | ---------------------------------------------------------------- |
20
+ | `@capacitor/core` & CLI | ^8.3.x | 8.3.1 (2026-04-16); Node 22+, SPM default for new iOS apps |
21
+ | `@capgo/capacitor-updater` | ^8.x | Major version tracks Capacitor major; v8 stores channels locally |
22
+ | `@capgo/cli` | latest | `npx @capgo/cli@latest …` — pin in CI |
23
+
24
+ ## Setup
25
+
26
+ ```bash
27
+ npm i @capgo/capacitor-updater && npx cap sync
28
+ npx @capgo/cli@latest init <API_KEY> # adds app, injects notifyAppReady, builds, uploads, tests
29
+ ```
30
+
31
+ ## Critical: notifyAppReady()
32
+
33
+ Call it as early as possible after the bundle boots. If the plugin doesn't hear it within `appReadyTimeout` (default 10s), it assumes the bundle is broken and **auto-rolls back** to the previous one — the safety net that makes OTA reversible.
34
+
35
+ ```typescript
36
+ import { CapacitorUpdater } from "@capgo/capacitor-updater";
37
+
38
+ CapacitorUpdater.notifyAppReady(); // top of bootstrap, before heavy async work
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ ```typescript
44
+ // capacitor.config.ts
45
+ import type { CapacitorConfig } from "@capacitor/cli";
46
+
47
+ const config: CapacitorConfig = {
48
+ appId: "com.example.app",
49
+ appName: "Example",
50
+ webDir: "dist",
51
+ plugins: {
52
+ CapacitorUpdater: {
53
+ autoUpdate: "atBackground", // apply when app backgrounds
54
+ appReadyTimeout: 10000,
55
+ autoDeleteFailed: true,
56
+ autoDeletePrevious: true,
57
+ resetWhenUpdate: true, // wipe OTA bundles on native app update
58
+ // defaultChannel: "Development", // TEST/QA BUILDS ONLY — omit in production
59
+ },
60
+ },
61
+ };
62
+
63
+ export default config;
64
+ ```
65
+
66
+ ### autoUpdate modes
67
+
68
+ | Value | Behavior |
69
+ | ---------------------------- | ------------------------------------------------------------ |
70
+ | `false` / `"off"` | No automatic updates — drive everything from JS |
71
+ | `"atBackground"` *(default)* | Download silently, apply when app moves to background |
72
+ | `"atInstall"` | Apply right after a fresh install / native update |
73
+ | `"onLaunch"` | Apply on next launch, then background |
74
+ | `"always"` | Apply immediately whenever a check runs |
75
+ | `"onlyDownload"` | Download only; emit `updateAvailable` for you to act |
76
+
77
+ `periodCheckDelay` sets poll frequency (seconds, **minimum 600** / 10 min).
78
+
79
+ ## Channels
80
+
81
+ A channel points to one JS bundle. Swap which bundle a channel points to for instant rollout/rollback without rebuilding.
82
+
83
+ ```bash
84
+ npx @capgo/cli@latest bundle upload --channel=production # build → upload → assign
85
+ npx @capgo/cli@latest channel set production -s default # make it the cloud default
86
+ npx @capgo/cli@latest channel set beta --self-assign # allow in-app setChannel()
87
+ ```
88
+
89
+ **Precedence** (highest first): forced device mapping → cloud per-device override → config `defaultChannel` → cloud Default Channel.
90
+
91
+ Ship production binaries **without** `defaultChannel` so users follow the cloud default. Recommended ladder:
92
+
93
+ | Channel | Bundle version | Audience |
94
+ | ------------- | -------------- | ------------------------- |
95
+ | `development` | `1.2.3-dev.1` | dev / emulator builds |
96
+ | `qa` | `1.2.3-qa.1` | QA verification |
97
+ | `staging` | `1.2.3-rc.1` | production-like sign-off |
98
+ | `production` | `1.2.3` | end users (store version) |
99
+
100
+ ```typescript
101
+ // Runtime switch — needs "Allow device self-assignment" on the channel (v8+).
102
+ await CapacitorUpdater.setChannel({ channel: "beta", triggerAutoUpdate: true });
103
+ ```
104
+
105
+ ## End-to-end encryption (v2)
106
+
107
+ Hybrid RSA-2048 + AES-256: a random AES key encrypts each bundle; your private RSA key signs the key + checksum; the app decrypts with the embedded public key.
108
+
109
+ ```bash
110
+ npx @capgo/cli@latest key create
111
+ npx @capgo/cli@latest bundle upload --key-v2 --channel=production
112
+ ```
113
+
114
+ - `.capgo_key_v2` (private) — **never commit**; store as a CI secret.
115
+ - `.capgo_key_v2.pub` (public) — safe to commit.
116
+ - With encryption configured, the plugin **strictly enforces** a valid signature; tampered/unsigned bundles are rejected.
117
+
118
+ ## Staged rollouts & monitoring
119
+
120
+ - Start at ~10% of users, widen over 24h; schedule heavy rollouts 1–4 AM local, Wi-Fi-preferred.
121
+ - Targets: ~95% adoption within 24h, ~82% global success rate.
122
+ - Roll back instantly from the channel dashboard (crown icon on the previous build).
123
+ - Watch the `downloadFailed` / `appReady` ratio in Capgo stats; a spike means roll back.
124
+
125
+ ## Store compliance
126
+
127
+ OTA of JS/HTML/CSS is permitted — Apple §3.3.2 (since iOS 4.3) and Google Play (webview/JS VM code is exempt). Shipping native code OTA is not.
128
+
129
+ ## Anti-Patterns
130
+
131
+ | Anti-pattern | Why it's wrong |
132
+ | ----------------------------------------- | ------------------------------------------------------------------------ |
133
+ | **Not calling `notifyAppReady()`** | Every bundle auto-rolls back after `appReadyTimeout`. The #1 OTA failure. |
134
+ | **OTA-ing a native change** | Bundle needs a native API the shell lacks → crash. Bump the binary first. |
135
+ | **`defaultChannel` in production builds** | Pins users off the cloud default; breaks instant rollback. |
136
+ | **Committing `.capgo_key_v2`** | Leaks your signing key. Commit only the `.pub`. |
137
+ | **`periodCheckDelay` < 600** | Silently clamped; never polls faster than 10 min. |
138
+ | **100% rollout with no canary** | A bad bundle hits everyone at once. Stage it. |
139
+ | **Unpinned `@capgo/cli@latest` in CI** | A CLI minor can change upload behavior mid-pipeline. Pin it. |
140
+
141
+ ## See Also
142
+
143
+ - [Capacitor & OTA Best Practices](../best-practices/capacitor/README.md) — full reference set
144
+ - [ct-vite-vitest-patterns](vite-vitest-patterns.md) — the build that produces `webDir`
145
+ - [ct-playwright-patterns](playwright-patterns.md) — E2E-verify a bundle before upload
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-toolkit",
3
- "version": "0.1.20",
3
+ "version": "0.1.24",
4
4
  "description": "Reusable Claude Code configuration toolkit with stack-specific connectors",
5
5
  "type": "module",
6
6
  "bin": {
package/src/detect.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { StackName } from "./types.js";
4
+
5
+ /** Result of detecting a stack in a project */
6
+ export interface DetectedStack {
7
+ name: StackName;
8
+ reason: string;
9
+ }
10
+
11
+ interface PackageJson {
12
+ dependencies?: Record<string, string>;
13
+ devDependencies?: Record<string, string>;
14
+ }
15
+
16
+ interface StackDetector {
17
+ name: StackName;
18
+ detect: (projectDir: string, pkg: PackageJson | null) => DetectedStack | null;
19
+ }
20
+
21
+ function loadPackageJson(projectDir: string): PackageJson | null {
22
+ const pkgPath = join(projectDir, "package.json");
23
+ if (!existsSync(pkgPath)) return null;
24
+ try {
25
+ return JSON.parse(readFileSync(pkgPath, "utf-8")) as PackageJson;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function hasDep(pkg: PackageJson | null, name: string): boolean {
32
+ if (!pkg) return false;
33
+ return name in (pkg.dependencies ?? {}) || name in (pkg.devDependencies ?? {});
34
+ }
35
+
36
+ function fileExists(projectDir: string, ...segments: string[]): boolean {
37
+ return existsSync(join(projectDir, ...segments));
38
+ }
39
+
40
+ function rootConfigExists(projectDir: string, prefix: string): string | null {
41
+ const glob = new Bun.Glob(`${prefix}.*`);
42
+ for (const match of glob.scanSync({ cwd: projectDir, onlyFiles: true })) {
43
+ return match;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ const DETECTORS: StackDetector[] = [
49
+ {
50
+ name: "solidjs",
51
+ detect: (_dir, pkg) =>
52
+ hasDep(pkg, "solid-js")
53
+ ? { name: "solidjs", reason: "found solid-js in dependencies" }
54
+ : null,
55
+ },
56
+ {
57
+ name: "vite",
58
+ detect: (dir, pkg) => {
59
+ if (hasDep(pkg, "vite")) return { name: "vite", reason: "found vite in dependencies" };
60
+ const viteConfig = rootConfigExists(dir, "vite.config");
61
+ if (viteConfig) return { name: "vite", reason: `found ${viteConfig}` };
62
+ const vitestConfig = rootConfigExists(dir, "vitest.config");
63
+ if (vitestConfig) return { name: "vite", reason: `found ${vitestConfig}` };
64
+ return null;
65
+ },
66
+ },
67
+ {
68
+ name: "vanilla-extract",
69
+ detect: (_dir, pkg) =>
70
+ hasDep(pkg, "@vanilla-extract/css")
71
+ ? { name: "vanilla-extract", reason: "found @vanilla-extract/css in dependencies" }
72
+ : null,
73
+ },
74
+ {
75
+ name: "rust-wasm",
76
+ detect: (dir) =>
77
+ fileExists(dir, "Cargo.toml") ? { name: "rust-wasm", reason: "found Cargo.toml" } : null,
78
+ },
79
+ {
80
+ name: "protobuf",
81
+ detect: (dir) => {
82
+ if (fileExists(dir, "buf.yaml")) return { name: "protobuf", reason: "found buf.yaml" };
83
+ if (fileExists(dir, "buf.gen.yaml"))
84
+ return { name: "protobuf", reason: "found buf.gen.yaml" };
85
+ const glob = new Bun.Glob("**/*.proto");
86
+ for (const _match of glob.scanSync({ cwd: dir, onlyFiles: true })) {
87
+ return { name: "protobuf", reason: "found .proto files" };
88
+ }
89
+ return null;
90
+ },
91
+ },
92
+ {
93
+ name: "cloudflare",
94
+ detect: (dir) => {
95
+ if (fileExists(dir, "wrangler.toml"))
96
+ return { name: "cloudflare", reason: "found wrangler.toml" };
97
+ if (fileExists(dir, "wrangler.jsonc"))
98
+ return { name: "cloudflare", reason: "found wrangler.jsonc" };
99
+ return null;
100
+ },
101
+ },
102
+ {
103
+ name: "i18n-typesafe",
104
+ detect: (_dir, pkg) =>
105
+ hasDep(pkg, "typesafe-i18n")
106
+ ? { name: "i18n-typesafe", reason: "found typesafe-i18n in dependencies" }
107
+ : null,
108
+ },
109
+ {
110
+ name: "playwright",
111
+ detect: (dir, pkg) => {
112
+ if (hasDep(pkg, "@playwright/test"))
113
+ return { name: "playwright", reason: "found @playwright/test in dependencies" };
114
+ const config = rootConfigExists(dir, "playwright.config");
115
+ if (config) return { name: "playwright", reason: `found ${config}` };
116
+ return null;
117
+ },
118
+ },
119
+ {
120
+ name: "storybook",
121
+ detect: (dir, pkg) => {
122
+ if (hasDep(pkg, "storybook"))
123
+ return { name: "storybook", reason: "found storybook in dependencies" };
124
+ if (fileExists(dir, ".storybook"))
125
+ return { name: "storybook", reason: "found .storybook/ directory" };
126
+ return null;
127
+ },
128
+ },
129
+ {
130
+ name: "capacitor",
131
+ detect: (dir, pkg) => {
132
+ if (hasDep(pkg, "@capgo/capacitor-updater"))
133
+ return { name: "capacitor", reason: "found @capgo/capacitor-updater in dependencies" };
134
+ if (hasDep(pkg, "@capacitor/core"))
135
+ return { name: "capacitor", reason: "found @capacitor/core in dependencies" };
136
+ const config = rootConfigExists(dir, "capacitor.config");
137
+ if (config) return { name: "capacitor", reason: `found ${config}` };
138
+ return null;
139
+ },
140
+ },
141
+ ];
142
+
143
+ /** Scan a project directory and detect which stacks are present */
144
+ export function detectStacks(projectDir: string): DetectedStack[] {
145
+ const pkg = loadPackageJson(projectDir);
146
+ const detected: DetectedStack[] = [];
147
+ for (const detector of DETECTORS) {
148
+ const result = detector.detect(projectDir, pkg);
149
+ if (result) detected.push(result);
150
+ }
151
+ return detected;
152
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export type { DetectedStack } from "./detect.js";
2
+ export { detectStacks } from "./detect.js";
1
3
  export type {
2
4
  ClaudeToolkitConfig,
3
5
  GitConfig,
package/src/types.ts CHANGED
@@ -6,6 +6,10 @@ export type StackName =
6
6
  | "protobuf"
7
7
  | "cloudflare"
8
8
  | "i18n-typesafe"
9
+ | "vite"
10
+ | "playwright"
11
+ | "storybook"
12
+ | "capacitor"
9
13
  | (string & {});
10
14
 
11
15
  /** Hook configuration for post-tool-use automation */
@@ -24,7 +28,7 @@ export interface HookConfig {
24
28
 
25
29
  /** Git workflow configuration */
26
30
  export interface GitConfig {
27
- /** Branch name prefix (e.g., "wq" → "wq/feature-name") */
31
+ /** Branch name prefix (e.g., "feat" → "feat/feature-name") */
28
32
  branchPrefix?: string;
29
33
  /** Branches protected from direct edits */
30
34
  protectedBranches?: string[];
@@ -0,0 +1,178 @@
1
+ ---
2
+ name: ct-capacitor-ota
3
+ description: Capacitor 8 native runtime and Capgo OTA live updates — channels, encryption, staged rollouts, and store-safe over-the-air delivery
4
+ ---
5
+
6
+ # Capacitor OTA (Capgo) Patterns
7
+
8
+ Ship JavaScript, HTML, and CSS changes to installed apps in minutes instead of waiting days for app store review. Capgo's `@capgo/capacitor-updater` swaps the web bundle the native shell loads; native code still ships through the stores.
9
+
10
+ **Golden rule:** OTA updates only the **web layer**. Anything that changes native code (new plugin, native dependency, Capacitor major bump) requires a new store binary. Never OTA a bundle whose native contract differs from the installed shell.
11
+
12
+ ## Versions (as of June 2026)
13
+
14
+ | Package | Version | Notes |
15
+ | -------------------------- | -------- | ----------------------------------------------------------- |
16
+ | `@capacitor/core` & CLI | ^8.3.x | 8.3.1 (2026-04-16); Node 22+, SPM default for new iOS apps |
17
+ | `@capgo/capacitor-updater` | ^8.x | Major version tracks Capacitor major; v8 stores channels locally |
18
+ | `@capgo/cli` | latest | `npx @capgo/cli@latest …` — pin in CI |
19
+
20
+ Capacitor 8 platform floor: Node 22, iOS deployment target 15.0 (Xcode 26+), Android `minSdk 24` / `compileSdk 36` / `targetSdk 36`, Gradle plugin 8.13.0, wrapper 8.14.3, Kotlin 2.2.20.
21
+
22
+ ## Setup
23
+
24
+ ```bash
25
+ npm i @capgo/capacitor-updater && npx cap sync
26
+ npx @capgo/cli@latest init <API_KEY> # adds app, injects notifyAppReady, builds, uploads, tests
27
+ ```
28
+
29
+ ## Critical: notifyAppReady()
30
+
31
+ Call it **as early as possible** after the web bundle boots. If the plugin doesn't hear it within `appReadyTimeout` (default 10s), it assumes the bundle is broken and **auto-rolls back** to the previous one.
32
+
33
+ ```typescript
34
+ import { CapacitorUpdater } from "@capgo/capacitor-updater";
35
+
36
+ // Run once, at the top of app bootstrap — before heavy async work.
37
+ CapacitorUpdater.notifyAppReady();
38
+ ```
39
+
40
+ This is the safety net that makes OTA reversible. A bundle that white-screens never sticks.
41
+
42
+ ## Configuration
43
+
44
+ ```typescript
45
+ // capacitor.config.ts
46
+ import type { CapacitorConfig } from "@capacitor/cli";
47
+
48
+ const config: CapacitorConfig = {
49
+ appId: "com.example.app",
50
+ appName: "Example",
51
+ webDir: "dist",
52
+ plugins: {
53
+ CapacitorUpdater: {
54
+ autoUpdate: "atBackground", // default — apply when app backgrounds
55
+ appReadyTimeout: 10000,
56
+ responseTimeout: 20,
57
+ autoDeleteFailed: true,
58
+ autoDeletePrevious: true,
59
+ resetWhenUpdate: true, // wipe OTA bundles on native app update
60
+ // defaultChannel: "Development", // TEST/QA BUILDS ONLY — omit in production
61
+ },
62
+ },
63
+ };
64
+
65
+ export default config;
66
+ ```
67
+
68
+ ### autoUpdate modes
69
+
70
+ | Value | Behavior |
71
+ | --------------------------- | -------------------------------------------------------------------- |
72
+ | `false` / `"off"` | No automatic updates — drive everything from JS |
73
+ | `"atBackground"` *(default)* | Download silently, apply when app moves to background |
74
+ | `"atInstall"` | Apply immediately after a fresh install / native update, then background |
75
+ | `"onLaunch"` | Apply immediately on next launch, then background |
76
+ | `"always"` | Apply immediately whenever an update check runs |
77
+ | `"onlyDownload"` | Download but never auto-apply; emit `updateAvailable` for you to act |
78
+
79
+ `periodCheckDelay` controls how often the plugin polls (seconds, **minimum 600** / 10 min).
80
+
81
+ ## Channels
82
+
83
+ A channel points to one JS bundle. Devices check their assigned channel; you swap which bundle a channel points to for instant rollout/rollback — no rebuild.
84
+
85
+ ```bash
86
+ npx @capgo/cli@latest bundle upload --channel=production # build → upload → assign
87
+ npx @capgo/cli@latest channel set production -s default # make it the cloud default
88
+ npx @capgo/cli@latest channel set beta --self-assign # allow in-app setChannel()
89
+ ```
90
+
91
+ **Channel precedence** (highest first): forced device mapping → cloud per-device override → config `defaultChannel` → cloud Default Channel (the ~99% path).
92
+
93
+ **Production rule:** ship production binaries **without** `defaultChannel` so users follow the cloud default. Only set `defaultChannel` in builds explicitly handed to testers.
94
+
95
+ Recommended ladder with semver pre-release tags:
96
+
97
+ | Channel | Bundle version | Audience |
98
+ | ------------- | --------------- | ------------------------- |
99
+ | `development` | `1.2.3-dev.1` | dev / emulator builds |
100
+ | `qa` | `1.2.3-qa.1` | QA verification |
101
+ | `staging` | `1.2.3-rc.1` | production-like sign-off |
102
+ | `production` | `1.2.3` | end users (store version) |
103
+
104
+ ### Runtime channel switching
105
+
106
+ ```typescript
107
+ // Requires "Allow device self-assignment" enabled on the channel (plugin v8+).
108
+ await CapacitorUpdater.setChannel({ channel: "beta", triggerAutoUpdate: true });
109
+ ```
110
+
111
+ In v8 `setChannel` stores the choice **locally** and takes effect on the next check (no replication lag). The backend still validates self-assignment permission.
112
+
113
+ ## Manual / background update flow
114
+
115
+ For full control, set `autoUpdate: false` and drive the lifecycle yourself:
116
+
117
+ ```typescript
118
+ import { App } from "@capacitor/app";
119
+ import { SplashScreen } from "@capacitor/splash-screen";
120
+
121
+ CapacitorUpdater.addListener("download", ({ percent }) => console.log(`↓ ${percent}%`));
122
+
123
+ App.addListener("appStateChange", async ({ isActive }) => {
124
+ if (isActive) {
125
+ const latest = await CapacitorUpdater.getLatest();
126
+ if (latest.url) {
127
+ const bundle = await CapacitorUpdater.download({ version: latest.version, url: latest.url });
128
+ await CapacitorUpdater.next({ id: bundle.id }); // apply on next background
129
+ }
130
+ }
131
+ });
132
+ ```
133
+
134
+ Key methods: `getLatest()`, `download()`, `next()` (queue), `set()` (apply now — destroys JS context), `current()`, `reload()`, `reset()`. Events: `download`, `downloadComplete`, `updateAvailable`, `downloadFailed`, `appReady`, `majorAvailable`.
135
+
136
+ ## End-to-end encryption (v2)
137
+
138
+ Hybrid RSA-2048 + AES-256: a random AES key encrypts each bundle; your **private** RSA key signs the AES key + checksum; the app decrypts with the embedded **public** key. Only your users can read the bundle — not even Capgo.
139
+
140
+ ```bash
141
+ npx @capgo/cli@latest key create # generates the key pair
142
+ npx @capgo/cli@latest bundle upload --key-v2 --channel=production
143
+ ```
144
+
145
+ - `.capgo_key_v2` (private) — **never commit**; store as a CI secret.
146
+ - `.capgo_key_v2.pub` (public) — safe to commit; injected into the app.
147
+ - Once encryption is configured, the plugin **strictly enforces** a valid signature — an unsigned/tampered bundle is rejected.
148
+
149
+ ## Delta updates
150
+
151
+ Direct/instant updates auto-enable delta updates — only changed files transfer, not the whole bundle. Keep deltas small: pin build tooling and strip build timestamps so unchanged files hash identically. Use `--delta-only` to require delta-capable clients.
152
+
153
+ ## Staged rollouts & monitoring
154
+
155
+ - Start at ~10% of users, widen over 24h; schedule heavy rollouts for 1–4 AM local time, Wi-Fi-preferred.
156
+ - Healthy targets: ~95% adoption within 24h, ~82% global success rate.
157
+ - Roll back instantly from the channel dashboard (crown icon on the previous build) — devices pick it up on next check-in.
158
+ - Watch the `downloadFailed` / `appReady` ratio and Capgo stats; a spike means rollback.
159
+
160
+ ## Store compliance
161
+
162
+ OTA of JS/HTML/CSS is allowed: **Apple** developer agreement §3.3.2 (since iOS 4.3) and **Google Play** (code in a webview/JS VM is exempt from the non-Play update restriction). Shipping *native* code OTA is not — that's what gets apps pulled.
163
+
164
+ ## Anti-Patterns
165
+
166
+ 1. **Not calling `notifyAppReady()`** — every bundle auto-rolls back after `appReadyTimeout`. This is the #1 "OTA doesn't work" cause.
167
+ 2. **OTA-ing a native change** — bundle that needs a plugin/native API the installed shell lacks crashes. Match the bundle to the shipped native version; bump the store binary first.
168
+ 3. **`defaultChannel` in production builds** — pins users off the cloud default and breaks instant rollback. Test builds only.
169
+ 4. **Committing `.capgo_key_v2`** — leaks your signing key. Commit only the `.pub`.
170
+ 5. **`periodCheckDelay` < 600** — silently clamped; the plugin won't poll faster than 10 minutes.
171
+ 6. **100% rollout with no canary** — a bad bundle hits everyone at once. Stage it.
172
+ 7. **Unpinned `@capgo/cli@latest` in CI** — a CLI minor can change upload behavior mid-pipeline. Pin the version.
173
+
174
+ ## See Also
175
+
176
+ - `ct-vite-vitest-patterns` — the build that produces `webDir`
177
+ - `ct-playwright-patterns` — E2E-verify a bundle before you upload it
178
+ - `ct-typescript-conventions` — typing `capacitor.config.ts` and the updater API