appshot-cli 0.9.2 → 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/README.md +198 -103
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +9 -0
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/frame.d.ts.map +1 -1
- package/dist/commands/frame.js +11 -2
- package/dist/commands/frame.js.map +1 -1
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +19 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +119 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/core/compose.d.ts +1 -0
- package/dist/core/compose.d.ts.map +1 -1
- package/dist/core/compose.js +14 -2
- package/dist/core/compose.js.map +1 -1
- package/dist/mcp/cli-options.d.ts +112 -0
- package/dist/mcp/cli-options.d.ts.map +1 -0
- package/dist/mcp/cli-options.js +158 -0
- package/dist/mcp/cli-options.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +1149 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/services/doctor.d.ts.map +1 -1
- package/dist/services/doctor.js +2 -1
- package/dist/services/doctor.js.map +1 -1
- package/dist/utils/filename-caption.d.ts +9 -0
- package/dist/utils/filename-caption.d.ts.map +1 -0
- package/dist/utils/filename-caption.js +19 -0
- package/dist/utils/filename-caption.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +7 -4
- package/skill/SKILL.md +225 -0
- package/skill/references/fonts.md +55 -0
- package/skill/references/gradients.md +69 -0
- package/skill/references/templates.md +170 -0
- package/skill/references/troubleshooting.md +228 -0
|
@@ -0,0 +1,1149 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import { APP_VERSION } from '../version.js';
|
|
9
|
+
import { detectLanguagesFromCaptions } from '../utils/language.js';
|
|
10
|
+
import { filenameToCaption } from '../utils/filename-caption.js';
|
|
11
|
+
import { createBuildArgs, createFrameArgs, createExportArgs, createInitArgs, createSpecsArgs, createValidateArgs, createCleanArgs, createLocalizeArgs, createPresetsArgs, createFontsArgs, createTemplateArgs, createQuickstartArgs } from './cli-options.js';
|
|
12
|
+
import { gradientPresets, getGradientPreset, getGradientsByCategory } from '../core/gradient-presets.js';
|
|
13
|
+
// Resolve CLI entry point - handles both dist (cli.js) and dev (cli.ts via tsx) builds
|
|
14
|
+
function resolveCliEntry() {
|
|
15
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
16
|
+
const dir = path.dirname(currentFile);
|
|
17
|
+
// Check if we're in src/ (dev) or dist/ (prod) - cross-platform
|
|
18
|
+
const pathParts = dir.split(path.sep);
|
|
19
|
+
const isDevMode = pathParts.includes('src');
|
|
20
|
+
if (isDevMode) {
|
|
21
|
+
// Dev mode: use tsx to run TypeScript directly
|
|
22
|
+
const tsEntry = path.resolve(dir, '../cli.ts');
|
|
23
|
+
return tsEntry;
|
|
24
|
+
}
|
|
25
|
+
// Prod mode: use compiled JS
|
|
26
|
+
return path.resolve(dir, '../cli.js');
|
|
27
|
+
}
|
|
28
|
+
const CLI_ENTRY = resolveCliEntry();
|
|
29
|
+
// In dev mode, we need to use tsx to run TypeScript
|
|
30
|
+
function getCliCommand() {
|
|
31
|
+
if (CLI_ENTRY.endsWith('.ts')) {
|
|
32
|
+
// Use tsx for TypeScript files
|
|
33
|
+
return {
|
|
34
|
+
execPath: process.execPath,
|
|
35
|
+
args: [path.resolve(path.dirname(CLI_ENTRY), '../node_modules/.bin/tsx'), CLI_ENTRY]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
execPath: process.execPath,
|
|
40
|
+
args: [CLI_ENTRY]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function runAppshotCli(args, options) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const start = Date.now();
|
|
46
|
+
const command = ['appshot', ...args].join(' ');
|
|
47
|
+
const cli = getCliCommand();
|
|
48
|
+
const child = spawn(cli.execPath, [...cli.args, ...args], {
|
|
49
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
FORCE_COLOR: '0'
|
|
53
|
+
// Note: APPSHOT_DISABLE_FONT_SCAN is passed through from environment if set
|
|
54
|
+
// This allows accurate font validation while CI can still disable scanning
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
let stdout = '';
|
|
58
|
+
let stderr = '';
|
|
59
|
+
child.stdout?.on('data', (chunk) => {
|
|
60
|
+
stdout += chunk.toString();
|
|
61
|
+
});
|
|
62
|
+
child.stderr?.on('data', (chunk) => {
|
|
63
|
+
stderr += chunk.toString();
|
|
64
|
+
});
|
|
65
|
+
child.once('error', (error) => reject(error));
|
|
66
|
+
child.once('close', (code) => {
|
|
67
|
+
resolve({
|
|
68
|
+
stdout: stdout.trim(),
|
|
69
|
+
stderr: stderr.trim(),
|
|
70
|
+
exitCode: code ?? 0,
|
|
71
|
+
durationMs: Date.now() - start,
|
|
72
|
+
command
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function cliResultToToolResponse(action, run) {
|
|
78
|
+
const duration = (run.durationMs / 1000).toFixed(1);
|
|
79
|
+
const status = run.exitCode === 0 ? 'succeeded' : `failed (code ${run.exitCode})`;
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: `${action} ${status} in ${duration}s\nCommand: ${run.command}`
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
structuredContent: {
|
|
88
|
+
stdout: run.stdout,
|
|
89
|
+
stderr: run.stderr,
|
|
90
|
+
exitCode: run.exitCode,
|
|
91
|
+
durationMs: run.durationMs,
|
|
92
|
+
command: run.command
|
|
93
|
+
},
|
|
94
|
+
isError: run.exitCode !== 0
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function readAppshotConfig(configPath) {
|
|
98
|
+
let target = path.resolve(process.cwd(), configPath ?? '.appshot/config.json');
|
|
99
|
+
// If path is a directory, append .appshot/config.json
|
|
100
|
+
try {
|
|
101
|
+
const stat = await fs.stat(target);
|
|
102
|
+
if (stat.isDirectory()) {
|
|
103
|
+
target = path.join(target, '.appshot', 'config.json');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Path doesn't exist yet, continue with original target
|
|
108
|
+
}
|
|
109
|
+
const data = await fs.readFile(target, 'utf8');
|
|
110
|
+
const config = JSON.parse(data);
|
|
111
|
+
return { path: target, config };
|
|
112
|
+
}
|
|
113
|
+
export async function startMcpServer() {
|
|
114
|
+
const server = new McpServer({
|
|
115
|
+
name: 'appshot-mcp',
|
|
116
|
+
version: APP_VERSION
|
|
117
|
+
});
|
|
118
|
+
registerProjectInfoTool(server);
|
|
119
|
+
registerDoctorTool(server);
|
|
120
|
+
registerBuildTool(server);
|
|
121
|
+
registerFrameTool(server);
|
|
122
|
+
registerExportTool(server);
|
|
123
|
+
registerInitTool(server);
|
|
124
|
+
registerSpecsTool(server);
|
|
125
|
+
registerValidateTool(server);
|
|
126
|
+
registerCleanTool(server);
|
|
127
|
+
registerLocalizeTool(server);
|
|
128
|
+
registerPresetsTool(server);
|
|
129
|
+
registerLanguagesTool(server);
|
|
130
|
+
registerConfigTool(server);
|
|
131
|
+
registerCaptionsTool(server);
|
|
132
|
+
registerGradientsTool(server);
|
|
133
|
+
registerBackgroundsTool(server);
|
|
134
|
+
registerFontsTool(server);
|
|
135
|
+
registerTemplateTool(server);
|
|
136
|
+
registerQuickstartTool(server);
|
|
137
|
+
const transport = new StdioServerTransport();
|
|
138
|
+
const cleanup = async () => {
|
|
139
|
+
await transport.close();
|
|
140
|
+
process.exit(0);
|
|
141
|
+
};
|
|
142
|
+
process.once('SIGINT', cleanup);
|
|
143
|
+
process.once('SIGTERM', cleanup);
|
|
144
|
+
await server.connect(transport);
|
|
145
|
+
}
|
|
146
|
+
function registerProjectInfoTool(server) {
|
|
147
|
+
const inputSchema = z.object({
|
|
148
|
+
configPath: z.string().optional().describe('Path to appshot.json config file')
|
|
149
|
+
});
|
|
150
|
+
server.registerTool('appshot.projectInfo', {
|
|
151
|
+
title: 'Read project configuration',
|
|
152
|
+
description: 'Loads appshot.json and returns device + language metadata',
|
|
153
|
+
inputSchema,
|
|
154
|
+
outputSchema: z.object({
|
|
155
|
+
path: z.string(),
|
|
156
|
+
deviceCount: z.number(),
|
|
157
|
+
languages: z.array(z.string()).optional(),
|
|
158
|
+
summary: z.record(z.string(), z.any()),
|
|
159
|
+
config: z.record(z.string(), z.any())
|
|
160
|
+
})
|
|
161
|
+
}, async (args) => {
|
|
162
|
+
const { path: resolvedPath, config } = await readAppshotConfig(args.configPath);
|
|
163
|
+
const devices = Object.keys(config.devices ?? {});
|
|
164
|
+
const languages = config.languages ?? (config.defaultLanguage ? [config.defaultLanguage] : undefined);
|
|
165
|
+
return {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: `Loaded ${path.basename(resolvedPath)} with ${devices.length} device(s)`
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
structuredContent: {
|
|
173
|
+
path: resolvedPath,
|
|
174
|
+
deviceCount: devices.length,
|
|
175
|
+
languages,
|
|
176
|
+
summary: {
|
|
177
|
+
output: config.output ?? 'final',
|
|
178
|
+
templates: Object.keys(config.templates ?? {})
|
|
179
|
+
},
|
|
180
|
+
config
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function registerDoctorTool(server) {
|
|
186
|
+
server.registerTool('appshot.doctor', {
|
|
187
|
+
title: 'Run doctor checks',
|
|
188
|
+
description: 'Runs appshot doctor to validate the current project'
|
|
189
|
+
}, async () => {
|
|
190
|
+
const result = await runAppshotCli(['doctor']);
|
|
191
|
+
return cliResultToToolResponse('Doctor', result);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function registerBuildTool(server) {
|
|
195
|
+
const inputSchema = z.object({
|
|
196
|
+
devices: z.array(z.string()).optional().describe('Device types to build (e.g., ["iphone", "ipad"])'),
|
|
197
|
+
presets: z.array(z.string()).optional().describe('App Store presets (e.g., ["iphone-6-9", "ipad-13"])'),
|
|
198
|
+
languages: z.array(z.string()).optional().describe('Languages to build (e.g., ["en", "es", "fr"])'),
|
|
199
|
+
configPath: z.string().optional().describe('Path to appshot.json config file'),
|
|
200
|
+
dryRun: z.boolean().optional().describe('Show what would be built without actually building'),
|
|
201
|
+
preview: z.boolean().optional().describe('Generate preview images'),
|
|
202
|
+
noFrame: z.boolean().optional().describe('Skip device frame overlay'),
|
|
203
|
+
noGradient: z.boolean().optional().describe('Skip gradient background'),
|
|
204
|
+
noCaption: z.boolean().optional().describe('Skip caption text'),
|
|
205
|
+
autoCaption: z.boolean().optional().describe('Auto-generate captions from filenames'),
|
|
206
|
+
backgroundImage: z.string().optional().describe('Path to background image'),
|
|
207
|
+
backgroundFit: z.enum(['cover', 'contain', 'fill', 'scale-down']).optional().describe('Background image fit mode'),
|
|
208
|
+
autoBackground: z.boolean().optional().describe('Auto-detect background images'),
|
|
209
|
+
noBackground: z.boolean().optional().describe('Skip background entirely'),
|
|
210
|
+
outputDir: z.string().optional().describe('Output directory for built screenshots'),
|
|
211
|
+
verbose: z.boolean().optional().describe('Show detailed output'),
|
|
212
|
+
concurrency: z.number().int().positive().optional().describe('Number of parallel builds')
|
|
213
|
+
});
|
|
214
|
+
server.registerTool('appshot.build', {
|
|
215
|
+
title: 'Run appshot build',
|
|
216
|
+
description: 'Generates screenshots for the configured devices and languages',
|
|
217
|
+
inputSchema
|
|
218
|
+
}, async (args) => {
|
|
219
|
+
const typedArgs = args;
|
|
220
|
+
let cwd;
|
|
221
|
+
if (typedArgs.configPath) {
|
|
222
|
+
// Set cwd to project directory for relative path resolution
|
|
223
|
+
const isDir = !typedArgs.configPath.endsWith('.json');
|
|
224
|
+
if (isDir) {
|
|
225
|
+
// Directory path: set cwd and convert to full config path for CLI
|
|
226
|
+
cwd = typedArgs.configPath;
|
|
227
|
+
typedArgs.configPath = path.join(typedArgs.configPath, '.appshot', 'config.json');
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// Full config.json path: derive cwd from it
|
|
231
|
+
cwd = path.dirname(path.dirname(typedArgs.configPath));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const buildArgs = createBuildArgs(typedArgs);
|
|
235
|
+
const result = await runAppshotCli(buildArgs, { cwd });
|
|
236
|
+
return cliResultToToolResponse('Build', result);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function registerFrameTool(server) {
|
|
240
|
+
const inputSchema = z.object({
|
|
241
|
+
input: z.string().describe('Path to screenshot file or directory'),
|
|
242
|
+
outputDir: z.string().optional().describe('Output directory for framed screenshots'),
|
|
243
|
+
device: z.string().optional().describe('Device type override (e.g., "iphone", "ipad")'),
|
|
244
|
+
recursive: z.boolean().optional().describe('Process directories recursively'),
|
|
245
|
+
format: z.enum(['png', 'jpeg']).optional().describe('Output image format'),
|
|
246
|
+
suffix: z.string().optional().describe('Suffix to add to output filenames'),
|
|
247
|
+
overwrite: z.boolean().optional().describe('Overwrite existing output files'),
|
|
248
|
+
dryRun: z.boolean().optional().describe('Show what would be processed without doing it'),
|
|
249
|
+
verbose: z.boolean().optional().describe('Show detailed output'),
|
|
250
|
+
frameTone: z.enum(['original', 'neutral']).optional().describe('Frame color tone')
|
|
251
|
+
});
|
|
252
|
+
server.registerTool('appshot.frame', {
|
|
253
|
+
title: 'Apply device frames',
|
|
254
|
+
description: 'Wraps the frame CLI for MCP clients',
|
|
255
|
+
inputSchema
|
|
256
|
+
}, async (args) => {
|
|
257
|
+
const frameArgs = createFrameArgs(args);
|
|
258
|
+
const result = await runAppshotCli(frameArgs);
|
|
259
|
+
return cliResultToToolResponse('Frame', result);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function registerExportTool(server) {
|
|
263
|
+
const inputSchema = z.object({
|
|
264
|
+
format: z.string().optional().describe('Export format (e.g., "fastlane")'),
|
|
265
|
+
sourceDir: z.string().optional().describe('Source directory with built screenshots'),
|
|
266
|
+
outputDir: z.string().optional().describe('Output directory for exported files'),
|
|
267
|
+
languages: z.array(z.string()).optional().describe('Languages to export'),
|
|
268
|
+
devices: z.array(z.string()).optional().describe('Devices to export'),
|
|
269
|
+
copy: z.boolean().optional().describe('Copy files instead of moving'),
|
|
270
|
+
flatten: z.boolean().optional().describe('Flatten directory structure'),
|
|
271
|
+
prefixDevice: z.boolean().optional().describe('Prefix filenames with device name'),
|
|
272
|
+
order: z.boolean().optional().describe('Apply screenshot ordering'),
|
|
273
|
+
clean: z.boolean().optional().describe('Clean output directory first'),
|
|
274
|
+
generateConfig: z.boolean().optional().describe('Generate Fastlane config'),
|
|
275
|
+
dryRun: z.boolean().optional().describe('Show what would be exported'),
|
|
276
|
+
verbose: z.boolean().optional().describe('Show detailed output'),
|
|
277
|
+
json: z.boolean().optional().describe('Output as JSON'),
|
|
278
|
+
configPath: z.string().optional().describe('Path to appshot.json config file')
|
|
279
|
+
});
|
|
280
|
+
server.registerTool('appshot.export', {
|
|
281
|
+
title: 'Export screenshots',
|
|
282
|
+
description: 'Runs appshot export fastlane with optional filters',
|
|
283
|
+
inputSchema
|
|
284
|
+
}, async (args) => {
|
|
285
|
+
const exportArgs = createExportArgs(args);
|
|
286
|
+
const result = await runAppshotCli(exportArgs);
|
|
287
|
+
return cliResultToToolResponse('Export', result);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function registerInitTool(server) {
|
|
291
|
+
const inputSchema = z.object({
|
|
292
|
+
force: z.boolean().optional().describe('Overwrite existing configuration files'),
|
|
293
|
+
projectDir: z.string().optional().describe('Directory to initialize the project in')
|
|
294
|
+
});
|
|
295
|
+
server.registerTool('appshot.init', {
|
|
296
|
+
title: 'Initialize project',
|
|
297
|
+
description: 'Scaffold a new appshot project with default configuration',
|
|
298
|
+
inputSchema
|
|
299
|
+
}, async (args) => {
|
|
300
|
+
const typedArgs = args;
|
|
301
|
+
const cwd = typedArgs.projectDir;
|
|
302
|
+
delete typedArgs.projectDir;
|
|
303
|
+
const initArgs = createInitArgs(typedArgs);
|
|
304
|
+
const result = await runAppshotCli(initArgs, { cwd });
|
|
305
|
+
return cliResultToToolResponse('Init', result);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function registerSpecsTool(server) {
|
|
309
|
+
const inputSchema = z.object({
|
|
310
|
+
device: z.string().optional().describe('Filter by device type: iphone, ipad, mac, watch, appletv, visionpro'),
|
|
311
|
+
required: z.boolean().optional().describe('Show only required App Store presets')
|
|
312
|
+
});
|
|
313
|
+
server.registerTool('appshot.specs', {
|
|
314
|
+
title: 'App Store specifications',
|
|
315
|
+
description: 'Get Apple App Store screenshot requirements and specifications (returns JSON)',
|
|
316
|
+
inputSchema
|
|
317
|
+
}, async (args) => {
|
|
318
|
+
const specsArgs = createSpecsArgs(args);
|
|
319
|
+
const result = await runAppshotCli(specsArgs);
|
|
320
|
+
return cliResultToToolResponse('Specs', result);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function registerValidateTool(server) {
|
|
324
|
+
const inputSchema = z.object({
|
|
325
|
+
strict: z.boolean().optional().describe('Validate against required presets only'),
|
|
326
|
+
fix: z.boolean().optional().describe('Suggest fixes for invalid screenshots')
|
|
327
|
+
});
|
|
328
|
+
server.registerTool('appshot.validate', {
|
|
329
|
+
title: 'Validate screenshots',
|
|
330
|
+
description: 'Validate screenshots against App Store requirements (returns JSON)',
|
|
331
|
+
inputSchema
|
|
332
|
+
}, async (args) => {
|
|
333
|
+
const validateArgs = createValidateArgs(args);
|
|
334
|
+
const result = await runAppshotCli(validateArgs);
|
|
335
|
+
return cliResultToToolResponse('Validate', result);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function registerCleanTool(server) {
|
|
339
|
+
const inputSchema = z.object({
|
|
340
|
+
outputDir: z.string().optional().describe('Directory to clean (default: "final")'),
|
|
341
|
+
all: z.boolean().optional().describe('Clean all generated files including processed cache'),
|
|
342
|
+
history: z.boolean().optional().describe('Clear caption autocomplete history'),
|
|
343
|
+
keepHistory: z.boolean().optional().describe('Keep history when using --all'),
|
|
344
|
+
configPath: z.string().optional().describe('Path to appshot config file or project directory')
|
|
345
|
+
});
|
|
346
|
+
server.registerTool('appshot.clean', {
|
|
347
|
+
title: 'Clean generated files',
|
|
348
|
+
description: 'Remove generated screenshots and optionally clear caches (auto-confirms)',
|
|
349
|
+
inputSchema
|
|
350
|
+
}, async (args) => {
|
|
351
|
+
const typedArgs = args;
|
|
352
|
+
let cwd;
|
|
353
|
+
if (typedArgs.configPath) {
|
|
354
|
+
const isDir = !typedArgs.configPath.endsWith('.json');
|
|
355
|
+
cwd = isDir ? typedArgs.configPath : path.dirname(path.dirname(typedArgs.configPath));
|
|
356
|
+
delete typedArgs.configPath;
|
|
357
|
+
}
|
|
358
|
+
const cleanArgs = createCleanArgs(typedArgs);
|
|
359
|
+
const result = await runAppshotCli(cleanArgs, { cwd });
|
|
360
|
+
return cliResultToToolResponse('Clean', result);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function registerLocalizeTool(server) {
|
|
364
|
+
const inputSchema = z.object({
|
|
365
|
+
languages: z.array(z.string()).describe('Language codes to translate to (e.g., ["es", "fr", "de"])'),
|
|
366
|
+
device: z.string().optional().describe('Specific device to localize, or "all" for all devices'),
|
|
367
|
+
model: z.string().optional().describe('OpenAI model to use (default: gpt-4o-mini)'),
|
|
368
|
+
sourceLanguage: z.string().optional().describe('Source language code (default: en)'),
|
|
369
|
+
overwrite: z.boolean().optional().describe('Overwrite existing translations')
|
|
370
|
+
});
|
|
371
|
+
server.registerTool('appshot.localize', {
|
|
372
|
+
title: 'Batch translate captions',
|
|
373
|
+
description: 'Translate captions to multiple languages using AI. Requires OPENAI_API_KEY environment variable.',
|
|
374
|
+
inputSchema
|
|
375
|
+
}, async (args) => {
|
|
376
|
+
const localizeArgs = createLocalizeArgs(args);
|
|
377
|
+
const result = await runAppshotCli(localizeArgs);
|
|
378
|
+
return cliResultToToolResponse('Localize', result);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
function registerPresetsTool(server) {
|
|
382
|
+
const inputSchema = z.object({
|
|
383
|
+
list: z.boolean().optional().describe('List all available presets'),
|
|
384
|
+
required: z.boolean().optional().describe('List only required App Store presets'),
|
|
385
|
+
category: z.string().optional().describe('Filter by category: iphone, ipad, mac, appletv, visionpro, watch'),
|
|
386
|
+
generate: z.array(z.string()).optional().describe('Generate config for specific preset IDs'),
|
|
387
|
+
outputFile: z.string().optional().describe('Output file for generated config')
|
|
388
|
+
});
|
|
389
|
+
server.registerTool('appshot.presets', {
|
|
390
|
+
title: 'List presets',
|
|
391
|
+
description: 'List available App Store presets and generate configuration (returns JSON)',
|
|
392
|
+
inputSchema
|
|
393
|
+
}, async (args) => {
|
|
394
|
+
const presetsArgs = createPresetsArgs(args);
|
|
395
|
+
const result = await runAppshotCli(presetsArgs);
|
|
396
|
+
return cliResultToToolResponse('Presets', result);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function registerLanguagesTool(server) {
|
|
400
|
+
const inputSchema = z.object({
|
|
401
|
+
device: z.string().optional().describe('Specific device to check (iphone/ipad/mac/watch), or omit for all devices'),
|
|
402
|
+
configPath: z.string().optional().describe('Path to appshot config file or project directory')
|
|
403
|
+
});
|
|
404
|
+
server.registerTool('appshot.languages', {
|
|
405
|
+
title: 'Discover available languages',
|
|
406
|
+
description: 'Scans caption files to discover which languages have translations available',
|
|
407
|
+
inputSchema
|
|
408
|
+
}, async (args) => {
|
|
409
|
+
let projectDir = process.cwd();
|
|
410
|
+
if (args.configPath) {
|
|
411
|
+
projectDir = args.configPath.endsWith('.json')
|
|
412
|
+
? path.dirname(path.dirname(args.configPath))
|
|
413
|
+
: args.configPath;
|
|
414
|
+
}
|
|
415
|
+
const captionsDir = path.join(projectDir, '.appshot', 'captions');
|
|
416
|
+
const byDevice = {};
|
|
417
|
+
const allLanguages = new Set();
|
|
418
|
+
let captionCount = 0;
|
|
419
|
+
const devices = args.device ? [args.device] : ['iphone', 'ipad', 'mac', 'watch'];
|
|
420
|
+
for (const device of devices) {
|
|
421
|
+
const captionPath = path.join(captionsDir, `${device}.json`);
|
|
422
|
+
try {
|
|
423
|
+
const data = await fs.readFile(captionPath, 'utf8');
|
|
424
|
+
const captions = JSON.parse(data);
|
|
425
|
+
const langs = detectLanguagesFromCaptions(captions);
|
|
426
|
+
if (langs.length > 0) {
|
|
427
|
+
byDevice[device] = langs;
|
|
428
|
+
langs.forEach(l => allLanguages.add(l));
|
|
429
|
+
}
|
|
430
|
+
captionCount += Object.keys(captions).length;
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// File doesn't exist or is invalid - skip
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const languages = Array.from(allLanguages).sort();
|
|
437
|
+
return {
|
|
438
|
+
content: [{
|
|
439
|
+
type: 'text',
|
|
440
|
+
text: `Found ${languages.length} language(s) across ${Object.keys(byDevice).length} device(s)`
|
|
441
|
+
}],
|
|
442
|
+
structuredContent: {
|
|
443
|
+
languages,
|
|
444
|
+
byDevice,
|
|
445
|
+
captionCount
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function registerConfigTool(server) {
|
|
451
|
+
const inputSchema = z.object({
|
|
452
|
+
configPath: z.string().optional().describe('Path to appshot config file or project directory'),
|
|
453
|
+
device: z.string().describe('Device to configure (iphone/ipad/mac/watch)'),
|
|
454
|
+
frameScale: z.number().optional().describe('Scale of device frame (0.1-1.5)'),
|
|
455
|
+
framePosition: z.number().optional().describe('Vertical position of device (0-100, or negative for offset)'),
|
|
456
|
+
captionPosition: z.enum(['above', 'below', 'overlay']).optional().describe('Caption position relative to device'),
|
|
457
|
+
captionSize: z.number().optional().describe('Font size for captions'),
|
|
458
|
+
marginTop: z.number().optional().describe('Top margin for caption box'),
|
|
459
|
+
marginBottom: z.number().optional().describe('Bottom margin for caption box')
|
|
460
|
+
});
|
|
461
|
+
server.registerTool('appshot.config', {
|
|
462
|
+
title: 'Update device configuration',
|
|
463
|
+
description: 'Modifies device-specific settings in the appshot config file',
|
|
464
|
+
inputSchema
|
|
465
|
+
}, async (args) => {
|
|
466
|
+
let configFile;
|
|
467
|
+
if (args.configPath) {
|
|
468
|
+
configFile = args.configPath.endsWith('.json')
|
|
469
|
+
? args.configPath
|
|
470
|
+
: path.join(args.configPath, '.appshot', 'config.json');
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
configFile = path.join(process.cwd(), '.appshot', 'config.json');
|
|
474
|
+
}
|
|
475
|
+
let config;
|
|
476
|
+
try {
|
|
477
|
+
const data = await fs.readFile(configFile, 'utf8');
|
|
478
|
+
config = JSON.parse(data);
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
return {
|
|
482
|
+
content: [{
|
|
483
|
+
type: 'text',
|
|
484
|
+
text: `Error reading config: ${err instanceof Error ? err.message : String(err)}`
|
|
485
|
+
}],
|
|
486
|
+
isError: true
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const devices = config.devices;
|
|
490
|
+
if (!devices || !devices[args.device]) {
|
|
491
|
+
return {
|
|
492
|
+
content: [{
|
|
493
|
+
type: 'text',
|
|
494
|
+
text: `Device "${args.device}" not found in config. Available: ${devices ? Object.keys(devices).join(', ') : 'none'}`
|
|
495
|
+
}],
|
|
496
|
+
isError: true
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const deviceConfig = devices[args.device];
|
|
500
|
+
const changes = [];
|
|
501
|
+
if (args.frameScale !== undefined) {
|
|
502
|
+
deviceConfig.frameScale = args.frameScale;
|
|
503
|
+
changes.push(`frameScale: ${args.frameScale}`);
|
|
504
|
+
}
|
|
505
|
+
if (args.framePosition !== undefined) {
|
|
506
|
+
deviceConfig.framePosition = args.framePosition;
|
|
507
|
+
changes.push(`framePosition: ${args.framePosition}`);
|
|
508
|
+
}
|
|
509
|
+
if (args.captionPosition !== undefined) {
|
|
510
|
+
deviceConfig.captionPosition = args.captionPosition;
|
|
511
|
+
changes.push(`captionPosition: ${args.captionPosition}`);
|
|
512
|
+
}
|
|
513
|
+
if (args.captionSize !== undefined) {
|
|
514
|
+
deviceConfig.captionSize = args.captionSize;
|
|
515
|
+
changes.push(`captionSize: ${args.captionSize}`);
|
|
516
|
+
}
|
|
517
|
+
if (args.marginTop !== undefined || args.marginBottom !== undefined) {
|
|
518
|
+
const captionBox = deviceConfig.captionBox ?? {};
|
|
519
|
+
if (args.marginTop !== undefined) {
|
|
520
|
+
captionBox.marginTop = args.marginTop;
|
|
521
|
+
changes.push(`captionBox.marginTop: ${args.marginTop}`);
|
|
522
|
+
}
|
|
523
|
+
if (args.marginBottom !== undefined) {
|
|
524
|
+
captionBox.marginBottom = args.marginBottom;
|
|
525
|
+
changes.push(`captionBox.marginBottom: ${args.marginBottom}`);
|
|
526
|
+
}
|
|
527
|
+
deviceConfig.captionBox = captionBox;
|
|
528
|
+
}
|
|
529
|
+
if (changes.length === 0) {
|
|
530
|
+
return {
|
|
531
|
+
content: [{
|
|
532
|
+
type: 'text',
|
|
533
|
+
text: 'No changes specified'
|
|
534
|
+
}]
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
await fs.writeFile(configFile, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
return {
|
|
542
|
+
content: [{
|
|
543
|
+
type: 'text',
|
|
544
|
+
text: `Error writing config: ${err instanceof Error ? err.message : String(err)}`
|
|
545
|
+
}],
|
|
546
|
+
isError: true
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
content: [{
|
|
551
|
+
type: 'text',
|
|
552
|
+
text: `Updated ${args.device} config:\n${changes.map(c => ` • ${c}`).join('\n')}`
|
|
553
|
+
}],
|
|
554
|
+
structuredContent: {
|
|
555
|
+
device: args.device,
|
|
556
|
+
changes,
|
|
557
|
+
configFile
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function registerCaptionsTool(server) {
|
|
563
|
+
const inputSchema = z.object({
|
|
564
|
+
configPath: z.string().optional().describe('Path to appshot config file or project directory'),
|
|
565
|
+
device: z.string().describe('Device to manage captions for (iphone/ipad/mac/watch)'),
|
|
566
|
+
action: z.enum(['list', 'get', 'set', 'bulk-set', 'auto']).describe('Action to perform'),
|
|
567
|
+
filename: z.string().optional().describe('Screenshot filename (required for get/set)'),
|
|
568
|
+
language: z.string().optional().describe('Language code (default: en)'),
|
|
569
|
+
caption: z.string().optional().describe('Caption text (required for set action)'),
|
|
570
|
+
captions: z.string().optional().describe('JSON object of filename:caption pairs for bulk-set (e.g., {"file1.png": "Caption 1", "file2.png": "Caption 2"})'),
|
|
571
|
+
overwrite: z.boolean().optional().describe('Overwrite existing captions (for auto action, default: false)')
|
|
572
|
+
});
|
|
573
|
+
server.registerTool('appshot.captions', {
|
|
574
|
+
title: 'Manage captions',
|
|
575
|
+
description: 'Read and write caption text for screenshots',
|
|
576
|
+
inputSchema
|
|
577
|
+
}, async (args) => {
|
|
578
|
+
let projectDir = process.cwd();
|
|
579
|
+
if (args.configPath) {
|
|
580
|
+
projectDir = args.configPath.endsWith('.json')
|
|
581
|
+
? path.dirname(path.dirname(args.configPath))
|
|
582
|
+
: args.configPath;
|
|
583
|
+
}
|
|
584
|
+
const captionFile = path.join(projectDir, '.appshot', 'captions', `${args.device}.json`);
|
|
585
|
+
let captions = {};
|
|
586
|
+
try {
|
|
587
|
+
const data = await fs.readFile(captionFile, 'utf8');
|
|
588
|
+
captions = JSON.parse(data);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// Write actions (set, bulk-set, auto) can proceed with empty captions
|
|
592
|
+
// Read actions (list, get) should report no file found
|
|
593
|
+
const writeActions = ['set', 'bulk-set', 'auto'];
|
|
594
|
+
if (!writeActions.includes(args.action)) {
|
|
595
|
+
return {
|
|
596
|
+
content: [{
|
|
597
|
+
type: 'text',
|
|
598
|
+
text: `No captions file found for ${args.device}`
|
|
599
|
+
}],
|
|
600
|
+
structuredContent: { device: args.device, captions: {} }
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (args.action === 'list') {
|
|
605
|
+
const captionCount = Object.keys(captions).length;
|
|
606
|
+
return {
|
|
607
|
+
content: [{
|
|
608
|
+
type: 'text',
|
|
609
|
+
text: `Found ${captionCount} caption(s) for ${args.device}`
|
|
610
|
+
}],
|
|
611
|
+
structuredContent: {
|
|
612
|
+
device: args.device,
|
|
613
|
+
captions
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
if (args.action === 'get') {
|
|
618
|
+
if (!args.filename) {
|
|
619
|
+
return {
|
|
620
|
+
content: [{
|
|
621
|
+
type: 'text',
|
|
622
|
+
text: 'filename is required for get action'
|
|
623
|
+
}],
|
|
624
|
+
isError: true
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const captionData = captions[args.filename];
|
|
628
|
+
return {
|
|
629
|
+
content: [{
|
|
630
|
+
type: 'text',
|
|
631
|
+
text: captionData ? `Caption for ${args.filename}` : `No caption found for ${args.filename}`
|
|
632
|
+
}],
|
|
633
|
+
structuredContent: {
|
|
634
|
+
filename: args.filename,
|
|
635
|
+
captions: typeof captionData === 'string' ? { en: captionData } : (captionData ?? {})
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
if (args.action === 'set') {
|
|
640
|
+
if (!args.filename) {
|
|
641
|
+
return {
|
|
642
|
+
content: [{
|
|
643
|
+
type: 'text',
|
|
644
|
+
text: 'filename is required for set action'
|
|
645
|
+
}],
|
|
646
|
+
isError: true
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (args.caption === undefined) {
|
|
650
|
+
return {
|
|
651
|
+
content: [{
|
|
652
|
+
type: 'text',
|
|
653
|
+
text: 'caption is required for set action'
|
|
654
|
+
}],
|
|
655
|
+
isError: true
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const lang = args.language ?? 'en';
|
|
659
|
+
const existing = captions[args.filename];
|
|
660
|
+
if (typeof existing === 'string') {
|
|
661
|
+
captions[args.filename] = { en: existing, [lang]: args.caption };
|
|
662
|
+
}
|
|
663
|
+
else if (existing) {
|
|
664
|
+
existing[lang] = args.caption;
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
captions[args.filename] = { [lang]: args.caption };
|
|
668
|
+
}
|
|
669
|
+
const captionsDir = path.dirname(captionFile);
|
|
670
|
+
await fs.mkdir(captionsDir, { recursive: true });
|
|
671
|
+
await fs.writeFile(captionFile, JSON.stringify(captions, null, 2) + '\n', 'utf8');
|
|
672
|
+
return {
|
|
673
|
+
content: [{
|
|
674
|
+
type: 'text',
|
|
675
|
+
text: `Updated caption for ${args.filename} (${lang})`
|
|
676
|
+
}],
|
|
677
|
+
structuredContent: {
|
|
678
|
+
filename: args.filename,
|
|
679
|
+
language: lang,
|
|
680
|
+
caption: args.caption,
|
|
681
|
+
updated: true
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (args.action === 'bulk-set') {
|
|
686
|
+
if (!args.captions) {
|
|
687
|
+
return {
|
|
688
|
+
content: [{
|
|
689
|
+
type: 'text',
|
|
690
|
+
text: 'captions JSON is required for bulk-set action'
|
|
691
|
+
}],
|
|
692
|
+
isError: true
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
let captionsToSet;
|
|
696
|
+
try {
|
|
697
|
+
captionsToSet = JSON.parse(args.captions);
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
return {
|
|
701
|
+
content: [{
|
|
702
|
+
type: 'text',
|
|
703
|
+
text: 'Invalid JSON in captions parameter'
|
|
704
|
+
}],
|
|
705
|
+
isError: true
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const lang = args.language ?? 'en';
|
|
709
|
+
let count = 0;
|
|
710
|
+
for (const [filename, captionText] of Object.entries(captionsToSet)) {
|
|
711
|
+
const existing = captions[filename];
|
|
712
|
+
if (typeof existing === 'string') {
|
|
713
|
+
captions[filename] = { en: existing, [lang]: captionText };
|
|
714
|
+
}
|
|
715
|
+
else if (existing) {
|
|
716
|
+
existing[lang] = captionText;
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
captions[filename] = { [lang]: captionText };
|
|
720
|
+
}
|
|
721
|
+
count++;
|
|
722
|
+
}
|
|
723
|
+
const captionsDir = path.dirname(captionFile);
|
|
724
|
+
await fs.mkdir(captionsDir, { recursive: true });
|
|
725
|
+
await fs.writeFile(captionFile, JSON.stringify(captions, null, 2) + '\n', 'utf8');
|
|
726
|
+
return {
|
|
727
|
+
content: [{
|
|
728
|
+
type: 'text',
|
|
729
|
+
text: `Set ${count} caption(s) for ${args.device} (${lang})`
|
|
730
|
+
}],
|
|
731
|
+
structuredContent: {
|
|
732
|
+
device: args.device,
|
|
733
|
+
language: lang,
|
|
734
|
+
count,
|
|
735
|
+
captions: captionsToSet,
|
|
736
|
+
updated: true
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (args.action === 'auto') {
|
|
741
|
+
// Use provided config path or default to .appshot/config.json
|
|
742
|
+
const configFile = args.configPath?.endsWith('.json')
|
|
743
|
+
? args.configPath
|
|
744
|
+
: path.join(projectDir, '.appshot', 'config.json');
|
|
745
|
+
let config;
|
|
746
|
+
try {
|
|
747
|
+
const data = await fs.readFile(configFile, 'utf8');
|
|
748
|
+
config = JSON.parse(data);
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
return {
|
|
752
|
+
content: [{
|
|
753
|
+
type: 'text',
|
|
754
|
+
text: 'Could not read config file. Run appshot init first.'
|
|
755
|
+
}],
|
|
756
|
+
isError: true
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const devices = config.devices;
|
|
760
|
+
const deviceConfig = devices?.[args.device];
|
|
761
|
+
const inputDir = deviceConfig?.input ?? `./screenshots/${args.device}`;
|
|
762
|
+
const screenshotsDir = path.resolve(projectDir, inputDir);
|
|
763
|
+
let files;
|
|
764
|
+
try {
|
|
765
|
+
const entries = await fs.readdir(screenshotsDir);
|
|
766
|
+
files = entries.filter(f => /\.(png|jpg|jpeg)$/i.test(f));
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return {
|
|
770
|
+
content: [{
|
|
771
|
+
type: 'text',
|
|
772
|
+
text: `Could not read screenshots directory: ${screenshotsDir}`
|
|
773
|
+
}],
|
|
774
|
+
isError: true
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (files.length === 0) {
|
|
778
|
+
return {
|
|
779
|
+
content: [{
|
|
780
|
+
type: 'text',
|
|
781
|
+
text: `No screenshots found in ${screenshotsDir}`
|
|
782
|
+
}],
|
|
783
|
+
structuredContent: { device: args.device, count: 0, captions: {} }
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
const lang = args.language ?? 'en';
|
|
787
|
+
const overwrite = args.overwrite ?? false;
|
|
788
|
+
let count = 0;
|
|
789
|
+
const generated = {};
|
|
790
|
+
for (const filename of files) {
|
|
791
|
+
const existing = captions[filename];
|
|
792
|
+
const hasCaption = existing && (typeof existing === 'string' ||
|
|
793
|
+
(typeof existing === 'object' && existing[lang]));
|
|
794
|
+
if (hasCaption && !overwrite) {
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
const captionText = filenameToCaption(filename);
|
|
798
|
+
if (typeof existing === 'string') {
|
|
799
|
+
captions[filename] = { en: existing, [lang]: captionText };
|
|
800
|
+
}
|
|
801
|
+
else if (existing) {
|
|
802
|
+
existing[lang] = captionText;
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
captions[filename] = { [lang]: captionText };
|
|
806
|
+
}
|
|
807
|
+
generated[filename] = captionText;
|
|
808
|
+
count++;
|
|
809
|
+
}
|
|
810
|
+
if (count > 0) {
|
|
811
|
+
const captionsDir = path.dirname(captionFile);
|
|
812
|
+
await fs.mkdir(captionsDir, { recursive: true });
|
|
813
|
+
await fs.writeFile(captionFile, JSON.stringify(captions, null, 2) + '\n', 'utf8');
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
content: [{
|
|
817
|
+
type: 'text',
|
|
818
|
+
text: count > 0
|
|
819
|
+
? `Generated ${count} caption(s) from filenames for ${args.device}`
|
|
820
|
+
: `No new captions generated (${files.length} files already have captions)`
|
|
821
|
+
}],
|
|
822
|
+
structuredContent: {
|
|
823
|
+
device: args.device,
|
|
824
|
+
language: lang,
|
|
825
|
+
count,
|
|
826
|
+
totalFiles: files.length,
|
|
827
|
+
captions: generated,
|
|
828
|
+
updated: count > 0
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
content: [{
|
|
834
|
+
type: 'text',
|
|
835
|
+
text: `Unknown action: ${args.action}`
|
|
836
|
+
}],
|
|
837
|
+
isError: true
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
function registerGradientsTool(server) {
|
|
842
|
+
const inputSchema = z.object({
|
|
843
|
+
configPath: z.string().optional().describe('Path to appshot config file or project directory'),
|
|
844
|
+
action: z.enum(['list', 'apply']).describe('Action to perform'),
|
|
845
|
+
category: z.enum(['warm', 'cool', 'vibrant', 'subtle', 'monochrome', 'brand']).optional().describe('Filter by category (for list action)'),
|
|
846
|
+
preset: z.string().optional().describe('Preset ID to apply (required for apply action)')
|
|
847
|
+
});
|
|
848
|
+
server.registerTool('appshot.gradients', {
|
|
849
|
+
title: 'Manage gradients',
|
|
850
|
+
description: 'List available gradient presets and apply them to config',
|
|
851
|
+
inputSchema
|
|
852
|
+
}, async (args) => {
|
|
853
|
+
if (args.action === 'list') {
|
|
854
|
+
const presets = args.category
|
|
855
|
+
? getGradientsByCategory(args.category)
|
|
856
|
+
: gradientPresets;
|
|
857
|
+
return {
|
|
858
|
+
content: [{
|
|
859
|
+
type: 'text',
|
|
860
|
+
text: `Found ${presets.length} gradient preset(s)${args.category ? ` in category "${args.category}"` : ''}`
|
|
861
|
+
}],
|
|
862
|
+
structuredContent: {
|
|
863
|
+
presets: presets.map(p => ({
|
|
864
|
+
id: p.id,
|
|
865
|
+
name: p.name,
|
|
866
|
+
colors: p.colors,
|
|
867
|
+
direction: p.direction,
|
|
868
|
+
category: p.category
|
|
869
|
+
}))
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
if (args.action === 'apply') {
|
|
874
|
+
if (!args.preset) {
|
|
875
|
+
return {
|
|
876
|
+
content: [{
|
|
877
|
+
type: 'text',
|
|
878
|
+
text: 'preset is required for apply action'
|
|
879
|
+
}],
|
|
880
|
+
isError: true
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const preset = getGradientPreset(args.preset);
|
|
884
|
+
if (!preset) {
|
|
885
|
+
return {
|
|
886
|
+
content: [{
|
|
887
|
+
type: 'text',
|
|
888
|
+
text: `Gradient preset "${args.preset}" not found. Use action: list to see available presets.`
|
|
889
|
+
}],
|
|
890
|
+
isError: true
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
let configFile;
|
|
894
|
+
if (args.configPath) {
|
|
895
|
+
configFile = args.configPath.endsWith('.json')
|
|
896
|
+
? args.configPath
|
|
897
|
+
: path.join(args.configPath, '.appshot', 'config.json');
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
configFile = path.join(process.cwd(), '.appshot', 'config.json');
|
|
901
|
+
}
|
|
902
|
+
let config;
|
|
903
|
+
try {
|
|
904
|
+
const data = await fs.readFile(configFile, 'utf8');
|
|
905
|
+
config = JSON.parse(data);
|
|
906
|
+
}
|
|
907
|
+
catch (err) {
|
|
908
|
+
return {
|
|
909
|
+
content: [{
|
|
910
|
+
type: 'text',
|
|
911
|
+
text: `Error reading config: ${err instanceof Error ? err.message : String(err)}`
|
|
912
|
+
}],
|
|
913
|
+
isError: true
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
config.gradient = {
|
|
917
|
+
colors: preset.colors,
|
|
918
|
+
direction: preset.direction
|
|
919
|
+
};
|
|
920
|
+
await fs.writeFile(configFile, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
921
|
+
return {
|
|
922
|
+
content: [{
|
|
923
|
+
type: 'text',
|
|
924
|
+
text: `Applied gradient preset "${preset.name}"`
|
|
925
|
+
}],
|
|
926
|
+
structuredContent: {
|
|
927
|
+
preset: preset.id,
|
|
928
|
+
colors: preset.colors,
|
|
929
|
+
direction: preset.direction,
|
|
930
|
+
applied: true
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
content: [{
|
|
936
|
+
type: 'text',
|
|
937
|
+
text: `Unknown action: ${args.action}`
|
|
938
|
+
}],
|
|
939
|
+
isError: true
|
|
940
|
+
};
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
function registerBackgroundsTool(server) {
|
|
944
|
+
const inputSchema = z.object({
|
|
945
|
+
configPath: z.string().optional().describe('Path to appshot config file or project directory'),
|
|
946
|
+
action: z.enum(['list', 'set', 'clear']).describe('Action to perform'),
|
|
947
|
+
device: z.string().optional().describe('Device to configure (omit for global background)'),
|
|
948
|
+
image: z.string().optional().describe('Path to background image (for set action)'),
|
|
949
|
+
fit: z.enum(['cover', 'contain', 'fill', 'scale-down']).optional().describe('Background fit mode (for set action)')
|
|
950
|
+
});
|
|
951
|
+
server.registerTool('appshot.backgrounds', {
|
|
952
|
+
title: 'Manage backgrounds',
|
|
953
|
+
description: 'Configure background images for screenshots',
|
|
954
|
+
inputSchema
|
|
955
|
+
}, async (args) => {
|
|
956
|
+
let configFile;
|
|
957
|
+
if (args.configPath) {
|
|
958
|
+
configFile = args.configPath.endsWith('.json')
|
|
959
|
+
? args.configPath
|
|
960
|
+
: path.join(args.configPath, '.appshot', 'config.json');
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
configFile = path.join(process.cwd(), '.appshot', 'config.json');
|
|
964
|
+
}
|
|
965
|
+
let config;
|
|
966
|
+
try {
|
|
967
|
+
const data = await fs.readFile(configFile, 'utf8');
|
|
968
|
+
config = JSON.parse(data);
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
return {
|
|
972
|
+
content: [{
|
|
973
|
+
type: 'text',
|
|
974
|
+
text: `Error reading config: ${err instanceof Error ? err.message : String(err)}`
|
|
975
|
+
}],
|
|
976
|
+
isError: true
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
if (args.action === 'list') {
|
|
980
|
+
const globalBg = config.background;
|
|
981
|
+
const devices = config.devices;
|
|
982
|
+
const deviceBackgrounds = {};
|
|
983
|
+
if (devices) {
|
|
984
|
+
for (const [device, deviceConfig] of Object.entries(devices)) {
|
|
985
|
+
if (deviceConfig.background) {
|
|
986
|
+
deviceBackgrounds[device] = deviceConfig.background;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
content: [{
|
|
992
|
+
type: 'text',
|
|
993
|
+
text: 'Background configuration loaded'
|
|
994
|
+
}],
|
|
995
|
+
structuredContent: {
|
|
996
|
+
global: globalBg ?? {},
|
|
997
|
+
devices: deviceBackgrounds
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (args.action === 'set') {
|
|
1002
|
+
if (!args.image) {
|
|
1003
|
+
return {
|
|
1004
|
+
content: [{
|
|
1005
|
+
type: 'text',
|
|
1006
|
+
text: 'image is required for set action'
|
|
1007
|
+
}],
|
|
1008
|
+
isError: true
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
if (args.device) {
|
|
1012
|
+
const devices = config.devices ?? {};
|
|
1013
|
+
if (!devices[args.device]) {
|
|
1014
|
+
return {
|
|
1015
|
+
content: [{
|
|
1016
|
+
type: 'text',
|
|
1017
|
+
text: `Device "${args.device}" not found in config`
|
|
1018
|
+
}],
|
|
1019
|
+
isError: true
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
devices[args.device].background = {
|
|
1023
|
+
image: args.image,
|
|
1024
|
+
...(args.fit && { fit: args.fit })
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
config.background = {
|
|
1029
|
+
mode: 'image',
|
|
1030
|
+
image: args.image,
|
|
1031
|
+
...(args.fit && { fit: args.fit })
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
await fs.writeFile(configFile, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
1035
|
+
return {
|
|
1036
|
+
content: [{
|
|
1037
|
+
type: 'text',
|
|
1038
|
+
text: `Set background${args.device ? ` for ${args.device}` : ' (global)'}: ${args.image}`
|
|
1039
|
+
}],
|
|
1040
|
+
structuredContent: {
|
|
1041
|
+
device: args.device ?? 'global',
|
|
1042
|
+
image: args.image,
|
|
1043
|
+
fit: args.fit,
|
|
1044
|
+
updated: true
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
if (args.action === 'clear') {
|
|
1049
|
+
if (args.device) {
|
|
1050
|
+
const devices = config.devices ?? {};
|
|
1051
|
+
if (devices[args.device]) {
|
|
1052
|
+
delete devices[args.device].background;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
delete config.background;
|
|
1057
|
+
}
|
|
1058
|
+
await fs.writeFile(configFile, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
1059
|
+
return {
|
|
1060
|
+
content: [{
|
|
1061
|
+
type: 'text',
|
|
1062
|
+
text: `Cleared background${args.device ? ` for ${args.device}` : ' (global)'}`
|
|
1063
|
+
}],
|
|
1064
|
+
structuredContent: {
|
|
1065
|
+
device: args.device ?? 'global',
|
|
1066
|
+
cleared: true
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
content: [{
|
|
1072
|
+
type: 'text',
|
|
1073
|
+
text: `Unknown action: ${args.action}`
|
|
1074
|
+
}],
|
|
1075
|
+
isError: true
|
|
1076
|
+
};
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
function registerFontsTool(server) {
|
|
1080
|
+
const inputSchema = z.object({
|
|
1081
|
+
action: z.enum(['list', 'validate', 'embedded']).describe('Action to perform'),
|
|
1082
|
+
font: z.string().optional().describe('Font name to validate (required for validate action)')
|
|
1083
|
+
});
|
|
1084
|
+
server.registerTool('appshot.fonts', {
|
|
1085
|
+
title: 'Manage fonts',
|
|
1086
|
+
description: 'List available fonts and check font availability',
|
|
1087
|
+
inputSchema
|
|
1088
|
+
}, async (args) => {
|
|
1089
|
+
if (args.action === 'validate' && !args.font) {
|
|
1090
|
+
return {
|
|
1091
|
+
content: [{
|
|
1092
|
+
type: 'text',
|
|
1093
|
+
text: 'font is required for validate action'
|
|
1094
|
+
}],
|
|
1095
|
+
isError: true
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
const fontsArgs = createFontsArgs(args);
|
|
1099
|
+
const result = await runAppshotCli(fontsArgs);
|
|
1100
|
+
return cliResultToToolResponse('Fonts', result);
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
function registerTemplateTool(server) {
|
|
1104
|
+
const inputSchema = z.object({
|
|
1105
|
+
template: z.string().optional().describe('Template ID to apply (modern, minimal, bold, elegant, showcase, playful, corporate)'),
|
|
1106
|
+
list: z.boolean().optional().describe('List all available templates'),
|
|
1107
|
+
preview: z.string().optional().describe('Preview template configuration by ID'),
|
|
1108
|
+
caption: z.string().optional().describe('Add a single caption to all screenshots'),
|
|
1109
|
+
captions: z.string().optional().describe('Add multiple captions as JSON'),
|
|
1110
|
+
device: z.string().optional().describe('Apply template to specific device only'),
|
|
1111
|
+
noBackup: z.boolean().optional().describe('Skip creating backup of current config'),
|
|
1112
|
+
dryRun: z.boolean().optional().describe('Preview changes without applying'),
|
|
1113
|
+
projectDir: z.string().optional().describe('Project directory to apply template to')
|
|
1114
|
+
});
|
|
1115
|
+
server.registerTool('appshot.template', {
|
|
1116
|
+
title: 'Apply template',
|
|
1117
|
+
description: 'Apply professional screenshot templates for quick App Store setup',
|
|
1118
|
+
inputSchema
|
|
1119
|
+
}, async (args) => {
|
|
1120
|
+
const typedArgs = args;
|
|
1121
|
+
const cwd = typedArgs.projectDir;
|
|
1122
|
+
delete typedArgs.projectDir;
|
|
1123
|
+
const templateArgs = createTemplateArgs(typedArgs);
|
|
1124
|
+
const result = await runAppshotCli(templateArgs, { cwd });
|
|
1125
|
+
return cliResultToToolResponse('Template', result);
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
function registerQuickstartTool(server) {
|
|
1129
|
+
const inputSchema = z.object({
|
|
1130
|
+
template: z.string().optional().describe('Template to use (default: modern)'),
|
|
1131
|
+
caption: z.string().optional().describe('Main caption for screenshots'),
|
|
1132
|
+
noInteractive: z.boolean().optional().describe('Skip interactive prompts'),
|
|
1133
|
+
force: z.boolean().optional().describe('Overwrite existing configuration'),
|
|
1134
|
+
projectDir: z.string().optional().describe('Directory to initialize the project in')
|
|
1135
|
+
});
|
|
1136
|
+
server.registerTool('appshot.quickstart', {
|
|
1137
|
+
title: 'Quickstart',
|
|
1138
|
+
description: 'Get started with App Store screenshots in seconds - initializes project, applies template, sets up captions',
|
|
1139
|
+
inputSchema
|
|
1140
|
+
}, async (args) => {
|
|
1141
|
+
const typedArgs = args;
|
|
1142
|
+
const cwd = typedArgs.projectDir;
|
|
1143
|
+
delete typedArgs.projectDir;
|
|
1144
|
+
const quickstartArgs = createQuickstartArgs(typedArgs);
|
|
1145
|
+
const result = await runAppshotCli(quickstartArgs, { cwd });
|
|
1146
|
+
return cliResultToToolResponse('Quickstart', result);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
//# sourceMappingURL=server.js.map
|