@insforge/cli 0.1.25 → 0.1.28

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/README.md CHANGED
@@ -594,6 +594,38 @@ npm link # makes `insforge` available globally
594
594
  npm run dev # watch mode for development
595
595
  ```
596
596
 
597
+ ### Testing
598
+
599
+ #### Unit tests
600
+
601
+ ```bash
602
+ npm run test:unit
603
+ ```
604
+
605
+ #### Real project integration tests
606
+
607
+ Run locally:
608
+
609
+ ```bash
610
+ INTEGRATION_TEST_ENABLED=true \
611
+ INTEGRATION_LOG_SOURCE=insforge.logs \
612
+ npm run test:integration:real
613
+ ```
614
+
615
+ Prerequisites:
616
+ - Logged in (`insforge login`) so `~/.insforge/credentials.json` exists
617
+ - Linked project in this repo (`insforge link`) so `.insforge/project.json` exists
618
+
619
+ Optional environment variables:
620
+ - `INSFORGE_API_URL`: Platform API URL override (defaults to `https://api.insforge.dev`)
621
+ - `INTEGRATION_LOG_SOURCE`: Log source for `logs` test (default `insforge.logs`)
622
+
623
+ Current real-project checks:
624
+ - `whoami --json`
625
+ - `metadata --json`
626
+ - `logs <source> --json`
627
+ - `docs instructions --json`
628
+
597
629
  ## Releasing
598
630
 
599
631
  Bump the version, push the tag, and create a GitHub Release — the CI will publish to npm automatically.
package/dist/index.js CHANGED
@@ -139,7 +139,7 @@ function getRootOpts(cmd) {
139
139
  // src/lib/auth.ts
140
140
  import { createServer } from "http";
141
141
  import { randomBytes, createHash } from "crypto";
142
- import { URL } from "url";
142
+ import { URL as URL2 } from "url";
143
143
  import * as clack from "@clack/prompts";
144
144
  var DEFAULT_CLIENT_ID = "clf_NK8cMUs41gm8ZcfdtSguVw";
145
145
  var OAUTH_SCOPES = "user:read organizations:read projects:read projects:write";
@@ -152,7 +152,7 @@ function generateState() {
152
152
  return randomBytes(16).toString("base64url");
153
153
  }
154
154
  function buildAuthorizeUrl(params) {
155
- const url = new URL(`${params.platformUrl}/api/oauth/v1/authorize`);
155
+ const url = new URL2(`${params.platformUrl}/api/oauth/v1/authorize`);
156
156
  url.searchParams.set("client_id", params.clientId);
157
157
  url.searchParams.set("redirect_uri", params.redirectUri);
158
158
  url.searchParams.set("response_type", "code");
@@ -205,7 +205,7 @@ function startCallbackServer() {
205
205
  rejectResult = reject;
206
206
  });
207
207
  const server = createServer((req, res) => {
208
- const url = new URL(req.url ?? "/", "http://localhost");
208
+ const url = new URL2(req.url ?? "/", "http://localhost");
209
209
  if (url.pathname === "/callback") {
210
210
  const code = url.searchParams.get("code");
211
211
  const state = url.searchParams.get("state");
@@ -314,7 +314,21 @@ ${authUrl}`);
314
314
 
315
315
  // src/lib/credentials.ts
316
316
  import * as clack2 from "@clack/prompts";
317
- async function requireAuth(apiUrl) {
317
+ async function requireAuth(apiUrl, allowOssBypass = true) {
318
+ const projConfig = getProjectConfig();
319
+ if (allowOssBypass && projConfig?.project_id === "oss-project") {
320
+ return {
321
+ access_token: "oss-token",
322
+ refresh_token: "oss-refresh",
323
+ user: {
324
+ id: "oss-user",
325
+ name: "OSS User",
326
+ email: "oss@insforge.local",
327
+ avatar_url: null,
328
+ email_verified: true
329
+ }
330
+ };
331
+ }
318
332
  const creds = getCredentials();
319
333
  if (creds && creds.access_token) return creds;
320
334
  clack2.log.info("You need to log in to continue.");
@@ -779,10 +793,45 @@ function buildOssHost(appkey, region) {
779
793
  return `https://${appkey}.${region}.insforge.app`;
780
794
  }
781
795
  function registerProjectLinkCommand(program2) {
782
- 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").action(async (opts, cmd) => {
796
+ 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("--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) => {
783
797
  const { json, apiUrl } = getRootOpts(cmd);
784
798
  try {
785
- await requireAuth(apiUrl);
799
+ if (opts.apiBaseUrl || opts.apiKey) {
800
+ try {
801
+ if (!opts.apiBaseUrl || !opts.apiKey) {
802
+ throw new CLIError("Both --api-base-url and --api-key must be provided together for direct linking.");
803
+ }
804
+ try {
805
+ new URL(opts.apiBaseUrl);
806
+ } catch {
807
+ throw new CLIError("Invalid --api-base-url. Please provide a valid URL.");
808
+ }
809
+ const projectConfig2 = {
810
+ project_id: "oss-project",
811
+ project_name: "oss-project",
812
+ org_id: "oss-org",
813
+ appkey: "oss",
814
+ region: "local",
815
+ api_key: opts.apiKey,
816
+ oss_host: opts.apiBaseUrl.replace(/\/$/, "")
817
+ // remove trailing slash if any
818
+ };
819
+ saveProjectConfig(projectConfig2);
820
+ if (json) {
821
+ outputJson({ success: true, project: { id: projectConfig2.project_id, name: projectConfig2.project_name, region: projectConfig2.region } });
822
+ } else {
823
+ outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
824
+ }
825
+ await installCliGlobally(json);
826
+ await installSkills(json);
827
+ await reportCliUsage("cli.link_direct", true, 6);
828
+ return;
829
+ } catch (err) {
830
+ await reportCliUsage("cli.link_direct", false);
831
+ handleError(err, json);
832
+ }
833
+ }
834
+ await requireAuth(apiUrl, false);
786
835
  let orgId = opts.orgId;
787
836
  let projectId = opts.projectId;
788
837
  if (!orgId && !projectId) {
@@ -861,6 +910,16 @@ function requireProjectConfig() {
861
910
  }
862
911
  return config;
863
912
  }
913
+ async function runRawSql(sql, unrestricted = false) {
914
+ const endpoint = unrestricted ? "/api/database/advance/rawsql/unrestricted" : "/api/database/advance/rawsql";
915
+ const res = await ossFetch(endpoint, {
916
+ method: "POST",
917
+ body: JSON.stringify({ query: sql })
918
+ });
919
+ const raw = await res.json();
920
+ const rows = raw.rows ?? raw.data ?? [];
921
+ return { rows, raw };
922
+ }
864
923
  async function getAnonKey() {
865
924
  const res = await ossFetch("/api/auth/tokens/anon", { method: "POST" });
866
925
  const data = await res.json();
@@ -887,17 +946,11 @@ function registerDbCommands(dbCmd2) {
887
946
  const { json } = getRootOpts(cmd);
888
947
  try {
889
948
  await requireAuth();
890
- const endpoint = opts.unrestricted ? "/api/database/advance/rawsql/unrestricted" : "/api/database/advance/rawsql";
891
- const res = await ossFetch(endpoint, {
892
- method: "POST",
893
- body: JSON.stringify({ query: sql })
894
- });
895
- const data = await res.json();
949
+ const { rows, raw } = await runRawSql(sql, !!opts.unrestricted);
896
950
  if (json) {
897
- outputJson(data);
951
+ outputJson(raw);
898
952
  } else {
899
- const rows = data.rows ?? data.data ?? null;
900
- if (rows && rows.length > 0) {
953
+ if (rows.length > 0) {
901
954
  const headers = Object.keys(rows[0]);
902
955
  outputTable(
903
956
  headers,
@@ -906,7 +959,7 @@ function registerDbCommands(dbCmd2) {
906
959
  console.log(`${rows.length} row(s) returned.`);
907
960
  } else {
908
961
  console.log("Query executed successfully.");
909
- if (rows && rows.length === 0) {
962
+ if (rows.length === 0) {
910
963
  console.log("No rows returned.");
911
964
  }
912
965
  }
@@ -1996,10 +2049,10 @@ async function copyDir(src, dest) {
1996
2049
  }
1997
2050
  }
1998
2051
  function registerCreateCommand(program2) {
1999
- 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, or empty").action(async (opts, cmd) => {
2052
+ 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) => {
2000
2053
  const { json, apiUrl } = getRootOpts(cmd);
2001
2054
  try {
2002
- await requireAuth(apiUrl);
2055
+ await requireAuth(apiUrl, false);
2003
2056
  if (!json) {
2004
2057
  clack10.intro("Create a new InsForge project");
2005
2058
  }
@@ -2035,7 +2088,7 @@ function registerCreateCommand(program2) {
2035
2088
  if (clack10.isCancel(name)) process.exit(0);
2036
2089
  projectName = name;
2037
2090
  }
2038
- const validTemplates = ["react", "nextjs", "chatbot", "empty"];
2091
+ const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "empty"];
2039
2092
  let template = opts.template;
2040
2093
  if (template && !validTemplates.includes(template)) {
2041
2094
  throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
@@ -2050,6 +2103,8 @@ function registerCreateCommand(program2) {
2050
2103
  { value: "react", label: "Web app template with React" },
2051
2104
  { value: "nextjs", label: "Web app template with Next.js" },
2052
2105
  { value: "chatbot", label: "AI Chatbot with Next.js" },
2106
+ { value: "crm", label: "CRM with Next.js" },
2107
+ { value: "e-commerce", label: "E-Commerce store with Next.js" },
2053
2108
  { value: "empty", label: "Empty project" }
2054
2109
  ]
2055
2110
  });
@@ -2075,8 +2130,9 @@ function registerCreateCommand(program2) {
2075
2130
  saveProjectConfig(projectConfig);
2076
2131
  s?.stop(`Project "${project.name}" created and linked`);
2077
2132
  const hasTemplate = template !== "empty";
2078
- if (template === "chatbot") {
2079
- await downloadGitHubTemplate("chatbot", projectConfig, json);
2133
+ const githubTemplates = ["chatbot", "crm", "e-commerce"];
2134
+ if (githubTemplates.includes(template)) {
2135
+ await downloadGitHubTemplate(template, projectConfig, json);
2080
2136
  } else if (hasTemplate) {
2081
2137
  await downloadTemplate(template, projectConfig, projectName, json, apiUrl);
2082
2138
  }
@@ -2168,8 +2224,8 @@ async function downloadTemplate(framework, projectConfig, projectName, json, _ap
2168
2224
  } catch {
2169
2225
  }
2170
2226
  const frame = framework === "nextjs" ? "nextjs" : "react";
2171
- const esc = (s2) => `'${s2.replace(/'/g, "'\\''")}'`;
2172
- const command = `npx create-insforge-app ${esc(targetDir)} --frame ${frame} --base-url ${esc(projectConfig.oss_host)} --anon-key ${esc(anonKey)} --skip-install`;
2227
+ const esc = (s2) => process.platform === "win32" ? `"${s2.replace(/"/g, '\\"')}"` : `'${s2.replace(/'/g, "'\\''")}'`;
2228
+ const command = `npx --yes create-insforge-app@latest ${esc(targetDir)} --frame ${frame} --base-url ${esc(projectConfig.oss_host)} --anon-key ${esc(anonKey)} --skip-install`;
2173
2229
  s?.message(`Running create-insforge-app (${frame})...`);
2174
2230
  await execAsync2(command, {
2175
2231
  maxBuffer: 10 * 1024 * 1024,
@@ -2218,6 +2274,7 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
2218
2274
  const key = prefix.slice(0, -1);
2219
2275
  if (/INSFORGE.*(URL|BASE_URL)$/.test(key)) return `${prefix}${projectConfig.oss_host}`;
2220
2276
  if (/INSFORGE.*ANON_KEY$/.test(key)) return `${prefix}${anonKey}`;
2277
+ if (key === "NEXT_PUBLIC_APP_URL") return `${prefix}https://${projectConfig.appkey}.insforge.site`;
2221
2278
  return `${prefix}${_value}`;
2222
2279
  }
2223
2280
  );
@@ -2227,20 +2284,28 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
2227
2284
  const migrationPath = path3.join(cwd, "migrations", "db_int.sql");
2228
2285
  const migrationExists = await fs3.stat(migrationPath).catch(() => null);
2229
2286
  if (migrationExists) {
2230
- const dbSpinner = !json ? clack10.spinner() : null;
2231
- dbSpinner?.start("Running database migrations...");
2232
- try {
2233
- const sql = await fs3.readFile(migrationPath, "utf-8");
2234
- await ossFetch("/api/database/advance/rawsql/unrestricted", {
2235
- method: "POST",
2236
- body: JSON.stringify({ query: sql })
2287
+ let shouldRun = json;
2288
+ if (!json) {
2289
+ const runMigration = await clack10.confirm({
2290
+ message: "This template includes a database migration. Apply it now?"
2237
2291
  });
2238
- dbSpinner?.stop("Database migrations applied");
2239
- } catch (err) {
2240
- dbSpinner?.stop("Database migration failed");
2241
- if (!json) {
2242
- clack10.log.warn(`Migration failed: ${err.message}`);
2243
- clack10.log.info('You can run the migration manually: insforge db query --unrestricted "$(cat migrations/db_int.sql)"');
2292
+ shouldRun = !clack10.isCancel(runMigration) && runMigration;
2293
+ }
2294
+ if (shouldRun) {
2295
+ const dbSpinner = !json ? clack10.spinner() : null;
2296
+ dbSpinner?.start("Running database migrations...");
2297
+ try {
2298
+ const sql = await fs3.readFile(migrationPath, "utf-8");
2299
+ await runRawSql(sql, true);
2300
+ dbSpinner?.stop("Database migrations applied");
2301
+ } catch (err) {
2302
+ dbSpinner?.stop("Database migration failed");
2303
+ if (!json) {
2304
+ clack10.log.warn(`Migration failed: ${err.message}`);
2305
+ clack10.log.info('You can run the migration manually: insforge db query --unrestricted "$(cat migrations/db_int.sql)"');
2306
+ } else {
2307
+ throw err;
2308
+ }
2244
2309
  }
2245
2310
  }
2246
2311
  }