@nlxai/cli 1.2.4-alpha.9 → 1.2.6
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/README.md +20 -0
- package/lib/commands/code/index.js +9 -0
- package/lib/commands/code/init.js +42 -0
- package/lib/commands/code/pull.js +86 -0
- package/lib/commands/code/push.js +55 -0
- package/lib/commands/data-requests/sync.js +1 -1
- package/lib/commands/http.js +5 -16
- package/lib/index.js +2 -0
- package/lib/utils/index.js +11 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -41,6 +41,10 @@ Commands:
|
|
|
41
41
|
data-requests Data Requests
|
|
42
42
|
data-requests sync [opts] <input> Sync Data Requests from an OpenAPI Specification or Swagger
|
|
43
43
|
test [options] <applicationId> Run conversation tests for a given application ID
|
|
44
|
+
code Work with code nodes in NLX flows
|
|
45
|
+
code init Initialize local folders for flows with code nodes
|
|
46
|
+
code pull [options] Pull code nodes from a flow into local files
|
|
47
|
+
code push [options] Push local code node files to NLX flow
|
|
44
48
|
http [options] <method> <path> [body] Perform an authenticated request to the management API
|
|
45
49
|
help [command] display help for command
|
|
46
50
|
|
|
@@ -94,6 +98,22 @@ Transcript:
|
|
|
94
98
|
└───────┘
|
|
95
99
|
|
|
96
100
|
Debug at: https://dev.platform.nlx.ai/flows/SimpleCarousel
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
> nlx code init
|
|
104
|
+
ℹ Fetching available flows...
|
|
105
|
+
✔ Select flows to initialize: ChattyKathy
|
|
106
|
+
✔ Created folder ChattyKathy
|
|
107
|
+
ℹ Fetching flow from conversationTrees/ChattyKathy-Omni
|
|
108
|
+
✔ Created/Updated 74af8e42-4c30-4ce0-aaec-bb2b155f2f95.js
|
|
109
|
+
|
|
110
|
+
> cd ChattyKathy
|
|
111
|
+
|
|
112
|
+
> echo "export default async (input, system) => { console.log('Hello world'); }" > 74af8e42-4c30-4ce0-aaec-bb2b155f2f95.js
|
|
113
|
+
|
|
114
|
+
> nlx code push
|
|
115
|
+
✔ Updated code for node 74af8e42-4c30-4ce0-aaec-bb2b155f2f95
|
|
116
|
+
✔ Pushed 1 code node(s) to flow.
|
|
97
117
|
```
|
|
98
118
|
|
|
99
119
|
## Running in CI
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { pullCommand } from "./pull.js";
|
|
3
|
+
import { pushCommand } from "./push.js";
|
|
4
|
+
import { initCommand } from "./init.js";
|
|
5
|
+
export const codeCommand = new Command("code")
|
|
6
|
+
.description("Work with code nodes in NLX flows")
|
|
7
|
+
.addCommand(initCommand)
|
|
8
|
+
.addCommand(pullCommand)
|
|
9
|
+
.addCommand(pushCommand);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { fetchManagementApiPaginated, } from "../../utils/index.js";
|
|
2
|
+
import { consola } from "consola";
|
|
3
|
+
import { checkbox } from "@inquirer/prompts";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import { pull } from "./pull.js";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
export default async function init() {
|
|
8
|
+
consola.info("Fetching available flows...");
|
|
9
|
+
const flows = await fetchManagementApiPaginated("conversationTrees");
|
|
10
|
+
// Filter flows with code nodes
|
|
11
|
+
const codeFlows = new Set();
|
|
12
|
+
for (const flow of flows) {
|
|
13
|
+
if (flow.nodes &&
|
|
14
|
+
Object.values(flow.nodes).some((node) => node.type === "code")) {
|
|
15
|
+
codeFlows.add(flow.intentId);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (codeFlows.size === 0) {
|
|
19
|
+
consola.info("No flows with code nodes found.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Interactive selection
|
|
23
|
+
const selected = await checkbox({
|
|
24
|
+
message: "Select flows to initialize:",
|
|
25
|
+
choices: Array.from(codeFlows).map((f) => ({ value: f, name: f })),
|
|
26
|
+
});
|
|
27
|
+
for (const flowId of selected) {
|
|
28
|
+
if (fs.existsSync(flowId)) {
|
|
29
|
+
consola.info(`Folder ${flowId} already exists. Skipping.`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
fs.mkdirSync(flowId);
|
|
33
|
+
consola.success(`Created folder ${flowId}`);
|
|
34
|
+
// Run pull in new folder
|
|
35
|
+
process.chdir(flowId);
|
|
36
|
+
await pull({ flowId });
|
|
37
|
+
process.chdir("..");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const initCommand = new Command("init")
|
|
41
|
+
.description("Initialize local folders for flows with code nodes")
|
|
42
|
+
.action(init);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { fetchManagementApi } from "../../utils/index.js";
|
|
3
|
+
import { consola } from "consola";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { tmpdir } from "os";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
// Helper: get current folder name
|
|
9
|
+
function getCurrentFolderName() {
|
|
10
|
+
return path.basename(process.cwd());
|
|
11
|
+
}
|
|
12
|
+
export async function pull({ flowId, force, interactive } = {}) {
|
|
13
|
+
const id = flowId || getCurrentFolderName();
|
|
14
|
+
const endpoint = `conversationTrees/${id}-Omni`;
|
|
15
|
+
consola.info(`Fetching flow from ${endpoint}`);
|
|
16
|
+
const flow = await fetchManagementApi(endpoint);
|
|
17
|
+
if (!flow.nodes) {
|
|
18
|
+
consola.error("No nodes found in flow.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
let codeNodeCount = 0;
|
|
22
|
+
for (const nodeId in flow.nodes) {
|
|
23
|
+
const node = flow.nodes[nodeId];
|
|
24
|
+
if (node.type === "code" && node.metadata?.code?.code) {
|
|
25
|
+
codeNodeCount++;
|
|
26
|
+
const fileName = `${nodeId}.js`;
|
|
27
|
+
if (fs.existsSync(fileName)) {
|
|
28
|
+
const existing = fs.readFileSync(fileName, "utf8");
|
|
29
|
+
if (existing.trim() === node.metadata.code.code.trim()) {
|
|
30
|
+
consola.info(`File ${fileName} already up to date.`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (interactive) {
|
|
34
|
+
// Write remote code to temp file
|
|
35
|
+
const tempFile = path.join(tmpdir(), `${nodeId}-remote.js`);
|
|
36
|
+
let content = "";
|
|
37
|
+
if (node.metadata?.name) {
|
|
38
|
+
content += `// ${node.metadata.name}\n`;
|
|
39
|
+
}
|
|
40
|
+
content += node.metadata.code.code;
|
|
41
|
+
if (existing.trim() === content.trim()) {
|
|
42
|
+
consola.info(`File ${fileName} already up to date.`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(tempFile, content);
|
|
46
|
+
const mergeTool = process.env.NLX_CODE_MERGE_TOOL || "vimdiff";
|
|
47
|
+
consola.info(`Launching merge tool (${mergeTool}) for ${fileName}...`);
|
|
48
|
+
try {
|
|
49
|
+
execSync(`${mergeTool} ${fileName} ${tempFile}`, {
|
|
50
|
+
stdio: "inherit",
|
|
51
|
+
});
|
|
52
|
+
consola.success(`Merge completed for ${fileName}`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
consola.error(`Merge tool failed for ${fileName}:`, err);
|
|
56
|
+
}
|
|
57
|
+
fs.unlinkSync(tempFile);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
else if (force) {
|
|
61
|
+
consola.warn(`Overwriting ${fileName} without prompting.`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
consola.error(`File ${fileName} already exists. Skipping.`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let content = "";
|
|
69
|
+
if (node.metadata?.name) {
|
|
70
|
+
content += `// ${node.metadata.name}\n`;
|
|
71
|
+
}
|
|
72
|
+
content += node.metadata.code.code;
|
|
73
|
+
fs.writeFileSync(fileName, content);
|
|
74
|
+
consola.success(`Created/Updated ${fileName}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (codeNodeCount === 0) {
|
|
78
|
+
consola.info("No code nodes found.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export const pullCommand = new Command("pull")
|
|
82
|
+
.description("Pull code nodes from a flow")
|
|
83
|
+
.option("--flow <flowId>", "Specify flow ID (default: current folder name)")
|
|
84
|
+
.option("-f, --force", "Overwrite existing files without prompting", false)
|
|
85
|
+
.option("-i, --interactive", "Use git mergetool for interactive merge", false)
|
|
86
|
+
.action(pull);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fetchManagementApi } from "../../utils/index.js";
|
|
2
|
+
import { consola } from "consola";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
function getCurrentFolderName() {
|
|
6
|
+
return path.basename(process.cwd());
|
|
7
|
+
}
|
|
8
|
+
export async function push({ flowId } = {}) {
|
|
9
|
+
const id = flowId || getCurrentFolderName();
|
|
10
|
+
const endpoint = `conversationTrees/${id}-Omni`;
|
|
11
|
+
const flow = await fetchManagementApi(endpoint);
|
|
12
|
+
if (!flow.nodes) {
|
|
13
|
+
consola.error("No nodes found in flow.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
let updated = 0;
|
|
17
|
+
for (const nodeId in flow.nodes) {
|
|
18
|
+
const node = flow.nodes[nodeId];
|
|
19
|
+
if (node.type === "code" && node.metadata?.code?.code) {
|
|
20
|
+
const fileName = `${nodeId}.js`;
|
|
21
|
+
if (!fs.existsSync(fileName)) {
|
|
22
|
+
consola.warn(`File ${fileName} does not exist. Skipping.`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
let localCode = fs.readFileSync(fileName, "utf8");
|
|
26
|
+
// Remove leading comment if present and matches node name
|
|
27
|
+
const lines = localCode.split("\n");
|
|
28
|
+
if (node.metadata?.name &&
|
|
29
|
+
lines[0].trim() === `// ${node.metadata.name}`) {
|
|
30
|
+
localCode = lines.slice(1).join("\n");
|
|
31
|
+
}
|
|
32
|
+
if (localCode.trim() === node.metadata.code.code.trim()) {
|
|
33
|
+
consola.info(`Code for ${nodeId} is already up to date.`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Update node code
|
|
37
|
+
node.metadata.code.code = localCode;
|
|
38
|
+
updated++;
|
|
39
|
+
consola.success(`Updated code for node ${nodeId}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (updated > 0) {
|
|
43
|
+
// Push updated flow
|
|
44
|
+
await fetchManagementApi(endpoint, "POST", flow);
|
|
45
|
+
consola.success(`Pushed ${updated} code node(s) to flow.`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
consola.info("No code changes to push.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
import { Command } from "commander";
|
|
52
|
+
export const pushCommand = new Command("push")
|
|
53
|
+
.description("Push local code node files to NLX flow")
|
|
54
|
+
.option("--flow <flowId>", "Specify flow ID (default: current folder name)")
|
|
55
|
+
.action(push);
|
|
@@ -261,7 +261,7 @@ The following features are not supported and will be silently ignored:
|
|
|
261
261
|
message: "Open JSON in your editor",
|
|
262
262
|
default: JSON.stringify(dataRequest, null, 2),
|
|
263
263
|
postfix: ".json",
|
|
264
|
-
|
|
264
|
+
waitForUserInput: false,
|
|
265
265
|
validate: (text) => {
|
|
266
266
|
try {
|
|
267
267
|
JSON.parse(text);
|
package/lib/commands/http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
-
import { fetchManagementApi } from "../utils/index.js";
|
|
3
|
+
import { fetchManagementApi, fetchManagementApiPaginated, } from "../utils/index.js";
|
|
4
4
|
import { consola } from "consola";
|
|
5
5
|
export const httpCommand = new Command("http")
|
|
6
6
|
.description("Perform an authenticated request to the management API")
|
|
@@ -38,23 +38,12 @@ export const httpCommand = new Command("http")
|
|
|
38
38
|
process.exit(1);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
let result
|
|
42
|
-
(opts.paginate
|
|
43
|
-
? apiPath.includes("?")
|
|
44
|
-
? "&size=1000"
|
|
45
|
-
: "?size=1000"
|
|
46
|
-
: ""), method.toUpperCase(), body);
|
|
47
|
-
let agg;
|
|
41
|
+
let result;
|
|
48
42
|
if (opts.paginate) {
|
|
49
|
-
|
|
50
|
-
agg = result[key];
|
|
51
|
-
while (result.nextPageId) {
|
|
52
|
-
result = await fetchManagementApi(apiPath + `?pageId=${result.nextPageId}`, method.toUpperCase(), body);
|
|
53
|
-
agg.push(...result[key]);
|
|
54
|
-
}
|
|
43
|
+
result = await fetchManagementApiPaginated(apiPath);
|
|
55
44
|
}
|
|
56
45
|
else {
|
|
57
|
-
|
|
46
|
+
result = await fetchManagementApi(apiPath, method.toUpperCase(), body);
|
|
58
47
|
}
|
|
59
|
-
console.log(JSON.stringify(
|
|
48
|
+
console.log(JSON.stringify(result, null, 2));
|
|
60
49
|
});
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { modalitiesCommand } from "./commands/modalities/index.js";
|
|
|
4
4
|
import { dataRequestsCommand } from "./commands/data-requests/index.js";
|
|
5
5
|
import { httpCommand } from "./commands/http.js";
|
|
6
6
|
import { testCommand } from "./commands/test.js";
|
|
7
|
+
import { codeCommand } from "./commands/code/index.js";
|
|
7
8
|
import { consola } from "consola";
|
|
8
9
|
import { readFileSync } from "fs";
|
|
9
10
|
import { resolve } from "path";
|
|
@@ -22,5 +23,6 @@ program.addCommand(authCommand);
|
|
|
22
23
|
program.addCommand(modalitiesCommand);
|
|
23
24
|
program.addCommand(dataRequestsCommand);
|
|
24
25
|
program.addCommand(testCommand);
|
|
26
|
+
program.addCommand(codeCommand);
|
|
25
27
|
program.addCommand(httpCommand);
|
|
26
28
|
program.parse(process.argv);
|
package/lib/utils/index.js
CHANGED
|
@@ -20,6 +20,17 @@ export const fetchManagementApi = async (path, method = "GET", body) => {
|
|
|
20
20
|
consola.debug("Response:", JSON.stringify(result));
|
|
21
21
|
return result;
|
|
22
22
|
};
|
|
23
|
+
export const fetchManagementApiPaginated = async (path) => {
|
|
24
|
+
let result = await fetchManagementApi(path + (path.includes("?") ? "&size=1000" : "?size=1000"));
|
|
25
|
+
let agg;
|
|
26
|
+
const key = Object.keys(result).filter((k) => k !== "nextPageId")[0];
|
|
27
|
+
agg = result[key];
|
|
28
|
+
while (result.nextPageId) {
|
|
29
|
+
result = await fetchManagementApi(path + `?page=${result.nextPageId}`);
|
|
30
|
+
agg.push(...result[key]);
|
|
31
|
+
}
|
|
32
|
+
return agg;
|
|
33
|
+
};
|
|
23
34
|
export const singleton = (fn) => {
|
|
24
35
|
let running = null;
|
|
25
36
|
return async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nlxai/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "Tools for integrating with NLX apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"NLX",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@inquirer/prompts": "^7.8.4",
|
|
37
|
-
"@nlxai/core": "^1.2.
|
|
37
|
+
"@nlxai/core": "^1.2.6",
|
|
38
38
|
"boxen": "^8.0.1",
|
|
39
39
|
"chalk": "^4.1.0",
|
|
40
40
|
"commander": "^14.0",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"@vitest/ui": "^3.2.4",
|
|
59
59
|
"vitest": "^3.2.4"
|
|
60
60
|
},
|
|
61
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "fb9a0b00ca2184fb69ffd1e11e8f55d30fe1d85e"
|
|
62
62
|
}
|