@dench.com/cli 0.3.6 → 0.3.8

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
@@ -29,8 +29,10 @@
29
29
  import { ConvexHttpClient } from "convex/browser";
30
30
  import { makeFunctionReference } from "convex/server";
31
31
  import {
32
+ assertNoUnknownFlags as assertNoUnknownFlagsRaw,
32
33
  CliArgError,
33
34
  getFlag,
35
+ getFlagWithAliases,
34
36
  hasFlag,
35
37
  parseJson as parseJsonRaw,
36
38
  shift as shiftRaw,
@@ -77,6 +79,24 @@ function parseJson(value: string | undefined): unknown {
77
79
  }
78
80
  }
79
81
 
82
+ function assertNoUnknownFlags(args: readonly string[], command: string): void {
83
+ try {
84
+ assertNoUnknownFlagsRaw(args, command);
85
+ } catch (error) {
86
+ if (error instanceof CliArgError) throw new CrmCliError(error.message);
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ // Pull `--data <JSON>` from args, accepting `--fields` as an alias because
92
+ // the entries.list output exposes the data envelope under `fields` and the
93
+ // rest of the surface uses `crm fields` to manage column schemas — both
94
+ // priors push callers toward `--fields`. We accept either; everywhere
95
+ // downstream still calls the parameter `data` to match the server-side API.
96
+ function getDataFlag(args: string[]): string | undefined {
97
+ return getFlagWithAliases(args, "--data", ["--fields"]);
98
+ }
99
+
80
100
  async function callQuery(
81
101
  ctx: CrmCliContext,
82
102
  fn: Parameters<ConvexHttpClient["query"]>[0],
@@ -121,26 +141,18 @@ function out(ctx: CrmCliContext, value: unknown): void {
121
141
 
122
142
  const api = {
123
143
  transaction: {
124
- begin: makeFunctionReference<"mutation">(
125
- "functions/crm/transaction:begin",
126
- ),
127
- addOp: makeFunctionReference<"mutation">(
128
- "functions/crm/transaction:addOp",
129
- ),
144
+ begin: makeFunctionReference<"mutation">("functions/crm/transaction:begin"),
145
+ addOp: makeFunctionReference<"mutation">("functions/crm/transaction:addOp"),
130
146
  commit: makeFunctionReference<"mutation">(
131
147
  "functions/crm/transaction:commit",
132
148
  ),
133
- abort: makeFunctionReference<"mutation">(
134
- "functions/crm/transaction:abort",
135
- ),
149
+ abort: makeFunctionReference<"mutation">("functions/crm/transaction:abort"),
136
150
  inspect: makeFunctionReference<"query">(
137
151
  "functions/crm/transaction:inspect",
138
152
  ),
139
153
  },
140
154
  reports: {
141
- generate: makeFunctionReference<"query">(
142
- "functions/crm/reports:generate",
143
- ),
155
+ generate: makeFunctionReference<"query">("functions/crm/reports:generate"),
144
156
  },
145
157
  enrich: {
146
158
  requestCellEnrichment: makeFunctionReference<"mutation">(
@@ -163,22 +175,14 @@ const api = {
163
175
  create: makeFunctionReference<"mutation">("functions/crm/fields:create"),
164
176
  update: makeFunctionReference<"mutation">("functions/crm/fields:update"),
165
177
  remove: makeFunctionReference<"mutation">("functions/crm/fields:remove"),
166
- reorder: makeFunctionReference<"mutation">(
167
- "functions/crm/fields:reorder",
168
- ),
178
+ reorder: makeFunctionReference<"mutation">("functions/crm/fields:reorder"),
169
179
  },
170
180
  entries: {
171
181
  list: makeFunctionReference<"query">("functions/crm/entries:list"),
172
182
  get: makeFunctionReference<"query">("functions/crm/entries:get"),
173
- create: makeFunctionReference<"mutation">(
174
- "functions/crm/entries:create",
175
- ),
176
- update: makeFunctionReference<"mutation">(
177
- "functions/crm/entries:update",
178
- ),
179
- remove: makeFunctionReference<"mutation">(
180
- "functions/crm/entries:remove",
181
- ),
183
+ create: makeFunctionReference<"mutation">("functions/crm/entries:create"),
184
+ update: makeFunctionReference<"mutation">("functions/crm/entries:update"),
185
+ remove: makeFunctionReference<"mutation">("functions/crm/entries:remove"),
182
186
  bulkDelete: makeFunctionReference<"mutation">(
183
187
  "functions/crm/entries:bulkDelete",
184
188
  ),
@@ -186,9 +190,7 @@ const api = {
186
190
  cells: {
187
191
  get: makeFunctionReference<"query">("functions/crm/cells:get"),
188
192
  set: makeFunctionReference<"mutation">("functions/crm/cells:set"),
189
- append: makeFunctionReference<"mutation">(
190
- "functions/crm/cells:append",
191
- ),
193
+ append: makeFunctionReference<"mutation">("functions/crm/cells:append"),
192
194
  },
193
195
  query: {
194
196
  queryEntries: makeFunctionReference<"query">(
@@ -208,9 +210,7 @@ const api = {
208
210
  },
209
211
  documents: {
210
212
  list: makeFunctionReference<"query">("functions/crm/documents:list"),
211
- create: makeFunctionReference<"mutation">(
212
- "functions/crm/documents:create",
213
- ),
213
+ create: makeFunctionReference<"mutation">("functions/crm/documents:create"),
214
214
  link: makeFunctionReference<"mutation">("functions/crm/documents:link"),
215
215
  },
216
216
  actions: {
@@ -288,11 +288,13 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
288
288
  const verb = ctx.args.shift();
289
289
  switch (verb) {
290
290
  case "list": {
291
+ assertNoUnknownFlags(ctx.args, "crm objects list");
291
292
  out(ctx, await callQuery(ctx, api.objects.list, {}));
292
293
  return;
293
294
  }
294
295
  case "get": {
295
296
  const name = shift(ctx.args, "object name");
297
+ assertNoUnknownFlags(ctx.args, "crm objects get");
296
298
  out(ctx, await callQuery(ctx, api.objects.get, { name }));
297
299
  return;
298
300
  }
@@ -301,6 +303,7 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
301
303
  const description = getFlag(ctx.args, "--description");
302
304
  const defaultView = getFlag(ctx.args, "--default-view") as any;
303
305
  const icon = getFlag(ctx.args, "--icon");
306
+ assertNoUnknownFlags(ctx.args, "crm objects create");
304
307
  out(
305
308
  ctx,
306
309
  await callMutation(ctx, api.objects.create, {
@@ -317,6 +320,7 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
317
320
  const description = getFlag(ctx.args, "--description");
318
321
  const defaultView = getFlag(ctx.args, "--default-view") as any;
319
322
  const icon = getFlag(ctx.args, "--icon");
323
+ assertNoUnknownFlags(ctx.args, "crm objects update");
320
324
  out(
321
325
  ctx,
322
326
  await callMutation(ctx, api.objects.update, {
@@ -331,12 +335,14 @@ async function runObjectsCommand(ctx: CrmCliContext): Promise<void> {
331
335
  case "rename": {
332
336
  const from = shift(ctx.args, "current name");
333
337
  const to = shift(ctx.args, "new name");
338
+ assertNoUnknownFlags(ctx.args, "crm objects rename");
334
339
  out(ctx, await callMutation(ctx, api.objects.rename, { from, to }));
335
340
  return;
336
341
  }
337
342
  case "delete":
338
343
  case "remove": {
339
344
  const name = shift(ctx.args, "object name");
345
+ assertNoUnknownFlags(ctx.args, "crm objects delete");
340
346
  out(ctx, await callMutation(ctx, api.objects.remove, { name }));
341
347
  return;
342
348
  }
@@ -350,6 +356,7 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
350
356
  switch (verb) {
351
357
  case "list": {
352
358
  const objectName = shift(ctx.args, "object name");
359
+ assertNoUnknownFlags(ctx.args, "crm fields list");
353
360
  out(ctx, await callQuery(ctx, api.fields.list, { objectName }));
354
361
  return;
355
362
  }
@@ -373,6 +380,7 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
373
380
  : hasFlag(ctx.args, "--no-indexed")
374
381
  ? false
375
382
  : undefined;
383
+ assertNoUnknownFlags(ctx.args, "crm fields create");
376
384
  out(
377
385
  ctx,
378
386
  await callMutation(ctx, api.fields.create, {
@@ -404,6 +412,7 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
404
412
  : hasFlag(ctx.args, "--no-indexed")
405
413
  ? false
406
414
  : undefined;
415
+ assertNoUnknownFlags(ctx.args, "crm fields update");
407
416
  out(
408
417
  ctx,
409
418
  await callMutation(ctx, api.fields.update, {
@@ -421,6 +430,7 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
421
430
  case "remove": {
422
431
  const objectName = shift(ctx.args, "object name");
423
432
  const fieldName = shift(ctx.args, "field name");
433
+ assertNoUnknownFlags(ctx.args, "crm fields delete");
424
434
  out(
425
435
  ctx,
426
436
  await callMutation(ctx, api.fields.remove, {
@@ -432,7 +442,10 @@ async function runFieldsCommand(ctx: CrmCliContext): Promise<void> {
432
442
  }
433
443
  case "reorder": {
434
444
  const objectName = shift(ctx.args, "object name");
435
- const orderedFieldNames = ctx.args.filter((arg) => !arg.startsWith("--"));
445
+ // Assert before consuming the rest as positional field names so a
446
+ // typo'd flag fails loudly rather than getting silently filtered.
447
+ assertNoUnknownFlags(ctx.args, "crm fields reorder");
448
+ const orderedFieldNames = [...ctx.args];
436
449
  out(
437
450
  ctx,
438
451
  await callMutation(ctx, api.fields.reorder, {
@@ -453,15 +466,14 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
453
466
  case "list": {
454
467
  const objectName = shift(ctx.args, "object name");
455
468
  const limit = parseInt(getFlag(ctx.args, "--limit") ?? "100", 10);
456
- out(
457
- ctx,
458
- await callQuery(ctx, api.entries.list, { objectName, limit }),
459
- );
469
+ assertNoUnknownFlags(ctx.args, "crm entries list");
470
+ out(ctx, await callQuery(ctx, api.entries.list, { objectName, limit }));
460
471
  return;
461
472
  }
462
473
  case "get": {
463
474
  const objectName = shift(ctx.args, "object name");
464
475
  const entryId = shift(ctx.args, "entry id");
476
+ assertNoUnknownFlags(ctx.args, "crm entries get");
465
477
  out(
466
478
  ctx,
467
479
  await callQuery(ctx, api.entries.get, {
@@ -473,9 +485,24 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
473
485
  }
474
486
  case "create": {
475
487
  const objectName = shift(ctx.args, "object name");
476
- const data =
477
- (parseJson(getFlag(ctx.args, "--data")) as JsonRecord | undefined) ??
478
- {};
488
+ const dataRaw = getDataFlag(ctx.args);
489
+ assertNoUnknownFlags(ctx.args, "crm entries create");
490
+ // Refuse silent blank-row creation. The agent that triggered this
491
+ // refactor passed `--fields` (which used to be silently dropped),
492
+ // got an empty data object, and only realized after creating 15
493
+ // blank rows. Be explicit: callers must pass --data (or --fields)
494
+ // with at least one key.
495
+ if (dataRaw === undefined) {
496
+ throw new CrmCliError(
497
+ 'dench crm entries create requires --data \'{"Field":"Value",...}\' (alias: --fields)',
498
+ );
499
+ }
500
+ const data = (parseJson(dataRaw) as JsonRecord | undefined) ?? {};
501
+ if (!data || Object.keys(data).length === 0) {
502
+ throw new CrmCliError(
503
+ 'dench crm entries create: --data must include at least one field. Pass --data \'{"Name":"…"}\' or use cells set after creation.',
504
+ );
505
+ }
479
506
  out(
480
507
  ctx,
481
508
  await callMutation(ctx, api.entries.create, { objectName, data }),
@@ -485,9 +512,19 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
485
512
  case "update": {
486
513
  const objectName = shift(ctx.args, "object name");
487
514
  const entryId = shift(ctx.args, "entry id");
488
- const data =
489
- (parseJson(getFlag(ctx.args, "--data")) as JsonRecord | undefined) ??
490
- {};
515
+ const dataRaw = getDataFlag(ctx.args);
516
+ assertNoUnknownFlags(ctx.args, "crm entries update");
517
+ if (dataRaw === undefined) {
518
+ throw new CrmCliError(
519
+ 'dench crm entries update requires --data \'{"Field":"Value",...}\' (alias: --fields)',
520
+ );
521
+ }
522
+ const data = (parseJson(dataRaw) as JsonRecord | undefined) ?? {};
523
+ if (!data || Object.keys(data).length === 0) {
524
+ throw new CrmCliError(
525
+ 'dench crm entries update: --data must include at least one field to change. Pass --data \'{"Field":"…"}\'.',
526
+ );
527
+ }
491
528
  out(
492
529
  ctx,
493
530
  await callMutation(ctx, api.entries.update, {
@@ -502,6 +539,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
502
539
  case "remove": {
503
540
  const _objectName = shift(ctx.args, "object name");
504
541
  const entryId = shift(ctx.args, "entry id");
542
+ assertNoUnknownFlags(ctx.args, "crm entries delete");
505
543
  out(
506
544
  ctx,
507
545
  await callMutation(ctx, api.entries.remove, {
@@ -514,6 +552,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
514
552
  case "bulkDelete": {
515
553
  const _objectName = shift(ctx.args, "object name");
516
554
  const idsCsv = getFlag(ctx.args, "--ids") ?? "";
555
+ assertNoUnknownFlags(ctx.args, "crm entries bulk-delete");
517
556
  const entryIds = idsCsv.split(",").filter(Boolean);
518
557
  out(
519
558
  ctx,
@@ -535,6 +574,7 @@ async function runCellsCommand(ctx: CrmCliContext): Promise<void> {
535
574
  const fieldName = shift(ctx.args, "field name");
536
575
  switch (verb) {
537
576
  case "get": {
577
+ assertNoUnknownFlags(ctx.args, "crm cells get");
538
578
  out(
539
579
  ctx,
540
580
  await callQuery(ctx, api.cells.get, {
@@ -547,6 +587,7 @@ async function runCellsCommand(ctx: CrmCliContext): Promise<void> {
547
587
  }
548
588
  case "set": {
549
589
  const value = parseJson(ctx.args.shift()) ?? null;
590
+ assertNoUnknownFlags(ctx.args, "crm cells set");
550
591
  out(
551
592
  ctx,
552
593
  await callMutation(ctx, api.cells.set, {
@@ -560,6 +601,7 @@ async function runCellsCommand(ctx: CrmCliContext): Promise<void> {
560
601
  }
561
602
  case "append": {
562
603
  const value = parseJson(ctx.args.shift()) ?? null;
604
+ assertNoUnknownFlags(ctx.args, "crm cells append");
563
605
  out(
564
606
  ctx,
565
607
  await callMutation(ctx, api.cells.append, {
@@ -584,6 +626,7 @@ async function runQueryCommand(ctx: CrmCliContext): Promise<void> {
584
626
  const select = getFlag(ctx.args, "--select")?.split(",").filter(Boolean);
585
627
  const sort = getFlag(ctx.args, "--sort");
586
628
  const limit = parseInt(getFlag(ctx.args, "--limit") ?? "100", 10);
629
+ assertNoUnknownFlags(ctx.args, "crm query");
587
630
  // Convex's wire serializer rejects $-prefixed object keys, so we
588
631
  // route the DSL through the dslJson string field whenever the filter
589
632
  // has any operator-shaped clause.
@@ -591,7 +634,8 @@ async function runQueryCommand(ctx: CrmCliContext): Promise<void> {
591
634
  const hasDollarKey = filter ? containsDollarKey(filter) : false;
592
635
  out(
593
636
  ctx,
594
- await callQuery(ctx,
637
+ await callQuery(
638
+ ctx,
595
639
  api.query.queryEntries,
596
640
  hasDollarKey
597
641
  ? { objectName, dslJson: JSON.stringify(dsl) }
@@ -616,6 +660,7 @@ async function runAggregateCommand(ctx: CrmCliContext): Promise<void> {
616
660
  const objectName = shift(ctx.args, "object name");
617
661
  const fieldName = getFlag(ctx.args, "--field");
618
662
  if (!fieldName) throw new CrmCliError("--field is required");
663
+ assertNoUnknownFlags(ctx.args, "crm aggregate");
619
664
  out(
620
665
  ctx,
621
666
  await callQuery(ctx, api.query.aggregate, { objectName, fieldName }),
@@ -624,13 +669,14 @@ async function runAggregateCommand(ctx: CrmCliContext): Promise<void> {
624
669
 
625
670
  async function runSearchCommand(ctx: CrmCliContext): Promise<void> {
626
671
  const objectName = getFlag(ctx.args, "--object");
672
+ const limit = parseInt(getFlag(ctx.args, "--limit") ?? "50", 10);
673
+ // Drain known flags before joining the rest into the search text so a
674
+ // typo like `--objct` surfaces instead of being concatenated into the
675
+ // query string.
676
+ assertNoUnknownFlags(ctx.args, "crm search");
627
677
  const text = ctx.args.join(" ").trim();
628
678
  if (!text) throw new CrmCliError("Search text required");
629
- const limit = parseInt(getFlag(ctx.args, "--limit") ?? "50", 10);
630
- out(
631
- ctx,
632
- await callQuery(ctx, api.query.search, { text, objectName, limit }),
633
- );
679
+ out(ctx, await callQuery(ctx, api.query.search, { text, objectName, limit }));
634
680
  }
635
681
 
636
682
  async function runStatusesCommand(ctx: CrmCliContext): Promise<void> {
@@ -638,12 +684,14 @@ async function runStatusesCommand(ctx: CrmCliContext): Promise<void> {
638
684
  switch (verb) {
639
685
  case "list": {
640
686
  const objectName = shift(ctx.args, "object name");
687
+ assertNoUnknownFlags(ctx.args, "crm statuses list");
641
688
  out(ctx, await callQuery(ctx, api.statuses.list, { objectName }));
642
689
  return;
643
690
  }
644
691
  case "set": {
645
692
  const objectName = shift(ctx.args, "object name");
646
693
  const statuses = parseJson(getFlag(ctx.args, "--statuses")) as any[];
694
+ assertNoUnknownFlags(ctx.args, "crm statuses set");
647
695
  if (!Array.isArray(statuses)) {
648
696
  throw new CrmCliError("--statuses (JSON array) is required");
649
697
  }
@@ -663,6 +711,10 @@ async function runStatusesCommand(ctx: CrmCliContext): Promise<void> {
663
711
 
664
712
  async function runBatchCommand(ctx: CrmCliContext): Promise<void> {
665
713
  const filePath = getFlag(ctx.args, "--file");
714
+ // The help text advertises --idempotency-key; thread it through so the
715
+ // doc and the implementation agree.
716
+ const idempotencyKey = getFlag(ctx.args, "--idempotency-key");
717
+ assertNoUnknownFlags(ctx.args, "crm batch");
666
718
  if (!filePath) {
667
719
  throw new CrmCliError(
668
720
  "dench crm batch requires --file <ops.jsonl> (one op per line)",
@@ -683,7 +735,8 @@ async function runBatchCommand(ctx: CrmCliContext): Promise<void> {
683
735
  ctx,
684
736
  await callMutation(ctx, api.batch.apply, {
685
737
  ops: ops as any,
686
- }),
738
+ ...(idempotencyKey ? { idempotencyKey } : {}),
739
+ } as any),
687
740
  );
688
741
  }
689
742
 
@@ -691,6 +744,7 @@ async function runDocsCommand(ctx: CrmCliContext): Promise<void> {
691
744
  const verb = ctx.args.shift();
692
745
  switch (verb) {
693
746
  case "list": {
747
+ assertNoUnknownFlags(ctx.args, "crm docs list");
694
748
  out(ctx, await callQuery(ctx, api.documents.list, {}));
695
749
  return;
696
750
  }
@@ -699,6 +753,7 @@ async function runDocsCommand(ctx: CrmCliContext): Promise<void> {
699
753
  const title = getFlag(ctx.args, "--title") ?? filePath.split("/").pop()!;
700
754
  const icon = getFlag(ctx.args, "--icon");
701
755
  const linkEntryId = getFlag(ctx.args, "--link-entry");
756
+ assertNoUnknownFlags(ctx.args, "crm docs create");
702
757
  out(
703
758
  ctx,
704
759
  await callMutation(ctx, api.documents.create, {
@@ -714,6 +769,7 @@ async function runDocsCommand(ctx: CrmCliContext): Promise<void> {
714
769
  const filePath = shift(ctx.args, "file path");
715
770
  const objectName = getFlag(ctx.args, "--object");
716
771
  const entryId = getFlag(ctx.args, "--entry");
772
+ assertNoUnknownFlags(ctx.args, "crm docs link");
717
773
  out(
718
774
  ctx,
719
775
  await callMutation(ctx, api.documents.link, {
@@ -734,6 +790,7 @@ async function runActionsCommand(ctx: CrmCliContext): Promise<void> {
734
790
  switch (verb) {
735
791
  case "list": {
736
792
  const entryId = shift(ctx.args, "entry id");
793
+ assertNoUnknownFlags(ctx.args, "crm actions list");
737
794
  out(
738
795
  ctx,
739
796
  await callQuery(ctx, api.actions.list, {
@@ -750,6 +807,7 @@ async function runActionsCommand(ctx: CrmCliContext): Promise<void> {
750
807
  const entryId = getFlag(ctx.args, "--entry-id");
751
808
  const goal = getFlag(ctx.args, "--goal") ?? "";
752
809
  const wait = hasFlag(ctx.args, "--wait");
810
+ assertNoUnknownFlags(ctx.args, "crm actions run");
753
811
  if (!fieldId || !entryId) {
754
812
  throw new CrmCliError(
755
813
  "dench crm actions run requires --field-id and --entry-id",
@@ -804,35 +862,50 @@ async function runTransactionCommand(ctx: CrmCliContext): Promise<void> {
804
862
  const verb = ctx.args.shift();
805
863
  switch (verb) {
806
864
  case "begin": {
865
+ assertNoUnknownFlags(ctx.args, "crm transaction begin");
807
866
  out(ctx, await callMutation(ctx, api.transaction.begin, {}));
808
867
  return;
809
868
  }
810
869
  case "add": {
811
870
  const txnId = shift(ctx.args, "txn id");
812
871
  const opType = shift(ctx.args, "op type (entries.create|update|delete)");
813
- const objectName = ctx.args[0];
814
872
  let op: any;
815
873
  if (opType === "entries.create") {
816
- op = {
817
- type: "entries.create",
818
- objectName: shift(ctx.args, "object name"),
819
- data:
820
- (parseJson(getFlag(ctx.args, "--data")) as JsonRecord | undefined) ??
821
- {},
822
- };
874
+ const opObjectName = shift(ctx.args, "object name");
875
+ const dataRaw = getDataFlag(ctx.args);
876
+ if (dataRaw === undefined) {
877
+ throw new CrmCliError(
878
+ "dench crm transaction add <txn> entries.create requires --data '{...}' (alias: --fields)",
879
+ );
880
+ }
881
+ const data = (parseJson(dataRaw) as JsonRecord | undefined) ?? {};
882
+ if (!data || Object.keys(data).length === 0) {
883
+ throw new CrmCliError(
884
+ "dench crm transaction add entries.create: --data must include at least one field.",
885
+ );
886
+ }
887
+ op = { type: "entries.create", objectName: opObjectName, data };
823
888
  } else if (opType === "entries.update") {
824
- op = {
825
- type: "entries.update",
826
- entryId: shift(ctx.args, "entry id"),
827
- data:
828
- (parseJson(getFlag(ctx.args, "--data")) as JsonRecord | undefined) ??
829
- {},
830
- };
889
+ const opEntryId = shift(ctx.args, "entry id");
890
+ const dataRaw = getDataFlag(ctx.args);
891
+ if (dataRaw === undefined) {
892
+ throw new CrmCliError(
893
+ "dench crm transaction add <txn> entries.update requires --data '{...}' (alias: --fields)",
894
+ );
895
+ }
896
+ const data = (parseJson(dataRaw) as JsonRecord | undefined) ?? {};
897
+ if (!data || Object.keys(data).length === 0) {
898
+ throw new CrmCliError(
899
+ "dench crm transaction add entries.update: --data must include at least one field.",
900
+ );
901
+ }
902
+ op = { type: "entries.update", entryId: opEntryId, data };
831
903
  } else if (opType === "entries.delete") {
832
904
  op = { type: "entries.delete", entryId: shift(ctx.args, "entry id") };
833
905
  } else {
834
906
  throw new CrmCliError(`Unknown txn op type: ${opType}`);
835
907
  }
908
+ assertNoUnknownFlags(ctx.args, "crm transaction add");
836
909
  out(
837
910
  ctx,
838
911
  await callMutation(ctx, api.transaction.addOp, {
@@ -844,6 +917,7 @@ async function runTransactionCommand(ctx: CrmCliContext): Promise<void> {
844
917
  }
845
918
  case "commit": {
846
919
  const txnId = shift(ctx.args, "txn id");
920
+ assertNoUnknownFlags(ctx.args, "crm transaction commit");
847
921
  out(
848
922
  ctx,
849
923
  await callMutation(ctx, api.transaction.commit, {
@@ -854,6 +928,7 @@ async function runTransactionCommand(ctx: CrmCliContext): Promise<void> {
854
928
  }
855
929
  case "abort": {
856
930
  const txnId = shift(ctx.args, "txn id");
931
+ assertNoUnknownFlags(ctx.args, "crm transaction abort");
857
932
  out(
858
933
  ctx,
859
934
  await callMutation(ctx, api.transaction.abort, {
@@ -864,6 +939,7 @@ async function runTransactionCommand(ctx: CrmCliContext): Promise<void> {
864
939
  }
865
940
  case "inspect": {
866
941
  const txnId = shift(ctx.args, "txn id");
942
+ assertNoUnknownFlags(ctx.args, "crm transaction inspect");
867
943
  out(
868
944
  ctx,
869
945
  await callQuery(ctx, api.transaction.inspect, {
@@ -878,6 +954,7 @@ async function runTransactionCommand(ctx: CrmCliContext): Promise<void> {
878
954
  ctx.args.unshift(verb ?? "");
879
955
  const filePath = getFlag(ctx.args, "--file");
880
956
  const commit = hasFlag(ctx.args, "--commit");
957
+ assertNoUnknownFlags(ctx.args, "crm transaction --file");
881
958
  if (!filePath) {
882
959
  throw new CrmCliError(
883
960
  "dench crm transaction --file <ops.json> [--commit]",
@@ -886,10 +963,9 @@ async function runTransactionCommand(ctx: CrmCliContext): Promise<void> {
886
963
  const fs = await import("node:fs/promises");
887
964
  const text = await fs.readFile(filePath, "utf-8");
888
965
  const ops = JSON.parse(text) as unknown[];
889
- const begun = (await callMutation(ctx,
890
- api.transaction.begin,
891
- {},
892
- )) as { txnId: string };
966
+ const begun = (await callMutation(ctx, api.transaction.begin, {})) as {
967
+ txnId: string;
968
+ };
893
969
  for (const op of ops) {
894
970
  await callMutation(ctx, api.transaction.addOp, {
895
971
  txnId: begun.txnId as any,
@@ -924,6 +1000,7 @@ async function runImportCommand(ctx: CrmCliContext): Promise<void> {
924
1000
  | "skip"
925
1001
  | "update"
926
1002
  | "error";
1003
+ assertNoUnknownFlags(ctx.args, "crm import");
927
1004
  if (!csvPath && !jsonlPath) {
928
1005
  throw new CrmCliError(
929
1006
  "dench crm import <object> requires --csv <path> or --jsonl <path>",
@@ -965,10 +1042,9 @@ async function runImportCommand(ctx: CrmCliContext): Promise<void> {
965
1042
 
966
1043
  async function runExportCommand(ctx: CrmCliContext): Promise<void> {
967
1044
  const objectName = shift(ctx.args, "object name");
968
- const format = (getFlag(ctx.args, "--format") ?? "jsonl") as
969
- | "csv"
970
- | "jsonl";
1045
+ const format = (getFlag(ctx.args, "--format") ?? "jsonl") as "csv" | "jsonl";
971
1046
  const limit = parseInt(getFlag(ctx.args, "--limit") ?? "10000", 10);
1047
+ assertNoUnknownFlags(ctx.args, "crm export");
972
1048
  // Page through entries.list in 1000-row chunks.
973
1049
  const all: any[] = [];
974
1050
  let cursor = 0;
@@ -1012,9 +1088,7 @@ export function parseCsv(
1012
1088
  const rows: Record<string, unknown>[] = [];
1013
1089
  const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
1014
1090
  if (lines.length === 0) return rows;
1015
- const header = parseCsvLine(lines[0]).map(
1016
- (col) => colMap[col] ?? col,
1017
- );
1091
+ const header = parseCsvLine(lines[0]).map((col) => colMap[col] ?? col);
1018
1092
  for (let i = 1; i < lines.length; i++) {
1019
1093
  const cols = parseCsvLine(lines[i]);
1020
1094
  const row: Record<string, unknown> = {};
@@ -1109,7 +1183,10 @@ async function runSqlCommand(ctx: CrmCliContext): Promise<void> {
1109
1183
  .split(",")
1110
1184
  .map((s) => {
1111
1185
  const desc = /\s+DESC\s*$/i.test(s);
1112
- return `${desc ? "-" : ""}${s.replace(/\s+(asc|desc)\s*$/i, "").trim().replace(/"/g, "")}`;
1186
+ return `${desc ? "-" : ""}${s
1187
+ .replace(/\s+(asc|desc)\s*$/i, "")
1188
+ .trim()
1189
+ .replace(/"/g, "")}`;
1113
1190
  })
1114
1191
  .join(",")
1115
1192
  : undefined;
@@ -1131,6 +1208,7 @@ async function runPeopleCommand(ctx: CrmCliContext): Promise<void> {
1131
1208
  const verb = ctx.args.shift();
1132
1209
  switch (verb) {
1133
1210
  case "search": {
1211
+ assertNoUnknownFlags(ctx.args, "crm people search");
1134
1212
  const text = ctx.args.join(" ").trim();
1135
1213
  if (!text) throw new CrmCliError("Search text required");
1136
1214
  out(
@@ -1144,9 +1222,19 @@ async function runPeopleCommand(ctx: CrmCliContext): Promise<void> {
1144
1222
  return;
1145
1223
  }
1146
1224
  case "upsert": {
1147
- const data =
1148
- (parseJson(getFlag(ctx.args, "--data")) as JsonRecord | undefined) ??
1149
- {};
1225
+ const dataRaw = getDataFlag(ctx.args);
1226
+ assertNoUnknownFlags(ctx.args, "crm people upsert");
1227
+ if (dataRaw === undefined) {
1228
+ throw new CrmCliError(
1229
+ 'dench crm people upsert requires --data \'{"Email Address":"…",...}\' (alias: --fields)',
1230
+ );
1231
+ }
1232
+ const data = (parseJson(dataRaw) as JsonRecord | undefined) ?? {};
1233
+ if (!data || Object.keys(data).length === 0) {
1234
+ throw new CrmCliError(
1235
+ "dench crm people upsert: --data must include at least one field.",
1236
+ );
1237
+ }
1150
1238
  out(
1151
1239
  ctx,
1152
1240
  await callMutation(ctx, api.entries.create, {
@@ -1159,11 +1247,13 @@ async function runPeopleCommand(ctx: CrmCliContext): Promise<void> {
1159
1247
  case "enrich": {
1160
1248
  const entryId = shift(ctx.args, "person entry id");
1161
1249
  const provider = getFlag(ctx.args, "--provider");
1250
+ const fieldName = getFlag(ctx.args, "--field") ?? "Email Address";
1251
+ assertNoUnknownFlags(ctx.args, "crm people enrich");
1162
1252
  out(
1163
1253
  ctx,
1164
1254
  await callMutation(ctx, api.enrich.requestObjectEnrichment, {
1165
1255
  objectName: "people",
1166
- fieldName: getFlag(ctx.args, "--field") ?? "Email Address",
1256
+ fieldName,
1167
1257
  provider,
1168
1258
  }),
1169
1259
  );
@@ -1182,6 +1272,7 @@ async function runCompaniesCommand(ctx: CrmCliContext): Promise<void> {
1182
1272
  const verb = ctx.args.shift();
1183
1273
  switch (verb) {
1184
1274
  case "search": {
1275
+ assertNoUnknownFlags(ctx.args, "crm companies search");
1185
1276
  const text = ctx.args.join(" ").trim();
1186
1277
  if (!text) throw new CrmCliError("Search text required");
1187
1278
  out(
@@ -1197,9 +1288,15 @@ async function runCompaniesCommand(ctx: CrmCliContext): Promise<void> {
1197
1288
  case "upsert": {
1198
1289
  const domain = getFlag(ctx.args, "--domain");
1199
1290
  const name = getFlag(ctx.args, "--name");
1291
+ assertNoUnknownFlags(ctx.args, "crm companies upsert");
1200
1292
  const data: Record<string, unknown> = {};
1201
1293
  if (domain) data["Domain"] = domain;
1202
1294
  if (name) data["Name"] = name;
1295
+ if (Object.keys(data).length === 0) {
1296
+ throw new CrmCliError(
1297
+ "dench crm companies upsert requires at least --domain or --name. For arbitrary fields, use `dench crm entries create company --data '{...}'`.",
1298
+ );
1299
+ }
1203
1300
  out(
1204
1301
  ctx,
1205
1302
  await callMutation(ctx, api.entries.create, {
@@ -1212,11 +1309,13 @@ async function runCompaniesCommand(ctx: CrmCliContext): Promise<void> {
1212
1309
  case "enrich": {
1213
1310
  const entryId = shift(ctx.args, "company entry id");
1214
1311
  const provider = getFlag(ctx.args, "--provider");
1312
+ const fieldName = getFlag(ctx.args, "--field") ?? "Domain";
1313
+ assertNoUnknownFlags(ctx.args, "crm companies enrich");
1215
1314
  out(
1216
1315
  ctx,
1217
1316
  await callMutation(ctx, api.enrich.requestObjectEnrichment, {
1218
1317
  objectName: "company",
1219
- fieldName: getFlag(ctx.args, "--field") ?? "Domain",
1318
+ fieldName,
1220
1319
  provider,
1221
1320
  }),
1222
1321
  );
@@ -1238,6 +1337,7 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
1238
1337
  const entryId = shift(ctx.args, "entry id");
1239
1338
  const fieldName = shift(ctx.args, "field name");
1240
1339
  const provider = getFlag(ctx.args, "--provider");
1340
+ assertNoUnknownFlags(ctx.args, "crm enrich cell");
1241
1341
  out(
1242
1342
  ctx,
1243
1343
  await callMutation(ctx, api.enrich.requestCellEnrichment, {
@@ -1254,6 +1354,7 @@ async function runEnrichCommand(ctx: CrmCliContext): Promise<void> {
1254
1354
  const fieldName = getFlag(ctx.args, "--field");
1255
1355
  const missingOnly = !hasFlag(ctx.args, "--all");
1256
1356
  const provider = getFlag(ctx.args, "--provider");
1357
+ assertNoUnknownFlags(ctx.args, "crm enrich object");
1257
1358
  if (!fieldName) {
1258
1359
  throw new CrmCliError(
1259
1360
  "dench crm enrich object <name> --field <field> [--provider ...] [--all]",
@@ -1286,11 +1387,7 @@ async function runReportsCommand(ctx: CrmCliContext): Promise<void> {
1286
1387
  }
1287
1388
  const _name = shift(ctx.args, "report name");
1288
1389
  const objectName = getFlag(ctx.args, "--object");
1289
- const type = getFlag(ctx.args, "--type") as
1290
- | "pie"
1291
- | "bar"
1292
- | "line"
1293
- | "table";
1390
+ const type = getFlag(ctx.args, "--type") as "pie" | "bar" | "line" | "table";
1294
1391
  const groupBy = getFlag(ctx.args, "--group-by");
1295
1392
  const metric = getFlag(ctx.args, "--metric") as
1296
1393
  | "count"
@@ -1300,6 +1397,7 @@ async function runReportsCommand(ctx: CrmCliContext): Promise<void> {
1300
1397
  | "max"
1301
1398
  | undefined;
1302
1399
  const metricField = getFlag(ctx.args, "--metric-field");
1400
+ assertNoUnknownFlags(ctx.args, "crm reports generate");
1303
1401
  if (!objectName || !type || !groupBy) {
1304
1402
  throw new CrmCliError(
1305
1403
  "dench crm reports generate requires --object, --type, --group-by",
@@ -1341,10 +1439,13 @@ Fields (columns):
1341
1439
  Entries (rows):
1342
1440
  dench crm entries list <object> [--limit N] [--json]
1343
1441
  dench crm entries get <object> <entryId>
1344
- dench crm entries create <object> --data '{"Field":"Value",...}'
1345
- dench crm entries update <object> <entryId> --data '{...}'
1442
+ dench crm entries create <object> --data '{"Field":"Value",...}' (alias: --fields)
1443
+ dench crm entries update <object> <entryId> --data '{...}' (alias: --fields)
1346
1444
  dench crm entries delete <object> <entryId>
1347
1445
  dench crm entries bulk-delete <object> --ids id1,id2,id3
1446
+ Note: --data is REQUIRED on create/update and must include >=1 field.
1447
+ An empty --data '{}' is rejected — use 'crm cells set' for
1448
+ single-field tweaks instead of creating a placeholder blank row.
1348
1449
 
1349
1450
  Cells (single-field updates):
1350
1451
  dench crm cells get <object> <entryId> <field>
@@ -1385,7 +1486,7 @@ SQL escape hatch (subset; flat SELECTs only):
1385
1486
 
1386
1487
  People + companies shortcuts:
1387
1488
  dench crm people search '<query>'
1388
- dench crm people upsert --data '{"Email Address":"jane@example.com",...}'
1489
+ dench crm people upsert --data '{"Email Address":"jane@example.com",...}' (alias: --fields)
1389
1490
  dench crm people enrich <entryId> [--provider apollo]
1390
1491
  dench crm companies search '<query>'
1391
1492
  dench crm companies upsert --domain acme.com [--name Acme]