deco-cli
Version:
CLI for managing decocms.com apps & projects
6,463 lines • 217 kB
JavaScript
import { deleteSession, getLocal, readSession, getConfig, readWranglerConfig, getAppDomain, setToken, DECO_CMS_API_LOCAL, setLocal, AUTH_PORT_CLI, createClient, saveSession, DECO_CMS_LOGIN_URL, writeWranglerConfig, createWorkspaceClient, createWorkspaceClientStub, writeConfigFile, getMCPConfig, workspaceClientParams, getAppUUID, getMCPConfigVersion, getRulesConfig, getConfigFilePath } from './chunk-KFMYHDGF.js';
import process28 from 'process';
import { Command } from 'commander';
import fs11, { writeFile, readFile } from 'fs/promises';
import { spawn } from 'child_process';
import { promises, watch, existsSync, mkdirSync, writeFileSync, readFileSync, statSync, unlinkSync, createReadStream, createWriteStream } from 'fs';
import path2, { dirname, join, relative, resolve, posix, isAbsolute } from 'path';
import { createServer } from 'http';
import { fileURLToPath, URL as URL$1 } from 'url';
import inquirer6 from 'inquirer';
import inquirerSearchList from 'inquirer-search-list';
import { z } from 'zod';
import inquirerSearchCheckbox from 'inquirer-search-checkbox';
import { pipeline } from 'stream/promises';
import { compile } from 'json-schema-to-typescript';
import { generateName } from 'json-schema-to-typescript/dist/src/utils.js';
import { MD5 } from 'object-hash';
import prettier from 'prettier';
import { Buffer } from 'buffer';
import chalk5 from 'chalk';
import { connect } from '@deco-cx/warp-node';
import { createServer as createServer$1 } from 'net';
import * as semver from 'semver';
import { glob } from 'glob';
import { homedir } from 'os';
import { createHash } from 'crypto';
import ignore from 'ignore';
var DECONFIG_DIR = ".deconfig";
var HEAD_FILE = "head";
function getDeconfigHeadPath(cwd) {
const targetCwd = process28.cwd();
return join(targetCwd, DECONFIG_DIR, HEAD_FILE);
}
function getDeconfigDir(cwd) {
const targetCwd = process28.cwd();
return join(targetCwd, DECONFIG_DIR);
}
function ensureDeconfigDir(cwd) {
const deconfigDir = getDeconfigDir();
if (!existsSync(deconfigDir)) {
mkdirSync(deconfigDir, { recursive: true });
}
}
async function readDeconfigHead(cwd) {
const headPath = getDeconfigHeadPath();
try {
const content = await promises.readFile(headPath, "utf-8");
const config = JSON.parse(content);
return config;
} catch (error) {
if (error.code === "ENOENT") {
return null;
}
console.warn(
`Warning: Could not read .deconfig/head file: ${error instanceof Error ? error.message : String(error)}`
);
return null;
}
}
async function writeDeconfigHead(config, cwd) {
ensureDeconfigDir();
const headPath = getDeconfigHeadPath();
try {
const content = JSON.stringify(config, null, 2);
await promises.writeFile(headPath, content, "utf-8");
} catch (error) {
console.error(
`\u274C Failed to write .deconfig/head file: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
var loginCommand = () => {
return new Promise((resolve3, reject) => {
let timeout;
const server = createServer(
async (req, res) => {
const url = new URL$1(req.url, `http://localhost:${AUTH_PORT_CLI}`);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (value) {
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
}
}
const { client, responseHeaders } = createClient(headers);
if (url.pathname === "/login/oauth") {
const credentials = {
provider: url.searchParams.get("provider") ?? "google",
options: { redirectTo: new URL$1("/auth/callback/oauth", url).href }
};
const { data } = await client.auth.signInWithOAuth(credentials);
if (data.url) {
const responseHeadersObj = {};
responseHeaders.forEach((value, key) => {
responseHeadersObj[key] = value;
});
res.writeHead(302, {
Location: data.url,
...responseHeadersObj
});
res.end();
return;
}
res.writeHead(500);
res.end("Error redirecting to OAuth provider");
return;
}
if (url.pathname === "/auth/callback/oauth") {
const code = url.searchParams.get("code");
if (!code) {
res.writeHead(400);
res.end("No code found");
return;
}
const { data, error } = await client.auth.exchangeCodeForSession(code);
if (error || !data?.session) {
res.writeHead(400);
res.end(error?.message ?? "Unknown error");
return;
}
try {
await saveSession(data);
} catch (e) {
console.error("Failed to save session data:", e);
}
if (timeout) {
clearTimeout(timeout);
}
const html = await fetch(
"https://admin.decocms.com/local-login-success.html"
).then((res2) => res2.text());
res.writeHead(200, {
"Content-Type": "text/html"
});
res.end(html);
server.close(() => resolve3());
return;
}
res.writeHead(404);
res.end("Not found");
}
);
server.listen(AUTH_PORT_CLI, () => {
const browserCommands = {
linux: "xdg-open",
darwin: "open",
win32: "start",
freebsd: "xdg-open",
openbsd: "xdg-open",
sunos: "xdg-open",
aix: "open"
};
const browser = process28.env.BROWSER ?? browserCommands[process28.platform] ?? "open";
console.log("\u{1F510} Starting authentication process...");
console.log("Opening browser for login...\n");
const command = process28.platform === "win32" && browser === "start" ? spawn("cmd", ["/c", "start", DECO_CMS_LOGIN_URL], {
detached: true
}) : spawn(browser, [DECO_CMS_LOGIN_URL], { detached: true });
command.unref();
command.on("error", () => {
console.log("\u26A0\uFE0F Could not automatically open browser");
});
timeout = setTimeout(() => {
console.log(
"\u{1F4CB} If your browser didn't open automatically, please click the following link:"
);
console.log(`
${DECO_CMS_LOGIN_URL}
`);
console.log("Waiting for authentication to complete...\n");
}, 1e3);
});
server.on("error", (err) => {
reject(err);
});
});
};
// src/commands/auth/whoami.ts
var whoamiCommand = async () => {
try {
const session = await readSession();
if (!session || !session.access_token || !session.refresh_token) {
console.log("\u274C Not logged in. Run `deco login` to authenticate.\n");
return;
}
const { client: supabase } = createClient();
const { error: setSessionError } = await supabase.auth.setSession({
access_token: session.access_token,
refresh_token: session.refresh_token
});
if (setSessionError) {
console.log("\u274C Session expired or invalid. Please log in again.\n");
return;
}
const { data, error } = await supabase.auth.getUser();
if (error || !data?.user) {
console.log("\u274C Could not retrieve user info. Please log in again.\n");
return;
}
const user = data.user;
console.log("\u{1F464} User Info:");
console.log(` \u{1F4BB} ID: ${user.id}`);
console.log(` \u{1F4E7} Email: ${user.email ?? "-"}`);
if (user.user_metadata?.full_name) {
console.log(` \u{1F4DA} Name: ${user.user_metadata.full_name}`);
}
if (user.user_metadata?.avatar_url) {
console.log(` \u{1F5BC}\uFE0F Avatar: ${user.user_metadata.avatar_url}`);
}
console.log("");
if (session.workspace) {
console.log(
`\u{1F3E2} Current Workspace: \x1B[1m${session.workspace}\x1B[0m
`
);
} else {
console.log("\u26A0\uFE0F No workspace selected.\n");
}
} catch (err) {
const message = typeof err === "object" && err && "message" in err ? err.message : String(err);
console.error("\u274C Error reading session:", message);
}
};
async function promptWorkspace(local = false, current = "") {
try {
inquirer6.registerPrompt("search-list", inquirerSearchList);
} catch {
console.warn(
"Could not load search functionality, falling back to basic list"
);
}
const session = await readSession();
if (!session) {
throw new Error("No session found. Please run 'deco login' first.");
}
const client = await createWorkspaceClient({ workspace: "", local });
try {
const response = await client.callTool(
{
name: "TEAMS_LIST",
arguments: {}
},
// @ts-expect-error We need to refactor TEAMS_LIST to stop returning array and use a proper object
z.any()
);
if (response.isError) {
throw new Error("Failed to fetch teams");
}
const { items: teams } = response.structuredContent;
if (!teams || teams.length === 0) {
throw new Error("No teams found. Please create a team first.");
}
const choices = teams.map((team) => ({
name: team.name,
value: team.slug
}));
let selectedSlug;
try {
const result = await inquirer6.prompt([
{
type: "search-list",
name: "selectedSlug",
message: "Select a workspace:",
choices,
default: current
}
]);
selectedSlug = result.selectedSlug;
} catch {
const result = await inquirer6.prompt([
{
type: "list",
name: "selectedSlug",
message: "Select a workspace:",
choices,
default: current
}
]);
selectedSlug = result.selectedSlug;
}
return selectedSlug;
} finally {
await client.close();
}
}
// src/lib/slugify.ts
function slugify(input) {
return input.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
}
function sanitizeConstantName(input) {
return input.toUpperCase().replace(/[\s_]+/g, "_").replace(/[^A-Z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
}
inquirer6.registerPrompt("search-checkbox", inquirerSearchCheckbox);
async function promptIntegrations(local = false, workspace = "") {
const session = await readSession();
if (!session) {
throw new Error("No session found. Please run 'deco login' first.");
}
const client = await createWorkspaceClient({ workspace, local });
try {
const response = await client.callTool(
{
name: "INTEGRATIONS_LIST",
arguments: {}
},
// @ts-expect-error We need to refactor INTEGRATIONS_LIST to stop returning array and use a proper object
z.any()
);
if (response.isError) {
throw new Error("Failed to fetch integrations");
}
const integrationsResponse = response.structuredContent?.items;
const integrations = (integrationsResponse || []).filter((c) => c.connection.type !== "INNATE").sort((a, b) => a.name.localeCompare(b.name));
if (!integrations || integrations.length === 0) {
throw new Error("No integrations found.");
}
const options = integrations.map((integration) => ({
name: `${integration.name} - ${integration.description}`,
value: integration.id,
short: integration.name
}));
const { selectedIntegrationIds } = await inquirer6.prompt([
{
type: "search-checkbox",
name: "selectedIntegrationIds",
message: "Select integrations (use space to select, enter to confirm):",
choices: options,
searchable: true,
highlight: true,
searchText: "Type to search integrations:",
emptyText: "No integrations found matching your search."
}
]);
const selectedIntegrations = integrations.filter(
(integration) => selectedIntegrationIds.includes(integration.id)
);
return selectedIntegrations.map(({ name, id }) => ({
name: sanitizeConstantName(name),
type: "mcp",
integration_id: id
}));
} finally {
await client.close();
}
}
async function ensureDir(dirPath) {
try {
await promises.mkdir(dirPath, { recursive: true });
} catch (error) {
if (error.code !== "EEXIST") {
throw error;
}
}
}
async function copyFile(src, dest) {
await ensureDir(dirname(dest));
await pipeline(createReadStream(src), createWriteStream(dest));
}
async function copyDir(src, dest, options = {}) {
await ensureDir(dest);
const entries = await promises.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);
if (entry.isDirectory()) {
await copyDir(srcPath, destPath, options);
} else if (entry.isFile()) {
if (!options.overwrite) {
try {
await promises.access(destPath);
continue;
} catch {
}
}
await copyFile(srcPath, destPath);
}
}
}
async function copy(src, dest, options = {}) {
const srcStat = await promises.stat(src);
if (srcStat.isDirectory()) {
await copyDir(src, dest, options);
} else if (srcStat.isFile()) {
if (!options.overwrite) {
try {
await promises.access(dest);
return;
} catch {
}
}
await copyFile(src, dest);
} else {
throw new Error(`Source ${src} is neither a file nor a directory`);
}
}
async function* walk(root, options = {}) {
const {
maxDepth = Infinity,
includeFiles = true,
includeDirs = false,
followSymlinks = false,
exts,
match,
skip
} = options;
async function* walkRecursive(dir, depth) {
if (depth > maxDepth) return;
let entries;
try {
entries = await promises.readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const entryPath = join(dir, entry.name);
const relativePath = relative(root, entryPath);
let isFile = entry.isFile();
let isDirectory = entry.isDirectory();
const isSymlink = entry.isSymbolicLink();
if (isSymlink && followSymlinks) {
try {
const stat = await promises.stat(entryPath);
isFile = stat.isFile();
isDirectory = stat.isDirectory();
} catch {
continue;
}
} else if (isSymlink && !followSymlinks) {
continue;
}
const walkEntry = {
path: entryPath,
name: entry.name,
isFile,
isDirectory,
isSymlink
};
if (skip && skip.some((pattern) => pattern.test(relativePath))) {
continue;
}
if (match && !match.some((pattern) => pattern.test(relativePath))) {
continue;
}
if (exts && isFile) {
const ext = entryPath.substring(entryPath.lastIndexOf(".") + 1);
if (!exts.includes(ext)) {
continue;
}
}
if (isFile && includeFiles) {
yield walkEntry;
}
if (isDirectory && includeDirs) {
yield walkEntry;
}
if (isDirectory) {
yield* walkRecursive(entryPath, depth + 1);
}
}
}
yield* walkRecursive(root, 0);
}
var IDE_SUPPORT = {
cursor: {
name: "Cursor",
createConfig: async (mcpConfig, projectRoot) => {
const outDir = join(projectRoot, ".cursor");
const configs = [];
const configPath = join(outDir, "mcp.json");
const existingConfig = await promises.readFile(configPath, "utf-8").then(JSON.parse).catch(() => ({ mcpServers: {} }));
const config = {
mcpServers: {
...existingConfig.mcpServers || {},
...mcpConfig.mcpServers
}
};
configs.push({
content: JSON.stringify(config, null, 2),
path: join(outDir, "mcp.json")
});
const rules = Object.entries(await getRulesConfig());
for (const [path4, content] of rules) {
configs.push({ content, path: join(outDir, "rules", path4) });
}
return configs;
}
},
vscode: {
name: "VS Code",
createConfig: async (mcpConfig, projectRoot) => {
const outDir = join(projectRoot, ".vscode");
const configs = [];
const configPath = join(outDir, "mcp.json");
const existingConfig = await promises.readFile(configPath, "utf-8").then(JSON.parse).catch(() => ({ mcpServers: {} }));
const config = {
mcpServers: {
...existingConfig.mcpServers || {},
...mcpConfig.mcpServers
}
};
configs.push({
content: JSON.stringify(config, null, 2),
path: join(outDir, "mcp.json")
});
const rules = Object.entries(await getRulesConfig());
for (const [path4, content] of rules) {
configs.push({ content, path: join(outDir, "rules", path4) });
}
return configs;
}
}
};
async function writeIDEConfig(configs) {
const targetDir = dirname(configs[0]?.path ?? "");
await Promise.all(
configs.map(async ({ content, path: path4 }) => {
await ensureDir(dirname(path4));
await promises.writeFile(path4, content);
})
);
console.log(`\u2705 IDE configuration written to: ${targetDir}`);
}
var setMCPPreferences = async (workspace, app) => {
const [appUUID, currentVersion] = await Promise.all([
getAppUUID(workspace, app),
getMCPConfigVersion()
]);
const prefsPath = join(process28.cwd(), ".deco", "preferences.json");
try {
await ensureDir(dirname(prefsPath));
let prefs = {};
try {
prefs = JSON.parse(await promises.readFile(prefsPath, "utf-8"));
} catch {
}
prefs[`mcp-install-version-${appUUID}`] = currentVersion;
await promises.writeFile(prefsPath, JSON.stringify(prefs, null, 2));
} catch (error) {
console.warn("Failed to save MCP preferences:", error);
}
};
async function promptIDESetup(cfg, projectRoot = process28.cwd()) {
await setMCPPreferences(cfg.workspace, cfg.app);
const mcpConfig = getMCPConfig(cfg.workspace, cfg.app);
const { wantsSentientIDE } = await inquirer6.prompt([
{
type: "confirm",
name: "wantsSentientIDE",
message: "Would you like to configure your IDE to use this project?",
default: true
}
]);
if (!wantsSentientIDE) {
return null;
}
const { selectedIDE } = await inquirer6.prompt([
{
type: "list",
name: "selectedIDE",
message: "Select your preferred IDE:",
choices: [
{ name: "Cursor", value: "cursor" },
{ name: "VS Code", value: "vscode" },
{ name: "None", value: "none" }
]
}
]);
const ideSupport = IDE_SUPPORT[selectedIDE];
if (selectedIDE === "none") {
return null;
}
const configs = await ideSupport.createConfig(mcpConfig, projectRoot);
return configs;
}
// src/lib/parse-binding-tool.ts
var SEPARATOR = "::";
var parser = {
fromBindingToolToScope: ({
bindingName,
toolName
}) => {
const parts = [bindingName, toolName];
if (parts.some((part) => part.includes(SEPARATOR))) {
throw new Error(
`binding name or tool name includes ${SEPARATOR} is not allowed`
);
}
return parts.join(SEPARATOR);
},
fromScopeToBindingTool: (scope) => {
const [bindingName, toolName] = scope.split(SEPARATOR);
return { bindingName, toolName };
}
};
// src/commands/gen/gen.ts
var toValidProperty = (property) => {
return isValidJavaScriptPropertyName(property) ? property : `["${property}"]`;
};
var formatDescription = (desc) => {
if (!desc) return "";
return desc.replace(/\*\//g, "*\\/").replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => ` * ${line}`).join("\n");
};
function slugify2(name) {
return name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
}
function format(content) {
try {
return prettier.format(content, {
parser: "babel-ts",
plugins: []
});
} catch {
return Promise.resolve(content);
}
}
var RESERVED_KEYWORDS = [
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"export",
"extends",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"let",
"new",
"return",
"super",
"switch",
"this",
"throw",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
"enum",
"await",
"implements",
"interface",
"package",
"private",
"protected",
"public",
"static",
"abstract",
"boolean",
"byte",
"char",
"double",
"final",
"float",
"goto",
"int",
"long",
"native",
"short",
"synchronized",
"throws",
"transient",
"volatile",
"null",
"true",
"false",
"undefined",
"NaN",
"Infinity"
];
function isValidJavaScriptPropertyName(name) {
const validIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
if (!validIdentifierRegex.test(name)) {
return false;
}
return !RESERVED_KEYWORDS.includes(name);
}
var CONTRACTS_BINDING = "@deco/contracts";
var unwrapMcpResult = (result, opts) => {
if ("isError" in result && result.isError) {
const message = (Array.isArray(result.content) ? result.content[0]?.text : void 0) ?? JSON.stringify(result);
throw new Error(opts?.errorMessage?.(message) ?? message);
}
return result;
};
var workspaceSlug = (workspace) => {
if (workspace.startsWith("/")) {
return workspace.slice(1).split("/")[1];
}
return workspace;
};
var genEnv = async ({
workspace,
local,
bindings,
selfUrl
}) => {
const wrangler = await readWranglerConfig();
const appName = `@${wrangler.scope ?? workspaceSlug(workspace)}/${wrangler.name}`;
const client = await createWorkspaceClient({ workspace, local });
const apiClient = await createWorkspaceClient({ local });
try {
const types = /* @__PURE__ */ new Map();
types.set("Env", 1);
let tsTypes = "";
const mapBindingTools = {};
const props = await Promise.all(
[
...bindings,
...selfUrl ? [
{
name: "SELF",
type: "mcp",
integration_url: selfUrl,
ignoreCache: true
}
] : []
].map(async (binding) => {
let connection;
let stateKey;
if ("integration_id" in binding) {
const integrationResult = await client.callTool({
name: "INTEGRATIONS_GET",
arguments: {
id: binding.integration_id
}
});
const integration = unwrapMcpResult(integrationResult, {
errorMessage: (error) => `Error getting integration ${binding.integration_id}: ${error}`
});
connection = integration.structuredContent.connection;
} else if ("integration_name" in binding || binding.type === "contract") {
const [integrationName, type] = "integration_name" in binding ? [binding.integration_name, binding.integration_name] : [CONTRACTS_BINDING, `${appName}-${MD5(binding.contract)}`];
stateKey = { type, key: binding.name };
const appResult = await apiClient.callTool({
name: "REGISTRY_GET_APP",
arguments: {
name: integrationName
}
});
const app = unwrapMcpResult(appResult, {
errorMessage: (error) => `Error getting app ${integrationName}: ${error}`
});
connection = app.structuredContent.connection;
} else if ("integration_url" in binding) {
connection = {
type: "HTTP",
url: binding.integration_url
};
} else {
throw new Error(`Unknown binding type: ${binding}`);
}
const tools = await apiClient.callTool({
name: "INTEGRATIONS_LIST_TOOLS",
arguments: {
connection,
ignoreCache: "ignoreCache" in binding ? binding.ignoreCache : void 0
}
});
if (!Array.isArray(tools.structuredContent?.tools)) {
console.warn(
`\u26A0\uFE0F No tools found for integration ${binding.name}. Skipping...`
);
return null;
}
if ("integration_name" in binding || binding.type === "contract") {
mapBindingTools[binding.name] = tools.structuredContent.tools.map(
(t) => t.name
);
}
const compiledTools = await Promise.all(
tools.structuredContent.tools.map(async (t) => {
const jsName = generateName(t.name, /* @__PURE__ */ new Set());
const inputName = `${jsName}Input`;
const outputName = `${jsName}Output`;
const customName = (schema) => {
let typeName = schema.title ?? schema.type;
if (Array.isArray(typeName)) {
typeName = typeName.join(",");
}
if (typeof typeName !== "string") {
return void 0;
}
const key = slugify2(typeName);
const count = types.get(key) ?? 0;
types.set(key, count + 1);
return count ? `${typeName}_${count}` : typeName;
};
const [inputTs, outputTs] = await Promise.all([
compile({ ...t.inputSchema, title: inputName }, inputName, {
additionalProperties: false,
customName,
format: false
}),
t.outputSchema ? await compile(
{ ...t.outputSchema, title: outputName },
outputName,
{
customName,
additionalProperties: false,
format: false
}
) : void 0
]);
tsTypes += `
${inputTs}
${outputTs ?? ""}
`;
return [
t.name,
inputName,
outputTs ? outputName : void 0,
t.description
];
})
);
return [binding.name, compiledTools, stateKey];
})
);
return await format(`
// Generated types - do not edit manually
${tsTypes}
import { z } from "zod";
export type Mcp<T extends Record<string, (input: any) => Promise<any>>> = {
[K in keyof T]: ((input: Parameters<T[K]>[0]) => Promise<Awaited<ReturnType<T[K]>>>) & {
asTool: () => Promise<{
inputSchema: z.ZodType<Parameters<T[K]>[0]>
outputSchema?: z.ZodType<Awaited<ReturnType<T[K]>>>
description: string
id: string
execute: (input: Parameters<T[K]>[0]) => Promise<Awaited<ReturnType<T[K]>>>
}>
}
}
export const StateSchema = z.object({
${props.filter((p) => p !== null && p[2] !== void 0).map((prop) => {
const [_, __, stateKey] = prop;
return `${stateKey.key}: z.object({
value: z.string(),
__type: z.literal("${stateKey.type}").default("${stateKey.type}"),
})`;
}).join(",\n")}
})
export interface Env {
DECO_CHAT_WORKSPACE: string;
DECO_CHAT_API_JWT_PUBLIC_KEY: string;
${props.filter((p) => p !== null).map(([propName, tools]) => {
return `${propName}: Mcp<{
${tools.map(([toolName, inputName, outputName, description]) => {
const docComment = description ? `/**
${formatDescription(description)}
*/` : "";
return `${docComment}
${toValidProperty(
toolName
)}: (input: ${inputName}) => Promise<${outputName ?? "any"}>;
`;
}).join("")}
}>;`;
}).join("")}
}
export const Scopes = {
${Object.entries(mapBindingTools).map(
([bindingName, tools]) => `${toValidProperty(bindingName)}: {
${tools.map(
(toolName) => `${toValidProperty(toolName)}: "${parser.fromBindingToolToScope(
{ bindingName, toolName }
)}"`
).join(",\n")}
}`
).join(",\n")}
}
`);
} finally {
await client.close();
await apiClient.close();
}
};
async function configureCommand(local) {
const currentConfig = await getConfig({ inlineOptions: { local } }).catch(
() => ({})
);
const wranglerConfig = await readWranglerConfig();
const defaultApp = typeof wranglerConfig.name === "string" ? wranglerConfig.name : "my-app";
const { app } = await inquirer6.prompt([
{
type: "input",
name: "app",
message: "Enter app name:",
default: defaultApp
}
]);
const workspace = await promptWorkspace(local, currentConfig.workspace);
const mcpConfig = await promptIDESetup({ workspace, app });
const bindings = await promptIntegrations(local, workspace);
const envContent = await genEnv({ workspace, local, bindings });
if (mcpConfig) {
await writeIDEConfig(mcpConfig);
}
await writeWranglerConfig({
name: app,
deco: {
...wranglerConfig.deco,
workspace,
bindings: [...bindings, ...wranglerConfig.deco?.bindings ?? []]
}
});
const outputPath = join(process28.cwd(), "deco.gen.ts");
await promises.writeFile(outputPath, envContent);
console.log(`\u2705 Environment types written to: ${outputPath}`);
console.log(`\u2705 Configuration saved:`);
console.log(` App: ${app}`);
console.log(` Workspace: ${workspace}`);
}
var envFile = ".dev.vars";
async function getCurrentEnvVars(projectRoot) {
const envFilepath = join(projectRoot, envFile);
const devVarsFile = await promises.readFile(envFilepath, "utf-8").catch(() => "");
const envVars = devVarsFile.split("\n").reduce(
(acc, line) => {
if (!line || line.startsWith("#")) {
return acc;
}
const firstEqualIndex = line.indexOf("=");
if (firstEqualIndex === -1) {
return acc;
}
const key = line.substring(0, firstEqualIndex);
const value = line.substring(firstEqualIndex + 1);
acc[key] = value;
return acc;
},
{}
);
return {
envVars,
envFilepath
};
}
async function writeEnvVars(projectRoot, envVars) {
await promises.writeFile(
join(projectRoot, envFile),
Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join("\n")
);
}
var getProjectRoot = () => {
const configPath = getConfigFilePath(process28.cwd()) ?? process28.cwd();
return dirname(configPath);
};
async function getEnvVars(projectRoot) {
if (!projectRoot) {
projectRoot = getProjectRoot();
}
const [currentEnvVars, session, config, wrangler] = await Promise.all([
getCurrentEnvVars(projectRoot).then(({ envVars }) => envVars),
readSession(),
getConfig({}),
readWranglerConfig(projectRoot)
]);
const encodedBindings = Buffer.from(JSON.stringify(config.bindings)).toString(
"base64"
);
const workspace = config.workspace ?? session?.workspace;
const decoEnvVars = {
DECO_WORKSPACE: workspace || "",
DECO_API_TOKEN: session?.access_token ?? "",
DECO_BINDINGS: encodedBindings,
DECO_APP_ENTRYPOINT: "http://localhost:8787"
};
const deprecatedEnvVars = {
DECO_CHAT_WORKSPACE: decoEnvVars.DECO_WORKSPACE,
DECO_CHAT_API_TOKEN: decoEnvVars.DECO_API_TOKEN,
DECO_CHAT_BINDINGS: decoEnvVars.DECO_BINDINGS,
DECO_CHAT_APP_ENTRYPOINT: decoEnvVars.DECO_APP_ENTRYPOINT
};
const env = {
...currentEnvVars,
...deprecatedEnvVars,
...decoEnvVars
};
const { name, scope } = wrangler;
if (name && workspace) {
const [_, slug] = workspace.split("/");
const appName = `@${scope ?? slug}/${name}`;
env.DECO_APP_NAME = appName;
env.DECO_CHAT_APP_NAME = appName;
}
if (config.local) {
const apiUrl = "http://localhost:3001";
env.DECO_API_URL = apiUrl;
env.DECO_CHAT_API_URL = apiUrl;
} else {
delete env.DECO_API_URL;
delete env.DECO_CHAT_API_URL;
}
return env;
}
async function ensureEnvVarsGitIgnore(projectRoot) {
const gitignorePath = join(projectRoot, ".gitignore");
try {
const gitignoreContent = await promises.readFile(gitignorePath, "utf-8");
const lines = gitignoreContent.split("\n");
const entryExists = lines.some(
(line) => line.trim() === envFile || line.trim() === `/${envFile}`
);
if (!entryExists) {
const newContent = gitignoreContent.endsWith("\n") ? gitignoreContent + envFile + "\n" : gitignoreContent + "\n" + envFile + "\n";
await promises.writeFile(gitignorePath, newContent);
}
} catch {
await promises.writeFile(gitignorePath, envFile + "\n");
}
}
async function addZodDependency(projectRoot) {
const packageJsonPath = join(projectRoot, "package.json");
const packageJsonContent = await promises.readFile(packageJsonPath, "utf-8");
const packageJson = JSON.parse(packageJsonContent);
if (!packageJson.dependencies) {
packageJson.dependencies = {};
}
packageJson.dependencies.zod = "^3.24.3";
await promises.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + "\n"
);
}
async function cleanBuildDirectory(projectRoot, directory) {
const buildDir = join(projectRoot, directory);
await promises.rm(buildDir, { recursive: true, force: true });
await promises.mkdir(buildDir);
}
async function ensureDevEnvironment(opts) {
const projectRoot = getProjectRoot();
if (opts.cleanBuildDirectory?.enabled) {
await cleanBuildDirectory(projectRoot, opts.cleanBuildDirectory.directory);
}
await ensureEnvVarsGitIgnore(projectRoot);
const env = await getEnvVars(projectRoot);
await writeEnvVars(projectRoot, env);
await addZodDependency(projectRoot);
}
function parseKeyValueEnvVar(input) {
const eqIndex = input.indexOf("=");
if (eqIndex === -1) {
return null;
}
const key = input.slice(0, eqIndex);
if (!key) {
console.warn(
`Warning: Skipping invalid environment variable line with empty key: "${input}"`
);
return null;
}
const value = input.slice(eqIndex + 1);
return { key, value };
}
function parseEnvFileContent(content) {
return content.split("\n").reduce(
(acc, line) => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
return acc;
}
const parsed = parseKeyValueEnvVar(trimmed);
if (parsed) {
acc[parsed.key] = parsed.value;
}
return acc;
},
{}
);
}
function parseJsonEnvVars(jsonObj) {
if (typeof jsonObj !== "object" || jsonObj === null) {
throw new Error("Invalid JSON: expected object");
}
const result = {};
for (const [key, value] of Object.entries(jsonObj)) {
result[key] = typeof value === "string" ? value : String(value);
}
return result;
}
async function parseEnvFile(filePath, workingDir) {
const envFilePath = isAbsolute(filePath) ? filePath : join(workingDir, filePath);
const fileContent = await promises.readFile(envFilePath, "utf-8");
try {
const jsonObj = JSON.parse(fileContent);
return parseJsonEnvVars(jsonObj);
} catch {
return parseEnvFileContent(fileContent);
}
}
function parseInlineJsonEnvVars(jsonString) {
const jsonObj = JSON.parse(jsonString);
return parseJsonEnvVars(jsonObj);
}
function isFilePath(input) {
if (input.includes("/") || input.includes("\\")) return true;
if (input.startsWith(".env")) return true;
if (input.endsWith(".json")) return true;
return false;
}
// src/commands/hosting/deploy.ts
function tryParseJson(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}
function normalizePath(path4) {
return posix.normalize(path4.replace(/\\/g, "/"));
}
function tryParseInlineJson(input) {
try {
const parsedVars = parseInlineJsonEnvVars(input);
const count = Object.keys(parsedVars).length;
return { vars: parsedVars, count };
} catch (error) {
console.warn(
`\u26A0\uFE0F Invalid JSON format: "${input}". Error: ${error instanceof Error ? error.message : String(error)}. Skipping.`
);
return null;
}
}
async function tryParseEnvFile(filePath, workingDir) {
try {
const parsedVars = await parseEnvFile(filePath, workingDir);
const count = Object.keys(parsedVars).length;
return { vars: parsedVars, count };
} catch (error) {
console.warn(
`\u26A0\uFE0F Failed to read env file "${filePath}": ${error instanceof Error ? error.message : String(error)}. Skipping.`
);
return null;
}
}
var WRANGLER_CONFIG_FILES = ["wrangler.toml", "wrangler.json"];
var deploy = async ({
cwd,
workspace,
app: appSlug,
local,
assetsDirectory,
skipConfirmation,
force,
promote = true,
unlisted = true,
dryRun = false,
inlineEnvVars = []
}) => {
console.log(
`
\u{1F680} ${dryRun ? "Preparing" : "Deploying"} '${appSlug}' to '${workspace}'${dryRun ? " (dry run)" : ""}...
`
);
try {
await promises.stat(cwd);
} catch {
throw new Error("Target directory not found");
}
const files = [];
let hasTsFile = false;
let foundWranglerConfigInWalk = false;
let foundWranglerConfigName = "";
for await (const entry of walk(cwd, {
includeFiles: true,
includeDirs: false,
skip: [
/node_modules/,
/\.git/,
/\.DS_Store/,
/\.env/,
/\.env\.local/,
/\.dev\.vars/,
/\.vite/
],
exts: [
"ts",
"mjs",
"js",
"cjs",
"toml",
"json",
"css",
"html",
"txt",
"wasm",
"sql"
]
})) {
const realPath = normalizePath(relative(cwd, entry.path));
const content = await promises.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 promises.readFile(entry.path);
const base64Content = Buffer.from(content).toString("base64");
files.push({ path: realPath, content: base64Content, asset: true });
}
}
let wranglerConfigStatus = "";
if (!foundWranglerConfigInWalk) {
let found = false;
for (const configFile of WRANGLER_CONFIG_FILES) {
const configPath = `${process28.cwd()}/${configFile}`;
try {
const configContent = await promises.readFile(configPath, "utf-8");
files.push({ path: configFile, content: configContent });
wranglerConfigStatus = `${configFile} \u2705 (found in ${configPath})`;
found = true;
break;
} catch {
}
}
if (!found) {
wranglerConfigStatus = "wrangler.toml/json \u274C";
}
} else {
wranglerConfigStatus = `${foundWranglerConfigName} \u2705 (found in project files)`;
}
const { envVars: fileEnvVars, envFilepath } = await getCurrentEnvVars(cwd);
const parsedInlineEnvVars = {};
const envVarSources = [];
for (const envVar of inlineEnvVars) {
const trimmedEnvVar = envVar.trim();
if (trimmedEnvVar.startsWith("{")) {
const result = tryParseInlineJson(trimmedEnvVar);
if (result && result.count > 0) {
Object.assign(parsedInlineEnvVars, result.vars);
envVarSources.push(`JSON (${result.count} vars)`);
}
continue;
}
if (isFilePath(trimmedEnvVar)) {
const result = await tryParseEnvFile(trimmedEnvVar, cwd);
if (result && result.count > 0) {
Object.assign(parsedInlineEnvVars, result.vars);
envVarSources.push(`${trimmedEnvVar} (${result.count} vars)`);
}
continue;
}
const parsed = parseKeyValueEnvVar(trimmedEnvVar);
if (!parsed) {
console.warn(
`\u26A0\uFE0F Invalid env var format: "${trimmedEnvVar}". Expected KEY=VALUE, JSON object, or file path. Skipping.`
);
continue;
}
parsedInlineEnvVars[parsed.key] = parsed.value;
}
const envVars = { ...fileEnvVars, ...parsedInlineEnvVars };
const envVarsFromFile = Object.keys(fileEnvVars).length;
const envVarsFromCLI = Object.keys(parsedInlineEnvVars).length;
const envVarsTotal = Object.keys(envVars).length;
let envVarsStatus = `Loaded ${envVarsFromFile} env vars from ${envFilepath}`;
if (envVarsFromCLI > 0) {
const sourcesInfo = envVarSources.length > 0 ? ` [${envVarSources.join(", ")}]` : "";
envVarsStatus += ` + ${envVarsFromCLI} from CLI${sourcesInfo} (${envVarsTotal} total)`;
}
const manifest = {
appSlug,
files,
envVars,
envFilepath,
bundle: hasTsFile,
unlisted,
force,
promote
};
console.log("\u{1F69A} Deployment summary:");
console.log(` App: ${appSlug}`);
console.log(` Files: ${files.length}`);
console.log(` ${envVarsStatus}`);
console.log(` ${wranglerConfigStatus}`);
if (promote) {
console.log(` Promote mode: true (deployment will replace production)`);
}
if (dryRun) {
const manifestPath = join(cwd, "deploy-manifest.json");
await promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
console.log(`
\u{1F4C4} Dry run complete! Deploy manifest written to:`);
console.log(` ${manifestPath}`);
console.log();
return;
}
const confirmed = skipConfirmation || (await inquirer6.prompt([
{
type: "confirm",
name: "proceed",
message: "Proceed with deployment?",
default: true
}
])).proceed;
if (!confirmed) {
console.log("\u274C Deployment cancelled");
process28.exit(0);
}
const client = await createWorkspaceClientStub({ workspace, local });
const deploy2 = async (options) => {
const response2 = await client.callTool({
name: "HOSTING_APP_DEPLOY",
arguments: manifest
});
if (response2.isError && Array.isArray(response2.content)) {
console.error("Error deploying: ", response2);
const errorText = response2.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");
process28.exit(1);
}
const confirmed2 = await inquirer6.prompt([
{
type: "confirm",
name: "proceed",
message: "Would you like to retry with the --force flag?",
default: true
}
]);
if (!confirmed2) {
process28.exit(1);
}
return deploy2({ ...options, force: true });
}
throw new Error(errorText ?? "Unknown error");
}
return response2;
};
const response = await deploy2(manifest);
if (!response.structuredContent || typeof response.structuredContent !== "object") {
console.error("\u274C Deployment failed: Invalid response structure");
console.error("Response:", JSON.stringify(response, null, 2));
throw new Error("Deployment response missing structuredContent");
}
const structuredContent = response.structuredContent;
const hosts = structuredContent.hosts;
if (!hosts || !Array.isArray(hosts) || hosts.length === 0) {
console.error("\u274C Deployment failed: No hosts returned in response");
console.error("Response:", JSON.stringify(response, null, 2));
throw new Error("Deployment response missing hosts array");
}
console.log(`
\u{1F389} Deployed! Available at:`);
hosts.forEach((host) => console.log(` ${host}`));
console.log();
const previewUrl = promote ? null : hosts[0];
if (process28.env.GITHUB_OUTPUT && previewUrl) {
await promises.appendFile(
process28.env.GITHUB_OUTPUT,
`preview_url=${previewUrl}
`
);
}
};
var listApps = async ({ workspace }) => {
console.log(`\u{1F50D} Listing apps in workspace '${workspace}'...`);
const client = await createWorkspaceClient({ workspace });
const response = await client.callTool(
{
name: "HOSTING_APPS_LIST",
arguments: {}
},
// @ts-expect-error We need to refactor HOSTING_APPS_LIST to stop returning array and use a proper object
z.any()
);
if (response.isError && Array.isArray(response.content)) {
throw new Error(response.content[0]?.text ?? "Unknown error");
}
const apps = response.structuredContent;
if (apps.length === 0) {
console.log("\u{1F4ED} No apps found in this workspace.");
} else {
console.log("\u{1F4F1} Apps in workspace:");
apps.forEach((app) => {
console.log(
` \u2022 ${app.slug} (${app.entrypoint}, Files: ${app.files.length})`
);
});
}
};
var promoteApp = async ({
workspace,
local,
appSlug,
deploymentId,
routePattern,
skipConfirmation = false
}) => {
const client = await createWorkspaceClient({ workspace, local });
let selectedAppSlug = appSlug;
if (!selectedAppSlug) {
console.log(`\u{1F50D} Fetching published apps in workspace '${workspace}'...`);
const publishedAppsResponse = await client.callTool({
name: "REGISTRY_LIST_PUBLISHED_APPS",
arguments: {}
});
if (publishedAppsResponse.isError && Array.isArray(publishedAppsResponse.content)) {
throw new Error(
publishedAppsResponse.content[0]?.text ?? "Failed to list published apps"
);
}
const { apps } = publishedAppsResponse.structuredContent;
if (apps.length === 0) {
console.log("\u{1F4ED} No published apps found in this workspace.");
return;
}
if (apps.length === 1) {
selectedAppSlug = apps[0].name;
console.log(
`\u{1F4E6} Using app: ${selectedAppSlug} (${apps[0].friendlyName || apps[0].name})`
);
} else {
const appOptions = apps.map((app2) => ({
name: `${app2.name}${app2.friendlyName ? ` (${app2.friendlyName})` : ""}${app2.description ? ` - ${app2.description}` : ""}`,
value: app2.name
}));
const { selectedApp } = await inquirer6.prompt([
{
type: "list",
name: "selectedApp",
message: "Select app to promote:",
choices: appOptions
}
]);
selectedAppSlug = selectedApp;
}
}
console.log(
`\u{1F680} Promoting deployment for app '${selectedAppSlug}' in workspace '${workspace}'...`
);
const deploymentsResponse = await client.callTool({
name: "HOSTING_APP_DEPLOYMENTS_LIST",
arguments: { appSlug: selectedAppSlug }
});
if (deploymentsResponse.isError && Array.isArray(deploymentsResponse.content)) {
throw new Error(
deploymentsResponse.content[0]?.text ?? "Failed to list deployments"
);
}
const { deployments, app } = deploymentsResponse.structuredContent;
if (deployments.length === 0) {
console.log("\u{1F4ED} No deployments found for this app.");
return;
}
let selectedDeploymentId = deploymentId;
if (!selectedDeploymentId) {
if (deployments.length === 1) {
selectedDeploymentId = deployments[0].id;
console.log(
`\u{1F4E6} Using deployment: ${selectedDeploymentId} (${deployments[0].entrypoint})`
);
} else {
const deploymentOptions = deployments.map((dep) => ({
name: `${dep.id} - ${new Date(
dep.created_at
).toLocaleString()} (${dep.entrypoint})`,
value: dep.id
}));
const { selectedDeployment: selectedDeployment2 } = await inquirer6.prompt([
{
type: "list",
name: "selectedDeployment",
message: "Select deployment to promote:",
choices: deploymentOptions
}
]);
selectedDeploymentId = selectedDeployment2;
}
}
const selectedDeployment = deployments.find(
(d) => d.id === selectedDeploymentId
);
if (!selectedDeployment) {
throw new Error(`Deployment ${selectedDeploymentId} not found`);
}
let selectedRoutePattern = routePattern;
if (!selectedRoutePattern) {
const { routeInput } = await inquirer6.prompt([
{
type: "input",
name: "routeInput",
message: "Enter route pattern to promote to:",
default: `${selectedAppSlug}.deco.page`,
validate: (input) => {
const trimmed = input.trim();
if (!trimmed) return "Route pattern is required";
return true;
}
}
]);
selectedRoutePattern = routeInput;
}
if (!skipConfirmation) {
console.log("\n\u{1F4CB} Promotion Summary:");
console.log(` App: ${app.slug}`);
console.log(` Deployment: ${selectedDeploymentId}`);
console.log(` Entrypoint: ${selectedDeployment.entrypoint}`);
console.log(` Route Pattern: ${selectedRoutePattern}`);
const { confirmed } = await inquirer6.prompt([
{
type: "confirm",
name: "confirmed",
message: "Do you want to proceed with the promotion?",
default: true
}
]);
if (!confirmed) {
console.log("\u274C Promotion cancelled.");
return;
}
}
try {
const promoteResponse = await client.callTool({
name: "HOSTING_APPS_PROMOTE",
arguments: {
deploymentId: selectedDeploymentId,
routePattern: selectedRoutePattern
}
});
if (promoteResponse.isError && Array.isArray(promoteResponse.content)) {
throw new Error(
promoteResponse.content[0]?.text ?? "Failed to promote deployment"
);
}
const result = promoteResponse.structuredContent;
if (result.success) {
console.log("\u2705 Deployment promoted successfully!");
console.log(`\u{1F310} Route updated: ${result.promotedRoute}`);
console.log("\u{1F9F9} Route cache purged");
} else {
throw new Error("Promotion failed");
}
} catch (error) {
console.error("\u274C Failed to promote deployment:", error);
throw error;
}
};
// package.json
var package_default = {
name: "deco-cli",
version: "0.28.6",
description: "CLI for managing decocms.com apps & projects"};
// src/lib/package-info.ts
var packageInfo = {
name: package_default.name,
version: package_default.version,
description: package_default.description
};
// src/lib/banner.ts
function displayBanner() {
const deco = chalk5.green(`
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557
\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
`);
const subtitle = chalk5.gray("Creating Deco project");
const version = chalk5.dim(`CLI v${packageInfo.version}`);
console.log(deco);
console.log(` ${subtitle}`);
console.log(` ${version}`);
console.log("");
}
var __filename2 = fileURLToPath(import.meta.url);
var __dirname2 = dirname(__filename2);
var DEFAULT_TEMPLATE = {
name: "Deco MCP app",
description: "A Deco MCP app",
repo: "deco-cx/deco-create",
branch: "main",
pathsToIgnore: []
};
function runCommand(command, args, cwd) {
return new Promise((resolve3) => {
const process29 = spawn(command, args, {
cwd,
stdio: "pipe"
});
process29.on("close", (code) => {
resolve3(code === 0);
});
process29.on("error", () => {
resolve3(false);
});
});
}
var PATHS_TO_IGNORE_ALWAYS = [".git"];
async function downloadTemplate(template, targetDir) {
if (template.name === "base") {
const templatePath = join(__dirname2, "../../../template/base");
await ensureDir(targetDir);
await copy(templatePath, targetDir, { overwrite: true });
console.log(`\u2705 Template '${template.name}' copied successfully!`);
return;
}
const tempDir = join(process28.cwd(), `.temp-${Date.now()}`);
try {
const success = await runCommand("git", [
"clone",
"--depth",
"1",
"--branch",
template.branch || "main",
`https://github.com/${template.repo}.git`,
tempDir
]);
if (!success) {
throw new Error(`Failed to clone template repository: ${template.repo}`);
}
const pathsToIgnore = [
...template.pathsToIgnore || [],
...PATHS_TO_IGNORE_ALWAYS
];
for (const path4 of pathsToIgnore) {
try {
const pathToRemove = join(tempDir, path4);
const isDirectory = await promises.stat(pathToRemove).then((stat) => stat.isDirectory());
await promises.rm(
pathToRemove,
isDirectory ? { recursive: true, force: true } : { force: true }
);
} catch {
console.warn(`Failed to remove ${path4} from the original template`);
}
}
const templatePath = join(tempDir, template.path || "");
try {
await promises.access(templatePath);
} catch {
throw new Error(`Template '${template.name}' not found in repository`);
}
await ensureDir(targetDir);
await copy(templatePath, targetDir, { overwrite: true });
console.log(`\u2705 Template '${template.name}' downloaded successfully!`);
} finally {
await promises.rm(tempDir, { recursive: true, force: true }).catch(() => {
});
}
}
async function customizeTemplate({
targetDir,
projectName,
workspace,
wranglerRoot
}) {
const packageJsonPath = join(targetDir, "package.json");
try {
const packageJsonContent = await promises.readFile(packageJsonPath, "utf-8");
const packageJson = JSON.parse(packageJsonContent);
packageJson.name = projectName;
await promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
} catch (error) {
console.warn(
"\u26A0\uFE0F Could not customize package.json:",
error instanceof Error ? error.message : String(error)
);
}
if (workspace) {
try {
const currentConfig = await readWranglerConfig(wranglerRoot || targetDir);
const bindings = currentConfig.deco?.bindings || [];
const newConfig = {
...currentConfig,
name: projectName,
scope: workspace,
deco: {
...currentConfig.deco,
workspace,
bindings
}
};
await writeWranglerConfig(newConfig, wranglerRoot || targetDir);
const envContent = await genEnv({
workspace,
local: false,
bindings: newConfig.deco.bindings || []
});
const outputPath = join(wranglerRoot || targetDir, "deco.gen.ts");
await promises.writeFile(outputPath, envContent);
console.log(`\u2705 Environment types written to: ${outputPath}`);
} catch (error) {
console.warn(
"\u26A0\uFE0F Could not update config file:",
error instanceof Error ? error.message : String(error)
);
}
}
}
async function createCommand(projectName, config = {}) {
try {
console.clear();
displayBanner();
let session = await readSession();
if (!session) {
console.log("\u{1F510} No session found. Starting authentication process...");
try {
await loginCommand();
console.log("\u2705 Successfully logged in to admin.decocms.com");
session = await readSession();
} catch (error) {
console.error(
"\u274C Login failed:",
error instanceof Error ? error.message : String(error)
);
console.warn(
"\u26A0\uFE0F Continuing without authentication. You can run 'deco login' later for a better experience."
);
}
}
const selectedTemplate = DEFAULT_TEMPLATE;
const finalProjectName = slugify(
projectName || (await inquirer6.prompt([
{
type: "input",
name: "projectName",
message: "Enter project name:",
validate: (value) => {
if (!value.trim()) {
return "Project name cannot be empty";
}
if (!/^[a-z0-9-]+$/.test(value)) {
return "Project name can only contain lowercase letters, numbers, and hyphens";
}
return true;
}
}
])).projectName
);
let workspace = config?.workspace;
if (session) {
try {
workspace = await promptWorkspace(config?.local, workspace);
console.log(`\u{1F4C1} Selected workspace: ${workspace}`);
} catch (error) {
console.error(
"\u274C Failed to select workspace:",
error instanceof Error ? error.message : String(error)
);
console.warn(
"\u26A0\uFE0F Could not select workspace. Continuing without workspace selection."
);
}
} else {
console.log(
"\u26A0\uFE0F No authentication session - skipping workspace selection"
);
}
const targetDir = join(process28.cwd(), finalProjectName);
try {
await promises.access(targetDir);
const { overwrite } = await inquirer6.prompt([
{
type: "list",
name: "overwrite",
message: `Directory '${finalProjectName}' already exists. Overwrite?`,
choices: ["No", "Yes"]
}
]);
if (overwrite === "No") {
console.log("\u274C Project creation cancelled.");
return;
}
await promises.rm(targetDir, { recursive: true });
} catch {
}
const wranglerRoot = join(targetDir, selectedTemplate.wranglerRoot || "");
const { initGit } = await inquirer6.prompt([
{
type: "list",
name: "initGit",
message: "Initialize a git repository?",
choices: ["No", "Yes"]
}
]);
const mcpResult = workspace ? await promptIDESetup({ workspace, app: finalProjectName }, targetDir) : null;
console.log(`\u{1F4E6} Downloading template '${selectedTemplate.name}'...`);
await downloadTemplate(selectedTemplate, targetDir);
if (mcpResult) {
await writeIDEConfig(mcpResult);
}
await customizeTemplate({
targetDir,
projectName: finalProjectName,
workspace,
wranglerRoot
});
if (initGit === "Yes") {
try {
const success = await runCommand("git", ["init"], targetDir);
if (success) {
console.log(`\u2705 Git repository initialized in '${finalProjectName}'`);
} else {
console.warn("\u26A0\uFE0F Failed to initialize git repository");
}
} catch (error) {
console.warn(
"\u26A0\uFE0F Could not initialize git repository:",
error instanceof Error ? error.message : String(error)
);
}
}
console.log(`
\u{1F389} Project '${finalProjectName}' created successfully!`);
console.log(`
Next steps:`);
console.log(` cd ${finalProjectName}`);
console.log(` npm install`);
console.log(` npm run dev`);
} catch (error) {
console.error(
"\u274C Failed to create project:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
function copyToClipboard(text) {
try {
let command;
let args = [];
switch (process28.platform) {
case "darwin":
command = "pbcopy";
break;
case "win32":
command = "clip";
break;
case "linux":
command = "xclip";
args = ["-selection", "clipboard"];
break;
default:
return Promise.resolve(false);
}
return new Promise((resolve3) => {
const clipProcess = spawn(command, args, { stdio: "pipe" });
clipProcess.stdin.write(text);
clipProcess.stdin.end();
clipProcess.on("close", (code) => {
resolve3(code === 0);
});
clipProcess.on("error", () => {
resolve3(false);
});
});
} catch {
return Promise.resolve(false);
}
}
async function findRunningAddr(port) {
const LOCALHOST_ENDPOINTS = ["localhost", "127.0.0.1", "0.0.0.0"];
for (const endpoint of LOCALHOST_ENDPOINTS) {
try {
const server = createServer$1();
await new Promise((resolve3, reject) => {
server.listen(port, endpoint, () => {
server.close();
resolve3();
});
server.on("error", reject);
});
} catch {
return endpoint;
}
}
return null;
}
async function waitForPort(port) {
let addr = await findRunningAddr(port);
if (addr) {
return addr;
}
console.log(chalk5.yellow(`Waiting for port ${port} to become available...`));
while (!addr) {
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
addr = await findRunningAddr(port);
}
console.log(chalk5.green(`Port ${port} is now available!`));
return addr;
}
async function monitorPortAvailability(port) {
while (true) {
const isAvailable = await findRunningAddr(port);
if (!isAvailable) {
console.log(chalk5.red(`\u26A0\uFE0F Warning: Port ${port} is no longer available!`));
}
await new Promise((resolve3) => setTimeout(resolve3, 2e3));
}
}
async function register(port, domain, onBeforeRegister) {
const server = `wss://${domain}`;
const serverUrl = `https://${domain}`;
try {
monitorPortAvailability(port).catch((err) => {
console.error("Port monitoring error:", err);
});
onBeforeRegister?.(serverUrl);
const host = await waitForPort(port);
const localAddr = `http://${host}:${port}`;
const tunnel = await connect({
domain,
localAddr,
server,
apiKey: process28.env.DECO_TUNNEL_SERVER_TOKEN ?? "c309424a-2dc4-46fe-bfc7-a7c10df59477"
});
await tunnel.registered;
const copied = await copyToClipboard(serverUrl);
console.log(
`
Tunnel started
-> \u{1F310} ${chalk5.bold("Preview")}: ${chalk5.cyan(
serverUrl
)}${copied ? chalk5.dim(" (copied to clipboard)") : ""}`
);
await tunnel.closed;
} catch (err) {
console.log("Tunnel connection error, retrying in 500ms...", err);
await new Promise((resolve3) => setTimeout(resolve3, 500));
return register(port, domain);
}
}
var link = async ({
port = 8787,
onBeforeRegister
} = {}) => {
const config = await getConfig({});
const wranglerConfig = await readWranglerConfig();
const app = typeof wranglerConfig.name === "string" ? wranglerConfig.name : "my-app";
const appDomain = getAppDomain(config.workspace, app);
await register(port, appDomain, onBeforeRegister);
};
async function devCommand(opts) {
try {
await ensureDevEnvironment(opts);
const commandOverride = opts.command ?? ["wrangler", "dev"];
const _config = await getConfig().catch(() => ({
workspace: "default",
bindings: [],
local: false,
enable_workflows: true
}));
const wranglerConfig = await readWranglerConfig();
const app = typeof wranglerConfig.name === "string" ? wranglerConfig.name : "my-app";
console.log(chalk5.gray(`Starting development server for '${app}'...`));
if (opts.genWatch) {
const watchPath = resolve(opts.genWatch);
console.log(
chalk5.gray(
`Setting up file watcher for TypeScript files in: ${watchPath}`
)
);
let isGenerating = false;
const debounceMs = 2500;
let debounceTimer = null;
const generateTypes = async () => {
if (isGenerating) return;
isGenerating = true;
try {
console.log(
chalk5.gray("TypeScript file changed, regenerating deco.gen.ts...")
);
const config = await getConfig();
const wranglerConfig2 = await readWranglerConfig();
const env = await genEnv({
workspace: config.workspace,
local: config.local,
bindings: config.bindings,
selfUrl: `https://${getAppDomain(
config.workspace,
wranglerConfig2.name ?? "my-app"
)}/mcp`
});
const outputPath = join(process28.cwd(), "deco.gen.ts");
await writeFile(outputPath, env);
console.log(chalk5.blue(`Generated types written to: ${outputPath}`));
} catch (error) {
console.error(
chalk5.red("Failed to generate types:"),
error instanceof Error ? error.message : String(error)
);
} finally {
isGenerating = false;
}
};
await generateTypes();
const watcher = watch(
watchPath,
{ recursive: true },
(_eventType, filename) => {
if (filename && filename.endsWith(".ts") && !filename.endsWith(".gen.ts")) {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(generateTypes, debounceMs);
}
}
);
const cleanupWatcher = () => {
console.log(chalk5.yellow("\nStopping file watcher..."));
watcher.close();
};
process28.on("SIGINT", cleanupWatcher);
process28.on("SIGTERM", cleanupWatcher);
}
console.log(chalk5.gray("Starting development server with tunnel..."));
await link({
port: 8787,
onBeforeRegister: () => {
console.log(chalk5.gray("Starting Wrangler development server..."));
const command = commandOverride ?? ["wrangler", "dev"];
const wranglerProcess = spawn("npx", command, {
stdio: "inherit",
shell: true
});
const cleanup = () => {
console.log(chalk5.yellow("\nStopping development server..."));
wranglerProcess.kill("SIGINT");
process28.exit(0);
};
process28.on("SIGINT", cleanup);
process28.on("SIGTERM", cleanup);
wranglerProcess.on("error", (error) => {
console.error(chalk5.red("Failed to start Wrangler:"), error.message);
process28.exit(1);
});
return wranglerProcess;
}
});
} catch (error) {
console.error(
chalk5.red("Development server failed:"),
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
function detectRuntime() {
if (typeof globalThis.Deno !== "undefined") return "deno";
if (process28.versions.bun) return "bun";
if (process28.versions.node) return "node";
return "unknown";
}
// src/commands/update/upgrade.ts
var getPackageJson = async () => {
return packageInfo;
};
var getLatestVersion = async (packageName) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5e3);
try {
const response = await fetch(
`https://registry.npmjs.org/${packageName}/latest`,
{
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Failed to fetch latest version: ${response.statusText}`);
}
const data = await response.json();
return data.version;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
throw new Error("Request timed out while checking for updates");
}
throw error;
}
};
var getInstallCommand = (runtime, packageName) => {
switch (runtime) {
case "bun":
return ["bun", ["install", "-g", packageName]];
case "deno":
return ["deno", ["install", "-Ar", "-g", "-f", `npm:${packageName}`]];
case "node":
return ["npm", ["install", "-g", packageName]];
case "unknown":
default:
console.log(chalk5.yellow("\u26A0\uFE0F Unknown runtime, falling back to npm"));
return ["npm", ["install", "-g", packageName]];
}
};
var upgrade = (packageName) => {
console.log(chalk5.yellow("\u{1F504} Upgrading to the latest version..."));
const runtime = detectRuntime();
console.log(chalk5.gray(`Detected runtime: ${runtime}`));
const [command, args] = getInstallCommand(runtime, packageName);
return new Promise((resolve3, reject) => {
const child = spawn(command, args, {
stdio: "inherit",
shell: true
});
child.on("close", (code) => {
if (code === 0) {
console.log(chalk5.green("\u{1F389} CLI updated successfully!"));
console.log(
chalk5.blue(
"Please restart your terminal or run 'deco --version' to verify."
)
);
resolve3();
} else {
console.error(chalk5.red("\u274C Failed to update the CLI."));
reject(
new Error(
`${command} ${args.join(" ")} failed with exit code ${code}`
)
);
}
});
child.on("error", (error) => {
console.error(chalk5.red("\u274C Failed to update the CLI."));
reject(error);
});
});
};
async function upgradeCommand() {
try {
const packageJson = await getPackageJson();
const currentVersion = packageJson.version;
console.log(chalk5.blue(`Current version: v${currentVersion}`));
console.log(chalk5.blue("Checking for updates..."));
const latestVersion = await getLatestVersion(packageJson.name);
if (semver.gt(latestVersion, currentVersion)) {
console.log(
chalk5.green(
`\u{1F4E6} New version available: ${chalk5.bold(`v${latestVersion}`)}`
)
);
const { confirmed } = await inquirer6.prompt([
{
type: "confirm",
name: "confirmed",
message: `Update from v${currentVersion} to v${latestVersion}?`,
default: true
}
]);
if (confirmed) {
await upgrade(packageJson.name);
} else {
console.log(chalk5.gray("Update cancelled."));
}
} else if (semver.eq(latestVersion, currentVersion)) {
console.log(
chalk5.green("\u2705 You are already running the latest version!")
);
} else {
console.log(chalk5.blue("\u2139\uFE0F You are running a development version."));
}
} catch (error) {
console.error(
chalk5.red("\u274C Failed to check for updates:"),
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
var DECO_DEPENDENCIES = [
"@deco/workers-runtime",
"@decocms/runtime"
];
var DECO_DEV_DEPENDENCIES = ["deco-cli"];
var getLatestVersion2 = async (packageName) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1e4);
try {
if (packageName === "@deco/workers-runtime") {
const response2 = await fetch(
"https://jsr.io/@deco/workers-runtime/meta.json",
{
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (response2.ok) {
const data2 = await response2.json();
return data2.latest;
}
}
const response = await fetch(
`https://registry.npmjs.org/${packageName}/latest`,
{
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Failed to fetch latest version: ${response.statusText}`);
}
const data = await response.json();
return data.version;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
throw new Error("Request timed out while checking for updates");
}
throw error;
}
};
var parseCurrentVersion = (versionString) => {
if (versionString.startsWith("npm:@jsr/")) {
const match = versionString.match(/@([^@]+)$/);
return match ? match[1] : versionString;
}
return versionString.replace(/^[\^~]/, "");
};
var formatVersionForPackageJson = (packageName, version) => {
if (packageName === "@deco/workers-runtime") {
return `npm:@jsr/deco__workers-runtime@${version}`;
}
return `^${version}`;
};
var discoverWorkspaces = async (rootPath) => {
const rootPackageJsonPath = resolve(rootPath, "package.json");
if (!existsSync(rootPackageJsonPath)) {
throw new Error("No package.json found in the current directory");
}
const rootPackageJson = JSON.parse(
await readFile(rootPackageJsonPath, "utf-8")
);
if (!rootPackageJson.workspaces) {
return [rootPackageJsonPath];
}
const packageJsonPaths = [rootPackageJsonPath];
for (const workspacePattern of rootPackageJson.workspaces) {
const workspaceDirs = await glob(workspacePattern, {
cwd: rootPath
});
for (const dir of workspaceDirs) {
const packageJsonPath = resolve(rootPath, dir, "package.json");
if (existsSync(packageJsonPath)) {
packageJsonPaths.push(packageJsonPath);
}
}
}
return packageJsonPaths;
};
var findPackageJsons = async (cwd) => {
return await discoverWorkspaces(cwd);
};
var checkForUpdates = async (packageJsonPath) => {
const packageJsonContent = await readFile(packageJsonPath, "utf-8");
const packageJson = JSON.parse(packageJsonContent);
const updates = [];
if (packageJson.dependencies) {
for (const depName of DECO_DEPENDENCIES) {
const currentVersionString = packageJson.dependencies[depName];
if (currentVersionString) {
try {
const currentVersion = parseCurrentVersion(currentVersionString);
const latestVersion = await getLatestVersion2(depName);
if (currentVersion !== latestVersion) {
updates.push({
name: depName,
currentVersion,
latestVersion,
isDev: false,
packagePath: packageJsonPath
});
}
} catch (error) {
console.warn(
chalk5.yellow(
`\u26A0\uFE0F Failed to check updates for ${depName}: ${error instanceof Error ? error.message : String(error)}`
)
);
}
}
}
}
if (packageJson.devDependencies) {
for (const depName of DECO_DEV_DEPENDENCIES) {
const currentVersionString = packageJson.devDependencies[depName];
if (currentVersionString) {
try {
const currentVersion = parseCurrentVersion(currentVersionString);
const latestVersion = await getLatestVersion2(depName);
if (currentVersion !== latestVersion) {
updates.push({
name: depName,
currentVersion,
latestVersion,
isDev: true,
packagePath: packageJsonPath
});
}
} catch (error) {
console.warn(
chalk5.yellow(
`\u26A0\uFE0F Failed to check updates for ${depName}: ${error instanceof Error ? error.message : String(error)}`
)
);
}
}
}
}
return updates;
};
var checkAllPackagesForUpdates = async (packageJsonPaths) => {
const allUpdates = [];
for (const packageJsonPath of packageJsonPaths) {
try {
const updates = await checkForUpdates(packageJsonPath);
allUpdates.push(...updates);
} catch (error) {
console.warn(
chalk5.yellow(
`\u26A0\uFE0F Failed to check updates for ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`
)
);
}
}
return allUpdates;
};
var applyUpdates = async (updates) => {
const updatesByPackage = /* @__PURE__ */ new Map();
for (const update2 of updates) {
if (!updatesByPackage.has(update2.packagePath)) {
updatesByPackage.set(update2.packagePath, []);
}
updatesByPackage.get(update2.packagePath).push(update2);
}
for (const [packageJsonPath, packageUpdates] of updatesByPackage) {
const packageJsonContent = await readFile(packageJsonPath, "utf-8");
const packageJson = JSON.parse(packageJsonContent);
for (const update2 of packageUpdates) {
const newVersionString = formatVersionForPackageJson(
update2.name,
update2.latestVersion
);
if (update2.isDev && packageJson.devDependencies) {
packageJson.devDependencies[update2.name] = newVersionString;
} else if (!update2.isDev && packageJson.dependencies) {
packageJson.dependencies[update2.name] = newVersionString;
}
const relativePath = packageJsonPath.replace(process28.cwd(), ".");
console.log(
chalk5.green(
`\u2705 Updated ${update2.name} in ${relativePath}: ${update2.currentVersion} \u2192 ${update2.latestVersion}`
)
);
}
await writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + "\n"
);
}
};
async function updateCommand(options) {
try {
const cwd = process28.cwd();
console.log(chalk5.blue("\u{1F50D} Searching for Deco dependencies to update..."));
const packageJsonPaths = await findPackageJsons(cwd);
if (packageJsonPaths.length === 1) {
console.log(chalk5.gray(`Found package.json at: ${packageJsonPaths[0]}`));
} else {
console.log(
chalk5.gray(
`Found ${packageJsonPaths.length} package.json files in workspace:`
)
);
for (const path4 of packageJsonPaths) {
const relativePath = path4.replace(cwd, ".");
console.log(chalk5.gray(` - ${relativePath}`));
}
}
const updates = await checkAllPackagesForUpdates(packageJsonPaths);
if (updates.length === 0) {
console.log(chalk5.green("\u2705 All Deco dependencies are up to date!"));
return;
}
console.log();
console.log(chalk5.yellow("\u{1F4E6} Available updates:"));
for (const update2 of updates) {
const depType = update2.isDev ? "(dev)" : "";
const relativePath = update2.packagePath.replace(cwd, ".");
console.log(
chalk5.blue(
` ${update2.name} ${depType} in ${relativePath}: ${update2.currentVersion} \u2192 ${update2.latestVersion}`
)
);
}
console.log();
let confirmed = options.yes || false;
if (!confirmed) {
const response = await inquirer6.prompt([
{
type: "confirm",
name: "confirmed",
message: `Update ${updates.length} Deco ${updates.length === 1 ? "dependency" : "dependencies"}?`,
default: true
}
]);
confirmed = response.confirmed;
}
if (!confirmed) {
console.log(chalk5.gray("Update cancelled."));
return;
}
console.log(chalk5.yellow("\u{1F504} Updating dependencies..."));
await applyUpdates(updates);
console.log();
console.log(chalk5.green("\u{1F389} Dependencies updated successfully!"));
console.log(
chalk5.blue(
"\u{1F4A1} Don't forget to run your package manager to install the new versions."
)
);
} catch (error) {
console.error(
chalk5.red("\u274C Failed to update dependencies:"),
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
async function addCommand({ workspace, local }) {
try {
const session = await readSession();
if (!session) {
console.error("\u274C No session found. Please run 'deco login' first.");
return;
}
const config = await getConfig({
inlineOptions: { workspace, local }
}).catch(() => ({
workspace: workspace || session.workspace || "default",
bindings: [],
local: local || false,
enable_workflows: true
}));
console.log(`\u{1F4C1} Using workspace: ${config.workspace}`);
console.log("\u{1F50D} Fetching available integrations...");
const selectedBindings = await promptIntegrations(
config.local,
config.workspace
);
if (selectedBindings.length === 0) {
console.log("\u2139\uFE0F No integrations selected. Nothing to add.");
return;
}
console.log(`\u2705 Selected ${selectedBindings.length} integration(s)`);
const newBindings = [];
for (const binding of selectedBindings) {
const integrationBinding = binding;
const { bindingName } = await inquirer6.prompt([
{
type: "input",
name: "bindingName",
message: `Enter binding name for integration "${integrationBinding.integration_id}":`,
default: integrationBinding.name,
validate: (value) => {
if (!value.trim()) {
return "Binding name cannot be empty";
}
if (!/^[A-Z_][A-Z0-9_]*$/.test(value)) {
return "Binding name must be uppercase with underscores (e.g., MY_INTEGRATION)";
}
return true;
}
}
]);
newBindings.push({
name: bindingName,
type: "mcp",
integration_id: integrationBinding.integration_id
});
}
const currentWranglerConfig = await readWranglerConfig();
const currentBindings = currentWranglerConfig.deco?.bindings || [];
const allBindings = [...currentBindings, ...newBindings];
const updatedConfig = {
...config,
bindings: allBindings
};
await writeConfigFile(updatedConfig);
console.log(`\u2705 Added ${newBindings.length} integration(s) successfully!`);
newBindings.forEach((binding) => {
const id = "integration_id" in binding ? binding.integration_id : "integration_name" in binding ? binding.integration_name : "unknown";
console.log(` - ${binding.name} (${id})`);
});
console.log("\n\u{1F4A1} Run 'deco gen' to update your environment types.");
console.log("\u{1F389} Your integrations are ready to use!");
} catch (error) {
console.error(
"\u274C Failed to add integrations:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
inquirer6.registerPrompt("search-checkbox", inquirerSearchCheckbox);
async function promptRegistry(local = false, workspace = "") {
const session = await readSession();
if (!session) {
throw new Error("No session found. Please run 'deco login' first.");
}
const client = await createWorkspaceClient({ workspace, local });
try {
const response = await client.callTool(
{
name: "DECO_INTEGRATIONS_SEARCH",
arguments: { query: "" }
// Empty query returns all apps
},
// @ts-expect-error We need to refactor DECO_INTEGRATIONS_SEARCH to use a proper schema
z.any()
);
if (response.isError) {
throw new Error("Failed to fetch registry apps");
}
const registryResponse = response.structuredContent?.integrations;
const apps = (registryResponse || []).sort((a, b) => {
if (a.verified && !b.verified) return -1;
if (!a.verified && b.verified) return 1;
return a.name.localeCompare(b.name);
});
if (!apps || apps.length === 0) {
throw new Error("No registry apps found.");
}
const options = apps.map((app) => ({
name: `${app.friendlyName || app.name}${app.verified ? " \u2713" : ""} - ${app.description || "No description"}`,
value: app.appName,
short: app.friendlyName || app.name
}));
const { selectedAppNames } = await inquirer6.prompt([
{
type: "search-checkbox",
name: "selectedAppNames",
message: "Select apps from registry (use space to select, enter to confirm):",
choices: options,
searchable: true,
highlight: true,
searchText: "Type to search registry apps:",
emptyText: "No apps found matching your search."
}
]);
const selectedApps = apps.filter(
(app) => selectedAppNames.includes(app.appName)
);
return selectedApps.map(({ friendlyName, name, appName }) => ({
name: sanitizeConstantName(friendlyName || name),
type: "mcp",
integration_name: appName
}));
} finally {
await client.close();
}
}
async function addRegistryCommand({
workspace,
local,
appName,
skipGen = false
}) {
try {
const session = await readSession();
if (!session) {
console.error("\u274C No session found. Please run 'deco login' first.");
return;
}
const config = await getConfig({
inlineOptions: { workspace, local }
}).catch(() => ({
workspace: workspace || session.workspace || "default",
bindings: [],
local: local || false,
enable_workflows: true
}));
console.log(`\u{1F4C1} Using workspace: ${config.workspace}`);
let selectedBindings;
if (appName) {
console.log(`\u{1F50D} Searching registry for "${appName}"...`);
const apiClient = await createWorkspaceClientStub({
local: config.local
});
const response = await apiClient.callTool({
name: "REGISTRY_GET_APP",
arguments: { name: appName }
});
if (response.isError) {
const errorText = response.content?.[0]?.text ?? `App "${appName}" not found in registry`;
throw new Error(errorText);
}
const app = response.structuredContent;
selectedBindings = [
{
name: sanitizeConstantName(app.friendlyName || app.name),
type: "mcp",
integration_name: app.appName
}
];
console.log(`\u2705 Found app: ${app.friendlyName || app.name}`);
} else {
console.log("\u{1F50D} Searching registry for available apps...");
selectedBindings = await promptRegistry(config.local, config.workspace);
if (selectedBindings.length === 0) {
console.log("\u2139\uFE0F No apps selected. Nothing to add.");
return;
}
console.log(
`\u2705 Selected ${selectedBindings.length} app(s) from registry`
);
}
const newBindings = [];
for (const binding of selectedBindings) {
const registryBinding = binding;
const { bindingName } = await inquirer6.prompt([
{
type: "input",
name: "bindingName",
message: `Enter binding name for app "${registryBinding.integration_name}":`,
default: registryBinding.name,
validate: (value) => {
if (!value.trim()) {
return "Binding name cannot be empty";
}
if (!/^[A-Z_][A-Z0-9_]*$/.test(value)) {
return "Binding name must be uppercase with underscores (e.g., MY_APP)";
}
return true;
}
}
]);
newBindings.push({
name: bindingName,
type: "mcp",
integration_name: registryBinding.integration_name
});
}
const currentWranglerConfig = await readWranglerConfig();
const currentBindings = currentWranglerConfig.deco?.bindings || [];
const allBindings = [...currentBindings, ...newBindings];
const updatedConfig = {
...config,
bindings: allBindings
};
await writeConfigFile(updatedConfig);
console.log(`\u2705 Added ${newBindings.length} app(s) successfully!`);
newBindings.forEach((binding) => {
const id = "integration_id" in binding ? binding.integration_id : "integration_name" in binding ? binding.integration_name : "unknown";
console.log(` - ${binding.name} (${id})`);
});
if (!skipGen) {
console.log("\n\u{1F504} Generating environment types...");
try {
const wranglerConfig = await readWranglerConfig();
const env = await genEnv({
workspace: config.workspace,
local: config.local,
bindings: updatedConfig.bindings,
selfUrl: `https://${getAppDomain(
config.workspace,
wranglerConfig.name ?? "my-app"
)}/mcp`
});
const DEFAULT_OUTPUT_PATH = "shared/deco.gen.ts";
await writeFile(DEFAULT_OUTPUT_PATH, env);
console.log(`\u2705 Types generated at ${DEFAULT_OUTPUT_PATH}`);
} catch (genError) {
console.warn(
"\u26A0\uFE0F Failed to generate types:",
genError instanceof Error ? genError.message : String(genError)
);
console.log("\u{1F4A1} Run 'deco gen' manually to generate types.");
}
} else {
console.log("\n\u{1F4A1} Run 'deco gen' to update your environment types.");
}
console.log("\u{1F389} Your apps are ready to use!");
} catch (error) {
console.error(
"\u274C Failed to add registry apps:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
async function autocompleteIntegrations(partial) {
try {
const config = await getConfig({}).catch(() => ({
workspace: void 0,
local: false
}));
if (!config.workspace) return [];
const client = await createWorkspaceClient({
workspace: config.workspace,
local: config.local
});
const response = await client.callTool({
name: "INTEGRATIONS_LIST",
arguments: {}
});
if (response.isError || !response.structuredContent) return [];
const integrations = response.structuredContent?.items || [];
return integrations.map((integration) => integration.id).filter((id) => id.toLowerCase().includes(partial.toLowerCase())).sort();
} catch {
return [];
}
}
async function autocompleteTools(partial, options) {
try {
if (!options.integration) return [];
const config = await getConfig({}).catch(() => ({
workspace: void 0,
local: false
}));
if (!config.workspace) return [];
const client = await createWorkspaceClient({
workspace: config.workspace,
local: config.local,
integrationId: options.integration
});
const toolsResponse = await client.listTools();
return toolsResponse.tools.map((tool) => tool.name).filter((name) => name.toLowerCase().includes(partial.toLowerCase())).sort();
} catch {
return [];
}
}
function parseSetArguments(setArgs) {
const result = {};
for (const arg of setArgs) {
const equalIndex = arg.indexOf("=");
if (equalIndex === -1) {
throw new Error(
`Invalid --set argument: ${arg}. Expected format: key=value`
);
}
const key = arg.slice(0, equalIndex);
const value = arg.slice(equalIndex + 1);
const keyParts = key.split(".");
let current = result;
for (let i = 0; i < keyParts.length - 1; i++) {
const part = keyParts[i];
if (!(part in current)) {
current[part] = {};
}
current = current[part];
}
const finalKey = keyParts[keyParts.length - 1];
try {
current[finalKey] = JSON.parse(value);
} catch {
current[finalKey] = value;
}
}
return result;
}
async function callToolCommand(toolName, options) {
try {
const config = await getConfig({
inlineOptions: { workspace: options.workspace }
});
if (!config.workspace) {
throw new Error("No workspace configured. Run 'deco configure' first.");
}
let payload = {};
if (options.payload) {
try {
payload = JSON.parse(options.payload);
} catch (error) {
throw new Error(
`Invalid JSON payload: ${error instanceof Error ? error.message : String(error)}`
);
}
}
if (options.set && options.set.length > 0) {
const setPayload = parseSetArguments(options.set);
payload = { ...payload, ...setPayload };
}
const client = await createWorkspaceClient({
workspace: config.workspace,
local: config.local,
integrationId: options.integration
});
const response = await client.callTool({
name: toolName,
arguments: payload
});
if (response.isError) {
const errorMessage = Array.isArray(response.content) ? response.content.map((c) => c.text).join("\n") : "Unknown error occurred";
throw new Error(errorMessage);
}
console.log(JSON.stringify(response.structuredContent, null, 2));
} catch (error) {
console.error(
"\u274C Tool call failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
// src/commands/completion/completion.ts
async function generateCompletions(options) {
const { current, previous, line } = options;
try {
const words = line.split(/\s+/);
const commandIndex = words.findIndex((word) => word === "call-tool");
if (commandIndex === -1) {
return;
}
const args = words.slice(commandIndex + 1);
const integrationIndex = args.findIndex(
(arg) => arg === "-i" || arg === "--integration"
);
const integration = integrationIndex !== -1 && integrationIndex + 1 < args.length ? args[integrationIndex + 1] : void 0;
let completions = [];
if (previous === "-i" || previous === "--integration") {
completions = await autocompleteIntegrations(current);
} else if (integration && !current.startsWith("-")) {
const toolIndex = args.findIndex(
(arg) => !arg.startsWith("-") && arg !== integration
);
if (toolIndex === -1 || args[toolIndex] === current) {
completions = await autocompleteTools(current, { integration });
}
} else if (!current.startsWith("-") && !integration) {
completions = [
"-i",
"--integration",
"-p",
"--payload",
"--set",
"-w",
"--workspace"
];
}
completions.forEach((completion2) => console.log(completion2));
} catch (error) {
console.error("Completion error:", error);
}
}
async function completionCommand(type, options) {
if (type === "call-tool") {
await generateCompletions({
current: options.current || "",
previous: options.previous || "",
line: options.line || ""
});
}
}
function generateBashCompletionScript() {
return `#!/bin/bash
# Deco CLI completion script for bash
_deco_completion() {
local cur prev words cword
_init_completion || return
# Check if we're completing for call-tool command
if [[ "\${words[*]}" == *"call-tool"* ]]; then
local line="\${COMP_LINE}"
local completions
# Call the deco CLI completion command
completions=$(deco completion call-tool --current="$cur" --previous="$prev" --line="$line" 2>/dev/null)
if [[ $? -eq 0 && -n "$completions" ]]; then
COMPREPLY=($(compgen -W "$completions" -- "$cur"))
return 0
fi
fi
# Default completion for other commands
case $prev in
-w|--workspace)
# Complete workspace names (could be enhanced to fetch from config)
return 0
;;
*)
# Complete with available subcommands and options
COMPREPLY=($(compgen -W "login logout whoami configure hosting dev add call-tool upgrade update link gen create" -- "$cur"))
;;
esac
}
# Register the completion function
complete -F _deco_completion deco
`;
}
function generateZshCompletionScript() {
return `#compdef deco
# Deco CLI completion script for zsh
_deco() {
local context state state_descr line
typeset -A opt_args
_arguments -C \\
'1: :_deco_commands' \\
'*:: :->args'
case \${words[1]} in
call-tool)
_deco_call_tool
;;
esac
}
_deco_commands() {
local commands
commands=(
'login:Log in to admin.decocms.com'
'logout:Log out and remove session data'
'whoami:Print current session info'
'configure:Save configuration options'
'hosting:Manage hosting apps'
'dev:Start development server'
'add:Add integrations'
'call-tool:Call a tool on an integration'
'upgrade:Upgrade the CLI'
'update:Update dependencies'
'link:Link project to remote domain'
'gen:Generate environment'
'create:Create new project'
)
_describe 'commands' commands
}
_deco_call_tool() {
local context state state_descr line
typeset -A opt_args
_arguments -C \\
'-i[Integration ID]:integration:_deco_integrations' \\
'--integration[Integration ID]:integration:_deco_integrations' \\
'-p[JSON payload]:payload:' \\
'--payload[JSON payload]:payload:' \\
'--set[Set key=value]:keyvalue:' \\
'-w[Workspace name]:workspace:' \\
'--workspace[Workspace name]:workspace:' \\
'1: :_deco_tools'
}
_deco_integrations() {
local integrations
integrations=($(deco completion call-tool --current="$PREFIX" --previous="-i" --line="$BUFFER" 2>/dev/null))
_describe 'integrations' integrations
}
_deco_tools() {
local tools integration
# Extract integration from command line
if [[ "$words" == *"-i"* ]]; then
local i_index=\${words[(i)-i]}
if (( i_index < \${#words} )); then
integration=\${words[i_index+1]}
fi
elif [[ "$words" == *"--integration"* ]]; then
local int_index=\${words[(i)--integration]}
if (( int_index < \${#words} )); then
integration=\${words[int_index+1]}
fi
fi
if [[ -n "$integration" ]]; then
tools=($(deco completion call-tool --current="$PREFIX" --previous="" --line="$BUFFER" 2>/dev/null))
_describe 'tools' tools
fi
}
_deco "$@"
`;
}
async function installCompletionCommand(shell, options = {}) {
try {
const targetShell = shell || process28.env.SHELL?.split("/").pop() || "bash";
let script;
let filename;
let installPath;
switch (targetShell) {
case "bash": {
script = generateBashCompletionScript();
filename = "deco-completion.bash";
installPath = options.output || join(homedir(), ".local/share/bash-completion/completions/deco");
break;
}
case "zsh": {
script = generateZshCompletionScript();
filename = "_deco";
const zshDirs = [
join(homedir(), ".zsh/completions"),
"/usr/local/share/zsh/site-functions",
"/opt/homebrew/share/zsh/site-functions"
];
installPath = options.output || zshDirs[0];
break;
}
default: {
console.error(`\u274C Unsupported shell: ${targetShell}`);
console.log("Supported shells: bash, zsh");
process28.exit(1);
}
}
const finalPath = options.output || (targetShell === "zsh" ? join(installPath, filename) : installPath);
await ensureDir(dirname(finalPath));
await writeFile(finalPath, script, "utf8");
console.log(`\u2705 Completion script installed to: ${finalPath}`);
if (!options.output) {
switch (targetShell) {
case "bash":
console.log("");
console.log("To enable completions, add this to your ~/.bashrc:");
console.log(`source "${finalPath}"`);
break;
case "zsh":
console.log("");
console.log("To enable completions, add this to your ~/.zshrc:");
console.log(`fpath=("${dirname(finalPath)}" $fpath)`);
console.log("autoload -U compinit && compinit");
break;
}
console.log("");
console.log("Then restart your shell or run:");
console.log("source ~/.bashrc # for bash");
console.log("source ~/.zshrc # for zsh");
}
} catch (error) {
console.error(
"\u274C Failed to install completion:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
async function fetchFileContent(filePath, branchName, workspace, local) {
const client = await createWorkspaceClient({
workspace,
local
});
try {
const response = await client.callTool({
name: "READ_FILE",
arguments: {
branch: branchName,
path: filePath
}
});
if (response.isError) {
const errorMessage = Array.isArray(response.content) ? response.content[0]?.text || "Failed to read file" : "Failed to read file";
throw new Error(errorMessage);
}
const result = response.structuredContent;
if (!result || !result.content) {
throw new Error("No content returned from READ_FILE tool");
}
return Buffer.from(result.content, "base64");
} catch (error) {
console.error(
`\u274C Failed to fetch content for ${filePath}:`,
error instanceof Error ? error.message : String(error)
);
throw error;
} finally {
await client.close();
}
}
async function putFileContent(filePath, content, branchName, metadata, workspace, local) {
const client = await createWorkspaceClient({
workspace,
local
});
try {
const base64Content = Buffer.isBuffer(content) ? content.toString("base64") : Buffer.from(content).toString("base64");
const response = await client.callTool({
name: "PUT_FILE",
arguments: {
branch: branchName,
path: filePath,
content: { base64: base64Content },
metadata
}
});
if (response.isError) {
const errorMessage = Array.isArray(response.content) ? response.content[0]?.text || "Failed to put file" : "Failed to put file";
throw new Error(errorMessage);
}
} catch (error) {
console.error(
`\u274C Failed to put file ${filePath}:`,
error instanceof Error ? error.message : String(error)
);
throw error;
} finally {
await client.close();
}
}
async function watch2(options, callback) {
const {
branchName,
fromCtime = 1,
pathFilter,
workspace,
local = false
} = options;
console.log(`\u{1F4E1} Watching branch "${branchName}" for changes...`);
if (pathFilter) {
console.log(` \u{1F50D} Path filter: ${pathFilter}`);
}
const { headers, url: baseUrl } = await workspaceClientParams({
workspace,
local,
pathname: "/deconfig/watch"
});
const searchParams = new URLSearchParams();
searchParams.set("branchName", branchName);
searchParams.set("fromCtime", fromCtime.toString());
if (pathFilter) {
searchParams.set("pathFilter", pathFilter);
}
const sseUrlObj = new URL(baseUrl);
sseUrlObj.search = searchParams.toString();
const sseUrl = sseUrlObj.href;
let retryCount = 0;
const maxRetries = 5;
const retryDelay = 2e3;
const connect2 = async () => {
console.log(`\u{1F504} Connecting to SSE stream... (attempt ${retryCount + 1})`);
try {
const response = await fetch(sseUrl, {
headers: {
Accept: "text/event-stream",
"Cache-Control": "no-cache",
...headers
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
if (!response.body) {
throw new Error("No response body");
}
console.log("\u2705 Connected to SSE stream");
retryCount = 0;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log("\u{1F4E1} SSE stream ended");
break;
}
const chunk = decoder.decode(value, { stream: true });
console.log(`\u{1F4E6} Received chunk: ${JSON.stringify(chunk)}`);
buffer += chunk;
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.trim()) {
console.log(`\u{1F4DD} Processing line: ${JSON.stringify(line)}`);
}
await processSSELine(line, branchName, workspace, local, callback);
}
}
} catch (error) {
console.error("\u274C Error reading SSE stream:", error);
throw error;
}
} catch (error) {
console.error(
"\u274C SSE connection failed:",
error instanceof Error ? error.message : String(error)
);
if (retryCount < maxRetries) {
retryCount++;
console.log(`\u23F3 Retrying in ${retryDelay / 1e3} seconds...`);
setTimeout(() => {
connect2().catch((retryError) => {
console.error(
"\u274C Retry failed:",
retryError instanceof Error ? retryError.message : String(retryError)
);
});
}, retryDelay);
} else {
throw new Error(`Failed to connect after ${maxRetries} attempts`);
}
}
};
await connect2();
}
async function processSSELine(line, branchName, workspace, local, callback) {
if (!line.trim()) return;
if (line.startsWith("event:")) {
line.substring(6).trim();
return;
}
if (line.startsWith("data:")) {
const jsonData = line.substring(5).trim();
console.log(`\u{1F50D} Parsing JSON data: ${jsonData}`);
try {
const event = JSON.parse(jsonData);
console.log(`\u2705 Parsed event:`, event);
const eventWithContent = { ...event };
if (event.type === "added" || event.type === "modified") {
console.log(`\u{1F4E5} Fetching content for ${event.path}...`);
try {
eventWithContent.content = await fetchFileContent(
event.path,
branchName,
workspace,
local
);
console.log(
`\u2705 Fetched content: ${eventWithContent.content?.length} bytes`
);
} catch (error) {
console.error(
`\u274C Failed to fetch content for ${event.path}:`,
error instanceof Error ? error.message : String(error)
);
}
}
console.log(`\u{1F504} Calling callback for event: ${event.type} ${event.path}`);
await callback(eventWithContent);
} catch (error) {
console.error("\u274C Failed to parse SSE data:", jsonData, error);
}
}
}
async function getCommand(options) {
const { path: filePath, branch, output, workspace, local } = options;
console.log(`\u{1F4E5} Getting file "${filePath}" from branch "${branch}"`);
try {
const content = await fetchFileContent(filePath, branch, workspace, local);
if (output) {
const dir = dirname(output);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(output, content);
console.log(`\u2705 File saved to: ${output} (${content.length} bytes)`);
} else {
process28.stdout.write(content);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("Session not found") || errorMessage.includes("Session expired")) {
console.error("\u{1F4A5} Get failed: Authentication required");
console.error(
" Please run 'deco login' first to authenticate with deco.chat"
);
} else if (errorMessage.includes("File not found")) {
console.error(`\u{1F4A5} File not found: ${filePath} (branch: ${branch})`);
} else {
console.error("\u{1F4A5} Get failed:", errorMessage);
}
process28.exit(1);
}
}
async function putCommand(options) {
const {
path: filePath,
branch,
file,
content,
metadata,
workspace,
local
} = options;
console.log(`\u{1F4E4} Putting file "${filePath}" to branch "${branch}"`);
try {
let fileContent;
if (file) {
if (!existsSync(file)) {
throw new Error(`Local file not found: ${file}`);
}
fileContent = readFileSync(file);
console.log(` \u{1F4C1} Reading from: ${file}`);
} else if (content !== void 0) {
fileContent = content;
} else {
console.log(" \u{1F4DD} Reading from stdin...");
const chunks = [];
for await (const chunk of process28.stdin) {
chunks.push(chunk);
}
fileContent = Buffer.concat(chunks);
}
let parsedMetadata;
if (metadata) {
try {
parsedMetadata = JSON.parse(metadata);
} catch {
throw new Error(`Invalid metadata JSON: ${metadata}`);
}
}
await putFileContent(
filePath,
fileContent,
branch,
parsedMetadata,
workspace,
local
);
const size = Buffer.isBuffer(fileContent) ? fileContent.length : Buffer.from(fileContent).length;
console.log(`\u2705 File uploaded successfully (${size} bytes)`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("Session not found") || errorMessage.includes("Session expired")) {
console.error("\u{1F4A5} Put failed: Authentication required");
console.error(
" Please run 'deco login' first to authenticate with deco.chat"
);
} else {
console.error("\u{1F4A5} Put failed:", errorMessage);
}
process28.exit(1);
}
}
async function watchCommand(options) {
const { path: pathFilter, branch, fromCtime, workspace, local } = options;
console.log(`\u{1F440} Watching branch "${branch}" for changes`);
if (pathFilter) {
console.log(` \u{1F3AF} Filtering path: ${pathFilter}`);
}
const logChanges = (event) => {
const timestamp = new Date(event.timestamp).toISOString();
console.log(`[${timestamp}] \u{1F4DD} ${event.type.toUpperCase()}: ${event.path}`);
if (event.content) {
console.log(` \u{1F4CA} Size: ${event.content.length} bytes`);
}
if (event.metadata) {
console.log(
` \u23F0 Modified: ${new Date(event.metadata.mtime).toISOString()}`
);
}
console.log(` \u{1F522} Patch ID: ${event.patchId}`);
console.log("");
};
process28.on("SIGINT", () => {
console.log("\n\u{1F6D1} Received SIGINT, shutting down gracefully...");
process28.exit(0);
});
process28.on("SIGTERM", () => {
console.log("\n\u{1F6D1} Received SIGTERM, shutting down gracefully...");
process28.exit(0);
});
try {
await watch2(
{
branchName: branch,
pathFilter,
fromCtime,
workspace,
local
},
logChanges
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("Session not found") || errorMessage.includes("Session expired")) {
console.error("\u{1F4A5} Watch failed: Authentication required");
console.error(
" Please run 'deco login' first to authenticate with deco.chat"
);
} else {
console.error("\u{1F4A5} Watch failed:", errorMessage);
}
process28.exit(1);
}
}
async function cloneCommand(options) {
const { branchName, path: localPath, pathFilter, workspace, local } = options;
console.log(`\u{1F4E5} Cloning branch "${branchName}" to local path: ${localPath}`);
if (!existsSync(localPath)) {
mkdirSync(localPath, { recursive: true });
console.log(`\u{1F4C1} Created local directory: ${localPath}`);
}
const { createWorkspaceClient: createWorkspaceClient2 } = await import('./mcp-7MUYOFGZ.js');
const client = await createWorkspaceClient2({ workspace, local });
try {
const response = await client.callTool({
name: "LIST_FILES",
arguments: {
branch: branchName,
prefix: pathFilter
}
});
if (response.isError) {
const errorMessage = Array.isArray(response.content) ? response.content[0]?.text || "Failed to list files" : "Failed to list files";
throw new Error(errorMessage);
}
const result = response.structuredContent;
console.log(`\u{1F4CB} Found ${result.count} files to clone`);
for (const [filePath] of Object.entries(result.files)) {
const localFilePath = join(
localPath,
filePath.startsWith("/") ? filePath.slice(1) : filePath
);
try {
console.log(`\u{1F4E5} Downloading: ${filePath}`);
const content = await fetchFileContent(
filePath,
branchName,
workspace,
local
);
const dir = dirname(localFilePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(localFilePath, content);
console.log(
` \u2705 Cloned to: ${localFilePath} (${content.length} bytes)`
);
} catch (error) {
console.error(
` \u274C Failed to clone ${filePath}:`,
error instanceof Error ? error.message : String(error)
);
}
}
console.log(
`\u{1F389} Clone completed! Downloaded ${result.count} files to ${localPath}`
);
await writeDeconfigHead({
workspace: workspace || "",
branch: branchName,
path: localPath,
pathFilter,
local
});
console.log(`\u{1F4DD} Saved deconfig HEAD to .deconfig/head`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("Session not found") || errorMessage.includes("Session expired")) {
console.error("\u{1F4A5} Clone failed: Authentication required");
console.error(
" Please run 'deco login' first to authenticate with deco.chat"
);
} else {
console.error("\u{1F4A5} Clone failed:", errorMessage);
}
process28.exit(1);
} finally {
await client.close();
}
}
var DeconfigIgnore = class {
ig;
rootPath;
patternCount = 0;
constructor(rootPath) {
this.rootPath = rootPath;
this.ig = ignore();
this.loadIgnoreFiles();
}
/**
* Load .deconfigignore files and add default patterns
*/
loadIgnoreFiles() {
this.addDefaultPatterns();
const ignoreFilePath = join(this.rootPath, ".deconfigignore");
if (existsSync(ignoreFilePath)) {
this.loadIgnoreFile(ignoreFilePath);
}
}
/**
* Add default ignore patterns
*/
addDefaultPatterns() {
const defaultPatterns = [
"node_modules/",
".git/",
".deconfig/",
".DS_Store",
"*.tmp",
"*.temp",
".env.local",
".env.*.local"
];
this.ig.add(defaultPatterns);
this.patternCount += defaultPatterns.length;
}
/**
* Load patterns from a specific .deconfigignore file
*/
loadIgnoreFile(filePath) {
try {
const content = readFileSync(filePath, "utf-8");
const lines = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
if (lines.length > 0) {
this.ig.add(lines);
this.patternCount += lines.length;
}
} catch (error) {
console.warn(
`Warning: Could not read ignore file ${filePath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Check if a file path should be ignored
*/
isIgnored(filePath) {
const relativePath = relative(this.rootPath, filePath);
if (relativePath.startsWith("..")) {
return false;
}
const normalizedPath = relativePath.replace(/\\/g, "/");
return this.ig.ignores(normalizedPath);
}
/**
* Filter an array of file paths, removing ignored ones
*/
filter(filePaths) {
const relativePaths = filePaths.map((path4) => {
const rel = relative(this.rootPath, path4);
return rel.replace(/\\/g, "/");
});
const filtered = this.ig.filter(relativePaths);
return filtered.map((relPath) => join(this.rootPath, relPath));
}
/**
* Get the number of loaded patterns (for logging)
*/
getPatternCount() {
return this.patternCount;
}
/**
* Create RegExp patterns compatible with the walk() function
* Since the ignore package handles complex logic, we create a single
* RegExp that checks each path against the ignore checker
*/
toWalkSkipPatterns() {
const ignoreRegex = new RegExp(".*");
ignoreRegex.test = (path4) => {
const relativePath = relative(this.rootPath, path4);
const normalizedPath = relativePath.replace(/\\/g, "/");
return this.ig.ignores(normalizedPath);
};
return [ignoreRegex];
}
};
function createIgnoreChecker(rootPath) {
return new DeconfigIgnore(rootPath);
}
async function pushSingleFile(localFilePath, remotePath, branchName, workspace, local) {
try {
const stats = statSync(localFilePath);
console.log(`\u{1F4E4} Pushing: ${remotePath} (${stats.size} bytes)`);
const content = readFileSync(localFilePath);
await putFileContent(
remotePath,
content,
branchName,
void 0,
// no metadata for now
workspace,
local
);
console.log(` \u2705 Pushed: ${remotePath}`);
} catch (error) {
console.error(
` \u274C Failed to push ${remotePath}:`,
error instanceof Error ? error.message : String(error)
);
}
}
async function watchAndSync(options) {
const { localPath, branchName, pathFilter, workspace, local } = options;
console.log(`\u{1F440} Watching directory "${localPath}" for changes...`);
console.log(" Press Ctrl+C to stop watching");
const ignoreChecker = createIgnoreChecker(localPath);
console.log(`\u{1F4CB} Loaded ${ignoreChecker.getPatternCount()} ignore patterns`);
const debounceMap = /* @__PURE__ */ new Map();
const DEBOUNCE_DELAY = 500;
try {
const watcher = watch(localPath, { recursive: true }, (_, filename) => {
if (!filename) return;
const fullPath = join(localPath, filename);
const relativePath = relative(localPath, fullPath);
const remotePath = `/${relativePath.replace(/\\/g, "/")}`;
if (pathFilter && !remotePath.startsWith(pathFilter)) {
return;
}
if (ignoreChecker.isIgnored(fullPath)) {
return;
}
try {
const stats = statSync(fullPath);
if (!stats.isFile()) {
return;
}
} catch {
return;
}
const existingTimeout = debounceMap.get(fullPath);
if (existingTimeout) {
clearTimeout(existingTimeout);
}
const timeout = setTimeout(async () => {
console.log(`\u{1F504} File changed: ${remotePath}`);
await pushSingleFile(
fullPath,
remotePath,
branchName,
workspace,
local
);
debounceMap.delete(fullPath);
}, DEBOUNCE_DELAY);
debounceMap.set(fullPath, timeout);
});
const cleanup = () => {
console.log("\n\u{1F6D1} Stopping file watcher...");
watcher.close();
for (const timeout of debounceMap.values()) {
clearTimeout(timeout);
}
process28.exit(0);
};
process28.on("SIGINT", cleanup);
process28.on("SIGTERM", cleanup);
await new Promise(() => {
});
} catch (error) {
console.error(
"\u274C Watch failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
}
function calculateFileHash(filePath) {
const content = readFileSync(filePath);
return createHash("sha256").update(content).digest("hex");
}
function extractHashFromAddress(address) {
const parts = address.split(":");
return parts.length >= 3 ? parts[2] : "";
}
async function pushCommand(options) {
const {
branchName,
path: localPath,
pathFilter,
workspace,
local,
dryRun,
watch: watchMode
} = options;
console.log(
`\u{1F4E4} ${watchMode ? "Watching and pushing" : "Pushing"} files from "${localPath}" to branch "${branchName}"${dryRun ? " (dry run)" : ""}...`
);
try {
const pathStats = statSync(localPath);
if (!pathStats.isDirectory()) {
throw new Error(`Path is not a directory: ${localPath}`);
}
const ignoreChecker = createIgnoreChecker(localPath);
console.log(`\u{1F4CB} Loaded ${ignoreChecker.getPatternCount()} ignore patterns`);
if (watchMode) {
if (dryRun) {
console.error("\u274C Cannot use --watch with --dry-run");
process28.exit(1);
}
console.log("\u{1F680} Performing initial push...");
await pushCommand({
...options,
watch: false
// Disable watch for initial push
});
console.log("\u2705 Initial push completed, starting watch mode...\n");
await watchAndSync({
localPath,
branchName,
pathFilter,
workspace,
local
});
return;
}
const currentLocalFiles = /* @__PURE__ */ new Map();
for await (const entry of walk(localPath, {
includeFiles: true,
includeDirs: false,
skip: ignoreChecker.toWalkSkipPatterns()
})) {
const relativePath = relative(localPath, entry.path);
const remotePath = `/${relativePath.replace(/\\/g, "/")}`;
if (pathFilter && !remotePath.startsWith(pathFilter)) {
continue;
}
const stats = statSync(entry.path);
const hash = calculateFileHash(entry.path);
currentLocalFiles.set(remotePath, {
hash,
size: stats.size
});
}
const client = await createWorkspaceClient({ workspace, local });
let remoteFiles = {};
try {
const response = await client.callTool({
name: "LIST_FILES",
arguments: {
branch: branchName,
prefix: pathFilter
}
});
if (response.isError) {
const errorMessage = Array.isArray(response.content) ? response.content[0]?.text || "Failed to list files" : "Failed to list files";
throw new Error(errorMessage);
}
const result = response.structuredContent;
remoteFiles = result.files;
console.log(`\u{1F4CB} Found ${result.count} remote files`);
} finally {
await client.close();
}
const toUpload = [];
const toUpdate = [];
for (const [remotePath, localInfo] of currentLocalFiles) {
const remoteInfo = remoteFiles[remotePath];
if (!remoteInfo) {
toUpload.push(remotePath);
} else {
const remoteHash = extractHashFromAddress(remoteInfo.address);
if (localInfo.hash !== remoteHash) {
toUpdate.push(remotePath);
}
}
}
console.log(
`\u{1F4CA} Changes detected: ${toUpload.length} new, ${toUpdate.length} modified`
);
if (dryRun) {
if (toUpload.length > 0) {
console.log("\n\u{1F4E4} Files to upload:");
for (const path4 of toUpload) {
const info = currentLocalFiles.get(path4);
console.log(` + ${path4} (${info.size} bytes)`);
}
}
if (toUpdate.length > 0) {
console.log("\n\u{1F4DD} Files to update:");
for (const path4 of toUpdate) {
const info = currentLocalFiles.get(path4);
console.log(` ~ ${path4} (${info.size} bytes)`);
}
}
console.log(
`
\u2705 Dry run completed. ${toUpload.length + toUpdate.length} changes detected.`
);
return;
}
let successCount = 0;
let errorCount = 0;
const totalOperations = toUpload.length + toUpdate.length;
if (totalOperations === 0) {
console.log("\u2705 No changes detected. Branch is up to date!");
return;
}
for (const remotePath of [...toUpload, ...toUpdate]) {
try {
const isUpdate = toUpdate.includes(remotePath);
const localInfo = currentLocalFiles.get(remotePath);
const localFilePath = join(localPath, remotePath.substring(1));
console.log(
`\u{1F4E4} ${isUpdate ? "Updating" : "Uploading"}: ${remotePath} (${localInfo.size} bytes)`
);
const content = readFileSync(localFilePath);
await putFileContent(
remotePath,
content,
branchName,
void 0,
// no metadata for now
workspace,
local
);
console.log(
` \u2705 ${isUpdate ? "Updated" : "Uploaded"}: ${remotePath}`
);
successCount++;
} catch (error) {
console.error(
` \u274C Failed to ${toUpdate.includes(remotePath) ? "update" : "upload"} ${remotePath}:`,
error instanceof Error ? error.message : String(error)
);
errorCount++;
}
}
if (errorCount > 0) {
console.log(
`
\u26A0\uFE0F Push completed with errors: ${successCount} succeeded, ${errorCount} failed`
);
process28.exit(1);
} else {
console.log(
`
\u{1F389} Push completed successfully! ${successCount} operations completed on ${branchName}`
);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("Session not found") || errorMessage.includes("Session expired")) {
console.error("\u{1F4A5} Push failed: Authentication required");
console.error(
" Please run 'deco login' first to authenticate with deco.chat"
);
} else {
console.error("\u{1F4A5} Push failed:", errorMessage);
}
process28.exit(1);
}
}
function getFileHash(filePath) {
try {
const content = readFileSync(filePath);
return createHash("sha256").update(content).digest("hex");
} catch {
return "";
}
}
async function pullCommand(options) {
const {
branchName,
path: localPath,
pathFilter,
workspace,
local,
dryRun
} = options;
console.log(
`\u{1F4E5} Pulling changes from branch "${branchName}" to "${localPath}"${dryRun ? " (dry run)" : ""}...`
);
if (!existsSync(localPath)) {
mkdirSync(localPath, { recursive: true });
console.log(`\u{1F4C1} Created local directory: ${localPath}`);
}
const { createWorkspaceClient: createWorkspaceClient2 } = await import('./mcp-7MUYOFGZ.js');
const client = await createWorkspaceClient2({ workspace, local });
try {
const response = await client.callTool({
name: "LIST_FILES",
arguments: {
branch: branchName,
prefix: pathFilter
}
});
if (response.isError) {
const errorMessage = Array.isArray(response.content) ? response.content[0]?.text || "Failed to list files" : "Failed to list files";
throw new Error(errorMessage);
}
const result = response.structuredContent;
console.log(`\u{1F4CB} Found ${result.count} remote files`);
const localFiles = /* @__PURE__ */ new Map();
if (existsSync(localPath)) {
const ignoreChecker = createIgnoreChecker(localPath);
for await (const entry of walk(localPath, {
includeFiles: true,
includeDirs: false,
skip: ignoreChecker.toWalkSkipPatterns()
})) {
const relativePath = relative(localPath, entry.path);
const remotePath = `/${relativePath.replace(/\\/g, "/")}`;
const hash = getFileHash(entry.path);
localFiles.set(remotePath, { hash, path: entry.path });
}
}
const toDownload = [];
const toUpdate = [];
const toDelete = [];
for (const [remoteFilePath, remoteMeta] of Object.entries(result.files)) {
if (pathFilter && !remoteFilePath.startsWith(pathFilter)) {
continue;
}
const localFile = localFiles.get(remoteFilePath);
if (!localFile) {
toDownload.push(remoteFilePath);
} else {
const parts = (remoteMeta.address || "").split(":");
const remoteHash = parts.length >= 3 ? parts[2] : "";
if (!remoteHash || localFile.hash !== remoteHash) {
toUpdate.push(remoteFilePath);
}
localFiles.delete(remoteFilePath);
}
}
for (const [remoteFilePath] of localFiles) {
if (!pathFilter || remoteFilePath.startsWith(pathFilter)) {
toDelete.push(remoteFilePath);
}
}
console.log(
`\u{1F4CA} Changes: ${toDownload.length} new, ${toUpdate.length} modified, ${toDelete.length} to delete`
);
if (dryRun) {
if (toDownload.length > 0) {
console.log("\n\u{1F4E5} Files to download:");
toDownload.forEach((path4) => console.log(` + ${path4}`));
}
if (toUpdate.length > 0) {
console.log("\n\u{1F4DD} Files to update:");
toUpdate.forEach((path4) => console.log(` ~ ${path4}`));
}
if (toDelete.length > 0) {
console.log("\n\u{1F5D1}\uFE0F Files to delete:");
toDelete.forEach((path4) => console.log(` - ${path4}`));
}
console.log(
`
\u2705 Dry run completed. ${toDownload.length + toUpdate.length + toDelete.length} changes detected.`
);
return;
}
let successCount = 0;
let errorCount = 0;
for (const filePath of toDownload) {
try {
console.log(`\u{1F4E5} Downloading: ${filePath}`);
const content = await fetchFileContent(
filePath,
branchName,
workspace,
local
);
const localFilePath = join(
localPath,
filePath.startsWith("/") ? filePath.slice(1) : filePath
);
const dir = dirname(localFilePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(localFilePath, content);
console.log(
` \u2705 Downloaded: ${localFilePath} (${content.length} bytes)`
);
successCount++;
} catch (error) {
console.error(
` \u274C Failed to download ${filePath}:`,
error instanceof Error ? error.message : String(error)
);
errorCount++;
}
}
for (const filePath of toUpdate) {
try {
console.log(`\u{1F4DD} Updating: ${filePath}`);
const content = await fetchFileContent(
filePath,
branchName,
workspace,
local
);
const localFilePath = join(
localPath,
filePath.startsWith("/") ? filePath.slice(1) : filePath
);
writeFileSync(localFilePath, content);
console.log(
` \u2705 Updated: ${localFilePath} (${content.length} bytes)`
);
successCount++;
} catch (error) {
console.error(
` \u274C Failed to update ${filePath}:`,
error instanceof Error ? error.message : String(error)
);
errorCount++;
}
}
for (const filePath of toDelete) {
const localFile = localFiles.get(filePath);
if (localFile) {
try {
console.log(`\u{1F5D1}\uFE0F Deleting: ${filePath}`);
unlinkSync(localFile.path);
console.log(` \u2705 Deleted: ${localFile.path}`);
successCount++;
} catch (error) {
console.error(
` \u274C Failed to delete ${filePath}:`,
error instanceof Error ? error.message : String(error)
);
errorCount++;
}
}
}
if (errorCount > 0) {
console.log(
`
\u26A0\uFE0F Pull completed with errors: ${successCount} succeeded, ${errorCount} failed`
);
} else {
console.log(
`
\u{1F389} Pull completed successfully! Applied ${successCount} changes from ${branchName}`
);
}
await writeDeconfigHead({
workspace: workspace || "",
branch: branchName,
path: localPath,
pathFilter,
local
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("Session not found") || errorMessage.includes("Session expired")) {
console.error("\u{1F4A5} Pull failed: Authentication required");
console.error(
" Please run 'deco login' first to authenticate with deco.chat"
);
} else {
console.error("\u{1F4A5} Pull failed:", errorMessage);
}
process28.exit(1);
} finally {
await client.close();
}
}
async function listCommand(options) {
const {
branchName,
pathFilter,
workspace,
local,
format: format2 = "plainString"
} = options;
console.log(`\u{1F4CB} Listing files in branch "${branchName}"...`);
if (pathFilter) {
console.log(` \u{1F50D} Path filter: ${pathFilter}`);
}
const { createWorkspaceClient: createWorkspaceClient2 } = await import('./mcp-7MUYOFGZ.js');
const client = await createWorkspaceClient2({ workspace, local });
try {
const response = await client.callTool({
name: "LIST_FILES",
arguments: {
branch: branchName,
prefix: pathFilter
}
});
if (response.isError) {
const errorMessage = Array.isArray(response.content) ? response.content[0]?.text || "Failed to list files" : "Failed to list files";
throw new Error(errorMessage);
}
const result = response.structuredContent;
if (result.count === 0) {
console.log("\u{1F4C2} No files found in this branch.");
return;
}
console.log(`\u{1F4CB} Found ${result.count} files`);
const fileChoices = Object.entries(result.files).map(([path4, info]) => {
const size = formatFileSize(info.sizeInBytes);
const lastModified = new Date(info.mtime).toLocaleString();
return {
name: `${path4} (${size}, modified: ${lastModified})`,
value: path4,
short: path4
};
});
fileChoices.push({
name: "\u{1F6AA} Exit",
value: "__EXIT__",
short: "Exit"
});
while (true) {
const { selectedFile } = await inquirer6.prompt([
{
type: "list",
name: "selectedFile",
message: "Select a file to view its content:",
choices: fileChoices,
pageSize: 15
}
]);
if (selectedFile === "__EXIT__") {
console.log("\u{1F44B} Goodbye!");
break;
}
try {
console.log(`
\u{1F4C4} Loading content for: ${selectedFile}`);
const content = await fetchFileContent(
selectedFile,
branchName,
workspace,
local
);
const fileInfo = result.files[selectedFile];
console.log(`
${"=".repeat(60)}`);
console.log(`\u{1F4C1} File: ${selectedFile}`);
console.log(`\u{1F4CA} Size: ${formatFileSize(fileInfo.sizeInBytes)}`);
console.log(
`\u23F0 Modified: ${new Date(fileInfo.mtime).toLocaleString()}`
);
console.log(`\u{1F3F7}\uFE0F Address: ${fileInfo.address}`);
if (Object.keys(fileInfo.metadata).length > 0) {
console.log(
`\u{1F4CB} Metadata: ${JSON.stringify(fileInfo.metadata, null, 2)}`
);
}
console.log(`${"=".repeat(60)}
`);
let displayContent;
switch (format2) {
case "json":
try {
const text = content.toString("utf-8");
const parsed = JSON.parse(text);
displayContent = JSON.stringify(parsed, null, 2);
} catch {
displayContent = content.toString("utf-8");
}
break;
case "base64":
displayContent = content.toString("base64");
break;
case "plainString":
default:
displayContent = content.toString("utf-8");
break;
}
const lines = displayContent.split("\n");
const maxPreviewLines = 50;
if (lines.length > maxPreviewLines) {
console.log(lines.slice(0, maxPreviewLines).join("\n"));
console.log(`
... (${lines.length - maxPreviewLines} more lines)`);
const { showMore } = await inquirer6.prompt([
{
type: "confirm",
name: "showMore",
message: "Show the complete file content?",
default: false
}
]);
if (showMore) {
console.log(`
${"=".repeat(60)}`);
console.log("\u{1F4C4} Complete file content:");
console.log(`${"=".repeat(60)}
`);
console.log(displayContent);
}
} else {
console.log(displayContent);
}
console.log(`
${"=".repeat(60)}
`);
const { continueReading } = await inquirer6.prompt([
{
type: "confirm",
name: "continueReading",
message: "Continue browsing files?",
default: true
}
]);
if (!continueReading) {
console.log("\u{1F44B} Goodbye!");
break;
}
} catch (error) {
console.error(
`\u274C Failed to read file ${selectedFile}:`,
error instanceof Error ? error.message : String(error)
);
const { tryAgain } = await inquirer6.prompt([
{
type: "confirm",
name: "tryAgain",
message: "Continue browsing other files?",
default: true
}
]);
if (!tryAgain) {
break;
}
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("Session not found") || errorMessage.includes("Session expired")) {
console.error("\u{1F4A5} List failed: Authentication required");
console.error(
" Please run 'deco login' first to authenticate with deco.chat"
);
} else {
console.error("\u{1F4A5} List failed:", errorMessage);
}
process28.exit(1);
} finally {
await client.close();
}
}
function formatFileSize(bytes) {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}
async function deleteCommand(options) {
const { path: filePath, branchName, workspace, local } = options;
console.log(`\u{1F5D1}\uFE0F Deleting file "${filePath}" from branch "${branchName}"`);
const { createWorkspaceClient: createWorkspaceClient2 } = await import('./mcp-7MUYOFGZ.js');
const client = await createWorkspaceClient2({ workspace, local });
try {
const response = await client.callTool({
name: "DELETE_FILE",
arguments: {
branch: branchName,
path: filePath
}
});
if (response.isError) {
const errorMessage = Array.isArray(response.content) ? response.content[0]?.text || "Failed to delete file" : "Failed to delete file";
throw new Error(errorMessage);
}
const result = response.structuredContent;
if (result.deleted) {
console.log(`\u2705 File deleted successfully: ${filePath}`);
} else {
console.log(`\u2139\uFE0F File was not found or already deleted: ${filePath}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("Session not found") || errorMessage.includes("Session expired")) {
console.error("\u{1F4A5} Delete failed: Authentication required");
console.error(
" Please run 'deco login' first to authenticate with deco.chat"
);
} else if (errorMessage.includes("File not found")) {
console.error(`\u{1F4A5} File not found: ${filePath} (branch: ${branchName})`);
} else {
console.error("\u{1F4A5} Delete failed:", errorMessage);
}
process28.exit(1);
} finally {
await client.close();
}
}
async function promptProject(orgSlug, local = false, current = "") {
try {
inquirer6.registerPrompt("search-list", inquirerSearchList);
} catch {
console.warn(
"Could not load search functionality, falling back to basic list"
);
}
const session = await readSession();
if (!session) {
throw new Error("No session found. Please run 'deco login' first.");
}
const client = await createWorkspaceClient({
workspace: "",
local
});
try {
const response = await client.callTool({
name: "PROJECTS_LIST",
arguments: { org: orgSlug }
});
if (response.isError) {
throw new Error(`Failed to fetch projects: ${response.content}`);
}
const { items: projects } = response.structuredContent;
if (!projects || projects.length === 0) {
throw new Error(
`No projects found in organization '${orgSlug}'. Please create a project first.`
);
}
const choices = projects.map((project2) => ({
name: `${project2.title} (${project2.slug})`,
value: project2.slug,
short: project2.slug
}));
let selectedSlug;
try {
const result = await inquirer6.prompt([
{
type: "search-list",
name: "selectedSlug",
message: "Select a project:",
choices,
default: current
}
]);
selectedSlug = result.selectedSlug;
} catch {
const result = await inquirer6.prompt([
{
type: "list",
name: "selectedSlug",
message: "Select a project:",
choices,
default: current
}
]);
selectedSlug = result.selectedSlug;
}
const selectedProject = projects.find((p) => p.slug === selectedSlug);
if (!selectedProject) {
throw new Error(`Project '${selectedSlug}' not found`);
}
return selectedProject;
} finally {
await client.close();
}
}
var manifestProjectSchema = z.object({
slug: z.string().min(1),
title: z.string().min(1),
description: z.string().optional()
});
var manifestAuthorSchema = z.object({
orgSlug: z.string().min(1),
orgId: z.string().optional(),
userId: z.string().optional(),
userEmail: z.string().email().optional()
});
var manifestResourcesSchema = z.object({
tools: z.array(z.string()),
views: z.array(z.string()),
workflows: z.array(z.string()),
documents: z.array(z.string()),
database: z.array(z.string()).optional().default([])
});
var manifestDependenciesSchema = z.object({
mcps: z.array(z.string())
});
var manifestSchema = z.object({
schemaVersion: z.literal("1.0"),
project: manifestProjectSchema,
author: manifestAuthorSchema,
resources: manifestResourcesSchema,
dependencies: manifestDependenciesSchema,
createdAt: z.string().datetime()
});
function parseManifest(data) {
return manifestSchema.parse(data);
}
// src/lib/mcp-manifest.ts
var MANIFEST_FILENAME = "deco.mcp.json";
async function readManifestFile(dirPath) {
const manifestPath = path2.join(dirPath, MANIFEST_FILENAME);
const content = await fs11.readFile(manifestPath, "utf-8");
const data = JSON.parse(content);
return parseManifest(data);
}
async function writeManifestFile(dirPath, manifest) {
const manifestPath = path2.join(dirPath, MANIFEST_FILENAME);
await fs11.writeFile(
manifestPath,
JSON.stringify(manifest, null, 2) + "\n",
"utf-8"
);
}
async function manifestExists(dirPath) {
const manifestPath = path2.join(dirPath, MANIFEST_FILENAME);
try {
await fs11.access(manifestPath);
return true;
} catch {
return false;
}
}
async function extractDependenciesFromTools(toolFiles) {
const integrationIds = /* @__PURE__ */ new Set();
for (const file of toolFiles) {
try {
const tool = JSON.parse(file.content);
if (tool.integration_id && typeof tool.integration_id === "string") {
integrationIds.add(tool.integration_id);
}
if (tool.integrationId && typeof tool.integrationId === "string") {
integrationIds.add(tool.integrationId);
}
if (Array.isArray(tool.dependencies)) {
for (const dependency of tool.dependencies) {
if (dependency && typeof dependency === "object") {
if (dependency.integrationId && typeof dependency.integrationId === "string") {
integrationIds.add(dependency.integrationId);
}
if (dependency.integration_id && typeof dependency.integration_id === "string") {
integrationIds.add(dependency.integration_id);
}
}
}
}
if (tool.tools_set && typeof tool.tools_set === "object") {
for (const key of Object.keys(tool.tools_set)) {
if (key.startsWith("i:")) {
integrationIds.add(key);
}
}
}
} catch (err) {
console.warn(`Warning: Could not parse tool file ${file.path}: ${err}`);
}
}
return Array.from(integrationIds).sort();
}
// src/lib/projects.ts
function containsControlCharacters(value) {
for (let index = 0; index < value.length; index++) {
const code = value.charCodeAt(index);
if (code >= 0 && code <= 31 || code === 127) {
return true;
}
}
return false;
}
function sanitizeProjectPath(rawPath) {
if (!rawPath) {
return null;
}
const trimmed = rawPath.trim();
if (!trimmed) {
return null;
}
let normalized = trimmed.replace(/\\/g, "/");
normalized = normalized.replace(/^\.\/+/, "");
normalized = normalized.replace(/^\/+/, "");
if (!normalized) {
return null;
}
if (normalized.includes("..")) {
return null;
}
if (containsControlCharacters(normalized)) {
return null;
}
return normalized;
}
// src/lib/code-conversion.ts
function viewJsonToCode(view) {
const lines = [];
lines.push(view.code);
lines.push("");
lines.push("// Metadata exports");
lines.push(`export const name = ${JSON.stringify(view.name)};`);
lines.push(`export const description = ${JSON.stringify(view.description)};`);
if (view.inputSchema) {
lines.push(
`export const inputSchema = ${JSON.stringify(view.inputSchema, null, 2)};`
);
}
if (view.importmap) {
lines.push(
`export const importmap = ${JSON.stringify(view.importmap, null, 2)};`
);
}
if (view.icon) {
lines.push(`export const icon = ${JSON.stringify(view.icon)};`);
}
if (view.tags && view.tags.length > 0) {
lines.push(`export const tags = ${JSON.stringify(view.tags)};`);
}
return lines.join("\n");
}
function viewCodeToJson(code) {
const view = {};
const nameMatch = code.match(/export const name = (.+);/);
const descMatch = code.match(/export const description = (.+);/);
const inputSchemaMatch = code.match(
/export const inputSchema = ([\s\S]+?);(?=\n(?:export const |$))/
);
const importmapMatch = code.match(
/export const importmap = ([\s\S]+?);(?=\n(?:export const |$))/
);
const iconMatch = code.match(/export const icon = (.+);/);
const tagsMatch = code.match(/export const tags = (.+);/);
if (!nameMatch || !descMatch) {
throw new Error(
"Invalid view file: missing required 'name' or 'description' export"
);
}
view.name = JSON.parse(nameMatch[1]);
view.description = JSON.parse(descMatch[1]);
if (inputSchemaMatch) {
view.inputSchema = JSON.parse(inputSchemaMatch[1]);
}
if (importmapMatch) {
view.importmap = JSON.parse(importmapMatch[1]);
}
if (iconMatch) {
view.icon = JSON.parse(iconMatch[1]);
}
if (tagsMatch) {
view.tags = JSON.parse(tagsMatch[1]);
}
const metadataMarkerMatch = code.match(/\n\/\/ Metadata exports\n/);
if (metadataMarkerMatch) {
view.code = code.slice(0, metadataMarkerMatch.index).trim();
} else {
const firstExportMatch = code.match(/\nexport const name = /);
if (firstExportMatch && firstExportMatch.index) {
view.code = code.slice(0, firstExportMatch.index).trim();
} else {
throw new Error("Invalid view file: cannot locate code section");
}
}
if (!view.code) {
throw new Error("Invalid view file: no code found");
}
return view;
}
function toolJsonToCode(tool) {
const lines = [];
lines.push(tool.execute);
lines.push("");
lines.push("// Metadata exports");
lines.push(`export const name = ${JSON.stringify(tool.name)};`);
lines.push(`export const description = ${JSON.stringify(tool.description)};`);
lines.push(
`export const inputSchema = ${JSON.stringify(tool.inputSchema, null, 2)};`
);
lines.push(
`export const outputSchema = ${JSON.stringify(tool.outputSchema, null, 2)};`
);
if (tool.dependencies && tool.dependencies.length > 0) {
lines.push(
`export const dependencies = ${JSON.stringify(tool.dependencies, null, 2)};`
);
}
return lines.join("\n");
}
function toolCodeToJson(code) {
const tool = {};
const nameMatch = code.match(/export const name = (.+);/);
const descMatch = code.match(/export const description = (.+);/);
const inputSchemaMatch = code.match(
/export const inputSchema = ({[\s\S]*?});/
);
const outputSchemaMatch = code.match(
/export const outputSchema = ({[\s\S]*?});/
);
const dependenciesMatch = code.match(
/export const dependencies = (\[[\s\S]*?]);/
);
if (!nameMatch || !descMatch || !inputSchemaMatch || !outputSchemaMatch) {
throw new Error(
"Invalid tool file: missing required exports (name, description, inputSchema, outputSchema)"
);
}
tool.name = JSON.parse(nameMatch[1]);
tool.description = JSON.parse(descMatch[1]);
tool.inputSchema = JSON.parse(inputSchemaMatch[1]);
tool.outputSchema = JSON.parse(outputSchemaMatch[1]);
if (dependenciesMatch) {
tool.dependencies = JSON.parse(dependenciesMatch[1]);
}
const metadataMarkerMatch = code.match(/\n\/\/ Metadata exports\n/);
if (metadataMarkerMatch) {
tool.execute = code.slice(0, metadataMarkerMatch.index).trim();
} else {
const firstExportMatch = code.match(/\nexport const name = /);
if (firstExportMatch && firstExportMatch.index) {
tool.execute = code.slice(0, firstExportMatch.index).trim();
} else {
throw new Error("Invalid tool file: cannot locate execute code");
}
}
if (!tool.execute) {
throw new Error("Invalid tool file: no execute code found");
}
return tool;
}
function workflowJsonToCode(workflow) {
const lines = [];
lines.push("// Step execution functions");
workflow.steps.forEach((step, index) => {
lines.push("");
lines.push(`// Step: ${step.def.name}`);
lines.push(`export const step_${index}_execute = ${step.def.execute};`);
});
lines.push("");
lines.push("// Metadata exports");
lines.push(`export const name = ${JSON.stringify(workflow.name)};`);
lines.push(
`export const description = ${JSON.stringify(workflow.description)};`
);
const stepsMetadata = workflow.steps.map((step) => ({
def: {
name: step.def.name,
title: step.def.title,
description: step.def.description,
inputSchema: step.def.inputSchema,
outputSchema: step.def.outputSchema,
dependencies: step.def.dependencies
},
input: step.input,
output: step.output,
options: step.options,
views: step.views
}));
lines.push(
`export const stepsMetadata = ${JSON.stringify(stepsMetadata, null, 2)};`
);
return lines.join("\n");
}
function workflowCodeToJson(code) {
const workflow = {};
const nameMatch = code.match(/export const name = (.+);/);
const descMatch = code.match(/export const description = (.+);/);
const stepsMetadataMatch = code.match(
/export const stepsMetadata = ([\s\S]+?);(?=\n*$)/
);
if (!nameMatch || !descMatch || !stepsMetadataMatch) {
throw new Error(
"Invalid workflow file: missing required exports (name, description, stepsMetadata)"
);
}
workflow.name = JSON.parse(nameMatch[1]);
workflow.description = JSON.parse(descMatch[1]);
const stepsMetadata = JSON.parse(stepsMetadataMatch[1]);
const stepExecuteMatches = Array.from(
code.matchAll(
/export const step_(\d+)_execute = ([\s\S]+?)(?=\n(?:\/\/ Step:|export const step_|\n\/\/ Metadata exports))/g
)
);
workflow.steps = stepsMetadata.map(
(stepMeta, index) => {
const executeMatch = stepExecuteMatches.find(
(match) => Number.parseInt(match[1], 10) === index
);
if (!executeMatch) {
throw new Error(
`Invalid workflow file: missing execute function for step ${index}`
);
}
const executeCode = executeMatch[2].trim();
const cleanExecute = executeCode.endsWith(";") ? executeCode.slice(0, -1) : executeCode;
return {
def: {
...stepMeta.def,
execute: cleanExecute
},
input: stepMeta.input,
output: stepMeta.output,
options: stepMeta.options,
views: stepMeta.views
};
}
);
return workflow;
}
function detectResourceType(filePath) {
const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
if (normalizedPath.includes("/views/") && normalizedPath.endsWith(".tsx")) {
return "view";
}
if (normalizedPath.includes("/tools/") && normalizedPath.endsWith(".ts")) {
return "tool";
}
if (normalizedPath.includes("/workflows/") && normalizedPath.endsWith(".ts")) {
return "workflow";
}
return null;
}
// src/commands/projects/export.ts
var ALLOWED_ROOTS = [
"/src/tools",
"/src/views",
"/src/workflows",
"/src/documents"
];
var AGENTS_DIR = "agents";
var DATABASE_DIR = "database";
function sanitizeTableFilename(tableName) {
return tableName.replace(/[^a-zA-Z0-9-_]/g, "-");
}
async function runWithConcurrency(items, limit, worker) {
if (items.length === 0 || limit <= 0) {
return;
}
let nextIndex = 0;
const size = Math.min(limit, items.length);
const runners = Array.from({ length: size }, async () => {
while (true) {
const currentIndex = nextIndex;
nextIndex += 1;
if (currentIndex >= items.length) {
break;
}
await worker(items[currentIndex], currentIndex);
}
});
await Promise.all(runners);
}
async function exportCommand(options) {
const { local, force } = options;
console.log("\u{1F4E6} Starting project export...\n");
let orgSlug = options.org;
if (!orgSlug) {
orgSlug = await promptWorkspace(local);
}
console.log(`\u{1F4CD} Organization: ${orgSlug}`);
let project2 = options.project;
let projectData;
if (!project2) {
projectData = await promptProject(orgSlug, local);
project2 = projectData.slug;
} else {
const client2 = await createWorkspaceClient({ workspace: "", local });
try {
const response = await client2.callTool({
name: "PROJECTS_LIST",
arguments: { org: orgSlug }
});
if (response.isError) {
throw new Error(`Failed to fetch projects: ${response.content}`);
}
const { items: projects } = response.structuredContent;
projectData = projects.find((p) => p.slug === project2);
if (!projectData) {
throw new Error(
`Project '${project2}' not found in organization '${orgSlug}'`
);
}
} finally {
await client2.close();
}
}
console.log(`\u{1F4CD} Project: ${projectData.title} (${projectData.slug})
`);
let outDir = options.out || "";
if (!outDir) {
const defaultOut = `./${orgSlug}__${projectData.slug}`;
const result = await inquirer6.prompt([
{
type: "input",
name: "outDir",
message: "Output directory:",
default: defaultOut
}
]);
outDir = result.outDir;
}
if (existsSync(outDir)) {
const files = await fs11.readdir(outDir);
if (files.length > 0) {
if (!force) {
throw new Error(
`Output directory '${outDir}' is not empty. Use --force to overwrite existing files.`
);
}
console.log(
`\u26A0\uFE0F Output directory is not empty. Using --force to overwrite.
`
);
}
} else {
mkdirSync(outDir, { recursive: true });
console.log(`\u{1F4C1} Created output directory: ${outDir}
`);
}
const resolvedOutDir = path2.resolve(outDir);
const workspace = `/${orgSlug}/${projectData.slug}`;
const client = await createWorkspaceClient({ workspace, local });
try {
console.log("\u{1F4CB} Fetching project files...");
const allFiles = [];
const resourcesByType = {
tools: [],
views: [],
workflows: [],
documents: [],
database: []
};
for (const root of ALLOWED_ROOTS) {
const response = await client.callTool({
name: "LIST_FILES",
arguments: {
branch: "main",
prefix: root
}
});
if (response.isError) {
console.warn(`\u26A0\uFE0F Failed to list files in ${root}: ${response.content}`);
continue;
}
const result = response.structuredContent;
if (result.count === 0) {
console.log(` ${root}: 0 files`);
continue;
}
console.log(` ${root}: ${result.count} files`);
const filePaths = Object.keys(result.files);
await runWithConcurrency(filePaths, 5, async (filePath) => {
try {
const content = await fetchFileContent(
filePath,
"main",
workspace,
local
);
const contentStr = content.toString("utf-8");
allFiles.push({ path: filePath, content: contentStr });
if (filePath.startsWith("/src/tools/")) {
resourcesByType.tools.push(filePath);
} else if (filePath.startsWith("/src/views/")) {
resourcesByType.views.push(filePath);
} else if (filePath.startsWith("/src/workflows/")) {
resourcesByType.workflows.push(filePath);
} else if (filePath.startsWith("/src/documents/")) {
resourcesByType.documents.push(filePath);
}
let relativePath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
if (relativePath.startsWith("src/")) {
relativePath = relativePath.slice(4);
}
const sanitizedRelativePath = sanitizeProjectPath(relativePath);
if (!sanitizedRelativePath) {
console.warn(` \u26A0\uFE0F Skipping unsafe path: ${filePath}`);
return;
}
const localPath = path2.join(outDir, sanitizedRelativePath);
const resolvedLocalPath = path2.resolve(localPath);
const relativeToOut = path2.relative(
resolvedOutDir,
resolvedLocalPath
);
if (relativeToOut.startsWith("..") || path2.isAbsolute(relativeToOut)) {
console.warn(
` \u26A0\uFE0F Skipping path outside output directory: ${sanitizedRelativePath}`
);
return;
}
await fs11.mkdir(path2.dirname(resolvedLocalPath), { recursive: true });
let finalContent = contentStr;
let finalPath = resolvedLocalPath;
if (filePath.endsWith(".json")) {
try {
const parsed = JSON.parse(contentStr);
if (filePath.startsWith("/src/views/")) {
const viewResource = parsed;
finalContent = viewJsonToCode(viewResource);
finalPath = resolvedLocalPath.replace(/\.json$/, ".tsx");
} else if (filePath.startsWith("/src/tools/")) {
const toolResource = parsed;
finalContent = toolJsonToCode(toolResource);
finalPath = resolvedLocalPath.replace(/\.json$/, ".ts");
} else if (filePath.startsWith("/src/workflows/")) {
const workflowResource = parsed;
finalContent = workflowJsonToCode(workflowResource);
finalPath = resolvedLocalPath.replace(/\.json$/, ".ts");
}
} catch (conversionError) {
console.warn(
` \u26A0\uFE0F Failed to convert ${filePath} to code file: ${conversionError instanceof Error ? conversionError.message : String(conversionError)}`
);
}
}
await fs11.writeFile(finalPath, finalContent, "utf-8");
} catch (error) {
console.warn(
` \u26A0\uFE0F Failed to download ${filePath}: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
console.log(`\u2705 Downloaded ${allFiles.length} files
`);
console.log("\u{1F464} Fetching agents...");
const agentsDir = path2.join(outDir, AGENTS_DIR);
mkdirSync(agentsDir, { recursive: true });
let agentCount = 0;
try {
const agentsListResponse = await client.callTool({
name: "AGENTS_LIST",
arguments: {}
});
if (agentsListResponse.isError) {
console.warn(
`\u26A0\uFE0F Failed to fetch agents: ${agentsListResponse.content}`
);
} else {
const agentsListData = agentsListResponse.structuredContent;
console.log(` Found ${agentsListData.items.length} agents`);
await runWithConcurrency(
agentsListData.items,
5,
async (agentSummary) => {
try {
const agentResponse = await client.callTool({
name: "AGENTS_GET",
arguments: { id: agentSummary.id }
});
if (agentResponse.isError) {
console.warn(
` \u26A0\uFE0F Failed to fetch agent ${agentSummary.name}: ${agentResponse.content}`
);
return;
}
const agent = agentResponse.structuredContent;
const exportAgent = {
name: agent.name,
avatar: agent.avatar,
instructions: agent.instructions,
description: agent.description,
tools_set: agent.tools_set,
max_steps: agent.max_steps,
max_tokens: agent.max_tokens,
model: agent.model,
memory: agent.memory,
views: agent.views,
visibility: agent.visibility,
temperature: agent.temperature
};
const safeFilename = agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
const agentFile = path2.join(agentsDir, `${safeFilename}.json`);
await fs11.writeFile(
agentFile,
JSON.stringify(exportAgent, null, 2) + "\n",
"utf-8"
);
const current = ++agentCount;
if (current % 5 === 0 || current === agentsListData.items.length) {
console.log(
` Exported ${current}/${agentsListData.items.length} agents...`
);
}
} catch (error) {
console.warn(
` \u26A0\uFE0F Failed to export agent ${agentSummary.name}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
);
console.log(` \u2705 Exported ${agentCount} agents
`);
}
} catch (error) {
console.warn(`\u26A0\uFE0F Failed to export agents: ${error}`);
}
console.log("\u{1F5C4}\uFE0F Exporting database schema...");
const databaseDir = path2.join(outDir, DATABASE_DIR);
mkdirSync(databaseDir, { recursive: true });
let tableCount = 0;
try {
const schemaResponse = await client.callTool({
name: "DATABASES_RUN_SQL",
arguments: {
sql: "SELECT type, name, tbl_name, sql FROM sqlite_master WHERE sql IS NOT NULL"
}
});
if (schemaResponse.isError) {
console.warn(
`\u26A0\uFE0F Failed to fetch database schema: ${schemaResponse.content}`
);
}
const statements = schemaResponse.structuredContent?.result ?? [];
const rows = statements.flatMap(
(statement) => Array.isArray(statement.results) ? statement.results : []
);
const tables = rows.map((row) => ({
type: String(row.type ?? ""),
name: String(row.name ?? ""),
tableName: String(row.tbl_name ?? row.name ?? ""),
sql: String(row.sql ?? "")
})).filter(
(entry) => entry.type.toLowerCase() === "table" && entry.name && entry.sql && !entry.name.startsWith("sqlite_") && !entry.name.startsWith("mastra_") && entry.sql.trim().toLowerCase().startsWith("create table")
);
const indexes = rows.map((row) => ({
type: String(row.type ?? ""),
name: String(row.name ?? ""),
tableName: String(row.tbl_name ?? ""),
sql: String(row.sql ?? "")
})).filter(
(entry) => entry.type.toLowerCase() === "index" && entry.sql && !entry.name.startsWith("sqlite_") && !entry.name.startsWith("mastra_") && tables.some((table) => table.tableName === entry.tableName)
);
const indexesByTable = /* @__PURE__ */ new Map();
for (const index of indexes) {
const collection = indexesByTable.get(index.tableName) ?? [];
collection.push({ name: index.name, sql: index.sql });
indexesByTable.set(index.tableName, collection);
}
for (const table of tables) {
const safeFilename = `${sanitizeTableFilename(table.tableName || table.name)}.json`;
const tablePath = path2.join(databaseDir, safeFilename);
const payload = {
name: table.tableName || table.name,
createSql: table.sql,
indexes: indexesByTable.get(table.tableName) ?? []
};
await fs11.writeFile(
tablePath,
JSON.stringify(payload, null, 2) + "\n",
"utf-8"
);
resourcesByType.database.push(`/${DATABASE_DIR}/${safeFilename}`);
tableCount++;
}
console.log(` \u2705 Exported ${tableCount} tables
`);
} catch (error) {
console.warn(
`\u26A0\uFE0F Failed to export database schema: ${error instanceof Error ? error.message : String(error)}`
);
}
console.log("\u{1F50D} Extracting dependencies...");
const toolFiles = allFiles.filter((f) => f.path.startsWith("/src/tools/"));
const dependencies = await extractDependenciesFromTools(toolFiles);
console.log(
` Found ${dependencies.length} MCP dependencies: ${dependencies.join(", ") || "none"}
`
);
console.log("\u{1F464} Fetching author info...");
let userEmail;
let userId;
try {
const profileResponse = await client.callTool({
name: "PROFILES_GET",
arguments: {}
});
if (!profileResponse.isError) {
const profile = profileResponse.structuredContent;
userEmail = profile.email;
userId = profile.id;
}
} catch {
}
console.log(` User: ${userEmail || "unknown"}
`);
console.log("\u{1F4DD} Writing manifest...");
const stripSrcPrefix = (paths) => paths.map((p) => p.replace(/^\/src\//, "/"));
const manifest = {
schemaVersion: "1.0",
project: {
slug: projectData.slug,
title: projectData.title,
description: projectData.description
},
author: {
orgSlug,
userId,
userEmail
},
resources: {
tools: stripSrcPrefix(resourcesByType.tools),
views: stripSrcPrefix(resourcesByType.views),
workflows: stripSrcPrefix(resourcesByType.workflows),
documents: stripSrcPrefix(resourcesByType.documents),
database: resourcesByType.database
},
dependencies: {
mcps: dependencies
},
createdAt: (/* @__PURE__ */ new Date()).toISOString()
};
await writeManifestFile(outDir, manifest);
console.log(
` \u2705 Manifest written to ${path2.join(outDir, "deco.mcp.json")}
`
);
console.log("\u{1F389} Export completed successfully!\n");
console.log("\u{1F4CA} Summary:");
console.log(` Tools: ${resourcesByType.tools.length}`);
console.log(` Views: ${resourcesByType.views.length}`);
console.log(` Workflows: ${resourcesByType.workflows.length}`);
console.log(` Documents: ${resourcesByType.documents.length}`);
console.log(` Database tables: ${resourcesByType.database.length}`);
console.log(` Agents: ${agentCount}`);
console.log(` Dependencies: ${dependencies.length}`);
console.log(` Output: ${outDir}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("\n\u{1F4A5} Export failed:", errorMessage);
process.exit(1);
} finally {
await client.close();
}
}
var ALLOWED_ROOTS2 = ["/tools", "/views", "/workflows", "/documents"];
var AGENTS_DIR2 = "agents";
var DATABASE_DIR2 = "database";
async function runWithConcurrency2(items, limit, worker) {
if (items.length === 0 || limit <= 0) {
return;
}
let nextIndex = 0;
const size = Math.min(limit, items.length);
const runners = Array.from({ length: size }, async () => {
while (true) {
const currentIndex = nextIndex;
nextIndex += 1;
if (currentIndex >= items.length) {
break;
}
await worker(items[currentIndex], currentIndex);
}
});
await Promise.all(runners);
}
var mapToRemotePath = (localPath) => {
for (const root of ALLOWED_ROOTS2) {
if (localPath.startsWith(root)) {
return `/src${localPath}`;
}
}
return localPath;
};
async function importCommand(options) {
const { local } = options;
console.log("\u{1F4E6} Starting project import...\n");
let fromDir = options.from || "./";
fromDir = path2.resolve(fromDir);
if (!existsSync(fromDir)) {
throw new Error(`Source directory '${fromDir}' does not exist`);
}
console.log(`\u{1F4CB} Reading manifest from ${fromDir}...`);
if (!await manifestExists(fromDir)) {
throw new Error(
`Manifest file 'deco.mcp.json' not found in '${fromDir}'. This directory is not a valid MCP project.`
);
}
const manifest = await readManifestFile(fromDir);
console.log(
` \u2705 Manifest validated (schema version ${manifest.schemaVersion})
`
);
let projectSlug = options.slug || manifest.project.slug;
const projectTitle = options.title || manifest.project.title;
const projectDescription = manifest.project.description;
console.log("\u{1F4CD} Project to import:");
console.log(` Slug: ${projectSlug}`);
console.log(` Title: ${projectTitle}`);
if (projectDescription) {
console.log(` Description: ${projectDescription}`);
}
console.log();
let orgSlug = options.org;
if (!orgSlug) {
orgSlug = await promptWorkspace(local);
}
console.log(`\u{1F4CD} Destination organization: ${orgSlug}
`);
console.log(`\u{1F528} Creating project '${projectSlug}'...`);
let client = await createWorkspaceClient({ workspace: "", local });
let projectId;
try {
const createResponse = await client.callTool({
name: "PROJECTS_CREATE",
arguments: {
org: orgSlug,
slug: projectSlug,
title: projectTitle,
description: projectDescription
}
});
if (createResponse.isError) {
const errorMsg = Array.isArray(createResponse.content) ? createResponse.content[0]?.text || "Failed to create project" : String(createResponse.content);
if (errorMsg.includes("already exists") || errorMsg.includes("duplicate")) {
console.error(
` \u274C Project with slug '${projectSlug}' already exists in '${orgSlug}'`
);
const result = await inquirer6.prompt([
{
type: "input",
name: "newSlug",
message: "Enter a new slug (or press Ctrl+C to abort):",
validate: (input) => input.trim().length > 0 ? true : "Slug cannot be empty"
}
]);
const newSlug = result.newSlug.trim();
const retryResponse = await client.callTool({
name: "PROJECTS_CREATE",
arguments: {
org: orgSlug,
slug: newSlug,
title: projectTitle,
description: projectDescription
}
});
if (retryResponse.isError) {
throw new Error(
`Failed to create project with slug '${newSlug}': ${retryResponse.content}`
);
}
const retryResult = retryResponse.structuredContent;
projectId = retryResult.id;
projectSlug = newSlug;
console.log(
` \u2705 Project created with slug '${newSlug}' (ID: ${projectId})
`
);
} else {
throw new Error(errorMsg);
}
} else {
const result = createResponse.structuredContent;
projectId = result.id;
console.log(` \u2705 Project created (ID: ${projectId})
`);
}
} finally {
await client.close();
}
console.log("\u{1F4E4} Pushing files to project...");
const projectWorkspace = `/${orgSlug}/${projectSlug}`;
const ignoreChecker = createIgnoreChecker(fromDir);
const filesToUpload = [];
for (const root of ALLOWED_ROOTS2) {
const localRoot = path2.join(fromDir, root.slice(1));
if (!existsSync(localRoot)) {
continue;
}
async function walkDir(dir, baseRemotePath) {
const entries = await fs11.readdir(dir, { withFileTypes: true });
const basePrefix = baseRemotePath.replace(/^\/+/, "");
for (const entry of entries) {
const localPath = path2.join(dir, entry.name);
if (ignoreChecker.isIgnored(localPath)) {
continue;
}
const joinedRemote = `${baseRemotePath}/${entry.name}`.replace(
/\\/g,
"/"
);
const sanitizedRelativePath = sanitizeProjectPath(joinedRemote);
if (!sanitizedRelativePath) {
console.warn(` \u26A0\uFE0F Skipping unsafe path: ${joinedRemote}`);
continue;
}
if (!sanitizedRelativePath.startsWith(basePrefix)) {
continue;
}
const nextRemotePath = `/${sanitizedRelativePath}`;
if (entry.isDirectory()) {
await walkDir(localPath, nextRemotePath);
} else if (entry.isFile()) {
filesToUpload.push({ remotePath: nextRemotePath, localPath });
}
}
}
await walkDir(localRoot, root);
}
console.log(` Found ${filesToUpload.length} files to upload`);
let uploadedCount = 0;
await runWithConcurrency2(
filesToUpload,
5,
async ({ remotePath, localPath }) => {
try {
const content = await fs11.readFile(localPath, "utf-8");
let finalContent = content;
let finalRemotePath = remotePath;
const resourceType = detectResourceType(localPath);
if (resourceType) {
try {
let jsonResource;
if (resourceType === "view") {
jsonResource = viewCodeToJson(content);
finalRemotePath = remotePath.replace(/\.tsx$/, ".json");
} else if (resourceType === "tool") {
jsonResource = toolCodeToJson(content);
finalRemotePath = remotePath.replace(/\.ts$/, ".json");
} else if (resourceType === "workflow") {
jsonResource = workflowCodeToJson(content);
finalRemotePath = remotePath.replace(/\.ts$/, ".json");
}
if (jsonResource) {
finalContent = JSON.stringify(jsonResource, null, 2);
}
} catch (conversionError) {
console.warn(
` \u26A0\uFE0F Failed to convert ${localPath} to JSON: ${conversionError instanceof Error ? conversionError.message : String(conversionError)}`
);
return;
}
} else if (remotePath.endsWith(".json")) {
try {
JSON.parse(content);
} catch {
console.warn(` \u26A0\uFE0F Skipping malformed JSON file: ${remotePath}`);
return;
}
}
if (/[^\x20-\x7E\r\n\t]/.test(finalContent)) {
console.warn(` \u26A0\uFE0F Skipping binary file: ${remotePath}`);
return;
}
const deconfigPath = mapToRemotePath(finalRemotePath);
await putFileContent(
deconfigPath,
finalContent,
"main",
void 0,
projectWorkspace,
local
);
const current = ++uploadedCount;
if (current % 10 === 0 || current === filesToUpload.length) {
console.log(
` Uploaded ${current}/${filesToUpload.length} files...`
);
}
} catch (error) {
console.error(
` \u274C Failed to upload ${remotePath}:`,
error instanceof Error ? error.message : String(error)
);
}
}
);
console.log(` \u2705 Uploaded ${uploadedCount} files
`);
const databasesDir = path2.join(fromDir, DATABASE_DIR2);
const agentsDir = path2.join(fromDir, AGENTS_DIR2);
let tablesImported = 0;
let agentCount = 0;
let projectClient = null;
const ensureProjectClient = async () => {
if (!projectClient) {
projectClient = await createWorkspaceClient({
workspace: projectWorkspace,
local
});
}
return projectClient;
};
try {
if (existsSync(databasesDir)) {
const tableFiles = (await fs11.readdir(databasesDir)).filter(
(name) => name.endsWith(".json")
);
if (tableFiles.length > 0) {
console.log("\u{1F5C4}\uFE0F Importing database schema...");
const projectClientInstance = await ensureProjectClient();
await runWithConcurrency2(tableFiles, 3, async (fileName) => {
try {
const filePath = path2.join(databasesDir, fileName);
const raw = await fs11.readFile(filePath, "utf-8");
const parsed = JSON.parse(raw);
if (!parsed?.name || !parsed?.createSql) {
console.warn(
` \u26A0\uFE0F Skipping invalid database schema file: ${fileName}`
);
return;
}
const tableName = parsed.name;
const indexes = Array.isArray(parsed.indexes) ? parsed.indexes.filter((index) => typeof index?.sql === "string") : [];
await projectClientInstance.callTool({
name: "DATABASES_RUN_SQL",
arguments: {
sql: parsed.createSql
}
});
for (const index of indexes) {
await projectClientInstance.callTool({
name: "DATABASES_RUN_SQL",
arguments: {
sql: String(index.sql)
}
});
}
tablesImported++;
console.log(` Imported table ${tableName}`);
} catch (error) {
console.error(
` \u274C Failed to import database schema from ${fileName}:`,
error instanceof Error ? error.message : String(error)
);
}
});
console.log(` \u2705 Imported ${tablesImported} tables
`);
} else {
console.log(" No database tables found, skipping\n");
}
} else {
console.log(" No database schema directory found, skipping\n");
}
if (existsSync(agentsDir)) {
console.log("\u{1F464} Importing agents...");
const projectClientInstance = await ensureProjectClient();
const agentFiles = await fs11.readdir(agentsDir);
const jsonFiles = agentFiles.filter((f) => f.endsWith(".json"));
console.log(` Found ${jsonFiles.length} agent files`);
await runWithConcurrency2(jsonFiles, 5, async (agentFile) => {
try {
const agentPath = path2.join(agentsDir, agentFile);
const agentContent = await fs11.readFile(agentPath, "utf-8");
const agentData = JSON.parse(agentContent);
const createAgentResponse = await projectClientInstance.callTool({
name: "AGENTS_CREATE",
arguments: agentData
});
if (createAgentResponse.isError) {
console.error(
` \u274C Failed to create agent from ${agentFile}:`,
createAgentResponse.content
);
return;
}
const current = ++agentCount;
if (current % 5 === 0 || current === jsonFiles.length) {
console.log(` Created ${current}/${jsonFiles.length} agents...`);
}
} catch (error) {
console.error(
` \u274C Failed to import agent ${agentFile}:`,
error instanceof Error ? error.message : String(error)
);
}
});
console.log(` \u2705 Imported ${agentCount} agents
`);
} else {
console.log("\u{1F464} No agents directory found, skipping\n");
}
} finally {
const maybeClosable = projectClient;
if (maybeClosable?.close) {
await maybeClosable.close();
}
}
console.log("\u{1F389} Import completed successfully!\n");
console.log("\u{1F4CA} Summary:");
console.log(` Project ID: ${projectId}`);
console.log(` Project slug: ${projectSlug}`);
console.log(` Organization: ${orgSlug}`);
console.log(` Files uploaded: ${uploadedCount}`);
console.log(` Database tables imported: ${tablesImported}`);
console.log(` Agents created: ${agentCount}`);
if (manifest.dependencies.mcps.length > 0) {
console.log(`
\u26A0\uFE0F Dependencies detected (not installed):`);
for (const mcp of manifest.dependencies.mcps) {
console.log(` - ${mcp}`);
}
console.log(
` You may need to install these integrations for full functionality.`
);
}
}
async function readStdin() {
return new Promise((resolve3, reject) => {
let data = "";
if (process28.stdin.isTTY) {
reject(
new Error("No input provided. Use -f <file> or pipe JSON via stdin.")
);
return;
}
process28.stdin.setEncoding("utf-8");
process28.stdin.on("data", (chunk) => {
data += chunk;
});
process28.stdin.on("end", () => {
resolve3(data);
});
process28.stdin.on("error", (err) => {
reject(err);
});
});
}
function validateConfig(config) {
if (!config || typeof config !== "object") {
return false;
}
const c = config;
if (typeof c.scopeName !== "string" || !c.scopeName.trim()) {
throw new Error("Missing or invalid 'scopeName' field");
}
if (typeof c.name !== "string" || !c.name.trim()) {
throw new Error("Missing or invalid 'name' field");
}
if (!c.connection || typeof c.connection !== "object") {
throw new Error("Missing or invalid 'connection' field");
}
const conn = c.connection;
const validTypes = ["HTTP", "SSE", "Websocket", "Deco", "INNATE", "BINDING"];
if (!validTypes.includes(conn.type)) {
throw new Error(
`Invalid connection type '${conn.type}'. Must be one of: ${validTypes.join(", ")}.`
);
}
return true;
}
async function publishApp({
file,
workspace,
local,
skipConfirmation
}) {
console.log(`
\u{1F4E6} Publishing app to registry...
`);
let fileContent;
const source = file ? `file "${file}"` : "stdin";
if (file) {
try {
fileContent = await promises.readFile(file, "utf-8");
} catch (error) {
throw new Error(
`Failed to read file "${file}": ${error instanceof Error ? error.message : String(error)}`
);
}
} else {
try {
fileContent = await readStdin();
} catch (error) {
throw new Error(
`Failed to read from stdin: ${error instanceof Error ? error.message : String(error)}`
);
}
}
let config;
try {
config = JSON.parse(fileContent);
} catch (error) {
throw new Error(
`Failed to parse JSON from ${source}: ${error instanceof Error ? error.message : String(error)}`
);
}
validateConfig(config);
const appConfig = config;
console.log("\u{1F4CB} Publish summary:");
console.log(` Scope: ${appConfig.scopeName}`);
console.log(` Name: ${appConfig.name}`);
console.log(` Full name: @${appConfig.scopeName}/${appConfig.name}`);
console.log(` Connection type: ${appConfig.connection.type}`);
if (appConfig.friendlyName) {
console.log(` Friendly name: ${appConfig.friendlyName}`);
}
if (appConfig.description) {
console.log(` Description: ${appConfig.description}`);
}
if (appConfig.icon) {
console.log(` Icon: ${appConfig.icon}`);
}
console.log(` Unlisted: ${appConfig.unlisted ?? true}`);
console.log(` Workspace: ${workspace}`);
console.log();
const confirmed = skipConfirmation || (await inquirer6.prompt([
{
type: "confirm",
name: "proceed",
message: "Proceed with publishing?",
default: true
}
])).proceed;
if (!confirmed) {
console.log("\u274C Publishing cancelled");
process28.exit(0);
}
const client = await createWorkspaceClientStub({ workspace, local });
const response = await client.callTool({
name: "REGISTRY_PUBLISH_APP",
arguments: {
scopeName: appConfig.scopeName,
name: appConfig.name,
connection: appConfig.connection,
friendlyName: appConfig.friendlyName,
description: appConfig.description,
icon: appConfig.icon,
metadata: appConfig.metadata,
unlisted: appConfig.unlisted,
tools: appConfig.tools
}
});
if (response.isError && Array.isArray(response.content)) {
const errorText = response.content[0]?.text ?? "Unknown error";
throw new Error(`Failed to publish app: ${errorText}`);
}
const result = response.structuredContent;
console.log(`
\u{1F389} Successfully published!`);
console.log(` App: @${appConfig.scopeName}/${appConfig.name}`);
if (result?.appName) {
console.log(` Registry name: ${result.appName}`);
}
console.log();
}
// src/commands.ts
var MIN_NODE_VERSION = "18.0.0";
var currentNodeVersion = process28.version.slice(1);
function compareVersions(version1, version2) {
const v1parts = version1.split(".").map(Number);
const v2parts = version2.split(".").map(Number);
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
const v1part = v1parts[i] || 0;
const v2part = v2parts[i] || 0;
if (v1part < v2part) return -1;
if (v1part > v2part) return 1;
}
return 0;
}
if (compareVersions(currentNodeVersion, MIN_NODE_VERSION) < 0) {
console.error(`\u274C Error: Node.js ${MIN_NODE_VERSION} or higher is required.`);
console.error(` Current version: ${process28.version}`);
console.error(` Please upgrade Node.js: https://nodejs.org/`);
process28.exit(1);
}
process28.removeAllListeners("warning");
process28.on("warning", (warning) => {
if (warning.name === "DeprecationWarning" && warning.message.includes("punycode")) {
return;
}
console.warn(warning.message);
});
var login = new Command("login").description("Log in to admin.decocms.com and retrieve tokens for CLI usage.").action(async () => {
try {
await loginCommand();
console.log("\u2705 Successfully logged in to admin.decocms.com");
} catch (error) {
console.error(
"\u274C Login failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var logout = new Command("logout").description("Log out of admin.decocms.com and remove local session data.").action(async () => {
try {
await deleteSession();
console.log("Logged out successfully. Session data removed.");
} catch (e) {
if (e instanceof Error) {
console.error("Failed to log out:", e.message);
} else {
console.error("Failed to log out:", String(e));
}
}
});
var whoami = new Command("whoami").description("Print info about the current session.").action(whoamiCommand);
var configure = new Command("configure").alias("config").description("Save configuration options for the current directory.").action(async () => {
try {
await configureCommand(getLocal());
} catch (error) {
console.error(
"\u274C Configuration failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var hostingList = new Command("list").description("List all apps in the current workspace.").option("-w, --workspace <workspace>", "Workspace name").action(async (options) => {
try {
const session = await readSession();
const workspace = options.workspace || session?.workspace;
if (!workspace) {
console.error(
"\u274C No workspace specified. Use -w flag or run 'deco configure' first."
);
process28.exit(1);
}
await listApps({ workspace });
} catch (error) {
console.error(
"\u274C Failed to list apps:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var hostingDeploy = new Command("deploy").description("Deploy the current directory into the current workspace.").option("-w, --workspace <workspace>", "Workspace name").option("-a, --app <app>", "App name").option("-y, --yes", "Skip confirmation").option("-p, --public", "Make the app public in the registry").option(
"-f, --force",
"Force the deployment even if there are breaking changes"
).option(
"--dry-run",
"Write deploy manifest to local filesystem instead of deploying"
).option("--no-promote", "Do not promote the deployment to production routes").option(
"-e, --env <value>",
'Set environment variables: KEY=VALUE, JSON object {"KEY":"VALUE"}, or file path /path/to/.env (can be used multiple times, overrides .dev.vars)',
(value, previous) => {
return previous ? [...previous, value] : [value];
}
).argument("[cwd]", "Working directory").action(async (cwd, options) => {
try {
const config = await getConfig({
inlineOptions: options
});
const wranglerConfig = await readWranglerConfig();
const assetsDirectory = wranglerConfig.assets?.directory;
const app = options.app ?? (typeof wranglerConfig.name === "string" ? wranglerConfig.name : "my-app");
await deploy({
...config,
app,
skipConfirmation: options.yes,
cwd: cwd ?? process28.cwd(),
unlisted: !options.public,
assetsDirectory,
force: options.force,
dryRun: options.dryRun,
promote: options.promote ?? true,
inlineEnvVars: options.env
});
} catch (error) {
console.error(
"\u274C Deployment failed:",
error instanceof Error ? error.message : JSON.stringify(error)
);
process28.exit(1);
}
});
var hostingPromote = new Command("promote").description("Promote a deployment to an existing route pattern.").option("-w, --workspace <workspace>", "Workspace name").option("-a, --app <app>", "App name").option("-d, --deployment <deployment>", "Deployment ID").option(
"-r, --route <route>",
"Route pattern (defaults to appName.deco.page)"
).option("-y, --yes", "Skip confirmation").action(async (options) => {
try {
const config = await getConfig({
inlineOptions: options
});
let app = options.app;
if (!app) {
try {
const wranglerConfig = await readWranglerConfig();
app = typeof wranglerConfig.name === "string" ? wranglerConfig.name : void 0;
} catch {
}
}
await promoteApp({
workspace: config.workspace,
local: config.local,
appSlug: app,
deploymentId: options.deployment,
routePattern: options.route,
skipConfirmation: options.yes
});
} catch (error) {
console.error(
"\u274C Promotion failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var linkCmd = new Command("link").description("Link the project to be accessed through a remote domain.").option("-p, --port <port>", "Port to link", parseInt).option("-e, --env <env>", "Environment variable to set").allowUnknownOption().action(async (options, cmd) => {
try {
const runCommand2 = cmd.args;
await link({
port: options.port,
onBeforeRegister: (server) => {
if (runCommand2.length === 0) {
console.log(
"\u26A0\uFE0F No command provided. Tunnel will connect to existing service on port."
);
return;
}
const [command, ...args] = runCommand2;
console.log(`\u{1F517} Starting command: ${command} ${args.join(" ")}`);
const childProcess = spawn(command, args, {
stdio: "inherit",
shell: true,
env: { ...process28.env, [options.env ?? "BASE_URL"]: server }
});
childProcess.on("error", (error) => {
console.error("\u274C Failed to start command:", error.message);
process28.exit(1);
});
return childProcess;
}
});
} catch (error) {
console.error(
"\u274C Link failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var upgrade2 = new Command("upgrade").description("Upgrade the deco CLI to the latest version.").action(upgradeCommand);
var update = new Command("update").description("Update Deco dependencies to their latest versions.").option("-y, --yes", "Skip confirmation prompts").action(async (options) => {
try {
await updateCommand({ yes: options.yes });
} catch (error) {
console.error(
"\u274C Update failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var dev = new Command("dev").description("Start a development server.").option(
"--clean-build-dir <directory>",
"Clean the build directory before starting the development server",
(directory) => {
return {
enabled: true,
directory
};
}
).option(
"--gen-watch [path]",
"Watch for TypeScript file changes and regenerate deco.gen.ts (defaults to current directory)"
).option("--vite", "Use Vite for development server").action((options) => {
devCommand({
cleanBuildDirectory: options.cleanBuildDir,
genWatch: options.genWatch === true ? "." : options.genWatch,
command: options.vite ? ["vite"] : void 0
});
});
var create = new Command("create").description("Create a new project from a template.").argument("[project-name]", "Name of the project").action(async (projectName) => {
try {
const config = await getConfig().catch(() => ({}));
await createCommand(projectName, config);
} catch (error) {
console.error(
"\u274C Project creation failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var addInstalled = new Command("installed").description("Add installed integrations to the current project.").option("-w, --workspace <workspace>", "Workspace name").action(async (options) => {
try {
await addCommand({
workspace: options.workspace,
local: getLocal()
});
} catch (error) {
console.error(
"\u274C Failed to add integrations:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var addRegistry = new Command("registry").description("Add apps from the registry to the current project.").option("-w, --workspace <workspace>", "Workspace name").action(async (options) => {
try {
await addRegistryCommand({
workspace: options.workspace,
local: getLocal()
});
} catch (error) {
console.error(
"\u274C Failed to add registry apps:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var add = new Command("add").description("Add integrations from the registry to the current project.").argument("[app-name]", "Registry app name (e.g., @deco/pinecone-assistant)").option("-w, --workspace <workspace>", "Workspace name").option("--no-gen", "Skip generating types after adding").addCommand(addInstalled).addCommand(addRegistry).action(async (appName, options) => {
if (appName) {
try {
await addRegistryCommand({
workspace: options.workspace,
local: getLocal(),
appName,
skipGen: !options.gen
});
} catch (error) {
console.error(
"\u274C Failed to add registry app:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
return;
}
try {
await addRegistryCommand({
workspace: options.workspace,
local: getLocal()
});
} catch (error) {
console.error(
"\u274C Failed to add registry apps:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var callTool = new Command("call-tool").description("Call a tool on an integration using MCP protocol.").argument("<tool>", "Name of the tool to call").option(
"-i, --integration <integration>",
"Integration ID to call the tool on"
).option("-p, --payload <payload>", "JSON payload to send to the tool").option(
"--set <key=value>",
"Set a key-value pair in the payload (can be used multiple times)",
(value, previous) => {
return previous ? [...previous, value] : [value];
}
).option("-w, --workspace <workspace>", "Workspace name").configureHelp({
subcommandTerm: (cmd) => cmd.name()
// for auto-completion
}).action(async (toolName, options) => {
if (!options.integration) {
console.error(
"\u274C Integration ID is required. Use -i or --integration flag."
);
try {
console.log("\u{1F50D} Available integrations:");
const integrations = await autocompleteIntegrations("");
if (integrations.length > 0) {
integrations.slice(0, 10).forEach((id) => console.log(` \u2022 ${id}`));
if (integrations.length > 10) {
console.log(` ... and ${integrations.length - 10} more`);
}
} else {
console.log(
" No integrations found. Run 'deco add' to add integrations."
);
}
} catch {
console.log(" Run 'deco add' to add integrations.");
}
process28.exit(1);
}
try {
await callToolCommand(toolName, {
integration: options.integration,
payload: options.payload,
set: options.set,
workspace: options.workspace
});
} catch (error) {
console.error(
"\u274C Tool call failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var completion = new Command("completion").description("Generate shell completions (internal command)").argument("<type>", "Type of completion to generate").option("--current <current>", "Current word being completed").option("--previous <previous>", "Previous word in command line").option("--line <line>", "Full command line").action(async (type, options) => {
try {
await completionCommand(type, {
current: options.current,
previous: options.previous,
line: options.line
});
} catch {
}
});
var installCompletion = new Command("install-completion").description("Install shell completion scripts").argument(
"[shell]",
"Target shell (bash, zsh). Auto-detected if not specified"
).option("-o, --output <path>", "Output path for completion script").action(async (shell, options) => {
try {
await installCompletionCommand(shell, {
output: options.output
});
} catch (error) {
console.error(
"\u274C Failed to install completion:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var hosting = new Command("hosting").description("Manage hosting apps in a workspace.").addCommand(hostingList).addCommand(hostingDeploy).addCommand(hostingPromote);
var gen = new Command("gen").description("Generate the environment that will be used to run the app.").option(
"-s, --self <url>",
"Useful to generate a SELF binding for own types based on local mcp server."
).option(
"-o, --output <path>",
"Output path for the generated environment file."
).action(async (options) => {
try {
const wranglerConfig = await readWranglerConfig();
const config = await getConfig({});
const env = await genEnv({
workspace: config.workspace,
local: config.local,
bindings: config.bindings,
selfUrl: options.self ?? `https://${getAppDomain(
config.workspace,
wranglerConfig.name ?? "my-app"
)}/mcp`
});
if (options.output) {
await writeFile(options.output, env);
} else {
console.log(env);
}
} catch (error) {
console.error(
"\u274C Failed to generate environment:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfigGet = new Command("get").description("Get a file from a deconfig branch.").argument("<path>", "File path to get").option("-b, --branch <branchName>", "Branch name", "main").option("-o, --output <file>", "Output file (defaults to stdout)").option("-w, --workspace <workspace>", "Workspace name").action(async (path4, options) => {
try {
const headConfig = await readDeconfigHead();
const finalOptions = {
branch: options.branch || headConfig?.branch || "main",
workspace: options.workspace || headConfig?.workspace
};
const config = await getConfig({
inlineOptions: { workspace: finalOptions.workspace }
});
await getCommand({
path: path4,
branch: finalOptions.branch,
output: options.output,
workspace: config.workspace,
local: config.local
});
if (options.branch || options.workspace || headConfig) {
await writeDeconfigHead({
workspace: config.workspace,
branch: finalOptions.branch,
path: headConfig?.path || ".",
pathFilter: headConfig?.pathFilter,
local: config.local
});
}
} catch (error) {
console.error(
"\u274C Get failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfigPut = new Command("put").description("Put a file to a deconfig branch.").argument("<path>", "File path to put").option("-b, --branch <branchName>", "Branch name", "main").option("-f, --file <file>", "Local file to upload").option("-c, --content <content>", "Content to upload").option("-m, --metadata <metadata>", "Metadata JSON string").option("-w, --workspace <workspace>", "Workspace name").action(async (path4, options) => {
try {
const headConfig = await readDeconfigHead();
const finalOptions = {
branch: options.branch || headConfig?.branch || "main",
workspace: options.workspace || headConfig?.workspace
};
const config = await getConfig({
inlineOptions: { workspace: finalOptions.workspace }
});
await putCommand({
path: path4,
branch: finalOptions.branch,
file: options.file,
content: options.content,
metadata: options.metadata,
workspace: config.workspace,
local: config.local
});
if (options.branch || options.workspace || headConfig) {
await writeDeconfigHead({
workspace: config.workspace,
branch: finalOptions.branch,
path: headConfig?.path || ".",
pathFilter: headConfig?.pathFilter,
local: config.local
});
}
} catch (error) {
console.error(
"\u274C Put failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfigWatch = new Command("watch").description("Watch a deconfig branch for changes.").option("-b, --branch <branchName>", "Branch name", "main").option("-p, --path <path>", "Path filter for watching specific files", ".").option(
"--from-ctime <ctime>",
"Start watching from this ctime",
(value) => parseInt(value),
1
).option("-w, --workspace <workspace>", "Workspace name").action(async (options) => {
try {
const headConfig = await readDeconfigHead();
const finalOptions = {
branch: options.branch || headConfig?.branch || "main",
workspace: options.workspace || headConfig?.workspace,
path: options.path || headConfig?.path || "."
};
const config = await getConfig({
inlineOptions: { workspace: finalOptions.workspace }
});
await watchCommand({
branch: finalOptions.branch,
path: finalOptions.path,
fromCtime: options.fromCtime,
workspace: config.workspace,
local: config.local
});
if (options.branch || options.workspace || options.path || headConfig) {
await writeDeconfigHead({
workspace: config.workspace,
branch: finalOptions.branch,
path: finalOptions.path,
pathFilter: headConfig?.pathFilter,
local: config.local
});
}
} catch (error) {
console.error(
"\u274C Watch failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfigClone = new Command("clone").description("Clone a deconfig branch to a local directory.").option("-b, --branch <branchName>", "Branch name to clone", "main").requiredOption(
"--path <path>",
"Local directory path to clone files to",
"."
).option("--path-filter <filter>", "Filter files by path pattern").option("-w, --workspace <workspace>", "Workspace name").action(async (options) => {
try {
const config = await getConfig({
inlineOptions: { workspace: options.workspace }
});
await cloneCommand({
branchName: options.branch,
path: options.path,
pathFilter: options.pathFilter,
workspace: config.workspace,
local: config.local
});
} catch (error) {
console.error(
"\u274C Clone failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfigPush = new Command("push").description(
"Push local files to a deconfig branch (rsync-like behavior with change detection)"
).option("-b, --branch <branchName>", "Branch name to push to", "main").requiredOption(
"--path <path>",
"Local directory path to push files from",
"."
).option("--path-filter <filter>", "Filter files by path pattern").option("--dry-run", "Show what would be pushed without making changes").option("--watch", "Watch directory for changes and auto-push modified files").option("-w, --workspace <workspace>", "Workspace name").addHelpText(
"after",
`
Examples:
$ deco deconfig push --path ./src --branch main
$ deco deconfig push --path ./docs --path-filter "/docs/" --dry-run
$ deco deconfig push --path ./src --watch --branch main
This command works like rsync:
- Compares local file hashes with remote file hashes
- Only uploads changed files (new or modified content)
- Respects .deconfigignore files (gitignore-style patterns)
- Built-in patterns: node_modules/, .git/, .deconfig/, .DS_Store, *.tmp, *.temp, .env.local
- Watch mode: Monitors directory and auto-pushes changes (500ms debounce)
.deconfigignore syntax (like .gitignore):
*.log # ignore all .log files
/temp # ignore temp in project root only
build/ # ignore build directories
!important.txt # negate pattern (include file)
Watch mode features:
- Performs initial push, then monitors for file changes
- Debounces rapid changes (500ms delay) to avoid spam
- Respects .deconfigignore patterns
- Graceful shutdown with Ctrl+C
Note: Deletion detection is not yet implemented - files deleted locally
will remain on the remote branch until manually deleted with 'deco deconfig delete'.
`
).action(async (options) => {
try {
const headConfig = await readDeconfigHead();
const finalOptions = {
branch: options.branch || headConfig?.branch || "main",
workspace: options.workspace || headConfig?.workspace,
path: options.path || headConfig?.path || ".",
pathFilter: options.pathFilter || headConfig?.pathFilter
};
const config = await getConfig({
inlineOptions: { workspace: finalOptions.workspace }
});
await pushCommand({
branchName: finalOptions.branch,
path: finalOptions.path,
pathFilter: finalOptions.pathFilter,
dryRun: options.dryRun,
watch: options.watch,
workspace: config.workspace,
local: config.local
});
if (options.branch || options.workspace || options.path || options.pathFilter || headConfig) {
await writeDeconfigHead({
workspace: config.workspace,
branch: finalOptions.branch,
path: finalOptions.path,
pathFilter: finalOptions.pathFilter,
local: config.local
});
}
} catch (error) {
console.error(
"\u274C Push failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfigPull = new Command("pull").description("Pull changes from a deconfig branch to local directory.").option("-b, --branch <branchName>", "Branch name to pull from", "main").requiredOption("--path <path>", "Local directory path to pull files to", ".").option("--path-filter <filter>", "Filter files by path pattern").option("--dry-run", "Show what would be changed without making changes").option("-w, --workspace <workspace>", "Workspace name").action(async (options) => {
try {
const headConfig = await readDeconfigHead();
const finalOptions = {
branch: options.branch || headConfig?.branch || "main",
workspace: options.workspace || headConfig?.workspace,
path: options.path || headConfig?.path || ".",
pathFilter: options.pathFilter || headConfig?.pathFilter
};
const config = await getConfig({
inlineOptions: { workspace: finalOptions.workspace }
});
await pullCommand({
branchName: finalOptions.branch,
path: finalOptions.path,
pathFilter: finalOptions.pathFilter,
dryRun: options.dryRun,
workspace: config.workspace,
local: config.local
});
if (options.branch || options.workspace || options.path || options.pathFilter || headConfig) {
await writeDeconfigHead({
workspace: config.workspace,
branch: finalOptions.branch,
path: finalOptions.path,
pathFilter: finalOptions.pathFilter,
local: config.local
});
}
} catch (error) {
console.error(
"\u274C Pull failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfigList = new Command("list").description("Interactively browse and view files in a deconfig branch.").option("-b, --branch <branchName>", "Branch name to list files from", "main").option("--path-filter <filter>", "Filter files by path pattern").option(
"--format <format>",
"Content display format: plainString, json, base64",
"plainString"
).option("-w, --workspace <workspace>", "Workspace name").action(async (options) => {
try {
const headConfig = await readDeconfigHead();
const finalOptions = {
branch: options.branch || headConfig?.branch || "main",
workspace: options.workspace || headConfig?.workspace,
pathFilter: options.pathFilter || headConfig?.pathFilter
};
const config = await getConfig({
inlineOptions: { workspace: finalOptions.workspace }
});
await listCommand({
branchName: finalOptions.branch,
pathFilter: finalOptions.pathFilter,
format: options.format,
workspace: config.workspace,
local: config.local
});
if (options.branch || options.workspace || options.pathFilter || headConfig) {
await writeDeconfigHead({
workspace: config.workspace,
branch: finalOptions.branch,
path: headConfig?.path || ".",
pathFilter: finalOptions.pathFilter,
local: config.local
});
}
} catch (error) {
console.error(
"\u274C List failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfigDelete = new Command("delete").description("Delete a file from a deconfig branch.").argument("<path>", "File path to delete").option("-b, --branch <branchName>", "Branch name", "main").option("-w, --workspace <workspace>", "Workspace name").action(async (path4, options) => {
try {
const headConfig = await readDeconfigHead();
const finalOptions = {
branch: options.branch || headConfig?.branch || "main",
workspace: options.workspace || headConfig?.workspace
};
const config = await getConfig({
inlineOptions: { workspace: finalOptions.workspace }
});
await deleteCommand({
path: path4,
branchName: finalOptions.branch,
workspace: config.workspace,
local: config.local
});
if (options.branch || options.workspace || headConfig) {
await writeDeconfigHead({
workspace: config.workspace,
branch: finalOptions.branch,
path: headConfig?.path || ".",
pathFilter: headConfig?.pathFilter,
local: config.local
});
}
} catch (error) {
console.error(
"\u274C Delete failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var deconfig = new Command("deconfig").description("Manage deconfig filesystem operations.").addCommand(deconfigGet).addCommand(deconfigPut).addCommand(deconfigWatch).addCommand(deconfigClone).addCommand(deconfigPush).addCommand(deconfigPull).addCommand(deconfigList).addCommand(deconfigDelete);
var projectExport = new Command("export").description("Export a project to a local directory.").option("--org <slug>", "Organization slug").option("--project <slug>", "Project slug").option("--out <dir>", "Output directory").option("--force", "Overwrite existing files in output directory").option("--local", "Use local API server (http://localhost:8787)").action(async (options) => {
try {
const config = await getConfig();
await exportCommand({
org: options.org,
project: options.project,
out: options.out,
force: options.force,
local: options.local || config.local
});
} catch (error) {
console.error(
"\u274C Export failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var projectImport = new Command("import").description("Import a project from a local directory.").argument(
"[directory]",
"Source directory (default: current directory)",
"./"
).option("--from <dir>", "Source directory (alternative to positional arg)").option("--org <slug>", "Destination organization slug").option("--slug <slug>", "Project slug override").option("--title <title>", "Project title override").action(async (directory, options) => {
try {
const config = await getConfig();
const fromDir = options.from || directory;
await importCommand({
from: fromDir,
org: options.org,
slug: options.slug,
title: options.title,
local: config.local
});
} catch (error) {
console.error(
"\u274C Import failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var project = new Command("project").description("Manage MCP project import/export.").addCommand(projectExport).addCommand(projectImport);
var registryPublish = new Command("publish").description(
"Publish an app to the registry from a JSON config file or stdin."
).option(
"-f, --file <file>",
"Path to JSON config file (reads from stdin if not provided)"
).option("-w, --workspace <workspace>", "Workspace name").option("-y, --yes", "Skip confirmation").action(async (options) => {
try {
const session = await readSession();
const workspace = options.workspace || session?.workspace;
if (!workspace) {
console.error(
"\u274C No workspace specified. Use -w flag or run 'deco configure' first."
);
process28.exit(1);
}
await publishApp({
file: options.file,
workspace,
local: getLocal(),
skipConfirmation: options.yes
});
} catch (error) {
console.error(
"\u274C Publish failed:",
error instanceof Error ? error.message : String(error)
);
process28.exit(1);
}
});
var registry = new Command("registry").description("Manage apps in the registry.").addCommand(registryPublish);
var program = new Command().name(packageInfo.name).version(packageInfo.version).description(packageInfo.description).configureOutput({
writeOut: (str) => {
if (str.includes(packageInfo.version) && str.trim() === packageInfo.version) {
const runtime = detectRuntime();
process28.stdout.write(`${packageInfo.version} (${runtime})
`);
} else {
process28.stdout.write(str);
}
},
writeErr: (str) => process28.stderr.write(str)
}).option(
"-t, --token <token>",
"Authentication token to use for API requests",
(token) => {
setToken(token);
}
).option(
"-l, --local",
`Deploy the app locally (Needs admin.decocms.com running at ${DECO_CMS_API_LOCAL})`,
() => {
setLocal(true);
}
).addHelpText("after", () => {
const runtime = detectRuntime();
return `
Runtime: ${runtime}`;
}).addCommand(login).addCommand(logout).addCommand(whoami).addCommand(hosting).addCommand(hostingDeploy).addCommand(hostingPromote).addCommand(dev).addCommand(configure).addCommand(add).addCommand(callTool).addCommand(upgrade2).addCommand(update).addCommand(linkCmd).addCommand(gen).addCommand(create).addCommand(deconfig).addCommand(project).addCommand(registry).addCommand(completion).addCommand(installCompletion);
export { deconfig, program };
//# sourceMappingURL=chunk-KHGXWUCH.js.map
//# sourceMappingURL=chunk-KHGXWUCH.js.map