@beaulewis/saas-cli 1.0.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.
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/bin/saas.js +2 -0
- package/dist/chunk-26VE6QJ4.js +120 -0
- package/dist/chunk-26VE6QJ4.js.map +1 -0
- package/dist/chunk-3KD5CFV3.js +196 -0
- package/dist/chunk-3KD5CFV3.js.map +1 -0
- package/dist/chunk-5BCEXHNM.js +108 -0
- package/dist/chunk-5BCEXHNM.js.map +1 -0
- package/dist/chunk-N4OIAZSA.js +110 -0
- package/dist/chunk-N4OIAZSA.js.map +1 -0
- package/dist/chunk-ZD2ZSBK3.js +224 -0
- package/dist/chunk-ZD2ZSBK3.js.map +1 -0
- package/dist/dart-DXLFNGHR.js +41 -0
- package/dist/dart-DXLFNGHR.js.map +1 -0
- package/dist/drift-XYY4D366.js +59 -0
- package/dist/drift-XYY4D366.js.map +1 -0
- package/dist/flutter-J5BYPVIW.js +41 -0
- package/dist/flutter-J5BYPVIW.js.map +1 -0
- package/dist/freezed-QXFQ4GJC.js +58 -0
- package/dist/freezed-QXFQ4GJC.js.map +1 -0
- package/dist/gorouter-QBMTTFVR.js +56 -0
- package/dist/gorouter-QBMTTFVR.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1437 -0
- package/dist/index.js.map +1 -0
- package/dist/package-QO75XHBD.js +43 -0
- package/dist/package-QO75XHBD.js.map +1 -0
- package/dist/powersync-I3LR7TDN.js +37 -0
- package/dist/powersync-I3LR7TDN.js.map +1 -0
- package/dist/repository-BAOVD3NG.js +34 -0
- package/dist/repository-BAOVD3NG.js.map +1 -0
- package/dist/riverpod-XUU656PM.js +42 -0
- package/dist/riverpod-XUU656PM.js.map +1 -0
- package/dist/widget-YDKHPRXM.js +42 -0
- package/dist/widget-YDKHPRXM.js.map +1 -0
- package/package.json +89 -0
- package/templates/drift/dao.hbs +51 -0
- package/templates/drift/migration.hbs +15 -0
- package/templates/freezed/model.hbs +20 -0
- package/templates/gorouter/route.hbs +18 -0
- package/templates/powersync/rules.hbs +10 -0
- package/templates/powersync/schema.hbs +19 -0
- package/templates/repository/repository.hbs +62 -0
- package/templates/riverpod/async-notifier.hbs +44 -0
- package/templates/riverpod/family.hbs +9 -0
- package/templates/riverpod/future.hbs +9 -0
- package/templates/riverpod/notifier.hbs +34 -0
- package/templates/riverpod/stream.hbs +9 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigError
|
|
3
|
+
} from "./chunk-5BCEXHNM.js";
|
|
4
|
+
|
|
5
|
+
// src/utils/config.ts
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { dirname, join } from "path";
|
|
10
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
11
|
+
function getGlobalConfigDir() {
|
|
12
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
13
|
+
if (xdgConfig) {
|
|
14
|
+
return join(xdgConfig, "saas-cli");
|
|
15
|
+
}
|
|
16
|
+
return join(homedir(), ".config", "saas-cli");
|
|
17
|
+
}
|
|
18
|
+
function getGlobalConfigPath() {
|
|
19
|
+
return join(getGlobalConfigDir(), "config.yaml");
|
|
20
|
+
}
|
|
21
|
+
function getCacheDir() {
|
|
22
|
+
const xdgCache = process.env.XDG_CACHE_HOME;
|
|
23
|
+
if (xdgCache) {
|
|
24
|
+
return join(xdgCache, "saas-cli");
|
|
25
|
+
}
|
|
26
|
+
return join(homedir(), ".cache", "saas-cli");
|
|
27
|
+
}
|
|
28
|
+
async function loadGlobalConfig() {
|
|
29
|
+
const configPath = getGlobalConfigPath();
|
|
30
|
+
if (!existsSync(configPath)) {
|
|
31
|
+
return getDefaultGlobalConfig();
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const content = await readFile(configPath, "utf-8");
|
|
35
|
+
const config = parseYaml(content);
|
|
36
|
+
return { ...getDefaultGlobalConfig(), ...config };
|
|
37
|
+
} catch (_error) {
|
|
38
|
+
throw new ConfigError(
|
|
39
|
+
`Failed to parse global config at ${configPath}`,
|
|
40
|
+
"Check the YAML syntax in your config file"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function getDefaultGlobalConfig() {
|
|
45
|
+
return {
|
|
46
|
+
context7: {
|
|
47
|
+
enabled: true
|
|
48
|
+
},
|
|
49
|
+
defaults: {
|
|
50
|
+
outputFormat: "pretty",
|
|
51
|
+
cacheTtl: 3600
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function getAPIKey(service, globalConfig) {
|
|
56
|
+
const envMap = {
|
|
57
|
+
context7: "CONTEXT7_API_KEY",
|
|
58
|
+
perplexity: "PERPLEXITY_API_KEY",
|
|
59
|
+
supabase: "SUPABASE_SERVICE_KEY",
|
|
60
|
+
redis: "REDIS_URL",
|
|
61
|
+
cloudflare: "CF_API_TOKEN",
|
|
62
|
+
onesignal: "ONESIGNAL_API_KEY",
|
|
63
|
+
posthog: "POSTHOG_API_KEY"
|
|
64
|
+
};
|
|
65
|
+
const envKey = envMap[service];
|
|
66
|
+
if (envKey && process.env[envKey]) {
|
|
67
|
+
return process.env[envKey];
|
|
68
|
+
}
|
|
69
|
+
const configValue = globalConfig[service];
|
|
70
|
+
if (configValue && typeof configValue === "object" && "apiKey" in configValue) {
|
|
71
|
+
return configValue.apiKey;
|
|
72
|
+
}
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/utils/output.ts
|
|
77
|
+
import Table from "cli-table3";
|
|
78
|
+
import pc from "picocolors";
|
|
79
|
+
function formatTable(headers, rows, options = {}) {
|
|
80
|
+
const table = new Table({
|
|
81
|
+
head: options.head !== false ? headers.map((h) => pc.cyan(h)) : void 0,
|
|
82
|
+
style: {
|
|
83
|
+
head: [],
|
|
84
|
+
border: []
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
table.push(row);
|
|
89
|
+
}
|
|
90
|
+
return table.toString();
|
|
91
|
+
}
|
|
92
|
+
function formatBox(title, content) {
|
|
93
|
+
const line = "\u2501".repeat(70);
|
|
94
|
+
const lines = [
|
|
95
|
+
pc.cyan(line),
|
|
96
|
+
pc.cyan(pc.bold(title)),
|
|
97
|
+
pc.cyan(line),
|
|
98
|
+
"",
|
|
99
|
+
content,
|
|
100
|
+
"",
|
|
101
|
+
pc.cyan(line)
|
|
102
|
+
];
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/utils/cache.ts
|
|
107
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
108
|
+
import { join as join2 } from "path";
|
|
109
|
+
import Keyv from "keyv";
|
|
110
|
+
import { KeyvFile } from "keyv-file";
|
|
111
|
+
var cacheInstance = null;
|
|
112
|
+
var DEFAULT_TTL = 3600 * 1e3;
|
|
113
|
+
async function getCache() {
|
|
114
|
+
if (cacheInstance) {
|
|
115
|
+
return cacheInstance;
|
|
116
|
+
}
|
|
117
|
+
const cacheDir = getCacheDir();
|
|
118
|
+
await mkdir2(cacheDir, { recursive: true });
|
|
119
|
+
const cachePath = join2(cacheDir, "cache.json");
|
|
120
|
+
cacheInstance = new Keyv({
|
|
121
|
+
store: new KeyvFile({ filename: cachePath }),
|
|
122
|
+
namespace: "saas-cli"
|
|
123
|
+
});
|
|
124
|
+
cacheInstance.on("error", () => {
|
|
125
|
+
});
|
|
126
|
+
return cacheInstance;
|
|
127
|
+
}
|
|
128
|
+
async function cacheGet(key) {
|
|
129
|
+
try {
|
|
130
|
+
const cache = await getCache();
|
|
131
|
+
return await cache.get(key);
|
|
132
|
+
} catch {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function cacheSet(key, value, ttl = DEFAULT_TTL) {
|
|
137
|
+
try {
|
|
138
|
+
const cache = await getCache();
|
|
139
|
+
await cache.set(key, value, ttl);
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function cacheKey(...parts) {
|
|
144
|
+
return parts.map((p) => p.toLowerCase().replace(/\s+/g, "-")).join(":");
|
|
145
|
+
}
|
|
146
|
+
async function cachedFetch(key, fetchFn, ttl = DEFAULT_TTL, skipCache = false) {
|
|
147
|
+
if (!skipCache) {
|
|
148
|
+
const cached = await cacheGet(key);
|
|
149
|
+
if (cached !== void 0) {
|
|
150
|
+
return cached;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const result = await fetchFn();
|
|
154
|
+
await cacheSet(key, result, ttl);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/services/http.ts
|
|
159
|
+
import got from "got";
|
|
160
|
+
var defaultOptions = {
|
|
161
|
+
timeout: {
|
|
162
|
+
request: 3e4
|
|
163
|
+
},
|
|
164
|
+
retry: {
|
|
165
|
+
limit: 3,
|
|
166
|
+
methods: ["GET"],
|
|
167
|
+
statusCodes: [408, 429, 500, 502, 503, 504],
|
|
168
|
+
errorCodes: ["ETIMEDOUT", "ECONNRESET", "ECONNREFUSED"]
|
|
169
|
+
},
|
|
170
|
+
headers: {
|
|
171
|
+
"User-Agent": "saas-cli/1.0.0"
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
function createAuthenticatedClient(baseUrl, apiKey, options = {}) {
|
|
175
|
+
return got.extend({
|
|
176
|
+
...defaultOptions,
|
|
177
|
+
...options,
|
|
178
|
+
prefixUrl: baseUrl,
|
|
179
|
+
headers: {
|
|
180
|
+
...defaultOptions.headers,
|
|
181
|
+
Authorization: `Bearer ${apiKey}`,
|
|
182
|
+
"Content-Type": "application/json"
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
loadGlobalConfig,
|
|
189
|
+
getAPIKey,
|
|
190
|
+
cacheKey,
|
|
191
|
+
cachedFetch,
|
|
192
|
+
createAuthenticatedClient,
|
|
193
|
+
formatTable,
|
|
194
|
+
formatBox
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=chunk-3KD5CFV3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/config.ts","../src/utils/output.ts","../src/utils/cache.ts","../src/services/http.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport type { GlobalConfig, ProjectConfig } from '../types/index.js';\nimport { ConfigError } from './error.js';\n\n/**\n * Get the global config directory path\n */\nexport function getGlobalConfigDir(): string {\n // Respect XDG_CONFIG_HOME on Linux\n const xdgConfig = process.env.XDG_CONFIG_HOME;\n if (xdgConfig) {\n return join(xdgConfig, 'saas-cli');\n }\n return join(homedir(), '.config', 'saas-cli');\n}\n\n/**\n * Get the global config file path\n */\nexport function getGlobalConfigPath(): string {\n return join(getGlobalConfigDir(), 'config.yaml');\n}\n\n/**\n * Get the cache directory path\n */\nexport function getCacheDir(): string {\n const xdgCache = process.env.XDG_CACHE_HOME;\n if (xdgCache) {\n return join(xdgCache, 'saas-cli');\n }\n return join(homedir(), '.cache', 'saas-cli');\n}\n\n/**\n * Load global configuration from ~/.config/saas-cli/config.yaml\n */\nexport async function loadGlobalConfig(): Promise<GlobalConfig> {\n const configPath = getGlobalConfigPath();\n\n if (!existsSync(configPath)) {\n return getDefaultGlobalConfig();\n }\n\n try {\n const content = await readFile(configPath, 'utf-8');\n const config = parseYaml(content) as GlobalConfig;\n return { ...getDefaultGlobalConfig(), ...config };\n } catch (_error) {\n throw new ConfigError(\n `Failed to parse global config at ${configPath}`,\n 'Check the YAML syntax in your config file',\n );\n }\n}\n\n/**\n * Save global configuration to ~/.config/saas-cli/config.yaml\n */\nexport async function saveGlobalConfig(config: GlobalConfig): Promise<void> {\n const configPath = getGlobalConfigPath();\n const configDir = dirname(configPath);\n\n try {\n await mkdir(configDir, { recursive: true, mode: 0o700 });\n const content = stringifyYaml(config as Record<string, unknown>);\n await writeFile(configPath, content, { mode: 0o600 });\n } catch (_error) {\n throw new ConfigError(\n `Failed to save global config to ${configPath}`,\n 'Check file permissions',\n );\n }\n}\n\n/**\n * Load project configuration from ./saas.yaml\n */\nexport async function loadProjectConfig(searchPath?: string): Promise<ProjectConfig | null> {\n const startPath = searchPath ?? process.cwd();\n const configPath = join(startPath, 'saas.yaml');\n\n if (!existsSync(configPath)) {\n // Also check for saas.yml\n const altPath = join(startPath, 'saas.yml');\n if (!existsSync(altPath)) {\n return null;\n }\n return loadProjectConfigFromPath(altPath);\n }\n\n return loadProjectConfigFromPath(configPath);\n}\n\n/**\n * Load project config from a specific path\n */\nasync function loadProjectConfigFromPath(configPath: string): Promise<ProjectConfig> {\n try {\n const content = await readFile(configPath, 'utf-8');\n return parseYaml(content) as ProjectConfig;\n } catch (_error) {\n throw new ConfigError(\n `Failed to parse project config at ${configPath}`,\n 'Check the YAML syntax in your saas.yaml file',\n );\n }\n}\n\n/**\n * Get default global configuration\n */\nexport function getDefaultGlobalConfig(): GlobalConfig {\n return {\n context7: {\n enabled: true,\n },\n defaults: {\n outputFormat: 'pretty',\n cacheTtl: 3600,\n },\n };\n}\n\n/**\n * Get a config value with environment variable override\n * Priority: env var > project config > global config > default\n */\nexport function getConfigValue<T>(\n key: string,\n globalConfig: GlobalConfig,\n projectConfig: ProjectConfig | null,\n defaultValue: T,\n): T {\n // Check environment variable first (convert key to SCREAMING_SNAKE_CASE)\n const envKey = key\n .replace(/([A-Z])/g, '_$1')\n .toUpperCase()\n .replace(/^_/, '');\n const envValue = process.env[envKey];\n\n if (envValue !== undefined) {\n // Try to parse as JSON for complex types, fallback to string\n try {\n return JSON.parse(envValue) as T;\n } catch {\n return envValue as unknown as T;\n }\n }\n\n // Navigate nested keys\n const keys = key.split('.');\n\n // Check project config\n if (projectConfig) {\n const projectValue = getNestedValue(projectConfig as unknown as Record<string, unknown>, keys);\n if (projectValue !== undefined) {\n return projectValue as T;\n }\n }\n\n // Check global config\n const globalValue = getNestedValue(globalConfig as unknown as Record<string, unknown>, keys);\n if (globalValue !== undefined) {\n return globalValue as T;\n }\n\n return defaultValue;\n}\n\n/**\n * Get a nested value from an object using an array of keys\n */\nfunction getNestedValue(obj: Record<string, unknown>, keys: string[]): unknown {\n let current: unknown = obj;\n\n for (const key of keys) {\n if (current === null || current === undefined || typeof current !== 'object') {\n return undefined;\n }\n current = (current as Record<string, unknown>)[key];\n }\n\n return current;\n}\n\n/**\n * Get API key from environment or config\n */\nexport function getAPIKey(\n service:\n | 'context7'\n | 'perplexity'\n | 'supabase'\n | 'redis'\n | 'cloudflare'\n | 'onesignal'\n | 'posthog',\n globalConfig: GlobalConfig,\n): string | undefined {\n // Environment variable takes precedence\n const envMap: Record<string, string> = {\n context7: 'CONTEXT7_API_KEY',\n perplexity: 'PERPLEXITY_API_KEY',\n supabase: 'SUPABASE_SERVICE_KEY',\n redis: 'REDIS_URL',\n cloudflare: 'CF_API_TOKEN',\n onesignal: 'ONESIGNAL_API_KEY',\n posthog: 'POSTHOG_API_KEY',\n };\n\n const envKey = envMap[service];\n if (envKey && process.env[envKey]) {\n return process.env[envKey];\n }\n\n // Fallback to config\n const configValue = globalConfig[service];\n if (configValue && typeof configValue === 'object' && 'apiKey' in configValue) {\n return configValue.apiKey;\n }\n\n return undefined;\n}\n\n/**\n * Expand environment variables in a string\n * Supports ${VAR} and $VAR syntax\n */\nexport function expandEnvVars(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}|\\$([A-Z_][A-Z0-9_]*)/gi, (_, braced, unbraced) => {\n const varName = braced || unbraced;\n return process.env[varName] ?? '';\n });\n}\n","import Table from 'cli-table3';\nimport pc from 'picocolors';\nimport type { GlobalOptions, OutputFormat } from '../types/index.js';\n\n/**\n * Check if colors should be disabled\n */\nexport function shouldUseColor(): boolean {\n return !process.env.NO_COLOR && process.stdout.isTTY !== false;\n}\n\n/**\n * Check if we're in a CI environment\n */\nexport function isCI(): boolean {\n return Boolean(process.env.CI || process.env.CONTINUOUS_INTEGRATION);\n}\n\n/**\n * Output data in the appropriate format\n */\nexport function output(data: unknown, options: GlobalOptions = {}, format?: OutputFormat): void {\n const outputFormat = options.json ? 'json' : (format ?? 'pretty');\n\n switch (outputFormat) {\n case 'json':\n console.log(JSON.stringify(data, null, 2));\n break;\n case 'minimal':\n if (typeof data === 'string') {\n console.log(data);\n } else {\n console.log(JSON.stringify(data));\n }\n break;\n default:\n if (typeof data === 'string') {\n console.log(data);\n } else {\n console.log(data);\n }\n break;\n }\n}\n\n/**\n * Log an info message\n */\nexport function log(message: string): void {\n console.log(message);\n}\n\n/**\n * Log a success message\n */\nexport function success(message: string): void {\n console.log(pc.green(message));\n}\n\n/**\n * Log a warning message\n */\nexport function warn(message: string): void {\n console.error(pc.yellow(`Warning: ${message}`));\n}\n\n/**\n * Log a verbose message (only shown with --verbose)\n */\nexport function verbose(message: string, options: GlobalOptions = {}): void {\n if (options.verbose) {\n console.log(pc.dim(message));\n }\n}\n\n/**\n * Log a debug message (only shown with --debug or DEBUG env)\n */\nexport function debug(message: string, options: GlobalOptions = {}): void {\n if (options.debug || process.env.DEBUG) {\n console.log(pc.dim(`[debug] ${message}`));\n }\n}\n\n/**\n * Format a table for display\n */\nexport function formatTable(\n headers: string[],\n rows: string[][],\n options: { head?: boolean } = {},\n): string {\n const table = new Table({\n head: options.head !== false ? headers.map((h) => pc.cyan(h)) : undefined,\n style: {\n head: [],\n border: [],\n },\n });\n\n for (const row of rows) {\n table.push(row);\n }\n return table.toString();\n}\n\n/**\n * Format a box with title and content\n */\nexport function formatBox(title: string, content: string): string {\n const line = '━'.repeat(70);\n const lines: string[] = [\n pc.cyan(line),\n pc.cyan(pc.bold(title)),\n pc.cyan(line),\n '',\n content,\n '',\n pc.cyan(line),\n ];\n return lines.join('\\n');\n}\n\n/**\n * Format a key-value list\n */\nexport function formatKeyValue(\n items: Record<string, string | number | boolean | undefined>,\n): string {\n const maxKeyLength = Math.max(...Object.keys(items).map((k) => k.length));\n\n return Object.entries(items)\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => {\n const paddedKey = key.padEnd(maxKeyLength);\n return `${pc.cyan(paddedKey)} ${value}`;\n })\n .join('\\n');\n}\n\n/**\n * Format a code block with optional language\n */\nexport function formatCode(code: string, language?: string): string {\n const header = language ? pc.dim(`// ${language}`) : '';\n const formattedCode = code\n .split('\\n')\n .map((line) => ` ${line}`)\n .join('\\n');\n\n return header ? `${header}\\n${formattedCode}` : formattedCode;\n}\n\n/**\n * Format a list of items with bullets\n */\nexport function formatList(items: string[], bullet = '•'): string {\n return items.map((item) => `${pc.cyan(bullet)} ${item}`).join('\\n');\n}\n\n/**\n * Format a section header\n */\nexport function formatHeader(title: string): string {\n return pc.cyan(pc.bold(`\\n${title}\\n`));\n}\n\n/**\n * Create a spinner-compatible logger that respects output options\n */\nexport function createLogger(options: GlobalOptions = {}) {\n return {\n log: (msg: string) => log(msg),\n success: (msg: string) => success(msg),\n warn: (msg: string) => warn(msg),\n verbose: (msg: string) => verbose(msg, options),\n debug: (msg: string) => debug(msg, options),\n };\n}\n\n/**\n * Truncate a string to a maximum length\n */\nexport function truncate(str: string, maxLength: number): string {\n if (str.length <= maxLength) return str;\n return str.slice(0, maxLength - 3) + '...';\n}\n\n/**\n * Mask sensitive values (show first 4 chars only)\n */\nexport function maskSecret(value: string): string {\n if (value.length <= 4) return '****';\n return value.slice(0, 4) + '*'.repeat(Math.min(value.length - 4, 20));\n}\n","import { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport Keyv from 'keyv';\nimport { KeyvFile } from 'keyv-file';\nimport { getCacheDir } from './config.js';\n\nlet cacheInstance: Keyv | null = null;\n\n/**\n * Default cache TTL (1 hour)\n */\nconst DEFAULT_TTL = 3600 * 1000;\n\n/**\n * Initialize and get the cache instance\n */\nexport async function getCache(): Promise<Keyv> {\n if (cacheInstance) {\n return cacheInstance;\n }\n\n const cacheDir = getCacheDir();\n await mkdir(cacheDir, { recursive: true });\n\n const cachePath = join(cacheDir, 'cache.json');\n\n cacheInstance = new Keyv({\n store: new KeyvFile({ filename: cachePath }),\n namespace: 'saas-cli',\n });\n\n // Handle errors silently - cache failures shouldn't break the CLI\n cacheInstance.on('error', () => {\n // Silently ignore cache errors\n });\n\n return cacheInstance;\n}\n\n/**\n * Get a cached value\n */\nexport async function cacheGet<T>(key: string): Promise<T | undefined> {\n try {\n const cache = await getCache();\n return (await cache.get(key)) as T | undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Set a cached value with optional TTL\n */\nexport async function cacheSet<T>(key: string, value: T, ttl: number = DEFAULT_TTL): Promise<void> {\n try {\n const cache = await getCache();\n await cache.set(key, value, ttl);\n } catch {\n // Silently ignore cache errors\n }\n}\n\n/**\n * Delete a cached value\n */\nexport async function cacheDelete(key: string): Promise<void> {\n try {\n const cache = await getCache();\n await cache.delete(key);\n } catch {\n // Silently ignore cache errors\n }\n}\n\n/**\n * Clear all cached values\n */\nexport async function cacheClear(): Promise<void> {\n try {\n const cache = await getCache();\n await cache.clear();\n } catch {\n // Silently ignore cache errors\n }\n}\n\n/**\n * Generate a cache key from multiple parts\n */\nexport function cacheKey(...parts: string[]): string {\n return parts.map((p) => p.toLowerCase().replace(/\\s+/g, '-')).join(':');\n}\n\n/**\n * Decorator to cache async function results\n */\nexport function withCache<T>(keyFn: (...args: unknown[]) => string, ttl: number = DEFAULT_TTL) {\n return (\n _target: unknown,\n _propertyKey: string,\n descriptor: PropertyDescriptor,\n ): PropertyDescriptor => {\n const originalMethod = descriptor.value as (...args: unknown[]) => Promise<T>;\n\n descriptor.value = async function (...args: unknown[]): Promise<T> {\n const key = keyFn(...args);\n\n // Try to get from cache\n const cached = await cacheGet<T>(key);\n if (cached !== undefined) {\n return cached;\n }\n\n // Call original method\n const result = await originalMethod.apply(this, args);\n\n // Cache the result\n await cacheSet(key, result, ttl);\n\n return result;\n };\n\n return descriptor;\n };\n}\n\n/**\n * Cached fetch helper - wraps a fetch function with caching\n */\nexport async function cachedFetch<T>(\n key: string,\n fetchFn: () => Promise<T>,\n ttl: number = DEFAULT_TTL,\n skipCache = false,\n): Promise<T> {\n if (!skipCache) {\n const cached = await cacheGet<T>(key);\n if (cached !== undefined) {\n return cached;\n }\n }\n\n const result = await fetchFn();\n await cacheSet(key, result, ttl);\n return result;\n}\n","import got, { type Got } from 'got';\n\n/**\n * Default request options\n */\nconst defaultOptions = {\n timeout: {\n request: 30000,\n },\n retry: {\n limit: 3,\n methods: ['GET'] as ('GET' | 'POST' | 'PUT' | 'DELETE')[],\n statusCodes: [408, 429, 500, 502, 503, 504],\n errorCodes: ['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED'],\n },\n headers: {\n 'User-Agent': 'saas-cli/1.0.0',\n },\n};\n\n/**\n * Create an HTTP client with default options\n */\nexport function createHttpClient(options: Record<string, unknown> = {}): Got {\n return got.extend({\n ...defaultOptions,\n ...options,\n });\n}\n\n/**\n * Create an authenticated HTTP client\n */\nexport function createAuthenticatedClient(\n baseUrl: string,\n apiKey: string,\n options: Record<string, unknown> = {},\n): Got {\n return got.extend({\n ...defaultOptions,\n ...options,\n prefixUrl: baseUrl,\n headers: {\n ...defaultOptions.headers,\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n });\n}\n\n/**\n * Handle rate limiting with exponential backoff\n */\nexport async function withRateLimitRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {\n let lastError: Error | undefined;\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error as Error;\n\n // Check if rate limited\n if (\n error &&\n typeof error === 'object' &&\n 'response' in error &&\n (error as { response?: { statusCode?: number } }).response?.statusCode === 429\n ) {\n // Get retry-after header or use exponential backoff\n const retryAfter = (error as { response?: { headers?: { 'retry-after'?: string } } })\n .response?.headers?.['retry-after'];\n const waitTime = retryAfter\n ? Number.parseInt(retryAfter, 10) * 1000\n : Math.pow(2, attempt) * 1000;\n\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n continue;\n }\n\n // Not a rate limit error, throw immediately\n throw error;\n }\n }\n\n throw lastError;\n}\n"],"mappings":"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","names":["mkdir","join","mkdir","join"]}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/utils/error.ts
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
|
|
4
|
+
// src/types/index.ts
|
|
5
|
+
var MODEL_ALIASES = {
|
|
6
|
+
sonar: "sonar",
|
|
7
|
+
pro: "sonar-pro",
|
|
8
|
+
reasoning: "sonar-reasoning-pro",
|
|
9
|
+
deep: "sonar-deep-research"
|
|
10
|
+
};
|
|
11
|
+
var LIBRARY_IDS = {
|
|
12
|
+
flutter: "/websites/flutter_cn",
|
|
13
|
+
dart: "/websites/dart_dev",
|
|
14
|
+
riverpod: "/rrousselgit/riverpod",
|
|
15
|
+
drift: "/simolus3/drift",
|
|
16
|
+
go_router: "/websites/flutter_cn",
|
|
17
|
+
gorouter: "/websites/flutter_cn",
|
|
18
|
+
supabase: "/supabase/supabase-js",
|
|
19
|
+
powersync: "/powersync-ja/powersync-js",
|
|
20
|
+
freezed: "/rrousselgit/freezed",
|
|
21
|
+
bloc: "/felangel/bloc",
|
|
22
|
+
dio: "/cfug/dio",
|
|
23
|
+
hive: "/isar/hive",
|
|
24
|
+
isar: "/isar/isar",
|
|
25
|
+
firebase: "/firebase/flutterfire"
|
|
26
|
+
};
|
|
27
|
+
var EXIT_CODES = {
|
|
28
|
+
SUCCESS: 0,
|
|
29
|
+
GENERAL_ERROR: 1,
|
|
30
|
+
USAGE_ERROR: 2,
|
|
31
|
+
CONFIG_ERROR: 3,
|
|
32
|
+
NETWORK_ERROR: 4,
|
|
33
|
+
AUTH_ERROR: 5
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/utils/error.ts
|
|
37
|
+
var CLIError = class extends Error {
|
|
38
|
+
constructor(message, exitCode = EXIT_CODES.GENERAL_ERROR, hint, seeAlso) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.exitCode = exitCode;
|
|
41
|
+
this.hint = hint;
|
|
42
|
+
this.seeAlso = seeAlso;
|
|
43
|
+
this.name = "CLIError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var APIError = class extends CLIError {
|
|
47
|
+
constructor(message, service, statusCode, hint) {
|
|
48
|
+
super(message, EXIT_CODES.NETWORK_ERROR, hint);
|
|
49
|
+
this.service = service;
|
|
50
|
+
this.statusCode = statusCode;
|
|
51
|
+
this.name = "APIError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var ConfigError = class extends CLIError {
|
|
55
|
+
constructor(message, hint) {
|
|
56
|
+
super(message, EXIT_CODES.CONFIG_ERROR, hint);
|
|
57
|
+
this.name = "ConfigError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var AuthError = class extends CLIError {
|
|
61
|
+
constructor(message, hint) {
|
|
62
|
+
super(message, EXIT_CODES.AUTH_ERROR, hint);
|
|
63
|
+
this.name = "AuthError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function formatError(error) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push(pc.red(`Error: ${error.message}`));
|
|
69
|
+
if (error.hint) {
|
|
70
|
+
lines.push(pc.yellow(`Hint: ${error.hint}`));
|
|
71
|
+
}
|
|
72
|
+
if (error.seeAlso) {
|
|
73
|
+
lines.push(pc.dim(`See: ${error.seeAlso}`));
|
|
74
|
+
}
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
function handleError(error) {
|
|
78
|
+
if (error instanceof CLIError) {
|
|
79
|
+
console.error(formatError(error));
|
|
80
|
+
if (process.env.DEBUG) {
|
|
81
|
+
console.error(pc.dim("\nStack trace:"));
|
|
82
|
+
console.error(pc.dim(error.stack ?? ""));
|
|
83
|
+
}
|
|
84
|
+
process.exit(error.exitCode);
|
|
85
|
+
}
|
|
86
|
+
if (error instanceof Error) {
|
|
87
|
+
console.error(pc.red(`Error: ${error.message}`));
|
|
88
|
+
if (process.env.DEBUG) {
|
|
89
|
+
console.error(pc.dim("\nStack trace:"));
|
|
90
|
+
console.error(pc.dim(error.stack ?? ""));
|
|
91
|
+
}
|
|
92
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
93
|
+
}
|
|
94
|
+
console.error(pc.red("An unexpected error occurred"));
|
|
95
|
+
console.error(pc.dim(String(error)));
|
|
96
|
+
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
MODEL_ALIASES,
|
|
101
|
+
LIBRARY_IDS,
|
|
102
|
+
CLIError,
|
|
103
|
+
APIError,
|
|
104
|
+
ConfigError,
|
|
105
|
+
AuthError,
|
|
106
|
+
handleError
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=chunk-5BCEXHNM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/error.ts","../src/types/index.ts"],"sourcesContent":["import pc from 'picocolors';\nimport { EXIT_CODES, type ExitCode } from '../types/index.js';\n\n/**\n * Custom CLI error with exit code and helpful hints\n */\nexport class CLIError extends Error {\n constructor(\n message: string,\n public exitCode: ExitCode = EXIT_CODES.GENERAL_ERROR,\n public hint?: string,\n public seeAlso?: string,\n ) {\n super(message);\n this.name = 'CLIError';\n }\n}\n\n/**\n * API-specific error with service context\n */\nexport class APIError extends CLIError {\n constructor(\n message: string,\n public service: string,\n public statusCode?: number,\n hint?: string,\n ) {\n super(message, EXIT_CODES.NETWORK_ERROR, hint);\n this.name = 'APIError';\n }\n}\n\n/**\n * Configuration error\n */\nexport class ConfigError extends CLIError {\n constructor(message: string, hint?: string) {\n super(message, EXIT_CODES.CONFIG_ERROR, hint);\n this.name = 'ConfigError';\n }\n}\n\n/**\n * Authentication error\n */\nexport class AuthError extends CLIError {\n constructor(message: string, hint?: string) {\n super(message, EXIT_CODES.AUTH_ERROR, hint);\n this.name = 'AuthError';\n }\n}\n\n/**\n * Format error message for display\n */\nfunction formatError(error: CLIError): string {\n const lines: string[] = [];\n\n // Main error message\n lines.push(pc.red(`Error: ${error.message}`));\n\n // Add hint if available\n if (error.hint) {\n lines.push(pc.yellow(`Hint: ${error.hint}`));\n }\n\n // Add see also if available\n if (error.seeAlso) {\n lines.push(pc.dim(`See: ${error.seeAlso}`));\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Handle errors and exit appropriately\n */\nexport function handleError(error: unknown): never {\n // Handle CLIError instances\n if (error instanceof CLIError) {\n console.error(formatError(error));\n\n // Show stack trace in debug mode\n if (process.env.DEBUG) {\n console.error(pc.dim('\\nStack trace:'));\n console.error(pc.dim(error.stack ?? ''));\n }\n\n process.exit(error.exitCode);\n }\n\n // Handle standard errors\n if (error instanceof Error) {\n console.error(pc.red(`Error: ${error.message}`));\n\n if (process.env.DEBUG) {\n console.error(pc.dim('\\nStack trace:'));\n console.error(pc.dim(error.stack ?? ''));\n }\n\n process.exit(EXIT_CODES.GENERAL_ERROR);\n }\n\n // Handle unknown errors\n console.error(pc.red('An unexpected error occurred'));\n console.error(pc.dim(String(error)));\n process.exit(EXIT_CODES.GENERAL_ERROR);\n}\n\n/**\n * Create an API error handler for a specific service\n */\nexport function createAPIErrorHandler(service: string) {\n return (error: unknown): never => {\n if (error instanceof APIError) {\n handleError(error);\n }\n\n // Handle got/HTTP errors\n if (error && typeof error === 'object' && 'response' in error) {\n const response = (error as { response?: { statusCode?: number; body?: unknown } }).response;\n const statusCode = response?.statusCode;\n\n let hint: string | undefined;\n\n switch (statusCode) {\n case 401:\n hint = `Check your ${service.toUpperCase()}_API_KEY environment variable`;\n break;\n case 403:\n hint = `Your API key may not have permission for this operation`;\n break;\n case 429:\n hint = `Rate limited. Wait a moment and try again`;\n break;\n case 404:\n hint = `Resource not found. Check if the library or endpoint exists`;\n break;\n case 500:\n case 502:\n case 503:\n hint = `${service} service is temporarily unavailable. Try again later`;\n break;\n }\n\n const apiError = new APIError(\n `${service} API request failed (${statusCode})`,\n service,\n statusCode,\n hint,\n );\n\n handleError(apiError);\n }\n\n // Re-throw as generic API error\n const message = error instanceof Error ? error.message : String(error);\n handleError(new APIError(message, service));\n };\n}\n\n/**\n * Wrap an async function with error handling\n */\nexport function withErrorHandler<T extends (...args: unknown[]) => Promise<unknown>>(\n fn: T,\n): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {\n return async (...args: Parameters<T>): Promise<Awaited<ReturnType<T>>> => {\n try {\n return (await fn(...args)) as Awaited<ReturnType<T>>;\n } catch (error) {\n handleError(error);\n }\n };\n}\n","/**\n * Global CLI configuration stored in ~/.config/saas-cli/config.yaml\n */\nexport interface GlobalConfig {\n context7?: {\n enabled?: boolean;\n apiKey?: string;\n };\n perplexity?: {\n apiKey?: string;\n defaultModel?: PerplexityModel;\n };\n supabase?: {\n projectRef?: string;\n url?: string;\n serviceKey?: string;\n };\n redis?: {\n url?: string;\n };\n cloudflare?: {\n accountId?: string;\n apiToken?: string;\n };\n onesignal?: {\n appId?: string;\n apiKey?: string;\n };\n posthog?: {\n projectId?: string;\n apiKey?: string;\n };\n defaults?: {\n outputFormat?: OutputFormat;\n cacheTtl?: number;\n };\n}\n\n/**\n * Project-level configuration stored in ./saas.yaml\n */\nexport interface ProjectConfig {\n project?: {\n name?: string;\n type?: 'flutter' | 'dart' | 'other';\n };\n flutter?: {\n path?: string;\n };\n supabase?: {\n path?: string;\n typesOutput?: string;\n };\n templates?: Record<string, { path: string }>;\n}\n\n/**\n * Output format options\n */\nexport type OutputFormat = 'pretty' | 'json' | 'minimal';\n\n/**\n * Global CLI options available on all commands\n */\nexport interface GlobalOptions {\n json?: boolean;\n verbose?: boolean;\n debug?: boolean;\n}\n\n/**\n * Perplexity AI model options\n */\nexport type PerplexityModel = 'sonar' | 'sonar-pro' | 'sonar-reasoning-pro' | 'sonar-deep-research';\n\n/**\n * Model aliases for user-friendly names\n */\nexport const MODEL_ALIASES: Record<string, PerplexityModel> = {\n sonar: 'sonar',\n pro: 'sonar-pro',\n reasoning: 'sonar-reasoning-pro',\n deep: 'sonar-deep-research',\n};\n\n/**\n * Context7 library ID mapping for common Flutter/Dart packages\n */\nexport const LIBRARY_IDS: Record<string, string> = {\n flutter: '/websites/flutter_cn',\n dart: '/websites/dart_dev',\n riverpod: '/rrousselgit/riverpod',\n drift: '/simolus3/drift',\n go_router: '/websites/flutter_cn',\n gorouter: '/websites/flutter_cn',\n supabase: '/supabase/supabase-js',\n powersync: '/powersync-ja/powersync-js',\n freezed: '/rrousselgit/freezed',\n bloc: '/felangel/bloc',\n dio: '/cfug/dio',\n hive: '/isar/hive',\n isar: '/isar/isar',\n firebase: '/firebase/flutterfire',\n};\n\n/**\n * Context7 API response types\n */\nexport interface Context7SearchResult {\n libraryId: string;\n name?: string;\n description?: string;\n}\n\nexport interface Context7DocsResult {\n content: string;\n source?: string;\n title?: string;\n}\n\n/**\n * Perplexity API response types\n */\nexport interface PerplexityResponse {\n choices: Array<{\n message: {\n content: string;\n role: string;\n };\n }>;\n citations?: string[];\n usage: {\n prompt_tokens: number;\n completion_tokens: number;\n };\n}\n\nexport interface PerplexityAskOptions {\n model?: PerplexityModel;\n maxTokens?: number;\n temperature?: number;\n searchRecencyFilter?: 'day' | 'week' | 'month' | 'year';\n searchDomainFilter?: string[];\n returnSources?: boolean;\n}\n\n/**\n * Column specification for code generation\n */\nexport interface ColumnSpec {\n name: string;\n type: string;\n isPrimaryKey?: boolean;\n isForeignKey?: boolean;\n foreignKeyTable?: string;\n foreignKeyColumn?: string;\n isNullable?: boolean;\n defaultValue?: string;\n isAutoIncrement?: boolean;\n}\n\n/**\n * RLS policy types for Supabase\n */\nexport type RLSPolicyType = 'user-owned' | 'team-owned' | 'public-read' | 'admin-only';\n\n/**\n * Command exit codes\n */\nexport const EXIT_CODES = {\n SUCCESS: 0,\n GENERAL_ERROR: 1,\n USAGE_ERROR: 2,\n CONFIG_ERROR: 3,\n NETWORK_ERROR: 4,\n AUTH_ERROR: 5,\n} as const;\n\nexport type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];\n"],"mappings":";AAAA,OAAO,QAAQ;;;AC8ER,IAAM,gBAAiD;AAAA,EAC5D,OAAO;AAAA,EACP,KAAK;AAAA,EACL,WAAW;AAAA,EACX,MAAM;AACR;AAKO,IAAM,cAAsC;AAAA,EACjD,SAAS;AAAA,EACT,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AAAA,EACX,SAAS;AAAA,EACT,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AACZ;AAkEO,IAAM,aAAa;AAAA,EACxB,SAAS;AAAA,EACT,eAAe;AAAA,EACf,aAAa;AAAA,EACb,cAAc;AAAA,EACd,eAAe;AAAA,EACf,YAAY;AACd;;;AD1KO,IAAM,WAAN,cAAuB,MAAM;AAAA,EAClC,YACE,SACO,WAAqB,WAAW,eAChC,MACA,SACP;AACA,UAAM,OAAO;AAJN;AACA;AACA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,WAAN,cAAuB,SAAS;AAAA,EACrC,YACE,SACO,SACA,YACP,MACA;AACA,UAAM,SAAS,WAAW,eAAe,IAAI;AAJtC;AACA;AAIP,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,cAAN,cAA0B,SAAS;AAAA,EACxC,YAAY,SAAiB,MAAe;AAC1C,UAAM,SAAS,WAAW,cAAc,IAAI;AAC5C,SAAK,OAAO;AAAA,EACd;AACF;AAKO,IAAM,YAAN,cAAwB,SAAS;AAAA,EACtC,YAAY,SAAiB,MAAe;AAC1C,UAAM,SAAS,WAAW,YAAY,IAAI;AAC1C,SAAK,OAAO;AAAA,EACd;AACF;AAKA,SAAS,YAAY,OAAyB;AAC5C,QAAM,QAAkB,CAAC;AAGzB,QAAM,KAAK,GAAG,IAAI,UAAU,MAAM,OAAO,EAAE,CAAC;AAG5C,MAAI,MAAM,MAAM;AACd,UAAM,KAAK,GAAG,OAAO,SAAS,MAAM,IAAI,EAAE,CAAC;AAAA,EAC7C;AAGA,MAAI,MAAM,SAAS;AACjB,UAAM,KAAK,GAAG,IAAI,QAAQ,MAAM,OAAO,EAAE,CAAC;AAAA,EAC5C;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAKO,SAAS,YAAY,OAAuB;AAEjD,MAAI,iBAAiB,UAAU;AAC7B,YAAQ,MAAM,YAAY,KAAK,CAAC;AAGhC,QAAI,QAAQ,IAAI,OAAO;AACrB,cAAQ,MAAM,GAAG,IAAI,gBAAgB,CAAC;AACtC,cAAQ,MAAM,GAAG,IAAI,MAAM,SAAS,EAAE,CAAC;AAAA,IACzC;AAEA,YAAQ,KAAK,MAAM,QAAQ;AAAA,EAC7B;AAGA,MAAI,iBAAiB,OAAO;AAC1B,YAAQ,MAAM,GAAG,IAAI,UAAU,MAAM,OAAO,EAAE,CAAC;AAE/C,QAAI,QAAQ,IAAI,OAAO;AACrB,cAAQ,MAAM,GAAG,IAAI,gBAAgB,CAAC;AACtC,cAAQ,MAAM,GAAG,IAAI,MAAM,SAAS,EAAE,CAAC;AAAA,IACzC;AAEA,YAAQ,KAAK,WAAW,aAAa;AAAA,EACvC;AAGA,UAAQ,MAAM,GAAG,IAAI,8BAA8B,CAAC;AACpD,UAAQ,MAAM,GAAG,IAAI,OAAO,KAAK,CAAC,CAAC;AACnC,UAAQ,KAAK,WAAW,aAAa;AACvC;","names":[]}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cacheKey,
|
|
3
|
+
cachedFetch,
|
|
4
|
+
createAuthenticatedClient
|
|
5
|
+
} from "./chunk-3KD5CFV3.js";
|
|
6
|
+
import {
|
|
7
|
+
APIError,
|
|
8
|
+
LIBRARY_IDS
|
|
9
|
+
} from "./chunk-5BCEXHNM.js";
|
|
10
|
+
|
|
11
|
+
// src/services/context7.ts
|
|
12
|
+
var CONTEXT7_BASE_URL = "https://context7.com/api/v2";
|
|
13
|
+
var Context7Client = class {
|
|
14
|
+
client;
|
|
15
|
+
constructor(apiKey) {
|
|
16
|
+
this.client = createAuthenticatedClient(CONTEXT7_BASE_URL, apiKey);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a library name to its Context7 library ID
|
|
20
|
+
*/
|
|
21
|
+
async resolveLibrary(libraryName, query) {
|
|
22
|
+
const normalizedName = libraryName.toLowerCase().replace(/-/g, "_");
|
|
23
|
+
if (LIBRARY_IDS[normalizedName]) {
|
|
24
|
+
return LIBRARY_IDS[normalizedName];
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.client.get("libs/search", {
|
|
28
|
+
searchParams: {
|
|
29
|
+
libraryName,
|
|
30
|
+
query
|
|
31
|
+
}
|
|
32
|
+
}).json();
|
|
33
|
+
if (response.results && response.results.length > 0 && response.results[0]) {
|
|
34
|
+
return response.results[0].libraryId;
|
|
35
|
+
}
|
|
36
|
+
return `/${libraryName}/${libraryName}`;
|
|
37
|
+
} catch (_error) {
|
|
38
|
+
return `/${libraryName}/${libraryName}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Query documentation for a specific library
|
|
43
|
+
*/
|
|
44
|
+
async queryDocs(libraryId, query) {
|
|
45
|
+
try {
|
|
46
|
+
const response = await this.client.get("context", {
|
|
47
|
+
searchParams: {
|
|
48
|
+
libraryId,
|
|
49
|
+
query,
|
|
50
|
+
tokens: 8e3
|
|
51
|
+
}
|
|
52
|
+
}).text();
|
|
53
|
+
if (!response || response.trim() === "") {
|
|
54
|
+
return "No documentation found for this query.";
|
|
55
|
+
}
|
|
56
|
+
return response;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error && typeof error === "object" && "response" in error) {
|
|
59
|
+
const response = error.response;
|
|
60
|
+
const statusCode = response?.statusCode;
|
|
61
|
+
if (statusCode === 202) {
|
|
62
|
+
throw new APIError(
|
|
63
|
+
"Library is being indexed",
|
|
64
|
+
"Context7",
|
|
65
|
+
202,
|
|
66
|
+
"The library is being indexed. Please try again in a few minutes."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (statusCode === 404) {
|
|
70
|
+
throw new APIError(
|
|
71
|
+
"Library not found",
|
|
72
|
+
"Context7",
|
|
73
|
+
404,
|
|
74
|
+
`Could not find library: ${libraryId}. Try a different package name.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Search for documentation (combines resolve + query)
|
|
83
|
+
*/
|
|
84
|
+
async search(library, query) {
|
|
85
|
+
const key = cacheKey("context7", library, query);
|
|
86
|
+
return cachedFetch(
|
|
87
|
+
key,
|
|
88
|
+
async () => {
|
|
89
|
+
const libraryId = await this.resolveLibrary(library, query);
|
|
90
|
+
return this.queryDocs(libraryId, query);
|
|
91
|
+
},
|
|
92
|
+
3600 * 1e3
|
|
93
|
+
// 1 hour cache
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var clientInstance = null;
|
|
98
|
+
var currentApiKey = null;
|
|
99
|
+
function getContext7Client(apiKey) {
|
|
100
|
+
if (!clientInstance || currentApiKey !== apiKey) {
|
|
101
|
+
clientInstance = new Context7Client(apiKey);
|
|
102
|
+
currentApiKey = apiKey;
|
|
103
|
+
}
|
|
104
|
+
return clientInstance;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
getContext7Client
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=chunk-N4OIAZSA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/services/context7.ts"],"sourcesContent":["import type { Got } from 'got';\nimport { LIBRARY_IDS } from '../types/index.js';\nimport { cachedFetch, cacheKey } from '../utils/cache.js';\nimport { APIError } from '../utils/error.js';\nimport { createAuthenticatedClient } from './http.js';\n\nconst CONTEXT7_BASE_URL = 'https://context7.com/api/v2';\n\n/**\n * Context7 API client for documentation lookup\n */\nexport class Context7Client {\n private client: Got;\n\n constructor(apiKey: string) {\n this.client = createAuthenticatedClient(CONTEXT7_BASE_URL, apiKey);\n }\n\n /**\n * Resolve a library name to its Context7 library ID\n */\n async resolveLibrary(libraryName: string, query: string): Promise<string> {\n // Check if we have a known mapping\n const normalizedName = libraryName.toLowerCase().replace(/-/g, '_');\n if (LIBRARY_IDS[normalizedName]) {\n return LIBRARY_IDS[normalizedName] as string;\n }\n\n // Search for the library\n try {\n const response = await this.client\n .get('libs/search', {\n searchParams: {\n libraryName,\n query,\n },\n })\n .json<{ results?: Array<{ libraryId: string; name: string }> }>();\n\n if (response.results && response.results.length > 0 && response.results[0]) {\n return response.results[0].libraryId;\n }\n\n // Fallback: try to construct a likely ID\n return `/${libraryName}/${libraryName}`;\n } catch (_error) {\n // If search fails, try the fallback pattern\n return `/${libraryName}/${libraryName}`;\n }\n }\n\n /**\n * Query documentation for a specific library\n */\n async queryDocs(libraryId: string, query: string): Promise<string> {\n try {\n const response = await this.client\n .get('context', {\n searchParams: {\n libraryId,\n query,\n tokens: 8000,\n },\n })\n .text();\n\n // API returns markdown text directly\n if (!response || response.trim() === '') {\n return 'No documentation found for this query.';\n }\n\n return response;\n } catch (error) {\n if (error && typeof error === 'object' && 'response' in error) {\n const response = (error as { response?: { statusCode?: number } }).response;\n const statusCode = response?.statusCode;\n\n if (statusCode === 202) {\n throw new APIError(\n 'Library is being indexed',\n 'Context7',\n 202,\n 'The library is being indexed. Please try again in a few minutes.',\n );\n }\n\n if (statusCode === 404) {\n throw new APIError(\n 'Library not found',\n 'Context7',\n 404,\n `Could not find library: ${libraryId}. Try a different package name.`,\n );\n }\n }\n\n throw error;\n }\n }\n\n /**\n * Search for documentation (combines resolve + query)\n */\n async search(library: string, query: string): Promise<string> {\n const key = cacheKey('context7', library, query);\n\n return cachedFetch(\n key,\n async () => {\n const libraryId = await this.resolveLibrary(library, query);\n return this.queryDocs(libraryId, query);\n },\n 3600 * 1000, // 1 hour cache\n );\n }\n}\n\n// Singleton instance cache\nlet clientInstance: Context7Client | null = null;\nlet currentApiKey: string | null = null;\n\n/**\n * Get or create a Context7 client instance\n */\nexport function getContext7Client(apiKey: string): Context7Client {\n if (!clientInstance || currentApiKey !== apiKey) {\n clientInstance = new Context7Client(apiKey);\n currentApiKey = apiKey;\n }\n return clientInstance;\n}\n"],"mappings":";;;;;;;;;;;AAMA,IAAM,oBAAoB;AAKnB,IAAM,iBAAN,MAAqB;AAAA,EAClB;AAAA,EAER,YAAY,QAAgB;AAC1B,SAAK,SAAS,0BAA0B,mBAAmB,MAAM;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe,aAAqB,OAAgC;AAExE,UAAM,iBAAiB,YAAY,YAAY,EAAE,QAAQ,MAAM,GAAG;AAClE,QAAI,YAAY,cAAc,GAAG;AAC/B,aAAO,YAAY,cAAc;AAAA,IACnC;AAGA,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OACzB,IAAI,eAAe;AAAA,QAClB,cAAc;AAAA,UACZ;AAAA,UACA;AAAA,QACF;AAAA,MACF,CAAC,EACA,KAA+D;AAElE,UAAI,SAAS,WAAW,SAAS,QAAQ,SAAS,KAAK,SAAS,QAAQ,CAAC,GAAG;AAC1E,eAAO,SAAS,QAAQ,CAAC,EAAE;AAAA,MAC7B;AAGA,aAAO,IAAI,WAAW,IAAI,WAAW;AAAA,IACvC,SAAS,QAAQ;AAEf,aAAO,IAAI,WAAW,IAAI,WAAW;AAAA,IACvC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,WAAmB,OAAgC;AACjE,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OACzB,IAAI,WAAW;AAAA,QACd,cAAc;AAAA,UACZ;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,QACV;AAAA,MACF,CAAC,EACA,KAAK;AAGR,UAAI,CAAC,YAAY,SAAS,KAAK,MAAM,IAAI;AACvC,eAAO;AAAA,MACT;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,SAAS,OAAO,UAAU,YAAY,cAAc,OAAO;AAC7D,cAAM,WAAY,MAAiD;AACnE,cAAM,aAAa,UAAU;AAE7B,YAAI,eAAe,KAAK;AACtB,gBAAM,IAAI;AAAA,YACR;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAEA,YAAI,eAAe,KAAK;AACtB,gBAAM,IAAI;AAAA,YACR;AAAA,YACA;AAAA,YACA;AAAA,YACA,2BAA2B,SAAS;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AAEA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAO,SAAiB,OAAgC;AAC5D,UAAM,MAAM,SAAS,YAAY,SAAS,KAAK;AAE/C,WAAO;AAAA,MACL;AAAA,MACA,YAAY;AACV,cAAM,YAAY,MAAM,KAAK,eAAe,SAAS,KAAK;AAC1D,eAAO,KAAK,UAAU,WAAW,KAAK;AAAA,MACxC;AAAA,MACA,OAAO;AAAA;AAAA,IACT;AAAA,EACF;AACF;AAGA,IAAI,iBAAwC;AAC5C,IAAI,gBAA+B;AAK5B,SAAS,kBAAkB,QAAgC;AAChE,MAAI,CAAC,kBAAkB,kBAAkB,QAAQ;AAC/C,qBAAiB,IAAI,eAAe,MAAM;AAC1C,oBAAgB;AAAA,EAClB;AACA,SAAO;AACT;","names":[]}
|