@supernovaio/cli
Version:
Supernova.io Command Line Interface
1,132 lines (1,130 loc) • 49.7 kB
JavaScript
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6e972506-8db2-5f20-b2ad-1ee6a5ae8d44")}catch(e){}}();
import { Flags } from "@oclif/core";
import { runCodeAnalysis } from "../code-analyzer/orchestrator/run-analysis.js";
import AdmZip from "adm-zip";
import axios, { isAxiosError } from "axios";
import inquirer from "inquirer";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { z } from "zod";
import { SentryCommand } from "../types/index.js";
import { NotAuthorizedError } from "../types/not-authorized.error.js";
import { appUrlForEnvironment } from "../types/environment.js";
import { ANALYZER_DOCS_URL, SETUP_INTRO_FRAME_INNER_WIDTH, SETUP_INTRO_HEADER_ART } from "./ui/constants.js";
import { resolveCandidatePackageDirs, resolveProjectAnalyzeTarget } from "./project-setup-resolver.js";
import { colorize, renderFramedLines, renderPanel, renderStepHeader, resolveFrameContentColumnWidth, } from "./ui/ui.js";
import { watchAnalyzeStatus } from "./analyze-status.js";
import { createApiClient } from "./http-client.js";
export const AnalyzeCommandConfigSchema = z.object({
exclude: z.array(z.string()).optional(),
designSystemId: z.string().optional(),
package: z.union([z.string(), z.array(z.string())]).optional(),
dryRun: z.boolean().optional(),
includeAllProps: z.boolean().optional(),
noWait: z.boolean().optional(),
suppressDryRunNotice: z.boolean().optional(),
wizard: z.boolean().optional(),
});
const SNAPSHOT_UPLOAD_CONCURRENCY = 2;
export const analyzeFlags = {
designSystemId: Flags.string({ char: "d", description: "Design system ID" }),
package: Flags.string({
char: "p",
description: "Component package name eg. @design-system-package. Can be repeated.",
multiple: true,
}),
exclude: Flags.string({
description: "Package name or relative path to exclude from the usage scan (eg. @utility-package or packages/utility). Can be repeated.",
multiple: true,
}),
dryRun: Flags.boolean({
description: "Run and write local snapshot only. Never uploads.",
default: false,
}),
includeAllProps: Flags.boolean({
description: "Include inherited framework, DOM, and style props in component API output.",
default: false,
}),
noWait: Flags.boolean({
description: "Start processing and exit without waiting for completion.",
default: false,
}),
suppressDryRunNotice: Flags.boolean({
hidden: true,
default: false,
}),
wizard: Flags.boolean({
description: "Run full wizard flow even when flags or a saved config are present.",
default: false,
}),
};
function analyzeIntroLineTone(line, _lineIndex) {
if (line === "✅ Before you start:" || line === "🔍 What will happen next:") {
return "blueBold";
}
return "none";
}
export class AnalyzeCommandBase extends SentryCommand {
get commandId() {
return this.id ?? this.constructor.name;
}
get configSchema() {
return AnalyzeCommandConfigSchema;
}
logStepHeader(title) {
for (const line of renderStepHeader(title)) {
this.log(line);
}
}
isWizardMode(flags) {
if (flags.wizard) {
return true;
}
const config = this.configService.get();
const configuredPackages = normalizeStringList(config?.analyze?.packages);
const packageFlags = normalizeStringList(flags.package);
const excludeFlags = normalizeStringList(flags.exclude);
return (!config?.designSystemId &&
packageFlags.length === 0 &&
excludeFlags.length === 0 &&
configuredPackages.length === 0 &&
!flags.dryRun &&
!flags.noWait);
}
async promptIntro() {
const sourceFolder = process.cwd();
this.log("");
this.log(renderPanel("Supernova Code Analyze for React", [
"Analyze how your design system components are used across your codebase.",
"Results will be available in the adoption dashboard.",
"Don't worry, you can preview scan results locally before uploading.",
"",
"✅ Before you start:",
" • Run this command from your repository root",
" • Dependencies installed where needed (e.g. npm install)",
"",
"🔍 What will happen next:",
` 1. We discover packages in your current folder: ${sourceFolder}`,
" 2. You pick which ones contain your components.",
" 3. We run the static code analysis to find component usage.",
" 4. You review results and upload them to Supernova.",
"",
"⭐ The adoption dashboard is available on Enterprise plan. Upgrade at supernova.io/pricing",
"",
`${colorize("📘 Docs:", "blue")} ${ANALYZER_DOCS_URL}`,
`${colorize("💬 Need help? Contact us:", "blue")} support@supernova.io`,
], { lineTone: analyzeIntroLineTone, titleTone: "blueBold", headerArt: SETUP_INTRO_HEADER_ART }));
this.log("");
const answer = await inquirer.prompt({
type: "confirm",
name: "continue",
message: colorize("Continue?", "blue"),
default: true,
});
if (!answer.continue) {
this.exit(0);
}
}
async promptDesignSystemWizard() {
const { accessible, workspaceById } = await this.fetchAccessibleDesignSystems();
const buildSelected = (dsId) => {
const ds = accessible.find(d => d.id === dsId);
const ws = ds ? workspaceById.get(ds.workspaceId) : undefined;
if (!ds || !ws) {
this.error(`Selected design system ${dsId} could not be resolved.`);
}
return {
id: ds.id,
name: ds.meta.name,
workspaceId: ws.id,
workspacePlan: ws.subscription.product,
};
};
if (accessible.length === 1) {
const only = accessible[0];
this.log("");
this.logStepHeader("Design system (auto-selected)");
this.log(`✔ Using ${only.meta.name}`);
this.log(`Selected design system ID: ${only.id}`);
return buildSelected(only.id);
}
this.log("");
this.logStepHeader("Design system (select)");
const selectedId = await this.promptDesignSystemFromList(accessible, workspaceById);
this.log(`Selected design system ID: ${selectedId}`);
return buildSelected(selectedId);
}
async promptSourceScanIntro() {
const sourceFolder = process.cwd();
this.log("");
this.logStepHeader("Design system source scan");
this.log(renderFramedLines([
"Scanning your codebase lets Supernova understand how your design system components are actually used.",
"",
"We'll look for packages in your current folder — you'll pick which ones contain your components.",
"",
"You can review scan results before uploading.",
]));
await inquirer.prompt([
{
type: "list",
name: "selected",
message: "Proceed with scan?",
choices: [{ name: "Continue", value: "continue" }],
},
]);
this.log(`Source folder: ${sourceFolder}`);
let discovered = [];
while (discovered.length === 0) {
const packageDirs = resolveCandidatePackageDirs(sourceFolder);
discovered = packageDirs
.map(dir => ({
name: readPackageName(dir),
relativePath: path.relative(sourceFolder, dir) || ".",
}))
.sort((a, b) => a.name.localeCompare(b.name));
if (discovered.length === 0) {
this.log("");
this.logStepHeader("Discovered packages");
this.log(` No packages found in ${sourceFolder}.`);
this.log("");
this.log(" Make sure you're running this command from the root of your repository.");
this.log("");
const retryChoice = await inquirer.prompt([
{
type: "list",
name: "selected",
message: "What would you like to do?",
choices: [
{ name: "Retry package discovery", value: "retry" },
{ name: "Cancel", value: "cancel" },
],
},
]);
if (retryChoice.selected === "cancel") {
this.exit(0);
}
}
}
this.log(`Discovered packages: ${discovered.map(p => p.name).join(", ")}`);
return discovered;
}
async promptPackageSelection(discovered) {
const sourceFolder = process.cwd();
const componentLibrariesTip = "Tip: select component libraries only - skip config, SDK, and utility packages.";
const lines = [
`Found ${discovered.length} packages in ${sourceFolder}`,
"Select the packages that contain your design system components.",
"",
componentLibrariesTip,
];
this.log("");
this.logStepHeader("Component libraries");
this.log(renderFramedLines(lines, {
minContentWidth: Math.max(SETUP_INTRO_FRAME_INNER_WIDTH, ...lines.map(l => l.length)),
lineTone: (line) => (line === componentLibrariesTip ? "green" : "none"),
}));
const selectionChoice = await inquirer.prompt({
type: "checkbox",
name: "selected",
message: "Which packages contain your component libraries?",
choices: discovered.map(p => ({
name: `${p.name} (${p.relativePath})`,
value: p.name,
})),
theme: { helpMode: "always" },
validate(answer) {
if (answer.length === 0) {
return "You must select at least one package.";
}
return true;
},
});
const selectedPackages = selectionChoice.selected;
this.log(`Packages to scan: ${selectedPackages.join(", ")}`);
return selectedPackages;
}
async promptScanScope() {
const sourceFolder = process.cwd();
this.log("");
this.logStepHeader("Scan scope");
const answer = await inquirer.prompt({
type: "list",
name: "scope",
message: "What would you like to scan for component usage?",
choices: [
{ name: `Entire repository — ${sourceFolder} (recommended)`, value: "all" },
{ name: "Let me choose specific packages", value: "select" },
],
});
return answer.scope;
}
async promptExcludePackageSelection(discovered) {
if (discovered.length === 0) {
return undefined;
}
const sourceFolder = process.cwd();
const lines = [
`Found ${discovered.length} packages in ${sourceFolder}`,
"Which packages should we scan for usage of your component libraries?",
"",
"All discovered packages are selected by default. Deselect any you want to skip.",
];
this.log("");
this.logStepHeader("Packages to scan");
this.log(renderFramedLines(lines, {
minContentWidth: Math.max(SETUP_INTRO_FRAME_INNER_WIDTH, ...lines.map(l => l.length)),
}));
const selectionChoice = await inquirer.prompt({
type: "checkbox",
name: "selected",
message: "Scan these packages for component usage:",
choices: discovered.map(p => ({
name: `${p.name} (${p.relativePath})`,
value: p.name,
checked: true,
})),
theme: { helpMode: "always" },
validate(answer) {
if (answer.length === 0) {
return "You must select at least one package.";
}
return true;
},
});
const selectedScanPackages = selectionChoice.selected;
const selectedSet = new Set(selectedScanPackages);
const excludedEntries = discovered.filter(p => !selectedSet.has(p.name));
this.log(`Scanning: ${selectedScanPackages.join(", ")}`);
return excludedEntries.map(p => {
const absDir = path.resolve(sourceFolder, p.relativePath);
return tryReadPackageName(absDir) ?? p.relativePath;
});
}
async createAuthenticatedApiClient() {
try {
return await createApiClient(this.env);
}
catch (error) {
if (error instanceof NotAuthorizedError && (await this.promptLoginIfInteractive())) {
return createApiClient(this.env);
}
throw error;
}
}
async executeAnalyze(flags, scannerType, wizardMode = false, selectedDesignSystem) {
const rootDir = process.cwd();
const config = this.configService.get();
const analyzeConfig = config?.analyze ?? {};
const configuredDesignSystemId = config?.designSystemId;
const packageFlags = normalizeStringList(flags.package);
const configuredPackages = normalizeStringList(analyzeConfig.packages);
const componentPackages = wizardMode || packageFlags.length > 0 ? packageFlags : configuredPackages;
if (componentPackages.length === 0) {
this.error("Parameter --package is required.");
}
const excludeFlags = normalizeStringList(flags.exclude);
const configuredExcludes = normalizeStringList(analyzeConfig.excludedPackages);
const candidatePackageDirs = resolveCandidatePackageDirs(rootDir);
const excludes = wizardMode ? excludeFlags : [...new Set([...configuredExcludes, ...excludeFlags])];
const repoId = analyzeConfig.repoId ?? crypto.randomUUID();
const repoName = wizardMode ? inferRepoName(rootDir) : (analyzeConfig.repoName ?? inferRepoName(rootDir));
const dryRun = flags.dryRun ?? false;
const includeAllProps = flags.includeAllProps ?? false;
const noWait = flags.noWait ?? false;
const suppressDryRunNotice = flags.suppressDryRunNotice ?? false;
const analyzeDesignSystemId = flags.designSystemId ?? configuredDesignSystemId;
if (dryRun && !suppressDryRunNotice) {
this.log("Dry run enabled. Upload will be skipped.");
}
const uploadContext = dryRun
? null
: {
apiClient: await this.createAuthenticatedApiClient(),
designSystemId: analyzeDesignSystemId ?? (await this.promptDesignSystemId()),
};
const executionTargets = resolveExecutionTargets({
candidatePackageDirs,
componentPackages,
excludes,
rootDir,
scannerType,
});
const plannedTargets = executionTargets.map(executionTarget => createPlannedExecutionTarget({ executionTarget, rootDir }));
const componentsPlanned = plannedTargets.filter(t => t.executionTarget.scanType === "components");
const usagePlanned = plannedTargets.filter(t => t.executionTarget.scanType === "usage");
this.log("");
this.log("■ ✨ Static analysis");
this.log("────────────────────");
this.log("");
this.log("Analyzing your codebase...");
this.log("");
const preparedUploads = [];
const warnings = new Set();
const incompleteComponentTargets = [];
let skippedTargets = 0;
const maxPathWidth = Math.max(...plannedTargets.map(t => t.pathLabel.length), 1);
const maxTargetIdWidth = Math.max(1, ...[componentsPlanned, usagePlanned].flatMap(section => {
if (section.length === 0) {
return [0];
}
return Array.from({ length: section.length }, (_, i) => formatSequenceId("", i + 1, section.length).length);
}));
const processPlannedTarget = async (plannedTarget, localIndex, sectionTotal) => {
const targetId = formatSequenceId("", localIndex + 1, sectionTotal);
const idPadded = targetId.padEnd(maxTargetIdWidth);
const { analyzeTarget, executionTarget, packageLabel, pathLabel } = plannedTarget;
const pathPadded = pathLabel.padEnd(maxPathWidth);
if (executionTarget.scanType === "components" && !analyzeTarget) {
const missingMessage = createMissingComponentsSourceMessage(executionTarget.importFrom);
if (scannerType === "components") {
this.error(missingMessage);
}
skippedTargets += 1;
this.log(`${idPadded} - ${pathPadded} ${missingMessage}`);
return;
}
if (!analyzeTarget) {
this.error(`Analyze target could not be resolved for ${stringifyImportFrom(executionTarget.importFrom)}.`);
}
if (executionTarget.scanType === "components" && shouldWarnMissingNodeModules(analyzeTarget.rootDir)) {
const relativePath = path.relative(rootDir, analyzeTarget.rootDir) || ".";
warnings.add(`Dependencies for component source at ${relativePath} appear to be missing (no node_modules found). Results may be incomplete. Install dependencies and rerun (for example: npm install, yarn install, or pnpm install).`);
incompleteComponentTargets.push({ pathLabel, relativePath });
}
const analysisResult = await runCodeAnalysis({
designSystemId: analyzeDesignSystemId,
includeAllProps,
importFrom: analyzeTarget.importFrom,
mode: executionTarget.scanType,
projectRoot: analyzeTarget.rootDir,
snapshotBaseRoot: shouldWriteSnapshotsToExecutionRoot(analyzeTarget.rootDir) ? rootDir : undefined,
});
const hasUsage = executionTarget.scanType === "usage" && hasUsageInSnapshot(analysisResult.snapshotRoot);
if (executionTarget.scanType === "usage" && !hasUsage) {
skippedTargets += 1;
this.log(`${idPadded} - ${pathPadded} Skipped - no design system usage found`);
return;
}
if (executionTarget.scanType === "components" && analysisResult.components.length === 0) {
skippedTargets += 1;
this.log(`${idPadded} - ${pathPadded} Skipped - no components found`);
return;
}
const countSuffix = executionTarget.scanType === "components"
? `${analysisResult.components.length} components found`
: `${countUsageRecordsInSnapshot(analysisResult.snapshotRoot)} usages found`;
this.log(`${idPadded} ✓ ${pathPadded} ${countSuffix}`);
preparedUploads.push({
localSnapshotId: analysisResult.snapshotId,
packageLabel,
pathLabel,
repoId,
repoName,
repoPackageName: readPackageName(analyzeTarget.rootDir),
scannerType: executionTarget.scanType,
snapshotRoot: analysisResult.snapshotRoot,
targetId,
});
};
if (componentsPlanned.length > 0) {
this.log(`Components (${componentsPlanned.length} ${componentsPlanned.length === 1 ? "package" : "packages"})`);
for (let i = 0; i < componentsPlanned.length; i++) {
await processPlannedTarget(componentsPlanned[i], i, componentsPlanned.length);
}
}
if (usagePlanned.length > 0) {
if (componentsPlanned.length > 0) {
this.log("");
}
this.log(`Usage (${usagePlanned.length} ${usagePlanned.length === 1 ? "package" : "packages"})`);
for (let i = 0; i < usagePlanned.length; i++) {
await processPlannedTarget(usagePlanned[i], i, usagePlanned.length);
}
}
if (preparedUploads.length === 0) {
this.log("No analyzable results found. Check that you are running the command from the correct directory.");
if (warnings.size > 0) {
this.log("");
this.log("Warnings");
for (const warning of warnings) {
this.log(`- ${warning}`);
}
}
if (wizardMode) {
this.log("");
const retryChoice = await inquirer.prompt({
type: "list",
name: "selected",
message: "What would you like to do?",
choices: [
{ name: "Go back and change package selection", value: "back" },
{ name: "Cancel", value: "cancel" },
],
});
if (retryChoice.selected === "back") {
return "retry-packages";
}
this.exit(0);
}
return "completed";
}
this.log("");
this.log(`✓ Scan complete - ${preparedUploads.length} snapshots ready, ${skippedTargets} packages skipped.`);
this.log(` Packages: ${componentPackages.join(", ")}`);
this.log(` Root: ${path.basename(rootDir) || rootDir}`);
if (wizardMode) {
this.log("");
this.log("■ ✨ Scan review");
this.log("────────────────────");
const hasIncomplete = incompleteComponentTargets.length > 0;
while (true) {
this.log("✓ Scan complete.");
if (hasIncomplete) {
this.log("");
this.log(colorize("⚠ Incomplete data detected", "yellow"));
const installInstruction = "Run npm install (or yarn/pnpm) and rerun the scan for complete results.";
const isSingle = incompleteComponentTargets.length === 1;
const frameWidth = resolveFrameContentColumnWidth({
minContentWidth: Math.max(SETUP_INTRO_FRAME_INNER_WIDTH, installInstruction.length),
innerSidePadding: 2,
});
const prefix = "Missing dependencies in ";
const suffix = " (no node_modules)";
const tokens = incompleteComponentTargets.map((t, i) => i < incompleteComponentTargets.length - 1 ? `${t.relativePath},` : t.relativePath);
const headerLines = [];
let current = prefix;
for (const token of tokens) {
const candidate = current.endsWith(" ") ? `${current}${token}` : `${current} ${token}`;
if (candidate.length + suffix.length > frameWidth && current.trim() !== prefix.trim()) {
headerLines.push(current.trimEnd());
current = token;
}
else {
current = candidate;
}
}
if (current.length + suffix.length <= frameWidth) {
headerLines.push(`${current}${suffix}`);
}
else {
headerLines.push(current.trimEnd(), suffix.trimStart());
}
const panelLines = [
...headerLines,
isSingle
? "Props for components in this package could not be resolved."
: "Props for components in these packages could not be resolved.",
"",
installInstruction,
];
this.log(renderFramedLines(panelLines, {
minContentWidth: Math.max(SETUP_INTRO_FRAME_INNER_WIDTH, installInstruction.length),
lineTone: (line) => (line === installInstruction ? "yellowBold" : "yellow"),
}));
this.log("");
this.log(colorize("Uploading incomplete data may affect adoption metrics accuracy.", "gray"));
this.log("");
}
const action = await inquirer.prompt({
type: "list",
name: "selected",
message: hasIncomplete ? "Continue anyway, or cancel and fix first?" : "Next action:",
choices: hasIncomplete
? [
{ name: "Upload anyway", value: "upload" },
{ name: "Explore results", value: "explore" },
{ name: "Cancel", value: "cancel" },
]
: [
{ name: "Upload scan results and continue", value: "upload" },
{ name: "Explore results", value: "explore" },
],
});
if (action.selected === "upload") {
break;
}
if (action.selected === "cancel") {
this.exit(0);
}
this.log("");
this.log("Scan results");
this.log("");
const componentsUploads = preparedUploads.filter(upload => upload.scannerType === "components");
const usageUploads = preparedUploads.filter(upload => upload.scannerType === "usage");
if (componentsUploads.length > 0) {
this.log("Components:");
for (const prep of componentsUploads) {
this.log(`- ${prep.snapshotRoot}`);
}
}
if (usageUploads.length > 0) {
if (componentsUploads.length > 0) {
this.log("");
}
this.log("Usage:");
for (const prep of usageUploads) {
this.log(`- ${prep.snapshotRoot}`);
}
}
this.log("");
}
}
if (warnings.size > 0) {
const remainingWarnings = wizardMode && incompleteComponentTargets.length > 0
? [...warnings].filter(w => !w.startsWith("Dependencies for component source at "))
: [...warnings];
if (remainingWarnings.length > 0) {
this.log("");
this.log("Warnings");
for (const warning of remainingWarnings) {
this.log(`- ${warning}`);
}
}
}
if (dryRun) {
this.persistAnalyzeConfig({
analyzeConfig,
designSystemId: analyzeDesignSystemId,
excludes,
packages: componentPackages,
repoId,
repoName,
wizardMode,
});
this.log("Upload skipped (dry-run enabled).");
return "completed";
}
if (!uploadContext) {
this.error("Upload context is missing.");
}
const { apiClient, designSystemId } = uploadContext;
this.log("");
this.log("■ ✨ Upload analysis snapshots");
this.log("─────────────────────────────");
this.log("");
this.log("Uploading analysis snapshots to Supernova...");
const uploadResults = await this.uploadSnapshotsWithConcurrency({
apiClient,
designSystemId,
uploads: preparedUploads,
});
const successfulUploads = uploadResults.filter(result => result.success);
const failedUploads = uploadResults.filter(result => !result.success);
const componentsCount = successfulUploads.filter(upload => upload.scannerType === "components").length;
const usageCount = successfulUploads.filter(upload => upload.scannerType === "usage").length;
this.log("");
if (successfulUploads.length === 0) {
this.error("No snapshots were uploaded successfully.");
}
else {
this.log(`Uploaded snapshots: ${successfulUploads.length}/${preparedUploads.length}`);
if (failedUploads.length > 0) {
this.log(`Failed uploads: ${failedUploads.length}`);
for (const failure of failedUploads) {
this.log(`- ${failure.uploadId} Package=${failure.packageLabel} Snapshot=${failure.localSnapshotId}`);
}
}
}
this.log(`↳ Components: ${componentsCount}`);
this.log(`↳ Usage: ${usageCount}`);
const processingRun = await this.startProcessingRun({
apiClient,
designSystemId,
});
this.persistAnalyzeConfig({
analyzeConfig,
designSystemId,
excludes,
packages: componentPackages,
repoId,
repoName,
wizardMode,
});
if (noWait) {
this.log("Processing continues in the background. Use `supernova code analyze status` to check progress.");
return "completed";
}
this.log("");
const watchResult = await watchAnalyzeStatus({
apiClient,
designSystemId,
error: message => this.error(message),
log: message => this.log(message),
processingRunId: processingRun.processingRunId,
});
if (watchResult.status === "processed") {
this.log("✓ Processing complete.");
const ds = selectedDesignSystem ?? (await this.fetchDesignSystemById(designSystemId));
await this.printAnalysisComplete(ds, preparedUploads.length, componentPackages.length, wizardMode);
return "completed";
}
if (watchResult.status === "failed") {
this.log("Processing did not complete successfully. See messages above for details.");
return "completed";
}
this.log("Processing is still running. Rerun `supernova code analyze status` later to check progress.");
return "completed";
}
async fetchDesignSystemById(designSystemId) {
const client = await this.apiClient();
const { designSystems, workspaces } = await client.designSystems.listUserDesignSystems();
const ds = designSystems.find(d => d.id === designSystemId);
const ws = ds ? workspaces.find(w => w.id === ds.workspaceId) : undefined;
if (!ds || !ws) {
this.error(`Design system ${designSystemId} could not be resolved.`);
}
return {
id: ds.id,
name: ds.meta.name,
workspaceId: ws.id,
workspacePlan: ws.subscription.product,
};
}
async buildAdoptionUrl(ds) {
const client = await this.apiClient();
const { designSystemVersions } = await client.designSystems.versions.list(ds.id);
const versionId = designSystemVersions[0]?.id;
if (!versionId) {
this.error(`Design system ${ds.id} has no versions.`);
}
const { brands } = await client.designSystems.versions.brands.list(ds.id, versionId);
const brandId = brands[0]?.id;
if (!brandId) {
this.error(`Design system ${ds.id} has no brands.`);
}
return `${appUrlForEnvironment(this.env)}/${ds.workspaceId}/${ds.id}/${versionId}/${brandId}/analytics/adoption`;
}
async printAnalysisComplete(ds, snapshotCount, packageCount, wizardMode) {
const isEnterprise = ds.workspacePlan === "enterprise";
const adoptionUrl = isEnterprise ? await this.buildAdoptionUrl(ds) : "";
const completeLines = [
"🚀 Component usage data uploaded to Supernova.",
"",
`✓ Design system ${ds.name}`,
`✓ Codebase scanned ${snapshotCount} snapshots, ${packageCount} component packages`,
"✓ Results uploaded",
];
if (isEnterprise) {
completeLines.push("", "View adoption dashboard:", `🔗 ${adoptionUrl}`);
}
else {
completeLines.push("", "Want to track component adoption across your codebase in Supernova?", "Adoption dashboard is available on Enterprise plan — supernova.io/pricing");
}
this.log("");
this.logStepHeader("Analysis complete");
this.log(renderFramedLines(completeLines, {
minContentWidth: Math.max(SETUP_INTRO_FRAME_INNER_WIDTH, ...completeLines.map(l => l.length)),
}));
if (isEnterprise && wizardMode) {
this.log("");
const openChoice = await inquirer.prompt({
type: "confirm",
name: "open",
message: colorize("Open in browser?", "blue"),
default: true,
});
if (openChoice.open) {
const { default: open } = await import("open");
await open(adoptionUrl);
}
}
}
persistAnalyzeConfig(input) {
const { analyzeConfig, designSystemId, excludes, packages, repoId, repoName, wizardMode } = input;
const analyze = wizardMode
? { repoId, repoName, packages, excludedPackages: excludes }
: { ...analyzeConfig, repoId, repoName, packages, excludedPackages: excludes };
this.configService.update({
analyze,
...(designSystemId ? { designSystemId } : {}),
});
}
async uploadSnapshot(input) {
const { apiClient, designSystemId, localSnapshotId, packageLabel, repoId, repoName, repoPackageName, scannerType, snapshotRoot, targetId, uploadId, } = input;
const initPath = `/code-snapshots/upload`;
const local = await this.createArchiveFromSnapshotRoot(snapshotRoot);
const initPayload = {
archiveChecksum: local.checksum,
archiveName: local.archiveName,
archiveSize: local.sizeBytes,
designSystemId,
repoId,
repoName,
repoPackageName,
scannerType: toApiScannerType(scannerType),
};
let initResponse;
try {
const response = await apiClient.post(initPath, initPayload);
initResponse = response.data.result;
}
catch (error) {
this.log(`${uploadId} INIT ${formatStatus("fail")}`);
this.logHttpError("upload init", error);
return { localSnapshotId, packageLabel, scannerType, stage: "init", success: false, targetId, uploadId };
}
if (!initResponse.uploadUrl) {
this.error(`Snapshot upload init response does not contain uploadUrl (${scannerType}).`);
}
try {
await axios.put(initResponse.uploadUrl, local.archive, {
headers: {
"Content-Length": local.archive.length,
"Content-Type": "application/zip",
},
});
}
catch (error) {
this.log(`${uploadId} PUT ${formatStatus("fail")}`);
this.logHttpError("signed upload PUT", error);
return { localSnapshotId, packageLabel, scannerType, stage: "put", success: false, targetId, uploadId };
}
const finalizePath = `/code-snapshots/${initResponse.snapshotId}/finalize`;
try {
await apiClient.post(finalizePath, {});
}
catch (error) {
this.log(`${uploadId} FINAL ${formatStatus("fail")}`);
this.logHttpError("finalize", error);
return { localSnapshotId, packageLabel, scannerType, stage: "finalize", success: false, targetId, uploadId };
}
this.log(`✓ Uploaded ${localSnapshotId} (${toApiScannerType(scannerType)})`);
return { localSnapshotId, packageLabel, scannerType, success: true, targetId, uploadId };
}
async uploadSnapshotsWithConcurrency(input) {
const { apiClient, designSystemId, uploads } = input;
const results = Array.from({ length: uploads.length });
let nextIndex = 0;
const workerCount = Math.min(SNAPSHOT_UPLOAD_CONCURRENCY, uploads.length);
await Promise.all(Array.from({ length: workerCount }, async () => {
while (true) {
const currentIndex = nextIndex;
nextIndex += 1;
if (currentIndex >= uploads.length) {
return;
}
const upload = uploads[currentIndex];
results[currentIndex] = await this.uploadSnapshot({
apiClient,
designSystemId,
packageLabel: upload.packageLabel,
uploadId: formatSequenceId("U", currentIndex + 1, uploads.length),
repoId: upload.repoId,
repoName: upload.repoName,
repoPackageName: upload.repoPackageName,
scannerType: upload.scannerType,
snapshotRoot: upload.snapshotRoot,
localSnapshotId: upload.localSnapshotId,
targetId: upload.targetId,
});
}
})).catch(error => {
throw error;
});
return results;
}
async startProcessingRun(input) {
const { apiClient, designSystemId } = input;
const processingPath = `/code-snapshots/process-run`;
try {
const response = await apiClient.post(processingPath, {
designSystemId,
cliVersion: this.config.version,
});
return response.data.result;
}
catch (error) {
this.log(`${formatStatus("fail")} processing run`);
this.logHttpError("process-run", error);
this.error("Failed to start batch processing run.");
}
}
async createArchiveFromSnapshotRoot(snapshotRoot) {
const zip = new AdmZip();
zip.addLocalFolder(snapshotRoot);
const archive = zip.toBuffer();
const checksum = crypto.createHash("sha256").update(archive).digest("hex");
return {
archive,
archiveName: `${path.basename(snapshotRoot)}.zip`,
checksum,
sizeBytes: archive.byteLength,
};
}
logHttpError(step, error) {
if (isAxiosError(error)) {
const status = error.response?.status;
const responseData = error.response?.data;
const message = typeof responseData === "string"
? responseData
: typeof responseData?.message === "string"
? responseData.message
: error.message;
this.log(`${step}${status ? ` (${status})` : ""}: ${message}`);
return;
}
this.log(`${step}, error: ${error instanceof Error ? error.message : String(error)}`);
}
}
function toApiScannerType(scannerType) {
return scannerType === "components" ? "Components" : "Usage";
}
function readPackageName(rootDir) {
return tryReadPackageName(rootDir) ?? path.basename(rootDir);
}
function tryReadPackageName(rootDir) {
const packageJsonPath = path.join(rootDir, "package.json");
if (!fs.existsSync(packageJsonPath)) {
return null;
}
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const name = packageJson.name?.trim();
return name || null;
}
catch {
return null;
}
}
function resolveExecutionTargets(input) {
const result = [];
if (input.scannerType === "components" || input.scannerType === "all") {
result.push(...input.componentPackages.map(importFrom => ({
discovery: false,
importFrom,
rootDir: input.rootDir,
scanType: "components",
})));
}
if (input.scannerType === "usage" || input.scannerType === "all") {
result.push(...resolveUsageExecutionTargets({
candidatePackageDirs: input.candidatePackageDirs,
componentPackages: input.componentPackages,
excludes: input.excludes,
rootDir: input.rootDir,
}));
}
return result;
}
function resolveUsageExecutionTargets(input) {
const { candidatePackageDirs, componentPackages, excludes, rootDir } = input;
const excludedNames = new Set(excludes);
const excludedPaths = new Set(excludes.map(entry => path.normalize(entry)));
const componentPackageNameByRaw = resolvePackageNameMap(rootDir, candidatePackageDirs, componentPackages);
const importFromExcludingSelf = (targetName) => componentPackages.filter(componentPackage => componentPackageNameByRaw.get(componentPackage) !== targetName);
const relativePathFor = (packageDir) => path.relative(rootDir, packageDir) || ".";
const isExcluded = (packageDir, packageName) => excludedNames.has(packageName) || excludedPaths.has(relativePathFor(packageDir));
if (candidatePackageDirs.length <= 1) {
const rootPackageName = readPackageName(rootDir);
if (isExcluded(rootDir, rootPackageName)) {
return [];
}
const importFrom = importFromExcludingSelf(rootPackageName);
if (importFrom.length === 0) {
return [];
}
return [{ discovery: false, importFrom, rootDir, scanType: "usage" }];
}
const targets = [];
for (const packageDir of candidatePackageDirs) {
const packageName = readPackageName(packageDir);
if (isExcluded(packageDir, packageName)) {
continue;
}
const importFrom = importFromExcludingSelf(packageName);
if (importFrom.length === 0) {
continue;
}
targets.push({
discovery: true,
importFrom,
rootDir: packageDir,
scanType: "usage",
});
}
return targets;
}
function resolvePackageNameMap(rootDir, candidatePackageDirs, rawInputs) {
const candidateNames = new Set(candidatePackageDirs.map(packageDir => readPackageName(packageDir)));
const nameByRaw = new Map();
for (const rawInput of rawInputs) {
if (candidateNames.has(rawInput)) {
nameByRaw.set(rawInput, rawInput);
continue;
}
const explicitPath = path.resolve(rootDir, rawInput);
if (fs.existsSync(explicitPath) && fs.statSync(explicitPath).isDirectory()) {
nameByRaw.set(rawInput, readPackageName(explicitPath));
continue;
}
nameByRaw.set(rawInput, rawInput);
}
return nameByRaw;
}
function shouldWarnMissingNodeModules(rootDir) {
if (!hasDeclaredDependencies(rootDir)) {
return false;
}
return !hasNodeModulesInAncestry(rootDir);
}
function hasDeclaredDependencies(rootDir) {
const packageJsonPath = path.join(rootDir, "package.json");
if (!fs.existsSync(packageJsonPath)) {
return false;
}
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
return [
packageJson.dependencies,
packageJson.devDependencies,
packageJson.optionalDependencies,
packageJson.peerDependencies,
].some(section => section && Object.keys(section).length > 0);
}
catch {
return false;
}
}
function hasNodeModulesInAncestry(rootDir) {
let current = path.resolve(rootDir);
while (true) {
const candidate = path.join(current, "node_modules");
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
return true;
}
const parent = path.dirname(current);
if (parent === current) {
return false;
}
current = parent;
}
}
function hasUsageInSnapshot(snapshotRoot) {
return countUsageRecordsInSnapshot(snapshotRoot) > 0;
}
export function countUsageRecordsFromSnapshotPayload(usageJson) {
const usageRecordSets = [];
if (usageJson.packages) {
for (const packageSection of Object.values(usageJson.packages)) {
usageRecordSets.push(packageSection.records ?? {});
}
}
return usageRecordSets.reduce((total, usageRecords) => total + Object.values(usageRecords).filter(item => (item.count ?? 0) > 0).length, 0);
}
function countUsageRecordsInSnapshot(snapshotRoot) {
const usageFilePath = path.join(snapshotRoot, "raw", "component-usage.json");
if (!fs.existsSync(usageFilePath)) {
return 0;
}
try {
const usageJson = JSON.parse(fs.readFileSync(usageFilePath, "utf8"));
return countUsageRecordsFromSnapshotPayload(usageJson);
}
catch {
return 0;
}
}
function shouldWriteSnapshotsToExecutionRoot(projectRoot) {
return projectRoot.split(path.sep).includes("node_modules");
}
export function resolveAnalyzeTarget(input) {
const target = resolveProjectAnalyzeTarget(input);
return target ? { importFrom: target.importFrom, rootDir: target.rootDir } : null;
}
function normalizeStringList(value) {
if (!value) {
return [];
}
const list = Array.isArray(value) ? value : [value];
return [...new Set(list.map(item => item.trim()).filter(Boolean))];
}
function stringifyImportFrom(importFrom) {
return Array.isArray(importFrom) ? importFrom.join(",") : importFrom;
}
function createMissingComponentsSourceMessage(importFrom) {
return `Package ${stringifyImportFrom(importFrom)} could not be resolved from local source. Component analysis must run against source code, so the components scan was skipped. Run the command from the package source instead.`;
}
function createPlannedExecutionTarget(input) {
const { executionTarget, rootDir } = input;
const analyzeTarget = resolveAnalyzeTarget({
importFrom: executionTarget.importFrom,
rootDir: executionTarget.rootDir,
scannerType: executionTarget.scanType,
});
return {
analyzeTarget,
executionTarget,
packageLabel: stringifyImportFrom(executionTarget.importFrom),
pathLabel: formatPathLabel(rootDir, analyzeTarget?.rootDir ?? executionTarget.rootDir),
};
}
function formatSequenceId(prefix, index, total) {
const width = String(total).length;
const left = String(index).padStart(width, "0");
const right = String(total).padStart(width, "0");
return `[${prefix}${left}/${right}]`;
}
function formatPathLabel(rootDir, targetPath) {
const relativePath = path.relative(rootDir, targetPath) || ".";
if (relativePath.length <= 48) {
return relativePath;
}
const segments = relativePath.split(path.sep);
return segments.length > 2 ? `.../${segments.slice(-2).join("/")}` : relativePath;
}
function inferRepoName(startPath) {
const executionDir = resolveExecutionDir(startPath);
const repoRoot = findRepoRoot(startPath) ?? executionDir;
const packageJsonPath = path.join(repoRoot, "package.json");
if (fs.existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const packageName = packageJson.name?.trim();
if (packageName) {
return packageName;
}
}
catch {
}
}
return path.basename(repoRoot) || repoRoot;
}
function findRepoRoot(startPath) {
let currentDir = resolveExecutionDir(startPath);
while (true) {
if (fs.existsSync(path.join(currentDir, ".git"))) {
return currentDir;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
return null;
}
currentDir = parentDir;
}
}
function resolveExecutionDir(startPath) {
return fs.statSync(startPath).isDirectory() ? startPath : path.dirname(startPath);
}
function formatStatus(status) {
const labels = {
done: "DONE ",
fail: "FAIL ",
skip: "SKIP ",
start: "START",
};
const colorEnabled = process.stdout.isTTY && !process.env.NO_COLOR;
if (!colorEnabled) {
return labels[status];
}
const colors = {
done: "\u001B[32m",
fail: "\u001B[31m",
skip: "\u001B[33m",
start: "\u001B[36m",
};
return `${colors[status]}${labels[status]}\u001B[0m`;
}
//# sourceMappingURL=analyze-command.js.map
//# debugId=6e972506-8db2-5f20-b2ad-1ee6a5ae8d44