newo 3.6.1 → 3.7.0
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 +41 -0
- 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 +7 -4
- 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 +14 -14
- package/dist/sync/conversations.d.ts +1 -1
- package/dist/sync/conversations.js +240 -193
- 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 +7 -4
- 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 +14 -14
- package/src/sync/conversations.ts +265 -212
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File discovery for `newo lint` / `newo format` / `newo check`.
|
|
3
|
+
*
|
|
4
|
+
* Walks a customer's tree (or any directory passed on the CLI), filters
|
|
5
|
+
* by format-aware extensions, and optionally narrows to files changed
|
|
6
|
+
* since the last push by consulting `.newo/{customer}/hashes.json`.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { NEWO_CUSTOMERS_DIR, customerDir } from '../fsutil.js';
|
|
11
|
+
import { loadHashes } from '../hash.js';
|
|
12
|
+
import { sha256 } from '../hash.js';
|
|
13
|
+
import { ALL_SCRIPT_EXTENSIONS, CLI_V1_EXTENSIONS, NEWO_V2_EXTENSIONS, } from '../format/types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Discover script files under a customer's tree.
|
|
16
|
+
* Respects format when given (else walks all recognized extensions).
|
|
17
|
+
*/
|
|
18
|
+
export async function discoverCustomerFiles(customer, opts = {}) {
|
|
19
|
+
const root = customerDir(customer.idn);
|
|
20
|
+
if (!(await fs.pathExists(root)))
|
|
21
|
+
return [];
|
|
22
|
+
const exts = pickExtensions(opts.format);
|
|
23
|
+
const ignoreSet = new Set(opts.ignore ?? []);
|
|
24
|
+
const hits = await walkForExtensions(root, exts, ignoreSet);
|
|
25
|
+
if (!opts.changedOnly) {
|
|
26
|
+
return hits.map(absPath => toDiscoveredFile(absPath, root));
|
|
27
|
+
}
|
|
28
|
+
const stored = await loadHashes(customer.idn);
|
|
29
|
+
const changed = [];
|
|
30
|
+
for (const absPath of hits) {
|
|
31
|
+
const current = sha256(await fs.readFile(absPath, 'utf8'));
|
|
32
|
+
if (stored[absPath] !== current) {
|
|
33
|
+
changed.push(toDiscoveredFile(absPath, root));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return changed;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Discover files under an arbitrary directory.
|
|
40
|
+
* Used when the user passes explicit paths to `newo lint some/dir`.
|
|
41
|
+
*/
|
|
42
|
+
export async function discoverFromPath(inputPath, opts = {}) {
|
|
43
|
+
const abs = path.resolve(inputPath);
|
|
44
|
+
if (!(await fs.pathExists(abs)))
|
|
45
|
+
return [];
|
|
46
|
+
const exts = pickExtensions(opts.format);
|
|
47
|
+
const ignoreSet = new Set(opts.ignore ?? []);
|
|
48
|
+
const stat = await fs.stat(abs);
|
|
49
|
+
let hits;
|
|
50
|
+
if (stat.isFile()) {
|
|
51
|
+
hits = exts.includes(path.extname(abs)) ? [abs] : [];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
hits = await walkForExtensions(abs, exts, ignoreSet);
|
|
55
|
+
}
|
|
56
|
+
const root = stat.isDirectory() ? abs : path.dirname(abs);
|
|
57
|
+
return hits.map(p => toDiscoveredFile(p, root));
|
|
58
|
+
}
|
|
59
|
+
function pickExtensions(format) {
|
|
60
|
+
if (!format)
|
|
61
|
+
return [...ALL_SCRIPT_EXTENSIONS];
|
|
62
|
+
const map = format === 'newo_v2' ? NEWO_V2_EXTENSIONS : CLI_V1_EXTENSIONS;
|
|
63
|
+
return Object.values(map);
|
|
64
|
+
}
|
|
65
|
+
function toDiscoveredFile(absPath, root) {
|
|
66
|
+
return {
|
|
67
|
+
absPath,
|
|
68
|
+
relPath: path.relative(root, absPath),
|
|
69
|
+
ext: path.extname(absPath),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function walkForExtensions(dir, exts, ignore) {
|
|
73
|
+
const out = [];
|
|
74
|
+
const stack = [dir];
|
|
75
|
+
while (stack.length > 0) {
|
|
76
|
+
const current = stack.pop();
|
|
77
|
+
if (ignore.has(current))
|
|
78
|
+
continue;
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
// Skip hidden dirs, node_modules, and the .newo state directory.
|
|
88
|
+
if (entry.name.startsWith('.'))
|
|
89
|
+
continue;
|
|
90
|
+
if (entry.name === 'node_modules')
|
|
91
|
+
continue;
|
|
92
|
+
const full = path.join(current, entry.name);
|
|
93
|
+
if (ignore.has(full))
|
|
94
|
+
continue;
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
stack.push(full);
|
|
97
|
+
}
|
|
98
|
+
else if (exts.includes(path.extname(entry.name))) {
|
|
99
|
+
out.push(full);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out.sort();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Default root for lint invocations with no path arguments - the
|
|
107
|
+
* `newo_customers/` directory at the cwd.
|
|
108
|
+
*/
|
|
109
|
+
export function defaultRoot() {
|
|
110
|
+
return NEWO_CUSTOMERS_DIR;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CustomerConfig } from '../types.js';
|
|
2
|
+
export interface LiveSchemaSnapshot {
|
|
3
|
+
actions: Array<{
|
|
4
|
+
name: string;
|
|
5
|
+
[k: string]: unknown;
|
|
6
|
+
}>;
|
|
7
|
+
fetchedAt: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function liveSchemaCachePath(customerIdn: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Fetch the current action catalog from NEWO and cache it.
|
|
12
|
+
* Returns a snapshot ready to pass to `createLinter`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function refreshLiveSchema(customer: CustomerConfig): Promise<LiveSchemaSnapshot>;
|
|
15
|
+
/**
|
|
16
|
+
* Load the cached snapshot if present. Returns null when the cache is
|
|
17
|
+
* missing or corrupt (caller should fall back to bundled schemas).
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadCachedLiveSchema(customerIdn: string): Promise<LiveSchemaSnapshot | null>;
|
|
20
|
+
//# sourceMappingURL=live-schema.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live schema refresh: hits `/api/v1/script/actions` via the existing
|
|
3
|
+
* NEWO api client, caches the response to `.newo/{customer}/actions.json`,
|
|
4
|
+
* and returns an object shaped for `createLinter({ schemas: { kind: 'inline', ... }})`.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { customerStateDir } from '../fsutil.js';
|
|
9
|
+
import { getValidAccessToken } from '../auth.js';
|
|
10
|
+
import { makeClient, getScriptActions } from '../api.js';
|
|
11
|
+
export async function liveSchemaCachePath(customerIdn) {
|
|
12
|
+
const dir = customerStateDir(customerIdn);
|
|
13
|
+
await fs.ensureDir(dir);
|
|
14
|
+
return path.join(dir, 'actions.json');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Fetch the current action catalog from NEWO and cache it.
|
|
18
|
+
* Returns a snapshot ready to pass to `createLinter`.
|
|
19
|
+
*/
|
|
20
|
+
export async function refreshLiveSchema(customer) {
|
|
21
|
+
const token = await getValidAccessToken(customer);
|
|
22
|
+
const client = await makeClient(false, token);
|
|
23
|
+
const actions = await getScriptActions(client);
|
|
24
|
+
const snapshot = {
|
|
25
|
+
actions: actions.map((a) => ({
|
|
26
|
+
name: a.idn ?? a.title,
|
|
27
|
+
title: a.title,
|
|
28
|
+
...(a.idn !== undefined ? { idn: a.idn } : {}),
|
|
29
|
+
arguments: a.arguments,
|
|
30
|
+
})),
|
|
31
|
+
fetchedAt: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
const cachePath = await liveSchemaCachePath(customer.idn);
|
|
34
|
+
await fs.writeJson(cachePath, snapshot, { spaces: 2 });
|
|
35
|
+
return snapshot;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load the cached snapshot if present. Returns null when the cache is
|
|
39
|
+
* missing or corrupt (caller should fall back to bundled schemas).
|
|
40
|
+
*/
|
|
41
|
+
export async function loadCachedLiveSchema(customerIdn) {
|
|
42
|
+
const cachePath = await liveSchemaCachePath(customerIdn);
|
|
43
|
+
if (!(await fs.pathExists(cachePath)))
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
return (await fs.readJson(cachePath));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=live-schema.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { textReporter } from './text.js';
|
|
2
|
+
import { jsonReporter } from './json.js';
|
|
3
|
+
import { sarifReporter } from './sarif.js';
|
|
4
|
+
export function pickReporter(name) {
|
|
5
|
+
switch (name) {
|
|
6
|
+
case 'json':
|
|
7
|
+
return jsonReporter;
|
|
8
|
+
case 'sarif':
|
|
9
|
+
return sarifReporter;
|
|
10
|
+
case 'text':
|
|
11
|
+
case undefined:
|
|
12
|
+
case '':
|
|
13
|
+
return textReporter;
|
|
14
|
+
default:
|
|
15
|
+
console.warn(`Unknown --format value '${name}', defaulting to text.`);
|
|
16
|
+
return textReporter;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const sarifReporter = {
|
|
2
|
+
write(report) {
|
|
3
|
+
const sarif = {
|
|
4
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
5
|
+
version: '2.1.0',
|
|
6
|
+
runs: [
|
|
7
|
+
{
|
|
8
|
+
tool: {
|
|
9
|
+
driver: {
|
|
10
|
+
name: 'newo-lint',
|
|
11
|
+
informationUri: 'https://github.com/sabbah13/newo-cli',
|
|
12
|
+
rules: [],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
results: report.results.flatMap(r => r.diagnostics.map(d => buildResult(r.filePath, d))),
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
return JSON.stringify(sarif, null, 2);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
function buildResult(filePath, d) {
|
|
23
|
+
return {
|
|
24
|
+
ruleId: d.code,
|
|
25
|
+
level: d.severity === 'error' ? 'error' : d.severity === 'warning' ? 'warning' : 'note',
|
|
26
|
+
message: { text: d.message },
|
|
27
|
+
locations: [
|
|
28
|
+
{
|
|
29
|
+
physicalLocation: {
|
|
30
|
+
artifactLocation: { uri: toUri(filePath) },
|
|
31
|
+
region: {
|
|
32
|
+
startLine: d.range.start.line,
|
|
33
|
+
startColumn: d.range.start.column,
|
|
34
|
+
endLine: d.range.end.line,
|
|
35
|
+
endColumn: d.range.end.column,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function toUri(absPath) {
|
|
43
|
+
// SARIF artifact URIs should be workspace-relative when possible.
|
|
44
|
+
const rel = absPath.replace(process.cwd() + '/', '');
|
|
45
|
+
return rel.replace(/\\/g, '/');
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=sarif.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable terminal reporter. Mirrors the ESLint 'stylish' layout:
|
|
3
|
+
*
|
|
4
|
+
* path/to/file.jinja
|
|
5
|
+
* 12:5 error Unknown skill: foo. Did you mean: bar? E100
|
|
6
|
+
* ...
|
|
7
|
+
*
|
|
8
|
+
* 2 problems (1 error, 1 warning)
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
const RED = '\x1b[31m';
|
|
12
|
+
const YELLOW = '\x1b[33m';
|
|
13
|
+
const CYAN = '\x1b[36m';
|
|
14
|
+
const GREY = '\x1b[90m';
|
|
15
|
+
const RESET = '\x1b[0m';
|
|
16
|
+
const BOLD = '\x1b[1m';
|
|
17
|
+
export const textReporter = {
|
|
18
|
+
write(report) {
|
|
19
|
+
const lines = [];
|
|
20
|
+
const filesWithIssues = report.results.filter(r => r.diagnostics.length > 0);
|
|
21
|
+
for (const result of filesWithIssues) {
|
|
22
|
+
lines.push(renderFile(result));
|
|
23
|
+
lines.push('');
|
|
24
|
+
}
|
|
25
|
+
lines.push(renderSummary(report));
|
|
26
|
+
return lines.join('\n');
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
function renderFile(result) {
|
|
30
|
+
const rel = path.relative(process.cwd(), result.filePath);
|
|
31
|
+
const header = `${BOLD}${CYAN}${rel}${RESET}`;
|
|
32
|
+
const rows = result.diagnostics.map(d => {
|
|
33
|
+
const loc = `${d.range.start.line}:${d.range.start.column}`;
|
|
34
|
+
const sev = d.severity === 'error'
|
|
35
|
+
? `${RED}error${RESET}`
|
|
36
|
+
: d.severity === 'warning'
|
|
37
|
+
? `${YELLOW}warning${RESET}`
|
|
38
|
+
: `${GREY}${d.severity}${RESET}`;
|
|
39
|
+
return ` ${loc.padEnd(7)} ${sev.padEnd(16)} ${d.message} ${GREY}${d.code}${RESET}`;
|
|
40
|
+
});
|
|
41
|
+
return [header, ...rows].join('\n');
|
|
42
|
+
}
|
|
43
|
+
function renderSummary(report) {
|
|
44
|
+
const total = report.errorCount + report.warningCount;
|
|
45
|
+
if (total === 0) {
|
|
46
|
+
return `${GREY}No issues found.${RESET}`;
|
|
47
|
+
}
|
|
48
|
+
const color = report.errorCount > 0 ? RED : YELLOW;
|
|
49
|
+
return `${color}${BOLD}${total} problems${RESET} (${report.errorCount} error${report.errorCount === 1 ? '' : 's'}, ${report.warningCount} warning${report.warningCount === 1 ? '' : 's'})`;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=text.js.map
|
package/dist/sync/attributes.js
CHANGED
|
@@ -6,6 +6,7 @@ import { writeFileSafe, customerAttributesPath, customerAttributesMapPath, custo
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
8
|
import yaml from 'js-yaml';
|
|
9
|
+
import { patchYamlToPyyaml } from '../format/yaml-patch.js';
|
|
9
10
|
/**
|
|
10
11
|
* Save customer attributes to YAML format and return content for hashing
|
|
11
12
|
*/
|
|
@@ -55,23 +56,22 @@ export async function saveCustomerAttributes(client, customer, verbose = false)
|
|
|
55
56
|
const attributesYaml = {
|
|
56
57
|
attributes: cleanAttributes
|
|
57
58
|
};
|
|
58
|
-
//
|
|
59
|
+
// Emit YAML without folding/wrapping; patchYamlToPyyaml handles long-line
|
|
60
|
+
// wrapping and converts JSON-like double-quoted values to single-quoted
|
|
61
|
+
// (so strings containing `"` stay valid YAML on reload).
|
|
59
62
|
let yamlContent = yaml.dump(attributesYaml, {
|
|
60
63
|
indent: 2,
|
|
61
64
|
quotingType: '"',
|
|
62
65
|
forceQuotes: false,
|
|
63
|
-
lineWidth:
|
|
66
|
+
lineWidth: -1,
|
|
64
67
|
noRefs: true,
|
|
65
68
|
sortKeys: false,
|
|
66
|
-
flowLevel: -1,
|
|
67
|
-
styles: {
|
|
68
|
-
'!!str': 'folded' // Use folded style for better line wrapping of long strings
|
|
69
|
-
}
|
|
69
|
+
flowLevel: -1,
|
|
70
70
|
});
|
|
71
|
-
// Post-process to fix enum format
|
|
71
|
+
// Post-process to fix enum format
|
|
72
72
|
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
|
|
73
|
-
//
|
|
74
|
-
yamlContent = yamlContent
|
|
73
|
+
// Convert JSON-like double-quoted values to single-quoted and wrap long lines
|
|
74
|
+
yamlContent = patchYamlToPyyaml(yamlContent);
|
|
75
75
|
// Save all files: attributes.yaml, ID mapping, and backup for diff tracking
|
|
76
76
|
await writeFileSafe(customerAttributesPath(customer.idn), yamlContent);
|
|
77
77
|
await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
|
|
@@ -139,19 +139,19 @@ export async function saveProjectAttributes(client, customer, projectId, project
|
|
|
139
139
|
const attributesYaml = {
|
|
140
140
|
attributes: cleanAttributes
|
|
141
141
|
};
|
|
142
|
-
//
|
|
142
|
+
// Emit YAML without folding/wrapping; patchYamlToPyyaml handles long-line
|
|
143
|
+
// wrapping and converts JSON-like double-quoted values to single-quoted.
|
|
143
144
|
let yamlContent = yaml.dump(attributesYaml, {
|
|
144
145
|
indent: 2,
|
|
145
146
|
quotingType: '"',
|
|
146
147
|
forceQuotes: false,
|
|
147
|
-
lineWidth:
|
|
148
|
+
lineWidth: -1,
|
|
148
149
|
noRefs: true,
|
|
149
150
|
sortKeys: false,
|
|
150
|
-
flowLevel: -1
|
|
151
|
+
flowLevel: -1,
|
|
151
152
|
});
|
|
152
|
-
// Post-process to fix enum format
|
|
153
153
|
yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
|
|
154
|
-
yamlContent = yamlContent
|
|
154
|
+
yamlContent = patchYamlToPyyaml(yamlContent);
|
|
155
155
|
// Save to project directory
|
|
156
156
|
const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
|
|
157
157
|
const projectDir = path.join(customerDir, 'projects', projectIdn);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AxiosInstance } from 'axios';
|
|
2
2
|
import type { CustomerConfig, ConversationOptions } from '../types.js';
|
|
3
3
|
/**
|
|
4
|
-
* Pull conversations for a customer and save
|
|
4
|
+
* Pull conversations for a customer and save incrementally.
|
|
5
5
|
*/
|
|
6
6
|
export declare function pullConversations(client: AxiosInstance, customer: CustomerConfig, options?: ConversationOptions, verbose?: boolean): Promise<void>;
|
|
7
7
|
//# sourceMappingURL=conversations.d.ts.map
|