owosk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +82 -0
- package/dist/index.js +1049 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 Owostack
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# owosk
|
|
2
|
+
|
|
3
|
+
The primary tool for managing your Owostack billing configuration from the terminal. Use it to synchronize your local catalog, validate your configuration, and manage your billing infrastructure.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the CLI globally:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g owosk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or use it directly with npx:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx owo --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
### `init`
|
|
22
|
+
|
|
23
|
+
Initialize a new Owostack project with a default `owo.config.ts`.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx owo init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `sync`
|
|
30
|
+
|
|
31
|
+
Push your local catalog configuration to the Owostack cloud.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx owo sync
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `pull`
|
|
38
|
+
|
|
39
|
+
Pull existing plans and features from the cloud into your local configuration.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx owo pull
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `diff`
|
|
46
|
+
|
|
47
|
+
Preview changes by comparing your local configuration with the cloud.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx owo diff
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `validate`
|
|
54
|
+
|
|
55
|
+
Check your local `owo.config.ts` for errors without applying changes.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx owo validate
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `connect`
|
|
62
|
+
|
|
63
|
+
Authenticate and connect your local environment to an organization.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx owo connect
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- **Declarative Catalog**: Manage your billing structure as code.
|
|
72
|
+
- **Idempotent Sync**: Safely push changes without duplicating resources.
|
|
73
|
+
- **Validation**: Catch configuration errors before they hit production.
|
|
74
|
+
- **Cloud Synchronization**: Keep your local and cloud environments in sync.
|
|
75
|
+
|
|
76
|
+
## Documentation
|
|
77
|
+
|
|
78
|
+
For full command references and guides, visit [docs.owostack.com/cli](https://docs.owostack.com/cli).
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
Apache-2.0
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/sync.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/lib/config.ts
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
14
|
+
import { writeFile } from "fs/promises";
|
|
15
|
+
var GLOBAL_CONFIG_DIR = join(homedir(), ".owostack");
|
|
16
|
+
var GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, "config.json");
|
|
17
|
+
function getApiUrl(configUrl) {
|
|
18
|
+
return process.env.OWOSTACK_API_URL || configUrl || "https://api.owostack.com";
|
|
19
|
+
}
|
|
20
|
+
function getTestApiUrl(configUrl) {
|
|
21
|
+
return process.env.OWOSTACK_API_TEST_URL || configUrl || "https://sandbox.owostack.com";
|
|
22
|
+
}
|
|
23
|
+
function getDashboardUrl(configUrl) {
|
|
24
|
+
return process.env.OWOSTACK_DASHBOARD_URL || configUrl || "https://app.owostack.com";
|
|
25
|
+
}
|
|
26
|
+
async function saveGlobalConfig(data) {
|
|
27
|
+
if (!existsSync(GLOBAL_CONFIG_DIR)) {
|
|
28
|
+
mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
await writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2), "utf8");
|
|
31
|
+
}
|
|
32
|
+
function loadGlobalConfig() {
|
|
33
|
+
try {
|
|
34
|
+
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
35
|
+
return JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
function getApiKey(cliKey) {
|
|
42
|
+
if (cliKey) return cliKey;
|
|
43
|
+
if (process.env.OWOSTACK_SECRET_KEY) return process.env.OWOSTACK_SECRET_KEY;
|
|
44
|
+
if (process.env.OWOSTACK_API_KEY) return process.env.OWOSTACK_API_KEY;
|
|
45
|
+
return loadGlobalConfig().apiKey || "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/lib/loader.ts
|
|
49
|
+
import { resolve, isAbsolute } from "path";
|
|
50
|
+
import { pathToFileURL } from "url";
|
|
51
|
+
import { existsSync as existsSync2 } from "fs";
|
|
52
|
+
function resolveConfigPath(configPath) {
|
|
53
|
+
return isAbsolute(configPath) ? configPath : resolve(process.cwd(), configPath);
|
|
54
|
+
}
|
|
55
|
+
async function loadOwostackFromConfig(fullPath) {
|
|
56
|
+
let configModule;
|
|
57
|
+
try {
|
|
58
|
+
const fileUrl = pathToFileURL(fullPath).href;
|
|
59
|
+
configModule = await import(fileUrl);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(`
|
|
62
|
+
\u274C Failed to load config from ${fullPath}`);
|
|
63
|
+
console.error(` ${e.message}
|
|
64
|
+
`);
|
|
65
|
+
console.error(
|
|
66
|
+
` Make sure the file exports an Owostack instance as default or named 'owo'.`
|
|
67
|
+
);
|
|
68
|
+
console.error(` Example owo.config.ts:
|
|
69
|
+
`);
|
|
70
|
+
console.error(
|
|
71
|
+
` import { Owostack, metered, boolean, plan } from "owostack";`
|
|
72
|
+
);
|
|
73
|
+
console.error(
|
|
74
|
+
` export default new Owostack({ secretKey: "...", catalog: [...] });
|
|
75
|
+
`
|
|
76
|
+
);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
return configModule.default || configModule.owo;
|
|
80
|
+
}
|
|
81
|
+
async function loadConfigSettings(configPath) {
|
|
82
|
+
try {
|
|
83
|
+
const fullPath = resolveConfigPath(configPath);
|
|
84
|
+
if (!existsSync2(fullPath)) return {};
|
|
85
|
+
const owo = await loadOwostackFromConfig(fullPath);
|
|
86
|
+
if (!owo || !owo._config) return {};
|
|
87
|
+
return {
|
|
88
|
+
apiUrl: owo._config.apiUrl,
|
|
89
|
+
environments: owo._config.environments,
|
|
90
|
+
filters: owo._config.filters,
|
|
91
|
+
connect: owo._config.connect
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/commands/sync.ts
|
|
99
|
+
async function runSyncSingle(options) {
|
|
100
|
+
const { configPath, dryRun, apiUrl } = options;
|
|
101
|
+
const apiKey = getApiKey(options.apiKey);
|
|
102
|
+
const fullPath = resolveConfigPath(configPath);
|
|
103
|
+
const s = p.spinner();
|
|
104
|
+
s.start(`Loading ${pc.cyan(configPath)}`);
|
|
105
|
+
const owo = await loadOwostackFromConfig(fullPath);
|
|
106
|
+
if (!owo || typeof owo.sync !== "function") {
|
|
107
|
+
s.stop(pc.red("Invalid configuration"));
|
|
108
|
+
p.log.error("Config file must export an Owostack instance.");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
s.message("Syncing with API...");
|
|
112
|
+
if (apiKey && typeof owo.setSecretKey === "function") {
|
|
113
|
+
owo.setSecretKey(apiKey);
|
|
114
|
+
}
|
|
115
|
+
if (apiUrl && typeof owo.setApiUrl === "function") {
|
|
116
|
+
owo.setApiUrl(apiUrl);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
if (dryRun) {
|
|
120
|
+
s.stop(pc.yellow("Dry run mode (showing catalog payload)"));
|
|
121
|
+
const { buildSyncPayload } = await import("owostack").catch(() => {
|
|
122
|
+
return { buildSyncPayload: null };
|
|
123
|
+
});
|
|
124
|
+
if (buildSyncPayload && owo._config?.catalog) {
|
|
125
|
+
const payload = buildSyncPayload(owo._config.catalog);
|
|
126
|
+
let featureSummary = payload.features.map(
|
|
127
|
+
(f) => `${pc.green("+")} ${pc.bold(f.slug)} ${pc.dim(`(${f.type})`)}`
|
|
128
|
+
).join("\n");
|
|
129
|
+
p.note(
|
|
130
|
+
featureSummary || pc.dim("No features defined"),
|
|
131
|
+
"Features to Sync"
|
|
132
|
+
);
|
|
133
|
+
let planSummary = "";
|
|
134
|
+
for (const p_obj of payload.plans) {
|
|
135
|
+
planSummary += `${pc.green("+")} ${pc.bold(p_obj.slug)} ${pc.dim(`${p_obj.currency} ${p_obj.price}/${p_obj.interval}`)}
|
|
136
|
+
`;
|
|
137
|
+
for (const f of p_obj.features) {
|
|
138
|
+
const status = f.enabled ? pc.green("\u2713") : pc.red("\u2717");
|
|
139
|
+
const configParts = [];
|
|
140
|
+
if (f.limit !== void 0)
|
|
141
|
+
configParts.push(
|
|
142
|
+
`limit: ${f.limit === null ? "unlimited" : f.limit}`
|
|
143
|
+
);
|
|
144
|
+
if (f.reset) configParts.push(`reset: ${f.reset}`);
|
|
145
|
+
if (f.overage) configParts.push(`overage: ${f.overage}`);
|
|
146
|
+
if (f.overagePrice) configParts.push(`price: ${f.overagePrice}`);
|
|
147
|
+
const configStr = configParts.length > 0 ? ` ${pc.dim(`(${configParts.join(", ")})`)}` : "";
|
|
148
|
+
planSummary += ` ${status} ${pc.dim(f.slug)}${configStr}
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
planSummary += "\n";
|
|
152
|
+
}
|
|
153
|
+
p.note(
|
|
154
|
+
planSummary.trim() || pc.dim("No plans defined"),
|
|
155
|
+
"Plans to Sync"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
p.log.info(pc.yellow("No changes were applied to the server."));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const result = await owo.sync();
|
|
162
|
+
s.stop(pc.green("Sync completed"));
|
|
163
|
+
if (!result.success) {
|
|
164
|
+
p.log.error(pc.red("Sync failed"));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const fc = result.features;
|
|
168
|
+
const pc_res = result.plans;
|
|
169
|
+
let summary = [];
|
|
170
|
+
if (fc.created.length)
|
|
171
|
+
summary.push(pc.green(`+ ${fc.created.length} features`));
|
|
172
|
+
if (fc.updated.length)
|
|
173
|
+
summary.push(pc.cyan(`~ ${fc.updated.length} features`));
|
|
174
|
+
if (pc_res.created.length)
|
|
175
|
+
summary.push(pc.green(`+ ${pc_res.created.length} plans`));
|
|
176
|
+
if (pc_res.updated.length)
|
|
177
|
+
summary.push(pc.cyan(`~ ${pc_res.updated.length} plans`));
|
|
178
|
+
if (summary.length > 0) {
|
|
179
|
+
p.note(summary.join("\n"), "Changes applied");
|
|
180
|
+
} else {
|
|
181
|
+
p.log.info(pc.dim("No changes detected. Catalog is up to date."));
|
|
182
|
+
}
|
|
183
|
+
if (result.warnings.length) {
|
|
184
|
+
p.log.warn(pc.yellow(`Warnings:
|
|
185
|
+
${result.warnings.join("\n")}`));
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
s.stop(pc.red("Sync failed"));
|
|
189
|
+
p.log.error(e.message);
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function runSync(options) {
|
|
194
|
+
p.intro(pc.bgYellow(pc.black(" sync ")));
|
|
195
|
+
const configSettings = await loadConfigSettings(options.config);
|
|
196
|
+
let apiUrl = configSettings.apiUrl;
|
|
197
|
+
const testUrl = getTestApiUrl(configSettings.environments?.test);
|
|
198
|
+
const liveUrl = getApiUrl(configSettings.environments?.live);
|
|
199
|
+
if (options.prod) {
|
|
200
|
+
p.log.step(pc.magenta("Production Mode: Syncing both environments"));
|
|
201
|
+
await runSyncSingle({
|
|
202
|
+
configPath: options.config,
|
|
203
|
+
dryRun: !!options.dryRun,
|
|
204
|
+
apiKey: options.key || "",
|
|
205
|
+
apiUrl: apiUrl || `${testUrl}/api/v1`
|
|
206
|
+
});
|
|
207
|
+
await runSyncSingle({
|
|
208
|
+
configPath: options.config,
|
|
209
|
+
dryRun: !!options.dryRun,
|
|
210
|
+
apiKey: options.key || "",
|
|
211
|
+
apiUrl: apiUrl || `${liveUrl}/api/v1`
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
await runSyncSingle({
|
|
215
|
+
configPath: options.config,
|
|
216
|
+
dryRun: !!options.dryRun,
|
|
217
|
+
apiKey: options.key || "",
|
|
218
|
+
apiUrl: apiUrl || `${liveUrl}/api/v1`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
p.outro(pc.green("Done! \u2728"));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/commands/pull.ts
|
|
225
|
+
import * as p2 from "@clack/prompts";
|
|
226
|
+
import pc2 from "picocolors";
|
|
227
|
+
import { existsSync as existsSync3 } from "fs";
|
|
228
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
229
|
+
|
|
230
|
+
// src/lib/api.ts
|
|
231
|
+
async function fetchPlans(options) {
|
|
232
|
+
if (!options.apiKey) {
|
|
233
|
+
console.error(
|
|
234
|
+
`
|
|
235
|
+
\u274C Missing API key. Pass --key or set OWOSTACK_SECRET_KEY.
|
|
236
|
+
`
|
|
237
|
+
);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const url = new URL(`${options.apiUrl}/plans`);
|
|
241
|
+
if (options.group) url.searchParams.set("group", options.group);
|
|
242
|
+
if (options.interval) url.searchParams.set("interval", options.interval);
|
|
243
|
+
if (options.currency) url.searchParams.set("currency", options.currency);
|
|
244
|
+
if (options.includeInactive) url.searchParams.set("includeInactive", "true");
|
|
245
|
+
const response = await fetch(url.toString(), {
|
|
246
|
+
method: "GET",
|
|
247
|
+
headers: { Authorization: `Bearer ${options.apiKey}` }
|
|
248
|
+
});
|
|
249
|
+
const data = await response.json();
|
|
250
|
+
if (!response.ok || !data?.success) {
|
|
251
|
+
const message = data?.error || data?.message || "Request failed";
|
|
252
|
+
console.error(`
|
|
253
|
+
\u274C Failed to fetch plans: ${message}
|
|
254
|
+
`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
return data?.plans || [];
|
|
258
|
+
}
|
|
259
|
+
async function fetchCreditSystems(apiKey, apiUrl) {
|
|
260
|
+
if (!apiKey) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const url = `${apiUrl}/credit-systems`;
|
|
265
|
+
const response = await fetch(url, {
|
|
266
|
+
method: "GET",
|
|
267
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
268
|
+
});
|
|
269
|
+
const data = await response.json();
|
|
270
|
+
if (!response.ok || !data?.success) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
return data?.creditSystems || [];
|
|
274
|
+
} catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/lib/generate.ts
|
|
280
|
+
function slugToIdentifier(slug, used) {
|
|
281
|
+
const parts = slug.replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
282
|
+
let id = parts.map((p9, i) => {
|
|
283
|
+
const lower = p9.toLowerCase();
|
|
284
|
+
return i === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
285
|
+
}).join("");
|
|
286
|
+
if (!id) id = "feature";
|
|
287
|
+
if (/^\d/.test(id)) id = `feature${id}`;
|
|
288
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
289
|
+
"break",
|
|
290
|
+
"case",
|
|
291
|
+
"catch",
|
|
292
|
+
"class",
|
|
293
|
+
"const",
|
|
294
|
+
"continue",
|
|
295
|
+
"debugger",
|
|
296
|
+
"default",
|
|
297
|
+
"delete",
|
|
298
|
+
"do",
|
|
299
|
+
"else",
|
|
300
|
+
"export",
|
|
301
|
+
"extends",
|
|
302
|
+
"finally",
|
|
303
|
+
"for",
|
|
304
|
+
"function",
|
|
305
|
+
"if",
|
|
306
|
+
"import",
|
|
307
|
+
"in",
|
|
308
|
+
"instanceof",
|
|
309
|
+
"new",
|
|
310
|
+
"return",
|
|
311
|
+
"super",
|
|
312
|
+
"switch",
|
|
313
|
+
"this",
|
|
314
|
+
"throw",
|
|
315
|
+
"try",
|
|
316
|
+
"typeof",
|
|
317
|
+
"var",
|
|
318
|
+
"void",
|
|
319
|
+
"while",
|
|
320
|
+
"with",
|
|
321
|
+
"yield"
|
|
322
|
+
]);
|
|
323
|
+
if (reserved.has(id)) id = `${id}Feature`;
|
|
324
|
+
let candidate = id;
|
|
325
|
+
let counter = 2;
|
|
326
|
+
while (used.has(candidate)) {
|
|
327
|
+
candidate = `${id}${counter++}`;
|
|
328
|
+
}
|
|
329
|
+
used.add(candidate);
|
|
330
|
+
return candidate;
|
|
331
|
+
}
|
|
332
|
+
function generateConfig(plans, creditSystems = [], defaultProvider) {
|
|
333
|
+
const creditSystemSlugs = new Set(creditSystems.map((cs) => cs.slug));
|
|
334
|
+
const creditSystemBySlug = new Map(creditSystems.map((cs) => [cs.slug, cs]));
|
|
335
|
+
const featuresBySlug = /* @__PURE__ */ new Map();
|
|
336
|
+
for (const plan of plans) {
|
|
337
|
+
for (const f of plan.features || []) {
|
|
338
|
+
if (creditSystemSlugs.has(f.slug)) continue;
|
|
339
|
+
if (!featuresBySlug.has(f.slug)) {
|
|
340
|
+
featuresBySlug.set(f.slug, {
|
|
341
|
+
slug: f.slug,
|
|
342
|
+
name: f.name,
|
|
343
|
+
type: f.type || "metered"
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const cs of creditSystems) {
|
|
349
|
+
for (const f of cs.features || []) {
|
|
350
|
+
if (!featuresBySlug.has(f.feature)) {
|
|
351
|
+
featuresBySlug.set(f.feature, {
|
|
352
|
+
slug: f.feature,
|
|
353
|
+
name: f.feature,
|
|
354
|
+
type: "metered"
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
360
|
+
const featureVars = /* @__PURE__ */ new Map();
|
|
361
|
+
const featureLines = [];
|
|
362
|
+
for (const feature of featuresBySlug.values()) {
|
|
363
|
+
const varName = slugToIdentifier(feature.slug, usedNames);
|
|
364
|
+
featureVars.set(feature.slug, varName);
|
|
365
|
+
const nameArg = feature.name ? `, { name: ${JSON.stringify(feature.name)} }` : "";
|
|
366
|
+
if (feature.type === "boolean") {
|
|
367
|
+
featureLines.push(
|
|
368
|
+
`export const ${varName} = boolean(${JSON.stringify(feature.slug)}${nameArg});`
|
|
369
|
+
);
|
|
370
|
+
} else {
|
|
371
|
+
featureLines.push(
|
|
372
|
+
`export const ${varName} = metered(${JSON.stringify(feature.slug)}${nameArg});`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const creditSystemLines = [];
|
|
377
|
+
const creditSystemVars = /* @__PURE__ */ new Map();
|
|
378
|
+
for (const cs of creditSystems) {
|
|
379
|
+
const varName = slugToIdentifier(cs.slug, usedNames);
|
|
380
|
+
creditSystemVars.set(cs.slug, varName);
|
|
381
|
+
const nameArg = cs.name ? `name: ${JSON.stringify(cs.name)}` : "";
|
|
382
|
+
const descArg = cs.description ? `description: ${JSON.stringify(cs.description)}` : "";
|
|
383
|
+
const featureEntries = (cs.features || []).map((f) => {
|
|
384
|
+
const childVar = featureVars.get(f.feature) || f.feature;
|
|
385
|
+
return `${childVar}(${f.creditCost})`;
|
|
386
|
+
});
|
|
387
|
+
const optsParts = [
|
|
388
|
+
nameArg,
|
|
389
|
+
descArg,
|
|
390
|
+
`features: [${featureEntries.join(", ")}]`
|
|
391
|
+
].filter(Boolean);
|
|
392
|
+
creditSystemLines.push(
|
|
393
|
+
`export const ${varName} = creditSystem(${JSON.stringify(cs.slug)}, { ${optsParts.join(", ")} });`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const planLines = [];
|
|
397
|
+
for (const plan of plans) {
|
|
398
|
+
const configLines = [];
|
|
399
|
+
configLines.push(`name: ${JSON.stringify(plan.name)}`);
|
|
400
|
+
if (plan.description)
|
|
401
|
+
configLines.push(`description: ${JSON.stringify(plan.description)}`);
|
|
402
|
+
configLines.push(`price: ${plan.price}`);
|
|
403
|
+
configLines.push(`currency: ${JSON.stringify(plan.currency)}`);
|
|
404
|
+
configLines.push(`interval: ${JSON.stringify(plan.interval)}`);
|
|
405
|
+
if (plan.planGroup)
|
|
406
|
+
configLines.push(`planGroup: ${JSON.stringify(plan.planGroup)}`);
|
|
407
|
+
if (plan.trialDays && plan.trialDays > 0)
|
|
408
|
+
configLines.push(`trialDays: ${plan.trialDays}`);
|
|
409
|
+
if (plan.provider)
|
|
410
|
+
configLines.push(`provider: ${JSON.stringify(plan.provider)}`);
|
|
411
|
+
const featureEntries = [];
|
|
412
|
+
for (const pf of plan.features || []) {
|
|
413
|
+
if (creditSystemSlugs.has(pf.slug)) {
|
|
414
|
+
const csVar = creditSystemVars.get(pf.slug);
|
|
415
|
+
if (csVar && pf.enabled) {
|
|
416
|
+
const opts = [];
|
|
417
|
+
if (pf.resetInterval || pf.reset)
|
|
418
|
+
opts.push(`reset: "${pf.resetInterval || pf.reset || "monthly"}"`);
|
|
419
|
+
if (pf.overage) opts.push(`overage: "${pf.overage}"`);
|
|
420
|
+
if (opts.length > 0) {
|
|
421
|
+
featureEntries.push(
|
|
422
|
+
`${csVar}.credits(${pf.limit ?? 0}, { ${opts.join(", ")} })`
|
|
423
|
+
);
|
|
424
|
+
} else {
|
|
425
|
+
featureEntries.push(`${csVar}.credits(${pf.limit ?? 0})`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const varName = featureVars.get(pf.slug) || pf.slug;
|
|
431
|
+
const globalFeature = featuresBySlug.get(pf.slug);
|
|
432
|
+
const featureType = globalFeature?.type || pf.type || "metered";
|
|
433
|
+
if (featureType === "boolean") {
|
|
434
|
+
featureEntries.push(
|
|
435
|
+
pf.enabled ? `${varName}.on()` : `${varName}.off()`
|
|
436
|
+
);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (pf.enabled === false) {
|
|
440
|
+
const config2 = { enabled: false };
|
|
441
|
+
if (pf.limit !== void 0) config2.limit = pf.limit;
|
|
442
|
+
if (pf.resetInterval || pf.reset)
|
|
443
|
+
config2.reset = pf.resetInterval || pf.reset;
|
|
444
|
+
if (pf.overage) config2.overage = pf.overage;
|
|
445
|
+
if (pf.overagePrice !== void 0)
|
|
446
|
+
config2.overagePrice = pf.overagePrice;
|
|
447
|
+
featureEntries.push(`${varName}.config(${JSON.stringify(config2)})`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const config = {};
|
|
451
|
+
if (pf.limit !== void 0) config.limit = pf.limit;
|
|
452
|
+
config.reset = pf.resetInterval || pf.reset || "monthly";
|
|
453
|
+
if (pf.overage) config.overage = pf.overage;
|
|
454
|
+
if (pf.overagePrice !== void 0) config.overagePrice = pf.overagePrice;
|
|
455
|
+
const configKeys = Object.keys(config);
|
|
456
|
+
const hasExtras = configKeys.some((k) => k !== "limit");
|
|
457
|
+
if (config.limit === null && !hasExtras) {
|
|
458
|
+
featureEntries.push(`${varName}.unlimited()`);
|
|
459
|
+
} else if (typeof config.limit === "number" && !hasExtras) {
|
|
460
|
+
featureEntries.push(`${varName}.limit(${config.limit})`);
|
|
461
|
+
} else if (typeof config.limit === "number") {
|
|
462
|
+
const { limit, ...rest } = config;
|
|
463
|
+
featureEntries.push(
|
|
464
|
+
`${varName}.limit(${limit}, ${JSON.stringify(rest)})`
|
|
465
|
+
);
|
|
466
|
+
} else {
|
|
467
|
+
featureEntries.push(`${varName}.config(${JSON.stringify(config)})`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
configLines.push(`features: [${featureEntries.join(", ")}]`);
|
|
471
|
+
planLines.push(
|
|
472
|
+
`plan(${JSON.stringify(plan.slug)}, {
|
|
473
|
+
${configLines.join(",\n ")}
|
|
474
|
+
})`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const hasCreditSystems = creditSystemLines.length > 0;
|
|
478
|
+
const providerLine = defaultProvider ? ` provider: ${JSON.stringify(defaultProvider)},
|
|
479
|
+
` : "";
|
|
480
|
+
return [
|
|
481
|
+
`import { Owostack, metered, boolean, creditSystem, plan } from "owostack";`,
|
|
482
|
+
``,
|
|
483
|
+
...featureLines,
|
|
484
|
+
...hasCreditSystems ? ["", ...creditSystemLines] : [],
|
|
485
|
+
``,
|
|
486
|
+
`export const owo = new Owostack({`,
|
|
487
|
+
` secretKey: process.env.OWOSTACK_SECRET_KEY!,`,
|
|
488
|
+
providerLine,
|
|
489
|
+
` catalog: [`,
|
|
490
|
+
` ${planLines.join(",\n ")}`,
|
|
491
|
+
` ],`,
|
|
492
|
+
`});`,
|
|
493
|
+
``
|
|
494
|
+
].join("\n");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/commands/pull.ts
|
|
498
|
+
async function runPull(options) {
|
|
499
|
+
p2.intro(pc2.bgYellow(pc2.black(" pull ")));
|
|
500
|
+
const fullPath = resolveConfigPath(options.config);
|
|
501
|
+
const apiKey = getApiKey(options.key);
|
|
502
|
+
const configSettings = await loadConfigSettings(options.config);
|
|
503
|
+
const baseUrl = getApiUrl(configSettings.apiUrl);
|
|
504
|
+
const filters = configSettings.filters || {};
|
|
505
|
+
const s = p2.spinner();
|
|
506
|
+
if (options.prod) {
|
|
507
|
+
p2.log.step(pc2.magenta("Production Mode: Fetching both environments"));
|
|
508
|
+
const testUrl = getTestApiUrl(configSettings.environments?.test);
|
|
509
|
+
const liveUrl = getApiUrl(configSettings.environments?.live);
|
|
510
|
+
s.start(`Fetching from ${pc2.dim("test")}...`);
|
|
511
|
+
const testPlans = await fetchPlans({
|
|
512
|
+
apiKey,
|
|
513
|
+
apiUrl: `${testUrl}/api/v1`,
|
|
514
|
+
...filters
|
|
515
|
+
});
|
|
516
|
+
s.stop(`Fetched ${testPlans.length} plans from test`);
|
|
517
|
+
s.start(`Fetching from ${pc2.dim("live")}...`);
|
|
518
|
+
const livePlans = await fetchPlans({
|
|
519
|
+
apiKey,
|
|
520
|
+
apiUrl: `${liveUrl}/api/v1`,
|
|
521
|
+
...filters
|
|
522
|
+
});
|
|
523
|
+
s.stop(`Fetched ${livePlans.length} plans from live`);
|
|
524
|
+
s.start(`Fetching credit systems...`);
|
|
525
|
+
const creditSystems = await fetchCreditSystems(apiKey, `${liveUrl}/api/v1`);
|
|
526
|
+
s.stop(`Fetched ${creditSystems.length} credit systems`);
|
|
527
|
+
const providers = new Set(
|
|
528
|
+
livePlans.map((p9) => p9.provider).filter(Boolean)
|
|
529
|
+
);
|
|
530
|
+
const defaultProvider = providers.size === 1 ? Array.from(providers)[0] : void 0;
|
|
531
|
+
const configContent = generateConfig(
|
|
532
|
+
livePlans,
|
|
533
|
+
creditSystems,
|
|
534
|
+
defaultProvider
|
|
535
|
+
);
|
|
536
|
+
if (options.dryRun) {
|
|
537
|
+
p2.note(configContent, "Generated Config (Dry Run)");
|
|
538
|
+
p2.outro(pc2.yellow("Dry run complete. No changes made."));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (existsSync3(fullPath) && !options.force) {
|
|
542
|
+
p2.log.error(pc2.red(`Config file already exists at ${fullPath}`));
|
|
543
|
+
p2.log.info(pc2.dim("Use --force to overwrite."));
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
await writeFile2(fullPath, configContent, "utf8");
|
|
547
|
+
p2.log.success(pc2.green(`Wrote configuration to ${fullPath}`));
|
|
548
|
+
} else {
|
|
549
|
+
s.start(`Fetching plans from ${pc2.dim(baseUrl)}...`);
|
|
550
|
+
const plans = await fetchPlans({
|
|
551
|
+
apiKey,
|
|
552
|
+
apiUrl: `${baseUrl}/api/v1`,
|
|
553
|
+
...filters
|
|
554
|
+
});
|
|
555
|
+
s.stop(`Fetched ${plans.length} plans`);
|
|
556
|
+
s.start(`Fetching credit systems...`);
|
|
557
|
+
const creditSystems = await fetchCreditSystems(apiKey, `${baseUrl}/api/v1`);
|
|
558
|
+
s.stop(`Fetched ${creditSystems.length} credit systems`);
|
|
559
|
+
const providers = new Set(
|
|
560
|
+
plans.map((p9) => p9.provider).filter(Boolean)
|
|
561
|
+
);
|
|
562
|
+
const defaultProvider = providers.size === 1 ? Array.from(providers)[0] : void 0;
|
|
563
|
+
const configContent = generateConfig(plans, creditSystems, defaultProvider);
|
|
564
|
+
if (options.dryRun) {
|
|
565
|
+
p2.note(configContent, "Generated Config (Dry Run)");
|
|
566
|
+
p2.outro(pc2.yellow("Dry run complete. No changes made."));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (existsSync3(fullPath) && !options.force) {
|
|
570
|
+
const confirm3 = await p2.confirm({
|
|
571
|
+
message: `Config file already exists. Overwrite?`,
|
|
572
|
+
initialValue: false
|
|
573
|
+
});
|
|
574
|
+
if (p2.isCancel(confirm3) || !confirm3) {
|
|
575
|
+
p2.outro(pc2.yellow("Operation cancelled"));
|
|
576
|
+
process.exit(0);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
await writeFile2(fullPath, configContent, "utf8");
|
|
580
|
+
p2.log.success(pc2.green(`Wrote configuration to ${fullPath}`));
|
|
581
|
+
}
|
|
582
|
+
p2.outro(pc2.green("Pull complete! \u2728"));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/commands/diff.ts
|
|
586
|
+
import * as p4 from "@clack/prompts";
|
|
587
|
+
import pc4 from "picocolors";
|
|
588
|
+
|
|
589
|
+
// src/lib/diff.ts
|
|
590
|
+
import pc3 from "picocolors";
|
|
591
|
+
import * as p3 from "@clack/prompts";
|
|
592
|
+
function normalizeFeature(pf) {
|
|
593
|
+
return {
|
|
594
|
+
slug: pf.slug,
|
|
595
|
+
enabled: pf.enabled,
|
|
596
|
+
limit: pf.limit ?? null,
|
|
597
|
+
// Handle both SDK 'reset' and API 'resetInterval'
|
|
598
|
+
reset: pf.reset || pf.resetInterval || "monthly",
|
|
599
|
+
// Handle both SDK 'overage' and API 'overage' (same name)
|
|
600
|
+
overage: pf.overage || "block",
|
|
601
|
+
overagePrice: pf.overagePrice ?? null
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function normalizePlan(plan) {
|
|
605
|
+
return {
|
|
606
|
+
slug: plan.slug,
|
|
607
|
+
name: plan.name ?? null,
|
|
608
|
+
description: plan.description ?? null,
|
|
609
|
+
price: plan.price ?? 0,
|
|
610
|
+
currency: plan.currency ?? null,
|
|
611
|
+
interval: plan.interval ?? null,
|
|
612
|
+
planGroup: plan.planGroup ?? null,
|
|
613
|
+
trialDays: plan.trialDays ?? 0,
|
|
614
|
+
features: (plan.features || []).map(normalizeFeature).sort((a, b) => a.slug.localeCompare(b.slug))
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function diffPlans(localPlans, remotePlans) {
|
|
618
|
+
const localMap = /* @__PURE__ */ new Map();
|
|
619
|
+
const remoteMap = /* @__PURE__ */ new Map();
|
|
620
|
+
for (const p9 of localPlans) localMap.set(p9.slug, normalizePlan(p9));
|
|
621
|
+
for (const p9 of remotePlans) remoteMap.set(p9.slug, normalizePlan(p9));
|
|
622
|
+
const onlyLocal = [];
|
|
623
|
+
const onlyRemote = [];
|
|
624
|
+
const changed = [];
|
|
625
|
+
for (const slug of localMap.keys()) {
|
|
626
|
+
if (!remoteMap.has(slug)) onlyLocal.push(slug);
|
|
627
|
+
}
|
|
628
|
+
for (const slug of remoteMap.keys()) {
|
|
629
|
+
if (!localMap.has(slug)) onlyRemote.push(slug);
|
|
630
|
+
}
|
|
631
|
+
for (const slug of localMap.keys()) {
|
|
632
|
+
if (!remoteMap.has(slug)) continue;
|
|
633
|
+
const local = localMap.get(slug);
|
|
634
|
+
const remote = remoteMap.get(slug);
|
|
635
|
+
const details = [];
|
|
636
|
+
const fields = [
|
|
637
|
+
"name",
|
|
638
|
+
"description",
|
|
639
|
+
"price",
|
|
640
|
+
"currency",
|
|
641
|
+
"interval",
|
|
642
|
+
"planGroup",
|
|
643
|
+
"trialDays"
|
|
644
|
+
];
|
|
645
|
+
for (const field of fields) {
|
|
646
|
+
if (local[field] !== remote[field]) {
|
|
647
|
+
const localVal = JSON.stringify(local[field]);
|
|
648
|
+
const remoteVal = JSON.stringify(remote[field]);
|
|
649
|
+
details.push(
|
|
650
|
+
`${String(field)}: ${pc3.green(localVal)} \u2192 ${pc3.red(remoteVal)}`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const localFeatures = new Map(local.features.map((f) => [f.slug, f]));
|
|
655
|
+
const remoteFeatures = new Map(
|
|
656
|
+
remote.features.map((f) => [f.slug, f])
|
|
657
|
+
);
|
|
658
|
+
for (const fslug of localFeatures.keys()) {
|
|
659
|
+
if (!remoteFeatures.has(fslug)) {
|
|
660
|
+
details.push(`feature ${fslug}: ${pc3.green("[local only]")}`);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const lf = localFeatures.get(fslug);
|
|
664
|
+
const rf = remoteFeatures.get(fslug);
|
|
665
|
+
if (JSON.stringify(lf) !== JSON.stringify(rf)) {
|
|
666
|
+
details.push(`feature ${fslug}:`);
|
|
667
|
+
details.push(` ${pc3.green(JSON.stringify(lf))}`);
|
|
668
|
+
details.push(` ${pc3.red(JSON.stringify(rf))}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
for (const fslug of remoteFeatures.keys()) {
|
|
672
|
+
if (!localFeatures.has(fslug)) {
|
|
673
|
+
details.push(`feature ${fslug}: ${pc3.red("[remote only]")}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (details.length > 0) {
|
|
677
|
+
changed.push({ slug, details });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { onlyLocal, onlyRemote, changed };
|
|
681
|
+
}
|
|
682
|
+
function printDiff(diff) {
|
|
683
|
+
if (diff.onlyLocal.length === 0 && diff.onlyRemote.length === 0 && diff.changed.length === 0) {
|
|
684
|
+
p3.log.success(pc3.green("No differences found."));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (diff.onlyLocal.length > 0) {
|
|
688
|
+
p3.note(
|
|
689
|
+
diff.onlyLocal.map((slug) => `${pc3.green("+")} ${slug}`).join("\n"),
|
|
690
|
+
"Only in Local"
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
if (diff.onlyRemote.length > 0) {
|
|
694
|
+
p3.note(
|
|
695
|
+
diff.onlyRemote.map((slug) => `${pc3.red("-")} ${slug}`).join("\n"),
|
|
696
|
+
"Only in Remote"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
if (diff.changed.length > 0) {
|
|
700
|
+
let changedText = "";
|
|
701
|
+
for (const item of diff.changed) {
|
|
702
|
+
changedText += `
|
|
703
|
+
${pc3.bold(item.slug)}
|
|
704
|
+
`;
|
|
705
|
+
for (const line of item.details) {
|
|
706
|
+
changedText += ` ${line}
|
|
707
|
+
`;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
p3.note(changedText.trim(), "Changed Plans");
|
|
711
|
+
}
|
|
712
|
+
const summary = [
|
|
713
|
+
diff.onlyLocal.length > 0 ? `${pc3.green(diff.onlyLocal.length.toString())} added` : "",
|
|
714
|
+
diff.onlyRemote.length > 0 ? `${pc3.red(diff.onlyRemote.length.toString())} removed` : "",
|
|
715
|
+
diff.changed.length > 0 ? `${pc3.yellow(diff.changed.length.toString())} changed` : ""
|
|
716
|
+
].filter(Boolean).join(" ");
|
|
717
|
+
p3.log.info(summary);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/commands/diff.ts
|
|
721
|
+
async function runDiff(options) {
|
|
722
|
+
p4.intro(pc4.bgYellow(pc4.black(" diff ")));
|
|
723
|
+
const fullPath = resolveConfigPath(options.config);
|
|
724
|
+
const apiKey = getApiKey(options.key);
|
|
725
|
+
const configSettings = await loadConfigSettings(options.config);
|
|
726
|
+
const baseUrl = getApiUrl(configSettings.apiUrl);
|
|
727
|
+
const s = p4.spinner();
|
|
728
|
+
if (options.prod) {
|
|
729
|
+
p4.log.step(pc4.magenta("Production Mode: Comparing both environments"));
|
|
730
|
+
const testUrl = getTestApiUrl(configSettings.environments?.test);
|
|
731
|
+
const liveUrl = getApiUrl(configSettings.environments?.live);
|
|
732
|
+
s.start("Loading local configuration...");
|
|
733
|
+
const owo = await loadOwostackFromConfig(fullPath);
|
|
734
|
+
s.stop("Configuration loaded");
|
|
735
|
+
const { buildSyncPayload } = await import("owostack").catch(() => ({
|
|
736
|
+
buildSyncPayload: null
|
|
737
|
+
}));
|
|
738
|
+
const localPayload = buildSyncPayload(owo._config.catalog);
|
|
739
|
+
p4.log.step(pc4.cyan(`Comparing with TEST: ${testUrl}`));
|
|
740
|
+
const testPlans = await fetchPlans({ apiKey, apiUrl: `${testUrl}/api/v1` });
|
|
741
|
+
printDiff(diffPlans(localPayload?.plans ?? [], testPlans));
|
|
742
|
+
p4.log.step(pc4.cyan(`Comparing with LIVE: ${liveUrl}`));
|
|
743
|
+
const livePlans = await fetchPlans({ apiKey, apiUrl: `${liveUrl}/api/v1` });
|
|
744
|
+
printDiff(diffPlans(localPayload?.plans ?? [], livePlans));
|
|
745
|
+
} else {
|
|
746
|
+
s.start("Loading local configuration...");
|
|
747
|
+
const owo = await loadOwostackFromConfig(fullPath);
|
|
748
|
+
s.stop("Configuration loaded");
|
|
749
|
+
const { buildSyncPayload } = await import("owostack").catch(() => ({
|
|
750
|
+
buildSyncPayload: null
|
|
751
|
+
}));
|
|
752
|
+
const localPayload = buildSyncPayload(owo._config.catalog);
|
|
753
|
+
s.start(`Fetching remote plans from ${pc4.dim(baseUrl)}...`);
|
|
754
|
+
const remotePlans = await fetchPlans({
|
|
755
|
+
apiKey,
|
|
756
|
+
apiUrl: `${baseUrl}/api/v1`
|
|
757
|
+
});
|
|
758
|
+
s.stop("Remote plans fetched");
|
|
759
|
+
printDiff(diffPlans(localPayload?.plans ?? [], remotePlans));
|
|
760
|
+
}
|
|
761
|
+
p4.outro(pc4.green("Diff complete \u2728"));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/commands/init.ts
|
|
765
|
+
import * as p6 from "@clack/prompts";
|
|
766
|
+
import pc6 from "picocolors";
|
|
767
|
+
import { existsSync as existsSync4 } from "fs";
|
|
768
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
769
|
+
|
|
770
|
+
// src/lib/connect.ts
|
|
771
|
+
import * as p5 from "@clack/prompts";
|
|
772
|
+
import pc5 from "picocolors";
|
|
773
|
+
async function initiateDeviceFlow(options) {
|
|
774
|
+
const url = `${options.apiUrl}/api/auth/cli/device`;
|
|
775
|
+
const response = await fetch(url, {
|
|
776
|
+
method: "POST",
|
|
777
|
+
headers: { "Content-Type": "application/json" }
|
|
778
|
+
});
|
|
779
|
+
const data = await response.json();
|
|
780
|
+
if (!response.ok || !data?.success) {
|
|
781
|
+
const message = data?.error || data?.message || "Failed to initiate device flow";
|
|
782
|
+
throw new Error(message);
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
deviceCode: data.deviceCode,
|
|
786
|
+
userCode: data.userCode,
|
|
787
|
+
expiresIn: data.expiresIn || 300
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
async function pollForToken(deviceCode, options, s) {
|
|
791
|
+
const startTime = Date.now();
|
|
792
|
+
const timeoutMs = options.timeout * 1e3;
|
|
793
|
+
const pollInterval = 3e3;
|
|
794
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
795
|
+
const url = `${options.apiUrl}/api/auth/cli/token?deviceCode=${deviceCode}`;
|
|
796
|
+
const response = await fetch(url, { method: "GET" });
|
|
797
|
+
const data = await response.json();
|
|
798
|
+
if (data?.success && data?.apiKey) {
|
|
799
|
+
return {
|
|
800
|
+
success: true,
|
|
801
|
+
apiKey: data.apiKey,
|
|
802
|
+
organizationId: data.organizationId
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
if (data?.error === "expired") {
|
|
806
|
+
throw new Error("Device code expired. Please try again.");
|
|
807
|
+
}
|
|
808
|
+
if (data?.error === "denied") {
|
|
809
|
+
throw new Error("Connection was denied by user.");
|
|
810
|
+
}
|
|
811
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
812
|
+
}
|
|
813
|
+
throw new Error("Connection timed out. Please try again.");
|
|
814
|
+
}
|
|
815
|
+
async function executeConnectFlow(options) {
|
|
816
|
+
try {
|
|
817
|
+
const deviceCode = await initiateDeviceFlow(options);
|
|
818
|
+
const authUrl = `${options.dashboardUrl}/cli/connect?code=${deviceCode.userCode}`;
|
|
819
|
+
p5.log.step(pc5.bold("Connect to your dashboard:"));
|
|
820
|
+
p5.log.message(`${pc5.cyan(pc5.underline(authUrl))}
|
|
821
|
+
`);
|
|
822
|
+
const shouldOpen = !options.noBrowser;
|
|
823
|
+
if (shouldOpen) {
|
|
824
|
+
try {
|
|
825
|
+
const { exec } = await import("child_process");
|
|
826
|
+
const platform = process.platform;
|
|
827
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
828
|
+
exec(`${cmd} "${authUrl}"`);
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const s = p5.spinner();
|
|
833
|
+
s.start(
|
|
834
|
+
`Waiting for you to approve in the dashboard (Code: ${pc5.bold(pc5.yellow(deviceCode.userCode))})`
|
|
835
|
+
);
|
|
836
|
+
const result = await pollForToken(deviceCode.deviceCode, options, s);
|
|
837
|
+
s.stop(pc5.green("Authorization granted"));
|
|
838
|
+
if (result.success && result.apiKey) {
|
|
839
|
+
await saveGlobalConfig({
|
|
840
|
+
apiKey: result.apiKey,
|
|
841
|
+
organizationId: result.organizationId
|
|
842
|
+
});
|
|
843
|
+
return result.apiKey;
|
|
844
|
+
}
|
|
845
|
+
} catch (e) {
|
|
846
|
+
p5.log.error(pc5.red(`\u2717 ${e.message}`));
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/commands/init.ts
|
|
852
|
+
async function runInit(options) {
|
|
853
|
+
p6.intro(pc6.bgYellow(pc6.black(" init ")));
|
|
854
|
+
const fullPath = resolveConfigPath(options.config);
|
|
855
|
+
let apiKey = getApiKey(options.key);
|
|
856
|
+
if (!apiKey) {
|
|
857
|
+
p6.log.warn(
|
|
858
|
+
pc6.yellow("No API key found. Let's connect your account first.")
|
|
859
|
+
);
|
|
860
|
+
apiKey = await executeConnectFlow({
|
|
861
|
+
apiUrl: getApiUrl(),
|
|
862
|
+
dashboardUrl: getDashboardUrl(),
|
|
863
|
+
noBrowser: false,
|
|
864
|
+
timeout: 300
|
|
865
|
+
}) || "";
|
|
866
|
+
if (!apiKey) {
|
|
867
|
+
p6.log.error(pc6.red("Could not obtain API key. Initialization aborted."));
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (existsSync4(fullPath) && !options.force) {
|
|
872
|
+
const confirm3 = await p6.confirm({
|
|
873
|
+
message: `Config file already exists at ${fullPath}. Overwrite?`,
|
|
874
|
+
initialValue: false
|
|
875
|
+
});
|
|
876
|
+
if (p6.isCancel(confirm3) || !confirm3) {
|
|
877
|
+
p6.outro(pc6.yellow("Initialization cancelled"));
|
|
878
|
+
process.exit(0);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const s = p6.spinner();
|
|
882
|
+
s.start("Generating project configuration...");
|
|
883
|
+
try {
|
|
884
|
+
const plans = await fetchPlans({ apiKey, apiUrl: `${getApiUrl()}/api/v1` });
|
|
885
|
+
const creditSystems = await fetchCreditSystems(
|
|
886
|
+
apiKey,
|
|
887
|
+
`${getApiUrl()}/api/v1`
|
|
888
|
+
);
|
|
889
|
+
const configContent = generateConfig(plans, creditSystems);
|
|
890
|
+
await writeFile3(fullPath, configContent, "utf8");
|
|
891
|
+
s.stop(pc6.green("Configuration created"));
|
|
892
|
+
p6.note(
|
|
893
|
+
`${pc6.dim("File:")} ${fullPath}
|
|
894
|
+
${pc6.dim("Plans:")} ${plans.length} imported
|
|
895
|
+
${pc6.dim("Credit Systems:")} ${creditSystems.length}`,
|
|
896
|
+
"\u2728 Project Initialized"
|
|
897
|
+
);
|
|
898
|
+
p6.outro(
|
|
899
|
+
pc6.cyan(
|
|
900
|
+
`Next step: Run ${pc6.bold("owostack sync")} to apply your catalog.`
|
|
901
|
+
)
|
|
902
|
+
);
|
|
903
|
+
} catch (e) {
|
|
904
|
+
s.stop(pc6.red("Initialization failed"));
|
|
905
|
+
p6.log.error(e.message);
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/commands/validate.ts
|
|
911
|
+
import * as p7 from "@clack/prompts";
|
|
912
|
+
import pc7 from "picocolors";
|
|
913
|
+
async function runValidate(options) {
|
|
914
|
+
p7.intro(pc7.bgYellow(pc7.black(" validate ")));
|
|
915
|
+
const fullPath = resolveConfigPath(options.config);
|
|
916
|
+
const s = p7.spinner();
|
|
917
|
+
s.start(`Loading ${pc7.cyan(options.config)}`);
|
|
918
|
+
const owo = await loadOwostackFromConfig(fullPath);
|
|
919
|
+
if (!owo || typeof owo.sync !== "function") {
|
|
920
|
+
s.stop(pc7.red("Invalid configuration"));
|
|
921
|
+
p7.log.error("Config file must export an Owostack instance.");
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
if (!owo._config?.catalog || owo._config.catalog.length === 0) {
|
|
925
|
+
s.stop(pc7.red("No catalog found"));
|
|
926
|
+
p7.log.error("Config has no catalog to validate.");
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
s.stop(
|
|
930
|
+
pc7.green(`Configuration loaded (${owo._config.catalog.length} entries)`)
|
|
931
|
+
);
|
|
932
|
+
const { buildSyncPayload } = await import("owostack").catch(() => ({
|
|
933
|
+
buildSyncPayload: null
|
|
934
|
+
}));
|
|
935
|
+
if (!buildSyncPayload) {
|
|
936
|
+
p7.log.error("buildSyncPayload unavailable from owostack.");
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
const payload = buildSyncPayload(owo._config.catalog);
|
|
941
|
+
p7.log.step(pc7.bold("Features"));
|
|
942
|
+
for (const f of payload.features) {
|
|
943
|
+
p7.log.message(`${pc7.green("\u2713")} ${f.slug} ${pc7.dim(`(${f.type})`)}`);
|
|
944
|
+
}
|
|
945
|
+
p7.log.step(pc7.bold("Plans"));
|
|
946
|
+
for (const p_obj of payload.plans) {
|
|
947
|
+
p7.log.message(
|
|
948
|
+
`${pc7.green("\u2713")} ${pc7.bold(p_obj.slug)} ${pc7.dim(`${p_obj.currency} ${p_obj.price} / ${p_obj.interval}`)}`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
if (options.prod) {
|
|
952
|
+
p7.log.step(pc7.magenta("Production Mode Check"));
|
|
953
|
+
const configSettings = await loadConfigSettings(options.config);
|
|
954
|
+
const testUrl = getTestApiUrl(configSettings.environments?.test);
|
|
955
|
+
const liveUrl = getApiUrl(configSettings.environments?.live);
|
|
956
|
+
const apiKey = getApiKey();
|
|
957
|
+
try {
|
|
958
|
+
const testPlans = await fetchPlans({
|
|
959
|
+
apiKey,
|
|
960
|
+
apiUrl: `${testUrl}/api/v1`
|
|
961
|
+
});
|
|
962
|
+
p7.log.success(
|
|
963
|
+
`TEST environment accessible (${testPlans.length} remote plans)`
|
|
964
|
+
);
|
|
965
|
+
} catch (e) {
|
|
966
|
+
p7.log.error(`TEST environment check failed: ${e.message}`);
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const livePlans = await fetchPlans({
|
|
970
|
+
apiKey,
|
|
971
|
+
apiUrl: `${liveUrl}/api/v1`
|
|
972
|
+
});
|
|
973
|
+
p7.log.success(
|
|
974
|
+
`LIVE environment accessible (${livePlans.length} remote plans)`
|
|
975
|
+
);
|
|
976
|
+
} catch (e) {
|
|
977
|
+
p7.log.error(`LIVE environment check failed: ${e.message}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
p7.outro(pc7.green("Validation passed! \u2728"));
|
|
981
|
+
} catch (e) {
|
|
982
|
+
p7.log.error(`Validation failed: ${e.message}`);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/commands/connect.ts
|
|
988
|
+
import * as p8 from "@clack/prompts";
|
|
989
|
+
import pc8 from "picocolors";
|
|
990
|
+
import { existsSync as existsSync5 } from "fs";
|
|
991
|
+
async function runConnect(options) {
|
|
992
|
+
p8.intro(pc8.bgYellow(pc8.black(" connect ")));
|
|
993
|
+
const configPath = "./owo.config.ts";
|
|
994
|
+
let apiUrl = getApiUrl();
|
|
995
|
+
let dashboardUrl = getDashboardUrl();
|
|
996
|
+
let noBrowser = options.browser === false;
|
|
997
|
+
if (existsSync5(resolveConfigPath(configPath))) {
|
|
998
|
+
const configSettings = await loadConfigSettings(configPath);
|
|
999
|
+
if (configSettings.connect?.dashboardUrl) {
|
|
1000
|
+
dashboardUrl = getDashboardUrl(configSettings.connect.dashboardUrl);
|
|
1001
|
+
}
|
|
1002
|
+
if (configSettings.apiUrl) {
|
|
1003
|
+
apiUrl = getApiUrl(configSettings.apiUrl);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const apiKey = await executeConnectFlow({
|
|
1007
|
+
apiUrl,
|
|
1008
|
+
dashboardUrl,
|
|
1009
|
+
noBrowser,
|
|
1010
|
+
timeout: 300
|
|
1011
|
+
});
|
|
1012
|
+
if (apiKey) {
|
|
1013
|
+
p8.note(
|
|
1014
|
+
`${pc8.dim("API Key:")} owo_***
|
|
1015
|
+
${pc8.dim("Config:")} ${GLOBAL_CONFIG_PATH}`,
|
|
1016
|
+
"Connected successfully!"
|
|
1017
|
+
);
|
|
1018
|
+
p8.outro(pc8.green("Authentication complete \u2728"));
|
|
1019
|
+
} else {
|
|
1020
|
+
p8.log.error(pc8.red("Connection failed. Please try again."));
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/lib/brand.ts
|
|
1026
|
+
import pc9 from "picocolors";
|
|
1027
|
+
var OWO_ASCII = `
|
|
1028
|
+
${pc9.yellow("\u2588\u2588\u2588\u2588\u2588\u2588\u2557")} ${pc9.white("\u2588\u2588\u2557 \u2588\u2588\u2557")} ${pc9.yellow("\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
1029
|
+
${pc9.yellow("\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557")} ${pc9.white("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc9.yellow("\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557")}
|
|
1030
|
+
${pc9.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc9.white("\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551")} ${pc9.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")}
|
|
1031
|
+
${pc9.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")} ${pc9.white("\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551")} ${pc9.yellow("\u2588\u2588\u2551 \u2588\u2588\u2551")}
|
|
1032
|
+
${pc9.yellow("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")} ${pc9.white("\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D")} ${pc9.yellow("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
|
|
1033
|
+
${pc9.yellow("\u255A\u2550\u2550\u2550\u2550\u2550\u255D")} ${pc9.white("\u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D")} ${pc9.yellow("\u255A\u2550\u2550\u2550\u2550\u2550\u255D")}
|
|
1034
|
+
`;
|
|
1035
|
+
function printBrand() {
|
|
1036
|
+
console.log(OWO_ASCII);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/index.ts
|
|
1040
|
+
var program = new Command();
|
|
1041
|
+
printBrand();
|
|
1042
|
+
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", "./owo.config.ts").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);
|
|
1044
|
+
program.command("pull").description("Pull plans from dashboard into owo.config.ts").option("--config <path>", "Path to config file", "./owo.config.ts").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);
|
|
1045
|
+
program.command("diff").description("Compare local config to dashboard plans").option("--config <path>", "Path to config file", "./owo.config.ts").option("--key <api-key>", "API secret key").option("--prod", "Execute in both test and live environments").action(runDiff);
|
|
1046
|
+
program.command("init").description("Initialize owo.config.ts from dashboard").option("--config <path>", "Path to config file", "./owo.config.ts").option("--key <api-key>", "API secret key").option("--force", "Overwrite existing config file", false).action(runInit);
|
|
1047
|
+
program.command("validate").description("Validate local config without syncing").option("--config <path>", "Path to config file", "./owo.config.ts").option("--prod", "Execute in both test and live environments").action(runValidate);
|
|
1048
|
+
program.command("connect").description("Connect CLI to dashboard via browser").option("--no-browser", "Don't open the browser automatically").action(runConnect);
|
|
1049
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "owosk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Owostack - sync catalog, manage billing infrastructure",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Abdulmumin1/owostack.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"owostack": "./dist/index.js",
|
|
17
|
+
"owo": "./dist/index.js",
|
|
18
|
+
"owos": "./dist/index.js",
|
|
19
|
+
"owon": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm --clean",
|
|
23
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
24
|
+
"start": "tsx src/index.ts"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@clack/prompts": "^1.0.1",
|
|
28
|
+
"owostack": "workspace:*",
|
|
29
|
+
"@owostack/types": "workspace:*",
|
|
30
|
+
"commander": "^14.0.3",
|
|
31
|
+
"picocolors": "^1.1.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.19.10",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"tsx": "^4.19.0",
|
|
37
|
+
"typescript": "^5.7.3"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|