sintetica-cli 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/commands/assets.d.ts +2 -0
- package/dist/commands/assets.js +67 -0
- package/dist/commands/credits.d.ts +2 -0
- package/dist/commands/credits.js +17 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +97 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +83 -0
- package/dist/commands/pipeline.d.ts +8 -0
- package/dist/commands/pipeline.js +112 -0
- package/dist/commands/products.d.ts +2 -0
- package/dist/commands/products.js +71 -0
- package/dist/commands/topup.d.ts +2 -0
- package/dist/commands/topup.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/dist/lib/api.d.ts +17 -0
- package/dist/lib/api.js +55 -0
- package/dist/lib/config.d.ts +9 -0
- package/dist/lib/config.js +42 -0
- package/package.json +36 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assetsCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const api_js_1 = require("../lib/api.js");
|
|
7
|
+
const listCommand = new commander_1.Command('list')
|
|
8
|
+
.description('List assets')
|
|
9
|
+
.option('--product <id>', 'Filter by product ID')
|
|
10
|
+
.option('--limit <n>', 'Max results', '20')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
try {
|
|
13
|
+
const params = { limit: opts.limit };
|
|
14
|
+
if (opts.product)
|
|
15
|
+
params.productId = opts.product;
|
|
16
|
+
const data = await (0, api_js_1.api)('/api/cli/assets', { params });
|
|
17
|
+
console.log(JSON.stringify(data));
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Failed' }));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const downloadCommand = new commander_1.Command('download')
|
|
25
|
+
.description('Download an asset (optionally optimized for a platform)')
|
|
26
|
+
.argument('<id>', 'Asset ID')
|
|
27
|
+
.option('--preset <preset>', 'Optimization preset (shopify, instagram, facebook, web, email, thumbnail)')
|
|
28
|
+
.option('--out <path>', 'Output file path')
|
|
29
|
+
.action(async (id, opts) => {
|
|
30
|
+
try {
|
|
31
|
+
if (opts.preset) {
|
|
32
|
+
// Use optimize endpoint
|
|
33
|
+
const { buffer, fileName } = await (0, api_js_1.downloadBinary)('/api/assets/optimize', {
|
|
34
|
+
assetId: id,
|
|
35
|
+
preset: opts.preset,
|
|
36
|
+
});
|
|
37
|
+
const outPath = opts.out || fileName;
|
|
38
|
+
(0, fs_1.writeFileSync)(outPath, buffer);
|
|
39
|
+
console.log(JSON.stringify({ downloaded: outPath, size: buffer.length }));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Fetch asset URL then download the original
|
|
43
|
+
const data = await (0, api_js_1.api)('/api/cli/assets', {
|
|
44
|
+
params: { limit: '100' },
|
|
45
|
+
});
|
|
46
|
+
const asset = data.assets.find(a => a.id === id);
|
|
47
|
+
if (!asset) {
|
|
48
|
+
throw new Error('Asset not found');
|
|
49
|
+
}
|
|
50
|
+
const res = await fetch(asset.cdnUrl);
|
|
51
|
+
if (!res.ok)
|
|
52
|
+
throw new Error('Failed to download');
|
|
53
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
54
|
+
const outPath = opts.out || asset.fileName || `asset-${id}.png`;
|
|
55
|
+
(0, fs_1.writeFileSync)(outPath, buffer);
|
|
56
|
+
console.log(JSON.stringify({ downloaded: outPath, size: buffer.length }));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Download failed' }));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
exports.assetsCommand = new commander_1.Command('assets')
|
|
65
|
+
.description('Manage assets')
|
|
66
|
+
.addCommand(listCommand)
|
|
67
|
+
.addCommand(downloadCommand);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.creditsCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const api_js_1 = require("../lib/api.js");
|
|
6
|
+
exports.creditsCommand = new commander_1.Command('credits')
|
|
7
|
+
.description('Check credit balance')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const data = await (0, api_js_1.api)('/api/cli/credits');
|
|
11
|
+
console.log(JSON.stringify(data));
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Failed to check credits' }));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const api_js_1 = require("../lib/api.js");
|
|
6
|
+
async function pollUntilDone(jobId) {
|
|
7
|
+
const POLL_INTERVAL = 5000;
|
|
8
|
+
const MAX_POLLS = 120; // 10 minutes max
|
|
9
|
+
for (let i = 0; i < MAX_POLLS; i++) {
|
|
10
|
+
const status = await (0, api_js_1.api)('/api/cli/generate/status', {
|
|
11
|
+
params: { jobId },
|
|
12
|
+
});
|
|
13
|
+
if (status.status === 'completed') {
|
|
14
|
+
return status;
|
|
15
|
+
}
|
|
16
|
+
if (status.status === 'failed') {
|
|
17
|
+
throw new Error(status.error || 'Generation failed');
|
|
18
|
+
}
|
|
19
|
+
// Print progress to stderr (keeps stdout clean for JSON)
|
|
20
|
+
process.stderr.write(`\r Status: ${status.status} | Progress: ${status.progress || 0}%`);
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
|
|
22
|
+
}
|
|
23
|
+
throw new Error('Generation timed out after 10 minutes');
|
|
24
|
+
}
|
|
25
|
+
const generateAction = new commander_1.Command('generate')
|
|
26
|
+
.description('Generate product photos')
|
|
27
|
+
.requiredOption('--product <id>', 'Product ID')
|
|
28
|
+
.option('--style <style>', 'Style preset (minimal, lifestyle, studio, editorial, flat_lay, nature)', 'minimal')
|
|
29
|
+
.option('--format <format>', 'Image format (instagram_square, instagram_story, facebook_post, tiktok, product_hero)', 'instagram_square')
|
|
30
|
+
.option('--count <n>', 'Number of variations', '4')
|
|
31
|
+
.option('--wait', 'Wait for generation to complete')
|
|
32
|
+
.action(async (opts) => {
|
|
33
|
+
try {
|
|
34
|
+
const result = await (0, api_js_1.api)('/api/cli/generate', {
|
|
35
|
+
body: {
|
|
36
|
+
productId: opts.product,
|
|
37
|
+
style: opts.style,
|
|
38
|
+
format: opts.format,
|
|
39
|
+
count: parseInt(opts.count),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
if (!opts.wait) {
|
|
43
|
+
console.log(JSON.stringify(result));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Poll until done
|
|
47
|
+
process.stderr.write(` Job ${result.jobId} started (${result.creditsCost} credits)\n`);
|
|
48
|
+
const finalStatus = await pollUntilDone(result.jobId);
|
|
49
|
+
process.stderr.write('\n');
|
|
50
|
+
console.log(JSON.stringify(finalStatus));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Generation failed' }));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const statusCommand = new commander_1.Command('status')
|
|
58
|
+
.description('Check generation job status')
|
|
59
|
+
.requiredOption('--job <id>', 'Job ID')
|
|
60
|
+
.option('--wait', 'Wait for completion')
|
|
61
|
+
.action(async (opts) => {
|
|
62
|
+
try {
|
|
63
|
+
if (opts.wait) {
|
|
64
|
+
const finalStatus = await pollUntilDone(opts.job);
|
|
65
|
+
process.stderr.write('\n');
|
|
66
|
+
console.log(JSON.stringify(finalStatus));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const status = await (0, api_js_1.api)('/api/cli/generate/status', {
|
|
70
|
+
params: { jobId: opts.job },
|
|
71
|
+
});
|
|
72
|
+
console.log(JSON.stringify(status));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Failed' }));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
exports.generateCommand = new commander_1.Command('generate')
|
|
81
|
+
.description('Generate product photos');
|
|
82
|
+
// Make the default action work as `sintetica generate --product <id>`
|
|
83
|
+
exports.generateCommand
|
|
84
|
+
.option('--product <id>', 'Product ID')
|
|
85
|
+
.option('--style <style>', 'Style preset', 'minimal')
|
|
86
|
+
.option('--format <format>', 'Image format', 'instagram_square')
|
|
87
|
+
.option('--count <n>', 'Number of variations', '4')
|
|
88
|
+
.option('--wait', 'Wait for completion')
|
|
89
|
+
.action(async (opts) => {
|
|
90
|
+
if (opts.product) {
|
|
91
|
+
await generateAction.parseAsync(['', '', '--product', opts.product, '--style', opts.style, '--format', opts.format, '--count', opts.count, ...(opts.wait ? ['--wait'] : [])]);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
exports.generateCommand.help();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
exports.generateCommand.addCommand(statusCommand);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loginCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const config_js_1 = require("../lib/config.js");
|
|
6
|
+
exports.loginCommand = new commander_1.Command('login')
|
|
7
|
+
.description('Authenticate with email/password or API key')
|
|
8
|
+
.option('--key <key>', 'API key (sk_live_...)')
|
|
9
|
+
.option('--email <email>', 'Account email')
|
|
10
|
+
.option('--password <password>', 'Account password')
|
|
11
|
+
.option('--base-url <url>', 'Server URL (default: https://sintetica.ro)')
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
if (opts.baseUrl) {
|
|
14
|
+
(0, config_js_1.setConfig)({ baseUrl: opts.baseUrl });
|
|
15
|
+
}
|
|
16
|
+
const baseUrl = opts.baseUrl || (0, config_js_1.getBaseUrl)();
|
|
17
|
+
// Method 1: API key
|
|
18
|
+
if (opts.key) {
|
|
19
|
+
if (!opts.key.startsWith('sk_live_')) {
|
|
20
|
+
console.error(JSON.stringify({ error: 'Invalid API key format. Must start with sk_live_' }));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`${baseUrl}/api/cli/credits`, {
|
|
25
|
+
headers: { 'Authorization': `Bearer ${opts.key}` },
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
console.error(JSON.stringify({ error: 'Invalid API key' }));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
(0, config_js_1.setConfig)({ apiKey: opts.key });
|
|
33
|
+
console.log(JSON.stringify({
|
|
34
|
+
success: true,
|
|
35
|
+
method: 'api_key',
|
|
36
|
+
balance: data.balance,
|
|
37
|
+
tier: data.tier,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(JSON.stringify({ error: `Connection failed: ${err instanceof Error ? err.message : 'Unknown'}` }));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Method 2: Email/password → auto-generates API key
|
|
47
|
+
if (opts.email && opts.password) {
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`${baseUrl}/api/cli/auth`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ email: opts.email, password: opts.password }),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const err = await res.json();
|
|
56
|
+
console.error(JSON.stringify({ error: err.error || 'Authentication failed' }));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
(0, config_js_1.setConfig)({ apiKey: data.apiKey });
|
|
61
|
+
console.log(JSON.stringify({
|
|
62
|
+
success: true,
|
|
63
|
+
method: 'email',
|
|
64
|
+
email: data.email,
|
|
65
|
+
credits: data.credits,
|
|
66
|
+
tier: data.tier,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.error(JSON.stringify({ error: `Connection failed: ${err instanceof Error ? err.message : 'Unknown'}` }));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.error(JSON.stringify({
|
|
76
|
+
error: 'Provide either --key or --email + --password',
|
|
77
|
+
usage: [
|
|
78
|
+
'sintetica login --email user@example.com --password mypassword',
|
|
79
|
+
'sintetica login --key sk_live_...',
|
|
80
|
+
],
|
|
81
|
+
}));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* sintetica pipeline — End-to-end: URL → product → generate → download
|
|
4
|
+
*
|
|
5
|
+
* Example:
|
|
6
|
+
* sintetica pipeline --url https://example.com/product --style minimal --out ./photos/
|
|
7
|
+
*/
|
|
8
|
+
export declare const pipelineCommand: Command;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pipelineCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const api_js_1 = require("../lib/api.js");
|
|
8
|
+
/**
|
|
9
|
+
* sintetica pipeline — End-to-end: URL → product → generate → download
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* sintetica pipeline --url https://example.com/product --style minimal --out ./photos/
|
|
13
|
+
*/
|
|
14
|
+
exports.pipelineCommand = new commander_1.Command('pipeline')
|
|
15
|
+
.description('End-to-end: import product from URL → generate photos → download')
|
|
16
|
+
.requiredOption('--url <url>', 'Product page URL to import')
|
|
17
|
+
.option('--name <name>', 'Override product name')
|
|
18
|
+
.option('--category <cat>', 'Product category', 'beauty')
|
|
19
|
+
.option('--style <style>', 'Photo style (minimal, lifestyle, studio, editorial, flat_lay, nature)', 'minimal')
|
|
20
|
+
.option('--format <format>', 'Image format (instagram_square, product_hero, etc.)', 'instagram_square')
|
|
21
|
+
.option('--count <n>', 'Number of variations', '4')
|
|
22
|
+
.option('--preset <preset>', 'Download preset (shopify, instagram, web, etc.)')
|
|
23
|
+
.option('--out <dir>', 'Output directory for downloaded photos', './sintetica-output')
|
|
24
|
+
.action(async (opts) => {
|
|
25
|
+
const { url, name, category, style, format, count, preset, out, } = opts;
|
|
26
|
+
const steps = [];
|
|
27
|
+
const log = (step, data) => {
|
|
28
|
+
steps.push({ step, ...data });
|
|
29
|
+
process.stderr.write(` [${steps.length}] ${step}\n`);
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
// Step 1: Import product
|
|
33
|
+
log('import', { url });
|
|
34
|
+
const product = await (0, api_js_1.api)('/api/cli/products/import', {
|
|
35
|
+
body: { url, name, category },
|
|
36
|
+
});
|
|
37
|
+
log('product_created', { id: product.id, name: product.name });
|
|
38
|
+
// Step 2: Generate photos
|
|
39
|
+
log('generate', { productId: product.id, style, format, count });
|
|
40
|
+
const gen = await (0, api_js_1.api)('/api/cli/generate', {
|
|
41
|
+
body: {
|
|
42
|
+
productId: product.id,
|
|
43
|
+
style,
|
|
44
|
+
format,
|
|
45
|
+
count: parseInt(count),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
log('job_submitted', { jobId: gen.jobId, creditsCost: gen.creditsCost });
|
|
49
|
+
// Step 3: Poll until complete
|
|
50
|
+
const POLL_INTERVAL = 5000;
|
|
51
|
+
const MAX_POLLS = 120;
|
|
52
|
+
let result = null;
|
|
53
|
+
for (let i = 0; i < MAX_POLLS; i++) {
|
|
54
|
+
const status = await (0, api_js_1.api)('/api/cli/generate/status', {
|
|
55
|
+
params: { jobId: gen.jobId },
|
|
56
|
+
});
|
|
57
|
+
if (status.status === 'completed') {
|
|
58
|
+
result = status;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
if (status.status === 'failed') {
|
|
62
|
+
throw new Error(status.error || 'Generation failed');
|
|
63
|
+
}
|
|
64
|
+
process.stderr.write(`\r Generating... (${i * 5}s)`);
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
|
|
66
|
+
}
|
|
67
|
+
if (!result)
|
|
68
|
+
throw new Error('Generation timed out');
|
|
69
|
+
process.stderr.write('\n');
|
|
70
|
+
log('generation_complete', { assets: result.assets.length });
|
|
71
|
+
// Step 4: Download all assets
|
|
72
|
+
if (!(0, fs_1.existsSync)(out))
|
|
73
|
+
(0, fs_1.mkdirSync)(out, { recursive: true });
|
|
74
|
+
const downloaded = [];
|
|
75
|
+
for (let i = 0; i < result.assets.length; i++) {
|
|
76
|
+
const asset = result.assets[i];
|
|
77
|
+
const fileName = `${product.name.replace(/[^a-zA-Z0-9]/g, '_')}_${style}_${i + 1}.${preset === 'web' ? 'webp' : 'jpg'}`;
|
|
78
|
+
const filePath = (0, path_1.join)(out, fileName);
|
|
79
|
+
if (preset) {
|
|
80
|
+
const { buffer } = await (0, api_js_1.downloadBinary)('/api/assets/optimize', {
|
|
81
|
+
assetId: asset.id,
|
|
82
|
+
preset,
|
|
83
|
+
});
|
|
84
|
+
(0, fs_1.writeFileSync)(filePath, buffer);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const res = await fetch(asset.cdnUrl);
|
|
88
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
89
|
+
(0, fs_1.writeFileSync)(filePath, buffer);
|
|
90
|
+
}
|
|
91
|
+
downloaded.push(filePath);
|
|
92
|
+
process.stderr.write(` Downloaded ${i + 1}/${result.assets.length}: ${fileName}\n`);
|
|
93
|
+
}
|
|
94
|
+
// Final output
|
|
95
|
+
console.log(JSON.stringify({
|
|
96
|
+
success: true,
|
|
97
|
+
product: { id: product.id, name: product.name },
|
|
98
|
+
jobId: gen.jobId,
|
|
99
|
+
assets: result.assets,
|
|
100
|
+
downloaded,
|
|
101
|
+
outputDir: out,
|
|
102
|
+
steps,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(JSON.stringify({
|
|
107
|
+
error: err instanceof Error ? err.message : 'Pipeline failed',
|
|
108
|
+
steps,
|
|
109
|
+
}));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.productsCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const api_js_1 = require("../lib/api.js");
|
|
6
|
+
const listCommand = new commander_1.Command('list')
|
|
7
|
+
.description('List products')
|
|
8
|
+
.option('--limit <n>', 'Max results', '20')
|
|
9
|
+
.action(async (opts) => {
|
|
10
|
+
try {
|
|
11
|
+
const data = await (0, api_js_1.api)('/api/cli/products', {
|
|
12
|
+
params: { limit: opts.limit },
|
|
13
|
+
});
|
|
14
|
+
console.log(JSON.stringify(data));
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Failed' }));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const createCommand = new commander_1.Command('create')
|
|
22
|
+
.description('Create a product from an image URL')
|
|
23
|
+
.requiredOption('--name <name>', 'Product name')
|
|
24
|
+
.requiredOption('--image-url <url>', 'URL to product image')
|
|
25
|
+
.option('--description <desc>', 'Product description')
|
|
26
|
+
.option('--category <cat>', 'Category (beauty, wellness, pharma, apparel, etc.)')
|
|
27
|
+
.option('--brand-name <brand>', 'Brand name')
|
|
28
|
+
.action(async (opts) => {
|
|
29
|
+
try {
|
|
30
|
+
const data = await (0, api_js_1.api)('/api/cli/products', {
|
|
31
|
+
body: {
|
|
32
|
+
name: opts.name,
|
|
33
|
+
imageUrl: opts.imageUrl,
|
|
34
|
+
description: opts.description,
|
|
35
|
+
category: opts.category,
|
|
36
|
+
brandName: opts.brandName,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
console.log(JSON.stringify(data));
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Failed' }));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const importCommand = new commander_1.Command('import')
|
|
47
|
+
.description('Import a product from any e-commerce URL (scrapes name, image, description)')
|
|
48
|
+
.requiredOption('--url <url>', 'Product page URL')
|
|
49
|
+
.option('--name <name>', 'Override extracted product name')
|
|
50
|
+
.option('--category <cat>', 'Category (beauty, wellness, pharma, apparel, etc.)')
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
try {
|
|
53
|
+
const data = await (0, api_js_1.api)('/api/cli/products/import', {
|
|
54
|
+
body: {
|
|
55
|
+
url: opts.url,
|
|
56
|
+
name: opts.name,
|
|
57
|
+
category: opts.category,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
console.log(JSON.stringify(data));
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Import failed' }));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
exports.productsCommand = new commander_1.Command('products')
|
|
68
|
+
.description('Manage products')
|
|
69
|
+
.addCommand(listCommand)
|
|
70
|
+
.addCommand(createCommand)
|
|
71
|
+
.addCommand(importCommand);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.topupCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const api_js_1 = require("../lib/api.js");
|
|
6
|
+
exports.topupCommand = new commander_1.Command('topup')
|
|
7
|
+
.description('Buy credits via Stripe')
|
|
8
|
+
.option('--package <id>', 'Package ID (credits_50, credits_150, credits_500, credits_1000)')
|
|
9
|
+
.option('--list', 'List available packages')
|
|
10
|
+
.action(async (opts) => {
|
|
11
|
+
try {
|
|
12
|
+
// List packages
|
|
13
|
+
if (opts.list || !opts.package) {
|
|
14
|
+
const data = await (0, api_js_1.api)('/api/cli/topup');
|
|
15
|
+
if (!opts.package) {
|
|
16
|
+
console.log(JSON.stringify({
|
|
17
|
+
...data,
|
|
18
|
+
usage: 'sintetica topup --package credits_50',
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(JSON.stringify(data));
|
|
23
|
+
}
|
|
24
|
+
if (!opts.package)
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Create checkout
|
|
28
|
+
if (opts.package) {
|
|
29
|
+
const data = await (0, api_js_1.api)('/api/cli/topup', { body: { packageId: opts.package } });
|
|
30
|
+
console.log(JSON.stringify(data));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : 'Failed' }));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const login_js_1 = require("./commands/login.js");
|
|
6
|
+
const credits_js_1 = require("./commands/credits.js");
|
|
7
|
+
const topup_js_1 = require("./commands/topup.js");
|
|
8
|
+
const products_js_1 = require("./commands/products.js");
|
|
9
|
+
const generate_js_1 = require("./commands/generate.js");
|
|
10
|
+
const assets_js_1 = require("./commands/assets.js");
|
|
11
|
+
const pipeline_js_1 = require("./commands/pipeline.js");
|
|
12
|
+
const program = new commander_1.Command();
|
|
13
|
+
program
|
|
14
|
+
.name('sintetica')
|
|
15
|
+
.description('Sintetica CLI — AI product photography from your terminal')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
program.addCommand(login_js_1.loginCommand);
|
|
18
|
+
program.addCommand(credits_js_1.creditsCommand);
|
|
19
|
+
program.addCommand(topup_js_1.topupCommand);
|
|
20
|
+
program.addCommand(products_js_1.productsCommand);
|
|
21
|
+
program.addCommand(generate_js_1.generateCommand);
|
|
22
|
+
program.addCommand(assets_js_1.assetsCommand);
|
|
23
|
+
program.addCommand(pipeline_js_1.pipelineCommand);
|
|
24
|
+
program.parse();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface ApiOptions {
|
|
2
|
+
method?: string;
|
|
3
|
+
body?: Record<string, unknown>;
|
|
4
|
+
params?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Make an authenticated API call to the Sintetica server.
|
|
8
|
+
*/
|
|
9
|
+
export declare function api<T = unknown>(path: string, options?: ApiOptions): Promise<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Download a binary response (for asset downloads).
|
|
12
|
+
*/
|
|
13
|
+
export declare function downloadBinary(path: string, body?: Record<string, unknown>): Promise<{
|
|
14
|
+
buffer: Buffer;
|
|
15
|
+
fileName: string;
|
|
16
|
+
}>;
|
|
17
|
+
export {};
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.api = api;
|
|
4
|
+
exports.downloadBinary = downloadBinary;
|
|
5
|
+
const config_js_1 = require("./config.js");
|
|
6
|
+
/**
|
|
7
|
+
* Make an authenticated API call to the Sintetica server.
|
|
8
|
+
*/
|
|
9
|
+
async function api(path, options = {}) {
|
|
10
|
+
const baseUrl = (0, config_js_1.getBaseUrl)();
|
|
11
|
+
const apiKey = (0, config_js_1.getApiKey)();
|
|
12
|
+
let url = `${baseUrl}${path}`;
|
|
13
|
+
if (options.params) {
|
|
14
|
+
const searchParams = new URLSearchParams(options.params);
|
|
15
|
+
url += `?${searchParams.toString()}`;
|
|
16
|
+
}
|
|
17
|
+
const res = await fetch(url, {
|
|
18
|
+
method: options.method || (options.body ? 'POST' : 'GET'),
|
|
19
|
+
headers: {
|
|
20
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
},
|
|
23
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const errorBody = await res.json().catch(() => ({ error: res.statusText }));
|
|
27
|
+
const msg = errorBody.error || `HTTP ${res.status}`;
|
|
28
|
+
throw new Error(msg);
|
|
29
|
+
}
|
|
30
|
+
return res.json();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Download a binary response (for asset downloads).
|
|
34
|
+
*/
|
|
35
|
+
async function downloadBinary(path, body) {
|
|
36
|
+
const baseUrl = (0, config_js_1.getBaseUrl)();
|
|
37
|
+
const apiKey = (0, config_js_1.getApiKey)();
|
|
38
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
},
|
|
44
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const errorBody = await res.json().catch(() => ({ error: res.statusText }));
|
|
48
|
+
throw new Error(errorBody.error || `HTTP ${res.status}`);
|
|
49
|
+
}
|
|
50
|
+
const disposition = res.headers.get('content-disposition') || '';
|
|
51
|
+
const fileNameMatch = disposition.match(/filename="(.+)"/);
|
|
52
|
+
const fileName = fileNameMatch?.[1] || 'download';
|
|
53
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
54
|
+
return { buffer, fileName };
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function getConfig(): Config;
|
|
6
|
+
export declare function setConfig(updates: Partial<Config>): void;
|
|
7
|
+
export declare function getApiKey(): string;
|
|
8
|
+
export declare function getBaseUrl(): string;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getConfig = getConfig;
|
|
4
|
+
exports.setConfig = setConfig;
|
|
5
|
+
exports.getApiKey = getApiKey;
|
|
6
|
+
exports.getBaseUrl = getBaseUrl;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const os_1 = require("os");
|
|
10
|
+
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.sintetica');
|
|
11
|
+
const CONFIG_FILE = (0, path_1.join)(CONFIG_DIR, 'config.json');
|
|
12
|
+
function getConfig() {
|
|
13
|
+
if (!(0, fs_1.existsSync)(CONFIG_FILE))
|
|
14
|
+
return {};
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf-8'));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function setConfig(updates) {
|
|
23
|
+
if (!(0, fs_1.existsSync)(CONFIG_DIR)) {
|
|
24
|
+
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
const current = getConfig();
|
|
27
|
+
const merged = { ...current, ...updates };
|
|
28
|
+
(0, fs_1.writeFileSync)(CONFIG_FILE, JSON.stringify(merged, null, 2));
|
|
29
|
+
}
|
|
30
|
+
function getApiKey() {
|
|
31
|
+
const config = getConfig();
|
|
32
|
+
const key = process.env.SINTETICA_API_KEY || config.apiKey;
|
|
33
|
+
if (!key) {
|
|
34
|
+
console.error('No API key configured. Run: sintetica login --key sk_live_...');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
return key;
|
|
38
|
+
}
|
|
39
|
+
function getBaseUrl() {
|
|
40
|
+
const config = getConfig();
|
|
41
|
+
return process.env.SINTETICA_BASE_URL || config.baseUrl || 'https://sintetica.ro';
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sintetica-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sintetica CLI — AI product photography from your terminal. Generate professional product photos with one command.",
|
|
5
|
+
"keywords": ["sintetica", "ai", "product-photography", "ecommerce", "shopify", "cli", "claude-code"],
|
|
6
|
+
"author": "Sintetica <hello@sintetica.ro>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://sintetica.ro/cli",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/edholofy/sintetica.ro",
|
|
12
|
+
"directory": "packages/cli"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"sintetica": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/**/*"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"dev": "tsx src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"commander": "^13.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.0.0",
|
|
30
|
+
"tsx": "^4.0.0",
|
|
31
|
+
"@types/node": "^22.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|