jettypod 4.4.118 → 4.4.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Tauri Build Script
5
+ *
6
+ * Orchestrates the Tauri release build:
7
+ * 1. Validates prerequisites (cargo-tauri, signing keys)
8
+ * 2. Builds the Vite frontend
9
+ * 3. Runs `cargo tauri build` with code signing
10
+ * 4. Optionally uploads to R2
11
+ *
12
+ * Usage:
13
+ * node scripts/tauri-build.js [--release] [--upload] [--target arm64|x64|universal]
14
+ *
15
+ * Environment variables:
16
+ * APPLE_SIGNING_IDENTITY - Code signing identity (default: "-" for ad-hoc)
17
+ * APPLE_TEAM_ID - Apple Developer Team ID (enables notarization)
18
+ * APPLE_ID - Apple ID email (for notarization)
19
+ * APPLE_PASSWORD - App-specific password (for notarization)
20
+ * TAURI_SIGNING_PRIVATE_KEY - Ed25519 private key for update signing
21
+ * TAURI_SIGNING_PRIVATE_KEY_PASSWORD - Password for the signing key
22
+ */
23
+
24
+ const { execSync, spawnSync } = require('child_process');
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ const ROOT = path.resolve(__dirname, '..');
29
+ const TAURI_DIR = path.join(ROOT, 'src-tauri');
30
+ const TAURI_CONF = path.join(TAURI_DIR, 'tauri.conf.json');
31
+ const TAURI_PROD_CONF = path.join(TAURI_DIR, 'tauri.prod.conf.json');
32
+ // Cargo uses the workspace-level target/ directory (repo root), not src-tauri/target/
33
+ const WORKSPACE_ROOT = path.resolve(ROOT, '..', '..');
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Argument parsing
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const args = process.argv.slice(2);
40
+ const isRelease = args.includes('--release');
41
+ const doUpload = args.includes('--upload');
42
+ const targetArg = args.find(a => a.startsWith('--target='));
43
+ const target = targetArg ? targetArg.split('=')[1] : null;
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Validation
47
+ // ---------------------------------------------------------------------------
48
+
49
+ function check(label, fn) {
50
+ process.stdout.write(` ${label}... `);
51
+ try {
52
+ const result = fn();
53
+ console.log('✅');
54
+ return result;
55
+ } catch (e) {
56
+ console.log('❌');
57
+ console.error(` ${e.message}`);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ function validate() {
63
+ console.log('\n🔍 Validating prerequisites:\n');
64
+
65
+ check('cargo-tauri CLI', () => {
66
+ try {
67
+ execSync('cargo tauri --version 2>/dev/null', { stdio: 'pipe', env: { ...process.env, PATH: `${process.env.HOME}/.cargo/bin:${process.env.PATH}` } });
68
+ } catch {
69
+ throw new Error('cargo-tauri not found. Install with: cargo install tauri-cli');
70
+ }
71
+ });
72
+
73
+ check('Rust toolchain', () => {
74
+ try {
75
+ execSync('rustc --version 2>/dev/null', { stdio: 'pipe', env: { ...process.env, PATH: `${process.env.HOME}/.cargo/bin:${process.env.PATH}` } });
76
+ } catch {
77
+ throw new Error('Rust not found. Install from https://rustup.rs');
78
+ }
79
+ });
80
+
81
+ check('tauri.conf.json', () => {
82
+ if (!fs.existsSync(TAURI_CONF)) {
83
+ throw new Error(`Missing ${TAURI_CONF}`);
84
+ }
85
+ });
86
+
87
+ if (isRelease) {
88
+ check('Signing identity', () => {
89
+ const identity = process.env.APPLE_SIGNING_IDENTITY;
90
+ if (!identity || identity === '-') {
91
+ console.log('\n ⚠️ Using ad-hoc signing. Set APPLE_SIGNING_IDENTITY for production.');
92
+ }
93
+ });
94
+
95
+ if (process.env.APPLE_TEAM_ID) {
96
+ check('Notarization credentials', () => {
97
+ if (!process.env.APPLE_ID) throw new Error('APPLE_ID required for notarization');
98
+ if (!process.env.APPLE_PASSWORD) throw new Error('APPLE_PASSWORD required for notarization');
99
+ });
100
+ }
101
+
102
+ check('Update signing key', () => {
103
+ if (!process.env.TAURI_SIGNING_PRIVATE_KEY) {
104
+ console.log('\n ⚠️ No TAURI_SIGNING_PRIVATE_KEY set. Updates won\'t be signed.');
105
+ }
106
+ });
107
+ }
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Build steps
112
+ // ---------------------------------------------------------------------------
113
+
114
+ function runCommand(label, cmd, opts = {}) {
115
+ console.log(`\n📦 ${label}...`);
116
+ const env = {
117
+ ...process.env,
118
+ PATH: `${process.env.HOME}/.cargo/bin:${process.env.PATH}`,
119
+ };
120
+
121
+ const result = spawnSync('sh', ['-c', cmd], {
122
+ stdio: 'inherit',
123
+ cwd: opts.cwd || ROOT,
124
+ env,
125
+ });
126
+
127
+ if (result.status !== 0) {
128
+ console.error(`\n❌ ${label} failed (exit code ${result.status})`);
129
+ process.exit(1);
130
+ }
131
+ }
132
+
133
+ function updateSigningIdentity() {
134
+ // Pass signing identity via APPLE_SIGNING_IDENTITY env var.
135
+ // Tauri reads this automatically — no need to mutate tauri.conf.json.
136
+ const identity = process.env.APPLE_SIGNING_IDENTITY;
137
+ if (identity && identity !== '-') {
138
+ console.log(` Using signing identity: ${identity}`);
139
+ } else {
140
+ console.log(' Using ad-hoc signing (no APPLE_SIGNING_IDENTITY set)');
141
+ }
142
+ }
143
+
144
+ function buildTauri() {
145
+ let buildCmd = 'cargo tauri build';
146
+
147
+ if (target === 'universal') {
148
+ buildCmd += ' --target universal-apple-darwin';
149
+ } else if (target === 'arm64') {
150
+ buildCmd += ' --target aarch64-apple-darwin';
151
+ } else if (target === 'x64') {
152
+ buildCmd += ' --target x86_64-apple-darwin';
153
+ }
154
+
155
+ // Apply production config overlay for release builds
156
+ if (isRelease && fs.existsSync(TAURI_PROD_CONF)) {
157
+ buildCmd += ` --config "${TAURI_PROD_CONF}"`;
158
+ console.log(' Applying production config overlay (JettyPod name + clean icon)');
159
+ }
160
+
161
+ // Pass signing key via environment
162
+ const extraEnv = [];
163
+ if (process.env.TAURI_SIGNING_PRIVATE_KEY) {
164
+ extraEnv.push('TAURI_SIGNING_PRIVATE_KEY');
165
+ }
166
+ if (process.env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD) {
167
+ extraEnv.push('TAURI_SIGNING_PRIVATE_KEY_PASSWORD');
168
+ }
169
+
170
+ runCommand('Building Tauri app', buildCmd, { cwd: TAURI_DIR });
171
+ }
172
+
173
+ function showArtifacts() {
174
+ const targetDir = path.join(WORKSPACE_ROOT, 'target');
175
+ // When cross-compiling, bundles go under target/{triple}/release/bundle/
176
+ const candidates = [
177
+ path.join(targetDir, 'release', 'bundle'),
178
+ path.join(targetDir, 'x86_64-apple-darwin', 'release', 'bundle'),
179
+ path.join(targetDir, 'aarch64-apple-darwin', 'release', 'bundle'),
180
+ path.join(targetDir, 'universal-apple-darwin', 'release', 'bundle'),
181
+ ];
182
+ const bundleDir = candidates.find(d => fs.existsSync(d)) || path.join(targetDir, 'release', 'bundle');
183
+
184
+ console.log('\n✅ Build complete! Artifacts:');
185
+
186
+ // Check for DMG files
187
+ const dmgDir = path.join(bundleDir, 'dmg');
188
+ if (fs.existsSync(dmgDir)) {
189
+ const dmgs = fs.readdirSync(dmgDir).filter(f => f.endsWith('.dmg'));
190
+ dmgs.forEach(f => {
191
+ const size = (fs.statSync(path.join(dmgDir, f)).size / 1024 / 1024).toFixed(1);
192
+ console.log(` 📀 ${f} (${size} MB)`);
193
+ });
194
+ }
195
+
196
+ // Check for app bundle
197
+ const macosDir = path.join(bundleDir, 'macos');
198
+ if (fs.existsSync(macosDir)) {
199
+ const apps = fs.readdirSync(macosDir).filter(f => f.endsWith('.app'));
200
+ apps.forEach(f => {
201
+ console.log(` 📱 ${f}`);
202
+ });
203
+ }
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Main
208
+ // ---------------------------------------------------------------------------
209
+
210
+ console.log('🚀 JettyPod Tauri Build');
211
+ console.log(` Mode: ${isRelease ? 'RELEASE' : 'DEBUG'}`);
212
+ console.log(` Target: ${target || 'native'}`);
213
+ console.log(` Upload: ${doUpload ? 'yes' : 'no'}`);
214
+
215
+ validate();
216
+
217
+ if (isRelease) {
218
+ updateSigningIdentity();
219
+ }
220
+
221
+ buildTauri();
222
+ showArtifacts();
223
+
224
+ if (doUpload) {
225
+ runCommand('Uploading to R2', 'node scripts/upload-tauri-to-r2.js');
226
+ }
227
+
228
+ console.log('\n🎉 Done!\n');
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Upload Tauri release artifacts to Cloudflare R2.
5
+ * Run after tauri build: node scripts/upload-tauri-to-r2.js
6
+ *
7
+ * Requires wrangler login or CLOUDFLARE_API_TOKEN env var.
8
+ *
9
+ * Uploads DMG and update signature files to R2 under the
10
+ * tauri/ prefix, organized by target/arch for the updater endpoint.
11
+ */
12
+
13
+ const { execSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const BUCKET_NAME = 'jettypod-releases';
18
+ // Cargo uses the workspace-level target/ directory (repo root), not src-tauri/target/
19
+ const WORKSPACE_ROOT = path.resolve(__dirname, '..', '..', '..');
20
+ const TARGET_DIR = path.join(WORKSPACE_ROOT, 'target');
21
+
22
+ // Tauri puts release bundles under target/release/bundle/
23
+ // or target/{triple}/release/bundle/ for cross-compilation
24
+ function findBundleDir() {
25
+ const candidates = [
26
+ path.join(TARGET_DIR, 'release', 'bundle'),
27
+ path.join(TARGET_DIR, 'aarch64-apple-darwin', 'release', 'bundle'),
28
+ path.join(TARGET_DIR, 'x86_64-apple-darwin', 'release', 'bundle'),
29
+ path.join(TARGET_DIR, 'universal-apple-darwin', 'release', 'bundle'),
30
+ ];
31
+
32
+ for (const dir of candidates) {
33
+ if (fs.existsSync(dir)) return dir;
34
+ }
35
+
36
+ return null;
37
+ }
38
+
39
+ function findArtifacts(bundleDir) {
40
+ const artifacts = [];
41
+
42
+ // DMG files
43
+ const dmgDir = path.join(bundleDir, 'dmg');
44
+ if (fs.existsSync(dmgDir)) {
45
+ for (const f of fs.readdirSync(dmgDir)) {
46
+ if (f.endsWith('.dmg')) {
47
+ artifacts.push({ name: f, path: path.join(dmgDir, f), type: 'dmg' });
48
+ }
49
+ }
50
+ }
51
+
52
+ // Update signature files (.tar.gz and .sig for Tauri updater)
53
+ const macosDir = path.join(bundleDir, 'macos');
54
+ if (fs.existsSync(macosDir)) {
55
+ for (const f of fs.readdirSync(macosDir)) {
56
+ if (f.endsWith('.tar.gz') || f.endsWith('.tar.gz.sig')) {
57
+ artifacts.push({ name: f, path: path.join(macosDir, f), type: 'update' });
58
+ }
59
+ }
60
+ }
61
+
62
+ return artifacts;
63
+ }
64
+
65
+ function uploadFile(artifact, r2Key) {
66
+ const stats = fs.statSync(artifact.path);
67
+ const sizeMB = (stats.size / (1024 * 1024)).toFixed(1);
68
+
69
+ process.stdout.write(` ${r2Key} (${sizeMB} MB)... `);
70
+
71
+ try {
72
+ execSync(
73
+ `npx wrangler r2 object put "${BUCKET_NAME}/${r2Key}" --file="${artifact.path}" --remote`,
74
+ { stdio: 'pipe' }
75
+ );
76
+ console.log('✅');
77
+ return true;
78
+ } catch (error) {
79
+ console.log('❌');
80
+ console.error(` ${error.message}`);
81
+ return false;
82
+ }
83
+ }
84
+
85
+ function main() {
86
+ console.log('🚀 Uploading Tauri release artifacts to R2...\n');
87
+
88
+ const bundleDir = findBundleDir();
89
+ if (!bundleDir) {
90
+ console.error('❌ No Tauri bundle directory found.');
91
+ console.error('Run `cargo tauri build` first.');
92
+ process.exit(1);
93
+ }
94
+
95
+ console.log(`Bundle dir: ${bundleDir}\n`);
96
+
97
+ const artifacts = findArtifacts(bundleDir);
98
+ if (artifacts.length === 0) {
99
+ console.error('❌ No release artifacts found.');
100
+ process.exit(1);
101
+ }
102
+
103
+ console.log(`Found ${artifacts.length} artifact(s):\n`);
104
+
105
+ let success = 0;
106
+ let failed = 0;
107
+
108
+ for (const artifact of artifacts) {
109
+ // Upload under tauri/ prefix for organization
110
+ const r2Key = `tauri/${artifact.name}`;
111
+ if (uploadFile(artifact, r2Key)) {
112
+ success++;
113
+ } else {
114
+ failed++;
115
+ }
116
+ }
117
+
118
+ console.log(`\n📦 Upload complete: ${success} uploaded, ${failed} failed`);
119
+
120
+ if (failed > 0) {
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ main();
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Standalone WebSocket server for browser dev mode.
5
+ *
6
+ * Polls the JettyPod SQLite database and cucumber-results.json for changes
7
+ * and broadcasts notifications to connected clients. This is the same
8
+ * behaviour embedded in electron/main.js, extracted so `npm run dev` can
9
+ * provide live updates without Electron running.
10
+ *
11
+ * Usage:
12
+ * node scripts/ws-server.js [project-path]
13
+ *
14
+ * project-path defaults to the repository root (two levels up from this script).
15
+ */
16
+
17
+ const { WebSocketServer } = require('ws');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const WS_PORT = 47808;
22
+ const DB_POLL_MS = 500;
23
+
24
+ const projectRoot = process.argv[2] || path.resolve(__dirname, '..', '..', '..');
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // State
28
+ // ---------------------------------------------------------------------------
29
+ const clients = new Set();
30
+ let dbPollInterval = null;
31
+ let testResultsPollInterval = null;
32
+ let lastDbMtimes = { db: null, wal: null };
33
+ let lastTestResultsMtime = null;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+ function log(msg) {
39
+ console.log(`[ws-server] ${msg}`);
40
+ }
41
+
42
+ function broadcast(message) {
43
+ const payload = JSON.stringify(message);
44
+ for (const client of clients) {
45
+ if (client.readyState === 1) { // WebSocket.OPEN
46
+ client.send(payload);
47
+ }
48
+ }
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Database polling (WAL-aware)
53
+ // ---------------------------------------------------------------------------
54
+ function startDatabasePolling() {
55
+ const dbPath = path.join(projectRoot, '.jettypod', 'work.db');
56
+
57
+ if (!fs.existsSync(dbPath)) {
58
+ log(`Database not found at ${dbPath}, retrying in 5s...`);
59
+ setTimeout(startDatabasePolling, 5000);
60
+ return;
61
+ }
62
+
63
+ try {
64
+ const dbStats = fs.statSync(dbPath);
65
+ lastDbMtimes.db = dbStats.mtimeMs;
66
+
67
+ const walPath = dbPath + '-wal';
68
+ if (fs.existsSync(walPath)) {
69
+ lastDbMtimes.wal = fs.statSync(walPath).mtimeMs;
70
+ }
71
+ } catch (err) {
72
+ log(`Failed to get initial db stats: ${err.message}`);
73
+ return;
74
+ }
75
+
76
+ log(`Polling database for changes (WAL-aware)...`);
77
+
78
+ dbPollInterval = setInterval(() => {
79
+ try {
80
+ let changed = false;
81
+
82
+ const dbStats = fs.statSync(dbPath);
83
+ if (dbStats.mtimeMs !== lastDbMtimes.db) {
84
+ lastDbMtimes.db = dbStats.mtimeMs;
85
+ changed = true;
86
+ }
87
+
88
+ const walPath = dbPath + '-wal';
89
+ if (fs.existsSync(walPath)) {
90
+ const walStats = fs.statSync(walPath);
91
+ if (walStats.mtimeMs !== lastDbMtimes.wal) {
92
+ lastDbMtimes.wal = walStats.mtimeMs;
93
+ changed = true;
94
+ }
95
+ }
96
+
97
+ if (changed) {
98
+ broadcast({ type: 'db_change', timestamp: Date.now() });
99
+ }
100
+ } catch {
101
+ // File might be temporarily locked during writes — ignore
102
+ }
103
+ }, DB_POLL_MS);
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Test results polling
108
+ // ---------------------------------------------------------------------------
109
+ function startTestResultsPolling() {
110
+ const testResultsPath = path.join(projectRoot, 'cucumber-results.json');
111
+
112
+ if (fs.existsSync(testResultsPath)) {
113
+ try {
114
+ lastTestResultsMtime = fs.statSync(testResultsPath).mtimeMs;
115
+ log(`Watching test results at ${testResultsPath}`);
116
+ } catch (err) {
117
+ log(`Failed to get initial test results stats: ${err.message}`);
118
+ }
119
+ } else {
120
+ log(`Test results file not found at ${testResultsPath}, will watch for creation`);
121
+ }
122
+
123
+ testResultsPollInterval = setInterval(() => {
124
+ try {
125
+ if (!fs.existsSync(testResultsPath)) {
126
+ if (lastTestResultsMtime !== null) {
127
+ lastTestResultsMtime = null;
128
+ }
129
+ return;
130
+ }
131
+
132
+ const stats = fs.statSync(testResultsPath);
133
+ if (stats.mtimeMs !== lastTestResultsMtime) {
134
+ lastTestResultsMtime = stats.mtimeMs;
135
+ broadcast({ type: 'test_change', timestamp: Date.now() });
136
+ }
137
+ } catch {
138
+ // ignore
139
+ }
140
+ }, DB_POLL_MS);
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Server
145
+ // ---------------------------------------------------------------------------
146
+ const wss = new WebSocketServer({ port: WS_PORT });
147
+
148
+ wss.on('connection', (ws) => {
149
+ clients.add(ws);
150
+ log(`Client connected. Total: ${clients.size}`);
151
+ ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
152
+
153
+ ws.on('close', () => {
154
+ clients.delete(ws);
155
+ log(`Client disconnected. Total: ${clients.size}`);
156
+ });
157
+
158
+ ws.on('error', (err) => {
159
+ log(`Client error: ${err.message}`);
160
+ ws.close();
161
+ clients.delete(ws);
162
+ });
163
+ });
164
+
165
+ wss.on('error', (err) => {
166
+ if (err.code === 'EADDRINUSE') {
167
+ log(`Port ${WS_PORT} already in use (Electron running?). Exiting.`);
168
+ process.exit(0);
169
+ }
170
+ log(`Server error: ${err.message}`);
171
+ });
172
+
173
+ wss.on('listening', () => {
174
+ log(`WebSocket server running on ws://localhost:${WS_PORT}`);
175
+ log(`Watching project: ${projectRoot}`);
176
+ startDatabasePolling();
177
+ startTestResultsPolling();
178
+ });
179
+
180
+ // Graceful shutdown
181
+ function shutdown() {
182
+ log('Shutting down...');
183
+ if (dbPollInterval) clearInterval(dbPollInterval);
184
+ if (testResultsPollInterval) clearInterval(testResultsPollInterval);
185
+ for (const client of clients) client.close();
186
+ clients.clear();
187
+ wss.close(() => process.exit(0));
188
+ }
189
+
190
+ process.on('SIGINT', shutdown);
191
+ process.on('SIGTERM', shutdown);
@@ -0,0 +1,12 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import { RouterProvider } from 'react-router-dom'
4
+ import { router } from './router'
5
+ import '@fontsource-variable/geist-mono'
6
+ import '../app/globals.css'
7
+
8
+ createRoot(document.getElementById('root')!).render(
9
+ <StrictMode>
10
+ <RouterProvider router={router} />
11
+ </StrictMode>
12
+ )
@@ -0,0 +1,107 @@
1
+ import { lazy, Suspense, useState, useEffect } from 'react'
2
+ import { createBrowserRouter, Outlet } from 'react-router-dom'
3
+ import { AppShell } from '../components/AppShell'
4
+ import { JettyLoader } from '../components/JettyLoader'
5
+ import { dataBridge } from '../lib/data-bridge'
6
+
7
+ // Lazy-load all pages — prefetch nav targets on app load so first navigation
8
+ // never shows a spinner. It's a desktop app; the extra chunks cost nothing.
9
+ const dashboardImport = () => import('../app/page')
10
+ const Dashboard = lazy(dashboardImport)
11
+ const workDetailImport = () => import('../app/work/[id]/page')
12
+ const WorkDetail = lazy(workDetailImport)
13
+ const proofDashboardImport = () => import('../app/work/[id]/proof/page')
14
+ const ProofDashboard = lazy(proofDashboardImport)
15
+ const decisionDetailImport = () => import('../app/decision/[id]/page')
16
+ const DecisionDetail = lazy(decisionDetailImport)
17
+ const testsImport = () => import('../app/tests/page')
18
+ const Tests = lazy(testsImport)
19
+ const settingsImport = () => import('../app/settings/page')
20
+ const Settings = lazy(settingsImport)
21
+ const prototypesImport = () => import('../app/prototypes/page')
22
+ const Prototypes = lazy(prototypesImport)
23
+
24
+ // Kick off chunk prefetches immediately
25
+ dashboardImport()
26
+ workDetailImport()
27
+ proofDashboardImport()
28
+ decisionDetailImport()
29
+ testsImport()
30
+ settingsImport()
31
+ prototypesImport()
32
+
33
+ const Welcome = lazy(() => import('../app/welcome/page'))
34
+ const Login = lazy(() => import('../app/login/page'))
35
+ const Signup = lazy(() => import('../app/signup/page'))
36
+ const Subscribe = lazy(() => import('../app/subscribe/page'))
37
+ const InstallClaude = lazy(() => import('../app/install-claude/page'))
38
+ const ConnectClaude = lazy(() => import('../app/connect-claude/page'))
39
+ // Dev-only pages — excluded from production to reduce bundle
40
+ const DesignSystem = import.meta.env.DEV ? lazy(() => import('../app/design-system/page')) : () => null
41
+ const DemoGates = import.meta.env.DEV ? lazy(() => import('../app/demo/gates/page')) : () => null
42
+ const DevSessionPrototype = import.meta.env.DEV ? lazy(() => import('../app/prototypes/dev-session/page')) : () => null
43
+ const ProofDashboardPrototype = import.meta.env.DEV ? lazy(() => import('../app/prototypes/proof-dashboard/page')) : () => null
44
+ const OnboardingPrototype = import.meta.env.DEV ? lazy(() => import('../app/prototypes/onboarding/page')) : () => null
45
+ const OnboardingMadlibPrototype = import.meta.env.DEV ? lazy(() => import('../app/prototypes/onboarding-madlib/page')) : () => null
46
+
47
+ function PageLoader() {
48
+ return (
49
+ <div className="h-full flex items-center justify-center bg-background">
50
+ <JettyLoader size={80} />
51
+ </div>
52
+ )
53
+ }
54
+
55
+ function RootLayout() {
56
+ const [projectName, setProjectName] = useState('JettyPod')
57
+
58
+ useEffect(() => {
59
+ dataBridge.getProjectRoot().then((root) => {
60
+ if (root) {
61
+ setProjectName(root.split('/').pop() || 'JettyPod')
62
+ }
63
+ })
64
+ }, [])
65
+
66
+ return (
67
+ <AppShell projectName={projectName}>
68
+ <Outlet />
69
+ </AppShell>
70
+ )
71
+ }
72
+
73
+ export const router = createBrowserRouter([
74
+ {
75
+ path: '/',
76
+ element: <RootLayout />,
77
+ children: [
78
+ {
79
+ // Dashboard layout route — kanban stays mounted while viewing work items.
80
+ // Navigating to /work/:id hides the kanban (display:none) instead of
81
+ // unmounting it, so returning to / is instant with no refetch.
82
+ element: <Suspense fallback={<PageLoader />}><Dashboard /></Suspense>,
83
+ children: [
84
+ { index: true, element: null },
85
+ { path: 'work/:id', element: <Suspense fallback={<PageLoader />}><WorkDetail /></Suspense> },
86
+ { path: 'work/:id/proof', element: <Suspense fallback={<PageLoader />}><ProofDashboard /></Suspense> },
87
+ { path: 'decision/:id', element: <Suspense fallback={<PageLoader />}><DecisionDetail /></Suspense> },
88
+ ],
89
+ },
90
+ { path: 'tests', element: <Suspense fallback={<PageLoader />}><Tests /></Suspense> },
91
+ { path: 'settings', element: <Suspense fallback={<PageLoader />}><Settings /></Suspense> },
92
+ { path: 'prototypes', element: <Suspense fallback={<PageLoader />}><Prototypes /></Suspense> },
93
+ { path: 'welcome', element: <Suspense fallback={<PageLoader />}><Welcome /></Suspense> },
94
+ { path: 'login', element: <Suspense fallback={<PageLoader />}><Login /></Suspense> },
95
+ { path: 'signup', element: <Suspense fallback={<PageLoader />}><Signup /></Suspense> },
96
+ { path: 'subscribe', element: <Suspense fallback={<PageLoader />}><Subscribe /></Suspense> },
97
+ { path: 'install-claude', element: <Suspense fallback={<PageLoader />}><InstallClaude /></Suspense> },
98
+ { path: 'connect-claude', element: <Suspense fallback={<PageLoader />}><ConnectClaude /></Suspense> },
99
+ { path: 'design-system', element: <Suspense fallback={<PageLoader />}><DesignSystem /></Suspense> },
100
+ { path: 'demo/gates', element: <Suspense fallback={<PageLoader />}><DemoGates /></Suspense> },
101
+ { path: 'prototypes/dev-session', element: <Suspense fallback={<PageLoader />}><DevSessionPrototype /></Suspense> },
102
+ { path: 'prototypes/proof-dashboard', element: <Suspense fallback={<PageLoader />}><ProofDashboardPrototype /></Suspense> },
103
+ { path: 'prototypes/onboarding', element: <Suspense fallback={<PageLoader />}><OnboardingPrototype /></Suspense> },
104
+ { path: 'prototypes/onboarding-madlib', element: <Suspense fallback={<PageLoader />}><OnboardingMadlibPrototype /></Suspense> },
105
+ ],
106
+ },
107
+ ])
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />