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