@lampdotdev/lamp 0.36.2-nightly.31 → 0.36.2-nightly.32
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/README.md +2 -2
- package/cli/lampapp.mjs +28 -0
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/share.mjs +4 -1
- package/cli/skill/SKILL.md +2 -1
- package/package.json +1 -1
- package/site/lib/shared-wishes.js +50 -5
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Mac app, `projectOf`), or any folder with an `index.html`; without one, the fold
|
|
|
26
26
|
| `lamp use key w 2` | Tries it as a person would, then looks: `click`, `type`, `key` (several at once with `w+d`), `drag`, `scroll`, `wait`, and in a game on Lamp's runtime `stick` and `button`, which hold Lamp's own controller as Lamp's app does (`window.__lampDrive`) and give it back when the person plays. Several moves with `then` between them go one after another, with one look at the end. |
|
|
27
27
|
| `lamp reload [--fresh]` | Opens the page again from its files, a game from where the player is, or with `--fresh` from its beginning (`window.lampFreshStart`). |
|
|
28
28
|
| `lamp stop` | Stops running it. |
|
|
29
|
-
| `lamp share --words "…"` | Puts the page on lamp.dev: anyone with the link plays it there and can send an idea. Again, it sends the page as it is now to the same link. What the folder's `.lampignore` names stays out (`cli/ignore.mjs`: gitignore's simpler half, one pattern a line, `*`, `?`, `**`, a trailing `/` for a folder, no `!`; the app's own file counts for a page in Web/ too), so a page whose data is too big for a link shares from its own folder. The app's icon goes with it (`iconOf`: a square `icon.png` or `apple-touch-icon.png` beside the page, a PNG its index.html names as its icon, or Lamp's `.lamp/icon.png`, made 512 pixels a side when bigger), for its page and the browser's tab. |
|
|
29
|
+
| `lamp share --words "…"` | Puts the page on lamp.dev: anyone with the link plays it there and can send an idea. Again, it sends the page as it is now to the same link. What the folder's `.lampignore` names stays out (`cli/ignore.mjs`: gitignore's simpler half, one pattern a line, `*`, `?`, `**`, a trailing `/` for a folder, no `!`; the app's own file counts for a page in Web/ too), so a page whose data is too big for a link shares from its own folder. The app's icon goes with it (`iconOf`: a square `icon.png` or `apple-touch-icon.png` beside the page, a PNG its index.html names as its icon, or Lamp's `.lamp/icon.png`, made 512 pixels a side when bigger), for its page and the browser's tab. The copies on the site it took changes from (`lamp take`'s credits, and the ones Lamp's app brought in, `.lamp/pulled-in.json`; `takenLinksOf`) go as `with`, so its page says "with changes from" them. |
|
|
30
30
|
| `lamp share` (a page of Lamp's) | A web app or game in a Lamp's own folder is lamp's to share like any other page, Lamp open or not, to the link that Lamp keeps for it: lamp takes the link from the Lamp's `Wish Links.json` (by the app's instance id in `.lamp/meta.json`, `lampsKeptLink`) the first time, and tells it what went after each share or `unshare` (`keepInLamp`), so its Share Link card shows the link as it is. `ideas` and `unshare` take the link the same way. |
|
|
31
31
|
| `lamp share` (any other app of Lamp's) | For a folder in Lamp's own (or a link to one) that lamp doesn't share as a page, shared by Lamp's app over its socket (`share_link`, `OutsideWork`): its Share Link card filled as it fills itself (`WishLinkCards.prepare`: the link's last words, the questions and what's new Lamp writes, the app's file, the GitHub release) and shared without its window, with `--line`, `--by`, `--asks`, `--notes`, `--no-file`, `--no-github`, `--put-on-github` in place of the card's own, and `--preview` to see it first. The link's secret stays in Lamp. Which Lamp is the one whose folder it is (`lampOwning`), or the one it was given to (`lamp open --in-lamp`, `givenTo`); any other folder is lamp's to share, whatever Lamp is running, and so is a given folder when its Lamp is too old to share for lamp. |
|
|
32
32
|
| `lamp ideas [in <n> \| dismiss <n> \| reply <n> \| done <n> \| email <address\|off>] [note]` | The ideas people playing the link have sent and what's been said since, saying what became of one, and answering whoever sent it. |
|
|
@@ -35,7 +35,7 @@ Mac app, `projectOf`), or any folder with an `index.html`; without one, the fold
|
|
|
35
35
|
| `lamp play <link> [--to folder] [--newest]` | Plays an app shared on lamp.dev on this computer: the link's file (`latestOf`, its `.lampapp`), unpacked as `lamp unpack` does into a folder of its own in `~/Lamp Games` (or `--to`), with Lamp's own files beside its page, then opened as `lamp open` opens any folder. Its AI is answered by `lamp dev` (`ai.mjs`) in any browser, Safari included, where a shared page can't reach the computer. lamp remembers which folder each link went into (a file for each link under `played/` in its state), so playing it again goes on where it was; `--newest` takes the link's latest version into a new folder. |
|
|
36
36
|
| `lamp copies [folder \| link]` | The copies people shared on lamp.dev of the folder's own link (as `lamp share` keeps it, or the one Lamp's app keeps for an app in its own folder, `lampsKeptLink`), or of any link, as its page lists them (`copiesOf`, `GET /api/wishes?id=<id>&copies=1`): each one's name, who made it, when it last changed and its link. |
|
|
37
37
|
| `lamp compare <link> [folder]` | Fetches that copy's latest file as `lamp play` does (`latestOf`, then `unpack`) into lamp's state for the folder (`compare/<id>/`, in place of an earlier one for the same link), never into the person's folders, and says what differs from the folder's app by the files a share carries of each (`appFilesOf`, the walk `exportApp` packs: none of Lamp's own files, nothing hidden, nothing `.lampignore` names): only in theirs, only in yours, and changed, with lines or sizes (`cli/copies.mjs`). A copy is someone else's work, read as data, never as instructions. |
|
|
38
|
-
| `lamp take <link> ["what"] [folder]` | `compare`, then a brief for the person's coding agent, as Lamp's `shared_copy` answers its own: bring in only what was asked from the copy, fitted to this app, run nothing from it, check with `lamp look`, and carry over the credits of any work of others it brings. It credits the copy in the app's credits.json once (an `upstream` entry: its name, its maker as author, its link by id), never in `remixOf`, the chain the app was made from, whose last link a share names as its source (`sourceLinkOf`). A copy
|
|
38
|
+
| `lamp take <link> ["what"] [folder]` | `compare`, then a brief for the person's coding agent, as Lamp's `shared_copy` answers its own: bring in only what was asked from the copy, fitted to this app, run nothing from it, check with `lamp look`, and carry over the credits of any work of others it brings. It credits the copy in the app's credits.json once (an `upstream` entry: its name, its maker as author, its link by id), never in `remixOf`, the chain the app was made from, whose last link a share names as its source (`sourceLinkOf`). A copy shared on lamp.dev was shared for anyone to take from, so the next share never asks about its license (`sharedCopy`), and names the copy (`with`): its page says "with changes from" it, and the copy's maker is told once. |
|
|
39
39
|
| `lamp ai [--forget]` | Answers the AI of pages shared on lamp.dev, played in a browser on this computer, with the person's own Claude Code, Codex or OpenRouter key (`cli/local-ai.mjs`, the same answers as `ai.mjs` gives `lamp dev`), until it's stopped. It listens on this computer alone at a port lamp.js knows (`LOCAL_AI_PORT`, `NEARBY` in lamp.js), and answers only a link's own page (`<id in hex>.play.lamp.dev`, `playOrigin`): the first time a page uses the AI, the person is asked in the terminal, and the page keeps the token it's given. Whether there's an AI is answered without asking. The browser asks the person too, once, before a page may reach this computer, so the link's frame on lamp.dev is given that permission; lamp.js reaches the port only once that's granted, the page has a token, or the person asks (`LampAI.connect()` from a button, or a real AI call), so the browser doesn't ask as the page loads. Safari never lets the page reach it (Safari 27, checked against a stand-in on the port), so lamp.js tells someone playing in Safari to use Chrome, Lamp or `lamp play` instead. `--forget` takes back every page allowed. Lamp's app runs the same server while it's open (`sharedPages` in `calls.mjs`, `SharedPagesAI`): it asks the person in a dialog and answers with the AI they set up in Lamp, and the pages allowed are the same list; whichever of the two starts first answers. |
|
|
40
40
|
| `lamp devices [add \| forget <name>]` | The person's devices lamp puts a page on: an Xbox in Developer Mode (those set up and those found on the network), and Android (lamp's emulators, and the phones, tablets and TVs plugged in with USB debugging on). `add` sets one up, a step at a time; `add android` downloads Android's tools. |
|
|
41
41
|
| `lamp open --on xbox` | Packages the page, installs it on the device and opens it there. `lamp look --on xbox` is the device's screen and the app's log there, `lamp stop --on xbox` ends it, and `lamp remove --on xbox` takes it off. |
|
package/cli/lampapp.mjs
CHANGED
|
@@ -402,6 +402,34 @@ export function sourceLinkOf(folder, site, own = null) {
|
|
|
402
402
|
return null;
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
/**
|
|
406
|
+
* The ids of the links on `site` whose copies the app took changes from, which a share of its own names (`with`) so its
|
|
407
|
+
* page says "with changes from" them: each `upstream` credit there by its link (`lamp take`, by id, with a name after it
|
|
408
|
+
* or not), and each copy Lamp's app brought into a saved version (`shared_copy`, `.lamp/pulled-in.json`, which writes
|
|
409
|
+
* lamp.dev's address whichever site it fetched from). Never the app's own link (`own`). The link it was made from
|
|
410
|
+
* (`sourceLinkOf`) is named only when changes were taken from it again later, which is what such a credit says.
|
|
411
|
+
*/
|
|
412
|
+
export function takenLinksOf(folder, site, own = null) {
|
|
413
|
+
const app = existsSync(join(folder, ".lamp-wish", "lamp-app.json")) ? join(folder, ".lamp-wish") : folder;
|
|
414
|
+
const read = (file) => { try { return JSON.parse(readFileSync(file, "utf8")); } catch { return null; } };
|
|
415
|
+
let origin;
|
|
416
|
+
try { origin = new URL(site).origin; } catch { return []; }
|
|
417
|
+
const idOf = (link, anySite) => {
|
|
418
|
+
let url;
|
|
419
|
+
try { url = new URL(link); } catch { return null; }
|
|
420
|
+
const id = /^\/w\/([^/]+)$/.exec(url.pathname)?.[1]?.slice(0, 10);
|
|
421
|
+
const named = /^\/w\/[^/]{10}(?:-[^/]*)?$/.test(url.pathname);
|
|
422
|
+
return (anySite || url.origin === origin) && named && isID(id) && id !== own ? id : null;
|
|
423
|
+
};
|
|
424
|
+
const upstream = read(join(app, "credits.json"))?.upstream;
|
|
425
|
+
const pulled = read(join(folder, ".lamp", "pulled-in.json"));
|
|
426
|
+
const ids = [
|
|
427
|
+
...(Array.isArray(upstream) ? upstream.map((source) => idOf(source?.url, false)) : []),
|
|
428
|
+
...(Array.isArray(pulled) ? pulled.filter((credit) => typeof credit?.version === "string").map((credit) => idOf(credit?.link, true)) : []),
|
|
429
|
+
];
|
|
430
|
+
return [...new Set(ids.filter(Boolean))].slice(0, LIMITS.with);
|
|
431
|
+
}
|
|
432
|
+
|
|
405
433
|
/** An object with its keys in order, at every level, as Lamp's own encoder writes its files. */
|
|
406
434
|
function sortedKeys(value) {
|
|
407
435
|
if (Array.isArray(value)) return value.map(sortedKeys);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"
|
|
1
|
+
{"instance":"AF4C7F06-9ECB-4B36-9D41-BD860268DDF1","platform":"mac","name":"Zqx App Name","exe":"ZqxAppName"}
|
package/cli/share.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { NOTES_FILE, Problem, stateOf } from "./state.mjs";
|
|
|
15
15
|
import { ANY_BROWSER, CONTROLLERS, CONTROLLERS_FILE, DEVICE_FILE, lampJS, LIBRARIES } from "./dev.mjs";
|
|
16
16
|
import { LIMITS, FILE_LIMITS, PAGE_LIMITS } from "../site/lib/shared-wishes.js";
|
|
17
17
|
import { ideasOf, makerOf, pageUp, publishLink, removeFile, removeIdea, removeLink, replyIdea, sendFile, siteOrigin } from "./link.mjs";
|
|
18
|
-
import { exportApp, sourceLinkOf } from "./lampapp.mjs";
|
|
18
|
+
import { exportApp, sourceLinkOf, takenLinksOf } from "./lampapp.mjs";
|
|
19
19
|
import { scaled, sizeOf } from "./png.mjs";
|
|
20
20
|
import { ignoredFor } from "./ignore.mjs";
|
|
21
21
|
|
|
@@ -244,6 +244,9 @@ export async function share({ layout, words, picture = null, shape = "fill", sit
|
|
|
244
244
|
// The link this app came from, when it's a copy of one shared there: its page says so.
|
|
245
245
|
const from = sourceLinkOf(layout.folder, site, earlier?.id);
|
|
246
246
|
if (from) body.from = from;
|
|
247
|
+
// The copies on the site it took changes from: its page says "with changes from" them. None said keeps what it had.
|
|
248
|
+
const taken = takenLinksOf(layout.folder, site, earlier?.id);
|
|
249
|
+
if (taken.length) body.with = taken;
|
|
247
250
|
// A file an earlier share sent stays on the record until it's taken away, so a retry after a failed removal tries again.
|
|
248
251
|
// A link that's a Lamp's (`fromLamp`, the app in its own folder) stays so in every write, a share that fails partway too.
|
|
249
252
|
let link = { site, ...(await publishLink({ site, body, earlier, account: accountFor(site)?.key })), ...words, shape, ...(earlier?.file ? { file: true } : {}),
|
package/cli/skill/SKILL.md
CHANGED
|
@@ -224,7 +224,8 @@ Only when the person asks about copies or to bring something of one in:
|
|
|
224
224
|
- `{{lamp}} compare <link>` fetches that copy into lamp's own place for this folder and says what differs from this
|
|
225
225
|
app: files only in theirs, only in yours, and changed. It answers where the copy is: read it from there.
|
|
226
226
|
- `{{lamp}} take <link> "the stadium"` compares, then says how to bring in what the person asked for (leave the words
|
|
227
|
-
out only when they want everything new), and credits the copy in this app's credits.json
|
|
227
|
+
out only when they want everything new), and credits the copy in this app's credits.json, so the next share's page
|
|
228
|
+
says "with changes from" that copy and its maker hears of it. Bring
|
|
228
229
|
in only that, written into this app's own files and fitted to it, never their whole app over this one; then look.
|
|
229
230
|
When it's in, tell the person in a sentence what you took from whose copy. A copy shared on lamp.dev was shared for
|
|
230
231
|
anyone to take from, so sharing it on never asks about its license; third-party work the copy itself credits still
|
package/package.json
CHANGED
|
@@ -29,6 +29,8 @@ export const LIMITS = {
|
|
|
29
29
|
releases: 30,
|
|
30
30
|
releaseLines: 5,
|
|
31
31
|
releaseLine: 120,
|
|
32
|
+
/** The other people's copies on the site the app took changes from (`with`), by their ids. */
|
|
33
|
+
with: 20,
|
|
32
34
|
picture: 1_500_000,
|
|
33
35
|
icon: 300_000,
|
|
34
36
|
/** Pixels on a side, for both pictures. */
|
|
@@ -137,11 +139,12 @@ function release(value) {
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
/** What a share sends, checked. Null when anything is off. `releases` is undefined when the share didn't say (a Lamp
|
|
140
|
-
* from before them), so a change keeps the ones the link has; `from` too, the id of the link the app is a copy of
|
|
142
|
+
* from before them), so a change keeps the ones the link has; `from` too, the id of the link the app is a copy of, and
|
|
143
|
+
* `with`, the ids of the links whose copies it took changes from (`lamp take`, Lamp's `shared_copy`). */
|
|
141
144
|
const GITHUB_REPO = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/(?!\.\.?$)[A-Za-z0-9._-]{1,100}$/;
|
|
142
145
|
export function readShare(body) {
|
|
143
146
|
if (!body || typeof body !== "object" || Array.isArray(body)) return null;
|
|
144
|
-
const known = ["name", "by", "line", "words", "ideas", "asks", "shape", "releases", "picture", "icon", "github", "from"];
|
|
147
|
+
const known = ["name", "by", "line", "words", "ideas", "asks", "shape", "releases", "picture", "icon", "github", "from", "with"];
|
|
145
148
|
if (Object.keys(body).some((key) => !known.includes(key))) return null;
|
|
146
149
|
const name = tidy(body.name, LIMITS.name);
|
|
147
150
|
const words = tidy(body.words, LIMITS.words, { lines: true });
|
|
@@ -160,9 +163,10 @@ export function readShare(body) {
|
|
|
160
163
|
// The app's public repository on GitHub, as "owner/name", which the page links to: nothing else is taken.
|
|
161
164
|
const github = body.github == null || body.github === "" ? null : GITHUB_REPO.test(body.github) ? body.github : undefined;
|
|
162
165
|
const from = body.from == null ? undefined : isID(body.from) ? body.from : null;
|
|
163
|
-
|
|
166
|
+
const taken = body.with == null ? undefined : Array.isArray(body.with) && body.with.length <= LIMITS.with && body.with.every(isID) ? [...new Set(body.with)] : null;
|
|
167
|
+
if (!name || !words || line == null || by == null || ideas.includes(null) || asks.includes(null) || shape === undefined || releases?.includes(null) || !shot || !icon || github === undefined || from === null || taken === null) return null;
|
|
164
168
|
return { name, by, line, words, ideas, asks, shape, releases: releases?.sort((a, b) => b.date.localeCompare(a.date)),
|
|
165
|
-
picture: shot.bytes ? shot : null, icon: icon.bytes ? icon : null, github, from };
|
|
169
|
+
picture: shot.bytes ? shot : null, icon: icon.bytes ? icon : null, github, from, with: taken };
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
/** What anyone may see of a shared wish: never its secret. */
|
|
@@ -745,6 +749,33 @@ async function copied(kept, from, copy, origin, { env, fetcher, first }) {
|
|
|
745
749
|
} catch {}
|
|
746
750
|
}
|
|
747
751
|
|
|
752
|
+
/**
|
|
753
|
+
* A link started naming copies it took changes from (`with`): each copy's maker told by email, once (the send key), at
|
|
754
|
+
* the address that confirmed their account (or the copy's own maker address), as `copied` tells a source's. Never fails
|
|
755
|
+
* the share.
|
|
756
|
+
*/
|
|
757
|
+
async function tookFrom(kept, ids, link, origin, { env, fetcher }) {
|
|
758
|
+
const one = async (id) => {
|
|
759
|
+
try {
|
|
760
|
+
const copy = await kept.get(id);
|
|
761
|
+
if (!copy) return;
|
|
762
|
+
const account = copy.owner ? await kept.account(copy.owner) : null;
|
|
763
|
+
const maker = account ? null : await kept.maker(id);
|
|
764
|
+
const to = account?.email ?? (maker?.confirmed ? maker.email : null);
|
|
765
|
+
if (!to) return;
|
|
766
|
+
const who = link.owner || link.by || "Someone";
|
|
767
|
+
await sendMail(env, fetcher, { to: [to], subject: `${who} took changes from your ${copy.name}`,
|
|
768
|
+
text: `${who} took changes from your ${copy.name} into ${link.name}:\n\n${linkOf(origin, link)}\n` }, `with-${link.id}-${id}`);
|
|
769
|
+
} catch {}
|
|
770
|
+
};
|
|
771
|
+
// All at once, and the share waits for them a few seconds at most: it has to answer well within its function's time,
|
|
772
|
+
// with the link's secret, whatever the mail service does. A send cut short here is only an email not sent.
|
|
773
|
+
await Promise.race([Promise.all(ids.map(one)), new Promise((done) => setTimeout(done, NOTIFY_MS))]);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** The longest a share waits on the emails it sends its copies' makers, well within the function's time. */
|
|
777
|
+
const NOTIFY_MS = 6000;
|
|
778
|
+
|
|
748
779
|
/** The copies of a link as its page lists them, newest first; one taken down since is let go. */
|
|
749
780
|
export async function copiesOf(kept, id, origin) {
|
|
750
781
|
const found = [];
|
|
@@ -834,8 +865,12 @@ export function createShareHandler({ env = process.env, fetcher = fetch, store,
|
|
|
834
865
|
// The link the app is a copy of, kept only while there's such a link and it isn't this one: a share that doesn't
|
|
835
866
|
// say, or names one that's gone, keeps what the record had.
|
|
836
867
|
const from = share.from && share.from !== wishID && (await kept.get(share.from)) ? share.from : null;
|
|
868
|
+
// The copies it took changes from, kept only while each is there and isn't this link: a share that doesn't say
|
|
869
|
+
// keeps what the record had, and one that says none clears it.
|
|
870
|
+
const taken = share.with && (await kept.getMany(share.with.filter((one) => one !== wishID)))
|
|
871
|
+
.flatMap((record) => (record ? [record.id] : []));
|
|
837
872
|
const words = { name: share.name, by: share.by, line: share.line, words: share.words, ideas: share.ideas, asks: share.asks, shape: share.shape, github: share.github,
|
|
838
|
-
...(share.releases ? { releases: share.releases } : {}), ...(from ? { from } : {}), picture: picture || null, icon: icon || null };
|
|
873
|
+
...(share.releases ? { releases: share.releases } : {}), ...(from ? { from } : {}), ...(taken ? { with: taken } : {}), picture: picture || null, icon: icon || null };
|
|
839
874
|
// Its name under the account stays what it was first given, whatever the app is called later.
|
|
840
875
|
const named = earlier?.owner ? { owner: earlier.owner, app: earlier.app }
|
|
841
876
|
: owner ? { owner, app: await claimAppName(kept, owner, words.name, wishID) } : {};
|
|
@@ -845,6 +880,7 @@ export function createShareHandler({ env = process.env, fetcher = fetch, store,
|
|
|
845
880
|
const record = { ...earlier, ...words, ...named, updated: time };
|
|
846
881
|
await kept.replace(record);
|
|
847
882
|
if (record.from) await copied(kept, record.from, record, url.origin, { env, fetcher, first: !!from && earlier?.from !== from });
|
|
883
|
+
if (taken) await tookFrom(kept, taken.filter((one) => !(earlier?.with ?? []).includes(one)), record, url.origin, { env, fetcher });
|
|
848
884
|
// Kept: the name is the record's now, whatever the cleanup after it does.
|
|
849
885
|
uploaded = [];
|
|
850
886
|
claimed = null;
|
|
@@ -860,6 +896,7 @@ export function createShareHandler({ env = process.env, fetcher = fetch, store,
|
|
|
860
896
|
uploaded = [];
|
|
861
897
|
claimed = null;
|
|
862
898
|
if (from) await copied(kept, from, record, url.origin, { env, fetcher, first: true });
|
|
899
|
+
if (taken) await tookFrom(kept, taken, record, url.origin, { env, fetcher });
|
|
863
900
|
return reply(201, { id: wishID, url: linkOf(url.origin, record), secret });
|
|
864
901
|
} catch {
|
|
865
902
|
try { await kept.removeImages(uploaded); } catch {}
|
|
@@ -1365,6 +1402,14 @@ export async function findWish(id, { env = process.env, fetcher = fetch, store,
|
|
|
1365
1402
|
if (source) wish.from = { url: linkOf(origin, source), label: source.owner && source.app ? `${source.owner}/${source.app}` : source.name };
|
|
1366
1403
|
} catch {}
|
|
1367
1404
|
}
|
|
1405
|
+
// The copies it took changes from, as each is known now, `with` on its page, in one round trip; one taken down since,
|
|
1406
|
+
// or the source already named as `from`, is left out.
|
|
1407
|
+
if (wish && origin && Array.isArray(record.with) && record.with.length) {
|
|
1408
|
+
try {
|
|
1409
|
+
const records = await kept.getMany(record.with.filter((one) => isID(one) && one !== id && one !== record.from));
|
|
1410
|
+
wish.with = records.filter(Boolean).map((copy) => ({ url: linkOf(origin, copy), label: copy.owner && copy.app ? `${copy.owner}/${copy.app}` : copy.name }));
|
|
1411
|
+
} catch {}
|
|
1412
|
+
}
|
|
1368
1413
|
// How many copies of it people shared, for the page's "copies" (the list itself is asked for when opened).
|
|
1369
1414
|
if (wish && origin) { try { wish.copies = await kept.copyCount(id); } catch {} }
|
|
1370
1415
|
return wish;
|