Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

A Node.js CLI (`@burgan-tech/vnext-workflow-cli`) that synchronizes local vNext workflow component definitions (JSON + embedded C# script `.csx` files) with a remote vNext API and its PostgreSQL database. There is no transpile step — the `bin/` entrypoint runs `src/` directly with CommonJS `require`.

The CLI is invoked as `workflow`, `wf`, or `vnext` (all three bins point at `bin/workflow.js`). The `vnext` alias is preferred on Windows where `wf` may clash.

## Commands

```bash
npm install # install deps
npm link # symlink the bins globally for local development
node bin/workflow.js # run the CLI directly (same as `npm run dev` / `npm start`)
```

- **`build` is a no-op** (`echo 'Build not needed for now'`) — nothing to compile.
- **There is no test suite, linter, or formatter configured.** Do not invent `npm test`/`npm run lint` commands; they will fail. Verify changes by running the CLI against a real vNext project directory.

The CLI always treats `process.cwd()` as the project root and requires a `vnext.config.json` in that directory. To exercise it, `cd` into a vNext workspace (not this repo) before running.

## Two distinct config systems (do not conflate)

1. **`vnext.config.json`** — lives in the *user's project root*, read fresh each run. Defines `domain` and `paths` (which folders hold which component types). Handled by [src/lib/vnextConfig.js](src/lib/vnextConfig.js) with a single-entry cache.
2. **CLI config** — global, stored in `~/.config/vnext-workflow-cli/config.json` via the `conf` package. Holds API/DB connection settings, structured as **domain profiles** (`ACTIVE_DOMAIN` + `DOMAINS[]`). Handled by [src/lib/config.js](src/lib/config.js).

`config.js` auto-migrates the old flat config format to the domain-aware format on module load (`migrateConfig`), and exposes a virtual `PROJECT_ROOT` key that always returns `process.cwd()` (it is never persisted). `DOMAIN_NAME`, `ACTIVE_DOMAIN`, and `PROJECT_ROOT` are reserved and cannot be set via `config.set` — they have dedicated domain commands.

**Auto domain resolution:** the `preAction` hook in [bin/workflow.js](bin/workflow.js) calls `config.resolveWorkspaceDomain(cwd)` before every command *except* `domain`. It reads the `domain` field from the project's `vnext.config.json` and, if a matching CLI domain profile exists, silently switches `ACTIVE_DOMAIN` to it. This is why running a command in a project folder uses that project's connection settings without a manual `wf domain use`.

## Architecture

`bin/workflow.js` wires up Commander, registers commands, and installs the `preAction` domain-resolution + banner hook. Each command in `src/commands/` orchestrates the shared libraries in `src/lib/`:

- **discover.js** — given `vnext.config.json` `paths`, locates component folders under `componentsRoot` and globs their JSON/CSX files. Crucially, it **only scans folders declared in `paths`** and always ignores `.meta/`, `*.diagram.json`, `package*.json`, and `*config*.json`.
- **csx.js** — embeds `.csx` content into the JSON files that reference it. A `.csx` file is matched to JSON by its `location` string (e.g. `./src/Rules/MyRule.csx`), and the JSON's per-reference `encoding` field decides the form: `NAT` writes plain text, `B64`/absent writes Base64 (default). It updates *every* matching `location` in the JSON tree recursively. **CSX→JSON matching is scoped to the CSX file's own component directory** (the parent of its `src/` folder) so that same-named `.csx` files in sibling components don't cross-contaminate.
- **workflow.js** — per-component publish logic: read JSON `key`/`version`/`flow`, map the component type to a `sys-*` flow name (`workflows`→`sys-flows`, `tasks`→`sys-tasks`, etc.), check the DB, delete if present, publish.
- **db.js** — PostgreSQL access with two interchangeable backends selected by `USE_DOCKER`: a direct `pg.Client` connection, or `docker exec ... psql` shelling into a container. Queries target `"<schema>"."Instances"` where schema = the flow name with `-`→`_`. Lookups are by `Key` only (version is ignored), newest `CreatedAt` first.
- **api.js** — axios calls: `GET /health`, `POST /api/v1/definitions/publish`, `GET /api/{version}/definitions/re-initialize`. `publishComponent` carefully unwraps RFC 7807 Problem Details (`detail`, `title`, `errors`, `errorCode`, `traceId`) into a structured `apiError` for rich error display.
- **ui.js** — all console output (chalk). `LOG` helpers, the active-domain banner, and two error renderers: `printApiError` (single component, tree-style) and `printErrorSummaryTable` (batch, table with expanded validation errors). Route user-facing output through this module rather than ad-hoc `console.log`.

## Command semantics

The four sync commands differ only in their DB/existing-component behavior — keep this table consistent when editing them:

| Command | Scans | If exists in DB | If new |
|---------|-------|-----------------|--------|
| `sync` | all components | **skip** | publish |
| `update` | git-changed files (or `--all` / `--file`) | delete + publish | publish |
| `reset` | interactively-chosen folder | delete + publish | publish |
| `csx` | CSX files only | n/a (no DB/API) | n/a |

`update` and `reset` re-initialize the system (`reinitializeSystem`) after a successful batch. `update`/`csx` default to git-changed files: `getGitChangedJson` / `getGitChangedCsx` run `git status --porcelain` from the **git root** (not project root), then filter results back down to `PROJECT_ROOT`.

## Conventions

- CommonJS only (`require`/`module.exports`), Node >= 14. No TypeScript, no ESM.
- Library functions take an explicit `projectRoot` argument rather than reading cwd directly; commands resolve it once via `config.get('PROJECT_ROOT')`.
- DB and API helpers swallow connection errors and return `false`/`null` rather than throwing — callers treat a missing instance as "not in DB".
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Cross-platform modern workflow management tool.
[![npm downloads](https://img.shields.io/npm/dm/@burgan-tech/vnext-workflow-cli.svg)](https://www.npmjs.com/package/@burgan-tech/vnext-workflow-cli)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A command-line interface (CLI) tool for managing vNext workflows, tasks, schemas, views, functions, and extensions. This tool helps you synchronize your local workflow definitions with the vNext API and database.
A command-line interface (CLI) tool for managing vNext workflows, tasks, schemas, views, functions, extensions, and mappings. This tool helps you synchronize your local workflow definitions with the vNext API and database.

**Package**: `@burgan-tech/vnext-workflow-cli`
**NPM**: https://www.npmjs.com/package/@burgan-tech/vnext-workflow-cli
Expand Down Expand Up @@ -71,7 +71,8 @@ Every vNext project must have a `vnext.config.json` file in the **project root**
"functions": "Functions",
"extensions": "Extensions",
"workflows": "Workflows",
"schemas": "Schemas"
"schemas": "Schemas",
"mappings": "Mappings"
}
}
```
Expand All @@ -88,6 +89,7 @@ Every vNext project must have a `vnext.config.json` file in the **project root**
| `paths.views` | Views folder name under componentsRoot |
| `paths.functions` | Functions folder name under componentsRoot |
| `paths.extensions` | Extensions folder name under componentsRoot |
| `paths.mappings` | Mappings folder name under componentsRoot |

### Component Discovery

Expand Down Expand Up @@ -226,6 +228,7 @@ wf reset # Select folder from interactive menu
extensions (Extensions/)
workflows (Workflows/)
schemas (Schemas/)
mappings (Mappings/)
──────────────
TUMU (All folders)
```
Expand Down
12 changes: 9 additions & 3 deletions src/lib/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
const axios = require('axios');
const pkg = require('../../package.json');

// Identifies requests as coming from the CLI (e.g. "vnext-workflow-cli/1.0.0")
const USER_AGENT = `vnext-workflow-cli/${pkg.version}`;

/**
* Tests the API connection
Expand All @@ -8,7 +12,8 @@ const axios = require('axios');
async function testApiConnection(baseUrl) {
try {
const response = await axios.get(`${baseUrl}/health`, {
timeout: 5000
timeout: 5000,
headers: { 'User-Agent': USER_AGENT }
});
return response.status === 200;
} catch (error) {
Expand All @@ -29,7 +34,8 @@ async function publishComponent(baseUrl, componentData) {
const response = await axios.post(url, componentData, {
headers: {
'accept': '*/*',
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'User-Agent': USER_AGENT
},
timeout: 30000
});
Expand Down Expand Up @@ -87,7 +93,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 });
await axios.get(url, { timeout: 10000, headers: { 'User-Agent': USER_AGENT } });
return true;
} catch (error) {
return false;
Expand Down
4 changes: 3 additions & 1 deletion src/lib/workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function detectComponentType(jsonPath, projectRoot) {
case 'views': return 'sys-views';
case 'functions': return 'sys-functions';
case 'extensions': return 'sys-extensions';
case 'mappings': return 'sys-mappings';
default: return `sys-${type.toLowerCase()}`;
}
}
Expand All @@ -62,7 +63,8 @@ function detectComponentType(jsonPath, projectRoot) {
if (pathLower.includes('/views/')) return 'sys-views';
if (pathLower.includes('/functions/')) return 'sys-functions';
if (pathLower.includes('/extensions/')) return 'sys-extensions';

if (pathLower.includes('/mappings/')) return 'sys-mappings';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

On Windows systems, file paths use backslashes (\\) instead of forward slashes (/). Since pathLower is derived directly from jsonPath (which contains platform-specific separators), checks like pathLower.includes('/mappings/') or the dynamic folderPattern check (e.g., /${folderName.toLowerCase()}/) will fail on Windows.

To make this fully cross-platform, you should normalize the path separators to forward slashes at the beginning of the detectComponentType function.

For example, at the start of detectComponentType (line 33):

const pathLower = jsonPath.toLowerCase().replace(/\\/g, '/');

If you normalize the path separators at the top of the function, the existing forward-slash checks will work seamlessly on all platforms, and you can keep this line as is. Otherwise, you can use a regex to support both separators here.

Suggested change
if (pathLower.includes('/mappings/')) return 'sys-mappings';
if (/[/\\]mappings[/\\]/.test(pathLower)) return 'sys-mappings';


return 'sys-flows'; // default
}

Expand Down
Loading