@dench.com/cli 3.6.5-staging.384 → 3.7.0-staging.387

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/crm.ts CHANGED
@@ -239,6 +239,7 @@ function normalizeRpcError(error: unknown): Error {
239
239
  userFacingConvexErrorMessage(error) ??
240
240
  (error instanceof Error ? error.message : String(error)),
241
241
  ),
242
+ { cause: error },
242
243
  );
243
244
  }
244
245
 
@@ -246,7 +247,8 @@ export function shift(args: string[], expected: string): string {
246
247
  try {
247
248
  return shiftRaw(args, expected);
248
249
  } catch (error) {
249
- if (error instanceof CliArgError) throw new CrmCliError(error.message);
250
+ if (error instanceof CliArgError)
251
+ throw new CrmCliError(error.message, { cause: error });
250
252
  throw error;
251
253
  }
252
254
  }
@@ -290,7 +292,8 @@ function resolveLimitFlag(
290
292
  try {
291
293
  parsed = parseLimitRaw(raw, opts);
292
294
  } catch (error) {
293
- if (error instanceof CliArgError) throw new CrmCliError(error.message);
295
+ if (error instanceof CliArgError)
296
+ throw new CrmCliError(error.message, { cause: error });
294
297
  throw error;
295
298
  }
296
299
  if (parsed.clamped) {
@@ -365,7 +368,8 @@ function parseJson(value: string | undefined): unknown {
365
368
  try {
366
369
  return parseJsonRaw(value);
367
370
  } catch (error) {
368
- if (error instanceof CliArgError) throw new CrmCliError(error.message);
371
+ if (error instanceof CliArgError)
372
+ throw new CrmCliError(error.message, { cause: error });
369
373
  throw error;
370
374
  }
371
375
  }
@@ -377,7 +381,8 @@ export function assertNoUnknownFlags(
377
381
  try {
378
382
  assertNoUnknownFlagsRaw(args, command);
379
383
  } catch (error) {
380
- if (error instanceof CliArgError) throw new CrmCliError(error.message);
384
+ if (error instanceof CliArgError)
385
+ throw new CrmCliError(error.message, { cause: error });
381
386
  throw error;
382
387
  }
383
388
  }
@@ -780,11 +785,23 @@ export async function callQuery(
780
785
  }
781
786
  }
782
787
 
788
+ /** Every parsed token must be accounted for before an external effect. */
789
+ function assertWriteArguments(ctx: CrmCliContext): void {
790
+ assertNoUnknownFlags(ctx.args, "crm");
791
+ if (ctx.args.length) {
792
+ const error = new CliArgError(
793
+ `Unexpected argument: ${ctx.args[0]}. Run the command with --help for usage.`,
794
+ );
795
+ throw new CrmCliError(error.message, { cause: error });
796
+ }
797
+ }
798
+
783
799
  export async function callMutation(
784
800
  ctx: CrmCliContext,
785
801
  fn: Parameters<ConvexHttpClient["mutation"]>[0],
786
802
  args: Record<string, unknown> = {},
787
803
  ): Promise<unknown> {
804
+ assertWriteArguments(ctx);
788
805
  try {
789
806
  return await ctx.convex.mutation(fn, {
790
807
  ...args,
@@ -800,6 +817,7 @@ export async function callAction(
800
817
  fn: Parameters<ConvexHttpClient["action"]>[0],
801
818
  args: Record<string, unknown> = {},
802
819
  ): Promise<unknown> {
820
+ assertWriteArguments(ctx);
803
821
  try {
804
822
  return await ctx.convex.action(fn, {
805
823
  ...args,
@@ -914,6 +932,9 @@ const api = {
914
932
  },
915
933
  objects: {
916
934
  list: makeFunctionReference<"query">("functions/crm/objects:list"),
935
+ listCompact: makeFunctionReference<"query">(
936
+ "functions/crm/objects:listCompact",
937
+ ),
917
938
  get: makeFunctionReference<"query">("functions/crm/objects:get"),
918
939
  materializeView: makeFunctionReference<"action">(
919
940
  "functions/crm/objects:completeView",
@@ -963,6 +984,9 @@ const api = {
963
984
  },
964
985
  fields: {
965
986
  list: makeFunctionReference<"query">("functions/crm/fields:list"),
987
+ listCompact: makeFunctionReference<"query">(
988
+ "functions/crm/fields:listCompact",
989
+ ),
966
990
  create: makeFunctionReference<"mutation">("functions/crm/fields:create"),
967
991
  update: makeFunctionReference<"mutation">("functions/crm/fields:update"),
968
992
  remove: makeFunctionReference<"mutation">("functions/crm/fields:remove"),
@@ -1013,6 +1037,9 @@ const api = {
1013
1037
  ),
1014
1038
  aggregate: makeFunctionReference<"query">("functions/crm/query:aggregate"),
1015
1039
  search: makeFunctionReference<"query">("functions/crm/query:search"),
1040
+ searchPage: makeFunctionReference<"query">(
1041
+ "functions/crm/query:searchPage",
1042
+ ),
1016
1043
  },
1017
1044
  statuses: {
1018
1045
  list: makeFunctionReference<"query">("functions/crm/statuses:list"),
@@ -1063,6 +1090,15 @@ const api = {
1063
1090
  },
1064
1091
  };
1065
1092
 
1093
+ /** These read commands have local, command-specific help (no runtime needed). */
1094
+ export function isCrmReadHelpRequest(args: readonly string[]): boolean {
1095
+ const command = args.find((arg) => arg !== "--json");
1096
+ return (
1097
+ (command === "query" || command === "aggregate") &&
1098
+ (args.includes("--help") || args.includes("-h"))
1099
+ );
1100
+ }
1101
+
1066
1102
  export async function runCrmCommand(opts: {
1067
1103
  convex: ConvexHttpClient;
1068
1104
  args: string[];
@@ -1088,6 +1124,21 @@ export async function runCrmCommand(opts: {
1088
1124
  crmHelp();
1089
1125
  return;
1090
1126
  }
1127
+ if (isCrmReadHelpRequest(opts.args)) {
1128
+ if (subcommand === "query") {
1129
+ console.log(`Usage: dench crm query <object> [--where '<JSON>'] [--select Name,Email] [--sort -Score] [--limit N] [--json]
1130
+ --page [--cursor '<continueCursor>'] Return {page,isDone,continueCursor} in index order; omit --sort.
1131
+ --assigned-to me|unassigned|<userId|email> (or --mine) [--assignee-field <Field>]
1132
+ Default limit: 100. Maximum: 1000 for a one-shot array, 100 for a cursor page.
1133
+ Continue cursor pages until isDone, including empty filtered pages.
1134
+ Example: dench crm query company --where '{"Status":"New"}' --select Name --limit 25 --json`);
1135
+ } else {
1136
+ console.log(`Usage: dench crm aggregate <object> --field <Field> [--sum <NumberField>] [--json]
1137
+ Count rows per value of --field. --sum also totals a number field within each bucket.
1138
+ Example: dench crm aggregate deal --field Stage --sum Amount --json`);
1139
+ }
1140
+ return;
1141
+ }
1091
1142
  switch (subcommand) {
1092
1143
  case "objects":
1093
1144
  return await runObjectsCommand(ctx);
@@ -1435,8 +1486,16 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
1435
1486
  const verb = ctx.args.shift();
1436
1487
  switch (verb) {
1437
1488
  case "list": {
1489
+ const compact = hasFlag(ctx.args, "--compact");
1438
1490
  assertNoUnknownFlags(ctx.args, "crm objects list");
1439
- out(ctx, await callQuery(ctx, api.objects.list, {}));
1491
+ out(
1492
+ ctx,
1493
+ await callQuery(
1494
+ ctx,
1495
+ compact ? api.objects.listCompact : api.objects.list,
1496
+ {},
1497
+ ),
1498
+ );
1440
1499
  return;
1441
1500
  }
1442
1501
  case "get": {
@@ -2135,8 +2194,16 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
2135
2194
  switch (verb) {
2136
2195
  case "list": {
2137
2196
  const objectName = shift(ctx.args, "object name");
2197
+ const compact = hasFlag(ctx.args, "--compact");
2138
2198
  assertNoUnknownFlags(ctx.args, "crm fields list");
2139
- out(ctx, await callQuery(ctx, api.fields.list, { objectName }));
2199
+ out(
2200
+ ctx,
2201
+ await callQuery(
2202
+ ctx,
2203
+ compact ? api.fields.listCompact : api.fields.list,
2204
+ { objectName },
2205
+ ),
2206
+ );
2140
2207
  return;
2141
2208
  }
2142
2209
  case "create": {
@@ -2270,7 +2337,7 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
2270
2337
  // Assert before consuming the rest as positional field names so a
2271
2338
  // typo'd flag fails loudly rather than getting silently filtered.
2272
2339
  assertNoUnknownFlags(ctx.args, "crm fields reorder");
2273
- const orderedFieldNames = [...ctx.args];
2340
+ const orderedFieldNames = ctx.args.splice(0);
2274
2341
  out(
2275
2342
  ctx,
2276
2343
  await callMutation(ctx, api.fields.reorder, {
@@ -2287,7 +2354,7 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
2287
2354
  // there are no stray flags so a typo'd `--limit` fails loud
2288
2355
  // instead of being silently consumed as an enum value.
2289
2356
  assertNoUnknownFlags(ctx.args, "crm fields enum-reorder");
2290
- const orderedValues = [...ctx.args];
2357
+ const orderedValues = ctx.args.splice(0);
2291
2358
  if (orderedValues.length === 0) {
2292
2359
  throw new CrmCliError(
2293
2360
  "dench crm fields enum-reorder requires at least one value. " +
@@ -2391,37 +2458,100 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2391
2458
  switch (verb) {
2392
2459
  case "list": {
2393
2460
  const objectName = shift(ctx.args, "object name");
2461
+ const withMeta = hasFlag(ctx.args, "--with-meta");
2462
+ const cursorSpecified = ctx.args.includes("--cursor");
2463
+ const cursorValue = ctx.args[ctx.args.indexOf("--cursor") + 1];
2464
+ if (
2465
+ cursorSpecified &&
2466
+ (!cursorValue?.trim() || cursorValue.startsWith("--"))
2467
+ )
2468
+ throw new CrmCliError(
2469
+ "--cursor requires a non-empty continuation cursor.",
2470
+ { cause: new CliArgError("Invalid --cursor") },
2471
+ );
2472
+ const cursor = getFlag(ctx.args, "--cursor");
2473
+ if (cursorSpecified && (!cursor?.trim() || cursor.startsWith("--")))
2474
+ throw new CrmCliError(
2475
+ "--cursor requires a non-empty continuation cursor.",
2476
+ );
2477
+ const assigneeScopeArgs = resolveAssigneeScopeFlags(ctx.args);
2394
2478
  const limit = resolveLimitFlag(ctx.args, {
2395
2479
  defaultValue: 100,
2396
- cap: 1000,
2480
+ cap:
2481
+ withMeta && assigneeScopeArgs.assignedTo !== undefined ? 100 : 1000,
2397
2482
  command: "crm entries list",
2483
+ ...(withMeta
2484
+ ? {
2485
+ fullDatasetHint:
2486
+ "Continue with --with-meta --cursor '<continueCursor>' when a cursor is returned.",
2487
+ }
2488
+ : {}),
2398
2489
  });
2399
- const withMeta = hasFlag(ctx.args, "--with-meta");
2400
- const assigneeScopeArgs = resolveAssigneeScopeFlags(ctx.args);
2490
+ if (cursor !== undefined && !withMeta) {
2491
+ throw new CrmCliError(
2492
+ "--cursor requires --with-meta on crm entries list.",
2493
+ );
2494
+ }
2401
2495
  assertNoUnknownFlags(ctx.args, "crm entries list");
2402
- const entries = await callQuery(ctx, api.entries.list, {
2403
- objectName,
2404
- limit: limit.effective,
2405
- ...assigneeScopeArgs,
2406
- });
2407
2496
  if (!withMeta) {
2408
2497
  // Bare-array output: the clamp warning already went to stderr
2409
2498
  // (stdout must stay a valid JSON array in --json mode).
2410
- out(ctx, entries);
2499
+ out(
2500
+ ctx,
2501
+ await callQuery(ctx, api.entries.list, {
2502
+ objectName,
2503
+ limit: limit.effective,
2504
+ ...assigneeScopeArgs,
2505
+ }),
2506
+ );
2411
2507
  return;
2412
2508
  }
2413
- const object = (await callQuery(ctx, api.objects.get, {
2414
- name: objectName,
2415
- })) as { entryCount?: number } | null;
2416
- const returnedCount = Array.isArray(entries) ? entries.length : 0;
2417
- const totalCount =
2418
- typeof object?.entryCount === "number"
2419
- ? object.entryCount
2420
- : returnedCount;
2509
+ // Catalog counts include hidden and unassigned records. Only the
2510
+ // readable page can tell us whether this caller has a continuation.
2511
+ const [result, object] = (await Promise.all([
2512
+ assigneeScopeArgs.assignedTo !== undefined
2513
+ ? callQuery(ctx, api.query.scanEntries, {
2514
+ objectName,
2515
+ dsl: { limit: limit.effective },
2516
+ paginationOpts: {
2517
+ cursor: cursor ?? null,
2518
+ numItems: limit.effective,
2519
+ },
2520
+ ...assigneeScopeArgs,
2521
+ })
2522
+ : callQuery(ctx, api.entries.listPage, {
2523
+ objectName,
2524
+ limit: limit.effective,
2525
+ ...(cursor !== undefined ? { cursor } : {}),
2526
+ }),
2527
+ callQuery(ctx, api.objects.get, { name: objectName }),
2528
+ ])) as [
2529
+ {
2530
+ page?: unknown[];
2531
+ isDone?: boolean;
2532
+ continueCursor?: string | null;
2533
+ truncated?: boolean;
2534
+ } | null,
2535
+ { entryCount?: number; visibility?: string } | null,
2536
+ ];
2537
+ const entries = Array.isArray(result?.page) ? result.page : [];
2538
+ const isDone = typeof result?.isDone === "boolean" ? result.isDone : null;
2539
+ const truncated = result?.truncated === true;
2540
+ const continueCursor =
2541
+ isDone === false ? result?.continueCursor || null : null;
2542
+ const hasMore = continueCursor
2543
+ ? true
2544
+ : isDone === true && !truncated
2545
+ ? false
2546
+ : null;
2421
2547
  out(ctx, {
2422
2548
  objectName,
2423
- totalCount,
2424
- returnedCount,
2549
+ totalCount:
2550
+ typeof object?.entryCount === "number" ? object.entryCount : null,
2551
+ totalCountScope: "object",
2552
+ visibility: object?.visibility ?? null,
2553
+ returnedCount: entries.length,
2554
+ returnedCountScope: "readable",
2425
2555
  limit: limit.effective,
2426
2556
  ...(limit.clamped
2427
2557
  ? {
@@ -2431,7 +2561,10 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
2431
2561
  },
2432
2562
  }
2433
2563
  : {}),
2434
- hasMore: totalCount > returnedCount,
2564
+ hasMore,
2565
+ isDone,
2566
+ truncated,
2567
+ continueCursor,
2435
2568
  entries,
2436
2569
  });
2437
2570
  return;
@@ -3152,25 +3285,31 @@ async function runAggregateCommand(ctx: CrmCliContext): Promise<void> {
3152
3285
 
3153
3286
  async function runSearchCommand(ctx: CrmCliContext): Promise<void> {
3154
3287
  const objectName = getFlag(ctx.args, "--object");
3155
- // Search caps at 100 server-side. Warn on stderr when clamped (the
3156
- // output is a bare JSON array, so stdout must stay untouched).
3288
+ const selectRaw = getFlag(ctx.args, "--select");
3289
+ const select = selectRaw?.split(",").map((name) => name.trim());
3290
+ if (select && (select.length > 30 || select.some((name) => !name))) {
3291
+ throw new CrmCliError(
3292
+ "--select requires between 1 and 30 nonempty field names.",
3293
+ );
3294
+ }
3295
+ const page = hasFlag(ctx.args, "--page");
3296
+ const cursor = getFlag(ctx.args, "--cursor");
3297
+ const projected = select !== undefined || page || cursor !== undefined;
3157
3298
  const limit = resolveLimitFlag(ctx.args, {
3158
- defaultValue: 50,
3299
+ defaultValue: projected ? 20 : 50,
3159
3300
  cap: 100,
3160
3301
  command: "crm search",
3161
3302
  });
3162
- // Drain known flags before joining the rest into the search text so a
3163
- // typo like `--objct` surfaces instead of being concatenated into the
3164
- // query string.
3165
3303
  assertNoUnknownFlags(ctx.args, "crm search");
3166
3304
  const text = ctx.args.join(" ").trim();
3167
3305
  if (!text) throw new CrmCliError("Search text required");
3168
3306
  out(
3169
3307
  ctx,
3170
- await callQuery(ctx, api.query.search, {
3308
+ await callQuery(ctx, projected ? api.query.searchPage : api.query.search, {
3171
3309
  text,
3172
3310
  objectName,
3173
3311
  limit: limit.effective,
3312
+ ...(projected ? { select, cursor } : {}),
3174
3313
  }),
3175
3314
  );
3176
3315
  }
@@ -3300,7 +3439,8 @@ async function runActionsCommand(ctx: CrmCliContext): Promise<void> {
3300
3439
  const objectName = shift(ctx.args, "object name");
3301
3440
  const fieldId = shift(ctx.args, "field id");
3302
3441
  const actionId = shift(ctx.args, "action id");
3303
- const entryIds = ctx.args.filter((a) => !a.startsWith("--"));
3442
+ assertNoUnknownFlags(ctx.args, "crm actions run");
3443
+ const entryIds = ctx.args.splice(0);
3304
3444
  if (entryIds.length === 0) {
3305
3445
  throw new CrmCliError("Provide at least one entry id");
3306
3446
  }
@@ -3840,7 +3980,7 @@ export function remapKeys(
3840
3980
  // ── SQL escape hatch ─────────────────────────────────────────────────────
3841
3981
 
3842
3982
  async function runSqlCommand(ctx: CrmCliContext): Promise<void> {
3843
- const sql = ctx.args.join(" ").trim();
3983
+ const sql = ctx.args.splice(0).join(" ").trim();
3844
3984
  if (!sql) {
3845
3985
  throw new CrmCliError("dench crm sql '<SELECT ... FROM ...>'");
3846
3986
  }
@@ -4329,6 +4469,7 @@ async function enrichCellThroughGateway(
4329
4469
  ctx: CrmCliContext,
4330
4470
  args: CellEnrichmentArgs,
4331
4471
  ): Promise<Record<string, unknown>> {
4472
+ assertWriteArguments(ctx);
4332
4473
  const target = await resolveEnrichmentTarget(ctx, args);
4333
4474
  const entry = asEntry(
4334
4475
  await callQuery(ctx, api.entries.get, {
@@ -4352,6 +4493,7 @@ async function enrichObjectThroughGateway(
4352
4493
  ctx: CrmCliContext,
4353
4494
  args: ObjectEnrichmentArgs,
4354
4495
  ): Promise<Record<string, unknown>> {
4496
+ assertWriteArguments(ctx);
4355
4497
  const target = await resolveEnrichmentTarget(ctx, args);
4356
4498
  const entries = asEntries(
4357
4499
  await callQuery(ctx, api.entries.list, {
@@ -4533,6 +4675,7 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
4533
4675
  const lookups = input.map((value, index) =>
4534
4676
  requireNonEmptyJsonRecord(value, `lookup ${index + 1}`),
4535
4677
  );
4678
+ assertWriteArguments(ctx);
4536
4679
  out(ctx, await enrichProfiles({ kind, lookups }, ctx.enrichmentGateway));
4537
4680
  return;
4538
4681
  }
@@ -4638,10 +4781,14 @@ async function runReportsCommand(ctx: CrmCliContext): Promise<void> {
4638
4781
  }
4639
4782
 
4640
4783
  function crmHelp(): void {
4641
- console.log(`Usage: dench crm <subcommand>
4784
+ console.log(crmHelpText());
4785
+ }
4786
+
4787
+ export function crmHelpText(): string {
4788
+ return `Usage: dench crm <subcommand>
4642
4789
 
4643
4790
  Objects (CRM tables):
4644
- dench crm objects list [--json]
4791
+ dench crm objects list [--json] [--compact]
4645
4792
  dench crm objects get <name>
4646
4793
  dench crm objects create <name> [--description ...] [--default-view kanban] [--icon ...]
4647
4794
  dench crm objects update <name> [--default-view ...] [--icon ...] [--display-name "Clients"] [--display-name-singular "Client"] [--visibility workspace|owner_only|owner_plus_members]
@@ -4728,7 +4875,7 @@ Sidebar (progressive disclosure — Library + home Pinned strip; layout is WORKS
4728
4875
  shared shortcut, not a Library move; open to any member.
4729
4876
 
4730
4877
  Fields (columns):
4731
- dench crm fields list <object> [--json]
4878
+ dench crm fields list <object> [--compact] [--json]
4732
4879
  dench crm fields create <object> <name> --type text|email|phone|url|number|boolean|date|enum|relation|user|tags|action|file|richtext \\
4733
4880
  [--required] [--description ...] [--enum-values JSON] [--enum-colors JSON] \\
4734
4881
  [--enum-multiple] [--related-object <name>] [--relationship many_to_one|many_to_many] \\
@@ -4785,8 +4932,12 @@ Fields (columns):
4785
4932
  Update the color (e.g. "#3b82f6") for an existing enum option.
4786
4933
 
4787
4934
  Entries (rows):
4788
- dench crm entries list <object> [--limit N] [--with-meta] [--json]
4935
+ dench crm entries list <object> [--limit N] [--with-meta [--cursor '<continueCursor>']] [--json]
4789
4936
  [--assigned-to me|unassigned|<userId|email> | --mine] [--assignee-field <Field>]
4937
+ --with-meta includes readable pagination; totalCount is the entire object's catalog count,
4938
+ not a readable or assigned total. Continue only when continueCursor is returned, preserving filters.
4939
+ Scoped reads may be truncated with no cursor (hasMore: null); repeating the same call will not advance.
4940
+ Assigned metadata pages cap at 100 rows; other list reads cap at 1000.
4790
4941
  dench crm entries get <object> <entryId> [--with-context] (--with-context appends the newest activity + notes)
4791
4942
  dench crm entries sync-history <object> <entryId> [--status]
4792
4943
  Queue a full, paged pull of the record's email history from every
@@ -4945,7 +5096,10 @@ Aggregates:
4945
5096
  dench crm aggregate <object> --field Status [--sum Amount]
4946
5097
 
4947
5098
  Full-text search:
4948
- dench crm search [--object <name>] [--limit N] <text>
5099
+ dench crm search [--object <name>] [--select "Name,Email"] [--limit N] [--page] [--cursor <cursor>] <text>
5100
+ --select returns a compact page with stable IDs, selected cells, isDone
5101
+ and continueCursor. Follow cursors until isDone for complete results.
5102
+ Without --select/--page/--cursor the legacy full-row array is unchanged.
4949
5103
 
4950
5104
  Statuses (kanban columns):
4951
5105
  dench crm statuses list <object>
@@ -5129,5 +5283,5 @@ unambiguous, but prefer canonical names from 'crm objects list'.
5129
5283
 
5130
5284
  Global flags:
5131
5285
  --json Output raw JSON instead of pretty-printed text.
5132
- `);
5286
+ `;
5133
5287
  }