@batadata/cli 0.2.11 → 0.2.13
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 +1 -0
- package/dist/commands/import.js +24 -2
- package/dist/commands/studio.d.ts +31 -0
- package/dist/commands/studio.js +262 -0
- package/dist/index.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,7 @@ flag always wins**, so linking never silently overrides an intentional request:
|
|
|
103
103
|
| `db branch create` / `db branch delete` | Manage branches |
|
|
104
104
|
| `db branch checkout <name-or-id>` | Pin a branch into the directory's link |
|
|
105
105
|
| `db studio` | Open the table browser in your browser |
|
|
106
|
+
| `studio` | Run Turbine Studio locally against a branch: a local web UI (`npx turbine-orm studio`) over a direct TCP connection, nothing hosted. Wakes the compute first. Read-only by default; PII redaction is turbine's UI feature, not a BataDB server control (`--project`, `--branch`, `--port`) |
|
|
106
107
|
| `restore points` | List recovery points and the PITR window (see [Point-in-time restore](#point-in-time-restore-pitr)) |
|
|
107
108
|
| `restore create` | Restore a branch to a timestamp/LSN as a **new** branch |
|
|
108
109
|
| `schema check <file>` | Check a proposed schema change against live query traffic |
|
package/dist/commands/import.js
CHANGED
|
@@ -859,8 +859,9 @@ export async function importDb(args) {
|
|
|
859
859
|
}
|
|
860
860
|
if (namedHits.length > 0 && !jsonMode) {
|
|
861
861
|
warn(`Source contains ${namedHits.join(", ")}, which shares a name with BataDB's seeded ` +
|
|
862
|
-
|
|
863
|
-
'
|
|
862
|
+
"platform scaffolding. On a freshly created target the seeded copy is dropped so the " +
|
|
863
|
+
"source's version restores as real data; importing into an existing --project target " +
|
|
864
|
+
'will likely fail with "relation already exists".');
|
|
864
865
|
}
|
|
865
866
|
// ── Phase 2: check pg_dump AND psql are new enough ──────────────────────────
|
|
866
867
|
// BOTH matter: pg_dump can't dump a newer server than itself, and psql must be
|
|
@@ -1072,6 +1073,27 @@ export async function importDb(args) {
|
|
|
1072
1073
|
const strandedHintJson = "The target project was left in place (NOT deleted) for diagnosis/retry — re-run with " +
|
|
1073
1074
|
`--project ${targetProjectId} --yes after fixing` +
|
|
1074
1075
|
(created ? `, or remove it with: bata projects delete ${targetProjectId} --yes.` : ".");
|
|
1076
|
+
// A freshly CREATED target's named scaffolding (public.health_check + its
|
|
1077
|
+
// sequence) is disposable furniture — but the SOURCE may carry same-named
|
|
1078
|
+
// tables (always true migrating FROM another BataDB, where provisioning
|
|
1079
|
+
// seeded them too). Doctrine says source objects are real data and must
|
|
1080
|
+
// restore + verify normally, so drop the target's seeded copies first;
|
|
1081
|
+
// otherwise the restore is GUARANTEED to die on "already exists". Never
|
|
1082
|
+
// touched on a --project target (created === false): its objects may be the
|
|
1083
|
+
// user's own data, and the old collide-loudly behavior stays.
|
|
1084
|
+
if (created && namedHits.length > 0) {
|
|
1085
|
+
phase("Dropping seeded scaffolding the source will re-create");
|
|
1086
|
+
for (const qn of namedHits) {
|
|
1087
|
+
const dot = qn.indexOf(".");
|
|
1088
|
+
const dropSql = `DROP TABLE IF EXISTS ${quoteIdent(qn.slice(0, dot))}.${quoteIdent(qn.slice(dot + 1))} CASCADE`;
|
|
1089
|
+
const dropRes = runPsql(targetUri, dropSql);
|
|
1090
|
+
if (dropRes.code !== 0) {
|
|
1091
|
+
emitError("RESTORE_FAILED", `Could not clear the target's seeded ${qn} before restore.`, `The source has its own ${qn}, which cannot restore over the target's seeded copy. ` +
|
|
1092
|
+
strandedHintJson, 1);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1075
1097
|
// ── Phase 4: migrate ───────────────────────────────────────────────────────
|
|
1076
1098
|
phase("Migrating schema + data (pg_dump | psql)");
|
|
1077
1099
|
const migration = await runMigration(source, targetUri);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bata studio: launch turbine-orm's Studio locally against a BataDB branch.
|
|
3
|
+
*
|
|
4
|
+
* Zero hosting infra: resolves the branch's DIRECT connection URI from the
|
|
5
|
+
* control plane, wakes the compute if it scaled to zero, then spawns
|
|
6
|
+
* `npx --yes turbine-orm@latest studio` with DATABASE_URL set. Studio itself
|
|
7
|
+
* is turbine's local web UI (loopback-only, read-only by default); everything
|
|
8
|
+
* it renders travels over a direct TCP connection from this machine.
|
|
9
|
+
*
|
|
10
|
+
* Compat-honest: PII redaction in Studio is turbine's feature (PII-tagged
|
|
11
|
+
* columns are redacted by default, `--show-pii` unredacts), not something
|
|
12
|
+
* BataDB enforces server-side.
|
|
13
|
+
*/
|
|
14
|
+
export interface StudioArgs {
|
|
15
|
+
project?: string;
|
|
16
|
+
branch?: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
write: boolean;
|
|
19
|
+
showPii: boolean;
|
|
20
|
+
noOpen: boolean;
|
|
21
|
+
help: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse `bata studio` args (both `--flag value` and `--flag=value` forms).
|
|
25
|
+
* Returns `{ error }` on a malformed flag so the caller can fail fast with
|
|
26
|
+
* INVALID_FLAG before touching the network. Exported for unit testing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseStudioArgs(args: string[]): StudioArgs | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function studio(args?: string[]): Promise<void>;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bata studio: launch turbine-orm's Studio locally against a BataDB branch.
|
|
3
|
+
*
|
|
4
|
+
* Zero hosting infra: resolves the branch's DIRECT connection URI from the
|
|
5
|
+
* control plane, wakes the compute if it scaled to zero, then spawns
|
|
6
|
+
* `npx --yes turbine-orm@latest studio` with DATABASE_URL set. Studio itself
|
|
7
|
+
* is turbine's local web UI (loopback-only, read-only by default); everything
|
|
8
|
+
* it renders travels over a direct TCP connection from this machine.
|
|
9
|
+
*
|
|
10
|
+
* Compat-honest: PII redaction in Studio is turbine's feature (PII-tagged
|
|
11
|
+
* columns are redacted by default, `--show-pii` unredacts), not something
|
|
12
|
+
* BataDB enforces server-side.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
16
|
+
import * as os from "node:os";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { api, asList, apiError } from "../api.js";
|
|
19
|
+
import { requireToken, loadConfig, isJsonMode } from "../config.js";
|
|
20
|
+
import { colors, log, spinner, info as logInfo } from "../utils/logger.js";
|
|
21
|
+
import { emitError, isRetryable } from "../utils/errors.js";
|
|
22
|
+
import { resolveProjectId, readLinkFile } from "../link.js";
|
|
23
|
+
/**
|
|
24
|
+
* Parse `bata studio` args (both `--flag value` and `--flag=value` forms).
|
|
25
|
+
* Returns `{ error }` on a malformed flag so the caller can fail fast with
|
|
26
|
+
* INVALID_FLAG before touching the network. Exported for unit testing.
|
|
27
|
+
*/
|
|
28
|
+
export function parseStudioArgs(args) {
|
|
29
|
+
const out = { write: false, showPii: false, noOpen: false, help: false };
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
const a = args[i];
|
|
32
|
+
const takeVal = (flag) => {
|
|
33
|
+
const next = args[++i];
|
|
34
|
+
if (next === undefined || next.startsWith("-")) {
|
|
35
|
+
return { error: `${flag} requires a value.` };
|
|
36
|
+
}
|
|
37
|
+
return next;
|
|
38
|
+
};
|
|
39
|
+
let v;
|
|
40
|
+
if (a === "--project") {
|
|
41
|
+
v = takeVal("--project");
|
|
42
|
+
if (typeof v !== "string")
|
|
43
|
+
return v;
|
|
44
|
+
out.project = v;
|
|
45
|
+
}
|
|
46
|
+
else if (a.startsWith("--project=")) {
|
|
47
|
+
out.project = a.slice("--project=".length);
|
|
48
|
+
}
|
|
49
|
+
else if (a === "--branch") {
|
|
50
|
+
v = takeVal("--branch");
|
|
51
|
+
if (typeof v !== "string")
|
|
52
|
+
return v;
|
|
53
|
+
out.branch = v;
|
|
54
|
+
}
|
|
55
|
+
else if (a.startsWith("--branch=")) {
|
|
56
|
+
out.branch = a.slice("--branch=".length);
|
|
57
|
+
}
|
|
58
|
+
else if (a === "--port" || a.startsWith("--port=")) {
|
|
59
|
+
const raw = a === "--port" ? takeVal("--port") : a.slice("--port=".length);
|
|
60
|
+
if (typeof raw !== "string")
|
|
61
|
+
return raw;
|
|
62
|
+
const n = Number(raw);
|
|
63
|
+
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
64
|
+
return { error: `Invalid port "${raw}". Use an integer between 1 and 65535.` };
|
|
65
|
+
}
|
|
66
|
+
out.port = n;
|
|
67
|
+
}
|
|
68
|
+
else if (a === "--write") {
|
|
69
|
+
out.write = true;
|
|
70
|
+
}
|
|
71
|
+
else if (a === "--show-pii") {
|
|
72
|
+
out.showPii = true;
|
|
73
|
+
}
|
|
74
|
+
else if (a === "--no-open") {
|
|
75
|
+
out.noOpen = true;
|
|
76
|
+
}
|
|
77
|
+
else if (a === "--help" || a === "-h") {
|
|
78
|
+
out.help = true;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return { error: `Unknown flag "${a}".` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function studioHelp() {
|
|
87
|
+
const usage = (line) => log(` ${colors.cyan(line)}`);
|
|
88
|
+
const note = (line) => log(` ${colors.dim(line)}`);
|
|
89
|
+
log();
|
|
90
|
+
log(` ${colors.bold("bata studio")}: launch Turbine Studio locally against a BataDB branch`);
|
|
91
|
+
log();
|
|
92
|
+
usage("bata studio [--project <id|name>] [--branch <name|id>] [--port <n>]");
|
|
93
|
+
usage("bata studio [--write] [--show-pii] [--no-open]");
|
|
94
|
+
log();
|
|
95
|
+
note("Runs turbine-orm's Studio on YOUR machine (npx turbine-orm@latest studio)");
|
|
96
|
+
note("over a direct TCP connection to the branch. Nothing is hosted by BataDB;");
|
|
97
|
+
note("close the terminal and it's gone.");
|
|
98
|
+
log();
|
|
99
|
+
note("--project <id|name> Target project (default: linked/default project)");
|
|
100
|
+
note("--branch <name|id> Target branch (default: the checked-out branch from");
|
|
101
|
+
note(" `bata db branch checkout`, else the primary branch)");
|
|
102
|
+
note("--port <n> Local port for Studio (default: turbine's 4983)");
|
|
103
|
+
note("--write Forwarded to turbine: enable single-row writes (default read-only)");
|
|
104
|
+
note("--show-pii Forwarded to turbine: show PII-tagged columns unredacted");
|
|
105
|
+
note("--no-open Forwarded to turbine: don't auto-open the browser");
|
|
106
|
+
log();
|
|
107
|
+
note("Honesty notes: Studio binds to loopback only and is read-only by default.");
|
|
108
|
+
note("PII redaction is turbine's client-side feature (PII-tagged columns are");
|
|
109
|
+
note("redacted in the UI), NOT a BataDB server-side control: the direct");
|
|
110
|
+
note("connection itself sees real data.");
|
|
111
|
+
note("A scaled-to-zero compute is woken first; that can take a few seconds.");
|
|
112
|
+
log();
|
|
113
|
+
}
|
|
114
|
+
function sleep(ms) {
|
|
115
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Wake a branch's compute and wait until it answers a query. Kicks the start
|
|
119
|
+
* endpoint when the compute reports suspended, then probes with `SELECT 1`
|
|
120
|
+
* via /v1/sql/execute: retryable failures (503 COMPUTE_STARTING, connection
|
|
121
|
+
* refused) keep polling; a hard failure stops early (Studio's own connection
|
|
122
|
+
* will surface the real error). Budget exhausted → COMPUTE_STARTING (exit 6),
|
|
123
|
+
* the retryable contract agents already know.
|
|
124
|
+
*/
|
|
125
|
+
async function wakeBranch(branchId, computeStatus, token, onTick) {
|
|
126
|
+
if (computeStatus === "suspended") {
|
|
127
|
+
// Non-fatal if it fails: the probe below (or the proxy) can still wake it.
|
|
128
|
+
await api.post(`/v1/computes/${branchId}/start`, {}, token).catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
const budget = 120_000;
|
|
131
|
+
const interval = 3_000;
|
|
132
|
+
const start = Date.now();
|
|
133
|
+
while (true) {
|
|
134
|
+
const res = await api.post("/v1/sql/execute", { branch_id: branchId, query: "SELECT 1" }, token);
|
|
135
|
+
if (res.ok && !res.data?.error)
|
|
136
|
+
return;
|
|
137
|
+
const body = res.data;
|
|
138
|
+
const retryable = isRetryable({ status: res.status, code: body?.code, message: body?.error });
|
|
139
|
+
if (!retryable)
|
|
140
|
+
return; // hard failure: let Studio's own connection report it
|
|
141
|
+
if (Date.now() - start >= budget) {
|
|
142
|
+
emitError("COMPUTE_STARTING", apiError(res, "Compute is still starting"), "compute is starting; retry `bata studio` in a few seconds");
|
|
143
|
+
}
|
|
144
|
+
onTick(Math.round((Date.now() - start) / 1000));
|
|
145
|
+
await sleep(interval);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function studio(args = []) {
|
|
149
|
+
const parsed = parseStudioArgs(args);
|
|
150
|
+
if ("error" in parsed) {
|
|
151
|
+
emitError("INVALID_FLAG", parsed.error, "Usage: bata studio [--project <id|name>] [--branch <name|id>] [--port <n>]");
|
|
152
|
+
}
|
|
153
|
+
if (parsed.help) {
|
|
154
|
+
studioHelp();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Studio is a long-running local web server with browser interaction; there
|
|
158
|
+
// is no headless JSON equivalent. Point agents at the headless surfaces.
|
|
159
|
+
if (isJsonMode()) {
|
|
160
|
+
emitError("INTERACTIVE_ONLY", "bata studio launches a local web UI and can't run in --json mode.", "Use `bata db query <sql> --json` or `bata schema dump` for headless access.");
|
|
161
|
+
}
|
|
162
|
+
const token = requireToken();
|
|
163
|
+
const config = loadConfig();
|
|
164
|
+
// Project precedence: --project (id OR name, resolved against /v1/projects)
|
|
165
|
+
// > .batadata link > config default.
|
|
166
|
+
let projectId = resolveProjectId().projectId;
|
|
167
|
+
if (parsed.project) {
|
|
168
|
+
const s = spinner("Looking up project");
|
|
169
|
+
const query = {};
|
|
170
|
+
if (config.defaultTeam)
|
|
171
|
+
query.team_id = config.defaultTeam;
|
|
172
|
+
const projRes = await api.get("/v1/projects", token, query);
|
|
173
|
+
s.stop();
|
|
174
|
+
if (!projRes.ok) {
|
|
175
|
+
emitError(projRes.status === 401 || projRes.status === 403 ? "INVALID_KEY"
|
|
176
|
+
: projRes.status >= 500 || projRes.status === 0 ? "API_UNAVAILABLE"
|
|
177
|
+
: "CLI_ERROR", apiError(projRes, "Failed to fetch projects."), "");
|
|
178
|
+
}
|
|
179
|
+
const found = asList(projRes.data).find((p) => p.id === parsed.project || p.name === parsed.project);
|
|
180
|
+
if (!found) {
|
|
181
|
+
emitError("NOT_FOUND", `Project "${parsed.project}" not found.`, "List projects with: bata status");
|
|
182
|
+
}
|
|
183
|
+
projectId = found.id;
|
|
184
|
+
}
|
|
185
|
+
if (!projectId) {
|
|
186
|
+
emitError("NO_PROJECT", "No project specified.", "Pass --project <id|name>, or run `bata link <project>` to set a default.");
|
|
187
|
+
}
|
|
188
|
+
const s = spinner("Fetching connection info");
|
|
189
|
+
const connRes = await api.get(`/v1/connection-info/${projectId}`, token, { reveal: "true" });
|
|
190
|
+
if (!connRes.ok || !connRes.data?.connections?.length) {
|
|
191
|
+
s.stop();
|
|
192
|
+
emitError(connRes.status >= 500 || connRes.status === 0 ? "API_UNAVAILABLE" : "NOT_FOUND", apiError(connRes, "Could not fetch connection info for this project."), "");
|
|
193
|
+
}
|
|
194
|
+
const conns = connRes.data.connections;
|
|
195
|
+
// Branch precedence: an explicit --branch wins, else the branch pinned by
|
|
196
|
+
// `bata db branch checkout` in .batadata/project.json, but ONLY when studio
|
|
197
|
+
// targets the linked project (same guard as `db query`: a pinned branch from
|
|
198
|
+
// project A must never resolve against an explicit --project B).
|
|
199
|
+
const link = readLinkFile()?.link;
|
|
200
|
+
const pinnedBranch = link && link.projectId === projectId ? link.branchId ?? undefined : undefined;
|
|
201
|
+
const branchRef = parsed.branch ?? pinnedBranch;
|
|
202
|
+
const conn = branchRef
|
|
203
|
+
? conns.find((c) => c.branch_id === branchRef || c.branch_name === branchRef)
|
|
204
|
+
: conns.find((c) => c.is_primary) ?? conns[0];
|
|
205
|
+
if (!conn) {
|
|
206
|
+
s.stop();
|
|
207
|
+
emitError("BRANCH_NOT_FOUND", `Branch "${branchRef}" not found in this project.`, "List branches with: bata db branches --json");
|
|
208
|
+
}
|
|
209
|
+
if (!conn.direct || !conn.branch_id) {
|
|
210
|
+
s.stop();
|
|
211
|
+
emitError("NOT_FOUND", "No direct connection string available for this branch.", "");
|
|
212
|
+
}
|
|
213
|
+
// BataDB computes scale to zero: wake the branch before handing the URI to
|
|
214
|
+
// Studio, so its first introspection doesn't hit a cold compute and die.
|
|
215
|
+
s.update("Waking compute");
|
|
216
|
+
await wakeBranch(conn.branch_id, conn.compute_status, token, (secs) => {
|
|
217
|
+
s.update(`Waking compute (${secs}s)`);
|
|
218
|
+
});
|
|
219
|
+
s.stop();
|
|
220
|
+
log();
|
|
221
|
+
logInfo(`Launching Turbine Studio for ${colors.cyan(connRes.data.project_name ?? projectId)}`
|
|
222
|
+
+ (conn.branch_name ? ` ${colors.dim(`branch ${conn.branch_name}`)}` : ""));
|
|
223
|
+
log(` ${colors.dim("Local UI over a direct TCP connection. Read-only by default;")}`);
|
|
224
|
+
log(` ${colors.dim("PII redaction is turbine's UI feature, not a BataDB server control.")}`);
|
|
225
|
+
log(` ${colors.dim("Press Ctrl+C to stop.")}`);
|
|
226
|
+
log();
|
|
227
|
+
// Run npx from a scratch dir so a turbine.config.* in the user's cwd can
|
|
228
|
+
// never override the DATABASE_URL we resolved (turbine's env wins over its
|
|
229
|
+
// config file, but a scratch cwd removes the question entirely). Studio
|
|
230
|
+
// introspects the live DB, so no config/client generation is needed.
|
|
231
|
+
const scratchDir = mkdtempSync(path.join(os.tmpdir(), "bata-studio-"));
|
|
232
|
+
const cleanup = () => {
|
|
233
|
+
try {
|
|
234
|
+
rmSync(scratchDir, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
/* best effort */
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const turbineArgs = ["--yes", "turbine-orm@latest", "studio"];
|
|
241
|
+
if (parsed.port !== undefined)
|
|
242
|
+
turbineArgs.push("--port", String(parsed.port));
|
|
243
|
+
if (parsed.write)
|
|
244
|
+
turbineArgs.push("--write");
|
|
245
|
+
if (parsed.showPii)
|
|
246
|
+
turbineArgs.push("--show-pii");
|
|
247
|
+
if (parsed.noOpen)
|
|
248
|
+
turbineArgs.push("--no-open");
|
|
249
|
+
const child = spawn("npx", turbineArgs, {
|
|
250
|
+
cwd: scratchDir,
|
|
251
|
+
stdio: "inherit",
|
|
252
|
+
env: { ...process.env, DATABASE_URL: conn.direct },
|
|
253
|
+
});
|
|
254
|
+
child.on("error", (err) => {
|
|
255
|
+
cleanup();
|
|
256
|
+
emitError("CLI_ERROR", `Failed to launch turbine-orm studio via npx: ${err.message}`, "Is npx (Node.js) on your PATH?");
|
|
257
|
+
});
|
|
258
|
+
child.on("exit", (code) => {
|
|
259
|
+
cleanup();
|
|
260
|
+
process.exit(code ?? 0);
|
|
261
|
+
});
|
|
262
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { handleCompute } from "./commands/compute.js";
|
|
|
17
17
|
import { handleRestore } from "./commands/restore.js";
|
|
18
18
|
import { handlePowdb } from "./commands/powdb.js";
|
|
19
19
|
import { importDb } from "./commands/import.js";
|
|
20
|
+
import { studio } from "./commands/studio.js";
|
|
20
21
|
import { link, unlink } from "./commands/link.js";
|
|
21
22
|
import { parseGlobalFlags } from "./args.js";
|
|
22
23
|
import { isJsonMode } from "./config.js";
|
|
@@ -63,6 +64,7 @@ function help() {
|
|
|
63
64
|
log(` ${colors.cyan("db branch delete")} Delete a branch`);
|
|
64
65
|
log(` ${colors.cyan("db branch checkout")} Pin a branch into ${colors.dim(".batadata/project.json")}`);
|
|
65
66
|
log(` ${colors.cyan("db studio")} Open table browser in browser`);
|
|
67
|
+
log(` ${colors.cyan("studio")} Run Turbine Studio locally against a branch ${colors.dim("(local UI, direct TCP)")}`);
|
|
66
68
|
log(` ${colors.cyan("db query")} Run a SQL query ${colors.dim("(--branch <id> to target a branch)")}`);
|
|
67
69
|
log();
|
|
68
70
|
log(` ${colors.bold("Compute")}`);
|
|
@@ -220,6 +222,10 @@ async function main() {
|
|
|
220
222
|
case "import":
|
|
221
223
|
await importDb(rest);
|
|
222
224
|
break;
|
|
225
|
+
// Local Turbine Studio against a branch
|
|
226
|
+
case "studio":
|
|
227
|
+
await studio(rest);
|
|
228
|
+
break;
|
|
223
229
|
// Dev
|
|
224
230
|
case "dev":
|
|
225
231
|
await dev();
|