@interopio/desktop-cli
Version:
CLI tool for setting up, building and packaging io.Connect Desktop projects
288 lines • 12.1 kB
JavaScript
;
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