@revturbine/cli 0.13.1 → 0.15.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 +1 -0
- package/dist/cli.js +241 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,7 @@ Commands that read a config name the version explicitly — there is no default:
|
|
|
81
81
|
| `history` | The Release Version Log, newest first. |
|
|
82
82
|
| `preview` | The open draft's staged changes. |
|
|
83
83
|
| `evaluate` | Run the live config's placement/entitlement decisions for a user context. |
|
|
84
|
+
| `generate types` | Generate a TypeScript module of entitlement handles (namespaced by entitlement type) from any config version, for type-safe `can()`/`gate()` call sites. `--out <path>` writes the file; the generated header records the exact command to regenerate it. `--json` for the raw handle map. |
|
|
84
85
|
|
|
85
86
|
`--json` on read commands emits machine-readable output. Results go to stdout,
|
|
86
87
|
diagnostics to stderr.
|
package/dist/cli.js
CHANGED
|
@@ -6014,11 +6014,11 @@ function collectPublicCollisions(rows, objectType, parentField) {
|
|
|
6014
6014
|
const parent = String(row[parentField] ?? "");
|
|
6015
6015
|
const period = String(row.billing_period ?? "");
|
|
6016
6016
|
const segment = row.segment_id == null ? "_default" : String(row.segment_id);
|
|
6017
|
-
const
|
|
6017
|
+
const key2 = `${tenant} ${parent} ${period} ${segment}`;
|
|
6018
6018
|
const id = String(row.handle ?? row.id ?? "");
|
|
6019
|
-
const bucket = publicByTuple.get(
|
|
6019
|
+
const bucket = publicByTuple.get(key2);
|
|
6020
6020
|
if (bucket) bucket.ids.push(id);
|
|
6021
|
-
else publicByTuple.set(
|
|
6021
|
+
else publicByTuple.set(key2, { ids: [id], parent, period, segment });
|
|
6022
6022
|
}
|
|
6023
6023
|
const findings = [];
|
|
6024
6024
|
for (const bucket of publicByTuple.values()) {
|
|
@@ -8785,9 +8785,9 @@ function getCredential(baseUrl) {
|
|
|
8785
8785
|
}
|
|
8786
8786
|
function removeCredential(baseUrl) {
|
|
8787
8787
|
const data = read();
|
|
8788
|
-
const
|
|
8789
|
-
if (!(
|
|
8790
|
-
delete data.credentials[
|
|
8788
|
+
const key2 = urlKey(baseUrl);
|
|
8789
|
+
if (!(key2 in data.credentials)) return false;
|
|
8790
|
+
delete data.credentials[key2];
|
|
8791
8791
|
write(data);
|
|
8792
8792
|
return true;
|
|
8793
8793
|
}
|
|
@@ -9208,11 +9208,11 @@ async function revokeIngestKey(baseUrl, headers, id, fetchImpl = fetch) {
|
|
|
9208
9208
|
});
|
|
9209
9209
|
return { ok: res.ok, status: res.status };
|
|
9210
9210
|
}
|
|
9211
|
-
function formatIngestKeyLine(
|
|
9212
|
-
const origins =
|
|
9213
|
-
const ips =
|
|
9214
|
-
const last =
|
|
9215
|
-
return `${
|
|
9211
|
+
function formatIngestKeyLine(key2) {
|
|
9212
|
+
const origins = key2.originAllowlist.length ? key2.originAllowlist.join(", ") : "(none)";
|
|
9213
|
+
const ips = key2.ipAllowlist.length ? ` ips: ${key2.ipAllowlist.join(", ")}` : "";
|
|
9214
|
+
const last = key2.lastUsedAt ? ` last used ${key2.lastUsedAt}` : "";
|
|
9215
|
+
return `${key2.id} ${key2.tokenPreview} origins: ${origins}${ips}${last}`;
|
|
9216
9216
|
}
|
|
9217
9217
|
|
|
9218
9218
|
// src/lib/delegate.ts
|
|
@@ -9280,7 +9280,7 @@ function unknownTopLevelKeys(raw) {
|
|
|
9280
9280
|
const parsed = schema.safeParse(raw);
|
|
9281
9281
|
if (!parsed.success || !isRecord3(parsed.data)) return [];
|
|
9282
9282
|
const recognized = new Set(Object.keys(parsed.data));
|
|
9283
|
-
return Object.keys(raw).filter((
|
|
9283
|
+
return Object.keys(raw).filter((key2) => !recognized.has(key2) && !LEGACY_HEADER_ALIASES.has(key2));
|
|
9284
9284
|
}
|
|
9285
9285
|
function offlineAdvisories(raw) {
|
|
9286
9286
|
const advisories = [];
|
|
@@ -9292,12 +9292,12 @@ function offlineAdvisories(raw) {
|
|
|
9292
9292
|
targetRef: { ...finding2.targetRef ?? {} }
|
|
9293
9293
|
});
|
|
9294
9294
|
}
|
|
9295
|
-
for (const
|
|
9295
|
+
for (const key2 of unknownTopLevelKeys(raw)) {
|
|
9296
9296
|
advisories.push({
|
|
9297
9297
|
code: UNKNOWN_FIELD_CODE,
|
|
9298
9298
|
severity: "warning",
|
|
9299
|
-
message: `Unknown top-level field '${
|
|
9300
|
-
targetRef: { field:
|
|
9299
|
+
message: `Unknown top-level field '${key2}' is not part of the RevTurbine config schema (bundled offline snapshot). The CLI uploads it unchanged so the server can validate it \u2014 remove it if it was a typo.`,
|
|
9300
|
+
targetRef: { field: key2, path: [key2] }
|
|
9301
9301
|
});
|
|
9302
9302
|
}
|
|
9303
9303
|
return advisories;
|
|
@@ -9481,6 +9481,181 @@ function finalOutputLines(params) {
|
|
|
9481
9481
|
return lines;
|
|
9482
9482
|
}
|
|
9483
9483
|
|
|
9484
|
+
// src/lib/handles-codegen.ts
|
|
9485
|
+
var UNTYPED_GROUP = "untyped";
|
|
9486
|
+
function isRecord4(value) {
|
|
9487
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9488
|
+
}
|
|
9489
|
+
function nonEmptyString(value) {
|
|
9490
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
9491
|
+
}
|
|
9492
|
+
function extractEntitlementHandles(config) {
|
|
9493
|
+
if (!isRecord4(config) || !Array.isArray(config.entitlements)) return {};
|
|
9494
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9495
|
+
for (const entry of config.entitlements) {
|
|
9496
|
+
if (!isRecord4(entry)) continue;
|
|
9497
|
+
const handle = nonEmptyString(entry.unique_handle) ?? nonEmptyString(entry.handle);
|
|
9498
|
+
if (!handle) continue;
|
|
9499
|
+
const type = nonEmptyString(entry.type) ?? UNTYPED_GROUP;
|
|
9500
|
+
const set = groups.get(type) ?? /* @__PURE__ */ new Set();
|
|
9501
|
+
set.add(handle);
|
|
9502
|
+
groups.set(type, set);
|
|
9503
|
+
}
|
|
9504
|
+
const byType = {};
|
|
9505
|
+
for (const type of [...groups.keys()].sort()) {
|
|
9506
|
+
byType[type] = [...groups.get(type)].sort();
|
|
9507
|
+
}
|
|
9508
|
+
return byType;
|
|
9509
|
+
}
|
|
9510
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
9511
|
+
var quote = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
9512
|
+
var key = (s) => IDENTIFIER.test(s) ? s : quote(s);
|
|
9513
|
+
function renderHandlesModule(byType, opts) {
|
|
9514
|
+
const lines = [
|
|
9515
|
+
"// Generated by `revturbine generate types` \u2014 do not edit by hand.",
|
|
9516
|
+
`// Source: ${opts.source}`,
|
|
9517
|
+
"// Regenerate (rerun after any `launch` that changes entitlements):",
|
|
9518
|
+
`// ${opts.regenerate}`,
|
|
9519
|
+
"",
|
|
9520
|
+
"/** Entitlement handles, namespaced by entitlement type. */"
|
|
9521
|
+
];
|
|
9522
|
+
const types = Object.keys(byType);
|
|
9523
|
+
if (types.length === 0) {
|
|
9524
|
+
lines.push("export const Entitlements = {} as const;");
|
|
9525
|
+
} else {
|
|
9526
|
+
lines.push("export const Entitlements = {");
|
|
9527
|
+
for (const type of types) {
|
|
9528
|
+
lines.push(` ${key(type)}: {`);
|
|
9529
|
+
for (const handle of byType[type]) {
|
|
9530
|
+
lines.push(` ${key(handle)}: ${quote(handle)},`);
|
|
9531
|
+
}
|
|
9532
|
+
lines.push(" },");
|
|
9533
|
+
}
|
|
9534
|
+
lines.push("} as const;");
|
|
9535
|
+
}
|
|
9536
|
+
const flat = [...new Set(types.flatMap((t) => byType[t]))].sort();
|
|
9537
|
+
lines.push(
|
|
9538
|
+
"",
|
|
9539
|
+
"/** Every entitlement handle \u2014 the argument type for can() / gate() / checkEntitlement(). */"
|
|
9540
|
+
);
|
|
9541
|
+
if (flat.length === 0) {
|
|
9542
|
+
lines.push("export type EntitlementHandle = never;");
|
|
9543
|
+
} else {
|
|
9544
|
+
lines.push("export type EntitlementHandle =");
|
|
9545
|
+
flat.forEach((handle, i) => {
|
|
9546
|
+
lines.push(` | ${quote(handle)}${i === flat.length - 1 ? ";" : ""}`);
|
|
9547
|
+
});
|
|
9548
|
+
}
|
|
9549
|
+
lines.push(
|
|
9550
|
+
"",
|
|
9551
|
+
"/** The entitlement-type namespaces present in this Playbook. */",
|
|
9552
|
+
"export type EntitlementTypeName = keyof typeof Entitlements;",
|
|
9553
|
+
""
|
|
9554
|
+
);
|
|
9555
|
+
return lines.join("\n");
|
|
9556
|
+
}
|
|
9557
|
+
function collectStrings(values) {
|
|
9558
|
+
const out = /* @__PURE__ */ new Set();
|
|
9559
|
+
for (const value of values) {
|
|
9560
|
+
const s = nonEmptyString(value);
|
|
9561
|
+
if (s) out.add(s);
|
|
9562
|
+
}
|
|
9563
|
+
return [...out].sort();
|
|
9564
|
+
}
|
|
9565
|
+
function extractPlanHandles(config) {
|
|
9566
|
+
if (!isRecord4(config) || !Array.isArray(config.plans)) return [];
|
|
9567
|
+
return collectStrings(
|
|
9568
|
+
config.plans.map(
|
|
9569
|
+
(p) => isRecord4(p) ? nonEmptyString(p.unique_handle) ?? nonEmptyString(p.handle) ?? p.id : null
|
|
9570
|
+
)
|
|
9571
|
+
);
|
|
9572
|
+
}
|
|
9573
|
+
function extractSegmentHandles(config) {
|
|
9574
|
+
if (!isRecord4(config) || !Array.isArray(config.segments)) return [];
|
|
9575
|
+
return collectStrings(
|
|
9576
|
+
config.segments.map((s) => isRecord4(s) ? nonEmptyString(s.handle) ?? s.id : null)
|
|
9577
|
+
);
|
|
9578
|
+
}
|
|
9579
|
+
function extractSurfaceTemplateIds(config) {
|
|
9580
|
+
if (!isRecord4(config)) return [];
|
|
9581
|
+
const ids = [];
|
|
9582
|
+
if (Array.isArray(config.surface_templates)) {
|
|
9583
|
+
for (const t of config.surface_templates) {
|
|
9584
|
+
if (isRecord4(t)) ids.push(t.id ?? t.template_id);
|
|
9585
|
+
}
|
|
9586
|
+
}
|
|
9587
|
+
const surfacesOf = (payload) => {
|
|
9588
|
+
if (!isRecord4(payload) || !Array.isArray(payload.surfaces)) return;
|
|
9589
|
+
for (const surface of payload.surfaces) {
|
|
9590
|
+
if (isRecord4(surface)) ids.push(surface.template_id);
|
|
9591
|
+
}
|
|
9592
|
+
};
|
|
9593
|
+
if (Array.isArray(config.placements)) {
|
|
9594
|
+
for (const placement of config.placements) {
|
|
9595
|
+
if (!isRecord4(placement) || !Array.isArray(placement.payloads)) continue;
|
|
9596
|
+
for (const payload of placement.payloads) surfacesOf(payload);
|
|
9597
|
+
}
|
|
9598
|
+
}
|
|
9599
|
+
if (Array.isArray(config.placement_payloads)) {
|
|
9600
|
+
for (const payload of config.placement_payloads) surfacesOf(payload);
|
|
9601
|
+
}
|
|
9602
|
+
return collectStrings(ids);
|
|
9603
|
+
}
|
|
9604
|
+
function extractUiPathActionTypes(config) {
|
|
9605
|
+
if (!isRecord4(config) || !Array.isArray(config.content_ui_paths)) return [];
|
|
9606
|
+
return collectStrings(
|
|
9607
|
+
config.content_ui_paths.map((p) => isRecord4(p) ? p.action_type : null)
|
|
9608
|
+
);
|
|
9609
|
+
}
|
|
9610
|
+
function renderFlatFamily(lines, opts) {
|
|
9611
|
+
lines.push("", `/** ${opts.doc} */`);
|
|
9612
|
+
if (opts.values.length === 0) {
|
|
9613
|
+
lines.push(`export const ${opts.constName} = {} as const;`);
|
|
9614
|
+
lines.push(`export type ${opts.typeName} = never;`);
|
|
9615
|
+
return;
|
|
9616
|
+
}
|
|
9617
|
+
lines.push(`export const ${opts.constName} = {`);
|
|
9618
|
+
for (const value of opts.values) {
|
|
9619
|
+
lines.push(` ${key(value)}: ${quote(value)},`);
|
|
9620
|
+
}
|
|
9621
|
+
lines.push("} as const;");
|
|
9622
|
+
lines.push(`export type ${opts.typeName} =`);
|
|
9623
|
+
opts.values.forEach((value, i) => {
|
|
9624
|
+
lines.push(` | ${quote(value)}${i === opts.values.length - 1 ? ";" : ""}`);
|
|
9625
|
+
});
|
|
9626
|
+
}
|
|
9627
|
+
function generateHandleTypes(config, opts) {
|
|
9628
|
+
const byType = extractEntitlementHandles(config);
|
|
9629
|
+
const handles = [...new Set(Object.values(byType).flat())].sort();
|
|
9630
|
+
const lines = [renderHandlesModule(byType, opts).trimEnd()];
|
|
9631
|
+
renderFlatFamily(lines, {
|
|
9632
|
+
doc: "Plan handles \u2014 the values plan targeting and planHandle options take.",
|
|
9633
|
+
constName: "Plans",
|
|
9634
|
+
typeName: "PlanHandle",
|
|
9635
|
+
values: extractPlanHandles(config)
|
|
9636
|
+
});
|
|
9637
|
+
renderFlatFamily(lines, {
|
|
9638
|
+
doc: "Segment handles.",
|
|
9639
|
+
constName: "Segments",
|
|
9640
|
+
typeName: "SegmentHandle",
|
|
9641
|
+
values: extractSegmentHandles(config)
|
|
9642
|
+
});
|
|
9643
|
+
renderFlatFamily(lines, {
|
|
9644
|
+
doc: "Surface-template ids \u2014 catalog entries plus every template a payload references.",
|
|
9645
|
+
constName: "SurfaceTemplates",
|
|
9646
|
+
typeName: "SurfaceTemplateId",
|
|
9647
|
+
values: extractSurfaceTemplateIds(config)
|
|
9648
|
+
});
|
|
9649
|
+
renderFlatFamily(lines, {
|
|
9650
|
+
doc: "Authored ui-path action types \u2014 exactly the key set uiPathResolvers must cover.",
|
|
9651
|
+
constName: "UiPathActionTypes",
|
|
9652
|
+
typeName: "UiPathActionType",
|
|
9653
|
+
values: extractUiPathActionTypes(config)
|
|
9654
|
+
});
|
|
9655
|
+
lines.push("");
|
|
9656
|
+
return { byType, handles, ts: lines.join("\n") };
|
|
9657
|
+
}
|
|
9658
|
+
|
|
9484
9659
|
// src/lib/output.ts
|
|
9485
9660
|
var EXIT = {
|
|
9486
9661
|
OK: 0,
|
|
@@ -9809,7 +9984,7 @@ function table(rows) {
|
|
|
9809
9984
|
}
|
|
9810
9985
|
var SHOW_KINDS = ["plans", "entitlements", "segments", "placements", "trials"];
|
|
9811
9986
|
function renderShow(kind, config) {
|
|
9812
|
-
const arr = (
|
|
9987
|
+
const arr = (key2) => Array.isArray(config[key2]) ? config[key2] : [];
|
|
9813
9988
|
const handleOf = (item) => item.handle ?? item.unique_handle ?? item.id ?? "";
|
|
9814
9989
|
switch (kind) {
|
|
9815
9990
|
case "plans": {
|
|
@@ -9870,6 +10045,8 @@ Command groups:
|
|
|
9870
10045
|
Check validate, diff, show
|
|
9871
10046
|
Stage/launch upload, launch, discard, restore
|
|
9872
10047
|
Inspect status, history, preview, evaluate
|
|
10048
|
+
Codegen generate types
|
|
10049
|
+
Keys ingest-keys create|list|revoke
|
|
9873
10050
|
|
|
9874
10051
|
Version selectors (no defaults \u2014 a command that reads a config requires one):
|
|
9875
10052
|
<file> a local Config File (positional, or --file <path>)
|
|
@@ -10448,25 +10625,55 @@ program.command("evaluate").description("Run placement/entitlement decisions for
|
|
|
10448
10625
|
emit(json, true);
|
|
10449
10626
|
}
|
|
10450
10627
|
);
|
|
10628
|
+
var generateCmd = program.command("generate").description("Code generation from a config version (see: generate types).");
|
|
10629
|
+
generateCmd.command("types").description(
|
|
10630
|
+
"Generate a TypeScript module of entitlement handles (namespaced by entitlement type) for type-safe can()/gate() calls."
|
|
10631
|
+
).argument("[file...]", "Path to a Config File").option("--draft", "The tenant's open draft").option("--live", "The current live Release").option("--release <id>", "A specific playbook version / Release").option("-o, --out <path>", "Write the generated module to this path (parent dirs created); default stdout").option("--json", "Emit the handle map as JSON instead of TypeScript").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").action(
|
|
10632
|
+
async (files, opts) => {
|
|
10633
|
+
const [sel] = requireSelectors(opts, files, {
|
|
10634
|
+
count: 1,
|
|
10635
|
+
allowed: ["file", "draft", "live", "release"],
|
|
10636
|
+
command: "generate types"
|
|
10637
|
+
});
|
|
10638
|
+
const conn = sel.kind !== "file" ? connect(opts.url, opts.tenantId) : null;
|
|
10639
|
+
const config = await loadVersion(conn, sel);
|
|
10640
|
+
const selectorArg = sel.kind === "file" ? sel.path : sel.kind === "release" ? `--release ${sel.id}` : `--${sel.kind}`;
|
|
10641
|
+
const regenerate = ["revturbine generate types", selectorArg, ...opts.out ? ["--out", opts.out] : []].join(" ");
|
|
10642
|
+
const result = generateHandleTypes(config, { source: describeSelector(sel), regenerate });
|
|
10643
|
+
if (result.handles.length === 0) diag("No entitlement handles found in this config version.");
|
|
10644
|
+
const content = opts.json ? `${JSON.stringify({ source: describeSelector(sel), entitlements: result.byType }, null, 2)}
|
|
10645
|
+
` : result.ts;
|
|
10646
|
+
if (opts.out) {
|
|
10647
|
+
const resolved = path2.resolve(opts.out);
|
|
10648
|
+
mkdirSync2(path2.dirname(resolved), { recursive: true });
|
|
10649
|
+
writeFileSync2(resolved, content);
|
|
10650
|
+
diag(
|
|
10651
|
+
`\u2713 Wrote ${result.handles.length} handle(s) across ${Object.keys(result.byType).length} type(s) to ${opts.out}`
|
|
10652
|
+
);
|
|
10653
|
+
} else {
|
|
10654
|
+
process.stdout.write(content);
|
|
10655
|
+
}
|
|
10656
|
+
}
|
|
10657
|
+
);
|
|
10451
10658
|
var ingestKeys = program.command("ingest-keys").description("Manage public ingest keys (embeddable SDK telemetry credentials) for the tenant.");
|
|
10452
10659
|
ingestKeys.command("create").description("Mint a public ingest key. The full token is printed ONCE and cannot be retrieved again.").requiredOption("--origin <url...>", "Allowed browser origin(s), e.g. https://app.example.com (repeatable; at least one required)").option("--ip <cidr...>", "Optional IP / CIDR allowlist (empty = no IP restriction)").option("-u, --url <url>", "RevTurbine instance URL", DEFAULT_URL).option("-t, --tenant-id <id>", "x-tenant-id (defaults to the stored token tenant)").option("--json", "Machine-readable output (token on stdout for scripted capture)").action(async (opts) => {
|
|
10453
10660
|
const conn = connect(opts.url, opts.tenantId);
|
|
10454
10661
|
const result = await createIngestKey(conn.url, conn.headers, { originAllowlist: opts.origin, ipAllowlist: opts.ip });
|
|
10455
10662
|
if (!result.ok || !result.key) httpFail(conn, "ingest-keys create", result.status, result.error);
|
|
10456
|
-
const
|
|
10663
|
+
const key2 = result.key;
|
|
10457
10664
|
if (opts.json) {
|
|
10458
|
-
emit(
|
|
10665
|
+
emit(key2, true);
|
|
10459
10666
|
return;
|
|
10460
10667
|
}
|
|
10461
10668
|
diag("\u2713 Minted a public ingest key. STORE THE TOKEN NOW \u2014 it is shown once and cannot be retrieved again.");
|
|
10462
10669
|
emit(
|
|
10463
|
-
|
|
10670
|
+
key2,
|
|
10464
10671
|
false,
|
|
10465
10672
|
[
|
|
10466
|
-
` id: ${
|
|
10467
|
-
` token: ${
|
|
10468
|
-
` origins: ${
|
|
10469
|
-
|
|
10673
|
+
` id: ${key2.id}`,
|
|
10674
|
+
` token: ${key2.token}`,
|
|
10675
|
+
` origins: ${key2.originAllowlist.join(", ")}`,
|
|
10676
|
+
key2.ipAllowlist.length ? ` ips: ${key2.ipAllowlist.join(", ")}` : void 0
|
|
10470
10677
|
].filter((line) => Boolean(line)).join("\n")
|
|
10471
10678
|
);
|
|
10472
10679
|
});
|
|
@@ -10508,6 +10715,17 @@ var COMMAND_EXAMPLES = {
|
|
|
10508
10715
|
" revturbine diff --live --draft What the open draft would change"
|
|
10509
10716
|
].join("\n"),
|
|
10510
10717
|
show: ["", "Examples:", " revturbine show plans --live", " revturbine show segments --file ./config.json"].join("\n"),
|
|
10718
|
+
generate: [
|
|
10719
|
+
"",
|
|
10720
|
+
"Examples:",
|
|
10721
|
+
" revturbine generate types revturbine.playbook.json --out src/revturbine-handles.ts",
|
|
10722
|
+
" revturbine generate types --live --out src/lib/revturbine-handles.gen.ts",
|
|
10723
|
+
" revturbine generate types --draft --json",
|
|
10724
|
+
"",
|
|
10725
|
+
"The generated module exports `Entitlements` (handles namespaced by",
|
|
10726
|
+
"entitlement type) and the `EntitlementHandle` union for can()/gate()",
|
|
10727
|
+
"call sites. Its header records the exact command to regenerate it."
|
|
10728
|
+
].join("\n"),
|
|
10511
10729
|
upload: ["", "Example:", " revturbine upload ./config.json Stage as the open draft"].join("\n"),
|
|
10512
10730
|
launch: [
|
|
10513
10731
|
"",
|
package/package.json
CHANGED