@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 +8 -8
- package/dist/cli/index.js +130 -65
- package/dist/cli/storage.d.ts +2 -0
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/db/storage-config.d.ts +26 -0
- package/dist/db/storage-sync.d.ts +35 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +136 -57
- package/dist/mcp/index.js +125 -60
- package/dist/mcp/storage-tools.d.ts +2 -0
- package/dist/server/index.js +126 -61
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +5654 -0
- package/package.json +6 -2
- package/dist/cli/cloud.d.ts +0 -2
- package/dist/db/cloud-config.d.ts +0 -13
- package/dist/db/cloud-sync.d.ts +0 -29
- package/dist/mcp/cloud-tools.d.ts +0 -2
package/README.md
CHANGED
|
@@ -65,19 +65,19 @@ MCP_HTTP=1 sandboxes-mcp
|
|
|
65
65
|
sandboxes-serve
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
##
|
|
68
|
+
## Storage Sync
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
Remote storage sync is optional. By default sandboxes use local SQLite at `~/.hasna/sandboxes/`.
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
sandboxes
|
|
74
|
-
sandboxes
|
|
75
|
-
sandboxes
|
|
76
|
-
sandboxes
|
|
73
|
+
sandboxes storage status
|
|
74
|
+
sandboxes storage push
|
|
75
|
+
sandboxes storage pull
|
|
76
|
+
sandboxes storage sync
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
Set `
|
|
80
|
-
`~/.hasna/sandboxes/
|
|
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
|
-
}
|
|
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
|
|
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/
|
|
9009
|
+
// src/cli/storage.ts
|
|
8981
9010
|
import chalk from "chalk";
|
|
8982
9011
|
|
|
8983
|
-
// src/db/
|
|
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
|
|
8988
|
-
|
|
8989
|
-
|
|
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
|
|
8992
|
-
|
|
9043
|
+
function getStorageDatabaseUrl() {
|
|
9044
|
+
const env = getStorageDatabaseEnv();
|
|
9045
|
+
return env ? readEnv(env.name) : undefined;
|
|
8993
9046
|
}
|
|
8994
|
-
function
|
|
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: "
|
|
9054
|
+
password_env: "SANDBOXES_DATABASE_PASSWORD",
|
|
9002
9055
|
ssl: true
|
|
9003
9056
|
}
|
|
9004
9057
|
};
|
|
9005
|
-
if (existsSync(
|
|
9058
|
+
if (existsSync(STORAGE_CONFIG_PATH)) {
|
|
9006
9059
|
try {
|
|
9007
|
-
const raw = JSON.parse(readFileSync(
|
|
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 =
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
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
|
|
9021
|
-
const direct =
|
|
9074
|
+
function getStorageConnectionString(dbName = "sandboxes") {
|
|
9075
|
+
const direct = getStorageDatabaseUrl();
|
|
9022
9076
|
if (direct)
|
|
9023
9077
|
return direct;
|
|
9024
|
-
const config =
|
|
9078
|
+
const config = getStorageConfig();
|
|
9025
9079
|
const { host, port, username, password_env, ssl } = config.rds;
|
|
9026
9080
|
if (!host || !username) {
|
|
9027
|
-
throw new Error("
|
|
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(`
|
|
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/
|
|
9091
|
+
// src/db/storage-sync.ts
|
|
9038
9092
|
init_database();
|
|
9039
9093
|
init_remote_storage();
|
|
9040
9094
|
init_pg_migrations();
|
|
9041
|
-
var
|
|
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
|
|
9136
|
-
return new PgAdapterAsync(
|
|
9189
|
+
async function getStoragePg() {
|
|
9190
|
+
return new PgAdapterAsync(getStorageConnectionString("sandboxes"));
|
|
9137
9191
|
}
|
|
9138
|
-
async function
|
|
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
|
|
9144
|
-
const config =
|
|
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 === "
|
|
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:
|
|
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
|
|
9218
|
+
async function pushStorageChanges(tables = [...STORAGE_TABLES]) {
|
|
9160
9219
|
const db2 = getDatabase();
|
|
9161
|
-
const remote = await
|
|
9220
|
+
const remote = await getStoragePg();
|
|
9162
9221
|
const results = [];
|
|
9163
9222
|
try {
|
|
9164
|
-
await
|
|
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
|
|
9240
|
+
async function pullStorageChanges(tables = [...STORAGE_TABLES]) {
|
|
9182
9241
|
const db2 = getDatabase();
|
|
9183
|
-
const remote = await
|
|
9242
|
+
const remote = await getStoragePg();
|
|
9184
9243
|
const results = [];
|
|
9185
9244
|
try {
|
|
9186
|
-
await
|
|
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
|
|
9262
|
+
async function syncStorageChanges(tables = [...STORAGE_TABLES]) {
|
|
9204
9263
|
return {
|
|
9205
|
-
push: await
|
|
9206
|
-
pull: await
|
|
9264
|
+
push: await pushStorageChanges(tables),
|
|
9265
|
+
pull: await pullStorageChanges(tables)
|
|
9207
9266
|
};
|
|
9208
9267
|
}
|
|
9209
|
-
function
|
|
9268
|
+
function parseStorageTables(raw) {
|
|
9210
9269
|
if (!raw)
|
|
9211
|
-
return [...
|
|
9270
|
+
return [...STORAGE_TABLES];
|
|
9212
9271
|
const requested = raw.split(",").map((table) => table.trim()).filter(Boolean);
|
|
9213
|
-
|
|
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/
|
|
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
|
|
9230
|
-
const
|
|
9231
|
-
|
|
9232
|
-
const status =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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,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 {
|
|
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 {
|
|
7
|
-
export type {
|
|
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";
|