it4-tools 0.1.1 → 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 +9 -8
- package/package.json +4 -1
- package/src/index.js +83 -12
- package/src/npmrc.js +23 -0
- package/src/nuget.js +97 -0
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
|
-
| `--
|
|
53
|
-
| `--
|
|
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.
|
|
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 {
|
|
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
|
|
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;
|
|
@@ -92,6 +112,13 @@ function printPluginsHint() {
|
|
|
92
112
|
log.message(` ${pc.dim("Liste o que está instalado com")} ${pc.cyan("it4 plugins")}.`);
|
|
93
113
|
}
|
|
94
114
|
|
|
115
|
+
function printSkillHint() {
|
|
116
|
+
log.info("Usa Claude Code? Instale a skill que ensina o Claude a operar o CLI:");
|
|
117
|
+
log.message(
|
|
118
|
+
` ${pc.cyan("it4 skills install --name it4-cli")} ${pc.dim("# comandos e flags do it4 direto no Claude Code")}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
function bail(message) {
|
|
96
123
|
cancel(message);
|
|
97
124
|
process.exit(0);
|
|
@@ -119,11 +146,61 @@ function runNpmInstallGlobalCli(version) {
|
|
|
119
146
|
});
|
|
120
147
|
}
|
|
121
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
|
+
|
|
122
194
|
async function main() {
|
|
123
195
|
const opts = parseArgs(process.argv);
|
|
124
196
|
|
|
125
197
|
intro(pc.bgBlue(pc.white(" IT4 Tools — setup ")));
|
|
126
198
|
|
|
199
|
+
if (opts.dryRun) {
|
|
200
|
+
await dryRun(opts);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
127
204
|
log.step("Autenticação Azure AD (device code flow)");
|
|
128
205
|
let bearer;
|
|
129
206
|
try {
|
|
@@ -161,16 +238,6 @@ async function main() {
|
|
|
161
238
|
const newContent = buildNpmrc(existing, pat.pat);
|
|
162
239
|
const summary = summarizeChanges(existing, pat.pat);
|
|
163
240
|
|
|
164
|
-
if (opts.dryRun) {
|
|
165
|
-
log.info(`Dry-run — não vou escrever ${NPMRC_PATH}.`);
|
|
166
|
-
log.info(`Conteúdo que seria escrito:\n\n${newContent}`);
|
|
167
|
-
if (summary.replaced) {
|
|
168
|
-
log.warn(`Linhas IT4 antigas que seriam removidas: ${summary.removedLines.length}.`);
|
|
169
|
-
}
|
|
170
|
-
outro(pc.dim("Dry-run completo."));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
241
|
await writeNpmrc(newContent);
|
|
175
242
|
if (summary.replaced) {
|
|
176
243
|
log.success(
|
|
@@ -180,6 +247,8 @@ async function main() {
|
|
|
180
247
|
log.success(`~/.npmrc configurado.`);
|
|
181
248
|
}
|
|
182
249
|
|
|
250
|
+
if (opts.nuget) await configureNuGet(pat.pat);
|
|
251
|
+
|
|
183
252
|
if (!opts.install) {
|
|
184
253
|
outro(`Pronto. Rode ${pc.cyan("npm i -g @it4solution/tools")} quando quiser instalar o CLI.`);
|
|
185
254
|
return;
|
|
@@ -195,6 +264,7 @@ async function main() {
|
|
|
195
264
|
if (!shouldInstall) {
|
|
196
265
|
log.info(`Quando quiser instalar o core: ${pc.cyan("npm i -g @it4solution/tools")}.`);
|
|
197
266
|
printPluginsHint();
|
|
267
|
+
printSkillHint();
|
|
198
268
|
outro(`Depois, ${pc.cyan("it4 --help")} lista os comandos.`);
|
|
199
269
|
return;
|
|
200
270
|
}
|
|
@@ -216,6 +286,7 @@ async function main() {
|
|
|
216
286
|
}
|
|
217
287
|
|
|
218
288
|
printPluginsHint();
|
|
289
|
+
printSkillHint();
|
|
219
290
|
outro(`Pronto. Depois de instalar os plugins, ${pc.cyan("it4 --help")} lista os comandos.`);
|
|
220
291
|
}
|
|
221
292
|
|
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
|
+
}
|