newo 3.4.0 → 3.4.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/api.d.ts +3 -1
  3. package/dist/api.js +49 -1
  4. package/dist/application/migration/MigrationEngine.d.ts +141 -0
  5. package/dist/application/migration/MigrationEngine.js +322 -0
  6. package/dist/application/migration/index.d.ts +5 -0
  7. package/dist/application/migration/index.js +5 -0
  8. package/dist/application/sync/SyncEngine.d.ts +134 -0
  9. package/dist/application/sync/SyncEngine.js +335 -0
  10. package/dist/application/sync/index.d.ts +5 -0
  11. package/dist/application/sync/index.js +5 -0
  12. package/dist/cli/commands/create-attribute.js +1 -1
  13. package/dist/cli/commands/create-customer.d.ts +3 -0
  14. package/dist/cli/commands/create-customer.js +159 -0
  15. package/dist/cli/commands/diff.d.ts +6 -0
  16. package/dist/cli/commands/diff.js +288 -0
  17. package/dist/cli/commands/help.js +63 -3
  18. package/dist/cli/commands/logs.d.ts +18 -0
  19. package/dist/cli/commands/logs.js +283 -0
  20. package/dist/cli/commands/pull.js +114 -10
  21. package/dist/cli/commands/push.js +122 -12
  22. package/dist/cli/commands/update-attribute.d.ts +3 -0
  23. package/dist/cli/commands/update-attribute.js +78 -0
  24. package/dist/cli/commands/watch.d.ts +6 -0
  25. package/dist/cli/commands/watch.js +195 -0
  26. package/dist/cli-new/bootstrap.d.ts +74 -0
  27. package/dist/cli-new/bootstrap.js +154 -0
  28. package/dist/cli-new/di/Container.d.ts +64 -0
  29. package/dist/cli-new/di/Container.js +122 -0
  30. package/dist/cli-new/di/tokens.d.ts +77 -0
  31. package/dist/cli-new/di/tokens.js +76 -0
  32. package/dist/cli.js +20 -0
  33. package/dist/domain/resources/common/types.d.ts +71 -0
  34. package/dist/domain/resources/common/types.js +42 -0
  35. package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
  36. package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
  37. package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
  38. package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
  39. package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
  40. package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
  41. package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
  42. package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
  43. package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
  44. package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
  45. package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
  46. package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
  47. package/dist/domain/strategies/sync/index.d.ts +13 -0
  48. package/dist/domain/strategies/sync/index.js +19 -0
  49. package/dist/sync/migrate.js +99 -23
  50. package/dist/types.d.ts +124 -0
  51. package/package.json +3 -1
  52. package/src/api.ts +53 -2
  53. package/src/application/migration/MigrationEngine.ts +492 -0
  54. package/src/application/migration/index.ts +5 -0
  55. package/src/application/sync/SyncEngine.ts +467 -0
  56. package/src/application/sync/index.ts +5 -0
  57. package/src/cli/commands/create-attribute.ts +1 -1
  58. package/src/cli/commands/create-customer.ts +185 -0
  59. package/src/cli/commands/diff.ts +360 -0
  60. package/src/cli/commands/help.ts +63 -3
  61. package/src/cli/commands/logs.ts +329 -0
  62. package/src/cli/commands/pull.ts +128 -11
  63. package/src/cli/commands/push.ts +131 -13
  64. package/src/cli/commands/update-attribute.ts +82 -0
  65. package/src/cli/commands/watch.ts +227 -0
  66. package/src/cli-new/bootstrap.ts +252 -0
  67. package/src/cli-new/di/Container.ts +152 -0
  68. package/src/cli-new/di/tokens.ts +105 -0
  69. package/src/cli.ts +25 -0
  70. package/src/domain/resources/common/types.ts +106 -0
  71. package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
  72. package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
  73. package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
  74. package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
  75. package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
  76. package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
  77. package/src/domain/strategies/sync/index.ts +46 -0
  78. package/src/sync/migrate.ts +103 -24
  79. package/src/types.ts +135 -0
@@ -0,0 +1,747 @@
1
+ /**
2
+ * ProjectSyncStrategy - Handles synchronization of Projects, Agents, Flows, and Skills
3
+ *
4
+ * This strategy implements ISyncStrategy for the Project resource hierarchy:
5
+ * Project → Agent → Flow → Skill
6
+ *
7
+ * Key responsibilities:
8
+ * - Pull complete project structure from NEWO platform
9
+ * - Push changed skills and new entities to platform
10
+ * - Detect changes using SHA256 hashes
11
+ * - Validate project structure before push
12
+ */
13
+
14
+ import type {
15
+ ISyncStrategy,
16
+ PullOptions,
17
+ PullResult,
18
+ PushResult,
19
+ ChangeItem,
20
+ ValidationResult,
21
+ ValidationError,
22
+ StatusSummary
23
+ } from './ISyncStrategy.js';
24
+ import type { CustomerConfig, ILogger, HashStore } from '../../resources/common/types.js';
25
+ import type { AxiosInstance } from 'axios';
26
+ import type {
27
+ ProjectMeta,
28
+ Agent,
29
+ Flow,
30
+ Skill,
31
+ FlowEvent,
32
+ FlowState,
33
+ ProjectData,
34
+ ProjectMap,
35
+ SkillMetadata,
36
+ FlowMetadata,
37
+ AgentMetadata,
38
+ ProjectMetadata
39
+ } from '../../../types.js';
40
+ import fs from 'fs-extra';
41
+ import yaml from 'js-yaml';
42
+ import {
43
+ listProjects,
44
+ listAgents,
45
+ listFlowSkills,
46
+ listFlowEvents,
47
+ listFlowStates,
48
+ updateSkill,
49
+ publishFlow
50
+ } from '../../../api.js';
51
+ import {
52
+ ensureState,
53
+ writeFileSafe,
54
+ mapPath,
55
+ projectMetadataPath,
56
+ agentMetadataPath,
57
+ flowMetadataPath,
58
+ skillMetadataPath,
59
+ skillScriptPath,
60
+ skillFolderPath,
61
+ flowsYamlPath,
62
+ customerProjectsDir,
63
+ projectDir
64
+ } from '../../../fsutil.js';
65
+ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
66
+ import { generateFlowsYaml } from '../../../sync/metadata.js';
67
+ import {
68
+ findSkillScriptFiles,
69
+ isContentDifferent,
70
+ getExtensionForRunner,
71
+ getSingleSkillFile,
72
+ validateSkillFolder
73
+ } from '../../../sync/skill-files.js';
74
+
75
+ /**
76
+ * Project data for local storage
77
+ */
78
+ export interface LocalProjectData {
79
+ projectId: string;
80
+ projectIdn: string;
81
+ metadata: ProjectMetadata;
82
+ agents: LocalAgentData[];
83
+ }
84
+
85
+ export interface LocalAgentData {
86
+ id: string;
87
+ idn: string;
88
+ metadata: AgentMetadata;
89
+ flows: LocalFlowData[];
90
+ }
91
+
92
+ export interface LocalFlowData {
93
+ id: string;
94
+ idn: string;
95
+ metadata: FlowMetadata;
96
+ skills: LocalSkillData[];
97
+ }
98
+
99
+ export interface LocalSkillData {
100
+ id: string;
101
+ idn: string;
102
+ metadata: SkillMetadata;
103
+ scriptPath: string;
104
+ scriptContent: string;
105
+ }
106
+
107
+ /**
108
+ * API client factory type
109
+ */
110
+ export type ApiClientFactory = (customer: CustomerConfig, verbose: boolean) => Promise<AxiosInstance>;
111
+
112
+ /**
113
+ * ProjectSyncStrategy - Handles project synchronization
114
+ */
115
+ export class ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalProjectData> {
116
+ readonly resourceType = 'projects';
117
+ readonly displayName = 'Projects';
118
+
119
+ constructor(
120
+ private apiClientFactory: ApiClientFactory,
121
+ private logger: ILogger
122
+ ) {}
123
+
124
+ /**
125
+ * Pull all projects from NEWO platform
126
+ */
127
+ async pull(customer: CustomerConfig, options: PullOptions = {}): Promise<PullResult<LocalProjectData>> {
128
+ const client = await this.apiClientFactory(customer, options.verbose ?? false);
129
+ const hashes: HashStore = {};
130
+ const projects: LocalProjectData[] = [];
131
+
132
+ this.logger.verbose(`📋 Loading project list for customer ${customer.idn}...`);
133
+
134
+ await ensureState(customer.idn);
135
+
136
+ // Fetch projects
137
+ const apiProjects = options.projectId
138
+ ? [{ id: options.projectId, idn: 'unknown', title: 'Project' } as ProjectMeta]
139
+ : await listProjects(client);
140
+
141
+ if (apiProjects.length === 0) {
142
+ this.logger.info(`No projects found for customer ${customer.idn}`);
143
+ return { items: [], count: 0, hashes: {} };
144
+ }
145
+
146
+ // Load existing map for reference
147
+ let existingMap: ProjectMap = { projects: {} };
148
+ const mapFile = mapPath(customer.idn);
149
+ if (await fs.pathExists(mapFile)) {
150
+ try {
151
+ const mapData = await fs.readJson(mapFile);
152
+ if (mapData && typeof mapData === 'object' && 'projects' in mapData) {
153
+ existingMap = mapData as ProjectMap;
154
+ }
155
+ } catch {
156
+ // Ignore errors, start fresh
157
+ }
158
+ }
159
+
160
+ // Count total skills for progress
161
+ let totalSkills = 0;
162
+ let processedSkills = 0;
163
+
164
+ for (const project of apiProjects) {
165
+ const agents = await listAgents(client, project.id);
166
+ for (const agent of agents) {
167
+ const flows = agent.flows || [];
168
+ for (const flow of flows) {
169
+ const skills = await listFlowSkills(client, flow.id);
170
+ totalSkills += skills.length;
171
+ }
172
+ }
173
+ }
174
+
175
+ this.logger.verbose(`📊 Total skills to process: ${totalSkills}`);
176
+
177
+ // Process each project
178
+ for (const project of apiProjects) {
179
+ this.logger.verbose(`📁 Processing project: ${project.title} (${project.idn})`);
180
+
181
+ const projectMeta: ProjectMetadata = {
182
+ id: project.id,
183
+ idn: project.idn,
184
+ title: project.title,
185
+ description: project.description || '',
186
+ created_at: project.created_at || '',
187
+ updated_at: project.updated_at || ''
188
+ };
189
+
190
+ // Save project metadata
191
+ const projectMetaPath = projectMetadataPath(customer.idn, project.idn);
192
+ const projectMetaYaml = yaml.dump(projectMeta, { indent: 2, quotingType: '"', forceQuotes: false });
193
+ await writeFileSafe(projectMetaPath, projectMetaYaml);
194
+ hashes[projectMetaPath] = sha256(projectMetaYaml);
195
+
196
+ const localProject: LocalProjectData = {
197
+ projectId: project.id,
198
+ projectIdn: project.idn,
199
+ metadata: projectMeta,
200
+ agents: []
201
+ };
202
+
203
+ const agents = await listAgents(client, project.id);
204
+ this.logger.verbose(` 📋 Found ${agents.length} agents in project ${project.title}`);
205
+
206
+ const projectData: ProjectData = {
207
+ projectId: project.id,
208
+ projectIdn: project.idn,
209
+ agents: {}
210
+ };
211
+
212
+ // Process each agent
213
+ for (const agent of agents) {
214
+ const localAgent = await this.pullAgent(
215
+ client, customer, project, agent, hashes, options, () => {
216
+ processedSkills++;
217
+ if (!options.verbose && totalSkills > 0) {
218
+ if (processedSkills % 10 === 0 || processedSkills === totalSkills) {
219
+ this.logger.progress(processedSkills, totalSkills, '📄 Processing skills');
220
+ }
221
+ }
222
+ }
223
+ );
224
+ localProject.agents.push(localAgent);
225
+
226
+ // Build project data for map
227
+ projectData.agents[agent.idn] = {
228
+ id: agent.id,
229
+ flows: {}
230
+ };
231
+
232
+ for (const flow of localAgent.flows) {
233
+ projectData.agents[agent.idn]!.flows[flow.idn] = {
234
+ id: flow.id,
235
+ skills: {}
236
+ };
237
+
238
+ for (const skill of flow.skills) {
239
+ projectData.agents[agent.idn]!.flows[flow.idn]!.skills[skill.idn] = skill.metadata;
240
+ }
241
+ }
242
+ }
243
+
244
+ existingMap.projects[project.idn] = projectData;
245
+ projects.push(localProject);
246
+ }
247
+
248
+ // Save updated project map
249
+ await writeFileSafe(mapFile, JSON.stringify(existingMap, null, 2));
250
+
251
+ // Generate flows.yaml
252
+ const flowsYamlContent = await generateFlowsYaml(existingMap, customer.idn, options.verbose ?? false);
253
+ const flowsYamlFilePath = flowsYamlPath(customer.idn);
254
+ hashes[flowsYamlFilePath] = sha256(flowsYamlContent);
255
+
256
+ // Save hashes
257
+ await saveHashes(hashes, customer.idn);
258
+
259
+ // Clean up deleted entities if not skipped
260
+ if (!options.skipCleanup) {
261
+ await this.cleanupDeletedEntities(customer.idn, existingMap, options.verbose ?? false);
262
+ }
263
+
264
+ return {
265
+ items: projects,
266
+ count: projects.length,
267
+ hashes
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Pull a single agent and its flows/skills
273
+ */
274
+ private async pullAgent(
275
+ client: AxiosInstance,
276
+ customer: CustomerConfig,
277
+ project: ProjectMeta,
278
+ agent: Agent,
279
+ hashes: HashStore,
280
+ options: PullOptions,
281
+ onSkillProcessed: () => void
282
+ ): Promise<LocalAgentData> {
283
+ this.logger.verbose(` 📁 Processing agent: ${agent.title} (${agent.idn})`);
284
+
285
+ const agentMeta: AgentMetadata = {
286
+ id: agent.id,
287
+ idn: agent.idn,
288
+ title: agent.title || '',
289
+ description: agent.description || ''
290
+ };
291
+
292
+ // Save agent metadata
293
+ const agentMetaPath = agentMetadataPath(customer.idn, project.idn, agent.idn);
294
+ const agentMetaYaml = yaml.dump(agentMeta, { indent: 2, quotingType: '"', forceQuotes: false });
295
+ await writeFileSafe(agentMetaPath, agentMetaYaml);
296
+ hashes[agentMetaPath] = sha256(agentMetaYaml);
297
+
298
+ const localAgent: LocalAgentData = {
299
+ id: agent.id,
300
+ idn: agent.idn,
301
+ metadata: agentMeta,
302
+ flows: []
303
+ };
304
+
305
+ const flows = agent.flows || [];
306
+ this.logger.verbose(` 📋 Found ${flows.length} flows in agent ${agent.title}`);
307
+
308
+ // Process each flow
309
+ for (const flow of flows) {
310
+ const localFlow = await this.pullFlow(
311
+ client, customer, project, agent, flow, hashes, options, onSkillProcessed
312
+ );
313
+ localAgent.flows.push(localFlow);
314
+ }
315
+
316
+ return localAgent;
317
+ }
318
+
319
+ /**
320
+ * Pull a single flow and its skills
321
+ */
322
+ private async pullFlow(
323
+ client: AxiosInstance,
324
+ customer: CustomerConfig,
325
+ project: ProjectMeta,
326
+ agent: Agent,
327
+ flow: Flow,
328
+ hashes: HashStore,
329
+ options: PullOptions,
330
+ onSkillProcessed: () => void
331
+ ): Promise<LocalFlowData> {
332
+ this.logger.verbose(` 📁 Processing flow: ${flow.title} (${flow.idn})`);
333
+
334
+ // Get flow events and states
335
+ const [events, states] = await Promise.all([
336
+ listFlowEvents(client, flow.id).catch(() => [] as FlowEvent[]),
337
+ listFlowStates(client, flow.id).catch(() => [] as FlowState[])
338
+ ]);
339
+
340
+ const flowMeta: FlowMetadata = {
341
+ id: flow.id,
342
+ idn: flow.idn,
343
+ title: flow.title,
344
+ description: flow.description || '',
345
+ default_runner_type: flow.default_runner_type,
346
+ default_model: flow.default_model,
347
+ events,
348
+ state_fields: states
349
+ };
350
+
351
+ // Save flow metadata
352
+ const flowMetaPath = flowMetadataPath(customer.idn, project.idn, agent.idn, flow.idn);
353
+ const flowMetaYaml = yaml.dump(flowMeta, { indent: 2, quotingType: '"', forceQuotes: false });
354
+ await writeFileSafe(flowMetaPath, flowMetaYaml);
355
+ hashes[flowMetaPath] = sha256(flowMetaYaml);
356
+
357
+ const localFlow: LocalFlowData = {
358
+ id: flow.id,
359
+ idn: flow.idn,
360
+ metadata: flowMeta,
361
+ skills: []
362
+ };
363
+
364
+ // Process skills
365
+ const skills = await listFlowSkills(client, flow.id);
366
+ this.logger.verbose(` 📋 Found ${skills.length} skills in flow ${flow.title}`);
367
+
368
+ for (const skill of skills) {
369
+ const localSkill = await this.pullSkill(
370
+ client, customer, project, agent, flow, skill, hashes, options
371
+ );
372
+ localFlow.skills.push(localSkill);
373
+ onSkillProcessed();
374
+ }
375
+
376
+ return localFlow;
377
+ }
378
+
379
+ /**
380
+ * Pull a single skill
381
+ */
382
+ private async pullSkill(
383
+ _client: AxiosInstance,
384
+ customer: CustomerConfig,
385
+ project: ProjectMeta,
386
+ agent: Agent,
387
+ flow: Flow,
388
+ skill: Skill,
389
+ hashes: HashStore,
390
+ options: PullOptions
391
+ ): Promise<LocalSkillData> {
392
+ this.logger.verbose(` 📄 Processing skill: ${skill.title} (${skill.idn})`);
393
+
394
+ const skillMeta: SkillMetadata = {
395
+ id: skill.id,
396
+ idn: skill.idn,
397
+ title: skill.title,
398
+ runner_type: skill.runner_type,
399
+ model: skill.model,
400
+ parameters: [...skill.parameters],
401
+ path: skill.path
402
+ };
403
+
404
+ // Save skill metadata
405
+ const skillMetaPath = skillMetadataPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn);
406
+ const skillMetaYaml = yaml.dump(skillMeta, { indent: 2, quotingType: '"', forceQuotes: false });
407
+ await writeFileSafe(skillMetaPath, skillMetaYaml);
408
+ hashes[skillMetaPath] = sha256(skillMetaYaml);
409
+
410
+ // Handle skill script
411
+ const scriptContent = skill.prompt_script || '';
412
+ const targetScriptPath = skillScriptPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn, skill.runner_type);
413
+ const folderPath = skillFolderPath(customer.idn, project.idn, agent.idn, flow.idn, skill.idn);
414
+
415
+ // Check for existing script files
416
+ const existingFiles = await findSkillScriptFiles(folderPath);
417
+ let shouldWrite = true;
418
+
419
+ if (existingFiles.length > 0) {
420
+ const hasContentMatch = existingFiles.some(file => !isContentDifferent(file.content, scriptContent));
421
+
422
+ if (hasContentMatch) {
423
+ const matchingFile = existingFiles.find(file => !isContentDifferent(file.content, scriptContent));
424
+ const correctName = `${skill.idn}.${getExtensionForRunner(skill.runner_type)}`;
425
+
426
+ if (matchingFile && matchingFile.fileName !== correctName) {
427
+ await fs.remove(matchingFile.filePath);
428
+ this.logger.verbose(` 🔄 Renamed ${matchingFile.fileName} → ${correctName}`);
429
+ } else if (matchingFile && matchingFile.fileName === correctName) {
430
+ shouldWrite = false;
431
+ hashes[matchingFile.filePath] = sha256(scriptContent);
432
+ }
433
+ } else if (!options.silentOverwrite) {
434
+ // In interactive mode, we'd ask for confirmation
435
+ // For now, just overwrite in strategy (interactive logic stays in CLI)
436
+ for (const file of existingFiles) {
437
+ await fs.remove(file.filePath);
438
+ }
439
+ } else {
440
+ for (const file of existingFiles) {
441
+ await fs.remove(file.filePath);
442
+ }
443
+ }
444
+ }
445
+
446
+ if (shouldWrite) {
447
+ await writeFileSafe(targetScriptPath, scriptContent);
448
+ hashes[targetScriptPath] = sha256(scriptContent);
449
+ }
450
+
451
+ return {
452
+ id: skill.id,
453
+ idn: skill.idn,
454
+ metadata: skillMeta,
455
+ scriptPath: targetScriptPath,
456
+ scriptContent
457
+ };
458
+ }
459
+
460
+ /**
461
+ * Push changed projects to NEWO platform
462
+ */
463
+ async push(customer: CustomerConfig, changes?: ChangeItem<LocalProjectData>[]): Promise<PushResult> {
464
+ const result: PushResult = { created: 0, updated: 0, deleted: 0, errors: [] };
465
+
466
+ // If no changes provided, detect them
467
+ if (!changes) {
468
+ changes = await this.getChanges(customer);
469
+ }
470
+
471
+ if (changes.length === 0) {
472
+ return result;
473
+ }
474
+
475
+ const client = await this.apiClientFactory(customer, false);
476
+ const existingHashes = await loadHashes(customer.idn);
477
+ const newHashes = { ...existingHashes };
478
+
479
+ // Load project map
480
+ const mapFile = mapPath(customer.idn);
481
+ if (!(await fs.pathExists(mapFile))) {
482
+ result.errors.push(`No project map found. Run pull first.`);
483
+ return result;
484
+ }
485
+
486
+ const mapData = await fs.readJson(mapFile) as ProjectMap;
487
+
488
+ // Process skill changes
489
+ for (const change of changes) {
490
+ try {
491
+ if (change.operation === 'modified') {
492
+ // Update existing skill
493
+ const updateResult = await this.pushSkillUpdate(client, customer, change, mapData, newHashes);
494
+ result.updated += updateResult;
495
+ } else if (change.operation === 'created') {
496
+ // Create new entity
497
+ const createResult = await this.pushNewEntity(client, customer, change, mapData, newHashes);
498
+ result.created += createResult;
499
+ }
500
+ } catch (error) {
501
+ result.errors.push(`Failed to push ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
502
+ }
503
+ }
504
+
505
+ // Save updated hashes
506
+ await saveHashes(newHashes, customer.idn);
507
+
508
+ // Publish flows if any changes were made
509
+ if (result.created > 0 || result.updated > 0) {
510
+ await this.publishAllFlows(client, mapData);
511
+ }
512
+
513
+ return result;
514
+ }
515
+
516
+ /**
517
+ * Push a skill update
518
+ */
519
+ private async pushSkillUpdate(
520
+ client: AxiosInstance,
521
+ _customer: CustomerConfig,
522
+ change: ChangeItem<LocalProjectData>,
523
+ mapData: ProjectMap,
524
+ newHashes: HashStore
525
+ ): Promise<number> {
526
+ // Extract skill info from path
527
+ // Path format: newo_customers/{customer}/projects/{project}/{agent}/{flow}/{skill}/{skill}.guidance
528
+ const pathParts = change.path.split('/');
529
+ const skillIdn = pathParts[pathParts.length - 2] || '';
530
+ const flowIdn = pathParts[pathParts.length - 3] || '';
531
+ const agentIdn = pathParts[pathParts.length - 4] || '';
532
+ const projectIdn = pathParts[pathParts.length - 5] || '';
533
+
534
+ // Look up skill in map
535
+ const projectData = mapData.projects[projectIdn];
536
+ const agentData = projectData?.agents[agentIdn];
537
+ const flowData = agentData?.flows[flowIdn];
538
+ const skillData = flowData?.skills[skillIdn];
539
+
540
+ if (!skillData) {
541
+ throw new Error(`Skill ${skillIdn} not found in project map`);
542
+ }
543
+
544
+ // Read script content
545
+ const content = await fs.readFile(change.path, 'utf8');
546
+
547
+ // Update skill
548
+ await updateSkill(client, {
549
+ id: skillData.id,
550
+ title: skillData.title,
551
+ idn: skillData.idn,
552
+ prompt_script: content,
553
+ runner_type: skillData.runner_type,
554
+ model: skillData.model,
555
+ parameters: skillData.parameters,
556
+ path: skillData.path
557
+ });
558
+
559
+ // Update hash
560
+ newHashes[change.path] = sha256(content);
561
+
562
+ this.logger.info(`↑ Pushed: ${skillIdn}`);
563
+ return 1;
564
+ }
565
+
566
+ /**
567
+ * Push a new entity
568
+ */
569
+ private async pushNewEntity(
570
+ _client: AxiosInstance,
571
+ _customer: CustomerConfig,
572
+ _change: ChangeItem<LocalProjectData>,
573
+ _mapData: ProjectMap,
574
+ _newHashes: HashStore
575
+ ): Promise<number> {
576
+ // Entity creation is handled separately for now
577
+ // This would be expanded for full entity creation support
578
+ return 0;
579
+ }
580
+
581
+ /**
582
+ * Publish all flows
583
+ */
584
+ private async publishAllFlows(client: AxiosInstance, mapData: ProjectMap): Promise<void> {
585
+ for (const projectData of Object.values(mapData.projects)) {
586
+ for (const agentData of Object.values(projectData.agents)) {
587
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
588
+ if (flowData.id) {
589
+ try {
590
+ await publishFlow(client, flowData.id, {
591
+ version: '1.0',
592
+ description: 'Published via NEWO CLI',
593
+ type: 'public'
594
+ });
595
+ this.logger.verbose(`📤 Published flow: ${flowIdn}`);
596
+ } catch (error) {
597
+ this.logger.warn(`Failed to publish flow ${flowIdn}`);
598
+ }
599
+ }
600
+ }
601
+ }
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Detect changes in project files
607
+ */
608
+ async getChanges(customer: CustomerConfig): Promise<ChangeItem<LocalProjectData>[]> {
609
+ const changes: ChangeItem<LocalProjectData>[] = [];
610
+
611
+ const mapFile = mapPath(customer.idn);
612
+ if (!(await fs.pathExists(mapFile))) {
613
+ return changes;
614
+ }
615
+
616
+ const hashes = await loadHashes(customer.idn);
617
+ const mapData = await fs.readJson(mapFile) as ProjectMap;
618
+
619
+ // Scan for changed skill scripts
620
+ for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
621
+ for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
622
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
623
+ for (const [skillIdn, _skillData] of Object.entries(flowData.skills)) {
624
+ const skillFile = await getSingleSkillFile(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
625
+
626
+ if (skillFile) {
627
+ const currentHash = sha256(skillFile.content);
628
+ const storedHash = hashes[skillFile.filePath];
629
+
630
+ if (storedHash !== currentHash) {
631
+ changes.push({
632
+ item: {} as LocalProjectData, // Simplified for now
633
+ operation: 'modified',
634
+ path: skillFile.filePath
635
+ });
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }
642
+
643
+ return changes;
644
+ }
645
+
646
+ /**
647
+ * Validate project structure
648
+ */
649
+ async validate(customer: CustomerConfig, _items: LocalProjectData[]): Promise<ValidationResult> {
650
+ const errors: ValidationError[] = [];
651
+
652
+ const mapFile = mapPath(customer.idn);
653
+ if (!(await fs.pathExists(mapFile))) {
654
+ errors.push({
655
+ field: 'projectMap',
656
+ message: 'No project map found. Run pull first.'
657
+ });
658
+ return { valid: false, errors };
659
+ }
660
+
661
+ const mapData = await fs.readJson(mapFile) as ProjectMap;
662
+
663
+ // Validate skill folders
664
+ for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
665
+ for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
666
+ for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
667
+ for (const skillIdn of Object.keys(flowData.skills)) {
668
+ const validation = await validateSkillFolder(customer.idn, projectIdn, agentIdn, flowIdn, skillIdn);
669
+
670
+ if (!validation.isValid) {
671
+ for (const error of validation.errors) {
672
+ errors.push({
673
+ field: `skill.${skillIdn}`,
674
+ message: error,
675
+ path: `${projectIdn}/${agentIdn}/${flowIdn}/${skillIdn}`
676
+ });
677
+ }
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+
684
+ return { valid: errors.length === 0, errors };
685
+ }
686
+
687
+ /**
688
+ * Get status summary
689
+ */
690
+ async getStatus(customer: CustomerConfig): Promise<StatusSummary> {
691
+ const changes = await this.getChanges(customer);
692
+
693
+ return {
694
+ resourceType: this.resourceType,
695
+ displayName: this.displayName,
696
+ changedCount: changes.length,
697
+ changes: changes.map(c => ({
698
+ path: c.path,
699
+ operation: c.operation
700
+ }))
701
+ };
702
+ }
703
+
704
+ /**
705
+ * Clean up deleted entities
706
+ */
707
+ private async cleanupDeletedEntities(
708
+ customerIdn: string,
709
+ projectMap: ProjectMap,
710
+ verbose: boolean
711
+ ): Promise<void> {
712
+ const projectsPath = customerProjectsDir(customerIdn);
713
+
714
+ if (!(await fs.pathExists(projectsPath))) {
715
+ return;
716
+ }
717
+
718
+ const localProjects = await fs.readdir(projectsPath);
719
+ const deletedPaths: string[] = [];
720
+
721
+ for (const localProjectIdn of localProjects) {
722
+ const localProjectPath = projectDir(customerIdn, localProjectIdn);
723
+ const stat = await fs.stat(localProjectPath).catch(() => null);
724
+
725
+ if (!stat || !stat.isDirectory()) continue;
726
+ if (localProjectIdn === 'flows.yaml') continue;
727
+
728
+ if (!projectMap.projects[localProjectIdn]) {
729
+ deletedPaths.push(localProjectPath);
730
+ }
731
+ }
732
+
733
+ if (deletedPaths.length > 0 && verbose) {
734
+ this.logger.info(`Found ${deletedPaths.length} deleted entities`);
735
+ }
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Factory function for creating ProjectSyncStrategy
741
+ */
742
+ export function createProjectSyncStrategy(
743
+ apiClientFactory: ApiClientFactory,
744
+ logger: ILogger
745
+ ): ProjectSyncStrategy {
746
+ return new ProjectSyncStrategy(apiClientFactory, logger);
747
+ }