@specific.dev/cli 0.1.159 → 0.1.161
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/dist/admin/404/index.html +1 -1
- package/dist/admin/404.html +1 -1
- package/dist/admin/__next.!KGRlZmF1bHQp.__PAGE__.txt +2 -2
- package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/__next._full.txt +2 -2
- package/dist/admin/__next._head.txt +1 -1
- package/dist/admin/__next._index.txt +1 -1
- package/dist/admin/__next._tree.txt +1 -1
- package/dist/admin/_next/static/chunks/{68598441248beee0.js → d5bf905a3d3f005b.js} +1 -1
- package/dist/admin/_not-found/__next._full.txt +1 -1
- package/dist/admin/_not-found/__next._head.txt +1 -1
- package/dist/admin/_not-found/__next._index.txt +1 -1
- package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/dist/admin/_not-found/__next._not-found.txt +1 -1
- package/dist/admin/_not-found/__next._tree.txt +1 -1
- package/dist/admin/_not-found/index.html +1 -1
- package/dist/admin/_not-found/index.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/databases/__next._full.txt +1 -1
- package/dist/admin/databases/__next._head.txt +1 -1
- package/dist/admin/databases/__next._index.txt +1 -1
- package/dist/admin/databases/__next._tree.txt +1 -1
- package/dist/admin/databases/index.html +1 -1
- package/dist/admin/databases/index.txt +1 -1
- package/dist/admin/fullscreen/__next._full.txt +2 -2
- package/dist/admin/fullscreen/__next._head.txt +1 -1
- package/dist/admin/fullscreen/__next._index.txt +1 -1
- package/dist/admin/fullscreen/__next._tree.txt +1 -1
- package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +2 -2
- package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
- package/dist/admin/fullscreen/databases/index.html +1 -1
- package/dist/admin/fullscreen/databases/index.txt +1 -1
- package/dist/admin/fullscreen/index.html +1 -1
- package/dist/admin/fullscreen/index.txt +2 -2
- package/dist/admin/index.html +1 -1
- package/dist/admin/index.txt +2 -2
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/mail/__next._full.txt +1 -1
- package/dist/admin/mail/__next._head.txt +1 -1
- package/dist/admin/mail/__next._index.txt +1 -1
- package/dist/admin/mail/__next._tree.txt +1 -1
- package/dist/admin/mail/index.html +1 -1
- package/dist/admin/mail/index.txt +1 -1
- package/dist/admin/services/__next.!KGRlZmF1bHQp.services.__PAGE__.txt +1 -1
- package/dist/admin/services/__next.!KGRlZmF1bHQp.services.txt +1 -1
- package/dist/admin/services/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/services/__next._full.txt +1 -1
- package/dist/admin/services/__next._head.txt +1 -1
- package/dist/admin/services/__next._index.txt +1 -1
- package/dist/admin/services/__next._tree.txt +1 -1
- package/dist/admin/services/index.html +1 -1
- package/dist/admin/services/index.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.__PAGE__.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/storage/__next._full.txt +1 -1
- package/dist/admin/storage/__next._head.txt +1 -1
- package/dist/admin/storage/__next._index.txt +1 -1
- package/dist/admin/storage/__next._tree.txt +1 -1
- package/dist/admin/storage/index.html +1 -1
- package/dist/admin/storage/index.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
- package/dist/admin/workflows/__next._full.txt +1 -1
- package/dist/admin/workflows/__next._head.txt +1 -1
- package/dist/admin/workflows/__next._index.txt +1 -1
- package/dist/admin/workflows/__next._tree.txt +1 -1
- package/dist/admin/workflows/index.html +1 -1
- package/dist/admin/workflows/index.txt +1 -1
- package/dist/cli.js +229 -21
- package/dist/docs/services.md +35 -0
- package/package.json +1 -1
- /package/dist/admin/_next/static/{tQIwPrBmfmqJ2i8TYKMQV → 4_42SkCjqmgu0V3Wfnmok}/_buildManifest.js +0 -0
- /package/dist/admin/_next/static/{tQIwPrBmfmqJ2i8TYKMQV → 4_42SkCjqmgu0V3Wfnmok}/_clientMiddlewareManifest.json +0 -0
- /package/dist/admin/_next/static/{tQIwPrBmfmqJ2i8TYKMQV → 4_42SkCjqmgu0V3Wfnmok}/_ssgManifest.js +0 -0
|
@@ -18,7 +18,7 @@ f:"$Sreact.suspense"
|
|
|
18
18
|
:HL["/_next/static/media/797e433ab948586e-s.p.479bea2b.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
|
19
19
|
:HL["/_next/static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
|
20
20
|
:HL["/_next/static/media/caa3a2e1cccd8315-s.p.3b6cae6d.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
|
21
|
-
0:{"P":null,"b":"
|
|
21
|
+
0:{"P":null,"b":"4_42SkCjqmgu0V3Wfnmok","c":["","workflows",""],"q":"","i":false,"f":[[["",{"children":["(default)",{"children":["workflows",{"children":["__PAGE__",{}]}]}]},"$undefined","$undefined",true],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/4ae8172f753ceb22.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/c52b2ebc474a0535.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"inter_b2991b2-module__9mH_6q__variable","suppressHydrationWarning":true,"children":["$","body",null,{"className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable antialiased","children":["$","$L2",null,{"attribute":"class","defaultTheme":"system","enableSystem":true,"disableTransitionOnChange":true,"children":["$","$L3",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]}]}]]}],{"children":[["$","$1","c",{"children":[[["$","script","script-0",{"src":"/_next/static/chunks/c7aaec1998a39e17.js","async":true,"nonce":"$undefined"}],["$","script","script-1",{"src":"/_next/static/chunks/b44581875c51fcd8.js","async":true,"nonce":"$undefined"}],["$","script","script-2",{"src":"/_next/static/chunks/9835cc4e14c5aee3.js","async":true,"nonce":"$undefined"}],["$","script","script-3",{"src":"/_next/static/chunks/dc5c264e3262a84a.js","async":true,"nonce":"$undefined"}]],["$","$L6",null,{"children":[["$","$L7",null,{}],["$","$L8",null,{"className":"overflow-hidden","children":["$","$L9",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L4",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[["$","$La",null,{"Component":"$b","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@c","$@d"]}}],[["$","script","script-0",{"src":"/_next/static/chunks/e3baf0c708c5b9ae.js","async":true,"nonce":"$undefined"}]],["$","$Le",null,{"children":["$","$f",null,{"name":"Next.MetadataOutlet","children":"$@10"}]}]]}],{},null,false,false]},null,false,false]},null,false,false]},null,false,false],["$","$1","h",{"children":[null,["$","$L11",null,{"children":"$L12"}],["$","div",null,{"hidden":true,"children":["$","$L13",null,{"children":["$","$f",null,{"name":"Next.Metadata","children":"$L14"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$15",[]],"S":true}
|
|
22
22
|
c:{}
|
|
23
23
|
d:"$0:f:0:1:1:children:1:children:1:children:0:props:children:0:props:serverProvidedParams:params"
|
|
24
24
|
12:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
|
package/dist/cli.js
CHANGED
|
@@ -367507,6 +367507,29 @@ function closeDebugLog() {
|
|
|
367507
367507
|
logStream = null;
|
|
367508
367508
|
}
|
|
367509
367509
|
}
|
|
367510
|
+
var UPLOAD_CHUNK_BYTES = 256 * 1024;
|
|
367511
|
+
function progressStreamFromBuffer(buffer, onProgress) {
|
|
367512
|
+
const total = buffer.length;
|
|
367513
|
+
let offset = 0;
|
|
367514
|
+
return new ReadableStream({
|
|
367515
|
+
pull(controller) {
|
|
367516
|
+
if (offset >= total) {
|
|
367517
|
+
controller.close();
|
|
367518
|
+
return;
|
|
367519
|
+
}
|
|
367520
|
+
const end = Math.min(offset + UPLOAD_CHUNK_BYTES, total);
|
|
367521
|
+
const chunk = new Uint8Array(buffer.subarray(offset, end));
|
|
367522
|
+
controller.enqueue(chunk);
|
|
367523
|
+
offset = end;
|
|
367524
|
+
if (onProgress) {
|
|
367525
|
+
try {
|
|
367526
|
+
onProgress(offset, total);
|
|
367527
|
+
} catch {
|
|
367528
|
+
}
|
|
367529
|
+
}
|
|
367530
|
+
}
|
|
367531
|
+
});
|
|
367532
|
+
}
|
|
367510
367533
|
var ApiError = class extends Error {
|
|
367511
367534
|
code;
|
|
367512
367535
|
httpStatus;
|
|
@@ -367600,17 +367623,22 @@ var ApiClient = class {
|
|
|
367600
367623
|
}
|
|
367601
367624
|
return response.json();
|
|
367602
367625
|
}
|
|
367603
|
-
async uploadTarball(deploymentId, tarball, appPath = ".") {
|
|
367626
|
+
async uploadTarball(deploymentId, tarball, appPath = ".", onProgress) {
|
|
367604
367627
|
const url = `${this.baseUrl}/deployments/${deploymentId}/upload`;
|
|
367605
367628
|
writeLog("api", `POST ${url} (${tarball.length} bytes, appPath: ${appPath})`);
|
|
367606
367629
|
const response = await fetch(url, {
|
|
367607
367630
|
method: "POST",
|
|
367608
367631
|
headers: {
|
|
367609
367632
|
"Content-Type": "application/octet-stream",
|
|
367633
|
+
"Content-Length": String(tarball.length),
|
|
367610
367634
|
"X-App-Path": appPath,
|
|
367611
367635
|
...await this.authHeaders()
|
|
367612
367636
|
},
|
|
367613
|
-
body:
|
|
367637
|
+
body: progressStreamFromBuffer(tarball, onProgress),
|
|
367638
|
+
// Required by undici when body is a ReadableStream — the request body
|
|
367639
|
+
// is streamed half-duplex, not duplex.
|
|
367640
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
367641
|
+
duplex: "half"
|
|
367614
367642
|
});
|
|
367615
367643
|
writeLog("api", `Response: ${response.status} ${response.statusText}`);
|
|
367616
367644
|
if (!response.ok) {
|
|
@@ -367622,6 +367650,62 @@ var ApiClient = class {
|
|
|
367622
367650
|
}
|
|
367623
367651
|
return response.json();
|
|
367624
367652
|
}
|
|
367653
|
+
/**
|
|
367654
|
+
* PUT the tarball directly to an S3 presigned URL handed back by
|
|
367655
|
+
* createDeployment. Bypasses the platform API entirely on the data path
|
|
367656
|
+
* so large repos don't time out at the ingress.
|
|
367657
|
+
*/
|
|
367658
|
+
async uploadTarballToPresignedUrl(presignedUrl, tarball, onProgress) {
|
|
367659
|
+
writeLog(
|
|
367660
|
+
"api",
|
|
367661
|
+
`PUT <presigned> (${tarball.length} bytes)`
|
|
367662
|
+
);
|
|
367663
|
+
const response = await fetch(presignedUrl, {
|
|
367664
|
+
method: "PUT",
|
|
367665
|
+
headers: {
|
|
367666
|
+
"Content-Type": "application/gzip",
|
|
367667
|
+
"Content-Length": String(tarball.length)
|
|
367668
|
+
},
|
|
367669
|
+
body: progressStreamFromBuffer(tarball, onProgress),
|
|
367670
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
367671
|
+
duplex: "half"
|
|
367672
|
+
});
|
|
367673
|
+
writeLog("api", `Response: ${response.status} ${response.statusText}`);
|
|
367674
|
+
if (!response.ok) {
|
|
367675
|
+
const bodySnippet = await response.text().catch(() => "");
|
|
367676
|
+
const snippet = bodySnippet.trim().slice(0, 200);
|
|
367677
|
+
const suffix = snippet ? ` \u2014 ${snippet}` : "";
|
|
367678
|
+
throw new Error(
|
|
367679
|
+
`Failed to upload tarball to S3: HTTP ${response.status} ${response.statusText}${suffix}`
|
|
367680
|
+
);
|
|
367681
|
+
}
|
|
367682
|
+
}
|
|
367683
|
+
/**
|
|
367684
|
+
* Tell the platform that the tarball is now in S3 at the deployment's
|
|
367685
|
+
* canonical key. Triggers the same scan + build pipeline as the legacy
|
|
367686
|
+
* /upload route, but without sending bytes through the API.
|
|
367687
|
+
*/
|
|
367688
|
+
async notifyUploaded(deploymentId, appPath = ".") {
|
|
367689
|
+
const url = `${this.baseUrl}/deployments/${deploymentId}/uploaded`;
|
|
367690
|
+
writeLog("api", `POST ${url} (appPath: ${appPath})`);
|
|
367691
|
+
const response = await fetch(url, {
|
|
367692
|
+
method: "POST",
|
|
367693
|
+
headers: {
|
|
367694
|
+
"Content-Type": "application/json",
|
|
367695
|
+
...await this.authHeaders()
|
|
367696
|
+
},
|
|
367697
|
+
body: JSON.stringify({ appPath })
|
|
367698
|
+
});
|
|
367699
|
+
writeLog("api", `Response: ${response.status} ${response.statusText}`);
|
|
367700
|
+
if (!response.ok) {
|
|
367701
|
+
throw await readApiError(
|
|
367702
|
+
response,
|
|
367703
|
+
"Failed to finalize tarball upload",
|
|
367704
|
+
`POST ${url}`
|
|
367705
|
+
);
|
|
367706
|
+
}
|
|
367707
|
+
return response.json();
|
|
367708
|
+
}
|
|
367625
367709
|
async getDeployment(deploymentId) {
|
|
367626
367710
|
const url = `${this.baseUrl}/deployments/${deploymentId}`;
|
|
367627
367711
|
writeLog("api", `GET ${url}`);
|
|
@@ -367907,9 +367991,29 @@ var SpecificClient = class {
|
|
|
367907
367991
|
await this.client.createDeployment(projectId, environment, options2)
|
|
367908
367992
|
);
|
|
367909
367993
|
}
|
|
367910
|
-
async uploadTarball(deploymentId, tarball, appPath) {
|
|
367994
|
+
async uploadTarball(deploymentId, tarball, appPath, options2) {
|
|
367995
|
+
if (options2?.maxTarballBytes !== void 0 && tarball.length > options2.maxTarballBytes) {
|
|
367996
|
+
throw new Error(
|
|
367997
|
+
`Tarball is ${formatBytes(tarball.length)}; the platform accepts up to ${formatBytes(options2.maxTarballBytes)}. Check for large files that don't need to be deployed (build artifacts, uncompressed media) and exclude them via .gitignore.`
|
|
367998
|
+
);
|
|
367999
|
+
}
|
|
368000
|
+
if (options2?.uploadUrl) {
|
|
368001
|
+
await this.client.uploadTarballToPresignedUrl(
|
|
368002
|
+
options2.uploadUrl,
|
|
368003
|
+
tarball,
|
|
368004
|
+
options2.onProgress
|
|
368005
|
+
);
|
|
368006
|
+
return toDeployment(
|
|
368007
|
+
await this.client.notifyUploaded(deploymentId, appPath ?? ".")
|
|
368008
|
+
);
|
|
368009
|
+
}
|
|
367911
368010
|
return toDeployment(
|
|
367912
|
-
await this.client.uploadTarball(
|
|
368011
|
+
await this.client.uploadTarball(
|
|
368012
|
+
deploymentId,
|
|
368013
|
+
tarball,
|
|
368014
|
+
appPath,
|
|
368015
|
+
options2?.onProgress
|
|
368016
|
+
)
|
|
367913
368017
|
);
|
|
367914
368018
|
}
|
|
367915
368019
|
async getDeployment(deploymentId) {
|
|
@@ -367951,9 +368055,18 @@ function toDeployment(response) {
|
|
|
367951
368055
|
changes: response.changes,
|
|
367952
368056
|
pendingActions: response.pendingActions,
|
|
367953
368057
|
publicUrls: response.publicUrls,
|
|
367954
|
-
createdAt: response.createdAt
|
|
368058
|
+
createdAt: response.createdAt,
|
|
368059
|
+
uploadUrl: response.uploadUrl,
|
|
368060
|
+
maxTarballBytes: response.maxTarballBytes
|
|
367955
368061
|
};
|
|
367956
368062
|
}
|
|
368063
|
+
function formatBytes(bytes) {
|
|
368064
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
368065
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
368066
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
368067
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
368068
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
368069
|
+
}
|
|
367957
368070
|
function isInsideGitRepository(dir) {
|
|
367958
368071
|
try {
|
|
367959
368072
|
const result = execSync("git rev-parse --is-inside-work-tree", {
|
|
@@ -368402,7 +368515,26 @@ function parseBuildRef(build) {
|
|
|
368402
368515
|
}
|
|
368403
368516
|
return void 0;
|
|
368404
368517
|
}
|
|
368405
|
-
function
|
|
368518
|
+
function parseHealthCheckBlock(serviceName, endpointName, healthCheck) {
|
|
368519
|
+
if (healthCheck === void 0) {
|
|
368520
|
+
return void 0;
|
|
368521
|
+
}
|
|
368522
|
+
const arr = Array.isArray(healthCheck) ? healthCheck : [healthCheck];
|
|
368523
|
+
const obj = arr[0];
|
|
368524
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
368525
|
+
throw new Error(`Service "${serviceName}" endpoint "${endpointName}" has an invalid health_check block.`);
|
|
368526
|
+
}
|
|
368527
|
+
const fields = obj;
|
|
368528
|
+
const pathValue = Array.isArray(fields.path) ? fields.path[0] : fields.path;
|
|
368529
|
+
if (typeof pathValue !== "string" || pathValue.trim() === "") {
|
|
368530
|
+
throw new Error(`Service "${serviceName}" endpoint "${endpointName}" health_check is missing required "path".`);
|
|
368531
|
+
}
|
|
368532
|
+
if (!pathValue.startsWith("/")) {
|
|
368533
|
+
throw new Error(`Service "${serviceName}" endpoint "${endpointName}" health_check "path" must start with "/" (got "${pathValue}").`);
|
|
368534
|
+
}
|
|
368535
|
+
return { path: pathValue };
|
|
368536
|
+
}
|
|
368537
|
+
function parseEndpoints(serviceName, fieldObj) {
|
|
368406
368538
|
const endpoints = [];
|
|
368407
368539
|
const endpointData = fieldObj.endpoint;
|
|
368408
368540
|
if (!endpointData) {
|
|
@@ -368416,9 +368548,11 @@ function parseEndpoints(fieldObj) {
|
|
|
368416
368548
|
if (ep && typeof ep === "object" && !Array.isArray(ep)) {
|
|
368417
368549
|
const epObj = ep;
|
|
368418
368550
|
const publicValue = Array.isArray(epObj.public) ? epObj.public[0] : epObj.public;
|
|
368551
|
+
const healthCheck = parseHealthCheckBlock(serviceName, "default", epObj.health_check);
|
|
368419
368552
|
endpoints.push({
|
|
368420
368553
|
name: "default",
|
|
368421
|
-
public: publicValue === true
|
|
368554
|
+
public: publicValue === true,
|
|
368555
|
+
...healthCheck ? { health_check: healthCheck } : {}
|
|
368422
368556
|
});
|
|
368423
368557
|
}
|
|
368424
368558
|
}
|
|
@@ -368428,9 +368562,11 @@ function parseEndpoints(fieldObj) {
|
|
|
368428
368562
|
if (fields && typeof fields === "object" && !Array.isArray(fields)) {
|
|
368429
368563
|
const fieldObj2 = fields;
|
|
368430
368564
|
const publicValue = Array.isArray(fieldObj2.public) ? fieldObj2.public[0] : fieldObj2.public;
|
|
368565
|
+
const healthCheck = parseHealthCheckBlock(serviceName, name, fieldObj2.health_check);
|
|
368431
368566
|
endpoints.push({
|
|
368432
368567
|
name,
|
|
368433
|
-
public: publicValue === true
|
|
368568
|
+
public: publicValue === true,
|
|
368569
|
+
...healthCheck ? { health_check: healthCheck } : {}
|
|
368434
368570
|
});
|
|
368435
368571
|
} else {
|
|
368436
368572
|
endpoints.push({
|
|
@@ -368492,7 +368628,7 @@ function parseServices(serviceData) {
|
|
|
368492
368628
|
if (fieldObj.root) {
|
|
368493
368629
|
service.root = String(fieldObj.root);
|
|
368494
368630
|
}
|
|
368495
|
-
const endpoints = parseEndpoints(fieldObj);
|
|
368631
|
+
const endpoints = parseEndpoints(name, fieldObj);
|
|
368496
368632
|
const volumes = parseVolumes(fieldObj);
|
|
368497
368633
|
if (volumes.length > 0) {
|
|
368498
368634
|
service.volumes = volumes;
|
|
@@ -374493,7 +374629,7 @@ function trackEvent(event, properties) {
|
|
|
374493
374629
|
event,
|
|
374494
374630
|
properties: {
|
|
374495
374631
|
...properties,
|
|
374496
|
-
cli_version: "0.1.
|
|
374632
|
+
cli_version: "0.1.161",
|
|
374497
374633
|
platform: process.platform,
|
|
374498
374634
|
node_version: process.version,
|
|
374499
374635
|
project_id: getProjectId()
|
|
@@ -375282,7 +375418,26 @@ function parseBuildRef2(build) {
|
|
|
375282
375418
|
}
|
|
375283
375419
|
return void 0;
|
|
375284
375420
|
}
|
|
375285
|
-
function
|
|
375421
|
+
function parseHealthCheckBlock2(serviceName, endpointName, healthCheck) {
|
|
375422
|
+
if (healthCheck === void 0) {
|
|
375423
|
+
return void 0;
|
|
375424
|
+
}
|
|
375425
|
+
const arr = Array.isArray(healthCheck) ? healthCheck : [healthCheck];
|
|
375426
|
+
const obj = arr[0];
|
|
375427
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
375428
|
+
throw new Error(`Service "${serviceName}" endpoint "${endpointName}" has an invalid health_check block.`);
|
|
375429
|
+
}
|
|
375430
|
+
const fields = obj;
|
|
375431
|
+
const pathValue = Array.isArray(fields.path) ? fields.path[0] : fields.path;
|
|
375432
|
+
if (typeof pathValue !== "string" || pathValue.trim() === "") {
|
|
375433
|
+
throw new Error(`Service "${serviceName}" endpoint "${endpointName}" health_check is missing required "path".`);
|
|
375434
|
+
}
|
|
375435
|
+
if (!pathValue.startsWith("/")) {
|
|
375436
|
+
throw new Error(`Service "${serviceName}" endpoint "${endpointName}" health_check "path" must start with "/" (got "${pathValue}").`);
|
|
375437
|
+
}
|
|
375438
|
+
return { path: pathValue };
|
|
375439
|
+
}
|
|
375440
|
+
function parseEndpoints2(serviceName, fieldObj) {
|
|
375286
375441
|
const endpoints = [];
|
|
375287
375442
|
const endpointData = fieldObj.endpoint;
|
|
375288
375443
|
if (!endpointData) {
|
|
@@ -375296,9 +375451,11 @@ function parseEndpoints2(fieldObj) {
|
|
|
375296
375451
|
if (ep && typeof ep === "object" && !Array.isArray(ep)) {
|
|
375297
375452
|
const epObj = ep;
|
|
375298
375453
|
const publicValue = Array.isArray(epObj.public) ? epObj.public[0] : epObj.public;
|
|
375454
|
+
const healthCheck = parseHealthCheckBlock2(serviceName, "default", epObj.health_check);
|
|
375299
375455
|
endpoints.push({
|
|
375300
375456
|
name: "default",
|
|
375301
|
-
public: publicValue === true
|
|
375457
|
+
public: publicValue === true,
|
|
375458
|
+
...healthCheck ? { health_check: healthCheck } : {}
|
|
375302
375459
|
});
|
|
375303
375460
|
}
|
|
375304
375461
|
}
|
|
@@ -375308,9 +375465,11 @@ function parseEndpoints2(fieldObj) {
|
|
|
375308
375465
|
if (fields && typeof fields === "object" && !Array.isArray(fields)) {
|
|
375309
375466
|
const fieldObj2 = fields;
|
|
375310
375467
|
const publicValue = Array.isArray(fieldObj2.public) ? fieldObj2.public[0] : fieldObj2.public;
|
|
375468
|
+
const healthCheck = parseHealthCheckBlock2(serviceName, name, fieldObj2.health_check);
|
|
375311
375469
|
endpoints.push({
|
|
375312
375470
|
name,
|
|
375313
|
-
public: publicValue === true
|
|
375471
|
+
public: publicValue === true,
|
|
375472
|
+
...healthCheck ? { health_check: healthCheck } : {}
|
|
375314
375473
|
});
|
|
375315
375474
|
} else {
|
|
375316
375475
|
endpoints.push({
|
|
@@ -375372,7 +375531,7 @@ function parseServices2(serviceData) {
|
|
|
375372
375531
|
if (fieldObj.root) {
|
|
375373
375532
|
service.root = String(fieldObj.root);
|
|
375374
375533
|
}
|
|
375375
|
-
const endpoints = parseEndpoints2(fieldObj);
|
|
375534
|
+
const endpoints = parseEndpoints2(name, fieldObj);
|
|
375376
375535
|
const volumes = parseVolumes2(fieldObj);
|
|
375377
375536
|
if (volumes.length > 0) {
|
|
375378
375537
|
service.volumes = volumes;
|
|
@@ -376361,11 +376520,37 @@ function getGitInfo(cwd) {
|
|
|
376361
376520
|
return null;
|
|
376362
376521
|
}
|
|
376363
376522
|
}
|
|
376364
|
-
function
|
|
376523
|
+
function formatBytes2(bytes) {
|
|
376365
376524
|
if (bytes < 1024) return `${bytes} B`;
|
|
376366
376525
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
376367
376526
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
376368
376527
|
}
|
|
376528
|
+
var PROGRESS_BAR_CELLS = 20;
|
|
376529
|
+
var PROGRESS_REPORT_STEP_BYTES = 1024 * 1024;
|
|
376530
|
+
function renderProgressBar(bytesSent, totalBytes) {
|
|
376531
|
+
if (totalBytes <= 0) return "";
|
|
376532
|
+
const ratio = Math.min(1, Math.max(0, bytesSent / totalBytes));
|
|
376533
|
+
const filled = Math.round(ratio * PROGRESS_BAR_CELLS);
|
|
376534
|
+
const empty = PROGRESS_BAR_CELLS - filled;
|
|
376535
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
376536
|
+
const pct = Math.floor(ratio * 100);
|
|
376537
|
+
return `${bar} ${pct}% ${formatBytes2(bytesSent)} / ${formatBytes2(totalBytes)}`;
|
|
376538
|
+
}
|
|
376539
|
+
var PROGRESS_BAR_REVEAL_DELAY_MS = 2e3;
|
|
376540
|
+
function makeUploadProgressHandler(setState) {
|
|
376541
|
+
let lastReported = 0;
|
|
376542
|
+
const startedAt = Date.now();
|
|
376543
|
+
return (bytesSent, totalBytes) => {
|
|
376544
|
+
if (Date.now() - startedAt < PROGRESS_BAR_REVEAL_DELAY_MS) return;
|
|
376545
|
+
const done = bytesSent >= totalBytes;
|
|
376546
|
+
if (!done && bytesSent - lastReported < PROGRESS_REPORT_STEP_BYTES) return;
|
|
376547
|
+
lastReported = bytesSent;
|
|
376548
|
+
setState((s) => ({
|
|
376549
|
+
...s,
|
|
376550
|
+
uploadProgress: { bytesSent, totalBytes }
|
|
376551
|
+
}));
|
|
376552
|
+
};
|
|
376553
|
+
}
|
|
376369
376554
|
function PhaseIndicator({
|
|
376370
376555
|
phase,
|
|
376371
376556
|
currentPhase,
|
|
@@ -376517,6 +376702,7 @@ function formatErrorCode(code) {
|
|
|
376517
376702
|
const labels = {
|
|
376518
376703
|
pre_deploy_failed: "Pre-deploy hook failed",
|
|
376519
376704
|
deploy_failed: "Service failed to deploy",
|
|
376705
|
+
health_check_failed: "Health check failed",
|
|
376520
376706
|
post_deploy_failed: "Post-deploy hook failed",
|
|
376521
376707
|
internal_error: "Internal error"
|
|
376522
376708
|
};
|
|
@@ -377080,7 +377266,11 @@ function DeployUI({ envFlag, preview, config }) {
|
|
|
377080
377266
|
setState((s) => ({ ...s, phase: "uploading", deployment: deployment2 }));
|
|
377081
377267
|
try {
|
|
377082
377268
|
writeLog("deploy", `Uploading tarball (${tarball.length} bytes, appPath: ${appPath})`);
|
|
377083
|
-
deployment2 = await client2.uploadTarball(deployment2.id, tarball, appPath
|
|
377269
|
+
deployment2 = await client2.uploadTarball(deployment2.id, tarball, appPath, {
|
|
377270
|
+
uploadUrl: deployment2.uploadUrl,
|
|
377271
|
+
maxTarballBytes: deployment2.maxTarballBytes,
|
|
377272
|
+
onProgress: makeUploadProgressHandler(setState)
|
|
377273
|
+
});
|
|
377084
377274
|
writeLog("deploy", "Tarball uploaded successfully");
|
|
377085
377275
|
} catch (err) {
|
|
377086
377276
|
const errorMsg = `Failed to upload tarball: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -377319,6 +377509,7 @@ function DeployUI({ envFlag, preview, config }) {
|
|
|
377319
377509
|
error,
|
|
377320
377510
|
buildOutput,
|
|
377321
377511
|
tarballSize,
|
|
377512
|
+
uploadProgress,
|
|
377322
377513
|
pendingActions,
|
|
377323
377514
|
missingSecrets,
|
|
377324
377515
|
currentSecretIndex,
|
|
@@ -377429,7 +377620,7 @@ function DeployUI({ envFlag, preview, config }) {
|
|
|
377429
377620
|
{
|
|
377430
377621
|
phase: "creating-tarball",
|
|
377431
377622
|
currentPhase: displayPhase,
|
|
377432
|
-
label: `Creating archive${tarballSize ? ` (${
|
|
377623
|
+
label: `Creating archive${tarballSize ? ` (${formatBytes2(tarballSize)})` : ""}`
|
|
377433
377624
|
}
|
|
377434
377625
|
), /* @__PURE__ */ React7.createElement(
|
|
377435
377626
|
PhaseIndicator,
|
|
@@ -377443,7 +377634,12 @@ function DeployUI({ envFlag, preview, config }) {
|
|
|
377443
377634
|
{
|
|
377444
377635
|
phase: "uploading",
|
|
377445
377636
|
currentPhase: displayPhase,
|
|
377446
|
-
label:
|
|
377637
|
+
label: (
|
|
377638
|
+
// Only show the bar while we're actively uploading. Once we move
|
|
377639
|
+
// to the next phase, drop back to plain "Uploading" so the
|
|
377640
|
+
// completed-step line stays clean.
|
|
377641
|
+
displayPhase === "uploading" && uploadProgress ? `Uploading ${renderProgressBar(uploadProgress.bytesSent, uploadProgress.totalBytes)}` : "Uploading"
|
|
377642
|
+
)
|
|
377447
377643
|
}
|
|
377448
377644
|
), /* @__PURE__ */ React7.createElement(
|
|
377449
377645
|
PhaseIndicator,
|
|
@@ -377647,7 +377843,19 @@ async function runDeployPipeline(options2) {
|
|
|
377647
377843
|
}
|
|
377648
377844
|
console.log("Uploading...");
|
|
377649
377845
|
try {
|
|
377650
|
-
|
|
377846
|
+
let lastPct = -1;
|
|
377847
|
+
deployment = await client2.uploadTarball(deployment.id, tarball, appPath, {
|
|
377848
|
+
uploadUrl: deployment.uploadUrl,
|
|
377849
|
+
maxTarballBytes: deployment.maxTarballBytes,
|
|
377850
|
+
onProgress: (bytesSent, totalBytes) => {
|
|
377851
|
+
const pct = Math.floor(bytesSent / totalBytes * 100);
|
|
377852
|
+
if (pct !== lastPct) {
|
|
377853
|
+
lastPct = pct;
|
|
377854
|
+
process.stdout.write(`\rUploading... ${pct}%`);
|
|
377855
|
+
if (bytesSent >= totalBytes) process.stdout.write("\n");
|
|
377856
|
+
}
|
|
377857
|
+
}
|
|
377858
|
+
});
|
|
377651
377859
|
} catch (err) {
|
|
377652
377860
|
console.error(`Error: Failed to upload: ${err instanceof Error ? err.message : String(err)}`);
|
|
377653
377861
|
process.exit(1);
|
|
@@ -378677,7 +378885,7 @@ function compareVersions(a, b) {
|
|
|
378677
378885
|
return 0;
|
|
378678
378886
|
}
|
|
378679
378887
|
async function checkForUpdate() {
|
|
378680
|
-
const currentVersion = "0.1.
|
|
378888
|
+
const currentVersion = "0.1.161";
|
|
378681
378889
|
const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
|
|
378682
378890
|
if (!response.ok) {
|
|
378683
378891
|
throw new Error(`Failed to check for updates: HTTP ${response.status}`);
|
|
@@ -379266,7 +379474,7 @@ async function domainAddCommand(service, domain, options2) {
|
|
|
379266
379474
|
var program = new Command();
|
|
379267
379475
|
var env = "production";
|
|
379268
379476
|
var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
|
|
379269
|
-
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.
|
|
379477
|
+
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.161").enablePositionalOptions();
|
|
379270
379478
|
program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
|
|
379271
379479
|
Examples:
|
|
379272
379480
|
$ specific init
|
package/dist/docs/services.md
CHANGED
|
@@ -121,6 +121,41 @@ service "worker" {
|
|
|
121
121
|
}
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
### Health checks
|
|
125
|
+
|
|
126
|
+
Add a `health_check` block inside an `endpoint` to tell Specific how to verify the service is healthy. The platform sends an HTTP `GET` to the given path on the endpoint's port — if the response is `2xx`, the service is considered healthy.
|
|
127
|
+
|
|
128
|
+
```hcl
|
|
129
|
+
service "api" {
|
|
130
|
+
build = build.api
|
|
131
|
+
command = "./api"
|
|
132
|
+
|
|
133
|
+
endpoint {
|
|
134
|
+
public = true
|
|
135
|
+
health_check {
|
|
136
|
+
path = "/healthz"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
env = {
|
|
141
|
+
PORT = port
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `path` - The HTTP path to probe (required, must start with `/`). The service must respond with a `2xx` status code when healthy.
|
|
147
|
+
- A health check should be a low-cost endpoint: don't query the database, call other services, or do real work. Just confirm the process is up and able to respond.
|
|
148
|
+
- Only one `health_check` per service is used. If multiple endpoints declare one, the first one wins.
|
|
149
|
+
- Background workers (services without endpoints) don't support `health_check` yet — only HTTP-serving endpoints do.
|
|
150
|
+
|
|
151
|
+
**Behavior in production:**
|
|
152
|
+
|
|
153
|
+
- During a deploy, the new pod doesn't receive traffic until its health check passes. The old version stays up until then, so users never hit a half-started process.
|
|
154
|
+
- If the health check starts failing after the deploy is live, the service is restarted automatically. Traffic is drained from the unhealthy instance well before the restart happens.
|
|
155
|
+
- If the health check never passes during a deploy (typically a misconfigured `path`), the deploy fails fast with a `health_check_failed` error pointing at the probe URL, rather than hanging until the deploy timeout.
|
|
156
|
+
|
|
157
|
+
A handler that returns `200 OK` with a constant body is sufficient. If the service already exposes a root path that always returns `2xx` (e.g. a static site), you can point `health_check` at `/` instead of adding a dedicated route.
|
|
158
|
+
|
|
124
159
|
## Private inter-service communication
|
|
125
160
|
|
|
126
161
|
Services can reference other services' endpoints using `private_url` to communicate with each other over a private network:
|
package/package.json
CHANGED
/package/dist/admin/_next/static/{tQIwPrBmfmqJ2i8TYKMQV → 4_42SkCjqmgu0V3Wfnmok}/_buildManifest.js
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/admin/_next/static/{tQIwPrBmfmqJ2i8TYKMQV → 4_42SkCjqmgu0V3Wfnmok}/_ssgManifest.js
RENAMED
|
File without changes
|