@cipherstash/stack 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/stash.js CHANGED
@@ -500,7 +500,7 @@ var require_picocolors = __commonJS({
500
500
  // src/bin/stash.ts
501
501
  var import_dotenv2 = __toESM(require_main(), 1);
502
502
  import { readFileSync } from "node:fs";
503
- import { dirname, join } from "node:path";
503
+ import { dirname as dirname2, join } from "node:path";
504
504
  import { fileURLToPath } from "node:url";
505
505
 
506
506
  // ../../node_modules/.pnpm/@clack+prompts@0.10.1/node_modules/@clack/prompts/dist/index.mjs
@@ -855,6 +855,41 @@ var dD = class extends x {
855
855
  };
856
856
  var A;
857
857
  A = /* @__PURE__ */ new WeakMap();
858
+ var kD = Object.defineProperty;
859
+ var $D = (e2, u2, t) => u2 in e2 ? kD(e2, u2, { enumerable: true, configurable: true, writable: true, value: t }) : e2[u2] = t;
860
+ var H = (e2, u2, t) => ($D(e2, typeof u2 != "symbol" ? u2 + "" : u2, t), t);
861
+ var SD = class extends x {
862
+ constructor(u2) {
863
+ super(u2, false), H(this, "options"), H(this, "cursor", 0), this.options = u2.options, this.value = [...u2.initialValues ?? []], this.cursor = Math.max(this.options.findIndex(({ value: t }) => t === u2.cursorAt), 0), this.on("key", (t) => {
864
+ t === "a" && this.toggleAll();
865
+ }), this.on("cursor", (t) => {
866
+ switch (t) {
867
+ case "left":
868
+ case "up":
869
+ this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
870
+ break;
871
+ case "down":
872
+ case "right":
873
+ this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
874
+ break;
875
+ case "space":
876
+ this.toggleValue();
877
+ break;
878
+ }
879
+ });
880
+ }
881
+ get _value() {
882
+ return this.options[this.cursor].value;
883
+ }
884
+ toggleAll() {
885
+ const u2 = this.value.length === this.options.length;
886
+ this.value = u2 ? [] : this.options.map((t) => t.value);
887
+ }
888
+ toggleValue() {
889
+ const u2 = this.value.includes(this._value);
890
+ this.value = u2 ? this.value.filter((t) => t !== this._value) : [...this.value, this._value];
891
+ }
892
+ };
858
893
  var OD = Object.defineProperty;
859
894
  var PD = (e2, u2, t) => u2 in e2 ? OD(e2, u2, { enumerable: true, configurable: true, writable: true, value: t }) : e2[u2] = t;
860
895
  var J = (e2, u2, t) => (PD(e2, typeof u2 != "symbol" ? u2 + "" : u2, t), t);
@@ -1023,6 +1058,46 @@ ${import_picocolors2.default.cyan(d2)}
1023
1058
  }
1024
1059
  } }).prompt();
1025
1060
  };
1061
+ var fe = (t) => {
1062
+ const n = (r, i) => {
1063
+ const s = r.label ?? String(r.value);
1064
+ return i === "active" ? `${import_picocolors2.default.cyan(A2)} ${s} ${r.hint ? import_picocolors2.default.dim(`(${r.hint})`) : ""}` : i === "selected" ? `${import_picocolors2.default.green(T)} ${import_picocolors2.default.dim(s)} ${r.hint ? import_picocolors2.default.dim(`(${r.hint})`) : ""}` : i === "cancelled" ? `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}` : i === "active-selected" ? `${import_picocolors2.default.green(T)} ${s} ${r.hint ? import_picocolors2.default.dim(`(${r.hint})`) : ""}` : i === "submitted" ? `${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.dim(F)} ${import_picocolors2.default.dim(s)}`;
1065
+ };
1066
+ return new SD({ options: t.options, initialValues: t.initialValues, required: t.required ?? true, cursorAt: t.cursorAt, validate(r) {
1067
+ if (this.required && r.length === 0) return `Please select at least one option.
1068
+ ${import_picocolors2.default.reset(import_picocolors2.default.dim(`Press ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" space ")))} to select, ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" enter ")))} to submit`))}`;
1069
+ }, render() {
1070
+ const r = `${import_picocolors2.default.gray(o)}
1071
+ ${b2(this.state)} ${t.message}
1072
+ `, i = (s, c) => {
1073
+ const a = this.value.includes(s.value);
1074
+ return c && a ? n(s, "active-selected") : a ? n(s, "selected") : n(s, c ? "active" : "inactive");
1075
+ };
1076
+ switch (this.state) {
1077
+ case "submit":
1078
+ return `${r}${import_picocolors2.default.gray(o)} ${this.options.filter(({ value: s }) => this.value.includes(s)).map((s) => n(s, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none")}`;
1079
+ case "cancel": {
1080
+ const s = this.options.filter(({ value: c }) => this.value.includes(c)).map((c) => n(c, "cancelled")).join(import_picocolors2.default.dim(", "));
1081
+ return `${r}${import_picocolors2.default.gray(o)} ${s.trim() ? `${s}
1082
+ ${import_picocolors2.default.gray(o)}` : ""}`;
1083
+ }
1084
+ case "error": {
1085
+ const s = this.error.split(`
1086
+ `).map((c, a) => a === 0 ? `${import_picocolors2.default.yellow(d2)} ${import_picocolors2.default.yellow(c)}` : ` ${c}`).join(`
1087
+ `);
1088
+ return `${r + import_picocolors2.default.yellow(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
1089
+ ${import_picocolors2.default.yellow(o)} `)}
1090
+ ${s}
1091
+ `;
1092
+ }
1093
+ default:
1094
+ return `${r}${import_picocolors2.default.cyan(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
1095
+ ${import_picocolors2.default.cyan(o)} `)}
1096
+ ${import_picocolors2.default.cyan(d2)}
1097
+ `;
1098
+ }
1099
+ } }).prompt();
1100
+ };
1026
1101
  var Me = (t = "", n = "") => {
1027
1102
  const r = `
1028
1103
  ${t}
@@ -1097,7 +1172,7 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
1097
1172
  }, R2 = (m2) => m2.replace(/\.+$/, ""), O2 = (m2) => {
1098
1173
  const h2 = (performance.now() - m2) / 1e3, w2 = Math.floor(h2 / 60), I2 = Math.floor(h2 % 60);
1099
1174
  return w2 > 0 ? `[${w2}m ${I2}s]` : `[${I2}s]`;
1100
- }, H = (m2 = "") => {
1175
+ }, H2 = (m2 = "") => {
1101
1176
  a = true, s = fD(), l2 = R2(m2), g2 = performance.now(), process.stdout.write(`${import_picocolors2.default.gray(o)}
1102
1177
  `);
1103
1178
  let h2 = 0, w2 = 0;
@@ -1120,7 +1195,7 @@ var Y2 = ({ indicator: t = "dots" } = {}) => {
1120
1195
  `) : process.stdout.write(`${w2} ${l2}
1121
1196
  `), E(), s();
1122
1197
  };
1123
- return { start: H, stop: N2, message: (m2 = "") => {
1198
+ return { start: H2, stop: N2, message: (m2 = "") => {
1124
1199
  l2 = R2(m2 ?? l2);
1125
1200
  } };
1126
1201
  };
@@ -1131,23 +1206,20 @@ function createBaseProvider() {
1131
1206
  name: "base",
1132
1207
  introMessage: "Setting up CipherStash for your project...",
1133
1208
  connectionOptions: [
1134
- { value: "drizzle", label: "Drizzle ORM", hint: "recommended" },
1209
+ { value: "drizzle", label: "Drizzle ORM" },
1210
+ { value: "supabase-js", label: "Supabase JS Client" },
1135
1211
  { value: "prisma", label: "Prisma" },
1136
1212
  { value: "raw-sql", label: "Raw SQL / pg" }
1137
1213
  ],
1138
1214
  getNextSteps(state) {
1139
1215
  const steps = [
1140
- "Install @cipherstash/stack: npm install @cipherstash/stack"
1216
+ "Create a CipherStash account and get your credentials:\n https://dashboard.cipherstash.com/sign-up\n Then set: CS_WORKSPACE_CRN, CS_CLIENT_ID, CS_CLIENT_KEY, CS_CLIENT_ACCESS_KEY",
1217
+ "Set up your database: npx stash-forge setup"
1141
1218
  ];
1142
- if (state.connectionMethod === "drizzle") {
1143
- steps.push("Import encryptedType from @cipherstash/stack/drizzle");
1144
- } else if (state.connectionMethod === "prisma") {
1145
- steps.push("Set up Prisma with @cipherstash/stack");
1219
+ if (state.clientFilePath) {
1220
+ steps.push(`Edit your encryption schema: ${state.clientFilePath}`);
1146
1221
  }
1147
- steps.push(
1148
- "Define your encrypted schema",
1149
- "Read the docs: https://docs.cipherstash.com"
1150
- );
1222
+ steps.push("Read the docs: https://cipherstash.com/docs");
1151
1223
  return steps;
1152
1224
  }
1153
1225
  };
@@ -1170,20 +1242,14 @@ function createSupabaseProvider() {
1170
1242
  ],
1171
1243
  getNextSteps(state) {
1172
1244
  const steps = [
1173
- "Install @cipherstash/stack: npm install @cipherstash/stack"
1245
+ "Create a CipherStash account and get your credentials:\n https://dashboard.cipherstash.com/sign-up\n Then set: CS_WORKSPACE_CRN, CS_CLIENT_ID, CS_CLIENT_KEY, CS_CLIENT_ACCESS_KEY",
1246
+ "Set up your database: npx stash-forge setup"
1174
1247
  ];
1175
- if (state.connectionMethod === "supabase-js") {
1176
- steps.push("Import encryptedSupabase from @cipherstash/stack/supabase");
1177
- } else if (state.connectionMethod === "drizzle") {
1178
- steps.push("Import encryptedType from @cipherstash/stack/drizzle");
1179
- } else if (state.connectionMethod === "prisma") {
1180
- steps.push("Set up Prisma with @cipherstash/stack");
1248
+ if (state.clientFilePath) {
1249
+ steps.push(`Edit your encryption schema: ${state.clientFilePath}`);
1181
1250
  }
1182
1251
  steps.push(
1183
- "Define your encrypted schema",
1184
- "Supabase guides: https://cipherstash.com/docs/supabase/encrypt-user-data",
1185
- "Multi-tenant encryption: https://docs.cipherstash.com/docs/multi-tenant",
1186
- "Migrating existing data: https://docs.cipherstash.com/docs/migration",
1252
+ "Supabase guides: https://cipherstash.com/docs/stack/encryption/supabase",
1187
1253
  "Need help? #supabase in Discord or support@cipherstash.com"
1188
1254
  );
1189
1255
  return steps;
@@ -1191,46 +1257,9 @@ function createSupabaseProvider() {
1191
1257
  };
1192
1258
  }
1193
1259
 
1194
- // src/bin/commands/init/stubs.ts
1195
- function delay(ms) {
1196
- return new Promise((resolve) => setTimeout(resolve, ms));
1197
- }
1198
- async function startDeviceCodeAuth() {
1199
- await delay(500);
1200
- return {
1201
- verificationUrl: "https://cipherstash.com/activate",
1202
- userCode: "ABCD-1234",
1203
- deviceCode: "device_code_placeholder",
1204
- expiresIn: 900,
1205
- interval: 5
1206
- };
1207
- }
1208
- async function pollForToken(deviceCode) {
1209
- void deviceCode;
1210
- await delay(2e3);
1211
- return {
1212
- accessToken: "stub_access_token",
1213
- refreshToken: "stub_refresh_token",
1214
- expiresIn: 3600
1215
- };
1216
- }
1217
- async function fetchWorkspaces(accessToken) {
1218
- void accessToken;
1219
- await delay(300);
1220
- return [
1221
- { id: "ws_1", name: "My First Workspace" },
1222
- { id: "ws_2", name: "Production" }
1223
- ];
1224
- }
1225
- async function createWorkspace(accessToken, name) {
1226
- void accessToken;
1227
- await delay(500);
1228
- return { id: `ws_${Date.now()}`, name };
1229
- }
1230
- async function installEqlExtension(databaseUrl) {
1231
- void databaseUrl;
1232
- await delay(1500);
1233
- }
1260
+ // src/bin/commands/init/steps/build-schema.ts
1261
+ import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
1262
+ import { dirname, resolve as resolve2 } from "node:path";
1234
1263
 
1235
1264
  // src/bin/commands/init/types.ts
1236
1265
  var CancelledError = class extends Error {
@@ -1239,105 +1268,362 @@ var CancelledError = class extends Error {
1239
1268
  this.name = "CancelledError";
1240
1269
  }
1241
1270
  };
1271
+ function toIntegration(method) {
1272
+ switch (method) {
1273
+ case "drizzle":
1274
+ return "drizzle";
1275
+ case "supabase-js":
1276
+ return "supabase";
1277
+ case "prisma":
1278
+ case "raw-sql":
1279
+ return "postgresql";
1280
+ }
1281
+ }
1242
1282
 
1243
- // src/bin/commands/init/steps/authenticate.ts
1244
- var authenticateStep = {
1245
- id: "authenticate",
1246
- name: "Authenticate",
1247
- async run(state, _provider) {
1248
- const s = Y2();
1249
- s.start("Starting authentication...");
1250
- const { verificationUrl, userCode, deviceCode } = await startDeviceCodeAuth();
1251
- s.stop("Authentication started");
1252
- Me(
1253
- `Open: ${verificationUrl}
1254
- Code: ${userCode}`,
1255
- "Authenticate with CipherStash"
1256
- );
1257
- s.start("Waiting for authentication...");
1258
- const token = await pollForToken(deviceCode);
1259
- s.stop("Authenticated successfully");
1260
- return { ...state, accessToken: token.accessToken };
1283
+ // src/bin/commands/init/utils.ts
1284
+ import { existsSync } from "node:fs";
1285
+ import { resolve } from "node:path";
1286
+ function isPackageInstalled(packageName) {
1287
+ const modulePath = resolve(process.cwd(), "node_modules", packageName);
1288
+ return existsSync(modulePath);
1289
+ }
1290
+ function detectPackageManager() {
1291
+ const cwd = process.cwd();
1292
+ if (existsSync(resolve(cwd, "bun.lockb")) || existsSync(resolve(cwd, "bun.lock")))
1293
+ return "bun";
1294
+ if (existsSync(resolve(cwd, "pnpm-lock.yaml"))) return "pnpm";
1295
+ if (existsSync(resolve(cwd, "yarn.lock"))) return "yarn";
1296
+ return "npm";
1297
+ }
1298
+ function prodInstallCommand(pm, packageName) {
1299
+ switch (pm) {
1300
+ case "bun":
1301
+ return `bun add ${packageName}`;
1302
+ case "pnpm":
1303
+ return `pnpm add ${packageName}`;
1304
+ case "yarn":
1305
+ return `yarn add ${packageName}`;
1306
+ case "npm":
1307
+ return `npm install ${packageName}`;
1261
1308
  }
1262
- };
1309
+ }
1310
+ function devInstallCommand(pm, packageName) {
1311
+ switch (pm) {
1312
+ case "bun":
1313
+ return `bun add -D ${packageName}`;
1314
+ case "pnpm":
1315
+ return `pnpm add -D ${packageName}`;
1316
+ case "yarn":
1317
+ return `yarn add -D ${packageName}`;
1318
+ case "npm":
1319
+ return `npm install -D ${packageName}`;
1320
+ }
1321
+ }
1322
+ function toCamelCase(str) {
1323
+ return str.replace(/_([a-z])/g, (_3, c) => c.toUpperCase());
1324
+ }
1325
+ function drizzleTsType(dataType) {
1326
+ switch (dataType) {
1327
+ case "number":
1328
+ return "number";
1329
+ case "boolean":
1330
+ return "boolean";
1331
+ case "date":
1332
+ return "Date";
1333
+ case "json":
1334
+ return "Record<string, unknown>";
1335
+ default:
1336
+ return "string";
1337
+ }
1338
+ }
1339
+ function generateDrizzleFromSchema(schema) {
1340
+ const varName = `${toCamelCase(schema.tableName)}Table`;
1341
+ const schemaVarName = `${toCamelCase(schema.tableName)}Schema`;
1342
+ const columnDefs = schema.columns.map((col) => {
1343
+ const opts = [];
1344
+ if (col.dataType !== "string") {
1345
+ opts.push(`dataType: '${col.dataType}'`);
1346
+ }
1347
+ if (col.searchOps.includes("equality")) {
1348
+ opts.push("equality: true");
1349
+ }
1350
+ if (col.searchOps.includes("orderAndRange")) {
1351
+ opts.push("orderAndRange: true");
1352
+ }
1353
+ if (col.searchOps.includes("freeTextSearch")) {
1354
+ opts.push("freeTextSearch: true");
1355
+ }
1356
+ const tsType = drizzleTsType(col.dataType);
1357
+ const optsStr = opts.length > 0 ? `, {
1358
+ ${opts.join(",\n ")},
1359
+ }` : "";
1360
+ return ` ${col.name}: encryptedType<${tsType}>('${col.name}'${optsStr}),`;
1361
+ });
1362
+ return `import { pgTable, integer, timestamp } from 'drizzle-orm/pg-core'
1363
+ import { encryptedType, extractEncryptionSchema } from '@cipherstash/stack/drizzle'
1364
+ import { Encryption } from '@cipherstash/stack'
1365
+
1366
+ export const ${varName} = pgTable('${schema.tableName}', {
1367
+ id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
1368
+ ${columnDefs.join("\n")}
1369
+ createdAt: timestamp('created_at').defaultNow(),
1370
+ })
1263
1371
 
1264
- // src/bin/commands/init/steps/detect-database-url.ts
1265
- function maskUrl(url) {
1266
- return url.replace(/:\/\/([^:]+):([^@]+)@/, "://***@");
1372
+ const ${schemaVarName} = extractEncryptionSchema(${varName})
1373
+
1374
+ export const encryptionClient = await Encryption({
1375
+ schemas: [${schemaVarName}],
1376
+ })
1377
+ `;
1267
1378
  }
1268
- var detectDatabaseUrlStep = {
1269
- id: "detect-database-url",
1270
- name: "Detect database URL",
1379
+ function generateSchemaFromDef(schema) {
1380
+ const varName = `${toCamelCase(schema.tableName)}Table`;
1381
+ const columnDefs = schema.columns.map((col) => {
1382
+ const parts = [` ${col.name}: encryptedColumn('${col.name}')`];
1383
+ if (col.dataType !== "string") {
1384
+ parts.push(`.dataType('${col.dataType}')`);
1385
+ }
1386
+ for (const op of col.searchOps) {
1387
+ parts.push(`.${op}()`);
1388
+ }
1389
+ return `${parts.join("\n ")},`;
1390
+ });
1391
+ return `import { encryptedTable, encryptedColumn } from '@cipherstash/stack/schema'
1392
+ import { Encryption } from '@cipherstash/stack'
1393
+
1394
+ export const ${varName} = encryptedTable('${schema.tableName}', {
1395
+ ${columnDefs.join("\n")}
1396
+ })
1397
+
1398
+ export const encryptionClient = await Encryption({
1399
+ schemas: [${varName}],
1400
+ })
1401
+ `;
1402
+ }
1403
+ function generateClientFromSchema(integration, schema) {
1404
+ switch (integration) {
1405
+ case "drizzle":
1406
+ return generateDrizzleFromSchema(schema);
1407
+ case "supabase":
1408
+ case "postgresql":
1409
+ return generateSchemaFromDef(schema);
1410
+ }
1411
+ }
1412
+ function generatePlaceholderClient(integration) {
1413
+ const placeholder = {
1414
+ tableName: "users",
1415
+ columns: [
1416
+ {
1417
+ name: "email",
1418
+ dataType: "string",
1419
+ searchOps: ["equality", "freeTextSearch"]
1420
+ },
1421
+ {
1422
+ name: "name",
1423
+ dataType: "string",
1424
+ searchOps: ["equality", "freeTextSearch"]
1425
+ }
1426
+ ]
1427
+ };
1428
+ return generateClientFromSchema(integration, placeholder);
1429
+ }
1430
+
1431
+ // src/bin/commands/init/steps/build-schema.ts
1432
+ var DEFAULT_CLIENT_PATH = "./src/encryption/index.ts";
1433
+ async function addColumn(index) {
1434
+ const name = await he({
1435
+ message: `Column ${index} name:`,
1436
+ placeholder: index === 1 ? "email" : "name",
1437
+ validate(value) {
1438
+ if (!value || value.trim().length === 0) {
1439
+ return "Column name is required.";
1440
+ }
1441
+ if (!/^[a-z_][a-z0-9_]*$/i.test(value)) {
1442
+ return "Column name must be a valid identifier.";
1443
+ }
1444
+ }
1445
+ });
1446
+ if (pD(name)) return void 0;
1447
+ const dataType = await ve({
1448
+ message: `Data type for "${name}":`,
1449
+ options: [
1450
+ { value: "string", label: "string", hint: "text, email, name, etc." },
1451
+ { value: "number", label: "number", hint: "integer or decimal" },
1452
+ { value: "boolean", label: "boolean" },
1453
+ { value: "date", label: "date", hint: "Date object" },
1454
+ { value: "json", label: "json", hint: "structured JSON data" }
1455
+ ]
1456
+ });
1457
+ if (pD(dataType)) return void 0;
1458
+ const searchOptions = [
1459
+ { value: "equality", label: "Exact match", hint: "eq, neq, in" },
1460
+ {
1461
+ value: "orderAndRange",
1462
+ label: "Order and range",
1463
+ hint: "gt, gte, lt, lte, between, sorting"
1464
+ }
1465
+ ];
1466
+ if (dataType === "string") {
1467
+ searchOptions.push({
1468
+ value: "freeTextSearch",
1469
+ label: "Free-text search",
1470
+ hint: "like, ilike, substring matching"
1471
+ });
1472
+ }
1473
+ const searchOps = await fe({
1474
+ message: `Search operations for "${name}":`,
1475
+ options: searchOptions,
1476
+ required: false
1477
+ });
1478
+ if (pD(searchOps)) return void 0;
1479
+ return { name, dataType, searchOps };
1480
+ }
1481
+ async function buildSchema() {
1482
+ const tableName = await he({
1483
+ message: "What is the name of your table?",
1484
+ placeholder: "users",
1485
+ validate(value) {
1486
+ if (!value || value.trim().length === 0) {
1487
+ return "Table name is required.";
1488
+ }
1489
+ if (!/^[a-z_][a-z0-9_]*$/i.test(value)) {
1490
+ return "Table name must be a valid identifier (letters, numbers, underscores).";
1491
+ }
1492
+ }
1493
+ });
1494
+ if (pD(tableName)) return void 0;
1495
+ const columns = [];
1496
+ M2.info("Add encrypted columns to your table. You can add more later.");
1497
+ while (true) {
1498
+ const column = await addColumn(columns.length + 1);
1499
+ if (!column) return void 0;
1500
+ columns.push(column);
1501
+ const addMore = await ye({
1502
+ message: "Add another encrypted column?",
1503
+ initialValue: false
1504
+ });
1505
+ if (pD(addMore)) return void 0;
1506
+ if (!addMore) break;
1507
+ }
1508
+ M2.success(
1509
+ `Schema defined: ${tableName} with ${columns.length} encrypted column${columns.length !== 1 ? "s" : ""}`
1510
+ );
1511
+ return { tableName, columns };
1512
+ }
1513
+ var buildSchemaStep = {
1514
+ id: "build-schema",
1515
+ name: "Build encryption schema",
1271
1516
  async run(state, _provider) {
1272
- const envUrl = process.env.DATABASE_URL;
1273
- if (envUrl) {
1274
- M2.success(`Detected DATABASE_URL in .env
1275
- ${maskUrl(envUrl)}`);
1276
- return { ...state, databaseUrl: envUrl };
1517
+ if (!state.connectionMethod) {
1518
+ M2.warn("Skipping schema generation (no connection method selected)");
1519
+ return { ...state, schemaGenerated: false };
1277
1520
  }
1278
- M2.warn("No DATABASE_URL found in .env");
1279
- const action = await ve({
1280
- message: "How would you like to proceed?",
1521
+ const integration = toIntegration(state.connectionMethod);
1522
+ const clientFilePath = await he({
1523
+ message: "Where should we create your encryption client?",
1524
+ placeholder: DEFAULT_CLIENT_PATH,
1525
+ defaultValue: DEFAULT_CLIENT_PATH
1526
+ });
1527
+ if (pD(clientFilePath)) throw new CancelledError();
1528
+ const resolvedPath = resolve2(process.cwd(), clientFilePath);
1529
+ if (existsSync2(resolvedPath)) {
1530
+ const action = await ve({
1531
+ message: `${clientFilePath} already exists. What would you like to do?`,
1532
+ options: [
1533
+ {
1534
+ value: "keep",
1535
+ label: "Keep existing file",
1536
+ hint: "skip code generation"
1537
+ },
1538
+ { value: "overwrite", label: "Overwrite with new schema" }
1539
+ ]
1540
+ });
1541
+ if (pD(action)) throw new CancelledError();
1542
+ if (action === "keep") {
1543
+ M2.info("Keeping existing encryption client file.");
1544
+ return { ...state, clientFilePath, schemaGenerated: false };
1545
+ }
1546
+ }
1547
+ const schemaChoice = await ve({
1548
+ message: "How would you like to set up your encryption schema?",
1281
1549
  options: [
1282
- { value: "enter", label: "Enter database URL now" },
1283
- { value: "skip", label: "Skip for now (I'll add it later)" }
1550
+ {
1551
+ value: "build",
1552
+ label: "Build schema now",
1553
+ hint: "interactive wizard"
1554
+ },
1555
+ {
1556
+ value: "placeholder",
1557
+ label: "Use placeholder schema",
1558
+ hint: "edit later"
1559
+ }
1284
1560
  ]
1285
1561
  });
1286
- if (pD(action)) throw new CancelledError();
1287
- if (action === "enter") {
1288
- const url = await he({
1289
- message: "Enter your database URL:",
1290
- placeholder: "postgresql://user:password@host:5432/database",
1291
- validate: (val) => {
1292
- if (!val.trim()) return "Database URL is required";
1293
- if (!val.startsWith("postgres"))
1294
- return "Must be a PostgreSQL connection string";
1295
- }
1296
- });
1297
- if (pD(url)) throw new CancelledError();
1298
- return { ...state, databaseUrl: url };
1562
+ if (pD(schemaChoice)) throw new CancelledError();
1563
+ let fileContents;
1564
+ if (schemaChoice === "build") {
1565
+ const schema = await buildSchema();
1566
+ if (!schema) throw new CancelledError();
1567
+ fileContents = generateClientFromSchema(integration, schema);
1568
+ } else {
1569
+ fileContents = generatePlaceholderClient(integration);
1299
1570
  }
1300
- return state;
1571
+ const dir = dirname(resolvedPath);
1572
+ if (!existsSync2(dir)) {
1573
+ mkdirSync(dir, { recursive: true });
1574
+ }
1575
+ writeFileSync(resolvedPath, fileContents, "utf-8");
1576
+ M2.success(`Encryption client written to ${clientFilePath}`);
1577
+ return { ...state, clientFilePath, schemaGenerated: true };
1301
1578
  }
1302
1579
  };
1303
1580
 
1304
- // src/bin/commands/init/steps/install-eql.ts
1305
- var installEqlStep = {
1306
- id: "install-eql",
1307
- name: "Install EQL extension",
1581
+ // src/bin/commands/init/steps/install-forge.ts
1582
+ import { execSync } from "node:child_process";
1583
+ var STACK_PACKAGE = "@cipherstash/stack";
1584
+ var FORGE_PACKAGE = "@cipherstash/stack-forge";
1585
+ async function installIfNeeded(packageName, buildCommand, depLabel) {
1586
+ if (isPackageInstalled(packageName)) {
1587
+ M2.success(`${packageName} is already installed.`);
1588
+ return true;
1589
+ }
1590
+ const pm = detectPackageManager();
1591
+ const cmd = buildCommand(pm, packageName);
1592
+ const install = await ye({
1593
+ message: `Install ${packageName} as a ${depLabel} dependency? (${cmd})`
1594
+ });
1595
+ if (pD(install)) throw new CancelledError();
1596
+ if (!install) {
1597
+ M2.info(`Skipping ${packageName} installation.`);
1598
+ Me(
1599
+ `You can install it manually later:
1600
+ ${cmd}`,
1601
+ "Manual Installation"
1602
+ );
1603
+ return false;
1604
+ }
1605
+ const s = Y2();
1606
+ s.start(`Installing ${packageName}...`);
1607
+ try {
1608
+ execSync(cmd, { cwd: process.cwd(), stdio: "pipe" });
1609
+ s.stop(`${packageName} installed successfully`);
1610
+ return true;
1611
+ } catch (err) {
1612
+ const message = err instanceof Error ? err.message : String(err);
1613
+ s.stop(`${packageName} installation failed`);
1614
+ M2.error(message);
1615
+ Me(`You can install it manually:
1616
+ ${cmd}`, "Manual Installation");
1617
+ return false;
1618
+ }
1619
+ }
1620
+ var installForgeStep = {
1621
+ id: "install-forge",
1622
+ name: "Install stack dependencies",
1308
1623
  async run(state, _provider) {
1309
- if (!state.databaseUrl) {
1310
- M2.warn("Skipping EQL extension installation (no database URL)");
1311
- return { ...state, eqlInstalled: false };
1312
- }
1313
- const install = await ye({
1314
- message: "Install the EQL encryption extension in your database?"
1315
- });
1316
- if (pD(install)) throw new CancelledError();
1317
- if (!install) {
1318
- M2.info("Skipping EQL extension installation");
1319
- Me(
1320
- "You can install it manually later:\n CREATE EXTENSION IF NOT EXISTS eql_v2;\n\nOr re-run this command:\n npx @cipherstash/stack init",
1321
- "Manual Installation"
1322
- );
1323
- return { ...state, eqlInstalled: false };
1324
- }
1325
- const s = Y2();
1326
- s.start("Installing EQL extension...");
1327
- try {
1328
- await installEqlExtension(state.databaseUrl);
1329
- s.stop("EQL extension installed successfully");
1330
- return { ...state, eqlInstalled: true };
1331
- } catch (err) {
1332
- const message = err instanceof Error ? err.message : String(err);
1333
- s.stop("EQL extension installation failed");
1334
- M2.error(message);
1335
- Me(
1336
- "You can install it manually:\n CREATE EXTENSION IF NOT EXISTS eql_v2;\n\nOr install via Database.dev:\n https://database.dev/cipherstash/eql",
1337
- "Manual Installation"
1338
- );
1339
- return { ...state, eqlInstalled: false };
1340
- }
1624
+ const stackInstalled = await installIfNeeded(STACK_PACKAGE, prodInstallCommand, "production");
1625
+ const forgeInstalled = await installIfNeeded(FORGE_PACKAGE, devInstallCommand, "dev");
1626
+ return { ...state, forgeInstalled, stackInstalled };
1341
1627
  }
1342
1628
  };
1343
1629
 
@@ -1367,75 +1653,14 @@ var selectConnectionStep = {
1367
1653
  }
1368
1654
  };
1369
1655
 
1370
- // src/bin/commands/init/steps/select-region.ts
1371
- var REGIONS = [
1372
- { value: "ap-southeast-2", label: "Asia Pacific (Sydney)" },
1373
- { value: "us-east-1", label: "US East (N. Virginia)" },
1374
- { value: "us-west-2", label: "US West (Oregon)" },
1375
- { value: "eu-west-1", label: "Europe (Ireland)" }
1376
- ];
1377
- var selectRegionStep = {
1378
- id: "select-region",
1379
- name: "Select region",
1380
- async run(state, _provider) {
1381
- const region = await ve({
1382
- message: "Where should we create your workspace?",
1383
- options: REGIONS
1384
- });
1385
- if (pD(region)) throw new CancelledError();
1386
- return { ...state, region };
1387
- }
1388
- };
1389
-
1390
- // src/bin/commands/init/steps/select-workspace.ts
1391
- var selectWorkspaceStep = {
1392
- id: "select-workspace",
1393
- name: "Select workspace",
1394
- async run(state, _provider) {
1395
- const s = Y2();
1396
- s.start("Loading workspaces...");
1397
- const workspaces = await fetchWorkspaces(state.accessToken);
1398
- s.stop("Workspaces loaded");
1399
- const options = [
1400
- ...workspaces.map((ws2) => ({ value: ws2.id, label: ws2.name })),
1401
- { value: "__create__", label: "Create new workspace" }
1402
- ];
1403
- const selected = await ve({
1404
- message: "Select a workspace",
1405
- options
1406
- });
1407
- if (pD(selected)) throw new CancelledError();
1408
- if (selected === "__create__") {
1409
- const name = await he({
1410
- message: "What should we call your new workspace?",
1411
- placeholder: "my-project",
1412
- validate: (val) => {
1413
- if (!val.trim()) return "Workspace name is required";
1414
- }
1415
- });
1416
- if (pD(name)) throw new CancelledError();
1417
- const s2 = Y2();
1418
- s2.start("Creating workspace...");
1419
- const ws2 = await createWorkspace(state.accessToken, name);
1420
- s2.stop(`Workspace created: ${ws2.name}`);
1421
- return { ...state, workspaceId: ws2.id, workspaceName: ws2.name };
1422
- }
1423
- const ws = workspaces.find((w2) => w2.id === selected);
1424
- return { ...state, workspaceId: ws.id, workspaceName: ws.name };
1425
- }
1426
- };
1427
-
1428
1656
  // src/bin/commands/init/index.ts
1429
1657
  var PROVIDER_MAP = {
1430
1658
  supabase: createSupabaseProvider
1431
1659
  };
1432
1660
  var STEPS = [
1433
- authenticateStep,
1434
- selectWorkspaceStep,
1435
- selectRegionStep,
1436
1661
  selectConnectionStep,
1437
- detectDatabaseUrlStep,
1438
- installEqlStep,
1662
+ buildSchemaStep,
1663
+ installForgeStep,
1439
1664
  nextStepsStep
1440
1665
  ];
1441
1666
  function resolveProvider(flags) {
@@ -1449,9 +1674,6 @@ function resolveProvider(flags) {
1449
1674
  async function initCommand(flags) {
1450
1675
  const provider = resolveProvider(flags);
1451
1676
  Ie("CipherStash Stack Setup");
1452
- M2.warn(
1453
- "This command is a prototype and a sneak peek at what's next. It doesn't perform any actual setup yet."
1454
- );
1455
1677
  M2.info(provider.introMessage);
1456
1678
  let state = {};
1457
1679
  try {
@@ -4966,7 +5188,7 @@ async function secretsCommand(args) {
4966
5188
 
4967
5189
  // src/bin/stash.ts
4968
5190
  (0, import_dotenv2.config)();
4969
- var __dirname = dirname(fileURLToPath(import.meta.url));
5191
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
4970
5192
  var pkg = JSON.parse(
4971
5193
  readFileSync(join(__dirname, "../../package.json"), "utf-8")
4972
5194
  );