diff --git a/packages/core/src/cli/commands/init.command.ts b/packages/core/src/cli/commands/init.command.ts index 73aa9dc8..7aa5240c 100644 --- a/packages/core/src/cli/commands/init.command.ts +++ b/packages/core/src/cli/commands/init.command.ts @@ -5,7 +5,7 @@ import { input, checkbox, confirm } from '@inquirer/prompts'; import { tryCatch } from '@dmno/ts-lib'; import Debug from 'debug'; -import { findDmnoServices } from '../../config-loader/find-services'; +import { findDmnoServices, selectAndApplyWorkspaceConfig } from '../../config-loader/find-services'; import { DmnoCommand } from '../lib/dmno-command'; import { joinAndCompact } from '../lib/formatting'; @@ -33,11 +33,13 @@ program.action(async (opts: { console.log(DMNO_DEV_BANNER); // console.log(kleur.gray('let us help you connect the dots ● ● ●')); - const [workspaceInfo] = await Promise.all([ + const [workspaceInfoInitial] = await Promise.all([ findDmnoServices(true), fallingDmnosAnimation(), ]); + // Handle workspace configuration selection after animation completes + const workspaceInfo = await selectAndApplyWorkspaceConfig(workspaceInfoInitial); // await fallingDmnoLoader('● Scanning repo', '● Scan complete'); diff --git a/packages/core/src/config-loader/config-loader.ts b/packages/core/src/config-loader/config-loader.ts index f36bbb66..86f171d1 100644 --- a/packages/core/src/config-loader/config-loader.ts +++ b/packages/core/src/config-loader/config-loader.ts @@ -12,7 +12,7 @@ import { ViteNodeServer } from 'vite-node/server'; import { createDeferredPromise } from '@dmno/ts-lib'; import { createDebugTimer } from '../cli/lib/debug-timer'; import { setupViteServer } from './vite-server'; -import { ScannedWorkspaceInfo, findDmnoServices } from './find-services'; +import { ScannedWorkspaceInfo, findDmnoServices, selectAndApplyWorkspaceConfig } from './find-services'; import { DmnoService, DmnoWorkspace, DmnoServiceConfig, } from '../config-engine/config-engine'; @@ -55,7 +55,8 @@ export class ConfigLoader { } private async finishInit() { - this.workspaceInfo = await findDmnoServices(); + const workspaceInfoInitial = await findDmnoServices(); + this.workspaceInfo = await selectAndApplyWorkspaceConfig(workspaceInfoInitial); // already filtered to only services with a .dmno folder const dmnoServicePackages = this.workspaceInfo.workspacePackages; diff --git a/packages/core/src/config-loader/find-services.ts b/packages/core/src/config-loader/find-services.ts index 9586e5e4..1cb896f6 100644 --- a/packages/core/src/config-loader/find-services.ts +++ b/packages/core/src/config-loader/find-services.ts @@ -5,6 +5,7 @@ import readYamlFile from 'read-yaml-file'; import { fdir } from 'fdir'; import { tryCatch } from '@dmno/ts-lib'; import Debug from 'debug'; +import { checkbox } from '@inquirer/prompts'; import { pathExists } from '../lib/fs-utils'; @@ -41,6 +42,7 @@ export type ScannedWorkspaceInfo = { workspacePackages: Array, autoSelectedPackage?: WorkspacePackagesListing, settings?: WorkspaceSettings, + workspaceConfigs?: WorkspaceConfigSource[], }; // list of locations to look for workspace project globs @@ -59,6 +61,13 @@ const WORKSPACE_SETTINGS_LOCATIONS = [ // { file: 'deno.jsonc', path: 'workspace', optional: true }, ]; +export type WorkspaceConfigSource = { + filePath: string; + settingsLocation: typeof WORKSPACE_SETTINGS_LOCATIONS[number]; + packagePatterns: string[]; + otherSettings?: WorkspaceSettings; +}; + export async function findDmnoServices(includeUnitialized = false): Promise { const startAt = new Date(); @@ -67,6 +76,7 @@ export async function findDmnoServices(includeUnitialized = false): Promise | undefined; + let workspaceConfigs: WorkspaceConfigSource[] = []; let otherSettings: WorkspaceSettings | undefined; @@ -82,9 +92,12 @@ export async function findDmnoServices(includeUnitialized = false): Promise ${settingsLocation.globsPath}`); @@ -97,16 +110,38 @@ export async function findDmnoServices(includeUnitialized = false): Promise 0) { + // If there's only one config or they're all equivalent, select immediately + if (workspaceConfigs.length === 1 || areWorkspaceConfigsEquivalent(workspaceConfigs)) { + const selectedConfig = await selectWorkspaceConfig(workspaceConfigs); + packagePatterns = selectedConfig.packagePatterns; + if (selectedConfig.otherSettings) { + otherSettings = selectedConfig.otherSettings; + } + } else { + // Return the configs to be selected by the calling code + // Use the first config temporarily for the scanning phase + packagePatterns = workspaceConfigs[0].packagePatterns; + if (workspaceConfigs[0].otherSettings) { + otherSettings = workspaceConfigs[0].otherSettings; + } + } + } // const { packageManager, rootWorkspacePath: rootServicePath } = await detectPackageManager(); @@ -205,5 +258,161 @@ export async function findDmnoServices(includeUnitialized = false): Promise p.dmnoFolder), autoSelectedPackage: packageFromPwd || packageFromCurrentPackageName, settings: otherSettings, + workspaceConfigs: workspaceConfigs.length > 1 && !areWorkspaceConfigsEquivalent(workspaceConfigs) ? workspaceConfigs : undefined, + }; +} + +function standardizePackagePatterns(patterns: string[]): string[] { + return _.uniq(patterns.map(p => p.trim()).filter(Boolean)).sort(); +} + +function areWorkspaceConfigsEquivalent(configs: WorkspaceConfigSource[]): boolean { + if (configs.length <= 1) return true; + + const standardizedPatterns = configs.map(config => + standardizePackagePatterns(config.packagePatterns) + ); + + const firstPatterns = standardizedPatterns[0]; + return standardizedPatterns.every(patterns => + _.isEqual(patterns, firstPatterns) + ); +} + +async function selectWorkspaceConfig(configs: WorkspaceConfigSource[]): Promise<{ packagePatterns: string[], otherSettings?: WorkspaceSettings }> { + if (configs.length === 0) { + throw new Error('No workspace configurations found'); + } + + if (configs.length === 1) { + return { + packagePatterns: configs[0].packagePatterns, + otherSettings: configs[0].otherSettings + }; + } + + // Check if all configurations are equivalent + if (areWorkspaceConfigsEquivalent(configs)) { + debug(`Found ${configs.length} workspace configurations, but they are equivalent - proceeding with first one`); + return { + packagePatterns: configs[0].packagePatterns, + otherSettings: configs[0].otherSettings + }; + } + + // Check if we're in a non-interactive environment + const isNonInteractive = process.env.CI || process.env.TERM === 'dumb' || !process.stdin.isTTY; + + if (isNonInteractive) { + debug(`Found ${configs.length} different workspace configurations, using first one in non-interactive mode`); + debug(`Selected: ${configs[0].filePath} - ${configs[0].packagePatterns.join(', ')}`); + return { + packagePatterns: configs[0].packagePatterns, + otherSettings: configs[0].otherSettings + }; + } + + // Show user selection for different configurations + const selectedConfigIndices = await checkbox({ + message: `Found ${configs.length} different workspace configurations.\nSelect which to use (Press to select, to toggle all, to proceed):\n`, + choices: configs.map((config, index) => ({ + name: `${config.filePath} - [${config.packagePatterns.join(', ')}]`, + value: index, + checked: index === 0 // Default to first one + })), + instructions: false + }); + + if (selectedConfigIndices.length === 0) { + throw new Error('No workspace configuration selected'); + } + + // Combine selected configurations and deduplicate + const selectedConfigs = selectedConfigIndices.map(index => configs[index]); + const allPatterns = _.flatten(selectedConfigs.map(config => config.packagePatterns)); + const deduplicatedPatterns = standardizePackagePatterns(allPatterns); + + // Merge other settings from selected configs + const mergedOtherSettings = selectedConfigs.reduce((acc, config) => { + if (config.otherSettings) { + return _.merge(acc, config.otherSettings); + } + return acc; + }, {} as WorkspaceSettings); + + debug(`Selected ${selectedConfigs.length} workspace configurations, deduplicated to ${deduplicatedPatterns.length} patterns`); + + return { + packagePatterns: deduplicatedPatterns, + otherSettings: Object.keys(mergedOtherSettings).length > 0 ? mergedOtherSettings : undefined + }; +} + +export async function selectAndApplyWorkspaceConfig(workspaceInfo: ScannedWorkspaceInfo): Promise { + if (!workspaceInfo.workspaceConfigs || workspaceInfo.workspaceConfigs.length <= 1) { + return workspaceInfo; + } + + const selectedConfig = await selectWorkspaceConfig(workspaceInfo.workspaceConfigs); + + // Get the workspace root path from the original info + const dmnoWorkspaceRootPath = workspaceInfo.workspacePackages.find(p => p.isRoot)?.path; + if (!dmnoWorkspaceRootPath) { + throw new Error('Unable to find workspace root path'); + } + + // Rebuild package paths with selected patterns + let packagePaths = [dmnoWorkspaceRootPath]; + if (workspaceInfo.isMonorepo && selectedConfig.packagePatterns?.length) { + const fullPackagePatterns = selectedConfig.packagePatterns.map((gi) => path.join(dmnoWorkspaceRootPath, gi)); + const patternsByType = _.groupBy( + fullPackagePatterns, + (s) => (s.includes('*') ? 'globs' : 'dirs'), + ); + + const expandedPathsFromGlobs = await ( + new fdir() + .withBasePath() + .onlyDirs() + .glob(...patternsByType.globs || []) + .exclude((dirName, _dirPath) => { + return dirName === 'node_modules'; + }) + .crawl(dmnoWorkspaceRootPath) + .withPromise() + ); + packagePaths.push(...patternsByType.dirs || []); + packagePaths.push(...expandedPathsFromGlobs); + packagePaths = packagePaths.map((p) => p.replace(/\/$/, '')); + packagePaths = _.uniq(packagePaths); + } + + // Rebuild workspace packages + const workspacePackages = _.compact(await Promise.all(packagePaths.map(async (packagePath) => { + const packageJson = await tryCatch( + async () => await readJsonFile(path.join(packagePath, 'package.json')), + (err) => { + if ((err as any).code === 'ENOENT') return undefined; + throw err; + }, + ); + + const dmnoFolderExists = await pathExists(path.join(packagePath, '.dmno')); + const packageName = packageJson?.name || packagePath.split('/').pop(); + + return { + isRoot: packagePath === dmnoWorkspaceRootPath, + path: packagePath, + relativePath: packagePath.substring(dmnoWorkspaceRootPath.length + 1), + name: packageName, + dmnoFolder: dmnoFolderExists, + }; + }))); + + return { + ...workspaceInfo, + workspacePackages, + settings: selectedConfig.otherSettings || workspaceInfo.settings, + workspaceConfigs: undefined // Clear this since we've made the selection }; }