@specific.dev/cli 0.1.214 → 0.1.216

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 (95) 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 +404 -11
  91. package/dist/docs/index.md +1 -1
  92. package/package.json +1 -1
  93. /package/dist/admin/_next/static/{f9IM_eLtNump8X2RkRAQa → vKHnCqyQs0lrlJ1nJ4DOH}/_buildManifest.js +0 -0
  94. /package/dist/admin/_next/static/{f9IM_eLtNump8X2RkRAQa → vKHnCqyQs0lrlJ1nJ4DOH}/_clientMiddlewareManifest.json +0 -0
  95. /package/dist/admin/_next/static/{f9IM_eLtNump8X2RkRAQa → vKHnCqyQs0lrlJ1nJ4DOH}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -367991,6 +367991,39 @@ var ApiClient = class {
367991
367991
  }
367992
367992
  return response.json();
367993
367993
  }
367994
+ async getAgentRegistration() {
367995
+ const url = `${this.baseUrl}/agent-registration`;
367996
+ writeLog("api", `GET ${url}`);
367997
+ const response = await fetch(url, {
367998
+ headers: await this.authHeaders()
367999
+ });
368000
+ writeLog("api", `Response: ${response.status} ${response.statusText}`);
368001
+ if (!response.ok) {
368002
+ throw await readApiError(
368003
+ response,
368004
+ "Failed to get agent registration",
368005
+ `GET ${url}`
368006
+ );
368007
+ }
368008
+ return response.json();
368009
+ }
368010
+ async claimAgentRegistration() {
368011
+ const url = `${this.baseUrl}/agent-registration/claim`;
368012
+ writeLog("api", `POST ${url}`);
368013
+ const response = await fetch(url, {
368014
+ method: "POST",
368015
+ headers: await this.authHeaders()
368016
+ });
368017
+ writeLog("api", `Response: ${response.status} ${response.statusText}`);
368018
+ if (!response.ok) {
368019
+ throw await readApiError(
368020
+ response,
368021
+ "Failed to claim agent registration",
368022
+ `POST ${url}`
368023
+ );
368024
+ }
368025
+ return response.json();
368026
+ }
367994
368027
  async submitFeedback(args) {
367995
368028
  const url = `${this.baseUrl}/feedback`;
367996
368029
  writeLog("api", `POST ${url}`);
@@ -368081,6 +368114,13 @@ var SpecificClient = class {
368081
368114
  async getMe() {
368082
368115
  return await this.client.getMe();
368083
368116
  }
368117
+ // --- Agent registration (WorkOS auth.md) ---
368118
+ async getAgentRegistration() {
368119
+ return await this.client.getAgentRegistration();
368120
+ }
368121
+ async claimAgentRegistration() {
368122
+ return await this.client.claimAgentRegistration();
368123
+ }
368084
368124
  // --- Deployments ---
368085
368125
  async createDeployment(projectId, environment, options2) {
368086
368126
  return toDeployment(
@@ -374758,6 +374798,93 @@ async function refreshAccessToken(refreshToken) {
374758
374798
  }
374759
374799
  return response.json();
374760
374800
  }
374801
+ var WORKOS_AUTHKIT_DOMAIN = "auth.specific.dev";
374802
+ function authkitUrl(path28) {
374803
+ return `https://${WORKOS_AUTHKIT_DOMAIN}${path28}`;
374804
+ }
374805
+ var AgentAuthError = class extends Error {
374806
+ constructor(code, message) {
374807
+ super(message);
374808
+ this.code = code;
374809
+ this.name = "AgentAuthError";
374810
+ }
374811
+ };
374812
+ async function postAuthkitJson(path28, body, operation) {
374813
+ const response = await fetch(authkitUrl(path28), {
374814
+ method: "POST",
374815
+ headers: { "Content-Type": "application/json" },
374816
+ body: JSON.stringify(body)
374817
+ });
374818
+ const text = await response.text();
374819
+ let json = null;
374820
+ try {
374821
+ json = JSON.parse(text);
374822
+ } catch {
374823
+ }
374824
+ if (!response.ok) {
374825
+ const code = json?.code ?? json?.error ?? `http_${response.status}`;
374826
+ const message = json?.message ?? json?.error_description ?? text.slice(0, 200);
374827
+ throw new AgentAuthError(String(code), `${operation}: ${message}`);
374828
+ }
374829
+ return json;
374830
+ }
374831
+ async function registerAgentIdentity() {
374832
+ return postAuthkitJson("/agent/identity", { type: "anonymous" }, "Agent registration failed");
374833
+ }
374834
+ async function refreshAgentIdentity(refreshToken) {
374835
+ return postAuthkitJson(
374836
+ "/agent/identity",
374837
+ { type: "refresh", refresh_token: refreshToken },
374838
+ "Agent identity refresh failed"
374839
+ );
374840
+ }
374841
+ async function startAgentClaim(claimToken, email) {
374842
+ const res = await postAuthkitJson(
374843
+ "/agent/identity/claim",
374844
+ { type: "service_auth", claim_token: claimToken, login_hint: email },
374845
+ "Starting the claim failed"
374846
+ );
374847
+ const verificationUri = res.attempt?.verification_uri ?? res.claim?.attempt?.verification_uri;
374848
+ if (!verificationUri) {
374849
+ throw new AgentAuthError("invalid_response", "Claim attempt response had no verification_uri");
374850
+ }
374851
+ return { verificationUri };
374852
+ }
374853
+ async function completeAgentClaim(claimToken, userCode) {
374854
+ const res = await postAuthkitJson(
374855
+ "/agent/identity/claim/complete",
374856
+ { claim_token: claimToken, user_code: userCode },
374857
+ "Completing the claim failed"
374858
+ );
374859
+ if (!res.identity?.assertion) {
374860
+ throw new AgentAuthError("invalid_response", "Claim completion returned no identity");
374861
+ }
374862
+ return res;
374863
+ }
374864
+ async function exchangeAgentAssertion(assertion) {
374865
+ const response = await fetch(authkitUrl("/oauth2/token"), {
374866
+ method: "POST",
374867
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
374868
+ body: new URLSearchParams({
374869
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
374870
+ assertion
374871
+ })
374872
+ });
374873
+ const text = await response.text();
374874
+ let json = null;
374875
+ try {
374876
+ json = JSON.parse(text);
374877
+ } catch {
374878
+ }
374879
+ if (!response.ok || !json?.access_token) {
374880
+ const code = json?.error ?? `http_${response.status}`;
374881
+ throw new AgentAuthError(
374882
+ String(code),
374883
+ `Agent token exchange failed: ${json?.error_description ?? text.slice(0, 200)}`
374884
+ );
374885
+ }
374886
+ return { access_token: json.access_token, expires_in: Number(json.expires_in ?? 300) };
374887
+ }
374761
374888
 
374762
374889
  // src/lib/auth/login.tsx
374763
374890
  import React from "react";
@@ -374997,13 +375124,16 @@ async function fetchGithubActionsIdToken() {
374997
375124
  }
374998
375125
 
374999
375126
  // src/lib/auth/credentials.ts
375127
+ function isAgentCredentials(credentials) {
375128
+ return credentials?.kind === "agent";
375129
+ }
375000
375130
  function getUserCredentialsDir() {
375001
375131
  return path18.join(os5.homedir(), ".specific");
375002
375132
  }
375003
375133
  function getCredentialsPath() {
375004
375134
  return path18.join(getUserCredentialsDir(), "credentials.json");
375005
375135
  }
375006
- function readUserCredentials() {
375136
+ function readStoredCredentials() {
375007
375137
  const credentialsPath = getCredentialsPath();
375008
375138
  if (!fs20.existsSync(credentialsPath)) {
375009
375139
  return null;
@@ -375015,6 +375145,15 @@ function readUserCredentials() {
375015
375145
  return null;
375016
375146
  }
375017
375147
  }
375148
+ function readUserCredentials() {
375149
+ const credentials = readStoredCredentials();
375150
+ if (!credentials || isAgentCredentials(credentials)) return null;
375151
+ return credentials;
375152
+ }
375153
+ function readAgentCredentials() {
375154
+ const credentials = readStoredCredentials();
375155
+ return isAgentCredentials(credentials) ? credentials : null;
375156
+ }
375018
375157
  function writeUserCredentials(credentials) {
375019
375158
  const dir = getUserCredentialsDir();
375020
375159
  if (!fs20.existsSync(dir)) {
@@ -375035,12 +375174,72 @@ function getEnvApiKey() {
375035
375174
  return process.env.SPECIFIC_API_KEY || null;
375036
375175
  }
375037
375176
  function isLoggedIn() {
375038
- return getEnvApiKey() !== null || readUserCredentials() !== null;
375177
+ return getEnvApiKey() !== null || readStoredCredentials() !== null;
375039
375178
  }
375040
375179
  function getUserId() {
375041
- const credentials = readUserCredentials();
375180
+ const credentials = readStoredCredentials();
375042
375181
  return credentials?.userId ?? null;
375043
375182
  }
375183
+ var ACCESS_TOKEN_REFRESH_BUFFER_MS = 30 * 1e3;
375184
+ var AgentRegistrationExpiredError = class extends Error {
375185
+ constructor() {
375186
+ super(
375187
+ "This agent registration has expired. Run 'specific login --agent' to register again."
375188
+ );
375189
+ this.name = "AgentRegistrationExpiredError";
375190
+ }
375191
+ };
375192
+ function applyAgentIdentity(credentials, identity, extra = {}) {
375193
+ const updated = {
375194
+ ...credentials,
375195
+ ...extra,
375196
+ assertion: identity.assertion,
375197
+ assertionExpiresAt: Date.parse(identity.expires_at) || Date.now() + 60 * 60 * 1e3,
375198
+ refreshToken: identity.refresh_token?.value ?? credentials.refreshToken,
375199
+ accessToken: void 0,
375200
+ accessTokenExpiresAt: void 0
375201
+ };
375202
+ writeUserCredentials(updated);
375203
+ return updated;
375204
+ }
375205
+ async function getAgentAccessToken(credentials) {
375206
+ const now = Date.now();
375207
+ if (credentials.accessToken && credentials.accessTokenExpiresAt && credentials.accessTokenExpiresAt - ACCESS_TOKEN_REFRESH_BUFFER_MS > now) {
375208
+ return credentials.accessToken;
375209
+ }
375210
+ let current = credentials;
375211
+ const mint = async () => {
375212
+ const token = await exchangeAgentAssertion(current.assertion);
375213
+ const expiresAt = getTokenExpiresAt(token.access_token) ?? Date.now() + token.expires_in * 1e3;
375214
+ current = { ...current, accessToken: token.access_token, accessTokenExpiresAt: expiresAt };
375215
+ writeUserCredentials(current);
375216
+ return token.access_token;
375217
+ };
375218
+ const refresh = async () => {
375219
+ if (!current.refreshToken) {
375220
+ throw new AgentRegistrationExpiredError();
375221
+ }
375222
+ try {
375223
+ const refreshed = await refreshAgentIdentity(current.refreshToken);
375224
+ current = applyAgentIdentity(current, refreshed.identity);
375225
+ } catch (err) {
375226
+ if (err instanceof AgentAuthError) throw new AgentRegistrationExpiredError();
375227
+ throw err;
375228
+ }
375229
+ };
375230
+ if (current.assertionExpiresAt - ACCESS_TOKEN_REFRESH_BUFFER_MS <= now) {
375231
+ await refresh();
375232
+ }
375233
+ try {
375234
+ return await mint();
375235
+ } catch (err) {
375236
+ if (err instanceof AgentAuthError && err.code === "invalid_grant") {
375237
+ await refresh();
375238
+ return await mint();
375239
+ }
375240
+ throw err;
375241
+ }
375242
+ }
375044
375243
  async function saveCredentialsFromToken(token) {
375045
375244
  const client2 = new ApiClient({ token: token.access_token });
375046
375245
  const user = await client2.getMe();
@@ -375060,10 +375259,16 @@ async function getValidAccessToken() {
375060
375259
  if (isGithubActions()) {
375061
375260
  return await fetchGithubActionsIdToken();
375062
375261
  }
375063
- const credentials = readUserCredentials();
375064
- if (!credentials) {
375065
- throw new Error("Not logged in. Run 'specific login' first.");
375262
+ const stored = readStoredCredentials();
375263
+ if (!stored) {
375264
+ throw new Error(
375265
+ "Not logged in. Run 'specific login' (or 'specific login --agent' for an unattended agent) first."
375266
+ );
375267
+ }
375268
+ if (isAgentCredentials(stored)) {
375269
+ return await getAgentAccessToken(stored);
375066
375270
  }
375271
+ const credentials = stored;
375067
375272
  if (!credentials.userId) {
375068
375273
  const result = await performLogin({ isReauthentication: true });
375069
375274
  if (!result.success) {
@@ -375164,7 +375369,7 @@ function trackEvent(event, properties) {
375164
375369
  event,
375165
375370
  properties: {
375166
375371
  ...properties,
375167
- cli_version: "0.1.214",
375372
+ cli_version: "0.1.216",
375168
375373
  platform: process.platform,
375169
375374
  node_version: process.version,
375170
375375
  project_id: getProjectId()
@@ -379730,6 +379935,119 @@ function cleanCommand(instanceKey) {
379730
379935
  }
379731
379936
  }
379732
379937
 
379938
+ // src/lib/auth/agent-login.ts
379939
+ function formatDeadline(date) {
379940
+ return date.toISOString().replace(/\.\d{3}Z$/, "Z");
379941
+ }
379942
+ async function performAgentLogin() {
379943
+ writeLog("auth", "Registering anonymous agent identity with AuthKit");
379944
+ const registration = await registerAgentIdentity();
379945
+ writeLog("auth", `Agent registration received: ${registration.id}`);
379946
+ const base = {
379947
+ kind: "agent",
379948
+ registrationId: registration.id,
379949
+ assertion: registration.identity.assertion,
379950
+ assertionExpiresAt: Date.parse(registration.identity.expires_at) || Date.now() + 36e5,
379951
+ refreshToken: registration.identity.refresh_token?.value,
379952
+ claimToken: registration.claim?.token,
379953
+ claimTokenExpiresAt: registration.claim ? Date.parse(registration.claim.expires_at) : void 0,
379954
+ claimed: false,
379955
+ userId: ""
379956
+ };
379957
+ const accessToken = await getAgentAccessToken(base);
379958
+ const client2 = new SpecificClient({ accessToken });
379959
+ const me = await client2.getMe();
379960
+ const info = await client2.getAgentRegistration();
379961
+ writeUserCredentials({ ...readAgentCredentials() ?? base, userId: me.id });
379962
+ writeLog("auth", `Agent credentials written for user ${me.id}`);
379963
+ return { registrationId: registration.id, claimDeadlineAt: new Date(info.claimDeadlineAt) };
379964
+ }
379965
+ function printAgentLoginSummary(result) {
379966
+ console.log("Registered this CLI as an agent. No login needed to deploy.");
379967
+ console.log("");
379968
+ console.log("This account is UNCLAIMED:");
379969
+ console.log(" - it can hold one project on the free tier");
379970
+ console.log(` - it and everything deployed to it are deleted at ${formatDeadline(result.claimDeadlineAt)}`);
379971
+ console.log(" unless a person claims it before then");
379972
+ console.log("");
379973
+ console.log("To hand the account to its owner, run (with their email):");
379974
+ console.log(" specific claim --email <email>");
379975
+ }
379976
+ function requireAgentCredentials() {
379977
+ const stored = readStoredCredentials();
379978
+ if (!stored) {
379979
+ throw new Error("Not registered as an agent. Run 'specific login --agent' first.");
379980
+ }
379981
+ if (stored.kind !== "agent") {
379982
+ throw new Error(
379983
+ "Claiming only applies to agent registrations. This CLI is logged in as a person already."
379984
+ );
379985
+ }
379986
+ return stored;
379987
+ }
379988
+ async function startClaim(email) {
379989
+ const credentials = requireAgentCredentials();
379990
+ if (credentials.claimed) {
379991
+ const client2 = new SpecificClient({ tokenProvider: getValidAccessToken });
379992
+ const me = await client2.getMe();
379993
+ return { phase: "completed", email: me.email, alreadyClaimed: true };
379994
+ }
379995
+ if (credentials.claimVerified) {
379996
+ return await handOverToPlatform(credentials);
379997
+ }
379998
+ if (!credentials.claimToken) {
379999
+ throw new Error("This registration has no claim token; run 'specific login --agent' again.");
380000
+ }
380001
+ writeLog("auth", `Starting agent claim for ${email}`);
380002
+ const { verificationUri } = await startAgentClaim(credentials.claimToken, email);
380003
+ return { phase: "started", verificationUri };
380004
+ }
380005
+ async function completeClaim(userCode) {
380006
+ let credentials = requireAgentCredentials();
380007
+ if (credentials.claimed) {
380008
+ const client2 = new SpecificClient({ tokenProvider: getValidAccessToken });
380009
+ const me = await client2.getMe();
380010
+ return { phase: "completed", email: me.email, alreadyClaimed: true };
380011
+ }
380012
+ if (credentials.claimVerified) {
380013
+ return await handOverToPlatform(credentials);
380014
+ }
380015
+ if (!credentials.claimToken) {
380016
+ throw new Error("This registration has no claim token; run 'specific login --agent' again.");
380017
+ }
380018
+ writeLog("auth", "Completing agent claim");
380019
+ const verified = await completeAgentClaim(credentials.claimToken, userCode.trim());
380020
+ credentials = applyAgentIdentity(credentials, verified.identity, { claimVerified: true });
380021
+ return await handOverToPlatform(credentials);
380022
+ }
380023
+ async function handOverToPlatform(credentials) {
380024
+ const client2 = new SpecificClient({ tokenProvider: getValidAccessToken });
380025
+ const result = await client2.claimAgentRegistration();
380026
+ writeUserCredentials({ ...credentials, claimVerified: void 0, claimed: true, userId: result.user.id });
380027
+ writeLog("auth", `Agent registration claimed by ${result.user.email}`);
380028
+ return { phase: "completed", email: result.user.email, alreadyClaimed: false };
380029
+ }
380030
+ function describeClaimError(err) {
380031
+ if (err instanceof AgentAuthError) {
380032
+ switch (err.code) {
380033
+ case "claim_not_confirmed":
380034
+ return "The user hasn't finished on the claim page yet. Ask them to complete it, then retry.";
380035
+ case "invalid_user_code":
380036
+ return "That code was not accepted. Ask the user to read it again from the claim page.";
380037
+ case "user_code_expired":
380038
+ return "The code expired. Run 'specific claim --email <email>' again for a fresh link.";
380039
+ case "claim_expired":
380040
+ case "claim_revoked":
380041
+ case "already_claimed":
380042
+ case "invalid_claim_token":
380043
+ return `${err.message}. Run 'specific login --agent' to register again.`;
380044
+ default:
380045
+ return err.message;
380046
+ }
380047
+ }
380048
+ return err instanceof Error ? err.message : String(err);
380049
+ }
380050
+
379733
380051
  // src/commands/login.tsx
379734
380052
  async function loginCommand(options2 = {}) {
379735
380053
  if (getEnvApiKey()) {
@@ -379740,6 +380058,16 @@ async function loginCommand(options2 = {}) {
379740
380058
  console.log("Already logged in.");
379741
380059
  return;
379742
380060
  }
380061
+ if (options2.agent) {
380062
+ try {
380063
+ const result2 = await performAgentLogin();
380064
+ printAgentLoginSummary(result2);
380065
+ } catch (err) {
380066
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
380067
+ process.exitCode = 1;
380068
+ }
380069
+ return;
380070
+ }
379743
380071
  if (options2.deviceCode) {
379744
380072
  const result2 = await performDeviceCodeLogin(options2.deviceCode);
379745
380073
  if (!result2.success) {
@@ -379795,6 +380123,64 @@ function logoutCommand() {
379795
380123
  render7(/* @__PURE__ */ React9.createElement(LogoutUI, null));
379796
380124
  }
379797
380125
 
380126
+ // src/commands/claim.ts
380127
+ import { createInterface } from "readline";
380128
+ function printCompleted(outcome) {
380129
+ if (outcome.alreadyClaimed) {
380130
+ console.log(`This agent account is already claimed by ${outcome.email}.`);
380131
+ return;
380132
+ }
380133
+ console.log(`Claimed. This account and its projects now belong to ${outcome.email}.`);
380134
+ console.log("They can manage everything at https://dashboard.specific.dev.");
380135
+ console.log("This CLI keeps working as their agent.");
380136
+ }
380137
+ async function askForCode() {
380138
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
380139
+ try {
380140
+ return await new Promise((resolve10) => {
380141
+ rl.question("Enter the code shown on the claim page: ", (answer) => resolve10(answer));
380142
+ });
380143
+ } finally {
380144
+ rl.close();
380145
+ }
380146
+ }
380147
+ async function claimCommand(options2) {
380148
+ try {
380149
+ if (options2.code) {
380150
+ const outcome = await completeClaim(options2.code);
380151
+ if (outcome.phase === "completed") printCompleted(outcome);
380152
+ return;
380153
+ }
380154
+ if (!options2.email) {
380155
+ console.error(
380156
+ "Usage: specific claim --email <email> (start: prints a link for the user)\n specific claim --code <code> (finish: the code the user read off that page)"
380157
+ );
380158
+ process.exitCode = 1;
380159
+ return;
380160
+ }
380161
+ const started = await startClaim(options2.email);
380162
+ if (started.phase === "completed") {
380163
+ printCompleted(started);
380164
+ return;
380165
+ }
380166
+ console.log("Claim started. Give the user this link:");
380167
+ console.log(` ${started.verificationUri}`);
380168
+ console.log("");
380169
+ console.log("They sign in there and are shown a short code.");
380170
+ if (isInteractive()) {
380171
+ const code = await askForCode();
380172
+ const outcome = await completeClaim(code);
380173
+ if (outcome.phase === "completed") printCompleted(outcome);
380174
+ return;
380175
+ }
380176
+ console.log("Once they give you the code, run:");
380177
+ console.log(" specific claim --code <code>");
380178
+ } catch (err) {
380179
+ console.error(`Error: ${describeClaimError(err)}`);
380180
+ process.exitCode = 1;
380181
+ }
380182
+ }
380183
+
379798
380184
  // src/commands/beta.tsx
379799
380185
  import React10, { useState as useState9, useEffect as useEffect9 } from "react";
379800
380186
  import { render as render8, Text as Text10, Box as Box9, useInput as useInput5, useApp as useApp5 } from "ink";
@@ -379892,7 +380278,7 @@ function compareVersions(a, b) {
379892
380278
  return 0;
379893
380279
  }
379894
380280
  async function checkForUpdate() {
379895
- const currentVersion = "0.1.214";
380281
+ const currentVersion = "0.1.216";
379896
380282
  const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
379897
380283
  if (!response.ok) {
379898
380284
  throw new Error(`Failed to check for updates: HTTP ${response.status}`);
@@ -380926,7 +381312,7 @@ async function feedbackCommand(message) {
380926
381312
  await client2.submitFeedback({
380927
381313
  message: trimmed,
380928
381314
  projectId,
380929
- cliVersion: "0.1.214"
381315
+ cliVersion: "0.1.216"
380930
381316
  });
380931
381317
  trackEvent("feedback_submitted", { hasProject: !!projectId });
380932
381318
  console.log("Thanks! We will take a look at your feedback shortly.");
@@ -380945,7 +381331,7 @@ async function feedbackCommand(message) {
380945
381331
  var program = new Command();
380946
381332
  var env = "production";
380947
381333
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
380948
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.214").enablePositionalOptions();
381334
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.216").enablePositionalOptions();
380949
381335
  program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
380950
381336
  Examples:
380951
381337
  $ specific init
@@ -381098,7 +381484,14 @@ Examples:
381098
381484
  $ specific feedback "specific query has no way to export results to CSV"`).action((message) => feedbackCommand(message));
381099
381485
  program.command("beta").description("Manage beta feature flags").action(betaCommand);
381100
381486
  program.command("update").description("Update Specific CLI to the latest version").action(updateCommand);
381101
- program.command("login").description("Log in to Specific").option("--device-code <code>", "Complete login using a device code from a previous non-interactive login").action((options2) => loginCommand(options2));
381487
+ program.command("login").description("Log in to Specific").option("--device-code <code>", "Complete login using a device code from a previous non-interactive login").option("--agent", "Register this CLI as an agent (WorkOS agent registration) \u2014 no browser or human needed").addHelpText("after", `
381488
+ Examples:
381489
+ $ specific login
381490
+ $ specific login --agent # unattended: registers an agent account the user can claim later`).action((options2) => loginCommand(options2));
381491
+ program.command("claim").description("Hand an agent-registered account to its owner (WorkOS claim ceremony)").option("--email <email>", "The owner's email: starts the claim and prints a link for them").option("--code <code>", "The code the owner read off the claim page: completes the claim").addHelpText("after", `
381492
+ Examples:
381493
+ $ specific claim --email jane@example.com
381494
+ $ specific claim --code 123456`).action((options2) => claimCommand(options2));
381102
381495
  program.command("logout").description("Log out of Specific").action(logoutCommand);
381103
381496
  var commandName = process.argv[2] || "help";
381104
381497
  trackEvent("cli_command_invoked", { command: commandName });
@@ -20,7 +20,7 @@ After deploying, the user can manage their project from the Specific Dashboard a
20
20
 
21
21
  ## Authentication
22
22
 
23
- Commands that talk to Specific Cloud — deploying, `specific status`, `specific deployment show`, `specific query`, and other project and environment commands — require the user to be signed in. Interactively, run `specific login`; it opens a browser (or prints a device-authorization link) for the user to approve.
23
+ Commands that talk to Specific Cloud — deploying, `specific status`, `specific deployment show`, `specific query`, and other project and environment commands — require the user to be signed in. Interactively, run `specific login`; it opens a browser (or prints a device-authorization link) for the user to approve. With no user available, run `specific login --agent`: it registers the CLI as an agent with no human involved, giving it an unclaimed account that holds one free-tier project and is deleted after 48 hours unless claimed. Hand it over with `specific claim --email <email>` (prints a link for the user) followed by `specific claim --code <code>` (the code they see after signing in).
24
24
 
25
25
  In headless environments where nobody can complete the browser flow (CI, cloud coding agents), set the `SPECIFIC_API_KEY` environment variable instead. The user creates an API key in the dashboard at https://dashboard.specific.dev/user-settings under **API keys**; when the variable is set, every command authenticates with it directly and no login is needed. If a command reports you are not logged in and an interactive login is not possible, ask the user to create an API key there and add it to this environment as `SPECIFIC_API_KEY`.
26
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/cli",
3
- "version": "0.1.214",
3
+ "version": "0.1.216",
4
4
  "description": "CLI for Specific infrastructure-as-code",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",