it4-tools 0.1.2 → 0.1.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.
package/README.md CHANGED
@@ -43,14 +43,15 @@ ou `it4 auth login` (mesmo caminho).
43
43
 
44
44
  ## Flags
45
45
 
46
- | Flag | Default | Descrição |
47
- | ----------------------- | --------------- | --------------------------------------------------------------------------------------------------- |
48
- | `--no-install` | — | Configura `.npmrc` mas não instala o CLI globalmente. |
49
- | `--cli-version <v>` | `latest` | Versão específica do `@it4solution/tools` a instalar. |
50
- | `--pat-validity <dias>` | `90` | Validade do PAT em dias (máx 365). |
51
- | `--scope <s>` | `vso.packaging` | Scope do PAT (`vso.packaging` = read; `vso.packaging_write` = read+write, necessário pra publicar). |
52
- | `--dry-run` | — | Autentica e mostra o `.npmrc` que seria escrito, sem salvar. |
53
- | `--help`, `-h` | — | Mostra essa ajuda. |
46
+ | Flag | Default | Descrição |
47
+ | ----------------------- | --------------- | ---------------------------------------------------------------------------------------------------- |
48
+ | `--no-install` | — | Configura `.npmrc` mas não instala o CLI globalmente. |
49
+ | `--cli-version <v>` | `latest` | Versão específica do `@it4solution/tools` a instalar. |
50
+ | `--pat-validity <dias>` | `90` | Validade do PAT em dias (máx 365). |
51
+ | `--scope <s>` | `vso.packaging` | Scope do PAT (`vso.packaging` = read; `vso.packaging_write` = read+write, necessário pra publicar). |
52
+ | `--no-nuget` | — | Não grava a credencial do feed NuGet no NuGet.Config do usuário (sem `dotnet`, o passo já é pulado). |
53
+ | `--dry-run` | — | Mostra o `.npmrc` que seria escrito (segredos mascarados), sem logar, criar PAT nem salvar. |
54
+ | `--help`, `-h` | — | Mostra essa ajuda. |
54
55
 
55
56
  ## Troubleshooting
56
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "it4-tools",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Bootstrap público: autentica no Azure Artifacts da IT4 e instala @it4solution/tools.",
5
5
  "license": "ISC",
6
6
  "author": "Diego Andrade",
@@ -24,6 +24,9 @@
24
24
  "access": "public",
25
25
  "registry": "https://registry.npmjs.org/"
26
26
  },
27
+ "scripts": {
28
+ "test": "node --test test/npmrc.test.js test/nuget.test.js"
29
+ },
27
30
  "dependencies": {
28
31
  "@azure/identity": "^4.4.1",
29
32
  "@clack/prompts": "^1.4.0",
package/src/index.js CHANGED
@@ -3,7 +3,20 @@ import { spawn } from "node:child_process";
3
3
  import { cancel, confirm, intro, isCancel, log, outro, spinner } from "@clack/prompts";
4
4
  import pc from "picocolors";
5
5
  import { exchangeForPat, getBearerToken } from "./auth.js";
6
- import { buildNpmrc, NPMRC_PATH, readNpmrc, summarizeChanges, writeNpmrc } from "./npmrc.js";
6
+ import {
7
+ hasDotnet,
8
+ NUGET_SOURCE_NAME,
9
+ userNuGetConfigPath,
10
+ writeNuGetCredential,
11
+ } from "./nuget.js";
12
+ import {
13
+ buildNpmrc,
14
+ maskSecrets,
15
+ NPMRC_PATH,
16
+ readNpmrc,
17
+ summarizeChanges,
18
+ writeNpmrc,
19
+ } from "./npmrc.js";
7
20
 
8
21
  const DEFAULTS = {
9
22
  install: true,
@@ -11,6 +24,7 @@ const DEFAULTS = {
11
24
  patValidityDays: 90,
12
25
  scope: "vso.packaging",
13
26
  dryRun: false,
27
+ nuget: true,
14
28
  };
15
29
 
16
30
  function printHelp() {
@@ -21,11 +35,14 @@ Uso:
21
35
 
22
36
  Opções:
23
37
  --no-install Configura .npmrc mas não instala @it4solution/tools.
38
+ --no-nuget Não grava a credencial do feed NuGet (NuGet.Config do
39
+ usuário). Sem dotnet instalado, o passo já é pulado.
24
40
  --cli-version <v> Versão do CLI a instalar (default: latest).
25
41
  --pat-validity <dias> Validade do PAT em dias (default: 90, máx 365).
26
42
  --scope <s> Scope do PAT: vso.packaging | vso.packaging_write
27
43
  (default: vso.packaging).
28
- --dry-run Mostra o .npmrc resultante sem escrever.
44
+ --dry-run Mostra o .npmrc resultante sem logar, sem criar PAT
45
+ e sem escrever (segredos mascarados).
29
46
  -h, --help Mostra essa ajuda.
30
47
 
31
48
  Detalhes: README.md ou docs/specs/04-auth-bootstrap.md
@@ -40,6 +57,9 @@ function parseArgs(argv) {
40
57
  case "--no-install":
41
58
  args.install = false;
42
59
  break;
60
+ case "--no-nuget":
61
+ args.nuget = false;
62
+ break;
43
63
  case "--dry-run":
44
64
  args.dryRun = true;
45
65
  break;
@@ -126,11 +146,61 @@ function runNpmInstallGlobalCli(version) {
126
146
  });
127
147
  }
128
148
 
149
+ /**
150
+ * O mesmo PAT (`vso.packaging`) vale para o feed NuGet. Gravado no NuGet.Config
151
+ * do usuário, dispensa PAT no NuGet.config do repo. Falha aqui não derruba o
152
+ * setup: o npm já está configurado.
153
+ */
154
+ async function configureNuGet(pat) {
155
+ if (!(await hasDotnet())) {
156
+ log.info("dotnet não encontrado: pulei a credencial NuGet (só o npm foi configurado).");
157
+ return;
158
+ }
159
+ const s = spinner();
160
+ s.start("Gravando credencial do feed NuGet...");
161
+ try {
162
+ const configFile = await writeNuGetCredential(pat);
163
+ const storage = process.platform === "win32" ? "cifrada pelo Windows" : "fora de qualquer repo";
164
+ s.stop(`Credencial NuGet gravada em ${configFile} (${storage}).`);
165
+ } catch (err) {
166
+ s.stop("Falha ao gravar a credencial NuGet.", 1);
167
+ log.warn(`${err.message} O npm já está configurado; rode de novo para tentar o NuGet.`);
168
+ }
169
+ }
170
+
171
+ const DRY_RUN_PAT = "<PAT-criado-no-setup-real>";
172
+
173
+ /**
174
+ * Dry-run sem efeito colateral: nada de device flow nem de PAT. Criar o PAT aqui
175
+ * emitiria uma credencial válida por 90 dias só para exibi-la.
176
+ */
177
+ async function dryRun(opts) {
178
+ const existing = await readNpmrc();
179
+ const newContent = buildNpmrc(existing, DRY_RUN_PAT);
180
+ const summary = summarizeChanges(existing, "");
181
+ log.info(`Dry-run: não loga, não cria PAT e não escreve ${NPMRC_PATH}.`);
182
+ log.info(`Conteúdo que seria escrito (segredos mascarados):\n\n${maskSecrets(newContent)}`);
183
+ if (summary.replaced) {
184
+ log.warn(`Linhas IT4 antigas que seriam removidas: ${summary.removedLines.length}.`);
185
+ }
186
+ if (opts.nuget && (await hasDotnet())) {
187
+ log.info(
188
+ `Credencial NuGet: a fonte "${NUGET_SOURCE_NAME}" seria gravada em ${userNuGetConfigPath()}.`,
189
+ );
190
+ }
191
+ outro(pc.dim("Dry-run completo."));
192
+ }
193
+
129
194
  async function main() {
130
195
  const opts = parseArgs(process.argv);
131
196
 
132
197
  intro(pc.bgBlue(pc.white(" IT4 Tools — setup ")));
133
198
 
199
+ if (opts.dryRun) {
200
+ await dryRun(opts);
201
+ return;
202
+ }
203
+
134
204
  log.step("Autenticação Azure AD (device code flow)");
135
205
  let bearer;
136
206
  try {
@@ -168,16 +238,6 @@ async function main() {
168
238
  const newContent = buildNpmrc(existing, pat.pat);
169
239
  const summary = summarizeChanges(existing, pat.pat);
170
240
 
171
- if (opts.dryRun) {
172
- log.info(`Dry-run — não vou escrever ${NPMRC_PATH}.`);
173
- log.info(`Conteúdo que seria escrito:\n\n${newContent}`);
174
- if (summary.replaced) {
175
- log.warn(`Linhas IT4 antigas que seriam removidas: ${summary.removedLines.length}.`);
176
- }
177
- outro(pc.dim("Dry-run completo."));
178
- return;
179
- }
180
-
181
241
  await writeNpmrc(newContent);
182
242
  if (summary.replaced) {
183
243
  log.success(
@@ -187,6 +247,8 @@ async function main() {
187
247
  log.success(`~/.npmrc configurado.`);
188
248
  }
189
249
 
250
+ if (opts.nuget) await configureNuGet(pat.pat);
251
+
190
252
  if (!opts.install) {
191
253
  outro(`Pronto. Rode ${pc.cyan("npm i -g @it4solution/tools")} quando quiser instalar o CLI.`);
192
254
  return;
package/src/npmrc.js CHANGED
@@ -49,6 +49,29 @@ export function buildNpmrc(existingContent, pat) {
49
49
  return result.join("\n") + "\n";
50
50
  }
51
51
 
52
+ const SECRET_LINE = /^(\s*[^=]*?(?:_authToken|_password|_auth)\s*=\s*)(.*)$/i;
53
+
54
+ /**
55
+ * Mascara os segredos de um conteúdo de .npmrc antes de exibi-lo.
56
+ *
57
+ * O preview do dry-run mostra o arquivo inteiro, inclusive linhas de outros
58
+ * registries que o dev já tinha: sem máscara, esses tokens iriam parar no
59
+ * terminal e no transcript de quem rodou (humano ou agente).
60
+ */
61
+ export function maskSecrets(content) {
62
+ return content
63
+ .split(/\r?\n/)
64
+ .map((line) => line.replace(SECRET_LINE, (_, key, value) => `${key}${maskValue(value)}`))
65
+ .join("\n");
66
+ }
67
+
68
+ function maskValue(value) {
69
+ const v = value.trim();
70
+ if (v.startsWith("<")) return v;
71
+ if (v.length <= 8) return "****";
72
+ return `${v.slice(0, 4)}...${v.slice(-4)}`;
73
+ }
74
+
52
75
  export function summarizeChanges(existingContent, pat) {
53
76
  const before = (existingContent ?? "").split(/\r?\n/).filter((l) => isIt4Line(l));
54
77
  const replaced = before.length > 0;
package/src/nuget.js ADDED
@@ -0,0 +1,97 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ /**
7
+ * Fonte NuGet da IT4 como os repos a declaram. O NuGet casa credencial com fonte
8
+ * pelo NOME, não pela URL: gravada com este nome na config do usuário, ela vale
9
+ * para todo repo que declara a fonte sem credencial própria.
10
+ *
11
+ * Mesma lógica de `plugin-publish-nuget/src/lib/nuget.ts`, duplicada porque o
12
+ * bootstrap é público e não depende dos pacotes do feed.
13
+ */
14
+ export const NUGET_SOURCE_NAME = "IT4-artifacts-packages";
15
+ export const NUGET_SOURCE_URL =
16
+ "https://it4solution.pkgs.visualstudio.com/IT4360/_packaging/IT4-packages/nuget/v3/index.json";
17
+
18
+ export function userNuGetConfigPath(
19
+ platform = process.platform,
20
+ env = process.env,
21
+ home = os.homedir(),
22
+ ) {
23
+ if (platform === "win32") {
24
+ const appData = env.APPDATA ?? path.win32.join(home, "AppData", "Roaming");
25
+ return path.win32.join(appData, "NuGet", "NuGet.Config");
26
+ }
27
+ return path.posix.join(home, ".nuget", "NuGet", "NuGet.Config");
28
+ }
29
+
30
+ /**
31
+ * Argumentos do `dotnet nuget add|update source`. No Windows a senha sai
32
+ * cifrada (DPAPI, default do dotnet); texto claro só onde o dotnet não cifra.
33
+ */
34
+ export function buildSourceArgs(action, { pat, configFile }, platform = process.platform) {
35
+ const target =
36
+ action === "add"
37
+ ? [NUGET_SOURCE_URL, "--name", NUGET_SOURCE_NAME]
38
+ : [NUGET_SOURCE_NAME, "--source", NUGET_SOURCE_URL];
39
+ const args = [
40
+ "nuget",
41
+ action,
42
+ "source",
43
+ ...target,
44
+ "--username",
45
+ "it4-tools",
46
+ "--password",
47
+ pat,
48
+ "--configfile",
49
+ configFile,
50
+ ];
51
+ if (platform !== "win32") args.push("--store-password-in-clear-text");
52
+ return args;
53
+ }
54
+
55
+ // Mesmo conteúdo que o NuGet gera no primeiro uso: sem o nuget.org, criar o
56
+ // arquivo tiraria a fonte pública do restore do dev.
57
+ const DEFAULT_USER_CONFIG = `<?xml version="1.0" encoding="utf-8"?>
58
+ <configuration>
59
+ <packageSources>
60
+ <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
61
+ </packageSources>
62
+ </configuration>
63
+ `;
64
+
65
+ /** `dotnet` é executável de verdade: sem shell, o PAT não passa pela linha do cmd. */
66
+ function runDotnet(args) {
67
+ return new Promise((resolve) => {
68
+ const child = spawn("dotnet", args, { stdio: ["ignore", "pipe", "pipe"] });
69
+ let output = "";
70
+ child.stdout.on("data", (chunk) => (output += chunk));
71
+ child.stderr.on("data", (chunk) => (output += chunk));
72
+ child.on("error", (err) => resolve({ code: -1, output: err.message }));
73
+ child.on("close", (code) => resolve({ code: code ?? 1, output }));
74
+ });
75
+ }
76
+
77
+ export async function hasDotnet() {
78
+ return (await runDotnet(["--version"])).code === 0;
79
+ }
80
+
81
+ /** Grava a credencial da fonte IT4 no NuGet.Config do usuário. Devolve o caminho. */
82
+ export async function writeNuGetCredential(pat) {
83
+ const configFile = userNuGetConfigPath();
84
+ try {
85
+ await fs.access(configFile);
86
+ } catch {
87
+ await fs.mkdir(path.dirname(configFile), { recursive: true });
88
+ await fs.writeFile(configFile, DEFAULT_USER_CONFIG, "utf8");
89
+ }
90
+ const added = await runDotnet(buildSourceArgs("add", { pat, configFile }));
91
+ if (added.code !== 0) {
92
+ // A fonte já existe na config do usuário: atualiza URL e credencial.
93
+ const updated = await runDotnet(buildSourceArgs("update", { pat, configFile }));
94
+ if (updated.code !== 0) throw new Error(updated.output.trim() || added.output.trim());
95
+ }
96
+ return configFile;
97
+ }