@eide/foir-cli 0.10.2 → 0.11.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +325 -46
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1188,7 +1188,8 @@ function createAppsMethods(client) {
1188
1188
  sourceMappings: params.sourceMappings ?? {},
1189
1189
  sinkMappings: params.sinkMappings ?? {},
1190
1190
  settings: params.settings,
1191
- placementFieldChoices: params.placementFieldChoices ?? {}
1191
+ placementFieldChoices: params.placementFieldChoices ?? {},
1192
+ updatePushSnapshot: params.updatePushSnapshot ?? false
1192
1193
  })
1193
1194
  );
1194
1195
  return resp.app;
@@ -1222,7 +1223,9 @@ function createAppsMethods(client) {
1222
1223
  name: params.name,
1223
1224
  sourceMappings: params.sourceMappings ?? {},
1224
1225
  sinkMappings: params.sinkMappings ?? {},
1225
- placementFieldChoices: params.placementFieldChoices ?? {}
1226
+ placementFieldChoices: params.placementFieldChoices ?? {},
1227
+ updatePushSnapshot: params.updatePushSnapshot ?? false,
1228
+ snapshotOnly: params.snapshotOnly ?? false
1226
1229
  })
1227
1230
  );
1228
1231
  return resp.app;
@@ -1347,7 +1350,8 @@ function createModelsMethods(client) {
1347
1350
  name: params.name,
1348
1351
  fields: params.fields.map(jsFieldToProto),
1349
1352
  config: params.config ? jsConfigToProto(params.config) : void 0,
1350
- configId: params.configId
1353
+ configId: params.configId,
1354
+ updatePushSnapshot: params.updatePushSnapshot ?? false
1351
1355
  })
1352
1356
  );
1353
1357
  return resp.model ?? null;
@@ -1360,7 +1364,9 @@ function createModelsMethods(client) {
1360
1364
  updateFields: params.fields != null,
1361
1365
  fields: params.fields?.map(jsFieldToProto) ?? [],
1362
1366
  config: params.config ? jsConfigToProto(params.config) : void 0,
1363
- changeDescription: params.changeDescription
1367
+ changeDescription: params.changeDescription,
1368
+ updatePushSnapshot: params.updatePushSnapshot ?? false,
1369
+ snapshotOnly: params.snapshotOnly ?? false
1364
1370
  })
1365
1371
  );
1366
1372
  return resp.model ?? null;
@@ -4866,6 +4872,93 @@ import { resolve as resolve4 } from "path";
4866
4872
  function zeroCounts() {
4867
4873
  return { created: 0, updated: 0, deleted: 0 };
4868
4874
  }
4875
+ var PushConflictError = class extends Error {
4876
+ conflicts;
4877
+ constructor(conflicts) {
4878
+ super(
4879
+ `Push conflicts with admin-UI changes on ${conflicts.length} field${conflicts.length === 1 ? "" : "s"}. Run \`foir pull\` to sync, or re-run with --force to overwrite.`
4880
+ );
4881
+ this.name = "PushConflictError";
4882
+ this.conflicts = conflicts;
4883
+ }
4884
+ };
4885
+ function canonicalize(v) {
4886
+ if (v === void 0) return "undefined";
4887
+ if (v === null || typeof v !== "object") return JSON.stringify(v);
4888
+ if (Array.isArray(v)) return "[" + v.map(canonicalize).join(",") + "]";
4889
+ const obj = v;
4890
+ const keys = Object.keys(obj).sort();
4891
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
4892
+ }
4893
+ function deepEqual(a, b) {
4894
+ return canonicalize(a) === canonicalize(b);
4895
+ }
4896
+ function indexByKey(items) {
4897
+ const out = /* @__PURE__ */ new Map();
4898
+ for (const it of items ?? []) {
4899
+ if (it && it.key) out.set(it.key, it);
4900
+ }
4901
+ return out;
4902
+ }
4903
+ function threeWayMergeKeyed(config2, platform, snapshot, parentLabel) {
4904
+ const conflicts = [];
4905
+ const merged = [];
4906
+ const keys = /* @__PURE__ */ new Set([
4907
+ ...config2.keys(),
4908
+ ...platform.keys(),
4909
+ ...snapshot ? snapshot.keys() : []
4910
+ ]);
4911
+ const noSnapshot = snapshot === null;
4912
+ for (const key of keys) {
4913
+ const inC = config2.has(key);
4914
+ const inP = platform.has(key);
4915
+ const inS = snapshot ? snapshot.has(key) : false;
4916
+ const c = config2.get(key);
4917
+ const p = platform.get(key);
4918
+ const s = snapshot ? snapshot.get(key) : void 0;
4919
+ if (noSnapshot) {
4920
+ if (inC) merged.push(c);
4921
+ else if (inP) merged.push(p);
4922
+ continue;
4923
+ }
4924
+ if (inC && inP && inS) {
4925
+ if (deepEqual(p, s)) {
4926
+ merged.push(c);
4927
+ } else if (deepEqual(c, s)) {
4928
+ merged.push(p);
4929
+ } else if (deepEqual(c, p)) {
4930
+ merged.push(c);
4931
+ } else {
4932
+ conflicts.push({ fieldKey: key, snapshot: s, platform: p, config: c, kind: "modified-both" });
4933
+ merged.push(p);
4934
+ }
4935
+ } else if (inC && !inS && !inP) {
4936
+ merged.push(c);
4937
+ } else if (inC && inS && !inP) {
4938
+ if (deepEqual(c, s)) {
4939
+ continue;
4940
+ }
4941
+ conflicts.push({ fieldKey: key, snapshot: s, platform: void 0, config: c, kind: "deleted-remotely-modified-locally" });
4942
+ } else if (!inC && inS && inP) {
4943
+ if (deepEqual(p, s)) {
4944
+ continue;
4945
+ }
4946
+ conflicts.push({ fieldKey: key, snapshot: s, platform: p, config: void 0, kind: "deleted-locally-modified-remotely" });
4947
+ merged.push(p);
4948
+ } else if (!inC && !inS && inP) {
4949
+ merged.push(p);
4950
+ } else if (inC && !inS && inP) {
4951
+ if (deepEqual(c, p)) {
4952
+ merged.push(c);
4953
+ } else {
4954
+ conflicts.push({ fieldKey: key, snapshot: void 0, platform: p, config: c, kind: "modified-both" });
4955
+ merged.push(p);
4956
+ }
4957
+ }
4958
+ }
4959
+ void parentLabel;
4960
+ return { merged, conflicts };
4961
+ }
4869
4962
  async function reconcileConfig(client, configId, manifest, options = {}) {
4870
4963
  const summary = {
4871
4964
  models: zeroCounts(),
@@ -4880,7 +4973,9 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
4880
4973
  apps: zeroCounts()
4881
4974
  };
4882
4975
  const operationBaseUrl = manifest.operationBaseUrl ?? "";
4883
- await reconcileModels(client, configId, manifest.models ?? [], summary);
4976
+ const modelConflicts = [];
4977
+ const mappingConflicts = [];
4978
+ await reconcileModels(client, configId, manifest.models ?? [], summary, options.force ?? false, modelConflicts);
4884
4979
  await reconcileOperations(client, configId, manifest.operations ?? [], operationBaseUrl, summary);
4885
4980
  await reconcileHooks(client, configId, manifest.hooks ?? [], summary);
4886
4981
  await reconcileSegments(client, configId, manifest.segments ?? [], summary);
@@ -4900,11 +4995,17 @@ async function reconcileConfig(client, configId, manifest, options = {}) {
4900
4995
  options.tenantId,
4901
4996
  options.projectId,
4902
4997
  manifest.apps ?? {},
4903
- summary
4998
+ summary,
4999
+ options.force ?? false,
5000
+ mappingConflicts
4904
5001
  );
5002
+ const allConflicts = [...modelConflicts, ...mappingConflicts];
5003
+ if (allConflicts.length > 0 && !options.force) {
5004
+ throw new PushConflictError(allConflicts);
5005
+ }
4905
5006
  return summary;
4906
5007
  }
4907
- async function reconcileModels(client, configId, models, summary) {
5008
+ async function reconcileModels(client, configId, models, summary, force, conflictOut) {
4908
5009
  const existing = await client.models.listModels({ limit: 200 });
4909
5010
  const allByKey = new Map(
4910
5011
  existing.items.map((m) => [m.key, m])
@@ -4913,6 +5014,7 @@ async function reconcileModels(client, configId, models, summary) {
4913
5014
  existing.items.filter((m) => m.configId === configId).map((m) => [m.key, m])
4914
5015
  );
4915
5016
  const manifestKeys = /* @__PURE__ */ new Set();
5017
+ const plans = [];
4916
5018
  for (const m of models) {
4917
5019
  if (!m.key || !m.name) continue;
4918
5020
  manifestKeys.add(m.key);
@@ -4921,30 +5023,77 @@ async function reconcileModels(client, configId, models, summary) {
4921
5023
  if (m.pluralKey) config2.pluralKey = m.pluralKey;
4922
5024
  if (m.description) config2.description = m.description;
4923
5025
  const ex = allByKey.get(m.key);
4924
- if (ex) {
4925
- await client.models.updateModel({
4926
- id: ex.id,
4927
- name: m.name,
4928
- fields: m.fields,
4929
- config: config2
5026
+ if (!ex) {
5027
+ plans.push({ kind: "create", model: m, config: config2 });
5028
+ continue;
5029
+ }
5030
+ const cfgFields = m.fields ?? [];
5031
+ const platFields = ex.fields ?? [];
5032
+ const snapFields = ex.lastPushedFields;
5033
+ const hasSnapshot = snapFields !== void 0 && snapFields !== null;
5034
+ const { merged, conflicts } = threeWayMergeKeyed(
5035
+ indexByKey(cfgFields),
5036
+ indexByKey(platFields),
5037
+ hasSnapshot ? indexByKey(snapFields) : null,
5038
+ `model "${m.key}"`
5039
+ );
5040
+ for (const c of conflicts) {
5041
+ conflictOut.push({ modelKey: m.key, ...c });
5042
+ }
5043
+ plans.push({
5044
+ kind: "update",
5045
+ id: ex.id,
5046
+ name: m.name,
5047
+ mergedFields: merged,
5048
+ config: config2,
5049
+ // The snapshot we persist is the config fields the CLI pushed —
5050
+ // so a re-push with the same config is a no-op.
5051
+ pushSnapshotFields: cfgFields
5052
+ });
5053
+ }
5054
+ for (const [key, ex] of configOwnedByKey) {
5055
+ if (manifestKeys.has(key)) continue;
5056
+ const platFields = ex.fields ?? [];
5057
+ const snapFields = ex.lastPushedFields;
5058
+ if (snapFields !== void 0 && !deepEqual(platFields, snapFields)) {
5059
+ conflictOut.push({
5060
+ modelKey: key,
5061
+ fieldKey: "<model>",
5062
+ snapshot: snapFields,
5063
+ platform: platFields,
5064
+ config: void 0,
5065
+ kind: "deleted-locally-modified-remotely"
4930
5066
  });
4931
- summary.models.updated++;
4932
- } else {
5067
+ if (!force) continue;
5068
+ }
5069
+ plans.push({ kind: "delete", id: ex.id });
5070
+ }
5071
+ if (conflictOut.length > 0 && !force) {
5072
+ throw new PushConflictError(conflictOut);
5073
+ }
5074
+ for (const p of plans) {
5075
+ if (p.kind === "create") {
4933
5076
  await client.models.createModel({
4934
- key: m.key,
4935
- name: m.name,
4936
- fields: m.fields,
4937
- config: config2,
4938
- configId
5077
+ key: p.model.key,
5078
+ name: p.model.name,
5079
+ fields: p.model.fields,
5080
+ config: p.config,
5081
+ configId,
5082
+ updatePushSnapshot: true
4939
5083
  });
4940
5084
  summary.models.created++;
4941
- }
4942
- }
4943
- for (const [key, ex] of configOwnedByKey) {
4944
- if (!manifestKeys.has(key)) {
4945
- await client.models.deleteModel(
4946
- ex.id
4947
- );
5085
+ } else if (p.kind === "update") {
5086
+ const fieldsToWrite = force ? p.pushSnapshotFields : p.mergedFields;
5087
+ await client.models.updateModel({
5088
+ id: p.id,
5089
+ name: p.name,
5090
+ fields: fieldsToWrite,
5091
+ config: p.config,
5092
+ updatePushSnapshot: true
5093
+ });
5094
+ summary.models.updated++;
5095
+ } else {
5096
+ await client.models.deleteModel(p.id);
4948
5097
  summary.models.deleted++;
4949
5098
  }
4950
5099
  }
@@ -5303,7 +5452,7 @@ async function reconcileApiKeys(client, configKey, apiKeys, summary, rotateKeys)
5303
5452
  }
5304
5453
  }
5305
5454
  }
5306
- async function reconcileApps(client, tenantId, projectId, apps, summary) {
5455
+ async function reconcileApps(client, tenantId, projectId, apps, summary, force, conflictOut) {
5307
5456
  const entries = Object.entries(apps);
5308
5457
  if (entries.length === 0) return;
5309
5458
  if (!tenantId || !projectId) {
@@ -5314,11 +5463,11 @@ async function reconcileApps(client, tenantId, projectId, apps, summary) {
5314
5463
  }
5315
5464
  const existingList = await client.apps.listApps(tenantId, projectId);
5316
5465
  const existingByName = new Map(existingList.map((a) => [a.name, a]));
5466
+ const plans = [];
5317
5467
  for (const [name, input] of entries) {
5318
5468
  const existing = existingByName.get(name);
5319
5469
  if (!existing) {
5320
- await installApp(client, tenantId, projectId, name, input);
5321
- summary.apps.created += 1;
5470
+ plans.push({ kind: "install", name, input });
5322
5471
  continue;
5323
5472
  }
5324
5473
  if (existing.manifestUrl !== input.source) {
@@ -5327,17 +5476,82 @@ async function reconcileApps(client, tenantId, projectId, apps, summary) {
5327
5476
  );
5328
5477
  continue;
5329
5478
  }
5330
- await client.apps.setAppMapping({
5331
- tenantId,
5332
- projectId,
5479
+ const toBag = (v) => {
5480
+ if (!v || typeof v !== "object") return { sources: {}, sinks: {}, placement_fields: {} };
5481
+ const m = v;
5482
+ return {
5483
+ sources: m.sources ?? {},
5484
+ sinks: m.sinks ?? {},
5485
+ placement_fields: m.placement_fields ?? {}
5486
+ };
5487
+ };
5488
+ const platformBag = toBag(existing.mappings);
5489
+ const snapshotRaw = existing.lastPushedMappings;
5490
+ const hasSnapshot = snapshotRaw !== void 0 && snapshotRaw !== null;
5491
+ const snapshotBag = hasSnapshot ? toBag(snapshotRaw) : null;
5492
+ const configSources = input.mappings?.sources ?? {};
5493
+ const configSinks = input.mappings?.sinks ?? {};
5494
+ const configPlacements = input.mappings?.placementFields ?? {};
5495
+ const mergeBag = (cfg, plat, snap, label) => {
5496
+ const cMap = new Map(Object.entries(cfg));
5497
+ const pMap = new Map(Object.entries(plat));
5498
+ const sMap = snap ? new Map(Object.entries(snap)) : null;
5499
+ const { merged, conflicts } = threeWayMergeKeyed(
5500
+ // The merge helper expects items to have a .key; wrap values.
5501
+ new Map(Array.from(cMap, ([k, v]) => [k, { key: k, value: v }])),
5502
+ new Map(Array.from(pMap, ([k, v]) => [k, { key: k, value: v }])),
5503
+ sMap ? new Map(Array.from(sMap, ([k, v]) => [k, { key: k, value: v }])) : null,
5504
+ `${name} ${label}`
5505
+ );
5506
+ for (const c of conflicts) {
5507
+ conflictOut.push({ modelKey: `app:${name}`, ...c, fieldKey: `${label}.${c.fieldKey}` });
5508
+ }
5509
+ const out = {};
5510
+ for (const item of merged) {
5511
+ out[item.key] = item.value;
5512
+ }
5513
+ return out;
5514
+ };
5515
+ const mergedSources = mergeBag(configSources, platformBag.sources, snapshotBag?.sources ?? null, "sources");
5516
+ const mergedSinks = mergeBag(configSinks, platformBag.sinks, snapshotBag?.sinks ?? null, "sinks");
5517
+ const mergedPlacements = mergeBag(configPlacements, platformBag.placement_fields, snapshotBag?.placement_fields ?? null, "placementFields");
5518
+ plans.push({
5519
+ kind: "setMapping",
5333
5520
  name,
5334
- sourceMappings: toSourceMappings(input.mappings?.sources ?? {}),
5335
- sinkMappings: toSinkMappings(input.mappings?.sinks ?? {}),
5336
- placementFieldChoices: toPlacementFieldChoices(
5337
- input.mappings?.placementFields ?? {}
5338
- )
5521
+ mergedSources,
5522
+ mergedSinks,
5523
+ mergedPlacements,
5524
+ configMappings: {
5525
+ sources: configSources,
5526
+ sinks: configSinks,
5527
+ placementFields: configPlacements
5528
+ }
5339
5529
  });
5340
- summary.apps.updated += 1;
5530
+ }
5531
+ if (conflictOut.length > 0 && !force) {
5532
+ throw new PushConflictError(conflictOut);
5533
+ }
5534
+ for (const p of plans) {
5535
+ if (p.kind === "install") {
5536
+ await installApp(client, tenantId, projectId, p.name, p.input);
5537
+ summary.apps.created += 1;
5538
+ } else if (p.kind === "setMapping") {
5539
+ const write = force ? p.configMappings : {
5540
+ sources: p.mergedSources,
5541
+ sinks: p.mergedSinks,
5542
+ placementFields: p.mergedPlacements
5543
+ };
5544
+ await client.apps.setAppMapping({
5545
+ tenantId,
5546
+ projectId,
5547
+ name: p.name,
5548
+ sourceMappings: toSourceMappings(write.sources),
5549
+ sinkMappings: toSinkMappings(write.sinks),
5550
+ placementFieldChoices: toPlacementFieldChoices(write.placementFields),
5551
+ updatePushSnapshot: true
5552
+ });
5553
+ summary.apps.updated += 1;
5554
+ }
5341
5555
  }
5342
5556
  }
5343
5557
  async function installApp(client, tenantId, projectId, name, input) {
@@ -5376,7 +5590,8 @@ async function installApp(client, tenantId, projectId, name, input) {
5376
5590
  settings: input.settings,
5377
5591
  placementFieldChoices: toPlacementFieldChoices(
5378
5592
  input.mappings?.placementFields ?? {}
5379
- )
5593
+ ),
5594
+ updatePushSnapshot: true
5380
5595
  });
5381
5596
  }
5382
5597
  function toSourceMappings(input) {
@@ -5466,7 +5681,11 @@ function printSummary(summary) {
5466
5681
  }
5467
5682
  }
5468
5683
  function registerPushCommand(program2, globalOpts) {
5469
- program2.command("push").description("Push foir.config.ts to the platform").option("--config <path>", "Path to config file (default: auto-discover)").option("--force", "Force reinstall (delete and recreate)", false).option(
5684
+ program2.command("push").description("Push foir.config.ts to the platform").option("--config <path>", "Path to config file (default: auto-discover)").option(
5685
+ "--force",
5686
+ "Overwrite platform state on three-way-merge conflicts with admin-UI edits. Without this flag, conflicts abort the push.",
5687
+ false
5688
+ ).option(
5470
5689
  "--rotate-keys",
5471
5690
  "Rotate existing API keys and rewrite their values in .env",
5472
5691
  false
@@ -5507,11 +5726,38 @@ function registerPushCommand(program2, globalOpts) {
5507
5726
  }
5508
5727
  const configId = applyResult.id;
5509
5728
  console.log(chalk6.dim("Reconciling resources..."));
5510
- const summary = await reconcileConfig(client, configId, config2, {
5511
- rotateKeys: opts.rotateKeys ?? false,
5512
- tenantId: resolved?.project.tenantId,
5513
- projectId: resolved?.project.id
5514
- });
5729
+ let summary;
5730
+ try {
5731
+ summary = await reconcileConfig(client, configId, config2, {
5732
+ rotateKeys: opts.rotateKeys ?? false,
5733
+ tenantId: resolved?.project.tenantId,
5734
+ projectId: resolved?.project.id,
5735
+ force: opts.force ?? false
5736
+ });
5737
+ } catch (e) {
5738
+ if (e instanceof PushConflictError) {
5739
+ console.error();
5740
+ console.error(chalk6.red("\u2716 Push aborted \u2014 three-way-merge conflicts detected"));
5741
+ console.error();
5742
+ console.error(
5743
+ chalk6.dim(
5744
+ " An admin-UI edit landed on the platform since the last `foir push`, and\n the local config changes the same field. Accepting either side would\n silently discard the other.\n"
5745
+ )
5746
+ );
5747
+ for (const c of e.conflicts) {
5748
+ console.error(chalk6.yellow(` ${c.modelKey} \xB7 ${c.fieldKey}`) + chalk6.dim(` (${c.kind})`));
5749
+ console.error(chalk6.dim(" snapshot: ") + JSON.stringify(c.snapshot));
5750
+ console.error(chalk6.dim(" platform: ") + JSON.stringify(c.platform));
5751
+ console.error(chalk6.dim(" config: ") + JSON.stringify(c.config));
5752
+ }
5753
+ console.error();
5754
+ console.error(chalk6.dim(" Resolution:"));
5755
+ console.error(chalk6.dim(" 1. `foir pull` to sync platform state into local config, OR"));
5756
+ console.error(chalk6.dim(" 2. re-run with --force to overwrite platform with local config"));
5757
+ process.exit(1);
5758
+ }
5759
+ throw e;
5760
+ }
5515
5761
  console.log();
5516
5762
  console.log(chalk6.green("\u2713 Config applied successfully"));
5517
5763
  console.log(` Config ID: ${chalk6.cyan(configId)}`);
@@ -5671,6 +5917,39 @@ export default defineConfig(${jsonContent});
5671
5917
  }
5672
5918
  writeFileSync3(outPath, formatted, "utf-8");
5673
5919
  console.log(chalk7.green(`\u2713 Exported to ${outPath}`));
5920
+ try {
5921
+ const { items: platformModels } = await client.models.listModels({ limit: 200 });
5922
+ for (const pm of platformModels) {
5923
+ await client.models.updateModel({
5924
+ id: pm.id,
5925
+ fields: pm.fields ?? [],
5926
+ snapshotOnly: true,
5927
+ updatePushSnapshot: true
5928
+ });
5929
+ }
5930
+ if (resolved && apps) {
5931
+ const tenantId = resolved.project.tenantId;
5932
+ const projectId = resolved.project.id;
5933
+ for (const [name, input] of Object.entries(apps)) {
5934
+ await client.apps.setAppMapping({
5935
+ tenantId,
5936
+ projectId,
5937
+ name,
5938
+ sourceMappings: input.mappings?.sources ?? {},
5939
+ sinkMappings: input.mappings?.sinks ?? {},
5940
+ placementFieldChoices: input.mappings?.placementFields ?? {},
5941
+ snapshotOnly: true
5942
+ });
5943
+ }
5944
+ }
5945
+ console.log(chalk7.dim(" Synced push snapshots; next push will be a no-op."));
5946
+ } catch (e) {
5947
+ console.warn(
5948
+ chalk7.yellow(
5949
+ ` Warning: failed to refresh push snapshots (${e.message}). Next push may flag spurious conflicts; re-run with --force if needed.`
5950
+ )
5951
+ );
5952
+ }
5674
5953
  const models = configData.models ?? [];
5675
5954
  const operations = configData.operations ?? [];
5676
5955
  const hooks = configData.hooks ?? [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eide/foir-cli",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "Universal platform CLI for Foir platform",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -50,7 +50,7 @@
50
50
  "@bufbuild/protovalidate": "^1.1.1",
51
51
  "@connectrpc/connect": "^2.0.0",
52
52
  "@connectrpc/connect-node": "^2.0.0",
53
- "@eide/foir-proto-ts": "^0.23.0",
53
+ "@eide/foir-proto-ts": "^0.26.0",
54
54
  "chalk": "^5.3.0",
55
55
  "commander": "^12.1.0",
56
56
  "dotenv": "^16.4.5",