cisco-axl 2.0.0 → 2.1.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/.claude-plugin/plugin.json +26 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/release.yml +6 -6
- package/README.md +50 -272
- package/claude-hooks.json +30 -0
- package/cli/commands/add.js +15 -5
- package/cli/commands/config.js +17 -23
- package/cli/commands/doctor.js +134 -0
- package/cli/commands/execute.js +15 -5
- package/cli/commands/remove.js +1 -1
- package/cli/commands/sql.js +1 -1
- package/cli/commands/update.js +15 -5
- package/cli/index.js +15 -2
- package/cli/utils/config.js +19 -4
- package/cli/utils/confirm.js +34 -0
- package/cli/utils/connection.js +5 -1
- package/cli/utils/readonly.js +13 -18
- package/cli/utils/stdin.js +20 -0
- package/cli/utils/wordlist.js +9 -0
- package/docs/api.md +191 -0
- package/docs/claude-code-hooks.md +85 -0
- package/docs/cli.md +109 -0
- package/package.json +3 -2
- package/skills/cisco-axl-cli/SKILL.md +61 -11
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { loadConfig, getConfigPath, getConfigDir } = require("../utils/config.js");
|
|
4
|
+
const { resolveConfig } = require("../utils/connection.js");
|
|
5
|
+
|
|
6
|
+
module.exports = function (program) {
|
|
7
|
+
program.command("doctor")
|
|
8
|
+
.description("Check AXL connectivity and configuration health")
|
|
9
|
+
.action(async (opts, command) => {
|
|
10
|
+
const globalOpts = command.optsWithGlobals();
|
|
11
|
+
let passed = 0;
|
|
12
|
+
let warned = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
const ok = (msg) => { console.log(` ✓ ${msg}`); passed++; };
|
|
16
|
+
const warn = (msg) => { console.log(` ⚠ ${msg}`); warned++; };
|
|
17
|
+
const fail = (msg) => { console.log(` ✗ ${msg}`); failed++; };
|
|
18
|
+
|
|
19
|
+
console.log("\n cisco-axl doctor");
|
|
20
|
+
console.log(" " + "─".repeat(50));
|
|
21
|
+
|
|
22
|
+
// 1. Configuration
|
|
23
|
+
console.log("\n Configuration");
|
|
24
|
+
let conn;
|
|
25
|
+
try {
|
|
26
|
+
const data = loadConfig();
|
|
27
|
+
if (!data.activeCluster) {
|
|
28
|
+
fail("No active cluster configured");
|
|
29
|
+
console.log(" Run: cisco-axl config add <name> --host <host> --username <user> --password <pass> --cucm-version <ver>");
|
|
30
|
+
printSummary(passed, warned, failed);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
ok(`Active cluster: ${data.activeCluster}`);
|
|
34
|
+
const cluster = data.clusters[data.activeCluster];
|
|
35
|
+
ok(`Host: ${cluster.host}`);
|
|
36
|
+
ok(`Username: ${cluster.username}`);
|
|
37
|
+
ok(`CUCM version: ${cluster.version}`);
|
|
38
|
+
|
|
39
|
+
if (cluster.insecure) warn("TLS verification: disabled (--insecure)");
|
|
40
|
+
else ok("TLS verification: enabled");
|
|
41
|
+
|
|
42
|
+
conn = await resolveConfig(globalOpts);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
fail(`Config error: ${err.message}`);
|
|
45
|
+
printSummary(passed, warned, failed);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. AXL API connectivity
|
|
50
|
+
console.log("\n AXL API");
|
|
51
|
+
try {
|
|
52
|
+
if (conn.insecure) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; }
|
|
53
|
+
const axlService = require("../../dist/index.js");
|
|
54
|
+
const service = new axlService(conn.host, conn.username, conn.password, conn.version);
|
|
55
|
+
|
|
56
|
+
await service.testAuthentication();
|
|
57
|
+
ok(`AXL API: connected`);
|
|
58
|
+
|
|
59
|
+
// Try a simple SQL query to verify read access
|
|
60
|
+
try {
|
|
61
|
+
const sqlResult = await service.executeSqlQuery("SELECT COUNT(*) AS cnt FROM device");
|
|
62
|
+
const rows = Array.isArray(sqlResult) ? sqlResult : sqlResult?.row || [];
|
|
63
|
+
const count = rows[0]?.cnt || "?";
|
|
64
|
+
ok(`SQL query: ${count} device(s) in database`);
|
|
65
|
+
} catch {
|
|
66
|
+
warn("SQL query: could not query device table — may lack permissions");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check operation count
|
|
70
|
+
try {
|
|
71
|
+
const ops = await service.returnOperations();
|
|
72
|
+
const count = Array.isArray(ops) ? ops.length : 0;
|
|
73
|
+
ok(`AXL operations: ${count} available`);
|
|
74
|
+
} catch {
|
|
75
|
+
warn("Could not list AXL operations");
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const msg = err.message || String(err);
|
|
79
|
+
if (msg.includes("401") || msg.includes("Authentication") || msg.includes("Unauthorized")) {
|
|
80
|
+
fail("AXL API: authentication failed — check username/password");
|
|
81
|
+
} else if (msg.includes("ECONNREFUSED")) {
|
|
82
|
+
fail("AXL API: connection refused — check host and port");
|
|
83
|
+
} else if (msg.includes("ENOTFOUND")) {
|
|
84
|
+
fail("AXL API: hostname not found — check host");
|
|
85
|
+
} else if (msg.includes("certificate")) {
|
|
86
|
+
fail("AXL API: TLS certificate error — try adding --insecure to the cluster config");
|
|
87
|
+
} else {
|
|
88
|
+
fail(`AXL API: ${msg}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 3. Security
|
|
93
|
+
console.log("\n Security");
|
|
94
|
+
try {
|
|
95
|
+
const fs = require("node:fs");
|
|
96
|
+
const configPath = getConfigPath();
|
|
97
|
+
const stats = fs.statSync(configPath);
|
|
98
|
+
const mode = (stats.mode & 0o777).toString(8);
|
|
99
|
+
if (mode === "600") ok(`Config file permissions: ${mode} (secure)`);
|
|
100
|
+
else warn(`Config file permissions: ${mode} — should be 600. Run: chmod 600 ${configPath}`);
|
|
101
|
+
} catch { /* config file may not exist yet */ }
|
|
102
|
+
|
|
103
|
+
// 4. Audit trail
|
|
104
|
+
try {
|
|
105
|
+
const fs = require("node:fs");
|
|
106
|
+
const path = require("node:path");
|
|
107
|
+
const auditPath = path.join(getConfigDir(), "audit.jsonl");
|
|
108
|
+
if (fs.existsSync(auditPath)) {
|
|
109
|
+
const stats = fs.statSync(auditPath);
|
|
110
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
|
|
111
|
+
ok(`Audit trail: ${sizeMB}MB`);
|
|
112
|
+
if (stats.size > 8 * 1024 * 1024) warn("Audit trail approaching 10MB rotation limit");
|
|
113
|
+
} else {
|
|
114
|
+
ok("Audit trail: empty (no operations logged yet)");
|
|
115
|
+
}
|
|
116
|
+
} catch { /* ignore */ }
|
|
117
|
+
|
|
118
|
+
printSummary(passed, warned, failed);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function printSummary(passed, warned, failed) {
|
|
122
|
+
console.log("\n " + "─".repeat(50));
|
|
123
|
+
console.log(` Results: ${passed} passed, ${warned} warning${warned !== 1 ? "s" : ""}, ${failed} failed`);
|
|
124
|
+
if (failed > 0) {
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
console.log(" Status: issues found — review failures above");
|
|
127
|
+
} else if (warned > 0) {
|
|
128
|
+
console.log(" Status: healthy with warnings");
|
|
129
|
+
} else {
|
|
130
|
+
console.log(" Status: all systems healthy");
|
|
131
|
+
}
|
|
132
|
+
console.log("");
|
|
133
|
+
}
|
|
134
|
+
};
|
package/cli/commands/execute.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const { createService } = require("../utils/connection.js");
|
|
11
11
|
const { printResult, printError } = require("../utils/output.js");
|
|
12
12
|
const { enforceReadOnly } = require("../utils/readonly.js");
|
|
13
|
+
const { readStdin } = require("../utils/stdin.js");
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Registers the execute command on the given Commander program.
|
|
@@ -20,6 +21,7 @@ module.exports = function registerExecuteCommand(program) {
|
|
|
20
21
|
.command("execute <operation>")
|
|
21
22
|
.description("Execute a raw AXL operation with JSON tags")
|
|
22
23
|
.option("--tags <json>", "JSON object of operation tags")
|
|
24
|
+
.option("--stdin", "read JSON tags from stdin (for piping)")
|
|
23
25
|
.option("--template <file>", "JSON template file with %%var%% placeholders")
|
|
24
26
|
.option("--vars <json>", "variables to resolve in template (JSON)")
|
|
25
27
|
.option("--csv <file>", "CSV file for bulk operations (use with --template)")
|
|
@@ -32,14 +34,22 @@ module.exports = function registerExecuteCommand(program) {
|
|
|
32
34
|
let errorMsg;
|
|
33
35
|
|
|
34
36
|
try {
|
|
35
|
-
enforceReadOnly(globalOpts, "execute");
|
|
37
|
+
await enforceReadOnly(globalOpts, "execute");
|
|
38
|
+
|
|
39
|
+
// Read from stdin if --stdin flag is set
|
|
40
|
+
if (cmdOpts.stdin) {
|
|
41
|
+
const stdinData = await readStdin();
|
|
42
|
+
if (!stdinData) throw new Error("--stdin specified but no data piped. Pipe JSON via: echo '{...}' | cisco-axl execute <op> --stdin");
|
|
43
|
+
cmdOpts.tags = stdinData.trim();
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
// Validate mutual exclusivity
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
const inputCount = [cmdOpts.tags, cmdOpts.template].filter(Boolean).length;
|
|
48
|
+
if (inputCount > 1) {
|
|
49
|
+
throw new Error("--tags, --stdin, and --template are mutually exclusive");
|
|
40
50
|
}
|
|
41
|
-
if (
|
|
42
|
-
throw new Error("
|
|
51
|
+
if (inputCount === 0) {
|
|
52
|
+
throw new Error("Provide input via --tags, --stdin, or --template");
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
const opts = {
|
package/cli/commands/remove.js
CHANGED
package/cli/commands/sql.js
CHANGED
package/cli/commands/update.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const { createService } = require("../utils/connection.js");
|
|
11
11
|
const { printResult, printError } = require("../utils/output.js");
|
|
12
12
|
const { enforceReadOnly } = require("../utils/readonly.js");
|
|
13
|
+
const { readStdin } = require("../utils/stdin.js");
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Registers the update command on the given Commander program.
|
|
@@ -20,6 +21,7 @@ module.exports = function registerUpdateCommand(program) {
|
|
|
20
21
|
.command("update <type> [identifier]")
|
|
21
22
|
.description("Update an existing AXL item by type and name or UUID")
|
|
22
23
|
.option("--data <json>", "JSON object of fields to update")
|
|
24
|
+
.option("--stdin", "read JSON data from stdin (for piping)")
|
|
23
25
|
.option("--template <file>", "JSON template file with %%var%% placeholders")
|
|
24
26
|
.option("--vars <json>", "variables to resolve in template (JSON)")
|
|
25
27
|
.option("--csv <file>", "CSV file for bulk operations (use with --template)")
|
|
@@ -32,14 +34,22 @@ module.exports = function registerUpdateCommand(program) {
|
|
|
32
34
|
let errorMsg;
|
|
33
35
|
|
|
34
36
|
try {
|
|
35
|
-
enforceReadOnly(globalOpts, "update");
|
|
37
|
+
await enforceReadOnly(globalOpts, "update");
|
|
38
|
+
|
|
39
|
+
// Read from stdin if --stdin flag is set
|
|
40
|
+
if (cmdOpts.stdin) {
|
|
41
|
+
const stdinData = await readStdin();
|
|
42
|
+
if (!stdinData) throw new Error("--stdin specified but no data piped. Pipe JSON via: echo '{...}' | cisco-axl update <type> <id> --stdin");
|
|
43
|
+
cmdOpts.data = stdinData.trim();
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
// Validate mutual exclusivity
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
const inputCount = [cmdOpts.data, cmdOpts.template].filter(Boolean).length;
|
|
48
|
+
if (inputCount > 1) {
|
|
49
|
+
throw new Error("--data, --stdin, and --template are mutually exclusive");
|
|
40
50
|
}
|
|
41
|
-
if (
|
|
42
|
-
throw new Error("
|
|
51
|
+
if (inputCount === 0) {
|
|
52
|
+
throw new Error("Provide input via --data, --stdin, or --template");
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
const opts = {
|
package/cli/index.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { Command } = require("commander");
|
|
4
|
-
const
|
|
4
|
+
const pkg = require("../package.json");
|
|
5
|
+
|
|
6
|
+
// Suppress Node.js TLS warning when --insecure is used
|
|
7
|
+
const originalEmitWarning = process.emitWarning;
|
|
8
|
+
process.emitWarning = (warning, ...args) => {
|
|
9
|
+
if (typeof warning === "string" && warning.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
|
|
10
|
+
originalEmitWarning.call(process, warning, ...args);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const updateNotifier = require("update-notifier").default || require("update-notifier");
|
|
15
|
+
updateNotifier({ pkg }).notify();
|
|
16
|
+
} catch {};
|
|
5
17
|
|
|
6
18
|
const program = new Command();
|
|
7
19
|
|
|
8
20
|
program
|
|
9
21
|
.name("cisco-axl")
|
|
10
22
|
.description("CLI for Cisco CUCM AXL operations")
|
|
11
|
-
.version(version)
|
|
23
|
+
.version(pkg.version)
|
|
12
24
|
.option("--format <type>", "output format: table, json, toon, csv", "table")
|
|
13
25
|
.option("--host <host>", "CUCM hostname (overrides config/env)")
|
|
14
26
|
.option("--username <user>", "CUCM username (overrides config/env)")
|
|
@@ -33,6 +45,7 @@ require("./commands/sql.js")(program);
|
|
|
33
45
|
require("./commands/execute.js")(program);
|
|
34
46
|
require("./commands/operations.js")(program);
|
|
35
47
|
require("./commands/describe.js")(program);
|
|
48
|
+
require("./commands/doctor.js")(program);
|
|
36
49
|
// require("./commands/phone.js")(program);
|
|
37
50
|
// require("./commands/line.js")(program);
|
|
38
51
|
// require("./commands/user.js")(program);
|
package/cli/utils/config.js
CHANGED
|
@@ -263,15 +263,30 @@ function resolveSsValue(id, field) {
|
|
|
263
263
|
|
|
264
264
|
try {
|
|
265
265
|
const data = JSON.parse(stdout);
|
|
266
|
-
// ss-cli returns an object; find the field case-insensitively
|
|
267
266
|
const fieldLower = field.toLowerCase();
|
|
267
|
+
|
|
268
|
+
// First: check top-level keys (simple key-value secrets)
|
|
268
269
|
const foundKey = Object.keys(data).find(
|
|
269
270
|
(k) => k.toLowerCase() === fieldLower
|
|
270
271
|
);
|
|
271
|
-
if (foundKey
|
|
272
|
-
return
|
|
272
|
+
if (foundKey !== undefined) {
|
|
273
|
+
return resolve(data[foundKey]);
|
|
273
274
|
}
|
|
274
|
-
|
|
275
|
+
|
|
276
|
+
// Second: check items array (Secret Server template secrets)
|
|
277
|
+
// Items have fieldName, slug, and itemValue properties
|
|
278
|
+
if (Array.isArray(data.items)) {
|
|
279
|
+
const item = data.items.find(
|
|
280
|
+
(i) =>
|
|
281
|
+
(i.slug && i.slug.toLowerCase() === fieldLower) ||
|
|
282
|
+
(i.fieldName && i.fieldName.toLowerCase() === fieldLower)
|
|
283
|
+
);
|
|
284
|
+
if (item) {
|
|
285
|
+
return resolve(item.itemValue);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return reject(new Error(`Field "${field}" not found in secret ${id}`));
|
|
275
290
|
} catch (parseErr) {
|
|
276
291
|
reject(new Error(`Failed to parse ss-cli output for secret ${id}: ${parseErr.message}`));
|
|
277
292
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const readline = require("readline");
|
|
2
|
+
const { getRandomWord } = require("./wordlist.js");
|
|
3
|
+
|
|
4
|
+
function checkWriteAllowed(clusterConfig, globalOpts = {}) {
|
|
5
|
+
const readOnly = clusterConfig?.readOnly || globalOpts.readOnly;
|
|
6
|
+
if (!readOnly) return Promise.resolve(true);
|
|
7
|
+
|
|
8
|
+
if (!process.stdin.isTTY) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"This cluster is configured as read-only. " +
|
|
11
|
+
"Interactive TTY required for write confirmation. " +
|
|
12
|
+
"Change config with: cisco-axl config update <name> --no-read-only"
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const word = getRandomWord();
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
rl.question(
|
|
21
|
+
`\n⚠ This cluster is configured as read-only.\nTo proceed, type "${word}" to confirm: `,
|
|
22
|
+
(answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
if (answer.trim().toLowerCase() === word.toLowerCase()) {
|
|
25
|
+
resolve({ confirmed: true, word });
|
|
26
|
+
} else {
|
|
27
|
+
reject(new Error("Confirmation failed. Write operation cancelled."));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { checkWriteAllowed };
|
package/cli/utils/connection.js
CHANGED
|
@@ -119,9 +119,13 @@ async function resolveConfig(flags = {}) {
|
|
|
119
119
|
async function createService(flags = {}) {
|
|
120
120
|
const config = await resolveConfig(flags);
|
|
121
121
|
|
|
122
|
-
// Handle --insecure flag
|
|
122
|
+
// Handle --insecure flag — suppress Node's TLS warning since user opted in
|
|
123
123
|
if (config.insecure || flags.insecure) {
|
|
124
124
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
125
|
+
process.emitWarning = ((orig) => function (warning, ...args) {
|
|
126
|
+
if (typeof warning === "string" && warning.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
|
|
127
|
+
return orig.call(process, warning, ...args);
|
|
128
|
+
})(process.emitWarning);
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
// Build axlService options
|
package/cli/utils/readonly.js
CHANGED
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { checkWriteAllowed } = require("./confirm.js");
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Check if read-only mode is active (via --read-only flag or cluster config).
|
|
5
|
-
*
|
|
7
|
+
* If read-only, requires interactive TTY confirmation with a random word.
|
|
8
|
+
* Non-interactive sessions (AI agents without TTY) fail automatically.
|
|
6
9
|
*
|
|
7
10
|
* @param {object} globalOpts - Commander global options
|
|
8
11
|
* @param {string} operation - The operation being attempted (for error message)
|
|
9
12
|
*/
|
|
10
|
-
function enforceReadOnly(globalOpts, operation) {
|
|
11
|
-
if (globalOpts.readOnly) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
`Operation "${operation}" blocked — read-only mode is active.\n` +
|
|
14
|
-
"Read-only mode only allows: get, list, describe, operations, sql query.\n" +
|
|
15
|
-
"Remove --read-only flag to perform write operations."
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Also check cluster config for readOnly setting
|
|
13
|
+
async function enforceReadOnly(globalOpts, operation) {
|
|
20
14
|
const { getActiveCluster } = require("./config.js");
|
|
21
15
|
const cluster = getActiveCluster(globalOpts.cluster);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
const clusterConfig = cluster || {};
|
|
17
|
+
|
|
18
|
+
// Build a combined config for checkWriteAllowed
|
|
19
|
+
const config = {
|
|
20
|
+
readOnly: globalOpts.readOnly || clusterConfig.readOnly || false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
await checkWriteAllowed(config, globalOpts);
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
module.exports = { enforceReadOnly };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read all data from stdin as a string.
|
|
5
|
+
* Returns null if stdin is a TTY (no piped input).
|
|
6
|
+
*/
|
|
7
|
+
function readStdin() {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
if (process.stdin.isTTY) {
|
|
10
|
+
return resolve(null);
|
|
11
|
+
}
|
|
12
|
+
const chunks = [];
|
|
13
|
+
process.stdin.setEncoding("utf-8");
|
|
14
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
15
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
16
|
+
process.stdin.on("error", reject);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { readStdin };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
function getRandomWord() {
|
|
4
|
+
// Generate a random 8-char string that doesn't exist in the codebase
|
|
5
|
+
// Not guessable, not brute-forceable from a known word list
|
|
6
|
+
return crypto.randomBytes(4).toString("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = { getRandomWord };
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Library API Reference
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
const axlService = require("cisco-axl");
|
|
7
|
+
|
|
8
|
+
let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
|
|
9
|
+
|
|
10
|
+
// With options
|
|
11
|
+
let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
|
|
12
|
+
logging: { level: "info" },
|
|
13
|
+
retry: { retries: 3, retryDelay: 1000 }
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## ESM / TypeScript
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
// CommonJS
|
|
21
|
+
const axlService = require("cisco-axl");
|
|
22
|
+
|
|
23
|
+
// ESM
|
|
24
|
+
import axlService from "cisco-axl";
|
|
25
|
+
import { AXLAuthError, AXLOperationError } from "cisco-axl";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import axlService from 'cisco-axl';
|
|
30
|
+
|
|
31
|
+
const service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
|
|
32
|
+
|
|
33
|
+
const tags = await service.getOperationTags("listRoutePartition");
|
|
34
|
+
tags.searchCriteria.name = "%%";
|
|
35
|
+
const result = await service.executeOperation("listRoutePartition", tags);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
See the `examples/typescript` directory for more examples.
|
|
39
|
+
|
|
40
|
+
## Logging
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// Via environment variable
|
|
44
|
+
// DEBUG=true
|
|
45
|
+
|
|
46
|
+
// Via constructor
|
|
47
|
+
let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
|
|
48
|
+
logging: {
|
|
49
|
+
level: "info", // "error" | "warn" | "info" | "debug"
|
|
50
|
+
handler: (level, message, data) => {
|
|
51
|
+
myLogger[level](message, data);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Change at runtime
|
|
57
|
+
service.setLogLevel("debug");
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Convenience Methods
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// Get a single item by name or UUID
|
|
64
|
+
await service.getItem("Phone", "SEP001122334455");
|
|
65
|
+
await service.getItem("Phone", { uuid: "abc-123" });
|
|
66
|
+
|
|
67
|
+
// List items with search criteria and returned tags
|
|
68
|
+
await service.listItems("RoutePartition"); // all partitions
|
|
69
|
+
await service.listItems("Phone", { name: "SEP%" }, { name: "", model: "" });
|
|
70
|
+
|
|
71
|
+
// Add, update, remove
|
|
72
|
+
await service.addItem("RoutePartition", { name: "NEW-PT", description: "New" });
|
|
73
|
+
await service.updateItem("Phone", "SEP001122334455", { description: "Updated" });
|
|
74
|
+
await service.removeItem("RoutePartition", "NEW-PT");
|
|
75
|
+
|
|
76
|
+
// SQL
|
|
77
|
+
const rows = await service.executeSqlQuery("SELECT name FROM routepartition");
|
|
78
|
+
await service.executeSqlUpdate("UPDATE routepartition SET description='test' WHERE name='NEW-PT'");
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Operation Discovery
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// List all operations
|
|
85
|
+
const ops = await service.returnOperations();
|
|
86
|
+
const phoneOps = await service.returnOperations("phone");
|
|
87
|
+
|
|
88
|
+
// Get tag schema
|
|
89
|
+
const tags = await service.getOperationTags("addRoutePartition");
|
|
90
|
+
|
|
91
|
+
// Get detailed metadata (required, nillable, type)
|
|
92
|
+
const detailed = await service.getOperationTagsDetailed("addRoutePartition");
|
|
93
|
+
console.log(detailed.routePartition.required); // true
|
|
94
|
+
console.log(detailed.routePartition.children.name.type); // "string"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Execute Any Operation
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const tags = await service.getOperationTags("addRoutePartition");
|
|
101
|
+
tags.routePartition.name = "INTERNAL-PT";
|
|
102
|
+
tags.routePartition.description = "Internal directory numbers";
|
|
103
|
+
|
|
104
|
+
const result = await service.executeOperation("addRoutePartition", tags);
|
|
105
|
+
console.log("UUID:", result);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Batch Operations
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const results = await service.executeBatch([
|
|
112
|
+
{ operation: "getPhone", tags: { name: "SEP001122334455" } },
|
|
113
|
+
{ operation: "getPhone", tags: { name: "SEP556677889900" } },
|
|
114
|
+
], 5); // concurrency limit
|
|
115
|
+
|
|
116
|
+
results.forEach((r) => {
|
|
117
|
+
console.log(r.success ? `${r.operation}: OK` : `${r.operation}: ${r.error.message}`);
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Error Handling
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const { AXLAuthError, AXLNotFoundError, AXLOperationError, AXLValidationError } = require("cisco-axl");
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await service.executeOperation("getPhone", { name: "INVALID" });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof AXLAuthError) console.log("Bad credentials");
|
|
130
|
+
else if (error instanceof AXLNotFoundError) console.log("Operation not found:", error.operation);
|
|
131
|
+
else if (error instanceof AXLOperationError) console.log("SOAP fault:", error.message);
|
|
132
|
+
else if (error instanceof AXLValidationError) console.log("Invalid input:", error.message);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Retry Configuration
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
let service = new axlService("10.10.20.1", "admin", "pass", "14.0", {
|
|
140
|
+
retry: {
|
|
141
|
+
retries: 3,
|
|
142
|
+
retryDelay: 1000,
|
|
143
|
+
retryOn: (error) => error.message.includes("ECONNRESET")
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## json-variables Support
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
var lineTemplate = {
|
|
152
|
+
pattern: "%%_extension_%%",
|
|
153
|
+
alertingName: "%%_firstName_%% %%_lastName_%%",
|
|
154
|
+
description: "%%_firstName_%% %%_lastName_%%",
|
|
155
|
+
_data: {
|
|
156
|
+
extension: "1001",
|
|
157
|
+
firstName: "Tom",
|
|
158
|
+
lastName: "Smith",
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const lineTags = jVar(lineTemplate);
|
|
163
|
+
await service.executeOperation("updateLine", lineTags);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Methods Reference
|
|
167
|
+
|
|
168
|
+
### Core
|
|
169
|
+
|
|
170
|
+
| Method | Description |
|
|
171
|
+
|--------|-------------|
|
|
172
|
+
| `new axlService(host, user, pass, version, opts?)` | Constructor |
|
|
173
|
+
| `testAuthentication()` | Test credentials against AXL endpoint |
|
|
174
|
+
| `returnOperations(filter?)` | List available operations |
|
|
175
|
+
| `getOperationTags(operation)` | Get tag schema for an operation |
|
|
176
|
+
| `getOperationTagsDetailed(operation)` | Get detailed tag metadata (required/nillable/type) |
|
|
177
|
+
| `executeOperation(operation, tags, opts?)` | Execute any AXL operation |
|
|
178
|
+
| `executeBatch(operations[], concurrency?)` | Parallel batch execution |
|
|
179
|
+
| `setLogLevel(level)` | Change log level at runtime |
|
|
180
|
+
|
|
181
|
+
### Convenience
|
|
182
|
+
|
|
183
|
+
| Method | Description |
|
|
184
|
+
|--------|-------------|
|
|
185
|
+
| `getItem(type, identifier, opts?)` | Get single item by name or UUID |
|
|
186
|
+
| `listItems(type, search?, returnedTags?, opts?)` | List items with filtering |
|
|
187
|
+
| `addItem(type, data, opts?)` | Add a new item |
|
|
188
|
+
| `updateItem(type, identifier, updates, opts?)` | Update an existing item |
|
|
189
|
+
| `removeItem(type, identifier, opts?)` | Remove an item |
|
|
190
|
+
| `executeSqlQuery(sql)` | Run a SQL SELECT query |
|
|
191
|
+
| `executeSqlUpdate(sql)` | Run a SQL INSERT/UPDATE/DELETE |
|