directify-cli 1.3.1 → 1.4.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/README.md CHANGED
@@ -116,10 +116,28 @@ directify tags delete 10
116
116
  ### Custom Fields
117
117
 
118
118
  ```bash
119
+ # List / get
119
120
  directify fields list # List all custom fields
120
121
  directify fields ls --json # Output as JSON
122
+ directify fields get 12
123
+
124
+ # Create (label + type required)
125
+ directify fields create \
126
+ --label "Price Range" \
127
+ --type select \
128
+ --options "$,$$,$$$" \
129
+ --filterable \
130
+ --on-card
131
+
132
+ directify fields create --label "Founded Year" --type number
133
+
134
+ # Update / delete
135
+ directify fields update 12 --label "Pricing Tier" --required true
136
+ directify fields delete 12
121
137
  ```
122
138
 
139
+ Field types: `text`, `number`, `date`, `file_upload`, `url`, `email`, `rich_editor`, `markdown`, `textarea`, `checkbox`, `rating`, `select`, `list`, `multi_select`, `button`, `javascript`, `html`. The `--name` (machine key) is auto-derived from `--label` if omitted. Set a field's value on a listing via `listings create/update --field "name=value"`.
140
+
123
141
  ### Listings
124
142
 
125
143
  ```bash
@@ -141,13 +159,15 @@ directify listings create \
141
159
  --email "info@bellatrattoria.com" \
142
160
  --categories 1,5,12 \
143
161
  --tags 3,7 \
162
+ --organizers 5,8 \
144
163
  --featured \
145
164
  --field "price_range=2" \
146
165
  --field "cuisine_type=Italian, Pasta"
147
166
 
148
- # Update
167
+ # Update (--organizers replaces the listing's current organizer links)
149
168
  directify listings update 456 \
150
169
  --name "Bella Trattoria NYC" \
170
+ --organizers 5 \
151
171
  --featured true \
152
172
  --field "hours_of_operation=Mon | 11:00 - 22:00"
153
173
 
@@ -174,6 +194,7 @@ Create a JSON file with an array of listings:
174
194
  "description": "Great food",
175
195
  "categories": [1, 2],
176
196
  "tags": [3],
197
+ "organizers": [5],
177
198
  "price_range": "2",
178
199
  "cuisine_type": "Italian"
179
200
  },
@@ -194,6 +215,27 @@ Then run:
194
215
  directify listings bulk-create --file ./listings.json
195
216
  ```
196
217
 
218
+ > **Linking organizers:** `--organizers` (and the `organizers` array in the bulk JSON) takes a comma-separated list of organizer IDs to associate the listing with. Only organizers in the same directory are linked; out-of-directory IDs are ignored. On `update`, the list replaces the listing's current organizers. Use `directify organizers list` to find the IDs.
219
+
220
+ ### Organizers
221
+
222
+ Organizers (agencies, studios, event hosts, etc.) can be linked to multiple listings.
223
+
224
+ ```bash
225
+ # List / get
226
+ directify organizers list
227
+ directify organizers get 5
228
+
229
+ # Create
230
+ directify organizers create --name "Acme Events" --website "https://acme-events.com"
231
+
232
+ # Update / delete
233
+ directify organizers update 5 --name "Acme Events Co."
234
+ directify organizers delete 5
235
+ ```
236
+
237
+ To link an organizer to a listing, pass its ID via `--organizers` on `listings create`/`update` (see above).
238
+
197
239
  ### Articles
198
240
 
199
241
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directify-cli",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Official CLI tool for Directify - manage your directories, listings, categories, tags, and articles from the command line.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,9 +1,11 @@
1
1
  import { Command } from 'commander';
2
2
  import { api, resolveDirectory } from '../utils/api.js';
3
- import { printTable, printJson, printError } from '../utils/output.js';
3
+ import { printTable, printJson, printSuccess, printError } from '../utils/output.js';
4
4
  import ora from 'ora';
5
5
 
6
- const fields = new Command('fields').description('List custom fields');
6
+ const fields = new Command('fields').description('Manage custom fields');
7
+
8
+ const FIELD_TYPES = 'text, number, date, file_upload, url, email, rich_editor, markdown, textarea, checkbox, rating, select, list, multi_select, button, javascript, html';
7
9
 
8
10
  fields
9
11
  .command('list')
@@ -38,4 +40,140 @@ fields
38
40
  }
39
41
  });
40
42
 
43
+ fields
44
+ .command('get <id>')
45
+ .description('Get a specific custom field')
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}/custom-fields/${id}`);
51
+ printJson(data.data || data);
52
+ } catch (err) {
53
+ printError(err.message);
54
+ process.exit(1);
55
+ }
56
+ });
57
+
58
+ fields
59
+ .command('create')
60
+ .description('Create a new custom field definition')
61
+ .requiredOption('--label <label>', 'Human-readable field label')
62
+ .requiredOption('--type <type>', `Field type (${FIELD_TYPES})`)
63
+ .option('--name <name>', 'Machine name/key (auto-derived from label if omitted)')
64
+ .option('--fieldable-type <type>', 'What the field attaches to: listing, organizer, article (default: listing)')
65
+ .option('--placeholder <text>', 'Input placeholder text')
66
+ .option('--description <text>', 'Help text shown under the field')
67
+ .option('--default <value>', 'Default value')
68
+ .option('--prefix <value>', 'Prefix shown before the value (e.g. $)')
69
+ .option('--suffix <value>', 'Suffix shown after the value (e.g. /mo)')
70
+ .option('--options <values>', 'Options for select/multi_select/list types (comma-separated)')
71
+ .option('--required', 'Mark the field as required')
72
+ .option('--filterable', 'Allow filtering listings by this field')
73
+ .option('--on-card', 'Show the value on listing cards')
74
+ .option('--public', 'Show the field on the public submission form')
75
+ .option('--hidden', 'Hide the field from the listing page')
76
+ .option('--order <n>', 'Sort order', parseInt)
77
+ .option('-d, --directory <id>', 'Directory ID')
78
+ .action(async (opts) => {
79
+ const spinner = ora('Creating custom field...').start();
80
+ try {
81
+ const dir = resolveDirectory(opts);
82
+ const body = { label: opts.label, type: opts.type };
83
+ if (opts.name) body.name = opts.name;
84
+ if (opts.fieldableType) body.fieldable_type = opts.fieldableType;
85
+ if (opts.placeholder) body.placeholder = opts.placeholder;
86
+ if (opts.description) body.description = opts.description;
87
+ if (opts.default) body.default_value = opts.default;
88
+ if (opts.prefix) body.value_prefix = opts.prefix;
89
+ if (opts.suffix) body.value_suffix = opts.suffix;
90
+ if (opts.options) body.options = opts.options.split(',').map((o) => o.trim());
91
+ if (opts.required) body.is_required = true;
92
+ if (opts.filterable) body.filterable = true;
93
+ if (opts.onCard) body.show_on_card = true;
94
+ if (opts.public) body.show_on_public_submission = true;
95
+ if (opts.hidden) body.is_visible = false;
96
+ if (opts.order !== undefined) body.order = opts.order;
97
+
98
+ const data = await api.post(`/directories/${dir}/custom-fields`, body);
99
+ spinner.stop();
100
+ const result = data.data || data;
101
+ printSuccess(`Custom field created: ${result.label} (${result.name}, ID: ${result.id})`);
102
+ } catch (err) {
103
+ spinner.stop();
104
+ printError(err.message);
105
+ process.exit(1);
106
+ }
107
+ });
108
+
109
+ fields
110
+ .command('update <id>')
111
+ .description('Update a custom field definition')
112
+ .option('--label <label>', 'Human-readable field label')
113
+ .option('--type <type>', `Field type (${FIELD_TYPES})`)
114
+ .option('--name <name>', 'Machine name/key')
115
+ .option('--fieldable-type <type>', 'What the field attaches to: listing, organizer, article')
116
+ .option('--placeholder <text>', 'Input placeholder text')
117
+ .option('--description <text>', 'Help text shown under the field')
118
+ .option('--default <value>', 'Default value')
119
+ .option('--prefix <value>', 'Prefix shown before the value')
120
+ .option('--suffix <value>', 'Suffix shown after the value')
121
+ .option('--options <values>', 'Options for select/multi_select/list types (comma-separated)')
122
+ .option('--required <bool>', 'Required status (true/false)')
123
+ .option('--filterable <bool>', 'Filterable status (true/false)')
124
+ .option('--on-card <bool>', 'Show on cards (true/false)')
125
+ .option('--public <bool>', 'Show on public submission form (true/false)')
126
+ .option('--visible <bool>', 'Visible on the listing page (true/false)')
127
+ .option('--order <n>', 'Sort order', parseInt)
128
+ .option('-d, --directory <id>', 'Directory ID')
129
+ .action(async (id, opts) => {
130
+ const spinner = ora('Updating custom field...').start();
131
+ try {
132
+ const dir = resolveDirectory(opts);
133
+ const body = {};
134
+ if (opts.label) body.label = opts.label;
135
+ if (opts.type) body.type = opts.type;
136
+ if (opts.name) body.name = opts.name;
137
+ if (opts.fieldableType) body.fieldable_type = opts.fieldableType;
138
+ if (opts.placeholder) body.placeholder = opts.placeholder;
139
+ if (opts.description) body.description = opts.description;
140
+ if (opts.default) body.default_value = opts.default;
141
+ if (opts.prefix) body.value_prefix = opts.prefix;
142
+ if (opts.suffix) body.value_suffix = opts.suffix;
143
+ if (opts.options) body.options = opts.options.split(',').map((o) => o.trim());
144
+ if (opts.required !== undefined) body.is_required = opts.required === 'true';
145
+ if (opts.filterable !== undefined) body.filterable = opts.filterable === 'true';
146
+ if (opts.onCard !== undefined) body.show_on_card = opts.onCard === 'true';
147
+ if (opts.public !== undefined) body.show_on_public_submission = opts.public === 'true';
148
+ if (opts.visible !== undefined) body.is_visible = opts.visible === 'true';
149
+ if (opts.order !== undefined) body.order = opts.order;
150
+
151
+ const data = await api.put(`/directories/${dir}/custom-fields/${id}`, body);
152
+ spinner.stop();
153
+ printSuccess(`Custom field updated: ${data.data?.label || data.label}`);
154
+ } catch (err) {
155
+ spinner.stop();
156
+ printError(err.message);
157
+ process.exit(1);
158
+ }
159
+ });
160
+
161
+ fields
162
+ .command('delete <id>')
163
+ .description('Delete a custom field definition (also removes its values from all listings)')
164
+ .option('-d, --directory <id>', 'Directory ID')
165
+ .action(async (id, opts) => {
166
+ const spinner = ora('Deleting custom field...').start();
167
+ try {
168
+ const dir = resolveDirectory(opts);
169
+ await api.delete(`/directories/${dir}/custom-fields/${id}`);
170
+ spinner.stop();
171
+ printSuccess(`Custom field ${id} deleted.`);
172
+ } catch (err) {
173
+ spinner.stop();
174
+ printError(err.message);
175
+ process.exit(1);
176
+ }
177
+ });
178
+
41
179
  export default fields;
@@ -73,6 +73,7 @@ listings
73
73
  .option('--lng <n>', 'Longitude', parseFloat)
74
74
  .option('--categories <ids>', 'Category IDs (comma-separated)')
75
75
  .option('--tags <ids>', 'Tag IDs (comma-separated)')
76
+ .option('--organizers <ids>', 'Organizer IDs to link (comma-separated)')
76
77
  .option('--featured', 'Mark as featured')
77
78
  .option('--inactive', 'Create as inactive')
78
79
  .option('--field <key=value...>', 'Custom field values (repeatable)', collect, [])
@@ -95,6 +96,7 @@ listings
95
96
  if (opts.lng) body.longitude = opts.lng;
96
97
  if (opts.categories) body.categories = opts.categories.split(',').map(Number);
97
98
  if (opts.tags) body.tags = opts.tags.split(',').map(Number);
99
+ if (opts.organizers) body.organizers = opts.organizers.split(',').map(Number);
98
100
  if (opts.featured) body.is_featured = true;
99
101
  if (opts.inactive) body.is_active = false;
100
102
 
@@ -132,6 +134,7 @@ listings
132
134
  .option('--lng <n>', 'Longitude', parseFloat)
133
135
  .option('--categories <ids>', 'Category IDs (comma-separated)')
134
136
  .option('--tags <ids>', 'Tag IDs (comma-separated)')
137
+ .option('--organizers <ids>', 'Organizer IDs to link, replaces current set (comma-separated; out-of-directory IDs are ignored)')
135
138
  .option('--featured <bool>', 'Featured status (true/false)')
136
139
  .option('--active <bool>', 'Active status (true/false)')
137
140
  .option('--field <key=value...>', 'Custom field values (repeatable)', collect, [])
@@ -155,6 +158,7 @@ listings
155
158
  if (opts.lng) body.longitude = opts.lng;
156
159
  if (opts.categories) body.categories = opts.categories.split(',').map(Number);
157
160
  if (opts.tags) body.tags = opts.tags.split(',').map(Number);
161
+ if (opts.organizers) body.organizers = opts.organizers.split(',').map(Number);
158
162
  if (opts.featured !== undefined) body.is_featured = opts.featured === 'true';
159
163
  if (opts.active !== undefined) body.is_active = opts.active === 'true';
160
164