pruff-skills 0.3.0 → 0.4.0
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/dist/cli.mjs +854 -38
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { Ie, M, Me, Se, Y, be, fe, he, ve, xe, ye } from "./_chunks/libs/@clack/
|
|
|
5
5
|
import { esm_default } from "./_chunks/libs/simple-git.mjs";
|
|
6
6
|
import { xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
|
|
7
7
|
import { require_dist } from "./_chunks/libs/@vercel/detect-agent.mjs";
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
|
|
9
9
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
import { stripVTControlCharacters } from "node:util";
|
|
@@ -6126,16 +6126,495 @@ async function sweepLine(text, { slow = false, indent = " " } = {}) {
|
|
|
6126
6126
|
process.stdout.write("\x1B[?25h");
|
|
6127
6127
|
}
|
|
6128
6128
|
}
|
|
6129
|
+
const WORKSPACE_CLAUDE_TEMPLATE = `# Pruff — Working Directory
|
|
6130
|
+
|
|
6131
|
+
This folder is the working directory for Pruff's frontend and backend. The two main git repos are:
|
|
6132
|
+
|
|
6133
|
+
- **\`Pruff-Client/\`** — React frontend
|
|
6134
|
+
- **\`Pruff-server/\`** — Node.js/Express backend
|
|
6135
|
+
|
|
6136
|
+
Other directories (\`sls-*/\`, \`slsl-*/\`, \`AWS-Tools/\`, etc.) are serverless microservices and tooling —
|
|
6137
|
+
clone them into this workspace as you need them, and don't modify them unless explicitly asked.
|
|
6138
|
+
|
|
6139
|
+
Each repo has its own \`CLAUDE.md\` and \`.claude/rules/\` with repo-specific patterns. This file covers
|
|
6140
|
+
workspace-level context only.
|
|
6141
|
+
|
|
6142
|
+
---
|
|
6143
|
+
|
|
6144
|
+
## Workspace Layout
|
|
6145
|
+
|
|
6146
|
+
### Main applications (modify freely when asked)
|
|
6147
|
+
|
|
6148
|
+
| Folder | What it is |
|
|
6149
|
+
|---|---|
|
|
6150
|
+
| \`Pruff-Client/\` | React 18 + Vite + MUI frontend. React Query + Redux Toolkit. |
|
|
6151
|
+
| \`Pruff-server/\` | Node.js 22 / Express / Prisma backend. Multi-tenant (DB-per-tenant). |
|
|
6152
|
+
|
|
6153
|
+
### Serverless microservices (\`sls-*/\`, \`slsl-*/\` — clone as needed, don't modify unless asked)
|
|
6154
|
+
|
|
6155
|
+
All live in the same AWS account, share the \`lambda-sqs\` IAM role, and most use a shared
|
|
6156
|
+
\`prisma-client-layer\` Lambda layer so they can reach tenant databases. They talk to \`Pruff-server\`
|
|
6157
|
+
over HTTPS with an \`x-api-key\`.
|
|
6158
|
+
|
|
6159
|
+
| Folder | Role |
|
|
6160
|
+
|---|---|
|
|
6161
|
+
| \`sls-SII-Services/\` | **SII (Chilean tax authority) scrapers & orchestrators.** Invoice generation, certificate validation, cession execution, F29/IVA extraction, credit-note references, cron orchestrators. Talks heavily to \`slsl-Data-Injection-Syncronizer\`. |
|
|
6162
|
+
| \`sls-TGR-Extraction/\` | **TGR (treasury) scrapers.** Extracts tax collections, debts, payment agreements. Pushes results into the data-injection pipeline. |
|
|
6163
|
+
| \`sls-TGR-Services/\` | **TGR-adjacent business logic.** Smaller sibling to \`sls-TGR-Extraction\`. |
|
|
6164
|
+
| \`sls-Public-Market/\` | **Mercado Público (Chilean public tenders) pipeline.** Awarded-tender fetcher, tender analyzer, lead generator. |
|
|
6165
|
+
| \`slsl-Data-Injection-Syncronizer/\` | **Fan-out hub between scrapers and Pruff-server.** Receives scraped data, resolves interested tenants, forwards per-tenant writes. Also hosts the \`ReflexOrchestrator\` change-detection pipeline. |
|
|
6166
|
+
| \`sls-Decision-Engine/\` | **Risk / Motor de Riesgo executor.** Three-stage SQS pipeline: \`prepare\` → \`delayedPrepare\` → \`executor\`. |
|
|
6167
|
+
| \`sls-Notification-Service/\` | **Outbound notifications** (email, in-app, collections). |
|
|
6168
|
+
| \`sls-Pruff-Webhooks/\` | **Outbound webhook delivery** to client integrations. |
|
|
6169
|
+
| \`sls-Pruff-Workers/\` | **Async background jobs for Pruff-server** (PDF rendering, tenant document/summary sync crons). |
|
|
6170
|
+
|
|
6171
|
+
### Tooling & utilities
|
|
6172
|
+
|
|
6173
|
+
| Folder | What it is |
|
|
6174
|
+
|---|---|
|
|
6175
|
+
| \`AWS-Tools/\` | Standalone Node/TypeScript scripts for AWS ops — not deployed; run locally with AWS creds. |
|
|
6176
|
+
|
|
6177
|
+
### Cross-service data flow (mental model)
|
|
6178
|
+
|
|
6179
|
+
\`\`\`
|
|
6180
|
+
sls-SII-Services ─┐
|
|
6181
|
+
sls-TGR-Extraction ├─► slsl-Data-Injection-Syncronizer ─► Pruff-server (per-tenant DBs)
|
|
6182
|
+
sls-Public-Market ─┘ │
|
|
6183
|
+
▼
|
|
6184
|
+
sls-Decision-Engine ──► Pruff-server
|
|
6185
|
+
│
|
|
6186
|
+
▼
|
|
6187
|
+
sls-Notification-Service / sls-Pruff-Webhooks (outbound)
|
|
6188
|
+
\`\`\`
|
|
6189
|
+
|
|
6190
|
+
\`sls-Pruff-Workers\` sits beside \`Pruff-server\` handling async/scheduled jobs that would block the API.
|
|
6191
|
+
|
|
6192
|
+
---
|
|
6193
|
+
|
|
6194
|
+
## What is Pruff
|
|
6195
|
+
|
|
6196
|
+
Pruff is a **multi-tenant B2B fintech platform** for Latin American financing institutions (factorings,
|
|
6197
|
+
credit companies, investment funds). It centralizes the full lifecycle of financial operations through
|
|
6198
|
+
three integrated modules:
|
|
6199
|
+
|
|
6200
|
+
\`\`\`
|
|
6201
|
+
DataHub (data ingestion) → Motor de Riesgo (risk/decision engine) → Core (operations)
|
|
6202
|
+
↑_________________________retroalimentación_________________________↓
|
|
6203
|
+
\`\`\`
|
|
6204
|
+
|
|
6205
|
+
- **DataHub** — Consolidates client, debtor, and market data from external sources (SII, TGR, Bureau, banks).
|
|
6206
|
+
- **Motor de Riesgo** — Applies configurable scoring rules, compliance checks, exposure limits, and alerts.
|
|
6207
|
+
- **Core (FinancingHub)** — Executes approved operations: cesiones, disbursements, payment schedules,
|
|
6208
|
+
collections, reconciliation, and double-entry accounting.
|
|
6209
|
+
|
|
6210
|
+
### Financial Products
|
|
6211
|
+
|
|
6212
|
+
| Product | What It Does |
|
|
6213
|
+
|---------|-------------|
|
|
6214
|
+
| **Factoring** | Client sells unpaid invoices for immediate liquidity. Core product. |
|
|
6215
|
+
| **Credits** | Structured financing with cuotas (installments) |
|
|
6216
|
+
| **Confirming** | Accelerated supplier payments (accounts payable) |
|
|
6217
|
+
| **Credit Lines** | Pre-approved revolving credit with partial draws |
|
|
6218
|
+
| **Funds** | Real-time visibility for investment funds over factoring portfolios |
|
|
6219
|
+
|
|
6220
|
+
---
|
|
6221
|
+
|
|
6222
|
+
## Multi-Tenancy Architecture
|
|
6223
|
+
|
|
6224
|
+
**Critical concept:** Pruff is **database-per-tenant**. Every tenant has its own MySQL database.
|
|
6225
|
+
|
|
6226
|
+
1. User authenticates via JWT → \`secretId\` extracted from company
|
|
6227
|
+
2. \`PrismaClientManager.getTenantClient(secretId)\` returns a tenant-scoped Prisma client
|
|
6228
|
+
3. **Every service that needs DB access receives \`prisma\` (tenant-scoped) as its first parameter**
|
|
6229
|
+
4. Use \`prismaGlobal\` only when the use case requires cross-tenant or shared data
|
|
6230
|
+
|
|
6231
|
+
---
|
|
6232
|
+
|
|
6233
|
+
## Pruff-server (Backend)
|
|
6234
|
+
|
|
6235
|
+
**Stack:** Node.js 22, Express 4, TypeScript 5, Prisma 5, MySQL 8, AWS (S3, SQS, SNS, Lambda, DynamoDB)
|
|
6236
|
+
**Package manager:** \`bun\` — \`bun.lock\` is the canonical lockfile. Use \`bun install\` / \`bun add <pkg>\`,
|
|
6237
|
+
never \`npm install\` or \`yarn\` (Docker/CI install with \`bun install --frozen-lockfile\`, so an
|
|
6238
|
+
npm/yarn-only change goes stale). \`npm run <script>\` still works for running scripts.
|
|
6239
|
+
|
|
6240
|
+
### Commands
|
|
6241
|
+
|
|
6242
|
+
\`\`\`bash
|
|
6243
|
+
cd Pruff-server
|
|
6244
|
+
|
|
6245
|
+
npm run local # Run locally (uses local.env)
|
|
6246
|
+
npm run dev # Run against dev environment (uses dev.env)
|
|
6247
|
+
npm run build # Compile TypeScript (tsc + tsc-alias)
|
|
6248
|
+
npm run test # Jest tests (uses test.env, sequential with 1 worker)
|
|
6249
|
+
npm run test:file <name> # Run single jest test by file name (not path)
|
|
6250
|
+
npm run lint # ESLint
|
|
6251
|
+
|
|
6252
|
+
# Database (Prisma)
|
|
6253
|
+
npm run db-update-local # prisma migrate dev (create + apply migration, local)
|
|
6254
|
+
npm run db-migrate-local # prisma migrate deploy (apply existing migrations, local)
|
|
6255
|
+
npm run db-migrate-tenants-local # Migrate all tenant databases
|
|
6256
|
+
npm run db-seed-tenants-local # Seed all active tenant databases
|
|
6257
|
+
\`\`\`
|
|
6258
|
+
|
|
6259
|
+
### Source Structure
|
|
6260
|
+
|
|
6261
|
+
\`\`\`
|
|
6262
|
+
src/
|
|
6263
|
+
controllers/ # Thin — validate input, call service, return response
|
|
6264
|
+
services/ # All business logic, organized by domain
|
|
6265
|
+
routes/
|
|
6266
|
+
api.ts # Main router
|
|
6267
|
+
internal/ # Staff endpoints (JWT auth)
|
|
6268
|
+
clients/ # Customer-facing endpoints (API key)
|
|
6269
|
+
external/ # Scraper/lambda endpoints (API key)
|
|
6270
|
+
public/ # No auth
|
|
6271
|
+
modules/ # Infrastructure wrappers (S3, SQS, SII, GoFirmex, etc.)
|
|
6272
|
+
middlewares/ # Auth, tenant resolution, rate limiting, error handling
|
|
6273
|
+
jobs/ # Cron jobs (iterate over all tenants)
|
|
6274
|
+
types/ # Shared TypeScript types
|
|
6275
|
+
lib/ # Utility functions
|
|
6276
|
+
\`\`\`
|
|
6277
|
+
|
|
6278
|
+
### API Tiers
|
|
6279
|
+
|
|
6280
|
+
| Tier | Base Route | Auth | Middleware |
|
|
6281
|
+
|------|-----------|------|-----------|
|
|
6282
|
+
| Internal | \`/api/internal\` | JWT | \`authenticateJWT\` + \`attachUserPermissions\` |
|
|
6283
|
+
| External | \`/api/external\` | API Key | \`authenticateWithApiKey\` |
|
|
6284
|
+
| Clients | \`/api/clients\` | Client API Key | \`authenticateClientsWithApiKey\` |
|
|
6285
|
+
| Public | \`/api/public\` | None | — |
|
|
6286
|
+
|
|
6287
|
+
### Service Pattern — Result\\<T\\>
|
|
6288
|
+
|
|
6289
|
+
All services return \`Result<T>\`. Never throw from services.
|
|
6290
|
+
|
|
6291
|
+
\`\`\`typescript
|
|
6292
|
+
type Result<T> = { ok: true; value: T } | { ok: false; error: AppError };
|
|
6293
|
+
|
|
6294
|
+
return { ok: true, value };
|
|
6295
|
+
return { ok: false, error: { status: APP_CODES.NOT_FOUND, message: 'NOT_FOUND', service: 'MyService', displayMessage: 'No encontrado' } };
|
|
6296
|
+
\`\`\`
|
|
6297
|
+
|
|
6298
|
+
### Controller Pattern — handleApiOutput / handleApiError
|
|
6299
|
+
|
|
6300
|
+
\`\`\`typescript
|
|
6301
|
+
async someAction(req: UserRequest, res: Response) {
|
|
6302
|
+
try {
|
|
6303
|
+
const result = await SomeService.execute(req.prisma, req.body);
|
|
6304
|
+
return handleApiOutput('SomeService', req, res, result);
|
|
6305
|
+
} catch (error) {
|
|
6306
|
+
handleApiError('SomeService', req, res, error);
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
\`\`\`
|
|
6310
|
+
|
|
6311
|
+
### Naming Conventions
|
|
6312
|
+
|
|
6313
|
+
- **Services:** \`PascalCase\` + \`Service\` suffix → \`CreateFactoringRequestService.ts\`
|
|
6314
|
+
- **DTOs/interfaces:** \`PascalCase\` + \`Dto\` suffix → \`CreateFactoringRequestDto\`
|
|
6315
|
+
- **Controllers:** \`camelCase\` + \`Controller\` suffix → \`invoicesController.ts\`
|
|
6316
|
+
- **Services are singletons:** \`export default new MyService();\`
|
|
6317
|
+
- **\`prisma\` is always the first parameter** when the service needs DB access
|
|
6318
|
+
|
|
6319
|
+
### Accounting Rules
|
|
6320
|
+
|
|
6321
|
+
- Every financial state change generates double-entry accounting entries automatically
|
|
6322
|
+
- \`SUM(debit) === SUM(credit)\` always — imbalanced entries are rejected
|
|
6323
|
+
- Entries use \`sourceKey\` to prevent duplicates and \`checkSum\` (SHA256) for audit integrity
|
|
6324
|
+
- Periods must be OPEN to accept entries
|
|
6325
|
+
|
|
6326
|
+
### Environment Files
|
|
6327
|
+
|
|
6328
|
+
- \`local.env\` — local MySQL, mocked AWS services
|
|
6329
|
+
- \`dev.env\` — connects to \`api.dev.pruff.com\` dependencies
|
|
6330
|
+
- \`test.env\` — for Jest tests
|
|
6331
|
+
|
|
6332
|
+
---
|
|
6333
|
+
|
|
6334
|
+
## Pruff-Client (Frontend)
|
|
6335
|
+
|
|
6336
|
+
**Stack:** React 18, TypeScript, Vite, MUI v5, Redux Toolkit, TanStack React Query v5, Axios
|
|
6337
|
+
**Package manager:** \`yarn\` — \`yarn.lock\` is the canonical lockfile.
|
|
6338
|
+
|
|
6339
|
+
### Commands
|
|
6340
|
+
|
|
6341
|
+
\`\`\`bash
|
|
6342
|
+
cd Pruff-Client
|
|
6343
|
+
|
|
6344
|
+
yarn start-local # Dev server → backend at http://localhost:80
|
|
6345
|
+
yarn start-dev # Dev server → backend at https://api.dev.pruff.com
|
|
6346
|
+
yarn start-prod # Dev server → backend at https://api.pruff.com
|
|
6347
|
+
yarn build # Production build
|
|
6348
|
+
yarn lint # ESLint
|
|
6349
|
+
yarn test # Jest + React Testing Library
|
|
6350
|
+
\`\`\`
|
|
6351
|
+
|
|
6352
|
+
### Source Structure
|
|
6353
|
+
|
|
6354
|
+
\`\`\`
|
|
6355
|
+
src/
|
|
6356
|
+
pages/ # Feature modules
|
|
6357
|
+
lib/
|
|
6358
|
+
api/ # Axios API integration modules
|
|
6359
|
+
designSystem/ # Shared MUI-based components
|
|
6360
|
+
mobileDesignSystem/ # Mobile-specific components
|
|
6361
|
+
common/ # Domain-specific shared components
|
|
6362
|
+
hooks/ # Custom React hooks
|
|
6363
|
+
functions/ # Business logic functions (utils.ts, permission checks)
|
|
6364
|
+
types/ # TypeScript type definitions
|
|
6365
|
+
services/ # Redux store + API configuration (requestHandler)
|
|
6366
|
+
layouts/ # Layout components
|
|
6367
|
+
assets/ # Images, fonts, icons
|
|
6368
|
+
\`\`\`
|
|
6369
|
+
|
|
6370
|
+
### State Management
|
|
6371
|
+
|
|
6372
|
+
- **React Query v5** — All server data (fetching, caching, mutations)
|
|
6373
|
+
- **Redux Toolkit** — Auth state, global alerts, client branding config only
|
|
6374
|
+
- **Redux Persist** — Persists auth across sessions
|
|
6375
|
+
|
|
6376
|
+
### API Client
|
|
6377
|
+
|
|
6378
|
+
All HTTP goes through \`requestHandler\` (Axios wrapper in \`src/lib/api.ts\`). Import from the relevant
|
|
6379
|
+
module in \`src/lib/api/<domain>/\`.
|
|
6380
|
+
|
|
6381
|
+
### Permission System
|
|
6382
|
+
|
|
6383
|
+
RBAC via \`user.roles[].permissions[].name\`. Key utilities in \`src/lib/functions/utils.ts\`:
|
|
6384
|
+
- \`validatePermissions(user, permission)\` — single check
|
|
6385
|
+
- \`hasAnyPermissions(user, permissions[])\` — OR logic
|
|
6386
|
+
- \`validateCompanyTypes(user, type)\` — product/feature gating
|
|
6387
|
+
|
|
6388
|
+
---
|
|
6389
|
+
|
|
6390
|
+
## Code Style (Both Repos)
|
|
6391
|
+
|
|
6392
|
+
- **Prettier:** \`printWidth: 80\`, \`singleQuote: true\`, \`trailingComma: 'all'\`, \`semi: true\`
|
|
6393
|
+
- **ESLint:** TypeScript strict, \`simple-import-sort\` enforced, \`no-console\` (warn/error)
|
|
6394
|
+
- **Import order** (enforced): external packages → absolute imports → relative imports, alphabetically sorted
|
|
6395
|
+
|
|
6396
|
+
---
|
|
6397
|
+
|
|
6398
|
+
## Chile-Specific Concepts
|
|
6399
|
+
|
|
6400
|
+
| Term | Meaning |
|
|
6401
|
+
|------|---------|
|
|
6402
|
+
| **RUT** | Chilean tax ID (e.g., \`12.345.678-9\`) |
|
|
6403
|
+
| **DTE** | Electronic tax document (33=factura, 34=exenta, 61=nota de crédito) |
|
|
6404
|
+
| **SII** | Servicio de Impuestos Internos — tax authority, source of invoices, F22, F29 |
|
|
6405
|
+
| **F22/F29** | Annual/monthly tax returns |
|
|
6406
|
+
| **TGR** | Tesorería General de la República — state treasury, tax debts |
|
|
6407
|
+
| **UF** | Unidad de Fomento — daily inflation-adjusted unit (CMF publishes value) |
|
|
6408
|
+
| **CMF** | Comisión para el Mercado Financiero — financial markets regulator |
|
|
6409
|
+
| **SGR** | Sociedad de Garantía Recíproca — mutual guarantee society for SMEs |
|
|
6410
|
+
| **Cedente** | Client selling receivables (factoring seller) |
|
|
6411
|
+
| **Deudor** | Invoice payer / debtor |
|
|
6412
|
+
| **Cesión** | Transfer of collection rights on an invoice |
|
|
6413
|
+
| **Giro/Desembolso** | Cash disbursement |
|
|
6414
|
+
| **Cobranza** | Collection management |
|
|
6415
|
+
| **Mora** | Payment delinquency |
|
|
6416
|
+
| **Conciliación** | Reconciling bank movements with operations |
|
|
6417
|
+
| **Provisión** | Recognized expected loss |
|
|
6418
|
+
| **Castigo** | Write-off of uncollectible amounts |
|
|
6419
|
+
| **Bureau** | Credit bureau with RUT-level payment history |
|
|
6420
|
+
| **Spread** | Profit margin between funding cost and lending rate |
|
|
6421
|
+
|
|
6422
|
+
---
|
|
6423
|
+
|
|
6424
|
+
## Pruff Knowledge
|
|
6425
|
+
|
|
6426
|
+
Durable, versioned knowledge (standards, architecture, how things work) lives in the \`Pruff Knowledge/\`
|
|
6427
|
+
repo cloned alongside this workspace by onboarding (\`Pruff-Tech/pruff-knowledge\`). Read it for context
|
|
6428
|
+
beyond this CLAUDE.md. Roadmap, releases, meeting notes, and tickets stay in Notion/Linear, not here.
|
|
6429
|
+
|
|
6430
|
+
**Start at a MOC** (\`_moc/\` — Maps of Content): the curated entry index per area. From there, follow the
|
|
6431
|
+
\`[[wikilinks]]\` to the concrete notes, or jump straight into the folder you need.
|
|
6432
|
+
|
|
6433
|
+
| Folder | What it covers |
|
|
6434
|
+
|---|---|
|
|
6435
|
+
| \`architecture/\` | Patterns and standards already implemented (SSE, error handling, multi-tenancy, database gateway…) |
|
|
6436
|
+
| \`infrastructure/\` | AWS services, ECS, Lambdas, and how they connect |
|
|
6437
|
+
| \`design-system/\` | UI components, tokens, and conventions |
|
|
6438
|
+
| \`operations/\` | Operational flow, modules, and how they fit together |
|
|
6439
|
+
| \`integrations/\` | Existing integrations and how they're used |
|
|
6440
|
+
| \`repos/\` | One doc per repository — what it does and how it connects |
|
|
6441
|
+
| \`guides/\` | How-tos (internal and external) |
|
|
6442
|
+
| \`glossary/\` | Domain terms — folio, cesión, RUT, mora, etc. |
|
|
6443
|
+
| \`postmortems/\` | Incidents — root cause and durable learnings (operational detail stays in Linear) |
|
|
6444
|
+
|
|
6445
|
+
Notes are frontmatter/filenames in English (\`kebab-case\`), body in Spanish. See the repo's own
|
|
6446
|
+
\`AGENTS.md\` for the agent-facing contract.
|
|
6447
|
+
|
|
6448
|
+
---
|
|
6449
|
+
|
|
6450
|
+
## Workflow
|
|
6451
|
+
|
|
6452
|
+
- **Always use \`/commit\`** — invoke the \`commit\` skill for all git commits. Never run \`git commit\` manually.
|
|
6453
|
+
- **Always use \`/create-pr\`** — invoke the \`create-pr\` skill for all pull requests. Never run \`gh pr create\` manually.
|
|
6454
|
+
- **Commits are authored to the human running Claude Code** — never set \`--author\` to Claude or any co-author.
|
|
6455
|
+
- **In \`sls-*/\` and \`slsl-*/\` repos, branch from \`origin/main\`** — verify this against each repo's actual
|
|
6456
|
+
\`dev\` vs \`main\` state before assuming; it has drifted before. Exception: \`Pruff-server\` and
|
|
6457
|
+
\`Pruff-Client\` branch from \`dev\` (their deploy flow promotes \`dev → main\`).
|
|
6458
|
+
- **Branch names embed the Linear issue code; PRs map 1:1 with Linear issues** — name branches so they
|
|
6459
|
+
contain the issue code (e.g. \`pru-1355-...\`) for Linear's git automation. When splitting work, plan
|
|
6460
|
+
the PR grouping first, create one Linear issue per PR (1:1), and stack dependent PRs
|
|
6461
|
+
(\`dev ← group-1 ← group-2 ← …\`) instead of one mega-PR. If more than one linear issue is in one PR, name of the branch
|
|
6462
|
+
should be the linear issue code of each issue.
|
|
6463
|
+
- **Be straightforward and concise** — lead with the core message, then the minimum supporting detail.
|
|
6464
|
+
Cut preamble and narration. When asked for a table or diagram, render real markdown, never a bullet
|
|
6465
|
+
list dressed up as one.
|
|
6466
|
+
- **Propose options and confirm before acting** — for any non-trivial design, format, or refactor choice,
|
|
6467
|
+
present alternatives with their tradeoffs in chat and wait for a pick before writing code or running
|
|
6468
|
+
commands. Gather context → confirm the plan → build.
|
|
6469
|
+
- **Explain the rationale, not just the diff** — when a change touches a non-obvious file (a new
|
|
6470
|
+
middleware, a \`Dockerfile\` edit, a cap/limit) or you're proposing an approach, explain in chat *why*
|
|
6471
|
+
it's needed and surface the cons/tradeoffs before adopting it.
|
|
6472
|
+
- **Numbers are verified facts, not estimates** — before stating a count, rate, latency, or cost, confirm
|
|
6473
|
+
it from the source (logs, metrics, a query) and cite the source + window inline. If you can't verify
|
|
6474
|
+
it, label it an estimate or omit it. Package-manager conventions, canonical branches, and other
|
|
6475
|
+
"what does this repo actually do" questions belong in this bucket too — check \`origin/main\`, not a
|
|
6476
|
+
possibly-stale local checkout or this file.
|
|
6477
|
+
- **Register out-of-scope findings in \`.claude/smell-findings/\`** — whenever you or a subagent spots a
|
|
6478
|
+
code smell, an unrelated bug, or an improvement opportunity outside the current task's scope, record
|
|
6479
|
+
it as a markdown file there instead of fixing it inline or dropping it.
|
|
6480
|
+
- Delegate context gathering to codebase-navigator agent if present, and implementation to implementer agent if present.
|
|
6481
|
+
|
|
6482
|
+
---
|
|
6483
|
+
|
|
6484
|
+
## Code Review
|
|
6485
|
+
|
|
6486
|
+
When reviewing PRs, always scope scans to the PR's changed files (never the whole repo). For
|
|
6487
|
+
react-doctor/diff tools, use PR-scoped diffs and manually verify risky logic.
|
|
6488
|
+
|
|
6489
|
+
---
|
|
6490
|
+
|
|
6491
|
+
## Critical Rules
|
|
6492
|
+
|
|
6493
|
+
1. **Every backend service takes \`prisma\` as its first parameter** — tenant isolation depends on it.
|
|
6494
|
+
Never import or use a global Prisma instance inside a service.
|
|
6495
|
+
2. **Services return \`Result<T>\`, never throw** — controllers handle the Result pattern.
|
|
6496
|
+
3. **Accounting entries must balance** — debit total must equal credit total.
|
|
6497
|
+
4. **Financial operations are fully auditable** — CloudWatch logs and approval chains must not be skipped.
|
|
6498
|
+
5. **SII/RUT data is sensitive** — Credentials are encrypted via \`encryptionModule\`.
|
|
6499
|
+
6. **Never modify \`prisma/schema.prisma\` without confirmation** — schema changes affect every tenant database.
|
|
6500
|
+
7. **Never run DB migrations automatically** — propose the change and let the developer run it.
|
|
6501
|
+
8. **Never push directly to \`dev\` or \`main\`** — all changes must go through a PR. No exceptions.
|
|
6502
|
+
`;
|
|
6503
|
+
const CONTROL_PLANE_PRESEED_SCRIPT = `import { PrismaClient } from '@prisma/client';
|
|
6504
|
+
|
|
6505
|
+
import seedRoles from './prisma/seeds/roles';
|
|
6506
|
+
import seedPermissions from './prisma/seeds/permissions';
|
|
6507
|
+
import seedRolesWithPermissions from './prisma/seeds/permissionsToRole';
|
|
6508
|
+
import seedCompanyTypes from './prisma/seeds/companyTypes';
|
|
6509
|
+
|
|
6510
|
+
const prisma = new PrismaClient();
|
|
6511
|
+
|
|
6512
|
+
async function main() {
|
|
6513
|
+
if ((await prisma.role.count()) === 0) await seedRoles();
|
|
6514
|
+
if ((await prisma.permission.count()) === 0) await seedPermissions();
|
|
6515
|
+
await seedRolesWithPermissions();
|
|
6516
|
+
await seedCompanyTypes();
|
|
6517
|
+
}
|
|
6518
|
+
|
|
6519
|
+
main()
|
|
6520
|
+
.catch((e) => {
|
|
6521
|
+
console.error(e);
|
|
6522
|
+
process.exit(1);
|
|
6523
|
+
})
|
|
6524
|
+
.finally(() => process.exit(0));
|
|
6525
|
+
`;
|
|
6526
|
+
const TENANT_USER_BACKFILL_SCRIPT = `import PrismaClientManager from './src/modules/prismaClient';
|
|
6527
|
+
import EncryptionModule from './src/modules/encryptionModule';
|
|
6528
|
+
|
|
6529
|
+
async function main() {
|
|
6530
|
+
const email = process.argv[2];
|
|
6531
|
+
const password = process.argv[3];
|
|
6532
|
+
const clientId = process.env.CLIENT_ID;
|
|
6533
|
+
if (!clientId) throw new Error('CLIENT_ID is required');
|
|
6534
|
+
|
|
6535
|
+
const tenantPrisma = PrismaClientManager.getGlobalClient();
|
|
6536
|
+
const existing = await tenantPrisma.user.findFirst({ where: { email } });
|
|
6537
|
+
if (existing) {
|
|
6538
|
+
console.log('tenant user already present: ' + email);
|
|
6539
|
+
return;
|
|
6540
|
+
}
|
|
6541
|
+
|
|
6542
|
+
const globalPrisma = PrismaClientManager.getGlobalClientFromTenantBase(clientId);
|
|
6543
|
+
const globalUser = await globalPrisma.user.findFirstOrThrow({ where: { email } });
|
|
6544
|
+
const encryptedPassword = await EncryptionModule.encrypt(password);
|
|
6545
|
+
|
|
6546
|
+
await tenantPrisma.user.create({
|
|
6547
|
+
data: {
|
|
6548
|
+
id: globalUser.id,
|
|
6549
|
+
email,
|
|
6550
|
+
name: email.split('@')[0],
|
|
6551
|
+
password: encryptedPassword,
|
|
6552
|
+
active: true,
|
|
6553
|
+
companyId: globalUser.companyId,
|
|
6554
|
+
roles: { connect: [{ id: 4 }] },
|
|
6555
|
+
},
|
|
6556
|
+
});
|
|
6557
|
+
console.log('tenant user created: ' + email + ' (id=' + globalUser.id + ')');
|
|
6558
|
+
}
|
|
6559
|
+
|
|
6560
|
+
main()
|
|
6561
|
+
.catch((e) => {
|
|
6562
|
+
console.error(e);
|
|
6563
|
+
process.exit(1);
|
|
6564
|
+
})
|
|
6565
|
+
.finally(() => process.exit(0));
|
|
6566
|
+
`;
|
|
6567
|
+
function renderLocalEnv(v) {
|
|
6568
|
+
const q = (value) => `"${value.replace(/"/g, "\\\"")}"`;
|
|
6569
|
+
return `DATABASE_URL=${q(`mysql://root@localhost:3306/pruff-local?connection_limit=5`)}
|
|
6570
|
+
NODE_ENV="local"
|
|
6571
|
+
IS_LOCAL_ENV="true"
|
|
6572
|
+
CLIENT_ID=${q(v.clientId)}
|
|
6573
|
+
CLIENT_RUT=${q(v.clientRut)}
|
|
6574
|
+
CLIENT_NAME=${q(v.clientName)}
|
|
6575
|
+
ADMIN_PASSWORD=${q(v.adminPassword)}
|
|
6576
|
+
JWT_SECRET=${q(v.jwtSecret)}
|
|
6577
|
+
API_KEY=${q(v.apiKey)}
|
|
6578
|
+
SECRET_SII_KEY=${q(v.siiKey)}
|
|
6579
|
+
SECRET_USER_REGISTRATION=${q(v.registrationSecret)}
|
|
6580
|
+
SECRET_PFX_CERTIFICATE=${q(v.pfxSecret)}
|
|
6581
|
+
USER_FORM_KEY=${q(v.userFormKey)}
|
|
6582
|
+
POSTHOG_ENABLED="false"
|
|
6583
|
+
|
|
6584
|
+
# --- Third-party integrations -------------------------------------------------
|
|
6585
|
+
# Left blank on purpose. Fill with real sandbox credentials from your team /
|
|
6586
|
+
# 1Password when you need the feature. The app boots and serves local data
|
|
6587
|
+
# without them.
|
|
6588
|
+
SII_SERVICE_URL=""
|
|
6589
|
+
SII_SERVICE_APIKEY=""
|
|
6590
|
+
TGR_SERVICE_URL=""
|
|
6591
|
+
TGR_SERVICE_APKEY=""
|
|
6592
|
+
AWS_ACCESS_KEY_ID=""
|
|
6593
|
+
AWS_SECRET_ACCESS_KEY=""
|
|
6594
|
+
POSTHOG_API_KEY=""
|
|
6595
|
+
POSTHOG_HOST=""
|
|
6596
|
+
BURO_USERNAME=""
|
|
6597
|
+
BURO_PASSWORD=""
|
|
6598
|
+
BURO_API_URL=""
|
|
6599
|
+
CMF_API_KEY=""
|
|
6600
|
+
MCP_OAUTH_PUBLIC_KEY=""
|
|
6601
|
+
MCP_ALLOWED_TENANTS=""
|
|
6602
|
+
`;
|
|
6603
|
+
}
|
|
6129
6604
|
const CLAUDE_CODE_PKG = "@anthropic-ai/claude-code";
|
|
6130
6605
|
const KNOWLEDGE_REPO = "https://github.com/Pruff-Tech/pruff-knowledge";
|
|
6131
6606
|
const KNOWLEDGE_DIR = "Pruff Knowledge";
|
|
6132
6607
|
const DEFAULT_WORKSPACE = join(homedir(), "pruff");
|
|
6133
6608
|
const MAIN_REPOS = [{
|
|
6134
6609
|
name: "Pruff-server",
|
|
6135
|
-
url: "https://github.com/Pruff-Tech/Pruff-server"
|
|
6610
|
+
url: "https://github.com/Pruff-Tech/Pruff-server",
|
|
6611
|
+
installCmd: "bun",
|
|
6612
|
+
installArgs: ["install"]
|
|
6136
6613
|
}, {
|
|
6137
6614
|
name: "Pruff-Client",
|
|
6138
|
-
url: "https://github.com/Pruff-Tech/Pruff-Client"
|
|
6615
|
+
url: "https://github.com/Pruff-Tech/Pruff-Client",
|
|
6616
|
+
installCmd: "yarn",
|
|
6617
|
+
installArgs: ["install"]
|
|
6139
6618
|
}];
|
|
6140
6619
|
const CORE_SKILLS = [
|
|
6141
6620
|
"commit",
|
|
@@ -6161,12 +6640,16 @@ async function runOnboarding(args) {
|
|
|
6161
6640
|
Ie(import_picocolors.default.bold("Pruff onboarding"));
|
|
6162
6641
|
installClaudeCode();
|
|
6163
6642
|
ensureObsidian();
|
|
6643
|
+
ensureBun();
|
|
6164
6644
|
const workspace = await resolveWorkspace(args);
|
|
6165
6645
|
mkdirSync(workspace, { recursive: true });
|
|
6166
6646
|
M.success(`Workspace ${import_picocolors.default.cyan(workspace)}`);
|
|
6167
6647
|
cloneKnowledge(workspace);
|
|
6168
|
-
for (const repo of MAIN_REPOS) cloneRepo(repo
|
|
6648
|
+
for (const repo of MAIN_REPOS) cloneRepo(repo, workspace);
|
|
6649
|
+
for (const repo of MAIN_REPOS) installDeps(repo, workspace);
|
|
6650
|
+
writeWorkspaceClaudeMd(workspace);
|
|
6169
6651
|
await installCatalog(workspace);
|
|
6652
|
+
if (await shouldSetupDatabase(args)) await setupLocalEnvironment(join(workspace, "Pruff-server"));
|
|
6170
6653
|
await showGuidance(workspace);
|
|
6171
6654
|
Se(`${import_picocolors.default.green("Done.")} Welcome to Pruff — happy building.`);
|
|
6172
6655
|
}
|
|
@@ -6176,12 +6659,30 @@ function has(cmd) {
|
|
|
6176
6659
|
stdio: "ignore"
|
|
6177
6660
|
}).status === 0;
|
|
6178
6661
|
}
|
|
6179
|
-
function run(cmd, args, cwd) {
|
|
6662
|
+
function run(cmd, args, cwd, env) {
|
|
6180
6663
|
return spawnSync(cmd, args, {
|
|
6181
6664
|
stdio: "inherit",
|
|
6182
|
-
cwd
|
|
6665
|
+
cwd,
|
|
6666
|
+
env: env ? {
|
|
6667
|
+
...process.env,
|
|
6668
|
+
...env
|
|
6669
|
+
} : void 0
|
|
6183
6670
|
}).status === 0;
|
|
6184
6671
|
}
|
|
6672
|
+
function capture(cmd, args, cwd, env) {
|
|
6673
|
+
const r = spawnSync(cmd, args, {
|
|
6674
|
+
encoding: "utf-8",
|
|
6675
|
+
cwd,
|
|
6676
|
+
env: env ? {
|
|
6677
|
+
...process.env,
|
|
6678
|
+
...env
|
|
6679
|
+
} : void 0
|
|
6680
|
+
});
|
|
6681
|
+
return {
|
|
6682
|
+
ok: r.status === 0,
|
|
6683
|
+
out: (r.stdout ?? "").trim()
|
|
6684
|
+
};
|
|
6685
|
+
}
|
|
6185
6686
|
function expandHome(input) {
|
|
6186
6687
|
return input.startsWith("~") ? join(homedir(), input.slice(1)) : input;
|
|
6187
6688
|
}
|
|
@@ -6223,6 +6724,19 @@ function ensureObsidian() {
|
|
|
6223
6724
|
M.warn(`Install manually: ${import_picocolors.default.cyan("brew install --cask obsidian")}`);
|
|
6224
6725
|
}
|
|
6225
6726
|
}
|
|
6727
|
+
function ensureBun() {
|
|
6728
|
+
if (has("bun")) {
|
|
6729
|
+
M.success("Bun already installed");
|
|
6730
|
+
return;
|
|
6731
|
+
}
|
|
6732
|
+
const s = Y();
|
|
6733
|
+
s.start("Installing Bun…");
|
|
6734
|
+
if (run("bash", ["-c", "curl -fsSL https://bun.sh/install | bash"])) s.stop("Bun installed");
|
|
6735
|
+
else {
|
|
6736
|
+
s.stop(import_picocolors.default.yellow("Bun install failed"));
|
|
6737
|
+
M.warn(`Install it manually: ${import_picocolors.default.cyan("curl -fsSL https://bun.sh/install | bash")} (or ${import_picocolors.default.cyan("brew install oven-sh/bun/bun")})`);
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6226
6740
|
async function resolveWorkspace(args) {
|
|
6227
6741
|
const fromArg = args.find((a) => !a.startsWith("-"));
|
|
6228
6742
|
if (fromArg) return expandHome(fromArg);
|
|
@@ -6266,7 +6780,8 @@ function cloneKnowledge(workspace) {
|
|
|
6266
6780
|
s.stop(import_picocolors.default.yellow("Knowledge clone failed"));
|
|
6267
6781
|
M.warn(`Could not clone ${import_picocolors.default.cyan(KNOWLEDGE_REPO)}.\n It's private — authenticate first with ${import_picocolors.default.cyan("gh auth login")} (or an SSH key), then re-run.`);
|
|
6268
6782
|
}
|
|
6269
|
-
function cloneRepo(
|
|
6783
|
+
function cloneRepo(repo, workspace) {
|
|
6784
|
+
const { name, url } = repo;
|
|
6270
6785
|
const target = join(workspace, name);
|
|
6271
6786
|
if (existsSync(join(target, ".git"))) {
|
|
6272
6787
|
M.success(`${name} already cloned`);
|
|
@@ -6284,6 +6799,33 @@ function cloneRepo(name, url, workspace) {
|
|
|
6284
6799
|
M.warn(`Could not clone ${name}. Authenticate with ${import_picocolors.default.cyan("gh auth login")} then run:\n ${import_picocolors.default.dim(`git clone ${url}`)} (inside ${workspace})`);
|
|
6285
6800
|
}
|
|
6286
6801
|
}
|
|
6802
|
+
function installDeps(repo, workspace) {
|
|
6803
|
+
const { name, installCmd, installArgs } = repo;
|
|
6804
|
+
const target = join(workspace, name);
|
|
6805
|
+
if (!existsSync(join(target, ".git"))) return;
|
|
6806
|
+
if (!has(installCmd)) {
|
|
6807
|
+
M.warn(`${import_picocolors.default.cyan(installCmd)} not found — skipping ${name} dependency install.\n Run manually once installed: ${import_picocolors.default.dim(`cd ${name} && ${installCmd} ${installArgs.join(" ")}`)}`);
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6810
|
+
const s = Y();
|
|
6811
|
+
s.start(`Installing ${name} dependencies (${installCmd})…`);
|
|
6812
|
+
const ok = run(installCmd, installArgs, target);
|
|
6813
|
+
if (ok && existsSync(join(target, "node_modules"))) s.stop(`${name} dependencies installed`);
|
|
6814
|
+
else {
|
|
6815
|
+
s.stop(import_picocolors.default.yellow(`${name} dependency install failed`));
|
|
6816
|
+
if (ok) M.warn(`${installCmd} exited 0 but ${name}/node_modules is missing — install likely no-opped.`);
|
|
6817
|
+
M.warn(`Run manually: ${import_picocolors.default.dim(`cd ${name} && ${installCmd} ${installArgs.join(" ")}`)}`);
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
function writeWorkspaceClaudeMd(workspace) {
|
|
6821
|
+
const target = join(workspace, "CLAUDE.md");
|
|
6822
|
+
if (existsSync(target)) {
|
|
6823
|
+
M.success("Workspace CLAUDE.md already present — leaving it untouched");
|
|
6824
|
+
return;
|
|
6825
|
+
}
|
|
6826
|
+
writeFileSync(target, WORKSPACE_CLAUDE_TEMPLATE, "utf-8");
|
|
6827
|
+
M.success("Wrote workspace CLAUDE.md");
|
|
6828
|
+
}
|
|
6287
6829
|
async function installCatalog(workspace) {
|
|
6288
6830
|
const prevCwd = process.cwd();
|
|
6289
6831
|
M.step("Installing Pruff rules, agents and skills into the workspace…");
|
|
@@ -6302,6 +6844,272 @@ async function installCatalog(workspace) {
|
|
|
6302
6844
|
process.chdir(prevCwd);
|
|
6303
6845
|
}
|
|
6304
6846
|
}
|
|
6847
|
+
async function setupLocalEnvironment(serverPath) {
|
|
6848
|
+
if (!existsSync(join(serverPath, ".git"))) {
|
|
6849
|
+
M.warn("Pruff-server not cloned — skipping local database setup.");
|
|
6850
|
+
return;
|
|
6851
|
+
}
|
|
6852
|
+
if (!ensureMysql()) return;
|
|
6853
|
+
ensureDbeaver();
|
|
6854
|
+
generateLocalEnv(serverPath);
|
|
6855
|
+
const env = parseEnvFile(join(serverPath, "local.env"));
|
|
6856
|
+
if (!env.DATABASE_URL || !env.CLIENT_ID) {
|
|
6857
|
+
M.warn("local.env is missing DATABASE_URL / CLIENT_ID — skipping database setup.");
|
|
6858
|
+
return;
|
|
6859
|
+
}
|
|
6860
|
+
setupLocalDatabase(serverPath, env);
|
|
6861
|
+
}
|
|
6862
|
+
async function shouldSetupDatabase(args) {
|
|
6863
|
+
if (args.includes("--no-db")) return false;
|
|
6864
|
+
if (args.includes("--db")) return true;
|
|
6865
|
+
if (!process.stdout.isTTY) return false;
|
|
6866
|
+
const answer = await ye({
|
|
6867
|
+
message: "Set up the local Pruff-server database now? (installs MySQL + DBeaver, migrates both DBs, seeds a local \"pruff\" tenant)",
|
|
6868
|
+
initialValue: true
|
|
6869
|
+
});
|
|
6870
|
+
if (pD(answer)) return false;
|
|
6871
|
+
return answer === true;
|
|
6872
|
+
}
|
|
6873
|
+
function ensureMysql() {
|
|
6874
|
+
if (!has("mysql")) {
|
|
6875
|
+
if (!has("brew")) {
|
|
6876
|
+
M.warn(`MySQL not found and Homebrew is unavailable.\n Install MySQL, then re-run: ${import_picocolors.default.cyan("npx pruff-skills onboarding --db")}`);
|
|
6877
|
+
return false;
|
|
6878
|
+
}
|
|
6879
|
+
const s = Y();
|
|
6880
|
+
s.start("Installing MySQL (brew)…");
|
|
6881
|
+
const ok = run("brew", ["install", "mysql"]);
|
|
6882
|
+
s.stop(ok ? "MySQL installed" : import_picocolors.default.yellow("MySQL install failed"));
|
|
6883
|
+
if (!ok) return false;
|
|
6884
|
+
} else M.success("MySQL already installed");
|
|
6885
|
+
if (mysqlRunning()) {
|
|
6886
|
+
M.success("MySQL already running");
|
|
6887
|
+
return true;
|
|
6888
|
+
}
|
|
6889
|
+
const s = Y();
|
|
6890
|
+
s.start("Starting MySQL…");
|
|
6891
|
+
run("brew", [
|
|
6892
|
+
"services",
|
|
6893
|
+
"start",
|
|
6894
|
+
"mysql"
|
|
6895
|
+
]);
|
|
6896
|
+
for (let i = 0; i < 30 && !mysqlRunning(); i++) spawnSync("sleep", ["1"]);
|
|
6897
|
+
const up = mysqlRunning();
|
|
6898
|
+
s.stop(up ? "MySQL running" : import_picocolors.default.yellow("MySQL did not come up"));
|
|
6899
|
+
if (!up) M.warn(`Start it manually: ${import_picocolors.default.cyan("brew services start mysql")}, then re-run with --db.`);
|
|
6900
|
+
return up;
|
|
6901
|
+
}
|
|
6902
|
+
function mysqlRunning() {
|
|
6903
|
+
return spawnSync("mysqladmin", [
|
|
6904
|
+
"ping",
|
|
6905
|
+
"-u",
|
|
6906
|
+
"root",
|
|
6907
|
+
"--silent"
|
|
6908
|
+
], { stdio: "ignore" }).status === 0;
|
|
6909
|
+
}
|
|
6910
|
+
function ensureDbeaver() {
|
|
6911
|
+
if (existsSync("/Applications/DBeaver.app")) {
|
|
6912
|
+
M.success("DBeaver already installed");
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6915
|
+
if (!has("brew")) {
|
|
6916
|
+
M.warn(`DBeaver not found. Install it from ${import_picocolors.default.cyan("https://dbeaver.io")}.`);
|
|
6917
|
+
return;
|
|
6918
|
+
}
|
|
6919
|
+
const s = Y();
|
|
6920
|
+
s.start("Installing DBeaver (brew)…");
|
|
6921
|
+
const ok = run("brew", [
|
|
6922
|
+
"install",
|
|
6923
|
+
"--cask",
|
|
6924
|
+
"dbeaver-community"
|
|
6925
|
+
]);
|
|
6926
|
+
s.stop(ok ? "DBeaver installed" : import_picocolors.default.yellow("DBeaver install failed"));
|
|
6927
|
+
}
|
|
6928
|
+
function generateLocalEnv(serverPath) {
|
|
6929
|
+
const target = join(serverPath, "local.env");
|
|
6930
|
+
if (existsSync(target)) {
|
|
6931
|
+
M.success("local.env already present — leaving it untouched");
|
|
6932
|
+
return;
|
|
6933
|
+
}
|
|
6934
|
+
const secret = () => randomBytes(16).toString("hex");
|
|
6935
|
+
writeFileSync(target, renderLocalEnv({
|
|
6936
|
+
clientId: "pruff",
|
|
6937
|
+
clientRut: "99.999.999-9",
|
|
6938
|
+
clientName: "Pruff Local",
|
|
6939
|
+
adminPassword: "pruff-local-1234",
|
|
6940
|
+
jwtSecret: secret(),
|
|
6941
|
+
apiKey: secret(),
|
|
6942
|
+
siiKey: secret(),
|
|
6943
|
+
registrationSecret: secret(),
|
|
6944
|
+
pfxSecret: secret(),
|
|
6945
|
+
userFormKey: secret()
|
|
6946
|
+
}), "utf-8");
|
|
6947
|
+
M.success("Wrote local.env (minimal bootable — fill third-party keys when needed)");
|
|
6948
|
+
}
|
|
6949
|
+
function parseEnvFile(path) {
|
|
6950
|
+
if (!existsSync(path)) return {};
|
|
6951
|
+
const out = {};
|
|
6952
|
+
for (const raw of readFileSync(path, "utf-8").split("\n")) {
|
|
6953
|
+
const line = raw.trim();
|
|
6954
|
+
if (!line || line.startsWith("#")) continue;
|
|
6955
|
+
const eq = line.indexOf("=");
|
|
6956
|
+
if (eq === -1) continue;
|
|
6957
|
+
const key = line.slice(0, eq).trim();
|
|
6958
|
+
let value = line.slice(eq + 1).trim();
|
|
6959
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
6960
|
+
out[key] = value;
|
|
6961
|
+
}
|
|
6962
|
+
return out;
|
|
6963
|
+
}
|
|
6964
|
+
function decideTenantAction(tenantDbExists, adminUserExists) {
|
|
6965
|
+
if (tenantDbExists && adminUserExists) return "already-provisioned";
|
|
6966
|
+
if (tenantDbExists) return "reset-and-seed";
|
|
6967
|
+
return "create-and-seed";
|
|
6968
|
+
}
|
|
6969
|
+
function parseMysqlUrl(url) {
|
|
6970
|
+
const u = new URL(url);
|
|
6971
|
+
return {
|
|
6972
|
+
host: u.hostname || "localhost",
|
|
6973
|
+
port: u.port || "3306",
|
|
6974
|
+
user: decodeURIComponent(u.username) || "root",
|
|
6975
|
+
password: u.password ? decodeURIComponent(u.password) : void 0
|
|
6976
|
+
};
|
|
6977
|
+
}
|
|
6978
|
+
function dbNameFromUrl(url) {
|
|
6979
|
+
return new URL(url).pathname.replace(/^\//, "");
|
|
6980
|
+
}
|
|
6981
|
+
function swapDbName(url, name) {
|
|
6982
|
+
const u = new URL(url);
|
|
6983
|
+
u.pathname = `/${name}`;
|
|
6984
|
+
return u.toString();
|
|
6985
|
+
}
|
|
6986
|
+
function mysqlArgs(conn) {
|
|
6987
|
+
const args = [
|
|
6988
|
+
"-h",
|
|
6989
|
+
conn.host,
|
|
6990
|
+
"-P",
|
|
6991
|
+
conn.port,
|
|
6992
|
+
"-u",
|
|
6993
|
+
conn.user
|
|
6994
|
+
];
|
|
6995
|
+
if (conn.password) args.push(`-p${conn.password}`);
|
|
6996
|
+
return args;
|
|
6997
|
+
}
|
|
6998
|
+
function mysqlQuery(conn, sql) {
|
|
6999
|
+
return capture("mysql", [
|
|
7000
|
+
...mysqlArgs(conn),
|
|
7001
|
+
"-N",
|
|
7002
|
+
"-B",
|
|
7003
|
+
"-e",
|
|
7004
|
+
sql
|
|
7005
|
+
]);
|
|
7006
|
+
}
|
|
7007
|
+
function mysqlExec(conn, sql) {
|
|
7008
|
+
return run("mysql", [
|
|
7009
|
+
...mysqlArgs(conn),
|
|
7010
|
+
"-e",
|
|
7011
|
+
sql
|
|
7012
|
+
]);
|
|
7013
|
+
}
|
|
7014
|
+
function dbExists(conn, name) {
|
|
7015
|
+
const { out } = mysqlQuery(conn, `SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${name}'`);
|
|
7016
|
+
return out.length > 0;
|
|
7017
|
+
}
|
|
7018
|
+
function tableExists(conn, db, table) {
|
|
7019
|
+
const { out } = mysqlQuery(conn, `SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '${db}' AND TABLE_NAME = '${table}'`);
|
|
7020
|
+
return out.length > 0;
|
|
7021
|
+
}
|
|
7022
|
+
function adminUserExists(conn, db, email) {
|
|
7023
|
+
if (!tableExists(conn, db, "User")) return false;
|
|
7024
|
+
const { ok, out } = mysqlQuery(conn, `SELECT COUNT(*) FROM \`${db}\`.\`User\` WHERE email = '${email}'`);
|
|
7025
|
+
return ok && parseInt(out, 10) > 0;
|
|
7026
|
+
}
|
|
7027
|
+
function migrateDeploy(serverPath, url) {
|
|
7028
|
+
return run("npx", [
|
|
7029
|
+
"prisma",
|
|
7030
|
+
"migrate",
|
|
7031
|
+
"deploy"
|
|
7032
|
+
], serverPath, { DATABASE_URL: url });
|
|
7033
|
+
}
|
|
7034
|
+
function runDisposableScript(serverPath, fileName, contents, env, extraArgs = []) {
|
|
7035
|
+
const scriptPath = join(serverPath, fileName);
|
|
7036
|
+
writeFileSync(scriptPath, contents, "utf-8");
|
|
7037
|
+
try {
|
|
7038
|
+
return run("npx", [
|
|
7039
|
+
"ts-node",
|
|
7040
|
+
"--transpile-only",
|
|
7041
|
+
"-r",
|
|
7042
|
+
"tsconfig-paths/register",
|
|
7043
|
+
scriptPath,
|
|
7044
|
+
...extraArgs
|
|
7045
|
+
], serverPath, env);
|
|
7046
|
+
} finally {
|
|
7047
|
+
rmSync(scriptPath, { force: true });
|
|
7048
|
+
}
|
|
7049
|
+
}
|
|
7050
|
+
function setupLocalDatabase(serverPath, env) {
|
|
7051
|
+
const controlPlaneUrl = env.DATABASE_URL;
|
|
7052
|
+
const clientId = env.CLIENT_ID;
|
|
7053
|
+
const adminPassword = env.ADMIN_PASSWORD;
|
|
7054
|
+
if (!controlPlaneUrl || !clientId || !adminPassword) {
|
|
7055
|
+
M.warn("local.env is missing DATABASE_URL / CLIENT_ID / ADMIN_PASSWORD — skipping database setup.");
|
|
7056
|
+
return;
|
|
7057
|
+
}
|
|
7058
|
+
const conn = parseMysqlUrl(controlPlaneUrl);
|
|
7059
|
+
const controlPlaneName = dbNameFromUrl(controlPlaneUrl);
|
|
7060
|
+
const tenantName = `tenant-${clientId}`;
|
|
7061
|
+
const tenantUrl = swapDbName(controlPlaneUrl, tenantName);
|
|
7062
|
+
const adminEmail = `admin@${clientId}.com`;
|
|
7063
|
+
M.step(`Setting up local databases (${controlPlaneName} + ${tenantName})…`);
|
|
7064
|
+
mysqlExec(conn, `CREATE DATABASE IF NOT EXISTS \`${controlPlaneName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
|
7065
|
+
if (!migrateDeploy(serverPath, controlPlaneUrl)) {
|
|
7066
|
+
M.warn("Control-plane migration failed — skipping the rest of database setup.");
|
|
7067
|
+
return;
|
|
7068
|
+
}
|
|
7069
|
+
runDisposableScript(serverPath, ".pruff-onboarding-preseed.ts", CONTROL_PLANE_PRESEED_SCRIPT, {
|
|
7070
|
+
...env,
|
|
7071
|
+
DATABASE_URL: controlPlaneUrl
|
|
7072
|
+
});
|
|
7073
|
+
const action = decideTenantAction(dbExists(conn, tenantName), adminUserExists(conn, tenantName, adminEmail));
|
|
7074
|
+
if (action === "already-provisioned") {
|
|
7075
|
+
M.success(`Tenant "${clientId}" already provisioned — skipping seed.`);
|
|
7076
|
+
printLoginHint(clientId, adminEmail, adminPassword);
|
|
7077
|
+
return;
|
|
7078
|
+
}
|
|
7079
|
+
if (action === "reset-and-seed") {
|
|
7080
|
+
M.warn(`Tenant "${clientId}" partially provisioned — resetting (Pruff-server seed is not idempotent).`);
|
|
7081
|
+
mysqlExec(conn, `DROP DATABASE IF EXISTS \`${tenantName}\``);
|
|
7082
|
+
}
|
|
7083
|
+
mysqlExec(conn, `CREATE DATABASE IF NOT EXISTS \`${tenantName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
|
7084
|
+
if (!migrateDeploy(serverPath, tenantUrl)) {
|
|
7085
|
+
M.warn("Tenant migration failed — skipping seed.");
|
|
7086
|
+
return;
|
|
7087
|
+
}
|
|
7088
|
+
const s = Y();
|
|
7089
|
+
s.start(`Seeding tenant "${clientId}"…`);
|
|
7090
|
+
const seeded = run("npx", [
|
|
7091
|
+
"prisma",
|
|
7092
|
+
"db",
|
|
7093
|
+
"seed"
|
|
7094
|
+
], serverPath, {
|
|
7095
|
+
...env,
|
|
7096
|
+
DATABASE_URL: tenantUrl
|
|
7097
|
+
});
|
|
7098
|
+
s.stop(seeded ? `Tenant "${clientId}" seeded` : import_picocolors.default.yellow("Tenant seed failed"));
|
|
7099
|
+
if (!seeded) return;
|
|
7100
|
+
if (!runDisposableScript(serverPath, ".pruff-onboarding-tenant-user.ts", TENANT_USER_BACKFILL_SCRIPT, {
|
|
7101
|
+
...env,
|
|
7102
|
+
DATABASE_URL: tenantUrl
|
|
7103
|
+
}, [adminEmail, adminPassword])) {
|
|
7104
|
+
M.warn("Tenant admin-user backfill failed — you may need to create the user manually.");
|
|
7105
|
+
return;
|
|
7106
|
+
}
|
|
7107
|
+
M.success(`Local tenant "${clientId}" ready.`);
|
|
7108
|
+
printLoginHint(clientId, adminEmail, adminPassword);
|
|
7109
|
+
}
|
|
7110
|
+
function printLoginHint(clientId, email, password) {
|
|
7111
|
+
M.info(`Log in locally with:\n ${import_picocolors.default.dim("secretId:")} ${import_picocolors.default.cyan(clientId)}\n ${import_picocolors.default.dim("email: ")} ${import_picocolors.default.cyan(email)}\n ${import_picocolors.default.dim("password:")} ${import_picocolors.default.cyan(password)}`);
|
|
7112
|
+
}
|
|
6305
7113
|
async function showGuidance(workspace) {
|
|
6306
7114
|
console.log();
|
|
6307
7115
|
await sweepLine("W O R K S P A C E R E A D Y", { slow: true });
|
|
@@ -6311,10 +7119,11 @@ async function showGuidance(workspace) {
|
|
|
6311
7119
|
line("");
|
|
6312
7120
|
line(`${import_picocolors.default.cyan(workspace)}/`);
|
|
6313
7121
|
const tree = [
|
|
7122
|
+
["CLAUDE.md", "workspace-level context — repo map, conventions, critical rules"],
|
|
6314
7123
|
[".claude/", "rules · agents · skills (apply to every repo below)"],
|
|
6315
7124
|
[`${KNOWLEDGE_DIR}/`, "Obsidian vault — open it in Obsidian"],
|
|
6316
|
-
["Pruff-server/", "backend"],
|
|
6317
|
-
["Pruff-Client/", "frontend"],
|
|
7125
|
+
["Pruff-server/", "backend (bun)"],
|
|
7126
|
+
["Pruff-Client/", "frontend (yarn)"],
|
|
6318
7127
|
["sls-*/", "serverless services — clone as you need them"]
|
|
6319
7128
|
];
|
|
6320
7129
|
const labelWidth = Math.max(...tree.map(([label]) => label.length)) + 2;
|
|
@@ -6326,9 +7135,8 @@ async function showGuidance(workspace) {
|
|
|
6326
7135
|
line(import_picocolors.default.bold("Next steps"));
|
|
6327
7136
|
line(`${import_picocolors.default.dim("1.")} Open ${import_picocolors.default.bold("Obsidian")} → the ${import_picocolors.default.cyan(KNOWLEDGE_DIR)} vault is registered and ready.`);
|
|
6328
7137
|
line(`${import_picocolors.default.dim("2.")} Start Claude Code from ${import_picocolors.default.cyan(workspace)} so the shared ${import_picocolors.default.dim(".claude/")} applies everywhere.`);
|
|
6329
|
-
line(`${import_picocolors.default.dim("3.")}
|
|
6330
|
-
line(`${import_picocolors.default.dim("4.")}
|
|
6331
|
-
line(`${import_picocolors.default.dim("5.")} Clone any serverless service into this folder with ${import_picocolors.default.dim("git clone <repo>")}.`);
|
|
7138
|
+
line(`${import_picocolors.default.dim("3.")} Dependencies are installed — ${import_picocolors.default.dim("cd Pruff-server && npm run local")} / ${import_picocolors.default.dim("cd Pruff-Client && yarn start-local")}.`);
|
|
7139
|
+
line(`${import_picocolors.default.dim("4.")} Clone any serverless service into this folder with ${import_picocolors.default.dim("git clone <repo>")}.`);
|
|
6332
7140
|
console.log();
|
|
6333
7141
|
}
|
|
6334
7142
|
function registerVault(vaultPath) {
|
|
@@ -6445,60 +7253,68 @@ async function showLogo(slow = false) {
|
|
|
6445
7253
|
process.stdout.write("\x1B[?25h");
|
|
6446
7254
|
}
|
|
6447
7255
|
}
|
|
6448
|
-
const
|
|
7256
|
+
const BUILDING_LINES = [
|
|
6449
7257
|
"██████╗ ██╗ ██╗██╗██╗ ██████╗ ██╗███╗ ██╗ ██████╗ ",
|
|
6450
7258
|
"██╔══██╗██║ ██║██║██║ ██╔══██╗██║████╗ ██║██╔════╝ ",
|
|
6451
7259
|
"██████╔╝██║ ██║██║██║ ██║ ██║██║██╔██╗ ██║██║ ███╗",
|
|
6452
7260
|
"██╔══██╗██║ ██║██║██║ ██║ ██║██║██║╚██╗██║██║ ██║",
|
|
6453
7261
|
"██████╔╝╚██████╔╝██║███████╗██████╔╝██║██║ ╚████║╚██████╔╝",
|
|
6454
7262
|
"╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ "
|
|
6455
|
-
]
|
|
7263
|
+
];
|
|
7264
|
+
const FINTECHS_LINES = [
|
|
6456
7265
|
"███████╗██╗███╗ ██╗████████╗███████╗ ██████╗██╗ ██╗███████╗",
|
|
6457
7266
|
"██╔════╝██║████╗ ██║╚══██╔══╝██╔════╝██╔════╝██║ ██║██╔════╝",
|
|
6458
7267
|
"█████╗ ██║██╔██╗ ██║ ██║ █████╗ ██║ ███████║███████╗",
|
|
6459
7268
|
"██╔══╝ ██║██║╚██╗██║ ██║ ██╔══╝ ██║ ██╔══██║╚════██║",
|
|
6460
7269
|
"██║ ██║██║ ╚████║ ██║ ███████╗╚██████╗██║ ██║███████║",
|
|
6461
7270
|
"╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝"
|
|
6462
|
-
]
|
|
6463
|
-
const TAGLINE_INDENT = "
|
|
6464
|
-
|
|
7271
|
+
];
|
|
7272
|
+
const TAGLINE_INDENT = "";
|
|
7273
|
+
const TAGLINE_ROWS = BUILDING_LINES.length + FINTECHS_LINES.length;
|
|
7274
|
+
const WHITES = [
|
|
7275
|
+
"\x1B[38;2;255;255;255m",
|
|
7276
|
+
"\x1B[38;2;228;230;236m",
|
|
7277
|
+
"\x1B[38;2;201;205;216m",
|
|
7278
|
+
"\x1B[38;2;174;180;196m",
|
|
7279
|
+
"\x1B[38;2;148;155;176m",
|
|
7280
|
+
"\x1B[38;2;122;131;156m"
|
|
7281
|
+
];
|
|
7282
|
+
function renderRevealRow(line, row, cols, fromLeft) {
|
|
6465
7283
|
const chars = Array.from(line);
|
|
6466
|
-
|
|
6467
|
-
let
|
|
6468
|
-
for (let c = 0; c <
|
|
6469
|
-
const
|
|
6470
|
-
|
|
6471
|
-
out += color;
|
|
6472
|
-
mode = color;
|
|
6473
|
-
}
|
|
6474
|
-
out += chars[c];
|
|
7284
|
+
const w = chars.length;
|
|
7285
|
+
let out = TAGLINE_INDENT + WHITES[Math.min(WHITES.length - 1, row)];
|
|
7286
|
+
for (let c = 0; c < w; c++) {
|
|
7287
|
+
const shown = fromLeft ? c < cols : c >= w - cols;
|
|
7288
|
+
out += shown ? chars[c] : " ";
|
|
6475
7289
|
}
|
|
6476
7290
|
return out + RESET;
|
|
6477
7291
|
}
|
|
6478
|
-
function
|
|
6479
|
-
|
|
7292
|
+
function renderTaglineReveal(cols) {
|
|
7293
|
+
const rows = [];
|
|
7294
|
+
BUILDING_LINES.forEach((line, row) => rows.push(renderRevealRow(line, row, cols, true)));
|
|
7295
|
+
FINTECHS_LINES.forEach((line, row) => rows.push(renderRevealRow(line, row, cols, false)));
|
|
7296
|
+
return rows.join("\n");
|
|
6480
7297
|
}
|
|
6481
7298
|
async function showTagline(slow = false) {
|
|
6482
7299
|
console.log();
|
|
6483
|
-
const rows =
|
|
6484
|
-
const width = Math.max(...
|
|
6485
|
-
const
|
|
6486
|
-
const
|
|
6487
|
-
const sweepMs = slow ? 34 : 18;
|
|
7300
|
+
const rows = TAGLINE_ROWS;
|
|
7301
|
+
const width = Math.max(...[...BUILDING_LINES, ...FINTECHS_LINES].map((l) => Array.from(l).length));
|
|
7302
|
+
const step = slow ? 2 : 3;
|
|
7303
|
+
const ms = slow ? 26 : 14;
|
|
6488
7304
|
if (!(!!process.stdout.isTTY && !process.env.CI && !process.env.NO_COLOR && !process.env.PRUFF_SKILLS_NO_ANIM)) {
|
|
6489
|
-
console.log(
|
|
7305
|
+
console.log(renderTaglineReveal(width));
|
|
6490
7306
|
return;
|
|
6491
7307
|
}
|
|
6492
7308
|
process.stdout.write("\x1B[?25l");
|
|
6493
7309
|
try {
|
|
6494
7310
|
let first = true;
|
|
6495
|
-
for (let
|
|
7311
|
+
for (let cols = 0; cols <= width; cols += step) {
|
|
6496
7312
|
if (!first) process.stdout.write(`\x1b[${rows}A`);
|
|
6497
|
-
process.stdout.write("\r" +
|
|
7313
|
+
process.stdout.write("\r" + renderTaglineReveal(cols) + "\n");
|
|
6498
7314
|
first = false;
|
|
6499
|
-
await sleep(
|
|
7315
|
+
await sleep(ms);
|
|
6500
7316
|
}
|
|
6501
|
-
process.stdout.write(`\x1b[${rows}A\r` +
|
|
7317
|
+
process.stdout.write(`\x1b[${rows}A\r` + renderTaglineReveal(width) + "\n");
|
|
6502
7318
|
if (slow) await sleep(500);
|
|
6503
7319
|
} finally {
|
|
6504
7320
|
process.stdout.write("\x1B[?25h");
|