Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | /** * Interactive configurator for the Spec-Up-T starter pack. * * The module collects starter metadata from the user, updates the first * spec entry in `specs.json`, and persists the changes. It can be required * programmatically or executed directly from the command line. */ const fs = require('node:fs'); const path = require('node:path'); const readline = require('readline'); const Logger = require('../../utils/logger'); const SPECS_KEY = 'specs'; const JSON_FILE_PATH = path.resolve(process.cwd(), 'specs.json'); const defaultQuestions = [ { field: 'title', prompt: 'Enter title', defaultValue: 'Spec-Up-T Starterpack' }, { field: 'description', prompt: 'Enter description', defaultValue: 'Create technical specifications in markdown. Based on the original Spec-Up, extended with Terminology tooling' }, { field: 'author', prompt: 'Enter author', defaultValue: 'Trust over IP Foundation' }, { field: 'logo', prompt: 'Enter logo URL', defaultValue: 'https://raw.githubusercontent.com/trustoverip/spec-up-t/refs/heads/master/src/install-from-boilerplate/boilerplate/static/logo.svg' }, { field: 'logo_link', prompt: 'Enter logo link URL', defaultValue: 'https://github.com/trustoverip/spec-up-t' }, { field: 'favicon', prompt: 'Enter favicon URL', defaultValue: 'https://raw.githubusercontent.com/trustoverip/spec-up-t/refs/heads/master/src/install-from-boilerplate/boilerplate/static/favicon.ico' }, { field: 'account', prompt: 'Enter account', defaultValue: 'trustoverip' }, { field: 'repo', prompt: 'Enter repo', defaultValue: 'spec-up-t-starter-pack' } ]; /** * Verifies that the specs.json file exists before prompting the user. */ function assertSpecsFileExists() { if (fs.existsSync(JSON_FILE_PATH)) { return; } Logger.error(`Error: ${JSON_FILE_PATH} does not exist.`); process.exit(1); } /** * Reads and parses the specs.json file, providing helpful feedback on failure. * * @returns {object} Parsed JSON contents. */ function loadSpecsConfig() { try { return JSON.parse(fs.readFileSync(JSON_FILE_PATH, 'utf8')); } catch (error) { Logger.error(`Error: Could not parse ${JSON_FILE_PATH}.`, error.message); process.exit(1); } } /** * Ensures the first spec entry exists and returns it for updates. Exits when the * structure is malformed because continuing would corrupt the file. * * @param {object} config - Parsed configuration object from specs.json. * @returns {{ config: object, primarySpec: object }} The config and its first spec entry. */ function resolvePrimarySpec(config) { const specs = config[SPECS_KEY]; if (!Array.isArray(specs) || specs.length === 0 || !specs[0]) { Logger.error(`Error: Invalid JSON structure. "${SPECS_KEY}[0]" is missing.`); process.exit(1); } return { config, primarySpec: specs[0] }; } /** * Builds the prompt text with the default value appended for clarity. * * @param {string} label - Question label displayed to the user. * @param {string} defaultValue - Value shown as default. * @returns {string} Prompt string formatted for readline. */ function formatPrompt(label, defaultValue) { return `${label} (${defaultValue}): `; } /** * Collects answers for all configuration questions from stdin/stdout. * * @param {Array<{ field: string, prompt: string, defaultValue: string }>} questions - Questions to ask. * @param {NodeJS.ReadableStream} input - Input stream for readline. * @param {NodeJS.WritableStream} output - Output stream for readline. * @returns {Promise<object>} Map of field names to user answers. */ function promptForAnswers(questions, input, output) { return new Promise((resolve, reject) => { const rl = readline.createInterface({ input, output }); const answers = {}; let index = 0; const askNext = () => { if (index >= questions.length) { rl.close(); resolve(answers); return; } const { field, prompt, defaultValue } = questions[index]; rl.question(formatPrompt(prompt, defaultValue), answer => { answers[field] = answer ? answer.trim() : defaultValue; index += 1; askNext(); }); }; rl.on('error', error => { rl.close(); reject(error); }); askNext(); }); } /** * Applies the collected answers to the spec configuration, keeping nested * `source` fields isolated from top-level metadata. * * @param {object} primarySpec - The spec entry to mutate. * @param {object} answers - User responses keyed by field. */ function applyAnswersToSpec(primarySpec, answers) { if (!primarySpec.source) { primarySpec.source = {}; } Object.entries(answers).forEach(([field, value]) => { if (['account', 'repo'].includes(field)) { primarySpec.source[field] = value; return; } primarySpec[field] = value; }); } /** * Persists the updated configuration back to specs.json. * * @param {object} config - Configuration object with modifications applied. */ function persistUpdatedConfig(config) { try { fs.writeFileSync(JSON_FILE_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8'); Logger.success(`Successfully updated ${JSON_FILE_PATH}.`); } catch (error) { Logger.error(`Error: Could not update ${JSON_FILE_PATH}.`, error.message); process.exit(1); } } /** * Generates the initial documentation output by running the render command. */ function generateInitialDocs() { const { execSync } = require('child_process'); try { Logger.info('\n🔨 Generating initial documentation...\n'); execSync('npm run render 4', { cwd: process.cwd(), stdio: 'inherit' }); Logger.success('Documentation generated in docs/ directory'); } catch (error) { Logger.error('Error: Could not generate documentation.', error.message); process.exit(1); } } /** * Runs the interactive configuration workflow. * * @param {object} [options] * @param {Array} [options.questions] - Override the default question set. * @param {NodeJS.ReadableStream} [options.input] - Custom input stream for testing. * @param {NodeJS.WritableStream} [options.output] - Custom output stream for testing. * @returns {Promise<void>} */ async function runStarterpackConfigurator({ questions = defaultQuestions, input = process.stdin, output = process.stdout } = {}) { assertSpecsFileExists(); Logger.info(`\nWelcome to the Spec-Up-T Starterpack configuration tool!\n` + '\nYou will be asked a series of questions to customize your project.\n' + "Here’s what each field means:\n" + '- "Title": The name of your project.\n' + '- "Description": A brief explanation of your project.\n' + '- "Author": The name of the person or organization creating the project.\n' + '- "Account": The GitHub account or organization where the repository will be hosted.\n' + '- "Repo": The name of the GitHub repository.\n\n' + 'Press Enter to accept the default value shown in parentheses.\n' ); try { const answers = await promptForAnswers(questions, input, output); const { config, primarySpec } = resolvePrimarySpec(loadSpecsConfig()); applyAnswersToSpec(primarySpec, answers); persistUpdatedConfig(config); generateInitialDocs(); } catch (error) { Logger.error('Configuration aborted due to an unexpected error.', error.message); process.exit(1); } } if (process.env.SPEC_UP_T_CONFIGURATOR_AUTORUN !== 'false') { runStarterpackConfigurator(); } module.exports = { runStarterpackConfigurator }; |