@oodarun/cli 0.1.12 → 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 +657 -160
- 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
|
|
@@ -1036,22 +1037,13 @@ function promptToken(question) {
|
|
|
1036
1037
|
process.stdin.on("data", onData);
|
|
1037
1038
|
});
|
|
1038
1039
|
}
|
|
1039
|
-
async function projectSubMenu(project, isOwner = true) {
|
|
1040
|
+
async function projectSubMenu(project, isOwner = true, publishedUrl = null) {
|
|
1040
1041
|
const icon = statusIcon(project.status);
|
|
1041
1042
|
const ownerLabel = project.owner ? ` ${c.gray}(${project.owner})${c.reset}` : "";
|
|
1042
1043
|
if (!isOwner) {
|
|
1043
|
-
let publishedUrl2 = null;
|
|
1044
|
-
try {
|
|
1045
|
-
const res = await fetch(`https://ooda.run/api/publish/${project.name}`);
|
|
1046
|
-
if (res.ok) {
|
|
1047
|
-
const info = await res.json();
|
|
1048
|
-
publishedUrl2 = info.latestUrl;
|
|
1049
|
-
}
|
|
1050
|
-
} catch {
|
|
1051
|
-
}
|
|
1052
1044
|
const choices2 = [];
|
|
1053
|
-
if (
|
|
1054
|
-
choices2.push({ name: `Open published site ${c.gray}${
|
|
1045
|
+
if (publishedUrl) {
|
|
1046
|
+
choices2.push({ name: `Open published site ${c.gray}${publishedUrl}${c.reset}`, value: "open-published" });
|
|
1055
1047
|
}
|
|
1056
1048
|
choices2.push(
|
|
1057
1049
|
new Separator(` ${c.gray}You don't have access to manage this project${c.reset}`),
|
|
@@ -1068,15 +1060,6 @@ async function projectSubMenu(project, isOwner = true) {
|
|
|
1068
1060
|
throw err;
|
|
1069
1061
|
}
|
|
1070
1062
|
}
|
|
1071
|
-
let publishedUrl = null;
|
|
1072
|
-
try {
|
|
1073
|
-
const res = await fetch(`https://ooda.run/api/publish/${project.name}`);
|
|
1074
|
-
if (res.ok) {
|
|
1075
|
-
const info = await res.json();
|
|
1076
|
-
publishedUrl = info.latestUrl;
|
|
1077
|
-
}
|
|
1078
|
-
} catch {
|
|
1079
|
-
}
|
|
1080
1063
|
const choices = [
|
|
1081
1064
|
{ name: "Connect (launch Claude)", value: "connect" },
|
|
1082
1065
|
{ name: "Re-upload files from local folder", value: "reupload" }
|
|
@@ -1234,7 +1217,8 @@ async function completeJwtLogin(result) {
|
|
|
1234
1217
|
console.log("");
|
|
1235
1218
|
return "org";
|
|
1236
1219
|
}
|
|
1237
|
-
async function ensureAuth() {
|
|
1220
|
+
async function ensureAuth(opts = {}) {
|
|
1221
|
+
const requireClaudeToken = opts.requireClaudeToken !== false;
|
|
1238
1222
|
let apiToken = null;
|
|
1239
1223
|
const orgClaude = isOrgMode() ? getOrgClaudeConfig() : null;
|
|
1240
1224
|
let claudeToken = getClaudeToken(orgClaude?.apiKeyHelper || void 0);
|
|
@@ -1253,6 +1237,9 @@ async function ensureAuth() {
|
|
|
1253
1237
|
`);
|
|
1254
1238
|
apiToken = await handleEmailPasswordAuth();
|
|
1255
1239
|
}
|
|
1240
|
+
if (!requireClaudeToken) {
|
|
1241
|
+
return { apiToken, claudeToken: "", claudeSource: "" };
|
|
1242
|
+
}
|
|
1256
1243
|
async function promptForClaudeToken() {
|
|
1257
1244
|
const tokenType = await select2({
|
|
1258
1245
|
message: "Claude token type:",
|
|
@@ -1946,19 +1933,27 @@ import path from 'path';
|
|
|
1946
1933
|
|
|
1947
1934
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
1948
1935
|
|
|
1949
|
-
// Resolve publish URL and auth
|
|
1950
|
-
|
|
1951
|
-
|
|
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 = '';
|
|
1952
1942
|
let orgProjectName = '';
|
|
1953
1943
|
try {
|
|
1954
1944
|
const orgCreds = JSON.parse(fs.readFileSync('/tmp/ooda-org.json', 'utf-8'));
|
|
1945
|
+
const apiBase = orgCreds.apiBase || 'https://api.ooda.run';
|
|
1955
1946
|
if (orgCreds.orgId && orgCreds.jwt) {
|
|
1956
|
-
PUBLISH_URL = '
|
|
1947
|
+
PUBLISH_URL = apiBase + '/org/' + orgCreds.orgId + '/publish';
|
|
1957
1948
|
publishToken = orgCreds.jwt;
|
|
1958
1949
|
}
|
|
1959
1950
|
if (orgCreds.projectName) orgProjectName = orgCreds.projectName;
|
|
1960
1951
|
} catch {
|
|
1961
|
-
//
|
|
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);
|
|
1962
1957
|
}
|
|
1963
1958
|
|
|
1964
1959
|
const projectDir = process.argv[2] || process.cwd();
|
|
@@ -2658,103 +2653,6 @@ export default function designBrowserSource() {
|
|
|
2658
2653
|
};
|
|
2659
2654
|
}
|
|
2660
2655
|
`.trim();
|
|
2661
|
-
var PUBLISH_SCRIPT2 = `
|
|
2662
|
-
import fs from 'fs';
|
|
2663
|
-
import path from 'path';
|
|
2664
|
-
import { execSync } from 'child_process';
|
|
2665
|
-
|
|
2666
|
-
const PUBLISH_URL = 'https://ooda.run/api/publish';
|
|
2667
|
-
const publishToken = process.env.PUBLISH_TOKEN || '';
|
|
2668
|
-
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
2669
|
-
|
|
2670
|
-
// Find the project directory (first arg or cwd)
|
|
2671
|
-
const projectDir = process.argv[2] || process.cwd();
|
|
2672
|
-
|
|
2673
|
-
// Detect build output directory
|
|
2674
|
-
const candidates = ['dist', 'build', 'out', '.output/public', '.next/static'];
|
|
2675
|
-
let outputDir = null;
|
|
2676
|
-
for (const c of candidates) {
|
|
2677
|
-
const p = path.join(projectDir, c);
|
|
2678
|
-
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
|
|
2679
|
-
outputDir = p;
|
|
2680
|
-
break;
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
if (!outputDir) {
|
|
2685
|
-
console.error('ERROR: No build output directory found. Run your build command first.');
|
|
2686
|
-
console.error('Looked for: ' + candidates.join(', '));
|
|
2687
|
-
process.exit(1);
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
|
-
console.log('Found build output: ' + outputDir);
|
|
2691
|
-
|
|
2692
|
-
// Derive slug from project name (hostname)
|
|
2693
|
-
let slug;
|
|
2694
|
-
try {
|
|
2695
|
-
slug = execSync('hostname', { encoding: 'utf-8' }).trim();
|
|
2696
|
-
} catch {
|
|
2697
|
-
slug = path.basename(projectDir);
|
|
2698
|
-
}
|
|
2699
|
-
// Clean slug to match requirements
|
|
2700
|
-
slug = slug.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
2701
|
-
if (slug.length < 3) slug = slug + '-site';
|
|
2702
|
-
if (slug.length > 64) slug = slug.slice(0, 64);
|
|
2703
|
-
|
|
2704
|
-
console.log('Publishing as: ' + slug);
|
|
2705
|
-
|
|
2706
|
-
// Collect all files recursively
|
|
2707
|
-
function collectFiles(dir, base) {
|
|
2708
|
-
const results = [];
|
|
2709
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2710
|
-
const full = path.join(dir, entry.name);
|
|
2711
|
-
if (entry.isDirectory()) {
|
|
2712
|
-
results.push(...collectFiles(full, base));
|
|
2713
|
-
} else if (entry.isFile()) {
|
|
2714
|
-
const stat = fs.statSync(full);
|
|
2715
|
-
if (stat.size <= MAX_FILE_SIZE) {
|
|
2716
|
-
const rel = path.relative(base, full);
|
|
2717
|
-
const content = fs.readFileSync(full).toString('base64');
|
|
2718
|
-
results.push({ path: rel, content });
|
|
2719
|
-
} else {
|
|
2720
|
-
console.log('Skipping large file: ' + path.relative(base, full));
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
return results;
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
const files = collectFiles(outputDir, outputDir);
|
|
2728
|
-
console.log('Collected ' + files.length + ' files');
|
|
2729
|
-
|
|
2730
|
-
if (files.length === 0) {
|
|
2731
|
-
console.error('ERROR: No files found in ' + outputDir);
|
|
2732
|
-
process.exit(1);
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
// Upload
|
|
2736
|
-
const body = JSON.stringify({ slug, files });
|
|
2737
|
-
console.log('Uploading (' + (body.length / 1024 / 1024).toFixed(1) + 'MB)...');
|
|
2738
|
-
|
|
2739
|
-
const res = await fetch(PUBLISH_URL, {
|
|
2740
|
-
method: 'POST',
|
|
2741
|
-
headers: { 'Content-Type': 'application/json', ...(publishToken ? { 'Authorization': 'Bearer ' + publishToken } : {}) },
|
|
2742
|
-
body,
|
|
2743
|
-
});
|
|
2744
|
-
|
|
2745
|
-
const result = await res.json();
|
|
2746
|
-
if (result.ok) {
|
|
2747
|
-
console.log('');
|
|
2748
|
-
console.log('Published successfully!');
|
|
2749
|
-
console.log('URL: ' + result.url);
|
|
2750
|
-
console.log('Version: ' + result.version);
|
|
2751
|
-
console.log('Files: ' + result.fileCount);
|
|
2752
|
-
console.log('Size: ' + (result.totalSize / 1024).toFixed(1) + 'KB');
|
|
2753
|
-
} else {
|
|
2754
|
-
console.error('Publish failed: ' + (result.error || JSON.stringify(result)));
|
|
2755
|
-
process.exit(1);
|
|
2756
|
-
}
|
|
2757
|
-
`.trim();
|
|
2758
2656
|
var CONFIG_PATCH_SCRIPT = `
|
|
2759
2657
|
import fs from 'fs';
|
|
2760
2658
|
import path from 'path';
|
|
@@ -3028,7 +2926,7 @@ async function provisionFromFolder(localPath, projectName, token, onProgress, br
|
|
|
3028
2926
|
await setupClaudeAuth(projectName, token, effectiveClaudeToken, progress, claudeEnv);
|
|
3029
2927
|
}
|
|
3030
2928
|
await deployClaudeConfig(projectName, token, projectRoot, project.framework, progress, gitInfo || null, branchName || null);
|
|
3031
|
-
await writeRemoteFile("/home/user/.ooda/publish.mjs",
|
|
2929
|
+
await writeRemoteFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT);
|
|
3032
2930
|
await patchServerConfig(projectName, token, projectRoot, progress);
|
|
3033
2931
|
await installSourcePlugin(projectName, token, projectRoot, project.framework, progress);
|
|
3034
2932
|
try {
|
|
@@ -3276,7 +3174,7 @@ async function provisionFromGitHub(parsed, projectName, token, onProgress, githu
|
|
|
3276
3174
|
gitWorkflowPatch.trim() + "\n"
|
|
3277
3175
|
);
|
|
3278
3176
|
await writeRemoteFile(`${projectRoot}/CLAUDE.md`, updatedClaudeMd);
|
|
3279
|
-
await writeRemoteFile(`${homeDir}/.ooda/publish.mjs`,
|
|
3177
|
+
await writeRemoteFile(`${homeDir}/.ooda/publish.mjs`, PUBLISH_SCRIPT);
|
|
3280
3178
|
await patchServerConfig(projectName, token, projectRoot, progress);
|
|
3281
3179
|
await installSourcePlugin(projectName, token, projectRoot, project.framework, progress);
|
|
3282
3180
|
try {
|
|
@@ -3379,7 +3277,7 @@ async function provisionFromTemplate(projectName, templateId, token, onProgress,
|
|
|
3379
3277
|
await setupClaudeAuth(projectName, token, effectiveClaudeToken, progress, claudeEnv);
|
|
3380
3278
|
}
|
|
3381
3279
|
await deployClaudeConfig(projectName, token, projectRoot, "vite", progress);
|
|
3382
|
-
await writeFile("/home/user/.ooda/publish.mjs",
|
|
3280
|
+
await writeFile("/home/user/.ooda/publish.mjs", PUBLISH_SCRIPT);
|
|
3383
3281
|
try {
|
|
3384
3282
|
await deployToolsViaRest(projectName, token, projectRoot, (msg) => {
|
|
3385
3283
|
progress({ step: msg });
|
|
@@ -3719,14 +3617,14 @@ async function pullChangesFromProject(projectName, token, projectRoot, localProj
|
|
|
3719
3617
|
}
|
|
3720
3618
|
execSync3(`git checkout -b "${localBranch}"`, localOpts);
|
|
3721
3619
|
try {
|
|
3722
|
-
const
|
|
3620
|
+
const fs9 = await import("fs");
|
|
3723
3621
|
const os2 = await import("os");
|
|
3724
|
-
const
|
|
3725
|
-
const patchFile =
|
|
3726
|
-
|
|
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");
|
|
3727
3625
|
execSync3(`git am "${patchFile}"`, localOpts);
|
|
3728
3626
|
try {
|
|
3729
|
-
|
|
3627
|
+
fs9.unlinkSync(patchFile);
|
|
3730
3628
|
} catch {
|
|
3731
3629
|
}
|
|
3732
3630
|
} catch (amError) {
|
|
@@ -4087,6 +3985,85 @@ function startServer(opts) {
|
|
|
4087
3985
|
});
|
|
4088
3986
|
}
|
|
4089
3987
|
|
|
3988
|
+
// src/cli/sites-client.ts
|
|
3989
|
+
function buildSitesUrl(apiBase, orgId) {
|
|
3990
|
+
return `${apiBase}/org/${encodeURIComponent(orgId)}/dashboard/sites`;
|
|
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
|
+
}
|
|
4051
|
+
async function fetchPublishedSiteUrl(opts) {
|
|
4052
|
+
try {
|
|
4053
|
+
const res = await fetch(buildSitesUrl(opts.apiBase, opts.orgId), {
|
|
4054
|
+
headers: { Authorization: `Bearer ${opts.jwt}` }
|
|
4055
|
+
});
|
|
4056
|
+
if (!res.ok) return null;
|
|
4057
|
+
const data = await res.json();
|
|
4058
|
+
const match = (data.sites ?? []).find(
|
|
4059
|
+
(s) => s.projectName === opts.projectName || s.slug === opts.projectName
|
|
4060
|
+
);
|
|
4061
|
+
return match?.url ?? null;
|
|
4062
|
+
} catch {
|
|
4063
|
+
return null;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4090
4067
|
// src/urls.ts
|
|
4091
4068
|
function parseLoaderBase() {
|
|
4092
4069
|
const raw = process.env.OODA_LOADER_BASE;
|
|
@@ -4135,10 +4112,9 @@ function buildSelectorChoices(state) {
|
|
|
4135
4112
|
} else {
|
|
4136
4113
|
projectChoices = state.projects.map((p) => {
|
|
4137
4114
|
const icon = statusIcon(p.status);
|
|
4138
|
-
const url = p.url ? ` ${c.gray}${toPublicUrl(p.url)}${c.reset}` : "";
|
|
4139
4115
|
const owner = p.owner ? ` ${c.gray}(${p.owner})${c.reset}` : "";
|
|
4140
4116
|
return {
|
|
4141
|
-
name: ` ${p.name}${owner} ${icon} ${p.status}
|
|
4117
|
+
name: ` ${p.name}${owner} ${icon} ${p.status}`,
|
|
4142
4118
|
value: { kind: "project", project: p }
|
|
4143
4119
|
};
|
|
4144
4120
|
});
|
|
@@ -4644,8 +4620,42 @@ function mergeOnboardingConfig(existing, projectRoot) {
|
|
|
4644
4620
|
return d;
|
|
4645
4621
|
}
|
|
4646
4622
|
|
|
4623
|
+
// src/cli/backup-client.ts
|
|
4624
|
+
function buildBackupUrl(apiBase, orgId, projectName) {
|
|
4625
|
+
return `${apiBase}/org/${encodeURIComponent(orgId)}/project/${encodeURIComponent(projectName)}/backup`;
|
|
4626
|
+
}
|
|
4627
|
+
function buildRestoreUrl(apiBase, orgId, projectName) {
|
|
4628
|
+
return `${apiBase}/org/${encodeURIComponent(orgId)}/project/${encodeURIComponent(projectName)}/restore`;
|
|
4629
|
+
}
|
|
4630
|
+
async function restoreProject(opts) {
|
|
4631
|
+
try {
|
|
4632
|
+
const res = await fetch(buildRestoreUrl(opts.apiBase, opts.orgId, opts.projectName), {
|
|
4633
|
+
method: "POST",
|
|
4634
|
+
headers: { Authorization: `Bearer ${opts.jwt}` }
|
|
4635
|
+
});
|
|
4636
|
+
if (!res.ok) return { restored: false, reason: `http-${res.status}` };
|
|
4637
|
+
const data = await res.json();
|
|
4638
|
+
return { restored: Boolean(data.restored), reason: data.reason, dir: data.dir };
|
|
4639
|
+
} catch {
|
|
4640
|
+
return { restored: false, reason: "network-error" };
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
async function backupProject(opts) {
|
|
4644
|
+
try {
|
|
4645
|
+
const res = await fetch(buildBackupUrl(opts.apiBase, opts.orgId, opts.projectName), {
|
|
4646
|
+
method: "POST",
|
|
4647
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${opts.jwt}` },
|
|
4648
|
+
body: JSON.stringify({ projectRoot: opts.projectRoot })
|
|
4649
|
+
});
|
|
4650
|
+
return res.ok;
|
|
4651
|
+
} catch {
|
|
4652
|
+
return false;
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4647
4656
|
// src/cli/session.ts
|
|
4648
4657
|
var IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4658
|
+
var BACKUP_INTERVAL_MS = 3 * 60 * 1e3;
|
|
4649
4659
|
var IDLE_WARNING_MS = 25 * 60 * 1e3;
|
|
4650
4660
|
var IDLE_CHECK_INTERVAL_MS = 60 * 1e3;
|
|
4651
4661
|
var _activeProject = null;
|
|
@@ -4715,8 +4725,27 @@ async function connectAndRunClaude(projectName, apiToken, claudeToken, projectUr
|
|
|
4715
4725
|
`);
|
|
4716
4726
|
return;
|
|
4717
4727
|
}
|
|
4718
|
-
const projectRoot = await findProjectRoot(projectName, apiToken);
|
|
4719
4728
|
console.log(` ${c.green}${c.bold}\u2713${c.reset} Connected ${c.gray}${elapsed()}${c.reset}`);
|
|
4729
|
+
const backupApiBase = process.env.OODA_API_BASE || "https://api.ooda.run";
|
|
4730
|
+
const backupOrgId = isOrgMode() ? getOrgId() : null;
|
|
4731
|
+
if (backupOrgId) {
|
|
4732
|
+
const r = await restoreProject({
|
|
4733
|
+
apiBase: backupApiBase,
|
|
4734
|
+
orgId: backupOrgId,
|
|
4735
|
+
projectName,
|
|
4736
|
+
jwt: getAccessToken() ?? ""
|
|
4737
|
+
});
|
|
4738
|
+
if (r.restored) {
|
|
4739
|
+
const where = r.dir ? ` ${c.gray}(${r.dir})${c.reset}` : "";
|
|
4740
|
+
console.log(` ${c.green}${c.bold}\u2713${c.reset} ${c.gray}Restored project files from last snapshot${c.reset}${where}`);
|
|
4741
|
+
} else if (r.reason === "no-backup") {
|
|
4742
|
+
console.log(` ${c.gray}\xB7 No snapshot to restore yet${c.reset}`);
|
|
4743
|
+
} else {
|
|
4744
|
+
console.log(` ${c.yellow}!${c.reset} ${c.gray}Snapshot restore failed (${r.reason ?? "unknown"})${c.reset}`);
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
const projectRoot = await findProjectRoot(projectName, apiToken);
|
|
4748
|
+
const canBackup = !!backupOrgId && projectRoot !== "/home/user" && projectRoot !== "/root";
|
|
4720
4749
|
pushPhase("setup", "Writing config files");
|
|
4721
4750
|
try {
|
|
4722
4751
|
const memRaw = await client.exec("free -m 2>/dev/null || cat /proc/meminfo 2>/dev/null | head -3");
|
|
@@ -4753,7 +4782,11 @@ async function connectAndRunClaude(projectName, apiToken, claudeToken, projectUr
|
|
|
4753
4782
|
const orgId = getOrgId();
|
|
4754
4783
|
const jwt = getAccessToken();
|
|
4755
4784
|
if (orgId && jwt) {
|
|
4756
|
-
|
|
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
|
+
);
|
|
4757
4790
|
}
|
|
4758
4791
|
}
|
|
4759
4792
|
const tokenType = getClaudeTokenType(claudeToken);
|
|
@@ -4996,6 +5029,23 @@ ${text}`;
|
|
|
4996
5029
|
claudeExec
|
|
4997
5030
|
].join("; ");
|
|
4998
5031
|
}
|
|
5032
|
+
let backupTimer = null;
|
|
5033
|
+
let backupInFlight = false;
|
|
5034
|
+
if (canBackup) {
|
|
5035
|
+
backupTimer = setInterval(() => {
|
|
5036
|
+
if (backupInFlight) return;
|
|
5037
|
+
backupInFlight = true;
|
|
5038
|
+
void backupProject({
|
|
5039
|
+
apiBase: backupApiBase,
|
|
5040
|
+
orgId: backupOrgId,
|
|
5041
|
+
projectName,
|
|
5042
|
+
projectRoot,
|
|
5043
|
+
jwt: getAccessToken() ?? ""
|
|
5044
|
+
}).finally(() => {
|
|
5045
|
+
backupInFlight = false;
|
|
5046
|
+
});
|
|
5047
|
+
}, BACKUP_INTERVAL_MS);
|
|
5048
|
+
}
|
|
4999
5049
|
let tokenRefreshTimer = null;
|
|
5000
5050
|
const orgApiKey = claudeEnv?.ANTHROPIC_API_KEY;
|
|
5001
5051
|
const useServerRefresh = !apiKeyHelper && orgApiKey && isOrgMode();
|
|
@@ -5144,6 +5194,22 @@ ${text}`;
|
|
|
5144
5194
|
currentCmdAlive = false;
|
|
5145
5195
|
_activeProject = null;
|
|
5146
5196
|
if (tokenRefreshTimer) clearInterval(tokenRefreshTimer);
|
|
5197
|
+
if (backupTimer) clearInterval(backupTimer);
|
|
5198
|
+
if (canBackup) {
|
|
5199
|
+
console.log(` ${c.gray}Saving project snapshot...${c.reset}`);
|
|
5200
|
+
const saved = await backupProject({
|
|
5201
|
+
apiBase: backupApiBase,
|
|
5202
|
+
orgId: backupOrgId,
|
|
5203
|
+
projectName,
|
|
5204
|
+
projectRoot,
|
|
5205
|
+
jwt: getAccessToken() ?? ""
|
|
5206
|
+
});
|
|
5207
|
+
if (saved) {
|
|
5208
|
+
console.log(` ${c.green}${c.bold}\u2713${c.reset} ${c.gray}Snapshot saved${c.reset}`);
|
|
5209
|
+
} else {
|
|
5210
|
+
console.log(` ${c.yellow}!${c.reset} ${c.gray}Snapshot save failed (project not backed up)${c.reset}`);
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5147
5213
|
console.log(` ${c.gray}Stopping project processes...${c.reset}`);
|
|
5148
5214
|
const result = await stopProject(projectName, apiToken);
|
|
5149
5215
|
if (result.ok) {
|
|
@@ -5372,8 +5438,382 @@ async function deployFromGitHubFlow(target, apiToken, claudeToken) {
|
|
|
5372
5438
|
}
|
|
5373
5439
|
}
|
|
5374
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
|
+
|
|
5375
5815
|
// src/cli/index.ts
|
|
5376
|
-
var CLI_VERSION = "0.1.
|
|
5816
|
+
var CLI_VERSION = "0.1.14";
|
|
5377
5817
|
function formatMutationError(result) {
|
|
5378
5818
|
const parts = [];
|
|
5379
5819
|
if (result.status !== void 0) parts.push(String(result.status));
|
|
@@ -5411,27 +5851,59 @@ function waitForEventOrCancel(emitter, event) {
|
|
|
5411
5851
|
}
|
|
5412
5852
|
var DEFAULT_PORT = 4444;
|
|
5413
5853
|
function parseArgs(argv) {
|
|
5414
|
-
|
|
5854
|
+
const rest = argv.slice(2);
|
|
5855
|
+
const first = rest[0];
|
|
5415
5856
|
let command = "menu";
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
if (
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
if (
|
|
5430
|
-
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;
|
|
5431
5872
|
i++;
|
|
5432
|
-
}
|
|
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
|
+
};
|
|
5433
5905
|
}
|
|
5434
|
-
return
|
|
5906
|
+
return args;
|
|
5435
5907
|
}
|
|
5436
5908
|
function eraseRenderedPrompt(visibleLines) {
|
|
5437
5909
|
const total = visibleLines + 2;
|
|
@@ -5560,14 +6032,24 @@ async function selectorLoop(apiToken, claudeToken, dashboardUrl, cwd) {
|
|
|
5560
6032
|
const project = action.project;
|
|
5561
6033
|
const currentUserName = isOrgMode() ? getOrgDisplayName() : null;
|
|
5562
6034
|
const isOwner = !isOrgMode() || !project.owner || project.owner === currentUserName;
|
|
5563
|
-
|
|
6035
|
+
let publishedUrl = null;
|
|
6036
|
+
const subOrgId = isOrgMode() ? getOrgId() : null;
|
|
6037
|
+
if (subOrgId) {
|
|
6038
|
+
const apiBase = process.env.OODA_API_BASE || "https://api.ooda.run";
|
|
6039
|
+
publishedUrl = await fetchPublishedSiteUrl({
|
|
6040
|
+
apiBase,
|
|
6041
|
+
orgId: subOrgId,
|
|
6042
|
+
projectName: project.name,
|
|
6043
|
+
jwt: getAccessToken() ?? ""
|
|
6044
|
+
});
|
|
6045
|
+
}
|
|
6046
|
+
const subAction = await projectSubMenu(project, isOwner, publishedUrl);
|
|
5564
6047
|
switch (subAction) {
|
|
5565
6048
|
case "connect":
|
|
5566
6049
|
await connectAndRunClaude(project.name, apiToken, claudeToken, project.url, void 0, project.previewUrl);
|
|
5567
6050
|
break;
|
|
5568
6051
|
case "open-published": {
|
|
5569
|
-
|
|
5570
|
-
await openUrl(publishUrl);
|
|
6052
|
+
await openUrl(publishedUrl ?? `https://${project.name}-p.ooda.run`);
|
|
5571
6053
|
break;
|
|
5572
6054
|
}
|
|
5573
6055
|
case "reupload": {
|
|
@@ -5841,6 +6323,10 @@ async function main() {
|
|
|
5841
6323
|
const args = parseArgs(process.argv);
|
|
5842
6324
|
const cwd = process.cwd();
|
|
5843
6325
|
printLogo(CLI_VERSION);
|
|
6326
|
+
if (args.command === "help") {
|
|
6327
|
+
console.log(buildHelpText(CLI_VERSION));
|
|
6328
|
+
process.exit(0);
|
|
6329
|
+
}
|
|
5844
6330
|
if (args.command === "list") {
|
|
5845
6331
|
await listProjectsCmd();
|
|
5846
6332
|
process.exit(0);
|
|
@@ -5851,11 +6337,22 @@ async function main() {
|
|
|
5851
6337
|
if (target && isGitHubTarget(target)) {
|
|
5852
6338
|
await deployFromGitHubFlow(target, apiToken, claudeToken);
|
|
5853
6339
|
} else {
|
|
5854
|
-
const deployPath = target ?
|
|
6340
|
+
const deployPath = target ? path11.resolve(target) : cwd;
|
|
5855
6341
|
await deployCurrentDir(deployPath, apiToken, claudeToken);
|
|
5856
6342
|
}
|
|
5857
6343
|
process.exit(0);
|
|
5858
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
|
+
}
|
|
5859
6356
|
if (args.command === "connect") {
|
|
5860
6357
|
if (!args.connectTarget) {
|
|
5861
6358
|
console.log(` ${c.red}Usage: ooda connect <project-name>${c.reset}`);
|