newo 3.6.2 → 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 (44) hide show
  1. package/CHANGELOG.md +37 -3
  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/lint/config.d.ts +4 -0
  13. package/dist/lint/config.js +14 -0
  14. package/dist/lint/discovery.d.ts +34 -0
  15. package/dist/lint/discovery.js +112 -0
  16. package/dist/lint/live-schema.d.ts +20 -0
  17. package/dist/lint/live-schema.js +52 -0
  18. package/dist/lint/reporters/index.d.ts +4 -0
  19. package/dist/lint/reporters/index.js +19 -0
  20. package/dist/lint/reporters/json.d.ts +3 -0
  21. package/dist/lint/reporters/json.js +6 -0
  22. package/dist/lint/reporters/sarif.d.ts +3 -0
  23. package/dist/lint/reporters/sarif.js +47 -0
  24. package/dist/lint/reporters/text.d.ts +3 -0
  25. package/dist/lint/reporters/text.js +51 -0
  26. package/dist/lint/reporters/types.d.ts +6 -0
  27. package/dist/lint/reporters/types.js +2 -0
  28. package/dist/sync/conversations.d.ts +1 -1
  29. package/dist/sync/conversations.js +240 -193
  30. package/package.json +3 -1
  31. package/src/cli/commands/check.ts +21 -0
  32. package/src/cli/commands/format.ts +131 -0
  33. package/src/cli/commands/help.ts +13 -0
  34. package/src/cli/commands/lint.ts +246 -0
  35. package/src/cli.ts +50 -9
  36. package/src/lint/config.ts +17 -0
  37. package/src/lint/discovery.ts +148 -0
  38. package/src/lint/live-schema.ts +62 -0
  39. package/src/lint/reporters/index.ts +22 -0
  40. package/src/lint/reporters/json.ts +12 -0
  41. package/src/lint/reporters/sarif.ts +59 -0
  42. package/src/lint/reporters/text.ts +58 -0
  43. package/src/lint/reporters/types.ts +7 -0
  44. package/src/sync/conversations.ts +265 -212
@@ -1,218 +1,265 @@
1
1
  /**
2
2
  * Conversations synchronization module
3
+ *
4
+ * Incremental/resumable conversation pull:
5
+ * - Writes per-persona JSON files to newo_customers/<idn>/conversations/<persona_id>.json as they arrive
6
+ * - Updates conversations.yaml aggregate after each persona finishes
7
+ * - Skips personas already fully fetched (resume support) unless --force passed via env NEWO_CONV_FORCE=1
8
+ * - Graceful on partial failure: individual persona errors do not abort the batch, state is preserved
3
9
  */
4
10
  import { listUserPersonas, getChatHistory } from '../api.js';
5
11
  import { writeFileSafe } from '../fsutil.js';
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
6
14
  import yaml from 'js-yaml';
7
15
  import pLimit from 'p-limit';
8
- // Concurrency limit for API calls
9
16
  const concurrencyLimit = pLimit(5);
10
- /**
11
- * Pull conversations for a customer and save to YAML
12
- */
13
- export async function pullConversations(client, customer, options = {}, verbose = false) {
14
- if (verbose)
15
- console.log(`💬 Fetching conversations for customer ${customer.idn}...`);
17
+ function personaFilePath(customerIdn, personaId) {
18
+ return path.join('newo_customers', customerIdn, 'conversations', `${personaId}.json`);
19
+ }
20
+ function aggregateYamlPath(customerIdn) {
21
+ return path.join('newo_customers', customerIdn, 'conversations.yaml');
22
+ }
23
+ async function readPersonaState(customerIdn, personaId) {
24
+ const p = personaFilePath(customerIdn, personaId);
25
+ if (!(await fs.pathExists(p)))
26
+ return null;
16
27
  try {
17
- // Get all user personas with pagination
18
- const allPersonas = [];
19
- let page = 1;
20
- const perPage = 50;
21
- let hasMore = true;
22
- while (hasMore) {
23
- const response = await listUserPersonas(client, page, perPage);
24
- allPersonas.push(...response.items);
25
- if (verbose)
26
- console.log(`📋 Page ${page}: Found ${response.items.length} personas (${allPersonas.length}/${response.metadata.total} total)`);
27
- hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
28
- page++;
28
+ return JSON.parse(await fs.readFile(p, 'utf8'));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ async function writePersonaState(customerIdn, state) {
35
+ await writeFileSafe(personaFilePath(customerIdn, state.id), JSON.stringify(state, null, 2));
36
+ }
37
+ async function writeAggregateYaml(customerIdn) {
38
+ const dir = path.join('newo_customers', customerIdn, 'conversations');
39
+ const files = (await fs.pathExists(dir)) ? await fs.readdir(dir) : [];
40
+ const personas = [];
41
+ for (const f of files) {
42
+ if (!f.endsWith('.json'))
43
+ continue;
44
+ try {
45
+ const state = JSON.parse(await fs.readFile(path.join(dir, f), 'utf8'));
46
+ personas.push({
47
+ id: state.id,
48
+ name: state.name,
49
+ phone: state.phone,
50
+ act_count: state.act_count,
51
+ acts: state.acts
52
+ });
29
53
  }
30
- if (options.maxPersonas && allPersonas.length > options.maxPersonas) {
31
- allPersonas.splice(options.maxPersonas);
32
- if (verbose)
33
- console.log(`⚠️ Limited to ${options.maxPersonas} personas as requested`);
54
+ catch {
55
+ // skip corrupted file
34
56
  }
57
+ }
58
+ personas.sort((a, b) => {
59
+ const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1].datetime : '1970-01-01T00:00:00.000Z';
60
+ const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1].datetime : '1970-01-01T00:00:00.000Z';
61
+ return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
62
+ });
63
+ const totalActs = personas.reduce((sum, p) => sum + p.acts.length, 0);
64
+ const data = {
65
+ personas,
66
+ total_personas: personas.length,
67
+ total_acts: totalActs,
68
+ generated_at: new Date().toISOString()
69
+ };
70
+ const yamlContent = yaml.dump(data, {
71
+ indent: 2,
72
+ quotingType: '"',
73
+ forceQuotes: false,
74
+ lineWidth: 120,
75
+ noRefs: true,
76
+ sortKeys: false,
77
+ flowLevel: -1
78
+ });
79
+ await writeFileSafe(aggregateYamlPath(customerIdn), yamlContent);
80
+ return { personas: personas.length, acts: totalActs };
81
+ }
82
+ function buildProcessedActs(raw) {
83
+ const sorted = [...raw].sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
84
+ return sorted.map(act => {
85
+ const processedAct = {
86
+ datetime: act.datetime,
87
+ type: act.reference_idn,
88
+ message: act.source_text
89
+ };
90
+ if (act.contact_information)
91
+ processedAct.contact_information = act.contact_information;
92
+ if (act.flow_idn && act.flow_idn !== 'unknown')
93
+ processedAct.flow_idn = act.flow_idn;
94
+ if (act.skill_idn && act.skill_idn !== 'unknown')
95
+ processedAct.skill_idn = act.skill_idn;
96
+ if (act.session_id && act.session_id !== 'unknown')
97
+ processedAct.session_id = act.session_id;
98
+ return processedAct;
99
+ });
100
+ }
101
+ /**
102
+ * Pull conversations for a customer and save incrementally.
103
+ */
104
+ export async function pullConversations(client, customer, options = {}, verbose = false) {
105
+ const force = process.env.NEWO_CONV_FORCE === '1';
106
+ console.log(`💬 Fetching conversations for ${customer.idn}${force ? ' (force re-fetch)' : ' (resume mode)'}...`);
107
+ // Ensure output dirs exist
108
+ await fs.ensureDir(path.join('newo_customers', customer.idn, 'conversations'));
109
+ // 1. Enumerate all personas
110
+ const allPersonas = [];
111
+ let page = 1;
112
+ const perPage = 50;
113
+ let hasMore = true;
114
+ while (hasMore) {
115
+ const response = await listUserPersonas(client, page, perPage);
116
+ allPersonas.push(...response.items);
35
117
  if (verbose)
36
- console.log(`👥 Processing ${allPersonas.length} personas...`);
37
- // Process personas concurrently with limited concurrency
38
- const processedPersonas = [];
39
- await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
40
- try {
41
- // Extract phone number from actors
42
- const phoneActor = persona.actors.find(actor => actor.integration_idn === 'newo_voice' &&
43
- actor.connector_idn === 'newo_voice_connector' &&
44
- actor.contact_information?.startsWith('+'));
45
- const phone = phoneActor?.contact_information || null;
46
- // Get acts for this persona
47
- const allActs = [];
48
- let actPage = 1;
49
- const actsPerPage = 100; // Higher limit for acts
50
- let hasMoreActs = true;
51
- // Get user actor IDs from persona actors first
52
- const userActors = persona.actors.filter(actor => actor.integration_idn === 'newo_voice' &&
53
- actor.connector_idn === 'newo_voice_connector');
54
- if (userActors.length === 0) {
55
- if (verbose)
56
- console.log(` 👤 ${persona.name}: No voice actors found, skipping`);
57
- // No voice actors, can't get chat history - add persona with empty acts
58
- processedPersonas.push({
59
- id: persona.id,
60
- name: persona.name,
61
- phone,
62
- act_count: persona.act_count,
63
- acts: []
118
+ console.log(`📋 Page ${page}: ${response.items.length} personas (${allPersonas.length}/${response.metadata.total})`);
119
+ hasMore = response.items.length === perPage && allPersonas.length < response.metadata.total;
120
+ page++;
121
+ }
122
+ if (options.maxPersonas && allPersonas.length > options.maxPersonas) {
123
+ allPersonas.splice(options.maxPersonas);
124
+ }
125
+ const total = allPersonas.length;
126
+ console.log(`👥 Found ${total} personas. Processing with concurrency=5...`);
127
+ let done = 0;
128
+ let skipped = 0;
129
+ let failed = 0;
130
+ await Promise.all(allPersonas.map(persona => concurrencyLimit(async () => {
131
+ try {
132
+ // Resume: skip if already complete
133
+ const existing = await readPersonaState(customer.idn, persona.id);
134
+ if (!force && existing && existing.complete) {
135
+ skipped++;
136
+ done++;
137
+ if (verbose)
138
+ console.log(`⏭️ [${done}/${total}] ${persona.name}: already complete (${existing.acts.length} acts)`);
139
+ return;
140
+ }
141
+ const phoneActor = persona.actors.find(actor => actor.integration_idn === 'newo_voice' &&
142
+ actor.connector_idn === 'newo_voice_connector' &&
143
+ actor.contact_information?.startsWith('+'));
144
+ const phone = phoneActor?.contact_information || null;
145
+ const userActors = persona.actors.filter(actor => actor.integration_idn === 'newo_voice' &&
146
+ actor.connector_idn === 'newo_voice_connector');
147
+ if (userActors.length === 0) {
148
+ const state = {
149
+ id: persona.id,
150
+ name: persona.name,
151
+ phone,
152
+ act_count: persona.act_count,
153
+ acts: [],
154
+ fetched_at: new Date().toISOString(),
155
+ complete: true
156
+ };
157
+ await writePersonaState(customer.idn, state);
158
+ done++;
159
+ if (verbose)
160
+ console.log(`✓ [${done}/${total}] ${persona.name}: no voice actors`);
161
+ return;
162
+ }
163
+ // Fetch acts paginated
164
+ const allActs = [];
165
+ let actPage = 1;
166
+ const actsPerPage = 100;
167
+ let hasMoreActs = true;
168
+ const maxPages = 50;
169
+ let lastError;
170
+ while (hasMoreActs && actPage <= maxPages) {
171
+ try {
172
+ const chatResponse = await getChatHistory(client, {
173
+ user_actor_id: userActors[0].id,
174
+ page: actPage,
175
+ per: actsPerPage
64
176
  });
65
- if (verbose)
66
- console.log(` ✓ Processed ${persona.name}: 0 acts (no voice actors)`);
67
- return; // Return from the concurrency function
68
- }
69
- // Safety mechanism to prevent infinite loops
70
- const maxPages = 50; // Limit to 50 pages (5000 acts max per persona)
71
- while (hasMoreActs && actPage <= maxPages) {
72
- try {
73
- const chatHistoryParams = {
177
+ if (chatResponse.items && chatResponse.items.length > 0) {
178
+ const convertedActs = chatResponse.items.map((item) => ({
179
+ id: item.id || `chat_${Math.random()}`,
180
+ command_act_id: null,
181
+ external_event_id: item.external_event_id || 'chat_history',
182
+ arguments: [],
183
+ reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
184
+ runtime_context_id: item.runtime_context_id || 'chat_history',
185
+ source_text: item.payload?.text || item.message || item.content || item.text || '',
186
+ original_text: item.payload?.text || item.message || item.content || item.text || '',
187
+ datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
74
188
  user_actor_id: userActors[0].id,
75
- page: actPage,
76
- per: actsPerPage
189
+ agent_actor_id: null,
190
+ user_persona_id: persona.id,
191
+ user_persona_name: persona.name,
192
+ agent_persona_id: item.agent_persona_id || 'unknown',
193
+ external_id: item.external_id || null,
194
+ integration_idn: 'newo_voice',
195
+ connector_idn: 'newo_voice_connector',
196
+ to_integration_idn: null,
197
+ to_connector_idn: null,
198
+ is_agent: Boolean(item.is_agent === true),
199
+ project_idn: null,
200
+ flow_idn: item.flow_idn || 'unknown',
201
+ skill_idn: item.skill_idn || 'unknown',
202
+ session_id: item.session_id || 'unknown',
203
+ recordings: item.recordings || [],
204
+ contact_information: item.contact_information || null
205
+ }));
206
+ allActs.push(...convertedActs);
207
+ // Save partial progress every page
208
+ const partialState = {
209
+ id: persona.id,
210
+ name: persona.name,
211
+ phone,
212
+ act_count: persona.act_count,
213
+ acts: buildProcessedActs(allActs),
214
+ fetched_at: new Date().toISOString(),
215
+ complete: false
77
216
  };
78
- if (verbose)
79
- console.log(` 📄 ${persona.name}: Fetching page ${actPage}...`);
80
- const chatResponse = await getChatHistory(client, chatHistoryParams);
81
- if (chatResponse.items && chatResponse.items.length > 0) {
82
- // Convert chat history format to acts format - create minimal ConversationAct objects
83
- const convertedActs = chatResponse.items.map((item) => ({
84
- id: item.id || `chat_${Math.random()}`,
85
- command_act_id: null,
86
- external_event_id: item.external_event_id || 'chat_history',
87
- arguments: [],
88
- reference_idn: (item.is_agent === true) ? 'agent_message' : 'user_message',
89
- runtime_context_id: item.runtime_context_id || 'chat_history',
90
- source_text: item.payload?.text || item.message || item.content || item.text || '',
91
- original_text: item.payload?.text || item.message || item.content || item.text || '',
92
- datetime: item.datetime || item.created_at || item.timestamp || new Date().toISOString(),
93
- user_actor_id: userActors[0].id,
94
- agent_actor_id: null,
95
- user_persona_id: persona.id,
96
- user_persona_name: persona.name,
97
- agent_persona_id: item.agent_persona_id || 'unknown',
98
- external_id: item.external_id || null,
99
- integration_idn: 'newo_voice',
100
- connector_idn: 'newo_voice_connector',
101
- to_integration_idn: null,
102
- to_connector_idn: null,
103
- is_agent: Boolean(item.is_agent === true),
104
- project_idn: null,
105
- flow_idn: item.flow_idn || 'unknown',
106
- skill_idn: item.skill_idn || 'unknown',
107
- session_id: item.session_id || 'unknown',
108
- recordings: item.recordings || [],
109
- contact_information: item.contact_information || null
110
- }));
111
- allActs.push(...convertedActs);
112
- if (verbose && convertedActs.length > 0) {
113
- console.log(` 👤 ${persona.name}: Chat History - ${convertedActs.length} messages (${allActs.length} total)`);
114
- }
115
- // Check if we should continue paginating
116
- const hasMetadata = chatResponse.metadata?.total !== undefined;
117
- const currentTotal = chatResponse.metadata?.total || 0;
118
- hasMoreActs = chatResponse.items.length === actsPerPage &&
119
- hasMetadata &&
120
- allActs.length < currentTotal;
121
- actPage++;
122
- if (verbose)
123
- console.log(` 📊 ${persona.name}: Page ${actPage - 1} done, ${allActs.length}/${currentTotal} total acts`);
124
- }
125
- else {
126
- // No more items
127
- hasMoreActs = false;
128
- if (verbose)
129
- console.log(` 📊 ${persona.name}: No more chat history items`);
130
- }
217
+ await writePersonaState(customer.idn, partialState);
218
+ const currentTotal = chatResponse.metadata?.total || 0;
219
+ hasMoreActs = chatResponse.items.length === actsPerPage && allActs.length < currentTotal;
220
+ actPage++;
131
221
  }
132
- catch (chatError) {
133
- if (verbose)
134
- console.log(` ⚠️ Chat history failed for ${persona.name}: ${chatError instanceof Error ? chatError.message : String(chatError)}`);
222
+ else {
135
223
  hasMoreActs = false;
136
224
  }
137
225
  }
138
- if (actPage > maxPages) {
226
+ catch (chatError) {
227
+ lastError = chatError instanceof Error ? chatError.message : String(chatError);
139
228
  if (verbose)
140
- console.log(` ⚠️ ${persona.name}: Reached max pages limit (${maxPages}), stopping pagination`);
229
+ console.log(`⚠️ ${persona.name} page ${actPage}: ${lastError}`);
230
+ hasMoreActs = false;
141
231
  }
142
- // Sort acts by datetime ascending (chronological order)
143
- allActs.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime());
144
- // Process acts into simplified format - exclude redundant fields
145
- const processedActs = allActs.map(act => {
146
- const processedAct = {
147
- datetime: act.datetime,
148
- type: act.reference_idn,
149
- message: act.source_text
150
- };
151
- // Only include non-redundant fields
152
- if (act.contact_information) {
153
- processedAct.contact_information = act.contact_information;
154
- }
155
- if (act.flow_idn && act.flow_idn !== 'unknown') {
156
- processedAct.flow_idn = act.flow_idn;
157
- }
158
- if (act.skill_idn && act.skill_idn !== 'unknown') {
159
- processedAct.skill_idn = act.skill_idn;
160
- }
161
- if (act.session_id && act.session_id !== 'unknown') {
162
- processedAct.session_id = act.session_id;
163
- }
164
- return processedAct;
165
- });
166
- processedPersonas.push({
167
- id: persona.id,
168
- name: persona.name,
169
- phone,
170
- act_count: persona.act_count,
171
- acts: processedActs
172
- });
173
- if (verbose)
174
- console.log(` ✓ Processed ${persona.name}: ${processedActs.length} acts`);
175
232
  }
176
- catch (error) {
177
- console.error(`❌ Failed to process persona ${persona.name}:`, error);
178
- // Continue with other personas
179
- }
180
- })));
181
- // Sort personas by most recent act time (descending) - use latest act from acts array
182
- processedPersonas.sort((a, b) => {
183
- const aLatestTime = a.acts.length > 0 ? a.acts[a.acts.length - 1].datetime : '1970-01-01T00:00:00.000Z';
184
- const bLatestTime = b.acts.length > 0 ? b.acts[b.acts.length - 1].datetime : '1970-01-01T00:00:00.000Z';
185
- return new Date(bLatestTime).getTime() - new Date(aLatestTime).getTime();
186
- });
187
- // Calculate totals
188
- const totalActs = processedPersonas.reduce((sum, persona) => sum + persona.acts.length, 0);
189
- // Create final conversations data
190
- const conversationsData = {
191
- personas: processedPersonas,
192
- total_personas: processedPersonas.length,
193
- total_acts: totalActs,
194
- generated_at: new Date().toISOString()
195
- };
196
- // Save to YAML file
197
- const conversationsPath = `newo_customers/${customer.idn}/conversations.yaml`;
198
- const yamlContent = yaml.dump(conversationsData, {
199
- indent: 2,
200
- quotingType: '"',
201
- forceQuotes: false,
202
- lineWidth: 120,
203
- noRefs: true,
204
- sortKeys: false,
205
- flowLevel: -1
206
- });
207
- await writeFileSafe(conversationsPath, yamlContent);
208
- if (verbose) {
209
- console.log(`✓ Saved conversations to ${conversationsPath}`);
210
- console.log(`📊 Summary: ${processedPersonas.length} personas, ${totalActs} total acts`);
233
+ const finalState = {
234
+ id: persona.id,
235
+ name: persona.name,
236
+ phone,
237
+ act_count: persona.act_count,
238
+ acts: buildProcessedActs(allActs),
239
+ fetched_at: new Date().toISOString(),
240
+ complete: !lastError
241
+ };
242
+ if (lastError)
243
+ finalState.last_error = lastError;
244
+ await writePersonaState(customer.idn, finalState);
245
+ // Incremental YAML aggregate every persona
246
+ const agg = await writeAggregateYaml(customer.idn);
247
+ done++;
248
+ if (lastError)
249
+ failed++;
250
+ console.log(`${lastError ? '⚠️ ' : '✓'} [${done}/${total}] ${persona.name}: ${finalState.acts.length} acts${lastError ? ` (partial: ${lastError})` : ''} | total so far: ${agg.personas} personas / ${agg.acts} acts`);
211
251
  }
212
- }
213
- catch (error) {
214
- console.error(`❌ Failed to pull conversations for ${customer.idn}:`, error);
215
- throw error;
216
- }
252
+ catch (error) {
253
+ failed++;
254
+ done++;
255
+ const msg = error instanceof Error ? error.message : String(error);
256
+ console.error(`❌ [${done}/${total}] ${persona.name}: ${msg}`);
257
+ }
258
+ })));
259
+ // Final aggregate write
260
+ const final = await writeAggregateYaml(customer.idn);
261
+ console.log(`\n✅ Done. ${final.personas} personas, ${final.acts} acts. Skipped ${skipped} (already cached), ${failed} had errors.`);
262
+ console.log(` Aggregate: ${aggregateYamlPath(customer.idn)}`);
263
+ console.log(` Per-persona: newo_customers/${customer.idn}/conversations/<id>.json`);
217
264
  }
218
265
  //# sourceMappingURL=conversations.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newo",
3
- "version": "3.6.2",
3
+ "version": "3.7.0",
4
4
  "description": "NEWO CLI: Professional command-line tool with modular architecture for NEWO AI Agent development. Features account migration, integration management, webhook automation, AKB knowledge base, project attributes, sandbox testing, IDN-based file management, real-time progress tracking, intelligent sync operations, and comprehensive multi-customer support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,6 +60,8 @@
60
60
  "node": ">=18"
61
61
  },
62
62
  "dependencies": {
63
+ "newo-dsl-analyzer": "^1.0.0",
64
+ "newo-dsl-core": "^1.0.0",
63
65
  "axios": "^1.7.7",
64
66
  "chokidar": "^5.0.0",
65
67
  "dotenv": "^16.4.5",
@@ -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
+ }
@@ -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.)