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: 2 additions & 0 deletions packages/cli/src/commands/applications/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ export default class ApplicationsAdd extends Command {
clientSecret: flags.clientSecret,
slug: flags.organization,
domain: flags.domain,
apiVersion: flags['api-version'],
scope,
email: flags.email,
password: flags.password
}

if (config.domain === configParam(ConfigParams.defaultDomain)) config.domain = undefined
if (!config.apiVersion || (config.apiVersion === configParam(ConfigParams.defaultApiVersion))) config.apiVersion = undefined


try {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/applications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const extraColumns = (flags: any): any => {
extra.id = { header: 'ID', get: (row: { id: any }) => clColor.dim(row.id || '') }
extra.appkey = { header: 'APPKEY', get: (row: { key: any }) => clColor.dim(row.key || '') }
extra.domain = { header: 'DOMAIN', get: (row: { domain: any }) => clColor.dim(row.domain || '') }
extra.apiVersion = { header: 'API VERSION', get: (row: { apiVersion: any }) => clColor.dim(row.apiVersion || '') }
}
return extra
}
Expand Down
13 changes: 11 additions & 2 deletions packages/cli/src/commands/applications/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { inspect } from 'node:util'
import type { ApiMode, AppAuth, AppInfo, AuthScope } from '@commercelayer/cli-core'
import { clApi, clApplication, clColor, clCommand, clConfig, clToken } from '@commercelayer/cli-core'
import clprovisioning from '@commercelayer/provisioning-sdk'
import type { ApiVersion } from '@commercelayer/sdk'
import commercelayer, { type Application, CommerceLayerStatic, type Organization } from '@commercelayer/sdk'
import type { Interfaces } from '@oclif/core'
import { Command, type Config, Errors, Flags } from '@oclif/core'
Expand Down Expand Up @@ -38,6 +39,11 @@ export default class ApplicationsLogin extends Command {
required: false,
hidden: true
}),
'api-version': Flags.string({
description: 'Core API version (default: the CLI default), saved with the application',
required: false,
hidden: true
}),
clientId: Flags.string({
name: 'clientId',
char: 'i',
Expand Down Expand Up @@ -111,12 +117,14 @@ export default class ApplicationsLogin extends Command {
clientSecret: flags.clientSecret,
slug: flags.organization,
domain: flags.domain,
apiVersion: flags['api-version'],
scope,
email: flags.email,
password: flags.password
}

if (config.domain === configParam(ConfigParams.defaultDomain)) config.domain = undefined
if (!config.apiVersion || (config.apiVersion === configParam(ConfigParams.defaultApiVersion))) config.apiVersion = undefined


try {
Expand Down Expand Up @@ -180,14 +188,15 @@ const getApplicationInfo = async (auth: AppAuth, accessToken: string): Promise<A

let org: Partial<Organization>, app: Partial<Application>, user: any
if (provisioning) {
const clp = clprovisioning({ domain: auth.domain, accessToken })
// Unversioned requests (/api/…): provisioning-sdk 3 types require 2026-05, the SDK omits the segment when undefined
const clp = clprovisioning({ apiVersion: undefined as unknown as Parameters<typeof clprovisioning>[0]['apiVersion'], domain: auth.domain, accessToken })
// User info
const usr = await clp.user.retrieve().catch(() => { error(clp.user.type()) })
if (usr) user = { name: `${usr.first_name}${(usr.first_name && usr.last_name)? ' ' : ''}${usr.last_name}`, email: usr.email }
org = { slug: 'provisioning', name: user?.name || 'Provisioning API' }
app = { name: 'Provisioning App' }
} else { // core
const cl = commercelayer({ organization: auth.slug || '', domain: auth.domain, accessToken })
const cl = commercelayer({ apiVersion: clApi.apiVersion({ 'api-version': auth.apiVersion }) as ApiVersion, organization: auth.slug || '', domain: auth.domain, accessToken })
// Organization info
org = await cl.organization.retrieve().catch(() => { error(cl.organization.type()) })
// Application info
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/applications/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default class ApplicationsScope extends Command {
const argv: string[] = []
if (current.slug) argv.push('-o', current.slug)
if (current.domain) argv.push('-d', current.domain)
if (current.apiVersion) argv.push('--api-version', current.apiVersion)

let appInfo: AppInfo

Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const currentApplication = (app?: AppInfo): AppInfo | undefined => {
name: app.name,
slug: app.slug,
domain: app.domain,
apiVersion: app.apiVersion,
kind: app.kind,
mode: app.mode,
organization: app.organization,
Expand Down Expand Up @@ -191,6 +192,7 @@ enum ConfigParams {
applicationTypeCheck = 'applicationTypeCheck',
scopeCheck = 'scopeCheck',
defaultDomain = 'defaultDomain',
defaultApiVersion = 'defaultApiVersion',
test = 'test'
}

Expand All @@ -203,6 +205,7 @@ const defaultConfig: any = {
test: 'defaultTestValue',
commandRetention: 30, // days of retention
defaultDomain: clConfig.api.default_domain,
defaultApiVersion: clConfig.api.default_api_version,
applicationTypeCheck: clConfig.cli.applications,
scopeCheck: clConfig.application.login_scopes
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/hooks/prerun/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const hook: Hook<'prerun'> = async function (opts) {
// Add to command line args application info read from config file
if (_flags.organization && configData.slug) opts.argv.splice(ffIdx, 0, '--organization=' + configData.slug)
if (_flags.domain && configData.domain) opts.argv.splice(ffIdx, 0, '--domain=' + configData.domain)
if (_flags['api-version'] && configData.apiVersion) opts.argv.splice(ffIdx, 0, '--api-version=' + configData.apiVersion)

// If command requires clientId and clientSecret (or scope) add them to the command line arguments
if (_flags.clientId && configData.clientId) opts.argv.splice(ffIdx, 0, '--clientId=' + configData.clientId)
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/patches/command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

import { clText } from "@commercelayer/cli-core"
import { CommerceLayerProvisioningStatic } from "@commercelayer/provisioning-sdk"

// The CommonJS declarations of the provisioning-sdk preview hide its named exports
// behind export=: the value comes from require(), typed with the ESM declarations
const { CommerceLayerProvisioningStatic } = require('@commercelayer/provisioning-sdk') as typeof import('@commercelayer/provisioning-sdk', { with: { 'resolution-mode': 'import' } })


const PROVISIONING = true
Expand Down
58 changes: 58 additions & 0 deletions packages/cli/test/hooks/prerun/application-flags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect } from 'chai'
import hook from '../../../src/hooks/prerun/login'

/**
* The application settings saved at login reach the commands as flags: the
* domain and the Core API version are added to the command line, unless the
* command has no such flag. Runs on a temporary config folder, with the
* application passed by --appkey: the saved current application is not used.
*/
describe('prerun hook: saved application flags', () => {
let configDir: string
const key = 'testAppKey'

const save = (app: Record<string, unknown>) => {
const apps = join(configDir, 'applications')
mkdirSync(apps, { recursive: true })
writeFileSync(join(apps, `${key}.config.json`), JSON.stringify({ key, mode: 'test', kind: 'integration', slug: 'cli-test-org', ...app }))
writeFileSync(join(apps, `${key}.token.json`), JSON.stringify({ accessToken: 'token', expires: new Date(Date.now() + 3_600_000).toISOString() }))
}

const run = async (flags: string[]): Promise<string[]> => {
const config = { configDir } as any
const Command = { id: 'resources:list', flags: Object.fromEntries(flags.map((f) => [f, {}])), baseFlags: {} } as any
const argv = ['skus', `--appkey=${key}`]
await hook.call({ config, error: (msg: string) => { throw new Error(msg) }, log: () => {} } as any, { Command, argv, config } as any)
return argv
}

beforeEach(() => {
configDir = mkdtempSync(join(tmpdir(), 'cl-cli-prerun-'))
})
afterEach(() => rmSync(configDir, { recursive: true, force: true }))

it('adds the saved domain and API version', async () => {
save({ domain: 'commercelayer.co', apiVersion: '2026-05' })
const argv = await run(['organization', 'domain', 'api-version', 'accessToken'])
expect(argv).to.include('--organization=cli-test-org')
expect(argv).to.include('--domain=commercelayer.co')
expect(argv).to.include('--api-version=2026-05')
expect(argv).to.include('--accessToken=token')
})

it('adds no API version when none is saved, so the default or CL_CLI_API_VERSION apply', async () => {
save({})
const argv = await run(['organization', 'domain', 'api-version', 'accessToken'])
expect(argv.some((a) => a.startsWith('--api-version'))).to.equal(false)
expect(argv.some((a) => a.startsWith('--domain'))).to.equal(false)
})

it('adds no API version to a command without the flag', async () => {
save({ apiVersion: '2026-05' })
const argv = await run(['organization', 'accessToken'])
expect(argv.some((a) => a.startsWith('--api-version'))).to.equal(false)
})
})
13 changes: 12 additions & 1 deletion packages/core/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ const extractDomain = (baseUrl: string): string | undefined => {
}


/**
* Core API version of the requests, as the domain is for their URL: the
* --api-version flag (saved with the application at login, or set by
* CL_CLI_API_VERSION), otherwise the default version (config.api.default_api_version).
* Undefined means unversioned requests (/api/…).
*/
const apiVersion = (flags?: Record<string, any>): string | undefined => {
return flags?.['api-version'] || process.env.CL_CLI_API_VERSION || config.api.default_api_version || undefined
}


/** Decode API execution mode */
const execMode = (liveFlag: string | boolean | undefined): ApiMode => {
return ((liveFlag === true) || (liveFlag === 'live')) ? 'live' : 'test'
Expand Down Expand Up @@ -54,7 +65,7 @@ const isRecordCountEstimated = (meta?: { recordCount?: number; recordCountEstima
}


export { baseURL, execMode, extractDomain, humanizeResource, isRecordCountEstimated, RECORD_COUNT_EXACT_MAX }
export { apiVersion, baseURL, execMode, extractDomain, humanizeResource, isRecordCountEstimated, RECORD_COUNT_EXACT_MAX }


const CACHEABLE_RESOURCES = [
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface AppKey {
interface AppAuth {
slug?: string
domain?: string
/** Core API version of the requests (see clApi.apiVersion), unversioned when missing */
apiVersion?: string
clientId: string
clientSecret?: string
scope?: AuthScope
Expand Down
14 changes: 13 additions & 1 deletion packages/core/src/command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */

import type { Command, Interfaces } from "@oclif/core"
import { type Command, Flags, type Interfaces } from "@oclif/core"

type FlagInput = Interfaces.FlagInput

Expand All @@ -16,6 +16,18 @@ export const commandFlags = <T extends FlagInput>(flags: T, exclude?: Array<keyo
}


/**
* The --api-version flag of the commands that call the Core API (see clApi.apiVersion).
* Hidden like --domain: set by CL_CLI_API_VERSION, mostly to pick the environment in development.
*/
export const apiVersionFlag = () => Flags.string({
description: 'the Core API version of the requests (e.g. 2026-05), unversioned by default',
required: false,
hidden: true,
env: 'CL_CLI_API_VERSION',
})


export const allFlags = (command: Command.Class): FlagInput => {
return { ...command.flags, ...command.baseFlags }
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ const LINK_RESOURCE_TYPES: readonly string[] = [

type ApiConfig = {
default_domain: string
/** Core API version used when none is given ('' would make the requests unversioned, /api/…) */
default_api_version: string
default_app_domain: string
default_stg_domain: string
token_expiration_mins: number
Expand Down Expand Up @@ -244,6 +246,7 @@ const RATE_LIMIT = {
const config: Config = {
api: { // CORE
default_domain: 'commercelayer.io',
default_api_version: '2026-05',
default_app_domain: 'commercelayer.app',
default_stg_domain: 'commercelayer.co',
token_expiration_mins: 60 * 4, // 4 hours (14400 secs)
Expand Down
38 changes: 38 additions & 0 deletions packages/core/test/unit/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from 'node:path'
import { api, coreApi, useMockedApi } from '@commercelayer/cli-test-utils'
import { expect } from 'chai'
import {
apiVersion,
baseURL,
execMode,
extractDomain,
Expand All @@ -16,6 +17,7 @@ import {
request,
requestRateLimitDelay,
} from '../../src/api'
import clConfig from '../../src/config'

describe('api', () => {
describe('baseURL', () => {
Expand Down Expand Up @@ -49,6 +51,42 @@ describe('api', () => {
expect(liveEnvironment('test')).to.equal(false)
})

describe('apiVersion', () => {
let env: string | undefined
beforeEach(() => {
env = process.env.CL_CLI_API_VERSION
delete process.env.CL_CLI_API_VERSION
})
afterEach(() => {
if (env === undefined) delete process.env.CL_CLI_API_VERSION
else process.env.CL_CLI_API_VERSION = env
})

it('defaults to 2026-05', () => {
expect(apiVersion()).to.equal('2026-05')
expect(apiVersion({})).to.equal('2026-05')
})

it('takes the flag, then the environment', () => {
process.env.CL_CLI_API_VERSION = '2017-08'
expect(apiVersion()).to.equal('2017-08')
expect(apiVersion({ 'api-version': '2026-05' })).to.equal('2026-05')
})

it('falls back to the default version, as the domain does', () => {
const saved = clConfig.api.default_api_version
try {
clConfig.api.default_api_version = '2025-01'
expect(apiVersion()).to.equal('2025-01')
process.env.CL_CLI_API_VERSION = '2017-08'
expect(apiVersion()).to.equal('2017-08')
expect(apiVersion({ 'api-version': '2026-05' })).to.equal('2026-05')
} finally {
clConfig.api.default_api_version = saved
}
})
})

it('tells estimated list counts', () => {
expect(RECORD_COUNT_EXACT_MAX).to.equal(10_000)
expect(isRecordCountEstimated({ recordCount: 10_000 })).to.equal(false)
Expand Down
13 changes: 12 additions & 1 deletion packages/test-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,18 @@ export const useMockedApi = (): void => {
}

/** A nock scope on the Core API of the test organization */
export const api = (base: string = coreApi()): nock.Scope => nock(base)
/** The version segment of the Core API paths (/api/2026-05/…) */
const API_VERSION_SEGMENT = /^\/api\/\d{4}-\d{2}\//

/**
* A mock of the API. The requests carry the Core API version in their path
* (/api/2026-05/orders): the mocks are written for /api/orders and match
* whatever version the CLI uses (versionedApi checks the version).
*/
export const api = (base: string = coreApi()): nock.Scope => nock(base).filteringPath((path) => path.replace(API_VERSION_SEGMENT, '/api/'))

/** A mock of the API that matches the exact request path, version included */
export const versionedApi = (base: string = coreApi()): nock.Scope => nock(base)

/** An unsigned JWT: the CLI commands only decode access tokens */
export const jwt = (payload: object, header: object = { alg: 'HS512', typ: 'JWT' }): string =>
Expand Down
5 changes: 4 additions & 1 deletion plugins/checkout/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { clColor, clOutput, clToken, clUpdate, clUtil } from '@commercelayer/cli-core'
import { clApi, clColor, clCommand, clOutput, clToken, clUpdate, clUtil } from '@commercelayer/cli-core'
import type { ApiVersion } from '@commercelayer/sdk'
import commercelayer, { type CommerceLayerClient, CommerceLayerStatic } from '@commercelayer/sdk'
import type { Interfaces } from '@oclif/core'
import { Args, Command, Flags } from '@oclif/core'
Expand Down Expand Up @@ -29,6 +30,7 @@ export default abstract class extends Command {
dependsOn: ['organization'],
env: 'CL_CLI_DOMAIN'
}),
'api-version': clCommand.apiVersionFlag(),
accessToken: Flags.string({
char: 'a',
description: 'custom access token to use instead of the one used for login',
Expand Down Expand Up @@ -81,6 +83,7 @@ export default abstract class extends Command {
const userAgent = clUtil.userAgent(this.config)

return commercelayer({
apiVersion: clApi.apiVersion(flags) as ApiVersion,
organization,
domain,
accessToken,
Expand Down
5 changes: 4 additions & 1 deletion plugins/cleanups/src/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { clColor, clFilter, clOutput, clToken, clUpdate, clUtil, type KeyValRel, type KeyValString } from '@commercelayer/cli-core'
import { clApi, clColor, clCommand, clFilter, clOutput, clToken, clUpdate, clUtil, type KeyValRel, type KeyValString } from '@commercelayer/cli-core'
import * as cliux from '@commercelayer/cli-ux'
import type { ApiVersion } from '@commercelayer/sdk'
import commercelayer, { type CommerceLayerClient, CommerceLayerStatic } from '@commercelayer/sdk'
import type { Interfaces } from '@oclif/core'
import { Args, Command, Flags } from '@oclif/core'
Expand Down Expand Up @@ -27,6 +28,7 @@ export default abstract class extends Command {
dependsOn: ['organization'],
env: 'CL_CLI_DOMAIN',
}),
'api-version': clCommand.apiVersionFlag(),
accessToken: Flags.string({
hidden: true,
required: true,
Expand Down Expand Up @@ -120,6 +122,7 @@ export default abstract class extends Command {
const userAgent = clUtil.userAgent(this.config)

return commercelayer({
apiVersion: clApi.apiVersion(flags) as ApiVersion,
organization,
domain,
accessToken,
Expand Down
Loading
Loading