newo 3.4.1 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/CHANGELOG.md +31 -0
- package/dist/api.d.ts +18 -0
- package/dist/api.js +28 -0
- package/dist/cli/commands/create-attribute.js +1 -1
- package/dist/cli/commands/export.d.ts +3 -0
- package/dist/cli/commands/export.js +62 -0
- package/dist/cli/commands/help.js +54 -42
- package/dist/cli/commands/pull.js +38 -14
- package/dist/cli/commands/push.js +32 -32
- package/dist/cli/commands/status.js +46 -7
- package/dist/cli/commands/update-attribute.d.ts +3 -0
- package/dist/cli/commands/update-attribute.js +78 -0
- package/dist/cli-new/bootstrap.d.ts +7 -1
- package/dist/cli-new/bootstrap.js +11 -5
- package/dist/cli-new/di/tokens.d.ts +1 -0
- package/dist/cli-new/di/tokens.js +1 -0
- package/dist/cli.js +8 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +5 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.js +97 -8
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.d.ts +80 -0
- package/dist/domain/strategies/sync/V2ProjectSyncStrategy.js +725 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/format/detect.d.ts +14 -0
- package/dist/format/detect.js +105 -0
- package/dist/format/extensions.d.ts +26 -0
- package/dist/format/extensions.js +45 -0
- package/dist/format/index.d.ts +11 -0
- package/dist/format/index.js +11 -0
- package/dist/format/paths-v2.d.ts +31 -0
- package/dist/format/paths-v2.js +104 -0
- package/dist/format/types.d.ts +28 -0
- package/dist/format/types.js +21 -0
- package/dist/format/v2-yaml.d.ts +143 -0
- package/dist/format/v2-yaml.js +222 -0
- package/dist/format/yaml-patch.d.ts +14 -0
- package/dist/format/yaml-patch.js +184 -0
- package/dist/fsutil.d.ts +10 -0
- package/dist/fsutil.js +25 -0
- package/dist/sync/attributes.js +3 -3
- package/dist/sync/skill-files.js +2 -2
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/api.ts +64 -0
- package/src/cli/commands/create-attribute.ts +1 -1
- package/src/cli/commands/export.ts +78 -0
- package/src/cli/commands/help.ts +54 -42
- package/src/cli/commands/pull.ts +46 -15
- package/src/cli/commands/push.ts +38 -31
- package/src/cli/commands/status.ts +59 -9
- package/src/cli/commands/update-attribute.ts +82 -0
- package/src/cli-new/bootstrap.ts +19 -7
- package/src/cli-new/di/tokens.ts +1 -0
- package/src/cli.ts +10 -0
- package/src/domain/strategies/sync/ProjectSyncStrategy.ts +122 -8
- package/src/domain/strategies/sync/V2ProjectSyncStrategy.ts +1007 -0
- package/src/env.ts +2 -0
- package/src/format/detect.ts +123 -0
- package/src/format/extensions.ts +61 -0
- package/src/format/index.ts +66 -0
- package/src/format/paths-v2.ts +207 -0
- package/src/format/types.ts +40 -0
- package/src/format/v2-yaml.ts +345 -0
- package/src/format/yaml-patch.ts +208 -0
- package/src/fsutil.ts +37 -0
- package/src/sync/attributes.ts +3 -3
- package/src/sync/skill-files.ts +2 -2
- package/src/types.ts +6 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2ProjectSyncStrategy - Handles synchronization in newo_v2 format
|
|
3
|
+
*
|
|
4
|
+
* Uses the SAME V1 API endpoints as ProjectSyncStrategy but writes/reads
|
|
5
|
+
* files in the newo_v2 directory layout:
|
|
6
|
+
* {CustomerIdn}/
|
|
7
|
+
* import_version.txt
|
|
8
|
+
* {ProjectIdn}/
|
|
9
|
+
* {project_idn}.yaml
|
|
10
|
+
* agents/{AgentIdn}/
|
|
11
|
+
* agent.yaml
|
|
12
|
+
* flows/{FlowIdn}/
|
|
13
|
+
* {FlowIdn}.yaml (inline skill defs, events, state_fields)
|
|
14
|
+
* skills/{SkillIdn}.nsl|.nslg
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
ISyncStrategy,
|
|
19
|
+
PullOptions,
|
|
20
|
+
PullResult,
|
|
21
|
+
PushResult,
|
|
22
|
+
ChangeItem,
|
|
23
|
+
ValidationResult,
|
|
24
|
+
ValidationError,
|
|
25
|
+
StatusSummary
|
|
26
|
+
} from './ISyncStrategy.js';
|
|
27
|
+
import type { CustomerConfig, ILogger, HashStore } from '../../resources/common/types.js';
|
|
28
|
+
import type { AxiosInstance } from 'axios';
|
|
29
|
+
import type {
|
|
30
|
+
ProjectMeta,
|
|
31
|
+
Agent,
|
|
32
|
+
Flow,
|
|
33
|
+
Skill,
|
|
34
|
+
FlowEvent,
|
|
35
|
+
FlowState,
|
|
36
|
+
ProjectData,
|
|
37
|
+
ProjectMap,
|
|
38
|
+
SkillMetadata
|
|
39
|
+
} from '../../../types.js';
|
|
40
|
+
import type { LocalProjectData, LocalAgentData, LocalFlowData, LocalSkillData, ApiClientFactory } from './ProjectSyncStrategy.js';
|
|
41
|
+
import fs from 'fs-extra';
|
|
42
|
+
import {
|
|
43
|
+
listProjects,
|
|
44
|
+
listAgents,
|
|
45
|
+
listFlowSkills,
|
|
46
|
+
listFlowEvents,
|
|
47
|
+
listFlowStates,
|
|
48
|
+
updateSkill,
|
|
49
|
+
publishFlow,
|
|
50
|
+
getProjectAttributes,
|
|
51
|
+
getCustomerAttributes,
|
|
52
|
+
listLibraries,
|
|
53
|
+
updateLibrarySkill,
|
|
54
|
+
} from '../../../api.js';
|
|
55
|
+
import type { LibraryResponse } from '../../../api.js';
|
|
56
|
+
import {
|
|
57
|
+
ensureStateOnly,
|
|
58
|
+
writeFileSafe,
|
|
59
|
+
mapPath,
|
|
60
|
+
} from '../../../fsutil.js';
|
|
61
|
+
import { sha256, saveHashes, loadHashes } from '../../../hash.js';
|
|
62
|
+
import {
|
|
63
|
+
v2ImportVersionPath,
|
|
64
|
+
v2ProjectYamlPath,
|
|
65
|
+
v2AgentYamlPath,
|
|
66
|
+
v2FlowYamlPath,
|
|
67
|
+
v2SkillScriptPath,
|
|
68
|
+
v2SkillRelativePath,
|
|
69
|
+
v2ProjectAttributesPath,
|
|
70
|
+
v2CustomerAttributesPath,
|
|
71
|
+
v2AkbDir,
|
|
72
|
+
v2AkbPath,
|
|
73
|
+
v2LibraryYamlPath,
|
|
74
|
+
v2LibrarySkillScriptPath,
|
|
75
|
+
v2LibrarySkillRelativePath,
|
|
76
|
+
} from '../../../format/paths-v2.js';
|
|
77
|
+
import {
|
|
78
|
+
V2_IMPORT_VERSION,
|
|
79
|
+
} from '../../../format/types.js';
|
|
80
|
+
import {
|
|
81
|
+
generateV2FlowYaml,
|
|
82
|
+
generateV2ProjectYaml,
|
|
83
|
+
generateV2AgentYaml,
|
|
84
|
+
buildV2InlineSkill,
|
|
85
|
+
buildV2FlowEvent,
|
|
86
|
+
buildV2StateField,
|
|
87
|
+
type V2InlineSkill,
|
|
88
|
+
} from '../../../format/v2-yaml.js';
|
|
89
|
+
import { isContentDifferent } from '../../../sync/skill-files.js';
|
|
90
|
+
import yaml from 'js-yaml';
|
|
91
|
+
import { patchYamlToPyyaml } from '../../../format/yaml-patch.js';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* V2ProjectSyncStrategy - same API, newo_v2 file layout
|
|
95
|
+
*/
|
|
96
|
+
export class V2ProjectSyncStrategy implements ISyncStrategy<ProjectMeta, LocalProjectData> {
|
|
97
|
+
readonly resourceType = 'projects';
|
|
98
|
+
readonly displayName = 'Projects (newo_v2)';
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
private apiClientFactory: ApiClientFactory,
|
|
102
|
+
private logger: ILogger
|
|
103
|
+
) {}
|
|
104
|
+
|
|
105
|
+
// ──────────────────────────────────────
|
|
106
|
+
// PULL
|
|
107
|
+
// ──────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
async pull(customer: CustomerConfig, options: PullOptions = {}): Promise<PullResult<LocalProjectData>> {
|
|
110
|
+
const client = await this.apiClientFactory(customer, options.verbose ?? false);
|
|
111
|
+
const hashes: HashStore = {};
|
|
112
|
+
const projects: LocalProjectData[] = [];
|
|
113
|
+
|
|
114
|
+
this.logger.verbose(`[newo_v2] Loading project list for customer ${customer.idn}...`);
|
|
115
|
+
|
|
116
|
+
// Use V2 state init (no V1 projects/ dir)
|
|
117
|
+
await ensureStateOnly(customer.idn);
|
|
118
|
+
|
|
119
|
+
// Write import_version.txt marker
|
|
120
|
+
const versionPath = v2ImportVersionPath(customer.idn);
|
|
121
|
+
await writeFileSafe(versionPath, V2_IMPORT_VERSION);
|
|
122
|
+
|
|
123
|
+
// Write V2 customer attributes: attributes.yaml (sorted, with !enum ValueType.X)
|
|
124
|
+
try {
|
|
125
|
+
const custAttrs = await getCustomerAttributes(client, true);
|
|
126
|
+
const attrs = custAttrs.attributes || [];
|
|
127
|
+
if (attrs.length > 0) {
|
|
128
|
+
const attrYaml = formatV2AttributesYaml(attrs);
|
|
129
|
+
const custAttrPath = v2CustomerAttributesPath(customer.idn);
|
|
130
|
+
await writeFileSafe(custAttrPath, attrYaml);
|
|
131
|
+
hashes[custAttrPath] = sha256(attrYaml);
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
this.logger.verbose(` Could not pull customer attributes`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fetch projects from API (same V1 endpoints)
|
|
138
|
+
const apiProjects = options.projectId
|
|
139
|
+
? [{ id: options.projectId, idn: 'unknown', title: 'Project' } as ProjectMeta]
|
|
140
|
+
: await listProjects(client);
|
|
141
|
+
|
|
142
|
+
if (apiProjects.length === 0) {
|
|
143
|
+
this.logger.info(`No projects found for customer ${customer.idn}`);
|
|
144
|
+
return { items: [], count: 0, hashes: {} };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Load existing map for reference
|
|
148
|
+
let existingMap: ProjectMap = { projects: {} };
|
|
149
|
+
const mapFile = mapPath(customer.idn);
|
|
150
|
+
if (await fs.pathExists(mapFile)) {
|
|
151
|
+
try {
|
|
152
|
+
const mapData = await fs.readJson(mapFile);
|
|
153
|
+
if (mapData && typeof mapData === 'object' && 'projects' in mapData) {
|
|
154
|
+
existingMap = mapData as ProjectMap;
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Start fresh
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Count total skills for progress
|
|
162
|
+
let totalSkills = 0;
|
|
163
|
+
let processedSkills = 0;
|
|
164
|
+
|
|
165
|
+
for (const project of apiProjects) {
|
|
166
|
+
const agents = await listAgents(client, project.id);
|
|
167
|
+
for (const agent of agents) {
|
|
168
|
+
const flows = agent.flows || [];
|
|
169
|
+
for (const flow of flows) {
|
|
170
|
+
const skills = await listFlowSkills(client, flow.id);
|
|
171
|
+
totalSkills += skills.length;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.logger.verbose(`[newo_v2] Total skills to process: ${totalSkills}`);
|
|
177
|
+
|
|
178
|
+
// Process each project
|
|
179
|
+
for (const project of apiProjects) {
|
|
180
|
+
this.logger.verbose(`[newo_v2] Processing project: ${project.title} (${project.idn})`);
|
|
181
|
+
|
|
182
|
+
// Write V2 project YAML: {project_idn}.yaml
|
|
183
|
+
// The API returns registry_idn (not registry) - map to V2 field name
|
|
184
|
+
const projectYaml = generateV2ProjectYaml({
|
|
185
|
+
idn: project.idn,
|
|
186
|
+
name: project.title || project.idn,
|
|
187
|
+
version: (project as any).version || '1.0.0',
|
|
188
|
+
description: project.description || '',
|
|
189
|
+
is_auto_update_enabled: (project as any).is_auto_update_enabled ?? false,
|
|
190
|
+
registry: (project as any).registry_idn || (project as any).registry || '',
|
|
191
|
+
registry_item_idn: (project as any).registry_item_idn || '',
|
|
192
|
+
});
|
|
193
|
+
const projectYamlPath = v2ProjectYamlPath(customer.idn, project.idn);
|
|
194
|
+
await writeFileSafe(projectYamlPath, projectYaml);
|
|
195
|
+
hashes[projectYamlPath] = sha256(projectYaml);
|
|
196
|
+
|
|
197
|
+
const localProject: LocalProjectData = {
|
|
198
|
+
projectId: project.id,
|
|
199
|
+
projectIdn: project.idn,
|
|
200
|
+
metadata: {
|
|
201
|
+
id: project.id,
|
|
202
|
+
idn: project.idn,
|
|
203
|
+
title: project.title,
|
|
204
|
+
description: project.description || '',
|
|
205
|
+
created_at: project.created_at || '',
|
|
206
|
+
updated_at: project.updated_at || '',
|
|
207
|
+
},
|
|
208
|
+
agents: []
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const agents = await listAgents(client, project.id);
|
|
212
|
+
this.logger.verbose(` Found ${agents.length} agents in project ${project.title}`);
|
|
213
|
+
|
|
214
|
+
const projectData: ProjectData = {
|
|
215
|
+
projectId: project.id,
|
|
216
|
+
projectIdn: project.idn,
|
|
217
|
+
agents: {}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Process each agent
|
|
221
|
+
for (const agent of agents) {
|
|
222
|
+
const localAgent = await this.pullAgent(
|
|
223
|
+
client, customer, project, agent, hashes, options, () => {
|
|
224
|
+
processedSkills++;
|
|
225
|
+
if (!options.verbose && totalSkills > 0) {
|
|
226
|
+
if (processedSkills % 10 === 0 || processedSkills === totalSkills) {
|
|
227
|
+
this.logger.progress(processedSkills, totalSkills, '[newo_v2] Processing skills');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
localProject.agents.push(localAgent);
|
|
233
|
+
|
|
234
|
+
// Build project data for map
|
|
235
|
+
projectData.agents[agent.idn] = {
|
|
236
|
+
id: agent.id,
|
|
237
|
+
flows: {}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
for (const flow of localAgent.flows) {
|
|
241
|
+
projectData.agents[agent.idn]!.flows[flow.idn] = {
|
|
242
|
+
id: flow.id,
|
|
243
|
+
skills: {}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
for (const skill of flow.skills) {
|
|
247
|
+
projectData.agents[agent.idn]!.flows[flow.idn]!.skills[skill.idn] = skill.metadata;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Pull libraries for this project
|
|
253
|
+
try {
|
|
254
|
+
const libraries = await listLibraries(client, project.id);
|
|
255
|
+
if (libraries.length > 0) {
|
|
256
|
+
this.logger.verbose(` Found ${libraries.length} libraries in project ${project.idn}`);
|
|
257
|
+
projectData.libraries = {};
|
|
258
|
+
|
|
259
|
+
for (const lib of libraries) {
|
|
260
|
+
await this.pullLibrary(client, customer, project, lib, hashes, options);
|
|
261
|
+
projectData.libraries[lib.idn] = {
|
|
262
|
+
id: lib.id,
|
|
263
|
+
skills: {}
|
|
264
|
+
};
|
|
265
|
+
for (const skill of lib.skills) {
|
|
266
|
+
projectData.libraries[lib.idn]!.skills[skill.idn] = {
|
|
267
|
+
id: skill.id,
|
|
268
|
+
idn: skill.idn,
|
|
269
|
+
title: skill.title,
|
|
270
|
+
runner_type: skill.runner_type,
|
|
271
|
+
model: skill.model,
|
|
272
|
+
parameters: [...skill.parameters],
|
|
273
|
+
path: skill.path
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
this.logger.verbose(` Could not pull libraries for project ${project.idn}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Write V2 project attributes: {project_idn}/attributes.yaml
|
|
283
|
+
try {
|
|
284
|
+
const projAttrs = await getProjectAttributes(client, project.id, true);
|
|
285
|
+
const attrs = projAttrs.attributes || [];
|
|
286
|
+
if (attrs.length > 0) {
|
|
287
|
+
const attrYaml = formatV2AttributesYaml(attrs);
|
|
288
|
+
const attrPath = v2ProjectAttributesPath(customer.idn, project.idn);
|
|
289
|
+
await writeFileSafe(attrPath, attrYaml);
|
|
290
|
+
hashes[attrPath] = sha256(attrYaml);
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
this.logger.verbose(` Could not pull attributes for project ${project.idn}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
existingMap.projects[project.idn] = projectData;
|
|
297
|
+
projects.push(localProject);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Write AKB stub files for all agents: akb/{AgentIdn}.yaml
|
|
301
|
+
// V2 format creates an empty [] file for every agent persona
|
|
302
|
+
const akbDirPath = v2AkbDir(customer.idn);
|
|
303
|
+
await fs.ensureDir(akbDirPath);
|
|
304
|
+
for (const project of projects) {
|
|
305
|
+
for (const agent of project.agents) {
|
|
306
|
+
const akbFilePath = v2AkbPath(customer.idn, agent.idn);
|
|
307
|
+
if (!(await fs.pathExists(akbFilePath))) {
|
|
308
|
+
await writeFileSafe(akbFilePath, '[]\n');
|
|
309
|
+
}
|
|
310
|
+
// Don't overwrite existing AKB files that may have content from AkbSyncStrategy
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Save updated project map
|
|
315
|
+
await writeFileSafe(mapFile, JSON.stringify(existingMap, null, 2));
|
|
316
|
+
|
|
317
|
+
// Save hashes
|
|
318
|
+
await saveHashes(hashes, customer.idn);
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
items: projects,
|
|
322
|
+
count: projects.length,
|
|
323
|
+
hashes
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Pull a single agent in V2 format
|
|
329
|
+
*/
|
|
330
|
+
private async pullAgent(
|
|
331
|
+
client: AxiosInstance,
|
|
332
|
+
customer: CustomerConfig,
|
|
333
|
+
project: ProjectMeta,
|
|
334
|
+
agent: Agent,
|
|
335
|
+
hashes: HashStore,
|
|
336
|
+
options: PullOptions,
|
|
337
|
+
onSkillProcessed: () => void
|
|
338
|
+
): Promise<LocalAgentData> {
|
|
339
|
+
this.logger.verbose(` [newo_v2] Processing agent: ${agent.title} (${agent.idn})`);
|
|
340
|
+
|
|
341
|
+
// Write V2 agent YAML: agents/{AgentIdn}/agent.yaml
|
|
342
|
+
// Preserve exact API values (null title stays null, "" description stays "")
|
|
343
|
+
const agentYaml = generateV2AgentYaml({
|
|
344
|
+
idn: agent.idn,
|
|
345
|
+
title: agent.title ?? null,
|
|
346
|
+
description: agent.description ?? null,
|
|
347
|
+
});
|
|
348
|
+
const agentYamlFilePath = v2AgentYamlPath(customer.idn, project.idn, agent.idn);
|
|
349
|
+
await writeFileSafe(agentYamlFilePath, agentYaml);
|
|
350
|
+
hashes[agentYamlFilePath] = sha256(agentYaml);
|
|
351
|
+
|
|
352
|
+
const localAgent: LocalAgentData = {
|
|
353
|
+
id: agent.id,
|
|
354
|
+
idn: agent.idn,
|
|
355
|
+
metadata: {
|
|
356
|
+
id: agent.id,
|
|
357
|
+
idn: agent.idn,
|
|
358
|
+
title: agent.title || '',
|
|
359
|
+
description: agent.description || '',
|
|
360
|
+
},
|
|
361
|
+
flows: []
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const flows = agent.flows || [];
|
|
365
|
+
this.logger.verbose(` Found ${flows.length} flows in agent ${agent.title}`);
|
|
366
|
+
|
|
367
|
+
for (const flow of flows) {
|
|
368
|
+
const localFlow = await this.pullFlow(
|
|
369
|
+
client, customer, project, agent, flow, hashes, options, onSkillProcessed
|
|
370
|
+
);
|
|
371
|
+
localAgent.flows.push(localFlow);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return localAgent;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Pull a single flow in V2 format
|
|
379
|
+
*
|
|
380
|
+
* In V2, the flow YAML contains inline skill definitions, events, and state_fields.
|
|
381
|
+
* Skills are written to flows/{FlowIdn}/skills/{SkillIdn}.nsl|.nslg
|
|
382
|
+
*/
|
|
383
|
+
private async pullFlow(
|
|
384
|
+
client: AxiosInstance,
|
|
385
|
+
customer: CustomerConfig,
|
|
386
|
+
project: ProjectMeta,
|
|
387
|
+
agent: Agent,
|
|
388
|
+
flow: Flow,
|
|
389
|
+
hashes: HashStore,
|
|
390
|
+
options: PullOptions,
|
|
391
|
+
onSkillProcessed: () => void
|
|
392
|
+
): Promise<LocalFlowData> {
|
|
393
|
+
this.logger.verbose(` [newo_v2] Processing flow: ${flow.title} (${flow.idn})`);
|
|
394
|
+
|
|
395
|
+
// Get flow events and states
|
|
396
|
+
const [events, states] = await Promise.all([
|
|
397
|
+
listFlowEvents(client, flow.id).catch(() => [] as FlowEvent[]),
|
|
398
|
+
listFlowStates(client, flow.id).catch(() => [] as FlowState[])
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
// Process skills
|
|
402
|
+
const skills = await listFlowSkills(client, flow.id);
|
|
403
|
+
this.logger.verbose(` Found ${skills.length} skills in flow ${flow.title}`);
|
|
404
|
+
|
|
405
|
+
// Build V2 inline skill definitions
|
|
406
|
+
const v2Skills: V2InlineSkill[] = [];
|
|
407
|
+
const localFlow: LocalFlowData = {
|
|
408
|
+
id: flow.id,
|
|
409
|
+
idn: flow.idn,
|
|
410
|
+
metadata: {
|
|
411
|
+
id: flow.id,
|
|
412
|
+
idn: flow.idn,
|
|
413
|
+
title: flow.title,
|
|
414
|
+
description: flow.description || '',
|
|
415
|
+
default_runner_type: flow.default_runner_type,
|
|
416
|
+
default_model: flow.default_model,
|
|
417
|
+
events,
|
|
418
|
+
state_fields: states
|
|
419
|
+
},
|
|
420
|
+
skills: []
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
for (const skill of skills) {
|
|
424
|
+
const localSkill = await this.pullSkill(
|
|
425
|
+
customer, project, agent, flow, skill, hashes, options
|
|
426
|
+
);
|
|
427
|
+
localFlow.skills.push(localSkill);
|
|
428
|
+
onSkillProcessed();
|
|
429
|
+
|
|
430
|
+
// Build inline skill definition for flow YAML
|
|
431
|
+
const relPath = v2SkillRelativePath(flow.idn, skill.idn, skill.runner_type);
|
|
432
|
+
v2Skills.push(buildV2InlineSkill(
|
|
433
|
+
skill.idn,
|
|
434
|
+
skill.title || '',
|
|
435
|
+
skill.runner_type,
|
|
436
|
+
skill.model?.model_idn || flow.default_model?.model_idn || '',
|
|
437
|
+
skill.model?.provider_idn || flow.default_model?.provider_idn || '',
|
|
438
|
+
skill.parameters.map(p => ({
|
|
439
|
+
name: p.name,
|
|
440
|
+
default_value: p.default_value ?? '',
|
|
441
|
+
})),
|
|
442
|
+
relPath
|
|
443
|
+
));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Build V2 events
|
|
447
|
+
const v2Events = events.map(e => buildV2FlowEvent(
|
|
448
|
+
e.idn,
|
|
449
|
+
e.skill_selector || 'skill_idn',
|
|
450
|
+
e.skill_idn || null,
|
|
451
|
+
e.state_idn || null,
|
|
452
|
+
e.integration_idn || null,
|
|
453
|
+
e.connector_idn || null,
|
|
454
|
+
e.interrupt_mode || 'queue'
|
|
455
|
+
));
|
|
456
|
+
|
|
457
|
+
// Build V2 state fields
|
|
458
|
+
const v2States = states.map(s => buildV2StateField(
|
|
459
|
+
s.idn,
|
|
460
|
+
s.title || '',
|
|
461
|
+
s.default_value ?? '',
|
|
462
|
+
s.scope || 'user'
|
|
463
|
+
));
|
|
464
|
+
|
|
465
|
+
// Write V2 flow YAML: flows/{FlowIdn}/{FlowIdn}.yaml
|
|
466
|
+
const flowYaml = generateV2FlowYaml(
|
|
467
|
+
flow.idn,
|
|
468
|
+
flow.title || flow.idn,
|
|
469
|
+
flow.description ?? null,
|
|
470
|
+
flow.default_runner_type || 'guidance',
|
|
471
|
+
flow.default_model?.provider_idn || '',
|
|
472
|
+
flow.default_model?.model_idn || '',
|
|
473
|
+
v2Skills,
|
|
474
|
+
v2Events,
|
|
475
|
+
v2States
|
|
476
|
+
);
|
|
477
|
+
const flowYamlFilePath = v2FlowYamlPath(customer.idn, project.idn, agent.idn, flow.idn);
|
|
478
|
+
await writeFileSafe(flowYamlFilePath, flowYaml);
|
|
479
|
+
hashes[flowYamlFilePath] = sha256(flowYaml);
|
|
480
|
+
|
|
481
|
+
return localFlow;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Pull a single skill script in V2 format
|
|
486
|
+
*
|
|
487
|
+
* Script goes to: flows/{FlowIdn}/skills/{SkillIdn}.nsl|.nslg
|
|
488
|
+
* No separate metadata.yaml - metadata is inline in the flow YAML
|
|
489
|
+
*/
|
|
490
|
+
private async pullSkill(
|
|
491
|
+
customer: CustomerConfig,
|
|
492
|
+
project: ProjectMeta,
|
|
493
|
+
agent: Agent,
|
|
494
|
+
flow: Flow,
|
|
495
|
+
skill: Skill,
|
|
496
|
+
hashes: HashStore,
|
|
497
|
+
options: PullOptions
|
|
498
|
+
): Promise<LocalSkillData> {
|
|
499
|
+
this.logger.verbose(` [newo_v2] Processing skill: ${skill.title} (${skill.idn})`);
|
|
500
|
+
|
|
501
|
+
const scriptContent = skill.prompt_script || '';
|
|
502
|
+
const targetPath = v2SkillScriptPath(
|
|
503
|
+
customer.idn, project.idn, agent.idn, flow.idn, skill.idn, skill.runner_type
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// Check for existing file and handle overwrites
|
|
507
|
+
let shouldWrite = true;
|
|
508
|
+
if (await fs.pathExists(targetPath)) {
|
|
509
|
+
const existingContent = await fs.readFile(targetPath, 'utf8');
|
|
510
|
+
if (!isContentDifferent(existingContent, scriptContent)) {
|
|
511
|
+
shouldWrite = false;
|
|
512
|
+
hashes[targetPath] = sha256(scriptContent);
|
|
513
|
+
} else if (!options.silentOverwrite) {
|
|
514
|
+
// In non-silent mode, we overwrite (interactive mode handled in CLI layer)
|
|
515
|
+
shouldWrite = true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (shouldWrite) {
|
|
520
|
+
await writeFileSafe(targetPath, scriptContent);
|
|
521
|
+
hashes[targetPath] = sha256(scriptContent);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const skillMeta: SkillMetadata = {
|
|
525
|
+
id: skill.id,
|
|
526
|
+
idn: skill.idn,
|
|
527
|
+
title: skill.title,
|
|
528
|
+
runner_type: skill.runner_type,
|
|
529
|
+
model: skill.model,
|
|
530
|
+
parameters: [...skill.parameters],
|
|
531
|
+
path: skill.path
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
id: skill.id,
|
|
536
|
+
idn: skill.idn,
|
|
537
|
+
metadata: skillMeta,
|
|
538
|
+
scriptPath: targetPath,
|
|
539
|
+
scriptContent
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Pull a library and its skills in V2 format
|
|
545
|
+
*
|
|
546
|
+
* Writes:
|
|
547
|
+
* {project}/libraries/{lib}/{lib}.yaml (with inline skill list)
|
|
548
|
+
* {project}/libraries/{lib}/skills/{skill}.nsl|.nslg
|
|
549
|
+
*/
|
|
550
|
+
private async pullLibrary(
|
|
551
|
+
_client: AxiosInstance,
|
|
552
|
+
customer: CustomerConfig,
|
|
553
|
+
project: ProjectMeta,
|
|
554
|
+
lib: LibraryResponse,
|
|
555
|
+
hashes: HashStore,
|
|
556
|
+
_options: PullOptions
|
|
557
|
+
): Promise<void> {
|
|
558
|
+
this.logger.verbose(` [newo_v2] Processing library: ${lib.idn} (${lib.skills.length} skills)`);
|
|
559
|
+
|
|
560
|
+
// Build V2 inline skill definitions for library YAML
|
|
561
|
+
const v2Skills: V2InlineSkill[] = [];
|
|
562
|
+
for (const skill of lib.skills) {
|
|
563
|
+
const relPath = v2LibrarySkillRelativePath(project.idn, lib.idn, skill.idn, skill.runner_type);
|
|
564
|
+
v2Skills.push(buildV2InlineSkill(
|
|
565
|
+
skill.idn,
|
|
566
|
+
skill.title || '',
|
|
567
|
+
skill.runner_type,
|
|
568
|
+
skill.model?.model_idn || '',
|
|
569
|
+
skill.model?.provider_idn || '',
|
|
570
|
+
skill.parameters.map(p => ({
|
|
571
|
+
name: p.name,
|
|
572
|
+
default_value: p.default_value ?? '',
|
|
573
|
+
})),
|
|
574
|
+
relPath
|
|
575
|
+
));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Sort skills same as flows
|
|
579
|
+
const { sortV2Skills, sortV2Parameters } = await import('../../../format/v2-yaml.js');
|
|
580
|
+
const sortedSkills = sortV2Skills(v2Skills).map(s => ({
|
|
581
|
+
...s,
|
|
582
|
+
parameters: sortV2Parameters(s.parameters),
|
|
583
|
+
}));
|
|
584
|
+
|
|
585
|
+
// Write library YAML: libraries/{lib}/{lib}.yaml
|
|
586
|
+
const libDef = {
|
|
587
|
+
library: {
|
|
588
|
+
idn: lib.idn,
|
|
589
|
+
skills: sortedSkills,
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
const libYaml = yaml.dump(libDef, { indent: 2, lineWidth: -1, noRefs: true, sortKeys: false });
|
|
593
|
+
const libYamlPath = v2LibraryYamlPath(customer.idn, project.idn, lib.idn);
|
|
594
|
+
await writeFileSafe(libYamlPath, libYaml);
|
|
595
|
+
hashes[libYamlPath] = sha256(libYaml);
|
|
596
|
+
|
|
597
|
+
// Write skill scripts
|
|
598
|
+
for (const skill of lib.skills) {
|
|
599
|
+
const scriptContent = skill.prompt_script || '';
|
|
600
|
+
const scriptPath = v2LibrarySkillScriptPath(
|
|
601
|
+
customer.idn, project.idn, lib.idn, skill.idn, skill.runner_type
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
let shouldWrite = true;
|
|
605
|
+
if (await fs.pathExists(scriptPath)) {
|
|
606
|
+
const existing = await fs.readFile(scriptPath, 'utf8');
|
|
607
|
+
if (!isContentDifferent(existing, scriptContent)) {
|
|
608
|
+
shouldWrite = false;
|
|
609
|
+
hashes[scriptPath] = sha256(scriptContent);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (shouldWrite) {
|
|
614
|
+
await writeFileSafe(scriptPath, scriptContent);
|
|
615
|
+
hashes[scriptPath] = sha256(scriptContent);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ──────────────────────────────────────
|
|
621
|
+
// PUSH
|
|
622
|
+
// ──────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
async push(customer: CustomerConfig, changes?: ChangeItem<LocalProjectData>[]): Promise<PushResult> {
|
|
625
|
+
const result: PushResult = { created: 0, updated: 0, deleted: 0, errors: [] };
|
|
626
|
+
|
|
627
|
+
if (!changes) {
|
|
628
|
+
changes = await this.getChanges(customer);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (changes.length === 0) {
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const client = await this.apiClientFactory(customer, false);
|
|
636
|
+
const existingHashes = await loadHashes(customer.idn);
|
|
637
|
+
const newHashes = { ...existingHashes };
|
|
638
|
+
|
|
639
|
+
// Load project map
|
|
640
|
+
const mapFile = mapPath(customer.idn);
|
|
641
|
+
if (!(await fs.pathExists(mapFile))) {
|
|
642
|
+
result.errors.push('No project map found. Run pull first.');
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const mapData = await fs.readJson(mapFile) as ProjectMap;
|
|
647
|
+
|
|
648
|
+
for (const change of changes) {
|
|
649
|
+
try {
|
|
650
|
+
if (change.operation === 'modified') {
|
|
651
|
+
// Detect if this is a library skill or flow skill by path
|
|
652
|
+
const isLibrary = change.path.includes('/libraries/');
|
|
653
|
+
const count = isLibrary
|
|
654
|
+
? await this.pushV2LibrarySkillUpdate(client, change, mapData, newHashes)
|
|
655
|
+
: await this.pushV2SkillUpdate(client, change, mapData, newHashes);
|
|
656
|
+
result.updated += count;
|
|
657
|
+
}
|
|
658
|
+
} catch (error) {
|
|
659
|
+
result.errors.push(
|
|
660
|
+
`Failed to push ${change.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
await saveHashes(newHashes, customer.idn);
|
|
666
|
+
|
|
667
|
+
if (result.created > 0 || result.updated > 0) {
|
|
668
|
+
await this.publishAllFlows(client, mapData);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Push a V2 skill update
|
|
676
|
+
*
|
|
677
|
+
* V2 path: newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skill}.nsl
|
|
678
|
+
*/
|
|
679
|
+
private async pushV2SkillUpdate(
|
|
680
|
+
client: AxiosInstance,
|
|
681
|
+
change: ChangeItem<LocalProjectData>,
|
|
682
|
+
mapData: ProjectMap,
|
|
683
|
+
newHashes: HashStore
|
|
684
|
+
): Promise<number> {
|
|
685
|
+
// Parse V2 path to extract entity hierarchy
|
|
686
|
+
// Path: .../newo_customers/{cust}/{proj}/agents/{agent}/flows/{flow}/skills/{skillFile}
|
|
687
|
+
const pathParts = change.path.split('/');
|
|
688
|
+
const skillFileName = pathParts[pathParts.length - 1] || '';
|
|
689
|
+
const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
|
|
690
|
+
// skills/ -> flow/ -> flows/ -> agent/ -> agents/ -> project/
|
|
691
|
+
const flowIdn = pathParts[pathParts.length - 3] || '';
|
|
692
|
+
const agentIdn = pathParts[pathParts.length - 5] || '';
|
|
693
|
+
const projectIdn = pathParts[pathParts.length - 7] || '';
|
|
694
|
+
|
|
695
|
+
// Look up skill in map
|
|
696
|
+
const projectData = mapData.projects[projectIdn];
|
|
697
|
+
const agentData = projectData?.agents[agentIdn];
|
|
698
|
+
const flowData = agentData?.flows[flowIdn];
|
|
699
|
+
const skillData = flowData?.skills[skillIdn];
|
|
700
|
+
|
|
701
|
+
if (!skillData) {
|
|
702
|
+
throw new Error(`Skill ${skillIdn} not found in project map (path: ${change.path})`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Read updated script content
|
|
706
|
+
const content = await fs.readFile(change.path, 'utf8');
|
|
707
|
+
|
|
708
|
+
// Update via V1 API
|
|
709
|
+
await updateSkill(client, {
|
|
710
|
+
id: skillData.id,
|
|
711
|
+
title: skillData.title,
|
|
712
|
+
idn: skillData.idn,
|
|
713
|
+
prompt_script: content,
|
|
714
|
+
runner_type: skillData.runner_type,
|
|
715
|
+
model: skillData.model,
|
|
716
|
+
parameters: skillData.parameters,
|
|
717
|
+
path: skillData.path
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
newHashes[change.path] = sha256(content);
|
|
721
|
+
this.logger.info(`[newo_v2] Pushed: ${skillIdn}`);
|
|
722
|
+
return 1;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Push a V2 library skill update
|
|
727
|
+
* Path: .../newo_customers/{cust}/{proj}/libraries/{lib}/skills/{skillFile}
|
|
728
|
+
*/
|
|
729
|
+
private async pushV2LibrarySkillUpdate(
|
|
730
|
+
client: AxiosInstance,
|
|
731
|
+
change: ChangeItem<LocalProjectData>,
|
|
732
|
+
mapData: ProjectMap,
|
|
733
|
+
newHashes: HashStore
|
|
734
|
+
): Promise<number> {
|
|
735
|
+
const pathParts = change.path.split('/');
|
|
736
|
+
const skillFileName = pathParts[pathParts.length - 1] || '';
|
|
737
|
+
const skillIdn = skillFileName.replace(/\.(nsl|nslg|jinja|guidance)$/, '');
|
|
738
|
+
// skills/ -> lib/ -> libraries/ -> project/
|
|
739
|
+
const libIdn = pathParts[pathParts.length - 3] || '';
|
|
740
|
+
const projectIdn = pathParts[pathParts.length - 5] || '';
|
|
741
|
+
|
|
742
|
+
const projectData = mapData.projects[projectIdn];
|
|
743
|
+
const libData = projectData?.libraries?.[libIdn];
|
|
744
|
+
const skillData = libData?.skills[skillIdn];
|
|
745
|
+
|
|
746
|
+
if (!skillData || !libData) {
|
|
747
|
+
throw new Error(`Library skill ${skillIdn} not found in project map (path: ${change.path})`);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const content = await fs.readFile(change.path, 'utf8');
|
|
751
|
+
|
|
752
|
+
await updateLibrarySkill(client, libData.id, skillData.id, {
|
|
753
|
+
prompt_script: content,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
newHashes[change.path] = sha256(content);
|
|
757
|
+
this.logger.info(`[newo_v2] Pushed library skill: ${libIdn}/${skillIdn}`);
|
|
758
|
+
return 1;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Publish all flows
|
|
763
|
+
*/
|
|
764
|
+
private async publishAllFlows(client: AxiosInstance, mapData: ProjectMap): Promise<void> {
|
|
765
|
+
for (const projectData of Object.values(mapData.projects)) {
|
|
766
|
+
for (const agentData of Object.values(projectData.agents)) {
|
|
767
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
768
|
+
if (flowData.id) {
|
|
769
|
+
try {
|
|
770
|
+
await publishFlow(client, flowData.id, {
|
|
771
|
+
version: '1.0',
|
|
772
|
+
description: 'Published via NEWO CLI (newo_v2)',
|
|
773
|
+
type: 'public'
|
|
774
|
+
});
|
|
775
|
+
this.logger.verbose(`[newo_v2] Published flow: ${flowIdn}`);
|
|
776
|
+
} catch {
|
|
777
|
+
this.logger.warn(`[newo_v2] Failed to publish flow ${flowIdn}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ──────────────────────────────────────
|
|
786
|
+
// STATUS / CHANGES
|
|
787
|
+
// ──────────────────────────────────────
|
|
788
|
+
|
|
789
|
+
async getChanges(customer: CustomerConfig): Promise<ChangeItem<LocalProjectData>[]> {
|
|
790
|
+
const changes: ChangeItem<LocalProjectData>[] = [];
|
|
791
|
+
|
|
792
|
+
const mapFile = mapPath(customer.idn);
|
|
793
|
+
if (!(await fs.pathExists(mapFile))) {
|
|
794
|
+
return changes;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const hashes = await loadHashes(customer.idn);
|
|
798
|
+
const mapData = await fs.readJson(mapFile) as ProjectMap;
|
|
799
|
+
|
|
800
|
+
// Scan V2 directory structure for changed skill scripts
|
|
801
|
+
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
802
|
+
// Flow skills
|
|
803
|
+
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
804
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
805
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
|
|
806
|
+
const scriptPath = v2SkillScriptPath(
|
|
807
|
+
customer.idn, projectIdn, agentIdn, flowIdn, skillIdn,
|
|
808
|
+
skillMeta.runner_type
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
if (await fs.pathExists(scriptPath)) {
|
|
812
|
+
const content = await fs.readFile(scriptPath, 'utf8');
|
|
813
|
+
const currentHash = sha256(content);
|
|
814
|
+
const storedHash = hashes[scriptPath];
|
|
815
|
+
|
|
816
|
+
if (storedHash !== currentHash) {
|
|
817
|
+
changes.push({
|
|
818
|
+
item: {} as LocalProjectData,
|
|
819
|
+
operation: 'modified',
|
|
820
|
+
path: scriptPath
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Library skills
|
|
829
|
+
if (projectData.libraries) {
|
|
830
|
+
for (const [libIdn, libData] of Object.entries(projectData.libraries)) {
|
|
831
|
+
for (const [skillIdn, skillMeta] of Object.entries(libData.skills)) {
|
|
832
|
+
const scriptPath = v2LibrarySkillScriptPath(
|
|
833
|
+
customer.idn, projectIdn, libIdn, skillIdn,
|
|
834
|
+
skillMeta.runner_type
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
if (await fs.pathExists(scriptPath)) {
|
|
838
|
+
const content = await fs.readFile(scriptPath, 'utf8');
|
|
839
|
+
const currentHash = sha256(content);
|
|
840
|
+
const storedHash = hashes[scriptPath];
|
|
841
|
+
|
|
842
|
+
if (storedHash !== currentHash) {
|
|
843
|
+
changes.push({
|
|
844
|
+
item: {} as LocalProjectData,
|
|
845
|
+
operation: 'modified',
|
|
846
|
+
path: scriptPath
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return changes;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async validate(customer: CustomerConfig, _items: LocalProjectData[]): Promise<ValidationResult> {
|
|
859
|
+
const errors: ValidationError[] = [];
|
|
860
|
+
|
|
861
|
+
const mapFile = mapPath(customer.idn);
|
|
862
|
+
if (!(await fs.pathExists(mapFile))) {
|
|
863
|
+
errors.push({
|
|
864
|
+
field: 'projectMap',
|
|
865
|
+
message: 'No project map found. Run pull first.'
|
|
866
|
+
});
|
|
867
|
+
return { valid: false, errors };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const mapData = await fs.readJson(mapFile) as ProjectMap;
|
|
871
|
+
|
|
872
|
+
// Validate V2 skill files exist
|
|
873
|
+
for (const [projectIdn, projectData] of Object.entries(mapData.projects)) {
|
|
874
|
+
for (const [agentIdn, agentData] of Object.entries(projectData.agents)) {
|
|
875
|
+
for (const [flowIdn, flowData] of Object.entries(agentData.flows)) {
|
|
876
|
+
for (const [skillIdn, skillMeta] of Object.entries(flowData.skills)) {
|
|
877
|
+
const scriptPath = v2SkillScriptPath(
|
|
878
|
+
customer.idn, projectIdn, agentIdn, flowIdn, skillIdn,
|
|
879
|
+
skillMeta.runner_type
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
if (!(await fs.pathExists(scriptPath))) {
|
|
883
|
+
errors.push({
|
|
884
|
+
field: `skill.${skillIdn}`,
|
|
885
|
+
message: `Script file not found: ${scriptPath}`,
|
|
886
|
+
path: scriptPath
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return { valid: errors.length === 0, errors };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async getStatus(customer: CustomerConfig): Promise<StatusSummary> {
|
|
898
|
+
const changes = await this.getChanges(customer);
|
|
899
|
+
|
|
900
|
+
return {
|
|
901
|
+
resourceType: this.resourceType,
|
|
902
|
+
displayName: this.displayName,
|
|
903
|
+
changedCount: changes.length,
|
|
904
|
+
changes: changes.map(c => ({
|
|
905
|
+
path: c.path,
|
|
906
|
+
operation: c.operation
|
|
907
|
+
}))
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ── V2 Attributes Formatting ──
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Map V1 API value_type to V2 export format
|
|
916
|
+
* V1 API: "string", "bool", "AttributeValueTypes.string", etc.
|
|
917
|
+
* V2 export: "ValueType.STRING", "ValueType.BOOL", etc.
|
|
918
|
+
*/
|
|
919
|
+
function toV2ValueType(apiValueType: string): string {
|
|
920
|
+
// Already in V2 format
|
|
921
|
+
if (apiValueType.startsWith('ValueType.')) return apiValueType;
|
|
922
|
+
|
|
923
|
+
// Strip AttributeValueTypes. prefix if present
|
|
924
|
+
const raw = apiValueType.replace(/^AttributeValueTypes\./, '');
|
|
925
|
+
|
|
926
|
+
const mapping: Record<string, string> = {
|
|
927
|
+
'string': 'ValueType.STRING',
|
|
928
|
+
'bool': 'ValueType.BOOL',
|
|
929
|
+
'number': 'ValueType.NUMBER',
|
|
930
|
+
'enum': 'ValueType.ENUM',
|
|
931
|
+
'json': 'ValueType.JSON',
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
return mapping[raw.toLowerCase()] || `ValueType.${raw.toUpperCase()}`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Format attributes as V2 YAML with:
|
|
939
|
+
* - Sorted by idn alphabetically
|
|
940
|
+
* - value_type as !enum "ValueType.X"
|
|
941
|
+
* - Proper quoting
|
|
942
|
+
*/
|
|
943
|
+
function formatV2AttributesYaml(attrs: Array<{
|
|
944
|
+
idn: string;
|
|
945
|
+
value: any;
|
|
946
|
+
title?: string | undefined;
|
|
947
|
+
description?: string | undefined;
|
|
948
|
+
group?: string | undefined;
|
|
949
|
+
is_hidden?: boolean | undefined;
|
|
950
|
+
possible_values?: any[] | undefined;
|
|
951
|
+
value_type?: string | undefined;
|
|
952
|
+
}>): string {
|
|
953
|
+
// Sort alphabetically by idn
|
|
954
|
+
const sorted = [...attrs].sort((a, b) => a.idn.localeCompare(b.idn));
|
|
955
|
+
|
|
956
|
+
const cleaned = sorted.map(a => ({
|
|
957
|
+
idn: a.idn,
|
|
958
|
+
value: a.value,
|
|
959
|
+
title: a.title || '',
|
|
960
|
+
description: a.description || '',
|
|
961
|
+
group: a.group || '',
|
|
962
|
+
is_hidden: a.is_hidden ?? false,
|
|
963
|
+
possible_values: a.possible_values || [],
|
|
964
|
+
value_type: new V2EnumValue(toV2ValueType(a.value_type || 'string')),
|
|
965
|
+
}));
|
|
966
|
+
|
|
967
|
+
const enumType = new yaml.Type('!enum', {
|
|
968
|
+
kind: 'scalar',
|
|
969
|
+
instanceOf: V2EnumValue,
|
|
970
|
+
resolve: () => true,
|
|
971
|
+
construct: (data: string) => new V2EnumValue(data),
|
|
972
|
+
represent: (data: unknown) => data instanceof V2EnumValue ? data.value : String(data),
|
|
973
|
+
});
|
|
974
|
+
const schema = yaml.DEFAULT_SCHEMA.extend([enumType]);
|
|
975
|
+
|
|
976
|
+
// Use lineWidth: -1 to prevent folding multiline strings (preserve |- literal block style)
|
|
977
|
+
const rawYaml = yaml.dump({ attributes: cleaned }, {
|
|
978
|
+
indent: 2,
|
|
979
|
+
quotingType: '"',
|
|
980
|
+
forceQuotes: false,
|
|
981
|
+
lineWidth: -1,
|
|
982
|
+
noRefs: true,
|
|
983
|
+
sortKeys: false,
|
|
984
|
+
schema,
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// Fix !enum quoting: js-yaml outputs `!enum ValueType.STRING` but V2 ZIP uses `!enum "ValueType.STRING"`
|
|
988
|
+
const enumFixed = rawYaml.replace(/!enum (\S+)/g, '!enum "$1"');
|
|
989
|
+
|
|
990
|
+
// Patch long-line wrapping to match pyyaml style
|
|
991
|
+
return patchYamlToPyyaml(enumFixed);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Wrapper class for !enum YAML tag */
|
|
995
|
+
class V2EnumValue {
|
|
996
|
+
constructor(public value: string) {}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Factory function for creating V2ProjectSyncStrategy
|
|
1001
|
+
*/
|
|
1002
|
+
export function createV2ProjectSyncStrategy(
|
|
1003
|
+
apiClientFactory: ApiClientFactory,
|
|
1004
|
+
logger: ILogger
|
|
1005
|
+
): V2ProjectSyncStrategy {
|
|
1006
|
+
return new V2ProjectSyncStrategy(apiClientFactory, logger);
|
|
1007
|
+
}
|