Skip to content

Commit d191fde

Browse files
feat(launch): rebuild the CLI on REST as the v2 skeleton
Replaces the GraphQL-backed command surface with a clean-slate v2 line built on the public Launch REST API. Ships the architecture every later command is built inside, proven by two working commands. Transport and data: - RestApiClient over cli-utilities' HttpClient, owning auth headers, x-cs-api-version, OAuth-gated refresh on 401, and retry on 429 plus 408 for idempotent methods only - Typed error envelope using the singular launch.PROJECT.* codes the API emits - api/* resource modules that know nothing about the terminal: a client in, typed data out Command architecture: - A flag catalog declaring each flag once as a real oclif definition; a command declares its inputs once and derives its oclif flags from that declaration - A resolution chain of flag, then config, then prompt, then default, with a normalize hook, resolved in catalog order so a dependent prompt sees its dependencies - LaunchCommand owning auth, parsing, resolution, the confirm gate and exit-code mapping: 0 success, 1 runtime, 2 usage, 3 cancelled - Declarative cross-flag rules and a redaction primitive for rendered output Commands: - launch:projects:list and launch:projects:get - launch:functions:serve carried over unchanged from v1 Testing: - 164 tests at 100% coverage, CI-enforced - Wire-level integration tests with nock, plus end-to-end tests through oclif's runCommand, against fixtures derived from the published OpenAPI spec and corroborated against a live environment BREAKING CHANGE: v2 carries only the launch:<resource>:<verb> taxonomy and no v1 command name. v1 continues to ship from the development branch, supported and un-deprecated. #claude_code# 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6755575 commit d191fde

118 files changed

Lines changed: 4902 additions & 14134 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: test
2+
on:
3+
pull_request:
4+
branches: [feature/development-v2]
5+
push:
6+
branches: [feature/development-v2]
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: 22
16+
- run: npm ci
17+
- run: npm run lint
18+
- run: npm run build
19+
- run: npm run test:coverage

.mocharc.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

.talismanrc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,16 @@ fileignoreconfig:
2626
- "headers\\?\\.reduce\\(\\(acc, \\{ key, value \\}\\)"
2727
- "\\(\\{ key, value \\}\\) => \\(\\{ key, value \\}\\)\\)"
2828
- "const \\{ token, apiKey \\} = configHandler\\.get"
29+
- filename: test/fixtures/project-get.json
30+
ignore_detectors:
31+
- filecontent
32+
- filename: test/fixtures/projects-list.json
33+
ignore_detectors:
34+
- filecontent
35+
- filename: test/integration/projects-get.test.ts
36+
ignore_detectors:
37+
- filecontent
38+
- filename: test/integration/projects-list.test.ts
39+
ignore_detectors:
40+
- filecontent
2941
version: "1.0"

AGENTS.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,104 @@
88
- Use only jest for writing test cases and refer existing unit test under the /src folder.
99
- Do not create code comments for any changes.
1010

11+
**Integration tests.** `test/integration/` drives real code with only the network faked by `nock`.
12+
`projects-list-command.test.ts` runs whole commands through `@oclif/test`'s `runCommand`, which
13+
covers `init()`, the resolution chain, rendering and the `catch()` exit-code mapping in one pass.
14+
Two things make that reliable and both are load-bearing:
15+
16+
- The `Config` is built from a root `Plugin` constructed with `ignoreManifest: true`. Without it, a
17+
generated `oclif.manifest.json``npm run prepack` writes one, and it is gitignored — makes oclif
18+
load the compiled `dist/commands` instead of `src`, so the suite would test stale compiled output
19+
and fail outright whenever `dist` is absent.
20+
- `console.log` is redirected straight to `process.stdout` so jest's console decoration stays out of
21+
the captured stdout, and `process.exitCode` is reset after each run because oclif sets it while
22+
handling a simulated CLI failure and would otherwise fail the whole jest run.
23+
24+
The confirm gate is the one part of `LaunchCommand` `runCommand` cannot reach yet: no shipped
25+
command declares `yes: {}`. It stays covered by `src/base/launch-command.test.ts` until one does.
26+
27+
## Adding a command (V2)
28+
29+
Five touch points, in this order:
30+
31+
1. **`src/flags/catalog.ts`** — only if the command introduces a flag no command uses yet.
32+
Transcribe it from the Commands Details page §"All flags" tables. Never set `required: true`.
33+
Catalog flag definition objects are shared by reference across every command that uses them —
34+
never mutate one in place.
35+
2. **`src/flags/resolution.ts`** — one entry per new flag, saying where its value may come from:
36+
`configPath`, `prompt`, `default`. `resolution` is typed `Record<FlagKey, ResolutionSpec>`, so a
37+
catalog key added without a matching resolution entry is a **compile error**, not something a
38+
test has to catch.
39+
3. **`src/api/<resource>.ts`** — only if the command calls an endpoint no command calls yet.
40+
These modules take a `RestApiClient` and return typed data. No `ux`, no prompts, no `process.exit`.
41+
4. **`src/api/index.ts`** — a new resource module must be registered here: a field on `ApiSurface`
42+
and its construction in `buildApi`. This is the one shared file every resource module edits,
43+
so expect to rebase on it.
44+
5. **`src/commands/launch/<resource>/<verb>.ts`** — the command itself: a `static inputs`
45+
declaration, `static flags = flagsFor(...)`, and a `run()` that calls the api and renders. The
46+
command's `flags` keys must equal its `inputs` keys — both shipped commands assert this by
47+
deriving `flags` from `inputs` via `flagsFor`, rather than declaring the two independently.
48+
49+
A command that declares `--project` in `inputs` must also declare `--org`: `resolution.project.normalize`
50+
reads `resolved.org` to resolve the project uid, and catalog order only resolves `org` first because
51+
both shipped commands declare it.
52+
53+
Everything else — parsing, resolution, prompting, name-to-uid normalisation, retries, auth
54+
headers, error mapping, exit codes, rendering — is inherited from `LaunchCommand`. If a new command
55+
needs a change in `src/base/`, `src/flags/` or `src/http/`, that is a signal worth raising rather
56+
than a routine edit.
57+
58+
**Confirm gate.** A destructive command opts in by adding `yes: {}` to its `inputs``--yes`
59+
is deliberately not a global flag — and `await this.confirm('<question>')` at the top of `run()`.
60+
It returns silently when `--yes` was passed, prompts on a TTY, exits 2 when there is neither, and
61+
exits 3 when the user declines. Never assume a yes yourself.
62+
63+
**Exit codes.** `src/config/constants.ts` owns them and `LaunchCommand.catch()` is the only place
64+
that maps an error to one:
65+
66+
| Code | Constant | Meaning |
67+
|---|---|---|
68+
| 0 | `EXIT_OK` | the command did what it was asked to do |
69+
| 1 | `EXIT_RUNTIME` | a runtime failure — `LaunchApiError`, an unauthenticated session, anything oclif handles |
70+
| 2 | `EXIT_USAGE` | a usage error — `UsageError`, `MissingInputError`, a failing cross-flag rule |
71+
| 3 | `EXIT_CANCELLED` | the user declined a confirmation (`CancelledError`) |
72+
73+
A declined confirmation is a deliberate "no", not a failure, so it does not share code 1 with an
74+
API 500 — a CI log has to be able to tell those apart. 130 would claim the process was killed by
75+
SIGINT, which is not what happened.
76+
77+
**Cross-flag rules.** A rule that is pure flag-versus-flag and evaluable from argv alone belongs in
78+
oclif's native `exclusive` / `relationships` on the catalog entry, where it also shows in `--help`.
79+
A rule that must read a *resolved* value (one that config, a prompt or a default may have supplied)
80+
belongs in `src/flags/rules.ts``exactlyOneOf`, `dependsOnValue`, `requiresFrameworkIn` — declared
81+
as a `static rules = [...]` array on the command. `resolveInputs` evaluates them after resolution,
82+
and a failing rule is a usage error (exit 2).
83+
84+
**Redaction.** Anything rendering an environment variable's value in a table or a detail block uses
85+
`src/output/redact.ts` (`REDACTED`, `redactedColumn`). Confirmation text and error text are not
86+
covered: nothing stops a future `variables:*` command from interpolating a value straight into
87+
`this.confirm(...)` or a thrown error's message. Building that guard needs a debug logger and an
88+
in-flight secret registry to redact against, neither of which exists yet — until one does, a command
89+
handling variable values must redact them itself before they reach `confirm()` or an error message.
90+
91+
Required-ness is declared in `inputs`, never as an oclif `required: true` flag: oclif's parse-time
92+
enforcement would fire before config or a prompt has had a chance to supply the value, so
93+
required-ness is enforced after the resolution chain runs instead. A command must read
94+
`this.resolved`, never `this.flags` — reading `this.flags` bypasses the resolution chain
95+
(config file, prompt, default) entirely and returns only what was passed on argv.
96+
97+
## Commits
98+
99+
Use Conventional Commits — `feat(scope): subject`, `fix(scope): subject`, `test:`, `docs:`,
100+
`chore:`, `refactor:`. Do not prefix a commit subject with a ticket id; reference the ticket in
101+
the pull request instead.
102+
103+
## What does not belong in this repository
104+
105+
This repo holds the CLI and nothing else. Never commit AI tooling or process scaffolding here —
106+
agent prompts, per-epic or per-ticket instructions, workflow runbooks, planning or hand-off
107+
documents, or generated analysis. Those live in the developer workspace, outside this repo.
108+
109+
`AGENTS.md` and `README.md` are the exception: repo-scoped guidance that a contributor reads to
110+
work on this codebase belongs here. A document written to drive an assistant through a ticket
111+
does not.

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
[![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
44

5+
> **This is the V2 line.** V2 ships as `@contentstack/cli-launch@2.x` from `feature/development-v2`
6+
> and carries only the `launch:<resource>:<verb>` taxonomy. V1 continues to ship as `1.x` from
7+
> `development` with its existing commands and flags, supported and un-deprecated (PRD G10).
8+
> The two are separate major versions of the same package; no V1 command name exists in V2.
9+
510
With Launch CLI, you can interact with the Contentstack Launch platform using the terminal to create, manage and deploy Launch projects.
611

712
<!-- toc -->
@@ -29,13 +34,13 @@ $ csdx launch
2934
# Commands
3035

3136
```sh-session
32-
$ csdx launch
33-
start with launch flow <GitHub|FileUpload>
34-
$ csdx launch:logs
35-
To see server logs
36-
$ csdx launch:logs --type d
37-
To see deployment logs
38-
$ csdx launch:functions
37+
$ csdx launch:projects:list --org <org-uid>
38+
List the Launch projects in an organization
39+
40+
$ csdx launch:projects:get --org <org-uid> --project <name-or-uid>
41+
Show a single project
42+
43+
$ csdx launch:functions:serve
3944
Run cloud functions locally
4045
```
4146

eslint.config.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ import tseslint from 'typescript-eslint';
66
export default tseslint.config(
77
eslint.configs.recommended,
88
tseslint.configs.recommended,
9+
{
10+
files: ['jest.config.js'],
11+
languageOptions: {
12+
globals: {
13+
module: 'writable',
14+
require: 'writable',
15+
},
16+
},
17+
},
918
{
1019
files: ['src/**/*.{js,ts}'],
1120
rules: {
@@ -33,6 +42,13 @@ export default tseslint.config(
3342
},
3443
{
3544
files: ['test/**/*.{js,ts}'],
45+
languageOptions: {
46+
globals: {
47+
module: 'writable',
48+
require: 'writable',
49+
Buffer: 'readonly',
50+
},
51+
},
3652
rules: {
3753
'max-len': 'off',
3854
// chai assertions (e.g. expect(x).to.be.true) read as unused expressions

jest.config.js

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
1-
/**
2-
* For a detailed explanation regarding each configuration property, visit:
3-
* https://jestjs.io/docs/configuration
4-
*/
5-
61
/** @type {import('jest').Config} */
72
const config = {
8-
// Indicates whether the coverage information should be collected while executing the test
9-
collectCoverage: false,
10-
11-
// The directory where Jest should output its coverage files
12-
coverageDirectory: 'coverage',
13-
14-
// Indicates which provider should be used to instrument code for coverage
15-
coverageProvider: 'v8',
16-
17-
// A preset that is used as a base for Jest's configuration
183
preset: 'ts-jest',
19-
20-
// The glob patterns Jest uses to detect test files
21-
testMatch: ['**/src/**/?(*.)+(spec|test).[tj]s?(x)'],
4+
testEnvironment: 'node',
5+
testMatch: ['**/src/**/*.test.ts', '**/test/integration/**/*.test.ts'],
6+
setupFilesAfterEnv: ['<rootDir>/test/credential-guard.setup.ts'],
7+
moduleNameMapper: {
8+
'^uuid$': '<rootDir>/test/uuid-shim.js',
9+
},
10+
collectCoverageFrom: [
11+
'src/**/*.ts',
12+
'!src/**/*.test.ts',
13+
'!src/util/cloud-function/**',
14+
'!src/commands/launch/functions/**',
15+
],
16+
coverageProvider: 'v8',
17+
coverageDirectory: 'coverage',
18+
coverageThreshold: {
19+
global: { statements: 100, branches: 100, functions: 100, lines: 100 },
20+
},
2221
};
2322

2423
module.exports = config;

0 commit comments

Comments
 (0)