Skip to content

Commit e0fbe76

Browse files
committed
add file input and destructive command confirmation
1 parent 09988a1 commit e0fbe76

13 files changed

Lines changed: 126 additions & 5 deletions

File tree

COMMANDS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,3 +1572,11 @@ Remove a key by ID or name
15721572
iterable keys delete <name-or-id>
15731573
```
15741574

1575+
### keys validate
1576+
1577+
Test the API connection
1578+
1579+
```
1580+
iterable keys validate
1581+
```
1582+

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ See the [full command reference](COMMANDS.md) for all 109 commands with paramete
116116
| `--output <format>` | Output format: `json`, `pretty`, `table` |
117117
| `--columns <cols>` | Comma-separated columns for table output |
118118
| `--json <data>` | Pass raw JSON (use `-` for stdin) |
119+
| `--file <path>` | Read JSON input from a file |
120+
| `--force, -f` | Skip confirmation prompts for destructive commands |
119121

120122
## Output Formats
121123

@@ -157,6 +159,7 @@ iterable keys activate <name> # Switch active key
157159
iterable keys deactivate # Deactivate current key
158160
iterable keys update <name> # Update an existing key
159161
iterable keys delete <name> # Remove a key
162+
iterable keys validate # Test the API connection
160163
```
161164

162165
Keys are stored securely using:

src/commands/catalogs.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const catalogCommands: CommandDefinition[] = [
5252
description: "Delete a catalog",
5353
clientMethod: "deleteCatalog",
5454
schema: DeleteCatalogParamsSchema,
55+
destructive: true,
5556
}),
5657
defineCommand({
5758
category: "catalogs",
@@ -80,13 +81,15 @@ export const catalogCommands: CommandDefinition[] = [
8081
description: "Delete a specific catalog item by ID",
8182
clientMethod: "deleteCatalogItem",
8283
schema: DeleteCatalogItemParamsSchema,
84+
destructive: true,
8385
}),
8486
defineCommand({
8587
category: "catalogs",
8688
name: "bulk-delete-items",
8789
description: "Bulk delete catalog items by their IDs",
8890
clientMethod: "bulkDeleteCatalogItems",
8991
schema: BulkDeleteCatalogItemsParamsSchema,
92+
destructive: true,
9093
}),
9194
defineCommand({
9295
category: "catalogs",

src/commands/lists.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const listCommands: CommandDefinition[] = [
5555
description: "Delete a user list",
5656
clientMethod: "deleteList",
5757
schema: DeleteListParamsSchema,
58+
destructive: true,
5859
}),
5960
defineCommand({
6061
category: "lists",

src/commands/snippets.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ export const snippetCommands: CommandDefinition[] = [
4545
description: "Delete a snippet by ID (numeric) or name (string)",
4646
clientMethod: "deleteSnippet",
4747
schema: DeleteSnippetParamsSchema,
48+
destructive: true,
4849
}),
4950
];

src/commands/templates.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const templateCommands: CommandDefinition[] = [
4141
description: "Delete one or more templates by ID",
4242
clientMethod: "deleteTemplates",
4343
schema: BulkDeleteTemplatesParamsSchema,
44+
destructive: true,
4445
}),
4546

4647
// Email templates

src/commands/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface CommandDefinition {
2020
cliTransforms?: Record<string, CliTransform>;
2121
examples?: string[];
2222
isAlias?: boolean;
23+
destructive?: boolean;
2324
}
2425

2526
export function defineCommand<
@@ -38,6 +39,7 @@ export function defineCommand<
3839
positionalArgs?: (keyof z.infer<TSchema> & string)[];
3940
cliTransforms?: Record<string, CliTransform>;
4041
examples?: string[];
42+
destructive?: boolean;
4143
}): CommandDefinition {
4244
const userExecute = def.execute;
4345
const execute: CommandDefinition["execute"] = userExecute
@@ -64,6 +66,7 @@ export function defineAlias<TSchema extends z.ZodType>(def: {
6466
positionalArgs?: (keyof z.infer<TSchema> & string)[];
6567
cliTransforms?: Record<string, CliTransform>;
6668
examples?: string[];
69+
destructive?: boolean;
6770
}): CommandDefinition {
6871
const execute: CommandDefinition["execute"] = (client, params) =>
6972
def.execute(client, params as z.infer<TSchema>);

src/commands/users.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ export const userCommands: CommandDefinition[] = [
5959
params.identifier.includes("@")
6060
? client.deleteUserByEmail({ email: params.identifier })
6161
: client.deleteUserByUserId({ userId: params.identifier }),
62+
destructive: true,
6263
}),
6364
defineCommand({
6465
category: "users",
6566
name: "delete-by-email",
6667
description: "Delete a user by email address (asynchronous)",
6768
clientMethod: "deleteUserByEmail",
6869
schema: DeleteUserByEmailParamsSchema,
70+
destructive: true,
6971
}),
7072
defineCommand({
7173
category: "users",
@@ -74,6 +76,7 @@ export const userCommands: CommandDefinition[] = [
7476
"Delete a user by user ID (asynchronous, deletes all users with same userId)",
7577
clientMethod: "deleteUserByUserId",
7678
schema: DeleteUserByUserIdParamsSchema,
79+
destructive: true,
7780
}),
7881
defineCommand({
7982
category: "users",

src/index.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

33
import { IterableClient } from "@iterable/api";
4+
import { readFileSync } from "fs";
45
import { z } from "zod";
56

67
import { loadCliConfig } from "./config.js";
@@ -75,14 +76,40 @@ async function main(): Promise<void> {
7576
});
7677

7778
try {
79+
let restArgs = parsed.rest;
80+
if (parsed.globalFlags.file) {
81+
const content = readFileSync(parsed.globalFlags.file, "utf-8").trim();
82+
restArgs = ["--json", content];
83+
}
84+
7885
const params = await parseCommand(
7986
command.schema,
8087
`iterable ${parsed.category} ${parsed.action}`,
81-
parsed.rest,
88+
restArgs,
8289
command.positionalArgs,
8390
command.cliTransforms
8491
);
8592

93+
if (
94+
command.destructive &&
95+
!parsed.globalFlags.force &&
96+
process.stdin.isTTY
97+
) {
98+
const { default: inquirer } = await import("inquirer");
99+
const { confirm } = await inquirer.prompt([
100+
{
101+
type: "confirm",
102+
name: "confirm",
103+
message: `This will run a destructive operation (${parsed.category} ${parsed.action}). Continue?`,
104+
default: false,
105+
},
106+
]);
107+
if (!confirm) {
108+
console.error("Aborted."); // eslint-disable-line no-console
109+
process.exit(0);
110+
}
111+
}
112+
86113
const result = await command.execute(client, params);
87114

88115
const format = parsed.globalFlags.output ?? getDefaultFormat();

src/keys-cli.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* eslint-disable no-console */
2+
import { IterableClient } from "@iterable/api";
23
import chalk from "chalk";
34
import inquirer from "inquirer";
45

6+
import { loadCliConfig } from "./config.js";
57
import type { ApiKeyMetadata, KeyManager } from "./key-manager.js";
68
import { getKeyManager } from "./key-manager.js";
79
import { getSpinner, isTestEnv } from "./utils/cli-env.js";
@@ -23,7 +25,6 @@ import {
2325

2426
function displayKeyDetails(meta: ApiKeyMetadata): void {
2527
const w = 12;
26-
console.log();
2728
console.log(` ${"Name:".padEnd(w)} ${chalk.white.bold(meta.name)}`);
2829
console.log(` ${"ID:".padEnd(w)} ${chalk.gray(meta.id)}`);
2930
console.log(` ${"Endpoint:".padEnd(w)} ${linkColor()(meta.baseUrl)}`);
@@ -474,6 +475,53 @@ export async function handleKeysCommand(args: string[]): Promise<void> {
474475
break;
475476
}
476477

478+
case "validate": {
479+
spinner.start("Validating API connection...");
480+
try {
481+
const config = await loadCliConfig();
482+
const client = new IterableClient({
483+
apiKey: config.apiKey,
484+
baseUrl: config.baseUrl,
485+
});
486+
try {
487+
await client.getUserFields();
488+
spinner.succeed("API connection successful");
489+
const w = 12;
490+
if (process.env.ITERABLE_API_KEY) {
491+
console.log(
492+
` ${"Source:".padEnd(w)} ${chalk.white("ITERABLE_API_KEY environment variable")}`
493+
);
494+
} else {
495+
const activeMeta = await keyManager.getActiveKeyMetadata();
496+
if (activeMeta) {
497+
console.log(
498+
` ${"Key:".padEnd(w)} ${chalk.white.bold(activeMeta.name)}`
499+
);
500+
}
501+
}
502+
const endpoint = config.baseUrl.replace("https://", "");
503+
console.log(` ${"Endpoint:".padEnd(w)} ${linkColor()(endpoint)}`);
504+
} finally {
505+
client.destroy();
506+
}
507+
} catch (error: unknown) {
508+
spinner.fail("API connection failed");
509+
if (process.env.ITERABLE_API_KEY) {
510+
showInfo("Source: ITERABLE_API_KEY environment variable");
511+
} else {
512+
const activeMeta = await keyManager
513+
.getActiveKeyMetadata()
514+
.catch(() => null);
515+
if (activeMeta) {
516+
showInfo(`Key: ${activeMeta.name}`);
517+
}
518+
}
519+
showError(error instanceof Error ? error.message : "Unknown error");
520+
process.exit(1);
521+
}
522+
break;
523+
}
524+
477525
default: {
478526
showSection("Available Commands", icons.key);
479527
console.log();

0 commit comments

Comments
 (0)