@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 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?.refresh_token) {
577
- throw new AuthError("Refresh token not found. Run `npx @insforge/cli login` again.");
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.email) {
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
- if (res.status === 404 && path5.startsWith("/api/compute")) {
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
- const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react", "todo"];
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.55";
5720
+ const cliVersion = "0.1.58";
5533
5721
  s?.stop("Data collected");
5534
5722
  if (!json) {
5535
5723
  console.log(`