lemonade-host 0.1.0 → 0.1.1

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 CHANGED
@@ -22,20 +22,21 @@ and save it:
22
22
  npx lemonade-host login
23
23
  ```
24
24
 
25
- It asks for the token and the site, and writes them to `~/.lemonade`
26
- with permissions only you can read. Or set `LEMONADE_TOKEN` and
27
- `LEMONADE_SITE` in the environment instead, which is what CI wants.
25
+ It asks for the token nothing else. A token is minted for one site, so it
26
+ already knows where to publish. The answer is written to `~/.lemonade` with
27
+ permissions only you can read. Or set `LEMONADE_TOKEN` in the environment
28
+ instead, which is what CI wants.
28
29
 
29
30
  ## Commands
30
31
 
31
32
  | | |
32
33
  |---|---|
33
- | `lemonade deploy [dir]` | Zip a folder and publish it. Defaults to `.`, and prefers `dist/`, `build/`, `out/` or `public/` if one exists |
34
+ | `lemonade deploy [dir]` | Zip a folder and publish it. Defaults to `.`, and prefers `dist/`, `build/`, `out/` or `public/` if one exists. `--dir` does the same thing |
34
35
  | `lemonade status` | The last few deploys for this site |
35
- | `lemonade login` | Save a token and site id to `~/.lemonade` |
36
+ | `lemonade login` | Save a token to `~/.lemonade` |
36
37
  | `lemonade whoami` | Which site the current token points at |
37
38
 
38
- Useful flags: `--token`, `--site`, `--origin` (for staging), `--yes` (skip
39
+ Useful flags: `--dir`, `--token`, `--origin` (for staging), `--yes` (skip
39
40
  the confirmation), `--json` (machine-readable output).
40
41
 
41
42
  ## A token reaches exactly one site
@@ -53,7 +54,6 @@ Revoke one any time from the same screen you minted it on.
53
54
  - run: npx lemonade-host deploy ./dist --yes
54
55
  env:
55
56
  LEMONADE_TOKEN: ${{ secrets.LEMONADE_TOKEN }}
56
- LEMONADE_SITE: ${{ secrets.LEMONADE_SITE }}
57
57
  ```
58
58
 
59
59
  Exit code is 0 on a successful publish and non-zero on anything else, so a
package/bin/lemonade.mjs CHANGED
@@ -23,7 +23,8 @@ const USAGE = `lemonade — publish a folder to Lemonade Host
23
23
 
24
24
  Options
25
25
  --token lm_… a deploy token (or set LEMONADE_TOKEN)
26
- --site st_… which site (or set LEMONADE_SITE)
26
+ --dir FOLDER which folder to publish (same as the positional argument)
27
+ --site st_… which site — only needed if the token reaches more than one
27
28
  --origin URL point at another instance, e.g. staging
28
29
  --yes, -y do not ask before publishing
29
30
  --json machine-readable output
@@ -36,7 +37,7 @@ deploy history. It cannot spend money, cancel anything, or see another site.
36
37
  `;
37
38
 
38
39
  const BOOLEAN = new Set(["yes", "y", "json", "include-hidden", "help", "h", "version", "v"]);
39
- const VALUED = new Set(["token", "site", "origin"]);
40
+ const VALUED = new Set(["token", "site", "origin", "dir"]);
40
41
 
41
42
  function parse(argv) {
42
43
  const args = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemonade-host",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Publish a folder to Lemonade Host from your terminal.",
5
5
  "keywords": [
6
6
  "lemonade",
package/src/commands.mjs CHANGED
@@ -54,20 +54,33 @@ function requireAuth(flags) {
54
54
  ` It came from ${cfg.from.token}.`,
55
55
  );
56
56
  }
57
- if (!cfg.site) {
58
- throw new Refused(
59
- "No site id.\n" +
60
- " Pass --site st_… , set LEMONADE_SITE, or run: lemonade login\n" +
61
- " The id is in the address bar of the site's page in your dashboard.",
62
- );
63
- }
64
- const site = normalizeSite(cfg.site);
65
- if (!site) {
57
+ // The site is OPTIONAL here. A token is minted for exactly one site, so
58
+ // the credential already implies it — asking for both was making people
59
+ // repeat something we know, and it made the documented promise ("set
60
+ // LEMONADE_TOKEN once and never touch it again") untrue. When it is
61
+ // absent, ask the server: `resolveSite` below.
62
+ const site = cfg.site ? normalizeSite(cfg.site) : null;
63
+ if (cfg.site && !site) {
66
64
  throw new Refused(`"${cfg.site}" is not a site id — they look like st_….`);
67
65
  }
68
66
  return { ...cfg, site };
69
67
  }
70
68
 
69
+ /**
70
+ * The site this run is about: the one configured, or the one the token was
71
+ * minted for. One round trip, and only when it is needed.
72
+ */
73
+ async function resolveSite(cfg) {
74
+ if (cfg.site) return cfg.site;
75
+ const me = await call(cfg, `${cfg.origin}/api/deploy/whoami`);
76
+ if (!me.site) {
77
+ throw new Refused(
78
+ "That token did not resolve to a site.\n Mint a fresh one in your dashboard under Deploy tools.",
79
+ );
80
+ }
81
+ return me.site;
82
+ }
83
+
71
84
  /**
72
85
  * Which folder to publish.
73
86
  *
@@ -137,8 +150,10 @@ async function call(cfg, url, init = {}) {
137
150
  /* ── deploy ──────────────────────────────────────────────────────────────── */
138
151
 
139
152
  export async function deploy(args, flags) {
140
- const cfg = requireAuth(flags);
141
- const { dir, guessed } = chooseDir(args[0]);
153
+ const base = requireAuth(flags);
154
+ // `--dir` is what the documentation has always shown; the positional form
155
+ // is what a shell user reaches for. Both work, and the flag wins.
156
+ const { dir, guessed } = chooseDir(flags.dir ?? args[0]);
142
157
 
143
158
  const { files, skipped } = collect(dir, { includeHidden: flags.includeHidden });
144
159
  if (files.length === 0) {
@@ -179,6 +194,8 @@ export async function deploy(args, flags) {
179
194
  );
180
195
  }
181
196
 
197
+ const cfg = { ...base, site: await resolveSite(base) };
198
+
182
199
  if (!flags.yes && !flags.json && !(await confirm(`Publish to ${cfg.site}?`))) {
183
200
  console.log("Nothing was published.");
184
201
  return 1;
@@ -227,7 +244,8 @@ export async function deploy(args, flags) {
227
244
  /* ── status ──────────────────────────────────────────────────────────────── */
228
245
 
229
246
  export async function status(_args, flags) {
230
- const cfg = requireAuth(flags);
247
+ const base = requireAuth(flags);
248
+ const cfg = { ...base, site: await resolveSite(base) };
231
249
  const body = await call(cfg, `${cfg.origin}/api/deploy/${cfg.site}/deployments`);
232
250
  const rows = body.deployments ?? [];
233
251
 
@@ -254,15 +272,19 @@ export async function status(_args, flags) {
254
272
  export async function whoami(_args, flags) {
255
273
  const cfg = requireAuth(flags);
256
274
  // Asking the SERVER rather than reading the config back: the question is
257
- // "does this token still work, and what does it reach", and only one of
258
- // those two can be answered locally.
259
- await call(cfg, `${cfg.origin}/api/deploy/${cfg.site}/deployments`);
275
+ // "does this token still work, and what does it reach", and neither half
276
+ // can be answered locally.
277
+ const me = await call(cfg, `${cfg.origin}/api/deploy/whoami`);
260
278
  if (flags.json) {
261
- console.log(JSON.stringify({ ok: true, site: cfg.site, origin: cfg.origin }, null, 2));
279
+ console.log(JSON.stringify(me, null, 2));
262
280
  return 0;
263
281
  }
264
- console.log(`Token is valid and reaches ${bold(cfg.site)}`);
265
- console.log(dim(` ${cfg.origin} · token from ${cfg.from.token}, site from ${cfg.from.site}`));
282
+ console.log(`Token is valid and reaches ${bold(me.name)} (${me.site})`);
283
+ console.log(` ${me.url}`);
284
+ if (!me.serving) {
285
+ console.log(dim(" Not serving yet — this site has not been published from the dashboard."));
286
+ }
287
+ console.log(dim(` ${cfg.origin} · token from ${cfg.from.token}`));
266
288
  return 0;
267
289
  }
268
290
 
@@ -278,20 +300,20 @@ export async function login(_args, flags) {
278
300
  if (!looksLikeToken(token)) {
279
301
  throw new Refused("That does not look like a deploy token — they start with lm_.");
280
302
  }
281
- const siteRaw = flags.site ?? (await rl.question("Site id (st_…, or paste the URL): "));
282
- const site = normalizeSite(siteRaw);
283
- if (!site) throw new Refused("That is not a site id — they look like st_….");
284
-
285
303
  const origin = (flags.origin ?? process.env.LEMONADE_ORIGIN ?? "https://app.lemonadehost.com").replace(/\/+$/, "");
286
304
 
287
- // Proved before it is saved. Writing an unverified token means the
288
- // first real deploy is where you learn it was wrong, and by then the
289
- // one place the raw token existed has usually been closed.
290
- const probe = { token, site, origin, from: { token: "prompt", site: "prompt" } };
291
- await call(probe, `${origin}/api/deploy/${site}/deployments`);
305
+ // No site prompt. The token names its own site, so asking would be
306
+ // asking somebody to look up an id we are about to be told anyway —
307
+ // and this is also how the token is PROVED before it is written down.
308
+ // Saving an unverified token means the first real deploy is where you
309
+ // learn it was wrong, by which time the one place the raw value existed
310
+ // has usually been closed.
311
+ const probe = { token, origin, from: { token: "prompt", site: null } };
312
+ const me = await call(probe, `${origin}/api/deploy/whoami`);
292
313
 
293
- const where = save({ token, site, origin });
314
+ const where = save({ token, site: me.site, origin });
294
315
  console.log(`\n${green("Saved.")} ${dim(where)} (readable only by you)`);
316
+ console.log(`Publishing to ${bold(me.name)} — ${me.url}`);
295
317
  console.log(dim("Now: lemonade deploy"));
296
318
  return 0;
297
319
  } finally {