@insforge/cli 0.1.80 → 0.1.82

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
@@ -674,17 +674,19 @@ Skill files are written to per-agent directories (e.g. `.claude/`, `.cursor/`, `
674
674
 
675
675
  The CLI reports anonymous usage events to [PostHog](https://posthog.com) so we can understand which features are being used and prioritize improvements.
676
676
 
677
- Analytics are enabled by default in the published npm package. If you build the CLI from source without setting `POSTHOG_API_KEY` at build time, analytics become a no-op automatically.
677
+ We capture only non-sensitive metadata: the command name, subcommand, outcome (`success`, `applied`, `aborted`, `dry_run`, `no_changes`, `all_skipped`, `error`), flag shape (e.g. `dry_run`, `json_mode`), section names from `insforge.toml` schema (e.g. `auth.smtp`), region, and an OSS-vs-cloud flag. We never send SQL, TOML file contents, credentials, environment variable values, or any free text you type.
678
+
679
+ If you build the CLI from source without setting `POSTHOG_API_KEY` at build time, analytics become a no-op automatically.
678
680
 
679
681
  ## Environment Variables
680
682
 
681
- | Variable | Description |
682
- | ----------------------- | ---------------------------------- |
683
- | `INSFORGE_ACCESS_TOKEN` | Override the stored access token |
684
- | `INSFORGE_PROJECT_ID` | Override the linked project ID |
685
- | `INSFORGE_API_URL` | Override the Platform API URL |
686
- | `INSFORGE_EMAIL` | Email for non-interactive login |
687
- | `INSFORGE_PASSWORD` | Password for non-interactive login |
683
+ | Variable | Description |
684
+ | ----------------------- | --------------------------------------------------------------- |
685
+ | `INSFORGE_ACCESS_TOKEN` | Override the stored access token |
686
+ | `INSFORGE_PROJECT_ID` | Override the linked project ID |
687
+ | `INSFORGE_API_URL` | Override the Platform API URL |
688
+ | `INSFORGE_EMAIL` | Email for non-interactive login |
689
+ | `INSFORGE_PASSWORD` | Password for non-interactive login |
688
690
 
689
691
  ## Non-Interactive / CI Usage
690
692
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync12 } from "fs";
5
- import { join as join15, dirname as dirname2 } from "path";
5
+ import { join as join14, dirname as dirname2 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { Command } from "commander";
8
8
  import * as clack18 from "@clack/prompts";
@@ -1215,9 +1215,24 @@ function trackPayments(subcommand, config, properties) {
1215
1215
  ...properties
1216
1216
  });
1217
1217
  }
1218
+ function trackConfig(subcommand, config, properties) {
1219
+ const distinctId = config?.project_id ?? FAKE_PROJECT_ID;
1220
+ captureEvent(distinctId, "cli_config_invoked", {
1221
+ subcommand,
1222
+ project_id: config?.project_id,
1223
+ project_name: config?.project_name,
1224
+ org_id: config?.org_id,
1225
+ region: config?.region,
1226
+ oss_mode: !config || config.project_id === FAKE_PROJECT_ID,
1227
+ ...properties
1228
+ });
1229
+ }
1218
1230
  async function shutdownAnalytics() {
1231
+ if (!client) return;
1232
+ const c = client;
1233
+ client = null;
1219
1234
  try {
1220
- if (client) await client.shutdown();
1235
+ await c.shutdown();
1221
1236
  } catch {
1222
1237
  }
1223
1238
  }
@@ -2614,6 +2629,7 @@ var execAsync2 = promisify3(exec2);
2614
2629
  var execFileAsync2 = promisify3(execFile2);
2615
2630
  var SAFE_REPO_PATTERN2 = /^(https?:\/\/|git@)[A-Za-z0-9._:/@~+-]+(\.git)?$/;
2616
2631
  var SAFE_BRANCH_PATTERN2 = /^[A-Za-z0-9._/-]+$/;
2632
+ var SAFE_MARKETPLACE_SLUG = /^[a-z0-9][a-z0-9-]{0,99}$/;
2617
2633
  async function waitForProjectActive(projectId, apiUrl, timeoutMs = 12e4) {
2618
2634
  const start = Date.now();
2619
2635
  while (Date.now() - start < timeoutMs) {
@@ -2722,9 +2738,18 @@ async function copyDir(src, dest) {
2722
2738
  }
2723
2739
  }
2724
2740
  function registerCreateCommand(program2) {
2725
- program2.command("create").description("Create a new InsForge project").option("--name <name>", "Project name").option("--org-id <id>", "Organization ID").option("--region <region>", "Deployment region (us-east, us-west, eu-central, ap-southeast)").option("--template <template>", "Template to use: react, nextjs, chatbot, crm, e-commerce, todo, or empty").option("--auth <provider>", "Wire a third-party auth provider into the chosen template (currently: better-auth)").action(async (opts, cmd) => {
2741
+ program2.command("create").description("Create a new InsForge project").option("--name <name>", "Project name").option("--org-id <id>", "Organization ID").option("--region <region>", "Deployment region (us-east, us-west, eu-central, ap-southeast)").option("--template <template>", "Template to use: react, nextjs, chatbot, crm, e-commerce, todo, or empty").option("--marketplace <slug>", "Install a marketplace template by slug (browse: https://insforge.dev/templates)").option("--auth <provider>", "Wire a third-party auth provider into the chosen template (currently: better-auth)").action(async (opts, cmd) => {
2726
2742
  const { json, apiUrl } = getRootOpts(cmd);
2727
2743
  try {
2744
+ if (opts.marketplace && opts.template) {
2745
+ throw new CLIError("--marketplace and --template are mutually exclusive");
2746
+ }
2747
+ if (opts.marketplace && !SAFE_MARKETPLACE_SLUG.test(opts.marketplace)) {
2748
+ throw new CLIError(
2749
+ `Invalid --marketplace slug "${opts.marketplace}". Slugs must match ${SAFE_MARKETPLACE_SLUG}.
2750
+ Browse available templates: https://insforge.dev/templates`
2751
+ );
2752
+ }
2728
2753
  await requireAuth(apiUrl, false);
2729
2754
  if (!json) {
2730
2755
  await animateBanner();
@@ -2781,6 +2806,9 @@ function registerCreateCommand(program2) {
2781
2806
  if (template && !validTemplates.includes(template)) {
2782
2807
  throw new CLIError(`Invalid template "${template}". Valid options: ${validTemplates.join(", ")}`);
2783
2808
  }
2809
+ if (opts.marketplace) {
2810
+ template = opts.marketplace;
2811
+ }
2784
2812
  if (!template) {
2785
2813
  if (json) {
2786
2814
  template = "empty";
@@ -2871,7 +2899,19 @@ function registerCreateCommand(program2) {
2871
2899
  projectLinked = true;
2872
2900
  s?.stop(`Project "${project.name}" created and linked`);
2873
2901
  const githubTemplates = ["chatbot", "crm", "e-commerce", "nextjs", "react", "todo"];
2874
- if (githubTemplates.includes(template)) {
2902
+ if (opts.marketplace) {
2903
+ const downloaded = await downloadGitHubTemplate(
2904
+ opts.marketplace,
2905
+ projectConfig,
2906
+ json
2907
+ );
2908
+ if (downloaded) {
2909
+ void reportMarketplaceDownload(
2910
+ opts.marketplace,
2911
+ apiUrl ?? "https://api.insforge.dev"
2912
+ );
2913
+ }
2914
+ } else if (githubTemplates.includes(template)) {
2875
2915
  await downloadGitHubTemplate(template, projectConfig, json);
2876
2916
  } else if (hasTemplate) {
2877
2917
  await downloadTemplate(template, projectConfig, projectName, json, apiUrl);
@@ -3136,6 +3176,7 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
3136
3176
  }
3137
3177
  }
3138
3178
  }
3179
+ return true;
3139
3180
  } catch (err) {
3140
3181
  s?.stop(`${templateName} template download failed`);
3141
3182
  const msg = `Failed to download ${templateName} template: ${err.message}`;
@@ -3145,11 +3186,25 @@ async function downloadGitHubTemplate(templateName, projectConfig, json) {
3145
3186
  clack12.log.warn(msg);
3146
3187
  clack12.log.info("You can manually clone from: https://github.com/InsForge/insforge-templates");
3147
3188
  }
3189
+ return false;
3148
3190
  } finally {
3149
3191
  await fs4.rm(tempDir, { recursive: true, force: true }).catch(() => {
3150
3192
  });
3151
3193
  }
3152
3194
  }
3195
+ async function reportMarketplaceDownload(slug, apiUrl) {
3196
+ try {
3197
+ const res = await fetch(`${apiUrl}/templates/v1/${encodeURIComponent(slug)}/downloads`, {
3198
+ method: "POST",
3199
+ headers: { "Content-Type": "application/json" },
3200
+ body: "{}"
3201
+ });
3202
+ if (!res.ok) {
3203
+ return;
3204
+ }
3205
+ } catch {
3206
+ }
3207
+ }
3153
3208
 
3154
3209
  // src/commands/projects/link.ts
3155
3210
  var execAsync3 = promisify4(exec3);
@@ -4539,19 +4594,21 @@ function registerFunctionsCommands(functionsCmd2) {
4539
4594
 
4540
4595
  // src/commands/functions/deploy.ts
4541
4596
  import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
4542
- import { join as join10 } from "path";
4597
+ function resolveDeployFilePath(opts) {
4598
+ if (!opts.file) {
4599
+ throw new CLIError("Missing required option: --file <path>");
4600
+ }
4601
+ if (!existsSync6(opts.file)) {
4602
+ throw new CLIError(`Source file not found: ${opts.file}`);
4603
+ }
4604
+ return opts.file;
4605
+ }
4543
4606
  function registerFunctionsDeployCommand(functionsCmd2) {
4544
4607
  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) => {
4545
4608
  const { json } = getRootOpts(cmd);
4546
4609
  try {
4547
4610
  await requireAuth();
4548
- const filePath = opts.file ?? join10(process.cwd(), "insforge", "functions", slug, "index.ts");
4549
- if (!existsSync6(filePath)) {
4550
- throw new CLIError(
4551
- `Source file not found: ${filePath}
4552
- Specify --file <path> or create ${join10("insforge", "functions", slug, "index.ts")}`
4553
- );
4554
- }
4611
+ const filePath = resolveDeployFilePath(opts);
4555
4612
  const code = readFileSync5(filePath, "utf-8");
4556
4613
  const name = opts.name ?? slug;
4557
4614
  const description = opts.description ?? "";
@@ -4783,7 +4840,7 @@ function registerStorageUploadCommand(storageCmd2) {
4783
4840
 
4784
4841
  // src/commands/storage/download.ts
4785
4842
  import { writeFileSync as writeFileSync5 } from "fs";
4786
- import { join as join11, basename as basename7 } from "path";
4843
+ import { join as join10, basename as basename7 } from "path";
4787
4844
  function registerStorageDownloadCommand(storageCmd2) {
4788
4845
  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) => {
4789
4846
  const { json } = getRootOpts(cmd);
@@ -4803,7 +4860,7 @@ function registerStorageDownloadCommand(storageCmd2) {
4803
4860
  throw new CLIError(err.error ?? `Download failed: ${res.status}`);
4804
4861
  }
4805
4862
  const buffer = Buffer.from(await res.arrayBuffer());
4806
- const outputPath = opts.output ?? join11(process.cwd(), basename7(objectKey));
4863
+ const outputPath = opts.output ?? join10(process.cwd(), basename7(objectKey));
4807
4864
  writeFileSync5(outputPath, buffer);
4808
4865
  if (json) {
4809
4866
  outputJson({ success: true, path: outputPath, size: buffer.length });
@@ -5935,7 +5992,7 @@ function registerComputeEventsCommand(computeCmd2) {
5935
5992
 
5936
5993
  // src/commands/compute/deploy.ts
5937
5994
  import { existsSync as existsSync9 } from "fs";
5938
- import { join as join13, resolve as resolve4 } from "path";
5995
+ import { join as join12, resolve as resolve4 } from "path";
5939
5996
 
5940
5997
  // src/lib/env-file.ts
5941
5998
  import { readFileSync as readFileSync7 } from "fs";
@@ -5980,7 +6037,7 @@ function parseEnvFile(path6) {
5980
6037
  // src/lib/flyctl.ts
5981
6038
  import { spawn, spawnSync } from "child_process";
5982
6039
  import { existsSync as existsSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
5983
- import { join as join12 } from "path";
6040
+ import { join as join11 } from "path";
5984
6041
  function ensureFlyctlAvailable() {
5985
6042
  const r = spawnSync("flyctl", ["version"], {
5986
6043
  encoding: "utf8",
@@ -5993,7 +6050,7 @@ function ensureFlyctlAvailable() {
5993
6050
  }
5994
6051
  }
5995
6052
  function ensureFlyTomlStub(opts) {
5996
- const path6 = join12(opts.dir, "fly.toml");
6053
+ const path6 = join11(opts.dir, "fly.toml");
5997
6054
  if (existsSync8(path6)) {
5998
6055
  return () => {
5999
6056
  };
@@ -6181,7 +6238,7 @@ function registerComputeDeployCommand(computeCmd2) {
6181
6238
  return;
6182
6239
  }
6183
6240
  const absDir = resolve4(dir);
6184
- const dockerfilePath = join13(absDir, "Dockerfile");
6241
+ const dockerfilePath = join12(absDir, "Dockerfile");
6185
6242
  if (!existsSync9(dockerfilePath)) {
6186
6243
  throw new CLIError(
6187
6244
  `No Dockerfile at ${dockerfilePath}.
@@ -6961,7 +7018,7 @@ function registerDiagnoseCommands(diagnoseCmd2) {
6961
7018
  const s = !json ? clack15.spinner() : null;
6962
7019
  s?.start("Collecting diagnostic data...");
6963
7020
  const data2 = await collectDiagnosticData(projectId, ossMode, apiUrl);
6964
- const cliVersion = "0.1.80";
7021
+ const cliVersion = "0.1.82";
6965
7022
  s?.stop("Data collected");
6966
7023
  if (!json) {
6967
7024
  console.log(`
@@ -8325,7 +8382,6 @@ function registerPaymentsCommands(paymentsCmd2) {
8325
8382
  }
8326
8383
 
8327
8384
  // src/commands/posthog/setup.ts
8328
- import { spawnSync as spawnSync2 } from "child_process";
8329
8385
  import * as clack16 from "@clack/prompts";
8330
8386
  import pc3 from "picocolors";
8331
8387
 
@@ -8528,41 +8584,20 @@ async function runSetup(opts) {
8528
8584
  outputSuccess(`Linked to InsForge project: ${proj.project_name} (${proj.project_id})`);
8529
8585
  }
8530
8586
  const dashboardConnection = await ensureDashboardConnection(proj.project_id, token, opts);
8531
- if (opts.json) {
8532
- return {
8533
- dashboardConnection,
8534
- wizardSkipped: true,
8535
- wizardCommand: WIZARD_COMMAND
8536
- };
8537
- }
8538
- outputInfo("Running the official PostHog setup wizard to wire PostHog into your app...");
8539
- outputInfo(
8540
- pc3.dim("(it will open a browser for OAuth and let you pick a PostHog project)")
8541
- );
8542
- const wizardResult = spawnSync2(NPX_COMMAND, ["-y", "@posthog/wizard@latest"], {
8543
- stdio: "inherit",
8544
- env: process.env
8545
- });
8546
- if (wizardResult.error) {
8547
- throw new CLIError(`Failed to launch PostHog wizard: ${wizardResult.error.message}`);
8548
- }
8549
- const exitCode = wizardResult.status ?? 1;
8550
- if (wizardResult.signal === "SIGINT" || exitCode === 130) {
8551
- clack16.outro("Setup cancelled.");
8552
- return {
8553
- dashboardConnection,
8554
- wizardSkipped: false,
8555
- wizardExitCode: exitCode
8556
- };
8557
- }
8558
- if (exitCode !== 0) {
8559
- throw new CLIError(`PostHog wizard exited with code ${exitCode}.`);
8587
+ if (!opts.json) {
8588
+ clack16.note(
8589
+ `Run this in your terminal to wire PostHog into your app code:
8590
+
8591
+ ${WIZARD_COMMAND}
8592
+
8593
+ Once it completes, open the Analytics page in your InsForge dashboard.`,
8594
+ "Next step"
8595
+ );
8560
8596
  }
8561
- clack16.outro("Done. Open the Analytics page in your InsForge dashboard to view data.");
8562
8597
  return {
8563
8598
  dashboardConnection,
8564
- wizardSkipped: false,
8565
- wizardExitCode: exitCode
8599
+ wizardSkipped: true,
8600
+ wizardCommand: WIZARD_COMMAND
8566
8601
  };
8567
8602
  }
8568
8603
  async function ensureDashboardConnection(projectId, token, opts) {
@@ -8589,10 +8624,14 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8589
8624
  process.stderr.write("Your browser should open automatically. If not, copy the URL above.\n");
8590
8625
  } else {
8591
8626
  clack16.log.info("PostHog is not yet connected to your InsForge dashboard.");
8592
- outputInfo("");
8593
- outputInfo(`Open this URL to authorize PostHog:
8594
- ${pc3.cyan(pc3.underline(authorizeUrl))}`);
8595
- outputInfo("");
8627
+ if (opts.skipBrowser) {
8628
+ clack16.log.info(`Open this URL to authorize PostHog:
8629
+ ${pc3.cyan(pc3.underline(authorizeUrl))}`);
8630
+ } else {
8631
+ clack16.log.info("Opening browser to authorize PostHog...");
8632
+ clack16.log.info(`If browser doesn't open, visit:
8633
+ ${pc3.cyan(pc3.underline(authorizeUrl))}`);
8634
+ }
8596
8635
  }
8597
8636
  if (!opts.skipBrowser) {
8598
8637
  try {
@@ -8602,7 +8641,11 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8602
8641
  }
8603
8642
  }
8604
8643
  const spinner11 = !opts.json && isInteractive ? clack16.spinner() : null;
8605
- spinner11?.start("Waiting for InsForge dashboard connection... (timeout: 15 minutes)");
8644
+ if (spinner11) {
8645
+ spinner11.start("Waiting for InsForge dashboard connection... (timeout: 15 minutes)");
8646
+ } else if (!opts.json) {
8647
+ clack16.log.info("Waiting for InsForge dashboard connection (up to 15 minutes)...");
8648
+ }
8606
8649
  try {
8607
8650
  await pollPosthogConnection(
8608
8651
  projectId,
@@ -8622,9 +8665,17 @@ async function runConnectFlow(projectId, token, authorizeUrl, opts) {
8622
8665
  },
8623
8666
  opts.apiUrl
8624
8667
  );
8625
- spinner11?.stop("InsForge dashboard connection received.");
8668
+ if (spinner11) {
8669
+ spinner11.stop("InsForge dashboard connection received.");
8670
+ } else if (!opts.json) {
8671
+ clack16.log.success("InsForge dashboard connection received.");
8672
+ }
8626
8673
  } catch (err) {
8627
- spinner11?.stop("InsForge dashboard connection wait failed.");
8674
+ if (spinner11) {
8675
+ spinner11.stop("InsForge dashboard connection wait failed.");
8676
+ } else if (!opts.json) {
8677
+ clack16.log.error("InsForge dashboard connection wait failed.");
8678
+ }
8628
8679
  throw err;
8629
8680
  }
8630
8681
  }
@@ -9111,7 +9162,9 @@ function configFromMetadata(raw) {
9111
9162
  function registerConfigExportCommand(cfg) {
9112
9163
  cfg.command("export").description("Pull live project config and write insforge.toml").option("--out <path>", "output path", "insforge.toml").option("--force", "overwrite without confirmation").action(async (opts, cmd) => {
9113
9164
  const { json } = getRootOpts(cmd);
9165
+ let projectConfig = null;
9114
9166
  try {
9167
+ projectConfig = getProjectConfig();
9115
9168
  await requireAuth();
9116
9169
  const target = resolve5(process.cwd(), opts.out);
9117
9170
  if (existsSync10(target) && !opts.force) {
@@ -9128,6 +9181,12 @@ function registerConfigExportCommand(cfg) {
9128
9181
  });
9129
9182
  if (!ok || p.isCancel(ok)) {
9130
9183
  console.log("Aborted.");
9184
+ await reportCliUsage("cli.config.export", true);
9185
+ trackConfig("export", projectConfig, {
9186
+ json_mode: !!json,
9187
+ force: !!opts.force,
9188
+ outcome: "aborted"
9189
+ });
9131
9190
  return;
9132
9191
  }
9133
9192
  }
@@ -9149,9 +9208,23 @@ function registerConfigExportCommand(cfg) {
9149
9208
  }
9150
9209
  }
9151
9210
  await reportCliUsage("cli.config.export", true);
9211
+ trackConfig("export", projectConfig, {
9212
+ json_mode: !!json,
9213
+ force: !!opts.force,
9214
+ skipped_count: skipped.length,
9215
+ outcome: "success"
9216
+ });
9152
9217
  } catch (err) {
9153
9218
  await reportCliUsage("cli.config.export", false);
9219
+ trackConfig("export", projectConfig, {
9220
+ json_mode: !!json,
9221
+ force: !!opts.force,
9222
+ outcome: "error"
9223
+ });
9224
+ await shutdownAnalytics();
9154
9225
  handleError(err, json);
9226
+ } finally {
9227
+ await shutdownAnalytics();
9155
9228
  }
9156
9229
  });
9157
9230
  }
@@ -9460,7 +9533,9 @@ function authPasswordWireKey(key) {
9460
9533
  function registerConfigPlanCommand(cfg) {
9461
9534
  cfg.command("plan").description("Show diff between insforge.toml and live project state").option("--file <path>", "path to insforge.toml", "insforge.toml").action(async (opts, cmd) => {
9462
9535
  const { json } = getRootOpts(cmd);
9536
+ let projectConfig = null;
9463
9537
  try {
9538
+ projectConfig = getProjectConfig();
9464
9539
  await requireAuth();
9465
9540
  const tomlPath = resolve6(process.cwd(), opts.file);
9466
9541
  const tomlSource = readFileSync8(tomlPath, "utf8");
@@ -9483,9 +9558,25 @@ function registerConfigPlanCommand(cfg) {
9483
9558
  }
9484
9559
  }
9485
9560
  await reportCliUsage("cli.config.plan", true);
9561
+ trackConfig("plan", projectConfig, {
9562
+ json_mode: !!json,
9563
+ changes_count: result.changes.length,
9564
+ skipped_count: skipped.length,
9565
+ sections_changed: Array.from(
9566
+ new Set(result.changes.map((c) => changePath(c)))
9567
+ ),
9568
+ outcome: "success"
9569
+ });
9486
9570
  } catch (err) {
9487
9571
  await reportCliUsage("cli.config.plan", false);
9572
+ trackConfig("plan", projectConfig, {
9573
+ json_mode: !!json,
9574
+ outcome: "error"
9575
+ });
9576
+ await shutdownAnalytics();
9488
9577
  handleError(err, json);
9578
+ } finally {
9579
+ await shutdownAnalytics();
9489
9580
  }
9490
9581
  });
9491
9582
  }
@@ -9498,7 +9589,9 @@ import pc6 from "picocolors";
9498
9589
  function registerConfigApplyCommand(cfg) {
9499
9590
  cfg.command("apply").description("Apply insforge.toml to the live project").option("--file <path>", "path to insforge.toml", "insforge.toml").option("--dry-run", "show plan, do not apply").option("--auto-approve", "skip confirmation prompt").action(async (opts, cmd) => {
9500
9591
  const { json, yes } = getRootOpts(cmd);
9592
+ let projectConfig = null;
9501
9593
  try {
9594
+ projectConfig = getProjectConfig();
9502
9595
  await requireAuth();
9503
9596
  const tomlPath = resolve7(process.cwd(), opts.file);
9504
9597
  const tomlSource = readFileSync9(tomlPath, "utf8");
@@ -9508,6 +9601,9 @@ function registerConfigApplyCommand(cfg) {
9508
9601
  const live = liveFromMetadata(raw);
9509
9602
  const result = diffConfig({ live, file });
9510
9603
  const approved = opts.autoApprove || yes;
9604
+ const sectionsChanged = Array.from(
9605
+ new Set(result.changes.map((c) => changePath(c)))
9606
+ );
9511
9607
  if (!json) {
9512
9608
  console.log(formatPlan(result));
9513
9609
  }
@@ -9518,6 +9614,13 @@ function registerConfigApplyCommand(cfg) {
9518
9614
  );
9519
9615
  }
9520
9616
  await reportCliUsage("cli.config.apply", true);
9617
+ trackConfig("apply", projectConfig, {
9618
+ dry_run: !!opts.dryRun,
9619
+ json_mode: !!json,
9620
+ changes_count: result.changes.length,
9621
+ sections_changed: sectionsChanged,
9622
+ outcome: result.changes.length === 0 ? "no_changes" : "dry_run"
9623
+ });
9521
9624
  return;
9522
9625
  }
9523
9626
  if (!approved) {
@@ -9535,6 +9638,12 @@ function registerConfigApplyCommand(cfg) {
9535
9638
  if (!ok || p2.isCancel(ok)) {
9536
9639
  console.log("Aborted.");
9537
9640
  await reportCliUsage("cli.config.apply", true);
9641
+ trackConfig("apply", projectConfig, {
9642
+ json_mode: !!json,
9643
+ changes_count: result.changes.length,
9644
+ sections_changed: sectionsChanged,
9645
+ outcome: "aborted"
9646
+ });
9538
9647
  return;
9539
9648
  }
9540
9649
  }
@@ -9571,9 +9680,25 @@ function registerConfigApplyCommand(cfg) {
9571
9680
  }
9572
9681
  }
9573
9682
  await reportCliUsage("cli.config.apply", true);
9683
+ trackConfig("apply", projectConfig, {
9684
+ auto_approved: !!approved,
9685
+ json_mode: !!json,
9686
+ changes_count: result.changes.length,
9687
+ applied_count: applied.length,
9688
+ skipped_count: skipped.length,
9689
+ sections_changed: sectionsChanged,
9690
+ outcome: applied.length > 0 ? "applied" : "all_skipped"
9691
+ });
9574
9692
  } catch (err) {
9575
9693
  await reportCliUsage("cli.config.apply", false);
9694
+ trackConfig("apply", projectConfig, {
9695
+ json_mode: !!json,
9696
+ outcome: "error"
9697
+ });
9698
+ await shutdownAnalytics();
9576
9699
  handleError(err, json);
9700
+ } finally {
9701
+ await shutdownAnalytics();
9577
9702
  }
9578
9703
  });
9579
9704
  }
@@ -9659,7 +9784,7 @@ function registerConfigCommand(program2) {
9659
9784
 
9660
9785
  // src/commands/ai/setup.ts
9661
9786
  import { appendFileSync as appendFileSync2, existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
9662
- import { isAbsolute, join as join14, relative as relative3, resolve as resolve8 } from "path";
9787
+ import { isAbsolute, join as join13, relative as relative3, resolve as resolve8 } from "path";
9663
9788
  import * as clack17 from "@clack/prompts";
9664
9789
  import pc7 from "picocolors";
9665
9790
 
@@ -9835,7 +9960,7 @@ function ensureLocalEnvIgnored(cwd, envFile) {
9835
9960
  if (!relEnvPath || relEnvPath.startsWith("..") || isAbsolute(relEnvPath)) {
9836
9961
  return false;
9837
9962
  }
9838
- const gitignorePath = join14(cwd, ".gitignore");
9963
+ const gitignorePath = join13(cwd, ".gitignore");
9839
9964
  const existing = existsSync12(gitignorePath) ? readFileSync11(gitignorePath, "utf-8") : "";
9840
9965
  const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()));
9841
9966
  const envBasename = envFile.replace(/\\/g, "/").split("/").pop() ?? envFile;
@@ -9857,7 +9982,7 @@ function registerAiCommands(aiCmd2) {
9857
9982
 
9858
9983
  // src/index.ts
9859
9984
  var __dirname = dirname2(fileURLToPath(import.meta.url));
9860
- var pkg = JSON.parse(readFileSync12(join15(__dirname, "../package.json"), "utf-8"));
9985
+ var pkg = JSON.parse(readFileSync12(join14(__dirname, "../package.json"), "utf-8"));
9861
9986
  var INSFORGE_LOGO = `
9862
9987
  \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
9863
9988
  \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