@lampdotdev/lamp 0.35.0 → 0.36.2-nightly.17

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.
@@ -34,8 +34,9 @@ looked earlier, `--reopen` puts it right.
34
34
  `{{lamp}} new path/to/folder --web --name "Its Name"` makes a new web app's page from Lamp's starting files:
35
35
  `index.html`, `style.css` and `app.js`, which are yours to write, `APP.md`, and `lamp.js`, Lamp's building blocks for
36
36
  a page (saved data with `LampStore`, the app's files and more). Read Lamp's `web-app` skill before writing it, and
37
- the comments in `lamp.js` for how each block is used; never edit `lamp.js`. What a block asks of Lamp's app as the
38
- page runs (its AI) isn't answered here. A page that wants none of Lamp's needs no starting files: write its
37
+ the comments in `lamp.js` for how each block is used; never edit `lamp.js`. Its AI (`LampAI`: ask, act, decide) is
38
+ answered by lamp itself on this Mac, with the person's own Claude Code or Codex (or OpenRouter, with their key in `OPENROUTER_API_KEY`), in every page lamp runs here (`dev`,
39
+ `open`, `look`, `use`); pictures and hearing need Lamp's app. Shared on lamp.dev, it's answered on the computer the page is played on while Lamp is open there or `{{lamp}} ai` runs there in a terminal, once the person allows that page. Never put a key or a model provider's address in the page. A page that wants none of Lamp's needs no starting files: write its
39
40
  `index.html` in an empty folder.
40
41
 
41
42
  ## A Mac app
@@ -98,12 +99,16 @@ here: keep its license file, its copyright headers and its author's credit on sc
98
99
 
99
100
  1. **Show the person the app** once, when you start, unless they don't want to watch: `{{lamp}} open`. It opens a
100
101
  window they can use and play in while you work, and returns at once.
101
- 2. **Change the files.**
102
+ 2. **Change the files.** First say what you're changing, in a few words of theirs: `{{lamp}} changing "Rain on the
103
+ roads"`. Do it before every change, however small: the app shows it on the card it puts up as the change goes in,
104
+ and without it the card says only that it's updating. It stands until you say something new.
102
105
  3. **Look**: `{{lamp}} look`. It waits for the change to go in, then answers how it went, a picture of the page (a
103
106
  file: read it; the last forty are kept), the screen as text, and the errors and the log since your last look. Look
104
107
  after every change, and read the errors and the picture before saying it works. `{{lamp}} look --reopen` opens the
105
108
  page again from its beginning first, a game with its saves kept: what someone opening it sees. `--size 900x600`
106
109
  looks at it as a window of that size shows it, for a layout that has to hold at any shape (with no window open).
110
+ When the answer says lamp found no Chrome, Edge, Brave or Chromium, there's no picture and `use` can't play the
111
+ page: tell the person, since only they can install one.
107
112
 
108
113
  Nothing has to be running first. With no window open, `look` runs the app itself in a copy of the page nobody sees,
109
114
  opened from the files as they are, so the first look has no changes to report. The copy goes on running between
@@ -175,14 +180,34 @@ every command here works on a web app, a game, a screen or a Mac app in one, and
175
180
 
176
181
  Only when the person asks: a link is public, and anyone who has it can play the page.
177
182
 
178
- - `{{lamp}} share --words "What it is, in a sentence or a few"` puts the page on lamp.dev and answers its link. `--line`
179
- is a line under its name, `--asks "Found a bug?|What's missing?"` what the box under the page asks people, and
180
- `--by "Their Name"` who it's by: only a name the person gave you to show. A page that's logging errors isn't shared
181
- until they're put right.
182
- - Again, after changes, it sends the page as it is now to the same link, with the same words unless you give new ones.
183
- - `{{lamp}} ideas` lists what people playing it have sent. Once one is in the game and the game shared again,
183
+ - Start with `{{lamp}} share --preview`: it says what would go and sends nothing. Show the person, and share only once they
184
+ say so.
185
+ - **A web app or game of Lamp's own** (its folder is in Lamp's) is shared here like any other page, whether Lamp is open
186
+ or not, to the link Lamp already has for it, and `ideas` and `unshare` work on that link too. Run `{{lamp}} share` with
187
+ no words of your own (it keeps the link's), and give `--line`, `--by` or `--asks "a|b"` only for what the person
188
+ wants changed.
189
+ - **Any other app of Lamp's** (a Mac or iPhone app, a wish of several; the preview says it's going through Lamp) is
190
+ shared by Lamp, which must be open, filled as its Share Link card fills itself: its words, the questions and what's new
191
+ Lamp writes, its file, its GitHub release. Give `--notes "What's new|And more"` and the rest only for what the person
192
+ wants changed. Its ideas and taking it down are on Lamp's Share Link card.
193
+ - **Any other page:** `{{lamp}} share --words "What it is, in a sentence or a few"` puts it on lamp.dev and answers its
194
+ link. `--line` is a line under its name, `--asks "Found a bug?|What's missing?"` what the box under the page asks
195
+ people, and `--by "Their Name"` who it's by, when they want another name than the one it goes out with (the name they last
196
+ gave, or Lamp's, or their name on this computer, which the share says). A link goes under the person's username on
197
+ lamp.dev (`lamp.dev/<username>/<app>`) once they have one: the first time you share, ask them which username they want
198
+ and their email, and run `{{lamp}} account <username> --email <theirs>`; they confirm it from the email, and the link
199
+ moves there at its next share. `{{lamp}} account` says which they're signed in as. A page that's logging errors
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.
202
+ - `{{lamp}} ideas` lists what people playing it have sent, each with the address of a picture of the app as they saw
203
+ it when they kept one in: look at it before building the idea. Once one is in the game and the game shared again,
184
204
  `{{lamp}} ideas in <number>` says so, and whoever asked to hear is written to; `{{lamp}} ideas dismiss <number>` lets
185
- one go.
205
+ one go. Words after the number are a note to whoever sent it, shown on the link's page and put in that email: write
206
+ one only when the person asks you to answer them, in their words. Senders can answer by email: `{{lamp}} ideas` shows
207
+ what's been said under each idea, and those they've answered; `{{lamp}} ideas reply <number> <words>` answers again,
208
+ and `{{lamp}} ideas done <number>` leaves one without a word. `{{lamp}} ideas --earlier` lists those seen to. `{{lamp}} ideas email <address>` sends new ideas and replies to the
209
+ person's own address too, once they confirm it from the email it sends (`off` stops them): only with an address
210
+ they gave you.
186
211
  - `{{lamp}} unshare` takes the link down.
187
212
 
188
213
  Tell the person the link, and that it's public.
package/cli/state.mjs CHANGED
@@ -22,6 +22,9 @@ export class Problem extends Error {}
22
22
 
23
23
  /** The runtime whose presence makes a page a game's (`GamePage.runtimeFile`). */
24
24
  export const RUNTIME_FILE = "lamp-game.js";
25
+ // The app's notes, at the top of its page: what it is and how it's made, for whoever works on it. Never part of the
26
+ // page: saving it changes nothing a page runs, and a shared page goes without it.
27
+ export const NOTES_FILE = "APP.md";
25
28
 
26
29
  /**
27
30
  * How the folder is laid out: an app Lamp made keeps its page in Web/, and any other folder with an index.html is a
package/cli/update.mjs CHANGED
@@ -1,32 +1,81 @@
1
- // Whether a newer lamp is out, for a lamp installed from npm (@lampdotdev/lamp). The copy in Lamp's app updates with
2
- // Lamp (Sparkle) and a checkout with its repository, so neither asks. At most once a day a process of its own asks npm's
3
- // registry, without holding up the command, and keeps what it heard; the next command says so in one line, once a day,
4
- // with the command that updates it. lamp never updates itself: a global install is the person's.
1
+ // Whether a newer lamp is out. For a lamp installed from npm (@lampdotdev/lamp): at most once a day a process of its own
2
+ // asks npm's registry, without holding up the command, and keeps what it heard; the next command says so in one line,
3
+ // once a day, with the command that updates it. lamp never updates a global install: it's the person's.
4
+ // For the copy in Lamp's app, run as the person's `lamp` (lamp.sh): lamp is published on npm at every change to main
5
+ // (`nightly`) as well as with each release (`latest`), much sooner than Lamp's app can be built, so at most once an hour
6
+ // the same background process fetches the newest of its channel into ~/.lamp/cli when it's newer than the app's own,
7
+ // and the launcher runs that one from then on, until the app has caught up. Lamp's own calls of lamp (LampCLI.swift)
8
+ // never do: the app and the lamp inside it are always the same.
5
9
 
6
- import { spawn } from "node:child_process";
7
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { spawn, spawnSync } from "node:child_process";
11
+ import { createHash } from "node:crypto";
12
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
8
13
  import { homedir } from "node:os";
9
14
  import { dirname, join, sep } from "node:path";
10
15
  import { fileURLToPath } from "node:url";
11
16
 
12
17
  const NAME = "@lampdotdev/lamp";
13
- const DAY = 24 * 60 * 60 * 1000;
18
+ const DAY = 24 * 60 * 60 * 1000, HOUR = 60 * 60 * 1000;
14
19
  const here = dirname(fileURLToPath(import.meta.url));
15
20
  const cache = () => join(process.env.LAMP_DEV_HOME ? dirname(process.env.LAMP_DEV_HOME) : join(homedir(), ".lamp"), "update.json");
16
21
 
17
22
  /** Whether this copy is one npm installed, and so one npm updates. */
18
23
  const fromNpm = () => here.includes(`${sep}node_modules${sep}@lampdotdev${sep}lamp${sep}`);
19
24
 
20
- const own = () => { try { return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version ?? null; } catch { return null; } };
25
+ // npm's package has its package.json at its top, beside cli/; Lamp's app carries lamp's own in cli/ (build.sh).
26
+ const own = () => {
27
+ for (const file of [join(here, "..", "package.json"), join(here, "package.json")]) {
28
+ try { const { name, version } = JSON.parse(readFileSync(file, "utf8")); if ((name === NAME || name === "lamp") && version) return version; } catch {}
29
+ }
30
+ return null;
31
+ };
32
+
33
+ /**
34
+ * The npm channel a copy run as the person's `lamp` from Lamp's app follows: Lamp Nightly's, `nightly`; Lamp's, `latest`.
35
+ * None for Lamp Dev (a build from source has its checkout's lamp) or any other copy.
36
+ */
37
+ const channel = () => {
38
+ if (!process.env.LAMP_FROM_APP || !process.env.LAMP_LAUNCHER) return null;
39
+ const app = process.env.LAMP_APP ?? "";
40
+ return app === "dev.lamp.host.nightly" ? "nightly" : app === "dev.lamp.host" ? "latest" : null;
41
+ };
42
+ /** Where fetched copies are kept, and the launcher's note of which to run (lamp.sh reads it). */
43
+ const fetched = () => join(dirname(cache()), "cli");
21
44
  const read = () => { try { return JSON.parse(readFileSync(cache(), "utf8")); } catch { return {}; } };
22
45
  const keep = (state) => { try { mkdirSync(dirname(cache()), { recursive: true }); writeFileSync(cache(), JSON.stringify(state)); } catch {} };
23
46
 
24
- /** Whether version a is newer than b, by their numbers (a pre-release is older than its release). */
47
+ /** This lamp's version, and where it came from, for `lamp --version`. */
48
+ export function versionLine() {
49
+ const mine = own() ?? "unknown";
50
+ const app = process.env.LAMP_BUNDLED_VERSION;
51
+ if (process.env.LAMP_FROM_APP && app && app !== mine) return `lamp ${mine} (fetched from npm, newer than the ${app} in Lamp's app)`;
52
+ if (process.env.LAMP_FROM_APP) return `lamp ${mine} (in Lamp's app)`;
53
+ return `lamp ${mine}${fromNpm() ? "" : " (from a checkout)"}`;
54
+ }
55
+
56
+ /**
57
+ * Whether version a is newer than b, as semver orders them: by their numbers, a pre-release older than its release, and
58
+ * two pre-releases by their parts in turn, numbers as numbers (0.36.2-nightly.10 after 0.36.2-nightly.9). Build
59
+ * metadata (+…) says nothing about order.
60
+ */
25
61
  export function newer(a, b) {
26
- const parts = (version) => { const [main, pre] = String(version).split("-"); return { numbers: main.split(".").map(Number), pre: pre ?? null }; };
62
+ const parts = (version) => {
63
+ const [main, ...rest] = String(version).split("+")[0].split("-");
64
+ return { numbers: main.split(".").map(Number), pre: rest.length ? rest.join("-").split(".") : null };
65
+ };
27
66
  const x = parts(a), y = parts(b);
28
67
  for (let i = 0; i < 3; i++) if ((x.numbers[i] || 0) !== (y.numbers[i] || 0)) return (x.numbers[i] || 0) > (y.numbers[i] || 0);
29
- return y.pre !== null && x.pre === null;
68
+ if (!x.pre || !y.pre) return !x.pre && !!y.pre;
69
+ for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
70
+ const p = x.pre[i], q = y.pre[i];
71
+ if (p === undefined || q === undefined) return q === undefined;
72
+ if (p === q) continue;
73
+ const pn = /^\d+$/.test(p), qn = /^\d+$/.test(q);
74
+ if (pn && qn) return Number(p) > Number(q);
75
+ if (pn !== qn) return qn; // a word part outranks a number part
76
+ return p > q;
77
+ }
78
+ return false;
30
79
  }
31
80
 
32
81
  /**
@@ -35,7 +84,15 @@ export function newer(a, b) {
35
84
  * LAMP_NO_UPDATE_CHECK set.
36
85
  */
37
86
  export function updateNotice() {
38
- if (!fromNpm() || process.env.CI || process.env.LAMP_NO_UPDATE_CHECK) return null;
87
+ if (process.env.CI || process.env.LAMP_NO_UPDATE_CHECK) return null;
88
+ if (channel()) {
89
+ const state = read(), now = Date.now();
90
+ if (!(state.fetchChecked > now - HOUR)) {
91
+ try { spawn(process.execPath, [fileURLToPath(import.meta.url), "--fetch", channel()], { detached: true, stdio: "ignore" }).unref(); } catch {}
92
+ }
93
+ return null;
94
+ }
95
+ if (!fromNpm()) return null;
39
96
  const mine = own(), state = read(), now = Date.now();
40
97
  if (!(state.checked > now - DAY)) {
41
98
  try { spawn(process.execPath, [fileURLToPath(import.meta.url), "--check"], { detached: true, stdio: "ignore" }).unref(); } catch {}
@@ -55,3 +112,51 @@ if (process.argv[1] === fileURLToPath(import.meta.url) && process.argv[2] === "-
55
112
  if (typeof latest === "string" && /^\d+\.\d+\.\d+/.test(latest)) keep({ ...read(), checked: Date.now(), latest });
56
113
  } catch {}
57
114
  }
115
+
116
+ // The background fetch, for the copy in Lamp's app: the newest lamp of its channel, when it's newer than the app's own,
117
+ // checked against npm's integrity hash, unpacked into ~/.lamp/cli/<version>, and named for the launcher with the app's
118
+ // version it's newer than, so an app updated since runs its own again.
119
+ if (process.argv[1] === fileURLToPath(import.meta.url) && process.argv[2] === "--fetch") {
120
+ const tag = process.argv[3];
121
+ keep({ ...read(), fetchChecked: Date.now() });
122
+ const app = process.env.LAMP_BUNDLED_VERSION || own();
123
+ try {
124
+ const answer = await fetch(`https://registry.npmjs.org/${NAME.replace("/", "%2F")}/${encodeURIComponent(tag)}`, { signal: AbortSignal.timeout(8000) });
125
+ const { version, dist } = answer.ok ? await answer.json() : {};
126
+ const folder = join(fetched(), String(version));
127
+ if (typeof version === "string" && /^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/.test(version) && app && newer(version, app)
128
+ && typeof dist?.tarball === "string" && /^sha512-/.test(dist?.integrity ?? "")) {
129
+ if (!existsSync(join(folder, "package", "cli", "lamp.mjs"))) {
130
+ const bytes = Buffer.from(await (await fetch(dist.tarball, { signal: AbortSignal.timeout(60000) })).arrayBuffer());
131
+ if (`sha512-${createHash("sha512").update(bytes).digest("base64")}` !== dist.integrity) throw new Error("integrity");
132
+ const part = `${folder}.part`;
133
+ rmSync(part, { recursive: true, force: true });
134
+ mkdirSync(part, { recursive: true });
135
+ writeFileSync(join(part, "lamp.tgz"), bytes);
136
+ if (spawnSync("tar", ["xzf", "lamp.tgz"], { cwd: part }).status !== 0) throw new Error("unpack");
137
+ rmSync(join(part, "lamp.tgz"));
138
+ rmSync(folder, { recursive: true, force: true });
139
+ renameSync(part, folder);
140
+ }
141
+ // The one used until now is retired as of now: its week starts here, not from when it was fetched.
142
+ let before = null;
143
+ try { before = readFileSync(join(fetched(), "use"), "utf8").split("\n")[1] || null; } catch {}
144
+ if (before && dirname(before) !== folder) { try { const now = new Date(); utimesSync(dirname(before), now, now); } catch {} }
145
+ writeFileSync(join(fetched(), "use"), `${app}\n${join(folder, "package")}\n`);
146
+ // Older ones go once they've been out of use a week (the launcher touches the one it runs, and the one retired above
147
+ // was touched as it was): a `lamp dev` started from one may run for days, reading its files (libraries,
148
+ // controllers) as it goes.
149
+ // Never one a process running now was started from (a `lamp dev` that has run past its week): its command line
150
+ // names the copy's lamp.mjs.
151
+ const running = spawnSync("ps", ["-axo", "command"], { encoding: "utf8" }).stdout ?? "";
152
+ for (const other of readdirSync(fetched())) {
153
+ if (other === "use" || other === String(version) || other.endsWith(".part")) continue;
154
+ const at = join(fetched(), other);
155
+ if (running.includes(`${at}${sep}`)) continue;
156
+ try { if (statSync(at).mtimeMs < Date.now() - 7 * DAY) rmSync(at, { recursive: true, force: true }); } catch {}
157
+ }
158
+ // The one in use is touched, so it's counted from when it was last named.
159
+ try { const now = new Date(); utimesSync(folder, now, now); } catch {}
160
+ }
161
+ } catch {}
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lampdotdev/lamp",
3
- "version": "0.35.0",
3
+ "version": "0.36.2-nightly.17",
4
4
  "description": "Runs a web app or game from its folder and puts each saved change into the open page.",
5
5
  "type": "module",
6
6
  "bin": {