pruff-skills 0.5.5 → 0.5.6

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.
@@ -1,5 +1,5 @@
1
1
  import { __toESM } from "../../rolldown-runtime.mjs";
2
- import { LD, RD, SD, _D, dD, fD, require_picocolors, require_src } from "./core.mjs";
2
+ import { LD, MD, RD, SD, _D, dD, fD, require_picocolors, require_src } from "./core.mjs";
3
3
  import { stripVTControlCharacters } from "node:util";
4
4
  import y from "node:process";
5
5
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
@@ -7,7 +7,7 @@ var import_src = require_src();
7
7
  function ce() {
8
8
  return y.platform !== "win32" ? y.env.TERM !== "linux" : !!y.env.CI || !!y.env.WT_SESSION || !!y.env.TERMINUS_SUBLIME || y.env.ConEmuTask === "{cmd::Cmder}" || y.env.TERM_PROGRAM === "Terminus-Sublime" || y.env.TERM_PROGRAM === "vscode" || y.env.TERM === "xterm-256color" || y.env.TERM === "alacritty" || y.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
9
9
  }
10
- const V = ce(), u = (t, n) => V ? t : n, le = u("◆", "*"), L = u("■", "x"), W = u("▲", "x"), C = u("◇", "o"), ue = u("┌", "T"), o = u("│", "|"), d = u("└", "—"), k = u("●", ">"), P = u("○", " "), A = u("◻", "[•]"), T = u("◼", "[+]"), F = u("◻", "[ ]"), _ = u("─", "-"), me = u("╮", "+"), de = u("├", "+"), pe = u("╯", "+"), q = u("●", "•"), D = u("◆", "*"), U = u("▲", "!"), K = u("■", "x"), b = (t) => {
10
+ const V = ce(), u = (t, n) => V ? t : n, le = u("◆", "*"), L = u("■", "x"), W = u("▲", "x"), C = u("◇", "o"), ue = u("┌", "T"), o = u("│", "|"), d = u("└", "—"), k = u("●", ">"), P = u("○", " "), A = u("◻", "[•]"), T = u("◼", "[+]"), F = u("◻", "[ ]"), $e = u("▪", "•"), _ = u("─", "-"), me = u("╮", "+"), de = u("├", "+"), pe = u("╯", "+"), q = u("●", "•"), D = u("◆", "*"), U = u("▲", "!"), K = u("■", "x"), b = (t) => {
11
11
  switch (t) {
12
12
  case "initial":
13
13
  case "active": return import_picocolors.default.cyan(le);
@@ -43,6 +43,26 @@ ${import_picocolors.default.yellow(d)} ${import_picocolors.default.yellow(this.
43
43
  ${import_picocolors.default.gray(o)}` : ""}`;
44
44
  default: return `${n}${import_picocolors.default.cyan(o)} ${i}
45
45
  ${import_picocolors.default.cyan(d)}
46
+ `;
47
+ }
48
+ }
49
+ }).prompt(), ge = (t) => new MD({
50
+ validate: t.validate,
51
+ mask: t.mask ?? $e,
52
+ render() {
53
+ const n = `${import_picocolors.default.gray(o)}
54
+ ${b(this.state)} ${t.message}
55
+ `, r = this.valueWithCursor, i = this.masked;
56
+ switch (this.state) {
57
+ case "error": return `${n.trim()}
58
+ ${import_picocolors.default.yellow(o)} ${i}
59
+ ${import_picocolors.default.yellow(d)} ${import_picocolors.default.yellow(this.error)}
60
+ `;
61
+ case "submit": return `${n}${import_picocolors.default.gray(o)} ${import_picocolors.default.dim(i)}`;
62
+ case "cancel": return `${n}${import_picocolors.default.gray(o)} ${import_picocolors.default.strikethrough(import_picocolors.default.dim(i ?? ""))}${i ? `
63
+ ${import_picocolors.default.gray(o)}` : ""}`;
64
+ default: return `${n}${import_picocolors.default.cyan(o)} ${r}
65
+ ${import_picocolors.default.cyan(d)}
46
66
  `;
47
67
  }
48
68
  }
@@ -351,4 +371,4 @@ ${J}${i.trimStart()}`), r = 3 + stripVTControlCharacters(i.trimStart()).length);
351
371
  }
352
372
  };
353
373
  };
354
- export { Ie, M, Me, Se, Y, be, fe, he, ve, xe, ye };
374
+ export { Ie, M, Me, Se, Y, be, fe, ge, he, ve, xe, ye };
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { __toESM } from "./_chunks/rolldown-runtime.mjs";
3
3
  import { pD, require_picocolors } from "./_chunks/libs/@clack/core.mjs";
4
- import { Ie, M, Me, Se, Y, be, fe, he, ve, xe, ye } from "./_chunks/libs/@clack/prompts.mjs";
4
+ import { Ie, M, Me, Se, Y, be, fe, ge, he, ve, xe, ye } from "./_chunks/libs/@clack/prompts.mjs";
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";
@@ -6562,6 +6562,9 @@ react-doctor/diff tools, use PR-scoped diffs and manually verify risky logic.
6562
6562
  7. **Never run DB migrations automatically** — propose the change and let the developer run it.
6563
6563
  8. **Never push directly to \`dev\` or \`main\`** — all changes must go through a PR. No exceptions.
6564
6564
  `;
6565
+ const CONTROL_PLANE_DB = "pruff-local";
6566
+ const INTELLIGENCE_DB = "pruff_intelligence";
6567
+ const FRONTEND_URL = "http://localhost:3001";
6565
6568
  const CONTROL_PLANE_PRESEED_SCRIPT = `import { PrismaClient } from '@prisma/client';
6566
6569
 
6567
6570
  import seedRoles from './prisma/seeds/roles';
@@ -6578,6 +6581,22 @@ async function main() {
6578
6581
  await seedCompanyTypes();
6579
6582
  }
6580
6583
 
6584
+ main()
6585
+ .catch((e) => {
6586
+ console.error(e);
6587
+ process.exit(1);
6588
+ })
6589
+ .finally(() => process.exit(0));
6590
+ `;
6591
+ const RUT_CONFLICT_PROBE_SCRIPT = `import PrismaClientManager from './src/modules/prismaClient';
6592
+
6593
+ async function main() {
6594
+ const rut = process.argv[2];
6595
+ const prisma = PrismaClientManager.getGlobalClient();
6596
+ const company = await prisma.company.findFirst({ where: { taxIdentifier: rut } });
6597
+ console.log('RUT_HOLDER=' + (company ? company.secretId : ''));
6598
+ }
6599
+
6581
6600
  main()
6582
6601
  .catch((e) => {
6583
6602
  console.error(e);
@@ -6588,6 +6607,8 @@ main()
6588
6607
  const TENANT_USER_BACKFILL_SCRIPT = `import PrismaClientManager from './src/modules/prismaClient';
6589
6608
  import EncryptionModule from './src/modules/encryptionModule';
6590
6609
 
6610
+ const SUPER_ADMIN_ROLE_ID = 4;
6611
+
6591
6612
  async function main() {
6592
6613
  const email = process.argv[2];
6593
6614
  const password = process.argv[3];
@@ -6596,8 +6617,15 @@ async function main() {
6596
6617
 
6597
6618
  const tenantPrisma = PrismaClientManager.getGlobalClient();
6598
6619
  const existing = await tenantPrisma.user.findFirst({ where: { email } });
6620
+
6599
6621
  if (existing) {
6600
- console.log('tenant user already present: ' + email);
6622
+ // create-intel-tenant-local resets tenant roles to Administrador only;
6623
+ // reconnect Super Administrador so the local admin keeps full access.
6624
+ await tenantPrisma.user.update({
6625
+ where: { id: existing.id },
6626
+ data: { roles: { connect: [{ id: SUPER_ADMIN_ROLE_ID }] } },
6627
+ });
6628
+ console.log('tenant user present: ' + email + ' (super-admin role ensured)');
6601
6629
  return;
6602
6630
  }
6603
6631
 
@@ -6613,7 +6641,7 @@ async function main() {
6613
6641
  password: encryptedPassword,
6614
6642
  active: true,
6615
6643
  companyId: globalUser.companyId,
6616
- roles: { connect: [{ id: 4 }] },
6644
+ roles: { connect: [{ id: SUPER_ADMIN_ROLE_ID }] },
6617
6645
  },
6618
6646
  });
6619
6647
  console.log('tenant user created: ' + email + ' (id=' + globalUser.id + ')');
@@ -6626,11 +6654,168 @@ main()
6626
6654
  })
6627
6655
  .finally(() => process.exit(0));
6628
6656
  `;
6657
+ const ACCOUNTING_PERIOD_SCRIPT = `import PrismaClientManager from './src/modules/prismaClient';
6658
+
6659
+ async function main() {
6660
+ const year = Number(process.argv[2]);
6661
+ const startDate = new Date(Date.UTC(year, 0, 1, 0, 0, 0));
6662
+ const endDate = new Date(Date.UTC(year, 11, 31, 23, 59, 59));
6663
+
6664
+ const prisma = PrismaClientManager.getGlobalClient();
6665
+ const overlap = await prisma.accountingPeriod.findFirst({
6666
+ where: { startDate: { lte: endDate }, endDate: { gte: startDate } },
6667
+ });
6668
+ if (overlap) {
6669
+ console.log('accounting period already covers ' + year + ': ' + overlap.label);
6670
+ return;
6671
+ }
6672
+
6673
+ const period = await prisma.accountingPeriod.create({
6674
+ data: { startDate, endDate, label: 'Año ' + year, status: 'OPEN' },
6675
+ });
6676
+ console.log('accounting period opened: ' + period.label + ' (id=' + period.id + ')');
6677
+ }
6678
+
6679
+ main()
6680
+ .catch((e) => {
6681
+ console.error(e);
6682
+ process.exit(1);
6683
+ })
6684
+ .finally(() => process.exit(0));
6685
+ `;
6686
+ const MIGRATE_TENANTS_LOCAL_SCRIPT = `import { exec } from 'child_process';
6687
+ import dotenv from 'dotenv';
6688
+ import PrismaClientManager from '../src/modules/prismaClient';
6689
+ const env = 'local.env';
6690
+ dotenv.config({ path: env });
6691
+
6692
+ async function getActiveTenants() {
6693
+ const prismaGlobal = PrismaClientManager.getGlobalClient();
6694
+ const companies = await prismaGlobal.company.findMany({
6695
+ where: { tenant: true },
6696
+ select: { secretId: true },
6697
+ });
6698
+ console.log('Companies:', companies.length);
6699
+ return companies.map((company) => company.secretId);
6700
+ }
6701
+
6702
+ function migrate(label: string, databaseUrl: string) {
6703
+ return new Promise((resolve, reject) => {
6704
+ const command = \`DATABASE_URL="\${databaseUrl}" npx prisma migrate deploy\`;
6705
+ console.log(\`Running command: \${command}\`);
6706
+ exec(command, (error, stdout, stderr) => {
6707
+ if (error) {
6708
+ console.error(\`Error migrating \${label}:\`, error);
6709
+ reject(error);
6710
+ return;
6711
+ }
6712
+ if (stderr) {
6713
+ console.error(\`Stderr for \${label}:\`, stderr);
6714
+ reject(new Error(stderr));
6715
+ return;
6716
+ }
6717
+ console.log(\`Migration output for \${label}:\`, stdout);
6718
+ resolve(stdout);
6719
+ });
6720
+ });
6721
+ }
6722
+
6723
+ async function migrateTenants() {
6724
+ console.log('Migrating main database');
6725
+ try {
6726
+ await migrate('local database', process.env.DATABASE_URL!);
6727
+ } catch (error) {
6728
+ console.error('Failed to migrate local database:', error);
6729
+ }
6730
+
6731
+ try {
6732
+ const tenantIds = await getActiveTenants();
6733
+ for (const tenantId of tenantIds) {
6734
+ console.log(\`Migrating tenant: \${tenantId}\`);
6735
+ const databaseUrl = process.env.DATABASE_URL!.replace(
6736
+ '/${CONTROL_PLANE_DB}',
6737
+ \`/tenant-\${tenantId}\`,
6738
+ );
6739
+ try {
6740
+ await migrate(\`tenant \${tenantId}\`, databaseUrl);
6741
+ } catch (error) {
6742
+ console.error(\`Failed to migrate tenant \${tenantId}:\`, error);
6743
+ }
6744
+ }
6745
+ } catch (error) {
6746
+ console.error('Error fetching tenants or migrating:', error);
6747
+ }
6748
+ }
6749
+
6750
+ migrateTenants();
6751
+ `;
6752
+ function mysqlUrl(dbPassword, db, query = "") {
6753
+ return `mysql://${dbPassword ? `root:${encodeURIComponent(dbPassword)}` : "root"}@localhost:3306/${db}${query}`;
6754
+ }
6755
+ const TEAM_PROVIDED_ENV = [
6756
+ {
6757
+ keys: [
6758
+ "AWS_ACCESS_KEY_ID",
6759
+ "AWS_SECRET_ACCESS_KEY",
6760
+ "PRUFF_PRIVATE_BUCKET"
6761
+ ],
6762
+ unlocks: "S3 — every document upload/download and generated PDF (referenced in ~50 places). Nothing file-related works without it. Ask for a dev IAM user, or run the `localstack` skill for a local S3."
6763
+ },
6764
+ {
6765
+ keys: [
6766
+ "SII_SERVICE_URL",
6767
+ "SII_SERVICE_APIKEY",
6768
+ "SII_QUERY_IDENTIFIER",
6769
+ "SII_ENCRYPTED_PASSWORD"
6770
+ ],
6771
+ unlocks: "SII scrapers — invoices, F29/F22, cesiones, certificate validation."
6772
+ },
6773
+ {
6774
+ keys: ["TGR_SERVICE_URL", "TGR_SERVICE_APKEY"],
6775
+ unlocks: "TGR — tax debts, collections, payment agreements."
6776
+ },
6777
+ {
6778
+ keys: ["EXTERNAL_DATABASE_URL", "EXTERNAL_DATABASE_APIKEY"],
6779
+ unlocks: "Bankruptcy / Rutificator / DepartmentOfLabor lookups used by Motor de Riesgo."
6780
+ },
6781
+ {
6782
+ keys: [
6783
+ "BURO_USERNAME",
6784
+ "BURO_PASSWORD",
6785
+ "BURO_API_URL",
6786
+ "CMF_API_KEY"
6787
+ ],
6788
+ unlocks: "Credit bureau + CMF risk data."
6789
+ },
6790
+ {
6791
+ keys: ["DECISION_ENGINE_SERVICE_URL", "DECISION_ENGINE_SERVICE_APIKEY"],
6792
+ unlocks: "Remote Motor de Riesgo. Local runs use EXECUTE_LOCAL_DECISION_ENGINE=true instead."
6793
+ },
6794
+ {
6795
+ keys: [
6796
+ "NOTIFICATION_SERVICE_URL",
6797
+ "EVENT_TOPIC_ARN",
6798
+ "WEBHOOK_DELIVERY_QUEUE_URL"
6799
+ ],
6800
+ unlocks: "Outbound email / events / webhooks."
6801
+ },
6802
+ {
6803
+ keys: ["PRUFF_AI_URL", "PRUFF_AI_INTERNAL_SERVICE_TOKEN"],
6804
+ unlocks: "Pruff AI features."
6805
+ }
6806
+ ];
6629
6807
  function renderLocalEnv(v) {
6630
6808
  const q = (value) => `"${value.replace(/"/g, "\\\"")}"`;
6631
- return `DATABASE_URL=${q(`mysql://root@localhost:3306/pruff-local?connection_limit=5`)}
6809
+ const teamProvided = TEAM_PROVIDED_ENV.flatMap((group) => [
6810
+ `# ${group.unlocks}`,
6811
+ ...group.keys.map((key) => `${key}=""`),
6812
+ ""
6813
+ ]).join("\n");
6814
+ return `DATABASE_URL=${q(mysqlUrl(v.dbPassword, CONTROL_PLANE_DB, "?connection_limit=5"))}
6815
+ INTELLIGENCE_DATABASE_URL=${q(mysqlUrl(v.dbPassword, INTELLIGENCE_DB))}
6632
6816
  NODE_ENV="local"
6633
6817
  IS_LOCAL_ENV="true"
6818
+ FRONTEND_URL=${q(FRONTEND_URL)}
6634
6819
  CLIENT_ID=${q(v.clientId)}
6635
6820
  CLIENT_RUT=${q(v.clientRut)}
6636
6821
  CLIENT_NAME=${q(v.clientName)}
@@ -6641,29 +6826,68 @@ SECRET_SII_KEY=${q(v.siiKey)}
6641
6826
  SECRET_USER_REGISTRATION=${q(v.registrationSecret)}
6642
6827
  SECRET_PFX_CERTIFICATE=${q(v.pfxSecret)}
6643
6828
  USER_FORM_KEY=${q(v.userFormKey)}
6829
+ EXECUTE_LOCAL_DECISION_ENGINE="true"
6644
6830
  POSTHOG_ENABLED="false"
6645
-
6646
- # --- Third-party integrations -------------------------------------------------
6647
- # Left blank on purpose. Fill with real sandbox credentials from your team /
6648
- # 1Password when you need the feature. The app boots and serves local data
6649
- # without them.
6650
- SII_SERVICE_URL=""
6651
- SII_SERVICE_APIKEY=""
6652
- TGR_SERVICE_URL=""
6653
- TGR_SERVICE_APKEY=""
6654
- AWS_ACCESS_KEY_ID=""
6655
- AWS_SECRET_ACCESS_KEY=""
6656
- POSTHOG_API_KEY=""
6657
- POSTHOG_HOST=""
6658
- BURO_USERNAME=""
6659
- BURO_PASSWORD=""
6660
- BURO_API_URL=""
6661
- CMF_API_KEY=""
6662
6831
  MCP_OAUTH_PUBLIC_KEY=""
6663
6832
  MCP_ALLOWED_TENANTS=""
6664
- `;
6833
+
6834
+ # --- Ask your team for these --------------------------------------------------
6835
+ # Left blank on purpose: real credentials, not something onboarding can generate.
6836
+ # The app boots and serves local data without them; each block notes what it
6837
+ # unlocks. Onboarding prints this same list when it finishes.
6838
+
6839
+ ${teamProvided}`;
6665
6840
  }
6841
+ const WORKSPACE_MCP_JSON = `${JSON.stringify({ mcpServers: {
6842
+ "linear-server": {
6843
+ type: "http",
6844
+ url: "https://mcp.linear.app/mcp"
6845
+ },
6846
+ posthog: {
6847
+ type: "http",
6848
+ url: "https://mcp.posthog.com/mcp"
6849
+ }
6850
+ } }, null, 2)}\n`;
6851
+ const WORKSPACE_SETTINGS_JSON = `${JSON.stringify({ permissions: { allow: [
6852
+ "Bash(npx tsc --noEmit*)",
6853
+ "Bash(./node_modules/.bin/tsc --noEmit*)",
6854
+ "Bash(npm run type-check)",
6855
+ "Bash(npm run lint)",
6856
+ "Bash(yarn lint*)",
6857
+ "Bash(git fetch *)",
6858
+ "Bash(git status*)",
6859
+ "Bash(git diff*)",
6860
+ "Bash(git log*)",
6861
+ "Bash(gh pr view:*)",
6862
+ "Bash(gh pr diff:*)",
6863
+ "Bash(gh pr list:*)",
6864
+ "Bash(gh run list:*)",
6865
+ "Bash(gh run view:*)",
6866
+ "Bash(aws sts get-caller-identity*)",
6867
+ "Bash(aws sqs get-queue-attributes*)",
6868
+ "Bash(aws sqs list-queues*)",
6869
+ "Bash(aws cloudwatch get-metric-statistics*)",
6870
+ "Bash(aws cloudwatch list-metrics*)",
6871
+ "Bash(aws logs describe-log-groups*)",
6872
+ "Bash(aws logs filter-log-events*)",
6873
+ "Bash(aws logs get-log-events*)",
6874
+ "Bash(aws logs start-query*)",
6875
+ "Bash(aws logs get-query-results*)",
6876
+ "Bash(npm view *)",
6877
+ "Bash(npm whoami*)",
6878
+ "mcp__linear-server__get_issue",
6879
+ "mcp__linear-server__list_issues",
6880
+ "mcp__linear-server__list_comments",
6881
+ "mcp__linear-server__search_documentation",
6882
+ "mcp__linear-server__list_teams",
6883
+ "mcp__linear-server__list_projects",
6884
+ "mcp__linear-server__list_milestones",
6885
+ "mcp__linear-server__get_project"
6886
+ ] } }, null, 2)}\n`;
6666
6887
  const CLAUDE_CODE_PKG = "@anthropic-ai/claude-code";
6888
+ const INTELLIGENCE_SCHEMA = "prisma/intelligence/schema.prisma";
6889
+ const INTELLIGENCE_GLOBAL_SEED = "prisma/intelligence/seeds/seedIntelligenceGlobal.ts";
6890
+ const INTELLIGENCE_TENANT_SCRIPT = "scripts/intelligence/createIntelligenceTenant.ts";
6667
6891
  const KNOWLEDGE_REPO = "https://github.com/Pruff-Tech/pruff-knowledge";
6668
6892
  const KNOWLEDGE_DIR = "Pruff Knowledge";
6669
6893
  const DEFAULT_WORKSPACE = join(homedir(), "pruff");
@@ -6683,23 +6907,36 @@ const CORE_SKILLS = [
6683
6907
  "create-pr",
6684
6908
  "linear",
6685
6909
  "start-issue",
6910
+ "update-linear-issues",
6686
6911
  "handoff",
6687
6912
  "pruff-logs",
6688
6913
  "pruff-pr-review",
6689
6914
  "review-pr-comments",
6690
- "react-doctor"
6915
+ "react-doctor",
6916
+ "pruff-knowledge",
6917
+ "find-skills",
6918
+ "localstack"
6691
6919
  ];
6692
6920
  const CORE_AGENTS = [
6693
6921
  "codebase-navigator",
6694
6922
  "db-query-reviewer",
6695
6923
  "pruff-logs-investigator"
6696
6924
  ];
6925
+ const issues = [];
6926
+ function note(issue) {
6927
+ issues.push(issue);
6928
+ }
6697
6929
  async function runOnboarding(args) {
6698
6930
  if (process.platform !== "darwin") {
6699
6931
  M.error(`${import_picocolors.default.bold("onboarding")} currently supports macOS only (detected ${import_picocolors.default.cyan(process.platform)}).`);
6700
6932
  return;
6701
6933
  }
6702
6934
  Ie(import_picocolors.default.bold("Pruff onboarding"));
6935
+ issues.length = 0;
6936
+ if (!await preflight()) {
6937
+ Se(import_picocolors.default.yellow("Onboarding stopped — fix the items above and re-run."));
6938
+ return;
6939
+ }
6703
6940
  installClaudeCode();
6704
6941
  ensureObsidian();
6705
6942
  ensureBun();
@@ -6710,10 +6947,70 @@ async function runOnboarding(args) {
6710
6947
  for (const repo of MAIN_REPOS) cloneRepo(repo, workspace);
6711
6948
  for (const repo of MAIN_REPOS) installDeps(repo, workspace);
6712
6949
  writeWorkspaceClaudeMd(workspace);
6950
+ writeHarnessConfig(workspace);
6713
6951
  await installCatalog(workspace);
6714
6952
  if (await shouldSetupDatabase(args)) await setupLocalEnvironment(join(workspace, "Pruff-server"));
6715
6953
  await showGuidance(workspace);
6716
- Se(`${import_picocolors.default.green("Done.")} Welcome to Pruff — happy building.`);
6954
+ showTeamCredentialsAsk(workspace);
6955
+ showIssues();
6956
+ if (issues.length === 0) Se(`${import_picocolors.default.green("Done.")} Welcome to Pruff — happy building.`);
6957
+ else Se(`${import_picocolors.default.yellow(`Finished with ${issues.length} issue${issues.length === 1 ? "" : "s"}.`)} Fix them and re-run — every step is idempotent.`);
6958
+ }
6959
+ async function preflight() {
6960
+ const missing = [
6961
+ "git",
6962
+ "node",
6963
+ "npm"
6964
+ ].filter((bin) => !has(bin));
6965
+ if (missing.length > 0) {
6966
+ M.error(`Missing required tool${missing.length === 1 ? "" : "s"}: ${import_picocolors.default.cyan(missing.join(", "))}.\n Install Xcode command line tools (${import_picocolors.default.dim("xcode-select --install")}) and Node 22 (${import_picocolors.default.dim("brew install node")}), then re-run.`);
6967
+ return false;
6968
+ }
6969
+ M.success("git · node · npm present");
6970
+ if (!ensureGh()) return false;
6971
+ return await ensureGitHubAuth();
6972
+ }
6973
+ function ensureGh() {
6974
+ if (has("gh")) return true;
6975
+ if (!has("brew")) {
6976
+ M.error(`GitHub CLI not found and Homebrew is unavailable.\n Install it from ${import_picocolors.default.cyan("https://cli.github.com")}, run ${import_picocolors.default.cyan("gh auth login")}, then re-run.`);
6977
+ return false;
6978
+ }
6979
+ const s = Y();
6980
+ s.start("Installing GitHub CLI (brew)…");
6981
+ const ok = run("brew", ["install", "gh"]);
6982
+ s.stop(ok ? "GitHub CLI installed" : import_picocolors.default.yellow("GitHub CLI install failed"));
6983
+ if (!ok) M.error(`Install it manually: ${import_picocolors.default.cyan("brew install gh")}, then re-run.`);
6984
+ return ok;
6985
+ }
6986
+ function isGitHubAuthenticated() {
6987
+ return spawnSync("gh", ["auth", "status"], { stdio: "ignore" }).status === 0;
6988
+ }
6989
+ async function ensureGitHubAuth() {
6990
+ if (isGitHubAuthenticated()) {
6991
+ M.success("GitHub authenticated");
6992
+ return true;
6993
+ }
6994
+ const instruction = `Authenticate first: ${import_picocolors.default.cyan("gh auth login")} (choose HTTPS + "authenticate Git with your GitHub credentials"), then re-run.`;
6995
+ if (!process.stdout.isTTY) {
6996
+ M.error(`Not authenticated with GitHub — the Pruff repos are private.\n ${instruction}`);
6997
+ return false;
6998
+ }
6999
+ const answer = await ye({
7000
+ message: "Not authenticated with GitHub (the Pruff repos are private). Run `gh auth login` now?",
7001
+ initialValue: true
7002
+ });
7003
+ if (pD(answer) || answer !== true) {
7004
+ M.error(instruction);
7005
+ return false;
7006
+ }
7007
+ run("gh", ["auth", "login"]);
7008
+ if (isGitHubAuthenticated()) {
7009
+ M.success("GitHub authenticated");
7010
+ return true;
7011
+ }
7012
+ M.error(`Still not authenticated.\n ${instruction}`);
7013
+ return false;
6717
7014
  }
6718
7015
  function has(cmd) {
6719
7016
  return spawnSync("command", ["-v", cmd], {
@@ -6840,7 +7137,8 @@ function cloneKnowledge(workspace) {
6840
7137
  return;
6841
7138
  }
6842
7139
  s.stop(import_picocolors.default.yellow("Knowledge clone failed"));
6843
- 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.`);
7140
+ note(`Knowledge vault not cloned — ${KNOWLEDGE_REPO} (check your GitHub org access).`);
7141
+ M.warn(`Could not clone ${import_picocolors.default.cyan(KNOWLEDGE_REPO)}.\n You are authenticated, so this is likely missing org access — ask the team to add you to ${import_picocolors.default.cyan("Pruff-Tech")}.`);
6844
7142
  }
6845
7143
  function cloneRepo(repo, workspace) {
6846
7144
  const { name, url } = repo;
@@ -6858,7 +7156,8 @@ function cloneRepo(repo, workspace) {
6858
7156
  ], workspace)) s.stop(`Cloned ${import_picocolors.default.cyan(name)}`);
6859
7157
  else {
6860
7158
  s.stop(import_picocolors.default.yellow(`${name} clone failed`));
6861
- 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})`);
7159
+ note(`${name} not cloned — every later step for it was skipped.`);
7160
+ M.warn(`Could not clone ${name}. You are authenticated, so this is likely missing org access.\n Ask the team for access to ${import_picocolors.default.cyan("Pruff-Tech")}, then run:\n ${import_picocolors.default.dim(`git clone ${url}`)} (inside ${workspace})`);
6862
7161
  }
6863
7162
  }
6864
7163
  function installDeps(repo, workspace) {
@@ -6866,6 +7165,7 @@ function installDeps(repo, workspace) {
6866
7165
  const target = join(workspace, name);
6867
7166
  if (!existsSync(join(target, ".git"))) return;
6868
7167
  if (!has(installCmd)) {
7168
+ note(`${name} dependencies not installed (${installCmd} missing).`);
6869
7169
  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(" ")}`)}`);
6870
7170
  return;
6871
7171
  }
@@ -6875,6 +7175,7 @@ function installDeps(repo, workspace) {
6875
7175
  if (ok && existsSync(join(target, "node_modules"))) s.stop(`${name} dependencies installed`);
6876
7176
  else {
6877
7177
  s.stop(import_picocolors.default.yellow(`${name} dependency install failed`));
7178
+ note(`${name} dependencies failed to install.`);
6878
7179
  if (ok) M.warn(`${installCmd} exited 0 but ${name}/node_modules is missing — install likely no-opped.`);
6879
7180
  M.warn(`Run manually: ${import_picocolors.default.dim(`cd ${name} && ${installCmd} ${installArgs.join(" ")}`)}`);
6880
7181
  }
@@ -6888,6 +7189,20 @@ function writeWorkspaceClaudeMd(workspace) {
6888
7189
  writeFileSync(target, WORKSPACE_CLAUDE_TEMPLATE, "utf-8");
6889
7190
  M.success("Wrote workspace CLAUDE.md");
6890
7191
  }
7192
+ function writeHarnessConfig(workspace) {
7193
+ writeIfAbsent(join(workspace, ".mcp.json"), WORKSPACE_MCP_JSON, "MCP servers (.mcp.json)");
7194
+ const claudeDir = join(workspace, ".claude");
7195
+ mkdirSync(claudeDir, { recursive: true });
7196
+ writeIfAbsent(join(claudeDir, "settings.json"), WORKSPACE_SETTINGS_JSON, "permission allowlist (.claude/settings.json)");
7197
+ }
7198
+ function writeIfAbsent(target, contents, label) {
7199
+ if (existsSync(target)) {
7200
+ M.success(`${label} already present — leaving it untouched`);
7201
+ return;
7202
+ }
7203
+ writeFileSync(target, contents, "utf-8");
7204
+ M.success(`Wrote ${label}`);
7205
+ }
6891
7206
  async function installCatalog(workspace) {
6892
7207
  const prevCwd = process.cwd();
6893
7208
  M.step("Installing Pruff rules, agents and skills into the workspace…");
@@ -6901,6 +7216,7 @@ async function installCatalog(workspace) {
6901
7216
  yes: true
6902
7217
  });
6903
7218
  } catch (error) {
7219
+ note("Skills/agents/rules catalog did not finish installing.");
6904
7220
  M.warn(`Catalog install hit an error: ${error instanceof Error ? error.message : String(error)}\n Re-run once authenticated: ${import_picocolors.default.cyan("npx pruff-skills onboarding")}`);
6905
7221
  } finally {
6906
7222
  process.chdir(prevCwd);
@@ -6908,14 +7224,19 @@ async function installCatalog(workspace) {
6908
7224
  }
6909
7225
  async function setupLocalEnvironment(serverPath) {
6910
7226
  if (!existsSync(join(serverPath, ".git"))) {
7227
+ note("Local database not set up — Pruff-server was not cloned.");
6911
7228
  M.warn("Pruff-server not cloned — skipping local database setup.");
6912
7229
  return;
6913
7230
  }
6914
- if (!ensureMysql()) return;
7231
+ if (!await ensureMysql()) return;
6915
7232
  ensureDbeaver();
6916
- generateLocalEnv(serverPath);
7233
+ const dbPassword = await resolveRootPassword();
7234
+ if (dbPassword === null) return;
7235
+ generateLocalEnv(serverPath, dbPassword);
7236
+ writeMigrateTenantsLocal(serverPath);
6917
7237
  const env = parseEnvFile(join(serverPath, "local.env"));
6918
7238
  if (!env.DATABASE_URL || !env.CLIENT_ID) {
7239
+ note("Local database not set up — local.env is missing DATABASE_URL / CLIENT_ID.");
6919
7240
  M.warn("local.env is missing DATABASE_URL / CLIENT_ID — skipping database setup.");
6920
7241
  return;
6921
7242
  }
@@ -6932,9 +7253,10 @@ async function shouldSetupDatabase(args) {
6932
7253
  if (pD(answer)) return false;
6933
7254
  return answer === true;
6934
7255
  }
6935
- function ensureMysql() {
7256
+ async function ensureMysql() {
6936
7257
  if (!has("mysql")) {
6937
7258
  if (!has("brew")) {
7259
+ note("Local database not set up — MySQL is missing and Homebrew is unavailable.");
6938
7260
  M.warn(`MySQL not found and Homebrew is unavailable.\n Install MySQL, then re-run: ${import_picocolors.default.cyan("npx pruff-skills onboarding --db")}`);
6939
7261
  return false;
6940
7262
  }
@@ -6942,7 +7264,10 @@ function ensureMysql() {
6942
7264
  s.start("Installing MySQL (brew)…");
6943
7265
  const ok = run("brew", ["install", "mysql"]);
6944
7266
  s.stop(ok ? "MySQL installed" : import_picocolors.default.yellow("MySQL install failed"));
6945
- if (!ok) return false;
7267
+ if (!ok) {
7268
+ note("Local database not set up — MySQL install failed.");
7269
+ return false;
7270
+ }
6946
7271
  } else M.success("MySQL already installed");
6947
7272
  if (mysqlRunning()) {
6948
7273
  M.success("MySQL already running");
@@ -6958,7 +7283,10 @@ function ensureMysql() {
6958
7283
  for (let i = 0; i < 30 && !mysqlRunning(); i++) spawnSync("sleep", ["1"]);
6959
7284
  const up = mysqlRunning();
6960
7285
  s.stop(up ? "MySQL running" : import_picocolors.default.yellow("MySQL did not come up"));
6961
- if (!up) M.warn(`Start it manually: ${import_picocolors.default.cyan("brew services start mysql")}, then re-run with --db.`);
7286
+ if (!up) {
7287
+ note("Local database not set up — MySQL never came up.");
7288
+ M.warn(`Start it manually: ${import_picocolors.default.cyan("brew services start mysql")}, then re-run with --db.`);
7289
+ }
6962
7290
  return up;
6963
7291
  }
6964
7292
  function mysqlRunning() {
@@ -6969,6 +7297,48 @@ function mysqlRunning() {
6969
7297
  "--silent"
6970
7298
  ], { stdio: "ignore" }).status === 0;
6971
7299
  }
7300
+ function canConnectAsRoot(password) {
7301
+ const args = [
7302
+ "-h",
7303
+ "localhost",
7304
+ "-P",
7305
+ "3306",
7306
+ "-u",
7307
+ "root"
7308
+ ];
7309
+ if (password) args.push(`-p${password}`);
7310
+ return spawnSync("mysql", [
7311
+ ...args,
7312
+ "-N",
7313
+ "-B",
7314
+ "-e",
7315
+ "SELECT 1"
7316
+ ], { stdio: "ignore" }).status === 0;
7317
+ }
7318
+ async function resolveRootPassword() {
7319
+ if (canConnectAsRoot("")) {
7320
+ M.success("MySQL root reachable (no password)");
7321
+ return "";
7322
+ }
7323
+ const manual = `Connect manually to find out (${import_picocolors.default.dim("mysql -u root -p")}), then re-run with ${import_picocolors.default.cyan("--db")}.`;
7324
+ if (!process.stdout.isTTY) {
7325
+ note("Local database not set up — MySQL root needs a password.");
7326
+ M.warn(`MySQL root rejects a passwordless connection.\n ${manual}`);
7327
+ return null;
7328
+ }
7329
+ for (let attempt = 0; attempt < 3; attempt++) {
7330
+ const answer = await ge({ message: attempt === 0 ? "MySQL root needs a password (this machine already had MySQL). Enter it:" : "Wrong password. Try again:" });
7331
+ if (pD(answer)) break;
7332
+ const password = (answer ?? "").trim();
7333
+ if (password && canConnectAsRoot(password)) {
7334
+ M.success("MySQL root reachable");
7335
+ return password;
7336
+ }
7337
+ }
7338
+ note("Local database not set up — could not authenticate to MySQL as root.");
7339
+ M.warn(`Could not authenticate to MySQL as root.\n ${manual}`);
7340
+ return null;
7341
+ }
6972
7342
  function ensureDbeaver() {
6973
7343
  if (existsSync("/Applications/DBeaver.app")) {
6974
7344
  M.success("DBeaver already installed");
@@ -6987,7 +7357,7 @@ function ensureDbeaver() {
6987
7357
  ]);
6988
7358
  s.stop(ok ? "DBeaver installed" : import_picocolors.default.yellow("DBeaver install failed"));
6989
7359
  }
6990
- function generateLocalEnv(serverPath) {
7360
+ function generateLocalEnv(serverPath, dbPassword) {
6991
7361
  const target = join(serverPath, "local.env");
6992
7362
  if (existsSync(target)) {
6993
7363
  M.success("local.env already present — leaving it untouched");
@@ -7004,10 +7374,16 @@ function generateLocalEnv(serverPath) {
7004
7374
  siiKey: secret(),
7005
7375
  registrationSecret: secret(),
7006
7376
  pfxSecret: secret(),
7007
- userFormKey: secret()
7377
+ userFormKey: secret(),
7378
+ dbPassword
7008
7379
  }), "utf-8");
7009
7380
  M.success("Wrote local.env (minimal bootable — fill third-party keys when needed)");
7010
7381
  }
7382
+ function writeMigrateTenantsLocal(serverPath) {
7383
+ const target = join(serverPath, "sincronizations", "migrateTenantsLocal.ts");
7384
+ if (!existsSync(join(serverPath, "sincronizations"))) return;
7385
+ writeIfAbsent(target, MIGRATE_TENANTS_LOCAL_SCRIPT, "sincronizations/migrateTenantsLocal.ts");
7386
+ }
7011
7387
  function parseEnvFile(path) {
7012
7388
  if (!existsSync(path)) return {};
7013
7389
  const out = {};
@@ -7093,6 +7469,29 @@ function migrateDeploy(serverPath, url) {
7093
7469
  "deploy"
7094
7470
  ], serverPath, { DATABASE_URL: url });
7095
7471
  }
7472
+ function rutConflict(holderSecretId, clientId) {
7473
+ return holderSecretId !== "" && holderSecretId !== clientId;
7474
+ }
7475
+ function findRutHolder(serverPath, env, url) {
7476
+ const scriptPath = join(serverPath, ".pruff-onboarding-rut-probe.ts");
7477
+ writeFileSync(scriptPath, RUT_CONFLICT_PROBE_SCRIPT, "utf-8");
7478
+ try {
7479
+ const { out } = capture("npx", [
7480
+ "ts-node",
7481
+ "--transpile-only",
7482
+ "-r",
7483
+ "tsconfig-paths/register",
7484
+ scriptPath,
7485
+ env.CLIENT_RUT ?? ""
7486
+ ], serverPath, {
7487
+ ...env,
7488
+ DATABASE_URL: url
7489
+ });
7490
+ return out.match(/^RUT_HOLDER=(.*)$/m)?.[1]?.trim() ?? "";
7491
+ } finally {
7492
+ rmSync(scriptPath, { force: true });
7493
+ }
7494
+ }
7096
7495
  function runDisposableScript(serverPath, fileName, contents, env, extraArgs = []) {
7097
7496
  const scriptPath = join(serverPath, fileName);
7098
7497
  writeFileSync(scriptPath, contents, "utf-8");
@@ -7122,9 +7521,15 @@ function setupLocalDatabase(serverPath, env) {
7122
7521
  const tenantName = `tenant-${clientId}`;
7123
7522
  const tenantUrl = swapDbName(controlPlaneUrl, tenantName);
7124
7523
  const adminEmail = `admin@${clientId}.com`;
7524
+ if (controlPlaneName !== "pruff-local") {
7525
+ note(`Local database not set up — DATABASE_URL must point at "${CONTROL_PLANE_DB}".`);
7526
+ M.warn(`DATABASE_URL points at ${import_picocolors.default.cyan(controlPlaneName)}, but Pruff-server hardcodes the local control plane as ${import_picocolors.default.cyan(CONTROL_PLANE_DB)}.\n Company/user seeds would silently write to the wrong database — fix local.env and re-run with --db.`);
7527
+ return;
7528
+ }
7125
7529
  M.step(`Setting up local databases (${controlPlaneName} + ${tenantName})…`);
7126
7530
  mysqlExec(conn, `CREATE DATABASE IF NOT EXISTS \`${controlPlaneName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
7127
7531
  if (!migrateDeploy(serverPath, controlPlaneUrl)) {
7532
+ note("Local database not set up — control-plane migration failed.");
7128
7533
  M.warn("Control-plane migration failed — skipping the rest of database setup.");
7129
7534
  return;
7130
7535
  }
@@ -7133,41 +7538,118 @@ function setupLocalDatabase(serverPath, env) {
7133
7538
  DATABASE_URL: controlPlaneUrl
7134
7539
  });
7135
7540
  const action = decideTenantAction(dbExists(conn, tenantName), adminUserExists(conn, tenantName, adminEmail));
7136
- if (action === "already-provisioned") {
7137
- M.success(`Tenant "${clientId}" already provisioned — skipping seed.`);
7138
- printLoginHint(clientId, adminEmail, adminPassword);
7541
+ if (action !== "already-provisioned") {
7542
+ const holder = findRutHolder(serverPath, env, controlPlaneUrl);
7543
+ if (rutConflict(holder, clientId)) {
7544
+ note(`Tenant "${clientId}" not seeded — CLIENT_RUT is already used by "${holder}".`);
7545
+ M.warn(`CLIENT_RUT ${import_picocolors.default.cyan(env.CLIENT_RUT ?? "")} already belongs to tenant ${import_picocolors.default.cyan(holder)} in ${controlPlaneName}.\n Seeding would reuse that company and never register "${clientId}" — the tenant could not be logged into.\n Change CLIENT_RUT (or CLIENT_ID) in local.env and re-run with --db.`);
7546
+ return;
7547
+ }
7548
+ if (action === "reset-and-seed") {
7549
+ M.warn(`Tenant "${clientId}" partially provisioned — resetting (Pruff-server seed is not idempotent).`);
7550
+ mysqlExec(conn, `DROP DATABASE IF EXISTS \`${tenantName}\``);
7551
+ }
7552
+ mysqlExec(conn, `CREATE DATABASE IF NOT EXISTS \`${tenantName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
7553
+ if (!migrateDeploy(serverPath, tenantUrl)) {
7554
+ note("Local database not set up — tenant migration failed.");
7555
+ M.warn("Tenant migration failed — skipping seed.");
7556
+ return;
7557
+ }
7558
+ const s = Y();
7559
+ s.start(`Seeding tenant "${clientId}"…`);
7560
+ const seeded = run("npx", [
7561
+ "prisma",
7562
+ "db",
7563
+ "seed"
7564
+ ], serverPath, {
7565
+ ...env,
7566
+ DATABASE_URL: tenantUrl
7567
+ });
7568
+ s.stop(seeded ? `Tenant "${clientId}" seeded` : import_picocolors.default.yellow("Tenant seed failed"));
7569
+ if (!seeded) {
7570
+ note(`Tenant "${clientId}" seed failed.`);
7571
+ return;
7572
+ }
7573
+ } else M.success(`Tenant "${clientId}" already provisioned — skipping seed.`);
7574
+ setupIntelligence(serverPath, env, conn, controlPlaneUrl, adminEmail, adminPassword);
7575
+ if (!runDisposableScript(serverPath, ".pruff-onboarding-tenant-user.ts", TENANT_USER_BACKFILL_SCRIPT, {
7576
+ ...env,
7577
+ DATABASE_URL: tenantUrl
7578
+ }, [adminEmail, adminPassword])) {
7579
+ note("Tenant admin user may be missing — the backfill script failed.");
7580
+ M.warn("Tenant admin-user backfill failed — you may need to create the user manually.");
7139
7581
  return;
7140
7582
  }
7141
- if (action === "reset-and-seed") {
7142
- M.warn(`Tenant "${clientId}" partially provisioned — resetting (Pruff-server seed is not idempotent).`);
7143
- mysqlExec(conn, `DROP DATABASE IF EXISTS \`${tenantName}\``);
7583
+ if (!runDisposableScript(serverPath, ".pruff-onboarding-accounting-period.ts", ACCOUNTING_PERIOD_SCRIPT, {
7584
+ ...env,
7585
+ DATABASE_URL: tenantUrl
7586
+ }, [String((/* @__PURE__ */ new Date()).getFullYear())])) {
7587
+ note("No open AccountingPeriod — accounting entries will be rejected until one exists.");
7588
+ M.warn("Could not open the current-year accounting period — create one under Contabilidad → Períodos.");
7144
7589
  }
7145
- mysqlExec(conn, `CREATE DATABASE IF NOT EXISTS \`${tenantName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
7146
- if (!migrateDeploy(serverPath, tenantUrl)) {
7147
- M.warn("Tenant migration failed — skipping seed.");
7590
+ M.success(`Local tenant "${clientId}" ready.`);
7591
+ printLoginHint(clientId, adminEmail, adminPassword);
7592
+ }
7593
+ function setupIntelligence(serverPath, env, conn, controlPlaneUrl, adminEmail, adminPassword) {
7594
+ const intelligenceUrl = env.INTELLIGENCE_DATABASE_URL;
7595
+ if (!intelligenceUrl) {
7596
+ note("Intelligence module unavailable — INTELLIGENCE_DATABASE_URL is not set in local.env.");
7597
+ M.warn(`local.env has no ${import_picocolors.default.cyan("INTELLIGENCE_DATABASE_URL")} — skipping Intelligence setup.`);
7148
7598
  return;
7149
7599
  }
7150
7600
  const s = Y();
7151
- s.start(`Seeding tenant "${clientId}"…`);
7152
- const seeded = run("npx", [
7601
+ s.start("Setting up Intelligence…");
7602
+ mysqlExec(conn, `CREATE DATABASE IF NOT EXISTS \`${dbNameFromUrl(intelligenceUrl)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
7603
+ const intelligenceEnv = {
7604
+ ...env,
7605
+ INTELLIGENCE_DATABASE_URL: intelligenceUrl
7606
+ };
7607
+ if (!(run("npx", [
7153
7608
  "prisma",
7154
- "db",
7155
- "seed"
7609
+ "migrate",
7610
+ "deploy",
7611
+ "--schema",
7612
+ INTELLIGENCE_SCHEMA
7613
+ ], serverPath, intelligenceEnv) && run("npx", [
7614
+ "prisma",
7615
+ "generate",
7616
+ "--schema",
7617
+ INTELLIGENCE_SCHEMA
7618
+ ], serverPath, intelligenceEnv))) {
7619
+ s.stop(import_picocolors.default.yellow("Intelligence database setup failed"));
7620
+ note(`Intelligence module unavailable — could not migrate/generate ${INTELLIGENCE_DB}.`);
7621
+ return;
7622
+ }
7623
+ if (!(run("npx", [
7624
+ "ts-node",
7625
+ "--transpile-only",
7626
+ "-r",
7627
+ "tsconfig-paths/register",
7628
+ INTELLIGENCE_GLOBAL_SEED
7156
7629
  ], serverPath, {
7157
7630
  ...env,
7158
- DATABASE_URL: tenantUrl
7159
- });
7160
- s.stop(seeded ? `Tenant "${clientId}" seeded` : import_picocolors.default.yellow("Tenant seed failed"));
7161
- if (!seeded) return;
7162
- if (!runDisposableScript(serverPath, ".pruff-onboarding-tenant-user.ts", TENANT_USER_BACKFILL_SCRIPT, {
7631
+ DATABASE_URL: controlPlaneUrl
7632
+ }) && run("npx", [
7633
+ "ts-node",
7634
+ "--transpile-only",
7635
+ "-r",
7636
+ "tsconfig-paths/register",
7637
+ INTELLIGENCE_TENANT_SCRIPT
7638
+ ], serverPath, {
7163
7639
  ...env,
7164
- DATABASE_URL: tenantUrl
7165
- }, [adminEmail, adminPassword])) {
7166
- M.warn("Tenant admin-user backfill failed — you may need to create the user manually.");
7640
+ DATABASE_URL: controlPlaneUrl,
7641
+ TENANT_SECRET_ID: env.CLIENT_ID,
7642
+ TENANT_RUT: env.CLIENT_RUT,
7643
+ TENANT_NAME: env.CLIENT_NAME,
7644
+ USER_EMAIL: adminEmail,
7645
+ USER_PASSWORD: adminPassword,
7646
+ USER_NAME: "Pruff Local Admin"
7647
+ }))) {
7648
+ s.stop(import_picocolors.default.yellow("Intelligence tenant enrolment failed"));
7649
+ note("Intelligence module unavailable — tenant enrolment failed.");
7167
7650
  return;
7168
7651
  }
7169
- M.success(`Local tenant "${clientId}" ready.`);
7170
- printLoginHint(clientId, adminEmail, adminPassword);
7652
+ s.stop("Intelligence ready");
7171
7653
  }
7172
7654
  function printLoginHint(clientId, email, password) {
7173
7655
  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)}`);
@@ -7198,7 +7680,29 @@ async function showGuidance(workspace) {
7198
7680
  line(`${import_picocolors.default.dim("1.")} Open ${import_picocolors.default.bold("Obsidian")} → the ${import_picocolors.default.cyan(KNOWLEDGE_DIR)} vault is registered and ready.`);
7199
7681
  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.`);
7200
7682
  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")}.`);
7201
- line(`${import_picocolors.default.dim("4.")} Clone any serverless service into this folder with ${import_picocolors.default.dim("git clone <repo>")}.`);
7683
+ line(`${import_picocolors.default.dim("4.")} Run ${import_picocolors.default.dim("/mcp")} inside Claude Code to authenticate Linear + PostHog (already configured in ${import_picocolors.default.dim(".mcp.json")}).`);
7684
+ line(`${import_picocolors.default.dim("5.")} Clone any serverless service into this folder with ${import_picocolors.default.dim("git clone <repo>")}.`);
7685
+ console.log();
7686
+ }
7687
+ function showTeamCredentialsAsk(workspace) {
7688
+ const envPath = join(workspace, "Pruff-server", "local.env");
7689
+ const line = (s) => console.log(` ${s}`);
7690
+ line(import_picocolors.default.bold("Ask your team for these credentials") + import_picocolors.default.dim(" (1Password / #dev)"));
7691
+ line(import_picocolors.default.dim(`They go in ${envPath} — the keys are already there, blank.`));
7692
+ line("");
7693
+ for (const group of TEAM_PROVIDED_ENV) {
7694
+ line(import_picocolors.default.cyan(group.keys.join(", ")));
7695
+ line(` ${import_picocolors.default.dim(group.unlocks)}`);
7696
+ }
7697
+ line("");
7698
+ line(import_picocolors.default.dim("Everything else — the databases, the local tenant, Intelligence — is already set up and needs nothing from anyone."));
7699
+ console.log();
7700
+ }
7701
+ function showIssues() {
7702
+ if (issues.length === 0) return;
7703
+ console.log();
7704
+ console.log(` ${import_picocolors.default.bold(import_picocolors.default.yellow("Did not complete"))}`);
7705
+ for (const issue of issues) console.log(` ${import_picocolors.default.yellow("•")} ${issue}`);
7202
7706
  console.log();
7203
7707
  }
7204
7708
  function registerVault(vaultPath) {
@@ -7838,8 +8342,18 @@ ${BOLD}Rules:${RESET}
7838
8342
  Install a whole repo's rule set (client/server also pull in common)
7839
8343
 
7840
8344
  ${BOLD}Onboarding:${RESET}
7841
- onboarding Set up a new machine (macOS): install Claude Code,
7842
- install Obsidian, and clone the Pruff knowledge vault
8345
+ onboarding [dir] Set up a new machine (macOS), end to end: prerequisites +
8346
+ GitHub auth, Claude Code, Obsidian, Bun, clone the
8347
+ knowledge vault + Pruff-server + Pruff-Client and install
8348
+ their deps, write the workspace CLAUDE.md / .mcp.json /
8349
+ settings.json, install the curated skills, agents and
8350
+ rules, then provision the local databases (control plane,
8351
+ tenant, Intelligence) and a login-ready local tenant.
8352
+ Idempotent — safe to re-run. [dir] defaults to ~/pruff.
8353
+
8354
+ ${BOLD}Onboarding Options:${RESET}
8355
+ --db Force the local database step (non-interactive shells)
8356
+ --no-db Skip the local database step
7843
8357
 
7844
8358
  ${BOLD}Updates:${RESET}
7845
8359
  update [skills...] Update skills to latest versions (alias: upgrade)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruff-skills",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "The official Pruff agent skills CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,17 +39,25 @@ human directly. Your output is always a **draft PR** — never a merge, never a
39
39
  3. **Follow the repo's own conventions exactly** — `Result<T>` and tenant-scoped
40
40
  `prisma` in Pruff-server, consts/types in sibling files, no `any`, no
41
41
  `console.log`. These aren't suggestions; a fix that violates them isn't done.
42
- 4. **Verify before opening the PR** — run the repo's typecheck/lint/tests. If the fix
43
- touches Prisma or raw SQL, run the `db-query-reviewer` subagent on your own diff
44
- before anyone else has to.
42
+ 4. **Verify before opening the PR** — once, after all edits are done, not after each
43
+ individual edit. Chain the repo's typecheck/lint/tests into a single command
44
+ (`&&`) rather than separate calls. If the fix touches Prisma or raw SQL, run the
45
+ `db-query-reviewer` subagent on your own diff before anyone else has to.
45
46
  5. **Commit and open the PR** using the `/commit` and `/create-pr-llm` skills — never a
46
47
  raw `git commit`/`gh pr create`, and never the legacy `/create-pr` — your PRs always
47
48
  go to `reviewer-shared` next, so the description needs `/create-pr-llm`'s anchored,
48
49
  verifiable claims and authored "judge this" questions for that skill to work with.
49
50
  The PR is **always a draft**; a human marks it ready.
50
- 6. **Post the PR link back** to `#fixer-queue` (and `#pr-review` if you want
51
- `reviewer-shared` to run its pipeline on it) — link the originating Linear issue
52
- and, if this came from `investigator-shared`, the triage message it came from.
51
+ 6. **Post the PR link to `#pr-review` — always, not optionally.** `reviewer-shared`
52
+ only listens in `#pr-review`; it does not see `#fixer-queue` at all, so replying
53
+ there, or having a human type `?reviewer` in a `#fixer-queue` thread, reaches no
54
+ one — that channel isn't in its allowlist. Posting to `#pr-review` is the *only*
55
+ path that actually invokes it, exactly like `investigator-shared` must post to
56
+ `#fixer-queue` rather than relying on a `@fixer-shared` mention landing. Also post
57
+ a short summary + the same PR link back to `#fixer-queue` for visibility — but
58
+ that second post is for humans following the queue, not a substitute for the
59
+ `#pr-review` post. Link the originating Linear issue and, if this came from
60
+ `investigator-shared`, the triage message it came from.
53
61
 
54
62
  ## Hard limits — these are enforced, not requested
55
63
 
@@ -36,6 +36,14 @@ don't guess which one they mean.
36
36
  equivalent by reading the diff and writing prose. If the PR body isn't in that
37
37
  format (no anchored claims, no ⚖️ column — i.e. it wasn't written by `fixer-shared`
38
38
  with `/create-pr-llm`), fall back to the legacy `pr-visualizer` skill instead.
39
+ **Never let it run in `light` mode, regardless of how small the diff looks.**
40
+ `light` is 0-agents — it only reads exactly what the PR description anchors, and a
41
+ real review missed a confirmed blocker this way: the PR claimed "no backend changes
42
+ needed" for an autocomplete filter, and `light` mode has no mechanism to check
43
+ whether that claim is true, because it never traces the value into the actual
44
+ service layer. Always pass `--standard` explicitly. Go to `--deep` only if a human
45
+ asks for a deeper look in `#pr-review` — never on your own initiative, and never
46
+ drop back to `light` for a PR that "looks contained."
39
47
  4. **Post the summary in `#pr-review`**: the artifact link (or, if artifact publishing
40
48
  isn't available in this session, a written equivalent), the highest-severity
41
49
  findings with a concrete failure scenario each ("this could be slow" is not a
@@ -74,7 +74,9 @@ there for the routing table. Repo work happens in a worktree created by
74
74
  `~/Desktop/pruff/.claude/scripts/wt-new.sh <repo> <branch>` — never on an
75
75
  active branch, never by hand.
76
76
 
77
- **Rules are per-working-directory and don't follow a `cd`.** Before your first
78
- edit or first substantive claim about a repo's conventions, `Read` that repo's
79
- own `CLAUDE.md` and `.claude/rules/*.md` — they're authoritative over anything
80
- summarized here.
77
+ **Your shell's working directory persists across commands within a turn —
78
+ you don't need to repeat `cd` on every call.** What doesn't carry over
79
+ automatically is loaded rules context: after you `cd` into a different repo,
80
+ `Read` that repo's own `CLAUDE.md` and `.claude/rules/*.md` before your first
81
+ edit or first substantive claim about its conventions — they're authoritative
82
+ over anything summarized here.