decocms
Version:
CLI for managing deco.chat apps & projects
194 lines ⢠6.95 kB
JavaScript
import inquirer from "inquirer";
import { promises as fs } from "fs";
import { relative, join, posix } from "path";
import { walk } from "../../lib/fs.js";
import { createWorkspaceClient } from "../../lib/mcp.js";
import { getCurrentEnvVars } from "../../lib/wrangler.js";
import { Buffer } from "node:buffer";
import process from "node:process";
function tryParseJson(text) {
try {
return JSON.parse(text);
}
catch {
return null;
}
}
function normalizePath(path) {
// Convert Windows backslashes to Unix forward slashes
return posix.normalize(path.replace(/\\/g, "/"));
}
const WRANGLER_CONFIG_FILES = ["wrangler.toml", "wrangler.json"];
export const deploy = async ({ cwd, workspace, app: appSlug, local, assetsDirectory, skipConfirmation, force, unlisted = true, dryRun = false, }) => {
console.log(`\nš ${dryRun ? "Preparing" : "Deploying"} '${appSlug}' to '${workspace}'${dryRun ? " (dry run)" : ""}...\n`);
// Ensure the target directory exists
try {
await fs.stat(cwd);
}
catch {
throw new Error("Target directory not found");
}
// 1. Prepare files to upload: all files in dist/ and wrangler.toml (if exists)
const files = [];
let hasTsFile = false;
let foundWranglerConfigInWalk = false;
let foundWranglerConfigName = "";
// Recursively walk cwd/ and add all files
for await (const entry of walk(cwd, {
includeFiles: true,
includeDirs: false,
skip: [
/node_modules/,
/\.git/,
/\.DS_Store/,
/\.env/,
/\.env\.local/,
/\.dev\.vars/,
],
exts: [
"ts",
"mjs",
"js",
"cjs",
"toml",
"json",
"css",
"html",
"txt",
"wasm",
"sql",
],
})) {
const realPath = normalizePath(relative(cwd, entry.path));
const content = await fs.readFile(entry.path, "utf-8");
files.push({ path: realPath, content });
if (realPath.endsWith(".ts")) {
hasTsFile = true;
}
if (WRANGLER_CONFIG_FILES.some((name) => realPath.includes(name))) {
foundWranglerConfigInWalk = true;
foundWranglerConfigName = realPath;
}
}
if (assetsDirectory) {
for await (const entry of walk(assetsDirectory, {
includeFiles: true,
includeDirs: false,
skip: [
/node_modules/,
/\.git/,
/\.DS_Store/,
/\.env/,
/\.env\.local/,
/\.dev\.vars/,
],
})) {
const realPath = normalizePath(relative(assetsDirectory, entry.path));
const content = await fs.readFile(entry.path);
const base64Content = Buffer.from(content).toString("base64");
files.push({ path: realPath, content: base64Content, asset: true });
}
}
// 2. wrangler.toml/json (optional)
let wranglerConfigStatus = "";
if (!foundWranglerConfigInWalk) {
let found = false;
for (const configFile of WRANGLER_CONFIG_FILES) {
const configPath = `${process.cwd()}/${configFile}`;
try {
const configContent = await fs.readFile(configPath, "utf-8");
files.push({ path: configFile, content: configContent });
wranglerConfigStatus = `${configFile} ā
(found in ${configPath})`;
found = true;
break;
}
catch (_) {
// not found, try next
}
}
if (!found) {
wranglerConfigStatus = "wrangler.toml/json ā";
}
}
else {
wranglerConfigStatus = `${foundWranglerConfigName} ā
(found in project files)`;
}
// 3. Load envVars from .dev.vars
const { envVars, envFilepath } = await getCurrentEnvVars(process.cwd());
const envVarsStatus = `Loaded ${Object.keys(envVars).length} env vars from ${envFilepath}`;
const manifest = {
appSlug,
files,
envVars,
envFilepath,
bundle: hasTsFile,
unlisted,
force,
};
console.log("š Deployment summary:");
console.log(` App: ${appSlug}`);
console.log(` Files: ${files.length}`);
console.log(` ${envVarsStatus}`);
console.log(` ${wranglerConfigStatus}`);
if (dryRun) {
const manifestPath = join(cwd, "deploy-manifest.json");
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
console.log(`\nš Dry run complete! Deploy manifest written to:`);
console.log(` ${manifestPath}`);
console.log();
return;
}
const confirmed = skipConfirmation ||
(await inquirer.prompt([
{
type: "confirm",
name: "proceed",
message: "Proceed with deployment?",
default: true,
},
])).proceed;
if (!confirmed) {
console.log("ā Deployment cancelled");
process.exit(0);
}
const client = await createWorkspaceClient({ workspace, local });
const deploy = async (options) => {
const response = await client.callTool({
name: "HOSTING_APP_DEPLOY",
arguments: manifest,
});
if (response.isError && Array.isArray(response.content)) {
console.error("Error deploying: ", response);
const errorText = response.content[0]?.text;
const errorTextJson = tryParseJson(errorText ?? "");
if (errorTextJson?.name === "MCPBreakingChangeError" && !force) {
console.log("Looks like you have breaking changes in your app.");
console.log(errorTextJson.message);
if (skipConfirmation) {
console.error("Use --force (-f) to deploy with breaking changes");
process.exit(1);
}
const confirmed = await inquirer.prompt([
{
type: "confirm",
name: "proceed",
message: "Would you like to retry with the --force flag?",
default: true,
},
]);
if (!confirmed) {
process.exit(1);
}
return deploy({ ...options, force: true });
}
throw new Error(errorTextJson ?? errorText ?? "Unknown error");
}
return response;
};
const response = await deploy(manifest);
const { hosts } = response.structuredContent;
console.log(`\nš Deployed! Available at:`);
hosts.forEach((host) => console.log(` ${host}`));
console.log();
};
//# sourceMappingURL=deploy.js.map