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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
|
7137
|
-
|
|
7138
|
-
|
|
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 (
|
|
7142
|
-
|
|
7143
|
-
|
|
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
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
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(
|
|
7152
|
-
|
|
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
|
-
"
|
|
7155
|
-
"
|
|
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:
|
|
7159
|
-
})
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
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:
|
|
7165
|
-
|
|
7166
|
-
|
|
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
|
-
|
|
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.")}
|
|
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
|
|
7842
|
-
|
|
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
|
@@ -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** —
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|