it4-tools 0.1.2 → 0.1.4

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
@@ -26,31 +26,26 @@ Fluxo:
26
26
  que não é IT4.
27
27
  4. **`npm i -g @it4solution/tools`** — instala o **core** do CLI (bin `it4`).
28
28
 
29
- O CLI é **core + plugins por capacidade**: o core traz só `auth`/`config`.
30
- Instale os plugins que for usar (o mesmo `~/.npmrc` configurado acima
31
- autentica o download deles):
29
+ O CLI é **core + plugins por capacidade**, e os plugins (`create`, `skills`,
30
+ `publish`, `task`, `library`, `docs`, `workspace`) já aparecem no `it4 --help` e se
31
+ **instalam sozinhos no primeiro uso**, do mesmo feed que o `~/.npmrc` acima autentica.
32
+ Não há nada para instalar à mão.
32
33
 
33
- ```bash
34
- it4 plugins install @it4solution/plugin-create # cria projetos
35
- it4 plugins install @it4solution/plugin-skills # skills do Claude Code
36
- it4 plugins install @it4solution/plugin-publish-nuget # publica libs .NET
37
- it4 plugins # lista o que está instalado
38
- ```
39
-
40
- Depois disso, `it4 --help` mostra os comandos disponíveis. Pra re-logar
34
+ `it4 --help` mostra os comandos disponíveis. Pra re-logar
41
35
  quando o PAT expirar (em ~90 dias), rode de novo `npx -y it4-tools`
42
- ou `it4 auth login` (mesmo caminho).
36
+ ou `it4 auth` (mesmo caminho).
43
37
 
44
38
  ## Flags
45
39
 
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. |
40
+ | Flag | Default | Descrição |
41
+ | ----------------------- | --------------- | ---------------------------------------------------------------------------------------------------- |
42
+ | `--no-install` | — | Configura `.npmrc` mas não instala o CLI globalmente. |
43
+ | `--cli-version <v>` | `latest` | Versão específica do `@it4solution/tools` a instalar. |
44
+ | `--pat-validity <dias>` | `90` | Validade do PAT em dias (máx 365). |
45
+ | `--scope <s>` | `vso.packaging` | Scope do PAT (`vso.packaging` = read; `vso.packaging_write` = read+write, necessário pra publicar). |
46
+ | `--no-nuget` | — | Não grava a credencial do feed NuGet no NuGet.Config do usuário (sem `dotnet`, o passo já é pulado). |
47
+ | `--dry-run` | — | Mostra o `.npmrc` que seria escrito (segredos mascarados), sem logar, criar PAT nem salvar. |
48
+ | `--help`, `-h` | — | Mostra essa ajuda. |
54
49
 
55
50
  ## Troubleshooting
56
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "it4-tools",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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;
@@ -78,18 +98,13 @@ function parseArgs(argv) {
78
98
  return args;
79
99
  }
80
100
 
81
- const PLUGINS = [
82
- ["@it4solution/plugin-create", "cria projetos (frontend, backend, monorepo)"],
83
- ["@it4solution/plugin-skills", "instala/atualiza skills do Claude Code"],
84
- ["@it4solution/plugin-publish-nuget", "publica libs .NET (nuget)"],
85
- ];
86
-
87
101
  function printPluginsHint() {
88
- log.info("O CLI é core + plugins. Instale só as capacidades que for usar:");
89
- for (const [name, desc] of PLUGINS) {
90
- log.message(` ${pc.cyan(`it4 plugins install ${name}`)} ${pc.dim(`# ${desc}`)}`);
91
- }
92
- log.message(` ${pc.dim("Liste o que está instalado com")} ${pc.cyan("it4 plugins")}.`);
102
+ log.info(
103
+ "O CLI é core + plugins, e os plugins se instalam sozinhos no primeiro uso de cada comando.",
104
+ );
105
+ log.message(
106
+ ` ${pc.cyan("it4 --help")} ${pc.dim("# lista todos os comandos, instalados ou não")}`,
107
+ );
93
108
  }
94
109
 
95
110
  function printSkillHint() {
@@ -126,11 +141,61 @@ function runNpmInstallGlobalCli(version) {
126
141
  });
127
142
  }
128
143
 
144
+ /**
145
+ * O mesmo PAT (`vso.packaging`) vale para o feed NuGet. Gravado no NuGet.Config
146
+ * do usuário, dispensa PAT no NuGet.config do repo. Falha aqui não derruba o
147
+ * setup: o npm já está configurado.
148
+ */
149
+ async function configureNuGet(pat) {
150
+ if (!(await hasDotnet())) {
151
+ log.info("dotnet não encontrado: pulei a credencial NuGet (só o npm foi configurado).");
152
+ return;
153
+ }
154
+ const s = spinner();
155
+ s.start("Gravando credencial do feed NuGet...");
156
+ try {
157
+ const configFile = await writeNuGetCredential(pat);
158
+ const storage = process.platform === "win32" ? "cifrada pelo Windows" : "fora de qualquer repo";
159
+ s.stop(`Credencial NuGet gravada em ${configFile} (${storage}).`);
160
+ } catch (err) {
161
+ s.stop("Falha ao gravar a credencial NuGet.", 1);
162
+ log.warn(`${err.message} O npm já está configurado; rode de novo para tentar o NuGet.`);
163
+ }
164
+ }
165
+
166
+ const DRY_RUN_PAT = "<PAT-criado-no-setup-real>";
167
+
168
+ /**
169
+ * Dry-run sem efeito colateral: nada de device flow nem de PAT. Criar o PAT aqui
170
+ * emitiria uma credencial válida por 90 dias só para exibi-la.
171
+ */
172
+ async function dryRun(opts) {
173
+ const existing = await readNpmrc();
174
+ const newContent = buildNpmrc(existing, DRY_RUN_PAT);
175
+ const summary = summarizeChanges(existing, "");
176
+ log.info(`Dry-run: não loga, não cria PAT e não escreve ${NPMRC_PATH}.`);
177
+ log.info(`Conteúdo que seria escrito (segredos mascarados):\n\n${maskSecrets(newContent)}`);
178
+ if (summary.replaced) {
179
+ log.warn(`Linhas IT4 antigas que seriam removidas: ${summary.removedLines.length}.`);
180
+ }
181
+ if (opts.nuget && (await hasDotnet())) {
182
+ log.info(
183
+ `Credencial NuGet: a fonte "${NUGET_SOURCE_NAME}" seria gravada em ${userNuGetConfigPath()}.`,
184
+ );
185
+ }
186
+ outro(pc.dim("Dry-run completo."));
187
+ }
188
+
129
189
  async function main() {
130
190
  const opts = parseArgs(process.argv);
131
191
 
132
192
  intro(pc.bgBlue(pc.white(" IT4 Tools — setup ")));
133
193
 
194
+ if (opts.dryRun) {
195
+ await dryRun(opts);
196
+ return;
197
+ }
198
+
134
199
  log.step("Autenticação Azure AD (device code flow)");
135
200
  let bearer;
136
201
  try {
@@ -168,16 +233,6 @@ async function main() {
168
233
  const newContent = buildNpmrc(existing, pat.pat);
169
234
  const summary = summarizeChanges(existing, pat.pat);
170
235
 
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
236
  await writeNpmrc(newContent);
182
237
  if (summary.replaced) {
183
238
  log.success(
@@ -187,6 +242,8 @@ async function main() {
187
242
  log.success(`~/.npmrc configurado.`);
188
243
  }
189
244
 
245
+ if (opts.nuget) await configureNuGet(pat.pat);
246
+
190
247
  if (!opts.install) {
191
248
  outro(`Pronto. Rode ${pc.cyan("npm i -g @it4solution/tools")} quando quiser instalar o CLI.`);
192
249
  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
+ }