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