@dench.com/cli 3.8.0-staging.389 → 3.8.2-staging.391

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.
@@ -8,6 +8,8 @@ export type TimelineRun = {
8
8
  startedAt?: number;
9
9
  waitUntil?: number;
10
10
  retryAt?: number;
11
+ /** A wait for acceptance: when it stops waiting either way. */
12
+ timeoutAt?: number;
11
13
  completedAt?: number;
12
14
  emailMessageId?: string;
13
15
  lastError?: string;
@@ -49,6 +51,8 @@ export type TimelineResult = {
49
51
  type: string;
50
52
  order: number;
51
53
  waitHours?: number;
54
+ /** If / else and Wait until they accept steps: what they check. */
55
+ label?: string;
52
56
  }>;
53
57
  runs: TimelineRun[];
54
58
  emailMessages: TimelineEmail[];
@@ -63,6 +67,10 @@ export type TimelineStep = {
63
67
  outcome: string;
64
68
  at: number | null;
65
69
  detail: string[];
70
+ /** An If / else or a wait for acceptance: the path it took. */
71
+ branch?: "yes" | "no";
72
+ /** Still waiting for them to accept: when it stops waiting. */
73
+ acceptanceDeadline?: number;
66
74
  subject?: string;
67
75
  text?: string;
68
76
  error?: string;
@@ -101,16 +109,66 @@ const EMAIL_STATUS: Record<string, string> = {
101
109
  failed: "could not send",
102
110
  };
103
111
 
112
+ function resultField(result: unknown, key: string): unknown {
113
+ return result && typeof result === "object"
114
+ ? (result as Record<string, unknown>)[key]
115
+ : undefined;
116
+ }
117
+
104
118
  function skipReason(result: unknown): string {
105
- const reason =
106
- result && typeof result === "object"
107
- ? (result as { reason?: unknown }).reason
108
- : undefined;
119
+ const reason = resultField(result, "reason");
109
120
  return (typeof reason === "string" && SKIP_REASONS[reason]) || "Skipped";
110
121
  }
111
122
 
123
+ /** Why an If / else or a wait for acceptance took its No path. */
124
+ const NO_REASONS: Record<string, string> = {
125
+ missing_linkedin_identity: "no LinkedIn profile",
126
+ missing_email_identity: "no email address",
127
+ linkedin_connection_timeout: "they did not accept in time",
128
+ linkedin_invite_withdrawn: "the connection request was withdrawn",
129
+ linkedin_invite_not_sent: "no connection request went out",
130
+ };
131
+
132
+ const LOGIC_TYPES = new Set(["condition", "wait_for_connection"]);
133
+
134
+ export function branchOf(run: TimelineRun): "yes" | "no" | undefined {
135
+ if (!LOGIC_TYPES.has(run.type) || run.status !== "sent") return undefined;
136
+ const branch = resultField(run.result, "branch");
137
+ return branch === "yes" || branch === "no" ? branch : undefined;
138
+ }
139
+
140
+ /** What the editor calls each exit: Yes / No, or Accepted / Not accepted. */
141
+ export function branchExitLabel(type: string, branch: "yes" | "no"): string {
142
+ if (type === "wait_for_connection")
143
+ return branch === "yes" ? "Accepted" : "Not accepted";
144
+ return branch === "yes" ? "Yes" : "No";
145
+ }
146
+
147
+ function logicOutcome(run: TimelineRun): string | undefined {
148
+ if (!LOGIC_TYPES.has(run.type)) return undefined;
149
+ const branch = branchOf(run);
150
+ if (branch) {
151
+ const reason = resultField(run.result, "reason");
152
+ const why =
153
+ branch === "no" && typeof reason === "string" ? NO_REASONS[reason] : "";
154
+ return `${branchExitLabel(run.type, branch)}${why ? ` (${why})` : ""}`;
155
+ }
156
+ if (run.status === "sent") return "Done";
157
+ if (run.type === "wait_for_connection")
158
+ return run.status === "waiting"
159
+ ? "Waiting for them to accept"
160
+ : run.status === "queued" || run.status === "running"
161
+ ? "Checking whether they accepted"
162
+ : undefined;
163
+ return ["queued", "running", "waiting"].includes(run.status)
164
+ ? "Checking"
165
+ : undefined;
166
+ }
167
+
112
168
  function outcomeOf(run: TimelineRun): string {
113
169
  if (run.status === "skipped") return skipReason(run.result);
170
+ const logic = logicOutcome(run);
171
+ if (logic) return logic;
114
172
  if (run.status === "sent") {
115
173
  if (run.type === "wait") return "Waited";
116
174
  if (run.type === "linkedin_visit") return "Visited";
@@ -153,6 +211,11 @@ export function timelineSteps(result: TimelineResult): TimelineStep[] {
153
211
  run.status === "sent" || run.status === "skipped"
154
212
  ? undefined
155
213
  : (run.lastError ?? email?.lastError)?.trim();
214
+ const branch = branchOf(run);
215
+ const waitingForAccept =
216
+ run.type === "wait_for_connection" &&
217
+ run.status === "waiting" &&
218
+ typeof run.timeoutAt === "number";
156
219
  return {
157
220
  step: (step?.order ?? run.stepIndex) + 1,
158
221
  stepId: String(run.stepId),
@@ -167,6 +230,8 @@ export function timelineSteps(result: TimelineResult): TimelineStep[] {
167
230
  run.scheduledAt ??
168
231
  null,
169
232
  detail,
233
+ ...(branch ? { branch } : {}),
234
+ ...(waitingForAccept ? { acceptanceDeadline: run.timeoutAt } : {}),
170
235
  ...(email?.subject ? { subject: email.subject } : {}),
171
236
  ...(text ? { text } : {}),
172
237
  ...(problem ? { error: problem } : {}),
@@ -1,4 +1,5 @@
1
1
  import { CliArgError, missingFlagValue } from "./cli-args";
2
+ import { FILE_COMMAND_ARGUMENTS } from "./file-command-contract";
2
3
 
3
4
  type PositionalArity = readonly [min: number, max: number | null];
4
5
 
@@ -8,6 +9,10 @@ type CommandArguments = {
8
9
  switches: string[];
9
10
  positionals: PositionalArity;
10
11
  flagPositionals: Record<string, PositionalArity>;
12
+ usage?: string;
13
+ description?: string;
14
+ examples?: string[];
15
+ notes?: string[];
11
16
  };
12
17
  const spec = (
13
18
  command: string,
@@ -138,9 +143,7 @@ export const COMMAND_ARGUMENTS = [
138
143
  ...["delete", "enable", "disable"].map((verb) =>
139
144
  spec(`cron ${verb}`, [1, 1]),
140
145
  ),
141
- spec("files rm", [1, 1], "", "--recursive -r"),
142
- spec("files mv", [2, 2]),
143
- spec("files write", [1, 1]),
146
+ ...FILE_COMMAND_ARGUMENTS,
144
147
  spec("stage", [1, 2], "", "--clean"),
145
148
  ...["identity", "organisation", "user", "tools", "mem"].flatMap((group) =>
146
149
  ["edit", "set"].map((verb) =>
@@ -200,16 +203,30 @@ export const COMMAND_ARGUMENTS = [
200
203
  browserSpec("stop", [0, 0]),
201
204
  ];
202
205
 
203
- export function assertDeclaredCommandFlags(argv: readonly string[]): void {
204
- const boundary = argv.indexOf("--");
205
- const args = argv
206
- .slice(0, boundary < 0 ? undefined : boundary)
207
- .filter((arg) => arg !== "--json");
206
+ /** Keep compatibility aliases identical in validation and local discovery. */
207
+ export function normalizeCommandAlias(argv: readonly string[]): string[] {
208
+ const args = [...argv];
208
209
  if (["routine", "routines"].includes(args[0])) args[0] = "cron";
209
210
  if (["ad", "adline"].includes(args[0])) args[0] = "ads";
210
- if (args[0] === "agent" && ["new", "send"].includes(args[1]))
211
+ if (args[0] === "agent" && ["new", "send", "follow"].includes(args[1]))
211
212
  args[0] = "chat";
212
213
  if (args[0] === "crm" && args[1] === "members") args.shift();
214
+ if (args[0] === "campaigns") args[0] = "campaign";
215
+ return args;
216
+ }
217
+
218
+ export function assertDeclaredCommandFlags(argv: readonly string[]): void {
219
+ const boundary = argv.indexOf("--");
220
+ const args = normalizeCommandAlias(
221
+ argv
222
+ .slice(0, boundary < 0 ? undefined : boundary)
223
+ .filter((arg) => arg !== "--json"),
224
+ );
225
+ if (args[0] === "files" && boundary >= 0) {
226
+ throw new CliArgError(
227
+ "dench files does not accept a child-command separator (--). Run `dench files --help` for usage.",
228
+ );
229
+ }
213
230
  const contract = COMMAND_ARGUMENTS.find(({ command }) =>
214
231
  command.split(" ").every((part, index) => args[index] === part),
215
232
  );
@@ -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; never interpret a data value as a command. */
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) => /^[<[(-]/.test(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 input = words(args.filter((arg) => arg !== "help").join(" "));
28
- const usageLines = usageText
29
- .split("\n")
30
- .map((line) => line.trim())
31
- .filter((line) => line.startsWith("dench "));
32
- const candidates = [
33
- ...usageLines.map((usage) => ({ usage, description: "" })),
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
- `dench ${entry.command} ${[...entry.values.map((flag) => `[${flag} <value>]`), ...entry.switches.map((flag) => `[${flag}]`)].join(" ")}`.trim(),
37
- description: "",
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
- !candidates.some((item) => matches(prefix, words(item.usage)))
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
- // Domain usage has real flag spellings; a backend schema is not a CLI parser.
69
- const exactUsage = selected.filter((item) => !item.description);
70
- const entries = exactUsage.length ? exactUsage : selected;
71
- const seen = new Set<string>();
72
- const lines = entries.filter((item) => {
73
- if (seen.has(item.usage)) return false;
74
- seen.add(item.usage);
75
- return true;
76
- });
77
- const heading = `Usage: dench ${prefix.join(" ")}${prefix.length ? "" : "<command>"}`;
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
- heading,
80
- ...lines
81
- .slice(0, 12)
82
- .map(
83
- (item) =>
84
- ` ${item.usage}${item.description ? `\n ${item.description}` : ""}`,
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
- ` ${lines.length - 12} more commands; add a subcommand before --help.`,
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
  }
@@ -4183,7 +4183,7 @@ const rawApiOperations = [
4183
4183
  id: "campaigns.analytics",
4184
4184
  group: "campaigns",
4185
4185
  summary:
4186
- "Campaign funnel: enrolled, contacted, invited, accepted, messaged, replied, opens and clicks.",
4186
+ "Campaign funnel: enrolled, contacted, invited, accepted, messaged, replied, opens and clicks, and for a workflow how many people took each If / else and Wait for them to accept path (branchStats).",
4187
4187
  cli: "dench campaign get",
4188
4188
  method: "GET",
4189
4189
  path: "/campaigns/{sequenceId}/analytics",
@@ -4237,7 +4237,7 @@ const rawApiOperations = [
4237
4237
  id: "campaigns.steps.replace",
4238
4238
  group: "campaigns",
4239
4239
  summary:
4240
- "Replace every step of a draft campaign (email, wait, LinkedIn visit, connection request, message, post reaction, InMail). Steps are checked first: a field the step type would drop, missing text or {TOKEN} single braces are rejected, not saved. Configure sender pools on the campaign; step sender ids are legacy-only.",
4240
+ "Replace every step of a draft campaign (email, wait, LinkedIn visit, connection request, message, post reaction, InMail, If / else, Wait for them to accept). A workflow (stepModel workflow, or steps with key, next, yesNext or noNext) branches: condition and wait_for_connection steps continue on yesNext or noNext, other steps on next; each points at another step's key or end. Steps are checked first: a field the step type would drop, missing text, {TOKEN} single braces or paths that don't hold together are rejected, not saved. Configure sender pools on the campaign; step sender ids are legacy-only.",
4241
4241
  cli: "dench campaign steps set",
4242
4242
  method: "PUT",
4243
4243
  path: "/campaigns/{sequenceId}/steps",
@@ -4249,7 +4249,7 @@ const rawApiOperations = [
4249
4249
  id: "campaigns.steps.update",
4250
4250
  group: "campaigns",
4251
4251
  summary:
4252
- "Edit the text, waits and conditions of a launched (or paused) campaign's steps in place. Each step keeps its id, type and place; pass the stepId and the updatedAt you read, and only the fields you change. Messages already sent or being prepared keep their text; drafts use campaigns.steps.replace.",
4252
+ "Edit the text, waits and conditions of a launched (or paused) campaign's steps in place. Each step keeps its id, type and place; pass the stepId and the updatedAt you read, and only the fields you change. A workflow keeps its paths and what each If / else checks (key, next, yesNext, noNext, check, conditionField, conditionOperator); conditionValue and acceptance waits stay editable. Messages already sent or being prepared keep their text; drafts use campaigns.steps.replace.",
4253
4253
  cli: "dench campaign steps edit",
4254
4254
  method: "PATCH",
4255
4255
  path: "/campaigns/{sequenceId}/steps",
@@ -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
+ ];