Skip to content

Commit b8f8580

Browse files
committed
Create CLAUDE.md
1 parent 6d405d4 commit b8f8580

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed

CLAUDE.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# gctools - Ghost Content Tools
2+
3+
CLI utilities for working with Ghost CMS content via the Admin API.
4+
5+
**Use `yarn` (not npm) for running scripts and installing dependencies.**
6+
7+
## Architecture Overview
8+
9+
```
10+
gctools/
11+
├── bin/cli.js # Entry point - registers all commands with prettyCLI
12+
├── commands/ # CLI command definitions (flags, options, run function)
13+
├── tasks/ # Core business logic (API calls, data processing)
14+
├── prompts/ # Interactive mode definitions (inquirer prompts)
15+
├── lib/ # Shared utilities
16+
└── test/ # Jest tests
17+
```
18+
19+
## Adding a New Feature
20+
21+
To add a new command, create/modify these files:
22+
23+
### 1. `tasks/<feature>.js` - Business Logic
24+
25+
```javascript
26+
import GhostAdminAPI from '@tryghost/admin-api';
27+
import {makeTaskRunner} from '@tryghost/listr-smart-renderer';
28+
import {discover} from '../lib/batch-ghost-discover.js';
29+
30+
const initialise = (options) => ({
31+
title: 'Initialising API connection',
32+
task: (ctx, task) => {
33+
ctx.api = new GhostAdminAPI({
34+
url: options.apiURL.replace('localhost', '127.0.0.1'),
35+
key: options.adminAPIKey,
36+
version: 'v5.0'
37+
});
38+
ctx.updated = [];
39+
}
40+
});
41+
42+
const getFullTaskList = (options) => [
43+
initialise(options),
44+
{
45+
title: 'Fetch content',
46+
task: async (ctx, task) => {
47+
ctx.posts = await discover({api: ctx.api, type: 'posts', ...});
48+
}
49+
},
50+
{
51+
title: 'Process content',
52+
task: async (ctx) => {
53+
// Return nested task runner for per-item progress
54+
return makeTaskRunner(tasks, {concurrent: 1});
55+
}
56+
}
57+
];
58+
59+
const getTaskRunner = (options) => {
60+
return makeTaskRunner(getFullTaskList(options), {topLevel: true, ...options});
61+
};
62+
63+
export default { initialise, getFullTaskList, getTaskRunner };
64+
```
65+
66+
### 2. `commands/<feature>.js` - CLI Definition
67+
68+
```javascript
69+
import {ui} from '@tryghost/pretty-cli';
70+
import featureTask from '../tasks/<feature>.js';
71+
72+
const id = 'feature-name';
73+
const group = 'Content:'; // or 'Members:', 'Tools:', 'Beta:'
74+
const flags = 'feature-name <apiURL> <adminAPIKey>';
75+
const desc = 'Description of what this does';
76+
const paramsDesc = ['URL to your Ghost API', 'Admin API key'];
77+
78+
const setup = (sywac) => {
79+
sywac.boolean('-V --verbose', {defaultValue: false, desc: 'Show verbose output'});
80+
sywac.string('--option', {defaultValue: null, desc: 'Option description'});
81+
sywac.enumeration('--choice', {defaultValue: 'all', choices: ['all', 'a', 'b']});
82+
};
83+
84+
const run = async (argv) => {
85+
let context = {errors: []};
86+
let runner = featureTask.getTaskRunner(argv);
87+
await runner.run(context);
88+
ui.log.ok(`Successfully updated ${context.updated.length} items.`);
89+
};
90+
91+
export default { id, group, flags, desc, paramsDesc, setup, run };
92+
```
93+
94+
### 3. `prompts/<feature>.js` - Interactive Mode
95+
96+
```javascript
97+
import inquirer from 'inquirer';
98+
import inquirerSearchCheckbox from 'inquirer-search-checkbox';
99+
inquirer.registerPrompt('search-checkbox', inquirerSearchCheckbox);
100+
import inquirerDatepickerPrompt from 'inquirer-datepicker-prompt';
101+
inquirer.registerPrompt('datetime', inquirerDatepickerPrompt);
102+
import {ui} from '@tryghost/pretty-cli';
103+
import featureTask from '../tasks/<feature>.js';
104+
import ghostAPICreds from '../lib/ghost-api-creds.js';
105+
import {getAPITagsObj, getAPIAuthorsObj} from '../lib/ghost-api-choices.js';
106+
107+
const choice = {
108+
name: 'Menu item name',
109+
value: 'featureName' // Used to identify in interactive.js
110+
};
111+
112+
const options = [
113+
...ghostAPICreds, // API URL and key prompts
114+
// Add feature-specific prompts
115+
];
116+
117+
async function run() {
118+
await inquirer.prompt(options).then(async (answers) => {
119+
let context = {errors: []};
120+
let runner = featureTask.getTaskRunner(answers);
121+
await runner.run(context);
122+
ui.log.ok(`Done.`);
123+
});
124+
}
125+
126+
export default { choice, options, run };
127+
```
128+
129+
### 4. Register the Feature
130+
131+
**`prompts/index.js`** - Add import and export:
132+
```javascript
133+
import featureName from './feature-name.js';
134+
// In export default: featureName,
135+
```
136+
137+
**`commands/interactive.js`** - Add to menu (find appropriate section):
138+
```javascript
139+
{
140+
name: tasks.featureName.choice.name,
141+
value: tasks.featureName.choice.value
142+
},
143+
```
144+
145+
**`bin/cli.js`** - Register CLI command:
146+
```javascript
147+
import featureName from '../commands/feature-name.js';
148+
// ...
149+
prettyCLI.command(featureName);
150+
```
151+
152+
## Key Libraries
153+
154+
- `@tryghost/admin-api` - Ghost Admin API client
155+
- `@tryghost/listr-smart-renderer` - Task runner with progress display (`makeTaskRunner`)
156+
- `@tryghost/pretty-cli` - CLI framework (wraps sywac)
157+
- `inquirer` - Interactive prompts
158+
159+
**Note:** Use native promises and `async/await`, not Bluebird. Use `sleep()` from `lib/utils.js` for delays.
160+
161+
## Common Patterns
162+
163+
### Filtering Posts
164+
```javascript
165+
let filter = [];
166+
if (options.status !== 'all') filter.push(`status:[${options.status}]`);
167+
if (options.visibility !== 'all') filter.push(`visibility:[${options.visibility}]`);
168+
if (options.tag) filter.push(`tags:[${transformToCommaString(options.tag, 'slug')}]`);
169+
if (options.author) filter.push(`author:[${transformToCommaString(options.author, 'slug')}]`);
170+
// Join with '+' for AND logic
171+
discover({...options, filter: filter.join('+')});
172+
```
173+
174+
### API Edit Call
175+
```javascript
176+
await ctx.api.posts.edit({
177+
id: post.id,
178+
updated_at: post.updated_at, // Required for optimistic locking
179+
// ... fields to update
180+
});
181+
```
182+
183+
### Processing Items with Progress
184+
```javascript
185+
import {sleep} from '../lib/utils.js';
186+
187+
for (const post of ctx.posts) {
188+
tasks.push({
189+
title: post.title,
190+
task: async () => {
191+
// Process post
192+
await sleep(options.delayBetweenCalls);
193+
}
194+
});
195+
}
196+
return makeTaskRunner(tasks, {concurrent: 1});
197+
```
198+
199+
## Utility Functions (`lib/utils.js`)
200+
201+
- `transformToCommaString(array, key)` - Convert `[{slug: 'a'}, {slug: 'b'}]` to `'a,b'`
202+
- `maybeStringToArray(input)` - Convert `'a, b, c'` to `['a', 'b', 'c']`
203+
- `sleep(ms)` - Promise-based delay
204+
205+
## Testing
206+
207+
```bash
208+
yarn test # Runs Jest + ESLint
209+
```
210+
211+
Tests are in `test/` directory. Mock the Ghost API client for unit tests.
212+
213+
## Command Groups
214+
215+
- `Interactive:` - The `i` command for menu-driven mode
216+
- `Tools:` - File utilities (zip, json splitting)
217+
- `Content:` - Post/page API operations
218+
- `Members:` - Member API operations
219+
- `Beta:` - Experimental features

0 commit comments

Comments
 (0)