@cipherstash/stack 0.8.0 → 0.9.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,104 +1268,343 @@ 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 devInstallCommand(pm, packageName) {
1299
+ switch (pm) {
1300
+ case "bun":
1301
+ return `bun add -D ${packageName}`;
1302
+ case "pnpm":
1303
+ return `pnpm add -D ${packageName}`;
1304
+ case "yarn":
1305
+ return `yarn add -D ${packageName}`;
1306
+ case "npm":
1307
+ return `npm install -D ${packageName}`;
1261
1308
  }
1262
- };
1309
+ }
1310
+ function toCamelCase(str) {
1311
+ return str.replace(/_([a-z])/g, (_3, c) => c.toUpperCase());
1312
+ }
1313
+ function drizzleTsType(dataType) {
1314
+ switch (dataType) {
1315
+ case "number":
1316
+ return "number";
1317
+ case "boolean":
1318
+ return "boolean";
1319
+ case "date":
1320
+ return "Date";
1321
+ case "json":
1322
+ return "Record<string, unknown>";
1323
+ default:
1324
+ return "string";
1325
+ }
1326
+ }
1327
+ function generateDrizzleFromSchema(schema) {
1328
+ const varName = `${toCamelCase(schema.tableName)}Table`;
1329
+ const schemaVarName = `${toCamelCase(schema.tableName)}Schema`;
1330
+ const columnDefs = schema.columns.map((col) => {
1331
+ const opts = [];
1332
+ if (col.dataType !== "string") {
1333
+ opts.push(`dataType: '${col.dataType}'`);
1334
+ }
1335
+ if (col.searchOps.includes("equality")) {
1336
+ opts.push("equality: true");
1337
+ }
1338
+ if (col.searchOps.includes("orderAndRange")) {
1339
+ opts.push("orderAndRange: true");
1340
+ }
1341
+ if (col.searchOps.includes("freeTextSearch")) {
1342
+ opts.push("freeTextSearch: true");
1343
+ }
1344
+ const tsType = drizzleTsType(col.dataType);
1345
+ const optsStr = opts.length > 0 ? `, {
1346
+ ${opts.join(",\n ")},
1347
+ }` : "";
1348
+ return ` ${col.name}: encryptedType<${tsType}>('${col.name}'${optsStr}),`;
1349
+ });
1350
+ return `import { pgTable, integer, timestamp } from 'drizzle-orm/pg-core'
1351
+ import { encryptedType, extractEncryptionSchema } from '@cipherstash/stack/drizzle'
1352
+ import { Encryption } from '@cipherstash/stack'
1263
1353
 
1264
- // src/bin/commands/init/steps/detect-database-url.ts
1265
- function maskUrl(url) {
1266
- return url.replace(/:\/\/([^:]+):([^@]+)@/, "://***@");
1354
+ export const ${varName} = pgTable('${schema.tableName}', {
1355
+ id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
1356
+ ${columnDefs.join("\n")}
1357
+ createdAt: timestamp('created_at').defaultNow(),
1358
+ })
1359
+
1360
+ const ${schemaVarName} = extractEncryptionSchema(${varName})
1361
+
1362
+ export const encryptionClient = await Encryption({
1363
+ schemas: [${schemaVarName}],
1364
+ })
1365
+ `;
1366
+ }
1367
+ function generateSchemaFromDef(schema) {
1368
+ const varName = `${toCamelCase(schema.tableName)}Table`;
1369
+ const columnDefs = schema.columns.map((col) => {
1370
+ const parts = [` ${col.name}: encryptedColumn('${col.name}')`];
1371
+ if (col.dataType !== "string") {
1372
+ parts.push(`.dataType('${col.dataType}')`);
1373
+ }
1374
+ for (const op of col.searchOps) {
1375
+ parts.push(`.${op}()`);
1376
+ }
1377
+ return `${parts.join("\n ")},`;
1378
+ });
1379
+ return `import { encryptedTable, encryptedColumn } from '@cipherstash/stack/schema'
1380
+ import { Encryption } from '@cipherstash/stack'
1381
+
1382
+ export const ${varName} = encryptedTable('${schema.tableName}', {
1383
+ ${columnDefs.join("\n")}
1384
+ })
1385
+
1386
+ export const encryptionClient = await Encryption({
1387
+ schemas: [${varName}],
1388
+ })
1389
+ `;
1390
+ }
1391
+ function generateClientFromSchema(integration, schema) {
1392
+ switch (integration) {
1393
+ case "drizzle":
1394
+ return generateDrizzleFromSchema(schema);
1395
+ case "supabase":
1396
+ case "postgresql":
1397
+ return generateSchemaFromDef(schema);
1398
+ }
1399
+ }
1400
+ function generatePlaceholderClient(integration) {
1401
+ const placeholder = {
1402
+ tableName: "users",
1403
+ columns: [
1404
+ {
1405
+ name: "email",
1406
+ dataType: "string",
1407
+ searchOps: ["equality", "freeTextSearch"]
1408
+ },
1409
+ {
1410
+ name: "name",
1411
+ dataType: "string",
1412
+ searchOps: ["equality", "freeTextSearch"]
1413
+ }
1414
+ ]
1415
+ };
1416
+ return generateClientFromSchema(integration, placeholder);
1267
1417
  }
1268
- var detectDatabaseUrlStep = {
1269
- id: "detect-database-url",
1270
- name: "Detect database URL",
1418
+
1419
+ // src/bin/commands/init/steps/build-schema.ts
1420
+ var DEFAULT_CLIENT_PATH = "./src/encryption/index.ts";
1421
+ async function addColumn(index) {
1422
+ const name = await he({
1423
+ message: `Column ${index} name:`,
1424
+ placeholder: index === 1 ? "email" : "name",
1425
+ validate(value) {
1426
+ if (!value || value.trim().length === 0) {
1427
+ return "Column name is required.";
1428
+ }
1429
+ if (!/^[a-z_][a-z0-9_]*$/i.test(value)) {
1430
+ return "Column name must be a valid identifier.";
1431
+ }
1432
+ }
1433
+ });
1434
+ if (pD(name)) return void 0;
1435
+ const dataType = await ve({
1436
+ message: `Data type for "${name}":`,
1437
+ options: [
1438
+ { value: "string", label: "string", hint: "text, email, name, etc." },
1439
+ { value: "number", label: "number", hint: "integer or decimal" },
1440
+ { value: "boolean", label: "boolean" },
1441
+ { value: "date", label: "date", hint: "Date object" },
1442
+ { value: "json", label: "json", hint: "structured JSON data" }
1443
+ ]
1444
+ });
1445
+ if (pD(dataType)) return void 0;
1446
+ const searchOptions = [
1447
+ { value: "equality", label: "Exact match", hint: "eq, neq, in" },
1448
+ {
1449
+ value: "orderAndRange",
1450
+ label: "Order and range",
1451
+ hint: "gt, gte, lt, lte, between, sorting"
1452
+ }
1453
+ ];
1454
+ if (dataType === "string") {
1455
+ searchOptions.push({
1456
+ value: "freeTextSearch",
1457
+ label: "Free-text search",
1458
+ hint: "like, ilike, substring matching"
1459
+ });
1460
+ }
1461
+ const searchOps = await fe({
1462
+ message: `Search operations for "${name}":`,
1463
+ options: searchOptions,
1464
+ required: false
1465
+ });
1466
+ if (pD(searchOps)) return void 0;
1467
+ return { name, dataType, searchOps };
1468
+ }
1469
+ async function buildSchema() {
1470
+ const tableName = await he({
1471
+ message: "What is the name of your table?",
1472
+ placeholder: "users",
1473
+ validate(value) {
1474
+ if (!value || value.trim().length === 0) {
1475
+ return "Table name is required.";
1476
+ }
1477
+ if (!/^[a-z_][a-z0-9_]*$/i.test(value)) {
1478
+ return "Table name must be a valid identifier (letters, numbers, underscores).";
1479
+ }
1480
+ }
1481
+ });
1482
+ if (pD(tableName)) return void 0;
1483
+ const columns = [];
1484
+ M2.info("Add encrypted columns to your table. You can add more later.");
1485
+ while (true) {
1486
+ const column = await addColumn(columns.length + 1);
1487
+ if (!column) return void 0;
1488
+ columns.push(column);
1489
+ const addMore = await ye({
1490
+ message: "Add another encrypted column?",
1491
+ initialValue: false
1492
+ });
1493
+ if (pD(addMore)) return void 0;
1494
+ if (!addMore) break;
1495
+ }
1496
+ M2.success(
1497
+ `Schema defined: ${tableName} with ${columns.length} encrypted column${columns.length !== 1 ? "s" : ""}`
1498
+ );
1499
+ return { tableName, columns };
1500
+ }
1501
+ var buildSchemaStep = {
1502
+ id: "build-schema",
1503
+ name: "Build encryption schema",
1271
1504
  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 };
1505
+ if (!state.connectionMethod) {
1506
+ M2.warn("Skipping schema generation (no connection method selected)");
1507
+ return { ...state, schemaGenerated: false };
1277
1508
  }
1278
- M2.warn("No DATABASE_URL found in .env");
1279
- const action = await ve({
1280
- message: "How would you like to proceed?",
1509
+ const integration = toIntegration(state.connectionMethod);
1510
+ const clientFilePath = await he({
1511
+ message: "Where should we create your encryption client?",
1512
+ placeholder: DEFAULT_CLIENT_PATH,
1513
+ defaultValue: DEFAULT_CLIENT_PATH
1514
+ });
1515
+ if (pD(clientFilePath)) throw new CancelledError();
1516
+ const resolvedPath = resolve2(process.cwd(), clientFilePath);
1517
+ if (existsSync2(resolvedPath)) {
1518
+ const action = await ve({
1519
+ message: `${clientFilePath} already exists. What would you like to do?`,
1520
+ options: [
1521
+ {
1522
+ value: "keep",
1523
+ label: "Keep existing file",
1524
+ hint: "skip code generation"
1525
+ },
1526
+ { value: "overwrite", label: "Overwrite with new schema" }
1527
+ ]
1528
+ });
1529
+ if (pD(action)) throw new CancelledError();
1530
+ if (action === "keep") {
1531
+ M2.info("Keeping existing encryption client file.");
1532
+ return { ...state, clientFilePath, schemaGenerated: false };
1533
+ }
1534
+ }
1535
+ const schemaChoice = await ve({
1536
+ message: "How would you like to set up your encryption schema?",
1281
1537
  options: [
1282
- { value: "enter", label: "Enter database URL now" },
1283
- { value: "skip", label: "Skip for now (I'll add it later)" }
1538
+ {
1539
+ value: "build",
1540
+ label: "Build schema now",
1541
+ hint: "interactive wizard"
1542
+ },
1543
+ {
1544
+ value: "placeholder",
1545
+ label: "Use placeholder schema",
1546
+ hint: "edit later"
1547
+ }
1284
1548
  ]
1285
1549
  });
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 };
1550
+ if (pD(schemaChoice)) throw new CancelledError();
1551
+ let fileContents;
1552
+ if (schemaChoice === "build") {
1553
+ const schema = await buildSchema();
1554
+ if (!schema) throw new CancelledError();
1555
+ fileContents = generateClientFromSchema(integration, schema);
1556
+ } else {
1557
+ fileContents = generatePlaceholderClient(integration);
1299
1558
  }
1300
- return state;
1559
+ const dir = dirname(resolvedPath);
1560
+ if (!existsSync2(dir)) {
1561
+ mkdirSync(dir, { recursive: true });
1562
+ }
1563
+ writeFileSync(resolvedPath, fileContents, "utf-8");
1564
+ M2.success(`Encryption client written to ${clientFilePath}`);
1565
+ return { ...state, clientFilePath, schemaGenerated: true };
1301
1566
  }
1302
1567
  };
1303
1568
 
1304
- // src/bin/commands/init/steps/install-eql.ts
1305
- var installEqlStep = {
1306
- id: "install-eql",
1307
- name: "Install EQL extension",
1569
+ // src/bin/commands/init/steps/install-forge.ts
1570
+ import { execSync } from "node:child_process";
1571
+ var FORGE_PACKAGE = "@cipherstash/stack-forge";
1572
+ var installForgeStep = {
1573
+ id: "install-forge",
1574
+ name: "Install stack-forge",
1308
1575
  async run(state, _provider) {
1309
- if (!state.databaseUrl) {
1310
- M2.warn("Skipping EQL extension installation (no database URL)");
1311
- return { ...state, eqlInstalled: false };
1576
+ if (isPackageInstalled(FORGE_PACKAGE)) {
1577
+ M2.success(`${FORGE_PACKAGE} is already installed.`);
1578
+ return { ...state, forgeInstalled: true };
1312
1579
  }
1580
+ const pm = detectPackageManager();
1581
+ const cmd = devInstallCommand(pm, FORGE_PACKAGE);
1313
1582
  const install = await ye({
1314
- message: "Install the EQL encryption extension in your database?"
1583
+ message: `Install ${FORGE_PACKAGE} as a dev dependency? (${cmd})`
1315
1584
  });
1316
1585
  if (pD(install)) throw new CancelledError();
1317
1586
  if (!install) {
1318
- M2.info("Skipping EQL extension installation");
1587
+ M2.info(`Skipping ${FORGE_PACKAGE} installation.`);
1319
1588
  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",
1589
+ `You can install it manually later:
1590
+ ${cmd}`,
1321
1591
  "Manual Installation"
1322
1592
  );
1323
- return { ...state, eqlInstalled: false };
1593
+ return { ...state, forgeInstalled: false };
1324
1594
  }
1325
1595
  const s = Y2();
1326
- s.start("Installing EQL extension...");
1596
+ s.start(`Installing ${FORGE_PACKAGE}...`);
1327
1597
  try {
1328
- await installEqlExtension(state.databaseUrl);
1329
- s.stop("EQL extension installed successfully");
1330
- return { ...state, eqlInstalled: true };
1598
+ execSync(cmd, { cwd: process.cwd(), stdio: "pipe" });
1599
+ s.stop(`${FORGE_PACKAGE} installed successfully`);
1600
+ return { ...state, forgeInstalled: true };
1331
1601
  } catch (err) {
1332
1602
  const message = err instanceof Error ? err.message : String(err);
1333
- s.stop("EQL extension installation failed");
1603
+ s.stop(`${FORGE_PACKAGE} installation failed`);
1334
1604
  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 };
1605
+ Me(`You can install it manually:
1606
+ ${cmd}`, "Manual Installation");
1607
+ return { ...state, forgeInstalled: false };
1340
1608
  }
1341
1609
  }
1342
1610
  };
@@ -1367,75 +1635,14 @@ var selectConnectionStep = {
1367
1635
  }
1368
1636
  };
1369
1637
 
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
1638
  // src/bin/commands/init/index.ts
1429
1639
  var PROVIDER_MAP = {
1430
1640
  supabase: createSupabaseProvider
1431
1641
  };
1432
1642
  var STEPS = [
1433
- authenticateStep,
1434
- selectWorkspaceStep,
1435
- selectRegionStep,
1436
1643
  selectConnectionStep,
1437
- detectDatabaseUrlStep,
1438
- installEqlStep,
1644
+ buildSchemaStep,
1645
+ installForgeStep,
1439
1646
  nextStepsStep
1440
1647
  ];
1441
1648
  function resolveProvider(flags) {
@@ -1449,9 +1656,6 @@ function resolveProvider(flags) {
1449
1656
  async function initCommand(flags) {
1450
1657
  const provider = resolveProvider(flags);
1451
1658
  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
1659
  M2.info(provider.introMessage);
1456
1660
  let state = {};
1457
1661
  try {
@@ -4966,7 +5170,7 @@ async function secretsCommand(args) {
4966
5170
 
4967
5171
  // src/bin/stash.ts
4968
5172
  (0, import_dotenv2.config)();
4969
- var __dirname = dirname(fileURLToPath(import.meta.url));
5173
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
4970
5174
  var pkg = JSON.parse(
4971
5175
  readFileSync(join(__dirname, "../../package.json"), "utf-8")
4972
5176
  );