@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.
@@ -0,0 +1,289 @@
1
+ import { getSdkClient, handleApiError } from '../utils/api.js';
2
+ import { config } from '../utils/config.js';
3
+ import { Formatter, CliErrorCode } from '../utils/formatter.js';
4
+ import { DomainFileKind } from '@api-now/core/models/kinds.js';
5
+ import { askInput, askSelect } from '../utils/prompt.js';
6
+ import { DataCatalogKind, DataCatalogVersion, } from '@api-now/core/models/index.js';
7
+ /**
8
+ * Registers catalog management commands (publish, list) with the main Commander program.
9
+ *
10
+ * @param program - The root Commander program.
11
+ */
12
+ export function registerCatalogCommands(program) {
13
+ const catalogCmd = program.command('catalog').description('Manage Data Catalog items');
14
+ catalogCmd
15
+ .command('publish')
16
+ .description('Publish a data domain file to the data catalog')
17
+ .option('--file <fid>', 'File ID of the data domain')
18
+ .option('--name <name>', 'Name for the catalog entry')
19
+ .option('--description <desc>', 'Description of the catalog entry')
20
+ .option('--scope <scope>', 'Publish scope: public, organization, private')
21
+ .option('-v, --catalog-version <ver>', 'Semantic version (e.g. 1.0.0)')
22
+ .option('--org <oid>', 'Organization ID (optional, defaults to configured default org)')
23
+ .option('--lifecycle <lifecycle>', 'Version lifecycle status (dev, beta, stable)', 'stable')
24
+ .option('--changelog <changelog>', 'Changelog description for this version')
25
+ .action(async (options) => {
26
+ const formatter = new Formatter({ format: config.resolved.format });
27
+ const resolved = config.resolved;
28
+ const orgId = options.org || resolved.org;
29
+ if (!orgId) {
30
+ formatter.error({
31
+ code: CliErrorCode.MISSING_REQUIRED,
32
+ message: 'Organization ID is required.',
33
+ detail: 'Pass --org <oid> or set a default organization using `apinow orgs set-default <oid>`.',
34
+ });
35
+ process.exit(1);
36
+ }
37
+ // Empathic interactive fallbacks
38
+ let fileId = options.file;
39
+ if (!fileId && process.stdout.isTTY) {
40
+ try {
41
+ const client = getSdkClient();
42
+ const res = await client.files.list(orgId, {
43
+ filter: {
44
+ field: 'kind',
45
+ operator: 'eq',
46
+ value: DomainFileKind,
47
+ },
48
+ }, { token: client.token });
49
+ const files = res.data || [];
50
+ if (files.length > 0) {
51
+ fileId = await askSelect('Select a data domain file to publish:', files.map((f) => ({
52
+ name: `${f.info?.name || 'Unnamed'} (${f.key})`,
53
+ value: f.key,
54
+ })));
55
+ }
56
+ }
57
+ catch (_err) {
58
+ // Ignore list error and fail/prompt below
59
+ }
60
+ }
61
+ if (!fileId) {
62
+ formatter.error({
63
+ code: CliErrorCode.MISSING_REQUIRED,
64
+ message: 'File ID is required.',
65
+ detail: 'Specify --file <fid>.',
66
+ });
67
+ process.exit(1);
68
+ }
69
+ let name = options.name;
70
+ if (!name && process.stdout.isTTY) {
71
+ name = await askInput('Enter catalog entry name:');
72
+ }
73
+ if (!name) {
74
+ formatter.error({
75
+ code: CliErrorCode.MISSING_REQUIRED,
76
+ message: 'Name is required.',
77
+ detail: 'Specify --name <name>.',
78
+ });
79
+ process.exit(1);
80
+ }
81
+ let description = options.description;
82
+ if (!description && process.stdout.isTTY) {
83
+ description = await askInput('Enter catalog entry description:');
84
+ }
85
+ if (!description) {
86
+ formatter.error({
87
+ code: CliErrorCode.MISSING_REQUIRED,
88
+ message: 'Description is required.',
89
+ detail: 'Specify --description <desc>.',
90
+ });
91
+ process.exit(1);
92
+ }
93
+ let scope = options.scope;
94
+ if (!scope && process.stdout.isTTY) {
95
+ scope = await askSelect('Select publish scope:', [
96
+ { name: 'Public (public)', value: 'public' },
97
+ { name: 'Organization (organization)', value: 'organization' },
98
+ { name: 'Private (private)', value: 'private' },
99
+ ]);
100
+ }
101
+ if (!scope) {
102
+ formatter.error({
103
+ code: CliErrorCode.MISSING_REQUIRED,
104
+ message: 'Scope is required.',
105
+ detail: 'Specify --scope <scope> (allowed: public, organization, private).',
106
+ });
107
+ process.exit(1);
108
+ }
109
+ const allowedScopes = ['public', 'organization', 'private'];
110
+ if (!allowedScopes.includes(scope)) {
111
+ formatter.error({
112
+ code: CliErrorCode.INVALID_ARGUMENT,
113
+ message: `Invalid scope '${scope}'.`,
114
+ detail: `Allowed: ${allowedScopes.join(', ')}`,
115
+ });
116
+ process.exit(1);
117
+ }
118
+ let version = options.catalogVersion;
119
+ if (!version && process.stdout.isTTY) {
120
+ version = await askInput('Enter semantic version:', '1.0.0');
121
+ }
122
+ if (!version) {
123
+ formatter.error({
124
+ code: CliErrorCode.MISSING_REQUIRED,
125
+ message: 'Version is required.',
126
+ detail: 'Specify --catalog-version <ver> or -v <ver>.',
127
+ });
128
+ process.exit(1);
129
+ }
130
+ const allowedLifecycles = ['dev', 'beta', 'stable'];
131
+ if (!allowedLifecycles.includes(options.lifecycle)) {
132
+ formatter.error({
133
+ code: CliErrorCode.INVALID_ARGUMENT,
134
+ message: `Invalid lifecycle '${options.lifecycle}'.`,
135
+ detail: `Allowed: ${allowedLifecycles.join(', ')}`,
136
+ });
137
+ process.exit(1);
138
+ }
139
+ try {
140
+ const client = getSdkClient();
141
+ let catalogId = null;
142
+ try {
143
+ const status = await client.dataCatalog.checkPublicationStatus(fileId, orgId, {
144
+ token: client.token,
145
+ });
146
+ catalogId = status.key;
147
+ }
148
+ catch (err) {
149
+ const apiErr = handleApiError(err);
150
+ if (apiErr.status !== 404) {
151
+ throw err;
152
+ }
153
+ }
154
+ if (!catalogId) {
155
+ if (config.resolved.format !== 'json') {
156
+ console.log('Catalog entry does not exist. Creating new catalog entry...');
157
+ }
158
+ const entry = {
159
+ key: '',
160
+ kind: DataCatalogKind,
161
+ organization: orgId,
162
+ file: fileId,
163
+ scope: scope,
164
+ name: name,
165
+ description: description,
166
+ publishedBy: '',
167
+ publishedAt: 0,
168
+ createdAt: 0,
169
+ updatedAt: 0,
170
+ tags: [],
171
+ };
172
+ const createdRecord = await client.dataCatalog.publish(entry, { token: client.token });
173
+ catalogId = createdRecord.key;
174
+ }
175
+ else {
176
+ if (config.resolved.format !== 'json') {
177
+ console.log(`Catalog entry already exists (ID: ${catalogId}). Publishing new version...`);
178
+ }
179
+ }
180
+ const versionPayload = DataCatalogVersion.createSchema({
181
+ scope: scope,
182
+ lifecycle: options.lifecycle,
183
+ version: version,
184
+ changelog: options.changelog || undefined,
185
+ });
186
+ const versionData = await client.dataCatalog.publishVersion(catalogId, versionPayload, {
187
+ token: client.token,
188
+ });
189
+ formatter.success(`Successfully published version '${version}' to the data catalog!`, versionData);
190
+ }
191
+ catch (err) {
192
+ const apiErr = handleApiError(err);
193
+ formatter.error(apiErr);
194
+ process.exit(1);
195
+ }
196
+ });
197
+ catalogCmd
198
+ .command('list')
199
+ .description('List published data catalog items or versions of a specific catalog entry')
200
+ .option('--scope <scope>', 'Filter by scope: all, public, organization, private', 'all')
201
+ .option('--org <oid>', 'Organization ID (optional, defaults to configured default org)')
202
+ .option('--key <key>', 'Data catalog item key to list all versions of a specific data domain')
203
+ .action(async (options) => {
204
+ const formatter = new Formatter({ format: config.resolved.format });
205
+ const resolved = config.resolved;
206
+ const orgId = options.org || resolved.org;
207
+ const allowedScopes = ['all', 'public', 'organization', 'private'];
208
+ if (!allowedScopes.includes(options.scope)) {
209
+ formatter.error({
210
+ message: `Invalid scope '${options.scope}'. Allowed: ${allowedScopes.join(', ')}`,
211
+ });
212
+ process.exit(1);
213
+ }
214
+ if (['organization', 'private'].includes(options.scope) && !orgId) {
215
+ formatter.error({
216
+ message: `Organization ID (--org <oid>) is required when filtering by scope '${options.scope}'.`,
217
+ });
218
+ process.exit(1);
219
+ }
220
+ try {
221
+ const client = getSdkClient();
222
+ if (options.key) {
223
+ const res = await client.dataCatalog.listVersions(options.key, { token: client.token });
224
+ const versions = res.data || [];
225
+ const rows = versions.map((v) => ({
226
+ key: v.key,
227
+ version: v.version,
228
+ published: new Date(v.published).toLocaleString(),
229
+ }));
230
+ formatter.list(rows, ['key', 'version', 'published'], {
231
+ headers: ['VERSION KEY', 'VERSION', 'PUBLISHED AT'],
232
+ });
233
+ }
234
+ else {
235
+ const results = [];
236
+ if (options.scope === 'all') {
237
+ const resPublic = await client.dataCatalog.list({ scope: 'public', oid: orgId }, { token: client.token });
238
+ results.push(...(resPublic.data || []));
239
+ if (orgId) {
240
+ try {
241
+ const resOrg = await client.dataCatalog.list({ scope: 'organization', oid: orgId }, { token: client.token });
242
+ results.push(...(resOrg.data || []));
243
+ }
244
+ catch {
245
+ // Ignore if organization query fails
246
+ }
247
+ try {
248
+ const resPrivate = await client.dataCatalog.list({ scope: 'private', oid: orgId }, { token: client.token });
249
+ results.push(...(resPrivate.data || []));
250
+ }
251
+ catch {
252
+ // Ignore if private query fails
253
+ }
254
+ }
255
+ }
256
+ else {
257
+ const res = await client.dataCatalog.list({ scope: options.scope, oid: orgId }, { token: client.token });
258
+ results.push(...(res.data || []));
259
+ }
260
+ const seen = new Set();
261
+ const uniqueResults = results.filter((item) => {
262
+ const key = item.key;
263
+ if (seen.has(key))
264
+ return false;
265
+ seen.add(key);
266
+ return true;
267
+ });
268
+ const rows = uniqueResults.map((item) => {
269
+ const lastVersion = item.versions && item.versions[0] ? item.versions[0].version : 'N/A';
270
+ return {
271
+ key: item.key,
272
+ name: item.name,
273
+ scope: item.scope,
274
+ lastVersion,
275
+ organization: item.organization || 'N/A',
276
+ };
277
+ });
278
+ formatter.list(rows, ['key', 'name', 'scope', 'lastVersion', 'organization'], {
279
+ headers: ['KEY', 'NAME', 'SCOPE', 'LAST VERSION', 'ORGANIZATION'],
280
+ });
281
+ }
282
+ }
283
+ catch (err) {
284
+ const apiErr = handleApiError(err);
285
+ formatter.error(apiErr);
286
+ process.exit(1);
287
+ }
288
+ });
289
+ }
@@ -0,0 +1,59 @@
1
+ import { config } from '../utils/config.js';
2
+ import { Formatter, CliErrorCode } from '../utils/formatter.js';
3
+ /**
4
+ * Registers configuration management commands with the main Commander program.
5
+ *
6
+ * @param program - The root Commander program.
7
+ */
8
+ export function registerConfigCommands(program) {
9
+ const configCmd = program.command('config').description('Manage CLI configuration settings');
10
+ configCmd
11
+ .command('get <key>')
12
+ .description('Get a configuration value')
13
+ .action((key) => {
14
+ const formatter = new Formatter({ format: config.resolved.format });
15
+ const configObj = config.resolved;
16
+ if (key in configObj) {
17
+ const val = configObj[key];
18
+ if (config.resolved.format === 'json') {
19
+ formatter.object({ [key]: val });
20
+ }
21
+ else {
22
+ console.log(val);
23
+ }
24
+ }
25
+ else {
26
+ formatter.error({
27
+ code: CliErrorCode.INVALID_ARGUMENT,
28
+ message: `Configuration key '${key}' not found.`,
29
+ detail: `Run \`apinow config set ${key} <value>\` to define it.`,
30
+ });
31
+ process.exit(1);
32
+ }
33
+ });
34
+ configCmd
35
+ .command('set <key> <value>')
36
+ .description('Set a configuration value')
37
+ .action((key, value) => {
38
+ const formatter = new Formatter({ format: config.resolved.format });
39
+ const allowedKeys = ['apiUrl', 'token', 'org'];
40
+ if (!allowedKeys.includes(key)) {
41
+ formatter.error({
42
+ code: CliErrorCode.INVALID_ARGUMENT,
43
+ message: `Invalid configuration key '${key}'.`,
44
+ detail: `Allowed keys: ${allowedKeys.join(', ')}`,
45
+ });
46
+ process.exit(1);
47
+ }
48
+ config.writeProperty(key, value);
49
+ formatter.success(`Configuration '${key}' set to '${value}'.`);
50
+ });
51
+ configCmd
52
+ .command('reset')
53
+ .description('Reset all configuration settings and remove stored files')
54
+ .action(() => {
55
+ const formatter = new Formatter({ format: config.resolved.format });
56
+ config.reset();
57
+ formatter.success('Configuration has been reset successfully.');
58
+ });
59
+ }
@@ -0,0 +1,293 @@
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
+ }