@spectratools/graphic-designer-cli 0.3.1
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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +4922 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +4973 -0
- package/dist/publish/index.d.ts +49 -0
- package/dist/publish/index.js +259 -0
- package/dist/qa.d.ts +38 -0
- package/dist/qa.js +901 -0
- package/dist/renderer.d.ts +3 -0
- package/dist/renderer.js +3608 -0
- package/dist/spec.schema-BxXBTOn-.d.ts +4809 -0
- package/dist/spec.schema.d.ts +3 -0
- package/dist/spec.schema.js +604 -0
- package/fonts/Inter-Bold.woff2 +0 -0
- package/fonts/Inter-Medium.woff2 +0 -0
- package/fonts/Inter-Regular.woff2 +0 -0
- package/fonts/Inter-SemiBold.woff2 +0 -0
- package/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/fonts/JetBrainsMono-Medium.woff2 +0 -0
- package/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/fonts/SpaceGrotesk-Bold.woff2 +0 -0
- package/fonts/SpaceGrotesk-Medium.woff2 +0 -0
- package/package.json +78 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
interface RetryPolicy {
|
|
2
|
+
maxRetries: number;
|
|
3
|
+
baseMs: number;
|
|
4
|
+
maxMs: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type GitHubPublishOptions = {
|
|
8
|
+
imagePath: string;
|
|
9
|
+
metadataPath: string;
|
|
10
|
+
repo: string;
|
|
11
|
+
pathPrefix?: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
token?: string;
|
|
14
|
+
commitMessage?: string;
|
|
15
|
+
retryPolicy?: RetryPolicy;
|
|
16
|
+
};
|
|
17
|
+
type GitHubPublishResult = {
|
|
18
|
+
target: 'github';
|
|
19
|
+
repo: string;
|
|
20
|
+
branch: string;
|
|
21
|
+
attempts: number;
|
|
22
|
+
files: Array<{
|
|
23
|
+
path: string;
|
|
24
|
+
htmlUrl?: string;
|
|
25
|
+
sha?: string;
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
28
|
+
declare function publishToGitHub(options: GitHubPublishOptions): Promise<GitHubPublishResult>;
|
|
29
|
+
|
|
30
|
+
type GistPublishOptions = {
|
|
31
|
+
imagePath: string;
|
|
32
|
+
metadataPath: string;
|
|
33
|
+
gistId?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
filenamePrefix?: string;
|
|
36
|
+
public?: boolean;
|
|
37
|
+
token?: string;
|
|
38
|
+
retryPolicy?: RetryPolicy;
|
|
39
|
+
};
|
|
40
|
+
type GistPublishResult = {
|
|
41
|
+
target: 'gist';
|
|
42
|
+
gistId: string;
|
|
43
|
+
htmlUrl: string;
|
|
44
|
+
attempts: number;
|
|
45
|
+
files: string[];
|
|
46
|
+
};
|
|
47
|
+
declare function publishToGist(options: GistPublishOptions): Promise<GistPublishResult>;
|
|
48
|
+
|
|
49
|
+
export { type GistPublishOptions, type GistPublishResult, type GitHubPublishOptions, type GitHubPublishResult, publishToGist, publishToGitHub };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// src/publish/github.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { basename, posix } from "path";
|
|
4
|
+
|
|
5
|
+
// src/utils/retry.ts
|
|
6
|
+
var DEFAULT_RETRY_POLICY = {
|
|
7
|
+
maxRetries: 3,
|
|
8
|
+
baseMs: 500,
|
|
9
|
+
maxMs: 4e3
|
|
10
|
+
};
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
async function withRetry(operation, policy = DEFAULT_RETRY_POLICY) {
|
|
15
|
+
let attempt = 0;
|
|
16
|
+
let lastError;
|
|
17
|
+
while (attempt <= policy.maxRetries) {
|
|
18
|
+
try {
|
|
19
|
+
const value = await operation();
|
|
20
|
+
return { value, attempts: attempt + 1 };
|
|
21
|
+
} catch (error) {
|
|
22
|
+
lastError = error;
|
|
23
|
+
if (attempt >= policy.maxRetries) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
const backoff = Math.min(policy.baseMs * 2 ** attempt, policy.maxMs);
|
|
27
|
+
const jitter = Math.floor(Math.random() * 125);
|
|
28
|
+
await sleep(backoff + jitter);
|
|
29
|
+
attempt += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw lastError;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/publish/github.ts
|
|
36
|
+
function requireGitHubToken(token) {
|
|
37
|
+
const resolved = token ?? process.env.GITHUB_TOKEN;
|
|
38
|
+
if (!resolved) {
|
|
39
|
+
throw new Error("GITHUB_TOKEN is required for GitHub publish adapter.");
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
async function githubJson(path, init, token, retryPolicy) {
|
|
44
|
+
return withRetry(async () => {
|
|
45
|
+
const response = await fetch(`https://api.github.com${path}`, {
|
|
46
|
+
...init,
|
|
47
|
+
headers: {
|
|
48
|
+
Accept: "application/vnd.github+json",
|
|
49
|
+
Authorization: `Bearer ${token}`,
|
|
50
|
+
"User-Agent": "spectratools-graphic-designer",
|
|
51
|
+
...init.headers
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
throw new Error(
|
|
57
|
+
`GitHub API ${path} failed (${response.status}): ${text || response.statusText}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return await response.json();
|
|
61
|
+
}, retryPolicy);
|
|
62
|
+
}
|
|
63
|
+
async function githubJsonMaybe(path, token, retryPolicy) {
|
|
64
|
+
const { value, attempts } = await withRetry(async () => {
|
|
65
|
+
const response = await fetch(`https://api.github.com${path}`, {
|
|
66
|
+
headers: {
|
|
67
|
+
Accept: "application/vnd.github+json",
|
|
68
|
+
Authorization: `Bearer ${token}`,
|
|
69
|
+
"User-Agent": "spectratools-graphic-designer"
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
if (response.status === 404) {
|
|
73
|
+
return { found: false };
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
throw new Error(
|
|
78
|
+
`GitHub API ${path} failed (${response.status}): ${text || response.statusText}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const json = await response.json();
|
|
82
|
+
return { found: true, value: json };
|
|
83
|
+
}, retryPolicy);
|
|
84
|
+
if (!value.found) {
|
|
85
|
+
return { found: false, attempts };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
found: true,
|
|
89
|
+
value: value.value,
|
|
90
|
+
attempts
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function parseRepo(repo) {
|
|
94
|
+
const [owner, name] = repo.split("/");
|
|
95
|
+
if (!owner || !name) {
|
|
96
|
+
throw new Error(`Invalid repo "${repo}". Expected owner/name.`);
|
|
97
|
+
}
|
|
98
|
+
return { owner, name };
|
|
99
|
+
}
|
|
100
|
+
function toApiContentPath(repo, filePath) {
|
|
101
|
+
const { owner, name } = parseRepo(repo);
|
|
102
|
+
return `/repos/${owner}/${name}/contents/${filePath}`;
|
|
103
|
+
}
|
|
104
|
+
function normalizePath(pathPrefix, filename) {
|
|
105
|
+
const trimmed = (pathPrefix ?? "artifacts").replace(/^\/+|\/+$/gu, "");
|
|
106
|
+
return posix.join(trimmed, filename);
|
|
107
|
+
}
|
|
108
|
+
async function publishToGitHub(options) {
|
|
109
|
+
const token = requireGitHubToken(options.token);
|
|
110
|
+
const branch = options.branch ?? "main";
|
|
111
|
+
const commitMessage = options.commitMessage ?? "chore(graphic-designer): publish deterministic artifacts";
|
|
112
|
+
const [imageBuffer, metadataBuffer] = await Promise.all([
|
|
113
|
+
readFile(options.imagePath),
|
|
114
|
+
readFile(options.metadataPath)
|
|
115
|
+
]);
|
|
116
|
+
const uploads = [
|
|
117
|
+
{
|
|
118
|
+
sourcePath: options.imagePath,
|
|
119
|
+
destination: normalizePath(options.pathPrefix, basename(options.imagePath)),
|
|
120
|
+
content: imageBuffer.toString("base64")
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
sourcePath: options.metadataPath,
|
|
124
|
+
destination: normalizePath(options.pathPrefix, basename(options.metadataPath)),
|
|
125
|
+
content: metadataBuffer.toString("base64")
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
let totalAttempts = 0;
|
|
129
|
+
const files = [];
|
|
130
|
+
for (const upload of uploads) {
|
|
131
|
+
const existingPath = `${toApiContentPath(options.repo, upload.destination)}?ref=${encodeURIComponent(branch)}`;
|
|
132
|
+
const existing = await githubJsonMaybe(
|
|
133
|
+
existingPath,
|
|
134
|
+
token,
|
|
135
|
+
options.retryPolicy
|
|
136
|
+
);
|
|
137
|
+
totalAttempts += existing.attempts;
|
|
138
|
+
const body = {
|
|
139
|
+
message: `${commitMessage} (${basename(upload.sourcePath)})`,
|
|
140
|
+
content: upload.content,
|
|
141
|
+
branch,
|
|
142
|
+
sha: existing.value?.sha
|
|
143
|
+
};
|
|
144
|
+
const putPath = toApiContentPath(options.repo, upload.destination);
|
|
145
|
+
const published = await githubJson(
|
|
146
|
+
putPath,
|
|
147
|
+
{
|
|
148
|
+
method: "PUT",
|
|
149
|
+
body: JSON.stringify(body)
|
|
150
|
+
},
|
|
151
|
+
token,
|
|
152
|
+
options.retryPolicy
|
|
153
|
+
);
|
|
154
|
+
totalAttempts += published.attempts;
|
|
155
|
+
files.push({
|
|
156
|
+
path: upload.destination,
|
|
157
|
+
...published.value.content?.sha ? { sha: published.value.content.sha } : {},
|
|
158
|
+
...published.value.content?.html_url ? { htmlUrl: published.value.content.html_url } : {}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
target: "github",
|
|
163
|
+
repo: options.repo,
|
|
164
|
+
branch,
|
|
165
|
+
attempts: totalAttempts,
|
|
166
|
+
files
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/publish/gist.ts
|
|
171
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
172
|
+
import { basename as basename2 } from "path";
|
|
173
|
+
function requireGitHubToken2(token) {
|
|
174
|
+
const resolved = token ?? process.env.GITHUB_TOKEN;
|
|
175
|
+
if (!resolved) {
|
|
176
|
+
throw new Error("GITHUB_TOKEN is required for gist publish adapter.");
|
|
177
|
+
}
|
|
178
|
+
return resolved;
|
|
179
|
+
}
|
|
180
|
+
async function gistJson(path, init, token, retryPolicy) {
|
|
181
|
+
return withRetry(async () => {
|
|
182
|
+
const response = await fetch(`https://api.github.com${path}`, {
|
|
183
|
+
...init,
|
|
184
|
+
headers: {
|
|
185
|
+
Accept: "application/vnd.github+json",
|
|
186
|
+
Authorization: `Bearer ${token}`,
|
|
187
|
+
"User-Agent": "spectratools-graphic-designer",
|
|
188
|
+
...init.headers
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const text = await response.text();
|
|
193
|
+
throw new Error(
|
|
194
|
+
`GitHub Gist API ${path} failed (${response.status}): ${text || response.statusText}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return await response.json();
|
|
198
|
+
}, retryPolicy);
|
|
199
|
+
}
|
|
200
|
+
async function publishToGist(options) {
|
|
201
|
+
const token = requireGitHubToken2(options.token);
|
|
202
|
+
const [imageBuffer, metadataBuffer] = await Promise.all([
|
|
203
|
+
readFile2(options.imagePath),
|
|
204
|
+
readFile2(options.metadataPath)
|
|
205
|
+
]);
|
|
206
|
+
const prefix = options.filenamePrefix ?? basename2(options.imagePath, ".png");
|
|
207
|
+
const imageBase64 = imageBuffer.toString("base64");
|
|
208
|
+
const metadataText = metadataBuffer.toString("utf8");
|
|
209
|
+
const readmeName = `${prefix}.md`;
|
|
210
|
+
const b64Name = `${prefix}.png.base64.txt`;
|
|
211
|
+
const metadataName = `${prefix}.meta.json`;
|
|
212
|
+
const markdown = [
|
|
213
|
+
`# ${prefix}`,
|
|
214
|
+
"",
|
|
215
|
+
"Deterministic graphic-designer artifact.",
|
|
216
|
+
"",
|
|
217
|
+
`- Source image filename: ${basename2(options.imagePath)}`,
|
|
218
|
+
`- Sidecar metadata filename: ${metadataName}`,
|
|
219
|
+
"",
|
|
220
|
+
"## Preview",
|
|
221
|
+
"",
|
|
222
|
+
``,
|
|
223
|
+
"",
|
|
224
|
+
"## Notes",
|
|
225
|
+
"",
|
|
226
|
+
`- This gist stores the PNG as base64 in \`${b64Name}\` for deterministic transport.`
|
|
227
|
+
].join("\n");
|
|
228
|
+
const payload = {
|
|
229
|
+
description: options.description ?? "graphic-designer publish",
|
|
230
|
+
public: options.public ?? false,
|
|
231
|
+
files: {
|
|
232
|
+
[readmeName]: { content: markdown },
|
|
233
|
+
[b64Name]: { content: imageBase64 },
|
|
234
|
+
[metadataName]: { content: metadataText }
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const endpoint = options.gistId ? `/gists/${options.gistId}` : "/gists";
|
|
238
|
+
const method = options.gistId ? "PATCH" : "POST";
|
|
239
|
+
const published = await gistJson(
|
|
240
|
+
endpoint,
|
|
241
|
+
{
|
|
242
|
+
method,
|
|
243
|
+
body: JSON.stringify(payload)
|
|
244
|
+
},
|
|
245
|
+
token,
|
|
246
|
+
options.retryPolicy
|
|
247
|
+
);
|
|
248
|
+
return {
|
|
249
|
+
target: "gist",
|
|
250
|
+
gistId: published.value.id,
|
|
251
|
+
htmlUrl: published.value.html_url,
|
|
252
|
+
attempts: published.attempts,
|
|
253
|
+
files: Object.keys(published.value.files ?? payload.files)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export {
|
|
257
|
+
publishToGist,
|
|
258
|
+
publishToGitHub
|
|
259
|
+
};
|
package/dist/qa.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { R as RenderMetadata, D as DesignSpec } from './spec.schema-BxXBTOn-.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
import '@napi-rs/canvas';
|
|
4
|
+
|
|
5
|
+
type QaSeverity = 'error' | 'warning';
|
|
6
|
+
type QaIssue = {
|
|
7
|
+
code: 'DIMENSIONS_MISMATCH' | 'ELEMENT_CLIPPED' | 'ELEMENT_OVERLAP' | 'LOW_CONTRAST' | 'FOOTER_SPACING' | 'TEXT_TRUNCATED' | 'MISSING_LAYOUT' | 'DRAW_OUT_OF_BOUNDS';
|
|
8
|
+
severity: QaSeverity;
|
|
9
|
+
message: string;
|
|
10
|
+
elementId?: string;
|
|
11
|
+
details?: Record<string, number | string | boolean>;
|
|
12
|
+
};
|
|
13
|
+
type QaReport = {
|
|
14
|
+
pass: boolean;
|
|
15
|
+
checkedAt: string;
|
|
16
|
+
imagePath: string;
|
|
17
|
+
expected: {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
scale: number;
|
|
21
|
+
minContrastRatio: number;
|
|
22
|
+
minFooterSpacing: number;
|
|
23
|
+
};
|
|
24
|
+
measured: {
|
|
25
|
+
width?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
footerSpacingPx?: number;
|
|
28
|
+
};
|
|
29
|
+
issues: QaIssue[];
|
|
30
|
+
};
|
|
31
|
+
declare function readMetadata(path: string): Promise<RenderMetadata>;
|
|
32
|
+
declare function runQa(options: {
|
|
33
|
+
imagePath: string;
|
|
34
|
+
spec: DesignSpec;
|
|
35
|
+
metadata?: RenderMetadata;
|
|
36
|
+
}): Promise<QaReport>;
|
|
37
|
+
|
|
38
|
+
export { type QaIssue, type QaReport, type QaSeverity, readMetadata, runQa };
|