@insforge/cli 0.1.54 → 0.1.57
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 +560 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
5
|
+
import { join as join12, dirname } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import * as clack11 from "@clack/prompts";
|
|
@@ -1211,6 +1211,9 @@ ${err.nextActions}`;
|
|
|
1211
1211
|
if (res.status === 404 && path5.startsWith("/api/compute")) {
|
|
1212
1212
|
message = "Compute services are not available on this backend.\nSelf-hosted: upgrade your InsForge instance. Cloud: contact your InsForge admin to enable compute.";
|
|
1213
1213
|
}
|
|
1214
|
+
if (res.status === 404 && path5 === "/api/database/migrations") {
|
|
1215
|
+
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
|
+
}
|
|
1214
1217
|
throw new CLIError(message);
|
|
1215
1218
|
}
|
|
1216
1219
|
return res;
|
|
@@ -2128,7 +2131,11 @@ function buildOssHost2(appkey, region) {
|
|
|
2128
2131
|
function registerProjectLinkCommand(program2) {
|
|
2129
2132
|
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) => {
|
|
2130
2133
|
const { json, apiUrl } = getRootOpts(cmd);
|
|
2134
|
+
const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo"];
|
|
2131
2135
|
try {
|
|
2136
|
+
if (opts.template && !validTemplates.includes(opts.template)) {
|
|
2137
|
+
throw new CLIError(`Invalid template "${opts.template}". Valid options: ${validTemplates.join(", ")}`);
|
|
2138
|
+
}
|
|
2132
2139
|
if (opts.apiBaseUrl || opts.apiKey) {
|
|
2133
2140
|
try {
|
|
2134
2141
|
if (!opts.apiBaseUrl || !opts.apiKey) {
|
|
@@ -2149,6 +2156,84 @@ function registerProjectLinkCommand(program2) {
|
|
|
2149
2156
|
oss_host: opts.apiBaseUrl.replace(/\/$/, "")
|
|
2150
2157
|
// remove trailing slash if any
|
|
2151
2158
|
};
|
|
2159
|
+
const template2 = opts.template;
|
|
2160
|
+
if (template2) {
|
|
2161
|
+
const defaultDir = `insforge-${template2}`;
|
|
2162
|
+
let dirName = defaultDir;
|
|
2163
|
+
if (!json) {
|
|
2164
|
+
const inputDir = await text2({
|
|
2165
|
+
message: "Directory name:",
|
|
2166
|
+
initialValue: defaultDir,
|
|
2167
|
+
validate: (v) => {
|
|
2168
|
+
if (v.length < 1) return "Directory name is required";
|
|
2169
|
+
const normalized = path4.basename(v).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2170
|
+
if (!normalized || normalized === "." || normalized === "..") return "Invalid directory name";
|
|
2171
|
+
return void 0;
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
if (isCancel2(inputDir)) process.exit(0);
|
|
2175
|
+
dirName = path4.basename(inputDir).replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
2176
|
+
}
|
|
2177
|
+
if (!dirName || dirName === "." || dirName === "..") {
|
|
2178
|
+
throw new CLIError("Invalid directory name.");
|
|
2179
|
+
}
|
|
2180
|
+
const templateDir = path4.resolve(process.cwd(), dirName);
|
|
2181
|
+
const dirExists = await fs4.stat(templateDir).catch(() => null);
|
|
2182
|
+
if (dirExists) {
|
|
2183
|
+
throw new CLIError(`Directory "${dirName}" already exists.`);
|
|
2184
|
+
}
|
|
2185
|
+
await fs4.mkdir(templateDir);
|
|
2186
|
+
process.chdir(templateDir);
|
|
2187
|
+
saveProjectConfig(projectConfig2);
|
|
2188
|
+
if (json) {
|
|
2189
|
+
outputJson({
|
|
2190
|
+
success: true,
|
|
2191
|
+
project: { id: projectConfig2.project_id, name: projectConfig2.project_name, region: projectConfig2.region },
|
|
2192
|
+
directory: dirName,
|
|
2193
|
+
template: template2
|
|
2194
|
+
});
|
|
2195
|
+
} else {
|
|
2196
|
+
outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
|
|
2197
|
+
}
|
|
2198
|
+
captureEvent(FAKE_ORG_ID, "template_selected", { template: template2, source: "link_direct" });
|
|
2199
|
+
await downloadGitHubTemplate(template2, projectConfig2, json);
|
|
2200
|
+
const templateDownloaded = await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null);
|
|
2201
|
+
if (templateDownloaded && !json) {
|
|
2202
|
+
const installSpinner = clack8.spinner();
|
|
2203
|
+
installSpinner.start("Installing dependencies...");
|
|
2204
|
+
try {
|
|
2205
|
+
await execAsync3("npm install", { cwd: process.cwd(), maxBuffer: 10 * 1024 * 1024 });
|
|
2206
|
+
installSpinner.stop("Dependencies installed");
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
installSpinner.stop("Failed to install dependencies");
|
|
2209
|
+
clack8.log.warn(`npm install failed: ${err.message}`);
|
|
2210
|
+
clack8.log.info("Run `npm install` manually to install dependencies.");
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
await installSkills(json);
|
|
2214
|
+
trackCommand("link", "oss-org", { direct: true, template: template2 });
|
|
2215
|
+
await reportCliUsage("cli.link_direct", true, 6, projectConfig2);
|
|
2216
|
+
try {
|
|
2217
|
+
const urlMatch = opts.apiBaseUrl.match(/^https?:\/\/([^.]+)\.[^.]+\.insforge\.app/);
|
|
2218
|
+
if (urlMatch) {
|
|
2219
|
+
await reportAgentConnected({ app_key: urlMatch[1] }, apiUrl);
|
|
2220
|
+
}
|
|
2221
|
+
} catch {
|
|
2222
|
+
}
|
|
2223
|
+
if (!json) {
|
|
2224
|
+
if (templateDownloaded) {
|
|
2225
|
+
const runCommand = `${pc2.cyan("cd")} ${pc2.green(dirName)} ${pc2.dim("&&")} ${pc2.cyan("npm run dev")}`;
|
|
2226
|
+
const steps = [
|
|
2227
|
+
`${pc2.bold("1.")} ${runCommand}`,
|
|
2228
|
+
`${pc2.bold("2.")} Open ${pc2.cyan("Claude Code")} or ${pc2.cyan("Cursor")} and prompt your agent to add more features`
|
|
2229
|
+
];
|
|
2230
|
+
clack8.note(steps.join("\n"), "What's next");
|
|
2231
|
+
} else {
|
|
2232
|
+
clack8.log.warn("Template download failed. You can retry or set up manually.");
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2152
2237
|
saveProjectConfig(projectConfig2);
|
|
2153
2238
|
if (json) {
|
|
2154
2239
|
outputJson({ success: true, project: { id: projectConfig2.project_id, name: projectConfig2.project_name, region: projectConfig2.region } });
|
|
@@ -2261,10 +2346,6 @@ function registerProjectLinkCommand(program2) {
|
|
|
2261
2346
|
}
|
|
2262
2347
|
const template = opts.template;
|
|
2263
2348
|
if (template) {
|
|
2264
|
-
const validTemplates = ["react", "nextjs", "chatbot", "crm", "e-commerce", "todo"];
|
|
2265
|
-
if (!validTemplates.includes(template)) {
|
|
2266
|
-
throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
|
|
2267
|
-
}
|
|
2268
2349
|
let dirName = project.name;
|
|
2269
2350
|
if (!json) {
|
|
2270
2351
|
const inputDir = await text2({
|
|
@@ -2292,12 +2373,7 @@ function registerProjectLinkCommand(program2) {
|
|
|
2292
2373
|
process.chdir(templateDir);
|
|
2293
2374
|
saveProjectConfig(projectConfig);
|
|
2294
2375
|
captureEvent(orgId ?? project.organization_id, "template_selected", { template, source: "link" });
|
|
2295
|
-
|
|
2296
|
-
if (githubTemplates.includes(template)) {
|
|
2297
|
-
await downloadGitHubTemplate(template, projectConfig, json);
|
|
2298
|
-
} else {
|
|
2299
|
-
await downloadTemplate(template, projectConfig, project.name, json, apiUrl);
|
|
2300
|
-
}
|
|
2376
|
+
await downloadGitHubTemplate(template, projectConfig, json);
|
|
2301
2377
|
const templateDownloaded = await fs4.stat(path4.join(process.cwd(), "package.json")).catch(() => null);
|
|
2302
2378
|
if (templateDownloaded && !json) {
|
|
2303
2379
|
const installSpinner = clack8.spinner();
|
|
@@ -2671,6 +2747,451 @@ function registerDbImportCommand(dbCmd2) {
|
|
|
2671
2747
|
});
|
|
2672
2748
|
}
|
|
2673
2749
|
|
|
2750
|
+
// src/commands/db/migrations.ts
|
|
2751
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
2752
|
+
import { join as join8 } from "path";
|
|
2753
|
+
|
|
2754
|
+
// src/lib/migrations.ts
|
|
2755
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
2756
|
+
import { join as join7 } from "path";
|
|
2757
|
+
var MIGRATION_VERSION_REGEX = /^\d{14}$/u;
|
|
2758
|
+
var MIGRATION_FILENAME_REGEX = /^(\d{14})_([a-z0-9-]+)\.sql$/u;
|
|
2759
|
+
function assertValidMigrationVersion(version) {
|
|
2760
|
+
if (!MIGRATION_VERSION_REGEX.test(version)) {
|
|
2761
|
+
throw new CLIError(`Invalid migration version: ${version}. Expected YYYYMMDDHHmmss.`);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
function parseMigrationFilename(filename) {
|
|
2765
|
+
const match = MIGRATION_FILENAME_REGEX.exec(filename);
|
|
2766
|
+
if (!match) {
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2769
|
+
return {
|
|
2770
|
+
filename,
|
|
2771
|
+
version: match[1],
|
|
2772
|
+
name: match[2]
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
function compareMigrationVersions(left, right) {
|
|
2776
|
+
return left.localeCompare(right);
|
|
2777
|
+
}
|
|
2778
|
+
function getRemoteMigrationVersionStatus(version, appliedVersions, latestRemoteVersion) {
|
|
2779
|
+
if (appliedVersions.has(version)) {
|
|
2780
|
+
return "already-applied";
|
|
2781
|
+
}
|
|
2782
|
+
if (latestRemoteVersion && compareMigrationVersions(version, latestRemoteVersion) < 0) {
|
|
2783
|
+
return "older-than-head";
|
|
2784
|
+
}
|
|
2785
|
+
return "pending";
|
|
2786
|
+
}
|
|
2787
|
+
function formatMigrationVersion(date) {
|
|
2788
|
+
const year = String(date.getUTCFullYear());
|
|
2789
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
2790
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
2791
|
+
const hour = String(date.getUTCHours()).padStart(2, "0");
|
|
2792
|
+
const minute = String(date.getUTCMinutes()).padStart(2, "0");
|
|
2793
|
+
const second = String(date.getUTCSeconds()).padStart(2, "0");
|
|
2794
|
+
return `${year}${month}${day}${hour}${minute}${second}`;
|
|
2795
|
+
}
|
|
2796
|
+
function incrementMigrationVersion(version) {
|
|
2797
|
+
const year = Number(version.slice(0, 4));
|
|
2798
|
+
const month = Number(version.slice(4, 6)) - 1;
|
|
2799
|
+
const day = Number(version.slice(6, 8));
|
|
2800
|
+
const hour = Number(version.slice(8, 10));
|
|
2801
|
+
const minute = Number(version.slice(10, 12));
|
|
2802
|
+
const second = Number(version.slice(12, 14));
|
|
2803
|
+
const nextTimestamp = Date.UTC(year, month, day, hour, minute, second + 1);
|
|
2804
|
+
return formatMigrationVersion(new Date(nextTimestamp));
|
|
2805
|
+
}
|
|
2806
|
+
function getMigrationsDir(cwd = process.cwd()) {
|
|
2807
|
+
return join7(cwd, "migrations");
|
|
2808
|
+
}
|
|
2809
|
+
function ensureMigrationsDir(cwd = process.cwd()) {
|
|
2810
|
+
const migrationsDir = getMigrationsDir(cwd);
|
|
2811
|
+
if (!existsSync3(migrationsDir)) {
|
|
2812
|
+
mkdirSync2(migrationsDir, { recursive: true });
|
|
2813
|
+
}
|
|
2814
|
+
return migrationsDir;
|
|
2815
|
+
}
|
|
2816
|
+
function listLocalMigrationFilenames(cwd = process.cwd()) {
|
|
2817
|
+
const migrationsDir = getMigrationsDir(cwd);
|
|
2818
|
+
if (!existsSync3(migrationsDir)) {
|
|
2819
|
+
return [];
|
|
2820
|
+
}
|
|
2821
|
+
return readdirSync(migrationsDir).sort((left, right) => left.localeCompare(right));
|
|
2822
|
+
}
|
|
2823
|
+
function parseStrictLocalMigrations(filenames) {
|
|
2824
|
+
const migrations = filenames.map((filename) => {
|
|
2825
|
+
const parsedMigration = parseMigrationFilename(filename);
|
|
2826
|
+
if (!parsedMigration) {
|
|
2827
|
+
throw new CLIError(
|
|
2828
|
+
`Invalid migration filename: ${filename}. Expected <migration_version>_<migration-name>.sql.`
|
|
2829
|
+
);
|
|
2830
|
+
}
|
|
2831
|
+
return parsedMigration;
|
|
2832
|
+
});
|
|
2833
|
+
assertNoDuplicateMigrationVersions(migrations);
|
|
2834
|
+
return migrations.sort((left, right) => compareMigrationVersions(left.version, right.version));
|
|
2835
|
+
}
|
|
2836
|
+
function assertNoDuplicateMigrationVersions(migrations) {
|
|
2837
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2838
|
+
for (const migration of migrations) {
|
|
2839
|
+
if (seen.has(migration.version)) {
|
|
2840
|
+
throw new CLIError(`Duplicate local migration version found: ${migration.version}`);
|
|
2841
|
+
}
|
|
2842
|
+
seen.add(migration.version);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function getNextLocalMigrationVersion(migrations, latestRemoteVersion, now = /* @__PURE__ */ new Date()) {
|
|
2846
|
+
const orderedMigrations = [...migrations].sort(
|
|
2847
|
+
(left, right) => compareMigrationVersions(left.version, right.version)
|
|
2848
|
+
);
|
|
2849
|
+
assertNoDuplicateMigrationVersions(orderedMigrations);
|
|
2850
|
+
const latestKnownVersion = orderedMigrations.reduce(
|
|
2851
|
+
(latestVersion, migration) => {
|
|
2852
|
+
if (!latestVersion || compareMigrationVersions(migration.version, latestVersion) > 0) {
|
|
2853
|
+
return migration.version;
|
|
2854
|
+
}
|
|
2855
|
+
return latestVersion;
|
|
2856
|
+
},
|
|
2857
|
+
latestRemoteVersion
|
|
2858
|
+
);
|
|
2859
|
+
const currentVersion = formatMigrationVersion(now);
|
|
2860
|
+
if (!latestKnownVersion || compareMigrationVersions(currentVersion, latestKnownVersion) > 0) {
|
|
2861
|
+
return currentVersion;
|
|
2862
|
+
}
|
|
2863
|
+
return incrementMigrationVersion(latestKnownVersion);
|
|
2864
|
+
}
|
|
2865
|
+
function formatMigrationSql(statements) {
|
|
2866
|
+
const normalizedStatements = statements.map((statement) => statement.trim().replace(/;\s*$/u, "")).filter(Boolean);
|
|
2867
|
+
return normalizedStatements.join(";\n\n").concat(normalizedStatements.length > 0 ? ";\n" : "");
|
|
2868
|
+
}
|
|
2869
|
+
function findOlderThanHeadLocalMigrations(migrations, appliedVersions, latestRemoteVersion) {
|
|
2870
|
+
return migrations.filter(
|
|
2871
|
+
(migration) => getRemoteMigrationVersionStatus(
|
|
2872
|
+
migration.version,
|
|
2873
|
+
appliedVersions,
|
|
2874
|
+
latestRemoteVersion
|
|
2875
|
+
) === "older-than-head"
|
|
2876
|
+
);
|
|
2877
|
+
}
|
|
2878
|
+
function findLocalMigrationByVersion(version, filenames) {
|
|
2879
|
+
const matches = filenames.map((filename) => parseMigrationFilename(filename)).filter(
|
|
2880
|
+
(migration) => migration !== null && migration.version === version
|
|
2881
|
+
);
|
|
2882
|
+
if (matches.length === 0) {
|
|
2883
|
+
throw new CLIError(`Local migration for version ${version} not found.`);
|
|
2884
|
+
}
|
|
2885
|
+
if (matches.length > 1) {
|
|
2886
|
+
throw new CLIError(
|
|
2887
|
+
`Multiple local migration files found for version ${version}.`
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
return matches[0];
|
|
2891
|
+
}
|
|
2892
|
+
function resolveMigrationTarget(target, filenames) {
|
|
2893
|
+
if (/^\d{14}$/u.test(target)) {
|
|
2894
|
+
return findLocalMigrationByVersion(target, filenames);
|
|
2895
|
+
}
|
|
2896
|
+
const parsedTarget = parseMigrationFilename(target);
|
|
2897
|
+
if (!parsedTarget) {
|
|
2898
|
+
throw new CLIError(
|
|
2899
|
+
"Migration file names must match <migration_version>_<migration-name>.sql."
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
if (!filenames.includes(target)) {
|
|
2903
|
+
throw new CLIError(`Local migration file not found: ${target}`);
|
|
2904
|
+
}
|
|
2905
|
+
return parsedTarget;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// src/commands/db/migrations.ts
|
|
2909
|
+
function getLatestRemoteVersion(migrations) {
|
|
2910
|
+
return migrations.reduce(
|
|
2911
|
+
(latestVersion, migration) => !latestVersion || compareMigrationVersions(migration.version, latestVersion) > 0 ? migration.version : latestVersion,
|
|
2912
|
+
null
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
function buildMigrationFilename(version, name) {
|
|
2916
|
+
return `${version}_${name}.sql`;
|
|
2917
|
+
}
|
|
2918
|
+
function buildOlderThanHeadError(migrationLabel, latestRemoteVersion) {
|
|
2919
|
+
return new CLIError(
|
|
2920
|
+
`Migration ${migrationLabel} is older than the current remote head (${latestRemoteVersion}) and is not applied remotely. Rename it with a newer timestamp, or delete it locally if it is stale.`
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
function formatCreatedAt(createdAt) {
|
|
2924
|
+
const date = new Date(createdAt);
|
|
2925
|
+
return Number.isNaN(date.getTime()) ? createdAt : date.toLocaleString();
|
|
2926
|
+
}
|
|
2927
|
+
async function fetchRemoteMigrations() {
|
|
2928
|
+
const res = await ossFetch("/api/database/migrations");
|
|
2929
|
+
const raw = await res.json();
|
|
2930
|
+
const migrations = Array.isArray(raw.migrations) ? raw.migrations : [];
|
|
2931
|
+
for (const migration of migrations) {
|
|
2932
|
+
assertValidMigrationVersion(migration.version);
|
|
2933
|
+
}
|
|
2934
|
+
return migrations;
|
|
2935
|
+
}
|
|
2936
|
+
function assertValidMigrationName(name) {
|
|
2937
|
+
if (!/^[a-z0-9-]+$/u.test(name)) {
|
|
2938
|
+
throw new CLIError("Migration name must use lowercase letters, numbers, and hyphens only.");
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
async function applyMigration(targetMigration, sql) {
|
|
2942
|
+
const body = {
|
|
2943
|
+
version: targetMigration.version,
|
|
2944
|
+
name: targetMigration.name,
|
|
2945
|
+
sql
|
|
2946
|
+
};
|
|
2947
|
+
const res = await ossFetch("/api/database/migrations", {
|
|
2948
|
+
method: "POST",
|
|
2949
|
+
body: JSON.stringify(body)
|
|
2950
|
+
});
|
|
2951
|
+
const createdMigration = await res.json();
|
|
2952
|
+
if (createdMigration.version !== targetMigration.version) {
|
|
2953
|
+
throw new CLIError(
|
|
2954
|
+
`Applied migration version mismatch. Expected ${targetMigration.version}, received ${createdMigration.version}.`
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
return createdMigration;
|
|
2958
|
+
}
|
|
2959
|
+
function registerDbMigrationsCommand(dbCmd2) {
|
|
2960
|
+
const migrationsCmd = dbCmd2.command("migrations").description("Manage database migration files");
|
|
2961
|
+
migrationsCmd.command("list").description("List applied remote database migrations").action(async (_opts, cmd) => {
|
|
2962
|
+
const { json } = getRootOpts(cmd);
|
|
2963
|
+
try {
|
|
2964
|
+
await requireAuth();
|
|
2965
|
+
const migrations = await fetchRemoteMigrations();
|
|
2966
|
+
if (json) {
|
|
2967
|
+
outputJson({ migrations });
|
|
2968
|
+
} else if (migrations.length === 0) {
|
|
2969
|
+
console.log("No database migrations found.");
|
|
2970
|
+
} else {
|
|
2971
|
+
outputTable(
|
|
2972
|
+
["Version", "Name", "Created At"],
|
|
2973
|
+
migrations.map((migration) => [
|
|
2974
|
+
migration.version,
|
|
2975
|
+
migration.name,
|
|
2976
|
+
formatCreatedAt(migration.createdAt)
|
|
2977
|
+
])
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
await reportCliUsage("cli.db.migrations.list", true);
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
await reportCliUsage("cli.db.migrations.list", false);
|
|
2983
|
+
handleError(err, json);
|
|
2984
|
+
}
|
|
2985
|
+
});
|
|
2986
|
+
migrationsCmd.command("fetch").description("Fetch applied remote migrations into migrations/").action(async (_opts, cmd) => {
|
|
2987
|
+
const { json } = getRootOpts(cmd);
|
|
2988
|
+
try {
|
|
2989
|
+
await requireAuth();
|
|
2990
|
+
const migrations = await fetchRemoteMigrations();
|
|
2991
|
+
const migrationsDir = ensureMigrationsDir();
|
|
2992
|
+
const createdFiles = [];
|
|
2993
|
+
const skippedFiles = [];
|
|
2994
|
+
for (const migration of [...migrations].sort(
|
|
2995
|
+
(left, right) => compareMigrationVersions(left.version, right.version)
|
|
2996
|
+
)) {
|
|
2997
|
+
assertValidMigrationName(migration.name);
|
|
2998
|
+
const filename = buildMigrationFilename(
|
|
2999
|
+
migration.version,
|
|
3000
|
+
migration.name
|
|
3001
|
+
);
|
|
3002
|
+
const filePath = join8(migrationsDir, filename);
|
|
3003
|
+
if (existsSync4(filePath)) {
|
|
3004
|
+
skippedFiles.push(filename);
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
3007
|
+
writeFileSync3(filePath, formatMigrationSql(migration.statements));
|
|
3008
|
+
createdFiles.push(filename);
|
|
3009
|
+
}
|
|
3010
|
+
if (json) {
|
|
3011
|
+
outputJson({
|
|
3012
|
+
directory: migrationsDir,
|
|
3013
|
+
totalRemoteMigrations: migrations.length,
|
|
3014
|
+
createdFiles,
|
|
3015
|
+
skippedFiles
|
|
3016
|
+
});
|
|
3017
|
+
} else {
|
|
3018
|
+
outputSuccess(
|
|
3019
|
+
`Fetched ${migrations.length} remote migration(s) into ${migrationsDir}.`
|
|
3020
|
+
);
|
|
3021
|
+
console.log(`Created: ${createdFiles.length}`);
|
|
3022
|
+
console.log(`Skipped: ${skippedFiles.length}`);
|
|
3023
|
+
}
|
|
3024
|
+
await reportCliUsage("cli.db.migrations.fetch", true);
|
|
3025
|
+
} catch (err) {
|
|
3026
|
+
await reportCliUsage("cli.db.migrations.fetch", false);
|
|
3027
|
+
handleError(err, json);
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
migrationsCmd.command("new <migration-name>").description("Create a new local migration file").action(async (migrationName, _opts, cmd) => {
|
|
3031
|
+
const { json } = getRootOpts(cmd);
|
|
3032
|
+
try {
|
|
3033
|
+
await requireAuth();
|
|
3034
|
+
assertValidMigrationName(migrationName);
|
|
3035
|
+
const migrations = await fetchRemoteMigrations();
|
|
3036
|
+
const latestRemoteVersion = getLatestRemoteVersion(migrations);
|
|
3037
|
+
const localMigrations = parseStrictLocalMigrations(listLocalMigrationFilenames());
|
|
3038
|
+
const nextVersion = getNextLocalMigrationVersion(
|
|
3039
|
+
localMigrations,
|
|
3040
|
+
latestRemoteVersion
|
|
3041
|
+
);
|
|
3042
|
+
const filename = buildMigrationFilename(nextVersion, migrationName);
|
|
3043
|
+
const migrationsDir = ensureMigrationsDir();
|
|
3044
|
+
const filePath = join8(migrationsDir, filename);
|
|
3045
|
+
try {
|
|
3046
|
+
writeFileSync3(filePath, "", { flag: "wx" });
|
|
3047
|
+
} catch (error) {
|
|
3048
|
+
if (error.code === "EEXIST") {
|
|
3049
|
+
throw new CLIError(`Migration file already exists: ${filename}`);
|
|
3050
|
+
}
|
|
3051
|
+
throw error;
|
|
3052
|
+
}
|
|
3053
|
+
if (json) {
|
|
3054
|
+
outputJson({ filename, path: filePath, version: nextVersion });
|
|
3055
|
+
} else {
|
|
3056
|
+
outputSuccess(`Created migration file ${filename}`);
|
|
3057
|
+
}
|
|
3058
|
+
await reportCliUsage("cli.db.migrations.new", true);
|
|
3059
|
+
} catch (err) {
|
|
3060
|
+
await reportCliUsage("cli.db.migrations.new", false);
|
|
3061
|
+
handleError(err, json);
|
|
3062
|
+
}
|
|
3063
|
+
});
|
|
3064
|
+
migrationsCmd.command("up [target]").description("Apply one or more local migration files").option("--all", "Apply all pending local migration files").option("--to <version-or-filename>", "Apply pending local migrations up to a version or file").action(async (target, options, cmd) => {
|
|
3065
|
+
const { json } = getRootOpts(cmd);
|
|
3066
|
+
try {
|
|
3067
|
+
await requireAuth();
|
|
3068
|
+
const migrations = await fetchRemoteMigrations();
|
|
3069
|
+
const latestRemoteVersion = getLatestRemoteVersion(migrations);
|
|
3070
|
+
const appliedRemoteVersions = new Set(
|
|
3071
|
+
migrations.map((migration) => migration.version)
|
|
3072
|
+
);
|
|
3073
|
+
const filenames = listLocalMigrationFilenames();
|
|
3074
|
+
const requestedModes = [Boolean(target), Boolean(options.all), Boolean(options.to)].filter(Boolean);
|
|
3075
|
+
if (requestedModes.length !== 1) {
|
|
3076
|
+
throw new CLIError(
|
|
3077
|
+
"Use exactly one apply mode: `up <target>`, `up --to <version-or-filename>`, or `up --all`."
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
const applySingleTarget = async (targetMigrationFilenameOrVersion) => {
|
|
3081
|
+
const targetMigration = resolveMigrationTarget(targetMigrationFilenameOrVersion, filenames);
|
|
3082
|
+
const validLocalMigrations = filenames.map((filename) => parseMigrationFilename(filename)).filter((migration) => migration !== null).sort((left, right) => compareMigrationVersions(left.version, right.version));
|
|
3083
|
+
const targetRemoteStatus = getRemoteMigrationVersionStatus(
|
|
3084
|
+
targetMigration.version,
|
|
3085
|
+
appliedRemoteVersions,
|
|
3086
|
+
latestRemoteVersion
|
|
3087
|
+
);
|
|
3088
|
+
if (targetRemoteStatus === "already-applied") {
|
|
3089
|
+
throw new CLIError(`Migration ${targetMigration.filename} is already applied remotely.`);
|
|
3090
|
+
}
|
|
3091
|
+
if (targetRemoteStatus === "older-than-head" && latestRemoteVersion) {
|
|
3092
|
+
throw buildOlderThanHeadError(targetMigration.filename, latestRemoteVersion);
|
|
3093
|
+
}
|
|
3094
|
+
const earlierPendingMigration = validLocalMigrations.find(
|
|
3095
|
+
(migration) => migration.version !== targetMigration.version && (!latestRemoteVersion || compareMigrationVersions(migration.version, latestRemoteVersion) > 0) && compareMigrationVersions(migration.version, targetMigration.version) < 0
|
|
3096
|
+
);
|
|
3097
|
+
if (earlierPendingMigration) {
|
|
3098
|
+
throw new CLIError(
|
|
3099
|
+
`Migration ${targetMigration.filename} is not the next pending local migration. Apply ${earlierPendingMigration.filename} first, or fix/delete it locally if it is invalid or no longer needed.`
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
const filePath = join8(getMigrationsDir(), targetMigration.filename);
|
|
3103
|
+
if (!existsSync4(filePath)) {
|
|
3104
|
+
throw new CLIError(`Local migration file not found: ${targetMigration.filename}`);
|
|
3105
|
+
}
|
|
3106
|
+
const sql = readFileSync4(filePath, "utf-8");
|
|
3107
|
+
if (!sql.trim()) {
|
|
3108
|
+
throw new CLIError(`Migration file is empty: ${targetMigration.filename}`);
|
|
3109
|
+
}
|
|
3110
|
+
return applyMigration(targetMigration, sql);
|
|
3111
|
+
};
|
|
3112
|
+
let appliedMigrations = [];
|
|
3113
|
+
if (target) {
|
|
3114
|
+
appliedMigrations = [await applySingleTarget(target)];
|
|
3115
|
+
} else {
|
|
3116
|
+
const localMigrations = parseStrictLocalMigrations(filenames);
|
|
3117
|
+
const olderThanHeadMigrations = findOlderThanHeadLocalMigrations(
|
|
3118
|
+
localMigrations,
|
|
3119
|
+
appliedRemoteVersions,
|
|
3120
|
+
latestRemoteVersion
|
|
3121
|
+
);
|
|
3122
|
+
const pendingMigrations = localMigrations.filter(
|
|
3123
|
+
(migration) => getRemoteMigrationVersionStatus(
|
|
3124
|
+
migration.version,
|
|
3125
|
+
appliedRemoteVersions,
|
|
3126
|
+
latestRemoteVersion
|
|
3127
|
+
) === "pending"
|
|
3128
|
+
);
|
|
3129
|
+
if (olderThanHeadMigrations.length > 0 && latestRemoteVersion) {
|
|
3130
|
+
throw buildOlderThanHeadError(
|
|
3131
|
+
olderThanHeadMigrations[0].filename,
|
|
3132
|
+
latestRemoteVersion
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
if (pendingMigrations.length === 0) {
|
|
3136
|
+
if (json) {
|
|
3137
|
+
outputJson({ appliedMigrations: [] });
|
|
3138
|
+
} else {
|
|
3139
|
+
outputSuccess("No pending local migrations to apply.");
|
|
3140
|
+
}
|
|
3141
|
+
await reportCliUsage("cli.db.migrations.up", true);
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
let migrationsToApply = pendingMigrations;
|
|
3145
|
+
if (options.to) {
|
|
3146
|
+
const targetVersion = /^\d{14}$/u.test(options.to) ? options.to : resolveMigrationTarget(options.to, filenames).version;
|
|
3147
|
+
const targetRemoteStatus = getRemoteMigrationVersionStatus(
|
|
3148
|
+
targetVersion,
|
|
3149
|
+
appliedRemoteVersions,
|
|
3150
|
+
latestRemoteVersion
|
|
3151
|
+
);
|
|
3152
|
+
if (targetRemoteStatus === "already-applied") {
|
|
3153
|
+
throw new CLIError(`Migration ${options.to} is already applied remotely.`);
|
|
3154
|
+
}
|
|
3155
|
+
if (targetRemoteStatus === "older-than-head" && latestRemoteVersion) {
|
|
3156
|
+
throw buildOlderThanHeadError(options.to, latestRemoteVersion);
|
|
3157
|
+
}
|
|
3158
|
+
migrationsToApply = pendingMigrations.filter(
|
|
3159
|
+
(migration) => compareMigrationVersions(migration.version, targetVersion) <= 0
|
|
3160
|
+
);
|
|
3161
|
+
if (migrationsToApply.length === 0 || migrationsToApply[migrationsToApply.length - 1]?.version !== targetVersion) {
|
|
3162
|
+
throw new CLIError(
|
|
3163
|
+
`Pending local migration not found for target ${options.to}.`
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
for (const migration of migrationsToApply) {
|
|
3168
|
+
const filePath = join8(getMigrationsDir(), migration.filename);
|
|
3169
|
+
if (!existsSync4(filePath)) {
|
|
3170
|
+
throw new CLIError(`Local migration file not found: ${migration.filename}`);
|
|
3171
|
+
}
|
|
3172
|
+
const sql = readFileSync4(filePath, "utf-8");
|
|
3173
|
+
if (!sql.trim()) {
|
|
3174
|
+
throw new CLIError(`Migration file is empty: ${migration.filename}`);
|
|
3175
|
+
}
|
|
3176
|
+
appliedMigrations.push(await applyMigration(migration, sql));
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
if (json) {
|
|
3180
|
+
outputJson({ appliedMigrations });
|
|
3181
|
+
} else {
|
|
3182
|
+
outputSuccess(`Applied ${appliedMigrations.length} migration file(s).`);
|
|
3183
|
+
for (const migration of appliedMigrations) {
|
|
3184
|
+
console.log(`- ${buildMigrationFilename(migration.version, migration.name)}`);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
await reportCliUsage("cli.db.migrations.up", true);
|
|
3188
|
+
} catch (err) {
|
|
3189
|
+
await reportCliUsage("cli.db.migrations.up", false);
|
|
3190
|
+
handleError(err, json);
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
|
|
2674
3195
|
// src/commands/records/list.ts
|
|
2675
3196
|
function registerRecordsCommands(recordsCmd2) {
|
|
2676
3197
|
recordsCmd2.command("list <table>").description("List records from a table").option("--select <columns>", "Columns to select (comma-separated)").option("--filter <filter>", 'Filter expression (e.g. "name=eq.John")').option("--order <order>", 'Order by (e.g. "created_at.desc")').option("--limit <n>", "Limit number of records", parseInt).option("--offset <n>", "Offset for pagination", parseInt).action(async (table, opts, cmd) => {
|
|
@@ -2854,21 +3375,21 @@ function registerFunctionsCommands(functionsCmd2) {
|
|
|
2854
3375
|
}
|
|
2855
3376
|
|
|
2856
3377
|
// src/commands/functions/deploy.ts
|
|
2857
|
-
import { readFileSync as
|
|
2858
|
-
import { join as
|
|
3378
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
3379
|
+
import { join as join9 } from "path";
|
|
2859
3380
|
function registerFunctionsDeployCommand(functionsCmd2) {
|
|
2860
3381
|
functionsCmd2.command("deploy <slug>").description("Deploy an edge function (create or update)").option("--file <path>", "Path to the function source file").option("--name <name>", "Function display name").option("--description <desc>", "Function description").action(async (slug, opts, cmd) => {
|
|
2861
3382
|
const { json } = getRootOpts(cmd);
|
|
2862
3383
|
try {
|
|
2863
3384
|
await requireAuth();
|
|
2864
|
-
const filePath = opts.file ??
|
|
2865
|
-
if (!
|
|
3385
|
+
const filePath = opts.file ?? join9(process.cwd(), "insforge", "functions", slug, "index.ts");
|
|
3386
|
+
if (!existsSync5(filePath)) {
|
|
2866
3387
|
throw new CLIError(
|
|
2867
3388
|
`Source file not found: ${filePath}
|
|
2868
|
-
Specify --file <path> or create ${
|
|
3389
|
+
Specify --file <path> or create ${join9("insforge", "functions", slug, "index.ts")}`
|
|
2869
3390
|
);
|
|
2870
3391
|
}
|
|
2871
|
-
const code =
|
|
3392
|
+
const code = readFileSync5(filePath, "utf-8");
|
|
2872
3393
|
const name = opts.name ?? slug;
|
|
2873
3394
|
const description = opts.description ?? "";
|
|
2874
3395
|
let exists = false;
|
|
@@ -3055,7 +3576,7 @@ function registerStorageBucketsCommand(storageCmd2) {
|
|
|
3055
3576
|
}
|
|
3056
3577
|
|
|
3057
3578
|
// src/commands/storage/upload.ts
|
|
3058
|
-
import { readFileSync as
|
|
3579
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
3059
3580
|
import { basename as basename5 } from "path";
|
|
3060
3581
|
function registerStorageUploadCommand(storageCmd2) {
|
|
3061
3582
|
storageCmd2.command("upload <file>").description("Upload a file to a storage bucket").requiredOption("--bucket <name>", "Target bucket name").option("--key <objectKey>", "Object key (defaults to filename)").action(async (file, opts, cmd) => {
|
|
@@ -3064,10 +3585,10 @@ function registerStorageUploadCommand(storageCmd2) {
|
|
|
3064
3585
|
await requireAuth();
|
|
3065
3586
|
const config = getProjectConfig();
|
|
3066
3587
|
if (!config) throw new ProjectNotLinkedError();
|
|
3067
|
-
if (!
|
|
3588
|
+
if (!existsSync6(file)) {
|
|
3068
3589
|
throw new CLIError(`File not found: ${file}`);
|
|
3069
3590
|
}
|
|
3070
|
-
const fileContent =
|
|
3591
|
+
const fileContent = readFileSync6(file);
|
|
3071
3592
|
const objectKey = opts.key ?? basename5(file);
|
|
3072
3593
|
const bucketName = opts.bucket;
|
|
3073
3594
|
const formData = new FormData();
|
|
@@ -3098,8 +3619,8 @@ function registerStorageUploadCommand(storageCmd2) {
|
|
|
3098
3619
|
}
|
|
3099
3620
|
|
|
3100
3621
|
// src/commands/storage/download.ts
|
|
3101
|
-
import { writeFileSync as
|
|
3102
|
-
import { join as
|
|
3622
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
3623
|
+
import { join as join10, basename as basename6 } from "path";
|
|
3103
3624
|
function registerStorageDownloadCommand(storageCmd2) {
|
|
3104
3625
|
storageCmd2.command("download <objectKey>").description("Download a file from a storage bucket").requiredOption("--bucket <name>", "Source bucket name").option("--output <path>", "Output file path (defaults to current directory)").action(async (objectKey, opts, cmd) => {
|
|
3105
3626
|
const { json } = getRootOpts(cmd);
|
|
@@ -3119,8 +3640,8 @@ function registerStorageDownloadCommand(storageCmd2) {
|
|
|
3119
3640
|
throw new CLIError(err.error ?? `Download failed: ${res.status}`);
|
|
3120
3641
|
}
|
|
3121
3642
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
3122
|
-
const outputPath = opts.output ??
|
|
3123
|
-
|
|
3643
|
+
const outputPath = opts.output ?? join10(process.cwd(), basename6(objectKey));
|
|
3644
|
+
writeFileSync4(outputPath, buffer);
|
|
3124
3645
|
if (json) {
|
|
3125
3646
|
outputJson({ success: true, path: outputPath, size: buffer.length });
|
|
3126
3647
|
} else {
|
|
@@ -4226,13 +4747,13 @@ function registerComputeLogsCommand(computeCmd2) {
|
|
|
4226
4747
|
}
|
|
4227
4748
|
|
|
4228
4749
|
// src/commands/compute/deploy.ts
|
|
4229
|
-
import { existsSync as
|
|
4230
|
-
import { join as
|
|
4750
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, renameSync } from "fs";
|
|
4751
|
+
import { join as join11 } from "path";
|
|
4231
4752
|
import { execSync, spawn } from "child_process";
|
|
4232
4753
|
function parseFlyToml(dir) {
|
|
4233
|
-
const tomlPath =
|
|
4234
|
-
if (!
|
|
4235
|
-
const content =
|
|
4754
|
+
const tomlPath = join11(dir, "fly.toml");
|
|
4755
|
+
if (!existsSync7(tomlPath)) return {};
|
|
4756
|
+
const content = readFileSync7(tomlPath, "utf-8");
|
|
4236
4757
|
const config = {};
|
|
4237
4758
|
const portMatch = content.match(/internal_port\s*=\s*(\d+)/);
|
|
4238
4759
|
if (portMatch) config.internalPort = Number(portMatch[1]);
|
|
@@ -4302,8 +4823,8 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
4302
4823
|
checkFlyctl();
|
|
4303
4824
|
const flyToken = getFlyToken();
|
|
4304
4825
|
const dir = directory ?? process.cwd();
|
|
4305
|
-
const dockerfilePath =
|
|
4306
|
-
if (!
|
|
4826
|
+
const dockerfilePath = join11(dir, "Dockerfile");
|
|
4827
|
+
if (!existsSync7(dockerfilePath)) {
|
|
4307
4828
|
throw new CLIError(`No Dockerfile found in ${dir}`);
|
|
4308
4829
|
}
|
|
4309
4830
|
const flyTomlDefaults = parseFlyToml(dir);
|
|
@@ -4348,15 +4869,15 @@ function registerComputeDeployCommand(computeCmd2) {
|
|
|
4348
4869
|
serviceId = service.id;
|
|
4349
4870
|
flyAppId = service.flyAppId;
|
|
4350
4871
|
}
|
|
4351
|
-
const existingTomlPath =
|
|
4352
|
-
const backupTomlPath =
|
|
4872
|
+
const existingTomlPath = join11(dir, "fly.toml");
|
|
4873
|
+
const backupTomlPath = join11(dir, "fly.toml.insforge-backup");
|
|
4353
4874
|
let hadExistingToml = false;
|
|
4354
|
-
if (
|
|
4875
|
+
if (existsSync7(existingTomlPath)) {
|
|
4355
4876
|
hadExistingToml = true;
|
|
4356
4877
|
renameSync(existingTomlPath, backupTomlPath);
|
|
4357
4878
|
}
|
|
4358
4879
|
const tomlContent = generateFlyToml(flyAppId, { port, memory, cpu, region });
|
|
4359
|
-
|
|
4880
|
+
writeFileSync5(existingTomlPath, tomlContent);
|
|
4360
4881
|
try {
|
|
4361
4882
|
if (!json) outputInfo("Building and deploying (this may take a few minutes)...");
|
|
4362
4883
|
await new Promise((resolve4, reject) => {
|
|
@@ -5081,7 +5602,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
|
|
|
5081
5602
|
const s = !json ? clack10.spinner() : null;
|
|
5082
5603
|
s?.start("Collecting diagnostic data...");
|
|
5083
5604
|
const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
|
|
5084
|
-
const cliVersion = "0.1.
|
|
5605
|
+
const cliVersion = "0.1.57";
|
|
5085
5606
|
s?.stop("Data collected");
|
|
5086
5607
|
if (!json) {
|
|
5087
5608
|
console.log(`
|
|
@@ -5310,7 +5831,7 @@ function formatBytesCompact(bytes) {
|
|
|
5310
5831
|
|
|
5311
5832
|
// src/index.ts
|
|
5312
5833
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5313
|
-
var pkg = JSON.parse(
|
|
5834
|
+
var pkg = JSON.parse(readFileSync8(join12(__dirname, "../package.json"), "utf-8"));
|
|
5314
5835
|
var INSFORGE_LOGO = `
|
|
5315
5836
|
\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
5316
5837
|
\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
@@ -5344,6 +5865,7 @@ registerDbTriggersCommand(dbCmd);
|
|
|
5344
5865
|
registerDbRpcCommand(dbCmd);
|
|
5345
5866
|
registerDbExportCommand(dbCmd);
|
|
5346
5867
|
registerDbImportCommand(dbCmd);
|
|
5868
|
+
registerDbMigrationsCommand(dbCmd);
|
|
5347
5869
|
var recordsCmd = program.command("records", { hidden: true }).description("CRUD operations on table records");
|
|
5348
5870
|
registerRecordsCommands(recordsCmd);
|
|
5349
5871
|
registerRecordsCreateCommand(recordsCmd);
|