UNPKG

@interopio/desktop-cli

Version:

CLI tool for setting up, building and packaging io.Connect Desktop projects

288 lines 12.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.CreateService = void 0; const node_path_1 = require("node:path"); const logger_1 = require("../utils/logger"); const error_handler_1 = require("../utils/error.handler"); const template_service_1 = require("./template.service"); const node_fs_1 = require("node:fs"); const colors_1 = require("../utils/colors"); const clack = __importStar(require("@clack/prompts")); /** * SetupService handles the interactive project setup flow using @clack/prompts. * * This service provides a branded, user-friendly setup experience that: * - Guides users through project configuration steps * - Validates user inputs and provides helpful defaults * - Collects feature selections and optional configurations * - Generates project structure using the TemplateService * - Creates license files when license keys are provided * * The setup flow follows the design document specification with: * 1. Product name input with validation * 2. Auto-generated folder name (editable) * 3. Multi-select feature customizations * 4. Auto-tests yes/no (defaults to Yes) * 5. Optional license key (hidden input) * 6. Confirmation before creation * 7. Progress indication during generation */ class CreateService { logger = logger_1.Logger.getInstance(); templateService = new template_service_1.TemplateService(); /** * Run the interactive setup flow */ async runSetup() { try { const result = await this.collectSetupData(); await this.confirmAndCreate(result); clack.outro((0, colors_1.green)('✨ Project ready!')); } catch (error) { if (clack.isCancel(error)) { clack.cancel('Setup cancelled.'); process.exit(0); } throw error; } } /** * Collect all setup data through interactive prompts */ async collectSetupData() { // Step 1: Product Name const productName = await clack.text({ message: 'What is your product name (can include spaces)?', placeholder: 'My Desktop App', validate: (value) => { if (!value.trim()) { return 'Product name is required'; } if (value.trim().length < 2) { return 'Product name must be at least 2 characters'; } if (!/^[a-z0-9\s\-_.()]+$/i.test(value.trim())) { return 'Product name can only contain letters, numbers, spaces, hyphens, underscores, periods and parentheses.'; } return undefined; } }); if (clack.isCancel(productName)) throw productName; // Step 2: Folder Name (auto-generated from product name) const defaultFolderName = this.generateFolderName(productName); const folderName = await clack.text({ message: 'What folder should we create for your project (no spaces; used for folder, exe, and app bundle)?', placeholder: defaultFolderName, validate: (value) => { if (!value.trim()) { return 'Folder name is required'; } if (!/^[a-z0-9-_]+$/i.test(value.trim())) { return 'Folder name can only contain letters, numbers, hyphens, and underscores'; } return undefined; } }); if (clack.isCancel(folderName)) throw folderName; // Step 3 & 4: Feature Selection (handle both multi-select and separate questions) const selectedFeatures = await this.collectFeatureSelections(); // Step 5: License Key (optional, multi-line editor) const hasLicense = await clack.confirm({ message: 'Do you have a valid license?', initialValue: false }); if (clack.isCancel(hasLicense)) throw hasLicense; let licenseKey; if (hasLicense) { clack.note('You can paste multi-line JSON. Press Enter when done.', 'License Input'); const licenseInput = await clack.text({ message: 'Paste your license.json content here:', validate: (value) => { if (!value.trim()) { return 'License content is required if you have a license'; } // Try to parse as JSON to validate format try { JSON.parse(value.trim()); return undefined; } catch { return 'Please provide valid JSON format for the license'; } } }); if (clack.isCancel(licenseInput)) throw licenseInput; licenseKey = licenseInput; } return { productName, productSlug: folderName, targetDirectory: (0, node_path_1.resolve)(process.cwd(), folderName), features: selectedFeatures, autoTests: selectedFeatures.includes('auto-tests'), licenseKey: licenseKey || undefined }; } /** * Collect feature selections using setupUI configuration */ async collectFeatureSelections() { const availableFeatures = this.templateService.getAvailableFeatureTemplates(); const selectedFeatures = []; // Separate features into multi-select and separate questions const multiSelectFeatures = availableFeatures.filter(template => !template.setupUI?.separate); const separateFeatures = availableFeatures.filter(template => template.setupUI?.separate); // Handle multi-select features if (multiSelectFeatures.length > 0) { const featureOptions = multiSelectFeatures.map(template => ({ value: template.name, label: template.setupUI?.text || template.description || template.displayName || template.name })); // Set initial selections based on setupUI.selected const initialSelections = multiSelectFeatures .filter(template => template.setupUI?.selected === true) .map(template => template.name); const features = await clack.multiselect({ message: 'Select any customizations, and press enter', options: featureOptions, initialValues: initialSelections, required: false }); if (clack.isCancel(features)) throw features; selectedFeatures.push(...features); } // Handle separate questions for (const template of separateFeatures) { const confirmed = await clack.confirm({ message: template.setupUI?.text || template.description || template.displayName || template.name, initialValue: template.setupUI?.selected ?? false }); if (clack.isCancel(confirmed)) throw confirmed; if (confirmed) { selectedFeatures.push(template.name); } } return selectedFeatures; } /** * Show confirmation and create the project */ async confirmAndCreate(setupData) { // Step 6: Confirmation const confirmed = await clack.confirm({ message: `Create project "${setupData.productSlug}" with selected customizations?`, initialValue: true }); if (clack.isCancel(confirmed) || !confirmed) { clack.cancel('Project creation cancelled.'); process.exit(0); } // Step 7: Project Creation with progress const s = clack.spinner(); s.start('Creating your project...'); try { // Generate project using template service const templateOptions = { productSlug: setupData.productSlug, productName: setupData.productName, company: "interop.io", copyright: "Copyright © 2024", version: "1.0.0", folderName: setupData.productSlug, targetDirectory: setupData.targetDirectory, features: setupData.features }; await this.templateService.generateProject('ioconnect-desktop', templateOptions); // Generate license.json if license key provided if (setupData.licenseKey) { await this.generateLicenseFile(setupData.targetDirectory, setupData.licenseKey); } s.stop('Project created successfully!'); // Show next steps clack.note(`cd ${setupData.productSlug}\n` + `npm install\n` + `npm run setup\n` + `npm run start`, 'Next steps'); } catch (error) { s.stop('Project creation failed'); throw new error_handler_1.CLIError('Failed to create project', { code: error_handler_1.ErrorCode.FILE_SYSTEM_ERROR, cause: error, suggestions: [ 'Check that the target directory does not already exist', 'Ensure you have write permissions in the current directory', 'Try running with --verbose for more details' ] }); } } /** * Generate NPM-compatible folder name from product name */ generateFolderName(productName) { return productName .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } /** * Generate license.json file with provided license JSON */ async generateLicenseFile(targetDirectory, licenseJson) { try { // Parse and re-format the license JSON to ensure proper formatting const licenseData = JSON.parse(licenseJson); const licensePath = (0, node_path_1.join)(targetDirectory, 'license.json'); (0, node_fs_1.writeFileSync)(licensePath, JSON.stringify(licenseData, null, 2), 'utf-8'); this.logger.debug(`License file created at: ${licensePath}`); } catch (error) { this.logger.warn('Failed to create license file:', error); // Don't fail the entire setup for license generation issues } } } exports.CreateService = CreateService; //# sourceMappingURL=create.service.js.map