@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,253 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import readline from "node:readline/promises";
5
+
6
+ import bundledApiConfig from "../default-edge-api-config.json";
7
+ import { CliEnv, CliOptions, EdgeApiConfig, EffectiveConfig, ProductConfig } from "./types";
8
+
9
+ const EDGE_API_CONFIG_URL = "https://cdn.youkno.ai/tools/edge-api-config.json";
10
+ const DEFAULT_EDGE_API_PATH = path.join(os.homedir(), ".edge-api");
11
+
12
+ const FILE_KEYS = ["PRODUCT", "ENV", "BASE_URL", "CLIENT_ID", "CLIENT_AUTH"] as const;
13
+
14
+ type EdgeApiFileConfig = Partial<Record<(typeof FILE_KEYS)[number], string>>;
15
+
16
+ function expandPath(inputPath: string): string {
17
+ if (inputPath.startsWith("~/")) {
18
+ return path.join(os.homedir(), inputPath.slice(2));
19
+ }
20
+ if (inputPath === "~") {
21
+ return os.homedir();
22
+ }
23
+ return inputPath;
24
+ }
25
+
26
+ function parseEdgeApiFile(filePath: string): EdgeApiFileConfig {
27
+ const out: EdgeApiFileConfig = {};
28
+ if (!fs.existsSync(filePath)) {
29
+ return out;
30
+ }
31
+
32
+ const content = fs.readFileSync(filePath, "utf8");
33
+ const lines = content.split(/\r?\n/);
34
+ for (const lineRaw of lines) {
35
+ const line = lineRaw.trim();
36
+ if (!line || line.startsWith("#")) {
37
+ continue;
38
+ }
39
+
40
+ const eqIndex = line.indexOf("=");
41
+ if (eqIndex <= 0) {
42
+ continue;
43
+ }
44
+
45
+ const key = line.slice(0, eqIndex).trim();
46
+ if (!FILE_KEYS.includes(key as (typeof FILE_KEYS)[number])) {
47
+ continue;
48
+ }
49
+
50
+ let value = line.slice(eqIndex + 1).trim();
51
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
52
+ value = value.slice(1, -1);
53
+ }
54
+
55
+ out[key as (typeof FILE_KEYS)[number]] = value;
56
+ }
57
+
58
+ return out;
59
+ }
60
+
61
+ function toCliEnv(env?: string): CliEnv {
62
+ if (env === "devlocal" || env === "devel" || env === "prod") {
63
+ return env;
64
+ }
65
+ return "prod";
66
+ }
67
+
68
+ function resolveProductsConfig(): EdgeApiConfig {
69
+ const candidatePaths = [
70
+ process.env.EDGE_API_CONFIG_PATH ? path.resolve(expandPath(process.env.EDGE_API_CONFIG_PATH)) : undefined,
71
+ path.resolve(process.cwd(), "edge-api-config.json"),
72
+ path.resolve(process.cwd(), "..", "edge-api-config.json"),
73
+ path.resolve(__dirname, "..", "..", "..", "edge-api-config.json")
74
+ ].filter((p): p is string => Boolean(p));
75
+
76
+ for (const candidatePath of candidatePaths) {
77
+ if (!fs.existsSync(candidatePath)) {
78
+ continue;
79
+ }
80
+ try {
81
+ const content = fs.readFileSync(candidatePath, "utf8");
82
+ return JSON.parse(content) as EdgeApiConfig;
83
+ } catch {
84
+ // Try next candidate
85
+ }
86
+ }
87
+
88
+ return bundledApiConfig as EdgeApiConfig;
89
+ }
90
+
91
+ function findProduct(product: string, config: EdgeApiConfig): ProductConfig | undefined {
92
+ const products = config.api?.products ?? {};
93
+ if (products[product]) {
94
+ return products[product];
95
+ }
96
+ for (const productConfig of Object.values(products)) {
97
+ const aliases = productConfig.aliases ?? [];
98
+ if (aliases.includes(product)) {
99
+ return productConfig;
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ function listProducts(config: EdgeApiConfig): string[] {
106
+ const products = config.api?.products ?? {};
107
+ return Object.entries(products)
108
+ .filter(([, value]) => value.active !== false)
109
+ .map(([key]) => key)
110
+ .sort((a, b) => a.localeCompare(b));
111
+ }
112
+
113
+ async function selectProductInteractively(products: string[]): Promise<string> {
114
+ const rl = readline.createInterface({
115
+ input: process.stdin,
116
+ output: process.stdout
117
+ });
118
+
119
+ try {
120
+ process.stdout.write("Select product:\n");
121
+ products.forEach((p, idx) => {
122
+ process.stdout.write(` ${idx + 1}) ${p}\n`);
123
+ });
124
+
125
+ while (true) {
126
+ const inputRaw = await rl.question("Choose product number or name: ");
127
+ const input = inputRaw.trim().toLowerCase();
128
+ if (!input) {
129
+ continue;
130
+ }
131
+
132
+ if (/^\d+$/.test(input)) {
133
+ const index = Number(input) - 1;
134
+ if (index >= 0 && index < products.length) {
135
+ return products[index];
136
+ }
137
+ }
138
+
139
+ if (products.includes(input)) {
140
+ return input;
141
+ }
142
+
143
+ process.stdout.write("Invalid selection.\n");
144
+ }
145
+ } finally {
146
+ rl.close();
147
+ }
148
+ }
149
+
150
+ function resolveProductBaseUrl(product: string, config: EdgeApiConfig): string | undefined {
151
+ const resolved = findProduct(product, config);
152
+ return resolved?.baseUrl ?? config.api?.defaultBaseUrl;
153
+ }
154
+
155
+ function resolveConsoleBaseUrl(product: string, env: CliEnv, config: EdgeApiConfig): string | undefined {
156
+ const resolved = findProduct(product, config);
157
+ if (!resolved) {
158
+ return undefined;
159
+ }
160
+ return env === "prod" ? resolved.consoleBaseProd : resolved.consoleBaseTest;
161
+ }
162
+
163
+ function readMergedFileConfig(options: CliOptions): { merged: EdgeApiFileConfig; usedFiles: string[] } {
164
+ const configFiles = [
165
+ "/etc/edge-api",
166
+ path.resolve(process.cwd(), ".edge-api"),
167
+ path.join(os.homedir(), ".edge-api")
168
+ ];
169
+
170
+ if (options.configPath) {
171
+ configFiles.unshift(path.resolve(process.cwd(), options.configPath));
172
+ }
173
+
174
+ const fileConfig = configFiles
175
+ .filter((filePath, index, arr) => arr.indexOf(filePath) === index)
176
+ .map((filePath) => ({ filePath, cfg: parseEdgeApiFile(filePath) }))
177
+ .filter((entry) => Object.keys(entry.cfg).length > 0);
178
+
179
+ const merged: EdgeApiFileConfig = {};
180
+ for (const entry of fileConfig) {
181
+ Object.assign(merged, entry.cfg);
182
+ }
183
+
184
+ return {
185
+ merged,
186
+ usedFiles: fileConfig.map((entry) => entry.filePath)
187
+ };
188
+ }
189
+
190
+ export async function resolveEffectiveConfig(options: CliOptions): Promise<EffectiveConfig> {
191
+ const { merged, usedFiles } = readMergedFileConfig(options);
192
+ const env = toCliEnv(options.env ?? merged.ENV);
193
+ const productsConfig = resolveProductsConfig();
194
+
195
+ let product = (options.product ?? merged.PRODUCT ?? "").trim().toLowerCase();
196
+ if (!product) {
197
+ if (process.stdin.isTTY && process.stdout.isTTY) {
198
+ const available = listProducts(productsConfig);
199
+ if (available.length > 0) {
200
+ product = await selectProductInteractively(available);
201
+ }
202
+ }
203
+ }
204
+
205
+ if (!product) {
206
+ throw new Error(
207
+ "No product specified. Pass --product <name>, set PRODUCT in ~/.edge-api, or run 'edge-cli product:default <name>'."
208
+ );
209
+ }
210
+
211
+ let baseUrl = options.baseUrl ?? merged.BASE_URL;
212
+ if (!baseUrl) {
213
+ if (env === "devlocal") {
214
+ baseUrl = "http://localhost:8080";
215
+ } else {
216
+ baseUrl = resolveProductBaseUrl(product, productsConfig);
217
+ }
218
+ }
219
+
220
+ if (!baseUrl) {
221
+ throw new Error(
222
+ `Unable to resolve base URL. Set --base-url, BASE_URL in .edge-api, or configure product '${product}' in edge-api-config.json (${EDGE_API_CONFIG_URL}).`
223
+ );
224
+ }
225
+
226
+ return {
227
+ product,
228
+ env,
229
+ baseUrl,
230
+ consoleBaseUrl: resolveConsoleBaseUrl(product, env, productsConfig),
231
+ clientId: options.clientId ?? merged.CLIENT_ID,
232
+ auth: options.auth ?? merged.CLIENT_AUTH,
233
+ configFiles: usedFiles
234
+ };
235
+ }
236
+
237
+ export function setDefaultProduct(product: string | undefined, filePath?: string): string {
238
+ const targetFile = path.resolve(expandPath(filePath ?? DEFAULT_EDGE_API_PATH));
239
+ const targetDir = path.dirname(targetFile);
240
+ fs.mkdirSync(targetDir, { recursive: true });
241
+
242
+ const existing = fs.existsSync(targetFile) ? fs.readFileSync(targetFile, "utf8") : "";
243
+ const lines = existing ? existing.split(/\r?\n/) : [];
244
+ const kept = lines.filter((line) => !line.trim().startsWith("PRODUCT="));
245
+
246
+ if (product && product.trim()) {
247
+ kept.push(`PRODUCT=${product.trim().toLowerCase()}`);
248
+ }
249
+
250
+ const content = kept.filter((line, idx, arr) => !(idx === arr.length - 1 && line === "")).join("\n");
251
+ fs.writeFileSync(targetFile, content ? `${content}\n` : "", "utf8");
252
+ return targetFile;
253
+ }
@@ -0,0 +1,12 @@
1
+ export function basicAuthHeader(username: string, password: string): string {
2
+ return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
3
+ }
4
+
5
+ export async function getText(url: string, headers: Record<string, string>): Promise<{ status: number; body: string }> {
6
+ const response = await fetch(url, {
7
+ method: "GET",
8
+ headers
9
+ });
10
+ const body = await response.text();
11
+ return { status: response.status, body };
12
+ }
@@ -0,0 +1,23 @@
1
+ import { EffectiveConfig } from "./types";
2
+
3
+ export function buildStateHeader(cfg: EffectiveConfig): string {
4
+ return `${cfg.product}.${cfg.env}`;
5
+ }
6
+
7
+ export function baseHeaders(cfg: EffectiveConfig, authHeader?: string): HeadersInit {
8
+ const headers: Record<string, string> = {
9
+ "X-edge-state": buildStateHeader(cfg)
10
+ };
11
+ if (authHeader) {
12
+ headers.Authorization = authHeader;
13
+ }
14
+ return headers;
15
+ }
16
+
17
+ export async function checkedText(response: Response, context: string): Promise<string> {
18
+ const body = await response.text();
19
+ if (!response.ok) {
20
+ throw new Error(`${context} failed (${response.status}): ${body}`);
21
+ }
22
+ return body;
23
+ }
@@ -0,0 +1,35 @@
1
+ export type CliEnv = "devlocal" | "devel" | "prod";
2
+
3
+ export type CliOptions = {
4
+ product?: string;
5
+ env?: CliEnv;
6
+ baseUrl?: string;
7
+ clientId?: string;
8
+ auth?: string;
9
+ configPath?: string;
10
+ };
11
+
12
+ export type ProductConfig = {
13
+ baseUrl?: string;
14
+ active?: boolean;
15
+ aliases?: string[];
16
+ consoleBaseProd?: string;
17
+ consoleBaseTest?: string;
18
+ };
19
+
20
+ export type EdgeApiConfig = {
21
+ api?: {
22
+ defaultBaseUrl?: string;
23
+ products?: Record<string, ProductConfig>;
24
+ };
25
+ };
26
+
27
+ export type EffectiveConfig = {
28
+ product: string;
29
+ env: CliEnv;
30
+ baseUrl: string;
31
+ consoleBaseUrl?: string;
32
+ clientId?: string;
33
+ auth?: string;
34
+ configFiles: string[];
35
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "Node",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "resolveJsonModule": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src"
12
+ },
13
+ "include": ["src/**/*.ts", "src/**/*.json"]
14
+ }