@gjsify/cli 0.14.0 → 0.16.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/dist/cli.gjs.mjs +45 -44
- package/lib/actions/build.js +24 -1
- package/lib/cli-app.js +2 -1
- package/lib/commands/foreach.d.ts +1 -0
- package/lib/commands/foreach.js +48 -13
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/install.js +132 -65
- package/lib/commands/login.d.ts +29 -0
- package/lib/commands/login.js +122 -96
- package/lib/commands/onboard.d.ts +14 -0
- package/lib/commands/onboard.js +453 -0
- package/lib/commands/publish.d.ts +92 -0
- package/lib/commands/publish.js +270 -295
- package/lib/commands/trust.d.ts +21 -0
- package/lib/commands/trust.js +40 -70
- package/lib/commands/workspace.d.ts +2 -0
- package/lib/commands/workspace.js +64 -14
- package/lib/utils/build-cache.d.ts +157 -0
- package/lib/utils/build-cache.js +670 -0
- package/lib/utils/install-backend-native.js +104 -9
- package/lib/utils/install-backend.d.ts +10 -0
- package/lib/utils/install-cache-fs.d.ts +9 -0
- package/lib/utils/install-cache-fs.js +20 -6
- package/lib/utils/install-lock.d.ts +15 -0
- package/lib/utils/install-lock.js +293 -0
- package/lib/utils/library-output.d.ts +30 -0
- package/lib/utils/library-output.js +92 -0
- package/lib/utils/npm-otp.d.ts +67 -0
- package/lib/utils/npm-otp.js +117 -0
- package/lib/utils/suggest.d.ts +13 -0
- package/lib/utils/suggest.js +51 -0
- package/package.json +19 -18
package/lib/commands/login.js
CHANGED
|
@@ -22,6 +22,13 @@ import { DEFAULT_REGISTRY, registryFor, whoami } from '@gjsify/npm-registry';
|
|
|
22
22
|
import { loadNpmrc } from '../utils/load-npmrc.js';
|
|
23
23
|
import { writeAuthToken } from '../utils/auth-npmrc.js';
|
|
24
24
|
import { promptCredentials, promptLine } from '../utils/prompt.js';
|
|
25
|
+
/** Thrown by {@link runLogin} on any recoverable login failure (bad creds, no token, …). */
|
|
26
|
+
export class LoginError extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'LoginError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
25
32
|
export const loginCommand = {
|
|
26
33
|
command: 'login',
|
|
27
34
|
description: "Log in to an npm registry (Node-free `npm login`). Prompts for username + password (hidden) and writes the token to ~/.npmrc. Use --otp for 2FA. Web-OAuth flow is not supported — this is npm's legacy credentials flow.",
|
|
@@ -38,105 +45,124 @@ export const loginCommand = {
|
|
|
38
45
|
.option('otp', { description: 'npm 2FA one-time code (prompted on demand if omitted).', type: 'string' })
|
|
39
46
|
.option('json', { description: 'Emit `{username, registry}` JSON on success.', type: 'boolean' }),
|
|
40
47
|
handler: async (args) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// sequence (Enter showing `^M`, or an empty username).
|
|
51
|
-
const { username, password } = await promptCredentials(args.username);
|
|
52
|
-
if (!username) {
|
|
53
|
-
console.error('gjsify login: a username is required.');
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
if (!password) {
|
|
57
|
-
console.error('gjsify login: a password is required.');
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
// npm legacy auth = HTTP Basic of `<username>:<password>` (the same
|
|
61
|
-
// credentials being logged in with). The password also lives in the
|
|
62
|
-
// PUT body, which is how the registry mints the token.
|
|
63
|
-
const basic = `Basic ${btoa(`${username}:${password}`)}`;
|
|
64
|
-
const couchUrl = `${registryClean}-/user/org.couchdb.user:${encodeURIComponent(username)}`;
|
|
65
|
-
const body = {
|
|
66
|
-
_id: `org.couchdb.user:${username}`,
|
|
67
|
-
name: username,
|
|
68
|
-
password,
|
|
69
|
-
type: 'user',
|
|
70
|
-
roles: [],
|
|
71
|
-
date: undefined, // the registry stamps this; Date is unavailable in some runtimes
|
|
72
|
-
};
|
|
73
|
-
function authHeaders(otp) {
|
|
74
|
-
const h = {
|
|
75
|
-
authorization: basic,
|
|
76
|
-
'content-type': 'application/json',
|
|
77
|
-
accept: '*/*',
|
|
78
|
-
};
|
|
79
|
-
if (otp)
|
|
80
|
-
h['npm-otp'] = otp;
|
|
81
|
-
return h;
|
|
82
|
-
}
|
|
83
|
-
async function putUser(path, otp) {
|
|
84
|
-
return fetch(`${couchUrl}${path}`, { method: 'PUT', headers: authHeaders(otp), body: JSON.stringify(body) });
|
|
85
|
-
}
|
|
86
|
-
let otp = args.otp;
|
|
87
|
-
let res = await putUser('', otp);
|
|
88
|
-
// 2FA: 401 + www-authenticate "otp" (or a one-time-pass body) → prompt + retry.
|
|
89
|
-
if (res.status === 401 && !otp) {
|
|
90
|
-
const wwwAuth = (res.headers.get('www-authenticate') ?? '').toLowerCase();
|
|
91
|
-
const text = await res.clone().text().catch(() => '');
|
|
92
|
-
if (wwwAuth.includes('otp') || /one-time pass/i.test(text)) {
|
|
93
|
-
otp = await promptLine(`This operation requires a one-time password.\nEnter OTP: `);
|
|
94
|
-
if (!otp) {
|
|
95
|
-
console.error('gjsify login: no OTP entered.');
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
res = await putUser('', otp);
|
|
48
|
+
try {
|
|
49
|
+
const { username, registry, npmrcPath } = await runLogin({
|
|
50
|
+
registry: args.registry,
|
|
51
|
+
scope: args.scope,
|
|
52
|
+
username: args.username,
|
|
53
|
+
otp: args.otp,
|
|
54
|
+
});
|
|
55
|
+
if (args.json) {
|
|
56
|
+
process.stdout.write(`${JSON.stringify({ username, registry })}\n`);
|
|
99
57
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// the current doc for its `_rev`, merge, and PUT to `…/-rev/<rev>`.
|
|
103
|
-
if (res.status === 409) {
|
|
104
|
-
const getRes = await fetch(`${couchUrl}?write=true`, { headers: authHeaders(otp) });
|
|
105
|
-
if (getRes.ok) {
|
|
106
|
-
const existing = (await getRes.json().catch(() => ({})));
|
|
107
|
-
for (const k of Object.keys(existing)) {
|
|
108
|
-
if (!body[k] || k === 'roles')
|
|
109
|
-
body[k] = existing[k];
|
|
110
|
-
}
|
|
111
|
-
if (body._rev)
|
|
112
|
-
res = await putUser(`/-rev/${body._rev}`, otp);
|
|
58
|
+
else {
|
|
59
|
+
process.stdout.write(`Logged in as ${username} on ${registry}\n(token written to ${npmrcPath})\n`);
|
|
113
60
|
}
|
|
114
61
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
console.error(`gjsify login: ${res.status} ${res.statusText} from ${registryClean}\n${text.slice(0, 400)}`);
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
const data = (await res.json().catch(() => ({})));
|
|
125
|
-
if (!data.token) {
|
|
126
|
-
console.error(`gjsify login: the registry accepted the login but returned no token (unexpected response shape). ` +
|
|
127
|
-
`Your registry may require the web-OAuth flow — use \`npm login\`.`);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
const npmrcPath = writeAuthToken(registryClean, data.token);
|
|
131
|
-
// Confirm the freshly-written token authenticates.
|
|
132
|
-
const verifyNpmrc = await loadNpmrc(process.cwd());
|
|
133
|
-
const who = await whoami(registryClean, verifyNpmrc);
|
|
134
|
-
const confirmedName = who.username ?? username;
|
|
135
|
-
if (args.json) {
|
|
136
|
-
process.stdout.write(`${JSON.stringify({ username: confirmedName, registry: registryClean })}\n`);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
process.stdout.write(`Logged in as ${confirmedName} on ${registryClean}\n(token written to ${npmrcPath})\n`);
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof LoginError) {
|
|
64
|
+
console.error(err.message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
140
68
|
}
|
|
141
69
|
},
|
|
142
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* The `gjsify login` core, extracted so `gjsify onboard` can authenticate the
|
|
73
|
+
* user when the whoami gate fails. Prompts for username + password (unless
|
|
74
|
+
* provided), PUTs the CouchDB user doc, writes the token to the userconfig
|
|
75
|
+
* `.npmrc`, and verifies it with `whoami`. Throws {@link LoginError} on any
|
|
76
|
+
* recoverable failure (the command handler / onboard present it).
|
|
77
|
+
*/
|
|
78
|
+
export async function runLogin(opts) {
|
|
79
|
+
const npmrc = await loadNpmrc(opts.cwd ?? process.cwd());
|
|
80
|
+
const registry = opts.registry ??
|
|
81
|
+
process.env.npm_config_registry ??
|
|
82
|
+
(opts.scope
|
|
83
|
+
? registryFor(opts.scope.startsWith('@') ? `${opts.scope}/_` : `@${opts.scope}/_`, npmrc)
|
|
84
|
+
: undefined) ??
|
|
85
|
+
DEFAULT_REGISTRY;
|
|
86
|
+
const registryClean = registry.endsWith('/') ? registry : `${registry}/`;
|
|
87
|
+
// Read username (visible) + password (masked) in a single raw-mode TTY
|
|
88
|
+
// session — robust against cooked line-discipline state + the shared stdin
|
|
89
|
+
// resume/pause race.
|
|
90
|
+
const { username, password } = await promptCredentials(opts.username);
|
|
91
|
+
if (!username)
|
|
92
|
+
throw new LoginError('gjsify login: a username is required.');
|
|
93
|
+
if (!password)
|
|
94
|
+
throw new LoginError('gjsify login: a password is required.');
|
|
95
|
+
// npm legacy auth = HTTP Basic of `<username>:<password>`. The password also
|
|
96
|
+
// lives in the PUT body, which is how the registry mints the token.
|
|
97
|
+
const basic = `Basic ${btoa(`${username}:${password}`)}`;
|
|
98
|
+
const couchUrl = `${registryClean}-/user/org.couchdb.user:${encodeURIComponent(username)}`;
|
|
99
|
+
const body = {
|
|
100
|
+
_id: `org.couchdb.user:${username}`,
|
|
101
|
+
name: username,
|
|
102
|
+
password,
|
|
103
|
+
type: 'user',
|
|
104
|
+
roles: [],
|
|
105
|
+
date: undefined, // the registry stamps this; Date is unavailable in some runtimes
|
|
106
|
+
};
|
|
107
|
+
function authHeaders(otp) {
|
|
108
|
+
const h = {
|
|
109
|
+
authorization: basic,
|
|
110
|
+
'content-type': 'application/json',
|
|
111
|
+
accept: '*/*',
|
|
112
|
+
};
|
|
113
|
+
if (otp)
|
|
114
|
+
h['npm-otp'] = otp;
|
|
115
|
+
return h;
|
|
116
|
+
}
|
|
117
|
+
async function putUser(path, otp) {
|
|
118
|
+
return fetch(`${couchUrl}${path}`, { method: 'PUT', headers: authHeaders(otp), body: JSON.stringify(body) });
|
|
119
|
+
}
|
|
120
|
+
let otp = opts.otp;
|
|
121
|
+
let res = await putUser('', otp);
|
|
122
|
+
// 2FA: 401 + www-authenticate "otp" (or a one-time-pass body) → prompt + retry.
|
|
123
|
+
if (res.status === 401 && !otp) {
|
|
124
|
+
const wwwAuth = (res.headers.get('www-authenticate') ?? '').toLowerCase();
|
|
125
|
+
const text = await res
|
|
126
|
+
.clone()
|
|
127
|
+
.text()
|
|
128
|
+
.catch(() => '');
|
|
129
|
+
if (wwwAuth.includes('otp') || /one-time pass/i.test(text)) {
|
|
130
|
+
otp = await promptLine(`This operation requires a one-time password.\nEnter OTP: `);
|
|
131
|
+
if (!otp)
|
|
132
|
+
throw new LoginError('gjsify login: no OTP entered.');
|
|
133
|
+
res = await putUser('', otp);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 409 Conflict: the user already exists (the normal login case). Fetch the
|
|
137
|
+
// current doc for its `_rev`, merge, and PUT to `…/-rev/<rev>`.
|
|
138
|
+
if (res.status === 409) {
|
|
139
|
+
const getRes = await fetch(`${couchUrl}?write=true`, { headers: authHeaders(otp) });
|
|
140
|
+
if (getRes.ok) {
|
|
141
|
+
const existing = (await getRes.json().catch(() => ({})));
|
|
142
|
+
for (const k of Object.keys(existing)) {
|
|
143
|
+
if (!body[k] || k === 'roles')
|
|
144
|
+
body[k] = existing[k];
|
|
145
|
+
}
|
|
146
|
+
if (body._rev)
|
|
147
|
+
res = await putUser(`/-rev/${body._rev}`, otp);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (res.status === 400) {
|
|
151
|
+
throw new LoginError(`gjsify login: there is no user with the username "${username}" on ${registryClean}.`);
|
|
152
|
+
}
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
const text = await res.text().catch(() => '<no body>');
|
|
155
|
+
throw new LoginError(`gjsify login: ${res.status} ${res.statusText} from ${registryClean}\n${text.slice(0, 400)}`);
|
|
156
|
+
}
|
|
157
|
+
const data = (await res.json().catch(() => ({})));
|
|
158
|
+
if (!data.token) {
|
|
159
|
+
throw new LoginError(`gjsify login: the registry accepted the login but returned no token (unexpected response shape). ` +
|
|
160
|
+
`Your registry may require the web-OAuth flow — use \`npm login\`.`);
|
|
161
|
+
}
|
|
162
|
+
const npmrcPath = writeAuthToken(registryClean, data.token);
|
|
163
|
+
// Confirm the freshly-written token authenticates.
|
|
164
|
+
const verifyNpmrc = await loadNpmrc(opts.cwd ?? process.cwd());
|
|
165
|
+
const who = await whoami(registryClean, verifyNpmrc);
|
|
166
|
+
const confirmedName = who.username ?? username;
|
|
167
|
+
return { username: confirmedName, registry: registryClean, npmrcPath };
|
|
168
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface OnboardOptions {
|
|
3
|
+
repository?: string;
|
|
4
|
+
workflow: string;
|
|
5
|
+
environment?: string;
|
|
6
|
+
registry?: string;
|
|
7
|
+
otp?: string;
|
|
8
|
+
concurrency: number;
|
|
9
|
+
'dry-run'?: boolean;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
yes?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const onboardCommand: Command<unknown, OnboardOptions>;
|
|
14
|
+
export {};
|