@insforge/cli 0.1.58 → 0.1.61

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
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync8 } from "fs";
5
- import { join as join12, dirname } from "path";
4
+ import { readFileSync as readFileSync7 } from "fs";
5
+ import { join as join13, dirname } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
8
8
  import * as clack11 from "@clack/prompts";
@@ -41,8 +41,8 @@ var LineReader = class {
41
41
  this.output.write(prompt);
42
42
  if (this.queue.length > 0) return this.queue.shift();
43
43
  if (this.closed) return null;
44
- return new Promise((resolve4) => {
45
- this.waiter = resolve4;
44
+ return new Promise((resolve5) => {
45
+ this.waiter = resolve5;
46
46
  });
47
47
  }
48
48
  close() {
@@ -411,8 +411,8 @@ function startCallbackServer() {
411
411
  return new Promise((resolveServer) => {
412
412
  let resolveResult;
413
413
  let rejectResult;
414
- const resultPromise = new Promise((resolve4, reject) => {
415
- resolveResult = resolve4;
414
+ const resultPromise = new Promise((resolve5, reject) => {
415
+ resolveResult = resolve5;
416
416
  rejectResult = reject;
417
417
  });
418
418
  const server = createServer((req, res) => {
@@ -748,7 +748,7 @@ async function reportAgentConnected(payload, apiUrl) {
748
748
  await fetch(`${baseUrl}/tracking/v1/agent-connected`, {
749
749
  method: "POST",
750
750
  headers,
751
- body: JSON.stringify(payload)
751
+ body: JSON.stringify({ ...payload, client: "cli" })
752
752
  });
753
753
  }
754
754
  async function streamDiagnosticAnalysis(payload, onEvent, apiUrl) {
@@ -1466,11 +1466,11 @@ async function collectDeploymentFiles(sourceDir) {
1466
1466
  return files;
1467
1467
  }
1468
1468
  async function createZipBuffer(sourceDir) {
1469
- return new Promise((resolve4, reject) => {
1469
+ return new Promise((resolve5, reject) => {
1470
1470
  const archive = archiver("zip", { zlib: { level: 9 } });
1471
1471
  const chunks = [];
1472
1472
  archive.on("data", (chunk) => chunks.push(chunk));
1473
- archive.on("end", () => resolve4(Buffer.concat(chunks)));
1473
+ archive.on("end", () => resolve5(Buffer.concat(chunks)));
1474
1474
  archive.on("error", (err) => reject(err));
1475
1475
  archive.directory(sourceDir, false, (entry) => {
1476
1476
  if (shouldExclude(entry.name)) return false;
@@ -1552,7 +1552,7 @@ async function pollDeployment(deploymentId, spinner7, syncBeforeRead) {
1552
1552
  const startTime = Date.now();
1553
1553
  let deployment = null;
1554
1554
  while (Date.now() - startTime < POLL_TIMEOUT_MS) {
1555
- await new Promise((resolve4) => setTimeout(resolve4, POLL_INTERVAL_MS));
1555
+ await new Promise((resolve5) => setTimeout(resolve5, POLL_INTERVAL_MS));
1556
1556
  try {
1557
1557
  if (syncBeforeRead) {
1558
1558
  await ossFetch(`/api/deployments/${deploymentId}/sync`, { method: "POST" });
@@ -2869,13 +2869,17 @@ import { join as join8 } from "path";
2869
2869
  // src/lib/migrations.ts
2870
2870
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
2871
2871
  import { join as join7 } from "path";
2872
- var MIGRATION_VERSION_REGEX = /^\d{14}$/u;
2873
- var MIGRATION_FILENAME_REGEX = /^(\d{14})_([a-z0-9-]+)\.sql$/u;
2872
+ var MIGRATION_VERSION_REGEX = /^\d{1,64}$/u;
2873
+ var MIGRATION_FILENAME_REGEX = /^(\d{1,64})_([a-z0-9-]+)\.sql$/u;
2874
2874
  function assertValidMigrationVersion(version) {
2875
2875
  if (!MIGRATION_VERSION_REGEX.test(version)) {
2876
- throw new CLIError(`Invalid migration version: ${version}. Expected YYYYMMDDHHmmss.`);
2876
+ throw new CLIError(`Invalid migration version: ${version}. Expected a numeric string of at most 64 digits (e.g. 0001 or 20260418091500).`);
2877
2877
  }
2878
2878
  }
2879
+ function canonicalMigrationVersion(version) {
2880
+ assertValidMigrationVersion(version);
2881
+ return BigInt(version).toString();
2882
+ }
2879
2883
  function parseMigrationFilename(filename) {
2880
2884
  const match = MIGRATION_FILENAME_REGEX.exec(filename);
2881
2885
  if (!match) {
@@ -2883,11 +2887,16 @@ function parseMigrationFilename(filename) {
2883
2887
  }
2884
2888
  return {
2885
2889
  filename,
2886
- version: match[1],
2890
+ version: canonicalMigrationVersion(match[1]),
2887
2891
  name: match[2]
2888
2892
  };
2889
2893
  }
2890
2894
  function compareMigrationVersions(left, right) {
2895
+ if (MIGRATION_VERSION_REGEX.test(left) && MIGRATION_VERSION_REGEX.test(right)) {
2896
+ const a = BigInt(left);
2897
+ const b = BigInt(right);
2898
+ return a < b ? -1 : a > b ? 1 : 0;
2899
+ }
2891
2900
  return left.localeCompare(right);
2892
2901
  }
2893
2902
  function getRemoteMigrationVersionStatus(version, appliedVersions, latestRemoteVersion) {
@@ -2909,6 +2918,10 @@ function formatMigrationVersion(date) {
2909
2918
  return `${year}${month}${day}${hour}${minute}${second}`;
2910
2919
  }
2911
2920
  function incrementMigrationVersion(version) {
2921
+ assertValidMigrationVersion(version);
2922
+ if (!/^\d{14}$/u.test(version)) {
2923
+ return String(BigInt(version) + 1n);
2924
+ }
2912
2925
  const year = Number(version.slice(0, 4));
2913
2926
  const month = Number(version.slice(4, 6)) - 1;
2914
2927
  const day = Number(version.slice(6, 8));
@@ -2991,8 +3004,9 @@ function findOlderThanHeadLocalMigrations(migrations, appliedVersions, latestRem
2991
3004
  );
2992
3005
  }
2993
3006
  function findLocalMigrationByVersion(version, filenames) {
3007
+ const canonicalVersion = canonicalMigrationVersion(version);
2994
3008
  const matches = filenames.map((filename) => parseMigrationFilename(filename)).filter(
2995
- (migration) => migration !== null && migration.version === version
3009
+ (migration) => migration !== null && migration.version === canonicalVersion
2996
3010
  );
2997
3011
  if (matches.length === 0) {
2998
3012
  throw new CLIError(`Local migration for version ${version} not found.`);
@@ -3005,7 +3019,7 @@ function findLocalMigrationByVersion(version, filenames) {
3005
3019
  return matches[0];
3006
3020
  }
3007
3021
  function resolveMigrationTarget(target, filenames) {
3008
- if (/^\d{14}$/u.test(target)) {
3022
+ if (/^\d{1,64}$/u.test(target)) {
3009
3023
  return findLocalMigrationByVersion(target, filenames);
3010
3024
  }
3011
3025
  const parsedTarget = parseMigrationFilename(target);
@@ -3044,7 +3058,7 @@ async function fetchRemoteMigrations() {
3044
3058
  const raw = await res.json();
3045
3059
  const migrations = Array.isArray(raw.migrations) ? raw.migrations : [];
3046
3060
  for (const migration of migrations) {
3047
- assertValidMigrationVersion(migration.version);
3061
+ migration.version = canonicalMigrationVersion(migration.version);
3048
3062
  }
3049
3063
  return migrations;
3050
3064
  }
@@ -3104,6 +3118,9 @@ function registerDbMigrationsCommand(dbCmd2) {
3104
3118
  await requireAuth();
3105
3119
  const migrations = await fetchRemoteMigrations();
3106
3120
  const migrationsDir = ensureMigrationsDir();
3121
+ const existingLocalVersions = new Set(
3122
+ listLocalMigrationFilenames().map((filename) => parseMigrationFilename(filename)).filter((migration) => migration !== null).map((migration) => migration.version)
3123
+ );
3107
3124
  const createdFiles = [];
3108
3125
  const skippedFiles = [];
3109
3126
  for (const migration of [...migrations].sort(
@@ -3115,12 +3132,13 @@ function registerDbMigrationsCommand(dbCmd2) {
3115
3132
  migration.name
3116
3133
  );
3117
3134
  const filePath = join8(migrationsDir, filename);
3118
- if (existsSync4(filePath)) {
3135
+ if (existingLocalVersions.has(migration.version) || existsSync4(filePath)) {
3119
3136
  skippedFiles.push(filename);
3120
3137
  continue;
3121
3138
  }
3122
3139
  writeFileSync3(filePath, formatMigrationSql(migration.statements));
3123
3140
  createdFiles.push(filename);
3141
+ existingLocalVersions.add(migration.version);
3124
3142
  }
3125
3143
  if (json) {
3126
3144
  outputJson({
@@ -4445,7 +4463,10 @@ function registerSchedulesGetCommand(schedulesCmd2) {
4445
4463
 
4446
4464
  // src/commands/schedules/create.ts
4447
4465
  function registerSchedulesCreateCommand(schedulesCmd2) {
4448
- schedulesCmd2.command("create").description("Create a new schedule").requiredOption("--name <name>", "Schedule name").requiredOption("--cron <expression>", "Cron expression (5-field format)").requiredOption("--url <url>", "URL to invoke").requiredOption("--method <method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)").option("--headers <json>", "HTTP headers as JSON").option("--body <json>", "Request body as JSON").action(async (opts, cmd) => {
4466
+ schedulesCmd2.command("create").description("Create a new schedule").requiredOption("--name <name>", "Schedule name").requiredOption(
4467
+ "--cron <expression>",
4468
+ 'Cron expression. 5-field cron (e.g. "*/5 * * * *", "0 9 * * 1-5") or pg_cron interval syntax for sub-minute cadence (e.g. "30 seconds", "5 minutes").'
4469
+ ).requiredOption("--url <url>", "URL to invoke").requiredOption("--method <method>", "HTTP method (GET, POST, PUT, PATCH, DELETE)").option("--headers <json>", "HTTP headers as JSON").option("--body <json>", "Request body as JSON").action(async (opts, cmd) => {
4449
4470
  const { json } = getRootOpts(cmd);
4450
4471
  try {
4451
4472
  await requireAuth();
@@ -4489,7 +4510,10 @@ function registerSchedulesCreateCommand(schedulesCmd2) {
4489
4510
 
4490
4511
  // src/commands/schedules/update.ts
4491
4512
  function registerSchedulesUpdateCommand(schedulesCmd2) {
4492
- schedulesCmd2.command("update <id>").description("Update a schedule").option("--name <name>", "New schedule name").option("--cron <expression>", "New cron expression").option("--url <url>", "New URL to invoke").option("--method <method>", "New HTTP method").option("--headers <json>", "New HTTP headers as JSON").option("--body <json>", "New request body as JSON").option("--active <bool>", "Enable/disable schedule (true/false)").action(async (id, opts, cmd) => {
4513
+ schedulesCmd2.command("update <id>").description("Update a schedule").option("--name <name>", "New schedule name").option(
4514
+ "--cron <expression>",
4515
+ 'New cron expression. 5-field cron or pg_cron interval syntax (e.g. "30 seconds").'
4516
+ ).option("--url <url>", "New URL to invoke").option("--method <method>", "New HTTP method").option("--headers <json>", "New HTTP headers as JSON").option("--body <json>", "New request body as JSON").option("--active <bool>", "Enable/disable schedule (true/false)").action(async (id, opts, cmd) => {
4493
4517
  const { json } = getRootOpts(cmd);
4494
4518
  try {
4495
4519
  await requireAuth();
@@ -4665,48 +4689,6 @@ function registerComputeGetCommand(computeCmd2) {
4665
4689
  });
4666
4690
  }
4667
4691
 
4668
- // src/commands/compute/create.ts
4669
- function registerComputeCreateCommand(computeCmd2) {
4670
- computeCmd2.command("create").description("Create and deploy a compute service").requiredOption("--name <name>", "Service name (DNS-safe, e.g. my-api)").requiredOption("--image <image>", "Docker image URL (e.g. nginx:alpine)").option("--port <port>", "Container port", "8080").option("--cpu <tier>", "CPU tier (shared-1x, shared-2x, performance-1x, performance-2x, performance-4x)", "shared-1x").option("--memory <mb>", "Memory in MB (256, 512, 1024, 2048, 4096, 8192)", "512").option("--region <region>", "Fly.io region", "iad").option("--env <json>", "Environment variables as JSON object").action(async (opts, cmd) => {
4671
- const { json } = getRootOpts(cmd);
4672
- try {
4673
- await requireAuth();
4674
- const body = {
4675
- name: opts.name,
4676
- imageUrl: opts.image,
4677
- port: Number(opts.port),
4678
- cpu: opts.cpu,
4679
- memory: Number(opts.memory),
4680
- region: opts.region
4681
- };
4682
- if (opts.env) {
4683
- try {
4684
- body.envVars = JSON.parse(opts.env);
4685
- } catch {
4686
- throw new CLIError("Invalid JSON for --env");
4687
- }
4688
- }
4689
- const res = await ossFetch("/api/compute/services", {
4690
- method: "POST",
4691
- body: JSON.stringify(body)
4692
- });
4693
- const service = await res.json();
4694
- if (json) {
4695
- outputJson(service);
4696
- } else {
4697
- outputSuccess(`Service "${service.name}" created [${service.status}]`);
4698
- if (service.endpointUrl) {
4699
- console.log(` Endpoint: ${service.endpointUrl}`);
4700
- }
4701
- }
4702
- await reportCliUsage("cli.compute.create", true);
4703
- } catch (err) {
4704
- await reportCliUsage("cli.compute.create", false);
4705
- handleError(err, json);
4706
- }
4707
- });
4708
- }
4709
-
4710
4692
  // src/commands/compute/update.ts
4711
4693
  function registerComputeUpdateCommand(computeCmd2) {
4712
4694
  computeCmd2.command("update <id>").description("Update a compute service").option("--image <image>", "Docker image URL").option("--port <port>", "Container port").option("--cpu <tier>", "CPU tier").option("--memory <mb>", "Memory in MB").option("--region <region>", "Fly.io region").option("--env <json>", "Environment variables as JSON object").action(async (id, opts, cmd) => {
@@ -4862,175 +4844,295 @@ function registerComputeLogsCommand(computeCmd2) {
4862
4844
  }
4863
4845
 
4864
4846
  // src/commands/compute/deploy.ts
4865
- import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, renameSync } from "fs";
4847
+ import { existsSync as existsSync8 } from "fs";
4848
+ import { join as join12, resolve as resolve4 } from "path";
4849
+
4850
+ // src/lib/flyctl.ts
4851
+ import { spawn, spawnSync } from "child_process";
4852
+ import { existsSync as existsSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
4866
4853
  import { join as join11 } from "path";
4867
- import { execSync, spawn } from "child_process";
4868
- function parseFlyToml(dir) {
4869
- const tomlPath = join11(dir, "fly.toml");
4870
- if (!existsSync7(tomlPath)) return {};
4871
- const content = readFileSync7(tomlPath, "utf-8");
4872
- const config = {};
4873
- const portMatch = content.match(/internal_port\s*=\s*(\d+)/);
4874
- if (portMatch) config.internalPort = Number(portMatch[1]);
4875
- const regionMatch = content.match(/primary_region\s*=\s*'([^']+)'/);
4876
- if (regionMatch) config.region = regionMatch[1];
4877
- const memoryMatch = content.match(/memory\s*=\s*'(\d+)/);
4878
- if (memoryMatch) config.memory = memoryMatch[1];
4879
- const cpuKindMatch = content.match(/cpu_kind\s*=\s*'([^']+)'/);
4880
- if (cpuKindMatch) config.cpuKind = cpuKindMatch[1];
4881
- const cpusMatch = content.match(/cpus\s*=\s*(\d+)/);
4882
- if (cpusMatch) config.cpus = Number(cpusMatch[1]);
4883
- return config;
4854
+ function ensureFlyctlAvailable() {
4855
+ const r = spawnSync("flyctl", ["version"], {
4856
+ encoding: "utf8",
4857
+ stdio: ["ignore", "pipe", "pipe"]
4858
+ });
4859
+ if (r.error || r.status !== 0) {
4860
+ throw new CLIError(
4861
+ "flyctl is required for source-mode deploy.\n \u2022 Install: curl -L https://fly.io/install.sh | sh\n \u2022 Or use --image <pre-built-image> instead.\n" + (r.stderr ? ` Detail: ${r.stderr.trim().slice(0, 200)}` : "")
4862
+ );
4863
+ }
4884
4864
  }
4885
- function memoryToCpuTier(cpuKind, cpus) {
4886
- if (!cpuKind) return void 0;
4887
- const kind = cpuKind === "performance" ? "performance" : "shared";
4888
- const count = cpus ?? 1;
4889
- return `${kind}-${count}x`;
4890
- }
4891
- function generateFlyToml(appName, opts) {
4892
- const cpuParts = opts.cpu.split("-");
4893
- const cpuKind = cpuParts[0] ?? "shared";
4894
- const cpuCount = parseInt(cpuParts[1] ?? "1", 10);
4895
- return `# Auto-generated by InsForge CLI
4896
- app = '${appName}'
4897
- primary_region = '${opts.region}'
4865
+ function ensureFlyTomlStub(opts) {
4866
+ const path5 = join11(opts.dir, "fly.toml");
4867
+ if (existsSync7(path5)) {
4868
+ return () => {
4869
+ };
4870
+ }
4871
+ const stub = `# Auto-generated by @insforge/cli for compute deploy. Safe to delete.
4872
+ app = "${opts.appId}"
4873
+ primary_region = "${opts.region}"
4898
4874
 
4899
4875
  [build]
4900
- dockerfile = 'Dockerfile'
4901
4876
 
4902
4877
  [http_service]
4903
4878
  internal_port = ${opts.port}
4904
4879
  force_https = true
4905
- auto_stop_machines = 'stop'
4880
+ auto_stop_machines = false
4906
4881
  auto_start_machines = true
4907
4882
  min_machines_running = 0
4908
-
4909
- [[vm]]
4910
- memory = '${opts.memory}mb'
4911
- cpu_kind = '${cpuKind}'
4912
- cpus = ${cpuCount}
4913
4883
  `;
4884
+ writeFileSync5(path5, stub, "utf8");
4885
+ return () => {
4886
+ try {
4887
+ unlinkSync2(path5);
4888
+ } catch {
4889
+ }
4890
+ };
4914
4891
  }
4915
- function checkFlyctl() {
4916
- try {
4917
- execSync("flyctl version", { stdio: "pipe" });
4918
- } catch {
4919
- throw new CLIError(
4920
- "flyctl is not installed.\nInstall it with: curl -L https://fly.io/install.sh | sh\nOr: brew install flyctl"
4921
- );
4922
- }
4923
- }
4924
- function getFlyToken() {
4925
- const token = process.env.FLY_API_TOKEN;
4926
- if (!token) {
4927
- throw new CLIError(
4928
- "FLY_API_TOKEN environment variable is required for compute deploy.\nSet it with: export FLY_API_TOKEN=<your-fly-token>"
4892
+ function flyctlBuildAndPush(opts) {
4893
+ return new Promise((resolve5, reject) => {
4894
+ const cleanupStub = ensureFlyTomlStub({
4895
+ dir: opts.dir,
4896
+ appId: opts.appId,
4897
+ region: opts.region,
4898
+ port: opts.port
4899
+ });
4900
+ const child = spawn(
4901
+ "flyctl",
4902
+ [
4903
+ "deploy",
4904
+ "--remote-only",
4905
+ "--build-only",
4906
+ "--push",
4907
+ "--app",
4908
+ opts.appId,
4909
+ "--image-label",
4910
+ opts.imageLabel,
4911
+ "--no-cache"
4912
+ ],
4913
+ {
4914
+ cwd: opts.dir,
4915
+ env: { ...process.env, FLY_API_TOKEN: opts.token },
4916
+ stdio: ["inherit", "pipe", "pipe"]
4917
+ }
4929
4918
  );
4930
- }
4931
- return token;
4919
+ let captured = "";
4920
+ child.stdout?.on("data", (b) => {
4921
+ const s = b.toString();
4922
+ captured += s;
4923
+ process.stdout.write(s);
4924
+ });
4925
+ child.stderr?.on("data", (b) => {
4926
+ const s = b.toString();
4927
+ captured += s;
4928
+ process.stderr.write(s);
4929
+ });
4930
+ child.on("error", (err) => {
4931
+ cleanupStub();
4932
+ reject(new CLIError(`flyctl deploy could not start: ${err.message}`));
4933
+ });
4934
+ child.on("exit", (code) => {
4935
+ cleanupStub();
4936
+ if (code !== 0) {
4937
+ return reject(
4938
+ new CLIError(`flyctl deploy --build-only failed (exit ${code}). See output above.`)
4939
+ );
4940
+ }
4941
+ const m = captured.match(/pushing manifest for registry\.fly\.io\/[^\s]+@(sha256:[0-9a-f]+)/);
4942
+ if (!m) {
4943
+ return reject(
4944
+ new CLIError(
4945
+ 'flyctl deploy succeeded but the buildkit "pushing manifest" line was not found. Cannot determine image digest \u2014 please re-run with FLY_LOG_LEVEL=debug and report.'
4946
+ )
4947
+ );
4948
+ }
4949
+ resolve5({ imageRef: `registry.fly.io/${opts.appId}@${m[1]}` });
4950
+ });
4951
+ });
4932
4952
  }
4953
+
4954
+ // src/commands/compute/deploy.ts
4933
4955
  function registerComputeDeployCommand(computeCmd2) {
4934
- computeCmd2.command("deploy [directory]").description("Build and deploy a Dockerfile as a compute service").requiredOption("--name <name>", "Service name").option("--port <port>", "Container port").option("--cpu <tier>", "CPU tier (shared-1x, shared-2x, performance-1x, etc.)").option("--memory <mb>", "Memory in MB (256, 512, 1024, 2048, 4096, 8192)").option("--region <region>", "Fly.io region").option("--env <json>", "Environment variables as JSON object").action(async (directory, opts, cmd) => {
4956
+ computeCmd2.command("deploy [dir]").description(
4957
+ "Deploy a compute service. Two modes:\n compute deploy <dir> --name <name> (source mode \u2014 flyctl remote build + push, requires flyctl on PATH; no Docker needed)\n compute deploy --image <url> --name <name> (image mode \u2014 deploys pre-built image, no flyctl/Docker required)"
4958
+ ).requiredOption("--name <name>", "Service name (DNS-safe, e.g. my-api)").option("--image <url>", "Pre-built image URL (image mode)").option("--port <port>", "Container port", "8080").option(
4959
+ "--cpu <tier>",
4960
+ "CPU tier in <kind>-<N>x format (e.g. shared-1x, performance-2x)",
4961
+ "shared-1x"
4962
+ ).option("--memory <mb>", "Memory in MB", "512").option("--region <region>", "Fly.io region", "iad").option("--env <json>", "Env vars as JSON object").action(async (dir, opts, cmd) => {
4935
4963
  const { json } = getRootOpts(cmd);
4936
4964
  try {
4937
4965
  await requireAuth();
4938
- checkFlyctl();
4939
- const flyToken = getFlyToken();
4940
- const dir = directory ?? process.cwd();
4941
- const dockerfilePath = join11(dir, "Dockerfile");
4942
- if (!existsSync7(dockerfilePath)) {
4943
- throw new CLIError(`No Dockerfile found in ${dir}`);
4944
- }
4945
- const flyTomlDefaults = parseFlyToml(dir);
4946
- const port = Number(opts.port) || flyTomlDefaults.internalPort || 8080;
4947
- const cpu = opts.cpu || memoryToCpuTier(flyTomlDefaults.cpuKind, flyTomlDefaults.cpus) || "shared-1x";
4948
- const memory = Number(opts.memory) || Number(flyTomlDefaults.memory) || 512;
4949
- const region = opts.region || flyTomlDefaults.region || "iad";
4966
+ if (dir && opts.image) {
4967
+ throw new CLIError("Cannot use both [dir] and --image \u2014 pick one mode.");
4968
+ }
4969
+ if (!dir && !opts.image) {
4970
+ throw new CLIError(
4971
+ "Must provide either [dir] (source mode) or --image <url> (image mode)."
4972
+ );
4973
+ }
4974
+ const port = Number(opts.port);
4975
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
4976
+ throw new CLIError(`Invalid --port: ${opts.port}`);
4977
+ }
4978
+ const memory = Number(opts.memory);
4979
+ if (!Number.isInteger(memory) || memory <= 0) {
4980
+ throw new CLIError(`Invalid --memory: ${opts.memory}`);
4981
+ }
4950
4982
  let envVars;
4951
4983
  if (opts.env) {
4984
+ let parsed;
4952
4985
  try {
4953
- envVars = JSON.parse(opts.env);
4986
+ parsed = JSON.parse(opts.env);
4954
4987
  } catch {
4955
4988
  throw new CLIError("Invalid JSON for --env");
4956
4989
  }
4990
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4991
+ throw new CLIError('--env must be a JSON object like {"KEY":"value"}');
4992
+ }
4993
+ for (const [k, v] of Object.entries(parsed)) {
4994
+ if (typeof v !== "string") {
4995
+ throw new CLIError(
4996
+ `--env values must be strings \u2014 got ${typeof v} for key "${k}"`
4997
+ );
4998
+ }
4999
+ }
5000
+ envVars = parsed;
5001
+ }
5002
+ const baseBody = {
5003
+ name: opts.name,
5004
+ port,
5005
+ cpu: opts.cpu,
5006
+ memory,
5007
+ region: opts.region
5008
+ };
5009
+ if (envVars) baseBody.envVars = envVars;
5010
+ if (!dir) {
5011
+ const body = { ...baseBody, imageUrl: opts.image };
5012
+ const listRes2 = await ossFetch("/api/compute/services");
5013
+ const existing2 = (await listRes2.json()).find(
5014
+ (s) => s.name === opts.name
5015
+ );
5016
+ let res;
5017
+ if (existing2) {
5018
+ if (!json) outputInfo(`Found existing service "${opts.name}", updating...`);
5019
+ const updateBody2 = { ...body };
5020
+ delete updateBody2.name;
5021
+ res = await ossFetch(`/api/compute/services/${encodeURIComponent(existing2.id)}`, {
5022
+ method: "PATCH",
5023
+ body: JSON.stringify(updateBody2)
5024
+ });
5025
+ } else {
5026
+ res = await ossFetch("/api/compute/services", {
5027
+ method: "POST",
5028
+ body: JSON.stringify(body)
5029
+ });
5030
+ }
5031
+ const service2 = await res.json();
5032
+ if (json) {
5033
+ outputJson(service2);
5034
+ } else {
5035
+ const verb = existing2 ? "updated" : "deployed";
5036
+ outputSuccess(`Service "${service2.name}" ${verb} [${service2.status}]`);
5037
+ if (service2.endpointUrl) console.log(` Endpoint: ${service2.endpointUrl}`);
5038
+ }
5039
+ await reportCliUsage("cli.compute.deploy", true);
5040
+ return;
5041
+ }
5042
+ const absDir = resolve4(dir);
5043
+ const dockerfilePath = join12(absDir, "Dockerfile");
5044
+ if (!existsSync8(dockerfilePath)) {
5045
+ throw new CLIError(
5046
+ `No Dockerfile at ${dockerfilePath}.
5047
+ Either:
5048
+ \u2022 Create one (ask your AI agent \u2014 see the insforge-cli skill)
5049
+ \u2022 Use --image <url> to deploy a pre-built image instead`
5050
+ );
4957
5051
  }
4958
- if (!json) outputInfo(`Checking for existing service "${opts.name}"...`);
5052
+ ensureFlyctlAvailable();
5053
+ if (!json) outputInfo(`Detected Dockerfile at ${dockerfilePath}`);
4959
5054
  const listRes = await ossFetch("/api/compute/services");
4960
- const services = await listRes.json();
4961
- const existing = services.find((s) => s.name === opts.name);
5055
+ const existing = (await listRes.json()).find((s) => s.name === opts.name);
4962
5056
  let serviceId;
4963
5057
  let flyAppId;
4964
5058
  if (existing) {
5059
+ if (!existing.flyAppId) {
5060
+ throw new CLIError(
5061
+ `Service "${opts.name}" exists but has no Fly app yet. Delete it and redeploy.`
5062
+ );
5063
+ }
4965
5064
  serviceId = existing.id;
4966
5065
  flyAppId = existing.flyAppId;
4967
- if (!json) outputInfo(`Found existing service, redeploying...`);
5066
+ if (!json) outputInfo(`Found existing service "${opts.name}" (${flyAppId}), updating...`);
4968
5067
  } else {
4969
5068
  if (!json) outputInfo(`Creating service "${opts.name}"...`);
4970
- const body = {
4971
- name: opts.name,
4972
- imageUrl: "dockerfile",
4973
- port,
4974
- cpu,
4975
- memory,
4976
- region
4977
- };
4978
- if (envVars) body.envVars = envVars;
4979
- const deployRes = await ossFetch("/api/compute/services/deploy", {
5069
+ const prepareRes = await ossFetch("/api/compute/services/deploy", {
4980
5070
  method: "POST",
4981
- body: JSON.stringify(body)
5071
+ body: JSON.stringify(baseBody)
4982
5072
  });
4983
- const service = await deployRes.json();
4984
- serviceId = service.id;
4985
- flyAppId = service.flyAppId;
4986
- }
4987
- const existingTomlPath = join11(dir, "fly.toml");
4988
- const backupTomlPath = join11(dir, "fly.toml.insforge-backup");
4989
- let hadExistingToml = false;
4990
- if (existsSync7(existingTomlPath)) {
4991
- hadExistingToml = true;
4992
- renameSync(existingTomlPath, backupTomlPath);
4993
- }
4994
- const tomlContent = generateFlyToml(flyAppId, { port, memory, cpu, region });
4995
- writeFileSync5(existingTomlPath, tomlContent);
5073
+ const prepared = await prepareRes.json();
5074
+ serviceId = prepared.id;
5075
+ flyAppId = prepared.flyAppId;
5076
+ if (!json) outputInfo(`Created Fly app ${flyAppId}`);
5077
+ }
5078
+ if (!json) outputInfo("Requesting deploy token...");
5079
+ const tokenRes = await ossFetch(
5080
+ `/api/compute/services/${encodeURIComponent(serviceId)}/deploy-token`,
5081
+ { method: "POST" }
5082
+ );
5083
+ const tokenJson = await tokenRes.json();
5084
+ const imageLabel = `cli-${Date.now()}`;
5085
+ if (!json) outputInfo(`Building & pushing on Fly remote builder...`);
5086
+ let imageRef;
4996
5087
  try {
4997
- if (!json) outputInfo("Building and deploying (this may take a few minutes)...");
4998
- await new Promise((resolve4, reject) => {
4999
- const child = spawn(
5000
- "flyctl",
5001
- ["deploy", "--remote-only", "--app", flyAppId, "--access-token", flyToken, "--yes"],
5002
- { cwd: dir, stdio: json ? "pipe" : "inherit" }
5003
- );
5004
- child.on("close", (code) => {
5005
- if (code === 0) resolve4();
5006
- else reject(new CLIError(`flyctl deploy failed with exit code ${code}`));
5007
- });
5008
- child.on("error", (err) => reject(new CLIError(`flyctl deploy error: ${err.message}`)));
5009
- });
5010
- if (!json) outputInfo("Syncing deployment info...");
5011
- const syncRes = await ossFetch(
5012
- `/api/compute/services/${encodeURIComponent(serviceId)}/sync`,
5013
- { method: "PATCH" }
5014
- );
5015
- const synced = await syncRes.json();
5016
- if (json) {
5017
- outputJson(synced);
5018
- } else {
5019
- outputSuccess(`Service "${synced.name}" deployed [${synced.status}]`);
5020
- if (synced.endpointUrl) {
5021
- console.log(` Endpoint: ${synced.endpointUrl}`);
5022
- }
5023
- }
5024
- await reportCliUsage("cli.compute.deploy", true);
5025
- } finally {
5026
- try {
5027
- unlinkSync2(existingTomlPath);
5028
- if (hadExistingToml) {
5029
- renameSync(backupTomlPath, existingTomlPath);
5088
+ ({ imageRef } = await flyctlBuildAndPush({
5089
+ dir: absDir,
5090
+ appId: flyAppId,
5091
+ imageLabel,
5092
+ token: tokenJson.token,
5093
+ region: opts.region,
5094
+ port
5095
+ }));
5096
+ } catch (buildErr) {
5097
+ if (!existing) {
5098
+ try {
5099
+ await ossFetch(`/api/compute/services/${encodeURIComponent(serviceId)}`, {
5100
+ method: "DELETE"
5101
+ });
5102
+ if (!json) outputInfo(`Rolled back service "${opts.name}" after build failure.`);
5103
+ } catch {
5104
+ if (!json) {
5105
+ outputInfo(
5106
+ `Build failed and rollback also failed. Run: npx @insforge/cli compute delete ${serviceId}`
5107
+ );
5108
+ }
5030
5109
  }
5031
- } catch {
5032
5110
  }
5111
+ throw buildErr;
5112
+ }
5113
+ if (!json) outputInfo("Launching machine...");
5114
+ const updateBody = {
5115
+ imageUrl: imageRef,
5116
+ port,
5117
+ cpu: opts.cpu,
5118
+ memory,
5119
+ region: opts.region
5120
+ };
5121
+ if (envVars) updateBody.envVars = envVars;
5122
+ const finalRes = await ossFetch(
5123
+ `/api/compute/services/${encodeURIComponent(serviceId)}`,
5124
+ { method: "PATCH", body: JSON.stringify(updateBody) }
5125
+ );
5126
+ const service = await finalRes.json();
5127
+ if (json) {
5128
+ outputJson(service);
5129
+ } else {
5130
+ const verb = existing ? "updated" : "deployed";
5131
+ outputSuccess(`Service "${service.name}" ${verb} [${service.status}]`);
5132
+ if (service.endpointUrl) console.log(` Endpoint: ${service.endpointUrl}`);
5133
+ console.log(` Image: ${imageRef} (built remotely; no local image to clean up)`);
5033
5134
  }
5135
+ await reportCliUsage("cli.compute.deploy", true);
5034
5136
  } catch (err) {
5035
5137
  await reportCliUsage("cli.compute.deploy", false);
5036
5138
  handleError(err, json);
@@ -5717,7 +5819,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
5717
5819
  const s = !json ? clack10.spinner() : null;
5718
5820
  s?.start("Collecting diagnostic data...");
5719
5821
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
5720
- const cliVersion = "0.1.58";
5822
+ const cliVersion = "0.1.61";
5721
5823
  s?.stop("Data collected");
5722
5824
  if (!json) {
5723
5825
  console.log(`
@@ -5946,7 +6048,7 @@ function formatBytesCompact(bytes) {
5946
6048
 
5947
6049
  // src/index.ts
5948
6050
  var __dirname = dirname(fileURLToPath(import.meta.url));
5949
- var pkg = JSON.parse(readFileSync8(join12(__dirname, "../package.json"), "utf-8"));
6051
+ var pkg = JSON.parse(readFileSync7(join13(__dirname, "../package.json"), "utf-8"));
5950
6052
  var INSFORGE_LOGO = `
5951
6053
  \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
5952
6054
  \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
@@ -6018,7 +6120,6 @@ registerDiagnoseCommands(diagnoseCmd);
6018
6120
  var computeCmd = program.command("compute").description("Manage compute services (Docker containers on Fly.io)");
6019
6121
  registerComputeListCommand(computeCmd);
6020
6122
  registerComputeGetCommand(computeCmd);
6021
- registerComputeCreateCommand(computeCmd);
6022
6123
  registerComputeDeployCommand(computeCmd);
6023
6124
  registerComputeUpdateCommand(computeCmd);
6024
6125
  registerComputeDeleteCommand(computeCmd);