api-emulator 0.5.2 → 0.7.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/README.md +61 -22
- package/dist/api.js +8 -8
- package/dist/api.js.map +1 -1
- package/dist/index.js +484 -97
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6,14 +6,13 @@ import { Command } from "commander";
|
|
|
6
6
|
import { isAbsolute, resolve } from "path";
|
|
7
7
|
|
|
8
8
|
// src/plugin-manifest.ts
|
|
9
|
+
function formatPluginFidelity(fidelity) {
|
|
10
|
+
if (!fidelity) return "unrated";
|
|
11
|
+
if (typeof fidelity === "string") return fidelity;
|
|
12
|
+
return fidelity.level;
|
|
13
|
+
}
|
|
9
14
|
function readPluginManifest(mod) {
|
|
10
|
-
return {
|
|
11
|
-
label: mod.label,
|
|
12
|
-
endpoints: mod.endpoints,
|
|
13
|
-
initConfig: mod.initConfig,
|
|
14
|
-
contract: mod.contract,
|
|
15
|
-
...mod.manifest
|
|
16
|
-
};
|
|
15
|
+
return mod.manifest ?? {};
|
|
17
16
|
}
|
|
18
17
|
function validatePluginManifest(manifest, pluginName) {
|
|
19
18
|
if (manifest.name && manifest.name !== pluginName) {
|
|
@@ -31,20 +30,21 @@ function validatePluginManifest(manifest, pluginName) {
|
|
|
31
30
|
async function loadExternalPluginModule(specifier) {
|
|
32
31
|
const modulePath = specifier.startsWith(".") || isAbsolute(specifier) ? resolve(specifier) : specifier;
|
|
33
32
|
const mod = await import(modulePath);
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
33
|
+
const plugin2 = mod.plugin ?? mod.default;
|
|
34
|
+
if (!plugin2 || typeof plugin2.register !== "function" || typeof plugin2.name !== "string") {
|
|
36
35
|
throw new Error(`Plugin "${specifier}" must export a ServicePlugin (as "plugin" or default export)`);
|
|
37
36
|
}
|
|
38
|
-
const name =
|
|
37
|
+
const name = plugin2.name;
|
|
39
38
|
const manifest = validatePluginManifest(readPluginManifest(mod), name);
|
|
40
39
|
return {
|
|
41
40
|
name,
|
|
42
41
|
label: manifest.label,
|
|
43
42
|
endpoints: manifest.endpoints,
|
|
43
|
+
fidelity: formatPluginFidelity(manifest.fidelity),
|
|
44
44
|
manifest,
|
|
45
45
|
async load() {
|
|
46
46
|
return {
|
|
47
|
-
plugin,
|
|
47
|
+
plugin: plugin2,
|
|
48
48
|
seedFromConfig: mod.seedFromConfig
|
|
49
49
|
};
|
|
50
50
|
},
|
|
@@ -124,6 +124,7 @@ import pc from "picocolors";
|
|
|
124
124
|
|
|
125
125
|
// src/portless.ts
|
|
126
126
|
import { execSync, spawnSync } from "child_process";
|
|
127
|
+
import { Socket } from "net";
|
|
127
128
|
import { createInterface } from "readline";
|
|
128
129
|
function isInteractive() {
|
|
129
130
|
return Boolean(process.stdin.isTTY) && !process.env.CI;
|
|
@@ -133,32 +134,60 @@ function hasPortless() {
|
|
|
133
134
|
return result.status === 0;
|
|
134
135
|
}
|
|
135
136
|
function promptYesNo(question) {
|
|
136
|
-
return new Promise((
|
|
137
|
+
return new Promise((resolve9) => {
|
|
137
138
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
138
139
|
rl.question(question, (answer) => {
|
|
139
140
|
rl.close();
|
|
140
141
|
const normalized = answer.trim().toLowerCase();
|
|
141
|
-
|
|
142
|
+
resolve9(normalized === "" || normalized === "y" || normalized === "yes");
|
|
142
143
|
});
|
|
143
144
|
});
|
|
144
145
|
}
|
|
145
|
-
function
|
|
146
|
-
const result = spawnSync("portless", ["
|
|
147
|
-
|
|
146
|
+
function portlessUrl(name) {
|
|
147
|
+
const result = spawnSync("portless", ["get", name], { encoding: "utf-8" });
|
|
148
|
+
if (result.status !== 0) return null;
|
|
149
|
+
const url = result.stdout.trim();
|
|
150
|
+
return url.length > 0 ? url : null;
|
|
151
|
+
}
|
|
152
|
+
async function canConnect(port) {
|
|
153
|
+
return new Promise((resolve9) => {
|
|
154
|
+
const socket = new Socket();
|
|
155
|
+
const done = (ok) => {
|
|
156
|
+
socket.destroy();
|
|
157
|
+
resolve9(ok);
|
|
158
|
+
};
|
|
159
|
+
socket.setTimeout(500);
|
|
160
|
+
socket.once("connect", () => done(true));
|
|
161
|
+
socket.once("error", () => done(false));
|
|
162
|
+
socket.once("timeout", () => done(false));
|
|
163
|
+
socket.connect(port, "127.0.0.1");
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function isProxyRunning() {
|
|
167
|
+
const url = portlessUrl("api-emulator-probe");
|
|
168
|
+
if (!url) return false;
|
|
169
|
+
try {
|
|
170
|
+
const parsed = new URL(url);
|
|
171
|
+
const port = parsed.port ? Number(parsed.port) : parsed.protocol === "http:" ? 80 : 443;
|
|
172
|
+
return await canConnect(port);
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
148
176
|
}
|
|
177
|
+
var portlessInstallCommand = "bun add --global portless";
|
|
149
178
|
async function ensurePortless() {
|
|
150
179
|
if (!hasPortless()) {
|
|
151
180
|
if (!isInteractive()) {
|
|
152
|
-
console.error(
|
|
181
|
+
console.error(`portless is required but not installed. Run: ${portlessInstallCommand}`);
|
|
153
182
|
process.exit(1);
|
|
154
183
|
}
|
|
155
|
-
const yes = await promptYesNo(
|
|
184
|
+
const yes = await promptYesNo(`portless is not installed. Install it now? (${portlessInstallCommand}) [Y/n] `);
|
|
156
185
|
if (!yes) {
|
|
157
186
|
console.error("Cannot continue without portless.");
|
|
158
187
|
process.exit(1);
|
|
159
188
|
}
|
|
160
189
|
try {
|
|
161
|
-
execSync(
|
|
190
|
+
execSync(portlessInstallCommand, { stdio: "inherit" });
|
|
162
191
|
} catch {
|
|
163
192
|
console.error("Failed to install portless.");
|
|
164
193
|
process.exit(1);
|
|
@@ -168,7 +197,7 @@ async function ensurePortless() {
|
|
|
168
197
|
process.exit(1);
|
|
169
198
|
}
|
|
170
199
|
}
|
|
171
|
-
if (!isProxyRunning()) {
|
|
200
|
+
if (!await isProxyRunning()) {
|
|
172
201
|
console.error("portless proxy is not running. Start it with: portless proxy start");
|
|
173
202
|
process.exit(1);
|
|
174
203
|
}
|
|
@@ -197,7 +226,7 @@ function removeAliases(aliases) {
|
|
|
197
226
|
}
|
|
198
227
|
}
|
|
199
228
|
function portlessBaseUrl(serviceName) {
|
|
200
|
-
return `https://${serviceName}.api-emulator.localhost`;
|
|
229
|
+
return portlessUrl(`${serviceName}.api-emulator`) ?? `https://${serviceName}.api-emulator.localhost`;
|
|
201
230
|
}
|
|
202
231
|
|
|
203
232
|
// src/base-url.ts
|
|
@@ -208,13 +237,13 @@ function resolveBaseUrl(opts) {
|
|
|
208
237
|
if (opts.baseUrl) {
|
|
209
238
|
return opts.baseUrl.replace(/\{service\}/g, opts.service);
|
|
210
239
|
}
|
|
211
|
-
const envBaseUrl = process.env.API_EMULATOR_BASE_URL
|
|
240
|
+
const envBaseUrl = process.env.API_EMULATOR_BASE_URL;
|
|
212
241
|
if (envBaseUrl) {
|
|
213
242
|
return envBaseUrl.replace(/\{service\}/g, opts.service);
|
|
214
243
|
}
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
return
|
|
244
|
+
const portlessUrl2 = process.env.PORTLESS_URL;
|
|
245
|
+
if (portlessUrl2) {
|
|
246
|
+
return portlessUrl2.replace(/\{service\}/g, opts.service);
|
|
218
247
|
}
|
|
219
248
|
return `http://localhost:${opts.port}`;
|
|
220
249
|
}
|
|
@@ -285,18 +314,36 @@ function createServiceRuntime(options) {
|
|
|
285
314
|
seed();
|
|
286
315
|
},
|
|
287
316
|
close() {
|
|
288
|
-
return new Promise((
|
|
317
|
+
return new Promise((resolve9, reject) => {
|
|
289
318
|
httpServer.close((err) => {
|
|
290
319
|
if (err) reject(err);
|
|
291
|
-
else
|
|
320
|
+
else resolve9();
|
|
292
321
|
});
|
|
293
322
|
});
|
|
294
323
|
}
|
|
295
324
|
};
|
|
296
325
|
}
|
|
297
326
|
|
|
327
|
+
// src/cli-notifier.ts
|
|
328
|
+
import { spawn } from "child_process";
|
|
329
|
+
import { platform } from "os";
|
|
330
|
+
function escapeAppleScript(value) {
|
|
331
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
332
|
+
}
|
|
333
|
+
function appleScriptNotification({ title, message }) {
|
|
334
|
+
return `display notification "${escapeAppleScript(message)}" with title "${escapeAppleScript(title)}"`;
|
|
335
|
+
}
|
|
336
|
+
function notifyIfRequested(options) {
|
|
337
|
+
if (!options.enabled || platform() !== "darwin") return;
|
|
338
|
+
const child = spawn("/usr/bin/osascript", ["-e", appleScriptNotification(options)], {
|
|
339
|
+
detached: true,
|
|
340
|
+
stdio: "ignore"
|
|
341
|
+
});
|
|
342
|
+
child.unref();
|
|
343
|
+
}
|
|
344
|
+
|
|
298
345
|
// src/commands/start.ts
|
|
299
|
-
var pkg = { version: "0.
|
|
346
|
+
var pkg = { version: "0.7.0" };
|
|
300
347
|
function loadSeedConfig(seedPath) {
|
|
301
348
|
if (seedPath) {
|
|
302
349
|
const fullPath = resolve3(seedPath);
|
|
@@ -313,17 +360,7 @@ function loadSeedConfig(seedPath) {
|
|
|
313
360
|
process.exit(1);
|
|
314
361
|
}
|
|
315
362
|
}
|
|
316
|
-
const autoFiles = [
|
|
317
|
-
"api-emulator.config.yaml",
|
|
318
|
-
"api-emulator.config.yml",
|
|
319
|
-
"api-emulator.config.json",
|
|
320
|
-
"emulate.config.yaml",
|
|
321
|
-
"emulate.config.yml",
|
|
322
|
-
"emulate.config.json",
|
|
323
|
-
"service-emulator.config.yaml",
|
|
324
|
-
"service-emulator.config.yml",
|
|
325
|
-
"service-emulator.config.json"
|
|
326
|
-
];
|
|
363
|
+
const autoFiles = ["api-emulator.config.yaml", "api-emulator.config.yml", "api-emulator.config.json"];
|
|
327
364
|
for (const file of autoFiles) {
|
|
328
365
|
const fullPath = resolve3(file);
|
|
329
366
|
if (existsSync2(fullPath)) {
|
|
@@ -394,17 +431,17 @@ async function startCommand(options) {
|
|
|
394
431
|
if (options.portless) {
|
|
395
432
|
portlessAliases.push({ name: `${svc}.api-emulator`, port });
|
|
396
433
|
}
|
|
397
|
-
|
|
398
|
-
const effectiveBaseUrl = options.portless ? portlessBaseUrl(svc) : options.baseUrl;
|
|
399
|
-
const baseUrl = resolveBaseUrl({ service: svc, port, baseUrl: effectiveBaseUrl, seedBaseUrl });
|
|
400
|
-
prepared.push({ svc, pluginModule, loadedPlugin, svcSeedConfig, port, baseUrl });
|
|
434
|
+
prepared.push({ svc, pluginModule, loadedPlugin, svcSeedConfig, port });
|
|
401
435
|
}
|
|
402
436
|
if (portlessAliases.length > 0) {
|
|
403
437
|
registerAliases(portlessAliases);
|
|
404
438
|
}
|
|
405
439
|
const serviceUrls = [];
|
|
406
440
|
const runningServices = [];
|
|
407
|
-
for (const { svc, pluginModule, loadedPlugin, svcSeedConfig, port
|
|
441
|
+
for (const { svc, pluginModule, loadedPlugin, svcSeedConfig, port } of prepared) {
|
|
442
|
+
const seedBaseUrl = typeof svcSeedConfig?.baseUrl === "string" && svcSeedConfig.baseUrl.length > 0 ? svcSeedConfig.baseUrl : void 0;
|
|
443
|
+
const effectiveBaseUrl = options.portless ? portlessBaseUrl(svc) : options.baseUrl;
|
|
444
|
+
const baseUrl = resolveBaseUrl({ service: svc, port, baseUrl: effectiveBaseUrl, seedBaseUrl });
|
|
408
445
|
serviceUrls.push({ name: svc, url: baseUrl });
|
|
409
446
|
const running = createServiceRuntime({
|
|
410
447
|
service: svc,
|
|
@@ -418,6 +455,11 @@ async function startCommand(options) {
|
|
|
418
455
|
runningServices.push(running);
|
|
419
456
|
}
|
|
420
457
|
printBanner(serviceUrls, tokens, configSource);
|
|
458
|
+
notifyIfRequested({
|
|
459
|
+
enabled: options.notify,
|
|
460
|
+
title: "api-emulator",
|
|
461
|
+
message: `Server started with ${serviceUrls.length} service${serviceUrls.length === 1 ? "" : "s"}`
|
|
462
|
+
});
|
|
421
463
|
const shutdown = () => {
|
|
422
464
|
console.log(`
|
|
423
465
|
${pc.dim("Shutting down...")}`);
|
|
@@ -462,13 +504,143 @@ function printBanner(services, tokens, configSource) {
|
|
|
462
504
|
}
|
|
463
505
|
|
|
464
506
|
// src/commands/init.ts
|
|
465
|
-
import { writeFileSync as
|
|
466
|
-
import {
|
|
507
|
+
import { writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
508
|
+
import { homedir } from "os";
|
|
509
|
+
import { resolve as resolve5 } from "path";
|
|
467
510
|
import { stringify as yamlStringify } from "yaml";
|
|
511
|
+
|
|
512
|
+
// src/generated-manifest.ts
|
|
513
|
+
import { createHash } from "crypto";
|
|
514
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
515
|
+
import { dirname, resolve as resolve4 } from "path";
|
|
516
|
+
var GENERATED_MANIFEST_FILE = ".api-emulator/manifest.json";
|
|
517
|
+
function checksum(content) {
|
|
518
|
+
return createHash("sha256").update(content).digest("hex");
|
|
519
|
+
}
|
|
520
|
+
function readGeneratedManifest(cwd = process.cwd()) {
|
|
521
|
+
const path = resolve4(cwd, GENERATED_MANIFEST_FILE);
|
|
522
|
+
if (!existsSync3(path)) return { version: 1, files: {} };
|
|
523
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
524
|
+
if (parsed.version !== 1 || typeof parsed.files !== "object" || parsed.files === null) {
|
|
525
|
+
throw new Error(`Invalid ${GENERATED_MANIFEST_FILE}`);
|
|
526
|
+
}
|
|
527
|
+
return parsed;
|
|
528
|
+
}
|
|
529
|
+
function writeGeneratedManifest(manifest, cwd = process.cwd()) {
|
|
530
|
+
const path = resolve4(cwd, GENERATED_MANIFEST_FILE);
|
|
531
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
532
|
+
const files = Object.fromEntries(Object.entries(manifest.files).sort(([a], [b]) => a.localeCompare(b)));
|
|
533
|
+
writeFileSync2(path, `${JSON.stringify({ version: 1, files }, null, 2)}
|
|
534
|
+
`, "utf-8");
|
|
535
|
+
}
|
|
536
|
+
function writeGeneratedFile(relativePath, content, options) {
|
|
537
|
+
const cwd = options.cwd ?? process.cwd();
|
|
538
|
+
const path = resolve4(cwd, relativePath);
|
|
539
|
+
const manifest = readGeneratedManifest(cwd);
|
|
540
|
+
const existing = manifest.files[relativePath];
|
|
541
|
+
if (existsSync3(path)) {
|
|
542
|
+
const current = readFileSync3(path, "utf-8");
|
|
543
|
+
if (current === content) {
|
|
544
|
+
manifest.files[relativePath] = { checksum: checksum(content), source: options.source };
|
|
545
|
+
writeGeneratedManifest(manifest, cwd);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (!options.yes && (!existing || existing.checksum !== checksum(current))) {
|
|
549
|
+
throw new Error(`Refusing to overwrite user-edited file: ${relativePath}. Re-run with --yes to overwrite.`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
553
|
+
writeFileSync2(path, content, "utf-8");
|
|
554
|
+
manifest.files[relativePath] = { checksum: checksum(content), source: options.source };
|
|
555
|
+
writeGeneratedManifest(manifest, cwd);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/commands/init.ts
|
|
559
|
+
var AGENT_SKILL_DIRS = {
|
|
560
|
+
agents: ".agents/skills",
|
|
561
|
+
claude: ".claude/skills",
|
|
562
|
+
codex: ".codex/skills",
|
|
563
|
+
cursor: ".cursor/skills",
|
|
564
|
+
factory: ".agents/skills",
|
|
565
|
+
global: `${homedir()}/.agents/skills`,
|
|
566
|
+
"user-agents": `${homedir()}/.agents/skills`,
|
|
567
|
+
windsurf: ".windsurf/skills"
|
|
568
|
+
};
|
|
569
|
+
function parseAgentTargets(input, nonInteractive) {
|
|
570
|
+
const values = input?.split(",").map((value) => value.trim()).filter(Boolean) ?? [];
|
|
571
|
+
if (values.length > 0) return values;
|
|
572
|
+
return nonInteractive ? ["agents"] : ["agents"];
|
|
573
|
+
}
|
|
574
|
+
function pluginAuthoringSkill() {
|
|
575
|
+
return `---
|
|
576
|
+
name: api-emulator-plugin-authoring
|
|
577
|
+
description: Create, validate, and install api-emulator provider plugins.
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
# API Emulator Plugin Authoring
|
|
581
|
+
|
|
582
|
+
Use this skill when adding or validating a provider plugin for api-emulator.
|
|
583
|
+
|
|
584
|
+
## Workflow
|
|
585
|
+
|
|
586
|
+
1. Create a provider clone scaffold with \`npx -p api-emulator api clone create <provider>\`.
|
|
587
|
+
2. Add provider-shaped routes, deterministic seed state, and manifest fidelity metadata.
|
|
588
|
+
3. Run \`npx -p api-emulator api validate-plugin <provider>\`.
|
|
589
|
+
4. Run the relevant smoke or test command before calling the plugin done.
|
|
590
|
+
|
|
591
|
+
## Safety
|
|
592
|
+
|
|
593
|
+
- Never call production APIs from smoke tests.
|
|
594
|
+
- Use dummy credentials and temporary CLI homes.
|
|
595
|
+
- Prefer documented base URL overrides before patching SDKs or CLIs.
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
function runtimeSkill() {
|
|
599
|
+
return `---
|
|
600
|
+
name: api-emulator-runtime
|
|
601
|
+
description: Run api-emulator locally with configured provider plugins.
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
# API Emulator Runtime
|
|
605
|
+
|
|
606
|
+
Use this skill when running local API replacements for development or tests.
|
|
607
|
+
|
|
608
|
+
## Commands
|
|
609
|
+
|
|
610
|
+
- \`npx -p api-emulator api list\`
|
|
611
|
+
- \`npx -p api-emulator api init\`
|
|
612
|
+
- \`npx -p api-emulator api install <plugin>\`
|
|
613
|
+
- \`npx -p api-emulator api clone create <provider>\`
|
|
614
|
+
- \`npx -p api-emulator api --service <service>\`
|
|
615
|
+
|
|
616
|
+
Keep seed data deterministic and reset emulator state between tests.
|
|
617
|
+
`;
|
|
618
|
+
}
|
|
619
|
+
function installAgentSkills(options) {
|
|
620
|
+
for (const target of parseAgentTargets(options.agents, options.nonInteractive)) {
|
|
621
|
+
const dir = AGENT_SKILL_DIRS[target];
|
|
622
|
+
if (!dir) {
|
|
623
|
+
throw new Error(`Unknown agent target: ${target}. Available: ${Object.keys(AGENT_SKILL_DIRS).join(", ")}`);
|
|
624
|
+
}
|
|
625
|
+
writeGeneratedFile(`${dir}/api-emulator-plugin-authoring/SKILL.md`, pluginAuthoringSkill(), {
|
|
626
|
+
source: "api init",
|
|
627
|
+
yes: options.yes
|
|
628
|
+
});
|
|
629
|
+
writeGeneratedFile(`${dir}/api-emulator-runtime/SKILL.md`, runtimeSkill(), {
|
|
630
|
+
source: "api init",
|
|
631
|
+
yes: options.yes
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
468
635
|
async function initCommand(options) {
|
|
636
|
+
if (options.agents || options.skillsOnly) {
|
|
637
|
+
installAgentSkills(options);
|
|
638
|
+
console.log("Installed api-emulator agent skills");
|
|
639
|
+
}
|
|
640
|
+
if (options.skillsOnly) return;
|
|
469
641
|
const filename = "api-emulator.config.yaml";
|
|
470
|
-
const fullPath =
|
|
471
|
-
if (
|
|
642
|
+
const fullPath = resolve5(filename);
|
|
643
|
+
if (existsSync4(fullPath)) {
|
|
472
644
|
console.error(`Config file already exists: ${filename}`);
|
|
473
645
|
process.exit(1);
|
|
474
646
|
}
|
|
@@ -490,7 +662,7 @@ async function initCommand(options) {
|
|
|
490
662
|
config = { ...DEFAULT_TOKENS, ...pluginModule.initConfig };
|
|
491
663
|
}
|
|
492
664
|
const content = yamlStringify(config);
|
|
493
|
-
|
|
665
|
+
writeFileSync3(fullPath, content, "utf-8");
|
|
494
666
|
console.log(`Created ${filename}`);
|
|
495
667
|
console.log(`
|
|
496
668
|
Run 'npx -p api-emulator api' to start the emulator.`);
|
|
@@ -504,18 +676,19 @@ async function listCommand(options = {}) {
|
|
|
504
676
|
for (const pluginModule of Object.values(pluginModules)) {
|
|
505
677
|
console.log(` ${pluginModule.name.padEnd(10)}${pluginModule.label}`);
|
|
506
678
|
console.log(` Endpoints: ${pluginModule.endpoints}`);
|
|
679
|
+
console.log(` Fidelity: ${pluginModule.fidelity}`);
|
|
507
680
|
console.log();
|
|
508
681
|
}
|
|
509
682
|
}
|
|
510
683
|
|
|
511
684
|
// src/commands/install.ts
|
|
512
685
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
513
|
-
import { existsSync as
|
|
514
|
-
import { resolve as
|
|
686
|
+
import { existsSync as existsSync6 } from "fs";
|
|
687
|
+
import { resolve as resolve7 } from "path";
|
|
515
688
|
|
|
516
689
|
// src/plugin-source-registry.ts
|
|
517
|
-
import { existsSync as
|
|
518
|
-
import { dirname, join, resolve as
|
|
690
|
+
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
|
|
691
|
+
import { dirname as dirname2, join, resolve as resolve6 } from "path";
|
|
519
692
|
var PLUGIN_SOURCES = {
|
|
520
693
|
cloudflare: {
|
|
521
694
|
name: "cloudflare",
|
|
@@ -533,17 +706,17 @@ var PACKAGE_ENTRY_DIRS = ["api-emulator", "trading-emulator"];
|
|
|
533
706
|
function candidateCatalogRoots(cwd = process.cwd()) {
|
|
534
707
|
const envRoots = process.env.API_EMULATOR_PLUGIN_CATALOGS?.split(",").map((value) => value.trim()).filter(Boolean) ?? [];
|
|
535
708
|
const roots = [...envRoots];
|
|
536
|
-
let current =
|
|
709
|
+
let current = resolve6(cwd);
|
|
537
710
|
for (; ; ) {
|
|
538
711
|
for (const dir of DEFAULT_CATALOG_DIRS) {
|
|
539
712
|
roots.push(join(current, dir));
|
|
540
|
-
roots.push(join(
|
|
713
|
+
roots.push(join(dirname2(current), dir));
|
|
541
714
|
}
|
|
542
|
-
const parent =
|
|
715
|
+
const parent = dirname2(current);
|
|
543
716
|
if (parent === current) break;
|
|
544
717
|
current = parent;
|
|
545
718
|
}
|
|
546
|
-
return [...new Set(roots.map((root) =>
|
|
719
|
+
return [...new Set(roots.map((root) => resolve6(root)))];
|
|
547
720
|
}
|
|
548
721
|
function sourceIdForRoot(root) {
|
|
549
722
|
const base = root.split(/[\\/]/).pop() ?? "local";
|
|
@@ -552,29 +725,29 @@ function sourceIdForRoot(root) {
|
|
|
552
725
|
return base;
|
|
553
726
|
}
|
|
554
727
|
function packageEntrySpecifier(packageRoot, pkg3) {
|
|
555
|
-
if (typeof pkg3.exports === "string") return
|
|
728
|
+
if (typeof pkg3.exports === "string") return resolve6(packageRoot, pkg3.exports);
|
|
556
729
|
if (typeof pkg3.exports === "object" && pkg3.exports !== null && "." in pkg3.exports && typeof pkg3.exports["."] === "string") {
|
|
557
|
-
return
|
|
730
|
+
return resolve6(packageRoot, pkg3.exports["."]);
|
|
558
731
|
}
|
|
559
732
|
if (typeof pkg3.exports === "object" && pkg3.exports !== null && "." in pkg3.exports && typeof pkg3.exports["."] === "object" && pkg3.exports["."] !== null && "import" in pkg3.exports["."] && typeof pkg3.exports["."].import === "string") {
|
|
560
|
-
return
|
|
733
|
+
return resolve6(packageRoot, pkg3.exports["."].import);
|
|
561
734
|
}
|
|
562
|
-
if (typeof pkg3.main === "string") return
|
|
735
|
+
if (typeof pkg3.main === "string") return resolve6(packageRoot, pkg3.main);
|
|
563
736
|
return packageRoot;
|
|
564
737
|
}
|
|
565
738
|
function discoverManifest(root, sourceId) {
|
|
566
739
|
const sources = [];
|
|
567
740
|
for (const manifestFile of CATALOG_MANIFEST_FILES) {
|
|
568
741
|
const manifestPath = join(root, manifestFile);
|
|
569
|
-
if (!
|
|
570
|
-
const manifest = JSON.parse(
|
|
742
|
+
if (!existsSync5(manifestPath)) continue;
|
|
743
|
+
const manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
|
|
571
744
|
for (const [name, entry] of Object.entries(manifest.plugins ?? {})) {
|
|
572
745
|
sources.push({
|
|
573
746
|
name,
|
|
574
747
|
sourceId,
|
|
575
748
|
kind: entry.kind ?? (entry.packageName ? "package" : "file"),
|
|
576
749
|
packageName: entry.packageName,
|
|
577
|
-
specifier: entry.specifier.startsWith(".") ?
|
|
750
|
+
specifier: entry.specifier.startsWith(".") ? resolve6(root, entry.specifier) : entry.specifier,
|
|
578
751
|
description: entry.description ?? `${name} plugin from ${sourceId} catalog`
|
|
579
752
|
});
|
|
580
753
|
}
|
|
@@ -582,31 +755,18 @@ function discoverManifest(root, sourceId) {
|
|
|
582
755
|
return sources;
|
|
583
756
|
}
|
|
584
757
|
function discoverCatalog(root) {
|
|
585
|
-
if (!
|
|
758
|
+
if (!existsSync5(root) || !statSync(root).isDirectory()) return [];
|
|
586
759
|
const sourceId = sourceIdForRoot(root);
|
|
587
760
|
const sources = discoverManifest(root, sourceId);
|
|
588
761
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
589
762
|
if (!entry.isDirectory() || !entry.name.startsWith("@")) continue;
|
|
590
763
|
const name = entry.name.slice(1);
|
|
591
764
|
const pluginRoot = join(root, entry.name);
|
|
592
|
-
for (const entryFile of PLUGIN_ENTRY_FILES) {
|
|
593
|
-
const specifier = join(pluginRoot, entryFile);
|
|
594
|
-
if (existsSync4(specifier)) {
|
|
595
|
-
sources.push({
|
|
596
|
-
name,
|
|
597
|
-
sourceId,
|
|
598
|
-
kind: "file",
|
|
599
|
-
specifier,
|
|
600
|
-
description: `${name} plugin from ${sourceId} catalog`
|
|
601
|
-
});
|
|
602
|
-
break;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
765
|
for (const entryDir of PACKAGE_ENTRY_DIRS) {
|
|
606
766
|
const packageRoot = join(pluginRoot, entryDir);
|
|
607
767
|
const packageJsonPath = join(packageRoot, "package.json");
|
|
608
|
-
if (!
|
|
609
|
-
const pkg3 = JSON.parse(
|
|
768
|
+
if (!existsSync5(packageJsonPath)) continue;
|
|
769
|
+
const pkg3 = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
|
|
610
770
|
sources.push({
|
|
611
771
|
name,
|
|
612
772
|
sourceId,
|
|
@@ -618,6 +778,19 @@ function discoverCatalog(root) {
|
|
|
618
778
|
});
|
|
619
779
|
break;
|
|
620
780
|
}
|
|
781
|
+
for (const entryFile of PLUGIN_ENTRY_FILES) {
|
|
782
|
+
const specifier = join(pluginRoot, entryFile);
|
|
783
|
+
if (existsSync5(specifier)) {
|
|
784
|
+
sources.push({
|
|
785
|
+
name,
|
|
786
|
+
sourceId,
|
|
787
|
+
kind: "file",
|
|
788
|
+
specifier,
|
|
789
|
+
description: `${name} plugin from ${sourceId} catalog`
|
|
790
|
+
});
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
621
794
|
}
|
|
622
795
|
return sources;
|
|
623
796
|
}
|
|
@@ -636,9 +809,9 @@ function resolvePluginSource(name) {
|
|
|
636
809
|
|
|
637
810
|
// src/commands/install.ts
|
|
638
811
|
function detectPackageManager() {
|
|
639
|
-
if (
|
|
640
|
-
if (
|
|
641
|
-
if (
|
|
812
|
+
if (existsSync6(resolve7("bun.lock")) || existsSync6(resolve7("bun.lockb"))) return "bun";
|
|
813
|
+
if (existsSync6(resolve7("pnpm-lock.yaml"))) return "pnpm";
|
|
814
|
+
if (existsSync6(resolve7("yarn.lock"))) return "yarn";
|
|
642
815
|
return "npm";
|
|
643
816
|
}
|
|
644
817
|
function installPackage(packageManager, packageName) {
|
|
@@ -676,12 +849,188 @@ async function installCommand(name, options = {}) {
|
|
|
676
849
|
console.log(`Recorded plugin in ${PLUGIN_LOCK_FILE}`);
|
|
677
850
|
}
|
|
678
851
|
|
|
852
|
+
// src/commands/validate-plugin.ts
|
|
853
|
+
import { existsSync as existsSync7, mkdtempSync, rmSync } from "fs";
|
|
854
|
+
import { tmpdir } from "os";
|
|
855
|
+
import { basename, extname, isAbsolute as isAbsolute2, join as join2, resolve as resolve8 } from "path";
|
|
856
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
857
|
+
function resolveSource(input) {
|
|
858
|
+
const source = resolvePluginSource(input);
|
|
859
|
+
if (source) return source;
|
|
860
|
+
const specifier = input.startsWith(".") || isAbsolute2(input) ? resolve8(input) : input;
|
|
861
|
+
return {
|
|
862
|
+
name: basename(input).replace(/\.[^.]+$/, ""),
|
|
863
|
+
sourceId: "specifier",
|
|
864
|
+
kind: "file",
|
|
865
|
+
specifier,
|
|
866
|
+
description: `${input} plugin specifier`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function assertLocalFile(path) {
|
|
870
|
+
if ((path.startsWith(".") || isAbsolute2(path)) && !existsSync7(resolve8(path))) {
|
|
871
|
+
throw new Error(`Plugin entry does not exist: ${path}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function canBuild(specifier) {
|
|
875
|
+
return [".ts", ".tsx", ".js", ".mjs"].includes(extname(specifier));
|
|
876
|
+
}
|
|
877
|
+
function buildEntry(specifier) {
|
|
878
|
+
if (!canBuild(specifier)) return;
|
|
879
|
+
const outdir = mkdtempSync(join2(tmpdir(), "api-emulator-plugin-validate-"));
|
|
880
|
+
try {
|
|
881
|
+
const result = spawnSync3("bun", ["build", specifier, "--packages", "external", "--outdir", outdir], {
|
|
882
|
+
stdio: "pipe",
|
|
883
|
+
encoding: "utf-8"
|
|
884
|
+
});
|
|
885
|
+
if (result.error) {
|
|
886
|
+
if (result.error.code === "ENOENT") {
|
|
887
|
+
console.log(" build: skipped (bun not found)");
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
throw result.error;
|
|
891
|
+
}
|
|
892
|
+
if (result.status !== 0) {
|
|
893
|
+
throw new Error((result.stderr || result.stdout || "Plugin build failed").trim());
|
|
894
|
+
}
|
|
895
|
+
console.log(" build: ok");
|
|
896
|
+
} finally {
|
|
897
|
+
rmSync(outdir, { recursive: true, force: true });
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
async function loadPlugin(specifier, expectedName) {
|
|
901
|
+
const module = await loadExternalPluginModule(specifier);
|
|
902
|
+
if (module.name !== expectedName) {
|
|
903
|
+
throw new Error(`Plugin exported name "${module.name}" does not match requested plugin "${expectedName}"`);
|
|
904
|
+
}
|
|
905
|
+
await module.load();
|
|
906
|
+
console.log(" load: ok");
|
|
907
|
+
}
|
|
908
|
+
async function validatePluginCommand(input, options = {}) {
|
|
909
|
+
const source = resolveSource(input);
|
|
910
|
+
console.log(`Validating ${source.name} from ${source.sourceId}`);
|
|
911
|
+
console.log(` specifier: ${source.specifier}`);
|
|
912
|
+
if (source.packageName) console.log(` package: ${source.packageName}`);
|
|
913
|
+
assertLocalFile(source.specifier);
|
|
914
|
+
console.log(" entry: ok");
|
|
915
|
+
if (!options.skipBuild) {
|
|
916
|
+
buildEntry(source.specifier);
|
|
917
|
+
}
|
|
918
|
+
if (options.skipLoad) {
|
|
919
|
+
console.log(" load: skipped");
|
|
920
|
+
} else {
|
|
921
|
+
await loadPlugin(source.specifier, source.name);
|
|
922
|
+
}
|
|
923
|
+
console.log(`Plugin ${source.name} is valid`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/commands/plugin.ts
|
|
927
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
928
|
+
import { join as join3 } from "path";
|
|
929
|
+
function normalizePluginName(input) {
|
|
930
|
+
const name = input.trim().replace(/^@/, "");
|
|
931
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
932
|
+
throw new Error("Plugin names must use lowercase letters, numbers, and hyphens");
|
|
933
|
+
}
|
|
934
|
+
return name;
|
|
935
|
+
}
|
|
936
|
+
function pluginEntry(name, fidelity) {
|
|
937
|
+
return `export const manifest = {
|
|
938
|
+
name: "${name}",
|
|
939
|
+
label: "${name} API emulator",
|
|
940
|
+
endpoints: "GET /health",
|
|
941
|
+
fidelity: {
|
|
942
|
+
level: "${fidelity}",
|
|
943
|
+
endpoints: ["GET /health"],
|
|
944
|
+
seedableResources: [],
|
|
945
|
+
smoke: "not-started",
|
|
946
|
+
},
|
|
947
|
+
initConfig: {
|
|
948
|
+
${JSON.stringify(name)}: {},
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
export const plugin = {
|
|
953
|
+
name: "${name}",
|
|
954
|
+
register(app) {
|
|
955
|
+
app.get("/health", (c) => c.json({ ok: true, service: "${name}" }));
|
|
956
|
+
},
|
|
957
|
+
};
|
|
958
|
+
`;
|
|
959
|
+
}
|
|
960
|
+
function catalogContent(name, specifier, description) {
|
|
961
|
+
const catalogPath = "api-emulator.catalog.json";
|
|
962
|
+
const catalog = existsSync8(catalogPath) ? JSON.parse(readFileSync5(catalogPath, "utf-8")) : {};
|
|
963
|
+
catalog.plugins ??= {};
|
|
964
|
+
catalog.plugins[name] = {
|
|
965
|
+
kind: "file",
|
|
966
|
+
specifier,
|
|
967
|
+
description
|
|
968
|
+
};
|
|
969
|
+
return `${JSON.stringify(catalog, null, 2)}
|
|
970
|
+
`;
|
|
971
|
+
}
|
|
972
|
+
async function pluginCreateCommand(input, options = {}) {
|
|
973
|
+
const name = normalizePluginName(input);
|
|
974
|
+
const pluginDir = options.dir ?? `@${name}`;
|
|
975
|
+
const entryPath = join3(pluginDir, "api-emulator.mjs");
|
|
976
|
+
const catalogSpecifier = `./${entryPath}`;
|
|
977
|
+
writeGeneratedFile(entryPath, pluginEntry(name, options.fidelity ?? "stub"), {
|
|
978
|
+
source: "api clone create",
|
|
979
|
+
yes: options.yes
|
|
980
|
+
});
|
|
981
|
+
writeGeneratedFile(
|
|
982
|
+
"api-emulator.catalog.json",
|
|
983
|
+
catalogContent(name, catalogSpecifier, `${name} API emulator plugin`),
|
|
984
|
+
{
|
|
985
|
+
source: "api clone create",
|
|
986
|
+
yes: options.yes
|
|
987
|
+
}
|
|
988
|
+
);
|
|
989
|
+
console.log(`Created ${entryPath}`);
|
|
990
|
+
console.log(`Recorded ${name} in api-emulator.catalog.json`);
|
|
991
|
+
console.log(`Run 'npx -p api-emulator api validate-plugin ${name}' to check the clone.`);
|
|
992
|
+
}
|
|
993
|
+
|
|
679
994
|
// src/index.ts
|
|
680
|
-
var pkg2 = { version: "0.
|
|
681
|
-
var defaultPort = process.env.API_EMULATOR_PORT ?? process.env.
|
|
995
|
+
var pkg2 = { version: "0.7.0" };
|
|
996
|
+
var defaultPort = process.env.API_EMULATOR_PORT ?? process.env.PORT ?? "4000";
|
|
682
997
|
var program = new Command();
|
|
683
|
-
program.name("api").description("Local API emulators you can run, share, and extend with plugins").version(pkg2.version);
|
|
684
|
-
|
|
998
|
+
program.name("api").description("Local API emulators you can run, share, and extend with plugins").version(pkg2.version).option("--notify", "Show a macOS notification for useful command milestones");
|
|
999
|
+
function notifyOption(command) {
|
|
1000
|
+
return command.option("--notify", "Show a macOS notification for useful command milestones");
|
|
1001
|
+
}
|
|
1002
|
+
function wantsNotify(command, opts) {
|
|
1003
|
+
return Boolean(opts.notify ?? command.optsWithGlobals().notify);
|
|
1004
|
+
}
|
|
1005
|
+
function wantsStartNotify(opts) {
|
|
1006
|
+
return opts.notify !== false;
|
|
1007
|
+
}
|
|
1008
|
+
function formatDuration(ms) {
|
|
1009
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1010
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1011
|
+
}
|
|
1012
|
+
async function runCommandWithNotification(label, enabled, fn) {
|
|
1013
|
+
const startedAt = Date.now();
|
|
1014
|
+
try {
|
|
1015
|
+
await fn();
|
|
1016
|
+
notifyIfRequested({
|
|
1017
|
+
enabled,
|
|
1018
|
+
title: "api-emulator",
|
|
1019
|
+
message: `${label} finished in ${formatDuration(Date.now() - startedAt)}`
|
|
1020
|
+
});
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
notifyIfRequested({
|
|
1023
|
+
enabled,
|
|
1024
|
+
title: "api-emulator",
|
|
1025
|
+
message: `${label} failed after ${formatDuration(Date.now() - startedAt)}`
|
|
1026
|
+
});
|
|
1027
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
notifyOption(
|
|
1032
|
+
program.command("start", { isDefault: true }).description("Start the emulator server").option("-p, --port <port>", "Base port", defaultPort).option("-s, --service <services>", "Comma-separated services to enable").option("--seed <file>", "Path to seed config file").option("--base-url <url>", "Override advertised base URL (supports {service} template)").option("--portless", "Serve over HTTPS via portless (auto-registers aliases)").option("--plugin <plugins>", "Comma-separated external plugin paths or package names").option("--no-notify", "Disable the macOS notification when the emulator server is ready")
|
|
1033
|
+
).action(async (opts) => {
|
|
685
1034
|
const port = parseInt(opts.port, 10);
|
|
686
1035
|
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
687
1036
|
console.error(`Invalid port: ${opts.port}`);
|
|
@@ -693,24 +1042,62 @@ program.command("start", { isDefault: true }).description("Start the emulator se
|
|
|
693
1042
|
seed: opts.seed,
|
|
694
1043
|
baseUrl: opts.baseUrl,
|
|
695
1044
|
portless: opts.portless,
|
|
696
|
-
plugin: opts.plugin
|
|
1045
|
+
plugin: opts.plugin,
|
|
1046
|
+
notify: wantsStartNotify(opts)
|
|
697
1047
|
});
|
|
698
1048
|
});
|
|
699
|
-
|
|
700
|
-
|
|
1049
|
+
notifyOption(
|
|
1050
|
+
program.command("init").description("Generate a starter config file").option("-s, --service <service>", "Service to generate config for", "all").option("--plugin <plugins>", "Comma-separated external plugin paths or package names").option("--agents <targets>", "Install agent skills for agents,user-agents,claude,codex,cursor,windsurf").option("--skills-only", "Only install agent skills").option("--yes", "Overwrite generated files that changed").option("--non-interactive", "Use defaults suitable for agents and CI")
|
|
1051
|
+
).action(async (opts, command) => {
|
|
1052
|
+
await runCommandWithNotification("init", wantsNotify(command, opts), async () => {
|
|
1053
|
+
await initCommand({
|
|
1054
|
+
service: opts.service,
|
|
1055
|
+
plugin: opts.plugin,
|
|
1056
|
+
agents: opts.agents,
|
|
1057
|
+
skillsOnly: opts.skillsOnly,
|
|
1058
|
+
yes: opts.yes,
|
|
1059
|
+
nonInteractive: opts.nonInteractive
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
701
1062
|
});
|
|
1063
|
+
function addPluginCreateCommand(command) {
|
|
1064
|
+
notifyOption(
|
|
1065
|
+
command.command("create <name>").description("Scaffold a provider clone").option("--dir <dir>", "Directory to create the clone in").option("--fidelity <level>", "Initial fidelity level", "stub").option("--yes", "Overwrite generated files that changed")
|
|
1066
|
+
).action(async (name, opts, actionCommand) => {
|
|
1067
|
+
await runCommandWithNotification("plugin create", wantsNotify(actionCommand, opts), async () => {
|
|
1068
|
+
await pluginCreateCommand(name, {
|
|
1069
|
+
dir: opts.dir,
|
|
1070
|
+
fidelity: opts.fidelity,
|
|
1071
|
+
yes: opts.yes
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
var plugin = program.command("plugin").description("Create and manage provider plugins");
|
|
1077
|
+
addPluginCreateCommand(plugin);
|
|
1078
|
+
var clone = program.command("clone").description("Create and manage provider clones");
|
|
1079
|
+
addPluginCreateCommand(clone);
|
|
702
1080
|
program.command("list").alias("list-services").description("List available services").option("--plugin <plugins>", "Comma-separated external plugin paths or package names").action(async (opts) => {
|
|
703
1081
|
await listCommand({ plugin: opts.plugin });
|
|
704
1082
|
});
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1083
|
+
notifyOption(
|
|
1084
|
+
program.command("install <plugin>").description("Install a provider plugin by name").option("--package-manager <name>", "Package manager to use").option("--no-package-manager", "Only record the plugin in api-emulator.lock")
|
|
1085
|
+
).action(async (plugin2, opts, command) => {
|
|
1086
|
+
await runCommandWithNotification("install", wantsNotify(command, opts), async () => {
|
|
1087
|
+
await installCommand(plugin2, {
|
|
708
1088
|
packageManager: opts.packageManager === false ? false : opts.packageManager
|
|
709
1089
|
});
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
notifyOption(
|
|
1093
|
+
program.command("validate-plugin <plugin>").description("Validate a provider plugin by name, path, or package").option("--skip-build", "Skip entrypoint build validation").option("--skip-load", "Skip runtime module loading validation")
|
|
1094
|
+
).action(async (plugin2, opts, command) => {
|
|
1095
|
+
await runCommandWithNotification("validate-plugin", wantsNotify(command, opts), async () => {
|
|
1096
|
+
await validatePluginCommand(plugin2, {
|
|
1097
|
+
skipBuild: opts.skipBuild,
|
|
1098
|
+
skipLoad: opts.skipLoad
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
714
1101
|
});
|
|
715
1102
|
program.parse();
|
|
716
1103
|
//# sourceMappingURL=index.js.map
|