@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 +32 -0
- package/dist/index.js +101 -36
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
951
|
+
outputJson(raw);
|
|
898
952
|
} else {
|
|
899
|
-
|
|
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
|
|
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
|
-
|
|
2079
|
-
|
|
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
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
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
|
-
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
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
|
}
|