@lovelybunch/cli 1.0.75-alpha.0 → 1.0.75-alpha.10
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/dist/cli.js +0 -0
- package/dist/commands/agent.d.ts +2 -2
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +173 -153
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/events.js +3 -3
- package/dist/commands/events.js.map +1 -1
- package/dist/commands/implement.d.ts.map +1 -1
- package/dist/commands/implement.js +41 -55
- package/dist/commands/implement.js.map +1 -1
- package/dist/commands/list.js +23 -23
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/propose.js +18 -18
- package/dist/commands/propose.js.map +1 -1
- package/dist/commands/resource.d.ts.map +1 -1
- package/dist/commands/resource.js +571 -2
- package/dist/commands/resource.js.map +1 -1
- package/dist/commands/show.js +43 -43
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/task.d.ts.map +1 -1
- package/dist/commands/task.js +60 -61
- package/dist/commands/task.js.map +1 -1
- package/dist/lib/registry.d.ts +22 -6
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/registry.js +153 -43
- package/dist/lib/registry.js.map +1 -1
- package/package.json +6 -5
|
@@ -3,14 +3,80 @@ import chalk from 'chalk';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import fetch, { FormData, fileFromSync } from 'node-fetch';
|
|
5
5
|
import { promises as fs } from 'fs';
|
|
6
|
-
import { writeFileSync, unlinkSync, mkdtempSync } from 'fs';
|
|
6
|
+
import { writeFileSync, unlinkSync, mkdtempSync, existsSync } from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
9
|
+
import inquirer from 'inquirer';
|
|
9
10
|
// ============================================================================
|
|
10
11
|
// Shared Utilities
|
|
11
12
|
// ============================================================================
|
|
12
13
|
const DEFAULT_API_BASE = 'http://localhost';
|
|
13
14
|
const GENERATION_TIMEOUT = 300_000; // 5 minutes
|
|
15
|
+
/**
|
|
16
|
+
* Determine MIME type from file extension
|
|
17
|
+
*/
|
|
18
|
+
function getMimeType(filePath) {
|
|
19
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
20
|
+
const mimeTypes = {
|
|
21
|
+
// Images
|
|
22
|
+
'.png': 'image/png',
|
|
23
|
+
'.jpg': 'image/jpeg',
|
|
24
|
+
'.jpeg': 'image/jpeg',
|
|
25
|
+
'.gif': 'image/gif',
|
|
26
|
+
'.webp': 'image/webp',
|
|
27
|
+
'.svg': 'image/svg+xml',
|
|
28
|
+
'.bmp': 'image/bmp',
|
|
29
|
+
'.ico': 'image/x-icon',
|
|
30
|
+
'.tiff': 'image/tiff',
|
|
31
|
+
'.tif': 'image/tiff',
|
|
32
|
+
'.avif': 'image/avif',
|
|
33
|
+
// Audio
|
|
34
|
+
'.mp3': 'audio/mpeg',
|
|
35
|
+
'.wav': 'audio/wav',
|
|
36
|
+
'.ogg': 'audio/ogg',
|
|
37
|
+
'.flac': 'audio/flac',
|
|
38
|
+
'.aac': 'audio/aac',
|
|
39
|
+
'.m4a': 'audio/mp4',
|
|
40
|
+
'.wma': 'audio/x-ms-wma',
|
|
41
|
+
'.opus': 'audio/opus',
|
|
42
|
+
// Video
|
|
43
|
+
'.mp4': 'video/mp4',
|
|
44
|
+
'.webm': 'video/webm',
|
|
45
|
+
'.mov': 'video/quicktime',
|
|
46
|
+
'.avi': 'video/x-msvideo',
|
|
47
|
+
'.mkv': 'video/x-matroska',
|
|
48
|
+
'.flv': 'video/x-flv',
|
|
49
|
+
'.wmv': 'video/x-ms-wmv',
|
|
50
|
+
'.m4v': 'video/mp4',
|
|
51
|
+
// Documents
|
|
52
|
+
'.pdf': 'application/pdf',
|
|
53
|
+
'.json': 'application/json',
|
|
54
|
+
'.xml': 'application/xml',
|
|
55
|
+
'.csv': 'text/csv',
|
|
56
|
+
'.txt': 'text/plain',
|
|
57
|
+
'.md': 'text/markdown',
|
|
58
|
+
'.html': 'text/html',
|
|
59
|
+
'.css': 'text/css',
|
|
60
|
+
'.js': 'text/javascript',
|
|
61
|
+
'.ts': 'text/typescript',
|
|
62
|
+
// Archives
|
|
63
|
+
'.zip': 'application/zip',
|
|
64
|
+
'.gz': 'application/gzip',
|
|
65
|
+
'.tar': 'application/x-tar',
|
|
66
|
+
};
|
|
67
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Format file size in bytes to human-readable string
|
|
71
|
+
*/
|
|
72
|
+
function formatFileSize(bytes) {
|
|
73
|
+
if (bytes === 0)
|
|
74
|
+
return '0 B';
|
|
75
|
+
const k = 1024;
|
|
76
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
77
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
78
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
79
|
+
}
|
|
14
80
|
/**
|
|
15
81
|
* Check if the Coconut server is running
|
|
16
82
|
*/
|
|
@@ -557,12 +623,515 @@ configured via "nut config set replicate <token>".
|
|
|
557
623
|
}
|
|
558
624
|
});
|
|
559
625
|
// ============================================================================
|
|
626
|
+
// List Resources Command
|
|
627
|
+
// ============================================================================
|
|
628
|
+
const listResourcesCommand = new Command('list')
|
|
629
|
+
.description('List all resources')
|
|
630
|
+
.alias('ls')
|
|
631
|
+
.option('-s, --search <query>', 'Filter resources by name, description, or tags')
|
|
632
|
+
.option('--type <type>', 'Filter by media type prefix (e.g. image, audio, video)')
|
|
633
|
+
.option('--json', 'Output as JSON')
|
|
634
|
+
.option('--api <url>', 'API server URL', DEFAULT_API_BASE)
|
|
635
|
+
.addHelpText('after', `
|
|
636
|
+
Examples:
|
|
637
|
+
$ nut resource list
|
|
638
|
+
$ nut resource ls --search "logo"
|
|
639
|
+
$ nut resource list --type image --json
|
|
640
|
+
`)
|
|
641
|
+
.action(async (options) => {
|
|
642
|
+
const isHealthy = await checkServerHealth(options.api);
|
|
643
|
+
if (!isHealthy) {
|
|
644
|
+
console.error(chalk.red('Error: Coconut server is not running.'));
|
|
645
|
+
console.error(chalk.yellow('Start the server with "nut serve" or specify a different URL with --api'));
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
const spinner = ora('Loading resources...').start();
|
|
649
|
+
try {
|
|
650
|
+
const response = await fetch(`${options.api}/api/v1/resources`);
|
|
651
|
+
const result = await response.json();
|
|
652
|
+
if (!response.ok || !result.success || !result.data) {
|
|
653
|
+
spinner.fail('Failed to load resources');
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
let resources = result.data;
|
|
657
|
+
// Apply type filter
|
|
658
|
+
if (options.type) {
|
|
659
|
+
const typePrefix = options.type.toLowerCase();
|
|
660
|
+
resources = resources.filter(r => r.type.toLowerCase().startsWith(typePrefix));
|
|
661
|
+
}
|
|
662
|
+
// Apply search filter (client-side, matching frontend behavior)
|
|
663
|
+
if (options.search) {
|
|
664
|
+
const query = options.search.toLowerCase();
|
|
665
|
+
resources = resources.filter(r => r.name.toLowerCase().includes(query) ||
|
|
666
|
+
r.metadata.description?.toLowerCase().includes(query) ||
|
|
667
|
+
r.metadata.tags?.some(tag => tag.toLowerCase().includes(query)));
|
|
668
|
+
}
|
|
669
|
+
spinner.stop();
|
|
670
|
+
if (options.json) {
|
|
671
|
+
console.log(JSON.stringify(resources, null, 2));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (resources.length === 0) {
|
|
675
|
+
if (options.search || options.type) {
|
|
676
|
+
console.log(chalk.yellow('No resources found matching your filters.'));
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
console.log(chalk.yellow('No resources found.'));
|
|
680
|
+
console.log(chalk.gray('Use "nut resource add <file>" to upload one.'));
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
console.log('\n' + chalk.bold.underline(`Resources (${resources.length})`));
|
|
685
|
+
console.log();
|
|
686
|
+
for (const r of resources) {
|
|
687
|
+
console.log(chalk.cyan('●') + ' ' + chalk.bold(r.name) + chalk.gray(` (${r.id})`));
|
|
688
|
+
console.log(' ' + chalk.gray(`Type: ${r.type} Size: ${formatFileSize(r.size)} Uploaded: ${new Date(r.uploadedAt).toLocaleDateString()}`));
|
|
689
|
+
if (r.metadata.description) {
|
|
690
|
+
const desc = r.metadata.description.length > 80
|
|
691
|
+
? r.metadata.description.slice(0, 80).trimEnd() + '...'
|
|
692
|
+
: r.metadata.description;
|
|
693
|
+
console.log(' ' + chalk.gray(`Description: ${desc}`));
|
|
694
|
+
}
|
|
695
|
+
if (r.metadata.tags && r.metadata.tags.length > 0) {
|
|
696
|
+
console.log(' ' + chalk.gray(`Tags: ${r.metadata.tags.join(', ')}`));
|
|
697
|
+
}
|
|
698
|
+
console.log();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
spinner.fail('Failed to load resources');
|
|
703
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
704
|
+
process.exit(1);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
// ============================================================================
|
|
708
|
+
// Get Resource Command
|
|
709
|
+
// ============================================================================
|
|
710
|
+
const getResourceCommand = new Command('get')
|
|
711
|
+
.description('Get details for a specific resource')
|
|
712
|
+
.alias('show')
|
|
713
|
+
.argument('<id>', 'Resource ID')
|
|
714
|
+
.option('--json', 'Output as JSON')
|
|
715
|
+
.option('-o, --output <path>', 'Download resource file to local path')
|
|
716
|
+
.option('--api <url>', 'API server URL', DEFAULT_API_BASE)
|
|
717
|
+
.addHelpText('after', `
|
|
718
|
+
Examples:
|
|
719
|
+
$ nut resource get res-1234567890-abc123def
|
|
720
|
+
$ nut resource show res-1234567890-abc123def --json
|
|
721
|
+
$ nut resource get res-1234567890-abc123def -o ./downloaded-file.png
|
|
722
|
+
`)
|
|
723
|
+
.action(async (id, options) => {
|
|
724
|
+
const isHealthy = await checkServerHealth(options.api);
|
|
725
|
+
if (!isHealthy) {
|
|
726
|
+
console.error(chalk.red('Error: Coconut server is not running.'));
|
|
727
|
+
console.error(chalk.yellow('Start the server with "nut serve" or specify a different URL with --api'));
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
const spinner = ora('Loading resource...').start();
|
|
731
|
+
try {
|
|
732
|
+
// Fetch all resources and find by ID (the list endpoint returns full metadata)
|
|
733
|
+
const response = await fetch(`${options.api}/api/v1/resources`);
|
|
734
|
+
const result = await response.json();
|
|
735
|
+
if (!response.ok || !result.success || !result.data) {
|
|
736
|
+
spinner.fail('Failed to load resources');
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
const resource = result.data.find(r => r.id === id);
|
|
740
|
+
if (!resource) {
|
|
741
|
+
spinner.fail(`Resource '${id}' not found`);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
spinner.stop();
|
|
745
|
+
// Handle download
|
|
746
|
+
if (options.output) {
|
|
747
|
+
const downloadSpinner = ora(`Downloading to ${options.output}...`).start();
|
|
748
|
+
try {
|
|
749
|
+
await downloadToFile(`${options.api}/api/v1/resources/${id}?download=true`, options.output);
|
|
750
|
+
downloadSpinner.succeed(`Downloaded to ${chalk.cyan(path.resolve(options.output))}`);
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
downloadSpinner.fail('Failed to download');
|
|
754
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (options.json) {
|
|
760
|
+
console.log(JSON.stringify(resource, null, 2));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
// Display formatted details
|
|
764
|
+
console.log();
|
|
765
|
+
console.log(chalk.bold.cyan(resource.name));
|
|
766
|
+
console.log();
|
|
767
|
+
console.log(` ${chalk.gray('ID:')} ${resource.id}`);
|
|
768
|
+
console.log(` ${chalk.gray('Type:')} ${resource.type}`);
|
|
769
|
+
console.log(` ${chalk.gray('Size:')} ${formatFileSize(resource.size)}`);
|
|
770
|
+
console.log(` ${chalk.gray('Uploaded:')} ${new Date(resource.uploadedAt).toLocaleString()}`);
|
|
771
|
+
if (resource.metadata.tags && resource.metadata.tags.length > 0) {
|
|
772
|
+
console.log(` ${chalk.gray('Tags:')} ${resource.metadata.tags.join(', ')}`);
|
|
773
|
+
}
|
|
774
|
+
if (resource.metadata.description) {
|
|
775
|
+
console.log(` ${chalk.gray('Description:')} ${resource.metadata.description}`);
|
|
776
|
+
}
|
|
777
|
+
if (resource.metadata.generationPrompt) {
|
|
778
|
+
console.log(` ${chalk.gray('Prompt:')} ${resource.metadata.generationPrompt}`);
|
|
779
|
+
}
|
|
780
|
+
if (resource.metadata.generationModel) {
|
|
781
|
+
console.log(` ${chalk.gray('Model:')} ${resource.metadata.generationModel}`);
|
|
782
|
+
}
|
|
783
|
+
console.log();
|
|
784
|
+
console.log(chalk.bold('Paths:'));
|
|
785
|
+
console.log(` ${chalk.gray('File:')} .nut/resources/files/${resource.path}`);
|
|
786
|
+
if (resource.thumbnailPath) {
|
|
787
|
+
console.log(` ${chalk.gray('Thumbnail:')} .nut/resources/thumbnails/${resource.thumbnailPath}`);
|
|
788
|
+
}
|
|
789
|
+
console.log(` ${chalk.gray('Metadata:')} .nut/resources/metadata/${resource.id}.json`);
|
|
790
|
+
console.log();
|
|
791
|
+
}
|
|
792
|
+
catch (error) {
|
|
793
|
+
spinner.fail('Failed to load resource');
|
|
794
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
// ============================================================================
|
|
799
|
+
// Add Resource Command
|
|
800
|
+
// ============================================================================
|
|
801
|
+
/**
|
|
802
|
+
* Check if a string looks like a URL
|
|
803
|
+
*/
|
|
804
|
+
function isUrl(input) {
|
|
805
|
+
return /^https?:\/\//i.test(input);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Download a URL to a temp file and return the temp path, filename, and MIME type.
|
|
809
|
+
* Caller is responsible for cleaning up the temp file/directory.
|
|
810
|
+
*/
|
|
811
|
+
async function downloadUrlToTemp(url) {
|
|
812
|
+
const response = await fetch(url);
|
|
813
|
+
if (!response.ok) {
|
|
814
|
+
throw new Error(`Failed to download ${url}: ${response.statusText}`);
|
|
815
|
+
}
|
|
816
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
817
|
+
const contentType = response.headers.get('content-type') || '';
|
|
818
|
+
// Derive a filename from the URL path, falling back to a timestamp-based name
|
|
819
|
+
const urlPath = new URL(url).pathname;
|
|
820
|
+
let fileName = path.basename(urlPath);
|
|
821
|
+
if (!fileName || fileName === '/' || !path.extname(fileName)) {
|
|
822
|
+
// Try to infer extension from content-type
|
|
823
|
+
const extMap = {
|
|
824
|
+
'image/png': '.png',
|
|
825
|
+
'image/jpeg': '.jpg',
|
|
826
|
+
'image/webp': '.webp',
|
|
827
|
+
'image/gif': '.gif',
|
|
828
|
+
'image/svg+xml': '.svg',
|
|
829
|
+
'audio/mpeg': '.mp3',
|
|
830
|
+
'audio/wav': '.wav',
|
|
831
|
+
'audio/ogg': '.ogg',
|
|
832
|
+
'video/mp4': '.mp4',
|
|
833
|
+
'video/webm': '.webm',
|
|
834
|
+
'application/pdf': '.pdf',
|
|
835
|
+
'application/json': '.json',
|
|
836
|
+
};
|
|
837
|
+
const ext = Object.entries(extMap).find(([ct]) => contentType.includes(ct))?.[1] || '.bin';
|
|
838
|
+
fileName = `downloaded-${Date.now()}${ext}`;
|
|
839
|
+
}
|
|
840
|
+
const mimeType = contentType.split(';')[0].trim() || getMimeType(fileName);
|
|
841
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), 'coconut-add-'));
|
|
842
|
+
const tempFilePath = path.join(tempDir, fileName);
|
|
843
|
+
writeFileSync(tempFilePath, buffer);
|
|
844
|
+
return { tempDir, tempFilePath, fileName, mimeType };
|
|
845
|
+
}
|
|
846
|
+
const addResourceCommand = new Command('add')
|
|
847
|
+
.description('Upload a file or URL to resources')
|
|
848
|
+
.alias('upload')
|
|
849
|
+
.argument('<file...>', 'File path(s) or URL(s) to upload')
|
|
850
|
+
.option('-t, --tags <tags>', 'Comma-separated tags')
|
|
851
|
+
.option('-d, --description <text>', 'Description of the resource')
|
|
852
|
+
.option('--json', 'Output as JSON')
|
|
853
|
+
.option('--api <url>', 'API server URL', DEFAULT_API_BASE)
|
|
854
|
+
.addHelpText('after', `
|
|
855
|
+
Examples:
|
|
856
|
+
$ nut resource add ./screenshot.png
|
|
857
|
+
$ nut resource upload ./logo.svg -t "branding, logo" -d "Company logo"
|
|
858
|
+
$ nut resource add ./image1.png ./image2.png -t "batch"
|
|
859
|
+
$ nut resource add https://example.com/photo.png
|
|
860
|
+
$ nut resource add ./data.json --json
|
|
861
|
+
`)
|
|
862
|
+
.action(async (files, options) => {
|
|
863
|
+
const isHealthy = await checkServerHealth(options.api);
|
|
864
|
+
if (!isHealthy) {
|
|
865
|
+
console.error(chalk.red('Error: Coconut server is not running.'));
|
|
866
|
+
console.error(chalk.yellow('Start the server with "nut serve" or specify a different URL with --api'));
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
const results = [];
|
|
870
|
+
for (const filePath of files) {
|
|
871
|
+
let localPath;
|
|
872
|
+
let fileName;
|
|
873
|
+
let mimeType;
|
|
874
|
+
let tempDir = null;
|
|
875
|
+
if (isUrl(filePath)) {
|
|
876
|
+
// ── URL: download to temp file first ──
|
|
877
|
+
const spinner = ora(`Downloading ${filePath}...`).start();
|
|
878
|
+
try {
|
|
879
|
+
const downloaded = await downloadUrlToTemp(filePath);
|
|
880
|
+
tempDir = downloaded.tempDir;
|
|
881
|
+
localPath = downloaded.tempFilePath;
|
|
882
|
+
fileName = downloaded.fileName;
|
|
883
|
+
mimeType = downloaded.mimeType;
|
|
884
|
+
spinner.succeed(`Downloaded ${chalk.cyan(fileName)}`);
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
spinner.fail('Failed to download URL');
|
|
888
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
// ── Local file ──
|
|
894
|
+
localPath = path.resolve(filePath);
|
|
895
|
+
if (!existsSync(localPath)) {
|
|
896
|
+
console.error(chalk.red(`File not found: ${localPath}`));
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
fileName = path.basename(filePath);
|
|
900
|
+
mimeType = getMimeType(localPath);
|
|
901
|
+
}
|
|
902
|
+
const uploadSpinner = ora(`Uploading ${fileName}...`).start();
|
|
903
|
+
try {
|
|
904
|
+
const formData = new FormData();
|
|
905
|
+
const file = fileFromSync(localPath, mimeType);
|
|
906
|
+
formData.set('file', file, fileName);
|
|
907
|
+
if (options.tags) {
|
|
908
|
+
formData.set('tags', options.tags);
|
|
909
|
+
}
|
|
910
|
+
if (options.description) {
|
|
911
|
+
formData.set('description', options.description);
|
|
912
|
+
}
|
|
913
|
+
const response = await fetch(`${options.api}/api/v1/resources`, {
|
|
914
|
+
method: 'POST',
|
|
915
|
+
body: formData,
|
|
916
|
+
});
|
|
917
|
+
const result = await response.json();
|
|
918
|
+
if (!response.ok || !result.success || !result.data) {
|
|
919
|
+
const errorMessage = result.error?.message || 'Upload failed';
|
|
920
|
+
uploadSpinner.fail(`Failed to upload ${fileName}`);
|
|
921
|
+
console.error(chalk.red(errorMessage));
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
const resource = result.data;
|
|
925
|
+
results.push(resource);
|
|
926
|
+
uploadSpinner.succeed(`Uploaded ${chalk.cyan(resource.name)}`);
|
|
927
|
+
if (!options.json) {
|
|
928
|
+
console.log();
|
|
929
|
+
console.log(` ${chalk.gray('ID:')} ${resource.id}`);
|
|
930
|
+
console.log(` ${chalk.gray('Name:')} ${resource.name}`);
|
|
931
|
+
console.log(` ${chalk.gray('Type:')} ${resource.type}`);
|
|
932
|
+
console.log(` ${chalk.gray('Size:')} ${formatFileSize(resource.size)}`);
|
|
933
|
+
if (resource.metadata.tags && resource.metadata.tags.length > 0) {
|
|
934
|
+
console.log(` ${chalk.gray('Tags:')} ${resource.metadata.tags.join(', ')}`);
|
|
935
|
+
}
|
|
936
|
+
if (resource.metadata.description) {
|
|
937
|
+
console.log(` ${chalk.gray('Desc:')} ${resource.metadata.description}`);
|
|
938
|
+
}
|
|
939
|
+
console.log(` ${chalk.gray('File:')} .nut/resources/files/${resource.path}`);
|
|
940
|
+
if (resource.thumbnailPath) {
|
|
941
|
+
console.log(` ${chalk.gray('Thumbnail:')} .nut/resources/thumbnails/${resource.thumbnailPath}`);
|
|
942
|
+
}
|
|
943
|
+
console.log(` ${chalk.gray('Metadata:')} .nut/resources/metadata/${resource.id}.json`);
|
|
944
|
+
console.log();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
uploadSpinner.fail(`Failed to upload ${fileName}`);
|
|
949
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
finally {
|
|
953
|
+
// Clean up temp directory if we downloaded from a URL
|
|
954
|
+
if (tempDir) {
|
|
955
|
+
try {
|
|
956
|
+
unlinkSync(localPath);
|
|
957
|
+
fs.rmdir(tempDir).catch(() => { });
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
// Ignore cleanup errors
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (options.json) {
|
|
966
|
+
console.log(JSON.stringify(results.length === 1 ? results[0] : results, null, 2));
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
// ============================================================================
|
|
970
|
+
// Update Resource Command
|
|
971
|
+
// ============================================================================
|
|
972
|
+
const updateResourceCommand = new Command('update')
|
|
973
|
+
.description('Update resource metadata (tags, description)')
|
|
974
|
+
.argument('<id>', 'Resource ID to update')
|
|
975
|
+
.option('-t, --tags <tags>', 'Comma-separated tags (replaces existing)')
|
|
976
|
+
.option('-d, --description <text>', 'Description (replaces existing)')
|
|
977
|
+
.option('--json', 'Output as JSON')
|
|
978
|
+
.option('--api <url>', 'API server URL', DEFAULT_API_BASE)
|
|
979
|
+
.addHelpText('after', `
|
|
980
|
+
Examples:
|
|
981
|
+
$ nut resource update res-123 -t "logo, branding"
|
|
982
|
+
$ nut resource update res-123 -d "Updated description"
|
|
983
|
+
$ nut resource update res-123 -t "new-tag" -d "New desc" --json
|
|
984
|
+
`)
|
|
985
|
+
.action(async (id, options) => {
|
|
986
|
+
if (!options.tags && !options.description) {
|
|
987
|
+
console.error(chalk.red('Error: At least one of --tags or --description must be provided.'));
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
const isHealthy = await checkServerHealth(options.api);
|
|
991
|
+
if (!isHealthy) {
|
|
992
|
+
console.error(chalk.red('Error: Coconut server is not running.'));
|
|
993
|
+
console.error(chalk.yellow('Start the server with "nut serve" or specify a different URL with --api'));
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
const spinner = ora('Updating resource...').start();
|
|
997
|
+
try {
|
|
998
|
+
const metadata = {};
|
|
999
|
+
if (options.tags !== undefined) {
|
|
1000
|
+
metadata.tags = options.tags.split(',').map(t => t.trim());
|
|
1001
|
+
}
|
|
1002
|
+
if (options.description !== undefined) {
|
|
1003
|
+
metadata.description = options.description;
|
|
1004
|
+
}
|
|
1005
|
+
const response = await fetch(`${options.api}/api/v1/resources/${id}`, {
|
|
1006
|
+
method: 'PUT',
|
|
1007
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1008
|
+
body: JSON.stringify({ metadata }),
|
|
1009
|
+
});
|
|
1010
|
+
const result = await response.json();
|
|
1011
|
+
if (!response.ok || !result.success || !result.data) {
|
|
1012
|
+
const errorMessage = result.error?.message || 'Update failed';
|
|
1013
|
+
spinner.fail('Failed to update resource');
|
|
1014
|
+
console.error(chalk.red(errorMessage));
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
const resource = result.data;
|
|
1018
|
+
spinner.succeed(`Updated ${chalk.cyan(resource.name)}`);
|
|
1019
|
+
if (options.json) {
|
|
1020
|
+
console.log(JSON.stringify(resource, null, 2));
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
console.log();
|
|
1024
|
+
console.log(` ${chalk.gray('ID:')} ${resource.id}`);
|
|
1025
|
+
console.log(` ${chalk.gray('Name:')} ${resource.name}`);
|
|
1026
|
+
if (resource.metadata.tags && resource.metadata.tags.length > 0) {
|
|
1027
|
+
console.log(` ${chalk.gray('Tags:')} ${resource.metadata.tags.join(', ')}`);
|
|
1028
|
+
}
|
|
1029
|
+
if (resource.metadata.description) {
|
|
1030
|
+
console.log(` ${chalk.gray('Description:')} ${resource.metadata.description}`);
|
|
1031
|
+
}
|
|
1032
|
+
console.log();
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
spinner.fail('Failed to update resource');
|
|
1037
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
// ============================================================================
|
|
1042
|
+
// Delete Resource Command
|
|
1043
|
+
// ============================================================================
|
|
1044
|
+
const deleteResourceCommand = new Command('delete')
|
|
1045
|
+
.description('Delete a resource')
|
|
1046
|
+
.alias('rm')
|
|
1047
|
+
.argument('<id>', 'Resource ID to delete')
|
|
1048
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
1049
|
+
.option('--json', 'Output as JSON')
|
|
1050
|
+
.option('--api <url>', 'API server URL', DEFAULT_API_BASE)
|
|
1051
|
+
.addHelpText('after', `
|
|
1052
|
+
Examples:
|
|
1053
|
+
$ nut resource delete res-1234567890-abc123def
|
|
1054
|
+
$ nut resource rm res-1234567890-abc123def --force
|
|
1055
|
+
$ nut resource delete res-123 --json
|
|
1056
|
+
`)
|
|
1057
|
+
.action(async (id, options) => {
|
|
1058
|
+
const isHealthy = await checkServerHealth(options.api);
|
|
1059
|
+
if (!isHealthy) {
|
|
1060
|
+
console.error(chalk.red('Error: Coconut server is not running.'));
|
|
1061
|
+
console.error(chalk.yellow('Start the server with "nut serve" or specify a different URL with --api'));
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
// Fetch resource details first to show name in confirmation
|
|
1065
|
+
const listSpinner = ora('Loading resource...').start();
|
|
1066
|
+
let resourceName = id;
|
|
1067
|
+
try {
|
|
1068
|
+
const listResponse = await fetch(`${options.api}/api/v1/resources`);
|
|
1069
|
+
const listResult = await listResponse.json();
|
|
1070
|
+
if (listResult.success && listResult.data) {
|
|
1071
|
+
const found = listResult.data.find(r => r.id === id);
|
|
1072
|
+
if (found) {
|
|
1073
|
+
resourceName = found.name;
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
listSpinner.fail(`Resource '${id}' not found`);
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
listSpinner.stop();
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
listSpinner.stop();
|
|
1084
|
+
// Proceed anyway; the delete endpoint will 404 if not found
|
|
1085
|
+
}
|
|
1086
|
+
// Confirm deletion
|
|
1087
|
+
if (!options.force) {
|
|
1088
|
+
const { confirm } = await inquirer.prompt([{
|
|
1089
|
+
type: 'confirm',
|
|
1090
|
+
name: 'confirm',
|
|
1091
|
+
message: `Delete resource "${resourceName}" (${id})?`,
|
|
1092
|
+
default: false,
|
|
1093
|
+
}]);
|
|
1094
|
+
if (!confirm) {
|
|
1095
|
+
console.log(chalk.yellow('Operation cancelled.'));
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const spinner = ora('Deleting resource...').start();
|
|
1100
|
+
try {
|
|
1101
|
+
const response = await fetch(`${options.api}/api/v1/resources/${id}`, {
|
|
1102
|
+
method: 'DELETE',
|
|
1103
|
+
});
|
|
1104
|
+
const result = await response.json();
|
|
1105
|
+
if (!response.ok || !result.success) {
|
|
1106
|
+
const errorMessage = result.error?.message || 'Delete failed';
|
|
1107
|
+
spinner.fail('Failed to delete resource');
|
|
1108
|
+
console.error(chalk.red(errorMessage));
|
|
1109
|
+
process.exit(1);
|
|
1110
|
+
}
|
|
1111
|
+
spinner.succeed(`Deleted ${chalk.cyan(resourceName)}`);
|
|
1112
|
+
if (options.json) {
|
|
1113
|
+
console.log(JSON.stringify({ success: true, id, name: resourceName }, null, 2));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch (error) {
|
|
1117
|
+
spinner.fail('Failed to delete resource');
|
|
1118
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error'));
|
|
1119
|
+
process.exit(1);
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
// ============================================================================
|
|
560
1123
|
// Parent Resource Command
|
|
561
1124
|
// ============================================================================
|
|
562
1125
|
const resource = new Command('resource')
|
|
563
1126
|
.description('Manage and generate resources (images, audio, video)')
|
|
564
1127
|
.alias('resources');
|
|
565
|
-
// Add subcommands
|
|
1128
|
+
// Add CRUD subcommands
|
|
1129
|
+
resource.addCommand(listResourcesCommand);
|
|
1130
|
+
resource.addCommand(getResourceCommand);
|
|
1131
|
+
resource.addCommand(addResourceCommand);
|
|
1132
|
+
resource.addCommand(updateResourceCommand);
|
|
1133
|
+
resource.addCommand(deleteResourceCommand);
|
|
1134
|
+
// Add generation subcommands
|
|
566
1135
|
resource.addCommand(generateImageCommand);
|
|
567
1136
|
resource.addCommand(generateAudioCommand);
|
|
568
1137
|
resource.addCommand(generateVideoCommand);
|