@mem0/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/development.md +91 -0
- package/dist/chunk-EJ5AQPMT.js +120 -0
- package/dist/chunk-I7ABQZUR.js +111 -0
- package/dist/chunk-J7DYZDMM.js +187 -0
- package/dist/chunk-O3XZVUUX.js +252 -0
- package/dist/config-WKOCXNAS.js +86 -0
- package/dist/entities-XPRXH4X4.js +119 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +523 -0
- package/dist/init-N25QFHYP.js +161 -0
- package/dist/memory-JYJGE4VO.js +387 -0
- package/dist/utils-BAMFZ5H5.js +124 -0
- package/package.json +42 -0
- package/src/backend/base.ts +115 -0
- package/src/backend/index.ts +7 -0
- package/src/backend/platform.ts +303 -0
- package/src/branding.ts +145 -0
- package/src/commands/config.ts +90 -0
- package/src/commands/entities.ts +139 -0
- package/src/commands/init.ts +182 -0
- package/src/commands/memory.ts +487 -0
- package/src/commands/utils.ts +139 -0
- package/src/config.ts +159 -0
- package/src/help.ts +374 -0
- package/src/index.ts +501 -0
- package/src/output.ts +230 -0
- package/tests/branding.test.ts +98 -0
- package/tests/cli-integration.test.ts +156 -0
- package/tests/commands.test.ts +221 -0
- package/tests/config.test.ts +113 -0
- package/tests/output.test.ts +115 -0
- package/tests/setup.ts +75 -0
- package/tsconfig.json +18 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for mem0 CLI.
|
|
3
|
+
*
|
|
4
|
+
* Config precedence (highest to lowest):
|
|
5
|
+
* 1. CLI flags (--api-key, --base-url, etc.)
|
|
6
|
+
* 2. Environment variables (MEM0_API_KEY, etc.)
|
|
7
|
+
* 3. Config file (~/.mem0/config.json)
|
|
8
|
+
* 4. Defaults
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".mem0");
|
|
16
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
17
|
+
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
|
|
18
|
+
export const CONFIG_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
export interface PlatformConfig {
|
|
21
|
+
apiKey: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DefaultsConfig {
|
|
26
|
+
userId: string;
|
|
27
|
+
agentId: string;
|
|
28
|
+
appId: string;
|
|
29
|
+
runId: string;
|
|
30
|
+
enableGraph: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Mem0Config {
|
|
34
|
+
version: number;
|
|
35
|
+
defaults: DefaultsConfig;
|
|
36
|
+
platform: PlatformConfig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createDefaultConfig(): Mem0Config {
|
|
40
|
+
return {
|
|
41
|
+
version: CONFIG_VERSION,
|
|
42
|
+
defaults: {
|
|
43
|
+
userId: "",
|
|
44
|
+
agentId: "",
|
|
45
|
+
appId: "",
|
|
46
|
+
runId: "",
|
|
47
|
+
enableGraph: false,
|
|
48
|
+
},
|
|
49
|
+
platform: {
|
|
50
|
+
apiKey: "",
|
|
51
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ensureConfigDir(): string {
|
|
57
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
58
|
+
return CONFIG_DIR;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function loadConfig(): Mem0Config {
|
|
62
|
+
const config = createDefaultConfig();
|
|
63
|
+
|
|
64
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
65
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
66
|
+
const data = JSON.parse(raw);
|
|
67
|
+
|
|
68
|
+
config.version = data.version ?? CONFIG_VERSION;
|
|
69
|
+
|
|
70
|
+
const plat = data.platform ?? {};
|
|
71
|
+
config.platform.apiKey = plat.api_key ?? "";
|
|
72
|
+
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
|
73
|
+
|
|
74
|
+
const defaults = data.defaults ?? {};
|
|
75
|
+
config.defaults.userId = defaults.user_id ?? "";
|
|
76
|
+
config.defaults.agentId = defaults.agent_id ?? "";
|
|
77
|
+
config.defaults.appId = defaults.app_id ?? "";
|
|
78
|
+
config.defaults.runId = defaults.run_id ?? "";
|
|
79
|
+
config.defaults.enableGraph = defaults.enable_graph ?? false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Environment variable overrides
|
|
83
|
+
if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY;
|
|
84
|
+
if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL;
|
|
85
|
+
if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID;
|
|
86
|
+
if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID;
|
|
87
|
+
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
|
|
88
|
+
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
|
|
89
|
+
if (process.env.MEM0_ENABLE_GRAPH) {
|
|
90
|
+
config.defaults.enableGraph = ["true", "1", "yes"].includes(
|
|
91
|
+
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return config;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function saveConfig(config: Mem0Config): void {
|
|
99
|
+
ensureConfigDir();
|
|
100
|
+
|
|
101
|
+
const data = {
|
|
102
|
+
version: config.version,
|
|
103
|
+
defaults: {
|
|
104
|
+
user_id: config.defaults.userId,
|
|
105
|
+
agent_id: config.defaults.agentId,
|
|
106
|
+
app_id: config.defaults.appId,
|
|
107
|
+
run_id: config.defaults.runId,
|
|
108
|
+
enable_graph: config.defaults.enableGraph,
|
|
109
|
+
},
|
|
110
|
+
platform: {
|
|
111
|
+
api_key: config.platform.apiKey,
|
|
112
|
+
base_url: config.platform.baseUrl,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
|
117
|
+
fs.chmodSync(CONFIG_FILE, 0o600);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function redactKey(key: string): string {
|
|
121
|
+
if (!key) return "(not set)";
|
|
122
|
+
if (key.length <= 8) return key.slice(0, 2) + "***";
|
|
123
|
+
return key.slice(0, 4) + "..." + key.slice(-4);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Key map from dotted config path to the config object fields. */
|
|
127
|
+
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
|
128
|
+
"platform.api_key": ["platform", "apiKey"],
|
|
129
|
+
"platform.base_url": ["platform", "baseUrl"],
|
|
130
|
+
"defaults.user_id": ["defaults", "userId"],
|
|
131
|
+
"defaults.agent_id": ["defaults", "agentId"],
|
|
132
|
+
"defaults.app_id": ["defaults", "appId"],
|
|
133
|
+
"defaults.run_id": ["defaults", "runId"],
|
|
134
|
+
"defaults.enable_graph": ["defaults", "enableGraph"],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
|
|
138
|
+
const mapping = KEY_MAP[dottedKey];
|
|
139
|
+
if (!mapping) return undefined;
|
|
140
|
+
const [section, field] = mapping;
|
|
141
|
+
return (config[section] as unknown as Record<string, unknown>)[field];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean {
|
|
145
|
+
const mapping = KEY_MAP[dottedKey];
|
|
146
|
+
if (!mapping) return false;
|
|
147
|
+
const [section, field] = mapping;
|
|
148
|
+
const obj = config[section] as unknown as Record<string, unknown>;
|
|
149
|
+
|
|
150
|
+
const current = obj[field];
|
|
151
|
+
if (typeof current === "boolean") {
|
|
152
|
+
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
|
153
|
+
} else if (typeof current === "number") {
|
|
154
|
+
obj[field] = parseInt(value, 10);
|
|
155
|
+
} else {
|
|
156
|
+
obj[field] = value;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
package/src/help.ts
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich-style help formatter for Commander.js that matches the Python CLI's
|
|
3
|
+
* Typer + Rich output (rounded box panels, brand purple, grouped options).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import type { Command, Help, Option, Argument } from "commander";
|
|
8
|
+
// Colors imported from chalk directly to match Typer/Rich defaults
|
|
9
|
+
|
|
10
|
+
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const cyanBold = chalk.cyan.bold; // option flags, command names
|
|
13
|
+
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
|
|
14
|
+
const yellowBold = chalk.yellow.bold; // metavar <value>
|
|
15
|
+
const yellow = chalk.yellow; // "Usage:" label
|
|
16
|
+
const bold = chalk.bold; // command name in usage
|
|
17
|
+
const dim = chalk.dim; // defaults, descriptions
|
|
18
|
+
const dimBorder = chalk.dim; // panel borders
|
|
19
|
+
|
|
20
|
+
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line no-control-regex
|
|
23
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
24
|
+
|
|
25
|
+
function stripAnsi(str: string): number {
|
|
26
|
+
return str.replace(ANSI_RE, "").length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Command display order (matches Python CLI) ──────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
|
|
32
|
+
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
|
|
33
|
+
{
|
|
34
|
+
panel: "Memory",
|
|
35
|
+
commands: ["add", "search", "get", "list", "update", "delete"],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
panel: "Management",
|
|
39
|
+
commands: ["init", "status", "import", "help", "entity", "config"],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Flat order derived from COMMAND_GROUPS. */
|
|
44
|
+
const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
|
|
45
|
+
|
|
46
|
+
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
|
|
47
|
+
|
|
48
|
+
const OPTION_PANELS: Record<string, Record<string, string>> = {
|
|
49
|
+
add: {
|
|
50
|
+
"--user-id": "Scope",
|
|
51
|
+
"--agent-id": "Scope",
|
|
52
|
+
"--app-id": "Scope",
|
|
53
|
+
"--run-id": "Scope",
|
|
54
|
+
"--output": "Output",
|
|
55
|
+
"--api-key": "Connection",
|
|
56
|
+
"--base-url": "Connection",
|
|
57
|
+
},
|
|
58
|
+
search: {
|
|
59
|
+
"--user-id": "Scope",
|
|
60
|
+
"--agent-id": "Scope",
|
|
61
|
+
"--app-id": "Scope",
|
|
62
|
+
"--run-id": "Scope",
|
|
63
|
+
"--top-k": "Search",
|
|
64
|
+
"--threshold": "Search",
|
|
65
|
+
"--rerank": "Search",
|
|
66
|
+
"--keyword": "Search",
|
|
67
|
+
"--filter": "Search",
|
|
68
|
+
"--fields": "Search",
|
|
69
|
+
"--graph": "Search",
|
|
70
|
+
"--no-graph": "Search",
|
|
71
|
+
"--output": "Output",
|
|
72
|
+
"--api-key": "Connection",
|
|
73
|
+
"--base-url": "Connection",
|
|
74
|
+
},
|
|
75
|
+
get: {
|
|
76
|
+
"--output": "Output",
|
|
77
|
+
"--api-key": "Connection",
|
|
78
|
+
"--base-url": "Connection",
|
|
79
|
+
},
|
|
80
|
+
list: {
|
|
81
|
+
"--user-id": "Scope",
|
|
82
|
+
"--agent-id": "Scope",
|
|
83
|
+
"--app-id": "Scope",
|
|
84
|
+
"--run-id": "Scope",
|
|
85
|
+
"--page": "Pagination",
|
|
86
|
+
"--page-size": "Pagination",
|
|
87
|
+
"--category": "Filters",
|
|
88
|
+
"--after": "Filters",
|
|
89
|
+
"--before": "Filters",
|
|
90
|
+
"--graph": "Filters",
|
|
91
|
+
"--no-graph": "Filters",
|
|
92
|
+
"--output": "Output",
|
|
93
|
+
"--api-key": "Connection",
|
|
94
|
+
"--base-url": "Connection",
|
|
95
|
+
},
|
|
96
|
+
update: {
|
|
97
|
+
"--output": "Output",
|
|
98
|
+
"--api-key": "Connection",
|
|
99
|
+
"--base-url": "Connection",
|
|
100
|
+
},
|
|
101
|
+
delete: {
|
|
102
|
+
"--user-id": "Scope",
|
|
103
|
+
"--agent-id": "Scope",
|
|
104
|
+
"--app-id": "Scope",
|
|
105
|
+
"--run-id": "Scope",
|
|
106
|
+
"--output": "Output",
|
|
107
|
+
"--api-key": "Connection",
|
|
108
|
+
"--base-url": "Connection",
|
|
109
|
+
},
|
|
110
|
+
status: {
|
|
111
|
+
"--output": "Output",
|
|
112
|
+
"--api-key": "Connection",
|
|
113
|
+
"--base-url": "Connection",
|
|
114
|
+
},
|
|
115
|
+
import: {
|
|
116
|
+
"--user-id": "Scope",
|
|
117
|
+
"--agent-id": "Scope",
|
|
118
|
+
"--output": "Output",
|
|
119
|
+
"--api-key": "Connection",
|
|
120
|
+
"--base-url": "Connection",
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const PANEL_ORDER: string[] = [
|
|
125
|
+
"Scope",
|
|
126
|
+
"Search",
|
|
127
|
+
"Pagination",
|
|
128
|
+
"Filters",
|
|
129
|
+
"Output",
|
|
130
|
+
"Connection",
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// ── Panel rendering ─────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Render a Rich-style ROUNDED box panel.
|
|
137
|
+
*
|
|
138
|
+
* ```
|
|
139
|
+
* ╭─ Title ────────────────────────╮
|
|
140
|
+
* │ row content padded │
|
|
141
|
+
* ╰────────────────────────────────╯
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
function renderPanel(
|
|
145
|
+
title: string,
|
|
146
|
+
rows: string[],
|
|
147
|
+
width: number,
|
|
148
|
+
): string {
|
|
149
|
+
if (rows.length === 0) return "";
|
|
150
|
+
|
|
151
|
+
// Inner width is total width minus the two border chars
|
|
152
|
+
const inner = width - 2;
|
|
153
|
+
|
|
154
|
+
// Top border: ╭─ Title ─...─╮
|
|
155
|
+
const titleStr = ` ${title} `;
|
|
156
|
+
const fillLen = Math.max(0, inner - 1 - titleStr.length);
|
|
157
|
+
const topLine =
|
|
158
|
+
dimBorder("╭─") +
|
|
159
|
+
dimBorder(titleStr) +
|
|
160
|
+
dimBorder("─".repeat(fillLen)) +
|
|
161
|
+
dimBorder("╮");
|
|
162
|
+
|
|
163
|
+
// Bottom border: ╰─...─╯
|
|
164
|
+
const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
|
165
|
+
|
|
166
|
+
// Content rows
|
|
167
|
+
const contentLines = rows.map((row) => {
|
|
168
|
+
const visLen = stripAnsi(row);
|
|
169
|
+
const pad = Math.max(0, inner - 1 - visLen);
|
|
170
|
+
return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return [topLine, ...contentLines, bottomLine].join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Format an option term (short + long) ────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function formatOptionTerm(opt: Option): string {
|
|
179
|
+
const parts: string[] = [];
|
|
180
|
+
if (opt.short) parts.push(opt.short);
|
|
181
|
+
if (opt.long) parts.push(opt.long);
|
|
182
|
+
let term = parts.join(", ");
|
|
183
|
+
|
|
184
|
+
// Append value placeholder for non-boolean options
|
|
185
|
+
if (opt.flags) {
|
|
186
|
+
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
|
187
|
+
if (match) {
|
|
188
|
+
term += " " + match[0];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return term;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Get the long flag name for panel lookup ─────────────────────────────
|
|
195
|
+
|
|
196
|
+
function getLongFlag(opt: Option): string {
|
|
197
|
+
if (opt.long) return opt.long;
|
|
198
|
+
return opt.short || "";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Format a default value ──────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
function formatDefault(opt: Option): string {
|
|
204
|
+
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
|
|
205
|
+
return dim(` [default: ${opt.defaultValue}]`);
|
|
206
|
+
}
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── The main help formatter ─────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
export function richFormatHelp(cmd: Command, helper: Help): string {
|
|
213
|
+
const width = process.stdout.columns || 80;
|
|
214
|
+
const lines: string[] = [];
|
|
215
|
+
|
|
216
|
+
const isRoot = !cmd.parent;
|
|
217
|
+
|
|
218
|
+
// ── Usage line ──
|
|
219
|
+
const usage = helper.commandUsage(cmd);
|
|
220
|
+
lines.push("");
|
|
221
|
+
if (isRoot) {
|
|
222
|
+
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
|
223
|
+
lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`);
|
|
224
|
+
} else {
|
|
225
|
+
// Subcommands: split into command path (bold) and args (yellow)
|
|
226
|
+
const usageParts = usage.split(" ");
|
|
227
|
+
const cmdPath: string[] = [];
|
|
228
|
+
const argParts: string[] = [];
|
|
229
|
+
let pastCmd = false;
|
|
230
|
+
for (const part of usageParts) {
|
|
231
|
+
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
|
|
232
|
+
cmdPath.push(part);
|
|
233
|
+
} else {
|
|
234
|
+
pastCmd = true;
|
|
235
|
+
argParts.push(part);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`);
|
|
239
|
+
}
|
|
240
|
+
lines.push("");
|
|
241
|
+
|
|
242
|
+
// ── Description ──
|
|
243
|
+
const desc = helper.commandDescription(cmd);
|
|
244
|
+
if (desc) {
|
|
245
|
+
// Split multi-line descriptions (e.g., title + tagline)
|
|
246
|
+
const descLines = desc.split("\n");
|
|
247
|
+
for (let i = 0; i < descLines.length; i++) {
|
|
248
|
+
const dLine = descLines[i];
|
|
249
|
+
// First line is the title, subsequent non-empty lines are tagline (dimmed)
|
|
250
|
+
if (i === 0 || dLine.trim() === "") {
|
|
251
|
+
lines.push(` ${dLine}`);
|
|
252
|
+
} else {
|
|
253
|
+
lines.push(` ${dim(dLine)}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
lines.push("");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Arguments panel (subcommands only) ──
|
|
260
|
+
if (!isRoot) {
|
|
261
|
+
const visibleArgs = helper.visibleArguments(cmd);
|
|
262
|
+
if (visibleArgs.length > 0) {
|
|
263
|
+
const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length));
|
|
264
|
+
const argRows = visibleArgs.map((a: Argument) => {
|
|
265
|
+
const name = cyanBold(a.name().padEnd(maxLen));
|
|
266
|
+
const description = helper.argumentDescription(a);
|
|
267
|
+
return ` ${name} ${description}`;
|
|
268
|
+
});
|
|
269
|
+
const panel = renderPanel("Arguments", argRows, width);
|
|
270
|
+
if (panel) lines.push(panel);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Collect options (grouped into panels for subcommands) ──
|
|
275
|
+
const visibleOpts = helper.visibleOptions(cmd);
|
|
276
|
+
const cmdName = cmd.name();
|
|
277
|
+
const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {};
|
|
278
|
+
|
|
279
|
+
const grouped: Record<string, Option[]> = { Options: [] };
|
|
280
|
+
for (const panelName of PANEL_ORDER) {
|
|
281
|
+
grouped[panelName] = [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const opt of visibleOpts) {
|
|
285
|
+
const flag = getLongFlag(opt);
|
|
286
|
+
const panel = panelMap[flag];
|
|
287
|
+
if (panel && PANEL_ORDER.includes(panel)) {
|
|
288
|
+
grouped[panel].push(opt);
|
|
289
|
+
} else {
|
|
290
|
+
grouped["Options"].push(opt);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Collect commands ──
|
|
295
|
+
const visibleCmds = helper.visibleCommands(cmd);
|
|
296
|
+
|
|
297
|
+
if (isRoot) {
|
|
298
|
+
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
|
299
|
+
if (grouped["Options"].length > 0) {
|
|
300
|
+
const optRows = formatOptionRows(grouped["Options"]);
|
|
301
|
+
const panel = renderPanel("Options", optRows, width);
|
|
302
|
+
if (panel) lines.push(panel);
|
|
303
|
+
}
|
|
304
|
+
if (visibleCmds.length > 0) {
|
|
305
|
+
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
|
|
306
|
+
for (const group of COMMAND_GROUPS) {
|
|
307
|
+
const groupCmds = group.commands
|
|
308
|
+
.map((name) => cmdMap.get(name))
|
|
309
|
+
.filter((c): c is Command => c !== undefined);
|
|
310
|
+
if (groupCmds.length === 0) continue;
|
|
311
|
+
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
|
|
312
|
+
const cmdRows = groupCmds.map((c) => {
|
|
313
|
+
const name = cyanBold(c.name().padEnd(maxLen));
|
|
314
|
+
const description = helper.subcommandDescription(c);
|
|
315
|
+
return ` ${name} ${description}`;
|
|
316
|
+
});
|
|
317
|
+
const panel = renderPanel(group.panel, cmdRows, width);
|
|
318
|
+
if (panel) lines.push(panel);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
// SUBCOMMANDS: Options/panels first, then sub-subcommands
|
|
323
|
+
const panelSequence = ["Options", ...PANEL_ORDER];
|
|
324
|
+
for (const panelName of panelSequence) {
|
|
325
|
+
const opts = grouped[panelName];
|
|
326
|
+
if (opts && opts.length > 0) {
|
|
327
|
+
const optRows = formatOptionRows(opts);
|
|
328
|
+
const panel = renderPanel(panelName, optRows, width);
|
|
329
|
+
if (panel) lines.push(panel);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
|
|
333
|
+
if (visibleCmds.length > 0) {
|
|
334
|
+
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
|
|
335
|
+
const cmdRows = visibleCmds.map((c) => {
|
|
336
|
+
const name = cyanBold(c.name().padEnd(maxLen));
|
|
337
|
+
const description = helper.subcommandDescription(c);
|
|
338
|
+
return ` ${name} ${description}`;
|
|
339
|
+
});
|
|
340
|
+
const panel = renderPanel("Commands", cmdRows, width);
|
|
341
|
+
if (panel) lines.push(panel);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
lines.push("");
|
|
346
|
+
return lines.join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Format option rows with aligned columns ─────────────────────────────
|
|
350
|
+
|
|
351
|
+
function formatOptionRows(opts: Option[]): string[] {
|
|
352
|
+
const terms = opts.map((o) => formatOptionTerm(o));
|
|
353
|
+
const maxTermLen = Math.max(...terms.map((t) => t.length));
|
|
354
|
+
|
|
355
|
+
return opts.map((opt, i) => {
|
|
356
|
+
const term = cyanBold(terms[i].padEnd(maxTermLen));
|
|
357
|
+
const desc = opt.description || "";
|
|
358
|
+
const def = formatDefault(opt);
|
|
359
|
+
return ` ${term} ${desc}${def}`;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
function sortCommands(cmds: Command[]): Command[] {
|
|
366
|
+
return [...cmds].sort((a, b) => {
|
|
367
|
+
const ai = COMMAND_ORDER.indexOf(a.name());
|
|
368
|
+
const bi = COMMAND_ORDER.indexOf(b.name());
|
|
369
|
+
// Unknown commands go to end, preserving original order
|
|
370
|
+
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
|
|
371
|
+
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
|
|
372
|
+
return aIdx - bIdx;
|
|
373
|
+
});
|
|
374
|
+
}
|