@amerged/ohmyhost-mcp 0.1.22 → 0.1.24
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 +5 -2
- package/dist/{chunk-NLBFEH6H.js → chunk-EEFUMPKG.js} +25 -24
- package/dist/index.js +1 -1
- package/dist/stdio-main.js +734 -269
- package/package.json +1 -1
- package/src/apps/mcp/package.json +47 -0
- package/src/apps/mcp/{local-client.ts → src/local-client.ts} +69 -12
- package/src/apps/mcp/{local-server.ts → src/local-server.ts} +327 -145
- package/src/apps/product-cli/package.json +45 -0
- package/src/apps/product-cli/{bin.ts → src/bin.ts} +3 -0
- package/src/apps/product-cli/{cli.ts → src/cli.ts} +363 -87
- package/src/apps/product-cli/{command.ts → src/command.ts} +58 -11
- package/src/apps/product-cli/{composition.ts → src/composition.ts} +98 -21
- package/src/apps/product-cli/{index.ts → src/index.ts} +1 -0
- package/src/apps/product-cli/{product-api.ts → src/product-api.ts} +65 -19
- package/src/apps/product-cli/{repository-init.ts → src/repository-init.ts} +79 -29
- package/src/apps/product-cli/{runtime-contract.ts → src/runtime-contract.ts} +16 -309
- package/src/apps/product-cli/{user-token-file.ts → src/user-token-file.ts} +46 -35
- package/src/apps/product-cli/{workspace.ts → src/workspace.ts} +6 -0
- package/src/packages/agent-skills/package.json +24 -0
- package/src/packages/agent-skills/src/generated-skill-resources.ts +165 -0
- package/src/packages/agent-skills/{index.ts → src/index.ts} +1 -1
- package/src/packages/contracts/package.json +137 -0
- package/src/packages/contracts/src/browser-capabilities.ts +133 -0
- package/src/packages/contracts/src/customer-auth-admission.ts +36 -0
- package/src/packages/contracts/{database-access.ts → src/database-access.ts} +10 -2
- package/src/packages/contracts/{framework-admission.ts → src/framework-admission.ts} +88 -13
- package/src/packages/contracts/src/framework-build-script.mjs +22 -0
- package/src/packages/contracts/src/framework-build-script.ts +1 -0
- package/src/packages/contracts/{index.ts → src/index.ts} +17 -16
- package/src/packages/contracts/src/operation-data-change-result.ts +86 -0
- package/src/packages/contracts/{operation-deployment-result.ts → src/operation-deployment-result.ts} +2 -2
- package/src/packages/contracts/{operation-failure.ts → src/operation-failure.ts} +24 -12
- package/src/packages/contracts/src/problem-policy.ts +498 -0
- package/src/packages/contracts/src/project-data-changes.ts +90 -0
- package/src/packages/contracts/src/secret-names.ts +27 -0
- package/src/packages/sdk-ts/package.json +21 -0
- package/src/packages/sdk-ts/{generated → src/generated}/index.ts +15 -0
- package/src/packages/sdk-ts/{generated → src/generated}/sdk.gen.ts +114 -5
- package/src/packages/sdk-ts/{generated → src/generated}/types.gen.ts +209 -10
- package/src/packages/workos-auth-contracts/package.json +39 -0
- package/src/packages/agent-skills/generated-skill-resources.ts +0 -165
- /package/src/apps/mcp/{index.ts → src/index.ts} +0 -0
- /package/src/apps/mcp/{stdio-main.ts → src/stdio-main.ts} +0 -0
- /package/src/apps/product-cli/{credential-store.ts → src/credential-store.ts} +0 -0
- /package/src/apps/product-cli/{device-token-expiry.ts → src/device-token-expiry.ts} +0 -0
- /package/src/apps/product-cli/{file-credential-store.ts → src/file-credential-store.ts} +0 -0
- /package/src/apps/product-cli/{profile-store.ts → src/profile-store.ts} +0 -0
- /package/src/apps/product-cli/{project-link-store.ts → src/project-link-store.ts} +0 -0
- /package/src/apps/product-cli/{psql-session.ts → src/psql-session.ts} +0 -0
- /package/src/packages/contracts/{application-root.ts → src/application-root.ts} +0 -0
- /package/src/packages/contracts/{billing.ts → src/billing.ts} +0 -0
- /package/src/packages/contracts/{credit-pricing.ts → src/credit-pricing.ts} +0 -0
- /package/src/packages/contracts/{database-compute.ts → src/database-compute.ts} +0 -0
- /package/src/packages/contracts/{database-migration-admission.ts → src/database-migration-admission.ts} +0 -0
- /package/src/packages/contracts/{feedback.ts → src/feedback.ts} +0 -0
- /package/src/packages/contracts/{framework-family.ts → src/framework-family.ts} +0 -0
- /package/src/packages/contracts/{functions.ts → src/functions.ts} +0 -0
- /package/src/packages/contracts/{platform-origins.ts → src/platform-origins.ts} +0 -0
- /package/src/packages/contracts/{project-context.ts → src/project-context.ts} +0 -0
- /package/src/packages/contracts/{project-exports.ts → src/project-exports.ts} +0 -0
- /package/src/packages/contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client/client.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client/index.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client/types.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client/utils.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/client.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/auth.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/bodySerializer.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/params.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/pathSerializer.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/queryKeySerializer.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/serverSentEvents.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/types.gen.ts +0 -0
- /package/src/packages/sdk-ts/{generated → src/generated}/core/utils.gen.ts +0 -0
- /package/src/packages/sdk-ts/{index.ts → src/index.ts} +0 -0
- /package/src/packages/sdk-ts/{managed-mail-response.ts → src/managed-mail-response.ts} +0 -0
- /package/src/packages/sdk-ts/{managed-mail.ts → src/managed-mail.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{device-flow.ts → src/device-flow.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{index.ts → src/index.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{management-rpc.ts → src/management-rpc.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{organizations.ts → src/organizations.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{parsing.ts → src/parsing.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{session-status.ts → src/session-status.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{tokens.ts → src/tokens.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{user-api-keys.ts → src/user-api-keys.ts} +0 -0
- /package/src/packages/workos-auth-contracts/{webhooks.ts → src/webhooks.ts} +0 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { assertOrganizationAccount } from "@ohmyhost/contracts/credit-pricing";
|
|
10
10
|
import { assertDatabaseCompute } from "@ohmyhost/contracts/database-compute";
|
|
11
11
|
import cliPackage from "../package.json" with { type: "json" };
|
|
12
|
+
import { isUserSecretName } from "@ohmyhost/contracts/secret-names";
|
|
12
13
|
import {
|
|
13
14
|
assertProjectContext,
|
|
14
15
|
assertProjectNotesReceipt,
|
|
@@ -72,6 +73,7 @@ import {
|
|
|
72
73
|
parseGithubOrganizationConnectionStatus,
|
|
73
74
|
parseCloudflareDnsAuthorizationStatus,
|
|
74
75
|
parseDeploymentPlan,
|
|
76
|
+
parseProjectDataChangePlan,
|
|
75
77
|
parseCurrentIdentity,
|
|
76
78
|
parseOrganization,
|
|
77
79
|
type PublicAccount,
|
|
@@ -307,7 +309,7 @@ const HELP_USAGE = Object.freeze({
|
|
|
307
309
|
"ohmyhost export get EXPORT_ULID --project ULID --json (poll the original job; signed ZIP download lasts 24 hours)",
|
|
308
310
|
"credits account": "ohmyhost credits account --organization ULID --json",
|
|
309
311
|
"referral link":
|
|
310
|
-
"ohmyhost referral link --organization ULID --json (the workspace's link to share; a new user
|
|
312
|
+
"ohmyhost referral link --organization ULID --json (the workspace's link to share; a new user whose first workspace comes from it starts with 30 days of Paid and 1,000 credits, and its first payment gives this workspace 1,000 credits plus 30 days of Paid unless a subscription or an unbounded grant already covers it)",
|
|
311
313
|
"credits balance": "ohmyhost credits balance --organization ULID --json",
|
|
312
314
|
"billing recharge get": "ohmyhost billing recharge get --organization ULID --json",
|
|
313
315
|
"billing recharge set":
|
|
@@ -340,7 +342,11 @@ const HELP_USAGE = Object.freeze({
|
|
|
340
342
|
"feedback submit":
|
|
341
343
|
"ohmyhost feedback submit --organization ULID --kind bug|issue|feature_request --title TITLE --description REDACTED_REPORT [--project ULID] [--environment ULID] [--operation ULID] [--error-code CODE] [--client-version VERSION] --idempotency-key KEY --json",
|
|
342
344
|
"project create":
|
|
343
|
-
"ohmyhost project create --organization ULID --name NAME [--data-mode shared|isolated] [--dev-access-mode protected|public] [--region us|eu] --idempotency-key KEY --json (the region is chosen once: us is the default, eu places the database, files and builds in the EU; it cannot be changed later)",
|
|
345
|
+
"ohmyhost project create --organization ULID --name NAME [--data-mode shared|isolated] [--dev-access-mode protected|public] [--region us|eu] --idempotency-key KEY --json (data mode is optional and defaults to shared; later assignments use project data plan/change without copying data; the region is chosen once: us is the default, eu places the database, files and builds in the EU; it cannot be changed later)",
|
|
346
|
+
"project data plan":
|
|
347
|
+
"ohmyhost project data plan --project ULID --change isolate_prod_keeps_data|isolate_dev_keeps_data|share_prod_keeps_data|reset_dev --json (shared is the optional creation default; review which data is kept or deleted; no data is copied)",
|
|
348
|
+
"project data change":
|
|
349
|
+
"ohmyhost project data change --project ULID --change isolate_prod_keeps_data|isolate_dev_keeps_data|share_prod_keeps_data|reset_dev --if-match ETAG --confirmation-token TOKEN --idempotency-key KEY --yes [--wait] --json (Owner only; use the reviewed plan's guards and the same key after uncertainty; no data is copied)",
|
|
344
350
|
"project list": "ohmyhost project list [--cursor ULID] [--limit LIMIT] --json",
|
|
345
351
|
"project context": "ohmyhost project context --project ULID --json",
|
|
346
352
|
"project notes set":
|
|
@@ -356,7 +362,7 @@ const HELP_USAGE = Object.freeze({
|
|
|
356
362
|
"ohmyhost project dev-access mode --project ULID --mode protected|public --idempotency-key KEY --yes --json",
|
|
357
363
|
"project flag status": "ohmyhost project flag status --project ULID --json",
|
|
358
364
|
"project flag set":
|
|
359
|
-
"ohmyhost project flag set --project ULID --enabled true|false --idempotency-key KEY --json (shows the small Powered by ohmyho.st flag on the production site; while it shows,
|
|
365
|
+
"ohmyhost project flag set --project ULID --enabled true|false --idempotency-key KEY --json (shows the small Powered by ohmyho.st flag on the production site; while it shows, this project's custom domain uses no domain credits and also works on Free, and each Paid period bought through Stripe adds 250 credits to the workspace, however many projects show the flag)",
|
|
360
366
|
"project handle check":
|
|
361
367
|
"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)",
|
|
362
368
|
"project handle set":
|
|
@@ -454,6 +460,90 @@ export const cliMetadataResult = (kind: "help" | "version", topic?: string): Cli
|
|
|
454
460
|
})
|
|
455
461
|
: success("version", { status: "succeeded", cli_version: CLI_VERSION });
|
|
456
462
|
|
|
463
|
+
/**
|
|
464
|
+
* How to correct a command the parser refused. It quotes the command's usage line only, never an
|
|
465
|
+
* argument, so a secret typed into the wrong place is not echoed back.
|
|
466
|
+
*/
|
|
467
|
+
export const invalidCommandAction = (argv: readonly string[], commandPrefix: string): string => {
|
|
468
|
+
const label = recognizedCommandLabel(argv);
|
|
469
|
+
if (label === "secret set" || label === "secret delete") {
|
|
470
|
+
const action = label === "secret set" ? "set" : "delete";
|
|
471
|
+
const at = argv.findIndex((value, index) => value === "secret" && argv[index + 1] === action);
|
|
472
|
+
const name = at < 0 ? undefined : argv[at + 2];
|
|
473
|
+
if (name !== undefined && !isUserSecretName(name, action))
|
|
474
|
+
return action === "set"
|
|
475
|
+
? "Choose a customer secret name of at most 128 uppercase letters, digits and underscores, starting with a letter. OHMYHOST_* and DATABASE_URL, HYPERDRIVE, BETTER_AUTH_SECRET, BETTER_AUTH_URL, ASSETS, FILES, IMAGES and STORAGE are reserved for the platform. Rename the application's own variable and its source references; platform bindings are installed automatically. Nothing was sent; changing only the idempotency key will not help."
|
|
476
|
+
: "Choose an existing customer secret name of at most 128 uppercase letters, digits and underscores, starting with a letter. The installed platform keys OHMYHOST_STORAGE_KEY, OHMYHOST_MAIL_KEY and BETTER_AUTH_SECRET cannot be deleted. Older reserved-name collisions can be removed with secret delete; deployed platform keys stay managed by ohmyho.st. Nothing was sent; changing only the idempotency key will not help.";
|
|
477
|
+
}
|
|
478
|
+
const usage = Object.hasOwn(HELP_USAGE, label)
|
|
479
|
+
? HELP_USAGE[label as CustomerCommandLabel].split("\n")[0]
|
|
480
|
+
: undefined;
|
|
481
|
+
if (usage === undefined)
|
|
482
|
+
return "Unknown command; nothing was sent. 'ohmyhost --help --json' lists every command with its flags. If a documented command is missing, this CLI is older than https://ohmyho.st/client-release.json: install the published version.";
|
|
483
|
+
// A required --project may come from this checkout's link; an optional one never needs it.
|
|
484
|
+
const linkable = /(?:^|\s)--project ULID/u.test(usage) && !argv.includes("--project");
|
|
485
|
+
return `The arguments of '${label}' are invalid or incomplete; nothing was sent. Usage: ${usage}.${
|
|
486
|
+
linkable
|
|
487
|
+
? ` --project may be omitted only inside a directory linked with '${commandPrefix} link' for this OHMYHOST_ENVIRONMENT; otherwise pass --project ULID.`
|
|
488
|
+
: ""
|
|
489
|
+
}`;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/** The next step after an unexpected client failure; the stderr diagnostic line identifies it. */
|
|
493
|
+
export const INTERNAL_ERROR_ACTION =
|
|
494
|
+
"The ohmyhost client stopped on an unexpected error; the stderr line starting with [ohmyhost: identifies it. Compare 'ohmyhost --version' with https://ohmyho.st/client-release.json and install the published client if it is older, then run the same command again with the same --idempotency-key. If the current client fails the same way, report it with feedback submit (kind bug, error code internal_error) and quote that stderr line.";
|
|
495
|
+
|
|
496
|
+
/** The local credential store or saved-login index failed; a sandbox is the usual cause. */
|
|
497
|
+
export const CREDENTIAL_STORE_UNAVAILABLE_ACTION =
|
|
498
|
+
"This computer's credential store (for example macOS Keychain or Windows Credential Manager) or the saved-login list in ~/.ohmyhost/profiles could not be read or written; nothing was changed. If the agent runs in a sandbox, run the command with access to them before asking the customer to sign in again. If the store is locked, ask the customer to unlock it, then run the command again. On a server, container or CI runner without a credential store, do not run login: load OHMYHOST_TOKEN from a private env file with a token from the portal's API Tokens page.";
|
|
499
|
+
|
|
500
|
+
const RESPONSE_CONTRACT_INVALID_ACTION =
|
|
501
|
+
"The ohmyho.st API answered in a form this client version does not accept, usually because the client is older than the service. Compare 'ohmyhost --version' with https://ohmyho.st/client-release.json; if it is older, install the published CLI and MCP and reload MCP. Then run the same command again with the same --idempotency-key: the change may already have been applied. If the current version answers this too, report it with feedback submit (kind bug, error code response_contract_invalid).";
|
|
502
|
+
|
|
503
|
+
const REQUEST_TIMEOUT_ACTION =
|
|
504
|
+
"The ohmyho.st API did not answer within 30 seconds. Run the same command again with the same --idempotency-key; the change may already be under way, and a new key could apply it twice.";
|
|
505
|
+
|
|
506
|
+
const LOGOUT_UNCONFIRMED_ACTION =
|
|
507
|
+
"The saved login was removed from this computer, but ohmyho.st did not confirm that its session ended. Running logout again cannot retry this, because the login is gone; do not sign in again just to revoke it. Tell the customer the session end was not confirmed.";
|
|
508
|
+
|
|
509
|
+
const STATEMENT_FILE_INVALID_ACTION =
|
|
510
|
+
"Put one INSERT, UPDATE or DELETE statement that starts with that keyword (no leading comment or WITH), UTF-8, at most 64 KiB, in the file named by --statement-file, then rerun with the same --idempotency-key.";
|
|
511
|
+
|
|
512
|
+
/** The command that signs a saved login in again as the same user, organization and alias. */
|
|
513
|
+
const signInCommand = (commandPrefix: string, profile: LocalProfile | undefined): string =>
|
|
514
|
+
profile === undefined
|
|
515
|
+
? `${commandPrefix} login --json`
|
|
516
|
+
: `${commandPrefix} login${
|
|
517
|
+
profile.organizationId === null ? "" : ` --organization ${profile.organizationId}`
|
|
518
|
+
} --user ${profile.userId} --profile-name ${profile.name} --json`;
|
|
519
|
+
|
|
520
|
+
/** The sign-in service ended a saved login, which was then removed from this computer. */
|
|
521
|
+
export const endedLoginAction = (
|
|
522
|
+
commandPrefix: string,
|
|
523
|
+
profile: LocalProfile | undefined,
|
|
524
|
+
): string =>
|
|
525
|
+
`The sign-in of ${
|
|
526
|
+
profile === undefined
|
|
527
|
+
? "this computer's saved login"
|
|
528
|
+
: `saved login ${profile.name} (user ${profile.userId}${
|
|
529
|
+
profile.organizationId === null ? "" : `, organization ${profile.organizationId}`
|
|
530
|
+
})`
|
|
531
|
+
} ended and it was removed. Run '${signInCommand(commandPrefix, profile)}' to sign it in again.`;
|
|
532
|
+
|
|
533
|
+
/** Renewing a saved login failed without ending it; it was kept. */
|
|
534
|
+
export const refreshFailedAction = (
|
|
535
|
+
commandPrefix: string,
|
|
536
|
+
profile: LocalProfile | undefined,
|
|
537
|
+
): string =>
|
|
538
|
+
`Renewing the saved login${profile === undefined ? "" : ` ${profile.name}`} at the sign-in service failed. Run the same command again in a minute, with the same --idempotency-key if it has one. If it fails again, run '${signInCommand(commandPrefix, profile)}' and send the customer the link and code.`;
|
|
539
|
+
|
|
540
|
+
/** The flags a failed login is repeated with; the boundary proof needs brace-free signatures. */
|
|
541
|
+
interface LoginFlags {
|
|
542
|
+
readonly organizationId: string | undefined;
|
|
543
|
+
readonly userId: string | undefined;
|
|
544
|
+
readonly profileName: string | undefined;
|
|
545
|
+
}
|
|
546
|
+
|
|
457
547
|
class ProductCliFailure extends Error {
|
|
458
548
|
public constructor(
|
|
459
549
|
public readonly exitCode: number,
|
|
@@ -489,7 +579,7 @@ export class ProductCli {
|
|
|
489
579
|
2,
|
|
490
580
|
"invalid_command",
|
|
491
581
|
false,
|
|
492
|
-
|
|
582
|
+
invalidCommandAction(argv, this.dependencies.commandPrefix),
|
|
493
583
|
);
|
|
494
584
|
}
|
|
495
585
|
this.recordDiagnostic({
|
|
@@ -497,7 +587,13 @@ export class ProductCli {
|
|
|
497
587
|
subsystem: "parser",
|
|
498
588
|
errorKind: "unexpected",
|
|
499
589
|
});
|
|
500
|
-
return failure(
|
|
590
|
+
return failure(
|
|
591
|
+
recognizedCommandLabel(argv),
|
|
592
|
+
1,
|
|
593
|
+
"internal_error",
|
|
594
|
+
false,
|
|
595
|
+
INTERNAL_ERROR_ACTION,
|
|
596
|
+
);
|
|
501
597
|
}
|
|
502
598
|
return this.runCommand(command, signal);
|
|
503
599
|
}
|
|
@@ -788,6 +884,9 @@ export class ProductCli {
|
|
|
788
884
|
});
|
|
789
885
|
}
|
|
790
886
|
if (command.kind === "project-create") return await this.createProject(command, signal);
|
|
887
|
+
if (command.kind === "project-data-plan") return await this.planProjectData(command, signal);
|
|
888
|
+
if (command.kind === "project-data-change")
|
|
889
|
+
return await this.changeProjectData(command, signal);
|
|
791
890
|
if (command.kind === "project-list") return await this.listProjects(command, signal);
|
|
792
891
|
if (command.kind === "project-handle-check")
|
|
793
892
|
return await this.checkProjectHandle(command, signal);
|
|
@@ -871,23 +970,21 @@ export class ProductCli {
|
|
|
871
970
|
);
|
|
872
971
|
}
|
|
873
972
|
if (command.kind === "database-write") {
|
|
874
|
-
const
|
|
875
|
-
buffer = Buffer.alloc(65_537);
|
|
876
|
-
let statement: string;
|
|
973
|
+
const statement = await readStatementFile(command.statementFile);
|
|
877
974
|
try {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
975
|
+
// The statement alone decides whether the file is usable; its parameters are checked next.
|
|
976
|
+
parseProjectDatabaseWrite({
|
|
977
|
+
environment: command.environment,
|
|
978
|
+
statement,
|
|
979
|
+
parameters: [],
|
|
980
|
+
});
|
|
981
|
+
} catch {
|
|
982
|
+
throw new ProductCliFailure(
|
|
983
|
+
2,
|
|
984
|
+
"statement_file_invalid",
|
|
985
|
+
false,
|
|
986
|
+
STATEMENT_FILE_INVALID_ACTION,
|
|
987
|
+
);
|
|
891
988
|
}
|
|
892
989
|
const body = parseProjectDatabaseWrite({
|
|
893
990
|
environment: command.environment,
|
|
@@ -1173,7 +1270,13 @@ export class ProductCli {
|
|
|
1173
1270
|
expiresAt,
|
|
1174
1271
|
};
|
|
1175
1272
|
const account = await this.dependencies.accountProfile(async () => signedIn.accessToken);
|
|
1176
|
-
|
|
1273
|
+
// A failed verification repeats this login with the flags it was given.
|
|
1274
|
+
const loginFlags = {
|
|
1275
|
+
organizationId,
|
|
1276
|
+
userId: expectedUserId,
|
|
1277
|
+
profileName: this.dependencies.profileName,
|
|
1278
|
+
};
|
|
1279
|
+
if (!isProfileUserId(account.userId)) throw this.unverifiedLogin(loginFlags);
|
|
1177
1280
|
if (expectedUserId !== undefined && account.userId !== expectedUserId)
|
|
1178
1281
|
throw new ProductCliFailure(
|
|
1179
1282
|
4,
|
|
@@ -1206,11 +1309,11 @@ export class ProductCli {
|
|
|
1206
1309
|
scopedId === null
|
|
1207
1310
|
? null
|
|
1208
1311
|
: account.organizations.find((candidate) => candidate.id === scopedId);
|
|
1209
|
-
if (organization === undefined) throw this.unverifiedLogin();
|
|
1312
|
+
if (organization === undefined) throw this.unverifiedLogin(loginFlags);
|
|
1210
1313
|
try {
|
|
1211
1314
|
assertVerifiedLogin(identity, account.userId, scopedId);
|
|
1212
1315
|
} catch (error) {
|
|
1213
|
-
if (error instanceof AccountVerificationError) throw this.unverifiedLogin();
|
|
1316
|
+
if (error instanceof AccountVerificationError) throw this.unverifiedLogin(loginFlags);
|
|
1214
1317
|
throw error;
|
|
1215
1318
|
}
|
|
1216
1319
|
const login = {
|
|
@@ -1276,12 +1379,16 @@ export class ProductCli {
|
|
|
1276
1379
|
}
|
|
1277
1380
|
|
|
1278
1381
|
/** The provider answered with another account than the one this login requested. */
|
|
1279
|
-
private unverifiedLogin(): ProductCliFailure {
|
|
1382
|
+
private unverifiedLogin(flags: LoginFlags): ProductCliFailure {
|
|
1280
1383
|
return new ProductCliFailure(
|
|
1281
1384
|
4,
|
|
1282
1385
|
"login_verification_failed",
|
|
1283
1386
|
true,
|
|
1284
|
-
`The signed-in account could not be verified; nothing was saved. Run '${this.dependencies.commandPrefix} login
|
|
1387
|
+
`The signed-in account could not be verified; nothing was saved. Run '${this.dependencies.commandPrefix} login${
|
|
1388
|
+
flags.organizationId === undefined ? "" : ` --organization ${flags.organizationId}`
|
|
1389
|
+
}${flags.userId === undefined ? "" : ` --user ${flags.userId}`}${
|
|
1390
|
+
flags.profileName === undefined ? "" : ` --profile-name ${flags.profileName}`
|
|
1391
|
+
} --json' again with the same flags and send the customer the new link and code.`,
|
|
1285
1392
|
);
|
|
1286
1393
|
}
|
|
1287
1394
|
|
|
@@ -1317,7 +1424,12 @@ export class ProductCli {
|
|
|
1317
1424
|
this.dependencies.lifecycleTimeoutMs,
|
|
1318
1425
|
);
|
|
1319
1426
|
} catch (error) {
|
|
1320
|
-
if (error instanceof AccountVerificationError)
|
|
1427
|
+
if (error instanceof AccountVerificationError)
|
|
1428
|
+
throw this.unverifiedLogin({
|
|
1429
|
+
organizationId: organization.id,
|
|
1430
|
+
userId: profile.userId,
|
|
1431
|
+
profileName: profile.name,
|
|
1432
|
+
});
|
|
1321
1433
|
if (!(error instanceof ProfileOrganizationFixedError)) throw error;
|
|
1322
1434
|
throw new ProductCliFailure(
|
|
1323
1435
|
2,
|
|
@@ -1375,21 +1487,12 @@ export class ProductCli {
|
|
|
1375
1487
|
"Remote token revocation is not available in this release.",
|
|
1376
1488
|
);
|
|
1377
1489
|
}
|
|
1490
|
+
// The login is already gone, so nothing this computer holds can repeat the revocation.
|
|
1378
1491
|
if (isTimeout(revocationError)) {
|
|
1379
|
-
throw new ProductCliFailure(
|
|
1380
|
-
8,
|
|
1381
|
-
"request_timeout",
|
|
1382
|
-
true,
|
|
1383
|
-
"Remote revocation timed out; local credentials were removed.",
|
|
1384
|
-
);
|
|
1492
|
+
throw new ProductCliFailure(8, "request_timeout", false, LOGOUT_UNCONFIRMED_ACTION);
|
|
1385
1493
|
}
|
|
1386
1494
|
if (revocationError instanceof LifecycleTransportError) {
|
|
1387
|
-
throw new ProductCliFailure(
|
|
1388
|
-
6,
|
|
1389
|
-
"token_revocation_failed",
|
|
1390
|
-
true,
|
|
1391
|
-
"Remote revocation failed; the local credential was removed.",
|
|
1392
|
-
);
|
|
1495
|
+
throw new ProductCliFailure(6, "token_revocation_failed", false, LOGOUT_UNCONFIRMED_ACTION);
|
|
1393
1496
|
}
|
|
1394
1497
|
throw revocationError;
|
|
1395
1498
|
}
|
|
@@ -1446,6 +1549,38 @@ export class ProductCli {
|
|
|
1446
1549
|
return success("operation reconcile", { status: "accepted", reconciliation });
|
|
1447
1550
|
}
|
|
1448
1551
|
|
|
1552
|
+
private async planProjectData(
|
|
1553
|
+
command: Extract<ProductCliCommand, { kind: "project-data-plan" }>,
|
|
1554
|
+
signal: AbortSignal,
|
|
1555
|
+
): Promise<CliResult> {
|
|
1556
|
+
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
1557
|
+
return success("project data plan", {
|
|
1558
|
+
status: "planned",
|
|
1559
|
+
plan: parseProjectDataChangePlan(
|
|
1560
|
+
await api.planProjectDataChange({ projectId: command.projectId, change: command.change }),
|
|
1561
|
+
),
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
private async changeProjectData(
|
|
1565
|
+
command: Extract<ProductCliCommand, { kind: "project-data-change" }>,
|
|
1566
|
+
signal: AbortSignal,
|
|
1567
|
+
): Promise<CliResult> {
|
|
1568
|
+
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
1569
|
+
let operation = parseOperation(
|
|
1570
|
+
await api.createProjectDataChange({
|
|
1571
|
+
projectId: command.projectId,
|
|
1572
|
+
change: command.change,
|
|
1573
|
+
ifMatch: command.ifMatch,
|
|
1574
|
+
confirmationToken: command.confirmationToken,
|
|
1575
|
+
idempotencyKey: command.idempotencyKey,
|
|
1576
|
+
}),
|
|
1577
|
+
);
|
|
1578
|
+
if (command.wait) operation = await this.waitForOperation(api, operation, signal);
|
|
1579
|
+
return success("project data change", {
|
|
1580
|
+
status: command.wait ? "completed" : "accepted",
|
|
1581
|
+
operation,
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1449
1584
|
private async createProject(
|
|
1450
1585
|
command: Extract<ProductCliCommand, { kind: "project-create" }>,
|
|
1451
1586
|
signal: AbortSignal,
|
|
@@ -1833,13 +1968,33 @@ export class ProductCli {
|
|
|
1833
1968
|
environment: command.environment,
|
|
1834
1969
|
}),
|
|
1835
1970
|
);
|
|
1836
|
-
let
|
|
1837
|
-
|
|
1971
|
+
let accepted: unknown;
|
|
1972
|
+
try {
|
|
1973
|
+
accepted = await api.createDeployment({
|
|
1838
1974
|
projectId: command.projectId,
|
|
1839
1975
|
planId: plan === null ? String(command.planId) : String(plan["id"]),
|
|
1840
1976
|
idempotencyKey: command.idempotencyKey,
|
|
1841
|
-
})
|
|
1842
|
-
)
|
|
1977
|
+
});
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
// A rerun of --commit re-plans, so its key meets a new plan ID; a new key would build the
|
|
1980
|
+
// same commit a second time. The deployment the key started is followed instead.
|
|
1981
|
+
const reused = plan === null ? undefined : reusedKeyProblem(error);
|
|
1982
|
+
if (reused === undefined) throw error;
|
|
1983
|
+
return failure(
|
|
1984
|
+
"deploy",
|
|
1985
|
+
7,
|
|
1986
|
+
reused.code,
|
|
1987
|
+
false,
|
|
1988
|
+
`This key already started a deployment from an earlier run of this command; a new key would build the same commit again. Run 'ohmyhost project status --project ${command.projectId} --json' and follow latest_operation with 'ohmyhost operation get OPERATION_ULID --json'.`,
|
|
1989
|
+
undefined,
|
|
1990
|
+
undefined,
|
|
1991
|
+
reused.requestId,
|
|
1992
|
+
reused.docsUrl,
|
|
1993
|
+
undefined,
|
|
1994
|
+
reused.message,
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
let operation = parseOperation(accepted);
|
|
1843
1998
|
if (command.wait) operation = await this.waitForOperation(api, operation, signal);
|
|
1844
1999
|
// Dev access follows the project-selected mode; the clean URL alone is insufficient when protected.
|
|
1845
2000
|
const notes =
|
|
@@ -1861,35 +2016,62 @@ export class ProductCli {
|
|
|
1861
2016
|
signal: AbortSignal,
|
|
1862
2017
|
): Promise<CliResult> {
|
|
1863
2018
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
2019
|
+
// The operation read answers an unknown, foreign or mistyped ID before any stream opens.
|
|
2020
|
+
let operation = parseOperation(await api.getOperation(command.operationId));
|
|
1864
2021
|
let eventCount = 0;
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2022
|
+
let lastEventId: string | undefined;
|
|
2023
|
+
for (let connection = 0; connection < OPERATION_STREAM_CONNECTIONS; connection += 1) {
|
|
2024
|
+
if (connection > 0)
|
|
2025
|
+
await this.dependencies.waitUntil(
|
|
2026
|
+
new Date(this.dependencies.now().getTime() + 1_000),
|
|
2027
|
+
signal,
|
|
2028
|
+
);
|
|
2029
|
+
const events =
|
|
2030
|
+
lastEventId === undefined
|
|
2031
|
+
? await api.streamOperationEvents(command.operationId, signal)
|
|
2032
|
+
: await api.streamOperationEvents(command.operationId, signal, lastEventId);
|
|
2033
|
+
for await (const event of events) {
|
|
2034
|
+
const parsed = parseOperationEvent(event);
|
|
2035
|
+
this.dependencies.operationEventSink?.write(`${JSON.stringify(parsed)}\n`);
|
|
2036
|
+
eventCount += 1;
|
|
2037
|
+
lastEventId = String(parsed["event_id"]);
|
|
2038
|
+
if (
|
|
2039
|
+
parsed["type"] === "OperationSucceeded" ||
|
|
2040
|
+
parsed["type"] === "OperationFailed" ||
|
|
2041
|
+
parsed["type"] === "OperationCancelled"
|
|
2042
|
+
)
|
|
2043
|
+
return success("logs", { status: "completed", event_count: eventCount });
|
|
1875
2044
|
}
|
|
2045
|
+
throwIfAborted(signal);
|
|
2046
|
+
// The server also closes a stream without a final event, for example when an interactive
|
|
2047
|
+
// sign-in expires. Only the operation itself says whether it ended meanwhile.
|
|
2048
|
+
operation = parseOperation(await api.getOperation(command.operationId));
|
|
2049
|
+
if (operation.state !== "queued" && operation.state !== "running")
|
|
2050
|
+
return success("logs", { status: "completed", event_count: eventCount, operation });
|
|
1876
2051
|
}
|
|
1877
|
-
|
|
2052
|
+
throw new ProductCliFailure(
|
|
2053
|
+
8,
|
|
2054
|
+
"operation_stream_ended",
|
|
2055
|
+
true,
|
|
2056
|
+
`The event stream ended before operation ${command.operationId} finished, and it is still running; nothing was cancelled. Poll 'ohmyhost operation get ${command.operationId} --json' about every 60 seconds until it is succeeded, failed or cancelled.`,
|
|
2057
|
+
operation,
|
|
2058
|
+
);
|
|
1878
2059
|
}
|
|
1879
2060
|
private async deploymentLogs(
|
|
1880
2061
|
command: Extract<ProductCliCommand, { kind: "deployment-logs" }>,
|
|
1881
2062
|
signal: AbortSignal,
|
|
1882
2063
|
): Promise<CliResult> {
|
|
1883
2064
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2065
|
+
// The diagnostics are one bounded page; a plain read answers a missing deployment at once,
|
|
2066
|
+
// where its event stream retried a 404 without end.
|
|
2067
|
+
const page = await api.getDeploymentLogs({
|
|
2068
|
+
projectId: command.projectId,
|
|
2069
|
+
deploymentId: command.deploymentId,
|
|
2070
|
+
limit: 100,
|
|
2071
|
+
});
|
|
2072
|
+
for (const item of page.items)
|
|
2073
|
+
this.dependencies.operationEventSink?.write(`${JSON.stringify(item)}\n`);
|
|
2074
|
+
return success("deployment logs", { status: "completed", event_count: page.items.length });
|
|
1893
2075
|
}
|
|
1894
2076
|
|
|
1895
2077
|
private async planRollback(
|
|
@@ -2016,7 +2198,7 @@ export class ProductCli {
|
|
|
2016
2198
|
)
|
|
2017
2199
|
: secret;
|
|
2018
2200
|
return success("secret set", {
|
|
2019
|
-
status: command.wait ? "ready" : "stored",
|
|
2201
|
+
status: command.wait && completed.delivery_state === "ready" ? "ready" : "stored",
|
|
2020
2202
|
secret: completed,
|
|
2021
2203
|
});
|
|
2022
2204
|
} finally {
|
|
@@ -2047,7 +2229,7 @@ export class ProductCli {
|
|
|
2047
2229
|
)
|
|
2048
2230
|
: deleted;
|
|
2049
2231
|
return success("secret delete", {
|
|
2050
|
-
status: command.wait ? "ready" : deleted.status,
|
|
2232
|
+
status: command.wait && delivery.delivery_state === "ready" ? "ready" : deleted.status,
|
|
2051
2233
|
secret: { name: deleted.name, ...secretDeliveryProjection(delivery) },
|
|
2052
2234
|
});
|
|
2053
2235
|
}
|
|
@@ -2069,13 +2251,16 @@ export class ProductCli {
|
|
|
2069
2251
|
10,
|
|
2070
2252
|
"secret_delivery_failed",
|
|
2071
2253
|
true,
|
|
2072
|
-
|
|
2254
|
+
`The secret is saved, but delivering it to the running app failed (last_error in 'ohmyhost secret list --project ${projectId} --environment ${environmentId} --json'). Do not set the value again: ohmyho.st retries delivery automatically, and the next deployment of this environment also delivers it. Read secret list again in a few minutes; if delivery_state stays failed, report last_error with feedback submit (kind bug, error code secret_delivery_failed).`,
|
|
2073
2255
|
);
|
|
2074
2256
|
}
|
|
2257
|
+
// Before an environment's first deployment nothing runs to deliver to: the value arrives
|
|
2258
|
+
// with that deployment, so waiting could only time out.
|
|
2075
2259
|
if (
|
|
2076
|
-
page.delivery_state === "
|
|
2077
|
-
page.
|
|
2078
|
-
|
|
2260
|
+
page.delivery_state === "not_deployed" ||
|
|
2261
|
+
(page.delivery_state === "ready" &&
|
|
2262
|
+
page.desired_generation >= desiredGeneration &&
|
|
2263
|
+
page.applied_generation >= desiredGeneration)
|
|
2079
2264
|
) {
|
|
2080
2265
|
// Every item carries the environment's generations; only the item with the stored
|
|
2081
2266
|
// secret's name describes the secret that was just set. A deletion has no item left
|
|
@@ -2099,7 +2284,7 @@ export class ProductCli {
|
|
|
2099
2284
|
8,
|
|
2100
2285
|
"secret_delivery_wait_timeout",
|
|
2101
2286
|
true,
|
|
2102
|
-
|
|
2287
|
+
`The secret is saved, but it had not reached the running app after about 2 minutes. Read 'ohmyhost secret list --project ${projectId} --environment ${environmentId} --json': pending means delivery is still under way, and not_deployed means it arrives with the first deployment. Do not set the value again.`,
|
|
2103
2288
|
);
|
|
2104
2289
|
}
|
|
2105
2290
|
|
|
@@ -2135,7 +2320,7 @@ export class ProductCli {
|
|
|
2135
2320
|
8,
|
|
2136
2321
|
"operation_wait_timeout",
|
|
2137
2322
|
true,
|
|
2138
|
-
`
|
|
2323
|
+
`The operation is still running; this command stopped waiting after about 2 minutes and cancelled nothing. Poll 'ohmyhost operation get ${operation.id} --json' about every 60 seconds until it is succeeded, failed or cancelled. Do not submit the same change again; a new key starts a second operation.`,
|
|
2139
2324
|
operation,
|
|
2140
2325
|
);
|
|
2141
2326
|
if (operation.state === "failed" && operation.error !== undefined)
|
|
@@ -2181,8 +2366,14 @@ export class ProductCli {
|
|
|
2181
2366
|
} catch (error) {
|
|
2182
2367
|
if (!(error instanceof TokenRefreshRejectedError)) throw error;
|
|
2183
2368
|
// The provider ended this login; only this saved login is removed.
|
|
2369
|
+
const ended = this.#profile;
|
|
2184
2370
|
await this.forgetLogin(store);
|
|
2185
|
-
throw new ProductCliFailure(
|
|
2371
|
+
throw new ProductCliFailure(
|
|
2372
|
+
5,
|
|
2373
|
+
"authentication_required",
|
|
2374
|
+
false,
|
|
2375
|
+
endedLoginAction(this.dependencies.commandPrefix, ended),
|
|
2376
|
+
);
|
|
2186
2377
|
}
|
|
2187
2378
|
if (refresh.refreshed) await store.replace(refresh.credential);
|
|
2188
2379
|
return refresh.credential.accessToken;
|
|
@@ -2239,14 +2430,14 @@ export class ProductCli {
|
|
|
2239
2430
|
);
|
|
2240
2431
|
}
|
|
2241
2432
|
if (isTimeout(error)) {
|
|
2242
|
-
throw new ProductCliFailure(8, "request_timeout", true,
|
|
2433
|
+
throw new ProductCliFailure(8, "request_timeout", true, REQUEST_TIMEOUT_ACTION);
|
|
2243
2434
|
}
|
|
2244
2435
|
if (error instanceof LifecycleTransportError) {
|
|
2245
2436
|
throw new ProductCliFailure(
|
|
2246
2437
|
5,
|
|
2247
2438
|
"token_refresh_failed",
|
|
2248
2439
|
true,
|
|
2249
|
-
|
|
2440
|
+
refreshFailedAction(this.dependencies.commandPrefix, this.#profile),
|
|
2250
2441
|
);
|
|
2251
2442
|
}
|
|
2252
2443
|
throw error;
|
|
@@ -2322,10 +2513,16 @@ const classifyFailure = (
|
|
|
2322
2513
|
actingAs?: Readonly<Record<string, unknown>>,
|
|
2323
2514
|
): CliResult => {
|
|
2324
2515
|
if (signal.aborted || isCancellation(error)) {
|
|
2325
|
-
return failure(
|
|
2516
|
+
return failure(
|
|
2517
|
+
command,
|
|
2518
|
+
130,
|
|
2519
|
+
"command_cancelled",
|
|
2520
|
+
true,
|
|
2521
|
+
"The command was stopped by an interrupt or termination signal, for example the shell's command timeout, before it finished. Run it again with the same --idempotency-key; a change may already be under way. Give login and commands with --wait or --follow several minutes, or run them in the background.",
|
|
2522
|
+
);
|
|
2326
2523
|
}
|
|
2327
2524
|
if (isTimeout(error)) {
|
|
2328
|
-
return failure(command, 8, "request_timeout", true,
|
|
2525
|
+
return failure(command, 8, "request_timeout", true, REQUEST_TIMEOUT_ACTION);
|
|
2329
2526
|
}
|
|
2330
2527
|
if (error instanceof ProductCliFailure) {
|
|
2331
2528
|
return failure(
|
|
@@ -2343,25 +2540,45 @@ const classifyFailure = (
|
|
|
2343
2540
|
2,
|
|
2344
2541
|
"secret_input_invalid",
|
|
2345
2542
|
false,
|
|
2346
|
-
|
|
2543
|
+
command === "export create"
|
|
2544
|
+
? "Pipe the ZIP password through standard input: non-blank UTF-8 of at most 1024 bytes. A final line break becomes part of the password."
|
|
2545
|
+
: "Pipe a non-empty UTF-8 value of at most 5120 bytes through standard input.",
|
|
2347
2546
|
);
|
|
2348
2547
|
}
|
|
2349
2548
|
if (error instanceof WorkosContractError) {
|
|
2350
2549
|
if (error.code === "access_denied") {
|
|
2351
|
-
return failure(
|
|
2550
|
+
return failure(
|
|
2551
|
+
command,
|
|
2552
|
+
4,
|
|
2553
|
+
"device_authorization_denied",
|
|
2554
|
+
false,
|
|
2555
|
+
"The customer declined the sign-in in the browser; nothing was saved. Ask the customer whether to sign in, then run the same login command again and send them the new link and code.",
|
|
2556
|
+
);
|
|
2352
2557
|
}
|
|
2353
2558
|
if (error.code === "expired_token") {
|
|
2354
|
-
return failure(
|
|
2559
|
+
return failure(
|
|
2560
|
+
command,
|
|
2561
|
+
4,
|
|
2562
|
+
"device_authorization_expired",
|
|
2563
|
+
false,
|
|
2564
|
+
"Nobody finished the sign-in within 30 minutes; nothing was saved. Run the same login command again and send the customer the new link and code at once.",
|
|
2565
|
+
);
|
|
2355
2566
|
}
|
|
2356
|
-
return failure(
|
|
2567
|
+
return failure(
|
|
2568
|
+
command,
|
|
2569
|
+
4,
|
|
2570
|
+
"device_authorization_failed",
|
|
2571
|
+
true,
|
|
2572
|
+
"The sign-in service answered unexpectedly; nothing was saved. Run the same login command again; if it fails twice more, ask the customer to try again later.",
|
|
2573
|
+
);
|
|
2357
2574
|
}
|
|
2358
2575
|
if (error instanceof CredentialStoreUnavailableError) {
|
|
2359
2576
|
return failure(
|
|
2360
2577
|
command,
|
|
2361
2578
|
3,
|
|
2362
2579
|
"credential_store_unavailable",
|
|
2363
|
-
|
|
2364
|
-
|
|
2580
|
+
false,
|
|
2581
|
+
CREDENTIAL_STORE_UNAVAILABLE_ACTION,
|
|
2365
2582
|
);
|
|
2366
2583
|
}
|
|
2367
2584
|
if (error instanceof ResponseContractError) {
|
|
@@ -2370,7 +2587,7 @@ const classifyFailure = (
|
|
|
2370
2587
|
7,
|
|
2371
2588
|
"response_contract_invalid",
|
|
2372
2589
|
false,
|
|
2373
|
-
|
|
2590
|
+
RESPONSE_CONTRACT_INVALID_ACTION,
|
|
2374
2591
|
);
|
|
2375
2592
|
}
|
|
2376
2593
|
if (error instanceof PublicResponseTooLargeError) {
|
|
@@ -2396,6 +2613,7 @@ const classifyFailure = (
|
|
|
2396
2613
|
problem.requestId,
|
|
2397
2614
|
problem.docsUrl,
|
|
2398
2615
|
ACCOUNT_SENSITIVE_PROBLEMS.has(problem.code) ? actingAs : undefined,
|
|
2616
|
+
problem.message,
|
|
2399
2617
|
);
|
|
2400
2618
|
} catch {
|
|
2401
2619
|
return failure(
|
|
@@ -2403,7 +2621,7 @@ const classifyFailure = (
|
|
|
2403
2621
|
7,
|
|
2404
2622
|
"response_contract_invalid",
|
|
2405
2623
|
false,
|
|
2406
|
-
|
|
2624
|
+
RESPONSE_CONTRACT_INVALID_ACTION,
|
|
2407
2625
|
);
|
|
2408
2626
|
}
|
|
2409
2627
|
}
|
|
@@ -2413,13 +2631,67 @@ const classifyFailure = (
|
|
|
2413
2631
|
7,
|
|
2414
2632
|
"response_contract_invalid",
|
|
2415
2633
|
false,
|
|
2416
|
-
|
|
2634
|
+
RESPONSE_CONTRACT_INVALID_ACTION,
|
|
2417
2635
|
);
|
|
2418
2636
|
}
|
|
2419
2637
|
if (error instanceof NetworkTransportError) {
|
|
2420
|
-
return failure(
|
|
2638
|
+
return failure(
|
|
2639
|
+
command,
|
|
2640
|
+
8,
|
|
2641
|
+
"network_error",
|
|
2642
|
+
true,
|
|
2643
|
+
"The request did not reach the ohmyho.st API, or its answer was cut off. If the agent runs in a sandbox without network access, run the command with network access. Otherwise run the same command again with the same --idempotency-key; the change may already have been applied, and a new key could apply it twice.",
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
return failure(command, 1, "internal_error", false, INTERNAL_ERROR_ACTION);
|
|
2647
|
+
};
|
|
2648
|
+
|
|
2649
|
+
/** A 409 idempotency_key_reused problem, with what its answer carries. */
|
|
2650
|
+
const reusedKeyProblem = (error: unknown): ReturnType<typeof parseSafeProblem> | undefined => {
|
|
2651
|
+
if (!looksLikeProblem(error)) return undefined;
|
|
2652
|
+
try {
|
|
2653
|
+
const problem = parseSafeProblem(error);
|
|
2654
|
+
return problem.code === "idempotency_key_reused" ? problem : undefined;
|
|
2655
|
+
} catch {
|
|
2656
|
+
return undefined;
|
|
2657
|
+
}
|
|
2658
|
+
};
|
|
2659
|
+
|
|
2660
|
+
/** One stream and at most three reconnects before an unfinished operation is handed back. */
|
|
2661
|
+
const OPERATION_STREAM_CONNECTIONS = 4;
|
|
2662
|
+
|
|
2663
|
+
/**
|
|
2664
|
+
* The single DML statement of `database write --statement-file`: a regular UTF-8 file of at most
|
|
2665
|
+
* 64 KiB. Anything else, including a missing file, is refused before any request.
|
|
2666
|
+
*/
|
|
2667
|
+
const readStatementFile = async (path: string): Promise<string> => {
|
|
2668
|
+
const invalid = () =>
|
|
2669
|
+
new ProductCliFailure(2, "statement_file_invalid", false, STATEMENT_FILE_INVALID_ACTION);
|
|
2670
|
+
let file: Awaited<ReturnType<typeof open>>;
|
|
2671
|
+
try {
|
|
2672
|
+
file = await open(path, "r");
|
|
2673
|
+
} catch {
|
|
2674
|
+
throw invalid();
|
|
2675
|
+
}
|
|
2676
|
+
const buffer = Buffer.alloc(65_537);
|
|
2677
|
+
try {
|
|
2678
|
+
const stat = await file.stat();
|
|
2679
|
+
if (!stat.isFile() || stat.size > 65_536) throw invalid();
|
|
2680
|
+
let length = 0;
|
|
2681
|
+
while (length < buffer.length) {
|
|
2682
|
+
const read = await file.read(buffer, length, buffer.length - length, null);
|
|
2683
|
+
if (read.bytesRead === 0) break;
|
|
2684
|
+
length += read.bytesRead;
|
|
2685
|
+
}
|
|
2686
|
+
if (length > 65_536) throw invalid();
|
|
2687
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, length));
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
if (error instanceof ProductCliFailure) throw error;
|
|
2690
|
+
throw invalid();
|
|
2691
|
+
} finally {
|
|
2692
|
+
buffer.fill(0);
|
|
2693
|
+
await file.close();
|
|
2421
2694
|
}
|
|
2422
|
-
return failure(command, 1, "internal_error", false, "Retry the command.");
|
|
2423
2695
|
};
|
|
2424
2696
|
|
|
2425
2697
|
const psqlUnavailable = (): CliResult =>
|
|
@@ -2448,6 +2720,7 @@ const failure = (
|
|
|
2448
2720
|
requestId?: string,
|
|
2449
2721
|
docsUrl?: string,
|
|
2450
2722
|
actingAs?: Readonly<Record<string, unknown>>,
|
|
2723
|
+
message?: string,
|
|
2451
2724
|
): CliResult => {
|
|
2452
2725
|
const document: CliErrorDocument = {
|
|
2453
2726
|
version: 1,
|
|
@@ -2462,6 +2735,7 @@ const failure = (
|
|
|
2462
2735
|
...(requestId === undefined ? {} : { request_id: requestId }),
|
|
2463
2736
|
...(docsUrl === undefined ? {} : { docs_url: docsUrl }),
|
|
2464
2737
|
...(operation?.error === undefined ? {} : { message: operation.error.message }),
|
|
2738
|
+
...(message === undefined ? {} : { message }),
|
|
2465
2739
|
...(actingAs === undefined ? {} : { acting_as: actingAs }),
|
|
2466
2740
|
},
|
|
2467
2741
|
};
|
|
@@ -2490,6 +2764,8 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
|
|
|
2490
2764
|
if (command.kind === "feedback-submit") return "feedback submit";
|
|
2491
2765
|
if (command.kind === "feedback-status") return "feedback status";
|
|
2492
2766
|
if (command.kind === "project-create") return "project create";
|
|
2767
|
+
if (command.kind === "project-data-plan") return "project data plan";
|
|
2768
|
+
if (command.kind === "project-data-change") return "project data change";
|
|
2493
2769
|
if (command.kind === "organization-create") return "organization create";
|
|
2494
2770
|
if (command.kind === "organization-list") return "organization list";
|
|
2495
2771
|
if (command.kind === "organization-use") return "organization use";
|