@siglume/api-sdk 2.0.4 → 3.0.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 +0 -8
- package/dist/bin/siglume.cjs +9 -211
- package/dist/bin/siglume.cjs.map +1 -1
- package/dist/bin/siglume.js +9 -211
- package/dist/bin/siglume.js.map +1 -1
- package/dist/cli/index.cjs +9 -211
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +0 -35
- package/dist/cli/index.d.ts +0 -35
- package/dist/cli/index.js +9 -211
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +0 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -42
- package/dist/index.d.ts +1 -42
- package/dist/index.js +0 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,8 +67,6 @@ siglume score . --remote
|
|
|
67
67
|
siglume preflight . # checks blockers without creating a draft
|
|
68
68
|
siglume register . # preflight + auto-register + confirm/publish
|
|
69
69
|
siglume register . --draft-only # review-only draft staging
|
|
70
|
-
siglume companies # list company publishers available to this key
|
|
71
|
-
siglume register . --company company_123
|
|
72
70
|
```
|
|
73
71
|
|
|
74
72
|
`siglume register` reads `tool_manual.json`, the local Git-ignored
|
|
@@ -167,12 +165,6 @@ After live or sandbox execution, inspect receipts with `siglume dev tail`,
|
|
|
167
165
|
publisher listing view is privacy-redacted. See
|
|
168
166
|
[`../docs/developer-observability.md`](../docs/developer-observability.md).
|
|
169
167
|
|
|
170
|
-
Company-name publishing is founder-only in the Phase 2 MVP. Use
|
|
171
|
-
`publisher_type: "company"` with `company_id` in `app_manifest.yaml`, or pass
|
|
172
|
-
`--company <company_id>` to the CLI. Paid company listings require the
|
|
173
|
-
company's verified settlement wallet; Siglume does not fall back to the
|
|
174
|
-
registrant's personal payout wallet.
|
|
175
|
-
|
|
176
168
|
Game APIs use the same publishing flow. To make a listing eligible for the
|
|
177
169
|
dedicated Game API Store entry point, include explicit game-oriented
|
|
178
170
|
`compatibility_tags` in the manifest, for example `["game", "unity",
|
package/dist/bin/siglume.cjs
CHANGED
|
@@ -1527,12 +1527,6 @@ function parseListing(data) {
|
|
|
1527
1527
|
seller_display_name: stringOrNull(data.seller_display_name),
|
|
1528
1528
|
seller_homepage_url: stringOrNull(data.seller_homepage_url),
|
|
1529
1529
|
seller_social_url: stringOrNull(data.seller_social_url),
|
|
1530
|
-
publisher_type: stringOrNull(data.publisher_type),
|
|
1531
|
-
publisher_company_id: stringOrNull(data.publisher_company_id),
|
|
1532
|
-
company_id: stringOrNull(data.company_id),
|
|
1533
|
-
company_name: stringOrNull(data.company_name),
|
|
1534
|
-
company_publish_status: stringOrNull(data.company_publish_status),
|
|
1535
|
-
company_terms_version: stringOrNull(data.company_terms_version),
|
|
1536
1530
|
review_status: stringOrNull(data.review_status),
|
|
1537
1531
|
review_note: stringOrNull(data.review_note),
|
|
1538
1532
|
submission_blockers: Array.isArray(data.submission_blockers) ? data.submission_blockers.filter((item) => typeof item === "string") : [],
|
|
@@ -1542,29 +1536,6 @@ function parseListing(data) {
|
|
|
1542
1536
|
raw: { ...data }
|
|
1543
1537
|
};
|
|
1544
1538
|
}
|
|
1545
|
-
function parseCompanyPublisher(data) {
|
|
1546
|
-
const wallets = Array.isArray(data.settlement_wallets) ? data.settlement_wallets.filter((item) => isRecord(item)) : [];
|
|
1547
|
-
return {
|
|
1548
|
-
company_id: String(data.company_id ?? data.id ?? ""),
|
|
1549
|
-
name: String(data.name ?? ""),
|
|
1550
|
-
status: String(data.status ?? ""),
|
|
1551
|
-
description: stringOrNull(data.description),
|
|
1552
|
-
is_founder: Boolean(data.is_founder ?? false),
|
|
1553
|
-
membership_role: stringOrNull(data.membership_role),
|
|
1554
|
-
membership_status: stringOrNull(data.membership_status),
|
|
1555
|
-
can_publish: Boolean(data.can_publish ?? true),
|
|
1556
|
-
can_approve: Boolean(data.can_approve ?? false),
|
|
1557
|
-
approval_required: Boolean(data.approval_required ?? false),
|
|
1558
|
-
paid_listing_allowed: Boolean(data.paid_listing_allowed ?? false),
|
|
1559
|
-
disabled_reasons: Array.isArray(data.disabled_reasons) ? data.disabled_reasons.filter((item) => typeof item === "string") : [],
|
|
1560
|
-
company_terms_version: stringOrNull(data.company_terms_version),
|
|
1561
|
-
active_listing_count: Number(data.active_listing_count ?? 0),
|
|
1562
|
-
pending_approval_count: Number(data.pending_approval_count ?? 0),
|
|
1563
|
-
settlement_wallet_ready: Boolean(data.settlement_wallet_ready ?? false),
|
|
1564
|
-
settlement_wallets: wallets.map((item) => ({ ...item })),
|
|
1565
|
-
raw: { ...data }
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
1539
|
function parseCapabilitySaveState(data) {
|
|
1569
1540
|
return {
|
|
1570
1541
|
capability_key: String(data.capability_key ?? ""),
|
|
@@ -2479,9 +2450,6 @@ var init_client = __esm({
|
|
|
2479
2450
|
"support_contact",
|
|
2480
2451
|
"seller_homepage_url",
|
|
2481
2452
|
"seller_social_url",
|
|
2482
|
-
"publisher_type",
|
|
2483
|
-
"company_id",
|
|
2484
|
-
"publisher_company_id",
|
|
2485
2453
|
"store_vertical",
|
|
2486
2454
|
"jurisdiction",
|
|
2487
2455
|
"price_model",
|
|
@@ -2555,25 +2523,6 @@ var init_client = __esm({
|
|
|
2555
2523
|
);
|
|
2556
2524
|
}
|
|
2557
2525
|
}
|
|
2558
|
-
const explicitPublisherType = payload.publisher_type !== void 0 && payload.publisher_type !== null;
|
|
2559
|
-
const companyId = String(payload.company_id ?? "").trim() || String(payload.publisher_company_id ?? "").trim();
|
|
2560
|
-
const publisherType = String(payload.publisher_type ?? "user").trim().toLowerCase();
|
|
2561
|
-
if (publisherType !== "user" && publisherType !== "company") {
|
|
2562
|
-
throw new SiglumeClientError("AppManifest.publisher_type must be 'user' or 'company'.");
|
|
2563
|
-
}
|
|
2564
|
-
if (publisherType === "company" && !companyId) {
|
|
2565
|
-
throw new SiglumeClientError("AppManifest.company_id is required when publisher_type='company'.");
|
|
2566
|
-
}
|
|
2567
|
-
if (publisherType === "user" && companyId) {
|
|
2568
|
-
throw new SiglumeClientError("AppManifest.company_id cannot be combined with publisher_type='user'.");
|
|
2569
|
-
}
|
|
2570
|
-
if (explicitPublisherType || companyId) {
|
|
2571
|
-
payload.publisher_type = publisherType;
|
|
2572
|
-
}
|
|
2573
|
-
if (companyId) {
|
|
2574
|
-
payload.company_id = companyId;
|
|
2575
|
-
payload.publisher_company_id = companyId;
|
|
2576
|
-
}
|
|
2577
2526
|
validateManifestPersistenceContract(payload);
|
|
2578
2527
|
if (payload.manifest && typeof payload.manifest === "object") {
|
|
2579
2528
|
delete payload.manifest.version;
|
|
@@ -2681,25 +2630,6 @@ var init_client = __esm({
|
|
|
2681
2630
|
const [data] = await this.request("GET", `/market/capabilities/${listing_id}`);
|
|
2682
2631
|
return parseListing(data);
|
|
2683
2632
|
}
|
|
2684
|
-
async list_company_publishers() {
|
|
2685
|
-
const [data] = await this.request("GET", "/market/company-publishers");
|
|
2686
|
-
return Array.isArray(data.items) ? data.items.filter((item) => isRecord(item)).map(parseCompanyPublisher) : [];
|
|
2687
|
-
}
|
|
2688
|
-
async request_company_publish_approval(listing_id, note) {
|
|
2689
|
-
const [data] = await this.request("POST", `/market/capabilities/${listing_id}/company-publish-approval`, {
|
|
2690
|
-
json_body: note ? { note } : {}
|
|
2691
|
-
});
|
|
2692
|
-
return parseListing(data);
|
|
2693
|
-
}
|
|
2694
|
-
async decide_company_publish_approval(listing_id, options) {
|
|
2695
|
-
const [data] = await this.request("POST", `/market/capabilities/${listing_id}/company-publish-approval/decision`, {
|
|
2696
|
-
json_body: {
|
|
2697
|
-
decision: options.decision,
|
|
2698
|
-
...options.reason ? { reason: options.reason } : {}
|
|
2699
|
-
}
|
|
2700
|
-
});
|
|
2701
|
-
return parseListing(data);
|
|
2702
|
-
}
|
|
2703
2633
|
async get_capability_state(capability_key, save_key = "default") {
|
|
2704
2634
|
const [data] = await this.request("GET", `/market/capability-state/${capability_key}/${save_key}`);
|
|
2705
2635
|
return parseCapabilitySaveState(data);
|
|
@@ -6958,12 +6888,7 @@ function ensureManifestPublisherIdentity(project) {
|
|
|
6958
6888
|
const sellerHomepageUrl = String(manifestPayload.seller_homepage_url ?? "").trim();
|
|
6959
6889
|
const sellerSocialUrl = String(manifestPayload.seller_social_url ?? "").trim();
|
|
6960
6890
|
const jurisdiction = String(manifestPayload.jurisdiction ?? "").trim();
|
|
6961
|
-
const companyId = String(manifestPayload.company_id ?? "").trim() || String(manifestPayload.publisher_company_id ?? "").trim();
|
|
6962
|
-
const publisherType = String(manifestPayload.publisher_type ?? "user").trim().toLowerCase();
|
|
6963
6891
|
const issues = [];
|
|
6964
|
-
if (companyId && publisherType !== "company") {
|
|
6965
|
-
issues.push('manifest.company_id requires manifest.publisher_type to be "company"');
|
|
6966
|
-
}
|
|
6967
6892
|
if (!docsUrl) {
|
|
6968
6893
|
issues.push("manifest.docs_url is required");
|
|
6969
6894
|
} else if (looksLikePlaceholder(docsUrl)) {
|
|
@@ -7123,108 +7048,23 @@ ${errors.map((error) => `- ${error}`).join("\n")}`
|
|
|
7123
7048
|
}
|
|
7124
7049
|
return preflight;
|
|
7125
7050
|
}
|
|
7126
|
-
function companyNameSlug(value) {
|
|
7127
|
-
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
7128
|
-
}
|
|
7129
7051
|
async function runRegistration(path = ".", options = {}, deps = {}) {
|
|
7130
7052
|
const project = await loadProject(path);
|
|
7131
|
-
let requestedCompanyId = String(options.company_id ?? "").trim();
|
|
7132
|
-
const requestedCompanySlug = String(options.company_slug ?? "").trim();
|
|
7133
|
-
let companyPublisherCandidates = null;
|
|
7134
|
-
if (requestedCompanySlug) {
|
|
7135
|
-
if (requestedCompanyId) {
|
|
7136
|
-
throw new SiglumeProjectError("--company and --company-slug cannot be combined.");
|
|
7137
|
-
}
|
|
7138
|
-
const slug = companyNameSlug(requestedCompanySlug);
|
|
7139
|
-
if (!slug && requestedCompanySlug !== requestedCompanyId) {
|
|
7140
|
-
throw new SiglumeProjectError(`Company slug ${requestedCompanySlug} is not slug-compatible; use --company <company_id> instead.`);
|
|
7141
|
-
}
|
|
7142
|
-
}
|
|
7143
|
-
if (requestedCompanyId) {
|
|
7144
|
-
project.manifest = {
|
|
7145
|
-
...project.manifest,
|
|
7146
|
-
publisher_type: "company",
|
|
7147
|
-
company_id: requestedCompanyId,
|
|
7148
|
-
publisher_company_id: requestedCompanyId
|
|
7149
|
-
};
|
|
7150
|
-
}
|
|
7151
7053
|
ensureExplicitToolManual(project);
|
|
7152
7054
|
ensureManifestPublisherIdentity(project);
|
|
7153
7055
|
ensureRuntimeValidationReady(project);
|
|
7154
7056
|
const client = await createClient(deps);
|
|
7155
|
-
if (requestedCompanySlug) {
|
|
7156
|
-
const slug = companyNameSlug(requestedCompanySlug);
|
|
7157
|
-
companyPublisherCandidates = await client.list_company_publishers();
|
|
7158
|
-
const matches = companyPublisherCandidates.filter(
|
|
7159
|
-
(item) => companyNameSlug(item.name || item.company_id) === slug || item.company_id === requestedCompanySlug
|
|
7160
|
-
);
|
|
7161
|
-
if (matches.length === 0) {
|
|
7162
|
-
throw new SiglumeProjectError(`Company slug ${requestedCompanySlug} is not available to this API key.`);
|
|
7163
|
-
}
|
|
7164
|
-
if (matches.length > 1) {
|
|
7165
|
-
throw new SiglumeProjectError(`Company slug ${requestedCompanySlug} is ambiguous; use --company <company_id> instead.`);
|
|
7166
|
-
}
|
|
7167
|
-
const match = matches[0];
|
|
7168
|
-
if (!match) {
|
|
7169
|
-
throw new SiglumeProjectError(`Company slug ${requestedCompanySlug} is not available to this API key.`);
|
|
7170
|
-
}
|
|
7171
|
-
if (match.can_publish === false) {
|
|
7172
|
-
const disabledReasons = match.disabled_reasons ?? [];
|
|
7173
|
-
const reasons = disabledReasons.length > 0 ? disabledReasons.join(", ") : "company publisher is disabled";
|
|
7174
|
-
throw new SiglumeProjectError(`Company ${match.company_id} cannot publish: ${reasons}.`);
|
|
7175
|
-
}
|
|
7176
|
-
requestedCompanyId = match.company_id;
|
|
7177
|
-
project.manifest = {
|
|
7178
|
-
...project.manifest,
|
|
7179
|
-
publisher_type: "company",
|
|
7180
|
-
company_id: requestedCompanyId,
|
|
7181
|
-
publisher_company_id: requestedCompanyId
|
|
7182
|
-
};
|
|
7183
|
-
}
|
|
7184
7057
|
const preflight = await registrationPreflight(project, client);
|
|
7185
|
-
let companyPublisherPreflight = null;
|
|
7186
|
-
const companyId = String(project.manifest.company_id ?? "").trim() || String(project.manifest.publisher_company_id ?? "").trim();
|
|
7187
|
-
const publisherType = String(project.manifest.publisher_type ?? "user").toLowerCase();
|
|
7188
|
-
if (publisherType === "company") {
|
|
7189
|
-
if (!companyId) {
|
|
7190
|
-
throw new SiglumeProjectError("Company registration requires --company <company_id> or manifest.company_id.");
|
|
7191
|
-
}
|
|
7192
|
-
const companies = companyPublisherCandidates ?? await client.list_company_publishers();
|
|
7193
|
-
companyPublisherCandidates = companies;
|
|
7194
|
-
const company = companies.find((item) => item.company_id === companyId);
|
|
7195
|
-
if (!company) {
|
|
7196
|
-
throw new SiglumeProjectError(`Company ${companyId} is not available to this API key.`);
|
|
7197
|
-
}
|
|
7198
|
-
if (company.can_publish === false) {
|
|
7199
|
-
const disabledReasons = company.disabled_reasons ?? [];
|
|
7200
|
-
const reasons = disabledReasons.length > 0 ? disabledReasons.join(", ") : "company publisher is disabled";
|
|
7201
|
-
throw new SiglumeProjectError(`Company ${companyId} cannot publish: ${reasons}.`);
|
|
7202
|
-
}
|
|
7203
|
-
companyPublisherPreflight = company;
|
|
7204
|
-
}
|
|
7205
7058
|
let developerPortalPreflight = null;
|
|
7206
7059
|
if (String(project.manifest.price_model ?? "free").toLowerCase() !== "free") {
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
throw new SiglumeProjectError(
|
|
7214
|
-
`Paid company registration requires a verified company settlement wallet for ${company.name}. Open the company settings and complete settlement readiness before registering.`
|
|
7215
|
-
);
|
|
7216
|
-
}
|
|
7217
|
-
developerPortalPreflight = { company_publisher: toJsonable(company) };
|
|
7218
|
-
} else {
|
|
7219
|
-
const portal = await client.get_developer_portal();
|
|
7220
|
-
const verifiedDestination = portal.payout_readiness?.verified_destination;
|
|
7221
|
-
if (verifiedDestination !== true) {
|
|
7222
|
-
throw new SiglumeProjectError(
|
|
7223
|
-
"Paid API registration requires a verified Polygon payout destination. Open https://siglume.com/owner/credits/payout and confirm the embedded-wallet payout token, or call GET /v1/market/developer/portal until payout_readiness.verified_destination is true."
|
|
7224
|
-
);
|
|
7225
|
-
}
|
|
7226
|
-
developerPortalPreflight = toJsonable(portal);
|
|
7060
|
+
const portal = await client.get_developer_portal();
|
|
7061
|
+
const verifiedDestination = portal.payout_readiness?.verified_destination;
|
|
7062
|
+
if (verifiedDestination !== true) {
|
|
7063
|
+
throw new SiglumeProjectError(
|
|
7064
|
+
"Paid API registration requires a verified Polygon payout destination. Open https://siglume.com/owner/credits/payout and confirm the embedded-wallet payout token, or call GET /v1/market/developer/portal until payout_readiness.verified_destination is true."
|
|
7065
|
+
);
|
|
7227
7066
|
}
|
|
7067
|
+
developerPortalPreflight = toJsonable(portal);
|
|
7228
7068
|
}
|
|
7229
7069
|
const receipt = await client.auto_register(project.manifest, project.tool_manual, {
|
|
7230
7070
|
runtime_validation: project.runtime_validation
|
|
@@ -7293,14 +7133,6 @@ async function getUsageReport(options, deps = {}) {
|
|
|
7293
7133
|
count: items.length
|
|
7294
7134
|
};
|
|
7295
7135
|
}
|
|
7296
|
-
async function listCompanyPublishersReport(deps = {}) {
|
|
7297
|
-
const client = await createClient(deps);
|
|
7298
|
-
const companies = await client.list_company_publishers();
|
|
7299
|
-
return {
|
|
7300
|
-
companies: companies.map((item) => toJsonable(item)),
|
|
7301
|
-
count: companies.length
|
|
7302
|
-
};
|
|
7303
|
-
}
|
|
7304
7136
|
async function diffJsonFiles(oldPath, newPath) {
|
|
7305
7137
|
const oldPayload = await loadJsonDocument(oldPath);
|
|
7306
7138
|
const newPayload = await loadJsonDocument(newPath);
|
|
@@ -8369,24 +8201,6 @@ function renderOperationTable(operations) {
|
|
|
8369
8201
|
...rows.map((row) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" "))
|
|
8370
8202
|
];
|
|
8371
8203
|
}
|
|
8372
|
-
function renderCompanyTable(companies) {
|
|
8373
|
-
const rows = companies.map((item) => [
|
|
8374
|
-
String(item.company_id ?? item.id ?? ""),
|
|
8375
|
-
String(item.name ?? ""),
|
|
8376
|
-
String(item.membership_role ?? (item.is_founder ? "founder" : "")),
|
|
8377
|
-
String(item.settlement_wallet_ready === true ? "ready" : "not_ready"),
|
|
8378
|
-
String(item.pending_approval_count ?? 0)
|
|
8379
|
-
]);
|
|
8380
|
-
const headers = ["company_id", "name", "role", "settlement", "pending"];
|
|
8381
|
-
const widths = headers.map(
|
|
8382
|
-
(header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0))
|
|
8383
|
-
);
|
|
8384
|
-
return [
|
|
8385
|
-
headers.map((header, index) => header.padEnd(widths[index] ?? header.length)).join(" "),
|
|
8386
|
-
widths.map((width) => "-".repeat(width)).join(" "),
|
|
8387
|
-
...rows.map((row) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" "))
|
|
8388
|
-
];
|
|
8389
|
-
}
|
|
8390
8204
|
async function runCli(argv, deps = {}) {
|
|
8391
8205
|
const stdout = deps.stdout;
|
|
8392
8206
|
const stderr = deps.stderr ?? console.error;
|
|
@@ -8542,21 +8356,7 @@ async function runCli(argv, deps = {}) {
|
|
|
8542
8356
|
}
|
|
8543
8357
|
if (report.runtime_validation_path) emit(stdout, `runtime_validation_path: ${String(report.runtime_validation_path)}`);
|
|
8544
8358
|
});
|
|
8545
|
-
program.command("
|
|
8546
|
-
const report = await listCompanyPublishersReport(deps);
|
|
8547
|
-
if (options.json) {
|
|
8548
|
-
emit(stdout, renderJson(report));
|
|
8549
|
-
return;
|
|
8550
|
-
}
|
|
8551
|
-
const companies = Array.isArray(report.companies) ? report.companies.filter((item) => Boolean(item && typeof item === "object")) : [];
|
|
8552
|
-
if (companies.length === 0) {
|
|
8553
|
-
emit(stdout, "No company publishers available for this API key.");
|
|
8554
|
-
return;
|
|
8555
|
-
}
|
|
8556
|
-
emit(stdout, "Company publishers");
|
|
8557
|
-
renderCompanyTable(companies).forEach((line) => emit(stdout, line));
|
|
8558
|
-
});
|
|
8559
|
-
program.command("register").option("--confirm", "explicitly confirm the registration; this is the default unless --draft-only is set", false).option("--draft-only", "create or refresh the draft without confirming publication", false).option("--submit-review", "legacy alias: publish immediately if your environment still routes through submit-review", false).option("--company <companyId>", "publish under a Siglume company name; revenue is split equally among active members", "").option("--company-slug <slug>", "publish under a Siglume company by matching the slugified company name", "").option("--json", "emit machine-readable JSON", false).argument("[path]", ".", "project path").action(async (path, options) => {
|
|
8359
|
+
program.command("register").option("--confirm", "explicitly confirm the registration; this is the default unless --draft-only is set", false).option("--draft-only", "create or refresh the draft without confirming publication", false).option("--submit-review", "legacy alias: publish immediately if your environment still routes through submit-review", false).option("--json", "emit machine-readable JSON", false).argument("[path]", ".", "project path").action(async (path, options) => {
|
|
8560
8360
|
const draftOnly = Boolean(options.draftOnly);
|
|
8561
8361
|
if (draftOnly && options.confirm) {
|
|
8562
8362
|
throw new SiglumeProjectError("--draft-only cannot be combined with --confirm.");
|
|
@@ -8568,9 +8368,7 @@ async function runCli(argv, deps = {}) {
|
|
|
8568
8368
|
const report = await runRegistration(path, {
|
|
8569
8369
|
confirm: shouldConfirm,
|
|
8570
8370
|
draft_only: draftOnly,
|
|
8571
|
-
submit_review: options.submitReview
|
|
8572
|
-
company_id: options.company,
|
|
8573
|
-
company_slug: options.companySlug
|
|
8371
|
+
submit_review: options.submitReview
|
|
8574
8372
|
}, deps);
|
|
8575
8373
|
if (options.json) {
|
|
8576
8374
|
emit(stdout, renderJson(report));
|