diff --git a/.structify/history.json b/.structify/history.json index 5bcc320..f719eda 100644 --- a/.structify/history.json +++ b/.structify/history.json @@ -10,6 +10,22 @@ "summary": "Generated Graph" }, { + "id": "3f6df786-8828-4725-9aa7-0a279ca9d856", + "timestamp": "2026-08-03T11:42:54.024Z", + "version": "1.0.1", + "operation": "deps", + "status": "success", + "duration": 94.2069, + "filesChanged": [], + "summary": "Dependency Audit" + }, + { + "id": "0898d986-a491-4d87-a774-0ed0b2622f74", + "timestamp": "2026-08-03T11:43:04.650Z", + "version": "1.0.1", + "operation": "graph", + "status": "success", + "duration": 150.2621, "id": "411f1106-292d-4a47-a796-a158c4c39e7c", "timestamp": "2026-07-10T06:49:45.458Z", "version": "1.0.1", diff --git a/apps/cli/src/commands/index.ts b/apps/cli/src/commands/index.ts index f158b9d..f8a938c 100644 --- a/apps/cli/src/commands/index.ts +++ b/apps/cli/src/commands/index.ts @@ -438,13 +438,14 @@ export function registerCommands(program: Command): void { program .command('upgrade') - .description('Preview or apply safe Structify project upgrades') + .description('Preview or apply safe Structify project upgrades or CLI updates') .option('-d, --dry-run', 'Preview upgrade plan without writing files') .option('-y, --yes', 'Apply safe metadata/package upgrades without confirmation') + .option('--cli', 'Check for updates to the structify-tool CLI package') .option('--path ', 'Project path to upgrade') .addHelpText( 'after', - '\nExamples:\n $ structify upgrade --dry-run\n $ structify upgrade --json', + '\nExamples:\n $ structify upgrade --dry-run\n $ structify upgrade --cli\n $ structify upgrade --json', ) .action(async (options, commandInstance) => { const globalOpts = program.opts(); diff --git a/apps/cli/src/commands/upgrade.ts b/apps/cli/src/commands/upgrade.ts index e2cc46b..5764602 100644 --- a/apps/cli/src/commands/upgrade.ts +++ b/apps/cli/src/commands/upgrade.ts @@ -5,18 +5,51 @@ import { StructifyCLIError } from '../utils/error.js'; import { getElapsedMs } from '../utils/middleware.js'; import { createUpgradePlan, executePatchPlan, appendHistoryEntry } from '@structify/core'; +import { checkCliVersionUpdate } from '../utils/version.js'; + export interface UpgradeOptions { dryRun?: boolean; yes?: boolean; + cli?: boolean; path?: string; } export async function handleUpgrade(options: UpgradeOptions, context: CLIContext): Promise { const output = new CLIOutput(context); + const elapsed = getElapsedMs(context.startTime); + + if (options.cli) { + const updateCheck = await checkCliVersionUpdate(context.packageVersion); + if (context.json) { + output.json({ + success: true, + command: 'upgrade --cli', + timestamp: new Date().toISOString(), + durationMs: elapsed, + data: updateCheck, + }); + return; + } + + output.heading('Structify CLI Package Update Status'); + output.info(`Current Version: v${updateCheck.currentVersion}`); + output.info(`Latest Version on npm: v${updateCheck.latestVersion}`); + output.info(''); + if (updateCheck.updateAvailable) { + output.warn( + `An update is available! (v${updateCheck.currentVersion} → v${updateCheck.latestVersion})`, + ); + output.info(`To upgrade, run: ${updateCheck.updateCommand}`); + } else { + output.success('Structify CLI is up to date!'); + } + output.showFooter('upgrade'); + return; + } + output.heading('Structify Project Upgrade'); const projectPath = path.resolve(context.cwd, options.path ?? '.'); const upgrade = createUpgradePlan(projectPath); - const elapsed = getElapsedMs(context.startTime); if (context.json) { if (!options.dryRun && options.yes && !upgrade.reviewRequired) { diff --git a/apps/cli/src/commands/validate.ts b/apps/cli/src/commands/validate.ts index 72579a9..f9498fd 100644 --- a/apps/cli/src/commands/validate.ts +++ b/apps/cli/src/commands/validate.ts @@ -53,10 +53,31 @@ export async function handleValidate(options: ValidateOptions, context: CLIConte throw new StructifyCLIError('VALIDATION_ERROR', `Failed to parse config file JSON: ${msg}`); } } else { - throw new StructifyCLIError( - 'USAGE_ERROR', - 'Please specify a configuration path using "--config " or use "--example" to check the built-in demo configuration.', - ); + // Auto-discover structify.config.json or structify.json in cwd + const defaultConfigPath = path.join(context.cwd, 'structify.config.json'); + const altConfigPath = path.join(context.cwd, 'structify.json'); + let targetPath: string | null = null; + + if (fs.existsSync(defaultConfigPath)) { + targetPath = defaultConfigPath; + } else if (fs.existsSync(altConfigPath)) { + targetPath = altConfigPath; + } + + if (targetPath) { + output.info(`Auto-detected config file: ${path.basename(targetPath)}`); + try { + configToValidate = JSON.parse(fs.readFileSync(targetPath, 'utf8')); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + throw new StructifyCLIError('VALIDATION_ERROR', `Failed to parse config file JSON: ${msg}`); + } + } else { + throw new StructifyCLIError( + 'USAGE_ERROR', + 'No structify.config.json or structify.json found in current directory. Specify a path using "--config " or use "--example" to check the demo config.', + ); + } } const result = validateStack(configToValidate as ProjectConfig); diff --git a/apps/cli/src/context.ts b/apps/cli/src/context.ts index 3396913..c8dfd55 100644 --- a/apps/cli/src/context.ts +++ b/apps/cli/src/context.ts @@ -40,9 +40,9 @@ export function createCLIContext( ): CLIContext { const argList = args || []; const manualNoColor = argList.includes('--no-color'); - const manualVerbose = argList.includes('--verbose'); - const manualDebug = argList.includes('--debug'); - const manualJson = argList.includes('--json'); + const manualVerbose = argList.includes('--no-verbose') ? false : argList.includes('--verbose'); + const manualDebug = argList.includes('--no-debug') ? false : argList.includes('--debug'); + const manualJson = argList.includes('--no-json') ? false : argList.includes('--json'); let manualCwd: string | undefined; const cwdIdx = argList.indexOf('--cwd'); @@ -51,9 +51,9 @@ export function createCLIContext( } const targetCwd = options.cwd - ? path.resolve(options.cwd) + ? path.resolve(process.cwd(), options.cwd) : manualCwd - ? path.resolve(manualCwd) + ? path.resolve(process.cwd(), manualCwd) : process.cwd(); let detectedPackageManager: 'npm' | 'none' = 'none'; @@ -65,6 +65,10 @@ export function createCLIContext( // Fail silently } + const debugVal = options.debug !== undefined ? options.debug : manualDebug; + const verboseVal = options.verbose !== undefined ? options.verbose : manualVerbose; + const jsonVal = options.json !== undefined ? options.json : manualJson; + return { packageName: 'structify-tool', packageVersion: getCliVersion(), @@ -75,9 +79,9 @@ export function createCLIContext( nodeVersion: process.version, platform: os.platform(), arch: os.arch(), - debug: !!options.debug || manualDebug, - verbose: !!options.verbose || manualVerbose, - json: !!options.json || manualJson, + debug: debugVal, + verbose: verboseVal, + json: jsonVal, noColor: !!options.noColor || manualNoColor || process.env.NO_COLOR === 'true', isCI: process.env.CI === 'true', isTTY: process.stdout.isTTY ?? false, diff --git a/apps/cli/src/index.spec.ts b/apps/cli/src/index.spec.ts index 11a0107..9056df6 100644 --- a/apps/cli/src/index.spec.ts +++ b/apps/cli/src/index.spec.ts @@ -120,7 +120,7 @@ describe('CLI Shell Unit Tests', () => { it('should not hardcode the Commander version output', () => { const source = fs.readFileSync(path.resolve(__dirname, 'index.ts'), 'utf8'); - expect(source).toContain('.version(getCliVersion())'); + expect(source).toContain('.version(getCliVersion()'); expect(source).not.toContain(".version('1.0.0')"); expect(source).not.toContain('.version("1.0.0")'); }); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 8377b15..565f777 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -1,10 +1,12 @@ +import path from 'path'; +import fs from 'fs'; import { Command } from 'commander'; import { registerCommands } from './commands/index.js'; import { createCLIContext } from './context.js'; import { CLIOutput } from './utils/output.js'; import { StructifyCLIError } from './utils/error.js'; import { runCentralizedCleanup } from './utils/prompts.js'; -import { getCliVersion } from './utils/version.js'; +import { getCliVersion, checkCliVersionUpdate } from './utils/version.js'; async function main() { process.on('exit', () => { @@ -35,15 +37,20 @@ async function main() { .description( 'Professional platform for initializing, extending, and inspecting software architectures', ) - .version(getCliVersion()); + .version(getCliVersion(), '-v, --v, -version, -V, --version', 'Output the version number'); // Register global options program .option('--verbose', 'Print additional diagnostic logs') + .option('--no-verbose', 'Disable additional diagnostic logs') .option('--debug', 'Enable debug output and stack traces') + .option('--no-debug', 'Disable debug output and stack traces') .option('--json', 'Render machine-readable JSON payloads') + .option('--no-json', 'Disable machine-readable JSON payloads') .option('--no-color', 'Omit colored console outputs') - .option('--cwd ', 'Change context working directory'); + .option('--cwd ', 'Change context working directory') + .option('--update', 'Check or perform CLI / project updates') + .option('--upgrade', 'Check or perform CLI / project updates'); // Register commands registerCommands(program); @@ -64,6 +71,113 @@ async function main() { process.exit(0); } + // Check if process.argv consists solely of global option flags (no subcommand provided) + const rawArgs = process.argv.slice(2); + const nonFlagArgs = rawArgs.filter((arg, i) => { + if (arg.startsWith('-')) return false; + const prev = rawArgs[i - 1]; + if ( + prev && + (prev === '--cwd' || + prev === '-c' || + prev === '-p' || + prev === '--config' || + prev === '--preset' || + prev === '--output') + ) { + return false; + } + return true; + }); + + if (nonFlagArgs.length === 0) { + const globalOpts = { + json: rawArgs.includes('--no-json') ? false : rawArgs.includes('--json'), + debug: rawArgs.includes('--no-debug') ? false : rawArgs.includes('--debug'), + verbose: rawArgs.includes('--no-verbose') ? false : rawArgs.includes('--verbose'), + noColor: rawArgs.includes('--no-color'), + }; + const context = createCLIContext(process.argv, globalOpts); + const output = new CLIOutput(context); + + if (rawArgs.includes('--update') || rawArgs.includes('--upgrade')) { + const updateCheck = await checkCliVersionUpdate(getCliVersion()); + if (context.json) { + output.json({ + command: 'update', + ...updateCheck, + }); + } else { + output.heading('Structify Update Check'); + output.info(`Installed Version: v${updateCheck.currentVersion}`); + output.info(`Latest Version on npm: v${updateCheck.latestVersion}`); + output.info(''); + if (updateCheck.updateAvailable) { + output.warn( + `Update available! (v${updateCheck.currentVersion} → v${updateCheck.latestVersion})`, + ); + output.info(`Run: ${updateCheck.updateCommand}`); + } else { + output.success('Structify is up to date!'); + } + } + return; + } + + if (context.json) { + const pkgPath = path.join(context.cwd, 'package.json'); + let manifest: unknown = null; + if (fs.existsSync(pkgPath)) { + try { + manifest = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + } catch (_e) { + // ignore parse errors + } + } + + if (manifest) { + output.json(manifest); + } else { + output.json({ + name: program.name(), + version: getCliVersion(), + status: 'online', + overview: { + projectName: path.basename(context.cwd), + workingDirectory: context.cwd, + detectedPackageManager: context.detectedPackageManager, + nodeVersion: context.nodeVersion, + platform: `${context.platform} (${context.arch})`, + }, + diagnosticsState: { + debug: context.debug, + verbose: context.verbose, + json: context.json, + noColor: context.noColor, + }, + }); + } + process.exit(0); + } else if ( + rawArgs.includes('--debug') || + rawArgs.includes('--no-debug') || + rawArgs.includes('--verbose') || + rawArgs.includes('--no-verbose') + ) { + output.heading('Structify Debug & Diagnostics State'); + output.info(`Project Name: ${path.basename(context.cwd)}`); + output.info(`Version: ${getCliVersion()}`); + output.info(`Working Directory: ${context.cwd}`); + output.info(`Node Version: ${context.nodeVersion}`); + output.info(`Platform: ${context.platform} (${context.arch})`); + output.info(`Debug: ${context.debug}`); + output.info(`Verbose: ${context.verbose}`); + output.info(`JSON Output: ${context.json}`); + output.info(`No Color: ${context.noColor}`); + process.exit(0); + } + } + try { await program.parseAsync(process.argv); } catch (err) { @@ -147,6 +261,7 @@ function formatHelpScreen(program: Command, context: ReturnType (context.noColor ? text : `\x1b[1m${text}\x1b[0m`); const gray = (text: string) => (context.noColor ? text : `\x1b[90m${text}\x1b[0m`); + const showAll = process.argv.includes('--all'); const lines: string[] = []; // 1. Usage @@ -155,12 +270,12 @@ function formatHelpScreen(program: Command, context: ReturnType', + purpose: 'Scaffold a brand new project interactively', + options: + '--yes (skip questions), --preset (use template), --dry-run (preview output)', + example: 'structify init my-app --yes', + }, + { + cmd: 'structify add ', + purpose: 'Add a tool/module to existing project (e.g., docker, tailwind, prisma)', + options: '--force (overwrite files), --dry-run (preview files to be created)', + example: 'structify add docker', + }, + ], + }, + { + title: 'HEALTH & INTELLIGENCE', + cmds: [ + { + cmd: 'structify doctor', + purpose: 'Check Node version, installed tools, and project health', + options: '--fix (auto-fix issues), --json (export raw data)', + example: 'structify doctor --json', + }, + { + cmd: 'structify graph', + purpose: 'Show visual project folder & file architecture tree', + options: '--cwd (target subfolder), --json (output as JSON)', + example: 'structify graph --cwd packages/core', + }, + { + cmd: 'structify deps', + purpose: 'Check for missing, outdated, or unused npm packages', + options: '--path (check subfolder), --json (output report as JSON)', + example: 'structify deps --json', + }, + { + cmd: 'structify inspect', + purpose: 'View summary of detected stack components & status', + options: '--path (target subfolder), --json (output as JSON)', + example: 'structify inspect', + }, + ], + }, + { + title: 'MAINTENANCE & UPGRADES', + cmds: [ + { + cmd: 'structify repair', + purpose: 'Auto-fix broken config files or missing metadata', + options: '--yes (apply fixes immediately), --dry-run (preview fixes only)', + example: 'structify repair --dry-run', + }, + { + cmd: 'structify upgrade', + purpose: 'Update project dependencies or check for CLI updates', + options: + '--cli (check structify CLI update), --yes (apply updates), --dry-run (preview)', + example: 'structify upgrade --cli', + }, + ], + }, + { + title: 'CONFIGURATION & PRESETS', + cmds: [ + { + cmd: 'structify preset ', + purpose: 'Manage saved stack templates (actions: list, save, apply, delete)', + options: 'list (show presets), save (save current), apply (use preset)', + example: 'structify preset list', + }, + { + cmd: 'structify validate', + purpose: 'Audit structify.config.json file for syntax & stack errors', + options: '--config (specify file), --example (check demo config)', + example: 'structify validate --example', + }, + ], + }, + ]; + + for (const cat of CATEGORIES) { + lines.push( + `${gray('╭─')} ${bold(cat.title)} ${gray('──────────────────────────────────────────────╮')}`, + ); + for (const item of cat.cmds) { + lines.push(`${gray('│')}`); + lines.push(`${gray('│')} ${cyan(item.cmd.padEnd(32))} ${gray('→')} ${bold(item.purpose)}`); + lines.push(`${gray('│')} ${gray('Options:')} ${purple(item.options)}`); + lines.push(`${gray('│')} ${gray('Example:')} ${cyan(item.example)}`); + } + lines.push(`${gray('│')}`); + lines.push( + gray('╰──────────────────────────────────────────────────────────────────────────╯'), + ); + lines.push(''); + } + + lines.push( + `${gray('Run')} ${purple('structify help ')} ${gray('for custom command parameters')}`, + ); + lines.push( + `${gray('Run')} ${purple('structify --help --all')} ${gray('to view all extended enterprise commands')}`, + ); + lines.push(''); + return lines.join('\n'); + } + + // Full extended view (--all flag) const setupGroup: string[] = []; const intelligenceGroup: string[] = []; const templatingGroup: string[] = []; @@ -196,7 +425,7 @@ function formatHelpScreen(program: Command, context: ReturnType 0) { lines.push( `${gray('╭─')} ${bold('SETUP & WORKSPACE')} ${gray('────────────────────────────────────╮')}`, ); - for (const line of setupGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of setupGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } - // Group 2: Intelligence & Auditing if (intelligenceGroup.length > 0) { lines.push( `${gray('╭─')} ${bold('INTELLIGENCE & AUDITING')} ${gray('──────────────────────────────╮')}`, ); - for (const line of intelligenceGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of intelligenceGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } - // Group 3: Core Templating & Generation if (templatingGroup.length > 0) { lines.push( `${gray('╭─')} ${bold('TEMPLATING & ARTIFACTS')} ${gray('───────────────────────────────╮')}`, ); - for (const line of templatingGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of templatingGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } - // Group 4: Registry & Packages if (registryGroup.length > 0) { lines.push( `${gray('╭─')} ${bold('REGISTRY & PACKAGES')} ${gray('──────────────────────────────────╮')}`, ); - for (const line of registryGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of registryGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } - // Group 5: Visualization & Graphs if (graphGroup.length > 0) { lines.push( `${gray('╭─')} ${bold('VISUALIZATION & GRAPHS')} ${gray('───────────────────────────────╮')}`, ); - for (const line of graphGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of graphGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } - // Group 6: Workspace State & Migrations if (migrationGroup.length > 0) { lines.push( `${gray('╭─')} ${bold('STATE & MIGRATIONS')} ${gray('───────────────────────────────────╮')}`, ); - for (const line of migrationGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of migrationGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } - // Group 7: Performance & Reports if (performanceGroup.length > 0) { lines.push( `${gray('╭─')} ${bold('PERFORMANCE & REPORTS')} ${gray('────────────────────────────────╮')}`, ); - for (const line of performanceGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of performanceGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } - // Group 8: Deep Explanations & Diagnostics if (diagnosticGroup.length > 0) { lines.push( `${gray('╭─')} ${bold('EXPLANATIONS & DIAGNOSTICS')} ${gray('───────────────────────────╮')}`, ); - for (const line of diagnosticGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of diagnosticGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } - // Group 9: Other Commands (Dynamic fallback) if (otherGroup.length > 0) { lines.push( `${gray('╭─')} ${bold('OTHER COMMANDS')} ${gray('───────────────────────────────────────╮')}`, ); - for (const line of otherGroup) { - lines.push(`${gray('│')} ${line}`); - } + for (const line of otherGroup) lines.push(`${gray('│')} ${line}`); lines.push(gray('╰────────────────────────────────────────────────────────╯')); lines.push(''); } diff --git a/apps/cli/src/utils/version.ts b/apps/cli/src/utils/version.ts index 74323be..9c331d2 100644 --- a/apps/cli/src/utils/version.ts +++ b/apps/cli/src/utils/version.ts @@ -73,3 +73,53 @@ function isStructifyPackage(packagePath: string): boolean { return false; } } + +export interface VersionCheckResult { + currentVersion: string; + latestVersion: string; + isLatest: boolean; + updateAvailable: boolean; + updateCommand: string; +} + +export async function checkCliVersionUpdate(currentVersion?: string): Promise { + const current = currentVersion || getCliVersion(); + let latest = current; + + try { + const res = await fetch('https://registry.npmjs.org/structify-tool/latest', { + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(3000), + }); + if (res.ok) { + const data = (await res.json()) as { version?: string }; + if (data && typeof data.version === 'string') { + latest = data.version; + } + } + } catch { + // Fail silently if network/timeout occurs + } + + const updateAvailable = latest !== current && compareVersions(latest, current) > 0; + + return { + currentVersion: current, + latestVersion: latest, + isLatest: !updateAvailable, + updateAvailable, + updateCommand: 'npm install -g structify-tool@latest', + }; +} + +function compareVersions(v1: string, v2: string): number { + const p1 = v1.split('.').map(Number); + const p2 = v2.split('.').map(Number); + for (let i = 0; i < Math.max(p1.length, p2.length); i++) { + const num1 = p1[i] || 0; + const num2 = p2[i] || 0; + if (num1 > num2) return 1; + if (num1 < num2) return -1; + } + return 0; +} diff --git a/package.json b/package.json index 6140902..3160adc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "structify-monorepo", - "version": "1.0.0", + "version": "1.3.2", "private": true, "workspaces": [ "apps/*",