owosk 0.1.3 → 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 +805 -390
  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,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,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));
@@ -427,7 +841,8 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
427
841
  featuresBySlug.set(f.slug, {
428
842
  slug: f.slug,
429
843
  name: f.name,
430
- type: f.type || "metered"
844
+ type: f.type || "metered",
845
+ meterType: f.meterType
431
846
  });
432
847
  }
433
848
  }
@@ -450,7 +865,9 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
450
865
  const varName = slugToIdentifier(feature.slug, usedNames);
451
866
  featureVars.set(feature.slug, varName);
452
867
  const nameArg = feature.name ? `, { name: ${JSON.stringify(feature.name)} }` : "";
453
- const decl = feature.type === "boolean" ? `boolean(${JSON.stringify(feature.slug)}${nameArg})` : `metered(${JSON.stringify(feature.slug)}${nameArg})`;
868
+ const isEntity = feature.meterType === "non_consumable";
869
+ const builder = feature.type === "boolean" ? "boolean" : isEntity ? "entity" : "metered";
870
+ const decl = `${builder}(${JSON.stringify(feature.slug)}${nameArg})`;
454
871
  if (isCjs) {
455
872
  featureLines.push(`const ${varName} = ${decl};`);
456
873
  featureLines.push(`exports.${varName} = ${varName};`);
@@ -497,6 +914,8 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
497
914
  configLines.push(`trialDays: ${plan.trialDays}`);
498
915
  if (plan.provider)
499
916
  configLines.push(`provider: ${JSON.stringify(plan.provider)}`);
917
+ if (plan.autoEnable) configLines.push(`autoEnable: true`);
918
+ if (plan.isAddon) configLines.push(`isAddon: true`);
500
919
  const featureEntries = [];
501
920
  for (const pf of plan.features || []) {
502
921
  if (creditSystemSlugs.has(pf.slug)) {
@@ -536,9 +955,13 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
536
955
  featureEntries.push(`${varName}.config(${JSON.stringify(config2)})`);
537
956
  continue;
538
957
  }
958
+ const isEntityFeature = globalFeature?.meterType === "non_consumable";
539
959
  const config = {};
540
960
  if (pf.limit !== void 0) config.limit = pf.limit;
541
- config.reset = pf.resetInterval || pf.reset || "monthly";
961
+ if (!isEntityFeature) {
962
+ const reset = pf.resetInterval || pf.reset || "monthly";
963
+ if (reset !== "none") config.reset = reset;
964
+ }
542
965
  if (pf.overage) config.overage = pf.overage;
543
966
  if (pf.overagePrice !== void 0) config.overagePrice = pf.overagePrice;
544
967
  const configKeys = Object.keys(config);
@@ -563,10 +986,39 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
563
986
  })`
564
987
  );
565
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
+ }
566
1011
  const hasCreditSystems = creditSystemLines.length > 0;
1012
+ const hasCreditPacks = creditPackLines.length > 0;
1013
+ const hasEntities = Array.from(featuresBySlug.values()).some(
1014
+ (f) => f.meterType === "non_consumable"
1015
+ );
567
1016
  const providerLine = defaultProvider ? ` provider: ${JSON.stringify(defaultProvider)},
568
1017
  ` : "";
569
- const imports = isCjs ? `const { Owostack, metered, boolean, creditSystem, plan } = require("owostack");` : `import { Owostack, metered, boolean, creditSystem, plan } from "owostack";`;
1018
+ const importParts = ["Owostack", "metered", "boolean"];
1019
+ if (hasEntities) importParts.push("entity");
1020
+ importParts.push("creditSystem", "creditPack", "plan");
1021
+ const imports = isCjs ? `const { ${importParts.join(", ")} } = require("owostack");` : `import { ${importParts.join(", ")} } from "owostack";`;
570
1022
  const tsCheck = !isTs ? `// @ts-check` : "";
571
1023
  const jsDoc = !isTs ? `/** @type {import('owostack').Owostack} */` : "";
572
1024
  const owoDecl = isCjs ? "exports.owo =" : "export const owo =";
@@ -577,13 +1029,15 @@ function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts
577
1029
  ``,
578
1030
  ...featureLines,
579
1031
  ...hasCreditSystems ? ["", ...creditSystemLines] : [],
1032
+ ...hasCreditPacks ? ["", ...creditPackLines] : [],
580
1033
  ``,
581
1034
  jsDoc,
582
1035
  `${owoDecl} new Owostack({`,
583
1036
  ` secretKey: ${secretKey},`,
584
1037
  providerLine,
585
1038
  ` catalog: [`,
586
- ` ${planLines.join(",\n ")}`,
1039
+ ` ${planLines.join(",\n ")}${hasCreditPacks ? "," : ""}`,
1040
+ ...hasCreditPacks ? [` ${creditPackLines.join(",\n ")}`] : [],
587
1041
  ` ],`,
588
1042
  `});`,
589
1043
  ``
@@ -595,8 +1049,8 @@ function getProjectInfo() {
595
1049
  const cwd = process.cwd();
596
1050
  let isEsm = false;
597
1051
  try {
598
- const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
599
- isEsm = pkg.type === "module";
1052
+ const pkg2 = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
1053
+ isEsm = pkg2.type === "module";
600
1054
  } catch {
601
1055
  }
602
1056
  return { isEsm };
@@ -611,15 +1065,15 @@ function determineFormat(fullPath) {
611
1065
  return "ts";
612
1066
  }
613
1067
  async function runPull(options) {
614
- p2.intro(pc3.bgYellow(pc3.black(" pull ")));
1068
+ p3.intro(pc4.bgYellow(pc4.black(" pull ")));
615
1069
  let fullPath;
616
1070
  if (options.config) {
617
1071
  fullPath = isAbsolute2(options.config) ? options.config : resolve2(process.cwd(), options.config);
618
1072
  } else {
619
1073
  const resolved = resolveConfigPath();
620
1074
  if (!resolved) {
621
- p2.log.error(
622
- 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.")
623
1077
  );
624
1078
  process.exit(1);
625
1079
  }
@@ -627,64 +1081,73 @@ async function runPull(options) {
627
1081
  }
628
1082
  const apiKey = getApiKey(options.key);
629
1083
  const configSettings = await loadConfigSettings(options.config);
630
- const baseUrl = getApiUrl(configSettings.apiUrl);
1084
+ const testUrl = getTestApiUrl(configSettings.environments?.test);
1085
+ const liveUrl = getApiUrl(configSettings.environments?.live);
631
1086
  const filters = configSettings.filters || {};
632
1087
  const format = determineFormat(fullPath);
633
- const s = p2.spinner();
1088
+ const s = p3.spinner();
634
1089
  if (options.prod) {
635
- p2.log.step(pc3.magenta("Production Mode: Fetching both environments"));
636
- const testUrl = getTestApiUrl(configSettings.environments?.test);
637
- const liveUrl = getApiUrl(configSettings.environments?.live);
638
- s.start(`Fetching from ${pc3.dim("test")}...`);
639
- const testPlans = await fetchPlans({
640
- apiKey,
641
- apiUrl: `${testUrl}/api/v1`,
642
- ...filters
643
- });
644
- s.stop(`Fetched ${testPlans.length} plans from test`);
645
- s.start(`Fetching from ${pc3.dim("live")}...`);
646
- 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({
647
1094
  apiKey,
648
- apiUrl: `${liveUrl}/api/v1`,
1095
+ apiUrl,
649
1096
  ...filters
650
1097
  });
651
- s.stop(`Fetched ${livePlans.length} plans from live`);
1098
+ s.stop(`Fetched ${plans.length} plans from prod`);
652
1099
  s.start(`Fetching credit systems...`);
653
- const creditSystems = await fetchCreditSystems(apiKey, `${liveUrl}/api/v1`);
1100
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
654
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`);
655
1105
  const providers = new Set(
656
- livePlans.map((p9) => p9.provider).filter(Boolean)
1106
+ plans.map((p9) => p9.provider).filter(Boolean)
657
1107
  );
658
1108
  const defaultProvider = providers.size === 1 ? Array.from(providers)[0] : void 0;
659
1109
  const configContent = generateConfig(
660
- livePlans,
1110
+ plans,
661
1111
  creditSystems,
1112
+ creditPacks,
662
1113
  defaultProvider,
663
1114
  format
664
1115
  );
665
1116
  if (options.dryRun) {
666
- p2.note(configContent, "Generated Config (Dry Run)");
667
- 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."));
668
1120
  return;
669
1121
  }
670
1122
  if (existsSync3(fullPath) && !options.force) {
671
- p2.log.error(pc3.red(`Config file already exists at ${fullPath}`));
672
- p2.log.info(pc3.dim("Use --force to overwrite."));
673
- 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
+ }
674
1131
  }
675
1132
  await writeFile2(fullPath, configContent, "utf8");
676
- p2.log.success(pc3.green(`Wrote configuration to ${fullPath}`));
1133
+ p3.log.success(pc4.green(`Wrote configuration to ${fullPath}`));
1134
+ printPullSummary(plans, creditSystems, creditPacks);
677
1135
  } else {
678
- 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")}...`);
679
1139
  const plans = await fetchPlans({
680
1140
  apiKey,
681
- apiUrl: `${baseUrl}/api/v1`,
1141
+ apiUrl,
682
1142
  ...filters
683
1143
  });
684
- s.stop(`Fetched ${plans.length} plans`);
1144
+ s.stop(`Fetched ${plans.length} plans from sandbox`);
685
1145
  s.start(`Fetching credit systems...`);
686
- const creditSystems = await fetchCreditSystems(apiKey, `${baseUrl}/api/v1`);
1146
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
687
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`);
688
1151
  const providers = new Set(
689
1152
  plans.map((p9) => p9.provider).filter(Boolean)
690
1153
  );
@@ -692,166 +1155,75 @@ async function runPull(options) {
692
1155
  const configContent = generateConfig(
693
1156
  plans,
694
1157
  creditSystems,
1158
+ creditPacks,
695
1159
  defaultProvider,
696
1160
  format
697
1161
  );
698
1162
  if (options.dryRun) {
699
- p2.note(configContent, "Generated Config (Dry Run)");
700
- 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."));
701
1166
  return;
702
1167
  }
703
1168
  if (existsSync3(fullPath) && !options.force) {
704
- const confirm3 = await p2.confirm({
1169
+ const confirm4 = await p3.confirm({
705
1170
  message: `Config file already exists. Overwrite?`,
706
1171
  initialValue: false
707
1172
  });
708
- if (p2.isCancel(confirm3) || !confirm3) {
709
- p2.outro(pc3.yellow("Operation cancelled"));
1173
+ if (p3.isCancel(confirm4) || !confirm4) {
1174
+ p3.outro(pc4.yellow("Operation cancelled"));
710
1175
  process.exit(0);
711
1176
  }
712
1177
  }
713
1178
  await writeFile2(fullPath, configContent, "utf8");
714
- p2.log.success(pc3.green(`Wrote configuration to ${fullPath}`));
1179
+ p3.log.success(pc4.green(`Wrote configuration to ${fullPath}`));
1180
+ printPullSummary(plans, creditSystems, creditPacks);
715
1181
  }
716
- p2.outro(pc3.green("Pull complete! \u2728"));
717
- }
718
-
719
- // src/commands/diff.ts
720
- import * as p4 from "@clack/prompts";
721
- import pc5 from "picocolors";
722
-
723
- // src/lib/diff.ts
724
- import pc4 from "picocolors";
725
- import * as p3 from "@clack/prompts";
726
- function normalizeFeature(pf) {
727
- return {
728
- slug: pf.slug,
729
- enabled: pf.enabled,
730
- limit: pf.limit ?? null,
731
- // Handle both SDK 'reset' and API 'resetInterval'
732
- reset: pf.reset || pf.resetInterval || "monthly",
733
- // Handle both SDK 'overage' and API 'overage' (same name)
734
- overage: pf.overage || "block",
735
- overagePrice: pf.overagePrice ?? null
736
- };
737
- }
738
- function normalizePlan(plan) {
739
- return {
740
- slug: plan.slug,
741
- name: plan.name ?? null,
742
- description: plan.description ?? null,
743
- price: plan.price ?? 0,
744
- currency: plan.currency ?? null,
745
- interval: plan.interval ?? null,
746
- planGroup: plan.planGroup ?? null,
747
- trialDays: plan.trialDays ?? 0,
748
- features: (plan.features || []).map(normalizeFeature).sort((a, b) => a.slug.localeCompare(b.slug))
749
- };
1182
+ p3.outro(pc4.green("Pull complete! \u2728"));
750
1183
  }
751
- function diffPlans(localPlans, remotePlans) {
752
- const localMap = /* @__PURE__ */ new Map();
753
- const remoteMap = /* @__PURE__ */ new Map();
754
- for (const p9 of localPlans) localMap.set(p9.slug, normalizePlan(p9));
755
- for (const p9 of remotePlans) remoteMap.set(p9.slug, normalizePlan(p9));
756
- const onlyLocal = [];
757
- const onlyRemote = [];
758
- const changed = [];
759
- for (const slug of localMap.keys()) {
760
- if (!remoteMap.has(slug)) onlyLocal.push(slug);
761
- }
762
- for (const slug of remoteMap.keys()) {
763
- if (!localMap.has(slug)) onlyRemote.push(slug);
764
- }
765
- for (const slug of localMap.keys()) {
766
- if (!remoteMap.has(slug)) continue;
767
- const local = localMap.get(slug);
768
- const remote = remoteMap.get(slug);
769
- const details = [];
770
- const fields = [
771
- "name",
772
- "description",
773
- "price",
774
- "currency",
775
- "interval",
776
- "planGroup",
777
- "trialDays"
778
- ];
779
- for (const field of fields) {
780
- if (local[field] !== remote[field]) {
781
- const localVal = JSON.stringify(local[field]);
782
- const remoteVal = JSON.stringify(remote[field]);
783
- details.push(
784
- `${String(field)}: ${pc4.green(localVal)} \u2192 ${pc4.red(remoteVal)}`
785
- );
786
- }
787
- }
788
- const localFeatures = new Map(local.features.map((f) => [f.slug, f]));
789
- const remoteFeatures = new Map(
790
- remote.features.map((f) => [f.slug, f])
791
- );
792
- for (const fslug of localFeatures.keys()) {
793
- if (!remoteFeatures.has(fslug)) {
794
- details.push(`feature ${fslug}: ${pc4.green("[local only]")}`);
795
- continue;
796
- }
797
- const lf = localFeatures.get(fslug);
798
- const rf = remoteFeatures.get(fslug);
799
- if (JSON.stringify(lf) !== JSON.stringify(rf)) {
800
- details.push(`feature ${fslug}:`);
801
- details.push(` ${pc4.green(JSON.stringify(lf))}`);
802
- details.push(` ${pc4.red(JSON.stringify(rf))}`);
803
- }
804
- }
805
- for (const fslug of remoteFeatures.keys()) {
806
- if (!localFeatures.has(fslug)) {
807
- details.push(`feature ${fslug}: ${pc4.red("[remote only]")}`);
808
- }
809
- }
810
- if (details.length > 0) {
811
- 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);
812
1189
  }
813
1190
  }
814
- return { onlyLocal, onlyRemote, changed };
815
- }
816
- function printDiff(diff) {
817
- if (diff.onlyLocal.length === 0 && diff.onlyRemote.length === 0 && diff.changed.length === 0) {
818
- p3.log.success(pc4.green("No differences found."));
819
- return;
820
- }
821
- if (diff.onlyLocal.length > 0) {
822
- p3.note(
823
- diff.onlyLocal.map((slug) => `${pc4.green("+")} ${slug}`).join("\n"),
824
- "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)") : ""}`
825
1196
  );
826
1197
  }
827
- if (diff.onlyRemote.length > 0) {
828
- p3.note(
829
- diff.onlyRemote.map((slug) => `${pc4.red("-")} ${slug}`).join("\n"),
830
- "Only in Remote"
831
- );
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
+ }
832
1206
  }
833
- if (diff.changed.length > 0) {
834
- let changedText = "";
835
- for (const item of diff.changed) {
836
- changedText += `
837
- ${pc4.bold(item.slug)}
838
- `;
839
- for (const line of item.details) {
840
- changedText += ` ${line}
841
- `;
842
- }
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
+ );
843
1213
  }
844
- p3.note(changedText.trim(), "Changed Plans");
845
1214
  }
846
- const summary = [
847
- diff.onlyLocal.length > 0 ? `${pc4.green(diff.onlyLocal.length.toString())} added` : "",
848
- diff.onlyRemote.length > 0 ? `${pc4.red(diff.onlyRemote.length.toString())} removed` : "",
849
- diff.changed.length > 0 ? `${pc4.yellow(diff.changed.length.toString())} changed` : ""
850
- ].filter(Boolean).join(" ");
851
- 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);
852
1222
  }
853
1223
 
854
1224
  // src/commands/diff.ts
1225
+ import * as p4 from "@clack/prompts";
1226
+ import pc5 from "picocolors";
855
1227
  async function runDiff(options) {
856
1228
  p4.intro(pc5.bgYellow(pc5.black(" diff ")));
857
1229
  const fullPath = resolveConfigPath(options.config);
@@ -861,12 +1233,12 @@ async function runDiff(options) {
861
1233
  }
862
1234
  const apiKey = getApiKey(options.key);
863
1235
  const configSettings = await loadConfigSettings(options.config);
864
- const baseUrl = getApiUrl(configSettings.apiUrl);
1236
+ const testUrl = getTestApiUrl(configSettings.environments?.test);
1237
+ const liveUrl = getApiUrl(configSettings.environments?.live);
865
1238
  const s = p4.spinner();
866
1239
  if (options.prod) {
867
- p4.log.step(pc5.magenta("Production Mode: Comparing both environments"));
868
- const testUrl = getTestApiUrl(configSettings.environments?.test);
869
- const liveUrl = getApiUrl(configSettings.environments?.live);
1240
+ p4.log.step(pc5.magenta("Production Mode: Comparing with PROD environment"));
1241
+ const apiUrl = `${liveUrl}/api/v1`;
870
1242
  s.start("Loading local configuration...");
871
1243
  let owo;
872
1244
  try {
@@ -881,18 +1253,34 @@ async function runDiff(options) {
881
1253
  );
882
1254
  process.exit(1);
883
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
+ }
884
1261
  s.stop("Configuration loaded");
885
1262
  const { buildSyncPayload } = await import("owostack").catch(() => ({
886
1263
  buildSyncPayload: null
887
1264
  }));
888
1265
  const localPayload = buildSyncPayload(owo._config.catalog);
889
- p4.log.step(pc5.cyan(`Comparing with TEST: ${testUrl}`));
890
- const testPlans = await fetchPlans({ apiKey, apiUrl: `${testUrl}/api/v1` });
891
- printDiff(diffPlans(localPayload?.plans ?? [], testPlans));
892
- p4.log.step(pc5.cyan(`Comparing with LIVE: ${liveUrl}`));
893
- const livePlans = await fetchPlans({ apiKey, apiUrl: `${liveUrl}/api/v1` });
894
- 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
+ );
895
1281
  } else {
1282
+ p4.log.step(pc5.cyan("Sandbox Mode: Comparing with SANDBOX environment"));
1283
+ const apiUrl = `${testUrl}/api/v1`;
896
1284
  s.start("Loading local configuration...");
897
1285
  let owo;
898
1286
  try {
@@ -907,18 +1295,34 @@ async function runDiff(options) {
907
1295
  );
908
1296
  process.exit(1);
909
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
+ }
910
1303
  s.stop("Configuration loaded");
911
1304
  const { buildSyncPayload } = await import("owostack").catch(() => ({
912
1305
  buildSyncPayload: null
913
1306
  }));
914
1307
  const localPayload = buildSyncPayload(owo._config.catalog);
915
- s.start(`Fetching remote plans from ${pc5.dim(baseUrl)}...`);
1308
+ s.start(`Fetching remote catalog from ${pc5.dim("sandbox")}...`);
916
1309
  const remotePlans = await fetchPlans({
917
1310
  apiKey,
918
- apiUrl: `${baseUrl}/api/v1`
1311
+ apiUrl
919
1312
  });
920
- s.stop("Remote plans fetched");
921
- 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
+ );
922
1326
  }
923
1327
  p4.outro(pc5.green("Diff complete \u2728"));
924
1328
  }
@@ -1033,8 +1437,8 @@ function getProjectInfo2() {
1033
1437
  const isTs = existsSync4(join3(cwd, "tsconfig.json"));
1034
1438
  let isEsm = false;
1035
1439
  try {
1036
- const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
1037
- isEsm = pkg.type === "module";
1440
+ const pkg2 = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
1441
+ isEsm = pkg2.type === "module";
1038
1442
  } catch {
1039
1443
  }
1040
1444
  return { isTs, isEsm };
@@ -1069,11 +1473,11 @@ async function runInit(options) {
1069
1473
  }
1070
1474
  }
1071
1475
  if (existsSync4(fullPath) && !options.force) {
1072
- const confirm3 = await p6.confirm({
1476
+ const confirm4 = await p6.confirm({
1073
1477
  message: `Config file already exists at ${fullPath}. Overwrite?`,
1074
1478
  initialValue: false
1075
1479
  });
1076
- if (p6.isCancel(confirm3) || !confirm3) {
1480
+ if (p6.isCancel(confirm4) || !confirm4) {
1077
1481
  p6.outro(pc7.yellow("Initialization cancelled"));
1078
1482
  process.exit(0);
1079
1483
  }
@@ -1081,11 +1485,9 @@ async function runInit(options) {
1081
1485
  const s = p6.spinner();
1082
1486
  s.start("Generating project configuration...");
1083
1487
  try {
1084
- const plans = await fetchPlans({ apiKey, apiUrl: `${getApiUrl()}/api/v1` });
1085
- const creditSystems = await fetchCreditSystems(
1086
- apiKey,
1087
- `${getApiUrl()}/api/v1`
1088
- );
1488
+ const apiUrl = `${getApiUrl()}/api/v1`;
1489
+ const plans = await fetchPlans({ apiKey, apiUrl });
1490
+ const creditSystems = await fetchCreditSystems(apiKey, apiUrl);
1089
1491
  const ext = extname3(fullPath);
1090
1492
  const { isEsm } = getProjectInfo2();
1091
1493
  let format = "ts";
@@ -1176,39 +1578,51 @@ async function runValidate(options) {
1176
1578
  for (const f of payload.features) {
1177
1579
  p7.log.message(`${pc8.green("\u2713")} ${f.slug} ${pc8.dim(`(${f.type})`)}`);
1178
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
+ }
1179
1589
  p7.log.step(pc8.bold("Plans"));
1180
1590
  for (const p_obj of payload.plans) {
1181
1591
  p7.log.message(
1182
- `${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}`)}`
1183
1593
  );
1184
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();
1185
1599
  if (options.prod) {
1186
- p7.log.step(pc8.magenta("Production Mode Check"));
1187
- const configSettings = await loadConfigSettings(options.config);
1188
- const testUrl = getTestApiUrl(configSettings.environments?.test);
1189
- const liveUrl = getApiUrl(configSettings.environments?.live);
1190
- const apiKey = getApiKey();
1600
+ p7.log.step(pc8.magenta("Production Mode: Checking PROD environment"));
1601
+ const apiUrl = `${liveUrl}/api/v1`;
1191
1602
  try {
1192
- const testPlans = await fetchPlans({
1603
+ const livePlans = await fetchPlans({
1193
1604
  apiKey,
1194
- apiUrl: `${testUrl}/api/v1`
1605
+ apiUrl
1195
1606
  });
1196
1607
  p7.log.success(
1197
- `TEST environment accessible (${testPlans.length} remote plans)`
1608
+ `PROD environment accessible (${livePlans.length} remote plans)`
1198
1609
  );
1199
1610
  } catch (e) {
1200
- p7.log.error(`TEST environment check failed: ${e.message}`);
1611
+ p7.log.error(`PROD environment check failed: ${e.message}`);
1201
1612
  }
1613
+ } else {
1614
+ p7.log.step(pc8.cyan("Sandbox Mode: Checking SANDBOX environment"));
1615
+ const apiUrl = `${testUrl}/api/v1`;
1202
1616
  try {
1203
- const livePlans = await fetchPlans({
1617
+ const testPlans = await fetchPlans({
1204
1618
  apiKey,
1205
- apiUrl: `${liveUrl}/api/v1`
1619
+ apiUrl
1206
1620
  });
1207
1621
  p7.log.success(
1208
- `LIVE environment accessible (${livePlans.length} remote plans)`
1622
+ `SANDBOX environment accessible (${testPlans.length} remote plans)`
1209
1623
  );
1210
1624
  } catch (e) {
1211
- p7.log.error(`LIVE environment check failed: ${e.message}`);
1625
+ p7.log.error(`SANDBOX environment check failed: ${e.message}`);
1212
1626
  }
1213
1627
  }
1214
1628
  p7.outro(pc8.green("Validation passed! \u2728"));
@@ -1259,25 +1673,26 @@ ${pc9.dim("Config:")} ${GLOBAL_CONFIG_PATH}`,
1259
1673
  // src/lib/brand.ts
1260
1674
  import pc10 from "picocolors";
1261
1675
  var OWO_ASCII = `
1262
- ${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")}
1263
- ${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")}
1264
- ${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")}
1265
- ${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")}
1266
- ${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")}
1267
- ${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")}
1268
1680
  `;
1269
1681
  function printBrand() {
1270
1682
  console.log(OWO_ASCII);
1271
1683
  }
1272
1684
 
1273
1685
  // src/index.ts
1686
+ import { createRequire } from "module";
1687
+ var require2 = createRequire(import.meta.url);
1688
+ var pkg = require2("../package.json");
1274
1689
  var program = new Command();
1275
1690
  printBrand();
1276
- program.name("owostack").description("CLI for Owostack billing infrastructure").version("0.1.0");
1277
- 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);
1278
- 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);
1279
- 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() }));
1280
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);
1281
- 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() }));
1282
1697
  program.command("connect").description("Connect CLI to dashboard via browser").option("--no-browser", "Don't open the browser automatically").action(runConnect);
1283
1698
  program.parse();