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.
Files changed (48) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +61 -0
  3. package/dist/cli/commands/check.d.ts +3 -0
  4. package/dist/cli/commands/check.js +15 -0
  5. package/dist/cli/commands/format.d.ts +3 -0
  6. package/dist/cli/commands/format.js +105 -0
  7. package/dist/cli/commands/help.js +13 -0
  8. package/dist/cli/commands/lint.d.ts +3 -0
  9. package/dist/cli/commands/lint.js +195 -0
  10. package/dist/cli-new/di/tokens.d.ts +1 -1
  11. package/dist/cli.js +45 -9
  12. package/dist/domain/strategies/sync/AttributeSyncStrategy.js +7 -4
  13. package/dist/lint/config.d.ts +4 -0
  14. package/dist/lint/config.js +14 -0
  15. package/dist/lint/discovery.d.ts +34 -0
  16. package/dist/lint/discovery.js +112 -0
  17. package/dist/lint/live-schema.d.ts +20 -0
  18. package/dist/lint/live-schema.js +52 -0
  19. package/dist/lint/reporters/index.d.ts +4 -0
  20. package/dist/lint/reporters/index.js +19 -0
  21. package/dist/lint/reporters/json.d.ts +3 -0
  22. package/dist/lint/reporters/json.js +6 -0
  23. package/dist/lint/reporters/sarif.d.ts +3 -0
  24. package/dist/lint/reporters/sarif.js +47 -0
  25. package/dist/lint/reporters/text.d.ts +3 -0
  26. package/dist/lint/reporters/text.js +51 -0
  27. package/dist/lint/reporters/types.d.ts +6 -0
  28. package/dist/lint/reporters/types.js +2 -0
  29. package/dist/sync/attributes.js +14 -14
  30. package/dist/sync/conversations.d.ts +1 -1
  31. package/dist/sync/conversations.js +240 -193
  32. package/package.json +3 -1
  33. package/src/cli/commands/check.ts +21 -0
  34. package/src/cli/commands/format.ts +131 -0
  35. package/src/cli/commands/help.ts +13 -0
  36. package/src/cli/commands/lint.ts +246 -0
  37. package/src/cli.ts +50 -9
  38. package/src/domain/strategies/sync/AttributeSyncStrategy.ts +7 -4
  39. package/src/lint/config.ts +17 -0
  40. package/src/lint/discovery.ts +148 -0
  41. package/src/lint/live-schema.ts +62 -0
  42. package/src/lint/reporters/index.ts +22 -0
  43. package/src/lint/reporters/json.ts +12 -0
  44. package/src/lint/reporters/sarif.ts +59 -0
  45. package/src/lint/reporters/text.ts +58 -0
  46. package/src/lint/reporters/types.ts +7 -0
  47. package/src/sync/attributes.ts +14 -14
  48. 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,4 @@
1
+ import type { Reporter, ReporterName } from './types.js';
2
+ export type { Reporter, ReporterName } from './types.js';
3
+ export declare function pickReporter(name: ReporterName | string | undefined): Reporter;
4
+ //# sourceMappingURL=index.d.ts.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,3 @@
1
+ import type { Reporter } from './types.js';
2
+ export declare const jsonReporter: Reporter;
3
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1,6 @@
1
+ export const jsonReporter = {
2
+ write(report) {
3
+ return JSON.stringify(report, null, 2);
4
+ },
5
+ };
6
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1,3 @@
1
+ import type { Reporter } from './types.js';
2
+ export declare const sarifReporter: Reporter;
3
+ //# sourceMappingURL=sarif.d.ts.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,3 @@
1
+ import type { Reporter } from './types.js';
2
+ export declare const textReporter: Reporter;
3
+ //# sourceMappingURL=text.d.ts.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
@@ -0,0 +1,6 @@
1
+ import type { ProjectLintReport } from 'newo-dsl-analyzer';
2
+ export type ReporterName = 'text' | 'json' | 'sarif';
3
+ export interface Reporter {
4
+ write(report: ProjectLintReport): string;
5
+ }
6
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
- // Configure YAML output to match reference format exactly
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: 80, // Wrap long lines to match reference format
66
+ lineWidth: -1,
64
67
  noRefs: true,
65
68
  sortKeys: false,
66
- flowLevel: -1, // Never use flow syntax
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 and improve JSON string formatting
71
+ // Post-process to fix enum format
72
72
  yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
73
- // Fix JSON string formatting to match reference (remove escape characters)
74
- yamlContent = yamlContent.replace(/\\"/g, '"');
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
- // Configure YAML output
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: 80,
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.replace(/\\"/g, '"');
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 to YAML
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