@specific.dev/cli 0.1.221 → 0.1.223

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 (94) 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 +325 -139
  91. package/package.json +1 -1
  92. /package/dist/admin/_next/static/{yV6Stb091ZxRiXDv0lNhI → MmCvvejntYmAQXZTK00Mr}/_buildManifest.js +0 -0
  93. /package/dist/admin/_next/static/{yV6Stb091ZxRiXDv0lNhI → MmCvvejntYmAQXZTK00Mr}/_clientMiddlewareManifest.json +0 -0
  94. /package/dist/admin/_next/static/{yV6Stb091ZxRiXDv0lNhI → MmCvvejntYmAQXZTK00Mr}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -367566,6 +367566,7 @@ function progressStreamFromBuffer(buffer, onProgress) {
367566
367566
  var FEEDBACK_NOT_ENABLED_CODE = "FEEDBACK_NOT_ENABLED";
367567
367567
  var CLI_DEPLOYMENTS_DISABLED_CODE = "CLI_DEPLOYMENTS_DISABLED";
367568
367568
  var PROJECT_PAUSED_CODE = "PROJECT_PAUSED";
367569
+ var QUOTA_EXCEEDED_CODE = "QUOTA_EXCEEDED";
367569
367570
  var ApiError = class extends Error {
367570
367571
  code;
367571
367572
  httpStatus;
@@ -374716,6 +374717,52 @@ function findEnvironmentByNameOrId(environments, flag) {
374716
374717
  return environments.find((e) => e.id === flag) ?? environments.find((e) => e.name === flag);
374717
374718
  }
374718
374719
 
374720
+ // src/lib/deployments/format.ts
374721
+ var DASHBOARD_URL = "https://dashboard.specific.dev";
374722
+ function dashboardBillingUrl(projectId) {
374723
+ return `${DASHBOARD_URL}/projects/${encodeURIComponent(projectId)}/organization-settings/billing`;
374724
+ }
374725
+ function dashboardDeploymentUrl(projectId, environmentId, deploymentId) {
374726
+ return `${DASHBOARD_URL}/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(environmentId)}/deployments/${encodeURIComponent(deploymentId)}`;
374727
+ }
374728
+ var PLAN_LIMIT_LABELS = {
374729
+ service: ["service", "services"],
374730
+ cron: ["cron", "crons"],
374731
+ postgres: ["postgres database", "postgres databases"],
374732
+ storage: ["storage bucket", "storage buckets"],
374733
+ redis: ["redis instance", "redis instances"],
374734
+ temporal: ["temporal namespace", "temporal namespaces"],
374735
+ mail: ["mail block", "mail blocks"]
374736
+ };
374737
+ function formatPlanLimitViolations(violations) {
374738
+ return violations.map((v) => {
374739
+ const [singular, plural] = PLAN_LIMIT_LABELS[v.resource] ?? [v.resource, v.resource];
374740
+ return `${v.declared} ${v.declared === 1 ? singular : plural} (limit ${v.limit})`;
374741
+ }).join(", ");
374742
+ }
374743
+ function formatErrorCode(code) {
374744
+ const labels = {
374745
+ build_failed: "Build failed",
374746
+ pre_deploy_failed: "Pre-deploy hook failed",
374747
+ deploy_failed: "Service failed to deploy",
374748
+ provisioning_failed: "Resource provisioning failed",
374749
+ health_check_failed: "Health check failed",
374750
+ post_deploy_failed: "Post-deploy hook failed",
374751
+ volume_reset_failed: "Volume reset failed",
374752
+ internal_error: "Internal error"
374753
+ };
374754
+ return labels[code] || code;
374755
+ }
374756
+
374757
+ // src/lib/project/quota.ts
374758
+ function formatProjectQuotaError(err) {
374759
+ if (!(err instanceof ApiError) || err.code !== QUOTA_EXCEEDED_CODE) return null;
374760
+ if (!err.apiError.includes("maxProjects")) return null;
374761
+ const limit = /limit:\s*(\d+)/.exec(err.apiError)?.[1];
374762
+ if (limit === void 0) return err.apiError;
374763
+ return `Your plan allows ${limit} project${limit === "1" ? "" : "s"}. Upgrade at ${DASHBOARD_URL} or delete a project.`;
374764
+ }
374765
+
374719
374766
  // src/lib/auth/freebuff.ts
374720
374767
  import { createHash as createHash2 } from "node:crypto";
374721
374768
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "node:fs";
@@ -375415,7 +375462,7 @@ function trackEvent(event, properties) {
375415
375462
  event,
375416
375463
  properties: {
375417
375464
  ...properties,
375418
- cli_version: "0.1.221",
375465
+ cli_version: "0.1.223",
375419
375466
  platform: process.platform,
375420
375467
  node_version: process.version,
375421
375468
  project_id: getProjectId()
@@ -377603,19 +377650,160 @@ import Spinner4 from "ink-spinner";
377603
377650
  import * as fs23 from "fs";
377604
377651
  import * as path21 from "path";
377605
377652
 
377606
- // src/lib/deployments/format.ts
377607
- function formatErrorCode(code) {
377608
- const labels = {
377609
- build_failed: "Build failed",
377610
- pre_deploy_failed: "Pre-deploy hook failed",
377611
- deploy_failed: "Service failed to deploy",
377612
- provisioning_failed: "Resource provisioning failed",
377613
- health_check_failed: "Health check failed",
377614
- post_deploy_failed: "Post-deploy hook failed",
377615
- volume_reset_failed: "Volume reset failed",
377616
- internal_error: "Internal error"
377653
+ // src/lib/auth/agent-login.ts
377654
+ function formatDeadline(date) {
377655
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
377656
+ }
377657
+ async function performAgentLogin() {
377658
+ writeLog("auth", "Registering anonymous agent identity with AuthKit");
377659
+ const registration = await registerAgentIdentity();
377660
+ writeLog("auth", `Agent registration received: ${registration.id}`);
377661
+ const base = {
377662
+ kind: "agent",
377663
+ registrationId: registration.id,
377664
+ assertion: registration.identity.assertion,
377665
+ assertionExpiresAt: Date.parse(registration.identity.expires_at) || Date.now() + 36e5,
377666
+ refreshToken: registration.identity.refresh_token?.value,
377667
+ claimToken: registration.claim?.token,
377668
+ claimTokenExpiresAt: registration.claim ? Date.parse(registration.claim.expires_at) : void 0,
377669
+ claimed: false,
377670
+ userId: ""
377617
377671
  };
377618
- return labels[code] || code;
377672
+ const accessToken = await getAgentAccessToken(base);
377673
+ const client2 = new SpecificClient({ accessToken, ...freebuffAttributionOptions() });
377674
+ const me = await client2.getMe();
377675
+ const info = await client2.getAgentRegistration();
377676
+ writeUserCredentials({ ...readAgentCredentials() ?? base, userId: me.id });
377677
+ writeLog("auth", `Agent credentials written for user ${me.id}`);
377678
+ return { registrationId: registration.id, claimDeadlineAt: new Date(info.claimDeadlineAt) };
377679
+ }
377680
+ function printAgentLoginSummary(result) {
377681
+ console.log("Registered this CLI as an agent. No login needed to deploy.");
377682
+ console.log("");
377683
+ printUnclaimedNotice(result.claimDeadlineAt);
377684
+ }
377685
+ function printUnclaimedNotice(claimDeadlineAt) {
377686
+ console.log("This account is UNCLAIMED:");
377687
+ console.log(" - it can hold one project on the free tier");
377688
+ console.log(
377689
+ claimDeadlineAt ? ` - it and everything deployed to it are deleted at ${formatDeadline(claimDeadlineAt)}` : " - it and everything deployed to it are deleted 48 hours after registration"
377690
+ );
377691
+ console.log(" unless a person claims it before then");
377692
+ console.log("");
377693
+ console.log("To hand the account to its owner, run (with their email):");
377694
+ console.log(" specific claim --email <email>");
377695
+ }
377696
+ function isExpiredRegistrationError(err) {
377697
+ return err instanceof AgentRegistrationExpiredError || err instanceof ApiError && err.code === "AGENT_REGISTRATION_EXPIRED";
377698
+ }
377699
+ async function describeAgentAccount(credentials) {
377700
+ const client2 = new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() });
377701
+ try {
377702
+ const info = await client2.getAgentRegistration();
377703
+ if (info.status === "expired") return { status: "expired" };
377704
+ if (info.status === "unclaimed") {
377705
+ return { status: "unclaimed", claimDeadlineAt: new Date(info.claimDeadlineAt) };
377706
+ }
377707
+ const me = await client2.getMe();
377708
+ const current = readAgentCredentials() ?? credentials;
377709
+ if (!current.claimed) {
377710
+ writeUserCredentials({ ...current, claimVerified: void 0, claimed: true, userId: me.id });
377711
+ }
377712
+ return { status: "claimed", email: me.email };
377713
+ } catch (err) {
377714
+ if (isExpiredRegistrationError(err)) return { status: "expired" };
377715
+ writeLog(
377716
+ "auth",
377717
+ `Agent registration lookup failed, using stored state: ${err instanceof Error ? err.message : String(err)}`
377718
+ );
377719
+ return credentials.claimed ? { status: "claimed", email: null } : { status: "unclaimed", claimDeadlineAt: null };
377720
+ }
377721
+ }
377722
+ async function printUnclaimedDeployReminder() {
377723
+ const credentials = readAgentCredentials();
377724
+ if (!credentials || credentials.claimed) return;
377725
+ const state = await describeAgentAccount(credentials);
377726
+ if (state.status !== "unclaimed") return;
377727
+ const when = state.claimDeadlineAt ? `at ${formatDeadline(state.claimDeadlineAt)}` : "when the claim window closes";
377728
+ console.log("");
377729
+ console.log(`This account is UNCLAIMED: it and this deployment are deleted ${when}`);
377730
+ console.log("unless a person claims it before then. To hand it over, run (with the owner's email):");
377731
+ console.log(" specific claim --email <email>");
377732
+ }
377733
+ function requireAgentCredentials() {
377734
+ const stored = readStoredCredentials();
377735
+ if (!stored) {
377736
+ throw new Error("Not registered as an agent. Run 'specific login --agent' first.");
377737
+ }
377738
+ if (stored.kind !== "agent") {
377739
+ throw new Error(
377740
+ "Claiming only applies to agent registrations. This CLI is logged in as a person already."
377741
+ );
377742
+ }
377743
+ return stored;
377744
+ }
377745
+ async function startClaim(email) {
377746
+ const credentials = requireAgentCredentials();
377747
+ if (credentials.claimed) {
377748
+ const client2 = new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() });
377749
+ const me = await client2.getMe();
377750
+ return { phase: "completed", email: me.email, alreadyClaimed: true };
377751
+ }
377752
+ if (credentials.claimVerified) {
377753
+ return await handOverToPlatform(credentials);
377754
+ }
377755
+ if (!credentials.claimToken) {
377756
+ throw new Error("This registration has no claim token; run 'specific login --agent' again.");
377757
+ }
377758
+ await new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() }).getAgentRegistration();
377759
+ writeLog("auth", `Starting agent claim for ${email}`);
377760
+ const { verificationUri } = await startAgentClaim(credentials.claimToken, email);
377761
+ return { phase: "started", verificationUri };
377762
+ }
377763
+ async function completeClaim(userCode) {
377764
+ let credentials = requireAgentCredentials();
377765
+ if (credentials.claimed) {
377766
+ const client2 = new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() });
377767
+ const me = await client2.getMe();
377768
+ return { phase: "completed", email: me.email, alreadyClaimed: true };
377769
+ }
377770
+ if (credentials.claimVerified) {
377771
+ return await handOverToPlatform(credentials);
377772
+ }
377773
+ if (!credentials.claimToken) {
377774
+ throw new Error("This registration has no claim token; run 'specific login --agent' again.");
377775
+ }
377776
+ writeLog("auth", "Completing agent claim");
377777
+ const verified = await completeAgentClaim(credentials.claimToken, userCode.trim());
377778
+ credentials = applyAgentIdentity(credentials, verified.identity, { claimVerified: true });
377779
+ return await handOverToPlatform(credentials);
377780
+ }
377781
+ async function handOverToPlatform(credentials) {
377782
+ const client2 = new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() });
377783
+ const result = await client2.claimAgentRegistration();
377784
+ writeUserCredentials({ ...credentials, claimVerified: void 0, claimed: true, userId: result.user.id });
377785
+ writeLog("auth", `Agent registration claimed by ${result.user.email}`);
377786
+ return { phase: "completed", email: result.user.email, alreadyClaimed: false };
377787
+ }
377788
+ function describeClaimError(err) {
377789
+ if (err instanceof AgentAuthError) {
377790
+ switch (err.code) {
377791
+ case "claim_not_confirmed":
377792
+ return "The user hasn't finished on the claim page yet. Ask them to complete it, then retry.";
377793
+ case "invalid_user_code":
377794
+ return "That code was not accepted. Ask the user to read it again from the claim page.";
377795
+ case "user_code_expired":
377796
+ return "The code expired. Run 'specific claim --email <email>' again for a fresh link.";
377797
+ case "claim_expired":
377798
+ case "claim_revoked":
377799
+ case "already_claimed":
377800
+ case "invalid_claim_token":
377801
+ return `${err.message}. Run 'specific login --agent' to register again.`;
377802
+ default:
377803
+ return err.message;
377804
+ }
377805
+ }
377806
+ return err instanceof Error ? err.message : String(err);
377619
377807
  }
377620
377808
 
377621
377809
  // src/lib/deployments/inputs.ts
@@ -377692,6 +377880,12 @@ function formatCliDeploymentsDisabledError(projectId, environmentId, detail) {
377692
377880
  "Once they have, run this command again."
377693
377881
  ].join("\n");
377694
377882
  }
377883
+ function formatPlanLimitExceededError(projectId, environmentId, deploymentId, action) {
377884
+ return `This deployment exceeds the ${action.planName} plan's limits: ${formatPlanLimitViolations(action.violations)}. Upgrade at ${dashboardBillingUrl(projectId)}, then start it from ${dashboardDeploymentUrl(projectId, environmentId, deploymentId)}, or remove the extra resources from specific.hcl and deploy again.`;
377885
+ }
377886
+ function formatUnknownPendingActionError(projectId, environmentId, deploymentId, type) {
377887
+ return `This deployment is waiting on "${type}", which this CLI version doesn't understand. Update the CLI or continue from ${dashboardDeploymentUrl(projectId, environmentId, deploymentId)}.`;
377888
+ }
377695
377889
  function formatBytes2(bytes) {
377696
377890
  if (bytes < 1024) return `${bytes} B`;
377697
377891
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -377870,6 +378064,23 @@ function getFailedBuild(pendingActions) {
377870
378064
  if (!pendingActions) return void 0;
377871
378065
  return pendingActions.find((a) => a.type === "build_failed");
377872
378066
  }
378067
+ function getPlanLimitAction(pendingActions) {
378068
+ if (!pendingActions) return void 0;
378069
+ return pendingActions.find(
378070
+ (a) => a.type === "plan_limit_exceeded"
378071
+ );
378072
+ }
378073
+ var KNOWN_PENDING_ACTION_TYPES = /* @__PURE__ */ new Set([
378074
+ "build_in_progress",
378075
+ "build_failed",
378076
+ "missing_secrets",
378077
+ "missing_configs",
378078
+ "plan_limit_exceeded"
378079
+ ]);
378080
+ function getUnknownPendingAction(pendingActions) {
378081
+ if (!pendingActions) return void 0;
378082
+ return pendingActions.find((a) => !KNOWN_PENDING_ACTION_TYPES.has(a.type));
378083
+ }
377873
378084
  function StructuredError({ error }) {
377874
378085
  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)));
377875
378086
  }
@@ -378033,7 +378244,7 @@ function DeployUI({ envFlag, preview, config }) {
378033
378244
  if (cancelled) return;
378034
378245
  setState({
378035
378246
  phase: "error",
378036
- error: err instanceof Error ? err.message : String(err)
378247
+ error: formatProjectQuotaError(err) ?? (err instanceof Error ? err.message : String(err))
378037
378248
  });
378038
378249
  }
378039
378250
  }
@@ -378523,6 +378734,24 @@ function DeployUI({ envFlag, preview, config }) {
378523
378734
  }));
378524
378735
  return;
378525
378736
  }
378737
+ const planLimit = getPlanLimitAction(status.pendingActions);
378738
+ if (planLimit) {
378739
+ if (pollInterval) clearInterval(pollInterval);
378740
+ const message = formatPlanLimitExceededError(
378741
+ state.projectId,
378742
+ state.environmentId,
378743
+ status.id,
378744
+ planLimit
378745
+ );
378746
+ writeLog("deploy:error", message);
378747
+ setState((s) => ({
378748
+ ...s,
378749
+ phase: "error",
378750
+ deployment: status,
378751
+ error: message
378752
+ }));
378753
+ return;
378754
+ }
378526
378755
  const missingSecrets2 = getMissingSecrets(status.pendingActions);
378527
378756
  if (missingSecrets2.length > 0) {
378528
378757
  if (pollInterval) clearInterval(pollInterval);
@@ -378565,6 +378794,24 @@ function DeployUI({ envFlag, preview, config }) {
378565
378794
  }));
378566
378795
  return;
378567
378796
  }
378797
+ const unknownAction = getUnknownPendingAction(status.pendingActions);
378798
+ if (unknownAction) {
378799
+ if (pollInterval) clearInterval(pollInterval);
378800
+ const message = formatUnknownPendingActionError(
378801
+ state.projectId,
378802
+ state.environmentId,
378803
+ status.id,
378804
+ unknownAction.type
378805
+ );
378806
+ writeLog("deploy:error", message);
378807
+ setState((s) => ({
378808
+ ...s,
378809
+ phase: "error",
378810
+ deployment: status,
378811
+ error: message
378812
+ }));
378813
+ return;
378814
+ }
378568
378815
  if (!status.pendingActions || status.pendingActions.length === 0) {
378569
378816
  if (pollInterval) clearInterval(pollInterval);
378570
378817
  pollInterval = void 0;
@@ -379106,6 +379353,13 @@ ${failedBuild.output}`);
379106
379353
  }
379107
379354
  process.exit(1);
379108
379355
  }
379356
+ const planLimit = getPlanLimitAction(status.pendingActions);
379357
+ if (planLimit) {
379358
+ console.error(
379359
+ `Error: ${formatPlanLimitExceededError(projectId, environmentId, deployment.id, planLimit)}`
379360
+ );
379361
+ process.exit(1);
379362
+ }
379109
379363
  const missingSecrets = getMissingSecrets(status.pendingActions);
379110
379364
  if (missingSecrets.length > 0) {
379111
379365
  const secretsToSubmit = {};
@@ -379151,6 +379405,13 @@ ${failedBuild.output}`);
379151
379405
  if (hasBuildsInProgress(status.pendingActions)) {
379152
379406
  continue;
379153
379407
  }
379408
+ const unknownAction = getUnknownPendingAction(status.pendingActions);
379409
+ if (unknownAction) {
379410
+ console.error(
379411
+ `Error: ${formatUnknownPendingActionError(projectId, environmentId, deployment.id, unknownAction.type)}`
379412
+ );
379413
+ process.exit(1);
379414
+ }
379154
379415
  if (!status.pendingActions || status.pendingActions.length === 0) {
379155
379416
  break;
379156
379417
  }
@@ -379222,6 +379483,7 @@ ${status.error.output}`);
379222
379483
  console.log(` ${name}: ${url}`);
379223
379484
  }
379224
379485
  }
379486
+ await printUnclaimedDeployReminder();
379225
379487
  }
379226
379488
  async function deployCommand(options2) {
379227
379489
  try {
@@ -380009,120 +380271,6 @@ function cleanCommand(instanceKey) {
380009
380271
  }
380010
380272
  }
380011
380273
 
380012
- // src/lib/auth/agent-login.ts
380013
- function formatDeadline(date) {
380014
- return date.toISOString().replace(/\.\d{3}Z$/, "Z");
380015
- }
380016
- async function performAgentLogin() {
380017
- writeLog("auth", "Registering anonymous agent identity with AuthKit");
380018
- const registration = await registerAgentIdentity();
380019
- writeLog("auth", `Agent registration received: ${registration.id}`);
380020
- const base = {
380021
- kind: "agent",
380022
- registrationId: registration.id,
380023
- assertion: registration.identity.assertion,
380024
- assertionExpiresAt: Date.parse(registration.identity.expires_at) || Date.now() + 36e5,
380025
- refreshToken: registration.identity.refresh_token?.value,
380026
- claimToken: registration.claim?.token,
380027
- claimTokenExpiresAt: registration.claim ? Date.parse(registration.claim.expires_at) : void 0,
380028
- claimed: false,
380029
- userId: ""
380030
- };
380031
- const accessToken = await getAgentAccessToken(base);
380032
- const client2 = new SpecificClient({ accessToken, ...freebuffAttributionOptions() });
380033
- const me = await client2.getMe();
380034
- const info = await client2.getAgentRegistration();
380035
- writeUserCredentials({ ...readAgentCredentials() ?? base, userId: me.id });
380036
- writeLog("auth", `Agent credentials written for user ${me.id}`);
380037
- return { registrationId: registration.id, claimDeadlineAt: new Date(info.claimDeadlineAt) };
380038
- }
380039
- function printAgentLoginSummary(result) {
380040
- console.log("Registered this CLI as an agent. No login needed to deploy.");
380041
- console.log("");
380042
- console.log("This account is UNCLAIMED:");
380043
- console.log(" - it can hold one project on the free tier");
380044
- console.log(` - it and everything deployed to it are deleted at ${formatDeadline(result.claimDeadlineAt)}`);
380045
- console.log(" unless a person claims it before then");
380046
- console.log("");
380047
- console.log("To hand the account to its owner, run (with their email):");
380048
- console.log(" specific claim --email <email>");
380049
- }
380050
- function requireAgentCredentials() {
380051
- const stored = readStoredCredentials();
380052
- if (!stored) {
380053
- throw new Error("Not registered as an agent. Run 'specific login --agent' first.");
380054
- }
380055
- if (stored.kind !== "agent") {
380056
- throw new Error(
380057
- "Claiming only applies to agent registrations. This CLI is logged in as a person already."
380058
- );
380059
- }
380060
- return stored;
380061
- }
380062
- async function startClaim(email) {
380063
- const credentials = requireAgentCredentials();
380064
- if (credentials.claimed) {
380065
- const client2 = new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() });
380066
- const me = await client2.getMe();
380067
- return { phase: "completed", email: me.email, alreadyClaimed: true };
380068
- }
380069
- if (credentials.claimVerified) {
380070
- return await handOverToPlatform(credentials);
380071
- }
380072
- if (!credentials.claimToken) {
380073
- throw new Error("This registration has no claim token; run 'specific login --agent' again.");
380074
- }
380075
- await new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() }).getAgentRegistration();
380076
- writeLog("auth", `Starting agent claim for ${email}`);
380077
- const { verificationUri } = await startAgentClaim(credentials.claimToken, email);
380078
- return { phase: "started", verificationUri };
380079
- }
380080
- async function completeClaim(userCode) {
380081
- let credentials = requireAgentCredentials();
380082
- if (credentials.claimed) {
380083
- const client2 = new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() });
380084
- const me = await client2.getMe();
380085
- return { phase: "completed", email: me.email, alreadyClaimed: true };
380086
- }
380087
- if (credentials.claimVerified) {
380088
- return await handOverToPlatform(credentials);
380089
- }
380090
- if (!credentials.claimToken) {
380091
- throw new Error("This registration has no claim token; run 'specific login --agent' again.");
380092
- }
380093
- writeLog("auth", "Completing agent claim");
380094
- const verified = await completeAgentClaim(credentials.claimToken, userCode.trim());
380095
- credentials = applyAgentIdentity(credentials, verified.identity, { claimVerified: true });
380096
- return await handOverToPlatform(credentials);
380097
- }
380098
- async function handOverToPlatform(credentials) {
380099
- const client2 = new SpecificClient({ tokenProvider: getValidAccessToken, ...freebuffAttributionOptions() });
380100
- const result = await client2.claimAgentRegistration();
380101
- writeUserCredentials({ ...credentials, claimVerified: void 0, claimed: true, userId: result.user.id });
380102
- writeLog("auth", `Agent registration claimed by ${result.user.email}`);
380103
- return { phase: "completed", email: result.user.email, alreadyClaimed: false };
380104
- }
380105
- function describeClaimError(err) {
380106
- if (err instanceof AgentAuthError) {
380107
- switch (err.code) {
380108
- case "claim_not_confirmed":
380109
- return "The user hasn't finished on the claim page yet. Ask them to complete it, then retry.";
380110
- case "invalid_user_code":
380111
- return "That code was not accepted. Ask the user to read it again from the claim page.";
380112
- case "user_code_expired":
380113
- return "The code expired. Run 'specific claim --email <email>' again for a fresh link.";
380114
- case "claim_expired":
380115
- case "claim_revoked":
380116
- case "already_claimed":
380117
- case "invalid_claim_token":
380118
- return `${err.message}. Run 'specific login --agent' to register again.`;
380119
- default:
380120
- return err.message;
380121
- }
380122
- }
380123
- return err instanceof Error ? err.message : String(err);
380124
- }
380125
-
380126
380274
  // src/commands/login.tsx
380127
380275
  async function loginCommand(options2 = {}) {
380128
380276
  if (getEnvApiKey()) {
@@ -380130,8 +380278,25 @@ async function loginCommand(options2 = {}) {
380130
380278
  return;
380131
380279
  }
380132
380280
  if (isLoggedIn()) {
380133
- console.log("Already logged in.");
380134
- return;
380281
+ const agent = readAgentCredentials();
380282
+ if (!agent) {
380283
+ console.log("Already logged in.");
380284
+ return;
380285
+ }
380286
+ const state = await describeAgentAccount(agent);
380287
+ if (state.status === "unclaimed") {
380288
+ console.log("Already registered as an agent. No login needed to deploy.");
380289
+ console.log("");
380290
+ printUnclaimedNotice(state.claimDeadlineAt);
380291
+ return;
380292
+ }
380293
+ if (state.status === "claimed") {
380294
+ console.log(
380295
+ state.email ? `Already logged in as an agent claimed by ${state.email}.` : "Already logged in as a claimed agent."
380296
+ );
380297
+ return;
380298
+ }
380299
+ console.log("The agent registration on this machine has expired.");
380135
380300
  }
380136
380301
  if (options2.agent) {
380137
380302
  try {
@@ -380353,7 +380518,7 @@ function compareVersions(a, b) {
380353
380518
  return 0;
380354
380519
  }
380355
380520
  async function checkForUpdate() {
380356
- const currentVersion = "0.1.221";
380521
+ const currentVersion = "0.1.223";
380357
380522
  const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
380358
380523
  if (!response.ok) {
380359
380524
  throw new Error(`Failed to check for updates: HTTP ${response.status}`);
@@ -380577,8 +380742,13 @@ async function projectNewCommand(name, options2) {
380577
380742
  const project = await client2.createProject(name, orgId);
380578
380743
  console.log(`Project created: ${project.id}`);
380579
380744
  } catch (error) {
380745
+ const quotaMessage = formatProjectQuotaError(error);
380746
+ if (quotaMessage) {
380747
+ console.error(quotaMessage);
380748
+ process.exit(1);
380749
+ }
380580
380750
  const message = error instanceof Error ? error.message : "Unknown error occurred";
380581
- console.error(`Failed to create project: ${message}`);
380751
+ console.error(`Error: ${message}`);
380582
380752
  process.exit(1);
380583
380753
  }
380584
380754
  }
@@ -381040,10 +381210,10 @@ async function deploymentShowCommand(idArg, options2) {
381040
381210
  const client2 = new SpecificClient({ tokenProvider: getValidAccessToken });
381041
381211
  let deploymentId = idArg;
381042
381212
  if (!deploymentId) {
381043
- const projectId = readProjectId();
381213
+ const projectId2 = readProjectId();
381044
381214
  const env2 = await resolveEnvironmentStatus(
381045
381215
  client2,
381046
- projectId,
381216
+ projectId2,
381047
381217
  options2.environment
381048
381218
  );
381049
381219
  const latest = env2.latestDeployment ?? env2.inProgressDeployment ?? env2.activeDeployment;
@@ -381060,7 +381230,12 @@ async function deploymentShowCommand(idArg, options2) {
381060
381230
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
381061
381231
  return;
381062
381232
  }
381063
- process.stdout.write(renderDeployment(deployment, includeOutput));
381233
+ let projectId = null;
381234
+ try {
381235
+ projectId = readProjectId();
381236
+ } catch {
381237
+ }
381238
+ process.stdout.write(renderDeployment(deployment, includeOutput, projectId));
381064
381239
  } catch (err) {
381065
381240
  const message = err instanceof Error ? err.message : "Unknown error occurred";
381066
381241
  console.error(`Error: ${message}`);
@@ -381159,7 +381334,7 @@ function formatBytes3(bytes) {
381159
381334
  if (bytes < 1024) return `${bytes} B`;
381160
381335
  return `${(bytes / 1024).toFixed(1)} KB`;
381161
381336
  }
381162
- function renderDeployment(deployment, includeOutput) {
381337
+ function renderDeployment(deployment, includeOutput, projectId) {
381163
381338
  const lines = [];
381164
381339
  lines.push(
381165
381340
  `${ANSI2.bold}Deployment ${deployment.id}${ANSI2.reset} ${ANSI2.dim}(${deployment.environment})${ANSI2.reset}`
@@ -381223,15 +381398,26 @@ function renderDeployment(deployment, includeOutput) {
381223
381398
  }
381224
381399
  }
381225
381400
  const pendingActions = deployment.pendingActions ?? [];
381401
+ const planLimit = pendingActions.find(
381402
+ (a) => a.type === "plan_limit_exceeded"
381403
+ );
381226
381404
  const missingSecrets = pendingActions.flatMap(
381227
381405
  (a) => a.type === "missing_secrets" ? a.secrets : []
381228
381406
  );
381229
381407
  const missingConfigs = pendingActions.flatMap(
381230
381408
  (a) => a.type === "missing_configs" ? a.configs : []
381231
381409
  );
381232
- if (missingSecrets.length > 0 || missingConfigs.length > 0) {
381410
+ if (planLimit || missingSecrets.length > 0 || missingConfigs.length > 0) {
381233
381411
  lines.push("");
381234
381412
  lines.push(colored(ANSI2.yellow, `${ANSI2.bold}Action required${ANSI2.reset}`));
381413
+ if (planLimit) {
381414
+ lines.push(
381415
+ ` This deployment exceeds the ${planLimit.planName} plan's limits: ${formatPlanLimitViolations(planLimit.violations)}.`
381416
+ );
381417
+ lines.push(
381418
+ ` Upgrade at ${projectId ? dashboardBillingUrl(projectId) : DASHBOARD_URL}, or remove the extra resources from specific.hcl and deploy again.`
381419
+ );
381420
+ }
381235
381421
  if (missingSecrets.length > 0) {
381236
381422
  lines.push(` Missing secrets: ${missingSecrets.join(", ")}`);
381237
381423
  }
@@ -381389,7 +381575,7 @@ async function feedbackCommand(message) {
381389
381575
  await client2.submitFeedback({
381390
381576
  message: trimmed,
381391
381577
  projectId,
381392
- cliVersion: "0.1.221"
381578
+ cliVersion: "0.1.223"
381393
381579
  });
381394
381580
  trackEvent("feedback_submitted", { hasProject: !!projectId });
381395
381581
  console.log("Thanks! We will take a look at your feedback shortly.");
@@ -381408,7 +381594,7 @@ async function feedbackCommand(message) {
381408
381594
  var program = new Command();
381409
381595
  var env = "production";
381410
381596
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
381411
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.221").enablePositionalOptions();
381597
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.223").enablePositionalOptions();
381412
381598
  program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
381413
381599
  Examples:
381414
381600
  $ specific init
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/cli",
3
- "version": "0.1.221",
3
+ "version": "0.1.223",
4
4
  "description": "CLI for Specific infrastructure-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",