newo 3.6.2 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -3
- package/README.md +61 -0
- package/dist/cli/commands/check.d.ts +3 -0
- package/dist/cli/commands/check.js +15 -0
- package/dist/cli/commands/format.d.ts +3 -0
- package/dist/cli/commands/format.js +105 -0
- package/dist/cli/commands/help.js +13 -0
- package/dist/cli/commands/lint.d.ts +3 -0
- package/dist/cli/commands/lint.js +195 -0
- package/dist/cli-new/di/tokens.d.ts +1 -1
- package/dist/cli.js +45 -9
- package/dist/domain/strategies/sync/AttributeSyncStrategy.js +38 -8
- package/dist/lint/config.d.ts +4 -0
- package/dist/lint/config.js +14 -0
- package/dist/lint/discovery.d.ts +34 -0
- package/dist/lint/discovery.js +112 -0
- package/dist/lint/live-schema.d.ts +20 -0
- package/dist/lint/live-schema.js +52 -0
- package/dist/lint/reporters/index.d.ts +4 -0
- package/dist/lint/reporters/index.js +19 -0
- package/dist/lint/reporters/json.d.ts +3 -0
- package/dist/lint/reporters/json.js +6 -0
- package/dist/lint/reporters/sarif.d.ts +3 -0
- package/dist/lint/reporters/sarif.js +47 -0
- package/dist/lint/reporters/text.d.ts +3 -0
- package/dist/lint/reporters/text.js +51 -0
- package/dist/lint/reporters/types.d.ts +6 -0
- package/dist/lint/reporters/types.js +2 -0
- package/dist/sync/attributes.js +38 -12
- package/dist/sync/conversations.d.ts +1 -1
- package/dist/sync/conversations.js +240 -193
- package/dist/sync/json-attr-utils.d.ts +67 -0
- package/dist/sync/json-attr-utils.js +98 -0
- package/package.json +3 -1
- package/src/cli/commands/check.ts +21 -0
- package/src/cli/commands/format.ts +131 -0
- package/src/cli/commands/help.ts +13 -0
- package/src/cli/commands/lint.ts +246 -0
- package/src/cli.ts +50 -9
- package/src/domain/strategies/sync/AttributeSyncStrategy.ts +45 -8
- package/src/lint/config.ts +17 -0
- package/src/lint/discovery.ts +148 -0
- package/src/lint/live-schema.ts +62 -0
- package/src/lint/reporters/index.ts +22 -0
- package/src/lint/reporters/json.ts +12 -0
- package/src/lint/reporters/sarif.ts +59 -0
- package/src/lint/reporters/text.ts +58 -0
- package/src/lint/reporters/types.ts +7 -0
- package/src/sync/attributes.ts +43 -14
- package/src/sync/conversations.ts +265 -212
- package/src/sync/json-attr-utils.ts +95 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.7.1] - 2026-04-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Workflow Builder canvas blank-screen after `newo push --only attributes`** - JSON-typed project/customer attributes (e.g. `project_attributes_private_dynamic_workflow_builder_canvas`) are now always coerced to a JSON STRING when persisted to `attributes.yaml` and when sent on push. Previously, when the API returned the `value` field as a parsed object, `yaml.dump` serialized it as a YAML structure and the next push sent `{"value": {...object...}}` instead of `{"value": "...json..."}` - the platform stored a shape Builder could not render and the canvas blanked out. Change-detection now compares both sides as canonical (compact) JSON, so pretty- vs compact-printed forms and string vs object representations no longer trigger spurious pushes. String-typed values are left bit-for-bit untouched, so no churn on existing repos. New helpers in `src/sync/json-attr-utils.ts` are wired into both `src/sync/attributes.ts` and `src/domain/strategies/sync/AttributeSyncStrategy.ts`. Reported by Bob; 19 regression tests in `test/json-attribute-roundtrip.test.js`.
|
|
15
|
+
|
|
16
|
+
## [3.7.0] - 2026-04-23
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`newo lint` command** - Static analysis for Guidance / Jinja / NSL / NSLG files. Exit codes 0/1/2. Flags: `--format`, `--reporter text|json|sarif`, `--max-warnings`, `--quiet`, `--rule`, `--rule-off`, `--changed`, `--live`, `--customer`. Powered by `newo-dsl-analyzer`, the same engine used by the VS Code extension.
|
|
21
|
+
- **`newo format` command** - Apply canonical formatting in place. `--check` flag reports without writing (CI gate). v1 is an identity transform that ensures final newline; concrete formatting rules iterate in later releases.
|
|
22
|
+
- **`newo check` command** - Umbrella command: runs `lint` then `format --check` for a single CI gate invocation.
|
|
23
|
+
- **`src/lint/` glue module** - File discovery respecting format-aware extensions and `.newo/{customer}/hashes.json` for `--changed`, `.neworc.yaml` config loader, live schema refresh (caches `/api/v1/script/actions` response to `.newo/{customer}/actions.json`), text/JSON/SARIF 2.1.0 reporters.
|
|
24
|
+
- **Dependency on `newo-dsl-analyzer` and `newo-dsl-core`** - the shared Newo DSL analysis engine published from the newo-nsl-lsp repo. See README for the plugin contract.
|
|
25
|
+
- **Offline mode for lint/format/check** - These commands do not require `NEWO_API_KEY` or any environment configuration when run with explicit paths and without `--customer` or `--live`. Safe for pre-commit hooks and sandboxed CI jobs.
|
|
26
|
+
- **`.neworc.yaml` config discovery** - Walks up the directory tree from cwd to find lint config. Supports `rules`, `plugins`, `extensions`, `ignore`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `src/cli.ts` short-circuits environment validation for offline `newo lint` / `newo format` / `newo check` invocations. All other commands still require credentials at startup.
|
|
31
|
+
|
|
32
|
+
### Publisher notes
|
|
33
|
+
|
|
34
|
+
Before running `npm publish` on this version, swap the local `file:` dependencies in `package.json` for semver ranges (npm rejects published packages that reference `file:` paths). The `newo-dsl-*` packages must be published from the [newo-nsl-lsp](https://github.com/newo-ai/newo-nsl-lsp) monorepo first:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"newo-dsl-analyzer": "^1.0.0",
|
|
39
|
+
"newo-dsl-core": "^1.0.0",
|
|
40
|
+
...
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Recommended release order:
|
|
45
|
+
1. Tag `v1.0.0` on newo-nsl-lsp -> CI publishes `newo-dsl-core`, `newo-dsl-data`, `newo-dsl-analyzer`, `newo-dsl-lsp` to npm.
|
|
46
|
+
2. In newo-cli: swap `file:` to `^1.0.0`, `npm install`, build, test.
|
|
47
|
+
3. Tag `v3.7.0` on newo-cli -> `npm publish --access public`.
|
|
48
|
+
|
|
10
49
|
## [3.6.2] - 2026-04-23
|
|
11
50
|
|
|
12
51
|
### Fixed
|
|
13
52
|
|
|
14
|
-
- **`attributes.yaml` YAML escaping
|
|
53
|
+
- **`attributes.yaml` YAML escaping** - Values containing double quotes (e.g. `["+37410333310"]`) are now serialized as single-quoted scalars instead of invalid double-quoted output. Previously `newo push --format newo_v2` could not parse round-tripped files because the generated YAML was malformed. Fix ported via PR #1 (commit `d3a1d4e`).
|
|
15
54
|
|
|
16
55
|
## [3.6.1] - 2026-04-14
|
|
17
56
|
|
|
@@ -1000,12 +1039,14 @@ Another Item: $Price [Modifiers: modifier3]
|
|
|
1000
1039
|
- GitHub Actions CI/CD integration
|
|
1001
1040
|
- Robust authentication with token refresh
|
|
1002
1041
|
|
|
1003
|
-
[Unreleased]: https://github.com/sabbah13/newo-cli/compare/v3.
|
|
1004
|
-
[3.
|
|
1042
|
+
[Unreleased]: https://github.com/sabbah13/newo-cli/compare/v3.7.1...HEAD
|
|
1043
|
+
[3.7.1]: https://github.com/sabbah13/newo-cli/compare/v3.7.0...v3.7.1
|
|
1005
1044
|
[3.3.0]: https://github.com/sabbah13/newo-cli/compare/v3.2.0...v3.3.0
|
|
1006
1045
|
[3.2.0]: https://github.com/sabbah13/newo-cli/compare/v3.1.0...v3.2.0
|
|
1007
1046
|
[3.1.0]: https://github.com/sabbah13/newo-cli/compare/v3.0.0...v3.1.0
|
|
1008
1047
|
[3.0.0]: https://github.com/sabbah13/newo-cli/compare/v2.0.6...v3.0.0
|
|
1048
|
+
[3.7.0]: https://github.com/sabbah13/newo-cli/compare/v3.6.2...v3.7.0
|
|
1049
|
+
[3.6.2]: https://github.com/sabbah13/newo-cli/compare/v3.6.1...v3.6.2
|
|
1009
1050
|
[2.0.6]: https://github.com/sabbah13/newo-cli/compare/v2.0.5...v2.0.6
|
|
1010
1051
|
[2.0.5]: https://github.com/sabbah13/newo-cli/compare/v2.0.4...v2.0.5
|
|
1011
1052
|
[2.0.4]: https://github.com/sabbah13/newo-cli/compare/v2.0.3...v2.0.4
|
package/README.md
CHANGED
|
@@ -153,6 +153,67 @@ NEWO_REFRESH_URL=custom_refresh_endpoint # Custom refresh endpoint
|
|
|
153
153
|
| `newo import-akb` | Import knowledge base articles | • Structured text parsing<br>• Bulk article import<br>• Validation and error reporting |
|
|
154
154
|
| `newo meta` | Get project metadata (debug) | • Project structure analysis<br>• Metadata validation |
|
|
155
155
|
|
|
156
|
+
### Lint, Format, Check (NEW v3.7.0)
|
|
157
|
+
|
|
158
|
+
Static-analysis over DSL files, powered by [`newo-dsl-analyzer`](https://www.npmjs.com/package/newo-dsl-analyzer). Same engine that runs in the VS Code extension - no drift between editor and CI.
|
|
159
|
+
|
|
160
|
+
| Command | Description |
|
|
161
|
+
|---------|-------------|
|
|
162
|
+
| `newo lint [paths...]` | Lint Guidance / Jinja / NSL / NSLG files. Exit 0 clean, 1 findings, 2 runtime error. |
|
|
163
|
+
| `newo format [paths...]` | Apply canonical formatting in place. Use `--check` to only report (no writes). |
|
|
164
|
+
| `newo check [paths...]` | Umbrella: runs `lint` then `format --check`. Single CI gate. |
|
|
165
|
+
|
|
166
|
+
**Lint flags:**
|
|
167
|
+
|
|
168
|
+
| Flag | Purpose |
|
|
169
|
+
|------|---------|
|
|
170
|
+
| `--format <cli_v1\|newo_v2>` | Restrict file extensions to one format's set |
|
|
171
|
+
| `--reporter <text\|json\|sarif>` | Output format. `sarif` is ready for GitHub Code Scanning |
|
|
172
|
+
| `--max-warnings <n>` | Exit 1 if warning count exceeds threshold |
|
|
173
|
+
| `--quiet` | Errors only (filters warnings from output) |
|
|
174
|
+
| `--rule <code>` / `--rule-off <code>` | Enable / disable specific rules (comma-separated or repeatable) |
|
|
175
|
+
| `--changed` | Lint only files modified since last `newo push` (uses `.newo/{customer}/hashes.json`) |
|
|
176
|
+
| `--live` | Refresh schemas from NEWO API (`/api/v1/script/actions`) and cache to `.newo/{customer}/actions.json` |
|
|
177
|
+
| `--customer <idn>` | Scope to one configured customer |
|
|
178
|
+
|
|
179
|
+
**Config file:** `.neworc.yaml` at your repo root (or any ancestor directory):
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
rules:
|
|
183
|
+
E100: error # Unknown skill
|
|
184
|
+
W101: off # Unknown function (useful when using bundled schemas without --live)
|
|
185
|
+
W102: warning # Unknown parameter
|
|
186
|
+
ignore:
|
|
187
|
+
- archived/
|
|
188
|
+
plugins:
|
|
189
|
+
- ./my-lint-plugin
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Examples:**
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Quick lint of the current directory
|
|
196
|
+
newo lint newo_customers
|
|
197
|
+
|
|
198
|
+
# Lint only changed files, as SARIF, suitable for GitHub PR comments
|
|
199
|
+
newo lint --changed --reporter sarif > lint.sarif
|
|
200
|
+
|
|
201
|
+
# Lint one customer with platform-fresh schemas
|
|
202
|
+
newo lint --customer acme --live
|
|
203
|
+
|
|
204
|
+
# CI gate
|
|
205
|
+
newo check --max-warnings 0
|
|
206
|
+
|
|
207
|
+
# Pre-commit: auto-format then lint
|
|
208
|
+
newo format && newo lint
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Exit codes:** 0 clean / 1 findings (errors present OR `--max-warnings` exceeded) / 2 runtime error.
|
|
212
|
+
|
|
213
|
+
**Offline mode:** `newo lint`, `newo format`, and `newo check` do not require `NEWO_API_KEY` or any environment configuration when run with explicit paths and without `--customer` / `--live`. Safe to use in sandboxes and pre-commit hooks.
|
|
214
|
+
|
|
215
|
+
**Plugin authors:** depend on [`newo-dsl-core`](https://www.npmjs.com/package/newo-dsl-core) and register your plugin in `.neworc.yaml`. See the analyzer README for the full rule API.
|
|
216
|
+
|
|
156
217
|
### V2 Format Support (NEW v3.6.0)
|
|
157
218
|
|
|
158
219
|
The CLI supports two formats that coexist in the same workspace:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `newo check` - umbrella command equivalent to running lint + format --check.
|
|
3
|
+
*
|
|
4
|
+
* A failing check exits non-zero if any of the sub-checks fail, so CI
|
|
5
|
+
* pipelines can gate merges on a single invocation.
|
|
6
|
+
*/
|
|
7
|
+
import { handleLintCommand } from './lint.js';
|
|
8
|
+
import { handleFormatCommand } from './format.js';
|
|
9
|
+
export async function handleCheckCommand(customerConfig, args, verbose) {
|
|
10
|
+
const lintArgs = { ...args };
|
|
11
|
+
await handleLintCommand(customerConfig, lintArgs, verbose);
|
|
12
|
+
const formatCheckArgs = { ...args, check: true };
|
|
13
|
+
await handleFormatCommand(customerConfig, formatCheckArgs, verbose);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `newo format` - apply canonical formatting to DSL files.
|
|
3
|
+
*
|
|
4
|
+
* Invokes newo-dsl-analyzer's Formatter. In v1 the formatter is an
|
|
5
|
+
* identity transform (just ensures a final newline). Concrete rules
|
|
6
|
+
* land in subsequent versions; the command surface is stable now so
|
|
7
|
+
* CI pipelines and pre-commit hooks can wire `newo format --check`
|
|
8
|
+
* immediately.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { createFormatter } from 'newo-dsl-analyzer';
|
|
13
|
+
import { selectSingleCustomer } from '../customer-selection.js';
|
|
14
|
+
import { handleCliError } from '../errors.js';
|
|
15
|
+
import { resolveFormat } from '../../format/detect.js';
|
|
16
|
+
import { discoverCustomerFiles, discoverFromPath, defaultRoot } from '../../lint/discovery.js';
|
|
17
|
+
export async function handleFormatCommand(customerConfig, args, verbose) {
|
|
18
|
+
try {
|
|
19
|
+
const fmtArgs = parseArgs(args);
|
|
20
|
+
const formatter = createFormatter();
|
|
21
|
+
const hasCustomerContext = fmtArgs.customer !== undefined ||
|
|
22
|
+
Object.keys(customerConfig.customers ?? {}).length > 0;
|
|
23
|
+
const { selectedCustomer, allCustomers, isMultiCustomer } = hasCustomerContext
|
|
24
|
+
? selectSingleCustomer(customerConfig, fmtArgs.customer)
|
|
25
|
+
: { selectedCustomer: null, allCustomers: [], isMultiCustomer: false };
|
|
26
|
+
const targetCustomer = selectedCustomer ?? (isMultiCustomer ? null : allCustomers[0] ?? null);
|
|
27
|
+
void targetCustomer;
|
|
28
|
+
const files = await resolveFiles(targetCustomer, allCustomers, fmtArgs, isMultiCustomer);
|
|
29
|
+
if (files.length === 0) {
|
|
30
|
+
console.log('No files matched.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
let touched = 0;
|
|
34
|
+
let needsFormat = 0;
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const source = await fs.readFile(file.absPath, 'utf8');
|
|
37
|
+
const result = formatter.format(source, file.absPath);
|
|
38
|
+
if (!result.changed)
|
|
39
|
+
continue;
|
|
40
|
+
needsFormat++;
|
|
41
|
+
if (fmtArgs.check) {
|
|
42
|
+
console.log(`would rewrite ${path.relative(process.cwd(), file.absPath)}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await fs.writeFile(file.absPath, result.formatted, 'utf8');
|
|
46
|
+
touched++;
|
|
47
|
+
if (verbose)
|
|
48
|
+
console.log(`formatted ${path.relative(process.cwd(), file.absPath)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (fmtArgs.check) {
|
|
52
|
+
if (needsFormat === 0) {
|
|
53
|
+
console.log('All files are properly formatted.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log(`${needsFormat} file(s) would be reformatted.`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(`Formatted ${touched} file(s).`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
handleCliError(err, 'format');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parseArgs(args) {
|
|
68
|
+
const positional = args._.slice(1).filter((p) => typeof p === 'string');
|
|
69
|
+
return {
|
|
70
|
+
positional,
|
|
71
|
+
formatVersion: args.format,
|
|
72
|
+
check: Boolean(args.check),
|
|
73
|
+
customer: args.customer,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function resolveFiles(selected, all, args, isMultiCustomer) {
|
|
77
|
+
if (args.positional.length > 0) {
|
|
78
|
+
const files = [];
|
|
79
|
+
for (const p of args.positional) {
|
|
80
|
+
files.push(...(await discoverFromPath(p, {
|
|
81
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
82
|
+
})));
|
|
83
|
+
}
|
|
84
|
+
return files;
|
|
85
|
+
}
|
|
86
|
+
if (selected) {
|
|
87
|
+
const formatVersion = resolveFormat(selected.idn, args.formatVersion).version;
|
|
88
|
+
return discoverCustomerFiles(selected, { format: formatVersion });
|
|
89
|
+
}
|
|
90
|
+
if (isMultiCustomer) {
|
|
91
|
+
const files = [];
|
|
92
|
+
for (const customer of all) {
|
|
93
|
+
const formatVersion = resolveFormat(customer.idn, args.formatVersion).version;
|
|
94
|
+
files.push(...(await discoverCustomerFiles(customer, { format: formatVersion })));
|
|
95
|
+
}
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
return discoverFromPath(defaultRoot(), {
|
|
99
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function toFormatVersion(v) {
|
|
103
|
+
return v === 'newo_v2' ? 'newo_v2' : 'cli_v1';
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -21,6 +21,19 @@ Core Commands:
|
|
|
21
21
|
newo meta [--customer <idn>] # get project metadata (debug)
|
|
22
22
|
newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles
|
|
23
23
|
|
|
24
|
+
Linting & Formatting (NEW):
|
|
25
|
+
newo lint [paths...] [--format <fmt>] [--reporter <text|json|sarif>] # static-analysis on DSL files
|
|
26
|
+
newo lint --changed # lint only files modified since last push
|
|
27
|
+
newo lint --live # refresh action catalog from NEWO API
|
|
28
|
+
newo lint --rule <code> --no-rule <code> # selectively enable/disable rules
|
|
29
|
+
newo lint --max-warnings <n> # fail when warnings exceed threshold
|
|
30
|
+
newo format [paths...] [--check] # apply canonical formatting (in-place or --check)
|
|
31
|
+
newo check [paths...] # umbrella: lint + format --check (CI gate)
|
|
32
|
+
|
|
33
|
+
Powered by newo-dsl-analyzer. Exit codes: 0 clean, 1 lint errors, 2 runtime error.
|
|
34
|
+
Discover rules via the Diagnostic code column (E100, W101, ...); configure
|
|
35
|
+
them in .neworc.yaml at your repo root. Plugin authors: depend on newo-dsl-core.
|
|
36
|
+
|
|
24
37
|
Project Management:
|
|
25
38
|
newo create-project <idn> [--title <title>] [--description <desc>] [--version <version>] [--auto-update] # create empty project on platform
|
|
26
39
|
newo list-registries [--customer <idn>] # list available project registries (production, staging, etc.)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `newo lint` - static analysis over Guidance / Jinja / NSL / NSLG files.
|
|
3
|
+
*
|
|
4
|
+
* Wraps newo-dsl-analyzer with newo-cli's customer/format/hash primitives.
|
|
5
|
+
* Exit codes:
|
|
6
|
+
* 0 clean (or only warnings below --max-warnings)
|
|
7
|
+
* 1 lint errors found, or warning threshold exceeded
|
|
8
|
+
* 2 unexpected runtime failure (handled by handleCliError)
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { createLinter, } from 'newo-dsl-analyzer';
|
|
13
|
+
import { selectSingleCustomer } from '../customer-selection.js';
|
|
14
|
+
import { handleCliError } from '../errors.js';
|
|
15
|
+
import { resolveFormat } from '../../format/detect.js';
|
|
16
|
+
import { discoverCustomerFiles, discoverFromPath, defaultRoot } from '../../lint/discovery.js';
|
|
17
|
+
import { loadNewoLintConfig } from '../../lint/config.js';
|
|
18
|
+
import { refreshLiveSchema, loadCachedLiveSchema } from '../../lint/live-schema.js';
|
|
19
|
+
import { pickReporter } from '../../lint/reporters/index.js';
|
|
20
|
+
export async function handleLintCommand(customerConfig, args, verbose) {
|
|
21
|
+
try {
|
|
22
|
+
const lintArgs = parseArgs(args);
|
|
23
|
+
const report = await run(customerConfig, lintArgs, verbose);
|
|
24
|
+
const reporter = pickReporter(lintArgs.reporter);
|
|
25
|
+
const output = reporter.write(report);
|
|
26
|
+
if (output.trim().length > 0)
|
|
27
|
+
process.stdout.write(output + '\n');
|
|
28
|
+
const exitCode = determineExitCode(report, lintArgs);
|
|
29
|
+
if (exitCode !== 0)
|
|
30
|
+
process.exit(exitCode);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
handleCliError(err, 'lint');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function parseArgs(args) {
|
|
37
|
+
const positional = args._.slice(1).filter((p) => typeof p === 'string');
|
|
38
|
+
// Multiple flag shapes: --rule=E100, --rule E100,W100
|
|
39
|
+
// Disabling rules uses --rule-off (not --no-rule) because minimist treats
|
|
40
|
+
// `--no-X` as `X: false` and swallows the next positional argument.
|
|
41
|
+
const rules = collectCsv(args.rule);
|
|
42
|
+
const noRules = collectCsv(args['rule-off']);
|
|
43
|
+
return {
|
|
44
|
+
positional,
|
|
45
|
+
formatVersion: args.format,
|
|
46
|
+
reporter: args.reporter ?? args['output-format'] ?? 'text',
|
|
47
|
+
maxWarnings: parseIntOr(args['max-warnings'], Number.POSITIVE_INFINITY),
|
|
48
|
+
quiet: Boolean(args.quiet),
|
|
49
|
+
rules,
|
|
50
|
+
noRules,
|
|
51
|
+
changed: Boolean(args.changed),
|
|
52
|
+
live: Boolean(args.live),
|
|
53
|
+
customer: args.customer,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function run(customerConfig, args, verbose) {
|
|
57
|
+
const config = loadNewoLintConfig();
|
|
58
|
+
// With explicit positional paths AND no customer/live flag, skip customer
|
|
59
|
+
// selection entirely - lint operates purely on the given filesystem paths.
|
|
60
|
+
const hasCustomerContext = args.customer !== undefined ||
|
|
61
|
+
args.live ||
|
|
62
|
+
Object.keys(customerConfig.customers ?? {}).length > 0;
|
|
63
|
+
const { selectedCustomer, allCustomers, isMultiCustomer } = hasCustomerContext
|
|
64
|
+
? selectSingleCustomer(customerConfig, args.customer)
|
|
65
|
+
: { selectedCustomer: null, allCustomers: [], isMultiCustomer: false };
|
|
66
|
+
const targetCustomer = selectedCustomer ?? (isMultiCustomer ? null : allCustomers[0] ?? null);
|
|
67
|
+
const schemas = await resolveSchemas(targetCustomer, args, verbose);
|
|
68
|
+
const ruleOverrides = {
|
|
69
|
+
...(config.rules ?? {}),
|
|
70
|
+
};
|
|
71
|
+
for (const code of args.noRules)
|
|
72
|
+
ruleOverrides[code] = 'off';
|
|
73
|
+
// --rule enables; we map unknown codes to 'warning' to avoid silently accepting typos.
|
|
74
|
+
for (const code of args.rules) {
|
|
75
|
+
if (!(code in ruleOverrides) || ruleOverrides[code] === 'off') {
|
|
76
|
+
ruleOverrides[code] = 'warning';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const linterOpts = {
|
|
80
|
+
rules: ruleOverrides,
|
|
81
|
+
...(schemas !== undefined ? { schemas } : {}),
|
|
82
|
+
};
|
|
83
|
+
const linter = createLinter(linterOpts);
|
|
84
|
+
const files = await resolveFiles(targetCustomer, allCustomers, args, isMultiCustomer);
|
|
85
|
+
if (files.length === 0) {
|
|
86
|
+
return { results: [], errorCount: 0, warningCount: 0 };
|
|
87
|
+
}
|
|
88
|
+
let errorCount = 0;
|
|
89
|
+
let warningCount = 0;
|
|
90
|
+
const results = [];
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const source = await fs.readFile(file.absPath, 'utf8');
|
|
93
|
+
const result = linter.lint(source, file.absPath);
|
|
94
|
+
for (const d of result.diagnostics) {
|
|
95
|
+
if (d.severity === 'error')
|
|
96
|
+
errorCount++;
|
|
97
|
+
else if (d.severity === 'warning')
|
|
98
|
+
warningCount++;
|
|
99
|
+
}
|
|
100
|
+
if (args.quiet) {
|
|
101
|
+
result.diagnostics = result.diagnostics.filter(d => d.severity === 'error');
|
|
102
|
+
}
|
|
103
|
+
results.push(result);
|
|
104
|
+
}
|
|
105
|
+
return { results, errorCount, warningCount: args.quiet ? 0 : warningCount };
|
|
106
|
+
}
|
|
107
|
+
async function resolveSchemas(customer, args, verbose) {
|
|
108
|
+
if (!customer)
|
|
109
|
+
return 'bundled';
|
|
110
|
+
if (args.live) {
|
|
111
|
+
if (verbose)
|
|
112
|
+
console.log(`Refreshing live schemas for ${customer.idn}...`);
|
|
113
|
+
const snapshot = await refreshLiveSchema(customer);
|
|
114
|
+
return { kind: 'inline', actions: snapshot.actions };
|
|
115
|
+
}
|
|
116
|
+
// Auto-use cached live snapshot if it exists (faster, always specific to
|
|
117
|
+
// the customer's actual NEWO account state). Fall back to bundled.
|
|
118
|
+
const cached = await loadCachedLiveSchema(customer.idn);
|
|
119
|
+
if (cached) {
|
|
120
|
+
if (verbose) {
|
|
121
|
+
const age = Math.round((Date.now() - Date.parse(cached.fetchedAt)) / 1000 / 60);
|
|
122
|
+
console.log(`Using cached schemas for ${customer.idn} (${age} min old). Use --live to refresh.`);
|
|
123
|
+
}
|
|
124
|
+
return { kind: 'inline', actions: cached.actions };
|
|
125
|
+
}
|
|
126
|
+
return 'bundled';
|
|
127
|
+
}
|
|
128
|
+
async function resolveFiles(selected, all, args, isMultiCustomer) {
|
|
129
|
+
// Explicit positional paths beat everything else.
|
|
130
|
+
if (args.positional.length > 0) {
|
|
131
|
+
const files = [];
|
|
132
|
+
for (const p of args.positional) {
|
|
133
|
+
const discovered = await discoverFromPath(p, {
|
|
134
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
135
|
+
});
|
|
136
|
+
files.push(...discovered);
|
|
137
|
+
}
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
if (selected) {
|
|
141
|
+
const formatVersion = resolveFormat(selected.idn, args.formatVersion).version;
|
|
142
|
+
return discoverCustomerFiles(selected, {
|
|
143
|
+
format: formatVersion,
|
|
144
|
+
changedOnly: args.changed,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (isMultiCustomer) {
|
|
148
|
+
const files = [];
|
|
149
|
+
for (const customer of all) {
|
|
150
|
+
const formatVersion = resolveFormat(customer.idn, args.formatVersion).version;
|
|
151
|
+
const customerFiles = await discoverCustomerFiles(customer, {
|
|
152
|
+
format: formatVersion,
|
|
153
|
+
changedOnly: args.changed,
|
|
154
|
+
});
|
|
155
|
+
files.push(...customerFiles);
|
|
156
|
+
}
|
|
157
|
+
return files;
|
|
158
|
+
}
|
|
159
|
+
// No customer context - lint cwd / newo_customers/ directly.
|
|
160
|
+
return discoverFromPath(defaultRoot(), {
|
|
161
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function determineExitCode(report, args) {
|
|
165
|
+
if (report.errorCount > 0)
|
|
166
|
+
return 1;
|
|
167
|
+
if (report.warningCount > args.maxWarnings)
|
|
168
|
+
return 1;
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
function collectCsv(value) {
|
|
172
|
+
if (value === undefined || value === null)
|
|
173
|
+
return [];
|
|
174
|
+
const items = Array.isArray(value) ? value : [value];
|
|
175
|
+
return items
|
|
176
|
+
.flatMap((v) => String(v).split(','))
|
|
177
|
+
.map(s => s.trim())
|
|
178
|
+
.filter(s => s.length > 0);
|
|
179
|
+
}
|
|
180
|
+
function parseIntOr(value, fallback) {
|
|
181
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
182
|
+
return value;
|
|
183
|
+
if (typeof value === 'string') {
|
|
184
|
+
const parsed = Number.parseInt(value, 10);
|
|
185
|
+
if (!Number.isNaN(parsed))
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
return fallback;
|
|
189
|
+
}
|
|
190
|
+
function toFormatVersion(v) {
|
|
191
|
+
return v === 'newo_v2' ? 'newo_v2' : 'cli_v1';
|
|
192
|
+
}
|
|
193
|
+
// Silence unused path import warning; path is used via discovery helpers.
|
|
194
|
+
void path;
|
|
195
|
+
//# sourceMappingURL=lint.js.map
|
|
@@ -62,7 +62,7 @@ export declare const RESOURCE_TYPES: {
|
|
|
62
62
|
/**
|
|
63
63
|
* All available resource types for sync operations
|
|
64
64
|
*/
|
|
65
|
-
export declare const ALL_RESOURCE_TYPES: ("projects" | "
|
|
65
|
+
export declare const ALL_RESOURCE_TYPES: ("projects" | "conversations" | "integrations" | "akb" | "attributes")[];
|
|
66
66
|
/**
|
|
67
67
|
* Pushable resource types (excludes read-only resources like conversations)
|
|
68
68
|
*/
|
package/dist/cli.js
CHANGED
|
@@ -47,16 +47,11 @@ import { handleWatchCommand } from './cli/commands/watch.js';
|
|
|
47
47
|
import { handleDiffCommand } from './cli/commands/diff.js';
|
|
48
48
|
import { handleLogsCommand } from './cli/commands/logs.js';
|
|
49
49
|
import { handleExportCommand } from './cli/commands/export.js';
|
|
50
|
+
import { handleLintCommand } from './cli/commands/lint.js';
|
|
51
|
+
import { handleFormatCommand } from './cli/commands/format.js';
|
|
52
|
+
import { handleCheckCommand } from './cli/commands/check.js';
|
|
50
53
|
dotenv.config();
|
|
51
54
|
async function main() {
|
|
52
|
-
try {
|
|
53
|
-
// Initialize and validate environment at startup
|
|
54
|
-
initializeEnvironment();
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
console.error('Environment validation failed:', error instanceof Error ? error.message : String(error));
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
55
|
const args = minimist(process.argv.slice(2));
|
|
61
56
|
const cmd = args._[0];
|
|
62
57
|
const verbose = Boolean(args.verbose || args.v);
|
|
@@ -67,11 +62,43 @@ async function main() {
|
|
|
67
62
|
}
|
|
68
63
|
if (verbose)
|
|
69
64
|
console.log(`🔍 Command parsed: "${cmd}"`);
|
|
70
|
-
// Handle help command first
|
|
65
|
+
// Handle help command first - no env or customer config needed
|
|
71
66
|
if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
|
|
72
67
|
handleHelpCommand();
|
|
73
68
|
return;
|
|
74
69
|
}
|
|
70
|
+
// Offline commands: lint/format/check don't need NEWO credentials
|
|
71
|
+
// UNLESS the user passes --customer or --live (both touch the API).
|
|
72
|
+
const isOfflineLint = (cmd === 'lint' || cmd === 'format' || cmd === 'check') &&
|
|
73
|
+
!args.customer &&
|
|
74
|
+
!args.live;
|
|
75
|
+
if (isOfflineLint) {
|
|
76
|
+
try {
|
|
77
|
+
const emptyConfig = { customers: {} };
|
|
78
|
+
switch (cmd) {
|
|
79
|
+
case 'lint':
|
|
80
|
+
await handleLintCommand(emptyConfig, args, verbose);
|
|
81
|
+
return;
|
|
82
|
+
case 'format':
|
|
83
|
+
await handleFormatCommand(emptyConfig, args, verbose);
|
|
84
|
+
return;
|
|
85
|
+
case 'check':
|
|
86
|
+
await handleCheckCommand(emptyConfig, args, verbose);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
handleCliError(error, cmd);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// All other commands (and customer-scoped lint/format/check) require env + customer config.
|
|
95
|
+
try {
|
|
96
|
+
initializeEnvironment();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error('Environment validation failed:', error instanceof Error ? error.message : String(error));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
75
102
|
// Handle list-customers command (doesn't need full customer config)
|
|
76
103
|
if (cmd === 'list-customers') {
|
|
77
104
|
try {
|
|
@@ -204,6 +231,15 @@ async function main() {
|
|
|
204
231
|
case 'diff':
|
|
205
232
|
await handleDiffCommand(customerConfig, args, verbose);
|
|
206
233
|
break;
|
|
234
|
+
case 'lint':
|
|
235
|
+
await handleLintCommand(customerConfig, args, verbose);
|
|
236
|
+
break;
|
|
237
|
+
case 'format':
|
|
238
|
+
await handleFormatCommand(customerConfig, args, verbose);
|
|
239
|
+
break;
|
|
240
|
+
case 'check':
|
|
241
|
+
await handleCheckCommand(customerConfig, args, verbose);
|
|
242
|
+
break;
|
|
207
243
|
default:
|
|
208
244
|
console.error('Unknown command:', cmd);
|
|
209
245
|
console.error('Run "newo --help" for usage information');
|
|
@@ -15,6 +15,7 @@ import path from 'path';
|
|
|
15
15
|
import { getCustomerAttributes, getProjectAttributes, updateCustomerAttribute, updateProjectAttribute, listProjects } from '../../../api.js';
|
|
16
16
|
import { writeFileSafe, customerAttributesPath, customerAttributesMapPath } from '../../../fsutil.js';
|
|
17
17
|
import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
|
|
18
|
+
import { isJsonValueType, normalizeJsonValueForStorage, jsonValuesEqual } from '../../../sync/json-attr-utils.js';
|
|
18
19
|
import { sha256, saveHashes, loadHashes } from '../../../hash.js';
|
|
19
20
|
/**
|
|
20
21
|
* AttributeSyncStrategy - Handles attribute synchronization
|
|
@@ -142,8 +143,15 @@ export class AttributeSyncStrategy {
|
|
|
142
143
|
*/
|
|
143
144
|
cleanAttribute(attr) {
|
|
144
145
|
let processedValue = attr.value;
|
|
145
|
-
//
|
|
146
|
-
|
|
146
|
+
// Coerce JSON-typed values to a STRING. The API may return parsed
|
|
147
|
+
// objects for `value_type: json`; if we let yaml.dump turn them into
|
|
148
|
+
// YAML structures, the next push sends an object and the Workflow
|
|
149
|
+
// Builder canvas blanks out. See src/sync/json-attr-utils.ts.
|
|
150
|
+
if (isJsonValueType(attr.value_type)) {
|
|
151
|
+
processedValue = normalizeJsonValueForStorage(attr.value);
|
|
152
|
+
}
|
|
153
|
+
else if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
|
|
154
|
+
// Legacy: reformat array-of-objects JSON strings for readability
|
|
147
155
|
try {
|
|
148
156
|
const parsed = JSON.parse(attr.value);
|
|
149
157
|
processedValue = JSON.stringify(parsed, null, 0);
|
|
@@ -243,10 +251,22 @@ export class AttributeSyncStrategy {
|
|
|
243
251
|
const remoteAttr = remoteMap.get(localAttr.idn);
|
|
244
252
|
if (!remoteAttr)
|
|
245
253
|
continue;
|
|
246
|
-
|
|
254
|
+
// For JSON-typed attrs, compare canonical JSON (handles
|
|
255
|
+
// pretty/compact and string/object differences). Always send the
|
|
256
|
+
// value as a STRING so the platform stores the canvas the way the
|
|
257
|
+
// Workflow Builder expects to read it back.
|
|
258
|
+
const isJson = isJsonValueType(localAttr.value_type);
|
|
259
|
+
const valuesAreEqual = isJson
|
|
260
|
+
? jsonValuesEqual(localAttr.value, remoteAttr.value)
|
|
261
|
+
: String(localAttr.value) === String(remoteAttr.value);
|
|
262
|
+
if (!valuesAreEqual) {
|
|
263
|
+
const valueToSend = isJson
|
|
264
|
+
? normalizeJsonValueForStorage(localAttr.value)
|
|
265
|
+
: localAttr.value;
|
|
247
266
|
await updateCustomerAttribute(client, {
|
|
248
|
-
|
|
249
|
-
|
|
267
|
+
...localAttr,
|
|
268
|
+
value: valueToSend,
|
|
269
|
+
id: attributeId
|
|
250
270
|
});
|
|
251
271
|
updatedCount++;
|
|
252
272
|
this.logger.info(` ✓ Updated customer attribute: ${localAttr.idn}`);
|
|
@@ -286,10 +306,20 @@ export class AttributeSyncStrategy {
|
|
|
286
306
|
const remoteAttr = remoteMap.get(localAttr.idn);
|
|
287
307
|
if (!remoteAttr)
|
|
288
308
|
continue;
|
|
289
|
-
|
|
309
|
+
// Same canonical-JSON / always-string-on-push policy as customer
|
|
310
|
+
// attributes (see pushCustomerAttributes for rationale).
|
|
311
|
+
const isJson = isJsonValueType(localAttr.value_type);
|
|
312
|
+
const valuesAreEqual = isJson
|
|
313
|
+
? jsonValuesEqual(localAttr.value, remoteAttr.value)
|
|
314
|
+
: String(localAttr.value) === String(remoteAttr.value);
|
|
315
|
+
if (!valuesAreEqual) {
|
|
316
|
+
const valueToSend = isJson
|
|
317
|
+
? normalizeJsonValueForStorage(localAttr.value)
|
|
318
|
+
: localAttr.value;
|
|
290
319
|
await updateProjectAttribute(client, project.id, {
|
|
291
|
-
|
|
292
|
-
|
|
320
|
+
...localAttr,
|
|
321
|
+
value: valueToSend,
|
|
322
|
+
id: attributeId
|
|
293
323
|
});
|
|
294
324
|
updatedCount++;
|
|
295
325
|
this.logger.info(` ✓ Updated project attribute: ${projectIdn}/${localAttr.idn}`);
|