scenri 0.9.2 → 0.9.4
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/CHANGELOG.md +73 -0
- package/README.md +3 -1
- package/dist/{chunk-SDVL5OJF.js → chunk-3BGDI4HI.js} +54 -16
- package/dist/{chunk-OJAG3FRX.js → chunk-3ZRMKVBB.js} +108 -12
- package/dist/{cli-FYWM4HBN.js → cli-MQO66C5O.js} +15 -7
- package/dist/index.js +2 -2
- package/dist/install-VXJPLYWM.js +4 -0
- package/dist/offer-W6NJXMWT.js +71 -0
- package/dist/{refresh-FKQABJOC.js → refresh-5HBDLLLB.js} +3 -3
- package/dist/serve.js +2707 -625
- package/dist/{src-CSR34EBZ.js → src-3ITTV2UE.js} +3 -3
- package/package.json +1 -1
- package/studio-dist/assets/{index-DfAHZRYM.css → index-BGts89lc.css} +1 -1
- package/studio-dist/assets/index-uiX1mW43.js +104 -0
- package/studio-dist/index.html +2 -2
- package/dist/install-4W6AARK6.js +0 -4
- package/dist/offer-IVTNHIDJ.js +0 -47
- package/studio-dist/assets/index-CqJgKSNM.js +0 -104
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.4](https://github.com/tonygorb/Scenri/compare/v0.9.3...v0.9.4) (2026-09-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* a rate-limited store is not an empty one ([1eef6eb](https://github.com/tonygorb/Scenri/commit/1eef6eb7471e1d2de97ccedcafba7962a5d393ad))
|
|
9
|
+
* **catalog:** a rate-limited store is not an empty one ([2e7f45a](https://github.com/tonygorb/Scenri/commit/2e7f45a07adcb202722fd07972d4e5d63bbda944))
|
|
10
|
+
* **catalog:** keep an AVIF picture, and say why a page was refused ([7e08137](https://github.com/tonygorb/Scenri/commit/7e08137b543d6b491e91cc6c7f4de9aa67be4b9e))
|
|
11
|
+
* **catalog:** keep the enum out of the error list, and cut what nothing produces ([1201832](https://github.com/tonygorb/Scenri/commit/1201832ac7df892506647224e790dbbaea080092))
|
|
12
|
+
* **catalog:** stop waiting out a store that has made up its mind ([8f6751a](https://github.com/tonygorb/Scenri/commit/8f6751a6462b6e44b06b08a6b2dc2aa12ad456d1))
|
|
13
|
+
|
|
14
|
+
## [0.9.3](https://github.com/tonygorb/Scenri/compare/v0.9.2...v0.9.3) (2026-09-14)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **catalog:** look for a shop without reading the whole catalog ([65774cd](https://github.com/tonygorb/Scenri/commit/65774cdcc7e45dc5468b564bfb7596d2fcbb1500))
|
|
20
|
+
* **catalog:** make a whole-catalogue import something a machine can hold ([9c44842](https://github.com/tonygorb/Scenri/commit/9c44842e2f421671543a418cb2a884b12e02f26e))
|
|
21
|
+
* **cli:** scan a site for a shop, and import only what was chosen ([3f02cc5](https://github.com/tonygorb/Scenri/commit/3f02cc592ec025ba920de6e63683977113e5854f))
|
|
22
|
+
* **composer:** the insert menu pages, and stops moving under the caret ([d3fa697](https://github.com/tonygorb/Scenri/commit/d3fa69744e02cd69ae6ab708b095ef7c45933f48))
|
|
23
|
+
* one website, a brand and its products ([ced5f4c](https://github.com/tonygorb/Scenri/commit/ced5f4cccbb5aedc500332b166d4e42c95d344ac))
|
|
24
|
+
* **studio:** reach every product, take them all, and watch it happen ([3f19576](https://github.com/tonygorb/Scenri/commit/3f195767d59204c68fdd8c0870f2faf2349585a5))
|
|
25
|
+
* **studio:** say what a website had on it, products included ([45665e7](https://github.com/tonygorb/Scenri/commit/45665e70c10a8f307bf57ee22f4a22ee936d6870))
|
|
26
|
+
* the composer insert menu pages, and stops moving under the caret ([1c36667](https://github.com/tonygorb/Scenri/commit/1c3666772b46552b4c4a76123f4a00fd7b651c69))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* **brand:** a website is a brand source, and "Invalid URL" is not a sentence ([cde0841](https://github.com/tonygorb/Scenri/commit/cde0841bcd79c49f8d0d3ed29da4c3429718f284))
|
|
32
|
+
* **brand:** find the logo and the colours a site actually has, safely ([a6e7d3b](https://github.com/tonygorb/Scenri/commit/a6e7d3be87196fd0386448c91f6dca712dfc0b94))
|
|
33
|
+
* **brand:** never crown a logo the evidence does not support ([07b427d](https://github.com/tonygorb/Scenri/commit/07b427d7e7b4acd4d4c03ac3f23ed4722f297b61))
|
|
34
|
+
* **brand:** pick the wordmark out of the nav, not a photo two elements later ([55cb444](https://github.com/tonygorb/Scenri/commit/55cb444b1c00abdd4ec28640375dc5dad518f8f0))
|
|
35
|
+
* **brand:** read the palette a site declares, for the theme it is wearing ([323b722](https://github.com/tonygorb/Scenri/commit/323b7229c8f89c2dac9b58c60d596eb157362715))
|
|
36
|
+
* **brand:** show the kit that was just built, instead of flashing it ([5f5672f](https://github.com/tonygorb/Scenri/commit/5f5672ff811f54e83fa44442e2f7db9a40f272cb))
|
|
37
|
+
* **brand:** stop inventing products, and stop crawling sites nobody asked about ([5a70c7d](https://github.com/tonygorb/Scenri/commit/5a70c7d9cff539ae4c8cae6a8b40f4c219ee5e49))
|
|
38
|
+
* **catalog:** a finished import says finished ([640a8a1](https://github.com/tonygorb/Scenri/commit/640a8a1fa8f08d1920fe83078f054303958646cd))
|
|
39
|
+
* **catalog:** a slow catalog is not a shut one ([06c8ba4](https://github.com/tonygorb/Scenri/commit/06c8ba491a4c2e65d585c33d2543cda286c5aba9))
|
|
40
|
+
* **catalog:** four pictures at once too, and count a refusal that threw ([c773029](https://github.com/tonygorb/Scenri/commit/c77302904c933f3b4b1c220a7ff2c8d529536b5f))
|
|
41
|
+
* **catalog:** import in batches, and stop the setup screen losing the products ([4257515](https://github.com/tonygorb/Scenri/commit/4257515a46bb07594dd88657d87046daa665a850))
|
|
42
|
+
* **catalog:** read a shop at a rate it will tolerate, and say when it stops ([98c2e53](https://github.com/tonygorb/Scenri/commit/98c2e53d573d037ba4f1f72e5212b97083f5afab))
|
|
43
|
+
* **catalog:** read the product pages a blocked storefront API still serves ([21ab82b](https://github.com/tonygorb/Scenri/commit/21ab82b0427f0cc30a6884637663439981bd1c29))
|
|
44
|
+
* **catalog:** stop reading the same catalog once per language ([1a870aa](https://github.com/tonygorb/Scenri/commit/1a870aabd1fd5b627d36e253787d6ef08b33337f))
|
|
45
|
+
* **catalog:** stop the crawler being pointed anywhere a caller likes ([d4a0204](https://github.com/tonygorb/Scenri/commit/d4a0204892b3cb7c6f94e03153505a9bad8d97e4))
|
|
46
|
+
* **catalog:** two things eight real websites found ([1c938f6](https://github.com/tonygorb/Scenri/commit/1c938f6babc0da62b55fa1fce6d04de069e04ae4))
|
|
47
|
+
* **codex:** ready now means a real codex run authenticated ([33cf286](https://github.com/tonygorb/Scenri/commit/33cf28653d63cdfeab267db9557ae0b696197bd0))
|
|
48
|
+
* **composer:** type the insert menu's page state as a number ([86ec924](https://github.com/tonygorb/Scenri/commit/86ec924f70c56e7d96e0bc5772331883ee64ef88))
|
|
49
|
+
* **deps:** take the patched vitest line, closing three Dependabot alerts ([9ec12d0](https://github.com/tonygorb/Scenri/commit/9ec12d0989b3cb9b033a61bb00c1b3c7e891bd32))
|
|
50
|
+
* **desktop:** say why the icon failed, not which PowerShell command failed ([8108827](https://github.com/tonygorb/Scenri/commit/81088274944b8df1042e2095451552d09a33c6df))
|
|
51
|
+
* **desktop:** the icon offer can no longer stop Scenri from starting ([dacdbba](https://github.com/tonygorb/Scenri/commit/dacdbba142709d11e7fac0c57828cbdd9acf0a2d))
|
|
52
|
+
* **install:** stop sending Windows users to an administrator they do not need ([42c43ea](https://github.com/tonygorb/Scenri/commit/42c43ea36f6b9d22bc4dae0d296a0cee7634cf18))
|
|
53
|
+
* **notifications:** the Tasks tab holds work in flight, not every import ever run ([8d67c75](https://github.com/tonygorb/Scenri/commit/8d67c7598b6a217478021274d91184a5d5bb957b))
|
|
54
|
+
* **products:** a hand-made product keeps all of its pictures ([14e6120](https://github.com/tonygorb/Scenri/commit/14e6120da46ee78cabf6e86fc0f2661255037259))
|
|
55
|
+
* **studio:** a building card is the wait, not a caption ([7ddc983](https://github.com/tonygorb/Scenri/commit/7ddc98337e8d98452e408798e0ec7cc88a561b33))
|
|
56
|
+
* **studio:** keep the store's half of the library as the server sorted it ([11bb05f](https://github.com/tonygorb/Scenri/commit/11bb05fd75edf5a0c4d8efed8227f7e0357b8f2a))
|
|
57
|
+
* **studio:** show the products arriving, and stop asking for more than can land ([62ebca0](https://github.com/tonygorb/Scenri/commit/62ebca0a0cff29bf0a5e914499363d750ecd0ee0))
|
|
58
|
+
* **studio:** your own work, newest first ([4bbfda2](https://github.com/tonygorb/Scenri/commit/4bbfda2d3e17bf586d974a93cad822af2e84d09f))
|
|
59
|
+
* Windows first run, Codex readiness and website brand import ([0831502](https://github.com/tonygorb/Scenri/commit/0831502163385fb12a04919d26acf5677d761427))
|
|
60
|
+
* your own work newest first, and a quieter building card ([20c4e1c](https://github.com/tonygorb/Scenri/commit/20c4e1c1cfd629ef88e4aa512e855103ad3e502a))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Performance Improvements
|
|
64
|
+
|
|
65
|
+
* **catalog:** crawl a whole store in batches, and close jobs a crash left open ([98f1ce9](https://github.com/tonygorb/Scenri/commit/98f1ce9daaa5fa70e97ac308c075269fdcbe3254))
|
|
66
|
+
* **catalog:** import one product at a time, pictures alongside ([a86abc8](https://github.com/tonygorb/Scenri/commit/a86abc886ae644ba265cdf5c6d316dc15dbad09b))
|
|
67
|
+
* **catalog:** make a large import progressive, bounded and watchable ([fff18dc](https://github.com/tonygorb/Scenri/commit/fff18dca84156b794d9eb67a582709f55b3f94b3))
|
|
68
|
+
* **catalog:** read twelve product pages at once, and stop cutting them short ([c137cfd](https://github.com/tonygorb/Scenri/commit/c137cfd3dc319093f8484e2b719f5aaae607ee87))
|
|
69
|
+
* **products:** a waiting card holds its space, and its picture is asked for early ([5a20f91](https://github.com/tonygorb/Scenri/commit/5a20f91c960ceeecfa813d503c437965449091f7))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
### Miscellaneous Chores
|
|
73
|
+
|
|
74
|
+
* **release:** pin the next version to 0.9.3 ([1b019ba](https://github.com/tonygorb/Scenri/commit/1b019bae1ab357421287c10d1765111e843134ae))
|
|
75
|
+
|
|
3
76
|
## [0.9.2](https://github.com/tonygorb/Scenri/compare/v0.9.1...v0.9.2) (2026-09-07)
|
|
4
77
|
|
|
5
78
|
|
package/README.md
CHANGED
|
@@ -63,7 +63,9 @@ Scenri runs on [Node.js](https://nodejs.org), version 22 or newer. If you are no
|
|
|
63
63
|
npx scenri
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
That is the whole install. npm asks once whether to proceed, downloads the current release, and opens `http://127.0.0.1:4747`. Keep the terminal window open while you work; closing it stops Scenri. Tomorrow the same command opens it again, with everything where you left it.
|
|
66
|
+
That is the whole install. npm asks once whether to proceed, downloads the current release, and opens `http://127.0.0.1:4747`. Keep the terminal window open while you work; closing it stops Scenri. Tomorrow the same command opens it again, with everything where you left it. Nothing here needs an administrator.
|
|
67
|
+
|
|
68
|
+
On Windows, if PowerShell answers `npx.ps1 cannot be loaded because running scripts is disabled on this system`, that is a stock Windows setting, not a fault: type `npx.cmd scenri` instead, or use Command Prompt. Still no administrator required.
|
|
67
69
|
|
|
68
70
|
On macOS and Windows, Scenri then asks once whether to put a **Scenri icon on your desktop**. Say yes and from then on a double-click starts Scenri and opens it in your browser, no terminal needed; the icon works offline and keeps working across updates. Said no? Settings, then About, then **Add to desktop**, or `npx scenri desktop` in a terminal. Scenri stays what it is, a local server and your browser: the icon is a launcher, not an app.
|
|
69
71
|
|
|
@@ -146,12 +146,35 @@ async function writeLnk(runImpl, opts) {
|
|
|
146
146
|
// src/desktop/install.ts
|
|
147
147
|
var UNSUPPORTED = "Desktop shortcuts are not available on this system yet.";
|
|
148
148
|
var NO_DESKTOP = "Your Desktop folder could not be found.";
|
|
149
|
+
var PROBE_FAILED = "This computer did not answer where your Desktop folder is.";
|
|
149
150
|
var COLLISION = "Something else named Scenri is already on your desktop. Move or rename it, then try again.";
|
|
150
151
|
var DENIED = "macOS did not allow writing to your Desktop. Allow it under System Settings > Privacy & Security > Files and Folders, then try again.";
|
|
151
152
|
var SUPPORT_FILES = ["launch.mjs", "Scenri.icns", "scenri.ico"];
|
|
153
|
+
function failureDetail(err) {
|
|
154
|
+
const e = err;
|
|
155
|
+
const lineOf = (text) => text.split("\n").map((l) => l.trim()).filter(Boolean)[0] ?? "";
|
|
156
|
+
const stderr = typeof e.stderr === "string" ? lineOf(e.stderr) : "";
|
|
157
|
+
if (stderr) return stderr.slice(0, 200);
|
|
158
|
+
if (e.killed === true || e.code === "ETIMEDOUT") return "it did not answer in time";
|
|
159
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
160
|
+
const useful = message.split("\n").map((l) => l.trim()).filter(Boolean).find((l) => !/^Command failed:/i.test(l));
|
|
161
|
+
return (useful ?? "the command did not finish").slice(0, 200);
|
|
162
|
+
}
|
|
152
163
|
async function installDesktop(deps) {
|
|
164
|
+
try {
|
|
165
|
+
return await install(deps);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return { ok: false, reason: "failed", message: `Scenri could not add the desktop icon (${failureDetail(err)}).` };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function install(deps) {
|
|
153
171
|
if (!isSupportedPlatform(deps.platform)) return { ok: false, reason: "unsupported", message: UNSUPPORTED };
|
|
154
|
-
|
|
172
|
+
let desktop;
|
|
173
|
+
try {
|
|
174
|
+
desktop = await desktopDir(deps);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return { ok: false, reason: "probe-failed", message: `${PROBE_FAILED} (${failureDetail(err)})` };
|
|
177
|
+
}
|
|
155
178
|
if (!desktop) return { ok: false, reason: "no-desktop", message: NO_DESKTOP };
|
|
156
179
|
const darwin = deps.platform === "darwin";
|
|
157
180
|
const support = launcherDir(deps.homedir);
|
|
@@ -164,7 +187,15 @@ async function installDesktop(deps) {
|
|
|
164
187
|
}
|
|
165
188
|
const nodeMajor = runningNodeMajor();
|
|
166
189
|
const nodePath = stableExecPath(deps.execPath, deps.platform);
|
|
167
|
-
|
|
190
|
+
try {
|
|
191
|
+
writeSupportFiles({ ...deps, execPath: nodePath, nodeMajor }, support);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
reason: "failed",
|
|
196
|
+
message: `Scenri could not write its launcher files to ${support} (${failureDetail(err)}).`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
168
199
|
try {
|
|
169
200
|
if (darwin) {
|
|
170
201
|
writeMacBundle({
|
|
@@ -186,18 +217,25 @@ async function installDesktop(deps) {
|
|
|
186
217
|
const code = err.code;
|
|
187
218
|
if (darwin && (code === "EPERM" || code === "EACCES"))
|
|
188
219
|
return { ok: false, reason: "desktop-denied", message: DENIED };
|
|
189
|
-
|
|
190
|
-
|
|
220
|
+
return { ok: false, reason: "failed", message: `Scenri could not write ${artifact} (${failureDetail(err)}).` };
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
writeLauncherRecord(deps.homedir, {
|
|
224
|
+
schema: LAUNCHER_SCHEMA,
|
|
225
|
+
createdBy: deps.version,
|
|
226
|
+
home: deps.home,
|
|
227
|
+
nodePath,
|
|
228
|
+
nodeMajor,
|
|
229
|
+
env: recordedEnv(deps.env),
|
|
230
|
+
artifact: { kind, path: artifact }
|
|
231
|
+
});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
reason: "failed",
|
|
236
|
+
message: `Scenri added the icon but could not record where it points (${failureDetail(err)}).`
|
|
237
|
+
};
|
|
191
238
|
}
|
|
192
|
-
writeLauncherRecord(deps.homedir, {
|
|
193
|
-
schema: LAUNCHER_SCHEMA,
|
|
194
|
-
createdBy: deps.version,
|
|
195
|
-
home: deps.home,
|
|
196
|
-
nodePath,
|
|
197
|
-
nodeMajor,
|
|
198
|
-
env: recordedEnv(deps.env),
|
|
199
|
-
artifact: { kind, path: artifact }
|
|
200
|
-
});
|
|
201
239
|
return { ok: true, kind, path: artifact };
|
|
202
240
|
}
|
|
203
241
|
var runningNodeMajor = () => Number(process.versions.node.split(".")[0]);
|
|
@@ -250,6 +288,6 @@ async function desktopStatus(deps) {
|
|
|
250
288
|
return { supported, platform: deps.platform, installed, path, current, record };
|
|
251
289
|
}
|
|
252
290
|
|
|
253
|
-
export { MAC_SCRIPT, POWERSHELL_ARGS, UNSUPPORTED, desktopStatus, installDesktop, isOurMacBundle, macPlist, removeDesktop, runningNodeMajor, stableExecPath, writeLnk, writeMacBundle, writeSupportFiles };
|
|
254
|
-
//# sourceMappingURL=chunk-
|
|
255
|
-
//# sourceMappingURL=chunk-
|
|
291
|
+
export { MAC_SCRIPT, POWERSHELL_ARGS, UNSUPPORTED, desktopStatus, failureDetail, installDesktop, isOurMacBundle, macPlist, removeDesktop, runningNodeMajor, stableExecPath, writeLnk, writeMacBundle, writeSupportFiles };
|
|
292
|
+
//# sourceMappingURL=chunk-3BGDI4HI.js.map
|
|
293
|
+
//# sourceMappingURL=chunk-3BGDI4HI.js.map
|
|
@@ -551,23 +551,32 @@ function openDb(homeDir) {
|
|
|
551
551
|
function createImageStore(homeDir) {
|
|
552
552
|
const dir = join(homeDir, "images");
|
|
553
553
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
554
|
-
const
|
|
554
|
+
const EXTS = ["png", "jpg", "jpeg", "webp", "gif", "avif", "heif", "heic"];
|
|
555
|
+
const fileFor = (hash, ext = "png") => join(dir, `${hash}.${ext}`);
|
|
556
|
+
const resolve = (hash) => {
|
|
557
|
+
for (const ext of EXTS) {
|
|
558
|
+
const file = fileFor(hash, ext);
|
|
559
|
+
if (existsSync(file)) return file;
|
|
560
|
+
}
|
|
561
|
+
return fileFor(hash);
|
|
562
|
+
};
|
|
555
563
|
return {
|
|
556
|
-
save(buf) {
|
|
564
|
+
save(buf, ext = "png") {
|
|
565
|
+
const clean = /^[a-z0-9]{2,5}$/i.test(ext) ? ext.toLowerCase() : "png";
|
|
557
566
|
const hash = createHash("sha256").update(buf).digest("hex").slice(0, 32);
|
|
558
|
-
const file = fileFor(hash);
|
|
567
|
+
const file = fileFor(hash, clean);
|
|
559
568
|
if (!existsSync(file)) writeFileSync(file, buf);
|
|
560
569
|
return hash;
|
|
561
570
|
},
|
|
562
571
|
pathFor(hash) {
|
|
563
572
|
if (!/^[a-f0-9]{32}$/.test(hash)) throw new Error("invalid image hash");
|
|
564
|
-
return
|
|
573
|
+
return resolve(hash);
|
|
565
574
|
},
|
|
566
575
|
read(hash) {
|
|
567
576
|
return readFileSync(this.pathFor(hash));
|
|
568
577
|
},
|
|
569
578
|
has(hash) {
|
|
570
|
-
return /^[a-f0-9]{32}$/.test(hash) && existsSync(fileFor(hash));
|
|
579
|
+
return /^[a-f0-9]{32}$/.test(hash) && EXTS.some((ext) => existsSync(fileFor(hash, ext)));
|
|
571
580
|
}
|
|
572
581
|
};
|
|
573
582
|
}
|
|
@@ -1335,8 +1344,9 @@ var productById = (db, id) => {
|
|
|
1335
1344
|
const r = db.prepare("SELECT * FROM catalog_products WHERE id=?").get(id);
|
|
1336
1345
|
return r ? rowProduct(r) : null;
|
|
1337
1346
|
};
|
|
1347
|
+
var LIST_COLS = "id, source_id, brand_id, external_key, title, description_html, url, handle, vendor, product_type, tags, category, price, compare_at_price, currency, available, status, variant, material, dimensions, created_at, updated_at";
|
|
1338
1348
|
var productsFor = (db, brandId) => db.prepare(
|
|
1339
|
-
|
|
1349
|
+
`SELECT ${LIST_COLS} FROM catalog_products WHERE brand_id=? AND status!='unavailable' ORDER BY created_at DESC, rowid DESC`
|
|
1340
1350
|
).all(brandId).map(rowProduct);
|
|
1341
1351
|
var rowVariant = (r) => ({
|
|
1342
1352
|
id: r.id,
|
|
@@ -1469,14 +1479,58 @@ function jobMethods(db) {
|
|
|
1469
1479
|
const r = db.prepare("SELECT * FROM import_jobs WHERE id=?").get(id);
|
|
1470
1480
|
return r ? rowJob(r) : null;
|
|
1471
1481
|
},
|
|
1482
|
+
/**
|
|
1483
|
+
* What the notifications bell needs: work in flight, and what has just
|
|
1484
|
+
* stopped.
|
|
1485
|
+
*
|
|
1486
|
+
* `listJobs` answers with every import the brand has ever run, and the
|
|
1487
|
+
* bell rendered all of them as tasks - so one finished import sat in the
|
|
1488
|
+
* Tasks tab, and in its count, for ever, and a brand with a few imports
|
|
1489
|
+
* behind it opened onto a list of things that were over. An hour is long
|
|
1490
|
+
* enough for the panel to still be showing you what landed while you were
|
|
1491
|
+
* on another screen, and short enough that it is never a history.
|
|
1492
|
+
*/
|
|
1493
|
+
listRecentJobs(brandId) {
|
|
1494
|
+
return db.prepare(
|
|
1495
|
+
`SELECT * FROM import_jobs
|
|
1496
|
+
WHERE brand_id=?
|
|
1497
|
+
AND (finished_at IS NULL OR finished_at > datetime('now','-1 hour'))
|
|
1498
|
+
ORDER BY created_at DESC`
|
|
1499
|
+
).all(brandId).map(rowJob);
|
|
1500
|
+
},
|
|
1472
1501
|
listJobs(brandId) {
|
|
1473
1502
|
return db.prepare("SELECT * FROM import_jobs WHERE brand_id=? ORDER BY created_at DESC").all(brandId).map(rowJob);
|
|
1474
1503
|
},
|
|
1504
|
+
/**
|
|
1505
|
+
* Close every job that was still running when the process last stopped.
|
|
1506
|
+
*
|
|
1507
|
+
* A job lives in the database and its worker lives in the process, so a
|
|
1508
|
+
* quit, a crash or a restart leaves a row that says `fetching_products`
|
|
1509
|
+
* with no `finished_at` and nothing on earth still working on it. The bell
|
|
1510
|
+
* then shows a task running for ever, and no later write can correct it
|
|
1511
|
+
* because nothing is left to do the writing. Nothing is in flight at
|
|
1512
|
+
* startup by definition, so anything unfinished here was interrupted.
|
|
1513
|
+
*
|
|
1514
|
+
* Whatever it had already saved stays saved; only the row is closed.
|
|
1515
|
+
*/
|
|
1516
|
+
reconcileInterruptedJobs() {
|
|
1517
|
+
const { changes } = db.prepare(
|
|
1518
|
+
`UPDATE import_jobs
|
|
1519
|
+
SET stage='cancelled',
|
|
1520
|
+
finished_at=datetime('now'),
|
|
1521
|
+
message=CASE WHEN upserted > 0
|
|
1522
|
+
THEN 'Interrupted when Scenri stopped, after saving ' || upserted || ' products'
|
|
1523
|
+
ELSE 'Interrupted when Scenri stopped' END
|
|
1524
|
+
WHERE finished_at IS NULL`
|
|
1525
|
+
).run();
|
|
1526
|
+
return changes;
|
|
1527
|
+
},
|
|
1475
1528
|
updateJob(id, patch) {
|
|
1476
1529
|
const cur = jobById(db, id);
|
|
1477
1530
|
if (!cur) return null;
|
|
1531
|
+
if (cur.finishedAt) return cur;
|
|
1478
1532
|
const stage = patch.stage ?? cur.stage;
|
|
1479
|
-
const finished = patch.finished || stage === "completed" || stage === "partial" || stage === "failed";
|
|
1533
|
+
const finished = patch.finished || stage === "completed" || stage === "partial" || stage === "no_catalog" || stage === "cancelled" || stage === "failed";
|
|
1480
1534
|
db.prepare(
|
|
1481
1535
|
`UPDATE import_jobs SET
|
|
1482
1536
|
source_id=?, platform=?, stage=?, discovered=?, fetched=?, upserted=?,
|
|
@@ -1683,11 +1737,18 @@ function productImportMethods(db) {
|
|
|
1683
1737
|
).run(sourceId);
|
|
1684
1738
|
return r2.changes;
|
|
1685
1739
|
}
|
|
1686
|
-
|
|
1740
|
+
db.exec("CREATE TEMP TABLE IF NOT EXISTS seen_keys (k TEXT PRIMARY KEY)");
|
|
1741
|
+
db.exec("DELETE FROM seen_keys");
|
|
1742
|
+
const add = db.prepare("INSERT OR IGNORE INTO seen_keys (k) VALUES (?)");
|
|
1743
|
+
db.transaction((keys) => {
|
|
1744
|
+
for (const k of keys) add.run(k);
|
|
1745
|
+
})(seenExternalKeys);
|
|
1687
1746
|
const r = db.prepare(
|
|
1688
1747
|
`UPDATE catalog_products SET status='unavailable', updated_at=datetime('now')
|
|
1689
|
-
WHERE source_id=? AND status='active'
|
|
1690
|
-
|
|
1748
|
+
WHERE source_id=? AND status='active'
|
|
1749
|
+
AND external_key NOT IN (SELECT k FROM seen_keys)`
|
|
1750
|
+
).run(sourceId);
|
|
1751
|
+
db.exec("DELETE FROM seen_keys");
|
|
1691
1752
|
return r.changes;
|
|
1692
1753
|
}
|
|
1693
1754
|
};
|
|
@@ -1779,6 +1840,18 @@ function mutationMethods(db) {
|
|
|
1779
1840
|
}
|
|
1780
1841
|
|
|
1781
1842
|
// ../core/src/catalog/library.ts
|
|
1843
|
+
var lighten = (p) => ({
|
|
1844
|
+
id: p.id,
|
|
1845
|
+
name: p.name,
|
|
1846
|
+
origin: p.origin,
|
|
1847
|
+
category: p.category ?? null,
|
|
1848
|
+
variant: p.variant ?? null,
|
|
1849
|
+
material: p.material ?? null,
|
|
1850
|
+
dimensions: p.dimensions ?? null,
|
|
1851
|
+
status: p.status,
|
|
1852
|
+
shots: p.shots.slice(0, 1),
|
|
1853
|
+
shotCount: p.shots.length
|
|
1854
|
+
});
|
|
1782
1855
|
function libraryMethods(db) {
|
|
1783
1856
|
return {
|
|
1784
1857
|
listLibraryProducts(brandId, brandJson) {
|
|
@@ -1836,6 +1909,29 @@ function libraryMethods(db) {
|
|
|
1836
1909
|
};
|
|
1837
1910
|
});
|
|
1838
1911
|
return [...manual, ...catalog];
|
|
1912
|
+
},
|
|
1913
|
+
/**
|
|
1914
|
+
* The whole library, in the shape the studio reads.
|
|
1915
|
+
*
|
|
1916
|
+
* Newest first, so an import lands where someone is watching and nothing
|
|
1917
|
+
* already on screen moves under them. Manual products keep their place at
|
|
1918
|
+
* the head - they are the ones a person made by hand.
|
|
1919
|
+
*/
|
|
1920
|
+
listLibraryIndex(brandId, brandJson) {
|
|
1921
|
+
return this.listLibraryProducts(brandId, brandJson).map(
|
|
1922
|
+
(p) => (
|
|
1923
|
+
// Only what came from a store. A manual product's pictures are already
|
|
1924
|
+
// in the brand document this was built from, so trimming them saves
|
|
1925
|
+
// nothing - and the product page asks the server for the whole record
|
|
1926
|
+
// only for catalog products, so a hand-made product with four
|
|
1927
|
+
// references was left showing one, everywhere, for good.
|
|
1928
|
+
p.origin === "catalog" ? lighten(p) : { ...lighten(p), shots: p.shots, shotCount: p.shots.length }
|
|
1929
|
+
)
|
|
1930
|
+
);
|
|
1931
|
+
},
|
|
1932
|
+
/** One product, whole, for the page that shows all of its pictures. */
|
|
1933
|
+
libraryProduct(brandId, brandJson, productId) {
|
|
1934
|
+
return this.listLibraryProducts(brandId, brandJson).find((p) => p.id === productId) ?? null;
|
|
1839
1935
|
}
|
|
1840
1936
|
};
|
|
1841
1937
|
}
|
|
@@ -1936,5 +2032,5 @@ function createCore(homeDir = defaultHome()) {
|
|
|
1936
2032
|
}
|
|
1937
2033
|
|
|
1938
2034
|
export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug };
|
|
1939
|
-
//# sourceMappingURL=chunk-
|
|
1940
|
-
//# sourceMappingURL=chunk-
|
|
2035
|
+
//# sourceMappingURL=chunk-3ZRMKVBB.js.map
|
|
2036
|
+
//# sourceMappingURL=chunk-3ZRMKVBB.js.map
|
|
@@ -2,10 +2,10 @@ import { adoptRunningInstall } from './chunk-ECS5LRHB.js';
|
|
|
2
2
|
import { detectInstallKind } from './chunk-PAIAXRAC.js';
|
|
3
3
|
import { readMeta } from './chunk-Y3ZPBPLP.js';
|
|
4
4
|
import { defaultHome, newestStaged, listStaged, compareSemver, entryOf } from './chunk-4MAFHYAD.js';
|
|
5
|
-
import { removeDesktop, installDesktop, POWERSHELL_ARGS } from './chunk-
|
|
5
|
+
import { removeDesktop, failureDetail, installDesktop, POWERSHELL_ARGS } from './chunk-3BGDI4HI.js';
|
|
6
6
|
import { launcherDir, logsDir, powershellPath } from './chunk-2IGINCPU.js';
|
|
7
7
|
import { execFile, spawn } from 'child_process';
|
|
8
|
-
import {
|
|
8
|
+
import { closeSync, existsSync, readFileSync, writeFileSync, mkdirSync, openSync, rmSync, statSync, renameSync, appendFileSync } from 'fs';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
import { join, dirname, delimiter, win32 } from 'path';
|
|
11
11
|
import { createServer } from 'http';
|
|
@@ -419,6 +419,15 @@ async function runDesktopCommand(opts, ownEntry) {
|
|
|
419
419
|
return (await addToDesktop(ownEntry)).ok ? 0 : 1;
|
|
420
420
|
}
|
|
421
421
|
async function addToDesktop(ownEntry, say = console.log, over = {}) {
|
|
422
|
+
try {
|
|
423
|
+
return await addOrRepair(ownEntry, say, over);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
const message = `Scenri could not add the desktop icon (${failureDetail(err)}).`;
|
|
426
|
+
say(` ${message}`);
|
|
427
|
+
return { ok: false, reason: "failed", message };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function addOrRepair(ownEntry, say, over) {
|
|
422
431
|
const deps = { ...installDeps(ownEntry), ...over };
|
|
423
432
|
const pkg = over.pkg ?? readMeta().name;
|
|
424
433
|
const installKind = over.installKind ?? detectInstallKind(ownEntry, deps.home);
|
|
@@ -454,7 +463,7 @@ async function addToDesktop(ownEntry, say = console.log, over = {}) {
|
|
|
454
463
|
}
|
|
455
464
|
async function rememberDecline(home) {
|
|
456
465
|
try {
|
|
457
|
-
const { createCore } = await import('./src-
|
|
466
|
+
const { createCore } = await import('./src-3ITTV2UE.js');
|
|
458
467
|
const core = createCore(home);
|
|
459
468
|
try {
|
|
460
469
|
core.store.setSetting("desktop.prompt", "declined");
|
|
@@ -519,8 +528,7 @@ function tail(path, bytes = 4096) {
|
|
|
519
528
|
return "";
|
|
520
529
|
}
|
|
521
530
|
}
|
|
522
|
-
var supportInstalled = () => existsSync(join(launcherDir(homedir()), "launcher.json"));
|
|
523
531
|
|
|
524
|
-
export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand
|
|
525
|
-
//# sourceMappingURL=cli-
|
|
526
|
-
//# sourceMappingURL=cli-
|
|
532
|
+
export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand };
|
|
533
|
+
//# sourceMappingURL=cli-MQO66C5O.js.map
|
|
534
|
+
//# sourceMappingURL=cli-MQO66C5O.js.map
|
package/dist/index.js
CHANGED
|
@@ -86,12 +86,12 @@ try {
|
|
|
86
86
|
break;
|
|
87
87
|
}
|
|
88
88
|
case "desktop": {
|
|
89
|
-
const { runDesktopCommand } = await import('./cli-
|
|
89
|
+
const { runDesktopCommand } = await import('./cli-MQO66C5O.js');
|
|
90
90
|
process.exit(await runDesktopCommand(command, fileURLToPath(import.meta.url)));
|
|
91
91
|
break;
|
|
92
92
|
}
|
|
93
93
|
case "open": {
|
|
94
|
-
const { runOpenCommand } = await import('./cli-
|
|
94
|
+
const { runOpenCommand } = await import('./cli-MQO66C5O.js');
|
|
95
95
|
process.exit(await runOpenCommand(fileURLToPath(import.meta.url)));
|
|
96
96
|
break;
|
|
97
97
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { UNSUPPORTED, desktopStatus, failureDetail, installDesktop, removeDesktop, runningNodeMajor, stableExecPath, writeSupportFiles } from './chunk-3BGDI4HI.js';
|
|
2
|
+
import './chunk-2IGINCPU.js';
|
|
3
|
+
//# sourceMappingURL=install-VXJPLYWM.js.map
|
|
4
|
+
//# sourceMappingURL=install-VXJPLYWM.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
|
|
3
|
+
// src/desktop/offer.ts
|
|
4
|
+
function shouldOfferDesktop(i) {
|
|
5
|
+
if (i.env.SCENRI_NO_DESKTOP === "1") return false;
|
|
6
|
+
if (!i.stdinTTY || !i.stdoutTTY) return false;
|
|
7
|
+
if (i.env.CI) return false;
|
|
8
|
+
if (i.env.SSH_TTY) return false;
|
|
9
|
+
if (i.platform !== "darwin" && i.platform !== "win32") return false;
|
|
10
|
+
if (i.installKind === "dev") return false;
|
|
11
|
+
if (i.launcherInstalled) return false;
|
|
12
|
+
if (i.declined) return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
var OFFER_QUESTION = " Add Scenri to your desktop? Then you can open it without a terminal. [Y/n] ";
|
|
16
|
+
var LATER_LINE = " Not now. Add it later with: npx scenri desktop, or from Settings > About.";
|
|
17
|
+
var STILL_RUNNING_LINE = " Scenri is running anyway. Add the icon later from Settings > About.";
|
|
18
|
+
var ASK_TIMEOUT_MS = 12e4;
|
|
19
|
+
function parseOfferAnswer(raw) {
|
|
20
|
+
const answer = raw.trim().toLowerCase();
|
|
21
|
+
if (answer === "" || answer.startsWith("y")) return "yes";
|
|
22
|
+
return "no";
|
|
23
|
+
}
|
|
24
|
+
async function offerDesktop(deps) {
|
|
25
|
+
let answer;
|
|
26
|
+
try {
|
|
27
|
+
answer = await deps.ask(OFFER_QUESTION);
|
|
28
|
+
} catch {
|
|
29
|
+
deps.say("");
|
|
30
|
+
deps.say(LATER_LINE);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (parseOfferAnswer(answer) === "no") {
|
|
34
|
+
deps.decline();
|
|
35
|
+
deps.say(LATER_LINE);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const res = await deps.add();
|
|
40
|
+
if (!res.ok) deps.say(STILL_RUNNING_LINE);
|
|
41
|
+
} catch {
|
|
42
|
+
deps.say(" Could not add the desktop icon.");
|
|
43
|
+
deps.say(STILL_RUNNING_LINE);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function askOnTerminal(question, timeoutMs = ASK_TIMEOUT_MS, io = {
|
|
47
|
+
input: process.stdin,
|
|
48
|
+
output: process.stdout
|
|
49
|
+
}) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const rl = createInterface({ input: io.input, output: io.output, terminal: false });
|
|
52
|
+
let done = false;
|
|
53
|
+
const finish = (fn) => {
|
|
54
|
+
if (done) return;
|
|
55
|
+
done = true;
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
rl.close();
|
|
58
|
+
fn();
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => finish(() => reject(new Error("nobody answered"))), timeoutMs);
|
|
61
|
+
timer.unref?.();
|
|
62
|
+
rl.question(question, (answer) => finish(() => resolve(answer)));
|
|
63
|
+
rl.once("close", () => {
|
|
64
|
+
if (!done) finish(() => reject(new Error("stdin closed")));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { OFFER_QUESTION, askOnTerminal, offerDesktop, parseOfferAnswer, shouldOfferDesktop };
|
|
70
|
+
//# sourceMappingURL=offer-W6NJXMWT.js.map
|
|
71
|
+
//# sourceMappingURL=offer-W6NJXMWT.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { adoptRunningInstall } from './chunk-ECS5LRHB.js';
|
|
2
2
|
import { newestStaged, compareSemver } from './chunk-4MAFHYAD.js';
|
|
3
|
-
import { runningNodeMajor, stableExecPath, isOurMacBundle, MAC_SCRIPT, macPlist, writeSupportFiles, writeMacBundle, writeLnk } from './chunk-
|
|
3
|
+
import { runningNodeMajor, stableExecPath, isOurMacBundle, MAC_SCRIPT, macPlist, writeSupportFiles, writeMacBundle, writeLnk } from './chunk-3BGDI4HI.js';
|
|
4
4
|
import { readLauncherRecord, launcherDir, LAUNCHER_SCHEMA, writeLauncherRecord } from './chunk-2IGINCPU.js';
|
|
5
5
|
import { existsSync, readFileSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
@@ -101,5 +101,5 @@ function sameFile(a, b) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export { refreshLauncher };
|
|
104
|
-
//# sourceMappingURL=refresh-
|
|
105
|
-
//# sourceMappingURL=refresh-
|
|
104
|
+
//# sourceMappingURL=refresh-5HBDLLLB.js.map
|
|
105
|
+
//# sourceMappingURL=refresh-5HBDLLLB.js.map
|