@contextium/cli 1.7.0 → 1.8.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.
@@ -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,220 +55,53 @@ 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
- .description('Apply a tag to a file')
258
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
259
- .requiredOption('-t, --tag <tagId>', 'Tag ID or slug:value (e.g., "topic:authentication")')
260
- .requiredOption('-f, --file <fileId>', 'File ID')
261
- .action(async (options) => {
262
- const spinner = ora('Applying tag...').start();
263
- try {
264
- const config = await loadConfig();
265
- const client = new ApiClient(config.api_key, config.api_url);
266
- const workspace = await resolveWorkspace(client, options.workspace);
267
- // Resolve tag by slug:value if not a UUID
268
- let tagId = options.tag;
269
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
270
- if (!uuidRegex.test(tagId)) {
271
- const tagsResponse = await client.get(`/workspaces/${workspace.id}/tags`);
272
- const allTags = tagsResponse.data.data || tagsResponse.data || [];
273
- const [typeSlug, value] = tagId.includes(':') ? tagId.split(':') : [null, tagId];
274
- const matchingTag = allTags.find((tag) => {
275
- if (typeSlug)
276
- return tag.typeSlug === typeSlug && tag.value === value;
277
- return tag.value === value;
278
- });
279
- if (!matchingTag) {
280
- spinner.fail(`Tag '${tagId}' not found. Use 'contextium tags list -w ${options.workspace}' to see available tags.`);
281
- process.exit(1);
282
- }
283
- tagId = matchingTag.id;
284
- }
285
- await client.post(`/tags/${tagId}/apply?workspaceId=${workspace.id}`, { fileId: options.file });
286
- spinner.stop();
287
- console.log(chalk.green('\n✓ Tag applied successfully!'));
288
- }
289
- catch (error) {
290
- spinner.fail(`Failed to apply tag: ${error.message}`);
291
- process.exit(1);
292
- }
64
+ .description('(deprecated) Tags are authored inline — add #<value> to the file content')
65
+ .option('-w, --workspace <id>', 'Workspace name, slug, or ID')
66
+ .option('-t, --tag <tagId>', 'Tag ID or value')
67
+ .option('-f, --file <fileId>', 'File ID')
68
+ .action(() => {
69
+ console.log(chalk.yellow('\n⚠ "contextium tags apply" is deprecated.'));
70
+ console.log(chalk.dim('Tags are now authored inline: add a #<value> token to the file content (e.g. contextium edit <fileId> ...).'));
71
+ console.log(chalk.dim('File content is the source of truth for a file\'s tags.'));
72
+ process.exit(1);
293
73
  });
294
74
  // Apply tag to multiple files
295
75
  tagsCommand
296
76
  .command('apply-bulk')
297
- .description('Apply a tag to multiple files')
298
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
299
- .requiredOption('-t, --tag <tagId>', 'Tag ID or slug:value (e.g., "topic:authentication")')
300
- .requiredOption('-f, --files <fileIds>', 'Comma-separated file IDs')
301
- .action(async (options) => {
302
- const fileIds = options.files.split(',').map((f) => f.trim());
303
- const spinner = ora(`Applying tag to ${fileIds.length} file(s)...`).start();
304
- try {
305
- const config = await loadConfig();
306
- const client = new ApiClient(config.api_key, config.api_url);
307
- const workspace = await resolveWorkspace(client, options.workspace);
308
- // Resolve tag by slug:value if not a UUID
309
- let tagId = options.tag;
310
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
311
- if (!uuidRegex.test(tagId)) {
312
- const tagsResponse = await client.get(`/workspaces/${workspace.id}/tags`);
313
- const allTags = tagsResponse.data.data || tagsResponse.data || [];
314
- const [typeSlug, value] = tagId.includes(':') ? tagId.split(':') : [null, tagId];
315
- const matchingTag = allTags.find((tag) => {
316
- if (typeSlug)
317
- return tag.typeSlug === typeSlug && tag.value === value;
318
- return tag.value === value;
319
- });
320
- if (!matchingTag) {
321
- spinner.fail(`Tag '${tagId}' not found.`);
322
- process.exit(1);
323
- }
324
- tagId = matchingTag.id;
325
- }
326
- await client.post(`/tags/${tagId}/apply-bulk?workspaceId=${workspace.id}`, { fileIds });
327
- spinner.stop();
328
- console.log(chalk.green(`\n✓ Tag applied to ${fileIds.length} file(s) successfully!`));
329
- }
330
- catch (error) {
331
- spinner.fail(`Failed to apply tag: ${error.message}`);
332
- process.exit(1);
333
- }
77
+ .description('(deprecated) Tags are authored inline — add #<value> to file content')
78
+ .option('-w, --workspace <id>', 'Workspace name, slug, or ID')
79
+ .option('-t, --tag <tagId>', 'Tag ID or value')
80
+ .option('-f, --files <fileIds>', 'Comma-separated file IDs')
81
+ .action(() => {
82
+ console.log(chalk.yellow('\n⚠ "contextium tags apply-bulk" is deprecated.'));
83
+ console.log(chalk.dim('Tags are now authored inline: add a #<value> token to each file\'s content.'));
84
+ process.exit(1);
334
85
  });
335
86
  // Remove tag from a file
336
87
  tagsCommand
337
88
  .command('remove')
338
- .description('Remove a tag from a file')
339
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
340
- .requiredOption('-t, --tag <tagId>', 'Tag ID or slug:value (e.g., "topic:authentication")')
341
- .requiredOption('-f, --file <fileId>', 'File ID')
342
- .action(async (options) => {
343
- const spinner = ora('Removing tag...').start();
344
- try {
345
- const config = await loadConfig();
346
- const client = new ApiClient(config.api_key, config.api_url);
347
- const workspace = await resolveWorkspace(client, options.workspace);
348
- // Resolve tag by slug:value if not a UUID
349
- let tagId = options.tag;
350
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
351
- if (!uuidRegex.test(tagId)) {
352
- const tagsResponse = await client.get(`/workspaces/${workspace.id}/tags`);
353
- const allTags = tagsResponse.data.data || tagsResponse.data || [];
354
- const [typeSlug, value] = tagId.includes(':') ? tagId.split(':') : [null, tagId];
355
- const matchingTag = allTags.find((tag) => {
356
- if (typeSlug)
357
- return tag.typeSlug === typeSlug && tag.value === value;
358
- return tag.value === value;
359
- });
360
- if (!matchingTag) {
361
- spinner.fail(`Tag '${tagId}' not found.`);
362
- process.exit(1);
363
- }
364
- tagId = matchingTag.id;
365
- }
366
- await client.post(`/tags/${tagId}/remove?workspaceId=${workspace.id}`, { fileId: options.file });
367
- spinner.stop();
368
- console.log(chalk.green('\n✓ Tag removed successfully!'));
369
- }
370
- catch (error) {
371
- spinner.fail(`Failed to remove tag: ${error.message}`);
372
- process.exit(1);
373
- }
89
+ .description('(deprecated) Untag by removing the #<value> token from the file content')
90
+ .option('-w, --workspace <id>', 'Workspace name, slug, or ID')
91
+ .option('-t, --tag <tagId>', 'Tag ID or value')
92
+ .option('-f, --file <fileId>', 'File ID')
93
+ .action(() => {
94
+ console.log(chalk.yellow('\n⚠ "contextium tags remove" is deprecated.'));
95
+ console.log(chalk.dim('To untag a file, delete the #<value> token from its content (e.g. contextium edit <fileId> ...).'));
96
+ console.log(chalk.dim('File content is the source of truth for a file\'s tags.'));
97
+ process.exit(1);
374
98
  });
375
99
  // Search files by tags
376
100
  tagsCommand
377
101
  .command('search')
378
102
  .description('Search files by tags')
379
103
  .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
380
- .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")')
381
105
  .option('-p, --library <libraryId>', 'Filter by context library ID')
382
106
  .action(async (options) => {
383
107
  const spinner = ora('Searching files...').start();
@@ -386,14 +110,14 @@ tagsCommand
386
110
  const client = new ApiClient(config.api_key, config.api_url);
387
111
  const workspace = await resolveWorkspace(client, options.workspace);
388
112
  const workspaceId = workspace.id;
389
- // Parse tag input - support both IDs and slug:value format
113
+ // Parse tag input - support both IDs and values
390
114
  let tagIds = [];
391
115
  const tagInput = options.tags.split(',').map((t) => t.trim());
392
116
  // Check if inputs are UUIDs or need to be resolved
393
117
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
394
118
  const needsResolution = tagInput.some((t) => !uuidRegex.test(t));
395
119
  if (needsResolution) {
396
- // Fetch all tags to resolve slug:value to IDs
120
+ // Fetch all tags to resolve values to IDs
397
121
  const tagsResponse = await client.get(`/workspaces/${workspaceId}/tags`);
398
122
  const allTags = tagsResponse.data.data || tagsResponse.data || [];
399
123
  for (const input of tagInput) {
@@ -401,14 +125,7 @@ tagsCommand
401
125
  tagIds.push(input);
402
126
  }
403
127
  else {
404
- // Try to match slug:value or just value
405
- const [typeSlug, value] = input.includes(':') ? input.split(':') : [null, input];
406
- const matchingTag = allTags.find((tag) => {
407
- if (typeSlug) {
408
- return tag.typeSlug === typeSlug && tag.value === value;
409
- }
410
- return tag.value === value;
411
- });
128
+ const matchingTag = allTags.find((tag) => tag.value === input);
412
129
  if (matchingTag) {
413
130
  tagIds.push(matchingTag.id);
414
131
  }
@@ -519,7 +236,7 @@ tagsCommand
519
236
  console.log(chalk.bold(`\n${tags.length} tag(s) on file:\n`));
520
237
  tags.forEach((entry) => {
521
238
  const tag = entry.tag || entry;
522
- console.log(` ${chalk.cyan(tag.typeSlug || tag.typeName || 'unknown')}:${tag.value}`);
239
+ console.log(` ${chalk.cyan(tag.value)}`);
523
240
  console.log(chalk.dim(` ID: ${tag.id} Color: ${tag.color || 'default'}`));
524
241
  });
525
242
  console.log('');
@@ -529,496 +246,7 @@ tagsCommand
529
246
  process.exit(1);
530
247
  }
531
248
  });
532
- // Update a tag
533
- tagsCommand
534
- .command('update')
535
- .description('Update an existing tag')
536
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
537
- .requiredOption('-t, --tag <tagId>', 'Tag ID or slug:value')
538
- .option('--value <value>', 'New tag value (lowercase, hyphens)')
539
- .option('--type <typeId>', 'New tag type ID')
540
- .option('-c, --color <hex>', 'New hex color code')
541
- .action(async (options) => {
542
- const spinner = ora('Updating tag...').start();
543
- try {
544
- const config = await loadConfig();
545
- const client = new ApiClient(config.api_key, config.api_url);
546
- const workspace = await resolveWorkspace(client, options.workspace);
547
- // Resolve tag by slug:value if not a UUID
548
- let tagId = options.tag;
549
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
550
- if (!uuidRegex.test(tagId)) {
551
- const tagsResponse = await client.get(`/workspaces/${workspace.id}/tags`);
552
- const allTags = tagsResponse.data.data || tagsResponse.data || [];
553
- const [typeSlug, value] = tagId.includes(':') ? tagId.split(':') : [null, tagId];
554
- const matchingTag = allTags.find((tag) => {
555
- if (typeSlug)
556
- return tag.typeSlug === typeSlug && tag.value === value;
557
- return tag.value === value;
558
- });
559
- if (!matchingTag) {
560
- spinner.fail(`Tag '${tagId}' not found.`);
561
- process.exit(1);
562
- }
563
- tagId = matchingTag.id;
564
- }
565
- const data = {};
566
- if (options.value)
567
- data.value = options.value;
568
- if (options.type)
569
- data.typeId = options.type;
570
- if (options.color)
571
- data.color = options.color;
572
- if (Object.keys(data).length === 0) {
573
- spinner.fail('No fields to update. Use --value, --type, or --color.');
574
- process.exit(1);
575
- }
576
- const response = await client.put(`/tags/${tagId}?workspaceId=${workspace.id}`, data);
577
- const tag = response.data.data || response.data;
578
- spinner.stop();
579
- console.log(chalk.green('\n✓ Tag updated successfully!'));
580
- console.log(` Tag: ${tag.typeSlug || ''}:${tag.value}`);
581
- console.log(` ID: ${tag.id}`);
582
- if (tag.color)
583
- console.log(` Color: ${tag.color}`);
584
- console.log('');
585
- }
586
- catch (error) {
587
- spinner.fail(`Failed to update tag: ${error.message}`);
588
- process.exit(1);
589
- }
590
- });
591
- // Get tag type by ID
592
- tagsCommand
593
- .command('type-get')
594
- .description('Get a tag type by ID')
595
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
596
- .requiredOption('-t, --type <typeId>', 'Tag type ID or slug')
597
- .action(async (options) => {
598
- const spinner = ora('Loading tag type...').start();
599
- try {
600
- const config = await loadConfig();
601
- const client = new ApiClient(config.api_key, config.api_url);
602
- const workspace = await resolveWorkspace(client, options.workspace);
603
- // Resolve type by slug if not a UUID
604
- let typeId = options.type;
605
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
606
- if (!uuidRegex.test(typeId)) {
607
- const typesResponse = await client.get(`/workspaces/${workspace.id}/tag-types`);
608
- const types = typesResponse.data.data || typesResponse.data || [];
609
- const match = types.find((t) => t.slug === typeId || t.name.toLowerCase() === typeId.toLowerCase());
610
- if (!match) {
611
- spinner.fail(`Tag type '${typeId}' not found.`);
612
- process.exit(1);
613
- }
614
- typeId = match.id;
615
- }
616
- const response = await client.get(`/tag-types/${typeId}`, {
617
- params: { workspaceId: workspace.id }
618
- });
619
- const tagType = response.data.data || response.data;
620
- spinner.stop();
621
- console.log(chalk.bold(`\nTag Type: ${tagType.name}\n`));
622
- console.log(` ID: ${tagType.id}`);
623
- console.log(` Slug: ${tagType.slug}`);
624
- console.log(` Color: ${tagType.color || 'default'}`);
625
- if (tagType.icon)
626
- console.log(` Icon: ${tagType.icon}`);
627
- if (tagType.description)
628
- console.log(` Description: ${tagType.description}`);
629
- console.log(` System: ${tagType.isSystem ? 'Yes' : 'No'}`);
630
- console.log(` Display Order: ${tagType.displayOrder}`);
631
- if (tagType.tagCount !== undefined)
632
- console.log(` Tag Count: ${tagType.tagCount}`);
633
- console.log('');
634
- }
635
- catch (error) {
636
- spinner.fail(`Failed to load tag type: ${error.message}`);
637
- process.exit(1);
638
- }
639
- });
640
- // Update tag type
641
- tagsCommand
642
- .command('type-update')
643
- .description('Update an existing tag type')
644
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
645
- .requiredOption('-t, --type <typeId>', 'Tag type ID or slug')
646
- .option('-n, --name <name>', 'New name')
647
- .option('-s, --slug <slug>', 'New slug')
648
- .option('-c, --color <hex>', 'New hex color code')
649
- .option('-i, --icon <icon>', 'New icon name')
650
- .option('-d, --description <text>', 'New description')
651
- .option('--order <number>', 'New display order')
652
- .action(async (options) => {
653
- const spinner = ora('Updating tag type...').start();
654
- try {
655
- const config = await loadConfig();
656
- const client = new ApiClient(config.api_key, config.api_url);
657
- const workspace = await resolveWorkspace(client, options.workspace);
658
- // Resolve type by slug if not a UUID
659
- let typeId = options.type;
660
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
661
- if (!uuidRegex.test(typeId)) {
662
- const typesResponse = await client.get(`/workspaces/${workspace.id}/tag-types`);
663
- const types = typesResponse.data.data || typesResponse.data || [];
664
- const match = types.find((t) => t.slug === typeId || t.name.toLowerCase() === typeId.toLowerCase());
665
- if (!match) {
666
- spinner.fail(`Tag type '${typeId}' not found.`);
667
- process.exit(1);
668
- }
669
- typeId = match.id;
670
- }
671
- const data = {};
672
- if (options.name)
673
- data.name = options.name;
674
- if (options.slug)
675
- data.slug = options.slug;
676
- if (options.color)
677
- data.color = options.color;
678
- if (options.icon)
679
- data.icon = options.icon;
680
- if (options.description)
681
- data.description = options.description;
682
- if (options.order !== undefined)
683
- data.displayOrder = parseInt(options.order, 10);
684
- if (Object.keys(data).length === 0) {
685
- spinner.fail('No fields to update. Use --name, --slug, --color, --icon, --description, or --order.');
686
- process.exit(1);
687
- }
688
- const response = await client.put(`/tag-types/${typeId}?workspaceId=${workspace.id}`, data);
689
- const tagType = response.data.data || response.data;
690
- spinner.stop();
691
- console.log(chalk.green('\n✓ Tag type updated successfully!'));
692
- console.log(` Name: ${tagType.name}`);
693
- console.log(` Slug: ${tagType.slug}`);
694
- console.log(` ID: ${tagType.id}`);
695
- if (tagType.color)
696
- console.log(` Color: ${tagType.color}`);
697
- console.log('');
698
- }
699
- catch (error) {
700
- spinner.fail(`Failed to update tag type: ${error.message}`);
701
- process.exit(1);
702
- }
703
- });
704
- // Delete tag type
705
- tagsCommand
706
- .command('delete-type')
707
- .description('Delete a tag type')
708
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
709
- .requiredOption('-t, --type <typeId>', 'Tag type ID or slug')
710
- .option('--confirm', 'Skip confirmation prompt')
711
- .action(async (options) => {
712
- const spinner = ora('Resolving tag type...').start();
713
- try {
714
- const config = await loadConfig();
715
- const client = new ApiClient(config.api_key, config.api_url);
716
- const workspace = await resolveWorkspace(client, options.workspace);
717
- let typeId = options.type;
718
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
719
- if (!uuidRegex.test(typeId)) {
720
- const typesResponse = await client.get(`/workspaces/${workspace.id}/tag-types`);
721
- const types = typesResponse.data.data || typesResponse.data || [];
722
- const match = types.find((t) => t.slug === typeId || t.name.toLowerCase() === typeId.toLowerCase());
723
- if (!match) {
724
- spinner.fail(`Tag type '${typeId}' not found.`);
725
- process.exit(1);
726
- }
727
- typeId = match.id;
728
- }
729
- spinner.stop();
730
- if (!options.confirm) {
731
- console.log(chalk.yellow(`\n⚠ This will permanently delete tag type ${options.type} and all its tags.`));
732
- console.log(chalk.dim(' Use --confirm to skip this warning.\n'));
733
- const readline = await import('readline');
734
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
735
- const answer = await new Promise((resolve) => {
736
- rl.question(' Continue? (y/N) ', resolve);
737
- });
738
- rl.close();
739
- if (answer.toLowerCase() !== 'y') {
740
- console.log(chalk.dim(' Cancelled.'));
741
- return;
742
- }
743
- }
744
- const deleteSpinner = ora('Deleting tag type...').start();
745
- await client.delete(`/tag-types/${typeId}?workspaceId=${workspace.id}`);
746
- deleteSpinner.stop();
747
- console.log(chalk.green('\n✓ Tag type deleted successfully.'));
748
- console.log();
749
- }
750
- catch (error) {
751
- spinner.fail(`Failed to delete tag type: ${error.message}`);
752
- process.exit(1);
753
- }
754
- });
755
- // Delete a tag
756
- tagsCommand
757
- .command('delete')
758
- .description('Delete a tag')
759
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
760
- .requiredOption('-i, --id <tagId>', 'Tag ID')
761
- .option('--confirm', 'Skip confirmation prompt')
762
- .action(async (options) => {
763
- const spinner = ora('Resolving workspace...').start();
764
- try {
765
- const config = await loadConfig();
766
- const client = new ApiClient(config.api_key, config.api_url);
767
- const workspace = await resolveWorkspace(client, options.workspace);
768
- spinner.stop();
769
- if (!options.confirm) {
770
- console.log(chalk.yellow(`\n⚠ This will permanently delete tag ${options.id}.`));
771
- const readline = await import('readline');
772
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
773
- const answer = await new Promise((resolve) => { rl.question(' Continue? (y/N) ', resolve); });
774
- rl.close();
775
- if (answer.toLowerCase() !== 'y') {
776
- console.log(chalk.dim(' Cancelled.'));
777
- return;
778
- }
779
- }
780
- const deleteSpinner = ora('Deleting tag...').start();
781
- await client.delete(`/tags/${options.id}?workspaceId=${workspace.id}`);
782
- deleteSpinner.stop();
783
- console.log(chalk.green('\n✓ Tag deleted successfully.'));
784
- }
785
- catch (error) {
786
- spinner.fail(`Failed to delete tag: ${error.message}`);
787
- process.exit(1);
788
- }
789
- });
790
- // List tag rules
791
- tagsCommand
792
- .command('list-rules')
793
- .description('List tag rules in a workspace')
794
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
795
- .option('--active-only', 'Return only active rules')
796
- .action(async (options) => {
797
- const spinner = ora('Loading tag rules...').start();
798
- try {
799
- const config = await loadConfig();
800
- const client = new ApiClient(config.api_key, config.api_url);
801
- const workspace = await resolveWorkspace(client, options.workspace);
802
- const url = `/workspaces/${workspace.id}/tag-rules${options.activeOnly ? '?activeOnly=true' : ''}`;
803
- const response = await client.get(url);
804
- const rules = response.data.data || response.data || [];
805
- spinner.stop();
806
- if (rules.length === 0) {
807
- console.log(chalk.yellow('No tag rules found.'));
808
- return;
809
- }
810
- console.log(chalk.bold(`\n${rules.length} tag rule(s):\n`));
811
- rules.forEach((rule) => {
812
- const status = rule.isActive ? chalk.green('active') : chalk.dim('inactive');
813
- console.log(chalk.cyan(`${rule.name}`) + chalk.dim(` [${status}]`));
814
- console.log(` ID: ${rule.id}`);
815
- console.log(` Pattern: ${rule.patternType}${rule.pattern ? ` — ${rule.pattern}` : ''}`);
816
- if (rule.tags?.length)
817
- console.log(` Tags: ${rule.tags.map((t) => `${t.typeSlug}:${t.value}`).join(', ')}`);
818
- if (rule.description)
819
- console.log(` Description: ${rule.description}`);
820
- console.log();
821
- });
822
- }
823
- catch (error) {
824
- spinner.fail(`Failed to list tag rules: ${error.message}`);
825
- process.exit(1);
826
- }
827
- });
828
- // Create a tag rule
829
- tagsCommand
830
- .command('create-rule')
831
- .description('Create a new tag rule')
832
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
833
- .requiredOption('-n, --name <name>', 'Rule name')
834
- .requiredOption('-p, --pattern-type <type>', 'Pattern type: match_all | filename_contains | file_extension | path_contains | path_starts_with | filename_regex')
835
- .requiredOption('-T, --tag-ids <ids>', 'Comma-separated tag IDs to apply')
836
- .option('--pattern <pattern>', 'Pattern value (not required for match_all)')
837
- .option('-d, --description <text>', 'Rule description')
838
- .option('--inactive', 'Create rule as inactive (default: active)')
839
- .option('--priority <n>', 'Rule priority (lower runs first)', parseInt)
840
- .action(async (options) => {
841
- const spinner = ora('Creating tag rule...').start();
842
- try {
843
- const config = await loadConfig();
844
- const client = new ApiClient(config.api_key, config.api_url);
845
- const workspace = await resolveWorkspace(client, options.workspace);
846
- const validTypes = ['match_all', 'filename_contains', 'file_extension', 'path_contains', 'path_starts_with', 'filename_regex'];
847
- if (!validTypes.includes(options.patternType)) {
848
- spinner.fail(`Invalid pattern type. Must be one of: ${validTypes.join(', ')}`);
849
- process.exit(1);
850
- }
851
- const tagIds = options.tagIds.split(',').map((id) => id.trim()).filter(Boolean);
852
- const body = { name: options.name, patternType: options.patternType, tagIds, isActive: !options.inactive };
853
- if (options.pattern)
854
- body.pattern = options.pattern;
855
- if (options.description)
856
- body.description = options.description;
857
- if (options.priority !== undefined)
858
- body.priority = options.priority;
859
- const response = await client.post(`/workspaces/${workspace.id}/tag-rules`, body);
860
- const rule = response.data.data || response.data;
861
- spinner.stop();
862
- console.log(chalk.green('\n✓ Tag rule created!'));
863
- console.log(` ID: ${rule.id}`);
864
- console.log(` Name: ${rule.name}`);
865
- console.log(` Pattern: ${rule.patternType}${rule.pattern ? ` — ${rule.pattern}` : ''}`);
866
- console.log(` Active: ${rule.isActive}\n`);
867
- }
868
- catch (error) {
869
- spinner.fail(`Failed to create tag rule: ${error.message}`);
870
- process.exit(1);
871
- }
872
- });
873
- // Update a tag rule
874
- tagsCommand
875
- .command('update-rule')
876
- .description('Update a tag rule')
877
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
878
- .requiredOption('-i, --id <ruleId>', 'Rule ID')
879
- .option('-n, --name <name>', 'New name')
880
- .option('-d, --description <text>', 'New description')
881
- .option('-p, --pattern-type <type>', 'New pattern type')
882
- .option('--pattern <pattern>', 'New pattern value')
883
- .option('--active', 'Set rule active')
884
- .option('--inactive', 'Set rule inactive')
885
- .option('-T, --tag-ids <ids>', 'New comma-separated tag IDs')
886
- .option('--priority <n>', 'New priority', parseInt)
887
- .action(async (options) => {
888
- const spinner = ora('Updating tag rule...').start();
889
- try {
890
- const config = await loadConfig();
891
- const client = new ApiClient(config.api_key, config.api_url);
892
- const workspace = await resolveWorkspace(client, options.workspace);
893
- const body = {};
894
- if (options.name)
895
- body.name = options.name;
896
- if (options.description)
897
- body.description = options.description;
898
- if (options.patternType)
899
- body.patternType = options.patternType;
900
- if (options.pattern !== undefined)
901
- body.pattern = options.pattern;
902
- if (options.active)
903
- body.isActive = true;
904
- if (options.inactive)
905
- body.isActive = false;
906
- if (options.tagIds)
907
- body.tagIds = options.tagIds.split(',').map((id) => id.trim()).filter(Boolean);
908
- if (options.priority !== undefined)
909
- body.priority = options.priority;
910
- await client.put(`/tag-rules/${options.id}?workspaceId=${workspace.id}`, body);
911
- spinner.stop();
912
- console.log(chalk.green('\n✓ Tag rule updated successfully.\n'));
913
- }
914
- catch (error) {
915
- spinner.fail(`Failed to update tag rule: ${error.message}`);
916
- process.exit(1);
917
- }
918
- });
919
- // Delete a tag rule
920
- tagsCommand
921
- .command('delete-rule')
922
- .description('Delete a tag rule')
923
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
924
- .requiredOption('-i, --id <ruleId>', 'Rule ID')
925
- .option('--confirm', 'Skip confirmation prompt')
926
- .action(async (options) => {
927
- const spinner = ora('Resolving workspace...').start();
928
- try {
929
- const config = await loadConfig();
930
- const client = new ApiClient(config.api_key, config.api_url);
931
- const workspace = await resolveWorkspace(client, options.workspace);
932
- spinner.stop();
933
- if (!options.confirm) {
934
- console.log(chalk.yellow(`\n⚠ This will permanently delete rule ${options.id}.`));
935
- const readline = await import('readline');
936
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
937
- const answer = await new Promise((resolve) => { rl.question(' Continue? (y/N) ', resolve); });
938
- rl.close();
939
- if (answer.toLowerCase() !== 'y') {
940
- console.log(chalk.dim(' Cancelled.'));
941
- return;
942
- }
943
- }
944
- const deleteSpinner = ora('Deleting tag rule...').start();
945
- await client.delete(`/tag-rules/${options.id}?workspaceId=${workspace.id}`);
946
- deleteSpinner.stop();
947
- console.log(chalk.green('\n✓ Tag rule deleted successfully.'));
948
- }
949
- catch (error) {
950
- spinner.fail(`Failed to delete tag rule: ${error.message}`);
951
- process.exit(1);
952
- }
953
- });
954
- // Test a tag rule pattern
955
- tagsCommand
956
- .command('test-pattern')
957
- .description('Test a tag rule pattern against workspace files (dry run)')
958
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
959
- .requiredOption('-p, --pattern-type <type>', 'Pattern type: match_all | filename_contains | file_extension | path_contains | path_starts_with | filename_regex')
960
- .option('--pattern <pattern>', 'Pattern value')
961
- .option('-l, --limit <n>', 'Max matching files to show (default 10)', parseInt)
962
- .action(async (options) => {
963
- const spinner = ora('Testing pattern...').start();
964
- try {
965
- const config = await loadConfig();
966
- const client = new ApiClient(config.api_key, config.api_url);
967
- const workspace = await resolveWorkspace(client, options.workspace);
968
- const body = { patternType: options.patternType, pattern: options.pattern || '', limit: options.limit || 10 };
969
- const response = await client.post(`/workspaces/${workspace.id}/tag-rules/test`, body);
970
- const result = response.data.data || response.data;
971
- spinner.stop();
972
- const matches = result.matchingPaths || result.paths || [];
973
- if (matches.length === 0) {
974
- console.log(chalk.yellow('\nNo files matched this pattern.\n'));
975
- }
976
- else {
977
- console.log(chalk.bold(`\n${matches.length} matching file(s):\n`));
978
- matches.forEach((p) => console.log(chalk.dim(` ${p}`)));
979
- console.log();
980
- }
981
- }
982
- catch (error) {
983
- spinner.fail(`Failed to test pattern: ${error.message}`);
984
- process.exit(1);
985
- }
986
- });
987
- // Apply a tag rule retroactively to existing files
988
- tagsCommand
989
- .command('apply-rule')
990
- .description('Apply a tag rule retroactively to all existing workspace files')
991
- .requiredOption('-w, --workspace <id>', 'Workspace name, slug, or ID')
992
- .requiredOption('-i, --id <ruleId>', 'Rule ID')
993
- .option('--confirm', 'Skip confirmation prompt')
994
- .action(async (options) => {
995
- const spinner = ora('Resolving workspace...').start();
996
- try {
997
- const config = await loadConfig();
998
- const client = new ApiClient(config.api_key, config.api_url);
999
- const workspace = await resolveWorkspace(client, options.workspace);
1000
- spinner.stop();
1001
- if (!options.confirm) {
1002
- console.log(chalk.yellow('\n⚠ This will apply the rule to ALL existing files in the workspace.'));
1003
- const readline = await import('readline');
1004
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1005
- const answer = await new Promise((resolve) => { rl.question(' Continue? (y/N) ', resolve); });
1006
- rl.close();
1007
- if (answer.toLowerCase() !== 'y') {
1008
- console.log(chalk.dim(' Cancelled.'));
1009
- return;
1010
- }
1011
- }
1012
- const applySpinner = ora('Applying rule to existing files...').start();
1013
- const response = await client.post(`/tag-rules/${options.id}/apply?workspaceId=${workspace.id}`, {});
1014
- const result = response.data.data || response.data;
1015
- applySpinner.stop();
1016
- const applied = result.applied ?? result.count ?? '?';
1017
- console.log(chalk.green(`\n✓ Rule applied. Tagged ${applied} file(s).\n`));
1018
- }
1019
- catch (error) {
1020
- spinner.fail(`Failed to apply rule: ${error.message}`);
1021
- process.exit(1);
1022
- }
1023
- });
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.
1024
252
  //# sourceMappingURL=tags.js.map