@specific.dev/cli 0.1.181 → 0.1.183

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 (98) 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 +1 -1
  4. package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
  5. package/dist/admin/__next._full.txt +1 -1
  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/_not-found/__next._full.txt +1 -1
  10. package/dist/admin/_not-found/__next._head.txt +1 -1
  11. package/dist/admin/_not-found/__next._index.txt +1 -1
  12. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  13. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  14. package/dist/admin/_not-found/__next._tree.txt +1 -1
  15. package/dist/admin/_not-found/index.html +1 -1
  16. package/dist/admin/_not-found/index.txt +1 -1
  17. package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.__PAGE__.txt +1 -1
  18. package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.txt +1 -1
  19. package/dist/admin/crons/__next.!KGRlZmF1bHQp.txt +1 -1
  20. package/dist/admin/crons/__next._full.txt +1 -1
  21. package/dist/admin/crons/__next._head.txt +1 -1
  22. package/dist/admin/crons/__next._index.txt +1 -1
  23. package/dist/admin/crons/__next._tree.txt +1 -1
  24. package/dist/admin/crons/index.html +1 -1
  25. package/dist/admin/crons/index.txt +1 -1
  26. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
  27. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
  28. package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
  29. package/dist/admin/databases/__next._full.txt +1 -1
  30. package/dist/admin/databases/__next._head.txt +1 -1
  31. package/dist/admin/databases/__next._index.txt +1 -1
  32. package/dist/admin/databases/__next._tree.txt +1 -1
  33. package/dist/admin/databases/index.html +1 -1
  34. package/dist/admin/databases/index.txt +1 -1
  35. package/dist/admin/fullscreen/__next._full.txt +1 -1
  36. package/dist/admin/fullscreen/__next._head.txt +1 -1
  37. package/dist/admin/fullscreen/__next._index.txt +1 -1
  38. package/dist/admin/fullscreen/__next._tree.txt +1 -1
  39. package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +1 -1
  40. package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
  41. package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
  42. package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
  43. package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
  44. package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
  45. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
  46. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
  47. package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
  48. package/dist/admin/fullscreen/databases/index.html +1 -1
  49. package/dist/admin/fullscreen/databases/index.txt +1 -1
  50. package/dist/admin/fullscreen/index.html +1 -1
  51. package/dist/admin/fullscreen/index.txt +1 -1
  52. package/dist/admin/index.html +1 -1
  53. package/dist/admin/index.txt +1 -1
  54. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
  55. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
  56. package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
  57. package/dist/admin/mail/__next._full.txt +1 -1
  58. package/dist/admin/mail/__next._head.txt +1 -1
  59. package/dist/admin/mail/__next._index.txt +1 -1
  60. package/dist/admin/mail/__next._tree.txt +1 -1
  61. package/dist/admin/mail/index.html +1 -1
  62. package/dist/admin/mail/index.txt +1 -1
  63. package/dist/admin/services/__next.!KGRlZmF1bHQp.services.__PAGE__.txt +1 -1
  64. package/dist/admin/services/__next.!KGRlZmF1bHQp.services.txt +1 -1
  65. package/dist/admin/services/__next.!KGRlZmF1bHQp.txt +1 -1
  66. package/dist/admin/services/__next._full.txt +1 -1
  67. package/dist/admin/services/__next._head.txt +1 -1
  68. package/dist/admin/services/__next._index.txt +1 -1
  69. package/dist/admin/services/__next._tree.txt +1 -1
  70. package/dist/admin/services/index.html +1 -1
  71. package/dist/admin/services/index.txt +1 -1
  72. package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.__PAGE__.txt +1 -1
  73. package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.txt +1 -1
  74. package/dist/admin/storage/__next.!KGRlZmF1bHQp.txt +1 -1
  75. package/dist/admin/storage/__next._full.txt +1 -1
  76. package/dist/admin/storage/__next._head.txt +1 -1
  77. package/dist/admin/storage/__next._index.txt +1 -1
  78. package/dist/admin/storage/__next._tree.txt +1 -1
  79. package/dist/admin/storage/index.html +1 -1
  80. package/dist/admin/storage/index.txt +1 -1
  81. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
  82. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
  83. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
  84. package/dist/admin/workflows/__next._full.txt +1 -1
  85. package/dist/admin/workflows/__next._head.txt +1 -1
  86. package/dist/admin/workflows/__next._index.txt +1 -1
  87. package/dist/admin/workflows/__next._tree.txt +1 -1
  88. package/dist/admin/workflows/index.html +1 -1
  89. package/dist/admin/workflows/index.txt +1 -1
  90. package/dist/cli.js +289 -26
  91. package/dist/docs/deployments.md +79 -0
  92. package/dist/docs/index.md +3 -0
  93. package/dist/docs/observability.md +2 -1
  94. package/dist/docs/previews.md +5 -0
  95. package/package.json +1 -1
  96. /package/dist/admin/_next/static/{7iJYom_N04p3jvXLOBn8G → NNVAA5krd_olwtUDgk5Bo}/_buildManifest.js +0 -0
  97. /package/dist/admin/_next/static/{7iJYom_N04p3jvXLOBn8G → NNVAA5krd_olwtUDgk5Bo}/_clientMiddlewareManifest.json +0 -0
  98. /package/dist/admin/_next/static/{7iJYom_N04p3jvXLOBn8G → NNVAA5krd_olwtUDgk5Bo}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -368079,8 +368079,13 @@ function toDeployment(response) {
368079
368079
  error: response.error,
368080
368080
  changes: response.changes,
368081
368081
  pendingActions: response.pendingActions,
368082
+ builds: response.builds,
368082
368083
  publicUrls: response.publicUrls,
368084
+ triggeredBy: response.triggeredBy,
368085
+ gitCommitSha: response.gitCommitSha,
368083
368086
  createdAt: response.createdAt,
368087
+ startedAt: response.startedAt,
368088
+ completedAt: response.completedAt,
368084
368089
  uploadUrl: response.uploadUrl,
368085
368090
  maxTarballBytes: response.maxTarballBytes
368086
368091
  };
@@ -374765,7 +374770,7 @@ function trackEvent(event, properties) {
374765
374770
  event,
374766
374771
  properties: {
374767
374772
  ...properties,
374768
- cli_version: "0.1.181",
374773
+ cli_version: "0.1.183",
374769
374774
  platform: process.platform,
374770
374775
  node_version: process.version,
374771
374776
  project_id: getProjectId()
@@ -376743,6 +376748,22 @@ import { render as render5, Text as Text7, Box as Box7, useApp as useApp3, useIn
376743
376748
  import Spinner4 from "ink-spinner";
376744
376749
  import * as fs23 from "fs";
376745
376750
  import * as path21 from "path";
376751
+
376752
+ // src/lib/deployments/format.ts
376753
+ function formatErrorCode(code) {
376754
+ const labels = {
376755
+ build_failed: "Build failed",
376756
+ pre_deploy_failed: "Pre-deploy hook failed",
376757
+ deploy_failed: "Service failed to deploy",
376758
+ health_check_failed: "Health check failed",
376759
+ post_deploy_failed: "Post-deploy hook failed",
376760
+ volume_reset_failed: "Volume reset failed",
376761
+ internal_error: "Internal error"
376762
+ };
376763
+ return labels[code] || code;
376764
+ }
376765
+
376766
+ // src/commands/deploy.tsx
376746
376767
  import { execFileSync as execFileSync2 } from "child_process";
376747
376768
  function getGitInfo(cwd) {
376748
376769
  try {
@@ -376944,17 +376965,6 @@ function getFailedBuild(pendingActions) {
376944
376965
  if (!pendingActions) return void 0;
376945
376966
  return pendingActions.find((a) => a.type === "build_failed");
376946
376967
  }
376947
- function formatErrorCode(code) {
376948
- const labels = {
376949
- pre_deploy_failed: "Pre-deploy hook failed",
376950
- deploy_failed: "Service failed to deploy",
376951
- health_check_failed: "Health check failed",
376952
- post_deploy_failed: "Post-deploy hook failed",
376953
- volume_reset_failed: "Volume reset failed",
376954
- internal_error: "Internal error"
376955
- };
376956
- return labels[code] || code;
376957
- }
376958
376968
  function StructuredError({ error }) {
376959
376969
  return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, { color: "red", bold: true }, formatErrorCode(error.code), ": ", error.message), error.resource && /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "Resource: ", error.resource), error.output && /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Output:"), /* @__PURE__ */ React7.createElement(Text7, null, error.output)));
376960
376970
  }
@@ -379147,7 +379157,7 @@ function compareVersions(a, b) {
379147
379157
  return 0;
379148
379158
  }
379149
379159
  async function checkForUpdate() {
379150
- const currentVersion = "0.1.181";
379160
+ const currentVersion = "0.1.183";
379151
379161
  const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
379152
379162
  if (!response.ok) {
379153
379163
  throw new Error(`Failed to check for updates: HTTP ${response.status}`);
@@ -379434,13 +379444,35 @@ function PreviewBlock({ env: env2 }) {
379434
379444
  const urls = managedUrls(env2.domains);
379435
379445
  return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, env2.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", env2.id, ")")), env2.prNumber !== null && /* @__PURE__ */ React12.createElement(Text12, null, " ", "PR #", env2.prNumber, env2.prTitle ? /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " \u2014 ", env2.prTitle) : null), urls.length > 0 && /* @__PURE__ */ React12.createElement(Text12, null, " ", "URL: ", urls.join(", ")), env2.expiresAt && /* @__PURE__ */ React12.createElement(Text12, null, " ", "Expires: ", /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, new Date(env2.expiresAt).toLocaleString())));
379436
379446
  }
379447
+ function PreviewConfigurationBlock({
379448
+ config,
379449
+ activePreviewCount,
379450
+ showDetails
379451
+ }) {
379452
+ const configured = config.github;
379453
+ const enabledConfigs = configured.filter((c) => c.prPreviewsEnabled);
379454
+ const labelGated = enabledConfigs.filter((c) => c.previewsRequireLabel);
379455
+ const githubSummary = enabledConfigs.length > 0 ? `enabled for ${enabledConfigs.length} environment${enabledConfigs.length === 1 ? "" : "s"}` : configured.length > 0 ? "configured but disabled" : "not configured";
379456
+ return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, "Preview configuration"), /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", marginLeft: 2 }, /* @__PURE__ */ React12.createElement(Text12, null, "PR previews: ", config.enabledForOrganization ? "available" : "unavailable", "; GitHub ", githubSummary, ";", " ", labelGated.length > 0 ? `requires ${config.requiredLabel}` : "no label required", "; active ", activePreviewCount), !showDetails && configured.length > 0 && /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " ", "Run `specific status --previews` for GitHub preview details."), showDetails && /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React12.createElement(Text12, null, "Required label: ", config.requiredLabel), configured.length === 0 ? /* @__PURE__ */ React12.createElement(Text12, null, "GitHub PR automation: not configured") : configured.map((githubConfig) => /* @__PURE__ */ React12.createElement(Box11, { key: githubConfig.environmentId, flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, githubConfig.environmentName), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", githubConfig.environmentId, ")")), /* @__PURE__ */ React12.createElement(Text12, null, " ", "PR previews: ", githubConfig.prPreviewsEnabled ? "enabled" : "disabled", "; ", "requires label: ", githubConfig.previewsRequireLabel ? "yes" : "no"), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Repository: ", githubConfig.repositoryFullName, /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", githubConfig.branch, ", ", githubConfig.hclPath, ")")))))));
379457
+ }
379437
379458
  function EnvironmentBlock({ env: env2 }) {
379438
- return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, env2.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", env2.id, ")")), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Status: ", /* @__PURE__ */ React12.createElement(Text12, { color: statusColor(env2.status), bold: true }, env2.status)), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Active deployment:", " ", env2.activeDeployment ? /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, env2.activeDeployment.id) : /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, "none")), env2.inProgressDeployment && /* @__PURE__ */ React12.createElement(Text12, null, " ", "In progress:", " ", /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, env2.inProgressDeployment.id), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", env2.inProgressDeployment.state, ")")), env2.domains && Object.keys(env2.domains).length > 0 && /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, " ", "Domains:"), Object.entries(env2.domains).map(([serviceName, urls]) => {
379459
+ const latest = env2.latestDeployment;
379460
+ const latestIsShownElsewhere = !latest || latest.id === env2.activeDeployment?.id || latest.id === env2.inProgressDeployment?.id;
379461
+ return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, env2.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", env2.id, ")")), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Status: ", /* @__PURE__ */ React12.createElement(Text12, { color: statusColor(env2.status), bold: true }, env2.status)), /* @__PURE__ */ React12.createElement(Text12, null, " ", "Active deployment:", " ", env2.activeDeployment ? /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, env2.activeDeployment.id) : /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, "none")), env2.inProgressDeployment && /* @__PURE__ */ React12.createElement(Text12, null, " ", "In progress:", " ", /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, env2.inProgressDeployment.id), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", env2.inProgressDeployment.state, ")")), !latestIsShownElsewhere && /* @__PURE__ */ React12.createElement(Text12, null, " ", "Last deployment:", " ", /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, latest.id), " ", /* @__PURE__ */ React12.createElement(
379462
+ Text12,
379463
+ {
379464
+ ...latest.state === "failed" ? { color: "red", bold: true } : {}
379465
+ },
379466
+ "(",
379467
+ latest.state,
379468
+ latest.triggeredBy ? ` via ${latest.triggeredBy}` : "",
379469
+ ")"
379470
+ )), !latestIsShownElsewhere && latest.state === "failed" && /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " ", "Run `specific deployment show ", latest.id, "` for details."), env2.domains && Object.keys(env2.domains).length > 0 && /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, null, " ", "Domains:"), Object.entries(env2.domains).map(([serviceName, urls]) => {
379439
379471
  const displayUrls = urls.custom.length > 0 ? urls.custom : urls.managed ? [urls.managed] : [];
379440
379472
  return /* @__PURE__ */ React12.createElement(Text12, { key: serviceName }, " - ", serviceName, displayUrls.length > 0 ? `: ${displayUrls.join(", ")}` : null);
379441
379473
  })));
379442
379474
  }
379443
- function StatusUI() {
379475
+ function StatusUI({ previews = false }) {
379444
379476
  const [state, setState] = useState11({ phase: "loading" });
379445
379477
  useEffect11(() => {
379446
379478
  (async () => {
@@ -379468,14 +379500,21 @@ function StatusUI() {
379468
379500
  return /* @__PURE__ */ React12.createElement(Text12, { color: "red" }, "Failed to get status: ", state.error);
379469
379501
  }
379470
379502
  const status = state.status;
379471
- return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, status.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", status.id, ")")), /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, "Environments"), status.environments.length === 0 ? /* @__PURE__ */ React12.createElement(Box11, { marginLeft: 2 }, /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, "No environments found.")) : /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", gap: 1, marginLeft: 2 }, status.environments.map((env2) => /* @__PURE__ */ React12.createElement(EnvironmentBlock, { key: env2.id, env: env2 })))), (status.previewEnvironments ?? []).length > 0 && /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, "Preview environments"), /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", gap: 1, marginLeft: 2 }, status.previewEnvironments.map((env2) => /* @__PURE__ */ React12.createElement(PreviewBlock, { key: env2.id, env: env2 })))));
379503
+ return /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React12.createElement(Text12, null, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, status.name), /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, " (", status.id, ")")), /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, "Environments"), status.environments.length === 0 ? /* @__PURE__ */ React12.createElement(Box11, { marginLeft: 2 }, /* @__PURE__ */ React12.createElement(Text12, { dimColor: true }, "No environments found.")) : /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", gap: 1, marginLeft: 2 }, status.environments.map((env2) => /* @__PURE__ */ React12.createElement(EnvironmentBlock, { key: env2.id, env: env2 })))), /* @__PURE__ */ React12.createElement(
379504
+ PreviewConfigurationBlock,
379505
+ {
379506
+ config: status.previewEnvironmentConfig,
379507
+ activePreviewCount: (status.previewEnvironments ?? []).length,
379508
+ showDetails: previews
379509
+ }
379510
+ ), (status.previewEnvironments ?? []).length > 0 && /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Text12, { bold: true }, "Preview environments"), /* @__PURE__ */ React12.createElement(Box11, { flexDirection: "column", gap: 1, marginLeft: 2 }, status.previewEnvironments.map((env2) => /* @__PURE__ */ React12.createElement(PreviewBlock, { key: env2.id, env: env2 })))));
379472
379511
  }
379473
- function statusCommand() {
379474
- render10(/* @__PURE__ */ React12.createElement(StatusUI, null));
379512
+ function statusCommand(options2 = {}) {
379513
+ render10(/* @__PURE__ */ React12.createElement(StatusUI, { previews: options2.previews ?? false }));
379475
379514
  }
379476
379515
 
379477
379516
  // src/lib/project/resolve-env.ts
379478
- async function resolveEnvironmentId(client2, projectId, envFlag) {
379517
+ async function resolveEnvironmentStatus(client2, projectId, envFlag) {
379479
379518
  const status = await client2.getProjectStatus(projectId);
379480
379519
  const environments = status.environments;
379481
379520
  const previewEnvironments = status.previewEnvironments ?? [];
@@ -379490,16 +379529,19 @@ async function resolveEnvironmentId(client2, projectId, envFlag) {
379490
379529
  `Environment "${envFlag}" not found (matched against name and ID). Available: ${available}`
379491
379530
  );
379492
379531
  }
379493
- return match.id;
379532
+ return match;
379494
379533
  }
379495
379534
  if (hasEnvironmentId()) {
379496
379535
  const savedId = readEnvironmentId();
379497
- if (environments.some((e) => e.id === savedId)) {
379498
- return savedId;
379536
+ const saved = environments.find(
379537
+ (e) => e.id === savedId
379538
+ );
379539
+ if (saved) {
379540
+ return saved;
379499
379541
  }
379500
379542
  }
379501
379543
  if (environments.length === 1) {
379502
- return environments[0].id;
379544
+ return environments[0];
379503
379545
  }
379504
379546
  if (environments.length === 0) {
379505
379547
  throw new Error("This project has no environments yet.");
@@ -379509,6 +379551,10 @@ async function resolveEnvironmentId(client2, projectId, envFlag) {
379509
379551
  `Project has multiple environments (${names}). Pass --environment <name|id>.`
379510
379552
  );
379511
379553
  }
379554
+ async function resolveEnvironmentId(client2, projectId, envFlag) {
379555
+ const match = await resolveEnvironmentStatus(client2, projectId, envFlag);
379556
+ return match.id;
379557
+ }
379512
379558
 
379513
379559
  // src/lib/query/render-table.ts
379514
379560
  var MAX_COL_WIDTH = 60;
@@ -379750,6 +379796,204 @@ function friendlyErrorMessage(raw) {
379750
379796
  return raw;
379751
379797
  }
379752
379798
 
379799
+ // src/commands/deployment.tsx
379800
+ var ANSI2 = {
379801
+ dim: "\x1B[2m",
379802
+ red: "\x1B[31m",
379803
+ green: "\x1B[32m",
379804
+ yellow: "\x1B[33m",
379805
+ bold: "\x1B[1m",
379806
+ reset: "\x1B[0m"
379807
+ };
379808
+ async function deploymentShowCommand(idArg, options2) {
379809
+ try {
379810
+ const format = resolveOutputFormat2(options2.format);
379811
+ const includeOutput = options2.output ?? false;
379812
+ const client2 = new SpecificClient({ tokenProvider: getValidAccessToken });
379813
+ let deploymentId = idArg;
379814
+ if (!deploymentId) {
379815
+ const projectId = readProjectId();
379816
+ const env2 = await resolveEnvironmentStatus(
379817
+ client2,
379818
+ projectId,
379819
+ options2.environment
379820
+ );
379821
+ const latest = env2.latestDeployment ?? env2.inProgressDeployment ?? env2.activeDeployment;
379822
+ if (!latest) {
379823
+ throw new Error(
379824
+ `No deployments found for environment "${env2.name}".`
379825
+ );
379826
+ }
379827
+ deploymentId = latest.id;
379828
+ }
379829
+ const deployment = await client2.getDeployment(deploymentId);
379830
+ if (format === "json") {
379831
+ const payload = includeOutput ? deployment : stripOutput(deployment);
379832
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
379833
+ return;
379834
+ }
379835
+ process.stdout.write(renderDeployment(deployment, includeOutput));
379836
+ } catch (err) {
379837
+ const message = err instanceof Error ? err.message : "Unknown error occurred";
379838
+ console.error(`Error: ${message}`);
379839
+ process.exit(1);
379840
+ }
379841
+ }
379842
+ function resolveOutputFormat2(format) {
379843
+ if (!format) {
379844
+ return isInteractive() ? "text" : "json";
379845
+ }
379846
+ if (format === "text" || format === "json") {
379847
+ return format;
379848
+ }
379849
+ throw new Error(`Invalid format "${format}". Expected one of: text, json.`);
379850
+ }
379851
+ function omitOutput(value) {
379852
+ const copy = { ...value };
379853
+ delete copy.output;
379854
+ return copy;
379855
+ }
379856
+ function stripOutput(deployment) {
379857
+ const stripped = { ...deployment };
379858
+ if (stripped.error) {
379859
+ stripped.error = omitOutput(stripped.error);
379860
+ }
379861
+ if (stripped.builds) {
379862
+ stripped.builds = stripped.builds.map(omitOutput);
379863
+ }
379864
+ if (stripped.pendingActions) {
379865
+ stripped.pendingActions = stripped.pendingActions.map(
379866
+ (a) => a.type === "build_failed" ? omitOutput(a) : a
379867
+ );
379868
+ }
379869
+ return stripped;
379870
+ }
379871
+ function stateColor(state) {
379872
+ switch (state) {
379873
+ case "active":
379874
+ case "completed":
379875
+ return ANSI2.green;
379876
+ case "failed":
379877
+ case "cancelled":
379878
+ return ANSI2.red;
379879
+ case "superseded":
379880
+ return ANSI2.dim;
379881
+ default:
379882
+ return ANSI2.yellow;
379883
+ }
379884
+ }
379885
+ function colored(color, text) {
379886
+ return `${color}${text}${ANSI2.reset}`;
379887
+ }
379888
+ function formatDuration(startIso, endIso) {
379889
+ const ms = new Date(endIso).getTime() - new Date(startIso).getTime();
379890
+ if (!Number.isFinite(ms) || ms < 0) return null;
379891
+ const totalSeconds = Math.round(ms / 1e3);
379892
+ const minutes = Math.floor(totalSeconds / 60);
379893
+ const seconds = totalSeconds % 60;
379894
+ return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
379895
+ }
379896
+ function formatBytes3(bytes) {
379897
+ if (bytes < 1024) return `${bytes} B`;
379898
+ return `${(bytes / 1024).toFixed(1)} KB`;
379899
+ }
379900
+ function renderDeployment(deployment, includeOutput) {
379901
+ const lines = [];
379902
+ lines.push(
379903
+ `${ANSI2.bold}Deployment ${deployment.id}${ANSI2.reset} ${ANSI2.dim}(${deployment.environment})${ANSI2.reset}`
379904
+ );
379905
+ lines.push(
379906
+ ` State: ${colored(stateColor(deployment.state), deployment.state)}`
379907
+ );
379908
+ if (deployment.stateMessage) {
379909
+ lines.push(` Message: ${deployment.stateMessage}`);
379910
+ }
379911
+ if (deployment.triggeredBy) {
379912
+ lines.push(` Triggered: ${deployment.triggeredBy}`);
379913
+ }
379914
+ if (deployment.gitCommitSha) {
379915
+ lines.push(` Commit: ${deployment.gitCommitSha}`);
379916
+ }
379917
+ lines.push(` Created: ${deployment.createdAt}`);
379918
+ if (deployment.completedAt) {
379919
+ const duration = deployment.startedAt ? formatDuration(deployment.startedAt, deployment.completedAt) : null;
379920
+ lines.push(
379921
+ ` Completed: ${deployment.completedAt}${duration ? ` ${ANSI2.dim}(took ${duration})${ANSI2.reset}` : ""}`
379922
+ );
379923
+ }
379924
+ const builds = deployment.builds ?? [];
379925
+ if (builds.length > 0) {
379926
+ lines.push("");
379927
+ lines.push(`${ANSI2.bold}Builds${ANSI2.reset}`);
379928
+ const nameWidth = Math.max(...builds.map((b) => b.name.length));
379929
+ for (const build of builds) {
379930
+ const marker = build.state === "failed" ? "\u2716" : build.state === "completed" ? "\u2714" : "\u2026";
379931
+ let line = ` ${colored(stateColor(build.state), marker)} ${build.name.padEnd(nameWidth)} ${colored(stateColor(build.state), build.state)}`;
379932
+ if (build.state === "failed" && build.error) {
379933
+ line += ` ${ANSI2.dim}\u2014 ${build.error}${ANSI2.reset}`;
379934
+ }
379935
+ lines.push(line);
379936
+ }
379937
+ }
379938
+ const pendingActions = deployment.pendingActions ?? [];
379939
+ const missingSecrets = pendingActions.flatMap(
379940
+ (a) => a.type === "missing_secrets" ? a.secrets : []
379941
+ );
379942
+ const missingConfigs = pendingActions.flatMap(
379943
+ (a) => a.type === "missing_configs" ? a.configs : []
379944
+ );
379945
+ if (missingSecrets.length > 0 || missingConfigs.length > 0) {
379946
+ lines.push("");
379947
+ lines.push(colored(ANSI2.yellow, `${ANSI2.bold}Action required${ANSI2.reset}`));
379948
+ if (missingSecrets.length > 0) {
379949
+ lines.push(` Missing secrets: ${missingSecrets.join(", ")}`);
379950
+ }
379951
+ if (missingConfigs.length > 0) {
379952
+ lines.push(` Missing configs: ${missingConfigs.join(", ")}`);
379953
+ }
379954
+ }
379955
+ if (deployment.error) {
379956
+ lines.push("");
379957
+ lines.push(
379958
+ colored(
379959
+ ANSI2.red,
379960
+ `${ANSI2.bold}${formatErrorCode(deployment.error.code)}:${ANSI2.reset}${ANSI2.red} ${deployment.error.message}`
379961
+ )
379962
+ );
379963
+ if (deployment.error.resource) {
379964
+ lines.push(`${ANSI2.dim}Resource: ${deployment.error.resource}${ANSI2.reset}`);
379965
+ }
379966
+ }
379967
+ const buildsWithOutput = builds.filter(
379968
+ (b) => typeof b.output === "string" && b.output.length > 0
379969
+ );
379970
+ const extraErrorOutput = deployment.error?.output && !buildsWithOutput.some((b) => b.output === deployment.error?.output) ? deployment.error.output : null;
379971
+ if (includeOutput) {
379972
+ const ordered = [...buildsWithOutput].sort(
379973
+ (a, b) => Number(b.state === "failed") - Number(a.state === "failed")
379974
+ );
379975
+ for (const build of ordered) {
379976
+ lines.push("");
379977
+ lines.push(`${ANSI2.bold}Output (${build.name})${ANSI2.reset}`);
379978
+ lines.push(build.output.trimEnd());
379979
+ }
379980
+ if (extraErrorOutput) {
379981
+ lines.push("");
379982
+ lines.push(`${ANSI2.bold}Output${ANSI2.reset}`);
379983
+ lines.push(extraErrorOutput.trimEnd());
379984
+ }
379985
+ } else {
379986
+ const totalBytes = buildsWithOutput.reduce((sum, b) => sum + b.output.length, 0) + (extraErrorOutput?.length ?? 0);
379987
+ if (totalBytes > 0) {
379988
+ lines.push("");
379989
+ lines.push(
379990
+ `${ANSI2.dim}Output hidden (${formatBytes3(totalBytes)}). Run \`specific deployment show ${deployment.id} --output\` to print it.${ANSI2.reset}`
379991
+ );
379992
+ }
379993
+ }
379994
+ return lines.join("\n") + "\n";
379995
+ }
379996
+
379753
379997
  // src/lib/domain/format-dns-instructions.ts
379754
379998
  function isApex(domain) {
379755
379999
  return domain.split(".").length === 2;
@@ -379830,7 +380074,7 @@ async function domainAddCommand(service, domain, options2) {
379830
380074
  var program = new Command();
379831
380075
  var env = "production";
379832
380076
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
379833
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.181").enablePositionalOptions();
380077
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.183").enablePositionalOptions();
379834
380078
  program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
379835
380079
  Examples:
379836
380080
  $ specific init
@@ -379912,9 +380156,28 @@ Examples:
379912
380156
  $ specific domain add web example.com --environment staging`).action((service, domain, options2) => {
379913
380157
  domainAddCommand(service, domain, options2);
379914
380158
  });
379915
- program.command("status").description("Show project, environments, and deployment status").addHelpText("after", `
380159
+ var deploymentCmd = program.command("deployment").description("Inspect deployments");
380160
+ deploymentCmd.command("show [id]").description(
380161
+ "Show a deployment's state, builds, and errors (defaults to the latest deployment)"
380162
+ ).option("-e, --environment <name|id>", "Target environment (defaults to the current one; ignored when an ID is given)").option(
380163
+ "--format <text|json>",
380164
+ "Output format (defaults to text on a TTY, json when piped)"
380165
+ ).option(
380166
+ "--output",
380167
+ "Include build output (logs); can be large, so it is hidden by default"
380168
+ ).addHelpText("after", `
380169
+ Examples:
380170
+ $ specific deployment show
380171
+ $ specific deployment show dep_0abc123
380172
+ $ specific deployment show dep_0abc123 --output
380173
+ $ specific deployment show --environment staging
380174
+ $ specific deployment show -e preview-a1b2c3d4 --format json`).action((id, options2) => {
380175
+ deploymentShowCommand(id, options2);
380176
+ });
380177
+ program.command("status").description("Show project, environments, and deployment status").option("--previews", "Show detailed preview environment and GitHub PR preview configuration").addHelpText("after", `
379916
380178
  Examples:
379917
- $ specific status`).action(statusCommand);
380179
+ $ specific status
380180
+ $ specific status --previews`).action((options2) => statusCommand(options2));
379918
380181
  program.command("query [sql]").description(
379919
380182
  "Run a SQL query against your environment's observability data (ClickHouse), or a Postgres database with --db"
379920
380183
  ).option("-e, --environment <name|id>", "Target environment (defaults to the current one)").option(
@@ -0,0 +1,79 @@
1
+ # Deployments
2
+
3
+ Every deploy — whether started with `specific deploy`, by a GitHub push, or from the dashboard — creates a deployment record with its state, the builds it ran, and any failure details. Use `specific deployment show` to inspect one, most importantly to answer "why did my deploy fail?" when you didn't run the deploy yourself.
4
+
5
+ ## Inspecting a deployment
6
+
7
+ ```sh
8
+ # The latest deployment of the current environment (whatever its state)
9
+ specific deployment show
10
+
11
+ # A specific deployment by ID
12
+ specific deployment show dep_0abc123
13
+
14
+ # The latest deployment of another environment (or a preview environment)
15
+ specific deployment show --environment staging
16
+ specific deployment show -e preview-a1b2c3d4
17
+
18
+ # Machine-readable output for agents and scripts
19
+ specific deployment show --format json
20
+
21
+ # Include the build output (logs) — hidden by default because it can be large
22
+ specific deployment show dep_0abc123 --output
23
+ ```
24
+
25
+ Flags:
26
+
27
+ - `-e, --environment <name|id>` — target environment when no ID is given (defaults to the current one). Run `specific status` to list every environment and its ID.
28
+ - `--format <text|json>` — output format. When stdout is a terminal, the default is `text`. When stdout is piped or redirected, the default is `json`.
29
+ - `--output` — include build output (logs) and the error's diagnostic output. Without this flag, output is omitted in **both** formats (a size hint is printed in text mode) so a state check never floods the terminal or an agent's context with logs.
30
+
31
+ The output includes:
32
+
33
+ - **State** — `pending`, `queued`, `deploying`, `active`, `failed`, `cancelled`, or `superseded`, plus the state message.
34
+ - **Trigger** — how the deploy was started (`cli`, `github`, …) and the git commit, when known.
35
+ - **Builds** — each image build with its state and, for failed builds, the error message. Add `--output` for the full **build output (logs)** — where compile errors, missing dependencies, and failing build commands show up.
36
+ - **Action required** — for pending deployments, the missing secrets and configs blocking it (see below).
37
+ - **Error** — a structured error for failed deployments (`build_failed`, `pre_deploy_failed`, `health_check_failed`, …) with the failing resource; add `--output` for its diagnostic output (pod logs, Kubernetes events).
38
+
39
+ The `text` format is for quick human inspection. Agents and scripts should use `json` to get complete, untruncated values.
40
+
41
+ ## Pending deployments
42
+
43
+ A deployment stays `pending` while builds run and until every required secret and config has a value. `specific deployment show` lists what is missing:
44
+
45
+ ```
46
+ Action required
47
+ Missing secrets: STRIPE_KEY
48
+ Missing configs: DOMAIN
49
+ ```
50
+
51
+ Values can be provided in the dashboard (the state message links the deployment page), or by deploying from the CLI with `specific deploy --secret KEY=value --config KEY=value`, which prompts for anything still missing. See `specific docs secrets-config`.
52
+
53
+ ## Finding the deployment to inspect
54
+
55
+ `specific status` shows, per environment, the active deployment, any in-progress deployment, and — when it differs — the **last** deployment. A failed deploy never becomes active, so after a failed GitHub push the environment still shows an older active deployment; the failure appears on the "Last deployment" line with its ID.
56
+
57
+ ```sh
58
+ specific status # find the deployment ID (active, in progress, or last/failed)
59
+ specific deployment show # or skip the lookup: shows the latest deployment directly
60
+ ```
61
+
62
+ ## Debugging a failed deploy
63
+
64
+ 1. Run `specific deployment show` (with `-e <env>` if it isn't the current environment) to see what failed.
65
+ 2. If a **build** failed: re-run with `--output` to print the build log with the exact command that failed. Fix the code or the `build` block in `specific.hcl` (see `specific docs builds`) and deploy again.
66
+ 3. If a **service** failed to deploy or its health check failed: re-run with `--output` for pod logs and Kubernetes events. For deeper investigation of runtime behaviour, query logs and metrics with `specific query` (see `specific docs observability`).
67
+
68
+ Build output is kept on the deployment record (the last portion of the log, which contains the failure). It is **not** part of the `observability.logs` stream — that stream contains runtime service and database logs only.
69
+
70
+ GitHub-triggered deploys behave exactly the same: the deployment record carries the build failure and its output, so `specific deployment show` works without access to GitHub.
71
+
72
+ ---
73
+
74
+ Related topics:
75
+
76
+ - Run `specific docs builds` for build configuration (`base`, `command`, custom Dockerfiles)
77
+ - Run `specific docs observability` for querying runtime logs and metrics of deployed services
78
+ - Run `specific docs previews` for pull-request preview environments
79
+ - Run `specific docs secrets-config` if a deploy is waiting for missing secrets or configs
@@ -12,6 +12,8 @@ This documentation is nested and references to other docs are in Markdown link f
12
12
 
13
13
  A full development environment can be started with `specific dev`. To deploy any changes, the user can run `specific deploy`.
14
14
 
15
+ If the project is connected to GitHub in the Specific Dashboard, pushes to the configured branch can deploy automatically. Run `specific status` to inspect the project and environment state. If a deploy fails — including GitHub-triggered deploys — run `specific deployment show` to see why, and add `--output` for the failed build's log.
16
+
15
17
  After deploying, the user can manage their project from the Specific Dashboard at https://dashboard.specific.dev — monitoring logs and metrics, scaling services, browsing the database and object storage, updating secrets and configuration, and deleting projects. Point them there whenever production management comes up.
16
18
 
17
19
  ## Topics
@@ -27,6 +29,7 @@ After deploying, the user can manage their project from the Specific Dashboard a
27
29
  - [Redis](/redis): define non-durable Redis-compatible databases for caching and more.
28
30
  - [Volumes](/volumes): define persistent storage volumes for services to store files in.
29
31
  - [Temporal](/temporal): managed durable workflow engine for background tasks, AI agents and more.
32
+ - [Deployments](/deployments): inspect deployments and debug failed deploys (including build output) with `specific deployment show`, regardless of whether the deploy was started from the CLI, GitHub, or the dashboard.
30
33
  - [Observability](/observability): query logs and metrics from running environments using `specific query` for debugging and analytics.
31
34
  - [Preview environments](/previews): ephemeral copies of an environment created per pull request, with the parent's databases branched, object storage forked, and volumes cloned.
32
35
  <!-- beta:mail -->
@@ -66,7 +66,7 @@ Column names follow OpenTelemetry's ClickHouse exporter conventions and are case
66
66
 
67
67
  ### `observability.logs`
68
68
 
69
- Unified log stream from services.
69
+ Unified log stream from services. This is **runtime** data: service and database logs from running containers. Build output from deploys is not part of this stream — it lives on the deployment record (run `specific deployment show`, see `specific docs deployments`).
70
70
 
71
71
  | Column | Type | Notes |
72
72
  |---|---|---|
@@ -227,3 +227,4 @@ Related topics:
227
227
  - Run `specific status` to list environments (standard and preview) and their IDs before querying a specific one
228
228
  - Run `specific docs services` for how services are defined
229
229
  - Run `specific docs postgres` for querying your application database (`specific psql` for local development, `specific query --db <name>` for production)
230
+ - Run `specific docs deployments` for debugging failed deploys — build output is on the deployment record (`specific deployment show`), not in `observability.logs`
@@ -9,6 +9,10 @@ A preview environment is a temporary, fully isolated copy of an existing environ
9
9
 
10
10
  Preview environments are ephemeral: pull-request previews are removed when the PR is closed or merged, and a preview can be given an expiry, after which Specific cleans it up automatically.
11
11
 
12
+ Pull-request previews depend on both the organization's plan and the GitHub environment configuration. Run `specific status` to see a concise preview summary, or `specific status --previews` to see whether preview environments are enabled, whether GitHub PR previews are configured, and whether PRs require a label.
13
+
14
+ Some projects require the `specific:preview` pull request label before Specific creates or updates a PR preview. When label gating is enabled, adding `specific:preview` starts the preview and removing it tears the preview down.
15
+
12
16
  Run `specific status` to list every environment and its `env_…` ID, including preview environments. Target a preview with `--environment` on commands that take one (see `specific docs observability`).
13
17
 
14
18
  ## What's copied from the parent
@@ -32,3 +36,4 @@ Related topics:
32
36
  - Run `specific docs volumes` for declaring persistent volumes
33
37
  - Run `specific docs services` for service configuration
34
38
  - Run `specific docs observability` for querying a preview's logs and metrics
39
+ - Run `specific docs deployments` for debugging a failed preview deploy (`specific deployment show -e <preview>` prints the failure; add `--output` for the build log)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/cli",
3
- "version": "0.1.181",
3
+ "version": "0.1.183",
4
4
  "description": "CLI for Specific infrastructure-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",