@oodarun/cli 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -298,7 +298,7 @@ createRoot(document.getElementById("root")!).render(
298
298
  });
299
299
 
300
300
  // src/cli/index.ts
301
- import path9 from "path";
301
+ import path11 from "path";
302
302
  import readline2 from "readline";
303
303
  import { select as select3, confirm, Separator as Separator3 } from "@inquirer/prompts";
304
304
 
@@ -460,6 +460,7 @@ function parseConfig(jsonString) {
460
460
  }
461
461
  return {
462
462
  name: userConfig.name,
463
+ slug: userConfig.slug,
463
464
  description: userConfig.description,
464
465
  tools: mergedTools,
465
466
  claude: userConfig.claude || DEFAULT_CONFIG.claude
@@ -845,6 +846,31 @@ async function loginWithPassword(email, password) {
845
846
  return null;
846
847
  }
847
848
  }
849
+ async function requestLoginCode(email) {
850
+ try {
851
+ const res = await fetch(`${ORG_API_BASE}/auth/request-code`, {
852
+ method: "POST",
853
+ headers: { "Content-Type": "application/json" },
854
+ body: JSON.stringify({ email })
855
+ });
856
+ return res.ok;
857
+ } catch {
858
+ return false;
859
+ }
860
+ }
861
+ async function verifyLoginCode(email, code) {
862
+ try {
863
+ const res = await fetch(`${ORG_API_BASE}/auth/verify-code`, {
864
+ method: "POST",
865
+ headers: { "Content-Type": "application/json" },
866
+ body: JSON.stringify({ email, code })
867
+ });
868
+ if (!res.ok) return null;
869
+ return await res.json();
870
+ } catch {
871
+ return null;
872
+ }
873
+ }
848
874
  async function signupWithPassword(email, password, name) {
849
875
  try {
850
876
  const res = await fetch(`${ORG_API_BASE}/auth/signup`, {
@@ -1128,58 +1154,49 @@ function patchFetchForOrg(orgId) {
1128
1154
  });
1129
1155
  }
1130
1156
  async function handleEmailPasswordAuth() {
1131
- const action = await select2({
1132
- message: "Do you have an account?",
1157
+ const method = await select2({
1158
+ message: "How would you like to sign in?",
1133
1159
  choices: [
1134
- { name: "Yes, log me in", value: "login" },
1135
- { name: "No, I need to sign up (requires invite)", value: "signup" }
1160
+ { name: "Email me a login code (recommended)", value: "code" },
1161
+ { name: "Use my password", value: "password" },
1162
+ { name: "Sign up (requires invite)", value: "signup" }
1136
1163
  ]
1137
1164
  });
1138
1165
  console.log("");
1139
- if (action === "signup") {
1140
- const email2 = await prompt(` ${c.blue}${c.bold}Email:${c.reset} `);
1141
- if (!email2) process.exit(1);
1142
- const name = await prompt(` ${c.blue}${c.bold}Your name:${c.reset} `);
1143
- if (!name) process.exit(1);
1144
- for (let attempt = 0; attempt < 3; attempt++) {
1145
- const password = await promptPassword(` ${c.blue}${c.bold}Password:${c.reset} `);
1146
- if (!password) process.exit(1);
1147
- const result = await signupWithPassword(email2, password, name);
1148
- if (!result) {
1149
- console.log(` ${c.red}Could not reach the server. Check your connection.${c.reset}`);
1150
- process.exit(1);
1151
- }
1152
- if (result.ok) {
1153
- const loginResult = await loginWithPassword(email2, password);
1154
- if (!loginResult) {
1155
- console.log(` ${c.red}Account created but login failed. Try again.${c.reset}`);
1156
- process.exit(1);
1157
- }
1158
- return completeJwtLogin(loginResult);
1159
- }
1160
- const remaining = 2 - attempt;
1161
- console.log(` ${c.red}${result.error || "Signup failed"}${c.reset}`);
1162
- if (result.errors?.length) {
1163
- for (const err of result.errors) {
1164
- console.log(` ${c.gray}\u2022 ${err}${c.reset}`);
1165
- }
1166
- }
1167
- if (remaining > 0) {
1168
- console.log(` ${c.gray}${remaining} attempt${remaining > 1 ? "s" : ""} remaining${c.reset}`);
1169
- }
1170
- }
1171
- console.log(` ${c.red}Too many failed attempts.${c.reset}`);
1166
+ if (method === "code") return emailCodeLoginInteractive();
1167
+ if (method === "signup") return signupInteractive();
1168
+ return passwordLoginInteractive();
1169
+ }
1170
+ async function emailCodeLoginInteractive(prefillEmail) {
1171
+ const email = prefillEmail || await prompt(` ${c.blue}${c.bold}Email:${c.reset} `);
1172
+ if (!email) process.exit(1);
1173
+ if (!await requestLoginCode(email)) {
1174
+ console.log(` ${c.red}Couldn't send a login code. Check the email address and try again.${c.reset}`);
1172
1175
  process.exit(1);
1173
1176
  }
1177
+ console.log(` ${c.gray}We emailed a 6-digit code to ${email} (expires in 10 minutes).${c.reset}`);
1178
+ console.log("");
1179
+ for (let attempt = 0; attempt < 3; attempt++) {
1180
+ const code = await promptToken(` ${c.blue}${c.bold}Code:${c.reset} `);
1181
+ if (!code) process.exit(1);
1182
+ const result = await verifyLoginCode(email, code);
1183
+ if (result) return completeJwtLogin(result);
1184
+ const remaining = 2 - attempt;
1185
+ if (remaining > 0) {
1186
+ console.log(` ${c.red}Invalid or expired code.${c.reset} ${c.gray}${remaining} attempt${remaining > 1 ? "s" : ""} remaining${c.reset}`);
1187
+ }
1188
+ }
1189
+ console.log(` ${c.red}Too many failed attempts.${c.reset}`);
1190
+ process.exit(1);
1191
+ }
1192
+ async function passwordLoginInteractive() {
1174
1193
  const email = await prompt(` ${c.blue}${c.bold}Email:${c.reset} `);
1175
1194
  if (!email) process.exit(1);
1176
1195
  for (let attempt = 0; attempt < 3; attempt++) {
1177
1196
  const password = await promptPassword(` ${c.blue}${c.bold}Password:${c.reset} `);
1178
1197
  if (!password) process.exit(1);
1179
1198
  const result = await loginWithPassword(email, password);
1180
- if (result) {
1181
- return completeJwtLogin(result);
1182
- }
1199
+ if (result) return completeJwtLogin(result);
1183
1200
  const remaining = 2 - attempt;
1184
1201
  if (remaining > 0) {
1185
1202
  console.log(` ${c.red}Invalid password.${c.reset} ${c.gray}${remaining} attempt${remaining > 1 ? "s" : ""} remaining${c.reset}`);
@@ -1188,6 +1205,126 @@ async function handleEmailPasswordAuth() {
1188
1205
  console.log(` ${c.red}Too many failed attempts.${c.reset}`);
1189
1206
  process.exit(1);
1190
1207
  }
1208
+ async function signupInteractive() {
1209
+ const email = await prompt(` ${c.blue}${c.bold}Email:${c.reset} `);
1210
+ if (!email) process.exit(1);
1211
+ const name = await prompt(` ${c.blue}${c.bold}Your name:${c.reset} `);
1212
+ if (!name) process.exit(1);
1213
+ for (let attempt = 0; attempt < 3; attempt++) {
1214
+ const password = await promptPassword(` ${c.blue}${c.bold}Password:${c.reset} `);
1215
+ if (!password) process.exit(1);
1216
+ const result = await signupWithPassword(email, password, name);
1217
+ if (!result) {
1218
+ console.log(` ${c.red}Could not reach the server. Check your connection.${c.reset}`);
1219
+ process.exit(1);
1220
+ }
1221
+ if (result.ok) {
1222
+ const loginResult = await loginWithPassword(email, password);
1223
+ if (!loginResult) {
1224
+ console.log(` ${c.red}Account created but login failed. Try again.${c.reset}`);
1225
+ process.exit(1);
1226
+ }
1227
+ return completeJwtLogin(loginResult);
1228
+ }
1229
+ const remaining = 2 - attempt;
1230
+ console.log(` ${c.red}${result.error || "Signup failed"}${c.reset}`);
1231
+ if (result.errors?.length) {
1232
+ for (const err of result.errors) {
1233
+ console.log(` ${c.gray}\u2022 ${err}${c.reset}`);
1234
+ }
1235
+ }
1236
+ if (remaining > 0) {
1237
+ console.log(` ${c.gray}${remaining} attempt${remaining > 1 ? "s" : ""} remaining${c.reset}`);
1238
+ }
1239
+ }
1240
+ console.log(` ${c.red}Too many failed attempts.${c.reset}`);
1241
+ process.exit(1);
1242
+ }
1243
+ async function loginCmd(opts) {
1244
+ if (opts.email && opts.code) {
1245
+ const result = await verifyLoginCode(opts.email, opts.code);
1246
+ if (!result) return failLogin(opts.json, "Invalid or expired code.");
1247
+ finalizeLogin(result, opts.org, opts.json);
1248
+ return;
1249
+ }
1250
+ if (opts.email) {
1251
+ if (!await requestLoginCode(opts.email)) return failLogin(opts.json, "Couldn't send a login code.");
1252
+ if (opts.json) {
1253
+ console.log(JSON.stringify({ ok: true, sent: true, email: opts.email }));
1254
+ } else {
1255
+ console.log(`
1256
+ ${c.green}${c.bold}\u2713${c.reset} Code sent to ${c.bold}${opts.email}${c.reset}`);
1257
+ console.log(` ${c.gray}Check your email, then run:${c.reset}`);
1258
+ console.log(` ${c.cyan}ooda login --email ${opts.email} --code <code>${c.reset}
1259
+ `);
1260
+ }
1261
+ return;
1262
+ }
1263
+ await emailCodeLoginInteractive();
1264
+ }
1265
+ function finalizeLogin(result, orgFlag, json) {
1266
+ if (result.orgs.length === 0) return failLogin(json, "No organizations for this account.");
1267
+ let orgId;
1268
+ let orgName;
1269
+ if (orgFlag) {
1270
+ const match = result.orgs.find((o) => o.orgId === orgFlag);
1271
+ if (!match) return failLogin(json, `You're not a member of org "${orgFlag}".`);
1272
+ orgId = match.orgId;
1273
+ orgName = match.orgName;
1274
+ } else if (result.orgs.length === 1) {
1275
+ orgId = result.orgs[0].orgId;
1276
+ orgName = result.orgs[0].orgName;
1277
+ } else {
1278
+ return failLogin(json, `Multiple organizations \u2014 pass --org <id>. Options: ${result.orgs.map((o) => o.orgId).join(", ")}`);
1279
+ }
1280
+ saveJwtTokens(result.accessToken, result.refreshToken, result.user.id, orgId);
1281
+ const selected = result.orgs.find((o) => o.orgId === orgId);
1282
+ setOrgMode(orgId, result.user.email, result.user.name, orgName, selected?.claudeConfig ?? void 0);
1283
+ patchFetchForOrg(orgId);
1284
+ if (json) {
1285
+ console.log(JSON.stringify({ ok: true, email: result.user.email, orgId, orgName }));
1286
+ } else {
1287
+ console.log(`
1288
+ ${c.green}${c.bold}\u2713${c.reset} Logged in as ${c.bold}${result.user.name}${c.reset} (${orgName})
1289
+ `);
1290
+ }
1291
+ }
1292
+ function failLogin(json, message) {
1293
+ if (json) {
1294
+ console.log(JSON.stringify({ ok: false, error: message }));
1295
+ } else {
1296
+ console.log(`
1297
+ ${c.red}${message}${c.reset}
1298
+ `);
1299
+ }
1300
+ process.exitCode = 1;
1301
+ }
1302
+ function whoamiCmd(opts) {
1303
+ const orgId = getOrgId();
1304
+ const token = getAccessToken();
1305
+ if (!orgId || !token) {
1306
+ if (opts.json) {
1307
+ console.log(JSON.stringify({ ok: false, authenticated: false }));
1308
+ } else {
1309
+ console.log(`
1310
+ ${c.yellow}Not signed in.${c.reset} ${c.gray}Run ${c.bold}ooda login${c.reset}${c.gray} to authenticate.${c.reset}
1311
+ `);
1312
+ }
1313
+ process.exitCode = 1;
1314
+ return;
1315
+ }
1316
+ const orgName = getOrgName() || orgId;
1317
+ const email = getOrgEmail();
1318
+ const name = getOrgDisplayName();
1319
+ if (opts.json) {
1320
+ console.log(JSON.stringify({ ok: true, authenticated: true, orgId, orgName, email: email ?? null, name: name ?? null }));
1321
+ } else {
1322
+ const who = name || email;
1323
+ console.log(`
1324
+ ${c.green}${c.bold}\u2713${c.reset} Signed in${who ? ` as ${c.bold}${who}${c.reset}` : ""} ${c.gray}(${orgName})${c.reset}
1325
+ `);
1326
+ }
1327
+ }
1191
1328
  async function completeJwtLogin(result) {
1192
1329
  let orgId;
1193
1330
  let orgName;
@@ -1216,7 +1353,8 @@ async function completeJwtLogin(result) {
1216
1353
  console.log("");
1217
1354
  return "org";
1218
1355
  }
1219
- async function ensureAuth() {
1356
+ async function ensureAuth(opts = {}) {
1357
+ const requireClaudeToken = opts.requireClaudeToken !== false;
1220
1358
  let apiToken = null;
1221
1359
  const orgClaude = isOrgMode() ? getOrgClaudeConfig() : null;
1222
1360
  let claudeToken = getClaudeToken(orgClaude?.apiKeyHelper || void 0);
@@ -1235,6 +1373,9 @@ async function ensureAuth() {
1235
1373
  `);
1236
1374
  apiToken = await handleEmailPasswordAuth();
1237
1375
  }
1376
+ if (!requireClaudeToken) {
1377
+ return { apiToken, claudeToken: "", claudeSource: "" };
1378
+ }
1238
1379
  async function promptForClaudeToken() {
1239
1380
  const tokenType = await select2({
1240
1381
  message: "Claude token type:",
@@ -1928,19 +2069,27 @@ import path from 'path';
1928
2069
 
1929
2070
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
1930
2071
 
1931
- // Resolve publish URL and auth \u2014 org mode uses JWT via org proxy
1932
- let PUBLISH_URL = 'https://ooda.run/api/publish';
1933
- let publishToken = process.env.PUBLISH_TOKEN || '';
2072
+ // Resolve publish URL and auth. Publishing goes exclusively through the
2073
+ // authenticated org proxy (POST /org/{orgId}/publish), which records the site
2074
+ // in D1 and materializes its access-control policy. The old unauthenticated
2075
+ // loader endpoint is retired, so org credentials are REQUIRED.
2076
+ let PUBLISH_URL = '';
2077
+ let publishToken = '';
1934
2078
  let orgProjectName = '';
1935
2079
  try {
1936
2080
  const orgCreds = JSON.parse(fs.readFileSync('/tmp/ooda-org.json', 'utf-8'));
2081
+ const apiBase = orgCreds.apiBase || 'https://api.ooda.run';
1937
2082
  if (orgCreds.orgId && orgCreds.jwt) {
1938
- PUBLISH_URL = 'https://api.ooda.run/org/' + orgCreds.orgId + '/publish';
2083
+ PUBLISH_URL = apiBase + '/org/' + orgCreds.orgId + '/publish';
1939
2084
  publishToken = orgCreds.jwt;
1940
2085
  }
1941
2086
  if (orgCreds.projectName) orgProjectName = orgCreds.projectName;
1942
2087
  } catch {
1943
- // No org credentials \u2014 use default publish endpoint
2088
+ // handled below
2089
+ }
2090
+ if (!PUBLISH_URL || !publishToken) {
2091
+ console.error('ERROR: Missing ooda org credentials (/tmp/ooda-org.json). Reconnect the project with the ooda CLI to refresh credentials, then publish again.');
2092
+ process.exit(1);
1944
2093
  }
1945
2094
 
1946
2095
  const projectDir = process.argv[2] || process.cwd();
@@ -2640,103 +2789,6 @@ export default function designBrowserSource() {
2640
2789
  };
2641
2790
  }
2642
2791
  `.trim();
2643
- var PUBLISH_SCRIPT2 = `
2644
- import fs from 'fs';
2645
- import path from 'path';
2646
- import { execSync } from 'child_process';
2647
-
2648
- const PUBLISH_URL = 'https://ooda.run/api/publish';
2649
- const publishToken = process.env.PUBLISH_TOKEN || '';
2650
- const MAX_FILE_SIZE = 10 * 1024 * 1024;
2651
-
2652
- // Find the project directory (first arg or cwd)
2653
- const projectDir = process.argv[2] || process.cwd();
2654
-
2655
- // Detect build output directory
2656
- const candidates = ['dist', 'build', 'out', '.output/public', '.next/static'];
2657
- let outputDir = null;
2658
- for (const c of candidates) {
2659
- const p = path.join(projectDir, c);
2660
- if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
2661
- outputDir = p;
2662
- break;
2663
- }
2664
- }
2665
-
2666
- if (!outputDir) {
2667
- console.error('ERROR: No build output directory found. Run your build command first.');
2668
- console.error('Looked for: ' + candidates.join(', '));
2669
- process.exit(1);
2670
- }
2671
-
2672
- console.log('Found build output: ' + outputDir);
2673
-
2674
- // Derive slug from project name (hostname)
2675
- let slug;
2676
- try {
2677
- slug = execSync('hostname', { encoding: 'utf-8' }).trim();
2678
- } catch {
2679
- slug = path.basename(projectDir);
2680
- }
2681
- // Clean slug to match requirements
2682
- slug = slug.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
2683
- if (slug.length < 3) slug = slug + '-site';
2684
- if (slug.length > 64) slug = slug.slice(0, 64);
2685
-
2686
- console.log('Publishing as: ' + slug);
2687
-
2688
- // Collect all files recursively
2689
- function collectFiles(dir, base) {
2690
- const results = [];
2691
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2692
- const full = path.join(dir, entry.name);
2693
- if (entry.isDirectory()) {
2694
- results.push(...collectFiles(full, base));
2695
- } else if (entry.isFile()) {
2696
- const stat = fs.statSync(full);
2697
- if (stat.size <= MAX_FILE_SIZE) {
2698
- const rel = path.relative(base, full);
2699
- const content = fs.readFileSync(full).toString('base64');
2700
- results.push({ path: rel, content });
2701
- } else {
2702
- console.log('Skipping large file: ' + path.relative(base, full));
2703
- }
2704
- }
2705
- }
2706
- return results;
2707
- }
2708
-
2709
- const files = collectFiles(outputDir, outputDir);
2710
- console.log('Collected ' + files.length + ' files');
2711
-
2712
- if (files.length === 0) {
2713
- console.error('ERROR: No files found in ' + outputDir);
2714
- process.exit(1);
2715
- }
2716
-
2717
- // Upload
2718
- const body = JSON.stringify({ slug, files });
2719
- console.log('Uploading (' + (body.length / 1024 / 1024).toFixed(1) + 'MB)...');
2720
-
2721
- const res = await fetch(PUBLISH_URL, {
2722
- method: 'POST',
2723
- headers: { 'Content-Type': 'application/json', ...(publishToken ? { 'Authorization': 'Bearer ' + publishToken } : {}) },
2724
- body,
2725
- });
2726
-
2727
- const result = await res.json();
2728
- if (result.ok) {
2729
- console.log('');
2730
- console.log('Published successfully!');
2731
- console.log('URL: ' + result.url);
2732
- console.log('Version: ' + result.version);
2733
- console.log('Files: ' + result.fileCount);
2734
- console.log('Size: ' + (result.totalSize / 1024).toFixed(1) + 'KB');
2735
- } else {
2736
- console.error('Publish failed: ' + (result.error || JSON.stringify(result)));
2737
- process.exit(1);
2738
- }
2739
- `.trim();
2740
2792
  var CONFIG_PATCH_SCRIPT = `
2741
2793
  import fs from 'fs';
2742
2794
  import path from 'path';
@@ -3010,7 +3062,7 @@ async function provisionFromFolder(localPath, projectName, token, onProgress, br
3010
3062
  await setupClaudeAuth(projectName, token, effectiveClaudeToken, progress, claudeEnv);
3011
3063
  }
3012
3064
  await deployClaudeConfig(projectName, token, projectRoot, project.framework, progress, gitInfo || null, branchName || null);
3013
- await writeRemoteFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT2);
3065
+ await writeRemoteFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT);
3014
3066
  await patchServerConfig(projectName, token, projectRoot, progress);
3015
3067
  await installSourcePlugin(projectName, token, projectRoot, project.framework, progress);
3016
3068
  try {
@@ -3258,7 +3310,7 @@ async function provisionFromGitHub(parsed, projectName, token, onProgress, githu
3258
3310
  gitWorkflowPatch.trim() + "\n"
3259
3311
  );
3260
3312
  await writeRemoteFile(`${projectRoot}/CLAUDE.md`, updatedClaudeMd);
3261
- await writeRemoteFile(`${homeDir}/.ooda/publish.mjs`, PUBLISH_SCRIPT2);
3313
+ await writeRemoteFile(`${homeDir}/.ooda/publish.mjs`, PUBLISH_SCRIPT);
3262
3314
  await patchServerConfig(projectName, token, projectRoot, progress);
3263
3315
  await installSourcePlugin(projectName, token, projectRoot, project.framework, progress);
3264
3316
  try {
@@ -3361,7 +3413,7 @@ async function provisionFromTemplate(projectName, templateId, token, onProgress,
3361
3413
  await setupClaudeAuth(projectName, token, effectiveClaudeToken, progress, claudeEnv);
3362
3414
  }
3363
3415
  await deployClaudeConfig(projectName, token, projectRoot, "vite", progress);
3364
- await writeFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT2);
3416
+ await writeFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT);
3365
3417
  try {
3366
3418
  await deployToolsViaRest(projectName, token, projectRoot, (msg) => {
3367
3419
  progress({ step: msg });
@@ -3701,14 +3753,14 @@ async function pullChangesFromProject(projectName, token, projectRoot, localProj
3701
3753
  }
3702
3754
  execSync3(`git checkout -b "${localBranch}"`, localOpts);
3703
3755
  try {
3704
- const fs7 = await import("fs");
3756
+ const fs9 = await import("fs");
3705
3757
  const os2 = await import("os");
3706
- const path10 = await import("path");
3707
- const patchFile = path10.join(os2.tmpdir(), `ooda-patch-${Date.now()}.patch`);
3708
- fs7.writeFileSync(patchFile, patchContent, "utf-8");
3758
+ const path12 = await import("path");
3759
+ const patchFile = path12.join(os2.tmpdir(), `ooda-patch-${Date.now()}.patch`);
3760
+ fs9.writeFileSync(patchFile, patchContent, "utf-8");
3709
3761
  execSync3(`git am "${patchFile}"`, localOpts);
3710
3762
  try {
3711
- fs7.unlinkSync(patchFile);
3763
+ fs9.unlinkSync(patchFile);
3712
3764
  } catch {
3713
3765
  }
3714
3766
  } catch (amError) {
@@ -4073,6 +4125,65 @@ function startServer(opts) {
4073
4125
  function buildSitesUrl(apiBase, orgId) {
4074
4126
  return `${apiBase}/org/${encodeURIComponent(orgId)}/dashboard/sites`;
4075
4127
  }
4128
+ function buildSiteUrl(apiBase, orgId, slug, sub) {
4129
+ const base = `${buildSitesUrl(apiBase, orgId)}/${encodeURIComponent(slug)}`;
4130
+ return sub ? `${base}/${sub}` : base;
4131
+ }
4132
+ var SitesApiError = class extends Error {
4133
+ constructor(status, message) {
4134
+ super(message);
4135
+ this.status = status;
4136
+ this.name = "SitesApiError";
4137
+ }
4138
+ };
4139
+ async function readJson(res) {
4140
+ const text = await res.text();
4141
+ let data;
4142
+ try {
4143
+ data = text ? JSON.parse(text) : {};
4144
+ } catch {
4145
+ data = {};
4146
+ }
4147
+ if (!res.ok) {
4148
+ const msg = data?.error || `Request failed (${res.status})`;
4149
+ throw new SitesApiError(res.status, msg);
4150
+ }
4151
+ return data;
4152
+ }
4153
+ function authHeaders(jwt, json = false) {
4154
+ return {
4155
+ Authorization: `Bearer ${jwt}`,
4156
+ ...json ? { "Content-Type": "application/json" } : {}
4157
+ };
4158
+ }
4159
+ async function listSites(auth) {
4160
+ const res = await fetch(buildSitesUrl(auth.apiBase, auth.orgId), {
4161
+ headers: authHeaders(auth.jwt)
4162
+ });
4163
+ const data = await readJson(res);
4164
+ return data.sites ?? [];
4165
+ }
4166
+ async function updateSiteAccess(auth, slug, body) {
4167
+ const res = await fetch(buildSiteUrl(auth.apiBase, auth.orgId, slug), {
4168
+ method: "PATCH",
4169
+ headers: authHeaders(auth.jwt, true),
4170
+ body: JSON.stringify(body)
4171
+ });
4172
+ return readJson(res);
4173
+ }
4174
+ async function revealSitePassword(auth, slug) {
4175
+ const res = await fetch(buildSiteUrl(auth.apiBase, auth.orgId, slug, "password"), {
4176
+ headers: authHeaders(auth.jwt)
4177
+ });
4178
+ return readJson(res);
4179
+ }
4180
+ async function deleteSite(auth, slug) {
4181
+ const res = await fetch(buildSiteUrl(auth.apiBase, auth.orgId, slug), {
4182
+ method: "DELETE",
4183
+ headers: authHeaders(auth.jwt)
4184
+ });
4185
+ return readJson(res);
4186
+ }
4076
4187
  async function fetchPublishedSiteUrl(opts) {
4077
4188
  try {
4078
4189
  const res = await fetch(buildSitesUrl(opts.apiBase, opts.orgId), {
@@ -4807,7 +4918,11 @@ async function connectAndRunClaude(projectName, apiToken, claudeToken, projectUr
4807
4918
  const orgId = getOrgId();
4808
4919
  const jwt = getAccessToken();
4809
4920
  if (orgId && jwt) {
4810
- await client.writeFile("/tmp/ooda-org.json", JSON.stringify({ orgId, jwt, projectName }));
4921
+ const apiBase = process.env.OODA_API_BASE;
4922
+ await client.writeFile(
4923
+ "/tmp/ooda-org.json",
4924
+ JSON.stringify({ orgId, jwt, projectName, ...apiBase ? { apiBase } : {} })
4925
+ );
4811
4926
  }
4812
4927
  }
4813
4928
  const tokenType = getClaudeTokenType(claudeToken);
@@ -5459,8 +5574,392 @@ async function deployFromGitHubFlow(target, apiToken, claudeToken) {
5459
5574
  }
5460
5575
  }
5461
5576
 
5577
+ // src/cli/publish.ts
5578
+ import fs8 from "fs";
5579
+ import path10 from "path";
5580
+
5581
+ // src/publish-core.ts
5582
+ import fs7 from "fs";
5583
+ import path9 from "path";
5584
+ var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
5585
+ var BUILD_DIR_CANDIDATES = [
5586
+ "dist",
5587
+ "build",
5588
+ "out",
5589
+ ".output/public",
5590
+ ".next/static"
5591
+ ];
5592
+ function detectBuildDir(projectDir) {
5593
+ for (const candidate of BUILD_DIR_CANDIDATES) {
5594
+ const full = path9.join(projectDir, candidate);
5595
+ try {
5596
+ if (fs7.existsSync(full) && fs7.statSync(full).isDirectory()) {
5597
+ return full;
5598
+ }
5599
+ } catch {
5600
+ }
5601
+ }
5602
+ return null;
5603
+ }
5604
+ function deriveSlug(rawName) {
5605
+ let slug = rawName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
5606
+ if (slug.length < 3) slug = `${slug}-site`;
5607
+ if (slug.length > 64) slug = slug.slice(0, 64);
5608
+ return slug;
5609
+ }
5610
+ function collectFiles2(dir, onSkip) {
5611
+ const results = [];
5612
+ const walk = (current) => {
5613
+ for (const entry of fs7.readdirSync(current, { withFileTypes: true })) {
5614
+ const full = path9.join(current, entry.name);
5615
+ if (entry.isDirectory()) {
5616
+ walk(full);
5617
+ } else if (entry.isFile()) {
5618
+ const rel = path9.relative(dir, full);
5619
+ if (fs7.statSync(full).size > MAX_FILE_SIZE2) {
5620
+ onSkip?.(rel);
5621
+ continue;
5622
+ }
5623
+ results.push({ path: rel, content: fs7.readFileSync(full).toString("base64") });
5624
+ }
5625
+ }
5626
+ };
5627
+ walk(dir);
5628
+ return results;
5629
+ }
5630
+ function appendSuffix(base, suffix) {
5631
+ const maxBase = 64 - suffix.length - 1;
5632
+ const trimmed = base.length > maxBase ? base.slice(0, maxBase).replace(/-+$/, "") : base;
5633
+ return `${trimmed}-${suffix}`;
5634
+ }
5635
+ function randomSlugSuffix() {
5636
+ let s = "";
5637
+ const bytes = crypto.getRandomValues(new Uint8Array(4));
5638
+ for (const b of bytes) s += (b % 36).toString(36);
5639
+ return s;
5640
+ }
5641
+ function buildPublishBody(opts) {
5642
+ return {
5643
+ slug: opts.slug,
5644
+ ...opts.projectName ? { projectName: opts.projectName } : {},
5645
+ files: opts.files
5646
+ };
5647
+ }
5648
+
5649
+ // src/cli/publish.ts
5650
+ var MAX_SLUG_ATTEMPTS = 5;
5651
+ async function publishLocalDir(opts) {
5652
+ const { projectDir, slugOverride, json } = opts;
5653
+ if (!isOrgMode()) {
5654
+ fail(json, "Publishing requires an organisation login. Run `ooda` and log in first.");
5655
+ return;
5656
+ }
5657
+ const orgId = getOrgId();
5658
+ const jwt = getAccessToken();
5659
+ if (!orgId || !jwt) {
5660
+ fail(json, "No active org session. Run `ooda` and log in first.");
5661
+ return;
5662
+ }
5663
+ const apiBase = process.env.OODA_API_BASE || "https://api.ooda.run";
5664
+ const auth = { apiBase, orgId, jwt };
5665
+ const outputDir = detectBuildDir(projectDir);
5666
+ if (!outputDir) {
5667
+ fail(
5668
+ json,
5669
+ `No build output found in ${projectDir}. Run your build command first.
5670
+ Looked for: ${BUILD_DIR_CANDIDATES.join(", ")}`
5671
+ );
5672
+ return;
5673
+ }
5674
+ const config = loadConfig(projectDir);
5675
+ const explicit = Boolean(slugOverride && slugOverride.trim());
5676
+ const persisted = !explicit && Boolean(config.slug);
5677
+ const base = deriveSlug(
5678
+ explicit ? slugOverride : config.slug || config.name || path10.basename(projectDir)
5679
+ );
5680
+ const allowSuffix = !explicit && !persisted;
5681
+ const skipped = [];
5682
+ const files = collectFiles2(outputDir, (rel) => skipped.push(rel));
5683
+ if (files.length === 0) {
5684
+ fail(json, `No files found in ${outputDir}.`);
5685
+ return;
5686
+ }
5687
+ let candidate = base;
5688
+ if (allowSuffix) {
5689
+ try {
5690
+ const taken = new Set((await listSites(auth)).map((s) => s.slug));
5691
+ if (taken.has(candidate)) candidate = appendSuffix(base, randomSlugSuffix());
5692
+ } catch {
5693
+ }
5694
+ }
5695
+ if (!json) {
5696
+ console.log(` ${c.gray}Build output: ${outputDir}${c.reset}`);
5697
+ for (const rel of skipped) console.log(` ${c.yellow}!${c.reset} ${c.gray}Skipped large file: ${rel}${c.reset}`);
5698
+ }
5699
+ let result = null;
5700
+ let finalSlug = candidate;
5701
+ let lastClashError = "";
5702
+ for (let attempt = 0; attempt < (allowSuffix ? MAX_SLUG_ATTEMPTS : 1); attempt++) {
5703
+ if (!json) console.log(` ${c.gray}Publishing ${files.length} files as ${c.reset}${c.bold}${candidate}${c.reset}${c.gray}...${c.reset}`);
5704
+ const { status, data } = await postPublish(apiBase, orgId, jwt, candidate, files);
5705
+ if (status >= 200 && status < 300 && data.ok) {
5706
+ result = data;
5707
+ finalSlug = candidate;
5708
+ break;
5709
+ }
5710
+ if (status === 403 && allowSuffix) {
5711
+ lastClashError = data.error || "slug taken";
5712
+ candidate = appendSuffix(base, randomSlugSuffix());
5713
+ continue;
5714
+ }
5715
+ if (status === 403) {
5716
+ fail(json, `Slug "${candidate}" is taken by another organisation. Choose another with --slug.`);
5717
+ return;
5718
+ }
5719
+ fail(json, data.error || `Publish failed (${status})`);
5720
+ return;
5721
+ }
5722
+ if (!result) {
5723
+ fail(json, `Couldn't find a free slug after ${MAX_SLUG_ATTEMPTS} attempts (last: ${lastClashError}).`);
5724
+ return;
5725
+ }
5726
+ const wrote = persistSlug(projectDir, finalSlug);
5727
+ const suffixed = finalSlug !== base;
5728
+ const publicUrl = result.url ? toPublicUrl(result.url) : "";
5729
+ if (json) {
5730
+ console.log(JSON.stringify({ ...result, slug: finalSlug, url: publicUrl || result.url, savedToConfig: wrote }, null, 2));
5731
+ return;
5732
+ }
5733
+ console.log(`
5734
+ ${c.green}${c.bold}\u2713${c.reset} Published`);
5735
+ console.log(` ${c.cyan}${publicUrl}${c.reset}`);
5736
+ if (result.version !== void 0) console.log(` ${c.gray}Version ${result.version} \xB7 ${result.fileCount} files \xB7 ${((result.totalSize ?? 0) / 1024).toFixed(1)}KB${c.reset}`);
5737
+ if (suffixed) console.log(` ${c.gray}Slug auto-suffixed to avoid a collision.${c.reset}`);
5738
+ if (wrote) console.log(` ${c.gray}Saved slug to ooda.json so re-publishes reuse this URL.${c.reset}`);
5739
+ console.log("");
5740
+ }
5741
+ async function postPublish(apiBase, orgId, jwt, slug, files) {
5742
+ try {
5743
+ const res = await fetch(`${apiBase}/org/${encodeURIComponent(orgId)}/publish`, {
5744
+ method: "POST",
5745
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` },
5746
+ body: JSON.stringify(buildPublishBody({ slug, files }))
5747
+ });
5748
+ const data = await res.json().catch(() => ({}));
5749
+ return { status: res.status, data };
5750
+ } catch (err) {
5751
+ return { status: 0, data: { error: `Could not reach the server: ${err instanceof Error ? err.message : String(err)}` } };
5752
+ }
5753
+ }
5754
+ function persistSlug(projectDir, slug) {
5755
+ const p = path10.join(projectDir, "ooda.json");
5756
+ let obj = {};
5757
+ if (fs8.existsSync(p)) {
5758
+ try {
5759
+ const parsed = JSON.parse(fs8.readFileSync(p, "utf-8"));
5760
+ if (parsed && typeof parsed === "object") obj = parsed;
5761
+ else return false;
5762
+ } catch {
5763
+ return false;
5764
+ }
5765
+ }
5766
+ if (obj.slug === slug) return false;
5767
+ obj.slug = slug;
5768
+ fs8.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n");
5769
+ return true;
5770
+ }
5771
+ function fail(json, message) {
5772
+ if (json) {
5773
+ console.log(JSON.stringify({ ok: false, error: message }));
5774
+ } else {
5775
+ console.log(`
5776
+ ${c.red}${message}${c.reset}
5777
+ `);
5778
+ }
5779
+ process.exitCode = 1;
5780
+ }
5781
+
5782
+ // src/cli/sites.ts
5783
+ var ACCESS_MODES = ["public", "password", "login"];
5784
+ async function runSitesCommand(args) {
5785
+ if (!isOrgMode()) return fail2(args.json, "Managing sites requires an organisation login. Run `ooda` and log in first.");
5786
+ const orgId = getOrgId();
5787
+ const jwt = getAccessToken();
5788
+ if (!orgId || !jwt) return fail2(args.json, "No active org session. Run `ooda` and log in first.");
5789
+ const apiBase = process.env.OODA_API_BASE || "https://api.ooda.run";
5790
+ const auth = { apiBase, orgId, jwt };
5791
+ const sub = args.subcommand || "list";
5792
+ try {
5793
+ switch (sub) {
5794
+ case "list":
5795
+ return await listCmd(auth, args.json);
5796
+ case "access":
5797
+ return await accessCmd(auth, args);
5798
+ case "password":
5799
+ return await passwordCmd(auth, args);
5800
+ case "delete":
5801
+ case "unpublish":
5802
+ return await deleteCmd(auth, args);
5803
+ default:
5804
+ return fail2(args.json, `Unknown sites subcommand: ${sub}. Use list | access | password | delete.`);
5805
+ }
5806
+ } catch (err) {
5807
+ if (err instanceof SitesApiError) return fail2(args.json, err.message);
5808
+ return fail2(args.json, err instanceof Error ? err.message : String(err));
5809
+ }
5810
+ }
5811
+ async function listCmd(auth, json) {
5812
+ const sites = await listSites(auth);
5813
+ if (json) {
5814
+ console.log(JSON.stringify(sites.map((s) => ({ ...s, url: toPublicUrl(s.url) })), null, 2));
5815
+ return;
5816
+ }
5817
+ if (sites.length === 0) {
5818
+ console.log(` ${c.gray}No published sites.${c.reset}
5819
+ `);
5820
+ return;
5821
+ }
5822
+ console.log("");
5823
+ for (const s of sites) {
5824
+ const own = s.isOwn ? `${c.green}*${c.reset}` : " ";
5825
+ console.log(` ${own} ${c.bold}${s.slug}${c.reset} ${c.gray}${s.effectiveMode}${c.reset}`);
5826
+ console.log(` ${c.cyan}${toPublicUrl(s.url)}${c.reset} ${c.gray}${s.publishedByName}${c.reset}`);
5827
+ }
5828
+ console.log("");
5829
+ }
5830
+ async function accessCmd(auth, args) {
5831
+ if (!args.slug) return fail2(args.json, "Usage: ooda sites access <slug> --mode <public|password|login>");
5832
+ const body = {};
5833
+ if (args.clearMode) {
5834
+ body.accessMode = null;
5835
+ } else if (args.mode !== void 0) {
5836
+ if (!ACCESS_MODES.includes(args.mode)) {
5837
+ return fail2(args.json, `Invalid --mode. Must be one of: ${ACCESS_MODES.join(", ")}`);
5838
+ }
5839
+ body.accessMode = args.mode;
5840
+ }
5841
+ if (args.clearPassword) {
5842
+ body.password = null;
5843
+ } else if (args.password !== void 0) {
5844
+ body.password = args.password;
5845
+ }
5846
+ if (Object.keys(body).length === 0) {
5847
+ return fail2(args.json, "Nothing to change. Pass --mode, --password, --clear-password, or --clear-mode.");
5848
+ }
5849
+ const result = await updateSiteAccess(auth, args.slug, body);
5850
+ if (args.json) {
5851
+ console.log(JSON.stringify(result, null, 2));
5852
+ return;
5853
+ }
5854
+ console.log(`
5855
+ ${c.green}${c.bold}\u2713${c.reset} ${c.bold}${args.slug}${c.reset} \u2192 ${c.cyan}${result.effectiveMode}${c.reset}${c.gray}${result.accessMode === null ? " (inherits org default)" : ""}${c.reset}
5856
+ `);
5857
+ }
5858
+ async function passwordCmd(auth, args) {
5859
+ if (!args.slug) return fail2(args.json, "Usage: ooda sites password <slug>");
5860
+ const result = await revealSitePassword(auth, args.slug);
5861
+ if (args.json) {
5862
+ console.log(JSON.stringify(result, null, 2));
5863
+ return;
5864
+ }
5865
+ if (!result.password) {
5866
+ console.log(`
5867
+ ${c.gray}No password set for ${args.slug} (not password-protected).${c.reset}
5868
+ `);
5869
+ return;
5870
+ }
5871
+ console.log(`
5872
+ ${c.bold}${args.slug}${c.reset} password: ${c.cyan}${result.password}${c.reset} ${c.gray}(source: ${result.source})${c.reset}
5873
+ `);
5874
+ }
5875
+ async function deleteCmd(auth, args) {
5876
+ if (!args.slug) return fail2(args.json, "Usage: ooda sites delete <slug>");
5877
+ await deleteSite(auth, args.slug);
5878
+ if (args.json) {
5879
+ console.log(JSON.stringify({ ok: true, slug: args.slug }));
5880
+ return;
5881
+ }
5882
+ console.log(`
5883
+ ${c.green}${c.bold}\u2713${c.reset} Unpublished ${c.bold}${args.slug}${c.reset}
5884
+ `);
5885
+ }
5886
+ function fail2(json, message) {
5887
+ if (json) {
5888
+ console.log(JSON.stringify({ ok: false, error: message }));
5889
+ } else {
5890
+ console.log(`
5891
+ ${c.red}${message}${c.reset}
5892
+ `);
5893
+ }
5894
+ process.exitCode = 1;
5895
+ }
5896
+
5897
+ // src/cli/help.ts
5898
+ function buildHelpText(version) {
5899
+ return `ooda v${version} \u2014 Cloud dev environments for Claude Code
5900
+
5901
+ Usage:
5902
+ ooda [command] [options]
5903
+
5904
+ Commands:
5905
+ (no command) Open the interactive project menu
5906
+ login Sign in with an email code (password fallback)
5907
+ whoami Show the current session (exits non-zero if none)
5908
+ list, ls List your projects
5909
+ connect <project> Connect to a project and run Claude (interactive)
5910
+ deploy [path|github-url] Deploy a local folder or GitHub repo as a project
5911
+ publish [path] Publish a built static site to {slug}-p.ooda.run
5912
+ sites [list] List your org's published sites
5913
+ sites access <slug> Change a published site's access policy
5914
+ sites password <slug> Reveal a site's effective password
5915
+ sites delete <slug> Unpublish a site
5916
+ help, --help, -h Show this help
5917
+
5918
+ login
5919
+ Passwordless sign-in via a 6-digit email code. No flags \u2192 interactive (with a
5920
+ password fallback). Flag mode (for agents/CI):
5921
+ --email <e> Send a login code to this email
5922
+ --email <e> --code <c> Verify the code and save the session
5923
+ --org <id> Choose the org when the account has several
5924
+ --json Machine-readable JSON output
5925
+
5926
+ publish [path]
5927
+ Publishes an already-built static site (no build is run). Looks for a build
5928
+ output dir (dist, build, out, .output/public, .next/static) in [path] (default:
5929
+ current dir). Requires an org login.
5930
+ --slug <slug> Override the slug (default: ooda.json name, else folder name)
5931
+ --json Machine-readable JSON output
5932
+
5933
+ sites access <slug>
5934
+ --mode <public|password|login> Set the access mode
5935
+ --password <pw> Set a per-site password (use with --mode password)
5936
+ --clear-password Remove the per-site password
5937
+ --clear-mode Clear the override (inherit the org default)
5938
+ --json Machine-readable JSON output
5939
+
5940
+ sites list | password | delete
5941
+ --json Machine-readable JSON output
5942
+
5943
+ Global:
5944
+ --port <n> Dashboard port for the interactive menu (default 4444)
5945
+
5946
+ Headless auth (for agents/CI):
5947
+ Set OODA_ACCESS_TOKEN and OODA_ORG_ID to skip interactive login. publish and
5948
+ sites then run fully non-interactively and exit 0 on success, non-zero on error.
5949
+
5950
+ Local dev:
5951
+ Set OODA_API_BASE (server) and OODA_LOADER_BASE (loader) to target a local
5952
+ stack instead of production.
5953
+
5954
+ Examples:
5955
+ ooda publish ./dist --slug my-app
5956
+ ooda sites list --json
5957
+ ooda sites access my-app --mode password --password hunter2
5958
+ ooda sites delete my-app --json`;
5959
+ }
5960
+
5462
5961
  // src/cli/index.ts
5463
- var CLI_VERSION = "0.1.13";
5962
+ var CLI_VERSION = "0.1.15";
5464
5963
  function formatMutationError(result) {
5465
5964
  const parts = [];
5466
5965
  if (result.status !== void 0) parts.push(String(result.status));
@@ -5498,27 +5997,77 @@ function waitForEventOrCancel(emitter, event) {
5498
5997
  }
5499
5998
  var DEFAULT_PORT = 4444;
5500
5999
  function parseArgs(argv) {
5501
- let port = DEFAULT_PORT;
6000
+ const rest = argv.slice(2);
6001
+ const first = rest[0];
5502
6002
  let command = "menu";
5503
- let connectTarget;
5504
- let deployTarget;
5505
- const firstArg = argv[2];
5506
- if (firstArg === "connect") {
5507
- command = "connect";
5508
- connectTarget = argv[3];
5509
- } else if (firstArg === "list" || firstArg === "ls") {
5510
- command = "list";
5511
- } else if (firstArg === "deploy") {
5512
- command = "deploy";
5513
- deployTarget = argv[3];
5514
- }
5515
- for (let i = 2; i < argv.length; i++) {
5516
- if (argv[i] === "--port" && argv[i + 1]) {
5517
- port = parseInt(argv[i + 1], 10) || DEFAULT_PORT;
6003
+ if (first === "connect") command = "connect";
6004
+ else if (first === "list" || first === "ls") command = "list";
6005
+ else if (first === "deploy") command = "deploy";
6006
+ else if (first === "publish") command = "publish";
6007
+ else if (first === "sites") command = "sites";
6008
+ else if (first === "login") command = "login";
6009
+ else if (first === "whoami") command = "whoami";
6010
+ else if (first === "help") command = "help";
6011
+ if (rest.includes("--help") || rest.includes("-h")) command = "help";
6012
+ const positionals = [];
6013
+ const flags = {};
6014
+ let port = DEFAULT_PORT;
6015
+ const startIdx = command === "menu" ? 0 : 1;
6016
+ for (let i = startIdx; i < rest.length; i++) {
6017
+ const arg = rest[i];
6018
+ if (arg === "--port" && rest[i + 1]) {
6019
+ port = parseInt(rest[i + 1], 10) || DEFAULT_PORT;
5518
6020
  i++;
5519
- }
6021
+ } else if (arg === "--slug" && rest[i + 1]) {
6022
+ flags.slug = rest[i + 1];
6023
+ i++;
6024
+ } else if (arg === "--email" && rest[i + 1]) {
6025
+ flags.email = rest[i + 1];
6026
+ i++;
6027
+ } else if (arg === "--code" && rest[i + 1]) {
6028
+ flags.code = rest[i + 1];
6029
+ i++;
6030
+ } else if (arg === "--org" && rest[i + 1]) {
6031
+ flags.org = rest[i + 1];
6032
+ i++;
6033
+ } else if (arg === "--mode" && rest[i + 1]) {
6034
+ flags.mode = rest[i + 1];
6035
+ i++;
6036
+ } else if (arg === "--password" && rest[i + 1] !== void 0) {
6037
+ flags.password = rest[i + 1];
6038
+ i++;
6039
+ } else if (arg === "--clear-password") flags.clearPassword = true;
6040
+ else if (arg === "--clear-mode") flags.clearMode = true;
6041
+ else if (arg === "--json") flags.json = true;
6042
+ else if (!arg.startsWith("--")) positionals.push(arg);
6043
+ }
6044
+ const args = { port, command, json: Boolean(flags.json) };
6045
+ if (command === "connect") {
6046
+ args.connectTarget = positionals[0];
6047
+ } else if (command === "deploy") {
6048
+ args.deployTarget = positionals[0];
6049
+ } else if (command === "publish") {
6050
+ args.publishTarget = positionals[0];
6051
+ args.publishSlug = flags.slug;
6052
+ } else if (command === "sites") {
6053
+ args.sites = {
6054
+ subcommand: positionals[0],
6055
+ slug: positionals[1],
6056
+ mode: flags.mode,
6057
+ password: flags.password,
6058
+ clearPassword: Boolean(flags.clearPassword),
6059
+ clearMode: Boolean(flags.clearMode),
6060
+ json: Boolean(flags.json)
6061
+ };
6062
+ } else if (command === "login") {
6063
+ args.login = {
6064
+ email: flags.email,
6065
+ code: flags.code,
6066
+ org: flags.org,
6067
+ json: Boolean(flags.json)
6068
+ };
5520
6069
  }
5521
- return { port, command, connectTarget, deployTarget };
6070
+ return args;
5522
6071
  }
5523
6072
  function eraseRenderedPrompt(visibleLines) {
5524
6073
  const total = visibleLines + 2;
@@ -5938,6 +6487,10 @@ async function main() {
5938
6487
  const args = parseArgs(process.argv);
5939
6488
  const cwd = process.cwd();
5940
6489
  printLogo(CLI_VERSION);
6490
+ if (args.command === "help") {
6491
+ console.log(buildHelpText(CLI_VERSION));
6492
+ process.exit(0);
6493
+ }
5941
6494
  if (args.command === "list") {
5942
6495
  await listProjectsCmd();
5943
6496
  process.exit(0);
@@ -5948,11 +6501,30 @@ async function main() {
5948
6501
  if (target && isGitHubTarget(target)) {
5949
6502
  await deployFromGitHubFlow(target, apiToken, claudeToken);
5950
6503
  } else {
5951
- const deployPath = target ? path9.resolve(target) : cwd;
6504
+ const deployPath = target ? path11.resolve(target) : cwd;
5952
6505
  await deployCurrentDir(deployPath, apiToken, claudeToken);
5953
6506
  }
5954
6507
  process.exit(0);
5955
6508
  }
6509
+ if (args.command === "publish") {
6510
+ await ensureAuth({ requireClaudeToken: false });
6511
+ const dir = args.publishTarget ? path11.resolve(args.publishTarget) : cwd;
6512
+ await publishLocalDir({ projectDir: dir, slugOverride: args.publishSlug, json: args.json });
6513
+ process.exit(process.exitCode ?? 0);
6514
+ }
6515
+ if (args.command === "sites") {
6516
+ await ensureAuth({ requireClaudeToken: false });
6517
+ await runSitesCommand(args.sites ?? {});
6518
+ process.exit(process.exitCode ?? 0);
6519
+ }
6520
+ if (args.command === "login") {
6521
+ await loginCmd(args.login ?? {});
6522
+ process.exit(process.exitCode ?? 0);
6523
+ }
6524
+ if (args.command === "whoami") {
6525
+ whoamiCmd({ json: args.json });
6526
+ process.exit(process.exitCode ?? 0);
6527
+ }
5956
6528
  if (args.command === "connect") {
5957
6529
  if (!args.connectTarget) {
5958
6530
  console.log(` ${c.red}Usage: ooda connect <project-name>${c.reset}`);