@homespunapps/cli 1.0.0

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.
@@ -0,0 +1,262 @@
1
+ // `homespun config <verb>` — inspect and manage the multi-profile CLI config.
2
+ //
3
+ // show describe the resolved (url, api_key) the CLI would use
4
+ // list list saved profiles (names + URLs + current marker)
5
+ // use <name> switch the active profile
6
+ // add <name> manually add a profile (for keys obtained out of band)
7
+ // rm <name> delete a profile
8
+ //
9
+ // All four mutating verbs operate on the multi-profile store at
10
+ // $XDG_CONFIG_HOME/homespun/config.json. See store.ts for the layout. They make
11
+ // NO network calls — purely local config management.
12
+ import { assertKnownFlags } from "../argv.js";
13
+ import { describeConfig } from "../config.js";
14
+ import { isValidProfileName, readStore, removeProfile, setCurrentProfile, storePath, upsertProfile, } from "../store.js";
15
+ import { printJson, fail } from "../output.js";
16
+ const SHOW_FLAGS = [];
17
+ const SHOW_BOOLS = [];
18
+ const LIST_FLAGS = [];
19
+ const LIST_BOOLS = [];
20
+ const USE_FLAGS = [];
21
+ const USE_BOOLS = [];
22
+ const ADD_FLAGS = ["api-key"];
23
+ const ADD_BOOLS = [];
24
+ const RM_FLAGS = [];
25
+ const RM_BOOLS = [];
26
+ export const configHelp = `homespun config — show and manage the CLI config (multi-profile)
27
+
28
+ Usage:
29
+ homespun config show Show the resolved relay config
30
+ homespun config list List saved profiles
31
+ homespun config use <profile> Switch the active profile
32
+ homespun config add <profile> --url <u> --api-key <k>
33
+ Add a profile manually
34
+ homespun config rm <profile> Delete a profile
35
+
36
+ A profile is one (url, api_key) pair under a short name (dev, staging, prod).
37
+ Switch via 'homespun config use', '--profile <name>', or the HOMESPUN_PROFILE env var.
38
+ The active profile's (url, api_key) is what every other command sees unless
39
+ overridden by --url / --api-key or HOMESPUN_URL / HOMESPUN_API_KEY.
40
+
41
+ Run \`homespun config <verb> --help\` for verb-specific help. The full API key is
42
+ never printed; only a short masked prefix.
43
+
44
+ The config file lives at \${XDG_CONFIG_HOME:-~/.config}/homespun/config.json
45
+ (mode 0600).`;
46
+ const showHelp = `homespun config show — show the resolved relay config
47
+
48
+ Usage:
49
+ homespun config show [options]
50
+
51
+ Reports the (url, api_key) the CLI would use right now, and where each value
52
+ came from (flag / env / profile / none). Purely inspects flags + env + the
53
+ saved config file; makes NO network call.
54
+
55
+ The API key is never printed in full — only a short masked prefix.
56
+
57
+ Options:
58
+ --url <url> Relay base URL (overrides HOMESPUN_URL) — affects the report.
59
+ --api-key <key> Agent API key (overrides HOMESPUN_API_KEY) — affects the report.
60
+ --profile <name> Profile to resolve against — affects the report.
61
+ -h, --help Show this help.
62
+
63
+ Output (stdout, JSON):
64
+ {
65
+ url, url_source, flag | env | profile | none
66
+ key_prefix, key_source, flag | env | profile | none
67
+ profile, profile_source, active profile name + how it was chosen
68
+ config_path
69
+ }`;
70
+ const listHelp = `homespun config list — list saved profiles
71
+
72
+ Usage:
73
+ homespun config list [options]
74
+
75
+ Prints every profile in the local config file, with its URL and a masked
76
+ key prefix. The active profile carries 'current: true'.
77
+
78
+ Options:
79
+ -h, --help Show this help.
80
+
81
+ Output (stdout, JSON):
82
+ {
83
+ current: <name|null>,
84
+ profiles: [ { name, url, key_prefix, current }, … ],
85
+ config_path
86
+ }`;
87
+ const useHelp = `homespun config use <profile> — switch the active profile
88
+
89
+ Usage:
90
+ homespun config use <profile>
91
+
92
+ Sets 'current_profile' in the config file. The named profile must exist
93
+ (create it first with 'homespun agent register --profile <name>' or
94
+ 'homespun config add <name>').
95
+
96
+ Options:
97
+ -h, --help Show this help.
98
+
99
+ Output (stdout, JSON):
100
+ { profile, saved_to }`;
101
+ const addHelp = `homespun config add <profile> — add a profile manually
102
+
103
+ Usage:
104
+ homespun config add <profile> --url <url> --api-key <api-key>
105
+
106
+ Saves a (url, api_key) pair under <profile> without contacting the relay.
107
+ Use this when an operator handed you an API key out of band (e.g. a closed-
108
+ registration relay) — for self-register and secret-mode relays, prefer
109
+ 'homespun agent register --profile <name>'.
110
+
111
+ If <profile> already exists, the existing values are overwritten.
112
+
113
+ Options:
114
+ --url <url> Relay base URL. REQUIRED.
115
+ --api-key <key> Agent API key. REQUIRED.
116
+ -h, --help Show this help.
117
+
118
+ Output (stdout, JSON):
119
+ { profile, saved_to }
120
+
121
+ Does NOT change 'current_profile' unless this is the first profile being
122
+ added. Use 'homespun config use' afterwards to switch.`;
123
+ const rmHelp = `homespun config rm <profile> — delete a profile
124
+
125
+ Usage:
126
+ homespun config rm <profile>
127
+
128
+ Removes the named profile from the config file. If it was the active profile,
129
+ 'current_profile' is cleared (the next command falls back to env / default
130
+ URL until another profile is selected via --profile or 'homespun config use').
131
+
132
+ Options:
133
+ -h, --help Show this help.
134
+
135
+ Output (stdout, JSON):
136
+ { profile, was_current, path }`;
137
+ async function runConfigShow(args) {
138
+ assertKnownFlags(args, SHOW_FLAGS, SHOW_BOOLS, "homespun config show");
139
+ printJson(describeConfig(args));
140
+ }
141
+ function maskKey(key) {
142
+ if (!key)
143
+ return null;
144
+ if (key.startsWith("hs_") && key.length >= 9)
145
+ return key.slice(0, 9) + "…";
146
+ return key.slice(0, 8) + "…";
147
+ }
148
+ async function runConfigList(args) {
149
+ assertKnownFlags(args, LIST_FLAGS, LIST_BOOLS, "homespun config list");
150
+ const store = readStore();
151
+ const profiles = Object.entries(store.profiles)
152
+ .map(([name, p]) => ({
153
+ name,
154
+ url: p.url ?? null,
155
+ key_prefix: maskKey(p.apiKey),
156
+ current: name === store.currentProfile,
157
+ }))
158
+ .sort((a, b) => a.name.localeCompare(b.name));
159
+ printJson({
160
+ current: store.currentProfile ?? null,
161
+ profiles,
162
+ config_path: storePath(),
163
+ });
164
+ }
165
+ async function runConfigUse(args) {
166
+ assertKnownFlags(args, USE_FLAGS, USE_BOOLS, "homespun config use");
167
+ const name = args.positionals[1];
168
+ if (!name) {
169
+ fail("missing profile name — usage: homespun config use <profile>", "invalid_args");
170
+ }
171
+ let savedTo;
172
+ try {
173
+ savedTo = setCurrentProfile(name);
174
+ }
175
+ catch (e) {
176
+ fail(e instanceof Error ? e.message : String(e), "config_error");
177
+ }
178
+ printJson({ profile: name, saved_to: savedTo });
179
+ }
180
+ async function runConfigAdd(args) {
181
+ assertKnownFlags(args, ADD_FLAGS, ADD_BOOLS, "homespun config add");
182
+ const name = args.positionals[1];
183
+ if (!name) {
184
+ fail("missing profile name — usage: homespun config add <profile> --url <url> --api-key <key>", "invalid_args");
185
+ }
186
+ if (!isValidProfileName(name)) {
187
+ fail(`invalid profile name '${name}' — letters, digits, _ and -, up to 32 chars`, "invalid_args");
188
+ }
189
+ const url = args.flags.get("url");
190
+ const apiKey = args.flags.get("api-key");
191
+ if (!url) {
192
+ fail("--url is required — usage: homespun config add <profile> --url <url> --api-key <key>", "invalid_args");
193
+ }
194
+ if (!apiKey) {
195
+ fail("--api-key is required — usage: homespun config add <profile> --url <url> --api-key <key>", "invalid_args");
196
+ }
197
+ // setCurrent=false: adding a profile shouldn't silently switch the user
198
+ // off whatever they were on. They use `homespun config use` after to flip.
199
+ // EXCEPT: if there's no current profile yet (first add), upsertProfile
200
+ // sets it automatically — that's the correct fresh-install behaviour.
201
+ const savedTo = upsertProfile(name, { url: url.replace(/\/$/, ""), apiKey }, false);
202
+ printJson({ profile: name, saved_to: savedTo });
203
+ }
204
+ async function runConfigRm(args) {
205
+ assertKnownFlags(args, RM_FLAGS, RM_BOOLS, "homespun config rm");
206
+ const name = args.positionals[1];
207
+ if (!name) {
208
+ fail("missing profile name — usage: homespun config rm <profile>", "invalid_args");
209
+ }
210
+ let result;
211
+ try {
212
+ result = removeProfile(name);
213
+ }
214
+ catch (e) {
215
+ fail(e instanceof Error ? e.message : String(e), "config_error");
216
+ }
217
+ printJson({
218
+ profile: name,
219
+ was_current: result.was_current,
220
+ path: result.path,
221
+ });
222
+ }
223
+ export async function runConfig(args) {
224
+ const verb = args.positionals[0];
225
+ // Per-verb --help: 'homespun config show --help' etc. Caught before dispatch
226
+ // so each runner doesn't need to repeat the check.
227
+ if (args.bools.has("help") && verb !== undefined) {
228
+ const helps = {
229
+ show: showHelp,
230
+ list: listHelp,
231
+ use: useHelp,
232
+ add: addHelp,
233
+ rm: rmHelp,
234
+ };
235
+ if (helps[verb] !== undefined) {
236
+ process.stdout.write(helps[verb] + "\n");
237
+ return;
238
+ }
239
+ }
240
+ switch (verb) {
241
+ case "show":
242
+ await runConfigShow(args);
243
+ break;
244
+ case "list":
245
+ await runConfigList(args);
246
+ break;
247
+ case "use":
248
+ await runConfigUse(args);
249
+ break;
250
+ case "add":
251
+ await runConfigAdd(args);
252
+ break;
253
+ case "rm":
254
+ await runConfigRm(args);
255
+ break;
256
+ case undefined:
257
+ fail("missing verb — usage: homespun config <show|list|use|add|rm> (run 'homespun config --help')", "invalid_args");
258
+ break;
259
+ default:
260
+ fail(`unknown config verb '${verb}' — expected show|list|use|add|rm (run 'homespun config --help')`, "invalid_args");
261
+ }
262
+ }
@@ -0,0 +1,168 @@
1
+ // `homespun data` — collection row CRUD for a v2 app (spec-cli §3.3). Mirrors
2
+ // `homespun records` (v1 app collections) but against `/v1/apps/:id/collections`
3
+ // and with a dedicated per-row GET (no client-side scan — spec-cli §8 ruling
4
+ // 3 confirms the relay route is real).
5
+ import { assertKnownFlags } from "../argv.js";
6
+ import { makeClient } from "../config.js";
7
+ import { fail, failFromError, printJson } from "../output.js";
8
+ import { resolveJson } from "../input.js";
9
+ import { resolveAppId } from "../resolve-app.js";
10
+ export const dataHelp = `homespun data — collection row CRUD for a v2 app
11
+
12
+ Usage:
13
+ homespun data <app> <collection> list [--since <cursor>] [--limit <n>]
14
+ homespun data <app> <collection> get <key>
15
+ homespun data <app> <collection> upsert --data <path|json> [--key <key>]
16
+ homespun data <app> <collection> update <key> --data <path|json> [--if-match <version>]
17
+ homespun data <app> <collection> delete <key> [--if-match <version>] [--yes]
18
+
19
+ <app> accepts either the app_id or its slug. upsert is the ONLY create-shaped
20
+ verb (spec-cli §8 ruling 4): omit --key to add a new row (server-generates
21
+ the key); pass --key to ensure a row exists at that key (returns the
22
+ existing row with deduped:true on a collision, never errors).
23
+
24
+ Output (stdout, single JSON). Errors on stderr:
25
+ {"error":{"code","message"}} with non-zero exit.`;
26
+ export async function runData(args) {
27
+ const appArg = args.positionals[0];
28
+ const collection = args.positionals[1];
29
+ const verb = args.positionals[2];
30
+ if ((appArg === undefined || verb === undefined) && args.bools.has("help")) {
31
+ process.stdout.write(dataHelp + "\n");
32
+ return;
33
+ }
34
+ if (!appArg || !collection || !verb) {
35
+ fail("usage: homespun data <app> <collection> <list|get|upsert|update|delete>", "invalid_args");
36
+ }
37
+ const sub = {
38
+ positionals: args.positionals.slice(3),
39
+ flags: args.flags,
40
+ bools: args.bools,
41
+ ...(args.danglingValueFlags !== undefined
42
+ ? { danglingValueFlags: args.danglingValueFlags }
43
+ : {}),
44
+ };
45
+ switch (verb) {
46
+ case "list":
47
+ return runList(appArg, collection, sub);
48
+ case "get":
49
+ return runGet(appArg, collection, sub);
50
+ case "upsert":
51
+ return runUpsert(appArg, collection, sub);
52
+ case "update":
53
+ return runUpdate(appArg, collection, sub);
54
+ case "delete":
55
+ return runDelete(appArg, collection, sub);
56
+ default:
57
+ fail(`unknown verb '${verb}' — homespun data <app> <collection> <list|get|upsert|update|delete>`, "invalid_args");
58
+ }
59
+ }
60
+ function parseIntFlag(args, name, defaultValue, bounds = {}) {
61
+ const raw = args.flags.get(name);
62
+ if (raw === undefined)
63
+ return defaultValue;
64
+ const n = Number(raw);
65
+ if (!Number.isInteger(n))
66
+ fail(`--${name} must be an integer`, "invalid_args");
67
+ if (bounds.min !== undefined && n < bounds.min) {
68
+ fail(`--${name} must be >= ${bounds.min}`, "invalid_args");
69
+ }
70
+ if (bounds.max !== undefined && n > bounds.max) {
71
+ fail(`--${name} must be <= ${bounds.max}`, "invalid_args");
72
+ }
73
+ return n;
74
+ }
75
+ async function runList(appArg, collection, args) {
76
+ assertKnownFlags(args, ["since", "limit", "url", "api-key"], ["help"], "homespun data list");
77
+ const since = args.flags.get("since");
78
+ const limit = parseIntFlag(args, "limit", undefined, { min: 1, max: 1000 });
79
+ const client = makeClient(args);
80
+ const appId = await resolveAppId(client, appArg);
81
+ try {
82
+ printJson(await client.listAppRows(appId, collection, {
83
+ since,
84
+ ...(limit !== undefined ? { limit } : {}),
85
+ }));
86
+ }
87
+ catch (e) {
88
+ failFromError(e);
89
+ }
90
+ }
91
+ async function runGet(appArg, collection, args) {
92
+ assertKnownFlags(args, ["url", "api-key"], ["help"], "homespun data get");
93
+ const key = args.positionals[0];
94
+ if (!key) {
95
+ fail("usage: homespun data <app> <collection> get <key>", "invalid_args");
96
+ }
97
+ const client = makeClient(args);
98
+ const appId = await resolveAppId(client, appArg);
99
+ try {
100
+ printJson(await client.getAppRow(appId, collection, key));
101
+ }
102
+ catch (e) {
103
+ failFromError(e);
104
+ }
105
+ }
106
+ async function runUpsert(appArg, collection, args) {
107
+ assertKnownFlags(args, ["data", "key", "url", "api-key"], ["help"], "homespun data upsert");
108
+ const dataRaw = args.flags.get("data");
109
+ if (dataRaw === undefined) {
110
+ fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
111
+ }
112
+ const data = resolveJson(dataRaw, "--data");
113
+ const key = args.flags.get("key");
114
+ const client = makeClient(args);
115
+ const appId = await resolveAppId(client, appArg);
116
+ try {
117
+ const body = { data };
118
+ if (key !== undefined)
119
+ body.key = key;
120
+ printJson(await client.upsertAppRow(appId, collection, body));
121
+ }
122
+ catch (e) {
123
+ failFromError(e);
124
+ }
125
+ }
126
+ async function runUpdate(appArg, collection, args) {
127
+ assertKnownFlags(args, ["data", "if-match", "url", "api-key"], ["help"], "homespun data update");
128
+ const key = args.positionals[0];
129
+ if (!key) {
130
+ fail("usage: homespun data <app> <collection> update <key> --data <path|json>", "invalid_args");
131
+ }
132
+ const dataRaw = args.flags.get("data");
133
+ if (dataRaw === undefined) {
134
+ fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
135
+ }
136
+ const data = resolveJson(dataRaw, "--data");
137
+ const ifMatch = parseIntFlag(args, "if-match", undefined, { min: 0 });
138
+ const client = makeClient(args);
139
+ const appId = await resolveAppId(client, appArg);
140
+ try {
141
+ const body = { data };
142
+ if (ifMatch !== undefined)
143
+ body.if_match = ifMatch;
144
+ printJson(await client.updateAppRow(appId, collection, key, body));
145
+ }
146
+ catch (e) {
147
+ failFromError(e);
148
+ }
149
+ }
150
+ async function runDelete(appArg, collection, args) {
151
+ assertKnownFlags(args, ["if-match", "url", "api-key"], ["yes", "help"], "homespun data delete");
152
+ const key = args.positionals[0];
153
+ if (!key) {
154
+ fail("usage: homespun data <app> <collection> delete <key>", "invalid_args");
155
+ }
156
+ const ifMatch = parseIntFlag(args, "if-match", undefined, { min: 0 });
157
+ const client = makeClient(args);
158
+ const appId = await resolveAppId(client, appArg);
159
+ try {
160
+ await client.deleteAppRow(appId, collection, key, {
161
+ ...(ifMatch !== undefined ? { ifMatch } : {}),
162
+ });
163
+ printJson({ deleted: true, key });
164
+ }
165
+ catch (e) {
166
+ failFromError(e);
167
+ }
168
+ }
@@ -0,0 +1,136 @@
1
+ // `homespun deploy` — create or redeploy an App (spec-cli §3.1). This is the
2
+ // create->redeploy loop the v2 vision names: no `--app` creates a new App;
3
+ // `--app <id>` redeploys an existing one (compat-gated unless --force).
4
+ import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { makeClient } from "../config.js";
7
+ import { assertKnownFlags } from "../argv.js";
8
+ import { fail, failFromError, printJson } from "../output.js";
9
+ import { resolveJson } from "../input.js";
10
+ import { resolveAppId } from "../resolve-app.js";
11
+ const KNOWN_FLAGS = ["app", "manifest", "slug", "visibility"];
12
+ const KNOWN_BOOLS = ["force"];
13
+ export const deployHelp = `homespun deploy — create or redeploy an app
14
+
15
+ Usage:
16
+ homespun deploy <dir|file> [--app <id>] [--manifest <path|json>]
17
+ [--slug <slug>] [--visibility private|link|public] [--force]
18
+
19
+ Packaging (one canonical shape, one escape hatch):
20
+ Directory (canonical): homespun deploy ./my-app
21
+ Reads ./my-app/index.html and ./my-app/manifest.json — fixed filenames,
22
+ no discovery heuristics. Both files are required.
23
+ Single file (escape hatch): homespun deploy ./index.html --manifest ./manifest.json
24
+ --manifest accepts a file path OR inline JSON.
25
+
26
+ Create vs. redeploy — decided by --app's presence, not two verbs:
27
+ (no --app) Create (POST /v1/apps). New apps default to private
28
+ (owner plus invited members, sign-in gated). --slug is
29
+ accepted with private or public visibility, including the
30
+ default; an explicit --visibility link always gets a
31
+ server-generated slug and rejects --slug.
32
+ --app <id> Redeploy (POST /v1/apps/:id/versions). --slug/--visibility
33
+ are rejected here (slug is immutable; change visibility via
34
+ 'homespun apps update'). --force overrides the compat gate.
35
+
36
+ Output: { app_id, slug, url, version, visibility, created, compat?, breaks? }
37
+ Errors on stderr: {"error":{"code","message"}} with non-zero exit.`;
38
+ function readBundle(source, manifestFlag) {
39
+ if (!existsSync(source)) {
40
+ fail(`no such file or directory: ${source}`, "invalid_args");
41
+ }
42
+ const st = statSync(source);
43
+ if (st.isDirectory()) {
44
+ if (manifestFlag !== undefined) {
45
+ fail("--manifest is only for the single-file escape hatch — a directory deploy reads <dir>/manifest.json", "invalid_args");
46
+ }
47
+ const htmlPath = join(source, "index.html");
48
+ const manifestPath = join(source, "manifest.json");
49
+ const missing = [];
50
+ if (!existsSync(htmlPath))
51
+ missing.push("index.html");
52
+ if (!existsSync(manifestPath))
53
+ missing.push("manifest.json");
54
+ if (missing.length > 0) {
55
+ fail(`directory deploy is missing required file(s): ${missing.join(", ")}`, "invalid_args");
56
+ }
57
+ return {
58
+ html: readFileSync(htmlPath, "utf8"),
59
+ manifest: JSON.parse(readFileSync(manifestPath, "utf8")),
60
+ };
61
+ }
62
+ // Single-file escape hatch.
63
+ if (manifestFlag === undefined) {
64
+ fail("single-file deploy requires --manifest <path|json>", "invalid_args");
65
+ }
66
+ return {
67
+ html: readFileSync(source, "utf8"),
68
+ manifest: resolveJson(manifestFlag, "--manifest"),
69
+ };
70
+ }
71
+ export async function runDeploy(args) {
72
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "homespun deploy");
73
+ const source = args.positionals[0];
74
+ if (!source) {
75
+ fail("usage: homespun deploy <dir|file> [--app <id>] ...", "invalid_args");
76
+ }
77
+ const appId = args.flags.get("app");
78
+ const slug = args.flags.get("slug");
79
+ const visibility = args.flags.get("visibility");
80
+ if (visibility !== undefined &&
81
+ !["private", "link", "public"].includes(visibility)) {
82
+ fail("--visibility must be private|link|public", "invalid_args");
83
+ }
84
+ const force = args.bools.has("force");
85
+ const bundle = readBundle(source, args.flags.get("manifest"));
86
+ const client = makeClient(args);
87
+ if (appId === undefined) {
88
+ // Create. Client-side mirror of the relay's slug_not_allowed_for_link —
89
+ // fail fast rather than round-trip a request that will 400 (spec-cli §3.1).
90
+ if (slug !== undefined && visibility === "link") {
91
+ 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");
92
+ }
93
+ try {
94
+ const out = await client.deployApp({
95
+ html: bundle.html,
96
+ manifest: bundle.manifest,
97
+ visibility,
98
+ slug,
99
+ });
100
+ printJson(out);
101
+ }
102
+ catch (e) {
103
+ failFromError(e);
104
+ }
105
+ return;
106
+ }
107
+ // Redeploy. slug/visibility are immutable here.
108
+ if (slug !== undefined) {
109
+ fail("--slug cannot be changed on redeploy (slug is immutable) — omit --app to create a new app instead", "invalid_args");
110
+ }
111
+ if (visibility !== undefined) {
112
+ fail("--visibility cannot be changed on redeploy — use 'homespun apps update --visibility' instead", "invalid_args");
113
+ }
114
+ const id = await resolveAppId(client, appId);
115
+ try {
116
+ const redeployed = await client.redeployApp(id, {
117
+ html: bundle.html,
118
+ manifest: bundle.manifest,
119
+ force,
120
+ });
121
+ const app = await client.getApp(id);
122
+ printJson({
123
+ app_id: redeployed.app_id,
124
+ slug: app.slug,
125
+ url: app.url,
126
+ version: redeployed.version,
127
+ visibility: app.visibility,
128
+ created: false,
129
+ compat: redeployed.compat,
130
+ ...(redeployed.breaks ? { breaks: redeployed.breaks } : {}),
131
+ });
132
+ }
133
+ catch (e) {
134
+ failFromError(e);
135
+ }
136
+ }