@amerged/ohmyhost-mcp 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-QT3HUPU3.js → chunk-475EJ7IK.js} +3 -3
- package/dist/index.js +1 -1
- package/dist/stdio-main.js +77 -5
- package/package.json +1 -1
- package/src/apps/mcp/local-client.ts +17 -0
- package/src/apps/mcp/local-server.ts +28 -1
- package/src/apps/product-cli/cli.ts +25 -0
- package/src/apps/product-cli/command.ts +30 -0
- package/src/apps/product-cli/composition.ts +5 -3
- package/src/apps/product-cli/product-api.ts +24 -0
- package/src/apps/product-cli/repository-init.ts +25 -1
- package/src/apps/product-cli/runtime-contract.ts +41 -6
- package/src/packages/agent-skills/generated-skill-resources.ts +2 -2
- package/src/packages/sdk-ts/generated/index.ts +6 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +50 -0
- package/src/packages/sdk-ts/generated/types.gen.ts +83 -0
|
@@ -254,6 +254,7 @@ interface CliErrorDocument {
|
|
|
254
254
|
readonly suggested_action: string;
|
|
255
255
|
readonly message?: string;
|
|
256
256
|
readonly retry_after_seconds?: number;
|
|
257
|
+
readonly request_id?: string;
|
|
257
258
|
};
|
|
258
259
|
}
|
|
259
260
|
|
|
@@ -312,6 +313,8 @@ const HELP_USAGE = Object.freeze({
|
|
|
312
313
|
"project dev-access create": "ohmyhost project dev-access create --project ULID --json",
|
|
313
314
|
"project handle check":
|
|
314
315
|
"ohmyhost project handle check --handle HANDLE --json (is this address free? answers with a reason and free alternatives; the address becomes HANDLE.check.omh.st)",
|
|
316
|
+
"project handle set":
|
|
317
|
+
"ohmyhost project handle set --project ULID --handle HANDLE --if-match ETAG --idempotency-key KEY --json (moves the project to a free address; the old one stops working and anyone may claim it)",
|
|
315
318
|
"database compute set":
|
|
316
319
|
"ohmyhost database compute set --project ULID --environment dev|prod --profile standard|performance --idempotency-key KEY --yes [--wait] --json\n" +
|
|
317
320
|
" standard: Free 0.25 CU/1 GB/60s idle, Paid 0.5 CU/2 GB/60s idle. performance: Paid 1 CU/4 GB/300s idle, 2.5x database compute credits per equal active minute; longer idle time also consumes credits.",
|
|
@@ -691,6 +694,8 @@ export class ProductCli {
|
|
|
691
694
|
if (command.kind === "project-list") return await this.listProjects(command, signal);
|
|
692
695
|
if (command.kind === "project-handle-check")
|
|
693
696
|
return await this.checkProjectHandle(command, signal);
|
|
697
|
+
if (command.kind === "project-handle-set")
|
|
698
|
+
return await this.changeProjectHandle(command, signal);
|
|
694
699
|
if (command.kind === "database-compute-set") {
|
|
695
700
|
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
696
701
|
let operation = parseOperation(await api.changeDatabaseCompute(command));
|
|
@@ -1169,6 +1174,22 @@ export class ProductCli {
|
|
|
1169
1174
|
return success("project list", { status: "succeeded", projects });
|
|
1170
1175
|
}
|
|
1171
1176
|
|
|
1177
|
+
private async changeProjectHandle(
|
|
1178
|
+
command: Extract<ProductCliCommand, { kind: "project-handle-set" }>,
|
|
1179
|
+
signal: AbortSignal,
|
|
1180
|
+
): Promise<CliResult> {
|
|
1181
|
+
const api = await this.authenticatedApi(command.credentialStore, signal);
|
|
1182
|
+
const operation = parseOperation(
|
|
1183
|
+
await api.changeProjectHandle({
|
|
1184
|
+
projectId: command.projectId,
|
|
1185
|
+
handle: command.handle,
|
|
1186
|
+
ifMatch: command.ifMatch,
|
|
1187
|
+
idempotencyKey: command.idempotencyKey,
|
|
1188
|
+
}),
|
|
1189
|
+
);
|
|
1190
|
+
return success("project handle set", { status: "accepted", operation });
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1172
1193
|
private async checkProjectHandle(
|
|
1173
1194
|
command: Extract<ProductCliCommand, { kind: "project-handle-check" }>,
|
|
1174
1195
|
signal: AbortSignal,
|
|
@@ -2030,6 +2051,7 @@ const classifyFailure = (command: CommandLabel, error: unknown, signal: AbortSig
|
|
|
2030
2051
|
problem.suggestedAction,
|
|
2031
2052
|
undefined,
|
|
2032
2053
|
problem.retryAfterSeconds,
|
|
2054
|
+
problem.requestId,
|
|
2033
2055
|
);
|
|
2034
2056
|
} catch {
|
|
2035
2057
|
return failure(
|
|
@@ -2079,6 +2101,7 @@ const failure = (
|
|
|
2079
2101
|
suggestedAction: string,
|
|
2080
2102
|
operation?: ReturnType<typeof parseOperation>,
|
|
2081
2103
|
retryAfterSeconds?: number,
|
|
2104
|
+
requestId?: string,
|
|
2082
2105
|
): CliResult => {
|
|
2083
2106
|
const document: CliErrorDocument = {
|
|
2084
2107
|
version: 1,
|
|
@@ -2090,6 +2113,7 @@ const failure = (
|
|
|
2090
2113
|
retryable,
|
|
2091
2114
|
suggested_action: suggestedAction,
|
|
2092
2115
|
...(retryAfterSeconds === undefined ? {} : { retry_after_seconds: retryAfterSeconds }),
|
|
2116
|
+
...(requestId === undefined ? {} : { request_id: requestId }),
|
|
2093
2117
|
...(operation?.error === undefined ? {} : { message: operation.error.message }),
|
|
2094
2118
|
},
|
|
2095
2119
|
};
|
|
@@ -2129,6 +2153,7 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
|
|
|
2129
2153
|
if (command.kind === "project-status") return "project status";
|
|
2130
2154
|
if (command.kind === "project-dev-access-create") return "project dev-access create";
|
|
2131
2155
|
if (command.kind === "project-handle-check") return "project handle check";
|
|
2156
|
+
if (command.kind === "project-handle-set") return "project handle set";
|
|
2132
2157
|
if (command.kind === "database-write") return "database write";
|
|
2133
2158
|
if (command.kind === "database-query") return "database query";
|
|
2134
2159
|
if (command.kind === "database-access-create") return "database access create";
|
|
@@ -132,6 +132,15 @@ export type ProductCliCommand =
|
|
|
132
132
|
readonly idempotencyKey: string;
|
|
133
133
|
readonly credentialStore: "native";
|
|
134
134
|
}
|
|
135
|
+
| {
|
|
136
|
+
/** Moves a project to a chosen address once its gateways serve it. */
|
|
137
|
+
readonly kind: "project-handle-set";
|
|
138
|
+
readonly projectId: string;
|
|
139
|
+
readonly handle: string;
|
|
140
|
+
readonly ifMatch: string;
|
|
141
|
+
readonly idempotencyKey: string;
|
|
142
|
+
readonly credentialStore: "native";
|
|
143
|
+
}
|
|
135
144
|
| {
|
|
136
145
|
/** Asks whether a project address is free; the candidate is sent exactly as the agent typed it. */
|
|
137
146
|
readonly kind: "project-handle-check";
|
|
@@ -494,6 +503,7 @@ export type RecognizedCommandLabel =
|
|
|
494
503
|
| "project status"
|
|
495
504
|
| "project dev-access create"
|
|
496
505
|
| "project handle check"
|
|
506
|
+
| "project handle set"
|
|
497
507
|
| "database write"
|
|
498
508
|
| "database query"
|
|
499
509
|
| "database access create"
|
|
@@ -585,6 +595,8 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
|
|
|
585
595
|
if (value === "project" && values[0] === "status") return "project status";
|
|
586
596
|
if (value === "project" && values[0] === "handle" && values[1] === "check")
|
|
587
597
|
return "project handle check";
|
|
598
|
+
if (value === "project" && values[0] === "handle" && values[1] === "set")
|
|
599
|
+
return "project handle set";
|
|
588
600
|
if (value === "project" && values[0] === "dev-access" && values[1] === "create")
|
|
589
601
|
return "project dev-access create";
|
|
590
602
|
if (value === "database" && values[0] === "compute" && values[1] === "set")
|
|
@@ -1054,6 +1066,24 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
|
|
|
1054
1066
|
}
|
|
1055
1067
|
if (first === "project") {
|
|
1056
1068
|
const subcommand = values.shift();
|
|
1069
|
+
if (subcommand === "handle" && values[0] === "set") {
|
|
1070
|
+
values.shift();
|
|
1071
|
+
if (yes || wait || follow || stdin) throw new InvalidCommandError();
|
|
1072
|
+
const options = parseOptions(values, [
|
|
1073
|
+
"--project",
|
|
1074
|
+
"--handle",
|
|
1075
|
+
"--if-match",
|
|
1076
|
+
"--idempotency-key",
|
|
1077
|
+
]);
|
|
1078
|
+
return {
|
|
1079
|
+
kind: "project-handle-set",
|
|
1080
|
+
projectId: options["--project"],
|
|
1081
|
+
handle: options["--handle"],
|
|
1082
|
+
ifMatch: options["--if-match"],
|
|
1083
|
+
idempotencyKey: options["--idempotency-key"],
|
|
1084
|
+
credentialStore,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1057
1087
|
if (subcommand === "handle" && values.shift() === "check") {
|
|
1058
1088
|
if (yes || wait || follow || stdin) throw new InvalidCommandError();
|
|
1059
1089
|
const options = parseOptions(values, ["--handle"]);
|
|
@@ -429,9 +429,11 @@ const runOfflineInit = async (
|
|
|
429
429
|
false,
|
|
430
430
|
error instanceof RepositoryInitConflictError
|
|
431
431
|
? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying."
|
|
432
|
-
: error instanceof RepositoryInitError && error.code === "
|
|
433
|
-
? "
|
|
434
|
-
:
|
|
432
|
+
: error instanceof RepositoryInitError && error.code === "repository_root_required"
|
|
433
|
+
? "Run ohmyhost init from the Git repository root with --root set to the application directory (for example --root website); commit the root ohmyhost.yaml before planning."
|
|
434
|
+
: error instanceof RepositoryInitError && error.code === "project_name_invalid"
|
|
435
|
+
? "Pass a lowercase deployment slug with '--project', then rerun init."
|
|
436
|
+
: "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
|
|
435
437
|
);
|
|
436
438
|
io.writeStdout(result.stdout);
|
|
437
439
|
return result.exitCode;
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
listEnvironmentSecrets,
|
|
84
84
|
listFunctionRuns,
|
|
85
85
|
listProjects,
|
|
86
|
+
changeProjectHandle,
|
|
86
87
|
getProjectHandleAvailability,
|
|
87
88
|
planDeployment,
|
|
88
89
|
planPaidProjectDomain,
|
|
@@ -211,6 +212,12 @@ export interface ProductApi {
|
|
|
211
212
|
getAccountProfile(): Promise<readonly PublicWorkspace[]>;
|
|
212
213
|
listProjects(input: { readonly cursor?: string; readonly limit: number }): Promise<ProjectPage>;
|
|
213
214
|
checkProjectHandle(input: { readonly handle: string }): Promise<ProjectHandleAvailability>;
|
|
215
|
+
changeProjectHandle(input: {
|
|
216
|
+
readonly projectId: string;
|
|
217
|
+
readonly handle: string;
|
|
218
|
+
readonly ifMatch: string;
|
|
219
|
+
readonly idempotencyKey: string;
|
|
220
|
+
}): Promise<Operation>;
|
|
214
221
|
getProjectStatus(projectId: string): Promise<ProjectStatus>;
|
|
215
222
|
writeProjectDatabase(
|
|
216
223
|
input: ProjectDatabaseWriteRequest & { projectId: string; idempotencyKey: string },
|
|
@@ -585,6 +592,23 @@ export class GeneratedSdkProductApi implements ProductApi {
|
|
|
585
592
|
return listProjects(input, { client: this.#client });
|
|
586
593
|
}
|
|
587
594
|
|
|
595
|
+
public async changeProjectHandle(input: {
|
|
596
|
+
readonly projectId: string;
|
|
597
|
+
readonly handle: string;
|
|
598
|
+
readonly ifMatch: string;
|
|
599
|
+
readonly idempotencyKey: string;
|
|
600
|
+
}): Promise<Operation> {
|
|
601
|
+
return changeProjectHandle(
|
|
602
|
+
{
|
|
603
|
+
project_id: input.projectId,
|
|
604
|
+
"Idempotency-Key": input.idempotencyKey,
|
|
605
|
+
"If-Match": input.ifMatch,
|
|
606
|
+
handle: input.handle,
|
|
607
|
+
},
|
|
608
|
+
{ client: this.#client },
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
588
612
|
public async checkProjectHandle(input: {
|
|
589
613
|
readonly handle: string;
|
|
590
614
|
}): Promise<ProjectHandleAvailability> {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstat, open, readFile, readdir, stat } from "node:fs/promises";
|
|
3
|
-
import { posix, resolve } from "node:path";
|
|
3
|
+
import { dirname, posix, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
normalizeApplicationRoot,
|
|
@@ -876,6 +876,30 @@ async function assertRepositoryRoot(root: string): Promise<void> {
|
|
|
876
876
|
if (!information.isDirectory()) {
|
|
877
877
|
throw new RepositoryInitError("repository_not_directory", "Repository path is not a directory");
|
|
878
878
|
}
|
|
879
|
+
// A Git worktree uses a .git file; never read or follow that file's target.
|
|
880
|
+
for (let ancestor = root; ; ancestor = dirname(ancestor)) {
|
|
881
|
+
let marker;
|
|
882
|
+
try {
|
|
883
|
+
marker = await lstat(resolve(ancestor, ".git"));
|
|
884
|
+
} catch (error) {
|
|
885
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
886
|
+
throw new RepositoryInitError(
|
|
887
|
+
"repository_unreadable",
|
|
888
|
+
"Could not determine the Git repository root",
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (marker !== undefined) {
|
|
893
|
+
if (ancestor !== root) {
|
|
894
|
+
throw new RepositoryInitError(
|
|
895
|
+
"repository_root_required",
|
|
896
|
+
"Run ohmyhost init from the Git repository root and select the application directory with --root; ohmyhost.yaml must be at the repository root.",
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (dirname(ancestor) === ancestor) return;
|
|
902
|
+
}
|
|
879
903
|
}
|
|
880
904
|
|
|
881
905
|
async function collectRepositoryFiles(root: string): Promise<string[]> {
|
|
@@ -129,6 +129,29 @@ const PROBLEM_POLICY = {
|
|
|
129
129
|
retryable: true,
|
|
130
130
|
action: "Retry project creation with the same idempotency key.",
|
|
131
131
|
},
|
|
132
|
+
project_handle_taken: {
|
|
133
|
+
status: 409,
|
|
134
|
+
retryable: false,
|
|
135
|
+
action:
|
|
136
|
+
"Run 'ohmyhost project handle check' for a free address, then rename to one of the alternatives it returns.",
|
|
137
|
+
},
|
|
138
|
+
project_handle_invalid: {
|
|
139
|
+
status: 409,
|
|
140
|
+
retryable: false,
|
|
141
|
+
action:
|
|
142
|
+
"Choose one to five lowercase words of letters and digits, three to fifty-nine characters, avoiding reserved words.",
|
|
143
|
+
},
|
|
144
|
+
project_handle_unchanged: {
|
|
145
|
+
status: 409,
|
|
146
|
+
retryable: false,
|
|
147
|
+
action: "The project already answers on this address; no rename is needed.",
|
|
148
|
+
},
|
|
149
|
+
project_rename_blocked: {
|
|
150
|
+
status: 409,
|
|
151
|
+
retryable: true,
|
|
152
|
+
action:
|
|
153
|
+
"Wait for the running operation to finish, read the project again, then rename with the new ETag.",
|
|
154
|
+
},
|
|
132
155
|
project_identity_unavailable: {
|
|
133
156
|
status: 503,
|
|
134
157
|
retryable: true,
|
|
@@ -196,6 +219,12 @@ const PROBLEM_POLICY = {
|
|
|
196
219
|
action:
|
|
197
220
|
"Run 'ohmyhost init', apply the matching ohmyhost framework skill, then plan the new commit.",
|
|
198
221
|
},
|
|
222
|
+
repository_configuration_missing: {
|
|
223
|
+
status: 409,
|
|
224
|
+
retryable: false,
|
|
225
|
+
action:
|
|
226
|
+
"Place ohmyhost.yaml at the Git repository root and set applicationRoot to the application directory (for example website). Commit and push the change, then plan the new commit.",
|
|
227
|
+
},
|
|
199
228
|
migration_filename_noncanonical: {
|
|
200
229
|
status: 409,
|
|
201
230
|
retryable: false,
|
|
@@ -278,6 +307,7 @@ export class ResponseContractError extends Error {
|
|
|
278
307
|
}
|
|
279
308
|
|
|
280
309
|
export interface SafeProblem {
|
|
310
|
+
readonly requestId: string;
|
|
281
311
|
readonly retryAfterSeconds?: number;
|
|
282
312
|
readonly code: ProblemCode;
|
|
283
313
|
readonly retryable: boolean;
|
|
@@ -676,9 +706,9 @@ export const parseDevAccessTicket = (value: unknown): PublicDevAccessTicket => {
|
|
|
676
706
|
!isUlid(input["project_id"]) ||
|
|
677
707
|
!isUlid(input["environment_id"]) ||
|
|
678
708
|
typeof origin !== "string" ||
|
|
679
|
-
!/^https:\/\/dev-[a-
|
|
709
|
+
!/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st$/u.test(origin) ||
|
|
680
710
|
typeof redeemUrl !== "string" ||
|
|
681
|
-
!/^https:\/\/dev-[a-
|
|
711
|
+
!/^https:\/\/dev-[a-z0-9]+(?:-[a-z0-9]+){0,4}\.(?:dev\.)?check\.omh\.st\/\.ohmyhost\/dev-access\/redeem\?stage=dev&ticket=[A-Za-z0-9_-]{43}$/u.test(
|
|
682
712
|
redeemUrl,
|
|
683
713
|
) ||
|
|
684
714
|
new URL(redeemUrl).origin !== origin ||
|
|
@@ -880,8 +910,8 @@ const parseProject = (value: unknown): PublicProject => {
|
|
|
880
910
|
!isUlid(input["organization_id"]) ||
|
|
881
911
|
!boundedString(input["name"], 1, 128) ||
|
|
882
912
|
input["name"] !== input["name"].trim() ||
|
|
883
|
-
!boundedString(input["handle"],
|
|
884
|
-
!/^[a-
|
|
913
|
+
!boundedString(input["handle"], 3, 59) ||
|
|
914
|
+
!/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(input["handle"]) ||
|
|
885
915
|
(input["data_mode"] !== "shared" && input["data_mode"] !== "isolated") ||
|
|
886
916
|
(input["region"] !== "us" && input["region"] !== "eu") ||
|
|
887
917
|
!isIsoInstant(input["created_at"])
|
|
@@ -910,6 +940,7 @@ export const parseProjectPage = (
|
|
|
910
940
|
export const parseProjectStatus = (value: unknown): Readonly<Record<string, unknown>> => {
|
|
911
941
|
const input = exactRecord(value, [
|
|
912
942
|
"project_id",
|
|
943
|
+
"etag",
|
|
913
944
|
"handle",
|
|
914
945
|
"region",
|
|
915
946
|
"lifecycle",
|
|
@@ -925,8 +956,10 @@ export const parseProjectStatus = (value: unknown): Readonly<Record<string, unkn
|
|
|
925
956
|
const handle = input["handle"];
|
|
926
957
|
if (
|
|
927
958
|
!isUlid(input["project_id"]) ||
|
|
928
|
-
|
|
929
|
-
!/^
|
|
959
|
+
typeof input["etag"] !== "string" ||
|
|
960
|
+
!/^"sha256-[a-f0-9]{64}"$/u.test(input["etag"]) ||
|
|
961
|
+
!boundedString(handle, 3, 59) ||
|
|
962
|
+
!/^[a-z0-9]+(?:-[a-z0-9]+){0,4}$/u.test(handle) ||
|
|
930
963
|
(input["region"] !== "us" && input["region"] !== "eu") ||
|
|
931
964
|
(input["lifecycle"] !== "active" &&
|
|
932
965
|
input["lifecycle"] !== "deleting" &&
|
|
@@ -1540,6 +1573,7 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
|
|
|
1540
1573
|
!isHttpsUrl(input["type"]) ||
|
|
1541
1574
|
!boundedString(input["title"], 1, 160) ||
|
|
1542
1575
|
!boundedString(input["request_id"], 1, 128) ||
|
|
1576
|
+
/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(input["request_id"]) ||
|
|
1543
1577
|
typeof input["retryable"] !== "boolean" ||
|
|
1544
1578
|
!boundedString(input["suggested_action"], 1, 500) ||
|
|
1545
1579
|
(input["detail"] !== undefined && !boundedString(input["detail"], 1, 2_000)) ||
|
|
@@ -1558,6 +1592,7 @@ export const parseSafeProblem = (value: unknown): SafeProblem => {
|
|
|
1558
1592
|
)
|
|
1559
1593
|
throw new ResponseContractError();
|
|
1560
1594
|
return {
|
|
1595
|
+
requestId: input["request_id"],
|
|
1561
1596
|
code: code as ProblemCode,
|
|
1562
1597
|
retryable: policy.retryable,
|
|
1563
1598
|
suggestedAction:
|