@amerged/ohmyhost-cli 0.1.10 → 0.1.13

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
@@ -1,6 +1,6 @@
1
1
  # @amerged/ohmyhost-cli
2
2
 
3
- ohmyho.st cli client, version 0.1.10.
3
+ ohmyho.st cli client, version 0.1.13.
4
4
 
5
5
  ```sh
6
6
  npm install --global @amerged/ohmyhost-cli
package/dist/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire as __ohmyhostCreateRequire } from "node:module"; const require = __ohmyhostCreateRequire(import.meta.url);
3
3
  import {
4
4
  runProductionCli
5
- } from "./chunk-Z75ASLEB.js";
5
+ } from "./chunk-ZWAAQEOZ.js";
6
6
  import "./chunk-4HFQ54FP.js";
7
7
 
8
8
  // apps/product-cli/dist/bin.js
@@ -15000,6 +15000,10 @@ var recognizedCommandLabel = (argv) => {
15000
15000
  return "project notes set";
15001
15001
  if (value === "project" && values[0] === "status")
15002
15002
  return "project status";
15003
+ if (value === "project" && values[0] === "handle" && values[1] === "check")
15004
+ return "project handle check";
15005
+ if (value === "project" && values[0] === "handle" && values[1] === "set")
15006
+ return "project handle set";
15003
15007
  if (value === "project" && values[0] === "dev-access" && values[1] === "create")
15004
15008
  return "project dev-access create";
15005
15009
  if (value === "database" && values[0] === "compute" && values[1] === "set")
@@ -15442,6 +15446,31 @@ var parseProductCliCommand = (argv) => {
15442
15446
  }
15443
15447
  if (first === "project") {
15444
15448
  const subcommand = values.shift();
15449
+ if (subcommand === "handle" && values[0] === "set") {
15450
+ values.shift();
15451
+ if (yes || wait || follow || stdin)
15452
+ throw new InvalidCommandError();
15453
+ const options2 = parseOptions(values, [
15454
+ "--project",
15455
+ "--handle",
15456
+ "--if-match",
15457
+ "--idempotency-key"
15458
+ ]);
15459
+ return {
15460
+ kind: "project-handle-set",
15461
+ projectId: options2["--project"],
15462
+ handle: options2["--handle"],
15463
+ ifMatch: options2["--if-match"],
15464
+ idempotencyKey: options2["--idempotency-key"],
15465
+ credentialStore
15466
+ };
15467
+ }
15468
+ if (subcommand === "handle" && values.shift() === "check") {
15469
+ if (yes || wait || follow || stdin)
15470
+ throw new InvalidCommandError();
15471
+ const options2 = parseOptions(values, ["--handle"]);
15472
+ return { kind: "project-handle-check", handle: options2["--handle"], credentialStore };
15473
+ }
15445
15474
  if (subcommand === "dev-access" && values.shift() === "create") {
15446
15475
  if (yes || wait || follow || stdin)
15447
15476
  throw new InvalidCommandError();
@@ -16517,7 +16546,7 @@ function assertOrganizationAccount(value) {
16517
16546
  if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "as_of,available_micros,monthly_micros,name,next_expiry,one_time_micros,organization_id,paid_until,plan,plan_source,reserved_micros,workos_organization_id")
16518
16547
  throw new TypeError("Invalid account fields");
16519
16548
  const v = value;
16520
- if (typeof v["organization_id"] !== "string" || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(v["organization_id"]) || typeof v["name"] !== "string" || v["name"].length > 128 || typeof v["workos_organization_id"] !== "string" || !/^org_[A-Za-z0-9_]+$/u.test(v["workos_organization_id"]) || v["plan"] !== "paid" && v["plan"] !== "free" || !["free", "stripe", "beta", "manual"].includes(String(v["plan_source"])) || v["plan"] === "free" !== (v["plan_source"] === "free"))
16549
+ if (typeof v["organization_id"] !== "string" || !/^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(v["organization_id"]) || typeof v["name"] !== "string" || v["name"].length > 128 || typeof v["workos_organization_id"] !== "string" || !/^org_[A-Za-z0-9_]+$/u.test(v["workos_organization_id"]) || v["plan"] !== "paid" && v["plan"] !== "free" || !["free", "stripe", "granted", "manual"].includes(String(v["plan_source"])) || v["plan"] === "free" !== (v["plan_source"] === "free"))
16521
16550
  throw new TypeError("Invalid account scope");
16522
16551
  for (const field of ["available_micros", "monthly_micros", "one_time_micros", "reserved_micros"])
16523
16552
  if (typeof v[field] !== "string" || !/^(0|[1-9][0-9]{0,18})$/u.test(v[field]))
@@ -16743,6 +16772,26 @@ var PROBLEM_POLICY = {
16743
16772
  retryable: true,
16744
16773
  action: "Retry project creation with the same idempotency key."
16745
16774
  },
16775
+ project_handle_taken: {
16776
+ status: 409,
16777
+ retryable: false,
16778
+ action: "Run 'ohmyhost project handle check' for a free address, then rename to one of the alternatives it returns."
16779
+ },
16780
+ project_handle_invalid: {
16781
+ status: 409,
16782
+ retryable: false,
16783
+ action: "Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words."
16784
+ },
16785
+ project_handle_unchanged: {
16786
+ status: 409,
16787
+ retryable: false,
16788
+ action: "The project already answers on this address; no rename is needed."
16789
+ },
16790
+ project_rename_blocked: {
16791
+ status: 409,
16792
+ retryable: true,
16793
+ action: "Wait for the running operation to finish, read the project again, then rename with the new ETag."
16794
+ },
16746
16795
  project_identity_unavailable: {
16747
16796
  status: 503,
16748
16797
  retryable: true,
@@ -16808,6 +16857,11 @@ var PROBLEM_POLICY = {
16808
16857
  retryable: false,
16809
16858
  action: "Run 'ohmyhost init', apply the matching ohmyhost framework skill, then plan the new commit."
16810
16859
  },
16860
+ repository_configuration_missing: {
16861
+ status: 409,
16862
+ retryable: false,
16863
+ action: "Place ohmyhost.yaml at the Git repository root and set applicationRoot to the application directory (for example website). Commit and push the change, then plan the new commit."
16864
+ },
16811
16865
  migration_filename_noncanonical: {
16812
16866
  status: 409,
16813
16867
  retryable: false,
@@ -17066,7 +17120,7 @@ var parseDevAccessTicket = (value) => {
17066
17120
  ]);
17067
17121
  const origin = input["origin"];
17068
17122
  const redeemUrl = input["redeem_url"];
17069
- if (!isUlid(input["ticket_id"]) || !isUlid(input["project_id"]) || !isUlid(input["environment_id"]) || typeof origin !== "string" || !/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st$/u.test(origin) || typeof redeemUrl !== "string" || !/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(redeemUrl) || new URL(redeemUrl).origin !== origin || !isIsoInstant(input["expires_at"])) {
17123
+ if (!isUlid(input["ticket_id"]) || !isUlid(input["project_id"]) || !isUlid(input["environment_id"]) || typeof origin !== "string" || !/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st$/u.test(origin) || typeof redeemUrl !== "string" || !/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(redeemUrl) || new URL(redeemUrl).origin !== origin || !isIsoInstant(input["expires_at"])) {
17070
17124
  throw new ResponseContractError();
17071
17125
  }
17072
17126
  return input;
@@ -17189,7 +17243,7 @@ var parseProject = (value) => {
17189
17243
  "region",
17190
17244
  "created_at"
17191
17245
  ]);
17192
- if (!isUlid(input["id"]) || !isUlid(input["organization_id"]) || !boundedString(input["name"], 1, 128) || input["name"] !== input["name"].trim() || !boundedString(input["handle"], 5, 59) || !/^[a-z]+-[a-z]+-[a-z]+$/u.test(input["handle"]) || input["data_mode"] !== "shared" && input["data_mode"] !== "isolated" || input["region"] !== "us" && input["region"] !== "eu" || !isIsoInstant(input["created_at"])) {
17246
+ if (!isUlid(input["id"]) || !isUlid(input["organization_id"]) || !boundedString(input["name"], 1, 128) || input["name"] !== input["name"].trim() || !boundedString(input["handle"], 3, 59) || !/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(input["handle"]) || input["data_mode"] !== "shared" && input["data_mode"] !== "isolated" || input["region"] !== "us" && input["region"] !== "eu" || !isIsoInstant(input["created_at"])) {
17193
17247
  throw new ResponseContractError();
17194
17248
  }
17195
17249
  return input;
@@ -17207,6 +17261,7 @@ var parseProjectPage = (value) => {
17207
17261
  var parseProjectStatus = (value) => {
17208
17262
  const input = exactRecord2(value, [
17209
17263
  "project_id",
17264
+ "etag",
17210
17265
  "handle",
17211
17266
  "region",
17212
17267
  "lifecycle",
@@ -17220,7 +17275,7 @@ var parseProjectStatus = (value) => {
17220
17275
  "cleanup_state"
17221
17276
  ]);
17222
17277
  const handle = input["handle"];
17223
- if (!isUlid(input["project_id"]) || !boundedString(handle, 5, 59) || !/^[a-z]+-[a-z]+-[a-z]+$/u.test(handle) || input["region"] !== "us" && input["region"] !== "eu" || input["lifecycle"] !== "active" && input["lifecycle"] !== "deleting" && input["lifecycle"] !== "deleted" || input["dev_url"] !== null && ![`https://dev-${handle}.dev.check.omh.st`, `https://dev-${handle}.check.omh.st`].includes(String(input["dev_url"])) || input["prod_url"] !== null && ![`https://${handle}.dev.check.omh.st`, `https://${handle}.check.omh.st`].includes(String(input["prod_url"])) || input["cleanup_state"] !== "not_started" && input["cleanup_state"] !== "pending" && input["cleanup_state"] !== "completed" && input["cleanup_state"] !== "reconciliation_required") {
17278
+ if (!isUlid(input["project_id"]) || typeof input["etag"] !== "string" || !/^"sha256-[a-f0-9]{64}"$/u.test(input["etag"]) || !boundedString(handle, 3, 59) || !/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(handle) || input["region"] !== "us" && input["region"] !== "eu" || input["lifecycle"] !== "active" && input["lifecycle"] !== "deleting" && input["lifecycle"] !== "deleted" || input["dev_url"] !== null && ![`https://dev-${handle}.dev.check.omh.st`, `https://dev-${handle}.check.omh.st`].includes(String(input["dev_url"])) || input["prod_url"] !== null && ![`https://${handle}.dev.check.omh.st`, `https://${handle}.check.omh.st`].includes(String(input["prod_url"])) || input["cleanup_state"] !== "not_started" && input["cleanup_state"] !== "pending" && input["cleanup_state"] !== "completed" && input["cleanup_state"] !== "reconciliation_required") {
17224
17279
  throw new ResponseContractError();
17225
17280
  }
17226
17281
  if (input["source"] !== null)
@@ -17530,13 +17585,14 @@ var parseSafeProblem = (value) => {
17530
17585
  if (typeof code !== "string" || !(code in PROBLEM_POLICY))
17531
17586
  throw new ResponseContractError();
17532
17587
  const policy = PROBLEM_POLICY[code];
17533
- if (input["status"] !== policy.status || !boundedString(input["type"], 1, 512) || !isHttpsUrl(input["type"]) || !boundedString(input["title"], 1, 160) || !boundedString(input["request_id"], 1, 128) || typeof input["retryable"] !== "boolean" || !boundedString(input["suggested_action"], 1, 500) || input["detail"] !== void 0 && !boundedString(input["detail"], 1, 2e3) || input["instance"] !== void 0 && !boundedString(input["instance"], 1, 512)) {
17588
+ if (input["status"] !== policy.status || !boundedString(input["type"], 1, 512) || !isHttpsUrl(input["type"]) || !boundedString(input["title"], 1, 160) || !boundedString(input["request_id"], 1, 128) || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(input["request_id"]) || typeof input["retryable"] !== "boolean" || !boundedString(input["suggested_action"], 1, 500) || input["detail"] !== void 0 && !boundedString(input["detail"], 1, 2e3) || input["instance"] !== void 0 && !boundedString(input["instance"], 1, 512)) {
17534
17589
  throw new ResponseContractError();
17535
17590
  }
17536
17591
  const retryAfter = input["retry_after_seconds"];
17537
17592
  if (retryAfter !== void 0 && (code !== "rate_limited" || typeof retryAfter !== "number" || !Number.isSafeInteger(retryAfter) || retryAfter < 1 || retryAfter > 86400))
17538
17593
  throw new ResponseContractError();
17539
17594
  return {
17595
+ requestId: input["request_id"],
17540
17596
  code,
17541
17597
  retryable: policy.retryable,
17542
17598
  suggestedAction: (code === "framework_conversion_required" ? parseFrameworkConversionDetail(input["detail"]) ?? policy.action : policy.action) + (retryAfter === void 0 ? "" : ` Retry after ${String(retryAfter)} seconds.`),
@@ -17810,7 +17866,7 @@ function exact2(v, keys) {
17810
17866
  // apps/product-cli/package.json
17811
17867
  var package_default = {
17812
17868
  name: "@ohmyhost/product-cli",
17813
- version: "0.1.10",
17869
+ version: "0.1.13",
17814
17870
  private: true,
17815
17871
  ohmyhost: {
17816
17872
  deployment: "production",
@@ -18173,6 +18229,8 @@ var HELP_USAGE = Object.freeze({
18173
18229
  "project notes set": "ohmyhost project notes set --project ULID --version NUMBER --markdown TEXT --idempotency-key KEY --json (no credentials or signed URLs)",
18174
18230
  "project status": "ohmyhost project status --project ULID --json",
18175
18231
  "project dev-access create": "ohmyhost project dev-access create --project ULID --json",
18232
+ "project handle check": "ohmyhost project handle check --handle HANDLE --json (is this address free? answers with a reason and free alternatives; the address becomes HANDLE.check.omh.st)",
18233
+ "project handle set": "ohmyhost project handle set --project ULID --handle HANDLE --if-match ETAG --idempotency-key KEY --json (moves the project to a free address; the old one stops working and anyone may claim it)",
18176
18234
  "database compute set": "ohmyhost database compute set --project ULID --environment dev|prod --profile standard|performance --idempotency-key KEY --yes [--wait] --json\n standard: Free 0.25 CU/1 GB/60s idle, Paid 0.5 CU/2 GB/60s idle. performance: Paid 1 CU/4 GB/300s idle, 2.5x database compute credits per equal active minute; longer idle time also consumes credits.",
18177
18235
  "database compute get": "ohmyhost database compute get --project ULID [--environment dev|prod] --json",
18178
18236
  "database write": "ohmyhost database write --project ULID --environment dev|prod --statement-file PATH --idempotency-key KEY [--parameters-json JSON] --yes --json",
@@ -18447,6 +18505,10 @@ var ProductCli = class {
18447
18505
  return await this.createProject(command, signal);
18448
18506
  if (command.kind === "project-list")
18449
18507
  return await this.listProjects(command, signal);
18508
+ if (command.kind === "project-handle-check")
18509
+ return await this.checkProjectHandle(command, signal);
18510
+ if (command.kind === "project-handle-set")
18511
+ return await this.changeProjectHandle(command, signal);
18450
18512
  if (command.kind === "database-compute-set") {
18451
18513
  const api = await this.authenticatedApi(command.credentialStore, signal);
18452
18514
  let operation = parseOperation(await api.changeDatabaseCompute(command));
@@ -18786,6 +18848,21 @@ var ProductCli = class {
18786
18848
  }));
18787
18849
  return success("project list", { status: "succeeded", projects });
18788
18850
  }
18851
+ async changeProjectHandle(command, signal) {
18852
+ const api = await this.authenticatedApi(command.credentialStore, signal);
18853
+ const operation = parseOperation(await api.changeProjectHandle({
18854
+ projectId: command.projectId,
18855
+ handle: command.handle,
18856
+ ifMatch: command.ifMatch,
18857
+ idempotencyKey: command.idempotencyKey
18858
+ }));
18859
+ return success("project handle set", { status: "accepted", operation });
18860
+ }
18861
+ async checkProjectHandle(command, signal) {
18862
+ const api = await this.authenticatedApi(command.credentialStore, signal);
18863
+ const handle = await api.checkProjectHandle({ handle: command.handle });
18864
+ return success("project handle check", { status: "succeeded", handle });
18865
+ }
18789
18866
  async getProjectStatus(command, signal) {
18790
18867
  const api = await this.authenticatedApi(command.credentialStore, signal);
18791
18868
  const project = parseProjectStatus(await api.getProjectStatus(command.projectId));
@@ -19290,7 +19367,7 @@ var classifyFailure = (command, error, signal) => {
19290
19367
  if (looksLikeProblem(error)) {
19291
19368
  try {
19292
19369
  const problem = parseSafeProblem(error);
19293
- return failure(command, 7, problem.code, problem.retryable, problem.suggestedAction, void 0, problem.retryAfterSeconds);
19370
+ return failure(command, 7, problem.code, problem.retryable, problem.suggestedAction, void 0, problem.retryAfterSeconds, problem.requestId);
19294
19371
  } catch {
19295
19372
  return failure(command, 7, "response_contract_invalid", false, "Retry only after the client or server contract is repaired.");
19296
19373
  }
@@ -19310,7 +19387,7 @@ var success = (command, body) => ({
19310
19387
  `,
19311
19388
  stderr: ""
19312
19389
  });
19313
- var failure = (command, exitCode, code, retryable, suggestedAction, operation, retryAfterSeconds) => {
19390
+ var failure = (command, exitCode, code, retryable, suggestedAction, operation, retryAfterSeconds, requestId) => {
19314
19391
  const document = {
19315
19392
  version: 1,
19316
19393
  command,
@@ -19321,6 +19398,7 @@ var failure = (command, exitCode, code, retryable, suggestedAction, operation, r
19321
19398
  retryable,
19322
19399
  suggested_action: suggestedAction,
19323
19400
  ...retryAfterSeconds === void 0 ? {} : { retry_after_seconds: retryAfterSeconds },
19401
+ ...requestId === void 0 ? {} : { request_id: requestId },
19324
19402
  ...operation?.error === void 0 ? {} : { message: operation.error.message }
19325
19403
  }
19326
19404
  };
@@ -19390,6 +19468,10 @@ var parsedCommandName = (command) => {
19390
19468
  return "project status";
19391
19469
  if (command.kind === "project-dev-access-create")
19392
19470
  return "project dev-access create";
19471
+ if (command.kind === "project-handle-check")
19472
+ return "project handle check";
19473
+ if (command.kind === "project-handle-set")
19474
+ return "project handle set";
19393
19475
  if (command.kind === "database-write")
19394
19476
  return "database write";
19395
19477
  if (command.kind === "database-query")
@@ -20931,6 +21013,23 @@ var getProjectStatus = (parameters, options) => {
20931
21013
  ...params
20932
21014
  });
20933
21015
  };
21016
+ var getProjectHandleAvailability = (parameters, options) => {
21017
+ const params = buildClientParams([parameters], [
21018
+ {
21019
+ args: [
21020
+ { in: "headers", key: "X-Request-Id" },
21021
+ { in: "path", key: "handle" }
21022
+ ]
21023
+ }
21024
+ ]);
21025
+ return (options?.client ?? client).get({
21026
+ responseStyle: "data",
21027
+ security: [{ scheme: "bearer", type: "http" }],
21028
+ url: "/v1/project-handles/{handle}",
21029
+ ...options,
21030
+ ...params
21031
+ });
21032
+ };
20934
21033
  var createProjectDevAccessTicket = (parameters, options) => {
20935
21034
  const params = buildClientParams([parameters], [
20936
21035
  {
@@ -21071,6 +21170,31 @@ var getCloudflareDnsAuthorizationStatus = (parameters, options) => {
21071
21170
  ...params
21072
21171
  });
21073
21172
  };
21173
+ var changeProjectHandle = (parameters, options) => {
21174
+ const params = buildClientParams([parameters], [
21175
+ {
21176
+ args: [
21177
+ { in: "headers", key: "X-Request-Id" },
21178
+ { in: "headers", key: "Idempotency-Key" },
21179
+ { in: "headers", key: "If-Match" },
21180
+ { in: "path", key: "project_id" },
21181
+ { in: "body", key: "handle" }
21182
+ ]
21183
+ }
21184
+ ]);
21185
+ return (options?.client ?? client).put({
21186
+ responseStyle: "data",
21187
+ security: [{ scheme: "bearer", type: "http" }],
21188
+ url: "/v1/projects/{project_id}/handle",
21189
+ ...options,
21190
+ ...params,
21191
+ headers: {
21192
+ "Content-Type": "application/json",
21193
+ ...options?.headers,
21194
+ ...params.headers
21195
+ }
21196
+ });
21197
+ };
21074
21198
  var configureProjectDomains = (parameters, options) => {
21075
21199
  const params = buildClientParams([parameters], [
21076
21200
  {
@@ -21817,6 +21941,17 @@ var GeneratedSdkProductApi = class {
21817
21941
  async listProjects(input) {
21818
21942
  return listProjects(input, { client: this.#client });
21819
21943
  }
21944
+ async changeProjectHandle(input) {
21945
+ return changeProjectHandle({
21946
+ project_id: input.projectId,
21947
+ "Idempotency-Key": input.idempotencyKey,
21948
+ "If-Match": input.ifMatch,
21949
+ handle: input.handle
21950
+ }, { client: this.#client });
21951
+ }
21952
+ async checkProjectHandle(input) {
21953
+ return getProjectHandleAvailability({ handle: input.handle }, { client: this.#client });
21954
+ }
21820
21955
  async getProjectStatus(projectId) {
21821
21956
  return getProjectStatus({ project_id: projectId }, { client: this.#client });
21822
21957
  }
@@ -22078,7 +22213,7 @@ var OwnerOnlyProjectLinkStore = class {
22078
22213
  // apps/product-cli/dist/repository-init.js
22079
22214
  import { createHash } from "node:crypto";
22080
22215
  import { lstat as lstat2, open as open3, readFile, readdir, stat } from "node:fs/promises";
22081
- import { posix, resolve as resolve2 } from "node:path";
22216
+ import { dirname as dirname2, posix, resolve as resolve2 } from "node:path";
22082
22217
 
22083
22218
  // packages/contracts/dist/application-root.js
22084
22219
  var APPLICATION_ROOT_PATTERN = /^(?:\.|[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*)$/u;
@@ -23325,6 +23460,24 @@ async function assertRepositoryRoot(root) {
23325
23460
  if (!information.isDirectory()) {
23326
23461
  throw new RepositoryInitError("repository_not_directory", "Repository path is not a directory");
23327
23462
  }
23463
+ for (let ancestor = root; ; ancestor = dirname2(ancestor)) {
23464
+ let marker;
23465
+ try {
23466
+ marker = await lstat2(resolve2(ancestor, ".git"));
23467
+ } catch (error) {
23468
+ if (error.code !== "ENOENT") {
23469
+ throw new RepositoryInitError("repository_unreadable", "Could not determine the Git repository root");
23470
+ }
23471
+ }
23472
+ if (marker !== void 0) {
23473
+ if (ancestor !== root) {
23474
+ throw new RepositoryInitError("repository_root_required", "Run ohmyhost init from the Git repository root and select the application directory with --root; ohmyhost.yaml must be at the repository root.");
23475
+ }
23476
+ return;
23477
+ }
23478
+ if (dirname2(ancestor) === ancestor)
23479
+ return;
23480
+ }
23328
23481
  }
23329
23482
  async function collectRepositoryFiles(root) {
23330
23483
  const files = [];
@@ -24302,7 +24455,7 @@ var runOfflineInit = async (command, io, diagnostics) => {
24302
24455
  });
24303
24456
  const code = error instanceof RepositoryInitBlockedError ? "repository_init_blocked" : error instanceof RepositoryInitConflictError ? "repository_configuration_exists" : known ? error.code : "internal_error";
24304
24457
  const exitCode = error instanceof RepositoryInitBlockedError ? 9 : known ? 2 : 1;
24305
- const result = errorDocument("init", exitCode, code, false, error instanceof RepositoryInitConflictError ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying." : error instanceof RepositoryInitError && error.code === "project_name_invalid" ? "Pass a lowercase deployment slug with '--project', then rerun init." : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.");
24458
+ const result = errorDocument("init", exitCode, code, false, error instanceof RepositoryInitConflictError ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying." : error instanceof RepositoryInitError && error.code === "repository_root_required" ? "Run ohmyhost init from the Git repository root with --root set to the application directory (for example --root website); commit the root ohmyhost.yaml before planning." : error instanceof RepositoryInitError && error.code === "project_name_invalid" ? "Pass a lowercase deployment slug with '--project', then rerun init." : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.");
24306
24459
  io.writeStdout(result.stdout);
24307
24460
  return result.exitCode;
24308
24461
  }
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ import {
47
47
  selectWorkspace,
48
48
  selectedWorkspaceId,
49
49
  workspaceOf
50
- } from "./chunk-Z75ASLEB.js";
50
+ } from "./chunk-ZWAAQEOZ.js";
51
51
  import {
52
52
  CredentialStoreIntegrityError,
53
53
  CredentialStoreUnavailableError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -254,6 +254,7 @@ interface CliErrorDocument {
254
254
  readonly suggested_action: string;
255
255
  readonly message?: string;
256
256
  readonly retry_after_seconds?: number;
257
+ readonly request_id?: string;
257
258
  };
258
259
  }
259
260
 
@@ -310,6 +311,10 @@ const HELP_USAGE = Object.freeze({
310
311
  "ohmyhost project notes set --project ULID --version NUMBER --markdown TEXT --idempotency-key KEY --json (no credentials or signed URLs)",
311
312
  "project status": "ohmyhost project status --project ULID --json",
312
313
  "project dev-access create": "ohmyhost project dev-access create --project ULID --json",
314
+ "project handle check":
315
+ "ohmyhost project handle check --handle HANDLE --json (is this address free? answers with a reason and free alternatives; the address becomes HANDLE.check.omh.st)",
316
+ "project handle set":
317
+ "ohmyhost project handle set --project ULID --handle HANDLE --if-match ETAG --idempotency-key KEY --json (moves the project to a free address; the old one stops working and anyone may claim it)",
313
318
  "database compute set":
314
319
  "ohmyhost database compute set --project ULID --environment dev|prod --profile standard|performance --idempotency-key KEY --yes [--wait] --json\n" +
315
320
  " standard: Free 0.25 CU/1 GB/60s idle, Paid 0.5 CU/2 GB/60s idle. performance: Paid 1 CU/4 GB/300s idle, 2.5x database compute credits per equal active minute; longer idle time also consumes credits.",
@@ -687,6 +692,10 @@ export class ProductCli {
687
692
  }
688
693
  if (command.kind === "project-create") return await this.createProject(command, signal);
689
694
  if (command.kind === "project-list") return await this.listProjects(command, signal);
695
+ if (command.kind === "project-handle-check")
696
+ return await this.checkProjectHandle(command, signal);
697
+ if (command.kind === "project-handle-set")
698
+ return await this.changeProjectHandle(command, signal);
690
699
  if (command.kind === "database-compute-set") {
691
700
  const api = await this.authenticatedApi(command.credentialStore, signal);
692
701
  let operation = parseOperation(await api.changeDatabaseCompute(command));
@@ -1165,6 +1174,31 @@ export class ProductCli {
1165
1174
  return success("project list", { status: "succeeded", projects });
1166
1175
  }
1167
1176
 
1177
+ private async changeProjectHandle(
1178
+ command: Extract<ProductCliCommand, { kind: "project-handle-set" }>,
1179
+ signal: AbortSignal,
1180
+ ): Promise<CliResult> {
1181
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1182
+ const operation = parseOperation(
1183
+ await api.changeProjectHandle({
1184
+ projectId: command.projectId,
1185
+ handle: command.handle,
1186
+ ifMatch: command.ifMatch,
1187
+ idempotencyKey: command.idempotencyKey,
1188
+ }),
1189
+ );
1190
+ return success("project handle set", { status: "accepted", operation });
1191
+ }
1192
+
1193
+ private async checkProjectHandle(
1194
+ command: Extract<ProductCliCommand, { kind: "project-handle-check" }>,
1195
+ signal: AbortSignal,
1196
+ ): Promise<CliResult> {
1197
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1198
+ const handle = await api.checkProjectHandle({ handle: command.handle });
1199
+ return success("project handle check", { status: "succeeded", handle });
1200
+ }
1201
+
1168
1202
  private async getProjectStatus(
1169
1203
  command: Extract<ProductCliCommand, { kind: "project-status" }>,
1170
1204
  signal: AbortSignal,
@@ -2017,6 +2051,7 @@ const classifyFailure = (command: CommandLabel, error: unknown, signal: AbortSig
2017
2051
  problem.suggestedAction,
2018
2052
  undefined,
2019
2053
  problem.retryAfterSeconds,
2054
+ problem.requestId,
2020
2055
  );
2021
2056
  } catch {
2022
2057
  return failure(
@@ -2066,6 +2101,7 @@ const failure = (
2066
2101
  suggestedAction: string,
2067
2102
  operation?: ReturnType<typeof parseOperation>,
2068
2103
  retryAfterSeconds?: number,
2104
+ requestId?: string,
2069
2105
  ): CliResult => {
2070
2106
  const document: CliErrorDocument = {
2071
2107
  version: 1,
@@ -2077,6 +2113,7 @@ const failure = (
2077
2113
  retryable,
2078
2114
  suggested_action: suggestedAction,
2079
2115
  ...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
2116
+ ...(requestId === undefined ? {} : { request_id: requestId }),
2080
2117
  ...(operation?.error === undefined ? {} : { message: operation.error.message }),
2081
2118
  },
2082
2119
  };
@@ -2115,6 +2152,8 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
2115
2152
  if (command.kind === "project-notes-set") return "project notes set";
2116
2153
  if (command.kind === "project-status") return "project status";
2117
2154
  if (command.kind === "project-dev-access-create") return "project dev-access create";
2155
+ if (command.kind === "project-handle-check") return "project handle check";
2156
+ if (command.kind === "project-handle-set") return "project handle set";
2118
2157
  if (command.kind === "database-write") return "database write";
2119
2158
  if (command.kind === "database-query") return "database query";
2120
2159
  if (command.kind === "database-access-create") return "database access create";
@@ -132,6 +132,21 @@ export type ProductCliCommand =
132
132
  readonly idempotencyKey: string;
133
133
  readonly credentialStore: "native";
134
134
  }
135
+ | {
136
+ /** Moves a project to a chosen address once its gateways serve it. */
137
+ readonly kind: "project-handle-set";
138
+ readonly projectId: string;
139
+ readonly handle: string;
140
+ readonly ifMatch: string;
141
+ readonly idempotencyKey: string;
142
+ readonly credentialStore: "native";
143
+ }
144
+ | {
145
+ /** Asks whether a project address is free; the candidate is sent exactly as the agent typed it. */
146
+ readonly kind: "project-handle-check";
147
+ readonly handle: string;
148
+ readonly credentialStore: "native";
149
+ }
135
150
  | { readonly kind: "help"; readonly topic?: string }
136
151
  | { readonly kind: "version" }
137
152
  | { readonly kind: "whoami"; readonly credentialStore: "native" }
@@ -487,6 +502,8 @@ export type RecognizedCommandLabel =
487
502
  | "project list"
488
503
  | "project status"
489
504
  | "project dev-access create"
505
+ | "project handle check"
506
+ | "project handle set"
490
507
  | "database write"
491
508
  | "database query"
492
509
  | "database access create"
@@ -576,6 +593,10 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
576
593
  if (value === "project" && values[0] === "notes" && values[1] === "set")
577
594
  return "project notes set";
578
595
  if (value === "project" && values[0] === "status") return "project status";
596
+ if (value === "project" && values[0] === "handle" && values[1] === "check")
597
+ return "project handle check";
598
+ if (value === "project" && values[0] === "handle" && values[1] === "set")
599
+ return "project handle set";
579
600
  if (value === "project" && values[0] === "dev-access" && values[1] === "create")
580
601
  return "project dev-access create";
581
602
  if (value === "database" && values[0] === "compute" && values[1] === "set")
@@ -1045,6 +1066,29 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1045
1066
  }
1046
1067
  if (first === "project") {
1047
1068
  const subcommand = values.shift();
1069
+ if (subcommand === "handle" && values[0] === "set") {
1070
+ values.shift();
1071
+ if (yes || wait || follow || stdin) throw new InvalidCommandError();
1072
+ const options = parseOptions(values, [
1073
+ "--project",
1074
+ "--handle",
1075
+ "--if-match",
1076
+ "--idempotency-key",
1077
+ ]);
1078
+ return {
1079
+ kind: "project-handle-set",
1080
+ projectId: options["--project"],
1081
+ handle: options["--handle"],
1082
+ ifMatch: options["--if-match"],
1083
+ idempotencyKey: options["--idempotency-key"],
1084
+ credentialStore,
1085
+ };
1086
+ }
1087
+ if (subcommand === "handle" && values.shift() === "check") {
1088
+ if (yes || wait || follow || stdin) throw new InvalidCommandError();
1089
+ const options = parseOptions(values, ["--handle"]);
1090
+ return { kind: "project-handle-check", handle: options["--handle"], credentialStore };
1091
+ }
1048
1092
  if (subcommand === "dev-access" && values.shift() === "create") {
1049
1093
  if (yes || wait || follow || stdin) throw new InvalidCommandError();
1050
1094
  const options = parseOptions(values, ["--project"]);
@@ -429,9 +429,11 @@ const runOfflineInit = async (
429
429
  false,
430
430
  error instanceof RepositoryInitConflictError
431
431
  ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying."
432
- : error instanceof RepositoryInitError && error.code === "project_name_invalid"
433
- ? "Pass a lowercase deployment slug with '--project', then rerun init."
434
- : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
432
+ : error instanceof RepositoryInitError && error.code === "repository_root_required"
433
+ ? "Run ohmyhost init from the Git repository root with --root set to the application directory (for example --root website); commit the root ohmyhost.yaml before planning."
434
+ : error instanceof RepositoryInitError && error.code === "project_name_invalid"
435
+ ? "Pass a lowercase deployment slug with '--project', then rerun init."
436
+ : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
435
437
  );
436
438
  io.writeStdout(result.stdout);
437
439
  return result.exitCode;
@@ -83,6 +83,8 @@ import {
83
83
  listEnvironmentSecrets,
84
84
  listFunctionRuns,
85
85
  listProjects,
86
+ changeProjectHandle,
87
+ getProjectHandleAvailability,
86
88
  planDeployment,
87
89
  planPaidProjectDomain,
88
90
  planDeploymentRollback,
@@ -115,6 +117,7 @@ import {
115
117
  type CurrentIdentity,
116
118
  type DevAccessTicket,
117
119
  type ProjectPage,
120
+ type ProjectHandleAvailability,
118
121
  type ProjectStatus,
119
122
  type PaidDomain,
120
123
  type PaidDomainPlan,
@@ -208,6 +211,13 @@ export interface ProductApi {
208
211
  /** The signed-in user's workspaces; an interactive session is required for this route. */
209
212
  getAccountProfile(): Promise<readonly PublicWorkspace[]>;
210
213
  listProjects(input: { readonly cursor?: string; readonly limit: number }): Promise<ProjectPage>;
214
+ checkProjectHandle(input: { readonly handle: string }): Promise<ProjectHandleAvailability>;
215
+ changeProjectHandle(input: {
216
+ readonly projectId: string;
217
+ readonly handle: string;
218
+ readonly ifMatch: string;
219
+ readonly idempotencyKey: string;
220
+ }): Promise<Operation>;
211
221
  getProjectStatus(projectId: string): Promise<ProjectStatus>;
212
222
  writeProjectDatabase(
213
223
  input: ProjectDatabaseWriteRequest & { projectId: string; idempotencyKey: string },
@@ -582,6 +592,29 @@ export class GeneratedSdkProductApi implements ProductApi {
582
592
  return listProjects(input, { client: this.#client });
583
593
  }
584
594
 
595
+ public async changeProjectHandle(input: {
596
+ readonly projectId: string;
597
+ readonly handle: string;
598
+ readonly ifMatch: string;
599
+ readonly idempotencyKey: string;
600
+ }): Promise<Operation> {
601
+ return changeProjectHandle(
602
+ {
603
+ project_id: input.projectId,
604
+ "Idempotency-Key": input.idempotencyKey,
605
+ "If-Match": input.ifMatch,
606
+ handle: input.handle,
607
+ },
608
+ { client: this.#client },
609
+ );
610
+ }
611
+
612
+ public async checkProjectHandle(input: {
613
+ readonly handle: string;
614
+ }): Promise<ProjectHandleAvailability> {
615
+ return getProjectHandleAvailability({ handle: input.handle }, { client: this.#client });
616
+ }
617
+
585
618
  public async getProjectStatus(projectId: string): Promise<ProjectStatus> {
586
619
  return getProjectStatus({ project_id: projectId }, { client: this.#client });
587
620
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, open, readFile, readdir, stat } from "node:fs/promises";
3
- import { posix, resolve } from "node:path";
3
+ import { dirname, posix, resolve } from "node:path";
4
4
 
5
5
  import {
6
6
  normalizeApplicationRoot,
@@ -876,6 +876,30 @@ async function assertRepositoryRoot(root: string): Promise<void> {
876
876
  if (!information.isDirectory()) {
877
877
  throw new RepositoryInitError("repository_not_directory", "Repository path is not a directory");
878
878
  }
879
+ // A Git worktree uses a .git file; never read or follow that file's target.
880
+ for (let ancestor = root; ; ancestor = dirname(ancestor)) {
881
+ let marker;
882
+ try {
883
+ marker = await lstat(resolve(ancestor, ".git"));
884
+ } catch (error) {
885
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
886
+ throw new RepositoryInitError(
887
+ "repository_unreadable",
888
+ "Could not determine the Git repository root",
889
+ );
890
+ }
891
+ }
892
+ if (marker !== undefined) {
893
+ if (ancestor !== root) {
894
+ throw new RepositoryInitError(
895
+ "repository_root_required",
896
+ "Run ohmyhost init from the Git repository root and select the application directory with --root; ohmyhost.yaml must be at the repository root.",
897
+ );
898
+ }
899
+ return;
900
+ }
901
+ if (dirname(ancestor) === ancestor) return;
902
+ }
879
903
  }
880
904
 
881
905
  async function collectRepositoryFiles(root: string): Promise<string[]> {
@@ -129,6 +129,29 @@ const PROBLEM_POLICY = {
129
129
  retryable: true,
130
130
  action: "Retry project creation with the same idempotency key.",
131
131
  },
132
+ project_handle_taken: {
133
+ status: 409,
134
+ retryable: false,
135
+ action:
136
+ "Run 'ohmyhost project handle check' for a free address, then rename to one of the alternatives it returns.",
137
+ },
138
+ project_handle_invalid: {
139
+ status: 409,
140
+ retryable: false,
141
+ action:
142
+ "Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words.",
143
+ },
144
+ project_handle_unchanged: {
145
+ status: 409,
146
+ retryable: false,
147
+ action: "The project already answers on this address; no rename is needed.",
148
+ },
149
+ project_rename_blocked: {
150
+ status: 409,
151
+ retryable: true,
152
+ action:
153
+ "Wait for the running operation to finish, read the project again, then rename with the new ETag.",
154
+ },
132
155
  project_identity_unavailable: {
133
156
  status: 503,
134
157
  retryable: true,
@@ -196,6 +219,12 @@ const PROBLEM_POLICY = {
196
219
  action:
197
220
  "Run 'ohmyhost init', apply the matching ohmyhost framework skill, then plan the new commit.",
198
221
  },
222
+ repository_configuration_missing: {
223
+ status: 409,
224
+ retryable: false,
225
+ action:
226
+ "Place ohmyhost.yaml at the Git repository root and set applicationRoot to the application directory (for example website). Commit and push the change, then plan the new commit.",
227
+ },
199
228
  migration_filename_noncanonical: {
200
229
  status: 409,
201
230
  retryable: false,
@@ -278,6 +307,7 @@ export class ResponseContractError extends Error {
278
307
  }
279
308
 
280
309
  export interface SafeProblem {
310
+ readonly requestId: string;
281
311
  readonly retryAfterSeconds?: number;
282
312
  readonly code: ProblemCode;
283
313
  readonly retryable: boolean;
@@ -676,9 +706,9 @@ export const parseDevAccessTicket = (value: unknown): PublicDevAccessTicket => {
676
706
  !isUlid(input["project_id"]) ||
677
707
  !isUlid(input["environment_id"]) ||
678
708
  typeof origin !== "string" ||
679
- !/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st$/u.test(origin) ||
709
+ !/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st$/u.test(origin) ||
680
710
  typeof redeemUrl !== "string" ||
681
- !/^https:\/\/dev-[a-z]+-[a-z]+-[a-z]+\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(
711
+ !/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(
682
712
  redeemUrl,
683
713
  ) ||
684
714
  new URL(redeemUrl).origin !== origin ||
@@ -880,8 +910,8 @@ const parseProject = (value: unknown): PublicProject => {
880
910
  !isUlid(input["organization_id"]) ||
881
911
  !boundedString(input["name"], 1, 128) ||
882
912
  input["name"] !== input["name"].trim() ||
883
- !boundedString(input["handle"], 5, 59) ||
884
- !/^[a-z]+-[a-z]+-[a-z]+$/u.test(input["handle"]) ||
913
+ !boundedString(input["handle"], 3, 59) ||
914
+ !/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(input["handle"]) ||
885
915
  (input["data_mode"] !== "shared" && input["data_mode"] !== "isolated") ||
886
916
  (input["region"] !== "us" && input["region"] !== "eu") ||
887
917
  !isIsoInstant(input["created_at"])
@@ -910,6 +940,7 @@ export const parseProjectPage = (
910
940
  export const parseProjectStatus = (value: unknown): Readonly<Record<string, unknown>> => {
911
941
  const input = exactRecord(value, [
912
942
  "project_id",
943
+ "etag",
913
944
  "handle",
914
945
  "region",
915
946
  "lifecycle",
@@ -925,8 +956,10 @@ export const parseProjectStatus = (value: unknown): Readonly<Record<string, unkn
925
956
  const handle = input["handle"];
926
957
  if (
927
958
  !isUlid(input["project_id"]) ||
928
- !boundedString(handle, 5, 59) ||
929
- !/^[a-z]+-[a-z]+-[a-z]+$/u.test(handle) ||
959
+ typeof input["etag"] !== "string" ||
960
+ !/^"sha256-[a-f0-9]{64}"$/u.test(input["etag"]) ||
961
+ !boundedString(handle, 3, 59) ||
962
+ !/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(handle) ||
930
963
  (input["region"] !== "us" && input["region"] !== "eu") ||
931
964
  (input["lifecycle"] !== "active" &&
932
965
  input["lifecycle"] !== "deleting" &&
@@ -1540,6 +1573,7 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
1540
1573
  !isHttpsUrl(input["type"]) ||
1541
1574
  !boundedString(input["title"], 1, 160) ||
1542
1575
  !boundedString(input["request_id"], 1, 128) ||
1576
+ /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(input["request_id"]) ||
1543
1577
  typeof input["retryable"] !== "boolean" ||
1544
1578
  !boundedString(input["suggested_action"], 1, 500) ||
1545
1579
  (input["detail"] !== undefined && !boundedString(input["detail"], 1, 2_000)) ||
@@ -1558,6 +1592,7 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
1558
1592
  )
1559
1593
  throw new ResponseContractError();
1560
1594
  return {
1595
+ requestId: input["request_id"],
1561
1596
  code: code as ProblemCode,
1562
1597
  retryable: policy.retryable,
1563
1598
  suggestedAction:
@@ -190,7 +190,7 @@ export function assertOrganizationAccount(value: unknown): void {
190
190
  typeof v["workos_organization_id"] !== "string" ||
191
191
  !/^org_[A-Za-z0-9_]+$/u.test(v["workos_organization_id"]) ||
192
192
  (v["plan"] !== "paid" && v["plan"] !== "free") ||
193
- !["free", "stripe", "beta", "manual"].includes(String(v["plan_source"])) ||
193
+ !["free", "stripe", "granted", "manual"].includes(String(v["plan_source"])) ||
194
194
  (v["plan"] === "free") !== (v["plan_source"] === "free")
195
195
  )
196
196
  throw new TypeError("Invalid account scope");
@@ -3,6 +3,7 @@
3
3
  export {
4
4
  applyPaidProjectDomain,
5
5
  changeDatabaseCompute,
6
+ changeProjectHandle,
6
7
  completeCloudflareDnsAuthorization,
7
8
  completeGithubSourceAuthorization,
8
9
  configureBillingRecharge,
@@ -42,6 +43,7 @@ export {
42
43
  getProjectContext,
43
44
  getProjectCreditBudget,
44
45
  getProjectExport,
46
+ getProjectHandleAvailability,
45
47
  getProjectMailDomain,
46
48
  getProjectSource,
47
49
  getProjectSourceAutoDeploy,
@@ -100,6 +102,11 @@ export type {
100
102
  ChangeDatabaseComputeErrors,
101
103
  ChangeDatabaseComputeResponse,
102
104
  ChangeDatabaseComputeResponses,
105
+ ChangeProjectHandleData,
106
+ ChangeProjectHandleError,
107
+ ChangeProjectHandleErrors,
108
+ ChangeProjectHandleResponse,
109
+ ChangeProjectHandleResponses,
103
110
  ClientOptions,
104
111
  CloudflareDnsAuthorization,
105
112
  CloudflareDnsAuthorizationStatus,
@@ -331,6 +338,11 @@ export type {
331
338
  GetProjectExportErrors,
332
339
  GetProjectExportResponse,
333
340
  GetProjectExportResponses,
341
+ GetProjectHandleAvailabilityData,
342
+ GetProjectHandleAvailabilityError,
343
+ GetProjectHandleAvailabilityErrors,
344
+ GetProjectHandleAvailabilityResponse,
345
+ GetProjectHandleAvailabilityResponses,
334
346
  GetProjectMailDomainData,
335
347
  GetProjectMailDomainError,
336
348
  GetProjectMailDomainErrors,
@@ -467,6 +479,7 @@ export type {
467
479
  ProjectExport,
468
480
  ProjectExportArchive,
469
481
  ProjectFilter,
482
+ ProjectHandleAvailability,
470
483
  ProjectId,
471
484
  ProjectLimit,
472
485
  ProjectMailDomain,
@@ -15,6 +15,8 @@ import type {
15
15
  ApplyPaidProjectDomainResponses,
16
16
  ChangeDatabaseComputeErrors,
17
17
  ChangeDatabaseComputeResponses,
18
+ ChangeProjectHandleErrors,
19
+ ChangeProjectHandleResponses,
18
20
  CompleteCloudflareDnsAuthorizationErrors,
19
21
  CompleteCloudflareDnsAuthorizationResponses,
20
22
  CompleteGithubSourceAuthorizationErrors,
@@ -100,6 +102,8 @@ import type {
100
102
  GetProjectErrors,
101
103
  GetProjectExportErrors,
102
104
  GetProjectExportResponses,
105
+ GetProjectHandleAvailabilityErrors,
106
+ GetProjectHandleAvailabilityResponses,
103
107
  GetProjectMailDomainErrors,
104
108
  GetProjectMailDomainResponses,
105
109
  GetProjectResponses,
@@ -447,7 +451,7 @@ export const getFeatureInterests = <ThrowOnError extends boolean = true>(
447
451
  "data"
448
452
  >({
449
453
  responseStyle: "data",
450
- url: "/v1/beta/feature-interests",
454
+ url: "/v1/site/feature-interests",
451
455
  ...options,
452
456
  ...params,
453
457
  });
@@ -492,7 +496,7 @@ export const registerFeatureInterest = <ThrowOnError extends boolean = true>(
492
496
  "data"
493
497
  >({
494
498
  responseStyle: "data",
495
- url: "/v1/beta/feature-interests",
499
+ url: "/v1/site/feature-interests",
496
500
  ...options,
497
501
  ...params,
498
502
  headers: {
@@ -521,7 +525,7 @@ export const getPublicDeploymentStats = <ThrowOnError extends boolean = true>(
521
525
  "data"
522
526
  >({
523
527
  responseStyle: "data",
524
- url: "/v1/beta/stats",
528
+ url: "/v1/site/stats",
525
529
  ...options,
526
530
  });
527
531
 
@@ -1682,6 +1686,48 @@ export const getProjectStatus = <ThrowOnError extends boolean = true>(
1682
1686
  });
1683
1687
  };
1684
1688
 
1689
+ /**
1690
+ * Check whether a project handle is free
1691
+ *
1692
+ * Reports whether a handle can be used for a project and why not when it cannot, with free alternatives an agent can offer. A handle is one to five lowercase words of letters and digits separated by single hyphens, three to fifty-nine characters, and never one of the reserved words. Availability is answered across all projects without revealing which organization holds a taken handle.
1693
+ */
1694
+ export const getProjectHandleAvailability = <ThrowOnError extends boolean = true>(
1695
+ parameters: {
1696
+ "X-Request-Id"?: string;
1697
+ handle: string;
1698
+ },
1699
+ options?: Options<never, ThrowOnError>,
1700
+ ): RequestResult<
1701
+ GetProjectHandleAvailabilityResponses,
1702
+ GetProjectHandleAvailabilityErrors,
1703
+ ThrowOnError,
1704
+ "data"
1705
+ > => {
1706
+ const params = buildClientParams(
1707
+ [parameters],
1708
+ [
1709
+ {
1710
+ args: [
1711
+ { in: "headers", key: "X-Request-Id" },
1712
+ { in: "path", key: "handle" },
1713
+ ],
1714
+ },
1715
+ ],
1716
+ );
1717
+ return (options?.client ?? client).get<
1718
+ GetProjectHandleAvailabilityResponses,
1719
+ GetProjectHandleAvailabilityErrors,
1720
+ ThrowOnError,
1721
+ "data"
1722
+ >({
1723
+ responseStyle: "data",
1724
+ security: [{ scheme: "bearer", type: "http" }],
1725
+ url: "/v1/project-handles/{handle}",
1726
+ ...options,
1727
+ ...params,
1728
+ });
1729
+ };
1730
+
1685
1731
  /**
1686
1732
  * Create a single-use dev access ticket
1687
1733
  *
@@ -2026,6 +2072,54 @@ export const getCloudflareDnsAuthorizationStatus = <ThrowOnError extends boolean
2026
2072
  });
2027
2073
  };
2028
2074
 
2075
+ /**
2076
+ * Move a project to a chosen address
2077
+ *
2078
+ * Starts a durable operation that re-publishes the project's Dev and Prod gateways at the chosen address and stores it only once both serve it. The previous address stops answering and returns to the pool for any project to claim. Ask GET /v1/project-handles/{handle} first; a taken, unusable or unchanged address is refused before any gateway moves, and a project with an operation in flight is refused until it finishes.
2079
+ */
2080
+ export const changeProjectHandle = <ThrowOnError extends boolean = true>(
2081
+ parameters: {
2082
+ "X-Request-Id"?: string;
2083
+ "Idempotency-Key": string;
2084
+ "If-Match": string;
2085
+ project_id: Ulid;
2086
+ handle: string;
2087
+ },
2088
+ options?: Options<never, ThrowOnError>,
2089
+ ): RequestResult<ChangeProjectHandleResponses, ChangeProjectHandleErrors, ThrowOnError, "data"> => {
2090
+ const params = buildClientParams(
2091
+ [parameters],
2092
+ [
2093
+ {
2094
+ args: [
2095
+ { in: "headers", key: "X-Request-Id" },
2096
+ { in: "headers", key: "Idempotency-Key" },
2097
+ { in: "headers", key: "If-Match" },
2098
+ { in: "path", key: "project_id" },
2099
+ { in: "body", key: "handle" },
2100
+ ],
2101
+ },
2102
+ ],
2103
+ );
2104
+ return (options?.client ?? client).put<
2105
+ ChangeProjectHandleResponses,
2106
+ ChangeProjectHandleErrors,
2107
+ ThrowOnError,
2108
+ "data"
2109
+ >({
2110
+ responseStyle: "data",
2111
+ security: [{ scheme: "bearer", type: "http" }],
2112
+ url: "/v1/projects/{project_id}/handle",
2113
+ ...options,
2114
+ ...params,
2115
+ headers: {
2116
+ "Content-Type": "application/json",
2117
+ ...options?.headers,
2118
+ ...params.headers,
2119
+ },
2120
+ });
2121
+ };
2122
+
2029
2123
  /**
2030
2124
  * Apply the server-derived project domains
2031
2125
  *
@@ -807,7 +807,7 @@ export type OrganizationAccount = {
807
807
  name: string;
808
808
  workos_organization_id: string;
809
809
  plan: "free" | "paid";
810
- plan_source: "free" | "stripe" | "beta" | "manual";
810
+ plan_source: "free" | "stripe" | "granted" | "manual";
811
811
  paid_until: string | null;
812
812
  available_micros: CreditMicros;
813
813
  monthly_micros: CreditMicros;
@@ -845,7 +845,30 @@ export type ProjectEnvironment = {
845
845
  name: string;
846
846
  };
847
847
 
848
+ export type ProjectHandleAvailability = {
849
+ /**
850
+ * The handle exactly as it was asked about.
851
+ */
852
+ handle: string;
853
+ /**
854
+ * True when a project may take this handle now.
855
+ */
856
+ available: boolean;
857
+ /**
858
+ * Why the handle cannot be used. Absent when it is available.
859
+ */
860
+ reason?: "taken" | "too_short" | "too_long" | "invalid_shape" | "prohibited_word";
861
+ /**
862
+ * Free handles to offer instead, empty when the handle is available.
863
+ */
864
+ alternatives: Array<string>;
865
+ };
866
+
848
867
  export type ProjectStatus = {
868
+ /**
869
+ * Optimistic-concurrency token for changing this project address; use it as If-Match on the handle endpoint.
870
+ */
871
+ etag: string;
849
872
  project_id: Ulid;
850
873
  handle: string;
851
874
  region: HostingRegion;
@@ -893,9 +916,14 @@ export type ProblemDetails = {
893
916
  | "mail_domain_required"
894
917
  | "storage_jurisdiction_conflict"
895
918
  | "framework_conversion_required"
919
+ | "repository_configuration_missing"
896
920
  | "migration_filename_noncanonical"
897
921
  | "environment_secret_mutation_blocked"
898
922
  | "workers_runtime_incompatible"
923
+ | "project_handle_invalid"
924
+ | "project_handle_taken"
925
+ | "project_handle_unchanged"
926
+ | "project_rename_blocked"
899
927
  | "payload_too_large"
900
928
  | "rate_limited"
901
929
  | "insufficient_organization_credits"
@@ -1589,7 +1617,7 @@ export type GetFeatureInterestsData = {
1589
1617
  };
1590
1618
  path?: never;
1591
1619
  query?: never;
1592
- url: "/v1/beta/feature-interests";
1620
+ url: "/v1/site/feature-interests";
1593
1621
  };
1594
1622
 
1595
1623
  export type GetFeatureInterestsErrors = {
@@ -1633,7 +1661,7 @@ export type RegisterFeatureInterestData = {
1633
1661
  };
1634
1662
  path?: never;
1635
1663
  query?: never;
1636
- url: "/v1/beta/feature-interests";
1664
+ url: "/v1/site/feature-interests";
1637
1665
  };
1638
1666
 
1639
1667
  export type RegisterFeatureInterestErrors = {
@@ -1683,7 +1711,7 @@ export type GetPublicDeploymentStatsData = {
1683
1711
  body?: never;
1684
1712
  path?: never;
1685
1713
  query?: never;
1686
- url: "/v1/beta/stats";
1714
+ url: "/v1/site/stats";
1687
1715
  };
1688
1716
 
1689
1717
  export type GetPublicDeploymentStatsErrors = {
@@ -3255,6 +3283,60 @@ export type GetProjectStatusResponses = {
3255
3283
 
3256
3284
  export type GetProjectStatusResponse = GetProjectStatusResponses[keyof GetProjectStatusResponses];
3257
3285
 
3286
+ export type GetProjectHandleAvailabilityData = {
3287
+ body?: never;
3288
+ headers?: {
3289
+ /**
3290
+ * Optional caller-provided correlation identifier.
3291
+ */
3292
+ "X-Request-Id"?: string;
3293
+ };
3294
+ path: {
3295
+ /**
3296
+ * The handle a customer would like to use.
3297
+ */
3298
+ handle: string;
3299
+ };
3300
+ query?: never;
3301
+ url: "/v1/project-handles/{handle}";
3302
+ };
3303
+
3304
+ export type GetProjectHandleAvailabilityErrors = {
3305
+ /**
3306
+ * The request failed.
3307
+ */
3308
+ 400: ProblemDetails;
3309
+ /**
3310
+ * The request failed.
3311
+ */
3312
+ 401: ProblemDetails;
3313
+ /**
3314
+ * The request failed.
3315
+ */
3316
+ 403: ProblemDetails;
3317
+ /**
3318
+ * The request failed.
3319
+ */
3320
+ 429: ProblemDetails;
3321
+ /**
3322
+ * The request failed.
3323
+ */
3324
+ 503: ProblemDetails;
3325
+ };
3326
+
3327
+ export type GetProjectHandleAvailabilityError =
3328
+ GetProjectHandleAvailabilityErrors[keyof GetProjectHandleAvailabilityErrors];
3329
+
3330
+ export type GetProjectHandleAvailabilityResponses = {
3331
+ /**
3332
+ * Whether the handle is usable, and alternatives when it is not.
3333
+ */
3334
+ 200: ProjectHandleAvailability;
3335
+ };
3336
+
3337
+ export type GetProjectHandleAvailabilityResponse =
3338
+ GetProjectHandleAvailabilityResponses[keyof GetProjectHandleAvailabilityResponses];
3339
+
3258
3340
  export type CreateProjectDevAccessTicketData = {
3259
3341
  body?: never;
3260
3342
  headers?: {
@@ -3739,6 +3821,80 @@ export type GetCloudflareDnsAuthorizationStatusResponses = {
3739
3821
  export type GetCloudflareDnsAuthorizationStatusResponse =
3740
3822
  GetCloudflareDnsAuthorizationStatusResponses[keyof GetCloudflareDnsAuthorizationStatusResponses];
3741
3823
 
3824
+ export type ChangeProjectHandleData = {
3825
+ body: {
3826
+ /**
3827
+ * One to five lowercase words of letters and digits.
3828
+ */
3829
+ handle: string;
3830
+ };
3831
+ headers: {
3832
+ /**
3833
+ * Optional caller-provided correlation identifier.
3834
+ */
3835
+ "X-Request-Id"?: string;
3836
+ /**
3837
+ * Identifies one mutation and its canonical request payload.
3838
+ */
3839
+ "Idempotency-Key": string;
3840
+ /**
3841
+ * Exact ETag returned for the current project or deployment state.
3842
+ */
3843
+ "If-Match": string;
3844
+ };
3845
+ path: {
3846
+ /**
3847
+ * Project identifier.
3848
+ */
3849
+ project_id: Ulid;
3850
+ };
3851
+ query?: never;
3852
+ url: "/v1/projects/{project_id}/handle";
3853
+ };
3854
+
3855
+ export type ChangeProjectHandleErrors = {
3856
+ /**
3857
+ * The request failed.
3858
+ */
3859
+ 400: ProblemDetails;
3860
+ /**
3861
+ * The request failed.
3862
+ */
3863
+ 401: ProblemDetails;
3864
+ /**
3865
+ * The request failed.
3866
+ */
3867
+ 403: ProblemDetails;
3868
+ /**
3869
+ * The resource does not exist or is not visible to the authenticated principal.
3870
+ */
3871
+ 404: ProblemDetails;
3872
+ /**
3873
+ * The idempotency key was already used with a different canonical request.
3874
+ */
3875
+ 409: IdempotencyConflictProblem;
3876
+ /**
3877
+ * The request failed.
3878
+ */
3879
+ 429: ProblemDetails;
3880
+ /**
3881
+ * The request failed.
3882
+ */
3883
+ 503: ProblemDetails;
3884
+ };
3885
+
3886
+ export type ChangeProjectHandleError = ChangeProjectHandleErrors[keyof ChangeProjectHandleErrors];
3887
+
3888
+ export type ChangeProjectHandleResponses = {
3889
+ /**
3890
+ * The mutation was accepted for asynchronous processing.
3891
+ */
3892
+ 202: Operation;
3893
+ };
3894
+
3895
+ export type ChangeProjectHandleResponse =
3896
+ ChangeProjectHandleResponses[keyof ChangeProjectHandleResponses];
3897
+
3742
3898
  export type ConfigureProjectDomainsData = {
3743
3899
  body?: never;
3744
3900
  headers: {