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
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
import type { MultiCustomerConfig, CliArgs } from '../../types.js';
|
|
10
|
+
|
|
11
|
+
export async function handleCheckCommand(
|
|
12
|
+
customerConfig: MultiCustomerConfig,
|
|
13
|
+
args: CliArgs,
|
|
14
|
+
verbose: boolean,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const lintArgs = { ...args };
|
|
17
|
+
await handleLintCommand(customerConfig, lintArgs as CliArgs, verbose);
|
|
18
|
+
|
|
19
|
+
const formatCheckArgs = { ...args, check: true } as CliArgs;
|
|
20
|
+
await handleFormatCommand(customerConfig, formatCheckArgs, verbose);
|
|
21
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
|
|
14
|
+
import { selectSingleCustomer } from '../customer-selection.js';
|
|
15
|
+
import { handleCliError } from '../errors.js';
|
|
16
|
+
import { resolveFormat } from '../../format/detect.js';
|
|
17
|
+
import { discoverCustomerFiles, discoverFromPath, defaultRoot } from '../../lint/discovery.js';
|
|
18
|
+
import type { MultiCustomerConfig, CliArgs, CustomerConfig } from '../../types.js';
|
|
19
|
+
import type { FormatVersion } from '../../format/types.js';
|
|
20
|
+
|
|
21
|
+
interface FormatArgs {
|
|
22
|
+
positional: string[];
|
|
23
|
+
formatVersion: string | undefined;
|
|
24
|
+
check: boolean;
|
|
25
|
+
customer: string | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function handleFormatCommand(
|
|
29
|
+
customerConfig: MultiCustomerConfig,
|
|
30
|
+
args: CliArgs,
|
|
31
|
+
verbose: boolean,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
const fmtArgs = parseArgs(args);
|
|
35
|
+
const formatter = createFormatter();
|
|
36
|
+
|
|
37
|
+
const hasCustomerContext =
|
|
38
|
+
fmtArgs.customer !== undefined ||
|
|
39
|
+
Object.keys(customerConfig.customers ?? {}).length > 0;
|
|
40
|
+
|
|
41
|
+
const { selectedCustomer, allCustomers, isMultiCustomer } = hasCustomerContext
|
|
42
|
+
? selectSingleCustomer(customerConfig, fmtArgs.customer)
|
|
43
|
+
: { selectedCustomer: null, allCustomers: [] as CustomerConfig[], isMultiCustomer: false };
|
|
44
|
+
|
|
45
|
+
const targetCustomer = selectedCustomer ?? (isMultiCustomer ? null : allCustomers[0] ?? null);
|
|
46
|
+
void targetCustomer;
|
|
47
|
+
|
|
48
|
+
const files = await resolveFiles(targetCustomer, allCustomers, fmtArgs, isMultiCustomer);
|
|
49
|
+
if (files.length === 0) {
|
|
50
|
+
console.log('No files matched.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let touched = 0;
|
|
55
|
+
let needsFormat = 0;
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const source = await fs.readFile(file.absPath, 'utf8');
|
|
58
|
+
const result = formatter.format(source, file.absPath);
|
|
59
|
+
if (!result.changed) continue;
|
|
60
|
+
needsFormat++;
|
|
61
|
+
if (fmtArgs.check) {
|
|
62
|
+
console.log(`would rewrite ${path.relative(process.cwd(), file.absPath)}`);
|
|
63
|
+
} else {
|
|
64
|
+
await fs.writeFile(file.absPath, result.formatted, 'utf8');
|
|
65
|
+
touched++;
|
|
66
|
+
if (verbose) console.log(`formatted ${path.relative(process.cwd(), file.absPath)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (fmtArgs.check) {
|
|
71
|
+
if (needsFormat === 0) {
|
|
72
|
+
console.log('All files are properly formatted.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log(`${needsFormat} file(s) would be reformatted.`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
} else {
|
|
78
|
+
console.log(`Formatted ${touched} file(s).`);
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
handleCliError(err, 'format');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseArgs(args: CliArgs): FormatArgs {
|
|
86
|
+
const positional = args._.slice(1).filter((p): p is string => typeof p === 'string');
|
|
87
|
+
return {
|
|
88
|
+
positional,
|
|
89
|
+
formatVersion: args.format as string | undefined,
|
|
90
|
+
check: Boolean(args.check),
|
|
91
|
+
customer: args.customer as string | undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function resolveFiles(
|
|
96
|
+
selected: CustomerConfig | null,
|
|
97
|
+
all: CustomerConfig[],
|
|
98
|
+
args: FormatArgs,
|
|
99
|
+
isMultiCustomer: boolean,
|
|
100
|
+
) {
|
|
101
|
+
if (args.positional.length > 0) {
|
|
102
|
+
const files = [];
|
|
103
|
+
for (const p of args.positional) {
|
|
104
|
+
files.push(
|
|
105
|
+
...(await discoverFromPath(p, {
|
|
106
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
107
|
+
})),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return files;
|
|
111
|
+
}
|
|
112
|
+
if (selected) {
|
|
113
|
+
const formatVersion = resolveFormat(selected.idn, args.formatVersion).version;
|
|
114
|
+
return discoverCustomerFiles(selected, { format: formatVersion });
|
|
115
|
+
}
|
|
116
|
+
if (isMultiCustomer) {
|
|
117
|
+
const files = [];
|
|
118
|
+
for (const customer of all) {
|
|
119
|
+
const formatVersion = resolveFormat(customer.idn, args.formatVersion).version;
|
|
120
|
+
files.push(...(await discoverCustomerFiles(customer, { format: formatVersion })));
|
|
121
|
+
}
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
return discoverFromPath(defaultRoot(), {
|
|
125
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toFormatVersion(v: string): FormatVersion {
|
|
130
|
+
return v === 'newo_v2' ? 'newo_v2' : 'cli_v1';
|
|
131
|
+
}
|
package/src/cli/commands/help.ts
CHANGED
|
@@ -22,6 +22,19 @@ Core Commands:
|
|
|
22
22
|
newo meta [--customer <idn>] # get project metadata (debug)
|
|
23
23
|
newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles
|
|
24
24
|
|
|
25
|
+
Linting & Formatting (NEW):
|
|
26
|
+
newo lint [paths...] [--format <fmt>] [--reporter <text|json|sarif>] # static-analysis on DSL files
|
|
27
|
+
newo lint --changed # lint only files modified since last push
|
|
28
|
+
newo lint --live # refresh action catalog from NEWO API
|
|
29
|
+
newo lint --rule <code> --no-rule <code> # selectively enable/disable rules
|
|
30
|
+
newo lint --max-warnings <n> # fail when warnings exceed threshold
|
|
31
|
+
newo format [paths...] [--check] # apply canonical formatting (in-place or --check)
|
|
32
|
+
newo check [paths...] # umbrella: lint + format --check (CI gate)
|
|
33
|
+
|
|
34
|
+
Powered by newo-dsl-analyzer. Exit codes: 0 clean, 1 lint errors, 2 runtime error.
|
|
35
|
+
Discover rules via the Diagnostic code column (E100, W101, ...); configure
|
|
36
|
+
them in .neworc.yaml at your repo root. Plugin authors: depend on newo-dsl-core.
|
|
37
|
+
|
|
25
38
|
Project Management:
|
|
26
39
|
newo create-project <idn> [--title <title>] [--description <desc>] [--version <version>] [--auto-update] # create empty project on platform
|
|
27
40
|
newo list-registries [--customer <idn>] # list available project registries (production, staging, etc.)
|
|
@@ -0,0 +1,246 @@
|
|
|
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 {
|
|
13
|
+
createLinter,
|
|
14
|
+
type LinterOptions,
|
|
15
|
+
type ProjectLintReport,
|
|
16
|
+
type RuleSeverity,
|
|
17
|
+
} from 'newo-dsl-analyzer';
|
|
18
|
+
|
|
19
|
+
import { selectSingleCustomer } from '../customer-selection.js';
|
|
20
|
+
import { handleCliError } from '../errors.js';
|
|
21
|
+
import { resolveFormat } from '../../format/detect.js';
|
|
22
|
+
import { discoverCustomerFiles, discoverFromPath, defaultRoot } from '../../lint/discovery.js';
|
|
23
|
+
import { loadNewoLintConfig } from '../../lint/config.js';
|
|
24
|
+
import { refreshLiveSchema, loadCachedLiveSchema } from '../../lint/live-schema.js';
|
|
25
|
+
import { pickReporter } from '../../lint/reporters/index.js';
|
|
26
|
+
import type { MultiCustomerConfig, CliArgs, CustomerConfig } from '../../types.js';
|
|
27
|
+
|
|
28
|
+
interface LintArgs {
|
|
29
|
+
positional: string[];
|
|
30
|
+
formatVersion: string | undefined;
|
|
31
|
+
reporter: string;
|
|
32
|
+
maxWarnings: number;
|
|
33
|
+
quiet: boolean;
|
|
34
|
+
rules: string[];
|
|
35
|
+
noRules: string[];
|
|
36
|
+
changed: boolean;
|
|
37
|
+
live: boolean;
|
|
38
|
+
customer: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function handleLintCommand(
|
|
42
|
+
customerConfig: MultiCustomerConfig,
|
|
43
|
+
args: CliArgs,
|
|
44
|
+
verbose: boolean,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
const lintArgs = parseArgs(args);
|
|
48
|
+
const report = await run(customerConfig, lintArgs, verbose);
|
|
49
|
+
const reporter = pickReporter(lintArgs.reporter);
|
|
50
|
+
const output = reporter.write(report);
|
|
51
|
+
if (output.trim().length > 0) process.stdout.write(output + '\n');
|
|
52
|
+
|
|
53
|
+
const exitCode = determineExitCode(report, lintArgs);
|
|
54
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
handleCliError(err, 'lint');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseArgs(args: CliArgs): LintArgs {
|
|
61
|
+
const positional = args._.slice(1).filter((p): p is string => typeof p === 'string');
|
|
62
|
+
|
|
63
|
+
// Multiple flag shapes: --rule=E100, --rule E100,W100
|
|
64
|
+
// Disabling rules uses --rule-off (not --no-rule) because minimist treats
|
|
65
|
+
// `--no-X` as `X: false` and swallows the next positional argument.
|
|
66
|
+
const rules = collectCsv(args.rule);
|
|
67
|
+
const noRules = collectCsv(args['rule-off']);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
positional,
|
|
71
|
+
formatVersion: args.format as string | undefined,
|
|
72
|
+
reporter: (args.reporter as string | undefined) ?? (args['output-format'] as string | undefined) ?? 'text',
|
|
73
|
+
maxWarnings: parseIntOr(args['max-warnings'], Number.POSITIVE_INFINITY),
|
|
74
|
+
quiet: Boolean(args.quiet),
|
|
75
|
+
rules,
|
|
76
|
+
noRules,
|
|
77
|
+
changed: Boolean(args.changed),
|
|
78
|
+
live: Boolean(args.live),
|
|
79
|
+
customer: args.customer as string | undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function run(
|
|
84
|
+
customerConfig: MultiCustomerConfig,
|
|
85
|
+
args: LintArgs,
|
|
86
|
+
verbose: boolean,
|
|
87
|
+
): Promise<ProjectLintReport> {
|
|
88
|
+
const config = loadNewoLintConfig();
|
|
89
|
+
|
|
90
|
+
// With explicit positional paths AND no customer/live flag, skip customer
|
|
91
|
+
// selection entirely - lint operates purely on the given filesystem paths.
|
|
92
|
+
const hasCustomerContext =
|
|
93
|
+
args.customer !== undefined ||
|
|
94
|
+
args.live ||
|
|
95
|
+
Object.keys(customerConfig.customers ?? {}).length > 0;
|
|
96
|
+
|
|
97
|
+
const { selectedCustomer, allCustomers, isMultiCustomer } = hasCustomerContext
|
|
98
|
+
? selectSingleCustomer(customerConfig, args.customer)
|
|
99
|
+
: { selectedCustomer: null, allCustomers: [] as CustomerConfig[], isMultiCustomer: false };
|
|
100
|
+
|
|
101
|
+
const targetCustomer = selectedCustomer ?? (isMultiCustomer ? null : allCustomers[0] ?? null);
|
|
102
|
+
|
|
103
|
+
const schemas = await resolveSchemas(targetCustomer, args, verbose);
|
|
104
|
+
|
|
105
|
+
const ruleOverrides: Record<string, RuleSeverity> = {
|
|
106
|
+
...(config.rules ?? {}),
|
|
107
|
+
};
|
|
108
|
+
for (const code of args.noRules) ruleOverrides[code] = 'off';
|
|
109
|
+
// --rule enables; we map unknown codes to 'warning' to avoid silently accepting typos.
|
|
110
|
+
for (const code of args.rules) {
|
|
111
|
+
if (!(code in ruleOverrides) || ruleOverrides[code] === 'off') {
|
|
112
|
+
ruleOverrides[code] = 'warning';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const linterOpts: LinterOptions = {
|
|
117
|
+
rules: ruleOverrides,
|
|
118
|
+
...(schemas !== undefined ? { schemas } : {}),
|
|
119
|
+
};
|
|
120
|
+
const linter = createLinter(linterOpts);
|
|
121
|
+
|
|
122
|
+
const files = await resolveFiles(targetCustomer, allCustomers, args, isMultiCustomer);
|
|
123
|
+
if (files.length === 0) {
|
|
124
|
+
return { results: [], errorCount: 0, warningCount: 0 };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let errorCount = 0;
|
|
128
|
+
let warningCount = 0;
|
|
129
|
+
const results = [];
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
const source = await fs.readFile(file.absPath, 'utf8');
|
|
132
|
+
const result = linter.lint(source, file.absPath);
|
|
133
|
+
for (const d of result.diagnostics) {
|
|
134
|
+
if (d.severity === 'error') errorCount++;
|
|
135
|
+
else if (d.severity === 'warning') warningCount++;
|
|
136
|
+
}
|
|
137
|
+
if (args.quiet) {
|
|
138
|
+
result.diagnostics = result.diagnostics.filter(d => d.severity === 'error');
|
|
139
|
+
}
|
|
140
|
+
results.push(result);
|
|
141
|
+
}
|
|
142
|
+
return { results, errorCount, warningCount: args.quiet ? 0 : warningCount };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function resolveSchemas(
|
|
146
|
+
customer: CustomerConfig | null,
|
|
147
|
+
args: LintArgs,
|
|
148
|
+
verbose: boolean,
|
|
149
|
+
): Promise<LinterOptions['schemas']> {
|
|
150
|
+
if (!customer) return 'bundled';
|
|
151
|
+
|
|
152
|
+
if (args.live) {
|
|
153
|
+
if (verbose) console.log(`Refreshing live schemas for ${customer.idn}...`);
|
|
154
|
+
const snapshot = await refreshLiveSchema(customer);
|
|
155
|
+
return { kind: 'inline', actions: snapshot.actions };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Auto-use cached live snapshot if it exists (faster, always specific to
|
|
159
|
+
// the customer's actual NEWO account state). Fall back to bundled.
|
|
160
|
+
const cached = await loadCachedLiveSchema(customer.idn);
|
|
161
|
+
if (cached) {
|
|
162
|
+
if (verbose) {
|
|
163
|
+
const age = Math.round((Date.now() - Date.parse(cached.fetchedAt)) / 1000 / 60);
|
|
164
|
+
console.log(`Using cached schemas for ${customer.idn} (${age} min old). Use --live to refresh.`);
|
|
165
|
+
}
|
|
166
|
+
return { kind: 'inline', actions: cached.actions };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return 'bundled';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function resolveFiles(
|
|
173
|
+
selected: CustomerConfig | null,
|
|
174
|
+
all: CustomerConfig[],
|
|
175
|
+
args: LintArgs,
|
|
176
|
+
isMultiCustomer: boolean,
|
|
177
|
+
): ReturnType<typeof discoverFromPath> {
|
|
178
|
+
// Explicit positional paths beat everything else.
|
|
179
|
+
if (args.positional.length > 0) {
|
|
180
|
+
const files = [];
|
|
181
|
+
for (const p of args.positional) {
|
|
182
|
+
const discovered = await discoverFromPath(p, {
|
|
183
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
184
|
+
});
|
|
185
|
+
files.push(...discovered);
|
|
186
|
+
}
|
|
187
|
+
return files;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (selected) {
|
|
191
|
+
const formatVersion = resolveFormat(selected.idn, args.formatVersion).version;
|
|
192
|
+
return discoverCustomerFiles(selected, {
|
|
193
|
+
format: formatVersion,
|
|
194
|
+
changedOnly: args.changed,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (isMultiCustomer) {
|
|
199
|
+
const files = [];
|
|
200
|
+
for (const customer of all) {
|
|
201
|
+
const formatVersion = resolveFormat(customer.idn, args.formatVersion).version;
|
|
202
|
+
const customerFiles = await discoverCustomerFiles(customer, {
|
|
203
|
+
format: formatVersion,
|
|
204
|
+
changedOnly: args.changed,
|
|
205
|
+
});
|
|
206
|
+
files.push(...customerFiles);
|
|
207
|
+
}
|
|
208
|
+
return files;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// No customer context - lint cwd / newo_customers/ directly.
|
|
212
|
+
return discoverFromPath(defaultRoot(), {
|
|
213
|
+
...(args.formatVersion ? { format: toFormatVersion(args.formatVersion) } : {}),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function determineExitCode(report: ProjectLintReport, args: LintArgs): number {
|
|
218
|
+
if (report.errorCount > 0) return 1;
|
|
219
|
+
if (report.warningCount > args.maxWarnings) return 1;
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function collectCsv(value: unknown): string[] {
|
|
224
|
+
if (value === undefined || value === null) return [];
|
|
225
|
+
const items = Array.isArray(value) ? value : [value];
|
|
226
|
+
return items
|
|
227
|
+
.flatMap((v: unknown) => String(v).split(','))
|
|
228
|
+
.map(s => s.trim())
|
|
229
|
+
.filter(s => s.length > 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseIntOr(value: unknown, fallback: number): number {
|
|
233
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
234
|
+
if (typeof value === 'string') {
|
|
235
|
+
const parsed = Number.parseInt(value, 10);
|
|
236
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
237
|
+
}
|
|
238
|
+
return fallback;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function toFormatVersion(v: string): 'cli_v1' | 'newo_v2' {
|
|
242
|
+
return v === 'newo_v2' ? 'newo_v2' : 'cli_v1';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Silence unused path import warning; path is used via discovery helpers.
|
|
246
|
+
void path;
|
package/src/cli.ts
CHANGED
|
@@ -47,19 +47,14 @@ 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
|
import type { CliArgs, NewoApiError } from './types.js';
|
|
51
54
|
|
|
52
55
|
dotenv.config();
|
|
53
56
|
|
|
54
57
|
async function main(): Promise<void> {
|
|
55
|
-
try {
|
|
56
|
-
// Initialize and validate environment at startup
|
|
57
|
-
initializeEnvironment();
|
|
58
|
-
} catch (error: unknown) {
|
|
59
|
-
console.error('Environment validation failed:', error instanceof Error ? error.message : String(error));
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
const args = minimist(process.argv.slice(2)) as CliArgs;
|
|
64
59
|
const cmd = args._[0];
|
|
65
60
|
const verbose = Boolean(args.verbose || args.v);
|
|
@@ -72,12 +67,46 @@ async function main(): Promise<void> {
|
|
|
72
67
|
|
|
73
68
|
if (verbose) console.log(`🔍 Command parsed: "${cmd}"`);
|
|
74
69
|
|
|
75
|
-
// Handle help command first
|
|
70
|
+
// Handle help command first - no env or customer config needed
|
|
76
71
|
if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
|
|
77
72
|
handleHelpCommand();
|
|
78
73
|
return;
|
|
79
74
|
}
|
|
80
75
|
|
|
76
|
+
// Offline commands: lint/format/check don't need NEWO credentials
|
|
77
|
+
// UNLESS the user passes --customer or --live (both touch the API).
|
|
78
|
+
const isOfflineLint =
|
|
79
|
+
(cmd === 'lint' || cmd === 'format' || cmd === 'check') &&
|
|
80
|
+
!args.customer &&
|
|
81
|
+
!args.live;
|
|
82
|
+
|
|
83
|
+
if (isOfflineLint) {
|
|
84
|
+
try {
|
|
85
|
+
const emptyConfig: import('./types.js').MultiCustomerConfig = { customers: {} };
|
|
86
|
+
switch (cmd) {
|
|
87
|
+
case 'lint':
|
|
88
|
+
await handleLintCommand(emptyConfig, args, verbose);
|
|
89
|
+
return;
|
|
90
|
+
case 'format':
|
|
91
|
+
await handleFormatCommand(emptyConfig, args, verbose);
|
|
92
|
+
return;
|
|
93
|
+
case 'check':
|
|
94
|
+
await handleCheckCommand(emptyConfig, args, verbose);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
} catch (error: unknown) {
|
|
98
|
+
handleCliError(error, cmd);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// All other commands (and customer-scoped lint/format/check) require env + customer config.
|
|
103
|
+
try {
|
|
104
|
+
initializeEnvironment();
|
|
105
|
+
} catch (error: unknown) {
|
|
106
|
+
console.error('Environment validation failed:', error instanceof Error ? error.message : String(error));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
81
110
|
// Handle list-customers command (doesn't need full customer config)
|
|
82
111
|
if (cmd === 'list-customers') {
|
|
83
112
|
try {
|
|
@@ -249,6 +278,18 @@ async function main(): Promise<void> {
|
|
|
249
278
|
await handleDiffCommand(customerConfig, args, verbose);
|
|
250
279
|
break;
|
|
251
280
|
|
|
281
|
+
case 'lint':
|
|
282
|
+
await handleLintCommand(customerConfig, args, verbose);
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case 'format':
|
|
286
|
+
await handleFormatCommand(customerConfig, args, verbose);
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case 'check':
|
|
290
|
+
await handleCheckCommand(customerConfig, args, verbose);
|
|
291
|
+
break;
|
|
292
|
+
|
|
252
293
|
default:
|
|
253
294
|
console.error('Unknown command:', cmd);
|
|
254
295
|
console.error('Run "newo --help" for usage information');
|
|
@@ -39,6 +39,11 @@ import {
|
|
|
39
39
|
customerAttributesMapPath
|
|
40
40
|
} from '../../../fsutil.js';
|
|
41
41
|
import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
|
|
42
|
+
import {
|
|
43
|
+
isJsonValueType,
|
|
44
|
+
normalizeJsonValueForStorage,
|
|
45
|
+
jsonValuesEqual
|
|
46
|
+
} from '../../../sync/json-attr-utils.js';
|
|
42
47
|
import { sha256, saveHashes, loadHashes } from '../../../hash.js';
|
|
43
48
|
|
|
44
49
|
/**
|
|
@@ -220,8 +225,14 @@ export class AttributeSyncStrategy implements ISyncStrategy<CustomerAttributesRe
|
|
|
220
225
|
private cleanAttribute(attr: CustomerAttribute): CustomerAttribute {
|
|
221
226
|
let processedValue = attr.value;
|
|
222
227
|
|
|
223
|
-
//
|
|
224
|
-
|
|
228
|
+
// Coerce JSON-typed values to a STRING. The API may return parsed
|
|
229
|
+
// objects for `value_type: json`; if we let yaml.dump turn them into
|
|
230
|
+
// YAML structures, the next push sends an object and the Workflow
|
|
231
|
+
// Builder canvas blanks out. See src/sync/json-attr-utils.ts.
|
|
232
|
+
if (isJsonValueType(attr.value_type)) {
|
|
233
|
+
processedValue = normalizeJsonValueForStorage(attr.value);
|
|
234
|
+
} else if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
|
|
235
|
+
// Legacy: reformat array-of-objects JSON strings for readability
|
|
225
236
|
try {
|
|
226
237
|
const parsed = JSON.parse(attr.value);
|
|
227
238
|
processedValue = JSON.stringify(parsed, null, 0);
|
|
@@ -342,10 +353,24 @@ export class AttributeSyncStrategy implements ISyncStrategy<CustomerAttributesRe
|
|
|
342
353
|
const remoteAttr = remoteMap.get(localAttr.idn);
|
|
343
354
|
if (!remoteAttr) continue;
|
|
344
355
|
|
|
345
|
-
|
|
356
|
+
// For JSON-typed attrs, compare canonical JSON (handles
|
|
357
|
+
// pretty/compact and string/object differences). Always send the
|
|
358
|
+
// value as a STRING so the platform stores the canvas the way the
|
|
359
|
+
// Workflow Builder expects to read it back.
|
|
360
|
+
const isJson = isJsonValueType(localAttr.value_type);
|
|
361
|
+
const valuesAreEqual = isJson
|
|
362
|
+
? jsonValuesEqual(localAttr.value, remoteAttr.value)
|
|
363
|
+
: String(localAttr.value) === String(remoteAttr.value);
|
|
364
|
+
|
|
365
|
+
if (!valuesAreEqual) {
|
|
366
|
+
const valueToSend = isJson
|
|
367
|
+
? normalizeJsonValueForStorage(localAttr.value)
|
|
368
|
+
: localAttr.value;
|
|
369
|
+
|
|
346
370
|
await updateCustomerAttribute(client, {
|
|
347
|
-
|
|
348
|
-
|
|
371
|
+
...localAttr,
|
|
372
|
+
value: valueToSend,
|
|
373
|
+
id: attributeId
|
|
349
374
|
});
|
|
350
375
|
updatedCount++;
|
|
351
376
|
this.logger.info(` ✓ Updated customer attribute: ${localAttr.idn}`);
|
|
@@ -399,10 +424,22 @@ export class AttributeSyncStrategy implements ISyncStrategy<CustomerAttributesRe
|
|
|
399
424
|
const remoteAttr = remoteMap.get(localAttr.idn);
|
|
400
425
|
if (!remoteAttr) continue;
|
|
401
426
|
|
|
402
|
-
|
|
427
|
+
// Same canonical-JSON / always-string-on-push policy as customer
|
|
428
|
+
// attributes (see pushCustomerAttributes for rationale).
|
|
429
|
+
const isJson = isJsonValueType(localAttr.value_type);
|
|
430
|
+
const valuesAreEqual = isJson
|
|
431
|
+
? jsonValuesEqual(localAttr.value, remoteAttr.value)
|
|
432
|
+
: String(localAttr.value) === String(remoteAttr.value);
|
|
433
|
+
|
|
434
|
+
if (!valuesAreEqual) {
|
|
435
|
+
const valueToSend = isJson
|
|
436
|
+
? normalizeJsonValueForStorage(localAttr.value)
|
|
437
|
+
: localAttr.value;
|
|
438
|
+
|
|
403
439
|
await updateProjectAttribute(client, project.id, {
|
|
404
|
-
|
|
405
|
-
|
|
440
|
+
...localAttr,
|
|
441
|
+
value: valueToSend,
|
|
442
|
+
id: attributeId
|
|
406
443
|
});
|
|
407
444
|
updatedCount++;
|
|
408
445
|
this.logger.info(` ✓ Updated project attribute: ${projectIdn}/${localAttr.idn}`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint config resolution for the newo CLI.
|
|
3
|
+
*
|
|
4
|
+
* Looks for `.neworc.yaml` / `.neworc.yml` / `.neworc.json` starting at
|
|
5
|
+
* the cwd and walking up to the filesystem root. Thin wrapper around
|
|
6
|
+
* newo-dsl-analyzer's `loadConfig` so consumers can override location
|
|
7
|
+
* per command if they need to.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { loadConfig as analyzerLoadConfig } from 'newo-dsl-analyzer';
|
|
11
|
+
import type { NewoLintConfig } from 'newo-dsl-analyzer';
|
|
12
|
+
|
|
13
|
+
export type { NewoLintConfig } from 'newo-dsl-analyzer';
|
|
14
|
+
|
|
15
|
+
export function loadNewoLintConfig(startDir: string = process.cwd()): NewoLintConfig {
|
|
16
|
+
return analyzerLoadConfig(path.resolve(startDir)) ?? {};
|
|
17
|
+
}
|