Skip to content
Open
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
13 changes: 11 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/src/tests/**/*.ts'],
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/src/index.ts'],
// Default: unit tests only (exclude integration tests)
testMatch: ['**/src/tests/**/*.test.ts'],
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/src/index.ts', 'integration\\.test\\.ts'],
setupFilesAfterEnv: ['<rootDir>/src/tests/setup.ts'],
// Map TypeScript path aliases to actual paths
moduleNameMapper: {
'^core/(.*)$': '<rootDir>/src/core/$1',
'^core$': '<rootDir>/src/core',
'^lib/(.*)$': '<rootDir>/src/lib/$1',
'^types/(.*)$': '<rootDir>/src/types/$1',
},
};
28 changes: 10 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agility/cli",
"version": "1.0.0-beta.12",
"version": "1.0.0-beta.13",
"description": "Agility CLI for working with your content. (Public Beta)",
"repository": {
"type": "git",
Expand All @@ -17,6 +17,8 @@
"postbuild": "chmod +x dist/index.js",
"refresh": "rm -rf ./node_modules ./package-lock.json && npm install",
"test": "jest",
"test:unit": "jest",
"test:integration": "jest --testMatch=\"**/*.integration.test.ts\" --testPathIgnorePatterns=\"/node_modules/|/dist/|/src/index.ts\"",
"debug": "node --inspect-brk -r ts-node/register src/index.ts"
},
"keywords": [
Expand Down Expand Up @@ -45,12 +47,13 @@
"dependencies": {
"@agility/content-fetch": "^2.0.10",
"@agility/content-sync": "^1.2.0",
"@agility/management-sdk": "^0.1.35",
"@agility/management-sdk": "^0.1.38",
"ansi-colors": "^4.1.3",
"blessed": "^0.1.81",
"blessed-contrib": "^4.11.0",
"cli-progress": "^3.11.2",
"date-fns": "^4.1.0",
"form-data": "^4.0.5",
"fuzzy": "^0.1.3",
"inquirer": "^8.0.0",
"inquirer-checkbox-plus-prompt": "^1.4.2",
Expand All @@ -76,4 +79,4 @@
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
}
36 changes: 23 additions & 13 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,33 @@ export class Auth {

async logout() {
const env = this.getEnv();
const key = this.getEnvKey(env);
const auth0Key = this.getEnvKey(env);
const patKey = `cli-pat-token:${env}`;

let removedAny = false;

try {
const removed = await keytar.deletePassword(SERVICE_NAME, key);
if (removed) {
console.log(`Logged out from ${env} environment.`);
// Remove Auth0 token
const removedAuth0 = await keytar.deletePassword(SERVICE_NAME, auth0Key);
if (removedAuth0) {
console.log(`✓ Removed Auth0 token for ${env} environment.`);
removedAny = true;
}

// Remove PAT token
const removedPAT = await keytar.deletePassword(SERVICE_NAME, patKey);
if (removedPAT) {
console.log(`✓ Removed Personal Access Token for ${env} environment.`);
removedAny = true;
}

if (removedAny) {
console.log(ansiColors.green(`\n🔓 Successfully logged out from ${env} environment.`));
} else {
console.log(`No token found in ${env} environment.`);
console.log(ansiColors.yellow(`No tokens found in ${env} environment.`));
}
} catch (err) {
console.error(`❌ Failed to delete token:`, err);
console.error(`❌ Failed to delete tokens:`, err);
}
exit();
}
Expand Down Expand Up @@ -871,13 +888,6 @@ export class Auth {
async validateCommand(commandType: "pull" | "sync" | "clean" | "interactive" | "push"): Promise<boolean> {
const missingFields: string[] = [];

// Validate that --publish flag is only used with sync command
if (state.publish && commandType !== "sync") {
console.log(ansiColors.red(`\n❌ The --publish flag is only available for sync commands.`));
console.log(ansiColors.yellow(`💡 Use: agility sync --sourceGuid="source" --targetGuid="target" --publish`));
return false;
}

// Check command-specific requirements
switch (commandType) {
case "pull":
Expand Down
139 changes: 139 additions & 0 deletions src/core/batch-workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* Batch Workflows Core Service
*
* Core batch workflow operations using the SDK's
* BatchWorkflowContent and BatchWorkflowPages methods.
*
* Supports: Publish, Unpublish, Approve, Decline, RequestApproval
*/

import { state, getApiClient } from './state';
import ansiColors from 'ansi-colors';
import { WorkflowOperationType, BatchWorkflowResult } from '../types';
import { getOperationName } from '../lib/workflows/workflow-helpers';

// Re-export types for convenience
export { WorkflowOperationType, BatchWorkflowResult };

// Re-export helpers from workflows folder
export { getOperationName, getOperationVerb, getOperationIcon } from '../lib/workflows/workflow-helpers';
export { parseWorkflowOptions, parseOperationType } from '../lib/workflows/workflow-options';

/**
* Batch size for processing - prevents API throttling
*/
const BATCH_SIZE = 250;

/**
* Extract detailed error message from various error formats
*/
function extractErrorDetails(error: any): string {
// Check for nested error structures (common in SDK exceptions)
if (error.innerError) {
return extractErrorDetails(error.innerError);
}

// Check for response data from API
if (error.response?.data) {
if (typeof error.response.data === 'string') {
return error.response.data;
}
if (error.response.data.message) {
return error.response.data.message;
}
if (error.response.data.error) {
return error.response.data.error;
}
return JSON.stringify(error.response.data);
}

// Check for status code
if (error.response?.status) {
return `HTTP ${error.response.status}: ${error.response.statusText || 'Unknown error'}`;
}

// Check for message property
if (error.message) {
return error.message;
}

// Fallback
return String(error) || 'Unknown workflow error';
}

/**
* Item type for batch workflow operations
*/
export type BatchItemType = 'content' | 'pages';

/**
* Unified batch workflow operation for content items or pages
*
* @param ids - Array of IDs to process
* @param locale - Target locale
* @param operation - Workflow operation type
* @param type - Item type: 'content' or 'pages'
* @returns Promise with batch result
*/
export async function batchWorkflow(
ids: number[],
locale: string,
operation: WorkflowOperationType,
type: BatchItemType
): Promise<BatchWorkflowResult> {
const label = type === 'content' ? 'content items' : 'pages';

try {
const apiClient = getApiClient();
const targetGuid = state.targetGuid;

if (!apiClient) {
throw new Error('API client not available in state');
}
if (!targetGuid || targetGuid.length === 0) {
throw new Error('Target GUID not available in state');
}
if (!locale) {
throw new Error('Locale not available in state');
}
if (!ids || ids.length === 0) {
throw new Error(`${label} IDs array is empty`);
}

// const operationName = getOperationName(operation);

// Log the attempt for debugging
// if (state.verbose) {
// console.log(ansiColors.gray(`${operationName}ing ${ids.length} ${label} to ${targetGuid[0]} (${locale})...`));
// }

// Call appropriate SDK method based on type
const processedIds = type === 'content'
? await apiClient.contentMethods.batchWorkflowContent(ids, targetGuid[0], locale, operation, false)
: await apiClient.pageMethods.batchWorkflowPages(ids, targetGuid[0], locale, operation, false);

return {
success: true,
processedIds,
failedCount: 0
};
} catch (error: any) {
return {
success: false,
processedIds: [],
failedCount: ids.length,
error: extractErrorDetails(error)
};
}
}

/**
* Create batches of items for processing
*/
export function createBatches<T>(items: T[], batchSize: number = BATCH_SIZE): T[][] {
const batches: T[][] = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
return batches;
}
Loading