jettypod 4.4.120 → 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.
- package/.env +2 -1
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
- package/apps/dashboard/app/demo/gates/page.tsx +3 -5
- package/apps/dashboard/app/design-system/page.tsx +1 -1
- package/apps/dashboard/app/globals.css +74 -2
- package/apps/dashboard/app/install-claude/page.tsx +3 -5
- package/apps/dashboard/app/login/page.tsx +17 -20
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +60 -12
- package/apps/dashboard/app/signup/page.tsx +14 -17
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +12 -15
- package/apps/dashboard/app/work/[id]/page.tsx +90 -75
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +70 -61
- package/apps/dashboard/components/CardMenu.tsx +0 -1
- package/apps/dashboard/components/ClaudePanel.tsx +541 -283
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/CopyableId.tsx +1 -2
- package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
- package/apps/dashboard/components/DragContext.tsx +132 -62
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +5 -6
- package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +0 -1
- package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
- package/apps/dashboard/components/EpicGroup.tsx +100 -70
- package/apps/dashboard/components/GateCard.tsx +0 -1
- package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
- package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
- package/apps/dashboard/components/JettyLoader.tsx +0 -1
- package/apps/dashboard/components/KanbanBoard.tsx +319 -173
- package/apps/dashboard/components/KanbanCard.tsx +341 -107
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
- package/apps/dashboard/components/MainNav.tsx +24 -25
- package/apps/dashboard/components/MessageBlock.tsx +93 -16
- package/apps/dashboard/components/ModeStartCard.tsx +0 -1
- package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
- package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
- package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
- package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
- package/apps/dashboard/components/ReviewFooter.tsx +12 -14
- package/apps/dashboard/components/SessionList.tsx +0 -1
- package/apps/dashboard/components/SubscribeContent.tsx +40 -11
- package/apps/dashboard/components/TestTree.tsx +1 -2
- package/apps/dashboard/components/TipCard.tsx +2 -4
- package/apps/dashboard/components/Toast.tsx +0 -1
- package/apps/dashboard/components/TypeIcon.tsx +7 -8
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
- package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
- package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
- package/apps/dashboard/components/WorkItemTree.tsx +2 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
- package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
- package/apps/dashboard/components/ui/Button.tsx +1 -1
- package/apps/dashboard/components/ui/Input.tsx +1 -1
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
- package/apps/dashboard/contexts/UsageContext.tsx +62 -31
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1302
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +226 -26
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +253 -122
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +3 -3
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -33
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +167 -30
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/jettypod.js +96 -4
- package/lib/bdd-preflight.js +96 -0
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/work-commands/index.js +58 -16
- package/lib/work-tracking/index.js +108 -8
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +43 -1
- package/skills-templates/chore-mode/SKILL.md +40 -1
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +14 -0
- package/skills-templates/feature-planning/SKILL.md +90 -1
- package/skills-templates/production-mode/SKILL.md +20 -0
- package/skills-templates/simple-improvement/SKILL.md +39 -2
- package/skills-templates/speed-mode/SKILL.md +10 -15
- package/skills-templates/stable-mode/SKILL.md +47 -0
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -55
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
- package/apps/dashboard/electron/ipc-handlers.js +0 -1026
- package/apps/dashboard/electron/main.js +0 -2306
- package/apps/dashboard/electron/preload.js +0 -125
- package/apps/dashboard/electron/session-manager.js +0 -163
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/backlog-parser.ts +0 -50
- package/apps/dashboard/lib/claude-process-manager.ts +0 -529
- package/apps/dashboard/lib/db-bridge.ts +0 -283
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -66
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/bug-icon.svg +0 -9
- package/apps/dashboard/public/buoy-icon.svg +0 -9
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/in-flight-seagull.svg +0 -9
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/pier-icon.svg +0 -14
- package/apps/dashboard/public/star-icon.svg +0 -9
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/public/wrench-icon.svg +0 -9
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react-swc'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [
|
|
8
|
+
react(),
|
|
9
|
+
tailwindcss(),
|
|
10
|
+
],
|
|
11
|
+
resolve: {
|
|
12
|
+
alias: {
|
|
13
|
+
'@': path.resolve(__dirname, '.'),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
server: {
|
|
17
|
+
port: 1420,
|
|
18
|
+
strictPort: true,
|
|
19
|
+
},
|
|
20
|
+
build: {
|
|
21
|
+
outDir: 'dist',
|
|
22
|
+
emptyOutDir: true,
|
|
23
|
+
rollupOptions: {
|
|
24
|
+
output: {
|
|
25
|
+
manualChunks: {
|
|
26
|
+
'vendor-motion': ['framer-motion'],
|
|
27
|
+
'vendor-dnd': ['@dnd-kit/core', '@dnd-kit/utilities'],
|
|
28
|
+
'vendor-markdown': ['react-markdown', 'remark-gfm'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
})
|
|
@@ -58,10 +58,41 @@ interface StripeCheckoutSession {
|
|
|
58
58
|
subscription: string;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
interface TauriPlatformArtifact {
|
|
62
|
+
url: string;
|
|
63
|
+
signature: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface TauriUpdateManifest {
|
|
67
|
+
version: string;
|
|
68
|
+
notes: string;
|
|
69
|
+
pub_date: string;
|
|
70
|
+
platforms: Record<string, TauriPlatformArtifact>;
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
// ─── JWT ────────────────────────────────────────────────────────────
|
|
62
74
|
|
|
63
75
|
const JWT_EXPIRY = 30 * 24 * 60 * 60; // 30 days
|
|
64
76
|
|
|
77
|
+
// Cache imported CryptoKeys to avoid re-importing on every JWT operation
|
|
78
|
+
const cryptoKeyCache = new Map<string, CryptoKey>();
|
|
79
|
+
|
|
80
|
+
async function getHmacKey(secret: string, usage: 'sign' | 'verify'): Promise<CryptoKey> {
|
|
81
|
+
const cacheKey = `${secret}:${usage}`;
|
|
82
|
+
let key = cryptoKeyCache.get(cacheKey);
|
|
83
|
+
if (!key) {
|
|
84
|
+
key = await crypto.subtle.importKey(
|
|
85
|
+
'raw',
|
|
86
|
+
new TextEncoder().encode(secret),
|
|
87
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
88
|
+
false,
|
|
89
|
+
[usage]
|
|
90
|
+
);
|
|
91
|
+
cryptoKeyCache.set(cacheKey, key);
|
|
92
|
+
}
|
|
93
|
+
return key;
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
async function signJWT(payload: JWTPayload, secret: string): Promise<string> {
|
|
66
97
|
const header = { alg: 'HS256', typ: 'JWT' };
|
|
67
98
|
const encode = (obj: unknown) =>
|
|
@@ -71,13 +102,7 @@ async function signJWT(payload: JWTPayload, secret: string): Promise<string> {
|
|
|
71
102
|
const payloadB64 = encode(payload);
|
|
72
103
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
73
104
|
|
|
74
|
-
const key = await
|
|
75
|
-
'raw',
|
|
76
|
-
new TextEncoder().encode(secret),
|
|
77
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
78
|
-
false,
|
|
79
|
-
['sign']
|
|
80
|
-
);
|
|
105
|
+
const key = await getHmacKey(secret, 'sign');
|
|
81
106
|
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signingInput));
|
|
82
107
|
const sigB64 = btoa(String.fromCharCode(...new Uint8Array(sig)))
|
|
83
108
|
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
@@ -92,13 +117,7 @@ async function verifyJWT(token: string, secret: string): Promise<JWTPayload | nu
|
|
|
92
117
|
const [headerB64, payloadB64, sigB64] = parts;
|
|
93
118
|
const signingInput = `${headerB64}.${payloadB64}`;
|
|
94
119
|
|
|
95
|
-
const key = await
|
|
96
|
-
'raw',
|
|
97
|
-
new TextEncoder().encode(secret),
|
|
98
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
99
|
-
false,
|
|
100
|
-
['verify']
|
|
101
|
-
);
|
|
120
|
+
const key = await getHmacKey(secret, 'verify');
|
|
102
121
|
|
|
103
122
|
const sigBytes = Uint8Array.from(
|
|
104
123
|
atob(sigB64.replace(/-/g, '+').replace(/_/g, '/')),
|
|
@@ -329,7 +348,7 @@ async function handleSendOTP(request: Request, env: Env): Promise<Response> {
|
|
|
329
348
|
|
|
330
349
|
await env.AUTH_KV.put(`otp:${email}`, code, { expirationTtl: OTP_TTL });
|
|
331
350
|
|
|
332
|
-
await fetch('https://api.resend.com/emails', {
|
|
351
|
+
const emailRes = await fetch('https://api.resend.com/emails', {
|
|
333
352
|
method: 'POST',
|
|
334
353
|
headers: {
|
|
335
354
|
Authorization: `Bearer ${env.RESEND_API_KEY}`,
|
|
@@ -343,6 +362,11 @@ async function handleSendOTP(request: Request, env: Env): Promise<Response> {
|
|
|
343
362
|
}),
|
|
344
363
|
});
|
|
345
364
|
|
|
365
|
+
if (!emailRes.ok) {
|
|
366
|
+
console.error('Failed to send OTP email:', emailRes.status, await emailRes.text());
|
|
367
|
+
return Response.json({ error: 'Failed to send email' }, { status: 502 });
|
|
368
|
+
}
|
|
369
|
+
|
|
346
370
|
return Response.json({ sent: true });
|
|
347
371
|
}
|
|
348
372
|
|
|
@@ -506,11 +530,17 @@ async function incrementUsage(db: D1Database, userId: string): Promise<void> {
|
|
|
506
530
|
// ─── Authenticated Route Handlers ───────────────────────────────────
|
|
507
531
|
|
|
508
532
|
async function handleGetMe(user: JWTPayload, env: Env): Promise<Response> {
|
|
509
|
-
//
|
|
510
|
-
const dbUser = await
|
|
533
|
+
// Run user fetch and usage check in parallel to avoid sequential DB queries
|
|
534
|
+
const [dbUser, usageFromJwt] = await Promise.all([
|
|
535
|
+
env.AUTH_DB.prepare('SELECT * FROM users WHERE id = ?').bind(user.sub).first<User>(),
|
|
536
|
+
checkUsageLimit(env.AUTH_DB, user.sub, user.plan),
|
|
537
|
+
]);
|
|
511
538
|
const currentPlan = dbUser?.plan || user.plan;
|
|
512
539
|
|
|
513
|
-
|
|
540
|
+
// If plan changed from JWT, re-check usage with correct plan
|
|
541
|
+
const usage = currentPlan !== user.plan
|
|
542
|
+
? await checkUsageLimit(env.AUTH_DB, user.sub, currentPlan)
|
|
543
|
+
: usageFromJwt;
|
|
514
544
|
|
|
515
545
|
const response: Record<string, unknown> = {
|
|
516
546
|
user: { id: user.sub, email: user.email, plan: currentPlan },
|
|
@@ -595,6 +625,68 @@ async function determinePlanFromStripe(customerId: string, env: Env): Promise<st
|
|
|
595
625
|
return 'free';
|
|
596
626
|
}
|
|
597
627
|
|
|
628
|
+
// ─── Platform Telemetry ─────────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
function logUpdateCheck(request: Request, platform: 'electron' | 'tauri', meta: Record<string, string> = {}): void {
|
|
631
|
+
const userAgent = request.headers.get('User-Agent') || 'unknown';
|
|
632
|
+
console.log(JSON.stringify({
|
|
633
|
+
event: 'update_check',
|
|
634
|
+
platform,
|
|
635
|
+
user_agent: userAgent,
|
|
636
|
+
...meta,
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ─── Tauri Update ──────────────────────────────────────────────────
|
|
641
|
+
|
|
642
|
+
/** Compare two semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal. */
|
|
643
|
+
function compareSemver(a: string, b: string): number {
|
|
644
|
+
const pa = a.replace(/^v/, '').split('.').map(Number);
|
|
645
|
+
const pb = b.replace(/^v/, '').split('.').map(Number);
|
|
646
|
+
for (let i = 0; i < 3; i++) {
|
|
647
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
648
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
649
|
+
}
|
|
650
|
+
return 0;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function handleTauriUpdate(
|
|
654
|
+
target: string,
|
|
655
|
+
arch: string,
|
|
656
|
+
currentVersion: string,
|
|
657
|
+
requestUrl: URL,
|
|
658
|
+
env: Env
|
|
659
|
+
): Promise<Response> {
|
|
660
|
+
const object = await env.RELEASE_ARTIFACTS.get('tauri-releases.json');
|
|
661
|
+
if (!object) {
|
|
662
|
+
return Response.json({ error: 'No Tauri releases available' }, { status: 404 });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const manifest = (await object.json()) as TauriUpdateManifest;
|
|
666
|
+
const platformKey = `${target}-${arch}`;
|
|
667
|
+
const platform = manifest.platforms[platformKey];
|
|
668
|
+
|
|
669
|
+
if (!platform) {
|
|
670
|
+
return Response.json({ error: `Unsupported platform: ${platformKey}` }, { status: 404 });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (compareSemver(manifest.version, currentVersion) <= 0) {
|
|
674
|
+
return new Response('', { status: 204 });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const downloadUrl = platform.url.startsWith('http')
|
|
678
|
+
? platform.url
|
|
679
|
+
: `${requestUrl.origin}/updates/download/${platform.url}`;
|
|
680
|
+
|
|
681
|
+
return Response.json({
|
|
682
|
+
version: manifest.version,
|
|
683
|
+
notes: manifest.notes,
|
|
684
|
+
pub_date: manifest.pub_date,
|
|
685
|
+
url: downloadUrl,
|
|
686
|
+
signature: platform.signature,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
598
690
|
// ─── Existing Route Handlers ────────────────────────────────────────
|
|
599
691
|
|
|
600
692
|
async function handleUpdateManifest(env: Env): Promise<Response> {
|
|
@@ -606,7 +698,7 @@ async function handleUpdateManifest(env: Env): Promise<Response> {
|
|
|
606
698
|
return new Response(object.body, {
|
|
607
699
|
headers: {
|
|
608
700
|
'Content-Type': 'text/yaml',
|
|
609
|
-
'Cache-Control': '
|
|
701
|
+
'Cache-Control': 'public, max-age=3600, must-revalidate',
|
|
610
702
|
'ETag': object.httpEtag,
|
|
611
703
|
},
|
|
612
704
|
});
|
|
@@ -641,22 +733,54 @@ async function handleArtifactDownload(pathname: string, env: Env): Promise<Respo
|
|
|
641
733
|
});
|
|
642
734
|
}
|
|
643
735
|
|
|
736
|
+
async function handleTauriPublicDownload(platformKey: string, env: Env): Promise<Response | null> {
|
|
737
|
+
const object = await env.RELEASE_ARTIFACTS.get('tauri-releases.json');
|
|
738
|
+
if (!object) return null;
|
|
739
|
+
|
|
740
|
+
const manifest = (await object.json()) as TauriUpdateManifest;
|
|
741
|
+
const platform = manifest.platforms[platformKey];
|
|
742
|
+
if (!platform?.url) return null;
|
|
743
|
+
|
|
744
|
+
const filename = platform.url;
|
|
745
|
+
const artifact = await env.RELEASE_ARTIFACTS.get(filename);
|
|
746
|
+
if (!artifact) return null;
|
|
747
|
+
|
|
748
|
+
const contentType = filename.endsWith('.dmg')
|
|
749
|
+
? 'application/x-apple-diskimage'
|
|
750
|
+
: filename.endsWith('.AppImage')
|
|
751
|
+
? 'application/x-executable'
|
|
752
|
+
: 'application/octet-stream';
|
|
753
|
+
|
|
754
|
+
return new Response(artifact.body, {
|
|
755
|
+
headers: {
|
|
756
|
+
'Content-Type': contentType,
|
|
757
|
+
'Content-Length': artifact.size.toString(),
|
|
758
|
+
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
759
|
+
'Cache-Control': 'public, max-age=86400',
|
|
760
|
+
'ETag': artifact.httpEtag,
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
644
765
|
async function handlePublicDownload(arch: string, env: Env): Promise<Response> {
|
|
645
|
-
//
|
|
766
|
+
// Try Tauri artifacts first
|
|
767
|
+
const platformKey = arch === 'arm64' ? 'darwin-aarch64' : 'darwin-x86_64';
|
|
768
|
+
const tauriResponse = await handleTauriPublicDownload(platformKey, env);
|
|
769
|
+
if (tauriResponse) return tauriResponse;
|
|
770
|
+
|
|
771
|
+
// Fall back to Electron manifest (latest-mac.yml) during transition
|
|
646
772
|
const manifest = await env.RELEASE_ARTIFACTS.get('latest-mac.yml');
|
|
647
773
|
if (!manifest) {
|
|
648
774
|
return Response.json({ error: 'No releases available' }, { status: 404 });
|
|
649
775
|
}
|
|
650
776
|
|
|
651
777
|
const yml = await manifest.text();
|
|
652
|
-
// Find DMG filenames from the manifest
|
|
653
778
|
const dmgFiles = [...yml.matchAll(/url:\s*(\S+\.dmg)/g)].map(m => m[1]);
|
|
654
779
|
|
|
655
780
|
let filename: string | undefined;
|
|
656
781
|
if (arch === 'arm64') {
|
|
657
782
|
filename = dmgFiles.find(f => f.includes('arm64'));
|
|
658
783
|
} else {
|
|
659
|
-
// x64 — the one without arm64
|
|
660
784
|
filename = dmgFiles.find(f => !f.includes('arm64'));
|
|
661
785
|
}
|
|
662
786
|
|
|
@@ -700,13 +824,7 @@ async function verifyStripeSignature(
|
|
|
700
824
|
if (age > 300) return false;
|
|
701
825
|
|
|
702
826
|
const signedPayload = `${timestamp}.${payload}`;
|
|
703
|
-
const key = await
|
|
704
|
-
'raw',
|
|
705
|
-
new TextEncoder().encode(secret),
|
|
706
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
707
|
-
false,
|
|
708
|
-
['sign']
|
|
709
|
-
);
|
|
827
|
+
const key = await getHmacKey(secret, 'sign');
|
|
710
828
|
const sig = await crypto.subtle.sign(
|
|
711
829
|
'HMAC',
|
|
712
830
|
key,
|
|
@@ -1023,12 +1141,19 @@ async function handleRequest(request: Request, url: URL, pathname: string, env:
|
|
|
1023
1141
|
return handlePublicDownload('arm64', env);
|
|
1024
1142
|
}
|
|
1025
1143
|
|
|
1144
|
+
if (request.method === 'GET' && pathname === '/download/linux') {
|
|
1145
|
+
const response = await handleTauriPublicDownload('linux-x86_64', env);
|
|
1146
|
+
if (response) return response;
|
|
1147
|
+
return Response.json({ error: 'No Linux release available' }, { status: 404 });
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1026
1150
|
// ── Update routes (paid plan or legacy cus_ token) ─────────────
|
|
1027
1151
|
|
|
1028
|
-
// Update manifest
|
|
1152
|
+
// Update manifest (Electron)
|
|
1029
1153
|
if (request.method === 'GET' && pathname === '/updates/latest-mac.yml') {
|
|
1030
1154
|
const authError = await authenticateUpdateRequest(request, env);
|
|
1031
1155
|
if (authError) return authError;
|
|
1156
|
+
logUpdateCheck(request, 'electron');
|
|
1032
1157
|
return handleUpdateManifest(env);
|
|
1033
1158
|
}
|
|
1034
1159
|
|
|
@@ -1039,6 +1164,18 @@ async function handleRequest(request: Request, url: URL, pathname: string, env:
|
|
|
1039
1164
|
return handleArtifactDownload(pathname, env);
|
|
1040
1165
|
}
|
|
1041
1166
|
|
|
1167
|
+
// Tauri update manifest — /tauri/:target/:arch/:current_version
|
|
1168
|
+
if (request.method === 'GET' && pathname.startsWith('/tauri/')) {
|
|
1169
|
+
const authError = await authenticateUpdateRequest(request, env);
|
|
1170
|
+
if (authError) return authError;
|
|
1171
|
+
const parts = pathname.split('/').filter(Boolean); // ["tauri", target, arch, version]
|
|
1172
|
+
if (parts.length === 4) {
|
|
1173
|
+
logUpdateCheck(request, 'tauri', { target: parts[1], arch: parts[2], current_version: parts[3] });
|
|
1174
|
+
return handleTauriUpdate(parts[1], parts[2], parts[3], url, env);
|
|
1175
|
+
}
|
|
1176
|
+
return Response.json({ error: 'Invalid update URL format' }, { status: 400 });
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1042
1179
|
// ── Stripe/checkout routes (existing) ──────────────────────────
|
|
1043
1180
|
|
|
1044
1181
|
if (request.method === 'POST' && pathname === '/checkout/create-session') {
|
|
@@ -245,28 +245,29 @@ function evaluateBashCommand(command, inputRef, cwd) {
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
// BLOCKED: work merge or tests merge from inside a worktree
|
|
249
|
-
//
|
|
248
|
+
// BLOCKED: work merge or tests merge from inside a worktree WITHOUT explicit ID
|
|
249
|
+
// Merge with explicit ID is safe: the merge command handles CWD internally via
|
|
250
|
+
// process.chdir() and does NOT delete the worktree (only cleanup does).
|
|
251
|
+
// Bare merge (no ID) is blocked because getCurrentWork() relies on branch detection
|
|
252
|
+
// which breaks after process.chdir() moves away from the worktree.
|
|
250
253
|
if (/jettypod\s+(work|tests)\s+merge\b/.test(strippedCommand)) {
|
|
251
254
|
const isInWorktree = cwd && cwd.includes('.jettypod-work');
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
const hasExplicitId = /merge\s+\d+/.test(strippedCommand);
|
|
256
|
+
if (isInWorktree && !hasExplicitId) {
|
|
254
257
|
const mainRepoPath = cwd.split('.jettypod-work')[0].replace(/\/$/, '');
|
|
255
|
-
const workIdMatch = strippedCommand.match(/merge\s+(\d+)/);
|
|
256
|
-
const workId = workIdMatch ? workIdMatch[1] : '<id>';
|
|
257
258
|
|
|
258
259
|
return {
|
|
259
260
|
allowed: false,
|
|
260
|
-
message: 'Cannot merge from inside a worktree',
|
|
261
|
-
hint: `
|
|
262
|
-
Main repo: ${mainRepoPath}
|
|
261
|
+
message: 'Cannot merge from inside a worktree without an explicit work item ID',
|
|
262
|
+
hint: `Provide the work item ID explicitly:
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
jettypod work merge <id>
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
This works from anywhere — the merge command handles CWD internally.
|
|
267
|
+
After merge, cd to main repo before cleanup:
|
|
268
268
|
|
|
269
|
-
|
|
269
|
+
cd ${mainRepoPath}
|
|
270
|
+
jettypod work cleanup <id>`
|
|
270
271
|
};
|
|
271
272
|
}
|
|
272
273
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "jettypod-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "JettyPod workflow CLI"
|
|
6
|
+
|
|
7
|
+
[[bin]]
|
|
8
|
+
name = "jettypod"
|
|
9
|
+
path = "src/main.rs"
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
jettypod-core = { path = "../jettypod-core" }
|
|
13
|
+
clap = { version = "4", features = ["derive"] }
|
|
14
|
+
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
|
15
|
+
anyhow = { workspace = true }
|
|
16
|
+
log = { workspace = true }
|
|
17
|
+
serde_json = { workspace = true }
|
|
18
|
+
chrono = { workspace = true }
|
|
19
|
+
rusqlite = { version = "0.31", features = ["bundled"] }
|