@specific.dev/cli 0.1.213 → 0.1.215
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 +406 -11
- package/dist/docs/index.md +1 -1
- package/package.json +1 -1
- /package/dist/admin/_next/static/{OaJNk85sbbCne3x8W7_JZ → iWFTGcbsQ24RGzC0LvjEN}/_buildManifest.js +0 -0
- /package/dist/admin/_next/static/{OaJNk85sbbCne3x8W7_JZ → iWFTGcbsQ24RGzC0LvjEN}/_clientMiddlewareManifest.json +0 -0
- /package/dist/admin/_next/static/{OaJNk85sbbCne3x8W7_JZ → iWFTGcbsQ24RGzC0LvjEN}/_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(
|
|
@@ -372408,6 +372448,17 @@ function createReshapeWatcher(options2) {
|
|
|
372408
372448
|
log(`Reshape initialization complete`);
|
|
372409
372449
|
return searchPath2;
|
|
372410
372450
|
}
|
|
372451
|
+
if (status?.status === "idle" && status.latest_completed_migration === lastMigrationName) {
|
|
372452
|
+
log(`All migrations through "${lastMigrationName}" are already completed, nothing to start`);
|
|
372453
|
+
clearPersistedState(stateFilePath);
|
|
372454
|
+
for (const file of currentMigrationFiles) {
|
|
372455
|
+
makeReadOnly(path11.join(migrationsDir, file), log);
|
|
372456
|
+
}
|
|
372457
|
+
const searchPath2 = getSchemaName(lastMigration);
|
|
372458
|
+
log(`Search path (schema name): ${searchPath2}`);
|
|
372459
|
+
log(`Reshape initialization complete`);
|
|
372460
|
+
return searchPath2;
|
|
372461
|
+
}
|
|
372411
372462
|
if (status?.status === "in_progress") {
|
|
372412
372463
|
log(`In-progress migration differs from on-disk file, aborting before re-initialization...`);
|
|
372413
372464
|
runReshape(["abort"], databaseUrl, migrationsDir, reshapeBinaryPath, log);
|
|
@@ -374747,6 +374798,93 @@ async function refreshAccessToken(refreshToken) {
|
|
|
374747
374798
|
}
|
|
374748
374799
|
return response.json();
|
|
374749
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
|
+
}
|
|
374750
374888
|
|
|
374751
374889
|
// src/lib/auth/login.tsx
|
|
374752
374890
|
import React from "react";
|
|
@@ -374986,13 +375124,16 @@ async function fetchGithubActionsIdToken() {
|
|
|
374986
375124
|
}
|
|
374987
375125
|
|
|
374988
375126
|
// src/lib/auth/credentials.ts
|
|
375127
|
+
function isAgentCredentials(credentials) {
|
|
375128
|
+
return credentials?.kind === "agent";
|
|
375129
|
+
}
|
|
374989
375130
|
function getUserCredentialsDir() {
|
|
374990
375131
|
return path18.join(os5.homedir(), ".specific");
|
|
374991
375132
|
}
|
|
374992
375133
|
function getCredentialsPath() {
|
|
374993
375134
|
return path18.join(getUserCredentialsDir(), "credentials.json");
|
|
374994
375135
|
}
|
|
374995
|
-
function
|
|
375136
|
+
function readStoredCredentials() {
|
|
374996
375137
|
const credentialsPath = getCredentialsPath();
|
|
374997
375138
|
if (!fs20.existsSync(credentialsPath)) {
|
|
374998
375139
|
return null;
|
|
@@ -375004,6 +375145,15 @@ function readUserCredentials() {
|
|
|
375004
375145
|
return null;
|
|
375005
375146
|
}
|
|
375006
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
|
+
}
|
|
375007
375157
|
function writeUserCredentials(credentials) {
|
|
375008
375158
|
const dir = getUserCredentialsDir();
|
|
375009
375159
|
if (!fs20.existsSync(dir)) {
|
|
@@ -375024,12 +375174,72 @@ function getEnvApiKey() {
|
|
|
375024
375174
|
return process.env.SPECIFIC_API_KEY || null;
|
|
375025
375175
|
}
|
|
375026
375176
|
function isLoggedIn() {
|
|
375027
|
-
return getEnvApiKey() !== null ||
|
|
375177
|
+
return getEnvApiKey() !== null || readStoredCredentials() !== null;
|
|
375028
375178
|
}
|
|
375029
375179
|
function getUserId() {
|
|
375030
|
-
const credentials =
|
|
375180
|
+
const credentials = readStoredCredentials();
|
|
375031
375181
|
return credentials?.userId ?? null;
|
|
375032
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
|
+
}
|
|
375033
375243
|
async function saveCredentialsFromToken(token) {
|
|
375034
375244
|
const client2 = new ApiClient({ token: token.access_token });
|
|
375035
375245
|
const user = await client2.getMe();
|
|
@@ -375049,10 +375259,16 @@ async function getValidAccessToken() {
|
|
|
375049
375259
|
if (isGithubActions()) {
|
|
375050
375260
|
return await fetchGithubActionsIdToken();
|
|
375051
375261
|
}
|
|
375052
|
-
const
|
|
375053
|
-
if (!
|
|
375054
|
-
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);
|
|
375055
375270
|
}
|
|
375271
|
+
const credentials = stored;
|
|
375056
375272
|
if (!credentials.userId) {
|
|
375057
375273
|
const result = await performLogin({ isReauthentication: true });
|
|
375058
375274
|
if (!result.success) {
|
|
@@ -375153,7 +375369,7 @@ function trackEvent(event, properties) {
|
|
|
375153
375369
|
event,
|
|
375154
375370
|
properties: {
|
|
375155
375371
|
...properties,
|
|
375156
|
-
cli_version: "0.1.
|
|
375372
|
+
cli_version: "0.1.215",
|
|
375157
375373
|
platform: process.platform,
|
|
375158
375374
|
node_version: process.version,
|
|
375159
375375
|
project_id: getProjectId()
|
|
@@ -379719,6 +379935,110 @@ function cleanCommand(instanceKey) {
|
|
|
379719
379935
|
}
|
|
379720
379936
|
}
|
|
379721
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.claimToken) {
|
|
379996
|
+
throw new Error("This registration has no claim token; run 'specific login --agent' again.");
|
|
379997
|
+
}
|
|
379998
|
+
writeLog("auth", `Starting agent claim for ${email}`);
|
|
379999
|
+
const { verificationUri } = await startAgentClaim(credentials.claimToken, email);
|
|
380000
|
+
return { phase: "started", verificationUri };
|
|
380001
|
+
}
|
|
380002
|
+
async function completeClaim(userCode) {
|
|
380003
|
+
let credentials = requireAgentCredentials();
|
|
380004
|
+
if (credentials.claimed) {
|
|
380005
|
+
const client3 = new SpecificClient({ tokenProvider: getValidAccessToken });
|
|
380006
|
+
const me = await client3.getMe();
|
|
380007
|
+
return { phase: "completed", email: me.email, alreadyClaimed: true };
|
|
380008
|
+
}
|
|
380009
|
+
if (!credentials.claimToken) {
|
|
380010
|
+
throw new Error("This registration has no claim token; run 'specific login --agent' again.");
|
|
380011
|
+
}
|
|
380012
|
+
writeLog("auth", "Completing agent claim");
|
|
380013
|
+
const verified = await completeAgentClaim(credentials.claimToken, userCode.trim());
|
|
380014
|
+
credentials = applyAgentIdentity(credentials, verified.identity);
|
|
380015
|
+
const client2 = new SpecificClient({ tokenProvider: getValidAccessToken });
|
|
380016
|
+
const result = await client2.claimAgentRegistration();
|
|
380017
|
+
writeUserCredentials({ ...credentials, claimed: true, userId: result.user.id });
|
|
380018
|
+
writeLog("auth", `Agent registration claimed by ${result.user.email}`);
|
|
380019
|
+
return { phase: "completed", email: result.user.email, alreadyClaimed: false };
|
|
380020
|
+
}
|
|
380021
|
+
function describeClaimError(err) {
|
|
380022
|
+
if (err instanceof AgentAuthError) {
|
|
380023
|
+
switch (err.code) {
|
|
380024
|
+
case "claim_not_confirmed":
|
|
380025
|
+
return "The user hasn't finished on the claim page yet. Ask them to complete it, then retry.";
|
|
380026
|
+
case "invalid_user_code":
|
|
380027
|
+
return "That code was not accepted. Ask the user to read it again from the claim page.";
|
|
380028
|
+
case "user_code_expired":
|
|
380029
|
+
return "The code expired. Run 'specific claim --email <email>' again for a fresh link.";
|
|
380030
|
+
case "claim_expired":
|
|
380031
|
+
case "claim_revoked":
|
|
380032
|
+
case "already_claimed":
|
|
380033
|
+
case "invalid_claim_token":
|
|
380034
|
+
return `${err.message}. Run 'specific login --agent' to register again.`;
|
|
380035
|
+
default:
|
|
380036
|
+
return err.message;
|
|
380037
|
+
}
|
|
380038
|
+
}
|
|
380039
|
+
return err instanceof Error ? err.message : String(err);
|
|
380040
|
+
}
|
|
380041
|
+
|
|
379722
380042
|
// src/commands/login.tsx
|
|
379723
380043
|
async function loginCommand(options2 = {}) {
|
|
379724
380044
|
if (getEnvApiKey()) {
|
|
@@ -379729,6 +380049,16 @@ async function loginCommand(options2 = {}) {
|
|
|
379729
380049
|
console.log("Already logged in.");
|
|
379730
380050
|
return;
|
|
379731
380051
|
}
|
|
380052
|
+
if (options2.agent) {
|
|
380053
|
+
try {
|
|
380054
|
+
const result2 = await performAgentLogin();
|
|
380055
|
+
printAgentLoginSummary(result2);
|
|
380056
|
+
} catch (err) {
|
|
380057
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
380058
|
+
process.exitCode = 1;
|
|
380059
|
+
}
|
|
380060
|
+
return;
|
|
380061
|
+
}
|
|
379732
380062
|
if (options2.deviceCode) {
|
|
379733
380063
|
const result2 = await performDeviceCodeLogin(options2.deviceCode);
|
|
379734
380064
|
if (!result2.success) {
|
|
@@ -379784,6 +380114,64 @@ function logoutCommand() {
|
|
|
379784
380114
|
render7(/* @__PURE__ */ React9.createElement(LogoutUI, null));
|
|
379785
380115
|
}
|
|
379786
380116
|
|
|
380117
|
+
// src/commands/claim.ts
|
|
380118
|
+
import { createInterface } from "readline";
|
|
380119
|
+
function printCompleted(outcome) {
|
|
380120
|
+
if (outcome.alreadyClaimed) {
|
|
380121
|
+
console.log(`This agent account is already claimed by ${outcome.email}.`);
|
|
380122
|
+
return;
|
|
380123
|
+
}
|
|
380124
|
+
console.log(`Claimed. This account and its projects now belong to ${outcome.email}.`);
|
|
380125
|
+
console.log("They can manage everything at https://dashboard.specific.dev.");
|
|
380126
|
+
console.log("This CLI keeps working as their agent.");
|
|
380127
|
+
}
|
|
380128
|
+
async function askForCode() {
|
|
380129
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
380130
|
+
try {
|
|
380131
|
+
return await new Promise((resolve10) => {
|
|
380132
|
+
rl.question("Enter the code shown on the claim page: ", (answer) => resolve10(answer));
|
|
380133
|
+
});
|
|
380134
|
+
} finally {
|
|
380135
|
+
rl.close();
|
|
380136
|
+
}
|
|
380137
|
+
}
|
|
380138
|
+
async function claimCommand(options2) {
|
|
380139
|
+
try {
|
|
380140
|
+
if (options2.code) {
|
|
380141
|
+
const outcome = await completeClaim(options2.code);
|
|
380142
|
+
if (outcome.phase === "completed") printCompleted(outcome);
|
|
380143
|
+
return;
|
|
380144
|
+
}
|
|
380145
|
+
if (!options2.email) {
|
|
380146
|
+
console.error(
|
|
380147
|
+
"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)"
|
|
380148
|
+
);
|
|
380149
|
+
process.exitCode = 1;
|
|
380150
|
+
return;
|
|
380151
|
+
}
|
|
380152
|
+
const started = await startClaim(options2.email);
|
|
380153
|
+
if (started.phase === "completed") {
|
|
380154
|
+
printCompleted(started);
|
|
380155
|
+
return;
|
|
380156
|
+
}
|
|
380157
|
+
console.log("Claim started. Give the user this link:");
|
|
380158
|
+
console.log(` ${started.verificationUri}`);
|
|
380159
|
+
console.log("");
|
|
380160
|
+
console.log("They sign in there and are shown a short code.");
|
|
380161
|
+
if (isInteractive()) {
|
|
380162
|
+
const code = await askForCode();
|
|
380163
|
+
const outcome = await completeClaim(code);
|
|
380164
|
+
if (outcome.phase === "completed") printCompleted(outcome);
|
|
380165
|
+
return;
|
|
380166
|
+
}
|
|
380167
|
+
console.log("Once they give you the code, run:");
|
|
380168
|
+
console.log(" specific claim --code <code>");
|
|
380169
|
+
} catch (err) {
|
|
380170
|
+
console.error(`Error: ${describeClaimError(err)}`);
|
|
380171
|
+
process.exitCode = 1;
|
|
380172
|
+
}
|
|
380173
|
+
}
|
|
380174
|
+
|
|
379787
380175
|
// src/commands/beta.tsx
|
|
379788
380176
|
import React10, { useState as useState9, useEffect as useEffect9 } from "react";
|
|
379789
380177
|
import { render as render8, Text as Text10, Box as Box9, useInput as useInput5, useApp as useApp5 } from "ink";
|
|
@@ -379881,7 +380269,7 @@ function compareVersions(a, b) {
|
|
|
379881
380269
|
return 0;
|
|
379882
380270
|
}
|
|
379883
380271
|
async function checkForUpdate() {
|
|
379884
|
-
const currentVersion = "0.1.
|
|
380272
|
+
const currentVersion = "0.1.215";
|
|
379885
380273
|
const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
|
|
379886
380274
|
if (!response.ok) {
|
|
379887
380275
|
throw new Error(`Failed to check for updates: HTTP ${response.status}`);
|
|
@@ -380915,7 +381303,7 @@ async function feedbackCommand(message) {
|
|
|
380915
381303
|
await client2.submitFeedback({
|
|
380916
381304
|
message: trimmed,
|
|
380917
381305
|
projectId,
|
|
380918
|
-
cliVersion: "0.1.
|
|
381306
|
+
cliVersion: "0.1.215"
|
|
380919
381307
|
});
|
|
380920
381308
|
trackEvent("feedback_submitted", { hasProject: !!projectId });
|
|
380921
381309
|
console.log("Thanks! We will take a look at your feedback shortly.");
|
|
@@ -380934,7 +381322,7 @@ async function feedbackCommand(message) {
|
|
|
380934
381322
|
var program = new Command();
|
|
380935
381323
|
var env = "production";
|
|
380936
381324
|
var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
|
|
380937
|
-
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.
|
|
381325
|
+
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.215").enablePositionalOptions();
|
|
380938
381326
|
program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
|
|
380939
381327
|
Examples:
|
|
380940
381328
|
$ specific init
|
|
@@ -381087,7 +381475,14 @@ Examples:
|
|
|
381087
381475
|
$ specific feedback "specific query has no way to export results to CSV"`).action((message) => feedbackCommand(message));
|
|
381088
381476
|
program.command("beta").description("Manage beta feature flags").action(betaCommand);
|
|
381089
381477
|
program.command("update").description("Update Specific CLI to the latest version").action(updateCommand);
|
|
381090
|
-
program.command("login").description("Log in to Specific").option("--device-code <code>", "Complete login using a device code from a previous non-interactive login").
|
|
381478
|
+
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", `
|
|
381479
|
+
Examples:
|
|
381480
|
+
$ specific login
|
|
381481
|
+
$ specific login --agent # unattended: registers an agent account the user can claim later`).action((options2) => loginCommand(options2));
|
|
381482
|
+
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", `
|
|
381483
|
+
Examples:
|
|
381484
|
+
$ specific claim --email jane@example.com
|
|
381485
|
+
$ specific claim --code 123456`).action((options2) => claimCommand(options2));
|
|
381091
381486
|
program.command("logout").description("Log out of Specific").action(logoutCommand);
|
|
381092
381487
|
var commandName = process.argv[2] || "help";
|
|
381093
381488
|
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/{OaJNk85sbbCne3x8W7_JZ → iWFTGcbsQ24RGzC0LvjEN}/_buildManifest.js
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/admin/_next/static/{OaJNk85sbbCne3x8W7_JZ → iWFTGcbsQ24RGzC0LvjEN}/_ssgManifest.js
RENAMED
|
File without changes
|