naro-cli 0.4.1 → 0.5.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.
@@ -207,6 +207,11 @@ function pathnameOf(url2) {
207
207
  }
208
208
  }
209
209
 
210
+ // ../../packages/core/src/paths.ts
211
+ function projectPath(teamSlug, projectSlug) {
212
+ return `/${teamSlug}/projects/${projectSlug}`;
213
+ }
214
+
210
215
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
211
216
  var external_exports = {};
212
217
  __export(external_exports, {
@@ -17224,6 +17229,7 @@ async function deployDirectory(sourceDir, options, log2) {
17224
17229
  log2("\u25B8 starting deployment");
17225
17230
  const ctx = await client.deployments.createCli({
17226
17231
  ...options.projectId ? { projectId: options.projectId } : { name },
17232
+ ...options.teamSlug ? { teamSlug: options.teamSlug } : {},
17227
17233
  production,
17228
17234
  ...git.repositoryUrl != null && { repositoryUrl: git.repositoryUrl },
17229
17235
  ...branch != null && { branch },
@@ -17284,7 +17290,10 @@ async function deployDirectory(sourceDir, options, log2) {
17284
17290
  status: detail.deployment.status,
17285
17291
  url: url2,
17286
17292
  production,
17287
- missingEnvKeys
17293
+ missingEnvKeys,
17294
+ teamSlug: ctx.teamSlug,
17295
+ projectSlug: ctx.projectSlug,
17296
+ gitConnection: ctx.gitConnection
17288
17297
  };
17289
17298
  }
17290
17299
  async function waitForFinish(client, ctx, log2) {
@@ -17305,6 +17314,7 @@ function deploymentUrl(detail, ctx, production) {
17305
17314
 
17306
17315
  export {
17307
17316
  parseGitHubRemote,
17317
+ projectPath,
17308
17318
  NaroError,
17309
17319
  isNaroError,
17310
17320
  toNaroError,
package/dist/deploy.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type CliGitConnection } from "@next-deploy/sdk";
1
2
  import type { BuildObserver, DeploymentArtifact, DeploymentStatus } from "./artifact-types";
2
3
  /** The slice of LocalBuildExecutor the deploy flow needs — injectable in tests. */
3
4
  export interface DeployBuildExecutor {
@@ -22,6 +23,8 @@ export interface DeployOptions {
22
23
  name?: string | undefined;
23
24
  /** Linked project id (.naro/project.json or --project). */
24
25
  projectId?: string | undefined;
26
+ /** Owning team slug, resolved by the caller (see commands/link.ts). */
27
+ teamSlug?: string | undefined;
25
28
  rootDirectory?: string | null;
26
29
  production?: boolean;
27
30
  env?: Record<string, string> | undefined;
@@ -49,6 +52,10 @@ export interface DeployResult {
49
52
  production: boolean;
50
53
  /** 끝까지 채워지지 않은 키. */
51
54
  missingEnvKeys: string[];
55
+ /** 서버가 확정한 목적지 — 출력과 링크 파일이 이 값을 쓴다. */
56
+ teamSlug: string;
57
+ projectSlug: string;
58
+ gitConnection: CliGitConnection;
52
59
  }
53
60
  /**
54
61
  * The full CLI deploy: authenticate, register the deployment with the
package/dist/deploy.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  parseDotenv,
6
6
  readProjectEnv,
7
7
  slugify
8
- } from "./chunk-YLFLDW2H.js";
8
+ } from "./chunk-ZHVBIQBE.js";
9
9
  export {
10
10
  deployDirectory,
11
11
  eventsObserver,
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  login,
16
16
  openBrowser,
17
17
  parseGitHubRemote,
18
+ projectPath,
18
19
  promptForMissingEnv,
19
20
  readProjectLink,
20
21
  refreshStoredSession,
@@ -25,7 +26,7 @@ import {
25
26
  slugify,
26
27
  toNaroError,
27
28
  writeProjectLink
28
- } from "./chunk-YLFLDW2H.js";
29
+ } from "./chunk-ZHVBIQBE.js";
29
30
 
30
31
  // src/args.ts
31
32
  var BOOLEAN_FLAGS = {
@@ -522,20 +523,34 @@ async function runGitDisconnect(context) {
522
523
  " Deploy from here with: naro deploy"
523
524
  ]);
524
525
  }
525
- async function offerAutoDeploy(context, projectId, overrides = {}) {
526
+ async function offerAutoDeploy(context, projectId, overrides) {
526
527
  const deps = { ...defaults, ...overrides };
527
528
  if (overrides.canPrompt === false) return;
528
529
  try {
529
530
  if (readProjectLink(context.cwd)?.autoDeployDeclined === true) return;
530
- const remote = await deps.detectRemote(context.cwd);
531
- const parsed = remote ? parseGitHubRemote(remote) : null;
532
- if (!parsed) return;
533
- const client = await context.client();
534
- const project = await client.projects.get(projectId);
535
- if (project.repositoryFullName) return;
536
- const wanted = await context.prompter.confirm(
537
- "Should your changes deploy automatically from now on?"
538
- );
531
+ const connection = overrides.gitConnection;
532
+ if (connection.state === "connected" || connection.state === "none") return;
533
+ const fullName = connection.fullName;
534
+ if (!fullName) return;
535
+ let wanted;
536
+ if (connection.state === "unreachable") {
537
+ const installed = connection.installedAccounts ?? [];
538
+ context.output.log(
539
+ `
540
+ \u25B8 Automatic deploys need the Naro GitHub App on \`${connection.repoOwner ?? fullName}\`.`
541
+ );
542
+ if (overrides.teamSlug && installed.length > 0)
543
+ context.output.log(
544
+ ` Team ${overrides.teamSlug} has it on: ${installed.join(", ")}.`
545
+ );
546
+ wanted = await context.prompter.confirm(
547
+ "Install it for this team now? (opens your browser)"
548
+ );
549
+ } else {
550
+ wanted = await context.prompter.confirm(
551
+ "Should your changes deploy automatically from now on?"
552
+ );
553
+ }
539
554
  if (!wanted) {
540
555
  const link = readProjectLink(context.cwd);
541
556
  writeProjectLink(context.cwd, {
@@ -547,18 +562,19 @@ async function offerAutoDeploy(context, projectId, overrides = {}) {
547
562
  );
548
563
  return;
549
564
  }
550
- const result = await client.projects.connectGit(projectId, parsed.fullName);
565
+ const client = await context.client();
566
+ const result = await client.projects.connectGit(projectId, fullName);
551
567
  if (result.ok) {
552
568
  connected(context, result.project);
553
569
  return;
554
570
  }
555
571
  if (!result.installUrl) return;
556
572
  context.output.log(`
557
- \u25B8 Naro needs access to ${parsed.fullName}.`);
573
+ \u25B8 Naro needs access to ${fullName}.`);
558
574
  context.output.log(` Opening ${result.installUrl}`);
559
575
  deps.openBrowser(result.installUrl);
560
576
  context.output.log(" Waiting for the installation to finish...\n");
561
- await waitForInstall(context, projectId, parsed.fullName, deps);
577
+ await waitForInstall(context, projectId, fullName, deps);
562
578
  } catch (error) {
563
579
  context.output.log(
564
580
  `\u25B8 Could not set up automatic deploys (${error instanceof Error ? error.message : String(error)}).`
@@ -567,10 +583,150 @@ async function offerAutoDeploy(context, projectId, overrides = {}) {
567
583
  }
568
584
  }
569
585
 
586
+ // src/commands/link.ts
587
+ var SWITCH_ACCOUNT = "Sign in as a different account\u2026";
588
+ async function resolveTeam(context, teams, deps = {}) {
589
+ const explicit = context.flags.team ?? process.env.NARO_TEAM;
590
+ if (explicit) {
591
+ const match = teams.find((team) => team.slug === explicit);
592
+ if (!match)
593
+ throw new NaroError(
594
+ "NOT_FOUND",
595
+ `You are not a member of team "${explicit}".`
596
+ );
597
+ return match;
598
+ }
599
+ const linked = context.projectLink();
600
+ if (linked?.teamSlug) {
601
+ const match = teams.find((team) => team.slug === linked.teamSlug);
602
+ if (match) return match;
603
+ }
604
+ if (teams.length === 0)
605
+ throw new NaroError("NOT_FOUND", "Your account has no teams yet.");
606
+ const canPrompt = deps.canPrompt?.() ?? (!context.flags.json && !context.flags.yes && isInteractive());
607
+ if (!canPrompt)
608
+ throw new NaroError(
609
+ "TEAM_REQUIRED",
610
+ `Cannot pick a team without a prompt. Pass --team <slug>, set NARO_TEAM, or run \`naro link\` first.
611
+ Your teams: ${teams.map((team) => team.slug).join(", ")}`
612
+ );
613
+ const account = deps.accountLabel ? await deps.accountLabel() : "this account";
614
+ if (teams.length === 1) {
615
+ const only = teams[0];
616
+ const label = only.isPersonal ? `${only.name} (personal)` : only.name;
617
+ context.output.log(`
618
+ ${account} \u2192 ${label}`);
619
+ const ok = await context.prompter.confirm(
620
+ "Deploy to this account and team?"
621
+ );
622
+ if (ok) return only;
623
+ return switchAndRetry(context, deps);
624
+ }
625
+ const options = [
626
+ ...teams.map(
627
+ (team) => `${team.name}${team.isPersonal ? " (personal)" : ""}`
628
+ ),
629
+ SWITCH_ACCOUNT
630
+ ];
631
+ const index = await context.prompter.select(
632
+ `Deploying as ${account} \u2014 which team owns this project?`,
633
+ options
634
+ );
635
+ const picked = teams[index];
636
+ if (picked) return picked;
637
+ return switchAndRetry(context, deps);
638
+ }
639
+ async function switchAndRetry(context, deps) {
640
+ if (!deps.switchAccount || !deps.listTeams)
641
+ throw new NaroError(
642
+ "CANCELED",
643
+ "Canceled. Switch accounts with `naro logout && naro login`."
644
+ );
645
+ await deps.switchAccount();
646
+ const teams = await deps.listTeams();
647
+ return resolveTeam(context, teams, deps);
648
+ }
649
+ function saveLink(context, project, teamSlug) {
650
+ writeProjectLink(context.cwd, {
651
+ projectId: project.id,
652
+ teamSlug,
653
+ projectName: project.name
654
+ });
655
+ if (ensureGitignoreHasNaro(context.cwd))
656
+ context.output.log("\u25B8 added .naro to .gitignore");
657
+ }
658
+ async function runLink(context) {
659
+ const client = await context.client();
660
+ const teams = await client.teams.list();
661
+ const team = await resolveTeam(context, teams);
662
+ const projects = await client.projects.list(team.slug);
663
+ let project;
664
+ if (context.flags.project) {
665
+ project = projects.find(
666
+ (candidate) => candidate.id === context.flags.project
667
+ );
668
+ if (!project) {
669
+ project = await client.projects.get(context.flags.project);
670
+ }
671
+ } else if (context.flags.name) {
672
+ project = projects.find(
673
+ (candidate) => candidate.name === context.flags.name || candidate.slug === context.flags.name
674
+ );
675
+ if (!project)
676
+ throw new NaroError(
677
+ "PROJECT_NOT_FOUND",
678
+ `No project named "${context.flags.name}" in team ${team.slug}. Run \`naro deploy\` to create it.`
679
+ );
680
+ } else {
681
+ const guess = slugify(context.cwd.split("/").at(-1) ?? "app");
682
+ const exact = projects.find(
683
+ (candidate) => candidate.name === guess || candidate.slug === guess
684
+ );
685
+ if (exact && (context.flags.yes || context.flags.json)) {
686
+ project = exact;
687
+ } else if (projects.length === 0) {
688
+ throw new NaroError(
689
+ "PROJECT_NOT_FOUND",
690
+ `Team ${team.slug} has no projects yet. Run \`naro deploy\` to create one.`
691
+ );
692
+ } else {
693
+ const options = projects.map(
694
+ (candidate) => `${candidate.name} (${candidate.subdomain})`
695
+ );
696
+ const index = await context.prompter.select(
697
+ `Link ${context.cwd} to which project?`,
698
+ options
699
+ );
700
+ project = projects[index];
701
+ }
702
+ }
703
+ if (!project)
704
+ throw new NaroError("PROJECT_NOT_FOUND", "No project selected.");
705
+ const owningTeam = teams.find((candidate) => candidate.id === project.teamId) ?? team;
706
+ saveLink(context, project, owningTeam.slug);
707
+ context.output.result(
708
+ {
709
+ projectId: project.id,
710
+ projectName: project.name,
711
+ teamSlug: owningTeam.slug,
712
+ subdomain: project.subdomain
713
+ },
714
+ [`\u2705 Linked to ${owningTeam.slug}/${project.name} (.naro/project.json)`]
715
+ );
716
+ }
717
+ async function runUnlink(context) {
718
+ const existing = readProjectLink(context.cwd);
719
+ const removed = removeProjectLink(context.cwd);
720
+ context.output.result(
721
+ { removed, ...existing ? { projectId: existing.projectId } : {} },
722
+ [removed ? "\u2705 Unlinked (.naro removed)" : "Nothing to unlink here."]
723
+ );
724
+ }
725
+
570
726
  // src/commands/deploy.ts
571
727
  async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
572
728
  const { flags, output } = context;
573
- const link = flags.project ? { projectId: flags.project } : readProjectLink(context.cwd);
729
+ const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
574
730
  if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
575
731
  const confirmed = await context.prompter.confirm("Deploy to production?");
576
732
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
@@ -578,12 +734,17 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
578
734
  output.log(`
579
735
  \u{1F680} Deploying ${context.cwd}
580
736
  `);
581
- const canPrompt = !flags.json && !flags.yes && isInteractive();
737
+ const canPrompt = deps.canPrompt?.() ?? (!flags.json && !flags.yes && isInteractive());
738
+ const team = await resolveDestination(context, link, canPrompt);
739
+ const account = await accountLabel(context);
740
+ output.log(` ${account} \u2192 ${team.slug}
741
+ `);
582
742
  const result = await deps.deploy(
583
743
  context.cwd,
584
744
  {
585
745
  name: flags.name,
586
746
  projectId: link?.projectId,
747
+ teamSlug: team.slug,
587
748
  rootDirectory: flags.root ?? null,
588
749
  branch: flags.branch,
589
750
  production: flags.prod,
@@ -598,13 +759,24 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
598
759
  },
599
760
  (message) => output.log(message)
600
761
  );
762
+ const existing = readProjectLink(context.cwd);
763
+ writeProjectLink(context.cwd, {
764
+ projectId: result.projectId,
765
+ teamSlug: result.teamSlug,
766
+ projectName: result.projectSlug,
767
+ ...existing?.autoDeployDeclined === true ? { autoDeployDeclined: true } : {}
768
+ });
601
769
  if (!link) {
602
- writeProjectLink(context.cwd, { projectId: result.projectId });
603
770
  if (ensureGitignoreHasNaro(context.cwd))
604
771
  output.log("\u25B8 added .naro to .gitignore");
605
772
  output.log("\u25B8 linked (.naro/project.json)");
606
773
  }
607
- await deps.offerAutoDeploy?.(context, result.projectId, { canPrompt });
774
+ await deps.offerAutoDeploy?.(context, result.projectId, {
775
+ canPrompt,
776
+ gitConnection: result.gitConnection,
777
+ teamSlug: result.teamSlug
778
+ });
779
+ const inspect = `${controlPlaneUrl()}${projectPath(result.teamSlug, result.projectSlug)}`;
608
780
  context.output.result(
609
781
  {
610
782
  deploymentId: result.deploymentId,
@@ -612,16 +784,54 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
612
784
  status: result.status,
613
785
  url: result.url,
614
786
  target: result.production ? "production" : "preview",
787
+ teamSlug: result.teamSlug,
788
+ inspectUrl: inspect,
615
789
  missingEnvKeys: result.missingEnvKeys
616
790
  },
617
791
  [
618
792
  "",
619
793
  `\u2705 Deployment ready (${result.production ? "production" : "preview"})`,
620
794
  "",
621
- result.url
795
+ ` ${result.url}`,
796
+ ` Inspect ${inspect}`
622
797
  ]
623
798
  );
624
799
  }
800
+ async function resolveDestination(context, link, canPrompt) {
801
+ const client = await context.client();
802
+ const teams = await client.teams.list();
803
+ const deps = scopeDeps(context, canPrompt);
804
+ if (link?.teamSlug) {
805
+ const match = teams.find((team) => team.slug === link.teamSlug);
806
+ if (match) return match;
807
+ } else if (link) {
808
+ const project = await client.projects.get(link.projectId);
809
+ const match = teams.find((team) => team.id === project.teamId);
810
+ if (match) return match;
811
+ }
812
+ return resolveTeam(context, teams, deps);
813
+ }
814
+ function scopeDeps(context, canPrompt) {
815
+ return {
816
+ canPrompt: () => canPrompt,
817
+ accountLabel: () => accountLabel(context),
818
+ switchAccount: async () => {
819
+ clearSession();
820
+ await runLogin(context);
821
+ },
822
+ listTeams: async () => (await context.client()).teams.list()
823
+ };
824
+ }
825
+ async function accountLabel(context) {
826
+ try {
827
+ const { user } = await context.auth();
828
+ if (!user) return "this account";
829
+ const name = user.email ?? user.id;
830
+ return user.github ? `${name} (${user.github})` : name;
831
+ } catch {
832
+ return "this account";
833
+ }
834
+ }
625
835
 
626
836
  // src/commands/deployments.ts
627
837
  function shortSha(sha) {
@@ -875,112 +1085,6 @@ ${serializeDotenv(vars)}`,
875
1085
  );
876
1086
  }
877
1087
 
878
- // src/commands/link.ts
879
- async function resolveTeam(context, teams) {
880
- if (context.flags.team) {
881
- const match = teams.find((team) => team.slug === context.flags.team);
882
- if (!match)
883
- throw new NaroError(
884
- "NOT_FOUND",
885
- `You are not a member of team "${context.flags.team}".`
886
- );
887
- return match;
888
- }
889
- const linked = context.projectLink();
890
- if (linked?.teamSlug) {
891
- const match = teams.find((team) => team.slug === linked.teamSlug);
892
- if (match) return match;
893
- }
894
- const personal = teams.find((team) => team.isPersonal);
895
- const fallback = personal ?? teams[0];
896
- if (!fallback)
897
- throw new NaroError("NOT_FOUND", "Your account has no teams yet.");
898
- if (teams.length > 1 && !personal) {
899
- const index = await context.prompter.select(
900
- "Which team?",
901
- teams.map((team) => `${team.name} (${team.slug})`)
902
- );
903
- return teams[index] ?? fallback;
904
- }
905
- return fallback;
906
- }
907
- function saveLink(context, project, teamSlug) {
908
- writeProjectLink(context.cwd, {
909
- projectId: project.id,
910
- teamSlug,
911
- projectName: project.name
912
- });
913
- if (ensureGitignoreHasNaro(context.cwd))
914
- context.output.log("\u25B8 added .naro to .gitignore");
915
- }
916
- async function runLink(context) {
917
- const client = await context.client();
918
- const teams = await client.teams.list();
919
- const team = await resolveTeam(context, teams);
920
- const projects = await client.projects.list(team.slug);
921
- let project;
922
- if (context.flags.project) {
923
- project = projects.find(
924
- (candidate) => candidate.id === context.flags.project
925
- );
926
- if (!project) {
927
- project = await client.projects.get(context.flags.project);
928
- }
929
- } else if (context.flags.name) {
930
- project = projects.find(
931
- (candidate) => candidate.name === context.flags.name || candidate.slug === context.flags.name
932
- );
933
- if (!project)
934
- throw new NaroError(
935
- "PROJECT_NOT_FOUND",
936
- `No project named "${context.flags.name}" in team ${team.slug}. Run \`naro deploy\` to create it.`
937
- );
938
- } else {
939
- const guess = slugify(context.cwd.split("/").at(-1) ?? "app");
940
- const exact = projects.find(
941
- (candidate) => candidate.name === guess || candidate.slug === guess
942
- );
943
- if (exact && (context.flags.yes || context.flags.json)) {
944
- project = exact;
945
- } else if (projects.length === 0) {
946
- throw new NaroError(
947
- "PROJECT_NOT_FOUND",
948
- `Team ${team.slug} has no projects yet. Run \`naro deploy\` to create one.`
949
- );
950
- } else {
951
- const options = projects.map(
952
- (candidate) => `${candidate.name} (${candidate.subdomain})`
953
- );
954
- const index = await context.prompter.select(
955
- `Link ${context.cwd} to which project?`,
956
- options
957
- );
958
- project = projects[index];
959
- }
960
- }
961
- if (!project)
962
- throw new NaroError("PROJECT_NOT_FOUND", "No project selected.");
963
- const owningTeam = teams.find((candidate) => candidate.id === project.teamId) ?? team;
964
- saveLink(context, project, owningTeam.slug);
965
- context.output.result(
966
- {
967
- projectId: project.id,
968
- projectName: project.name,
969
- teamSlug: owningTeam.slug,
970
- subdomain: project.subdomain
971
- },
972
- [`\u2705 Linked to ${owningTeam.slug}/${project.name} (.naro/project.json)`]
973
- );
974
- }
975
- async function runUnlink(context) {
976
- const existing = readProjectLink(context.cwd);
977
- const removed = removeProjectLink(context.cwd);
978
- context.output.result(
979
- { removed, ...existing ? { projectId: existing.projectId } : {} },
980
- [removed ? "\u2705 Unlinked (.naro removed)" : "Nothing to unlink here."]
981
- );
982
- }
983
-
984
1088
  // src/commands/logs.ts
985
1089
  var FOLLOW_INTERVAL_MS = 3e3;
986
1090
  function sleep(ms) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro": "./dist/index.js"
@@ -28,8 +28,8 @@
28
28
  "tsx": "4.23.12",
29
29
  "typescript": "7.0.2",
30
30
  "vitest": "3.2.7",
31
- "@next-deploy/build": "0.1.0",
32
31
  "@next-deploy/core": "0.1.0",
32
+ "@next-deploy/build": "0.1.0",
33
33
  "@next-deploy/config": "0.1.0",
34
34
  "@next-deploy/sdk": "0.1.0"
35
35
  },