@youkno/edge-cli 1.20.2314

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,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildStateHeader = buildStateHeader;
4
+ exports.baseHeaders = baseHeaders;
5
+ exports.checkedText = checkedText;
6
+ function buildStateHeader(cfg) {
7
+ return `${cfg.product}.${cfg.env}`;
8
+ }
9
+ function baseHeaders(cfg, authHeader) {
10
+ const headers = {
11
+ "X-edge-state": buildStateHeader(cfg)
12
+ };
13
+ if (authHeader) {
14
+ headers.Authorization = authHeader;
15
+ }
16
+ return headers;
17
+ }
18
+ async function checkedText(response, context) {
19
+ const body = await response.text();
20
+ if (!response.ok) {
21
+ throw new Error(`${context} failed (${response.status}): ${body}`);
22
+ }
23
+ return body;
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@youkno/edge-cli",
3
+ "version": "1.20.2314",
4
+ "description": "Cross-platform Edge CLI",
5
+ "bin": "dist/index.js",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json",
12
+ "dev": "tsx src/index.ts",
13
+ "start": "node dist/index.js",
14
+ "typecheck": "tsc -p tsconfig.json --noEmit"
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "dependencies": {
20
+ "commander": "^14.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.4.0",
24
+ "tsx": "^4.20.5",
25
+ "typescript": "^5.9.2"
26
+ }
27
+ }
@@ -0,0 +1,28 @@
1
+ import { Command } from "commander";
2
+
3
+ import { resolveEffectiveConfig } from "../lib/config";
4
+ import { CliOptions } from "../lib/types";
5
+
6
+ export function registerConfig(program: Command): void {
7
+ program
8
+ .command("config")
9
+ .description("Print effective configuration")
10
+ .option("--json", "Print as JSON")
11
+ .action(async function configAction(this: Command, commandOptions: { json?: boolean }) {
12
+ const parent = this.parent;
13
+ const opts = (parent?.opts() ?? {}) as CliOptions;
14
+ const resolved = await resolveEffectiveConfig(opts);
15
+
16
+ if (commandOptions.json) {
17
+ process.stdout.write(`${JSON.stringify(resolved, null, 2)}\n`);
18
+ return;
19
+ }
20
+
21
+ process.stdout.write(`product=${resolved.product}\n`);
22
+ process.stdout.write(`env=${resolved.env}\n`);
23
+ process.stdout.write(`baseUrl=${resolved.baseUrl}\n`);
24
+ process.stdout.write(`clientId=${resolved.clientId ?? ""}\n`);
25
+ process.stdout.write(`auth=${resolved.auth ? "<configured>" : ""}\n`);
26
+ process.stdout.write(`configFiles=${resolved.configFiles.join(",") || "<none>"}\n`);
27
+ });
28
+ }
@@ -0,0 +1,35 @@
1
+ import { Command } from "commander";
2
+
3
+ import { resolveEffectiveConfig } from "../lib/config";
4
+ import { getText, basicAuthHeader } from "../lib/http";
5
+ import { CliOptions } from "../lib/types";
6
+
7
+ export function registerHealthCheck(program: Command): void {
8
+ program
9
+ .command("health-check")
10
+ .description("Call /health_check on the selected API")
11
+ .option("--key <key>", "Health check key; can also come from HEALTH_CHECK_KEY")
12
+ .action(async function healthCheckAction(this: Command, commandOptions: { key?: string }) {
13
+ const parent = this.parent;
14
+ const opts = (parent?.opts() ?? {}) as CliOptions;
15
+ const resolved = await resolveEffectiveConfig(opts);
16
+ const key = commandOptions.key ?? process.env.HEALTH_CHECK_KEY;
17
+
18
+ if (!key) {
19
+ throw new Error("Missing health check key. Pass --key or set HEALTH_CHECK_KEY.");
20
+ }
21
+
22
+ const stateHeader = `${resolved.product}.${resolved.env}`;
23
+ const url = `${resolved.baseUrl.replace(/\/$/, "")}/health_check`;
24
+ const { status, body } = await getText(url, {
25
+ "X-edge-state": stateHeader,
26
+ Authorization: basicAuthHeader("api_user", key)
27
+ });
28
+
29
+ if (status >= 400) {
30
+ throw new Error(`Health check failed (${status}) for ${url}: ${body}`);
31
+ }
32
+
33
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
34
+ });
35
+ }
@@ -0,0 +1,59 @@
1
+ import { Command } from "commander";
2
+
3
+ import { login, listLogins, logout, setDefaultLogin } from "../lib/auth";
4
+ import { resolveEffectiveConfig } from "../lib/config";
5
+ import { CliOptions } from "../lib/types";
6
+
7
+ export function registerLogin(program: Command): void {
8
+ program
9
+ .command("login")
10
+ .description("Authenticate via browser and store JWT tokens")
11
+ .action(async function loginAction(this: Command) {
12
+ const parent = this.parent;
13
+ const opts = (parent?.opts() ?? {}) as CliOptions;
14
+ const cfg = await resolveEffectiveConfig(opts);
15
+ const email = await login(cfg);
16
+ process.stdout.write(`Successfully authenticated as ${email}\n`);
17
+ });
18
+
19
+ program
20
+ .command("login:list")
21
+ .description("List saved accounts for current product/environment")
22
+ .action(async function listAction(this: Command) {
23
+ const parent = this.parent;
24
+ const opts = (parent?.opts() ?? {}) as CliOptions;
25
+ const cfg = await resolveEffectiveConfig(opts);
26
+ const accounts = listLogins(cfg);
27
+ if (accounts.length === 0) {
28
+ process.stdout.write(`No saved JWT session for ${cfg.product}/${cfg.env}.\n`);
29
+ return;
30
+ }
31
+ for (const account of accounts) {
32
+ process.stdout.write(`${account.isDefault ? "*" : " "} ${account.email}\n`);
33
+ }
34
+ });
35
+
36
+ program
37
+ .command("login:use")
38
+ .description("Set default account for current product/environment")
39
+ .argument("<email>")
40
+ .action(async function useAction(this: Command, email: string) {
41
+ const parent = this.parent;
42
+ const opts = (parent?.opts() ?? {}) as CliOptions;
43
+ const cfg = await resolveEffectiveConfig(opts);
44
+ setDefaultLogin(cfg, email);
45
+ process.stdout.write(`Default account: ${email}\n`);
46
+ });
47
+
48
+ program
49
+ .command("logout")
50
+ .description("Logout account (default if not provided)")
51
+ .argument("[email]")
52
+ .action(async function logoutAction(this: Command, email?: string) {
53
+ const parent = this.parent;
54
+ const opts = (parent?.opts() ?? {}) as CliOptions;
55
+ const cfg = await resolveEffectiveConfig(opts);
56
+ const removed = await logout(cfg, email);
57
+ process.stdout.write(`Logged out: ${removed}\n`);
58
+ });
59
+ }
@@ -0,0 +1,24 @@
1
+ import { Command } from "commander";
2
+
3
+ import { setDefaultProduct } from "../lib/config";
4
+
5
+ export function registerProduct(program: Command): void {
6
+ program
7
+ .command("product:default")
8
+ .description("Set or clear default PRODUCT in .edge-api")
9
+ .argument("[product]", "Product key (e.g. belong, stash, alleaves)")
10
+ .option("--file <path>", "Target edge-api config file (default: ~/.edge-api)")
11
+ .option("--clear", "Remove PRODUCT from target .edge-api file")
12
+ .action(function productDefaultAction(product: string | undefined, options: { file?: string; clear?: boolean }) {
13
+ if (!options.clear && !product) {
14
+ throw new Error("Missing product. Pass <product> or use --clear.");
15
+ }
16
+
17
+ const target = setDefaultProduct(options.clear ? undefined : product, options.file);
18
+ if (options.clear) {
19
+ process.stdout.write(`Cleared PRODUCT in ${target}\n`);
20
+ } else {
21
+ process.stdout.write(`Set PRODUCT=${(product ?? "").toLowerCase()} in ${target}\n`);
22
+ }
23
+ });
24
+ }
@@ -0,0 +1,213 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import readline from "node:readline";
4
+
5
+ import { Command } from "commander";
6
+
7
+ import { resolveAuthHeader } from "../lib/auth";
8
+ import { resolveEffectiveConfig } from "../lib/config";
9
+ import { baseHeaders, checkedText } from "../lib/request";
10
+ import { CliOptions, EffectiveConfig } from "../lib/types";
11
+
12
+ type ShellOptions = {
13
+ json?: boolean;
14
+ dryRun?: boolean;
15
+ validate?: boolean;
16
+ continueOnError?: boolean;
17
+ requestId?: string;
18
+ vars?: string[];
19
+ };
20
+
21
+ function resolveInlineScript(script: string, vars?: string[]): string {
22
+ const header = (vars ?? []).join("\n");
23
+ return header ? `${header}\n${script}\n` : `${script}\n`;
24
+ }
25
+
26
+ function renderEsh(filePath: string, stack: string[] = []): string {
27
+ const absolutePath = path.resolve(filePath);
28
+ if (stack.includes(absolutePath)) {
29
+ throw new Error(`Circular include detected: ${absolutePath}`);
30
+ }
31
+ if (!fs.existsSync(absolutePath)) {
32
+ throw new Error(`Script file not found: ${absolutePath}`);
33
+ }
34
+
35
+ const lines = fs.readFileSync(absolutePath, "utf8").split(/\r?\n/);
36
+ const out: string[] = [];
37
+ const nextStack = [...stack, absolutePath];
38
+
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const line = lines[i] ?? "";
41
+ const trimmed = line.trim();
42
+ if (trimmed.startsWith("include ")) {
43
+ let includePath = trimmed.replace(/^include\s+/, "").trim();
44
+ if ((includePath.startsWith('"') && includePath.endsWith('"')) || (includePath.startsWith("'") && includePath.endsWith("'"))) {
45
+ includePath = includePath.slice(1, -1);
46
+ }
47
+ if (!includePath) {
48
+ throw new Error(`Missing include path in ${absolutePath}:${i + 1}`);
49
+ }
50
+ const includeFile = path.isAbsolute(includePath)
51
+ ? includePath
52
+ : path.resolve(path.dirname(absolutePath), includePath);
53
+ out.push(renderEsh(includeFile, nextStack));
54
+ continue;
55
+ }
56
+ out.push(line);
57
+ }
58
+
59
+ return out.join("\n");
60
+ }
61
+
62
+ async function shellCmd(cfg: EffectiveConfig, authHeader: string, cmd: string, wantJson: boolean) {
63
+ const url = new URL(`${cfg.baseUrl.replace(/\/$/, "")}/shell`);
64
+ url.searchParams.set("cmd", cmd);
65
+
66
+ const headers = baseHeaders(cfg, authHeader) as Record<string, string>;
67
+ if (wantJson) {
68
+ headers.Accept = "application/json";
69
+ }
70
+
71
+ const response = await fetch(url, {
72
+ method: "GET",
73
+ headers
74
+ });
75
+ const body = await checkedText(response, "shell");
76
+
77
+ if (wantJson) {
78
+ try {
79
+ const parsed = JSON.parse(body);
80
+ process.stdout.write(`${JSON.stringify(parsed, null, 2)}\n`);
81
+ return;
82
+ } catch {
83
+ // fall through
84
+ }
85
+ }
86
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
87
+ }
88
+
89
+ async function shellScript(
90
+ cfg: EffectiveConfig,
91
+ authHeader: string,
92
+ scriptText: string,
93
+ options: ShellOptions,
94
+ scriptName = "inline.esh"
95
+ ) {
96
+ const url = new URL(`${cfg.baseUrl.replace(/\/$/, "")}/shell/script`);
97
+ url.searchParams.set("scriptName", scriptName);
98
+ url.searchParams.set("dryRun", String(Boolean(options.dryRun || options.validate)));
99
+ url.searchParams.set("continueOnError", String(Boolean(options.continueOnError)));
100
+ if (options.requestId) {
101
+ url.searchParams.set("requestId", options.requestId);
102
+ }
103
+
104
+ const headers = baseHeaders(cfg, authHeader) as Record<string, string>;
105
+ headers["Content-Type"] = "text/plain";
106
+ if (options.json) {
107
+ headers.Accept = "application/json";
108
+ }
109
+
110
+ const response = await fetch(url, {
111
+ method: "POST",
112
+ headers,
113
+ body: scriptText
114
+ });
115
+ const body = await checkedText(response, "shell script");
116
+
117
+ if (options.json) {
118
+ try {
119
+ const parsed = JSON.parse(body);
120
+ process.stdout.write(`${JSON.stringify(parsed, null, 2)}\n`);
121
+ return;
122
+ } catch {
123
+ // fall through
124
+ }
125
+ }
126
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
127
+ }
128
+
129
+ async function repl(cfg: EffectiveConfig, authHeader: string, options: ShellOptions) {
130
+ const rl = readline.createInterface({
131
+ input: process.stdin,
132
+ output: process.stdout,
133
+ terminal: true
134
+ });
135
+
136
+ process.stdout.write("Type Ctrl-D to exit, 'help' for help\n");
137
+ rl.setPrompt(`${cfg.env}> `);
138
+ rl.prompt();
139
+
140
+ rl.on("line", async (line) => {
141
+ const cmd = line.trim();
142
+ if (!cmd) {
143
+ rl.prompt();
144
+ return;
145
+ }
146
+ try {
147
+ await shellCmd(cfg, authHeader, cmd, Boolean(options.json));
148
+ } catch (err) {
149
+ process.stderr.write(`ERROR: ${err instanceof Error ? err.message : String(err)}\n`);
150
+ }
151
+ rl.prompt();
152
+ });
153
+
154
+ await new Promise<void>((resolve) => {
155
+ rl.on("close", () => {
156
+ process.stdout.write("\n");
157
+ resolve();
158
+ });
159
+ });
160
+ }
161
+
162
+ export function registerShell(program: Command): void {
163
+ program
164
+ .command("shell")
165
+ .description("Run shell command/script against Edge API")
166
+ .option("--json", "request JSON output when available")
167
+ .option("--dry-run", "run script in dry-run mode")
168
+ .option("--validate", "alias for --dry-run")
169
+ .option("--continue-on-error", "continue script execution on errors")
170
+ .option("--request-id <id>", "request ID")
171
+ .option("-v, --var <key=value>", "script variable", (value: string, prev: string[] = []) => {
172
+ if (!value.includes("=")) {
173
+ throw new Error(`Invalid variable assignment: ${value} (expected key=value)`);
174
+ }
175
+ prev.push(value);
176
+ return prev;
177
+ })
178
+ .argument("[target]", "Command or .esh file")
179
+ .argument("[inlineScript...]", "Inline script chunks when using --")
180
+ .action(async function shellAction(this: Command, target?: string, inlineScript?: string[]) {
181
+ const parent = this.parent;
182
+ const rootOpts = (parent?.opts() ?? {}) as CliOptions;
183
+ const cfg = await resolveEffectiveConfig(rootOpts);
184
+ const options = this.opts<ShellOptions>();
185
+ const authHeader = await resolveAuthHeader(cfg);
186
+
187
+ if (!target && (!inlineScript || inlineScript.length === 0)) {
188
+ await repl(cfg, authHeader, options);
189
+ return;
190
+ }
191
+
192
+ if (target?.endsWith(".esh")) {
193
+ const rendered = renderEsh(target);
194
+ const payload = resolveInlineScript(rendered, options.vars);
195
+ await shellScript(cfg, authHeader, payload, options, path.basename(target));
196
+ return;
197
+ }
198
+
199
+ if (inlineScript && inlineScript.length > 0) {
200
+ const payload = resolveInlineScript(inlineScript.join(" "), options.vars);
201
+ await shellScript(cfg, authHeader, payload, options, "inline.esh");
202
+ return;
203
+ }
204
+
205
+ if (options.dryRun || options.validate || options.continueOnError || options.requestId || (options.vars?.length ?? 0) > 0) {
206
+ const payload = resolveInlineScript(target ?? "", options.vars);
207
+ await shellScript(cfg, authHeader, payload, options, "inline.esh");
208
+ return;
209
+ }
210
+
211
+ await shellCmd(cfg, authHeader, target ?? "", Boolean(options.json));
212
+ });
213
+ }
@@ -0,0 +1,26 @@
1
+ import { Command } from "commander";
2
+
3
+ import { resolveAccessToken } from "../lib/auth";
4
+ import { resolveEffectiveConfig } from "../lib/config";
5
+ import { CliOptions } from "../lib/types";
6
+
7
+ export function registerToken(program: Command): void {
8
+ program
9
+ .command("token")
10
+ .description("Print a JWT access token for API calls (e.g. Postman)")
11
+ .option("--refresh", "Force refresh using refresh token when available")
12
+ .option("--header", "Print as Authorization header line")
13
+ .action(async function tokenAction(this: Command, commandOptions: { refresh?: boolean; header?: boolean }) {
14
+ const parent = this.parent;
15
+ const opts = (parent?.opts() ?? {}) as CliOptions;
16
+ const cfg = await resolveEffectiveConfig(opts);
17
+ const token = await resolveAccessToken(cfg, Boolean(commandOptions.refresh));
18
+
19
+ if (commandOptions.header) {
20
+ process.stdout.write(`Authorization: Bearer ${token}\n`);
21
+ return;
22
+ }
23
+
24
+ process.stdout.write(`${token}\n`);
25
+ });
26
+ }
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { Command } from "commander";
5
+
6
+ import { resolveAuthHeader } from "../lib/auth";
7
+ import { resolveEffectiveConfig } from "../lib/config";
8
+ import { baseHeaders, checkedText } from "../lib/request";
9
+ import { CliOptions } from "../lib/types";
10
+
11
+ export function registerUploadUsers(program: Command): void {
12
+ program
13
+ .command("upload-users")
14
+ .description("Upload users CSV for a client")
15
+ .option("-q, --quiet", "quiet mode")
16
+ .option("-l, --lenient", "lenient parser mode")
17
+ .option("-p, --partial", "partial mode")
18
+ .argument("<file>", "CSV file")
19
+ .action(async function uploadUsersAction(
20
+ this: Command,
21
+ file: string,
22
+ commandOptions: { quiet?: boolean; lenient?: boolean; partial?: boolean }
23
+ ) {
24
+ const parent = this.parent;
25
+ const opts = (parent?.opts() ?? {}) as CliOptions;
26
+ const cfg = await resolveEffectiveConfig(opts);
27
+
28
+ if (!cfg.clientId) {
29
+ throw new Error("No client ID specified. Pass --client or set CLIENT_ID in .edge-api.");
30
+ }
31
+
32
+ const absoluteFile = path.resolve(process.cwd(), file);
33
+ if (!fs.existsSync(absoluteFile)) {
34
+ throw new Error(`File not found: ${absoluteFile}`);
35
+ }
36
+
37
+ const authHeader = await resolveAuthHeader(cfg);
38
+ const url = new URL(`${cfg.baseUrl.replace(/\/$/, "")}/api/v1/clients/${cfg.clientId}/users`);
39
+ url.searchParams.set("dummy", "true");
40
+ if (commandOptions.partial) {
41
+ url.searchParams.set("partial", "true");
42
+ }
43
+ if (commandOptions.lenient) {
44
+ url.searchParams.set("lenient", "true");
45
+ }
46
+ if (commandOptions.quiet) {
47
+ url.searchParams.set("quiet", "true");
48
+ }
49
+
50
+ const data = fs.readFileSync(absoluteFile);
51
+ const form = new FormData();
52
+ form.append("file", new Blob([data], { type: "text/csv" }), path.basename(absoluteFile));
53
+
54
+ const response = await fetch(url, {
55
+ method: "POST",
56
+ headers: baseHeaders(cfg, authHeader),
57
+ body: form
58
+ });
59
+
60
+ const body = await checkedText(response, "upload-users");
61
+ process.stdout.write(body.endsWith("\n") ? body : `${body}\n`);
62
+ });
63
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "api": {
3
+ "defaultBaseUrl": "https://api.youkno.ai",
4
+ "products": {
5
+ "youkno": {
6
+ "baseUrl": "https://api.youkno.ai",
7
+ "active": false,
8
+ "aliases": ["belong"],
9
+ "consoleBaseProd": "https://belong-prod.web.app",
10
+ "consoleBaseTest": "https://belong-prod--testapp-q8kk4dm6.web.app"
11
+ },
12
+ "stash": {
13
+ "baseUrl": "https://api.mystash.co",
14
+ "active": false,
15
+ "aliases": [],
16
+ "consoleBaseProd": "https://stash-prod.web.app",
17
+ "consoleBaseTest": "https://stash-prod--testapp-ef23abcf.web.app"
18
+ },
19
+ "alleaves": {
20
+ "baseUrl": "https://api.alleaves.shop",
21
+ "active": true,
22
+ "aliases": [],
23
+ "consoleBaseProd": "https://console.alleaves.shop",
24
+ "consoleBaseTest": "https://alleaves-prod--testapp-vn1b2q7v.web.app"
25
+ },
26
+ "vera": {
27
+ "baseUrl": "https://edge-server-dfkcix6pxa-uc.a.run.app",
28
+ "active": false,
29
+ "aliases": [],
30
+ "consoleBaseProd": "https://vera-af0c2.web.app",
31
+ "consoleBaseTest": "https://vera-af0c2--testapp-23e7fe3c.web.app"
32
+ },
33
+ "vip": {
34
+ "baseUrl": "https://edge-server-a465iqwona-uc.a.run.app",
35
+ "active": false,
36
+ "aliases": [],
37
+ "consoleBaseProd": "https://vip-app-6a3d4.web.app",
38
+ "consoleBaseTest": "https://vip-app-6a3d4--testapp-h34bc19a.web.app"
39
+ },
40
+ "dinamo1948": {
41
+ "baseUrl": "https://api.dinamo1948.ro",
42
+ "active": true,
43
+ "aliases": [],
44
+ "consoleBaseProd": "https://console.dinamo1948.ro",
45
+ "consoleBaseTest": "https://dinamo1948-prod--testapp-f48fznbf.web.app"
46
+ },
47
+ "propatrimonio": {
48
+ "baseUrl": "https://api.propatrimonio.org",
49
+ "active": true,
50
+ "aliases": []
51
+ },
52
+ "campaigns": {
53
+ "baseUrl": "https://api.getcampaigns.com",
54
+ "active": true,
55
+ "aliases": [],
56
+ "consoleBaseProd": "https://console.getcampaigns.com",
57
+ "consoleBaseTest": "https://campaigns-abd4a--testapp-ht9twxoz.web.app"
58
+ }
59
+ }
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+
5
+ import { registerConfig } from "./commands/config";
6
+ import { registerHealthCheck } from "./commands/healthCheck";
7
+ import { registerLogin } from "./commands/login";
8
+ import { registerProduct } from "./commands/product";
9
+ import { registerShell } from "./commands/shell";
10
+ import { registerToken } from "./commands/token";
11
+ import { registerUploadUsers } from "./commands/uploadUsers";
12
+ import { CliEnv } from "./lib/types";
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name("edge-cli")
18
+ .description("Edge API command line client")
19
+ .version("0.2.0")
20
+ .option("-p, --product <product>", "Product key (youkno, stash, alleaves, ...)")
21
+ .option("--env <env>", "Environment (devlocal|devel|prod)", "prod")
22
+ .option("--base-url <url>", "Override API base URL")
23
+ .option("--client <clientId>", "Client ID")
24
+ .option("--auth <token:secret>", "Client auth token")
25
+ .option("--config-path <path>", "Additional edge-api style config file")
26
+ .hook("preAction", (command) => {
27
+ const env = command.opts().env as string;
28
+ const validEnvs: CliEnv[] = ["devlocal", "devel", "prod"];
29
+ if (!validEnvs.includes(env as CliEnv)) {
30
+ throw new Error(`Invalid --env '${env}'. Expected one of: ${validEnvs.join(", ")}`);
31
+ }
32
+ });
33
+
34
+ registerConfig(program);
35
+ registerHealthCheck(program);
36
+ registerProduct(program);
37
+ registerLogin(program);
38
+ registerToken(program);
39
+ registerShell(program);
40
+ registerUploadUsers(program);
41
+
42
+ program.parseAsync(process.argv).catch((error) => {
43
+ process.stderr.write(`ERROR: ${error instanceof Error ? error.message : String(error)}\n`);
44
+ process.exit(1);
45
+ });