UNPKG

@supernovaio/cli

Version:

Supernova.io Command Line Interface

1,132 lines (1,130 loc) 49.7 kB
!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