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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ agility-files/
> **Recommended Practices:**
>
> - **Persist your mappings** through shared file storage or a repository (e.g., Git) when working on a team
> - **Do not have multiple instances of the CLI syncing the same source→target instance pairs simultaneously** - this can cause mapping conflicts and duplicate content
> - **Do not run multiple CLI processes against the same source→target pair at the same time** - each invocation handles exactly one source/target pair; running two overlapping processes against that same pair can still cause mapping conflicts and duplicate content
> - **Back up your `agility-files/mappings/` directory** before performing destructive operations

## File Structure
Expand Down
98 changes: 51 additions & 47 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { serverUser } from "../types/serverUser";
import { state } from "./state";
import * as mgmtApi from "@agility/management-sdk";
const open = require("open");

Check warning on line 4 in src/core/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint Check

A `require()` style import is forbidden
const FormData = require("form-data");

Check warning on line 5 in src/core/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint Check

A `require()` style import is forbidden
import fs from "fs";
import path from "path";

Expand All @@ -10,7 +10,7 @@
function getKeytar() {
if (_keytar === undefined) {
try {
_keytar = require("keytar");

Check warning on line 13 in src/core/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint Check

A `require()` style import is forbidden
} catch {
_keytar = null;
}
Expand Down Expand Up @@ -131,7 +131,7 @@
determineBaseUrl(guid?: string): string {
let baseGUID = guid;
if (!baseGUID) {
baseGUID = state.sourceGuid[0];
baseGUID = state.sourceGuid;
}

if (state.dev) {
Expand Down Expand Up @@ -167,7 +167,7 @@
determineFetchUrl(guid?: string): string {
let baseGUID = guid;
if (!baseGUID) {
baseGUID = state.sourceGuid[0];
baseGUID = state.sourceGuid;
}

// Content Fetch API URLs are determined by GUID suffix only
Expand Down Expand Up @@ -200,7 +200,7 @@
determineCloudMgmtUrl(guid?: string): string {
let baseGUID = guid;
if (!baseGUID) {
baseGUID = state.sourceGuid[0];
baseGUID = state.sourceGuid;
}

// Cloud Management API URLs are determined by GUID suffix only
Expand All @@ -225,7 +225,7 @@
return "https://mgmt.aglty.io";
}

getBaseUrl(guid: string, userBaseUrl: string = null): string {

Check warning on line 228 in src/core/auth.ts

View workflow job for this annotation

GitHub Actions / ESLint Check

'userBaseUrl' is assigned a value but never used
let baseUrl = this.determineBaseUrl(guid);
return `${baseUrl}/oauth`;
}
Expand Down Expand Up @@ -351,18 +351,25 @@
}
}

// Step 3: Get API keys for all GUIDs
const allGuids = [...state.sourceGuid, ...state.targetGuid];
state.apiKeys = [];
// Step 3: Get API keys for the source and target GUIDs
state.sourceApiKeys = null;
state.targetApiKeys = null;
const failedGuids: string[] = [];

for (const guid of allGuids) {
for (const [role, guid] of [
["source", state.sourceGuid],
["target", state.targetGuid],
] as const) {
if (guid) {
try {
const previewKey = await this.getPreviewKey(guid);
const fetchKey = await this.getFetchKey(guid);

state.apiKeys.push({ guid, previewKey, fetchKey });
if (role === "source") {
state.sourceApiKeys = { previewKey, fetchKey };
} else {
state.targetApiKeys = { previewKey, fetchKey };
}
} catch (error) {
failedGuids.push(guid);
}
Expand Down Expand Up @@ -401,9 +408,9 @@
state.cachedApiClient = new mgmtApi.ApiClient(state.mgmtApiOptions);

// Load user data for interactive prompts and general use
if (state.sourceGuid.length > 0) {
if (state.sourceGuid) {
try {
const primaryGuid = state.sourceGuid[0];
const primaryGuid = state.sourceGuid;
const user = await this.getUser(primaryGuid);
if (user) {
state.user = user;
Expand All @@ -415,22 +422,22 @@
}
}

// Step 6: Auto-detect available locales for ALL GUIDs in the matrix
if (allGuids.length > 0) {
// Step 6: Auto-detect available locales for the source and target GUIDs
if (state.sourceGuid || state.targetGuid) {
try {
//Get the locales for the SOURCE GUID
let sourceLocales: string[] = [];
if (state.sourceGuid.length > 0) {
sourceLocales = (await state.cachedApiClient.instanceMethods.getLocales(state.sourceGuid[0])).map(
if (state.sourceGuid) {
sourceLocales = (await state.cachedApiClient.instanceMethods.getLocales(state.sourceGuid)).map(
(locale: any) => locale.localeCode
);
state.availableLocales = sourceLocales;
}

//Get the locales for the TARGET GUID
let targetLocales: string[] = [];
if (state.targetGuid.length > 0) {
targetLocales = (await state.cachedApiClient.instanceMethods.getLocales(state.targetGuid[0])).map(
if (state.targetGuid) {
targetLocales = (await state.cachedApiClient.instanceMethods.getLocales(state.targetGuid)).map(
(locale: any) => locale.localeCode
);

Expand All @@ -451,7 +458,7 @@
const validationScope = state.locale.length > 0 ? "specified" : "source";
console.log(
ansiColors.yellow(
`⚠️ Target instance ${state.targetGuid[0]}: Missing ${validationScope} locales ${missingLocales.join(", ")} (available: ${targetLocales.join(", ")})`
`⚠️ Target instance ${state.targetGuid}: Missing ${validationScope} locales ${missingLocales.join(", ")} (available: ${targetLocales.join(", ")})`
)
);
return false; // Cannot proceed with missing locales
Expand All @@ -465,7 +472,7 @@
if (validLocales.length === 0) {
console.log(
ansiColors.yellow(
`⚠️ None of the specified locales exist in the source instance ${state.sourceGuid[0]}. Using all available locales.`
`⚠️ None of the specified locales exist in the source instance ${state.sourceGuid}. Using all available locales.`
)
);
} else {
Expand All @@ -474,11 +481,11 @@
}

const guidLocaleMap = new Map<string, string[]>();
guidLocaleMap.set(state.sourceGuid[0], localesToUse);
guidLocaleMap.set(state.sourceGuid, localesToUse);

if (state.targetGuid.length > 0) {
if (state.targetGuid) {
//if we have a target...
guidLocaleMap.set(state.targetGuid[0], localesToUse);
guidLocaleMap.set(state.targetGuid, localesToUse);
}

state.locale = localesToUse; // Set the state locale list to the determined locales
Expand All @@ -499,11 +506,8 @@
if (state.locale.length > 0) {
// User specified locales explicitly, use those
const guidLocaleMap = new Map<string, string[]>();
for (const guid of allGuids) {
if (guid) {
guidLocaleMap.set(guid, state.locale);
}
}
if (state.sourceGuid) guidLocaleMap.set(state.sourceGuid, state.locale);
if (state.targetGuid) guidLocaleMap.set(state.targetGuid, state.locale);
state.guidLocaleMap = guidLocaleMap;
state.availableLocales = state.locale;
console.log(`📝 Using user-specified locales: ${state.locale.join(", ")}`);
Expand Down Expand Up @@ -995,7 +999,7 @@
// Check command-specific requirements
switch (commandType) {
case "pull":
if (!state.sourceGuid || state.sourceGuid.length === 0)
if (!state.sourceGuid)
missingFields.push("sourceGuid (use --sourceGuid or AGILITY_GUID in .env)");

// Check for locales: either user-specified OR auto-detected per-GUID mappings
Expand All @@ -1011,9 +1015,9 @@
case "push":
case "sync":
// Both push and sync require source and target GUIDs
if (!state.sourceGuid || state.sourceGuid.length === 0)
if (!state.sourceGuid)
missingFields.push("sourceGuid (use --sourceGuid or AGILITY_GUID in .env)");
if (!state.targetGuid || state.targetGuid.length === 0)
if (!state.targetGuid)
missingFields.push("targetGuid (use --targetGuid or AGILITY_TARGET_GUID in .env)");

// Check for locales: either user-specified OR auto-detected per-GUID mappings
Expand Down Expand Up @@ -1046,21 +1050,21 @@

// Validate instance access and set up API configuration
try {
if (commandType === "sync" && state.targetGuid && state.targetGuid.length > 0) {
// Sync operation - validate access to both source and target (use first GUID for validation)
if (commandType === "sync" && state.targetGuid) {
// Sync operation - validate access to both source and target
if (!state.isAgilityDev && !state.dev) {
await this.validateInstanceAccess(state.sourceGuid[0], "source");
await this.validateInstanceAccess(state.sourceGuid, "source");
}
await this.validateInstanceAccess(state.targetGuid[0], "target");
await this.validateInstanceAccess(state.targetGuid, "target");

// Configure for target instance (sync writes to target - use first target GUID)
const targetBaseUrl = state.baseUrl || this.determineBaseUrl(state.targetGuid[0]);
// Configure for target instance (sync writes to target)
const targetBaseUrl = state.baseUrl || this.determineBaseUrl(state.targetGuid);
state.mgmtApiOptions!.baseUrl = targetBaseUrl;
state.baseUrl = targetBaseUrl;

// Get API keys for source instance (needed for pull phase of sync - use first source GUID)
const previewKey = await this.getPreviewKey(state.sourceGuid[0]);
const fetchKey = await this.getFetchKey(state.sourceGuid[0]);
// Get API keys for source instance (needed for pull phase of sync)
const previewKey = await this.getPreviewKey(state.sourceGuid);
const fetchKey = await this.getFetchKey(state.sourceGuid);

state.previewKey = previewKey;
state.fetchKey = fetchKey;
Expand All @@ -1069,22 +1073,22 @@
if (!state.apiKeyForPull) {
console.log(
ansiColors.red(
`Could not retrieve the required API key (preview: ${state.preview}) for source instance ${state.sourceGuid[0]}. Check API key configuration in Agility.`
`Could not retrieve the required API key (preview: ${state.preview}) for source instance ${state.sourceGuid}. Check API key configuration in Agility.`
)
);
return false;
}
} else if (commandType === "pull" && state.sourceGuid && state.sourceGuid.length > 0) {
// Pull operation - validate source access and get API keys (use first source GUID for validation)
await this.validateInstanceAccess(state.sourceGuid[0], "instance");
} else if (commandType === "pull" && state.sourceGuid) {
// Pull operation - validate source access and get API keys
await this.validateInstanceAccess(state.sourceGuid, "instance");

const baseUrl = state.baseUrl || this.determineBaseUrl(state.sourceGuid[0]);
const baseUrl = state.baseUrl || this.determineBaseUrl(state.sourceGuid);
state.mgmtApiOptions!.baseUrl = baseUrl;
state.baseUrl = baseUrl;

// Get API keys for pull operations (use first source GUID)
const previewKey = await this.getPreviewKey(state.sourceGuid[0]);
const fetchKey = await this.getFetchKey(state.sourceGuid[0]);
// Get API keys for pull operations
const previewKey = await this.getPreviewKey(state.sourceGuid);
const fetchKey = await this.getFetchKey(state.sourceGuid);

state.previewKey = previewKey;
state.fetchKey = fetchKey;
Expand All @@ -1093,7 +1097,7 @@
if (!state.apiKeyForPull) {
console.log(
ansiColors.red(
`Could not retrieve the required API key (preview: ${state.preview}) for instance ${state.sourceGuid[0]}. Check API key configuration in Agility.`
`Could not retrieve the required API key (preview: ${state.preview}) for instance ${state.sourceGuid}. Check API key configuration in Agility.`
)
);
return false;
Expand Down
8 changes: 4 additions & 4 deletions src/core/batch-workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export async function batchWorkflow(
if (!apiClient) {
throw new Error("API client not available in state");
}
if (!targetGuid || targetGuid.length === 0) {
if (!targetGuid) {
throw new Error("Target GUID not available in state");
}
if (!locale) {
Expand All @@ -313,8 +313,8 @@ export async function batchWorkflow(
// Get batch ID immediately using returnBatchId=true
const batchIdResult =
type === "content"
? await apiClient.contentMethods.batchWorkflowContent(ids, targetGuid[0], locale, operation, true)
: await apiClient.pageMethods.batchWorkflowPages(ids, targetGuid[0], locale, operation, true);
? await apiClient.contentMethods.batchWorkflowContent(ids, targetGuid, locale, operation, true)
: await apiClient.pageMethods.batchWorkflowPages(ids, targetGuid, locale, operation, true);

const batchID = Array.isArray(batchIdResult) ? batchIdResult[0] : batchIdResult;

Expand All @@ -323,7 +323,7 @@ export async function batchWorkflow(
}

// Custom polling with batch ID tracking and progress display
const pollResult = await pollBatchWorkflow(batchID, targetGuid[0], type, ids.length);
const pollResult = await pollBatchWorkflow(batchID, targetGuid, type, ids.length);

if (pollResult.success) {
// Handle partial success
Expand Down
12 changes: 6 additions & 6 deletions src/core/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ export class Logs {
if (this.logs.length > 0) {
// Count GUID occurrences in log messages to identify which GUID this logger belongs to
const guidCounts = new Map<string, number>();
const allGuids = [...(state.sourceGuid || []), ...(state.targetGuid || [])];
const allGuids = [state.sourceGuid, state.targetGuid].filter(Boolean);

this.logs.forEach((log) => {
allGuids.forEach((guid) => {
Expand All @@ -479,8 +479,8 @@ export class Logs {

// Build filename with GUID
if (this.operationType === "push" || this.operationType === "sync") {
const sourceGuid = state.sourceGuid?.[0] || "unknown";
const targetGuid = state.targetGuid?.[0] || "unknown";
const sourceGuid = state.sourceGuid || "unknown";
const targetGuid = state.targetGuid || "unknown";
filename = `${sourceGuid}-${targetGuid}-${this.operationType}-${timestamp}.txt`;
} else {
// For pull operations, use the specific GUID this logger is for
Expand Down Expand Up @@ -591,7 +591,7 @@ export class Logs {
*/
private initializeGuidColors(): void {
const state = getState();
const allGuids = [...(state.sourceGuid || []), ...(state.targetGuid || [])];
const allGuids = [state.sourceGuid, state.targetGuid].filter(Boolean);

// Assign unique colors to each GUID
allGuids.forEach((guid, index) => {
Expand Down Expand Up @@ -1045,8 +1045,8 @@ export class Logs {
GUID: this.guid || "Not specified",
"Operation Type": this.operationType,
"Entity Type": this.entityType || "All entities",
"Source GUIDs": state.sourceGuid?.join(", ") || "None",
"Target GUIDs": state.targetGuid?.join(", ") || "None",
"Source GUID": state.sourceGuid || "None",
"Target GUID": state.targetGuid || "None",
Locales: this.guid ? state.guidLocaleMap?.get(this.guid)?.join(", ") || "Not specified" : "Multiple",
Channel: state.channel || "Not specified",
Elements: state.elements || "All",
Expand Down
4 changes: 2 additions & 2 deletions src/core/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ export class PublishService {
constructor(options: PublishOptions = {}) {
const state = getState();

if (!state.targetGuid?.length) {
if (!state.targetGuid) {
throw new Error("PublishService requires targetGuid to be set in state");
}

this.apiClient = getApiClient();
this.targetGuid = state.targetGuid[0];
this.targetGuid = state.targetGuid;
this.options = { verbose: false, ...options };
}

Expand Down
19 changes: 7 additions & 12 deletions src/core/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,20 @@ export class Pull {
initializeLogger("pull");
}

// TODO: Add support for multiple GUIDs, multiple locales, multiple chanels
// Currently only supports one GUID, one locale, one channel
// Get all GUIDs to process (both source and target)
let allGuids = [];
if (fromPush === true) {
allGuids = [...state.sourceGuid, ...state.targetGuid];
} else {
allGuids = [...state.sourceGuid];
}
// Get the GUIDs to process: both source and target when called from push, source only for a standalone pull
const guidsToProcess: string[] = fromPush
? [state.sourceGuid, state.targetGuid].filter(Boolean)
: [state.sourceGuid].filter(Boolean);

if (allGuids.length === 0) {
if (guidsToProcess.length === 0) {
throw new Error("No GUIDs specified for pull operation");
}

// Calculate total operations using per-GUID locale mapping
let totalOperations = 0;
const operationDetails: string[] = [];

for (const guid of allGuids) {
for (const guid of guidsToProcess) {
const guidLocales = state.guidLocaleMap.get(guid) || ["en-us"];
totalOperations += guidLocales.length;
operationDetails.push(`${guid}: ${guidLocales.join(", ")}`);
Expand All @@ -57,7 +52,7 @@ export class Pull {
// This ensures we're pulling the latest data from the CDN
// Skip when called from push - the refresh-mappings workflow handles this separately
if (!fromPush) {
for (const guid of allGuids) {
for (const guid of guidsToProcess) {
try {
await waitForFetchApiSync(guid, "fetch", false);
} catch (error: any) {
Expand Down
Loading
Loading