owosk 0.1.4 → 0.2.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/index.js +785 -383
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/sync.ts
7
- import * as p from "@clack/prompts";
8
- import pc from "picocolors";
7
+ import * as p2 from "@clack/prompts";
8
+ import pc3 from "picocolors";
9
9
 
10
10
  // src/lib/config.ts
11
11
  import { join } from "path";
@@ -15,7 +15,7 @@ import { writeFile } from "fs/promises";
15
15
  var GLOBAL_CONFIG_DIR = join(homedir(), ".owostack");
16
16
  var GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, "config.json");
17
17
  function getApiUrl(configUrl) {
18
- return process.env.OWOSTACK_API_URL || configUrl || "https://api.owostack.com";
18
+ return process.env.OWOSTACK_API_URL || configUrl || "https://sandbox.owostack.com";
19
19
  }
20
20
  function getTestApiUrl(configUrl) {
21
21
  return process.env.OWOSTACK_API_TEST_URL || configUrl || "https://sandbox.owostack.com";
@@ -73,7 +73,7 @@ function resolveConfigPath(configPath) {
73
73
  async function loadOwostackFromConfig(fullPath) {
74
74
  try {
75
75
  const configModule = await jiti.import(fullPath);
76
- const instance = configModule.default || configModule.owo || configModule;
76
+ const instance = configModule.owo || configModule.default || configModule;
77
77
  if (instance && typeof instance.sync === "function") {
78
78
  return instance;
79
79
  }
@@ -132,45 +132,518 @@ async function loadConfigSettings(configPath) {
132
132
  }
133
133
  }
134
134
 
135
+ // src/lib/api.ts
136
+ import pc from "picocolors";
137
+ async function fetchPlans(options) {
138
+ if (!options.apiKey) {
139
+ console.error(
140
+ `
141
+ \u274C Missing API key. Pass --key or set OWOSTACK_SECRET_KEY.
142
+ `
143
+ );
144
+ process.exit(1);
145
+ }
146
+ const url = new URL(`${options.apiUrl}/plans`);
147
+ if (options.group) url.searchParams.set("group", options.group);
148
+ if (options.interval) url.searchParams.set("interval", options.interval);
149
+ if (options.currency) url.searchParams.set("currency", options.currency);
150
+ if (options.includeInactive) url.searchParams.set("includeInactive", "true");
151
+ try {
152
+ const response = await fetch(url.toString(), {
153
+ method: "GET",
154
+ headers: { Authorization: `Bearer ${options.apiKey}` }
155
+ });
156
+ const data = await response.json();
157
+ if (!response.ok || !data?.success) {
158
+ const message = data?.error || data?.message || "Request failed";
159
+ console.error(`
160
+ \u274C Failed to fetch plans: ${message}
161
+ `);
162
+ process.exit(1);
163
+ }
164
+ return data?.plans || [];
165
+ } catch (error) {
166
+ if (error.name === "TypeError" && error.message.includes("fetch failed")) {
167
+ console.error(
168
+ `
169
+ \u274C Connection failed: Could not reach the API at ${pc.cyan(options.apiUrl)}`
170
+ );
171
+ console.error(
172
+ ` Please check your internet connection or ensure the API is running.`
173
+ );
174
+ console.error(
175
+ ` You can override the API URL by setting the ${pc.bold("OWOSTACK_API_URL")} environment variable.
176
+ `
177
+ );
178
+ } else {
179
+ console.error(`
180
+ \u274C Unexpected error: ${error.message}
181
+ `);
182
+ }
183
+ process.exit(1);
184
+ }
185
+ }
186
+ async function fetchCreditSystems(apiKey, apiUrl) {
187
+ if (!apiKey) {
188
+ return [];
189
+ }
190
+ try {
191
+ const url = `${apiUrl}/credit-systems`;
192
+ const response = await fetch(url, {
193
+ method: "GET",
194
+ headers: { Authorization: `Bearer ${apiKey}` }
195
+ });
196
+ const data = await response.json();
197
+ if (!response.ok || !data?.success) {
198
+ return [];
199
+ }
200
+ return data?.creditSystems || [];
201
+ } catch {
202
+ return [];
203
+ }
204
+ }
205
+ async function fetchCreditPacks(apiKey, apiUrl) {
206
+ if (!apiKey) {
207
+ return [];
208
+ }
209
+ try {
210
+ const url = `${apiUrl}/credit-packs`;
211
+ const response = await fetch(url, {
212
+ method: "GET",
213
+ headers: { Authorization: `Bearer ${apiKey}` }
214
+ });
215
+ const data = await response.json();
216
+ if (!response.ok || !data?.success) {
217
+ return [];
218
+ }
219
+ return data?.data || [];
220
+ } catch {
221
+ return [];
222
+ }
223
+ }
224
+
225
+ // src/lib/diff.ts
226
+ import pc2 from "picocolors";
227
+ import * as p from "@clack/prompts";
228
+ function normalizeFeature(pf) {
229
+ return {
230
+ slug: pf.slug,
231
+ enabled: pf.enabled,
232
+ limit: pf.limit ?? null,
233
+ // Handle both SDK 'reset' and API 'resetInterval'
234
+ reset: pf.reset || pf.resetInterval || "monthly",
235
+ // Handle both SDK 'overage' and API 'overage' (same name)
236
+ overage: pf.overage || "block",
237
+ overagePrice: pf.overagePrice ?? null
238
+ };
239
+ }
240
+ function normalizePlan(plan) {
241
+ return {
242
+ slug: plan.slug,
243
+ name: plan.name ?? null,
244
+ description: plan.description ?? null,
245
+ price: plan.price ?? 0,
246
+ currency: plan.currency ?? null,
247
+ interval: plan.interval ?? null,
248
+ planGroup: plan.planGroup ?? null,
249
+ trialDays: plan.trialDays ?? 0,
250
+ isAddon: plan.isAddon ?? false,
251
+ autoEnable: plan.autoEnable ?? false,
252
+ features: (plan.features || []).map(normalizeFeature).sort((a, b) => a.slug.localeCompare(b.slug))
253
+ };
254
+ }
255
+ function normalizeCreditPack(pack) {
256
+ return {
257
+ slug: pack.slug,
258
+ name: pack.name ?? null,
259
+ description: pack.description ?? null,
260
+ credits: pack.credits ?? 0,
261
+ price: pack.price ?? 0,
262
+ currency: pack.currency ?? null,
263
+ creditSystem: pack.creditSystem || pack.creditSystemId || null,
264
+ provider: pack.provider ?? null
265
+ };
266
+ }
267
+ function diffPlans(localPlans, remotePlans, localCreditSystems = [], remoteCreditSystems = [], localCreditPacks = [], remoteCreditPacks = []) {
268
+ const localMap = /* @__PURE__ */ new Map();
269
+ const remoteMap = /* @__PURE__ */ new Map();
270
+ for (const p9 of localPlans) localMap.set(p9.slug, normalizePlan(p9));
271
+ for (const p9 of remotePlans) remoteMap.set(p9.slug, normalizePlan(p9));
272
+ const onlyLocal = [];
273
+ const onlyRemote = [];
274
+ const changed = [];
275
+ for (const slug of localMap.keys()) {
276
+ if (!remoteMap.has(slug)) onlyLocal.push(slug);
277
+ }
278
+ for (const slug of remoteMap.keys()) {
279
+ if (!localMap.has(slug)) onlyRemote.push(slug);
280
+ }
281
+ for (const slug of localMap.keys()) {
282
+ if (!remoteMap.has(slug)) continue;
283
+ const local = localMap.get(slug);
284
+ const remote = remoteMap.get(slug);
285
+ const details = [];
286
+ const fields = [
287
+ "name",
288
+ "description",
289
+ "price",
290
+ "currency",
291
+ "interval",
292
+ "planGroup",
293
+ "trialDays",
294
+ "isAddon",
295
+ "autoEnable"
296
+ ];
297
+ for (const field of fields) {
298
+ if (local[field] !== remote[field]) {
299
+ const localVal = JSON.stringify(local[field]);
300
+ const remoteVal = JSON.stringify(remote[field]);
301
+ details.push(
302
+ `${String(field)}: ${pc2.green(localVal)} \u2192 ${pc2.red(remoteVal)}`
303
+ );
304
+ }
305
+ }
306
+ const localFeatures = new Map(
307
+ local.features.map((f) => [f.slug, f])
308
+ );
309
+ const remoteFeatures = new Map(
310
+ remote.features.map((f) => [f.slug, f])
311
+ );
312
+ for (const fslug of localFeatures.keys()) {
313
+ if (!remoteFeatures.has(fslug)) {
314
+ details.push(` ${pc2.green("+")} feature ${pc2.bold(fslug)}`);
315
+ continue;
316
+ }
317
+ const lf = localFeatures.get(fslug);
318
+ const rf = remoteFeatures.get(fslug);
319
+ if (JSON.stringify(lf) !== JSON.stringify(rf)) {
320
+ details.push(` ${pc2.yellow("~")} feature ${pc2.bold(fslug)}`);
321
+ const featureFields = [
322
+ "enabled",
323
+ "limit",
324
+ "reset",
325
+ "overage",
326
+ "overagePrice"
327
+ ];
328
+ for (const ff of featureFields) {
329
+ if (JSON.stringify(lf[ff]) !== JSON.stringify(rf[ff])) {
330
+ const lv = lf[ff] === null ? "unlimited" : String(lf[ff]);
331
+ const rv = rf[ff] === null ? "unlimited" : String(rf[ff]);
332
+ details.push(
333
+ ` ${pc2.dim(String(ff))}: ${pc2.green(lv)} \u2192 ${pc2.red(rv)}`
334
+ );
335
+ }
336
+ }
337
+ }
338
+ }
339
+ for (const fslug of remoteFeatures.keys()) {
340
+ if (!localFeatures.has(fslug)) {
341
+ details.push(` ${pc2.red("-")} feature ${pc2.bold(fslug)}`);
342
+ }
343
+ }
344
+ if (details.length > 0) {
345
+ changed.push({ slug, details });
346
+ }
347
+ }
348
+ const localCsMap = /* @__PURE__ */ new Map();
349
+ const remoteCsMap = /* @__PURE__ */ new Map();
350
+ for (const cs of localCreditSystems) localCsMap.set(cs.slug, cs);
351
+ for (const cs of remoteCreditSystems) remoteCsMap.set(cs.slug, cs);
352
+ const csOnlyLocal = [];
353
+ const csOnlyRemote = [];
354
+ const csChanged = [];
355
+ for (const slug of localCsMap.keys()) {
356
+ if (!remoteCsMap.has(slug)) csOnlyLocal.push(slug);
357
+ }
358
+ for (const slug of remoteCsMap.keys()) {
359
+ if (!localCsMap.has(slug)) csOnlyRemote.push(slug);
360
+ }
361
+ for (const slug of localCsMap.keys()) {
362
+ if (!remoteCsMap.has(slug)) continue;
363
+ const local = localCsMap.get(slug);
364
+ const remote = remoteCsMap.get(slug);
365
+ const details = [];
366
+ if (local.name !== remote.name) {
367
+ details.push(
368
+ `name: ${pc2.green(JSON.stringify(local.name))} \u2192 ${pc2.red(JSON.stringify(remote.name))}`
369
+ );
370
+ }
371
+ if (local.description !== remote.description) {
372
+ details.push(
373
+ `description: ${pc2.green(JSON.stringify(local.description))} \u2192 ${pc2.red(JSON.stringify(remote.description))}`
374
+ );
375
+ }
376
+ const localF = new Map(
377
+ (local.features || []).map((f) => [f.feature, f])
378
+ );
379
+ const remoteF = new Map(
380
+ (remote.features || []).map((f) => [f.feature, f])
381
+ );
382
+ for (const fslug of localF.keys()) {
383
+ if (!remoteF.has(fslug)) {
384
+ details.push(` ${pc2.green("+")} credit cost for ${pc2.bold(fslug)}`);
385
+ continue;
386
+ }
387
+ const lf = localF.get(fslug);
388
+ const rf = remoteF.get(fslug);
389
+ if (lf.creditCost !== rf.creditCost) {
390
+ details.push(
391
+ ` ${pc2.yellow("~")} credit cost for ${pc2.bold(fslug)}: ${pc2.green(String(lf.creditCost))} \u2192 ${pc2.red(String(rf.creditCost))}`
392
+ );
393
+ }
394
+ }
395
+ for (const fslug of remoteF.keys()) {
396
+ if (!localF.has(fslug)) {
397
+ details.push(` ${pc2.red("-")} credit cost for ${pc2.bold(fslug)}`);
398
+ }
399
+ }
400
+ if (details.length > 0) {
401
+ csChanged.push({ slug, details });
402
+ }
403
+ }
404
+ const localPackMap = /* @__PURE__ */ new Map();
405
+ const remotePackMap = /* @__PURE__ */ new Map();
406
+ for (const pack of localCreditPacks)
407
+ localPackMap.set(pack.slug, normalizeCreditPack(pack));
408
+ for (const pack of remoteCreditPacks)
409
+ remotePackMap.set(pack.slug, normalizeCreditPack(pack));
410
+ const packOnlyLocal = [];
411
+ const packOnlyRemote = [];
412
+ const packChanged = [];
413
+ for (const slug of localPackMap.keys()) {
414
+ if (!remotePackMap.has(slug)) packOnlyLocal.push(slug);
415
+ }
416
+ for (const slug of remotePackMap.keys()) {
417
+ if (!localPackMap.has(slug)) packOnlyRemote.push(slug);
418
+ }
419
+ for (const slug of localPackMap.keys()) {
420
+ if (!remotePackMap.has(slug)) continue;
421
+ const local = localPackMap.get(slug);
422
+ const remote = remotePackMap.get(slug);
423
+ const details = [];
424
+ const packFields = [
425
+ "name",
426
+ "description",
427
+ "credits",
428
+ "price",
429
+ "currency",
430
+ "creditSystem",
431
+ "provider"
432
+ ];
433
+ for (const field of packFields) {
434
+ if (local[field] !== remote[field]) {
435
+ const localVal = JSON.stringify(local[field]);
436
+ const remoteVal = JSON.stringify(remote[field]);
437
+ details.push(
438
+ `${String(field)}: ${pc2.green(localVal)} \u2192 ${pc2.red(remoteVal)}`
439
+ );
440
+ }
441
+ }
442
+ if (details.length > 0) {
443
+ packChanged.push({ slug, details });
444
+ }
445
+ }
446
+ return {
447
+ onlyLocal,
448
+ onlyRemote,
449
+ changed,
450
+ creditSystems: {
451
+ onlyLocal: csOnlyLocal,
452
+ onlyRemote: csOnlyRemote,
453
+ changed: csChanged
454
+ },
455
+ creditPacks: {
456
+ onlyLocal: packOnlyLocal,
457
+ onlyRemote: packOnlyRemote,
458
+ changed: packChanged
459
+ }
460
+ };
461
+ }
462
+ function printDiff(diff) {
463
+ const hasPlanDiff = diff.onlyLocal.length > 0 || diff.onlyRemote.length > 0 || diff.changed.length > 0;
464
+ const hasCsDiff = diff.creditSystems.onlyLocal.length > 0 || diff.creditSystems.onlyRemote.length > 0 || diff.creditSystems.changed.length > 0;
465
+ const hasPackDiff = diff.creditPacks.onlyLocal.length > 0 || diff.creditPacks.onlyRemote.length > 0 || diff.creditPacks.changed.length > 0;
466
+ if (!hasPlanDiff && !hasCsDiff && !hasPackDiff) {
467
+ p.log.success(pc2.green("Everything is in sync. No differences found."));
468
+ return;
469
+ }
470
+ if (hasPlanDiff) {
471
+ const lines = [];
472
+ if (diff.onlyLocal.length > 0) {
473
+ for (const slug of diff.onlyLocal) {
474
+ lines.push(
475
+ `${pc2.green("+")} ${pc2.bold(slug)} ${pc2.dim("(local only \u2014 will be created on sync)")}`
476
+ );
477
+ }
478
+ }
479
+ if (diff.onlyRemote.length > 0) {
480
+ for (const slug of diff.onlyRemote) {
481
+ lines.push(
482
+ `${pc2.red("-")} ${pc2.bold(slug)} ${pc2.dim("(remote only \u2014 not in local config)")}`
483
+ );
484
+ }
485
+ }
486
+ if (diff.changed.length > 0) {
487
+ for (const item of diff.changed) {
488
+ lines.push(`${pc2.yellow("~")} ${pc2.bold(item.slug)}`);
489
+ for (const line of item.details) {
490
+ lines.push(` ${line}`);
491
+ }
492
+ }
493
+ }
494
+ p.note(lines.join("\n"), "Plans Diff");
495
+ }
496
+ if (hasCsDiff) {
497
+ const csLines = [];
498
+ const csDiff = diff.creditSystems;
499
+ if (csDiff.onlyLocal.length > 0) {
500
+ for (const slug of csDiff.onlyLocal) {
501
+ csLines.push(
502
+ `${pc2.green("+")} ${pc2.bold(slug)} ${pc2.dim("(local only \u2014 will be created on sync)")}`
503
+ );
504
+ }
505
+ }
506
+ if (csDiff.onlyRemote.length > 0) {
507
+ for (const slug of csDiff.onlyRemote) {
508
+ csLines.push(
509
+ `${pc2.red("-")} ${pc2.bold(slug)} ${pc2.dim("(remote only \u2014 not in local config)")}`
510
+ );
511
+ }
512
+ }
513
+ if (csDiff.changed.length > 0) {
514
+ for (const item of csDiff.changed) {
515
+ csLines.push(`${pc2.yellow("~")} ${pc2.bold(item.slug)}`);
516
+ for (const line of item.details) {
517
+ csLines.push(` ${line}`);
518
+ }
519
+ }
520
+ }
521
+ p.note(csLines.join("\n"), "Credit Systems Diff");
522
+ }
523
+ if (hasPackDiff) {
524
+ const packLines = [];
525
+ const packDiff = diff.creditPacks;
526
+ if (packDiff.onlyLocal.length > 0) {
527
+ for (const slug of packDiff.onlyLocal) {
528
+ packLines.push(
529
+ `${pc2.green("+")} ${pc2.bold(slug)} ${pc2.dim("(local only \u2014 will be created on sync)")}`
530
+ );
531
+ }
532
+ }
533
+ if (packDiff.onlyRemote.length > 0) {
534
+ for (const slug of packDiff.onlyRemote) {
535
+ packLines.push(
536
+ `${pc2.red("-")} ${pc2.bold(slug)} ${pc2.dim("(remote only \u2014 not in local config)")}`
537
+ );
538
+ }
539
+ }
540
+ if (packDiff.changed.length > 0) {
541
+ for (const item of packDiff.changed) {
542
+ packLines.push(`${pc2.yellow("~")} ${pc2.bold(item.slug)}`);
543
+ for (const line of item.details) {
544
+ packLines.push(` ${line}`);
545
+ }
546
+ }
547
+ }
548
+ p.note(packLines.join("\n"), "Credit Packs Diff");
549
+ }
550
+ const planParts = [
551
+ diff.onlyLocal.length > 0 ? `${pc2.green(pc2.bold(diff.onlyLocal.length.toString()))} plans to add` : "",
552
+ diff.onlyRemote.length > 0 ? `${pc2.red(pc2.bold(diff.onlyRemote.length.toString()))} plans to remove` : "",
553
+ diff.changed.length > 0 ? `${pc2.yellow(pc2.bold(diff.changed.length.toString()))} plans modified` : ""
554
+ ].filter(Boolean);
555
+ const csParts = [
556
+ diff.creditSystems.onlyLocal.length > 0 ? `${pc2.green(pc2.bold(diff.creditSystems.onlyLocal.length.toString()))} systems to add` : "",
557
+ diff.creditSystems.onlyRemote.length > 0 ? `${pc2.red(pc2.bold(diff.creditSystems.onlyRemote.length.toString()))} systems to remove` : "",
558
+ diff.creditSystems.changed.length > 0 ? `${pc2.yellow(pc2.bold(diff.creditSystems.changed.length.toString()))} systems modified` : ""
559
+ ].filter(Boolean);
560
+ const packParts = [
561
+ diff.creditPacks.onlyLocal.length > 0 ? `${pc2.green(pc2.bold(diff.creditPacks.onlyLocal.length.toString()))} packs to add` : "",
562
+ diff.creditPacks.onlyRemote.length > 0 ? `${pc2.red(pc2.bold(diff.creditPacks.onlyRemote.length.toString()))} packs to remove` : "",
563
+ diff.creditPacks.changed.length > 0 ? `${pc2.yellow(pc2.bold(diff.creditPacks.changed.length.toString()))} packs modified` : ""
564
+ ].filter(Boolean);
565
+ const parts = [...planParts, ...csParts, ...packParts].join(pc2.dim(" \xB7 "));
566
+ p.log.info(parts);
567
+ }
568
+
135
569
  // src/commands/sync.ts
136
570
  async function runSyncSingle(options) {
137
- const { configPath, dryRun, apiUrl } = options;
571
+ const { configPath, dryRun, autoApprove, environment } = options;
138
572
  const apiKey = getApiKey(options.apiKey);
573
+ const apiUrl = `${options.apiUrl}/api/v1`;
139
574
  const fullPath = resolveConfigPath(configPath);
140
575
  if (!fullPath) {
141
- p.log.error(
142
- pc.red(
576
+ p2.log.error(
577
+ pc3.red(
143
578
  `Configuration file not found.${configPath ? ` looked at ${configPath}` : " searched defaults."}`
144
579
  )
145
580
  );
146
581
  process.exit(1);
147
582
  }
148
- const s = p.spinner();
149
- s.start(`Loading ${pc.cyan(fullPath)}`);
583
+ const s = p2.spinner();
584
+ s.start(`Loading ${pc3.cyan(fullPath)}`);
150
585
  let owo;
151
586
  try {
152
587
  owo = await loadOwostackFromConfig(fullPath);
153
588
  } catch (e) {
154
- s.stop(pc.red("Failed to load configuration"));
155
- p.log.error(pc.red(`Error: ${e.message}`));
156
- p.note(
589
+ s.stop(pc3.red("Failed to load configuration"));
590
+ p2.log.error(pc3.red(`Error: ${e.message}`));
591
+ p2.note(
157
592
  `import { Owostack, metered, boolean, plan } from "owostack";
158
593
  export default new Owostack({ secretKey: "...", catalog: [...] });`,
159
594
  "Example owo.config.ts"
160
595
  );
161
- p.log.info(
162
- pc.dim(
596
+ p2.log.info(
597
+ pc3.dim(
163
598
  "Make sure 'owostack' is installed in your project: 'npm install owostack'"
164
599
  )
165
600
  );
166
601
  process.exit(1);
167
602
  }
168
603
  if (!owo || typeof owo.sync !== "function") {
169
- s.stop(pc.red("Invalid configuration"));
170
- p.log.error("Config file must export an Owostack instance.");
604
+ s.stop(pc3.red("Invalid configuration"));
605
+ p2.log.error("Config file must export an Owostack instance.");
171
606
  process.exit(1);
172
607
  }
173
- s.message("Syncing with API...");
608
+ s.stop("Configuration loaded");
609
+ const { buildSyncPayload } = await import("owostack").catch(() => ({
610
+ buildSyncPayload: null
611
+ }));
612
+ const localPayload = buildSyncPayload?.(owo._config.catalog);
613
+ s.start(`Fetching remote catalog from ${pc3.dim(environment)}...`);
614
+ const remotePlans = await fetchPlans({ apiKey, apiUrl });
615
+ const remoteCreditSystems = await fetchCreditSystems(apiKey, apiUrl);
616
+ const remoteCreditPacks = await fetchCreditPacks(apiKey, apiUrl);
617
+ s.stop("Remote catalog fetched");
618
+ const diff = diffPlans(
619
+ localPayload?.plans ?? [],
620
+ remotePlans,
621
+ localPayload?.creditSystems ?? [],
622
+ remoteCreditSystems,
623
+ localPayload?.creditPacks ?? [],
624
+ remoteCreditPacks
625
+ );
626
+ printDiff(diff);
627
+ const hasChanges = diff.onlyLocal.length > 0 || diff.onlyRemote.length > 0 || diff.changed.length > 0 || diff.creditSystems.onlyLocal.length > 0 || diff.creditSystems.onlyRemote.length > 0 || diff.creditSystems.changed.length > 0 || diff.creditPacks.onlyLocal.length > 0 || diff.creditPacks.onlyRemote.length > 0 || diff.creditPacks.changed.length > 0;
628
+ if (!hasChanges) {
629
+ p2.outro(pc3.green("Everything is already in sync! \u2728"));
630
+ return;
631
+ }
632
+ if (dryRun) {
633
+ p2.log.info(pc3.yellow("Dry run - no changes were applied."));
634
+ return;
635
+ }
636
+ if (!autoApprove) {
637
+ const confirm4 = await p2.confirm({
638
+ message: `Proceed with sync to ${pc3.cyan(environment)}?`,
639
+ initialValue: false
640
+ });
641
+ if (p2.isCancel(confirm4) || !confirm4) {
642
+ p2.outro(pc3.yellow("Sync cancelled"));
643
+ process.exit(0);
644
+ }
645
+ }
646
+ s.start(`Syncing with ${pc3.cyan(environment)}...`);
174
647
  if (apiKey && typeof owo.setSecretKey === "function") {
175
648
  owo.setSecretKey(apiKey);
176
649
  }
@@ -178,189 +651,130 @@ export default new Owostack({ secretKey: "...", catalog: [...] });`,
178
651
  owo.setApiUrl(apiUrl);
179
652
  }
180
653
  try {
181
- if (dryRun) {
182
- s.stop(pc.yellow("Dry run mode (showing catalog payload)"));
183
- const { buildSyncPayload } = await import("owostack").catch(() => {
184
- return { buildSyncPayload: null };
185
- });
186
- if (buildSyncPayload && owo._config?.catalog) {
187
- const payload = buildSyncPayload(owo._config.catalog);
188
- let featureSummary = payload.features.map(
189
- (f) => `${pc.green("+")} ${pc.bold(f.slug)} ${pc.dim(`(${f.type})`)}`
190
- ).join("\n");
191
- p.note(
192
- featureSummary || pc.dim("No features defined"),
193
- "Features to Sync"
194
- );
195
- let planSummary = "";
196
- for (const p_obj of payload.plans) {
197
- planSummary += `${pc.green("+")} ${pc.bold(p_obj.slug)} ${pc.dim(`${p_obj.currency} ${p_obj.price}/${p_obj.interval}`)}
198
- `;
199
- for (const f of p_obj.features) {
200
- const status = f.enabled ? pc.green("\u2713") : pc.red("\u2717");
201
- const configParts = [];
202
- if (f.limit !== void 0)
203
- configParts.push(
204
- `limit: ${f.limit === null ? "unlimited" : f.limit}`
205
- );
206
- if (f.reset) configParts.push(`reset: ${f.reset}`);
207
- if (f.overage) configParts.push(`overage: ${f.overage}`);
208
- if (f.overagePrice) configParts.push(`price: ${f.overagePrice}`);
209
- const configStr = configParts.length > 0 ? ` ${pc.dim(`(${configParts.join(", ")})`)}` : "";
210
- planSummary += ` ${status} ${pc.dim(f.slug)}${configStr}
211
- `;
212
- }
213
- planSummary += "\n";
214
- }
215
- p.note(
216
- planSummary.trim() || pc.dim("No plans defined"),
217
- "Plans to Sync"
218
- );
219
- }
220
- p.log.info(pc.yellow("No changes were applied to the server."));
221
- return;
222
- }
223
654
  const result = await owo.sync();
224
- s.stop(pc.green("Sync completed"));
655
+ s.stop(pc3.green("Sync completed"));
225
656
  if (!result.success) {
226
- p.log.error(pc.red("Sync failed"));
657
+ p2.log.error(pc3.red("Sync failed"));
227
658
  process.exit(1);
228
659
  }
229
- const fc = result.features;
230
- const pc_res = result.plans;
231
- let summary = [];
232
- if (fc.created.length)
233
- summary.push(pc.green(`+ ${fc.created.length} features`));
234
- if (fc.updated.length)
235
- summary.push(pc.cyan(`~ ${fc.updated.length} features`));
236
- if (pc_res.created.length)
237
- summary.push(pc.green(`+ ${pc_res.created.length} plans`));
238
- if (pc_res.updated.length)
239
- summary.push(pc.cyan(`~ ${pc_res.updated.length} plans`));
240
- if (summary.length > 0) {
241
- p.note(summary.join("\n"), "Changes applied");
660
+ const lines = [];
661
+ if (diff.onlyLocal.length > 0) {
662
+ for (const slug of diff.onlyLocal) {
663
+ lines.push(`${pc3.green("+")} ${pc3.bold(slug)}`);
664
+ }
665
+ }
666
+ if (diff.onlyRemote.length > 0) {
667
+ for (const slug of diff.onlyRemote) {
668
+ lines.push(`${pc3.red("-")} ${pc3.bold(slug)}`);
669
+ }
670
+ }
671
+ if (diff.changed.length > 0) {
672
+ for (const item of diff.changed) {
673
+ lines.push(`${pc3.cyan("~")} ${pc3.bold(item.slug)}`);
674
+ for (const detail of item.details) {
675
+ lines.push(` ${detail}`);
676
+ }
677
+ }
678
+ }
679
+ if (diff.creditSystems.onlyLocal.length > 0) {
680
+ for (const slug of diff.creditSystems.onlyLocal) {
681
+ lines.push(`${pc3.green("+")} ${pc3.bold(slug)}`);
682
+ }
683
+ }
684
+ if (diff.creditSystems.onlyRemote.length > 0) {
685
+ for (const slug of diff.creditSystems.onlyRemote) {
686
+ lines.push(`${pc3.red("-")} ${pc3.bold(slug)}`);
687
+ }
688
+ }
689
+ if (diff.creditSystems.changed.length > 0) {
690
+ for (const item of diff.creditSystems.changed) {
691
+ lines.push(`${pc3.cyan("~")} ${pc3.bold(item.slug)}`);
692
+ for (const detail of item.details) {
693
+ lines.push(` ${detail}`);
694
+ }
695
+ }
696
+ }
697
+ if (diff.creditPacks.onlyLocal.length > 0) {
698
+ for (const slug of diff.creditPacks.onlyLocal) {
699
+ lines.push(`${pc3.green("+")} ${pc3.bold(slug)}`);
700
+ }
701
+ }
702
+ if (diff.creditPacks.onlyRemote.length > 0) {
703
+ for (const slug of diff.creditPacks.onlyRemote) {
704
+ lines.push(`${pc3.red("-")} ${pc3.bold(slug)}`);
705
+ }
706
+ }
707
+ if (diff.creditPacks.changed.length > 0) {
708
+ for (const item of diff.creditPacks.changed) {
709
+ lines.push(`${pc3.cyan("~")} ${pc3.bold(item.slug)}`);
710
+ for (const detail of item.details) {
711
+ lines.push(` ${detail}`);
712
+ }
713
+ }
714
+ }
715
+ if (lines.length > 0) {
716
+ p2.note(lines.join("\n"), "Changes applied");
717
+ const counts = [
718
+ diff.onlyLocal.length > 0 ? `${pc3.green(pc3.bold(diff.onlyLocal.length.toString()))} plans added` : "",
719
+ diff.onlyRemote.length > 0 ? `${pc3.red(pc3.bold(diff.onlyRemote.length.toString()))} plans removed` : "",
720
+ diff.changed.length > 0 ? `${pc3.cyan(pc3.bold(diff.changed.length.toString()))} plans modified` : "",
721
+ diff.creditSystems.onlyLocal.length > 0 ? `${pc3.green(pc3.bold(diff.creditSystems.onlyLocal.length.toString()))} systems added` : "",
722
+ diff.creditSystems.onlyRemote.length > 0 ? `${pc3.red(pc3.bold(diff.creditSystems.onlyRemote.length.toString()))} systems removed` : "",
723
+ diff.creditSystems.changed.length > 0 ? `${pc3.cyan(pc3.bold(diff.creditSystems.changed.length.toString()))} systems modified` : "",
724
+ diff.creditPacks.onlyLocal.length > 0 ? `${pc3.green(pc3.bold(diff.creditPacks.onlyLocal.length.toString()))} packs added` : "",
725
+ diff.creditPacks.onlyRemote.length > 0 ? `${pc3.red(pc3.bold(diff.creditPacks.onlyRemote.length.toString()))} packs removed` : "",
726
+ diff.creditPacks.changed.length > 0 ? `${pc3.cyan(pc3.bold(diff.creditPacks.changed.length.toString()))} packs modified` : ""
727
+ ].filter(Boolean).join(pc3.dim(" \xB7 "));
728
+ p2.log.info(counts);
242
729
  } else {
243
- p.log.info(pc.dim("No changes detected. Catalog is up to date."));
730
+ p2.log.success(pc3.dim("No changes detected. Catalog is up to date."));
244
731
  }
245
- if (result.warnings.length) {
246
- p.log.warn(pc.yellow(`Warnings:
732
+ if (result.warnings && result.warnings.length) {
733
+ p2.log.warn(pc3.yellow(`Warnings:
247
734
  ${result.warnings.join("\n")}`));
248
735
  }
249
736
  } catch (e) {
250
- s.stop(pc.red("Sync failed"));
251
- p.log.error(e.message);
737
+ s.stop(pc3.red("Sync failed"));
738
+ p2.log.error(e.message);
252
739
  throw e;
253
740
  }
254
741
  }
255
742
  async function runSync(options) {
256
- p.intro(pc.bgYellow(pc.black(" sync ")));
743
+ p2.intro(pc3.bgYellow(pc3.black(" sync ")));
257
744
  const configSettings = await loadConfigSettings(options.config);
258
- let apiUrl = configSettings.apiUrl;
259
745
  const testUrl = getTestApiUrl(configSettings.environments?.test);
260
746
  const liveUrl = getApiUrl(configSettings.environments?.live);
261
747
  if (options.prod) {
262
- p.log.step(pc.magenta("Production Mode: Syncing both environments"));
263
- await runSyncSingle({
264
- configPath: options.config,
265
- dryRun: !!options.dryRun,
266
- apiKey: options.key || "",
267
- apiUrl: apiUrl || `${testUrl}/api/v1`
268
- });
748
+ p2.log.step(pc3.magenta("Production Mode: Syncing to PROD environment"));
269
749
  await runSyncSingle({
270
750
  configPath: options.config,
271
751
  dryRun: !!options.dryRun,
752
+ autoApprove: !!options.yes,
272
753
  apiKey: options.key || "",
273
- apiUrl: apiUrl || `${liveUrl}/api/v1`
754
+ apiUrl: configSettings.apiUrl || liveUrl,
755
+ environment: "prod"
274
756
  });
275
757
  } else {
758
+ p2.log.step(pc3.cyan("Sandbox Mode: Syncing to SANDBOX environment"));
276
759
  await runSyncSingle({
277
760
  configPath: options.config,
278
761
  dryRun: !!options.dryRun,
762
+ autoApprove: !!options.yes,
279
763
  apiKey: options.key || "",
280
- apiUrl: apiUrl || `${liveUrl}/api/v1`
764
+ apiUrl: testUrl,
765
+ environment: "sandbox"
281
766
  });
282
767
  }
283
- p.outro(pc.green("Done! \u2728"));
768
+ p2.outro(pc3.green("Done! \u2728"));
284
769
  }
285
770
 
286
771
  // src/commands/pull.ts
287
- import * as p2 from "@clack/prompts";
288
- import pc3 from "picocolors";
772
+ import * as p3 from "@clack/prompts";
773
+ import pc4 from "picocolors";
289
774
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
290
775
  import { writeFile as writeFile2 } from "fs/promises";
291
776
  import { join as join2, resolve as resolve2, extname as extname2, isAbsolute as isAbsolute2 } from "path";
292
777
 
293
- // src/lib/api.ts
294
- import pc2 from "picocolors";
295
- async function fetchPlans(options) {
296
- if (!options.apiKey) {
297
- console.error(
298
- `
299
- \u274C Missing API key. Pass --key or set OWOSTACK_SECRET_KEY.
300
- `
301
- );
302
- process.exit(1);
303
- }
304
- const url = new URL(`${options.apiUrl}/plans`);
305
- if (options.group) url.searchParams.set("group", options.group);
306
- if (options.interval) url.searchParams.set("interval", options.interval);
307
- if (options.currency) url.searchParams.set("currency", options.currency);
308
- if (options.includeInactive) url.searchParams.set("includeInactive", "true");
309
- try {
310
- const response = await fetch(url.toString(), {
311
- method: "GET",
312
- headers: { Authorization: `Bearer ${options.apiKey}` }
313
- });
314
- const data = await response.json();
315
- if (!response.ok || !data?.success) {
316
- const message = data?.error || data?.message || "Request failed";
317
- console.error(`
318
- \u274C Failed to fetch plans: ${message}
319
- `);
320
- process.exit(1);
321
- }
322
- return data?.plans || [];
323
- } catch (error) {
324
- if (error.name === "TypeError" && error.message.includes("fetch failed")) {
325
- console.error(
326
- `
327
- \u274C Connection failed: Could not reach the API at ${pc2.cyan(options.apiUrl)}`
328
- );
329
- console.error(
330
- ` Please check your internet connection or ensure the API is running.`
331
- );
332
- console.error(
333
- ` You can override the API URL by setting the ${pc2.bold("OWOSTACK_API_URL")} environment variable.
334
- `
335
- );
336
- } else {
337
- console.error(`
338
- \u274C Unexpected error: ${error.message}
339
- `);
340
- }
341
- process.exit(1);
342
- }
343
- }
344
- async function fetchCreditSystems(apiKey, apiUrl) {
345
- if (!apiKey) {
346
- return [];
347
- }
348
- try {
349
- const url = `${apiUrl}/credit-systems`;
350
- const response = await fetch(url, {
351
- method: "GET",
352
- headers: { Authorization: `Bearer ${apiKey}` }
353
- });
354
- const data = await response.json();
355
- if (!response.ok || !data?.success) {
356
- return [];
357
- }
358
- return data?.creditSystems || [];
359
- } catch {
360
- return [];
361
- }
362
- }
363
-
364
778
  // src/lib/generate.ts
365
779
  function slugToIdentifier(slug, used) {
366
780
  const parts = slug.replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
@@ -414,7 +828,7 @@ function slugToIdentifier(slug, used) {
414
828
  used.add(candidate);
415
829
  return candidate;
416
830
  }
417
- function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts") {
831
+ function generateConfig(plans, creditSystems = [], creditPacks = [], defaultProvider, format = "ts") {
418
832
  const isTs = format === "ts";
419
833
  const isCjs = format === "cjs";
420
834
  const creditSystemSlugs = new Set(creditSystems.map((cs) => cs.slug));
@@ -500,6 +914,8 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
500
914
  configLines.push(`trialDays: ${plan.trialDays}`);
501
915
  if (plan.provider)
502
916
  configLines.push(`provider: ${JSON.stringify(plan.provider)}`);
917
+ if (plan.autoEnable) configLines.push(`autoEnable: true`);
918
+ if (plan.isAddon) configLines.push(`isAddon: true`);
503
919
  const featureEntries = [];
504
920
  for (const pf of plan.features || []) {
505
921
  if (creditSystemSlugs.has(pf.slug)) {
@@ -570,7 +986,30 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
570
986
  })`
571
987
  );
572
988
  }
989
+ const creditPackLines = [];
990
+ for (const pack of creditPacks) {
991
+ const configLines = [];
992
+ configLines.push(`name: ${JSON.stringify(pack.name)}`);
993
+ if (pack.description)
994
+ configLines.push(`description: ${JSON.stringify(pack.description)}`);
995
+ configLines.push(`credits: ${pack.credits}`);
996
+ configLines.push(`price: ${pack.price}`);
997
+ configLines.push(`currency: ${JSON.stringify(pack.currency)}`);
998
+ configLines.push(
999
+ `creditSystem: ${JSON.stringify(pack.creditSystemId || pack.creditSystem)}`
1000
+ );
1001
+ if (pack.provider)
1002
+ configLines.push(`provider: ${JSON.stringify(pack.provider)}`);
1003
+ if (pack.metadata)
1004
+ configLines.push(`metadata: ${JSON.stringify(pack.metadata)}`);
1005
+ creditPackLines.push(
1006
+ `creditPack(${JSON.stringify(pack.slug)}, {
1007
+ ${configLines.join(",\n ")}
1008
+ })`
1009
+ );
1010
+ }
573
1011
  const hasCreditSystems = creditSystemLines.length > 0;
1012
+ const hasCreditPacks = creditPackLines.length > 0;
574
1013
  const hasEntities = Array.from(featuresBySlug.values()).some(
575
1014
  (f) => f.meterType === "non_consumable"
576
1015
  );
@@ -578,7 +1017,7 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
578
1017
  ` : "";
579
1018
  const importParts = ["Owostack", "metered", "boolean"];
580
1019
  if (hasEntities) importParts.push("entity");
581
- importParts.push("creditSystem", "plan");
1020
+ importParts.push("creditSystem", "creditPack", "plan");
582
1021
  const imports = isCjs ? `const { ${importParts.join(", ")} } = require("owostack");` : `import { ${importParts.join(", ")} } from "owostack";`;
583
1022
  const tsCheck = !isTs ? `// @ts-check` : "";
584
1023
  const jsDoc = !isTs ? `/** @type {import('owostack').Owostack} */` : "";
@@ -590,13 +1029,15 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
590
1029
  ``,
591
1030
  ...featureLines,
592
1031
  ...hasCreditSystems ? ["", ...creditSystemLines] : [],
1032
+ ...hasCreditPacks ? ["", ...creditPackLines] : [],
593
1033
  ``,
594
1034
  jsDoc,
595
1035
  `${owoDecl} new Owostack({`,
596
1036
  ` secretKey: ${secretKey},`,
597
1037
  providerLine,
598
1038
  ` catalog: [`,
599
- ` ${planLines.join(",\n ")}`,
1039
+ ` ${planLines.join(",\n ")}${hasCreditPacks ? "," : ""}`,
1040
+ ...hasCreditPacks ? [` ${creditPackLines.join(",\n ")}`] : [],
600
1041
  ` ],`,
601
1042
  `});`,
602
1043
  ``
@@ -608,8 +1049,8 @@ function getProjectInfo() {
608
1049
  const cwd = process.cwd();
609
1050
  let isEsm = false;
610
1051
  try {
611
- const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
612
- isEsm = pkg.type === "module";
1052
+ const pkg2 = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
1053
+ isEsm = pkg2.type === "module";
613
1054
  } catch {
614
1055
  }
615
1056
  return { isEsm };
@@ -624,15 +1065,15 @@ function determineFormat(fullPath) {
624
1065
  return "ts";
625
1066
  }
626
1067
  async function runPull(options) {
627
- p2.intro(pc3.bgYellow(pc3.black(" pull ")));
1068
+ p3.intro(pc4.bgYellow(pc4.black(" pull ")));
628
1069
  let fullPath;
629
1070
  if (options.config) {
630
1071
  fullPath = isAbsolute2(options.config) ? options.config : resolve2(process.cwd(), options.config);
631
1072
  } else {
632
1073
  const resolved = resolveConfigPath();
633
1074
  if (!resolved) {
634
- p2.log.error(
635
- pc3.red("No configuration file found. Run 'owostack init' first.")
1075
+ p3.log.error(
1076
+ pc4.red("No configuration file found. Run 'owostack init' first.")
636
1077
  );
637
1078
  process.exit(1);
638
1079
  }
@@ -640,64 +1081,73 @@ async function runPull(options) {
640
1081
  }
641
1082
  const apiKey = getApiKey(options.key);
642
1083
  const configSettings = await loadConfigSettings(options.config);
643
- const baseUrl = getApiUrl(configSettings.apiUrl);
1084
+ const testUrl = getTestApiUrl(configSettings.environments?.test);
1085
+ const liveUrl = getApiUrl(configSettings.environments?.live);
644
1086
  const filters = configSettings.filters || {};
645
1087
  const format = determineFormat(fullPath);
646
- const s = p2.spinner();
1088
+ const s = p3.spinner();
647
1089
  if (options.prod) {
648
- p2.log.step(pc3.magenta("Production Mode: Fetching both environments"));
649
- const testUrl = getTestApiUrl(configSettings.environments?.test);
650
- const liveUrl = getApiUrl(configSettings.environments?.live);
651
- s.start(`Fetching from ${pc3.dim("test")}...`);
652
- const testPlans = await fetchPlans({
653
- apiKey,
654
- apiUrl: `${testUrl}/api/v1`,
655
- ...filters
656
- });
657
- s.stop(`Fetched ${testPlans.length} plans from test`);
658
- s.start(`Fetching from ${pc3.dim("live")}...`);
659
- const livePlans = await fetchPlans({
1090
+ p3.log.step(pc4.magenta("Production Mode: Pulling from PROD environment"));
1091
+ const apiUrl = `${liveUrl}/api/v1`;
1092
+ s.start(`Fetching plans from ${pc4.dim("prod")}...`);
1093
+ const plans = await fetchPlans({
660
1094
  apiKey,
661
- apiUrl: `${liveUrl}/api/v1`,
1095
+ apiUrl,
662
1096
  ...filters
663
1097
  });
664
- s.stop(`Fetched ${livePlans.length} plans from live`);
1098
+ s.stop(`Fetched ${plans.length} plans from prod`);
665
1099
  s.start(`Fetching credit systems...`);
666
- const creditSystems = await fetchCreditSystems(apiKey, `${liveUrl}/api/v1`);
1100
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
667
1101
  s.stop(`Fetched ${creditSystems.length} credit systems`);
1102
+ s.start(`Fetching credit packs...`);
1103
+ const creditPacks = await fetchCreditPacks(apiKey, apiUrl);
1104
+ s.stop(`Fetched ${creditPacks.length} credit packs`);
668
1105
  const providers = new Set(
669
- livePlans.map((p9) => p9.provider).filter(Boolean)
1106
+ plans.map((p9) => p9.provider).filter(Boolean)
670
1107
  );
671
1108
  const defaultProvider = providers.size === 1 ? Array.from(providers)[0] : void 0;
672
1109
  const configContent = generateConfig(
673
- livePlans,
1110
+ plans,
674
1111
  creditSystems,
1112
+ creditPacks,
675
1113
  defaultProvider,
676
1114
  format
677
1115
  );
678
1116
  if (options.dryRun) {
679
- p2.note(configContent, "Generated Config (Dry Run)");
680
- p2.outro(pc3.yellow("Dry run complete. No changes made."));
1117
+ p3.note(configContent, "Generated Config (Dry Run)");
1118
+ printPullSummary(plans, creditSystems, creditPacks);
1119
+ p3.outro(pc4.yellow("Dry run complete. No changes made."));
681
1120
  return;
682
1121
  }
683
1122
  if (existsSync3(fullPath) && !options.force) {
684
- p2.log.error(pc3.red(`Config file already exists at ${fullPath}`));
685
- p2.log.info(pc3.dim("Use --force to overwrite."));
686
- process.exit(1);
1123
+ const confirm4 = await p3.confirm({
1124
+ message: `Config file already exists at ${fullPath}. Overwrite?`,
1125
+ initialValue: false
1126
+ });
1127
+ if (p3.isCancel(confirm4) || !confirm4) {
1128
+ p3.outro(pc4.yellow("Operation cancelled"));
1129
+ process.exit(0);
1130
+ }
687
1131
  }
688
1132
  await writeFile2(fullPath, configContent, "utf8");
689
- p2.log.success(pc3.green(`Wrote configuration to ${fullPath}`));
1133
+ p3.log.success(pc4.green(`Wrote configuration to ${fullPath}`));
1134
+ printPullSummary(plans, creditSystems, creditPacks);
690
1135
  } else {
691
- s.start(`Fetching plans from ${pc3.dim(baseUrl)}...`);
1136
+ p3.log.step(pc4.cyan("Sandbox Mode: Pulling from SANDBOX environment"));
1137
+ const apiUrl = `${testUrl}/api/v1`;
1138
+ s.start(`Fetching plans from ${pc4.dim("sandbox")}...`);
692
1139
  const plans = await fetchPlans({
693
1140
  apiKey,
694
- apiUrl: `${baseUrl}/api/v1`,
1141
+ apiUrl,
695
1142
  ...filters
696
1143
  });
697
- s.stop(`Fetched ${plans.length} plans`);
1144
+ s.stop(`Fetched ${plans.length} plans from sandbox`);
698
1145
  s.start(`Fetching credit systems...`);
699
- const creditSystems = await fetchCreditSystems(apiKey, `${baseUrl}/api/v1`);
1146
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
700
1147
  s.stop(`Fetched ${creditSystems.length} credit systems`);
1148
+ s.start(`Fetching credit packs...`);
1149
+ const creditPacks = await fetchCreditPacks(apiKey, apiUrl);
1150
+ s.stop(`Fetched ${creditPacks.length} credit packs`);
701
1151
  const providers = new Set(
702
1152
  plans.map((p9) => p9.provider).filter(Boolean)
703
1153
  );
@@ -705,166 +1155,75 @@ async function runPull(options) {
705
1155
  const configContent = generateConfig(
706
1156
  plans,
707
1157
  creditSystems,
1158
+ creditPacks,
708
1159
  defaultProvider,
709
1160
  format
710
1161
  );
711
1162
  if (options.dryRun) {
712
- p2.note(configContent, "Generated Config (Dry Run)");
713
- p2.outro(pc3.yellow("Dry run complete. No changes made."));
1163
+ p3.note(configContent, "Generated Config (Dry Run)");
1164
+ printPullSummary(plans, creditSystems, creditPacks);
1165
+ p3.outro(pc4.yellow("Dry run complete. No changes made."));
714
1166
  return;
715
1167
  }
716
1168
  if (existsSync3(fullPath) && !options.force) {
717
- const confirm3 = await p2.confirm({
1169
+ const confirm4 = await p3.confirm({
718
1170
  message: `Config file already exists. Overwrite?`,
719
1171
  initialValue: false
720
1172
  });
721
- if (p2.isCancel(confirm3) || !confirm3) {
722
- p2.outro(pc3.yellow("Operation cancelled"));
1173
+ if (p3.isCancel(confirm4) || !confirm4) {
1174
+ p3.outro(pc4.yellow("Operation cancelled"));
723
1175
  process.exit(0);
724
1176
  }
725
1177
  }
726
1178
  await writeFile2(fullPath, configContent, "utf8");
727
- p2.log.success(pc3.green(`Wrote configuration to ${fullPath}`));
1179
+ p3.log.success(pc4.green(`Wrote configuration to ${fullPath}`));
1180
+ printPullSummary(plans, creditSystems, creditPacks);
728
1181
  }
729
- p2.outro(pc3.green("Pull complete! \u2728"));
730
- }
731
-
732
- // src/commands/diff.ts
733
- import * as p4 from "@clack/prompts";
734
- import pc5 from "picocolors";
735
-
736
- // src/lib/diff.ts
737
- import pc4 from "picocolors";
738
- import * as p3 from "@clack/prompts";
739
- function normalizeFeature(pf) {
740
- return {
741
- slug: pf.slug,
742
- enabled: pf.enabled,
743
- limit: pf.limit ?? null,
744
- // Handle both SDK 'reset' and API 'resetInterval'
745
- reset: pf.reset || pf.resetInterval || "monthly",
746
- // Handle both SDK 'overage' and API 'overage' (same name)
747
- overage: pf.overage || "block",
748
- overagePrice: pf.overagePrice ?? null
749
- };
750
- }
751
- function normalizePlan(plan) {
752
- return {
753
- slug: plan.slug,
754
- name: plan.name ?? null,
755
- description: plan.description ?? null,
756
- price: plan.price ?? 0,
757
- currency: plan.currency ?? null,
758
- interval: plan.interval ?? null,
759
- planGroup: plan.planGroup ?? null,
760
- trialDays: plan.trialDays ?? 0,
761
- features: (plan.features || []).map(normalizeFeature).sort((a, b) => a.slug.localeCompare(b.slug))
762
- };
1182
+ p3.outro(pc4.green("Pull complete! \u2728"));
763
1183
  }
764
- function diffPlans(localPlans, remotePlans) {
765
- const localMap = /* @__PURE__ */ new Map();
766
- const remoteMap = /* @__PURE__ */ new Map();
767
- for (const p9 of localPlans) localMap.set(p9.slug, normalizePlan(p9));
768
- for (const p9 of remotePlans) remoteMap.set(p9.slug, normalizePlan(p9));
769
- const onlyLocal = [];
770
- const onlyRemote = [];
771
- const changed = [];
772
- for (const slug of localMap.keys()) {
773
- if (!remoteMap.has(slug)) onlyLocal.push(slug);
774
- }
775
- for (const slug of remoteMap.keys()) {
776
- if (!localMap.has(slug)) onlyRemote.push(slug);
777
- }
778
- for (const slug of localMap.keys()) {
779
- if (!remoteMap.has(slug)) continue;
780
- const local = localMap.get(slug);
781
- const remote = remoteMap.get(slug);
782
- const details = [];
783
- const fields = [
784
- "name",
785
- "description",
786
- "price",
787
- "currency",
788
- "interval",
789
- "planGroup",
790
- "trialDays"
791
- ];
792
- for (const field of fields) {
793
- if (local[field] !== remote[field]) {
794
- const localVal = JSON.stringify(local[field]);
795
- const remoteVal = JSON.stringify(remote[field]);
796
- details.push(
797
- `${String(field)}: ${pc4.green(localVal)} \u2192 ${pc4.red(remoteVal)}`
798
- );
799
- }
800
- }
801
- const localFeatures = new Map(local.features.map((f) => [f.slug, f]));
802
- const remoteFeatures = new Map(
803
- remote.features.map((f) => [f.slug, f])
804
- );
805
- for (const fslug of localFeatures.keys()) {
806
- if (!remoteFeatures.has(fslug)) {
807
- details.push(`feature ${fslug}: ${pc4.green("[local only]")}`);
808
- continue;
809
- }
810
- const lf = localFeatures.get(fslug);
811
- const rf = remoteFeatures.get(fslug);
812
- if (JSON.stringify(lf) !== JSON.stringify(rf)) {
813
- details.push(`feature ${fslug}:`);
814
- details.push(` ${pc4.green(JSON.stringify(lf))}`);
815
- details.push(` ${pc4.red(JSON.stringify(rf))}`);
816
- }
817
- }
818
- for (const fslug of remoteFeatures.keys()) {
819
- if (!localFeatures.has(fslug)) {
820
- details.push(`feature ${fslug}: ${pc4.red("[remote only]")}`);
821
- }
822
- }
823
- if (details.length > 0) {
824
- changed.push({ slug, details });
1184
+ function printPullSummary(plans, creditSystems, creditPacks = []) {
1185
+ const featureSlugs = /* @__PURE__ */ new Set();
1186
+ for (const plan of plans) {
1187
+ for (const f of plan.features || []) {
1188
+ featureSlugs.add(f.slug);
825
1189
  }
826
1190
  }
827
- return { onlyLocal, onlyRemote, changed };
828
- }
829
- function printDiff(diff) {
830
- if (diff.onlyLocal.length === 0 && diff.onlyRemote.length === 0 && diff.changed.length === 0) {
831
- p3.log.success(pc4.green("No differences found."));
832
- return;
833
- }
834
- if (diff.onlyLocal.length > 0) {
835
- p3.note(
836
- diff.onlyLocal.map((slug) => `${pc4.green("+")} ${slug}`).join("\n"),
837
- "Only in Local"
1191
+ const lines = [];
1192
+ for (const plan of plans) {
1193
+ const featureCount = (plan.features || []).length;
1194
+ lines.push(
1195
+ `${pc4.green("\u2193")} ${pc4.bold(plan.slug)} ${pc4.dim(`${plan.currency} ${plan.price}/${plan.interval}`)} ${pc4.dim(`(${featureCount} features)`)} ${plan.isAddon ? pc4.cyan("(addon)") : ""}`
838
1196
  );
839
1197
  }
840
- if (diff.onlyRemote.length > 0) {
841
- p3.note(
842
- diff.onlyRemote.map((slug) => `${pc4.red("-")} ${slug}`).join("\n"),
843
- "Only in Remote"
844
- );
1198
+ if (creditSystems.length > 0) {
1199
+ lines.push("");
1200
+ for (const cs of creditSystems) {
1201
+ const childCount = (cs.features || []).length;
1202
+ lines.push(
1203
+ `${pc4.green("\u2193")} ${pc4.bold(cs.slug)} ${pc4.dim(`credit system (${childCount} features)`)}`
1204
+ );
1205
+ }
845
1206
  }
846
- if (diff.changed.length > 0) {
847
- let changedText = "";
848
- for (const item of diff.changed) {
849
- changedText += `
850
- ${pc4.bold(item.slug)}
851
- `;
852
- for (const line of item.details) {
853
- changedText += ` ${line}
854
- `;
855
- }
1207
+ if (creditPacks.length > 0) {
1208
+ lines.push("");
1209
+ for (const pack of creditPacks) {
1210
+ lines.push(
1211
+ `${pc4.green("\u2193")} ${pc4.bold(pack.slug)} ${pc4.dim(`${pack.currency} ${pack.price} for ${pack.credits} credits`)} ${pc4.cyan("(pack)")}`
1212
+ );
856
1213
  }
857
- p3.note(changedText.trim(), "Changed Plans");
858
1214
  }
859
- const summary = [
860
- diff.onlyLocal.length > 0 ? `${pc4.green(diff.onlyLocal.length.toString())} added` : "",
861
- diff.onlyRemote.length > 0 ? `${pc4.red(diff.onlyRemote.length.toString())} removed` : "",
862
- diff.changed.length > 0 ? `${pc4.yellow(diff.changed.length.toString())} changed` : ""
863
- ].filter(Boolean).join(" ");
864
- p3.log.info(summary);
1215
+ p3.note(lines.join("\n"), "Pulled");
1216
+ const counts = [
1217
+ `${pc4.bold(plans.length.toString())} plans`,
1218
+ `${pc4.bold(featureSlugs.size.toString())} features`,
1219
+ creditSystems.length > 0 ? `${pc4.bold(creditSystems.length.toString())} credit systems` : ""
1220
+ ].filter(Boolean).join(pc4.dim(" \xB7 "));
1221
+ p3.log.info(counts);
865
1222
  }
866
1223
 
867
1224
  // src/commands/diff.ts
1225
+ import * as p4 from "@clack/prompts";
1226
+ import pc5 from "picocolors";
868
1227
  async function runDiff(options) {
869
1228
  p4.intro(pc5.bgYellow(pc5.black(" diff ")));
870
1229
  const fullPath = resolveConfigPath(options.config);
@@ -874,12 +1233,12 @@ async function runDiff(options) {
874
1233
  }
875
1234
  const apiKey = getApiKey(options.key);
876
1235
  const configSettings = await loadConfigSettings(options.config);
877
- const baseUrl = getApiUrl(configSettings.apiUrl);
1236
+ const testUrl = getTestApiUrl(configSettings.environments?.test);
1237
+ const liveUrl = getApiUrl(configSettings.environments?.live);
878
1238
  const s = p4.spinner();
879
1239
  if (options.prod) {
880
- p4.log.step(pc5.magenta("Production Mode: Comparing both environments"));
881
- const testUrl = getTestApiUrl(configSettings.environments?.test);
882
- const liveUrl = getApiUrl(configSettings.environments?.live);
1240
+ p4.log.step(pc5.magenta("Production Mode: Comparing with PROD environment"));
1241
+ const apiUrl = `${liveUrl}/api/v1`;
883
1242
  s.start("Loading local configuration...");
884
1243
  let owo;
885
1244
  try {
@@ -894,18 +1253,34 @@ async function runDiff(options) {
894
1253
  );
895
1254
  process.exit(1);
896
1255
  }
1256
+ if (!owo || !owo._config) {
1257
+ s.stop(pc5.red("Invalid configuration"));
1258
+ p4.log.error("Config file must export an Owostack instance.");
1259
+ process.exit(1);
1260
+ }
897
1261
  s.stop("Configuration loaded");
898
1262
  const { buildSyncPayload } = await import("owostack").catch(() => ({
899
1263
  buildSyncPayload: null
900
1264
  }));
901
1265
  const localPayload = buildSyncPayload(owo._config.catalog);
902
- p4.log.step(pc5.cyan(`Comparing with TEST: ${testUrl}`));
903
- const testPlans = await fetchPlans({ apiKey, apiUrl: `${testUrl}/api/v1` });
904
- printDiff(diffPlans(localPayload?.plans ?? [], testPlans));
905
- p4.log.step(pc5.cyan(`Comparing with LIVE: ${liveUrl}`));
906
- const livePlans = await fetchPlans({ apiKey, apiUrl: `${liveUrl}/api/v1` });
907
- printDiff(diffPlans(localPayload?.plans ?? [], livePlans));
1266
+ s.start(`Fetching remote catalog from ${pc5.dim("prod")}...`);
1267
+ const livePlans = await fetchPlans({ apiKey, apiUrl });
1268
+ const liveCreditSystems = await fetchCreditSystems(apiKey, apiUrl);
1269
+ const liveCreditPacks = await fetchCreditPacks(apiKey, apiUrl);
1270
+ s.stop("Remote catalog fetched");
1271
+ printDiff(
1272
+ diffPlans(
1273
+ localPayload?.plans ?? [],
1274
+ livePlans,
1275
+ localPayload?.creditSystems ?? [],
1276
+ liveCreditSystems,
1277
+ localPayload?.creditPacks ?? [],
1278
+ liveCreditPacks
1279
+ )
1280
+ );
908
1281
  } else {
1282
+ p4.log.step(pc5.cyan("Sandbox Mode: Comparing with SANDBOX environment"));
1283
+ const apiUrl = `${testUrl}/api/v1`;
909
1284
  s.start("Loading local configuration...");
910
1285
  let owo;
911
1286
  try {
@@ -920,18 +1295,34 @@ async function runDiff(options) {
920
1295
  );
921
1296
  process.exit(1);
922
1297
  }
1298
+ if (!owo || !owo._config) {
1299
+ s.stop(pc5.red("Invalid configuration"));
1300
+ p4.log.error("Config file must export an Owostack instance.");
1301
+ process.exit(1);
1302
+ }
923
1303
  s.stop("Configuration loaded");
924
1304
  const { buildSyncPayload } = await import("owostack").catch(() => ({
925
1305
  buildSyncPayload: null
926
1306
  }));
927
1307
  const localPayload = buildSyncPayload(owo._config.catalog);
928
- s.start(`Fetching remote plans from ${pc5.dim(baseUrl)}...`);
1308
+ s.start(`Fetching remote catalog from ${pc5.dim("sandbox")}...`);
929
1309
  const remotePlans = await fetchPlans({
930
1310
  apiKey,
931
- apiUrl: `${baseUrl}/api/v1`
1311
+ apiUrl
932
1312
  });
933
- s.stop("Remote plans fetched");
934
- printDiff(diffPlans(localPayload?.plans ?? [], remotePlans));
1313
+ const remoteCreditSystems = await fetchCreditSystems(apiKey, apiUrl);
1314
+ const remoteCreditPacks = await fetchCreditPacks(apiKey, apiUrl);
1315
+ s.stop("Remote catalog fetched");
1316
+ printDiff(
1317
+ diffPlans(
1318
+ localPayload?.plans ?? [],
1319
+ remotePlans,
1320
+ localPayload?.creditSystems ?? [],
1321
+ remoteCreditSystems,
1322
+ localPayload?.creditPacks ?? [],
1323
+ remoteCreditPacks
1324
+ )
1325
+ );
935
1326
  }
936
1327
  p4.outro(pc5.green("Diff complete \u2728"));
937
1328
  }
@@ -1046,8 +1437,8 @@ function getProjectInfo2() {
1046
1437
  const isTs = existsSync4(join3(cwd, "tsconfig.json"));
1047
1438
  let isEsm = false;
1048
1439
  try {
1049
- const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
1050
- isEsm = pkg.type === "module";
1440
+ const pkg2 = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
1441
+ isEsm = pkg2.type === "module";
1051
1442
  } catch {
1052
1443
  }
1053
1444
  return { isTs, isEsm };
@@ -1082,11 +1473,11 @@ async function runInit(options) {
1082
1473
  }
1083
1474
  }
1084
1475
  if (existsSync4(fullPath) && !options.force) {
1085
- const confirm3 = await p6.confirm({
1476
+ const confirm4 = await p6.confirm({
1086
1477
  message: `Config file already exists at ${fullPath}. Overwrite?`,
1087
1478
  initialValue: false
1088
1479
  });
1089
- if (p6.isCancel(confirm3) || !confirm3) {
1480
+ if (p6.isCancel(confirm4) || !confirm4) {
1090
1481
  p6.outro(pc7.yellow("Initialization cancelled"));
1091
1482
  process.exit(0);
1092
1483
  }
@@ -1094,11 +1485,9 @@ async function runInit(options) {
1094
1485
  const s = p6.spinner();
1095
1486
  s.start("Generating project configuration...");
1096
1487
  try {
1097
- const plans = await fetchPlans({ apiKey, apiUrl: `${getApiUrl()}/api/v1` });
1098
- const creditSystems = await fetchCreditSystems(
1099
- apiKey,
1100
- `${getApiUrl()}/api/v1`
1101
- );
1488
+ const apiUrl = `${getApiUrl()}/api/v1`;
1489
+ const plans = await fetchPlans({ apiKey, apiUrl });
1490
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
1102
1491
  const ext = extname3(fullPath);
1103
1492
  const { isEsm } = getProjectInfo2();
1104
1493
  let format = "ts";
@@ -1189,39 +1578,51 @@ async function runValidate(options) {
1189
1578
  for (const f of payload.features) {
1190
1579
  p7.log.message(`${pc8.green("\u2713")} ${f.slug} ${pc8.dim(`(${f.type})`)}`);
1191
1580
  }
1581
+ if (payload.creditSystems && payload.creditSystems.length > 0) {
1582
+ p7.log.step(pc8.bold("Credit Systems"));
1583
+ for (const cs of payload.creditSystems) {
1584
+ p7.log.message(
1585
+ `${pc8.green("\u2713")} ${pc8.bold(cs.slug)} ${pc8.dim(`(${cs.features.length} features)`)}`
1586
+ );
1587
+ }
1588
+ }
1192
1589
  p7.log.step(pc8.bold("Plans"));
1193
1590
  for (const p_obj of payload.plans) {
1194
1591
  p7.log.message(
1195
- `${pc8.green("\u2713")} ${pc8.bold(p_obj.slug)} ${pc8.dim(`${p_obj.currency} ${p_obj.price} / ${p_obj.interval}`)}`
1592
+ `${pc8.green("\u2713")} ${p_obj.isAddon ? pc8.cyan("(addon)") : ""} ${pc8.bold(p_obj.slug)} ${pc8.dim(`${p_obj.currency} ${p_obj.price} / ${p_obj.interval}`)}`
1196
1593
  );
1197
1594
  }
1595
+ const configSettings = await loadConfigSettings(options.config);
1596
+ const testUrl = getTestApiUrl(configSettings.environments?.test);
1597
+ const liveUrl = getApiUrl(configSettings.environments?.live);
1598
+ const apiKey = getApiKey();
1198
1599
  if (options.prod) {
1199
- p7.log.step(pc8.magenta("Production Mode Check"));
1200
- const configSettings = await loadConfigSettings(options.config);
1201
- const testUrl = getTestApiUrl(configSettings.environments?.test);
1202
- const liveUrl = getApiUrl(configSettings.environments?.live);
1203
- const apiKey = getApiKey();
1600
+ p7.log.step(pc8.magenta("Production Mode: Checking PROD environment"));
1601
+ const apiUrl = `${liveUrl}/api/v1`;
1204
1602
  try {
1205
- const testPlans = await fetchPlans({
1603
+ const livePlans = await fetchPlans({
1206
1604
  apiKey,
1207
- apiUrl: `${testUrl}/api/v1`
1605
+ apiUrl
1208
1606
  });
1209
1607
  p7.log.success(
1210
- `TEST environment accessible (${testPlans.length} remote plans)`
1608
+ `PROD environment accessible (${livePlans.length} remote plans)`
1211
1609
  );
1212
1610
  } catch (e) {
1213
- p7.log.error(`TEST environment check failed: ${e.message}`);
1611
+ p7.log.error(`PROD environment check failed: ${e.message}`);
1214
1612
  }
1613
+ } else {
1614
+ p7.log.step(pc8.cyan("Sandbox Mode: Checking SANDBOX environment"));
1615
+ const apiUrl = `${testUrl}/api/v1`;
1215
1616
  try {
1216
- const livePlans = await fetchPlans({
1617
+ const testPlans = await fetchPlans({
1217
1618
  apiKey,
1218
- apiUrl: `${liveUrl}/api/v1`
1619
+ apiUrl
1219
1620
  });
1220
1621
  p7.log.success(
1221
- `LIVE environment accessible (${livePlans.length} remote plans)`
1622
+ `SANDBOX environment accessible (${testPlans.length} remote plans)`
1222
1623
  );
1223
1624
  } catch (e) {
1224
- p7.log.error(`LIVE environment check failed: ${e.message}`);
1625
+ p7.log.error(`SANDBOX environment check failed: ${e.message}`);
1225
1626
  }
1226
1627
  }
1227
1628
  p7.outro(pc8.green("Validation passed! \u2728"));
@@ -1272,25 +1673,26 @@ ${pc9.dim("Config:")} ${GLOBAL_CONFIG_PATH}`,
1272
1673
  // src/lib/brand.ts
1273
1674
  import pc10 from "picocolors";
1274
1675
  var OWO_ASCII = `
1275
- ${pc10.yellow("\u2588\u2588\u2588\u2588\u2588\u2588\u2557")} ${pc10.white("\u2588\u2588\u2557 \u2588\u2588\u2557")} ${pc10.yellow("\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
1276
- ${pc10.yellow("\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557")} ${pc10.white("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc10.yellow("\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557")}
1277
- ${pc10.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc10.white("\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551")} ${pc10.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")}
1278
- ${pc10.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc10.white("\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551")} ${pc10.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")}
1279
- ${pc10.yellow("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")} ${pc10.white("\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D")} ${pc10.yellow("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
1280
- ${pc10.yellow("\u255A\u2550\u2550\u2550\u2550\u2550\u255D")} ${pc10.white("\u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D")} ${pc10.yellow("\u255A\u2550\u2550\u2550\u2550\u2550\u255D")}
1676
+ ${pc10.dim("\u2591\u2591\u2588\u2588\u2588\u2591\u2591")} ${pc10.yellow("\u2591\u2591\u2591 \u2591\u2591\u2591")} ${pc10.dim("\u2591\u2591\u2588\u2588\u2588\u2591\u2591")}
1677
+ ${pc10.dim("\u2591\u2588 \u2588\u2591")} ${pc10.yellow("\u2591\u2588\u2591 \u2591\u2588\u2591")} ${pc10.dim("\u2591\u2588 \u2588\u2591")}
1678
+ ${pc10.dim("\u2591\u2588 \u2588\u2591")} ${pc10.yellow("\u2591\u2588\u2591 \u2588\u2591 \u2591\u2588\u2591")} ${pc10.dim("\u2591\u2588 \u2588\u2591")}
1679
+ ${pc10.dim("\u2591\u2591\u2588\u2588\u2588\u2591\u2591")} ${pc10.yellow("\u2591\u2591\u2588\u2591\u2588\u2591\u2588\u2591\u2591")} ${pc10.dim("\u2591\u2591\u2588\u2588\u2588\u2591\u2591")}
1281
1680
  `;
1282
1681
  function printBrand() {
1283
1682
  console.log(OWO_ASCII);
1284
1683
  }
1285
1684
 
1286
1685
  // src/index.ts
1686
+ import { createRequire } from "module";
1687
+ var require2 = createRequire(import.meta.url);
1688
+ var pkg = require2("../package.json");
1287
1689
  var program = new Command();
1288
1690
  printBrand();
1289
- program.name("owostack").description("CLI for Owostack billing infrastructure").version("0.1.0");
1290
- program.command("sync").description("Push catalog to the API").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--prod", "Execute in both test and live environments").option("--dry-run", "Show what would change without applying").action(runSync);
1291
- program.command("pull").description("Pull plans from dashboard into owo.config.ts").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--force", "Overwrite existing config file", false).option("--prod", "Execute in both test and live environments").option("--dry-run", "Show what would change without applying").action(runPull);
1292
- program.command("diff").description("Compare local config to dashboard plans").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--prod", "Execute in both test and live environments").action(runDiff);
1691
+ program.name("owostack").description("CLI for Owostack billing infrastructure").version(pkg.version).option("--prod", "Execute in production environment (default: sandbox)");
1692
+ program.command("sync").description("Push catalog to the API").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--dry-run", "Show what would change without applying").option("--yes", "Auto-approve changes without interactive prompt").action((options) => runSync({ ...options, ...program.opts() }));
1693
+ program.command("pull").description("Pull plans from dashboard into owo.config.ts").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--force", "Overwrite existing config file", false).option("--dry-run", "Show what would change without applying").action((options) => runPull({ ...options, ...program.opts() }));
1694
+ program.command("diff").description("Compare local config to dashboard plans").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").action((options) => runDiff({ ...options, ...program.opts() }));
1293
1695
  program.command("init").description("Initialize owo.config.ts from dashboard").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--force", "Overwrite existing config file", false).action(runInit);
1294
- program.command("validate").description("Validate local config without syncing").option("--config <path>", "Path to config file").option("--prod", "Execute in both test and live environments").action(runValidate);
1696
+ program.command("validate").description("Validate local config without syncing").option("--config <path>", "Path to config file").action((options) => runValidate({ ...options, ...program.opts() }));
1295
1697
  program.command("connect").description("Connect CLI to dashboard via browser").option("--no-browser", "Don't open the browser automatically").action(runConnect);
1296
1698
  program.parse();