grok-studio 0.1.0 → 0.1.2

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
@@ -1,117 +1,50 @@
1
1
  # Grok Studio
2
2
 
3
- Self-hosted Grok generation workspace for local/LAN use. The app keeps xAI credentials on the server, lets the browser upload or paste an image, optionally prepares a video-ready first frame, then generates and stores video outputs on disk.
3
+ Self-hosted Grok image-to-video studio. Your xAI credentials stay on the server; turn an image into video from a web UI or the CLI, with outputs stored on disk. Built for local / LAN use.
4
4
 
5
- ## Product Model
6
-
7
- The UI is organized around generated outputs, active work, and source-scoped lineage.
8
-
9
- - **Left rail**: `+` creates a new source image. Active Jobs appears above the output gallery when queued or running work exists; it is global and not tied to the currently selected source. Outputs are typed as `Frame` or `Video`; status text is only for exceptional or in-progress states.
10
- - **Result Graph**: the center canvas shows lineage for the current source. Nodes are concrete artifacts/runs:
11
- - `Source` is the root image.
12
- - each Prep run creates a separate `Frame` node.
13
- - each Video run creates a separate `Video` node.
14
- - **Inspector**: the right panel is the only place for details and actions. Select a node to inspect it, download video output, rerun, prepare another frame, or animate the selected frame.
15
-
16
- Lineage is data-driven:
17
-
18
- - Source-linked video jobs attach only when `inputFrame.source === "source"` and the job's `clientSourceId` matches the current source.
19
- - Frame-linked video jobs attach only when `inputFrame.source === "prep"` and `preparedImageId` matches a frame in the current graph.
20
- - UI cache must not decide graph edges. If lineage fields are missing, do not guess by recent history.
21
-
22
- ## Run
5
+ ## Install & run
23
6
 
24
7
  ```bash
25
- vp install
26
- cp .env.example .env
27
- vpr build
28
- vpr start
8
+ npx grok-studio
29
9
  ```
30
10
 
31
- For normal local use:
32
-
33
- ```bash
34
- vpr launch
35
- ```
36
-
37
- `launch` builds the app, checks xAI auth, opens the browser login flow when no token exists, then starts the web app. Open <http://127.0.0.1:8787>.
38
-
39
- ## LAN Access
40
-
41
- For local-network access, set:
42
-
43
- ```dotenv
44
- HOST=0.0.0.0
45
- ACCESS_TOKEN=
46
- ```
47
-
48
- Then restart the service and open `http://<mac-lan-ip>:8787` or `http://<mac-local-hostname>.local:8787` from another device on the same LAN.
49
-
50
- `ACCESS_TOKEN` can be left empty for trusted local/LAN use. Set it before exposing the app outside a trusted network.
11
+ That's it. First run opens the xAI browser login if needed, then serves on <http://127.0.0.1:8787>. (`npm i -g grok-studio` also works.)
51
12
 
52
- ## Configuration
53
-
54
- - `HOST` / `PORT` control where the server binds.
55
- - `ACCESS_TOKEN` optionally protects browser/API access outside loopback.
56
- - `WORKSPACE_DIR` stores `images/`, `videos/`, `jobs/`, and `prepared-images/`.
57
- - `XAI_AUTH_MODE=oauth` reads `XAI_OAUTH_TOKEN_FILE`, refreshes it when needed, and `vpr launch` can bootstrap it interactively.
58
- - `XAI_AUTH_MODE=api_key` uses `XAI_API_KEY`.
59
- - `XAI_VIDEO_MODEL` controls image-to-video generation.
60
- - `XAI_IMAGE_MODEL` controls first-frame image editing.
61
- - `DEFAULT_DURATION_SECONDS` supports up to 15 seconds.
62
- - `MAX_VARIATIONS` limits sequential variations per video job.
63
-
64
- The app binds to `127.0.0.1` by default. Use `HOST=0.0.0.0` only for LAN/tunnel exposure.
65
-
66
- ## Grok Login Test
67
-
68
- To test the xAI/Grok browser login without touching the token file used by the running service:
69
-
70
- ```bash
71
- vpr login
72
- ```
73
-
74
- The command opens the xAI authorization page, waits for the local callback, saves the token state to a test path, and verifies it with the API. If xAI shows a fallback code instead of redirecting, paste that code into the terminal prompt.
75
-
76
- Useful variants:
13
+ ## CLI
77
14
 
78
15
  ```bash
79
- vpr login --check
80
- vpr login --print-url-only
81
- vpr login --live --force
16
+ grok-studio serve [--open] # start the web app (default command)
17
+ grok-studio gen --image a.png --prompt "slow head turn" \
18
+ [--prep --duration 6 --resolution 720p --aspect 9:16 --count 2 --out clip.mp4]
19
+ grok-studio login # xAI OAuth login (browser)
20
+ grok-studio status # config + xAI auth + server health
82
21
  ```
83
22
 
84
- `vpr login` is Vite+'s shorthand for `vp run login`. Vite+ built-in top-level commands such as `vp build` and `vp test` cannot be extended by this project, so custom project commands use `vp run <task>` / `vpr <task>`.
23
+ `gen` is headless: image →(optional `--prep` first frame)→ video, no UI; result paths print to stdout.
85
24
 
86
- By default, login writes to `~/.grok-video-web/login-test/xai-oauth.json` so repeated login-flow tests do not disturb the live service token. Use `--live --force` only when intentionally repairing `XAI_OAUTH_TOKEN_FILE`. `--print-url-only` is only for smoke-checking that the OAuth authorization URL can be built; it exits immediately, so do not use that URL to complete a real login.
25
+ ## Web UI
87
26
 
88
- ## Quality Loop
27
+ - **Left rail** — `+` adds a source image; **Active Jobs** lists running work globally; **Outputs** is a gallery of every `Frame` / `Video`.
28
+ - **Result Graph** (center) — every node is one concrete run: `Source` → `Frame` (each prep) → `Video` (each run). Forks are sibling nodes. Lineage is derived purely from data (`inputFrame.preparedImageId` / `clientSourceId`), never guessed from history.
29
+ - **Inspector** (right) — the only place to view a result, download, rerun, prep, or animate the selected node.
89
30
 
90
- Vite+ (`vp`/`vpr`) owns build, lint/format/type checks, and tests:
31
+ ## Configure (`.env`)
91
32
 
92
- ```bash
93
- vpr fmt
94
- vpr fmt:check
95
- vpr lint
96
- vpr lint:fix
97
- vpr check
98
- vp test
99
- vpr build
100
- vpr smoke
33
+ ```dotenv
34
+ HOST=127.0.0.1 # 0.0.0.0 for LAN access
35
+ PORT=8787
36
+ ACCESS_TOKEN= # set to gate non-loopback access (empty = open on loopback/LAN)
37
+ WORKSPACE_DIR=./workspace # holds images/ videos/ jobs/ prepared-images/
38
+ XAI_AUTH_MODE=oauth # or: api_key (+ XAI_API_KEY)
39
+ XAI_VIDEO_MODEL=grok-imagine-video
40
+ XAI_IMAGE_MODEL=grok-imagine-image-quality
101
41
  ```
102
42
 
103
- Quality gates include:
104
-
105
- - React-specific lint rules for hooks, effect dependencies, JSX keys, nested components, unsafe JSX, and button types.
106
- - Type-aware linting and TypeScript checks.
107
- - Architecture guardrails in `tests/architecture.test.ts` to keep the main app shell thin, limit local React state in `App.tsx`, split stylesheet ownership, and keep server route files focused.
108
- - Browser smoke coverage in `scripts/smoke.mjs`, including paste input, image lightbox, video-card scroll behavior, and legacy history not restoring unrelated Prep outputs.
109
-
110
- ## Local Service
111
-
112
- If installed as the local launchd service, restart it after building:
43
+ ## Develop
113
44
 
114
45
  ```bash
115
- launchctl kickstart -k gui/$(id -u)/com.pengx17.grok-video-web
116
- curl -fsS http://127.0.0.1:8787/health
46
+ vp install
47
+ vpr check && vp test && vpr build && vpr smoke
117
48
  ```
49
+
50
+ Quality gates: React/a11y lint, type checks, an architecture-size test (`tests/architecture.test.ts`), and a Playwright smoke (`scripts/smoke.mjs`).
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { Command } from "commander";
3
4
  import path from "node:path";
4
5
  import process$1 from "node:process";
5
6
  import { config } from "dotenv";
6
7
  import { z } from "zod";
7
8
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
9
  import crypto from "node:crypto";
9
- import http from "node:http";
10
- import readline from "node:readline/promises";
11
10
  import { execFile } from "node:child_process";
11
+ import http from "node:http";
12
+ import * as clack from "@clack/prompts";
12
13
  import { fileURLToPath } from "node:url";
13
14
  import express from "express";
14
15
  import multer from "multer";
@@ -1000,21 +1001,16 @@ function formatError(error) {
1000
1001
  }
1001
1002
  //#endregion
1002
1003
  //#region src/server/gen.ts
1003
- async function runGenCli(argv) {
1004
- const args = parseArgs$1(argv);
1005
- if (!args.image) {
1006
- printHelp$2();
1007
- throw new Error("--image <path> is required.");
1008
- }
1009
- if (!fs.existsSync(args.image)) throw new Error(`Image not found: ${args.image}`);
1004
+ async function runGenCli(opts) {
1005
+ if (!fs.existsSync(opts.image)) throw new Error(`Image not found: ${opts.image}`);
1010
1006
  const config = loadConfig();
1011
1007
  try {
1012
1008
  await resolveXaiAuth(config);
1013
1009
  } catch (error) {
1014
1010
  throw new Error(`xAI auth not ready (${error instanceof Error ? error.message : String(error)}). Run: grok-studio login`);
1015
1011
  }
1016
- let imagePath = path.resolve(args.image);
1017
- if (args.prep) {
1012
+ let imagePath = path.resolve(opts.image);
1013
+ if (opts.prep) {
1018
1014
  process.stderr.write("preparing first frame…\n");
1019
1015
  imagePath = (await generateGrokImageEdit({
1020
1016
  config,
@@ -1024,11 +1020,11 @@ async function runGenCli(argv) {
1024
1020
  })).localPath;
1025
1021
  process.stderr.write(`prepared frame: ${imagePath}\n`);
1026
1022
  }
1027
- const overrides = { prompt: args.prompt ?? "" };
1028
- if (args.duration !== void 0) overrides.durationSeconds = args.duration;
1029
- if (args.resolution) overrides.resolution = args.resolution;
1030
- if (args.aspect) overrides.aspectRatio = args.aspect;
1031
- if (args.count !== void 0) overrides.count = args.count;
1023
+ const overrides = { prompt: opts.prompt ?? "" };
1024
+ if (opts.duration !== void 0) overrides.durationSeconds = opts.duration;
1025
+ if (opts.resolution) overrides.resolution = opts.resolution;
1026
+ if (opts.aspect) overrides.aspectRatio = opts.aspect;
1027
+ if (opts.count !== void 0) overrides.count = opts.count;
1032
1028
  const options = normalizeGenerationOptions(overrides, defaultGenerationOptions(config.defaults), config.defaults.maxVariations);
1033
1029
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1034
1030
  const outputs = [];
@@ -1042,7 +1038,7 @@ async function runGenCli(argv) {
1042
1038
  jobId: `gen_${stamp}_${take}`,
1043
1039
  onStatus: (status) => process.stderr.write(` take ${take}: ${status}\n`)
1044
1040
  });
1045
- outputs.push(resolveOutput(result.localPath, args.out, take, options.count));
1041
+ outputs.push(resolveOutput(result.localPath, opts.out, take, options.count));
1046
1042
  }
1047
1043
  for (const file of outputs) console.log(file);
1048
1044
  }
@@ -1052,59 +1048,6 @@ function resolveOutput(localPath, out, take, count) {
1052
1048
  fs.copyFileSync(localPath, path.resolve(target));
1053
1049
  return path.resolve(target);
1054
1050
  }
1055
- function parseArgs$1(values) {
1056
- const parsed = { prep: false };
1057
- for (let index = 0; index < values.length; index += 1) {
1058
- const value = values[index];
1059
- if (value === "--help" || value === "-h") {
1060
- printHelp$2();
1061
- process.exit(0);
1062
- }
1063
- if (value === "--prep") {
1064
- parsed.prep = true;
1065
- continue;
1066
- }
1067
- const next = values[index + 1];
1068
- const requireNext = () => {
1069
- if (!next || next.startsWith("--")) throw new Error(`${value} requires a value.`);
1070
- index += 1;
1071
- return next;
1072
- };
1073
- if (value === "--image") parsed.image = requireNext();
1074
- else if (value === "--prompt") parsed.prompt = requireNext();
1075
- else if (value === "--out") parsed.out = requireNext();
1076
- else if (value === "--resolution") parsed.resolution = requireNext();
1077
- else if (value === "--aspect") parsed.aspect = requireNext();
1078
- else if (value === "--duration") parsed.duration = parsePositiveInt(requireNext(), value);
1079
- else if (value === "--count") parsed.count = parsePositiveInt(requireNext(), value);
1080
- else throw new Error(`Unknown argument: ${value}`);
1081
- }
1082
- return parsed;
1083
- }
1084
- function parsePositiveInt(value, flag) {
1085
- const parsed = Number(value);
1086
- if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} requires a positive integer.`);
1087
- return parsed;
1088
- }
1089
- function printHelp$2() {
1090
- console.log(`Usage: grok-studio gen --image <path> [options]
1091
-
1092
- Headlessly turn an image into a video (no UI).
1093
-
1094
- Options:
1095
- --image <path> Source image (required).
1096
- --prompt <text> Motion prompt.
1097
- --prep Run a first-frame prep pass before the video.
1098
- --duration <seconds> Clip length. Default from config.
1099
- --resolution <res> e.g. 720p / 1080p.
1100
- --aspect <ratio> e.g. source / 9:16 / 16:9 / 1:1.
1101
- --count <n> Number of takes. Default 1.
1102
- --out <path> Write the result here (count>1 appends -N). Default: workspace path.
1103
-
1104
- Example:
1105
- grok-studio gen --image portrait.png --prompt "slow head turn" --duration 6 --out clip.mp4
1106
- `);
1107
- }
1108
1051
  //#endregion
1109
1052
  //#region src/server/oauth-login.ts
1110
1053
  var DISCOVERY_URL = `https://auth.x.ai/.well-known/openid-configuration`;
@@ -1128,18 +1071,15 @@ async function runXaiOauthLogin(options) {
1128
1071
  nonce
1129
1072
  });
1130
1073
  try {
1131
- console.log("Open this URL to authorize Grok Studio with xAI:");
1132
- console.log(authorizeUrl);
1133
- console.log();
1074
+ clack.note(authorizeUrl, "Authorize Grok Studio with xAI");
1134
1075
  if (options.printUrlOnly) return;
1135
- console.log(`Waiting for callback on ${redirectUri}`);
1136
- console.log("If xAI shows a fallback code, paste that code here and press Return.");
1137
1076
  if (!options.noBrowser) openBrowser$1(authorizeUrl);
1138
- const callback = await waitForCallbackOrFallbackInput({
1077
+ const callback = await awaitAuthorization({
1139
1078
  server,
1140
1079
  timeoutMs: (options.timeoutSeconds ?? 600) * 1e3,
1141
1080
  redirectUri,
1142
- expectedState: state
1081
+ expectedState: state,
1082
+ noBrowser: Boolean(options.noBrowser)
1143
1083
  });
1144
1084
  if (callback.error) throw new Error(`xAI authorization failed: ${callback.error_description ?? callback.error}`);
1145
1085
  if (callback.state !== state) throw new Error("xAI authorization failed: state mismatch.");
@@ -1170,7 +1110,7 @@ async function runXaiOauthLogin(options) {
1170
1110
  last_refresh: (/* @__PURE__ */ new Date()).toISOString(),
1171
1111
  source: "grok-video-web-oauth-login"
1172
1112
  });
1173
- console.log(`xAI OAuth login successful. Token state saved to ${options.outputPath}`);
1113
+ clack.log.success(`xAI OAuth login successful. Token saved to ${options.outputPath}`);
1174
1114
  } finally {
1175
1115
  await closeServer(server.server);
1176
1116
  }
@@ -1266,24 +1206,25 @@ async function waitForCallback(serverState, timeoutMs) {
1266
1206
  }
1267
1207
  throw new Error("Timed out waiting for xAI OAuth callback.");
1268
1208
  }
1269
- async function waitForCallbackOrFallbackInput(input) {
1270
- const rl = readline.createInterface({
1271
- input: process$1.stdin,
1272
- output: process$1.stdout
1273
- });
1209
+ async function awaitAuthorization(input) {
1210
+ if (input.noBrowser) return pasteFallback(input.redirectUri, input.expectedState);
1211
+ const spinner = clack.spinner();
1212
+ spinner.start("Waiting for browser login…");
1274
1213
  try {
1275
- return await Promise.race([waitForCallback(input.server, input.timeoutMs), waitForFallbackInput(rl, {
1276
- redirectUri: input.redirectUri,
1277
- expectedState: input.expectedState
1278
- })]);
1279
- } finally {
1280
- rl.close();
1214
+ const result = await waitForCallback(input.server, input.timeoutMs);
1215
+ spinner.stop("Authorized in browser.");
1216
+ return result;
1217
+ } catch {
1218
+ spinner.stop("No browser callback received.");
1219
+ return pasteFallback(input.redirectUri, input.expectedState);
1281
1220
  }
1282
1221
  }
1283
- async function waitForFallbackInput(rl, input) {
1222
+ async function pasteFallback(redirectUri, expectedState) {
1284
1223
  while (true) {
1285
- const parsed = parseCallbackInput(await rl.question(`Paste the full ${input.redirectUri} callback URL or xAI fallback code: `), input.expectedState);
1286
- if (parsed) return parsed;
1224
+ const value = await clack.text({ message: `Paste the ${redirectUri} callback URL or the xAI fallback code` });
1225
+ if (clack.isCancel(value)) throw new Error("Login cancelled.");
1226
+ const parsed = parseCallbackInput(String(value), expectedState);
1227
+ if (parsed?.code || parsed?.error) return parsed;
1287
1228
  }
1288
1229
  }
1289
1230
  function parseCallbackInput(value, expectedState) {
@@ -1337,11 +1278,10 @@ function writeJson0600(filePath, value) {
1337
1278
  //#endregion
1338
1279
  //#region src/server/login.ts
1339
1280
  var DEFAULT_TEST_TOKEN_FILE = "~/.grok-video-web/login-test/xai-oauth.json";
1340
- async function runLoginCli(argv) {
1341
- const args = parseArgs(argv);
1281
+ async function runLoginCli(opts) {
1342
1282
  const config = loadConfig();
1343
1283
  if (config.xai.authMode !== "oauth") throw new Error("Grok login flow only applies when XAI_AUTH_MODE=oauth.");
1344
- const outputPath = path.resolve(expandHome(args.output ?? (args.live ? config.xai.oauthTokenFile : DEFAULT_TEST_TOKEN_FILE)));
1284
+ const outputPath = path.resolve(expandHome(opts.output ?? (opts.live ? config.xai.oauthTokenFile : DEFAULT_TEST_TOKEN_FILE)));
1345
1285
  const authConfig = {
1346
1286
  ...config,
1347
1287
  xai: {
@@ -1349,115 +1289,26 @@ async function runLoginCli(argv) {
1349
1289
  oauthTokenFile: outputPath
1350
1290
  }
1351
1291
  };
1352
- if (args.check) {
1292
+ if (opts.check) {
1353
1293
  const auth = await resolveXaiAuth(authConfig);
1354
1294
  console.log(`xAI OAuth ready: ${outputPath}`);
1355
1295
  console.log(`Auth mode: ${auth.label}`);
1356
1296
  console.log(`Base URL: ${auth.baseUrl}`);
1357
1297
  return;
1358
1298
  }
1359
- if (fs.existsSync(outputPath) && !args.force && !args.printUrlOnly && (args.live || args.output)) {
1360
- console.error(`Refusing to overwrite existing token state: ${outputPath}`);
1361
- console.error("Pass --force to refresh this token file. The default test token path can be overwritten without --force.");
1362
- process.exit(1);
1363
- }
1299
+ if (fs.existsSync(outputPath) && !opts.force && !opts.printUrlOnly && (opts.live || opts.output)) throw new Error(`Refusing to overwrite existing token state: ${outputPath}. Pass --force to refresh it.`);
1364
1300
  await runXaiOauthLogin({
1365
1301
  outputPath,
1366
- noBrowser: args.noBrowser,
1367
- port: args.port,
1368
- timeoutSeconds: args.timeoutSeconds,
1369
- printUrlOnly: args.printUrlOnly
1302
+ noBrowser: opts.browser === false,
1303
+ port: opts.port,
1304
+ timeoutSeconds: opts.timeout,
1305
+ printUrlOnly: opts.printUrlOnly
1370
1306
  });
1371
- if (!args.printUrlOnly) {
1307
+ if (!opts.printUrlOnly) {
1372
1308
  await resolveXaiAuth(authConfig);
1373
1309
  console.log("xAI OAuth token verified with the API.");
1374
1310
  }
1375
1311
  }
1376
- function parseArgs(values) {
1377
- const parsed = {
1378
- live: false,
1379
- force: false,
1380
- noBrowser: false,
1381
- check: false,
1382
- printUrlOnly: false
1383
- };
1384
- for (let index = 0; index < values.length; index += 1) {
1385
- const value = values[index];
1386
- if (value === "--") continue;
1387
- if (value === "--help" || value === "-h") {
1388
- printHelp$1();
1389
- process.exit(0);
1390
- }
1391
- if (value === "--force") {
1392
- parsed.force = true;
1393
- continue;
1394
- }
1395
- if (value === "--live") {
1396
- parsed.live = true;
1397
- continue;
1398
- }
1399
- if (value === "--no-browser") {
1400
- parsed.noBrowser = true;
1401
- continue;
1402
- }
1403
- if (value === "--check") {
1404
- parsed.check = true;
1405
- continue;
1406
- }
1407
- if (value === "--print-url-only") {
1408
- parsed.printUrlOnly = true;
1409
- parsed.noBrowser = true;
1410
- continue;
1411
- }
1412
- if (value === "--output") {
1413
- parsed.output = requireValue(values, index, value);
1414
- index += 1;
1415
- continue;
1416
- }
1417
- if (value === "--port") {
1418
- parsed.port = parseInteger(requireValue(values, index, value), value);
1419
- index += 1;
1420
- continue;
1421
- }
1422
- if (value === "--timeout") {
1423
- parsed.timeoutSeconds = parseInteger(requireValue(values, index, value), value);
1424
- index += 1;
1425
- continue;
1426
- }
1427
- throw new Error(`Unknown argument: ${value}`);
1428
- }
1429
- return parsed;
1430
- }
1431
- function printHelp$1() {
1432
- console.log(`Usage: grok-studio login [options]
1433
-
1434
- Options:
1435
- --live Write to XAI_OAUTH_TOKEN_FILE instead of the safe test token file.
1436
- --output <path> Token state file to write. Defaults to ~/.grok-video-web/login-test/xai-oauth.json.
1437
- --force Allow replacing an existing explicit or live token state file.
1438
- --check Verify the selected token file without opening login.
1439
- --no-browser Print the auth URL and wait instead of opening a browser.
1440
- --print-url-only Print a disposable auth URL for smoke testing, then exit.
1441
- --port <number> Local callback port. Default: 56121.
1442
- --timeout <seconds> Login wait timeout. Default: 600.
1443
-
1444
- Safe test example:
1445
- grok-studio login
1446
-
1447
- Live token repair:
1448
- grok-studio login --live --force
1449
- `);
1450
- }
1451
- function requireValue(values, index, flag) {
1452
- const next = values[index + 1];
1453
- if (!next || next.startsWith("--")) throw new Error(`${flag} requires a value.`);
1454
- return next;
1455
- }
1456
- function parseInteger(value, flag) {
1457
- const parsed = Number(value);
1458
- if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${flag} requires a positive integer.`);
1459
- return parsed;
1460
- }
1461
1312
  function expandHome(value) {
1462
1313
  if (value === "~") return process.env.HOME ?? process.cwd();
1463
1314
  if (value.startsWith("~/")) return path.join(process.env.HOME ?? process.cwd(), value.slice(2));
@@ -1832,14 +1683,15 @@ function cleanOptionalString(value) {
1832
1683
  //#endregion
1833
1684
  //#region src/server/serve.ts
1834
1685
  async function startServer(options = {}) {
1686
+ clack.intro("Grok Studio");
1835
1687
  const config = loadConfig();
1836
1688
  await ensureAuthReady(config);
1837
1689
  const { app } = createServer(config);
1838
1690
  app.listen(config.port, config.host, () => {
1839
- const url = `http://${config.host}:${config.port}`;
1840
- console.log(`Grok Studio is running: ${url}`);
1841
- if (config.accessToken) console.log("ACCESS_TOKEN is configured; enter the value from .env in the browser gate.");
1842
- else console.log("ACCESS_TOKEN is not set; API access is open to whoever can reach this host.");
1691
+ const url = `http://${config.host === "0.0.0.0" || config.host === "::" ? "127.0.0.1" : config.host}:${config.port}`;
1692
+ const access = config.accessToken ? "Access: token required (set in .env)" : "Access: open — no ACCESS_TOKEN set";
1693
+ clack.note(`${url}\n${access}`, "Running");
1694
+ clack.outro("Press Ctrl+C to stop.");
1843
1695
  if (options.open) openBrowser(url);
1844
1696
  });
1845
1697
  }
@@ -1850,13 +1702,11 @@ async function ensureAuthReady(config) {
1850
1702
  }
1851
1703
  try {
1852
1704
  await resolveXaiAuth(config);
1853
- console.log(`xAI OAuth ready: ${config.xai.oauthTokenFile}`);
1705
+ clack.log.success(`xAI OAuth ready (${config.xai.oauthTokenFile})`);
1854
1706
  return;
1855
1707
  } catch (error) {
1856
- if (fs.existsSync(config.xai.oauthTokenFile)) {
1857
- console.warn(`xAI OAuth token check failed: ${error instanceof Error ? error.message : String(error)}`);
1858
- console.warn("Starting OAuth login to repair the token state.");
1859
- } else console.log(`No xAI OAuth token found at ${config.xai.oauthTokenFile}. Starting login.`);
1708
+ if (fs.existsSync(config.xai.oauthTokenFile)) clack.log.warn(`xAI token check failed: ${error instanceof Error ? error.message : String(error)} — re-running login.`);
1709
+ else clack.log.info("No xAI token found starting login.");
1860
1710
  }
1861
1711
  await runXaiOauthLogin({ outputPath: config.xai.oauthTokenFile });
1862
1712
  await resolveXaiAuth(config);
@@ -1868,35 +1718,22 @@ function openBrowser(url) {
1868
1718
  }
1869
1719
  //#endregion
1870
1720
  //#region src/server/cli.ts
1871
- var [command, ...rest] = process.argv.slice(2);
1721
+ var program = new Command();
1722
+ program.name("grok-studio").description("Self-hosted Grok image-to-video studio (web app + CLI).").version(readVersion(), "-v, --version");
1723
+ program.command("serve", { isDefault: true }).description("Start the web app (ensures xAI auth, then serves HTTP).").option("--open", "open the browser after starting").action(async (opts) => {
1724
+ await startServer({ open: Boolean(opts.open) });
1725
+ });
1726
+ program.command("login").description("Run the xAI OAuth login.").option("--live", "write to the live token file instead of the safe test path").option("--output <path>", "token state file to write").option("--force", "allow replacing an existing token file").option("--check", "verify the selected token file without logging in").option("--no-browser", "print the auth URL and wait instead of opening a browser").option("--print-url-only", "print a disposable auth URL, then exit").option("--port <number>", "local callback port", Number).option("--timeout <seconds>", "login wait timeout", Number).action(async (opts) => {
1727
+ await runLoginCli(opts);
1728
+ });
1729
+ program.command("gen").description("Headlessly turn an image into a video (no UI).").requiredOption("--image <path>", "source image").option("--prompt <text>", "motion prompt").option("--prep", "run a first-frame prep pass before the video").option("--duration <seconds>", "clip length", Number).option("--resolution <res>", "e.g. 720p / 1080p").option("--aspect <ratio>", "e.g. source / 9:16 / 16:9 / 1:1").option("--count <n>", "number of takes", Number).option("--out <path>", "write the result here (count>1 appends -N)").action(async (opts) => {
1730
+ await runGenCli(opts);
1731
+ });
1732
+ program.command("status").description("Print config, xAI auth, and server health.").action(async () => {
1733
+ await runStatus();
1734
+ });
1872
1735
  try {
1873
- switch (command) {
1874
- case void 0:
1875
- case "serve":
1876
- await startServer({ open: rest.includes("--open") });
1877
- break;
1878
- case "login":
1879
- await runLoginCli(rest);
1880
- break;
1881
- case "gen":
1882
- await runGenCli(rest);
1883
- break;
1884
- case "status":
1885
- await runStatus();
1886
- break;
1887
- case "-v":
1888
- case "--version":
1889
- console.log(readVersion());
1890
- break;
1891
- case "-h":
1892
- case "--help":
1893
- printHelp();
1894
- break;
1895
- default:
1896
- console.error(`Unknown command: ${command}\n`);
1897
- printHelp();
1898
- process.exit(1);
1899
- }
1736
+ await program.parseAsync(process.argv);
1900
1737
  } catch (error) {
1901
1738
  console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
1902
1739
  process.exit(1);
@@ -1930,20 +1767,5 @@ function readVersion() {
1930
1767
  return "0.0.0";
1931
1768
  }
1932
1769
  }
1933
- function printHelp() {
1934
- console.log(`Grok Studio — self-hosted image-to-video studio
1935
-
1936
- Usage: grok-studio <command> [options]
1937
-
1938
- Commands:
1939
- serve Start the web app (default). Ensures xAI auth, then serves HTTP.
1940
- --open open the browser after starting
1941
- login [options] Run the xAI OAuth login (see: grok-studio login --help)
1942
- gen [options] Headlessly turn an image into a video (see: grok-studio gen --help)
1943
- status Print config, xAI auth, and server health
1944
- --version, -v Print the version
1945
- --help, -h Print this help
1946
- `);
1947
- }
1948
1770
  //#endregion
1949
1771
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grok-studio",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "Self-hosted Grok image-to-video studio (web app + CLI).",
6
6
  "bin": {
@@ -35,7 +35,9 @@
35
35
  "prepack": "vp run build"
36
36
  },
37
37
  "dependencies": {
38
+ "@clack/prompts": "^1.5.0",
38
39
  "@xyflow/react": "^12.11.0",
40
+ "commander": "^15.0.0",
39
41
  "dotenv": "^17.2.3",
40
42
  "express": "^5.2.1",
41
43
  "lucide-react": "^0.561.0",