mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-29 19:00:09 +00:00
Compare commits
24 Commits
@vercel/fr
...
update/ls-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0479338c1 | ||
|
|
37a8037128 | ||
|
|
65fd5c49f9 | ||
|
|
75a19a1b8f | ||
|
|
d2456a21a1 | ||
|
|
628071f659 | ||
|
|
8f02a5d0b1 | ||
|
|
f4cb7b3b8b | ||
|
|
5a7461dfe3 | ||
|
|
88c309ec40 | ||
|
|
e383068427 | ||
|
|
599f8f675c | ||
|
|
d660f110a3 | ||
|
|
0a8bc494fc | ||
|
|
34e008f42e | ||
|
|
9fada88f99 | ||
|
|
0d9082dcb8 | ||
|
|
037633b3f1 | ||
|
|
1a6f3c0270 | ||
|
|
4da688cff3 | ||
|
|
af40cf43ae | ||
|
|
4c100a191c | ||
|
|
a637f2f949 | ||
|
|
f3683fff3f |
@@ -5,7 +5,8 @@
|
||||
"description": "API for the vercel/vercel repo",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"vercel-build": "node ../utils/run.js build all"
|
||||
"//TODO": "We should add this pkg to yarn workspaces",
|
||||
"vercel-build": "cd .. && yarn install && yarn vercel-build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "5.11.1",
|
||||
|
||||
@@ -25,8 +25,8 @@ export default new Map([
|
||||
['logout', 'logout'],
|
||||
['logs', 'logs'],
|
||||
['ls', 'list'],
|
||||
['project', 'projects'],
|
||||
['projects', 'projects'],
|
||||
['project', 'project'],
|
||||
['projects', 'project'],
|
||||
['pull', 'pull'],
|
||||
['remove', 'remove'],
|
||||
['rm', 'remove'],
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import title from 'title';
|
||||
import Now from '../util';
|
||||
import getArgs from '../util/get-args';
|
||||
import { handleError } from '../util/error';
|
||||
import cmd from '../util/output/cmd';
|
||||
import logo from '../util/output/logo';
|
||||
import elapsed from '../util/output/elapsed';
|
||||
import strlen from '../util/strlen';
|
||||
@@ -42,6 +42,8 @@ const help = () => {
|
||||
-m, --meta Filter deployments by metadata (e.g.: ${chalk.dim(
|
||||
'`-m KEY=value`'
|
||||
)}). Can appear many times.
|
||||
-i, --inspect Display dashboard inspect URLs
|
||||
--prod Filter for production URLs
|
||||
-N, --next Show next page of results
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
@@ -77,6 +79,9 @@ export default async function main(client: Client) {
|
||||
'-m': '--meta',
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
'--inspect': Boolean,
|
||||
'-i': '--inspect',
|
||||
'--prod': Boolean,
|
||||
'--confirm': Boolean,
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -99,9 +104,10 @@ export default async function main(client: Client) {
|
||||
}
|
||||
|
||||
const yes = argv['--confirm'] || false;
|
||||
const inspect = argv['--inspect'] || false;
|
||||
const prod = argv['--prod'] || false;
|
||||
|
||||
const meta = parseMeta(argv['--meta']);
|
||||
const { includeScheme } = config;
|
||||
|
||||
let paths = [process.cwd()];
|
||||
const pathValidation = await validatePaths(client, paths);
|
||||
@@ -139,6 +145,7 @@ export default async function main(client: Client) {
|
||||
link.project = linkedProject.project;
|
||||
}
|
||||
|
||||
const isTeam = org?.type === 'team';
|
||||
let { contextName, team } = await getScope(client);
|
||||
|
||||
// If user passed in a custom scope, update the current team & context name
|
||||
@@ -255,37 +262,51 @@ export default async function main(client: Client) {
|
||||
}
|
||||
|
||||
log(
|
||||
`Deployments under ${chalk.bold(contextName)} ${elapsed(
|
||||
`${prod ? `Production deployments` : `Deployments`} for ${chalk.bold(
|
||||
chalk.magenta(app)
|
||||
)} under ${chalk.bold(chalk.magenta(contextName))} ${elapsed(
|
||||
Date.now() - start
|
||||
)}`
|
||||
);
|
||||
|
||||
// information to help the user find other deployments or instances
|
||||
if (app == null) {
|
||||
log(
|
||||
`To list more deployments for a project run ${cmd(
|
||||
`${getCommandName('ls [project]')}`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
log(
|
||||
`To list more deployments for a project, run ${getCommandName(
|
||||
'ls [project]'
|
||||
)}.`
|
||||
);
|
||||
|
||||
print('\n');
|
||||
|
||||
client.output.print(
|
||||
`${table(
|
||||
[
|
||||
['project', 'latest deployment', 'state', 'age', 'username'].map(
|
||||
header => chalk.dim(header)
|
||||
),
|
||||
(isTeam
|
||||
? [
|
||||
'age',
|
||||
inspect ? 'inspect url' : 'deployment url',
|
||||
'state',
|
||||
'duration',
|
||||
'username',
|
||||
]
|
||||
: [
|
||||
'age',
|
||||
inspect ? 'inspect url' : 'deployment url',
|
||||
'state',
|
||||
'duration',
|
||||
]
|
||||
).map(header => chalk.bold(chalk.cyan(header))),
|
||||
...deployments
|
||||
.sort(sortRecent())
|
||||
.map(dep => [
|
||||
.map((dep, i) => [
|
||||
[
|
||||
getProjectName(dep),
|
||||
chalk.bold((includeScheme ? 'https://' : '') + dep.url),
|
||||
stateString(dep.state),
|
||||
chalk.gray(ms(Date.now() - dep.createdAt)),
|
||||
dep.creator.username,
|
||||
i === 0
|
||||
? chalk.bold(`${getDeployUrl(dep, inspect)}`)
|
||||
: `${getDeployUrl(dep, inspect)}`,
|
||||
stateString(dep.state),
|
||||
chalk.gray(getDeploymentDuration(dep)),
|
||||
isTeam ? chalk.gray(dep.creator.username) : '',
|
||||
],
|
||||
])
|
||||
// flatten since the previous step returns a nested
|
||||
@@ -298,8 +319,8 @@ export default async function main(client: Client) {
|
||||
),
|
||||
],
|
||||
{
|
||||
align: ['l', 'l', 'r', 'l', 'l'],
|
||||
hsep: ' '.repeat(4),
|
||||
align: isTeam ? ['l', 'l', 'l', 'l', 'l'] : ['l', 'l', 'l', 'l'],
|
||||
hsep: ' '.repeat(isTeam ? 4 : 5),
|
||||
stringLength: strlen,
|
||||
}
|
||||
).replace(/^/gm, ' ')}\n\n`
|
||||
@@ -315,27 +336,43 @@ export default async function main(client: Client) {
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectName(d: Deployment) {
|
||||
// We group both file and files into a single project
|
||||
if (d.name === 'file') {
|
||||
return 'files';
|
||||
}
|
||||
function getDeployUrl(
|
||||
deployment: Deployment,
|
||||
inspect: boolean | undefined
|
||||
): string {
|
||||
return inspect ? deployment.inspectorUrl : 'https://' + deployment.url;
|
||||
}
|
||||
|
||||
return d.name;
|
||||
export function getDeploymentDuration(dep: Deployment): string {
|
||||
if (!dep || !dep.ready || !dep.buildingAt) {
|
||||
return '?';
|
||||
}
|
||||
const duration = ms(dep.ready - dep.buildingAt);
|
||||
if (duration === '0ms') {
|
||||
return '--';
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
|
||||
// renders the state string
|
||||
export function stateString(s: string) {
|
||||
const CIRCLE = '● ';
|
||||
// make `s` title case
|
||||
const sTitle = title(s);
|
||||
switch (s) {
|
||||
case 'INITIALIZING':
|
||||
return chalk.yellow(s);
|
||||
|
||||
case 'BUILDING':
|
||||
case 'DEPLOYING':
|
||||
case 'ANALYZING':
|
||||
return chalk.yellow(CIRCLE) + sTitle;
|
||||
case 'ERROR':
|
||||
return chalk.red(s);
|
||||
|
||||
return chalk.red(CIRCLE) + sTitle;
|
||||
case 'READY':
|
||||
return s;
|
||||
|
||||
return chalk.green(CIRCLE) + sTitle;
|
||||
case 'QUEUED':
|
||||
return chalk.white(CIRCLE) + sTitle;
|
||||
case 'CANCELED':
|
||||
return chalk.gray(sTitle);
|
||||
default:
|
||||
return chalk.gray('UNKNOWN');
|
||||
}
|
||||
|
||||
55
packages/cli/src/commands/project/add.ts
Normal file
55
packages/cli/src/commands/project/add.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import Client from '../../util/client';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
export default async function add(
|
||||
client: Client,
|
||||
args: string[],
|
||||
contextName: string
|
||||
) {
|
||||
const { output } = client;
|
||||
if (args.length !== 1) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project add <name>')}`
|
||||
)}`
|
||||
);
|
||||
|
||||
if (args.length > 1) {
|
||||
const example = chalk.cyan(
|
||||
`${getCommandName(`project add "${args.join(' ')}"`)}`
|
||||
);
|
||||
output.log(
|
||||
`If your project name has spaces, make sure to wrap it in quotes. Example: \n ${example} `
|
||||
);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const [name] = args;
|
||||
try {
|
||||
await client.fetch('/projects', {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
// project already exists, so we can
|
||||
// show a success message
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
output.log(
|
||||
`${chalk.cyan('Success!')} Project ${chalk.bold(
|
||||
name.toLowerCase()
|
||||
)} added (${chalk.bold(contextName)}) ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
111
packages/cli/src/commands/project/index.ts
Normal file
111
packages/cli/src/commands/project/index.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import chalk from 'chalk';
|
||||
import Client from '../../util/client';
|
||||
import getArgs from '../../util/get-args';
|
||||
import getInvalidSubcommand from '../../util/get-invalid-subcommand';
|
||||
import getScope from '../../util/get-scope';
|
||||
import handleError from '../../util/handle-error';
|
||||
import logo from '../../util/output/logo';
|
||||
import { getPkgName } from '../../util/pkg-name';
|
||||
import validatePaths from '../../util/validate-paths';
|
||||
import add from './add';
|
||||
import list from './list';
|
||||
import rm from './rm';
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} project`)} [options] <command>
|
||||
|
||||
${chalk.dim('Commands:')}
|
||||
|
||||
ls Show all projects in the selected team/user
|
||||
add [name] Add a new project
|
||||
rm [name] Remove a project
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
-S, --scope Set a custom scope
|
||||
-N, --next Show next page of results
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Add a new project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} project add my-project`)}
|
||||
|
||||
${chalk.gray('–')} Paginate projects, where ${chalk.dim(
|
||||
'`1584722256178`'
|
||||
)} is the time in milliseconds since the UNIX epoch.
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} project ls --next 1584722256178`)}
|
||||
`);
|
||||
};
|
||||
|
||||
const COMMAND_CONFIG = {
|
||||
ls: ['ls', 'list'],
|
||||
add: ['add'],
|
||||
rm: ['rm', 'remove'],
|
||||
connect: ['connect'],
|
||||
};
|
||||
|
||||
export default async function main(client: Client) {
|
||||
let argv: any;
|
||||
let subcommand: string | string[];
|
||||
|
||||
try {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
'--yes': Boolean,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
subcommand = argv._[0] || 'list';
|
||||
const args = argv._.slice(1);
|
||||
const { output } = client;
|
||||
|
||||
let paths = [process.cwd()];
|
||||
const pathValidation = await validatePaths(client, paths);
|
||||
if (!pathValidation.valid) {
|
||||
return pathValidation.exitCode;
|
||||
}
|
||||
|
||||
let contextName = '';
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (error) {
|
||||
if (error.code === 'NOT_AUTHORIZED' || error.code === 'TEAM_DELETED') {
|
||||
output.error(error.message);
|
||||
return 1;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
switch (subcommand) {
|
||||
case 'ls':
|
||||
case 'list':
|
||||
return await list(client, argv, args, contextName);
|
||||
case 'add':
|
||||
return await add(client, args, contextName);
|
||||
case 'rm':
|
||||
case 'remove':
|
||||
return await rm(client, args);
|
||||
default:
|
||||
output.error(getInvalidSubcommand(COMMAND_CONFIG));
|
||||
help();
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
86
packages/cli/src/commands/project/list.ts
Normal file
86
packages/cli/src/commands/project/list.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import Client from '../../util/client';
|
||||
import getCommandFlags from '../../util/get-command-flags';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
import strlen from '../../util/strlen';
|
||||
|
||||
export default async function list(
|
||||
client: Client,
|
||||
argv: any,
|
||||
args: string[],
|
||||
contextName: string
|
||||
) {
|
||||
const { output } = client;
|
||||
if (args.length !== 0) {
|
||||
output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project ls')}`
|
||||
)}`
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
output.spinner(`Fetching projects in ${chalk.bold(contextName)}`);
|
||||
|
||||
let projectsUrl = '/v4/projects/?limit=20';
|
||||
|
||||
const next = argv['--next'] || false;
|
||||
if (next) {
|
||||
projectsUrl += `&until=${next}`;
|
||||
}
|
||||
|
||||
const {
|
||||
projects: list,
|
||||
pagination,
|
||||
}: {
|
||||
projects: [{ name: string; updatedAt: number }];
|
||||
pagination: { count: number; next: number };
|
||||
} = await client.fetch(projectsUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
output.stopSpinner();
|
||||
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
output.log(
|
||||
`${list.length > 0 ? 'Projects' : 'No projects'} found under ${chalk.bold(
|
||||
contextName
|
||||
)} ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
|
||||
if (list.length > 0) {
|
||||
const cur = Date.now();
|
||||
const header = [['', 'name', 'updated'].map(title => chalk.dim(title))];
|
||||
|
||||
const out = table(
|
||||
header.concat(
|
||||
list.map(secret => [
|
||||
'',
|
||||
chalk.bold(secret.name),
|
||||
chalk.gray(`${ms(cur - secret.updatedAt)} ago`),
|
||||
])
|
||||
),
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen,
|
||||
}
|
||||
);
|
||||
|
||||
if (out) {
|
||||
output.print(`\n${out}\n\n`);
|
||||
}
|
||||
|
||||
if (pagination && pagination.count === 20) {
|
||||
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
|
||||
const nextCmd = `project ls${flags} --next ${pagination.next}`;
|
||||
output.log(`To display the next page run ${getCommandName(nextCmd)}`);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
63
packages/cli/src/commands/project/rm.ts
Normal file
63
packages/cli/src/commands/project/rm.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import Client from '../../util/client';
|
||||
import { emoji, prependEmoji } from '../../util/emoji';
|
||||
import confirm from '../../util/input/confirm';
|
||||
import { getCommandName } from '../../util/pkg-name';
|
||||
|
||||
const e = encodeURIComponent;
|
||||
|
||||
export default async function rm(client: Client, args: string[]) {
|
||||
if (args.length !== 1) {
|
||||
client.output.error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project rm <name>')}`
|
||||
)}`
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const name = args[0];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const yes = await readConfirmation(client, name);
|
||||
|
||||
if (!yes) {
|
||||
client.output.log('User abort');
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
await client.fetch(`/v2/projects/${e(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
client.output.error('No such project exists');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
client.output.log(
|
||||
`${chalk.cyan('Success!')} Project ${chalk.bold(name)} removed ${chalk.gray(
|
||||
`[${elapsed}]`
|
||||
)}`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function readConfirmation(
|
||||
client: Client,
|
||||
projectName: string
|
||||
): Promise<boolean> {
|
||||
client.output.print(
|
||||
prependEmoji(
|
||||
`The project ${chalk.bold(projectName)} will be removed permanently.\n` +
|
||||
`It will also delete everything under the project including deployments.\n`,
|
||||
emoji('warning')
|
||||
)
|
||||
);
|
||||
|
||||
return await confirm(client, `${chalk.bold.red('Are you sure?')}`, false);
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import ms from 'ms';
|
||||
import table from 'text-table';
|
||||
import strlen from '../util/strlen';
|
||||
import getArgs from '../util/get-args';
|
||||
import { handleError, error } from '../util/error';
|
||||
import exit from '../util/exit';
|
||||
import logo from '../util/output/logo';
|
||||
import getScope from '../util/get-scope';
|
||||
import getCommandFlags from '../util/get-command-flags';
|
||||
import { getPkgName, getCommandName } from '../util/pkg-name';
|
||||
import Client from '../util/client';
|
||||
|
||||
const e = encodeURIComponent;
|
||||
|
||||
const help = () => {
|
||||
console.log(`
|
||||
${chalk.bold(`${logo} ${getPkgName()} projects`)} [options] <command>
|
||||
|
||||
${chalk.dim('Commands:')}
|
||||
|
||||
ls Show all projects in the selected team/user
|
||||
add [name] Add a new project
|
||||
rm [name] Remove a project
|
||||
|
||||
${chalk.dim('Options:')}
|
||||
|
||||
-h, --help Output usage information
|
||||
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
|
||||
'TOKEN'
|
||||
)} Login token
|
||||
-S, --scope Set a custom scope
|
||||
-N, --next Show next page of results
|
||||
|
||||
${chalk.dim('Examples:')}
|
||||
|
||||
${chalk.gray('–')} Add a new project
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} projects add my-project`)}
|
||||
|
||||
${chalk.gray('–')} Paginate projects, where ${chalk.dim(
|
||||
'`1584722256178`'
|
||||
)} is the time in milliseconds since the UNIX epoch.
|
||||
|
||||
${chalk.cyan(`$ ${getPkgName()} projects ls --next 1584722256178`)}
|
||||
`);
|
||||
};
|
||||
|
||||
let argv: any;
|
||||
let subcommand: string | string[];
|
||||
|
||||
const main = async (client: Client) => {
|
||||
try {
|
||||
argv = getArgs(client.argv.slice(2), {
|
||||
'--next': Number,
|
||||
'-N': '--next',
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
argv._ = argv._.slice(1);
|
||||
|
||||
subcommand = argv._[0] || 'list';
|
||||
|
||||
if (argv['--help']) {
|
||||
help();
|
||||
return exit(2);
|
||||
}
|
||||
|
||||
const { output } = client;
|
||||
|
||||
let contextName = null;
|
||||
|
||||
try {
|
||||
({ contextName } = await getScope(client));
|
||||
} catch (err) {
|
||||
if (err.code === 'NOT_AUTHORIZED' || err.code === 'TEAM_DELETED') {
|
||||
output.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await run({ client, contextName });
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export default async (client: Client) => {
|
||||
try {
|
||||
await main(client);
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
async function run({
|
||||
client,
|
||||
contextName,
|
||||
}: {
|
||||
client: Client;
|
||||
contextName: string;
|
||||
}) {
|
||||
const { output } = client;
|
||||
const args = argv._.slice(1);
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
if (subcommand === 'ls' || subcommand === 'list') {
|
||||
if (args.length !== 0) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('projects ls')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
return exit(2);
|
||||
}
|
||||
|
||||
output.spinner(`Fetching projects in ${chalk.bold(contextName)}`);
|
||||
|
||||
let projectsUrl = '/v4/projects/?limit=20';
|
||||
|
||||
const next = argv['--next'];
|
||||
if (next) {
|
||||
projectsUrl += `&until=${next}`;
|
||||
}
|
||||
|
||||
const {
|
||||
projects: list,
|
||||
pagination,
|
||||
}: {
|
||||
projects: [{ name: string; updatedAt: number }];
|
||||
pagination: { count: number; next: number };
|
||||
} = await client.fetch(projectsUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
output.stopSpinner();
|
||||
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
console.log(
|
||||
`> ${
|
||||
list.length > 0 ? 'Projects' : 'No projects'
|
||||
} found under ${chalk.bold(contextName)} ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
|
||||
if (list.length > 0) {
|
||||
const cur = Date.now();
|
||||
const header = [['', 'name', 'updated'].map(title => chalk.dim(title))];
|
||||
|
||||
const out = table(
|
||||
header.concat(
|
||||
list.map(secret => [
|
||||
'',
|
||||
chalk.bold(secret.name),
|
||||
chalk.gray(`${ms(cur - secret.updatedAt)} ago`),
|
||||
])
|
||||
),
|
||||
{
|
||||
align: ['l', 'l', 'l'],
|
||||
hsep: ' '.repeat(2),
|
||||
stringLength: strlen,
|
||||
}
|
||||
);
|
||||
|
||||
if (out) {
|
||||
console.log(`\n${out}\n`);
|
||||
}
|
||||
|
||||
if (pagination && pagination.count === 20) {
|
||||
const flags = getCommandFlags(argv, ['_', '--next', '-N', '-d', '-y']);
|
||||
const nextCmd = `projects ls${flags} --next ${pagination.next}`;
|
||||
console.log(`To display the next page run ${getCommandName(nextCmd)}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'rm' || subcommand === 'remove') {
|
||||
if (args.length !== 1) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('project rm <name>')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
const name = args[0];
|
||||
|
||||
const yes = await readConfirmation(name);
|
||||
if (!yes) {
|
||||
console.error(error('User abort'));
|
||||
return exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
await client.fetch(`/v2/projects/${e(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
console.error(error('No such project exists'));
|
||||
return exit(1);
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Project ${chalk.bold(
|
||||
name
|
||||
)} removed ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
if (args.length !== 1) {
|
||||
console.error(
|
||||
error(
|
||||
`Invalid number of arguments. Usage: ${chalk.cyan(
|
||||
`${getCommandName('projects add <name>')}`
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
||||
if (args.length > 1) {
|
||||
const example = chalk.cyan(
|
||||
`${getCommandName(`projects add "${args.join(' ')}"`)}`
|
||||
);
|
||||
console.log(
|
||||
`> If your project name has spaces, make sure to wrap it in quotes. Example: \n ${example} `
|
||||
);
|
||||
}
|
||||
|
||||
return exit(1);
|
||||
}
|
||||
|
||||
const [name] = args;
|
||||
try {
|
||||
await client.fetch('/projects', {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
// project already exists, so we can
|
||||
// show a success message
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const elapsed = ms(Date.now() - start);
|
||||
|
||||
console.log(
|
||||
`${chalk.cyan('> Success!')} Project ${chalk.bold(
|
||||
name.toLowerCase()
|
||||
)} added (${chalk.bold(contextName)}) ${chalk.gray(`[${elapsed}]`)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(error('Please specify a valid subcommand: ls | add | rm'));
|
||||
help();
|
||||
exit(2);
|
||||
}
|
||||
|
||||
process.on('uncaughtException', err => {
|
||||
handleError(err);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
function readConfirmation(projectName: string) {
|
||||
return new Promise(resolve => {
|
||||
process.stdout.write(
|
||||
`The project: ${chalk.bold(projectName)} will be removed permanently.\n` +
|
||||
`It will also delete everything under the project including deployments.\n`
|
||||
);
|
||||
|
||||
process.stdout.write(
|
||||
`${chalk.bold.red('> Are you sure?')} ${chalk.gray('[y/N] ')}`
|
||||
);
|
||||
|
||||
process.stdin
|
||||
.on('data', d => {
|
||||
process.stdin.pause();
|
||||
resolve(d.toString().trim().toLowerCase() === 'y');
|
||||
})
|
||||
.resume();
|
||||
});
|
||||
}
|
||||
@@ -172,7 +172,8 @@ const main = async () => {
|
||||
// * a subcommand (as in: `vercel ls`)
|
||||
const targetOrSubcommand = argv._[2];
|
||||
|
||||
const betaCommands: string[] = ['build'];
|
||||
// Currently no beta commands - add here as needed
|
||||
const betaCommands: string[] = [''];
|
||||
if (betaCommands.includes(targetOrSubcommand)) {
|
||||
console.log(
|
||||
`${chalk.grey(
|
||||
@@ -652,8 +653,8 @@ const main = async () => {
|
||||
case 'logout':
|
||||
func = require('./commands/logout').default;
|
||||
break;
|
||||
case 'projects':
|
||||
func = require('./commands/projects').default;
|
||||
case 'project':
|
||||
func = require('./commands/project').default;
|
||||
break;
|
||||
case 'pull':
|
||||
func = require('./commands/pull').default;
|
||||
|
||||
@@ -28,7 +28,6 @@ export interface AuthConfig {
|
||||
export interface GlobalConfig {
|
||||
_?: string;
|
||||
currentTeam?: string;
|
||||
includeScheme?: string;
|
||||
collectMetrics?: boolean;
|
||||
api?: string;
|
||||
|
||||
|
||||
@@ -6,16 +6,16 @@ import { exec } from 'child_process';
|
||||
import { GitMetadata } from '../../types';
|
||||
import { Output } from '../output';
|
||||
|
||||
export function isDirty(directory: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
export function isDirty(directory: string, output: Output): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
exec('git status -s', { cwd: directory }, function (err, stdout, stderr) {
|
||||
if (err) return reject(err);
|
||||
if (stderr)
|
||||
return reject(
|
||||
new Error(
|
||||
`Failed to determine if git repo has been modified: ${stderr.trim()}`
|
||||
)
|
||||
);
|
||||
let debugMessage = `Failed to determine if Git repo has been modified:`;
|
||||
if (err || stderr) {
|
||||
if (err) debugMessage += `\n${err}`;
|
||||
if (stderr) debugMessage += `\n${stderr.trim()}`;
|
||||
output.debug(debugMessage);
|
||||
return resolve(false);
|
||||
}
|
||||
resolve(stdout.trim().length > 0);
|
||||
});
|
||||
});
|
||||
@@ -64,10 +64,19 @@ export async function createGitMeta(
|
||||
return;
|
||||
}
|
||||
const [commit, dirty] = await Promise.all([
|
||||
getLastCommit(directory),
|
||||
isDirty(directory),
|
||||
getLastCommit(directory).catch(err => {
|
||||
output.debug(
|
||||
`Failed to get last commit. The directory is likely not a Git repo, there are no latest commits, or it is corrupted.\n${err}`
|
||||
);
|
||||
return;
|
||||
}),
|
||||
isDirty(directory, output),
|
||||
]);
|
||||
|
||||
if (!commit) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
remoteUrl,
|
||||
commitAuthorName: commit.author.name,
|
||||
|
||||
11
packages/cli/test/fixtures/unit/create-git-meta/git-corrupt/git/config
generated
vendored
Normal file
11
packages/cli/test/fixtures/unit/create-git-meta/git-corrupt/git/config
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
fileMode = false
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
[user]
|
||||
name = TechBug2012
|
||||
email = <>
|
||||
[remote "origin"]
|
||||
url = https://github.com/MatthewStanciu/git-test
|
||||
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||
19
packages/cli/test/helpers/parse-table.ts
Normal file
19
packages/cli/test/helpers/parse-table.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function pluckIdentifiersFromDeploymentList(output: string): {
|
||||
project: string | undefined;
|
||||
org: string | undefined;
|
||||
} {
|
||||
const project = output.match(/(?<=Deployments for )(.*)(?= under)/);
|
||||
const org = output.match(/(?<=under )(.*)(?= \[)/);
|
||||
|
||||
return {
|
||||
project: project?.[0],
|
||||
org: org?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSpacedTableRow(output: string): string[] {
|
||||
return output
|
||||
.trim()
|
||||
.replace(/ {1} +/g, ',')
|
||||
.split(',');
|
||||
}
|
||||
23
packages/cli/test/helpers/read-output-stream.ts
Normal file
23
packages/cli/test/helpers/read-output-stream.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { MockClient } from '../mocks/client';
|
||||
|
||||
export function readOutputStream(
|
||||
client: MockClient,
|
||||
length: number = 3
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const timeout = setTimeout(() => {
|
||||
reject();
|
||||
}, 3000);
|
||||
|
||||
client.stderr.resume();
|
||||
client.stderr.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
if (chunks.length === length) {
|
||||
clearTimeout(timeout);
|
||||
resolve(chunks.toString().replace(/,/g, ''));
|
||||
}
|
||||
});
|
||||
client.stderr.on('error', reject);
|
||||
});
|
||||
}
|
||||
11
packages/cli/test/integration.js
vendored
11
packages/cli/test/integration.js
vendored
@@ -1323,12 +1323,7 @@ test('[vc projects] should create a project successfully', async t => {
|
||||
Math.random().toString(36).split('.')[1]
|
||||
}`;
|
||||
|
||||
const vc = execa(binaryPath, [
|
||||
'projects',
|
||||
'add',
|
||||
projectName,
|
||||
...defaultArgs,
|
||||
]);
|
||||
const vc = execa(binaryPath, ['project', 'add', projectName, ...defaultArgs]);
|
||||
|
||||
await waitForPrompt(vc, chunk =>
|
||||
chunk.includes(`Success! Project ${projectName} added`)
|
||||
@@ -1339,7 +1334,7 @@ test('[vc projects] should create a project successfully', async t => {
|
||||
|
||||
// creating the same project again should succeed
|
||||
const vc2 = execa(binaryPath, [
|
||||
'projects',
|
||||
'project',
|
||||
'add',
|
||||
projectName,
|
||||
...defaultArgs,
|
||||
@@ -3517,7 +3512,7 @@ test('`vc --debug project ls` should output the projects listing', async t => {
|
||||
|
||||
t.is(exitCode, 0, formatOutput({ stderr, stdout }));
|
||||
t.true(
|
||||
stdout.includes('> Projects found under'),
|
||||
stderr.includes('> Projects found under'),
|
||||
formatOutput({ stderr, stdout })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,17 +7,22 @@ import { Build, User } from '../../src/types';
|
||||
let deployments = new Map<string, Deployment>();
|
||||
let deploymentBuilds = new Map<Deployment, Build[]>();
|
||||
|
||||
type State = Deployment['readyState'];
|
||||
|
||||
export function useDeployment({
|
||||
creator,
|
||||
state = 'READY',
|
||||
}: {
|
||||
creator: Pick<User, 'id' | 'email' | 'name'>;
|
||||
state?: State;
|
||||
}) {
|
||||
const createdAt = Date.now();
|
||||
const url = new URL(chance().url());
|
||||
|
||||
const deployment: Deployment = {
|
||||
id: `dpl_${chance().guid()}`,
|
||||
url: url.hostname,
|
||||
name: '',
|
||||
name: chance.name,
|
||||
meta: {},
|
||||
regions: [],
|
||||
routes: [],
|
||||
@@ -26,13 +31,15 @@ export function useDeployment({
|
||||
version: 2,
|
||||
createdAt,
|
||||
createdIn: 'sfo1',
|
||||
buildingAt: Date.now(),
|
||||
ownerId: creator.id,
|
||||
creator: {
|
||||
uid: creator.id,
|
||||
email: creator.email,
|
||||
username: creator.name,
|
||||
},
|
||||
readyState: 'READY',
|
||||
readyState: state,
|
||||
state: state,
|
||||
env: {},
|
||||
build: { env: {} },
|
||||
target: 'production',
|
||||
|
||||
@@ -25,8 +25,6 @@ type GetMatcherType<TP, TResult> = TP extends PromiseFunction
|
||||
? (...args: Tail<Parameters<TP>>) => TResult
|
||||
: TP;
|
||||
|
||||
//type T = GetMatcherType<typeof matchers['toOutput'], void>;
|
||||
|
||||
type GetMatchersType<TMatchers, TResult> = {
|
||||
[P in keyof TMatchers]: GetMatcherType<TMatchers[P], TResult>;
|
||||
};
|
||||
|
||||
@@ -157,6 +157,64 @@ export function useProject(project: Partial<Project> = defaultProject) {
|
||||
|
||||
res.json({ envs });
|
||||
});
|
||||
client.scenario.post(`/v4/projects/${project.id}/link`, (req, res) => {
|
||||
const { type, repo, org } = req.body;
|
||||
if (
|
||||
(type === 'github' || type === 'gitlab' || type === 'bitbucket') &&
|
||||
(repo === 'user/repo' || repo === 'user2/repo2')
|
||||
) {
|
||||
project.link = {
|
||||
type,
|
||||
repo,
|
||||
repoId: 1010,
|
||||
org,
|
||||
gitCredentialId: '',
|
||||
sourceless: true,
|
||||
createdAt: 1656109539791,
|
||||
updatedAt: 1656109539791,
|
||||
};
|
||||
res.json(project);
|
||||
} else {
|
||||
if (type === 'github') {
|
||||
res.status(400).json({
|
||||
message: `To link a GitHub repository, you need to install the GitHub integration first. (400)\nInstall GitHub App: https://github.com/apps/vercel`,
|
||||
meta: {
|
||||
action: 'Install GitHub App',
|
||||
link: 'https://github.com/apps/vercel',
|
||||
repo,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
code: 'repo_not_found',
|
||||
message: `The repository "${repo}" couldn't be found in your linked ${formatProvider(
|
||||
type
|
||||
)} account.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
client.scenario.delete(`/v4/projects/${project.id}/link`, (_req, res) => {
|
||||
if (project.link) {
|
||||
project.link = undefined;
|
||||
}
|
||||
res.json(project);
|
||||
});
|
||||
client.scenario.get(`/v4/projects`, (req, res) => {
|
||||
res.json({
|
||||
projects: [defaultProject],
|
||||
pagination: null,
|
||||
});
|
||||
});
|
||||
client.scenario.post(`/projects`, (req, res) => {
|
||||
const { name } = req.body;
|
||||
if (name === project.name) {
|
||||
res.json(project);
|
||||
}
|
||||
});
|
||||
client.scenario.delete(`/:version/projects/${project.id}`, (_req, res) => {
|
||||
res.json({});
|
||||
});
|
||||
|
||||
return { project, envs };
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { client, MockClient } from '../../mocks/client';
|
||||
import { client } from '../../mocks/client';
|
||||
import { useUser } from '../../mocks/user';
|
||||
import list, { stateString } from '../../../src/commands/list';
|
||||
import { join } from 'path';
|
||||
import { useTeams } from '../../mocks/team';
|
||||
import { defaultProject, useProject } from '../../mocks/project';
|
||||
import { useDeployment } from '../../mocks/deployment';
|
||||
import { readOutputStream } from '../../helpers/read-output-stream';
|
||||
import {
|
||||
parseSpacedTableRow,
|
||||
pluckIdentifiersFromDeploymentList,
|
||||
} from '../../helpers/parse-table';
|
||||
|
||||
const fixture = (name: string) =>
|
||||
join(__dirname, '../../fixtures/unit/commands/list', name);
|
||||
@@ -30,11 +35,12 @@ describe('list', () => {
|
||||
|
||||
await list(client);
|
||||
|
||||
const output = await readOutputStream(client);
|
||||
const output = await readOutputStream(client, 4);
|
||||
|
||||
const { org } = getDataFromIntro(output.split('\n')[0]);
|
||||
const header: string[] = parseTable(output.split('\n')[2]);
|
||||
const data: string[] = parseTable(output.split('\n')[3]);
|
||||
const { org } = pluckIdentifiersFromDeploymentList(output.split('\n')[0]);
|
||||
const header: string[] = parseSpacedTableRow(output.split('\n')[3]);
|
||||
const data: string[] = parseSpacedTableRow(output.split('\n')[4]);
|
||||
data.shift();
|
||||
data.splice(2, 1);
|
||||
|
||||
expect(org).toEqual(team[0].slug);
|
||||
@@ -47,7 +53,7 @@ describe('list', () => {
|
||||
]);
|
||||
|
||||
expect(data).toEqual([
|
||||
deployment.url,
|
||||
`https://${deployment.url}`,
|
||||
stateString(deployment.state || ''),
|
||||
user.name,
|
||||
]);
|
||||
@@ -72,11 +78,12 @@ describe('list', () => {
|
||||
client.setArgv(deployment.name);
|
||||
await list(client);
|
||||
|
||||
const output = await readOutputStream(client);
|
||||
const output = await readOutputStream(client, 4);
|
||||
|
||||
const { org } = getDataFromIntro(output.split('\n')[0]);
|
||||
const header: string[] = parseTable(output.split('\n')[2]);
|
||||
const data: string[] = parseTable(output.split('\n')[3]);
|
||||
const { org } = pluckIdentifiersFromDeploymentList(output.split('\n')[0]);
|
||||
const header: string[] = parseSpacedTableRow(output.split('\n')[3]);
|
||||
const data: string[] = parseSpacedTableRow(output.split('\n')[4]);
|
||||
data.shift();
|
||||
data.splice(2, 1);
|
||||
|
||||
expect(org).toEqual(teamSlug);
|
||||
@@ -89,7 +96,7 @@ describe('list', () => {
|
||||
'username',
|
||||
]);
|
||||
expect(data).toEqual([
|
||||
deployment.url,
|
||||
`https://${deployment.url}`,
|
||||
stateString(deployment.state || ''),
|
||||
user.name,
|
||||
]);
|
||||
@@ -98,42 +105,3 @@ describe('list', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getDataFromIntro(output: string): {
|
||||
project: string | undefined;
|
||||
org: string | undefined;
|
||||
} {
|
||||
const project = output.match(/(?<=Deployments for )(.*)(?= under)/);
|
||||
const org = output.match(/(?<=under )(.*)(?= \[)/);
|
||||
|
||||
return {
|
||||
project: project?.[0],
|
||||
org: org?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
function parseTable(output: string): string[] {
|
||||
return output
|
||||
.trim()
|
||||
.replace(/ {3} +/g, ',')
|
||||
.split(',');
|
||||
}
|
||||
|
||||
function readOutputStream(client: MockClient): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const timeout = setTimeout(() => {
|
||||
reject();
|
||||
}, 3000);
|
||||
|
||||
client.stderr.resume();
|
||||
client.stderr.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
if (chunks.length === 3) {
|
||||
clearTimeout(timeout);
|
||||
resolve(chunks.toString().replace(/,/g, ''));
|
||||
}
|
||||
});
|
||||
client.stderr.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
97
packages/cli/test/unit/commands/project.test.ts
Normal file
97
packages/cli/test/unit/commands/project.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import projects from '../../../src/commands/project';
|
||||
import { useUser } from '../../mocks/user';
|
||||
import { useTeams } from '../../mocks/team';
|
||||
import { defaultProject, useProject } from '../../mocks/project';
|
||||
import { client } from '../../mocks/client';
|
||||
import { Project } from '../../../src/types';
|
||||
import { readOutputStream } from '../../helpers/read-output-stream';
|
||||
import {
|
||||
pluckIdentifiersFromDeploymentList,
|
||||
parseSpacedTableRow,
|
||||
} from '../../helpers/parse-table';
|
||||
|
||||
describe('project', () => {
|
||||
describe('list', () => {
|
||||
it('should list deployments under a user', async () => {
|
||||
const user = useUser();
|
||||
const project = useProject({
|
||||
...defaultProject,
|
||||
});
|
||||
|
||||
client.setArgv('project', 'ls');
|
||||
await projects(client);
|
||||
|
||||
const output = await readOutputStream(client, 2);
|
||||
const { org } = pluckIdentifiersFromDeploymentList(output.split('\n')[0]);
|
||||
const header: string[] = parseSpacedTableRow(output.split('\n')[2]);
|
||||
const data: string[] = parseSpacedTableRow(output.split('\n')[3]);
|
||||
data.pop();
|
||||
|
||||
expect(org).toEqual(user.username);
|
||||
expect(header).toEqual(['name', 'updated']);
|
||||
expect(data).toEqual([project.project.name]);
|
||||
});
|
||||
it('should list deployments for a team', async () => {
|
||||
useUser();
|
||||
const team = useTeams('team_dummy');
|
||||
const project = useProject({
|
||||
...defaultProject,
|
||||
});
|
||||
|
||||
client.config.currentTeam = team[0].id;
|
||||
client.setArgv('project', 'ls');
|
||||
await projects(client);
|
||||
|
||||
const output = await readOutputStream(client, 2);
|
||||
const { org } = pluckIdentifiersFromDeploymentList(output.split('\n')[0]);
|
||||
const header: string[] = parseSpacedTableRow(output.split('\n')[2]);
|
||||
const data: string[] = parseSpacedTableRow(output.split('\n')[3]);
|
||||
data.pop();
|
||||
|
||||
expect(org).toEqual(team[0].slug);
|
||||
expect(header).toEqual(['name', 'updated']);
|
||||
expect(data).toEqual([project.project.name]);
|
||||
});
|
||||
});
|
||||
describe('add', () => {
|
||||
it('should add a project', async () => {
|
||||
const user = useUser();
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'test-project',
|
||||
name: 'test-project',
|
||||
});
|
||||
|
||||
client.setArgv('project', 'add', 'test-project');
|
||||
await projects(client);
|
||||
|
||||
const project: Project = await client.fetch(`/v8/projects/test-project`);
|
||||
expect(project).toBeDefined();
|
||||
|
||||
expect(client.stderr).toOutput(
|
||||
`Success! Project test-project added (${user.username})`
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('rm', () => {
|
||||
it('should remove a project', async () => {
|
||||
useUser();
|
||||
useProject({
|
||||
...defaultProject,
|
||||
id: 'test-project',
|
||||
name: 'test-project',
|
||||
});
|
||||
|
||||
client.setArgv('project', 'rm', 'test-project');
|
||||
const projectsPromise = projects(client);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`The project test-project will be removed permanently.`
|
||||
);
|
||||
client.stdin.write('y\n');
|
||||
|
||||
const exitCode = await projectsPromise;
|
||||
expect(exitCode).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { join } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import { getWriteableDirectory } from '@vercel/build-utils';
|
||||
import {
|
||||
createGitMeta,
|
||||
@@ -41,7 +42,7 @@ describe('createGitMeta', () => {
|
||||
const directory = fixture('dirty');
|
||||
try {
|
||||
await fs.rename(join(directory, 'git'), join(directory, '.git'));
|
||||
const dirty = await isDirty(directory);
|
||||
const dirty = await isDirty(directory, client.output);
|
||||
expect(dirty).toBeTruthy();
|
||||
} finally {
|
||||
await fs.rename(join(directory, '.git'), join(directory, 'git'));
|
||||
@@ -51,7 +52,7 @@ describe('createGitMeta', () => {
|
||||
const directory = fixture('not-dirty');
|
||||
try {
|
||||
await fs.rename(join(directory, 'git'), join(directory, '.git'));
|
||||
const dirty = await isDirty(directory);
|
||||
const dirty = await isDirty(directory, client.output);
|
||||
expect(dirty).toBeFalsy();
|
||||
} finally {
|
||||
await fs.rename(join(directory, '.git'), join(directory, 'git'));
|
||||
@@ -125,4 +126,27 @@ describe('createGitMeta', () => {
|
||||
await fs.rename(join(directory, '.git'), join(directory, 'git'));
|
||||
}
|
||||
});
|
||||
it('fails when `.git` is corrupt', async () => {
|
||||
const directory = fixture('git-corrupt');
|
||||
const tmpDir = join(os.tmpdir(), 'git-corrupt');
|
||||
try {
|
||||
// Copy the fixture into a temp dir so that we don't pick
|
||||
// up Git information from the `vercel/vercel` repo itself
|
||||
await fs.copy(directory, tmpDir);
|
||||
await fs.rename(join(tmpDir, 'git'), join(tmpDir, '.git'));
|
||||
|
||||
client.output.debugEnabled = true;
|
||||
const data = await createGitMeta(tmpDir, client.output);
|
||||
|
||||
await expect(client.stderr).toOutput(
|
||||
`Failed to get last commit. The directory is likely not a Git repo, there are no latest commits, or it is corrupted.`
|
||||
);
|
||||
await expect(client.stderr).toOutput(
|
||||
`Failed to determine if Git repo has been modified:`
|
||||
);
|
||||
expect(data).toBeUndefined();
|
||||
} finally {
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,6 +53,8 @@ export interface Deployment {
|
||||
| 'BUILDING'
|
||||
| 'DEPLOYING'
|
||||
| 'READY'
|
||||
| 'QUEUED'
|
||||
| 'CANCELED'
|
||||
| 'ERROR';
|
||||
state?:
|
||||
| 'INITIALIZING'
|
||||
@@ -60,6 +62,8 @@ export interface Deployment {
|
||||
| 'BUILDING'
|
||||
| 'DEPLOYING'
|
||||
| 'READY'
|
||||
| 'QUEUED'
|
||||
| 'CANCELED'
|
||||
| 'ERROR';
|
||||
createdAt: number;
|
||||
createdIn: string;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
],
|
||||
"build": {
|
||||
"env": {
|
||||
"ENABLE_FILE_SYSTEM_API": "1"
|
||||
"ENABLE_VC_BUILD": "1"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
|
||||
Reference in New Issue
Block a user