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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [0.8.1] - 2026-05-15

### Added

- `--debug` / `-d` flag for `tail-logs` and `purge-cache` commands to show API endpoint

### Changed

- API endpoint is now only shown when `--debug` flag is explicitly used

### Fixed

- Correct API endpoint resolution for non-production Cloud Manager environments

## [0.8.0] - 2026-05-15

### Added
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ The following command will tail your edge function logs to help you debug your a
aio aem edge-functions tail-logs first-function
```

### Debug Mode

To see the API endpoint used for log tailing (useful for troubleshooting connectivity issues), use the `--debug` / `-d` flag:

```
aio aem edge-functions tail-logs first-function --debug
```

## Purge cache

Edge Function responses can be cached at the CDN layer. When cached content becomes stale due to inter-resource dependencies, you can explicitly purge it:
Expand All @@ -228,6 +236,7 @@ aio aem edge-functions purge-cache first-function -k my-key --soft
| `--surrogateKey` / `-k` | Surrogate key to purge (can be specified multiple times) |
| `--all` / `-a` | Purge all cached content for the edge function |
| `--soft` / `-s` | Perform a soft purge (retain stale entries, reduce origin load) |
| `--debug` / `-d` | Show debug information including API endpoint |

Surrogate keys are set on Edge Function responses via the `Surrogate-Key` header. For more information on caching and purging, see the [AEM Edge Functions documentation](https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/developing/edge-functions).

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@adobe/aio-cli-plugin-aem-edge-functions",
"description": "Adobe I/O CLI plugin for interacting with AEM Edge Functions",
"version": "0.8.0",
"version": "0.8.1",
"author": "Adobe Inc.",
"engines": {
"npm": ">= 8.0.0",
Expand Down
12 changes: 8 additions & 4 deletions src/commands/aem/edge-functions/info.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ class InfoCommand extends BaseCommand {
let adcFetchFailed = false;
let adcError = null;

let isStage = this.isStageEnv();

// Fetch Cloud Manager data if we have orgId and programId
if (orgId && programId && !this.flags.batch) {
this.spinnerStart('Loading Cloud Manager program and environment names...');
try {
const { accessToken, apiKey, data } = await this.getTokenAndKey();
const cloudManagerUrl = this.getBaseUrl(data?.env === 'stage');
isStage = data?.env === 'stage';
const cloudManagerUrl = this.getBaseUrl(isStage);
const cloudmanager = new Cloudmanager(
`${cloudManagerUrl}/api`,
apiKey,
Expand Down Expand Up @@ -191,11 +194,12 @@ class InfoCommand extends BaseCommand {

// Display Cloud Manager URL
if (orgId && programId) {
const experienceHost = isStage ? 'experience-stage.adobe.com' : 'experience.adobe.com';
let cloudManagerUrl;
if (edgeDelivery) {
cloudManagerUrl = `https://experience.adobe.com/#/@${orgId}/cloud-manager/edge-delivery.html/program/${programId}`;
cloudManagerUrl = `https://${experienceHost}/#/@${orgId}/cloud-manager/edge-delivery.html/program/${programId}`;
} else if (environmentId) {
cloudManagerUrl = `https://experience.adobe.com/#/@${orgId}/cloud-manager/environments.html/program/${programId}/environment/${environmentId}`;
cloudManagerUrl = `https://${experienceHost}/#/@${orgId}/cloud-manager/environments.html/program/${programId}/environment/${environmentId}`;
}

if (cloudManagerUrl) {
Expand Down Expand Up @@ -223,7 +227,7 @@ class InfoCommand extends BaseCommand {
if (this.flags.debug) {
console.log(`\nTimestamp (UTC): ${chalk.cyan(new Date().toISOString())}`);

const apiEndpoint = this.getApiBasePath() ? this.getApiBasePath() : null;
const apiEndpoint = this.getApiBasePath(isStage) ? this.getApiBasePath(isStage) : null;
const adcClientId = this.getConfig(this.CONFIG_ADC_CLIENT_ID);
const adcClientSecret = this.getConfig(this.CONFIG_ADC_CLIENT_SECRET);
const adcScopes = this.getConfig(this.CONFIG_ADC_SCOPES);
Expand Down
33 changes: 15 additions & 18 deletions src/commands/aem/edge-functions/purge-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ By default performs a hard purge (immediate removal). Use --soft for soft purge
char: 's',
description: 'Perform a soft purge (retain stale entries for revalidation)',
default: false
}),
debug: Flags.boolean({
char: 'd',
description: 'Show debug information including API endpoint',
default: false
})
};

Expand Down Expand Up @@ -83,28 +88,20 @@ By default performs a hard purge (immediate removal). Use --soft for soft purge
body.surrogateKeys = surrogateKey;
}

// Get access token
const basePath = this.getApiBasePath();
if (!basePath) {
this.error('API endpoint not configured. Run "aio aem edge-functions setup" first.');
}
// Get access token and detect stage
const { accessToken, isStage } = await this.getAccessTokenAndStage();

let accessToken = process.env.AEM_EDGE_FUNCTIONS_TOKEN;
if (!accessToken) {
const adcConfigured = this.getConfig(this.CONFIG_ADC_CONFIGURED);
if (adcConfigured) {
const adcToken = await this.getAdcToken();
if (adcToken) {
accessToken = adcToken.accessToken;
}
}
if (!accessToken) {
accessToken = (await this.getTokenAndKey())?.accessToken;
}
this.error('No access token available. Please authenticate first.');
}

if (!accessToken) {
this.error('No access token available. Please authenticate first.');
const basePath = this.getApiBasePath(isStage);
if (!basePath) {
this.error('API endpoint not configured. Run "aio aem edge-functions setup" first.');
}

if (this.flags.debug) {
console.log(`Using API endpoint: ${basePath}`);
}

const request = new Request(basePath, {
Expand Down
11 changes: 9 additions & 2 deletions src/commands/aem/edge-functions/tail-logs.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
'use strict';

const BaseCommand = require('../../../libs/base-command');
const { Args } = require('@oclif/core');
const { Args, Flags } = require('@oclif/core');

class TailLogsCommand extends BaseCommand {
static description = 'Tail logs from your AEM edge function.';
Expand All @@ -23,10 +23,17 @@ class TailLogsCommand extends BaseCommand {
required: true
})
};
static flags = {
debug: Flags.boolean({
char: 'd',
description: 'Show debug information including API endpoint',
default: false
})
};

async run() {
const fastly = await this.getFastlyCli();
await fastly.logTail(this.args.serviceId);
await fastly.logTail(this.args.serviceId, { debug: this.flags.debug });
}
}

Expand Down
55 changes: 44 additions & 11 deletions src/libs/base-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,11 +318,26 @@ class BaseCommand extends Command {
return !stage ? 'https://cloudmanager.adobe.io' : 'https://cloudmanager-stage.adobe.io';
}

/**
* Detect whether the CLI is configured for the Cloud Manager stage environment.
* Checks AIO_CLI_ENV env var and aio config 'cli.env' / 'ims.contexts.cli.env'.
* This does not require a token and works in batch mode.
*/
isStageEnv() {
if (process.env.AIO_CLI_ENV === 'stage') return true;
const cliEnv = Config.get('cli.env');
if (cliEnv === 'stage') return true;
const imsEnv = Config.get('ims.contexts.cli.env');
if (imsEnv === 'stage') return true;
return false;
}

/**
* Get the API base path for AEM Edge Functions (without the edge functions path suffix)
* @param {boolean} [stage=false] Whether to use the Cloud Manager stage domain suffix (-cmstg)
* @returns {string|null} The computed base URL or null if configuration is incomplete
*/
getApiBasePath() {
getApiBasePath(stage = false) {
if (process.env.AEM_EDGE_FUNCTIONS_API_ENDPOINT) {
return process.env.AEM_EDGE_FUNCTIONS_API_ENDPOINT;
}
Expand All @@ -338,27 +353,33 @@ class BaseCommand extends Command {
return null;
}

const stageSuffix = stage ? '-cmstg' : '';
return isEdgeDelivery
? `https://${siteDomain}/adobe/experimental/compute-expires-20251231/cdn`
: `https://author-p${programId}-e${environmentId}.adobeaemcloud.com/adobe/experimental/compute-expires-20251231/cdn`;
: `https://author-p${programId}-e${environmentId}${stageSuffix}.adobeaemcloud.com/adobe/experimental/compute-expires-20251231/cdn`;
}

/**
* Get the API endpoint for AEM Edge Functions
* @param {boolean} [stage=false] Whether to use the Cloud Manager stage domain suffix (-cmstg)
* @returns {string|null} The computed API endpoint or null if configuration is incomplete
*/
getApiEndpoint() {
const basePath = this.getApiBasePath();
getApiEndpoint(stage = false) {
const basePath = this.getApiBasePath(stage);
if (!basePath) return null;

return basePath + (process.env.AEM_EDGE_FUNCTIONS_API_ENDPOINT_URL ?? '/edgeFunctions/fastly');
}

async getFastlyCli() {
const apiEndpoint = this.getApiEndpoint();

// For edge function API requests, try to use ADC token if configured
/**
* Get an access token and detect whether the environment is stage.
* Tries (in order): AEM_EDGE_FUNCTIONS_TOKEN env var, ADC OAuth, IMS token.
* Stage is detected from IMS context data when available, falling back to isStageEnv().
* @returns {Promise<{accessToken: string|null, isStage: boolean}>}
*/
async getAccessTokenAndStage() {
let accessToken = process.env.AEM_EDGE_FUNCTIONS_TOKEN;
let isStage = this.isStageEnv();

if (!accessToken) {
const adcConfigured = this.getConfig(this.CONFIG_ADC_CONFIGURED);
Expand All @@ -370,18 +391,30 @@ class BaseCommand extends Command {
accessToken = adcToken.accessToken;
} else {
ux.warn('Failed to get ADC token, falling back to IMS token');
accessToken = (await this.getTokenAndKey())?.accessToken;
const tokenResult = await this.getTokenAndKey();
accessToken = tokenResult?.accessToken;
isStage = tokenResult?.data?.env === 'stage' || isStage;
}
} catch (error) {
ux.warn(`Failed to get ADC token: ${error.message}, falling back to IMS token`);
accessToken = (await this.getTokenAndKey())?.accessToken;
const tokenResult = await this.getTokenAndKey();
accessToken = tokenResult?.accessToken;
isStage = tokenResult?.data?.env === 'stage' || isStage;
}
} else {
// No ADC configured, use IMS token
accessToken = (await this.getTokenAndKey())?.accessToken;
const tokenResult = await this.getTokenAndKey();
accessToken = tokenResult?.accessToken;
isStage = tokenResult?.data?.env === 'stage' || isStage;
}
}

return { accessToken, isStage };
}

async getFastlyCli() {
const { accessToken, isStage } = await this.getAccessTokenAndStage();
const apiEndpoint = this.getApiEndpoint(isStage);
return new FastlyCli(accessToken, apiEndpoint);
}
}
Expand Down
17 changes: 13 additions & 4 deletions src/libs/fastly-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,16 @@ class FastlyCli {
}
}

async run(args, { filterOutput: shouldFilter = false } = {}) {
async run(args, { filterOutput: shouldFilter = false, debug = false } = {}) {
if (!this.fastlyCliPath) {
await this.init();
}

// print API endpoint only in explicit debug mode
if (debug) {
console.log(`Using API endpoint: ${this.apiEndpoint}`);
}

const env = {
...process.env,
FASTLY_API_TOKEN: this.apiToken,
Expand Down Expand Up @@ -197,7 +203,10 @@ class FastlyCli {
async deploy(serviceId, { debug = false } = {}) {
this.ensureTokenIsSet();
this.ensureServiceIdIsSafe(serviceId);
await this.run(['compute', 'deploy', '--service-id', serviceId], { filterOutput: !debug });
await this.run(['compute', 'deploy', '--service-id', serviceId], {
filterOutput: !debug,
debug
});
}

async serve({ watch = false } = {}) {
Expand All @@ -208,10 +217,10 @@ class FastlyCli {
await this.run(args);
}

async logTail(serviceId) {
async logTail(serviceId, { debug = false } = {}) {
this.ensureTokenIsSet();
this.ensureServiceIdIsSafe(serviceId);
await this.run(['log-tail', '--service-id', serviceId]);
await this.run(['log-tail', '--service-id', serviceId], { debug });
}
}

Expand Down
3 changes: 3 additions & 0 deletions test/commands/aem/edge-functions/purge-cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ describe('PurgeCacheCommand', () => {
it('should error when API endpoint is not configured', async () => {
command.args = { serviceId: 'my-func' };
command.flags = { all: true, soft: false };
command.getAccessTokenAndStage = sandbox
.stub()
.resolves({ accessToken: 'token', isStage: false });
command.getApiBasePath = sandbox.stub().returns(null);

await assert.rejects(() => command.run(), /command error/);
Expand Down
Loading