directify-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/README.md ADDED
@@ -0,0 +1,297 @@
1
+ # Directify CLI
2
+
3
+ Official command-line tool for [Directify](https://directify.app) - manage your directory websites, listings, categories, tags, and articles from the terminal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g directify-cli
9
+ ```
10
+
11
+ Or run directly with npx:
12
+
13
+ ```bash
14
+ npx directify-cli --help
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### 1. Get your API token
20
+
21
+ Go to your Directify dashboard: **Settings > API** and generate a new token.
22
+
23
+ ### 2. Authenticate
24
+
25
+ ```bash
26
+ directify auth login YOUR_API_TOKEN
27
+ ```
28
+
29
+ ### 3. Set your default directory
30
+
31
+ ```bash
32
+ # List your directories to find the ID
33
+ directify directories list
34
+
35
+ # Set the default so you don't need --directory on every command
36
+ directify config set-directory 123
37
+ ```
38
+
39
+ ### 4. Start managing
40
+
41
+ ```bash
42
+ # List listings
43
+ directify listings list
44
+
45
+ # Create a listing
46
+ directify listings create --name "Bella Trattoria" --description "Authentic Italian cuisine" --address "123 Main St"
47
+
48
+ # Create a category
49
+ directify categories create --title "Italian" --icon "🍝"
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ ### Authentication
55
+
56
+ ```bash
57
+ directify auth login <token> # Authenticate with your API token
58
+ directify auth logout # Remove stored token
59
+ directify auth status # Check authentication status
60
+ ```
61
+
62
+ ### Configuration
63
+
64
+ ```bash
65
+ directify config set-directory <id> # Set default directory
66
+ directify config get-directory # Show default directory
67
+ ```
68
+
69
+ ### Directories
70
+
71
+ ```bash
72
+ directify directories list # List all your directories
73
+ directify dirs ls # Short alias
74
+ directify dirs ls --json # Output as JSON
75
+ ```
76
+
77
+ ### Categories
78
+
79
+ ```bash
80
+ # List
81
+ directify categories list
82
+ directify cats ls --json
83
+
84
+ # Create
85
+ directify categories create --title "Italian" --icon "🍝" --description "Italian restaurants"
86
+ directify categories create --title "Japanese" --parent-id 5 --order 2
87
+
88
+ # Update
89
+ directify categories update 42 --title "Italian Cuisine" --order 1
90
+
91
+ # Delete
92
+ directify categories delete 42
93
+
94
+ # Get details
95
+ directify categories get 42
96
+ ```
97
+
98
+ ### Tags
99
+
100
+ ```bash
101
+ # List
102
+ directify tags list
103
+ directify tags ls --json
104
+
105
+ # Create
106
+ directify tags create --title "Featured" --color "#f59e0b" --text-color "#ffffff"
107
+ directify tags create --title "New" --heroicon "heroicon-o-sparkles"
108
+
109
+ # Update
110
+ directify tags update 10 --title "Hot" --color "#ef4444"
111
+
112
+ # Delete
113
+ directify tags delete 10
114
+ ```
115
+
116
+ ### Custom Fields
117
+
118
+ ```bash
119
+ directify fields list # List all custom fields
120
+ directify fields ls --json # Output as JSON
121
+ ```
122
+
123
+ ### Listings
124
+
125
+ ```bash
126
+ # List (paginated)
127
+ directify listings list
128
+ directify listings list --page 2
129
+ directify listings ls --json
130
+
131
+ # Get a specific listing
132
+ directify listings get 456
133
+
134
+ # Create
135
+ directify listings create \
136
+ --name "Bella Trattoria" \
137
+ --url "https://bellatrattoria.com" \
138
+ --description "Authentic Italian cuisine" \
139
+ --address "123 Main Street, New York" \
140
+ --phone "+1-212-555-1234" \
141
+ --email "info@bellatrattoria.com" \
142
+ --categories 1,5,12 \
143
+ --tags 3,7 \
144
+ --featured \
145
+ --field "price_range=2" \
146
+ --field "cuisine_type=Italian, Pasta"
147
+
148
+ # Update
149
+ directify listings update 456 \
150
+ --name "Bella Trattoria NYC" \
151
+ --featured true \
152
+ --field "hours_of_operation=Mon | 11:00 - 22:00"
153
+
154
+ # Delete
155
+ directify listings delete 456
156
+
157
+ # Check if URL exists
158
+ directify listings exists --url "https://example.com"
159
+
160
+ # Bulk create from JSON file
161
+ directify listings bulk-create --file ./listings.json
162
+ ```
163
+
164
+ #### Bulk Create JSON Format
165
+
166
+ Create a JSON file with an array of listings:
167
+
168
+ ```json
169
+ {
170
+ "listings": [
171
+ {
172
+ "name": "Restaurant One",
173
+ "url": "https://restaurant-one.com",
174
+ "description": "Great food",
175
+ "categories": [1, 2],
176
+ "tags": [3],
177
+ "price_range": "2",
178
+ "cuisine_type": "Italian"
179
+ },
180
+ {
181
+ "name": "Restaurant Two",
182
+ "url": "https://restaurant-two.com",
183
+ "description": "Amazing sushi",
184
+ "categories": [3],
185
+ "cuisine_type": "Japanese"
186
+ }
187
+ ]
188
+ }
189
+ ```
190
+
191
+ Then run:
192
+
193
+ ```bash
194
+ directify listings bulk-create --file ./listings.json
195
+ ```
196
+
197
+ ### Articles
198
+
199
+ ```bash
200
+ # List
201
+ directify articles list
202
+ directify articles list --page 2 --json
203
+
204
+ # Get
205
+ directify articles get 789
206
+
207
+ # Create
208
+ directify articles create \
209
+ --title "Best Italian Restaurants in NYC" \
210
+ --markdown "# Top Picks\n\nHere are our favorites..." \
211
+ --categories "Reviews,Italian" \
212
+ --thumbnail-url "https://example.com/image.jpg" \
213
+ --seo-title "Best Italian Restaurants" \
214
+ --seo-description "Discover the top Italian restaurants in New York City"
215
+
216
+ # Update
217
+ directify articles update 789 --title "Updated Title" --active true
218
+
219
+ # Toggle active/inactive
220
+ directify articles toggle 789
221
+
222
+ # Delete
223
+ directify articles delete 789
224
+
225
+ # Check if slug exists
226
+ directify articles exists --slug "best-italian-restaurants"
227
+ ```
228
+
229
+ ## Global Options
230
+
231
+ All resource commands support these options:
232
+
233
+ | Option | Description |
234
+ |--------|-------------|
235
+ | `-d, --directory <id>` | Directory ID (overrides default) |
236
+ | `--json` | Output as JSON (on list commands) |
237
+ | `--help` | Show help for a command |
238
+
239
+ ## Custom Fields
240
+
241
+ When creating or updating listings, you can set custom field values using the `--field` flag:
242
+
243
+ ```bash
244
+ directify listings create \
245
+ --name "My Restaurant" \
246
+ --field "price_range=3" \
247
+ --field "cuisine_type=Italian, Pizza" \
248
+ --field "hours_of_operation=Mon | 11:00 - 22:00
249
+ Tue | 11:00 - 22:00
250
+ Wed | Closed" \
251
+ --field "menu_highlights=Carbonara | Classic Roman pasta | \$22
252
+ Margherita | Fresh mozzarella | \$18"
253
+ ```
254
+
255
+ Use `directify fields list` to see available custom fields and their names.
256
+
257
+ ## Using with LLMs / AI Agents
258
+
259
+ The CLI is designed to work well with AI-powered workflows. Use `--json` output for machine-readable responses:
260
+
261
+ ```bash
262
+ # Get all listings as JSON for processing
263
+ directify listings list --json | jq '.data[].name'
264
+
265
+ # Pipe data between commands
266
+ directify listings get 456 --json | jq '.categories'
267
+ ```
268
+
269
+ ## Rate Limits
270
+
271
+ The API allows **120 requests per minute** per directory. If you hit the rate limit, the CLI will show an error message. Implement delays between requests for bulk operations.
272
+
273
+ ## Configuration Storage
274
+
275
+ The CLI stores your auth token and default directory in your system's config directory:
276
+
277
+ - **macOS**: `~/Library/Preferences/directify-cli-nodejs/`
278
+ - **Linux**: `~/.config/directify-cli-nodejs/`
279
+ - **Windows**: `%APPDATA%/directify-cli-nodejs/`
280
+
281
+ ## Troubleshooting
282
+
283
+ ### "Not authenticated" error
284
+ Run `directify auth login <token>` with your API token from Settings > API.
285
+
286
+ ### "No directory specified" error
287
+ Either pass `--directory <id>` or set a default: `directify config set-directory <id>`
288
+
289
+ ### "Rate limit exceeded" error
290
+ Wait a moment and retry. The limit is 120 requests per minute per directory.
291
+
292
+ ### "Validation error"
293
+ Check the error details for which fields failed validation. Use `directify fields list` to see available custom field names.
294
+
295
+ ## License
296
+
297
+ MIT
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "directify-cli",
3
+ "version": "1.0.0",
4
+ "description": "Official CLI tool for Directify - manage your directories, listings, categories, tags, and articles from the command line.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "directify": "./src/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "lint": "eslint src/"
13
+ },
14
+ "keywords": [
15
+ "directify",
16
+ "directory",
17
+ "cli",
18
+ "listings",
19
+ "api"
20
+ ],
21
+ "author": "Directify",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "chalk": "^5.3.0",
25
+ "commander": "^12.1.0",
26
+ "conf": "^13.0.1",
27
+ "ora": "^8.1.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ }
32
+ }
@@ -0,0 +1,193 @@
1
+ import { Command } from 'commander';
2
+ import { api, resolveDirectory } from '../utils/api.js';
3
+ import { printTable, printJson, printSuccess, printError } from '../utils/output.js';
4
+ import ora from 'ora';
5
+
6
+ const articles = new Command('articles').description('Manage blog articles');
7
+
8
+ articles
9
+ .command('list')
10
+ .alias('ls')
11
+ .description('List all articles')
12
+ .option('-d, --directory <id>', 'Directory ID')
13
+ .option('--page <n>', 'Page number', '1')
14
+ .option('--json', 'Output as JSON')
15
+ .action(async (opts) => {
16
+ const spinner = ora('Fetching articles...').start();
17
+ try {
18
+ const dir = resolveDirectory(opts);
19
+ const data = await api.get(`/directories/${dir}/articles?page=${opts.page}`);
20
+ spinner.stop();
21
+ if (opts.json) {
22
+ printJson(data);
23
+ } else {
24
+ const items = data.data || data;
25
+ printTable(items, [
26
+ { key: 'id', label: 'ID' },
27
+ { key: 'title', label: 'Title', maxWidth: 40 },
28
+ { key: 'slug', label: 'Slug', maxWidth: 25 },
29
+ { key: 'active', label: 'Active' },
30
+ { key: 'published_at', label: 'Published' },
31
+ ]);
32
+ if (data.meta) {
33
+ console.log(`\nPage ${data.meta.current_page} of ${data.meta.last_page} (${data.meta.total} total)`);
34
+ }
35
+ }
36
+ } catch (err) {
37
+ spinner.stop();
38
+ printError(err.message);
39
+ process.exit(1);
40
+ }
41
+ });
42
+
43
+ articles
44
+ .command('get <id>')
45
+ .description('Get a specific article')
46
+ .option('-d, --directory <id>', 'Directory ID')
47
+ .action(async (id, opts) => {
48
+ try {
49
+ const dir = resolveDirectory(opts);
50
+ const data = await api.get(`/directories/${dir}/articles/${id}`);
51
+ printJson(data.data || data);
52
+ } catch (err) {
53
+ printError(err.message);
54
+ process.exit(1);
55
+ }
56
+ });
57
+
58
+ articles
59
+ .command('create')
60
+ .description('Create a new article')
61
+ .requiredOption('--title <title>', 'Article title')
62
+ .option('--slug <slug>', 'URL slug (auto-generated from title if not set)')
63
+ .option('--content <text>', 'HTML content')
64
+ .option('--markdown <text>', 'Markdown content')
65
+ .option('--thumbnail-url <url>', 'Thumbnail image URL')
66
+ .option('--categories <names>', 'Category names (comma-separated)')
67
+ .option('--seo-title <text>', 'SEO title')
68
+ .option('--seo-description <text>', 'SEO description')
69
+ .option('--inactive', 'Create as inactive')
70
+ .option('-d, --directory <id>', 'Directory ID')
71
+ .action(async (opts) => {
72
+ const spinner = ora('Creating article...').start();
73
+ try {
74
+ const dir = resolveDirectory(opts);
75
+ const body = { title: opts.title };
76
+ if (opts.slug) body.slug = opts.slug;
77
+ if (opts.content) body.content = opts.content;
78
+ if (opts.markdown) body.markdown = opts.markdown;
79
+ if (opts.thumbnailUrl) body.thumbnail_url = opts.thumbnailUrl;
80
+ if (opts.categories) body.categories = opts.categories.split(',').map((s) => s.trim());
81
+ if (opts.seoTitle || opts.seoDescription) {
82
+ body.seo = {};
83
+ if (opts.seoTitle) body.seo.title = opts.seoTitle;
84
+ if (opts.seoDescription) body.seo.description = opts.seoDescription;
85
+ }
86
+ body.active = !opts.inactive;
87
+
88
+ const data = await api.post(`/directories/${dir}/articles`, body);
89
+ spinner.stop();
90
+ const result = data.data || data;
91
+ printSuccess(`Article created: ${result.title} (ID: ${result.id})`);
92
+ } catch (err) {
93
+ spinner.stop();
94
+ printError(err.message);
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ articles
100
+ .command('update <id>')
101
+ .description('Update an article')
102
+ .option('--title <title>', 'Article title')
103
+ .option('--slug <slug>', 'URL slug')
104
+ .option('--content <text>', 'HTML content')
105
+ .option('--markdown <text>', 'Markdown content')
106
+ .option('--thumbnail-url <url>', 'Thumbnail image URL')
107
+ .option('--categories <names>', 'Category names (comma-separated)')
108
+ .option('--seo-title <text>', 'SEO title')
109
+ .option('--seo-description <text>', 'SEO description')
110
+ .option('--active <bool>', 'Active status (true/false)')
111
+ .option('-d, --directory <id>', 'Directory ID')
112
+ .action(async (id, opts) => {
113
+ const spinner = ora('Updating article...').start();
114
+ try {
115
+ const dir = resolveDirectory(opts);
116
+ const body = {};
117
+ if (opts.title) body.title = opts.title;
118
+ if (opts.slug) body.slug = opts.slug;
119
+ if (opts.content) body.content = opts.content;
120
+ if (opts.markdown) body.markdown = opts.markdown;
121
+ if (opts.thumbnailUrl) body.thumbnail_url = opts.thumbnailUrl;
122
+ if (opts.categories) body.categories = opts.categories.split(',').map((s) => s.trim());
123
+ if (opts.active !== undefined) body.active = opts.active === 'true';
124
+ if (opts.seoTitle || opts.seoDescription) {
125
+ body.seo = {};
126
+ if (opts.seoTitle) body.seo.title = opts.seoTitle;
127
+ if (opts.seoDescription) body.seo.description = opts.seoDescription;
128
+ }
129
+
130
+ const data = await api.put(`/directories/${dir}/articles/${id}`, body);
131
+ spinner.stop();
132
+ printSuccess(`Article updated: ${data.data?.title || data.title}`);
133
+ } catch (err) {
134
+ spinner.stop();
135
+ printError(err.message);
136
+ process.exit(1);
137
+ }
138
+ });
139
+
140
+ articles
141
+ .command('delete <id>')
142
+ .description('Delete an article')
143
+ .option('-d, --directory <id>', 'Directory ID')
144
+ .action(async (id, opts) => {
145
+ const spinner = ora('Deleting article...').start();
146
+ try {
147
+ const dir = resolveDirectory(opts);
148
+ await api.delete(`/directories/${dir}/articles/${id}`);
149
+ spinner.stop();
150
+ printSuccess(`Article ${id} deleted.`);
151
+ } catch (err) {
152
+ spinner.stop();
153
+ printError(err.message);
154
+ process.exit(1);
155
+ }
156
+ });
157
+
158
+ articles
159
+ .command('toggle <id>')
160
+ .description('Toggle article active/inactive status')
161
+ .option('-d, --directory <id>', 'Directory ID')
162
+ .action(async (id, opts) => {
163
+ const spinner = ora('Toggling article...').start();
164
+ try {
165
+ const dir = resolveDirectory(opts);
166
+ const data = await api.patch(`/directories/${dir}/articles/${id}/toggle`);
167
+ spinner.stop();
168
+ const result = data.data || data;
169
+ printSuccess(`Article "${result.title}" is now ${result.active ? 'active' : 'inactive'}.`);
170
+ } catch (err) {
171
+ spinner.stop();
172
+ printError(err.message);
173
+ process.exit(1);
174
+ }
175
+ });
176
+
177
+ articles
178
+ .command('exists')
179
+ .description('Check if an article with a given slug exists')
180
+ .requiredOption('--slug <slug>', 'Slug to check')
181
+ .option('-d, --directory <id>', 'Directory ID')
182
+ .action(async (opts) => {
183
+ try {
184
+ const dir = resolveDirectory(opts);
185
+ const data = await api.post(`/directories/${dir}/articles/exists`, { slug: opts.slug });
186
+ printSuccess(data.message);
187
+ } catch (err) {
188
+ printError(err.message);
189
+ process.exit(1);
190
+ }
191
+ });
192
+
193
+ export default articles;
@@ -0,0 +1,54 @@
1
+ import { Command } from 'commander';
2
+ import { setToken, clearToken, getToken, api } from '../utils/api.js';
3
+ import { printSuccess, printError, printInfo } from '../utils/output.js';
4
+ import ora from 'ora';
5
+
6
+ const auth = new Command('auth').description('Manage authentication');
7
+
8
+ auth
9
+ .command('login <token>')
10
+ .description('Authenticate with your Directify API token')
11
+ .action(async (token) => {
12
+ const spinner = ora('Verifying token...').start();
13
+ try {
14
+ setToken(token);
15
+ const data = await api.get('/user');
16
+ spinner.stop();
17
+ printSuccess(`Authenticated as ${data.name} (${data.email})`);
18
+ } catch (err) {
19
+ clearToken();
20
+ spinner.stop();
21
+ printError(`Authentication failed: ${err.message}`);
22
+ process.exit(1);
23
+ }
24
+ });
25
+
26
+ auth
27
+ .command('logout')
28
+ .description('Remove stored authentication token')
29
+ .action(() => {
30
+ clearToken();
31
+ printSuccess('Logged out successfully.');
32
+ });
33
+
34
+ auth
35
+ .command('status')
36
+ .description('Check current authentication status')
37
+ .action(async () => {
38
+ const token = getToken();
39
+ if (!token) {
40
+ printInfo('Not authenticated. Run: directify auth login <token>');
41
+ return;
42
+ }
43
+ const spinner = ora('Checking...').start();
44
+ try {
45
+ const data = await api.get('/user');
46
+ spinner.stop();
47
+ printSuccess(`Authenticated as ${data.name} (${data.email})`);
48
+ } catch (err) {
49
+ spinner.stop();
50
+ printError(`Token invalid: ${err.message}`);
51
+ }
52
+ });
53
+
54
+ export default auth;