@api-now/cli 1.0.4

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.md ADDED
@@ -0,0 +1,15 @@
1
+ # API Now Ecosystem License
2
+
3
+ Copyright (c) 2025 API Now
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the Software **exclusively within the API Now ecosystem**.
6
+
7
+ **Restrictions:**
8
+
9
+ - The Software may not be used, copied, modified, merged, published, or distributed outside of the API Now ecosystem.
10
+ - The Software may not be sublicensed, sold, or used as part of any product or service that is not part of the API Now ecosystem.
11
+ - Any use of the Software outside the API Now ecosystem is strictly prohibited.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
+
15
+ For questions about permitted use, please contact: <info@apinow.app>
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # API NOW! CLI
2
+
3
+ The **API NOW! CLI** is a robust, cross-platform command-line tool designed to manage settings, handle authentication, organize directories/files, and publish assets directly to the Data Catalog from your terminal.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - **OAuth2 Loopback Authentication**: Authenticate using your Google, GitHub, or LinkedIn accounts.
10
+ - **Interactive Onboarding**: Guided setup prompts for first-time users to configure organization names and slugs (validated against reserved keywords/taken values).
11
+ - **Default Organization Management**: List organizations and pin a default workspace ID.
12
+ - **Metadata and Media Uploads**: Create, list, and read blueprints, domains, and multimedia assets.
13
+ - **Data Catalog Publishing**: Publish schemas and datasets to the global/private catalog with automatic semantic versioning support.
14
+ - **Developer Formatting Options**: Toggle outputs between human-friendly ASCII tables and machine-readable JSON.
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ Install the CLI globally from npm:
21
+
22
+ ```bash
23
+ npm install -g @api-now/cli
24
+ ```
25
+
26
+ Once installed, the CLI is available as the `apinow` command.
27
+
28
+ ---
29
+
30
+ ## Usage & Commands
31
+
32
+ All commands support the following global options:
33
+ - `--api-url <url>`: Override the target API Server URL (Default: `http://localhost:8080`). Note that the platform is actively under development and the final default URL will be set once the platform is officially released.
34
+ - `--format <text|json>`: Define the output layout (Default: `text`).
35
+
36
+ ### 1. Configuration (`config`)
37
+ Read and write persistent CLI configurations stored in the OS settings folder depending on the platform:
38
+ - **Linux**: `~/.config/apinow-cli/config.json` (or respects `$XDG_CONFIG_HOME`)
39
+ - **macOS**: `~/Library/Preferences/apinow-cli/config.json`
40
+ - **Windows**: `%APPDATA%\apinow-cli\config.json`
41
+
42
+ ```bash
43
+ # Get a configuration property (e.g. apiUrl, defaultOrg)
44
+ apinow config get <key>
45
+
46
+ # Set a configuration property
47
+ apinow config set <key> <value>
48
+ ```
49
+
50
+ ### 2. Authentication (`auth`)
51
+ Securely log in to the API platform using OAuth2.
52
+
53
+ ```bash
54
+ # Log in using Google, GitHub, or LinkedIn
55
+ apinow auth login <google|github|linkedin>
56
+
57
+ # Verify current authentication status and user identity details
58
+ apinow auth status
59
+
60
+ # Log out and erase stored authentication tokens
61
+ apinow auth logout
62
+ ```
63
+
64
+ *Note: On your first login or checking status with no registered organization, an interactive step will automatically guide you through creating your first organization with live slug verification.*
65
+
66
+ #### Personal Access Tokens (`auth tokens`)
67
+ Generate and manage Personal Access Tokens (PATs) for programmatic access.
68
+
69
+ ```bash
70
+ # List all personal access tokens
71
+ apinow auth tokens list
72
+
73
+ # Create a new personal access token
74
+ apinow auth tokens create [--name <token_name>] [--expires-at <duration_or_timestamp>]
75
+
76
+ Example:
77
+ apinow auth tokens create --name "Test Token" --expires-at "30 days"
78
+
79
+ # Delete a personal access token by its ID
80
+ apinow auth tokens delete <token_id>
81
+ ```
82
+
83
+ ### 3. Organizations (`orgs`)
84
+ Manage organization contexts.
85
+
86
+ ```bash
87
+ # List all organizations you belong to
88
+ apinow orgs list
89
+
90
+ # Get the default configured organization
91
+ apinow orgs get-default
92
+
93
+ # Set default organization to avoid passing --org CLI parameters
94
+ apinow orgs set-default <organization_id>
95
+ ```
96
+
97
+ ### 4. Files (`files`)
98
+ Manage domain and API files in your organizations.
99
+
100
+ ```bash
101
+ # List files with optional filters
102
+ apinow files list --org <org_id> --parent <parent_id> --kind <domain|api>
103
+
104
+ # Create file metadata and upload local media
105
+ apinow files create --name "My Domain File" --kind "domain" --org <org_id> --parent <parent_id> --media ./path/to/schema.ts
106
+
107
+ # Create file metadata and pipe media contents from STDIN
108
+ echo '{"schema": "content"}' | apinow files create --name "My Piped File" --kind "api" --org <org_id> --stdin
109
+
110
+ # Read file metadata (default) or media content (--media)
111
+ apinow files read --id <file_id> --org <org_id> [--media]
112
+ ```
113
+
114
+ ### 5. Data Catalog (`catalog`)
115
+ Publish and browse published catalog items.
116
+
117
+ ```bash
118
+ # Publish a local data domain file to the catalog
119
+ apinow catalog publish --file <file_id> --name "Catalog Name" --description "Catalog Description" --scope public --catalog-version 1.0.0
120
+
121
+ # List all published data domains in the catalog
122
+ apinow catalog list --scope <all|public|organization|private> [--key <domain_key>]
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Development
128
+
129
+ ### Local Setup & Building
130
+ 1. Clone the repository and install dependencies:
131
+ ```bash
132
+ npm install
133
+ ```
134
+
135
+ 2. Build the package:
136
+ ```bash
137
+ npm run build
138
+ ```
139
+
140
+ 3. Run the CLI locally:
141
+ ```bash
142
+ # Run directly via tsx (Development)
143
+ npm run dev -- [command]
144
+
145
+ # Or execute the compiled build
146
+ node dist/index.js [command]
147
+ ```
148
+
149
+ ### Formatting and Linting
150
+ To format the source code with Prettier:
151
+ ```bash
152
+ npm run format
153
+ ```
154
+
155
+ To run the ESLint static code analysis checks:
156
+ ```bash
157
+ npm run lint
158
+ ```
159
+
160
+ ### Type Checking
161
+ To run the TypeScript compiler in dry-run mode:
162
+ ```bash
163
+ npm run typecheck
164
+ ```
@@ -0,0 +1,314 @@
1
+ import { Argument } from 'commander';
2
+ import http from 'node:http';
3
+ import open from 'open';
4
+ import { config } from '../utils/config.js';
5
+ import { getSdkClient, handleApiError } from '../utils/api.js';
6
+ import { Formatter, style, debug } from '../utils/formatter.js';
7
+ import { setupFirstOrganizationIfNeeded } from '../utils/org-setup.js';
8
+ import { registerTokensCommands } from './tokens.js';
9
+ /**
10
+ * Registers authentication management commands (login, status, logout) with the main Commander program.
11
+ *
12
+ * @param program - The root Commander program.
13
+ */
14
+ export function registerAuthCommands(program) {
15
+ const authCmd = program.command('auth').description('Manage CLI authentication');
16
+ registerTokensCommands(authCmd);
17
+ authCmd
18
+ .command('login')
19
+ .description('Authenticate with the API platform using OAuth2 (google, github, or linkedin)')
20
+ .addArgument(new Argument('provider', 'Authentication provider').choices(['google', 'github', 'linkedin']))
21
+ .action(async (provider) => {
22
+ const formatter = new Formatter({ format: config.resolved.format });
23
+ const allowedProviders = ['google', 'github', 'linkedin'];
24
+ if (!allowedProviders.includes(provider)) {
25
+ formatter.error({
26
+ message: `Invalid provider '${provider}'. Allowed: ${allowedProviders.join(', ')}`,
27
+ });
28
+ process.exit(1);
29
+ }
30
+ const apiUrl = config.resolved.apiUrl;
31
+ if (config.resolved.format !== 'json') {
32
+ console.log(`Starting authentication flow using provider '${provider}'...`);
33
+ }
34
+ try {
35
+ await runLoginServer(apiUrl, provider, formatter);
36
+ formatter.success('Successfully authenticated!');
37
+ const client = getSdkClient();
38
+ await setupFirstOrganizationIfNeeded(client, formatter);
39
+ }
40
+ catch (err) {
41
+ if (err instanceof Error) {
42
+ formatter.error({ message: err.message });
43
+ }
44
+ else {
45
+ formatter.error({ message: 'Authentication failed.' });
46
+ }
47
+ process.exit(1);
48
+ }
49
+ });
50
+ authCmd
51
+ .command('logout')
52
+ .description('Log out and clear the stored token')
53
+ .action(() => {
54
+ const formatter = new Formatter({ format: config.resolved.format });
55
+ config.clearConfigKey('token');
56
+ formatter.success('Logged out successfully.');
57
+ });
58
+ authCmd
59
+ .command('status')
60
+ .description('Check current authentication status')
61
+ .action(async () => {
62
+ const formatter = new Formatter({ format: config.resolved.format });
63
+ const resolved = config.resolved;
64
+ if (!resolved.token) {
65
+ formatter.error({ message: 'Not logged in. Use `auth login <provider>` to authenticate.' });
66
+ process.exit(1);
67
+ }
68
+ try {
69
+ const client = getSdkClient();
70
+ const user = await client.users.me({ token: client.token });
71
+ if (resolved.format === 'json') {
72
+ formatter.object({ authenticated: true, user });
73
+ }
74
+ else {
75
+ console.log(style.green('✔ Authenticated successfully!'));
76
+ console.log(`User: ${user.name} (${user.email})`);
77
+ console.log(`Default Org: ${resolved.org || 'None'}`);
78
+ }
79
+ await setupFirstOrganizationIfNeeded(client, formatter);
80
+ }
81
+ catch (err) {
82
+ const apiErr = handleApiError(err);
83
+ formatter.error({
84
+ message: 'Authentication token is invalid or expired.',
85
+ detail: apiErr.message,
86
+ });
87
+ process.exit(1);
88
+ }
89
+ });
90
+ }
91
+ /**
92
+ * Spins up a temporary local HTTP server to receive the OAuth authentication token
93
+ * from the browser-based callback page.
94
+ *
95
+ * @param apiUrl - The target backend API server base URL.
96
+ * @param provider - The OAuth login provider.
97
+ * @param formatter - Output formatter.
98
+ * @returns A promise resolving to the retrieved authentication token.
99
+ */
100
+ function runLoginServer(apiUrl, provider, formatter) {
101
+ return new Promise((resolve, reject) => {
102
+ const server = http.createServer((req, res) => {
103
+ if (req.method === 'GET' && req.url?.startsWith('/callback')) {
104
+ debug('auth', `Received request for callback: ${req.url}`);
105
+ res.writeHead(200, { 'Content-Type': 'text/html' });
106
+ const html = `
107
+ <!DOCTYPE html>
108
+ <html>
109
+ <head>
110
+ <title>API NOW! CLI Login</title>
111
+ <style>
112
+ body {
113
+ font-family: system-ui, -apple-system, sans-serif;
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ height: 100vh;
118
+ margin: 0;
119
+ background: #121214;
120
+ color: #e1e1e6;
121
+ }
122
+ .card {
123
+ background: #202024;
124
+ padding: 2rem;
125
+ border-radius: 8px;
126
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
127
+ text-align: center;
128
+ max-width: 450px;
129
+ width: 100%;
130
+ }
131
+ h1 { color: #04d361; margin-top: 0; }
132
+ .status { font-weight: bold; margin: 1rem 0; line-height: 1.4; }
133
+ .diagnostics {
134
+ display: none;
135
+ text-align: left;
136
+ margin-top: 1.5rem;
137
+ padding: 1rem;
138
+ background: #2f2f33;
139
+ border-radius: 4px;
140
+ font-size: 0.8rem;
141
+ font-family: monospace;
142
+ white-space: pre-wrap;
143
+ word-break: break-all;
144
+ border-left: 4px solid #f75a68;
145
+ }
146
+ </style>
147
+ </head>
148
+ <body>
149
+ <div class="card">
150
+ <h1>API NOW!</h1>
151
+ <p class="status" id="status">Retrieving token from server...</p>
152
+ <div class="diagnostics" id="diagnostics">
153
+ <strong>Diagnostic Details:</strong>
154
+ <div id="diagnostics-content" style="margin-top: 0.5rem;"></div>
155
+ </div>
156
+ </div>
157
+ <script>
158
+ fetch('${apiUrl}/auth/token', { credentials: 'include' })
159
+ .then(r => {
160
+ if (!r.ok) {
161
+ throw new Error('Token request failed: ' + r.statusText + ' (status: ' + r.status + ')');
162
+ }
163
+ return r.json();
164
+ })
165
+ .then(data => {
166
+ console.log("[DEBUG] Parse response JSON:", data);
167
+ const token = data.token || data.data?.token || data.data;
168
+ if (!token) {
169
+ throw new Error('Token not found in API response structure');
170
+ }
171
+
172
+ console.log("[DEBUG] POSTing token to local server callback...");
173
+ return fetch('/token', {
174
+ method: 'POST',
175
+ headers: { 'Content-Type': 'application/json' },
176
+ body: JSON.stringify({ token })
177
+ });
178
+ })
179
+ .then(r => {
180
+ if (!r.ok) {
181
+ throw new Error('Failed to save token in CLI server (status: ' + r.status + ')');
182
+ }
183
+ document.getElementById('status').innerText = 'Authentication successful! You can close this tab now.';
184
+ document.getElementById('status').style.color = '#04d361';
185
+ })
186
+ .catch(err => {
187
+ console.error("[ERROR]", err);
188
+ document.getElementById('status').innerText = 'Authentication failed.';
189
+ document.getElementById('status').style.color = '#f75a68';
190
+
191
+ const diag = document.getElementById('diagnostics');
192
+ const content = document.getElementById('diagnostics-content');
193
+ diag.style.display = 'block';
194
+ content.innerText =
195
+ 'Error Message: ' + err.message + '\\n\\n' +
196
+ 'Originating URL: ' + window.location.href + '\\n' +
197
+ 'Target API URL: ${apiUrl}/auth/token\\n\\n' +
198
+ 'TIP: If you get "Unauthorized (status: 401)", make sure that:\\n' +
199
+ '1. You completed the login flow on the browser window that opened.\\n' +
200
+ '2. The host of the page you are on (' + window.location.hostname + ') matches the host of the API (' + new URL('${apiUrl}').hostname + ') to satisfy SameSite cookie policy.\\n' +
201
+ '3. Check browser developer console (F12) for detailed CORS/Network errors.';
202
+
203
+ fetch('/error', {
204
+ method: 'POST',
205
+ headers: { 'Content-Type': 'application/json' },
206
+ body: JSON.stringify({ message: err.message })
207
+ }).catch(console.error);
208
+ });
209
+ </script>
210
+ </body>
211
+ </html>
212
+ `;
213
+ res.end(html);
214
+ return;
215
+ }
216
+ if (req.method === 'POST' && req.url === '/token') {
217
+ let body = '';
218
+ req.on('data', (chunk) => {
219
+ body += chunk;
220
+ });
221
+ req.on('end', () => {
222
+ try {
223
+ const data = JSON.parse(body);
224
+ if (data.token) {
225
+ debug('auth', 'Storing retrieved token to CLI configuration...');
226
+ config.writeProperty('token', data.token);
227
+ res.writeHead(200, { 'Content-Type': 'application/json' });
228
+ res.end(JSON.stringify({ success: true }));
229
+ resolve(data.token);
230
+ }
231
+ else {
232
+ res.writeHead(400, { 'Content-Type': 'application/json' });
233
+ res.end(JSON.stringify({ error: 'Token missing' }));
234
+ reject(new Error('Token missing from browser callback.'));
235
+ }
236
+ }
237
+ catch (e) {
238
+ if (e instanceof Error) {
239
+ res.writeHead(400, { 'Content-Type': 'application/json' });
240
+ res.end(JSON.stringify({ error: e.message }));
241
+ reject(new Error('Invalid body received: ' + e.message));
242
+ }
243
+ else {
244
+ res.writeHead(400, { 'Content-Type': 'application/json' });
245
+ res.end(JSON.stringify({ error: 'Invalid body received' }));
246
+ reject(new Error('Invalid body received'));
247
+ }
248
+ }
249
+ finally {
250
+ cleanup();
251
+ }
252
+ });
253
+ return;
254
+ }
255
+ if (req.method === 'POST' && req.url === '/error') {
256
+ let body = '';
257
+ req.on('data', (chunk) => {
258
+ body += chunk;
259
+ });
260
+ req.on('end', () => {
261
+ try {
262
+ const data = JSON.parse(body);
263
+ debug('auth', `Received login error from browser callback: ${data.message}`);
264
+ reject(new Error(data.message || 'Browser login failed'));
265
+ }
266
+ catch {
267
+ reject(new Error('Browser login failed'));
268
+ }
269
+ finally {
270
+ cleanup();
271
+ }
272
+ });
273
+ return;
274
+ }
275
+ res.writeHead(404);
276
+ res.end();
277
+ });
278
+ const handleSigInt = () => {
279
+ cleanup();
280
+ process.exit(130);
281
+ };
282
+ process.on('SIGINT', handleSigInt);
283
+ process.on('SIGTERM', handleSigInt);
284
+ const timeout = setTimeout(() => {
285
+ reject(new Error('Authentication timed out.'));
286
+ cleanup();
287
+ }, 300000);
288
+ function cleanup() {
289
+ clearTimeout(timeout);
290
+ process.off('SIGINT', handleSigInt);
291
+ process.off('SIGTERM', handleSigInt);
292
+ server.close();
293
+ }
294
+ server.listen(0, '127.0.0.1', async () => {
295
+ const port = server.address().port;
296
+ // Smart hostname matching: resolve 127.0.0.1 vs localhost context dynamically to enable cookie transport
297
+ const apiHost = new URL(apiUrl).hostname;
298
+ const redirectHost = apiHost === 'localhost' ? 'localhost' : '127.0.0.1';
299
+ const loginUrl = `${apiUrl}/auth/${provider}/redirect?r=http://${redirectHost}:${port}/callback`;
300
+ debug('auth', `Ephemeral loopback server listening on http://127.0.0.1:${port}`);
301
+ debug('auth', `Browser callback origin configured to http://${redirectHost}:${port}`);
302
+ if (formatter.format !== 'json') {
303
+ console.log(`Opening browser to log in...`);
304
+ console.log(`If it doesn't open automatically, navigate to:\n ${loginUrl}`);
305
+ }
306
+ try {
307
+ await open(loginUrl);
308
+ }
309
+ catch (_err) {
310
+ // Ignore open failure
311
+ }
312
+ });
313
+ });
314
+ }