@amerged/ohmyhost-cli 0.1.43 → 0.1.45

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/dist/index.js CHANGED
@@ -100,7 +100,7 @@ import {
100
100
  selectWorkspace,
101
101
  verifiedUserId,
102
102
  workspaceOf
103
- } from "./chunk-5TO5I2HF.js";
103
+ } from "./chunk-C6KEAOJP.js";
104
104
  import {
105
105
  CredentialStoreIntegrityError,
106
106
  CredentialStoreUnavailableError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/product-cli",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "private": true,
5
5
  "ohmyhost": {
6
6
  "deployment": "production",
@@ -419,8 +419,14 @@ const HELP_USAGE = Object.freeze({
419
419
  "ohmyhost database access list --project ULID [--environment dev|prod] --json",
420
420
  "database access revoke":
421
421
  "ohmyhost database access revoke --project ULID --access ULID --yes --json",
422
+ "database watch create":
423
+ "ohmyhost database watch create --project ULID --environment dev|prod --table NAME [--schema public] --columns COL[,COL] [--operations insert,update,delete] --idempotency-key KEY --yes --json (publishes only the listed columns, at most 32, of each row change as the event type data.<topic> printed in this answer; the table must exist as an ordinary table of the application; a sleeping database wakes; next, subscribe with 'ohmyhost database webhook create ... --event-types data.<topic>')",
424
+ "database watch list":
425
+ "ohmyhost database watch list --project ULID [--environment dev|prod] --json (dev when omitted; a sleeping database wakes)",
426
+ "database watch delete":
427
+ "ohmyhost database watch delete --project ULID --environment dev|prod --watch ID --idempotency-key KEY --yes --json",
422
428
  "database webhook create":
423
- "ohmyhost database webhook create --project ULID --environment dev|prod --url HTTPS_URL --event-types TYPE[,TYPE] [--headers-file PATH] --idempotency-key KEY --yes --json (the signing secret is shown once in this answer; types: database.created, database.ready, database.woke, database.slept, database.suspended, database.resized, database.deleted; the headers file is a UTF-8 JSON object of at most 8 KiB)",
429
+ "ohmyhost database webhook create --project ULID --environment dev|prod --url HTTPS_URL --event-types TYPE[,TYPE] [--headers-file PATH] --idempotency-key KEY --yes --json (the signing secret is shown once in this answer; types: database.created, database.ready, database.woke, database.slept, database.suspended, database.resized, database.deleted, data.<topic> for the rows of one watched table printed by database watch create, or data.* for every watched table; a bare * is not accepted and does not include data events; the headers file is a UTF-8 JSON object of at most 8 KiB)",
424
430
  "database webhook list":
425
431
  "ohmyhost database webhook list --project ULID [--environment dev|prod] --json (dev when omitted; the secret is never listed)",
426
432
  "database webhook get":
@@ -434,7 +440,7 @@ const HELP_USAGE = Object.freeze({
434
440
  "database webhook replay":
435
441
  "ohmyhost database webhook replay --project ULID --environment dev|prod --delivery ID --yes --json",
436
442
  "database schedule set":
437
- "ohmyhost database schedule set --project ULID --environment dev|prod --name NAME (--cron EXPR | --every-seconds N) --statements-file PATH [--timeout-ms N] [--enabled true|false] --yes --json (runs the statements in the file, a UTF-8 JSON array of 1 to 20 SQL strings, as the runtime role of the application; cron has five fields in UTC; every-seconds is 60 to 2678400; timeout-ms is 1000 to 30000, default 10000)",
443
+ "ohmyhost database schedule set --project ULID --environment dev|prod --name NAME (--cron EXPR | --every-seconds N) --statements-file PATH [--timeout-ms N] [--enabled true|false] --yes --json (runs the statements in the file, a UTF-8 JSON array of 1 to 20 SQL strings, as the runtime role of the application; cron has five fields in UTC; every-seconds is 60 to 2678400; timeout-ms is 1000 to 30000, default 10000). A project whose Stop budget is reached has its schedules disabled automatically, within about an hour of the billed usage reaching the budget: the credit service does this after its hourly pgcf pass, and Continue budgets are never touched. While the budget is reached, schedule set with enabled true and schedule run are refused with project_budget_exceeded (HTTP 402). Re-enabling is manual: raise the budget (or wait for the next UTC month) and run database schedule set --enabled true again with the schedule body (read it with schedule get).",
438
444
  "database schedule list":
439
445
  "ohmyhost database schedule list --project ULID [--environment dev|prod] --json (dev when omitted)",
440
446
  "database schedule get":
@@ -444,9 +450,9 @@ const HELP_USAGE = Object.freeze({
444
450
  "database schedule runs":
445
451
  "ohmyhost database schedule runs --project ULID [--environment dev|prod] --name NAME [--limit 1..50] --json",
446
452
  "database schedule run":
447
- "ohmyhost database schedule run --project ULID --environment dev|prod --name NAME --yes --json (queues one run now, in addition to the timer)",
453
+ "ohmyhost database schedule run --project ULID --environment dev|prod --name NAME --yes --json (queues one run now, in addition to the timer; refused with project_budget_exceeded (HTTP 402) while the project's Stop budget is reached)",
448
454
  "database executions":
449
- "ohmyhost database executions --project ULID [--environment dev|prod] --from ISO --to ISO --granularity hour|day [--limit 1..100] [--cursor CURSOR] --json (UTC timestamps like 2026-10-30T00:00:00.000Z aligned to the granularity, at most 31 days; counts schedule runs, webhook attempts and events per hour or day; usage counters only, nothing is charged for them until the operator prices them)",
455
+ "ohmyhost database executions --project ULID [--environment dev|prod] --from ISO --to ISO --granularity hour|day [--limit 1..100] [--cursor CURSOR] --json (UTC timestamps like 2026-10-30T00:00:00.000Z aligned to the granularity, at most 31 days; counts schedule runs, webhook attempts and events per hour or day; these are counters only, executions have no price of their own and the awake compute time of the database is what is billed)",
450
456
  "database psql":
451
457
  "ohmyhost database psql --project ULID --environment dev|prod [--mode read|write] [--ttl 5m|1h|24h|SECONDS] [--json] (starts local psql with a temporary credential and revokes it on exit)",
452
458
  link: "ohmyhost link --project ULID --repository-owner OWNER --repository-name REPOSITORY --idempotency-key KEY --json (uses the workspace GitHub connection and waits for the source-link operation)",
@@ -1191,6 +1197,12 @@ export class ProductCli {
1191
1197
  return await this.listDatabaseAccess(command, signal);
1192
1198
  if (command.kind === "database-access-revoke")
1193
1199
  return await this.revokeDatabaseAccess(command, signal);
1200
+ if (command.kind === "database-watch-create")
1201
+ return await this.createDatabaseWatch(command, signal);
1202
+ if (command.kind === "database-watch-list")
1203
+ return await this.listDatabaseWatches(command, signal);
1204
+ if (command.kind === "database-watch-delete")
1205
+ return await this.deleteDatabaseWatch(command, signal);
1194
1206
  if (command.kind === "database-webhook-create")
1195
1207
  return await this.createDatabaseWebhook(command, signal);
1196
1208
  if (command.kind === "database-webhook-list")
@@ -1952,6 +1964,51 @@ export class ProductCli {
1952
1964
  return success("database access revoke", { status: "succeeded", access });
1953
1965
  }
1954
1966
 
1967
+ /** The answer names the event type to subscribe to; the watch already carries the columns and the topic. */
1968
+ private async createDatabaseWatch(
1969
+ command: Extract<ProductCliCommand, { kind: "database-watch-create" }>,
1970
+ signal: AbortSignal,
1971
+ ): Promise<CliResult> {
1972
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1973
+ const watch = await api.createProjectDatabaseWatch({
1974
+ projectId: command.projectId,
1975
+ environment: command.environment,
1976
+ idempotencyKey: command.idempotencyKey,
1977
+ request: command.request,
1978
+ });
1979
+ return success("database watch create", {
1980
+ status: "succeeded",
1981
+ watch,
1982
+ next_action: `Run '${this.dependencies.commandPrefix} database webhook create --project ${command.projectId} --environment ${command.environment} --url HTTPS_URL --event-types ${watch.event_type} --idempotency-key KEY --yes --json' to receive ${watch.event_type} events at an HTTPS receiver.`,
1983
+ });
1984
+ }
1985
+
1986
+ private async listDatabaseWatches(
1987
+ command: Extract<ProductCliCommand, { kind: "database-watch-list" }>,
1988
+ signal: AbortSignal,
1989
+ ): Promise<CliResult> {
1990
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1991
+ const page = await api.listProjectDatabaseWatches({
1992
+ projectId: command.projectId,
1993
+ environment: command.environment,
1994
+ });
1995
+ return success("database watch list", { status: "succeeded", watch: page.items });
1996
+ }
1997
+
1998
+ private async deleteDatabaseWatch(
1999
+ command: Extract<ProductCliCommand, { kind: "database-watch-delete" }>,
2000
+ signal: AbortSignal,
2001
+ ): Promise<CliResult> {
2002
+ const api = await this.authenticatedApi(command.credentialStore, signal);
2003
+ const deleted = await api.deleteProjectDatabaseWatch({
2004
+ projectId: command.projectId,
2005
+ environment: command.environment,
2006
+ idempotencyKey: command.idempotencyKey,
2007
+ watchId: command.watchId,
2008
+ });
2009
+ return success("database watch delete", { status: "succeeded", deleted });
2010
+ }
2011
+
1955
2012
  /** The signing secret is returned by create only; every other answer carries no secret. */
1956
2013
  private async createDatabaseWebhook(
1957
2014
  command: Extract<ProductCliCommand, { kind: "database-webhook-create" }>,
@@ -3526,6 +3583,9 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
3526
3583
  if (command.kind === "database-access-list") return "database access list";
3527
3584
  if (command.kind === "database-access-revoke") return "database access revoke";
3528
3585
  if (command.kind === "database-psql") return "database psql";
3586
+ if (command.kind === "database-watch-create") return "database watch create";
3587
+ if (command.kind === "database-watch-list") return "database watch list";
3588
+ if (command.kind === "database-watch-delete") return "database watch delete";
3529
3589
  if (command.kind === "database-webhook-create") return "database webhook create";
3530
3590
  if (command.kind === "database-webhook-list") return "database webhook list";
3531
3591
  if (command.kind === "database-webhook-get") return "database webhook get";
@@ -6,9 +6,12 @@ import {
6
6
  parseDatabaseListLimit,
7
7
  parseDatabaseScheduleName,
8
8
  parseDatabaseUsageWindow,
9
+ parseDatabaseWatchCreateRequest,
10
+ parseDatabaseWatchId,
9
11
  parseDatabaseWebhookCreateRequest,
10
12
  parseDatabaseWebhookId,
11
- type DatabaseWebhookEventType,
13
+ type DatabaseWatchRequest,
14
+ type DatabaseWebhookEventPattern,
12
15
  } from "@ohmyhost/contracts/database-events";
13
16
  import {
14
17
  databaseSizeForMemory,
@@ -404,12 +407,36 @@ export type ProductCliCommand =
404
407
  readonly yes: true;
405
408
  readonly credentialStore: "native";
406
409
  }
410
+ | {
411
+ readonly kind: "database-watch-create";
412
+ readonly projectId: string;
413
+ readonly environment: "dev" | "prod";
414
+ readonly request: DatabaseWatchRequest;
415
+ readonly idempotencyKey: string;
416
+ readonly yes: true;
417
+ readonly credentialStore: "native";
418
+ }
419
+ | {
420
+ readonly kind: "database-watch-list";
421
+ readonly projectId: string;
422
+ readonly environment: "dev" | "prod";
423
+ readonly credentialStore: "native";
424
+ }
425
+ | {
426
+ readonly kind: "database-watch-delete";
427
+ readonly projectId: string;
428
+ readonly environment: "dev" | "prod";
429
+ readonly watchId: string;
430
+ readonly idempotencyKey: string;
431
+ readonly yes: true;
432
+ readonly credentialStore: "native";
433
+ }
407
434
  | {
408
435
  readonly kind: "database-webhook-create";
409
436
  readonly projectId: string;
410
437
  readonly environment: "dev" | "prod";
411
438
  readonly url: string;
412
- readonly eventTypes: readonly DatabaseWebhookEventType[];
439
+ readonly eventTypes: readonly DatabaseWebhookEventPattern[];
413
440
  readonly headersFile: string | null;
414
441
  readonly idempotencyKey: string;
415
442
  readonly yes: true;
@@ -1003,6 +1030,9 @@ export type RecognizedCommandLabel =
1003
1030
  | "database access create"
1004
1031
  | "database access list"
1005
1032
  | "database access revoke"
1033
+ | "database watch create"
1034
+ | "database watch list"
1035
+ | "database watch delete"
1006
1036
  | "database webhook create"
1007
1037
  | "database webhook list"
1008
1038
  | "database webhook get"
@@ -1174,6 +1204,11 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
1174
1204
  if (values[1] === "list") return "database access list";
1175
1205
  if (values[1] === "revoke") return "database access revoke";
1176
1206
  }
1207
+ if (value === "database" && values[0] === "watch") {
1208
+ if (values[1] === "create") return "database watch create";
1209
+ if (values[1] === "list") return "database watch list";
1210
+ if (values[1] === "delete") return "database watch delete";
1211
+ }
1177
1212
  if (value === "database" && values[0] === "webhook") {
1178
1213
  if (values[1] === "create") return "database webhook create";
1179
1214
  if (values[1] === "list") return "database webhook list";
@@ -2063,6 +2098,92 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
2063
2098
  }
2064
2099
  throw new InvalidCommandError();
2065
2100
  }
2101
+ if (first === "database" && values[0] === "watch") {
2102
+ values.shift();
2103
+ const subcommand = values.shift();
2104
+ if (subcommand === "create") {
2105
+ if (!yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2106
+ const options = parseOptionalOptions(values, [
2107
+ "--project",
2108
+ "--environment",
2109
+ "--table",
2110
+ "--schema",
2111
+ "--columns",
2112
+ "--operations",
2113
+ "--idempotency-key",
2114
+ ]);
2115
+ const projectId = options["--project"],
2116
+ environment = options["--environment"],
2117
+ table = options["--table"],
2118
+ schema = options["--schema"],
2119
+ columns = options["--columns"],
2120
+ operations = options["--operations"],
2121
+ idempotencyKey = options["--idempotency-key"];
2122
+ if (
2123
+ !isValue(projectId) ||
2124
+ !isValue(idempotencyKey) ||
2125
+ !isValue(table) ||
2126
+ !isValue(columns) ||
2127
+ (environment !== "dev" && environment !== "prod")
2128
+ )
2129
+ throw new InvalidCommandError();
2130
+ const request = contractValue(() =>
2131
+ parseDatabaseWatchCreateRequest({
2132
+ schema,
2133
+ table,
2134
+ columns: columns.split(","),
2135
+ ...(operations === undefined ? {} : { operations: operations.split(",") }),
2136
+ }),
2137
+ );
2138
+ return {
2139
+ kind: "database-watch-create",
2140
+ projectId,
2141
+ environment,
2142
+ request,
2143
+ idempotencyKey,
2144
+ yes: true,
2145
+ credentialStore,
2146
+ };
2147
+ }
2148
+ if (subcommand === "list") {
2149
+ if (yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2150
+ const options = parseOptionalOptions(values, ["--project", "--environment"]);
2151
+ const projectId = options["--project"],
2152
+ environment = options["--environment"] ?? "dev";
2153
+ if (!isValue(projectId) || (environment !== "dev" && environment !== "prod"))
2154
+ throw new InvalidCommandError();
2155
+ return { kind: "database-watch-list", projectId, environment, credentialStore };
2156
+ }
2157
+ if (subcommand === "delete") {
2158
+ if (!yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2159
+ const options = parseOptionalOptions(values, [
2160
+ "--project",
2161
+ "--environment",
2162
+ "--watch",
2163
+ "--idempotency-key",
2164
+ ]);
2165
+ const projectId = options["--project"],
2166
+ environment = options["--environment"],
2167
+ watchId = options["--watch"],
2168
+ idempotencyKey = options["--idempotency-key"];
2169
+ if (
2170
+ !isValue(projectId) ||
2171
+ !isValue(idempotencyKey) ||
2172
+ (environment !== "dev" && environment !== "prod")
2173
+ )
2174
+ throw new InvalidCommandError();
2175
+ return {
2176
+ kind: "database-watch-delete",
2177
+ projectId,
2178
+ environment,
2179
+ watchId: contractValue(() => parseDatabaseWatchId(watchId)),
2180
+ idempotencyKey,
2181
+ yes: true,
2182
+ credentialStore,
2183
+ };
2184
+ }
2185
+ throw new InvalidCommandError();
2186
+ }
2066
2187
  if (first === "database" && values[0] === "webhook") {
2067
2188
  values.shift();
2068
2189
  const subcommand = values.shift();
@@ -33,6 +33,9 @@ import {
33
33
  assertDatabaseWebhookDeliveryPage,
34
34
  assertDatabaseWebhookPage,
35
35
  assertDatabaseWebhookReplayAccepted,
36
+ assertDatabaseWatch,
37
+ assertDatabaseWatchDeleted,
38
+ assertDatabaseWatchPage,
36
39
  assertDatabaseWebhookTest,
37
40
  type DatabaseExecutionUsagePage,
38
41
  type DatabaseSchedule,
@@ -45,7 +48,11 @@ import {
45
48
  type DatabaseWebhookCreated,
46
49
  type DatabaseWebhookDeleted,
47
50
  type DatabaseWebhookDeliveryPage,
48
- type DatabaseWebhookEventType,
51
+ type DatabaseWatch,
52
+ type DatabaseWatchDeleted,
53
+ type DatabaseWatchPage,
54
+ type DatabaseWatchRequest,
55
+ type DatabaseWebhookEventPattern,
49
56
  type DatabaseWebhookPage,
50
57
  type DatabaseWebhookReplayAccepted,
51
58
  type DatabaseWebhookTestResult,
@@ -152,6 +159,9 @@ import {
152
159
  getProjectDatabaseWebhook,
153
160
  deleteProjectDatabaseWebhook,
154
161
  testProjectDatabaseWebhook,
162
+ createProjectDatabaseWatch,
163
+ listProjectDatabaseWatches,
164
+ deleteProjectDatabaseWatch,
155
165
  listProjectDatabaseWebhookDeliveries,
156
166
  replayProjectDatabaseWebhookDelivery,
157
167
  setProjectDatabaseSchedule,
@@ -328,7 +338,7 @@ export interface ProductApi {
328
338
  readonly environment: "dev" | "prod";
329
339
  readonly idempotencyKey: string;
330
340
  readonly url: string;
331
- readonly eventTypes: readonly DatabaseWebhookEventType[];
341
+ readonly eventTypes: readonly DatabaseWebhookEventPattern[];
332
342
  readonly headers: Readonly<Record<string, string>>;
333
343
  }): Promise<DatabaseWebhookCreated>;
334
344
  listProjectDatabaseWebhooks(input: {
@@ -350,6 +360,22 @@ export interface ProductApi {
350
360
  readonly environment: "dev" | "prod";
351
361
  readonly webhookId: string;
352
362
  }): Promise<DatabaseWebhookTestResult>;
363
+ createProjectDatabaseWatch(input: {
364
+ readonly projectId: string;
365
+ readonly environment: "dev" | "prod";
366
+ readonly idempotencyKey: string;
367
+ readonly request: DatabaseWatchRequest;
368
+ }): Promise<DatabaseWatch>;
369
+ listProjectDatabaseWatches(input: {
370
+ readonly projectId: string;
371
+ readonly environment: "dev" | "prod";
372
+ }): Promise<DatabaseWatchPage>;
373
+ deleteProjectDatabaseWatch(input: {
374
+ readonly projectId: string;
375
+ readonly environment: "dev" | "prod";
376
+ readonly idempotencyKey: string;
377
+ readonly watchId: string;
378
+ }): Promise<DatabaseWatchDeleted>;
353
379
  listProjectDatabaseWebhookDeliveries(input: {
354
380
  readonly projectId: string;
355
381
  readonly environment: "dev" | "prod";
@@ -566,6 +592,7 @@ export interface ProductApi {
566
592
 
567
593
  /** A webhook answer that names another environment, webhook or delivery than the request did. */
568
594
  const WEBHOOK_SCOPE_MISMATCH = "Webhook response scope mismatch";
595
+ const WATCH_SCOPE_MISMATCH = "Watch response scope mismatch";
569
596
  /** A schedule answer that names another environment or schedule than the request did. */
570
597
  const SCHEDULE_SCOPE_MISMATCH = "Schedule response scope mismatch";
571
598
 
@@ -889,6 +916,63 @@ export class GeneratedSdkProductApi implements ProductApi {
889
916
  return value;
890
917
  }
891
918
 
919
+ public async createProjectDatabaseWatch(
920
+ input: Parameters<ProductApi["createProjectDatabaseWatch"]>[0],
921
+ ): Promise<DatabaseWatch> {
922
+ const value = await createProjectDatabaseWatch(
923
+ {
924
+ project_id: input.projectId,
925
+ "Idempotency-Key": input.idempotencyKey,
926
+ databaseWatchCreateRequest: {
927
+ environment: input.environment,
928
+ schema: input.request.schema,
929
+ table: input.request.table,
930
+ columns: [...input.request.columns],
931
+ operations: [...input.request.operations],
932
+ },
933
+ },
934
+ { client: this.#client },
935
+ );
936
+ assertDatabaseWatch(value);
937
+ if (
938
+ value.environment !== input.environment ||
939
+ value.schema !== input.request.schema ||
940
+ value.table !== input.request.table
941
+ )
942
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
943
+ return value;
944
+ }
945
+
946
+ public async listProjectDatabaseWatches(
947
+ input: Parameters<ProductApi["listProjectDatabaseWatches"]>[0],
948
+ ): Promise<DatabaseWatchPage> {
949
+ const value = await listProjectDatabaseWatches(
950
+ { project_id: input.projectId, environment: input.environment },
951
+ { client: this.#client },
952
+ );
953
+ assertDatabaseWatchPage(value);
954
+ if (value.items.some((item) => item.environment !== input.environment))
955
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
956
+ return value;
957
+ }
958
+
959
+ public async deleteProjectDatabaseWatch(
960
+ input: Parameters<ProductApi["deleteProjectDatabaseWatch"]>[0],
961
+ ): Promise<DatabaseWatchDeleted> {
962
+ const value = await deleteProjectDatabaseWatch(
963
+ {
964
+ project_id: input.projectId,
965
+ environment: input.environment,
966
+ "Idempotency-Key": input.idempotencyKey,
967
+ watch_id: input.watchId,
968
+ },
969
+ { client: this.#client },
970
+ );
971
+ assertDatabaseWatchDeleted(value);
972
+ if (value.id !== input.watchId) throw new TypeError(WATCH_SCOPE_MISMATCH);
973
+ return value;
974
+ }
975
+
892
976
  /** The signing secret exists only in this answer; the CLI prints it once and stores nothing. */
893
977
  public async createProjectDatabaseWebhook(
894
978
  input: Parameters<ProductApi["createProjectDatabaseWebhook"]>[0],