@amerged/ohmyhost-cli 0.1.27 → 0.1.29

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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +1439 -20
  3. package/dist/bin.js +1 -1
  4. package/dist/{chunk-KTRUF6C5.js → chunk-Y4VB4EGQ.js} +39104 -17260
  5. package/dist/index.js +27 -1
  6. package/package.json +1 -1
  7. package/src/apps/product-cli/package.json +4 -2
  8. package/src/apps/product-cli/src/cli.ts +312 -2
  9. package/src/apps/product-cli/src/command.ts +261 -0
  10. package/src/apps/product-cli/src/composition.ts +74 -4
  11. package/src/apps/product-cli/src/index.ts +6 -0
  12. package/src/apps/product-cli/src/product-api.ts +5 -0
  13. package/src/apps/product-cli/src/project-link-store.ts +92 -1
  14. package/src/apps/product-cli/src/repository-init.ts +12 -0
  15. package/src/apps/product-cli/src/runtime-contract.ts +34 -1
  16. package/src/apps/product-cli/src/source-managed-client.ts +189 -0
  17. package/src/apps/product-cli/src/source-publication-store.ts +563 -0
  18. package/src/apps/product-cli/src/source-publisher.ts +195 -0
  19. package/src/apps/product-cli/src/source-workspace.ts +552 -0
  20. package/src/packages/contracts/package.json +10 -0
  21. package/src/packages/contracts/src/index.ts +5 -0
  22. package/src/packages/contracts/src/managed-sources.ts +582 -0
  23. package/src/packages/contracts/src/mcp-source-codec.ts +150 -0
  24. package/src/packages/contracts/src/operation-failure.ts +22 -6
  25. package/src/packages/contracts/src/problem-policy.ts +68 -14
  26. package/src/packages/sdk-ts/package.json +1 -1
  27. package/src/packages/sdk-ts/src/generated/index.ts +92 -0
  28. package/src/packages/sdk-ts/src/generated/sdk.gen.ts +541 -0
  29. package/src/packages/sdk-ts/src/generated/types.gen.ts +912 -4
  30. package/src/packages/workos-auth-contracts/src/tokens.ts +61 -4
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  ACCOUNT_SENSITIVE_PROBLEMS,
4
4
  AccountVerificationError,
5
5
  CREDENTIAL_STORE_UNAVAILABLE_ACTION,
6
+ GeneratedSdkManagedSourceApi,
6
7
  GeneratedSdkProductApi,
7
8
  GeneratedSdkSessionRevoker,
8
9
  INTERNAL_ERROR_ACTION,
@@ -10,6 +11,7 @@ import {
10
11
  InvalidCommandError,
11
12
  LifecycleTransportError,
12
13
  LinkedProjectAmbiguousError,
14
+ LocalSourcePublicationError,
13
15
  NativeKeyringProfileRegistry,
14
16
  NetworkTransportError,
15
17
  OwnerOnlyProjectLinkStore,
@@ -27,7 +29,9 @@ import {
27
29
  RepositoryInitBlockedError,
28
30
  RepositoryInitConflictError,
29
31
  RepositoryInitError,
32
+ SOURCE_SNAPSHOT_LIMITS,
30
33
  SecretInputError,
34
+ SourceWorkspaceError,
31
35
  StderrDeviceAuthorizationPresenter,
32
36
  TokenRefreshRejectedError,
33
37
  UserTokenFileError,
@@ -36,17 +40,22 @@ import {
36
40
  chooseWorkspace,
37
41
  classifyCliFailure,
38
42
  cliMetadataResult,
43
+ completeLocalSourcePublication,
39
44
  createAuthenticatedProductTransport,
40
45
  createBoundedFetch,
41
46
  createProductionAuthenticatedProductApi,
47
+ createSourceSnapshot,
42
48
  defaultProfileName,
43
49
  describeProfile,
44
50
  endedLoginAction,
45
51
  extractProfileName,
46
52
  initializeRepository,
53
+ inspectLocalSourcePublications,
54
+ inspectSourceWorkspace,
47
55
  invalidCommandAction,
48
56
  isProfileName,
49
57
  isProfileUserId,
58
+ isSourcePublicationIdempotencyKey,
50
59
  linkedOrganization,
51
60
  listSavedProfiles,
52
61
  loadProfiles,
@@ -64,14 +73,18 @@ import {
64
73
  parsePoweredByFlag,
65
74
  parseProductCliCommand,
66
75
  parseProjectDataChangePlan,
76
+ parseProjectSource,
77
+ parseProjectSourceBinding,
67
78
  parseSafeProblem,
68
79
  parseWithLinkedProject,
80
+ prepareLocalSourcePublication,
69
81
  prepareUserTokenFile,
70
82
  profileIndexDirectory,
71
83
  profileLimitAction,
72
84
  profileSelectionError,
73
85
  publicProfile,
74
86
  publicWorkspace,
87
+ publishSourceSnapshot,
75
88
  readBoundedResponseBody,
76
89
  readProjectLinks,
77
90
  readSecretValueFromStandardInput,
@@ -86,7 +99,7 @@ import {
86
99
  selectWorkspace,
87
100
  verifiedUserId,
88
101
  workspaceOf
89
- } from "./chunk-KTRUF6C5.js";
102
+ } from "./chunk-Y4VB4EGQ.js";
90
103
  import {
91
104
  CredentialStoreIntegrityError,
92
105
  CredentialStoreUnavailableError,
@@ -239,6 +252,7 @@ export {
239
252
  CREDENTIAL_STORE_UNAVAILABLE_ACTION,
240
253
  CredentialStoreIntegrityError,
241
254
  CredentialStoreUnavailableError,
255
+ GeneratedSdkManagedSourceApi,
242
256
  GeneratedSdkProductApi,
243
257
  GeneratedSdkSessionRevoker,
244
258
  INTERNAL_ERROR_ACTION,
@@ -247,6 +261,7 @@ export {
247
261
  LEGACY_CREDENTIAL_ACCOUNT,
248
262
  LifecycleTransportError,
249
263
  LinkedProjectAmbiguousError,
264
+ LocalSourcePublicationError,
250
265
  NativeKeyringProfileRegistry,
251
266
  NativeNapiKeyringCredentialStore,
252
267
  NetworkTransportError,
@@ -266,7 +281,9 @@ export {
266
281
  RepositoryInitBlockedError,
267
282
  RepositoryInitConflictError,
268
283
  RepositoryInitError,
284
+ SOURCE_SNAPSHOT_LIMITS,
269
285
  SecretInputError,
286
+ SourceWorkspaceError,
270
287
  StderrDeviceAuthorizationPresenter,
271
288
  TokenRefreshRejectedError,
272
289
  UserTokenFileError,
@@ -275,18 +292,23 @@ export {
275
292
  chooseWorkspace,
276
293
  classifyCliFailure,
277
294
  cliMetadataResult,
295
+ completeLocalSourcePublication,
278
296
  createAuthenticatedProductTransport,
279
297
  createBoundedFetch,
280
298
  createNativeEntryFactory,
281
299
  createProductionAuthenticatedProductApi,
300
+ createSourceSnapshot,
282
301
  defaultProfileName,
283
302
  describeProfile,
284
303
  endedLoginAction,
285
304
  extractProfileName,
286
305
  initializeRepository,
306
+ inspectLocalSourcePublications,
307
+ inspectSourceWorkspace,
287
308
  invalidCommandAction,
288
309
  isProfileName,
289
310
  isProfileUserId,
311
+ isSourcePublicationIdempotencyKey,
290
312
  linkedOrganization,
291
313
  listSavedProfiles,
292
314
  loadProfiles,
@@ -305,14 +327,18 @@ export {
305
327
  parsePoweredByFlag,
306
328
  parseProductCliCommand,
307
329
  parseProjectDataChangePlan,
330
+ parseProjectSource,
331
+ parseProjectSourceBinding,
308
332
  parseSafeProblem,
309
333
  parseWithLinkedProject,
334
+ prepareLocalSourcePublication,
310
335
  prepareUserTokenFile,
311
336
  profileIndexDirectory,
312
337
  profileLimitAction,
313
338
  profileSelectionError,
314
339
  publicProfile,
315
340
  publicWorkspace,
341
+ publishSourceSnapshot,
316
342
  readBoundedResponseBody,
317
343
  readProjectLinks,
318
344
  readSecretValueFromStandardInput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/product-cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "private": true,
5
5
  "ohmyhost": {
6
6
  "deployment": "production",
@@ -39,7 +39,9 @@
39
39
  "@napi-rs/keyring": "1.3.0",
40
40
  "@ohmyhost/contracts": "workspace:*",
41
41
  "@ohmyhost/sdk-ts": "workspace:*",
42
- "@ohmyhost/workos-auth-contracts": "workspace:*"
42
+ "@ohmyhost/workos-auth-contracts": "workspace:*",
43
+ "ignore": "7.0.12",
44
+ "isomorphic-git": "1.43.0"
43
45
  },
44
46
  "license": "Apache-2.0"
45
47
  }
@@ -1,4 +1,24 @@
1
1
  import { open } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import {
4
+ assertManagedSourceResponse,
5
+ type ManagedProjectSource,
6
+ } from "@ohmyhost/contracts/managed-sources";
7
+ import {
8
+ createSourceSnapshot,
9
+ inspectSourceWorkspace,
10
+ SourceWorkspaceError,
11
+ type SourceWorkspace,
12
+ } from "./source-workspace.js";
13
+ import { publishSourceSnapshot } from "./source-publisher.js";
14
+ import {
15
+ prepareLocalSourcePublication,
16
+ completeLocalSourcePublication,
17
+ inspectLocalSourcePublications,
18
+ LocalSourcePublicationError,
19
+ type LocalSourceMetadataDirectory,
20
+ } from "./source-publication-store.js";
21
+ import { readProjectLinks } from "./project-link-store.js";
2
22
  import {
3
23
  parseProjectDatabaseWrite,
4
24
  assertProjectDatabaseWriteReceipt,
@@ -94,6 +114,7 @@ import {
94
114
  parseOperationEvent,
95
115
  parseProjectPage,
96
116
  parseProjectStatus,
117
+ parseProjectSourceBinding,
97
118
  parseProjectDatabaseQuery,
98
119
  parsePaidDomain,
99
120
  parsePaidDomainPlan,
@@ -212,6 +233,11 @@ export class PublicResponseTooLargeError extends Error {
212
233
  }
213
234
 
214
235
  export interface ProductCliDependencies {
236
+ readonly sourceDirectory?: string;
237
+ readonly sourceMetadataDirectory?: LocalSourceMetadataDirectory;
238
+ readonly sourceWorkspace?: () => Promise<SourceWorkspace>;
239
+ readonly linkedSourceProvider?: "github" | "managed";
240
+ readonly linkedProjectId?: string;
215
241
  readonly apiToken?: string;
216
242
  readonly commandPrefix: string;
217
243
  /** One credential store for a run without saved logins; a chosen saved login uses its own. */
@@ -234,11 +260,15 @@ export interface ProductCliDependencies {
234
260
  * keeping product mutation capabilities out of login.
235
261
  */
236
262
  readonly accountProfile: (accessToken: () => Promise<string>) => Promise<PublicAccount>;
237
- /** Optional onboarding guidance; production supplies only this one read-only SDK query. */
263
+ /** Optional onboarding guidance uses only these narrow read-only SDK queries. */
238
264
  readonly githubConnectionStatus?: (
239
265
  accessToken: () => Promise<string>,
240
266
  organizationId: string,
241
267
  ) => ReturnType<ProductApi["getGithubConnection"]>;
268
+ readonly projectSourceStatus?: (
269
+ accessToken: () => Promise<string>,
270
+ projectId: string,
271
+ ) => Promise<unknown>;
242
272
  readonly projectLinkStore: ProjectLinkStore;
243
273
  readonly productApiOrigin: string;
244
274
  readonly tokenRevoker: TokenRevoker;
@@ -389,6 +419,24 @@ const HELP_USAGE = Object.freeze({
389
419
  "source auto-deploy set":
390
420
  "ohmyhost source auto-deploy set --project ULID --branch BRANCH --enabled true|false --idempotency-key KEY --json",
391
421
  "source auto-deploy status": "ohmyhost source auto-deploy status --project ULID --json",
422
+ "source inspect":
423
+ "ohmyhost source inspect [--directory PATH] --json (read local source and sanitized GitHub context without sign-in)",
424
+ "source status": "ohmyhost source status --project ULID --json",
425
+ "source files": "ohmyhost source files --project ULID [--commit SHA] --json",
426
+ "source file": "ohmyhost source file --project ULID --path PATH [--commit SHA] --json",
427
+ "source versions":
428
+ "ohmyhost source versions --project ULID [--limit 1..100] [--before SHA] --json",
429
+ "source diff": "ohmyhost source diff --project ULID --from SHA --to SHA --json",
430
+ "source upload":
431
+ "ohmyhost source upload --project ULID --operation ULID --json (observe the same upload after uncertainty)",
432
+ "source publish":
433
+ "ohmyhost source publish --project ULID --mode initialize|commit|switch [--directory PATH] --expected-source-generation N [--expected-source-connection ULID] [--expected-commit SHA] --message TEXT --idempotency-key KEY [--wait] --json (current files, no history import; one managed commit)",
434
+ "source publish complete":
435
+ "ohmyhost source publish complete --project ULID [--directory PATH] [--operation ULID] --json (confirm the recorded publication and save this directory's own committed base)",
436
+ "source initialize":
437
+ "ohmyhost source initialize --project ULID --template empty|vite-react --expected-source-generation N --idempotency-key KEY [--wait] --json",
438
+ "source restore":
439
+ "ohmyhost source restore --project ULID --expected-source-generation N --expected-commit SHA --restore-commit SHA --message TEXT --idempotency-key KEY [--wait] --json (restore as a new version; publication is separate)",
392
440
  "domain cloudflare authorize":
393
441
  "ohmyhost domain cloudflare authorize --project ULID --zone ZONE --idempotency-key KEY --json",
394
442
  "domain cloudflare status": "ohmyhost domain cloudflare status --project ULID --json",
@@ -608,6 +656,24 @@ export class ProductCli {
608
656
  if (command.kind === "help" || command.kind === "version")
609
657
  return cliMetadataResult(command.kind, command.kind === "help" ? command.topic : undefined);
610
658
  if (command.kind === "init") throw new ProviderCapabilityBlockedError();
659
+ if (command.kind === "source-inspect")
660
+ return success(name, {
661
+ status: "inspected",
662
+ workspace: await inspectSourceWorkspace(
663
+ resolve(this.dependencies.sourceDirectory ?? process.cwd(), command.directory),
664
+ ),
665
+ pending_publications: await inspectLocalSourcePublications(
666
+ resolve(this.dependencies.sourceDirectory ?? process.cwd(), command.directory),
667
+ ),
668
+ });
669
+ if (
670
+ command.kind === "source-read" ||
671
+ command.kind === "source-publish" ||
672
+ command.kind === "source-initialize" ||
673
+ command.kind === "source-restore" ||
674
+ command.kind === "source-complete"
675
+ )
676
+ return await this.managedSource(command, signal);
611
677
  if (
612
678
  this.dependencies.apiToken !== undefined &&
613
679
  (command.kind === "organization-create" ||
@@ -1083,12 +1149,31 @@ export class ProductCli {
1083
1149
  }
1084
1150
 
1085
1151
  private async githubNextAction(
1086
- api: Pick<ProductApi, "getGithubConnection">,
1152
+ api: Pick<ProductApi, "getGithubConnection"> & {
1153
+ readonly managedSource?: Pick<NonNullable<ProductApi["managedSource"]>, "getSource">;
1154
+ },
1087
1155
  organizationId: string,
1088
1156
  ): Promise<Readonly<Record<string, string>>> {
1089
1157
  // Optional onboarding guidance must not turn a completed login into failure when
1090
1158
  // a scoped key cannot read source connections or the status endpoint is unavailable.
1091
1159
  try {
1160
+ let provider = this.dependencies.linkedSourceProvider;
1161
+ if (this.dependencies.linkedProjectId !== undefined) {
1162
+ if (api.managedSource === undefined) return {};
1163
+ const binding = await api.managedSource.getSource(this.dependencies.linkedProjectId);
1164
+ if (binding !== null) parseProjectSourceBinding(binding);
1165
+ provider =
1166
+ binding === null ? undefined : (binding as { provider: "github" | "managed" }).provider;
1167
+ }
1168
+ if (provider === "managed") return {};
1169
+ if (provider !== "github" && this.dependencies.sourceWorkspace !== undefined) {
1170
+ const workspace = await this.dependencies.sourceWorkspace();
1171
+ if (!workspace.git?.remotes.some((remote) => remote.githubRepository !== null))
1172
+ return {
1173
+ next_action:
1174
+ "Ask once whether to deploy directly with ohmyho.st storing the versions or set up GitHub for this project. Inspect current source first; a missing GitHub connection does not require GitHub.",
1175
+ };
1176
+ }
1092
1177
  const github = parseGithubOrganizationConnectionStatus(
1093
1178
  await api.getGithubConnection(organizationId),
1094
1179
  );
@@ -1347,6 +1432,7 @@ export class ProductCli {
1347
1432
  }
1348
1433
  this.#profile = profile;
1349
1434
  const readGithubConnection = this.dependencies.githubConnectionStatus;
1435
+ const readProjectSource = this.dependencies.projectSourceStatus;
1350
1436
  if (organization !== null)
1351
1437
  return success("login", {
1352
1438
  status: "authenticated",
@@ -1356,6 +1442,17 @@ export class ProductCli {
1356
1442
  ? {}
1357
1443
  : await this.githubNextAction(
1358
1444
  {
1445
+ ...(readProjectSource === undefined
1446
+ ? {}
1447
+ : {
1448
+ managedSource: {
1449
+ getSource: (selectedProjectId) =>
1450
+ readProjectSource(
1451
+ async () => credential.accessToken,
1452
+ selectedProjectId,
1453
+ ),
1454
+ },
1455
+ }),
1359
1456
  getGithubConnection: (selectedOrganizationId) =>
1360
1457
  readGithubConnection(
1361
1458
  async () => credential.accessToken,
@@ -1844,6 +1941,7 @@ export class ProductCli {
1844
1941
  if (
1845
1942
  project.project_id !== command.projectId ||
1846
1943
  source === null ||
1944
+ source.provider !== "github" ||
1847
1945
  source.status !== "ready" ||
1848
1946
  source.repository_full_name.toLowerCase() !==
1849
1947
  `${command.repositoryOwner}/${command.repositoryName}`.toLowerCase()
@@ -1866,6 +1964,201 @@ export class ProductCli {
1866
1964
  return success("link", { status: "completed", operation, organization_id: organizationId });
1867
1965
  }
1868
1966
 
1967
+ private async managedSource(
1968
+ command: Extract<
1969
+ ProductCliCommand,
1970
+ {
1971
+ kind:
1972
+ | "source-read"
1973
+ | "source-publish"
1974
+ | "source-initialize"
1975
+ | "source-restore"
1976
+ | "source-complete";
1977
+ }
1978
+ >,
1979
+ signal: AbortSignal,
1980
+ ): Promise<CliResult> {
1981
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1982
+ const source = api.managedSource;
1983
+ if (source === undefined) throw new ProviderCapabilityBlockedError();
1984
+ const name = parsedCommandName(command);
1985
+ if (command.kind === "source-complete") {
1986
+ const completed = await completeLocalSourcePublication({
1987
+ api: source,
1988
+ getOperation: (id) => api.getOperation(id),
1989
+ directory: resolve(this.dependencies.sourceDirectory ?? process.cwd(), command.directory),
1990
+ metadataDirectory: this.dependencies.sourceMetadataDirectory,
1991
+ apiOrigin: this.dependencies.productApiOrigin,
1992
+ identity: await api.getCurrentIdentity(),
1993
+ projectId: command.projectId,
1994
+ operationId: command.operationId,
1995
+ });
1996
+ return success(name, completed);
1997
+ }
1998
+ if (command.kind === "source-read") {
1999
+ const result =
2000
+ command.action === "status"
2001
+ ? await source.getSource(command.projectId)
2002
+ : command.action === "files" || command.action === "file"
2003
+ ? await source.files({
2004
+ projectId: command.projectId,
2005
+ commitSha: command.commitSha,
2006
+ path: command.path,
2007
+ })
2008
+ : command.action === "versions"
2009
+ ? await source.versions({
2010
+ projectId: command.projectId,
2011
+ limit: command.limit,
2012
+ beforeCommitSha: command.beforeCommitSha,
2013
+ })
2014
+ : command.action === "diff"
2015
+ ? await source.diff({
2016
+ projectId: command.projectId,
2017
+ fromCommitSha: String(command.fromCommitSha),
2018
+ toCommitSha: String(command.toCommitSha),
2019
+ })
2020
+ : await source.getUpload({
2021
+ projectId: command.projectId,
2022
+ operationId: String(command.operationId),
2023
+ });
2024
+ if (command.action === "status") {
2025
+ if (result !== null) parseProjectSourceBinding(result);
2026
+ } else
2027
+ assertManagedSourceResponse(command.action === "file" ? "files" : command.action, result);
2028
+ return success(name, { status: "succeeded", source: result, context: this.context() });
2029
+ }
2030
+ let operation: ReturnType<typeof parseOperation>;
2031
+ let receipt: unknown;
2032
+ let snapshot: { sha256: string; total_bytes: number; files: number } | undefined;
2033
+ if (command.kind === "source-publish") {
2034
+ const directory = resolve(
2035
+ this.dependencies.sourceDirectory ?? process.cwd(),
2036
+ command.directory,
2037
+ );
2038
+ const captured = await createSourceSnapshot({
2039
+ directory,
2040
+ });
2041
+ const prepared = await prepareLocalSourcePublication({
2042
+ directory,
2043
+ metadataDirectory: this.dependencies.sourceMetadataDirectory,
2044
+ apiOrigin: this.dependencies.productApiOrigin,
2045
+ identity: await api.getCurrentIdentity(),
2046
+ projectId: command.projectId,
2047
+ request: command.request,
2048
+ explicitExpectedCommit: command.explicitExpectedCommit,
2049
+ snapshotSha256: captured.sha256,
2050
+ idempotencyKey: command.idempotencyKey,
2051
+ });
2052
+ const published = await publishSourceSnapshot({
2053
+ api: source,
2054
+ projectId: command.projectId,
2055
+ request: prepared.request,
2056
+ snapshot: captured,
2057
+ idempotencyKey: command.idempotencyKey,
2058
+ signal,
2059
+ accepted: prepared.accepted,
2060
+ rejected: prepared.rejected,
2061
+ });
2062
+ operation = parseOperation(published.operation);
2063
+ receipt = published.receipt;
2064
+ snapshot = {
2065
+ sha256: captured.sha256,
2066
+ total_bytes: captured.totalBytes,
2067
+ files: captured.files.length,
2068
+ };
2069
+ } else
2070
+ operation = parseOperation(
2071
+ command.kind === "source-initialize"
2072
+ ? await source.initialize({
2073
+ projectId: command.projectId,
2074
+ body: command.request,
2075
+ idempotencyKey: command.idempotencyKey,
2076
+ })
2077
+ : await source.restore({
2078
+ projectId: command.projectId,
2079
+ body: command.request,
2080
+ idempotencyKey: command.idempotencyKey,
2081
+ }),
2082
+ );
2083
+ if (command.wait) operation = await this.waitForOperation(api, operation, signal);
2084
+ if (operation.state === "succeeded" && command.kind === "source-publish") {
2085
+ const completed = await completeLocalSourcePublication({
2086
+ api: source,
2087
+ getOperation: (id) => api.getOperation(id),
2088
+ directory: resolve(this.dependencies.sourceDirectory ?? process.cwd(), command.directory),
2089
+ metadataDirectory: this.dependencies.sourceMetadataDirectory,
2090
+ apiOrigin: this.dependencies.productApiOrigin,
2091
+ identity: await api.getCurrentIdentity(),
2092
+ projectId: command.projectId,
2093
+ operationId: operation.id,
2094
+ });
2095
+ receipt = completed.receipt;
2096
+ } else if (operation.state === "succeeded") {
2097
+ const current = await source.getSource(command.projectId);
2098
+ parseProjectSourceBinding(current);
2099
+ if (
2100
+ current === null ||
2101
+ typeof current !== "object" ||
2102
+ !("provider" in current) ||
2103
+ current.provider !== "managed"
2104
+ )
2105
+ throw new ResponseContractError();
2106
+ const managed = current as ManagedProjectSource;
2107
+ const expectedGeneration =
2108
+ command.kind === "source-initialize" ||
2109
+ (command.kind === "source-publish" && command.request.mode !== "commit")
2110
+ ? command.request.expected_source_generation + 1
2111
+ : command.request.expected_source_generation;
2112
+ if (managed.source_generation !== expectedGeneration || managed.status !== "ready")
2113
+ throw new ResponseContractError();
2114
+ const [organizationId, ...others] = parseCurrentIdentity(
2115
+ await api.getCurrentIdentity(),
2116
+ ).organization_ids;
2117
+ if (organizationId === undefined || others.length > 0) throw new ResponseContractError();
2118
+ const prior = (
2119
+ await readProjectLinks(
2120
+ join(
2121
+ this.dependencies.sourceDirectory ?? process.cwd(),
2122
+ this.dependencies.sourceMetadataDirectory ?? ".ohmyhost",
2123
+ ),
2124
+ this.dependencies.productApiOrigin,
2125
+ )
2126
+ ).links.find(
2127
+ (link) => link.organization_id === organizationId && link.project_id === command.projectId,
2128
+ );
2129
+ const confirmed =
2130
+ prior?.version === 3 &&
2131
+ prior.source.provider === "managed" &&
2132
+ prior.source.source_connection_id === managed.source_connection_id &&
2133
+ prior.source.source_generation === managed.source_generation
2134
+ ? prior.source.last_confirmed_commit_sha
2135
+ : null;
2136
+ await this.dependencies.projectLinkStore.save({
2137
+ version: 3,
2138
+ api_origin: this.dependencies.productApiOrigin,
2139
+ organization_id: organizationId,
2140
+ project_id: command.projectId,
2141
+ source: {
2142
+ provider: "managed",
2143
+ source_connection_id: managed.source_connection_id,
2144
+ source_generation: managed.source_generation,
2145
+ namespace: managed.namespace,
2146
+ repository_name: managed.repository_name,
2147
+ branch: "main",
2148
+ last_confirmed_commit_sha: confirmed,
2149
+ },
2150
+ });
2151
+ }
2152
+ return success(name, {
2153
+ status: command.wait ? "completed" : "accepted",
2154
+ operation,
2155
+ ...(receipt === undefined ? {} : { receipt }),
2156
+ ...(snapshot === undefined ? {} : { snapshot }),
2157
+ next_action:
2158
+ "Read the resulting commit_sha, then plan and deploy that exact version. Publication is a separate action.",
2159
+ });
2160
+ }
2161
+
1869
2162
  private async configureAutoDeploy(
1870
2163
  command: Extract<ProductCliCommand, { kind: "source-auto-deploy-set" }>,
1871
2164
  signal: AbortSignal,
@@ -2512,6 +2805,17 @@ const classifyFailure = (
2512
2805
  signal: AbortSignal,
2513
2806
  actingAs?: Readonly<Record<string, unknown>>,
2514
2807
  ): CliResult => {
2808
+ if (error instanceof LocalSourcePublicationError)
2809
+ return failure(
2810
+ command,
2811
+ 2,
2812
+ error.code,
2813
+ error.code === "source_publication_pending",
2814
+ error.message,
2815
+ error.operation,
2816
+ );
2817
+ if (error instanceof SourceWorkspaceError)
2818
+ return failure(command, 2, error.code, error.code === "source_snapshot_changed", error.message);
2515
2819
  if (signal.aborted || isCancellation(error)) {
2516
2820
  return failure(
2517
2821
  command,
@@ -2743,6 +3047,12 @@ const failure = (
2743
3047
  };
2744
3048
 
2745
3049
  const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
3050
+ if (command.kind === "source-inspect") return "source inspect";
3051
+ if (command.kind === "source-publish") return "source publish";
3052
+ if (command.kind === "source-complete") return "source publish complete";
3053
+ if (command.kind === "source-initialize") return "source initialize";
3054
+ if (command.kind === "source-restore") return "source restore";
3055
+ if (command.kind === "source-read") return `source ${command.action}`;
2746
3056
  if (command.kind === "export-create") return "export create";
2747
3057
  if (command.kind === "export-get") return "export get";
2748
3058
  if (command.kind === "token-create") return "token create";