|
| 1 | +--- |
| 2 | +description: "Contentstack core CLI package patterns — plugin aggregation, hooks, and entry point" |
| 3 | +globs: ["packages/contentstack/src/**/*.ts", "packages/contentstack/src/**/*.js"] |
| 4 | +alwaysApply: false |
| 5 | +--- |
| 6 | + |
| 7 | +# Contentstack Core Package Standards |
| 8 | + |
| 9 | +## Overview |
| 10 | + |
| 11 | +The `@contentstack/cli` core package is the entry point for the entire CLI. Unlike plugin packages (auth, config), it: |
| 12 | +- **Aggregates all plugins** — declared in `oclif.plugins` array in `package.json` |
| 13 | +- **Implements hooks** — `init` and `prerun` hooks in `src/hooks/` for global behaviors |
| 14 | +- **Shares interfaces** — Core types used across all plugins in `src/interfaces/` |
| 15 | +- **Provides utilities** — Helper classes like `CsdxContext` in `src/utils/` |
| 16 | +- **Has no command files** — Commands are provided by plugin packages |
| 17 | + |
| 18 | +## Architecture |
| 19 | + |
| 20 | +### Entry Point |
| 21 | + |
| 22 | +```typescript |
| 23 | +// ✅ GOOD - bin/run.js (CommonJS) |
| 24 | +// This is the executable entry point referenced in package.json "bin" |
| 25 | +// Standard OCLIF entry point pattern |
| 26 | +``` |
| 27 | + |
| 28 | +### Package Configuration |
| 29 | + |
| 30 | +The `oclif` configuration in `package.json`: |
| 31 | +```json |
| 32 | +{ |
| 33 | + "oclif": { |
| 34 | + "bin": "csdx", |
| 35 | + "topicSeparator": ":", |
| 36 | + "helpClass": "./lib/help.js", |
| 37 | + "plugins": [ |
| 38 | + "@oclif/plugin-help", |
| 39 | + "@oclif/plugin-not-found", |
| 40 | + "@oclif/plugin-plugins", |
| 41 | + "@contentstack/cli-config", |
| 42 | + "@contentstack/cli-auth" |
| 43 | + // ... more plugins |
| 44 | + ], |
| 45 | + "hooks": { |
| 46 | + "init": [ |
| 47 | + "./lib/hooks/init/context-init", |
| 48 | + "./lib/hooks/init/utils-init" |
| 49 | + ], |
| 50 | + "prerun": [ |
| 51 | + "./lib/hooks/prerun/init-context-for-command", |
| 52 | + "./lib/hooks/prerun/command-deprecation-check", |
| 53 | + "./lib/hooks/prerun/default-rate-limit-check", |
| 54 | + "./lib/hooks/prerun/latest-version-warning" |
| 55 | + ] |
| 56 | + }, |
| 57 | + "topics": { |
| 58 | + "auth": { "description": "Perform authentication-related activities" }, |
| 59 | + "config": { "description": "Perform configuration related activities" }, |
| 60 | + "cm": { "description": "Perform content management activities" } |
| 61 | + } |
| 62 | + } |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +## Hook Lifecycle |
| 67 | + |
| 68 | +### OCLIF Hook Execution Order |
| 69 | + |
| 70 | +1. **CLI initialization** → Node process starts |
| 71 | +2. **`init` hooks** → Set up global context and utilities (executed once) |
| 72 | +3. **Command detection** → OCLIF matches command name to plugin |
| 73 | +4. **`prerun` hooks** → Validate state, check auth, prepare for command execution (per command) |
| 74 | +5. **Command execution** → Plugin command's `run()` method executes |
| 75 | + |
| 76 | +### Init Hooks |
| 77 | + |
| 78 | +Init hooks run once during CLI startup. Use them for expensive setup operations. |
| 79 | + |
| 80 | +```typescript |
| 81 | +// ✅ GOOD - src/hooks/init/context-init.ts |
| 82 | +// Initialize CLI context that commands depend on |
| 83 | +import { CsdxContext } from '../../utils'; |
| 84 | +import { configHandler } from '@contentstack/cli-utilities'; |
| 85 | + |
| 86 | +export default function (opts): void { |
| 87 | + // Store command ID for session-based log organization |
| 88 | + if (opts.id) { |
| 89 | + configHandler.set('currentCommandId', opts.id); |
| 90 | + } |
| 91 | + // Make context available to all commands via this.config.context |
| 92 | + this.config.context = new CsdxContext(opts, this.config); |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +### Prerun Hooks |
| 97 | + |
| 98 | +Prerun hooks run before each command. Use them for validation and state checks. |
| 99 | + |
| 100 | +```typescript |
| 101 | +// ✅ GOOD - src/hooks/prerun/auth-guard.ts |
| 102 | +// Validate authentication before running protected commands |
| 103 | + |
| 104 | +import { cliux, isAuthenticated, managementSDKClient } from '@contentstack/cli-utilities'; |
| 105 | + |
| 106 | +export default async function (opts): Promise<void> { |
| 107 | + const { context: { region = null } = {} } = this.config; |
| 108 | + |
| 109 | + // Validate region is set (required for all non-region commands) |
| 110 | + if (opts.Command.id !== 'config:set:region') { |
| 111 | + if (!region) { |
| 112 | + cliux.error('No region found, please set a region via config:set:region'); |
| 113 | + this.exit(); |
| 114 | + return; |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + // Example: Validate auth for protected commands |
| 119 | + if (isProtectedCommand(opts.Command.id)) { |
| 120 | + if (!isAuthenticated()) { |
| 121 | + cliux.error('Please log in to execute this command'); |
| 122 | + this.exit(); |
| 123 | + } |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +### Hook Patterns |
| 129 | + |
| 130 | +#### Accessing Configuration |
| 131 | +```typescript |
| 132 | +// ✅ GOOD - Access global config in hooks |
| 133 | +export default function (opts): void { |
| 134 | + const { config } = this; // OCLIF Config object |
| 135 | + const { context, region } = config; // Custom properties set by other hooks |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +#### Async Hooks |
| 140 | +```typescript |
| 141 | +// ✅ GOOD - Async hooks for operations requiring I/O |
| 142 | +export default async function (opts): Promise<void> { |
| 143 | + const client = await managementSDKClient({ host: this.config.region.cma }); |
| 144 | + const user = await client.getUser(); |
| 145 | + // Hook runs to completion before command starts |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +#### Early Exit |
| 150 | +```typescript |
| 151 | +// ✅ GOOD - Exit hook execution when validation fails |
| 152 | +export default function (opts): void { |
| 153 | + if (!isValid()) { |
| 154 | + cliux.error('Validation failed'); |
| 155 | + this.exit(); // Stops command from executing |
| 156 | + return; |
| 157 | + } |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +## Context Object |
| 162 | + |
| 163 | +The `CsdxContext` class wraps OCLIF config and adds CLI-specific state. |
| 164 | + |
| 165 | +```typescript |
| 166 | +// ✅ GOOD - Accessing context in commands |
| 167 | +import { CLIConfig } from '../interfaces'; |
| 168 | + |
| 169 | +export default class MyCommand extends Command { |
| 170 | + async run(): Promise<void> { |
| 171 | + const config: CLIConfig = this.config; |
| 172 | + const { context } = config; |
| 173 | + |
| 174 | + // Available context properties: |
| 175 | + // - context.id: unique session identifier |
| 176 | + // - context.user: authenticated user info (authtoken, email) |
| 177 | + // - context.region: current region configuration |
| 178 | + // - context.config: regional configuration |
| 179 | + // - context.plugin: current plugin metadata |
| 180 | + } |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +## Shared Interfaces |
| 185 | + |
| 186 | +Interfaces in `src/interfaces/index.ts` are exported and consumed by all plugins. |
| 187 | + |
| 188 | +```typescript |
| 189 | +// ✅ GOOD - Define shared types |
| 190 | +export interface Context { |
| 191 | + id: string; |
| 192 | + user: { |
| 193 | + authtoken: string; |
| 194 | + email: string; |
| 195 | + }; |
| 196 | + region: Region; |
| 197 | + plugin: Plugin; |
| 198 | + config: any; |
| 199 | +} |
| 200 | + |
| 201 | +export interface CLIConfig extends Config { |
| 202 | + context: Context; |
| 203 | +} |
| 204 | + |
| 205 | +export interface Region { |
| 206 | + name: string; |
| 207 | + cma: string; // Content Management API endpoint |
| 208 | + cda: string; // Content Delivery API endpoint |
| 209 | +} |
| 210 | +``` |
| 211 | + |
| 212 | +## Utilities |
| 213 | + |
| 214 | +Core utilities in `src/utils/` provide shared functionality. |
| 215 | + |
| 216 | +```typescript |
| 217 | +// ✅ GOOD - src/utils/context-handler.ts |
| 218 | +// Wrapper around context initialization and access |
| 219 | +export class CsdxContext { |
| 220 | + constructor(opts: any, config: any) { |
| 221 | + this.id = opts.id || generateId(); |
| 222 | + this.region = config.region; |
| 223 | + this.user = extractUserFromToken(); |
| 224 | + } |
| 225 | +} |
| 226 | + |
| 227 | +// Export utilities for use in hooks and contexts |
| 228 | +export { CsdxContext }; |
| 229 | +``` |
| 230 | + |
| 231 | +## Plugin Registration |
| 232 | + |
| 233 | +Plugins are registered via `oclif.plugins` in `package.json`. Each plugin package must: |
| 234 | + |
| 235 | +1. **Provide commands** — via `oclif.commands` in its `package.json` |
| 236 | +2. **Be installed** — as a dependency in the core package |
| 237 | +3. **Be listed** — in `oclif.plugins` array for auto-discovery |
| 238 | + |
| 239 | +```json |
| 240 | +{ |
| 241 | + "dependencies": { |
| 242 | + "@contentstack/cli-config": "~1.20.0-beta.1", |
| 243 | + "@contentstack/cli-auth": "~1.8.0-beta.1" |
| 244 | + }, |
| 245 | + "oclif": { |
| 246 | + "plugins": [ |
| 247 | + "@contentstack/cli-config", |
| 248 | + "@contentstack/cli-auth" |
| 249 | + ] |
| 250 | + } |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +### Plugin Discovery |
| 255 | + |
| 256 | +OCLIF automatically discovers commands in: |
| 257 | +1. Built-in plugins (`@oclif/plugin-help`, etc.) |
| 258 | +2. Core package commands (none in contentstack core) |
| 259 | +3. Registered plugins (listed in `oclif.plugins`) |
| 260 | + |
| 261 | +## Differences from Plugin Packages |
| 262 | + |
| 263 | +| Aspect | Core Package | Plugin Package | |
| 264 | +|--------|--------------|----------------| |
| 265 | +| **OCLIF config** | No `commands` field | Has `oclif.commands: "./lib/commands"` | |
| 266 | +| **Source structure** | `src/hooks/`, `src/interfaces/`, `src/utils/` | `src/commands/`, `src/services/` | |
| 267 | +| **Entry point** | `bin/run.js` | None | |
| 268 | +| **Dependencies** | References all plugins | Depends on `@contentstack/cli-command` | |
| 269 | +| **Execution role** | Aggregates and initializes | Implements business logic | |
| 270 | + |
| 271 | +## Build Process |
| 272 | + |
| 273 | +The core package build includes hook compilation and OCLIF manifest generation. |
| 274 | + |
| 275 | +```bash |
| 276 | +# In package.json scripts |
| 277 | +"build": "pnpm compile && oclif manifest && oclif readme" |
| 278 | +``` |
| 279 | + |
| 280 | +### Build Steps |
| 281 | + |
| 282 | +1. **compile** — TypeScript → JavaScript in `lib/` |
| 283 | +2. **oclif manifest** — Generate `oclif.manifest.json` for plugin discovery |
| 284 | +3. **oclif readme** — Generate README with available commands |
| 285 | + |
| 286 | +### Build Artifacts |
| 287 | + |
| 288 | +- `lib/` — Compiled hooks, utilities, interfaces |
| 289 | +- `oclif.manifest.json` — Plugin and command registry |
| 290 | +- `bin/run.js` — Executable entry point |
| 291 | +- `README.md` — Generated command documentation |
| 292 | + |
| 293 | +## Testing Hooks |
| 294 | + |
| 295 | +Hooks cannot be tested with standard command testing. Test hook behavior by: |
| 296 | + |
| 297 | +1. **Unit test hook functions** — Import and invoke directly |
| 298 | +2. **Integration test via CLI** — Run commands that trigger hooks |
| 299 | +3. **Mock OCLIF config** — Provide mocked `this.config` object |
| 300 | + |
| 301 | +```typescript |
| 302 | +// ✅ GOOD - Test hook function directly |
| 303 | +import contextInit from '../src/hooks/init/context-init'; |
| 304 | + |
| 305 | +describe('context-init hook', () => { |
| 306 | + it('should set context on config', () => { |
| 307 | + const mockConfig = { context: null }; |
| 308 | + const hookContext = { config: mockConfig }; |
| 309 | + const opts = { id: 'test-command' }; |
| 310 | + |
| 311 | + contextInit.call(hookContext, opts); |
| 312 | + |
| 313 | + expect(mockConfig.context).to.exist; |
| 314 | + }); |
| 315 | +}); |
| 316 | +``` |
| 317 | + |
| 318 | +## Error Handling in Hooks |
| 319 | + |
| 320 | +Hooks should fail fast and provide clear error messages to users. |
| 321 | + |
| 322 | +```typescript |
| 323 | +// ✅ GOOD - Clear error messages with user guidance |
| 324 | +export default function (opts): void { |
| 325 | + if (!isRegionSet()) { |
| 326 | + cliux.error('No region configured'); |
| 327 | + cliux.print('Run: csdx config:set:region --region us', { color: 'blue' }); |
| 328 | + this.exit(); |
| 329 | + } |
| 330 | +} |
| 331 | +``` |
| 332 | + |
| 333 | +## Best Practices |
| 334 | + |
| 335 | +### Hook Organization |
| 336 | +- Keep hooks focused on a single concern (validation, initialization, etc.) |
| 337 | +- Use descriptive names that indicate when they run (`prerun-`, `init-`) |
| 338 | +- Initialize dependencies in `init` hooks, not in `prerun` hooks |
| 339 | + |
| 340 | +### Performance |
| 341 | +- Minimize work in `init` hooks (they run once per CLI session) |
| 342 | +- Cache expensive operations in context for reuse |
| 343 | +- Avoid repeated API calls across hooks |
| 344 | + |
| 345 | +### Ordering |
| 346 | +- Place hooks that prepare data before hooks that consume it |
| 347 | +- Auth validation (`auth-guard`) should run after region validation |
| 348 | +- Version warnings can run last (non-critical) |
| 349 | + |
| 350 | +### Context Usage |
| 351 | +- Store computed values in context to avoid recalculation |
| 352 | +- Make context available to all commands via `this.config.context` |
| 353 | +- Document context properties that plugins should expect |
| 354 | + |
| 355 | +### Plugin Development |
| 356 | +- Ensure plugins depend on `@contentstack/cli-command`, not the core package |
| 357 | +- Commands should extend the shared Command base class |
| 358 | +- Plugins should not modify or depend on core hooks directly |
0 commit comments