@vltpkg/cli-sdk 1.0.0-rc.26 → 1.0.0-rc.29
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/commands/access.d.ts +22 -0
- package/dist/commands/access.js +246 -0
- package/dist/commands/deprecate.d.ts +13 -0
- package/dist/commands/deprecate.js +139 -0
- package/dist/commands/dist-tag.d.ts +21 -0
- package/dist/commands/dist-tag.js +177 -0
- package/dist/commands/init.js +25 -9
- package/dist/commands/install.js +10 -0
- package/dist/commands/profile.d.ts +13 -0
- package/dist/commands/profile.js +104 -0
- package/dist/commands/token.d.ts +24 -1
- package/dist/commands/token.js +112 -5
- package/dist/commands/unpublish.d.ts +15 -0
- package/dist/commands/unpublish.js +200 -0
- package/dist/config/definition.d.ts +20 -3
- package/dist/config/definition.js +32 -24
- package/dist/config/index.js +1 -0
- package/dist/custom-help.js +7 -0
- package/dist/exec-command.js +1 -1
- package/dist/index.js +1 -1
- package/dist/output.d.ts +2 -1
- package/dist/output.js +55 -1
- package/dist/telemetry.d.ts +58 -0
- package/dist/telemetry.js +170 -0
- package/package.json +26 -25
package/dist/commands/init.js
CHANGED
|
@@ -2,13 +2,16 @@ import { mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { relative, resolve } from 'node:path';
|
|
3
3
|
import { minimatch } from 'minimatch';
|
|
4
4
|
import { init } from '@vltpkg/init';
|
|
5
|
+
import { install } from '@vltpkg/graph';
|
|
5
6
|
import { load, save } from '@vltpkg/vlt-json';
|
|
6
7
|
import { assertWSConfig, asWSConfig } from '@vltpkg/workspaces';
|
|
7
8
|
import { commandUsage } from "../config/usage.js";
|
|
8
9
|
export const usage = () => commandUsage({
|
|
9
10
|
command: 'init',
|
|
10
11
|
usage: '',
|
|
11
|
-
description: `
|
|
12
|
+
description: `Initialize a new project in the current directory.
|
|
13
|
+
Creates a package.json, a .gitignore, and installs
|
|
14
|
+
dependencies so the project is ready to use immediately.`,
|
|
12
15
|
options: {
|
|
13
16
|
workspace: {
|
|
14
17
|
value: '<path|glob>',
|
|
@@ -23,17 +26,20 @@ export const views = {
|
|
|
23
26
|
// if results is an array, it means multiple workspaces were initialized
|
|
24
27
|
if (Array.isArray(results)) {
|
|
25
28
|
for (const result of results) {
|
|
26
|
-
for (const [type,
|
|
27
|
-
output.push(`Wrote ${type} to ${path}
|
|
29
|
+
for (const [type, value] of Object.entries(result)) {
|
|
30
|
+
output.push(`Wrote ${type} to ${value.path}`);
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
else {
|
|
32
35
|
// otherwise, it's a single result
|
|
33
|
-
for (const [type,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
for (const [type, value] of Object.entries(results)) {
|
|
37
|
+
if ('data' in value) {
|
|
38
|
+
output.push(`Wrote ${type} to ${value.path}:\n\n${JSON.stringify(value.data, null, 2)}`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
output.push(`Wrote ${type} to ${value.path}`);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
output.push(`\nModify/add properties using \`vlt pkg\`. For example:
|
|
@@ -43,6 +49,11 @@ ${JSON.stringify(data, null, 2)}`);
|
|
|
43
49
|
},
|
|
44
50
|
};
|
|
45
51
|
export const command = async (conf) => {
|
|
52
|
+
/* c8 ignore start */
|
|
53
|
+
const allowScripts = conf.get('allow-scripts') ?
|
|
54
|
+
String(conf.get('allow-scripts'))
|
|
55
|
+
: ':not(*)';
|
|
56
|
+
/* c8 ignore stop */
|
|
46
57
|
if (conf.values.workspace?.length) {
|
|
47
58
|
const workspacesConfig = load('workspaces', assertWSConfig);
|
|
48
59
|
const parsedWSConfig = asWSConfig(workspacesConfig ?? {});
|
|
@@ -84,7 +95,7 @@ export const command = async (conf) => {
|
|
|
84
95
|
else {
|
|
85
96
|
// otherwise we assume it's an Record<string, string[]> object
|
|
86
97
|
// and we'll add the new workspaces to the `packages` keys
|
|
87
|
-
workspaces =
|
|
98
|
+
workspaces = workspacesConfig ?? {};
|
|
88
99
|
// if the `packages` key is not being used
|
|
89
100
|
if (!workspaces.packages) {
|
|
90
101
|
workspaces.packages = addToConfig;
|
|
@@ -110,7 +121,12 @@ export const command = async (conf) => {
|
|
|
110
121
|
// finally, we add the new workspaces to the config file
|
|
111
122
|
save('workspaces', workspaces);
|
|
112
123
|
}
|
|
124
|
+
// run install to set up node_modules and vlt-lock.json
|
|
125
|
+
await install({ ...conf.options, allowScripts });
|
|
113
126
|
return results;
|
|
114
127
|
}
|
|
115
|
-
|
|
128
|
+
const result = await init({ cwd: process.cwd() });
|
|
129
|
+
// run install to set up node_modules and vlt-lock.json
|
|
130
|
+
await install({ ...conf.options, allowScripts });
|
|
131
|
+
return result;
|
|
116
132
|
};
|
package/dist/commands/install.js
CHANGED
|
@@ -2,6 +2,7 @@ import { commandUsage } from "../config/usage.js";
|
|
|
2
2
|
import { install } from '@vltpkg/graph';
|
|
3
3
|
import { parseAddArgs } from "../parse-add-remove-args.js";
|
|
4
4
|
import { InstallReporter } from "./install/reporter.js";
|
|
5
|
+
import { trackInstall } from "../telemetry.js";
|
|
5
6
|
export const usage = () => commandUsage({
|
|
6
7
|
command: 'install',
|
|
7
8
|
usage: '[packages ...]',
|
|
@@ -119,6 +120,7 @@ export const command = async (conf) => {
|
|
|
119
120
|
String(conf.get('allow-scripts'))
|
|
120
121
|
: ':not(*)';
|
|
121
122
|
/* c8 ignore stop */
|
|
123
|
+
const installStart = Date.now();
|
|
122
124
|
const { buildQueue, graph, diff } = await install({
|
|
123
125
|
...conf.options,
|
|
124
126
|
frozenLockfile,
|
|
@@ -126,5 +128,13 @@ export const command = async (conf) => {
|
|
|
126
128
|
allowScripts,
|
|
127
129
|
lockfileOnly,
|
|
128
130
|
}, add);
|
|
131
|
+
/* c8 ignore next 9 - telemetry is best-effort */
|
|
132
|
+
try {
|
|
133
|
+
trackInstall({
|
|
134
|
+
dependency_count: graph.nodes.size,
|
|
135
|
+
duration_ms: Date.now() - installStart,
|
|
136
|
+
}, conf.values.telemetry);
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
129
139
|
return { buildQueue, graph, diff };
|
|
130
140
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JSONField } from '@vltpkg/types';
|
|
2
|
+
import type { CommandFn, CommandUsage } from '../index.ts';
|
|
3
|
+
export declare const usage: CommandUsage;
|
|
4
|
+
export type ProfileData = Record<string, JSONField>;
|
|
5
|
+
export type ProfileResult = ProfileData | {
|
|
6
|
+
property: string;
|
|
7
|
+
value: JSONField;
|
|
8
|
+
};
|
|
9
|
+
export declare const views: {
|
|
10
|
+
readonly human: (result: ProfileResult) => string;
|
|
11
|
+
readonly json: (r: ProfileResult) => ProfileResult;
|
|
12
|
+
};
|
|
13
|
+
export declare const command: CommandFn<ProfileResult>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { error } from '@vltpkg/error-cause';
|
|
2
|
+
import { RegistryClient } from '@vltpkg/registry-client';
|
|
3
|
+
import { commandUsage } from "../config/usage.js";
|
|
4
|
+
export const usage = () => commandUsage({
|
|
5
|
+
command: 'profile',
|
|
6
|
+
usage: '<command> [<args>]',
|
|
7
|
+
description: `Get or set profile properties for the authenticated user
|
|
8
|
+
on the configured registry.`,
|
|
9
|
+
subcommands: {
|
|
10
|
+
get: {
|
|
11
|
+
usage: '[<property>]',
|
|
12
|
+
description: 'Display profile information. Optionally pass a property name to get a single value.',
|
|
13
|
+
},
|
|
14
|
+
set: {
|
|
15
|
+
usage: '<property> <value>',
|
|
16
|
+
description: 'Set a profile property to the given value.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
options: {
|
|
20
|
+
registry: {
|
|
21
|
+
value: '<url>',
|
|
22
|
+
description: 'Registry URL to query for profile info.',
|
|
23
|
+
},
|
|
24
|
+
identity: {
|
|
25
|
+
value: '<name>',
|
|
26
|
+
description: 'Identity namespace used to look up auth tokens.',
|
|
27
|
+
},
|
|
28
|
+
otp: {
|
|
29
|
+
description: 'Provide an OTP to use when updating profile properties.',
|
|
30
|
+
value: '<otp>',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const stringify = (v) => typeof v === 'string' ? v
|
|
35
|
+
: typeof v === 'number' || typeof v === 'boolean' ? `${v}`
|
|
36
|
+
: v === null ? 'null'
|
|
37
|
+
: JSON.stringify(v);
|
|
38
|
+
export const views = {
|
|
39
|
+
human: result => {
|
|
40
|
+
if ('property' in result) {
|
|
41
|
+
return stringify(result.value);
|
|
42
|
+
}
|
|
43
|
+
return Object.entries(result)
|
|
44
|
+
.map(([k, v]) => `${k}: ${stringify(v)}`)
|
|
45
|
+
.join('\n');
|
|
46
|
+
},
|
|
47
|
+
json: r => r,
|
|
48
|
+
};
|
|
49
|
+
export const command = async (conf) => {
|
|
50
|
+
const [sub, ...args] = conf.positionals;
|
|
51
|
+
switch (sub) {
|
|
52
|
+
case 'get':
|
|
53
|
+
return getProfile(conf, args);
|
|
54
|
+
case 'set':
|
|
55
|
+
return setProfile(conf, args);
|
|
56
|
+
default: {
|
|
57
|
+
throw error('Invalid profile subcommand', {
|
|
58
|
+
found: sub,
|
|
59
|
+
validOptions: ['get', 'set'],
|
|
60
|
+
code: 'EUSAGE',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const getProfile = async (conf, args) => {
|
|
66
|
+
const rc = new RegistryClient(conf.options);
|
|
67
|
+
const registryUrl = new URL(conf.options.registry);
|
|
68
|
+
const url = new URL('-/npm/v1/user', registryUrl);
|
|
69
|
+
const response = await rc.request(url, { useCache: false });
|
|
70
|
+
const data = response.json();
|
|
71
|
+
const [property] = args;
|
|
72
|
+
if (property) {
|
|
73
|
+
if (!(property in data)) {
|
|
74
|
+
throw error('Property not found in profile', {
|
|
75
|
+
found: property,
|
|
76
|
+
validOptions: Object.keys(data),
|
|
77
|
+
code: 'EUSAGE',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return { property, value: data[property] };
|
|
81
|
+
}
|
|
82
|
+
return data;
|
|
83
|
+
};
|
|
84
|
+
const setProfile = async (conf, args) => {
|
|
85
|
+
const [property, ...rest] = args;
|
|
86
|
+
const value = rest.join(' ');
|
|
87
|
+
if (!property || !value) {
|
|
88
|
+
throw error('set requires a property name and value', {
|
|
89
|
+
code: 'EUSAGE',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const rc = new RegistryClient(conf.options);
|
|
93
|
+
const registryUrl = new URL(conf.options.registry);
|
|
94
|
+
const url = new URL('-/npm/v1/user', registryUrl);
|
|
95
|
+
const response = await rc.request(url, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'content-type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ [property]: value }),
|
|
99
|
+
otp: conf.options.otp,
|
|
100
|
+
useCache: false,
|
|
101
|
+
});
|
|
102
|
+
const data = response.json();
|
|
103
|
+
return { property, value: data[property] };
|
|
104
|
+
};
|
package/dist/commands/token.d.ts
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
1
|
import type { CommandFn, CommandUsage } from '../index.ts';
|
|
2
|
+
export type TokenInfo = {
|
|
3
|
+
/** The server-side key/id for the token */
|
|
4
|
+
key: string;
|
|
5
|
+
/** A truncated prefix of the token value */
|
|
6
|
+
token: string;
|
|
7
|
+
/** ISO date when the token was created */
|
|
8
|
+
created: string;
|
|
9
|
+
/** Whether this token is read-only */
|
|
10
|
+
readonly: boolean;
|
|
11
|
+
/** CIDR whitelist, if any */
|
|
12
|
+
cidr_whitelist?: string[];
|
|
13
|
+
};
|
|
14
|
+
export type RegistryTokens = {
|
|
15
|
+
registry: string;
|
|
16
|
+
alias?: string;
|
|
17
|
+
tokens: TokenInfo[];
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
export type TokenListResult = RegistryTokens[];
|
|
2
21
|
export declare const usage: CommandUsage;
|
|
3
|
-
export declare const
|
|
22
|
+
export declare const views: {
|
|
23
|
+
readonly human: (r: TokenListResult | void) => string | undefined;
|
|
24
|
+
readonly json: (r: TokenListResult | void) => TokenListResult | undefined;
|
|
25
|
+
};
|
|
26
|
+
export declare const command: CommandFn<TokenListResult | void>;
|
package/dist/commands/token.js
CHANGED
|
@@ -1,25 +1,132 @@
|
|
|
1
1
|
import { error } from '@vltpkg/error-cause';
|
|
2
|
-
import { deleteToken, setToken } from '@vltpkg/registry-client';
|
|
2
|
+
import { deleteToken, normalizeRegistryKey, RegistryClient, setToken, } from '@vltpkg/registry-client';
|
|
3
3
|
import { commandUsage } from "../config/usage.js";
|
|
4
4
|
import { readPassword } from "../read-password.js";
|
|
5
5
|
export const usage = () => commandUsage({
|
|
6
6
|
command: 'token',
|
|
7
|
-
usage: ['add', 'rm'],
|
|
8
|
-
description: `
|
|
7
|
+
usage: ['list', 'add', 'rm'],
|
|
8
|
+
description: `Manage registry authentication tokens in the vlt keychain.`,
|
|
9
|
+
subcommands: {
|
|
10
|
+
list: {
|
|
11
|
+
usage: '',
|
|
12
|
+
description: `List all tokens for configured registries.
|
|
13
|
+
Queries each registry's token API and displays
|
|
14
|
+
token metadata including key, creation date,
|
|
15
|
+
and permissions.`,
|
|
16
|
+
},
|
|
17
|
+
add: {
|
|
18
|
+
usage: '',
|
|
19
|
+
description: `Add a token for the specified registry.
|
|
20
|
+
You will be prompted to paste the bearer token.`,
|
|
21
|
+
},
|
|
22
|
+
rm: {
|
|
23
|
+
usage: '',
|
|
24
|
+
description: `Remove the stored token for the specified registry.`,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
9
27
|
options: {
|
|
10
28
|
registry: {
|
|
11
29
|
value: '<url>',
|
|
12
30
|
description: 'Registry URL to manage tokens for.',
|
|
13
31
|
},
|
|
32
|
+
registries: {
|
|
33
|
+
value: '<alias=url>',
|
|
34
|
+
description: 'Named registry aliases (used by the list subcommand).',
|
|
35
|
+
},
|
|
14
36
|
identity: {
|
|
15
37
|
value: '<name>',
|
|
16
38
|
description: 'Identity namespace used to store auth tokens.',
|
|
17
39
|
},
|
|
18
40
|
},
|
|
19
41
|
});
|
|
42
|
+
const formatDate = (iso) => {
|
|
43
|
+
const d = new Date(iso);
|
|
44
|
+
return d.toLocaleDateString('en-US', {
|
|
45
|
+
year: 'numeric',
|
|
46
|
+
month: 'short',
|
|
47
|
+
day: 'numeric',
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const formatTokenEntry = (t) => {
|
|
51
|
+
const parts = [
|
|
52
|
+
`key: ${t.key}`,
|
|
53
|
+
`token: ${t.token}…`,
|
|
54
|
+
`created: ${formatDate(t.created)}`,
|
|
55
|
+
`readonly: ${t.readonly ? 'yes' : 'no'}`,
|
|
56
|
+
];
|
|
57
|
+
if (t.cidr_whitelist && t.cidr_whitelist.length > 0) {
|
|
58
|
+
parts.push(`cidr: ${t.cidr_whitelist.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
return parts.join(' │ ');
|
|
61
|
+
};
|
|
62
|
+
const formatRegistryTokens = (r) => {
|
|
63
|
+
const header = r.alias ? `${r.alias} (${r.registry})` : r.registry;
|
|
64
|
+
if (r.error)
|
|
65
|
+
return `${header}\n error: ${r.error}`;
|
|
66
|
+
if (r.tokens.length === 0)
|
|
67
|
+
return `${header}\n (no tokens found)`;
|
|
68
|
+
return [
|
|
69
|
+
header,
|
|
70
|
+
...r.tokens.map(t => ` ${formatTokenEntry(t)}`),
|
|
71
|
+
].join('\n');
|
|
72
|
+
};
|
|
73
|
+
export const views = {
|
|
74
|
+
human: (r) => {
|
|
75
|
+
if (!r)
|
|
76
|
+
return;
|
|
77
|
+
return r.map(formatRegistryTokens).join('\n\n');
|
|
78
|
+
},
|
|
79
|
+
json: (r) => {
|
|
80
|
+
if (!r)
|
|
81
|
+
return;
|
|
82
|
+
return r;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const listTokens = async (rc, registry, alias) => {
|
|
86
|
+
const tokensUrl = new URL('-/npm/v1/tokens', registry);
|
|
87
|
+
try {
|
|
88
|
+
const objects = await rc.scroll(tokensUrl, {
|
|
89
|
+
useCache: false,
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
registry,
|
|
93
|
+
alias,
|
|
94
|
+
tokens: objects.map(o => ({
|
|
95
|
+
key: o.key,
|
|
96
|
+
token: o.token,
|
|
97
|
+
created: o.created,
|
|
98
|
+
readonly: o.readonly,
|
|
99
|
+
cidr_whitelist: o.cidr_whitelist,
|
|
100
|
+
})),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
registry,
|
|
106
|
+
alias,
|
|
107
|
+
tokens: [],
|
|
108
|
+
error: err instanceof Error ? err.message : String(err),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
20
112
|
export const command = async (conf) => {
|
|
21
|
-
const reg =
|
|
113
|
+
const reg = normalizeRegistryKey(conf.options.registry);
|
|
22
114
|
switch (conf.positionals[0]) {
|
|
115
|
+
case 'list': {
|
|
116
|
+
const rc = new RegistryClient(conf.options);
|
|
117
|
+
const results = [];
|
|
118
|
+
// Always query the default registry first
|
|
119
|
+
results.push(await listTokens(rc, conf.options.registry, 'default'));
|
|
120
|
+
// Then query all configured registry aliases
|
|
121
|
+
const registries = conf.options.registries;
|
|
122
|
+
for (const [alias, registry] of Object.entries(registries)) {
|
|
123
|
+
// Skip if it's the same as the default registry
|
|
124
|
+
if (registry !== conf.options.registry) {
|
|
125
|
+
results.push(await listTokens(rc, registry, alias));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
23
130
|
case 'add': {
|
|
24
131
|
await setToken(reg, `Bearer ${await readPassword('Paste bearer token: ')}`, conf.options.identity);
|
|
25
132
|
break;
|
|
@@ -31,7 +138,7 @@ export const command = async (conf) => {
|
|
|
31
138
|
default: {
|
|
32
139
|
throw error('Invalid token subcommand', {
|
|
33
140
|
found: conf.positionals[0],
|
|
34
|
-
validOptions: ['add', 'rm'],
|
|
141
|
+
validOptions: ['list', 'add', 'rm'],
|
|
35
142
|
code: 'EUSAGE',
|
|
36
143
|
});
|
|
37
144
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CommandFn, CommandUsage } from '../index.ts';
|
|
2
|
+
export declare const usage: CommandUsage;
|
|
3
|
+
export type CommandResult = {
|
|
4
|
+
/** The package name */
|
|
5
|
+
name: string;
|
|
6
|
+
/** The version that was unpublished, or undefined for entire package */
|
|
7
|
+
version?: string;
|
|
8
|
+
/** The registry URL */
|
|
9
|
+
registry: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const views: {
|
|
12
|
+
readonly human: (result: CommandResult) => string;
|
|
13
|
+
readonly json: (r: CommandResult) => CommandResult;
|
|
14
|
+
};
|
|
15
|
+
export declare const command: CommandFn<CommandResult>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { error } from '@vltpkg/error-cause';
|
|
2
|
+
import { RegistryClient } from '@vltpkg/registry-client';
|
|
3
|
+
import { Spec } from '@vltpkg/spec';
|
|
4
|
+
import { asError } from '@vltpkg/types';
|
|
5
|
+
import { commandUsage } from "../config/usage.js";
|
|
6
|
+
export const usage = () => commandUsage({
|
|
7
|
+
command: 'unpublish',
|
|
8
|
+
usage: ['<package>@<version>', '<package> --force'],
|
|
9
|
+
description: `Remove a package version from the registry.
|
|
10
|
+
|
|
11
|
+
To unpublish a single version, specify the package name and version.
|
|
12
|
+
To unpublish an entire package, specify the package name and use --force.
|
|
13
|
+
|
|
14
|
+
⚠️ Unpublishing is a destructive action that cannot be undone.
|
|
15
|
+
Consider using \`vlt deprecate\` instead if you want to discourage
|
|
16
|
+
usage of a package without removing it.`,
|
|
17
|
+
examples: {
|
|
18
|
+
'my-package@1.0.0': {
|
|
19
|
+
description: 'Unpublish a specific version',
|
|
20
|
+
},
|
|
21
|
+
'@scope/my-package@1.0.0': {
|
|
22
|
+
description: 'Unpublish a specific version of a scoped package',
|
|
23
|
+
},
|
|
24
|
+
'my-package --force': {
|
|
25
|
+
description: 'Unpublish an entire package (requires --force)',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
options: {
|
|
29
|
+
force: {
|
|
30
|
+
description: 'Required to unpublish an entire package (all versions).',
|
|
31
|
+
},
|
|
32
|
+
otp: {
|
|
33
|
+
description: 'Provide a one-time password for authentication.',
|
|
34
|
+
value: '<otp>',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
export const views = {
|
|
39
|
+
human: (result) => {
|
|
40
|
+
if (result.version) {
|
|
41
|
+
return `⚠️ ${result.name}@${result.version} has been unpublished from ${result.registry}.`;
|
|
42
|
+
}
|
|
43
|
+
return `⚠️ ${result.name} (all versions) has been unpublished from ${result.registry}.`;
|
|
44
|
+
},
|
|
45
|
+
json: (r) => r,
|
|
46
|
+
};
|
|
47
|
+
export const command = async (conf) => {
|
|
48
|
+
const specArg = conf.positionals[0];
|
|
49
|
+
if (!specArg) {
|
|
50
|
+
throw error('unpublish requires a package spec argument (e.g. pkg@version)', { code: 'EUSAGE' });
|
|
51
|
+
}
|
|
52
|
+
const { registry, otp, force } = conf.options;
|
|
53
|
+
const registryUrl = new URL(registry);
|
|
54
|
+
const spec = Spec.parseArgs(specArg, conf.options);
|
|
55
|
+
const name = spec.name;
|
|
56
|
+
/* c8 ignore start - Spec.parseArgs always produces a name for valid input */
|
|
57
|
+
if (!name) {
|
|
58
|
+
throw error('Package name is required', {
|
|
59
|
+
found: specArg,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/* c8 ignore stop */
|
|
63
|
+
const version = spec.bareSpec;
|
|
64
|
+
// If no version specified, require --force to unpublish the entire package
|
|
65
|
+
if (!version) {
|
|
66
|
+
if (!force) {
|
|
67
|
+
throw error('Refusing to unpublish entire package without --force.\n' +
|
|
68
|
+
'To unpublish a specific version, use: vlt unpublish <package>@<version>\n' +
|
|
69
|
+
'To unpublish all versions, use: vlt unpublish <package> --force');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const rc = new RegistryClient(conf.options);
|
|
73
|
+
const encodedName = name.startsWith('@') ? name.replace('/', '%2F') : name;
|
|
74
|
+
if (version) {
|
|
75
|
+
// Unpublish a specific version:
|
|
76
|
+
// 1. Fetch the packument
|
|
77
|
+
// 2. Remove the version from the packument
|
|
78
|
+
// 3. PUT the updated packument back
|
|
79
|
+
const packumentUrl = new URL(encodedName, registryUrl);
|
|
80
|
+
let packumentResponse;
|
|
81
|
+
try {
|
|
82
|
+
packumentResponse = await rc.request(packumentUrl, {
|
|
83
|
+
useCache: false,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw error('Failed to fetch package metadata', {
|
|
88
|
+
cause: asError(err),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (packumentResponse.statusCode !== 200) {
|
|
92
|
+
throw error('Package not found on the registry', {
|
|
93
|
+
url: packumentUrl,
|
|
94
|
+
response: packumentResponse,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const packument = packumentResponse.json();
|
|
98
|
+
const versions = packument.versions;
|
|
99
|
+
const distTags = packument['dist-tags'];
|
|
100
|
+
if (!versions?.[version]) {
|
|
101
|
+
throw error(`Version ${version} not found in package ${name}`, {
|
|
102
|
+
found: version,
|
|
103
|
+
wanted: Object.keys(versions ?? {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Remove the version
|
|
107
|
+
delete versions[version];
|
|
108
|
+
// Remove any dist-tags pointing to this version
|
|
109
|
+
if (distTags) {
|
|
110
|
+
for (const [tag, tagVersion] of Object.entries(distTags)) {
|
|
111
|
+
if (tagVersion === version) {
|
|
112
|
+
delete distTags[tag];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Remove the version from the time field if present
|
|
117
|
+
const time = packument.time;
|
|
118
|
+
if (time?.[version]) {
|
|
119
|
+
delete time[version];
|
|
120
|
+
}
|
|
121
|
+
// PUT the updated packument
|
|
122
|
+
const putUrl = new URL(`${encodedName}/-rev/${packument._rev}`, registryUrl);
|
|
123
|
+
let response;
|
|
124
|
+
try {
|
|
125
|
+
response = await rc.request(putUrl, {
|
|
126
|
+
method: 'PUT',
|
|
127
|
+
headers: {
|
|
128
|
+
'content-type': 'application/json',
|
|
129
|
+
'npm-auth-type': 'web',
|
|
130
|
+
'npm-command': 'unpublish',
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify(packument),
|
|
133
|
+
otp,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
throw error('Failed to unpublish package version', {
|
|
138
|
+
cause: asError(err),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (response.statusCode !== 200 && response.statusCode !== 201) {
|
|
142
|
+
throw error('Failed to unpublish package version', {
|
|
143
|
+
url: putUrl,
|
|
144
|
+
response,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Unpublish entire package — DELETE the packument
|
|
150
|
+
// First fetch the packument to get the _rev
|
|
151
|
+
const packumentUrl = new URL(encodedName, registryUrl);
|
|
152
|
+
let packumentResponse;
|
|
153
|
+
try {
|
|
154
|
+
packumentResponse = await rc.request(packumentUrl, {
|
|
155
|
+
useCache: false,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
throw error('Failed to fetch package metadata', {
|
|
160
|
+
cause: asError(err),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (packumentResponse.statusCode !== 200) {
|
|
164
|
+
throw error('Package not found on the registry', {
|
|
165
|
+
url: packumentUrl,
|
|
166
|
+
response: packumentResponse,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const packument = packumentResponse.json();
|
|
170
|
+
const deleteUrl = new URL(`${encodedName}/-rev/${packument._rev}`, registryUrl);
|
|
171
|
+
let response;
|
|
172
|
+
try {
|
|
173
|
+
response = await rc.request(deleteUrl, {
|
|
174
|
+
method: 'DELETE',
|
|
175
|
+
headers: {
|
|
176
|
+
'content-type': 'application/json',
|
|
177
|
+
'npm-auth-type': 'web',
|
|
178
|
+
'npm-command': 'unpublish',
|
|
179
|
+
},
|
|
180
|
+
otp,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
throw error('Failed to unpublish package', {
|
|
185
|
+
cause: asError(err),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (response.statusCode !== 200 && response.statusCode !== 201) {
|
|
189
|
+
throw error('Failed to unpublish package', {
|
|
190
|
+
url: deleteUrl,
|
|
191
|
+
response,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
...(version ? { version } : {}),
|
|
198
|
+
registry: registryUrl.origin,
|
|
199
|
+
};
|
|
200
|
+
};
|