@amerged/ohmyhost-cli 0.1.15 → 0.1.17
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/bin.js +1 -1
- package/dist/{chunk-3BEMNNWB.js → chunk-IZZDZV6T.js} +388 -52
- package/dist/index.js +7 -1
- package/package.json +1 -1
- package/src/apps/product-cli/cli.ts +89 -10
- package/src/apps/product-cli/command.ts +135 -10
- package/src/apps/product-cli/composition.ts +47 -3
- package/src/apps/product-cli/index.ts +1 -0
- package/src/apps/product-cli/product-api.ts +55 -1
- package/src/apps/product-cli/project-link-store.ts +45 -2
- package/src/apps/product-cli/repository-init.ts +2 -1
- package/src/apps/product-cli/runtime-contract.ts +56 -3
- package/src/packages/sdk-ts/generated/index.ts +33 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +241 -3
- package/src/packages/sdk-ts/generated/types.gen.ts +337 -10
- package/src/packages/sdk-ts/managed-mail-response.ts +7 -0
- package/src/packages/sdk-ts/managed-mail.ts +7 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
parseCloudflareDnsAuthorization,
|
|
31
31
|
parseCloudflareDnsAuthorizationStatus,
|
|
32
32
|
parseCurrentIdentity,
|
|
33
|
+
parseDevAccessState,
|
|
33
34
|
parseDevAccessTicket,
|
|
34
35
|
parseOperation,
|
|
35
36
|
parseOperationEvent,
|
|
@@ -37,9 +38,11 @@ import {
|
|
|
37
38
|
parsePaidDomainPlan,
|
|
38
39
|
parseProductCliCommand,
|
|
39
40
|
parseSafeProblem,
|
|
41
|
+
parseWithLinkedProject,
|
|
40
42
|
prepareUserTokenFile,
|
|
41
43
|
publicWorkspace,
|
|
42
44
|
readBoundedResponseBody,
|
|
45
|
+
readLinkedProjectId,
|
|
43
46
|
readSecretValueFromStandardInput,
|
|
44
47
|
recognizedCommandLabel,
|
|
45
48
|
resolveProductCliEnvironment,
|
|
@@ -47,7 +50,7 @@ import {
|
|
|
47
50
|
selectWorkspace,
|
|
48
51
|
selectedWorkspaceId,
|
|
49
52
|
workspaceOf
|
|
50
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-IZZDZV6T.js";
|
|
51
54
|
import {
|
|
52
55
|
CredentialStoreIntegrityError,
|
|
53
56
|
CredentialStoreUnavailableError,
|
|
@@ -228,6 +231,7 @@ export {
|
|
|
228
231
|
parseCloudflareDnsAuthorizationStatus,
|
|
229
232
|
parseCredential,
|
|
230
233
|
parseCurrentIdentity,
|
|
234
|
+
parseDevAccessState,
|
|
231
235
|
parseDevAccessTicket,
|
|
232
236
|
parseOperation,
|
|
233
237
|
parseOperationEvent,
|
|
@@ -235,9 +239,11 @@ export {
|
|
|
235
239
|
parsePaidDomainPlan,
|
|
236
240
|
parseProductCliCommand,
|
|
237
241
|
parseSafeProblem,
|
|
242
|
+
parseWithLinkedProject,
|
|
238
243
|
prepareUserTokenFile,
|
|
239
244
|
publicWorkspace,
|
|
240
245
|
readBoundedResponseBody,
|
|
246
|
+
readLinkedProjectId,
|
|
241
247
|
readSecretValueFromStandardInput,
|
|
242
248
|
recognizedCommandLabel,
|
|
243
249
|
resolveProductCliEnvironment,
|
package/package.json
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
parseOrganizationCredits,
|
|
68
68
|
parseProjectCreditBudget,
|
|
69
69
|
parseDevAccessTicket,
|
|
70
|
+
parseDevAccessState,
|
|
70
71
|
parseGuardedActionPlan,
|
|
71
72
|
parseEnvironmentSecret,
|
|
72
73
|
parseEnvironmentSecretDeletion,
|
|
@@ -254,6 +255,7 @@ interface CliErrorDocument {
|
|
|
254
255
|
readonly message?: string;
|
|
255
256
|
readonly retry_after_seconds?: number;
|
|
256
257
|
readonly request_id?: string;
|
|
258
|
+
readonly docs_url?: string;
|
|
257
259
|
};
|
|
258
260
|
}
|
|
259
261
|
|
|
@@ -303,13 +305,20 @@ const HELP_USAGE = Object.freeze({
|
|
|
303
305
|
"feedback submit":
|
|
304
306
|
"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",
|
|
305
307
|
"project create":
|
|
306
|
-
"ohmyhost project create --organization ULID --name NAME [--data-mode shared|isolated] [--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)",
|
|
308
|
+
"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)",
|
|
307
309
|
"project list": "ohmyhost project list [--cursor ULID] [--limit LIMIT] --json",
|
|
308
310
|
"project context": "ohmyhost project context --project ULID --json",
|
|
309
311
|
"project notes set":
|
|
310
312
|
"ohmyhost project notes set --project ULID --version NUMBER --markdown TEXT --idempotency-key KEY --json (no credentials or signed URLs)",
|
|
311
313
|
"project status": "ohmyhost project status --project ULID --json",
|
|
312
314
|
"project dev-access create": "ohmyhost project dev-access create --project ULID --json",
|
|
315
|
+
"project dev-share link": "ohmyhost project dev-share link --project ULID --json",
|
|
316
|
+
"project dev-share rotate":
|
|
317
|
+
"ohmyhost project dev-share rotate --project ULID --idempotency-key KEY --yes --json",
|
|
318
|
+
"project dev-share revoke":
|
|
319
|
+
"ohmyhost project dev-share revoke --project ULID --idempotency-key KEY --yes --json",
|
|
320
|
+
"project dev-access mode":
|
|
321
|
+
"ohmyhost project dev-access mode --project ULID --mode protected|public --idempotency-key KEY --yes --json",
|
|
313
322
|
"project handle check":
|
|
314
323
|
"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)",
|
|
315
324
|
"project handle set":
|
|
@@ -347,9 +356,9 @@ const HELP_USAGE = Object.freeze({
|
|
|
347
356
|
"domain paid status": "ohmyhost domain paid status --project ULID --json",
|
|
348
357
|
"domain paid delete":
|
|
349
358
|
"ohmyhost domain paid delete --project ULID --hostname HOST --idempotency-key KEY --yes --json",
|
|
350
|
-
plan: "ohmyhost plan --project ULID --commit SHA --json",
|
|
359
|
+
plan: "ohmyhost plan --project ULID --commit SHA [--environment dev|prod] --json",
|
|
351
360
|
deploy:
|
|
352
|
-
"ohmyhost deploy --project ULID --plan-id ULID --idempotency-key KEY --yes [--wait] --json",
|
|
361
|
+
"ohmyhost deploy --project ULID (--plan-id ULID | --commit SHA [--environment dev|prod]) --idempotency-key KEY --yes [--wait] --json",
|
|
353
362
|
logs: "ohmyhost logs OPERATION_ULID --follow --json",
|
|
354
363
|
"deployment logs": "ohmyhost deployment logs --project ULID --deployment ULID --follow --json",
|
|
355
364
|
"rollback plan": "ohmyhost rollback plan --project ULID --deployment DEPLOYMENT_ULID --json",
|
|
@@ -387,6 +396,8 @@ const HELP_USAGE = Object.freeze({
|
|
|
387
396
|
"mail domain set":
|
|
388
397
|
"ohmyhost mail domain set --project ULID --environment ULID --domain DOMAIN --sending true --receiving false --idempotency-key KEY --json",
|
|
389
398
|
"mail domain status": "ohmyhost mail domain status --project ULID --environment ULID --json",
|
|
399
|
+
"mail domain delete":
|
|
400
|
+
"ohmyhost mail domain delete --project ULID --environment ULID --idempotency-key KEY --yes --json",
|
|
390
401
|
} satisfies Readonly<Record<CustomerCommandLabel, string>>);
|
|
391
402
|
|
|
392
403
|
const HELP_COMMANDS = Object.freeze(
|
|
@@ -740,6 +751,37 @@ export class ProductCli {
|
|
|
740
751
|
if (command.kind === "project-status") return await this.getProjectStatus(command, signal);
|
|
741
752
|
if (command.kind === "project-dev-access-create")
|
|
742
753
|
return await this.createDevAccessTicket(command, signal);
|
|
754
|
+
if (
|
|
755
|
+
command.kind === "project-dev-share-link" ||
|
|
756
|
+
command.kind === "project-dev-share-rotate" ||
|
|
757
|
+
command.kind === "project-dev-share-revoke" ||
|
|
758
|
+
command.kind === "project-dev-access-mode-set"
|
|
759
|
+
) {
|
|
760
|
+
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
761
|
+
const state = parseDevAccessState(
|
|
762
|
+
command.kind === "project-dev-share-link"
|
|
763
|
+
? await api.ensureDevShareLink(command.projectId)
|
|
764
|
+
: command.kind === "project-dev-share-rotate"
|
|
765
|
+
? await api.rotateDevShareLink(command.projectId, command.idempotencyKey)
|
|
766
|
+
: command.kind === "project-dev-share-revoke"
|
|
767
|
+
? await api.revokeDevShareLink(command.projectId, command.idempotencyKey)
|
|
768
|
+
: await api.setDevAccessMode(
|
|
769
|
+
command.projectId,
|
|
770
|
+
command.mode,
|
|
771
|
+
command.idempotencyKey,
|
|
772
|
+
),
|
|
773
|
+
);
|
|
774
|
+
return success(
|
|
775
|
+
command.kind === "project-dev-share-link"
|
|
776
|
+
? "project dev-share link"
|
|
777
|
+
: command.kind === "project-dev-share-rotate"
|
|
778
|
+
? "project dev-share rotate"
|
|
779
|
+
: command.kind === "project-dev-share-revoke"
|
|
780
|
+
? "project dev-share revoke"
|
|
781
|
+
: "project dev-access mode",
|
|
782
|
+
{ status: "succeeded", state },
|
|
783
|
+
);
|
|
784
|
+
}
|
|
743
785
|
if (command.kind === "database-write") {
|
|
744
786
|
const file = await open(command.statementFile, "r"),
|
|
745
787
|
buffer = Buffer.alloc(65_537);
|
|
@@ -1169,6 +1211,7 @@ export class ProductCli {
|
|
|
1169
1211
|
organizationId: command.organizationId,
|
|
1170
1212
|
name: command.name,
|
|
1171
1213
|
...(command.dataMode === undefined ? {} : { dataMode: command.dataMode }),
|
|
1214
|
+
...(command.devAccessMode === undefined ? {} : { devAccessMode: command.devAccessMode }),
|
|
1172
1215
|
...(command.region === undefined ? {} : { region: command.region }),
|
|
1173
1216
|
idempotencyKey: command.idempotencyKey,
|
|
1174
1217
|
}),
|
|
@@ -1451,7 +1494,10 @@ export class ProductCli {
|
|
|
1451
1494
|
): Promise<CliResult> {
|
|
1452
1495
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
1453
1496
|
const autoDeploy = await api.getAutoDeploy(command.projectId);
|
|
1454
|
-
return success("source auto-deploy status", {
|
|
1497
|
+
return success("source auto-deploy status", {
|
|
1498
|
+
status: autoDeploy.generation === 0 ? "not_configured" : "ready",
|
|
1499
|
+
auto_deploy: autoDeploy,
|
|
1500
|
+
});
|
|
1455
1501
|
}
|
|
1456
1502
|
|
|
1457
1503
|
private async authorizeCloudflareDns(
|
|
@@ -1507,7 +1553,16 @@ export class ProductCli {
|
|
|
1507
1553
|
): Promise<CliResult> {
|
|
1508
1554
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
1509
1555
|
const plan = parseDeploymentPlan(await api.planDeployment(command));
|
|
1510
|
-
|
|
1556
|
+
// Say which environment this plan changes and how the change reaches the live site.
|
|
1557
|
+
const notes =
|
|
1558
|
+
plan["environment"] === "prod"
|
|
1559
|
+
? [
|
|
1560
|
+
`This plan builds straight into Prod (${String(plan["route"])}); Prod traffic switches once it is activated.`,
|
|
1561
|
+
]
|
|
1562
|
+
: [
|
|
1563
|
+
`This plan updates Dev only (${String(plan["route"])}). To go live, deploy with --environment prod or promote the Dev deployment with '${this.dependencies.commandPrefix} deployment promote plan --project ULID --deployment DEV_DEPLOYMENT_ULID --json'.`,
|
|
1564
|
+
];
|
|
1565
|
+
return success("plan", { status: "planned", plan, notes });
|
|
1511
1566
|
}
|
|
1512
1567
|
|
|
1513
1568
|
private async deploy(
|
|
@@ -1515,20 +1570,37 @@ export class ProductCli {
|
|
|
1515
1570
|
signal: AbortSignal,
|
|
1516
1571
|
): Promise<CliResult> {
|
|
1517
1572
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
1518
|
-
|
|
1573
|
+
// With --commit the deploy plans first, so an approved deploy needs no separate plan step.
|
|
1574
|
+
const plan =
|
|
1575
|
+
command.commitSha === null
|
|
1576
|
+
? null
|
|
1577
|
+
: parseDeploymentPlan(
|
|
1578
|
+
await api.planDeployment({
|
|
1579
|
+
projectId: command.projectId,
|
|
1580
|
+
commitSha: command.commitSha,
|
|
1581
|
+
environment: command.environment,
|
|
1582
|
+
}),
|
|
1583
|
+
);
|
|
1584
|
+
let operation = parseOperation(
|
|
1585
|
+
await api.createDeployment({
|
|
1586
|
+
projectId: command.projectId,
|
|
1587
|
+
planId: plan === null ? String(command.planId) : String(plan["id"]),
|
|
1588
|
+
idempotencyKey: command.idempotencyKey,
|
|
1589
|
+
}),
|
|
1590
|
+
);
|
|
1519
1591
|
if (command.wait) operation = await this.waitForOperation(api, operation, signal);
|
|
1520
|
-
//
|
|
1521
|
-
// this sentence the first thing a customer sees after a successful deploy looks like a failure.
|
|
1592
|
+
// Dev access follows the project-selected mode; the clean URL alone is insufficient when protected.
|
|
1522
1593
|
const notes =
|
|
1523
1594
|
operation.state === "succeeded"
|
|
1524
1595
|
? [
|
|
1525
|
-
`
|
|
1596
|
+
`Check project status for dev_access_mode. For protected Dev, run '${this.dependencies.commandPrefix} project dev-share link --project ULID --json' and open or share its persistent share_url; public Dev opens directly.`,
|
|
1526
1597
|
]
|
|
1527
1598
|
: [];
|
|
1528
1599
|
return success("deploy", {
|
|
1529
1600
|
status: command.wait ? "completed" : "accepted",
|
|
1530
1601
|
operation,
|
|
1531
|
-
...(
|
|
1602
|
+
...(plan === null ? {} : { plan }),
|
|
1603
|
+
...(notes.length > 0 && plan?.["environment"] !== "prod" ? { notes } : {}),
|
|
1532
1604
|
});
|
|
1533
1605
|
}
|
|
1534
1606
|
|
|
@@ -2044,6 +2116,7 @@ const classifyFailure = (command: CommandLabel, error: unknown, signal: AbortSig
|
|
|
2044
2116
|
undefined,
|
|
2045
2117
|
problem.retryAfterSeconds,
|
|
2046
2118
|
problem.requestId,
|
|
2119
|
+
problem.docsUrl,
|
|
2047
2120
|
);
|
|
2048
2121
|
} catch {
|
|
2049
2122
|
return failure(
|
|
@@ -2094,6 +2167,7 @@ const failure = (
|
|
|
2094
2167
|
operation?: ReturnType<typeof parseOperation>,
|
|
2095
2168
|
retryAfterSeconds?: number,
|
|
2096
2169
|
requestId?: string,
|
|
2170
|
+
docsUrl?: string,
|
|
2097
2171
|
): CliResult => {
|
|
2098
2172
|
const document: CliErrorDocument = {
|
|
2099
2173
|
version: 1,
|
|
@@ -2106,6 +2180,7 @@ const failure = (
|
|
|
2106
2180
|
suggested_action: suggestedAction,
|
|
2107
2181
|
...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
|
|
2108
2182
|
...(requestId === undefined ? {} : { request_id: requestId }),
|
|
2183
|
+
...(docsUrl === undefined ? {} : { docs_url: docsUrl }),
|
|
2109
2184
|
...(operation?.error === undefined ? {} : { message: operation.error.message }),
|
|
2110
2185
|
},
|
|
2111
2186
|
};
|
|
@@ -2144,6 +2219,10 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
|
|
|
2144
2219
|
if (command.kind === "project-notes-set") return "project notes set";
|
|
2145
2220
|
if (command.kind === "project-status") return "project status";
|
|
2146
2221
|
if (command.kind === "project-dev-access-create") return "project dev-access create";
|
|
2222
|
+
if (command.kind === "project-dev-share-link") return "project dev-share link";
|
|
2223
|
+
if (command.kind === "project-dev-share-rotate") return "project dev-share rotate";
|
|
2224
|
+
if (command.kind === "project-dev-share-revoke") return "project dev-share revoke";
|
|
2225
|
+
if (command.kind === "project-dev-access-mode-set") return "project dev-access mode";
|
|
2147
2226
|
if (command.kind === "project-handle-check") return "project handle check";
|
|
2148
2227
|
if (command.kind === "project-handle-set") return "project handle set";
|
|
2149
2228
|
if (command.kind === "database-write") return "database write";
|
|
@@ -200,6 +200,7 @@ export type ProductCliCommand =
|
|
|
200
200
|
readonly organizationId: string;
|
|
201
201
|
readonly name: string;
|
|
202
202
|
readonly dataMode?: "shared" | "isolated";
|
|
203
|
+
readonly devAccessMode?: "protected" | "public";
|
|
203
204
|
readonly region?: "us" | "eu";
|
|
204
205
|
readonly idempotencyKey: string;
|
|
205
206
|
readonly credentialStore: "native";
|
|
@@ -215,6 +216,30 @@ export type ProductCliCommand =
|
|
|
215
216
|
readonly projectId: string;
|
|
216
217
|
readonly credentialStore: "native";
|
|
217
218
|
}
|
|
219
|
+
| {
|
|
220
|
+
readonly kind: "project-dev-share-link";
|
|
221
|
+
readonly projectId: string;
|
|
222
|
+
readonly credentialStore: "native";
|
|
223
|
+
}
|
|
224
|
+
| {
|
|
225
|
+
readonly kind: "project-dev-share-rotate";
|
|
226
|
+
readonly projectId: string;
|
|
227
|
+
readonly idempotencyKey: string;
|
|
228
|
+
readonly credentialStore: "native";
|
|
229
|
+
}
|
|
230
|
+
| {
|
|
231
|
+
readonly kind: "project-dev-share-revoke";
|
|
232
|
+
readonly projectId: string;
|
|
233
|
+
readonly idempotencyKey: string;
|
|
234
|
+
readonly credentialStore: "native";
|
|
235
|
+
}
|
|
236
|
+
| {
|
|
237
|
+
readonly kind: "project-dev-access-mode-set";
|
|
238
|
+
readonly projectId: string;
|
|
239
|
+
readonly mode: "protected" | "public";
|
|
240
|
+
readonly idempotencyKey: string;
|
|
241
|
+
readonly credentialStore: "native";
|
|
242
|
+
}
|
|
218
243
|
| {
|
|
219
244
|
readonly kind: "project-dev-access-create";
|
|
220
245
|
readonly projectId: string;
|
|
@@ -294,12 +319,16 @@ export type ProductCliCommand =
|
|
|
294
319
|
readonly kind: "plan";
|
|
295
320
|
readonly projectId: string;
|
|
296
321
|
readonly commitSha: string;
|
|
322
|
+
readonly environment: "dev" | "prod";
|
|
297
323
|
readonly credentialStore: "native";
|
|
298
324
|
}
|
|
299
325
|
| {
|
|
300
326
|
readonly kind: "deploy";
|
|
301
327
|
readonly projectId: string;
|
|
302
|
-
|
|
328
|
+
/** Exactly one of an existing plan or a commit that is planned first. */
|
|
329
|
+
readonly planId: string | null;
|
|
330
|
+
readonly commitSha: string | null;
|
|
331
|
+
readonly environment: "dev" | "prod";
|
|
303
332
|
readonly idempotencyKey: string;
|
|
304
333
|
readonly yes: true;
|
|
305
334
|
readonly wait: boolean;
|
|
@@ -403,6 +432,7 @@ export type ProductCliCommand =
|
|
|
403
432
|
readonly label:
|
|
404
433
|
| "mail domain set"
|
|
405
434
|
| "mail domain status"
|
|
435
|
+
| "mail domain delete"
|
|
406
436
|
| "mail setup"
|
|
407
437
|
| "mail status"
|
|
408
438
|
| "mail webhook set"
|
|
@@ -507,6 +537,10 @@ export type RecognizedCommandLabel =
|
|
|
507
537
|
| "project list"
|
|
508
538
|
| "project status"
|
|
509
539
|
| "project dev-access create"
|
|
540
|
+
| "project dev-share link"
|
|
541
|
+
| "project dev-share rotate"
|
|
542
|
+
| "project dev-share revoke"
|
|
543
|
+
| "project dev-access mode"
|
|
510
544
|
| "project handle check"
|
|
511
545
|
| "project handle set"
|
|
512
546
|
| "database write"
|
|
@@ -548,7 +582,8 @@ export type RecognizedCommandLabel =
|
|
|
548
582
|
| "mail messages get"
|
|
549
583
|
| "mail messages retry"
|
|
550
584
|
| "mail domain set"
|
|
551
|
-
| "mail domain status"
|
|
585
|
+
| "mail domain status"
|
|
586
|
+
| "mail domain delete";
|
|
552
587
|
|
|
553
588
|
export const recognizedCommandLabel = (argv: readonly string[]): RecognizedCommandLabel => {
|
|
554
589
|
const values = [...argv];
|
|
@@ -610,6 +645,18 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
|
|
|
610
645
|
return "project handle check";
|
|
611
646
|
if (value === "project" && values[0] === "handle" && values[1] === "set")
|
|
612
647
|
return "project handle set";
|
|
648
|
+
if (
|
|
649
|
+
value === "project" &&
|
|
650
|
+
values[0] === "dev-share" &&
|
|
651
|
+
["link", "rotate", "revoke"].includes(values[1] ?? "")
|
|
652
|
+
)
|
|
653
|
+
return values[1] === "link"
|
|
654
|
+
? "project dev-share link"
|
|
655
|
+
: values[1] === "rotate"
|
|
656
|
+
? "project dev-share rotate"
|
|
657
|
+
: "project dev-share revoke";
|
|
658
|
+
if (value === "project" && values[0] === "dev-access" && values[1] === "mode")
|
|
659
|
+
return "project dev-access mode";
|
|
613
660
|
if (value === "project" && values[0] === "dev-access" && values[1] === "create")
|
|
614
661
|
return "project dev-access create";
|
|
615
662
|
if (value === "database" && values[0] === "compute" && values[1] === "set")
|
|
@@ -669,6 +716,7 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
|
|
|
669
716
|
if (value === "mail" && values[0] === "domain") {
|
|
670
717
|
if (values[1] === "set") return "mail domain set";
|
|
671
718
|
if (values[1] === "status") return "mail domain status";
|
|
719
|
+
if (values[1] === "delete") return "mail domain delete";
|
|
672
720
|
}
|
|
673
721
|
return "unknown";
|
|
674
722
|
}
|
|
@@ -1116,6 +1164,38 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
|
|
|
1116
1164
|
const options = parseOptions(values, ["--handle"]);
|
|
1117
1165
|
return { kind: "project-handle-check", handle: options["--handle"], credentialStore };
|
|
1118
1166
|
}
|
|
1167
|
+
if (subcommand === "dev-share") {
|
|
1168
|
+
const action = values.shift();
|
|
1169
|
+
if (action === "link") {
|
|
1170
|
+
if (yes || wait || follow || stdin) throw new InvalidCommandError();
|
|
1171
|
+
const options = parseOptions(values, ["--project"]);
|
|
1172
|
+
return { kind: "project-dev-share-link", projectId: options["--project"], credentialStore };
|
|
1173
|
+
}
|
|
1174
|
+
if ((action === "rotate" || action === "revoke") && yes && !wait && !follow && !stdin) {
|
|
1175
|
+
const options = parseOptions(values, ["--project", "--idempotency-key"]);
|
|
1176
|
+
return {
|
|
1177
|
+
kind: action === "rotate" ? "project-dev-share-rotate" : "project-dev-share-revoke",
|
|
1178
|
+
projectId: options["--project"],
|
|
1179
|
+
idempotencyKey: options["--idempotency-key"],
|
|
1180
|
+
credentialStore,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
throw new InvalidCommandError();
|
|
1184
|
+
}
|
|
1185
|
+
if (subcommand === "dev-access" && values[0] === "mode") {
|
|
1186
|
+
values.shift();
|
|
1187
|
+
if (!yes || wait || follow || stdin) throw new InvalidCommandError();
|
|
1188
|
+
const options = parseOptions(values, ["--project", "--mode", "--idempotency-key"]);
|
|
1189
|
+
if (options["--mode"] !== "protected" && options["--mode"] !== "public")
|
|
1190
|
+
throw new InvalidCommandError();
|
|
1191
|
+
return {
|
|
1192
|
+
kind: "project-dev-access-mode-set",
|
|
1193
|
+
projectId: options["--project"],
|
|
1194
|
+
mode: options["--mode"],
|
|
1195
|
+
idempotencyKey: options["--idempotency-key"],
|
|
1196
|
+
credentialStore,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1119
1199
|
if (subcommand === "dev-access" && values.shift() === "create") {
|
|
1120
1200
|
if (yes || wait || follow || stdin) throw new InvalidCommandError();
|
|
1121
1201
|
const options = parseOptions(values, ["--project"]);
|
|
@@ -1177,15 +1257,20 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
|
|
|
1177
1257
|
"--name",
|
|
1178
1258
|
"--idempotency-key",
|
|
1179
1259
|
"--data-mode",
|
|
1260
|
+
"--dev-access-mode",
|
|
1180
1261
|
"--region",
|
|
1181
1262
|
]);
|
|
1182
1263
|
const dataMode = options["--data-mode"];
|
|
1264
|
+
const devAccessMode = options["--dev-access-mode"];
|
|
1183
1265
|
const region = options["--region"];
|
|
1184
1266
|
if (
|
|
1185
1267
|
!options["--organization"] ||
|
|
1186
1268
|
!options["--name"] ||
|
|
1187
1269
|
!options["--idempotency-key"] ||
|
|
1188
1270
|
(dataMode !== undefined && dataMode !== "shared" && dataMode !== "isolated") ||
|
|
1271
|
+
(devAccessMode !== undefined &&
|
|
1272
|
+
devAccessMode !== "protected" &&
|
|
1273
|
+
devAccessMode !== "public") ||
|
|
1189
1274
|
(region !== undefined && region !== "us" && region !== "eu")
|
|
1190
1275
|
)
|
|
1191
1276
|
throw new InvalidCommandError();
|
|
@@ -1194,6 +1279,7 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
|
|
|
1194
1279
|
organizationId: options["--organization"],
|
|
1195
1280
|
name: options["--name"],
|
|
1196
1281
|
...(dataMode === undefined ? {} : { dataMode }),
|
|
1282
|
+
...(devAccessMode === undefined ? {} : { devAccessMode }),
|
|
1197
1283
|
...(region === undefined ? {} : { region }),
|
|
1198
1284
|
idempotencyKey: options["--idempotency-key"],
|
|
1199
1285
|
credentialStore,
|
|
@@ -1465,22 +1551,46 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
|
|
|
1465
1551
|
}
|
|
1466
1552
|
if (first === "plan") {
|
|
1467
1553
|
if (yes || wait || follow || stdin) throw new InvalidCommandError();
|
|
1468
|
-
const options =
|
|
1554
|
+
const options = parseOptionalOptions(values, ["--project", "--commit", "--environment"]);
|
|
1555
|
+
const projectId = options["--project"];
|
|
1556
|
+
const commitSha = options["--commit"];
|
|
1557
|
+
if (projectId === undefined || commitSha === undefined) throw new InvalidCommandError();
|
|
1469
1558
|
return {
|
|
1470
1559
|
kind: "plan",
|
|
1471
|
-
projectId
|
|
1472
|
-
commitSha
|
|
1560
|
+
projectId,
|
|
1561
|
+
commitSha,
|
|
1562
|
+
environment: deploymentEnvironment(options["--environment"]),
|
|
1473
1563
|
credentialStore,
|
|
1474
1564
|
};
|
|
1475
1565
|
}
|
|
1476
1566
|
if (first === "deploy") {
|
|
1477
1567
|
if (!yes || follow || stdin) throw new InvalidCommandError();
|
|
1478
|
-
const options =
|
|
1568
|
+
const options = parseOptionalOptions(values, [
|
|
1569
|
+
"--project",
|
|
1570
|
+
"--plan-id",
|
|
1571
|
+
"--commit",
|
|
1572
|
+
"--environment",
|
|
1573
|
+
"--idempotency-key",
|
|
1574
|
+
]);
|
|
1575
|
+
const projectId = options["--project"];
|
|
1576
|
+
const idempotencyKey = options["--idempotency-key"];
|
|
1577
|
+
const planId = options["--plan-id"] ?? null;
|
|
1578
|
+
const commitSha = options["--commit"] ?? null;
|
|
1579
|
+
// A plan already names its environment, so --environment only accompanies --commit.
|
|
1580
|
+
if (
|
|
1581
|
+
projectId === undefined ||
|
|
1582
|
+
idempotencyKey === undefined ||
|
|
1583
|
+
(planId === null) === (commitSha === null) ||
|
|
1584
|
+
(planId !== null && options["--environment"] !== undefined)
|
|
1585
|
+
)
|
|
1586
|
+
throw new InvalidCommandError();
|
|
1479
1587
|
return {
|
|
1480
1588
|
kind: "deploy",
|
|
1481
|
-
projectId
|
|
1482
|
-
planId
|
|
1483
|
-
|
|
1589
|
+
projectId,
|
|
1590
|
+
planId,
|
|
1591
|
+
commitSha,
|
|
1592
|
+
environment: deploymentEnvironment(options["--environment"]),
|
|
1593
|
+
idempotencyKey,
|
|
1484
1594
|
yes,
|
|
1485
1595
|
wait,
|
|
1486
1596
|
credentialStore,
|
|
@@ -1752,10 +1862,12 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
|
|
|
1752
1862
|
throw new InvalidCommandError();
|
|
1753
1863
|
}
|
|
1754
1864
|
if (first === "mail") {
|
|
1755
|
-
if (yes || wait || follow || stdin) throw new InvalidCommandError();
|
|
1756
1865
|
const area = values.shift();
|
|
1757
1866
|
const sub =
|
|
1758
1867
|
area === "webhook" || area === "messages" || area === "domain" ? values.shift() : undefined;
|
|
1868
|
+
// Only retiring the mail domain is destructive, and it requires --yes.
|
|
1869
|
+
const retiring = area === "domain" && sub === "delete";
|
|
1870
|
+
if (yes !== retiring || wait || follow || stdin) throw new InvalidCommandError();
|
|
1759
1871
|
const options = parseOptionalOptions(values, [
|
|
1760
1872
|
"--project",
|
|
1761
1873
|
"--environment",
|
|
@@ -1792,6 +1904,13 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
|
|
|
1792
1904
|
} else if (area === "status" || (area === "domain" && sub === "status")) {
|
|
1793
1905
|
request = { ...scope, action: "status" };
|
|
1794
1906
|
label = area === "domain" ? "mail domain status" : "mail status";
|
|
1907
|
+
} else if (retiring) {
|
|
1908
|
+
request = {
|
|
1909
|
+
...scope,
|
|
1910
|
+
action: "domain_delete",
|
|
1911
|
+
idempotencyKey: required("--idempotency-key"),
|
|
1912
|
+
};
|
|
1913
|
+
label = "mail domain delete";
|
|
1795
1914
|
} else if (area === "webhook" && sub === "set") {
|
|
1796
1915
|
request = {
|
|
1797
1916
|
...scope,
|
|
@@ -1857,6 +1976,12 @@ const parseOptions = <T extends string>(
|
|
|
1857
1976
|
return result;
|
|
1858
1977
|
};
|
|
1859
1978
|
|
|
1979
|
+
const deploymentEnvironment = (value: string | undefined): "dev" | "prod" => {
|
|
1980
|
+
if (value === undefined || value === "dev") return "dev";
|
|
1981
|
+
if (value === "prod") return "prod";
|
|
1982
|
+
throw new InvalidCommandError();
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1860
1985
|
const parseOptionalOptions = <T extends string>(
|
|
1861
1986
|
values: readonly string[],
|
|
1862
1987
|
allowed: readonly T[],
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
} from "./credential-store.js";
|
|
45
45
|
import { GeneratedSdkProductApi, type ProductApi } from "./product-api.js";
|
|
46
46
|
import { workspaceOf, type WorkspaceSession } from "./workspace.js";
|
|
47
|
-
import { OwnerOnlyProjectLinkStore } from "./project-link-store.js";
|
|
47
|
+
import { OwnerOnlyProjectLinkStore, readLinkedProjectId } from "./project-link-store.js";
|
|
48
48
|
import {
|
|
49
49
|
initializeRepository,
|
|
50
50
|
RepositoryInitBlockedError,
|
|
@@ -67,6 +67,8 @@ export interface ProductCliIo {
|
|
|
67
67
|
|
|
68
68
|
export interface ProductCliCompositionOverrides {
|
|
69
69
|
readonly credentialStore?: CredentialStore;
|
|
70
|
+
/** Directory whose project link supplies a missing --project; defaults to the process directory. */
|
|
71
|
+
readonly workingDirectory?: string;
|
|
70
72
|
readonly readSecretValue?: (signal: AbortSignal) => Promise<string>;
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -221,7 +223,16 @@ export const runProductionCli = async (
|
|
|
221
223
|
const diagnostics = new StderrDiagnosticSink(io);
|
|
222
224
|
let command: ProductCliCommand;
|
|
223
225
|
try {
|
|
224
|
-
|
|
226
|
+
const parsed = await parseWithLinkedProject(argv, async () => {
|
|
227
|
+
const profile = resolveProductCliEnvironment(environment);
|
|
228
|
+
return readLinkedProjectId(
|
|
229
|
+
join(overrides.workingDirectory ?? process.cwd(), profile.linkDirectory),
|
|
230
|
+
profile.apiOrigin,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
command = parsed.command;
|
|
234
|
+
if (parsed.linkedProjectId !== null)
|
|
235
|
+
io.writeStderr(`ohmyhost: using linked project ${parsed.linkedProjectId}\n`);
|
|
225
236
|
} catch (error) {
|
|
226
237
|
const result =
|
|
227
238
|
error instanceof InvalidCommandError
|
|
@@ -344,7 +355,10 @@ export const runProductionCli = async (
|
|
|
344
355
|
signal,
|
|
345
356
|
},
|
|
346
357
|
),
|
|
347
|
-
projectLinkStore: new OwnerOnlyProjectLinkStore(
|
|
358
|
+
projectLinkStore: new OwnerOnlyProjectLinkStore(
|
|
359
|
+
join(process.cwd(), profile.linkDirectory),
|
|
360
|
+
process.cwd(),
|
|
361
|
+
),
|
|
348
362
|
productApiOrigin: profile.apiOrigin,
|
|
349
363
|
tokenRefresher: new PublicWorkosTokenRefresher(environment),
|
|
350
364
|
tokenRevoker: lifecycle,
|
|
@@ -588,6 +602,36 @@ export class GeneratedSdkSessionRevoker implements TokenRevoker {
|
|
|
588
602
|
}
|
|
589
603
|
}
|
|
590
604
|
|
|
605
|
+
/**
|
|
606
|
+
* A command run inside a linked directory may omit --project; the link supplies it. The
|
|
607
|
+
* link is read only when the command needs a project it did not name.
|
|
608
|
+
*/
|
|
609
|
+
export async function parseWithLinkedProject(
|
|
610
|
+
argv: readonly string[],
|
|
611
|
+
linkedProjectId: () => Promise<string | null>,
|
|
612
|
+
): Promise<{ readonly command: ProductCliCommand; readonly linkedProjectId: string | null }> {
|
|
613
|
+
try {
|
|
614
|
+
return { command: parseProductCliCommand(argv), linkedProjectId: null };
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (!(error instanceof InvalidCommandError) || argv.includes("--project")) throw error;
|
|
617
|
+
let projectId: string | null;
|
|
618
|
+
try {
|
|
619
|
+
projectId = await linkedProjectId();
|
|
620
|
+
} catch {
|
|
621
|
+
projectId = null;
|
|
622
|
+
}
|
|
623
|
+
if (projectId === null) throw error;
|
|
624
|
+
try {
|
|
625
|
+
return {
|
|
626
|
+
command: parseProductCliCommand([...argv, "--project", projectId]),
|
|
627
|
+
linkedProjectId: projectId,
|
|
628
|
+
};
|
|
629
|
+
} catch {
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
591
635
|
class StderrDiagnosticSink implements DiagnosticSink {
|
|
592
636
|
public constructor(private readonly io: ProductCliIo) {}
|
|
593
637
|
|