@azx-pbc/helix-cli 0.4.0 → 0.6.0

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 (3) hide show
  1. package/README.md +87 -12
  2. package/dist/helix.js +918 -182
  3. package/package.json +3 -3
package/dist/helix.js CHANGED
@@ -13,6 +13,11 @@ var options = {
13
13
  description: { type: "string" },
14
14
  visibility: { type: "string" },
15
15
  path: { type: "string" },
16
+ repo: { type: "string" },
17
+ branch: { type: "string" },
18
+ reusable: { type: "boolean" },
19
+ force: { type: "boolean" },
20
+ json: { type: "boolean" },
16
21
  help: { type: "boolean" }
17
22
  };
18
23
  function parseCliArgs(argv) {
@@ -21,7 +26,7 @@ function parseCliArgs(argv) {
21
26
  }
22
27
 
23
28
  // src/client.ts
24
- import { z as z27 } from "zod";
29
+ import { z as z29 } from "zod";
25
30
 
26
31
  // ../shared/src/visibility.ts
27
32
  import { z } from "zod";
@@ -176,6 +181,16 @@ var DeployReportSchema = z4.object({
176
181
  /** Candidate roots the planner considered. */
177
182
  candidates: z4.array(z4.string().max(1024)).max(20).default([])
178
183
  });
184
+ var VersionSourceSchema = z4.object({
185
+ provider: z4.literal("github"),
186
+ repoFullName: z4.string(),
187
+ repositoryId: z4.string(),
188
+ ref: z4.string(),
189
+ commitSha: z4.string(),
190
+ workflowRef: z4.string(),
191
+ runId: z4.string(),
192
+ runAttempt: z4.int().positive()
193
+ });
179
194
  var VersionSchema = z4.object({
180
195
  id: z4.uuid(),
181
196
  appId: z4.uuid(),
@@ -186,7 +201,9 @@ var VersionSchema = z4.object({
186
201
  status: VersionStatusSchema,
187
202
  createdAt: z4.iso.datetime(),
188
203
  /** Client-asserted salvage provenance (ADR-0038); absent on CLI uploads. */
189
- deployReport: DeployReportSchema.optional()
204
+ deployReport: DeployReportSchema.optional(),
205
+ /** Verified CI provenance (ADR-0051); absent on non-CI uploads. */
206
+ source: VersionSourceSchema.optional()
190
207
  });
191
208
 
192
209
  // ../shared/src/manifest.ts
@@ -217,7 +234,7 @@ var HEADER_VALUE_SAFE = /^[\t\x20-\x7e]*$/;
217
234
  var HEADER_VALUE_WIRE = /^[\t\x20-\x7e\x80-\xff]*$/;
218
235
  var headerName = (from) => {
219
236
  const base = z6.string().min(1);
220
- return (from === "request" ? base.max(64).regex(HEADER_TOKEN, "must be an RFC 7230 token") : base).transform((n) => n.trim().toLowerCase()).refine((n) => !FORBIDDEN_HEADER_NAMES.has(n), "reserved header name").refine((n) => !n.startsWith("x-helix-"), "reserved header prefix");
237
+ return (from === "request" ? base.max(64).regex(HEADER_TOKEN, "must be an RFC 7230 token") : base).transform((n2) => n2.trim().toLowerCase()).refine((n2) => !FORBIDDEN_HEADER_NAMES.has(n2), "reserved header name").refine((n2) => !n2.startsWith("x-helix-"), "reserved header prefix");
221
238
  };
222
239
  var headerTemplate = (from) => from === "request" ? z6.string().max(512).regex(HEADER_VALUE_SAFE) : z6.string().regex(HEADER_VALUE_WIRE);
223
240
  var injectionRecipe = (from) => z6.discriminatedUnion("kind", [
@@ -1256,7 +1273,29 @@ var DeploymentConfigResponseSchema = z13.object({
1256
1273
  * chasing a rejection it can't see the cause of.
1257
1274
  */
1258
1275
  deployMaxFileMb: z13.number().positive().optional(),
1259
- deployMaxBundleMb: z13.number().positive().optional()
1276
+ deployMaxBundleMb: z13.number().positive().optional(),
1277
+ /**
1278
+ * Repo-backed deploys (ADR-0051). `audience` is the exact `aud` the CI route
1279
+ * accepts, so `helix deploy` in GitHub Actions requests its token for this
1280
+ * value rather than for whatever URL it was pointed at. `serverUrl` is the
1281
+ * GitHub web origin for commit and run links. Optional only to tolerate a
1282
+ * portal that predates the feature; when `enabled` is false the CI route and
1283
+ * the binding routes answer 404 and clients hide the surface.
1284
+ */
1285
+ repoDeploy: z13.object({
1286
+ enabled: z13.boolean(),
1287
+ audience: z13.url(),
1288
+ serverUrl: z13.url()
1289
+ }).optional(),
1290
+ /**
1291
+ * The CLI compatibility floor. A CLI older than `minVersion` warns and tells
1292
+ * the user to run `@latest`; it does not refuse to run. Absent on a portal
1293
+ * that predates the handshake, so a CLI treats absence as "no floor stated",
1294
+ * not as an error. `minVersion` is a plain string rather than a validated
1295
+ * semver: the CLI parses this whole response, so a stricter schema would turn
1296
+ * an unexpected format into a failed command instead of a skipped check.
1297
+ */
1298
+ cli: z13.object({ minVersion: z13.string() }).optional()
1260
1299
  });
1261
1300
 
1262
1301
  // ../shared/src/catalogue.ts
@@ -1718,7 +1757,12 @@ var GatewayCallSchema = z18.object({
1718
1757
  method: z18.string().nullable(),
1719
1758
  /** LLM stop reason; null for non-LLM calls. */
1720
1759
  stopReason: z18.string().nullable(),
1721
- /** Short upstream error string; null on success. */
1760
+ /**
1761
+ * Short upstream error string; null on success. Admin-only in practice — it
1762
+ * can quote request content and, on an auth failure, the key, so it serves
1763
+ * the admin audit log's audience while the app-scoped feed
1764
+ * (`/api/v1/apps/:slug/audit`) nulls it for non-admin callers.
1765
+ */
1722
1766
  errorDetail: z18.string().nullable(),
1723
1767
  outcome: GatewayOutcomeSchema,
1724
1768
  createdAt: z18.iso.datetime()
@@ -1763,11 +1807,59 @@ var PlatformUsageSchema = z18.object({
1763
1807
  )
1764
1808
  });
1765
1809
 
1766
- // ../shared/src/data.ts
1810
+ // ../shared/src/visitors.ts
1767
1811
  import { z as z19 } from "zod";
1768
- var CollectionItemSchema = z19.object({
1769
- id: z19.uuid(),
1770
- collection: z19.string(),
1812
+ var VISITOR_RANGES = ["7d", "30d", "90d"];
1813
+ var VisitorRangeSchema = z19.enum(VISITOR_RANGES);
1814
+ var count = z19.int().nonnegative();
1815
+ var VisitorTotalsSchema = z19.object({
1816
+ visits: count,
1817
+ uniqueVisitors: count
1818
+ });
1819
+ var GeoStatusSchema = z19.object({
1820
+ available: z19.boolean(),
1821
+ reason: z19.string().nullable(),
1822
+ attribution: z19.object({ text: z19.string(), url: z19.url() }).nullable()
1823
+ });
1824
+ var VisitorLocationSchema = z19.object({
1825
+ /** ISO 3166-1 alpha-2. */
1826
+ country: z19.string(),
1827
+ countryName: z19.string().nullable(),
1828
+ /** First-level subdivision (state, province); null when the database has none. */
1829
+ region: z19.string().nullable(),
1830
+ visitors: count,
1831
+ visits: count
1832
+ });
1833
+ var VisitorSeriesPointSchema = z19.object({
1834
+ /** Bucket-start ISO timestamp (one calendar day). */
1835
+ bucket: z19.iso.datetime(),
1836
+ visits: count,
1837
+ /** Distinct visitors that day — a visitor active on two days counts on each. */
1838
+ uniqueVisitors: count
1839
+ });
1840
+ var VisitorSummarySchema = z19.object({
1841
+ appId: z19.uuid(),
1842
+ range: VisitorRangeSchema,
1843
+ /** The selected window. */
1844
+ current: VisitorTotalsSchema,
1845
+ /** The window of the same length immediately before it, for the deltas. */
1846
+ prior: VisitorTotalsSchema,
1847
+ /** Dense, zero-filled daily buckets, oldest first. */
1848
+ series: z19.array(VisitorSeriesPointSchema),
1849
+ /** The top {@link VISITOR_LOCATION_LIMIT} locations by visitors. */
1850
+ locations: z19.array(VisitorLocationSchema),
1851
+ /** Every resolved location past the top ones. */
1852
+ otherLocations: VisitorTotalsSchema,
1853
+ /** Private, proxied or unknown networks, and every row when geo is unavailable. */
1854
+ unresolved: VisitorTotalsSchema,
1855
+ geo: GeoStatusSchema
1856
+ });
1857
+
1858
+ // ../shared/src/data.ts
1859
+ import { z as z20 } from "zod";
1860
+ var CollectionItemSchema = z20.object({
1861
+ id: z20.uuid(),
1862
+ collection: z20.string(),
1771
1863
  /**
1772
1864
  * Which tier collected the row (dev-mode §5). The runtime roles are RLS-pinned
1773
1865
  * to one tier each, but the portal reads across both, so the discriminator has
@@ -1776,141 +1868,141 @@ var CollectionItemSchema = z19.object({
1776
1868
  */
1777
1869
  env: EnvSchema,
1778
1870
  /** The submitting user, if authenticated; null for anonymous/public visitors. */
1779
- userOid: z19.string().nullable(),
1871
+ userOid: z20.string().nullable(),
1780
1872
  /**
1781
1873
  * The submitter's captured claims — display half, rendered but never compared
1782
1874
  * (see `GatewayCallSchema.userName`). Null whenever `userOid` is, and for
1783
1875
  * password/dev-token principals.
1784
1876
  */
1785
- userName: z19.string().nullable(),
1786
- userEmail: z19.string().nullable(),
1877
+ userName: z20.string().nullable(),
1878
+ userEmail: z20.string().nullable(),
1787
1879
  /** The submitter's kind; null whenever `userOid` is, and on pre-column rows. */
1788
1880
  userKind: PrincipalKindSchema.nullable(),
1789
- item: z19.unknown(),
1881
+ item: z20.unknown(),
1790
1882
  /** Hashed IP / truncated UA for abuse triage; null if not captured. */
1791
- meta: z19.unknown().nullable(),
1792
- createdAt: z19.iso.datetime()
1883
+ meta: z20.unknown().nullable(),
1884
+ createdAt: z20.iso.datetime()
1793
1885
  });
1794
- var CollectionItemsPageSchema = z19.object({
1795
- rows: z19.array(CollectionItemSchema),
1886
+ var CollectionItemsPageSchema = z20.object({
1887
+ rows: z20.array(CollectionItemSchema),
1796
1888
  /** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
1797
- nextBefore: z19.iso.datetime().optional()
1889
+ nextBefore: z20.iso.datetime().optional()
1798
1890
  });
1799
- var CollectionSummarySchema = z19.object({
1800
- name: z19.string(),
1891
+ var CollectionSummarySchema = z20.object({
1892
+ name: z20.string(),
1801
1893
  env: EnvSchema,
1802
- count: z19.int().nonnegative(),
1894
+ count: z20.int().nonnegative(),
1803
1895
  /** Newest row's timestamp; null only if the group is somehow empty. */
1804
- lastAt: z19.iso.datetime().nullable()
1896
+ lastAt: z20.iso.datetime().nullable()
1805
1897
  });
1806
1898
 
1807
1899
  // ../shared/src/collectionTable.ts
1808
1900
  var BOM = String.fromCharCode(65279);
1809
1901
 
1810
1902
  // ../shared/src/connections.ts
1811
- import { z as z20 } from "zod";
1903
+ import { z as z21 } from "zod";
1812
1904
  var CONNECTION_STATUSES = ["live", "reconnect-needed", "invalidated"];
1813
- var ConnectionStatusSchema = z20.enum(CONNECTION_STATUSES);
1814
- var ConnectionMaterialSchema = z20.strictObject({
1815
- access: z20.string().min(1),
1816
- refresh: z20.string().min(1)
1905
+ var ConnectionStatusSchema = z21.enum(CONNECTION_STATUSES);
1906
+ var ConnectionMaterialSchema = z21.strictObject({
1907
+ access: z21.string().min(1),
1908
+ refresh: z21.string().min(1)
1817
1909
  });
1818
1910
  var MAX_GRANTED_SCOPES = 64;
1819
- var GrantedScopesSchema = z20.array(ScopeTokenSchema).max(MAX_GRANTED_SCOPES);
1820
- var UserConnectionSchema = z20.strictObject({
1821
- id: z20.uuid(),
1822
- userOid: z20.string().min(1),
1823
- providerId: z20.uuid(),
1824
- providerRevision: z20.int().positive(),
1911
+ var GrantedScopesSchema = z21.array(ScopeTokenSchema).max(MAX_GRANTED_SCOPES);
1912
+ var UserConnectionSchema = z21.strictObject({
1913
+ id: z21.uuid(),
1914
+ userOid: z21.string().min(1),
1915
+ providerId: z21.uuid(),
1916
+ providerRevision: z21.int().positive(),
1825
1917
  env: EnvSchema,
1826
1918
  status: ConnectionStatusSchema,
1827
- material: z20.string().min(1),
1919
+ material: z21.string().min(1),
1828
1920
  grantedScopes: GrantedScopesSchema,
1829
- grantedAt: z20.iso.datetime(),
1830
- expiresAt: z20.iso.datetime(),
1831
- renewBeforeNext: z20.boolean(),
1832
- pendingRetire: z20.string().min(1).nullable(),
1833
- lastRenewedAt: z20.iso.datetime().nullable(),
1834
- createdAt: z20.iso.datetime(),
1835
- updatedAt: z20.iso.datetime()
1836
- });
1837
- var MyConnectionSchema = z20.strictObject({
1838
- id: z20.uuid(),
1921
+ grantedAt: z21.iso.datetime(),
1922
+ expiresAt: z21.iso.datetime(),
1923
+ renewBeforeNext: z21.boolean(),
1924
+ pendingRetire: z21.string().min(1).nullable(),
1925
+ lastRenewedAt: z21.iso.datetime().nullable(),
1926
+ createdAt: z21.iso.datetime(),
1927
+ updatedAt: z21.iso.datetime()
1928
+ });
1929
+ var MyConnectionSchema = z21.strictObject({
1930
+ id: z21.uuid(),
1839
1931
  providerRef: ProviderRefSchema,
1840
- providerDisplayName: z20.string().min(1).max(200),
1932
+ providerDisplayName: z21.string().min(1).max(200),
1841
1933
  env: EnvSchema,
1842
1934
  status: ConnectionStatusSchema.exclude(["invalidated"]),
1843
1935
  grantedScopes: GrantedScopesSchema,
1844
- grantedAt: z20.iso.datetime(),
1845
- sharedApps: z20.array(BoundAppSchema)
1936
+ grantedAt: z21.iso.datetime(),
1937
+ sharedApps: z21.array(BoundAppSchema)
1846
1938
  });
1847
- var MyConnectionsResponseSchema = z20.strictObject({
1848
- connections: z20.array(MyConnectionSchema)
1939
+ var MyConnectionsResponseSchema = z21.strictObject({
1940
+ connections: z21.array(MyConnectionSchema)
1849
1941
  });
1850
- var DisconnectResponseSchema = z20.strictObject({
1851
- outcome: z20.enum(["disconnected", "already_removed"])
1942
+ var DisconnectResponseSchema = z21.strictObject({
1943
+ outcome: z21.enum(["disconnected", "already_removed"])
1852
1944
  });
1853
1945
 
1854
1946
  // ../shared/src/consent.ts
1855
- import { z as z21 } from "zod";
1856
- var ConsentStateSchema = z21.string().min(43).max(128);
1857
- var ConsentNonceSchema = z21.string().min(16).max(128);
1858
- var ConsentAttemptTagSchema = z21.string().min(1).max(128).regex(/^[A-Za-z0-9._~-]+$/, "must be URL-safe punctuation, letters, or digits");
1859
- var ConsentIdentitySchema = z21.discriminatedUnion("kind", [
1860
- z21.strictObject({ kind: z21.literal("user"), userOid: z21.string().min(1).max(128) }),
1861
- z21.strictObject({
1862
- kind: z21.literal("dev"),
1863
- developerOid: z21.string().min(1).max(128),
1947
+ import { z as z22 } from "zod";
1948
+ var ConsentStateSchema = z22.string().min(43).max(128);
1949
+ var ConsentNonceSchema = z22.string().min(16).max(128);
1950
+ var ConsentAttemptTagSchema = z22.string().min(1).max(128).regex(/^[A-Za-z0-9._~-]+$/, "must be URL-safe punctuation, letters, or digits");
1951
+ var ConsentIdentitySchema = z22.discriminatedUnion("kind", [
1952
+ z22.strictObject({ kind: z22.literal("user"), userOid: z22.string().min(1).max(128) }),
1953
+ z22.strictObject({
1954
+ kind: z22.literal("dev"),
1955
+ developerOid: z22.string().min(1).max(128),
1864
1956
  nonce: ConsentNonceSchema
1865
1957
  })
1866
1958
  ]);
1867
1959
  var HttpUrlNoUserinfoSchema = (() => {
1868
1960
  const userinfo = /^https?:\/\/[/\\]*[^/?#\\]*@/i;
1869
- return z21.url({ protocol: /^https?$/ }).refine(
1961
+ return z22.url({ protocol: /^https?$/ }).refine(
1870
1962
  (value) => !userinfo.test(value.replace(/[\t\n\r]/g, "")),
1871
1963
  "URL must not carry userinfo"
1872
1964
  );
1873
1965
  })();
1874
- var ConsultRequestSchema = z21.strictObject({
1966
+ var ConsultRequestSchema = z22.strictObject({
1875
1967
  identity: ConsentIdentitySchema,
1876
- appSlug: z21.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
1968
+ appSlug: z22.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
1877
1969
  providerRef: ProviderRefSchema,
1878
1970
  openerOrigin: HttpUrlNoUserinfoSchema.transform((value) => new URL(value).origin),
1879
1971
  callbackUrl: HttpUrlNoUserinfoSchema
1880
1972
  });
1881
- var ConsultResponseSchema = z21.discriminatedUnion("outcome", [
1882
- z21.strictObject({ outcome: z21.literal("already_connected") }),
1883
- z21.strictObject({ outcome: z21.literal("not_available") }),
1884
- z21.strictObject({ outcome: z21.literal("started"), authorizeUrl: z21.url() })
1973
+ var ConsultResponseSchema = z22.discriminatedUnion("outcome", [
1974
+ z22.strictObject({ outcome: z22.literal("already_connected") }),
1975
+ z22.strictObject({ outcome: z22.literal("not_available") }),
1976
+ z22.strictObject({ outcome: z22.literal("started"), authorizeUrl: z22.url() })
1885
1977
  ]);
1886
- var CancelRequestSchema = z21.strictObject({
1978
+ var CancelRequestSchema = z22.strictObject({
1887
1979
  identity: ConsentIdentitySchema,
1888
1980
  state: ConsentStateSchema
1889
1981
  });
1890
1982
  var CONSENT_CANCEL_OUTCOMES = ["cancelled", "not_cancellable"];
1891
- var CancelResponseSchema = z21.strictObject({
1892
- outcome: z21.enum(CONSENT_CANCEL_OUTCOMES)
1983
+ var CancelResponseSchema = z22.strictObject({
1984
+ outcome: z22.enum(CONSENT_CANCEL_OUTCOMES)
1893
1985
  });
1894
- var ConnectionStatusIdentitySchema = z21.discriminatedUnion("kind", [
1895
- z21.strictObject({ kind: z21.literal("user"), userOid: z21.string().min(1).max(128) }),
1896
- z21.strictObject({ kind: z21.literal("dev"), developerOid: z21.string().min(1).max(128) })
1986
+ var ConnectionStatusIdentitySchema = z22.discriminatedUnion("kind", [
1987
+ z22.strictObject({ kind: z22.literal("user"), userOid: z22.string().min(1).max(128) }),
1988
+ z22.strictObject({ kind: z22.literal("dev"), developerOid: z22.string().min(1).max(128) })
1897
1989
  ]);
1898
- var ConnectionStatusRequestSchema = z21.strictObject({
1990
+ var ConnectionStatusRequestSchema = z22.strictObject({
1899
1991
  identity: ConnectionStatusIdentitySchema,
1900
- appSlug: z21.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
1992
+ appSlug: z22.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
1901
1993
  providerRef: ProviderRefSchema
1902
1994
  });
1903
- var ConnectionStatusResponseSchema = z21.strictObject({
1904
- status: z21.enum(["connected", "not_connected", "not_available"])
1995
+ var ConnectionStatusResponseSchema = z22.strictObject({
1996
+ status: z22.enum(["connected", "not_connected", "not_available"])
1905
1997
  });
1906
- var AttemptCancelRequestSchema = z21.strictObject({
1998
+ var AttemptCancelRequestSchema = z22.strictObject({
1907
1999
  provider: ProviderRefSchema,
1908
2000
  attempt: ConsentAttemptTagSchema
1909
2001
  });
1910
- var DevConsentStartResponseSchema = z21.discriminatedUnion("outcome", [
1911
- z21.strictObject({ outcome: z21.literal("started"), popupUrl: z21.url() }),
1912
- z21.strictObject({ outcome: z21.literal("already_connected") }),
1913
- z21.strictObject({ outcome: z21.literal("not_available") })
2002
+ var DevConsentStartResponseSchema = z22.discriminatedUnion("outcome", [
2003
+ z22.strictObject({ outcome: z22.literal("started"), popupUrl: z22.url() }),
2004
+ z22.strictObject({ outcome: z22.literal("already_connected") }),
2005
+ z22.strictObject({ outcome: z22.literal("not_available") })
1914
2006
  ]);
1915
2007
  var HELIX_CONNECT_MESSAGE_SOURCE = "helix-connect";
1916
2008
  var CONNECT_MESSAGE_VERSION = 1;
@@ -1931,32 +2023,32 @@ var CONSENT_MESSAGE_REASONS = [
1931
2023
  "provider_incompatible",
1932
2024
  "service_unavailable"
1933
2025
  ];
1934
- var ConnectOutcomeMessageSchema = z21.strictObject({
1935
- source: z21.literal(HELIX_CONNECT_MESSAGE_SOURCE),
1936
- version: z21.literal(CONNECT_MESSAGE_VERSION),
2026
+ var ConnectOutcomeMessageSchema = z22.strictObject({
2027
+ source: z22.literal(HELIX_CONNECT_MESSAGE_SOURCE),
2028
+ version: z22.literal(CONNECT_MESSAGE_VERSION),
1937
2029
  attempt: ConsentAttemptTagSchema.optional(),
1938
2030
  provider: ProviderRefSchema,
1939
- outcome: z21.enum(CONSENT_MESSAGE_OUTCOMES),
1940
- reason: z21.enum(CONSENT_MESSAGE_REASONS).nullable()
2031
+ outcome: z22.enum(CONSENT_MESSAGE_OUTCOMES),
2032
+ reason: z22.enum(CONSENT_MESSAGE_REASONS).nullable()
1941
2033
  }).refine((message) => message.outcome === "error" || message.reason === null, {
1942
2034
  error: "reason is only carried with the error outcome"
1943
2035
  });
1944
- var ConnectResultSchema = z21.object({
1945
- outcome: z21.enum(CONSENT_MESSAGE_OUTCOMES),
1946
- provider: z21.string(),
2036
+ var ConnectResultSchema = z22.object({
2037
+ outcome: z22.enum(CONSENT_MESSAGE_OUTCOMES),
2038
+ provider: z22.string(),
1947
2039
  attempt: ConsentAttemptTagSchema.optional(),
1948
- reason: z21.enum(CONSENT_MESSAGE_REASONS).optional()
2040
+ reason: z22.enum(CONSENT_MESSAGE_REASONS).optional()
1949
2041
  }).refine((result) => result.outcome === "error" || result.reason === void 0, {
1950
2042
  error: "reason is only carried with the error outcome"
1951
2043
  });
1952
2044
 
1953
2045
  // ../shared/src/exchange.ts
1954
- import { z as z22 } from "zod";
1955
- var ExchangeCodeSchema = z22.string().min(1).max(1024);
1956
- var CodeVerifierSchema = z22.string().min(43).max(128).regex(/^[A-Za-z0-9\-._~]+$/, "must be an RFC 7636 code_verifier (43-128 unreserved characters)");
1957
- var ExchangeRequestSchema = z22.strictObject({
1958
- providerId: z22.uuid(),
1959
- providerRevision: z22.int().positive(),
2046
+ import { z as z23 } from "zod";
2047
+ var ExchangeCodeSchema = z23.string().min(1).max(1024);
2048
+ var CodeVerifierSchema = z23.string().min(43).max(128).regex(/^[A-Za-z0-9\-._~]+$/, "must be an RFC 7636 code_verifier (43-128 unreserved characters)");
2049
+ var ExchangeRequestSchema = z23.strictObject({
2050
+ providerId: z23.uuid(),
2051
+ providerRevision: z23.int().positive(),
1960
2052
  env: EnvSchema,
1961
2053
  code: ExchangeCodeSchema,
1962
2054
  codeVerifier: CodeVerifierSchema,
@@ -1967,24 +2059,24 @@ var EXCHANGE_REJECTION_REASONS = [
1967
2059
  "missing_refresh_token",
1968
2060
  "missing_permissions"
1969
2061
  ];
1970
- var ExchangeRejectionReasonSchema = z22.enum(EXCHANGE_REJECTION_REASONS);
2062
+ var ExchangeRejectionReasonSchema = z23.enum(EXCHANGE_REJECTION_REASONS);
1971
2063
  var MAX_EXCHANGED_SCOPES = 64;
1972
- var ExchangedScopesSchema = z22.array(ScopeTokenSchema).max(MAX_EXCHANGED_SCOPES);
1973
- var ExchangeResponseSchema = z22.discriminatedUnion("outcome", [
1974
- z22.strictObject({
1975
- outcome: z22.literal("exchanged"),
1976
- access: z22.string().min(1),
1977
- refresh: z22.string().min(1),
2064
+ var ExchangedScopesSchema = z23.array(ScopeTokenSchema).max(MAX_EXCHANGED_SCOPES);
2065
+ var ExchangeResponseSchema = z23.discriminatedUnion("outcome", [
2066
+ z23.strictObject({
2067
+ outcome: z23.literal("exchanged"),
2068
+ access: z23.string().min(1),
2069
+ refresh: z23.string().min(1),
1978
2070
  grantedScopes: ExchangedScopesSchema,
1979
- accessExpiresAt: z22.iso.datetime()
2071
+ accessExpiresAt: z23.iso.datetime()
1980
2072
  }),
1981
- z22.strictObject({ outcome: z22.literal("rejected"), reason: ExchangeRejectionReasonSchema }),
1982
- z22.strictObject({ outcome: z22.literal("provider_unavailable") }),
1983
- z22.strictObject({ outcome: z22.literal("exchange_failed") })
2073
+ z23.strictObject({ outcome: z23.literal("rejected"), reason: ExchangeRejectionReasonSchema }),
2074
+ z23.strictObject({ outcome: z23.literal("provider_unavailable") }),
2075
+ z23.strictObject({ outcome: z23.literal("exchange_failed") })
1984
2076
  ]);
1985
2077
 
1986
2078
  // ../shared/src/devTokens.ts
1987
- import { z as z23 } from "zod";
2079
+ import { z as z24 } from "zod";
1988
2080
  function isValidDevOrigin(input) {
1989
2081
  if (input.includes("*")) return false;
1990
2082
  let url;
@@ -1998,40 +2090,83 @@ function isValidDevOrigin(input) {
1998
2090
  if (url.username !== "" || url.password !== "") return false;
1999
2091
  return input === url.origin || input === `${url.origin}/`;
2000
2092
  }
2001
- var DevOriginSchema = z23.string().refine(
2093
+ var DevOriginSchema = z24.string().refine(
2002
2094
  isValidDevOrigin,
2003
2095
  "must be an exact origin (scheme://host[:port]) \u2014 no path, query, or wildcard"
2004
2096
  ).transform((o) => new URL(o).origin);
2005
- var TtlDaysSchema = z23.number().int().min(1).max(365).optional();
2006
- var DevTokenMintRequestSchema = z23.object({
2007
- origins: z23.array(DevOriginSchema).min(1).max(20),
2097
+ var TtlDaysSchema = z24.number().int().min(1).max(365).optional();
2098
+ var DevTokenMintRequestSchema = z24.object({
2099
+ origins: z24.array(DevOriginSchema).min(1).max(20),
2008
2100
  ttlDays: TtlDaysSchema
2009
2101
  });
2010
- var DevTokenRotateRequestSchema = z23.object({
2102
+ var DevTokenRotateRequestSchema = z24.object({
2011
2103
  ttlDays: TtlDaysSchema
2012
2104
  });
2013
- var DevTokenMetadataSchema = z23.object({
2014
- id: z23.string(),
2015
- developerOid: z23.string(),
2016
- origins: z23.array(z23.string()),
2017
- expiresAt: z23.string(),
2018
- revokedAt: z23.string().nullable().optional(),
2019
- createdAt: z23.string()
2105
+ var DevTokenMetadataSchema = z24.object({
2106
+ id: z24.string(),
2107
+ developerOid: z24.string(),
2108
+ origins: z24.array(z24.string()),
2109
+ expiresAt: z24.string(),
2110
+ revokedAt: z24.string().nullable().optional(),
2111
+ createdAt: z24.string()
2020
2112
  });
2021
- var DevTokenMintResponseSchema = z23.object({
2022
- token: z23.string(),
2113
+ var DevTokenMintResponseSchema = z24.object({
2114
+ token: z24.string(),
2023
2115
  metadata: DevTokenMetadataSchema
2024
2116
  });
2025
2117
 
2118
+ // ../shared/src/repoBinding.ts
2119
+ import { z as z25 } from "zod";
2120
+ function hasControlChar(v) {
2121
+ for (let i = 0; i < v.length; i++) {
2122
+ const c = v.charCodeAt(i);
2123
+ if (c < 32 || c === 127) return true;
2124
+ }
2125
+ return false;
2126
+ }
2127
+ var RepoFullNameSchema = z25.string().max(140).regex(
2128
+ /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/,
2129
+ "must be owner/name, as in the repository's GitHub URL"
2130
+ ).refine((v) => !/\/\.{1,2}$/.test(v), "must be owner/name, as in the repository's GitHub URL");
2131
+ var BranchNameSchema = z25.string().min(1).max(255).refine(
2132
+ (v) => !v.startsWith("refs/") && !hasControlChar(v) && !/[\s~^:?*[\\]/.test(v) && !v.includes("..") && !v.includes("@{") && !v.includes("//") && !/^[-/]/.test(v) && !/(\/|\.lock|\.)$/.test(v),
2133
+ "must be a branch name such as main, without refs/heads/"
2134
+ );
2135
+ var WorkflowRefSchema = z25.string().max(512).regex(
2136
+ /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}\/\.github\/workflows\/[^@\s]+\.ya?ml@\S+$/,
2137
+ "must look like owner/repo/.github/workflows/deploy.yml@refs/heads/main"
2138
+ );
2139
+ var EnvironmentNameSchema = z25.string().min(1).max(255).refine((v) => !hasControlChar(v) && v.trim() === v, "must be an environment name");
2140
+ var SetRepoBindingRequestSchema = z25.object({
2141
+ repoFullName: RepoFullNameSchema,
2142
+ branch: BranchNameSchema,
2143
+ workflowRef: WorkflowRefSchema.optional(),
2144
+ environment: EnvironmentNameSchema.optional()
2145
+ });
2146
+ var REPO_PROVIDERS = ["github"];
2147
+ var RepoBindingSchema = z25.object({
2148
+ provider: z25.enum(REPO_PROVIDERS),
2149
+ repoFullName: z25.string(),
2150
+ branch: z25.string(),
2151
+ workflowRef: z25.string().optional(),
2152
+ environment: z25.string().optional(),
2153
+ /** Pinned from the first accepted token; absent until then. */
2154
+ repositoryId: z25.string().optional(),
2155
+ repositoryOwnerId: z25.string().optional(),
2156
+ pinnedAt: z25.iso.datetime().optional(),
2157
+ createdAt: z25.iso.datetime(),
2158
+ updatedAt: z25.iso.datetime()
2159
+ });
2160
+
2026
2161
  // ../shared/src/instruction.ts
2027
- import { z as z24 } from "zod";
2162
+ import { z as z26 } from "zod";
2028
2163
  var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
2029
- var InstructionCapabilitySchema = z24.enum(INSTRUCTION_CAPABILITIES);
2030
- var AttestedInstructionSchema = z24.strictObject({
2164
+ var InstructionCapabilitySchema = z26.enum(INSTRUCTION_CAPABILITIES);
2165
+ var AttestedInstructionSchema = z26.strictObject({
2031
2166
  /** App the call is attributed to (registry app id). */
2032
- appId: z24.string().min(1),
2167
+ appId: z26.string().min(1),
2033
2168
  /** Authenticated user, or the anonymous sentinel on `public` apps. */
2034
- userOid: z24.string().min(1),
2169
+ userOid: z26.string().min(1),
2035
2170
  /**
2036
2171
  * Which kind of principal `userOid` is (the `PrincipalKind` vocabulary the
2037
2172
  * edge records at capture time — recorded, never inferred). The delegated
@@ -2048,9 +2183,9 @@ var AttestedInstructionSchema = z24.strictObject({
2048
2183
  userKind: PrincipalKindSchema.optional(),
2049
2184
  capability: InstructionCapabilitySchema,
2050
2185
  /** The allowlisted origin the edge authorized (scheme + host + port). */
2051
- origin: z24.url(),
2186
+ origin: z26.url(),
2052
2187
  /** Connection (secret) name to inject, if this is a secret-backed call. */
2053
- connection: z24.string().min(1).optional(),
2188
+ connection: z26.string().min(1).optional(),
2054
2189
  /**
2055
2190
  * Provider reference for a delegated call — the user's connection to this
2056
2191
  * provider is what egress injects. Exactly
@@ -2060,7 +2195,7 @@ var AttestedInstructionSchema = z24.strictObject({
2060
2195
  */
2061
2196
  provider: ProviderRefSchema.optional(),
2062
2197
  /** Correlates the edge audit row with the egress call. */
2063
- requestId: z24.string().min(1),
2198
+ requestId: z26.string().min(1),
2064
2199
  /**
2065
2200
  * The HTTP method + URL pathname the edge authorized (ADR-0013 step 2, issue #6).
2066
2201
  * Egress refuses a mismatched verb/resource, so a captured instruction can't be
@@ -2075,8 +2210,8 @@ var AttestedInstructionSchema = z24.strictObject({
2075
2210
  * means "old edge", not tampering — egress asserts ONLY when the claim is
2076
2211
  * present. Make required once a fleet is reliably past deploy.
2077
2212
  */
2078
- method: z24.string().min(1).optional(),
2079
- path: z24.string().optional(),
2213
+ method: z26.string().min(1).optional(),
2214
+ path: z26.string().optional(),
2080
2215
  /**
2081
2216
  * Environment tier this call is scoped to (dev-mode design §6). Egress resolves
2082
2217
  * the connection secret within this tier — a `dev` instruction can never reach a
@@ -2105,7 +2240,7 @@ var INSTRUCTION_TTL_SECONDS = 30;
2105
2240
  var INSTRUCTION_BURN_RETENTION_SECONDS = INSTRUCTION_TTL_SECONDS + 15;
2106
2241
 
2107
2242
  // ../shared/src/fetch.ts
2108
- import { z as z25 } from "zod";
2243
+ import { z as z27 } from "zod";
2109
2244
  var FETCH_ERROR_CODES = [
2110
2245
  "forbidden",
2111
2246
  "rate_limited",
@@ -2118,26 +2253,26 @@ var FETCH_ERROR_CODES = [
2118
2253
  "provider_unavailable",
2119
2254
  "provider_misconfigured"
2120
2255
  ];
2121
- var FetchErrorCodeSchema = z25.enum(FETCH_ERROR_CODES);
2122
- var FetchErrorProviderSchema = z25.strictObject({
2256
+ var FetchErrorCodeSchema = z27.enum(FETCH_ERROR_CODES);
2257
+ var FetchErrorProviderSchema = z27.strictObject({
2123
2258
  ref: ProviderRefSchema,
2124
- displayName: z25.string().min(1).max(200).optional()
2259
+ displayName: z27.string().min(1).max(200).optional()
2125
2260
  });
2126
- var FetchProxyErrorSchema = z25.object({
2261
+ var FetchProxyErrorSchema = z27.object({
2127
2262
  code: FetchErrorCodeSchema,
2128
- message: z25.string(),
2263
+ message: z27.string(),
2129
2264
  /** Present only on the delegated-call codes that carry it (`connection_required`, `provider_unavailable`). */
2130
2265
  provider: FetchErrorProviderSchema.optional()
2131
2266
  });
2132
2267
 
2133
2268
  // ../shared/src/sessions.ts
2134
- import { z as z26 } from "zod";
2135
- var SessionPrincipalKindSchema = z26.enum(["user", "password"]);
2136
- var SessionSummarySchema = z26.object({
2137
- id: z26.uuid(),
2138
- appId: z26.uuid(),
2269
+ import { z as z28 } from "zod";
2270
+ var SessionPrincipalKindSchema = z28.enum(["user", "password"]);
2271
+ var SessionSummarySchema = z28.object({
2272
+ id: z28.uuid(),
2273
+ appId: z28.uuid(),
2139
2274
  /** App slug at read time; the FK guarantees the app row exists. */
2140
- slug: z26.string(),
2275
+ slug: z28.string(),
2141
2276
  /**
2142
2277
  * The identity half — Entra's `oid` claim since ADR-0048 (a pairwise `sub`
2143
2278
  * on rows that predate the re-base; a `pw_*` pseudonym for shared-password
@@ -2145,58 +2280,70 @@ var SessionSummarySchema = z26.object({
2145
2280
  * resort: the platform deliberately holds no Graph `/users` grant, so no
2146
2281
  * principal id it stores resolves to a name.
2147
2282
  */
2148
- userOid: z26.string(),
2283
+ userOid: z28.string(),
2149
2284
  /**
2150
2285
  * The display half, captured at login. Render, never compare. Null for
2151
2286
  * shared-password visitors (each login is a fresh pseudonym) and for rows
2152
2287
  * predating the columns.
2153
2288
  */
2154
- userName: z26.string().nullable(),
2155
- userEmail: z26.string().nullable(),
2289
+ userName: z28.string().nullable(),
2290
+ userEmail: z28.string().nullable(),
2156
2291
  userKind: SessionPrincipalKindSchema.nullable(),
2157
2292
  /** Group-id snapshot as of this session's login/refresh. */
2158
- groups: z26.array(z26.string()),
2159
- createdAt: z26.iso.datetime(),
2293
+ groups: z28.array(z28.string()),
2294
+ createdAt: z28.iso.datetime(),
2160
2295
  /** Null while the handoff is un-redeemed — impossible here (live rows only). */
2161
- activatedAt: z26.iso.datetime().nullable(),
2296
+ activatedAt: z28.iso.datetime().nullable(),
2162
2297
  /** When silent re-auth (`prompt=none`) becomes due; past = overdue. */
2163
- refreshDueAt: z26.iso.datetime(),
2298
+ refreshDueAt: z28.iso.datetime(),
2164
2299
  /** Hard cap — the session is dead at this instant regardless of activity. */
2165
- expiresAt: z26.iso.datetime()
2300
+ expiresAt: z28.iso.datetime()
2166
2301
  });
2167
- var SessionListResponseSchema = z26.object({
2168
- rows: z26.array(SessionSummarySchema),
2169
- groupNames: z26.record(z26.string(), z26.string().nullable()),
2170
- groupsResolved: z26.boolean()
2302
+ var SessionListResponseSchema = z28.object({
2303
+ rows: z28.array(SessionSummarySchema),
2304
+ groupNames: z28.record(z28.string(), z28.string().nullable()),
2305
+ groupsResolved: z28.boolean()
2171
2306
  });
2172
- var SessionRevokeRequestSchema = z26.object({
2307
+ var SessionRevokeRequestSchema = z28.object({
2173
2308
  /**
2174
2309
  * A session's principal id as seen in the list — the `oid` claim, a `pw_*`
2175
2310
  * pseudonym, or a pre-re-base pairwise `sub`. Opaque to the admin; copied
2176
2311
  * from the row they clicked, so its shape is whatever the session rows
2177
2312
  * hold — bounded only against abuse, not parsed.
2178
2313
  */
2179
- userOid: z26.string().min(1).max(200)
2314
+ userOid: z28.string().min(1).max(200)
2180
2315
  });
2181
- var SessionRevokeResultSchema = z26.object({
2182
- removed: z26.int().nonnegative(),
2316
+ var SessionRevokeResultSchema = z28.object({
2317
+ removed: z28.int().nonnegative(),
2183
2318
  /** Slugs of the apps the removed sessions lived on; empty when none. */
2184
- apps: z26.array(z26.string())
2319
+ apps: z28.array(z28.string())
2185
2320
  });
2186
2321
 
2322
+ // src/version.ts
2323
+ var CLI_VERSION = true ? "0.6.0" : "latest";
2324
+
2187
2325
  // src/client.ts
2188
2326
  var CliError = class extends Error {
2189
2327
  code;
2190
- constructor(message, code) {
2328
+ /** The portal envelope's `details`, when it sent one. */
2329
+ details;
2330
+ constructor(message, code, details) {
2191
2331
  super(message);
2192
2332
  this.name = "CliError";
2193
2333
  this.code = code;
2334
+ this.details = details;
2194
2335
  }
2195
2336
  };
2196
- var VersionListSchema = z27.array(VersionSchema);
2337
+ function errorReason(err) {
2338
+ if (!(err instanceof CliError)) return void 0;
2339
+ const reason = err.details?.reason;
2340
+ return typeof reason === "string" ? reason : void 0;
2341
+ }
2342
+ var VersionListSchema = z29.array(VersionSchema);
2197
2343
  var PortalClient = class {
2198
2344
  #baseUrl;
2199
2345
  #tokenProvider;
2346
+ #deploymentConfig;
2200
2347
  /** `token` may be a static string (dev token) or an async provider (OIDC). */
2201
2348
  constructor(baseUrl, token) {
2202
2349
  this.#baseUrl = baseUrl.replace(/\/+$/, "");
@@ -2219,19 +2366,67 @@ var PortalClient = class {
2219
2366
  getAuthConfig() {
2220
2367
  return this.#json(AuthConfigResponseSchema, "GET", "/api/v1/auth/config", {});
2221
2368
  }
2369
+ /**
2370
+ * Public deployment topology (`GET /api/v1/config`), incl. the CI token
2371
+ * audience and the CLI version floor. Fetched once per client: the
2372
+ * compatibility check and the command that follows it share one request.
2373
+ */
2374
+ getDeploymentConfig() {
2375
+ this.#deploymentConfig ??= this.#json(
2376
+ DeploymentConfigResponseSchema,
2377
+ "GET",
2378
+ "/api/v1/config",
2379
+ {}
2380
+ ).catch((err) => {
2381
+ this.#deploymentConfig = void 0;
2382
+ throw err;
2383
+ });
2384
+ return this.#deploymentConfig;
2385
+ }
2386
+ /** The app's GitHub binding (ADR-0051), or null when it has none. */
2387
+ async getRepoBinding(slug) {
2388
+ try {
2389
+ return await this.#json(RepoBindingSchema, "GET", `/api/v1/apps/${enc(slug)}/repo-binding`, {
2390
+ auth: true
2391
+ });
2392
+ } catch (err) {
2393
+ if (errorReason(err) === "no_binding") return null;
2394
+ throw err;
2395
+ }
2396
+ }
2397
+ putRepoBinding(slug, body) {
2398
+ return this.#json(RepoBindingSchema, "PUT", `/api/v1/apps/${enc(slug)}/repo-binding`, {
2399
+ auth: true,
2400
+ body
2401
+ });
2402
+ }
2403
+ async deleteRepoBinding(slug) {
2404
+ const res = await fetch(this.#url(`/api/v1/apps/${enc(slug)}/repo-binding`), {
2405
+ method: "DELETE",
2406
+ headers: await this.#authHeaders(true)
2407
+ });
2408
+ if (!res.ok) await this.#parse(z29.unknown(), res);
2409
+ }
2222
2410
  /** The authenticated actor, per the portal — powers `helix whoami`. */
2223
2411
  me() {
2224
2412
  return this.#json(PortalMeResponseSchema, "GET", "/api/v1/me", { auth: true });
2225
2413
  }
2226
2414
  /**
2227
2415
  * The deployment-rendered agent skill (`GET /api/v1/skill`, ADR-0036) —
2228
- * `text/markdown`, already substituted with this deployment's hosts, servable
2229
- * models, and approval baselines. Authed like every other `/api/v1` read.
2230
- * Powers `helix skill`.
2416
+ * `text/markdown`, with this deployment's hosts filled in. Authed like every
2417
+ * other `/api/v1` read. Powers `helix skill`.
2231
2418
  */
2232
2419
  getSkill() {
2233
2420
  return this.#text("/api/v1/skill", { auth: true });
2234
2421
  }
2422
+ /**
2423
+ * The deployment capability catalogue (`GET /api/v1/capabilities`), unparsed.
2424
+ * `helix capabilities` validates it when formatting, and falls back to the raw
2425
+ * JSON when a portal of a different version sends a shape this CLI doesn't know.
2426
+ */
2427
+ getCapabilities() {
2428
+ return this.#json(z29.unknown(), "GET", "/api/v1/capabilities", { auth: true });
2429
+ }
2235
2430
  listVersions(slug) {
2236
2431
  return this.#json(VersionListSchema, "GET", `/api/v1/apps/${enc(slug)}/versions`, {
2237
2432
  auth: true
@@ -2258,8 +2453,22 @@ var PortalClient = class {
2258
2453
  });
2259
2454
  return this.#parse(UploadVersionResponseSchema, res);
2260
2455
  }
2261
- #url(path3) {
2262
- return `${this.#baseUrl}${path3}`;
2456
+ /**
2457
+ * Upload from GitHub Actions to the CI route (ADR-0051), authenticated by the
2458
+ * job's GitHub OIDC token rather than a Helix sign-in.
2459
+ */
2460
+ async uploadCiVersion(slug, zip, githubToken) {
2461
+ const form = new FormData();
2462
+ form.append("bundle", new Blob([new Uint8Array(zip)]), "bundle.zip");
2463
+ const res = await fetch(this.#url(`/api/v1/apps/${enc(slug)}/ci/versions`), {
2464
+ method: "POST",
2465
+ headers: { authorization: `Bearer ${githubToken}` },
2466
+ body: form
2467
+ });
2468
+ return this.#parse(UploadVersionResponseSchema, res);
2469
+ }
2470
+ #url(path5) {
2471
+ return `${this.#baseUrl}${path5}`;
2263
2472
  }
2264
2473
  async #authHeaders(auth) {
2265
2474
  if (!auth) return {};
@@ -2269,10 +2478,10 @@ var PortalClient = class {
2269
2478
  }
2270
2479
  return { authorization: `Bearer ${token}` };
2271
2480
  }
2272
- async #json(schema, method, path3, opts) {
2481
+ async #json(schema, method, path5, opts) {
2273
2482
  const headers = await this.#authHeaders(opts.auth ?? false);
2274
2483
  if (opts.body !== void 0) headers["content-type"] = "application/json";
2275
- const res = await fetch(this.#url(path3), {
2484
+ const res = await fetch(this.#url(path5), {
2276
2485
  method,
2277
2486
  headers,
2278
2487
  body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
@@ -2283,26 +2492,35 @@ var PortalClient = class {
2283
2492
  const text = await res.text();
2284
2493
  const data = text ? JSON.parse(text) : void 0;
2285
2494
  if (!res.ok) {
2286
- const parsed = ApiErrorSchema.safeParse(data);
2287
- if (parsed.success) throw new CliError(parsed.data.error.message, parsed.data.error.code);
2288
- throw new CliError(`request failed (HTTP ${res.status})`);
2495
+ throw errorFromResponse(res.status, data);
2289
2496
  }
2290
2497
  return schema.parse(data);
2291
2498
  }
2292
2499
  /** A non-JSON GET (the markdown skill) — errors still typed via the envelope. */
2293
- async #text(path3, opts) {
2500
+ async #text(path5, opts) {
2294
2501
  const headers = await this.#authHeaders(opts.auth ?? false);
2295
- const res = await fetch(this.#url(path3), { method: "GET", headers });
2502
+ const res = await fetch(this.#url(path5), { method: "GET", headers });
2296
2503
  const body = await res.text();
2297
2504
  if (!res.ok) {
2298
2505
  const data = body ? safeJsonParse(body) : void 0;
2299
- const parsed = ApiErrorSchema.safeParse(data);
2300
- if (parsed.success) throw new CliError(parsed.data.error.message, parsed.data.error.code);
2301
- throw new CliError(`request failed (HTTP ${res.status})`);
2506
+ throw errorFromResponse(res.status, data);
2302
2507
  }
2303
2508
  return body;
2304
2509
  }
2305
2510
  };
2511
+ function errorFromResponse(status, data) {
2512
+ const parsed = ApiErrorSchema.safeParse(data);
2513
+ if (!parsed.success) return new CliError(`request failed (HTTP ${status})`);
2514
+ const { message, code, details } = parsed.data.error;
2515
+ if (status === 404 && code === "not_found" && message.startsWith("route ")) {
2516
+ return new CliError(
2517
+ `${message}. This portal is probably older than this CLI (${CLI_VERSION}); ask your operator to update Helix, or run an older @azx-pbc/helix-cli`,
2518
+ code,
2519
+ details
2520
+ );
2521
+ }
2522
+ return new CliError(message, code, details);
2523
+ }
2306
2524
  function enc(segment) {
2307
2525
  return encodeURIComponent(segment);
2308
2526
  }
@@ -2335,6 +2553,60 @@ function zipDirectory(dir) {
2335
2553
  });
2336
2554
  }
2337
2555
 
2556
+ // src/capabilities.ts
2557
+ var TRIGGER_LABELS = {
2558
+ externalOrigins: "any externalOrigins entry",
2559
+ fetchOrigins: "any fetch.origins entry",
2560
+ mcp: "any mcp server",
2561
+ publicVisibility: "public visibility",
2562
+ budgetAboveBaseline: "any budget above its baseline",
2563
+ uncuratedLlmModel: "an LLM model not in the list above"
2564
+ };
2565
+ var n = (value) => value.toLocaleString("en-US");
2566
+ function formatCapabilities(c) {
2567
+ const b = c.approval.baselines;
2568
+ const lines = [
2569
+ "Helix capabilities on this deployment",
2570
+ "",
2571
+ `Visibility modes: ${c.visibility.modes.join(", ")}`,
2572
+ "",
2573
+ "LLM (capabilities.llm)",
2574
+ ` models: ${c.llm.models.length ? c.llm.models.join(", ") : "(none servable)"}`,
2575
+ ` baseline: $${n(b.dollarsPerDay)}/day`,
2576
+ "",
2577
+ "App data (capabilities.data)",
2578
+ ` available: ${c.data.provisioned ? "yes" : "no"}`,
2579
+ ` baselines: ${n(b.writesPerDay)} writes/day, ${n(b.bytesPerDay)} bytes/day`,
2580
+ "",
2581
+ "Outbound HTTP (capabilities.fetch, externalOrigins)",
2582
+ ` connections: ${c.fetch.connections.length ? c.fetch.connections.map((x) => x.name).join(", ") : "(none)"}`,
2583
+ ...c.fetch.providers.length ? [
2584
+ " providers:",
2585
+ ...c.fetch.providers.map(
2586
+ (p) => ` ${p.ref} (${p.displayName}, ${p.env}) \u2192 ${p.apiOrigins.join(", ")}`
2587
+ )
2588
+ ] : [" providers: (none)"],
2589
+ ` externalOrigins: ${c.fetch.externalOriginsPermitted ? "may be requested" : "not permitted"}`,
2590
+ ` baseline: ${n(b.fetchRequestsPerDay)} proxied requests/day`,
2591
+ "",
2592
+ `MCP: ${c.mcp.enforced ? "enforced" : "carried in the manifest, not enforced"}`,
2593
+ `Offline: ${c.offline.available ? `available \u2014 ${c.offline.scopeRule}` : "not available"}`,
2594
+ `Deploy limits: ${n(c.deploy.maxFileMb)} MB per file, ${n(c.deploy.maxBundleMb)} MB per bundle`,
2595
+ `Dev gateway: ${c.devApiBase ? `${c.devApiBase}/<slug>` : "not deployed"}`,
2596
+ "",
2597
+ "Needs admin approval:",
2598
+ ...c.approval.elevationTriggers.map((t) => ` - ${TRIGGER_LABELS[t]}`),
2599
+ "Everything else applies immediately."
2600
+ ];
2601
+ return lines.join("\n");
2602
+ }
2603
+ function renderCapabilities(raw, opts) {
2604
+ if (opts.json) return JSON.stringify(raw, null, 2);
2605
+ const parsed = CapabilityCatalogueSchema.safeParse(raw);
2606
+ if (parsed.success) return formatCapabilities(parsed.data);
2607
+ return "(this CLI could not format the portal's response; showing it as JSON)\n" + JSON.stringify(raw, null, 2);
2608
+ }
2609
+
2338
2610
  // src/auth/deviceFlow.ts
2339
2611
  import * as oidc from "openid-client";
2340
2612
  var DEVICE_SCOPES = "openid profile email offline_access";
@@ -2444,6 +2716,28 @@ async function deleteTokens(portalUrl, file = defaultTokenPath()) {
2444
2716
  return true;
2445
2717
  }
2446
2718
 
2719
+ // src/auth/githubActions.ts
2720
+ import { z as z30 } from "zod";
2721
+ function githubTokenSource(env) {
2722
+ const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;
2723
+ const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
2724
+ return requestUrl && requestToken ? { requestUrl, requestToken } : null;
2725
+ }
2726
+ var IdTokenResponseSchema = z30.object({ value: z30.string().min(1) });
2727
+ async function requestGithubIdToken(source, audience, fetchImpl = fetch) {
2728
+ const url = new URL(source.requestUrl);
2729
+ url.searchParams.set("audience", audience);
2730
+ const res = await fetchImpl(url, {
2731
+ headers: { authorization: `Bearer ${source.requestToken}`, accept: "application/json" }
2732
+ });
2733
+ if (!res.ok) {
2734
+ throw new CliError(`GitHub refused the ID token request (HTTP ${res.status})`);
2735
+ }
2736
+ const parsed = IdTokenResponseSchema.safeParse(await res.json());
2737
+ if (!parsed.success) throw new CliError("GitHub's ID token response had no token");
2738
+ return parsed.data.value;
2739
+ }
2740
+
2447
2741
  // src/commands.ts
2448
2742
  function requireSlug(config) {
2449
2743
  if (!config.slug) {
@@ -2498,13 +2792,53 @@ async function createCommand(client, config, opts) {
2498
2792
  console.log(`Created app "${app.slug}".`);
2499
2793
  printApp(app);
2500
2794
  }
2501
- async function deployCommand(client, config, opts) {
2795
+ async function deployCommand(client, config, opts, env = process.env) {
2502
2796
  const slug = requireSlug(config);
2797
+ const github = config.token ? null : githubTokenSource(env);
2798
+ if (!config.token && !github && env.GITHUB_ACTIONS === "true") {
2799
+ throw new CliError(
2800
+ "running in GitHub Actions without an ID token; add `permissions: id-token: write` to the workflow (see `helix ci init`)"
2801
+ );
2802
+ }
2803
+ if (github && !isSafePortalUrl(config.portalUrl)) {
2804
+ throw new CliError(
2805
+ `won't send a GitHub token over plain http (${config.portalUrl}); use the portal's https URL`
2806
+ );
2807
+ }
2808
+ if (github && opts.promote) {
2809
+ throw new CliError(
2810
+ "CI deploys land as a preview; promote it in the portal or with `helix promote <n>`"
2811
+ );
2812
+ }
2503
2813
  const zip = config.bundle ? await readFile2(config.bundle) : await zipDirectory(config.dir);
2504
2814
  console.log(`Uploading bundle to "${slug}"\u2026`);
2505
- const { version, warnings } = await client.uploadVersion(slug, zip);
2815
+ let result;
2816
+ if (github) {
2817
+ const deployment = await client.getDeploymentConfig();
2818
+ if (!deployment.repoDeploy?.enabled) {
2819
+ throw new CliError("this portal does not accept repo-backed deploys (ask its operator)");
2820
+ }
2821
+ const idToken = await requestGithubIdToken(github, deployment.repoDeploy.audience);
2822
+ try {
2823
+ result = await client.uploadCiVersion(slug, zip, idToken);
2824
+ } catch (err) {
2825
+ if (err instanceof CliError && err.code === "forbidden" && !errorReason(err)) {
2826
+ throw new CliError(
2827
+ `${err.message}; check that "${slug}" is connected to this repo (helix ci show)`,
2828
+ err.code
2829
+ );
2830
+ }
2831
+ throw err;
2832
+ }
2833
+ } else {
2834
+ result = await client.uploadVersion(slug, zip);
2835
+ }
2836
+ const { version, warnings } = result;
2506
2837
  console.log(`Uploaded as preview:`);
2507
2838
  printVersion(version);
2839
+ if (version.source) {
2840
+ console.log(` from: ${version.source.repoFullName}@${version.source.commitSha.slice(0, 7)}`);
2841
+ }
2508
2842
  if (warnings.length > 0) {
2509
2843
  console.log(`
2510
2844
  ${warnings.length} CSP warning(s):`);
@@ -2520,6 +2854,10 @@ Promoted version ${version.number} to live.`);
2520
2854
  Not live yet \u2014 promote with: helix promote ${version.number}`);
2521
2855
  }
2522
2856
  }
2857
+ function isSafePortalUrl(url) {
2858
+ const { protocol, hostname } = new URL(url);
2859
+ return protocol === "https:" || protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(hostname);
2860
+ }
2523
2861
  async function versionsCommand(client, config) {
2524
2862
  const slug = requireSlug(config);
2525
2863
  const versions = await client.listVersions(slug);
@@ -2573,6 +2911,9 @@ async function skillCommand(client, _config, opts) {
2573
2911
  await writeFile2(outPath, skill, "utf8");
2574
2912
  console.log(`Wrote skill to ${outPath}`);
2575
2913
  }
2914
+ async function capabilitiesCommand(client, opts) {
2915
+ console.log(renderCapabilities(await client.getCapabilities(), opts));
2916
+ }
2576
2917
 
2577
2918
  // src/auth/session.ts
2578
2919
  var REFRESH_MARGIN_MS = 6e4;
@@ -2615,6 +2956,12 @@ function makeTokenProvider(opts, deps = defaultDeps) {
2615
2956
  };
2616
2957
  }
2617
2958
 
2959
+ // src/ci/commands.ts
2960
+ import { execFile } from "node:child_process";
2961
+ import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
2962
+ import path4 from "node:path";
2963
+ import { promisify } from "node:util";
2964
+
2618
2965
  // src/config.ts
2619
2966
  import { readFile as readFile3 } from "node:fs/promises";
2620
2967
  import path2 from "node:path";
@@ -2644,9 +2991,371 @@ async function resolveConfig(flags, env = process.env, cwd = process.cwd()) {
2644
2991
  };
2645
2992
  }
2646
2993
 
2994
+ // src/ci/remote.ts
2995
+ function parseGithubRemote(remote, githubHost = "github.com") {
2996
+ const url = remote.trim();
2997
+ const scp = /^[\w.-]+@([^:/]+):(.+)$/.exec(url);
2998
+ let host;
2999
+ let path5;
3000
+ if (scp && !url.includes("://")) {
3001
+ host = scp[1];
3002
+ path5 = scp[2];
3003
+ } else {
3004
+ let parsed;
3005
+ try {
3006
+ parsed = new URL(url);
3007
+ } catch {
3008
+ return null;
3009
+ }
3010
+ if (!["https:", "http:", "ssh:", "git:"].includes(parsed.protocol)) return null;
3011
+ host = parsed.hostname;
3012
+ path5 = parsed.pathname.replace(/^\/+/, "");
3013
+ }
3014
+ if (host.toLowerCase() !== githubHost.toLowerCase()) return null;
3015
+ const match = /^([A-Za-z0-9][A-Za-z0-9-]{0,38})\/([A-Za-z0-9._-]+?)(\.git)?\/?$/.exec(path5);
3016
+ return match ? `${match[1]}/${match[2]}` : null;
3017
+ }
3018
+
3019
+ // src/ci/workflow.ts
3020
+ import { access, readFile as readFile4 } from "node:fs/promises";
3021
+ import path3 from "node:path";
3022
+ var REUSABLE_WORKFLOW_PATH = "AZX-PBC-OSS/helix-actions/.github/workflows/deploy.yml";
3023
+ var REUSABLE_WORKFLOW_TAG = "v1";
3024
+ var REUSABLE_WORKFLOW_REF = `${REUSABLE_WORKFLOW_PATH}@refs/tags/${REUSABLE_WORKFLOW_TAG}`;
3025
+ var WORKFLOW_FILE = ".github/workflows/helix-deploy.yml";
3026
+ var DEFAULT_PNPM_VERSION = "10";
3027
+ async function exists(file) {
3028
+ try {
3029
+ await access(file);
3030
+ return true;
3031
+ } catch {
3032
+ return false;
3033
+ }
3034
+ }
3035
+ var LOCKFILES = [
3036
+ ["pnpm-lock.yaml", "pnpm"],
3037
+ ["yarn.lock", "yarn"],
3038
+ ["package-lock.json", "npm"]
3039
+ ];
3040
+ var posix = (p) => p.split(path3.sep).join("/");
3041
+ async function readPackageJson(file) {
3042
+ try {
3043
+ return JSON.parse(await readFile4(file, "utf8"));
3044
+ } catch {
3045
+ return null;
3046
+ }
3047
+ }
3048
+ async function detectBuildPlan(appDir, repoRoot) {
3049
+ let packageManager = "npm";
3050
+ let lockfilePath = null;
3051
+ search: for (const dir of [appDir, repoRoot]) {
3052
+ for (const [name, pm] of LOCKFILES) {
3053
+ if (await exists(path3.join(dir, name))) {
3054
+ packageManager = pm;
3055
+ lockfilePath = posix(path3.relative(repoRoot, path3.join(dir, name)));
3056
+ break search;
3057
+ }
3058
+ }
3059
+ }
3060
+ const appPkg = await readPackageJson(path3.join(appDir, "package.json"));
3061
+ let pnpmPackageJson = null;
3062
+ if (packageManager === "pnpm") {
3063
+ for (const dir of [appDir, repoRoot]) {
3064
+ const pkg = dir === appDir ? appPkg : await readPackageJson(path3.join(dir, "package.json"));
3065
+ if (typeof pkg?.packageManager === "string" && pkg.packageManager.startsWith("pnpm@")) {
3066
+ pnpmPackageJson = posix(path3.relative(repoRoot, path3.join(dir, "package.json")));
3067
+ break;
3068
+ }
3069
+ }
3070
+ }
3071
+ const buildScript = typeof appPkg?.scripts?.build === "string" ? "build" : null;
3072
+ return {
3073
+ packageManager,
3074
+ hasLockfile: lockfilePath !== null,
3075
+ lockfilePath,
3076
+ pnpmPackageJson,
3077
+ buildScript
3078
+ };
3079
+ }
3080
+ var INSTALL = {
3081
+ npm: "npm ci",
3082
+ pnpm: "pnpm install --frozen-lockfile",
3083
+ yarn: "yarn install --frozen-lockfile"
3084
+ };
3085
+ var HEADER = (branch) => `# Deploys this app to Helix as a preview on every push to ${branch}.
3086
+ # Written by \`helix ci init\`. The repo needs no secret: the deploy job
3087
+ # authenticates with its own GitHub OIDC token. The build job runs the repo's
3088
+ # code without that permission. A preview goes live only when someone promotes
3089
+ # it, in the portal or with \`helix promote <n>\`.
3090
+ name: Deploy to Helix
3091
+ on:
3092
+ push:
3093
+ branches: [${JSON.stringify(branch)}]
3094
+ workflow_dispatch:
3095
+ # Each job asks for only what it needs.
3096
+ permissions: {}
3097
+ `;
3098
+ var inDir = (workingDirectory, rel) => workingDirectory === "." ? rel : `${workingDirectory}/${rel}`;
3099
+ var FIND_OUTPUT = `if [ -f helix.json ]; then cfg=helix.json; elif [ -f azx.json ]; then cfg=azx.json; else echo "::error::no helix.json in this directory"; exit 1; fi
3100
+ echo "config=$cfg" >> "$GITHUB_OUTPUT"
3101
+ echo "dir=$(node -p "require('./' + process.argv[1]).dir || 'dist'" "$cfg")" >> "$GITHUB_OUTPUT"`;
3102
+ function renderStarterWorkflow(opts) {
3103
+ const { plan, workingDirectory: wd } = opts;
3104
+ const sub = wd !== ".";
3105
+ const install = plan.hasLockfile ? INSTALL[plan.packageManager] : "npm install";
3106
+ const pnpmSetup = plan.packageManager !== "pnpm" ? [] : [
3107
+ " - uses: pnpm/action-setup@v4",
3108
+ " with:",
3109
+ plan.pnpmPackageJson ? ` package_json_file: ${plan.pnpmPackageJson}` : ` version: ${DEFAULT_PNPM_VERSION}`
3110
+ ];
3111
+ const cache = plan.lockfilePath ? [
3112
+ ` cache: ${plan.packageManager}`,
3113
+ ` cache-dependency-path: ${plan.lockfilePath}`
3114
+ ] : [];
3115
+ const lines = [
3116
+ HEADER(opts.branch),
3117
+ "jobs:",
3118
+ " build:",
3119
+ " runs-on: ubuntu-latest",
3120
+ " permissions:",
3121
+ " contents: read",
3122
+ ...sub ? [" defaults:", " run:", ` working-directory: ${JSON.stringify(wd)}`] : [],
3123
+ " steps:",
3124
+ " - uses: actions/checkout@v4",
3125
+ ...pnpmSetup,
3126
+ " - uses: actions/setup-node@v4",
3127
+ " with:",
3128
+ " node-version: 24",
3129
+ ...cache,
3130
+ ` - run: ${install}`,
3131
+ ...plan.buildScript ? [` - run: ${plan.packageManager} run ${plan.buildScript}`] : [],
3132
+ " - id: output",
3133
+ " run: |",
3134
+ ` ${FIND_OUTPUT}`,
3135
+ " - uses: actions/upload-artifact@v4",
3136
+ " with:",
3137
+ " name: helix-bundle",
3138
+ " path: |",
3139
+ ` ${inDir(wd, "${{ steps.output.outputs.config }}")}`,
3140
+ ` ${inDir(wd, "${{ steps.output.outputs.dir }}")}`,
3141
+ " include-hidden-files: true",
3142
+ " if-no-files-found: error",
3143
+ " deploy:",
3144
+ " needs: build",
3145
+ " runs-on: ubuntu-latest",
3146
+ " permissions:",
3147
+ " id-token: write",
3148
+ " steps:",
3149
+ " - uses: actions/download-artifact@v4",
3150
+ " with:",
3151
+ " name: helix-bundle",
3152
+ ` path: ${JSON.stringify(wd)}`,
3153
+ " - uses: actions/setup-node@v4",
3154
+ " with:",
3155
+ " node-version: 24",
3156
+ ` - run: npx --yes @azx-pbc/helix-cli@${opts.cliVersion} deploy`,
3157
+ ...sub ? [` working-directory: ${JSON.stringify(wd)}`] : [],
3158
+ ""
3159
+ ];
3160
+ return lines.join("\n");
3161
+ }
3162
+ function renderReusableWorkflow(opts) {
3163
+ const { plan } = opts;
3164
+ const lines = [
3165
+ HEADER(opts.branch),
3166
+ "jobs:",
3167
+ " deploy:",
3168
+ ` uses: ${REUSABLE_WORKFLOW_PATH}@${REUSABLE_WORKFLOW_TAG}`,
3169
+ // A caller grants the reusable workflow at most these; its own jobs narrow
3170
+ // them per job.
3171
+ " permissions:",
3172
+ " contents: read",
3173
+ " id-token: write",
3174
+ " with:",
3175
+ ` package-manager: ${plan.packageManager}`,
3176
+ ` working-directory: ${JSON.stringify(opts.workingDirectory)}`,
3177
+ ` build-script: ${JSON.stringify(plan.buildScript ?? "")}`,
3178
+ ...plan.packageManager === "pnpm" && !plan.pnpmPackageJson ? [` pnpm-version: ${JSON.stringify(DEFAULT_PNPM_VERSION)}`] : [],
3179
+ ` cli-version: ${JSON.stringify(opts.cliVersion)}`,
3180
+ ""
3181
+ ];
3182
+ return lines.join("\n");
3183
+ }
3184
+
3185
+ // src/ci/commands.ts
3186
+ var run = promisify(execFile);
3187
+ var defaultGit = async (args, cwd) => (await run("git", args, { cwd })).stdout.trim();
3188
+ function requireSlug2(config) {
3189
+ if (!config.slug) throw new CliError("no app slug; set it in helix.json or pass --slug");
3190
+ return config.slug;
3191
+ }
3192
+ async function ciInitCommand(client, config, opts, deps = {}) {
3193
+ const cwd = deps.cwd ?? process.cwd();
3194
+ const git = deps.git ?? defaultGit;
3195
+ const log = deps.log ?? console.log;
3196
+ const slug = requireSlug2(config);
3197
+ const deployment = await client.getDeploymentConfig();
3198
+ if (!deployment.repoDeploy?.enabled) {
3199
+ throw new CliError("this portal does not accept repo-backed deploys (ask its operator)");
3200
+ }
3201
+ const githubHost = new URL(deployment.repoDeploy.serverUrl).hostname;
3202
+ let repoRoot;
3203
+ try {
3204
+ repoRoot = await git(["rev-parse", "--show-toplevel"], cwd);
3205
+ } catch {
3206
+ throw new CliError("not inside a git checkout; run this from the app's directory in its repo");
3207
+ }
3208
+ const repo = opts.repo ?? parseGithubRemote(await originUrl(git, cwd), githubHost);
3209
+ if (!repo) {
3210
+ throw new CliError(
3211
+ `couldn't read a ${githubHost} repo from the origin remote; pass --repo owner/name`
3212
+ );
3213
+ }
3214
+ const branch = opts.branch ?? await defaultBranch(git, cwd);
3215
+ if (!branch || branch === "HEAD") {
3216
+ throw new CliError("couldn't tell which branch to deploy from; pass --branch <name>");
3217
+ }
3218
+ const workflowPath = path4.join(repoRoot, WORKFLOW_FILE);
3219
+ if (!opts.force && await fileExists(workflowPath)) {
3220
+ throw new CliError(`${WORKFLOW_FILE} already exists; pass --force to replace it`);
3221
+ }
3222
+ const workingDirectory = path4.relative(repoRoot, cwd).split(path4.sep).join("/") || ".";
3223
+ const plan = await detectBuildPlan(cwd, repoRoot);
3224
+ const render = opts.reusable ? renderReusableWorkflow : renderStarterWorkflow;
3225
+ const workflow = render({ branch, workingDirectory, plan, cliVersion: CLI_VERSION });
3226
+ const existing = await client.getRepoBinding(slug);
3227
+ const workflowRef = opts.reusable ? REUSABLE_WORKFLOW_REF : existing?.workflowRef;
3228
+ const environment = existing?.environment;
3229
+ if (existing?.workflowRef && workflowRef !== existing.workflowRef && !opts.force) {
3230
+ throw new CliError(
3231
+ `the binding pins deploys to workflow ${existing.workflowRef}; --reusable would replace that pin. Pass --force to replace it.`
3232
+ );
3233
+ }
3234
+ const binding = await client.putRepoBinding(slug, {
3235
+ repoFullName: repo,
3236
+ branch,
3237
+ ...workflowRef ? { workflowRef } : {},
3238
+ ...environment ? { environment } : {}
3239
+ });
3240
+ const configFile = await writeHelixJson(cwd, {
3241
+ slug,
3242
+ portalUrl: config.portalUrl,
3243
+ dir: config.dir
3244
+ });
3245
+ await mkdir2(path4.dirname(workflowPath), { recursive: true });
3246
+ await writeFile3(workflowPath, workflow, "utf8");
3247
+ log(`Connected "${slug}" to ${repo}.`);
3248
+ log(`Branch: ${branch}${opts.branch ? "" : " (use --branch to change)"}`);
3249
+ printBinding(binding, log);
3250
+ log(`Wrote ${path4.relative(cwd, workflowPath) || WORKFLOW_FILE} and ${configFile}.`);
3251
+ if (isLocalPortal(config.portalUrl)) {
3252
+ log(`warning: helix.json points CI at ${config.portalUrl}, which a GitHub runner can't reach.`);
3253
+ }
3254
+ log("Commit both files and push; each push lands a preview to promote.");
3255
+ }
3256
+ async function ciShowCommand(client, config, log = console.log) {
3257
+ const slug = requireSlug2(config);
3258
+ const binding = await client.getRepoBinding(slug);
3259
+ if (!binding) {
3260
+ log(`"${slug}" is not connected to a repo. Run: helix ci init`);
3261
+ return;
3262
+ }
3263
+ printBinding(binding, log);
3264
+ }
3265
+ async function ciRemoveCommand(client, config, log = console.log) {
3266
+ const slug = requireSlug2(config);
3267
+ await client.deleteRepoBinding(slug);
3268
+ log(`Disconnected "${slug}" from its repo. CI deploys to it are now refused.`);
3269
+ }
3270
+ function printBinding(b, log) {
3271
+ log(` repo: ${b.repoFullName}`);
3272
+ log(` branch: ${b.branch}`);
3273
+ if (b.workflowRef) log(` workflow: ${b.workflowRef}`);
3274
+ if (b.environment) log(` environment: ${b.environment}`);
3275
+ log(
3276
+ b.repositoryId ? ` pinned: repo id ${b.repositoryId}, owner id ${b.repositoryOwnerId ?? "?"}` : " pinned: not yet (the first deploy pins the repo's ids)"
3277
+ );
3278
+ }
3279
+ async function originUrl(git, cwd) {
3280
+ try {
3281
+ return await git(["remote", "get-url", "origin"], cwd);
3282
+ } catch {
3283
+ return "";
3284
+ }
3285
+ }
3286
+ async function defaultBranch(git, cwd) {
3287
+ try {
3288
+ const remoteHead = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
3289
+ if (remoteHead.startsWith("origin/")) return remoteHead.slice("origin/".length);
3290
+ } catch {
3291
+ }
3292
+ return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
3293
+ }
3294
+ async function fileExists(file) {
3295
+ try {
3296
+ await readFile5(file);
3297
+ return true;
3298
+ } catch {
3299
+ return false;
3300
+ }
3301
+ }
3302
+ async function writeHelixJson(cwd, values) {
3303
+ for (const name2 of CONFIG_FILENAMES) {
3304
+ const file = path4.join(cwd, name2);
3305
+ let existing;
3306
+ try {
3307
+ existing = JSON.parse(await readFile5(file, "utf8"));
3308
+ } catch (err) {
3309
+ if (err.code === "ENOENT") continue;
3310
+ throw err;
3311
+ }
3312
+ await writeFile3(file, `${JSON.stringify({ ...existing, ...values }, null, 2)}
3313
+ `, "utf8");
3314
+ return name2;
3315
+ }
3316
+ const name = CONFIG_FILENAMES[0];
3317
+ await writeFile3(path4.join(cwd, name), `${JSON.stringify(values, null, 2)}
3318
+ `, "utf8");
3319
+ return name;
3320
+ }
3321
+ function isLocalPortal(url) {
3322
+ const host = new URL(url).hostname;
3323
+ return host === "localhost" || host === "127.0.0.1" || host.endsWith(".local") || host === "::1";
3324
+ }
3325
+
3326
+ // src/compat.ts
3327
+ var NPX_LATEST = "npx -y @azx-pbc/helix-cli@latest";
3328
+ function parseVersion(v) {
3329
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
3330
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
3331
+ }
3332
+ function isOlderThan(version, floor) {
3333
+ const a = parseVersion(version);
3334
+ const b = parseVersion(floor);
3335
+ if (!a || !b) return false;
3336
+ for (let i = 0; i < 3; i++) {
3337
+ if (a[i] !== b[i]) return a[i] < b[i];
3338
+ }
3339
+ return false;
3340
+ }
3341
+ async function warnIfCliTooOld(client, cliVersion, warn = (msg) => console.error(msg)) {
3342
+ try {
3343
+ const floor = (await client.getDeploymentConfig()).cli?.minVersion;
3344
+ if (floor && isOlderThan(cliVersion, floor)) {
3345
+ warn(
3346
+ `warning: this portal needs helix-cli ${floor} or newer (you have ${cliVersion}). Run \`${NPX_LATEST} \u2026\`, or update your global install.`
3347
+ );
3348
+ }
3349
+ } catch {
3350
+ }
3351
+ }
3352
+
2647
3353
  // src/bin.ts
2648
3354
  var USAGE = `helix \u2014 Helix deploy CLI
2649
3355
 
3356
+ Run it with \`npx -y @azx-pbc/helix-cli@latest <command>\` to always use the newest
3357
+ version, or install it with \`npm i -g @azx-pbc/helix-cli\`.
3358
+
2650
3359
  Usage:
2651
3360
  helix login # sign in via the browser (OIDC device flow)
2652
3361
  helix logout
@@ -2656,10 +3365,15 @@ Usage:
2656
3365
  helix versions
2657
3366
  helix promote <number>
2658
3367
  helix rollback [number]
3368
+ helix capabilities [--json] # what this deployment offers: models, baselines, connections\u2026
2659
3369
  helix skill [--path <file>] # write this deployment's agent skill to disk
3370
+ helix ci init [--repo <owner/name>] [--branch <b>] [--reusable] [--force]
3371
+ # connect the app to its GitHub repo and write the workflow
3372
+ helix ci show | helix ci remove
2660
3373
 
2661
3374
  Common flags: --slug <slug> --portal-url <url> --token <token>
2662
- Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; CI/scripts).
3375
+ Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; local scripts).
3376
+ In GitHub Actions, deploy uses the job's OIDC token (permissions: id-token: write).
2663
3377
  Config file: helix.json { slug, portalUrl, dir, description }
2664
3378
  Visibility: internal | group:<id>[,<id>...] | password | public
2665
3379
  `;
@@ -2681,6 +3395,7 @@ async function main() {
2681
3395
  config.portalUrl,
2682
3396
  makeTokenProvider({ portalUrl: config.portalUrl, staticToken: config.token })
2683
3397
  );
3398
+ if (command !== "logout") await warnIfCliTooOld(client, CLI_VERSION);
2684
3399
  switch (command) {
2685
3400
  case "login":
2686
3401
  await loginCommand(client, config);
@@ -2691,6 +3406,9 @@ async function main() {
2691
3406
  case "whoami":
2692
3407
  await whoamiCommand(client);
2693
3408
  break;
3409
+ case "capabilities":
3410
+ await capabilitiesCommand(client, { json: values.json });
3411
+ break;
2694
3412
  case "skill":
2695
3413
  await skillCommand(client, config, { path: values.path });
2696
3414
  break;
@@ -2704,6 +3422,24 @@ async function main() {
2704
3422
  visibility: values.visibility
2705
3423
  });
2706
3424
  break;
3425
+ case "ci": {
3426
+ const sub = positionals[1];
3427
+ if (sub === "init") {
3428
+ await ciInitCommand(client, config, {
3429
+ repo: values.repo,
3430
+ branch: values.branch,
3431
+ reusable: values.reusable,
3432
+ force: values.force
3433
+ });
3434
+ } else if (sub === "show") {
3435
+ await ciShowCommand(client, config);
3436
+ } else if (sub === "remove") {
3437
+ await ciRemoveCommand(client, config);
3438
+ } else {
3439
+ throw new CliError("usage: helix ci init | show | remove");
3440
+ }
3441
+ break;
3442
+ }
2707
3443
  case "versions":
2708
3444
  await versionsCommand(client, config);
2709
3445
  break;