@letterapp/cli 0.1.0 → 0.2.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/README.md +174 -23
- package/dist/index.js +538 -54
- package/dist/index.js.map +1 -0
- package/package.json +14 -6
- package/dist/commands/login.js +0 -151
- package/dist/commands/registry.js +0 -21
- package/dist/lib/api.js +0 -29
- package/dist/lib/args.js +0 -49
- package/dist/lib/browser.js +0 -33
- package/dist/lib/config.js +0 -35
- package/dist/lib/env-file.js +0 -45
- package/dist/lib/pm.js +0 -72
- package/dist/lib/ui.js +0 -57
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/output.ts","../src/config.ts","../src/client.ts","../src/browser.ts","../src/env-file.ts","../src/pm.ts","../src/commands/login.ts","../src/commands/auth.ts","../src/commands/status.ts","../src/commands/config.ts"],"sourcesContent":["declare const PKG_VERSION: string;\nimport { Command } from \"commander\";\nimport { setJsonMode } from \"./output.js\";\nimport { registerLoginCommand } from \"./commands/login.js\";\nimport { registerAuthCommands } from \"./commands/auth.js\";\nimport { registerStatusCommand } from \"./commands/status.js\";\nimport { registerConfigCommands } from \"./commands/config.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"letter\")\n .description(\"Connect your app to Letter, then manage it from the command line\")\n .version(PKG_VERSION)\n .option(\"--json\", \"Output raw JSON (for scripting / agents)\")\n .hook(\"preAction\", (thisCommand) => {\n if (thisCommand.opts().json) setJsonMode(true);\n });\n\n// v1 ships the secure setup/login flow plus connection helpers. The command\n// structure is the extension point: future authenticated areas (sequences,\n// broadcast, contacts, events, keys) register the same way and reuse the\n// credential store in config.ts + the client in client.ts. See README roadmap.\nregisterLoginCommand(program); // default command (`letter` == `letter login`)\nregisterAuthCommands(program);\nregisterStatusCommand(program);\nregisterConfigCommands(program);\n\nprogram.parseAsync();\n","import chalk from \"chalk\";\nimport ora, { type Ora } from \"ora\";\nimport { createInterface } from \"node:readline\";\n\nlet jsonMode = false;\n\nexport function setJsonMode(enabled: boolean) {\n jsonMode = enabled;\n}\nexport function isJsonMode() {\n return jsonMode;\n}\n\nexport function printJson(data: unknown) {\n console.log(JSON.stringify(data, null, 2));\n}\n\nexport function log(msg = \"\") {\n if (jsonMode) return;\n console.log(msg);\n}\n\nexport function printSuccess(msg: string) {\n if (jsonMode) return;\n console.log(chalk.green(\"✓\") + \" \" + msg);\n}\n\nexport function printInfo(msg: string) {\n if (jsonMode) return;\n console.log(chalk.cyan(\"›\") + \" \" + msg);\n}\n\nexport function printWarning(msg: string) {\n if (jsonMode) return;\n console.log(chalk.yellow(\"!\") + \" \" + msg);\n}\n\nexport function printError(err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n if (jsonMode) {\n printJson({ error: msg });\n return;\n }\n console.error(chalk.red(\"✗\") + \" \" + msg);\n}\n\n/** The Letter wordmark banner. Suppressed in JSON mode. */\nexport function banner() {\n if (jsonMode) return;\n console.log(\"\");\n console.log(\" \" + chalk.bold(\"Letter\") + chalk.red(\".\") + \" \" + chalk.dim(\"CLI\"));\n console.log(\"\");\n}\n\nexport function spinner(text: string): Ora {\n return ora({ text, color: \"cyan\", isEnabled: !jsonMode });\n}\n\n/**\n * Prompts the user and resolves with their input. Returns \"\" immediately on a\n * non-interactive stdin so automated/agent runs don't hang.\n */\nexport function prompt(question: string): Promise<string> {\n if (!process.stdin.isTTY) return Promise.resolve(\"\");\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\nexport const c = chalk;\n","import Conf from \"conf\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { mkdir, readFile, writeFile, rm } from \"node:fs/promises\";\n\n/** The SDK / API default. When the resolved base equals this we don't bother\n * writing LETTER_BASE_URL into the project env. */\nexport const DEFAULT_BASE_URL = \"https://api.letter.app\";\n\n/* -------------------------------------------------------------------------- */\n/* Non-secret CLI preferences (base URL). */\n/* Stored with `conf`, mirroring how the SDK/MCP resolve their endpoint. */\n/* -------------------------------------------------------------------------- */\n\nconst config = new Conf({\n projectName: \"letterapp-cli\",\n schema: {\n baseUrl: { type: \"string\", default: DEFAULT_BASE_URL },\n },\n});\n\n/** Resolve the API base: explicit flag > env > stored config > prod default. */\nexport function getBaseUrl(flag?: string): string {\n const raw =\n flag ||\n process.env.LETTER_BASE_URL ||\n (config.get(\"baseUrl\") as string) ||\n DEFAULT_BASE_URL;\n return raw.replace(/\\/$/, \"\");\n}\n\nexport function setBaseUrl(url: string): void {\n config.set(\"baseUrl\", url.replace(/\\/$/, \"\"));\n}\n\nexport function getConfigPath(): string {\n return config.path;\n}\n\nexport function resetConfig(): void {\n config.clear();\n}\n\n/* -------------------------------------------------------------------------- */\n/* Secret credential store (~/.letter/credentials.json). */\n/* Written here, read by @letterapp/mcp so the secret never lands in an MCP */\n/* config. Kept separate from `conf` and locked to owner-only permissions. */\n/* -------------------------------------------------------------------------- */\n\nexport type StoredCredential = {\n apiKey: string;\n baseUrl: string;\n project: { slug: string; name: string };\n savedAt: string;\n};\n\nexport function credentialsPath(): string {\n return path.join(homedir(), \".letter\", \"credentials.json\");\n}\n\nexport async function saveCredential(cred: StoredCredential): Promise<string> {\n const file = credentialsPath();\n await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });\n await writeFile(file, `${JSON.stringify(cred, null, 2)}\\n`, { mode: 0o600 });\n return file;\n}\n\nexport async function readCredential(): Promise<StoredCredential | null> {\n try {\n const raw = await readFile(credentialsPath(), \"utf8\");\n return JSON.parse(raw) as StoredCredential;\n } catch {\n return null;\n }\n}\n\nexport async function clearCredential(): Promise<void> {\n await rm(credentialsPath(), { force: true });\n}\n","import { getBaseUrl, readCredential } from \"./config.js\";\n\nconst USER_AGENT = \"@letterapp/cli\";\n\n/* -------------------------------------------------------------------------- */\n/* Device authorization (RFC 8628 style). No secret ever passes through argv. */\n/* -------------------------------------------------------------------------- */\n\nexport type StartResponse = {\n device_code: string;\n user_code: string;\n verification_uri: string;\n verification_uri_complete: string;\n interval: number;\n expires_in: number;\n};\n\nexport type PollResponse =\n | { status: \"authorization_pending\" }\n | { status: \"slow_down\"; retryAfter?: number }\n | { status: \"access_denied\" }\n | { status: \"expired_token\" }\n | {\n status: \"approved\";\n api_key: string;\n base_url: string;\n project: { slug: string; name: string };\n };\n\n/** Starts a device-authorization flow against the given API base. */\nexport async function startDeviceAuth(base: string): Promise<StartResponse> {\n const res = await fetch(`${base}/v1/cli/auth/start`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"user-agent\": USER_AGENT },\n body: \"{}\",\n });\n if (!res.ok) {\n throw new Error(\n `Could not start login (HTTP ${res.status}). Is ${base} reachable?`,\n );\n }\n return (await res.json()) as StartResponse;\n}\n\n/** Polls once for approval. */\nexport async function pollDeviceAuth(\n base: string,\n deviceCode: string,\n): Promise<PollResponse> {\n const res = await fetch(`${base}/v1/cli/auth/poll`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"user-agent\": USER_AGENT },\n body: JSON.stringify({ device_code: deviceCode }),\n });\n\n if (res.status === 429) {\n const retryAfter = Number(res.headers.get(\"retry-after\") ?? \"5\");\n return { status: \"slow_down\", retryAfter };\n }\n if (!res.ok) {\n throw new Error(`Login poll failed (HTTP ${res.status}).`);\n }\n return (await res.json()) as PollResponse;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Authenticated client. Resolves the key from the stored credential (written */\n/* by `letter login`) or LETTER_API_KEY. Used by data commands like `status`. */\n/* -------------------------------------------------------------------------- */\n\nexport interface ApiResponse<T = unknown> {\n status: number;\n data: T;\n}\n\nclass LetterClient {\n private maxRetries = 3;\n\n private async resolveAuth(): Promise<{ base: string; token: string }> {\n const cred = await readCredential();\n const token = process.env.LETTER_API_KEY || cred?.apiKey || \"\";\n if (!token) {\n throw new Error(\n \"Not connected. Run `letter login` (or set LETTER_API_KEY) first.\",\n );\n }\n const base = getBaseUrl(process.env.LETTER_API_KEY ? undefined : cred?.baseUrl);\n return { base, token };\n }\n\n private async request<T = unknown>(\n method: string,\n path: string,\n attempt = 1,\n ): Promise<ApiResponse<T>> {\n const { base, token } = await this.resolveAuth();\n const res = await fetch(`${base}${path}`, {\n method,\n headers: {\n authorization: `Bearer ${token}`,\n accept: \"application/json\",\n \"user-agent\": USER_AGENT,\n },\n });\n\n if (res.status === 429 && attempt <= this.maxRetries) {\n const retryAfter = Number(res.headers.get(\"retry-after\") || 2);\n await new Promise((r) => setTimeout(r, retryAfter * 1000 * attempt));\n return this.request<T>(method, path, attempt + 1);\n }\n\n const data = (res.status === 204 ? {} : await res.json()) as T;\n if (!res.ok) {\n const msg =\n (data as { error?: { message?: string } })?.error?.message ||\n `HTTP ${res.status}`;\n const err = new Error(msg) as Error & { status: number };\n err.status = res.status;\n throw err;\n }\n return { status: res.status, data };\n }\n\n get<T = unknown>(path: string) {\n return this.request<T>(\"GET\", path);\n }\n}\n\nexport const client = new LetterClient();\n","import { spawn } from \"node:child_process\";\n\n/**\n * Opens `url` in the default browser. Best-effort and non-blocking: if it fails\n * (headless box, no DISPLAY) the caller has already printed the URL so the user\n * can open it manually. Returns true if a launcher was spawned.\n */\nexport function openUrl(url: string): boolean {\n const platform = process.platform;\n let command: string;\n let args: string[];\n\n if (platform === \"darwin\") {\n command = \"open\";\n args = [url];\n } else if (platform === \"win32\") {\n command = \"cmd\";\n // `start` needs an empty title arg; the comma-free form avoids quoting woes.\n args = [\"/c\", \"start\", \"\", url];\n } else {\n command = \"xdg-open\";\n args = [url];\n }\n\n try {\n const child = spawn(command, args, { stdio: \"ignore\", detached: true });\n child.on(\"error\", () => {});\n child.unref();\n return true;\n } catch {\n return false;\n }\n}\n","import path from \"node:path\";\nimport { readFile, writeFile, stat } from \"node:fs/promises\";\n\n/**\n * Upserts `key=value` in an env file, creating it if needed. Existing keys are\n * replaced in place; new keys are appended. Returns the file path. The value is\n * never logged by this module - callers print only the key name.\n */\nexport async function upsertEnv(\n cwd: string,\n file: string,\n entries: Record<string, string>,\n): Promise<string> {\n const filePath = path.join(cwd, file);\n let contents = \"\";\n try {\n contents = await readFile(filePath, \"utf8\");\n } catch {\n contents = \"\";\n }\n\n let next = contents;\n for (const [key, value] of Object.entries(entries)) {\n const line = `${key}=${value}`;\n const re = new RegExp(`^${escapeRegExp(key)}=.*$`, \"m\");\n if (re.test(next)) {\n next = next.replace(re, line);\n } else {\n if (next.length && !next.endsWith(\"\\n\")) next += \"\\n\";\n next += `${line}\\n`;\n }\n }\n\n await writeFile(filePath, next, \"utf8\");\n return filePath;\n}\n\n/** True if a file exists at `cwd/name`. */\nexport async function fileExists(cwd: string, name: string): Promise<boolean> {\n try {\n await stat(path.join(cwd, name));\n return true;\n } catch {\n return false;\n }\n}\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","import { spawn } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport { fileExists } from \"./env-file.js\";\n\nexport type PackageManager = \"pnpm\" | \"yarn\" | \"bun\" | \"npm\";\n\n/**\n * Detects the package manager from lockfiles (then the `npm_config_user_agent`\n * of the running process), defaulting to npm.\n */\nexport async function detectPackageManager(\n cwd: string,\n): Promise<PackageManager> {\n if (await fileExists(cwd, \"pnpm-lock.yaml\")) return \"pnpm\";\n if (await fileExists(cwd, \"yarn.lock\")) return \"yarn\";\n if (await fileExists(cwd, \"bun.lockb\")) return \"bun\";\n if (await fileExists(cwd, \"package-lock.json\")) return \"npm\";\n\n const ua = process.env.npm_config_user_agent ?? \"\";\n if (ua.startsWith(\"pnpm\")) return \"pnpm\";\n if (ua.startsWith(\"yarn\")) return \"yarn\";\n if (ua.startsWith(\"bun\")) return \"bun\";\n return \"npm\";\n}\n\n/** Detects a likely web framework for friendlier guidance. */\nexport async function detectFramework(cwd: string): Promise<string | null> {\n try {\n const pkg = JSON.parse(await readFile(`${cwd}/package.json`, \"utf8\")) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.dependencies, ...pkg.devDependencies };\n if (deps.next) return \"Next.js\";\n if (deps.nuxt) return \"Nuxt\";\n if (deps[\"@remix-run/node\"] || deps[\"@remix-run/react\"]) return \"Remix\";\n if (deps.express) return \"Express\";\n if (deps.fastify) return \"Fastify\";\n if (deps.hono) return \"Hono\";\n if (deps[\"@sveltejs/kit\"]) return \"SvelteKit\";\n return null;\n } catch {\n return null;\n }\n}\n\nexport function installCommand(pm: PackageManager, pkg: string): string {\n switch (pm) {\n case \"pnpm\":\n return `pnpm add ${pkg}`;\n case \"yarn\":\n return `yarn add ${pkg}`;\n case \"bun\":\n return `bun add ${pkg}`;\n default:\n return `npm install ${pkg}`;\n }\n}\n\n/** Runs the install command, streaming output. Resolves to the exit code. */\nexport function runInstall(\n pm: PackageManager,\n pkg: string,\n cwd: string,\n): Promise<number> {\n const args = pm === \"npm\" ? [\"install\", pkg] : [\"add\", pkg];\n return new Promise((resolve) => {\n const child = spawn(pm, args, {\n cwd,\n stdio: \"inherit\",\n shell: process.platform === \"win32\",\n });\n child.on(\"error\", () => resolve(1));\n child.on(\"close\", (code) => resolve(code ?? 1));\n });\n}\n","import { Command } from \"commander\";\nimport {\n getBaseUrl,\n DEFAULT_BASE_URL,\n saveCredential,\n} from \"../config.js\";\nimport { startDeviceAuth, pollDeviceAuth } from \"../client.js\";\nimport { openUrl } from \"../browser.js\";\nimport { upsertEnv } from \"../env-file.js\";\nimport {\n detectFramework,\n detectPackageManager,\n installCommand,\n runInstall,\n} from \"../pm.js\";\nimport {\n banner,\n c,\n log,\n printError,\n printInfo,\n printSuccess,\n printWarning,\n prompt,\n} from \"../output.js\";\n\nconst SDK_PACKAGE = \"@letterapp/node\";\nconst ENV_FILE = \".env.local\";\nconst sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));\n\ninterface LoginOptions {\n open: boolean;\n install: boolean;\n yes?: boolean;\n baseUrl?: string;\n apiKey?: string;\n}\n\nexport function registerLoginCommand(program: Command) {\n program\n .command(\"login\", { isDefault: true })\n .alias(\"init\")\n .description(\"Connect this project to Letter (interactive device login)\")\n .option(\"--no-open\", \"Don't open the browser; print the URL to open manually\")\n .option(\"-y, --yes\", \"Non-interactive: don't wait for Enter (agents/CI)\")\n .option(\"--no-install\", \"Skip installing the SDK\")\n .option(\"--base-url <url>\", \"Target a self-hosted or local Letter instance\")\n .option(\n \"--api-key <key>\",\n \"CI only: write a key without the device flow (never use in chat)\",\n )\n .action(async (opts: LoginOptions) => {\n const code = await runLogin(opts);\n if (code !== 0) process.exitCode = code;\n });\n}\n\nasync function runLogin(opts: LoginOptions): Promise<number> {\n const base = getBaseUrl(opts.baseUrl);\n const cwd = process.cwd();\n const interactive = process.stdin.isTTY && !opts.yes;\n\n banner();\n\n // CI escape hatch: write the provided key non-interactively. Documented as\n // automation-only; interactive/agent use should rely on the device flow so\n // no secret passes through the command line or chat.\n if (opts.apiKey) {\n const entries: Record<string, string> = { LETTER_API_KEY: opts.apiKey };\n if (base !== DEFAULT_BASE_URL) entries.LETTER_BASE_URL = base;\n const file = await upsertEnv(cwd, ENV_FILE, entries);\n printSuccess(`Saved LETTER_API_KEY to ${rel(cwd, file)} (--api-key).`);\n printWarning(\"--api-key is for CI. For interactive setup, run `letter login`.\");\n return 0;\n }\n\n // 1. Begin the flow.\n let flow;\n try {\n flow = await startDeviceAuth(base);\n } catch (err) {\n printError(err);\n return 1;\n }\n\n log(c.bold(\"Confirm this code in your browser:\"));\n log();\n log(\" \" + c.cyan(c.bold(flow.user_code)));\n log();\n\n // 2. Open the browser (interactive: wait for Enter; otherwise auto/print).\n if (interactive && opts.open) {\n await prompt(c.dim(\"Press Enter to open your browser… \"));\n }\n if (opts.open) openUrl(flow.verification_uri_complete);\n printInfo(\"If your browser didn't open, visit:\");\n log(\" \" + c.blue(flow.verification_uri_complete));\n log();\n printInfo(\"Waiting for you to approve… (Ctrl+C to cancel)\");\n\n // 3. Poll until the user approves/denies or the code expires.\n const deadline = Date.now() + flow.expires_in * 1000;\n let intervalMs = Math.max(1, flow.interval) * 1000;\n\n while (Date.now() < deadline) {\n await sleep(intervalMs);\n let res;\n try {\n res = await pollDeviceAuth(base, flow.device_code);\n } catch (err) {\n printError(err);\n return 1;\n }\n\n if (res.status === \"authorization_pending\") continue;\n if (res.status === \"slow_down\") {\n intervalMs += 1000;\n continue;\n }\n if (res.status === \"access_denied\") {\n printError(new Error(\"Request denied in the browser. Nothing was changed.\"));\n return 1;\n }\n if (res.status === \"expired_token\") {\n printError(new Error(\"This login expired. Run the command again to retry.\"));\n return 1;\n }\n\n return finish(res.api_key, res.base_url, res.project, cwd, opts.install);\n }\n\n printError(new Error(\"Timed out waiting for approval. Run the command again.\"));\n return 1;\n}\n\nasync function finish(\n apiKey: string,\n baseUrl: string,\n project: { slug: string; name: string },\n cwd: string,\n doInstall: boolean,\n): Promise<number> {\n log();\n printSuccess(`Approved for project ${c.bold(project.name)}.`);\n\n // Write the key to the project env (value never printed) + shared store.\n const entries: Record<string, string> = { LETTER_API_KEY: apiKey };\n if (baseUrl && baseUrl !== DEFAULT_BASE_URL) entries.LETTER_BASE_URL = baseUrl;\n const envFile = await upsertEnv(cwd, ENV_FILE, entries);\n printSuccess(`Saved LETTER_API_KEY to ${rel(cwd, envFile)}.`);\n\n try {\n const credFile = await saveCredential({\n apiKey,\n baseUrl: baseUrl || DEFAULT_BASE_URL,\n project,\n savedAt: new Date().toISOString(),\n });\n printSuccess(`Stored credentials in ${tildify(credFile)} for tooling (MCP).`);\n } catch {\n printWarning(\"Could not write ~/.letter/credentials.json (continuing).\");\n }\n\n // Install the SDK.\n const pm = await detectPackageManager(cwd);\n if (doInstall) {\n printInfo(`Installing ${SDK_PACKAGE} with ${pm}…`);\n const code = await runInstall(pm, SDK_PACKAGE, cwd);\n if (code === 0) printSuccess(`Installed ${SDK_PACKAGE}.`);\n else printWarning(`Install failed. Run: ${installCommand(pm, SDK_PACKAGE)}`);\n } else {\n printInfo(`Skipped install. Run: ${installCommand(pm, SDK_PACKAGE)}`);\n }\n\n printNextSteps(await detectFramework(cwd));\n return 0;\n}\n\nfunction printNextSteps(framework: string | null): void {\n log();\n log(c.bold(\"Next steps\"));\n if (framework) log(c.dim(`Detected ${framework}.`));\n log(` 1. Create a server-side client that reads ${c.cyan(\"process.env.LETTER_API_KEY\")}.`);\n log(` 2. Call ${c.cyan(\"letter.identify(...)\")} where users sign up or log in.`);\n log(` 3. Call ${c.cyan(\"letter.track(...)\")} on 2-3 key actions.`);\n log();\n log(c.dim(\"Check it landed: \") + c.bold(\"letter status\"));\n log(c.dim(\"Your API key is in .env.local - keep it out of source control.\"));\n log();\n}\n\nfunction rel(cwd: string, file: string): string {\n return file.startsWith(cwd) ? file.slice(cwd.length + 1) || file : file;\n}\n\nfunction tildify(file: string): string {\n const home = process.env.HOME || process.env.USERPROFILE;\n return home && file.startsWith(home) ? `~${file.slice(home.length)}` : file;\n}\n","import { Command } from \"commander\";\nimport { readCredential, clearCredential } from \"../config.js\";\nimport { c, isJsonMode, printJson, printInfo, printSuccess } from \"../output.js\";\n\nfunction mask(key: string): string {\n return key.length > 12 ? key.slice(0, 8) + \"…\" + key.slice(-4) : \"set\";\n}\n\nexport function registerAuthCommands(program: Command) {\n const auth = program.command(\"auth\").description(\"Manage the stored Letter connection\");\n\n auth\n .command(\"status\")\n .description(\"Show whether this machine is connected to Letter\")\n .action(async () => {\n const cred = await readCredential();\n const envKey = process.env.LETTER_API_KEY;\n\n if (!cred && !envKey) {\n if (isJsonMode()) printJson({ connected: false });\n else printInfo(\"Not connected. Run \" + c.bold(\"letter login\") + \" to set up.\");\n return;\n }\n\n if (isJsonMode()) {\n printJson({\n connected: true,\n source: envKey ? \"env\" : \"credentials\",\n project: cred?.project ?? null,\n base_url: cred?.baseUrl ?? null,\n key: envKey ? \"env\" : cred ? mask(cred.apiKey) : null,\n });\n return;\n }\n\n printSuccess(\"Connected\" + (cred ? ` to ${c.bold(cred.project.name)}` : \"\"));\n if (cred) {\n console.log(c.dim(\" Project: \") + cred.project.slug);\n console.log(c.dim(\" Key: \") + mask(cred.apiKey));\n console.log(c.dim(\" API: \") + cred.baseUrl);\n }\n if (envKey) console.log(c.dim(\" LETTER_API_KEY is set in the environment.\"));\n });\n\n auth\n .command(\"logout\")\n .description(\"Remove the stored credential (~/.letter/credentials.json)\")\n .action(async () => {\n await clearCredential();\n printSuccess(\"Removed stored credential.\");\n });\n}\n","import { Command } from \"commander\";\nimport { client } from \"../client.js\";\nimport { readCredential } from \"../config.js\";\nimport { c, isJsonMode, printError, printInfo, printJson, printSuccess, spinner } from \"../output.js\";\n\ntype StatusResponse = {\n contacts?: number;\n events?: number;\n [k: string]: unknown;\n};\n\nexport function registerStatusCommand(program: Command) {\n program\n .command(\"status\")\n .description(\"Check whether your project has received any contacts or events\")\n .action(async () => {\n const spin = spinner(\"Checking your Letter project…\").start();\n try {\n const { data } = await client.get<StatusResponse>(\"/v1/status\");\n spin.stop();\n\n if (isJsonMode()) {\n printJson(data);\n return;\n }\n\n const cred = await readCredential();\n const contacts = data.contacts ?? 0;\n const events = data.events ?? 0;\n\n if (cred) printInfo(`Project ${c.bold(cred.project.name)}`);\n if (contacts > 0 || events > 0) {\n printSuccess(`Connected. ${contacts} contact(s), ${events} event(s) received.`);\n } else {\n printInfo(\"Connected, but no data yet. Fire your first identify/track call.\");\n }\n } catch (err) {\n spin.stop();\n printError(err);\n process.exitCode = 1;\n }\n });\n}\n","import { Command } from \"commander\";\nimport {\n getBaseUrl,\n setBaseUrl,\n getConfigPath,\n resetConfig,\n} from \"../config.js\";\nimport { c, isJsonMode, printError, printJson, printSuccess } from \"../output.js\";\n\nexport function registerConfigCommands(program: Command) {\n const cfg = program.command(\"config\").description(\"Manage CLI configuration\");\n\n cfg\n .command(\"set\")\n .description(\"Set a config value\")\n .argument(\"<key>\", \"Config key: base-url\")\n .argument(\"<value>\", \"Value to set\")\n .action((key: string, value: string) => {\n switch (key) {\n case \"base-url\":\n setBaseUrl(value);\n printSuccess(`Base URL set to ${getBaseUrl()}`);\n break;\n default:\n printError(new Error(`Unknown config key: ${key}. Valid keys: base-url`));\n process.exitCode = 1;\n }\n });\n\n cfg\n .command(\"get\")\n .description(\"Show CLI configuration\")\n .argument(\"[key]\", \"Config key (omit to show all)\")\n .action((key?: string) => {\n const all = { base_url: getBaseUrl(), config_path: getConfigPath() };\n if (key && key !== \"base-url\" && key !== \"path\") {\n printError(new Error(`Unknown config key: ${key}`));\n process.exitCode = 1;\n return;\n }\n if (isJsonMode()) {\n printJson(key === \"base-url\" ? { base_url: all.base_url } : key === \"path\" ? { config_path: all.config_path } : all);\n return;\n }\n if (key === \"base-url\") console.log(all.base_url);\n else if (key === \"path\") console.log(all.config_path);\n else for (const [k, v] of Object.entries(all)) console.log(c.bold(k + \":\") + \" \" + v);\n });\n\n cfg\n .command(\"reset\")\n .description(\"Reset CLI configuration to defaults\")\n .action(() => {\n resetConfig();\n printSuccess(\"Configuration reset.\");\n });\n}\n"],"mappings":"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","names":["path","path","readFile","writeFile","spawn","readFile","readFile","spawn","program","program","program","program"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letterapp/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Letter CLI - connect your app to Letter in one command. Interactive, secure device login: no API key ever touches your shell or chat.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://letter.app",
|
|
@@ -30,19 +30,27 @@
|
|
|
30
30
|
"cli",
|
|
31
31
|
"analytics",
|
|
32
32
|
"email",
|
|
33
|
-
"onboarding"
|
|
33
|
+
"onboarding",
|
|
34
|
+
"ai-agents"
|
|
34
35
|
],
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public"
|
|
37
38
|
},
|
|
38
39
|
"scripts": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"start": "node dist/index.js",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"chalk": "^5.4.1",
|
|
47
|
+
"commander": "^13.1.0",
|
|
48
|
+
"conf": "^13.1.0",
|
|
49
|
+
"ora": "^8.2.0"
|
|
43
50
|
},
|
|
44
51
|
"devDependencies": {
|
|
45
52
|
"@types/node": "^22.10.2",
|
|
53
|
+
"tsup": "^8.4.0",
|
|
46
54
|
"typescript": "^5.7.2"
|
|
47
55
|
}
|
|
48
56
|
}
|
package/dist/commands/login.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { parseArgs, flagString, flagBool } from "../lib/args.js";
|
|
2
|
-
import { resolveApiBase, DEFAULT_API_BASE, saveCredential } from "../lib/config.js";
|
|
3
|
-
import { startDeviceAuth, pollDeviceAuth } from "../lib/api.js";
|
|
4
|
-
import { openUrl } from "../lib/browser.js";
|
|
5
|
-
import { upsertEnv } from "../lib/env-file.js";
|
|
6
|
-
import { detectFramework, detectPackageManager, installCommand, runInstall, } from "../lib/pm.js";
|
|
7
|
-
import { banner, color, error, info, log, prompt, success, warn, } from "../lib/ui.js";
|
|
8
|
-
const SDK_PACKAGE = "@letterapp/node";
|
|
9
|
-
const ENV_FILE = ".env.local";
|
|
10
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
|
-
/**
|
|
12
|
-
* `letter login` / `letter init` (the default command). Runs the interactive
|
|
13
|
-
* device-authorization flow, writes the minted key to the project env + the
|
|
14
|
-
* shared credential store, and installs the SDK. The API key is NEVER printed.
|
|
15
|
-
*/
|
|
16
|
-
export async function runLogin(argv) {
|
|
17
|
-
const { flags } = parseArgs(argv);
|
|
18
|
-
const base = resolveApiBase(flagString(flags, "base-url"));
|
|
19
|
-
const cwd = process.cwd();
|
|
20
|
-
const autoYes = flagBool(flags, "yes") === true || flags.y === true;
|
|
21
|
-
const allowOpen = flagBool(flags, "open") !== false; // --no-open disables
|
|
22
|
-
const doInstall = flagBool(flags, "install") !== false; // --no-install disables
|
|
23
|
-
const ciKey = flagString(flags, "api-key");
|
|
24
|
-
banner();
|
|
25
|
-
// CI escape hatch: write the provided key non-interactively. Documented as
|
|
26
|
-
// for automation only - interactive/agent use should rely on the device flow
|
|
27
|
-
// so no secret is passed through the command line / chat.
|
|
28
|
-
if (ciKey) {
|
|
29
|
-
const entries = { LETTER_API_KEY: ciKey };
|
|
30
|
-
if (base !== DEFAULT_API_BASE)
|
|
31
|
-
entries.LETTER_BASE_URL = base;
|
|
32
|
-
const file = await upsertEnv(cwd, ENV_FILE, entries);
|
|
33
|
-
success(`Saved LETTER_API_KEY to ${rel(cwd, file)} (--api-key).`);
|
|
34
|
-
warn("--api-key is for CI. For interactive setup, run `letter login`.");
|
|
35
|
-
return 0;
|
|
36
|
-
}
|
|
37
|
-
// 1. Begin the flow.
|
|
38
|
-
let flow;
|
|
39
|
-
try {
|
|
40
|
-
flow = await startDeviceAuth(base);
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
error(err.message);
|
|
44
|
-
return 1;
|
|
45
|
-
}
|
|
46
|
-
log(`${color.bold("Confirm this code in your browser:")}`);
|
|
47
|
-
log();
|
|
48
|
-
log(` ${color.cyan(color.bold(flow.user_code))}`);
|
|
49
|
-
log();
|
|
50
|
-
// 2. Open the browser (interactive: wait for Enter; otherwise auto/print).
|
|
51
|
-
const interactive = process.stdin.isTTY && !autoYes;
|
|
52
|
-
if (interactive && allowOpen) {
|
|
53
|
-
await prompt(color.dim("Press Enter to open your browser… "));
|
|
54
|
-
}
|
|
55
|
-
if (allowOpen)
|
|
56
|
-
openUrl(flow.verification_uri_complete);
|
|
57
|
-
info(`If your browser didn't open, visit:`);
|
|
58
|
-
log(` ${color.blue(flow.verification_uri_complete)}`);
|
|
59
|
-
log();
|
|
60
|
-
info("Waiting for you to approve… (Ctrl+C to cancel)");
|
|
61
|
-
// 3. Poll until the user approves/denies or the code expires.
|
|
62
|
-
const deadline = Date.now() + flow.expires_in * 1000;
|
|
63
|
-
let intervalMs = Math.max(1, flow.interval) * 1000;
|
|
64
|
-
while (Date.now() < deadline) {
|
|
65
|
-
await sleep(intervalMs);
|
|
66
|
-
let res;
|
|
67
|
-
try {
|
|
68
|
-
res = await pollDeviceAuth(base, flow.device_code);
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
error(err.message);
|
|
72
|
-
return 1;
|
|
73
|
-
}
|
|
74
|
-
if (res.status === "authorization_pending")
|
|
75
|
-
continue;
|
|
76
|
-
if (res.status === "slow_down") {
|
|
77
|
-
intervalMs += 1000;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (res.status === "access_denied") {
|
|
81
|
-
error("Request denied in the browser. Nothing was changed.");
|
|
82
|
-
return 1;
|
|
83
|
-
}
|
|
84
|
-
if (res.status === "expired_token") {
|
|
85
|
-
error("This login expired. Run the command again to retry.");
|
|
86
|
-
return 1;
|
|
87
|
-
}
|
|
88
|
-
// Approved.
|
|
89
|
-
return finish(res.api_key, res.base_url, res.project, cwd, doInstall);
|
|
90
|
-
}
|
|
91
|
-
error("Timed out waiting for approval. Run the command again to retry.");
|
|
92
|
-
return 1;
|
|
93
|
-
}
|
|
94
|
-
async function finish(apiKey, baseUrl, project, cwd, doInstall) {
|
|
95
|
-
log();
|
|
96
|
-
success(`Approved for project ${color.bold(project.name)}.`);
|
|
97
|
-
// Write the key to the project env (value never printed) + shared store.
|
|
98
|
-
const entries = { LETTER_API_KEY: apiKey };
|
|
99
|
-
if (baseUrl && baseUrl !== DEFAULT_API_BASE)
|
|
100
|
-
entries.LETTER_BASE_URL = baseUrl;
|
|
101
|
-
const envFile = await upsertEnv(cwd, ENV_FILE, entries);
|
|
102
|
-
success(`Saved LETTER_API_KEY to ${rel(cwd, envFile)}.`);
|
|
103
|
-
try {
|
|
104
|
-
const credFile = await saveCredential({
|
|
105
|
-
apiKey,
|
|
106
|
-
baseUrl: baseUrl || DEFAULT_API_BASE,
|
|
107
|
-
project,
|
|
108
|
-
savedAt: new Date().toISOString(),
|
|
109
|
-
});
|
|
110
|
-
success(`Stored credentials in ${tildify(credFile)} for tooling (MCP).`);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
warn("Could not write ~/.letter/credentials.json (continuing).");
|
|
114
|
-
}
|
|
115
|
-
// Install the SDK.
|
|
116
|
-
const pm = await detectPackageManager(cwd);
|
|
117
|
-
if (doInstall) {
|
|
118
|
-
info(`Installing ${SDK_PACKAGE} with ${pm}…`);
|
|
119
|
-
const code = await runInstall(pm, SDK_PACKAGE, cwd);
|
|
120
|
-
if (code === 0)
|
|
121
|
-
success(`Installed ${SDK_PACKAGE}.`);
|
|
122
|
-
else
|
|
123
|
-
warn(`Install failed. Run: ${installCommand(pm, SDK_PACKAGE)}`);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
info(`Skipped install. Run: ${installCommand(pm, SDK_PACKAGE)}`);
|
|
127
|
-
}
|
|
128
|
-
const framework = await detectFramework(cwd);
|
|
129
|
-
printNextSteps(framework);
|
|
130
|
-
return 0;
|
|
131
|
-
}
|
|
132
|
-
function printNextSteps(framework) {
|
|
133
|
-
log();
|
|
134
|
-
log(color.bold("Next steps"));
|
|
135
|
-
if (framework)
|
|
136
|
-
log(color.dim(`Detected ${framework}.`));
|
|
137
|
-
log(` 1. Create a server-side client that reads ${color.cyan("process.env.LETTER_API_KEY")}.`);
|
|
138
|
-
log(` 2. Call ${color.cyan("letter.identify(...)")} where users sign up or log in.`);
|
|
139
|
-
log(` 3. Call ${color.cyan("letter.track(...)")} on 2-3 key actions.`);
|
|
140
|
-
log();
|
|
141
|
-
log(color.dim("Full guide: https://letter.app/docs/agent-setup"));
|
|
142
|
-
log(color.dim("Your API key is in .env.local - keep it out of source control."));
|
|
143
|
-
log();
|
|
144
|
-
}
|
|
145
|
-
function rel(cwd, file) {
|
|
146
|
-
return file.startsWith(cwd) ? file.slice(cwd.length + 1) || file : file;
|
|
147
|
-
}
|
|
148
|
-
function tildify(file) {
|
|
149
|
-
const home = process.env.HOME || process.env.USERPROFILE;
|
|
150
|
-
return home && file.startsWith(home) ? `~${file.slice(home.length)}` : file;
|
|
151
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { runLogin } from "./login.js";
|
|
2
|
-
/**
|
|
3
|
-
* Command registry. v1 ships only the device-login setup flow (as `login`,
|
|
4
|
-
* `init`, and the default), but the shape here is the extension point: future
|
|
5
|
-
* authenticated subcommands (`sequences`, `broadcast`, `contacts`, `events`,
|
|
6
|
-
* `keys`, `status`) register the same way and reuse the credential store in
|
|
7
|
-
* lib/config.ts + the API client in lib/api.ts. See README "Roadmap".
|
|
8
|
-
*/
|
|
9
|
-
export const COMMANDS = [
|
|
10
|
-
{
|
|
11
|
-
name: "login",
|
|
12
|
-
aliases: ["init"],
|
|
13
|
-
summary: "Connect this project to Letter (interactive device login)",
|
|
14
|
-
run: runLogin,
|
|
15
|
-
},
|
|
16
|
-
];
|
|
17
|
-
/** The command that runs when none is given. */
|
|
18
|
-
export const DEFAULT_COMMAND = "login";
|
|
19
|
-
export function findCommand(name) {
|
|
20
|
-
return COMMANDS.find((c) => c.name === name || c.aliases?.includes(name));
|
|
21
|
-
}
|
package/dist/lib/api.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
const USER_AGENT = "@letterapp/cli";
|
|
2
|
-
/** Starts a device-authorization flow against the given API base. */
|
|
3
|
-
export async function startDeviceAuth(base) {
|
|
4
|
-
const res = await fetch(`${base}/v1/cli/auth/start`, {
|
|
5
|
-
method: "POST",
|
|
6
|
-
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
7
|
-
body: "{}",
|
|
8
|
-
});
|
|
9
|
-
if (!res.ok) {
|
|
10
|
-
throw new Error(`Could not start login (HTTP ${res.status}). Is ${base} reachable?`);
|
|
11
|
-
}
|
|
12
|
-
return (await res.json());
|
|
13
|
-
}
|
|
14
|
-
/** Polls once for approval. */
|
|
15
|
-
export async function pollDeviceAuth(base, deviceCode) {
|
|
16
|
-
const res = await fetch(`${base}/v1/cli/auth/poll`, {
|
|
17
|
-
method: "POST",
|
|
18
|
-
headers: { "content-type": "application/json", "user-agent": USER_AGENT },
|
|
19
|
-
body: JSON.stringify({ device_code: deviceCode }),
|
|
20
|
-
});
|
|
21
|
-
if (res.status === 429) {
|
|
22
|
-
const retryAfter = Number(res.headers.get("retry-after") ?? "5");
|
|
23
|
-
return { status: "slow_down", retryAfter };
|
|
24
|
-
}
|
|
25
|
-
if (!res.ok) {
|
|
26
|
-
throw new Error(`Login poll failed (HTTP ${res.status}).`);
|
|
27
|
-
}
|
|
28
|
-
return (await res.json());
|
|
29
|
-
}
|
package/dist/lib/args.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export function parseArgs(argv) {
|
|
2
|
-
const positionals = [];
|
|
3
|
-
const flags = {};
|
|
4
|
-
for (let i = 0; i < argv.length; i++) {
|
|
5
|
-
const arg = argv[i];
|
|
6
|
-
if (arg.startsWith("--")) {
|
|
7
|
-
const body = arg.slice(2);
|
|
8
|
-
const eq = body.indexOf("=");
|
|
9
|
-
if (eq !== -1) {
|
|
10
|
-
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
11
|
-
}
|
|
12
|
-
else if (body.startsWith("no-")) {
|
|
13
|
-
flags[body.slice(3)] = false;
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
const next = argv[i + 1];
|
|
17
|
-
if (next && !next.startsWith("-")) {
|
|
18
|
-
flags[body] = next;
|
|
19
|
-
i++;
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
flags[body] = true;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
else if (arg.startsWith("-") && arg.length > 1) {
|
|
27
|
-
for (const ch of arg.slice(1))
|
|
28
|
-
flags[ch] = true;
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
positionals.push(arg);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return { positionals, flags };
|
|
35
|
-
}
|
|
36
|
-
export function flagString(flags, ...names) {
|
|
37
|
-
for (const n of names) {
|
|
38
|
-
const v = flags[n];
|
|
39
|
-
if (typeof v === "string")
|
|
40
|
-
return v;
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
export function flagBool(flags, name) {
|
|
45
|
-
const v = flags[name];
|
|
46
|
-
if (typeof v === "boolean")
|
|
47
|
-
return v;
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
package/dist/lib/browser.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
/**
|
|
3
|
-
* Opens `url` in the default browser. Best-effort and non-blocking: if it fails
|
|
4
|
-
* (headless box, no DISPLAY) the caller has already printed the URL so the user
|
|
5
|
-
* can open it manually. Returns true if a launcher was spawned.
|
|
6
|
-
*/
|
|
7
|
-
export function openUrl(url) {
|
|
8
|
-
const platform = process.platform;
|
|
9
|
-
let command;
|
|
10
|
-
let args;
|
|
11
|
-
if (platform === "darwin") {
|
|
12
|
-
command = "open";
|
|
13
|
-
args = [url];
|
|
14
|
-
}
|
|
15
|
-
else if (platform === "win32") {
|
|
16
|
-
command = "cmd";
|
|
17
|
-
// `start` needs an empty title arg; the comma-free form avoids quoting woes.
|
|
18
|
-
args = ["/c", "start", "", url];
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
command = "xdg-open";
|
|
22
|
-
args = [url];
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
26
|
-
child.on("error", () => { });
|
|
27
|
-
child.unref();
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
package/dist/lib/config.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
/** The SDK's built-in default; when the resolved base equals this we don't
|
|
5
|
-
* write LETTER_BASE_URL to the project env. */
|
|
6
|
-
export const DEFAULT_API_BASE = "https://api.letter.app";
|
|
7
|
-
/** Resolve the API base: explicit flag > env > prod default. */
|
|
8
|
-
export function resolveApiBase(flagValue) {
|
|
9
|
-
const raw = flagValue || process.env.LETTER_BASE_URL || DEFAULT_API_BASE;
|
|
10
|
-
return raw.replace(/\/$/, "");
|
|
11
|
-
}
|
|
12
|
-
function credentialsPath() {
|
|
13
|
-
return path.join(homedir(), ".letter", "credentials.json");
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Persists the credential to ~/.letter/credentials.json with owner-only
|
|
17
|
-
* permissions. Read by tools like @letterapp/mcp so the secret never has to be
|
|
18
|
-
* pasted into an MCP config.
|
|
19
|
-
*/
|
|
20
|
-
export async function saveCredential(cred) {
|
|
21
|
-
const file = credentialsPath();
|
|
22
|
-
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
23
|
-
await writeFile(file, `${JSON.stringify(cred, null, 2)}\n`, { mode: 0o600 });
|
|
24
|
-
return file;
|
|
25
|
-
}
|
|
26
|
-
/** Reads the stored credential, or null if none. */
|
|
27
|
-
export async function readCredential() {
|
|
28
|
-
try {
|
|
29
|
-
const raw = await readFile(credentialsPath(), "utf8");
|
|
30
|
-
return JSON.parse(raw);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
package/dist/lib/env-file.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { readFile, writeFile, stat } from "node:fs/promises";
|
|
3
|
-
/**
|
|
4
|
-
* Upserts `key=value` in an env file, creating it if needed. Existing keys are
|
|
5
|
-
* replaced in place; new keys are appended. Returns the file path. The value is
|
|
6
|
-
* never logged by this module - callers print only the key name.
|
|
7
|
-
*/
|
|
8
|
-
export async function upsertEnv(cwd, file, entries) {
|
|
9
|
-
const filePath = path.join(cwd, file);
|
|
10
|
-
let contents = "";
|
|
11
|
-
try {
|
|
12
|
-
contents = await readFile(filePath, "utf8");
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
contents = "";
|
|
16
|
-
}
|
|
17
|
-
let next = contents;
|
|
18
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
19
|
-
const line = `${key}=${value}`;
|
|
20
|
-
const re = new RegExp(`^${escapeRegExp(key)}=.*$`, "m");
|
|
21
|
-
if (re.test(next)) {
|
|
22
|
-
next = next.replace(re, line);
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
if (next.length && !next.endsWith("\n"))
|
|
26
|
-
next += "\n";
|
|
27
|
-
next += `${line}\n`;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
await writeFile(filePath, next, "utf8");
|
|
31
|
-
return filePath;
|
|
32
|
-
}
|
|
33
|
-
/** True if a file exists at `cwd/name`. */
|
|
34
|
-
export async function fileExists(cwd, name) {
|
|
35
|
-
try {
|
|
36
|
-
await stat(path.join(cwd, name));
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function escapeRegExp(s) {
|
|
44
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45
|
-
}
|
package/dist/lib/pm.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { fileExists } from "./env-file.js";
|
|
3
|
-
/**
|
|
4
|
-
* Detects the package manager from lockfiles (then the `npm_config_user_agent`
|
|
5
|
-
* of the running process), defaulting to npm.
|
|
6
|
-
*/
|
|
7
|
-
export async function detectPackageManager(cwd) {
|
|
8
|
-
if (await fileExists(cwd, "pnpm-lock.yaml"))
|
|
9
|
-
return "pnpm";
|
|
10
|
-
if (await fileExists(cwd, "yarn.lock"))
|
|
11
|
-
return "yarn";
|
|
12
|
-
if (await fileExists(cwd, "bun.lockb"))
|
|
13
|
-
return "bun";
|
|
14
|
-
if (await fileExists(cwd, "package-lock.json"))
|
|
15
|
-
return "npm";
|
|
16
|
-
const ua = process.env.npm_config_user_agent ?? "";
|
|
17
|
-
if (ua.startsWith("pnpm"))
|
|
18
|
-
return "pnpm";
|
|
19
|
-
if (ua.startsWith("yarn"))
|
|
20
|
-
return "yarn";
|
|
21
|
-
if (ua.startsWith("bun"))
|
|
22
|
-
return "bun";
|
|
23
|
-
return "npm";
|
|
24
|
-
}
|
|
25
|
-
/** Detects a likely web framework for friendlier guidance. */
|
|
26
|
-
export async function detectFramework(cwd) {
|
|
27
|
-
try {
|
|
28
|
-
const pkgPath = `${cwd}/package.json`;
|
|
29
|
-
const { readFile } = await import("node:fs/promises");
|
|
30
|
-
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
31
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
32
|
-
if (deps.next)
|
|
33
|
-
return "Next.js";
|
|
34
|
-
if (deps.nuxt)
|
|
35
|
-
return "Nuxt";
|
|
36
|
-
if (deps["@remix-run/node"] || deps["@remix-run/react"])
|
|
37
|
-
return "Remix";
|
|
38
|
-
if (deps.express)
|
|
39
|
-
return "Express";
|
|
40
|
-
if (deps.fastify)
|
|
41
|
-
return "Fastify";
|
|
42
|
-
if (deps.hono)
|
|
43
|
-
return "Hono";
|
|
44
|
-
if (deps["@sveltejs/kit"])
|
|
45
|
-
return "SvelteKit";
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export function installCommand(pm, pkg) {
|
|
53
|
-
switch (pm) {
|
|
54
|
-
case "pnpm":
|
|
55
|
-
return `pnpm add ${pkg}`;
|
|
56
|
-
case "yarn":
|
|
57
|
-
return `yarn add ${pkg}`;
|
|
58
|
-
case "bun":
|
|
59
|
-
return `bun add ${pkg}`;
|
|
60
|
-
default:
|
|
61
|
-
return `npm install ${pkg}`;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/** Runs the install command, streaming output. Resolves to the exit code. */
|
|
65
|
-
export function runInstall(pm, pkg, cwd) {
|
|
66
|
-
const args = pm === "npm" ? ["install", pkg] : pm === "yarn" ? ["add", pkg] : ["add", pkg];
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
const child = spawn(pm, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
69
|
-
child.on("error", () => resolve(1));
|
|
70
|
-
child.on("close", (code) => resolve(code ?? 1));
|
|
71
|
-
});
|
|
72
|
-
}
|
package/dist/lib/ui.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import readline from "node:readline";
|
|
2
|
-
const useColor = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
3
|
-
function wrap(open, close) {
|
|
4
|
-
return (s) => useColor ? `\u001b[${open}m${s}\u001b[${close}m` : s;
|
|
5
|
-
}
|
|
6
|
-
export const color = {
|
|
7
|
-
bold: wrap(1, 22),
|
|
8
|
-
dim: wrap(2, 22),
|
|
9
|
-
red: wrap(31, 39),
|
|
10
|
-
green: wrap(32, 39),
|
|
11
|
-
yellow: wrap(33, 39),
|
|
12
|
-
blue: wrap(34, 39),
|
|
13
|
-
cyan: wrap(36, 39),
|
|
14
|
-
gray: wrap(90, 39),
|
|
15
|
-
};
|
|
16
|
-
export function log(msg = "") {
|
|
17
|
-
process.stdout.write(`${msg}\n`);
|
|
18
|
-
}
|
|
19
|
-
export function info(msg) {
|
|
20
|
-
log(`${color.cyan("›")} ${msg}`);
|
|
21
|
-
}
|
|
22
|
-
export function success(msg) {
|
|
23
|
-
log(`${color.green("✓")} ${msg}`);
|
|
24
|
-
}
|
|
25
|
-
export function warn(msg) {
|
|
26
|
-
log(`${color.yellow("!")} ${msg}`);
|
|
27
|
-
}
|
|
28
|
-
export function error(msg) {
|
|
29
|
-
process.stderr.write(`${color.red("✗")} ${msg}\n`);
|
|
30
|
-
}
|
|
31
|
-
export function step(n, total, msg) {
|
|
32
|
-
log(`${color.dim(`[${n}/${total}]`)} ${msg}`);
|
|
33
|
-
}
|
|
34
|
-
/** The Letter wordmark banner. */
|
|
35
|
-
export function banner() {
|
|
36
|
-
log();
|
|
37
|
-
log(` ${color.bold("Letter")}${color.red(".")} ${color.dim("CLI")}`);
|
|
38
|
-
log();
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Prompts the user and resolves with their input. Returns "" immediately on a
|
|
42
|
-
* non-interactive stdin so automated/agent runs don't hang.
|
|
43
|
-
*/
|
|
44
|
-
export function prompt(question) {
|
|
45
|
-
if (!process.stdin.isTTY)
|
|
46
|
-
return Promise.resolve("");
|
|
47
|
-
const rl = readline.createInterface({
|
|
48
|
-
input: process.stdin,
|
|
49
|
-
output: process.stdout,
|
|
50
|
-
});
|
|
51
|
-
return new Promise((resolve) => {
|
|
52
|
-
rl.question(question, (answer) => {
|
|
53
|
-
rl.close();
|
|
54
|
-
resolve(answer);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
}
|