netlibrary-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/netlibrary.js +33 -0
- package/commands/agents.js +173 -0
- package/commands/archive.js +40 -0
- package/commands/comments.js +49 -0
- package/commands/config.js +71 -0
- package/commands/embeds.js +120 -0
- package/commands/info.js +57 -0
- package/commands/library.js +158 -0
- package/commands/member.js +211 -0
- package/commands/search.js +63 -0
- package/commands/stacks.js +210 -0
- package/commands/stats.js +36 -0
- package/commands/tasks.js +78 -0
- package/lib/api.js +58 -0
- package/lib/config.js +39 -0
- package/lib/output.js +66 -0
- package/lib/payment.js +114 -0
- package/package.json +28 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const api = require('../lib/api');
|
|
4
|
+
const output = require('../lib/output');
|
|
5
|
+
|
|
6
|
+
module.exports = function (program) {
|
|
7
|
+
const lib = program.command('library').description('Browse and manage library items');
|
|
8
|
+
|
|
9
|
+
lib
|
|
10
|
+
.command('browse')
|
|
11
|
+
.description('Browse the library catalog')
|
|
12
|
+
.option('-p, --page <n>', 'Page number', '1')
|
|
13
|
+
.option('-l, --limit <n>', 'Items per page (max 50)', '20')
|
|
14
|
+
.option('-c, --category <cat>', 'Filter by category')
|
|
15
|
+
.option('-m, --media-type <type>', 'Filter by media type')
|
|
16
|
+
.option('-s, --sort <sort>', 'Sort: newest, oldest, title, author', 'newest')
|
|
17
|
+
.option('--search <query>', 'Search within results')
|
|
18
|
+
.option('--operator <address>', 'Filter by uploader address')
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
try {
|
|
21
|
+
const data = await api.get('/library', {
|
|
22
|
+
query: {
|
|
23
|
+
page: opts.page,
|
|
24
|
+
limit: opts.limit,
|
|
25
|
+
category: opts.category,
|
|
26
|
+
mediaType: opts.mediaType,
|
|
27
|
+
sort: opts.sort,
|
|
28
|
+
search: opts.search,
|
|
29
|
+
operator: opts.operator,
|
|
30
|
+
},
|
|
31
|
+
auth: false,
|
|
32
|
+
});
|
|
33
|
+
output.table(
|
|
34
|
+
['Title', 'Author', 'Type', 'Categories', 'Key'],
|
|
35
|
+
(data.items || data.entries || []).map(i => [
|
|
36
|
+
i.title || '—',
|
|
37
|
+
i.author || '—',
|
|
38
|
+
i.mediaType || '—',
|
|
39
|
+
(i.categories || []).join(', '),
|
|
40
|
+
i.contentKey,
|
|
41
|
+
])
|
|
42
|
+
);
|
|
43
|
+
output.pagination(data.pagination);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
output.error(err.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
lib
|
|
51
|
+
.command('get <contentKey>')
|
|
52
|
+
.description('Get a single library item')
|
|
53
|
+
.action(async (contentKey) => {
|
|
54
|
+
try {
|
|
55
|
+
const data = await api.get(`/library/${encodeURIComponent(contentKey)}`, { auth: false });
|
|
56
|
+
const entry = data.entry || data;
|
|
57
|
+
output.item(entry, [
|
|
58
|
+
['Title', 'title'],
|
|
59
|
+
['Author', 'author'],
|
|
60
|
+
['Content Key', 'contentKey'],
|
|
61
|
+
['Media Type', 'mediaType'],
|
|
62
|
+
['Categories', 'categories', v => (v || []).join(', ')],
|
|
63
|
+
['File Size', 'fileSize', v => v ? `${(v / 1024 / 1024).toFixed(2)} MB` : null],
|
|
64
|
+
['CDN URL', 'cdnUrl'],
|
|
65
|
+
['Uploaded', 'uploadedAt', v => v ? new Date(v).toLocaleString() : null],
|
|
66
|
+
['Uploader', 'uploaderUsername'],
|
|
67
|
+
]);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
output.error(err.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
lib
|
|
75
|
+
.command('write')
|
|
76
|
+
.description('Register a library item (metadata only)')
|
|
77
|
+
.requiredOption('-t, --title <title>', 'Item title')
|
|
78
|
+
.option('-k, --content-key <key>', 'Net Protocol content key')
|
|
79
|
+
.option('-u, --cdn-url <url>', 'CDN URL')
|
|
80
|
+
.option('-a, --author <author>', 'Author name')
|
|
81
|
+
.option('-c, --category <cat...>', 'Categories')
|
|
82
|
+
.option('-m, --media-type <type>', 'Media type')
|
|
83
|
+
.option('--cover-url <url>', 'Cover image URL')
|
|
84
|
+
.option('--file-name <name>', 'Original file name')
|
|
85
|
+
.option('--file-size <bytes>', 'File size in bytes')
|
|
86
|
+
.option('--isbn <isbn>', 'ISBN')
|
|
87
|
+
.option('--year <year>', 'Publication year')
|
|
88
|
+
.option('--publisher <pub>', 'Publisher')
|
|
89
|
+
.option('--add-to-stack <stackId>', 'Add to stack after registering')
|
|
90
|
+
.action(async (opts) => {
|
|
91
|
+
try {
|
|
92
|
+
if (!opts.contentKey && !opts.cdnUrl) {
|
|
93
|
+
output.error('Either --content-key or --cdn-url is required');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const body = {};
|
|
97
|
+
if (opts.contentKey) body.contentKey = opts.contentKey;
|
|
98
|
+
if (opts.cdnUrl) body.cdnUrl = opts.cdnUrl;
|
|
99
|
+
body.title = opts.title;
|
|
100
|
+
if (opts.author) body.author = opts.author;
|
|
101
|
+
if (opts.category) body.categories = opts.category;
|
|
102
|
+
if (opts.mediaType) body.mediaType = opts.mediaType;
|
|
103
|
+
if (opts.coverUrl) body.coverUrl = opts.coverUrl;
|
|
104
|
+
if (opts.fileName) body.fileName = opts.fileName;
|
|
105
|
+
if (opts.fileSize) body.fileSize = parseInt(opts.fileSize);
|
|
106
|
+
if (opts.isbn) body.isbn = opts.isbn;
|
|
107
|
+
if (opts.year) body.publicationYear = parseInt(opts.year);
|
|
108
|
+
if (opts.publisher) body.publisher = opts.publisher;
|
|
109
|
+
if (opts.addToStack) body.addToStack = opts.addToStack;
|
|
110
|
+
|
|
111
|
+
const data = await api.post('/library/write', body);
|
|
112
|
+
output.success(`Item registered: ${data.contentKey || opts.contentKey}`);
|
|
113
|
+
if (output.isJsonMode()) output.json(data);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
output.error(err.message);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
lib
|
|
121
|
+
.command('upload <file>')
|
|
122
|
+
.description('Upload a file to the library')
|
|
123
|
+
.requiredOption('-t, --title <title>', 'Item title')
|
|
124
|
+
.option('-a, --author <author>', 'Author name')
|
|
125
|
+
.option('-c, --category <cat...>', 'Categories')
|
|
126
|
+
.option('--add-to-stack <stackId>', 'Add to stack after upload')
|
|
127
|
+
.action(async (file, opts) => {
|
|
128
|
+
try {
|
|
129
|
+
const filePath = path.resolve(file);
|
|
130
|
+
if (!fs.existsSync(filePath)) {
|
|
131
|
+
output.error(`File not found: ${filePath}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
136
|
+
const fileName = path.basename(filePath);
|
|
137
|
+
const sizeMB = (fileBuffer.length / 1024 / 1024).toFixed(2);
|
|
138
|
+
|
|
139
|
+
if (!output.isJsonMode()) {
|
|
140
|
+
console.log(`Uploading ${fileName} (${sizeMB} MB)...`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const form = new FormData();
|
|
144
|
+
form.append('file', new Blob([fileBuffer]), fileName);
|
|
145
|
+
form.append('title', opts.title);
|
|
146
|
+
if (opts.author) form.append('author', opts.author);
|
|
147
|
+
if (opts.category) opts.category.forEach(c => form.append('categories', c));
|
|
148
|
+
if (opts.addToStack) form.append('addToStack', opts.addToStack);
|
|
149
|
+
|
|
150
|
+
const data = await api.post('/library/upload', form);
|
|
151
|
+
output.success(`Uploaded: ${data.contentKey || fileName}`);
|
|
152
|
+
if (output.isJsonMode()) output.json(data);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
output.error(err.message);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const api = require('../lib/api');
|
|
3
|
+
const output = require('../lib/output');
|
|
4
|
+
const { handlePayment } = require('../lib/payment');
|
|
5
|
+
|
|
6
|
+
const PRICES = {
|
|
7
|
+
membership: 2,
|
|
8
|
+
'storage-pass': 20,
|
|
9
|
+
'stack-unlock': 5,
|
|
10
|
+
'grid-unlock': 2,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = function (program) {
|
|
14
|
+
const cmd = program.command('member').description('Membership and purchases');
|
|
15
|
+
|
|
16
|
+
cmd
|
|
17
|
+
.command('status')
|
|
18
|
+
.description('Check membership status and available purchases')
|
|
19
|
+
.action(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const data = await api.get('/agents/membership');
|
|
22
|
+
if (output.isJsonMode()) {
|
|
23
|
+
output.json(data);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (data.isMember) {
|
|
27
|
+
output.item(data, [
|
|
28
|
+
['Status', () => chalk.green('Member')],
|
|
29
|
+
['Member ID', 'memberId'],
|
|
30
|
+
['ENS', 'ensSubname'],
|
|
31
|
+
['Joined', 'joinedAt', v => v ? new Date(v).toLocaleDateString() : null],
|
|
32
|
+
['Storage Pass', 'hasUnlimitedStoragePass', v => v ? chalk.green('Yes') : 'No'],
|
|
33
|
+
['Stack Pass', 'hasStackPass', v => v ? chalk.green('Yes') : 'No'],
|
|
34
|
+
['Grid Pass', 'hasGridPass', v => v ? chalk.green('Yes') : 'No'],
|
|
35
|
+
]);
|
|
36
|
+
} else {
|
|
37
|
+
console.log(chalk.yellow('Not a member.'));
|
|
38
|
+
console.log(`\nJoin with: ${chalk.cyan('netlibrary member join')}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (data.availablePurchases && data.availablePurchases.length > 0) {
|
|
42
|
+
console.log('\nAvailable purchases:');
|
|
43
|
+
output.table(
|
|
44
|
+
['Type', 'Price', 'Description', 'Available'],
|
|
45
|
+
data.availablePurchases.map(p => [
|
|
46
|
+
p.type,
|
|
47
|
+
p.priceDisplay,
|
|
48
|
+
p.description,
|
|
49
|
+
p.available ? chalk.green('Yes') : chalk.dim(p.reason || 'No'),
|
|
50
|
+
])
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
output.error(err.message);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
cmd
|
|
60
|
+
.command('join')
|
|
61
|
+
.description('Purchase Net Library membership ($2 USDC)')
|
|
62
|
+
.option('--tx-hash <hash>', 'Payment tx hash (if already paid)')
|
|
63
|
+
.option('--admin-grant', 'Grant without payment (admin only)')
|
|
64
|
+
.option('--target <agentId>', 'Target agent for admin grant')
|
|
65
|
+
.action(async (opts) => {
|
|
66
|
+
try {
|
|
67
|
+
const body = { purchaseType: 'membership' };
|
|
68
|
+
|
|
69
|
+
if (opts.adminGrant) {
|
|
70
|
+
body.adminGrant = true;
|
|
71
|
+
if (opts.target) body.targetAgentId = opts.target;
|
|
72
|
+
} else {
|
|
73
|
+
body.txHash = await handlePayment(PRICES.membership, { txHash: opts.txHash });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data = await api.post('/agents/membership', body);
|
|
77
|
+
output.success(`Membership activated! Member #${data.memberId} (${data.ensSubname})`);
|
|
78
|
+
if (output.isJsonMode()) output.json(data);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
output.error(err.message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
cmd
|
|
86
|
+
.command('buy <type>')
|
|
87
|
+
.description('Purchase: storage-pass ($20), stack-unlock ($5), grid-unlock ($2)')
|
|
88
|
+
.option('--tx-hash <hash>', 'Payment tx hash (if already paid)')
|
|
89
|
+
.option('--stack-id <id>', 'Stack ID (required for stack-unlock)')
|
|
90
|
+
.option('--admin-grant', 'Grant without payment (admin only)')
|
|
91
|
+
.option('--target <agentId>', 'Target agent for admin grant')
|
|
92
|
+
.action(async (type, opts) => {
|
|
93
|
+
try {
|
|
94
|
+
const validTypes = ['storage-pass', 'stack-unlock', 'grid-unlock'];
|
|
95
|
+
if (!validTypes.includes(type)) {
|
|
96
|
+
output.error(`Invalid type. Choose: ${validTypes.join(', ')}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (type === 'stack-unlock' && !opts.stackId) {
|
|
101
|
+
output.error('--stack-id is required for stack-unlock');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const body = { purchaseType: type };
|
|
106
|
+
if (opts.stackId) body.stackId = opts.stackId;
|
|
107
|
+
|
|
108
|
+
if (opts.adminGrant) {
|
|
109
|
+
body.adminGrant = true;
|
|
110
|
+
if (opts.target) body.targetAgentId = opts.target;
|
|
111
|
+
} else {
|
|
112
|
+
body.txHash = await handlePayment(PRICES[type], { txHash: opts.txHash });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = await api.post('/agents/membership', body);
|
|
116
|
+
output.success(`${type} purchased!`);
|
|
117
|
+
if (output.isJsonMode()) output.json(data);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
output.error(err.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
cmd
|
|
125
|
+
.command('ens')
|
|
126
|
+
.description('Mint or retry ENS subname')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
try {
|
|
129
|
+
const data = await api.post('/agents/ens', {});
|
|
130
|
+
if (data.alreadyMinted) {
|
|
131
|
+
output.success(`ENS already minted: ${data.ensSubname}`);
|
|
132
|
+
} else {
|
|
133
|
+
output.success(`ENS minted: ${data.ensSubname}`);
|
|
134
|
+
}
|
|
135
|
+
if (output.isJsonMode()) output.json(data);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
output.error(err.message);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
cmd
|
|
143
|
+
.command('link [url]')
|
|
144
|
+
.description('Check or set Net Protocol link (no arg = check, with URL = set)')
|
|
145
|
+
.option('--label <label>', 'Display label for the link')
|
|
146
|
+
.option('--remove', 'Remove the current link')
|
|
147
|
+
.action(async (url, opts) => {
|
|
148
|
+
try {
|
|
149
|
+
if (opts.remove) {
|
|
150
|
+
const data = await api.del('/agents/link', {});
|
|
151
|
+
output.success('Link removed');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!url) {
|
|
156
|
+
// Check link status
|
|
157
|
+
const data = await api.get('/agents/link');
|
|
158
|
+
if (output.isJsonMode()) {
|
|
159
|
+
output.json(data);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (data.linked) {
|
|
163
|
+
output.item(data, [
|
|
164
|
+
['Linked', () => chalk.green('Yes')],
|
|
165
|
+
['URL', 'url'],
|
|
166
|
+
['Label', 'label'],
|
|
167
|
+
['Linked At', 'linkedAt', v => v ? new Date(v).toLocaleString() : null],
|
|
168
|
+
]);
|
|
169
|
+
} else {
|
|
170
|
+
console.log(chalk.dim('No Net Protocol link set.'));
|
|
171
|
+
console.log(`Set one with: ${chalk.cyan('netlibrary member link <url>')}`);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// Set link
|
|
175
|
+
const body = { url };
|
|
176
|
+
if (opts.label) body.label = opts.label;
|
|
177
|
+
const data = await api.post('/agents/link', body);
|
|
178
|
+
output.success(`Linked: ${data.url}`);
|
|
179
|
+
if (output.isJsonMode()) output.json(data);
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
output.error(err.message);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
cmd
|
|
188
|
+
.command('verify')
|
|
189
|
+
.description('Re-check ERC-8004 verification status')
|
|
190
|
+
.option('--token-id <id>', 'Specific ERC-8004 token ID to verify')
|
|
191
|
+
.action(async (opts) => {
|
|
192
|
+
try {
|
|
193
|
+
const body = {};
|
|
194
|
+
if (opts.tokenId) body.tokenId = parseInt(opts.tokenId);
|
|
195
|
+
const data = await api.post('/agents/verify-8004', body);
|
|
196
|
+
if (output.isJsonMode()) {
|
|
197
|
+
output.json(data);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (data.verified) {
|
|
201
|
+
output.success(`ERC-8004 verified! Token ID: ${data.tokenId}`);
|
|
202
|
+
} else {
|
|
203
|
+
output.warn('Not verified on ERC-8004.');
|
|
204
|
+
console.log(data.message || '');
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
output.error(err.message);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const api = require('../lib/api');
|
|
2
|
+
const output = require('../lib/output');
|
|
3
|
+
|
|
4
|
+
module.exports = function (program) {
|
|
5
|
+
program
|
|
6
|
+
.command('search [query]')
|
|
7
|
+
.description('Search library items and stacks')
|
|
8
|
+
.option('-c, --category <cat>', 'Filter by category')
|
|
9
|
+
.option('-a, --author <author>', 'Filter by author')
|
|
10
|
+
.option('-m, --media-type <type>', 'Filter by media type')
|
|
11
|
+
.option('-l, --limit <n>', 'Max results (max 50)', '20')
|
|
12
|
+
.action(async (query, opts) => {
|
|
13
|
+
try {
|
|
14
|
+
if (!query && !opts.category && !opts.author) {
|
|
15
|
+
output.error('Provide a search query, --category, or --author');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const data = await api.get('/search', {
|
|
19
|
+
query: {
|
|
20
|
+
q: query,
|
|
21
|
+
category: opts.category,
|
|
22
|
+
author: opts.author,
|
|
23
|
+
mediaType: opts.mediaType,
|
|
24
|
+
limit: opts.limit,
|
|
25
|
+
},
|
|
26
|
+
auth: false,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (data.items && data.items.length > 0) {
|
|
30
|
+
if (!output.isJsonMode()) console.log('\nItems:');
|
|
31
|
+
output.table(
|
|
32
|
+
['Title', 'Author', 'Type', 'Key'],
|
|
33
|
+
data.items.map(i => [
|
|
34
|
+
i.title || '—',
|
|
35
|
+
i.author || '—',
|
|
36
|
+
i.mediaType || '—',
|
|
37
|
+
i.contentKey,
|
|
38
|
+
])
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (data.stacks && data.stacks.length > 0) {
|
|
43
|
+
if (!output.isJsonMode()) console.log('\nStacks:');
|
|
44
|
+
output.table(
|
|
45
|
+
['Name', 'Owner', 'Items', 'ID'],
|
|
46
|
+
data.stacks.map(s => [
|
|
47
|
+
s.name || '—',
|
|
48
|
+
s.ownerUsername || s.owner || '—',
|
|
49
|
+
s.itemCount || 0,
|
|
50
|
+
s.id,
|
|
51
|
+
])
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!output.isJsonMode()) {
|
|
56
|
+
console.log(`\n${data.totalResults || 0} total results`);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
output.error(err.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const api = require('../lib/api');
|
|
2
|
+
const output = require('../lib/output');
|
|
3
|
+
const { handlePayment } = require('../lib/payment');
|
|
4
|
+
|
|
5
|
+
module.exports = function (program) {
|
|
6
|
+
const cmd = program.command('stacks').description('Browse and manage stacks');
|
|
7
|
+
|
|
8
|
+
cmd
|
|
9
|
+
.command('list')
|
|
10
|
+
.description('Browse public stacks')
|
|
11
|
+
.option('-p, --page <n>', 'Page number', '1')
|
|
12
|
+
.option('-l, --limit <n>', 'Items per page (max 50)', '20')
|
|
13
|
+
.option('-o, --owner <address>', 'Filter by owner address')
|
|
14
|
+
.option('-s, --sort <sort>', 'Sort: newest, oldest, name, popular', 'newest')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
try {
|
|
17
|
+
const data = await api.get('/stacks', {
|
|
18
|
+
query: {
|
|
19
|
+
page: opts.page,
|
|
20
|
+
limit: opts.limit,
|
|
21
|
+
owner: opts.owner,
|
|
22
|
+
sort: opts.sort,
|
|
23
|
+
},
|
|
24
|
+
auth: false,
|
|
25
|
+
});
|
|
26
|
+
output.table(
|
|
27
|
+
['Name', 'Owner', 'Items', 'Upvotes', 'ID'],
|
|
28
|
+
(data.stacks || []).map(s => [
|
|
29
|
+
s.name || '—',
|
|
30
|
+
s.ownerUsername || s.owner || '—',
|
|
31
|
+
s.itemCount || 0,
|
|
32
|
+
s.upvotes || 0,
|
|
33
|
+
s.id,
|
|
34
|
+
])
|
|
35
|
+
);
|
|
36
|
+
output.pagination(data.pagination);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
output.error(err.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
cmd
|
|
44
|
+
.command('get <stackId>')
|
|
45
|
+
.description('Get a stack with all items')
|
|
46
|
+
.action(async (stackId) => {
|
|
47
|
+
try {
|
|
48
|
+
const data = await api.get(`/stacks/${stackId}`, { auth: false });
|
|
49
|
+
const s = data.stack || {};
|
|
50
|
+
output.item(s, [
|
|
51
|
+
['Name', 'name'],
|
|
52
|
+
['ID', 'id'],
|
|
53
|
+
['Owner', 'ownerUsername', v => v || s.owner],
|
|
54
|
+
['Items', 'itemCount'],
|
|
55
|
+
['Upvotes', 'upvotes'],
|
|
56
|
+
['Views', 'views'],
|
|
57
|
+
['Created', 'createdAt', v => v ? new Date(v).toLocaleString() : null],
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
if (data.items && data.items.length > 0) {
|
|
61
|
+
if (!output.isJsonMode()) console.log('\nItems:');
|
|
62
|
+
output.table(
|
|
63
|
+
['Title', 'Author', 'Type', 'Key'],
|
|
64
|
+
data.items.map(i => {
|
|
65
|
+
const book = i.book || {};
|
|
66
|
+
return [
|
|
67
|
+
book.title || i.title || '—',
|
|
68
|
+
book.author || '—',
|
|
69
|
+
book.mediaType || '—',
|
|
70
|
+
i.contentKey,
|
|
71
|
+
];
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
output.error(err.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
cmd
|
|
82
|
+
.command('create')
|
|
83
|
+
.description('Create a new stack')
|
|
84
|
+
.requiredOption('-n, --name <name>', 'Stack name')
|
|
85
|
+
.option('-d, --description <desc>', 'Stack description')
|
|
86
|
+
.option('--private', 'Make stack private')
|
|
87
|
+
.option('--items <keys...>', 'Initial content keys (max 20)')
|
|
88
|
+
.option('--tx-hash <hash>', 'Payment tx hash (for non-members)')
|
|
89
|
+
.action(async (opts) => {
|
|
90
|
+
try {
|
|
91
|
+
const body = { name: opts.name };
|
|
92
|
+
if (opts.description) body.description = opts.description;
|
|
93
|
+
if (opts.private) body.isPublic = false;
|
|
94
|
+
if (opts.items) body.items = opts.items;
|
|
95
|
+
if (opts.txHash) body.txHash = opts.txHash;
|
|
96
|
+
|
|
97
|
+
const data = await api.post('/stacks/write', body);
|
|
98
|
+
output.success(`Stack created: ${data.stack?.id || data.id}`);
|
|
99
|
+
if (output.isJsonMode()) output.json(data);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err.status === 402) {
|
|
102
|
+
output.warn('Non-members must pay $5 USDC to create a stack.');
|
|
103
|
+
const txHash = await handlePayment(5);
|
|
104
|
+
const body = { name: opts.name, txHash };
|
|
105
|
+
if (opts.description) body.description = opts.description;
|
|
106
|
+
if (opts.private) body.isPublic = false;
|
|
107
|
+
if (opts.items) body.items = opts.items;
|
|
108
|
+
const data = await api.post('/stacks/write', body);
|
|
109
|
+
output.success(`Stack created: ${data.stack?.id || data.id}`);
|
|
110
|
+
} else {
|
|
111
|
+
output.error(err.message);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
cmd
|
|
118
|
+
.command('add <stackId> <contentKey>')
|
|
119
|
+
.description('Add an item to a stack')
|
|
120
|
+
.action(async (stackId, contentKey) => {
|
|
121
|
+
try {
|
|
122
|
+
const data = await api.put('/stacks/write', {
|
|
123
|
+
stackId,
|
|
124
|
+
action: 'add-item',
|
|
125
|
+
contentKey,
|
|
126
|
+
});
|
|
127
|
+
output.success(`Added to stack. Items: ${data.itemCount}`);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
output.error(err.message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
cmd
|
|
135
|
+
.command('remove <stackId> <contentKey>')
|
|
136
|
+
.description('Remove an item from a stack')
|
|
137
|
+
.action(async (stackId, contentKey) => {
|
|
138
|
+
try {
|
|
139
|
+
const data = await api.put('/stacks/write', {
|
|
140
|
+
stackId,
|
|
141
|
+
action: 'remove-item',
|
|
142
|
+
contentKey,
|
|
143
|
+
});
|
|
144
|
+
output.success(`Removed from stack. Items: ${data.itemCount}`);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
output.error(err.message);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
cmd
|
|
152
|
+
.command('update <stackId>')
|
|
153
|
+
.description('Update stack metadata')
|
|
154
|
+
.option('-n, --name <name>', 'New name')
|
|
155
|
+
.option('-d, --description <desc>', 'New description')
|
|
156
|
+
.option('--private', 'Make private')
|
|
157
|
+
.option('--public', 'Make public')
|
|
158
|
+
.action(async (stackId, opts) => {
|
|
159
|
+
try {
|
|
160
|
+
const body = { stackId, action: 'update-metadata' };
|
|
161
|
+
if (opts.name) body.name = opts.name;
|
|
162
|
+
if (opts.description) body.description = opts.description;
|
|
163
|
+
if (opts.private) body.isPrivate = true;
|
|
164
|
+
if (opts.public) body.isPrivate = false;
|
|
165
|
+
|
|
166
|
+
const data = await api.put('/stacks/write', body);
|
|
167
|
+
output.success('Stack updated');
|
|
168
|
+
if (output.isJsonMode()) output.json(data);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
output.error(err.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
cmd
|
|
176
|
+
.command('unlock <stackId>')
|
|
177
|
+
.description('Unlock a stack (remove 20-item limit, $5 USDC)')
|
|
178
|
+
.option('--tx-hash <hash>', 'Payment tx hash (if already paid)')
|
|
179
|
+
.action(async (stackId, opts) => {
|
|
180
|
+
try {
|
|
181
|
+
const txHash = await handlePayment(5, { txHash: opts.txHash });
|
|
182
|
+
const data = await api.put('/stacks/write', {
|
|
183
|
+
stackId,
|
|
184
|
+
action: 'unlock',
|
|
185
|
+
txHash,
|
|
186
|
+
});
|
|
187
|
+
output.success(`Stack ${stackId} unlocked!`);
|
|
188
|
+
if (output.isJsonMode()) output.json(data);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
output.error(err.message);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
cmd
|
|
196
|
+
.command('bulk-remove <stackId> <contentKeys...>')
|
|
197
|
+
.description('Remove multiple items from a stack')
|
|
198
|
+
.action(async (stackId, contentKeys) => {
|
|
199
|
+
try {
|
|
200
|
+
const data = await api.del('/stacks/write', {
|
|
201
|
+
stackId,
|
|
202
|
+
items: contentKeys,
|
|
203
|
+
});
|
|
204
|
+
output.success(`Removed ${data.removed} items. Remaining: ${data.itemCount}`);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
output.error(err.message);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const api = require('../lib/api');
|
|
2
|
+
const output = require('../lib/output');
|
|
3
|
+
|
|
4
|
+
module.exports = function (program) {
|
|
5
|
+
program
|
|
6
|
+
.command('stats')
|
|
7
|
+
.description('Show library statistics')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const data = await api.get('/stats', { auth: false });
|
|
11
|
+
if (output.isJsonMode()) {
|
|
12
|
+
output.json(data);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const lib = data.library || {};
|
|
16
|
+
const members = data.members || {};
|
|
17
|
+
const stacks = data.stacks || {};
|
|
18
|
+
output.item(data, [
|
|
19
|
+
['Total Items', () => lib.totalItems],
|
|
20
|
+
['Members', () => `${members.total || 0} (${members.agents || 0} agents, ${members.humans || 0} humans)`],
|
|
21
|
+
['Total Stacks', () => stacks.total],
|
|
22
|
+
['Categories', () => {
|
|
23
|
+
const cats = lib.categories || {};
|
|
24
|
+
return Object.entries(cats).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
25
|
+
}],
|
|
26
|
+
['Media Types', () => {
|
|
27
|
+
const types = lib.mediaTypes || {};
|
|
28
|
+
return Object.entries(types).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
29
|
+
}],
|
|
30
|
+
]);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
output.error(err.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
};
|