owosk 0.1.2 → 0.1.3
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/README.md +2 -2
- package/dist/index.js +399 -165
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npx owo --help
|
|
|
20
20
|
|
|
21
21
|
### `init`
|
|
22
22
|
|
|
23
|
-
Initialize a new Owostack project with a default `owo.config.ts
|
|
23
|
+
Initialize a new Owostack project with a default configuration file (`owo.config.ts` or `owo.config.js`).
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
npx owo init
|
|
@@ -52,7 +52,7 @@ npx owo diff
|
|
|
52
52
|
|
|
53
53
|
### `validate`
|
|
54
54
|
|
|
55
|
-
Check your local
|
|
55
|
+
Check your local configuration for errors without applying changes.
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
npx owo validate
|
package/dist/index.js
CHANGED
|
@@ -46,17 +46,41 @@ function getApiKey(cliKey) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// src/lib/loader.ts
|
|
49
|
-
import { resolve, isAbsolute } from "path";
|
|
50
|
-
import { pathToFileURL } from "url";
|
|
49
|
+
import { resolve, isAbsolute, extname } from "path";
|
|
51
50
|
import { existsSync as existsSync2 } from "fs";
|
|
51
|
+
import { createJiti } from "jiti";
|
|
52
|
+
var jiti = createJiti(import.meta.url);
|
|
53
|
+
var DEFAULT_CONFIG_NAMES = [
|
|
54
|
+
"owo.config.ts",
|
|
55
|
+
"owo.config.js",
|
|
56
|
+
"owo.config.mjs",
|
|
57
|
+
"owo.config.cjs",
|
|
58
|
+
"owo.config.mts",
|
|
59
|
+
"owo.config.cts"
|
|
60
|
+
];
|
|
52
61
|
function resolveConfigPath(configPath) {
|
|
53
|
-
|
|
62
|
+
if (configPath) {
|
|
63
|
+
const fullPath = isAbsolute(configPath) ? configPath : resolve(process.cwd(), configPath);
|
|
64
|
+
if (existsSync2(fullPath)) return fullPath;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
for (const name of DEFAULT_CONFIG_NAMES) {
|
|
68
|
+
const fullPath = resolve(process.cwd(), name);
|
|
69
|
+
if (existsSync2(fullPath)) return fullPath;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
54
72
|
}
|
|
55
73
|
async function loadOwostackFromConfig(fullPath) {
|
|
56
|
-
let configModule;
|
|
57
74
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
75
|
+
const configModule = await jiti.import(fullPath);
|
|
76
|
+
const instance = configModule.default || configModule.owo || configModule;
|
|
77
|
+
if (instance && typeof instance.sync === "function") {
|
|
78
|
+
return instance;
|
|
79
|
+
}
|
|
80
|
+
if (typeof configModule.sync === "function") {
|
|
81
|
+
return configModule;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
60
84
|
} catch (e) {
|
|
61
85
|
console.error(`
|
|
62
86
|
\u274C Failed to load config from ${fullPath}`);
|
|
@@ -65,23 +89,36 @@ async function loadOwostackFromConfig(fullPath) {
|
|
|
65
89
|
console.error(
|
|
66
90
|
` Make sure the file exports an Owostack instance as default or named 'owo'.`
|
|
67
91
|
);
|
|
68
|
-
|
|
92
|
+
const ext = extname(fullPath);
|
|
93
|
+
const isTs = ext === ".ts" || ext === ".mts" || ext === ".cts";
|
|
94
|
+
if (isTs) {
|
|
95
|
+
console.error(` Example owo.config.ts:
|
|
69
96
|
`);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
console.error(
|
|
98
|
+
` import { Owostack, metered, boolean, plan } from "owostack";`
|
|
99
|
+
);
|
|
100
|
+
console.error(
|
|
101
|
+
` export default new Owostack({ secretKey: "...", catalog: [...] });
|
|
75
102
|
`
|
|
76
|
-
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
console.error(` Example owo.config.js:
|
|
106
|
+
`);
|
|
107
|
+
console.error(
|
|
108
|
+
` const { Owostack, metered, boolean, plan } = require("owostack");`
|
|
109
|
+
);
|
|
110
|
+
console.error(
|
|
111
|
+
` module.exports = new Owostack({ secretKey: "...", catalog: [...] });
|
|
112
|
+
`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
77
115
|
process.exit(1);
|
|
78
116
|
}
|
|
79
|
-
return configModule.default || configModule.owo;
|
|
80
117
|
}
|
|
81
118
|
async function loadConfigSettings(configPath) {
|
|
82
119
|
try {
|
|
83
120
|
const fullPath = resolveConfigPath(configPath);
|
|
84
|
-
if (!
|
|
121
|
+
if (!fullPath) return {};
|
|
85
122
|
const owo = await loadOwostackFromConfig(fullPath);
|
|
86
123
|
if (!owo || !owo._config) return {};
|
|
87
124
|
return {
|
|
@@ -90,7 +127,7 @@ async function loadConfigSettings(configPath) {
|
|
|
90
127
|
filters: owo._config.filters,
|
|
91
128
|
connect: owo._config.connect
|
|
92
129
|
};
|
|
93
|
-
} catch {
|
|
130
|
+
} catch (e) {
|
|
94
131
|
return {};
|
|
95
132
|
}
|
|
96
133
|
}
|
|
@@ -100,9 +137,34 @@ async function runSyncSingle(options) {
|
|
|
100
137
|
const { configPath, dryRun, apiUrl } = options;
|
|
101
138
|
const apiKey = getApiKey(options.apiKey);
|
|
102
139
|
const fullPath = resolveConfigPath(configPath);
|
|
140
|
+
if (!fullPath) {
|
|
141
|
+
p.log.error(
|
|
142
|
+
pc.red(
|
|
143
|
+
`Configuration file not found.${configPath ? ` looked at ${configPath}` : " searched defaults."}`
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
103
148
|
const s = p.spinner();
|
|
104
|
-
s.start(`Loading ${pc.cyan(
|
|
105
|
-
|
|
149
|
+
s.start(`Loading ${pc.cyan(fullPath)}`);
|
|
150
|
+
let owo;
|
|
151
|
+
try {
|
|
152
|
+
owo = await loadOwostackFromConfig(fullPath);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
s.stop(pc.red("Failed to load configuration"));
|
|
155
|
+
p.log.error(pc.red(`Error: ${e.message}`));
|
|
156
|
+
p.note(
|
|
157
|
+
`import { Owostack, metered, boolean, plan } from "owostack";
|
|
158
|
+
export default new Owostack({ secretKey: "...", catalog: [...] });`,
|
|
159
|
+
"Example owo.config.ts"
|
|
160
|
+
);
|
|
161
|
+
p.log.info(
|
|
162
|
+
pc.dim(
|
|
163
|
+
"Make sure 'owostack' is installed in your project: 'npm install owostack'"
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
106
168
|
if (!owo || typeof owo.sync !== "function") {
|
|
107
169
|
s.stop(pc.red("Invalid configuration"));
|
|
108
170
|
p.log.error("Config file must export an Owostack instance.");
|
|
@@ -223,11 +285,13 @@ async function runSync(options) {
|
|
|
223
285
|
|
|
224
286
|
// src/commands/pull.ts
|
|
225
287
|
import * as p2 from "@clack/prompts";
|
|
226
|
-
import
|
|
227
|
-
import { existsSync as existsSync3 } from "fs";
|
|
288
|
+
import pc3 from "picocolors";
|
|
289
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
228
290
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
291
|
+
import { join as join2, resolve as resolve2, extname as extname2, isAbsolute as isAbsolute2 } from "path";
|
|
229
292
|
|
|
230
293
|
// src/lib/api.ts
|
|
294
|
+
import pc2 from "picocolors";
|
|
231
295
|
async function fetchPlans(options) {
|
|
232
296
|
if (!options.apiKey) {
|
|
233
297
|
console.error(
|
|
@@ -242,19 +306,40 @@ async function fetchPlans(options) {
|
|
|
242
306
|
if (options.interval) url.searchParams.set("interval", options.interval);
|
|
243
307
|
if (options.currency) url.searchParams.set("currency", options.currency);
|
|
244
308
|
if (options.includeInactive) url.searchParams.set("includeInactive", "true");
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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(`
|
|
253
318
|
\u274C Failed to fetch plans: ${message}
|
|
254
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
|
+
}
|
|
255
341
|
process.exit(1);
|
|
256
342
|
}
|
|
257
|
-
return data?.plans || [];
|
|
258
343
|
}
|
|
259
344
|
async function fetchCreditSystems(apiKey, apiUrl) {
|
|
260
345
|
if (!apiKey) {
|
|
@@ -329,7 +414,9 @@ function slugToIdentifier(slug, used) {
|
|
|
329
414
|
used.add(candidate);
|
|
330
415
|
return candidate;
|
|
331
416
|
}
|
|
332
|
-
function generateConfig(plans, creditSystems = [], defaultProvider) {
|
|
417
|
+
function generateConfig(plans, creditSystems = [], defaultProvider, format = "ts") {
|
|
418
|
+
const isTs = format === "ts";
|
|
419
|
+
const isCjs = format === "cjs";
|
|
333
420
|
const creditSystemSlugs = new Set(creditSystems.map((cs) => cs.slug));
|
|
334
421
|
const creditSystemBySlug = new Map(creditSystems.map((cs) => [cs.slug, cs]));
|
|
335
422
|
const featuresBySlug = /* @__PURE__ */ new Map();
|
|
@@ -363,14 +450,12 @@ function generateConfig(plans, creditSystems = [], defaultProvider) {
|
|
|
363
450
|
const varName = slugToIdentifier(feature.slug, usedNames);
|
|
364
451
|
featureVars.set(feature.slug, varName);
|
|
365
452
|
const nameArg = feature.name ? `, { name: ${JSON.stringify(feature.name)} }` : "";
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
);
|
|
453
|
+
const decl = feature.type === "boolean" ? `boolean(${JSON.stringify(feature.slug)}${nameArg})` : `metered(${JSON.stringify(feature.slug)}${nameArg})`;
|
|
454
|
+
if (isCjs) {
|
|
455
|
+
featureLines.push(`const ${varName} = ${decl};`);
|
|
456
|
+
featureLines.push(`exports.${varName} = ${varName};`);
|
|
370
457
|
} else {
|
|
371
|
-
featureLines.push(
|
|
372
|
-
`export const ${varName} = metered(${JSON.stringify(feature.slug)}${nameArg});`
|
|
373
|
-
);
|
|
458
|
+
featureLines.push(`export const ${varName} = ${decl};`);
|
|
374
459
|
}
|
|
375
460
|
}
|
|
376
461
|
const creditSystemLines = [];
|
|
@@ -389,9 +474,13 @@ function generateConfig(plans, creditSystems = [], defaultProvider) {
|
|
|
389
474
|
descArg,
|
|
390
475
|
`features: [${featureEntries.join(", ")}]`
|
|
391
476
|
].filter(Boolean);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
477
|
+
const decl = `creditSystem(${JSON.stringify(cs.slug)}, { ${optsParts.join(", ")} })`;
|
|
478
|
+
if (isCjs) {
|
|
479
|
+
creditSystemLines.push(`const ${varName} = ${decl};`);
|
|
480
|
+
creditSystemLines.push(`exports.${varName} = ${varName};`);
|
|
481
|
+
} else {
|
|
482
|
+
creditSystemLines.push(`export const ${varName} = ${decl};`);
|
|
483
|
+
}
|
|
395
484
|
}
|
|
396
485
|
const planLines = [];
|
|
397
486
|
for (const plan of plans) {
|
|
@@ -477,44 +566,83 @@ function generateConfig(plans, creditSystems = [], defaultProvider) {
|
|
|
477
566
|
const hasCreditSystems = creditSystemLines.length > 0;
|
|
478
567
|
const providerLine = defaultProvider ? ` provider: ${JSON.stringify(defaultProvider)},
|
|
479
568
|
` : "";
|
|
569
|
+
const imports = isCjs ? `const { Owostack, metered, boolean, creditSystem, plan } = require("owostack");` : `import { Owostack, metered, boolean, creditSystem, plan } from "owostack";`;
|
|
570
|
+
const tsCheck = !isTs ? `// @ts-check` : "";
|
|
571
|
+
const jsDoc = !isTs ? `/** @type {import('owostack').Owostack} */` : "";
|
|
572
|
+
const owoDecl = isCjs ? "exports.owo =" : "export const owo =";
|
|
573
|
+
const secretKey = isTs ? "process.env.OWOSTACK_SECRET_KEY!" : "process.env.OWOSTACK_SECRET_KEY";
|
|
480
574
|
return [
|
|
481
|
-
|
|
575
|
+
tsCheck,
|
|
576
|
+
imports,
|
|
482
577
|
``,
|
|
483
578
|
...featureLines,
|
|
484
579
|
...hasCreditSystems ? ["", ...creditSystemLines] : [],
|
|
485
580
|
``,
|
|
486
|
-
|
|
487
|
-
|
|
581
|
+
jsDoc,
|
|
582
|
+
`${owoDecl} new Owostack({`,
|
|
583
|
+
` secretKey: ${secretKey},`,
|
|
488
584
|
providerLine,
|
|
489
585
|
` catalog: [`,
|
|
490
586
|
` ${planLines.join(",\n ")}`,
|
|
491
587
|
` ],`,
|
|
492
588
|
`});`,
|
|
493
589
|
``
|
|
494
|
-
].join("\n");
|
|
590
|
+
].filter(Boolean).join("\n");
|
|
495
591
|
}
|
|
496
592
|
|
|
497
593
|
// src/commands/pull.ts
|
|
594
|
+
function getProjectInfo() {
|
|
595
|
+
const cwd = process.cwd();
|
|
596
|
+
let isEsm = false;
|
|
597
|
+
try {
|
|
598
|
+
const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
599
|
+
isEsm = pkg.type === "module";
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
return { isEsm };
|
|
603
|
+
}
|
|
604
|
+
function determineFormat(fullPath) {
|
|
605
|
+
const ext = extname2(fullPath);
|
|
606
|
+
const { isEsm } = getProjectInfo();
|
|
607
|
+
if (ext === ".ts" || ext === ".mts" || ext === ".cts") return "ts";
|
|
608
|
+
if (ext === ".mjs") return "esm";
|
|
609
|
+
if (ext === ".cjs") return "cjs";
|
|
610
|
+
if (ext === ".js") return isEsm ? "esm" : "cjs";
|
|
611
|
+
return "ts";
|
|
612
|
+
}
|
|
498
613
|
async function runPull(options) {
|
|
499
|
-
p2.intro(
|
|
500
|
-
|
|
614
|
+
p2.intro(pc3.bgYellow(pc3.black(" pull ")));
|
|
615
|
+
let fullPath;
|
|
616
|
+
if (options.config) {
|
|
617
|
+
fullPath = isAbsolute2(options.config) ? options.config : resolve2(process.cwd(), options.config);
|
|
618
|
+
} else {
|
|
619
|
+
const resolved = resolveConfigPath();
|
|
620
|
+
if (!resolved) {
|
|
621
|
+
p2.log.error(
|
|
622
|
+
pc3.red("No configuration file found. Run 'owostack init' first.")
|
|
623
|
+
);
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
fullPath = resolved;
|
|
627
|
+
}
|
|
501
628
|
const apiKey = getApiKey(options.key);
|
|
502
629
|
const configSettings = await loadConfigSettings(options.config);
|
|
503
630
|
const baseUrl = getApiUrl(configSettings.apiUrl);
|
|
504
631
|
const filters = configSettings.filters || {};
|
|
632
|
+
const format = determineFormat(fullPath);
|
|
505
633
|
const s = p2.spinner();
|
|
506
634
|
if (options.prod) {
|
|
507
|
-
p2.log.step(
|
|
635
|
+
p2.log.step(pc3.magenta("Production Mode: Fetching both environments"));
|
|
508
636
|
const testUrl = getTestApiUrl(configSettings.environments?.test);
|
|
509
637
|
const liveUrl = getApiUrl(configSettings.environments?.live);
|
|
510
|
-
s.start(`Fetching from ${
|
|
638
|
+
s.start(`Fetching from ${pc3.dim("test")}...`);
|
|
511
639
|
const testPlans = await fetchPlans({
|
|
512
640
|
apiKey,
|
|
513
641
|
apiUrl: `${testUrl}/api/v1`,
|
|
514
642
|
...filters
|
|
515
643
|
});
|
|
516
644
|
s.stop(`Fetched ${testPlans.length} plans from test`);
|
|
517
|
-
s.start(`Fetching from ${
|
|
645
|
+
s.start(`Fetching from ${pc3.dim("live")}...`);
|
|
518
646
|
const livePlans = await fetchPlans({
|
|
519
647
|
apiKey,
|
|
520
648
|
apiUrl: `${liveUrl}/api/v1`,
|
|
@@ -531,22 +659,23 @@ async function runPull(options) {
|
|
|
531
659
|
const configContent = generateConfig(
|
|
532
660
|
livePlans,
|
|
533
661
|
creditSystems,
|
|
534
|
-
defaultProvider
|
|
662
|
+
defaultProvider,
|
|
663
|
+
format
|
|
535
664
|
);
|
|
536
665
|
if (options.dryRun) {
|
|
537
666
|
p2.note(configContent, "Generated Config (Dry Run)");
|
|
538
|
-
p2.outro(
|
|
667
|
+
p2.outro(pc3.yellow("Dry run complete. No changes made."));
|
|
539
668
|
return;
|
|
540
669
|
}
|
|
541
670
|
if (existsSync3(fullPath) && !options.force) {
|
|
542
|
-
p2.log.error(
|
|
543
|
-
p2.log.info(
|
|
671
|
+
p2.log.error(pc3.red(`Config file already exists at ${fullPath}`));
|
|
672
|
+
p2.log.info(pc3.dim("Use --force to overwrite."));
|
|
544
673
|
process.exit(1);
|
|
545
674
|
}
|
|
546
675
|
await writeFile2(fullPath, configContent, "utf8");
|
|
547
|
-
p2.log.success(
|
|
676
|
+
p2.log.success(pc3.green(`Wrote configuration to ${fullPath}`));
|
|
548
677
|
} else {
|
|
549
|
-
s.start(`Fetching plans from ${
|
|
678
|
+
s.start(`Fetching plans from ${pc3.dim(baseUrl)}...`);
|
|
550
679
|
const plans = await fetchPlans({
|
|
551
680
|
apiKey,
|
|
552
681
|
apiUrl: `${baseUrl}/api/v1`,
|
|
@@ -560,10 +689,15 @@ async function runPull(options) {
|
|
|
560
689
|
plans.map((p9) => p9.provider).filter(Boolean)
|
|
561
690
|
);
|
|
562
691
|
const defaultProvider = providers.size === 1 ? Array.from(providers)[0] : void 0;
|
|
563
|
-
const configContent = generateConfig(
|
|
692
|
+
const configContent = generateConfig(
|
|
693
|
+
plans,
|
|
694
|
+
creditSystems,
|
|
695
|
+
defaultProvider,
|
|
696
|
+
format
|
|
697
|
+
);
|
|
564
698
|
if (options.dryRun) {
|
|
565
699
|
p2.note(configContent, "Generated Config (Dry Run)");
|
|
566
|
-
p2.outro(
|
|
700
|
+
p2.outro(pc3.yellow("Dry run complete. No changes made."));
|
|
567
701
|
return;
|
|
568
702
|
}
|
|
569
703
|
if (existsSync3(fullPath) && !options.force) {
|
|
@@ -572,22 +706,22 @@ async function runPull(options) {
|
|
|
572
706
|
initialValue: false
|
|
573
707
|
});
|
|
574
708
|
if (p2.isCancel(confirm3) || !confirm3) {
|
|
575
|
-
p2.outro(
|
|
709
|
+
p2.outro(pc3.yellow("Operation cancelled"));
|
|
576
710
|
process.exit(0);
|
|
577
711
|
}
|
|
578
712
|
}
|
|
579
713
|
await writeFile2(fullPath, configContent, "utf8");
|
|
580
|
-
p2.log.success(
|
|
714
|
+
p2.log.success(pc3.green(`Wrote configuration to ${fullPath}`));
|
|
581
715
|
}
|
|
582
|
-
p2.outro(
|
|
716
|
+
p2.outro(pc3.green("Pull complete! \u2728"));
|
|
583
717
|
}
|
|
584
718
|
|
|
585
719
|
// src/commands/diff.ts
|
|
586
720
|
import * as p4 from "@clack/prompts";
|
|
587
|
-
import
|
|
721
|
+
import pc5 from "picocolors";
|
|
588
722
|
|
|
589
723
|
// src/lib/diff.ts
|
|
590
|
-
import
|
|
724
|
+
import pc4 from "picocolors";
|
|
591
725
|
import * as p3 from "@clack/prompts";
|
|
592
726
|
function normalizeFeature(pf) {
|
|
593
727
|
return {
|
|
@@ -647,7 +781,7 @@ function diffPlans(localPlans, remotePlans) {
|
|
|
647
781
|
const localVal = JSON.stringify(local[field]);
|
|
648
782
|
const remoteVal = JSON.stringify(remote[field]);
|
|
649
783
|
details.push(
|
|
650
|
-
`${String(field)}: ${
|
|
784
|
+
`${String(field)}: ${pc4.green(localVal)} \u2192 ${pc4.red(remoteVal)}`
|
|
651
785
|
);
|
|
652
786
|
}
|
|
653
787
|
}
|
|
@@ -657,20 +791,20 @@ function diffPlans(localPlans, remotePlans) {
|
|
|
657
791
|
);
|
|
658
792
|
for (const fslug of localFeatures.keys()) {
|
|
659
793
|
if (!remoteFeatures.has(fslug)) {
|
|
660
|
-
details.push(`feature ${fslug}: ${
|
|
794
|
+
details.push(`feature ${fslug}: ${pc4.green("[local only]")}`);
|
|
661
795
|
continue;
|
|
662
796
|
}
|
|
663
797
|
const lf = localFeatures.get(fslug);
|
|
664
798
|
const rf = remoteFeatures.get(fslug);
|
|
665
799
|
if (JSON.stringify(lf) !== JSON.stringify(rf)) {
|
|
666
800
|
details.push(`feature ${fslug}:`);
|
|
667
|
-
details.push(` ${
|
|
668
|
-
details.push(` ${
|
|
801
|
+
details.push(` ${pc4.green(JSON.stringify(lf))}`);
|
|
802
|
+
details.push(` ${pc4.red(JSON.stringify(rf))}`);
|
|
669
803
|
}
|
|
670
804
|
}
|
|
671
805
|
for (const fslug of remoteFeatures.keys()) {
|
|
672
806
|
if (!localFeatures.has(fslug)) {
|
|
673
|
-
details.push(`feature ${fslug}: ${
|
|
807
|
+
details.push(`feature ${fslug}: ${pc4.red("[remote only]")}`);
|
|
674
808
|
}
|
|
675
809
|
}
|
|
676
810
|
if (details.length > 0) {
|
|
@@ -681,18 +815,18 @@ function diffPlans(localPlans, remotePlans) {
|
|
|
681
815
|
}
|
|
682
816
|
function printDiff(diff) {
|
|
683
817
|
if (diff.onlyLocal.length === 0 && diff.onlyRemote.length === 0 && diff.changed.length === 0) {
|
|
684
|
-
p3.log.success(
|
|
818
|
+
p3.log.success(pc4.green("No differences found."));
|
|
685
819
|
return;
|
|
686
820
|
}
|
|
687
821
|
if (diff.onlyLocal.length > 0) {
|
|
688
822
|
p3.note(
|
|
689
|
-
diff.onlyLocal.map((slug) => `${
|
|
823
|
+
diff.onlyLocal.map((slug) => `${pc4.green("+")} ${slug}`).join("\n"),
|
|
690
824
|
"Only in Local"
|
|
691
825
|
);
|
|
692
826
|
}
|
|
693
827
|
if (diff.onlyRemote.length > 0) {
|
|
694
828
|
p3.note(
|
|
695
|
-
diff.onlyRemote.map((slug) => `${
|
|
829
|
+
diff.onlyRemote.map((slug) => `${pc4.red("-")} ${slug}`).join("\n"),
|
|
696
830
|
"Only in Remote"
|
|
697
831
|
);
|
|
698
832
|
}
|
|
@@ -700,7 +834,7 @@ function printDiff(diff) {
|
|
|
700
834
|
let changedText = "";
|
|
701
835
|
for (const item of diff.changed) {
|
|
702
836
|
changedText += `
|
|
703
|
-
${
|
|
837
|
+
${pc4.bold(item.slug)}
|
|
704
838
|
`;
|
|
705
839
|
for (const line of item.details) {
|
|
706
840
|
changedText += ` ${line}
|
|
@@ -710,47 +844,75 @@ ${pc3.bold(item.slug)}
|
|
|
710
844
|
p3.note(changedText.trim(), "Changed Plans");
|
|
711
845
|
}
|
|
712
846
|
const summary = [
|
|
713
|
-
diff.onlyLocal.length > 0 ? `${
|
|
714
|
-
diff.onlyRemote.length > 0 ? `${
|
|
715
|
-
diff.changed.length > 0 ? `${
|
|
847
|
+
diff.onlyLocal.length > 0 ? `${pc4.green(diff.onlyLocal.length.toString())} added` : "",
|
|
848
|
+
diff.onlyRemote.length > 0 ? `${pc4.red(diff.onlyRemote.length.toString())} removed` : "",
|
|
849
|
+
diff.changed.length > 0 ? `${pc4.yellow(diff.changed.length.toString())} changed` : ""
|
|
716
850
|
].filter(Boolean).join(" ");
|
|
717
851
|
p3.log.info(summary);
|
|
718
852
|
}
|
|
719
853
|
|
|
720
854
|
// src/commands/diff.ts
|
|
721
855
|
async function runDiff(options) {
|
|
722
|
-
p4.intro(
|
|
856
|
+
p4.intro(pc5.bgYellow(pc5.black(" diff ")));
|
|
723
857
|
const fullPath = resolveConfigPath(options.config);
|
|
858
|
+
if (!fullPath) {
|
|
859
|
+
p4.log.error(pc5.red("No configuration file found."));
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
724
862
|
const apiKey = getApiKey(options.key);
|
|
725
863
|
const configSettings = await loadConfigSettings(options.config);
|
|
726
864
|
const baseUrl = getApiUrl(configSettings.apiUrl);
|
|
727
865
|
const s = p4.spinner();
|
|
728
866
|
if (options.prod) {
|
|
729
|
-
p4.log.step(
|
|
867
|
+
p4.log.step(pc5.magenta("Production Mode: Comparing both environments"));
|
|
730
868
|
const testUrl = getTestApiUrl(configSettings.environments?.test);
|
|
731
869
|
const liveUrl = getApiUrl(configSettings.environments?.live);
|
|
732
870
|
s.start("Loading local configuration...");
|
|
733
|
-
|
|
871
|
+
let owo;
|
|
872
|
+
try {
|
|
873
|
+
owo = await loadOwostackFromConfig(fullPath);
|
|
874
|
+
} catch (e) {
|
|
875
|
+
s.stop(pc5.red("Failed to load configuration"));
|
|
876
|
+
p4.log.error(pc5.red(`Error: ${e.message}`));
|
|
877
|
+
p4.log.info(
|
|
878
|
+
pc5.dim(
|
|
879
|
+
"Make sure 'owostack' is installed in your project: 'npm install owostack'"
|
|
880
|
+
)
|
|
881
|
+
);
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
734
884
|
s.stop("Configuration loaded");
|
|
735
885
|
const { buildSyncPayload } = await import("owostack").catch(() => ({
|
|
736
886
|
buildSyncPayload: null
|
|
737
887
|
}));
|
|
738
888
|
const localPayload = buildSyncPayload(owo._config.catalog);
|
|
739
|
-
p4.log.step(
|
|
889
|
+
p4.log.step(pc5.cyan(`Comparing with TEST: ${testUrl}`));
|
|
740
890
|
const testPlans = await fetchPlans({ apiKey, apiUrl: `${testUrl}/api/v1` });
|
|
741
891
|
printDiff(diffPlans(localPayload?.plans ?? [], testPlans));
|
|
742
|
-
p4.log.step(
|
|
892
|
+
p4.log.step(pc5.cyan(`Comparing with LIVE: ${liveUrl}`));
|
|
743
893
|
const livePlans = await fetchPlans({ apiKey, apiUrl: `${liveUrl}/api/v1` });
|
|
744
894
|
printDiff(diffPlans(localPayload?.plans ?? [], livePlans));
|
|
745
895
|
} else {
|
|
746
896
|
s.start("Loading local configuration...");
|
|
747
|
-
|
|
897
|
+
let owo;
|
|
898
|
+
try {
|
|
899
|
+
owo = await loadOwostackFromConfig(fullPath);
|
|
900
|
+
} catch (e) {
|
|
901
|
+
s.stop(pc5.red("Failed to load configuration"));
|
|
902
|
+
p4.log.error(pc5.red(`Error: ${e.message}`));
|
|
903
|
+
p4.log.info(
|
|
904
|
+
pc5.dim(
|
|
905
|
+
"Make sure 'owostack' is installed in your project: 'npm install owostack'"
|
|
906
|
+
)
|
|
907
|
+
);
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
748
910
|
s.stop("Configuration loaded");
|
|
749
911
|
const { buildSyncPayload } = await import("owostack").catch(() => ({
|
|
750
912
|
buildSyncPayload: null
|
|
751
913
|
}));
|
|
752
914
|
const localPayload = buildSyncPayload(owo._config.catalog);
|
|
753
|
-
s.start(`Fetching remote plans from ${
|
|
915
|
+
s.start(`Fetching remote plans from ${pc5.dim(baseUrl)}...`);
|
|
754
916
|
const remotePlans = await fetchPlans({
|
|
755
917
|
apiKey,
|
|
756
918
|
apiUrl: `${baseUrl}/api/v1`
|
|
@@ -758,34 +920,44 @@ async function runDiff(options) {
|
|
|
758
920
|
s.stop("Remote plans fetched");
|
|
759
921
|
printDiff(diffPlans(localPayload?.plans ?? [], remotePlans));
|
|
760
922
|
}
|
|
761
|
-
p4.outro(
|
|
923
|
+
p4.outro(pc5.green("Diff complete \u2728"));
|
|
762
924
|
}
|
|
763
925
|
|
|
764
926
|
// src/commands/init.ts
|
|
765
927
|
import * as p6 from "@clack/prompts";
|
|
766
|
-
import
|
|
767
|
-
import { existsSync as existsSync4 } from "fs";
|
|
928
|
+
import pc7 from "picocolors";
|
|
929
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
768
930
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
931
|
+
import { join as join3, resolve as resolve3, isAbsolute as isAbsolute3, extname as extname3 } from "path";
|
|
769
932
|
|
|
770
933
|
// src/lib/connect.ts
|
|
771
934
|
import * as p5 from "@clack/prompts";
|
|
772
|
-
import
|
|
935
|
+
import pc6 from "picocolors";
|
|
773
936
|
async function initiateDeviceFlow(options) {
|
|
774
937
|
const url = `${options.apiUrl}/api/auth/cli/device`;
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
938
|
+
try {
|
|
939
|
+
const response = await fetch(url, {
|
|
940
|
+
method: "POST",
|
|
941
|
+
headers: { "Content-Type": "application/json" }
|
|
942
|
+
});
|
|
943
|
+
const data = await response.json();
|
|
944
|
+
if (!response.ok || !data?.success) {
|
|
945
|
+
const message = data?.error || data?.message || "Failed to initiate device flow";
|
|
946
|
+
throw new Error(message);
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
deviceCode: data.deviceCode,
|
|
950
|
+
userCode: data.userCode,
|
|
951
|
+
expiresIn: data.expiresIn || 300
|
|
952
|
+
};
|
|
953
|
+
} catch (error) {
|
|
954
|
+
if (error.name === "TypeError" && error.message.includes("fetch failed")) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`Could not reach the API at ${options.apiUrl}. Please check your internet connection or ensure the API is running.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
throw error;
|
|
783
960
|
}
|
|
784
|
-
return {
|
|
785
|
-
deviceCode: data.deviceCode,
|
|
786
|
-
userCode: data.userCode,
|
|
787
|
-
expiresIn: data.expiresIn || 300
|
|
788
|
-
};
|
|
789
961
|
}
|
|
790
962
|
async function pollForToken(deviceCode, options, s) {
|
|
791
963
|
const startTime = Date.now();
|
|
@@ -793,22 +965,29 @@ async function pollForToken(deviceCode, options, s) {
|
|
|
793
965
|
const pollInterval = 3e3;
|
|
794
966
|
while (Date.now() - startTime < timeoutMs) {
|
|
795
967
|
const url = `${options.apiUrl}/api/auth/cli/token?deviceCode=${deviceCode}`;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
968
|
+
try {
|
|
969
|
+
const response = await fetch(url, { method: "GET" });
|
|
970
|
+
const data = await response.json();
|
|
971
|
+
if (data?.success && data?.apiKey) {
|
|
972
|
+
return {
|
|
973
|
+
success: true,
|
|
974
|
+
apiKey: data.apiKey,
|
|
975
|
+
organizationId: data.organizationId
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
if (data?.error === "expired") {
|
|
979
|
+
throw new Error("Device code expired. Please try again.");
|
|
980
|
+
}
|
|
981
|
+
if (data?.error === "denied") {
|
|
982
|
+
throw new Error("Connection was denied by user.");
|
|
983
|
+
}
|
|
984
|
+
} catch (error) {
|
|
985
|
+
if (error.name === "TypeError" && error.message.includes("fetch failed")) {
|
|
986
|
+
} else {
|
|
987
|
+
throw error;
|
|
988
|
+
}
|
|
810
989
|
}
|
|
811
|
-
await new Promise((
|
|
990
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollInterval));
|
|
812
991
|
}
|
|
813
992
|
throw new Error("Connection timed out. Please try again.");
|
|
814
993
|
}
|
|
@@ -816,8 +995,8 @@ async function executeConnectFlow(options) {
|
|
|
816
995
|
try {
|
|
817
996
|
const deviceCode = await initiateDeviceFlow(options);
|
|
818
997
|
const authUrl = `${options.dashboardUrl}/cli/connect?code=${deviceCode.userCode}`;
|
|
819
|
-
p5.log.step(
|
|
820
|
-
p5.log.message(`${
|
|
998
|
+
p5.log.step(pc6.bold("Connect to your dashboard:"));
|
|
999
|
+
p5.log.message(`${pc6.cyan(pc6.underline(authUrl))}
|
|
821
1000
|
`);
|
|
822
1001
|
const shouldOpen = !options.noBrowser;
|
|
823
1002
|
if (shouldOpen) {
|
|
@@ -831,10 +1010,10 @@ async function executeConnectFlow(options) {
|
|
|
831
1010
|
}
|
|
832
1011
|
const s = p5.spinner();
|
|
833
1012
|
s.start(
|
|
834
|
-
`Waiting for you to approve in the dashboard (Code: ${
|
|
1013
|
+
`Waiting for you to approve in the dashboard (Code: ${pc6.bold(pc6.yellow(deviceCode.userCode))})`
|
|
835
1014
|
);
|
|
836
1015
|
const result = await pollForToken(deviceCode.deviceCode, options, s);
|
|
837
|
-
s.stop(
|
|
1016
|
+
s.stop(pc6.green("Authorization granted"));
|
|
838
1017
|
if (result.success && result.apiKey) {
|
|
839
1018
|
await saveGlobalConfig({
|
|
840
1019
|
apiKey: result.apiKey,
|
|
@@ -843,19 +1022,40 @@ async function executeConnectFlow(options) {
|
|
|
843
1022
|
return result.apiKey;
|
|
844
1023
|
}
|
|
845
1024
|
} catch (e) {
|
|
846
|
-
p5.log.error(
|
|
1025
|
+
p5.log.error(pc6.red(`\u2717 ${e.message}`));
|
|
847
1026
|
}
|
|
848
1027
|
return null;
|
|
849
1028
|
}
|
|
850
1029
|
|
|
851
1030
|
// src/commands/init.ts
|
|
1031
|
+
function getProjectInfo2() {
|
|
1032
|
+
const cwd = process.cwd();
|
|
1033
|
+
const isTs = existsSync4(join3(cwd, "tsconfig.json"));
|
|
1034
|
+
let isEsm = false;
|
|
1035
|
+
try {
|
|
1036
|
+
const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
|
|
1037
|
+
isEsm = pkg.type === "module";
|
|
1038
|
+
} catch {
|
|
1039
|
+
}
|
|
1040
|
+
return { isTs, isEsm };
|
|
1041
|
+
}
|
|
852
1042
|
async function runInit(options) {
|
|
853
|
-
p6.intro(
|
|
854
|
-
|
|
1043
|
+
p6.intro(pc7.bgYellow(pc7.black(" init ")));
|
|
1044
|
+
let targetPath = options.config;
|
|
1045
|
+
if (!targetPath) {
|
|
1046
|
+
const existing = resolveConfigPath();
|
|
1047
|
+
if (existing) {
|
|
1048
|
+
targetPath = existing;
|
|
1049
|
+
} else {
|
|
1050
|
+
const { isTs } = getProjectInfo2();
|
|
1051
|
+
targetPath = isTs ? "owo.config.ts" : "owo.config.js";
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const fullPath = isAbsolute3(targetPath) ? targetPath : resolve3(process.cwd(), targetPath);
|
|
855
1055
|
let apiKey = getApiKey(options.key);
|
|
856
1056
|
if (!apiKey) {
|
|
857
1057
|
p6.log.warn(
|
|
858
|
-
|
|
1058
|
+
pc7.yellow("No API key found. Let's connect your account first.")
|
|
859
1059
|
);
|
|
860
1060
|
apiKey = await executeConnectFlow({
|
|
861
1061
|
apiUrl: getApiUrl(),
|
|
@@ -864,7 +1064,7 @@ async function runInit(options) {
|
|
|
864
1064
|
timeout: 300
|
|
865
1065
|
}) || "";
|
|
866
1066
|
if (!apiKey) {
|
|
867
|
-
p6.log.error(
|
|
1067
|
+
p6.log.error(pc7.red("Could not obtain API key. Initialization aborted."));
|
|
868
1068
|
process.exit(1);
|
|
869
1069
|
}
|
|
870
1070
|
}
|
|
@@ -874,7 +1074,7 @@ async function runInit(options) {
|
|
|
874
1074
|
initialValue: false
|
|
875
1075
|
});
|
|
876
1076
|
if (p6.isCancel(confirm3) || !confirm3) {
|
|
877
|
-
p6.outro(
|
|
1077
|
+
p6.outro(pc7.yellow("Initialization cancelled"));
|
|
878
1078
|
process.exit(0);
|
|
879
1079
|
}
|
|
880
1080
|
}
|
|
@@ -886,22 +1086,40 @@ async function runInit(options) {
|
|
|
886
1086
|
apiKey,
|
|
887
1087
|
`${getApiUrl()}/api/v1`
|
|
888
1088
|
);
|
|
889
|
-
const
|
|
1089
|
+
const ext = extname3(fullPath);
|
|
1090
|
+
const { isEsm } = getProjectInfo2();
|
|
1091
|
+
let format = "ts";
|
|
1092
|
+
if (ext === ".ts" || ext === ".mts" || ext === ".cts") {
|
|
1093
|
+
format = "ts";
|
|
1094
|
+
} else if (ext === ".mjs") {
|
|
1095
|
+
format = "esm";
|
|
1096
|
+
} else if (ext === ".cjs") {
|
|
1097
|
+
format = "cjs";
|
|
1098
|
+
} else if (ext === ".js") {
|
|
1099
|
+
format = isEsm ? "esm" : "cjs";
|
|
1100
|
+
}
|
|
1101
|
+
const configContent = generateConfig(
|
|
1102
|
+
plans,
|
|
1103
|
+
creditSystems,
|
|
1104
|
+
void 0,
|
|
1105
|
+
format
|
|
1106
|
+
);
|
|
890
1107
|
await writeFile3(fullPath, configContent, "utf8");
|
|
891
|
-
s.stop(
|
|
1108
|
+
s.stop(pc7.green("Configuration created"));
|
|
892
1109
|
p6.note(
|
|
893
|
-
`${
|
|
894
|
-
${
|
|
895
|
-
${
|
|
1110
|
+
`${pc7.dim("File:")} ${fullPath}
|
|
1111
|
+
${pc7.dim("Format:")} ${format.toUpperCase()}
|
|
1112
|
+
${pc7.dim("Plans:")} ${plans.length} imported
|
|
1113
|
+
${pc7.dim("Credit Systems:")} ${creditSystems.length}`,
|
|
896
1114
|
"\u2728 Project Initialized"
|
|
897
1115
|
);
|
|
898
1116
|
p6.outro(
|
|
899
|
-
|
|
900
|
-
`Next step: Run ${
|
|
1117
|
+
pc7.cyan(
|
|
1118
|
+
`Next step: Run ${pc7.bold("owostack sync")} to apply your catalog.`
|
|
901
1119
|
)
|
|
902
1120
|
);
|
|
903
1121
|
} catch (e) {
|
|
904
|
-
s.stop(
|
|
1122
|
+
s.stop(pc7.red("Initialization failed"));
|
|
905
1123
|
p6.log.error(e.message);
|
|
906
1124
|
process.exit(1);
|
|
907
1125
|
}
|
|
@@ -909,25 +1127,41 @@ ${pc6.dim("Credit Systems:")} ${creditSystems.length}`,
|
|
|
909
1127
|
|
|
910
1128
|
// src/commands/validate.ts
|
|
911
1129
|
import * as p7 from "@clack/prompts";
|
|
912
|
-
import
|
|
1130
|
+
import pc8 from "picocolors";
|
|
913
1131
|
async function runValidate(options) {
|
|
914
|
-
p7.intro(
|
|
1132
|
+
p7.intro(pc8.bgYellow(pc8.black(" validate ")));
|
|
915
1133
|
const fullPath = resolveConfigPath(options.config);
|
|
1134
|
+
if (!fullPath) {
|
|
1135
|
+
p7.log.error(pc8.red("No configuration file found."));
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
916
1138
|
const s = p7.spinner();
|
|
917
|
-
s.start(`Loading ${
|
|
918
|
-
|
|
1139
|
+
s.start(`Loading ${pc8.cyan(fullPath)}`);
|
|
1140
|
+
let owo;
|
|
1141
|
+
try {
|
|
1142
|
+
owo = await loadOwostackFromConfig(fullPath);
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
s.stop(pc8.red("Failed to load configuration"));
|
|
1145
|
+
p7.log.error(pc8.red(`Error: ${e.message}`));
|
|
1146
|
+
p7.log.info(
|
|
1147
|
+
pc8.dim(
|
|
1148
|
+
"Make sure 'owostack' is installed in your project: 'npm install owostack'"
|
|
1149
|
+
)
|
|
1150
|
+
);
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
}
|
|
919
1153
|
if (!owo || typeof owo.sync !== "function") {
|
|
920
|
-
s.stop(
|
|
1154
|
+
s.stop(pc8.red("Invalid configuration"));
|
|
921
1155
|
p7.log.error("Config file must export an Owostack instance.");
|
|
922
1156
|
process.exit(1);
|
|
923
1157
|
}
|
|
924
1158
|
if (!owo._config?.catalog || owo._config.catalog.length === 0) {
|
|
925
|
-
s.stop(
|
|
1159
|
+
s.stop(pc8.red("No catalog found"));
|
|
926
1160
|
p7.log.error("Config has no catalog to validate.");
|
|
927
1161
|
process.exit(1);
|
|
928
1162
|
}
|
|
929
1163
|
s.stop(
|
|
930
|
-
|
|
1164
|
+
pc8.green(`Configuration loaded (${owo._config.catalog.length} entries)`)
|
|
931
1165
|
);
|
|
932
1166
|
const { buildSyncPayload } = await import("owostack").catch(() => ({
|
|
933
1167
|
buildSyncPayload: null
|
|
@@ -938,18 +1172,18 @@ async function runValidate(options) {
|
|
|
938
1172
|
}
|
|
939
1173
|
try {
|
|
940
1174
|
const payload = buildSyncPayload(owo._config.catalog);
|
|
941
|
-
p7.log.step(
|
|
1175
|
+
p7.log.step(pc8.bold("Features"));
|
|
942
1176
|
for (const f of payload.features) {
|
|
943
|
-
p7.log.message(`${
|
|
1177
|
+
p7.log.message(`${pc8.green("\u2713")} ${f.slug} ${pc8.dim(`(${f.type})`)}`);
|
|
944
1178
|
}
|
|
945
|
-
p7.log.step(
|
|
1179
|
+
p7.log.step(pc8.bold("Plans"));
|
|
946
1180
|
for (const p_obj of payload.plans) {
|
|
947
1181
|
p7.log.message(
|
|
948
|
-
`${
|
|
1182
|
+
`${pc8.green("\u2713")} ${pc8.bold(p_obj.slug)} ${pc8.dim(`${p_obj.currency} ${p_obj.price} / ${p_obj.interval}`)}`
|
|
949
1183
|
);
|
|
950
1184
|
}
|
|
951
1185
|
if (options.prod) {
|
|
952
|
-
p7.log.step(
|
|
1186
|
+
p7.log.step(pc8.magenta("Production Mode Check"));
|
|
953
1187
|
const configSettings = await loadConfigSettings(options.config);
|
|
954
1188
|
const testUrl = getTestApiUrl(configSettings.environments?.test);
|
|
955
1189
|
const liveUrl = getApiUrl(configSettings.environments?.live);
|
|
@@ -977,7 +1211,7 @@ async function runValidate(options) {
|
|
|
977
1211
|
p7.log.error(`LIVE environment check failed: ${e.message}`);
|
|
978
1212
|
}
|
|
979
1213
|
}
|
|
980
|
-
p7.outro(
|
|
1214
|
+
p7.outro(pc8.green("Validation passed! \u2728"));
|
|
981
1215
|
} catch (e) {
|
|
982
1216
|
p7.log.error(`Validation failed: ${e.message}`);
|
|
983
1217
|
process.exit(1);
|
|
@@ -986,16 +1220,16 @@ async function runValidate(options) {
|
|
|
986
1220
|
|
|
987
1221
|
// src/commands/connect.ts
|
|
988
1222
|
import * as p8 from "@clack/prompts";
|
|
989
|
-
import
|
|
1223
|
+
import pc9 from "picocolors";
|
|
990
1224
|
import { existsSync as existsSync5 } from "fs";
|
|
991
1225
|
async function runConnect(options) {
|
|
992
|
-
p8.intro(
|
|
993
|
-
const
|
|
1226
|
+
p8.intro(pc9.bgYellow(pc9.black(" connect ")));
|
|
1227
|
+
const fullPath = resolveConfigPath();
|
|
994
1228
|
let apiUrl = getApiUrl();
|
|
995
1229
|
let dashboardUrl = getDashboardUrl();
|
|
996
1230
|
let noBrowser = options.browser === false;
|
|
997
|
-
if (existsSync5(
|
|
998
|
-
const configSettings = await loadConfigSettings(
|
|
1231
|
+
if (fullPath && existsSync5(fullPath)) {
|
|
1232
|
+
const configSettings = await loadConfigSettings(fullPath);
|
|
999
1233
|
if (configSettings.connect?.dashboardUrl) {
|
|
1000
1234
|
dashboardUrl = getDashboardUrl(configSettings.connect.dashboardUrl);
|
|
1001
1235
|
}
|
|
@@ -1011,26 +1245,26 @@ async function runConnect(options) {
|
|
|
1011
1245
|
});
|
|
1012
1246
|
if (apiKey) {
|
|
1013
1247
|
p8.note(
|
|
1014
|
-
`${
|
|
1015
|
-
${
|
|
1248
|
+
`${pc9.dim("API Key:")} owo_***
|
|
1249
|
+
${pc9.dim("Config:")} ${GLOBAL_CONFIG_PATH}`,
|
|
1016
1250
|
"Connected successfully!"
|
|
1017
1251
|
);
|
|
1018
|
-
p8.outro(
|
|
1252
|
+
p8.outro(pc9.green("Authentication complete \u2728"));
|
|
1019
1253
|
} else {
|
|
1020
|
-
p8.log.error(
|
|
1254
|
+
p8.log.error(pc9.red("Connection failed. Please try again."));
|
|
1021
1255
|
process.exit(1);
|
|
1022
1256
|
}
|
|
1023
1257
|
}
|
|
1024
1258
|
|
|
1025
1259
|
// src/lib/brand.ts
|
|
1026
|
-
import
|
|
1260
|
+
import pc10 from "picocolors";
|
|
1027
1261
|
var OWO_ASCII = `
|
|
1028
|
-
${
|
|
1029
|
-
${
|
|
1030
|
-
${
|
|
1031
|
-
${
|
|
1032
|
-
${
|
|
1033
|
-
${
|
|
1262
|
+
${pc10.yellow("\u2588\u2588\u2588\u2588\u2588\u2588\u2557")} ${pc10.white("\u2588\u2588\u2557 \u2588\u2588\u2557")} ${pc10.yellow("\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
1263
|
+
${pc10.yellow("\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557")} ${pc10.white("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc10.yellow("\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557")}
|
|
1264
|
+
${pc10.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc10.white("\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551")} ${pc10.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")}
|
|
1265
|
+
${pc10.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc10.white("\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551")} ${pc10.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")}
|
|
1266
|
+
${pc10.yellow("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")} ${pc10.white("\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D")} ${pc10.yellow("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
|
|
1267
|
+
${pc10.yellow("\u255A\u2550\u2550\u2550\u2550\u2550\u255D")} ${pc10.white("\u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D")} ${pc10.yellow("\u255A\u2550\u2550\u2550\u2550\u2550\u255D")}
|
|
1034
1268
|
`;
|
|
1035
1269
|
function printBrand() {
|
|
1036
1270
|
console.log(OWO_ASCII);
|
|
@@ -1040,10 +1274,10 @@ function printBrand() {
|
|
|
1040
1274
|
var program = new Command();
|
|
1041
1275
|
printBrand();
|
|
1042
1276
|
program.name("owostack").description("CLI for Owostack billing infrastructure").version("0.1.0");
|
|
1043
|
-
program.command("sync").description("Push catalog to the API").option("--config <path>", "Path to config file"
|
|
1044
|
-
program.command("pull").description("Pull plans from dashboard into owo.config.ts").option("--config <path>", "Path to config file"
|
|
1045
|
-
program.command("diff").description("Compare local config to dashboard plans").option("--config <path>", "Path to config file"
|
|
1046
|
-
program.command("init").description("Initialize owo.config.ts from dashboard").option("--config <path>", "Path to config file"
|
|
1047
|
-
program.command("validate").description("Validate local config without syncing").option("--config <path>", "Path to config file"
|
|
1277
|
+
program.command("sync").description("Push catalog to the API").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--prod", "Execute in both test and live environments").option("--dry-run", "Show what would change without applying").action(runSync);
|
|
1278
|
+
program.command("pull").description("Pull plans from dashboard into owo.config.ts").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--force", "Overwrite existing config file", false).option("--prod", "Execute in both test and live environments").option("--dry-run", "Show what would change without applying").action(runPull);
|
|
1279
|
+
program.command("diff").description("Compare local config to dashboard plans").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--prod", "Execute in both test and live environments").action(runDiff);
|
|
1280
|
+
program.command("init").description("Initialize owo.config.ts from dashboard").option("--config <path>", "Path to config file").option("--key <api-key>", "API secret key").option("--force", "Overwrite existing config file", false).action(runInit);
|
|
1281
|
+
program.command("validate").description("Validate local config without syncing").option("--config <path>", "Path to config file").option("--prod", "Execute in both test and live environments").action(runValidate);
|
|
1048
1282
|
program.command("connect").description("Connect CLI to dashboard via browser").option("--no-browser", "Don't open the browser automatically").action(runConnect);
|
|
1049
1283
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "owosk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI for Owostack - sync catalog, manage billing infrastructure",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/Abdulmumin1/owostack
|
|
8
|
+
"url": "https://github.com/Abdulmumin1/owostack",
|
|
9
9
|
"directory": "packages/cli"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"dist"
|
|
14
14
|
],
|
|
15
15
|
"bin": {
|
|
16
|
+
"owosk": "./dist/index.js",
|
|
16
17
|
"owostack": "./dist/index.js",
|
|
17
18
|
"owo": "./dist/index.js",
|
|
18
19
|
"owos": "./dist/index.js",
|
|
@@ -23,8 +24,8 @@
|
|
|
23
24
|
"commander": "^14.0.3",
|
|
24
25
|
"jiti": "^2.6.1",
|
|
25
26
|
"picocolors": "^1.1.1",
|
|
26
|
-
"owostack": "0.1.2",
|
|
27
|
-
"
|
|
27
|
+
"@owostack/types": "0.1.2",
|
|
28
|
+
"owostack": "0.1.2"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/node": "^22.19.10",
|