naro-cli 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1,91 +1,1262 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ NaroClient,
4
+ NaroError,
5
+ clearSession,
6
+ controlPlaneUrl,
7
+ createAuthClient,
3
8
  deployDirectory,
9
+ ensureGitignoreHasNaro,
10
+ isExpired,
11
+ isNaroError,
12
+ isTerminalStatus,
13
+ loadStoredSession,
14
+ login,
15
+ openBrowser,
4
16
  readProjectEnv,
5
- runLogin,
6
- runLogout,
7
- runWhoami
8
- } from "./chunk-UNCVFCTH.js";
17
+ readProjectLink,
18
+ refreshStoredSession,
19
+ removeProjectLink,
20
+ resolveAuth,
21
+ saveSession,
22
+ serializeDotenv,
23
+ slugify,
24
+ toNaroError,
25
+ writeProjectLink
26
+ } from "./chunk-B6F7QIW7.js";
9
27
 
10
- // src/index.ts
11
- function parseFlags(argv) {
12
- const flags = { production: false };
28
+ // src/args.ts
29
+ var BOOLEAN_FLAGS = {
30
+ "--json": "json",
31
+ "--yes": "yes",
32
+ "-y": "yes",
33
+ "--debug": "debug",
34
+ "--help": "help",
35
+ "-h": "help",
36
+ "--version": "version",
37
+ "-v": "version",
38
+ "--prod": "prod",
39
+ "--production": "prod",
40
+ "--follow": "follow",
41
+ "-f": "follow",
42
+ "--reveal": "reveal"
43
+ };
44
+ var STRING_FLAGS = {
45
+ "--token": "token",
46
+ "--project": "project",
47
+ "--team": "team",
48
+ "--cwd": "cwd",
49
+ "--name": "name",
50
+ "--root": "root",
51
+ "--branch": "branch",
52
+ "--type": "type",
53
+ "--env": "env",
54
+ "--level": "level"
55
+ };
56
+ function parseArgs(argv) {
57
+ const flags = {
58
+ json: false,
59
+ yes: false,
60
+ debug: false,
61
+ help: false,
62
+ version: false,
63
+ prod: false,
64
+ follow: false,
65
+ reveal: false
66
+ };
67
+ const positionals = [];
68
+ const unknown = [];
13
69
  for (let i = 0; i < argv.length; i++) {
14
70
  const arg = argv[i];
15
- if (arg === "--prod" || arg === "--production") flags.production = true;
16
- else if (arg === "--name") flags.name = argv[++i];
17
- else if (arg === "--root") flags.root = argv[++i];
18
- else if (arg === "--branch") flags.branch = argv[++i];
71
+ if (arg === void 0) continue;
72
+ const booleanKey = BOOLEAN_FLAGS[arg];
73
+ if (booleanKey) {
74
+ flags[booleanKey] = true;
75
+ continue;
76
+ }
77
+ const eq = arg.indexOf("=");
78
+ const stringKey = STRING_FLAGS[arg] ?? (eq > 0 ? STRING_FLAGS[arg.slice(0, eq)] : void 0);
79
+ if (stringKey) {
80
+ if (eq > 0 && STRING_FLAGS[arg.slice(0, eq)]) {
81
+ flags[stringKey] = arg.slice(
82
+ eq + 1
83
+ );
84
+ continue;
85
+ }
86
+ const value = argv[i + 1];
87
+ if (value === void 0 || value.startsWith("-") && value !== "-") {
88
+ unknown.push(`${arg} (missing value)`);
89
+ continue;
90
+ }
91
+ i += 1;
92
+ flags[stringKey] = value;
93
+ continue;
94
+ }
95
+ if (arg.startsWith("-") && arg !== "-") {
96
+ unknown.push(arg);
97
+ continue;
98
+ }
99
+ positionals.push(arg);
19
100
  }
20
- return flags;
101
+ const [command, ...rest] = positionals;
102
+ return { command, positionals: rest, flags, unknown };
21
103
  }
22
- var HELP = `naro \u2014 deploy the current app to naro
23
104
 
24
- Usage:
25
- naro login
26
- naro whoami
27
- naro logout
28
- naro deploy [--name <name>] [--root <dir>] [--branch <name>] [--prod]
29
-
30
- Options:
31
- --name Override the deployment name (defaults to package.json name)
32
- --root Sub-directory that contains the Next.js app (where package.json is)
33
- --branch Override the detected git branch
34
- --prod Publish to production ({name}.naro.sh) instead of preview
35
-
36
- Credentials:
37
- Run \`naro login\` to authenticate via GitHub.
38
- \`naro deploy\` signs you in automatically (opens your browser) when needed.
39
- Set NARO_CONTROL_PLANE_URL to use a custom control plane (defaults to https://naro.sh).
40
- `;
41
- async function main() {
42
- const [command, ...rest] = process.argv.slice(2);
43
- if (!command || command === "help" || command === "--help") {
44
- process.stdout.write(HELP);
45
- return;
105
+ // src/output.ts
106
+ var processStreams = {
107
+ stdout: (text) => process.stdout.write(text),
108
+ stderr: (text) => process.stderr.write(text)
109
+ };
110
+ var Output = class {
111
+ constructor(json, debug, streams = processStreams) {
112
+ this.json = json;
113
+ this.debug = debug;
114
+ this.streams = streams;
46
115
  }
47
- if (command === "login") return runLogin();
48
- if (command === "whoami") return runWhoami();
49
- if (command === "logout") return runLogout();
50
- if (command !== "deploy") {
51
- process.stderr.write(`Unknown command: ${command}
52
-
53
- ${HELP}`);
116
+ resultPrinted = false;
117
+ /** Progress/log line for humans. */
118
+ log(message) {
119
+ if (this.json) this.streams.stderr(`${message}
120
+ `);
121
+ else this.streams.stdout(`${message}
122
+ `);
123
+ }
124
+ /** Extra diagnostics, only with --debug. */
125
+ debugLog(message) {
126
+ if (this.debug) this.streams.stderr(`[debug] ${message}
127
+ `);
128
+ }
129
+ /**
130
+ * Final command result. JSON mode prints {success:true, ...data}; human
131
+ * mode prints the lines instead.
132
+ */
133
+ result(data, humanLines = []) {
134
+ this.resultPrinted = true;
135
+ if (this.json) {
136
+ this.streams.stdout(
137
+ `${JSON.stringify({ success: true, ...data }, null, 2)}
138
+ `
139
+ );
140
+ return;
141
+ }
142
+ for (const line of humanLines) this.streams.stdout(`${line}
143
+ `);
144
+ }
145
+ /** Command failure: stable {success:false, code, message} in JSON mode. */
146
+ fail(error) {
147
+ const naroError = isNaroError(error) ? error : toNaroError(error);
148
+ this.resultPrinted = true;
54
149
  process.exitCode = 1;
55
- return;
150
+ if (this.json) {
151
+ this.streams.stdout(
152
+ `${JSON.stringify(
153
+ {
154
+ success: false,
155
+ code: naroError.code,
156
+ message: naroError.message,
157
+ ...naroError.details ?? {}
158
+ },
159
+ null,
160
+ 2
161
+ )}
162
+ `
163
+ );
164
+ return;
165
+ }
166
+ this.streams.stderr(`\u2717 ${naroError.message}
167
+ `);
168
+ if (this.debug && naroError.cause instanceof Error) {
169
+ this.streams.stderr(`[debug] cause: ${naroError.cause.stack ?? ""}
170
+ `);
171
+ }
172
+ }
173
+ get hasResult() {
174
+ return this.resultPrinted;
56
175
  }
57
- const flags = parseFlags(rest);
58
- const cwd = process.cwd();
59
- const env = readProjectEnv(cwd);
60
- process.stdout.write(`
61
- \u{1F680} Deploying ${cwd}
176
+ };
62
177
 
178
+ // src/prompt.ts
179
+ import { createInterface } from "readline";
180
+ import { Writable } from "stream";
181
+ function isInteractive() {
182
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
183
+ }
184
+ async function readStdin() {
185
+ let data = "";
186
+ for await (const chunk of process.stdin) data += String(chunk);
187
+ return data.replace(/\r?\n$/, "");
188
+ }
189
+ var TtyPrompter = class {
190
+ async ask(question) {
191
+ const rl = createInterface({
192
+ input: process.stdin,
193
+ output: process.stdout
194
+ });
195
+ try {
196
+ return await new Promise(
197
+ (resolve) => rl.question(question, resolve)
198
+ );
199
+ } finally {
200
+ rl.close();
201
+ }
202
+ }
203
+ async askHidden(question) {
204
+ let muted = false;
205
+ const output = new Writable({
206
+ write(chunk, _enc, done) {
207
+ if (!muted) process.stdout.write(chunk);
208
+ done();
209
+ }
210
+ });
211
+ const rl = createInterface({
212
+ input: process.stdin,
213
+ output,
214
+ terminal: true
215
+ });
216
+ try {
217
+ const answer = new Promise(
218
+ (resolve) => rl.question(question, resolve)
219
+ );
220
+ muted = true;
221
+ const value = await answer;
222
+ process.stdout.write("\n");
223
+ return value;
224
+ } finally {
225
+ rl.close();
226
+ }
227
+ }
228
+ async confirm(question) {
229
+ const answer = (await this.ask(`${question} (y/N) `)).trim().toLowerCase();
230
+ return answer === "y" || answer === "yes";
231
+ }
232
+ async select(question, options) {
233
+ process.stdout.write(`${question}
63
234
  `);
64
- const result = await deployDirectory(
235
+ options.forEach((option, index) => {
236
+ process.stdout.write(` ${String(index + 1)}. ${option}
237
+ `);
238
+ });
239
+ for (; ; ) {
240
+ const answer = Number.parseInt(
241
+ (await this.ask(`Choose 1-${String(options.length)}: `)).trim(),
242
+ 10
243
+ );
244
+ if (Number.isInteger(answer) && answer >= 1 && answer <= options.length)
245
+ return answer - 1;
246
+ }
247
+ }
248
+ };
249
+ var NonInteractivePrompter = class {
250
+ constructor(assumeYes) {
251
+ this.assumeYes = assumeYes;
252
+ }
253
+ refuse(question) {
254
+ throw new NaroError(
255
+ "VALIDATION_FAILED",
256
+ `Interactive input required (${question.trim()}). Pass the value via flags/stdin or run without --json in a terminal.`
257
+ );
258
+ }
259
+ async ask(question) {
260
+ this.refuse(question);
261
+ }
262
+ async askHidden(question) {
263
+ this.refuse(question);
264
+ }
265
+ async confirm() {
266
+ return this.assumeYes;
267
+ }
268
+ async select(question) {
269
+ this.refuse(question);
270
+ }
271
+ };
272
+ function createPrompter(options) {
273
+ if (options.yes || options.json || !isInteractive())
274
+ return new NonInteractivePrompter(options.yes);
275
+ return new TtyPrompter();
276
+ }
277
+
278
+ // src/context.ts
279
+ function createContext(flags, positionals = []) {
280
+ const cwd = flags.cwd ?? process.cwd();
281
+ const output = new Output(flags.json, flags.debug);
282
+ const prompter = createPrompter({ json: flags.json, yes: flags.yes });
283
+ let resolvedAuth = null;
284
+ let resolvedClient = null;
285
+ const auth = async () => {
286
+ if (resolvedAuth) return resolvedAuth;
287
+ const interactive = !flags.json && !flags.yes && isInteractive() ? { openBrowser, log: (message) => output.log(message) } : false;
288
+ resolvedAuth = await resolveAuth({
289
+ explicitToken: flags.token,
290
+ interactiveLogin: interactive
291
+ });
292
+ return resolvedAuth;
293
+ };
294
+ return {
65
295
  cwd,
296
+ flags,
297
+ positionals,
298
+ output,
299
+ prompter,
300
+ positionalOrThrow: (count, usage) => {
301
+ if (positionals.length < count)
302
+ throw new NaroError("VALIDATION_FAILED", usage);
303
+ return positionals.slice(0, count);
304
+ },
305
+ auth,
306
+ client: async () => {
307
+ if (resolvedClient) return resolvedClient;
308
+ const resolved = await auth();
309
+ resolvedClient = new NaroClient({
310
+ baseUrl: controlPlaneUrl(),
311
+ getAccessToken: () => resolved.getAccessToken()
312
+ });
313
+ return resolvedClient;
314
+ },
315
+ projectLink: () => readProjectLink(cwd),
316
+ resolveProjectId: async () => {
317
+ if (flags.project) return flags.project;
318
+ const link = readProjectLink(cwd);
319
+ if (link) return link.projectId;
320
+ throw new NaroError(
321
+ "PROJECT_NOT_LINKED",
322
+ "No project is linked here. Run `naro link` first, or pass --project <id>."
323
+ );
324
+ }
325
+ };
326
+ }
327
+
328
+ // src/router.ts
329
+ import { readFileSync } from "fs";
330
+ import { dirname, join as join2 } from "path";
331
+ import { fileURLToPath } from "url";
332
+
333
+ // src/commands/auth.ts
334
+ async function runLogin(context) {
335
+ const { output } = context;
336
+ output.log("Opening your browser to sign in with GitHub...");
337
+ const session = await login({
338
+ openBrowser: (url) => {
339
+ output.log(`If your browser did not open, visit:
340
+ ${url}`);
341
+ openBrowser(url);
342
+ }
343
+ });
344
+ saveSession(session);
345
+ output.result(
346
+ {
347
+ user: {
348
+ id: session.user.id,
349
+ email: session.user.email,
350
+ github: session.user.github
351
+ }
352
+ },
353
+ [`\u2705 Logged in as ${session.user.email ?? session.user.id}`]
354
+ );
355
+ }
356
+ async function runLogout(context) {
357
+ const session = loadStoredSession();
358
+ if (session) {
359
+ try {
360
+ const client = createAuthClient();
361
+ await client.auth.setSession({
362
+ access_token: session.access_token,
363
+ refresh_token: session.refresh_token
364
+ });
365
+ await client.auth.signOut();
366
+ } catch {
367
+ }
368
+ }
369
+ clearSession();
370
+ context.output.result({ loggedOut: true }, ["Logged out."]);
371
+ }
372
+ async function currentSession() {
373
+ let session = loadStoredSession();
374
+ if (!session)
375
+ throw new NaroError("NOT_LOGGED_IN", "Not logged in. Run `naro login`.");
376
+ if (isExpired(session)) {
377
+ try {
378
+ session = await refreshStoredSession(session.refresh_token);
379
+ saveSession(session);
380
+ } catch {
381
+ throw new NaroError(
382
+ "NOT_LOGGED_IN",
383
+ "Session expired. Run `naro login`."
384
+ );
385
+ }
386
+ }
387
+ return session;
388
+ }
389
+ async function runWhoami(context) {
390
+ const session = await currentSession();
391
+ const handle = session.user.github ? ` (${session.user.github})` : "";
392
+ context.output.result(
393
+ {
394
+ user: {
395
+ id: session.user.id,
396
+ email: session.user.email,
397
+ github: session.user.github
398
+ }
399
+ },
400
+ [`Logged in as ${session.user.email ?? session.user.id}${handle}`]
401
+ );
402
+ }
403
+ async function runToken(context) {
404
+ const session = await currentSession();
405
+ process.stderr.write(
406
+ "\u26A0 Access tokens expire after about an hour \u2014 generate one per CI run.\n"
407
+ );
408
+ context.output.result(
409
+ { token: session.access_token, expiresAt: session.expires_at },
410
+ [session.access_token]
411
+ );
412
+ }
413
+
414
+ // src/commands/deploy.ts
415
+ async function runDeploy(context) {
416
+ const { flags, output } = context;
417
+ const link = flags.project ? { projectId: flags.project } : readProjectLink(context.cwd);
418
+ if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
419
+ const confirmed = await context.prompter.confirm("Deploy to production?");
420
+ if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
421
+ }
422
+ output.log(`
423
+ \u{1F680} Deploying ${context.cwd}
424
+ `);
425
+ const localEnv = link ? {} : readProjectEnv(context.cwd);
426
+ if (link)
427
+ output.log(
428
+ "\u25B8 using project env vars (manage them with `naro env`, sync local files with `naro env pull`)"
429
+ );
430
+ const result = await deployDirectory(
431
+ context.cwd,
66
432
  {
67
433
  name: flags.name,
434
+ projectId: link?.projectId,
68
435
  rootDirectory: flags.root ?? null,
69
436
  branch: flags.branch,
70
- production: flags.production,
71
- env
437
+ production: flags.prod,
438
+ // Local dotenv files seed the project ONCE (first, unlinked deploy).
439
+ // After that the platform owns env — a stray .env.local must not
440
+ // silently overwrite values set via the dashboard or `naro env add`.
441
+ env: localEnv,
442
+ token: flags.token,
443
+ allowLogin: !flags.json && !flags.yes
444
+ },
445
+ (message) => output.log(message)
446
+ );
447
+ if (!link) {
448
+ writeProjectLink(context.cwd, { projectId: result.projectId });
449
+ if (ensureGitignoreHasNaro(context.cwd))
450
+ output.log("\u25B8 added .naro to .gitignore");
451
+ output.log("\u25B8 linked (.naro/project.json)");
452
+ }
453
+ context.output.result(
454
+ {
455
+ deploymentId: result.deploymentId,
456
+ projectId: result.projectId,
457
+ status: result.status,
458
+ url: result.url,
459
+ target: result.production ? "production" : "preview"
72
460
  },
73
- (message) => process.stdout.write(`${message}
74
- `)
461
+ [
462
+ "",
463
+ `\u2705 Deployment ready (${result.production ? "production" : "preview"})`,
464
+ "",
465
+ result.url
466
+ ]
75
467
  );
76
- process.stdout.write(`
77
- \u2705 Live at ${result.url}
468
+ }
78
469
 
79
- `);
470
+ // src/commands/deployments.ts
471
+ function shortSha(sha) {
472
+ return sha ? sha.slice(0, 7) : "-";
80
473
  }
81
- void main().catch((error) => {
82
- process.stderr.write(
83
- `
84
- \u2717 ${error instanceof Error ? error.message : String(error)}
85
- `
474
+ function formatDeploymentLine(deployment) {
475
+ return [
476
+ deployment.id.padEnd(38),
477
+ deployment.status.padEnd(10),
478
+ deployment.branch.padEnd(16),
479
+ shortSha(deployment.gitCommitSha).padEnd(9),
480
+ deployment.url ?? ""
481
+ ].join(" ");
482
+ }
483
+ async function runDeployments(context) {
484
+ const client = await context.client();
485
+ const projectId = await context.resolveProjectId();
486
+ const deployments = await client.deployments.list(projectId);
487
+ context.output.result({ projectId, deployments }, [
488
+ `Deployments for ${projectId}:`,
489
+ "",
490
+ [
491
+ "ID".padEnd(38),
492
+ "STATUS".padEnd(10),
493
+ "BRANCH".padEnd(16),
494
+ "COMMIT".padEnd(9),
495
+ "URL"
496
+ ].join(" "),
497
+ ...deployments.map(formatDeploymentLine)
498
+ ]);
499
+ }
500
+ async function runInspect(context) {
501
+ const [deploymentId] = context.positionalOrThrow(
502
+ 1,
503
+ "Usage: naro inspect <deployment-id>"
504
+ );
505
+ const client = await context.client();
506
+ const detail = await client.deployments.get(deploymentId);
507
+ const { deployment } = detail;
508
+ const lastLogs = detail.logs.slice(-15);
509
+ context.output.result({ deployment, logs: detail.logs }, [
510
+ `Deployment ${deployment.id}`,
511
+ ` status: ${deployment.status}${deployment.errorCode ? ` (${deployment.errorCode})` : ""}`,
512
+ ` branch: ${deployment.branch}`,
513
+ ` commit: ${deployment.gitCommitSha ?? "-"}`,
514
+ ` url: ${deployment.url ?? "-"}`,
515
+ ` created: ${deployment.createdAt}`,
516
+ ` finished: ${deployment.finishedAt ?? "-"}`,
517
+ ...deployment.errorMessage ? [` error: ${deployment.errorMessage}`] : [],
518
+ ...lastLogs.length > 0 ? [
519
+ "",
520
+ "Recent build logs:",
521
+ ...lastLogs.map((log) => ` [${log.stage}] ${log.message}`)
522
+ ] : []
523
+ ]);
524
+ }
525
+
526
+ // src/commands/confirm.ts
527
+ async function confirmOrAbort(context, question) {
528
+ if (context.flags.yes) return;
529
+ if (context.flags.json || !isInteractive()) {
530
+ throw new NaroError(
531
+ "CONFIRMATION_REQUIRED",
532
+ `${question} Re-run with --yes to confirm.`
533
+ );
534
+ }
535
+ const confirmed = await context.prompter.confirm(question);
536
+ if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
537
+ }
538
+
539
+ // src/commands/domains.ts
540
+ function domainLines(domain) {
541
+ const lines = [
542
+ ` ${domain.hostname.padEnd(36)} ${domain.status.padEnd(12)} ssl: ${domain.sslStatus}`
543
+ ];
544
+ if (domain.status !== "active" && domain.verification.length > 0) {
545
+ lines.push(" Required DNS records:");
546
+ for (const record of domain.verification) {
547
+ lines.push(
548
+ ` Type: ${record.type.toUpperCase().padEnd(6)} Name: ${record.name}`,
549
+ ` Value: ${record.value}`
550
+ );
551
+ }
552
+ }
553
+ return lines;
554
+ }
555
+ async function runDomainsList(context) {
556
+ const client = await context.client();
557
+ const projectId = await context.resolveProjectId();
558
+ const domains = await client.domains.list(projectId);
559
+ context.output.result(
560
+ { projectId, domains },
561
+ domains.length === 0 ? ["No custom domains. Add one with `naro domains add example.com`."] : [
562
+ `Domains (${String(domains.length)}):`,
563
+ ...domains.flatMap(domainLines)
564
+ ]
565
+ );
566
+ }
567
+ async function runDomainsAdd(context) {
568
+ const [hostname] = context.positionalOrThrow(
569
+ 1,
570
+ "Usage: naro domains add <hostname>"
571
+ );
572
+ const client = await context.client();
573
+ const projectId = await context.resolveProjectId();
574
+ const domain = await client.domains.add(projectId, hostname);
575
+ context.output.result({ projectId, domain }, [
576
+ `\u2705 Domain added: ${domain.hostname}`,
577
+ "",
578
+ ...domainLines(domain),
579
+ "",
580
+ domain.status === "active" ? "The domain is active." : "Status: Pending verification \u2014 add the DNS records above, then run `naro domains list` to refresh."
581
+ ]);
582
+ }
583
+ async function runDomainsRemove(context) {
584
+ const [hostname] = context.positionalOrThrow(
585
+ 1,
586
+ "Usage: naro domains remove <hostname>"
587
+ );
588
+ await confirmOrAbort(
589
+ context,
590
+ `Remove ${String(hostname)}? Traffic to it will stop routing here.`
591
+ );
592
+ const client = await context.client();
593
+ const projectId = await context.resolveProjectId();
594
+ await client.domains.remove(projectId, hostname);
595
+ context.output.result({ projectId, hostname, removed: true }, [
596
+ `\u2705 Removed ${String(hostname)}`
597
+ ]);
598
+ }
599
+
600
+ // src/commands/env.ts
601
+ import { existsSync, writeFileSync } from "fs";
602
+ import { join } from "path";
603
+ var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
604
+ var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
605
+ function parseTarget(value) {
606
+ if (value === void 0) return "all";
607
+ if (!TARGETS.has(value))
608
+ throw new NaroError(
609
+ "VALIDATION_FAILED",
610
+ "Environment must be production, preview or all."
611
+ );
612
+ return value;
613
+ }
614
+ function maskedValue(row) {
615
+ if (!row.isSecret) return row.value ?? "";
616
+ return "\u2022\u2022\u2022\u2022\u2022\u2022 (secret)";
617
+ }
618
+ async function runEnvList(context) {
619
+ const client = await context.client();
620
+ const projectId = await context.resolveProjectId();
621
+ const reveal = context.flags.reveal;
622
+ const rows = await client.env.list(projectId, { reveal });
623
+ context.output.result(
624
+ {
625
+ projectId,
626
+ envVars: rows.map((row) => ({
627
+ id: row.id,
628
+ key: row.key,
629
+ target: row.target,
630
+ isSecret: row.isSecret,
631
+ value: row.isSecret && !reveal ? null : row.value
632
+ }))
633
+ },
634
+ [
635
+ `Environment variables (${String(rows.length)}):`,
636
+ ...rows.map(
637
+ (row) => ` ${row.key.padEnd(32)} ${row.target.padEnd(12)} ${reveal ? row.value ?? "" : maskedValue(row)}`
638
+ )
639
+ ]
640
+ );
641
+ }
642
+ async function runEnvAdd(context) {
643
+ context.positionalOrThrow(
644
+ 1,
645
+ "Usage: naro env add KEY [production|preview|all]"
646
+ );
647
+ const [key, targetArg] = context.positionals;
648
+ if (!key || !KEY_RE.test(key) || key.length > 256)
649
+ throw new NaroError(
650
+ "VALIDATION_FAILED",
651
+ "Keys must look like MY_VARIABLE (letters, digits, underscore)."
652
+ );
653
+ const target = parseTarget(targetArg);
654
+ let value;
655
+ if (!process.stdin.isTTY) {
656
+ value = await readStdin();
657
+ } else {
658
+ value = await context.prompter.askHidden(`? What's the value of ${key}? `);
659
+ }
660
+ if (value.length === 0)
661
+ throw new NaroError("VALIDATION_FAILED", `No value provided for ${key}.`);
662
+ const isSecret = !key.startsWith("NEXT_PUBLIC_");
663
+ const client = await context.client();
664
+ const projectId = await context.resolveProjectId();
665
+ await client.env.set(projectId, [{ key, value, target, isSecret }]);
666
+ context.output.result({ projectId, key, target, isSecret }, [
667
+ `\u2705 Added ${key} (${target}${isSecret ? ", secret" : ""})`
668
+ ]);
669
+ }
670
+ async function runEnvRemove(context) {
671
+ context.positionalOrThrow(
672
+ 1,
673
+ "Usage: naro env remove KEY [production|preview|all]"
674
+ );
675
+ const [key, targetArg] = context.positionals;
676
+ const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
677
+ await confirmOrAbort(
678
+ context,
679
+ `Remove ${String(key)}${target ? ` (${target})` : ""} from this project?`
680
+ );
681
+ const client = await context.client();
682
+ const projectId = await context.resolveProjectId();
683
+ const removed = await client.env.remove(projectId, key, target);
684
+ context.output.result({ projectId, key, removed }, [
685
+ `\u2705 Removed ${String(key)} (${String(removed)} entr${removed === 1 ? "y" : "ies"})`
686
+ ]);
687
+ }
688
+ async function runEnvPull(context) {
689
+ const file = context.positionals[0] ?? ".env.local";
690
+ const surface = context.flags.env === "production" ? "production" : "preview";
691
+ if (context.flags.env && !["production", "preview"].includes(context.flags.env))
692
+ throw new NaroError(
693
+ "VALIDATION_FAILED",
694
+ "--env must be production or preview."
695
+ );
696
+ const client = await context.client();
697
+ const projectId = await context.resolveProjectId();
698
+ const rows = await client.env.list(projectId, { reveal: true });
699
+ const selected = rows.filter(
700
+ (row) => row.target === "all" || row.target === surface
701
+ );
702
+ const vars = Object.fromEntries(
703
+ selected.map((row) => [row.key, row.value ?? ""])
704
+ );
705
+ const path = join(context.cwd, file);
706
+ if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
707
+ const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
708
+ if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
709
+ }
710
+ writeFileSync(
711
+ path,
712
+ `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
713
+ ${serializeDotenv(vars)}`,
714
+ { mode: 384 }
715
+ );
716
+ context.output.result(
717
+ { projectId, file, environment: surface, count: selected.length },
718
+ [`\u2705 Pulled ${String(selected.length)} variable(s) into ${file}`]
719
+ );
720
+ }
721
+
722
+ // src/commands/link.ts
723
+ async function resolveTeam(context, teams) {
724
+ if (context.flags.team) {
725
+ const match = teams.find((team) => team.slug === context.flags.team);
726
+ if (!match)
727
+ throw new NaroError(
728
+ "NOT_FOUND",
729
+ `You are not a member of team "${context.flags.team}".`
730
+ );
731
+ return match;
732
+ }
733
+ const linked = context.projectLink();
734
+ if (linked?.teamSlug) {
735
+ const match = teams.find((team) => team.slug === linked.teamSlug);
736
+ if (match) return match;
737
+ }
738
+ const personal = teams.find((team) => team.isPersonal);
739
+ const fallback = personal ?? teams[0];
740
+ if (!fallback)
741
+ throw new NaroError("NOT_FOUND", "Your account has no teams yet.");
742
+ if (teams.length > 1 && !personal) {
743
+ const index = await context.prompter.select(
744
+ "Which team?",
745
+ teams.map((team) => `${team.name} (${team.slug})`)
746
+ );
747
+ return teams[index] ?? fallback;
748
+ }
749
+ return fallback;
750
+ }
751
+ function saveLink(context, project, teamSlug) {
752
+ writeProjectLink(context.cwd, {
753
+ projectId: project.id,
754
+ teamSlug,
755
+ projectName: project.name
756
+ });
757
+ if (ensureGitignoreHasNaro(context.cwd))
758
+ context.output.log("\u25B8 added .naro to .gitignore");
759
+ }
760
+ async function runLink(context) {
761
+ const client = await context.client();
762
+ const teams = await client.teams.list();
763
+ const team = await resolveTeam(context, teams);
764
+ const projects = await client.projects.list(team.slug);
765
+ let project;
766
+ if (context.flags.project) {
767
+ project = projects.find(
768
+ (candidate) => candidate.id === context.flags.project
769
+ );
770
+ if (!project) {
771
+ project = await client.projects.get(context.flags.project);
772
+ }
773
+ } else if (context.flags.name) {
774
+ project = projects.find(
775
+ (candidate) => candidate.name === context.flags.name || candidate.slug === context.flags.name
776
+ );
777
+ if (!project)
778
+ throw new NaroError(
779
+ "PROJECT_NOT_FOUND",
780
+ `No project named "${context.flags.name}" in team ${team.slug}. Run \`naro deploy\` to create it.`
781
+ );
782
+ } else {
783
+ const guess = slugify(context.cwd.split("/").at(-1) ?? "app");
784
+ const exact = projects.find(
785
+ (candidate) => candidate.name === guess || candidate.slug === guess
786
+ );
787
+ if (exact && (context.flags.yes || context.flags.json)) {
788
+ project = exact;
789
+ } else if (projects.length === 0) {
790
+ throw new NaroError(
791
+ "PROJECT_NOT_FOUND",
792
+ `Team ${team.slug} has no projects yet. Run \`naro deploy\` to create one.`
793
+ );
794
+ } else {
795
+ const options = projects.map(
796
+ (candidate) => `${candidate.name} (${candidate.subdomain})`
797
+ );
798
+ const index = await context.prompter.select(
799
+ `Link ${context.cwd} to which project?`,
800
+ options
801
+ );
802
+ project = projects[index];
803
+ }
804
+ }
805
+ if (!project)
806
+ throw new NaroError("PROJECT_NOT_FOUND", "No project selected.");
807
+ const owningTeam = teams.find((candidate) => candidate.id === project.teamId) ?? team;
808
+ saveLink(context, project, owningTeam.slug);
809
+ context.output.result(
810
+ {
811
+ projectId: project.id,
812
+ projectName: project.name,
813
+ teamSlug: owningTeam.slug,
814
+ subdomain: project.subdomain
815
+ },
816
+ [`\u2705 Linked to ${owningTeam.slug}/${project.name} (.naro/project.json)`]
817
+ );
818
+ }
819
+ async function runUnlink(context) {
820
+ const existing = readProjectLink(context.cwd);
821
+ const removed = removeProjectLink(context.cwd);
822
+ context.output.result(
823
+ { removed, ...existing ? { projectId: existing.projectId } : {} },
824
+ [removed ? "\u2705 Unlinked (.naro removed)" : "Nothing to unlink here."]
825
+ );
826
+ }
827
+
828
+ // src/commands/logs.ts
829
+ var FOLLOW_INTERVAL_MS = 3e3;
830
+ function sleep(ms) {
831
+ return new Promise((resolve) => setTimeout(resolve, ms));
832
+ }
833
+ function formatBuildLog(log) {
834
+ return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
835
+ }
836
+ function formatRuntimeLog(entry) {
837
+ const time = new Date(entry.ts).toISOString();
838
+ const request = entry.method && entry.path ? ` ${entry.method} ${entry.path}${entry.status ? ` ${String(entry.status)}` : ""}` : "";
839
+ return `${time} [${entry.level}]${request} ${entry.message}`.trimEnd();
840
+ }
841
+ async function latestProductionDeploymentId(client, projectId) {
842
+ const deployments = await client.deployments.list(projectId);
843
+ const latest = deployments.find(
844
+ (deployment) => deployment.status === "READY"
845
+ );
846
+ const fallback = deployments[0];
847
+ const chosen = latest ?? fallback;
848
+ if (!chosen)
849
+ throw new NaroError(
850
+ "DEPLOYMENT_NOT_FOUND",
851
+ "This project has no deployments yet."
852
+ );
853
+ return chosen.id;
854
+ }
855
+ async function buildLogs(context, client, deploymentId) {
856
+ if (!context.flags.follow) {
857
+ const detail = await client.deployments.get(deploymentId);
858
+ context.output.result(
859
+ { deploymentId, type: "build", logs: detail.logs },
860
+ detail.logs.map(formatBuildLog)
861
+ );
862
+ return;
863
+ }
864
+ let lastSequence = -1;
865
+ for (; ; ) {
866
+ const detail = await client.deployments.get(deploymentId);
867
+ for (const log of detail.logs) {
868
+ if (log.sequence > lastSequence) {
869
+ lastSequence = log.sequence;
870
+ context.output.log(formatBuildLog(log));
871
+ }
872
+ }
873
+ if (isTerminalStatus(detail.deployment.status)) {
874
+ context.output.result(
875
+ {
876
+ deploymentId,
877
+ type: "build",
878
+ status: detail.deployment.status,
879
+ logs: detail.logs
880
+ },
881
+ [`\u2014 deployment ${detail.deployment.status} \u2014`]
882
+ );
883
+ return;
884
+ }
885
+ await sleep(FOLLOW_INTERVAL_MS);
886
+ }
887
+ }
888
+ async function runtimeLogs(context, client, projectId) {
889
+ const environment = context.flags.env === "preview" ? "preview" : "production";
890
+ if (context.flags.env && !["production", "preview"].includes(context.flags.env))
891
+ throw new NaroError(
892
+ "VALIDATION_FAILED",
893
+ "--env must be production or preview."
894
+ );
895
+ const options = {
896
+ environment,
897
+ ...context.flags.level ? { level: context.flags.level } : {}
898
+ };
899
+ if (!context.flags.follow) {
900
+ const entries = await client.logs.runtime(projectId, options);
901
+ context.output.result(
902
+ { projectId, type: "runtime", environment, entries },
903
+ entries.length > 0 ? entries.map(formatRuntimeLog) : ["(no runtime logs yet \u2014 the worker logs on each request)"]
904
+ );
905
+ return;
906
+ }
907
+ context.output.log(
908
+ `Following ${environment} runtime logs (Ctrl-C to stop)...`
909
+ );
910
+ let lastTs = -1;
911
+ let boundary = /* @__PURE__ */ new Set();
912
+ for (; ; ) {
913
+ const entries = await client.logs.runtime(projectId, options);
914
+ for (const entry of entries) {
915
+ if (entry.ts < lastTs) continue;
916
+ const key = JSON.stringify(entry);
917
+ if (entry.ts === lastTs) {
918
+ if (boundary.has(key)) continue;
919
+ } else {
920
+ lastTs = entry.ts;
921
+ boundary = /* @__PURE__ */ new Set();
922
+ }
923
+ boundary.add(key);
924
+ context.output.log(formatRuntimeLog(entry));
925
+ }
926
+ await sleep(FOLLOW_INTERVAL_MS);
927
+ }
928
+ }
929
+ async function runLogs(context) {
930
+ const client = await context.client();
931
+ const [deploymentId] = context.positionals;
932
+ const type = context.flags.type ?? (deploymentId !== void 0 ? "build" : "runtime");
933
+ if (type !== "build" && type !== "runtime")
934
+ throw new NaroError(
935
+ "VALIDATION_FAILED",
936
+ "--type must be build or runtime."
937
+ );
938
+ if (type === "runtime") {
939
+ if (deploymentId)
940
+ throw new NaroError(
941
+ "VALIDATION_FAILED",
942
+ "Runtime logs are per project, not per deployment. Drop the deployment id or use --type build."
943
+ );
944
+ const projectId = await context.resolveProjectId();
945
+ await runtimeLogs(context, client, projectId);
946
+ return;
947
+ }
948
+ const target = deploymentId ?? await latestProductionDeploymentId(
949
+ client,
950
+ await context.resolveProjectId()
951
+ );
952
+ await buildLogs(context, client, target);
953
+ }
954
+
955
+ // src/commands/projects.ts
956
+ function productionUrl(subdomain) {
957
+ return `https://${subdomain}.${new URL(controlPlaneUrl()).hostname}`;
958
+ }
959
+ async function runProjects(context) {
960
+ const client = await context.client();
961
+ const teams = await client.teams.list();
962
+ const team = await resolveTeam(context, teams);
963
+ const projects = await client.projects.list(team.slug);
964
+ context.output.result(
965
+ {
966
+ team: team.slug,
967
+ projects: projects.map((project) => ({
968
+ id: project.id,
969
+ name: project.name,
970
+ subdomain: project.subdomain,
971
+ url: productionUrl(project.subdomain),
972
+ defaultBranch: project.defaultBranch,
973
+ updatedAt: project.updatedAt
974
+ }))
975
+ },
976
+ [
977
+ `Projects in ${team.slug} (${String(projects.length)}):`,
978
+ ...projects.map(
979
+ (project) => ` ${project.name.padEnd(24)} ${productionUrl(project.subdomain).padEnd(40)} ${project.id}`
980
+ )
981
+ ]
86
982
  );
87
- process.exitCode = 1;
88
- });
983
+ }
984
+ async function runProjectInspect(context) {
985
+ const client = await context.client();
986
+ const projectId = await context.resolveProjectId();
987
+ const project = await client.projects.get(projectId);
988
+ const url = productionUrl(project.subdomain);
989
+ context.output.result({ project: { ...project, url } }, [
990
+ `${project.name}`,
991
+ ` id: ${project.id}`,
992
+ ` team: ${project.teamId}`,
993
+ ` production URL: ${url}`,
994
+ ` preview URL: https://${project.subdomain}-preview.${new URL(controlPlaneUrl()).hostname}`,
995
+ ` default branch: ${project.defaultBranch}`,
996
+ ` root directory: ${project.rootDirectory ?? "(repo root)"}`,
997
+ ` repository: ${project.repositoryFullName ?? project.repositoryUrl}`
998
+ ]);
999
+ }
1000
+
1001
+ // src/commands/promote.ts
1002
+ async function promoteAndWait(context, client, project, sourceDeploymentId, action) {
1003
+ const promoted = await client.deployments.promote(
1004
+ project.id,
1005
+ sourceDeploymentId
1006
+ );
1007
+ context.output.log(
1008
+ `\u25B8 ${action === "promote" ? "promoting" : "rolling back to"} ${sourceDeploymentId}`
1009
+ );
1010
+ const detail = await client.deployments.wait(promoted.id, {
1011
+ timeoutMs: 5 * 6e4
1012
+ });
1013
+ if (detail.deployment.status !== "READY") {
1014
+ throw new NaroError(
1015
+ detail.deployment.errorCode ?? "DEPLOY_FAILED",
1016
+ detail.deployment.errorMessage ?? `The ${action} failed.`,
1017
+ { details: { deploymentId: promoted.id } }
1018
+ );
1019
+ }
1020
+ context.output.result(
1021
+ {
1022
+ deploymentId: promoted.id,
1023
+ sourceDeploymentId,
1024
+ projectId: project.id,
1025
+ status: detail.deployment.status,
1026
+ url: detail.deployment.url
1027
+ },
1028
+ [
1029
+ `\u2705 Production now serves ${sourceDeploymentId}`,
1030
+ "",
1031
+ detail.deployment.url ?? ""
1032
+ ]
1033
+ );
1034
+ }
1035
+ async function runPromote(context) {
1036
+ const [deploymentId] = context.positionalOrThrow(
1037
+ 1,
1038
+ "Usage: naro promote <deployment-id>"
1039
+ );
1040
+ const client = await context.client();
1041
+ const projectId = await context.resolveProjectId();
1042
+ const project = await client.projects.get(projectId);
1043
+ await confirmOrAbort(
1044
+ context,
1045
+ `Promote ${String(deploymentId)} to production (${project.subdomain})?`
1046
+ );
1047
+ await promoteAndWait(
1048
+ context,
1049
+ client,
1050
+ project,
1051
+ deploymentId,
1052
+ "promote"
1053
+ );
1054
+ }
1055
+ async function runRollback(context) {
1056
+ const client = await context.client();
1057
+ const projectId = await context.resolveProjectId();
1058
+ const project = await client.projects.get(projectId);
1059
+ let target = context.positionals[0];
1060
+ if (!target) {
1061
+ const { current, target: candidate } = await client.deployments.rollbackTarget(projectId, project.defaultBranch);
1062
+ if (!candidate)
1063
+ throw new NaroError(
1064
+ "DEPLOYMENT_NOT_FOUND",
1065
+ "No earlier production build with a different artifact to roll back to. Pass a deployment id explicitly."
1066
+ );
1067
+ context.output.log(
1068
+ `\u25B8 current production: ${current.id}${current.gitCommitSha ? ` (${current.gitCommitSha.slice(0, 7)})` : ""}`
1069
+ );
1070
+ target = candidate.id;
1071
+ }
1072
+ await confirmOrAbort(
1073
+ context,
1074
+ `Are you sure you want to rollback production to ${target}?`
1075
+ );
1076
+ await promoteAndWait(context, client, project, target, "rollback");
1077
+ }
1078
+
1079
+ // src/router.ts
1080
+ var HELP = `naro \u2014 deploy Next.js apps to Naro (https://naro.sh)
1081
+
1082
+ Usage:
1083
+ naro login | logout | whoami
1084
+ naro token Print an access token for CI (NARO_TOKEN)
1085
+
1086
+ naro link | init Link this directory to a Naro project
1087
+ naro unlink Remove the link (.naro/)
1088
+ naro projects [--team <slug>] List projects
1089
+ naro project inspect Show the linked project
1090
+
1091
+ naro deploy [--prod] Build locally and deploy (preview by default)
1092
+ naro deployments List deployments of the linked project
1093
+ naro inspect <deployment-id> Show one deployment + build logs
1094
+
1095
+ naro logs [deployment-id] Runtime logs (or build logs of a deployment)
1096
+ [--type build|runtime] [--env production|preview] [--level error] [--follow]
1097
+
1098
+ naro env list List env vars (secrets masked)
1099
+ naro env add KEY [environment] Add/update (value from prompt or stdin)
1100
+ naro env remove KEY [environment] Remove
1101
+ naro env pull [file] Write env vars to .env.local
1102
+
1103
+ naro domains list | add <host> | remove <host>
1104
+
1105
+ naro promote <deployment-id> Publish a build to production
1106
+ naro rollback [deployment-id] Roll production back to the previous build
1107
+
1108
+ Global options:
1109
+ --json Machine-readable output (single JSON object on stdout)
1110
+ --yes, -y Never prompt; assume yes (CI / agents)
1111
+ --token <t> Access token (or set NARO_TOKEN)
1112
+ --project <id> Target project (overrides .naro/project.json)
1113
+ --team <slug> Team scope for project listing/linking
1114
+ --cwd <dir> Run as if started in <dir>
1115
+ --prod Deploy/target production
1116
+ --debug Verbose diagnostics
1117
+
1118
+ Credentials: \`naro login\` opens a browser (GitHub). CI: NARO_TOKEN=$(naro token).
1119
+ Set NARO_CONTROL_PLANE_URL to use a custom control plane (default https://naro.sh).
1120
+ `;
1121
+ function route(parsed) {
1122
+ const { command, positionals } = parsed;
1123
+ const sub = positionals[0];
1124
+ switch (command) {
1125
+ case "login":
1126
+ return { handler: runLogin, positionals };
1127
+ case "logout":
1128
+ return { handler: runLogout, positionals };
1129
+ case "whoami":
1130
+ return { handler: runWhoami, positionals };
1131
+ case "token":
1132
+ return { handler: runToken, positionals };
1133
+ case "link":
1134
+ case "init":
1135
+ return { handler: runLink, positionals };
1136
+ case "unlink":
1137
+ return { handler: runUnlink, positionals };
1138
+ case "projects":
1139
+ return { handler: runProjects, positionals };
1140
+ case "project":
1141
+ if (sub === void 0 || sub === "inspect")
1142
+ return {
1143
+ handler: runProjectInspect,
1144
+ positionals: positionals.slice(1)
1145
+ };
1146
+ return null;
1147
+ case "deploy":
1148
+ return { handler: runDeploy, positionals };
1149
+ case "deployments":
1150
+ case "ls":
1151
+ return { handler: runDeployments, positionals };
1152
+ case "inspect":
1153
+ return { handler: runInspect, positionals };
1154
+ case "logs":
1155
+ return { handler: runLogs, positionals };
1156
+ case "env":
1157
+ switch (sub) {
1158
+ case void 0:
1159
+ case "list":
1160
+ case "ls":
1161
+ return { handler: runEnvList, positionals: positionals.slice(1) };
1162
+ case "add":
1163
+ return { handler: runEnvAdd, positionals: positionals.slice(1) };
1164
+ case "remove":
1165
+ case "rm":
1166
+ return { handler: runEnvRemove, positionals: positionals.slice(1) };
1167
+ case "pull":
1168
+ return { handler: runEnvPull, positionals: positionals.slice(1) };
1169
+ default:
1170
+ return null;
1171
+ }
1172
+ case "domains":
1173
+ switch (sub) {
1174
+ case void 0:
1175
+ case "list":
1176
+ case "ls":
1177
+ return { handler: runDomainsList, positionals: positionals.slice(1) };
1178
+ case "add":
1179
+ return { handler: runDomainsAdd, positionals: positionals.slice(1) };
1180
+ case "remove":
1181
+ case "rm":
1182
+ return {
1183
+ handler: runDomainsRemove,
1184
+ positionals: positionals.slice(1)
1185
+ };
1186
+ default:
1187
+ return null;
1188
+ }
1189
+ case "promote":
1190
+ return { handler: runPromote, positionals };
1191
+ case "rollback":
1192
+ return { handler: runRollback, positionals };
1193
+ default:
1194
+ return null;
1195
+ }
1196
+ }
1197
+ function version() {
1198
+ try {
1199
+ const pkg = JSON.parse(
1200
+ readFileSync(
1201
+ join2(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
1202
+ "utf8"
1203
+ )
1204
+ );
1205
+ return pkg.version ?? "unknown";
1206
+ } catch {
1207
+ return "unknown";
1208
+ }
1209
+ }
1210
+
1211
+ // src/index.ts
1212
+ async function main(argv = process.argv.slice(2)) {
1213
+ const parsed = parseArgs(argv);
1214
+ if (parsed.flags.version) {
1215
+ process.stdout.write(`${version()}
1216
+ `);
1217
+ return;
1218
+ }
1219
+ if (!parsed.command || parsed.command === "help" || parsed.flags.help) {
1220
+ if (parsed.flags.json) {
1221
+ process.stdout.write(
1222
+ `${JSON.stringify({ success: true, help: HELP }, null, 2)}
1223
+ `
1224
+ );
1225
+ return;
1226
+ }
1227
+ process.stdout.write(HELP);
1228
+ if (parsed.command === void 0 && !parsed.flags.help && argv.length > 0)
1229
+ process.exitCode = 1;
1230
+ return;
1231
+ }
1232
+ const output = new Output(parsed.flags.json, parsed.flags.debug);
1233
+ if (parsed.unknown.length > 0) {
1234
+ output.fail(
1235
+ new NaroError(
1236
+ "VALIDATION_FAILED",
1237
+ `Unknown option: ${parsed.unknown.join(", ")}. See \`naro help\`.`
1238
+ )
1239
+ );
1240
+ return;
1241
+ }
1242
+ const routed = route(parsed);
1243
+ if (!routed) {
1244
+ output.fail(
1245
+ new NaroError(
1246
+ "VALIDATION_FAILED",
1247
+ `Unknown command: ${[parsed.command, ...parsed.positionals].join(" ")}. See \`naro help\`.`
1248
+ )
1249
+ );
1250
+ return;
1251
+ }
1252
+ const context = createContext(parsed.flags, routed.positionals);
1253
+ try {
1254
+ await routed.handler(context);
1255
+ } catch (error) {
1256
+ context.output.fail(error);
1257
+ }
1258
+ }
1259
+ void main();
89
1260
  export {
90
- parseFlags
1261
+ main
91
1262
  };