@lingo.dev/cli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1533 @@
1
+ import { C as discoverLocales, S as writeLocaleFile, T as tryReadFile, _ as runExtractionPipeline, b as mergeEntries, c as formatCheckResult, d as computeSourceStatus, f as formatStatusTable, g as generateTypes, h as toApiPayload, l as runChecks, m as planLocalization, n as UpdateService, p as applyTranslations, s as VERSION, t as UpdateError, u as computeLocaleStatus, v as toLocaleEntries, w as findSourceFiles, x as readLocaleFile, y as getActiveEntries } from "./update-RHUBOb93.js";
2
+ import { Args, CliConfig, Command, Options } from "@effect/cli";
3
+ import { Console, Context, Data, Effect, Layer, Option, pipe } from "effect";
4
+ import pc from "picocolors";
5
+ import * as fs$1 from "node:fs/promises";
6
+ import * as path from "node:path";
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import { createClient } from "@supabase/supabase-js";
10
+ import cfonts from "cfonts";
11
+ import logUpdate from "log-update";
12
+ import * as clack from "@clack/prompts";
13
+ import { spawn } from "node:child_process";
14
+ import * as ci from "ci-info";
15
+ import * as crypto from "node:crypto";
16
+ import { fileURLToPath } from "node:url";
17
+ //#region src/services/cli-options.ts
18
+ /**
19
+ * Shared CLI option definitions used across extract, localize, status, and ship commands.
20
+ */
21
+ const sourceLocaleOption = Options.text("source-locale").pipe(Options.withDescription("Source locale code"), Options.withDefault("en"));
22
+ const targetLocaleOption = pipe(Options.text("target-locale"), Options.withDescription("Target locale(s) to localize"), Options.repeated);
23
+ const srcOption = Options.text("src").pipe(Options.withDescription("Source directory to scan"), Options.withDefault("src"));
24
+ const outOption = Options.text("out").pipe(Options.withDescription("Output directory for locale files"), Options.withDefault("locales"));
25
+ const jsonOption = Options.boolean("json").pipe(Options.withDescription("Machine-readable JSON output for CI"));
26
+ //#endregion
27
+ //#region src/commands/extract.ts
28
+ const check = Options.boolean("check").pipe(Options.withDescription("Check if JSONC is up to date (CI gate)"));
29
+ const types = Options.text("types").pipe(Options.withDescription("Output path for generated type declarations"), Options.withDefault("lingo.d.ts"));
30
+ const extractCommand = Command.make("extract", {
31
+ check,
32
+ src: srcOption,
33
+ sourceLocale: sourceLocaleOption,
34
+ out: outOption,
35
+ types
36
+ }, ({ check, src, sourceLocale, out, types: typesOut }) => Effect.gen(function* () {
37
+ const cwd = process.cwd();
38
+ const srcDir = path.resolve(cwd, src);
39
+ const outDir = path.resolve(cwd, out);
40
+ const localePath = path.join(outDir, `${sourceLocale}.jsonc`);
41
+ const typesPath = path.resolve(cwd, typesOut);
42
+ const filePaths = yield* Effect.tryPromise(() => findSourceFiles(srcDir));
43
+ if (filePaths.length === 0) {
44
+ yield* Console.log(`No .ts/.tsx files found in ${src}/`);
45
+ return;
46
+ }
47
+ const { messages, warnings, collisions } = runExtractionPipeline(yield* Effect.tryPromise(() => Promise.all(filePaths.map(async (f) => ({
48
+ code: await fs$1.readFile(f, "utf-8"),
49
+ filePath: path.relative(cwd, f)
50
+ })))));
51
+ for (const w of warnings) yield* Console.log(` warn: ${w.src} - ${w.message}`);
52
+ if (collisions.length > 0) {
53
+ for (const c of collisions) {
54
+ yield* Console.error(` collision: key "${c.key}" ->`);
55
+ for (const e of c.entries) yield* Console.error(` "${e.source}" at ${e.src}`);
56
+ }
57
+ return yield* Effect.die(/* @__PURE__ */ new Error(`${collisions.length} hash collision(s) detected. Add context strings to disambiguate.`));
58
+ }
59
+ const entries = toLocaleEntries(messages);
60
+ const existingContent = yield* Effect.tryPromise(() => tryReadFile(localePath));
61
+ const existing = existingContent ? readLocaleFile(existingContent) : { entries: [] };
62
+ const merged = mergeEntries(existing, entries);
63
+ const jsonc = writeLocaleFile(merged);
64
+ const typesContent = generateTypes(merged);
65
+ if (check) {
66
+ const currentJsonc = existingContent ?? "";
67
+ const currentTypes = (yield* Effect.tryPromise(() => tryReadFile(typesPath))) ?? "";
68
+ if (jsonc !== currentJsonc || typesContent !== currentTypes) {
69
+ yield* Console.error("JSONC is stale. Run `lingo extract` to update.");
70
+ return yield* Effect.die(/* @__PURE__ */ new Error("Stale JSONC"));
71
+ }
72
+ yield* Console.log("Up to date.");
73
+ return;
74
+ }
75
+ yield* Effect.tryPromise(() => fs$1.mkdir(outDir, { recursive: true }));
76
+ yield* Effect.tryPromise(() => fs$1.writeFile(localePath, jsonc));
77
+ yield* Effect.tryPromise(() => fs$1.writeFile(typesPath, typesContent));
78
+ const newCount = entries.filter((e) => !existing.entries.some((ex) => ex.key === e.key)).length;
79
+ const orphanCount = merged.entries.filter((e) => e.metadata.orphan).length;
80
+ yield* Console.log(`Extracted ${messages.length} strings (${newCount} new, ${orphanCount} orphaned) -> ${path.relative(cwd, localePath)}`);
81
+ })).pipe(Command.withDescription("Extract translatable strings from source files"));
82
+ //#endregion
83
+ //#region src/services/supabase.ts
84
+ const supabase = createClient(process.env.LINGO_SUPABASE_URL ?? "https://nlugbbdqxnqwhydszieg.supabase.co", process.env.LINGO_SUPABASE_ANON_KEY ?? "sb_publishable_ZZH1cgFbkmb7Y0H482RFmA_jsvBxc3e", { auth: {
85
+ persistSession: false,
86
+ autoRefreshToken: false
87
+ } });
88
+ const sendOtp = (email) => Effect.tryPromise({
89
+ try: async () => {
90
+ const { error } = await supabase.auth.signInWithOtp({
91
+ email,
92
+ options: { shouldCreateUser: true }
93
+ });
94
+ if (error) throw error;
95
+ },
96
+ catch: (e) => new SupabaseError({
97
+ cause: e,
98
+ message: `Failed to send code to ${email}. Check the email address and try again.`
99
+ })
100
+ });
101
+ const verifyOtp = (email, code) => Effect.tryPromise({
102
+ try: async () => {
103
+ const { data, error } = await supabase.auth.verifyOtp({
104
+ email,
105
+ token: code,
106
+ type: "email"
107
+ });
108
+ if (error) throw error;
109
+ if (!data.session) throw new Error("No session returned");
110
+ return data.session;
111
+ },
112
+ catch: (e) => new SupabaseError({
113
+ cause: e,
114
+ message: "Invalid or expired code. Run lingo login to try again."
115
+ })
116
+ });
117
+ const refreshSession = (refreshToken) => Effect.tryPromise({
118
+ try: async () => {
119
+ const { data, error } = await supabase.auth.refreshSession({ refresh_token: refreshToken });
120
+ if (error) throw error;
121
+ if (!data.session) throw new Error("No session returned");
122
+ return data.session;
123
+ },
124
+ catch: (e) => new SupabaseError({
125
+ cause: e,
126
+ message: "Session expired"
127
+ })
128
+ });
129
+ var SupabaseError = class extends Data.TaggedError("SupabaseError") {};
130
+ //#endregion
131
+ //#region src/services/auth.ts
132
+ var AuthError = class extends Data.TaggedError("AuthError") {};
133
+ var AuthContext = class extends Context.Tag("AuthContext")() {};
134
+ var AuthService = class extends Context.Tag("AuthService")() {};
135
+ function credentialsPaths(homeDir) {
136
+ const dir = path.join(homeDir ?? os.homedir(), ".lingo");
137
+ return {
138
+ dir,
139
+ file: path.join(dir, "credentials.json")
140
+ };
141
+ }
142
+ const AuthServiceLive = Layer.effect(AuthService, pipe(Effect.context(), Effect.map((ctx) => {
143
+ const authCtx = Context.get(ctx, AuthContext);
144
+ const { dir: credsDir, file: credsFile } = credentialsPaths(authCtx.homeDir);
145
+ const readFile = () => {
146
+ try {
147
+ return JSON.parse(fs.readFileSync(credsFile, "utf-8"));
148
+ } catch {
149
+ return null;
150
+ }
151
+ };
152
+ const writeFile = (creds) => {
153
+ fs.mkdirSync(credsDir, {
154
+ recursive: true,
155
+ mode: 448
156
+ });
157
+ fs.writeFileSync(credsFile, JSON.stringify(creds, null, 2), { mode: 384 });
158
+ };
159
+ const deleteFile = () => {
160
+ try {
161
+ fs.unlinkSync(credsFile);
162
+ } catch {}
163
+ };
164
+ const resolve = Effect.gen(function* () {
165
+ if (authCtx.apiKeyFlag) return {
166
+ type: "api-key",
167
+ apiKey: authCtx.apiKeyFlag
168
+ };
169
+ const envKey = process.env.LINGO_API_KEY;
170
+ if (envKey) return {
171
+ type: "api-key",
172
+ apiKey: envKey
173
+ };
174
+ const stored = readFile();
175
+ if (!stored) return yield* Effect.fail(new AuthError({ message: "Not authenticated. Run lingo login to get started." }));
176
+ return stored;
177
+ });
178
+ const store = (creds) => Effect.try({
179
+ try: () => writeFile(creds),
180
+ catch: (e) => new AuthError({ message: `Failed to save credentials: ${e}` })
181
+ });
182
+ return {
183
+ resolve,
184
+ store,
185
+ clear: Effect.sync(() => deleteFile()),
186
+ ensureFresh: Effect.gen(function* () {
187
+ const creds = yield* resolve;
188
+ if (creds.type === "api-key") return creds;
189
+ const now = Math.floor(Date.now() / 1e3);
190
+ if (creds.expiresAt > now + 60) return creds;
191
+ const session = yield* pipe(refreshSession(creds.refreshToken), Effect.mapError(() => new AuthError({ message: "Session expired. Run lingo login to get started." })));
192
+ const refreshed = {
193
+ type: "session",
194
+ email: session.user.email ?? creds.email,
195
+ accessToken: session.access_token,
196
+ refreshToken: session.refresh_token,
197
+ expiresAt: session.expires_at ?? now + session.expires_in
198
+ };
199
+ yield* store(refreshed);
200
+ return refreshed;
201
+ })
202
+ };
203
+ })));
204
+ //#endregion
205
+ //#region src/services/api-client.ts
206
+ const BASE_URL = process.env.LINGO_API_URL ?? "https://api.lingo.dev";
207
+ var ApiError = class extends Data.TaggedError("ApiError") {};
208
+ var ApiClient = class extends Context.Tag("ApiClient")() {};
209
+ function authHeaders(creds) {
210
+ if (creds.type === "session") return { Authorization: `Bearer ${creds.accessToken}` };
211
+ return { "X-API-Key": creds.apiKey };
212
+ }
213
+ function request(method, path, creds, body) {
214
+ return Effect.tryPromise({
215
+ try: async () => {
216
+ const res = await fetch(`${BASE_URL}${path}`, {
217
+ method,
218
+ headers: {
219
+ "Content-Type": "application/json",
220
+ ...authHeaders(creds)
221
+ },
222
+ body: body ? JSON.stringify(body) : void 0
223
+ });
224
+ if (!res.ok) {
225
+ const text = await res.text().catch(() => "");
226
+ throw new Error(`${res.status}: ${text}`);
227
+ }
228
+ return await res.json();
229
+ },
230
+ catch: (e) => new ApiError({ message: e instanceof Error ? e.message : String(e) })
231
+ });
232
+ }
233
+ const ApiClientLive = Layer.effect(ApiClient, Effect.map(AuthService, (auth) => {
234
+ const withAuth = (fn) => pipe(auth.ensureFresh, Effect.flatMap(fn));
235
+ return {
236
+ listMemberships: withAuth((creds) => request("GET", "/users/me/memberships", creds)),
237
+ createOrganization: (name) => withAuth((creds) => request("POST", "/organizations", creds, { name })),
238
+ listEngines: (orgId) => withAuth((creds) => request("GET", `/organizations/${orgId}/engines`, creds)),
239
+ createEngine: (orgId, name) => withAuth((creds) => request("POST", "/engines", creds, {
240
+ ownerOrganizationId: orgId,
241
+ name
242
+ })),
243
+ getMe: withAuth((creds) => request("GET", "/users/me", creds)),
244
+ updateMe: (payload) => withAuth((creds) => request("PATCH", "/users/me", creds, payload)),
245
+ localize: (params) => withAuth((creds) => request("POST", "/process/localize", creds, params))
246
+ };
247
+ }));
248
+ //#endregion
249
+ //#region src/services/config.ts
250
+ var ConfigError = class extends Data.TaggedError("ConfigError") {};
251
+ var ConfigService = class extends Context.Tag("ConfigService")() {};
252
+ const CONFIG_DIR = ".lingo";
253
+ const CONFIG_FILE = "config.json";
254
+ const HOME = os.homedir();
255
+ function findConfigPath(startDir) {
256
+ let dir = path.resolve(startDir);
257
+ while (true) {
258
+ const candidate = path.join(dir, CONFIG_DIR, CONFIG_FILE);
259
+ try {
260
+ fs.accessSync(candidate, fs.constants.R_OK);
261
+ return candidate;
262
+ } catch {}
263
+ const parent = path.dirname(dir);
264
+ if (parent === dir || dir === HOME) return null;
265
+ dir = parent;
266
+ }
267
+ }
268
+ function readConfigAt(filePath) {
269
+ try {
270
+ const content = fs.readFileSync(filePath, "utf-8");
271
+ const parsed = JSON.parse(content);
272
+ if (parsed.orgId && parsed.engineId) return parsed;
273
+ return null;
274
+ } catch {
275
+ return null;
276
+ }
277
+ }
278
+ const ConfigServiceLive = Layer.succeed(ConfigService, {
279
+ resolve: Effect.sync(() => {
280
+ const configPath = findConfigPath(process.cwd());
281
+ if (!configPath) return null;
282
+ return readConfigAt(configPath);
283
+ }),
284
+ store: (config) => Effect.try({
285
+ try: () => {
286
+ const dir = path.join(process.cwd(), CONFIG_DIR);
287
+ fs.mkdirSync(dir, { recursive: true });
288
+ fs.writeFileSync(path.join(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
289
+ },
290
+ catch: (e) => new ConfigError({ message: `Failed to save config: ${e}` })
291
+ }),
292
+ clear: Effect.sync(() => {
293
+ try {
294
+ fs.unlinkSync(path.join(process.cwd(), CONFIG_DIR, CONFIG_FILE));
295
+ } catch {}
296
+ }),
297
+ resolvedPath: Effect.sync(() => findConfigPath(process.cwd()))
298
+ });
299
+ //#endregion
300
+ //#region src/commands/localize.ts
301
+ const dryRun = Options.boolean("dry-run").pipe(Options.withDescription("Show what would change without writing"));
302
+ const localizeCommand = Command.make("localize", {
303
+ sourceLocale: sourceLocaleOption,
304
+ targetLocale: targetLocaleOption,
305
+ out: outOption,
306
+ dryRun
307
+ }, ({ sourceLocale, targetLocale: targetLocaleChunk, out, dryRun }) => Effect.gen(function* () {
308
+ const api = yield* ApiClient;
309
+ const config = yield* ConfigService;
310
+ const targetLocales = [...targetLocaleChunk];
311
+ if (targetLocales.length === 0) {
312
+ yield* Console.error("No target locales specified. Use --target-locale (e.g., --target-locale es --target-locale fr).");
313
+ return;
314
+ }
315
+ const cwd = process.cwd();
316
+ const outDir = path.resolve(cwd, out);
317
+ const sourcePath = path.join(outDir, `${sourceLocale}.jsonc`);
318
+ const sourceContent = yield* Effect.tryPromise(() => tryReadFile(sourcePath));
319
+ if (!sourceContent) {
320
+ yield* Console.error(`No source locale file at ${out}/${sourceLocale}.jsonc. Run \`lingo extract\` first.`);
321
+ return yield* Effect.die(/* @__PURE__ */ new Error(`Missing ${out}/${sourceLocale}.jsonc`));
322
+ }
323
+ const sourceFile = readLocaleFile(sourceContent);
324
+ const projectConfig = yield* config.resolve;
325
+ let totalNew = 0;
326
+ const failedLocales = [];
327
+ for (const locale of targetLocales) {
328
+ const targetPath = path.join(outDir, `${locale}.jsonc`);
329
+ const targetContent = yield* Effect.tryPromise(() => tryReadFile(targetPath));
330
+ const targetFile = targetContent ? readLocaleFile(targetContent) : { entries: [] };
331
+ const plan = planLocalization(sourceFile, targetFile, locale);
332
+ if (plan.missing.length === 0) {
333
+ yield* Console.log(` ${locale}: up to date (${plan.existing} strings)`);
334
+ continue;
335
+ }
336
+ if (dryRun) {
337
+ yield* Console.log(` ${locale}: ${plan.missing.length} strings to localize (dry run)`);
338
+ totalNew += plan.missing.length;
339
+ continue;
340
+ }
341
+ const { data, hints } = toApiPayload(plan.missing);
342
+ const result = yield* pipe(api.localize({
343
+ sourceLocale,
344
+ targetLocale: locale,
345
+ data,
346
+ hints,
347
+ engineId: projectConfig?.engineId,
348
+ triggerType: "cli"
349
+ }), Effect.catchAll((e) => Effect.gen(function* () {
350
+ yield* Console.error(` ${locale}: failed - ${e.message}`);
351
+ failedLocales.push(locale);
352
+ return null;
353
+ })));
354
+ if (!result) continue;
355
+ const translated = applyTranslations(plan.missing, result.data);
356
+ const merged = mergeEntries(targetFile, [...getActiveEntries(targetFile.entries), ...translated]);
357
+ yield* Effect.tryPromise(() => fs$1.mkdir(outDir, { recursive: true }));
358
+ yield* Effect.tryPromise(() => fs$1.writeFile(targetPath, writeLocaleFile(merged)));
359
+ totalNew += plan.missing.length;
360
+ yield* Console.log(` ${locale}: ${plan.missing.length} localized (${plan.existing + plan.missing.length} total)`);
361
+ }
362
+ if (dryRun) yield* Console.log(`\nDry run: ${totalNew} strings would be localized across ${targetLocales.length} locale(s).`);
363
+ else if (totalNew > 0 || failedLocales.length > 0) {
364
+ yield* Console.log(`\nLocalized ${totalNew} strings across ${targetLocales.length} locale(s).`);
365
+ if (failedLocales.length > 0) yield* Console.error(`Failed: ${failedLocales.join(", ")}`);
366
+ }
367
+ })).pipe(Command.withDescription("Localize missing strings to target locales"));
368
+ //#endregion
369
+ //#region src/commands/ship.ts
370
+ const shipCommand = Command.make("ship", {
371
+ sourceLocale: sourceLocaleOption,
372
+ targetLocale: targetLocaleOption,
373
+ src: srcOption,
374
+ out: outOption,
375
+ json: jsonOption
376
+ }, ({ sourceLocale, targetLocale: targetLocaleChunk, src, out, json }) => Effect.gen(function* () {
377
+ const api = yield* ApiClient;
378
+ const projectConfig = yield* (yield* ConfigService).resolve;
379
+ const targetLocales = [...targetLocaleChunk];
380
+ const cwd = process.cwd();
381
+ const srcDir = path.resolve(cwd, src);
382
+ const outDir = path.resolve(cwd, out);
383
+ const localePath = path.join(outDir, `${sourceLocale}.jsonc`);
384
+ const typesPath = path.resolve(cwd, "lingo.d.ts");
385
+ if (!json) yield* Console.log("Extracting...");
386
+ const filePaths = yield* Effect.tryPromise(() => findSourceFiles(srcDir));
387
+ if (filePaths.length === 0) {
388
+ yield* Console.error(`No .ts/.tsx files found in ${src}/`);
389
+ return yield* Effect.die(/* @__PURE__ */ new Error(`No source files in ${src}/`));
390
+ }
391
+ const { messages, warnings, collisions } = runExtractionPipeline(yield* Effect.tryPromise(() => Promise.all(filePaths.map(async (f) => ({
392
+ code: await fs$1.readFile(f, "utf-8"),
393
+ filePath: path.relative(cwd, f)
394
+ })))));
395
+ if (!json) for (const w of warnings) yield* Console.log(` warn: ${w.src} - ${w.message}`);
396
+ if (collisions.length > 0) {
397
+ for (const c of collisions) {
398
+ yield* Console.error(` collision: key "${c.key}" ->`);
399
+ for (const e of c.entries) yield* Console.error(` "${e.source}" at ${e.src}`);
400
+ }
401
+ return yield* Effect.die(/* @__PURE__ */ new Error(`${collisions.length} hash collision(s) detected.`));
402
+ }
403
+ const entries = toLocaleEntries(messages);
404
+ const existingContent = yield* Effect.tryPromise(() => tryReadFile(localePath));
405
+ const existing = existingContent ? readLocaleFile(existingContent) : { entries: [] };
406
+ const merged = mergeEntries(existing, entries);
407
+ yield* Effect.tryPromise(() => fs$1.mkdir(outDir, { recursive: true }));
408
+ yield* Effect.tryPromise(() => fs$1.writeFile(localePath, writeLocaleFile(merged)));
409
+ yield* Effect.tryPromise(() => fs$1.writeFile(typesPath, generateTypes(merged)));
410
+ if (!json) {
411
+ const newCount = entries.filter((e) => !existing.entries.some((ex) => ex.key === e.key)).length;
412
+ yield* Console.log(` ${messages.length} strings extracted (${newCount} new)`);
413
+ }
414
+ let totalLocalized = 0;
415
+ const failedLocales = [];
416
+ if (targetLocales.length > 0) {
417
+ if (!json) yield* Console.log("Localizing...");
418
+ const sourceFile = readLocaleFile(yield* Effect.tryPromise(() => fs$1.readFile(localePath, "utf-8")));
419
+ for (const locale of targetLocales) {
420
+ const targetPath = path.join(outDir, `${locale}.jsonc`);
421
+ const targetContent = yield* Effect.tryPromise(() => tryReadFile(targetPath));
422
+ const targetFile = targetContent ? readLocaleFile(targetContent) : { entries: [] };
423
+ const plan = planLocalization(sourceFile, targetFile, locale);
424
+ if (plan.missing.length === 0) {
425
+ if (!json) yield* Console.log(` ${locale}: up to date`);
426
+ continue;
427
+ }
428
+ const { data, hints } = toApiPayload(plan.missing);
429
+ const result = yield* pipe(api.localize({
430
+ sourceLocale,
431
+ targetLocale: locale,
432
+ data,
433
+ hints,
434
+ engineId: projectConfig?.engineId,
435
+ triggerType: "cli"
436
+ }), Effect.catchAll((e) => Effect.gen(function* () {
437
+ yield* Console.error(` ${locale}: failed - ${e.message}`);
438
+ failedLocales.push(locale);
439
+ return null;
440
+ })));
441
+ if (!result) continue;
442
+ const translated = applyTranslations(plan.missing, result.data);
443
+ const mergedTarget = mergeEntries(targetFile, [...getActiveEntries(targetFile.entries), ...translated]);
444
+ yield* Effect.tryPromise(() => fs$1.writeFile(targetPath, writeLocaleFile(mergedTarget)));
445
+ totalLocalized += plan.missing.length;
446
+ if (!json) yield* Console.log(` ${locale}: ${plan.missing.length} strings localized`);
447
+ }
448
+ }
449
+ const sourceFile = readLocaleFile(yield* Effect.tryPromise(() => fs$1.readFile(localePath, "utf-8")));
450
+ const statuses = [computeSourceStatus(sourceFile, sourceLocale)];
451
+ for (const locale of targetLocales) {
452
+ const content = yield* Effect.tryPromise(() => tryReadFile(path.join(outDir, `${locale}.jsonc`)));
453
+ const targetFile = content ? readLocaleFile(content) : { entries: [] };
454
+ statuses.push(computeLocaleStatus(sourceFile, targetFile, locale));
455
+ }
456
+ if (json) yield* Console.log(JSON.stringify({
457
+ extracted: messages.length,
458
+ localized: totalLocalized,
459
+ ...failedLocales.length > 0 ? { failed: failedLocales } : {},
460
+ statuses
461
+ }, null, 2));
462
+ else {
463
+ yield* Console.log("");
464
+ yield* Console.log(formatStatusTable(statuses));
465
+ if (statuses.reduce((sum, s) => sum + s.missing, 0) === 0 && totalLocalized === 0) yield* Console.log("\nAll locales up to date. Nothing to do.");
466
+ }
467
+ })).pipe(Command.withDescription("Extract, localize, and report status (idempotent pipeline)"));
468
+ //#endregion
469
+ //#region src/commands/status.ts
470
+ const statusCommand = Command.make("status", {
471
+ json: jsonOption,
472
+ sourceLocale: sourceLocaleOption,
473
+ out: outOption
474
+ }, ({ json, sourceLocale, out }) => Effect.gen(function* () {
475
+ const cwd = process.cwd();
476
+ const outDir = path.resolve(cwd, out);
477
+ const sourcePath = path.join(outDir, `${sourceLocale}.jsonc`);
478
+ const sourceContent = yield* Effect.tryPromise(() => tryReadFile(sourcePath));
479
+ if (!sourceContent) {
480
+ yield* Console.error(`No source locale file at ${out}/${sourceLocale}.jsonc. Run \`lingo extract\` first.`);
481
+ return yield* Effect.die(/* @__PURE__ */ new Error(`Missing ${out}/${sourceLocale}.jsonc`));
482
+ }
483
+ const sourceFile = readLocaleFile(sourceContent);
484
+ const targetLocales = yield* Effect.tryPromise(() => discoverLocales(outDir, sourceLocale));
485
+ const statuses = [computeSourceStatus(sourceFile, sourceLocale)];
486
+ for (const locale of targetLocales) {
487
+ const content = yield* Effect.tryPromise(() => tryReadFile(path.join(outDir, `${locale}.jsonc`)));
488
+ const targetFile = content ? readLocaleFile(content) : { entries: [] };
489
+ statuses.push(computeLocaleStatus(sourceFile, targetFile, locale));
490
+ }
491
+ if (json) yield* Console.log(JSON.stringify(statuses, null, 2));
492
+ else yield* Console.log(formatStatusTable(statuses));
493
+ const totalMissing = statuses.reduce((sum, s) => sum + s.missing, 0);
494
+ if (totalMissing > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`${totalMissing} missing translation(s) across ${statuses.length} locale(s)`));
495
+ })).pipe(Command.withDescription("Show per-locale translation completeness"));
496
+ //#endregion
497
+ //#region src/commands/check.ts
498
+ const checkCommand = Command.make("check", {
499
+ json: jsonOption,
500
+ src: srcOption,
501
+ sourceLocale: sourceLocaleOption,
502
+ out: outOption
503
+ }, ({ json, src, sourceLocale, out }) => Effect.gen(function* () {
504
+ const cwd = process.cwd();
505
+ const srcDir = path.resolve(cwd, src);
506
+ const outDir = path.resolve(cwd, out);
507
+ const localePath = path.join(outDir, `${sourceLocale}.jsonc`);
508
+ const typesPath = path.resolve(cwd, "lingo.d.ts");
509
+ const filePaths = yield* Effect.tryPromise(() => findSourceFiles(srcDir));
510
+ const { messages, warnings } = runExtractionPipeline(yield* Effect.tryPromise(() => Promise.all(filePaths.map(async (f) => ({
511
+ code: await fs$1.readFile(f, "utf-8"),
512
+ filePath: path.relative(cwd, f)
513
+ })))));
514
+ const entries = toLocaleEntries(messages);
515
+ const existingJsoncContent = (yield* Effect.tryPromise(() => tryReadFile(localePath))) ?? "";
516
+ const merged = mergeEntries(existingJsoncContent ? readLocaleFile(existingJsoncContent) : { entries: [] }, entries);
517
+ const generatedJsonc = writeLocaleFile(merged);
518
+ const generatedTypes = generateTypes(merged);
519
+ const existingTypes = (yield* Effect.tryPromise(() => tryReadFile(typesPath))) ?? "";
520
+ const targetLocales = yield* Effect.tryPromise(() => discoverLocales(outDir, sourceLocale).catch(() => []));
521
+ const targetFiles = yield* Effect.tryPromise(() => Promise.all(targetLocales.map(async (locale) => {
522
+ const content = await tryReadFile(path.join(outDir, `${locale}.jsonc`));
523
+ return {
524
+ locale,
525
+ file: content ? readLocaleFile(content) : { entries: [] }
526
+ };
527
+ })));
528
+ const result = runChecks({
529
+ warnings,
530
+ messageCount: messages.length,
531
+ generatedJsonc,
532
+ existingJsonc: existingJsoncContent,
533
+ generatedTypes,
534
+ existingTypes,
535
+ sourceFile: merged,
536
+ targetFiles
537
+ });
538
+ if (json) yield* Console.log(JSON.stringify(result, null, 2));
539
+ else yield* Console.log(formatCheckResult(result));
540
+ if (result.issues.length > 0) return yield* Effect.die(/* @__PURE__ */ new Error(`${result.issues.length} issue(s) found`));
541
+ })).pipe(Command.withDescription("Verify i18n consistency: context, translations, types"));
542
+ //#endregion
543
+ //#region src/commands/guide.ts
544
+ const allowedGuides = new Set([
545
+ "index",
546
+ "setup",
547
+ "api",
548
+ "migrate"
549
+ ]);
550
+ const guideArg = pipe(Args.text({ name: "guide" }), Args.withDescription("Guide name"), Args.withDefault("index"));
551
+ async function resolveGuidesDir() {
552
+ const here = path.dirname(new URL(import.meta.url).pathname);
553
+ const candidates = [path.resolve(here, "..", "..", "guides"), path.resolve(here, "..", "guides")];
554
+ for (const candidate of candidates) try {
555
+ if ((await fs$1.stat(candidate)).isDirectory()) return candidate;
556
+ } catch {}
557
+ throw new Error("guides directory not found in package");
558
+ }
559
+ async function readGuide(name) {
560
+ const guidesDir = await resolveGuidesDir();
561
+ const file = path.join(guidesDir, `${name}.md`);
562
+ return fs$1.readFile(file, "utf8");
563
+ }
564
+ const guideCommand = Command.make("guide", { guide: guideArg }, ({ guide }) => Effect.gen(function* () {
565
+ const name = guide.toLowerCase();
566
+ if (!allowedGuides.has(name)) {
567
+ yield* Console.log("Unknown guide. Use one of: index, setup, api, migrate.");
568
+ return;
569
+ }
570
+ const content = yield* Effect.tryPromise(() => readGuide(name));
571
+ yield* Console.log(content);
572
+ })).pipe(Command.withDescription("Print Lingo guides (setup, api, migrate)."));
573
+ //#endregion
574
+ //#region src/commands/install.ts
575
+ /**
576
+ * `lingo install` - One command to set up the @lingo.dev/react DX stack.
577
+ * Detects AI tools, installs packages, copies the /lingo skill.
578
+ * Non-destructive and idempotent.
579
+ */
580
+ async function exists(p) {
581
+ try {
582
+ await fs$1.access(p);
583
+ return true;
584
+ } catch {
585
+ return false;
586
+ }
587
+ }
588
+ /** Walk up from cwd looking for a file/dir. Returns the directory containing it, or null. */
589
+ async function findUp(name, from) {
590
+ let dir = from;
591
+ while (true) {
592
+ if (await exists(path.join(dir, name))) return dir;
593
+ const parent = path.dirname(dir);
594
+ if (parent === dir) return null;
595
+ dir = parent;
596
+ }
597
+ }
598
+ async function detectProject(cwd) {
599
+ const lockfiles = [
600
+ ["pnpm-lock.yaml", "pnpm"],
601
+ ["bun.lockb", "bun"],
602
+ ["bun.lock", "bun"],
603
+ ["yarn.lock", "yarn"],
604
+ ["package-lock.json", "npm"]
605
+ ];
606
+ let pm = "npm";
607
+ for (const [file, manager] of lockfiles) if (await findUp(file, cwd)) {
608
+ pm = manager;
609
+ break;
610
+ }
611
+ let framework = "unknown";
612
+ const pkgPath = path.join(cwd, "package.json");
613
+ if (await exists(pkgPath)) {
614
+ const pkg = JSON.parse(await fs$1.readFile(pkgPath, "utf8"));
615
+ const deps = {
616
+ ...pkg.dependencies,
617
+ ...pkg.devDependencies
618
+ };
619
+ if (deps.next) {
620
+ const hasPages = await exists(path.join(cwd, "pages")) || await exists(path.join(cwd, "src/pages"));
621
+ const hasApp = await exists(path.join(cwd, "app")) || await exists(path.join(cwd, "src/app"));
622
+ framework = hasPages ? "next-pages" : hasApp ? "next-app" : "next-pages";
623
+ } else if (deps.vite) framework = "vite";
624
+ else if (deps.react) framework = "react";
625
+ }
626
+ const aiToolDirs = [
627
+ [".claude", "claude-code"],
628
+ [".cursor", "cursor"],
629
+ [".windsurf", "windsurf"],
630
+ [".vscode", "vscode"]
631
+ ];
632
+ const aiTools = [];
633
+ for (const [dir, tool] of aiToolDirs) if (await findUp(dir, cwd)) aiTools.push(tool);
634
+ return {
635
+ framework,
636
+ pm,
637
+ aiTools
638
+ };
639
+ }
640
+ function installCmd(pm, packages, dev) {
641
+ return [
642
+ pm,
643
+ pm === "yarn" ? "add" : pm === "npm" ? "install" : "add",
644
+ dev ? pm === "npm" ? "--save-dev" : "-D" : "",
645
+ ...packages
646
+ ].filter(Boolean).join(" ");
647
+ }
648
+ async function installPackages(cwd, pm, framework) {
649
+ const pkgPath = path.join(cwd, "package.json");
650
+ const pkg = JSON.parse(await fs$1.readFile(pkgPath, "utf8"));
651
+ const allDeps = {
652
+ ...pkg.dependencies,
653
+ ...pkg.devDependencies
654
+ };
655
+ const toInstall = [];
656
+ if (!allDeps["@lingo.dev/react"]) toInstall.push("@lingo.dev/react");
657
+ if (framework.startsWith("next") && !allDeps["@lingo.dev/react-next"]) toInstall.push("@lingo.dev/react-next");
658
+ const toInstallDev = [];
659
+ if (!allDeps["@lingo.dev/cli"]) toInstallDev.push("@lingo.dev/cli");
660
+ const commands = [];
661
+ if (toInstall.length > 0) commands.push(installCmd(pm, toInstall, false));
662
+ if (toInstallDev.length > 0) commands.push(installCmd(pm, toInstallDev, true));
663
+ return commands;
664
+ }
665
+ const AI_TOOL_DIR = {
666
+ "claude-code": ".claude",
667
+ cursor: ".cursor",
668
+ windsurf: ".windsurf",
669
+ vscode: ".vscode"
670
+ };
671
+ const SKILL_NAMES = ["lingo"];
672
+ async function copySkills(cwd, aiTools, skillsSourceDir) {
673
+ const copied = [];
674
+ for (const tool of aiTools) {
675
+ const root = await findUp(AI_TOOL_DIR[tool], cwd);
676
+ if (!root) continue;
677
+ const targetDir = path.join(root, AI_TOOL_DIR[tool], "skills");
678
+ for (const name of SKILL_NAMES) {
679
+ const src = path.join(skillsSourceDir, name, "SKILL.md");
680
+ const dest = path.join(targetDir, name, "SKILL.md");
681
+ if (await exists(dest)) continue;
682
+ await fs$1.mkdir(path.dirname(dest), { recursive: true });
683
+ await fs$1.copyFile(src, dest);
684
+ copied.push(`${tool}: ${name}`);
685
+ }
686
+ }
687
+ return copied;
688
+ }
689
+ const installCommand = Command.make("install", {}, () => Effect.gen(function* () {
690
+ const cwd = process.cwd();
691
+ const project = yield* Effect.tryPromise(() => detectProject(cwd));
692
+ yield* Console.log(` Detected: ${{
693
+ "next-pages": "Next.js Pages Router",
694
+ "next-app": "Next.js App Router",
695
+ vite: "Vite + React",
696
+ react: "React",
697
+ unknown: "React (unknown framework)"
698
+ }[project.framework]}`);
699
+ yield* Console.log(` Detected: ${project.pm} (package manager)`);
700
+ if (project.aiTools.length > 0) yield* Console.log(` Detected: ${project.aiTools.join(", ")} (AI tools)`);
701
+ yield* Console.log("");
702
+ const commands = yield* Effect.tryPromise(() => installPackages(cwd, project.pm, project.framework));
703
+ if (commands.length > 0) {
704
+ for (const cmd of commands) yield* Console.log(` Run: ${cmd}`);
705
+ yield* Console.log("");
706
+ } else yield* Console.log(" Packages already installed.");
707
+ if (project.aiTools.length > 0) {
708
+ const thisDir = path.dirname(new URL(import.meta.url).pathname);
709
+ const candidates = [path.resolve(thisDir, "..", "..", "skills"), path.resolve(thisDir, "..", "skills")];
710
+ const skillsDir = yield* Effect.tryPromise(async () => {
711
+ for (const c of candidates) if (await exists(c)) return c;
712
+ return "";
713
+ });
714
+ if (yield* Effect.tryPromise(() => exists(skillsDir))) if ((yield* Effect.tryPromise(() => copySkills(cwd, project.aiTools, skillsDir))).length > 0) yield* Console.log(` Installed /lingo skill for: ${project.aiTools.join(", ")}`);
715
+ else yield* Console.log(" Skills already installed.");
716
+ else yield* Console.log(" Skills directory not found (running from source). Skipping skill copy.");
717
+ }
718
+ yield* Console.log("");
719
+ yield* Console.log(" Run /lingo in your AI assistant, or \"npx @lingo.dev/cli guide\" for instructions.");
720
+ })).pipe(Command.withDescription("Set up @lingo.dev/react DX stack: packages, skills"));
721
+ //#endregion
722
+ //#region src/constants.ts
723
+ const TAGLINE = "Lingo.dev — The Localization Engineering Platform";
724
+ //#endregion
725
+ //#region src/services/banner.ts
726
+ const SCRAMBLE_GLYPHS = "░▒▓█▄▀╔╗╚╝║═╬╦╩╠╣┃━";
727
+ const FRAME_MS = 20;
728
+ const REVEAL_MS = 1200;
729
+ const SETTLE_MS = 300;
730
+ const COLUMN_JITTER_MS = 120;
731
+ const GLITCH_ZONE_MS = 80;
732
+ const ANSI_RE = /\x1B\[[0-9;]*m/g;
733
+ const stripAnsi$1 = (s) => s.replace(ANSI_RE, "");
734
+ function randomGlyph() {
735
+ return SCRAMBLE_GLYPHS[Math.floor(Math.random() * 19)];
736
+ }
737
+ function shouldSkip() {
738
+ return !!(process.env.NO_COLOR || process.env.CI || !process.stdout.isTTY);
739
+ }
740
+ function renderArt() {
741
+ const result = cfonts.render("Lingo.dev", {
742
+ font: "block",
743
+ space: false
744
+ });
745
+ if (!result) return [];
746
+ return stripAnsi$1(result.string).split("\n").filter((line) => line.trim().length > 0);
747
+ }
748
+ function computeRevealTimes(width) {
749
+ return Array.from({ length: width }, (_, col) => {
750
+ const base = col / width * REVEAL_MS;
751
+ const jitter = (Math.random() - .5) * COLUMN_JITTER_MS;
752
+ return Math.max(0, base + jitter);
753
+ });
754
+ }
755
+ function buildFrame(lines, elapsed, revealTimes) {
756
+ return lines.map((line) => Array.from(line).map((char, col) => {
757
+ if (char === " ") return " ";
758
+ const revealAt = revealTimes[col] ?? 0;
759
+ if (elapsed >= revealAt) return char;
760
+ if (revealAt - elapsed < GLITCH_ZONE_MS && Math.random() > .5) return char;
761
+ return randomGlyph();
762
+ }).join("")).join("\n");
763
+ }
764
+ const CLEAR_SCREEN = "\x1B[2J\x1B[H";
765
+ function animateBanner(signal) {
766
+ if (shouldSkip()) return Promise.resolve();
767
+ const lines = renderArt();
768
+ if (lines.length === 0) return Promise.resolve();
769
+ const revealTimes = computeRevealTimes(Math.max(...lines.map((l) => l.length)));
770
+ const totalMs = REVEAL_MS + SETTLE_MS;
771
+ process.stdout.write(CLEAR_SCREEN);
772
+ return new Promise((resolve) => {
773
+ const start = Date.now();
774
+ const tick = setInterval(() => {
775
+ if (signal?.aborted) {
776
+ clearInterval(tick);
777
+ logUpdate.clear();
778
+ resolve();
779
+ return;
780
+ }
781
+ const elapsed = Date.now() - start;
782
+ logUpdate("\n" + buildFrame(lines, elapsed, revealTimes));
783
+ if (elapsed >= totalMs) {
784
+ clearInterval(tick);
785
+ logUpdate("\n" + lines.join("\n"));
786
+ logUpdate.done();
787
+ resolve();
788
+ }
789
+ }, FRAME_MS);
790
+ });
791
+ }
792
+ //#endregion
793
+ //#region src/services/prompt.ts
794
+ var PromptCancelledError = class extends Data.TaggedError("PromptCancelledError") {};
795
+ function unwrapCancel(value) {
796
+ if (clack.isCancel(value)) return Effect.fail(new PromptCancelledError({ message: "Cancelled." }));
797
+ return Effect.succeed(value);
798
+ }
799
+ const text = (opts) => Effect.flatMap(Effect.tryPromise(() => clack.text(opts)), unwrapCancel);
800
+ const select = (opts) => Effect.flatMap(Effect.tryPromise(() => clack.select(opts)), unwrapCancel);
801
+ const multiselect = (opts) => Effect.flatMap(Effect.tryPromise(() => clack.multiselect(opts)), unwrapCancel);
802
+ const spinner = () => clack.spinner();
803
+ const cancel = (message) => Effect.sync(() => clack.cancel(message));
804
+ const log = {
805
+ info: (message) => Effect.sync(() => clack.log.info(message)),
806
+ success: (message) => Effect.sync(() => clack.log.success(message)),
807
+ warn: (message) => Effect.sync(() => clack.log.warn(message)),
808
+ error: (message) => Effect.sync(() => clack.log.error(message)),
809
+ step: (message) => Effect.sync(() => clack.log.step(message)),
810
+ message: (message) => Effect.sync(() => clack.log.message(message))
811
+ };
812
+ //#endregion
813
+ //#region src/services/telemetry.ts
814
+ var TelemetryService = class extends Context.Tag("TelemetryService")() {};
815
+ const noop = {
816
+ capture: () => Effect.void,
817
+ identify: () => Effect.void,
818
+ shutdown: Effect.void
819
+ };
820
+ function resolveRepoId() {
821
+ return process.env.GITHUB_REPOSITORY_ID ?? process.env.CI_PROJECT_ID ?? process.env.BITBUCKET_REPO_UUID ?? hashOf(process.env.GITHUB_REPOSITORY ?? process.env.CI_PROJECT_PATH ?? process.env.CIRCLE_PROJECT_REPONAME ?? process.cwd());
822
+ }
823
+ function detectCI() {
824
+ if (!ci.isCI) return {
825
+ isCI: false,
826
+ provider: null,
827
+ isPR: null,
828
+ repoId: null
829
+ };
830
+ return {
831
+ isCI: true,
832
+ provider: ci.id,
833
+ isPR: ci.isPR,
834
+ repoId: resolveRepoId()
835
+ };
836
+ }
837
+ function getAnonymousId() {
838
+ const dir = path.join(os.homedir(), ".lingo");
839
+ const file = path.join(dir, "anonymous-id");
840
+ try {
841
+ return fs.readFileSync(file, "utf-8").trim();
842
+ } catch {
843
+ const id = crypto.randomUUID();
844
+ try {
845
+ fs.mkdirSync(dir, { recursive: true });
846
+ fs.writeFileSync(file, id);
847
+ } catch {}
848
+ return id;
849
+ }
850
+ }
851
+ function resolveDistinctId(ci, orgId, engineId) {
852
+ if (!ci.isCI) return getAnonymousId();
853
+ return `ci:${[
854
+ ci.provider ?? "ci",
855
+ ci.repoId,
856
+ orgId,
857
+ engineId
858
+ ].filter(Boolean).join(":")}`;
859
+ }
860
+ function isDisabled() {
861
+ if (process.env.LINGO_TELEMETRY_DISABLED === "1") return true;
862
+ if (process.env.DO_NOT_TRACK === "1") return true;
863
+ return false;
864
+ }
865
+ function hashOf(input) {
866
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 12);
867
+ }
868
+ const POSTHOG_KEY = "phc_eR0iSoQufBxNY36k0f0T15UvHJdTfHlh8rJcxsfhfXk";
869
+ const POSTHOG_HOST = "https://eu.i.posthog.com";
870
+ const FLUSH_SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), "flush-telemetry.js");
871
+ const TelemetryServiceLive = Layer.effect(TelemetryService, Effect.gen(function* () {
872
+ if (isDisabled()) return noop;
873
+ const projectConfig = yield* pipe((yield* ConfigService).resolve, Effect.catchAll(() => Effect.succeed(null)));
874
+ const ciCtx = detectCI();
875
+ const distinctId = resolveDistinctId(ciCtx, projectConfig?.orgId ?? null, projectConfig?.engineId ?? null);
876
+ const creds = yield* pipe((yield* AuthService).resolve, Effect.catchAll(() => Effect.succeed(null)));
877
+ let identifyTraits;
878
+ if (creds?.type === "session") identifyTraits = { email: creds.email };
879
+ else if (creds?.type === "api-key") {
880
+ const me = yield* pipe((yield* ApiClient).getMe, Effect.catchAll(() => Effect.succeed(null)));
881
+ if (me) identifyTraits = {
882
+ email: me.email,
883
+ name: me.name,
884
+ user_id: me.id
885
+ };
886
+ }
887
+ const globalProperties = {
888
+ $lib: "@lingo.dev/cli",
889
+ cli_version: VERSION,
890
+ os: process.platform,
891
+ arch: process.arch,
892
+ node_version: process.version,
893
+ is_ci: ciCtx.isCI,
894
+ ci_provider: ciCtx.provider,
895
+ ci_pr: ciCtx.isPR,
896
+ auth_method: creds?.type ?? null,
897
+ has_project_config: !!projectConfig,
898
+ org_id: projectConfig?.orgId ?? null,
899
+ engine_id: projectConfig?.engineId ?? null,
900
+ is_tty: process.stdout.isTTY ?? false,
901
+ shell: process.env.SHELL ?? null
902
+ };
903
+ const buffer = [];
904
+ let flushed = false;
905
+ return {
906
+ capture: (event, properties) => Effect.sync(() => {
907
+ if (flushed) return;
908
+ buffer.push({
909
+ event,
910
+ properties: {
911
+ ...globalProperties,
912
+ ...properties
913
+ },
914
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
915
+ });
916
+ }),
917
+ identify: (traits) => Effect.sync(() => {
918
+ if (traits) identifyTraits = {
919
+ ...identifyTraits,
920
+ ...traits
921
+ };
922
+ }),
923
+ shutdown: Effect.sync(() => {
924
+ if (flushed) return;
925
+ flushed = true;
926
+ if (buffer.length === 0 && !identifyTraits) return;
927
+ const payload = {
928
+ apiKey: POSTHOG_KEY,
929
+ host: POSTHOG_HOST,
930
+ distinctId,
931
+ events: buffer,
932
+ identifyTraits
933
+ };
934
+ const file = path.join(os.tmpdir(), `lingo-telemetry-${process.pid}-${Date.now()}.json`);
935
+ try {
936
+ fs.writeFileSync(file, JSON.stringify(payload));
937
+ } catch {
938
+ return;
939
+ }
940
+ try {
941
+ spawn(process.execPath, [FLUSH_SCRIPT, file], {
942
+ detached: true,
943
+ stdio: "ignore",
944
+ windowsHide: true
945
+ }).unref();
946
+ } catch {
947
+ try {
948
+ fs.unlinkSync(file);
949
+ } catch {}
950
+ }
951
+ })
952
+ };
953
+ }).pipe(Effect.catchAll(() => Effect.succeed(noop))));
954
+ function trackCommand(commandName, flags) {
955
+ return (effect) => {
956
+ return Effect.gen(function* () {
957
+ const telemetry = yield* TelemetryService;
958
+ const start = Date.now();
959
+ yield* telemetry.capture("cli_command_started", {
960
+ command: commandName,
961
+ flags
962
+ });
963
+ const result = yield* pipe(effect, Effect.tapError((error) => {
964
+ const tag = "_tag" in error ? error._tag : "UnknownError";
965
+ const cancelled = tag === "PromptCancelledError";
966
+ return telemetry.capture("cli_command_completed", {
967
+ command: commandName,
968
+ success: false,
969
+ cancelled,
970
+ duration_ms: Date.now() - start,
971
+ ...cancelled ? {} : { error_type: tag }
972
+ });
973
+ }));
974
+ yield* telemetry.capture("cli_command_completed", {
975
+ command: commandName,
976
+ success: true,
977
+ duration_ms: Date.now() - start,
978
+ error_type: null
979
+ });
980
+ return result;
981
+ });
982
+ };
983
+ }
984
+ //#endregion
985
+ //#region src/commands/login.ts
986
+ const email = pipe(Options.text("email"), Options.withDescription("Email address (skips prompt)"), Options.optional);
987
+ const code = pipe(Options.text("code"), Options.withDescription("OTP code (skips prompt, requires --email)"), Options.optional);
988
+ const apiKey = pipe(Options.text("api-key"), Options.withDescription("Authenticate with an API key instead of email"), Options.optional);
989
+ const loginCommand = Command.make("login", {
990
+ email,
991
+ code,
992
+ apiKey
993
+ }, ({ email: emailOpt, code: codeOpt, apiKey: apiKeyOpt }) => Effect.gen(function* () {
994
+ const auth = yield* AuthService;
995
+ const apiKeyValue = Option.getOrUndefined(apiKeyOpt);
996
+ const emailFlag = Option.getOrUndefined(emailOpt);
997
+ const codeFlag = Option.getOrUndefined(codeOpt);
998
+ const telemetry = yield* TelemetryService;
999
+ if (apiKeyValue) {
1000
+ yield* auth.store({
1001
+ type: "api-key",
1002
+ apiKey: apiKeyValue
1003
+ });
1004
+ yield* telemetry.capture("cli_login", { method: "api-key" });
1005
+ yield* log.success("Authenticated with API key.");
1006
+ return;
1007
+ }
1008
+ yield* Effect.async((resume, signal) => {
1009
+ animateBanner(signal).then(() => resume(Effect.void));
1010
+ });
1011
+ yield* log.step(pc.dim(TAGLINE));
1012
+ const emailAddress = emailFlag ?? (yield* text({
1013
+ message: "Email",
1014
+ validate: (v) => !v.includes("@") ? "Enter a valid email address." : void 0
1015
+ }));
1016
+ const s = spinner();
1017
+ s.start("Sending code...");
1018
+ yield* sendOtp(emailAddress);
1019
+ s.stop(`Code sent to ${pc.green(emailAddress)}. Check your inbox.`);
1020
+ const otpCode = codeFlag ?? (yield* text({
1021
+ message: "Verification code",
1022
+ placeholder: "6-digit code from email"
1023
+ }));
1024
+ s.start("Verifying...");
1025
+ const session = yield* verifyOtp(emailAddress, otpCode);
1026
+ s.stop(`Authenticated as ${pc.green(emailAddress)}.`);
1027
+ const creds = {
1028
+ type: "session",
1029
+ email: session.user.email ?? emailAddress,
1030
+ accessToken: session.access_token,
1031
+ refreshToken: session.refresh_token,
1032
+ expiresAt: session.expires_at ?? Math.floor(Date.now() / 1e3) + session.expires_in
1033
+ };
1034
+ yield* auth.store(creds);
1035
+ yield* telemetry.capture("cli_login", { method: "otp" });
1036
+ yield* telemetry.identify({ email: creds.email });
1037
+ })).pipe(Command.withDescription("Authenticate with Lingo.dev"));
1038
+ //#endregion
1039
+ //#region src/commands/logout.ts
1040
+ const logoutCommand = Command.make("logout", {}, () => Effect.gen(function* () {
1041
+ const auth = yield* AuthService;
1042
+ const telemetry = yield* TelemetryService;
1043
+ yield* auth.clear;
1044
+ yield* telemetry.capture("cli_logout");
1045
+ yield* log.success("Logged out.");
1046
+ })).pipe(Command.withDescription("Clear stored credentials"));
1047
+ //#endregion
1048
+ //#region src/commands/whoami.ts
1049
+ const json = Options.boolean("json").pipe(Options.withDescription("Output as JSON"));
1050
+ const whoamiCommand = Command.make("whoami", { json }, ({ json }) => Effect.gen(function* () {
1051
+ const auth = yield* AuthService;
1052
+ const api = yield* ApiClient;
1053
+ const config = yield* ConfigService;
1054
+ const creds = yield* auth.resolve;
1055
+ const projectConfig = yield* config.resolve;
1056
+ const info = {};
1057
+ if (creds.type === "session") {
1058
+ const me = yield* pipe(api.getMe, Effect.catchAll(() => Effect.succeed(null)));
1059
+ info.email = me?.email ?? creds.email;
1060
+ if (me?.name) info.name = me.name;
1061
+ info.auth = "session";
1062
+ } else info.auth = "api-key";
1063
+ if (projectConfig) {
1064
+ info.orgId = projectConfig.orgId;
1065
+ info.engineId = projectConfig.engineId;
1066
+ if (creds.type === "session") {
1067
+ const org = (yield* pipe(api.listMemberships, Effect.catchAll(() => Effect.succeed([])))).find((m) => m.organizationId === projectConfig.orgId);
1068
+ if (org) info.org = org.organizationName;
1069
+ }
1070
+ if (info.orgId) {
1071
+ const eng = (yield* pipe(api.listEngines(info.orgId), Effect.catchAll(() => Effect.succeed([])))).find((e) => e.id === projectConfig.engineId);
1072
+ if (eng) info.engine = eng.name;
1073
+ }
1074
+ }
1075
+ if (json) yield* Console.log(JSON.stringify(info));
1076
+ else {
1077
+ const lines = [];
1078
+ if (info.email) lines.push(` ${pc.bold("Email:")} ${pc.green(info.email)}`);
1079
+ if (info.org) lines.push(` ${pc.bold("Org:")} ${info.org} ${pc.dim(`(${info.orgId})`)}`);
1080
+ else if (info.orgId) lines.push(` ${pc.bold("Org:")} ${pc.dim(info.orgId)}`);
1081
+ if (info.engine) lines.push(` ${pc.bold("Engine:")} ${info.engine} ${pc.dim(`(${info.engineId})`)}`);
1082
+ else if (info.engineId) lines.push(` ${pc.bold("Engine:")} ${pc.dim(info.engineId)}`);
1083
+ lines.push(` ${pc.bold("Auth:")} ${info.auth}`);
1084
+ yield* Console.log(lines.join("\n"));
1085
+ }
1086
+ })).pipe(Command.withDescription("Show current identity and context"));
1087
+ //#endregion
1088
+ //#region src/commands/link.ts
1089
+ const CREATE_NEW = "__create_new__";
1090
+ const org = pipe(Options.text("org"), Options.withDescription("Organization ID (skips prompt)"), Options.optional);
1091
+ const engine = pipe(Options.text("engine"), Options.withDescription("Engine ID (skips prompt)"), Options.optional);
1092
+ const linkCommand = Command.make("link", {
1093
+ org,
1094
+ engine
1095
+ }, ({ org: orgOpt, engine: engineOpt }) => Effect.gen(function* () {
1096
+ yield* (yield* AuthService).resolve;
1097
+ const api = yield* ApiClient;
1098
+ const config = yield* ConfigService;
1099
+ const telemetry = yield* TelemetryService;
1100
+ const orgFlag = Option.getOrUndefined(orgOpt);
1101
+ const engineFlag = Option.getOrUndefined(engineOpt);
1102
+ let orgId;
1103
+ let orgName;
1104
+ let createdOrg = false;
1105
+ let createdEngine = false;
1106
+ if (orgFlag) {
1107
+ orgId = orgFlag;
1108
+ orgName = orgFlag;
1109
+ } else {
1110
+ const memberships = yield* api.listMemberships;
1111
+ const selected = yield* select({
1112
+ message: "Organization",
1113
+ options: [...memberships.map((m) => ({
1114
+ value: m.organizationId,
1115
+ label: m.organizationName,
1116
+ hint: pc.dim(m.organizationId)
1117
+ })), {
1118
+ value: CREATE_NEW,
1119
+ label: pc.dim("Create new organization")
1120
+ }]
1121
+ });
1122
+ if (selected === CREATE_NEW) {
1123
+ const result = yield* createOrganization(api);
1124
+ orgId = result.orgId;
1125
+ orgName = result.orgName;
1126
+ createdOrg = true;
1127
+ } else {
1128
+ orgId = selected;
1129
+ orgName = memberships.find((m) => m.organizationId === orgId)?.organizationName ?? orgId;
1130
+ }
1131
+ }
1132
+ let engineId;
1133
+ let engineName;
1134
+ if (engineFlag) {
1135
+ engineId = engineFlag;
1136
+ engineName = engineFlag;
1137
+ } else {
1138
+ const engines = yield* api.listEngines(orgId);
1139
+ const selected = yield* select({
1140
+ message: "Localization engine",
1141
+ options: [...engines.map((e) => ({
1142
+ value: e.id,
1143
+ label: e.name,
1144
+ hint: e.description ? pc.dim(e.description) : void 0
1145
+ })), {
1146
+ value: CREATE_NEW,
1147
+ label: pc.dim("Create new engine")
1148
+ }]
1149
+ });
1150
+ if (selected === CREATE_NEW) {
1151
+ const result = yield* createEngine(api, orgId);
1152
+ engineId = result.engineId;
1153
+ engineName = result.engineName;
1154
+ createdEngine = true;
1155
+ } else {
1156
+ engineId = selected;
1157
+ engineName = engines.find((e) => e.id === engineId)?.name ?? engineId;
1158
+ }
1159
+ }
1160
+ yield* config.store({
1161
+ orgId,
1162
+ engineId
1163
+ });
1164
+ yield* telemetry.capture("cli_project_linked", {
1165
+ created_org: createdOrg,
1166
+ created_engine: createdEngine
1167
+ });
1168
+ yield* log.success(`Linked to ${pc.green(orgName)} / ${pc.green(engineName)}.`);
1169
+ yield* log.message(pc.dim("Config saved to .lingo/config.json. Commit this file to your repository."));
1170
+ })).pipe(Command.withDescription("Connect this directory to an org and engine"));
1171
+ function createOrganization(api) {
1172
+ return Effect.gen(function* () {
1173
+ const name = yield* text({
1174
+ message: "Organization name",
1175
+ placeholder: "Acme Inc"
1176
+ });
1177
+ const usecases = yield* multiselect({
1178
+ message: "What are you planning to use Lingo.dev for? (space to select, enter to confirm)",
1179
+ required: true,
1180
+ options: [
1181
+ {
1182
+ value: "web-app",
1183
+ label: "Web app localization"
1184
+ },
1185
+ {
1186
+ value: "mobile-app",
1187
+ label: "Mobile app localization"
1188
+ },
1189
+ {
1190
+ value: "translation-api",
1191
+ label: "Translation API"
1192
+ },
1193
+ {
1194
+ value: "ci-cd",
1195
+ label: "CI/CD localization workflows"
1196
+ },
1197
+ {
1198
+ value: "static-content",
1199
+ label: "Static content localization (e.g. Markdown, JSON)"
1200
+ },
1201
+ {
1202
+ value: "cms",
1203
+ label: "CMS content localization"
1204
+ },
1205
+ {
1206
+ value: "emails",
1207
+ label: "Emails localization"
1208
+ }
1209
+ ]
1210
+ });
1211
+ const attribution = yield* text({
1212
+ message: "How did you hear about us?",
1213
+ placeholder: "e.g. Twitter, Reddit, coworker...",
1214
+ validate: (v) => !v.trim() ? "This helps us build a better product." : void 0
1215
+ });
1216
+ yield* pipe(api.updateMe({
1217
+ usecases,
1218
+ selfAttribution: attribution.trim()
1219
+ }), Effect.catchAll(() => Effect.void));
1220
+ const s = spinner();
1221
+ s.start("Creating organization...");
1222
+ const created = yield* api.createOrganization(name);
1223
+ s.stop(`Created organization ${pc.green(created.name)}.`);
1224
+ return {
1225
+ orgId: created.id,
1226
+ orgName: created.name
1227
+ };
1228
+ });
1229
+ }
1230
+ function createEngine(api, orgId) {
1231
+ return Effect.gen(function* () {
1232
+ yield* log.message(pc.dim("A localization engine is a stateful translation API you configure for your project."));
1233
+ const name = yield* text({
1234
+ message: "Engine name",
1235
+ placeholder: "default"
1236
+ });
1237
+ const s = spinner();
1238
+ s.start("Creating localization engine...");
1239
+ const created = yield* api.createEngine(orgId, name);
1240
+ s.stop(`Created localization engine ${pc.green(created.name)}.`);
1241
+ return {
1242
+ engineId: created.id,
1243
+ engineName: created.name
1244
+ };
1245
+ });
1246
+ }
1247
+ //#endregion
1248
+ //#region src/commands/unlink.ts
1249
+ const unlinkCommand = Command.make("unlink", {}, () => Effect.gen(function* () {
1250
+ const config = yield* ConfigService;
1251
+ const telemetry = yield* TelemetryService;
1252
+ yield* config.clear;
1253
+ yield* telemetry.capture("cli_project_unlinked");
1254
+ yield* log.success("Unlinked.");
1255
+ })).pipe(Command.withDescription("Remove project config from this directory"));
1256
+ //#endregion
1257
+ //#region src/commands/update.ts
1258
+ const updateCommand = Command.make("update", {}, () => Effect.gen(function* () {
1259
+ const update = yield* UpdateService;
1260
+ const telemetry = yield* TelemetryService;
1261
+ yield* log.step(`Current version: ${pc.dim(`v${VERSION}`)}`);
1262
+ const info = yield* update.check;
1263
+ if (!info) {
1264
+ yield* log.success("Already up to date.");
1265
+ return;
1266
+ }
1267
+ yield* log.info(`Update available: ${pc.dim(info.current)} → ${pc.green(info.latest)}`);
1268
+ if (info.isMajor) yield* log.warn("This is a major version update. Review the changelog before upgrading.");
1269
+ yield* telemetry.capture("cli_update_started", {
1270
+ from_version: info.current,
1271
+ to_version: info.latest,
1272
+ install_method: update.installMethod.type
1273
+ });
1274
+ const s = spinner();
1275
+ s.start("Updating...");
1276
+ const result = yield* Effect.either(update.execute());
1277
+ if (result._tag === "Left") {
1278
+ s.stop("Update failed.");
1279
+ const error = result.left;
1280
+ if (error instanceof UpdateError) yield* log.info(error.message);
1281
+ yield* telemetry.capture("cli_update_completed", {
1282
+ from_version: info.current,
1283
+ to_version: info.latest,
1284
+ success: false
1285
+ });
1286
+ return;
1287
+ }
1288
+ s.stop("Updated successfully.");
1289
+ yield* telemetry.capture("cli_update_completed", {
1290
+ from_version: info.current,
1291
+ to_version: info.latest,
1292
+ success: true
1293
+ });
1294
+ })).pipe(Command.withDescription("Update the CLI to the latest version"));
1295
+ //#endregion
1296
+ //#region src/cli.ts
1297
+ const subcommands = [
1298
+ extractCommand,
1299
+ localizeCommand,
1300
+ shipCommand,
1301
+ statusCommand,
1302
+ checkCommand,
1303
+ guideCommand,
1304
+ installCommand,
1305
+ loginCommand,
1306
+ logoutCommand,
1307
+ whoamiCommand,
1308
+ linkCommand,
1309
+ unlinkCommand,
1310
+ updateCommand
1311
+ ];
1312
+ const command = Command.make("lingo").pipe(Command.withDescription(TAGLINE), Command.withSubcommands([
1313
+ extractCommand,
1314
+ localizeCommand,
1315
+ shipCommand,
1316
+ statusCommand,
1317
+ checkCommand,
1318
+ guideCommand,
1319
+ installCommand,
1320
+ loginCommand,
1321
+ logoutCommand,
1322
+ whoamiCommand,
1323
+ linkCommand,
1324
+ unlinkCommand,
1325
+ updateCommand
1326
+ ]));
1327
+ const cliConfig = CliConfig.make({
1328
+ showBuiltIns: false,
1329
+ showTypes: false
1330
+ });
1331
+ const helpConfig = {
1332
+ name: "lingo",
1333
+ version: VERSION
1334
+ };
1335
+ const run = Command.run(command, {
1336
+ name: helpConfig.name,
1337
+ version: helpConfig.version
1338
+ });
1339
+ //#endregion
1340
+ //#region src/help/descriptor.ts
1341
+ function unwrapCommand(desc) {
1342
+ if (desc._tag === "Map") return unwrapCommand(desc.command);
1343
+ return desc;
1344
+ }
1345
+ function extractCommandInfo(desc) {
1346
+ const cmd = unwrapCommand(desc);
1347
+ return {
1348
+ name: cmd.name,
1349
+ description: extractDescription(cmd.description)
1350
+ };
1351
+ }
1352
+ function collectArgs(node) {
1353
+ if (!node) return [];
1354
+ switch (node._tag) {
1355
+ case "Empty": return [];
1356
+ case "Single": return [{
1357
+ name: extractPseudoName(node) || "arg",
1358
+ description: extractDescription(node.description),
1359
+ required: true
1360
+ }];
1361
+ case "Map": return collectArgs(node.args);
1362
+ case "WithDefault": {
1363
+ const inner = collectArgs(node.args);
1364
+ const fallback = formatFallback(node.fallback);
1365
+ return inner.map((a) => ({
1366
+ ...a,
1367
+ required: false,
1368
+ defaultValue: fallback
1369
+ }));
1370
+ }
1371
+ case "Both": return [...collectArgs(node.left), ...collectArgs(node.right)];
1372
+ case "Variadic": return collectArgs(node.args).map((a) => ({
1373
+ ...a,
1374
+ required: false
1375
+ }));
1376
+ default: return [];
1377
+ }
1378
+ }
1379
+ function collectOptions(node) {
1380
+ if (!node) return [];
1381
+ switch (node._tag) {
1382
+ case "Empty": return [];
1383
+ case "Single": return [singleToOptionInfo(node, true)];
1384
+ case "Map": return collectOptions(node.options);
1385
+ case "WithDefault": {
1386
+ const inner = collectOptions(node.options);
1387
+ const fallback = formatFallback(node.fallback);
1388
+ return inner.map((o) => ({
1389
+ ...o,
1390
+ required: false,
1391
+ defaultValue: o.isBool ? void 0 : fallback
1392
+ }));
1393
+ }
1394
+ case "Both": return [...collectOptions(node.left), ...collectOptions(node.right)];
1395
+ case "Variadic": return collectOptions(node.argumentOption).map((o) => ({
1396
+ ...o,
1397
+ required: false,
1398
+ repeatable: true
1399
+ }));
1400
+ default: return [];
1401
+ }
1402
+ }
1403
+ function singleToOptionInfo(node, required) {
1404
+ const prim = node.primitiveType;
1405
+ const isBool = prim?._tag === "Bool";
1406
+ return {
1407
+ long: node.name,
1408
+ short: node.aliases?.[0],
1409
+ type: isBool ? "" : primTypeName(prim),
1410
+ description: extractDescription(node.description),
1411
+ required,
1412
+ repeatable: false,
1413
+ isBool
1414
+ };
1415
+ }
1416
+ function primTypeName(prim) {
1417
+ if (!prim) return "value";
1418
+ switch (prim._tag) {
1419
+ case "Text": return "str";
1420
+ case "Integer": return "int";
1421
+ case "Float": return "num";
1422
+ case "Date": return "date";
1423
+ case "Bool": return "";
1424
+ case "Choice": return prim.alternatives.map((a) => a[0]).join(" | ");
1425
+ default: return "value";
1426
+ }
1427
+ }
1428
+ function extractDescription(helpDoc) {
1429
+ if (!helpDoc) return "";
1430
+ return extractHelpDocText(helpDoc).trim();
1431
+ }
1432
+ function extractHelpDocText(doc) {
1433
+ if (!doc) return "";
1434
+ switch (doc._tag) {
1435
+ case "Empty": return "";
1436
+ case "Paragraph": return extractSpanText(doc.value);
1437
+ case "Sequence": return [extractHelpDocText(doc.left), extractHelpDocText(doc.right)].filter(Boolean).join(" ");
1438
+ case "Header": return extractSpanText(doc.value);
1439
+ default: return "";
1440
+ }
1441
+ }
1442
+ function extractSpanText(span) {
1443
+ if (!span) return "";
1444
+ switch (span._tag) {
1445
+ case "Text": return span.value;
1446
+ case "Strong":
1447
+ case "Weak":
1448
+ case "Highlight": return extractSpanText(span.value);
1449
+ case "URI": return span.value;
1450
+ case "Sequence": return extractSpanText(span.left) + extractSpanText(span.right);
1451
+ default: return "";
1452
+ }
1453
+ }
1454
+ function extractPseudoName(node) {
1455
+ const pn = node.pseudoName;
1456
+ if (pn && pn._tag === "Some") return pn.value;
1457
+ return node.name?.replace(/[<>]/g, "");
1458
+ }
1459
+ function formatFallback(fallback) {
1460
+ if (fallback === null || fallback === void 0) return void 0;
1461
+ if (typeof fallback === "object" && "_tag" in fallback) {
1462
+ if (fallback._tag === "None") return void 0;
1463
+ if (fallback._tag === "Some") return formatFallback(fallback.value);
1464
+ }
1465
+ if (typeof fallback === "boolean") return fallback ? "true" : "false";
1466
+ return String(fallback);
1467
+ }
1468
+ //#endregion
1469
+ //#region src/help/renderer.ts
1470
+ function renderRootHelp(config, subcommandDescriptors) {
1471
+ const rootInfo = { name: config.name };
1472
+ const subs = subcommandDescriptors.map((d) => extractCommandInfo(d));
1473
+ const lines = [];
1474
+ lines.push(`${pc.bold(rootInfo.name)} ${pc.dim(`v${config.version}`)}`);
1475
+ lines.push("");
1476
+ lines.push(`${pc.bold("Usage:")} ${rootInfo.name} <command> [flags]`);
1477
+ if (subs.length > 0) {
1478
+ lines.push("");
1479
+ lines.push(pc.bold("Commands:"));
1480
+ const maxLen = Math.max(...subs.map((s) => s.name.length));
1481
+ for (const sub of subs) lines.push(` ${pc.green(sub.name.padEnd(maxLen))} ${pc.dim(sub.description)}`);
1482
+ }
1483
+ lines.push("");
1484
+ lines.push(pc.dim(`Run ${rootInfo.name} <command> --help for more information.`));
1485
+ return lines.join("\n");
1486
+ }
1487
+ function renderCommandHelp(config, descriptor) {
1488
+ const cmd = unwrapCommand(descriptor);
1489
+ const info = extractCommandInfo(descriptor);
1490
+ const args = collectArgs(cmd.args);
1491
+ const opts = collectOptions(cmd.options);
1492
+ const lines = [];
1493
+ lines.push(`${pc.bold(config.name)} ${pc.green(info.name)}${info.description ? ` ${pc.dim("-")} ${info.description}` : ""}`);
1494
+ lines.push("");
1495
+ const usageParts = [config.name, info.name];
1496
+ for (const arg of args) usageParts.push(arg.required ? `<${arg.name}>` : `[${arg.name}]`);
1497
+ if (opts.length > 0) usageParts.push("[flags]");
1498
+ lines.push(`${pc.bold("Usage:")} ${usageParts.join(" ")}`);
1499
+ if (args.length > 0) {
1500
+ lines.push("");
1501
+ lines.push(pc.bold("Arguments:"));
1502
+ const maxLen = Math.max(...args.map((a) => a.name.length));
1503
+ for (const arg of args) {
1504
+ const name = pc.green(arg.name.padEnd(maxLen));
1505
+ const parts = [arg.description];
1506
+ if (arg.defaultValue !== void 0) parts.push(pc.dim(`[default: ${arg.defaultValue}]`));
1507
+ lines.push(` ${name} ${parts.filter(Boolean).join(" ")}`);
1508
+ }
1509
+ }
1510
+ if (opts.length > 0) {
1511
+ lines.push("");
1512
+ lines.push(pc.bold("Options:"));
1513
+ const signatures = opts.map(formatOptionSignature);
1514
+ const maxLen = Math.max(...signatures.map((s) => stripAnsi(s).length));
1515
+ for (let i = 0; i < opts.length; i++) {
1516
+ const sig = signatures[i];
1517
+ const pad = " ".repeat(maxLen - stripAnsi(sig).length);
1518
+ const parts = [opts[i].description];
1519
+ if (opts[i].defaultValue !== void 0) parts.push(pc.dim(`[default: ${opts[i].defaultValue}]`));
1520
+ if (opts[i].repeatable) parts.push(pc.dim("(repeatable)"));
1521
+ lines.push(` ${sig}${pad} ${parts.filter(Boolean).join(" ")}`);
1522
+ }
1523
+ }
1524
+ return lines.join("\n");
1525
+ }
1526
+ function formatOptionSignature(opt) {
1527
+ return `${opt.short ? `${pc.yellow(`-${opt.short}`)}, ` : " "}${pc.yellow(`--${opt.long}`)}${opt.type ? ` ${pc.dim(`<${opt.type}>`)}` : ""}`;
1528
+ }
1529
+ function stripAnsi(str) {
1530
+ return str.replace(/\u001b\[[0-9;]*m/g, "");
1531
+ }
1532
+ //#endregion
1533
+ export { AuthService as C, AuthError as S, ConfigError as _, extractCommandInfo as a, ApiClientLive as b, command as c, subcommands as d, TelemetryService as f, cancel as g, PromptCancelledError as h, collectOptions as i, helpConfig as l, trackCommand as m, renderRootHelp as n, unwrapCommand as o, TelemetryServiceLive as p, collectArgs as r, cliConfig as s, renderCommandHelp as t, run as u, ConfigService as v, AuthServiceLive as w, AuthContext as x, ConfigServiceLive as y };