@insforge/cli 0.1.55 → 0.1.58
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/index.js +206 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -539,6 +539,9 @@ To sign in, open this URL in your browser:
|
|
|
539
539
|
|
|
540
540
|
// src/lib/credentials.ts
|
|
541
541
|
import * as clack3 from "@clack/prompts";
|
|
542
|
+
function isPatLogin(creds) {
|
|
543
|
+
return creds?.refresh_token?.startsWith("uak_") ?? false;
|
|
544
|
+
}
|
|
542
545
|
async function requireAuth(apiUrl, allowOssBypass = true) {
|
|
543
546
|
const projConfig = getProjectConfig();
|
|
544
547
|
if (allowOssBypass && projConfig?.project_id === FAKE_PROJECT_ID) {
|
|
@@ -556,6 +559,10 @@ async function requireAuth(apiUrl, allowOssBypass = true) {
|
|
|
556
559
|
}
|
|
557
560
|
const creds = getCredentials();
|
|
558
561
|
if (creds && creds.access_token) return creds;
|
|
562
|
+
if (isPatLogin(creds)) {
|
|
563
|
+
await refreshAccessToken(apiUrl);
|
|
564
|
+
return getCredentials();
|
|
565
|
+
}
|
|
559
566
|
clack3.log.info("You need to log in to continue.");
|
|
560
567
|
for (; ; ) {
|
|
561
568
|
try {
|
|
@@ -573,10 +580,43 @@ async function requireAuth(apiUrl, allowOssBypass = true) {
|
|
|
573
580
|
}
|
|
574
581
|
async function refreshAccessToken(apiUrl) {
|
|
575
582
|
const creds = getCredentials();
|
|
576
|
-
if (!creds
|
|
577
|
-
throw new AuthError("
|
|
583
|
+
if (!creds) {
|
|
584
|
+
throw new AuthError("Not logged in. Run `npx @insforge/cli login` first.");
|
|
578
585
|
}
|
|
579
586
|
const platformUrl = getPlatformApiUrl(apiUrl);
|
|
587
|
+
if (isPatLogin(creds)) {
|
|
588
|
+
let res;
|
|
589
|
+
try {
|
|
590
|
+
res = await fetch(`${platformUrl}/auth/v1/exchange-api-key`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: { "Content-Type": "application/json" },
|
|
593
|
+
body: JSON.stringify({ apiKey: creds.refresh_token })
|
|
594
|
+
});
|
|
595
|
+
} catch {
|
|
596
|
+
throw new AuthError(
|
|
597
|
+
`Unable to reach auth server at ${platformUrl}. Check your network connection.`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
if (!res.ok) {
|
|
601
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
602
|
+
throw new AuthError(
|
|
603
|
+
"API key is invalid or revoked. Run `npx @insforge/cli login --user-api-key <new-key>` again."
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
throw new AuthError(
|
|
607
|
+
`Auth server returned HTTP ${res.status} while refreshing session. Please retry shortly.`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
const data = await res.json().catch(() => ({}));
|
|
611
|
+
if (typeof data.token !== "string" || data.token.length === 0) {
|
|
612
|
+
throw new AuthError("Exchange endpoint returned an invalid response (missing token).");
|
|
613
|
+
}
|
|
614
|
+
saveCredentials({ ...creds, access_token: data.token });
|
|
615
|
+
return data.token;
|
|
616
|
+
}
|
|
617
|
+
if (!creds.refresh_token) {
|
|
618
|
+
throw new AuthError("Refresh token not found. Run `npx @insforge/cli login` again.");
|
|
619
|
+
}
|
|
580
620
|
const config = getGlobalConfig();
|
|
581
621
|
const clientId = config.oauth_client_id ?? DEFAULT_CLIENT_ID;
|
|
582
622
|
try {
|
|
@@ -774,10 +814,12 @@ async function createProject(orgId, name, region, apiUrl) {
|
|
|
774
814
|
|
|
775
815
|
// src/commands/login.ts
|
|
776
816
|
function registerLoginCommand(program2) {
|
|
777
|
-
program2.command("login").description("Authenticate with InsForge platform").option("--email", "Login with email and password instead of browser").option("--client-id <id>", "OAuth client ID (defaults to insforge-cli)").action(async (opts, cmd) => {
|
|
817
|
+
program2.command("login").description("Authenticate with InsForge platform").option("--email", "Login with email and password instead of browser").option("--client-id <id>", "OAuth client ID (defaults to insforge-cli)").option("--user-api-key <key>", "Authenticate with a uak_ personal access token").action(async (opts, cmd) => {
|
|
778
818
|
const { json, apiUrl } = getRootOpts(cmd);
|
|
779
819
|
try {
|
|
780
|
-
if (opts.
|
|
820
|
+
if (opts.userApiKey) {
|
|
821
|
+
await loginWithUserApiKey(opts.userApiKey, json, apiUrl);
|
|
822
|
+
} else if (opts.email) {
|
|
781
823
|
await loginWithEmail(json, apiUrl);
|
|
782
824
|
} else {
|
|
783
825
|
await loginWithOAuth(json, apiUrl);
|
|
@@ -846,6 +888,78 @@ async function loginWithOAuth(json, apiUrl) {
|
|
|
846
888
|
console.log(JSON.stringify({ success: true, user: creds.user }));
|
|
847
889
|
}
|
|
848
890
|
}
|
|
891
|
+
async function loginWithUserApiKey(key, json, apiUrl) {
|
|
892
|
+
if (!json) {
|
|
893
|
+
clack4.intro("InsForge CLI");
|
|
894
|
+
}
|
|
895
|
+
if (!key.startsWith("uak_")) {
|
|
896
|
+
throw new CLIError('Invalid API key \u2014 must start with "uak_".');
|
|
897
|
+
}
|
|
898
|
+
const s = !json ? clack4.spinner() : null;
|
|
899
|
+
s?.start("Verifying API key...");
|
|
900
|
+
let jwt;
|
|
901
|
+
let user;
|
|
902
|
+
try {
|
|
903
|
+
const exchanged = await exchangePatForJwt(key, apiUrl);
|
|
904
|
+
jwt = exchanged.token;
|
|
905
|
+
user = exchanged.user;
|
|
906
|
+
} catch (err) {
|
|
907
|
+
s?.stop("API key verification failed");
|
|
908
|
+
throw err instanceof CLIError ? err : new CLIError(err instanceof Error ? err.message : String(err));
|
|
909
|
+
}
|
|
910
|
+
saveCredentials({
|
|
911
|
+
access_token: jwt,
|
|
912
|
+
refresh_token: key,
|
|
913
|
+
user
|
|
914
|
+
});
|
|
915
|
+
if (!json) {
|
|
916
|
+
s?.stop(`Authenticated as ${user.email}`);
|
|
917
|
+
clack4.outro("Done");
|
|
918
|
+
} else {
|
|
919
|
+
console.log(JSON.stringify({ success: true, user }));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function exchangePatForJwt(apiKey, apiUrl) {
|
|
923
|
+
const baseUrl = getPlatformApiUrl(apiUrl);
|
|
924
|
+
const fullUrl = `${baseUrl}/auth/v1/exchange-api-key`;
|
|
925
|
+
let res;
|
|
926
|
+
try {
|
|
927
|
+
res = await fetch(fullUrl, {
|
|
928
|
+
method: "POST",
|
|
929
|
+
headers: { "Content-Type": "application/json" },
|
|
930
|
+
body: JSON.stringify({ apiKey })
|
|
931
|
+
});
|
|
932
|
+
} catch (err) {
|
|
933
|
+
throw new CLIError(formatFetchError(err, fullUrl));
|
|
934
|
+
}
|
|
935
|
+
if (!res.ok) {
|
|
936
|
+
const body = await res.json().catch(() => ({}));
|
|
937
|
+
const msg = body.message ?? body.error ?? `HTTP ${res.status}`;
|
|
938
|
+
throw new CLIError(`API key is invalid or revoked: ${msg}`);
|
|
939
|
+
}
|
|
940
|
+
const data = await res.json().catch(() => ({}));
|
|
941
|
+
if (typeof data.token !== "string" || data.token.length === 0) {
|
|
942
|
+
throw new CLIError("Exchange endpoint returned an invalid response (missing token).");
|
|
943
|
+
}
|
|
944
|
+
const jwt = data.token;
|
|
945
|
+
let profileRes;
|
|
946
|
+
try {
|
|
947
|
+
profileRes = await fetch(`${baseUrl}/auth/v1/profile`, {
|
|
948
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
949
|
+
});
|
|
950
|
+
} catch (err) {
|
|
951
|
+
throw new CLIError(formatFetchError(err, `${baseUrl}/auth/v1/profile`));
|
|
952
|
+
}
|
|
953
|
+
if (!profileRes.ok) {
|
|
954
|
+
throw new CLIError(`Exchange succeeded but could not fetch profile: HTTP ${profileRes.status}`);
|
|
955
|
+
}
|
|
956
|
+
const profile = await profileRes.json().catch(() => null);
|
|
957
|
+
const user = profile && typeof profile === "object" && "user" in profile ? profile.user : profile ?? void 0;
|
|
958
|
+
if (!user) {
|
|
959
|
+
throw new CLIError("Exchange succeeded but profile response was empty");
|
|
960
|
+
}
|
|
961
|
+
return { token: jwt, user };
|
|
962
|
+
}
|
|
849
963
|
|
|
850
964
|
// src/lib/output.ts
|
|
851
965
|
import Table from "cli-table3";
|
|
@@ -1123,7 +1237,7 @@ async function reportCliUsage(toolName, success, maxRetries = 1, explicitConfig)
|
|
|
1123
1237
|
|
|
1124
1238
|
// src/lib/analytics.ts
|
|
1125
1239
|
import { PostHog } from "posthog-node";
|
|
1126
|
-
var POSTHOG_API_KEY = "";
|
|
1240
|
+
var POSTHOG_API_KEY = "phc_ueV1ii62wdBTkH7E70ugyeqHIHu8dFDdjs0qq3TZhJz";
|
|
1127
1241
|
var POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
|
|
1128
1242
|
var client = null;
|
|
1129
1243
|
function getClient() {
|
|
@@ -1208,10 +1322,11 @@ async function ossFetch(path5, options = {}) {
|
|
|
1208
1322
|
message += `
|
|
1209
1323
|
${err.nextActions}`;
|
|
1210
1324
|
}
|
|
1211
|
-
|
|
1325
|
+
const isRouteLevel404 = !err.error || err.error === "NOT_FOUND";
|
|
1326
|
+
if (res.status === 404 && isRouteLevel404 && path5.startsWith("/api/compute")) {
|
|
1212
1327
|
message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
|
|
1213
1328
|
}
|
|
1214
|
-
if (res.status === 404 && path5 === "/api/database/migrations") {
|
|
1329
|
+
if (res.status === 404 && isRouteLevel404 && path5 === "/api/database/migrations") {
|
|
1215
1330
|
message = "Database migrations are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin about database migration support.";
|
|
1216
1331
|
}
|
|
1217
1332
|
throw new CLIError(message);
|
|
@@ -2131,7 +2246,11 @@ function buildOssHost2(appkey, region) {
|
|
|
2131
2246
|
function registerProjectLinkCommand(program2) {
|
|
2132
2247
|
program2.command("link").description("Link current directory to an InsForge project").option("--project-id <id>", "Project ID to link").option("--org-id <id>", "Organization ID").option("--template <template>", "Download a template after linking: react, nextjs, chatbot, crm, e-commerce, todo").option("--api-base-url <url>", "API Base URL for direct linking (OSS/Self-hosted)").option("--api-key <key>", "API Key for direct linking (OSS/Self-hosted)").action(async (opts, cmd) => {
|
|
2133
2248
|
const { json, apiUrl } = getRootOpts(cmd);
|
|
2249
|
+
const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo"];
|
|
2134
2250
|
try {
|
|
2251
|
+
if (opts.template && !validTemplates.includes(opts.template)) {
|
|
2252
|
+
throw new CLIError(`Invalid template "${opts.template}". Valid options: ${validTemplates.join(", ")}`);
|
|
2253
|
+
}
|
|
2135
2254
|
if (opts.apiBaseUrl || opts.apiKey) {
|
|
2136
2255
|
try {
|
|
2137
2256
|
if (!opts.apiBaseUrl || !opts.apiKey) {
|
|
@@ -2152,6 +2271,84 @@ function registerProjectLinkCommand(program2) {
|
|
|
2152
2271
|
oss_host: opts.apiBaseUrl.replace(/\/$/, "")
|
|
2153
2272
|
// remove trailing slash if any
|
|
2154
2273
|
};
|
|
2274
|
+
const template2 = opts.template;
|
|
2275
|
+
if (template2) {
|
|
2276
|
+
const defaultDir = `insforge-${template2}`;
|
|
2277
|
+
let dirName = defaultDir;
|
|
2278
|
+
if (!json) {
|
|
2279
|
+
const inputDir = await text2({
|
|
2280
|
+
message: "Directory name:",
|
|
2281
|
+
initialValue: defaultDir,
|
|
2282
|
+
validate: (v) => {
|
|
2283
|
+
if (v.length < 1) return "Directory name is required";
|
|
2284
|
+
const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2285
|
+
if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
|
|
2286
|
+
return void 0;
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
if (isCancel2(inputDir)) process.exit(0);
|
|
2290
|
+
dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2291
|
+
}
|
|
2292
|
+
if (!dirName || dirName === "." || dirName === "..") {
|
|
2293
|
+
throw new CLIError("Invalid directory name.");
|
|
2294
|
+
}
|
|
2295
|
+
const templateDir = path4.resolve(process.cwd(), dirName);
|
|
2296
|
+
const dirExists = await fs4.stat(templateDir).catch(() => null);
|
|
2297
|
+
if (dirExists) {
|
|
2298
|
+
throw new CLIError(`Directory "${dirName}" already exists.`);
|
|
2299
|
+
}
|
|
2300
|
+
await fs4.mkdir(templateDir);
|
|
2301
|
+
process.chdir(templateDir);
|
|
2302
|
+
saveProjectConfig(projectConfig2);
|
|
2303
|
+
if (json) {
|
|
2304
|
+
outputJson({
|
|
2305
|
+
success: true,
|
|
2306
|
+
project: { id: projectConfig2.project_id, name: projectConfig2.project_name, region: projectConfig2.region },
|
|
2307
|
+
directory: dirName,
|
|
2308
|
+
template: template2
|
|
2309
|
+
});
|
|
2310
|
+
} else {
|
|
2311
|
+
outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
|
|
2312
|
+
}
|
|
2313
|
+
captureEvent(FAKE_ORG_ID, "template_selected", { template: template2, source: "link_direct" });
|
|
2314
|
+
await downloadGitHubTemplate(template2, projectConfig2, json);
|
|
2315
|
+
const templateDownloaded = await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null);
|
|
2316
|
+
if (templateDownloaded && !json) {
|
|
2317
|
+
const installSpinner = clack8.spinner();
|
|
2318
|
+
installSpinner.start("Installing dependencies...");
|
|
2319
|
+
try {
|
|
2320
|
+
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
2321
|
+
installSpinner.stop("Dependencies installed");
|
|
2322
|
+
} catch (err) {
|
|
2323
|
+
installSpinner.stop("Failed to install dependencies");
|
|
2324
|
+
clack8.log.warn(`npm install failed: ${err.message}`);
|
|
2325
|
+
clack8.log.info("Run `npm install` manually to install dependencies.");
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
await installSkills(json);
|
|
2329
|
+
trackCommand("link", "oss-org", { direct: true, template: template2 });
|
|
2330
|
+
await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
|
|
2331
|
+
try {
|
|
2332
|
+
const urlMatch = opts.apiBaseUrl.match(/^https?:\/\/([^.]+)\.[^.]+\.insforge\.app/);
|
|
2333
|
+
if (urlMatch) {
|
|
2334
|
+
await reportAgentConnected({ app_key: urlMatch[1] }, apiUrl);
|
|
2335
|
+
}
|
|
2336
|
+
} catch {
|
|
2337
|
+
}
|
|
2338
|
+
if (!json) {
|
|
2339
|
+
if (templateDownloaded) {
|
|
2340
|
+
const runCommand = `${pc2.cyan("cd")} ${pc2.green(dirName)} ${pc2.dim("&&")} ${pc2.cyan("npm run dev")}`;
|
|
2341
|
+
const steps = [
|
|
2342
|
+
`${pc2.bold("1.")} ${runCommand}`,
|
|
2343
|
+
`${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
|
|
2344
|
+
];
|
|
2345
|
+
clack8.note(steps.join("\n"), "What's next");
|
|
2346
|
+
} else {
|
|
2347
|
+
clack8.log.warn("Template download failed. You can retry or set up manually.");
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2155
2352
|
saveProjectConfig(projectConfig2);
|
|
2156
2353
|
if (json) {
|
|
2157
2354
|
outputJson({ success: true, project: { id: projectConfig2.project_id, name: projectConfig2.project_name, region: projectConfig2.region } });
|
|
@@ -2264,10 +2461,6 @@ function registerProjectLinkCommand(program2) {
|
|
|
2264
2461
|
}
|
|
2265
2462
|
const template = opts.template;
|
|
2266
2463
|
if (template) {
|
|
2267
|
-
const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo"];
|
|
2268
|
-
if (!validTemplates.includes(template)) {
|
|
2269
|
-
throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
|
|
2270
|
-
}
|
|
2271
2464
|
let dirName = project.name;
|
|
2272
2465
|
if (!json) {
|
|
2273
2466
|
const inputDir = await text2({
|
|
@@ -2295,12 +2488,7 @@ function registerProjectLinkCommand(program2) {
|
|
|
2295
2488
|
process.chdir(templateDir);
|
|
2296
2489
|
saveProjectConfig(projectConfig);
|
|
2297
2490
|
captureEvent(orgId ?? project.organization_id, "template_selected", { template, source: "link" });
|
|
2298
|
-
|
|
2299
|
-
if (githubTemplates.includes(template)) {
|
|
2300
|
-
await downloadGitHubTemplate(template, projectConfig, json);
|
|
2301
|
-
} else {
|
|
2302
|
-
await downloadTemplate(template, projectConfig, project.name, json, apiUrl);
|
|
2303
|
-
}
|
|
2491
|
+
await downloadGitHubTemplate(template, projectConfig, json);
|
|
2304
2492
|
const templateDownloaded = await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null);
|
|
2305
2493
|
if (templateDownloaded && !json) {
|
|
2306
2494
|
const installSpinner = clack8.spinner();
|
|
@@ -5529,7 +5717,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
5529
5717
|
const s = !json ? clack10.spinner() : null;
|
|
5530
5718
|
s?.start("Collecting diagnostic data...");
|
|
5531
5719
|
const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
|
|
5532
|
-
const cliVersion = "0.1.
|
|
5720
|
+
const cliVersion = "0.1.58";
|
|
5533
5721
|
s?.stop("Data collected");
|
|
5534
5722
|
if (!json) {
|
|
5535
5723
|
console.log(`
|