@amityco/social-plus-vise 0.12.4 → 0.12.5
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/server.js +18 -3
- package/dist/tools/design.js +248 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -251,6 +251,7 @@ The flow above is what the skill teaches your AI agent. You — the human — dr
|
|
|
251
251
|
| `vise design check [path]` | Advisory, **non-blocking** report on how closely the UI code matches the contract (token coverage + on/off-contract color literals). Never fails a build and is **not** a `vise check` gate |
|
|
252
252
|
| `vise design preview [path] [--reference <prototype>]` | Write a self-contained `sp-vise/design-preview.html`: the contract's tokens as visual swatches + the conformance report + the HTML reference embedded for side-by-side review. Vise renders the artifact; a human/VLM judges the visual match. Dependency-free — **not** an automated pixel diff |
|
|
253
253
|
| `vise design reference [path] [--title <name>]` | Write a self-contained `sp-vise/design-reference.html`: human/VLM-readable design-system spec — token swatches, type samples, component demos, and a growth-layer summary. Pairs with `design-contract.json` (machine-readable). Use `--title` to name the design system (e.g. `--title Streamly`). Advisory — **not** an enforcement gate |
|
|
254
|
+
| `vise design init-tokens [path] [--force]` | Scaffold `src/styles/social-plus-tokens.css` — the dedicated, customer-editable token file for social.plus features. **Greenfield:** neutral defaults (full `--sp-*` token set). **Brownfield:** seeded from your existing concrete tokens. Idempotent — never overwrites an existing file (use `--force` to override). After editing, run `vise design extract --from-project` to refresh the contract. `design_init_tokens` |
|
|
254
255
|
|
|
255
256
|
The extracted contract is **advisory input for generation**, not an enforcement gate: a token-poor prototype yields a weaker — never wrong — contract, and absence of a prototype simply means no contract (the existing `*.design.reuse-detected-tokens` rules still cover reuse of a host project's own design system).
|
|
256
257
|
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
|
|
10
|
-
import { designCheckTool, designExtractTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
|
|
10
|
+
import { designCheckTool, designExtractTool, designInitTokensTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
|
|
11
11
|
import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
|
|
12
12
|
import { planHarnessTool } from "./tools/harness.js";
|
|
13
13
|
import { planIntegrationTool } from "./tools/integration.js";
|
|
@@ -38,6 +38,7 @@ const tools = new Map([
|
|
|
38
38
|
designCheckTool,
|
|
39
39
|
designPreviewTool,
|
|
40
40
|
designReferenceTool,
|
|
41
|
+
designInitTokensTool,
|
|
41
42
|
].map((tool) => [tool.name, tool]));
|
|
42
43
|
const bundledSkillName = "social-plus-vise";
|
|
43
44
|
const cliResult = await handleCli(process.argv.slice(2));
|
|
@@ -305,7 +306,15 @@ async function handleCli(args) {
|
|
|
305
306
|
});
|
|
306
307
|
return "exit";
|
|
307
308
|
}
|
|
308
|
-
|
|
309
|
+
if (sub === "init-tokens") {
|
|
310
|
+
assertOnlyKnownFlags(subArgs, ["force"], "design init-tokens");
|
|
311
|
+
await printToolResult(designInitTokensTool, {
|
|
312
|
+
repoPath: positionalRepoPath(subArgs),
|
|
313
|
+
force: hasFlag(subArgs, "force"),
|
|
314
|
+
});
|
|
315
|
+
return "exit";
|
|
316
|
+
}
|
|
317
|
+
console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", "preview", "reference", or "init-tokens".`);
|
|
309
318
|
process.exitCode = 1;
|
|
310
319
|
return "exit";
|
|
311
320
|
}
|
|
@@ -515,6 +524,7 @@ Usage:
|
|
|
515
524
|
vise design check [repoPath]
|
|
516
525
|
vise design preview [repoPath] [--reference <prototypePath>]
|
|
517
526
|
vise design reference [repoPath] [--title "Streamly"] [--no-write]
|
|
527
|
+
vise design init-tokens [repoPath] [--force]
|
|
518
528
|
|
|
519
529
|
extract Build a graded design contract and write it to sp-vise/design-contract.json.
|
|
520
530
|
Declared CSS custom properties become exact tokens; repeated literal values
|
|
@@ -535,7 +545,12 @@ preview Write a self-contained sp-vise/design-preview.html: the contract's to
|
|
|
535
545
|
reference Write a self-contained sp-vise/design-reference.html: human/VLM-readable
|
|
536
546
|
design-system spec (token swatches, type samples, component demos, growth-layer
|
|
537
547
|
summary). Pairs with the machine-readable design-contract.json. Use --title to
|
|
538
|
-
set the design system name. Advisory — not an enforcement gate
|
|
548
|
+
set the design system name. Advisory — not an enforcement gate.
|
|
549
|
+
init-tokens Scaffold src/styles/social-plus-tokens.css in the customer's project —
|
|
550
|
+
the dedicated editable token file for social.plus features. Greenfield:
|
|
551
|
+
neutral defaults. Brownfield (existing tokens found): seeded from their
|
|
552
|
+
concrete values. Idempotent (never overwrites); use --force to overwrite.
|
|
553
|
+
After editing, run vise design extract --from-project to refresh the contract.`;
|
|
539
554
|
}
|
|
540
555
|
return `${packageName}
|
|
541
556
|
|
package/dist/tools/design.js
CHANGED
|
@@ -157,7 +157,16 @@ export async function extractDesignContractFromProject(repoPath) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
inputs.sort();
|
|
160
|
-
|
|
160
|
+
// Hash each source file so design check can detect staleness without re-extracting.
|
|
161
|
+
const input_digests = {};
|
|
162
|
+
for (const rel of inputs) {
|
|
163
|
+
try {
|
|
164
|
+
const content = await readFile(path.join(root, rel), "utf8");
|
|
165
|
+
input_digests[rel] = `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
166
|
+
}
|
|
167
|
+
catch { /* file read already succeeded above; defensive only */ }
|
|
168
|
+
}
|
|
169
|
+
return buildDesignContract({ css, html: [], inputs }, { kind: "host-project", inputs, input_digests, file_count: inputs.length }, moduleTokens);
|
|
161
170
|
}
|
|
162
171
|
export async function writeDesignContract(repoPath, contract) {
|
|
163
172
|
const sidecarDir = path.join(path.resolve(repoPath), "sp-vise");
|
|
@@ -381,6 +390,215 @@ function safeCss(value) {
|
|
|
381
390
|
return value.replace(/[<>"]/g, "").slice(0, 200);
|
|
382
391
|
}
|
|
383
392
|
// ---------------------------------------------------------------------------
|
|
393
|
+
// Social-plus token scaffold (vise design init-tokens)
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
//
|
|
396
|
+
// Creates a dedicated `src/styles/social-plus-tokens.css` in the customer's
|
|
397
|
+
// project — the single editable source for social.plus feature styling.
|
|
398
|
+
// The contract always points at this file; customers edit it freely without
|
|
399
|
+
// needing an AI agent. Design check detects changes and prompts re-extract.
|
|
400
|
+
//
|
|
401
|
+
// Two cases:
|
|
402
|
+
// Greenfield (no existing design system): scaffold Option-B neutral defaults.
|
|
403
|
+
// Brownfield (existing tokens found): seed from their concrete values.
|
|
404
|
+
// Already exists: leave it untouched (idempotent — never clobbers edits).
|
|
405
|
+
/** Relative path within the customer's project where sp tokens live. */
|
|
406
|
+
export const SP_TOKENS_PATH = "src/styles/social-plus-tokens.css";
|
|
407
|
+
/** Option-B neutral default — a clean, adaptive light-mode system using system
|
|
408
|
+
* fonts. All token names use `--sp-` prefix to avoid collision with the
|
|
409
|
+
* customer's own design system. Will be replaced with the official social.plus
|
|
410
|
+
* palette (Option A) once that palette is finalised. */
|
|
411
|
+
export const NEUTRAL_SP_TOKENS_DEFAULT = `/* social-plus-tokens.css — social.plus feature design system.
|
|
412
|
+
* This file controls the look of all social.plus features in your app.
|
|
413
|
+
* Edit freely. Run: vise design extract --from-project . to refresh the contract.
|
|
414
|
+
* NOTE: design check scans the whole project; the main-app palette appearing as
|
|
415
|
+
* "off-contract" is expected and advisory only — not a failure. */
|
|
416
|
+
:root {
|
|
417
|
+
/* ── Brand / interactive ───────────────────────────────── */
|
|
418
|
+
--sp-color-brand: #1054DE;
|
|
419
|
+
--sp-color-brand-hover: #0D47C5;
|
|
420
|
+
--sp-color-brand-subtle: #EEF3FF;
|
|
421
|
+
--sp-color-brand-text: #FFFFFF;
|
|
422
|
+
|
|
423
|
+
/* ── Backgrounds ───────────────────────────────────────── */
|
|
424
|
+
--sp-color-bg: #FFFFFF;
|
|
425
|
+
--sp-color-surface: #F8F9FA;
|
|
426
|
+
--sp-color-surface-raised: #FFFFFF;
|
|
427
|
+
--sp-color-surface-hover: #F0F2F4;
|
|
428
|
+
--sp-color-overlay: rgba(0, 0, 0, 0.5);
|
|
429
|
+
|
|
430
|
+
/* ── Text ──────────────────────────────────────────────── */
|
|
431
|
+
--sp-color-text: #0D1017;
|
|
432
|
+
--sp-color-text-muted: #5C6370;
|
|
433
|
+
--sp-color-text-faint: #9AA0AD;
|
|
434
|
+
--sp-color-text-disabled: #C1C7CE;
|
|
435
|
+
--sp-color-text-on-brand: #FFFFFF;
|
|
436
|
+
|
|
437
|
+
/* ── Semantic ──────────────────────────────────────────── */
|
|
438
|
+
--sp-color-success: #1FAF64;
|
|
439
|
+
--sp-color-warning: #F59E0B;
|
|
440
|
+
--sp-color-error: #EF4444;
|
|
441
|
+
--sp-color-info: #3B82F6;
|
|
442
|
+
|
|
443
|
+
/* ── Border ────────────────────────────────────────────── */
|
|
444
|
+
--sp-color-border: #E5E7EB;
|
|
445
|
+
--sp-color-border-strong: #D1D5DB;
|
|
446
|
+
|
|
447
|
+
/* ── Typography ────────────────────────────────────────── */
|
|
448
|
+
--sp-font-body: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
449
|
+
--sp-font-display: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
450
|
+
--sp-fs-xs: 12px;
|
|
451
|
+
--sp-fs-sm: 14px;
|
|
452
|
+
--sp-fs-md: 16px;
|
|
453
|
+
--sp-fs-lg: 20px;
|
|
454
|
+
--sp-fs-xl: 24px;
|
|
455
|
+
--sp-fs-2xl: 32px;
|
|
456
|
+
--sp-fw-regular: 400;
|
|
457
|
+
--sp-fw-medium: 500;
|
|
458
|
+
--sp-fw-bold: 700;
|
|
459
|
+
--sp-lh-tight: 1.2;
|
|
460
|
+
--sp-lh-normal: 1.5;
|
|
461
|
+
--sp-lh-relaxed: 1.7;
|
|
462
|
+
--sp-ls-tight: -0.01em;
|
|
463
|
+
--sp-ls-wide: 0.04em;
|
|
464
|
+
|
|
465
|
+
/* ── Spacing ───────────────────────────────────────────── */
|
|
466
|
+
--sp-space-1: 4px;
|
|
467
|
+
--sp-space-2: 8px;
|
|
468
|
+
--sp-space-3: 12px;
|
|
469
|
+
--sp-space-4: 16px;
|
|
470
|
+
--sp-space-5: 24px;
|
|
471
|
+
--sp-space-6: 32px;
|
|
472
|
+
--sp-space-8: 48px;
|
|
473
|
+
--sp-space-10: 64px;
|
|
474
|
+
|
|
475
|
+
/* ── Radius ────────────────────────────────────────────── */
|
|
476
|
+
--sp-radius-sm: 6px;
|
|
477
|
+
--sp-radius-md: 10px;
|
|
478
|
+
--sp-radius-lg: 16px;
|
|
479
|
+
--sp-radius-xl: 24px;
|
|
480
|
+
--sp-radius-pill: 999px;
|
|
481
|
+
|
|
482
|
+
/* ── Border width ──────────────────────────────────────── */
|
|
483
|
+
--sp-border-width-thin: 1px;
|
|
484
|
+
--sp-border-width-base: 2px;
|
|
485
|
+
--sp-border-width-thick: 4px;
|
|
486
|
+
|
|
487
|
+
/* ── Elevation ─────────────────────────────────────────── */
|
|
488
|
+
--sp-shadow-1: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
489
|
+
--sp-shadow-2: 0 4px 16px rgba(0, 0, 0, 0.10);
|
|
490
|
+
--sp-shadow-3: 0 16px 40px rgba(0, 0, 0, 0.14);
|
|
491
|
+
|
|
492
|
+
/* ── Opacity ───────────────────────────────────────────── */
|
|
493
|
+
--sp-opacity-disabled: 0.4;
|
|
494
|
+
--sp-opacity-muted: 0.7;
|
|
495
|
+
|
|
496
|
+
/* ── Motion ────────────────────────────────────────────── */
|
|
497
|
+
--sp-duration-fast: 120ms;
|
|
498
|
+
--sp-duration-base: 200ms;
|
|
499
|
+
--sp-duration-slow: 400ms;
|
|
500
|
+
--sp-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
|
501
|
+
--sp-ease-emphasized:cubic-bezier(0.2, 0, 0, 1);
|
|
502
|
+
|
|
503
|
+
/* ── Sizing ────────────────────────────────────────────── */
|
|
504
|
+
--sp-size-icon-sm: 16px;
|
|
505
|
+
--sp-size-icon-md: 24px;
|
|
506
|
+
--sp-size-icon-lg: 32px;
|
|
507
|
+
--sp-control-height-sm: 32px;
|
|
508
|
+
--sp-control-height-md: 40px;
|
|
509
|
+
--sp-control-height-lg: 48px;
|
|
510
|
+
|
|
511
|
+
/* ── Z-index ───────────────────────────────────────────── */
|
|
512
|
+
--sp-z-nav: 10;
|
|
513
|
+
--sp-z-dropdown: 100;
|
|
514
|
+
--sp-z-modal: 1000;
|
|
515
|
+
--sp-z-toast: 2000;
|
|
516
|
+
|
|
517
|
+
/* ── Breakpoints ───────────────────────────────────────── */
|
|
518
|
+
--sp-bp-sm: 640px;
|
|
519
|
+
--sp-bp-md: 768px;
|
|
520
|
+
--sp-bp-lg: 1024px;
|
|
521
|
+
--sp-bp-xl: 1280px;
|
|
522
|
+
}
|
|
523
|
+
`;
|
|
524
|
+
export const designInitTokensTool = {
|
|
525
|
+
name: "design_init_tokens",
|
|
526
|
+
description: "Scaffold src/styles/social-plus-tokens.css in the customer's project — the dedicated, customer-editable token file for social.plus features. Greenfield: neutral defaults. Brownfield (existing tokens found): seed from their concrete values. Idempotent: never overwrites an existing file.",
|
|
527
|
+
inputSchema: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
repoPath: { type: "string", description: "Project root. Defaults to the current directory." },
|
|
531
|
+
force: { type: "boolean", description: "Overwrite existing social-plus-tokens.css (default false — never clobbers)." },
|
|
532
|
+
},
|
|
533
|
+
additionalProperties: false,
|
|
534
|
+
},
|
|
535
|
+
async call(input) {
|
|
536
|
+
const args = objectInput(input);
|
|
537
|
+
const repoPath = optionalStringField(args, "repoPath") ?? ".";
|
|
538
|
+
const force = args.force === true;
|
|
539
|
+
return textResult(await initSpTokens(repoPath, force));
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
export async function initSpTokens(repoPath, force = false) {
|
|
543
|
+
const root = path.resolve(repoPath);
|
|
544
|
+
const target = path.join(root, SP_TOKENS_PATH);
|
|
545
|
+
// Idempotent: don't overwrite unless forced.
|
|
546
|
+
try {
|
|
547
|
+
await stat(target);
|
|
548
|
+
if (!force) {
|
|
549
|
+
return {
|
|
550
|
+
status: "exists",
|
|
551
|
+
file: target,
|
|
552
|
+
message: `${SP_TOKENS_PATH} already exists — skipping. Use --force to overwrite.`,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch { /* file doesn't exist — proceed to scaffold */ }
|
|
557
|
+
// Try brownfield seeding: extract concrete token values from the existing project.
|
|
558
|
+
const existingContract = await extractDesignContractFromProject(root);
|
|
559
|
+
const hasBrownfieldTokens = existingContract.tokens.length > 0;
|
|
560
|
+
let css;
|
|
561
|
+
let seededFrom;
|
|
562
|
+
if (hasBrownfieldTokens) {
|
|
563
|
+
// Seed from their existing concrete values, namespaced as --sp-*.
|
|
564
|
+
const lines = [
|
|
565
|
+
`/* social-plus-tokens.css — social.plus feature design system.`,
|
|
566
|
+
` * Seeded from your existing design tokens on ${new Date().toISOString().slice(0, 10)}.`,
|
|
567
|
+
` * Edit freely to customize social.plus features independently from your main app.`,
|
|
568
|
+
` * Run: vise design extract --from-project . to refresh the contract. */`,
|
|
569
|
+
`:root {`,
|
|
570
|
+
];
|
|
571
|
+
const categories = [...new Set(existingContract.tokens.map((t) => t.category))];
|
|
572
|
+
for (const cat of categories) {
|
|
573
|
+
const tokensInCat = existingContract.tokens.filter((t) => t.category === cat && t.name);
|
|
574
|
+
if (tokensInCat.length === 0)
|
|
575
|
+
continue;
|
|
576
|
+
lines.push(``, ` /* ── ${cat} ──────────────────────────────────────────── */`);
|
|
577
|
+
for (const t of tokensInCat) {
|
|
578
|
+
const spName = `--sp-${t.name.replace(/^--/, "")}`;
|
|
579
|
+
lines.push(` ${spName}: ${t.value}; /* seeded from ${t.name} */`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
lines.push(`}`);
|
|
583
|
+
css = lines.join("\n") + "\n";
|
|
584
|
+
seededFrom = existingContract.source.inputs;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
// Greenfield: neutral Option-B defaults.
|
|
588
|
+
css = NEUTRAL_SP_TOKENS_DEFAULT;
|
|
589
|
+
}
|
|
590
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
591
|
+
await writeFile(target, css, "utf8");
|
|
592
|
+
return {
|
|
593
|
+
status: hasBrownfieldTokens ? "seeded" : "scaffolded",
|
|
594
|
+
file: target,
|
|
595
|
+
...(seededFrom ? { seeded_from: seededFrom } : {}),
|
|
596
|
+
message: hasBrownfieldTokens
|
|
597
|
+
? `Seeded ${SP_TOKENS_PATH} from your existing design tokens. Edit it to customize social.plus features, then run vise design extract --from-project .`
|
|
598
|
+
: `Scaffolded ${SP_TOKENS_PATH} with neutral defaults. Fill in your colors, then run vise design extract --from-project .`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
384
602
|
// Design-system reference document (human/VLM-readable, advisory)
|
|
385
603
|
// ---------------------------------------------------------------------------
|
|
386
604
|
//
|
|
@@ -927,6 +1145,9 @@ export async function runDesignCheck(repoPath) {
|
|
|
927
1145
|
note: ADVISORY_NOTE,
|
|
928
1146
|
};
|
|
929
1147
|
}
|
|
1148
|
+
// Freshness check: compare source file content hashes to those recorded at extract time.
|
|
1149
|
+
// Advisory only — never blocks, just surfaces a nudge to re-extract.
|
|
1150
|
+
const staleContract = await checkContractFreshness(repoRoot, contract);
|
|
930
1151
|
const files = (await collectFiles(repoRoot, MAX_SCAN_FILES)).filter((file) => SCAN_EXTS.has(path.extname(file).toLowerCase()));
|
|
931
1152
|
if (files.length === 0) {
|
|
932
1153
|
return { status: "no-sources", message: "No UI source files found to check against the contract.", contract: contractSummary(contract), note: ADVISORY_NOTE };
|
|
@@ -1018,9 +1239,35 @@ export async function runDesignCheck(repoPath) {
|
|
|
1018
1239
|
count: undefinedRefs.length,
|
|
1019
1240
|
sample: undefinedRefs.slice(0, OFF_CONTRACT_SAMPLE),
|
|
1020
1241
|
},
|
|
1242
|
+
...(staleContract ? { staleContract } : {}),
|
|
1021
1243
|
note: ADVISORY_NOTE,
|
|
1022
1244
|
};
|
|
1023
1245
|
}
|
|
1246
|
+
/** Compare source.inputs file content against hashes recorded at extract time.
|
|
1247
|
+
* Returns null if the contract is fresh or has no recorded digests. */
|
|
1248
|
+
async function checkContractFreshness(repoRoot, contract) {
|
|
1249
|
+
const recorded = contract.source?.input_digests;
|
|
1250
|
+
if (!recorded || Object.keys(recorded).length === 0)
|
|
1251
|
+
return undefined;
|
|
1252
|
+
const changed = [];
|
|
1253
|
+
for (const [rel, storedDigest] of Object.entries(recorded)) {
|
|
1254
|
+
try {
|
|
1255
|
+
const content = await readFile(path.join(repoRoot, rel), "utf8");
|
|
1256
|
+
const currentDigest = `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
1257
|
+
if (currentDigest !== storedDigest)
|
|
1258
|
+
changed.push(rel);
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
changed.push(rel); // file deleted or unreadable — also stale
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (changed.length === 0)
|
|
1265
|
+
return undefined;
|
|
1266
|
+
return {
|
|
1267
|
+
changedFiles: changed,
|
|
1268
|
+
hint: `Run \`vise design extract --from-project\` to refresh the contract against the updated file(s).`,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1024
1271
|
function dedupeByToken(refs) {
|
|
1025
1272
|
const seen = new Set();
|
|
1026
1273
|
const out = [];
|
package/package.json
CHANGED