Skip to content

Commit 79ed280

Browse files
committed
Adds list cmd to deconfig
Signed-off-by: Marcos Candeia <[email protected]>
1 parent d79705c commit 79ed280

File tree

4 files changed

+288
-9
lines changed

4 files changed

+288
-9
lines changed

packages/cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "deco-cli",
3-
"version": "0.19.0",
3+
"version": "0.19.2",
44
"description": "CLI for managing decocms.com apps & projects",
55
"license": "MIT",
66
"author": "Deco team",
@@ -76,4 +76,4 @@
7676
"publishConfig": {
7777
"access": "public"
7878
}
79-
}
79+
}

packages/cli/src/cli.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
watchCommand,
7979
pushCommand,
8080
pullCommand,
81+
listCommand,
8182
} from "./commands/deconfig/index.js";
8283
import { detectRuntime } from "./lib/runtime.js";
8384

@@ -539,7 +540,7 @@ const gen = new Command("gen")
539540
const deconfigGet = new Command("get")
540541
.description("Get a file from a deconfig branch.")
541542
.argument("<path>", "File path to get")
542-
.requiredOption("-b, --branch <branchName>", "Branch name")
543+
.option("-b, --branch <branchName>", "Branch name", "main")
543544
.option("-o, --output <file>", "Output file (defaults to stdout)")
544545
.option("-w, --workspace <workspace>", "Workspace name")
545546
.action(async (path, options) => {
@@ -567,7 +568,7 @@ const deconfigGet = new Command("get")
567568
const deconfigPut = new Command("put")
568569
.description("Put a file to a deconfig branch.")
569570
.argument("<path>", "File path to put")
570-
.requiredOption("-b, --branch <branchName>", "Branch name")
571+
.option("-b, --branch <branchName>", "Branch name", "main")
571572
.option("-f, --file <file>", "Local file to upload")
572573
.option("-c, --content <content>", "Content to upload")
573574
.option("-m, --metadata <metadata>", "Metadata JSON string")
@@ -598,7 +599,7 @@ const deconfigPut = new Command("put")
598599
// Watch command for deconfig
599600
const deconfigWatch = new Command("watch")
600601
.description("Watch a deconfig branch for changes.")
601-
.requiredOption("-b, --branch <branchName>", "Branch name")
602+
.option("-b, --branch <branchName>", "Branch name", "main")
602603
.option("-p, --path <path>", "Path filter for watching specific files")
603604
.option(
604605
"--from-ctime <ctime>",
@@ -631,7 +632,7 @@ const deconfigWatch = new Command("watch")
631632
// Clone command for deconfig
632633
const deconfigClone = new Command("clone")
633634
.description("Clone a deconfig branch to a local directory.")
634-
.requiredOption("-b, --branch <branchName>", "Branch name to clone")
635+
.option("-b, --branch <branchName>", "Branch name to clone", "main")
635636
.requiredOption("--path <path>", "Local directory path to clone files to")
636637
.option("--path-filter <filter>", "Filter files by path pattern")
637638
.option("-w, --workspace <workspace>", "Workspace name")
@@ -659,7 +660,7 @@ const deconfigClone = new Command("clone")
659660
// Push command for deconfig
660661
const deconfigPush = new Command("push")
661662
.description("Push local files to a deconfig branch.")
662-
.requiredOption("-b, --branch <branchName>", "Branch name to push to")
663+
.option("-b, --branch <branchName>", "Branch name to push to", "main")
663664
.requiredOption("--path <path>", "Local directory path to push files from")
664665
.option("--path-filter <filter>", "Filter files by path pattern")
665666
.option("--dry-run", "Show what would be pushed without making changes")
@@ -689,7 +690,7 @@ const deconfigPush = new Command("push")
689690
// Pull command for deconfig
690691
const deconfigPull = new Command("pull")
691692
.description("Pull changes from a deconfig branch to local directory.")
692-
.requiredOption("-b, --branch <branchName>", "Branch name to pull from")
693+
.option("-b, --branch <branchName>", "Branch name to pull from", "main")
693694
.requiredOption("--path <path>", "Local directory path to pull files to")
694695
.option("--path-filter <filter>", "Filter files by path pattern")
695696
.option("--dry-run", "Show what would be changed without making changes")
@@ -716,6 +717,38 @@ const deconfigPull = new Command("pull")
716717
}
717718
});
718719

720+
// List command for deconfig
721+
const deconfigList = new Command("list")
722+
.description("Interactively browse and view files in a deconfig branch.")
723+
.option("-b, --branch <branchName>", "Branch name to list files from", "main")
724+
.option("--path-filter <filter>", "Filter files by path pattern")
725+
.option(
726+
"--format <format>",
727+
"Content display format: plainString, json, base64",
728+
"plainString",
729+
)
730+
.option("-w, --workspace <workspace>", "Workspace name")
731+
.action(async (options) => {
732+
try {
733+
const config = await getConfig({
734+
inlineOptions: { workspace: options.workspace },
735+
});
736+
await listCommand({
737+
branchName: options.branch,
738+
pathFilter: options.pathFilter,
739+
format: options.format,
740+
workspace: config.workspace,
741+
local: config.local,
742+
});
743+
} catch (error) {
744+
console.error(
745+
"❌ List failed:",
746+
error instanceof Error ? error.message : String(error),
747+
);
748+
process.exit(1);
749+
}
750+
});
751+
719752
// Deconfig parent command
720753
const deconfig = new Command("deconfig")
721754
.description("Manage deconfig filesystem operations.")
@@ -724,7 +757,8 @@ const deconfig = new Command("deconfig")
724757
.addCommand(deconfigWatch)
725758
.addCommand(deconfigClone)
726759
.addCommand(deconfigPush)
727-
.addCommand(deconfigPull);
760+
.addCommand(deconfigPull)
761+
.addCommand(deconfigList);
728762

729763
// Main CLI program
730764
const program = new Command()

packages/cli/src/commands/deconfig/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { watchCommand } from "./watch.js";
44
export { cloneCommand } from "./clone.js";
55
export { pushCommand } from "./push.js";
66
export { pullCommand } from "./pull.js";
7+
export { listCommand } from "./list.js";
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import inquirer from "inquirer";
2+
import { fetchFileContent } from "./base.js";
3+
import process from "node:process";
4+
5+
interface ListOptions {
6+
branchName: string;
7+
pathFilter?: string;
8+
workspace?: string;
9+
local?: boolean;
10+
format?: "plainString" | "json" | "base64";
11+
}
12+
13+
interface FileInfo {
14+
address: string;
15+
metadata: Record<string, unknown>;
16+
sizeInBytes: number;
17+
mtime: number;
18+
ctime: number;
19+
}
20+
21+
export async function listCommand(options: ListOptions): Promise<void> {
22+
const {
23+
branchName,
24+
pathFilter,
25+
workspace,
26+
local,
27+
format = "plainString",
28+
} = options;
29+
30+
console.log(`📋 Listing files in branch "${branchName}"...`);
31+
if (pathFilter) {
32+
console.log(` 🔍 Path filter: ${pathFilter}`);
33+
}
34+
35+
// Get list of files from the branch
36+
const { createWorkspaceClient } = await import("../../lib/mcp.js");
37+
const client = await createWorkspaceClient({ workspace, local });
38+
39+
try {
40+
const response = await client.callTool({
41+
name: "LIST_FILES",
42+
arguments: {
43+
branch: branchName,
44+
prefix: pathFilter,
45+
},
46+
});
47+
48+
if (response.isError) {
49+
const errorMessage = Array.isArray(response.content)
50+
? response.content[0]?.text || "Failed to list files"
51+
: "Failed to list files";
52+
throw new Error(errorMessage);
53+
}
54+
55+
const result = response.structuredContent as {
56+
files: Record<string, FileInfo>;
57+
count: number;
58+
};
59+
60+
if (result.count === 0) {
61+
console.log("📂 No files found in this branch.");
62+
return;
63+
}
64+
65+
console.log(`📋 Found ${result.count} files`);
66+
67+
// Prepare file choices for interactive selection
68+
const fileChoices = Object.entries(result.files).map(([path, info]) => {
69+
const size = formatFileSize(info.sizeInBytes);
70+
const lastModified = new Date(info.mtime).toLocaleString();
71+
return {
72+
name: `${path} (${size}, modified: ${lastModified})`,
73+
value: path,
74+
short: path,
75+
};
76+
});
77+
78+
// Add option to exit
79+
fileChoices.push({
80+
name: "🚪 Exit",
81+
value: "__EXIT__",
82+
short: "Exit",
83+
});
84+
85+
while (true) {
86+
// Interactive file selection
87+
const { selectedFile } = await inquirer.prompt([
88+
{
89+
type: "list",
90+
name: "selectedFile",
91+
message: "Select a file to view its content:",
92+
choices: fileChoices,
93+
pageSize: 15,
94+
},
95+
]);
96+
97+
if (selectedFile === "__EXIT__") {
98+
console.log("👋 Goodbye!");
99+
break;
100+
}
101+
102+
// Display file content
103+
try {
104+
console.log(`\n📄 Loading content for: ${selectedFile}`);
105+
106+
const content = await fetchFileContent(
107+
selectedFile,
108+
branchName,
109+
workspace,
110+
local,
111+
);
112+
113+
const fileInfo = result.files[selectedFile];
114+
115+
console.log(`\n${"=".repeat(60)}`);
116+
console.log(`📁 File: ${selectedFile}`);
117+
console.log(`📊 Size: ${formatFileSize(fileInfo.sizeInBytes)}`);
118+
console.log(
119+
`⏰ Modified: ${new Date(fileInfo.mtime).toLocaleString()}`,
120+
);
121+
console.log(`🏷️ Address: ${fileInfo.address}`);
122+
if (Object.keys(fileInfo.metadata).length > 0) {
123+
console.log(
124+
`📋 Metadata: ${JSON.stringify(fileInfo.metadata, null, 2)}`,
125+
);
126+
}
127+
console.log(`${"=".repeat(60)}\n`);
128+
129+
// Display content based on format
130+
let displayContent: string;
131+
132+
switch (format) {
133+
case "json":
134+
try {
135+
const text = content.toString("utf-8");
136+
const parsed = JSON.parse(text);
137+
displayContent = JSON.stringify(parsed, null, 2);
138+
} catch {
139+
displayContent = content.toString("utf-8");
140+
}
141+
break;
142+
case "base64":
143+
displayContent = content.toString("base64");
144+
break;
145+
case "plainString":
146+
default:
147+
displayContent = content.toString("utf-8");
148+
break;
149+
}
150+
151+
// For large files, show a preview with option to see more
152+
const lines = displayContent.split("\n");
153+
const maxPreviewLines = 50;
154+
155+
if (lines.length > maxPreviewLines) {
156+
console.log(lines.slice(0, maxPreviewLines).join("\n"));
157+
console.log(`\n... (${lines.length - maxPreviewLines} more lines)`);
158+
159+
const { showMore } = await inquirer.prompt([
160+
{
161+
type: "confirm",
162+
name: "showMore",
163+
message: "Show the complete file content?",
164+
default: false,
165+
},
166+
]);
167+
168+
if (showMore) {
169+
console.log(`\n${"=".repeat(60)}`);
170+
console.log("📄 Complete file content:");
171+
console.log(`${"=".repeat(60)}\n`);
172+
console.log(displayContent);
173+
}
174+
} else {
175+
console.log(displayContent);
176+
}
177+
178+
console.log(`\n${"=".repeat(60)}\n`);
179+
180+
// Ask if user wants to continue browsing
181+
const { continueReading } = await inquirer.prompt([
182+
{
183+
type: "confirm",
184+
name: "continueReading",
185+
message: "Continue browsing files?",
186+
default: true,
187+
},
188+
]);
189+
190+
if (!continueReading) {
191+
console.log("👋 Goodbye!");
192+
break;
193+
}
194+
} catch (error) {
195+
console.error(
196+
`❌ Failed to read file ${selectedFile}:`,
197+
error instanceof Error ? error.message : String(error),
198+
);
199+
200+
const { tryAgain } = await inquirer.prompt([
201+
{
202+
type: "confirm",
203+
name: "tryAgain",
204+
message: "Continue browsing other files?",
205+
default: true,
206+
},
207+
]);
208+
209+
if (!tryAgain) {
210+
break;
211+
}
212+
}
213+
}
214+
} catch (error) {
215+
const errorMessage = error instanceof Error ? error.message : String(error);
216+
217+
if (
218+
errorMessage.includes("Session not found") ||
219+
errorMessage.includes("Session expired")
220+
) {
221+
console.error("💥 List failed: Authentication required");
222+
console.error(
223+
" Please run 'deco login' first to authenticate with deco.chat",
224+
);
225+
} else {
226+
console.error("💥 List failed:", errorMessage);
227+
}
228+
229+
process.exit(1);
230+
} finally {
231+
// Always close the client connection
232+
await client.close();
233+
}
234+
}
235+
236+
function formatFileSize(bytes: number): string {
237+
if (bytes === 0) return "0 B";
238+
239+
const k = 1024;
240+
const sizes = ["B", "KB", "MB", "GB"];
241+
const i = Math.floor(Math.log(bytes) / Math.log(k));
242+
243+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
244+
}

0 commit comments

Comments
 (0)