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 +15 -20
- package/package.json +4 -1
- package/src/index.js +80 -23
- package/src/npmrc.js +23 -0
- package/src/nuget.js +97 -0
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
| `--
|
|
53
|
-
| `--
|
|
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.
|
|
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 {
|
|
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;
|
|
@@ -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(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|