@specific.dev/cli 0.1.68 → 0.1.70
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/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/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 +212 -174
- package/dist/docs/index.md +1 -0
- package/dist/docs/integrations/python.md +220 -0
- package/dist/postinstall.js +18 -4
- package/package.json +1 -1
- /package/dist/admin/_next/static/{w4VP36_YGzWIvqWZUyEgj → HT8dN-xP5FgfDQtqZwDzf}/_buildManifest.js +0 -0
- /package/dist/admin/_next/static/{w4VP36_YGzWIvqWZUyEgj → HT8dN-xP5FgfDQtqZwDzf}/_clientMiddlewareManifest.json +0 -0
- /package/dist/admin/_next/static/{w4VP36_YGzWIvqWZUyEgj → HT8dN-xP5FgfDQtqZwDzf}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -183842,6 +183842,18 @@ var RefreshTokenExpiredError = class extends Error {
|
|
|
183842
183842
|
|
|
183843
183843
|
// src/lib/auth/workos.ts
|
|
183844
183844
|
var WORKOS_CLIENT_ID = "client_01K4HSP8CE0R73V7Z67WE9CC86";
|
|
183845
|
+
function getTokenExpiresAt(accessToken) {
|
|
183846
|
+
try {
|
|
183847
|
+
const payload = accessToken.split(".")[1];
|
|
183848
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
183849
|
+
if (typeof decoded.exp === "number") {
|
|
183850
|
+
return decoded.exp * 1e3;
|
|
183851
|
+
}
|
|
183852
|
+
return null;
|
|
183853
|
+
} catch {
|
|
183854
|
+
return null;
|
|
183855
|
+
}
|
|
183856
|
+
}
|
|
183845
183857
|
async function initiateDeviceAuthorization() {
|
|
183846
183858
|
const response = await fetch(
|
|
183847
183859
|
"https://api.workos.com/user_management/authorize/device",
|
|
@@ -183906,6 +183918,157 @@ import React from "react";
|
|
|
183906
183918
|
import { render, Box, Text } from "ink";
|
|
183907
183919
|
import Spinner from "ink-spinner";
|
|
183908
183920
|
|
|
183921
|
+
// src/lib/auth/login-flow.ts
|
|
183922
|
+
function runLoginFlow(options2) {
|
|
183923
|
+
const { setState, isReauthentication } = options2;
|
|
183924
|
+
let cancelled = false;
|
|
183925
|
+
async function run() {
|
|
183926
|
+
try {
|
|
183927
|
+
setState({ phase: "initiating" });
|
|
183928
|
+
writeLog("auth", "Starting login flow");
|
|
183929
|
+
if (isReauthentication) {
|
|
183930
|
+
clearUserCredentials();
|
|
183931
|
+
}
|
|
183932
|
+
writeLog("auth", "Initiating device authorization with WorkOS");
|
|
183933
|
+
const deviceAuth = await initiateDeviceAuthorization();
|
|
183934
|
+
writeLog(
|
|
183935
|
+
"auth",
|
|
183936
|
+
`Device authorization received: user_code=${deviceAuth.user_code}`
|
|
183937
|
+
);
|
|
183938
|
+
if (cancelled) return;
|
|
183939
|
+
setState({
|
|
183940
|
+
phase: "waiting-for-browser",
|
|
183941
|
+
userCode: deviceAuth.user_code,
|
|
183942
|
+
verificationUri: deviceAuth.verification_uri_complete
|
|
183943
|
+
});
|
|
183944
|
+
const { default: open3 } = await Promise.resolve().then(() => (init_open(), open_exports));
|
|
183945
|
+
await open3(deviceAuth.verification_uri_complete);
|
|
183946
|
+
const token = await pollUntilToken(deviceAuth, () => cancelled);
|
|
183947
|
+
if (cancelled || !token) return;
|
|
183948
|
+
writeLog("auth", "Fetching user info and saving credentials...");
|
|
183949
|
+
await saveCredentialsFromToken(token);
|
|
183950
|
+
if (cancelled) return;
|
|
183951
|
+
writeLog("auth", "Credentials written successfully");
|
|
183952
|
+
setState({ phase: "success", email: token.user.email });
|
|
183953
|
+
writeLog("auth", "Login flow completed successfully");
|
|
183954
|
+
} catch (err) {
|
|
183955
|
+
if (cancelled) return;
|
|
183956
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
183957
|
+
writeLog("auth", `Login error: ${message}`);
|
|
183958
|
+
setState({ phase: "error", message });
|
|
183959
|
+
}
|
|
183960
|
+
}
|
|
183961
|
+
run();
|
|
183962
|
+
return {
|
|
183963
|
+
cancel: () => {
|
|
183964
|
+
cancelled = true;
|
|
183965
|
+
}
|
|
183966
|
+
};
|
|
183967
|
+
}
|
|
183968
|
+
async function pollUntilToken(deviceAuth, isCancelled) {
|
|
183969
|
+
let interval = deviceAuth.interval * 1e3;
|
|
183970
|
+
const expiresAt = Date.now() + deviceAuth.expires_in * 1e3;
|
|
183971
|
+
while (!isCancelled() && Date.now() < expiresAt) {
|
|
183972
|
+
await sleep(interval);
|
|
183973
|
+
if (isCancelled()) return null;
|
|
183974
|
+
writeLog(
|
|
183975
|
+
"auth",
|
|
183976
|
+
`Polling for token (timeRemaining=${Math.round((expiresAt - Date.now()) / 1e3)}s)`
|
|
183977
|
+
);
|
|
183978
|
+
const response = await pollForToken(deviceAuth.device_code);
|
|
183979
|
+
writeLog(
|
|
183980
|
+
"auth",
|
|
183981
|
+
`Poll response: ${JSON.stringify(response).substring(0, 200)}`
|
|
183982
|
+
);
|
|
183983
|
+
if (!("error" in response)) {
|
|
183984
|
+
writeLog("auth", "Token received successfully from WorkOS");
|
|
183985
|
+
return response;
|
|
183986
|
+
}
|
|
183987
|
+
if (response.error === "slow_down") {
|
|
183988
|
+
interval += 1e3;
|
|
183989
|
+
writeLog("auth", `Slowing down, new interval: ${interval}ms`);
|
|
183990
|
+
} else if (response.error !== "authorization_pending") {
|
|
183991
|
+
throw new Error(`Authentication failed: ${response.error}`);
|
|
183992
|
+
}
|
|
183993
|
+
}
|
|
183994
|
+
if (!isCancelled()) {
|
|
183995
|
+
throw new Error("Authentication timed out. Please try again.");
|
|
183996
|
+
}
|
|
183997
|
+
return null;
|
|
183998
|
+
}
|
|
183999
|
+
function sleep(ms) {
|
|
184000
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
184001
|
+
}
|
|
184002
|
+
|
|
184003
|
+
// src/lib/auth/login.tsx
|
|
184004
|
+
function LoginUI({
|
|
184005
|
+
state,
|
|
184006
|
+
isReauthentication
|
|
184007
|
+
}) {
|
|
184008
|
+
if (state.phase === "error") {
|
|
184009
|
+
return /* @__PURE__ */ React.createElement(Text, { color: "red" }, "Error: ", state.message);
|
|
184010
|
+
}
|
|
184011
|
+
if (state.phase === "success") {
|
|
184012
|
+
return /* @__PURE__ */ React.createElement(Text, { color: "green" }, isReauthentication ? "Re-authenticated" : "Logged in", " as ", state.email);
|
|
184013
|
+
}
|
|
184014
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1 }, isReauthentication && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Session expired. Please log in again."), /* @__PURE__ */ React.createElement(Text, { bold: true }, "Log in to Specific"), state.phase === "waiting-for-browser" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, null, "Your authentication code:", " ", /* @__PURE__ */ React.createElement(Text, { color: "cyan", bold: true }, state.userCode))), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React.createElement(Text, null, " Waiting for authentication in browser...")), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "If the browser didn't open, visit: ", state.verificationUri)) : /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React.createElement(Text, null, " Initiating login...")));
|
|
184015
|
+
}
|
|
184016
|
+
function performLogin(options2 = {}) {
|
|
184017
|
+
return new Promise((resolve10) => {
|
|
184018
|
+
let currentState = { phase: "initiating" };
|
|
184019
|
+
let flowHandle;
|
|
184020
|
+
const instance = render(
|
|
184021
|
+
/* @__PURE__ */ React.createElement(
|
|
184022
|
+
LoginUI,
|
|
184023
|
+
{
|
|
184024
|
+
state: currentState,
|
|
184025
|
+
isReauthentication: options2.isReauthentication
|
|
184026
|
+
}
|
|
184027
|
+
)
|
|
184028
|
+
);
|
|
184029
|
+
const cleanup = () => {
|
|
184030
|
+
flowHandle?.cancel();
|
|
184031
|
+
instance.unmount();
|
|
184032
|
+
};
|
|
184033
|
+
const handleExit = () => {
|
|
184034
|
+
cleanup();
|
|
184035
|
+
process.exit(0);
|
|
184036
|
+
};
|
|
184037
|
+
process.on("SIGINT", handleExit);
|
|
184038
|
+
process.on("SIGTERM", handleExit);
|
|
184039
|
+
flowHandle = runLoginFlow({
|
|
184040
|
+
isReauthentication: options2.isReauthentication,
|
|
184041
|
+
setState: (newState) => {
|
|
184042
|
+
currentState = newState;
|
|
184043
|
+
instance.rerender(
|
|
184044
|
+
/* @__PURE__ */ React.createElement(
|
|
184045
|
+
LoginUI,
|
|
184046
|
+
{
|
|
184047
|
+
state: currentState,
|
|
184048
|
+
isReauthentication: options2.isReauthentication
|
|
184049
|
+
}
|
|
184050
|
+
)
|
|
184051
|
+
);
|
|
184052
|
+
if (newState.phase === "success") {
|
|
184053
|
+
setTimeout(() => {
|
|
184054
|
+
process.off("SIGINT", handleExit);
|
|
184055
|
+
process.off("SIGTERM", handleExit);
|
|
184056
|
+
instance.unmount();
|
|
184057
|
+
resolve10({ success: true, userEmail: newState.email });
|
|
184058
|
+
}, 100);
|
|
184059
|
+
} else if (newState.phase === "error") {
|
|
184060
|
+
setTimeout(() => {
|
|
184061
|
+
process.off("SIGINT", handleExit);
|
|
184062
|
+
process.off("SIGTERM", handleExit);
|
|
184063
|
+
instance.unmount();
|
|
184064
|
+
resolve10({ success: false, error: new Error(newState.message) });
|
|
184065
|
+
}, 100);
|
|
184066
|
+
}
|
|
184067
|
+
}
|
|
184068
|
+
});
|
|
184069
|
+
});
|
|
184070
|
+
}
|
|
184071
|
+
|
|
183909
184072
|
// src/lib/api/client.ts
|
|
183910
184073
|
var ApiClient = class {
|
|
183911
184074
|
baseUrl;
|
|
@@ -184217,166 +184380,6 @@ var ApiClient = class {
|
|
|
184217
184380
|
}
|
|
184218
184381
|
};
|
|
184219
184382
|
|
|
184220
|
-
// src/lib/auth/login-flow.ts
|
|
184221
|
-
function runLoginFlow(options2) {
|
|
184222
|
-
const { setState, isReauthentication } = options2;
|
|
184223
|
-
let cancelled = false;
|
|
184224
|
-
async function run() {
|
|
184225
|
-
try {
|
|
184226
|
-
setState({ phase: "initiating" });
|
|
184227
|
-
writeLog("auth", "Starting login flow");
|
|
184228
|
-
if (isReauthentication) {
|
|
184229
|
-
clearUserCredentials();
|
|
184230
|
-
}
|
|
184231
|
-
writeLog("auth", "Initiating device authorization with WorkOS");
|
|
184232
|
-
const deviceAuth = await initiateDeviceAuthorization();
|
|
184233
|
-
writeLog(
|
|
184234
|
-
"auth",
|
|
184235
|
-
`Device authorization received: user_code=${deviceAuth.user_code}`
|
|
184236
|
-
);
|
|
184237
|
-
if (cancelled) return;
|
|
184238
|
-
setState({
|
|
184239
|
-
phase: "waiting-for-browser",
|
|
184240
|
-
userCode: deviceAuth.user_code,
|
|
184241
|
-
verificationUri: deviceAuth.verification_uri_complete
|
|
184242
|
-
});
|
|
184243
|
-
const { default: open3 } = await Promise.resolve().then(() => (init_open(), open_exports));
|
|
184244
|
-
await open3(deviceAuth.verification_uri_complete);
|
|
184245
|
-
const token = await pollUntilToken(deviceAuth, () => cancelled);
|
|
184246
|
-
if (cancelled || !token) return;
|
|
184247
|
-
writeLog("auth", "Fetching user info from platform API...");
|
|
184248
|
-
const client2 = new ApiClient({ token: token.access_token });
|
|
184249
|
-
const user = await client2.getMe();
|
|
184250
|
-
writeLog("auth", `User info received: id=${user.id}`);
|
|
184251
|
-
if (cancelled) return;
|
|
184252
|
-
writeLog("auth", "Writing credentials to disk...");
|
|
184253
|
-
writeUserCredentials({
|
|
184254
|
-
accessToken: token.access_token,
|
|
184255
|
-
refreshToken: token.refresh_token,
|
|
184256
|
-
expiresAt: Date.now() + token.expires_in * 1e3,
|
|
184257
|
-
userId: user.id
|
|
184258
|
-
});
|
|
184259
|
-
writeLog("auth", "Credentials written successfully");
|
|
184260
|
-
setState({ phase: "success", email: token.user.email });
|
|
184261
|
-
writeLog("auth", "Login flow completed successfully");
|
|
184262
|
-
} catch (err) {
|
|
184263
|
-
if (cancelled) return;
|
|
184264
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
184265
|
-
writeLog("auth", `Login error: ${message}`);
|
|
184266
|
-
setState({ phase: "error", message });
|
|
184267
|
-
}
|
|
184268
|
-
}
|
|
184269
|
-
run();
|
|
184270
|
-
return {
|
|
184271
|
-
cancel: () => {
|
|
184272
|
-
cancelled = true;
|
|
184273
|
-
}
|
|
184274
|
-
};
|
|
184275
|
-
}
|
|
184276
|
-
async function pollUntilToken(deviceAuth, isCancelled) {
|
|
184277
|
-
let interval = deviceAuth.interval * 1e3;
|
|
184278
|
-
const expiresAt = Date.now() + deviceAuth.expires_in * 1e3;
|
|
184279
|
-
while (!isCancelled() && Date.now() < expiresAt) {
|
|
184280
|
-
await sleep(interval);
|
|
184281
|
-
if (isCancelled()) return null;
|
|
184282
|
-
writeLog(
|
|
184283
|
-
"auth",
|
|
184284
|
-
`Polling for token (timeRemaining=${Math.round((expiresAt - Date.now()) / 1e3)}s)`
|
|
184285
|
-
);
|
|
184286
|
-
const response = await pollForToken(deviceAuth.device_code);
|
|
184287
|
-
writeLog(
|
|
184288
|
-
"auth",
|
|
184289
|
-
`Poll response: ${JSON.stringify(response).substring(0, 200)}`
|
|
184290
|
-
);
|
|
184291
|
-
if (!("error" in response)) {
|
|
184292
|
-
writeLog("auth", "Token received successfully from WorkOS");
|
|
184293
|
-
return response;
|
|
184294
|
-
}
|
|
184295
|
-
if (response.error === "slow_down") {
|
|
184296
|
-
interval += 1e3;
|
|
184297
|
-
writeLog("auth", `Slowing down, new interval: ${interval}ms`);
|
|
184298
|
-
} else if (response.error !== "authorization_pending") {
|
|
184299
|
-
throw new Error(`Authentication failed: ${response.error}`);
|
|
184300
|
-
}
|
|
184301
|
-
}
|
|
184302
|
-
if (!isCancelled()) {
|
|
184303
|
-
throw new Error("Authentication timed out. Please try again.");
|
|
184304
|
-
}
|
|
184305
|
-
return null;
|
|
184306
|
-
}
|
|
184307
|
-
function sleep(ms) {
|
|
184308
|
-
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
184309
|
-
}
|
|
184310
|
-
|
|
184311
|
-
// src/lib/auth/login.tsx
|
|
184312
|
-
function LoginUI({
|
|
184313
|
-
state,
|
|
184314
|
-
isReauthentication
|
|
184315
|
-
}) {
|
|
184316
|
-
if (state.phase === "error") {
|
|
184317
|
-
return /* @__PURE__ */ React.createElement(Text, { color: "red" }, "Error: ", state.message);
|
|
184318
|
-
}
|
|
184319
|
-
if (state.phase === "success") {
|
|
184320
|
-
return /* @__PURE__ */ React.createElement(Text, { color: "green" }, isReauthentication ? "Re-authenticated" : "Logged in", " as ", state.email);
|
|
184321
|
-
}
|
|
184322
|
-
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1 }, isReauthentication && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Session expired. Please log in again."), /* @__PURE__ */ React.createElement(Text, { bold: true }, "Log in to Specific"), state.phase === "waiting-for-browser" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, null, "Your authentication code:", " ", /* @__PURE__ */ React.createElement(Text, { color: "cyan", bold: true }, state.userCode))), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React.createElement(Text, null, " Waiting for authentication in browser...")), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "If the browser didn't open, visit: ", state.verificationUri)) : /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React.createElement(Text, null, " Initiating login...")));
|
|
184323
|
-
}
|
|
184324
|
-
function performLogin(options2 = {}) {
|
|
184325
|
-
return new Promise((resolve10) => {
|
|
184326
|
-
let currentState = { phase: "initiating" };
|
|
184327
|
-
let flowHandle;
|
|
184328
|
-
const instance = render(
|
|
184329
|
-
/* @__PURE__ */ React.createElement(
|
|
184330
|
-
LoginUI,
|
|
184331
|
-
{
|
|
184332
|
-
state: currentState,
|
|
184333
|
-
isReauthentication: options2.isReauthentication
|
|
184334
|
-
}
|
|
184335
|
-
)
|
|
184336
|
-
);
|
|
184337
|
-
const cleanup = () => {
|
|
184338
|
-
flowHandle?.cancel();
|
|
184339
|
-
instance.unmount();
|
|
184340
|
-
};
|
|
184341
|
-
const handleExit = () => {
|
|
184342
|
-
cleanup();
|
|
184343
|
-
process.exit(0);
|
|
184344
|
-
};
|
|
184345
|
-
process.on("SIGINT", handleExit);
|
|
184346
|
-
process.on("SIGTERM", handleExit);
|
|
184347
|
-
flowHandle = runLoginFlow({
|
|
184348
|
-
isReauthentication: options2.isReauthentication,
|
|
184349
|
-
setState: (newState) => {
|
|
184350
|
-
currentState = newState;
|
|
184351
|
-
instance.rerender(
|
|
184352
|
-
/* @__PURE__ */ React.createElement(
|
|
184353
|
-
LoginUI,
|
|
184354
|
-
{
|
|
184355
|
-
state: currentState,
|
|
184356
|
-
isReauthentication: options2.isReauthentication
|
|
184357
|
-
}
|
|
184358
|
-
)
|
|
184359
|
-
);
|
|
184360
|
-
if (newState.phase === "success") {
|
|
184361
|
-
setTimeout(() => {
|
|
184362
|
-
process.off("SIGINT", handleExit);
|
|
184363
|
-
process.off("SIGTERM", handleExit);
|
|
184364
|
-
instance.unmount();
|
|
184365
|
-
resolve10({ success: true, userEmail: newState.email });
|
|
184366
|
-
}, 100);
|
|
184367
|
-
} else if (newState.phase === "error") {
|
|
184368
|
-
setTimeout(() => {
|
|
184369
|
-
process.off("SIGINT", handleExit);
|
|
184370
|
-
process.off("SIGTERM", handleExit);
|
|
184371
|
-
instance.unmount();
|
|
184372
|
-
resolve10({ success: false, error: new Error(newState.message) });
|
|
184373
|
-
}, 100);
|
|
184374
|
-
}
|
|
184375
|
-
}
|
|
184376
|
-
});
|
|
184377
|
-
});
|
|
184378
|
-
}
|
|
184379
|
-
|
|
184380
184383
|
// src/lib/auth/credentials.ts
|
|
184381
184384
|
function getUserCredentialsDir() {
|
|
184382
184385
|
return path6.join(os5.homedir(), ".specific");
|
|
@@ -184419,11 +184422,33 @@ function getUserId() {
|
|
|
184419
184422
|
const credentials = readUserCredentials();
|
|
184420
184423
|
return credentials?.userId ?? null;
|
|
184421
184424
|
}
|
|
184425
|
+
async function saveCredentialsFromToken(token) {
|
|
184426
|
+
const client2 = new ApiClient({ token: token.access_token });
|
|
184427
|
+
const user = await client2.getMe();
|
|
184428
|
+
const expiresAt = getTokenExpiresAt(token.access_token) ?? Date.now() + 3600 * 1e3;
|
|
184429
|
+
writeUserCredentials({
|
|
184430
|
+
accessToken: token.access_token,
|
|
184431
|
+
refreshToken: token.refresh_token,
|
|
184432
|
+
expiresAt,
|
|
184433
|
+
userId: user.id
|
|
184434
|
+
});
|
|
184435
|
+
}
|
|
184422
184436
|
async function getValidAccessToken() {
|
|
184423
184437
|
const credentials = readUserCredentials();
|
|
184424
184438
|
if (!credentials) {
|
|
184425
184439
|
throw new Error("Not logged in. Run 'specific login' first.");
|
|
184426
184440
|
}
|
|
184441
|
+
if (!credentials.userId) {
|
|
184442
|
+
const result = await performLogin({ isReauthentication: true });
|
|
184443
|
+
if (!result.success) {
|
|
184444
|
+
throw result.error || new Error("Re-authentication failed");
|
|
184445
|
+
}
|
|
184446
|
+
const newCredentials = readUserCredentials();
|
|
184447
|
+
if (!newCredentials) {
|
|
184448
|
+
throw new Error("Failed to read credentials after re-authentication");
|
|
184449
|
+
}
|
|
184450
|
+
return newCredentials.accessToken;
|
|
184451
|
+
}
|
|
184427
184452
|
const now = Date.now();
|
|
184428
184453
|
const bufferMs = 5 * 60 * 1e3;
|
|
184429
184454
|
if (credentials.expiresAt - bufferMs > now) {
|
|
@@ -184431,10 +184456,12 @@ async function getValidAccessToken() {
|
|
|
184431
184456
|
}
|
|
184432
184457
|
try {
|
|
184433
184458
|
const response = await refreshAccessToken(credentials.refreshToken);
|
|
184459
|
+
const expiresAt = getTokenExpiresAt(response.access_token) ?? Date.now() + 3600 * 1e3;
|
|
184434
184460
|
const updatedCredentials = {
|
|
184435
184461
|
accessToken: response.access_token,
|
|
184436
184462
|
refreshToken: response.refresh_token,
|
|
184437
|
-
expiresAt
|
|
184463
|
+
expiresAt,
|
|
184464
|
+
userId: credentials.userId
|
|
184438
184465
|
};
|
|
184439
184466
|
writeUserCredentials(updatedCredentials);
|
|
184440
184467
|
return updatedCredentials.accessToken;
|
|
@@ -184458,6 +184485,7 @@ async function getValidAccessToken() {
|
|
|
184458
184485
|
var POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
184459
184486
|
var client = null;
|
|
184460
184487
|
var anonymousId = null;
|
|
184488
|
+
var identified = false;
|
|
184461
184489
|
function isEnabled() {
|
|
184462
184490
|
return true;
|
|
184463
184491
|
}
|
|
@@ -184491,16 +184519,29 @@ function getClient() {
|
|
|
184491
184519
|
function trackEvent(event, properties) {
|
|
184492
184520
|
const ph = getClient();
|
|
184493
184521
|
if (!ph) return;
|
|
184522
|
+
const userId = getUserId();
|
|
184523
|
+
if (userId && !identified) {
|
|
184524
|
+
identified = true;
|
|
184525
|
+
ph.identify({
|
|
184526
|
+
distinctId: userId,
|
|
184527
|
+
properties: {
|
|
184528
|
+
anonymous_id: getAnonymousId()
|
|
184529
|
+
}
|
|
184530
|
+
});
|
|
184531
|
+
ph.alias({
|
|
184532
|
+
distinctId: userId,
|
|
184533
|
+
alias: getAnonymousId()
|
|
184534
|
+
});
|
|
184535
|
+
}
|
|
184494
184536
|
ph.capture({
|
|
184495
|
-
distinctId: getAnonymousId(),
|
|
184537
|
+
distinctId: userId ?? getAnonymousId(),
|
|
184496
184538
|
event,
|
|
184497
184539
|
properties: {
|
|
184498
184540
|
...properties,
|
|
184499
|
-
cli_version: "0.1.
|
|
184541
|
+
cli_version: "0.1.70",
|
|
184500
184542
|
platform: process.platform,
|
|
184501
184543
|
node_version: process.version,
|
|
184502
|
-
project_id: getProjectId()
|
|
184503
|
-
user_id: getUserId() ?? void 0
|
|
184544
|
+
project_id: getProjectId()
|
|
184504
184545
|
}
|
|
184505
184546
|
});
|
|
184506
184547
|
}
|
|
@@ -193090,11 +193131,7 @@ function DeployUI({ environment, config, skipBuildTest }) {
|
|
|
193090
193131
|
}
|
|
193091
193132
|
} else {
|
|
193092
193133
|
const successResponse = response;
|
|
193093
|
-
|
|
193094
|
-
accessToken: successResponse.access_token,
|
|
193095
|
-
refreshToken: successResponse.refresh_token,
|
|
193096
|
-
expiresAt: Date.now() + successResponse.expires_in * 1e3
|
|
193097
|
-
});
|
|
193134
|
+
await saveCredentialsFromToken(successResponse);
|
|
193098
193135
|
setState(
|
|
193099
193136
|
(s) => s.projectId ? { phase: "testing-builds", projectId: s.projectId } : { phase: "loading-projects" }
|
|
193100
193137
|
);
|
|
@@ -194601,8 +194638,8 @@ function compareVersions(a, b) {
|
|
|
194601
194638
|
return 0;
|
|
194602
194639
|
}
|
|
194603
194640
|
async function checkForUpdate() {
|
|
194604
|
-
const currentVersion = "0.1.
|
|
194605
|
-
const response = await fetch(`${BINARIES_BASE_URL}/latest`);
|
|
194641
|
+
const currentVersion = "0.1.70";
|
|
194642
|
+
const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
|
|
194606
194643
|
if (!response.ok) {
|
|
194607
194644
|
throw new Error(`Failed to check for updates: HTTP ${response.status}`);
|
|
194608
194645
|
}
|
|
@@ -194739,7 +194776,8 @@ function UpdateUI() {
|
|
|
194739
194776
|
if (cancelled) return;
|
|
194740
194777
|
trackEvent("cli_updated", {
|
|
194741
194778
|
from_version: state.checkResult.currentVersion,
|
|
194742
|
-
to_version: state.checkResult.latestVersion
|
|
194779
|
+
to_version: state.checkResult.latestVersion,
|
|
194780
|
+
source: "manual"
|
|
194743
194781
|
});
|
|
194744
194782
|
writeCheckTimestamp();
|
|
194745
194783
|
setState((s) => ({ ...s, phase: "success" }));
|
|
@@ -194799,7 +194837,7 @@ function updateCommand() {
|
|
|
194799
194837
|
var program = new Command();
|
|
194800
194838
|
var env = "production";
|
|
194801
194839
|
var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
|
|
194802
|
-
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.
|
|
194840
|
+
program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.70").enablePositionalOptions();
|
|
194803
194841
|
program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").action((options2) => initCommand(options2));
|
|
194804
194842
|
program.command("docs [topic]").description("Fetch LLM-optimized documentation").action(docsCommand);
|
|
194805
194843
|
program.command("check").description("Validate specific.hcl configuration").action(checkCommand);
|
package/dist/docs/index.md
CHANGED
|
@@ -35,6 +35,7 @@ A full development environment can be started with `specific dev`. To deploy any
|
|
|
35
35
|
|
|
36
36
|
The following is a list of common frameworks and libraries with guidance on how to use them with Specific:
|
|
37
37
|
|
|
38
|
+
- [Python](/integrations/python): Flask, FastAPI, Django, and other Python frameworks
|
|
38
39
|
- [Next.js](/integrations/nextjs): full-stack React framework
|
|
39
40
|
- [Drizzle ORM](/integrations/drizzle): TypeScript ORM with type safety
|
|
40
41
|
- [Prisma](/integrations/prisma): TypeScript ORM with auto-generated client
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
|
|
3
|
+
Python is supported out of the box with Specific. This guide covers running Python services with virtual environments, module resolution, and common frameworks.
|
|
4
|
+
|
|
5
|
+
## Basic configuration
|
|
6
|
+
|
|
7
|
+
Use `base = "python"` for builds. For development, point the dev command at your virtual environment's Python binary:
|
|
8
|
+
|
|
9
|
+
```hcl
|
|
10
|
+
build "api" {
|
|
11
|
+
base = "python"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
service "api" {
|
|
15
|
+
build = build.api
|
|
16
|
+
command = "python app.py"
|
|
17
|
+
|
|
18
|
+
endpoint {
|
|
19
|
+
public = true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
env = {
|
|
23
|
+
PORT = port
|
|
24
|
+
DATABASE_URL = postgres.main.url
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
dev {
|
|
28
|
+
command = ".venv/bin/python app.py"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
postgres "main" {}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
In production, `base = "python"` handles dependency installation automatically (see [Builds](/builds) for details). In development, you manage your own virtual environment.
|
|
36
|
+
|
|
37
|
+
## Virtual environments
|
|
38
|
+
|
|
39
|
+
Create a virtual environment in your project before running `specific dev`:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
python3 -m venv .venv
|
|
43
|
+
.venv/bin/pip install -r requirements.txt
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or with `uv`:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
uv venv .venv
|
|
50
|
+
uv pip install -r requirements.txt
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
There are two ways to reference the venv in your dev command:
|
|
54
|
+
|
|
55
|
+
**Direct path (recommended):**
|
|
56
|
+
|
|
57
|
+
```hcl
|
|
58
|
+
dev {
|
|
59
|
+
command = ".venv/bin/python app.py"
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Shell activation:**
|
|
64
|
+
|
|
65
|
+
```hcl
|
|
66
|
+
dev {
|
|
67
|
+
command = "source .venv/bin/activate && python app.py"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Both work because service commands run in a shell. The direct path is simpler and avoids potential issues with shell activation scripts.
|
|
72
|
+
|
|
73
|
+
## Module resolution
|
|
74
|
+
|
|
75
|
+
Python module imports work naturally because `specific dev` sets the working directory to the service's `root` (or the project root if no `root` is set). Standard imports resolve as expected:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
myproject/
|
|
79
|
+
specific.hcl
|
|
80
|
+
app.py
|
|
81
|
+
myapp/
|
|
82
|
+
__init__.py
|
|
83
|
+
utils.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# app.py
|
|
88
|
+
from myapp.utils import get_greeting # works
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
You can also use `python -m` to run modules:
|
|
92
|
+
|
|
93
|
+
```hcl
|
|
94
|
+
dev {
|
|
95
|
+
command = ".venv/bin/python -m myapp"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Monorepo support
|
|
100
|
+
|
|
101
|
+
For monorepos, use `root` to set the working directory. The venv path is relative to `root`:
|
|
102
|
+
|
|
103
|
+
```hcl
|
|
104
|
+
build "api" {
|
|
105
|
+
base = "python"
|
|
106
|
+
root = "backend"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
service "api" {
|
|
110
|
+
build = build.api
|
|
111
|
+
command = "python app.py"
|
|
112
|
+
|
|
113
|
+
endpoint {
|
|
114
|
+
public = true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
env = {
|
|
118
|
+
PORT = port
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
dev {
|
|
122
|
+
command = ".venv/bin/python app.py"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Here `.venv/bin/python` resolves to `backend/.venv/bin/python`.
|
|
128
|
+
|
|
129
|
+
## Common frameworks
|
|
130
|
+
|
|
131
|
+
### Flask
|
|
132
|
+
|
|
133
|
+
```hcl
|
|
134
|
+
build "api" {
|
|
135
|
+
base = "python"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
service "api" {
|
|
139
|
+
build = build.api
|
|
140
|
+
command = "flask run --host 0.0.0.0 --port $PORT"
|
|
141
|
+
|
|
142
|
+
endpoint {
|
|
143
|
+
public = true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
env = {
|
|
147
|
+
PORT = port
|
|
148
|
+
FLASK_APP = "app"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
dev {
|
|
152
|
+
command = ".venv/bin/flask run --host 0.0.0.0 --port $PORT --reload"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### FastAPI with uvicorn
|
|
158
|
+
|
|
159
|
+
```hcl
|
|
160
|
+
build "api" {
|
|
161
|
+
base = "python"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
service "api" {
|
|
165
|
+
build = build.api
|
|
166
|
+
command = "uvicorn app:app --host 0.0.0.0 --port $PORT"
|
|
167
|
+
|
|
168
|
+
endpoint {
|
|
169
|
+
public = true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
env = {
|
|
173
|
+
PORT = port
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
dev {
|
|
177
|
+
command = ".venv/bin/uvicorn app:app --host 0.0.0.0 --port $PORT --reload"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Django
|
|
183
|
+
|
|
184
|
+
```hcl
|
|
185
|
+
build "web" {
|
|
186
|
+
base = "python"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
service "web" {
|
|
190
|
+
build = build.web
|
|
191
|
+
command = "gunicorn myproject.wsgi --bind 0.0.0.0:$PORT"
|
|
192
|
+
|
|
193
|
+
endpoint {
|
|
194
|
+
public = true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
env = {
|
|
198
|
+
PORT = port
|
|
199
|
+
DATABASE_URL = postgres.main.url
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
pre_deploy {
|
|
203
|
+
command = "python manage.py migrate"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
dev {
|
|
207
|
+
command = ".venv/bin/python manage.py runserver 0.0.0.0:$PORT"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
postgres "main" {}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Database migrations
|
|
215
|
+
|
|
216
|
+
Use `pre_deploy` hooks for production migrations and `specific exec` for development:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
specific exec api -- .venv/bin/python manage.py migrate
|
|
220
|
+
```
|