@oodarun/cli 0.1.13 → 0.1.14

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
@@ -1216,7 +1217,8 @@ async function completeJwtLogin(result) {
1216
1217
  console.log("");
1217
1218
  return "org";
1218
1219
  }
1219
- async function ensureAuth() {
1220
+ async function ensureAuth(opts = {}) {
1221
+ const requireClaudeToken = opts.requireClaudeToken !== false;
1220
1222
  let apiToken = null;
1221
1223
  const orgClaude = isOrgMode() ? getOrgClaudeConfig() : null;
1222
1224
  let claudeToken = getClaudeToken(orgClaude?.apiKeyHelper || void 0);
@@ -1235,6 +1237,9 @@ async function ensureAuth() {
1235
1237
  `);
1236
1238
  apiToken = await handleEmailPasswordAuth();
1237
1239
  }
1240
+ if (!requireClaudeToken) {
1241
+ return { apiToken, claudeToken: "", claudeSource: "" };
1242
+ }
1238
1243
  async function promptForClaudeToken() {
1239
1244
  const tokenType = await select2({
1240
1245
  message: "Claude token type:",
@@ -1928,19 +1933,27 @@ import path from 'path';
1928
1933
 
1929
1934
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
1930
1935
 
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 || '';
1936
+ // Resolve publish URL and auth. Publishing goes exclusively through the
1937
+ // authenticated org proxy (POST /org/{orgId}/publish), which records the site
1938
+ // in D1 and materializes its access-control policy. The old unauthenticated
1939
+ // loader endpoint is retired, so org credentials are REQUIRED.
1940
+ let PUBLISH_URL = '';
1941
+ let publishToken = '';
1934
1942
  let orgProjectName = '';
1935
1943
  try {
1936
1944
  const orgCreds = JSON.parse(fs.readFileSync('/tmp/ooda-org.json', 'utf-8'));
1945
+ const apiBase = orgCreds.apiBase || 'https://api.ooda.run';
1937
1946
  if (orgCreds.orgId && orgCreds.jwt) {
1938
- PUBLISH_URL = 'https://api.ooda.run/org/' + orgCreds.orgId + '/publish';
1947
+ PUBLISH_URL = apiBase + '/org/' + orgCreds.orgId + '/publish';
1939
1948
  publishToken = orgCreds.jwt;
1940
1949
  }
1941
1950
  if (orgCreds.projectName) orgProjectName = orgCreds.projectName;
1942
1951
  } catch {
1943
- // No org credentials \u2014 use default publish endpoint
1952
+ // handled below
1953
+ }
1954
+ if (!PUBLISH_URL || !publishToken) {
1955
+ console.error('ERROR: Missing ooda org credentials (/tmp/ooda-org.json). Reconnect the project with the ooda CLI to refresh credentials, then publish again.');
1956
+ process.exit(1);
1944
1957
  }
1945
1958
 
1946
1959
  const projectDir = process.argv[2] || process.cwd();
@@ -2640,103 +2653,6 @@ export default function designBrowserSource() {
2640
2653
  };
2641
2654
  }
2642
2655
  `.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
2656
  var CONFIG_PATCH_SCRIPT = `
2741
2657
  import fs from 'fs';
2742
2658
  import path from 'path';
@@ -3010,7 +2926,7 @@ async function provisionFromFolder(localPath, projectName, token, onProgress, br
3010
2926
  await setupClaudeAuth(projectName, token, effectiveClaudeToken, progress, claudeEnv);
3011
2927
  }
3012
2928
  await deployClaudeConfig(projectName, token, projectRoot, project.framework, progress, gitInfo || null, branchName || null);
3013
- await writeRemoteFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT2);
2929
+ await writeRemoteFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT);
3014
2930
  await patchServerConfig(projectName, token, projectRoot, progress);
3015
2931
  await installSourcePlugin(projectName, token, projectRoot, project.framework, progress);
3016
2932
  try {
@@ -3258,7 +3174,7 @@ async function provisionFromGitHub(parsed, projectName, token, onProgress, githu
3258
3174
  gitWorkflowPatch.trim() + "\n"
3259
3175
  );
3260
3176
  await writeRemoteFile(`${projectRoot}/CLAUDE.md`, updatedClaudeMd);
3261
- await writeRemoteFile(`${homeDir}/.ooda/publish.mjs`, PUBLISH_SCRIPT2);
3177
+ await writeRemoteFile(`${homeDir}/.ooda/publish.mjs`, PUBLISH_SCRIPT);
3262
3178
  await patchServerConfig(projectName, token, projectRoot, progress);
3263
3179
  await installSourcePlugin(projectName, token, projectRoot, project.framework, progress);
3264
3180
  try {
@@ -3361,7 +3277,7 @@ async function provisionFromTemplate(projectName, templateId, token, onProgress,
3361
3277
  await setupClaudeAuth(projectName, token, effectiveClaudeToken, progress, claudeEnv);
3362
3278
  }
3363
3279
  await deployClaudeConfig(projectName, token, projectRoot, "vite", progress);
3364
- await writeFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT2);
3280
+ await writeFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT);
3365
3281
  try {
3366
3282
  await deployToolsViaRest(projectName, token, projectRoot, (msg) => {
3367
3283
  progress({ step: msg });
@@ -3701,14 +3617,14 @@ async function pullChangesFromProject(projectName, token, projectRoot, localProj
3701
3617
  }
3702
3618
  execSync3(`git checkout -b "${localBranch}"`, localOpts);
3703
3619
  try {
3704
- const fs7 = await import("fs");
3620
+ const fs9 = await import("fs");
3705
3621
  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");
3622
+ const path12 = await import("path");
3623
+ const patchFile = path12.join(os2.tmpdir(), `ooda-patch-${Date.now()}.patch`);
3624
+ fs9.writeFileSync(patchFile, patchContent, "utf-8");
3709
3625
  execSync3(`git am "${patchFile}"`, localOpts);
3710
3626
  try {
3711
- fs7.unlinkSync(patchFile);
3627
+ fs9.unlinkSync(patchFile);
3712
3628
  } catch {
3713
3629
  }
3714
3630
  } catch (amError) {
@@ -4073,6 +3989,65 @@ function startServer(opts) {
4073
3989
  function buildSitesUrl(apiBase, orgId) {
4074
3990
  return `${apiBase}/org/${encodeURIComponent(orgId)}/dashboard/sites`;
4075
3991
  }
3992
+ function buildSiteUrl(apiBase, orgId, slug, sub) {
3993
+ const base = `${buildSitesUrl(apiBase, orgId)}/${encodeURIComponent(slug)}`;
3994
+ return sub ? `${base}/${sub}` : base;
3995
+ }
3996
+ var SitesApiError = class extends Error {
3997
+ constructor(status, message) {
3998
+ super(message);
3999
+ this.status = status;
4000
+ this.name = "SitesApiError";
4001
+ }
4002
+ };
4003
+ async function readJson(res) {
4004
+ const text = await res.text();
4005
+ let data;
4006
+ try {
4007
+ data = text ? JSON.parse(text) : {};
4008
+ } catch {
4009
+ data = {};
4010
+ }
4011
+ if (!res.ok) {
4012
+ const msg = data?.error || `Request failed (${res.status})`;
4013
+ throw new SitesApiError(res.status, msg);
4014
+ }
4015
+ return data;
4016
+ }
4017
+ function authHeaders(jwt, json = false) {
4018
+ return {
4019
+ Authorization: `Bearer ${jwt}`,
4020
+ ...json ? { "Content-Type": "application/json" } : {}
4021
+ };
4022
+ }
4023
+ async function listSites(auth) {
4024
+ const res = await fetch(buildSitesUrl(auth.apiBase, auth.orgId), {
4025
+ headers: authHeaders(auth.jwt)
4026
+ });
4027
+ const data = await readJson(res);
4028
+ return data.sites ?? [];
4029
+ }
4030
+ async function updateSiteAccess(auth, slug, body) {
4031
+ const res = await fetch(buildSiteUrl(auth.apiBase, auth.orgId, slug), {
4032
+ method: "PATCH",
4033
+ headers: authHeaders(auth.jwt, true),
4034
+ body: JSON.stringify(body)
4035
+ });
4036
+ return readJson(res);
4037
+ }
4038
+ async function revealSitePassword(auth, slug) {
4039
+ const res = await fetch(buildSiteUrl(auth.apiBase, auth.orgId, slug, "password"), {
4040
+ headers: authHeaders(auth.jwt)
4041
+ });
4042
+ return readJson(res);
4043
+ }
4044
+ async function deleteSite(auth, slug) {
4045
+ const res = await fetch(buildSiteUrl(auth.apiBase, auth.orgId, slug), {
4046
+ method: "DELETE",
4047
+ headers: authHeaders(auth.jwt)
4048
+ });
4049
+ return readJson(res);
4050
+ }
4076
4051
  async function fetchPublishedSiteUrl(opts) {
4077
4052
  try {
4078
4053
  const res = await fetch(buildSitesUrl(opts.apiBase, opts.orgId), {
@@ -4807,7 +4782,11 @@ async function connectAndRunClaude(projectName, apiToken, claudeToken, projectUr
4807
4782
  const orgId = getOrgId();
4808
4783
  const jwt = getAccessToken();
4809
4784
  if (orgId && jwt) {
4810
- await client.writeFile("/tmp/ooda-org.json", JSON.stringify({ orgId, jwt, projectName }));
4785
+ const apiBase = process.env.OODA_API_BASE;
4786
+ await client.writeFile(
4787
+ "/tmp/ooda-org.json",
4788
+ JSON.stringify({ orgId, jwt, projectName, ...apiBase ? { apiBase } : {} })
4789
+ );
4811
4790
  }
4812
4791
  }
4813
4792
  const tokenType = getClaudeTokenType(claudeToken);
@@ -5459,8 +5438,382 @@ async function deployFromGitHubFlow(target, apiToken, claudeToken) {
5459
5438
  }
5460
5439
  }
5461
5440
 
5441
+ // src/cli/publish.ts
5442
+ import fs8 from "fs";
5443
+ import path10 from "path";
5444
+
5445
+ // src/publish-core.ts
5446
+ import fs7 from "fs";
5447
+ import path9 from "path";
5448
+ var MAX_FILE_SIZE2 = 10 * 1024 * 1024;
5449
+ var BUILD_DIR_CANDIDATES = [
5450
+ "dist",
5451
+ "build",
5452
+ "out",
5453
+ ".output/public",
5454
+ ".next/static"
5455
+ ];
5456
+ function detectBuildDir(projectDir) {
5457
+ for (const candidate of BUILD_DIR_CANDIDATES) {
5458
+ const full = path9.join(projectDir, candidate);
5459
+ try {
5460
+ if (fs7.existsSync(full) && fs7.statSync(full).isDirectory()) {
5461
+ return full;
5462
+ }
5463
+ } catch {
5464
+ }
5465
+ }
5466
+ return null;
5467
+ }
5468
+ function deriveSlug(rawName) {
5469
+ let slug = rawName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
5470
+ if (slug.length < 3) slug = `${slug}-site`;
5471
+ if (slug.length > 64) slug = slug.slice(0, 64);
5472
+ return slug;
5473
+ }
5474
+ function collectFiles2(dir, onSkip) {
5475
+ const results = [];
5476
+ const walk = (current) => {
5477
+ for (const entry of fs7.readdirSync(current, { withFileTypes: true })) {
5478
+ const full = path9.join(current, entry.name);
5479
+ if (entry.isDirectory()) {
5480
+ walk(full);
5481
+ } else if (entry.isFile()) {
5482
+ const rel = path9.relative(dir, full);
5483
+ if (fs7.statSync(full).size > MAX_FILE_SIZE2) {
5484
+ onSkip?.(rel);
5485
+ continue;
5486
+ }
5487
+ results.push({ path: rel, content: fs7.readFileSync(full).toString("base64") });
5488
+ }
5489
+ }
5490
+ };
5491
+ walk(dir);
5492
+ return results;
5493
+ }
5494
+ function appendSuffix(base, suffix) {
5495
+ const maxBase = 64 - suffix.length - 1;
5496
+ const trimmed = base.length > maxBase ? base.slice(0, maxBase).replace(/-+$/, "") : base;
5497
+ return `${trimmed}-${suffix}`;
5498
+ }
5499
+ function randomSlugSuffix() {
5500
+ let s = "";
5501
+ const bytes = crypto.getRandomValues(new Uint8Array(4));
5502
+ for (const b of bytes) s += (b % 36).toString(36);
5503
+ return s;
5504
+ }
5505
+ function buildPublishBody(opts) {
5506
+ return {
5507
+ slug: opts.slug,
5508
+ ...opts.projectName ? { projectName: opts.projectName } : {},
5509
+ files: opts.files
5510
+ };
5511
+ }
5512
+
5513
+ // src/cli/publish.ts
5514
+ var MAX_SLUG_ATTEMPTS = 5;
5515
+ async function publishLocalDir(opts) {
5516
+ const { projectDir, slugOverride, json } = opts;
5517
+ if (!isOrgMode()) {
5518
+ fail(json, "Publishing requires an organisation login. Run `ooda` and log in first.");
5519
+ return;
5520
+ }
5521
+ const orgId = getOrgId();
5522
+ const jwt = getAccessToken();
5523
+ if (!orgId || !jwt) {
5524
+ fail(json, "No active org session. Run `ooda` and log in first.");
5525
+ return;
5526
+ }
5527
+ const apiBase = process.env.OODA_API_BASE || "https://api.ooda.run";
5528
+ const auth = { apiBase, orgId, jwt };
5529
+ const outputDir = detectBuildDir(projectDir);
5530
+ if (!outputDir) {
5531
+ fail(
5532
+ json,
5533
+ `No build output found in ${projectDir}. Run your build command first.
5534
+ Looked for: ${BUILD_DIR_CANDIDATES.join(", ")}`
5535
+ );
5536
+ return;
5537
+ }
5538
+ const config = loadConfig(projectDir);
5539
+ const explicit = Boolean(slugOverride && slugOverride.trim());
5540
+ const persisted = !explicit && Boolean(config.slug);
5541
+ const base = deriveSlug(
5542
+ explicit ? slugOverride : config.slug || config.name || path10.basename(projectDir)
5543
+ );
5544
+ const allowSuffix = !explicit && !persisted;
5545
+ const skipped = [];
5546
+ const files = collectFiles2(outputDir, (rel) => skipped.push(rel));
5547
+ if (files.length === 0) {
5548
+ fail(json, `No files found in ${outputDir}.`);
5549
+ return;
5550
+ }
5551
+ let candidate = base;
5552
+ if (allowSuffix) {
5553
+ try {
5554
+ const taken = new Set((await listSites(auth)).map((s) => s.slug));
5555
+ if (taken.has(candidate)) candidate = appendSuffix(base, randomSlugSuffix());
5556
+ } catch {
5557
+ }
5558
+ }
5559
+ if (!json) {
5560
+ console.log(` ${c.gray}Build output: ${outputDir}${c.reset}`);
5561
+ for (const rel of skipped) console.log(` ${c.yellow}!${c.reset} ${c.gray}Skipped large file: ${rel}${c.reset}`);
5562
+ }
5563
+ let result = null;
5564
+ let finalSlug = candidate;
5565
+ let lastClashError = "";
5566
+ for (let attempt = 0; attempt < (allowSuffix ? MAX_SLUG_ATTEMPTS : 1); attempt++) {
5567
+ if (!json) console.log(` ${c.gray}Publishing ${files.length} files as ${c.reset}${c.bold}${candidate}${c.reset}${c.gray}...${c.reset}`);
5568
+ const { status, data } = await postPublish(apiBase, orgId, jwt, candidate, files);
5569
+ if (status >= 200 && status < 300 && data.ok) {
5570
+ result = data;
5571
+ finalSlug = candidate;
5572
+ break;
5573
+ }
5574
+ if (status === 403 && allowSuffix) {
5575
+ lastClashError = data.error || "slug taken";
5576
+ candidate = appendSuffix(base, randomSlugSuffix());
5577
+ continue;
5578
+ }
5579
+ if (status === 403) {
5580
+ fail(json, `Slug "${candidate}" is taken by another organisation. Choose another with --slug.`);
5581
+ return;
5582
+ }
5583
+ fail(json, data.error || `Publish failed (${status})`);
5584
+ return;
5585
+ }
5586
+ if (!result) {
5587
+ fail(json, `Couldn't find a free slug after ${MAX_SLUG_ATTEMPTS} attempts (last: ${lastClashError}).`);
5588
+ return;
5589
+ }
5590
+ const wrote = persistSlug(projectDir, finalSlug);
5591
+ const suffixed = finalSlug !== base;
5592
+ const publicUrl = result.url ? toPublicUrl(result.url) : "";
5593
+ if (json) {
5594
+ console.log(JSON.stringify({ ...result, slug: finalSlug, url: publicUrl || result.url, savedToConfig: wrote }, null, 2));
5595
+ return;
5596
+ }
5597
+ console.log(`
5598
+ ${c.green}${c.bold}\u2713${c.reset} Published`);
5599
+ console.log(` ${c.cyan}${publicUrl}${c.reset}`);
5600
+ 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}`);
5601
+ if (suffixed) console.log(` ${c.gray}Slug auto-suffixed to avoid a collision.${c.reset}`);
5602
+ if (wrote) console.log(` ${c.gray}Saved slug to ooda.json so re-publishes reuse this URL.${c.reset}`);
5603
+ console.log("");
5604
+ }
5605
+ async function postPublish(apiBase, orgId, jwt, slug, files) {
5606
+ try {
5607
+ const res = await fetch(`${apiBase}/org/${encodeURIComponent(orgId)}/publish`, {
5608
+ method: "POST",
5609
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` },
5610
+ body: JSON.stringify(buildPublishBody({ slug, files }))
5611
+ });
5612
+ const data = await res.json().catch(() => ({}));
5613
+ return { status: res.status, data };
5614
+ } catch (err) {
5615
+ return { status: 0, data: { error: `Could not reach the server: ${err instanceof Error ? err.message : String(err)}` } };
5616
+ }
5617
+ }
5618
+ function persistSlug(projectDir, slug) {
5619
+ const p = path10.join(projectDir, "ooda.json");
5620
+ let obj = {};
5621
+ if (fs8.existsSync(p)) {
5622
+ try {
5623
+ const parsed = JSON.parse(fs8.readFileSync(p, "utf-8"));
5624
+ if (parsed && typeof parsed === "object") obj = parsed;
5625
+ else return false;
5626
+ } catch {
5627
+ return false;
5628
+ }
5629
+ }
5630
+ if (obj.slug === slug) return false;
5631
+ obj.slug = slug;
5632
+ fs8.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n");
5633
+ return true;
5634
+ }
5635
+ function fail(json, message) {
5636
+ if (json) {
5637
+ console.log(JSON.stringify({ ok: false, error: message }));
5638
+ } else {
5639
+ console.log(`
5640
+ ${c.red}${message}${c.reset}
5641
+ `);
5642
+ }
5643
+ process.exitCode = 1;
5644
+ }
5645
+
5646
+ // src/cli/sites.ts
5647
+ var ACCESS_MODES = ["public", "password", "login"];
5648
+ async function runSitesCommand(args) {
5649
+ if (!isOrgMode()) return fail2(args.json, "Managing sites requires an organisation login. Run `ooda` and log in first.");
5650
+ const orgId = getOrgId();
5651
+ const jwt = getAccessToken();
5652
+ if (!orgId || !jwt) return fail2(args.json, "No active org session. Run `ooda` and log in first.");
5653
+ const apiBase = process.env.OODA_API_BASE || "https://api.ooda.run";
5654
+ const auth = { apiBase, orgId, jwt };
5655
+ const sub = args.subcommand || "list";
5656
+ try {
5657
+ switch (sub) {
5658
+ case "list":
5659
+ return await listCmd(auth, args.json);
5660
+ case "access":
5661
+ return await accessCmd(auth, args);
5662
+ case "password":
5663
+ return await passwordCmd(auth, args);
5664
+ case "delete":
5665
+ case "unpublish":
5666
+ return await deleteCmd(auth, args);
5667
+ default:
5668
+ return fail2(args.json, `Unknown sites subcommand: ${sub}. Use list | access | password | delete.`);
5669
+ }
5670
+ } catch (err) {
5671
+ if (err instanceof SitesApiError) return fail2(args.json, err.message);
5672
+ return fail2(args.json, err instanceof Error ? err.message : String(err));
5673
+ }
5674
+ }
5675
+ async function listCmd(auth, json) {
5676
+ const sites = await listSites(auth);
5677
+ if (json) {
5678
+ console.log(JSON.stringify(sites.map((s) => ({ ...s, url: toPublicUrl(s.url) })), null, 2));
5679
+ return;
5680
+ }
5681
+ if (sites.length === 0) {
5682
+ console.log(` ${c.gray}No published sites.${c.reset}
5683
+ `);
5684
+ return;
5685
+ }
5686
+ console.log("");
5687
+ for (const s of sites) {
5688
+ const own = s.isOwn ? `${c.green}*${c.reset}` : " ";
5689
+ console.log(` ${own} ${c.bold}${s.slug}${c.reset} ${c.gray}${s.effectiveMode}${c.reset}`);
5690
+ console.log(` ${c.cyan}${toPublicUrl(s.url)}${c.reset} ${c.gray}${s.publishedByName}${c.reset}`);
5691
+ }
5692
+ console.log("");
5693
+ }
5694
+ async function accessCmd(auth, args) {
5695
+ if (!args.slug) return fail2(args.json, "Usage: ooda sites access <slug> --mode <public|password|login>");
5696
+ const body = {};
5697
+ if (args.clearMode) {
5698
+ body.accessMode = null;
5699
+ } else if (args.mode !== void 0) {
5700
+ if (!ACCESS_MODES.includes(args.mode)) {
5701
+ return fail2(args.json, `Invalid --mode. Must be one of: ${ACCESS_MODES.join(", ")}`);
5702
+ }
5703
+ body.accessMode = args.mode;
5704
+ }
5705
+ if (args.clearPassword) {
5706
+ body.password = null;
5707
+ } else if (args.password !== void 0) {
5708
+ body.password = args.password;
5709
+ }
5710
+ if (Object.keys(body).length === 0) {
5711
+ return fail2(args.json, "Nothing to change. Pass --mode, --password, --clear-password, or --clear-mode.");
5712
+ }
5713
+ const result = await updateSiteAccess(auth, args.slug, body);
5714
+ if (args.json) {
5715
+ console.log(JSON.stringify(result, null, 2));
5716
+ return;
5717
+ }
5718
+ console.log(`
5719
+ ${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}
5720
+ `);
5721
+ }
5722
+ async function passwordCmd(auth, args) {
5723
+ if (!args.slug) return fail2(args.json, "Usage: ooda sites password <slug>");
5724
+ const result = await revealSitePassword(auth, args.slug);
5725
+ if (args.json) {
5726
+ console.log(JSON.stringify(result, null, 2));
5727
+ return;
5728
+ }
5729
+ if (!result.password) {
5730
+ console.log(`
5731
+ ${c.gray}No password set for ${args.slug} (not password-protected).${c.reset}
5732
+ `);
5733
+ return;
5734
+ }
5735
+ console.log(`
5736
+ ${c.bold}${args.slug}${c.reset} password: ${c.cyan}${result.password}${c.reset} ${c.gray}(source: ${result.source})${c.reset}
5737
+ `);
5738
+ }
5739
+ async function deleteCmd(auth, args) {
5740
+ if (!args.slug) return fail2(args.json, "Usage: ooda sites delete <slug>");
5741
+ await deleteSite(auth, args.slug);
5742
+ if (args.json) {
5743
+ console.log(JSON.stringify({ ok: true, slug: args.slug }));
5744
+ return;
5745
+ }
5746
+ console.log(`
5747
+ ${c.green}${c.bold}\u2713${c.reset} Unpublished ${c.bold}${args.slug}${c.reset}
5748
+ `);
5749
+ }
5750
+ function fail2(json, message) {
5751
+ if (json) {
5752
+ console.log(JSON.stringify({ ok: false, error: message }));
5753
+ } else {
5754
+ console.log(`
5755
+ ${c.red}${message}${c.reset}
5756
+ `);
5757
+ }
5758
+ process.exitCode = 1;
5759
+ }
5760
+
5761
+ // src/cli/help.ts
5762
+ function buildHelpText(version) {
5763
+ return `ooda v${version} \u2014 Cloud dev environments for Claude Code
5764
+
5765
+ Usage:
5766
+ ooda [command] [options]
5767
+
5768
+ Commands:
5769
+ (no command) Open the interactive project menu
5770
+ list, ls List your projects
5771
+ connect <project> Connect to a project and run Claude (interactive)
5772
+ deploy [path|github-url] Deploy a local folder or GitHub repo as a project
5773
+ publish [path] Publish a built static site to {slug}-p.ooda.run
5774
+ sites [list] List your org's published sites
5775
+ sites access <slug> Change a published site's access policy
5776
+ sites password <slug> Reveal a site's effective password
5777
+ sites delete <slug> Unpublish a site
5778
+ help, --help, -h Show this help
5779
+
5780
+ publish [path]
5781
+ Publishes an already-built static site (no build is run). Looks for a build
5782
+ output dir (dist, build, out, .output/public, .next/static) in [path] (default:
5783
+ current dir). Requires an org login.
5784
+ --slug <slug> Override the slug (default: ooda.json name, else folder name)
5785
+ --json Machine-readable JSON output
5786
+
5787
+ sites access <slug>
5788
+ --mode <public|password|login> Set the access mode
5789
+ --password <pw> Set a per-site password (use with --mode password)
5790
+ --clear-password Remove the per-site password
5791
+ --clear-mode Clear the override (inherit the org default)
5792
+ --json Machine-readable JSON output
5793
+
5794
+ sites list | password | delete
5795
+ --json Machine-readable JSON output
5796
+
5797
+ Global:
5798
+ --port <n> Dashboard port for the interactive menu (default 4444)
5799
+
5800
+ Headless auth (for agents/CI):
5801
+ Set OODA_ACCESS_TOKEN and OODA_ORG_ID to skip interactive login. publish and
5802
+ sites then run fully non-interactively and exit 0 on success, non-zero on error.
5803
+
5804
+ Local dev:
5805
+ Set OODA_API_BASE (server) and OODA_LOADER_BASE (loader) to target a local
5806
+ stack instead of production.
5807
+
5808
+ Examples:
5809
+ ooda publish ./dist --slug my-app
5810
+ ooda sites list --json
5811
+ ooda sites access my-app --mode password --password hunter2
5812
+ ooda sites delete my-app --json`;
5813
+ }
5814
+
5462
5815
  // src/cli/index.ts
5463
- var CLI_VERSION = "0.1.13";
5816
+ var CLI_VERSION = "0.1.14";
5464
5817
  function formatMutationError(result) {
5465
5818
  const parts = [];
5466
5819
  if (result.status !== void 0) parts.push(String(result.status));
@@ -5498,27 +5851,59 @@ function waitForEventOrCancel(emitter, event) {
5498
5851
  }
5499
5852
  var DEFAULT_PORT = 4444;
5500
5853
  function parseArgs(argv) {
5501
- let port = DEFAULT_PORT;
5854
+ const rest = argv.slice(2);
5855
+ const first = rest[0];
5502
5856
  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;
5857
+ if (first === "connect") command = "connect";
5858
+ else if (first === "list" || first === "ls") command = "list";
5859
+ else if (first === "deploy") command = "deploy";
5860
+ else if (first === "publish") command = "publish";
5861
+ else if (first === "sites") command = "sites";
5862
+ else if (first === "help") command = "help";
5863
+ if (rest.includes("--help") || rest.includes("-h")) command = "help";
5864
+ const positionals = [];
5865
+ const flags = {};
5866
+ let port = DEFAULT_PORT;
5867
+ const startIdx = command === "menu" ? 0 : 1;
5868
+ for (let i = startIdx; i < rest.length; i++) {
5869
+ const arg = rest[i];
5870
+ if (arg === "--port" && rest[i + 1]) {
5871
+ port = parseInt(rest[i + 1], 10) || DEFAULT_PORT;
5518
5872
  i++;
5519
- }
5873
+ } else if (arg === "--slug" && rest[i + 1]) {
5874
+ flags.slug = rest[i + 1];
5875
+ i++;
5876
+ } else if (arg === "--mode" && rest[i + 1]) {
5877
+ flags.mode = rest[i + 1];
5878
+ i++;
5879
+ } else if (arg === "--password" && rest[i + 1] !== void 0) {
5880
+ flags.password = rest[i + 1];
5881
+ i++;
5882
+ } else if (arg === "--clear-password") flags.clearPassword = true;
5883
+ else if (arg === "--clear-mode") flags.clearMode = true;
5884
+ else if (arg === "--json") flags.json = true;
5885
+ else if (!arg.startsWith("--")) positionals.push(arg);
5886
+ }
5887
+ const args = { port, command, json: Boolean(flags.json) };
5888
+ if (command === "connect") {
5889
+ args.connectTarget = positionals[0];
5890
+ } else if (command === "deploy") {
5891
+ args.deployTarget = positionals[0];
5892
+ } else if (command === "publish") {
5893
+ args.publishTarget = positionals[0];
5894
+ args.publishSlug = flags.slug;
5895
+ } else if (command === "sites") {
5896
+ args.sites = {
5897
+ subcommand: positionals[0],
5898
+ slug: positionals[1],
5899
+ mode: flags.mode,
5900
+ password: flags.password,
5901
+ clearPassword: Boolean(flags.clearPassword),
5902
+ clearMode: Boolean(flags.clearMode),
5903
+ json: Boolean(flags.json)
5904
+ };
5520
5905
  }
5521
- return { port, command, connectTarget, deployTarget };
5906
+ return args;
5522
5907
  }
5523
5908
  function eraseRenderedPrompt(visibleLines) {
5524
5909
  const total = visibleLines + 2;
@@ -5938,6 +6323,10 @@ async function main() {
5938
6323
  const args = parseArgs(process.argv);
5939
6324
  const cwd = process.cwd();
5940
6325
  printLogo(CLI_VERSION);
6326
+ if (args.command === "help") {
6327
+ console.log(buildHelpText(CLI_VERSION));
6328
+ process.exit(0);
6329
+ }
5941
6330
  if (args.command === "list") {
5942
6331
  await listProjectsCmd();
5943
6332
  process.exit(0);
@@ -5948,11 +6337,22 @@ async function main() {
5948
6337
  if (target && isGitHubTarget(target)) {
5949
6338
  await deployFromGitHubFlow(target, apiToken, claudeToken);
5950
6339
  } else {
5951
- const deployPath = target ? path9.resolve(target) : cwd;
6340
+ const deployPath = target ? path11.resolve(target) : cwd;
5952
6341
  await deployCurrentDir(deployPath, apiToken, claudeToken);
5953
6342
  }
5954
6343
  process.exit(0);
5955
6344
  }
6345
+ if (args.command === "publish") {
6346
+ await ensureAuth({ requireClaudeToken: false });
6347
+ const dir = args.publishTarget ? path11.resolve(args.publishTarget) : cwd;
6348
+ await publishLocalDir({ projectDir: dir, slugOverride: args.publishSlug, json: args.json });
6349
+ process.exit(process.exitCode ?? 0);
6350
+ }
6351
+ if (args.command === "sites") {
6352
+ await ensureAuth({ requireClaudeToken: false });
6353
+ await runSitesCommand(args.sites ?? {});
6354
+ process.exit(process.exitCode ?? 0);
6355
+ }
5956
6356
  if (args.command === "connect") {
5957
6357
  if (!args.connectTarget) {
5958
6358
  console.log(` ${c.red}Usage: ooda connect <project-name>${c.reset}`);