@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.
- package/dist/admin/404/index.html +1 -1
- package/dist/admin/404.html +1 -1
- package/dist/admin/__next.!KGRlZmF1bHQp.__PAGE__.txt +1 -1
- package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/__next._full.txt +1 -1
- package/dist/admin/__next._head.txt +1 -1
- package/dist/admin/__next._index.txt +1 -1
- package/dist/admin/__next._tree.txt +1 -1
- package/dist/admin/_not-found/__next._full.txt +1 -1
- package/dist/admin/_not-found/__next._head.txt +1 -1
- package/dist/admin/_not-found/__next._index.txt +1 -1
- package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/dist/admin/_not-found/__next._not-found.txt +1 -1
- package/dist/admin/_not-found/__next._tree.txt +1 -1
- package/dist/admin/_not-found/index.html +1 -1
- package/dist/admin/_not-found/index.txt +1 -1
- package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.__PAGE__.txt +1 -1
- package/dist/admin/crons/__next.!KGRlZmF1bHQp.crons.txt +1 -1
- package/dist/admin/crons/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/crons/__next._full.txt +1 -1
- package/dist/admin/crons/__next._head.txt +1 -1
- package/dist/admin/crons/__next._index.txt +1 -1
- package/dist/admin/crons/__next._tree.txt +1 -1
- package/dist/admin/crons/index.html +1 -1
- package/dist/admin/crons/index.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
- package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/databases/__next._full.txt +1 -1
- package/dist/admin/databases/__next._head.txt +1 -1
- package/dist/admin/databases/__next._index.txt +1 -1
- package/dist/admin/databases/__next._tree.txt +1 -1
- package/dist/admin/databases/index.html +1 -1
- package/dist/admin/databases/index.txt +1 -1
- package/dist/admin/fullscreen/__next._full.txt +1 -1
- package/dist/admin/fullscreen/__next._head.txt +1 -1
- package/dist/admin/fullscreen/__next._index.txt +1 -1
- package/dist/admin/fullscreen/__next._tree.txt +1 -1
- package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +1 -1
- package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
- package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
- package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
- package/dist/admin/fullscreen/databases/index.html +1 -1
- package/dist/admin/fullscreen/databases/index.txt +1 -1
- package/dist/admin/fullscreen/index.html +1 -1
- package/dist/admin/fullscreen/index.txt +1 -1
- package/dist/admin/index.html +1 -1
- package/dist/admin/index.txt +1 -1
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
- package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/mail/__next._full.txt +1 -1
- package/dist/admin/mail/__next._head.txt +1 -1
- package/dist/admin/mail/__next._index.txt +1 -1
- package/dist/admin/mail/__next._tree.txt +1 -1
- package/dist/admin/mail/index.html +1 -1
- package/dist/admin/mail/index.txt +1 -1
- package/dist/admin/services/__next.!KGRlZmF1bHQp.services.__PAGE__.txt +1 -1
- package/dist/admin/services/__next.!KGRlZmF1bHQp.services.txt +1 -1
- package/dist/admin/services/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/services/__next._full.txt +1 -1
- package/dist/admin/services/__next._head.txt +1 -1
- package/dist/admin/services/__next._index.txt +1 -1
- package/dist/admin/services/__next._tree.txt +1 -1
- package/dist/admin/services/index.html +1 -1
- package/dist/admin/services/index.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.__PAGE__.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.storage.txt +1 -1
- package/dist/admin/storage/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/storage/__next._full.txt +1 -1
- package/dist/admin/storage/__next._head.txt +1 -1
- package/dist/admin/storage/__next._index.txt +1 -1
- package/dist/admin/storage/__next._tree.txt +1 -1
- package/dist/admin/storage/index.html +1 -1
- package/dist/admin/storage/index.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
- package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
- package/dist/admin/workflows/__next._full.txt +1 -1
- package/dist/admin/workflows/__next._head.txt +1 -1
- package/dist/admin/workflows/__next._index.txt +1 -1
- package/dist/admin/workflows/__next._tree.txt +1 -1
- package/dist/admin/workflows/index.html +1 -1
- package/dist/admin/workflows/index.txt +1 -1
- package/dist/cli.js +404 -11
- package/dist/docs/index.md +1 -1
- package/package.json +1 -1
- /package/dist/admin/_next/static/{f9IM_eLtNump8X2RkRAQa → vKHnCqyQs0lrlJ1nJ4DOH}/_buildManifest.js +0 -0
- /package/dist/admin/_next/static/{f9IM_eLtNump8X2RkRAQa → vKHnCqyQs0lrlJ1nJ4DOH}/_clientMiddlewareManifest.json +0 -0
- /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
|
|
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 ||
|
|
375177
|
+
return getEnvApiKey() !== null || readStoredCredentials() !== null;
|
|
375039
375178
|
}
|
|
375040
375179
|
function getUserId() {
|
|
375041
|
-
const credentials =
|
|
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
|
|
375064
|
-
if (!
|
|
375065
|
-
throw new Error(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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").
|
|
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 });
|
package/dist/docs/index.md
CHANGED
|
@@ -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
/package/dist/admin/_next/static/{f9IM_eLtNump8X2RkRAQa → vKHnCqyQs0lrlJ1nJ4DOH}/_buildManifest.js
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/admin/_next/static/{f9IM_eLtNump8X2RkRAQa → vKHnCqyQs0lrlJ1nJ4DOH}/_ssgManifest.js
RENAMED
|
File without changes
|