supcoflow 0.8.0 → 0.9.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.
@@ -30,6 +30,52 @@ export function repoDelOrigin(url) {
30
30
  }
31
31
  return null;
32
32
  }
33
+ // El `origin` con forma de alias de SSH (`git@github-gf0:dueño/repo`, `ssh://git@github-gf0/dueño/repo`): el host no
34
+ // es github.com, pero el dueño/repo sí tiene la forma de uno de GitHub. NO se resuelve (no se corre `ssh` ni se lee su
35
+ // configuración: `Match exec` ejecutaría programas de la persona): sólo se le dice qué asociar a mano, una vez.
36
+ const FORMAS_DE_ALIAS = [
37
+ /^[A-Za-z0-9._-]+@([A-Za-z0-9._-]+):([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/,
38
+ /^ssh:\/\/[A-Za-z0-9._-]+@([A-Za-z0-9._-]+)(?::\d+)?\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/,
39
+ ];
40
+ export function aliasDelOrigin(url) {
41
+ const limpia = url.trim();
42
+ for (const forma of FORMAS_DE_ALIAS) {
43
+ const m = forma.exec(limpia);
44
+ if (!m)
45
+ continue;
46
+ const repo = `${m[2]}/${m[3]}`;
47
+ if (m[1].toLowerCase() === "github.com" || !esRepoValido(repo))
48
+ return null;
49
+ return { host: m[1], repo };
50
+ }
51
+ return null;
52
+ }
53
+ // Lo que se muestra de un `origin`: nunca el usuario:clave@ de una URL (podría ser un token). Las credenciales se quitan de la
54
+ // URL ENTERA y recién después se limita el tamaño (recortar antes dejaría pasar un `@` lejano con su clave a medias). Lo que no se
55
+ // puede interpretar con seguridad no se muestra crudo: sale un marcador neutro.
56
+ const TOPE_DEL_ORIGEN_MOSTRADO = 200;
57
+ export function origenParaMostrar(url) {
58
+ const limpia = url.trim();
59
+ const url_ = /^([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^/\s]*)(.*)$/s.exec(limpia);
60
+ let mostrable = null;
61
+ if (url_) {
62
+ // La autoridad llega hasta la primera `/`; el usuario:clave es todo lo anterior al ÚLTIMO `@` de la autoridad.
63
+ const arroba = url_[2].lastIndexOf("@");
64
+ const usuario = arroba === -1 ? "" : url_[2].slice(0, arroba).split(":")[0];
65
+ const host = arroba === -1 ? url_[2] : url_[2].slice(arroba + 1);
66
+ mostrable = `${url_[1]}${usuario === "git" ? "git@" : ""}${host}${url_[3]}`;
67
+ }
68
+ else if (/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:/.test(limpia)) {
69
+ mostrable = limpia; // `git@host:o/r`: el usuario de un scp no lleva clave
70
+ }
71
+ if (mostrable === null || /\s/.test(mostrable))
72
+ return "(an address I don't recognize)";
73
+ return mostrable.length > TOPE_DEL_ORIGEN_MOSTRADO ? mostrable.slice(0, TOPE_DEL_ORIGEN_MOSTRADO) + "…" : mostrable;
74
+ }
75
+ // El remoto de asociación explícita: la persona le dice una sola vez a qué repo de GitHub corresponde este clon cuando su
76
+ // `origin` no es una dirección de GitHub (alias de SSH, otro servidor). Con las mismas formas estrictas de `origin`;
77
+ // nunca se usa para traer nada.
78
+ export const REMOTO_DE_ASOCIACION = "supcoflow";
33
79
  export async function leerRepoLocal(git, cwd) {
34
80
  const top = await git(["rev-parse", "--show-toplevel"], cwd);
35
81
  const raiz = top.codigo === 0 ? top.stdout.trim() : "";
@@ -37,32 +83,68 @@ export async function leerRepoLocal(git, cwd) {
37
83
  return { repo: null, raiz: null, motivo: "sin_git" };
38
84
  const origin = await git(["remote", "get-url", "origin"], cwd);
39
85
  const url = origin.codigo === 0 ? origin.stdout.trim() : "";
86
+ const repoDelOrigen = url ? repoDelOrigin(url) : null;
87
+ // Si `origin` es de GitHub, gana `origin`, como siempre.
88
+ if (repoDelOrigen)
89
+ return { repo: repoDelOrigen, raiz };
90
+ const asociado = await git(["remote", "get-url", REMOTO_DE_ASOCIACION], cwd);
91
+ const urlAsociada = asociado.codigo === 0 ? asociado.stdout.trim() : "";
92
+ const repoAsociado = urlAsociada ? repoDelOrigin(urlAsociada) : null;
93
+ if (repoAsociado)
94
+ return { repo: repoAsociado, raiz };
95
+ const alias = url ? aliasDelOrigin(url) : null;
96
+ const comun = { repo: null, raiz, ...(url ? { origen: origenParaMostrar(url) } : {}), ...(urlAsociada ? { hayRemotoSupcoflow: true } : {}) };
40
97
  if (!url)
41
- return { repo: null, raiz, motivo: "sin_origin" };
42
- const repo = repoDelOrigin(url);
43
- if (!repo)
44
- return { repo: null, raiz, motivo: "origin_no_es_github" };
45
- return { repo, raiz };
98
+ return { ...comun, motivo: "sin_origin" };
99
+ if (alias)
100
+ return { ...comun, motivo: "origin_alias_ssh", alias };
101
+ return { ...comun, motivo: "origin_no_es_github" };
102
+ }
103
+ // El comando exacto que asocia este clon con un repo de GitHub. `add` si todavía no hay remoto `supcoflow`; `set-url` si
104
+ // hay uno que no es una dirección de GitHub.
105
+ export function comandoDeAsociacion(repo, hayRemoto) {
106
+ return `git remote ${hayRemoto ? "set-url" : "add"} ${REMOTO_DE_ASOCIACION} https://github.com/${repo}.git`;
46
107
  }
47
108
  const MOTIVOS = {
48
109
  sin_git: "this folder isn't a git repo",
49
110
  sin_origin: "the repo has no remote called \"origin\"",
50
111
  origin_no_es_github: "the \"origin\" remote isn't a GitHub repo",
112
+ origin_alias_ssh: "the \"origin\" remote isn't a GitHub repo",
51
113
  };
114
+ // La frase que dice la verdad sobre un clon que no se puede atar a `repo` (owner/repo de GitHub) y cómo atarlo.
115
+ export function frasePorQueNoAtaClon(local, repo) {
116
+ if (local.motivo === "sin_git")
117
+ return "This folder isn't a git clone.";
118
+ const sugerido = repo ?? local.alias?.repo ?? null;
119
+ const como = sugerido === null ? ""
120
+ : ` If this is ${sugerido} on GitHub, tell me once: \`${comandoDeAsociacion(sugerido, local.hayRemotoSupcoflow === true)}\` (it's never fetched).`;
121
+ if (local.motivo === "origin_alias_ssh" && repo === null && local.alias) {
122
+ return `Your origin uses the SSH alias \`${local.alias.host}\`, which I don't resolve.${como}`;
123
+ }
124
+ const clon = local.origen ? `This clone is \`${local.origen}\`, which I can't tie to github.com${repo ? `/${repo}` : ""}.` : "This clone has no \"origin\" remote I can tie to GitHub.";
125
+ return `${clon}${como}`;
126
+ }
52
127
  export async function repoPedido(d) {
53
128
  const local = await leerRepoLocal(d.git, d.cwd);
54
129
  if (d.flagRepo !== null) {
55
130
  if (!esRepoValido(d.flagRepo)) {
56
131
  throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "repo_invalido", "The value of --repo must be owner/repo (for example owner/my-app), without the URL.");
57
132
  }
58
- // La raíz sólo se da si el origin local es ESE repo: con otro, lo que se escriba en esta
133
+ // La raíz sólo se da si el repo local reconocido es ESE repo: con otro, lo que se escriba en esta
59
134
  // carpeta (el archivo del proyecto) no le pertenece.
60
135
  const mismo = local.repo !== null && local.repo.toLowerCase() === d.flagRepo.toLowerCase();
61
- return { repo: d.flagRepo, raiz: mismo ? local.raiz : null };
136
+ if (mismo)
137
+ return { repo: d.flagRepo, raiz: local.raiz };
138
+ // El mensaje de «no es tu clon» dice por qué, y deja el comando exacto para atarlo si la persona SABE que lo es.
139
+ const sinClon = local.repo !== null
140
+ ? `This clone is github.com/${local.repo}, not ${d.flagRepo}.`
141
+ : frasePorQueNoAtaClon(local, d.flagRepo);
142
+ return { repo: d.flagRepo, raiz: null, sinClon };
62
143
  }
63
144
  if (local.repo === null) {
64
145
  const porque = local.motivo ? MOTIVOS[local.motivo] : "the folder couldn't be read";
65
- throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "sin_repo", `I couldn't find a GitHub repo here (${porque}). Pass --repo owner/repo.`);
146
+ const detalle = local.motivo === "origin_alias_ssh" ? ` ${frasePorQueNoAtaClon(local, null)}` : "";
147
+ throw new FalloDeLaTerminal(SALIDA.entradaInvalida, "sin_repo", `I couldn't find a GitHub repo here (${porque}).${detalle} Pass --repo owner/repo.`);
66
148
  }
67
149
  return { repo: local.repo, raiz: local.raiz };
68
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supcoflow",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "La terminal de Supcoflow: iniciar sesión y manejar el chat de soporte desde la consola o un asistente.",
5
5
  "type": "module",
6
6
  "bin": { "supcoflow": "dist/cli.js" },