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.
- package/dist/index.js +805 -390
- 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
|
|
8
|
-
import
|
|
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://
|
|
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.
|
|
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,
|
|
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
|
-
|
|
142
|
-
|
|
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 =
|
|
149
|
-
s.start(`Loading ${
|
|
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(
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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(
|
|
170
|
-
|
|
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.
|
|
174
|
-
|
|
175
|
-
|
|
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 (
|
|
178
|
-
|
|
632
|
+
if (dryRun) {
|
|
633
|
+
p2.log.info(pc3.yellow("Dry run - no changes were applied."));
|
|
634
|
+
return;
|
|
179
635
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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(
|
|
655
|
+
s.stop(pc3.green("Sync completed"));
|
|
225
656
|
if (!result.success) {
|
|
226
|
-
|
|
657
|
+
p2.log.error(pc3.red("Sync failed"));
|
|
227
658
|
process.exit(1);
|
|
228
659
|
}
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
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
|
-
|
|
730
|
+
p2.log.success(pc3.dim("No changes detected. Catalog is up to date."));
|
|
244
731
|
}
|
|
245
|
-
if (result.warnings.length) {
|
|
246
|
-
|
|
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(
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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:
|
|
764
|
+
apiUrl: testUrl,
|
|
765
|
+
environment: "sandbox"
|
|
281
766
|
});
|
|
282
767
|
}
|
|
283
|
-
|
|
768
|
+
p2.outro(pc3.green("Done! \u2728"));
|
|
284
769
|
}
|
|
285
770
|
|
|
286
771
|
// src/commands/pull.ts
|
|
287
|
-
import * as
|
|
288
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
599
|
-
isEsm =
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
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
|
|
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 =
|
|
1088
|
+
const s = p3.spinner();
|
|
634
1089
|
if (options.prod) {
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
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
|
|
1095
|
+
apiUrl,
|
|
649
1096
|
...filters
|
|
650
1097
|
});
|
|
651
|
-
s.stop(`Fetched ${
|
|
1098
|
+
s.stop(`Fetched ${plans.length} plans from prod`);
|
|
652
1099
|
s.start(`Fetching credit systems...`);
|
|
653
|
-
const creditSystems = await fetchCreditSystems(apiKey,
|
|
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
|
-
|
|
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
|
-
|
|
1110
|
+
plans,
|
|
661
1111
|
creditSystems,
|
|
1112
|
+
creditPacks,
|
|
662
1113
|
defaultProvider,
|
|
663
1114
|
format
|
|
664
1115
|
);
|
|
665
1116
|
if (options.dryRun) {
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
1133
|
+
p3.log.success(pc4.green(`Wrote configuration to ${fullPath}`));
|
|
1134
|
+
printPullSummary(plans, creditSystems, creditPacks);
|
|
677
1135
|
} else {
|
|
678
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
700
|
-
|
|
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
|
|
1169
|
+
const confirm4 = await p3.confirm({
|
|
705
1170
|
message: `Config file already exists. Overwrite?`,
|
|
706
1171
|
initialValue: false
|
|
707
1172
|
});
|
|
708
|
-
if (
|
|
709
|
-
|
|
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
|
-
|
|
1179
|
+
p3.log.success(pc4.green(`Wrote configuration to ${fullPath}`));
|
|
1180
|
+
printPullSummary(plans, creditSystems, creditPacks);
|
|
715
1181
|
}
|
|
716
|
-
|
|
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
|
|
752
|
-
const
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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 (
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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 (
|
|
834
|
-
|
|
835
|
-
for (const
|
|
836
|
-
|
|
837
|
-
${pc4.bold(
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
|
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
|
|
868
|
-
const
|
|
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
|
-
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
printDiff(
|
|
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
|
|
1308
|
+
s.start(`Fetching remote catalog from ${pc5.dim("sandbox")}...`);
|
|
916
1309
|
const remotePlans = await fetchPlans({
|
|
917
1310
|
apiKey,
|
|
918
|
-
apiUrl
|
|
1311
|
+
apiUrl
|
|
919
1312
|
});
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
1037
|
-
isEsm =
|
|
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
|
|
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(
|
|
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
|
|
1085
|
-
const
|
|
1086
|
-
|
|
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
|
|
1187
|
-
const
|
|
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
|
|
1603
|
+
const livePlans = await fetchPlans({
|
|
1193
1604
|
apiKey,
|
|
1194
|
-
apiUrl
|
|
1605
|
+
apiUrl
|
|
1195
1606
|
});
|
|
1196
1607
|
p7.log.success(
|
|
1197
|
-
`
|
|
1608
|
+
`PROD environment accessible (${livePlans.length} remote plans)`
|
|
1198
1609
|
);
|
|
1199
1610
|
} catch (e) {
|
|
1200
|
-
p7.log.error(`
|
|
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
|
|
1617
|
+
const testPlans = await fetchPlans({
|
|
1204
1618
|
apiKey,
|
|
1205
|
-
apiUrl
|
|
1619
|
+
apiUrl
|
|
1206
1620
|
});
|
|
1207
1621
|
p7.log.success(
|
|
1208
|
-
`
|
|
1622
|
+
`SANDBOX environment accessible (${testPlans.length} remote plans)`
|
|
1209
1623
|
);
|
|
1210
1624
|
} catch (e) {
|
|
1211
|
-
p7.log.error(`
|
|
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
|
-
|
|
1263
|
-
${pc10.
|
|
1264
|
-
${pc10.
|
|
1265
|
-
${pc10.
|
|
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(
|
|
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("--
|
|
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("--
|
|
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").
|
|
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").
|
|
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();
|