@contextium/cli 1.7.1 → 1.8.1

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.
@@ -15,105 +15,12 @@ async function resolveWorkspace(client, workspaceName) {
15
15
  return workspace;
16
16
  }
17
17
  export const tagsCommand = new Command('tags')
18
- .description('Manage tags and tag types');
19
- // List tag types
20
- tagsCommand
21
- .command('types')
22
- .description('List tag types in a workspace')
23
- .option('-w, --workspace <id>', 'Workspace name, slug, or ID')
24
- .action(async (options) => {
25
- const spinner = ora('Loading tag types...').start();
26
- try {
27
- const config = await loadConfig();
28
- const client = new ApiClient(config.api_key, config.api_url);
29
- if (!options.workspace) {
30
- spinner.fail('Error: Workspace is required. Use -w flag.');
31
- process.exit(1);
32
- }
33
- const workspace = await resolveWorkspace(client, options.workspace);
34
- const response = await client.get(`/workspaces/${workspace.id}/tag-types`);
35
- const tagTypesData = response.data;
36
- const tagTypes = tagTypesData.data || tagTypesData || [];
37
- if (tagTypes.length === 0) {
38
- spinner.stop();
39
- console.log(chalk.yellow('No tag types found.'));
40
- return;
41
- }
42
- spinner.stop();
43
- console.log(chalk.bold(`\n${tagTypes.length} tag type(s):\n`));
44
- tagTypes.forEach((type) => {
45
- console.log(chalk.cyan(`${type.name} (${type.slug})`));
46
- console.log(` ID: ${type.id}`);
47
- console.log(` Color: ${type.color}`);
48
- if (type.icon)
49
- console.log(` Icon: ${type.icon}`);
50
- if (type.description)
51
- console.log(` Description: ${type.description}`);
52
- console.log(` System: ${type.isSystem ? 'Yes' : 'No'}`);
53
- console.log(` Display Order: ${type.displayOrder}`);
54
- if (type.tagCount !== undefined)
55
- console.log(` Tag Count: ${type.tagCount}`);
56
- console.log('');
57
- });
58
- }
59
- catch (error) {
60
- spinner.fail(`Failed to load tag types: ${error.message}`);
61
- process.exit(1);
62
- }
63
- });
64
- // Create tag type
65
- tagsCommand
66
- .command('create-type')
67
- .description('Create a new tag type')
68
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
69
- .requiredOption('-n, --name <name>', 'Type name')
70
- .option('-s, --slug <slug>', 'Type slug (auto-generated from name if not provided)')
71
- .option('-c, --color <hex>', 'Hex color code')
72
- .option('-i, --icon <icon>', 'Icon name')
73
- .option('-d, --description <text>', 'Description')
74
- .action(async (options) => {
75
- const spinner = ora('Creating tag type...').start();
76
- try {
77
- const config = await loadConfig();
78
- const client = new ApiClient(config.api_key, config.api_url);
79
- const workspace = await resolveWorkspace(client, options.workspace);
80
- const slug = options.slug || options.name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
81
- // Validate slug format
82
- if (!/^[a-z0-9-]+$/.test(slug)) {
83
- spinner.fail('Error: Slug must contain only lowercase letters, numbers, and hyphens');
84
- process.exit(1);
85
- }
86
- const data = {
87
- name: options.name,
88
- slug,
89
- };
90
- if (options.color)
91
- data.color = options.color;
92
- if (options.icon)
93
- data.icon = options.icon;
94
- if (options.description)
95
- data.description = options.description;
96
- const response = await client.post(`/workspaces/${workspace.id}/tag-types`, data);
97
- const tagTypeData = response.data;
98
- const tagType = tagTypeData.data || tagTypeData;
99
- spinner.stop();
100
- console.log(chalk.green('\n✓ Tag type created successfully!'));
101
- console.log(` ID: ${tagType.id}`);
102
- console.log(` Name: ${tagType.name}`);
103
- console.log(` Slug: ${tagType.slug}`);
104
- console.log(` Color: ${tagType.color}\n`);
105
- }
106
- catch (error) {
107
- spinner.fail(`Failed to create tag type: ${error.message}`);
108
- process.exit(1);
109
- }
110
- });
18
+ .description('Manage flat, untyped tags');
111
19
  // List tags
112
20
  tagsCommand
113
21
  .command('list')
114
22
  .description('List tags in a workspace')
115
23
  .option('-w, --workspace <id>', 'Workspace name, slug, or ID')
116
- .option('-t, --type <typeId>', 'Filter by tag type ID')
117
24
  .action(async (options) => {
118
25
  const spinner = ora('Loading tags...').start();
119
26
  try {
@@ -124,11 +31,7 @@ tagsCommand
124
31
  process.exit(1);
125
32
  }
126
33
  const workspace = await resolveWorkspace(client, options.workspace);
127
- let url = `/workspaces/${workspace.id}/tags`;
128
- if (options.type) {
129
- url += `?typeId=${options.type}`;
130
- }
131
- const response = await client.get(url);
34
+ const response = await client.get(`/workspaces/${workspace.id}/tags`);
132
35
  const tagsData = response.data;
133
36
  const tags = tagsData.data || tagsData || [];
134
37
  if (tags.length === 0) {
@@ -138,25 +41,13 @@ tagsCommand
138
41
  }
139
42
  spinner.stop();
140
43
  console.log(chalk.bold(`\n${tags.length} tag(s):\n`));
141
- // Group by type
142
- const tagsByType = new Map();
143
44
  tags.forEach((tag) => {
144
- const typeName = tag.typeName || 'Unknown';
145
- if (!tagsByType.has(typeName)) {
146
- tagsByType.set(typeName, []);
147
- }
148
- tagsByType.get(typeName).push(tag);
149
- });
150
- tagsByType.forEach((typeTags, typeName) => {
151
- console.log(chalk.cyan(typeName + ':'));
152
- typeTags.forEach((tag) => {
153
- console.log(` ${tag.typeSlug}:${tag.value}`);
154
- console.log(` ID: ${tag.id}`);
155
- console.log(` Color: ${tag.color}`);
156
- if (tag.usageCount !== undefined)
157
- console.log(` Usage: ${tag.usageCount} files`);
158
- console.log('');
159
- });
45
+ console.log(chalk.cyan(` ${tag.value}`));
46
+ console.log(` ID: ${tag.id}`);
47
+ console.log(` Color: ${tag.color}`);
48
+ if (tag.usageCount !== undefined)
49
+ console.log(` Usage: ${tag.usageCount} files`);
50
+ console.log('');
160
51
  });
161
52
  }
162
53
  catch (error) {
@@ -164,104 +55,20 @@ tagsCommand
164
55
  process.exit(1);
165
56
  }
166
57
  });
167
- // Tag analytics
168
- tagsCommand
169
- .command('analytics')
170
- .description('Show tag usage analytics (most-used tags first)')
171
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
172
- .option('-l, --limit <n>', 'Max tags to show', parseInt)
173
- .action(async (options) => {
174
- const spinner = ora('Loading tag analytics...').start();
175
- try {
176
- const config = await loadConfig();
177
- const client = new ApiClient(config.api_key, config.api_url);
178
- const workspace = await resolveWorkspace(client, options.workspace);
179
- const response = await client.get(`/workspaces/${workspace.id}/tags/analytics`);
180
- const data = response.data.data || response.data || [];
181
- const list = options.limit ? data.slice(0, options.limit) : data;
182
- spinner.stop();
183
- if (list.length === 0) {
184
- console.log(chalk.yellow('No tag usage data found.'));
185
- return;
186
- }
187
- console.log(chalk.bold(`\nTag usage analytics (${list.length} tags):\n`));
188
- list.forEach((item, i) => {
189
- const count = item.usageCount ?? item.count ?? 0;
190
- const label = item.typeSlug ? `${item.typeSlug}:${item.value}` : item.value;
191
- console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.cyan(label)} — ${chalk.bold(count)} file${count !== 1 ? 's' : ''}`);
192
- });
193
- console.log();
194
- }
195
- catch (error) {
196
- spinner.fail(`Failed to load analytics: ${error.message}`);
197
- process.exit(1);
198
- }
199
- });
200
- // Create tag
201
- tagsCommand
202
- .command('create')
203
- .description('Create a new tag')
204
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
205
- .requiredOption('-t, --type <typeId>', 'Tag type ID or slug')
206
- .requiredOption('-v, --value <value>', 'Tag value (lowercase, hyphen-separated)')
207
- .option('-c, --color <hex>', 'Hex color code')
208
- .action(async (options) => {
209
- const spinner = ora('Creating tag...').start();
210
- try {
211
- const config = await loadConfig();
212
- const client = new ApiClient(config.api_key, config.api_url);
213
- // Validate value format
214
- if (!/^[a-z0-9-]+$/.test(options.value)) {
215
- spinner.fail('Error: Tag value must contain only lowercase letters, numbers, and hyphens');
216
- process.exit(1);
217
- }
218
- const workspace = await resolveWorkspace(client, options.workspace);
219
- // Resolve type by slug if not a UUID
220
- let typeId = options.type;
221
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
222
- if (!uuidRegex.test(typeId)) {
223
- const typesResponse = await client.get(`/workspaces/${workspace.id}/tag-types`);
224
- const typesData = typesResponse.data;
225
- const types = typesData.data || typesData || [];
226
- const matchingType = types.find((t) => t.slug === typeId || t.name.toLowerCase() === typeId.toLowerCase());
227
- if (!matchingType) {
228
- spinner.fail(`Tag type '${typeId}' not found. Use 'contextium tags types -w ${options.workspace}' to list types.`);
229
- process.exit(1);
230
- }
231
- typeId = matchingType.id;
232
- }
233
- const data = {
234
- typeId,
235
- value: options.value,
236
- };
237
- if (options.color)
238
- data.color = options.color;
239
- const response = await client.post(`/workspaces/${workspace.id}/tags`, data);
240
- const tagData = response.data;
241
- const tag = tagData.data || tagData;
242
- spinner.stop();
243
- console.log(chalk.green('\n✓ Tag created successfully!'));
244
- console.log(` ID: ${tag.id}`);
245
- console.log(` Tag: ${tag.typeSlug}:${tag.value}`);
246
- console.log(` Type: ${tag.typeName}`);
247
- console.log(` Color: ${tag.color}\n`);
248
- }
249
- catch (error) {
250
- spinner.fail(`Failed to create tag: ${error.message}`);
251
- process.exit(1);
252
- }
253
- });
58
+ // Tags are authored inline: add a #<value> token to a file's content (e.g.
59
+ // `contextium edit-file <id> --content "… #urgent"`) and it's created on save.
60
+ // There is no standalone "create a bare tag" command.
254
61
  // Apply tag to a file
255
62
  tagsCommand
256
63
  .command('apply')
257
64
  .description('(deprecated) Tags are authored inline — add #<value> to the file content')
258
65
  .option('-w, --workspace <id>', 'Workspace name, slug, or ID')
259
- .option('-t, --tag <tagId>', 'Tag ID or slug:value')
66
+ .option('-t, --tag <tagId>', 'Tag ID or value')
260
67
  .option('-f, --file <fileId>', 'File ID')
261
68
  .action(() => {
262
69
  console.log(chalk.yellow('\n⚠ "contextium tags apply" is deprecated.'));
263
70
  console.log(chalk.dim('Tags are now authored inline: add a #<value> token to the file content (e.g. contextium edit <fileId> ...).'));
264
- console.log(chalk.dim('File content is the source of truth for a file\'s tags; tag rules still auto-apply.'));
71
+ console.log(chalk.dim('File content is the source of truth for a file\'s tags.'));
265
72
  process.exit(1);
266
73
  });
267
74
  // Apply tag to multiple files
@@ -269,12 +76,11 @@ tagsCommand
269
76
  .command('apply-bulk')
270
77
  .description('(deprecated) Tags are authored inline — add #<value> to file content')
271
78
  .option('-w, --workspace <id>', 'Workspace name, slug, or ID')
272
- .option('-t, --tag <tagId>', 'Tag ID or slug:value')
79
+ .option('-t, --tag <tagId>', 'Tag ID or value')
273
80
  .option('-f, --files <fileIds>', 'Comma-separated file IDs')
274
81
  .action(() => {
275
82
  console.log(chalk.yellow('\n⚠ "contextium tags apply-bulk" is deprecated.'));
276
83
  console.log(chalk.dim('Tags are now authored inline: add a #<value> token to each file\'s content.'));
277
- console.log(chalk.dim('For pattern-based bulk tagging, use tag rules (contextium tags create-rule).'));
278
84
  process.exit(1);
279
85
  });
280
86
  // Remove tag from a file
@@ -282,7 +88,7 @@ tagsCommand
282
88
  .command('remove')
283
89
  .description('(deprecated) Untag by removing the #<value> token from the file content')
284
90
  .option('-w, --workspace <id>', 'Workspace name, slug, or ID')
285
- .option('-t, --tag <tagId>', 'Tag ID or slug:value')
91
+ .option('-t, --tag <tagId>', 'Tag ID or value')
286
92
  .option('-f, --file <fileId>', 'File ID')
287
93
  .action(() => {
288
94
  console.log(chalk.yellow('\n⚠ "contextium tags remove" is deprecated.'));
@@ -295,7 +101,7 @@ tagsCommand
295
101
  .command('search')
296
102
  .description('Search files by tags')
297
103
  .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
298
- .requiredOption('-t, --tags <tagIds>', 'Comma-separated tag IDs or tag values (e.g., "md" or "file-type:md")')
104
+ .requiredOption('-t, --tags <tagIds>', 'Comma-separated tag IDs or tag values (e.g., "md" or "draft")')
299
105
  .option('-p, --library <libraryId>', 'Filter by context library ID')
300
106
  .action(async (options) => {
301
107
  const spinner = ora('Searching files...').start();
@@ -304,14 +110,14 @@ tagsCommand
304
110
  const client = new ApiClient(config.api_key, config.api_url);
305
111
  const workspace = await resolveWorkspace(client, options.workspace);
306
112
  const workspaceId = workspace.id;
307
- // Parse tag input - support both IDs and slug:value format
113
+ // Parse tag input - support both IDs and values
308
114
  let tagIds = [];
309
115
  const tagInput = options.tags.split(',').map((t) => t.trim());
310
116
  // Check if inputs are UUIDs or need to be resolved
311
117
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
312
118
  const needsResolution = tagInput.some((t) => !uuidRegex.test(t));
313
119
  if (needsResolution) {
314
- // Fetch all tags to resolve slug:value to IDs
120
+ // Fetch all tags to resolve values to IDs
315
121
  const tagsResponse = await client.get(`/workspaces/${workspaceId}/tags`);
316
122
  const allTags = tagsResponse.data.data || tagsResponse.data || [];
317
123
  for (const input of tagInput) {
@@ -319,14 +125,7 @@ tagsCommand
319
125
  tagIds.push(input);
320
126
  }
321
127
  else {
322
- // Try to match slug:value or just value
323
- const [typeSlug, value] = input.includes(':') ? input.split(':') : [null, input];
324
- const matchingTag = allTags.find((tag) => {
325
- if (typeSlug) {
326
- return tag.typeSlug === typeSlug && tag.value === value;
327
- }
328
- return tag.value === value;
329
- });
128
+ const matchingTag = allTags.find((tag) => tag.value === input);
330
129
  if (matchingTag) {
331
130
  tagIds.push(matchingTag.id);
332
131
  }
@@ -437,7 +236,7 @@ tagsCommand
437
236
  console.log(chalk.bold(`\n${tags.length} tag(s) on file:\n`));
438
237
  tags.forEach((entry) => {
439
238
  const tag = entry.tag || entry;
440
- console.log(` ${chalk.cyan(tag.typeSlug || tag.typeName || 'unknown')}:${tag.value}`);
239
+ console.log(` ${chalk.cyan(tag.value)}`);
441
240
  console.log(chalk.dim(` ID: ${tag.id} Color: ${tag.color || 'default'}`));
442
241
  });
443
242
  console.log('');
@@ -447,496 +246,7 @@ tagsCommand
447
246
  process.exit(1);
448
247
  }
449
248
  });
450
- // Update a tag
451
- tagsCommand
452
- .command('update')
453
- .description('Update an existing tag')
454
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
455
- .requiredOption('-t, --tag <tagId>', 'Tag ID or slug:value')
456
- .option('--value <value>', 'New tag value (lowercase, hyphens)')
457
- .option('--type <typeId>', 'New tag type ID')
458
- .option('-c, --color <hex>', 'New hex color code')
459
- .action(async (options) => {
460
- const spinner = ora('Updating tag...').start();
461
- try {
462
- const config = await loadConfig();
463
- const client = new ApiClient(config.api_key, config.api_url);
464
- const workspace = await resolveWorkspace(client, options.workspace);
465
- // Resolve tag by slug:value if not a UUID
466
- let tagId = options.tag;
467
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
468
- if (!uuidRegex.test(tagId)) {
469
- const tagsResponse = await client.get(`/workspaces/${workspace.id}/tags`);
470
- const allTags = tagsResponse.data.data || tagsResponse.data || [];
471
- const [typeSlug, value] = tagId.includes(':') ? tagId.split(':') : [null, tagId];
472
- const matchingTag = allTags.find((tag) => {
473
- if (typeSlug)
474
- return tag.typeSlug === typeSlug && tag.value === value;
475
- return tag.value === value;
476
- });
477
- if (!matchingTag) {
478
- spinner.fail(`Tag '${tagId}' not found.`);
479
- process.exit(1);
480
- }
481
- tagId = matchingTag.id;
482
- }
483
- const data = {};
484
- if (options.value)
485
- data.value = options.value;
486
- if (options.type)
487
- data.typeId = options.type;
488
- if (options.color)
489
- data.color = options.color;
490
- if (Object.keys(data).length === 0) {
491
- spinner.fail('No fields to update. Use --value, --type, or --color.');
492
- process.exit(1);
493
- }
494
- const response = await client.put(`/tags/${tagId}?workspaceId=${workspace.id}`, data);
495
- const tag = response.data.data || response.data;
496
- spinner.stop();
497
- console.log(chalk.green('\n✓ Tag updated successfully!'));
498
- console.log(` Tag: ${tag.typeSlug || ''}:${tag.value}`);
499
- console.log(` ID: ${tag.id}`);
500
- if (tag.color)
501
- console.log(` Color: ${tag.color}`);
502
- console.log('');
503
- }
504
- catch (error) {
505
- spinner.fail(`Failed to update tag: ${error.message}`);
506
- process.exit(1);
507
- }
508
- });
509
- // Get tag type by ID
510
- tagsCommand
511
- .command('type-get')
512
- .description('Get a tag type by ID')
513
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
514
- .requiredOption('-t, --type <typeId>', 'Tag type ID or slug')
515
- .action(async (options) => {
516
- const spinner = ora('Loading tag type...').start();
517
- try {
518
- const config = await loadConfig();
519
- const client = new ApiClient(config.api_key, config.api_url);
520
- const workspace = await resolveWorkspace(client, options.workspace);
521
- // Resolve type by slug if not a UUID
522
- let typeId = options.type;
523
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
524
- if (!uuidRegex.test(typeId)) {
525
- const typesResponse = await client.get(`/workspaces/${workspace.id}/tag-types`);
526
- const types = typesResponse.data.data || typesResponse.data || [];
527
- const match = types.find((t) => t.slug === typeId || t.name.toLowerCase() === typeId.toLowerCase());
528
- if (!match) {
529
- spinner.fail(`Tag type '${typeId}' not found.`);
530
- process.exit(1);
531
- }
532
- typeId = match.id;
533
- }
534
- const response = await client.get(`/tag-types/${typeId}`, {
535
- params: { workspaceId: workspace.id }
536
- });
537
- const tagType = response.data.data || response.data;
538
- spinner.stop();
539
- console.log(chalk.bold(`\nTag Type: ${tagType.name}\n`));
540
- console.log(` ID: ${tagType.id}`);
541
- console.log(` Slug: ${tagType.slug}`);
542
- console.log(` Color: ${tagType.color || 'default'}`);
543
- if (tagType.icon)
544
- console.log(` Icon: ${tagType.icon}`);
545
- if (tagType.description)
546
- console.log(` Description: ${tagType.description}`);
547
- console.log(` System: ${tagType.isSystem ? 'Yes' : 'No'}`);
548
- console.log(` Display Order: ${tagType.displayOrder}`);
549
- if (tagType.tagCount !== undefined)
550
- console.log(` Tag Count: ${tagType.tagCount}`);
551
- console.log('');
552
- }
553
- catch (error) {
554
- spinner.fail(`Failed to load tag type: ${error.message}`);
555
- process.exit(1);
556
- }
557
- });
558
- // Update tag type
559
- tagsCommand
560
- .command('type-update')
561
- .description('Update an existing tag type')
562
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
563
- .requiredOption('-t, --type <typeId>', 'Tag type ID or slug')
564
- .option('-n, --name <name>', 'New name')
565
- .option('-s, --slug <slug>', 'New slug')
566
- .option('-c, --color <hex>', 'New hex color code')
567
- .option('-i, --icon <icon>', 'New icon name')
568
- .option('-d, --description <text>', 'New description')
569
- .option('--order <number>', 'New display order')
570
- .action(async (options) => {
571
- const spinner = ora('Updating tag type...').start();
572
- try {
573
- const config = await loadConfig();
574
- const client = new ApiClient(config.api_key, config.api_url);
575
- const workspace = await resolveWorkspace(client, options.workspace);
576
- // Resolve type by slug if not a UUID
577
- let typeId = options.type;
578
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
579
- if (!uuidRegex.test(typeId)) {
580
- const typesResponse = await client.get(`/workspaces/${workspace.id}/tag-types`);
581
- const types = typesResponse.data.data || typesResponse.data || [];
582
- const match = types.find((t) => t.slug === typeId || t.name.toLowerCase() === typeId.toLowerCase());
583
- if (!match) {
584
- spinner.fail(`Tag type '${typeId}' not found.`);
585
- process.exit(1);
586
- }
587
- typeId = match.id;
588
- }
589
- const data = {};
590
- if (options.name)
591
- data.name = options.name;
592
- if (options.slug)
593
- data.slug = options.slug;
594
- if (options.color)
595
- data.color = options.color;
596
- if (options.icon)
597
- data.icon = options.icon;
598
- if (options.description)
599
- data.description = options.description;
600
- if (options.order !== undefined)
601
- data.displayOrder = parseInt(options.order, 10);
602
- if (Object.keys(data).length === 0) {
603
- spinner.fail('No fields to update. Use --name, --slug, --color, --icon, --description, or --order.');
604
- process.exit(1);
605
- }
606
- const response = await client.put(`/tag-types/${typeId}?workspaceId=${workspace.id}`, data);
607
- const tagType = response.data.data || response.data;
608
- spinner.stop();
609
- console.log(chalk.green('\n✓ Tag type updated successfully!'));
610
- console.log(` Name: ${tagType.name}`);
611
- console.log(` Slug: ${tagType.slug}`);
612
- console.log(` ID: ${tagType.id}`);
613
- if (tagType.color)
614
- console.log(` Color: ${tagType.color}`);
615
- console.log('');
616
- }
617
- catch (error) {
618
- spinner.fail(`Failed to update tag type: ${error.message}`);
619
- process.exit(1);
620
- }
621
- });
622
- // Delete tag type
623
- tagsCommand
624
- .command('delete-type')
625
- .description('Delete a tag type')
626
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
627
- .requiredOption('-t, --type <typeId>', 'Tag type ID or slug')
628
- .option('--confirm', 'Skip confirmation prompt')
629
- .action(async (options) => {
630
- const spinner = ora('Resolving tag type...').start();
631
- try {
632
- const config = await loadConfig();
633
- const client = new ApiClient(config.api_key, config.api_url);
634
- const workspace = await resolveWorkspace(client, options.workspace);
635
- let typeId = options.type;
636
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
637
- if (!uuidRegex.test(typeId)) {
638
- const typesResponse = await client.get(`/workspaces/${workspace.id}/tag-types`);
639
- const types = typesResponse.data.data || typesResponse.data || [];
640
- const match = types.find((t) => t.slug === typeId || t.name.toLowerCase() === typeId.toLowerCase());
641
- if (!match) {
642
- spinner.fail(`Tag type '${typeId}' not found.`);
643
- process.exit(1);
644
- }
645
- typeId = match.id;
646
- }
647
- spinner.stop();
648
- if (!options.confirm) {
649
- console.log(chalk.yellow(`\n⚠ This will permanently delete tag type ${options.type} and all its tags.`));
650
- console.log(chalk.dim(' Use --confirm to skip this warning.\n'));
651
- const readline = await import('readline');
652
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
653
- const answer = await new Promise((resolve) => {
654
- rl.question(' Continue? (y/N) ', resolve);
655
- });
656
- rl.close();
657
- if (answer.toLowerCase() !== 'y') {
658
- console.log(chalk.dim(' Cancelled.'));
659
- return;
660
- }
661
- }
662
- const deleteSpinner = ora('Deleting tag type...').start();
663
- await client.delete(`/tag-types/${typeId}?workspaceId=${workspace.id}`);
664
- deleteSpinner.stop();
665
- console.log(chalk.green('\n✓ Tag type deleted successfully.'));
666
- console.log();
667
- }
668
- catch (error) {
669
- spinner.fail(`Failed to delete tag type: ${error.message}`);
670
- process.exit(1);
671
- }
672
- });
673
- // Delete a tag
674
- tagsCommand
675
- .command('delete')
676
- .description('Delete a tag')
677
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
678
- .requiredOption('-i, --id <tagId>', 'Tag ID')
679
- .option('--confirm', 'Skip confirmation prompt')
680
- .action(async (options) => {
681
- const spinner = ora('Resolving workspace...').start();
682
- try {
683
- const config = await loadConfig();
684
- const client = new ApiClient(config.api_key, config.api_url);
685
- const workspace = await resolveWorkspace(client, options.workspace);
686
- spinner.stop();
687
- if (!options.confirm) {
688
- console.log(chalk.yellow(`\n⚠ This will permanently delete tag ${options.id}.`));
689
- const readline = await import('readline');
690
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
691
- const answer = await new Promise((resolve) => { rl.question(' Continue? (y/N) ', resolve); });
692
- rl.close();
693
- if (answer.toLowerCase() !== 'y') {
694
- console.log(chalk.dim(' Cancelled.'));
695
- return;
696
- }
697
- }
698
- const deleteSpinner = ora('Deleting tag...').start();
699
- await client.delete(`/tags/${options.id}?workspaceId=${workspace.id}`);
700
- deleteSpinner.stop();
701
- console.log(chalk.green('\n✓ Tag deleted successfully.'));
702
- }
703
- catch (error) {
704
- spinner.fail(`Failed to delete tag: ${error.message}`);
705
- process.exit(1);
706
- }
707
- });
708
- // List tag rules
709
- tagsCommand
710
- .command('list-rules')
711
- .description('List tag rules in a workspace')
712
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
713
- .option('--active-only', 'Return only active rules')
714
- .action(async (options) => {
715
- const spinner = ora('Loading tag rules...').start();
716
- try {
717
- const config = await loadConfig();
718
- const client = new ApiClient(config.api_key, config.api_url);
719
- const workspace = await resolveWorkspace(client, options.workspace);
720
- const url = `/workspaces/${workspace.id}/tag-rules${options.activeOnly ? '?activeOnly=true' : ''}`;
721
- const response = await client.get(url);
722
- const rules = response.data.data || response.data || [];
723
- spinner.stop();
724
- if (rules.length === 0) {
725
- console.log(chalk.yellow('No tag rules found.'));
726
- return;
727
- }
728
- console.log(chalk.bold(`\n${rules.length} tag rule(s):\n`));
729
- rules.forEach((rule) => {
730
- const status = rule.isActive ? chalk.green('active') : chalk.dim('inactive');
731
- console.log(chalk.cyan(`${rule.name}`) + chalk.dim(` [${status}]`));
732
- console.log(` ID: ${rule.id}`);
733
- console.log(` Pattern: ${rule.patternType}${rule.pattern ? ` — ${rule.pattern}` : ''}`);
734
- if (rule.tags?.length)
735
- console.log(` Tags: ${rule.tags.map((t) => `${t.typeSlug}:${t.value}`).join(', ')}`);
736
- if (rule.description)
737
- console.log(` Description: ${rule.description}`);
738
- console.log();
739
- });
740
- }
741
- catch (error) {
742
- spinner.fail(`Failed to list tag rules: ${error.message}`);
743
- process.exit(1);
744
- }
745
- });
746
- // Create a tag rule
747
- tagsCommand
748
- .command('create-rule')
749
- .description('Create a new tag rule')
750
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
751
- .requiredOption('-n, --name <name>', 'Rule name')
752
- .requiredOption('-p, --pattern-type <type>', 'Pattern type: match_all | filename_contains | file_extension | path_contains | path_starts_with | filename_regex')
753
- .requiredOption('-T, --tag-ids <ids>', 'Comma-separated tag IDs to apply')
754
- .option('--pattern <pattern>', 'Pattern value (not required for match_all)')
755
- .option('-d, --description <text>', 'Rule description')
756
- .option('--inactive', 'Create rule as inactive (default: active)')
757
- .option('--priority <n>', 'Rule priority (lower runs first)', parseInt)
758
- .action(async (options) => {
759
- const spinner = ora('Creating tag rule...').start();
760
- try {
761
- const config = await loadConfig();
762
- const client = new ApiClient(config.api_key, config.api_url);
763
- const workspace = await resolveWorkspace(client, options.workspace);
764
- const validTypes = ['match_all', 'filename_contains', 'file_extension', 'path_contains', 'path_starts_with', 'filename_regex'];
765
- if (!validTypes.includes(options.patternType)) {
766
- spinner.fail(`Invalid pattern type. Must be one of: ${validTypes.join(', ')}`);
767
- process.exit(1);
768
- }
769
- const tagIds = options.tagIds.split(',').map((id) => id.trim()).filter(Boolean);
770
- const body = { name: options.name, patternType: options.patternType, tagIds, isActive: !options.inactive };
771
- if (options.pattern)
772
- body.pattern = options.pattern;
773
- if (options.description)
774
- body.description = options.description;
775
- if (options.priority !== undefined)
776
- body.priority = options.priority;
777
- const response = await client.post(`/workspaces/${workspace.id}/tag-rules`, body);
778
- const rule = response.data.data || response.data;
779
- spinner.stop();
780
- console.log(chalk.green('\n✓ Tag rule created!'));
781
- console.log(` ID: ${rule.id}`);
782
- console.log(` Name: ${rule.name}`);
783
- console.log(` Pattern: ${rule.patternType}${rule.pattern ? ` — ${rule.pattern}` : ''}`);
784
- console.log(` Active: ${rule.isActive}\n`);
785
- }
786
- catch (error) {
787
- spinner.fail(`Failed to create tag rule: ${error.message}`);
788
- process.exit(1);
789
- }
790
- });
791
- // Update a tag rule
792
- tagsCommand
793
- .command('update-rule')
794
- .description('Update a tag rule')
795
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
796
- .requiredOption('-i, --id <ruleId>', 'Rule ID')
797
- .option('-n, --name <name>', 'New name')
798
- .option('-d, --description <text>', 'New description')
799
- .option('-p, --pattern-type <type>', 'New pattern type')
800
- .option('--pattern <pattern>', 'New pattern value')
801
- .option('--active', 'Set rule active')
802
- .option('--inactive', 'Set rule inactive')
803
- .option('-T, --tag-ids <ids>', 'New comma-separated tag IDs')
804
- .option('--priority <n>', 'New priority', parseInt)
805
- .action(async (options) => {
806
- const spinner = ora('Updating tag rule...').start();
807
- try {
808
- const config = await loadConfig();
809
- const client = new ApiClient(config.api_key, config.api_url);
810
- const workspace = await resolveWorkspace(client, options.workspace);
811
- const body = {};
812
- if (options.name)
813
- body.name = options.name;
814
- if (options.description)
815
- body.description = options.description;
816
- if (options.patternType)
817
- body.patternType = options.patternType;
818
- if (options.pattern !== undefined)
819
- body.pattern = options.pattern;
820
- if (options.active)
821
- body.isActive = true;
822
- if (options.inactive)
823
- body.isActive = false;
824
- if (options.tagIds)
825
- body.tagIds = options.tagIds.split(',').map((id) => id.trim()).filter(Boolean);
826
- if (options.priority !== undefined)
827
- body.priority = options.priority;
828
- await client.put(`/tag-rules/${options.id}?workspaceId=${workspace.id}`, body);
829
- spinner.stop();
830
- console.log(chalk.green('\n✓ Tag rule updated successfully.\n'));
831
- }
832
- catch (error) {
833
- spinner.fail(`Failed to update tag rule: ${error.message}`);
834
- process.exit(1);
835
- }
836
- });
837
- // Delete a tag rule
838
- tagsCommand
839
- .command('delete-rule')
840
- .description('Delete a tag rule')
841
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
842
- .requiredOption('-i, --id <ruleId>', 'Rule ID')
843
- .option('--confirm', 'Skip confirmation prompt')
844
- .action(async (options) => {
845
- const spinner = ora('Resolving workspace...').start();
846
- try {
847
- const config = await loadConfig();
848
- const client = new ApiClient(config.api_key, config.api_url);
849
- const workspace = await resolveWorkspace(client, options.workspace);
850
- spinner.stop();
851
- if (!options.confirm) {
852
- console.log(chalk.yellow(`\n⚠ This will permanently delete rule ${options.id}.`));
853
- const readline = await import('readline');
854
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
855
- const answer = await new Promise((resolve) => { rl.question(' Continue? (y/N) ', resolve); });
856
- rl.close();
857
- if (answer.toLowerCase() !== 'y') {
858
- console.log(chalk.dim(' Cancelled.'));
859
- return;
860
- }
861
- }
862
- const deleteSpinner = ora('Deleting tag rule...').start();
863
- await client.delete(`/tag-rules/${options.id}?workspaceId=${workspace.id}`);
864
- deleteSpinner.stop();
865
- console.log(chalk.green('\n✓ Tag rule deleted successfully.'));
866
- }
867
- catch (error) {
868
- spinner.fail(`Failed to delete tag rule: ${error.message}`);
869
- process.exit(1);
870
- }
871
- });
872
- // Test a tag rule pattern
873
- tagsCommand
874
- .command('test-pattern')
875
- .description('Test a tag rule pattern against workspace files (dry run)')
876
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
877
- .requiredOption('-p, --pattern-type <type>', 'Pattern type: match_all | filename_contains | file_extension | path_contains | path_starts_with | filename_regex')
878
- .option('--pattern <pattern>', 'Pattern value')
879
- .option('-l, --limit <n>', 'Max matching files to show (default 10)', parseInt)
880
- .action(async (options) => {
881
- const spinner = ora('Testing pattern...').start();
882
- try {
883
- const config = await loadConfig();
884
- const client = new ApiClient(config.api_key, config.api_url);
885
- const workspace = await resolveWorkspace(client, options.workspace);
886
- const body = { patternType: options.patternType, pattern: options.pattern || '', limit: options.limit || 10 };
887
- const response = await client.post(`/workspaces/${workspace.id}/tag-rules/test`, body);
888
- const result = response.data.data || response.data;
889
- spinner.stop();
890
- const matches = result.matchingPaths || result.paths || [];
891
- if (matches.length === 0) {
892
- console.log(chalk.yellow('\nNo files matched this pattern.\n'));
893
- }
894
- else {
895
- console.log(chalk.bold(`\n${matches.length} matching file(s):\n`));
896
- matches.forEach((p) => console.log(chalk.dim(` ${p}`)));
897
- console.log();
898
- }
899
- }
900
- catch (error) {
901
- spinner.fail(`Failed to test pattern: ${error.message}`);
902
- process.exit(1);
903
- }
904
- });
905
- // Apply a tag rule retroactively to existing files
906
- tagsCommand
907
- .command('apply-rule')
908
- .description('Apply a tag rule retroactively to all existing workspace files')
909
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
910
- .requiredOption('-i, --id <ruleId>', 'Rule ID')
911
- .option('--confirm', 'Skip confirmation prompt')
912
- .action(async (options) => {
913
- const spinner = ora('Resolving workspace...').start();
914
- try {
915
- const config = await loadConfig();
916
- const client = new ApiClient(config.api_key, config.api_url);
917
- const workspace = await resolveWorkspace(client, options.workspace);
918
- spinner.stop();
919
- if (!options.confirm) {
920
- console.log(chalk.yellow('\n⚠ This will apply the rule to ALL existing files in the workspace.'));
921
- const readline = await import('readline');
922
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
923
- const answer = await new Promise((resolve) => { rl.question(' Continue? (y/N) ', resolve); });
924
- rl.close();
925
- if (answer.toLowerCase() !== 'y') {
926
- console.log(chalk.dim(' Cancelled.'));
927
- return;
928
- }
929
- }
930
- const applySpinner = ora('Applying rule to existing files...').start();
931
- const response = await client.post(`/tag-rules/${options.id}/apply?workspaceId=${workspace.id}`, {});
932
- const result = response.data.data || response.data;
933
- applySpinner.stop();
934
- const applied = result.applied ?? result.count ?? '?';
935
- console.log(chalk.green(`\n✓ Rule applied. Tagged ${applied} file(s).\n`));
936
- }
937
- catch (error) {
938
- spinner.fail(`Failed to apply rule: ${error.message}`);
939
- process.exit(1);
940
- }
941
- });
249
+ // Tag vocabulary is content-driven: a tag exists while some file's content
250
+ // contains its #value, and disappears when no file references it. There are no
251
+ // rename/recolor/delete-tag commands — edit the files' content instead.
942
252
  //# sourceMappingURL=tags.js.map