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.
- package/CHANGELOG.md +16 -0
- package/dist/api.d.ts +3 -1
- package/dist/api.js +49 -1
- package/dist/application/migration/MigrationEngine.d.ts +141 -0
- package/dist/application/migration/MigrationEngine.js +322 -0
- package/dist/application/migration/index.d.ts +5 -0
- package/dist/application/migration/index.js +5 -0
- package/dist/application/sync/SyncEngine.d.ts +134 -0
- package/dist/application/sync/SyncEngine.js +335 -0
- package/dist/application/sync/index.d.ts +5 -0
- package/dist/application/sync/index.js +5 -0
- package/dist/cli/commands/create-attribute.js +1 -1
- package/dist/cli/commands/create-customer.d.ts +3 -0
- package/dist/cli/commands/create-customer.js +159 -0
- package/dist/cli/commands/diff.d.ts +6 -0
- package/dist/cli/commands/diff.js +288 -0
- package/dist/cli/commands/help.js +63 -3
- package/dist/cli/commands/logs.d.ts +18 -0
- package/dist/cli/commands/logs.js +283 -0
- package/dist/cli/commands/pull.js +114 -10
- package/dist/cli/commands/push.js +122 -12
- package/dist/cli/commands/update-attribute.d.ts +3 -0
- package/dist/cli/commands/update-attribute.js +78 -0
- package/dist/cli/commands/watch.d.ts +6 -0
- package/dist/cli/commands/watch.js +195 -0
- package/dist/cli-new/bootstrap.d.ts +74 -0
- package/dist/cli-new/bootstrap.js +154 -0
- package/dist/cli-new/di/Container.d.ts +64 -0
- package/dist/cli-new/di/Container.js +122 -0
- package/dist/cli-new/di/tokens.d.ts +77 -0
- package/dist/cli-new/di/tokens.js +76 -0
- package/dist/cli.js +20 -0
- package/dist/domain/resources/common/types.d.ts +71 -0
- package/dist/domain/resources/common/types.js +42 -0
- package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
- package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
- package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
- package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
- package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
- package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
- package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
- package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
- package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
- package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
- package/dist/domain/strategies/sync/index.d.ts +13 -0
- package/dist/domain/strategies/sync/index.js +19 -0
- package/dist/sync/migrate.js +99 -23
- package/dist/types.d.ts +124 -0
- package/package.json +3 -1
- package/src/api.ts +53 -2
- package/src/application/migration/MigrationEngine.ts +492 -0
- package/src/application/migration/index.ts +5 -0
- package/src/application/sync/SyncEngine.ts +467 -0
- package/src/application/sync/index.ts +5 -0
- package/src/cli/commands/create-attribute.ts +1 -1
- package/src/cli/commands/create-customer.ts +185 -0
- package/src/cli/commands/diff.ts +360 -0
- package/src/cli/commands/help.ts +63 -3
- package/src/cli/commands/logs.ts +329 -0
- package/src/cli/commands/pull.ts +128 -11
- package/src/cli/commands/push.ts +131 -13
- package/src/cli/commands/update-attribute.ts +82 -0
- package/src/cli/commands/watch.ts +227 -0
- package/src/cli-new/bootstrap.ts +252 -0
- package/src/cli-new/di/Container.ts +152 -0
- package/src/cli-new/di/tokens.ts +105 -0
- package/src/cli.ts +25 -0
- package/src/domain/resources/common/types.ts +106 -0
- package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
- package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
- package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
- package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
- package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
- package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
- package/src/domain/strategies/sync/index.ts +46 -0
- package/src/sync/migrate.ts +103 -24
- 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
|
+
}
|