@unisonlabs/sixminute-admin 0.1.608-next → 0.1.610-next

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,10 +58,17 @@ sixminute-admin research export --env production --tenant tenant_... --from 2026
58
58
  sixminute-admin activity-type list --env production --tenant tenant_...
59
59
  sixminute-admin member list --env production --tenant tenant_...
60
60
  sixminute-admin matters import --env production --tenant tenant_... --from-file matters.json
61
+ sixminute-admin matter list --env production --tenant tenant_... --all
62
+ sixminute-admin matter edit --env production --tenant tenant_... --matter matter_... --name "Example Holding: lease review"
63
+ sixminute-admin matter archive --env production --tenant tenant_... --matter matter_...
64
+ sixminute-admin client edit --env production --tenant tenant_... --client client_... --external-ref C-101
61
65
  sixminute-admin connector status --env production --firm tenant_...
62
66
  sixminute-admin audit --env production --limit 20
63
67
  ```
64
68
 
65
69
  `sixminute-admin --help` lists every command, with the warnings on the destructive ones.
66
- `client create`, `matter create` and `matters import` work only on a firm marked internal (our own).
70
+ Creating, editing and archiving clients and matters, and `matters import`, work only on a firm
71
+ marked internal (our own). Archive is the delete: the row and its entries stay, and an archived
72
+ matter leaves the pickers; nothing deletes a client or a matter. A client is archived only once
73
+ none of its matters is open: archive the matters first.
67
74
  `sixminute-admin --version` prints the version and the API commit it was built from.
@@ -21,7 +21,7 @@ import { homedir } from "node:os";
21
21
  import { fileURLToPath } from "node:url";
22
22
 
23
23
  // tools/admin/src/build-info.ts
24
- var BUILT = { version: "0.1.608-next", commit: "b4feeba888b88d7044085c3b07dee4d6f515a4e0", surface: "e80083c611d5d5e0" };
24
+ var BUILT = { version: "0.1.610-next", commit: "7e589dae1055faaac0407d389ef7f333e3b84aec", surface: "e738440f007ffca1" };
25
25
 
26
26
  // tools/admin/src/run.ts
27
27
  import { rmSync as rmSync3 } from "node:fs";
@@ -6712,12 +6712,31 @@ is required; without one every command answers 403.
6712
6712
 
6713
6713
  sixminute-admin client create --env <env> --tenant tenant_... --name "Example Holding" [--external-ref C-100] [--id client_...] [--json]
6714
6714
  Creates one client. A name already in use is refused.
6715
- sixminute-admin client list --env <env> --tenant tenant_... [--json]
6716
- Every client of the firm with its id, which matter create takes.
6715
+ sixminute-admin client list --env <env> --tenant tenant_... [--all] [--json]
6716
+ Every client of the firm with its id, which matter create takes, and
6717
+ whether it is active or archived. Archived clients are left out
6718
+ unless --all.
6719
+ sixminute-admin client edit --env <env> --tenant tenant_... --client client_... [--name "..."] [--external-ref R] [--active true|false] [--json]
6720
+ Changes only the fields given; at least one is required. A name
6721
+ already in use is refused.
6722
+ sixminute-admin client archive --env <env> --tenant tenant_... --client client_... [--json]
6723
+ sixminute-admin client unarchive --env <env> --tenant tenant_... --client client_... [--json]
6724
+ Sets the client inactive, or active again.
6717
6725
  sixminute-admin matter create --env <env> --tenant tenant_... --client client_... --name "Example Holding: general advice" \\
6718
6726
  [--number 2026-0001] [--description "..."] [--external-ref R] [--billable true|false] [--temporary true|false] [--id matter_...] [--json]
6719
6727
  Creates one matter under a client of the firm. --number is the firm's
6720
6728
  matter number, unique within the client. An unknown client is refused.
6729
+ sixminute-admin matter list --env <env> --tenant tenant_... [--client client_...] [--all] [--json]
6730
+ Every matter of the firm, or of one client: id, number, active or
6731
+ archived, client and name. Archived matters are left out unless --all.
6732
+ sixminute-admin matter edit --env <env> --tenant tenant_... --matter matter_... \\
6733
+ [--name "..."] [--number N] [--description "..."] [--external-ref R] [--active true|false] [--json]
6734
+ Changes only the fields given; at least one is required. A matter
6735
+ belongs to one client for life, so there is no --client.
6736
+ sixminute-admin matter archive --env <env> --tenant tenant_... --matter matter_... [--json]
6737
+ sixminute-admin matter unarchive --env <env> --tenant tenant_... --matter matter_... [--json]
6738
+ Sets the matter inactive, or active again. A temporary matter with
6739
+ entries on it is refused: move the entries first.
6721
6740
  sixminute-admin matters import --env <env> --tenant tenant_... --from-file tools/admin/examples/matters-import.json [--json]
6722
6741
  Clients with their matters, from a document: the file IS the body of
6723
6742
  POST /v1/matters/import, checked against its schema before anything is
@@ -6725,10 +6744,14 @@ is required; without one every command answers 403.
6725
6744
  missing rows are created and known rows corrected, so running the same
6726
6745
  file again changes nothing. Prints how many were created, updated and
6727
6746
  left alone.
6728
- client create, matter create and matters import WORK ONLY ON AN INTERNAL
6729
- FIRM, one marked with tenant internal set: a real firm's clients and
6730
- matters come from its practice system. Any other firm is refused, exit 2,
6731
- and nothing is written.
6747
+ The client and matter create, edit, archive and unarchive commands and
6748
+ matters import WORK ONLY ON AN INTERNAL FIRM, one marked with tenant
6749
+ internal set: a real firm's clients and matters come from its practice
6750
+ system. Any other firm is refused, exit 2, and nothing is written.
6751
+ Archive is the delete: it keeps the row and every entry on it, and an
6752
+ archived matter leaves the pickers. client archive refuses while the
6753
+ client has an open matter, and prints the matter archive line for each
6754
+ one. Nothing here deletes a client or a matter.
6732
6755
 
6733
6756
  sixminute-admin member list --env <env> --tenant tenant_... [--json]
6734
6757
  The firm's members and its pending invitations.
@@ -6800,7 +6823,8 @@ var BOOLEAN_FLAGS = new Set([
6800
6823
  "required",
6801
6824
  "proposed",
6802
6825
  "confirm",
6803
- "password-stdin"
6826
+ "password-stdin",
6827
+ "all"
6804
6828
  ]);
6805
6829
  function parseFlags(argv, offset) {
6806
6830
  const values = new Map;
@@ -6971,8 +6995,8 @@ async function runAdmin(argv, deps) {
6971
6995
  return runMember(run2, subcommand);
6972
6996
  if (group === "client")
6973
6997
  return runClient(run2, subcommand);
6974
- if (group === "matter" && subcommand === "create")
6975
- return runMatterCreate(run2);
6998
+ if (group === "matter")
6999
+ return runMatter(run2, subcommand);
6976
7000
  if (group === "matters" && subcommand === "import")
6977
7001
  return runMattersImport(run2);
6978
7002
  if (group === "tier" && subcommand === "set")
@@ -7457,7 +7481,34 @@ async function readMatterSets(firm, matterId) {
7457
7481
  since = page.next_cursor;
7458
7482
  }
7459
7483
  }
7460
- var CLIENT_PAGE = 2000;
7484
+ var PULL_PAGE = 2000;
7485
+ async function pullAll(run2, tenant, path, row) {
7486
+ const rows = [];
7487
+ let since = null;
7488
+ let head = null;
7489
+ for (;; ) {
7490
+ const query = since === null ? `?limit=${PULL_PAGE}` : `?limit=${PULL_PAGE}&since=${enc(since)}`;
7491
+ const answer = await call(run2.api, "GET", tenantPath(tenant, `${path}${query}`));
7492
+ head ??= actingHead(tenant, answer);
7493
+ const page = pullResultSchema(row).parse(expectOk(answer));
7494
+ rows.push(...page.rows);
7495
+ if (page.next_cursor === null)
7496
+ return { rows, head };
7497
+ since = page.next_cursor;
7498
+ }
7499
+ }
7500
+ var stateOf = (row) => row.active ? "active" : "archived";
7501
+ function countLine(noun, shown, hidden) {
7502
+ return `${shown} ${noun}(s)${hidden === 0 ? "" : `; ${hidden} archived not shown, --all lists them`}`;
7503
+ }
7504
+ function patchOf(fields, schema, flagNames) {
7505
+ const body = definedFields(fields);
7506
+ if (Object.keys(body).length === 0)
7507
+ throw new AdminUsageError(`nothing to change: give at least one of ${flagNames}`);
7508
+ checkBody(schema, body);
7509
+ return body;
7510
+ }
7511
+ var clientLine = (c) => `client ${c.id} ${stateOf(c)} ${c.name}${c.external_ref === null ? "" : ` ref ${c.external_ref}`}`;
7461
7512
  async function runClient(run2, subcommand) {
7462
7513
  const { flags } = run2;
7463
7514
  const tenant = required(flags, "tenant", "the firm the client belongs to");
@@ -7478,50 +7529,95 @@ async function runClient(run2, subcommand) {
7478
7529
  ]);
7479
7530
  }
7480
7531
  if (subcommand === "list") {
7481
- const clients = [];
7482
- let since = null;
7483
- let head = null;
7484
- for (;; ) {
7485
- const query = since === null ? `?limit=${CLIENT_PAGE}` : `?limit=${CLIENT_PAGE}&since=${enc(since)}`;
7486
- const answer = await call(run2.api, "GET", tenantPath(tenant, `/clients${query}`));
7487
- head ??= actingHead(tenant, answer);
7488
- const page = pullResultSchema(ClientSchema).parse(expectOk(answer));
7489
- clients.push(...page.rows);
7490
- if (page.next_cursor === null)
7491
- break;
7492
- since = page.next_cursor;
7493
- }
7532
+ const { rows, head } = await pullAll(run2, tenant, "/clients", ClientSchema);
7533
+ const clients = switched(flags, "all") ? rows : rows.filter((c) => c.active);
7494
7534
  return render(run2, { clients }, [
7495
7535
  head,
7496
- `${clients.length} client(s)`,
7497
- ...clients.map((c) => ` ${c.id} ${c.name}${c.external_ref === null ? "" : ` ref ${c.external_ref}`}${c.active ? "" : " (inactive)"}`)
7536
+ countLine("client", clients.length, rows.length - clients.length),
7537
+ ...clients.map((c) => ` ${c.id} ${stateOf(c)} ${c.name}${c.external_ref === null ? "" : ` ref ${c.external_ref}`}`)
7498
7538
  ]);
7499
7539
  }
7540
+ if (subcommand === "edit" || subcommand === "archive" || subcommand === "unarchive") {
7541
+ const id = required(flags, "client", "which client_... to change (client list --all prints them)");
7542
+ const body = subcommand === "edit" ? patchOf({ name: optional(flags, "name"), external_ref: optional(flags, "external-ref"), active: booleanOf(flags, "active") }, ClientPatchSchema, "--name, --external-ref, --active") : { active: subcommand === "unarchive" };
7543
+ if (body["active"] === false) {
7544
+ const { rows } = await pullAll(run2, tenant, "/matters", MatterSchema);
7545
+ const open = rows.filter((m) => m.client_id === id && m.active);
7546
+ if (open.length > 0) {
7547
+ throw new AdminUsageError(`${id} still has ${open.length} open matter(s), which stay pickable while they are open; archive them first:
7548
+ ` + open.map((m) => ` sixminute-admin matter archive --env ${run2.environment.name} --tenant ${tenant} --matter ${m.id} (${m.name})`).join(`
7549
+ `));
7550
+ }
7551
+ }
7552
+ const answer = await call(run2.api, "PATCH", tenantPath(tenant, `/clients/${enc(id)}`), body);
7553
+ if (answer.status !== 200)
7554
+ return refused(run2, answer, [actingHead(tenant, answer)]);
7555
+ const client = ClientSchema.parse(answer.body);
7556
+ return render(run2, { client }, [actingHead(tenant, answer), clientLine(client)]);
7557
+ }
7500
7558
  throw new AdminUsageError(`unknown command: client ${subcommand ?? ""}`.trim());
7501
7559
  }
7502
- async function runMatterCreate(run2) {
7560
+ var matterLines = (m) => [
7561
+ `matter ${m.id} ${m.code ?? "no number"} ${stateOf(m)} ${m.name}`,
7562
+ `client ${m.client_id}`
7563
+ ];
7564
+ async function runMatter(run2, subcommand) {
7503
7565
  const { flags } = run2;
7504
7566
  const tenant = required(flags, "tenant", "the firm the matter belongs to");
7505
- const body = definedFields({
7506
- id: optional(flags, "id"),
7507
- client_id: required(flags, "client", "the client_... the matter is under (client list prints them)"),
7508
- name: required(flags, "name", "what the matter is called"),
7509
- code: optional(flags, "number"),
7510
- description: optional(flags, "description"),
7511
- external_ref: optional(flags, "external-ref"),
7512
- billable: booleanOf(flags, "billable"),
7513
- temporary: booleanOf(flags, "temporary")
7514
- });
7515
- checkBody(MatterInputSchema, body);
7516
- const answer = await call(run2.api, "POST", tenantPath(tenant, "/matters"), body);
7517
- if (answer.status !== 201)
7518
- return refused(run2, answer, [actingHead(tenant, answer)]);
7519
- const matter = MatterSchema.parse(answer.body);
7520
- return render(run2, { matter }, [
7521
- actingHead(tenant, answer),
7522
- `matter ${matter.id} ${matter.code ?? "no number"} ${matter.name}`,
7523
- `client ${matter.client_id}`
7524
- ]);
7567
+ if (subcommand === "create") {
7568
+ const body = definedFields({
7569
+ id: optional(flags, "id"),
7570
+ client_id: required(flags, "client", "the client_... the matter is under (client list prints them)"),
7571
+ name: required(flags, "name", "what the matter is called"),
7572
+ code: optional(flags, "number"),
7573
+ description: optional(flags, "description"),
7574
+ external_ref: optional(flags, "external-ref"),
7575
+ billable: booleanOf(flags, "billable"),
7576
+ temporary: booleanOf(flags, "temporary")
7577
+ });
7578
+ checkBody(MatterInputSchema, body);
7579
+ const answer = await call(run2.api, "POST", tenantPath(tenant, "/matters"), body);
7580
+ if (answer.status !== 201)
7581
+ return refused(run2, answer, [actingHead(tenant, answer)]);
7582
+ const matter = MatterSchema.parse(answer.body);
7583
+ return render(run2, { matter }, [
7584
+ actingHead(tenant, answer),
7585
+ `matter ${matter.id} ${matter.code ?? "no number"} ${matter.name}`,
7586
+ `client ${matter.client_id}`
7587
+ ]);
7588
+ }
7589
+ if (subcommand === "list") {
7590
+ const client = optional(flags, "client");
7591
+ if (client !== undefined)
7592
+ checkBody(MatterSchema.pick({ client_id: true }), { client_id: client });
7593
+ const { rows, head } = await pullAll(run2, tenant, "/matters", MatterSchema);
7594
+ const ofClient = client === undefined ? rows : rows.filter((m) => m.client_id === client);
7595
+ const matters = switched(flags, "all") ? ofClient : ofClient.filter((m) => m.active);
7596
+ return render(run2, { matters }, [
7597
+ head,
7598
+ countLine("matter", matters.length, ofClient.length - matters.length),
7599
+ ...matters.map((m) => ` ${m.id} ${m.code ?? "no number"} ${stateOf(m)} ${m.client_id} ${m.name}`)
7600
+ ]);
7601
+ }
7602
+ if (subcommand === "edit" || subcommand === "archive" || subcommand === "unarchive") {
7603
+ const id = required(flags, "matter", "which matter_... to change (matter list --all prints them)");
7604
+ if (subcommand === "edit" && flags.values.has("client")) {
7605
+ throw new AdminUsageError("--client is not taken on matter edit: a matter belongs to one client for life; create a new matter under the other client");
7606
+ }
7607
+ const body = subcommand === "edit" ? patchOf({
7608
+ name: optional(flags, "name"),
7609
+ code: optional(flags, "number"),
7610
+ description: optional(flags, "description"),
7611
+ external_ref: optional(flags, "external-ref"),
7612
+ active: booleanOf(flags, "active")
7613
+ }, MatterPatchSchema, "--name, --number, --description, --external-ref, --active") : { active: subcommand === "unarchive" };
7614
+ const answer = await call(run2.api, "PATCH", tenantPath(tenant, `/matters/${enc(id)}`), body);
7615
+ if (answer.status !== 200)
7616
+ return refused(run2, answer, [actingHead(tenant, answer)]);
7617
+ const matter = MatterSchema.parse(answer.body);
7618
+ return render(run2, { matter }, [actingHead(tenant, answer), ...matterLines(matter)]);
7619
+ }
7620
+ throw new AdminUsageError(`unknown command: matter ${subcommand ?? ""}`.trim());
7525
7621
  }
7526
7622
  async function runMattersImport(run2) {
7527
7623
  const { flags } = run2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unisonlabs/sixminute-admin",
3
- "version": "0.1.608-next",
3
+ "version": "0.1.610-next",
4
4
  "description": "Sixminute operator tool: the admin commands of the Sixminute API, run with your own operator sign-in.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {