@homespunapps/cli 1.6.78 → 1.6.80
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 +15 -0
- package/dist/commands/apps.js +99 -2
- package/dist/help-catalog.js +33 -2
- package/package.json +2 -2
package/dist/argv.js
CHANGED
|
@@ -75,6 +75,21 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
75
75
|
// `homespun credentials mint --members`: opt the credential into the app's
|
|
76
76
|
// member directory in its boot/hello payloads.
|
|
77
77
|
"members",
|
|
78
|
+
// `homespun apps transfer <app> --show`: print the app's pending ownership
|
|
79
|
+
// transfer instead of starting or withdrawing one. Named "show" rather than
|
|
80
|
+
// "status", which "apps list --status <value>" and "connections deliveries
|
|
81
|
+
// --status <value>" already own as a value flag under the same name.
|
|
82
|
+
"show",
|
|
83
|
+
// `homespun apps transfer <app> --cancel`: withdraw the app's pending
|
|
84
|
+
// ownership transfer.
|
|
85
|
+
"cancel",
|
|
86
|
+
// `homespun apps transfer <app> --to <email> --keep-as-member`: the
|
|
87
|
+
// recipient becomes the owner, the current owner stays on as a plain
|
|
88
|
+
// member. Default when neither this nor --remove-me is given.
|
|
89
|
+
"keep-as-member",
|
|
90
|
+
// `homespun apps transfer <app> --to <email> --remove-me`: the current
|
|
91
|
+
// owner is removed the moment the recipient accepts the transfer.
|
|
92
|
+
"remove-me",
|
|
78
93
|
]);
|
|
79
94
|
/**
|
|
80
95
|
* Flags that may be repeated, accumulating one value per occurrence instead
|
package/dist/commands/apps.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function runApps(args) {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
if (verb === undefined) {
|
|
29
|
-
fail("missing verb: homespun apps <list|show|audit|update|share-link|delete|wake|watch|domain>", "invalid_args");
|
|
29
|
+
fail("missing verb: homespun apps <list|show|audit|update|share-link|delete|wake|watch|domain|transfer>", "invalid_args");
|
|
30
30
|
}
|
|
31
31
|
const sub = {
|
|
32
32
|
positionals: args.positionals.slice(1),
|
|
@@ -61,8 +61,10 @@ export async function runApps(args) {
|
|
|
61
61
|
return runWatch(sub);
|
|
62
62
|
case "domain":
|
|
63
63
|
return runDomain(sub);
|
|
64
|
+
case "transfer":
|
|
65
|
+
return runTransfer(sub);
|
|
64
66
|
default:
|
|
65
|
-
fail(`unknown verb '${verb}': homespun apps <list|show|audit|update|share-link|delete|deleted|restore|purge|wake|watch|domain>`, "invalid_args");
|
|
67
|
+
fail(`unknown verb '${verb}': homespun apps <list|show|audit|update|share-link|delete|deleted|restore|purge|wake|watch|domain|transfer>`, "invalid_args");
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
// ---------------------------------------------------------------------------
|
|
@@ -560,3 +562,98 @@ async function runDomain(args) {
|
|
|
560
562
|
failFromError(e);
|
|
561
563
|
}
|
|
562
564
|
}
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// `homespun apps transfer <app> --to <email> [--keep-as-member|--remove-me]`
|
|
567
|
+
// `<app> --show` | `<app> --cancel` (issue #1847)
|
|
568
|
+
//
|
|
569
|
+
// Transferring an app is a two-party job, same shape as `domain` above: this
|
|
570
|
+
// side only ever mints or withdraws a PENDING transfer. Ownership does not
|
|
571
|
+
// move until the recipient accepts it by email, so the initiating path's
|
|
572
|
+
// output has to say that in plain words: a caller reading only a JSON blob
|
|
573
|
+
// back has no way to tell "the app moved" from "an invite went out".
|
|
574
|
+
//
|
|
575
|
+
// Flag naming note: the inspection mode is `--show`, not `--status`. `apps
|
|
576
|
+
// list --status <value>` and `connections deliveries --status <value>`
|
|
577
|
+
// already own `status` as a VALUE flag, and the argv parser has one global
|
|
578
|
+
// boolean-flags set shared by every noun/verb (see BOOLEAN_FLAGS in
|
|
579
|
+
// argv.ts), so there is no per-command scoping that would let `--status` be
|
|
580
|
+
// a bare boolean here and a value flag there. `--show` says the same thing
|
|
581
|
+
// without the collision.
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
const TRANSFER_USAGE = "usage: homespun apps transfer <app> --to <email> [--keep-as-member|--remove-me] | homespun apps transfer <app> --show | homespun apps transfer <app> --cancel";
|
|
584
|
+
/** Render a pending transfer for the human-readable (non `--json`) path. */
|
|
585
|
+
function describeTransfer(transfer) {
|
|
586
|
+
const stay = transfer.keepAsMember
|
|
587
|
+
? "you will stay on as a member once they accept"
|
|
588
|
+
: "you will be removed from the app once they accept";
|
|
589
|
+
return (`transfer pending to ${transfer.toEmail}, sent ${transfer.createdAt}, ` +
|
|
590
|
+
`expires ${transfer.expiresAt}: ${stay}. ` +
|
|
591
|
+
"Nothing has moved yet: the recipient has to accept it by email first.");
|
|
592
|
+
}
|
|
593
|
+
async function runTransfer(args) {
|
|
594
|
+
assertKnownFlags(args, ...specFor("apps", "transfer"));
|
|
595
|
+
const appArg = args.positionals[0];
|
|
596
|
+
if (!appArg)
|
|
597
|
+
fail(TRANSFER_USAGE, "invalid_args");
|
|
598
|
+
const to = args.flags.get("to");
|
|
599
|
+
const show = args.bools.has("show");
|
|
600
|
+
const cancel = args.bools.has("cancel");
|
|
601
|
+
const keepAsMember = args.bools.has("keep-as-member");
|
|
602
|
+
const removeMe = args.bools.has("remove-me");
|
|
603
|
+
const modeCount = [to !== undefined, show, cancel].filter(Boolean).length;
|
|
604
|
+
if (modeCount === 0)
|
|
605
|
+
fail(TRANSFER_USAGE, "invalid_args");
|
|
606
|
+
if (modeCount > 1) {
|
|
607
|
+
fail("--to, --show and --cancel are mutually exclusive", "invalid_args");
|
|
608
|
+
}
|
|
609
|
+
if (keepAsMember && removeMe) {
|
|
610
|
+
fail("--keep-as-member and --remove-me are mutually exclusive", "invalid_args");
|
|
611
|
+
}
|
|
612
|
+
if ((keepAsMember || removeMe) && to === undefined) {
|
|
613
|
+
fail("--keep-as-member and --remove-me only apply when initiating a transfer with --to", "invalid_args");
|
|
614
|
+
}
|
|
615
|
+
const client = makeClient(args);
|
|
616
|
+
const id = await resolveAppId(client, appArg);
|
|
617
|
+
const json = args.bools.has("json");
|
|
618
|
+
try {
|
|
619
|
+
if (show) {
|
|
620
|
+
const { transfer } = await client.getAppTransfer(id);
|
|
621
|
+
if (json) {
|
|
622
|
+
printJson({ transfer });
|
|
623
|
+
}
|
|
624
|
+
else if (!transfer) {
|
|
625
|
+
process.stdout.write("no ownership transfer is pending for this app\n");
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
process.stdout.write(describeTransfer(transfer) + "\n");
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (cancel) {
|
|
633
|
+
await client.cancelAppTransfer(id);
|
|
634
|
+
if (json) {
|
|
635
|
+
printJson({ cancelled: true, app_id: id });
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
process.stdout.write("the pending transfer, if any, has been withdrawn\n");
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// initiate: keepAsMember defaults to true (--remove-me is the only way
|
|
643
|
+
// to opt out), matching the relay's own default.
|
|
644
|
+
const { transfer } = await client.createAppTransfer(id, {
|
|
645
|
+
email: to,
|
|
646
|
+
keepAsMember: !removeMe,
|
|
647
|
+
});
|
|
648
|
+
if (json) {
|
|
649
|
+
printJson({ transfer });
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
process.stdout.write(describeTransfer(transfer) + "\n");
|
|
653
|
+
process.stdout.write(`Check its status with 'homespun apps transfer ${appArg} --show', or withdraw it with 'homespun apps transfer ${appArg} --cancel'.\n`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch (e) {
|
|
657
|
+
failFromError(e);
|
|
658
|
+
}
|
|
659
|
+
}
|
package/dist/help-catalog.js
CHANGED
|
@@ -31,7 +31,7 @@ const APPS = {
|
|
|
31
31
|
noun: "apps",
|
|
32
32
|
tagline: "app lifecycle management",
|
|
33
33
|
group: "app",
|
|
34
|
-
rootSummary: "App lifecycle: list, show, audit (security review of your apps' collection permissions), update, delete, deleted (what is in the trash), restore, purge (destroy a deleted app for good), wake, domain (custom domains), watch (stream the app's change feed as JSON-lines).",
|
|
34
|
+
rootSummary: "App lifecycle: list, show, audit (security review of your apps' collection permissions), update, delete, deleted (what is in the trash), restore, purge (destroy a deleted app for good), wake, domain (custom domains), watch (stream the app's change feed as JSON-lines), transfer (hand ownership to another human).",
|
|
35
35
|
verbs: [
|
|
36
36
|
{
|
|
37
37
|
verb: "list",
|
|
@@ -152,12 +152,43 @@ const APPS = {
|
|
|
152
152
|
},
|
|
153
153
|
],
|
|
154
154
|
},
|
|
155
|
+
{
|
|
156
|
+
verb: "transfer",
|
|
157
|
+
positionals: "<app>",
|
|
158
|
+
summary: "Hands app ownership to another human. Nothing moves until they accept the invite by email; e.g. `homespun apps transfer shop --to new-owner@example.com`, then check it with `homespun apps transfer shop --show` or pull it back with `homespun apps transfer shop --cancel`.",
|
|
159
|
+
flags: [
|
|
160
|
+
{
|
|
161
|
+
name: "to",
|
|
162
|
+
value: "<email>",
|
|
163
|
+
description: "Invite this email to take ownership",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
bools: [
|
|
167
|
+
{
|
|
168
|
+
name: "keep-as-member",
|
|
169
|
+
description: "Stay on as a member once accepted (default when neither this nor --remove-me is given)",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "remove-me",
|
|
173
|
+
description: "Be removed from the app once accepted",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "show",
|
|
177
|
+
description: "Show the pending transfer instead of starting one",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "cancel",
|
|
181
|
+
description: "Withdraw the pending transfer",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
155
185
|
],
|
|
156
186
|
notes: [
|
|
157
187
|
"<app> accepts either the app_id or its slug (resolved via GET /v1/apps?slug= when it does not look like a cuid).",
|
|
158
188
|
'watch streams the app\'s change feed as JSON-lines on stdout, one compact SerializedFeedEntry object per line, identical whether served over the live WebSocket (primary) or the long-poll fallback (used automatically when the WS upgrade fails, for example a locked-down network blocks outbound WS). A dormancy transition mid-watch emits a single {"type":"_dormant"} line and exits 0.',
|
|
189
|
+
"transfer prints a plain-language explanation by default (what will happen, that the recipient must accept by email, and that ownership has not moved yet), not just a JSON object; pass --json for the raw { transfer } record instead. audit does the same: a human report by default, --json for the machine one.",
|
|
159
190
|
],
|
|
160
|
-
outputNote: 'Output is JSON,
|
|
191
|
+
outputNote: 'Output is JSON, JSON-lines for watch, and a human-readable report by default for audit and transfer (pass --json for the raw object on those two). Errors go to stderr as {"error":{"code","message"}} with a non-zero exit.',
|
|
161
192
|
};
|
|
162
193
|
const DATA = {
|
|
163
194
|
noun: "data",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homespunapps/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.80",
|
|
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.
|
|
39
|
+
"@homespunapps/core": "^1.6.80",
|
|
40
40
|
"qrcode-terminal": "^0.12.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|