zako 0.1.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/dist/cli.mjs +682 -0
- package/package.json +23 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { exec } from "node:child_process";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { readFile, unlink, mkdir, writeFile, chmod, access } from "node:fs/promises";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { createInterface } from "node:readline/promises";
|
|
10
|
+
const CONFIG_DIR = join(homedir(), ".config", "zakoducthunt");
|
|
11
|
+
const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
12
|
+
const getCredentialsPath = () => CREDENTIALS_FILE;
|
|
13
|
+
const loadCredentials = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(CREDENTIALS_FILE, "utf-8");
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
if (!parsed.apiKey || !parsed.apiBaseUrl) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.debug("[credential-store]", error instanceof Error ? error.message : error);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const saveCredentials = async (credentials) => {
|
|
27
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
28
|
+
await writeFile(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), "utf-8");
|
|
29
|
+
await chmod(CREDENTIALS_FILE, 384);
|
|
30
|
+
};
|
|
31
|
+
const deleteCredentials = async () => {
|
|
32
|
+
try {
|
|
33
|
+
await unlink(CREDENTIALS_FILE);
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.debug("[credential-store] delete failed:", error instanceof Error ? error.message : error);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const loginCommand = async ({ apiUrl, webUrl }) => {
|
|
41
|
+
console.log("Logging in to zakoducthunt...");
|
|
42
|
+
const port = await startCallbackServer(apiUrl);
|
|
43
|
+
const loginUrl = `${webUrl}/me/cli-auth?port=${port}`;
|
|
44
|
+
console.log(`
|
|
45
|
+
Opening browser: ${loginUrl}`);
|
|
46
|
+
console.log("If the browser doesn't open, visit the URL above manually.\n");
|
|
47
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
48
|
+
exec(`${openCmd} ${JSON.stringify(loginUrl)}`);
|
|
49
|
+
};
|
|
50
|
+
const startCallbackServer = (apiUrl) => {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const server = createServer((req, res) => {
|
|
53
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
54
|
+
if (url.pathname === "/callback") {
|
|
55
|
+
const key = url.searchParams.get("key");
|
|
56
|
+
if (!key) {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
server.close();
|
|
59
|
+
console.error("Error: No API key received from browser.");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
saveCredentials({ apiKey: key, apiBaseUrl: apiUrl }).then(() => {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
server.close();
|
|
66
|
+
console.log("Login successful! Credentials saved.");
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}, 100);
|
|
69
|
+
});
|
|
70
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
71
|
+
res.end(
|
|
72
|
+
"<html><body><h2>Login successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>"
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
res.writeHead(404);
|
|
77
|
+
res.end("Not found");
|
|
78
|
+
});
|
|
79
|
+
const timeout = setTimeout(() => {
|
|
80
|
+
server.close();
|
|
81
|
+
reject(new Error("Login timed out after 120 seconds."));
|
|
82
|
+
}, 12e4);
|
|
83
|
+
server.listen(0, () => {
|
|
84
|
+
const addr = server.address();
|
|
85
|
+
resolve(addr.port);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const logoutCommand = async () => {
|
|
90
|
+
const deleted = await deleteCredentials();
|
|
91
|
+
if (deleted) {
|
|
92
|
+
console.log(`Logged out. Credentials removed from ${getCredentialsPath()}`);
|
|
93
|
+
} else {
|
|
94
|
+
console.log("Not logged in (no credentials found).");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const PRODUCTS_PATHS = {
|
|
98
|
+
list: "/api/public/products",
|
|
99
|
+
batch: "/api/public/products/batch",
|
|
100
|
+
search: "/api/public/products/search",
|
|
101
|
+
create: "/api/user/products",
|
|
102
|
+
update: (id) => `/api/user/products/items/${id}`
|
|
103
|
+
};
|
|
104
|
+
const USER_PRODUCTS_PATHS = {
|
|
105
|
+
list: "/api/user/products"
|
|
106
|
+
};
|
|
107
|
+
const OGP_PATHS = {
|
|
108
|
+
preview: "/api/public/ogp/preview"
|
|
109
|
+
};
|
|
110
|
+
const USERS_PATHS = {
|
|
111
|
+
me: "/api/user/me"
|
|
112
|
+
};
|
|
113
|
+
const searchCommand = async ({ query, apiUrl, json }) => {
|
|
114
|
+
const params = new URLSearchParams({ q: query, limit: "20" });
|
|
115
|
+
const url = `${apiUrl}${PRODUCTS_PATHS.search}?${params}`;
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
headers: { "Content-Type": "application/json" }
|
|
118
|
+
});
|
|
119
|
+
const result = await response.json();
|
|
120
|
+
if (!result.success) {
|
|
121
|
+
console.error("Search failed:", "error" in result ? result.error : "Unknown error");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const items = result.data.items;
|
|
125
|
+
if (json) {
|
|
126
|
+
console.log(JSON.stringify(items, null, 2));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (items.length === 0) {
|
|
130
|
+
console.log(`No products found for "${query}".`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
console.log(`Found ${items.length} product(s) for "${query}":
|
|
134
|
+
`);
|
|
135
|
+
for (const item of items) {
|
|
136
|
+
console.log(` ${item.name}`);
|
|
137
|
+
console.log(` ${item.tagline}`);
|
|
138
|
+
console.log(` Categories: ${item.categories.join(", ")}`);
|
|
139
|
+
console.log("");
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const PRODUCT_CATEGORIES = [
|
|
143
|
+
"saas",
|
|
144
|
+
"developer_tools",
|
|
145
|
+
"ai",
|
|
146
|
+
"productivity",
|
|
147
|
+
"design",
|
|
148
|
+
"business",
|
|
149
|
+
"finance",
|
|
150
|
+
"social",
|
|
151
|
+
"entertainment",
|
|
152
|
+
"game",
|
|
153
|
+
"education",
|
|
154
|
+
"health_fitness",
|
|
155
|
+
"lifestyle",
|
|
156
|
+
"ecommerce",
|
|
157
|
+
"utility",
|
|
158
|
+
"other"
|
|
159
|
+
];
|
|
160
|
+
const PLATFORMS = [
|
|
161
|
+
"web",
|
|
162
|
+
"ios",
|
|
163
|
+
"android",
|
|
164
|
+
"windows",
|
|
165
|
+
"mac",
|
|
166
|
+
"linux",
|
|
167
|
+
"chrome_extension",
|
|
168
|
+
"firefox_extension",
|
|
169
|
+
"cli",
|
|
170
|
+
"api"
|
|
171
|
+
];
|
|
172
|
+
const productCategorySchema = z.enum(PRODUCT_CATEGORIES);
|
|
173
|
+
const platformSchema = z.enum(PLATFORMS);
|
|
174
|
+
const zakoducthuntConfigSchema = z.object({
|
|
175
|
+
$schema: z.string().optional(),
|
|
176
|
+
product: z.object({
|
|
177
|
+
url: z.string().url("url must be a valid URL"),
|
|
178
|
+
name: z.string().min(1, "name is required").max(100, "name must be 100 characters or less"),
|
|
179
|
+
tagline: z.string().min(1, "tagline is required").max(200, "tagline must be 200 characters or less"),
|
|
180
|
+
description: z.string().max(5e3, "description must be 5000 characters or less").optional(),
|
|
181
|
+
categories: z.array(productCategorySchema).min(1, "at least one category is required").max(3, "at most 3 categories allowed"),
|
|
182
|
+
platforms: z.array(platformSchema).max(5, "at most 5 platforms allowed").optional(),
|
|
183
|
+
visibility: z.enum(["active", "draft"]).default("active")
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
const CONFIG_FILENAME = "zakoducthunt.config.json";
|
|
187
|
+
const loadConfig = async (dir) => {
|
|
188
|
+
const filePath = join(dir, CONFIG_FILENAME);
|
|
189
|
+
let raw;
|
|
190
|
+
try {
|
|
191
|
+
raw = await readFile(filePath, "utf-8");
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
+
throw new Error(`Config file not found: ${filePath} (${message})
|
|
195
|
+
Run \`zako init\` to create one.`);
|
|
196
|
+
}
|
|
197
|
+
let json;
|
|
198
|
+
try {
|
|
199
|
+
json = JSON.parse(raw);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
202
|
+
throw new Error(`Invalid JSON in ${filePath}: ${message}`);
|
|
203
|
+
}
|
|
204
|
+
const result = zakoducthuntConfigSchema.safeParse(json);
|
|
205
|
+
if (!result.success) {
|
|
206
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
207
|
+
throw new Error(`Invalid config in ${filePath}:
|
|
208
|
+
${issues}`);
|
|
209
|
+
}
|
|
210
|
+
return result.data;
|
|
211
|
+
};
|
|
212
|
+
const resolveAuth = async (options) => {
|
|
213
|
+
if (options.apiKey) {
|
|
214
|
+
return {
|
|
215
|
+
apiKey: options.apiKey,
|
|
216
|
+
apiBaseUrl: options.apiUrl ?? options.defaultApiUrl
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const envKey = process.env.ZAKODUCTHUNT_API_KEY;
|
|
220
|
+
if (envKey) {
|
|
221
|
+
return {
|
|
222
|
+
apiKey: envKey,
|
|
223
|
+
apiBaseUrl: options.apiUrl ?? process.env.ZAKODUCTHUNT_API_URL ?? options.defaultApiUrl
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const creds = await loadCredentials();
|
|
227
|
+
if (creds) {
|
|
228
|
+
return {
|
|
229
|
+
apiKey: creds.apiKey,
|
|
230
|
+
apiBaseUrl: options.apiUrl ?? creds.apiBaseUrl
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
throw new Error("Not authenticated. Run `zako login` first, or pass --api-key.");
|
|
234
|
+
};
|
|
235
|
+
const createFetcher = (options) => {
|
|
236
|
+
return async (endpoint, init) => {
|
|
237
|
+
const url = `${options.baseUrl}${endpoint}`;
|
|
238
|
+
const response = await fetch(url, {
|
|
239
|
+
...init,
|
|
240
|
+
headers: {
|
|
241
|
+
"Content-Type": "application/json",
|
|
242
|
+
"x-api-key": options.apiKey,
|
|
243
|
+
...init?.headers
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
if (response.status === 204) {
|
|
247
|
+
return { success: true, data: void 0 };
|
|
248
|
+
}
|
|
249
|
+
return await response.json();
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
const createCliApiClient = (options) => {
|
|
253
|
+
const fetcher = createFetcher(options);
|
|
254
|
+
return {
|
|
255
|
+
me: () => fetcher(USERS_PATHS.me),
|
|
256
|
+
listMyProducts: () => fetcher(USER_PRODUCTS_PATHS.list),
|
|
257
|
+
batchGetProducts: (ids) => fetcher(PRODUCTS_PATHS.batch, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: JSON.stringify({ ids })
|
|
260
|
+
}),
|
|
261
|
+
searchProducts: (query, limit = 20) => {
|
|
262
|
+
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
263
|
+
return fetcher(`${PRODUCTS_PATHS.search}?${params}`);
|
|
264
|
+
},
|
|
265
|
+
createProduct: (input) => fetcher(PRODUCTS_PATHS.create, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
body: JSON.stringify(input)
|
|
268
|
+
}),
|
|
269
|
+
updateProduct: (id, input) => fetcher(PRODUCTS_PATHS.update(id), {
|
|
270
|
+
method: "PATCH",
|
|
271
|
+
body: JSON.stringify(input)
|
|
272
|
+
}),
|
|
273
|
+
ogpPreview: (url) => fetcher(OGP_PATHS.preview, {
|
|
274
|
+
method: "POST",
|
|
275
|
+
body: JSON.stringify({ url })
|
|
276
|
+
})
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
const publishCommand = async (options) => {
|
|
280
|
+
const config = await loadConfig(process.cwd());
|
|
281
|
+
console.log(`Publishing "${config.product.name}"...`);
|
|
282
|
+
const auth = await resolveAuth({
|
|
283
|
+
apiKey: options.apiKey,
|
|
284
|
+
apiUrl: options.apiUrl,
|
|
285
|
+
defaultApiUrl: options.defaultApiUrl
|
|
286
|
+
});
|
|
287
|
+
const client = createCliApiClient({ baseUrl: auth.apiBaseUrl, apiKey: auth.apiKey });
|
|
288
|
+
const myProductsResult = await client.listMyProducts();
|
|
289
|
+
if (!myProductsResult.success) {
|
|
290
|
+
console.error("Failed to fetch your products.");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
const productListItems = myProductsResult.data.products;
|
|
294
|
+
let existingProductId;
|
|
295
|
+
if (productListItems.length > 0) {
|
|
296
|
+
const ids = productListItems.map((p) => p.id);
|
|
297
|
+
const batchResult = await client.batchGetProducts(ids);
|
|
298
|
+
if (batchResult.success) {
|
|
299
|
+
const match = batchResult.data.items.find((p) => p.url === config.product.url);
|
|
300
|
+
existingProductId = match?.id;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const productInput = {
|
|
304
|
+
url: config.product.url,
|
|
305
|
+
name: config.product.name,
|
|
306
|
+
tagline: config.product.tagline,
|
|
307
|
+
...config.product.description !== void 0 && { description: config.product.description },
|
|
308
|
+
categories: config.product.categories,
|
|
309
|
+
...config.product.platforms !== void 0 && { platforms: config.product.platforms },
|
|
310
|
+
visibility: config.product.visibility
|
|
311
|
+
};
|
|
312
|
+
if (existingProductId) {
|
|
313
|
+
console.log(`Updating existing product (id: ${existingProductId})...`);
|
|
314
|
+
const result = await client.updateProduct(existingProductId, productInput);
|
|
315
|
+
if (!result.success) {
|
|
316
|
+
console.error("Update failed:", "error" in result ? result.error : "Unknown error");
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
if (options.json) {
|
|
320
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
321
|
+
} else {
|
|
322
|
+
console.log(`Updated: ${result.data.name} (${result.data.id})`);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
console.log("Creating new product...");
|
|
326
|
+
const result = await client.createProduct(productInput);
|
|
327
|
+
if (!result.success) {
|
|
328
|
+
console.error("Create failed:", "error" in result ? result.error : "Unknown error");
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
if (options.json) {
|
|
332
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
333
|
+
} else {
|
|
334
|
+
console.log(`Created: ${result.data.name} (${result.data.id})`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const prompt = async (question) => {
|
|
339
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
340
|
+
try {
|
|
341
|
+
return await rl.question(question);
|
|
342
|
+
} finally {
|
|
343
|
+
rl.close();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const initCommand = async ({ apiUrl }) => {
|
|
347
|
+
const configPath = join(process.cwd(), CONFIG_FILENAME);
|
|
348
|
+
try {
|
|
349
|
+
await access(configPath);
|
|
350
|
+
console.error(`${CONFIG_FILENAME} already exists in this directory.`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
console.debug("[init] Config file not found, creating new:", error instanceof Error ? error.message : error);
|
|
354
|
+
}
|
|
355
|
+
console.log("Initializing zakoducthunt.config.json\n");
|
|
356
|
+
const url = await prompt("Product URL: ");
|
|
357
|
+
if (!url) {
|
|
358
|
+
console.error("URL is required.");
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
let name = "";
|
|
362
|
+
let tagline = "";
|
|
363
|
+
try {
|
|
364
|
+
console.log("Fetching metadata from URL...");
|
|
365
|
+
const response = await fetch(`${apiUrl}/api/public/ogp/preview`, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: { "Content-Type": "application/json" },
|
|
368
|
+
body: JSON.stringify({ url })
|
|
369
|
+
});
|
|
370
|
+
const result = await response.json();
|
|
371
|
+
if (result.success && result.data) {
|
|
372
|
+
name = result.data.title ?? "";
|
|
373
|
+
tagline = result.data.description ?? "";
|
|
374
|
+
if (name) {
|
|
375
|
+
console.log(` Found: ${name}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.debug("[init] OGP fetch failed:", error instanceof Error ? error.message : error);
|
|
380
|
+
console.log(" Could not fetch metadata (continuing without it).");
|
|
381
|
+
}
|
|
382
|
+
if (!name) {
|
|
383
|
+
name = await prompt("Product name: ");
|
|
384
|
+
}
|
|
385
|
+
if (!tagline) {
|
|
386
|
+
tagline = await prompt("Tagline: ");
|
|
387
|
+
}
|
|
388
|
+
const config = {
|
|
389
|
+
product: {
|
|
390
|
+
url,
|
|
391
|
+
name: name || "My Product",
|
|
392
|
+
tagline: tagline || "A brief description of my product",
|
|
393
|
+
categories: ["other"],
|
|
394
|
+
visibility: "draft"
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
398
|
+
console.log(`
|
|
399
|
+
Created ${CONFIG_FILENAME}`);
|
|
400
|
+
console.log("Edit the file to add categories, platforms, and a description.");
|
|
401
|
+
console.log("Then run `zako publish` to publish your product.");
|
|
402
|
+
};
|
|
403
|
+
const apiKeyCommand = async (options) => {
|
|
404
|
+
const auth = await resolveAuth({
|
|
405
|
+
apiKey: options.apiKey,
|
|
406
|
+
apiUrl: options.apiUrl,
|
|
407
|
+
defaultApiUrl: options.defaultApiUrl
|
|
408
|
+
});
|
|
409
|
+
const authBaseUrl = auth.apiBaseUrl;
|
|
410
|
+
const headers = {
|
|
411
|
+
"Content-Type": "application/json",
|
|
412
|
+
"x-api-key": auth.apiKey
|
|
413
|
+
};
|
|
414
|
+
switch (options.subcommand) {
|
|
415
|
+
case "create": {
|
|
416
|
+
if (!options.name) {
|
|
417
|
+
console.error("--name is required for api-key create");
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
const response = await fetch(`${authBaseUrl}/api/auth/api-key/create-key`, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers,
|
|
423
|
+
body: JSON.stringify({ name: options.name })
|
|
424
|
+
});
|
|
425
|
+
const result = await response.json();
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
console.error("Failed to create API key:", result.error?.message ?? "Unknown error");
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
if (options.json) {
|
|
431
|
+
console.log(JSON.stringify(result, null, 2));
|
|
432
|
+
} else {
|
|
433
|
+
console.log("API key created successfully!");
|
|
434
|
+
console.log(`
|
|
435
|
+
Key: ${result.key}
|
|
436
|
+
`);
|
|
437
|
+
console.log("Save this key — it won't be shown again.");
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case "list": {
|
|
442
|
+
const response = await fetch(`${authBaseUrl}/api/auth/api-key/list-keys`, {
|
|
443
|
+
method: "GET",
|
|
444
|
+
headers
|
|
445
|
+
});
|
|
446
|
+
const result = await response.json();
|
|
447
|
+
if (!response.ok) {
|
|
448
|
+
console.error("Failed to list API keys.");
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
const keys = result.keys ?? [];
|
|
452
|
+
if (options.json) {
|
|
453
|
+
console.log(JSON.stringify(keys, null, 2));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (keys.length === 0) {
|
|
457
|
+
console.log("No API keys found.");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
console.log(`${keys.length} API key(s):
|
|
461
|
+
`);
|
|
462
|
+
for (const key of keys) {
|
|
463
|
+
const name = key.name ?? "(unnamed)";
|
|
464
|
+
const prefix = key.start ?? "zk_***";
|
|
465
|
+
const created = new Date(key.createdAt).toLocaleDateString();
|
|
466
|
+
const lastUsed = key.lastRequest ? new Date(key.lastRequest).toLocaleDateString() : "never";
|
|
467
|
+
console.log(` ${prefix} ${name} (created: ${created}, last used: ${lastUsed}) [id: ${key.id}]`);
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "revoke": {
|
|
472
|
+
if (!options.id) {
|
|
473
|
+
console.error("--id is required for api-key revoke");
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
const response = await fetch(`${authBaseUrl}/api/auth/api-key/delete-key`, {
|
|
477
|
+
method: "POST",
|
|
478
|
+
headers,
|
|
479
|
+
body: JSON.stringify({ keyId: options.id })
|
|
480
|
+
});
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
const result = await response.json();
|
|
483
|
+
console.error("Failed to revoke API key:", result.error?.message ?? "Unknown error");
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
console.log(`API key ${options.id} revoked.`);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
default:
|
|
490
|
+
console.error(`Unknown api-key subcommand: ${options.subcommand}`);
|
|
491
|
+
console.error("Available: create, list, revoke");
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const formatDate = (dateStr) => {
|
|
496
|
+
return new Date(dateStr).toLocaleDateString();
|
|
497
|
+
};
|
|
498
|
+
const printProfile = (profile, apiBaseUrl) => {
|
|
499
|
+
console.log("Logged in\n");
|
|
500
|
+
console.log(` User ID: ${profile.userId}`);
|
|
501
|
+
if (profile.headline) {
|
|
502
|
+
console.log(` Headline: ${profile.headline}`);
|
|
503
|
+
}
|
|
504
|
+
if (profile.bio) {
|
|
505
|
+
console.log(` Bio: ${profile.bio}`);
|
|
506
|
+
}
|
|
507
|
+
if (profile.website) {
|
|
508
|
+
console.log(` Website: ${profile.website}`);
|
|
509
|
+
}
|
|
510
|
+
if (profile.socialLinks && profile.socialLinks.length > 0) {
|
|
511
|
+
for (const link of profile.socialLinks) {
|
|
512
|
+
const display = link.username ? `@${link.username}` : link.url ?? link.provider;
|
|
513
|
+
console.log(` ${link.provider}:${" ".repeat(Math.max(1, 10 - link.provider.length))}${display}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
console.log(` Created: ${formatDate(profile.createdAt)}`);
|
|
517
|
+
console.log(` Updated: ${formatDate(profile.updatedAt)}`);
|
|
518
|
+
console.log(`
|
|
519
|
+
API URL: ${apiBaseUrl}`);
|
|
520
|
+
console.log(` Creds: ${getCredentialsPath()}`);
|
|
521
|
+
};
|
|
522
|
+
const profileCommand = async (options) => {
|
|
523
|
+
if (options.apiKey) {
|
|
524
|
+
const client2 = createCliApiClient({
|
|
525
|
+
baseUrl: options.apiUrl,
|
|
526
|
+
apiKey: options.apiKey
|
|
527
|
+
});
|
|
528
|
+
const result2 = await client2.me();
|
|
529
|
+
if (!result2.success) {
|
|
530
|
+
console.error("Failed to fetch profile:", result2.error.message);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
if (options.json) {
|
|
534
|
+
console.log(JSON.stringify(result2.data, null, 2));
|
|
535
|
+
} else {
|
|
536
|
+
printProfile(result2.data, options.apiUrl);
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const creds = await loadCredentials();
|
|
541
|
+
if (!creds) {
|
|
542
|
+
console.log("Not logged in. Run `zako login` first.");
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
const apiBaseUrl = options.apiUrl ?? creds.apiBaseUrl;
|
|
546
|
+
const client = createCliApiClient({
|
|
547
|
+
baseUrl: apiBaseUrl,
|
|
548
|
+
apiKey: creds.apiKey
|
|
549
|
+
});
|
|
550
|
+
const result = await client.me();
|
|
551
|
+
if (!result.success) {
|
|
552
|
+
console.error("Failed to fetch profile:", result.error.message);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
if (options.json) {
|
|
556
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
557
|
+
} else {
|
|
558
|
+
printProfile(result.data, apiBaseUrl);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
const DEFAULT_API_URL = "https://zakoducthunt.jp";
|
|
562
|
+
const DEFAULT_WEB_URL = "https://zakoducthunt.jp";
|
|
563
|
+
const VERSION = "0.1.0";
|
|
564
|
+
const HELP_TEXT = `
|
|
565
|
+
zako - zakoducthunt CLI
|
|
566
|
+
|
|
567
|
+
Usage:
|
|
568
|
+
zako <command> [options]
|
|
569
|
+
|
|
570
|
+
Commands:
|
|
571
|
+
login Log in via browser (OAuth)
|
|
572
|
+
logout Clear stored credentials
|
|
573
|
+
profile Show current user profile
|
|
574
|
+
search Search products
|
|
575
|
+
publish Publish product from zakoducthunt.config.json
|
|
576
|
+
init Initialize zakoducthunt.config.json
|
|
577
|
+
api-key Manage API keys (create, list, revoke)
|
|
578
|
+
|
|
579
|
+
Global Options:
|
|
580
|
+
--api-url <url> API base URL (default: ${DEFAULT_API_URL})
|
|
581
|
+
--web-url <url> Web app URL (default: ${DEFAULT_WEB_URL})
|
|
582
|
+
--api-key <key> API key (overrides stored credentials)
|
|
583
|
+
--json Output as JSON
|
|
584
|
+
--help, -h Show help
|
|
585
|
+
--version, -v Show version
|
|
586
|
+
|
|
587
|
+
Examples:
|
|
588
|
+
zako login
|
|
589
|
+
zako search --query "my app"
|
|
590
|
+
zako init
|
|
591
|
+
zako publish
|
|
592
|
+
zako api-key create --name "CI deploy"
|
|
593
|
+
zako api-key list
|
|
594
|
+
zako api-key revoke --id <key-id>
|
|
595
|
+
`.trim();
|
|
596
|
+
const main = async () => {
|
|
597
|
+
const { positionals, values } = parseArgs({
|
|
598
|
+
allowPositionals: true,
|
|
599
|
+
options: {
|
|
600
|
+
help: { type: "boolean", short: "h", default: false },
|
|
601
|
+
version: { type: "boolean", short: "v", default: false },
|
|
602
|
+
"api-url": { type: "string" },
|
|
603
|
+
"web-url": { type: "string" },
|
|
604
|
+
"api-key": { type: "string" },
|
|
605
|
+
json: { type: "boolean", default: false },
|
|
606
|
+
query: { type: "string" },
|
|
607
|
+
name: { type: "string" },
|
|
608
|
+
id: { type: "string" }
|
|
609
|
+
},
|
|
610
|
+
strict: false
|
|
611
|
+
});
|
|
612
|
+
if (values.version) {
|
|
613
|
+
console.log(`zako ${VERSION}`);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (values.help || positionals.length === 0) {
|
|
617
|
+
console.log(HELP_TEXT);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const command = positionals[0];
|
|
621
|
+
const apiUrl = values["api-url"] ?? DEFAULT_API_URL;
|
|
622
|
+
const webUrl = values["web-url"] ?? DEFAULT_WEB_URL;
|
|
623
|
+
const apiKey = values["api-key"];
|
|
624
|
+
const json = values.json ?? false;
|
|
625
|
+
try {
|
|
626
|
+
switch (command) {
|
|
627
|
+
case "login":
|
|
628
|
+
await loginCommand({ apiUrl, webUrl });
|
|
629
|
+
break;
|
|
630
|
+
case "logout":
|
|
631
|
+
await logoutCommand();
|
|
632
|
+
break;
|
|
633
|
+
case "profile":
|
|
634
|
+
await profileCommand({ apiKey, apiUrl, defaultApiUrl: apiUrl, json });
|
|
635
|
+
break;
|
|
636
|
+
case "search": {
|
|
637
|
+
const query = values.query;
|
|
638
|
+
if (!query) {
|
|
639
|
+
console.error("--query is required for search");
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
await searchCommand({ query, apiUrl, json });
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
case "publish":
|
|
646
|
+
await publishCommand({ apiKey, apiUrl, defaultApiUrl: apiUrl, json });
|
|
647
|
+
break;
|
|
648
|
+
case "init":
|
|
649
|
+
await initCommand({ apiUrl });
|
|
650
|
+
break;
|
|
651
|
+
case "api-key": {
|
|
652
|
+
const subcommand = positionals[1];
|
|
653
|
+
if (!subcommand) {
|
|
654
|
+
console.error("Subcommand required: create, list, revoke");
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
await apiKeyCommand({
|
|
658
|
+
subcommand,
|
|
659
|
+
name: values.name,
|
|
660
|
+
id: values.id,
|
|
661
|
+
apiKey,
|
|
662
|
+
apiUrl,
|
|
663
|
+
defaultApiUrl: apiUrl,
|
|
664
|
+
json
|
|
665
|
+
});
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
default:
|
|
669
|
+
console.error(`Unknown command: ${command}`);
|
|
670
|
+
console.log(HELP_TEXT);
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (error instanceof Error) {
|
|
675
|
+
console.error(`Error: ${error.message}`);
|
|
676
|
+
} else {
|
|
677
|
+
console.error("An unknown error occurred.");
|
|
678
|
+
}
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zako",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"zako": "./dist/cli.mjs"
|
|
7
|
+
},
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "vite build",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "bun test",
|
|
13
|
+
"cli": "bun run ./src/cli.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"zod": "^3.25.36"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22",
|
|
20
|
+
"typescript": "^5",
|
|
21
|
+
"vite": "^6"
|
|
22
|
+
}
|
|
23
|
+
}
|