@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/README.md +30 -11
- package/dashboard-dist/assets/AdminApp-eGVdfeZo.js +6 -0
- package/dashboard-dist/assets/AdminLogin-DPuKez-W.js +1 -0
- package/dashboard-dist/assets/{App-C_nY6S8g.js → App-Dc5SAGN3.js} +1 -1
- package/dashboard-dist/assets/{InternalDashboard-CGGYXACm.js → InternalDashboard-oJnYMgak.js} +1 -1
- package/dashboard-dist/assets/SiteAuth-zqMzPUk6.js +1 -0
- package/dashboard-dist/assets/api--5oZGh0X.js +1 -0
- package/dashboard-dist/assets/index-CrjhrzUZ.css +1 -0
- package/dashboard-dist/assets/index-e3u3oOC5.js +50 -0
- package/dashboard-dist/index.html +2 -2
- package/dist/cli.js +533 -133
- package/package.json +2 -1
- package/dashboard-dist/assets/AdminApp-B0pStzVl.js +0 -6
- package/dashboard-dist/assets/api-DbuItvnH.js +0 -1
- package/dashboard-dist/assets/index-5I_ipmZc.js +0 -50
- package/dashboard-dist/assets/index-lADDHY9o.css +0 -1
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
|
|
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
|
|
1932
|
-
|
|
1933
|
-
|
|
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 = '
|
|
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
|
-
//
|
|
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",
|
|
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`,
|
|
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",
|
|
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
|
|
3620
|
+
const fs9 = await import("fs");
|
|
3705
3621
|
const os2 = await import("os");
|
|
3706
|
-
const
|
|
3707
|
-
const patchFile =
|
|
3708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
5854
|
+
const rest = argv.slice(2);
|
|
5855
|
+
const first = rest[0];
|
|
5502
5856
|
let command = "menu";
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
if (
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
if (
|
|
5517
|
-
port = parseInt(
|
|
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
|
|
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 ?
|
|
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}`);
|