Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,22 @@ If the configured port is busy, `tdd run` picks an available one. An explicit

### Review in the cloud

Sign in to discover projects and read their builds from any directory:

```bash
pnpm exec vizzly login
pnpm exec vizzly project link your-org/your-project
pnpm exec vizzly orgs
pnpm exec vizzly projects --org your-org
pnpm exec vizzly builds --project your-project --org your-org
```

Browsing doesn't change your checkout's upload destination. When you're ready
to upload, choose a project interactively or supply its qualified name:

```bash
pnpm exec vizzly project link
# Or: pnpm exec vizzly project link your-org/your-project
pnpm exec vizzly project status
pnpm exec vizzly run "pnpm test"
```

Expand All @@ -89,6 +102,34 @@ Replace `"pnpm test"` with your test command.
If you linked with an older CLI, run `project link` once in each repository to
save its project selection. Handwritten config may need a manual edit; the
command prints what to add. Explicit `--token` and `VIZZLY_TOKEN` still work.
Before an upload, Vizzly checks the token's project and shows the destination.
If it conflicts with your checkout's configured project, fix the binding or
remove the token override before uploading.

An explicit token can expose fewer projects than your user account. Use
`--auth user` to browse with your saved login while keeping that upload token:

```bash
pnpm exec vizzly projects --auth user
pnpm exec vizzly builds --auth user --project another-project --org another-org
```

Cloud read commands accept `--auth auto|user|token`. The default, `auto`, uses
an explicit flag, environment, or config token first, then your saved login,
then the checkout's linked credential. `user` selects only your saved login;
`token` selects only an upload or explicit API credential. `--token` and
`--auth user` cannot be combined. User login and upload credentials stay separate.

Lists show a page of accessible results and tell you how to fetch the next one.
Use `--limit` and `--offset` for scripts; JSON keeps paging details under
`pagination`. `orgs` and `projects` also report the credential source under `auth`.
A project token can see only its own project. Organization/project discovery
now honors explicit token settings consistently with other cloud reads.

Vizzly refreshes an expired or nearly expired selected login automatically.
If the saved session is rejected, run `vizzly login` and retry your command.
Connection or server errors preserve your saved session and ask you to retry.
Submitted API writes are never automatically replayed after an auth failure.

To upload screenshots you already have:

Expand Down Expand Up @@ -218,7 +259,8 @@ with `pnpm exec` when Vizzly is installed locally.
| --- | --- |
| `vizzly init` | Walk through setup. Use `--quick` to create config without questions. |
| `vizzly config [key]` | Show your current config. |
| `vizzly project link <org>/<project>` | Link this checkout to a cloud project. |
| `vizzly project link [org/project]` | Choose this checkout's upload project; omit the name for a picker. |
| `vizzly project status` | Show the configured and effective upload destinations. |
| `vizzly orgs` | List your organizations. |
| `vizzly projects` | List projects you can access. |
| `vizzly login` | Sign in through the browser. |
Expand Down
27 changes: 27 additions & 0 deletions skills/vizzly/references/cli-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,33 @@ before trying again. Treat schema examples as argument arrays, not shell scripts

## Choose the evidence

Discover accessible organizations and projects before guessing a destination:

```bash
vizzly orgs --auth user --json
vizzly projects --auth user --json
vizzly builds --auth user --project <project-slug> --org <org-slug> --json
```

These reads don't require linking a checkout. `--auth user` selects the saved
login even when the repo launcher exports a project token. A project token sees
only its own project. In `auto` mode, explicit flag/env/config credentials win
over a saved login; a saved login wins over an implicit linked upload credential.
Use `--auth token` to deliberately inspect the token's view. Don't combine
`--auth user` with `--token`.

Follow `pagination.nextOffset` when `pagination.hasMore` is true. A list command
returns one logical page; large limits can require several bounded API requests.
If the next offset is null while more results exist, narrow your filters to stay
within the API's offset limit. An expired selected login is refreshed automatically;
a rejected session asks for login. Report the recovery action when interactive
login isn't authorized. Network failures preserve the session.

`vizzly project status --json` describes configured and effective upload
destinations separately. Use `project link` only when setup or changing the
upload destination is authorized. It issues or reuses a project credential and
binds the checkout. Browsing another project must not change that binding.

Use the ID supplied by the task. If no cloud build is named, find recent builds
and choose the one matching the branch, commit, or pull request:

Expand Down
51 changes: 37 additions & 14 deletions src/adapters/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
} from '../../domains/previews/preview.commands.js';
import { orgsCommand } from '../../domains/projects/orgs.commands.js';
import { projectLinkCommand } from '../../domains/projects/project.commands.js';
import { validateProjectLinkOptions } from '../../domains/projects/project.validations.js';
import { projectStatusCommand } from '../../domains/projects/project-status.commands.js';
import {
projectsCommand,
validateProjectsOptions,
Expand Down Expand Up @@ -589,6 +589,12 @@ async function finishUpdateNotifications() {

program.hook('preAction', async (_command, actionCommand) => {
if (actionCommand.name() === 'init') return;
if (
['run', 'upload', 'login'].includes(actionCommand.name()) ||
(actionCommand.parent?.name() === 'tdd' && actionCommand.name() === 'start')
) {
await saveUserPath().catch(() => {});
}
updateOptions = actionCommand.optsWithGlobals();
let notification = await notifyUpdates(updateOptions);
pendingUpdateCheck = notification?.check;
Expand Down Expand Up @@ -978,6 +984,7 @@ program

program
.command('status')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('Check the status of a build')
.argument('<build-id>', 'Build ID to check status for')
.action(async (buildId, options) => {
Expand All @@ -994,6 +1001,7 @@ program

program
.command('builds')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('List and query builds')
.option('-b, --build <id>', 'Get a specific build by ID')
.option('--branch <branch>', 'Filter by branch')
Expand Down Expand Up @@ -1035,6 +1043,7 @@ Examples:

program
.command('comparisons')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('Query and search comparisons')
.option('-b, --build <id>', 'Get comparisons for a specific build')
.option('--id <id>', 'Get a specific comparison by ID')
Expand Down Expand Up @@ -1078,6 +1087,7 @@ let contextCmd = program

contextCmd
.command('build')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('Fetch build context')
.argument('<build-id>', 'Build ID to fetch context for')
.option('--source <source>', 'Context source: auto, cloud, or local', 'auto')
Expand Down Expand Up @@ -1118,6 +1128,7 @@ Deprecation:

contextCmd
.command('comparison')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('Fetch a comparison context bundle')
.argument('<comparison-id>', 'Comparison ID to fetch context for')
.option('--source <source>', 'Context source: auto, cloud, or local', 'auto')
Expand Down Expand Up @@ -1163,6 +1174,7 @@ Deprecation:

contextCmd
.command('screenshot')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('Fetch screenshot context and historical memory')
.argument('<name>', 'Screenshot name')
.option('--source <source>', 'Context source: auto, cloud, or local', 'auto')
Expand Down Expand Up @@ -1195,6 +1207,7 @@ Examples:

contextCmd
.command('similar')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('Fetch project-scoped matches for a fingerprint hash')
.argument('<fingerprint-hash>', 'Fingerprint hash to search for')
.option('--source <source>', 'Context source: auto, cloud, or local', 'auto')
Expand Down Expand Up @@ -1227,6 +1240,7 @@ Examples:

contextCmd
.command('review-queue')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('Fetch pending review context for a project')
.option('--source <source>', 'Context source: auto, cloud, or local', 'auto')
.option('-p, --project <slug-or-id>', 'Project scope for user auth lookups')
Expand Down Expand Up @@ -1291,6 +1305,7 @@ Note: Baselines are stored locally in .vizzly/baselines/ during TDD mode.

let api = program
.command('api')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('Make raw API requests (for power users)')
.argument('<endpoint>', 'API endpoint (e.g., /api/sdk/builds)')
.option(
Expand Down Expand Up @@ -1463,12 +1478,15 @@ Workflow:

program
.command('orgs')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('List organizations you have access to')
.option('--limit <n>', 'Maximum results to return (1-250)', Number, 50)
.option('--offset <n>', 'Skip first N results', Number, 0)
.addHelpText(
'after',
`
Examples:
$ vizzly orgs # List all organizations
$ vizzly orgs # List accessible organizations
$ vizzly orgs --json # Output as JSON for scripting

Note: Shows organizations from your user account (via vizzly login)
Expand All @@ -1483,6 +1501,7 @@ or the single organization for a project token.

program
.command('projects')
.option('--auth <source>', 'Credential source: auto, user, token', 'auto')
.description('List projects you have access to')
.option('--org <slug>', 'Filter by organization slug')
.option('--limit <n>', 'Maximum results to return (1-250)', Number, 50)
Expand All @@ -1491,14 +1510,17 @@ program
'after',
`
Examples:
$ vizzly projects # List all projects
$ vizzly projects # List accessible projects
$ vizzly projects --org my-company # Filter by organization
$ vizzly projects --json # Output as JSON for scripting

Workflow:
1. List orgs: vizzly orgs
2. List projects: vizzly projects --org <org-slug>
3. Query builds: vizzly builds
3. Query builds: vizzly builds --project <project-slug> --org <org-slug>

Use --auth user to browse your account when VIZZLY_TOKEN selects a project.
Linking is needed only to choose a checkout upload destination.
`
)
.action(async options => {
Expand All @@ -1514,10 +1536,19 @@ Workflow:

let projectCommand = program
.command('project')
.description('Manage the project linked to this local checkout')
.description('Show or choose the upload destination')
.addHelpText(
'after',
'\nDiscover: vizzly orgs, vizzly projects --auth user\nBrowse: vizzly builds --auth user --project <slug> --org <slug>\nChoose uploads: vizzly project link\nInspect uploads: vizzly project status\nBrowsing another project does not change this checkout’s upload destination.\n'
)
.showHelpAfterError('(Run vizzly project --help for available commands)')
.showSuggestionAfterError();

projectCommand
.command('status')
.description('Show configured and effective upload destinations')
.action(async options => projectStatusCommand(options, getGlobalOptions()));

projectCommand
.command('link [selector]')
.description('Link a Vizzly project for cloud uploads')
Expand All @@ -1529,6 +1560,7 @@ projectCommand
'after',
`
Examples:
$ vizzly project link # Choose from accessible projects
$ vizzly project link vizzly/storybook
$ vizzly project link --org vizzly --project storybook

Expand All @@ -1539,11 +1571,6 @@ used for cloud uploads; your user login remains separate for review actions.
.action(async (selector, options) => {
let globalOptions = getGlobalOptions();

const validationErrors = validateProjectLinkOptions(selector, options);
if (validationErrors.length > 0) {
await reportValidationErrors(validationErrors);
}

await projectLinkCommand(selector, options, globalOptions, commandDeps);
});

Expand Down Expand Up @@ -1652,10 +1679,6 @@ program
await whoamiCommand(options, globalOptions, commandDeps);
});

// Save PATH for the menubar app before commands read or update credentials
// in the same config file.
if (needsRuntimeBootstrap) await saveUserPath().catch(() => {});

let commandNames = new Set(program.commands.map(command => command.name()));
let nestedCommandNames = new Map(
program.commands.map(command => [
Expand Down
40 changes: 25 additions & 15 deletions src/adapters/cli/setup.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { loadConfig } from '../../domains/config/config-loader.js';
import { projectLinkCommand } from '../../domains/projects/project.commands.js';
import { pickProject } from '../../domains/projects/project-picker.service.js';

function runProcess(command, args) {
return new Promise((resolve, reject) => {
Expand All @@ -18,21 +20,29 @@ function runProcess(command, args) {
});
}

export async function connectProject(project, options) {
let exitCode = 0;
let link = () =>
projectLinkCommand(project, {}, options, {
exit: code => {
exitCode = code;
},
});
let result = await link();
if (result?.reason === 'auth_required') {
export async function connectProject(options) {
let config = await loadConfig(options.config, options);
let selected;
try {
selected = await pickProject(config, options);
} catch (error) {
if (error.code !== 'LOGIN_REQUIRED') throw error;
let cli = fileURLToPath(new URL('./cli.js', import.meta.url));
await runProcess(process.execPath, [cli, 'login']);
result = await link();
await runProcess(process.execPath, [
cli,
'login',
'--api-url',
config.apiUrl,
]);
config = await loadConfig(options.config, options);
selected = await pickProject(config, options);
}
let status = result?.linked ? 'linked' : 'failed';
if (!result?.linked && exitCode === 1) status = 'auth_required';
return { status, project };
if (!selected) return { status: 'cancelled' };
let project = `${selected.organizationSlug}/${selected.projectSlug}`;
let result = await projectLinkCommand(project, {}, options, {
exit: code => {
process.exitCode = code;
},
});
return { status: result?.linked ? 'linked' : 'failed', project };
}
Loading
Loading