@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.
Files changed (87) hide show
  1. package/dist/admin/404/index.html +1 -1
  2. package/dist/admin/404.html +1 -1
  3. package/dist/admin/__next.!KGRlZmF1bHQp.__PAGE__.txt +2 -2
  4. package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
  5. package/dist/admin/__next._full.txt +2 -2
  6. package/dist/admin/__next._head.txt +1 -1
  7. package/dist/admin/__next._index.txt +1 -1
  8. package/dist/admin/__next._tree.txt +1 -1
  9. package/dist/admin/_next/static/chunks/{68598441248beee0.js → d5bf905a3d3f005b.js} +1 -1
  10. package/dist/admin/_not-found/__next._full.txt +1 -1
  11. package/dist/admin/_not-found/__next._head.txt +1 -1
  12. package/dist/admin/_not-found/__next._index.txt +1 -1
  13. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  14. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  15. package/dist/admin/_not-found/__next._tree.txt +1 -1
  16. package/dist/admin/_not-found/index.html +1 -1
  17. package/dist/admin/_not-found/index.txt +1 -1
  18. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
  19. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
  20. package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
  21. package/dist/admin/databases/__next._full.txt +1 -1
  22. package/dist/admin/databases/__next._head.txt +1 -1
  23. package/dist/admin/databases/__next._index.txt +1 -1
  24. package/dist/admin/databases/__next._tree.txt +1 -1
  25. package/dist/admin/databases/index.html +1 -1
  26. package/dist/admin/databases/index.txt +1 -1
  27. package/dist/admin/fullscreen/__next._full.txt +2 -2
  28. package/dist/admin/fullscreen/__next._head.txt +1 -1
  29. package/dist/admin/fullscreen/__next._index.txt +1 -1
  30. package/dist/admin/fullscreen/__next._tree.txt +1 -1
  31. package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +2 -2
  32. package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
  33. package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
  34. package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
  35. package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
  36. package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
  37. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
  38. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
  39. package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
  40. package/dist/admin/fullscreen/databases/index.html +1 -1
  41. package/dist/admin/fullscreen/databases/index.txt +1 -1
  42. package/dist/admin/fullscreen/index.html +1 -1
  43. package/dist/admin/fullscreen/index.txt +2 -2
  44. package/dist/admin/index.html +1 -1
  45. package/dist/admin/index.txt +2 -2
  46. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
  47. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
  48. package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
  49. package/dist/admin/mail/__next._full.txt +1 -1
  50. package/dist/admin/mail/__next._head.txt +1 -1
  51. package/dist/admin/mail/__next._index.txt +1 -1
  52. package/dist/admin/mail/__next._tree.txt +1 -1
  53. package/dist/admin/mail/index.html +1 -1
  54. package/dist/admin/mail/index.txt +1 -1
  55. package/dist/admin/services/__next.!KGRlZmF1bHQp.services.__PAGE__.txt +1 -1
  56. package/dist/admin/services/__next.!KGRlZmF1bHQp.services.txt +1 -1
  57. package/dist/admin/services/__next.!KGRlZmF1bHQp.txt +1 -1
  58. package/dist/admin/services/__next._full.txt +1 -1
  59. package/dist/admin/services/__next._head.txt +1 -1
  60. package/dist/admin/services/__next._index.txt +1 -1
  61. package/dist/admin/services/__next._tree.txt +1 -1
  62. package/dist/admin/services/index.html +1 -1
  63. package/dist/admin/services/index.txt +1 -1
  64. package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.__PAGE__.txt +1 -1
  65. package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.txt +1 -1
  66. package/dist/admin/storage/__next.!KGRlZmF1bHQp.txt +1 -1
  67. package/dist/admin/storage/__next._full.txt +1 -1
  68. package/dist/admin/storage/__next._head.txt +1 -1
  69. package/dist/admin/storage/__next._index.txt +1 -1
  70. package/dist/admin/storage/__next._tree.txt +1 -1
  71. package/dist/admin/storage/index.html +1 -1
  72. package/dist/admin/storage/index.txt +1 -1
  73. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
  74. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
  75. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
  76. package/dist/admin/workflows/__next._full.txt +1 -1
  77. package/dist/admin/workflows/__next._head.txt +1 -1
  78. package/dist/admin/workflows/__next._index.txt +1 -1
  79. package/dist/admin/workflows/__next._tree.txt +1 -1
  80. package/dist/admin/workflows/index.html +1 -1
  81. package/dist/admin/workflows/index.txt +1 -1
  82. package/dist/cli.js +229 -21
  83. package/dist/docs/services.md +35 -0
  84. package/package.json +1 -1
  85. /package/dist/admin/_next/static/{tQIwPrBmfmqJ2i8TYKMQV → 4_42SkCjqmgu0V3Wfnmok}/_buildManifest.js +0 -0
  86. /package/dist/admin/_next/static/{tQIwPrBmfmqJ2i8TYKMQV → 4_42SkCjqmgu0V3Wfnmok}/_clientMiddlewareManifest.json +0 -0
  87. /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":"tQIwPrBmfmqJ2i8TYKMQV","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}
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: new Uint8Array(tarball)
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(deploymentId, tarball, appPath)
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 parseEndpoints(fieldObj) {
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.159",
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 parseEndpoints2(fieldObj) {
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 formatBytes(bytes) {
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 ? ` (${formatBytes(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: "Uploading"
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
- deployment = await client2.uploadTarball(deployment.id, tarball, appPath);
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.159";
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.159").enablePositionalOptions();
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/cli",
3
- "version": "0.1.159",
3
+ "version": "0.1.161",
4
4
  "description": "CLI for Specific infrastructure-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",