@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.
@@ -0,0 +1,22 @@
1
+ import type { CommandFn, CommandUsage } from '../index.ts';
2
+ export declare const usage: CommandUsage;
3
+ export type AccessResult = {
4
+ package: string;
5
+ access: string;
6
+ } | {
7
+ packages: Record<string, string>;
8
+ } | {
9
+ granted: {
10
+ team: string;
11
+ permissions: string;
12
+ };
13
+ } | {
14
+ revoked: {
15
+ team: string;
16
+ };
17
+ };
18
+ export declare const views: {
19
+ readonly human: (result: AccessResult) => string;
20
+ readonly json: (r: AccessResult) => AccessResult;
21
+ };
22
+ export declare const command: CommandFn<AccessResult>;
@@ -0,0 +1,246 @@
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: 'access',
6
+ usage: '<command> [<args>]',
7
+ description: `Set or get access levels for published packages
8
+ and manage team-based package permissions.`,
9
+ subcommands: {
10
+ 'list packages': {
11
+ usage: '[<scope|user|org>]',
12
+ description: 'List packages with access info for a scope, user, or org.',
13
+ },
14
+ 'get status': {
15
+ usage: '<package>',
16
+ description: 'Get the access/visibility status of a package.',
17
+ },
18
+ 'set status': {
19
+ usage: '<package>',
20
+ description: `Set the access/visibility of a package. Use --access to specify the level.`,
21
+ },
22
+ grant: {
23
+ usage: '<read-only|read-write> <scope:team> [<package>]',
24
+ description: 'Grant access to a scope:team for a package.',
25
+ },
26
+ revoke: {
27
+ usage: '<scope:team> [<package>]',
28
+ description: 'Revoke access from a scope:team for a package.',
29
+ },
30
+ },
31
+ options: {
32
+ registry: {
33
+ value: '<url>',
34
+ description: 'Registry URL to manage access on.',
35
+ },
36
+ otp: {
37
+ description: 'Provide an OTP for access changes.',
38
+ value: '<otp>',
39
+ },
40
+ },
41
+ });
42
+ export const views = {
43
+ human: (result) => {
44
+ if ('packages' in result) {
45
+ const entries = Object.entries(result.packages);
46
+ if (entries.length === 0)
47
+ return 'No packages found.';
48
+ return entries
49
+ .map(([name, access]) => `${name}: ${access}`)
50
+ .join('\n');
51
+ }
52
+ if ('granted' in result) {
53
+ return `Granted ${result.granted.permissions} access to ${result.granted.team}.`;
54
+ }
55
+ if ('revoked' in result) {
56
+ return `Revoked access from ${result.revoked.team}.`;
57
+ }
58
+ return `${result.package}: ${result.access}`;
59
+ },
60
+ json: (r) => r,
61
+ };
62
+ export const command = async (conf) => {
63
+ const [sub, ...args] = conf.positionals;
64
+ switch (sub) {
65
+ case 'list':
66
+ return listPackages(conf, args);
67
+ case 'get':
68
+ return getStatus(conf, args);
69
+ case 'set':
70
+ return setStatus(conf, args);
71
+ case 'grant':
72
+ return grant(conf, args);
73
+ case 'revoke':
74
+ return revoke(conf, args);
75
+ default: {
76
+ throw error('Invalid access subcommand', {
77
+ found: sub,
78
+ validOptions: ['list', 'get', 'set', 'grant', 'revoke'],
79
+ code: 'EUSAGE',
80
+ });
81
+ }
82
+ }
83
+ };
84
+ const encodePkgName = (name) => name.startsWith('@') ?
85
+ `@${encodeURIComponent(name.slice(1))}`
86
+ : encodeURIComponent(name);
87
+ const listPackages = async (conf, args) => {
88
+ const [keyword, entity] = args;
89
+ if (keyword !== 'packages') {
90
+ throw error('Expected `list packages [<scope|user|org>]`', {
91
+ found: keyword,
92
+ code: 'EUSAGE',
93
+ });
94
+ }
95
+ const rc = new RegistryClient(conf.options);
96
+ const registryUrl = new URL(conf.options.registry);
97
+ // Determine who to list for — use entity arg or fall back to scope from package.json
98
+ const scope = entity ?? getDefaultScope(conf);
99
+ const url = new URL(`-/org/${encodeURIComponent(scope)}/package`, registryUrl);
100
+ const response = await rc.request(url, { useCache: false });
101
+ const data = response.json();
102
+ return { packages: data };
103
+ };
104
+ const getStatus = async (conf, args) => {
105
+ const [keyword, pkg] = args;
106
+ if (keyword !== 'status') {
107
+ throw error('Expected `get status <package>`', {
108
+ found: keyword,
109
+ code: 'EUSAGE',
110
+ });
111
+ }
112
+ if (!pkg) {
113
+ throw error('Package name is required for `get status`', {
114
+ code: 'EUSAGE',
115
+ });
116
+ }
117
+ const rc = new RegistryClient(conf.options);
118
+ const registryUrl = new URL(conf.options.registry);
119
+ const url = new URL(`-/package/${encodePkgName(pkg)}/access`, registryUrl);
120
+ const response = await rc.request(url, { useCache: false });
121
+ const data = response.json();
122
+ return { package: pkg, access: data.access };
123
+ };
124
+ const setStatus = async (conf, args) => {
125
+ // vlt access set status=<public|restricted> <package>
126
+ const [statusArg, pkg] = args;
127
+ if (!statusArg?.startsWith('status=')) {
128
+ throw error('Expected `set status=<public|restricted> <package>`', { found: statusArg, code: 'EUSAGE' });
129
+ }
130
+ const accessLevel = statusArg.slice('status='.length);
131
+ if (accessLevel !== 'public' && accessLevel !== 'restricted') {
132
+ throw error('Access level must be `public` or `restricted`', {
133
+ found: accessLevel,
134
+ validOptions: ['public', 'restricted'],
135
+ code: 'EUSAGE',
136
+ });
137
+ }
138
+ if (!pkg) {
139
+ throw error('Package name is required for `set status`', {
140
+ code: 'EUSAGE',
141
+ });
142
+ }
143
+ const rc = new RegistryClient(conf.options);
144
+ const registryUrl = new URL(conf.options.registry);
145
+ const url = new URL(`-/package/${encodePkgName(pkg)}/access`, registryUrl);
146
+ await rc.request(url, {
147
+ method: 'PUT',
148
+ headers: { 'content-type': 'application/json' },
149
+ body: JSON.stringify({ access: accessLevel }),
150
+ otp: conf.options.otp,
151
+ useCache: false,
152
+ });
153
+ return { package: pkg, access: accessLevel };
154
+ };
155
+ const grant = async (conf, args) => {
156
+ // vlt access grant <read-only|read-write> <scope:team> [<package>]
157
+ const [permissions, scopeTeam, pkg] = args;
158
+ if (permissions !== 'read-only' && permissions !== 'read-write') {
159
+ throw error('Permissions must be `read-only` or `read-write`', {
160
+ found: permissions,
161
+ validOptions: ['read-only', 'read-write'],
162
+ code: 'EUSAGE',
163
+ });
164
+ }
165
+ if (!scopeTeam?.includes(':')) {
166
+ throw error('Team must be in the format `<scope>:<team>`', {
167
+ found: scopeTeam,
168
+ code: 'EUSAGE',
169
+ });
170
+ }
171
+ const [scope, team] = scopeTeam.split(':');
172
+ if (!scope || !team) {
173
+ throw error('Team must be in the format `<scope>:<team>`', {
174
+ found: scopeTeam,
175
+ code: 'EUSAGE',
176
+ });
177
+ }
178
+ const pkgName = pkg ?? getDefaultPkgName(conf);
179
+ const rc = new RegistryClient(conf.options);
180
+ const registryUrl = new URL(conf.options.registry);
181
+ const url = new URL(`-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/package`, registryUrl);
182
+ await rc.request(url, {
183
+ method: 'PUT',
184
+ headers: { 'content-type': 'application/json' },
185
+ body: JSON.stringify({
186
+ package: pkgName,
187
+ permissions,
188
+ }),
189
+ otp: conf.options.otp,
190
+ useCache: false,
191
+ });
192
+ return {
193
+ granted: {
194
+ team: `${scope}:${team}`,
195
+ permissions,
196
+ },
197
+ };
198
+ };
199
+ const revoke = async (conf, args) => {
200
+ // vlt access revoke <scope:team> [<package>]
201
+ const [scopeTeam, pkg] = args;
202
+ if (!scopeTeam?.includes(':')) {
203
+ throw error('Team must be in the format `<scope>:<team>`', {
204
+ found: scopeTeam,
205
+ code: 'EUSAGE',
206
+ });
207
+ }
208
+ const [scope, team] = scopeTeam.split(':');
209
+ if (!scope || !team) {
210
+ throw error('Team must be in the format `<scope>:<team>`', {
211
+ found: scopeTeam,
212
+ code: 'EUSAGE',
213
+ });
214
+ }
215
+ const pkgName = pkg ?? getDefaultPkgName(conf);
216
+ const rc = new RegistryClient(conf.options);
217
+ const registryUrl = new URL(conf.options.registry);
218
+ const url = new URL(`-/team/${encodeURIComponent(scope)}/${encodeURIComponent(team)}/package`, registryUrl);
219
+ await rc.request(url, {
220
+ method: 'DELETE',
221
+ headers: { 'content-type': 'application/json' },
222
+ body: JSON.stringify({ package: pkgName }),
223
+ otp: conf.options.otp,
224
+ useCache: false,
225
+ });
226
+ return {
227
+ revoked: {
228
+ team: `${scope}:${team}`,
229
+ },
230
+ };
231
+ };
232
+ const getDefaultScope = (conf) => {
233
+ const name = conf.options.packageJson.maybeRead(conf.projectRoot)?.name;
234
+ if (name?.startsWith('@')) {
235
+ const scope = name.split('/')[0];
236
+ if (scope)
237
+ return scope;
238
+ }
239
+ throw error('Could not determine scope. Provide a scope, user, or org.', { code: 'EUSAGE' });
240
+ };
241
+ const getDefaultPkgName = (conf) => {
242
+ const name = conf.options.packageJson.maybeRead(conf.projectRoot)?.name;
243
+ if (name)
244
+ return name;
245
+ throw error('Could not determine package name. Provide a package name or run from a package directory.', { code: 'EUSAGE' });
246
+ };
@@ -0,0 +1,13 @@
1
+ import type { CommandFn, CommandUsage } from '../index.ts';
2
+ export declare const usage: CommandUsage;
3
+ export type CommandResult = {
4
+ name: string;
5
+ version: string;
6
+ message: string;
7
+ versions: string[];
8
+ };
9
+ export declare const views: {
10
+ readonly human: (result: CommandResult) => string;
11
+ readonly json: (r: CommandResult) => CommandResult;
12
+ };
13
+ export declare const command: CommandFn<CommandResult>;
@@ -0,0 +1,139 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { PackageInfoClient } from '@vltpkg/package-info';
3
+ import { RegistryClient } from '@vltpkg/registry-client';
4
+ import { Spec } from '@vltpkg/spec';
5
+ import { satisfies } from '@vltpkg/semver';
6
+ import { asError } from '@vltpkg/types';
7
+ import { commandUsage } from "../config/usage.js";
8
+ export const usage = () => commandUsage({
9
+ command: 'deprecate',
10
+ usage: '<pkg>[@<version>] <message>',
11
+ description: `Update the npm registry entry for a package, providing a
12
+ deprecation warning to all who attempt to install it.
13
+
14
+ It works on version ranges as well as specific versions,
15
+ so you can un-deprecate a previously deprecated package by
16
+ specifying the version range with an empty string as the
17
+ message.`,
18
+ examples: {
19
+ 'my-package "this package is no longer maintained"': {
20
+ description: 'Deprecate all versions of a package',
21
+ },
22
+ 'my-package@"<0.2.0" "critical bug, please update"': {
23
+ description: 'Deprecate specific versions',
24
+ },
25
+ 'my-package ""': {
26
+ description: 'Un-deprecate a package',
27
+ },
28
+ },
29
+ options: {
30
+ registry: {
31
+ value: '<url>',
32
+ description: 'The registry to update.',
33
+ },
34
+ otp: {
35
+ description: `Provide an OTP to use when deprecating a package.`,
36
+ value: '<otp>',
37
+ },
38
+ },
39
+ });
40
+ export const views = {
41
+ human: result => {
42
+ if (result.message === '') {
43
+ return `✅ Un-deprecated ${result.name}@${result.version} (${result.versions.length} version${result.versions.length === 1 ? '' : 's'})`;
44
+ }
45
+ return `⚠️ Deprecated ${result.name}@${result.version} (${result.versions.length} version${result.versions.length === 1 ? '' : 's'}): ${result.message}`;
46
+ },
47
+ json: r => r,
48
+ };
49
+ export const command = async (conf) => {
50
+ const specArg = conf.positionals[0];
51
+ const message = conf.positionals[1];
52
+ if (!specArg) {
53
+ throw error('deprecate requires a package spec and message argument', {
54
+ code: 'EUSAGE',
55
+ });
56
+ }
57
+ if (message === undefined) {
58
+ throw error('deprecate requires a message argument', {
59
+ code: 'EUSAGE',
60
+ });
61
+ }
62
+ const spec = Spec.parseArgs(specArg, conf.options);
63
+ const { name } = spec;
64
+ if (!name || name === '(unknown)') {
65
+ throw error('could not determine package name from spec', {
66
+ found: specArg,
67
+ });
68
+ }
69
+ const { registry, otp } = conf.options;
70
+ const registryUrl = new URL(registry);
71
+ // Fetch the current packument
72
+ const pic = new PackageInfoClient(conf.options);
73
+ const packument = await pic.packument(spec);
74
+ // Determine which versions match the spec
75
+ const versionRange = spec.bareSpec;
76
+ const matchedVersions = [];
77
+ for (const version of Object.keys(packument.versions)) {
78
+ if (!versionRange || satisfies(version, versionRange)) {
79
+ matchedVersions.push(version);
80
+ }
81
+ }
82
+ if (matchedVersions.length === 0) {
83
+ throw error('no versions found matching the spec', {
84
+ found: specArg,
85
+ wanted: Object.keys(packument.versions),
86
+ });
87
+ }
88
+ // Build the update payload with deprecated field set on matched versions
89
+ const versions = {};
90
+ for (const version of matchedVersions) {
91
+ const manifest = packument.versions[version];
92
+ /* c8 ignore next */
93
+ if (!manifest)
94
+ continue;
95
+ versions[version] = {
96
+ ...manifest,
97
+ ...(message === '' ?
98
+ { deprecated: undefined }
99
+ : { deprecated: message }),
100
+ };
101
+ }
102
+ const body = {
103
+ _id: name,
104
+ name,
105
+ versions,
106
+ };
107
+ const rc = new RegistryClient(conf.options);
108
+ const packageUrl = new URL(name.startsWith('@') ? name.replace('/', '%2F') : name, registryUrl);
109
+ let response;
110
+ try {
111
+ response = await rc.request(packageUrl, {
112
+ method: 'PUT',
113
+ headers: {
114
+ 'content-type': 'application/json',
115
+ 'npm-auth-type': 'web',
116
+ 'npm-command': 'deprecate',
117
+ },
118
+ body: JSON.stringify(body),
119
+ otp,
120
+ });
121
+ }
122
+ catch (err) {
123
+ throw error('failed to update deprecation status', {
124
+ cause: asError(err),
125
+ });
126
+ }
127
+ if (response.statusCode !== 200 && response.statusCode !== 201) {
128
+ throw error('failed to update deprecation status', {
129
+ url: packageUrl,
130
+ response,
131
+ });
132
+ }
133
+ return {
134
+ name,
135
+ version: versionRange || '*',
136
+ message,
137
+ versions: matchedVersions,
138
+ };
139
+ };
@@ -0,0 +1,21 @@
1
+ import type { CommandFn, CommandUsage } from '../index.ts';
2
+ export declare const usage: CommandUsage;
3
+ export type DistTagLsResult = {
4
+ id: string;
5
+ tags: Record<string, string>;
6
+ };
7
+ export type DistTagAddResult = {
8
+ id: string;
9
+ tag: string;
10
+ version: string;
11
+ };
12
+ export type DistTagRmResult = {
13
+ id: string;
14
+ tag: string;
15
+ };
16
+ export type CommandResult = DistTagLsResult | DistTagAddResult | DistTagRmResult;
17
+ export declare const views: {
18
+ readonly human: (result: CommandResult) => string;
19
+ readonly json: (r: CommandResult) => CommandResult;
20
+ };
21
+ export declare const command: CommandFn<CommandResult>;
@@ -0,0 +1,177 @@
1
+ import { error } from '@vltpkg/error-cause';
2
+ import { RegistryClient } from '@vltpkg/registry-client';
3
+ import { Spec } from '@vltpkg/spec';
4
+ import { commandUsage } from "../config/usage.js";
5
+ export const usage = () => commandUsage({
6
+ command: 'dist-tag',
7
+ usage: [
8
+ 'add <pkg>@<version> [<tag>]',
9
+ 'rm <pkg> <tag>',
10
+ 'ls [<pkg>]',
11
+ ],
12
+ description: `Manage distribution tags for a package.
13
+
14
+ Distribution tags (dist-tags) provide aliases for package versions,
15
+ allowing users to install specific versions using tag names instead
16
+ of version numbers. The most common tag is \`latest\`, which is used
17
+ by default when no tag is specified during install.`,
18
+ subcommands: {
19
+ add: {
20
+ usage: '<pkg>@<version> [<tag>]',
21
+ description: 'Tag the specified version of a package with the given tag, or "latest" if unspecified.',
22
+ },
23
+ rm: {
24
+ usage: '<pkg> <tag>',
25
+ description: 'Remove a dist-tag from a package.',
26
+ },
27
+ ls: {
28
+ usage: '[<pkg>]',
29
+ description: 'List all dist-tags for a package, defaulting to the package in the current directory.',
30
+ },
31
+ },
32
+ options: {
33
+ registry: {
34
+ value: '<url>',
35
+ description: 'Registry URL to manage dist-tags for.',
36
+ },
37
+ identity: {
38
+ value: '<name>',
39
+ description: 'Identity namespace used to look up auth tokens.',
40
+ },
41
+ },
42
+ });
43
+ const isLsResult = (r) => 'tags' in r;
44
+ const isAddResult = (r) => 'version' in r;
45
+ export const views = {
46
+ human: result => {
47
+ if (isLsResult(result)) {
48
+ const entries = Object.entries(result.tags);
49
+ if (entries.length === 0)
50
+ return 'No dist-tags found.';
51
+ return entries
52
+ .map(([tag, version]) => `${tag}: ${version}`)
53
+ .join('\n');
54
+ }
55
+ if (isAddResult(result)) {
56
+ return `+${result.tag}: ${result.id}@${result.version}`;
57
+ }
58
+ return `-${result.tag}: ${result.id}`;
59
+ },
60
+ json: r => r,
61
+ };
62
+ /**
63
+ * Build the dist-tags API URL for a package.
64
+ * Uses the `/-/package/{name}/dist-tags` endpoint.
65
+ */
66
+ const distTagsUrl = (name, registry, tag) => {
67
+ const encoded = name.startsWith('@') ? name.replace('/', '%2f') : name;
68
+ const path = tag ?
69
+ `-/package/${encoded}/dist-tags/${encodeURIComponent(tag)}`
70
+ : `-/package/${encoded}/dist-tags`;
71
+ return new URL(path, registry);
72
+ };
73
+ const readPackageName = (positional, conf) => {
74
+ if (positional) {
75
+ const spec = Spec.parseArgs(positional, conf.options);
76
+ return spec.name;
77
+ }
78
+ const manifest = conf.options.packageJson.maybeRead(conf.projectRoot);
79
+ if (manifest?.name)
80
+ return manifest.name;
81
+ throw error('Could not determine package name', {
82
+ code: 'EUSAGE',
83
+ });
84
+ };
85
+ export const command = async (conf) => {
86
+ const [sub, ...args] = conf.positionals;
87
+ if (!sub) {
88
+ throw error('dist-tag command requires a subcommand', {
89
+ code: 'EUSAGE',
90
+ validOptions: ['add', 'rm', 'remove', 'ls', 'list'],
91
+ });
92
+ }
93
+ const rc = new RegistryClient(conf.options);
94
+ const registry = conf.options.registry;
95
+ switch (sub) {
96
+ case 'add': {
97
+ const specArg = args[0];
98
+ if (!specArg) {
99
+ throw error('dist-tag add requires a package@version argument', {
100
+ code: 'EUSAGE',
101
+ });
102
+ }
103
+ const spec = Spec.parseArgs(specArg, conf.options);
104
+ const name = spec.name;
105
+ const version = spec.bareSpec;
106
+ if (!version) {
107
+ throw error('dist-tag add requires a version in the spec', {
108
+ code: 'EUSAGE',
109
+ found: specArg,
110
+ });
111
+ }
112
+ const tag = args[1] ?? conf.options.tag;
113
+ const url = distTagsUrl(name, registry, tag);
114
+ const response = await rc.request(url, {
115
+ method: 'PUT',
116
+ headers: {
117
+ 'content-type': 'application/json',
118
+ },
119
+ body: JSON.stringify(version),
120
+ useCache: false,
121
+ });
122
+ if (response.statusCode < 200 || response.statusCode >= 300) {
123
+ throw error('Failed to add dist-tag', {
124
+ url,
125
+ response,
126
+ });
127
+ }
128
+ return { id: name, tag, version };
129
+ }
130
+ case 'rm':
131
+ case 'remove': {
132
+ const pkgArg = args[0];
133
+ const tag = args[1];
134
+ if (!pkgArg || !tag) {
135
+ throw error('dist-tag rm requires a package name and tag', {
136
+ code: 'EUSAGE',
137
+ });
138
+ }
139
+ const name = readPackageName(pkgArg, conf);
140
+ const url = distTagsUrl(name, registry, tag);
141
+ const response = await rc.request(url, {
142
+ method: 'DELETE',
143
+ useCache: false,
144
+ });
145
+ if (response.statusCode < 200 || response.statusCode >= 300) {
146
+ throw error('Failed to remove dist-tag', {
147
+ url,
148
+ response,
149
+ });
150
+ }
151
+ return { id: name, tag };
152
+ }
153
+ case 'ls':
154
+ case 'list': {
155
+ const name = readPackageName(args[0], conf);
156
+ const url = distTagsUrl(name, registry);
157
+ const response = await rc.request(url, {
158
+ useCache: false,
159
+ });
160
+ if (response.statusCode < 200 || response.statusCode >= 300) {
161
+ throw error('Failed to list dist-tags', {
162
+ url,
163
+ response,
164
+ });
165
+ }
166
+ const tags = response.json();
167
+ return { id: name, tags };
168
+ }
169
+ default: {
170
+ throw error('Invalid dist-tag subcommand', {
171
+ found: sub,
172
+ validOptions: ['add', 'rm', 'remove', 'ls', 'list'],
173
+ code: 'EUSAGE',
174
+ });
175
+ }
176
+ }
177
+ };