it4-tools 0.1.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/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # it4-tools (bootstrap público)
2
+
3
+ Bootstrap de auth da IT4 Solution. Roda **uma vez** numa máquina nova pra
4
+ configurar o `~/.npmrc` apontando pro feed Azure Artifacts privado e (por
5
+ default) instalar globalmente `@it4solution/tools`.
6
+
7
+ > Não confundir com `@it4solution/tools` — este package (`it4-tools`,
8
+ > unscoped, no npmjs.com público) só faz **bootstrap**. A CLI real é o
9
+ > `@it4solution/tools` (privado, feed Azure), que esse bootstrap instala
10
+ > no final.
11
+
12
+ ## Uso
13
+
14
+ ```bash
15
+ npx -y it4-tools
16
+ ```
17
+
18
+ Fluxo:
19
+
20
+ 1. **Device code flow** — abre uma URL + código no navegador pra você logar
21
+ com sua conta IT4 (Azure AD).
22
+ 2. **Cria um PAT** (Personal Access Token) no Azure DevOps com scope
23
+ `vso.packaging` (read), válido por 90 dias, com `displayName="it4-tools (<hostname>)"`.
24
+ 3. **Escreve `~/.npmrc`** com o token. Migra config IT4 antiga (substitui
25
+ linhas do feed IT4 com token vencido/compartilhado), preserva tudo
26
+ que não é IT4.
27
+ 4. **`npm i -g @it4solution/tools`** — instala o CLI real.
28
+
29
+ Depois disso, `it4 --help` mostra os comandos disponíveis. Pra re-logar
30
+ quando o PAT expirar (em ~90 dias), rode de novo `npx -y it4-tools`
31
+ ou `it4 auth login` (mesmo caminho).
32
+
33
+ ## Flags
34
+
35
+ | Flag | Default | Descrição |
36
+ | ----------------------- | --------------- | --------------------------------------------------------------------------------------------------- |
37
+ | `--no-install` | — | Configura `.npmrc` mas não instala o CLI globalmente. |
38
+ | `--cli-version <v>` | `latest` | Versão específica do `@it4solution/tools` a instalar. |
39
+ | `--pat-validity <dias>` | `90` | Validade do PAT em dias (máx 365). |
40
+ | `--scope <s>` | `vso.packaging` | Scope do PAT (`vso.packaging` = read; `vso.packaging_write` = read+write, necessário pra publicar). |
41
+ | `--dry-run` | — | Autentica e mostra o `.npmrc` que seria escrito, sem salvar. |
42
+ | `--help`, `-h` | — | Mostra essa ajuda. |
43
+
44
+ ## Troubleshooting
45
+
46
+ - **Browser não abre / sem internet no terminal**: copie a URL e o código
47
+ manualmente pra outra máquina/celular e cole lá. O device flow tolera
48
+ ambientes headless por design.
49
+ - **`AADSTS...` no login**: sua conta provavelmente não tem permissão no
50
+ tenant da IT4. Fale com o time de TI/identidade.
51
+ - **PAT criado mas `npm install` ainda dá 401**: confira `npm config get
52
+ userconfig` — deve apontar pro mesmo `~/.npmrc` que o bootstrap escreveu.
53
+ Em alguns setups (corp Windows com `npmrc` em `AppData\Roaming`), pode
54
+ haver divergência.
55
+
56
+ ## Mais detalhes
57
+
58
+ Spec canônica: ver `docs/specs/04-auth-bootstrap.md` no repo
59
+ `@it4solution/tools`.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "it4-tools",
3
+ "version": "0.1.0",
4
+ "description": "Bootstrap público: autentica no Azure Artifacts da IT4 e instala @it4solution/tools.",
5
+ "license": "ISC",
6
+ "author": "Diego Andrade",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "bin": {
10
+ "it4-tools": "src/index.js"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://it4solution@dev.azure.com/it4solution/IT4360/_git/IT4.Tools"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "registry": "https://registry.npmjs.org/"
26
+ },
27
+ "dependencies": {
28
+ "@azure/identity": "^4.4.1",
29
+ "@clack/prompts": "^1.4.0",
30
+ "picocolors": "^1.1.1"
31
+ }
32
+ }
package/src/auth.js ADDED
@@ -0,0 +1,68 @@
1
+ import os from "node:os";
2
+ import { DeviceCodeCredential } from "@azure/identity";
3
+
4
+ // Public clientId da Azure CLI — reusado por inúmeras ferramentas de dev.
5
+ // V2: registrar app dedicado "IT4 Tools" no Entra ID e trocar aqui (ver
6
+ // docs/specs/04-auth-bootstrap.md, tabela "Defaults documentados").
7
+ const AZURE_CLI_CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";
8
+
9
+ // Resource ID do Azure DevOps (constante global da Microsoft).
10
+ const AZURE_DEVOPS_RESOURCE = "499b84ac-1321-427f-aa17-267ca6975798/.default";
11
+
12
+ const IT4_ORG = "it4solution";
13
+ const PAT_ENDPOINT = `https://vssps.dev.azure.com/${IT4_ORG}/_apis/tokens/pats?api-version=7.1-preview.1`;
14
+
15
+ export async function getBearerToken({ onPrompt }) {
16
+ const credential = new DeviceCodeCredential({
17
+ tenantId: "organizations",
18
+ clientId: AZURE_CLI_CLIENT_ID,
19
+ userPromptCallback: ({ verificationUri, userCode }) => {
20
+ onPrompt({ verificationUri, userCode });
21
+ },
22
+ });
23
+ const token = await credential.getToken(AZURE_DEVOPS_RESOURCE);
24
+ if (!token?.token) {
25
+ throw new Error("Azure AD não retornou um token.");
26
+ }
27
+ return token.token;
28
+ }
29
+
30
+ export async function exchangeForPat({ bearerToken, validityDays, scope }) {
31
+ const validTo = new Date(Date.now() + validityDays * 24 * 60 * 60 * 1000).toISOString();
32
+ const displayName = `it4-tools (${os.hostname()})`;
33
+
34
+ const res = await fetch(PAT_ENDPOINT, {
35
+ method: "POST",
36
+ headers: {
37
+ Authorization: `Bearer ${bearerToken}`,
38
+ "Content-Type": "application/json",
39
+ Accept: "application/json",
40
+ },
41
+ body: JSON.stringify({
42
+ displayName,
43
+ scope,
44
+ validTo,
45
+ allOrgs: false,
46
+ }),
47
+ });
48
+
49
+ if (!res.ok) {
50
+ const body = await res.text().catch(() => "");
51
+ throw new Error(`Falha ao criar PAT (HTTP ${res.status} ${res.statusText}): ${body}`);
52
+ }
53
+
54
+ const data = await res.json();
55
+ if (data.patTokenError && data.patTokenError !== "none") {
56
+ throw new Error(`Azure DevOps recusou criar PAT: ${data.patTokenError}`);
57
+ }
58
+ if (!data.patToken?.token) {
59
+ throw new Error("Resposta do Azure DevOps não trouxe o PAT.");
60
+ }
61
+
62
+ return {
63
+ pat: data.patToken.token,
64
+ displayName: data.patToken.displayName,
65
+ validTo: data.patToken.validTo,
66
+ scope: data.patToken.scope,
67
+ };
68
+ }
package/src/index.js ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { cancel, confirm, intro, isCancel, log, outro, spinner } from "@clack/prompts";
4
+ import pc from "picocolors";
5
+ import { exchangeForPat, getBearerToken } from "./auth.js";
6
+ import { buildNpmrc, NPMRC_PATH, readNpmrc, summarizeChanges, writeNpmrc } from "./npmrc.js";
7
+
8
+ const DEFAULTS = {
9
+ install: true,
10
+ cliVersion: "latest",
11
+ patValidityDays: 90,
12
+ scope: "vso.packaging",
13
+ dryRun: false,
14
+ };
15
+
16
+ function printHelp() {
17
+ process.stdout.write(`it4-tools — bootstrap de auth da IT4 Solution
18
+
19
+ Uso:
20
+ npx -y it4-tools [opções]
21
+
22
+ Opções:
23
+ --no-install Configura .npmrc mas não instala @it4solution/tools.
24
+ --cli-version <v> Versão do CLI a instalar (default: latest).
25
+ --pat-validity <dias> Validade do PAT em dias (default: 90, máx 365).
26
+ --scope <s> Scope do PAT: vso.packaging | vso.packaging_write
27
+ (default: vso.packaging).
28
+ --dry-run Mostra o .npmrc resultante sem escrever.
29
+ -h, --help Mostra essa ajuda.
30
+
31
+ Detalhes: README.md ou docs/specs/04-auth-bootstrap.md
32
+ `);
33
+ }
34
+
35
+ function parseArgs(argv) {
36
+ const args = { ...DEFAULTS };
37
+ for (let i = 2; i < argv.length; i++) {
38
+ const a = argv[i];
39
+ switch (a) {
40
+ case "--no-install":
41
+ args.install = false;
42
+ break;
43
+ case "--dry-run":
44
+ args.dryRun = true;
45
+ break;
46
+ case "-h":
47
+ case "--help":
48
+ printHelp();
49
+ process.exit(0);
50
+ break;
51
+ case "--cli-version":
52
+ args.cliVersion = argv[++i];
53
+ break;
54
+ case "--pat-validity": {
55
+ const n = Number.parseInt(argv[++i], 10);
56
+ if (!Number.isFinite(n) || n < 1 || n > 365) {
57
+ console.error(`Flag --pat-validity inválida: precisa ser 1..365.`);
58
+ process.exit(1);
59
+ }
60
+ args.patValidityDays = n;
61
+ break;
62
+ }
63
+ case "--scope": {
64
+ const v = argv[++i];
65
+ if (v !== "vso.packaging" && v !== "vso.packaging_write") {
66
+ console.error(`Flag --scope inválida: use "vso.packaging" ou "vso.packaging_write".`);
67
+ process.exit(1);
68
+ }
69
+ args.scope = v;
70
+ break;
71
+ }
72
+ default:
73
+ console.error(`Flag desconhecida: ${a}`);
74
+ printHelp();
75
+ process.exit(1);
76
+ }
77
+ }
78
+ return args;
79
+ }
80
+
81
+ function bail(message) {
82
+ cancel(message);
83
+ process.exit(0);
84
+ }
85
+
86
+ function unwrap(value, cancelMessage = "Cancelado.") {
87
+ if (isCancel(value)) bail(cancelMessage);
88
+ return value;
89
+ }
90
+
91
+ function runNpmInstallGlobalCli(version) {
92
+ const pkg =
93
+ version && version !== "latest" ? `@it4solution/tools@${version}` : "@it4solution/tools";
94
+ const args = ["install", "--global", pkg];
95
+ return new Promise((resolve, reject) => {
96
+ const child =
97
+ process.platform === "win32"
98
+ ? spawn(`npm ${args.join(" ")}`, { shell: true, stdio: "inherit" })
99
+ : spawn("npm", args, { stdio: "inherit" });
100
+ child.on("error", reject);
101
+ child.on("close", (code) => {
102
+ if (code === 0) resolve();
103
+ else reject(new Error(`npm install falhou (exit ${code}).`));
104
+ });
105
+ });
106
+ }
107
+
108
+ async function main() {
109
+ const opts = parseArgs(process.argv);
110
+
111
+ intro(pc.bgBlue(pc.white(" IT4 Tools — setup ")));
112
+
113
+ log.step("Autenticação Azure AD (device code flow)");
114
+ let bearer;
115
+ try {
116
+ bearer = await getBearerToken({
117
+ onPrompt: ({ verificationUri, userCode }) => {
118
+ log.info(`Abra ${pc.cyan(verificationUri)} no navegador.`);
119
+ log.info(`Cole o código: ${pc.bold(pc.yellow(userCode))}`);
120
+ log.info("Aguardando login...");
121
+ },
122
+ });
123
+ } catch (err) {
124
+ log.error(`Falha no login Azure AD: ${err.message}`);
125
+ process.exit(1);
126
+ }
127
+ log.success("Autenticado.");
128
+
129
+ const patSpinner = spinner();
130
+ patSpinner.start("Criando PAT no Azure DevOps...");
131
+ let pat;
132
+ try {
133
+ pat = await exchangeForPat({
134
+ bearerToken: bearer,
135
+ validityDays: opts.patValidityDays,
136
+ scope: opts.scope,
137
+ });
138
+ } catch (err) {
139
+ patSpinner.stop("Falha ao criar PAT.", 1);
140
+ log.error(err.message);
141
+ process.exit(1);
142
+ }
143
+ const validToShort = pat.validTo.slice(0, 10);
144
+ patSpinner.stop(`PAT criado (${pat.displayName}), válido até ${validToShort}.`);
145
+
146
+ const existing = await readNpmrc();
147
+ const newContent = buildNpmrc(existing, pat.pat);
148
+ const summary = summarizeChanges(existing, pat.pat);
149
+
150
+ if (opts.dryRun) {
151
+ log.info(`Dry-run — não vou escrever ${NPMRC_PATH}.`);
152
+ log.info(`Conteúdo que seria escrito:\n\n${newContent}`);
153
+ if (summary.replaced) {
154
+ log.warn(`Linhas IT4 antigas que seriam removidas: ${summary.removedLines.length}.`);
155
+ }
156
+ outro(pc.dim("Dry-run completo."));
157
+ return;
158
+ }
159
+
160
+ await writeNpmrc(newContent);
161
+ if (summary.replaced) {
162
+ log.success(
163
+ `~/.npmrc atualizado (${summary.removedLines.length} linha(s) IT4 antiga(s) substituída(s)).`,
164
+ );
165
+ } else {
166
+ log.success(`~/.npmrc configurado.`);
167
+ }
168
+
169
+ if (!opts.install) {
170
+ outro(`Pronto. Rode ${pc.cyan("npm i -g @it4solution/tools")} quando quiser instalar o CLI.`);
171
+ return;
172
+ }
173
+
174
+ const shouldInstall = unwrap(
175
+ await confirm({
176
+ message: "Instalar @it4solution/tools globalmente agora?",
177
+ initialValue: true,
178
+ }),
179
+ );
180
+
181
+ if (!shouldInstall) {
182
+ outro(
183
+ `Beleza. Quando quiser instalar: ${pc.cyan("npm i -g @it4solution/tools")}. Depois use ${pc.cyan("it4 --help")}.`,
184
+ );
185
+ return;
186
+ }
187
+
188
+ const installSpinner = spinner();
189
+ installSpinner.start(
190
+ `Instalando @it4solution/tools${opts.cliVersion !== "latest" ? `@${opts.cliVersion}` : ""}...`,
191
+ );
192
+ try {
193
+ await runNpmInstallGlobalCli(opts.cliVersion);
194
+ installSpinner.stop("@it4solution/tools instalado.");
195
+ } catch (err) {
196
+ installSpinner.stop("npm install falhou.", 1);
197
+ log.error(err.message);
198
+ log.info(
199
+ `Você ainda pode tentar manualmente: ${pc.cyan("npm i -g @it4solution/tools")}. O ~/.npmrc já foi configurado.`,
200
+ );
201
+ process.exit(1);
202
+ }
203
+
204
+ outro(`Pronto. Use ${pc.cyan("it4 --help")} pra ver os comandos disponíveis.`);
205
+ }
206
+
207
+ main().catch((err) => {
208
+ console.error(err);
209
+ process.exit(1);
210
+ });
package/src/npmrc.js ADDED
@@ -0,0 +1,61 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+
5
+ export const NPMRC_PATH = path.join(os.homedir(), ".npmrc");
6
+
7
+ const IT4_FEED_URL =
8
+ "it4solution.pkgs.visualstudio.com/IT4360/_packaging/IT4-packages/npm/registry/";
9
+ const IT4_HOST_PATTERN =
10
+ /^\/\/[^/]*(?:it4solution\.pkgs\.visualstudio\.com|pkgs\.dev\.azure\.com\/it4solution)/i;
11
+ const IT4_SCOPE_REGISTRY = /^@it4solution:registry\s*=/i;
12
+ const DEFAULT_REGISTRY = /^registry\s*=\s*https?:\/\/registry\.npmjs\.org/im;
13
+
14
+ export async function readNpmrc() {
15
+ try {
16
+ return await fs.readFile(NPMRC_PATH, "utf8");
17
+ } catch (err) {
18
+ if (err.code === "ENOENT") return "";
19
+ throw err;
20
+ }
21
+ }
22
+
23
+ export async function writeNpmrc(content) {
24
+ await fs.writeFile(NPMRC_PATH, content, "utf8");
25
+ }
26
+
27
+ function isIt4Line(line) {
28
+ const trimmed = line.trim();
29
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) return false;
30
+ if (IT4_SCOPE_REGISTRY.test(trimmed)) return true;
31
+ if (IT4_HOST_PATTERN.test(trimmed)) return true;
32
+ return false;
33
+ }
34
+
35
+ export function buildNpmrc(existingContent, pat) {
36
+ const lines = (existingContent ?? "").split(/\r?\n/);
37
+ const kept = lines.filter((line) => !isIt4Line(line));
38
+ while (kept.length > 0 && kept[kept.length - 1].trim() === "") kept.pop();
39
+
40
+ const it4Block = [
41
+ `@it4solution:registry=https://${IT4_FEED_URL}`,
42
+ `//${IT4_FEED_URL}:_authToken=${pat}`,
43
+ ];
44
+
45
+ const result = [...kept, ...it4Block];
46
+ if (!DEFAULT_REGISTRY.test(existingContent)) {
47
+ result.push("registry=https://registry.npmjs.org/");
48
+ }
49
+ return result.join("\n") + "\n";
50
+ }
51
+
52
+ export function summarizeChanges(existingContent, pat) {
53
+ const before = (existingContent ?? "").split(/\r?\n/).filter((l) => isIt4Line(l));
54
+ const replaced = before.length > 0;
55
+ return {
56
+ replaced,
57
+ removedLines: before,
58
+ feedUrl: `https://${IT4_FEED_URL}`,
59
+ patPreview: pat ? `${pat.slice(0, 4)}...${pat.slice(-4)}` : "",
60
+ };
61
+ }