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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ npm run clean # Remove dist directory
npm run compile # Babel compile src to dist
npm run types # Generate TypeScript declarations

npm test # Run all tests once (vitest)
npm test # Run all tests once (node --test)
npm test:watch # Run tests in watch mode
npm run test:reporter # Run Playwright visual regression tests for reporter UI
npm run test:reporter:visual # Self-test: run reporter tests with Vizzly
Expand All @@ -64,8 +64,8 @@ npm run format:check # Prettier check

### Testing Specific Files/Patterns
```bash
npx vitest run tests/unit/config-loader.spec.js # Single test file
npx vitest run tests/services/ # Directory
node --test tests/commands/builds.test.js # Single test file
node --test $(find tests/services -name '*.test.js') # Directory
```

### Reporter Development
Expand Down Expand Up @@ -198,7 +198,7 @@ The package provides multiple entry points for different use cases:

## Testing

- **Vitest** for unit/integration tests (`npm test`)
- **Node test runner** for unit/integration tests (`npm test`)
- **Playwright** for reporter UI visual tests (`npm run test:reporter`)
- Coverage thresholds: 75% lines/functions, 70% branches
- Tests mirror `src/` structure in `tests/`
Expand Down
38 changes: 29 additions & 9 deletions src/commands/orgs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@
* Organizations command - List organizations the user has access to
*/

import { createApiClient } from '../api/client.js';
import { loadConfig } from '../utils/config-loader.js';
import { getApiUrl } from '../utils/environment-config.js';
import * as output from '../utils/output.js';
import { createApiClient as defaultCreateApiClient } from '../api/client.js';
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
import { getAccessToken as defaultGetAccessToken } from '../utils/global-config.js';
import * as defaultOutput from '../utils/output.js';

/**
* Organizations command implementation
* @param {Object} options - Command options
* @param {Object} globalOptions - Global CLI options
* @param {Object} deps - Dependencies for testing
*/
export async function orgsCommand(_options = {}, globalOptions = {}) {
export async function orgsCommand(
_options = {},
globalOptions = {},
deps = {}
) {
let {
loadConfig = defaultLoadConfig,
createApiClient = defaultCreateApiClient,
getApiUrl = defaultGetApiUrl,
getAccessToken = defaultGetAccessToken,
output = defaultOutput,
exit = code => process.exit(code),
} = deps;

output.configure({
json: globalOptions.json,
verbose: globalOptions.verbose,
Expand All @@ -22,17 +37,22 @@ export async function orgsCommand(_options = {}, globalOptions = {}) {
try {
let config = await loadConfig(globalOptions.config, globalOptions);

if (!config.apiKey) {
// Prefer user auth token for listing orgs (project tokens are org-scoped).
// Falls back to config.apiKey which may be: VIZZLY_TOKEN, --token flag, or project token.
let token = (await getAccessToken()) || config.apiKey;

if (!token) {
output.error(
'API token required. Use --token, set VIZZLY_TOKEN, or run "vizzly login"'
);
output.cleanup();
process.exit(1);
exit(1);
return;
}

let client = createApiClient({
baseUrl: config.apiUrl || getApiUrl(),
token: config.apiKey,
token,
});

output.startSpinner('Fetching organizations...');
Expand Down Expand Up @@ -83,7 +103,7 @@ export async function orgsCommand(_options = {}, globalOptions = {}) {
output.stopSpinner();
output.error('Failed to fetch organizations', error);
output.cleanup();
process.exit(1);
exit(1);
}
}

Expand Down
38 changes: 29 additions & 9 deletions src/commands/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@
* Projects command - List projects the user has access to
*/

import { createApiClient } from '../api/client.js';
import { loadConfig } from '../utils/config-loader.js';
import { getApiUrl } from '../utils/environment-config.js';
import * as output from '../utils/output.js';
import { createApiClient as defaultCreateApiClient } from '../api/client.js';
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
import { getAccessToken as defaultGetAccessToken } from '../utils/global-config.js';
import * as defaultOutput from '../utils/output.js';

/**
* Projects command implementation
* @param {Object} options - Command options
* @param {Object} globalOptions - Global CLI options
* @param {Object} deps - Dependencies for testing
*/
export async function projectsCommand(options = {}, globalOptions = {}) {
export async function projectsCommand(
options = {},
globalOptions = {},
deps = {}
) {
let {
loadConfig = defaultLoadConfig,
createApiClient = defaultCreateApiClient,
getApiUrl = defaultGetApiUrl,
getAccessToken = defaultGetAccessToken,
output = defaultOutput,
exit = code => process.exit(code),
} = deps;

output.configure({
json: globalOptions.json,
verbose: globalOptions.verbose,
Expand All @@ -22,17 +37,22 @@ export async function projectsCommand(options = {}, globalOptions = {}) {
try {
let config = await loadConfig(globalOptions.config, globalOptions);

if (!config.apiKey) {
// Prefer user auth token for listing projects (project tokens are org-scoped).
// Falls back to config.apiKey which may be: VIZZLY_TOKEN, --token flag, or project token.
let token = (await getAccessToken()) || config.apiKey;

if (!token) {
output.error(
'API token required. Use --token, set VIZZLY_TOKEN, or run "vizzly login"'
);
output.cleanup();
process.exit(1);
exit(1);
return;
}

let client = createApiClient({
baseUrl: config.apiUrl || getApiUrl(),
token: config.apiKey,
token,
});

// Build query params
Expand Down Expand Up @@ -105,7 +125,7 @@ export async function projectsCommand(options = {}, globalOptions = {}) {
output.stopSpinner();
output.error('Failed to fetch projects', error);
output.cleanup();
process.exit(1);
exit(1);
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/utils/global-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,14 @@ export async function hasValidTokens() {
}

/**
* Get the access token from global config if available
* Get the access token from global config if valid and not expired
* @returns {Promise<string|null>} Access token or null
*/
export async function getAccessToken() {
const auth = await getAuthTokens();
let valid = await hasValidTokens();
if (!valid) return null;

let auth = await getAuthTokens();
return auth?.accessToken || null;
}

Expand Down
Loading