@lampdotdev/lamp 0.36.2-nightly.23 → 0.36.2-nightly.25
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/ignore.mjs +56 -0
- package/cli/lampapp.mjs +16 -8
- package/cli/lamps/mac-starter/.lamp/meta.json +1 -1
- package/cli/share.mjs +4 -1
- package/cli/skill/SKILL.md +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Mac app, `projectOf`), or any folder with an `index.html`; without one, the fold
|
|
|
18
18
|
| `lamp new <folder> [--name "Its Name"] [--web \| --mac]` | Makes a new web game's folder from Lamp's starting files, or with `--web` a new web app's, or with `--mac` a new Mac app in Swift. |
|
|
19
19
|
| `lamp init [--update]` | Puts Lamp's game runtime beside a page that's already there, changing none of the page's own files. `--update` writes again the Lamp files the folder has (`lamp.js` too), adding the game's only to a page that's a game already. |
|
|
20
20
|
| `lamp unpack <file.lampapp> [folder]` | Unpacks a Lamp app file into a new folder beside it, named for it. A file carries none of Lamp's own files: a page gets them beside it as it's unpacked (the runtime for a game, lamp.js, the libraries its import map names), and a Mac app gets Package.swift, build.sh and LampSupport.swift from Lamp's starting files as it's first built (`writeMacStarter`), where it has none. One with a widget, a share extension or a 3D scene is left to Lamp's app, which writes the files they're built with. |
|
|
21
|
-
| `lamp export [--to file] [--anyway]` | Makes the app's Lamp app file as Lamp's Share… does (`Sharing.export`, followed by `cli/lampapp.mjs`): its own files as its kind travels, Assets/, APP.md, what it declares, its credits with the app added to the chain, the licenses they name, its icon's design and lamp-app.json, never Lamp's own files. Refused when a license its credits name is gone or something in it is shaped like a key; `--anyway` sends work its credits give no license for. The shapes of keys, the names a license goes by and the sizes another Lamp opens are in `Resources/share-rules.json`, which Lamp's app reads too (`ShareRules`). |
|
|
21
|
+
| `lamp export [--to file] [--anyway]` | Makes the app's Lamp app file as Lamp's Share… does (`Sharing.export`, followed by `cli/lampapp.mjs`): its own files as its kind travels, Assets/, APP.md, what it declares, its credits with the app added to the chain, the licenses they name, its icon's design and lamp-app.json, never Lamp's own files, nor what the folder's `.lampignore` names. Refused when a license its credits name is gone or something in it is shaped like a key; `--anyway` sends work its credits give no license for. The shapes of keys, the names a license goes by and the sizes another Lamp opens are in `Resources/share-rules.json`, which Lamp's app reads too (`ShareRules`). |
|
|
22
22
|
| `lamp dev [--open] [--lan] [--port N]` | Runs the page and says how each change went. Each save goes into every open copy of it, with the card Lamp's app puts on an app taking a change (`page.js`, left out of lamp's pictures; `lamp changing "<words>"` says on it what's changing). `--open` opens a window for it; `--lan` lets other devices on the network open it. |
|
|
23
23
|
| `lamp open` | Shows the page in a window, running it in the background if it isn't running, and returns at once. |
|
|
24
24
|
| `lamp open --in-lamp` | Gives a Mac app to Lamp's app (`cli/outside.mjs`), over the socket the person's own coding agent uses (`OutsideServer`, `OutsideWork`): `open_project` takes the folder on, with Lamp's review the first time, and from then on `look`, `reload` and `dev` call `apply_change`, so a change goes into the open app as Lamp's app puts it there (`LiveReload`) and its outcome is said: live, held, reopened, not_built, crashed. `use` calls `use_app`, in plain sight in the app Lamp's app has open, and `stop` calls `close_app`. A folder Lamp's app no longer has (its refusal says `taken_on: false`) is lamp's own again, said once. `lamp open --here` has lamp run its own copy again. `LAMP_APP` names another Lamp by its bundle id (a Lamp Dev instance). A Mac app `lamp new --mac` makes has the `lamp-app.json` Lamp's app takes a folder on by. |
|
|
@@ -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. 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. |
|
|
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. |
|
package/cli/ignore.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// `.lampignore`: what an app's folder keeps out of what it shares (its page on lamp.dev, and the app's file that goes
|
|
2
|
+
// with its link): data only its maker's computer needs, too big to share, say (a 600 MB parts library a page fetches
|
|
3
|
+
// from elsewhere when it isn't there). The patterns are gitignore's simpler half: one a line, `#` for a note, `*`, `?` and
|
|
4
|
+
// `**`, a trailing `/` for a folder, a leading `/` (or any `/` inside) for a path from the folder's top, and otherwise a
|
|
5
|
+
// name matched at any depth. Nothing starting with `!`: what's left out stays out.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join, relative, sep } from "node:path";
|
|
9
|
+
|
|
10
|
+
export const IGNORE_FILE = ".lampignore";
|
|
11
|
+
|
|
12
|
+
/** A pattern as a regular expression over a path from the folder's top ("ldraw/parts/3001.dat"). */
|
|
13
|
+
function compile(line) {
|
|
14
|
+
let pattern = line.trim();
|
|
15
|
+
const folder = pattern.endsWith("/");
|
|
16
|
+
if (folder) pattern = pattern.replace(/\/+$/, "");
|
|
17
|
+
const anchored = pattern.startsWith("/") || pattern.includes("/");
|
|
18
|
+
pattern = pattern.replace(/^\/+/, "");
|
|
19
|
+
if (!pattern) return null;
|
|
20
|
+
let source = "";
|
|
21
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
22
|
+
const c = pattern[i];
|
|
23
|
+
if (c === "*" && pattern[i + 1] === "*") {
|
|
24
|
+
// `**/` matches any folders, none included; a lone `**` anything at all.
|
|
25
|
+
if (pattern[i + 2] === "/") { source += "(?:.*/)?"; i += 2; } else { source += ".*"; i += 1; }
|
|
26
|
+
} else if (c === "*") source += "[^/]*";
|
|
27
|
+
else if (c === "?") source += "[^/]";
|
|
28
|
+
else source += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
29
|
+
}
|
|
30
|
+
// What it names is left out with everything under it; a folder pattern names folders only, so it must have more after it.
|
|
31
|
+
return new RegExp(`^${anchored ? "" : "(?:.*/)?"}${source}${folder ? "/" : "(?:/|$)"}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a path from the folder's top ("ldraw/parts/3001.dat"), a folder's when `folder` is true, is one its
|
|
36
|
+
* `.lampignore` leaves out; never, with none.
|
|
37
|
+
*/
|
|
38
|
+
export function ignoredIn(folder) {
|
|
39
|
+
let lines = [];
|
|
40
|
+
try { lines = readFileSync(join(folder, IGNORE_FILE), "utf8").split(/\r?\n/); } catch {}
|
|
41
|
+
const tests = lines.map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!")).map(compile).filter(Boolean);
|
|
42
|
+
if (!tests.length) return () => false;
|
|
43
|
+
// A folder's own path is asked about with a trailing slash too, so a folder-only pattern names it (and never a file).
|
|
44
|
+
return (path, folder = false) => tests.some((test) => test.test(path) || (folder && test.test(`${path}/`)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a path from a page's top is left out by the page's `.lampignore` or by its app's (`app`, the folder above it
|
|
49
|
+
* when the page is in Web/, whose patterns count from its own top).
|
|
50
|
+
*/
|
|
51
|
+
export function ignoredFor(root, app = root) {
|
|
52
|
+
const own = ignoredIn(root);
|
|
53
|
+
if (app === root) return own;
|
|
54
|
+
const outer = ignoredIn(app), under = relative(app, root).split(sep).join("/");
|
|
55
|
+
return (path, folder = false) => own(path, folder) || outer(under ? `${under}/${path}` : path, folder);
|
|
56
|
+
}
|
package/cli/lampapp.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { inflateRawSync } from "node:zlib";
|
|
|
9
9
|
import { Problem, RESOURCES, manifestOf, projectOf, stateOf } from "./state.mjs";
|
|
10
10
|
import { LIMITS, nameOf, zipOf } from "./share.mjs";
|
|
11
11
|
import { isID } from "../site/lib/shared-wishes.js";
|
|
12
|
+
import { ignoredFor, ignoredIn } from "./ignore.mjs";
|
|
12
13
|
|
|
13
14
|
// What a Lamp app file may hold, written once in Resources/share-rules.json, which Lamp's app reads too (`ShareRules`):
|
|
14
15
|
// the sizes another Lamp opens, the names a license goes by, and the shapes of keys a shared file never carries.
|
|
@@ -185,7 +186,7 @@ function filesUnder(dir, prefix = "", { hidden = () => false, skip = () => false
|
|
|
185
186
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return found; }
|
|
186
187
|
for (const entry of entries) {
|
|
187
188
|
const path = prefix + entry.name;
|
|
188
|
-
if ((entry.name.startsWith(".") && !hidden(path)) || skip(path)) continue;
|
|
189
|
+
if ((entry.name.startsWith(".") && !hidden(path)) || skip(path, entry.isDirectory())) continue;
|
|
189
190
|
if (entry.isDirectory()) found.push(...filesUnder(join(dir, entry.name), path + "/", { hidden: () => false, skip }));
|
|
190
191
|
else if (entry.isFile()) found.push(path);
|
|
191
192
|
}
|
|
@@ -237,6 +238,10 @@ export function exportApp(given, { anyway = false, link = null } = {}) {
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
const files = new Map(), from = (path) => readFileSync(join(app, path));
|
|
241
|
+
// The folder's .lampignore covers the app's own files too (its code beside the page, Assets/), as they are from the
|
|
242
|
+
// folder's top; a wish's own copy in .lamp-wish/ isn't the folder's, so its rules don't reach in there.
|
|
243
|
+
const appIgnored = app === folder ? ignoredIn(folder) : () => false;
|
|
244
|
+
const keep = (path) => !appIgnored(path);
|
|
240
245
|
let kind, site = false, flags = {};
|
|
241
246
|
if (project.kind === "page") {
|
|
242
247
|
kind = typeof manifest?.platform === "string" ? manifest.platform : project.game ? "game" : "web-app";
|
|
@@ -245,19 +250,21 @@ export function exportApp(given, { anyway = false, link = null } = {}) {
|
|
|
245
250
|
// page is Web/ in the file and whose own files beside it go as themselves.
|
|
246
251
|
// A site stays one at every export, whether its page is at the folder's top or (unpacked) in Web/.
|
|
247
252
|
site = kind === "web-app" && (manifest?.site === true || !project.lamps);
|
|
248
|
-
|
|
253
|
+
// What the folder's .lampignore keeps to this computer stays out of the file, as it does of the page.
|
|
254
|
+
const ignored = ignoredFor(project.root, folder);
|
|
255
|
+
for (const path of filesUnder(project.root, "", { hidden: (path) => site && SERVED_HIDDEN.has(path), skip: (path, dir) => path === "node_modules" || ignored(path, dir) || (!project.lamps && (wishFolder ? path === ".lamp-wish" || path === "LAMP-LICENSE.txt" : WISH_FILES.has(path))) })) {
|
|
249
256
|
if (!lamps(path)) files.set(`Web/${path}`, readFileSync(join(project.root, path)));
|
|
250
257
|
}
|
|
251
258
|
if (project.lamps || wishFolder) {
|
|
252
259
|
// The Swift beside the page that answers its calls of the device, and a game's phone page.
|
|
253
|
-
for (const path of filesUnder(join(app, "Sources"), "Sources/")) if (path.endsWith(".swift") && (kind === "game" ? path === "Sources/Native.swift" : !["Sources/LampSupport.swift", "Sources/LampPage.swift", "Sources/LampControllerKit.swift", "Sources/LampScreen.swift", "Sources/LampWebApp.swift"].includes(path))) files.set(path, from(path));
|
|
254
|
-
if (kind === "game") for (const path of [...filesUnder(join(app, "Phone"), "Phone/"), ...filesUnder(join(app, "src"), "src/")]) files.set(path, from(path));
|
|
260
|
+
for (const path of filesUnder(join(app, "Sources"), "Sources/")) if (keep(path) && path.endsWith(".swift") && (kind === "game" ? path === "Sources/Native.swift" : !["Sources/LampSupport.swift", "Sources/LampPage.swift", "Sources/LampControllerKit.swift", "Sources/LampScreen.swift", "Sources/LampWebApp.swift"].includes(path))) files.set(path, from(path));
|
|
261
|
+
if (kind === "game") for (const path of [...filesUnder(join(app, "Phone"), "Phone/"), ...filesUnder(join(app, "src"), "src/")]) if (keep(path)) files.set(path, from(path));
|
|
255
262
|
}
|
|
256
263
|
} else {
|
|
257
264
|
kind = "mac";
|
|
258
|
-
for (const path of filesUnder(join(folder, "Sources"), "Sources/")) if (path.endsWith(".swift") && !SWIFT_LAMPS.has(basename(path))) files.set(path, from(path));
|
|
265
|
+
for (const path of filesUnder(join(folder, "Sources"), "Sources/")) if (keep(path) && path.endsWith(".swift") && !SWIFT_LAMPS.has(basename(path))) files.set(path, from(path));
|
|
259
266
|
for (const [extra, flag] of [["Widget", "widget"], ["Share", "share"]]) {
|
|
260
|
-
const code = filesUnder(join(folder, extra), `${extra}/`).filter((path) => path.endsWith(".swift") && !SWIFT_LAMPS.has(basename(path)));
|
|
267
|
+
const code = filesUnder(join(folder, extra), `${extra}/`).filter((path) => keep(path) && path.endsWith(".swift") && !SWIFT_LAMPS.has(basename(path)));
|
|
261
268
|
for (const path of code) files.set(path, from(path));
|
|
262
269
|
flags[flag] = code.length > 0;
|
|
263
270
|
}
|
|
@@ -271,11 +278,12 @@ export function exportApp(given, { anyway = false, link = null } = {}) {
|
|
|
271
278
|
let attached = 0;
|
|
272
279
|
// A page at the folder's top with no `.lamp-wish/` keeps an Assets/ there as its own: it went with the page.
|
|
273
280
|
for (const path of project.kind === "page" && !project.lamps && !wishFolder ? [] : filesUnder(join(app, "Assets"), "Assets/")) {
|
|
281
|
+
if (!keep(path)) continue;
|
|
274
282
|
attached += statSync(join(app, path)).size;
|
|
275
283
|
if (attached > MAX_ARCHIVE - 8 * 1024 * 1024) throw new Problem(`${name} is too big to share: its pictures and other files come to over ${Math.floor((MAX_ARCHIVE - 8 * 1024 * 1024) / 1_000_000)} MB`);
|
|
276
284
|
files.set(path, from(path));
|
|
277
285
|
}
|
|
278
|
-
if (existsSync(join(app, "APP.md"))) files.set("APP.md", from("APP.md"));
|
|
286
|
+
if (keep("APP.md") && existsSync(join(app, "APP.md"))) files.set("APP.md", from("APP.md"));
|
|
279
287
|
// What the app declares; the Lamp that opens the file checks each of it again as it reads it.
|
|
280
288
|
let declares = false;
|
|
281
289
|
try {
|
|
@@ -286,7 +294,7 @@ export function exportApp(given, { anyway = false, link = null } = {}) {
|
|
|
286
294
|
// icon.png at the top of a page lamp made is the page's, which went with it.
|
|
287
295
|
for (const icon of ["icon.json", "icon-glass.json", "icon-art.png", "icon.png"]) {
|
|
288
296
|
if (icon === "icon.png" && project.kind === "page" && !project.lamps && !wishFolder) continue;
|
|
289
|
-
if (existsSync(join(app, icon))) files.set(icon, from(icon));
|
|
297
|
+
if (keep(icon) && existsSync(join(app, icon))) files.set(icon, from(icon));
|
|
290
298
|
}
|
|
291
299
|
const releases = releasesOf(app);
|
|
292
300
|
if (releases.length) files.set("releases.json", Buffer.from(JSON.stringify(releases, null, 2)));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"instance":"
|
|
1
|
+
{"instance":"99F1CF1A-EC7E-4440-8F31-0B2D19F376A5","platform":"mac","name":"Zqx App Name","exe":"ZqxAppName"}
|
package/cli/share.mjs
CHANGED
|
@@ -17,6 +17,7 @@ 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
18
|
import { exportApp, sourceLinkOf } from "./lampapp.mjs";
|
|
19
19
|
import { scaled, sizeOf } from "./png.mjs";
|
|
20
|
+
import { ignoredFor } from "./ignore.mjs";
|
|
20
21
|
|
|
21
22
|
export { siteOrigin };
|
|
22
23
|
|
|
@@ -104,10 +105,12 @@ export function zipOf(files) {
|
|
|
104
105
|
*/
|
|
105
106
|
export function pageFiles(layout, { device = ANY_BROWSER } = {}) {
|
|
106
107
|
const files = new Map();
|
|
108
|
+
// What the folder's .lampignore keeps to this computer (data too big to share, say) never goes.
|
|
109
|
+
const ignored = ignoredFor(layout.root, layout.folder);
|
|
107
110
|
const walk = (dir, prefix) => {
|
|
108
111
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
109
112
|
const path = prefix + entry.name;
|
|
110
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules" || path === NOTES_FILE) continue;
|
|
113
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || path === NOTES_FILE || ignored(path, entry.isDirectory())) continue;
|
|
111
114
|
if (entry.isDirectory()) walk(join(dir, entry.name), path + "/");
|
|
112
115
|
else if (entry.isFile()) files.set(path, readFileSync(join(dir, entry.name)));
|
|
113
116
|
}
|
package/cli/skill/SKILL.md
CHANGED
|
@@ -198,7 +198,9 @@ Only when the person asks: a link is public, and anyone who has it can play the
|
|
|
198
198
|
and their email, and run `{{lamp}} account <username> --email <theirs>`; they confirm it from the email, and the link
|
|
199
199
|
moves there at its next share. `{{lamp}} account` says which they're signed in as. A page that's logging errors
|
|
200
200
|
isn't shared until they're put right. Again, after changes, it sends the page as it is now to the same link, with the
|
|
201
|
-
same words unless you give new ones.
|
|
201
|
+
same words unless you give new ones. Files only this computer needs (data too big for a link, which the page fetches
|
|
202
|
+
from elsewhere when it isn't there) go in a `.lampignore` at the folder's top, gitignore's way (`ldraw/`, `*.csv`):
|
|
203
|
+
share and export leave them out. Never share from a copy of the folder made for it.
|
|
202
204
|
- `{{lamp}} ideas` lists what people playing it have sent, each with the address of a picture of the app as they saw
|
|
203
205
|
it when they kept one in: look at it before building the idea. Once one is in the game and the game shared again,
|
|
204
206
|
`{{lamp}} ideas in <number>` says so, and whoever asked to hear is written to; `{{lamp}} ideas dismiss <number>` lets
|