newo 1.7.0 ā 1.7.2
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 +30 -0
- package/dist/cli.js +150 -49
- package/dist/sync.js +47 -21
- package/package.json +1 -1
- package/src/cli.ts +149 -42
- package/src/sync.ts +47 -23
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.7.2] - 2025-09-15
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **YAML Enum Parsing**: Fixed attributes push check error with `!enum` format
|
|
12
|
+
- Changed from YAML parsing to file stats for change detection
|
|
13
|
+
- Prevents parsing errors with custom enum format in attributes.yaml
|
|
14
|
+
- Maintains functionality while avoiding format conflicts
|
|
15
|
+
|
|
16
|
+
### Enhanced
|
|
17
|
+
- **Status Command Scope**: Extended status checking to include all file types
|
|
18
|
+
- Now tracks `attributes.yaml` files with modification times and sizes
|
|
19
|
+
- Added `flows.yaml` file tracking and statistics
|
|
20
|
+
- Comprehensive file monitoring across entire project structure
|
|
21
|
+
- Better visibility into all managed files
|
|
22
|
+
|
|
23
|
+
## [1.7.1] - 2025-09-15
|
|
24
|
+
|
|
25
|
+
### Enhanced
|
|
26
|
+
- **Multi-Customer Commands**: Improved user experience for multi-customer operations
|
|
27
|
+
- `newo status` now automatically checks all customers when no default is specified
|
|
28
|
+
- `newo push` provides interactive customer selection dialog when multiple customers exist
|
|
29
|
+
- No more error messages for commands that support multi-customer operations
|
|
30
|
+
- Better user guidance with clear options for customer selection
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **Command Flow**: Moved customer selection logic into command-specific handlers
|
|
34
|
+
- Prevents early exit errors for multi-customer operations
|
|
35
|
+
- Each command now handles customer selection appropriately
|
|
36
|
+
- Maintains backward compatibility with single-customer setups
|
|
37
|
+
|
|
8
38
|
## [1.7.0] - 2025-09-15
|
|
9
39
|
|
|
10
40
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -151,45 +151,9 @@ async function main() {
|
|
|
151
151
|
}
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
console.error(`Unknown customer: ${args.customer}`);
|
|
158
|
-
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
|
-
selectedCustomer = customer;
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
// For pull command, try to get default but fall back to all customers if multiple exist
|
|
165
|
-
if (cmd === 'pull') {
|
|
166
|
-
try {
|
|
167
|
-
selectedCustomer = tryGetDefaultCustomer(customerConfig);
|
|
168
|
-
if (!selectedCustomer) {
|
|
169
|
-
// Multiple customers exist with no default, pull from all
|
|
170
|
-
allCustomers = getAllCustomers(customerConfig);
|
|
171
|
-
if (verbose)
|
|
172
|
-
console.log(`š„ No default customer specified, pulling from all ${allCustomers.length} customers`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
catch (error) {
|
|
176
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
-
console.error(message);
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
// For other commands, require explicit customer selection
|
|
183
|
-
try {
|
|
184
|
-
selectedCustomer = getDefaultCustomer(customerConfig);
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
-
console.error(message);
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
154
|
+
// Customer selection logic moved inside command processing to avoid early failures
|
|
155
|
+
if (verbose)
|
|
156
|
+
console.log(`š Command parsed: "${cmd}"`);
|
|
193
157
|
if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
|
|
194
158
|
console.log(`NEWO CLI - Multi-Customer Support
|
|
195
159
|
Usage:
|
|
@@ -201,7 +165,7 @@ Usage:
|
|
|
201
165
|
newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles from file
|
|
202
166
|
|
|
203
167
|
Flags:
|
|
204
|
-
--customer <idn> # specify customer (if not set, uses default or
|
|
168
|
+
--customer <idn> # specify customer (if not set, uses default or interactive selection)
|
|
205
169
|
--verbose, -v # enable detailed logging
|
|
206
170
|
|
|
207
171
|
Environment Variables:
|
|
@@ -219,8 +183,9 @@ Multi-Customer Examples:
|
|
|
219
183
|
# Commands:
|
|
220
184
|
newo pull # Pull from all customers (if no default set)
|
|
221
185
|
newo pull --customer acme # Pull projects for Acme only
|
|
222
|
-
newo
|
|
223
|
-
newo
|
|
186
|
+
newo status # Status for all customers (if no default set)
|
|
187
|
+
newo push # Interactive selection for multiple customers
|
|
188
|
+
newo push --customer globex # Push changes for Globex only
|
|
224
189
|
|
|
225
190
|
File Structure:
|
|
226
191
|
newo_customers/
|
|
@@ -233,7 +198,28 @@ File Structure:
|
|
|
233
198
|
`);
|
|
234
199
|
return;
|
|
235
200
|
}
|
|
201
|
+
if (verbose)
|
|
202
|
+
console.log(`š Starting command processing for: ${cmd}`);
|
|
236
203
|
if (cmd === 'pull') {
|
|
204
|
+
// Handle customer selection for pull command
|
|
205
|
+
if (args.customer) {
|
|
206
|
+
const customer = getCustomer(customerConfig, args.customer);
|
|
207
|
+
if (!customer) {
|
|
208
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
209
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
selectedCustomer = customer;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// Try to get default, fall back to all customers
|
|
216
|
+
selectedCustomer = tryGetDefaultCustomer(customerConfig);
|
|
217
|
+
if (!selectedCustomer) {
|
|
218
|
+
allCustomers = getAllCustomers(customerConfig);
|
|
219
|
+
if (verbose)
|
|
220
|
+
console.log(`š„ No default customer specified, pulling from all ${allCustomers.length} customers`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
237
223
|
if (selectedCustomer) {
|
|
238
224
|
// Single customer pull
|
|
239
225
|
const accessToken = await getValidAccessToken(selectedCustomer);
|
|
@@ -255,7 +241,128 @@ File Structure:
|
|
|
255
241
|
}
|
|
256
242
|
return;
|
|
257
243
|
}
|
|
244
|
+
if (cmd === 'status') {
|
|
245
|
+
// Handle customer selection for status command
|
|
246
|
+
if (args.customer) {
|
|
247
|
+
const customer = getCustomer(customerConfig, args.customer);
|
|
248
|
+
if (!customer) {
|
|
249
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
250
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
selectedCustomer = customer;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Try to get default, fall back to all customers
|
|
257
|
+
selectedCustomer = tryGetDefaultCustomer(customerConfig);
|
|
258
|
+
if (!selectedCustomer) {
|
|
259
|
+
allCustomers = getAllCustomers(customerConfig);
|
|
260
|
+
console.log(`š Checking status for ${allCustomers.length} customers...`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (selectedCustomer) {
|
|
264
|
+
// Single customer status
|
|
265
|
+
await status(selectedCustomer, verbose);
|
|
266
|
+
}
|
|
267
|
+
else if (allCustomers.length > 0) {
|
|
268
|
+
// Multi-customer status
|
|
269
|
+
for (const customer of allCustomers) {
|
|
270
|
+
console.log(`\nš Status for customer: ${customer.idn}`);
|
|
271
|
+
await status(customer, verbose);
|
|
272
|
+
}
|
|
273
|
+
console.log(`\nā
Status check completed for all ${allCustomers.length} customers`);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (cmd === 'push') {
|
|
278
|
+
// Handle customer selection for push command
|
|
279
|
+
if (args.customer) {
|
|
280
|
+
const customer = getCustomer(customerConfig, args.customer);
|
|
281
|
+
if (!customer) {
|
|
282
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
283
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
selectedCustomer = customer;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// Try to get default, provide interactive selection if multiple exist
|
|
290
|
+
selectedCustomer = tryGetDefaultCustomer(customerConfig);
|
|
291
|
+
if (!selectedCustomer) {
|
|
292
|
+
// Multiple customers exist with no default, ask user
|
|
293
|
+
allCustomers = getAllCustomers(customerConfig);
|
|
294
|
+
console.log(`\nš¤ Multiple customers available for push:`);
|
|
295
|
+
allCustomers.forEach((customer, index) => {
|
|
296
|
+
console.log(` ${index + 1}. ${customer.idn}`);
|
|
297
|
+
});
|
|
298
|
+
console.log(` ${allCustomers.length + 1}. All customers`);
|
|
299
|
+
const readline = await import('readline');
|
|
300
|
+
const rl = readline.createInterface({
|
|
301
|
+
input: process.stdin,
|
|
302
|
+
output: process.stdout
|
|
303
|
+
});
|
|
304
|
+
const choice = await new Promise((resolve) => {
|
|
305
|
+
rl.question(`\nSelect customer to push (1-${allCustomers.length + 1}): `, resolve);
|
|
306
|
+
});
|
|
307
|
+
rl.close();
|
|
308
|
+
const choiceNum = parseInt(choice.trim());
|
|
309
|
+
if (choiceNum === allCustomers.length + 1) {
|
|
310
|
+
// User selected "All customers"
|
|
311
|
+
console.log(`š Pushing to all ${allCustomers.length} customers...`);
|
|
312
|
+
}
|
|
313
|
+
else if (choiceNum >= 1 && choiceNum <= allCustomers.length) {
|
|
314
|
+
// User selected specific customer
|
|
315
|
+
selectedCustomer = allCustomers[choiceNum - 1] || null;
|
|
316
|
+
allCustomers = []; // Clear to indicate single customer mode
|
|
317
|
+
if (selectedCustomer) {
|
|
318
|
+
console.log(`š Pushing to customer: ${selectedCustomer.idn}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.error('Invalid choice. Exiting.');
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (selectedCustomer) {
|
|
328
|
+
// Single customer push
|
|
329
|
+
const accessToken = await getValidAccessToken(selectedCustomer);
|
|
330
|
+
const client = await makeClient(verbose, accessToken);
|
|
331
|
+
await pushChanged(client, selectedCustomer, verbose);
|
|
332
|
+
}
|
|
333
|
+
else if (allCustomers.length > 0) {
|
|
334
|
+
// Multi-customer push (user selected "All customers")
|
|
335
|
+
console.log(`š Pushing to ${allCustomers.length} customers...`);
|
|
336
|
+
for (const customer of allCustomers) {
|
|
337
|
+
console.log(`\nš¤ Pushing for customer: ${customer.idn}`);
|
|
338
|
+
const accessToken = await getValidAccessToken(customer);
|
|
339
|
+
const client = await makeClient(verbose, accessToken);
|
|
340
|
+
await pushChanged(client, customer, verbose);
|
|
341
|
+
}
|
|
342
|
+
console.log(`\nā
Push completed for all ${allCustomers.length} customers`);
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
258
346
|
// For all other commands, require a single selected customer
|
|
347
|
+
if (args.customer) {
|
|
348
|
+
const customer = getCustomer(customerConfig, args.customer);
|
|
349
|
+
if (!customer) {
|
|
350
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
351
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
selectedCustomer = customer;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
try {
|
|
358
|
+
selectedCustomer = getDefaultCustomer(customerConfig);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
362
|
+
console.error(message);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
259
366
|
if (!selectedCustomer) {
|
|
260
367
|
console.error('Customer selection required for this command');
|
|
261
368
|
process.exit(1);
|
|
@@ -263,13 +370,7 @@ File Structure:
|
|
|
263
370
|
// Get access token for the selected customer
|
|
264
371
|
const accessToken = await getValidAccessToken(selectedCustomer);
|
|
265
372
|
const client = await makeClient(verbose, accessToken);
|
|
266
|
-
if (cmd === '
|
|
267
|
-
await pushChanged(client, selectedCustomer, verbose);
|
|
268
|
-
}
|
|
269
|
-
else if (cmd === 'status') {
|
|
270
|
-
await status(selectedCustomer, verbose);
|
|
271
|
-
}
|
|
272
|
-
else if (cmd === 'meta') {
|
|
373
|
+
if (cmd === 'meta') {
|
|
273
374
|
if (!selectedCustomer.projectId) {
|
|
274
375
|
console.error(`No project ID configured for customer ${selectedCustomer.idn}`);
|
|
275
376
|
console.error(`Set NEWO_CUSTOMER_${selectedCustomer.idn.toUpperCase()}_PROJECT_ID in your .env file`);
|
package/dist/sync.js
CHANGED
|
@@ -396,27 +396,15 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
396
396
|
if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
|
|
397
397
|
if (verbose)
|
|
398
398
|
console.log('š Checking customer attributes for changes...');
|
|
399
|
-
|
|
399
|
+
// Check file modification time for change detection instead of YAML parsing
|
|
400
|
+
const attributesStats = await fs.stat(attributesFile);
|
|
400
401
|
const idMapping = await fs.readJson(attributesMapFile);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (verbose)
|
|
408
|
-
console.log(`ā ļø Skipping attribute ${attribute.idn} - no ID mapping for push`);
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
// For now, just validate the structure (actual push would require change detection)
|
|
412
|
-
// This ensures the push functionality is ready when change detection is implemented
|
|
413
|
-
if (verbose) {
|
|
414
|
-
console.log(`ā Attribute ${attribute.idn} ready for push (ID: ${attributeId})`);
|
|
415
|
-
}
|
|
416
|
-
attributesPushed++;
|
|
417
|
-
}
|
|
418
|
-
if (verbose)
|
|
419
|
-
console.log(`š Found ${attributesPushed} attributes ready for push operations`);
|
|
402
|
+
// Count attributes by ID mapping instead of parsing YAML (avoids enum parsing issues)
|
|
403
|
+
const attributeCount = Object.keys(idMapping).length;
|
|
404
|
+
if (verbose) {
|
|
405
|
+
console.log(`š Found ${attributeCount} attributes ready for push operations`);
|
|
406
|
+
console.log(`š
Attributes file last modified: ${attributesStats.mtime.toISOString()}`);
|
|
407
|
+
// TODO: Implement change detection by comparing with last push timestamp
|
|
420
408
|
}
|
|
421
409
|
}
|
|
422
410
|
else if (verbose) {
|
|
@@ -424,7 +412,8 @@ export async function pushChanged(client, customer, verbose = false) {
|
|
|
424
412
|
}
|
|
425
413
|
}
|
|
426
414
|
catch (error) {
|
|
427
|
-
|
|
415
|
+
if (verbose)
|
|
416
|
+
console.log(`ā ļø Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
428
417
|
}
|
|
429
418
|
await saveHashes(newHashes, customer.idn);
|
|
430
419
|
console.log(pushed ? `ā
Push complete. ${pushed} file(s) updated.` : 'ā
Nothing to push.');
|
|
@@ -499,6 +488,43 @@ export async function status(customer, verbose = false) {
|
|
|
499
488
|
}
|
|
500
489
|
}
|
|
501
490
|
}
|
|
491
|
+
// Check attributes file for changes
|
|
492
|
+
try {
|
|
493
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
494
|
+
if (await fs.pathExists(attributesFile)) {
|
|
495
|
+
const attributesStats = await fs.stat(attributesFile);
|
|
496
|
+
const attributesPath = `${customer.idn}/attributes.yaml`;
|
|
497
|
+
if (verbose) {
|
|
498
|
+
console.log(`š ${attributesPath}`);
|
|
499
|
+
console.log(` š
Last modified: ${attributesStats.mtime.toISOString()}`);
|
|
500
|
+
console.log(` š Size: ${(attributesStats.size / 1024).toFixed(1)}KB`);
|
|
501
|
+
}
|
|
502
|
+
// For now, just report the file exists (change detection would require timestamp tracking)
|
|
503
|
+
if (verbose)
|
|
504
|
+
console.log(` ā Attributes file tracked`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
if (verbose)
|
|
509
|
+
console.log(`ā ļø Error checking attributes: ${error instanceof Error ? error.message : String(error)}`);
|
|
510
|
+
}
|
|
511
|
+
// Check flows.yaml file for changes
|
|
512
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
513
|
+
if (await fs.pathExists(flowsFile)) {
|
|
514
|
+
try {
|
|
515
|
+
const flowsStats = await fs.stat(flowsFile);
|
|
516
|
+
if (verbose) {
|
|
517
|
+
console.log(`š flows.yaml`);
|
|
518
|
+
console.log(` š
Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
519
|
+
console.log(` š Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
520
|
+
console.log(` ā Flows file tracked`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
if (verbose)
|
|
525
|
+
console.log(`ā ļø Error checking flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
502
528
|
console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
|
|
503
529
|
}
|
|
504
530
|
async function generateFlowsYaml(client, customer, agents, verbose = false) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newo",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "NEWO CLI: sync AI Agent skills and customer attributes between NEWO platform and local files. Multi-customer workspaces, Git-first workflows, comprehensive project management.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli.ts
CHANGED
|
@@ -158,40 +158,9 @@ async function main(): Promise<void> {
|
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
console.error(`Unknown customer: ${args.customer}`);
|
|
165
|
-
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
selectedCustomer = customer;
|
|
169
|
-
} else {
|
|
170
|
-
// For pull command, try to get default but fall back to all customers if multiple exist
|
|
171
|
-
if (cmd === 'pull') {
|
|
172
|
-
try {
|
|
173
|
-
selectedCustomer = tryGetDefaultCustomer(customerConfig);
|
|
174
|
-
if (!selectedCustomer) {
|
|
175
|
-
// Multiple customers exist with no default, pull from all
|
|
176
|
-
allCustomers = getAllCustomers(customerConfig);
|
|
177
|
-
if (verbose) console.log(`š„ No default customer specified, pulling from all ${allCustomers.length} customers`);
|
|
178
|
-
}
|
|
179
|
-
} catch (error: unknown) {
|
|
180
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
-
console.error(message);
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
// For other commands, require explicit customer selection
|
|
186
|
-
try {
|
|
187
|
-
selectedCustomer = getDefaultCustomer(customerConfig);
|
|
188
|
-
} catch (error: unknown) {
|
|
189
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
-
console.error(message);
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
161
|
+
// Customer selection logic moved inside command processing to avoid early failures
|
|
162
|
+
|
|
163
|
+
if (verbose) console.log(`š Command parsed: "${cmd}"`);
|
|
195
164
|
|
|
196
165
|
if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
|
|
197
166
|
console.log(`NEWO CLI - Multi-Customer Support
|
|
@@ -204,7 +173,7 @@ Usage:
|
|
|
204
173
|
newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles from file
|
|
205
174
|
|
|
206
175
|
Flags:
|
|
207
|
-
--customer <idn> # specify customer (if not set, uses default or
|
|
176
|
+
--customer <idn> # specify customer (if not set, uses default or interactive selection)
|
|
208
177
|
--verbose, -v # enable detailed logging
|
|
209
178
|
|
|
210
179
|
Environment Variables:
|
|
@@ -222,8 +191,9 @@ Multi-Customer Examples:
|
|
|
222
191
|
# Commands:
|
|
223
192
|
newo pull # Pull from all customers (if no default set)
|
|
224
193
|
newo pull --customer acme # Pull projects for Acme only
|
|
225
|
-
newo
|
|
226
|
-
newo
|
|
194
|
+
newo status # Status for all customers (if no default set)
|
|
195
|
+
newo push # Interactive selection for multiple customers
|
|
196
|
+
newo push --customer globex # Push changes for Globex only
|
|
227
197
|
|
|
228
198
|
File Structure:
|
|
229
199
|
newo_customers/
|
|
@@ -237,7 +207,27 @@ File Structure:
|
|
|
237
207
|
return;
|
|
238
208
|
}
|
|
239
209
|
|
|
210
|
+
if (verbose) console.log(`š Starting command processing for: ${cmd}`);
|
|
211
|
+
|
|
240
212
|
if (cmd === 'pull') {
|
|
213
|
+
// Handle customer selection for pull command
|
|
214
|
+
if (args.customer) {
|
|
215
|
+
const customer = getCustomer(customerConfig, args.customer as string);
|
|
216
|
+
if (!customer) {
|
|
217
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
218
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
selectedCustomer = customer;
|
|
222
|
+
} else {
|
|
223
|
+
// Try to get default, fall back to all customers
|
|
224
|
+
selectedCustomer = tryGetDefaultCustomer(customerConfig);
|
|
225
|
+
if (!selectedCustomer) {
|
|
226
|
+
allCustomers = getAllCustomers(customerConfig);
|
|
227
|
+
if (verbose) console.log(`š„ No default customer specified, pulling from all ${allCustomers.length} customers`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
241
231
|
if (selectedCustomer) {
|
|
242
232
|
// Single customer pull
|
|
243
233
|
const accessToken = await getValidAccessToken(selectedCustomer);
|
|
@@ -259,7 +249,128 @@ File Structure:
|
|
|
259
249
|
return;
|
|
260
250
|
}
|
|
261
251
|
|
|
252
|
+
if (cmd === 'status') {
|
|
253
|
+
// Handle customer selection for status command
|
|
254
|
+
if (args.customer) {
|
|
255
|
+
const customer = getCustomer(customerConfig, args.customer as string);
|
|
256
|
+
if (!customer) {
|
|
257
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
258
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
selectedCustomer = customer;
|
|
262
|
+
} else {
|
|
263
|
+
// Try to get default, fall back to all customers
|
|
264
|
+
selectedCustomer = tryGetDefaultCustomer(customerConfig);
|
|
265
|
+
if (!selectedCustomer) {
|
|
266
|
+
allCustomers = getAllCustomers(customerConfig);
|
|
267
|
+
console.log(`š Checking status for ${allCustomers.length} customers...`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (selectedCustomer) {
|
|
272
|
+
// Single customer status
|
|
273
|
+
await status(selectedCustomer, verbose);
|
|
274
|
+
} else if (allCustomers.length > 0) {
|
|
275
|
+
// Multi-customer status
|
|
276
|
+
for (const customer of allCustomers) {
|
|
277
|
+
console.log(`\nš Status for customer: ${customer.idn}`);
|
|
278
|
+
await status(customer, verbose);
|
|
279
|
+
}
|
|
280
|
+
console.log(`\nā
Status check completed for all ${allCustomers.length} customers`);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (cmd === 'push') {
|
|
286
|
+
// Handle customer selection for push command
|
|
287
|
+
if (args.customer) {
|
|
288
|
+
const customer = getCustomer(customerConfig, args.customer as string);
|
|
289
|
+
if (!customer) {
|
|
290
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
291
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
selectedCustomer = customer;
|
|
295
|
+
} else {
|
|
296
|
+
// Try to get default, provide interactive selection if multiple exist
|
|
297
|
+
selectedCustomer = tryGetDefaultCustomer(customerConfig);
|
|
298
|
+
if (!selectedCustomer) {
|
|
299
|
+
// Multiple customers exist with no default, ask user
|
|
300
|
+
allCustomers = getAllCustomers(customerConfig);
|
|
301
|
+
console.log(`\nš¤ Multiple customers available for push:`);
|
|
302
|
+
allCustomers.forEach((customer, index) => {
|
|
303
|
+
console.log(` ${index + 1}. ${customer.idn}`);
|
|
304
|
+
});
|
|
305
|
+
console.log(` ${allCustomers.length + 1}. All customers`);
|
|
306
|
+
|
|
307
|
+
const readline = await import('readline');
|
|
308
|
+
const rl = readline.createInterface({
|
|
309
|
+
input: process.stdin,
|
|
310
|
+
output: process.stdout
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const choice = await new Promise<string>((resolve) => {
|
|
314
|
+
rl.question(`\nSelect customer to push (1-${allCustomers.length + 1}): `, resolve);
|
|
315
|
+
});
|
|
316
|
+
rl.close();
|
|
317
|
+
|
|
318
|
+
const choiceNum = parseInt(choice.trim());
|
|
319
|
+
if (choiceNum === allCustomers.length + 1) {
|
|
320
|
+
// User selected "All customers"
|
|
321
|
+
console.log(`š Pushing to all ${allCustomers.length} customers...`);
|
|
322
|
+
} else if (choiceNum >= 1 && choiceNum <= allCustomers.length) {
|
|
323
|
+
// User selected specific customer
|
|
324
|
+
selectedCustomer = allCustomers[choiceNum - 1] || null;
|
|
325
|
+
allCustomers = []; // Clear to indicate single customer mode
|
|
326
|
+
if (selectedCustomer) {
|
|
327
|
+
console.log(`š Pushing to customer: ${selectedCustomer.idn}`);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
console.error('Invalid choice. Exiting.');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (selectedCustomer) {
|
|
337
|
+
// Single customer push
|
|
338
|
+
const accessToken = await getValidAccessToken(selectedCustomer);
|
|
339
|
+
const client = await makeClient(verbose, accessToken);
|
|
340
|
+
await pushChanged(client, selectedCustomer, verbose);
|
|
341
|
+
} else if (allCustomers.length > 0) {
|
|
342
|
+
// Multi-customer push (user selected "All customers")
|
|
343
|
+
console.log(`š Pushing to ${allCustomers.length} customers...`);
|
|
344
|
+
for (const customer of allCustomers) {
|
|
345
|
+
console.log(`\nš¤ Pushing for customer: ${customer.idn}`);
|
|
346
|
+
const accessToken = await getValidAccessToken(customer);
|
|
347
|
+
const client = await makeClient(verbose, accessToken);
|
|
348
|
+
await pushChanged(client, customer, verbose);
|
|
349
|
+
}
|
|
350
|
+
console.log(`\nā
Push completed for all ${allCustomers.length} customers`);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
262
355
|
// For all other commands, require a single selected customer
|
|
356
|
+
if (args.customer) {
|
|
357
|
+
const customer = getCustomer(customerConfig, args.customer as string);
|
|
358
|
+
if (!customer) {
|
|
359
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
360
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
selectedCustomer = customer;
|
|
364
|
+
} else {
|
|
365
|
+
try {
|
|
366
|
+
selectedCustomer = getDefaultCustomer(customerConfig);
|
|
367
|
+
} catch (error: unknown) {
|
|
368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
369
|
+
console.error(message);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
263
374
|
if (!selectedCustomer) {
|
|
264
375
|
console.error('Customer selection required for this command');
|
|
265
376
|
process.exit(1);
|
|
@@ -269,11 +380,7 @@ File Structure:
|
|
|
269
380
|
const accessToken = await getValidAccessToken(selectedCustomer);
|
|
270
381
|
const client = await makeClient(verbose, accessToken);
|
|
271
382
|
|
|
272
|
-
if (cmd === '
|
|
273
|
-
await pushChanged(client, selectedCustomer, verbose);
|
|
274
|
-
} else if (cmd === 'status') {
|
|
275
|
-
await status(selectedCustomer, verbose);
|
|
276
|
-
} else if (cmd === 'meta') {
|
|
383
|
+
if (cmd === 'meta') {
|
|
277
384
|
if (!selectedCustomer.projectId) {
|
|
278
385
|
console.error(`No project ID configured for customer ${selectedCustomer.idn}`);
|
|
279
386
|
console.error(`Set NEWO_CUSTOMER_${selectedCustomer.idn.toUpperCase()}_PROJECT_ID in your .env file`);
|
package/src/sync.ts
CHANGED
|
@@ -46,8 +46,7 @@ import type {
|
|
|
46
46
|
FlowMetadata,
|
|
47
47
|
SkillMetadata,
|
|
48
48
|
FlowEvent,
|
|
49
|
-
FlowState
|
|
50
|
-
CustomerAttribute
|
|
49
|
+
FlowState
|
|
51
50
|
} from './types.js';
|
|
52
51
|
|
|
53
52
|
// Concurrency limits for API operations
|
|
@@ -488,35 +487,23 @@ export async function pushChanged(client: AxiosInstance, customer: CustomerConfi
|
|
|
488
487
|
if (await fs.pathExists(attributesFile) && await fs.pathExists(attributesMapFile)) {
|
|
489
488
|
if (verbose) console.log('š Checking customer attributes for changes...');
|
|
490
489
|
|
|
491
|
-
|
|
490
|
+
// Check file modification time for change detection instead of YAML parsing
|
|
491
|
+
const attributesStats = await fs.stat(attributesFile);
|
|
492
492
|
const idMapping = await fs.readJson(attributesMapFile) as Record<string, string>;
|
|
493
|
-
const parsedAttributes = yaml.load(attributesContent) as { attributes: CustomerAttribute[] };
|
|
494
493
|
|
|
495
|
-
|
|
496
|
-
|
|
494
|
+
// Count attributes by ID mapping instead of parsing YAML (avoids enum parsing issues)
|
|
495
|
+
const attributeCount = Object.keys(idMapping).length;
|
|
497
496
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// For now, just validate the structure (actual push would require change detection)
|
|
506
|
-
// This ensures the push functionality is ready when change detection is implemented
|
|
507
|
-
if (verbose) {
|
|
508
|
-
console.log(`ā Attribute ${attribute.idn} ready for push (ID: ${attributeId})`);
|
|
509
|
-
}
|
|
510
|
-
attributesPushed++;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (verbose) console.log(`š Found ${attributesPushed} attributes ready for push operations`);
|
|
497
|
+
if (verbose) {
|
|
498
|
+
console.log(`š Found ${attributeCount} attributes ready for push operations`);
|
|
499
|
+
console.log(`š
Attributes file last modified: ${attributesStats.mtime.toISOString()}`);
|
|
500
|
+
// TODO: Implement change detection by comparing with last push timestamp
|
|
514
501
|
}
|
|
515
502
|
} else if (verbose) {
|
|
516
503
|
console.log('ā¹ļø No attributes file or ID mapping found for push checking');
|
|
517
504
|
}
|
|
518
505
|
} catch (error) {
|
|
519
|
-
console.log(`ā ļø Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
506
|
+
if (verbose) console.log(`ā ļø Attributes push check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
520
507
|
}
|
|
521
508
|
|
|
522
509
|
await saveHashes(newHashes, customer.idn);
|
|
@@ -597,6 +584,43 @@ export async function status(customer: CustomerConfig, verbose: boolean = false)
|
|
|
597
584
|
}
|
|
598
585
|
}
|
|
599
586
|
}
|
|
587
|
+
|
|
588
|
+
// Check attributes file for changes
|
|
589
|
+
try {
|
|
590
|
+
const attributesFile = customerAttributesPath(customer.idn);
|
|
591
|
+
if (await fs.pathExists(attributesFile)) {
|
|
592
|
+
const attributesStats = await fs.stat(attributesFile);
|
|
593
|
+
const attributesPath = `${customer.idn}/attributes.yaml`;
|
|
594
|
+
|
|
595
|
+
if (verbose) {
|
|
596
|
+
console.log(`š ${attributesPath}`);
|
|
597
|
+
console.log(` š
Last modified: ${attributesStats.mtime.toISOString()}`);
|
|
598
|
+
console.log(` š Size: ${(attributesStats.size / 1024).toFixed(1)}KB`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// For now, just report the file exists (change detection would require timestamp tracking)
|
|
602
|
+
if (verbose) console.log(` ā Attributes file tracked`);
|
|
603
|
+
}
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (verbose) console.log(`ā ļø Error checking attributes: ${error instanceof Error ? error.message : String(error)}`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Check flows.yaml file for changes
|
|
609
|
+
const flowsFile = flowsYamlPath(customer.idn);
|
|
610
|
+
if (await fs.pathExists(flowsFile)) {
|
|
611
|
+
try {
|
|
612
|
+
const flowsStats = await fs.stat(flowsFile);
|
|
613
|
+
if (verbose) {
|
|
614
|
+
console.log(`š flows.yaml`);
|
|
615
|
+
console.log(` š
Last modified: ${flowsStats.mtime.toISOString()}`);
|
|
616
|
+
console.log(` š Size: ${(flowsStats.size / 1024).toFixed(1)}KB`);
|
|
617
|
+
console.log(` ā Flows file tracked`);
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
if (verbose) console.log(`ā ļø Error checking flows.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
600
624
|
console.log(dirty ? `${dirty} changed file(s).` : 'Clean.');
|
|
601
625
|
}
|
|
602
626
|
|