Skip to content

Commit eb79e55

Browse files
committed
✨ Add orgs and projects commands for discovery
- Add `vizzly orgs` to list organizations user has access to - Add `vizzly projects` to list projects (with --org filter) - Both commands support --json for scripting/LLM use - Include in Account help category
1 parent 2e3767b commit eb79e55

File tree

3 files changed

+287
-1
lines changed

3 files changed

+287
-1
lines changed

src/cli.js

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { init } from './commands/init.js';
2121
import { loginCommand, validateLoginOptions } from './commands/login.js';
2222
import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
23+
import { orgsCommand, validateOrgsOptions } from './commands/orgs.js';
2324
import { previewCommand, validatePreviewOptions } from './commands/preview.js';
2425
import {
2526
projectListCommand,
@@ -28,6 +29,10 @@ import {
2829
projectTokenCommand,
2930
validateProjectOptions,
3031
} from './commands/project.js';
32+
import {
33+
projectsCommand,
34+
validateProjectsOptions,
35+
} from './commands/projects.js';
3136
import {
3237
approveCommand,
3338
commentCommand,
@@ -131,7 +136,7 @@ const formatHelp = (cmd, helper) => {
131136
key: 'auth',
132137
icon: '▸',
133138
title: 'Account',
134-
names: ['login', 'logout', 'whoami'],
139+
names: ['login', 'logout', 'whoami', 'orgs', 'projects'],
135140
},
136141
{
137142
key: 'project',
@@ -931,6 +936,75 @@ Workflow:
931936
await commentCommand(buildId, message, options, globalOptions);
932937
});
933938

939+
program
940+
.command('orgs')
941+
.description('List organizations you have access to')
942+
.addHelpText(
943+
'after',
944+
`
945+
Examples:
946+
$ vizzly orgs # List all organizations
947+
$ vizzly orgs --json # Output as JSON for scripting
948+
949+
Note: Shows organizations from your user account (via vizzly login)
950+
or the single organization for a project token.
951+
`
952+
)
953+
.action(async options => {
954+
const globalOptions = program.opts();
955+
956+
const validationErrors = validateOrgsOptions(options);
957+
if (validationErrors.length > 0) {
958+
output.error('Validation errors:');
959+
for (let error of validationErrors) {
960+
output.printErr(` - ${error}`);
961+
}
962+
process.exit(1);
963+
}
964+
965+
await orgsCommand(options, globalOptions);
966+
});
967+
968+
program
969+
.command('projects')
970+
.description('List projects you have access to')
971+
.option('--org <slug>', 'Filter by organization slug')
972+
.option(
973+
'--limit <n>',
974+
'Maximum results to return (1-250)',
975+
val => parseInt(val, 10),
976+
50
977+
)
978+
.option('--offset <n>', 'Skip first N results', val => parseInt(val, 10), 0)
979+
.addHelpText(
980+
'after',
981+
`
982+
Examples:
983+
$ vizzly projects # List all projects
984+
$ vizzly projects --org my-company # Filter by organization
985+
$ vizzly projects --json # Output as JSON for scripting
986+
987+
Workflow:
988+
1. List orgs: vizzly orgs
989+
2. List projects: vizzly projects --org <org-slug>
990+
3. Query builds: vizzly builds
991+
`
992+
)
993+
.action(async options => {
994+
const globalOptions = program.opts();
995+
996+
const validationErrors = validateProjectsOptions(options);
997+
if (validationErrors.length > 0) {
998+
output.error('Validation errors:');
999+
for (let error of validationErrors) {
1000+
output.printErr(` - ${error}`);
1001+
}
1002+
process.exit(1);
1003+
}
1004+
1005+
await projectsCommand(options, globalOptions);
1006+
});
1007+
9341008
program
9351009
.command('finalize')
9361010
.description('Finalize a parallel build after all shards complete')

src/commands/orgs.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Organizations command - List organizations the user has access to
3+
*/
4+
5+
import { createApiClient } from '../api/client.js';
6+
import { loadConfig } from '../utils/config-loader.js';
7+
import { getApiUrl } from '../utils/environment-config.js';
8+
import * as output from '../utils/output.js';
9+
10+
/**
11+
* Organizations command implementation
12+
* @param {Object} options - Command options
13+
* @param {Object} globalOptions - Global CLI options
14+
*/
15+
export async function orgsCommand(_options = {}, globalOptions = {}) {
16+
output.configure({
17+
json: globalOptions.json,
18+
verbose: globalOptions.verbose,
19+
color: !globalOptions.noColor,
20+
});
21+
22+
try {
23+
let config = await loadConfig(globalOptions.config, globalOptions);
24+
25+
if (!config.apiKey) {
26+
output.error(
27+
'API token required. Use --token, set VIZZLY_TOKEN, or run "vizzly login"'
28+
);
29+
process.exit(1);
30+
}
31+
32+
let client = createApiClient({
33+
baseUrl: config.apiUrl || getApiUrl(),
34+
token: config.apiKey,
35+
});
36+
37+
output.startSpinner('Fetching organizations...');
38+
39+
let response = await client.request('/api/sdk/organizations');
40+
41+
output.stopSpinner();
42+
43+
let orgs = response.organizations || [];
44+
45+
if (globalOptions.json) {
46+
output.data({
47+
organizations: orgs.map(org => ({
48+
id: org.id,
49+
name: org.name,
50+
slug: org.slug,
51+
role: org.role,
52+
projectCount: org.projectCount,
53+
createdAt: org.created_at,
54+
})),
55+
count: orgs.length,
56+
});
57+
} else {
58+
output.header('orgs');
59+
60+
let colors = output.getColors();
61+
62+
if (orgs.length === 0) {
63+
output.print(' No organizations found');
64+
} else {
65+
output.labelValue('Count', String(orgs.length));
66+
output.blank();
67+
68+
for (let org of orgs) {
69+
let roleLabel = org.role === 'token' ? 'via token' : org.role;
70+
output.print(
71+
` ${colors.bold(org.name)} ${colors.dim(`@${org.slug}`)}`
72+
);
73+
output.print(
74+
` ${colors.dim(`${org.projectCount} projects · ${roleLabel}`)}`
75+
);
76+
}
77+
}
78+
}
79+
80+
output.cleanup();
81+
} catch (error) {
82+
output.stopSpinner();
83+
output.error('Failed to fetch organizations', error);
84+
process.exit(1);
85+
}
86+
}
87+
88+
/**
89+
* Validate orgs options
90+
* @param {Object} _options - Command options
91+
* @returns {string[]} Validation errors
92+
*/
93+
export function validateOrgsOptions(_options = {}) {
94+
return [];
95+
}

src/commands/projects.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Projects command - List projects the user has access to
3+
*/
4+
5+
import { createApiClient } from '../api/client.js';
6+
import { loadConfig } from '../utils/config-loader.js';
7+
import { getApiUrl } from '../utils/environment-config.js';
8+
import * as output from '../utils/output.js';
9+
10+
/**
11+
* Projects command implementation
12+
* @param {Object} options - Command options
13+
* @param {Object} globalOptions - Global CLI options
14+
*/
15+
export async function projectsCommand(options = {}, globalOptions = {}) {
16+
output.configure({
17+
json: globalOptions.json,
18+
verbose: globalOptions.verbose,
19+
color: !globalOptions.noColor,
20+
});
21+
22+
try {
23+
let config = await loadConfig(globalOptions.config, globalOptions);
24+
25+
if (!config.apiKey) {
26+
output.error(
27+
'API token required. Use --token, set VIZZLY_TOKEN, or run "vizzly login"'
28+
);
29+
process.exit(1);
30+
}
31+
32+
let client = createApiClient({
33+
baseUrl: config.apiUrl || getApiUrl(),
34+
token: config.apiKey,
35+
});
36+
37+
// Build query params
38+
let params = new URLSearchParams();
39+
if (options.org) params.set('organization', options.org);
40+
if (options.limit) params.set('limit', String(options.limit));
41+
if (options.offset) params.set('offset', String(options.offset));
42+
43+
let queryString = params.toString();
44+
let endpoint = `/api/sdk/projects${queryString ? `?${queryString}` : ''}`;
45+
46+
output.startSpinner('Fetching projects...');
47+
48+
let response = await client.request(endpoint);
49+
50+
output.stopSpinner();
51+
52+
let projects = response.projects || [];
53+
let pagination = response.pagination || {};
54+
55+
if (globalOptions.json) {
56+
output.data({
57+
projects: projects.map(p => ({
58+
id: p.id,
59+
name: p.name,
60+
slug: p.slug,
61+
organizationName: p.organizationName,
62+
organizationSlug: p.organizationSlug,
63+
buildCount: p.buildCount,
64+
createdAt: p.created_at,
65+
updatedAt: p.updated_at,
66+
})),
67+
pagination,
68+
});
69+
} else {
70+
output.header('projects');
71+
72+
let colors = output.getColors();
73+
74+
if (projects.length === 0) {
75+
output.print(' No projects found');
76+
if (options.org) {
77+
output.hint(`No projects in organization "${options.org}"`);
78+
}
79+
} else {
80+
output.labelValue(
81+
'Showing',
82+
`${projects.length} of ${pagination.total}`
83+
);
84+
output.blank();
85+
86+
for (let project of projects) {
87+
output.print(
88+
` ${colors.bold(project.name)} ${colors.dim(`@${project.organizationSlug}/${project.slug}`)}`
89+
);
90+
output.print(` ${colors.dim(`${project.buildCount} builds`)}`);
91+
}
92+
93+
if (pagination.hasMore) {
94+
output.blank();
95+
output.hint(
96+
`Use --offset ${(options.offset || 0) + projects.length} to see more`
97+
);
98+
}
99+
}
100+
}
101+
102+
output.cleanup();
103+
} catch (error) {
104+
output.stopSpinner();
105+
output.error('Failed to fetch projects', error);
106+
process.exit(1);
107+
}
108+
}
109+
110+
/**
111+
* Validate projects options
112+
* @param {Object} _options - Command options
113+
* @returns {string[]} Validation errors
114+
*/
115+
export function validateProjectsOptions(_options = {}) {
116+
return [];
117+
}

0 commit comments

Comments
 (0)