@pixeyedev/core 0.4.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/ci.d.ts +14 -0
- package/dist/ci.js +70 -0
- package/dist/client.d.ts +48 -0
- package/dist/client.js +112 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +111 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/discover.d.ts +10 -0
- package/dist/discover.js +70 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/parallel.d.ts +4 -0
- package/dist/parallel.js +13 -0
- package/package.json +33 -0
package/dist/ci.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CIEnvironment {
|
|
2
|
+
provider: string;
|
|
3
|
+
commit: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
baseBranch?: string;
|
|
6
|
+
prNumber?: number;
|
|
7
|
+
repository?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Try to detect the base branch using git merge-base.
|
|
11
|
+
* Checks common default branch names (main, master, develop).
|
|
12
|
+
*/
|
|
13
|
+
export declare const detectBaseBranchFromGit: () => string | undefined;
|
|
14
|
+
export declare const detectCIEnvironment: () => CIEnvironment | null;
|
package/dist/ci.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
const detectGitHubActions = () => {
|
|
3
|
+
if (!process.env.GITHUB_ACTIONS) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const commit = process.env.GITHUB_SHA;
|
|
7
|
+
const branch = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME;
|
|
8
|
+
const runId = process.env.GITHUB_RUN_ID;
|
|
9
|
+
if (!commit || !branch || !runId) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const prRaw = process.env.PIXEYE_PR;
|
|
13
|
+
const prNumber = prRaw ? parseInt(prRaw, 10) : undefined;
|
|
14
|
+
// PR target branch: GITHUB_BASE_REF is set on pull_request events
|
|
15
|
+
const baseBranch = process.env.GITHUB_BASE_REF || undefined;
|
|
16
|
+
return {
|
|
17
|
+
provider: 'github-actions',
|
|
18
|
+
commit,
|
|
19
|
+
branch,
|
|
20
|
+
baseBranch,
|
|
21
|
+
prNumber: prNumber && !isNaN(prNumber) ? prNumber : undefined,
|
|
22
|
+
repository: process.env.GITHUB_REPOSITORY
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
const detectGitLabCI = () => {
|
|
26
|
+
if (!process.env.GITLAB_CI) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const commit = process.env.CI_COMMIT_SHA;
|
|
30
|
+
const branch = process.env.CI_COMMIT_REF_NAME;
|
|
31
|
+
const pipelineId = process.env.CI_PIPELINE_ID;
|
|
32
|
+
if (!commit || !branch || !pipelineId) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const mrIid = process.env.CI_MERGE_REQUEST_IID;
|
|
36
|
+
return {
|
|
37
|
+
provider: 'gitlab-ci',
|
|
38
|
+
commit,
|
|
39
|
+
branch,
|
|
40
|
+
baseBranch: process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME || undefined,
|
|
41
|
+
prNumber: mrIid ? parseInt(mrIid, 10) : undefined,
|
|
42
|
+
repository: undefined
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Try to detect the base branch using git merge-base.
|
|
47
|
+
* Checks common default branch names (main, master, develop).
|
|
48
|
+
*/
|
|
49
|
+
export const detectBaseBranchFromGit = () => {
|
|
50
|
+
const candidates = ['main', 'master', 'develop'];
|
|
51
|
+
for (const candidate of candidates) {
|
|
52
|
+
try {
|
|
53
|
+
execFileSync('git', ['rev-parse', '--verify', `origin/${candidate}`], { stdio: 'ignore' });
|
|
54
|
+
execFileSync('git', ['merge-base', 'HEAD', `origin/${candidate}`], { stdio: 'ignore' });
|
|
55
|
+
return candidate;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Branch doesn't exist or no common ancestor
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
export const detectCIEnvironment = () => {
|
|
64
|
+
const ci = detectGitHubActions() ?? detectGitLabCI() ?? null;
|
|
65
|
+
// If CI detected but no baseBranch, try git merge-base
|
|
66
|
+
if (ci && !ci.baseBranch) {
|
|
67
|
+
ci.baseBranch = detectBaseBranchFromGit();
|
|
68
|
+
}
|
|
69
|
+
return ci;
|
|
70
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { CreateBuildResponse, ListBuildsResponse, PipelineFinalizeResponse, PresignUploadResponse, ScreenshotManifestEntry, SubmitShardResponse } from '@pixeyedev/shared';
|
|
2
|
+
export declare class PixeyeError extends Error {
|
|
3
|
+
status: number;
|
|
4
|
+
path: string;
|
|
5
|
+
body: string;
|
|
6
|
+
constructor(status: number, path: string, body: string);
|
|
7
|
+
}
|
|
8
|
+
export interface PixeyeClientOptions {
|
|
9
|
+
serverUrl: string;
|
|
10
|
+
token: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class PixeyeClient {
|
|
13
|
+
private serverUrl;
|
|
14
|
+
private token;
|
|
15
|
+
constructor(options: PixeyeClientOptions);
|
|
16
|
+
private request;
|
|
17
|
+
createBuild(opts: {
|
|
18
|
+
projectToken: string;
|
|
19
|
+
commitSha: string;
|
|
20
|
+
branch: string;
|
|
21
|
+
prNumber?: number;
|
|
22
|
+
shardCount?: number;
|
|
23
|
+
partial?: boolean;
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
runner?: string;
|
|
26
|
+
baseBranch?: string;
|
|
27
|
+
authorName?: string;
|
|
28
|
+
commitMessage?: string;
|
|
29
|
+
}): Promise<CreateBuildResponse>;
|
|
30
|
+
presignUpload(opts: {
|
|
31
|
+
buildId: string;
|
|
32
|
+
hashes: string[];
|
|
33
|
+
formats?: Record<string, 'png' | 'webp'>;
|
|
34
|
+
}): Promise<PresignUploadResponse>;
|
|
35
|
+
uploadFile(presignedUrl: string, buffer: Buffer, contentType: string): Promise<void>;
|
|
36
|
+
submitShard(opts: {
|
|
37
|
+
buildId: string;
|
|
38
|
+
shardIndex: number;
|
|
39
|
+
screenshots: ScreenshotManifestEntry[];
|
|
40
|
+
}): Promise<SubmitShardResponse>;
|
|
41
|
+
listBuilds(opts: {
|
|
42
|
+
commitSha: string;
|
|
43
|
+
branch: string;
|
|
44
|
+
}): Promise<ListBuildsResponse>;
|
|
45
|
+
finalizePipeline(pipelineId: string): Promise<PipelineFinalizeResponse>;
|
|
46
|
+
checkFileExists(hash: string): Promise<boolean>;
|
|
47
|
+
uploadLegacy(buffer: Buffer, hash: string, filename: string): Promise<void>;
|
|
48
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { CreateBuildResponseSchema, ListBuildsResponseSchema, PipelineFinalizeResponseSchema, PresignUploadResponseSchema, SubmitShardResponseSchema } from '@pixeyedev/shared';
|
|
2
|
+
export class PixeyeError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
path;
|
|
5
|
+
body;
|
|
6
|
+
constructor(status, path, body) {
|
|
7
|
+
super(`Pixeye API ${path} failed (${status}): ${body}`);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.path = path;
|
|
10
|
+
this.body = body;
|
|
11
|
+
this.name = 'PixeyeError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class PixeyeClient {
|
|
15
|
+
serverUrl;
|
|
16
|
+
token;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.serverUrl = options.serverUrl.replace(/\/$/, '');
|
|
19
|
+
this.token = options.token;
|
|
20
|
+
}
|
|
21
|
+
async request(path, options = {}) {
|
|
22
|
+
const url = `${this.serverUrl}${path}`;
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
...options,
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
Authorization: `Bearer ${this.token}`,
|
|
28
|
+
...options.headers
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const body = await response.text();
|
|
33
|
+
throw new PixeyeError(response.status, path, body);
|
|
34
|
+
}
|
|
35
|
+
return response.json();
|
|
36
|
+
}
|
|
37
|
+
async createBuild(opts) {
|
|
38
|
+
const body = { ...opts };
|
|
39
|
+
if (opts.runner) {
|
|
40
|
+
body.metadata = { ...(opts.metadata ?? {}), runner: opts.runner };
|
|
41
|
+
}
|
|
42
|
+
const raw = await this.request('/api/v1/builds', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
body: JSON.stringify(body)
|
|
45
|
+
});
|
|
46
|
+
return CreateBuildResponseSchema.parse(raw);
|
|
47
|
+
}
|
|
48
|
+
async presignUpload(opts) {
|
|
49
|
+
const raw = await this.request('/api/v1/upload/presign', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
token: this.token,
|
|
53
|
+
buildId: opts.buildId,
|
|
54
|
+
hashes: opts.hashes,
|
|
55
|
+
...(opts.formats && Object.keys(opts.formats).length > 0 ? { formats: opts.formats } : {})
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
return PresignUploadResponseSchema.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
async uploadFile(presignedUrl, buffer, contentType) {
|
|
61
|
+
const res = await fetch(presignedUrl, {
|
|
62
|
+
method: 'PUT',
|
|
63
|
+
body: new Uint8Array(buffer),
|
|
64
|
+
headers: { 'Content-Type': contentType }
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new PixeyeError(res.status, presignedUrl, await res.text());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async submitShard(opts) {
|
|
71
|
+
const raw = await this.request(`/api/v1/builds/${opts.buildId}/shards`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
shardIndex: opts.shardIndex,
|
|
75
|
+
screenshots: opts.screenshots,
|
|
76
|
+
projectToken: this.token
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
return SubmitShardResponseSchema.parse(raw);
|
|
80
|
+
}
|
|
81
|
+
async listBuilds(opts) {
|
|
82
|
+
const params = new URLSearchParams({
|
|
83
|
+
commitSha: opts.commitSha,
|
|
84
|
+
branch: opts.branch
|
|
85
|
+
});
|
|
86
|
+
const raw = await this.request(`/api/v1/builds?${params}`);
|
|
87
|
+
return ListBuildsResponseSchema.parse(raw);
|
|
88
|
+
}
|
|
89
|
+
async finalizePipeline(pipelineId) {
|
|
90
|
+
const raw = await this.request(`/api/v1/pipelines/${pipelineId}/finalize`, {
|
|
91
|
+
method: 'POST'
|
|
92
|
+
});
|
|
93
|
+
return PipelineFinalizeResponseSchema.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
async checkFileExists(hash) {
|
|
96
|
+
const result = await this.request(`/api/v1/upload/${hash}?token=${encodeURIComponent(this.token)}`);
|
|
97
|
+
return result.exists;
|
|
98
|
+
}
|
|
99
|
+
async uploadLegacy(buffer, hash, filename) {
|
|
100
|
+
const blob = new Blob([new Uint8Array(buffer)], { type: 'image/png' });
|
|
101
|
+
const formData = new FormData();
|
|
102
|
+
formData.append('file', blob, filename);
|
|
103
|
+
formData.append('hash', hash);
|
|
104
|
+
formData.append('token', this.token);
|
|
105
|
+
const url = `${this.serverUrl}/api/v1/upload`;
|
|
106
|
+
const response = await fetch(url, { method: 'POST', body: formData });
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const body = await response.text();
|
|
109
|
+
throw new PixeyeError(response.status, '/api/v1/upload', body);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface PixeyeConfig {
|
|
2
|
+
server?: string;
|
|
3
|
+
token?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ProjectConfig {
|
|
6
|
+
server: string;
|
|
7
|
+
token: string;
|
|
8
|
+
screenshotDir?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface EnvConfig {
|
|
11
|
+
server?: string;
|
|
12
|
+
token?: string;
|
|
13
|
+
commit?: string;
|
|
14
|
+
branch?: string;
|
|
15
|
+
pr?: number;
|
|
16
|
+
runner?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedConfig {
|
|
19
|
+
serverUrl: string;
|
|
20
|
+
token: string;
|
|
21
|
+
commitSha: string;
|
|
22
|
+
branch: string;
|
|
23
|
+
prNumber?: number;
|
|
24
|
+
runner?: string;
|
|
25
|
+
baseBranch?: string;
|
|
26
|
+
shardIndex: number;
|
|
27
|
+
shardTotal: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const readGlobalConfig: () => Promise<PixeyeConfig>;
|
|
30
|
+
export declare const writeGlobalConfig: (config: PixeyeConfig) => Promise<void>;
|
|
31
|
+
export declare const readProjectConfig: (cwd: string) => Promise<ProjectConfig | null>;
|
|
32
|
+
export declare const writeProjectConfig: (cwd: string, config: ProjectConfig) => Promise<void>;
|
|
33
|
+
export declare const readEnvConfig: () => EnvConfig;
|
|
34
|
+
/** @internal Exported for testing */
|
|
35
|
+
export declare const parseYamlLike: (content: string) => ProjectConfig;
|
|
36
|
+
export declare const resolveConfig: (overrides?: Partial<ResolvedConfig> & {
|
|
37
|
+
cwd?: string;
|
|
38
|
+
}) => Promise<ResolvedConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { detectCIEnvironment } from './ci.js';
|
|
5
|
+
import { DEFAULT_SERVER_URL } from './constants.js';
|
|
6
|
+
const GLOBAL_CONFIG_DIR = join(homedir(), '.pixeye');
|
|
7
|
+
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, 'config.json');
|
|
8
|
+
const PROJECT_CONFIG_NAME = '.pixeye.yml';
|
|
9
|
+
export const readGlobalConfig = async () => {
|
|
10
|
+
try {
|
|
11
|
+
const content = await readFile(GLOBAL_CONFIG_PATH, 'utf-8');
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export const writeGlobalConfig = async (config) => {
|
|
19
|
+
await mkdir(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
20
|
+
await writeFile(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
21
|
+
};
|
|
22
|
+
export const readProjectConfig = async (cwd) => {
|
|
23
|
+
try {
|
|
24
|
+
const content = await readFile(join(cwd, PROJECT_CONFIG_NAME), 'utf-8');
|
|
25
|
+
return parseYamlLike(content);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export const writeProjectConfig = async (cwd, config) => {
|
|
32
|
+
const lines = [
|
|
33
|
+
`server: ${config.server}`,
|
|
34
|
+
`token: ${config.token}`,
|
|
35
|
+
...(config.screenshotDir ? [`screenshotDir: ${config.screenshotDir}`] : [])
|
|
36
|
+
];
|
|
37
|
+
await writeFile(join(cwd, PROJECT_CONFIG_NAME), lines.join('\n') + '\n');
|
|
38
|
+
};
|
|
39
|
+
export const readEnvConfig = () => {
|
|
40
|
+
const intOrUndef = (val) => {
|
|
41
|
+
if (val === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const n = parseInt(val, 10);
|
|
45
|
+
return isNaN(n) ? undefined : n;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
server: process.env.PIXEYE_SERVER || undefined,
|
|
49
|
+
token: process.env.PIXEYE_TOKEN || undefined,
|
|
50
|
+
commit: process.env.PIXEYE_COMMIT || undefined,
|
|
51
|
+
branch: process.env.PIXEYE_BRANCH || undefined,
|
|
52
|
+
pr: intOrUndef(process.env.PIXEYE_PR),
|
|
53
|
+
runner: process.env.PIXEYE_RUNNER || undefined
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** @internal Exported for testing */
|
|
57
|
+
export const parseYamlLike = (content) => {
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const line of content.split('\n')) {
|
|
60
|
+
const match = line.match(/^([\w-]+):\s*(.+)$/);
|
|
61
|
+
if (match) {
|
|
62
|
+
let value = match[2].trim();
|
|
63
|
+
const isQuoted = (value.startsWith('"') && value.endsWith('"')) ||
|
|
64
|
+
(value.startsWith("'") && value.endsWith("'"));
|
|
65
|
+
// Strip surrounding quotes
|
|
66
|
+
if (isQuoted) {
|
|
67
|
+
value = value.slice(1, -1);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Remove inline comments only for unquoted values
|
|
71
|
+
value = value.replace(/\s+#.*$/, '');
|
|
72
|
+
}
|
|
73
|
+
result[match[1]] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (typeof result.server !== 'string' || typeof result.token !== 'string') {
|
|
77
|
+
throw new Error('.pixeye.yml: "server" and "token" are required');
|
|
78
|
+
}
|
|
79
|
+
return { server: result.server, token: result.token, screenshotDir: result.screenshotDir };
|
|
80
|
+
};
|
|
81
|
+
export const resolveConfig = async (overrides = {}) => {
|
|
82
|
+
const envConfig = readEnvConfig();
|
|
83
|
+
const ci = detectCIEnvironment();
|
|
84
|
+
const projectConfig = await readProjectConfig(overrides.cwd ?? process.cwd());
|
|
85
|
+
const globalConfig = await readGlobalConfig();
|
|
86
|
+
const serverUrl = overrides.serverUrl ??
|
|
87
|
+
envConfig.server ??
|
|
88
|
+
projectConfig?.server ??
|
|
89
|
+
globalConfig.server ??
|
|
90
|
+
DEFAULT_SERVER_URL;
|
|
91
|
+
const token = overrides.token ?? envConfig.token ?? projectConfig?.token ?? globalConfig.token;
|
|
92
|
+
if (!token) {
|
|
93
|
+
throw new Error('Pixeye token required. Set PIXEYE_TOKEN, pass --token, or run `pixeye login`.');
|
|
94
|
+
}
|
|
95
|
+
const commitSha = overrides.commitSha ?? envConfig.commit ?? ci?.commit;
|
|
96
|
+
const branch = overrides.branch ?? envConfig.branch ?? ci?.branch;
|
|
97
|
+
if (!commitSha || !branch) {
|
|
98
|
+
throw new Error('commitSha and branch required. Set PIXEYE_COMMIT/PIXEYE_BRANCH, pass --commit/--branch, or run in CI.');
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
serverUrl: serverUrl.replace(/\/$/, ''),
|
|
102
|
+
token,
|
|
103
|
+
commitSha,
|
|
104
|
+
branch,
|
|
105
|
+
prNumber: overrides.prNumber ?? envConfig.pr ?? ci?.prNumber,
|
|
106
|
+
runner: overrides.runner ?? envConfig.runner,
|
|
107
|
+
baseBranch: overrides.baseBranch ?? ci?.baseBranch,
|
|
108
|
+
shardIndex: overrides.shardIndex ?? 0,
|
|
109
|
+
shardTotal: overrides.shardTotal ?? 1
|
|
110
|
+
};
|
|
111
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface DiscoveredFile {
|
|
2
|
+
filePath: string;
|
|
3
|
+
name: string;
|
|
4
|
+
fileHash: string;
|
|
5
|
+
format: 'png' | 'webp';
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
metadata?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export declare const discoverScreenshots: (directory: string) => Promise<DiscoveredFile[]>;
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
5
|
+
const WEBP_RIFF = Buffer.from('RIFF');
|
|
6
|
+
const WEBP_WEBP = Buffer.from('WEBP');
|
|
7
|
+
const readPngDimensions = (buffer) => {
|
|
8
|
+
if (buffer.length < 24 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
9
|
+
throw new Error('Invalid PNG file: missing or incorrect PNG signature');
|
|
10
|
+
}
|
|
11
|
+
const width = buffer.readUInt32BE(16);
|
|
12
|
+
const height = buffer.readUInt32BE(20);
|
|
13
|
+
return { width, height };
|
|
14
|
+
};
|
|
15
|
+
const readWebpDimensions = (buffer) => {
|
|
16
|
+
if (buffer.length < 30 ||
|
|
17
|
+
!buffer.subarray(0, 4).equals(WEBP_RIFF) ||
|
|
18
|
+
!buffer.subarray(8, 12).equals(WEBP_WEBP)) {
|
|
19
|
+
throw new Error('Invalid WebP file: missing RIFF/WEBP signature');
|
|
20
|
+
}
|
|
21
|
+
// VP8 lossy: chunk starts at offset 12, dimensions at offset 26-29
|
|
22
|
+
const chunk = buffer.subarray(12, 16).toString('ascii');
|
|
23
|
+
if (chunk === 'VP8 ') {
|
|
24
|
+
const width = buffer.readUInt16LE(26) & 0x3fff;
|
|
25
|
+
const height = buffer.readUInt16LE(28) & 0x3fff;
|
|
26
|
+
return { width, height };
|
|
27
|
+
}
|
|
28
|
+
// VP8L lossless: chunk starts at offset 12, dimensions encoded at offset 21
|
|
29
|
+
if (chunk === 'VP8L') {
|
|
30
|
+
const bits = buffer.readUInt32LE(21);
|
|
31
|
+
const width = (bits & 0x3fff) + 1;
|
|
32
|
+
const height = ((bits >> 14) & 0x3fff) + 1;
|
|
33
|
+
return { width, height };
|
|
34
|
+
}
|
|
35
|
+
// VP8X extended: dimensions at offset 24-29
|
|
36
|
+
if (chunk === 'VP8X') {
|
|
37
|
+
const width = (buffer.readUIntLE(24, 3) & 0xffffff) + 1;
|
|
38
|
+
const height = (buffer.readUIntLE(27, 3) & 0xffffff) + 1;
|
|
39
|
+
return { width, height };
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unsupported WebP chunk type: ${chunk}`);
|
|
42
|
+
};
|
|
43
|
+
const readDimensions = (buffer, format) => {
|
|
44
|
+
return format === 'png' ? readPngDimensions(buffer) : readWebpDimensions(buffer);
|
|
45
|
+
};
|
|
46
|
+
export const discoverScreenshots = async (directory) => {
|
|
47
|
+
const files = await glob('**/*.{png,webp}', { cwd: directory, posix: true });
|
|
48
|
+
files.sort();
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const relativePath of files) {
|
|
51
|
+
const filePath = `${directory}/${relativePath}`;
|
|
52
|
+
const fileBuffer = await readFile(filePath);
|
|
53
|
+
const format = relativePath.endsWith('.webp') ? 'webp' : 'png';
|
|
54
|
+
const fileHash = createHash('sha256').update(fileBuffer).digest('hex');
|
|
55
|
+
const { width, height } = readDimensions(fileBuffer, format);
|
|
56
|
+
const name = relativePath.replace(/\.(png|webp)$/, '');
|
|
57
|
+
// Check for companion metadata file
|
|
58
|
+
const metadataPath = filePath.replace(/\.(png|webp)$/, '.pixeye.json');
|
|
59
|
+
let metadata;
|
|
60
|
+
try {
|
|
61
|
+
const metadataContent = await readFile(metadataPath, 'utf-8');
|
|
62
|
+
metadata = JSON.parse(metadataContent);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// No metadata file or invalid JSON — skip
|
|
66
|
+
}
|
|
67
|
+
results.push({ filePath, name, fileHash, format, width, height, metadata });
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { PixeyeClientOptions } from './client.js';
|
|
2
|
+
export { PixeyeClient, PixeyeError } from './client.js';
|
|
3
|
+
export type { EnvConfig, PixeyeConfig, ProjectConfig, ResolvedConfig } from './config.js';
|
|
4
|
+
export { parseYamlLike, readEnvConfig, readGlobalConfig, readProjectConfig, resolveConfig, writeGlobalConfig, writeProjectConfig } from './config.js';
|
|
5
|
+
export type { CIEnvironment } from './ci.js';
|
|
6
|
+
export { detectBaseBranchFromGit, detectCIEnvironment } from './ci.js';
|
|
7
|
+
export type { DiscoveredFile } from './discover.js';
|
|
8
|
+
export { discoverScreenshots } from './discover.js';
|
|
9
|
+
export { runParallel } from './parallel.js';
|
|
10
|
+
export { DEFAULT_SERVER_URL, DEFAULT_UPLOAD_CONCURRENCY } from './constants.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { PixeyeClient, PixeyeError } from './client.js';
|
|
2
|
+
export { parseYamlLike, readEnvConfig, readGlobalConfig, readProjectConfig, resolveConfig, writeGlobalConfig, writeProjectConfig } from './config.js';
|
|
3
|
+
export { detectBaseBranchFromGit, detectCIEnvironment } from './ci.js';
|
|
4
|
+
export { discoverScreenshots } from './discover.js';
|
|
5
|
+
// Parallel execution
|
|
6
|
+
export { runParallel } from './parallel.js';
|
|
7
|
+
// Constants
|
|
8
|
+
export { DEFAULT_SERVER_URL, DEFAULT_UPLOAD_CONCURRENCY } from './constants.js';
|
package/dist/parallel.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run async tasks in parallel with a concurrency limit.
|
|
3
|
+
*/
|
|
4
|
+
export const runParallel = async (items, concurrency, fn) => {
|
|
5
|
+
let index = 0;
|
|
6
|
+
const worker = async () => {
|
|
7
|
+
while (index < items.length) {
|
|
8
|
+
const i = index++;
|
|
9
|
+
await fn(items[i]);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pixeyedev/core",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"lint": "echo 'ok'",
|
|
22
|
+
"ai-check": "exit 0",
|
|
23
|
+
"publish:dry": "npm run build && npm publish --access public --dry-run"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@pixeyedev/shared": "^0.4.0",
|
|
27
|
+
"glob": "^11.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"typescript": "^5.7.0"
|
|
32
|
+
}
|
|
33
|
+
}
|