From dc5211f0b99320e7a13982094ae3f9b58dc5c459 Mon Sep 17 00:00:00 2001 From: ikarakayali Date: Tue, 14 Jul 2026 09:28:38 +0300 Subject: [PATCH] axios adjustment and folder option for wf update command --- README.md | 28 +++++++++++++-- bin/workflow.js | 11 ++++++ src/commands/update.js | 74 +++++++++++++++++++++++++++++++++++--- src/lib/api.js | 21 ++++++----- src/lib/discover.js | 82 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d3ce795..2e18444 100644 --- a/README.md +++ b/README.md @@ -194,9 +194,21 @@ wf sync **Use when**: You modified existing components and want to update them ```bash -wf update # Process changed files in Git (CSX + JSON) -wf update --all # Update all (asks for confirmation) -wf update --file x.json # Process a single file +wf update # Process changed files in Git (CSX + JSON) +wf update --all # Update all (asks for confirmation) +wf update --file x.json # Process a single file +wf update --folder person # Process every component under a feature folder, ignoring Git +``` + +**`--folder ` (`-d`)**: Updates every component belonging to a feature, across all component types, regardless of Git status. It resolves `` in two ways: +- **Feature name** (e.g. `person`): matches `` under every component-type root (`Workflows/person`, `Tasks/person`, `Views/person`, `Schemas/person`, …) and updates all of them together. +- **Exact path** (e.g. `Workflows/person` or an absolute path): updates only that specific folder. + +If nothing matches, the command lists the feature folder names it did find so you can correct a typo. `--file` takes precedence over `--folder`, which takes precedence over `--all`. + +```bash +wf update --folder person # Every "person" folder across all component types +wf update -d Workflows/person # Only Workflows/person ``` --- @@ -424,6 +436,15 @@ wf reset wf csx ``` +### 5b. Update a Whole Feature Folder (No Git Needed) +```bash +# Update every "person" component across Workflows/, Tasks/, Views/, Schemas/, ... +wf update --folder person + +# Or target one exact folder +wf update --folder Workflows/person +``` + ### 6. Multidomain Workflow ```bash # Add domains (one-time setup) @@ -454,6 +475,7 @@ wf domain list |---------|----------|-----------------|------------|----------| | `sync` | Yes | Skip | Publish | Add missing components | | `update` | Yes | Delete + Publish | Publish | Update changed components | +| `update --folder ` | Yes | Delete + Publish | Publish | Update every component in a feature folder (ignores Git) | | `reset` | Yes | Delete + Publish | Publish | Force reset components | | `csx` | No | N/A | N/A | Only update CSX in JSONs | diff --git a/bin/workflow.js b/bin/workflow.js index 742bacc..6dd4312 100755 --- a/bin/workflow.js +++ b/bin/workflow.js @@ -54,6 +54,17 @@ program .description('Update workflows') .option('-a, --all', 'Update all workflows') .option('-f, --file ', 'Update a specific workflow') + .option('-d, --folder ', 'Update all components under a feature folder (across all component types), ignoring git') + .addHelpText('after', ` +Examples: + wf update Update git-changed components (default) + wf update --all Update all components + wf update --file Views/x.json Update a single component file + wf update --folder person Update every component under the "person" feature (Tasks/person, Workflows/person, Views/person, ...) + wf update -d Workflows/person Update only the components in that exact folder + +Note: --file takes precedence over --folder, which takes precedence over --all. +`) .action(updateCommand); // Sync command diff --git a/src/commands/update.js b/src/commands/update.js index 51b940c..74f209d 100644 --- a/src/commands/update.js +++ b/src/commands/update.js @@ -2,8 +2,9 @@ const chalk = require('chalk'); const ora = require('ora'); const path = require('path'); const inquirer = require('inquirer'); +const { glob } = require('glob'); const config = require('../lib/config'); -const { discoverComponents, findAllJsonFiles } = require('../lib/discover'); +const { discoverComponents, findAllJsonFiles, resolveFeatureFolders, listFeatureFolders, toGlobPattern } = require('../lib/discover'); const { getDomain, getComponentTypes } = require('../lib/vnextConfig'); const { publishComponent, reinitializeSystem } = require('../lib/api'); const { getInstanceId, deleteWorkflow } = require('../lib/db'); @@ -44,12 +45,57 @@ async function updateCommand(options) { version: config.get('API_VERSION'), domain: domain }; - + + // Folder mode: resolve the feature folder name to a set of directories. + // --file wins over --folder if both are given (most specific). + const ignorePatterns = [ + '**/.meta/**', + '**/.meta', + '**/*.diagram.json', + '**/package*.json', + '**/*config*.json' + ]; + let folderDirs = []; + const useFolder = !!options.folder && !options.file; + + if (useFolder) { + folderDirs = await resolveFeatureFolders(projectRoot, options.folder); + + if (folderDirs.length === 0) { + LOG.error(`No folder matched "${options.folder}"`); + const available = await listFeatureFolders(projectRoot); + if (available.length > 0) { + console.log(chalk.dim(`\n Available folders: ${available.join(', ')}\n`)); + } + return; + } + + console.log(chalk.blue(`\n Folder: ${options.folder} → ${folderDirs.length} folder(s)\n`)); + } + // FIRST: Update changed CSX files let csxFiles = []; const csxResults = { success: 0, failed: 0, errors: [] }; - - if (options.all) { + + if (useFolder) { + // Find CSX files within the matched feature folders + const csxSpinner = ora('Finding CSX files in folder...').start(); + try { + for (const dir of folderDirs) { + const found = await glob(toGlobPattern(dir, '**/*.csx'), { + ignore: ['**/.meta/**', '**/.meta', '**/node_modules/**', '**/dist/**'] + }); + csxFiles.push(...found); + } + if (csxFiles.length > 0) { + csxSpinner.succeed(chalk.green(`${csxFiles.length} CSX files found`)); + } else { + csxSpinner.info(chalk.dim('No CSX files in folder')); + } + } catch (error) { + csxSpinner.warn(chalk.yellow(`CSX scan error: ${error.message}`)); + } + } else if (options.all) { // Find all CSX files const csxSpinner = ora('Finding all CSX files...').start(); try { @@ -108,6 +154,26 @@ async function updateCommand(options) { : path.join(projectRoot, options.file); jsonFiles = [{ path: filePath, type: detectComponentType(filePath, projectRoot), fileName: path.basename(filePath) }]; console.log(chalk.blue(`\n File: ${path.basename(filePath)}\n`)); + } else if (useFolder) { + // All JSON files within the matched feature folders (git-independent) + const spinner = ora('Finding JSON files in folder...').start(); + + for (const dir of folderDirs) { + const files = await glob(toGlobPattern(dir, '**/*.json'), { ignore: ignorePatterns }); + jsonFiles.push(...files.map(f => ({ + path: f, + type: detectComponentType(f, projectRoot), + fileName: path.basename(f) + }))); + } + + if (jsonFiles.length === 0) { + spinner.info(chalk.yellow('No JSON files in folder')); + console.log(); + return; + } + + spinner.succeed(chalk.green(`${jsonFiles.length} JSON files found`)); } else if (options.all) { // All JSON files LOG.warning('ALL components will be updated!'); diff --git a/src/lib/api.js b/src/lib/api.js index 440dae4..6692acc 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,8 +1,14 @@ const axios = require('axios'); -const pkg = require('../../package.json'); +const https = require('node:https'); +const http = require('node:http'); -// Identifies requests as coming from the CLI (e.g. "vnext-workflow-cli/1.0.0") -const USER_AGENT = `vnext-workflow-cli/${pkg.version}`; +// Create axios instance with custom agents for both HTTP and HTTPS +const apiClient = axios.create({ + httpAgent: new http.Agent({ keepAlive: true }), + httpsAgent: new https.Agent({ + rejectUnauthorized: false // Allow self-signed certificates + }) +}); /** * Tests the API connection @@ -11,7 +17,7 @@ const USER_AGENT = `vnext-workflow-cli/${pkg.version}`; */ async function testApiConnection(baseUrl) { try { - const response = await axios.get(`${baseUrl}/health`, { + const response = await apiClient.get(`${baseUrl}/health`, { timeout: 5000, headers: { 'User-Agent': USER_AGENT } }); @@ -31,11 +37,10 @@ async function publishComponent(baseUrl, componentData) { const url = `${baseUrl}/api/v1/definitions/publish`; try { - const response = await axios.post(url, componentData, { + const response = await apiClient.post(url, componentData, { headers: { 'accept': '*/*', - 'Content-Type': 'application/json', - 'User-Agent': USER_AGENT + 'Content-Type': 'application/json' }, timeout: 30000 }); @@ -93,7 +98,7 @@ async function publishComponent(baseUrl, componentData) { async function reinitializeSystem(baseUrl, version) { const url = `${baseUrl}/api/${version}/definitions/re-initialize`; try { - await axios.get(url, { timeout: 10000, headers: { 'User-Agent': USER_AGENT } }); + await apiClient.get(url, { timeout: 10000 }); return true; } catch (error) { return false; diff --git a/src/lib/discover.js b/src/lib/discover.js index 1bdb43c..e671e5c 100644 --- a/src/lib/discover.js +++ b/src/lib/discover.js @@ -150,6 +150,86 @@ function listDiscovered(discovered, componentTypes) { return results; } +/** + * Resolves a folder name to a list of directories to update. + * + * Two resolution modes (in order): + * a) Exact path: if `name` resolves to an existing directory (absolute, or + * relative to projectRoot, or relative to componentsRoot), that single + * directory is returned. + * b) Feature name: otherwise, `name` is treated as a feature folder name and + * matched against every discovered component-type root. Every + * `/` that exists as a directory is collected, so a + * feature spread across Workflows/, Views/, Schemas/, … is gathered. + * + * @param {string} projectRoot - Project root folder + * @param {string} name - Folder name or relative/absolute path + * @returns {Promise} Absolute directory paths (empty if nothing matched) + */ +async function resolveFeatureFolders(projectRoot, name) { + const isDir = (p) => fs.existsSync(p) && fs.statSync(p).isDirectory(); + + // a) Exact-path resolution + const candidates = []; + if (path.isAbsolute(name)) { + candidates.push(name); + } else { + candidates.push(path.join(projectRoot, name)); + try { + candidates.push(path.join(getComponentsRoot(projectRoot), name)); + } catch (error) { + // componentsRoot may be unavailable; ignore and fall through + } + } + + for (const candidate of candidates) { + if (isDir(candidate)) { + return [path.resolve(candidate)]; + } + } + + // b) Feature-name match across discovered component roots + const discovered = await discoverComponents(projectRoot); + const dirs = []; + for (const componentDir of Object.values(discovered)) { + const featureDir = path.join(componentDir, name); + if (isDir(featureDir)) { + dirs.push(path.resolve(featureDir)); + } + } + + return dirs; +} + +/** + * Lists available feature folder names — the union of immediate subdirectory + * names across all discovered component-type roots. Used for error messages + * when a requested folder name does not match anything. + * + * @param {string} projectRoot - Project root folder + * @returns {Promise} Sorted unique feature folder names + */ +async function listFeatureFolders(projectRoot) { + const discovered = await discoverComponents(projectRoot); + const names = new Set(); + + for (const componentDir of Object.values(discovered)) { + let entries = []; + try { + entries = fs.readdirSync(componentDir, { withFileTypes: true }); + } catch (error) { + continue; + } + for (const entry of entries) { + if (entry.isDirectory() && !entry.name.startsWith('.')) { + names.add(entry.name); + } + } + } + + return Array.from(names).sort(); +} + /** * Detects component type from file path * @param {string} filePath - File path @@ -177,5 +257,7 @@ module.exports = { findAllCsxInComponents, getComponentDir, listDiscovered, + resolveFeatureFolders, + listFeatureFolders, detectComponentTypeFromPath };