cisco-axl 2.0.0 → 2.1.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.
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "cisco-axl",
3
+ "description": "Cisco CUCM AXL CLI skills for provisioning phones, lines, route patterns, partitions, CSS, and more via the Administrative XML API",
4
+ "version": "2.0.0",
5
+ "author": {
6
+ "name": "Jeremy Worden",
7
+ "url": "https://github.com/sieteunoseis"
8
+ },
9
+ "homepage": "https://github.com/sieteunoseis/cisco-axl#readme",
10
+ "repository": "https://github.com/sieteunoseis/cisco-axl",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "cisco",
14
+ "cucm",
15
+ "axl",
16
+ "unified-communications",
17
+ "voip",
18
+ "telephony",
19
+ "provisioning",
20
+ "soap",
21
+ "callmanager",
22
+ "cli",
23
+ "automation",
24
+ "bulk-provisioning"
25
+ ]
26
+ }
@@ -0,0 +1 @@
1
+ buy_me_a_coffee: automatebldrs
@@ -12,7 +12,7 @@ jobs:
12
12
 
13
13
  strategy:
14
14
  matrix:
15
- node-version: [18, 20, 22]
15
+ node-version: [20, 22]
16
16
 
17
17
  steps:
18
18
  - uses: actions/checkout@v4
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Cisco AXL Library & CLI
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/cisco-axl.svg)](https://www.npmjs.com/package/cisco-axl)
4
+ [![CI](https://github.com/sieteunoseis/cisco-axl/actions/workflows/ci.yml/badge.svg)](https://github.com/sieteunoseis/cisco-axl/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js Version](https://img.shields.io/node/v/cisco-axl.svg)](https://nodejs.org)
7
+ [![Skills](https://img.shields.io/badge/skills.sh-cisco--axl--cli-blue)](https://skills.sh/sieteunoseis/cisco-axl)
8
+ [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-orange?logo=buy-me-a-coffee)](https://buymeacoffee.com/automatebldrs)
9
+
3
10
  A JavaScript library and CLI to interact with Cisco CUCM via AXL SOAP API. Dynamically discovers all AXL operations from the WSDL schema — any operation for your specified version is available without static definitions.
4
11
 
5
12
  Administrative XML (AXL) information can be found at:
@@ -26,7 +33,7 @@ npx cisco-axl --help
26
33
  ### AI Agent Skills
27
34
 
28
35
  ```bash
29
- npx skillsadd sieteunoseis/cisco-axl
36
+ npx skills add sieteunoseis/cisco-axl
30
37
  ```
31
38
 
32
39
  ## Requirements
@@ -145,6 +152,38 @@ Template file (`phone-template.json`):
145
152
  --debug Enable debug logging
146
153
  ```
147
154
 
155
+ ### Command Chaining
156
+
157
+ Shell `&&` chains commands sequentially — each waits for the previous to complete, and the chain stops on the first failure:
158
+
159
+ ```bash
160
+ # Create a partition, CSS, and line in order
161
+ cisco-axl add RoutePartition --data '{"name":"PT_INTERNAL","description":"Internal"}' && \
162
+ cisco-axl add Css --data '{"name":"CSS_INTERNAL","members":{"member":{"routePartitionName":"PT_INTERNAL","index":"1"}}}' && \
163
+ cisco-axl add Line --data '{"pattern":"1000","routePartitionName":"PT_INTERNAL"}'
164
+ ```
165
+
166
+ ### Piping with --stdin
167
+
168
+ Use `--stdin` to pipe JSON between commands or from other tools like `jq`:
169
+
170
+ ```bash
171
+ # Get a phone's config, modify it with jq, update it
172
+ cisco-axl get Phone SEP001122334455 --format json | \
173
+ jq '.description = "Updated via pipe"' | \
174
+ cisco-axl update Phone SEP001122334455 --stdin
175
+
176
+ # Pipe JSON from a file
177
+ cat phone-config.json | cisco-axl add Phone --stdin
178
+
179
+ # Discover tags, fill them in, execute
180
+ cisco-axl describe applyPhone --format json | \
181
+ jq '.name = "SEP001122334455"' | \
182
+ cisco-axl execute applyPhone --stdin
183
+ ```
184
+
185
+ The `--stdin` flag is available on `add`, `update`, and `execute`. It is mutually exclusive with `--data`/`--tags` and `--template`.
186
+
148
187
  ### Audit Trail
149
188
 
150
189
  All operations are logged to `~/.cisco-axl/audit.jsonl` (JSONL format). Credentials are never logged. Use `--no-audit` to skip.
@@ -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 add command on the given Commander program.
@@ -20,6 +21,7 @@ module.exports = function registerAddCommand(program) {
20
21
  .command("add <type>")
21
22
  .description("Add a new AXL item of the given type")
22
23
  .option("--data <json>", "JSON definition of the item to add")
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)")
@@ -34,12 +36,20 @@ module.exports = function registerAddCommand(program) {
34
36
  try {
35
37
  enforceReadOnly(globalOpts, "add");
36
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 add <type> --stdin");
43
+ cmdOpts.data = stdinData.trim();
44
+ }
45
+
37
46
  // Validate mutual exclusivity
38
- if (cmdOpts.data && cmdOpts.template) {
39
- throw new Error("--data and --template are mutually exclusive");
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 (!cmdOpts.data && !cmdOpts.template) {
42
- throw new Error("Either --data or --template must be provided");
51
+ if (inputCount === 0) {
52
+ throw new Error("Provide input via --data, --stdin, or --template");
43
53
  }
44
54
 
45
55
  const opts = {
@@ -14,38 +14,32 @@ const { createService } = require("../utils/connection.js");
14
14
  * @param {import("commander").Command} program
15
15
  */
16
16
  module.exports = function registerConfigCommand(program) {
17
- const config = program.command("config").description("Manage CUCM cluster configuration");
17
+ const config = program
18
+ .command("config")
19
+ .description("Manage CUCM cluster configuration");
18
20
 
19
21
  // ── config add <name> ───────────────────────────────────────────────────────
20
22
 
21
23
  config
22
24
  .command("add <name>")
23
- .description("Add a named CUCM cluster to config")
24
- .requiredOption("--cucm-version <ver>", "CUCM version (e.g. 14.0)")
25
- .action((name, opts) => {
25
+ .description("Add a named CUCM cluster to config (use --host, --username, --password, --version-cucm)")
26
+ .option("--cucm-version <ver>", "CUCM version (e.g. 14.0)")
27
+ .option("--insecure", "skip TLS verification for this cluster")
28
+ .action((name, opts, cmd) => {
26
29
  try {
27
- // --host, --username, --password, --insecure come from global program options
28
- const globalOpts = program.opts();
29
-
30
+ const globalOpts = cmd.optsWithGlobals();
30
31
  const host = globalOpts.host;
31
32
  const username = globalOpts.username;
32
33
  const password = globalOpts.password;
33
- const insecure = globalOpts.insecure;
34
-
35
- if (!host) {
36
- throw new Error("Missing required option: --host");
37
- }
38
- if (!username) {
39
- throw new Error("Missing required option: --username");
40
- }
41
- if (!password) {
42
- throw new Error("Missing required option: --password");
43
- }
44
-
45
- // Commander camelCases --cucm-version to cucmVersion; addCluster expects version
46
- const clusterOpts = { host, username, password, version: opts.cucmVersion };
47
- if (insecure !== undefined) {
48
- clusterOpts.insecure = insecure;
34
+ const version = opts.cucmVersion || globalOpts.versionCucm;
35
+ if (!host) throw new Error("Missing required option: --host");
36
+ if (!username) throw new Error("Missing required option: --username");
37
+ if (!password) throw new Error("Missing required option: --password");
38
+ if (!version) throw new Error("Missing required option: --cucm-version or --version-cucm");
39
+
40
+ const clusterOpts = { host, username, password, version };
41
+ if (opts.insecure || globalOpts.insecure) {
42
+ clusterOpts.insecure = true;
49
43
  }
50
44
 
51
45
  configUtil.addCluster(name, clusterOpts);
@@ -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)")
@@ -34,12 +36,20 @@ module.exports = function registerExecuteCommand(program) {
34
36
  try {
35
37
  enforceReadOnly(globalOpts, "execute");
36
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
+ }
45
+
37
46
  // Validate mutual exclusivity
38
- if (cmdOpts.tags && cmdOpts.template) {
39
- throw new Error("--tags and --template are mutually exclusive");
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 (!cmdOpts.tags && !cmdOpts.template) {
42
- throw new Error("Either --tags or --template must be provided");
51
+ if (inputCount === 0) {
52
+ throw new Error("Provide input via --tags, --stdin, or --template");
43
53
  }
44
54
 
45
55
  const opts = {
@@ -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)")
@@ -34,12 +36,20 @@ module.exports = function registerUpdateCommand(program) {
34
36
  try {
35
37
  enforceReadOnly(globalOpts, "update");
36
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
+ }
45
+
37
46
  // Validate mutual exclusivity
38
- if (cmdOpts.data && cmdOpts.template) {
39
- throw new Error("--data and --template are mutually exclusive");
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 (!cmdOpts.data && !cmdOpts.template) {
42
- throw new Error("Either --data or --template must be provided");
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,18 @@
1
1
  "use strict";
2
2
 
3
3
  const { Command } = require("commander");
4
- const { version } = require("../package.json");
4
+ const pkg = require("../package.json");
5
+
6
+ import("update-notifier").then(({ default: updateNotifier }) => {
7
+ updateNotifier({ pkg }).notify();
8
+ }).catch(() => {});
5
9
 
6
10
  const program = new Command();
7
11
 
8
12
  program
9
13
  .name("cisco-axl")
10
14
  .description("CLI for Cisco CUCM AXL operations")
11
- .version(version)
15
+ .version(pkg.version)
12
16
  .option("--format <type>", "output format: table, json, toon, csv", "table")
13
17
  .option("--host <host>", "CUCM hostname (overrides config/env)")
14
18
  .option("--username <user>", "CUCM username (overrides config/env)")
@@ -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 === undefined) {
272
- return reject(new Error(`Field "${field}" not found in ss-cli response for secret ${id}`));
272
+ if (foundKey !== undefined) {
273
+ return resolve(data[foundKey]);
273
274
  }
274
- resolve(data[foundKey]);
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
  }
@@ -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
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cisco-axl",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A library and CLI for Cisco CUCM AXL operations",
5
5
  "main": "dist/index.js",
6
6
  "module": "main.mjs",
@@ -50,7 +50,8 @@
50
50
  "cli-table3": "^0.6.5",
51
51
  "commander": "^14.0.3",
52
52
  "csv-stringify": "^6.7.0",
53
- "strong-soap": "^5.0.8"
53
+ "strong-soap": "^5.0.8",
54
+ "update-notifier": "^7.3.1"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/node": "^22.19.15",
@@ -7,6 +7,21 @@ description: Use when managing Cisco CUCM via the cisco-axl CLI — phones, line
7
7
 
8
8
  A CLI for Cisco Unified Communications Manager (CUCM) Administrative XML (AXL) operations.
9
9
 
10
+ ## Prerequisites
11
+
12
+ The CLI must be available. Either:
13
+
14
+ ```bash
15
+ # Option 1: Use npx (no install needed, works immediately)
16
+ npx cisco-axl --help
17
+
18
+ # Option 2: Install globally for faster repeated use
19
+ npm install -g cisco-axl
20
+ ```
21
+
22
+ If using npx, prefix all commands with `npx`: `npx cisco-axl list Phone ...`
23
+ If installed globally, use directly: `cisco-axl list Phone ...`
24
+
10
25
  ## Setup
11
26
 
12
27
  Configure a CUCM cluster:
@@ -167,6 +182,34 @@ cisco-axl config use prod
167
182
  cisco-axl list Phone --search "name=SEP%" --cluster lab # override per-command
168
183
  ```
169
184
 
185
+ ## Command Chaining
186
+
187
+ Shell `&&` chains commands sequentially — each waits for the previous to complete, and the chain stops on the first failure:
188
+
189
+ ```bash
190
+ # Create a partition, CSS, and line in order
191
+ cisco-axl add RoutePartition --data '{"name":"PT_INTERNAL","description":"Internal"}' && \
192
+ cisco-axl add Css --data '{"name":"CSS_INTERNAL","members":{"member":{"routePartitionName":"PT_INTERNAL","index":"1"}}}' && \
193
+ cisco-axl add Line --data '{"pattern":"1000","routePartitionName":"PT_INTERNAL"}'
194
+ ```
195
+
196
+ ## Piping with --stdin
197
+
198
+ Use `--stdin` to pipe JSON between commands or from other tools:
199
+
200
+ ```bash
201
+ # Get a phone's config, modify it with jq, update it
202
+ cisco-axl get Phone SEP001122334455 --format json | jq '.description = "Updated via pipe"' | cisco-axl update Phone SEP001122334455 --stdin
203
+
204
+ # Pipe JSON from a file
205
+ cat phone-config.json | cisco-axl add Phone --stdin
206
+
207
+ # Chain get → transform → execute
208
+ cisco-axl describe applyPhone --format json | jq '.name = "SEP001122334455"' | cisco-axl execute applyPhone --stdin
209
+ ```
210
+
211
+ The `--stdin` flag is available on `add`, `update`, and `execute` commands. It is mutually exclusive with `--data`/`--tags` and `--template`.
212
+
170
213
  ## Tips
171
214
 
172
215
  - Item types are PascalCase matching AXL: `Phone`, `Line`, `RoutePartition`, `Css`, `DevicePool`, `SipTrunk`, `TransPattern`, `RouteGroup`, `RouteList`, etc.