@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.
- package/README.md +87 -12
- package/dist/helix.js +918 -182
- 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
|
|
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((
|
|
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
|
-
/**
|
|
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/
|
|
1810
|
+
// ../shared/src/visitors.ts
|
|
1767
1811
|
import { z as z19 } from "zod";
|
|
1768
|
-
var
|
|
1769
|
-
|
|
1770
|
-
|
|
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:
|
|
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:
|
|
1786
|
-
userEmail:
|
|
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:
|
|
1881
|
+
item: z20.unknown(),
|
|
1790
1882
|
/** Hashed IP / truncated UA for abuse triage; null if not captured. */
|
|
1791
|
-
meta:
|
|
1792
|
-
createdAt:
|
|
1883
|
+
meta: z20.unknown().nullable(),
|
|
1884
|
+
createdAt: z20.iso.datetime()
|
|
1793
1885
|
});
|
|
1794
|
-
var CollectionItemsPageSchema =
|
|
1795
|
-
rows:
|
|
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:
|
|
1889
|
+
nextBefore: z20.iso.datetime().optional()
|
|
1798
1890
|
});
|
|
1799
|
-
var CollectionSummarySchema =
|
|
1800
|
-
name:
|
|
1891
|
+
var CollectionSummarySchema = z20.object({
|
|
1892
|
+
name: z20.string(),
|
|
1801
1893
|
env: EnvSchema,
|
|
1802
|
-
count:
|
|
1894
|
+
count: z20.int().nonnegative(),
|
|
1803
1895
|
/** Newest row's timestamp; null only if the group is somehow empty. */
|
|
1804
|
-
lastAt:
|
|
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
|
|
1903
|
+
import { z as z21 } from "zod";
|
|
1812
1904
|
var CONNECTION_STATUSES = ["live", "reconnect-needed", "invalidated"];
|
|
1813
|
-
var ConnectionStatusSchema =
|
|
1814
|
-
var ConnectionMaterialSchema =
|
|
1815
|
-
access:
|
|
1816
|
-
refresh:
|
|
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 =
|
|
1820
|
-
var UserConnectionSchema =
|
|
1821
|
-
id:
|
|
1822
|
-
userOid:
|
|
1823
|
-
providerId:
|
|
1824
|
-
providerRevision:
|
|
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:
|
|
1919
|
+
material: z21.string().min(1),
|
|
1828
1920
|
grantedScopes: GrantedScopesSchema,
|
|
1829
|
-
grantedAt:
|
|
1830
|
-
expiresAt:
|
|
1831
|
-
renewBeforeNext:
|
|
1832
|
-
pendingRetire:
|
|
1833
|
-
lastRenewedAt:
|
|
1834
|
-
createdAt:
|
|
1835
|
-
updatedAt:
|
|
1836
|
-
});
|
|
1837
|
-
var MyConnectionSchema =
|
|
1838
|
-
id:
|
|
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:
|
|
1932
|
+
providerDisplayName: z21.string().min(1).max(200),
|
|
1841
1933
|
env: EnvSchema,
|
|
1842
1934
|
status: ConnectionStatusSchema.exclude(["invalidated"]),
|
|
1843
1935
|
grantedScopes: GrantedScopesSchema,
|
|
1844
|
-
grantedAt:
|
|
1845
|
-
sharedApps:
|
|
1936
|
+
grantedAt: z21.iso.datetime(),
|
|
1937
|
+
sharedApps: z21.array(BoundAppSchema)
|
|
1846
1938
|
});
|
|
1847
|
-
var MyConnectionsResponseSchema =
|
|
1848
|
-
connections:
|
|
1939
|
+
var MyConnectionsResponseSchema = z21.strictObject({
|
|
1940
|
+
connections: z21.array(MyConnectionSchema)
|
|
1849
1941
|
});
|
|
1850
|
-
var DisconnectResponseSchema =
|
|
1851
|
-
outcome:
|
|
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
|
|
1856
|
-
var ConsentStateSchema =
|
|
1857
|
-
var ConsentNonceSchema =
|
|
1858
|
-
var ConsentAttemptTagSchema =
|
|
1859
|
-
var ConsentIdentitySchema =
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
kind:
|
|
1863
|
-
developerOid:
|
|
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
|
|
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 =
|
|
1966
|
+
var ConsultRequestSchema = z22.strictObject({
|
|
1875
1967
|
identity: ConsentIdentitySchema,
|
|
1876
|
-
appSlug:
|
|
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 =
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
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 =
|
|
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 =
|
|
1892
|
-
outcome:
|
|
1983
|
+
var CancelResponseSchema = z22.strictObject({
|
|
1984
|
+
outcome: z22.enum(CONSENT_CANCEL_OUTCOMES)
|
|
1893
1985
|
});
|
|
1894
|
-
var ConnectionStatusIdentitySchema =
|
|
1895
|
-
|
|
1896
|
-
|
|
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 =
|
|
1990
|
+
var ConnectionStatusRequestSchema = z22.strictObject({
|
|
1899
1991
|
identity: ConnectionStatusIdentitySchema,
|
|
1900
|
-
appSlug:
|
|
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 =
|
|
1904
|
-
status:
|
|
1995
|
+
var ConnectionStatusResponseSchema = z22.strictObject({
|
|
1996
|
+
status: z22.enum(["connected", "not_connected", "not_available"])
|
|
1905
1997
|
});
|
|
1906
|
-
var AttemptCancelRequestSchema =
|
|
1998
|
+
var AttemptCancelRequestSchema = z22.strictObject({
|
|
1907
1999
|
provider: ProviderRefSchema,
|
|
1908
2000
|
attempt: ConsentAttemptTagSchema
|
|
1909
2001
|
});
|
|
1910
|
-
var DevConsentStartResponseSchema =
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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 =
|
|
1935
|
-
source:
|
|
1936
|
-
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:
|
|
1940
|
-
reason:
|
|
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 =
|
|
1945
|
-
outcome:
|
|
1946
|
-
provider:
|
|
2036
|
+
var ConnectResultSchema = z22.object({
|
|
2037
|
+
outcome: z22.enum(CONSENT_MESSAGE_OUTCOMES),
|
|
2038
|
+
provider: z22.string(),
|
|
1947
2039
|
attempt: ConsentAttemptTagSchema.optional(),
|
|
1948
|
-
reason:
|
|
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
|
|
1955
|
-
var ExchangeCodeSchema =
|
|
1956
|
-
var CodeVerifierSchema =
|
|
1957
|
-
var ExchangeRequestSchema =
|
|
1958
|
-
providerId:
|
|
1959
|
-
providerRevision:
|
|
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 =
|
|
2062
|
+
var ExchangeRejectionReasonSchema = z23.enum(EXCHANGE_REJECTION_REASONS);
|
|
1971
2063
|
var MAX_EXCHANGED_SCOPES = 64;
|
|
1972
|
-
var ExchangedScopesSchema =
|
|
1973
|
-
var ExchangeResponseSchema =
|
|
1974
|
-
|
|
1975
|
-
outcome:
|
|
1976
|
-
access:
|
|
1977
|
-
refresh:
|
|
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:
|
|
2071
|
+
accessExpiresAt: z23.iso.datetime()
|
|
1980
2072
|
}),
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
2006
|
-
var DevTokenMintRequestSchema =
|
|
2007
|
-
origins:
|
|
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 =
|
|
2102
|
+
var DevTokenRotateRequestSchema = z24.object({
|
|
2011
2103
|
ttlDays: TtlDaysSchema
|
|
2012
2104
|
});
|
|
2013
|
-
var DevTokenMetadataSchema =
|
|
2014
|
-
id:
|
|
2015
|
-
developerOid:
|
|
2016
|
-
origins:
|
|
2017
|
-
expiresAt:
|
|
2018
|
-
revokedAt:
|
|
2019
|
-
createdAt:
|
|
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 =
|
|
2022
|
-
token:
|
|
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
|
|
2162
|
+
import { z as z26 } from "zod";
|
|
2028
2163
|
var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
|
|
2029
|
-
var InstructionCapabilitySchema =
|
|
2030
|
-
var AttestedInstructionSchema =
|
|
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:
|
|
2167
|
+
appId: z26.string().min(1),
|
|
2033
2168
|
/** Authenticated user, or the anonymous sentinel on `public` apps. */
|
|
2034
|
-
userOid:
|
|
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:
|
|
2186
|
+
origin: z26.url(),
|
|
2052
2187
|
/** Connection (secret) name to inject, if this is a secret-backed call. */
|
|
2053
|
-
connection:
|
|
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:
|
|
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:
|
|
2079
|
-
path:
|
|
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
|
|
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 =
|
|
2122
|
-
var FetchErrorProviderSchema =
|
|
2256
|
+
var FetchErrorCodeSchema = z27.enum(FETCH_ERROR_CODES);
|
|
2257
|
+
var FetchErrorProviderSchema = z27.strictObject({
|
|
2123
2258
|
ref: ProviderRefSchema,
|
|
2124
|
-
displayName:
|
|
2259
|
+
displayName: z27.string().min(1).max(200).optional()
|
|
2125
2260
|
});
|
|
2126
|
-
var FetchProxyErrorSchema =
|
|
2261
|
+
var FetchProxyErrorSchema = z27.object({
|
|
2127
2262
|
code: FetchErrorCodeSchema,
|
|
2128
|
-
message:
|
|
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
|
|
2135
|
-
var SessionPrincipalKindSchema =
|
|
2136
|
-
var SessionSummarySchema =
|
|
2137
|
-
id:
|
|
2138
|
-
appId:
|
|
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:
|
|
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:
|
|
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:
|
|
2155
|
-
userEmail:
|
|
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:
|
|
2159
|
-
createdAt:
|
|
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:
|
|
2296
|
+
activatedAt: z28.iso.datetime().nullable(),
|
|
2162
2297
|
/** When silent re-auth (`prompt=none`) becomes due; past = overdue. */
|
|
2163
|
-
refreshDueAt:
|
|
2298
|
+
refreshDueAt: z28.iso.datetime(),
|
|
2164
2299
|
/** Hard cap — the session is dead at this instant regardless of activity. */
|
|
2165
|
-
expiresAt:
|
|
2300
|
+
expiresAt: z28.iso.datetime()
|
|
2166
2301
|
});
|
|
2167
|
-
var SessionListResponseSchema =
|
|
2168
|
-
rows:
|
|
2169
|
-
groupNames:
|
|
2170
|
-
groupsResolved:
|
|
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 =
|
|
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:
|
|
2314
|
+
userOid: z28.string().min(1).max(200)
|
|
2180
2315
|
});
|
|
2181
|
-
var SessionRevokeResultSchema =
|
|
2182
|
-
removed:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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`,
|
|
2229
|
-
*
|
|
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
|
-
|
|
2262
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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(
|
|
2500
|
+
async #text(path5, opts) {
|
|
2294
2501
|
const headers = await this.#authHeaders(opts.auth ?? false);
|
|
2295
|
-
const res = await fetch(this.#url(
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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;
|