@hasna/sandboxes 0.1.29 → 0.1.30

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/README.md CHANGED
@@ -65,19 +65,19 @@ MCP_HTTP=1 sandboxes-mcp
65
65
  sandboxes-serve
66
66
  ```
67
67
 
68
- ## Cloud Sync
68
+ ## Storage Sync
69
69
 
70
- Cloud sync is optional. By default sandboxes use local SQLite at `~/.hasna/sandboxes/`.
70
+ Remote storage sync is optional. By default sandboxes use local SQLite at `~/.hasna/sandboxes/`.
71
71
 
72
72
  ```bash
73
- sandboxes cloud status
74
- sandboxes cloud push
75
- sandboxes cloud pull
76
- sandboxes cloud sync
73
+ sandboxes storage status
74
+ sandboxes storage push
75
+ sandboxes storage pull
76
+ sandboxes storage sync
77
77
  ```
78
78
 
79
- Set `HASNA_SANDBOXES_CLOUD_DATABASE_URL` or configure
80
- `~/.hasna/sandboxes/cloud/config.json` to run in hybrid/cloud mode with
79
+ Set `HASNA_SANDBOXES_DATABASE_URL` or configure
80
+ `~/.hasna/sandboxes/storage/config.json` to run in hybrid/remote mode with
81
81
  PostgreSQL.
82
82
 
83
83
  ## Data Directory
package/dist/cli/index.js CHANGED
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- } else {}
996
+ }
997
997
  };
998
998
  }
999
999
  return this;
@@ -2157,18 +2157,19 @@ function now() {
2157
2157
  return new Date().toISOString().replace("T", " ").replace("Z", "");
2158
2158
  }
2159
2159
  function resolvePartialId(table, partialId) {
2160
+ if (!PARTIAL_ID_TABLES.has(table)) {
2161
+ throw new Error(`Invalid partial-id table: ${table}`);
2162
+ }
2160
2163
  const database = getDatabase();
2161
- const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? || '%'`).all(partialId);
2162
- if (rows.length === 1)
2163
- return rows[0].id;
2164
- if (rows.length === 0)
2165
- return null;
2166
- const exact = rows.find((r) => r.id === partialId);
2164
+ const exact = database.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
2167
2165
  if (exact)
2168
2166
  return exact.id;
2167
+ const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? ORDER BY id LIMIT 2`).all(`${partialId}%`);
2168
+ if (rows.length === 1)
2169
+ return rows[0].id;
2169
2170
  return null;
2170
2171
  }
2171
- var MIGRATIONS, db = null;
2172
+ var MIGRATIONS, db = null, PARTIAL_ID_TABLES;
2172
2173
  var init_database = __esm(() => {
2173
2174
  MIGRATIONS = [
2174
2175
  `
@@ -2315,8 +2316,28 @@ INSERT OR IGNORE INTO _migrations (id) VALUES (6);
2315
2316
  `
2316
2317
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
2317
2318
  INSERT OR IGNORE INTO _migrations (id) VALUES (7);
2319
+ `,
2320
+ `
2321
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_status_created ON sandboxes(status, created_at DESC);
2322
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_provider_created ON sandboxes(provider, created_at DESC);
2323
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_project_created ON sandboxes(project_id, created_at DESC);
2324
+ CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_started ON sandbox_sessions(sandbox_id, started_at DESC);
2325
+ CREATE INDEX IF NOT EXISTS idx_sessions_status_started ON sandbox_sessions(status, started_at DESC);
2326
+ CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_status_started ON sandbox_sessions(sandbox_id, status, started_at DESC);
2327
+ CREATE INDEX IF NOT EXISTS idx_events_sandbox_created ON sandbox_events(sandbox_id, created_at ASC);
2328
+ CREATE INDEX IF NOT EXISTS idx_events_session_created ON sandbox_events(session_id, created_at ASC);
2329
+ INSERT OR IGNORE INTO _migrations (id) VALUES (8);
2318
2330
  `
2319
2331
  ];
2332
+ PARTIAL_ID_TABLES = new Set([
2333
+ "agents",
2334
+ "projects",
2335
+ "sandbox_sessions",
2336
+ "sandboxes",
2337
+ "snapshots",
2338
+ "templates",
2339
+ "webhooks"
2340
+ ]);
2320
2341
  });
2321
2342
 
2322
2343
  // node_modules/postgres-array/index.js
@@ -7287,6 +7308,9 @@ var init_pg_migrations = __esm(() => {
7287
7308
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_status ON sandboxes(status)`,
7288
7309
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_provider ON sandboxes(provider)`,
7289
7310
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_project ON sandboxes(project_id)`,
7311
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_status_created ON sandboxes(status, created_at DESC)`,
7312
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_provider_created ON sandboxes(provider, created_at DESC)`,
7313
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_project_created ON sandboxes(project_id, created_at DESC)`,
7290
7314
  `CREATE TABLE IF NOT EXISTS sandbox_sessions (
7291
7315
  id TEXT PRIMARY KEY,
7292
7316
  sandbox_id TEXT NOT NULL REFERENCES sandboxes(id) ON DELETE CASCADE,
@@ -7300,6 +7324,9 @@ var init_pg_migrations = __esm(() => {
7300
7324
  )`,
7301
7325
  `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox ON sandbox_sessions(sandbox_id)`,
7302
7326
  `CREATE INDEX IF NOT EXISTS idx_sessions_status ON sandbox_sessions(status)`,
7327
+ `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_started ON sandbox_sessions(sandbox_id, started_at DESC)`,
7328
+ `CREATE INDEX IF NOT EXISTS idx_sessions_status_started ON sandbox_sessions(status, started_at DESC)`,
7329
+ `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_status_started ON sandbox_sessions(sandbox_id, status, started_at DESC)`,
7303
7330
  `CREATE TABLE IF NOT EXISTS sandbox_events (
7304
7331
  id TEXT PRIMARY KEY,
7305
7332
  sandbox_id TEXT NOT NULL REFERENCES sandboxes(id) ON DELETE CASCADE,
@@ -7311,6 +7338,8 @@ var init_pg_migrations = __esm(() => {
7311
7338
  `CREATE INDEX IF NOT EXISTS idx_events_sandbox ON sandbox_events(sandbox_id)`,
7312
7339
  `CREATE INDEX IF NOT EXISTS idx_events_session ON sandbox_events(session_id)`,
7313
7340
  `CREATE INDEX IF NOT EXISTS idx_events_type ON sandbox_events(type)`,
7341
+ `CREATE INDEX IF NOT EXISTS idx_events_sandbox_created ON sandbox_events(sandbox_id, created_at ASC)`,
7342
+ `CREATE INDEX IF NOT EXISTS idx_events_session_created ON sandbox_events(session_id, created_at ASC)`,
7314
7343
  `CREATE TABLE IF NOT EXISTS webhooks (
7315
7344
  id TEXT PRIMARY KEY,
7316
7345
  url TEXT NOT NULL,
@@ -7652,7 +7681,7 @@ function listEvents(opts) {
7652
7681
  const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
7653
7682
  const limit = opts?.limit ?? 100;
7654
7683
  const offset = opts?.offset ?? 0;
7655
- const rows = db2.query(`SELECT * FROM sandbox_events${where} ORDER BY created_at ASC LIMIT ? OFFSET ?`).all(...[...params, limit, offset]);
7684
+ const rows = db2.query(`SELECT * FROM sandbox_events${where} ORDER BY created_at ASC, rowid ASC LIMIT ? OFFSET ?`).all(...[...params, limit, offset]);
7656
7685
  return rows.map(rowToEvent);
7657
7686
  }
7658
7687
  var init_events = __esm(() => {
@@ -8977,68 +9006,93 @@ var {
8977
9006
  // src/cli/index.tsx
8978
9007
  import chalk2 from "chalk";
8979
9008
 
8980
- // src/cli/cloud.ts
9009
+ // src/cli/storage.ts
8981
9010
  import chalk from "chalk";
8982
9011
 
8983
- // src/db/cloud-config.ts
9012
+ // src/db/storage-config.ts
8984
9013
  import { existsSync, readFileSync } from "fs";
8985
9014
  import { homedir } from "os";
8986
9015
  import { join } from "path";
8987
- var CONFIG_PATH = join(homedir(), ".hasna", "sandboxes", "cloud", "config.json");
8988
- function isMode(value) {
8989
- return value === "local" || value === "hybrid" || value === "cloud";
9016
+ var STORAGE_CONFIG_PATH = join(homedir(), ".hasna", "sandboxes", "storage", "config.json");
9017
+ var SANDBOXES_STORAGE_ENV = "HASNA_SANDBOXES_DATABASE_URL";
9018
+ var SANDBOXES_STORAGE_FALLBACK_ENV = "SANDBOXES_DATABASE_URL";
9019
+ var SANDBOXES_STORAGE_MODE_ENV = "HASNA_SANDBOXES_STORAGE_MODE";
9020
+ var SANDBOXES_STORAGE_MODE_FALLBACK_ENV = "SANDBOXES_STORAGE_MODE";
9021
+ var STORAGE_DATABASE_ENV = [SANDBOXES_STORAGE_ENV, SANDBOXES_STORAGE_FALLBACK_ENV];
9022
+ function readEnv(name) {
9023
+ const value = process.env[name]?.trim();
9024
+ return value || undefined;
9025
+ }
9026
+ function normalizeStorageMode(value) {
9027
+ const normalized = value?.trim().toLowerCase();
9028
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
9029
+ return normalized;
9030
+ return;
9031
+ }
9032
+ function getStorageDatabaseEnvName() {
9033
+ for (const name of STORAGE_DATABASE_ENV) {
9034
+ if (readEnv(name))
9035
+ return name;
9036
+ }
9037
+ return null;
9038
+ }
9039
+ function getStorageDatabaseEnv() {
9040
+ const name = getStorageDatabaseEnvName();
9041
+ return name ? { name } : null;
8990
9042
  }
8991
- function envConnectionString() {
8992
- return process.env["HASNA_SANDBOXES_CLOUD_DATABASE_URL"] ?? process.env["OPEN_SANDBOXES_CLOUD_DATABASE_URL"] ?? process.env["SANDBOXES_CLOUD_DATABASE_URL"];
9043
+ function getStorageDatabaseUrl() {
9044
+ const env = getStorageDatabaseEnv();
9045
+ return env ? readEnv(env.name) : undefined;
8993
9046
  }
8994
- function getCloudConfig() {
9047
+ function getStorageConfig() {
8995
9048
  const config = {
8996
9049
  mode: "local",
8997
9050
  rds: {
8998
9051
  host: "",
8999
9052
  port: 5432,
9000
9053
  username: "",
9001
- password_env: "SANDBOXES_CLOUD_DATABASE_PASSWORD",
9054
+ password_env: "SANDBOXES_DATABASE_PASSWORD",
9002
9055
  ssl: true
9003
9056
  }
9004
9057
  };
9005
- if (existsSync(CONFIG_PATH)) {
9058
+ if (existsSync(STORAGE_CONFIG_PATH)) {
9006
9059
  try {
9007
- const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
9008
- config.mode = raw.mode ?? config.mode;
9060
+ const raw = JSON.parse(readFileSync(STORAGE_CONFIG_PATH, "utf-8"));
9061
+ config.mode = normalizeStorageMode(raw.mode) ?? config.mode;
9009
9062
  config.rds = { ...config.rds, ...raw.rds ?? {} };
9010
9063
  } catch {}
9011
9064
  }
9012
- const modeOverride = process.env["HASNA_SANDBOXES_CLOUD_MODE"] ?? process.env["OPEN_SANDBOXES_CLOUD_MODE"] ?? process.env["SANDBOXES_CLOUD_MODE"];
9013
- if (isMode(modeOverride)) {
9014
- config.mode = modeOverride;
9015
- } else if (envConnectionString() && config.mode === "local") {
9065
+ const modeOverride = readEnv(SANDBOXES_STORAGE_MODE_ENV) ?? readEnv(SANDBOXES_STORAGE_MODE_FALLBACK_ENV);
9066
+ const mode = normalizeStorageMode(modeOverride);
9067
+ if (mode) {
9068
+ config.mode = mode;
9069
+ } else if (getStorageDatabaseUrl() && config.mode === "local") {
9016
9070
  config.mode = "hybrid";
9017
9071
  }
9018
9072
  return config;
9019
9073
  }
9020
- function getConnectionString(dbName = "sandboxes") {
9021
- const direct = envConnectionString();
9074
+ function getStorageConnectionString(dbName = "sandboxes") {
9075
+ const direct = getStorageDatabaseUrl();
9022
9076
  if (direct)
9023
9077
  return direct;
9024
- const config = getCloudConfig();
9078
+ const config = getStorageConfig();
9025
9079
  const { host, port, username, password_env, ssl } = config.rds;
9026
9080
  if (!host || !username) {
9027
- throw new Error("Cloud database is not configured. Set HASNA_SANDBOXES_CLOUD_DATABASE_URL or configure ~/.hasna/sandboxes/cloud/config.json.");
9081
+ throw new Error("Storage database is not configured. Set HASNA_SANDBOXES_DATABASE_URL or configure ~/.hasna/sandboxes/storage/config.json.");
9028
9082
  }
9029
9083
  const password = process.env[password_env];
9030
9084
  if (!password) {
9031
- throw new Error(`Cloud database password is not set. Export ${password_env}.`);
9085
+ throw new Error(`Storage database password is not set. Export ${password_env}.`);
9032
9086
  }
9033
9087
  const sslParam = ssl ? "?sslmode=require" : "";
9034
9088
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
9035
9089
  }
9036
9090
 
9037
- // src/db/cloud-sync.ts
9091
+ // src/db/storage-sync.ts
9038
9092
  init_database();
9039
9093
  init_remote_storage();
9040
9094
  init_pg_migrations();
9041
- var CLOUD_TABLES = [
9095
+ var STORAGE_TABLES = [
9042
9096
  "projects",
9043
9097
  "agents",
9044
9098
  "sandboxes",
@@ -9132,21 +9186,26 @@ function upsertSqlite(db2, table, rows) {
9132
9186
  }
9133
9187
  return written;
9134
9188
  }
9135
- async function getCloudPg() {
9136
- return new PgAdapterAsync(getConnectionString("sandboxes"));
9189
+ async function getStoragePg() {
9190
+ return new PgAdapterAsync(getStorageConnectionString("sandboxes"));
9137
9191
  }
9138
- async function runCloudMigrations(remote) {
9192
+ async function runStorageMigrations(remote) {
9139
9193
  for (const migration of PG_MIGRATIONS) {
9140
9194
  await remote.exec(migration);
9141
9195
  }
9142
9196
  }
9143
- function getCloudStatus(db2 = getDatabase()) {
9144
- const config = getCloudConfig();
9197
+ function getStorageStatus(db2 = getDatabase()) {
9198
+ const config = getStorageConfig();
9199
+ const activeEnv = getStorageDatabaseEnv();
9145
9200
  return {
9201
+ configured: Boolean(activeEnv),
9146
9202
  mode: config.mode,
9147
- enabled: config.mode === "hybrid" || config.mode === "cloud",
9203
+ enabled: config.mode === "hybrid" || config.mode === "remote",
9204
+ env: STORAGE_DATABASE_ENV,
9205
+ activeEnv: activeEnv?.name ?? null,
9206
+ service: "sandboxes",
9148
9207
  db_path: getDbPath(),
9149
- tables: CLOUD_TABLES.map((table) => {
9208
+ tables: STORAGE_TABLES.map((table) => {
9150
9209
  try {
9151
9210
  const row = db2.query(`SELECT COUNT(*) as count FROM ${quoteId(table)}`).get();
9152
9211
  return { table, rows: row.count };
@@ -9156,12 +9215,12 @@ function getCloudStatus(db2 = getDatabase()) {
9156
9215
  })
9157
9216
  };
9158
9217
  }
9159
- async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
9218
+ async function pushStorageChanges(tables = [...STORAGE_TABLES]) {
9160
9219
  const db2 = getDatabase();
9161
- const remote = await getCloudPg();
9220
+ const remote = await getStoragePg();
9162
9221
  const results = [];
9163
9222
  try {
9164
- await runCloudMigrations(remote);
9223
+ await runStorageMigrations(remote);
9165
9224
  for (const table of tables) {
9166
9225
  const result = { table, direction: "push", rowsRead: 0, rowsWritten: 0, errors: [] };
9167
9226
  try {
@@ -9178,12 +9237,12 @@ async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
9178
9237
  }
9179
9238
  return results;
9180
9239
  }
9181
- async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
9240
+ async function pullStorageChanges(tables = [...STORAGE_TABLES]) {
9182
9241
  const db2 = getDatabase();
9183
- const remote = await getCloudPg();
9242
+ const remote = await getStoragePg();
9184
9243
  const results = [];
9185
9244
  try {
9186
- await runCloudMigrations(remote);
9245
+ await runStorageMigrations(remote);
9187
9246
  for (const table of tables) {
9188
9247
  const result = { table, direction: "pull", rowsRead: 0, rowsWritten: 0, errors: [] };
9189
9248
  try {
@@ -9200,20 +9259,26 @@ async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
9200
9259
  }
9201
9260
  return results;
9202
9261
  }
9203
- async function syncCloudChanges(tables = [...CLOUD_TABLES]) {
9262
+ async function syncStorageChanges(tables = [...STORAGE_TABLES]) {
9204
9263
  return {
9205
- push: await pushCloudChanges(tables),
9206
- pull: await pullCloudChanges(tables)
9264
+ push: await pushStorageChanges(tables),
9265
+ pull: await pullStorageChanges(tables)
9207
9266
  };
9208
9267
  }
9209
- function parseCloudTables(raw) {
9268
+ function parseStorageTables(raw) {
9210
9269
  if (!raw)
9211
- return [...CLOUD_TABLES];
9270
+ return [...STORAGE_TABLES];
9212
9271
  const requested = raw.split(",").map((table) => table.trim()).filter(Boolean);
9213
- return requested.length > 0 ? requested : [...CLOUD_TABLES];
9272
+ if (requested.length === 0)
9273
+ return [...STORAGE_TABLES];
9274
+ const allowed = new Set(STORAGE_TABLES);
9275
+ const invalid = requested.filter((table) => !allowed.has(table));
9276
+ if (invalid.length > 0)
9277
+ throw new Error(`Unknown sandboxes sync table(s): ${invalid.join(", ")}`);
9278
+ return requested;
9214
9279
  }
9215
9280
 
9216
- // src/cli/cloud.ts
9281
+ // src/cli/storage.ts
9217
9282
  function printJson(value) {
9218
9283
  console.log(JSON.stringify(value, null, 2));
9219
9284
  }
@@ -9226,10 +9291,10 @@ function printResults(results) {
9226
9291
  }
9227
9292
  }
9228
9293
  }
9229
- function registerCloudCommands(program2) {
9230
- const cloud = program2.command("cloud").description("Manage sandboxes local/remote cloud sync");
9231
- cloud.command("status").description("Show local database and cloud sync status").option("--json", "Output as JSON").action((opts) => {
9232
- const status = getCloudStatus();
9294
+ function registerStorageCommands(program2) {
9295
+ const storage = program2.command("storage").description("Manage sandboxes local/remote storage sync");
9296
+ storage.command("status").description("Show local database and storage sync status").option("--json", "Output as JSON").action((opts) => {
9297
+ const status = getStorageStatus();
9233
9298
  if (opts.json) {
9234
9299
  printJson(status);
9235
9300
  return;
@@ -9241,9 +9306,9 @@ function registerCloudCommands(program2) {
9241
9306
  console.log(` ${table.table}: ${table.rows}`);
9242
9307
  }
9243
9308
  });
9244
- cloud.command("push").description("Push local sandboxes data to PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
9309
+ storage.command("push").description("Push local sandboxes data to PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
9245
9310
  try {
9246
- const results = await pushCloudChanges(parseCloudTables(opts.tables));
9311
+ const results = await pushStorageChanges(parseStorageTables(opts.tables));
9247
9312
  if (opts.json)
9248
9313
  printJson(results);
9249
9314
  else
@@ -9256,9 +9321,9 @@ function registerCloudCommands(program2) {
9256
9321
  process.exitCode = 1;
9257
9322
  }
9258
9323
  });
9259
- cloud.command("pull").description("Pull PostgreSQL sandboxes data into the local database").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
9324
+ storage.command("pull").description("Pull PostgreSQL sandboxes data into the local database").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
9260
9325
  try {
9261
- const results = await pullCloudChanges(parseCloudTables(opts.tables));
9326
+ const results = await pullStorageChanges(parseStorageTables(opts.tables));
9262
9327
  if (opts.json)
9263
9328
  printJson(results);
9264
9329
  else
@@ -9271,9 +9336,9 @@ function registerCloudCommands(program2) {
9271
9336
  process.exitCode = 1;
9272
9337
  }
9273
9338
  });
9274
- cloud.command("sync").description("Push local changes, then pull remote changes").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
9339
+ storage.command("sync").description("Push local changes, then pull remote changes").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
9275
9340
  try {
9276
- const result = await syncCloudChanges(parseCloudTables(opts.tables));
9341
+ const result = await syncStorageChanges(parseStorageTables(opts.tables));
9277
9342
  if (opts.json) {
9278
9343
  printJson(result);
9279
9344
  return;
@@ -9290,10 +9355,10 @@ function registerCloudCommands(program2) {
9290
9355
  process.exitCode = 1;
9291
9356
  }
9292
9357
  });
9293
- cloud.command("migrate").description("Apply PostgreSQL migrations").option("--connection-string <url>", "PostgreSQL connection string").option("--json", "Output as JSON").action(async (opts) => {
9358
+ storage.command("migrate").description("Apply PostgreSQL migrations").option("--connection-string <url>", "PostgreSQL connection string").option("--json", "Output as JSON").action(async (opts) => {
9294
9359
  try {
9295
9360
  const { applyPgMigrations: applyPgMigrations2 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
9296
- const result = await applyPgMigrations2(opts.connectionString || getConnectionString("sandboxes"));
9361
+ const result = await applyPgMigrations2(opts.connectionString || getStorageConnectionString("sandboxes"));
9297
9362
  if (opts.json) {
9298
9363
  printJson(result);
9299
9364
  return;
@@ -9442,7 +9507,7 @@ function parseEnvVars(envArgs) {
9442
9507
  return vars;
9443
9508
  }
9444
9509
  var program2 = new Command().name("sandboxes").description("Universal cloud sandbox manager for AI coding agents").version(getPackageVersion());
9445
- registerCloudCommands(program2);
9510
+ registerStorageCommands(program2);
9446
9511
  program2.command("create").description("Create a new sandbox").option("-p, --provider <provider>", "Provider (e2b, daytona, modal)").option("-i, --image <image>", "Container image").option("-t, --timeout <seconds>", "Timeout in seconds").option("-n, --name <name>", "Sandbox name").option("-e, --env <KEY=VAL...>", "Environment variables", (val, acc) => {
9447
9512
  acc.push(val);
9448
9513
  return acc;
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerStorageCommands(program: Command): void;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * PostgreSQL migrations for open-sandboxes cloud sync.
2
+ * PostgreSQL migrations for open-sandboxes storage sync.
3
3
  *
4
4
  * Equivalent to the SQLite schema in database.ts, translated for PostgreSQL.
5
5
  */
@@ -0,0 +1,26 @@
1
+ export type StorageMode = "local" | "hybrid" | "remote";
2
+ export interface StorageConfig {
3
+ mode: StorageMode;
4
+ rds: {
5
+ host: string;
6
+ port: number;
7
+ username: string;
8
+ password_env: string;
9
+ ssl: boolean;
10
+ };
11
+ }
12
+ export interface StorageEnv {
13
+ name: string;
14
+ }
15
+ export declare const SANDBOXES_STORAGE_ENV = "HASNA_SANDBOXES_DATABASE_URL";
16
+ export declare const SANDBOXES_STORAGE_FALLBACK_ENV = "SANDBOXES_DATABASE_URL";
17
+ export declare const SANDBOXES_STORAGE_MODE_ENV = "HASNA_SANDBOXES_STORAGE_MODE";
18
+ export declare const SANDBOXES_STORAGE_MODE_FALLBACK_ENV = "SANDBOXES_STORAGE_MODE";
19
+ export declare const STORAGE_DATABASE_ENV: readonly ["HASNA_SANDBOXES_DATABASE_URL", "SANDBOXES_DATABASE_URL"];
20
+ export declare const STORAGE_MODE_ENV: readonly ["HASNA_SANDBOXES_STORAGE_MODE", "SANDBOXES_STORAGE_MODE"];
21
+ export declare function getStorageDatabaseEnvName(): (typeof STORAGE_DATABASE_ENV)[number] | null;
22
+ export declare function getStorageDatabaseEnv(): StorageEnv | null;
23
+ export declare function getStorageDatabaseUrl(): string | undefined;
24
+ export declare function getStorageConfig(): StorageConfig;
25
+ export declare function getStorageConnectionString(dbName?: string): string;
26
+ export declare const getConnectionString: typeof getStorageConnectionString;
@@ -0,0 +1,35 @@
1
+ import { type Database } from "bun:sqlite";
2
+ import { STORAGE_DATABASE_ENV, type StorageMode } from "./storage-config.js";
3
+ import { PgAdapterAsync } from "./remote-storage.js";
4
+ export interface SyncResult {
5
+ table: string;
6
+ direction: "push" | "pull";
7
+ rowsRead: number;
8
+ rowsWritten: number;
9
+ errors: string[];
10
+ }
11
+ export interface StorageStatus {
12
+ configured: boolean;
13
+ mode: StorageMode;
14
+ enabled: boolean;
15
+ env: typeof STORAGE_DATABASE_ENV;
16
+ activeEnv: string | null;
17
+ service: "sandboxes";
18
+ db_path: string;
19
+ tables: Array<{
20
+ table: string;
21
+ rows: number;
22
+ }>;
23
+ }
24
+ export declare const STORAGE_TABLES: readonly ["projects", "agents", "sandboxes", "sandbox_sessions", "sandbox_events", "webhooks", "templates", "snapshots", "feedback"];
25
+ export declare const SANDBOXES_STORAGE_TABLES: readonly ["projects", "agents", "sandboxes", "sandbox_sessions", "sandbox_events", "webhooks", "templates", "snapshots", "feedback"];
26
+ export declare function getStoragePg(): Promise<PgAdapterAsync>;
27
+ export declare function runStorageMigrations(remote: PgAdapterAsync): Promise<void>;
28
+ export declare function getStorageStatus(db?: Database): StorageStatus;
29
+ export declare function pushStorageChanges(tables?: string[]): Promise<SyncResult[]>;
30
+ export declare function pullStorageChanges(tables?: string[]): Promise<SyncResult[]>;
31
+ export declare function syncStorageChanges(tables?: string[]): Promise<{
32
+ push: SyncResult[];
33
+ pull: SyncResult[];
34
+ }>;
35
+ export declare function parseStorageTables(raw?: string): string[];
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export * from "./types/index.js";
2
2
  export { getDatabase, closeDatabase, resetDatabase, uuid, shortId, now, resolvePartialId } from "./db/database.js";
3
- export { getCloudConfig, getConnectionString } from "./db/cloud-config.js";
3
+ export { SANDBOXES_STORAGE_ENV, SANDBOXES_STORAGE_FALLBACK_ENV, SANDBOXES_STORAGE_MODE_ENV, SANDBOXES_STORAGE_MODE_FALLBACK_ENV, STORAGE_DATABASE_ENV, STORAGE_MODE_ENV, getConnectionString, getStorageConfig, getStorageConnectionString, getStorageDatabaseEnv, getStorageDatabaseEnvName, getStorageDatabaseUrl, type StorageConfig, type StorageEnv, type StorageMode, } from "./db/storage-config.js";
4
4
  export { PgAdapterAsync } from "./db/remote-storage.js";
5
5
  export { applyPgMigrations } from "./db/pg-migrate.js";
6
- export { CLOUD_TABLES, getCloudPg, getCloudStatus, parseCloudTables, pullCloudChanges, pushCloudChanges, runCloudMigrations, syncCloudChanges, } from "./db/cloud-sync.js";
7
- export type { CloudStatus, SyncResult } from "./db/cloud-sync.js";
6
+ export { SANDBOXES_STORAGE_TABLES, STORAGE_TABLES, getStoragePg, getStorageStatus, parseStorageTables, pullStorageChanges, pushStorageChanges, runStorageMigrations, syncStorageChanges, } from "./db/storage-sync.js";
7
+ export type { StorageStatus, SyncResult } from "./db/storage-sync.js";
8
8
  export { createSandbox, getSandbox, listSandboxes, updateSandbox, deleteSandbox } from "./db/sandboxes.js";
9
9
  export { createSession, getSession, listSessions, updateSession, endSession } from "./db/sessions.js";
10
10
  export { addEvent, listEvents } from "./db/events.js";