@homespunapps/cli 1.6.48 → 1.6.49
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/dist/argv.js +4 -0
- package/dist/commands/config.js +6 -2
- package/dist/commands/connection.js +3 -2
- package/dist/commands/ingest.js +2 -1
- package/dist/commands/template.js +66 -2
- package/dist/help-catalog.js +45 -9
- package/dist/input.js +56 -1
- package/package.json +3 -3
package/dist/argv.js
CHANGED
|
@@ -63,6 +63,10 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
63
63
|
// `homespun template publish --attest-example-only`: attest the captured template
|
|
64
64
|
// and its seed rows carry no real personal data (marketplace PR 10).
|
|
65
65
|
"attest-example-only",
|
|
66
|
+
// `homespun template upgrade --accept-permissions`: accept that the new version
|
|
67
|
+
// of a template asks for more than the installed one (#1502). Required when it
|
|
68
|
+
// does; it never clears an update that would strand rows.
|
|
69
|
+
"accept-permissions",
|
|
66
70
|
// `homespun review respond --clear`: clear a publisher response (sends null).
|
|
67
71
|
"clear",
|
|
68
72
|
// `homespun credentials mint --no-expiry`: the explicit opt-in to NO EXPIRY
|
package/dist/commands/config.js
CHANGED
|
@@ -14,6 +14,7 @@ import { specFor } from "../help-catalog.js";
|
|
|
14
14
|
import { describeConfig } from "../config.js";
|
|
15
15
|
import { isValidProfileName, readStore, removeProfile, setCurrentProfile, storePath, upsertProfile, } from "../store.js";
|
|
16
16
|
import { printJson, fail } from "../output.js";
|
|
17
|
+
import { resolveSecretFlag } from "../input.js";
|
|
17
18
|
const showHelp = `homespun config show — show the resolved relay config
|
|
18
19
|
|
|
19
20
|
Usage:
|
|
@@ -83,7 +84,10 @@ If <profile> already exists, the existing values are overwritten.
|
|
|
83
84
|
|
|
84
85
|
Options:
|
|
85
86
|
--url <url> Relay base URL. REQUIRED.
|
|
86
|
-
--api-key <key
|
|
87
|
+
--api-key <key|-> Agent API key. REQUIRED.
|
|
88
|
+
Pass - to read it from stdin, or set
|
|
89
|
+
HOMESPUN_CONFIG_API_KEY, instead of putting it on
|
|
90
|
+
the command line where ps and shell history can see it.
|
|
87
91
|
-h, --help Show this help.
|
|
88
92
|
|
|
89
93
|
Output (stdout, JSON):
|
|
@@ -158,7 +162,7 @@ async function runConfigAdd(args) {
|
|
|
158
162
|
fail(`invalid profile name '${name}' — letters, digits, _ and -, up to 32 chars`, "invalid_args");
|
|
159
163
|
}
|
|
160
164
|
const url = args.flags.get("url");
|
|
161
|
-
const apiKey = args.flags.get("api-key");
|
|
165
|
+
const apiKey = await resolveSecretFlag(args.flags.get("api-key"), "HOMESPUN_CONFIG_API_KEY", "--api-key");
|
|
162
166
|
if (!url) {
|
|
163
167
|
fail("--url is required — usage: homespun config add <profile> --url <url> --api-key <key>", "invalid_args");
|
|
164
168
|
}
|
|
@@ -18,6 +18,7 @@ import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
|
|
|
18
18
|
import { makeClient } from "../config.js";
|
|
19
19
|
import { fail, failFromError, printJson } from "../output.js";
|
|
20
20
|
import { resolveAppId } from "../resolve-app.js";
|
|
21
|
+
import { resolveSecretFlag } from "../input.js";
|
|
21
22
|
export async function runConnection(args) {
|
|
22
23
|
const verb = args.positionals[0];
|
|
23
24
|
if ((verb === undefined || verb === "help") && args.bools.has("help")) {
|
|
@@ -88,7 +89,7 @@ async function runCreate(args) {
|
|
|
88
89
|
const authorizeUrl = args.flags.get("authorize-url");
|
|
89
90
|
const tokenEndpoint = args.flags.get("token-url");
|
|
90
91
|
const clientId = args.flags.get("client-id");
|
|
91
|
-
const clientSecret = args.flags.get("client-secret");
|
|
92
|
+
const clientSecret = await resolveSecretFlag(args.flags.get("client-secret"), "HOMESPUN_CONNECTION_CLIENT_SECRET", "--client-secret");
|
|
92
93
|
if (!authorizeUrl || !tokenEndpoint || !clientId || !clientSecret) {
|
|
93
94
|
fail("kind=oauth2 requires --authorize-url, --token-url, --client-id and --client-secret", "invalid_args");
|
|
94
95
|
}
|
|
@@ -126,7 +127,7 @@ async function runCreate(args) {
|
|
|
126
127
|
}));
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
|
-
const headerValue = args.flags.get("header-value");
|
|
130
|
+
const headerValue = await resolveSecretFlag(args.flags.get("header-value"), "HOMESPUN_CONNECTION_HEADER_VALUE", "--header-value");
|
|
130
131
|
if (!headerValue) {
|
|
131
132
|
fail("--header-value is required for a static connection", "invalid_args");
|
|
132
133
|
}
|
package/dist/commands/ingest.js
CHANGED
|
@@ -20,6 +20,7 @@ import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
|
|
|
20
20
|
import { makeClient } from "../config.js";
|
|
21
21
|
import { fail, failFromError, printJson } from "../output.js";
|
|
22
22
|
import { resolveAppId } from "../resolve-app.js";
|
|
23
|
+
import { resolveSecretFlag } from "../input.js";
|
|
23
24
|
export async function runIngest(args) {
|
|
24
25
|
const verb = args.positionals[0];
|
|
25
26
|
if ((verb === undefined || verb === "help") && args.bools.has("help")) {
|
|
@@ -125,7 +126,7 @@ async function runSigningSecretSet(args) {
|
|
|
125
126
|
if (!appArg || !name) {
|
|
126
127
|
fail("usage: homespun ingest signing-secret set --app <idOrSlug> --name <hookName> [--secret <value>] [--grace-seconds <n>]", "invalid_args");
|
|
127
128
|
}
|
|
128
|
-
const secret = args.flags.get("secret");
|
|
129
|
+
const secret = await resolveSecretFlag(args.flags.get("secret"), "HOMESPUN_INGEST_SIGNING_SECRET", "--secret");
|
|
129
130
|
const graceRaw = args.flags.get("grace-seconds");
|
|
130
131
|
let graceSeconds;
|
|
131
132
|
if (graceRaw !== undefined) {
|
|
@@ -19,7 +19,7 @@ export async function runTemplate(args) {
|
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
if (verb === undefined) {
|
|
22
|
-
fail("missing verb: homespun template <publish|unpublish|config-contract|install|list-pending|show|approve|reject>", "invalid_args");
|
|
22
|
+
fail("missing verb: homespun template <publish|unpublish|config-contract|install|upgrade-check|upgrade|revert|list-pending|show|approve|reject>", "invalid_args");
|
|
23
23
|
}
|
|
24
24
|
const sub = {
|
|
25
25
|
positionals: args.positionals.slice(1),
|
|
@@ -38,6 +38,12 @@ export async function runTemplate(args) {
|
|
|
38
38
|
return runConfigContract(sub);
|
|
39
39
|
case "install":
|
|
40
40
|
return runInstall(sub);
|
|
41
|
+
case "upgrade-check":
|
|
42
|
+
return runUpgradeCheck(sub);
|
|
43
|
+
case "upgrade":
|
|
44
|
+
return runUpgrade(sub);
|
|
45
|
+
case "revert":
|
|
46
|
+
return runRevert(sub);
|
|
41
47
|
case "list-pending":
|
|
42
48
|
return runListPending(sub);
|
|
43
49
|
case "show":
|
|
@@ -47,7 +53,7 @@ export async function runTemplate(args) {
|
|
|
47
53
|
case "reject":
|
|
48
54
|
return runReject(sub);
|
|
49
55
|
default:
|
|
50
|
-
fail(`unknown verb '${verb}' (homespun template <publish|unpublish|config-contract|install|list-pending|show|approve|reject>)`, "invalid_args");
|
|
56
|
+
fail(`unknown verb '${verb}' (homespun template <publish|unpublish|config-contract|install|upgrade-check|upgrade|revert|list-pending|show|approve|reject>)`, "invalid_args");
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
function parseIntFlag(args, name, bounds = {}) {
|
|
@@ -244,3 +250,61 @@ async function runReject(args) {
|
|
|
244
250
|
failFromError(e);
|
|
245
251
|
}
|
|
246
252
|
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// upgrade-check / upgrade / revert (#1502)
|
|
255
|
+
//
|
|
256
|
+
// These take an APP, not a template ref. An installed template is a fork, so
|
|
257
|
+
// the question is "is there a newer version of the template THIS app came
|
|
258
|
+
// from", which only the app can answer.
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
async function runUpgradeCheck(args) {
|
|
261
|
+
assertKnownFlags(args, ...specFor("template", "upgrade-check"));
|
|
262
|
+
const appId = args.positionals[0];
|
|
263
|
+
if (!appId) {
|
|
264
|
+
fail("usage: homespun template upgrade-check <app>", "invalid_args");
|
|
265
|
+
}
|
|
266
|
+
const client = makeClient(args);
|
|
267
|
+
try {
|
|
268
|
+
printJson(await client.checkTemplateUpgrade(appId));
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
failFromError(e);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function runUpgrade(args) {
|
|
275
|
+
assertKnownFlags(args, ...specFor("template", "upgrade"));
|
|
276
|
+
const appId = args.positionals[0];
|
|
277
|
+
if (!appId) {
|
|
278
|
+
fail("usage: homespun template upgrade <app>", "invalid_args");
|
|
279
|
+
}
|
|
280
|
+
const client = makeClient(args);
|
|
281
|
+
const expectVersion = args.flags.get("expect-version");
|
|
282
|
+
try {
|
|
283
|
+
printJson(await client.upgradeTemplate(appId, {
|
|
284
|
+
// Only ever true when the flag is present. An omitted flag must not
|
|
285
|
+
// satisfy the consent gate: the point of the gate is that somebody
|
|
286
|
+
// looked at what the new version is asking for.
|
|
287
|
+
...(args.bools.has("accept-permissions")
|
|
288
|
+
? { acceptPermissions: true }
|
|
289
|
+
: {}),
|
|
290
|
+
...(expectVersion !== undefined ? { expectVersion } : {}),
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
failFromError(e);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function runRevert(args) {
|
|
298
|
+
assertKnownFlags(args, ...specFor("template", "revert"));
|
|
299
|
+
const appId = args.positionals[0];
|
|
300
|
+
if (!appId) {
|
|
301
|
+
fail("usage: homespun template revert <app>", "invalid_args");
|
|
302
|
+
}
|
|
303
|
+
const client = makeClient(args);
|
|
304
|
+
try {
|
|
305
|
+
printJson(await client.revertTemplateUpgrade(appId));
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
failFromError(e);
|
|
309
|
+
}
|
|
310
|
+
}
|
package/dist/help-catalog.js
CHANGED
|
@@ -477,8 +477,8 @@ const INGEST = {
|
|
|
477
477
|
},
|
|
478
478
|
{
|
|
479
479
|
name: "secret",
|
|
480
|
-
value: "<value
|
|
481
|
-
description: "set only: a provider-generated signing secret to store verbatim; omit to have the relay mint one (shown once)",
|
|
480
|
+
value: "<value|->",
|
|
481
|
+
description: "set only: a provider-generated signing secret to store verbatim; omit to have the relay mint one (shown once). Pass - to read it from stdin, or set HOMESPUN_INGEST_SIGNING_SECRET, instead of putting it on the command line where ps and shell history can see it",
|
|
482
482
|
},
|
|
483
483
|
{
|
|
484
484
|
name: "grace-seconds",
|
|
@@ -521,6 +521,7 @@ const INGEST = {
|
|
|
521
521
|
"signing-secret manages a hook's OPT-IN signing secret, distinct from the URL secret above: it is what a provider (GitHub, Stripe, ...) HMACs the request body with. `set` without --secret mints one and returns { secret, fingerprint, setAt } with the value shown ONCE; `set --secret <value>` stores a provider-generated value verbatim and returns { fingerprint, setAt } without echoing it; `clear` removes it. A rotation keeps the previous secret valid for --grace-seconds so deliveries verify while you update the provider. A hook that declares `verify` in its manifest rule (GitHub scheme in v1) requires a valid signature over the raw body and stays fail-closed (401) until this secret is set; the fingerprint (a plaintext-derived id) lets you confirm which secret is set without the relay ever showing it.",
|
|
522
522
|
"Hooks are declared in the app manifest (x-homespun-manifest.ingest) and materialized at deploy, so there is no create or delete verb here: add or remove a hook by editing the manifest and redeploying.",
|
|
523
523
|
"backfill seeds a hook's collection with historical data: it POSTs an array of raw provider bodies to the OWNER endpoint (POST /v1/apps/:id/ingest-hooks/:name/backfill) and runs each through the SAME mapping the live public URL uses, so a backfilled row is byte-identical to a live delivery. It reads a JSON-array or NDJSON --file (each entry is a whole provider payload, any JSON value, not necessarily an object) and chunks it into --chunk bodies per call (default 500). It reuses the receive pipeline, so map/dedupeKey/upsertOn/row-schema validation and the collection quota all apply, but it SKIPS the public-URL brakes (the per-IP rate limit and the per-app hourly cap) and never verifies a signature (you are the authenticated owner). Wake is suppressed, so a large historical load never wakes a dormant app. Dedupe is ON: re-running the same file is idempotent for a body-path dedupeKey; a header:<name> dedupeKey cannot resolve here (no request headers), so it does not dedupe. Prints aggregate { total, accepted, dropped_duplicate, failed } counts.",
|
|
524
|
+
"--secret on signing-secret set takes the value straight from argv where it is visible in shell history and to other local users via ps for the life of the process. Prefer '--secret -' to read it from stdin, or set HOMESPUN_INGEST_SIGNING_SECRET, both of which never touch argv.",
|
|
524
525
|
],
|
|
525
526
|
};
|
|
526
527
|
const GRANTS = {
|
|
@@ -792,8 +793,8 @@ const CONNECTIONS = {
|
|
|
792
793
|
},
|
|
793
794
|
{
|
|
794
795
|
name: "header-value",
|
|
795
|
-
value: "<value
|
|
796
|
-
description: 'static only, required. The header value to send, e.g. "Bearer sk_live_..."',
|
|
796
|
+
value: "<value|->",
|
|
797
|
+
description: 'static only, required. The header value to send, e.g. "Bearer sk_live_...". Pass - to read it from stdin, or set HOMESPUN_CONNECTION_HEADER_VALUE, instead of putting it on the command line where ps and shell history can see it',
|
|
797
798
|
},
|
|
798
799
|
{
|
|
799
800
|
name: "authorize-url",
|
|
@@ -812,8 +813,8 @@ const CONNECTIONS = {
|
|
|
812
813
|
},
|
|
813
814
|
{
|
|
814
815
|
name: "client-secret",
|
|
815
|
-
value: "<secret
|
|
816
|
-
description: "oauth2 only, required. Your OAuth2 app's client secret",
|
|
816
|
+
value: "<secret|->",
|
|
817
|
+
description: "oauth2 only, required. Your OAuth2 app's client secret. Pass - to read it from stdin, or set HOMESPUN_CONNECTION_CLIENT_SECRET, instead of putting it on the command line where ps and shell history can see it",
|
|
817
818
|
},
|
|
818
819
|
{
|
|
819
820
|
name: "scopes",
|
|
@@ -891,6 +892,7 @@ const CONNECTIONS = {
|
|
|
891
892
|
"A connection is the stored credential a manifest webhook rule authenticates its delivery target with, bound to a host so it can never be sent to another one. There is no update verb: change a connection by deleting and recreating it.",
|
|
892
893
|
"Every stored secret (a static header value, or an oauth2 client secret and its tokens) is encrypted at rest and never returned by any call; list returns metadata plus a non-reversible fingerprint only.",
|
|
893
894
|
"OAuth2 consent is inherently a human-in-a-browser step: the relay refuses an agent-key caller at the authorize endpoint. authorize-url never makes a network call, it builds the URL locally so you can hand it to the signed-in app owner to open. A newly created oauth2 connection starts in pending_auth until the owner completes it.",
|
|
895
|
+
"--header-value and --client-secret on create take the value straight from argv where it is visible in shell history and to other local users via ps for the life of the process. Prefer '--header-value -' / '--client-secret -' to read the value from stdin, or set HOMESPUN_CONNECTION_HEADER_VALUE / HOMESPUN_CONNECTION_CLIENT_SECRET, both of which never touch argv.",
|
|
894
896
|
],
|
|
895
897
|
};
|
|
896
898
|
const KEY = {
|
|
@@ -1035,8 +1037,8 @@ const CONFIG = {
|
|
|
1035
1037
|
flags: [
|
|
1036
1038
|
{
|
|
1037
1039
|
name: "api-key",
|
|
1038
|
-
value: "<key
|
|
1039
|
-
description: "Agent API key to save in the profile, required",
|
|
1040
|
+
value: "<key|->",
|
|
1041
|
+
description: "Agent API key to save in the profile, required. Pass - to read it from stdin, or set HOMESPUN_CONFIG_API_KEY, instead of putting it on the command line where ps and shell history can see it",
|
|
1040
1042
|
},
|
|
1041
1043
|
],
|
|
1042
1044
|
},
|
|
@@ -1050,6 +1052,7 @@ const CONFIG = {
|
|
|
1050
1052
|
"A profile is one url and api_key pair under a short name (dev, staging, prod). Switch via 'homespun config use', --profile <name>, or the HOMESPUN_PROFILE env var. The active profile is what every other command sees unless overridden by --url, --api-key, HOMESPUN_URL or HOMESPUN_API_KEY.",
|
|
1051
1053
|
"Every verb is purely local: it inspects flags, env, and the saved config file and makes no network call. The full API key is never printed, only a short masked prefix. The config file lives at ${XDG_CONFIG_HOME:-~/.config}/homespun/config.json (mode 0600).",
|
|
1052
1054
|
"add requires both --url and --api-key, and overwrites the existing values if the profile already exists. Use it when an operator handed you an API key out of band, for example a closed-registration relay; for self-register and secret-mode relays prefer 'homespun agent register --profile <name>'. It does not change current_profile unless it is the first profile added, so run 'homespun config use' afterwards to switch. rm clears current_profile when it removes the active profile, and the next command falls back to env or the default URL until another profile is selected.",
|
|
1055
|
+
"--api-key on add takes the value straight from argv where it is visible in shell history and to other local users via ps for the life of the process. Prefer '--api-key -' to read it from stdin, or set HOMESPUN_CONFIG_API_KEY, both of which never touch argv.",
|
|
1053
1056
|
],
|
|
1054
1057
|
};
|
|
1055
1058
|
const SKILL = {
|
|
@@ -1367,7 +1370,7 @@ const TEMPLATE = {
|
|
|
1367
1370
|
noun: "template",
|
|
1368
1371
|
tagline: "community marketplace templates",
|
|
1369
1372
|
group: "other",
|
|
1370
|
-
rootSummary: "Community marketplace templates: publish an owned app, unpublish your own listing, read a template's config-contract, install one, and the operator review queue (list-pending, show, approve, reject).",
|
|
1373
|
+
rootSummary: "Community marketplace templates: publish an owned app, unpublish your own listing, read a template's config-contract, install one, keep an installed app current (upgrade-check, upgrade, revert), and the operator review queue (list-pending, show, approve, reject).",
|
|
1371
1374
|
verbs: [
|
|
1372
1375
|
{
|
|
1373
1376
|
verb: "publish",
|
|
@@ -1449,6 +1452,39 @@ const TEMPLATE = {
|
|
|
1449
1452
|
},
|
|
1450
1453
|
],
|
|
1451
1454
|
},
|
|
1455
|
+
{
|
|
1456
|
+
verb: "upgrade-check",
|
|
1457
|
+
positionals: "<app>",
|
|
1458
|
+
summary: "Reports whether a newer version of the app's source template is available.",
|
|
1459
|
+
flags: [],
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
verb: "upgrade",
|
|
1463
|
+
positionals: "<app>",
|
|
1464
|
+
summary: "Updates an app in place to its template's current version, keeping its data.",
|
|
1465
|
+
flags: [
|
|
1466
|
+
{
|
|
1467
|
+
name: "expect-version",
|
|
1468
|
+
value: "<semver>",
|
|
1469
|
+
description: "Refuse unless this is still the version on offer, so a republish mid-flight cannot slip through",
|
|
1470
|
+
},
|
|
1471
|
+
],
|
|
1472
|
+
// A value-less flag belongs in `bools`, not `flags`: the catalog is what
|
|
1473
|
+
// assertKnownFlags reads, so declaring it as a value flag makes the real
|
|
1474
|
+
// parser reject `--accept-permissions` as missing its value.
|
|
1475
|
+
bools: [
|
|
1476
|
+
{
|
|
1477
|
+
name: "accept-permissions",
|
|
1478
|
+
description: "Accept a version that asks for more than the installed one; required when it does",
|
|
1479
|
+
},
|
|
1480
|
+
],
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
verb: "revert",
|
|
1484
|
+
positionals: "<app>",
|
|
1485
|
+
summary: "Puts an app back on the version it ran before its last template update.",
|
|
1486
|
+
flags: [],
|
|
1487
|
+
},
|
|
1452
1488
|
{
|
|
1453
1489
|
verb: "list-pending",
|
|
1454
1490
|
summary: "Lists pending submissions in the review queue (operator only).",
|
package/dist/input.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Helpers for reading CLI inputs that may be either a file path or an inline
|
|
2
|
-
// literal (JSON, or raw text for an HTML template body)
|
|
2
|
+
// literal (JSON, or raw text for an HTML template body), plus a shared
|
|
3
|
+
// resolver for flags that carry a caller-supplied secret.
|
|
3
4
|
import { readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { fail, warn } from "./output.js";
|
|
4
6
|
/**
|
|
5
7
|
* True if `value` names an existing file. Only a missing path (ENOENT) is
|
|
6
8
|
* treated as "not a file" — any other fs error (EACCES, ELOOP, …) propagates
|
|
@@ -40,3 +42,56 @@ export function resolveJson(value, label) {
|
|
|
40
42
|
export function resolveText(value) {
|
|
41
43
|
return isFilePath(value) ? readFileSync(value, "utf8") : value;
|
|
42
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Drain process.stdin to a utf8 string and trim exactly one trailing
|
|
47
|
+
* newline (or CRLF) — the byte an `echo` or heredoc appends and which is
|
|
48
|
+
* never part of the secret itself. The caller is responsible for gating on
|
|
49
|
+
* `process.stdin.isTTY` first; in a TTY this blocks waiting for ^D.
|
|
50
|
+
*/
|
|
51
|
+
async function readStdinSecret() {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
for await (const chunk of process.stdin) {
|
|
54
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
55
|
+
}
|
|
56
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
57
|
+
if (raw.endsWith("\r\n"))
|
|
58
|
+
return raw.slice(0, -2);
|
|
59
|
+
if (raw.endsWith("\n"))
|
|
60
|
+
return raw.slice(0, -1);
|
|
61
|
+
return raw;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a flag that carries a caller-supplied secret (a third-party
|
|
65
|
+
* client secret, a static connection's header value, a webhook signing
|
|
66
|
+
* secret, an agent API key) — anything an operator types in, as opposed to
|
|
67
|
+
* a token the relay generates and hands back. A value on argv sits in shell
|
|
68
|
+
* history and is readable by any other local user via `ps` or
|
|
69
|
+
* /proc/<pid>/cmdline for the life of the process, so this offers two ways
|
|
70
|
+
* around that, checked in order:
|
|
71
|
+
*
|
|
72
|
+
* 1. the flag given as exactly "-": read the value from stdin, refusing
|
|
73
|
+
* when stdin is a TTY so the command fails fast instead of hanging on
|
|
74
|
+
* ^D. Mirrors `feedback create --message -`.
|
|
75
|
+
* 2. `envVar`, read only when the flag was not given at all.
|
|
76
|
+
*
|
|
77
|
+
* The flag's literal value keeps working (scripts that already pass it
|
|
78
|
+
* verbatim must not break), but that path is the exposed one, so it prints
|
|
79
|
+
* a stderr warning naming the two alternatives above. Mirrors the
|
|
80
|
+
* `--secret` / `HOMESPUN_REGISTER_SECRET` fallback in `agent register`.
|
|
81
|
+
*
|
|
82
|
+
* `flagLabel` (e.g. "--client-secret") appears only in messages.
|
|
83
|
+
*/
|
|
84
|
+
export async function resolveSecretFlag(flagValue, envVar, flagLabel) {
|
|
85
|
+
if (flagValue === "-") {
|
|
86
|
+
if (process.stdin.isTTY) {
|
|
87
|
+
fail(`'${flagLabel} -' expects the secret on stdin, but stdin is a TTY`, "invalid_args");
|
|
88
|
+
}
|
|
89
|
+
return readStdinSecret();
|
|
90
|
+
}
|
|
91
|
+
if (flagValue !== undefined) {
|
|
92
|
+
warn(`${flagLabel} was passed on the command line: it is visible in your shell history and, while this command runs, to any other user on this machine via ps or /proc. Use '${flagLabel} -' to read it from stdin instead, or set ${envVar}.`);
|
|
93
|
+
return flagValue;
|
|
94
|
+
}
|
|
95
|
+
const env = process.env[envVar];
|
|
96
|
+
return env !== undefined && env !== "" ? env : undefined;
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homespunapps/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.49",
|
|
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",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc",
|
|
34
|
-
"typecheck": "tsc --noEmit",
|
|
34
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
35
35
|
"test": "vitest run",
|
|
36
36
|
"test:unit": "vitest run"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@homespunapps/core": "^1.6.
|
|
39
|
+
"@homespunapps/core": "^1.6.49",
|
|
40
40
|
"qrcode-terminal": "^0.12.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|