owosk 0.1.4 → 0.2.1

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 +826 -404
  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
  }
@@ -95,7 +95,7 @@ async function loadOwostackFromConfig(fullPath) {
95
95
  console.error(` Example owo.config.ts:
96
96
  `);
97
97
  console.error(
98
- ` import { Owostack, metered, boolean, plan } from "owostack";`
98
+ ` import { Owostack, metered, boolean, entity, creditSystem, creditPack, plan } from "owostack";`
99
99
  );
100
100
  console.error(
101
101
  ` export default new Owostack({ secretKey: "...", catalog: [...] });
@@ -105,7 +105,7 @@ async function loadOwostackFromConfig(fullPath) {
105
105
  console.error(` Example owo.config.js:
106
106
  `);
107
107
  console.error(
108
- ` const { Owostack, metered, boolean, plan } = require("owostack");`
108
+ ` const { Owostack, metered, boolean, entity, creditSystem, creditPack, plan } = require("owostack");`
109
109
  );
110
110
  console.error(
111
111
  ` module.exports = new Owostack({ secretKey: "...", catalog: [...] });
@@ -132,235 +132,649 @@ 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...");
174
- if (apiKey && typeof owo.setSecretKey === "function") {
175
- owo.setSecretKey(apiKey);
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;
176
631
  }
177
- if (apiUrl && typeof owo.setApiUrl === "function") {
178
- owo.setApiUrl(apiUrl);
632
+ if (dryRun) {
633
+ p2.log.info(pc3.yellow("Dry run - no changes were applied."));
634
+ return;
179
635
  }
180
- 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;
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);
222
644
  }
645
+ }
646
+ s.start(`Syncing with ${pc3.cyan(environment)}...`);
647
+ if (apiKey && typeof owo.setSecretKey === "function") {
648
+ owo.setSecretKey(apiKey);
649
+ }
650
+ if (apiUrl && typeof owo.setApiUrl === "function") {
651
+ owo.setApiUrl(apiUrl);
652
+ }
653
+ try {
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,11 +828,10 @@ 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));
421
- const creditSystemBySlug = new Map(creditSystems.map((cs) => [cs.slug, cs]));
422
835
  const featuresBySlug = /* @__PURE__ */ new Map();
423
836
  for (const plan of plans) {
424
837
  for (const f of plan.features || []) {
@@ -444,7 +857,16 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
444
857
  }
445
858
  }
446
859
  }
447
- const usedNames = /* @__PURE__ */ new Set();
860
+ const usedNames = /* @__PURE__ */ new Set([
861
+ "Owostack",
862
+ "metered",
863
+ "boolean",
864
+ "entity",
865
+ "creditSystem",
866
+ "creditPack",
867
+ "plan",
868
+ "owo"
869
+ ]);
448
870
  const featureVars = /* @__PURE__ */ new Map();
449
871
  const featureLines = [];
450
872
  for (const feature of featuresBySlug.values()) {
@@ -456,7 +878,6 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
456
878
  const decl = `${builder}(${JSON.stringify(feature.slug)}${nameArg})`;
457
879
  if (isCjs) {
458
880
  featureLines.push(`const ${varName} = ${decl};`);
459
- featureLines.push(`exports.${varName} = ${varName};`);
460
881
  } else {
461
882
  featureLines.push(`export const ${varName} = ${decl};`);
462
883
  }
@@ -466,21 +887,20 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
466
887
  for (const cs of creditSystems) {
467
888
  const varName = slugToIdentifier(cs.slug, usedNames);
468
889
  creditSystemVars.set(cs.slug, varName);
469
- const nameArg = cs.name ? `name: ${JSON.stringify(cs.name)}` : "";
470
- const descArg = cs.description ? `description: ${JSON.stringify(cs.description)}` : "";
890
+ const configLines = [];
891
+ if (cs.name) configLines.push(`name: ${JSON.stringify(cs.name)}`);
892
+ if (cs.description)
893
+ configLines.push(`description: ${JSON.stringify(cs.description)}`);
471
894
  const featureEntries = (cs.features || []).map((f) => {
472
895
  const childVar = featureVars.get(f.feature) || f.feature;
473
896
  return `${childVar}(${f.creditCost})`;
474
897
  });
475
- const optsParts = [
476
- nameArg,
477
- descArg,
478
- `features: [${featureEntries.join(", ")}]`
479
- ].filter(Boolean);
480
- const decl = `creditSystem(${JSON.stringify(cs.slug)}, { ${optsParts.join(", ")} })`;
898
+ configLines.push(`features: [${featureEntries.join(", ")}]`);
899
+ const decl = `creditSystem(${JSON.stringify(cs.slug)}, {
900
+ ${configLines.join(",\n ")}
901
+ })`;
481
902
  if (isCjs) {
482
903
  creditSystemLines.push(`const ${varName} = ${decl};`);
483
- creditSystemLines.push(`exports.${varName} = ${varName};`);
484
904
  } else {
485
905
  creditSystemLines.push(`export const ${varName} = ${decl};`);
486
906
  }
@@ -500,6 +920,8 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
500
920
  configLines.push(`trialDays: ${plan.trialDays}`);
501
921
  if (plan.provider)
502
922
  configLines.push(`provider: ${JSON.stringify(plan.provider)}`);
923
+ if (plan.autoEnable) configLines.push(`autoEnable: true`);
924
+ if (plan.isAddon) configLines.push(`isAddon: true`);
503
925
  const featureEntries = [];
504
926
  for (const pf of plan.features || []) {
505
927
  if (creditSystemSlugs.has(pf.slug)) {
@@ -570,7 +992,30 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
570
992
  })`
571
993
  );
572
994
  }
995
+ const creditPackLines = [];
996
+ for (const pack of creditPacks) {
997
+ const configLines = [];
998
+ configLines.push(`name: ${JSON.stringify(pack.name)}`);
999
+ if (pack.description)
1000
+ configLines.push(`description: ${JSON.stringify(pack.description)}`);
1001
+ configLines.push(`credits: ${pack.credits}`);
1002
+ configLines.push(`price: ${pack.price}`);
1003
+ configLines.push(`currency: ${JSON.stringify(pack.currency)}`);
1004
+ configLines.push(
1005
+ `creditSystem: ${JSON.stringify(pack.creditSystemId || pack.creditSystem)}`
1006
+ );
1007
+ if (pack.provider)
1008
+ configLines.push(`provider: ${JSON.stringify(pack.provider)}`);
1009
+ if (pack.metadata)
1010
+ configLines.push(`metadata: ${JSON.stringify(pack.metadata)}`);
1011
+ creditPackLines.push(
1012
+ `creditPack(${JSON.stringify(pack.slug)}, {
1013
+ ${configLines.join(",\n ")}
1014
+ })`
1015
+ );
1016
+ }
573
1017
  const hasCreditSystems = creditSystemLines.length > 0;
1018
+ const hasCreditPacks = creditPackLines.length > 0;
574
1019
  const hasEntities = Array.from(featuresBySlug.values()).some(
575
1020
  (f) => f.meterType === "non_consumable"
576
1021
  );
@@ -578,12 +1023,26 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
578
1023
  ` : "";
579
1024
  const importParts = ["Owostack", "metered", "boolean"];
580
1025
  if (hasEntities) importParts.push("entity");
581
- importParts.push("creditSystem", "plan");
1026
+ importParts.push("creditSystem", "creditPack", "plan");
582
1027
  const imports = isCjs ? `const { ${importParts.join(", ")} } = require("owostack");` : `import { ${importParts.join(", ")} } from "owostack";`;
583
1028
  const tsCheck = !isTs ? `// @ts-check` : "";
584
1029
  const jsDoc = !isTs ? `/** @type {import('owostack').Owostack} */` : "";
585
- const owoDecl = isCjs ? "exports.owo =" : "export const owo =";
1030
+ const owoDecl = isCjs ? "const owo =" : "export const owo =";
586
1031
  const secretKey = isTs ? "process.env.OWOSTACK_SECRET_KEY!" : "process.env.OWOSTACK_SECRET_KEY";
1032
+ const catalogEntries = [...planLines, ...creditPackLines];
1033
+ const footer = isCjs ? `module.exports = { owo, ${Array.from(usedNames).filter(
1034
+ (n) => ![
1035
+ "Owostack",
1036
+ "metered",
1037
+ "boolean",
1038
+ "entity",
1039
+ "creditSystem",
1040
+ "creditPack",
1041
+ "plan",
1042
+ "owo"
1043
+ ].includes(n)
1044
+ ).join(", ")} };`.replace(", };", " };") : "";
1045
+ const finalFooter = isCjs && footer.includes("{ owo, }") ? "module.exports = { owo };" : footer;
587
1046
  return [
588
1047
  tsCheck,
589
1048
  imports,
@@ -596,10 +1055,11 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
596
1055
  ` secretKey: ${secretKey},`,
597
1056
  providerLine,
598
1057
  ` catalog: [`,
599
- ` ${planLines.join(",\n ")}`,
1058
+ ` ${catalogEntries.join(",\n ")}`,
600
1059
  ` ],`,
601
1060
  `});`,
602
- ``
1061
+ ``,
1062
+ finalFooter
603
1063
  ].filter(Boolean).join("\n");
604
1064
  }
605
1065
 
@@ -608,8 +1068,8 @@ function getProjectInfo() {
608
1068
  const cwd = process.cwd();
609
1069
  let isEsm = false;
610
1070
  try {
611
- const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
612
- isEsm = pkg.type === "module";
1071
+ const pkg2 = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
1072
+ isEsm = pkg2.type === "module";
613
1073
  } catch {
614
1074
  }
615
1075
  return { isEsm };
@@ -624,15 +1084,15 @@ function determineFormat(fullPath) {
624
1084
  return "ts";
625
1085
  }
626
1086
  async function runPull(options) {
627
- p2.intro(pc3.bgYellow(pc3.black(" pull ")));
1087
+ p3.intro(pc4.bgYellow(pc4.black(" pull ")));
628
1088
  let fullPath;
629
1089
  if (options.config) {
630
1090
  fullPath = isAbsolute2(options.config) ? options.config : resolve2(process.cwd(), options.config);
631
1091
  } else {
632
1092
  const resolved = resolveConfigPath();
633
1093
  if (!resolved) {
634
- p2.log.error(
635
- pc3.red("No configuration file found. Run 'owostack init' first.")
1094
+ p3.log.error(
1095
+ pc4.red("No configuration file found. Run 'owostack init' first.")
636
1096
  );
637
1097
  process.exit(1);
638
1098
  }
@@ -640,64 +1100,73 @@ async function runPull(options) {
640
1100
  }
641
1101
  const apiKey = getApiKey(options.key);
642
1102
  const configSettings = await loadConfigSettings(options.config);
643
- const baseUrl = getApiUrl(configSettings.apiUrl);
1103
+ const testUrl = getTestApiUrl(configSettings.environments?.test);
1104
+ const liveUrl = getApiUrl(configSettings.environments?.live);
644
1105
  const filters = configSettings.filters || {};
645
1106
  const format = determineFormat(fullPath);
646
- const s = p2.spinner();
1107
+ const s = p3.spinner();
647
1108
  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({
1109
+ p3.log.step(pc4.magenta("Production Mode: Pulling from PROD environment"));
1110
+ const apiUrl = `${liveUrl}/api/v1`;
1111
+ s.start(`Fetching plans from ${pc4.dim("prod")}...`);
1112
+ const plans = await fetchPlans({
660
1113
  apiKey,
661
- apiUrl: `${liveUrl}/api/v1`,
1114
+ apiUrl,
662
1115
  ...filters
663
1116
  });
664
- s.stop(`Fetched ${livePlans.length} plans from live`);
1117
+ s.stop(`Fetched ${plans.length} plans from prod`);
665
1118
  s.start(`Fetching credit systems...`);
666
- const creditSystems = await fetchCreditSystems(apiKey, `${liveUrl}/api/v1`);
1119
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
667
1120
  s.stop(`Fetched ${creditSystems.length} credit systems`);
1121
+ s.start(`Fetching credit packs...`);
1122
+ const creditPacks = await fetchCreditPacks(apiKey, apiUrl);
1123
+ s.stop(`Fetched ${creditPacks.length} credit packs`);
668
1124
  const providers = new Set(
669
- livePlans.map((p9) => p9.provider).filter(Boolean)
1125
+ plans.map((p9) => p9.provider).filter(Boolean)
670
1126
  );
671
1127
  const defaultProvider = providers.size === 1 ? Array.from(providers)[0] : void 0;
672
1128
  const configContent = generateConfig(
673
- livePlans,
1129
+ plans,
674
1130
  creditSystems,
1131
+ creditPacks,
675
1132
  defaultProvider,
676
1133
  format
677
1134
  );
678
1135
  if (options.dryRun) {
679
- p2.note(configContent, "Generated Config (Dry Run)");
680
- p2.outro(pc3.yellow("Dry run complete. No changes made."));
1136
+ p3.note(configContent, "Generated Config (Dry Run)");
1137
+ printPullSummary(plans, creditSystems, creditPacks);
1138
+ p3.outro(pc4.yellow("Dry run complete. No changes made."));
681
1139
  return;
682
1140
  }
683
1141
  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);
1142
+ const confirm4 = await p3.confirm({
1143
+ message: `Config file already exists at ${fullPath}. Overwrite?`,
1144
+ initialValue: false
1145
+ });
1146
+ if (p3.isCancel(confirm4) || !confirm4) {
1147
+ p3.outro(pc4.yellow("Operation cancelled"));
1148
+ process.exit(0);
1149
+ }
687
1150
  }
688
1151
  await writeFile2(fullPath, configContent, "utf8");
689
- p2.log.success(pc3.green(`Wrote configuration to ${fullPath}`));
1152
+ p3.log.success(pc4.green(`Wrote configuration to ${fullPath}`));
1153
+ printPullSummary(plans, creditSystems, creditPacks);
690
1154
  } else {
691
- s.start(`Fetching plans from ${pc3.dim(baseUrl)}...`);
1155
+ p3.log.step(pc4.cyan("Sandbox Mode: Pulling from SANDBOX environment"));
1156
+ const apiUrl = `${testUrl}/api/v1`;
1157
+ s.start(`Fetching plans from ${pc4.dim("sandbox")}...`);
692
1158
  const plans = await fetchPlans({
693
1159
  apiKey,
694
- apiUrl: `${baseUrl}/api/v1`,
1160
+ apiUrl,
695
1161
  ...filters
696
1162
  });
697
- s.stop(`Fetched ${plans.length} plans`);
1163
+ s.stop(`Fetched ${plans.length} plans from sandbox`);
698
1164
  s.start(`Fetching credit systems...`);
699
- const creditSystems = await fetchCreditSystems(apiKey, `${baseUrl}/api/v1`);
1165
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
700
1166
  s.stop(`Fetched ${creditSystems.length} credit systems`);
1167
+ s.start(`Fetching credit packs...`);
1168
+ const creditPacks = await fetchCreditPacks(apiKey, apiUrl);
1169
+ s.stop(`Fetched ${creditPacks.length} credit packs`);
701
1170
  const providers = new Set(
702
1171
  plans.map((p9) => p9.provider).filter(Boolean)
703
1172
  );
@@ -705,166 +1174,75 @@ async function runPull(options) {
705
1174
  const configContent = generateConfig(
706
1175
  plans,
707
1176
  creditSystems,
1177
+ creditPacks,
708
1178
  defaultProvider,
709
1179
  format
710
1180
  );
711
1181
  if (options.dryRun) {
712
- p2.note(configContent, "Generated Config (Dry Run)");
713
- p2.outro(pc3.yellow("Dry run complete. No changes made."));
1182
+ p3.note(configContent, "Generated Config (Dry Run)");
1183
+ printPullSummary(plans, creditSystems, creditPacks);
1184
+ p3.outro(pc4.yellow("Dry run complete. No changes made."));
714
1185
  return;
715
1186
  }
716
1187
  if (existsSync3(fullPath) && !options.force) {
717
- const confirm3 = await p2.confirm({
1188
+ const confirm4 = await p3.confirm({
718
1189
  message: `Config file already exists. Overwrite?`,
719
1190
  initialValue: false
720
1191
  });
721
- if (p2.isCancel(confirm3) || !confirm3) {
722
- p2.outro(pc3.yellow("Operation cancelled"));
1192
+ if (p3.isCancel(confirm4) || !confirm4) {
1193
+ p3.outro(pc4.yellow("Operation cancelled"));
723
1194
  process.exit(0);
724
1195
  }
725
1196
  }
726
1197
  await writeFile2(fullPath, configContent, "utf8");
727
- p2.log.success(pc3.green(`Wrote configuration to ${fullPath}`));
1198
+ p3.log.success(pc4.green(`Wrote configuration to ${fullPath}`));
1199
+ printPullSummary(plans, creditSystems, creditPacks);
728
1200
  }
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
- };
1201
+ p3.outro(pc4.green("Pull complete! \u2728"));
763
1202
  }
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 });
1203
+ function printPullSummary(plans, creditSystems, creditPacks = []) {
1204
+ const featureSlugs = /* @__PURE__ */ new Set();
1205
+ for (const plan of plans) {
1206
+ for (const f of plan.features || []) {
1207
+ featureSlugs.add(f.slug);
825
1208
  }
826
1209
  }
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"
1210
+ const lines = [];
1211
+ for (const plan of plans) {
1212
+ const featureCount = (plan.features || []).length;
1213
+ lines.push(
1214
+ `${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
1215
  );
839
1216
  }
840
- if (diff.onlyRemote.length > 0) {
841
- p3.note(
842
- diff.onlyRemote.map((slug) => `${pc4.red("-")} ${slug}`).join("\n"),
843
- "Only in Remote"
844
- );
1217
+ if (creditSystems.length > 0) {
1218
+ lines.push("");
1219
+ for (const cs of creditSystems) {
1220
+ const childCount = (cs.features || []).length;
1221
+ lines.push(
1222
+ `${pc4.green("\u2193")} ${pc4.bold(cs.slug)} ${pc4.dim(`credit system (${childCount} features)`)}`
1223
+ );
1224
+ }
845
1225
  }
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
- }
1226
+ if (creditPacks.length > 0) {
1227
+ lines.push("");
1228
+ for (const pack of creditPacks) {
1229
+ lines.push(
1230
+ `${pc4.green("\u2193")} ${pc4.bold(pack.slug)} ${pc4.dim(`${pack.currency} ${pack.price} for ${pack.credits} credits`)} ${pc4.cyan("(pack)")}`
1231
+ );
856
1232
  }
857
- p3.note(changedText.trim(), "Changed Plans");
858
1233
  }
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);
1234
+ p3.note(lines.join("\n"), "Pulled");
1235
+ const counts = [
1236
+ `${pc4.bold(plans.length.toString())} plans`,
1237
+ `${pc4.bold(featureSlugs.size.toString())} features`,
1238
+ creditSystems.length > 0 ? `${pc4.bold(creditSystems.length.toString())} credit systems` : ""
1239
+ ].filter(Boolean).join(pc4.dim(" \xB7 "));
1240
+ p3.log.info(counts);
865
1241
  }
866
1242
 
867
1243
  // src/commands/diff.ts
1244
+ import * as p4 from "@clack/prompts";
1245
+ import pc5 from "picocolors";
868
1246
  async function runDiff(options) {
869
1247
  p4.intro(pc5.bgYellow(pc5.black(" diff ")));
870
1248
  const fullPath = resolveConfigPath(options.config);
@@ -874,12 +1252,12 @@ async function runDiff(options) {
874
1252
  }
875
1253
  const apiKey = getApiKey(options.key);
876
1254
  const configSettings = await loadConfigSettings(options.config);
877
- const baseUrl = getApiUrl(configSettings.apiUrl);
1255
+ const testUrl = getTestApiUrl(configSettings.environments?.test);
1256
+ const liveUrl = getApiUrl(configSettings.environments?.live);
878
1257
  const s = p4.spinner();
879
1258
  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);
1259
+ p4.log.step(pc5.magenta("Production Mode: Comparing with PROD environment"));
1260
+ const apiUrl = `${liveUrl}/api/v1`;
883
1261
  s.start("Loading local configuration...");
884
1262
  let owo;
885
1263
  try {
@@ -894,18 +1272,34 @@ async function runDiff(options) {
894
1272
  );
895
1273
  process.exit(1);
896
1274
  }
1275
+ if (!owo || !owo._config) {
1276
+ s.stop(pc5.red("Invalid configuration"));
1277
+ p4.log.error("Config file must export an Owostack instance.");
1278
+ process.exit(1);
1279
+ }
897
1280
  s.stop("Configuration loaded");
898
1281
  const { buildSyncPayload } = await import("owostack").catch(() => ({
899
1282
  buildSyncPayload: null
900
1283
  }));
901
1284
  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));
1285
+ s.start(`Fetching remote catalog from ${pc5.dim("prod")}...`);
1286
+ const livePlans = await fetchPlans({ apiKey, apiUrl });
1287
+ const liveCreditSystems = await fetchCreditSystems(apiKey, apiUrl);
1288
+ const liveCreditPacks = await fetchCreditPacks(apiKey, apiUrl);
1289
+ s.stop("Remote catalog fetched");
1290
+ printDiff(
1291
+ diffPlans(
1292
+ localPayload?.plans ?? [],
1293
+ livePlans,
1294
+ localPayload?.creditSystems ?? [],
1295
+ liveCreditSystems,
1296
+ localPayload?.creditPacks ?? [],
1297
+ liveCreditPacks
1298
+ )
1299
+ );
908
1300
  } else {
1301
+ p4.log.step(pc5.cyan("Sandbox Mode: Comparing with SANDBOX environment"));
1302
+ const apiUrl = `${testUrl}/api/v1`;
909
1303
  s.start("Loading local configuration...");
910
1304
  let owo;
911
1305
  try {
@@ -920,18 +1314,34 @@ async function runDiff(options) {
920
1314
  );
921
1315
  process.exit(1);
922
1316
  }
1317
+ if (!owo || !owo._config) {
1318
+ s.stop(pc5.red("Invalid configuration"));
1319
+ p4.log.error("Config file must export an Owostack instance.");
1320
+ process.exit(1);
1321
+ }
923
1322
  s.stop("Configuration loaded");
924
1323
  const { buildSyncPayload } = await import("owostack").catch(() => ({
925
1324
  buildSyncPayload: null
926
1325
  }));
927
1326
  const localPayload = buildSyncPayload(owo._config.catalog);
928
- s.start(`Fetching remote plans from ${pc5.dim(baseUrl)}...`);
1327
+ s.start(`Fetching remote catalog from ${pc5.dim("sandbox")}...`);
929
1328
  const remotePlans = await fetchPlans({
930
1329
  apiKey,
931
- apiUrl: `${baseUrl}/api/v1`
1330
+ apiUrl
932
1331
  });
933
- s.stop("Remote plans fetched");
934
- printDiff(diffPlans(localPayload?.plans ?? [], remotePlans));
1332
+ const remoteCreditSystems = await fetchCreditSystems(apiKey, apiUrl);
1333
+ const remoteCreditPacks = await fetchCreditPacks(apiKey, apiUrl);
1334
+ s.stop("Remote catalog fetched");
1335
+ printDiff(
1336
+ diffPlans(
1337
+ localPayload?.plans ?? [],
1338
+ remotePlans,
1339
+ localPayload?.creditSystems ?? [],
1340
+ remoteCreditSystems,
1341
+ localPayload?.creditPacks ?? [],
1342
+ remoteCreditPacks
1343
+ )
1344
+ );
935
1345
  }
936
1346
  p4.outro(pc5.green("Diff complete \u2728"));
937
1347
  }
@@ -1046,8 +1456,8 @@ function getProjectInfo2() {
1046
1456
  const isTs = existsSync4(join3(cwd, "tsconfig.json"));
1047
1457
  let isEsm = false;
1048
1458
  try {
1049
- const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
1050
- isEsm = pkg.type === "module";
1459
+ const pkg2 = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
1460
+ isEsm = pkg2.type === "module";
1051
1461
  } catch {
1052
1462
  }
1053
1463
  return { isTs, isEsm };
@@ -1082,11 +1492,11 @@ async function runInit(options) {
1082
1492
  }
1083
1493
  }
1084
1494
  if (existsSync4(fullPath) && !options.force) {
1085
- const confirm3 = await p6.confirm({
1495
+ const confirm4 = await p6.confirm({
1086
1496
  message: `Config file already exists at ${fullPath}. Overwrite?`,
1087
1497
  initialValue: false
1088
1498
  });
1089
- if (p6.isCancel(confirm3) || !confirm3) {
1499
+ if (p6.isCancel(confirm4) || !confirm4) {
1090
1500
  p6.outro(pc7.yellow("Initialization cancelled"));
1091
1501
  process.exit(0);
1092
1502
  }
@@ -1094,11 +1504,9 @@ async function runInit(options) {
1094
1504
  const s = p6.spinner();
1095
1505
  s.start("Generating project configuration...");
1096
1506
  try {
1097
- const plans = await fetchPlans({ apiKey, apiUrl: `${getApiUrl()}/api/v1` });
1098
- const creditSystems = await fetchCreditSystems(
1099
- apiKey,
1100
- `${getApiUrl()}/api/v1`
1101
- );
1507
+ const apiUrl = `${getApiUrl()}/api/v1`;
1508
+ const plans = await fetchPlans({ apiKey, apiUrl });
1509
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
1102
1510
  const ext = extname3(fullPath);
1103
1511
  const { isEsm } = getProjectInfo2();
1104
1512
  let format = "ts";
@@ -1114,6 +1522,7 @@ async function runInit(options) {
1114
1522
  const configContent = generateConfig(
1115
1523
  plans,
1116
1524
  creditSystems,
1525
+ [],
1117
1526
  void 0,
1118
1527
  format
1119
1528
  );
@@ -1128,7 +1537,7 @@ ${pc7.dim("Credit Systems:")} ${creditSystems.length}`,
1128
1537
  );
1129
1538
  p6.outro(
1130
1539
  pc7.cyan(
1131
- `Next step: Run ${pc7.bold("owostack sync")} to apply your catalog.`
1540
+ `Next step: Run ${pc7.bold("owosk sync")} to apply your catalog.`
1132
1541
  )
1133
1542
  );
1134
1543
  } catch (e) {
@@ -1189,39 +1598,51 @@ async function runValidate(options) {
1189
1598
  for (const f of payload.features) {
1190
1599
  p7.log.message(`${pc8.green("\u2713")} ${f.slug} ${pc8.dim(`(${f.type})`)}`);
1191
1600
  }
1601
+ if (payload.creditSystems && payload.creditSystems.length > 0) {
1602
+ p7.log.step(pc8.bold("Credit Systems"));
1603
+ for (const cs of payload.creditSystems) {
1604
+ p7.log.message(
1605
+ `${pc8.green("\u2713")} ${pc8.bold(cs.slug)} ${pc8.dim(`(${cs.features.length} features)`)}`
1606
+ );
1607
+ }
1608
+ }
1192
1609
  p7.log.step(pc8.bold("Plans"));
1193
1610
  for (const p_obj of payload.plans) {
1194
1611
  p7.log.message(
1195
- `${pc8.green("\u2713")} ${pc8.bold(p_obj.slug)} ${pc8.dim(`${p_obj.currency} ${p_obj.price} / ${p_obj.interval}`)}`
1612
+ `${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
1613
  );
1197
1614
  }
1615
+ const configSettings = await loadConfigSettings(options.config);
1616
+ const testUrl = getTestApiUrl(configSettings.environments?.test);
1617
+ const liveUrl = getApiUrl(configSettings.environments?.live);
1618
+ const apiKey = getApiKey();
1198
1619
  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();
1620
+ p7.log.step(pc8.magenta("Production Mode: Checking PROD environment"));
1621
+ const apiUrl = `${liveUrl}/api/v1`;
1204
1622
  try {
1205
- const testPlans = await fetchPlans({
1623
+ const livePlans = await fetchPlans({
1206
1624
  apiKey,
1207
- apiUrl: `${testUrl}/api/v1`
1625
+ apiUrl
1208
1626
  });
1209
1627
  p7.log.success(
1210
- `TEST environment accessible (${testPlans.length} remote plans)`
1628
+ `PROD environment accessible (${livePlans.length} remote plans)`
1211
1629
  );
1212
1630
  } catch (e) {
1213
- p7.log.error(`TEST environment check failed: ${e.message}`);
1631
+ p7.log.error(`PROD environment check failed: ${e.message}`);
1214
1632
  }
1633
+ } else {
1634
+ p7.log.step(pc8.cyan("Sandbox Mode: Checking SANDBOX environment"));
1635
+ const apiUrl = `${testUrl}/api/v1`;
1215
1636
  try {
1216
- const livePlans = await fetchPlans({
1637
+ const testPlans = await fetchPlans({
1217
1638
  apiKey,
1218
- apiUrl: `${liveUrl}/api/v1`
1639
+ apiUrl
1219
1640
  });
1220
1641
  p7.log.success(
1221
- `LIVE environment accessible (${livePlans.length} remote plans)`
1642
+ `SANDBOX environment accessible (${testPlans.length} remote plans)`
1222
1643
  );
1223
1644
  } catch (e) {
1224
- p7.log.error(`LIVE environment check failed: ${e.message}`);
1645
+ p7.log.error(`SANDBOX environment check failed: ${e.message}`);
1225
1646
  }
1226
1647
  }
1227
1648
  p7.outro(pc8.green("Validation passed! \u2728"));
@@ -1272,25 +1693,26 @@ ${pc9.dim("Config:")} ${GLOBAL_CONFIG_PATH}`,
1272
1693
  // src/lib/brand.ts
1273
1694
  import pc10 from "picocolors";
1274
1695
  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")}
1696
+ ${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")}
1697
+ ${pc10.dim("\u2591\u2588 \u2588\u2591")} ${pc10.yellow("\u2591\u2588\u2591 \u2591\u2588\u2591")} ${pc10.dim("\u2591\u2588 \u2588\u2591")}
1698
+ ${pc10.dim("\u2591\u2588 \u2588\u2591")} ${pc10.yellow("\u2591\u2588\u2591 \u2588\u2591 \u2591\u2588\u2591")} ${pc10.dim("\u2591\u2588 \u2588\u2591")}
1699
+ ${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
1700
  `;
1282
1701
  function printBrand() {
1283
1702
  console.log(OWO_ASCII);
1284
1703
  }
1285
1704
 
1286
1705
  // src/index.ts
1706
+ import { createRequire } from "module";
1707
+ var require2 = createRequire(import.meta.url);
1708
+ var pkg = require2("../package.json");
1287
1709
  var program = new Command();
1288
1710
  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);
1711
+ program.name("owostack").description("CLI for Owostack billing infrastructure").version(pkg.version).option("--prod", "Execute in production environment (default: sandbox)");
1712
+ 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() }));
1713
+ 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() }));
1714
+ 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
1715
  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);
1716
+ program.command("validate").description("Validate local config without syncing").option("--config <path>", "Path to config file").action((options) => runValidate({ ...options, ...program.opts() }));
1295
1717
  program.command("connect").description("Connect CLI to dashboard via browser").option("--no-browser", "Don't open the browser automatically").action(runConnect);
1296
1718
  program.parse();