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.
- package/.env +4 -3
- 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 +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- 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 +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- 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 +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- 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 +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- 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 +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- 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 +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.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/scripts/ws-server.js +191 -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 +228 -80
- 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/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- 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 -378
- 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]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- 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 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -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, '/')),
|
|
@@ -176,37 +195,9 @@ async function validateAccess(
|
|
|
176
195
|
return { valid: false, error: 'Invalid customer token' };
|
|
177
196
|
}
|
|
178
197
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
`https://api.stripe.com/v1/customers/${encodeURIComponent(customerId)}/subscriptions?status=active&limit=1`,
|
|
183
|
-
{ headers }
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
if (subResponse.ok) {
|
|
187
|
-
const subData = (await subResponse.json()) as { data: unknown[] };
|
|
188
|
-
if (subData.data && subData.data.length > 0) {
|
|
189
|
-
return { valid: true };
|
|
190
|
-
}
|
|
191
|
-
} else if (subResponse.status === 404) {
|
|
192
|
-
return { valid: false, error: 'Customer not found' };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const chargeResponse = await fetch(
|
|
196
|
-
`https://api.stripe.com/v1/charges?customer=${encodeURIComponent(customerId)}&limit=1`,
|
|
197
|
-
{ headers }
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
if (chargeResponse.ok) {
|
|
201
|
-
const chargeData = (await chargeResponse.json()) as {
|
|
202
|
-
data: Array<{ paid: boolean; refunded: boolean }>;
|
|
203
|
-
};
|
|
204
|
-
const hasSuccessfulPayment = chargeData.data?.some(
|
|
205
|
-
(charge) => charge.paid && !charge.refunded
|
|
206
|
-
);
|
|
207
|
-
if (hasSuccessfulPayment) {
|
|
208
|
-
return { valid: true };
|
|
209
|
-
}
|
|
198
|
+
const plan = await determinePlanFromStripe(customerId, env);
|
|
199
|
+
if (plan !== 'free') {
|
|
200
|
+
return { valid: true };
|
|
210
201
|
}
|
|
211
202
|
|
|
212
203
|
return { valid: false, error: 'No active subscription or purchase found' };
|
|
@@ -318,6 +309,16 @@ async function googleAuthCallback(request: Request, env: Env): Promise<Response>
|
|
|
318
309
|
|
|
319
310
|
const OTP_TTL = 300; // 5 minutes
|
|
320
311
|
const OTP_LENGTH = 6;
|
|
312
|
+
const OTP_SEND_RATE_LIMIT = 5; // max sends per window
|
|
313
|
+
const OTP_VERIFY_RATE_LIMIT = 10; // max verify attempts per window
|
|
314
|
+
const RATE_LIMIT_WINDOW = 900; // 15 minutes
|
|
315
|
+
|
|
316
|
+
async function checkRateLimit(kv: KVNamespace, key: string, limit: number): Promise<boolean> {
|
|
317
|
+
const current = parseInt(await kv.get(key) || '0', 10);
|
|
318
|
+
if (current >= limit) return false;
|
|
319
|
+
await kv.put(key, String(current + 1), { expirationTtl: RATE_LIMIT_WINDOW });
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
321
322
|
|
|
322
323
|
function generateOTP(): string {
|
|
323
324
|
const array = new Uint32Array(1);
|
|
@@ -326,18 +327,28 @@ function generateOTP(): string {
|
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
async function handleSendOTP(request: Request, env: Env): Promise<Response> {
|
|
329
|
-
|
|
330
|
+
let body: { email?: string };
|
|
331
|
+
try {
|
|
332
|
+
body = (await request.json()) as { email?: string };
|
|
333
|
+
} catch {
|
|
334
|
+
return Response.json({ error: 'Invalid request body' }, { status: 400 });
|
|
335
|
+
}
|
|
330
336
|
const email = body.email?.trim().toLowerCase();
|
|
331
337
|
|
|
332
338
|
if (!email || !email.includes('@')) {
|
|
333
339
|
return Response.json({ error: 'Valid email required' }, { status: 400 });
|
|
334
340
|
}
|
|
335
341
|
|
|
342
|
+
const allowed = await checkRateLimit(env.AUTH_KV, `rate:otp-send:${email}`, OTP_SEND_RATE_LIMIT);
|
|
343
|
+
if (!allowed) {
|
|
344
|
+
return Response.json({ error: 'Too many requests. Try again later.' }, { status: 429 });
|
|
345
|
+
}
|
|
346
|
+
|
|
336
347
|
const code = generateOTP();
|
|
337
348
|
|
|
338
349
|
await env.AUTH_KV.put(`otp:${email}`, code, { expirationTtl: OTP_TTL });
|
|
339
350
|
|
|
340
|
-
await fetch('https://api.resend.com/emails', {
|
|
351
|
+
const emailRes = await fetch('https://api.resend.com/emails', {
|
|
341
352
|
method: 'POST',
|
|
342
353
|
headers: {
|
|
343
354
|
Authorization: `Bearer ${env.RESEND_API_KEY}`,
|
|
@@ -351,11 +362,21 @@ async function handleSendOTP(request: Request, env: Env): Promise<Response> {
|
|
|
351
362
|
}),
|
|
352
363
|
});
|
|
353
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
|
+
|
|
354
370
|
return Response.json({ sent: true });
|
|
355
371
|
}
|
|
356
372
|
|
|
357
373
|
async function handleVerifyOTP(request: Request, env: Env): Promise<Response> {
|
|
358
|
-
|
|
374
|
+
let body: { email?: string; code?: string };
|
|
375
|
+
try {
|
|
376
|
+
body = (await request.json()) as { email?: string; code?: string };
|
|
377
|
+
} catch {
|
|
378
|
+
return Response.json({ error: 'Invalid request body' }, { status: 400 });
|
|
379
|
+
}
|
|
359
380
|
const email = body.email?.trim().toLowerCase();
|
|
360
381
|
const code = body.code?.trim();
|
|
361
382
|
|
|
@@ -363,6 +384,11 @@ async function handleVerifyOTP(request: Request, env: Env): Promise<Response> {
|
|
|
363
384
|
return Response.json({ error: 'Email and code required' }, { status: 400 });
|
|
364
385
|
}
|
|
365
386
|
|
|
387
|
+
const allowed = await checkRateLimit(env.AUTH_KV, `rate:otp-verify:${email}`, OTP_VERIFY_RATE_LIMIT);
|
|
388
|
+
if (!allowed) {
|
|
389
|
+
return Response.json({ error: 'Too many attempts. Try again later.' }, { status: 429 });
|
|
390
|
+
}
|
|
391
|
+
|
|
366
392
|
const storedCode = await env.AUTH_KV.get(`otp:${email}`);
|
|
367
393
|
if (!storedCode || storedCode !== code) {
|
|
368
394
|
return Response.json({ error: 'Invalid or expired code' }, { status: 401 });
|
|
@@ -504,11 +530,17 @@ async function incrementUsage(db: D1Database, userId: string): Promise<void> {
|
|
|
504
530
|
// ─── Authenticated Route Handlers ───────────────────────────────────
|
|
505
531
|
|
|
506
532
|
async function handleGetMe(user: JWTPayload, env: Env): Promise<Response> {
|
|
507
|
-
//
|
|
508
|
-
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
|
+
]);
|
|
509
538
|
const currentPlan = dbUser?.plan || user.plan;
|
|
510
539
|
|
|
511
|
-
|
|
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;
|
|
512
544
|
|
|
513
545
|
const response: Record<string, unknown> = {
|
|
514
546
|
user: { id: user.sub, email: user.email, plan: currentPlan },
|
|
@@ -539,7 +571,12 @@ async function handleUsageIncrement(user: JWTPayload, env: Env): Promise<Respons
|
|
|
539
571
|
}
|
|
540
572
|
|
|
541
573
|
async function handleLinkStripe(request: Request, user: JWTPayload, env: Env): Promise<Response> {
|
|
542
|
-
|
|
574
|
+
let body: { customerId?: string };
|
|
575
|
+
try {
|
|
576
|
+
body = (await request.json()) as { customerId?: string };
|
|
577
|
+
} catch {
|
|
578
|
+
return Response.json({ error: 'Invalid request body' }, { status: 400 });
|
|
579
|
+
}
|
|
543
580
|
const customerId = body.customerId;
|
|
544
581
|
|
|
545
582
|
if (!customerId?.startsWith('cus_')) {
|
|
@@ -588,6 +625,68 @@ async function determinePlanFromStripe(customerId: string, env: Env): Promise<st
|
|
|
588
625
|
return 'free';
|
|
589
626
|
}
|
|
590
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
|
+
|
|
591
690
|
// ─── Existing Route Handlers ────────────────────────────────────────
|
|
592
691
|
|
|
593
692
|
async function handleUpdateManifest(env: Env): Promise<Response> {
|
|
@@ -599,7 +698,7 @@ async function handleUpdateManifest(env: Env): Promise<Response> {
|
|
|
599
698
|
return new Response(object.body, {
|
|
600
699
|
headers: {
|
|
601
700
|
'Content-Type': 'text/yaml',
|
|
602
|
-
'Cache-Control': '
|
|
701
|
+
'Cache-Control': 'public, max-age=3600, must-revalidate',
|
|
603
702
|
'ETag': object.httpEtag,
|
|
604
703
|
},
|
|
605
704
|
});
|
|
@@ -634,22 +733,54 @@ async function handleArtifactDownload(pathname: string, env: Env): Promise<Respo
|
|
|
634
733
|
});
|
|
635
734
|
}
|
|
636
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
|
+
|
|
637
765
|
async function handlePublicDownload(arch: string, env: Env): Promise<Response> {
|
|
638
|
-
//
|
|
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
|
|
639
772
|
const manifest = await env.RELEASE_ARTIFACTS.get('latest-mac.yml');
|
|
640
773
|
if (!manifest) {
|
|
641
774
|
return Response.json({ error: 'No releases available' }, { status: 404 });
|
|
642
775
|
}
|
|
643
776
|
|
|
644
777
|
const yml = await manifest.text();
|
|
645
|
-
// Find DMG filenames from the manifest
|
|
646
778
|
const dmgFiles = [...yml.matchAll(/url:\s*(\S+\.dmg)/g)].map(m => m[1]);
|
|
647
779
|
|
|
648
780
|
let filename: string | undefined;
|
|
649
781
|
if (arch === 'arm64') {
|
|
650
782
|
filename = dmgFiles.find(f => f.includes('arm64'));
|
|
651
783
|
} else {
|
|
652
|
-
// x64 — the one without arm64
|
|
653
784
|
filename = dmgFiles.find(f => !f.includes('arm64'));
|
|
654
785
|
}
|
|
655
786
|
|
|
@@ -667,6 +798,7 @@ async function handlePublicDownload(arch: string, env: Env): Promise<Response> {
|
|
|
667
798
|
'Content-Type': 'application/x-apple-diskimage',
|
|
668
799
|
'Content-Length': object.size.toString(),
|
|
669
800
|
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
801
|
+
'Cache-Control': 'public, max-age=86400',
|
|
670
802
|
'ETag': object.httpEtag,
|
|
671
803
|
},
|
|
672
804
|
});
|
|
@@ -692,13 +824,7 @@ async function verifyStripeSignature(
|
|
|
692
824
|
if (age > 300) return false;
|
|
693
825
|
|
|
694
826
|
const signedPayload = `${timestamp}.${payload}`;
|
|
695
|
-
const key = await
|
|
696
|
-
'raw',
|
|
697
|
-
new TextEncoder().encode(secret),
|
|
698
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
699
|
-
false,
|
|
700
|
-
['sign']
|
|
701
|
-
);
|
|
827
|
+
const key = await getHmacKey(secret, 'sign');
|
|
702
828
|
const sig = await crypto.subtle.sign(
|
|
703
829
|
'HMAC',
|
|
704
830
|
key,
|
|
@@ -834,30 +960,33 @@ async function handleCheckoutSuccess(url: URL, env: Env): Promise<Response> {
|
|
|
834
960
|
return new Response('Invalid session', { status: 400 });
|
|
835
961
|
}
|
|
836
962
|
|
|
837
|
-
const session = (await response.json()) as StripeCheckoutSession;
|
|
838
|
-
const customerId = session.customer;
|
|
839
|
-
|
|
840
963
|
return new Response(
|
|
841
964
|
`<!DOCTYPE html>
|
|
842
965
|
<html>
|
|
843
|
-
<head
|
|
966
|
+
<head>
|
|
967
|
+
<meta charset="utf-8">
|
|
968
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
969
|
+
<title>JettyPod \u2014 Subscription Activated</title>
|
|
844
970
|
<style>
|
|
845
|
-
body { font-family: -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #
|
|
846
|
-
.card { background: white; padding: 48px; border-radius: 16px; box-shadow: 0 2px
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
971
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #F6F5F0; }
|
|
972
|
+
.card { background: white; padding: 48px 56px; border-radius: 16px; box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.06); text-align: center; max-width: 440px; }
|
|
973
|
+
.check { width: 56px; height: 56px; background: #E8EEEF; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 24px; }
|
|
974
|
+
.check svg { width: 28px; height: 28px; color: #4A6365; }
|
|
975
|
+
h1 { color: #18181b; margin: 0 0 12px; font-size: 24px; letter-spacing: -0.025em; }
|
|
976
|
+
p { color: #52525b; line-height: 1.6; margin: 0; font-size: 15px; }
|
|
977
|
+
.cta { display: inline-block; margin-top: 32px; background: #819D9F; color: white; text-decoration: none; padding: 12px 32px; border-radius: 12px; font-size: 15px; font-weight: 500; box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 4px 12px rgba(129,157,159,0.2); transition: filter 0.15s; }
|
|
978
|
+
.cta:hover { filter: brightness(1.05); }
|
|
979
|
+
</style>
|
|
980
|
+
</head>
|
|
852
981
|
<body>
|
|
853
982
|
<div class="card">
|
|
854
|
-
<
|
|
855
|
-
<
|
|
856
|
-
<
|
|
857
|
-
<
|
|
983
|
+
<div class="check"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg></div>
|
|
984
|
+
<h1>You're all set!</h1>
|
|
985
|
+
<p>Your subscription is active. Head back to JettyPod to get started.</p>
|
|
986
|
+
<a class="cta" href="jettypod://subscription/activated">Open JettyPod</a>
|
|
858
987
|
</div>
|
|
859
988
|
</body></html>`,
|
|
860
|
-
{ headers: { 'Content-Type': 'text/html' } }
|
|
989
|
+
{ headers: { 'Content-Type': 'text/html; charset=utf-8' } }
|
|
861
990
|
);
|
|
862
991
|
}
|
|
863
992
|
|
|
@@ -1012,12 +1141,19 @@ async function handleRequest(request: Request, url: URL, pathname: string, env:
|
|
|
1012
1141
|
return handlePublicDownload('arm64', env);
|
|
1013
1142
|
}
|
|
1014
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
|
+
|
|
1015
1150
|
// ── Update routes (paid plan or legacy cus_ token) ─────────────
|
|
1016
1151
|
|
|
1017
|
-
// Update manifest
|
|
1152
|
+
// Update manifest (Electron)
|
|
1018
1153
|
if (request.method === 'GET' && pathname === '/updates/latest-mac.yml') {
|
|
1019
1154
|
const authError = await authenticateUpdateRequest(request, env);
|
|
1020
1155
|
if (authError) return authError;
|
|
1156
|
+
logUpdateCheck(request, 'electron');
|
|
1021
1157
|
return handleUpdateManifest(env);
|
|
1022
1158
|
}
|
|
1023
1159
|
|
|
@@ -1028,6 +1164,18 @@ async function handleRequest(request: Request, url: URL, pathname: string, env:
|
|
|
1028
1164
|
return handleArtifactDownload(pathname, env);
|
|
1029
1165
|
}
|
|
1030
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
|
+
|
|
1031
1179
|
// ── Stripe/checkout routes (existing) ──────────────────────────
|
|
1032
1180
|
|
|
1033
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"] }
|