@insforge/cli 0.1.44 → 0.1.46

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
@@ -106,6 +106,39 @@ var ProjectNotLinkedError = class extends CLIError {
106
106
  super("No project linked. Run `npx @insforge/cli link` first.", 3, "PROJECT_NOT_LINKED");
107
107
  }
108
108
  };
109
+ function formatFetchError(err, url) {
110
+ if (!(err instanceof Error)) return `Network request to ${url} failed: ${String(err)}`;
111
+ if (err.message !== "fetch failed") return err.message;
112
+ const cause = err.cause;
113
+ const code = cause?.code ?? cause?.errno;
114
+ const causeMsg = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : void 0;
115
+ let host = url;
116
+ try {
117
+ host = new URL(url).host;
118
+ } catch {
119
+ }
120
+ switch (code) {
121
+ case "ENOTFOUND":
122
+ case "EAI_AGAIN":
123
+ return `Cannot resolve ${host} (DNS lookup failed: ${code}). Check your internet connection or DNS settings.`;
124
+ case "ECONNREFUSED":
125
+ return `Connection to ${host} refused. The server may be down or blocked by a firewall.`;
126
+ case "ETIMEDOUT":
127
+ case "UND_ERR_CONNECT_TIMEOUT":
128
+ return `Connection to ${host} timed out. Check your network, VPN, or proxy settings.`;
129
+ case "ECONNRESET":
130
+ case "UND_ERR_SOCKET":
131
+ return `Connection to ${host} was reset. A proxy, VPN, or firewall may be interfering.`;
132
+ case "CERT_HAS_EXPIRED":
133
+ case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
134
+ case "SELF_SIGNED_CERT_IN_CHAIN":
135
+ case "DEPTH_ZERO_SELF_SIGNED_CERT":
136
+ return `TLS certificate error contacting ${host} (${code}). Your network may be intercepting HTTPS (corporate proxy / VPN).`;
137
+ }
138
+ if (code) return `Network error contacting ${host}: ${code}${causeMsg ? ` \u2014 ${causeMsg}` : ""}`;
139
+ if (causeMsg) return `Network error contacting ${host}: ${causeMsg}`;
140
+ return `Network error contacting ${host}.`;
141
+ }
109
142
  function getDeploymentError(metadata) {
110
143
  if (!metadata || typeof metadata.error !== "object" || !metadata.error) return null;
111
144
  return metadata.error.errorMessage ?? null;
@@ -167,36 +200,48 @@ function buildAuthorizeUrl(params) {
167
200
  return url.toString();
168
201
  }
169
202
  async function exchangeCodeForTokens(params) {
170
- const res = await fetch(`${params.platformUrl}/api/oauth/v1/token`, {
171
- method: "POST",
172
- headers: { "Content-Type": "application/json" },
173
- body: JSON.stringify({
174
- grant_type: "authorization_code",
175
- code: params.code,
176
- redirect_uri: params.redirectUri,
177
- client_id: params.clientId,
178
- code_verifier: params.codeVerifier
179
- })
180
- });
203
+ const tokenUrl = `${params.platformUrl}/api/oauth/v1/token`;
204
+ let res;
205
+ try {
206
+ res = await fetch(tokenUrl, {
207
+ method: "POST",
208
+ headers: { "Content-Type": "application/json" },
209
+ body: JSON.stringify({
210
+ grant_type: "authorization_code",
211
+ code: params.code,
212
+ redirect_uri: params.redirectUri,
213
+ client_id: params.clientId,
214
+ code_verifier: params.codeVerifier
215
+ })
216
+ });
217
+ } catch (err) {
218
+ throw new Error(`Token exchange failed \u2014 ${formatFetchError(err, tokenUrl)}`, { cause: err });
219
+ }
181
220
  if (!res.ok) {
182
221
  const err = await res.json().catch(() => ({}));
183
- throw new Error(err.error_description ?? err.error ?? "Token exchange failed");
222
+ throw new Error(err.error_description ?? err.error ?? `Token exchange failed (HTTP ${res.status})`);
184
223
  }
185
224
  return await res.json();
186
225
  }
187
226
  async function refreshOAuthToken(params) {
188
- const res = await fetch(`${params.platformUrl}/api/oauth/v1/token`, {
189
- method: "POST",
190
- headers: { "Content-Type": "application/json" },
191
- body: JSON.stringify({
192
- grant_type: "refresh_token",
193
- refresh_token: params.refreshToken,
194
- client_id: params.clientId
195
- })
196
- });
227
+ const tokenUrl = `${params.platformUrl}/api/oauth/v1/token`;
228
+ let res;
229
+ try {
230
+ res = await fetch(tokenUrl, {
231
+ method: "POST",
232
+ headers: { "Content-Type": "application/json" },
233
+ body: JSON.stringify({
234
+ grant_type: "refresh_token",
235
+ refresh_token: params.refreshToken,
236
+ client_id: params.clientId
237
+ })
238
+ });
239
+ } catch (err) {
240
+ throw new Error(`Token refresh failed \u2014 ${formatFetchError(err, tokenUrl)}`, { cause: err });
241
+ }
197
242
  if (!res.ok) {
198
243
  const err = await res.json().catch(() => ({}));
199
- throw new Error(err.error_description ?? err.error ?? "Token refresh failed");
244
+ throw new Error(err.error_description ?? err.error ?? `Token refresh failed (HTTP ${res.status})`);
200
245
  }
201
246
  return await res.json();
202
247
  }
@@ -394,11 +439,22 @@ async function platformFetch(path5, options = {}, apiUrl) {
394
439
  Authorization: `Bearer ${token}`,
395
440
  ...options.headers ?? {}
396
441
  };
397
- const res = await fetch(`${baseUrl}${path5}`, { ...options, headers });
442
+ const fullUrl = `${baseUrl}${path5}`;
443
+ let res;
444
+ try {
445
+ res = await fetch(fullUrl, { ...options, headers });
446
+ } catch (err) {
447
+ throw new CLIError(formatFetchError(err, fullUrl));
448
+ }
398
449
  if (res.status === 401) {
399
450
  const newToken = await refreshAccessToken(apiUrl);
400
451
  headers.Authorization = `Bearer ${newToken}`;
401
- const retryRes = await fetch(`${baseUrl}${path5}`, { ...options, headers });
452
+ let retryRes;
453
+ try {
454
+ retryRes = await fetch(fullUrl, { ...options, headers });
455
+ } catch (err) {
456
+ throw new CLIError(formatFetchError(err, fullUrl));
457
+ }
402
458
  if (!retryRes.ok) {
403
459
  const err = await retryRes.json().catch(() => ({}));
404
460
  throw new CLIError(err.error ?? `Request failed: ${retryRes.status}`, retryRes.status === 403 ? 5 : 1);
@@ -709,6 +765,28 @@ import { join as join2 } from "path";
709
765
  import { promisify } from "util";
710
766
  import * as clack5 from "@clack/prompts";
711
767
  var execAsync = promisify(exec);
768
+ var SKILL_INSTALL_TIMEOUT_MS = 6e4;
769
+ function describeExecError(err) {
770
+ const e = err;
771
+ if (e.killed && (e.signal === "SIGTERM" || e.signal === "SIGKILL")) {
772
+ return `timed out after ${SKILL_INSTALL_TIMEOUT_MS / 1e3}s \u2014 the npm registry may be slow or blocked by your network`;
773
+ }
774
+ if (e.code === "ENOENT") {
775
+ return "`npx` is not on your PATH \u2014 install Node.js 18+ and reopen your shell";
776
+ }
777
+ const stderr = (typeof e.stderr === "string" ? e.stderr : e.stderr?.toString()) ?? "";
778
+ if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(stderr)) return "cannot reach the npm registry (DNS lookup failed) \u2014 check your internet connection";
779
+ if (/ECONNREFUSED/i.test(stderr)) return "connection to the npm registry was refused \u2014 a proxy or firewall is likely blocking it";
780
+ if (/ETIMEDOUT|ESOCKETTIMEDOUT|network timeout/i.test(stderr)) return "the npm registry timed out \u2014 check your VPN, proxy, or corporate network";
781
+ if (/CERT_HAS_EXPIRED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|SELF_SIGNED_CERT/i.test(stderr)) return "TLS error reaching the npm registry \u2014 a corporate proxy may be intercepting HTTPS";
782
+ if (/\bE404\b|404 Not Found/i.test(stderr)) return "npm returned 404 \u2014 the `skills` package or a dependency could not be found (check your npm registry config)";
783
+ if (/EACCES|permission denied/i.test(stderr)) return "permission denied writing files \u2014 run from a directory you own, without sudo";
784
+ if (/ENOSPC|no space left/i.test(stderr)) return "no disk space left to install the package";
785
+ if (/\b401\b|EAUTH|authentication/i.test(stderr)) return "npm authentication failed \u2014 check ~/.npmrc";
786
+ if (typeof e.code === "number") return `npx exited with code ${e.code}`;
787
+ if (typeof e.code === "string") return e.code;
788
+ return e.message ?? "unknown error";
789
+ }
712
790
  var GITIGNORE_ENTRIES = [
713
791
  ".insforge",
714
792
  ".agent",
@@ -741,33 +819,41 @@ async function installSkills(json) {
741
819
  if (!json) clack5.log.info("Installing InsForge agent skills...");
742
820
  await execAsync("npx skills add insforge/agent-skills -y -a antigravity -a augment -a claude-code -a cline -a codex -a cursor -a gemini-cli -a github-copilot -a kilo -a qoder -a qwen-code -a roo -a trae -a windsurf", {
743
821
  cwd: process.cwd(),
744
- timeout: 6e4
822
+ timeout: SKILL_INSTALL_TIMEOUT_MS
745
823
  });
746
824
  if (!json) clack5.log.success("InsForge agent skills installed.");
747
- } catch {
748
- if (!json) clack5.log.warn("Failed to install agent skills. You can run manually: npx skills add insforge/agent-skills");
825
+ } catch (err) {
826
+ if (!json) {
827
+ clack5.log.warn(`Could not install agent skills: ${describeExecError(err)}`);
828
+ clack5.log.info("Run `npx skills add insforge/agent-skills` once resolved to see the full output.");
829
+ }
749
830
  }
750
831
  try {
751
832
  if (!json) clack5.log.info("Installing find-skills...");
752
833
  await execAsync("npx skills add https://github.com/vercel-labs/skills --skill find-skills -y", {
753
834
  cwd: process.cwd(),
754
- timeout: 6e4
835
+ timeout: SKILL_INSTALL_TIMEOUT_MS
755
836
  });
756
837
  if (!json) clack5.log.success("find-skills installed.");
757
- } catch {
758
- if (!json) clack5.log.warn("Failed to install find-skills. You can run manually: npx skills add https://github.com/vercel-labs/skills --skill find-skills");
838
+ } catch (err) {
839
+ if (!json) {
840
+ clack5.log.warn(`Could not install find-skills: ${describeExecError(err)}`);
841
+ clack5.log.info("Run `npx skills add https://github.com/vercel-labs/skills --skill find-skills` once resolved.");
842
+ }
759
843
  }
760
844
  try {
761
845
  updateGitignore();
762
846
  } catch {
763
847
  }
764
848
  }
765
- async function reportCliUsage(toolName, success, maxRetries = 1) {
766
- let config;
767
- try {
768
- config = getProjectConfig();
769
- } catch {
770
- return;
849
+ async function reportCliUsage(toolName, success, maxRetries = 1, explicitConfig) {
850
+ let config = explicitConfig;
851
+ if (!config) {
852
+ try {
853
+ config = getProjectConfig();
854
+ } catch {
855
+ return;
856
+ }
771
857
  }
772
858
  if (!config) return;
773
859
  const payload = JSON.stringify({
@@ -1215,7 +1301,7 @@ async function copyDir(src, dest) {
1215
1301
  }
1216
1302
  }
1217
1303
  function registerCreateCommand(program2) {
1218
- program2.command("create").description("Create a new InsForge project").option("--name <name>", "Project name").option("--org-id <id>", "Organization ID").option("--region <region>", "Deployment region (us-east, us-west, eu-central, ap-southeast)").option("--template <template>", "Template to use: react, nextjs, chatbot, crm, e-commerce, or empty").action(async (opts, cmd) => {
1304
+ program2.command("create").description("Create a new InsForge project").option("--name <name>", "Project name").option("--org-id <id>", "Organization ID").option("--region <region>", "Deployment region (us-east, us-west, eu-central, ap-southeast)").option("--template <template>", "Template to use: react, nextjs, chatbot, crm, e-commerce, todo, or empty").action(async (opts, cmd) => {
1219
1305
  const { json, apiUrl } = getRootOpts(cmd);
1220
1306
  try {
1221
1307
  await requireAuth(apiUrl, false);
@@ -1266,7 +1352,7 @@ function registerCreateCommand(program2) {
1266
1352
  if (projectName.length < 2 || projectName === "." || projectName === "..") {
1267
1353
  throw new CLIError("Project name must be at least 2 safe characters (letters, numbers, hyphens).");
1268
1354
  }
1269
- const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "empty"];
1355
+ const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo", "empty"];
1270
1356
  let template = opts.template;
1271
1357
  if (template && !validTemplates.includes(template)) {
1272
1358
  throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
@@ -1296,7 +1382,8 @@ function registerCreateCommand(program2) {
1296
1382
  { value: "nextjs", label: "Web app template with Next.js" },
1297
1383
  { value: "chatbot", label: "AI Chatbot with Next.js" },
1298
1384
  { value: "crm", label: "CRM with Next.js" },
1299
- { value: "e-commerce", label: "E-Commerce store with Next.js" }
1385
+ { value: "e-commerce", label: "E-Commerce store with Next.js" },
1386
+ { value: "todo", label: "Todo app with Next.js" }
1300
1387
  ]
1301
1388
  });
1302
1389
  if (clack7.isCancel(selected)) process.exit(0);
@@ -1359,7 +1446,7 @@ function registerCreateCommand(program2) {
1359
1446
  saveProjectConfig(projectConfig);
1360
1447
  projectLinked = true;
1361
1448
  s?.stop(`Project "${project.name}" created and linked`);
1362
- const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react"];
1449
+ const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react", "todo"];
1363
1450
  if (githubTemplates.includes(template)) {
1364
1451
  await downloadGitHubTemplate(template, projectConfig, json);
1365
1452
  } else if (hasTemplate) {
@@ -1618,7 +1705,7 @@ function buildOssHost2(appkey, region) {
1618
1705
  return `https://${appkey}.${region}.insforge.app`;
1619
1706
  }
1620
1707
  function registerProjectLinkCommand(program2) {
1621
- 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").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) => {
1708
+ 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) => {
1622
1709
  const { json, apiUrl } = getRootOpts(cmd);
1623
1710
  try {
1624
1711
  if (opts.apiBaseUrl || opts.apiKey) {
@@ -1649,7 +1736,7 @@ function registerProjectLinkCommand(program2) {
1649
1736
  }
1650
1737
  trackCommand("link", "oss-org", { direct: true });
1651
1738
  await installSkills(json);
1652
- await reportCliUsage("cli.link_direct", true, 6);
1739
+ await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
1653
1740
  try {
1654
1741
  const urlMatch = opts.apiBaseUrl.match(/^https?:\/\/([^.]+)\.[^.]+\.insforge\.app/);
1655
1742
  if (urlMatch) {
@@ -1747,14 +1834,14 @@ function registerProjectLinkCommand(program2) {
1747
1834
  outputSuccess(`Linked to project "${project.name}" (${project.appkey}.${project.region})`);
1748
1835
  }
1749
1836
  await installSkills(json);
1750
- await reportCliUsage("cli.link", true, 6);
1837
+ await reportCliUsage("cli.link", true, 6, projectConfig);
1751
1838
  try {
1752
1839
  await reportAgentConnected({ project_id: project.id }, apiUrl);
1753
1840
  } catch {
1754
1841
  }
1755
1842
  const template = opts.template;
1756
1843
  if (template) {
1757
- const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce"];
1844
+ const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo"];
1758
1845
  if (!validTemplates.includes(template)) {
1759
1846
  throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
1760
1847
  }
@@ -1785,7 +1872,7 @@ function registerProjectLinkCommand(program2) {
1785
1872
  process.chdir(templateDir);
1786
1873
  saveProjectConfig(projectConfig);
1787
1874
  captureEvent(orgId ?? project.organization_id, "template_selected", { template, source: "link" });
1788
- const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react"];
1875
+ const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react", "todo"];
1789
1876
  if (githubTemplates.includes(template)) {
1790
1877
  await downloadGitHubTemplate(template, projectConfig, json);
1791
1878
  } else {