@amerged/ohmyhost-mcp 0.1.19 → 0.1.21
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 +1 -1
- package/dist/{chunk-PXGLL6SR.js → chunk-4WY7AAPR.js} +11 -11
- package/dist/index.js +1 -1
- package/dist/stdio-main.js +1575 -310
- package/package.json +1 -1
- package/src/apps/mcp/local-client.ts +119 -17
- package/src/apps/mcp/local-server.ts +321 -88
- package/src/apps/mcp/stdio-main.ts +11 -2
- package/src/apps/product-cli/cli.ts +370 -92
- package/src/apps/product-cli/command.ts +163 -6
- package/src/apps/product-cli/composition.ts +748 -120
- package/src/apps/product-cli/credential-store.ts +5 -3
- package/src/apps/product-cli/index.ts +4 -0
- package/src/apps/product-cli/product-api.ts +32 -4
- package/src/apps/product-cli/profile-store.ts +609 -0
- package/src/apps/product-cli/project-link-store.ts +169 -20
- package/src/apps/product-cli/runtime-contract.ts +63 -7
- package/src/apps/product-cli/workspace.ts +75 -13
- package/src/packages/agent-skills/generated-skill-resources.ts +10 -10
- package/src/packages/contracts/operation-failure.ts +3 -3
- package/src/packages/sdk-ts/generated/index.ts +21 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +142 -0
- package/src/packages/sdk-ts/generated/types.gen.ts +200 -2
|
@@ -43,17 +43,28 @@ import {
|
|
|
43
43
|
type StoredCredential,
|
|
44
44
|
} from "./credential-store.js";
|
|
45
45
|
import type { ProductApi } from "./product-api.js";
|
|
46
|
+
import {
|
|
47
|
+
defaultProfileName,
|
|
48
|
+
isProfileUserId,
|
|
49
|
+
ProfileLimitError,
|
|
50
|
+
profileLimitAction,
|
|
51
|
+
ProfileNameConflictError,
|
|
52
|
+
publicProfile,
|
|
53
|
+
type LocalProfile,
|
|
54
|
+
type ProfileRegistry,
|
|
55
|
+
} from "./profile-store.js";
|
|
46
56
|
import type { ProjectLinkStore } from "./project-link-store.js";
|
|
47
57
|
import { DeviceTokenExpiryError, resolveDeviceTokenExpiresAt } from "./device-token-expiry.js";
|
|
48
58
|
import {
|
|
59
|
+
AccountVerificationError,
|
|
60
|
+
assertVerifiedLogin,
|
|
49
61
|
chooseWorkspace,
|
|
50
62
|
InteractiveSessionRequiredError,
|
|
63
|
+
ProfileOrganizationFixedError,
|
|
51
64
|
publicWorkspace,
|
|
52
65
|
selectWorkspace,
|
|
53
|
-
selectedWorkspaceId,
|
|
54
66
|
workspaceOf,
|
|
55
67
|
type WorkspaceSelection,
|
|
56
|
-
type WorkspaceSession,
|
|
57
68
|
} from "./workspace.js";
|
|
58
69
|
import {
|
|
59
70
|
parseCloudflareDnsAuthorization,
|
|
@@ -63,11 +74,14 @@ import {
|
|
|
63
74
|
parseDeploymentPlan,
|
|
64
75
|
parseCurrentIdentity,
|
|
65
76
|
parseOrganization,
|
|
77
|
+
type PublicAccount,
|
|
66
78
|
type PublicWorkspace,
|
|
67
79
|
parseOrganizationCredits,
|
|
68
80
|
parseProjectCreditBudget,
|
|
69
81
|
parseDevAccessTicket,
|
|
70
82
|
parseDevAccessState,
|
|
83
|
+
parsePoweredByFlag,
|
|
84
|
+
parseOrganizationReferral,
|
|
71
85
|
parseGuardedActionPlan,
|
|
72
86
|
parseEnvironmentSecret,
|
|
73
87
|
parseEnvironmentSecretDeletion,
|
|
@@ -198,7 +212,18 @@ export class PublicResponseTooLargeError extends Error {
|
|
|
198
212
|
export interface ProductCliDependencies {
|
|
199
213
|
readonly apiToken?: string;
|
|
200
214
|
readonly commandPrefix: string;
|
|
201
|
-
|
|
215
|
+
/** One credential store for a run without saved logins; a chosen saved login uses its own. */
|
|
216
|
+
readonly credentialStore?: (kind: "native") => CredentialStore;
|
|
217
|
+
/** Saved interactive logins; login, logout, profile list and workspace binding need them. */
|
|
218
|
+
readonly profiles?: ProfileRegistry;
|
|
219
|
+
/** The saved login this run acts as, chosen before the command started and never by another run. */
|
|
220
|
+
readonly profile?: LocalProfile;
|
|
221
|
+
/** The alias `login` gives the login it adds. */
|
|
222
|
+
readonly profileName?: string;
|
|
223
|
+
/** The OHMYHOST_PROFILE binding of this process, reported by `profile list`. */
|
|
224
|
+
readonly profileBinding?: string;
|
|
225
|
+
/** Read-only `GET /v1/me`, which verifies a fresh login before anything is stored. */
|
|
226
|
+
readonly currentIdentity?: (accessToken: () => Promise<string>) => Promise<unknown>;
|
|
202
227
|
readonly deviceFlow: WorkosDeviceFlowClient;
|
|
203
228
|
readonly deviceAuthorizationPresenter: DeviceAuthorizationPresenter;
|
|
204
229
|
readonly productApi: (accessToken: () => Promise<string>) => ProductApi;
|
|
@@ -206,9 +231,7 @@ export interface ProductCliDependencies {
|
|
|
206
231
|
* Reads the signed-in customer's workspaces through a separate read-only dependency,
|
|
207
232
|
* keeping product mutation capabilities out of login.
|
|
208
233
|
*/
|
|
209
|
-
readonly accountProfile: (
|
|
210
|
-
accessToken: () => Promise<string>,
|
|
211
|
-
) => Promise<readonly PublicWorkspace[]>;
|
|
234
|
+
readonly accountProfile: (accessToken: () => Promise<string>) => Promise<PublicAccount>;
|
|
212
235
|
/** Optional onboarding guidance; production supplies only this one read-only SDK query. */
|
|
213
236
|
readonly githubConnectionStatus?: (
|
|
214
237
|
accessToken: () => Promise<string>,
|
|
@@ -256,6 +279,8 @@ interface CliErrorDocument {
|
|
|
256
279
|
readonly retry_after_seconds?: number;
|
|
257
280
|
readonly request_id?: string;
|
|
258
281
|
readonly docs_url?: string;
|
|
282
|
+
/** The account the command ran as, for answers that depend on which account that is. */
|
|
283
|
+
readonly acting_as?: Readonly<Record<string, unknown>>;
|
|
259
284
|
};
|
|
260
285
|
}
|
|
261
286
|
|
|
@@ -265,9 +290,14 @@ type CustomerCommandLabel = Exclude<CommandLabel, "unknown" | "help" | "version"
|
|
|
265
290
|
const CLI_VERSION = cliPackage.version;
|
|
266
291
|
const HELP_USAGE = Object.freeze({
|
|
267
292
|
init: "ohmyhost init [--directory PATH] [--root PATH] [--project SLUG] [--region us|eu] [--dry-run] --json (pass the project's hosting region so storage.jurisdiction matches it; us when omitted)",
|
|
268
|
-
login:
|
|
269
|
-
|
|
270
|
-
|
|
293
|
+
login:
|
|
294
|
+
"ohmyhost login [--organization ULID] [--user USER_ID] [--profile-name NAME] --json (adds one saved login; nothing is saved unless the browser signed in as that user and organization)",
|
|
295
|
+
logout:
|
|
296
|
+
"ohmyhost logout [--profile-name NAME] [--revoke] --json (removes only the selected saved login)",
|
|
297
|
+
whoami:
|
|
298
|
+
"ohmyhost whoami [--profile-name NAME] --json (the effective user, organization and saved login)",
|
|
299
|
+
"profile list":
|
|
300
|
+
"ohmyhost profile list --json (saved logins on this computer: name, user and organization, never a token; pass --profile-name NAME or set OHMYHOST_PROFILE to choose one)",
|
|
271
301
|
"github connect":
|
|
272
302
|
"ohmyhost github connect --organization ULID --idempotency-key KEY --json (connect once, then link covered repositories without another browser consent)",
|
|
273
303
|
"github status": "ohmyhost github status --organization ULID --json",
|
|
@@ -276,6 +306,8 @@ const HELP_USAGE = Object.freeze({
|
|
|
276
306
|
"export get":
|
|
277
307
|
"ohmyhost export get EXPORT_ULID --project ULID --json (poll the original job; signed ZIP download lasts 24 hours)",
|
|
278
308
|
"credits account": "ohmyhost credits account --organization ULID --json",
|
|
309
|
+
"referral link":
|
|
310
|
+
"ohmyhost referral link --organization ULID --json (the workspace's link to share; a new user who signs up through it starts with a free Paid month and 1,000 credits, and their first payment gives this workspace the same)",
|
|
279
311
|
"credits balance": "ohmyhost credits balance --organization ULID --json",
|
|
280
312
|
"billing recharge get": "ohmyhost billing recharge get --organization ULID --json",
|
|
281
313
|
"billing recharge set":
|
|
@@ -291,10 +323,11 @@ const HELP_USAGE = Object.freeze({
|
|
|
291
323
|
"budget set":
|
|
292
324
|
"ohmyhost budget set --project ULID --credits NUMBER|none [--mode continue|stop] --idempotency-key KEY --json",
|
|
293
325
|
"organization create":
|
|
294
|
-
"ohmyhost organization create --name NAME --idempotency-key KEY [--source SOURCE] --json (SOURCE is optional attribution from a link's r value; the new workspace
|
|
326
|
+
"ohmyhost organization create --name NAME --idempotency-key KEY [--source SOURCE] --json (SOURCE is optional attribution from a link's r value; a login without organization is bound to the new workspace, another login keeps its own)",
|
|
295
327
|
"organization list":
|
|
296
|
-
"ohmyhost organization list --json (the workspaces
|
|
297
|
-
"organization use":
|
|
328
|
+
"ohmyhost organization list [--profile-name NAME] --json (the workspaces of the chosen login's user and the one that login is scoped to)",
|
|
329
|
+
"organization use":
|
|
330
|
+
"ohmyhost organization use --organization ULID [--profile-name NAME] --json (binds a login that has no organization yet; another organization needs its own login)",
|
|
298
331
|
"operation get": "ohmyhost operation get OPERATION_ULID --json",
|
|
299
332
|
"operation reconcile":
|
|
300
333
|
"ohmyhost operation reconcile OPERATION_ULID --idempotency-key KEY --yes --json",
|
|
@@ -321,6 +354,9 @@ const HELP_USAGE = Object.freeze({
|
|
|
321
354
|
"ohmyhost project dev-share revoke --project ULID --idempotency-key KEY --yes --json",
|
|
322
355
|
"project dev-access mode":
|
|
323
356
|
"ohmyhost project dev-access mode --project ULID --mode protected|public --idempotency-key KEY --yes --json",
|
|
357
|
+
"project flag status": "ohmyhost project flag status --project ULID --json",
|
|
358
|
+
"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, a Free workspace may connect its own domain without the domain fee and each Paid period adds 250 credits)",
|
|
324
360
|
"project handle check":
|
|
325
361
|
"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)",
|
|
326
362
|
"project handle set":
|
|
@@ -377,7 +413,7 @@ const HELP_USAGE = Object.freeze({
|
|
|
377
413
|
"function runs":
|
|
378
414
|
"ohmyhost function runs --project ULID --environment ENVIRONMENT_ULID [--limit 1-100] --json",
|
|
379
415
|
"secret set":
|
|
380
|
-
"printf '%s' \"$SECRET_VALUE\" | ohmyhost secret set NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY --stdin [--wait] --json",
|
|
416
|
+
"printf '%s' \"$SECRET_VALUE\" | ohmyhost secret set NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY [--profile-user USER_ID --profile-organization ULID | --token-user USER_ID --token-organization ULID] --stdin [--wait] --json (with --profile-user and --profile-organization the saved login that runs it must belong to that user and organization; with --token-user and --token-organization it runs only with an OHMYHOST_TOKEN of that user and organization, never with a saved login)",
|
|
381
417
|
"secret delete":
|
|
382
418
|
"ohmyhost secret delete NAME --project ULID --environment ENVIRONMENT_ULID --idempotency-key KEY [--wait] --json",
|
|
383
419
|
"mail setup":
|
|
@@ -411,8 +447,9 @@ export const cliMetadataResult = (kind: "help" | "version", topic?: string): Cli
|
|
|
411
447
|
? success("help", {
|
|
412
448
|
status: "succeeded",
|
|
413
449
|
cli_version: CLI_VERSION,
|
|
414
|
-
usage: "ohmyhost <command> [options] --json",
|
|
415
|
-
environment:
|
|
450
|
+
usage: "ohmyhost <command> [options] [--profile-name NAME] --json",
|
|
451
|
+
environment:
|
|
452
|
+
"OHMYHOST_ENVIRONMENT=development|production (default: production); OHMYHOST_PROFILE=NAME binds this process to one saved login",
|
|
416
453
|
commands: HELP_COMMANDS.filter((entry) => !topic || entry.command === topic),
|
|
417
454
|
})
|
|
418
455
|
: success("version", { status: "succeeded", cli_version: CLI_VERSION });
|
|
@@ -431,7 +468,12 @@ class ProductCliFailure extends Error {
|
|
|
431
468
|
}
|
|
432
469
|
|
|
433
470
|
export class ProductCli {
|
|
434
|
-
|
|
471
|
+
/** The saved login of this run; binding a login without organization replaces it. */
|
|
472
|
+
#profile: LocalProfile | undefined;
|
|
473
|
+
|
|
474
|
+
public constructor(private readonly dependencies: ProductCliDependencies) {
|
|
475
|
+
this.#profile = dependencies.profile;
|
|
476
|
+
}
|
|
435
477
|
|
|
436
478
|
public async run(
|
|
437
479
|
argv: readonly string[],
|
|
@@ -479,8 +521,9 @@ export class ProductCli {
|
|
|
479
521
|
// A user API token is bound to one workspace and cannot read the account profile.
|
|
480
522
|
throw new InteractiveSessionRequiredError();
|
|
481
523
|
}
|
|
524
|
+
if (command.kind === "profile-list") return await this.listProfiles();
|
|
482
525
|
if (command.kind === "login")
|
|
483
|
-
return await this.login(command.
|
|
526
|
+
return await this.login(command.organizationId, command.userId, signal);
|
|
484
527
|
if (command.kind === "logout") {
|
|
485
528
|
return await this.logout(command.credentialStore, command.revoke, signal);
|
|
486
529
|
}
|
|
@@ -583,6 +626,13 @@ export class ProductCli {
|
|
|
583
626
|
}
|
|
584
627
|
return success(name, { status: "succeeded", billing: result });
|
|
585
628
|
}
|
|
629
|
+
if (command.kind === "referral-link") {
|
|
630
|
+
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
631
|
+
const referral = parseOrganizationReferral(
|
|
632
|
+
await api.getOrganizationReferral(command.organizationId),
|
|
633
|
+
);
|
|
634
|
+
return success("referral link", { status: "succeeded", referral });
|
|
635
|
+
}
|
|
586
636
|
if (command.kind === "credits-account") {
|
|
587
637
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
588
638
|
const account = await api.getOrganizationAccount(command.organizationId);
|
|
@@ -628,12 +678,9 @@ export class ProductCli {
|
|
|
628
678
|
idempotencyKey: command.idempotencyKey,
|
|
629
679
|
}),
|
|
630
680
|
);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
await api.getAccountProfile(),
|
|
681
|
+
const selection = chooseWorkspace(
|
|
682
|
+
(await api.getAccountProfile()).organizations,
|
|
634
683
|
organization.id,
|
|
635
|
-
command.credentialStore,
|
|
636
|
-
signal,
|
|
637
684
|
);
|
|
638
685
|
if (selection.outcome !== "selected")
|
|
639
686
|
throw new ProductCliFailure(
|
|
@@ -642,10 +689,28 @@ export class ProductCli {
|
|
|
642
689
|
true,
|
|
643
690
|
`The new workspace is not yet readable for this account. Run '${this.dependencies.commandPrefix} organization use --organization ${organization.id} --json' again.`,
|
|
644
691
|
);
|
|
692
|
+
const current = this.#profile;
|
|
693
|
+
// A login in another organization keeps it: the new workspace gets its own login, so no
|
|
694
|
+
// other agent that uses this login is moved into the new workspace.
|
|
695
|
+
if (
|
|
696
|
+
current !== undefined &&
|
|
697
|
+
current.organizationId !== null &&
|
|
698
|
+
current.organizationId !== organization.id
|
|
699
|
+
)
|
|
700
|
+
return success("organization create", {
|
|
701
|
+
status: "succeeded",
|
|
702
|
+
organization,
|
|
703
|
+
selected: current.organizationId,
|
|
704
|
+
profile: publicProfile(current),
|
|
705
|
+
next_action: `Run '${this.dependencies.commandPrefix} login --organization ${organization.id} --user ${current.userId} --json' to add a login for the new workspace as the same user; profile ${current.name} stays in organization ${current.organizationId}.`,
|
|
706
|
+
});
|
|
707
|
+
// A created workspace is only usable once it is in the access token, so bind it now.
|
|
708
|
+
const profile = await this.bindOrganization(selection.organization, signal);
|
|
645
709
|
return success("organization create", {
|
|
646
710
|
status: "succeeded",
|
|
647
711
|
organization,
|
|
648
712
|
selected: organization.id,
|
|
713
|
+
profile: publicProfile(profile),
|
|
649
714
|
next_action: `Run '${this.dependencies.commandPrefix} whoami --json' to confirm the selected workspace, then create a project.`,
|
|
650
715
|
...(await this.githubNextAction(
|
|
651
716
|
await this.authenticatedApi(command.credentialStore, signal),
|
|
@@ -655,26 +720,26 @@ export class ProductCli {
|
|
|
655
720
|
}
|
|
656
721
|
if (command.kind === "organization-list") {
|
|
657
722
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
658
|
-
const
|
|
723
|
+
const account = await api.getAccountProfile();
|
|
659
724
|
return success("organization list", {
|
|
660
725
|
status: "listed",
|
|
661
|
-
organizations: organizations.map(publicWorkspace),
|
|
662
|
-
selected:
|
|
726
|
+
organizations: account.organizations.map(publicWorkspace),
|
|
727
|
+
selected: this.#profile?.organizationId ?? null,
|
|
663
728
|
});
|
|
664
729
|
}
|
|
665
730
|
if (command.kind === "organization-use") {
|
|
666
731
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
667
|
-
const selection =
|
|
668
|
-
await api.getAccountProfile(),
|
|
732
|
+
const selection = chooseWorkspace(
|
|
733
|
+
(await api.getAccountProfile()).organizations,
|
|
669
734
|
command.organizationId,
|
|
670
|
-
command.credentialStore,
|
|
671
|
-
signal,
|
|
672
735
|
);
|
|
673
736
|
if (selection.outcome !== "selected") throw this.unknownWorkspace(selection);
|
|
737
|
+
const profile = await this.bindOrganization(selection.organization, signal);
|
|
674
738
|
return success("organization use", {
|
|
675
739
|
status: "selected",
|
|
676
740
|
organization: publicWorkspace(selection.organization),
|
|
677
741
|
selected: selection.organization.id,
|
|
742
|
+
profile: publicProfile(profile),
|
|
678
743
|
...(await this.githubNextAction(
|
|
679
744
|
await this.authenticatedApi(command.credentialStore, signal),
|
|
680
745
|
selection.organization.id,
|
|
@@ -758,6 +823,22 @@ export class ProductCli {
|
|
|
758
823
|
if (command.kind === "project-status") return await this.getProjectStatus(command, signal);
|
|
759
824
|
if (command.kind === "project-dev-access-create")
|
|
760
825
|
return await this.createDevAccessTicket(command, signal);
|
|
826
|
+
if (command.kind === "project-flag-status" || command.kind === "project-flag-set") {
|
|
827
|
+
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
828
|
+
const flag = parsePoweredByFlag(
|
|
829
|
+
command.kind === "project-flag-status"
|
|
830
|
+
? await api.getPoweredByFlag(command.projectId)
|
|
831
|
+
: await api.setPoweredByFlag(
|
|
832
|
+
command.projectId,
|
|
833
|
+
command.enabled,
|
|
834
|
+
command.idempotencyKey,
|
|
835
|
+
),
|
|
836
|
+
);
|
|
837
|
+
return success(
|
|
838
|
+
command.kind === "project-flag-status" ? "project flag status" : "project flag set",
|
|
839
|
+
{ status: "succeeded", flag },
|
|
840
|
+
);
|
|
841
|
+
}
|
|
761
842
|
if (
|
|
762
843
|
command.kind === "project-dev-share-link" ||
|
|
763
844
|
command.kind === "project-dev-share-rotate" ||
|
|
@@ -887,8 +968,16 @@ export class ProductCli {
|
|
|
887
968
|
false,
|
|
888
969
|
`A user API token cannot create, list or select a workspace. Run '${this.dependencies.commandPrefix} login --json' in a process without OHMYHOST_TOKEN.`,
|
|
889
970
|
);
|
|
971
|
+
if (error instanceof ProfileLimitError)
|
|
972
|
+
return failure(
|
|
973
|
+
name,
|
|
974
|
+
2,
|
|
975
|
+
"profile_limit_reached",
|
|
976
|
+
false,
|
|
977
|
+
profileLimitAction(this.dependencies.commandPrefix),
|
|
978
|
+
);
|
|
890
979
|
this.recordDiagnostic(diagnosticDescriptor(error, signal));
|
|
891
|
-
return classifyFailure(name, error, signal);
|
|
980
|
+
return classifyFailure(name, error, signal, this.context());
|
|
892
981
|
}
|
|
893
982
|
}
|
|
894
983
|
|
|
@@ -929,33 +1018,59 @@ export class ProductCli {
|
|
|
929
1018
|
return success("whoami", {
|
|
930
1019
|
status: "authenticated",
|
|
931
1020
|
identity,
|
|
1021
|
+
context: this.context(),
|
|
932
1022
|
...(identity.organization_ids.length === 1 && identity.organization_ids[0] !== undefined
|
|
933
1023
|
? await this.githubNextAction(api, identity.organization_ids[0])
|
|
934
1024
|
: {}),
|
|
935
1025
|
});
|
|
936
|
-
const selection = await
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
command.credentialStore,
|
|
940
|
-
signal,
|
|
941
|
-
);
|
|
942
|
-
if (selection.outcome === "selected")
|
|
1026
|
+
const selection = chooseWorkspace((await api.getAccountProfile()).organizations, undefined);
|
|
1027
|
+
if (selection.outcome === "selected") {
|
|
1028
|
+
await this.bindOrganization(selection.organization, signal);
|
|
943
1029
|
return success("whoami", {
|
|
944
1030
|
status: "authenticated",
|
|
945
1031
|
identity: parseCurrentIdentity(await api.getCurrentIdentity()),
|
|
946
1032
|
organization: publicWorkspace(selection.organization),
|
|
947
1033
|
selected: selection.organization.id,
|
|
1034
|
+
context: this.context(),
|
|
948
1035
|
...(await this.githubNextAction(api, selection.organization.id)),
|
|
949
1036
|
});
|
|
1037
|
+
}
|
|
950
1038
|
return success("whoami", {
|
|
951
1039
|
status: "authenticated",
|
|
952
1040
|
identity,
|
|
1041
|
+
context: this.context(),
|
|
953
1042
|
organizations:
|
|
954
1043
|
selection.outcome === "choice_required" ? selection.organizations.map(publicWorkspace) : [],
|
|
955
|
-
next_action:
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1044
|
+
next_action: this.workspaceAction(selection),
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** Which account this run acts as, without any credential. */
|
|
1049
|
+
private context(): Readonly<Record<string, unknown>> {
|
|
1050
|
+
if (this.dependencies.apiToken !== undefined) return { credential: "environment_token" };
|
|
1051
|
+
return {
|
|
1052
|
+
credential: "profile",
|
|
1053
|
+
profile: this.#profile === undefined ? null : publicProfile(this.#profile),
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** The next step for a login that has no organization yet. */
|
|
1058
|
+
private workspaceAction(selection: WorkspaceSelection): string {
|
|
1059
|
+
const prefix = this.dependencies.commandPrefix;
|
|
1060
|
+
const named = this.#profile === undefined ? "" : ` --profile-name ${this.#profile.name}`;
|
|
1061
|
+
return selection.outcome === "choice_required"
|
|
1062
|
+
? `Ask the customer which workspace to use, then run '${prefix} organization use --organization ULID${named} --json'.`
|
|
1063
|
+
: `Run '${prefix} organization create --name NAME --idempotency-key KEY${named} --json' to create the first workspace.`;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
private async listProfiles(): Promise<CliResult> {
|
|
1067
|
+
const profiles = this.dependencies.profiles;
|
|
1068
|
+
if (profiles === undefined) throw new ProviderCapabilityBlockedError();
|
|
1069
|
+
return success("profile list", {
|
|
1070
|
+
status: "listed",
|
|
1071
|
+
profiles: (await profiles.list()).map(publicProfile),
|
|
1072
|
+
bound: this.dependencies.profileBinding ?? null,
|
|
1073
|
+
environment_token: this.dependencies.apiToken !== undefined,
|
|
959
1074
|
});
|
|
960
1075
|
}
|
|
961
1076
|
|
|
@@ -974,10 +1089,14 @@ export class ProductCli {
|
|
|
974
1089
|
|
|
975
1090
|
// The reviewed boundary proof parses this signature, so keep its parameters brace-free.
|
|
976
1091
|
private async login(
|
|
977
|
-
credentialStore: "native",
|
|
978
1092
|
organizationId: string | undefined,
|
|
1093
|
+
expectedUserId: string | undefined,
|
|
979
1094
|
signal: AbortSignal,
|
|
980
1095
|
): Promise<CliResult> {
|
|
1096
|
+
const profiles = this.dependencies.profiles;
|
|
1097
|
+
const readIdentity = this.dependencies.currentIdentity;
|
|
1098
|
+
if (profiles === undefined || readIdentity === undefined)
|
|
1099
|
+
throw new ProviderCapabilityBlockedError();
|
|
981
1100
|
// A device code lives five minutes and the provider offers no way to lengthen it, so the
|
|
982
1101
|
// sign-in outlives the code instead: while this window lasts, a lapsed code is replaced.
|
|
983
1102
|
const signInDeadline = this.dependencies.now().getTime() + SIGN_IN_WINDOW_MS;
|
|
@@ -1046,78 +1165,169 @@ export class ProductCli {
|
|
|
1046
1165
|
"Run login again; the authorization response was incomplete.",
|
|
1047
1166
|
);
|
|
1048
1167
|
}
|
|
1049
|
-
|
|
1168
|
+
// Nothing is stored before the browser's account is verified, so a wrong, refused or
|
|
1169
|
+
// cancelled sign-in leaves every saved login, and every command using one, as it was.
|
|
1170
|
+
const signedIn: CurrentCredential = {
|
|
1050
1171
|
accessToken: result.tokens.accessToken,
|
|
1051
1172
|
refreshToken: result.tokens.refreshToken,
|
|
1052
1173
|
expiresAt,
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
);
|
|
1174
|
+
};
|
|
1175
|
+
const account = await this.dependencies.accountProfile(async () => signedIn.accessToken);
|
|
1176
|
+
if (!isProfileUserId(account.userId)) throw this.unverifiedLogin();
|
|
1177
|
+
if (expectedUserId !== undefined && account.userId !== expectedUserId)
|
|
1178
|
+
throw new ProductCliFailure(
|
|
1179
|
+
4,
|
|
1180
|
+
"login_account_mismatch",
|
|
1181
|
+
false,
|
|
1182
|
+
`The browser signed in as user ${account.userId}, not ${expectedUserId}. Nothing was saved and every saved login is unchanged. Sign out of ohmyho.st in that browser or use the browser profile of ${expectedUserId}, then run '${this.dependencies.commandPrefix} login${organizationId === undefined ? "" : ` --organization ${organizationId}`} --user ${expectedUserId} --json' again.`,
|
|
1183
|
+
);
|
|
1184
|
+
const selection = chooseWorkspace(account.organizations, organizationId);
|
|
1185
|
+
if (selection.outcome === "not_a_member") throw this.unknownWorkspace(selection);
|
|
1186
|
+
// The workspace lives in the access token, so a chosen one is requested before storing.
|
|
1187
|
+
const credential =
|
|
1188
|
+
selection.outcome === "selected"
|
|
1189
|
+
? await runBounded(
|
|
1190
|
+
(bounded) =>
|
|
1191
|
+
this.dependencies.tokenRefresher.refresh(
|
|
1192
|
+
signedIn.refreshToken,
|
|
1193
|
+
bounded,
|
|
1194
|
+
selection.organization.workos_id,
|
|
1195
|
+
),
|
|
1196
|
+
signal,
|
|
1197
|
+
this.dependencies.lifecycleTimeoutMs,
|
|
1198
|
+
)
|
|
1199
|
+
: signedIn;
|
|
1200
|
+
const identity = await readIdentity(async () => credential.accessToken);
|
|
1201
|
+
const scopedId =
|
|
1202
|
+
selection.outcome === "selected"
|
|
1203
|
+
? selection.organization.id
|
|
1204
|
+
: (parseCurrentIdentity(identity).organization_ids[0] ?? null);
|
|
1205
|
+
const organization =
|
|
1206
|
+
scopedId === null
|
|
1207
|
+
? null
|
|
1208
|
+
: account.organizations.find((candidate) => candidate.id === scopedId);
|
|
1209
|
+
if (organization === undefined) throw this.unverifiedLogin();
|
|
1210
|
+
try {
|
|
1211
|
+
assertVerifiedLogin(identity, account.userId, scopedId);
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (error instanceof AccountVerificationError) throw this.unverifiedLogin();
|
|
1214
|
+
throw error;
|
|
1215
|
+
}
|
|
1216
|
+
const login = {
|
|
1217
|
+
userId: account.userId,
|
|
1218
|
+
organizationId: scopedId,
|
|
1219
|
+
organizationName: organization?.name ?? null,
|
|
1220
|
+
};
|
|
1221
|
+
const profile: LocalProfile = {
|
|
1222
|
+
name: this.dependencies.profileName ?? defaultProfileName(await profiles.list(), login),
|
|
1223
|
+
...login,
|
|
1224
|
+
};
|
|
1225
|
+
try {
|
|
1226
|
+
await profiles.save(
|
|
1227
|
+
profile,
|
|
1228
|
+
organization === null
|
|
1229
|
+
? credential
|
|
1230
|
+
: {
|
|
1231
|
+
...credential,
|
|
1232
|
+
workosOrganizationId: organization.workos_id,
|
|
1233
|
+
organizationId: organization.id,
|
|
1234
|
+
},
|
|
1235
|
+
);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
if (!(error instanceof ProfileNameConflictError)) throw error;
|
|
1238
|
+
throw new ProductCliFailure(
|
|
1239
|
+
2,
|
|
1240
|
+
"profile_name_conflict",
|
|
1241
|
+
false,
|
|
1242
|
+
`The name ${profile.name} already belongs to the saved login of user ${error.profile.userId} in organization ${String(error.profile.organizationId ?? "none")}. Nothing was saved. Choose another --profile-name, or remove that login with '${this.dependencies.commandPrefix} logout --profile-name ${profile.name} --json' first.`,
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
this.#profile = profile;
|
|
1064
1246
|
const readGithubConnection = this.dependencies.githubConnectionStatus;
|
|
1065
|
-
if (
|
|
1247
|
+
if (organization !== null)
|
|
1066
1248
|
return success("login", {
|
|
1067
1249
|
status: "authenticated",
|
|
1068
|
-
organization: publicWorkspace(
|
|
1250
|
+
organization: publicWorkspace(organization),
|
|
1251
|
+
profile: publicProfile(profile),
|
|
1069
1252
|
...(readGithubConnection === undefined
|
|
1070
1253
|
? {}
|
|
1071
1254
|
: await this.githubNextAction(
|
|
1072
1255
|
{
|
|
1073
1256
|
getGithubConnection: (selectedOrganizationId) =>
|
|
1074
1257
|
readGithubConnection(
|
|
1075
|
-
() =>
|
|
1258
|
+
async () => credential.accessToken,
|
|
1076
1259
|
selectedOrganizationId,
|
|
1077
1260
|
),
|
|
1078
1261
|
},
|
|
1079
|
-
|
|
1262
|
+
organization.id,
|
|
1080
1263
|
)),
|
|
1081
1264
|
});
|
|
1082
|
-
if (selection.outcome === "not_a_member") throw this.unknownWorkspace(selection);
|
|
1083
1265
|
return success("login", {
|
|
1084
1266
|
status: "authenticated",
|
|
1085
1267
|
organization: null,
|
|
1268
|
+
profile: publicProfile(profile),
|
|
1086
1269
|
organizations:
|
|
1087
1270
|
selection.outcome === "choice_required"
|
|
1088
1271
|
? selection.organizations.map(publicWorkspace)
|
|
1089
1272
|
: [],
|
|
1090
|
-
next_action:
|
|
1091
|
-
selection.outcome === "choice_required"
|
|
1092
|
-
? `Ask the customer which workspace to use, then run '${this.dependencies.commandPrefix} organization use --organization ULID --json'.`
|
|
1093
|
-
: `Run '${this.dependencies.commandPrefix} organization create --name NAME --idempotency-key KEY --json' to create the first workspace.`,
|
|
1273
|
+
next_action: this.workspaceAction(selection),
|
|
1094
1274
|
});
|
|
1095
1275
|
}
|
|
1096
1276
|
}
|
|
1097
1277
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1278
|
+
/** The provider answered with another account than the one this login requested. */
|
|
1279
|
+
private unverifiedLogin(): ProductCliFailure {
|
|
1280
|
+
return new ProductCliFailure(
|
|
1281
|
+
4,
|
|
1282
|
+
"login_verification_failed",
|
|
1283
|
+
true,
|
|
1284
|
+
`The signed-in account could not be verified; nothing was saved. Run '${this.dependencies.commandPrefix} login --json' again.`,
|
|
1285
|
+
);
|
|
1103
1286
|
}
|
|
1104
1287
|
|
|
1105
|
-
/**
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1288
|
+
/**
|
|
1289
|
+
* Bind the login of this run, which has no organization yet, to one of its user's organizations.
|
|
1290
|
+
* A login that already has one keeps it; another organization is its own login.
|
|
1291
|
+
*/
|
|
1292
|
+
private async bindOrganization(
|
|
1293
|
+
organization: PublicWorkspace,
|
|
1110
1294
|
signal: AbortSignal,
|
|
1111
|
-
): Promise<
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1295
|
+
): Promise<LocalProfile> {
|
|
1296
|
+
const profiles = this.dependencies.profiles;
|
|
1297
|
+
const profile = this.#profile;
|
|
1298
|
+
if (profiles === undefined || profile === undefined)
|
|
1299
|
+
throw new InteractiveSessionRequiredError();
|
|
1300
|
+
let bound: LocalProfile;
|
|
1301
|
+
try {
|
|
1302
|
+
bound = await runBounded(
|
|
1115
1303
|
(bounded) =>
|
|
1116
|
-
selectWorkspace(
|
|
1304
|
+
selectWorkspace(
|
|
1305
|
+
{
|
|
1306
|
+
store: profiles.credential(profile),
|
|
1307
|
+
refresher: this.dependencies.tokenRefresher,
|
|
1308
|
+
profiles,
|
|
1309
|
+
profile,
|
|
1310
|
+
},
|
|
1311
|
+
organization,
|
|
1312
|
+
bounded,
|
|
1313
|
+
(accessToken) =>
|
|
1314
|
+
this.dependencies.productApi(async () => accessToken).getCurrentIdentity(),
|
|
1315
|
+
),
|
|
1117
1316
|
signal,
|
|
1118
1317
|
this.dependencies.lifecycleTimeoutMs,
|
|
1119
1318
|
);
|
|
1120
|
-
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
if (error instanceof AccountVerificationError) throw this.unverifiedLogin();
|
|
1321
|
+
if (!(error instanceof ProfileOrganizationFixedError)) throw error;
|
|
1322
|
+
throw new ProductCliFailure(
|
|
1323
|
+
2,
|
|
1324
|
+
"profile_organization_fixed",
|
|
1325
|
+
false,
|
|
1326
|
+
`Profile ${profile.name} stays signed in to organization ${String(profile.organizationId)}. Run '${this.dependencies.commandPrefix} login --organization ${organization.id} --user ${profile.userId} --json' to add a separate login for ${organization.id} as the same user; a saved login never switches organizations.`,
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
this.#profile = bound;
|
|
1330
|
+
return bound;
|
|
1121
1331
|
}
|
|
1122
1332
|
|
|
1123
1333
|
private unknownWorkspace(selection: WorkspaceSelection): ProductCliFailure {
|
|
@@ -1134,10 +1344,11 @@ export class ProductCli {
|
|
|
1134
1344
|
revoke: boolean,
|
|
1135
1345
|
signal: AbortSignal,
|
|
1136
1346
|
): Promise<CliResult> {
|
|
1137
|
-
const store = this.
|
|
1347
|
+
const store = this.store(credentialStore);
|
|
1348
|
+
const removed = this.#profile === undefined ? {} : { profile: publicProfile(this.#profile) };
|
|
1138
1349
|
if (!revoke) {
|
|
1139
|
-
await
|
|
1140
|
-
return success("logout", { status: "logged_out" });
|
|
1350
|
+
await this.forgetLogin(store);
|
|
1351
|
+
return success("logout", { status: "logged_out", ...removed });
|
|
1141
1352
|
}
|
|
1142
1353
|
const credential = await store.read();
|
|
1143
1354
|
let revocationError: unknown;
|
|
@@ -1152,7 +1363,7 @@ export class ProductCli {
|
|
|
1152
1363
|
} catch (error) {
|
|
1153
1364
|
revocationError = error;
|
|
1154
1365
|
} finally {
|
|
1155
|
-
await
|
|
1366
|
+
await this.forgetLogin(store);
|
|
1156
1367
|
}
|
|
1157
1368
|
if (revocationError !== undefined) {
|
|
1158
1369
|
if (isCancellation(revocationError) && signal.aborted) throw revocationError;
|
|
@@ -1182,7 +1393,34 @@ export class ProductCli {
|
|
|
1182
1393
|
}
|
|
1183
1394
|
throw revocationError;
|
|
1184
1395
|
}
|
|
1185
|
-
return success("logout", { status: "logged_out" });
|
|
1396
|
+
return success("logout", { status: "logged_out", ...removed });
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/** The credential store of this run's saved login; it follows a binding made during the run. */
|
|
1400
|
+
private store(kind: "native"): CredentialStore {
|
|
1401
|
+
const profiles = this.dependencies.profiles;
|
|
1402
|
+
if (profiles !== undefined && this.#profile !== undefined)
|
|
1403
|
+
return profiles.credential(this.#profile);
|
|
1404
|
+
if (this.dependencies.credentialStore === undefined)
|
|
1405
|
+
throw new ProductCliFailure(5, "authentication_required", false, this.loginAction());
|
|
1406
|
+
return this.dependencies.credentialStore(kind);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/** Removes only this run's saved login; every other saved login stays untouched. */
|
|
1410
|
+
private async forgetLogin(store: CredentialStore): Promise<void> {
|
|
1411
|
+
const profiles = this.dependencies.profiles;
|
|
1412
|
+
if (profiles !== undefined && this.#profile !== undefined) await profiles.remove(this.#profile);
|
|
1413
|
+
else await store.clear();
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** How to sign this run's login in again, naming its organization and alias when known. */
|
|
1417
|
+
private loginAction(): string {
|
|
1418
|
+
const profile = this.#profile;
|
|
1419
|
+
return `Run '${this.dependencies.commandPrefix} login${
|
|
1420
|
+
profile?.organizationId === undefined || profile.organizationId === null
|
|
1421
|
+
? ""
|
|
1422
|
+
: ` --organization ${profile.organizationId}`
|
|
1423
|
+
}${profile === undefined ? "" : ` --profile-name ${profile.name}`} --json'.`;
|
|
1186
1424
|
}
|
|
1187
1425
|
|
|
1188
1426
|
private async getOperation(
|
|
@@ -1271,7 +1509,7 @@ export class ProductCli {
|
|
|
1271
1509
|
): Promise<CliResult> {
|
|
1272
1510
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
1273
1511
|
const project = parseProjectStatus(await api.getProjectStatus(command.projectId));
|
|
1274
|
-
return success("project status", { status: "succeeded", project });
|
|
1512
|
+
return success("project status", { status: "succeeded", project, context: this.context() });
|
|
1275
1513
|
}
|
|
1276
1514
|
|
|
1277
1515
|
private async createDevAccessTicket(
|
|
@@ -1476,14 +1714,21 @@ export class ProductCli {
|
|
|
1476
1714
|
`${command.repositoryOwner}/${command.repositoryName}`.toLowerCase()
|
|
1477
1715
|
)
|
|
1478
1716
|
throw new ResponseContractError();
|
|
1717
|
+
// The link names the organization the server authorized, so this checkout can be linked once
|
|
1718
|
+
// per organization and another account's link is never used by mistake.
|
|
1719
|
+
const [organizationId, ...others] = parseCurrentIdentity(
|
|
1720
|
+
await api.getCurrentIdentity(),
|
|
1721
|
+
).organization_ids;
|
|
1722
|
+
if (organizationId === undefined || others.length > 0) throw new ResponseContractError();
|
|
1479
1723
|
await this.dependencies.projectLinkStore.save({
|
|
1480
|
-
version:
|
|
1724
|
+
version: 2,
|
|
1481
1725
|
api_origin: this.dependencies.productApiOrigin,
|
|
1726
|
+
organization_id: organizationId,
|
|
1482
1727
|
project_id: command.projectId,
|
|
1483
1728
|
installation_id: source.installation_id,
|
|
1484
1729
|
repository_full_name: source.repository_full_name,
|
|
1485
1730
|
});
|
|
1486
|
-
return success("link", { status: "completed", operation });
|
|
1731
|
+
return success("link", { status: "completed", operation, organization_id: organizationId });
|
|
1487
1732
|
}
|
|
1488
1733
|
|
|
1489
1734
|
private async configureAutoDeploy(
|
|
@@ -1925,18 +2170,19 @@ export class ProductCli {
|
|
|
1925
2170
|
signal: AbortSignal,
|
|
1926
2171
|
): Promise<string> {
|
|
1927
2172
|
if (this.dependencies.apiToken !== undefined) return this.dependencies.apiToken;
|
|
1928
|
-
const store = this.
|
|
2173
|
+
const store = this.store(credentialStore);
|
|
1929
2174
|
const stored = await store.read();
|
|
1930
2175
|
if (stored === undefined) {
|
|
1931
|
-
throw new ProductCliFailure(5, "authentication_required", false,
|
|
2176
|
+
throw new ProductCliFailure(5, "authentication_required", false, this.loginAction());
|
|
1932
2177
|
}
|
|
1933
2178
|
let refresh: { readonly credential: CurrentCredential; readonly refreshed: boolean };
|
|
1934
2179
|
try {
|
|
1935
2180
|
refresh = await this.refreshIfExpired(stored, signal);
|
|
1936
2181
|
} catch (error) {
|
|
1937
2182
|
if (!(error instanceof TokenRefreshRejectedError)) throw error;
|
|
1938
|
-
|
|
1939
|
-
|
|
2183
|
+
// The provider ended this login; only this saved login is removed.
|
|
2184
|
+
await this.forgetLogin(store);
|
|
2185
|
+
throw new ProductCliFailure(5, "authentication_required", false, this.loginAction());
|
|
1940
2186
|
}
|
|
1941
2187
|
if (refresh.refreshed) await store.replace(refresh.credential);
|
|
1942
2188
|
return refresh.credential.accessToken;
|
|
@@ -2049,7 +2295,32 @@ const redactVerificationUri = (value: string): string => {
|
|
|
2049
2295
|
return url.toString();
|
|
2050
2296
|
};
|
|
2051
2297
|
|
|
2052
|
-
|
|
2298
|
+
/**
|
|
2299
|
+
* Answers that mean "this account cannot see or use that": they name the account that ran, so a
|
|
2300
|
+
* wrong saved login is told apart from a missing organization or GitHub authorization.
|
|
2301
|
+
*/
|
|
2302
|
+
export const ACCOUNT_SENSITIVE_PROBLEMS: ReadonlySet<string> = new Set([
|
|
2303
|
+
"resource_not_found",
|
|
2304
|
+
"forbidden",
|
|
2305
|
+
"organization_required",
|
|
2306
|
+
"github_connection_required",
|
|
2307
|
+
"github_connection_revoked",
|
|
2308
|
+
"repository_not_installed",
|
|
2309
|
+
]);
|
|
2310
|
+
|
|
2311
|
+
/** The stable CLI answer for a failure outside a command run, such as choosing the account. */
|
|
2312
|
+
export const classifyCliFailure = (
|
|
2313
|
+
command: RecognizedCommandLabel,
|
|
2314
|
+
error: unknown,
|
|
2315
|
+
signal: AbortSignal,
|
|
2316
|
+
): CliResult => classifyFailure(command, error, signal);
|
|
2317
|
+
|
|
2318
|
+
const classifyFailure = (
|
|
2319
|
+
command: CommandLabel,
|
|
2320
|
+
error: unknown,
|
|
2321
|
+
signal: AbortSignal,
|
|
2322
|
+
actingAs?: Readonly<Record<string, unknown>>,
|
|
2323
|
+
): CliResult => {
|
|
2053
2324
|
if (signal.aborted || isCancellation(error)) {
|
|
2054
2325
|
return failure(command, 130, "command_cancelled", true, "Run the command again.");
|
|
2055
2326
|
}
|
|
@@ -2124,6 +2395,7 @@ const classifyFailure = (command: CommandLabel, error: unknown, signal: AbortSig
|
|
|
2124
2395
|
problem.retryAfterSeconds,
|
|
2125
2396
|
problem.requestId,
|
|
2126
2397
|
problem.docsUrl,
|
|
2398
|
+
ACCOUNT_SENSITIVE_PROBLEMS.has(problem.code) ? actingAs : undefined,
|
|
2127
2399
|
);
|
|
2128
2400
|
} catch {
|
|
2129
2401
|
return failure(
|
|
@@ -2175,6 +2447,7 @@ const failure = (
|
|
|
2175
2447
|
retryAfterSeconds?: number,
|
|
2176
2448
|
requestId?: string,
|
|
2177
2449
|
docsUrl?: string,
|
|
2450
|
+
actingAs?: Readonly<Record<string, unknown>>,
|
|
2178
2451
|
): CliResult => {
|
|
2179
2452
|
const document: CliErrorDocument = {
|
|
2180
2453
|
version: 1,
|
|
@@ -2189,6 +2462,7 @@ const failure = (
|
|
|
2189
2462
|
...(requestId === undefined ? {} : { request_id: requestId }),
|
|
2190
2463
|
...(docsUrl === undefined ? {} : { docs_url: docsUrl }),
|
|
2191
2464
|
...(operation?.error === undefined ? {} : { message: operation.error.message }),
|
|
2465
|
+
...(actingAs === undefined ? {} : { acting_as: actingAs }),
|
|
2192
2466
|
},
|
|
2193
2467
|
};
|
|
2194
2468
|
return { exitCode, stdout: `${JSON.stringify(document)}\n`, stderr: "" };
|
|
@@ -2206,6 +2480,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
|
|
|
2206
2480
|
if (command.kind === "billing-status") return "billing status";
|
|
2207
2481
|
if (command.kind === "billing-portal") return "billing portal";
|
|
2208
2482
|
if (command.kind === "credits-account") return "credits account";
|
|
2483
|
+
if (command.kind === "referral-link") return "referral link";
|
|
2209
2484
|
if (command.kind === "credits-balance") return "credits balance";
|
|
2210
2485
|
if (command.kind === "credits-usage") return "credits usage";
|
|
2211
2486
|
if (command.kind === "budget-get") return "budget get";
|
|
@@ -2231,6 +2506,8 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
|
|
|
2231
2506
|
if (command.kind === "project-dev-share-rotate") return "project dev-share rotate";
|
|
2232
2507
|
if (command.kind === "project-dev-share-revoke") return "project dev-share revoke";
|
|
2233
2508
|
if (command.kind === "project-dev-access-mode-set") return "project dev-access mode";
|
|
2509
|
+
if (command.kind === "project-flag-status") return "project flag status";
|
|
2510
|
+
if (command.kind === "project-flag-set") return "project flag set";
|
|
2234
2511
|
if (command.kind === "project-handle-check") return "project handle check";
|
|
2235
2512
|
if (command.kind === "project-handle-set") return "project handle set";
|
|
2236
2513
|
if (command.kind === "database-write") return "database write";
|
|
@@ -2258,6 +2535,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
|
|
|
2258
2535
|
if (command.kind === "secret-set") return "secret set";
|
|
2259
2536
|
if (command.kind === "secret-delete") return "secret delete";
|
|
2260
2537
|
if (command.kind === "managed-mail") return command.label;
|
|
2538
|
+
if (command.kind === "profile-list") return "profile list";
|
|
2261
2539
|
return command.kind;
|
|
2262
2540
|
};
|
|
2263
2541
|
|