Skip to content

feat: add godaddy api command for direct API access#2

Open
wcole1-godaddy wants to merge 2 commits intomainfrom
feature/api-command
Open

feat: add godaddy api command for direct API access#2
wcole1-godaddy wants to merge 2 commits intomainfrom
feature/api-command

Conversation

@wcole1-godaddy
Copy link
Contributor

Add a new command that allows direct, authenticated requests to any GoDaddy API endpoint, similar to gh api and vercel api. Features:

  • Support for all HTTP methods (GET, POST, PUT, PATCH, DELETE)
  • Field arguments (-f key=value) and JSON file body (-F path)
  • Custom headers (-H "Key: Value")
  • JSON path queries for filtering output (-q .path.to.value)
  • Response header display (-i, --include)
  • Debug mode shows request/response details (--debug)
  • Proactive token expiry checking with helpful messages
  • Specific handling for 401/403 responses

Also includes:

  • Comprehensive documentation in README
  • Unit tests for API module and command
  • Improved test reliability for CI environments

wcole1-godaddy and others added 2 commits February 3, 2026 16:22
Add a new command that allows direct, authenticated requests to any
GoDaddy API endpoint, similar to `gh api` and `vercel api`. Features:

- Support for all HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Field arguments (-f key=value) and JSON file body (-F path)
- Custom headers (-H "Key: Value")
- JSON path queries for filtering output (-q .path.to.value)
- Response header display (-i, --include)
- Debug mode shows request/response details (--debug)
- Proactive token expiry checking with helpful messages
- Specific handling for 401/403 responses

Also includes:
- Comprehensive documentation in README
- Unit tests for API module and command
- Improved test reliability for CI environments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
encoding: "utf-8",
});
it.skipIf(!canRunCli)("should display help and exit with code 0", () => {
const result = execSync(`node ${CLI_PATH} --help`, {

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium test

This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 4 days ago

To fix the problem, avoid building a single shell command string that embeds CLI_PATH. Instead, invoke node with arguments passed as an array so the path is not parsed by a shell. This removes any risk from spaces or metacharacters in process.cwd(); node receives the path as a literal argument.

The minimal, behavior-preserving change within tests/integration/cli-smoke.test.ts is:

  • Keep execSync but stop using a shell-style command string.
  • Call execSync with an array of arguments, and ensure shell is disabled so the command is executed directly.
  • For each current use:
    • execSync(`node ${CLI_PATH} --help`, …)execSync("node", [CLI_PATH, "--help"], { encoding: "utf-8", shell: false })
    • Similarly for application --help, --version, --env invalid-env env get, and nonexistent-command.
  • Optionally, for pnpm run build, you can also avoid the shell; however, the reported issue concerns CLI_PATH, so we will leave that call unchanged to minimize scope, unless the broader project expects shell features there.

No new imports are needed; we are already importing execSync from node:child_process. All changes occur inside tests/integration/cli-smoke.test.ts on the lines where execSync is currently passed a template string involving CLI_PATH.

Suggested changeset 1
tests/integration/cli-smoke.test.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/integration/cli-smoke.test.ts b/tests/integration/cli-smoke.test.ts
--- a/tests/integration/cli-smoke.test.ts
+++ b/tests/integration/cli-smoke.test.ts
@@ -36,8 +36,9 @@
 describe("CLI Smoke Tests", () => {
 	describe("--help", () => {
 		it.skipIf(!canRunCli)("should display help and exit with code 0", () => {
-			const result = execSync(`node ${CLI_PATH} --help`, {
+			const result = execSync("node", [CLI_PATH, "--help"], {
 				encoding: "utf-8",
+				shell: false,
 			});
 
 			expect(result).toContain("GoDaddy");
@@ -47,8 +47,9 @@
 		});
 
 		it.skipIf(!canRunCli)("should display subcommand help", () => {
-			const result = execSync(`node ${CLI_PATH} application --help`, {
+			const result = execSync("node", [CLI_PATH, "application", "--help"], {
 				encoding: "utf-8",
+				shell: false,
 			});
 
 			expect(result).toContain("info");
@@ -59,8 +59,9 @@
 
 	describe("--version", () => {
 		it.skipIf(!canRunCli)("should display version and exit with code 0", () => {
-			const result = execSync(`node ${CLI_PATH} --version`, {
+			const result = execSync("node", [CLI_PATH, "--version"], {
 				encoding: "utf-8",
+				shell: false,
 			});
 
 			expect(result.trim()).toMatch(/^\d+\.\d+\.\d+$/);
@@ -72,10 +72,15 @@
 			"should exit with error for invalid --env value",
 			() => {
 				expect(() => {
-					execSync(`node ${CLI_PATH} --env invalid-env env get`, {
-						encoding: "utf-8",
-						stdio: "pipe",
-					});
+					execSync(
+						"node",
+						[CLI_PATH, "--env", "invalid-env", "env", "get"],
+						{
+							encoding: "utf-8",
+							stdio: "pipe",
+							shell: false,
+						},
+					);
 				}).toThrow();
 			},
 		);
@@ -84,10 +89,15 @@
 	describe("unknown command", () => {
 		it.skipIf(!canRunCli)("should show error for unknown command", () => {
 			expect(() => {
-				execSync(`node ${CLI_PATH} nonexistent-command`, {
-					encoding: "utf-8",
-					stdio: "pipe",
-				});
+				execSync(
+					"node",
+					[CLI_PATH, "nonexistent-command"],
+					{
+						encoding: "utf-8",
+						stdio: "pipe",
+						shell: false,
+					},
+				);
 			}).toThrow();
 		});
 	});
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
encoding: "utf-8",
});
it.skipIf(!canRunCli)("should display version and exit with code 0", () => {
const result = execSync(`node ${CLI_PATH} --version`, {

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium test

This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 4 days ago

In general, to fix this issue you should avoid constructing a full shell command string that includes tainted or environment-derived paths and instead pass the executable and its arguments separately to an API that does not go through a shell (for Node’s child_process, that is execFileSync or spawnSync with an argument array). This prevents shell interpretation of spaces and special characters in file paths or arguments.

For this file, the best minimal-change fix is:

  • Keep using execSync where it is only given a fixed string literal command (e.g., "pnpm run build"), because there is no tainted interpolation there.
  • For every place where CLI_PATH is interpolated into a template string passed to execSync, switch to spawnSync (or execFileSync) and pass:
    • the command "node" as the executable,
    • an array of arguments containing CLI_PATH and the rest of the CLI arguments separately,
    • the encoding/stdio options as before.
  • Update imports to include spawnSync from node:child_process.
  • Adapt expectations:
    • Where the code previously captured execSync’s stdout as the return value, now read result.stdout from spawnSync.
    • Where the code previously wrapped execSync in expect(() => { ... }).toThrow(), now perform spawnSync and assert that status is non-zero (an error exit), which preserves the tested behavior without relying on thrown exceptions.

Specifically:

  • In tests/integration/cli-smoke.test.ts:
    • Add spawnSync to the node:child_process import.
    • Replace the four execSync calls on lines 39, 50, 62, 75, and 87 that interpolate CLI_PATH with spawnSync("node", [CLI_PATH, ...args], { encoding: "utf-8", ... }), adjusting to capture stdout or assert on status as appropriate.
Suggested changeset 1
tests/integration/cli-smoke.test.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/integration/cli-smoke.test.ts b/tests/integration/cli-smoke.test.ts
--- a/tests/integration/cli-smoke.test.ts
+++ b/tests/integration/cli-smoke.test.ts
@@ -1,4 +1,4 @@
-import { execSync } from "node:child_process";
+import { execSync, spawnSync } from "node:child_process";
 import { existsSync } from "node:fs";
 import { join } from "node:path";
 import { describe, expect, it } from "vitest";
@@ -36,34 +36,37 @@
 describe("CLI Smoke Tests", () => {
 	describe("--help", () => {
 		it.skipIf(!canRunCli)("should display help and exit with code 0", () => {
-			const result = execSync(`node ${CLI_PATH} --help`, {
+			const result = spawnSync("node", [CLI_PATH, "--help"], {
 				encoding: "utf-8",
 			});
 
-			expect(result).toContain("GoDaddy");
-			expect(result).toContain("application");
-			expect(result).toContain("auth");
-			expect(result).toContain("env");
+			expect(result.status).toBe(0);
+			expect(result.stdout).toContain("GoDaddy");
+			expect(result.stdout).toContain("application");
+			expect(result.stdout).toContain("auth");
+			expect(result.stdout).toContain("env");
 		});
 
 		it.skipIf(!canRunCli)("should display subcommand help", () => {
-			const result = execSync(`node ${CLI_PATH} application --help`, {
+			const result = spawnSync("node", [CLI_PATH, "application", "--help"], {
 				encoding: "utf-8",
 			});
 
-			expect(result).toContain("info");
-			expect(result).toContain("deploy");
-			expect(result).toContain("release");
+			expect(result.status).toBe(0);
+			expect(result.stdout).toContain("info");
+			expect(result.stdout).toContain("deploy");
+			expect(result.stdout).toContain("release");
 		});
 	});
 
 	describe("--version", () => {
 		it.skipIf(!canRunCli)("should display version and exit with code 0", () => {
-			const result = execSync(`node ${CLI_PATH} --version`, {
+			const result = spawnSync("node", [CLI_PATH, "--version"], {
 				encoding: "utf-8",
 			});
 
-			expect(result.trim()).toMatch(/^\d+\.\d+\.\d+$/);
+			expect(result.status).toBe(0);
+			expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
 		});
 	});
 
@@ -71,24 +57,26 @@
 		it.skipIf(!canRunCli)(
 			"should exit with error for invalid --env value",
 			() => {
-				expect(() => {
-					execSync(`node ${CLI_PATH} --env invalid-env env get`, {
+				const result = spawnSync(
+					"node",
+					[CLI_PATH, "--env", "invalid-env", "env", "get"],
+					{
 						encoding: "utf-8",
 						stdio: "pipe",
-					});
-				}).toThrow();
+					},
+				);
+				expect(result.status).not.toBe(0);
 			},
 		);
 	});
 
 	describe("unknown command", () => {
 		it.skipIf(!canRunCli)("should show error for unknown command", () => {
-			expect(() => {
-				execSync(`node ${CLI_PATH} nonexistent-command`, {
-					encoding: "utf-8",
-					stdio: "pipe",
-				});
-			}).toThrow();
+			const result = spawnSync("node", [CLI_PATH, "nonexistent-command"], {
+				encoding: "utf-8",
+				stdio: "pipe",
+			});
+			expect(result.status).not.toBe(0);
 		});
 	});
 });
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
"should exit with error for invalid --env value",
() => {
expect(() => {
execSync(`node ${CLI_PATH} --env invalid-env env get`, {

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium test

This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 4 days ago

In general, the problem is that we’re building a single shell command string (node ${CLI_PATH} ...) that includes a path derived from process.cwd(), and passing it to execSync, which invokes a shell. To fix this, we should avoid the shell and pass the command and its arguments separately so that the path is not reinterpreted by the shell.

The best fix here is to switch all execSync calls that run node ${CLI_PATH} ... to use execFileSync (from node:child_process) with an argument array: execFileSync("node", [CLI_PATH, "--help"]), etc. This preserves existing behavior (we’re still running the same Node script with the same arguments) but removes any shell interpretation of CLI_PATH or other arguments. We will need to import execFileSync alongside execSync. For the pnpm run build invocation, the taint source is not involved and the command is static, so it can remain as-is; only the calls that interpolate CLI_PATH need changing.

Concretely in tests/integration/cli-smoke.test.ts:

  • Update the import on line 1 to include execFileSync.
  • Replace the execSync calls that construct commands using template literals with execFileSync("node", [...args]):
    • Help test: execSync(\node ${CLI_PATH} --help`, ...)execFileSync("node", [CLI_PATH, "--help"], ...)`.
    • Subcommand help: execSync(\node ${CLI_PATH} application --help`, ...)execFileSync("node", [CLI_PATH, "application", "--help"], ...)`.
    • Version: execSync(\node ${CLI_PATH} --version`, ...)execFileSync("node", [CLI_PATH, "--version"], ...)`.
    • Invalid env: execSync(\node ${CLI_PATH} --env invalid-env env get`, ...)execFileSync("node", [CLI_PATH, "--env", "invalid-env", "env", "get"], ...)`.
    • Unknown command: execSync(\node ${CLI_PATH} nonexistent-command`, ...)execFileSync("node", [CLI_PATH, "nonexistent-command"], ...)`.

No other behavioral changes are needed.

Suggested changeset 1
tests/integration/cli-smoke.test.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/integration/cli-smoke.test.ts b/tests/integration/cli-smoke.test.ts
--- a/tests/integration/cli-smoke.test.ts
+++ b/tests/integration/cli-smoke.test.ts
@@ -1,4 +1,4 @@
-import { execSync } from "node:child_process";
+import { execSync, execFileSync } from "node:child_process";
 import { existsSync } from "node:fs";
 import { join } from "node:path";
 import { describe, expect, it } from "vitest";
@@ -36,7 +36,7 @@
 describe("CLI Smoke Tests", () => {
 	describe("--help", () => {
 		it.skipIf(!canRunCli)("should display help and exit with code 0", () => {
-			const result = execSync(`node ${CLI_PATH} --help`, {
+			const result = execFileSync("node", [CLI_PATH, "--help"], {
 				encoding: "utf-8",
 			});
 
@@ -47,7 +47,7 @@
 		});
 
 		it.skipIf(!canRunCli)("should display subcommand help", () => {
-			const result = execSync(`node ${CLI_PATH} application --help`, {
+			const result = execFileSync("node", [CLI_PATH, "application", "--help"], {
 				encoding: "utf-8",
 			});
 
@@ -59,7 +59,7 @@
 
 	describe("--version", () => {
 		it.skipIf(!canRunCli)("should display version and exit with code 0", () => {
-			const result = execSync(`node ${CLI_PATH} --version`, {
+			const result = execFileSync("node", [CLI_PATH, "--version"], {
 				encoding: "utf-8",
 			});
 
@@ -72,7 +72,7 @@
 			"should exit with error for invalid --env value",
 			() => {
 				expect(() => {
-					execSync(`node ${CLI_PATH} --env invalid-env env get`, {
+					execFileSync("node", [CLI_PATH, "--env", "invalid-env", "env", "get"], {
 						encoding: "utf-8",
 						stdio: "pipe",
 					});
@@ -84,7 +84,7 @@
 	describe("unknown command", () => {
 		it.skipIf(!canRunCli)("should show error for unknown command", () => {
 			expect(() => {
-				execSync(`node ${CLI_PATH} nonexistent-command`, {
+				execFileSync("node", [CLI_PATH, "nonexistent-command"], {
 					encoding: "utf-8",
 					stdio: "pipe",
 				});
EOF
@@ -1,4 +1,4 @@
import { execSync } from "node:child_process";
import { execSync, execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
@@ -36,7 +36,7 @@
describe("CLI Smoke Tests", () => {
describe("--help", () => {
it.skipIf(!canRunCli)("should display help and exit with code 0", () => {
const result = execSync(`node ${CLI_PATH} --help`, {
const result = execFileSync("node", [CLI_PATH, "--help"], {
encoding: "utf-8",
});

@@ -47,7 +47,7 @@
});

it.skipIf(!canRunCli)("should display subcommand help", () => {
const result = execSync(`node ${CLI_PATH} application --help`, {
const result = execFileSync("node", [CLI_PATH, "application", "--help"], {
encoding: "utf-8",
});

@@ -59,7 +59,7 @@

describe("--version", () => {
it.skipIf(!canRunCli)("should display version and exit with code 0", () => {
const result = execSync(`node ${CLI_PATH} --version`, {
const result = execFileSync("node", [CLI_PATH, "--version"], {
encoding: "utf-8",
});

@@ -72,7 +72,7 @@
"should exit with error for invalid --env value",
() => {
expect(() => {
execSync(`node ${CLI_PATH} --env invalid-env env get`, {
execFileSync("node", [CLI_PATH, "--env", "invalid-env", "env", "get"], {
encoding: "utf-8",
stdio: "pipe",
});
@@ -84,7 +84,7 @@
describe("unknown command", () => {
it.skipIf(!canRunCli)("should show error for unknown command", () => {
expect(() => {
execSync(`node ${CLI_PATH} nonexistent-command`, {
execFileSync("node", [CLI_PATH, "nonexistent-command"], {
encoding: "utf-8",
stdio: "pipe",
});
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant