apiskill 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/MCP.md +12 -0
- package/README.ja.md +108 -0
- package/README.ko.md +108 -0
- package/README.md +119 -0
- package/README.zh.md +119 -0
- package/dist/assets/index-DH0wsJCI.js +299 -0
- package/dist/assets/index-vocUDpcf.css +1 -0
- package/dist/index.html +13 -0
- package/docs/cli.ja.md +90 -0
- package/docs/cli.ko.md +90 -0
- package/docs/cli.md +117 -0
- package/docs/cli.zh.md +117 -0
- package/docs/mcp.ja.md +79 -0
- package/docs/mcp.ko.md +79 -0
- package/docs/mcp.md +79 -0
- package/docs/mcp.zh.md +79 -0
- package/docs/web.ja.md +44 -0
- package/docs/web.ko.md +44 -0
- package/docs/web.md +57 -0
- package/docs/web.zh.md +57 -0
- package/index.html +12 -0
- package/mcp-config.example.json +13 -0
- package/package.json +44 -0
- package/scripts/apiskill-cli.mjs +372 -0
- package/scripts/lib/apiskill-core.mjs +520 -0
- package/scripts/lib/mock-server.mjs +262 -0
- package/scripts/lib/openapi-importer.mjs +169 -0
- package/scripts/lib/openapi-store.mjs +542 -0
- package/scripts/mcp-server.mjs +408 -0
- package/skills/apiskill/SKILL.md +71 -0
- package/skills/apiskill/agents/openai.yaml +4 -0
- package/src/AddApiDialog.tsx +590 -0
- package/src/App.tsx +2570 -0
- package/src/DocumentVersionManager.tsx +264 -0
- package/src/main.tsx +10 -0
- package/src/manualApiConfig.ts +401 -0
- package/src/styles.css +2101 -0
- package/src/swagger.ts +664 -0
- package/src/types.ts +115 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +1380 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import {
|
|
7
|
+
createBlankDocument,
|
|
8
|
+
endpointToManualConfig,
|
|
9
|
+
findEndpoint,
|
|
10
|
+
formatManualCli,
|
|
11
|
+
listCachedVersions,
|
|
12
|
+
listEndpoints,
|
|
13
|
+
parseManualCliText,
|
|
14
|
+
readCachedDocument,
|
|
15
|
+
saveImportedDocument,
|
|
16
|
+
saveManualOperation,
|
|
17
|
+
deleteManualOperation,
|
|
18
|
+
} from './lib/openapi-store.mjs';
|
|
19
|
+
import { crawlOpenApi, importFromCurl, importFromLocalFile, importFromUrl } from './lib/openapi-importer.mjs';
|
|
20
|
+
import { checkCache, formatCheckText, queryEndpoints } from './lib/apiskill-core.mjs';
|
|
21
|
+
import { startMockServer } from './lib/mock-server.mjs';
|
|
22
|
+
|
|
23
|
+
const program = new Command();
|
|
24
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.name('apiskill')
|
|
28
|
+
.description('API Skill CLI: import OpenAPI docs and create/query/edit/delete local API configs.')
|
|
29
|
+
.version('0.1.0', '-V, --cli-version', 'Print CLI version')
|
|
30
|
+
.showHelpAfterError()
|
|
31
|
+
.addHelpText(
|
|
32
|
+
'after',
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
apiskill import https://example.com/openapi.yaml
|
|
37
|
+
apiskill crawl https://example.com/swagger
|
|
38
|
+
apiskill import-file ./openapi.json
|
|
39
|
+
apiskill import-curl --file ./request.curl
|
|
40
|
+
apiskill run web
|
|
41
|
+
apiskill mock
|
|
42
|
+
apiskill document create --title "My API" --doc-version 1.0.0
|
|
43
|
+
apiskill check
|
|
44
|
+
apiskill versions
|
|
45
|
+
apiskill query /admin/api/v1/activity/activity/statistics/date/list
|
|
46
|
+
apiskill query /admin/api/v1/activity --method GET
|
|
47
|
+
apiskill api list --query user --method post
|
|
48
|
+
apiskill api query GET /api/v1/user --format cli
|
|
49
|
+
apiskill api create --file ./api-config.yaml
|
|
50
|
+
apiskill api edit GET /api/v1/user --file ./api-config.json --version 20260429T000000Z-manual-user
|
|
51
|
+
apiskill api delete GET /api/v1/user --version 20260429T000000Z-manual-user
|
|
52
|
+
|
|
53
|
+
CLI config format:
|
|
54
|
+
{"api":{"method":"post","path":"/api/v1/example","summary":"创建示例","responses":[{"status":"200"}]}}
|
|
55
|
+
YAML is also accepted with root key api:, config:, or operation:.
|
|
56
|
+
`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const run = program.command('run').description('Run API Skill local services.');
|
|
60
|
+
|
|
61
|
+
run
|
|
62
|
+
.command('web')
|
|
63
|
+
.description('Start the API Skill web console from the CLI.')
|
|
64
|
+
.option('-p, --port <number>', 'Web server port', '8888')
|
|
65
|
+
.option('--host <host>', 'Host to bind', '127.0.0.1')
|
|
66
|
+
.option('--strict-port', 'Fail if the requested port is already in use')
|
|
67
|
+
.option('--cwd <dir>', 'Project directory used for the default cache location')
|
|
68
|
+
.option('--cache-dir <dir>', 'Cache directory. Defaults to <cwd>/cache')
|
|
69
|
+
.action(async (options) => {
|
|
70
|
+
await runWeb(options);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
registerMockCommand(run.command('mock').description('Start a local mock API server from the cached OpenAPI document.'));
|
|
74
|
+
|
|
75
|
+
registerMockCommand(program.command('mock').description('Start a local mock API server from the cached OpenAPI document.'));
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.command('check')
|
|
79
|
+
.description('Check whether a usable cached OpenAPI document is configured. Prints import examples when no cache is available.')
|
|
80
|
+
.option('-j, --json', 'Print JSON')
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
const result = await checkCache();
|
|
83
|
+
print(options.json ? result : formatCheckText(result));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
program
|
|
87
|
+
.command('query')
|
|
88
|
+
.description('Query API operations by path or keyword. A single exact match prints API CLI config; multiple matches print a list.')
|
|
89
|
+
.argument('<pathOrKeyword>', 'Exact API path or search keyword')
|
|
90
|
+
.option('-m, --method <method>', 'Optional HTTP method filter: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD')
|
|
91
|
+
.option('-v, --version <versionId>', 'Version id. Defaults to latest.')
|
|
92
|
+
.option('-l, --limit <number>', 'Maximum list rows when multiple endpoints match', '20')
|
|
93
|
+
.option('-j, --json', 'Print JSON')
|
|
94
|
+
.addHelpText(
|
|
95
|
+
'after',
|
|
96
|
+
`
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
apiskill query /admin/api/v1/activity/activity/statistics/date/list
|
|
100
|
+
apiskill query /admin/api/v1/activity/activity/statistics/date/list --method GET
|
|
101
|
+
apiskill query activity --method POST --limit 10
|
|
102
|
+
|
|
103
|
+
Behavior:
|
|
104
|
+
- If one endpoint matches exactly, prints API CLI JSON for that endpoint.
|
|
105
|
+
- If multiple endpoints match, prints a concise candidate list.
|
|
106
|
+
- Use --method GET/POST/DELETE/etc. to narrow results.
|
|
107
|
+
`,
|
|
108
|
+
)
|
|
109
|
+
.action(async (pathOrKeyword, options) => {
|
|
110
|
+
const { document } = await readCachedDocument(options.version);
|
|
111
|
+
const result = queryEndpoints(document, pathOrKeyword, options);
|
|
112
|
+
if (result.kind === 'single') {
|
|
113
|
+
print(options.json ? result.endpoint : formatManualCli(endpointToManualConfig(result.endpoint, document)));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
print(options.json ? result.endpoints : result.endpoints.map((endpoint) => `${endpoint.method.toUpperCase()}\t${endpoint.path}\t${endpoint.summary}`).join('\n'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command('versions')
|
|
121
|
+
.description('List cached OpenAPI document versions.')
|
|
122
|
+
.option('-j, --json', 'Print JSON')
|
|
123
|
+
.action(async (options) => {
|
|
124
|
+
const versions = await listCachedVersions();
|
|
125
|
+
print(options.json ? versions : versions.map((item) => `${item.versionId}\t${item.mode || ''}\t${item.paths ?? 0} paths\t${item.title || item.inputUrl || ''}`).join('\n'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
program
|
|
129
|
+
.command('import')
|
|
130
|
+
.description('Import a direct OpenAPI/Swagger JSON or YAML URL.')
|
|
131
|
+
.argument('<url>', 'OpenAPI JSON/YAML URL')
|
|
132
|
+
.option('--auth <username:password>', 'Basic auth credentials')
|
|
133
|
+
.option('-j, --json', 'Print JSON')
|
|
134
|
+
.action(async (url, options) => {
|
|
135
|
+
const result = await importFromUrl(url, { basicAuth: options.auth });
|
|
136
|
+
const saved = await saveImportedDocument({ document: result.document, mode: 'file', inputUrl: url, resolvedUrl: result.resolvedUrl });
|
|
137
|
+
printResult(saved, options.json);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
program
|
|
141
|
+
.command('crawl')
|
|
142
|
+
.description('Crawl Swagger UI / Knife4j / Redoc page and import discovered OpenAPI document.')
|
|
143
|
+
.argument('<url>', 'Online API document page URL')
|
|
144
|
+
.option('--auth <username:password>', 'Basic auth credentials')
|
|
145
|
+
.option('-j, --json', 'Print JSON')
|
|
146
|
+
.action(async (url, options) => {
|
|
147
|
+
const result = await crawlOpenApi(url, { basicAuth: options.auth });
|
|
148
|
+
const saved = await saveImportedDocument({ document: result.document, mode: 'crawl', inputUrl: url, resolvedUrl: result.resolvedUrl });
|
|
149
|
+
printResult(saved, options.json);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
program
|
|
153
|
+
.command('import-file')
|
|
154
|
+
.description('Import a local OpenAPI/Swagger JSON or YAML file.')
|
|
155
|
+
.argument('<file>', 'Local JSON/YAML file path')
|
|
156
|
+
.option('-j, --json', 'Print JSON')
|
|
157
|
+
.action(async (file, options) => {
|
|
158
|
+
const result = await importFromLocalFile(file);
|
|
159
|
+
const saved = await saveImportedDocument({ document: result.document, mode: 'upload', inputUrl: file, resolvedUrl: result.resolvedUrl });
|
|
160
|
+
printResult(saved, options.json);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
program
|
|
164
|
+
.command('import-curl')
|
|
165
|
+
.description('Execute a curl command and import JSON/YAML OpenAPI response.')
|
|
166
|
+
.option('-c, --command <curl>', 'Curl command text')
|
|
167
|
+
.option('-f, --file <file>', 'Read curl command from file')
|
|
168
|
+
.option('-j, --json', 'Print JSON')
|
|
169
|
+
.action(async (options) => {
|
|
170
|
+
const curlText = options.file ? await readFile(options.file, 'utf8') : options.command;
|
|
171
|
+
if (!curlText) throw new Error('请通过 --command 或 --file 提供 curl 命令');
|
|
172
|
+
const result = await importFromCurl(curlText);
|
|
173
|
+
const saved = await saveImportedDocument({ document: result.document, mode: 'curl', inputUrl: curlText, resolvedUrl: result.resolvedUrl });
|
|
174
|
+
printResult(saved, options.json);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const document = program.command('document').alias('doc').description('Create and maintain local OpenAPI documents.');
|
|
178
|
+
|
|
179
|
+
document
|
|
180
|
+
.command('create')
|
|
181
|
+
.description('Create a blank local OpenAPI document version for AI/CLI/MCP-driven API authoring.')
|
|
182
|
+
.option('-t, --title <title>', 'Document title', 'API Skill Document')
|
|
183
|
+
.option('--doc-version <version>', 'OpenAPI info.version value', '1.0.0')
|
|
184
|
+
.option('-d, --description <description>', 'Document description', 'Created from API Skill blank document.')
|
|
185
|
+
.option('--environment-name <name>', 'Optional environment name metadata')
|
|
186
|
+
.option('--environment-base-url <url>', 'Optional environment base URL metadata')
|
|
187
|
+
.option('-j, --json', 'Print JSON')
|
|
188
|
+
.action(async (options) => {
|
|
189
|
+
const saved = await createBlankDocument({
|
|
190
|
+
title: options.title,
|
|
191
|
+
version: options.docVersion,
|
|
192
|
+
description: options.description,
|
|
193
|
+
environmentName: options.environmentName,
|
|
194
|
+
environmentBaseUrl: options.environmentBaseUrl,
|
|
195
|
+
});
|
|
196
|
+
printResult(saved, options.json);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const api = program.command('api').description('Create, query, edit, and delete API operations in local versions.');
|
|
200
|
+
|
|
201
|
+
api
|
|
202
|
+
.command('list')
|
|
203
|
+
.description('List/search API operations from a cached version.')
|
|
204
|
+
.option('-v, --version <versionId>', 'Version id. Defaults to latest.')
|
|
205
|
+
.option('-q, --query <keyword>', 'Search keyword')
|
|
206
|
+
.option('-m, --method <method>', 'HTTP method filter')
|
|
207
|
+
.option('-l, --limit <number>', 'Maximum rows', '50')
|
|
208
|
+
.option('-j, --json', 'Print JSON')
|
|
209
|
+
.action(async (options) => {
|
|
210
|
+
const { document } = await readCachedDocument(options.version);
|
|
211
|
+
const query = String(options.query || '').toLowerCase();
|
|
212
|
+
const method = String(options.method || '').toLowerCase();
|
|
213
|
+
const limit = Number(options.limit) || 50;
|
|
214
|
+
const endpoints = listEndpoints(document)
|
|
215
|
+
.filter((endpoint) => !query || endpoint.searchable.includes(query))
|
|
216
|
+
.filter((endpoint) => !method || endpoint.method === method)
|
|
217
|
+
.slice(0, limit);
|
|
218
|
+
print(options.json ? endpoints : endpoints.map((endpoint) => `${endpoint.method.toUpperCase()}\t${endpoint.path}\t${endpoint.summary}`).join('\n'));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
api
|
|
222
|
+
.command('query')
|
|
223
|
+
.description('Get one API operation.')
|
|
224
|
+
.argument('<method>', 'HTTP method')
|
|
225
|
+
.argument('<path>', 'OpenAPI path')
|
|
226
|
+
.option('-v, --version <versionId>', 'Version id. Defaults to latest.')
|
|
227
|
+
.option('-f, --format <json|cli|raw>', 'Output format', 'json')
|
|
228
|
+
.action(async (method, path, options) => {
|
|
229
|
+
const { document } = await readCachedDocument(options.version);
|
|
230
|
+
const endpoint = findEndpoint(document, method, path);
|
|
231
|
+
if (!endpoint) throw new Error(`接口不存在: ${method.toUpperCase()} ${path}`);
|
|
232
|
+
if (options.format === 'cli') print(formatManualCli(endpointToManualConfig(endpoint, document)));
|
|
233
|
+
else if (options.format === 'raw') print(endpoint.operation);
|
|
234
|
+
else print(endpoint);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
api
|
|
238
|
+
.command('create')
|
|
239
|
+
.description('Create an API operation. If --version is omitted, a new manual version is created.')
|
|
240
|
+
.option('-v, --version <versionId>', 'Version id to append to')
|
|
241
|
+
.option('-f, --file <file>', 'JSON/YAML/CLI config file')
|
|
242
|
+
.option('-c, --config <text>', 'Inline JSON/YAML/CLI config')
|
|
243
|
+
.option('-j, --json', 'Print JSON')
|
|
244
|
+
.action(async (options) => {
|
|
245
|
+
const config = await readConfig(options);
|
|
246
|
+
const saved = await saveManualOperation({ versionId: options.version, config });
|
|
247
|
+
printResult(saved, options.json);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
api
|
|
251
|
+
.command('edit')
|
|
252
|
+
.description('Edit/replace one API operation.')
|
|
253
|
+
.argument('<method>', 'Original HTTP method')
|
|
254
|
+
.argument('<path>', 'Original OpenAPI path')
|
|
255
|
+
.option('-f, --file <file>', 'JSON/YAML/CLI config file')
|
|
256
|
+
.option('-c, --config <text>', 'Inline JSON/YAML/CLI config')
|
|
257
|
+
.option('-v, --version <versionId>', 'Version id. Defaults to latest.')
|
|
258
|
+
.option('-j, --json', 'Print JSON')
|
|
259
|
+
.action(async (method, path, options) => {
|
|
260
|
+
const versionId = options.version || (await readCachedDocument()).meta?.versionId;
|
|
261
|
+
const config = await readConfig(options);
|
|
262
|
+
const saved = await saveManualOperation({ versionId, config, replaceTarget: { method, path } });
|
|
263
|
+
printResult(saved, options.json);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
api
|
|
267
|
+
.command('delete')
|
|
268
|
+
.description('Delete one API operation from a cached version.')
|
|
269
|
+
.argument('<method>', 'HTTP method')
|
|
270
|
+
.argument('<path>', 'OpenAPI path')
|
|
271
|
+
.option('-v, --version <versionId>', 'Version id. Defaults to latest.')
|
|
272
|
+
.option('-j, --json', 'Print JSON')
|
|
273
|
+
.action(async (method, path, options) => {
|
|
274
|
+
const versionId = options.version || (await readCachedDocument()).meta?.versionId;
|
|
275
|
+
const saved = await deleteManualOperation({ versionId, method, path });
|
|
276
|
+
printResult(saved, options.json);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
program.parseAsync().catch((error) => {
|
|
280
|
+
console.error(error instanceof Error ? error.message : error);
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
function registerMockCommand(command) {
|
|
285
|
+
command
|
|
286
|
+
.option('-p, --port <number>', 'Mock server port', '4010')
|
|
287
|
+
.option('--host <host>', 'Host to bind', '127.0.0.1')
|
|
288
|
+
.option('-v, --version <versionId>', 'Version id. Defaults to latest.')
|
|
289
|
+
.option('-j, --json', 'Print startup info as JSON')
|
|
290
|
+
.action(async (options) => {
|
|
291
|
+
await runMock(options);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function runWeb(options) {
|
|
296
|
+
const projectRoot = resolve(options.cwd || process.cwd());
|
|
297
|
+
const cacheDir = options.cacheDir ? resolve(projectRoot, options.cacheDir) : process.env.APISKILL_CACHE_DIR || resolve(projectRoot, 'cache');
|
|
298
|
+
const port = Number(options.port) || 8888;
|
|
299
|
+
const host = options.host || '127.0.0.1';
|
|
300
|
+
|
|
301
|
+
process.env.APISKILL_ROOT = projectRoot;
|
|
302
|
+
process.env.APISKILL_CACHE_DIR = cacheDir;
|
|
303
|
+
|
|
304
|
+
const { createServer } = await import('vite');
|
|
305
|
+
const server = await createServer({
|
|
306
|
+
root: packageRoot,
|
|
307
|
+
configFile: resolve(packageRoot, 'vite.config.ts'),
|
|
308
|
+
clearScreen: false,
|
|
309
|
+
server: {
|
|
310
|
+
host,
|
|
311
|
+
port,
|
|
312
|
+
strictPort: Boolean(options.strictPort),
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await server.listen();
|
|
317
|
+
console.log(`API Skill web is using cache: ${cacheDir}`);
|
|
318
|
+
server.printUrls();
|
|
319
|
+
|
|
320
|
+
const close = async () => {
|
|
321
|
+
await server.close();
|
|
322
|
+
process.exit(0);
|
|
323
|
+
};
|
|
324
|
+
process.once('SIGINT', close);
|
|
325
|
+
process.once('SIGTERM', close);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function runMock(options) {
|
|
329
|
+
let cached;
|
|
330
|
+
try {
|
|
331
|
+
cached = await readCachedDocument(options.version);
|
|
332
|
+
} catch {
|
|
333
|
+
throw new Error('当前没有可用的 API 文档数据,请先导入、爬取或新建文档后再启动 MOCK 服务');
|
|
334
|
+
}
|
|
335
|
+
const { document, meta } = cached;
|
|
336
|
+
const mock = await startMockServer({
|
|
337
|
+
document,
|
|
338
|
+
meta,
|
|
339
|
+
host: options.host || '127.0.0.1',
|
|
340
|
+
port: Number(options.port) || 4010,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const info = {
|
|
344
|
+
url: mock.url,
|
|
345
|
+
versionId: mock.versionId,
|
|
346
|
+
title: mock.title,
|
|
347
|
+
routes: mock.routesCount,
|
|
348
|
+
};
|
|
349
|
+
print(options.json ? info : `API Skill MOCK 服务已启动: ${mock.url}\n版本: ${mock.versionId || 'latest'}\n接口: ${mock.routesCount}\n按 Ctrl+C 停止服务`);
|
|
350
|
+
|
|
351
|
+
const close = async () => {
|
|
352
|
+
await new Promise((resolve) => mock.server.close(resolve));
|
|
353
|
+
process.exit(0);
|
|
354
|
+
};
|
|
355
|
+
process.once('SIGINT', close);
|
|
356
|
+
process.once('SIGTERM', close);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function readConfig(options) {
|
|
360
|
+
const text = options.file ? await readFile(options.file, 'utf8') : options.config;
|
|
361
|
+
if (!text) throw new Error('请通过 --file 或 --config 提供 API CLI 配置');
|
|
362
|
+
return parseManualCliText(text);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function printResult(saved, json) {
|
|
366
|
+
print(json ? saved : `saved ${saved.meta.versionId}\npaths ${saved.meta.paths}\nschemas ${saved.meta.schemas}\nfile ${saved.meta.savedPath}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function print(value) {
|
|
370
|
+
if (typeof value === 'string') console.log(value);
|
|
371
|
+
else console.log(JSON.stringify(value, null, 2));
|
|
372
|
+
}
|