@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.
@@ -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: `Create a new package.json file in the current directory.`,
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, { path }] of Object.entries(result)) {
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, { path, data }] of Object.entries(results)) {
34
- output.push(`Wrote ${type} to ${path}:
35
-
36
- ${JSON.stringify(data, null, 2)}`);
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 = (workspacesConfig ?? {});
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
- return init({ cwd: process.cwd() });
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
  };
@@ -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
+ };
@@ -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 command: CommandFn<void>;
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>;
@@ -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: `Explicitly add or remove tokens in the vlt keychain`,
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 = new URL(conf.options.registry).origin;
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
+ };