Repository navigation
Expand file tree
/
Copy pathTaskfile.yml
More file actions
574 lines (507 loc) · 20.4 KB
/
Copy pathTaskfile.yml
File metadata and controls
574 lines (507 loc) · 20.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
version: '3'
vars:
GCP_PROJECT: msanii-484501
BACKEND_DIR: src/backend
dotenv: ['.env']
tasks:
install:
desc: Install all dependencies (Poetry + npm)
cmds:
- task: install:backend
- task: install:frontend
install:backend:
desc: Install Python backend dependencies via Poetry
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry install --no-interaction
sources:
- pyproject.toml
- poetry.lock
install:frontend:
desc: Install Node.js frontend dependencies
cmds:
- npm ci
sources:
- package-lock.json
setup:
desc: Install all dependencies and pull secrets from GSM
cmds:
- task: install
- task: setup:secrets
setup:secrets:
desc: Pull secrets from GSM into .env (alias for setup:secrets:dev)
cmds:
- task: setup:secrets:dev
setup:secrets:dev:
desc: |
Merge DEV-flavored secrets from GSM into .env (Stripe test mode, dev PostHog).
Additive only: keys with a live (uncommented, non-empty) value in .env are preserved;
keys missing, commented out, or empty get filled from GSM.
cmds:
- task: _setup:secrets:internal
vars:
ENV_SUFFIX: DEV
BYPASS_PAYWALLS: "false"
POSTHOG_ENABLED: "false"
PAYPAL_ENV: "sandbox"
FRONTEND_URL: "http://localhost:8080"
VITE_BACKEND_API_URL: "http://localhost:8000"
setup:secrets:prod:
desc: |
Merge PROD-flavored secrets from GSM into .env (Stripe LIVE mode, prod PostHog).
Additive only: existing live values in .env are preserved.
Use with care — never run a local dev server with prod Stripe keys against prod data.
To do a clean refill, run setup:secrets:reset:prod instead.
prompt: Merge PROD secrets (Stripe LIVE keys) into .env? Continue?
cmds:
- task: _setup:secrets:internal
vars:
ENV_SUFFIX: PROD
BYPASS_PAYWALLS: "false"
POSTHOG_ENABLED: "false"
PAYPAL_ENV: "live"
# No trailing slash — every consumer appends "/path".
FRONTEND_URL: "https://www.msanii-beta.com"
VITE_BACKEND_API_URL: "https://msanii-backend-529844341368.northamerica-northeast2.run.app"
setup:secrets:reset:dev:
desc: |
DESTRUCTIVE: replace .env entirely with DEV-flavored GSM secrets.
Your current .env is backed up to .env.backup (gitignored) before deletion.
Use when you want a clean slate; otherwise prefer the additive setup:secrets:dev.
prompt: This will DELETE your .env and refill with DEV secrets. Continue?
cmds:
- task: _setup:secrets:reset-env-file
- task: setup:secrets:dev
setup:secrets:reset:prod:
desc: |
DESTRUCTIVE: replace .env entirely with PROD-flavored GSM secrets (Stripe LIVE).
Your current .env is backed up to .env.backup (gitignored) before deletion.
Use when you want a clean slate; otherwise prefer the additive setup:secrets:prod.
prompt: This will DELETE your .env and refill with PROD secrets (Stripe LIVE). Continue?
cmds:
- task: _setup:secrets:reset-env-file
- task: _setup:secrets:internal
vars:
ENV_SUFFIX: PROD
BYPASS_PAYWALLS: "false"
POSTHOG_ENABLED: "false"
PAYPAL_ENV: "live"
# No trailing slash — every consumer appends "/path".
FRONTEND_URL: "https://www.msanii-beta.com"
VITE_BACKEND_API_URL: "https://msanii-backend-529844341368.northamerica-northeast2.run.app"
secrets:push:stripe:dev:
desc: |
Push the Stripe TEST-mode values from your local .env into GSM so a
teammate gets them via `task setup:secrets:dev`. Additive + idempotent:
a secret already in GSM is left untouched (rotate those deliberately via
`gcloud secrets versions add`), and a key with no live value in .env is
skipped. Values are piped .env -> gcloud and never printed.
Deliberately NOT pushed: STRIPE_WEBHOOK_SECRET — for local testing that
is per-developer (each `stripe listen` login has its own whsec_...), and
the GSM copy belongs to the deployed endpoint.
prompt: Push Stripe TEST values from .env into GSM (msanii-484501)? Continue?
preconditions:
- sh: gcloud auth print-identity-token > /dev/null 2>&1
msg: |
Not authenticated with gcloud. Run:
gcloud auth login --update-adc
gcloud config set project {{.GCP_PROJECT}}
- sh: "[[ -f .env ]]"
msg: No .env found — nothing to push.
cmds:
- |
echo "Legend: ✓ created ◌ already in GSM (untouched) ✗ skipped/failed"
env_value_pipe() { # extract KEY's value; stays inside the pipe, never echoed
grep -E "^[[:space:]]*$1[[:space:]]*=" .env | head -1 \
| sed -E "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*//" | tr -d '\n'
}
push_key() {
local env_key="$1" gsm_name="$2"
if ! grep -qE "^[[:space:]]*${env_key}[[:space:]]*=[[:space:]]*[^[:space:]#]" .env; then
echo " ✗ ${env_key} — no live value in .env, skipped"
return
fi
if gcloud secrets describe "$gsm_name" --project="{{.GCP_PROJECT}}" >/dev/null 2>&1; then
echo " ◌ ${gsm_name} — already in GSM, untouched"
return
fi
if env_value_pipe "$env_key" \
| gcloud secrets create "$gsm_name" --data-file=- \
--replication-policy=automatic --project="{{.GCP_PROJECT}}" >/dev/null 2>&1; then
echo " ✓ ${gsm_name} — created from local ${env_key}"
else
echo " ✗ ${gsm_name} — create FAILED (check gcloud output/permissions)"
fi
}
# Refuse to file a LIVE key under a _DEV name. Checked in the shell —
# the value itself is never printed.
if grep -qE "^[[:space:]]*STRIPE_SECRET_KEY[[:space:]]*=[[:space:]]*sk_live_" .env; then
echo "REFUSING: local STRIPE_SECRET_KEY is a LIVE key (sk_live_...) — not pushing it as _DEV."
exit 1
fi
push_key STRIPE_SECRET_KEY STRIPE_SECRET_KEY_DEV
push_key STRIPE_CREDITS_PRODUCT_ID STRIPE_CREDITS_PRODUCT_ID_DEV
push_key STRIPE_PRICE_MONTHLY STRIPE_PRICE_MONTHLY
push_key STRIPE_PRICE_ANNUAL STRIPE_PRICE_ANNUAL
push_key STRIPE_PRICE_PRO_MAX_MONTHLY STRIPE_PRICE_PRO_MAX_MONTHLY
push_key STRIPE_PRICE_PRO_MAX_ANNUAL STRIPE_PRICE_PRO_MAX_ANNUAL
echo
echo "Done. Teammates now get these via: task setup:secrets:dev"
_setup:secrets:reset-env-file:
desc: Internal — back up and delete .env. Used by setup:secrets:reset:* tasks.
internal: true
cmds:
- |
if [[ -f .env ]]; then
cp .env .env.backup
echo "Backed up current .env to .env.backup ($(wc -l < .env | tr -d ' ') lines)"
rm .env
echo "Deleted .env"
else
echo "No .env to back up"
fi
_setup:secrets:internal:
desc: Internal — do not call directly. Use setup:secrets:dev or setup:secrets:prod.
internal: true
preconditions:
- sh: gcloud auth print-identity-token > /dev/null 2>&1
msg: |
Not authenticated with gcloud. Run:
gcloud auth login --update-adc
gcloud config set project {{.GCP_PROJECT}}
- sh: '[[ "{{.ENV_SUFFIX}}" == "DEV" || "{{.ENV_SUFFIX}}" == "PROD" ]]'
msg: ENV_SUFFIX must be DEV or PROD
cmds:
- |
echo "Merging {{.ENV_SUFFIX}} secrets from GSM into .env (project: {{.GCP_PROJECT}})..."
echo "Legend: ✓ added ◌ kept your local value ✗ not in GSM"
touch .env
# True if KEY has an uncommented, non-empty value in .env.
# Handles all of these formats as "live":
# KEY=value
# KEY = value (whitespace around =)
# KEY=value (leading whitespace)
# Treated as "missing" (will be filled from GSM/literal):
# # KEY=value (commented out)
# KEY= (empty value)
# KEY = (empty with whitespace)
has_live_entry() {
local key="$1"
grep -qE "^[[:space:]]*${key}[[:space:]]*=[[:space:]]*[^[:space:]#]" .env
}
# ---- Section machinery ----
# Each "section" buffers its additions in PENDING_LINES. end_section()
# emits the section header + buffered lines to .env, but only if at
# least one key was added (so re-runs don't produce empty headers).
PENDING_LINES=()
CURRENT_SECTION_TITLE=""
start_section() {
CURRENT_SECTION_TITLE="$1"
PENDING_LINES=()
echo
echo "[$CURRENT_SECTION_TITLE]"
}
end_section() {
if (( ${#PENDING_LINES[@]} == 0 )); then
return
fi
{
echo ""
echo "# ============================================================================"
echo "# ${CURRENT_SECTION_TITLE}"
echo "# ============================================================================"
for line in "${PENDING_LINES[@]}"; do
echo "$line"
done
} >> .env
PENDING_LINES=()
}
# Try a shared GSM secret (GSM name == env-var name)
try_gsm() {
local key="$1"
if has_live_entry "$key"; then
echo " ◌ $key"
return
fi
local value
if value=$(gcloud secrets versions access latest --secret="$key" --project="{{.GCP_PROJECT}}" 2>/dev/null); then
PENDING_LINES+=("${key}=${value}")
echo " ✓ $key"
else
echo " ✗ $key (not in GSM or no access)"
fi
}
# Try an env-scoped GSM secret (GSM stores NAME_DEV / NAME_PROD; written as NAME).
# Optional 2nd arg: GSM base name, when the env-var name differs from the
# GSM secret name (e.g. VITE_PAYPAL_CLIENT_ID reuses PAYPAL_CLIENT_ID_*).
try_gsm_scoped() {
local base="$1"
local gsm_base="${2:-$base}"
if has_live_entry "$base"; then
echo " ◌ $base"
return
fi
local gsm_name="${gsm_base}_{{.ENV_SUFFIX}}"
local value
if value=$(gcloud secrets versions access latest --secret="$gsm_name" --project="{{.GCP_PROJECT}}" 2>/dev/null); then
PENDING_LINES+=("${base}=${value}")
echo " ✓ $base (from $gsm_name)"
else
echo " ✗ $gsm_name (not in GSM or no access)"
fi
}
# Literal value (env-specific config, not from GSM)
try_literal() {
local key="$1"
local value="$2"
if has_live_entry "$key"; then
echo " ◌ $key"
return
fi
PENDING_LINES+=("${key}=${value}")
echo " ✓ $key"
}
# ============ Sections (mirror docs/.env.example structure) ============
start_section "SUPABASE"
try_gsm "VITE_SUPABASE_URL"
try_gsm "VITE_SUPABASE_ANON_KEY"
try_gsm "VITE_SUPABASE_SECRET_KEY"
try_gsm "DATABASE_PW"
end_section
start_section "OPENAI API KEYS"
try_gsm "OPENAI_API_KEY"
try_gsm "OPENAI_LLM_MODEL"
try_gsm "OPENAI_LLM_MODEL_LARGE"
end_section
start_section "ENCRYPTION KEYS & SECRETS"
try_gsm "INTEGRATION_ENCRYPTION_KEY"
try_gsm "INTEGRATION_OAUTH_STATE_SECRET"
try_gsm "CREDENTIALS_AES_KEY"
end_section
start_section "GOOGLE DRIVE AUTH MSANII BACKEND"
try_gsm "GOOGLE_DRIVE_CLIENT_ID"
try_gsm "GOOGLE_DRIVE_CLIENT_SECRET"
end_section
start_section "DROPBOX AUTH MSANII BACKEND"
try_gsm "DROPBOX_APP_KEY"
try_gsm "DROPBOX_APP_SECRET"
end_section
start_section "SPOTIFY INTEGRATION"
try_gsm "SPOTIFY_CLIENT_ID"
try_gsm "SPOTIFY_CLIENT_SECRET"
end_section
start_section "POSTHOG"
try_literal "POSTHOG_ENABLED" "{{.POSTHOG_ENABLED}}"
try_gsm "POSTHOG_PROJECT_TOKEN"
try_literal "POSTHOG_HOST" "https://us.i.posthog.com"
try_gsm "POSTHOG_PERSONAL_API_KEY"
try_gsm "POSTHOG_PROJECT_ID"
end_section
start_section "STRIPE INTEGRATION"
try_gsm_scoped "STRIPE_SECRET_KEY"
# NOTE for LOCAL testing: the GSM value is the deployed endpoint's
# signing secret. Webhooks forwarded through `stripe listen` are signed
# with YOUR CLI's own whsec_... (printed at listen startup) — overwrite
# this in your .env with that value or every local delivery 400s.
try_gsm_scoped "STRIPE_WEBHOOK_SECRET"
try_gsm "STRIPE_PRICE_MONTHLY"
try_gsm "STRIPE_PRICE_ANNUAL"
try_gsm "STRIPE_PRICE_PRO_MAX_MONTHLY"
try_gsm "STRIPE_PRICE_PRO_MAX_ANNUAL"
# Optional: single Stripe Product that ad-hoc credit purchases file
# under (see .env.example). Credit checkout works without it.
try_gsm_scoped "STRIPE_CREDITS_PRODUCT_ID"
end_section
start_section "PAYPAL (royalty payouts)"
try_literal "PAYPAL_ENV" "{{.PAYPAL_ENV}}"
try_gsm_scoped "PAYPAL_CLIENT_ID"
try_gsm_scoped "PAYPAL_CLIENT_SECRET"
try_gsm_scoped "VITE_PAYPAL_CLIENT_ID" "PAYPAL_CLIENT_ID"
# On DEV (sandbox) runs, also drop the LIVE creds in COMMENTED-OUT, so a
# local live-payment smoke test is just: uncomment the three lines below and
# flip PAYPAL_ENV to live. They override the sandbox values above (last
# duplicate key in the file wins). Idempotent — the marker guards re-adds.
if [[ "{{.ENV_SUFFIX}}" == "DEV" ]] && ! grep -q "To test LIVE payments" .env; then
live_id=$(gcloud secrets versions access latest --secret="PAYPAL_CLIENT_ID_PROD" --project="{{.GCP_PROJECT}}" 2>/dev/null || true)
live_secret=$(gcloud secrets versions access latest --secret="PAYPAL_CLIENT_SECRET_PROD" --project="{{.GCP_PROJECT}}" 2>/dev/null || true)
if [[ -n "$live_id" && -n "$live_secret" ]]; then
PENDING_LINES+=("")
PENDING_LINES+=("# To test LIVE payments: uncomment the three lines below and set")
PENDING_LINES+=("# PAYPAL_CLIENT_ID=${live_id}")
PENDING_LINES+=("# PAYPAL_CLIENT_SECRET=${live_secret}")
PENDING_LINES+=("# VITE_PAYPAL_CLIENT_ID=${live_id}")
echo " ✓ LIVE PayPal override (commented, from *_PROD)"
else
echo " ✗ PAYPAL_*_PROD not in GSM / no access — skipped live override"
fi
fi
end_section
start_section "RESEND SMTP"
try_gsm "RESEND_API_KEY"
try_gsm "RESEND_FROM_EMAIL"
end_section
# DEV-only: turn on credits + licensing so they can be exercised locally.
# Gated to DEV so a PROD refill never silently flips these feature flags on.
if [[ "{{.ENV_SUFFIX}}" == "DEV" ]]; then
start_section "CREDIT SYSTEM & LICENSING (local testing)"
try_literal "CREDITS_ENABLED" "true"
try_literal "LICENSING_ENABLED" "true"
end_section
fi
start_section "LOCAL TESTING VARS & LOCAL ADMIN RIGHTS"
try_gsm "ADMIN_EMAILS"
try_gsm "TESTER_EMAILS"
try_gsm_scoped "VITE_APP_ENV"
try_literal "OPS_NOTIFICATION_EMAIL" "tech@greenboxanalytics.ca"
try_literal "BYPASS_PAYWALLS" "{{.BYPASS_PAYWALLS}}"
try_literal "FRONTEND_URL" "{{.FRONTEND_URL}}"
try_literal "VITE_FRONTEND_URL" "{{.FRONTEND_URL}}"
try_literal "VITE_BACKEND_API_URL" "{{.VITE_BACKEND_API_URL}}"
try_literal "ALLOWED_ORIGINS" "{{.FRONTEND_URL}}"
end_section
echo
echo "Done. .env now has $(wc -l < .env | tr -d ' ') lines."
echo "Tip: to fully reset .env, run 'task setup:secrets:reset:dev' (backs up first)."
test:
desc: Run all backend tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest -v {{.CLI_ARGS}}
test:health:
desc: Run health/smoke tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_health.py -v {{.CLI_ARGS}}
test:artists:
desc: Run artist endpoint tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_artists.py -v {{.CLI_ARGS}}
test:projects:
desc: Run project endpoint tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_projects.py -v {{.CLI_ARGS}}
test:registry:
desc: Run all registry tests (works, stakes, licenses, collaborators)
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_registry_*.py -v {{.CLI_ARGS}}
test:boards:
desc: Run kanban board tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_boards.py -v {{.CLI_ARGS}}
test:settings:
desc: Run workspace settings tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_settings.py -v {{.CLI_ARGS}}
test:members:
desc: Run project member tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_project_members.py -v {{.CLI_ARGS}}
test:splitsheet:
desc: Run split sheet generation tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_splitsheet.py -v {{.CLI_ARGS}}
test:files:
desc: Run file management tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_files.py -v {{.CLI_ARGS}}
test:notes:
desc: Run notes and folders tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_notes.py -v {{.CLI_ARGS}}
test:notifications:
desc: Run notification tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_notifications.py -v {{.CLI_ARGS}}
test:teamcard:
desc: Run team card tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_teamcard.py -v {{.CLI_ARGS}}
test:zoe:
desc: Run Zoe AI chatbot tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_zoe.py -v {{.CLI_ARGS}}
test:oneclick:
desc: Run OneClick royalty calculator tests
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run pytest tests/test_oneclick.py -v {{.CLI_ARGS}}
lint:
desc: Run all linters (ruff + ESLint)
cmds:
- task: lint:backend
- task: lint:frontend
lint:backend:
desc: Run ruff lint and format check on backend
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run ruff check .
- poetry run ruff format --check .
lint:frontend:
desc: Run ESLint on frontend
cmds:
- npm run lint
format:
desc: Auto-fix formatting (ruff + ESLint)
cmds:
- task: format:backend
- task: format:frontend
format:backend:
desc: Auto-fix Python formatting with ruff
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run ruff format .
- poetry run ruff check --fix .
format:frontend:
desc: Auto-fix JS/TS formatting with ESLint
cmds:
- npx eslint . --fix
ci:
desc: Run full CI pipeline locally (lint + test + build)
cmds:
- task: lint
- task: test
- npm run build
env:
VITE_SUPABASE_URL: '{{.VITE_SUPABASE_URL | default "https://placeholder.supabase.co"}}'
VITE_SUPABASE_ANON_KEY: '{{.VITE_SUPABASE_ANON_KEY | default "placeholder"}}'
VITE_BACKEND_API_URL: '{{.VITE_BACKEND_API_URL | default "https://placeholder.example.com"}}'
dev:
desc: Start the frontend, the backend, and the partner API dev servers
deps: [dev:backend, dev:partner, dev:frontend]
dev:backend:
desc: Start FastAPI backend server
dir: "{{.BACKEND_DIR}}"
cmds:
- poetry run uvicorn main:app --host 0.0.0.0 --port 8000 --reload
dev:partner:
desc: |
Start the partner API on :8001 — the same app with PARTNER_API_ENABLED=true,
matching the second Cloud Run service in prod. The flag is set HERE and must
never go in .env: it makes the process 404 every route outside /partner/* and
/health, which would take the main backend on :8000 down with it.
Point the console's API panel at this with VITE_PARTNER_API_URL=http://localhost:8001.
dir: "{{.BACKEND_DIR}}"
env:
PARTNER_API_ENABLED: "true"
cmds:
- poetry run uvicorn main:app --host 0.0.0.0 --port 8001 --reload
dev:frontend:
desc: Start Vite frontend dev server
cmds:
- npm run dev
pricing:
desc: Open the LLM cost & credit model dashboard in your browser
cmds:
- python3 src/backend/subscriptions/pricing_model/open.py