diffpx 0.0.2

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/bin/diffpx.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ const crypto = require('crypto');
3
+ const {loadConfig} = require('../src/configLoader');
4
+ const {runJob} = require('../src/runner');
5
+
6
+ (async () => {
7
+ const {config, devicesConfig, settings} = await loadConfig();
8
+
9
+ const ppKey = process.env.PP_KEY;
10
+ if (!ppKey) throw new Error('No API key found in .env or settings.yml');
11
+
12
+ const timestamp = new Date().toISOString().replace('T', ' ').split('.')[0];
13
+ const groupId = crypto.randomUUID();
14
+
15
+ const payload = {
16
+ timestamp,
17
+ groupId,
18
+ settings,
19
+ devices: devicesConfig,
20
+ snapshots: config
21
+ };
22
+
23
+ try {
24
+ await runJob({
25
+ ppKey,
26
+ payload
27
+ });
28
+ } catch (err) {
29
+ console.error('🔴 Error:', err.message);
30
+ process.exitCode = 1;
31
+ }
32
+ })();
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "diffpx",
3
+ "version": "0.0.2",
4
+ "bin": {
5
+ "diffpx": "bin/diffpx.js"
6
+ },
7
+ "scripts": {
8
+ "start": "node bin/diffpx.js"
9
+ },
10
+ "dependencies": {
11
+ "axios": "^1.7.4",
12
+ "cli-progress": "^3.12.0",
13
+ "dotenv": "^16.4.5",
14
+ "js-yaml": "^4.1.0"
15
+ },
16
+ "description": "",
17
+ "keywords": [],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "files": [
21
+ "bin",
22
+ "src"
23
+ ]
24
+ }
@@ -0,0 +1,11 @@
1
+ const fs = require('fs');
2
+ const yaml = require('js-yaml');
3
+ const validateConfig = require('./validateConfig');
4
+
5
+ async function loadConfig() {
6
+ const { config, devicesConfig } = await validateConfig();
7
+ const settings = yaml.load(fs.readFileSync('settings.yml', 'utf8'));
8
+ return { config, devicesConfig, settings };
9
+ }
10
+
11
+ module.exports = { loadConfig };
package/src/runner.js ADDED
@@ -0,0 +1,117 @@
1
+ const cliProgress = require('cli-progress');
2
+ const {pollRun, submitRun} = require('./upload');
3
+
4
+ function normalizePxLabel(v) {
5
+ if (v === null || v === undefined) return '';
6
+ const s = String(v).trim();
7
+ if (!s) return '';
8
+ return s.endsWith('px') ? s : `${s}px`;
9
+ }
10
+
11
+ function getInitialSnapshotName(payload) {
12
+ const n = payload?.snapshots?.[0]?.name;
13
+ return n ? String(n) : 'snapshots';
14
+ }
15
+
16
+ function getInitialWidthName(payload) {
17
+ const w = payload?.settings?.width?.[0];
18
+ const label = normalizePxLabel(w);
19
+ return label || 'widths';
20
+ }
21
+
22
+ function getProgress(status) {
23
+ const p = status?.progress || status?.data?.progress || {};
24
+
25
+ const snapshotsTotal = Number(p.snapshotsTotal ?? p.outerTotal ?? p.totalSnapshots ?? p.total ?? 0) || 0;
26
+ const snapshotsDone = Number(p.snapshotsDone ?? p.outerDone ?? p.completedSnapshots ?? p.completed ?? 0) || 0;
27
+ const snapshotName = String(p.snapshotName ?? p.outerName ?? p.currentSnapshot ?? '') || '';
28
+
29
+ const widthsTotal = Number(p.widthsTotal ?? p.innerTotal ?? p.runsTotal ?? p.totalRuns ?? 0) || 0;
30
+ const widthsDone = Number(p.widthsDone ?? p.innerDone ?? p.runsDone ?? p.doneRuns ?? 0) || 0;
31
+ const widthLabel = String(p.widthLabel ?? p.runName ?? p.innerName ?? p.currentWidth ?? '') || '';
32
+
33
+ const state = String(status?.state || status?.status || '').toLowerCase();
34
+ const error = status?.error || status?.message || null;
35
+
36
+ return {
37
+ snapshotsTotal,
38
+ snapshotsDone,
39
+ snapshotName,
40
+ widthsTotal,
41
+ widthsDone,
42
+ widthLabel,
43
+ state,
44
+ error
45
+ };
46
+ }
47
+
48
+ async function runJob({ppKey, payload}) {
49
+ const multiBar = new cliProgress.MultiBar({
50
+ clearOnComplete: false,
51
+ hideCursor: true,
52
+ barCompleteChar: 'â–ˆ',
53
+ barIncompleteChar: 'â–‘',
54
+ format: '{name} [{bar}] {percentage}% | {value}/{total}'
55
+ }, cliProgress.Presets.shades_classic);
56
+
57
+ const initialSnapshotsTotal = Array.isArray(payload?.snapshots) ? payload.snapshots.length : 1;
58
+ const initialWidthsTotal = Array.isArray(payload?.settings?.width) ? payload.settings.width.length : 1;
59
+
60
+ const snapshotsBar = multiBar.create(
61
+ Math.max(1, initialSnapshotsTotal),
62
+ 0,
63
+ {name: getInitialSnapshotName(payload)}
64
+ );
65
+
66
+ const widthsBar = multiBar.create(
67
+ Math.max(1, initialWidthsTotal),
68
+ 0,
69
+ {name: getInitialWidthName(payload)}
70
+ );
71
+
72
+ let jobId = null;
73
+
74
+ try {
75
+ const res = await submitRun(ppKey, payload);
76
+ jobId = res?.jobId || res?.id || res?.job_id || res?.data?.jobId || null;
77
+ if (!jobId) {
78
+ throw new Error('Server did not return a job id');
79
+ }
80
+
81
+ const finalStatus = await pollRun(ppKey, jobId, {
82
+ intervalMs: Number(process.env.PP_POLL_INTERVAL_MS || 1000),
83
+ onStatus: (status) => {
84
+ const p = getProgress(status);
85
+
86
+ const snapshotLabel = p.snapshotName || getInitialSnapshotName(payload);
87
+ const widthLabel = normalizePxLabel(p.widthLabel) || getInitialWidthName(payload);
88
+
89
+ const snapTotal = p.snapshotsTotal > 0 ? p.snapshotsTotal : Math.max(1, initialSnapshotsTotal);
90
+ const snapDone = p.snapshotsTotal > 0 ? Math.min(p.snapshotsDone, p.snapshotsTotal) : snapshotsBar.value;
91
+
92
+ snapshotsBar.setTotal(snapTotal);
93
+ snapshotsBar.update(snapDone, {name: snapshotLabel});
94
+
95
+ const wTotal = p.widthsTotal > 0 ? p.widthsTotal : Math.max(1, initialWidthsTotal);
96
+ const wDone = p.widthsTotal > 0 ? Math.min(p.widthsDone, p.widthsTotal) : widthsBar.value;
97
+
98
+ widthsBar.setTotal(wTotal);
99
+ widthsBar.update(wDone, {name: widthLabel});
100
+ }
101
+ });
102
+
103
+ const finalState = String(finalStatus?.state || finalStatus?.status || '').toLowerCase();
104
+ if (finalState === 'error' || finalState === 'failed' || finalState === 'canceled' || finalState === 'cancelled') {
105
+ const msg = finalStatus?.error || finalStatus?.message || 'Job failed';
106
+ throw new Error(msg);
107
+ }
108
+
109
+ return finalStatus;
110
+ } finally {
111
+ snapshotsBar.stop();
112
+ widthsBar.stop();
113
+ multiBar.stop();
114
+ }
115
+ }
116
+
117
+ module.exports = {runJob};
package/src/upload.js ADDED
@@ -0,0 +1,80 @@
1
+ const axios = require('axios');
2
+ require('dotenv').config();
3
+
4
+ const ppUrl = process.env.PP_URL || '';
5
+
6
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
7
+
8
+ function axiosErrorMessage(err) {
9
+ const status = err?.response?.status;
10
+ const statusText = err?.response?.statusText;
11
+
12
+ const data = err?.response?.data;
13
+ const body = typeof data === 'string'
14
+ ? ((data.match(/<pre>([\s\S]*?)<\/pre>/i)?.[1] || data).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim())
15
+ : '';
16
+
17
+ return [status ? `HTTP ${status}${statusText ? ` ${statusText}` : ''}` : '', body || '']
18
+ .filter(Boolean)
19
+ .join(' | ');
20
+ }
21
+
22
+ async function submitRun(ppKey, payload) {
23
+ if (!ppUrl) {
24
+ throw new Error('PP_URL not configured');
25
+ }
26
+
27
+ try {
28
+ const res = await axios.post(`${ppUrl}/runs`, payload, {
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ Authorization: `Bearer ${ppKey}`
32
+ },
33
+ maxBodyLength: Infinity
34
+ });
35
+
36
+ return res.data;
37
+ } catch (err) {
38
+ throw new Error(axiosErrorMessage(err));
39
+ }
40
+ }
41
+
42
+ async function fetchJobStatus(ppKey, jobId) {
43
+ if (!ppUrl) {
44
+ throw new Error('PP_URL not configured');
45
+ }
46
+
47
+ try {
48
+ const res = await axios.get(`${ppUrl}/runs/${encodeURIComponent(jobId)}`, {
49
+ headers: {
50
+ Authorization: `Bearer ${ppKey}`
51
+ }
52
+ });
53
+
54
+ return res.data;
55
+ } catch (err) {
56
+ throw new Error(axiosErrorMessage(err));
57
+ }
58
+ }
59
+
60
+ async function pollRun(ppKey, jobId, {intervalMs = 1000, onStatus} = {}) {
61
+ for (;;) {
62
+ const status = await fetchJobStatus(ppKey, jobId);
63
+ if (onStatus) onStatus(status);
64
+
65
+ const state = String(status?.state || status?.status || '').toLowerCase();
66
+ if (state === 'done' || state === 'finished' || state === 'success' || state === 'completed') {
67
+ return status;
68
+ }
69
+ if (state === 'error' || state === 'failed' || state === 'canceled' || state === 'cancelled') {
70
+ return status;
71
+ }
72
+
73
+ await sleep(intervalMs);
74
+ }
75
+ }
76
+
77
+ module.exports = {
78
+ submitRun,
79
+ pollRun
80
+ };
@@ -0,0 +1,72 @@
1
+ const fs = require('fs');
2
+ const yaml = require('js-yaml');
3
+ const axios = require('axios');
4
+ require('dotenv').config({ override: false });
5
+
6
+ async function validateConfig() {
7
+ const errors = [];
8
+
9
+ const ppKey = process.env.PP_KEY;
10
+ const ppUrl = process.env.PP_URL;
11
+
12
+ if (!ppKey) errors.push("🔴 Missing PP_KEY in .env");
13
+ if (!ppUrl) errors.push("🔴 Missing PP_URL in .env");
14
+
15
+ if (errors.length > 0) {
16
+ console.error("🔴 Configuration check failed:");
17
+ for (const err of errors) console.error(" " + err);
18
+ process.exit(1);
19
+ }
20
+
21
+ try {
22
+ await axios.get(`${ ppUrl }/auth/ping`, {
23
+ headers: { Authorization: `Bearer ${ ppKey }` },
24
+ timeout: 5000
25
+ });
26
+ } catch (err) {
27
+ const msg = err.response?.data?.error || err.message;
28
+ if (String(msg).includes("ECONNREFUSED") || String(msg).includes("ENOTFOUND")) {
29
+ console.error("🔴 API validation failed: Connection to server failed. Make sure PP_URL in .env is correct");
30
+ } else {
31
+ console.error("🔴 API validation failed:", msg);
32
+ }
33
+ process.exit(1);
34
+ }
35
+
36
+ const config = yaml.load(fs.readFileSync('snapshots.yml', 'utf8'));
37
+ const devicesList = yaml.load(fs.readFileSync('devices.yml', 'utf8'));
38
+
39
+ const devicesConfig = {};
40
+ for (const d of devicesList) devicesConfig[d.name] = d;
41
+
42
+ const seenNames = new Set();
43
+ const configErrors = [];
44
+
45
+ for (const entry of config) {
46
+ if (seenNames.has(entry.name)) {
47
+ configErrors.push(`🔴 Duplicate snapshot name "${ entry.name }" found in snapshots.yml`);
48
+ } else {
49
+ seenNames.add(entry.name);
50
+ }
51
+
52
+ if (entry.devices && entry.devices.length > 0) {
53
+ for (const d of entry.devices) {
54
+ if (!devicesConfig[d]) {
55
+ configErrors.push(`🔴 Snapshot "${ entry.name }": Device "${ d }" not found in devices.yml`);
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ if (configErrors.length > 0) {
62
+ console.error("🔴 Configuration check failed:");
63
+ for (const err of configErrors) console.error(" " + err);
64
+ process.exit(1);
65
+ } else {
66
+ console.log("🟢 Configuration check passed.");
67
+ }
68
+
69
+ return { config, devicesConfig };
70
+ }
71
+
72
+ module.exports = validateConfig;