@homespunapps/cli 1.6.35 → 1.6.36

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.
@@ -9,7 +9,18 @@ import { specFor } from "../help-catalog.js";
9
9
  import { fail, failFromError, printJson } from "../output.js";
10
10
  import { resolveJson } from "../input.js";
11
11
  import { resolveAppId } from "../resolve-app.js";
12
- function readBundle(source, manifestFlag) {
12
+ function readBundle(source, manifestFlag, isRedeploy) {
13
+ // No source at all: only a redeploy can do this, and only to change the
14
+ // manifest alone (the live document is inherited).
15
+ if (source === undefined) {
16
+ if (!isRedeploy) {
17
+ fail("usage: homespun deploy <dir|file> [--app <id>] ...", "invalid_args");
18
+ }
19
+ if (manifestFlag === undefined) {
20
+ fail("nothing to deploy: pass a directory or file, or --manifest <path|json> for a manifest-only redeploy", "invalid_args");
21
+ }
22
+ return { manifest: resolveJson(manifestFlag, "--manifest") };
23
+ }
13
24
  if (!existsSync(source)) {
14
25
  fail(`no such file or directory: ${source}`, "invalid_args");
15
26
  }
@@ -33,9 +44,13 @@ function readBundle(source, manifestFlag) {
33
44
  manifest: JSON.parse(readFileSync(manifestPath, "utf8")),
34
45
  };
35
46
  }
36
- // Single-file escape hatch.
47
+ // Single-file escape hatch. A create must pair it with --manifest; a
48
+ // redeploy may omit --manifest, which keeps the live manifest.
37
49
  if (manifestFlag === undefined) {
38
- fail("single-file deploy requires --manifest <path|json>", "invalid_args");
50
+ if (!isRedeploy) {
51
+ fail("single-file deploy requires --manifest <path|json>", "invalid_args");
52
+ }
53
+ return { html: readFileSync(source, "utf8") };
39
54
  }
40
55
  return {
41
56
  html: readFileSync(source, "utf8"),
@@ -45,9 +60,6 @@ function readBundle(source, manifestFlag) {
45
60
  export async function runDeploy(args) {
46
61
  assertKnownFlags(args, ...specFor("deploy"));
47
62
  const source = args.positionals[0];
48
- if (!source) {
49
- fail("usage: homespun deploy <dir|file> [--app <id>] ...", "invalid_args");
50
- }
51
63
  const appId = args.flags.get("app");
52
64
  const slug = args.flags.get("slug");
53
65
  const visibility = args.flags.get("visibility");
@@ -57,7 +69,7 @@ export async function runDeploy(args) {
57
69
  }
58
70
  const force = args.bools.has("force");
59
71
  const check = args.bools.has("check");
60
- const bundle = readBundle(source, args.flags.get("manifest"));
72
+ const bundle = readBundle(source, args.flags.get("manifest"), appId !== undefined);
61
73
  const client = makeClient(args);
62
74
  // Dry run (--check): validate + report what a real deploy would do, persist
63
75
  // NOTHING. Runs for both create (no --app) and redeploy (--app), the latter
@@ -67,8 +79,8 @@ export async function runDeploy(args) {
67
79
  const id = appId !== undefined ? await resolveAppId(client, appId) : undefined;
68
80
  const result = await client.checkDeploy({
69
81
  ...(id !== undefined ? { app_id: id } : {}),
70
- html: bundle.html,
71
- manifest: bundle.manifest,
82
+ ...(bundle.html !== undefined ? { html: bundle.html } : {}),
83
+ ...(bundle.manifest !== undefined ? { manifest: bundle.manifest } : {}),
72
84
  ...(force ? { force } : {}),
73
85
  });
74
86
  printJson(result);
@@ -85,6 +97,9 @@ export async function runDeploy(args) {
85
97
  fail("a caller-supplied --slug is not allowed with visibility 'link' (link slugs are always server-generated); drop --visibility link, or omit --slug", "invalid_args");
86
98
  }
87
99
  try {
100
+ // readBundle guarantees both halves on the create path (a create can
101
+ // inherit nothing), so the non-null assertions are the type system
102
+ // catching up with a check that already ran.
88
103
  const out = await client.deployApp({
89
104
  html: bundle.html,
90
105
  manifest: bundle.manifest,
@@ -107,9 +122,13 @@ export async function runDeploy(args) {
107
122
  }
108
123
  const id = await resolveAppId(client, appId);
109
124
  try {
125
+ // Only what this invocation actually read is sent: an omitted html or
126
+ // manifest keeps what is live, so `homespun deploy ./index.html --app <id>`
127
+ // ships the document alone and `--manifest` with no file ships the
128
+ // manifest alone.
110
129
  const redeployed = await client.redeployApp(id, {
111
- html: bundle.html,
112
- manifest: bundle.manifest,
130
+ ...(bundle.html !== undefined ? { html: bundle.html } : {}),
131
+ ...(bundle.manifest !== undefined ? { manifest: bundle.manifest } : {}),
113
132
  force,
114
133
  });
115
134
  const app = await client.getApp(id);
@@ -777,7 +777,7 @@ const DEPLOY = {
777
777
  verbs: [
778
778
  {
779
779
  verb: "",
780
- positionals: "<dir|file>",
780
+ positionals: "[dir|file]",
781
781
  summary: "Creates a new app, or redeploys an existing one when --app is given.",
782
782
  flags: [
783
783
  {
@@ -813,7 +813,8 @@ const DEPLOY = {
813
813
  notes: [
814
814
  "Packaging has one canonical shape and one escape hatch. A directory deploy (homespun deploy ./my-app) reads ./my-app/index.html and ./my-app/manifest.json: fixed filenames, no discovery heuristics, and both files are required. The single-file escape hatch (homespun deploy ./index.html --manifest ./manifest.json) takes the manifest from --manifest, which accepts a file path or inline JSON.",
815
815
  "Create versus redeploy is decided by the presence of --app, not by two verbs. With no --app this creates an app (POST /v1/apps); new apps default to private (owner plus invited members, sign-in gated), --slug is accepted with private or public visibility including the default, and an explicit --visibility link always gets a server-generated slug and rejects --slug. With --app <id> this redeploys (POST /v1/apps/:id/versions), where --slug and --visibility are rejected because the slug is immutable and visibility changes go through 'homespun apps update'.",
816
- "--check is a dry run. It runs the full manifest and asset-shape validation, the redeploy compat gate (with --app), and the schedule-timezone advisory, then prints { ok, warnings, compat, breaks } without creating a version or mutating anything. An invalid manifest fails the same way a real deploy would, and a redeploy the compat gate would refuse reports the break instead of applying it.",
816
+ "On redeploy, what you do not send is kept. 'homespun deploy ./index.html --app <id>' ships the document alone and keeps the live manifest; 'homespun deploy --app <id> --manifest ./manifest.json' ships the manifest alone and keeps the live document, with no file argument at all; a directory deploy still ships both. The live asset set is carried forward either way. A create can inherit nothing, so it still needs both halves.",
817
+ "--check is a dry run. It runs the full manifest and asset validation (shape and MIME), the redeploy compat gate (with --app), and the schedule-timezone advisory, then prints { ok, warnings, compat, breaks } without creating a version or mutating anything. An invalid manifest fails the same way a real deploy would, and a redeploy the compat gate would refuse reports the break instead of applying it. It resolves omitted fields exactly as a real redeploy would, so it reports on the deploy that would actually run.",
817
818
  ],
818
819
  outputNote: 'Output is JSON: { app_id, slug, url, version, visibility, created, share_url, compat, breaks, warnings }. share_url is present only when creating a link-visibility app: it carries the app share token in its #k= fragment and is shown ONCE, it is not recoverable later, and it can be rotated with \'homespun apps share-link rotate <app>\'. warnings flags non-fatal issues, for example an app that declares schedules with no timezone set (reminders fire at 08:00 UTC until one is set). Errors go to stderr as {"error":{"code","message"}} with a non-zero exit.',
819
820
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homespunapps/cli",
3
- "version": "1.6.35",
3
+ "version": "1.6.36",
4
4
  "description": "Command-line client for the Homespun relay: deploy a real multi-user web app from your agent, then keep reading and writing its data.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "test:unit": "vitest run"
37
37
  },
38
38
  "dependencies": {
39
- "@homespunapps/core": "^1.6.35",
39
+ "@homespunapps/core": "^1.6.36",
40
40
  "qrcode-terminal": "^0.12.0"
41
41
  },
42
42
  "devDependencies": {