Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/sdk/generated/domain-map.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
],
"sideEffects": [
{
"method": "uploadImage",
"method": "upload",
"reason": "private_rest",
"specPath": "src/spec/upload.ts"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@google/stitch-sdk",
"version": "0.2.0",
"version": "0.3.0",
"type": "module",
"private": false,
"description": "Generate UI screens from text prompts and extract their HTML and screenshots programmatically.",
Expand Down
6 changes: 3 additions & 3 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ export type {

// Upload types
export type {
UploadImageInput,
UploadImageResult,
UploadImageErrorCode,
UploadInput,
UploadResult,
UploadErrorCode,
} from "./spec/upload.js";

// Download types
Expand Down
33 changes: 12 additions & 21 deletions packages/sdk/src/project-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,26 @@ import { DownloadAssetsHandler } from './download-handler.js';
import { DownloadAssetsInputSchema } from './spec/download.js';
import type { DownloadAssetsOutput } from './spec/download.js';
import {
UploadImageInputSchema,
type UploadImageInput,
UploadInputSchema,
type UploadInput,
} from './spec/upload.js';
import { UploadImageHandler } from './upload-handler.js';
import { UploadHandler } from './upload-handler.js';

export class Project extends GeneratedProject {

/**
* Upload an image file to the project and create a new Screen from it.
*
* WHY THIS IS NOT GENERATED:
* BatchCreateScreens is a private REST endpoint — it has no MCP tool
* in tools-manifest.json. It also requires reading a file from disk and
* base64-encoding it, which the codegen arg-routing model cannot express.
*
* @param filePath - Absolute or relative path to the image (PNG, JPG, JPEG, WEBP).
* @param opts - Optional screen title and createScreenInstances flag.
* @returns An array of Screen objects created from the upload.
* @throws {StitchError} on file not found, unsupported format, or upload failure.
* Upload any supported design or document file asset (PNG, JPG, WEBP, HTML) into the project.
* Creates a new screen canvas entity from the file contents.
*
* @example
* const [screen] = await project.uploadImage('./mockup.png', { title: 'Home Screen' });
* const html = await screen.getHtml();
* @param filePath - Absolute or relative path to the asset file.
* @param opts - Optional parameter overrides.
*/
async uploadImage(
async upload(
filePath: string,
opts?: Partial<Omit<UploadImageInput, 'filePath'>>,
opts?: Partial<Omit<UploadInput, 'filePath'>>,
): Promise<Screen[]> {
const input = UploadImageInputSchema.parse({ filePath, ...opts });
const handler = new UploadImageHandler(this.client);
const input = UploadInputSchema.parse({ filePath, ...opts });
const handler = new UploadHandler(this.client);
const result = await handler.execute(this.projectId, input);

if (!result.success) {
Expand Down
26 changes: 15 additions & 11 deletions packages/sdk/src/spec/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,43 +27,45 @@ export const SUPPORTED_MIME_TYPES = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.html': 'text/html',
'.htm': 'text/html',
} as const;

export type SupportedExtension = keyof typeof SUPPORTED_MIME_TYPES;

// ── Input ──────────────────────────────────────────────────────────────────────

export const UploadImageInputSchema = z.object({
/** Absolute or relative path to the image file on disk. */
export const UploadInputSchema = z.object({
/** Absolute or relative path to the asset file on disk. */
filePath: z.string().min(1),
/** Optional display title for the created screen. */
title: z.string().optional(),
/** If true (default), creates screen instances on the project canvas. */
createScreenInstances: z.boolean().default(true),
});

export type UploadImageInput = z.infer<typeof UploadImageInputSchema>;
export type UploadInput = z.infer<typeof UploadInputSchema>;

// ── Error Codes ────────────────────────────────────────────────────────────────

export const UploadImageErrorCode = z.enum([
export const UploadErrorCode = z.enum([
'FILE_NOT_FOUND',
'UNSUPPORTED_FORMAT',
'UPLOAD_FAILED',
'AUTH_FAILED',
'UNKNOWN_ERROR',
]);

export type UploadImageErrorCode = z.infer<typeof UploadImageErrorCode>;
export type UploadErrorCode = z.infer<typeof UploadErrorCode>;

// ── Result ─────────────────────────────────────────────────────────────────────

export type UploadImageResult =
export type UploadResult =
| { success: true; screens: Screen[] }
| {
success: false;
error: {
code: UploadImageErrorCode;
code: UploadErrorCode;
message: string;
recoverable: boolean;
};
Expand All @@ -72,9 +74,11 @@ export type UploadImageResult =
// ── Interface ──────────────────────────────────────────────────────────────────

/**
* Contract for the uploadImage operation.
* Implementations must never throw — all failures return UploadImageResult.
* Contract for the upload operation.
* Implementations must never throw — all failures return UploadResult.
*/
export interface UploadImageSpec {
execute(projectId: string, input: UploadImageInput): Promise<UploadImageResult>;
export interface UploadSpec {
execute(projectId: string, input: UploadInput): Promise<UploadResult>;
}


49 changes: 27 additions & 22 deletions packages/sdk/src/upload-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ import type { StitchToolClientSpec } from './spec/client.js';
import {
SUPPORTED_MIME_TYPES,
type SupportedExtension,
type UploadImageInput,
type UploadImageResult,
type UploadImageErrorCode,
type UploadImageSpec,
type UploadInput,
type UploadResult,
type UploadErrorCode,
type UploadSpec,
} from './spec/upload.js';
import { Screen } from '../generated/src/screen.js';

Expand All @@ -53,17 +53,25 @@ function buildBatchCreateScreensBody(
projectId: string,
fileContentBase64: string,
mimeType: string,
input: UploadImageInput,
input: UploadInput,
) {
const fileObj = {
fileContentBase64,
mimeType,
};

const isHtml = mimeType === 'text/html';
const screen: Record<string, unknown> = {
screenshot: {
fileContentBase64,
mimeType,
},
screenType: 'IMAGE',
screenType: isHtml ? 'DOCUMENT' : 'IMAGE',
isCreatedByClient: true,
};

if (isHtml) {
screen['htmlCode'] = fileObj;
} else {
screen['screenshot'] = fileObj;
}

if (input.title) {
screen['title'] = input.title;
}
Expand All @@ -76,16 +84,14 @@ function buildBatchCreateScreensBody(
}

/**
* Handler for uploadImage — implements UploadImageSpec.
*
* Never throws. All failures are returned as UploadImageResult with a typed
* error code. The caller (Project.uploadImage) surfaces failures as StitchError.
* Handler for the upload capability — implements UploadSpec.
* Never throws. All failures return an UploadResult value with a classified error code.
*/
export class UploadImageHandler implements UploadImageSpec {
export class UploadHandler implements UploadSpec {
constructor(private readonly client: StitchToolClientSpec) {}

async execute(projectId: string, input: UploadImageInput): Promise<UploadImageResult> {
// ── Step 1: Validate extension → typed error code ────────────────────────
async execute(projectId: string, input: UploadInput): Promise<UploadResult> {
// ── Step 1: Validate extension ───────────────────────────────────────────
const ext = path.extname(input.filePath).toLowerCase();
const mimeType = SUPPORTED_MIME_TYPES[ext as SupportedExtension];
if (!mimeType) {
Expand Down Expand Up @@ -117,12 +123,10 @@ export class UploadImageHandler implements UploadImageSpec {
body,
);

// ── Step 3: Project the response into Screen[] ───────────────────────
// BatchCreateScreens returns { results: [{ screen: { ... } }] }
// ── Step 3: Project the response into Screen[] ─────────────────────────
const results: Array<{ screen: any }> = raw?.results ?? [];
const screens: Screen[] = results.map((r) => {
const screenData = { ...r.screen, projectId };
// If API didn't return an ID but returned a file name, extract ID from it
if (!screenData.id && screenData.screenshot?.name) {
const parts = screenData.screenshot.name.split('/files/');
if (parts.length === 2) {
Expand All @@ -132,7 +136,6 @@ export class UploadImageHandler implements UploadImageSpec {
return new Screen(this.client as any, screenData);
});


return { success: true, screens };
} catch (err) {
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
Expand All @@ -146,7 +149,7 @@ export class UploadImageHandler implements UploadImageSpec {
};
}
const msg = err instanceof Error ? err.message : String(err);
const code: UploadImageErrorCode =
const code: UploadErrorCode =
msg.includes('401') || msg.includes('403') || msg.toLowerCase().includes('auth')
? 'AUTH_FAILED'
: 'UPLOAD_FAILED';
Expand All @@ -158,3 +161,5 @@ export class UploadImageHandler implements UploadImageSpec {
}
}
}


2 changes: 1 addition & 1 deletion packages/sdk/src/version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Auto-generated by scripts/inject-version.ts — do not edit.
export const SDK_VERSION = '0.2.0';
export const SDK_VERSION = '0.3.0';
4 changes: 2 additions & 2 deletions packages/sdk/test/unit/extension-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ describe('SDK Extension Resolution', () => {
const project = projects[0];

// 4. Assert that the returned object is actually the extended subclass
// By verifying the existence of the handwritten uploadImage method
// By verifying the existence of the handwritten upload method
expect(project).toBeDefined();
expect(typeof project!.uploadImage).toBe('function');
expect(typeof project!.upload).toBe('function');
});
});
1 change: 1 addition & 0 deletions packages/sdk/test/unit/sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ describe("SDK Unit Tests", () => {
expect(result).toEqual([]);
});


it("generate should throw StitchError on failure", async () => {
const project = new Project(mockClient, projectId);

Expand Down
Loading
Loading