diff --git a/.talismanrc b/.talismanrc index b66e75a899..101aad31d7 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,4 +1,4 @@ fileignoreconfig: - filename: pnpm-lock.yaml - checksum: 64ca5ee7000d70e76ba367e3cacc2158e5cd33e41c7844fb6af79837a9469bb6 + checksum: 4bad5f9428f5bc7ed837c91567b28afc97acefa7086eb03d2ffd90b4a2820233 version: '1.0' diff --git a/MIGRATION.md b/MIGRATION.md index 7853bc0d8a..e9eaf9147d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -189,6 +189,45 @@ csdx cm:entries:publish --content-types blog --environments prod --locales en-us csdx cm:stacks:bulk-entries --operation publish --content-types blog --environments prod --locales en-us -k blt123 ``` +### 7. 🚀 Launch Plugin Now Opt-In + +**What Changed:** +- The `launch` plugin (`@contentstack/cli-launch`) is no longer bundled with the CLI +- All `launch:*` commands now require installing the plugin explicitly before use +- Brings `launch` in line with the opt-in, modular plugin model used elsewhere in 2.x + +> ⚠️ **This is a change relative to the 2.x beta — not only relative to 1.x.** `@contentstack/cli-launch` was bundled throughout the 1.x releases **and** the entire 2.x beta period, so `launch:*` worked out of the box for beta users too. Starting with 2.x GA it becomes opt-in, so beta users who rely on `launch:*` are also affected when they upgrade. + +**Before (1.x.x and 2.x.x-beta):** +- `launch:*` commands were bundled with the CLI and available by default after installation + +**After (2.x.x GA):** +- `launch:*` commands are provided by a separate, opt-in plugin that must be installed explicitly +- Running any `launch:*` command without the plugin installed fails with a `command not found` error where it previously worked out of the box + +**Affected commands:** `launch`, `launch:deployments`, `launch:environments`, `launch:functions`, `launch:logs`, `launch:open`, `launch:rollback` + +**Installation Methods:** + + +**Option 1: Using CLI Plugin Manager** +```bash +csdx plugins:install @contentstack/cli-launch +``` + +**Option 2: Using npm** +```bash +npm install -g @contentstack/cli-launch +``` + +**Usage:** +After installation, launch commands will be available through the CLI: +```bash +csdx launch --help +``` + +**Migration Action:** If you use any `launch:*` command, install the `@contentstack/cli-launch` plugin before (or immediately after) upgrading to 2.x GA to avoid `command not found` errors. This applies to 2.x beta users as well, since `launch` was bundled during the beta. + ## Troubleshooting ### Common Issues @@ -209,6 +248,11 @@ csdx cm:stacks:bulk-entries --operation publish --content-types blog --environme - The 2.x.x-beta version should be faster due to TypeScript modules - If you are experiencing issues, switch to console log mode for debugging +**5. `launch:*` command not found:** +- In 2.x GA, `launch` is an opt-in plugin and is no longer bundled (it was bundled in 1.x and during the 2.x beta) +- Install it with `csdx plugins:install @contentstack/cli-launch`, then re-run your `launch:*` command +- Verify it is installed with `csdx plugins` + ### Getting Help **Documentation:** diff --git a/packages/contentstack-auth/package.json b/packages/contentstack-auth/package.json index 635f23f589..58f13a3472 100644 --- a/packages/contentstack-auth/package.json +++ b/packages/contentstack-auth/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-auth", "description": "Contentstack CLI plugin for authentication activities", - "version": "2.0.0-beta.16", + "version": "2.0.0-beta.17", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "scripts": { @@ -15,8 +15,8 @@ "lint": "eslint src/**/*.ts" }, "dependencies": { - "@contentstack/cli-command": "~2.0.0-beta.10", - "@contentstack/cli-utilities": "~2.0.0-beta.11", + "@contentstack/cli-command": "~2.0.0-beta.11", + "@contentstack/cli-utilities": "~2.0.0-beta.12", "@oclif/core": "^4.11.4", "otplib": "^12.0.1" }, diff --git a/packages/contentstack-command/package.json b/packages/contentstack-command/package.json index a8ff922f9b..09a161b5a3 100644 --- a/packages/contentstack-command/package.json +++ b/packages/contentstack-command/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-command", "description": "Contentstack CLI plugin for configuration", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "author": "Contentstack", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -14,7 +14,7 @@ "lint": "eslint src/**/*.ts" }, "dependencies": { - "@contentstack/cli-utilities": "~2.0.0-beta.11", + "@contentstack/cli-utilities": "~2.0.0-beta.12", "contentstack": "^3.27.0", "@oclif/core": "^4.11.4" }, diff --git a/packages/contentstack-config/package.json b/packages/contentstack-config/package.json index f8f35ef7dd..88fdfac5ab 100644 --- a/packages/contentstack-config/package.json +++ b/packages/contentstack-config/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-config", "description": "Contentstack CLI plugin for configuration", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "author": "Contentstack", "scripts": { "build": "pnpm compile && oclif manifest && oclif readme", @@ -14,8 +14,8 @@ "lint": "eslint src/**/*.ts" }, "dependencies": { - "@contentstack/cli-command": "~2.0.0-beta.10", - "@contentstack/cli-utilities": "~2.0.0-beta.11", + "@contentstack/cli-command": "~2.0.0-beta.11", + "@contentstack/cli-utilities": "~2.0.0-beta.12", "@contentstack/utils": "~1.9.1", "@oclif/core": "^4.11.4" }, diff --git a/packages/contentstack-utilities/package.json b/packages/contentstack-utilities/package.json index 7905d26f95..685088f31f 100644 --- a/packages/contentstack-utilities/package.json +++ b/packages/contentstack-utilities/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/cli-utilities", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "description": "Utilities for contentstack projects", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -31,7 +31,7 @@ "@contentstack/management": "~1.30.3", "@contentstack/marketplace-sdk": "^1.5.2", "@oclif/core": "^4.11.4", - "axios": "^1.16.1", + "axios": "^1.18.1", "chalk": "^5.6.2", "cli-cursor": "^3.1.0", "cli-progress": "^3.12.0", @@ -42,7 +42,7 @@ "inquirer": "12.11.1", "inquirer-search-checkbox": "^1.0.0", "inquirer-search-list": "^1.2.6", - "js-yaml": "^4.2.0", + "js-yaml": "^4.3.0", "klona": "^2.0.6", "lodash": "^4.18.1", "mkdirp": "^1.0.4", diff --git a/packages/contentstack-utilities/src/content-type-utils.ts b/packages/contentstack-utilities/src/content-type-utils.ts index 0edfe85b38..92cfd7377c 100644 --- a/packages/contentstack-utilities/src/content-type-utils.ts +++ b/packages/contentstack-utilities/src/content-type-utils.ts @@ -7,6 +7,15 @@ import { resolve as pResolve } from 'node:path'; * @param ignoredFiles - Files to ignore (defaults to schema.json, .DS_Store, __master.json, __priority.json) * @returns Array of content type schemas (empty if the path is missing or has no eligible files) */ +const DEFAULT_GF_IGNORED_FILES = ['schema.json', '.DS_Store', '__master.json', '__priority.json', 'field_rules_uid.json', 'globalfields.json']; + +export function readGlobalFieldSchemas( + dirPath: string, + ignoredFiles: string[] = DEFAULT_GF_IGNORED_FILES, +): Record[] { + return readContentTypeSchemas(dirPath, ignoredFiles); +} + export function readContentTypeSchemas( dirPath: string, ignoredFiles: string[] = ['schema.json', '.DS_Store', '__master.json', '__priority.json', 'field_rules_uid.json'], diff --git a/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts b/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts index bf0d843e74..07a9dd25b3 100644 --- a/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts +++ b/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts @@ -421,14 +421,19 @@ export default class CLIProgressManager { if (!this.showConsoleLogs && process.progressBar) { const totalProcessed = process.current; - const percentage = Math.round((totalProcessed / process.total) * 100); + // Reconcile the bar's total with the number of items actually processed so the + // rendered "value/total" matches the "Complete (success/processed)" summary. A + // process registered with an estimated total larger than the items actually ticked + // would otherwise render mismatched counts, e.g. "37/37 | Complete (27/27)". + process.progressBar.setTotal(totalProcessed); + const percentage = totalProcessed > 0 ? 100 : 0; const formattedPercentage = this.formatPercentage(percentage); const statusText = success - ? getChalk().green(`✓ Complete (${process.successCount}/${process.current})`) - : getChalk().red(`✗ Failed (${process.successCount}/${process.current})`); + ? getChalk().green(`✓ Complete (${process.successCount}/${totalProcessed})`) + : getChalk().red(`✗ Failed (${process.successCount}/${totalProcessed})`); const displayName = this.formatProcessName(processName); const indentedLabel = ` ├─ ${displayName}`.padEnd(25); - process.progressBar.update(process.total, { + process.progressBar.update(totalProcessed, { label: success ? getChalk().green(indentedLabel) : getChalk().red(indentedLabel), status: statusText, percentage: formattedPercentage, diff --git a/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts b/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts index cb0752b48d..ab6f20ab35 100644 --- a/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts +++ b/packages/contentstack-utilities/test/unit/cliProgressManager.test.ts @@ -34,6 +34,7 @@ const mockProgressBar = { stop: sinon.stub(), increment: sinon.stub(), update: sinon.stub(), + setTotal: sinon.stub(), }; const mockMultiBar = { @@ -335,6 +336,74 @@ describe('CLIProgressManager', () => { }); }); + describe('Complete count reconciliation', () => { + let bar: { update: sinon.SinonStub; setTotal: sinon.SinonStub; increment: sinon.SinonStub }; + + // Inject a stub bar at the rendering seam so assertions do not depend on the + // load-order-sensitive cli-progress module mock (which does not bind in CI). + function injectBar(processName: string) { + bar = { update: sinon.stub(), setTotal: sinon.stub(), increment: sinon.stub() }; + (progressManager as any).processes.get(processName).progressBar = bar; + } + + beforeEach(() => { + progressManager = new CLIProgressManager({ + enableNestedProgress: true, + moduleName: 'RECONCILE_TEST', + showConsoleLogs: false, + }); + }); + + fancy.it('renders the processed count, not the registered estimate, when fewer items were ticked', () => { + progressManager.addProcess('gf-update', 37); + injectBar('gf-update'); + for (let i = 0; i < 27; i++) { + progressManager.tick(true, `item-${i}`, null, 'gf-update'); + } + + progressManager.completeProcess('gf-update', true); + + expect(bar.setTotal.calledWith(27)).to.equal(true); + expect(bar.update.lastCall.args[0]).to.equal(27); + }); + + fancy.it('renders the full count unchanged when every registered item was ticked', () => { + progressManager.addProcess('entries-create', 59); + injectBar('entries-create'); + for (let i = 0; i < 59; i++) { + progressManager.tick(true, `item-${i}`, null, 'entries-create'); + } + + progressManager.completeProcess('entries-create', true); + + expect(bar.update.lastCall.args[0]).to.equal(59); + }); + + fancy.it('renders the registered total when a process completes with no ticks (skip case)', () => { + progressManager.addProcess('skipped', 5); + injectBar('skipped'); + + progressManager.completeProcess('skipped', true); + + expect(bar.update.lastCall.args[0]).to.equal(5); + }); + + fancy.it('reconciles the denominator for a failed process with partial ticks', () => { + progressManager.addProcess('entries', 59); + injectBar('entries'); + for (let i = 0; i < 38; i++) { + progressManager.tick(true, `ok-${i}`, null, 'entries'); + } + for (let i = 0; i < 3; i++) { + progressManager.tick(false, `err-${i}`, 'boom', 'entries'); + } + + progressManager.completeProcess('entries', false); + + expect(bar.update.lastCall.args[0]).to.equal(41); + }); + }); + describe('Progress Tracking', () => { beforeEach(() => { progressManager = new CLIProgressManager({ diff --git a/packages/contentstack-utilities/test/unit/content-type-utils.test.ts b/packages/contentstack-utilities/test/unit/content-type-utils.test.ts index 89778292be..79d03b3ba0 100644 --- a/packages/contentstack-utilities/test/unit/content-type-utils.test.ts +++ b/packages/contentstack-utilities/test/unit/content-type-utils.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { readContentTypeSchemas } from '../../src/content-type-utils'; +import { readContentTypeSchemas, readGlobalFieldSchemas } from '../../src/content-type-utils'; describe('readContentTypeSchemas', () => { afterEach(() => { @@ -144,4 +144,110 @@ describe('readContentTypeSchemas', () => { sinon.restore(); }); + + it('should NOT ignore globalfields.json (that is readGlobalFieldSchemas responsibility)', () => { + const mockBulk = [{ uid: 'gf_1', title: 'GF 1' }]; + const mockPerUid = { uid: 'gf_2', title: 'GF 2', schema: [] }; + + sinon.stub(require('fs'), 'existsSync').returns(true); + sinon.stub(require('fs'), 'readdirSync').returns(['globalfields.json', 'gf_2.json']); + const readFileStub = sinon.stub(require('fs'), 'readFileSync'); + readFileStub.withArgs(sinon.match(/globalfields\.json/), 'utf8').returns(JSON.stringify(mockBulk)); + readFileStub.withArgs(sinon.match(/gf_2\.json/), 'utf8').returns(JSON.stringify(mockPerUid)); + + const result = readContentTypeSchemas('/test/path'); + + expect(result).to.have.lengthOf(2); + + sinon.restore(); + }); +}); + +describe('readGlobalFieldSchemas', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should return empty array when directory does not exist', () => { + sinon.stub(require('fs'), 'existsSync').returns(false); + + const result = readGlobalFieldSchemas('/nonexistent/path'); + + expect(result).to.be.an('array'); + expect(result).to.have.lengthOf(0); + }); + + it('should read per-uid JSON files and ignore globalfields.json by default', () => { + const mockGF = { uid: 'gf_1', title: 'GF 1', schema: [] }; + + sinon.stub(require('fs'), 'existsSync').returns(true); + sinon.stub(require('fs'), 'readdirSync').returns(['gf_1.json', 'globalfields.json', '.DS_Store', 'schema.json']); + const readFileStub = sinon.stub(require('fs'), 'readFileSync'); + readFileStub.withArgs(sinon.match(/gf_1\.json/), 'utf8').returns(JSON.stringify(mockGF)); + + const result = readGlobalFieldSchemas('/test/path'); + + expect(result).to.be.an('array'); + expect(result).to.have.lengthOf(1); + expect(result[0].uid).to.equal('gf_1'); + }); + + it('should not include globalfields.json — prevents bulk-array corruption on import', () => { + const mockBulkArray = [{ uid: 'gf_1' }, { uid: 'gf_2' }]; + const mockPerUid = { uid: 'gf_3', title: 'GF 3', schema: [] }; + + sinon.stub(require('fs'), 'existsSync').returns(true); + sinon.stub(require('fs'), 'readdirSync').returns(['globalfields.json', 'gf_3.json']); + const readFileStub = sinon.stub(require('fs'), 'readFileSync'); + readFileStub.withArgs(sinon.match(/globalfields\.json/), 'utf8').returns(JSON.stringify(mockBulkArray)); + readFileStub.withArgs(sinon.match(/gf_3\.json/), 'utf8').returns(JSON.stringify(mockPerUid)); + + const result = readGlobalFieldSchemas('/test/path'); + + // Only the per-uid file is returned; globalfields.json array not parsed as a schema entry + expect(result).to.have.lengthOf(1); + expect(Array.isArray(result[0])).to.be.false; + expect((result[0] as any).uid).to.equal('gf_3'); + }); + + it('should read multiple per-uid files', () => { + const mockGF1 = { uid: 'gf_1', title: 'GF 1', schema: [] }; + const mockGF2 = { uid: 'gf_2', title: 'GF 2', schema: [] }; + + sinon.stub(require('fs'), 'existsSync').returns(true); + sinon.stub(require('fs'), 'readdirSync').returns(['gf_1.json', 'gf_2.json', 'globalfields.json']); + const readFileStub = sinon.stub(require('fs'), 'readFileSync'); + readFileStub.withArgs(sinon.match(/gf_1\.json/), 'utf8').returns(JSON.stringify(mockGF1)); + readFileStub.withArgs(sinon.match(/gf_2\.json/), 'utf8').returns(JSON.stringify(mockGF2)); + + const result = readGlobalFieldSchemas('/test/path'); + + expect(result).to.have.lengthOf(2); + expect(result.map((r: any) => r.uid)).to.include.members(['gf_1', 'gf_2']); + }); + + it('should return empty array when directory contains only globalfields.json', () => { + sinon.stub(require('fs'), 'existsSync').returns(true); + sinon.stub(require('fs'), 'readdirSync').returns(['globalfields.json', '.DS_Store']); + + const result = readGlobalFieldSchemas('/test/path'); + + expect(result).to.be.an('array'); + expect(result).to.have.lengthOf(0); + }); + + it('readContentTypeSchemas default ignore list is unchanged — no globalfields.json exclusion', () => { + sinon.stub(require('fs'), 'existsSync').returns(true); + sinon.stub(require('fs'), 'readdirSync').returns(['globalfields.json']); + sinon.stub(require('fs'), 'readFileSync') + .withArgs(sinon.match(/globalfields\.json/), 'utf8') + .returns(JSON.stringify([{ uid: 'gf_1' }])); + + // readContentTypeSchemas includes globalfields.json; readGlobalFieldSchemas excludes it + const ctResult = readContentTypeSchemas('/test/path'); + const gfResult = readGlobalFieldSchemas('/test/path'); + + expect(ctResult).to.have.lengthOf(1); + expect(gfResult).to.have.lengthOf(0); + }); }); diff --git a/packages/contentstack/README.md b/packages/contentstack/README.md index d3477ee4d1..9b449ee445 100644 --- a/packages/contentstack/README.md +++ b/packages/contentstack/README.md @@ -76,13 +76,6 @@ USAGE * [`csdx config:set:rate-limit`](#csdx-configsetrate-limit) * [`csdx config:set:region [REGION]`](#csdx-configsetregion-region) * [`csdx help [COMMAND]`](#csdx-help-command) -* [`csdx launch`](#csdx-launch) -* [`csdx launch:deployments`](#csdx-launchdeployments) -* [`csdx launch:environments`](#csdx-launchenvironments) -* [`csdx launch:functions`](#csdx-launchfunctions) -* [`csdx launch:logs`](#csdx-launchlogs) -* [`csdx launch:open`](#csdx-launchopen) -* [`csdx launch:rollback`](#csdx-launchrollback) * [`csdx login`](#csdx-login) * [`csdx logout`](#csdx-logout) * [`csdx plugins`](#csdx-plugins) @@ -1681,283 +1674,13 @@ DESCRIPTION _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/6.2.53/src/commands/help.ts)_ -## `csdx launch` - -Launch related operations - -``` -USAGE - $ csdx launch [-d ] [-c ] [--type GitHub|FileUpload] [--framework Gatsby|NextJs|CRA (Create - React App)|CSR (Client-Side Rendered)|Analog|Angular|Nuxt|Astro|VueJs|Remix|Other] [--org ] [-n ] [-e - ] [--branch ] [--build-command ] [--out-dir ] [--server-command ] - [--variable-type Import variables from a stack|Manually add custom variables to the list|Import variables from the - .env.local file|Skip adding environment variables...] [-a ] [--env-variables ] [--redeploy-latest] - [--redeploy-last-upload] [--response-mode buffered|streaming] - -FLAGS - -a, --alias= [optional] Alias (name) for the delivery token. - -c, --config= Path to the local '.cs-launch.json' file - -d, --data-dir= Current working directory - -e, --environment= [optional] Environment name for the Launch project. - -n, --name= [optional] Name of the project. - --branch= [optional] GitHub branch name. - --build-command= [optional] Build Command. - --env-variables= [optional] Provide the environment variables in the key:value format, separated by - comma. For example: APP_ENV:prod, TEST_ENV:testVal. - --framework=