@api-now/cli 1.0.4 → 1.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.
@@ -1,293 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { getSdkClient, handleApiError } from '../utils/api.js';
4
- import { config } from '../utils/config.js';
5
- import { Formatter, CliErrorCode } from '../utils/formatter.js';
6
- import { ApiFileKind, DomainFileKind, FolderKind } from '@api-now/core/models/kinds.js';
7
- import { File } from '@api-now/core/models/index.js';
8
- import { askInput, askSelect } from '../utils/prompt.js';
9
- /**
10
- * Registers files management commands (list, create, read) with the main Commander program.
11
- *
12
- * @param program - The root Commander program.
13
- */
14
- export function registerFilesCommands(program) {
15
- const filesCmd = program.command('files').description('Manage files and metadata');
16
- filesCmd
17
- .command('list')
18
- .description('List files in an organization')
19
- .option('--org <oid>', 'Organization ID (optional, defaults to configured default org)')
20
- .option('--kind <kind>', 'Filter by file kind (e.g. domain, api, folder)')
21
- .option('--parent <parent>', 'Filter by parent folder ID')
22
- .action(async (options) => {
23
- const formatter = new Formatter({ format: config.resolved.format });
24
- const resolved = config.resolved;
25
- const orgId = options.org || resolved.org;
26
- if (!orgId) {
27
- formatter.error({
28
- code: CliErrorCode.MISSING_REQUIRED,
29
- message: 'Organization ID is required.',
30
- detail: 'Pass --org <oid> or set a default organization using `apinow orgs set-default <oid>`.',
31
- });
32
- process.exit(1);
33
- }
34
- try {
35
- const client = getSdkClient();
36
- const listOpts = {};
37
- if (options.parent) {
38
- listOpts.parent = options.parent;
39
- }
40
- if (options.kind) {
41
- const targetKind = getFileMetaKind(options.kind);
42
- listOpts.filter = {
43
- field: 'kind',
44
- operator: 'eq',
45
- value: targetKind,
46
- };
47
- }
48
- const res = await client.files.list(orgId, listOpts, { token: client.token });
49
- const files = res.data || [];
50
- const rows = files.map((f) => ({
51
- id: f.key,
52
- name: f.info?.name || '',
53
- kind: f.kind,
54
- isShortcut: f.isShortcut ? 'Yes' : 'No',
55
- }));
56
- formatter.list(rows, ['id', 'name', 'kind', 'isShortcut'], {
57
- headers: ['ID', 'NAME', 'KIND', 'SHORTCUT'],
58
- });
59
- }
60
- catch (err) {
61
- const apiErr = handleApiError(err);
62
- formatter.error(apiErr);
63
- process.exit(1);
64
- }
65
- });
66
- filesCmd
67
- .command('create')
68
- .description('Create a new file metadata record (and optionally upload media)')
69
- .option('--name <name>', 'Name of the file')
70
- .option('--kind <kind>', 'Kind of the file (e.g., domain, api)')
71
- .option('--org <oid>', 'Organization ID (optional, defaults to configured default org)')
72
- .option('--parent <parent>', 'Parent folder ID')
73
- .option('--media <path>', 'Local path to the file content/media to upload')
74
- .option('--stdin', 'Read media content from STDIN')
75
- .action(async (options) => {
76
- const formatter = new Formatter({ format: config.resolved.format });
77
- const resolved = config.resolved;
78
- const orgId = options.org || resolved.org;
79
- if (!orgId) {
80
- formatter.error({
81
- code: CliErrorCode.MISSING_REQUIRED,
82
- message: 'Organization ID is required.',
83
- detail: 'Pass --org <oid> or set a default organization using `apinow orgs set-default <oid>`.',
84
- });
85
- process.exit(1);
86
- }
87
- if (options.media && options.stdin) {
88
- formatter.error({
89
- code: CliErrorCode.INVALID_ARGUMENT,
90
- message: 'Cannot specify both --media <path> and --stdin',
91
- });
92
- process.exit(1);
93
- }
94
- // Empathic interactive fallbacks
95
- let name = options.name;
96
- if (!name && process.stdout.isTTY) {
97
- name = await askInput('Enter file name:');
98
- }
99
- if (!name) {
100
- formatter.error({
101
- code: CliErrorCode.MISSING_REQUIRED,
102
- message: 'Name is required.',
103
- detail: 'Specify --name <name>.',
104
- });
105
- process.exit(1);
106
- }
107
- let kind = options.kind;
108
- if (!kind && process.stdout.isTTY) {
109
- kind = await askSelect('Select file kind:', [
110
- { name: 'API Domain / Schema (domain)', value: 'domain' },
111
- { name: 'API Schema (api)', value: 'api' },
112
- { name: 'Folder (folder)', value: 'folder' },
113
- ]);
114
- }
115
- if (!kind) {
116
- formatter.error({
117
- code: CliErrorCode.MISSING_REQUIRED,
118
- message: 'Kind is required.',
119
- detail: 'Specify --kind <kind> (allowed: api, domain, folder).',
120
- });
121
- process.exit(1);
122
- }
123
- try {
124
- let mediaContent;
125
- if (options.media) {
126
- const filePath = path.resolve(options.media);
127
- if (!fs.existsSync(filePath)) {
128
- formatter.error({
129
- code: CliErrorCode.INVALID_ARGUMENT,
130
- message: `Media file not found at path: ${filePath}`,
131
- });
132
- process.exit(1);
133
- }
134
- mediaContent = fs.readFileSync(filePath, 'utf8');
135
- }
136
- else if (options.stdin) {
137
- mediaContent = await readStdin();
138
- }
139
- let fileKey;
140
- let fileContent;
141
- if (mediaContent !== undefined) {
142
- try {
143
- fileContent = JSON.parse(mediaContent);
144
- }
145
- catch {
146
- throw new Error('invalid API now platform file');
147
- }
148
- if (!fileContent ||
149
- typeof fileContent !== 'object' ||
150
- Array.isArray(fileContent) ||
151
- !('key' in fileContent) ||
152
- typeof fileContent.key !== 'string' ||
153
- !fileContent.key.trim()) {
154
- throw new Error('invalid API now platform file');
155
- }
156
- fileKey = fileContent.key;
157
- }
158
- const client = getSdkClient();
159
- const fileMeta = File.createSchema({
160
- kind: getFileMetaKind(kind),
161
- info: {
162
- name,
163
- },
164
- key: fileKey,
165
- });
166
- const createOpts = {};
167
- if (options.parent) {
168
- createOpts.parent = options.parent;
169
- }
170
- const file = await client.files.createMeta(fileMeta, orgId, createOpts, { token: client.token });
171
- const fileId = file.key;
172
- if (mediaContent !== undefined) {
173
- await client.files.createMedia(fileContent, orgId, fileId, {}, { token: client.token });
174
- }
175
- formatter.success('File created successfully!', file);
176
- }
177
- catch (err) {
178
- const apiErr = handleApiError(err);
179
- formatter.error({
180
- code: CliErrorCode.API_FAILURE,
181
- ...apiErr,
182
- });
183
- process.exit(1);
184
- }
185
- });
186
- filesCmd
187
- .command('read')
188
- .description('Read file metadata or media content')
189
- .option('--id <fid>', 'File ID')
190
- .option('--org <oid>', 'Organization ID (optional, defaults to configured default org)')
191
- .option('--meta', 'Read file metadata (default)')
192
- .option('--media', 'Read file media/content')
193
- .action(async (options) => {
194
- const formatter = new Formatter({ format: config.resolved.format });
195
- const resolved = config.resolved;
196
- const orgId = options.org || resolved.org;
197
- if (!orgId) {
198
- formatter.error({
199
- code: CliErrorCode.MISSING_REQUIRED,
200
- message: 'Organization ID is required.',
201
- detail: 'Pass --org <oid> or set a default organization using `apinow orgs set-default <oid>`.',
202
- });
203
- process.exit(1);
204
- }
205
- let id = options.id;
206
- if (!id && process.stdout.isTTY) {
207
- try {
208
- const client = getSdkClient();
209
- const res = await client.files.list(orgId, {}, { token: client.token });
210
- const files = res.data || [];
211
- if (files.length > 0) {
212
- id = await askSelect('Select a file to read:', files.map((f) => ({
213
- name: `${f.info?.name || 'Unnamed'} (${f.kind})`,
214
- value: f.key,
215
- })));
216
- }
217
- }
218
- catch (_err) {
219
- // Ignore and let standard missing error handle below
220
- }
221
- }
222
- if (!id) {
223
- formatter.error({
224
- code: CliErrorCode.MISSING_REQUIRED,
225
- message: 'File ID is required.',
226
- detail: 'Specify --id <fid>.',
227
- });
228
- process.exit(1);
229
- }
230
- try {
231
- const client = getSdkClient();
232
- if (options.media) {
233
- const res = await client.files.readMedia(orgId, id, { token: client.token });
234
- const content = res.media;
235
- if (resolved.format === 'json') {
236
- console.log(JSON.stringify(content, null, 2));
237
- }
238
- else {
239
- if (typeof content === 'object' && content !== null) {
240
- console.log(JSON.stringify(content, null, 2));
241
- }
242
- else {
243
- console.log(content);
244
- }
245
- }
246
- }
247
- else {
248
- const meta = await client.files.read(orgId, id, { token: client.token });
249
- formatter.object(meta);
250
- }
251
- }
252
- catch (err) {
253
- const apiErr = handleApiError(err);
254
- formatter.error(apiErr);
255
- process.exit(1);
256
- }
257
- });
258
- }
259
- /**
260
- * Translates a user-friendly kind name (e.g. 'api', 'domain', 'folder')
261
- * to the corresponding core platform file metadata Kind value.
262
- */
263
- function getFileMetaKind(kind) {
264
- const k = kind.toLowerCase();
265
- if (k === 'api') {
266
- return ApiFileKind;
267
- }
268
- if (k === 'domain') {
269
- return DomainFileKind;
270
- }
271
- if (k === 'folder') {
272
- return FolderKind;
273
- }
274
- throw new Error(`Unsupported file kind "${kind}". Allowed: api, domain, folder`);
275
- }
276
- /**
277
- * Asynchronously reads all input from standard input (STDIN).
278
- */
279
- async function readStdin() {
280
- return new Promise((resolve, reject) => {
281
- let data = '';
282
- process.stdin.setEncoding('utf8');
283
- process.stdin.on('data', (chunk) => {
284
- data += chunk;
285
- });
286
- process.stdin.on('end', () => {
287
- resolve(data);
288
- });
289
- process.stdin.on('error', (err) => {
290
- reject(err);
291
- });
292
- });
293
- }
@@ -1,104 +0,0 @@
1
- import { getSdkClient, handleApiError } from '../utils/api.js';
2
- import { config } from '../utils/config.js';
3
- import { Formatter, CliErrorCode } from '../utils/formatter.js';
4
- /**
5
- * Registers organization commands (list, set-default) with the main Commander program.
6
- *
7
- * @param program - The root Commander program.
8
- */
9
- export function registerOrgsCommands(program) {
10
- const orgsCmd = program.command('orgs').description('Manage organization configurations');
11
- orgsCmd
12
- .command('list')
13
- .description('List all organizations you belong to')
14
- .action(async () => {
15
- const formatter = new Formatter({ format: config.resolved.format });
16
- try {
17
- const client = getSdkClient();
18
- const res = await client.organizations.list({ token: client.token });
19
- const orgs = res.data || [];
20
- const resolved = config.resolved;
21
- const rows = orgs.map((org) => ({
22
- id: org.key,
23
- name: org.name,
24
- slug: org.slug,
25
- isDefault: org.key === resolved.org ? 'Yes' : 'No',
26
- }));
27
- formatter.list(rows, ['id', 'name', 'slug', 'isDefault'], {
28
- headers: ['ID', 'NAME', 'SLUG', 'DEFAULT'],
29
- });
30
- }
31
- catch (err) {
32
- const apiErr = handleApiError(err);
33
- formatter.error(apiErr);
34
- process.exit(1);
35
- }
36
- });
37
- orgsCmd
38
- .command('set-default <oid>')
39
- .description('Set your default organization ID')
40
- .action(async (oid) => {
41
- const formatter = new Formatter({ format: config.resolved.format });
42
- try {
43
- const client = getSdkClient();
44
- const res = await client.organizations.list({ token: client.token });
45
- const orgs = res.data || [];
46
- const exists = orgs.some((org) => org.key === oid);
47
- if (!exists) {
48
- formatter.error({
49
- code: CliErrorCode.INVALID_ARGUMENT,
50
- message: `Organization ID '${oid}' not found in your authorized list.`,
51
- detail: 'Run `apinow orgs list` to see all authorized organizations and their IDs.',
52
- });
53
- process.exit(1);
54
- }
55
- config.writeProperty('org', oid);
56
- formatter.success(`Default organization ID set to '${oid}'.`);
57
- }
58
- catch (err) {
59
- const apiErr = handleApiError(err);
60
- formatter.error(apiErr);
61
- process.exit(1);
62
- }
63
- });
64
- orgsCmd
65
- .command('default')
66
- .alias('get-default')
67
- .description('Get the default configured organization')
68
- .action(async () => {
69
- const formatter = new Formatter({ format: config.resolved.format });
70
- try {
71
- const resolved = config.resolved;
72
- if (!resolved.org) {
73
- formatter.error({
74
- code: CliErrorCode.MISSING_REQUIRED,
75
- message: 'No default organization configured.',
76
- detail: 'Set one using `apinow orgs set-default <oid>`. Run `apinow orgs list` to list available organizations.',
77
- });
78
- process.exit(1);
79
- }
80
- const client = getSdkClient();
81
- const res = await client.organizations.list({ token: client.token });
82
- const orgs = res.data || [];
83
- const org = orgs.find((org) => org.key === resolved.org);
84
- if (!org) {
85
- formatter.error({
86
- code: CliErrorCode.INVALID_ARGUMENT,
87
- message: `Default organization ID '${resolved.org}' not found in your authorized list.`,
88
- detail: 'Run `apinow orgs set-default <oid>` to configure a valid default organization.',
89
- });
90
- process.exit(1);
91
- }
92
- formatter.object({
93
- id: org.key,
94
- name: org.name,
95
- slug: org.slug,
96
- });
97
- }
98
- catch (err) {
99
- const apiErr = handleApiError(err);
100
- formatter.error(apiErr);
101
- process.exit(1);
102
- }
103
- });
104
- }
@@ -1,145 +0,0 @@
1
- import { getSdkClient, handleApiError } from '../utils/api.js';
2
- import { config } from '../utils/config.js';
3
- import { Formatter, CliErrorCode, style } from '../utils/formatter.js';
4
- /**
5
- * Helper function to decode the active token and extract its ID (jti or id claim).
6
- *
7
- * @returns The active token's ID if available, otherwise null.
8
- */
9
- function getActiveTokenId() {
10
- const token = config.resolved.token;
11
- if (!token)
12
- return null;
13
- try {
14
- const parts = token.split('.');
15
- if (parts.length === 3) {
16
- const payload = parts[1];
17
- const decoded = Buffer.from(payload, 'base64url').toString('utf8');
18
- const parsed = JSON.parse(decoded);
19
- if (parsed && typeof parsed === 'object') {
20
- return parsed.jti || parsed.id || null;
21
- }
22
- }
23
- }
24
- catch {
25
- // Ignore and return null if decoding fails
26
- }
27
- return null;
28
- }
29
- /**
30
- * Registers personal access tokens commands (list, create, delete) under the auth command namespace.
31
- *
32
- * @param authCmd - The auth subcommand router.
33
- */
34
- export function registerTokensCommands(authCmd) {
35
- const tokensCmd = authCmd.command('tokens').description('Manage personal access tokens');
36
- tokensCmd
37
- .command('list')
38
- .description('List all personal access tokens')
39
- .action(async () => {
40
- const formatter = new Formatter({ format: config.resolved.format });
41
- try {
42
- const client = getSdkClient();
43
- if (!client.token) {
44
- formatter.error({
45
- code: CliErrorCode.UNAUTHORIZED,
46
- message: 'Not logged in.',
47
- detail: 'Use `apinow auth login <provider>` to authenticate.',
48
- });
49
- process.exit(1);
50
- }
51
- const tokens = await client.users.listTokens({ token: client.token });
52
- const activeTokenId = getActiveTokenId();
53
- const rows = tokens.map((token) => ({
54
- id: token.id,
55
- name: token.name || 'N/A',
56
- lastUsedAt: token.lastUsedAt || 'Never',
57
- expiresAt: token.expiresAt || 'Never',
58
- active: token.id === activeTokenId ? 'Yes' : 'No',
59
- }));
60
- formatter.list(rows, ['id', 'name', 'lastUsedAt', 'expiresAt', 'active'], {
61
- headers: ['ID', 'NAME', 'LAST USED', 'EXPIRES', 'ACTIVE'],
62
- });
63
- }
64
- catch (err) {
65
- const apiErr = handleApiError(err);
66
- formatter.error(apiErr);
67
- process.exit(1);
68
- }
69
- });
70
- tokensCmd
71
- .command('create')
72
- .description('Create a new personal access token')
73
- .option('--name <name>', 'Optional name to identify the token')
74
- .option('--expires-at <expires>', 'Optional expiration time (e.g. "30 days", "10 minutes", or timestamp)')
75
- .action(async (options) => {
76
- const formatter = new Formatter({ format: config.resolved.format });
77
- try {
78
- const client = getSdkClient();
79
- if (!client.token) {
80
- formatter.error({
81
- code: CliErrorCode.UNAUTHORIZED,
82
- message: 'Not logged in.',
83
- detail: 'Use `apinow auth login <provider>` to authenticate.',
84
- });
85
- process.exit(1);
86
- }
87
- const payload = {};
88
- if (options.name) {
89
- payload.name = options.name;
90
- }
91
- if (options.expiresAt) {
92
- if (/^\d+$/.test(options.expiresAt)) {
93
- payload.expiresAt = parseInt(options.expiresAt, 10);
94
- }
95
- else {
96
- payload.expiresAt = options.expiresAt;
97
- }
98
- }
99
- const res = await client.users.createToken(payload, { token: client.token });
100
- if (config.resolved.format === 'json') {
101
- formatter.success('Token created successfully!', res);
102
- }
103
- else {
104
- formatter.success('Token created successfully!');
105
- console.log('\n' +
106
- style.yellow('⚠ WARNING: Make sure to copy your personal access token now. You will NOT be able to see it again!') +
107
- '\n');
108
- console.log(`${style.bold('Raw Token')}: ${style.cyan(res.token)}`);
109
- console.log(`${style.bold('ID')}: ${res.id}`);
110
- console.log(`${style.bold('Name')}: ${res.name || 'N/A'}`);
111
- console.log(`${style.bold('Type')}: ${res.type}`);
112
- console.log(`${style.bold('Expires At')}: ${res.expiresAt || 'Never'}\n`);
113
- }
114
- }
115
- catch (err) {
116
- const apiErr = handleApiError(err);
117
- formatter.error(apiErr);
118
- process.exit(1);
119
- }
120
- });
121
- tokensCmd
122
- .command('delete <id>')
123
- .description('Delete a personal access token by ID')
124
- .action(async (id) => {
125
- const formatter = new Formatter({ format: config.resolved.format });
126
- try {
127
- const client = getSdkClient();
128
- if (!client.token) {
129
- formatter.error({
130
- code: CliErrorCode.UNAUTHORIZED,
131
- message: 'Not logged in.',
132
- detail: 'Use `apinow auth login <provider>` to authenticate.',
133
- });
134
- process.exit(1);
135
- }
136
- await client.users.deleteToken(id, { token: client.token });
137
- formatter.success(`Token '${id}' deleted successfully.`);
138
- }
139
- catch (err) {
140
- const apiErr = handleApiError(err);
141
- formatter.error(apiErr);
142
- process.exit(1);
143
- }
144
- });
145
- }
package/dist/utils/api.js DELETED
@@ -1,54 +0,0 @@
1
- import { StoreSdk } from '@api-now/core/sdk/StoreSdkNode.js';
2
- import { config } from './config.js';
3
- import { CliErrorCode } from './formatter.js';
4
- /**
5
- * Configures and returns a StoreSdk instance for communicating with the API NOW! platform.
6
- * Reads the token from local configuration and configures it on the SDK automatically.
7
- *
8
- * @param options - Configuration overrides for the client connection.
9
- * @returns A StoreSdk instance ready for requests.
10
- */
11
- export function getSdkClient(options = {}) {
12
- const resolved = config.resolved;
13
- const apiUrl = options.apiUrl || resolved.apiUrl;
14
- const sdk = new StoreSdk(apiUrl);
15
- if (resolved.token) {
16
- sdk.token = resolved.token;
17
- }
18
- return sdk;
19
- }
20
- /**
21
- * Standardizes raw error throws from the SDK into a consistent, readable format.
22
- *
23
- * @param error - The raw error object caught in a try/catch block.
24
- * @returns A standardized ApiErrorResponse object.
25
- */
26
- export function handleApiError(error) {
27
- if (error && typeof error === 'object') {
28
- const err = error;
29
- const status = err.status;
30
- const code = err.code;
31
- let message = err.message || 'An unknown error occurred.';
32
- const help = err.help;
33
- if (status === 401) {
34
- message = 'Unauthorized. Please login using `auth login`.';
35
- return {
36
- message,
37
- status,
38
- code: CliErrorCode.UNAUTHORIZED,
39
- help,
40
- };
41
- }
42
- return {
43
- message,
44
- status,
45
- code: code || CliErrorCode.API_FAILURE,
46
- help,
47
- };
48
- }
49
- const err = error;
50
- return {
51
- message: err?.message || 'An unknown error occurred.',
52
- code: CliErrorCode.UNKNOWN,
53
- };
54
- }