@robbeverhelst/do 0.0.1 → 1.1.0

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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/dist/do.js +582 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,15 @@
1
- # @do/cli
1
+ # @robbeverhelst/do
2
2
 
3
3
  `do` — an agent-drivable command-line client over the **do** public API
4
4
  (`/api/v1`). A thin HTTP client: per-tenant API-key auth, the same validation and
5
5
  RLS wall as the web app, no direct database access.
6
6
 
7
+ > Published to npm as **`@robbeverhelst/do`** (the `@do` scope is taken); the
8
+ > installed command is `do`. The workspace package is named `@do/cli` internally.
9
+
7
10
  ```sh
8
- bunx @do/cli help
9
- bunx @do/cli today
11
+ bunx @robbeverhelst/do help
12
+ bunx @robbeverhelst/do today
10
13
  ```
11
14
 
12
15
  ## Scope (v1)
package/dist/do.js CHANGED
@@ -19,7 +19,14 @@ var __export = (target, all) => {
19
19
  import { homedir } from "os";
20
20
 
21
21
  // src/args.ts
22
- var GLOBAL_BOOLEAN_FLAGS = ["json", "quiet", "verbose", "help", "version"];
22
+ var GLOBAL_BOOLEAN_FLAGS = [
23
+ "json",
24
+ "quiet",
25
+ "verbose",
26
+ "help",
27
+ "version",
28
+ "allow-destructive"
29
+ ];
23
30
  var SHORT_ALIASES = {
24
31
  h: "help",
25
32
  q: "quiet",
@@ -4577,6 +4584,7 @@ var calendarEventLinks = pgTable("calendar_event_links", {
4577
4584
  var apiKeys = pgTable("api_keys", {
4578
4585
  id: text("id").primaryKey(),
4579
4586
  tenantId: text("tenant_id").notNull().references(() => tenants.id),
4587
+ userId: text("user_id").references(() => users.id),
4580
4588
  name: text("name").notNull(),
4581
4589
  prefix: text("prefix").notNull(),
4582
4590
  keyHash: text("key_hash").notNull().unique(),
@@ -20097,7 +20105,23 @@ var apiKeyScopeSchema = exports_external.enum([
20097
20105
  "notes:read",
20098
20106
  "notes:write",
20099
20107
  "contacts:read",
20100
- "contacts:write"
20108
+ "contacts:write",
20109
+ "projects:read",
20110
+ "projects:write",
20111
+ "routines:read",
20112
+ "routines:write",
20113
+ "settings:read",
20114
+ "settings:write",
20115
+ "calendar:read",
20116
+ "members:read",
20117
+ "members:write",
20118
+ "sharing:read",
20119
+ "sharing:write",
20120
+ "comments:read",
20121
+ "comments:write",
20122
+ "roles:read",
20123
+ "links:read",
20124
+ "links:write"
20101
20125
  ]);
20102
20126
  var apiKeySchema = createSelectSchema(apiKeys, {
20103
20127
  scopes: exports_external.array(apiKeyScopeSchema),
@@ -20115,6 +20139,7 @@ var createApiKeyResultSchema = apiKeyPublicSchema.extend({ key: exports_external
20115
20139
  var apiKeyContextSchema = exports_external.object({
20116
20140
  apiKeyId: exports_external.string(),
20117
20141
  tenantId: exports_external.string(),
20142
+ userId: exports_external.string().nullable(),
20118
20143
  scopes: exports_external.array(apiKeyScopeSchema)
20119
20144
  });
20120
20145
  // ../../packages/schema/src/contracts/webhooks.ts
@@ -20965,7 +20990,7 @@ function parseCapture(input) {
20965
20990
 
20966
20991
  // src/meta.ts
20967
20992
  var CLI_NAME = "do";
20968
- var VERSION = "0.0.0";
20993
+ var VERSION = "1.1.0";
20969
20994
  var API_PREFIX = "/api/v1";
20970
20995
  var DEFAULT_API_URL = "http://localhost:3001";
20971
20996
 
@@ -20984,6 +21009,12 @@ class DoClient {
20984
21009
  patch(path, body) {
20985
21010
  return this.request("PATCH", path, { body });
20986
21011
  }
21012
+ put(path, body) {
21013
+ return this.request("PUT", path, { body });
21014
+ }
21015
+ delete(path) {
21016
+ return this.request("DELETE", path, {});
21017
+ }
20987
21018
  url(path, query) {
20988
21019
  const origin = this.opts.apiUrl.replace(/\/+$/, "");
20989
21020
  const url2 = new URL(`${origin}${API_PREFIX}${path}`);
@@ -21127,7 +21158,15 @@ var ENTITIES = [
21127
21158
  canGet: true,
21128
21159
  listFlags: [
21129
21160
  { name: "filter", description: "all (default) or unsorted (the GTD inbox)", query: "filter" }
21130
- ]
21161
+ ],
21162
+ write: {
21163
+ scope: "tasks:write",
21164
+ createSchema: createTaskInputSchema,
21165
+ createInput: "CreateTaskInput",
21166
+ updateSchema: updateTaskInputSchema,
21167
+ updateInput: "UpdateTaskInput",
21168
+ canDelete: true
21169
+ }
21131
21170
  },
21132
21171
  {
21133
21172
  name: "habits",
@@ -21135,22 +21174,79 @@ var ENTITIES = [
21135
21174
  readScope: "habits:read",
21136
21175
  returns: "HabitSummary",
21137
21176
  canGet: true,
21138
- listFlags: []
21177
+ listFlags: [],
21178
+ write: {
21179
+ scope: "habits:write",
21180
+ createSchema: createHabitInputSchema,
21181
+ createInput: "CreateHabitInput",
21182
+ updateSchema: updateHabitInputSchema,
21183
+ updateInput: "UpdateHabitInput",
21184
+ canDelete: true
21185
+ }
21139
21186
  },
21140
21187
  {
21141
- name: "occurrences",
21142
- path: "/occurrences",
21143
- readScope: "occurrences:read",
21144
- returns: "Occurrence",
21145
- canGet: false,
21146
- listFlags: [
21147
- {
21148
- name: "template-id",
21149
- description: "ULID of the recurring template (required)",
21150
- required: true,
21151
- query: "templateId"
21152
- }
21153
- ]
21188
+ name: "projects",
21189
+ path: "/projects",
21190
+ readScope: "projects:read",
21191
+ returns: "ProjectView",
21192
+ canGet: true,
21193
+ listFlags: [],
21194
+ write: {
21195
+ scope: "projects:write",
21196
+ createSchema: createProjectInputSchema,
21197
+ createInput: "CreateProjectInput",
21198
+ updateSchema: updateProjectInputSchema,
21199
+ updateInput: "UpdateProjectInput",
21200
+ canDelete: true
21201
+ }
21202
+ },
21203
+ {
21204
+ name: "routines",
21205
+ path: "/routines",
21206
+ readScope: "routines:read",
21207
+ returns: "RoutineView",
21208
+ canGet: true,
21209
+ listFlags: [],
21210
+ write: {
21211
+ scope: "routines:write",
21212
+ createSchema: createRoutineInputSchema,
21213
+ createInput: "CreateRoutineInput",
21214
+ updateSchema: updateRoutineInputSchema,
21215
+ updateInput: "UpdateRoutineInput",
21216
+ canDelete: true
21217
+ }
21218
+ },
21219
+ {
21220
+ name: "notes",
21221
+ path: "/notes",
21222
+ readScope: "notes:read",
21223
+ returns: "Note",
21224
+ canGet: true,
21225
+ listFlags: [],
21226
+ write: {
21227
+ scope: "notes:write",
21228
+ createSchema: createNoteInputSchema,
21229
+ createInput: "CreateNoteInput",
21230
+ updateSchema: updateNoteInputSchema,
21231
+ updateInput: "UpdateNoteInput",
21232
+ canDelete: true
21233
+ }
21234
+ },
21235
+ {
21236
+ name: "contacts",
21237
+ path: "/contacts",
21238
+ readScope: "contacts:read",
21239
+ returns: "Contact",
21240
+ canGet: true,
21241
+ listFlags: [],
21242
+ write: {
21243
+ scope: "contacts:write",
21244
+ createSchema: createContactInputSchema,
21245
+ createInput: "CreateContactInput",
21246
+ updateSchema: updateContactInputSchema,
21247
+ updateInput: "UpdateContactInput",
21248
+ canDelete: true
21249
+ }
21154
21250
  },
21155
21251
  {
21156
21252
  name: "shopping-lists",
@@ -21158,7 +21254,15 @@ var ENTITIES = [
21158
21254
  readScope: "shopping:read",
21159
21255
  returns: "ShoppingList",
21160
21256
  canGet: true,
21161
- listFlags: []
21257
+ listFlags: [],
21258
+ write: {
21259
+ scope: "shopping:write",
21260
+ createSchema: createShoppingListInputSchema,
21261
+ createInput: "CreateShoppingListInput",
21262
+ updateSchema: updateShoppingListInputSchema,
21263
+ updateInput: "UpdateShoppingListInput",
21264
+ canDelete: true
21265
+ }
21162
21266
  },
21163
21267
  {
21164
21268
  name: "inventory-items",
@@ -21166,7 +21270,15 @@ var ENTITIES = [
21166
21270
  readScope: "inventory:read",
21167
21271
  returns: "InventoryItem",
21168
21272
  canGet: true,
21169
- listFlags: []
21273
+ listFlags: [],
21274
+ write: {
21275
+ scope: "inventory:write",
21276
+ createSchema: createInventoryItemInputSchema,
21277
+ createInput: "CreateInventoryItemInput",
21278
+ updateSchema: updateInventoryItemInputSchema,
21279
+ updateInput: "UpdateInventoryItemInput",
21280
+ canDelete: true
21281
+ }
21170
21282
  },
21171
21283
  {
21172
21284
  name: "recipes",
@@ -21174,7 +21286,15 @@ var ENTITIES = [
21174
21286
  readScope: "meals:read",
21175
21287
  returns: "Recipe",
21176
21288
  canGet: true,
21177
- listFlags: []
21289
+ listFlags: [],
21290
+ write: {
21291
+ scope: "meals:write",
21292
+ createSchema: createRecipeInputSchema,
21293
+ createInput: "CreateRecipeInput",
21294
+ updateSchema: updateRecipeInputSchema,
21295
+ updateInput: "UpdateRecipeInput",
21296
+ canDelete: true
21297
+ }
21178
21298
  },
21179
21299
  {
21180
21300
  name: "meal-plans",
@@ -21182,23 +21302,30 @@ var ENTITIES = [
21182
21302
  readScope: "meals:read",
21183
21303
  returns: "MealPlan",
21184
21304
  canGet: true,
21185
- listFlags: []
21186
- },
21187
- {
21188
- name: "notes",
21189
- path: "/notes",
21190
- readScope: "notes:read",
21191
- returns: "Note",
21192
- canGet: true,
21193
- listFlags: []
21305
+ listFlags: [],
21306
+ write: {
21307
+ scope: "meals:write",
21308
+ createSchema: createMealPlanInputSchema,
21309
+ createInput: "CreateMealPlanInput",
21310
+ updateSchema: updateMealPlanInputSchema,
21311
+ updateInput: "UpdateMealPlanInput",
21312
+ canDelete: true
21313
+ }
21194
21314
  },
21195
21315
  {
21196
- name: "contacts",
21197
- path: "/contacts",
21198
- readScope: "contacts:read",
21199
- returns: "Contact",
21200
- canGet: true,
21201
- listFlags: []
21316
+ name: "occurrences",
21317
+ path: "/occurrences",
21318
+ readScope: "occurrences:read",
21319
+ returns: "Occurrence",
21320
+ canGet: false,
21321
+ listFlags: [
21322
+ {
21323
+ name: "template-id",
21324
+ description: "ULID of the recurring template (required)",
21325
+ required: true,
21326
+ query: "templateId"
21327
+ }
21328
+ ]
21202
21329
  },
21203
21330
  {
21204
21331
  name: "webhooks",
@@ -21210,6 +21337,18 @@ var ENTITIES = [
21210
21337
  }
21211
21338
  ];
21212
21339
  var entityByName = (name) => ENTITIES.find((e) => e.name === name);
21340
+ function entityOperations(e) {
21341
+ const ops = ["list"];
21342
+ if (e.canGet)
21343
+ ops.push("get");
21344
+ if (e.write?.createSchema)
21345
+ ops.push("create");
21346
+ if (e.write?.updateSchema)
21347
+ ops.push("update");
21348
+ if (e.write?.canDelete)
21349
+ ops.push("delete");
21350
+ return ops;
21351
+ }
21213
21352
 
21214
21353
  // src/registry.ts
21215
21354
  var GLOBAL_FLAGS = [
@@ -21227,7 +21366,17 @@ var GLOBAL_FLAGS = [
21227
21366
  description: "API origin; overrides DO_API_URL and the config file."
21228
21367
  },
21229
21368
  { name: "help", type: "boolean", description: "Show help for the command." },
21230
- { name: "version", type: "boolean", description: "Print the CLI version." }
21369
+ { name: "version", type: "boolean", description: "Print the CLI version." },
21370
+ {
21371
+ name: "data",
21372
+ type: "string",
21373
+ description: `JSON object body for create/update (e.g. --data '{"title":"Buy milk"}').`
21374
+ },
21375
+ {
21376
+ name: "allow-destructive",
21377
+ type: "boolean",
21378
+ description: "Permit a destructive verb (delete/revoke/remove-member). Off by default \u2014 a guardrail for autonomous agents."
21379
+ }
21231
21380
  ];
21232
21381
  var COMMAND_SPECS = [
21233
21382
  {
@@ -21288,6 +21437,199 @@ var COMMAND_SPECS = [
21288
21437
  scopes: ["habits:write"],
21289
21438
  returns: "HabitSummary"
21290
21439
  },
21440
+ {
21441
+ path: ["settings", "get"],
21442
+ summary: "Show the caller's settings for the Space.",
21443
+ args: [],
21444
+ flags: [],
21445
+ scopes: ["settings:read"],
21446
+ returns: "UserSettings",
21447
+ notes: "Needs a key minted while logged in (it carries the user identity)."
21448
+ },
21449
+ {
21450
+ path: ["settings", "update"],
21451
+ summary: "Patch the caller's settings.",
21452
+ args: [],
21453
+ flags: [{ name: "data", type: "string", description: "JSON of fields to change." }],
21454
+ scopes: ["settings:write"],
21455
+ returns: "UserSettings"
21456
+ },
21457
+ {
21458
+ path: ["invite", "create"],
21459
+ summary: "Create a Space invite (email/link/guest).",
21460
+ args: [],
21461
+ flags: [
21462
+ {
21463
+ name: "data",
21464
+ type: "string",
21465
+ description: 'JSON CreateInviteInput, e.g. {"type":"link"} or {"type":"email","email":"a@b.c"}.'
21466
+ }
21467
+ ],
21468
+ scopes: ["members:write"],
21469
+ returns: "Invite"
21470
+ },
21471
+ {
21472
+ path: ["invite", "list"],
21473
+ summary: "List the Space's invites.",
21474
+ args: [],
21475
+ flags: [],
21476
+ scopes: ["members:read"],
21477
+ returns: "Invite"
21478
+ },
21479
+ {
21480
+ path: ["invite", "revoke"],
21481
+ summary: "Revoke an invite (destructive).",
21482
+ args: [{ name: "id", required: true, description: "Invite ULID." }],
21483
+ flags: [
21484
+ { name: "allow-destructive", type: "boolean", description: "Required to actually revoke." }
21485
+ ],
21486
+ scopes: ["members:write"],
21487
+ returns: "None",
21488
+ notes: "Destructive \u2014 gated behind --allow-destructive + a members:write key."
21489
+ },
21490
+ {
21491
+ path: ["member", "list"],
21492
+ summary: "List the Space's members.",
21493
+ args: [],
21494
+ flags: [],
21495
+ scopes: ["members:read"],
21496
+ returns: "MemberView"
21497
+ },
21498
+ {
21499
+ path: ["member", "role"],
21500
+ summary: "Set a member's role.",
21501
+ args: [{ name: "id", required: true, description: "Member ULID (tenant_members.id)." }],
21502
+ flags: [
21503
+ { name: "role", type: "string", description: "Base role: owner|admin|member|guest." },
21504
+ { name: "data", type: "string", description: "JSON SetMemberRoleInput (overrides --role)." }
21505
+ ],
21506
+ scopes: ["members:write"],
21507
+ returns: "MemberView"
21508
+ },
21509
+ {
21510
+ path: ["member", "remove"],
21511
+ summary: "Remove a member from the Space (destructive).",
21512
+ args: [{ name: "id", required: true, description: "Member ULID." }],
21513
+ flags: [
21514
+ { name: "allow-destructive", type: "boolean", description: "Required to actually remove." }
21515
+ ],
21516
+ scopes: ["members:write"],
21517
+ returns: "None",
21518
+ notes: "Destructive \u2014 gated behind --allow-destructive + a members:write key."
21519
+ },
21520
+ {
21521
+ path: ["share"],
21522
+ summary: "Share a node with a member or role.",
21523
+ args: [{ name: "node", required: true, description: "Node ULID to share." }],
21524
+ flags: [
21525
+ { name: "to", type: "string", description: "Subject ULID (member id, or role id)." },
21526
+ {
21527
+ name: "cap",
21528
+ type: "string",
21529
+ description: "Capability: view|comment|edit|admin.",
21530
+ default: "view"
21531
+ },
21532
+ {
21533
+ name: "subject-type",
21534
+ type: "string",
21535
+ description: "member (default) or role.",
21536
+ default: "member"
21537
+ }
21538
+ ],
21539
+ scopes: ["sharing:write"],
21540
+ returns: "NodeGrantView"
21541
+ },
21542
+ {
21543
+ path: ["share", "list"],
21544
+ summary: "List the grants on a node.",
21545
+ args: [{ name: "node", required: true, description: "Node ULID." }],
21546
+ flags: [],
21547
+ scopes: ["sharing:read"],
21548
+ returns: "NodeGrantView"
21549
+ },
21550
+ {
21551
+ path: ["share", "revoke"],
21552
+ summary: "Revoke a node grant (destructive).",
21553
+ args: [
21554
+ { name: "node", required: true, description: "Node ULID." },
21555
+ { name: "grant", required: true, description: "Grant ULID." }
21556
+ ],
21557
+ flags: [
21558
+ { name: "allow-destructive", type: "boolean", description: "Required to actually revoke." }
21559
+ ],
21560
+ scopes: ["sharing:write"],
21561
+ returns: "None",
21562
+ notes: "Destructive \u2014 gated behind --allow-destructive + a sharing:write key."
21563
+ },
21564
+ {
21565
+ path: ["assign"],
21566
+ summary: "Assign/delegate a node to a member.",
21567
+ args: [{ name: "node", required: true, description: "Node ULID to assign." }],
21568
+ flags: [
21569
+ { name: "to", type: "string", description: "Member ULID to assign to." },
21570
+ { name: "cap", type: "string", description: "Capability (default edit).", default: "edit" }
21571
+ ],
21572
+ scopes: ["sharing:write"],
21573
+ returns: "NodeGrantView"
21574
+ },
21575
+ {
21576
+ path: ["comment"],
21577
+ summary: "Post a comment on a node.",
21578
+ args: [
21579
+ { name: "node", required: true, description: "Node ULID." },
21580
+ { name: "body", required: true, description: "Comment text." }
21581
+ ],
21582
+ flags: [],
21583
+ scopes: ["comments:write"],
21584
+ returns: "CommentView"
21585
+ },
21586
+ {
21587
+ path: ["comment", "list"],
21588
+ summary: "List a node's comment thread.",
21589
+ args: [{ name: "node", required: true, description: "Node ULID." }],
21590
+ flags: [],
21591
+ scopes: ["comments:read"],
21592
+ returns: "CommentView"
21593
+ },
21594
+ {
21595
+ path: ["role", "list"],
21596
+ summary: "List the Space's custom roles.",
21597
+ args: [],
21598
+ flags: [],
21599
+ scopes: ["roles:read"],
21600
+ returns: "RoleView"
21601
+ },
21602
+ {
21603
+ path: ["link", "create"],
21604
+ summary: "Mint a public share-link to a node.",
21605
+ args: [{ name: "node", required: true, description: "Node ULID to link." }],
21606
+ flags: [
21607
+ { name: "cap", type: "string", description: "Capability (default view).", default: "view" },
21608
+ { name: "expires", type: "string", description: "Expiry (ISO date/time)." }
21609
+ ],
21610
+ scopes: ["links:write"],
21611
+ returns: "CreateShareLinkResult",
21612
+ notes: "The signed token is returned once, in the create response."
21613
+ },
21614
+ {
21615
+ path: ["link", "list"],
21616
+ summary: "List the share-links for a node.",
21617
+ args: [{ name: "node", required: true, description: "Node ULID." }],
21618
+ flags: [],
21619
+ scopes: ["links:read"],
21620
+ returns: "ShareLinkPublic"
21621
+ },
21622
+ {
21623
+ path: ["link", "revoke"],
21624
+ summary: "Revoke a share-link (destructive).",
21625
+ args: [{ name: "id", required: true, description: "Share-link ULID." }],
21626
+ flags: [
21627
+ { name: "allow-destructive", type: "boolean", description: "Required to actually revoke." }
21628
+ ],
21629
+ scopes: ["links:write"],
21630
+ returns: "ShareLinkPublic",
21631
+ notes: "Destructive \u2014 gated behind --allow-destructive + a links:write key."
21632
+ },
21291
21633
  {
21292
21634
  path: ["whoami"],
21293
21635
  summary: "Show the tenant and scopes the configured key resolves to.",
@@ -21402,9 +21744,16 @@ function buildManifest() {
21402
21744
  })),
21403
21745
  entities: ENTITIES.map((e) => ({
21404
21746
  name: e.name,
21405
- operations: e.canGet ? ["list", "get"] : ["list"],
21747
+ operations: entityOperations(e),
21406
21748
  path: `${API_PREFIX}${e.path}`,
21407
21749
  readScope: e.readScope,
21750
+ ...e.write ? { writeScope: e.write.scope } : {},
21751
+ ...e.write?.createInput || e.write?.updateInput ? {
21752
+ inputs: {
21753
+ ...e.write.createInput ? { create: e.write.createInput } : {},
21754
+ ...e.write.updateInput ? { update: e.write.updateInput } : {}
21755
+ }
21756
+ } : {},
21408
21757
  returns: e.returns,
21409
21758
  listFlags: e.listFlags.map((f) => ({
21410
21759
  name: f.name,
@@ -21691,30 +22040,57 @@ async function dispatch(positionals, flags, io, mode, config2) {
21691
22040
  }
21692
22041
  const entity = entityByName(positionals[0] ?? "");
21693
22042
  if (entity) {
21694
- const op = positionals[1];
21695
- const rest = positionals.slice(2);
21696
- const client = requireClient();
21697
- if (op === "list") {
21698
- const query = {};
21699
- for (const f of entity.listFlags) {
21700
- const value = flagString(flags, f.name);
21701
- if (value !== undefined)
21702
- query[f.query ?? f.name] = value;
21703
- else if (f.required)
21704
- throw usageError(`\`${entity.name} list\` requires --${f.name}`);
21705
- }
21706
- return { data: await client.get(entity.path, { query }), render: renderList };
21707
- }
21708
- if (op === "get") {
21709
- if (!entity.canGet)
21710
- throw usageError(`\`${entity.name}\` has no get-by-id; use \`${entity.name} list\``);
21711
- const id = requireArg(rest, 0, "id");
21712
- return { data: await client.get(`${entity.path}/${id}`), render: renderRecord };
21713
- }
21714
- throw usageError(`unknown operation for ${entity.name}: \`${op ?? ""}\` (use list or get)`);
22043
+ return dispatchEntity(entity, positionals.slice(1), flags, requireClient);
21715
22044
  }
21716
22045
  throw usageError(`unknown command: \`${positionals.join(" ")}\` \u2014 run \`do help\``);
21717
22046
  }
22047
+ async function dispatchEntity(entity, rest, flags, requireClient) {
22048
+ const op = rest[0];
22049
+ const args = rest.slice(1);
22050
+ const client = requireClient();
22051
+ if (op === "list") {
22052
+ const query = {};
22053
+ for (const f of entity.listFlags) {
22054
+ const value = flagString(flags, f.name);
22055
+ if (value !== undefined)
22056
+ query[f.query ?? f.name] = value;
22057
+ else if (f.required)
22058
+ throw usageError(`\`${entity.name} list\` requires --${f.name}`);
22059
+ }
22060
+ return { data: await client.get(entity.path, { query }), render: renderList };
22061
+ }
22062
+ if (op === "get") {
22063
+ if (!entity.canGet)
22064
+ throw usageError(`\`${entity.name}\` has no get-by-id; use \`${entity.name} list\``);
22065
+ const id = requireArg(args, 0, "id");
22066
+ return { data: await client.get(`${entity.path}/${id}`), render: renderRecord };
22067
+ }
22068
+ if (op === "create") {
22069
+ const write = requireWrite(entity, "create");
22070
+ if (!write.createSchema)
22071
+ throw usageError(`\`${entity.name}\` cannot be created`);
22072
+ const body = parseInput(write.createSchema, { id: ulid3(), ...readDataFlag(flags) });
22073
+ return { data: await client.post(entity.path, body), render: renderRecord };
22074
+ }
22075
+ if (op === "update") {
22076
+ const write = requireWrite(entity, "update");
22077
+ if (!write.updateSchema)
22078
+ throw usageError(`\`${entity.name}\` cannot be updated`);
22079
+ const id = requireArg(args, 0, "id");
22080
+ const body = parseInput(write.updateSchema, readDataFlag(flags));
22081
+ return { data: await client.patch(`${entity.path}/${id}`, body), render: renderRecord };
22082
+ }
22083
+ if (op === "delete") {
22084
+ const write = requireWrite(entity, "delete");
22085
+ if (!write.canDelete)
22086
+ throw usageError(`\`${entity.name}\` cannot be deleted`);
22087
+ const id = requireArg(args, 0, "id");
22088
+ assertDestructiveAllowed(flags, `delete ${entity.name} ${id}`);
22089
+ await client.delete(`${entity.path}/${id}`);
22090
+ return { data: { ok: true, deleted: id }, render: renderRecord };
22091
+ }
22092
+ throw usageError(`unknown operation for ${entity.name}: \`${op ?? ""}\` (use ${entityOperations(entity).join(", ")})`);
22093
+ }
21718
22094
  function runFixed(spec, ctx) {
21719
22095
  const key = spec.path.join(" ");
21720
22096
  const handler = HANDLERS[key];
@@ -21775,6 +22151,121 @@ var HANDLERS = {
21775
22151
  render: renderHabit
21776
22152
  };
21777
22153
  },
22154
+ "settings get": async ({ requireClient }) => ({
22155
+ data: await requireClient().get("/settings"),
22156
+ render: renderRecord
22157
+ }),
22158
+ "settings update": async ({ flags, requireClient }) => {
22159
+ const body = parseInput(updateUserSettingsInputSchema, readDataFlag(flags));
22160
+ return { data: await requireClient().patch("/settings", body), render: renderRecord };
22161
+ },
22162
+ "invite create": async ({ flags, requireClient }) => {
22163
+ const body = parseInput(createInviteInputSchema, { id: ulid3(), ...readDataFlag(flags) });
22164
+ return { data: await requireClient().post("/invites", body), render: renderRecord };
22165
+ },
22166
+ "invite list": async ({ requireClient }) => ({
22167
+ data: await requireClient().get("/invites"),
22168
+ render: renderList
22169
+ }),
22170
+ "invite revoke": async ({ args, flags, requireClient }) => {
22171
+ const id = requireArg(args, 0, "invite id");
22172
+ assertDestructiveAllowed(flags, `revoke invite ${id}`);
22173
+ await requireClient().delete(`/invites/${id}`);
22174
+ return { data: { ok: true, revoked: id }, render: renderRecord };
22175
+ },
22176
+ "member list": async ({ requireClient }) => ({
22177
+ data: await requireClient().get("/members"),
22178
+ render: renderList
22179
+ }),
22180
+ "member role": async ({ args, flags, requireClient }) => {
22181
+ const id = requireArg(args, 0, "member id");
22182
+ const role = flagString(flags, "role");
22183
+ const raw = flagString(flags, "data") ? readDataFlag(flags) : role ? { role } : {};
22184
+ const body = parseInput(setMemberRoleInputSchema, raw);
22185
+ return { data: await requireClient().patch(`/members/${id}/role`, body), render: renderRecord };
22186
+ },
22187
+ "member remove": async ({ args, flags, requireClient }) => {
22188
+ const id = requireArg(args, 0, "member id");
22189
+ assertDestructiveAllowed(flags, `remove member ${id}`);
22190
+ await requireClient().delete(`/members/${id}`);
22191
+ return { data: { ok: true, removed: id }, render: renderRecord };
22192
+ },
22193
+ share: async ({ args, flags, requireClient }) => {
22194
+ const node = requireArg(args, 0, "node id");
22195
+ const subjectId = requireFlag(flags, "to", "share needs --to <member-or-role-id>");
22196
+ const body = parseInput(createNodeGrantInputSchema, {
22197
+ id: ulid3(),
22198
+ subjectType: flagString(flags, "subject-type") ?? "member",
22199
+ subjectId,
22200
+ capability: flagString(flags, "cap") ?? "view"
22201
+ });
22202
+ return {
22203
+ data: await requireClient().post(`/sharing/nodes/${node}/grants`, body),
22204
+ render: renderRecord
22205
+ };
22206
+ },
22207
+ "share list": async ({ args, requireClient }) => {
22208
+ const node = requireArg(args, 0, "node id");
22209
+ return { data: await requireClient().get(`/sharing/nodes/${node}/grants`), render: renderList };
22210
+ },
22211
+ "share revoke": async ({ args, flags, requireClient }) => {
22212
+ const node = requireArg(args, 0, "node id");
22213
+ const grant = requireArg(args, 1, "grant id");
22214
+ assertDestructiveAllowed(flags, `revoke grant ${grant}`);
22215
+ await requireClient().delete(`/sharing/nodes/${node}/grants/${grant}`);
22216
+ return { data: { ok: true, revoked: grant }, render: renderRecord };
22217
+ },
22218
+ assign: async ({ args, flags, requireClient }) => {
22219
+ const node = requireArg(args, 0, "node id");
22220
+ const memberId = requireFlag(flags, "to", "assign needs --to <member-id>");
22221
+ const body = parseInput(assignNodeInputSchema, {
22222
+ id: ulid3(),
22223
+ memberId,
22224
+ ...flagString(flags, "cap") ? { capability: flagString(flags, "cap") } : {}
22225
+ });
22226
+ return {
22227
+ data: await requireClient().put(`/sharing/nodes/${node}/assignee`, body),
22228
+ render: renderRecord
22229
+ };
22230
+ },
22231
+ comment: async ({ args, requireClient }) => {
22232
+ const node = requireArg(args, 0, "node id");
22233
+ const body = args.slice(1).join(" ").trim();
22234
+ if (body.length === 0)
22235
+ throw usageError('comment needs text, e.g. `do comment <node> "Nice!"`');
22236
+ const input = parseInput(createCommentInputSchema, { id: ulid3(), nodeId: node, body });
22237
+ return { data: await requireClient().post("/comments", input), render: renderRecord };
22238
+ },
22239
+ "comment list": async ({ args, requireClient }) => {
22240
+ const node = requireArg(args, 0, "node id");
22241
+ return {
22242
+ data: await requireClient().get("/comments", { query: { nodeId: node } }),
22243
+ render: renderList
22244
+ };
22245
+ },
22246
+ "role list": async ({ requireClient }) => ({
22247
+ data: await requireClient().get("/roles"),
22248
+ render: renderList
22249
+ }),
22250
+ "link create": async ({ args, flags, requireClient }) => {
22251
+ const node = requireArg(args, 0, "node id");
22252
+ const body = parseInput(createShareLinkInputSchema, {
22253
+ id: ulid3(),
22254
+ nodeId: node,
22255
+ ...flagString(flags, "cap") ? { capability: flagString(flags, "cap") } : {},
22256
+ ...flagString(flags, "expires") ? { expiresAt: flagString(flags, "expires") } : {}
22257
+ });
22258
+ return { data: await requireClient().post("/share-links", body), render: renderRecord };
22259
+ },
22260
+ "link list": async ({ args, requireClient }) => {
22261
+ const node = requireArg(args, 0, "node id");
22262
+ return { data: await requireClient().get(`/share-links/node/${node}`), render: renderList };
22263
+ },
22264
+ "link revoke": async ({ args, flags, requireClient }) => {
22265
+ const id = requireArg(args, 0, "share-link id");
22266
+ assertDestructiveAllowed(flags, `revoke share-link ${id}`);
22267
+ return { data: await requireClient().delete(`/share-links/${id}`), render: renderRecord };
22268
+ },
21778
22269
  whoami: async ({ requireClient }) => ({
21779
22270
  data: await requireClient().get("/whoami"),
21780
22271
  render: renderWhoami
@@ -21849,6 +22340,12 @@ function requireArg(args, index2, name) {
21849
22340
  }
21850
22341
  return value;
21851
22342
  }
22343
+ function requireFlag(flags, name, hint) {
22344
+ const value = flagString(flags, name);
22345
+ if (value === undefined || value.length === 0)
22346
+ throw usageError(hint);
22347
+ return value;
22348
+ }
21852
22349
  function assertConfigKey(key) {
21853
22350
  if (!CONFIG_KEYS.includes(key)) {
21854
22351
  throw usageError(`unknown config key \`${key}\` \u2014 valid keys: ${CONFIG_KEYS.join(", ")}`);
@@ -21882,6 +22379,32 @@ function redact(secret) {
21882
22379
  return "********";
21883
22380
  return `${secret.slice(0, 6)}\u2026${secret.slice(-2)}`;
21884
22381
  }
22382
+ function requireWrite(entity, op) {
22383
+ if (!entity.write) {
22384
+ throw usageError(`\`${entity.name}\` is read-only \u2014 \`${op}\` is not available`);
22385
+ }
22386
+ return entity.write;
22387
+ }
22388
+ function readDataFlag(flags) {
22389
+ const raw = flagString(flags, "data");
22390
+ if (raw === undefined)
22391
+ return {};
22392
+ let parsed;
22393
+ try {
22394
+ parsed = JSON.parse(raw);
22395
+ } catch (err) {
22396
+ throw usageError(`--data is not valid JSON: ${err.message}`);
22397
+ }
22398
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
22399
+ throw usageError("--data must be a JSON object");
22400
+ }
22401
+ return parsed;
22402
+ }
22403
+ function assertDestructiveAllowed(flags, action) {
22404
+ if (!flagBool(flags, "allow-destructive")) {
22405
+ throw new CliError("destructive_blocked", `refusing to ${action} without --allow-destructive (a guardrail for autonomous agents)`, EXIT.USAGE);
22406
+ }
22407
+ }
21885
22408
 
21886
22409
  // src/run.ts
21887
22410
  async function run(argv, io) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbeverhelst/do",
3
- "version": "0.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "do — the agent-drivable CLI over the public /api/v1 surface",
6
6
  "type": "module",