@dench.com/cli 3.8.1-staging.390 → 3.8.2-staging.392
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/ads.ts +6 -2
- package/apps.ts +7 -3
- package/browser.ts +8 -3
- package/campaign.ts +1 -1
- package/chat.ts +6 -2
- package/cron.ts +7 -3
- package/dench.ts +52 -94
- package/image.ts +6 -2
- package/leads.ts +6 -2
- package/lib/command-args.ts +26 -9
- package/lib/command-help.ts +192 -39
- package/lib/file-command-contract.ts +106 -0
- package/lib/references.generated.json +3 -3
- package/meetings.ts +6 -2
- package/members.ts +6 -2
- package/package.json +1 -1
- package/search.ts +6 -2
- package/tools.ts +9 -5
- package/web.ts +20 -6
package/lib/command-help.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { COMMAND_ARGUMENTS } from "./command-args";
|
|
1
|
+
import { COMMAND_ARGUMENTS, normalizeCommandAlias } from "./command-args";
|
|
2
2
|
import { apiOperations } from "./command-registry";
|
|
3
|
+
import {
|
|
4
|
+
FILE_COMMAND_ARGUMENTS,
|
|
5
|
+
FILE_COMMAND_NOTES,
|
|
6
|
+
} from "./file-command-contract";
|
|
3
7
|
|
|
4
|
-
/** Literal command words only;
|
|
8
|
+
/** Literal command words only; placeholders and quoted data are not commands. */
|
|
5
9
|
function words(usage: string): string[] {
|
|
6
10
|
const parts = usage
|
|
7
11
|
.replace(/^dench\s+/, "")
|
|
8
12
|
.split(/\s+/)
|
|
9
13
|
.filter(Boolean);
|
|
10
|
-
const end = parts.findIndex((part) => /^[<[(
|
|
14
|
+
const end = parts.findIndex((part) => /^[<[('"|-]/.test(part));
|
|
11
15
|
return end < 0 ? parts : parts.slice(0, end);
|
|
12
16
|
}
|
|
13
|
-
|
|
17
|
+
/** Resolve only actual CLI aliases, while callers retain the requested spelling. */
|
|
18
|
+
export function normalizeHelpCommand(argv: readonly string[]): string[] {
|
|
19
|
+
return normalizeCommandAlias(argv.filter((arg) => arg !== "help"));
|
|
20
|
+
}
|
|
14
21
|
function matches(
|
|
15
22
|
prefix: readonly string[],
|
|
16
23
|
candidate: readonly string[],
|
|
@@ -19,28 +26,127 @@ function matches(
|
|
|
19
26
|
candidate[index]?.split("|").includes(word),
|
|
20
27
|
);
|
|
21
28
|
}
|
|
29
|
+
type Entry = {
|
|
30
|
+
usage: string;
|
|
31
|
+
description: string;
|
|
32
|
+
rank: number;
|
|
33
|
+
examples?: string[];
|
|
34
|
+
notes?: string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Even a dispatcher with no richer usage must disclose required arguments. */
|
|
38
|
+
export function argumentUsage(
|
|
39
|
+
entry: (typeof COMMAND_ARGUMENTS)[number],
|
|
40
|
+
): string {
|
|
41
|
+
if (entry.usage) return entry.usage;
|
|
42
|
+
const [min, max] = entry.positionals;
|
|
43
|
+
const count = max ?? min;
|
|
44
|
+
const positional = Array.from({ length: count }, (_, i) => {
|
|
45
|
+
const name = count === 1 ? "argument" : `argument${i + 1}`;
|
|
46
|
+
return i < min ? `<${name}>` : `[<${name}>]`;
|
|
47
|
+
});
|
|
48
|
+
if (max === null) positional.push("[arguments...]");
|
|
49
|
+
return [
|
|
50
|
+
`dench ${entry.command}`,
|
|
51
|
+
...positional,
|
|
52
|
+
...entry.values.map((flag) => `[${flag} <value>]`),
|
|
53
|
+
...entry.switches.map((flag) => `[${flag}]`),
|
|
54
|
+
].join(" ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function nativeFileUsage(usage: string): boolean {
|
|
58
|
+
const path = words(usage);
|
|
59
|
+
return (
|
|
60
|
+
path[0] !== "files" ||
|
|
61
|
+
path.length < 2 ||
|
|
62
|
+
FILE_COMMAND_ARGUMENTS.some((entry) =>
|
|
63
|
+
matches(path.slice(0, 2), entry.command.split(" ")),
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Prefer richer syntax PER command, never suppress uncovered sibling commands. */
|
|
69
|
+
function preferredEntries(entries: Entry[]): Entry[] {
|
|
70
|
+
const groups = new Map<string, Entry[]>();
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const key = words(entry.usage).join(" ");
|
|
73
|
+
if (!key || !nativeFileUsage(entry.usage)) continue;
|
|
74
|
+
const existing = groups.get(key) ?? [];
|
|
75
|
+
const rank = existing[0]?.rank ?? -1;
|
|
76
|
+
if (entry.rank > rank) groups.set(key, [entry]);
|
|
77
|
+
else if (
|
|
78
|
+
entry.rank === rank &&
|
|
79
|
+
!existing.some((item) => item.usage === entry.usage)
|
|
80
|
+
) {
|
|
81
|
+
groups.set(key, [...existing, entry]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...groups.values()].flat();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stripUsageComment(line: string): string {
|
|
88
|
+
let quote = "";
|
|
89
|
+
for (let i = 0; i < line.length; i++) {
|
|
90
|
+
const char = line[i];
|
|
91
|
+
if (char === "\\" && quote !== "'") {
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (char === quote) quote = "";
|
|
96
|
+
else if (!quote && (char === "'" || char === '"')) quote = char;
|
|
97
|
+
else if (!quote && char === "#" && /\s/.test(line[i - 1] ?? ""))
|
|
98
|
+
return line.slice(0, i).trim();
|
|
99
|
+
}
|
|
100
|
+
return line.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Preserve immediately following option lines without turning prose into syntax. */
|
|
104
|
+
function usageEntries(text: string): Entry[] {
|
|
105
|
+
const entries: Entry[] = [];
|
|
106
|
+
let current: Entry | undefined;
|
|
107
|
+
let indent = 0;
|
|
108
|
+
for (const line of text.split("\n")) {
|
|
109
|
+
const value = line.trim();
|
|
110
|
+
const depth = line.length - line.trimStart().length;
|
|
111
|
+
if (value.startsWith("dench ")) {
|
|
112
|
+
current = {
|
|
113
|
+
usage: stripUsageComment(value).replace(/\\$/, "").trim(),
|
|
114
|
+
description: "",
|
|
115
|
+
rank: 3,
|
|
116
|
+
};
|
|
117
|
+
entries.push(current);
|
|
118
|
+
indent = depth;
|
|
119
|
+
} else if (current && depth > indent && /^(?:\[?-{1,2}|<)/.test(value)) {
|
|
120
|
+
current.usage += ` ${stripUsageComment(value).replace(/\\$/, "").trim()}`;
|
|
121
|
+
} else {
|
|
122
|
+
current = undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return entries;
|
|
126
|
+
}
|
|
22
127
|
|
|
23
128
|
/** Registry metadata supplies discovery, never invokes command implementations. */
|
|
24
129
|
export function commandHelp(argv: readonly string[], usageText = ""): string {
|
|
25
130
|
const args = [...argv];
|
|
26
131
|
if (args[0] === "help") args.shift();
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
132
|
+
const requested = words(args.filter((arg) => arg !== "help").join(" "));
|
|
133
|
+
const input = normalizeHelpCommand(requested);
|
|
134
|
+
const aliased = input.join(" ") !== requested.join(" ");
|
|
135
|
+
if (input[0] === "files" && input[1] === "mkdir") {
|
|
136
|
+
return "dench files mkdir is not a native CLI command. Directory creation is available as files.mkdir through the API/SDK. Run dench files --help for supported file commands.";
|
|
137
|
+
}
|
|
138
|
+
const candidates: Entry[] = [
|
|
139
|
+
...usageEntries(usageText),
|
|
34
140
|
...COMMAND_ARGUMENTS.map((entry) => ({
|
|
35
|
-
usage:
|
|
36
|
-
|
|
37
|
-
|
|
141
|
+
usage: argumentUsage(entry),
|
|
142
|
+
description: entry.description ?? "",
|
|
143
|
+
rank: entry.usage ? 4 : 2,
|
|
144
|
+
examples: entry.examples,
|
|
145
|
+
notes: entry.notes,
|
|
38
146
|
})),
|
|
39
147
|
...apiOperations
|
|
40
148
|
.filter((op) => {
|
|
41
149
|
if (!op.deprecated) return true;
|
|
42
|
-
// Keep exact legacy leaf help available without adding those commands
|
|
43
|
-
// back to the broad index or a parent family's discovery.
|
|
44
150
|
const command = words(op.cli);
|
|
45
151
|
return (
|
|
46
152
|
command.length > 1 &&
|
|
@@ -53,41 +159,88 @@ export function commandHelp(argv: readonly string[], usageText = ""): string {
|
|
|
53
159
|
description: op.deprecated
|
|
54
160
|
? `Compatibility command. ${op.summary}`
|
|
55
161
|
: op.summary,
|
|
162
|
+
rank: 1,
|
|
56
163
|
})),
|
|
57
164
|
];
|
|
165
|
+
const preferred = preferredEntries(candidates);
|
|
166
|
+
if (!input.length) {
|
|
167
|
+
const families = new Map<string, Set<string>>();
|
|
168
|
+
for (const entry of preferred) {
|
|
169
|
+
const path = words(entry.usage);
|
|
170
|
+
if (!path.length) continue;
|
|
171
|
+
const set = families.get(path[0]) ?? new Set<string>();
|
|
172
|
+
set.add(path.join(" "));
|
|
173
|
+
families.set(path[0], set);
|
|
174
|
+
}
|
|
175
|
+
return [
|
|
176
|
+
"Dench CLI",
|
|
177
|
+
"Usage: dench <command> [arguments] [options]",
|
|
178
|
+
"",
|
|
179
|
+
"Commands:",
|
|
180
|
+
...[...families]
|
|
181
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
182
|
+
.map(
|
|
183
|
+
([name, paths]) =>
|
|
184
|
+
` ${name}${paths.size > 1 ? ` (${paths.size} commands)` : ""}`,
|
|
185
|
+
),
|
|
186
|
+
"",
|
|
187
|
+
"Run dench <command> --help or dench <command> <subcommand> --help for syntax and examples.",
|
|
188
|
+
"Connect: dench signin. Inspect the workspace: dench context --json.",
|
|
189
|
+
"--json selects structured results. --help and -h run locally without authentication.",
|
|
190
|
+
"Backend overrides: --prod, --staging, --backend <name>, --host <url>.",
|
|
191
|
+
"Versioned references: dench reference --list --json.",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
58
194
|
let prefix = input;
|
|
59
195
|
while (
|
|
60
196
|
prefix.length &&
|
|
61
|
-
!
|
|
62
|
-
)
|
|
197
|
+
!preferred.some((item) => matches(prefix, words(item.usage)))
|
|
198
|
+
)
|
|
63
199
|
prefix = prefix.slice(0, -1);
|
|
64
|
-
|
|
65
|
-
const selected = candidates.filter((item) =>
|
|
200
|
+
const selected = preferred.filter((item) =>
|
|
66
201
|
matches(prefix, words(item.usage)),
|
|
67
202
|
);
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
203
|
+
const lines = selected.slice(0, 12);
|
|
204
|
+
const exact = lines.filter(
|
|
205
|
+
(item) => words(item.usage).length === prefix.length,
|
|
206
|
+
);
|
|
207
|
+
const contract = COMMAND_ARGUMENTS.find(
|
|
208
|
+
(entry) => entry.command === prefix.join(" "),
|
|
209
|
+
);
|
|
210
|
+
const printedUsage = lines.map((item) => item.usage).join(" ");
|
|
211
|
+
const missingFlags = contract
|
|
212
|
+
? [
|
|
213
|
+
...contract.values
|
|
214
|
+
.filter((flag) => !printedUsage.includes(flag))
|
|
215
|
+
.map((flag) => `${flag} <value>`),
|
|
216
|
+
...contract.switches.filter((flag) => !printedUsage.includes(flag)),
|
|
217
|
+
]
|
|
218
|
+
: [];
|
|
219
|
+
const displayPath = aliased
|
|
220
|
+
? requested.slice(0, prefix.length + requested.length - input.length)
|
|
221
|
+
: prefix;
|
|
78
222
|
return [
|
|
79
|
-
|
|
80
|
-
...
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
...(lines.length > 12
|
|
223
|
+
`Usage: dench ${displayPath.join(" ")}${displayPath.length ? "" : "<command>"}`,
|
|
224
|
+
...(aliased ? [`Alias of dench ${prefix.join(" ")}.`] : []),
|
|
225
|
+
...lines.map(
|
|
226
|
+
(item) =>
|
|
227
|
+
` ${item.usage}${item.description ? `\n ${item.description}` : ""}`,
|
|
228
|
+
),
|
|
229
|
+
...(selected.length > 12
|
|
87
230
|
? [
|
|
88
|
-
` ${
|
|
231
|
+
` ${selected.length - 12} more commands; add a subcommand before --help.`,
|
|
89
232
|
]
|
|
90
233
|
: []),
|
|
234
|
+
...(missingFlags.length
|
|
235
|
+
? [`Additional options: ${missingFlags.join(", ")}`]
|
|
236
|
+
: []),
|
|
237
|
+
...exact.flatMap((item) => item.notes ?? []),
|
|
238
|
+
...(prefix.length === 1 && prefix[0] === "files" ? FILE_COMMAND_NOTES : []),
|
|
239
|
+
...exact.flatMap((item) =>
|
|
240
|
+
item.examples?.length
|
|
241
|
+
? ["Examples:", ...item.examples.map((value) => ` ${value}`)]
|
|
242
|
+
: [],
|
|
243
|
+
),
|
|
91
244
|
"Options: --help, -h (local help); --json (structured command results).",
|
|
92
245
|
].join("\n");
|
|
93
246
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Native file commands implemented by cli/dench.ts. API operations are not
|
|
2
|
+
* necessarily CLI commands: files.mkdir is available only through the API/SDK. */
|
|
3
|
+
type FileCommandArguments = {
|
|
4
|
+
command: string;
|
|
5
|
+
positionals: readonly [number, number];
|
|
6
|
+
values: string[];
|
|
7
|
+
switches: string[];
|
|
8
|
+
flagPositionals: Record<string, readonly [number, number | null]>;
|
|
9
|
+
usage: string;
|
|
10
|
+
description: string;
|
|
11
|
+
examples: string[];
|
|
12
|
+
notes: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const FILE_COMMAND_ARGUMENTS: FileCommandArguments[] = [
|
|
16
|
+
{
|
|
17
|
+
command: "files ls",
|
|
18
|
+
positionals: [0, 1] as const,
|
|
19
|
+
values: [],
|
|
20
|
+
switches: ["--recursive", "-r"],
|
|
21
|
+
flagPositionals: {},
|
|
22
|
+
usage: "dench files ls [<path>] [--recursive|-r] [--json]",
|
|
23
|
+
description: "List workspace files; the default path is /.",
|
|
24
|
+
examples: ["dench files ls /reports --recursive --json"],
|
|
25
|
+
notes: ["--recursive (or -r) includes descendants."],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
command: "files cat",
|
|
29
|
+
positionals: [1, 1] as const,
|
|
30
|
+
values: [],
|
|
31
|
+
switches: [],
|
|
32
|
+
flagPositionals: {},
|
|
33
|
+
usage: "dench files cat <path> [--json]",
|
|
34
|
+
description: "Read a workspace file's inline text.",
|
|
35
|
+
examples: ["dench files cat /reports/summary.md"],
|
|
36
|
+
notes: [
|
|
37
|
+
"Use cat for virtual files such as IDENTITY.md and ORGANISATION.md, which have no download blob.",
|
|
38
|
+
"For binary files or files without inline text, use files download.",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
command: "files write",
|
|
43
|
+
positionals: [1, 1] as const,
|
|
44
|
+
values: [],
|
|
45
|
+
switches: [],
|
|
46
|
+
flagPositionals: {},
|
|
47
|
+
usage: "dench files write <path> [--json]",
|
|
48
|
+
description: "Write a workspace file's text from stdin.",
|
|
49
|
+
examples: [
|
|
50
|
+
"dench files write /reports/summary.md --json < ./summary.md",
|
|
51
|
+
"printf '%s\\n' 'Report ready.' | dench files write /reports/summary.md --json",
|
|
52
|
+
],
|
|
53
|
+
notes: [
|
|
54
|
+
"The destination path is required. Pipe or redirect the raw UTF-8 file text into stdin.",
|
|
55
|
+
"--json controls the result format; stdin is file content, not an API JSON envelope. No --stdin, --content or --file flag is supported.",
|
|
56
|
+
"Writing replaces the complete text; empty stdin writes an empty file.",
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
command: "files mv",
|
|
61
|
+
positionals: [2, 2] as const,
|
|
62
|
+
values: [],
|
|
63
|
+
switches: [],
|
|
64
|
+
flagPositionals: {},
|
|
65
|
+
usage: "dench files mv <src> <dst> [--json]",
|
|
66
|
+
description: "Move or rename a workspace file or directory.",
|
|
67
|
+
examples: ["dench files mv /reports/draft.md /reports/summary.md --json"],
|
|
68
|
+
notes: ["Both paths refer to workspace storage, not local disk."],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
command: "files rm",
|
|
72
|
+
positionals: [1, 1] as const,
|
|
73
|
+
values: [],
|
|
74
|
+
switches: ["--recursive", "-r"],
|
|
75
|
+
flagPositionals: {},
|
|
76
|
+
usage: "dench files rm <path> [--recursive|-r] [--json]",
|
|
77
|
+
description: "Remove a workspace file or directory.",
|
|
78
|
+
examples: ["dench files rm /reports/old --recursive --json"],
|
|
79
|
+
notes: [
|
|
80
|
+
"Directories require --recursive (or -r). An absent path is already removed.",
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
command: "files download",
|
|
85
|
+
positionals: [1, 2] as const,
|
|
86
|
+
values: [],
|
|
87
|
+
switches: ["--url"],
|
|
88
|
+
flagPositionals: {},
|
|
89
|
+
usage: "dench files download <path> [<local-dest>] [--url] [--json]",
|
|
90
|
+
description:
|
|
91
|
+
"Download workspace bytes to local disk, or print a signed URL.",
|
|
92
|
+
examples: [
|
|
93
|
+
"dench files download /reports/summary.pdf ./summary.pdf --json",
|
|
94
|
+
"dench files download /reports/summary.pdf --url --json",
|
|
95
|
+
],
|
|
96
|
+
notes: [
|
|
97
|
+
"Without --url, downloads directories recursively and defaults the local destination to the path's basename (/ becomes workspace).",
|
|
98
|
+
"--url prints a short-lived signed URL for one stored file instead of saving locally; local-dest is unused in this mode.",
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export const FILE_COMMAND_NOTES = [
|
|
104
|
+
"Workspace paths are POSIX paths under /workspace; /reports/a.md and /workspace/reports/a.md address the same file.",
|
|
105
|
+
"These commands read or update workspace storage. Use dench stage to upload local files or directories.",
|
|
106
|
+
];
|