@userland.fun/cli 0.1.0 → 0.1.2
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 +10 -0
- package/dist/index.js +135 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,8 +21,12 @@ userland signup --username <username>
|
|
|
21
21
|
userland login --username <username>
|
|
22
22
|
userland auth status
|
|
23
23
|
userland auth save-key --username <username> --api-key <api-key>
|
|
24
|
+
userland accounts list
|
|
25
|
+
userland accounts use <account-id>
|
|
24
26
|
userland apps publish examples/<example-slug>
|
|
27
|
+
userland apps publish examples/<example-slug> --account <account-id>
|
|
25
28
|
userland apps list
|
|
29
|
+
USERLAND_ACCOUNT_ID=<account-id> userland apps list
|
|
26
30
|
userland apps releases <app-id>
|
|
27
31
|
userland apps rollback <app-id> <release-id>
|
|
28
32
|
userland apps secrets set <app-id> <NAME> --value <value>
|
|
@@ -36,8 +40,12 @@ npm run userland -- signup --username <username>
|
|
|
36
40
|
npm run userland -- login --username <username>
|
|
37
41
|
npm run userland -- auth status
|
|
38
42
|
npm run userland -- auth save-key --username <username> --api-key <api-key>
|
|
43
|
+
npm run userland -- accounts list
|
|
44
|
+
npm run userland -- accounts use <account-id>
|
|
39
45
|
npm run userland -- apps publish examples/<example-slug>
|
|
46
|
+
npm run userland -- apps publish examples/<example-slug> --account <account-id>
|
|
40
47
|
npm run userland -- apps list
|
|
48
|
+
npm run userland -- apps list --account <account-id>
|
|
41
49
|
npm run userland -- apps releases <app-id>
|
|
42
50
|
npm run userland -- apps rollback <app-id> <release-id>
|
|
43
51
|
npm run userland -- apps secrets set <app-id> <NAME> --value <value>
|
|
@@ -46,6 +54,8 @@ npm run userland -- apps events <app-id>
|
|
|
46
54
|
|
|
47
55
|
`signup`, `login`, and `auth save-key` save the API key to `~/.userland/credentials.json` with `0600` permissions. Account username and password are stored in the OS keychain: macOS Keychain, Windows Credential Manager, or Linux Secret Service through `secret-tool`. App commands prefer `USERLAND_API_KEY` when it is set, then fall back to the saved API key.
|
|
48
56
|
|
|
57
|
+
Most users do not need to select an account. If no account is selected, the API uses the actor's default account. Team, client, and agency workflows can select an account with `--account <account-id>`, `USERLAND_ACCOUNT_ID`, or `userland accounts use <account-id>`. Platform account members manage apps, releases, secrets, billing, and settings; they are separate from app users inside a published app.
|
|
58
|
+
|
|
49
59
|
## Validation
|
|
50
60
|
|
|
51
61
|
Build and inspect the publish tarball:
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@ const KEYCHAIN_SERVICE = "fun.userland.cli";
|
|
|
9
9
|
const KEYCHAIN_ACCOUNT = "default";
|
|
10
10
|
async function main() {
|
|
11
11
|
const [command, ...args] = process.argv.slice(2);
|
|
12
|
+
if (isHelpCommand(command)) {
|
|
13
|
+
usage(0);
|
|
14
|
+
}
|
|
12
15
|
if (command === "apps") {
|
|
13
16
|
await appsCommand(args);
|
|
14
17
|
return;
|
|
@@ -17,6 +20,10 @@ async function main() {
|
|
|
17
20
|
await authCommand(args);
|
|
18
21
|
return;
|
|
19
22
|
}
|
|
23
|
+
if (command === "accounts") {
|
|
24
|
+
await accountsCommand(args);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
20
27
|
if (command === "signup") {
|
|
21
28
|
await signupCommand(args);
|
|
22
29
|
return;
|
|
@@ -42,7 +49,7 @@ async function appsCommand(args) {
|
|
|
42
49
|
return;
|
|
43
50
|
}
|
|
44
51
|
if (subcommand === "list") {
|
|
45
|
-
await listAppsCommand();
|
|
52
|
+
await listAppsCommand(rest);
|
|
46
53
|
return;
|
|
47
54
|
}
|
|
48
55
|
if (subcommand === "releases") {
|
|
@@ -83,6 +90,18 @@ async function authCommand(args) {
|
|
|
83
90
|
}
|
|
84
91
|
usage(1);
|
|
85
92
|
}
|
|
93
|
+
async function accountsCommand(args) {
|
|
94
|
+
const [subcommand, ...rest] = args;
|
|
95
|
+
if (subcommand === "list") {
|
|
96
|
+
await listAccountsCommand();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (subcommand === "use") {
|
|
100
|
+
await useAccountCommand(rest);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
usage(1);
|
|
104
|
+
}
|
|
86
105
|
async function signupCommand(args) {
|
|
87
106
|
const options = parseAuthOptions(args);
|
|
88
107
|
const username = options.username ?? (await promptRequired("Username: "));
|
|
@@ -99,7 +118,8 @@ async function signupCommand(args) {
|
|
|
99
118
|
await saveAccountCredentials({ username: response.username, password });
|
|
100
119
|
const filePath = await saveCredentials({
|
|
101
120
|
api_key: response.api_key,
|
|
102
|
-
api_base_url: await apiBaseUrl()
|
|
121
|
+
api_base_url: await apiBaseUrl(),
|
|
122
|
+
account_id: response.account_id ?? null
|
|
103
123
|
});
|
|
104
124
|
console.log(`Created Userland account ${response.username}`);
|
|
105
125
|
console.log(`Saved API key to ${filePath}`);
|
|
@@ -119,10 +139,13 @@ async function loginCommand(args) {
|
|
|
119
139
|
body: JSON.stringify({ username, password })
|
|
120
140
|
});
|
|
121
141
|
if (options.save !== false) {
|
|
142
|
+
const baseUrl = await apiBaseUrl();
|
|
143
|
+
const accountId = response.account_id ?? (await discoverDefaultAccountId(response.api_key, baseUrl).catch(() => undefined));
|
|
122
144
|
await saveAccountCredentials({ username, password });
|
|
123
145
|
const filePath = await saveCredentials({
|
|
124
146
|
api_key: response.api_key,
|
|
125
|
-
api_base_url:
|
|
147
|
+
api_base_url: baseUrl,
|
|
148
|
+
account_id: accountId ?? null
|
|
126
149
|
});
|
|
127
150
|
console.log(`Saved API key to ${filePath}`);
|
|
128
151
|
console.log(`Saved account login to ${accountCredentialStoreLabel()}`);
|
|
@@ -135,10 +158,16 @@ async function authStatusCommand() {
|
|
|
135
158
|
const account = await readAccountCredentials();
|
|
136
159
|
const filePath = credentialsPath();
|
|
137
160
|
const apiKeySource = process.env.USERLAND_API_KEY ? "env" : credentials?.api_key ? "file" : "missing";
|
|
161
|
+
const selectedAccountId = process.env.USERLAND_ACCOUNT_ID ?? credentials?.account_id;
|
|
162
|
+
const accountSource = process.env.USERLAND_ACCOUNT_ID ? "env" : credentials?.account_id ? "file" : apiKeySource === "missing" ? "missing" : "default";
|
|
138
163
|
console.log(`api_base_url=${await apiBaseUrl(credentials)}`);
|
|
139
164
|
console.log(`api_key=${apiKeySource}`);
|
|
140
165
|
console.log(`credentials_file=${filePath}`);
|
|
141
|
-
console.log(`account=${
|
|
166
|
+
console.log(`account=${accountSource}`);
|
|
167
|
+
if (selectedAccountId) {
|
|
168
|
+
console.log(`account_id=${selectedAccountId}`);
|
|
169
|
+
}
|
|
170
|
+
console.log(`account_login=${account ? "keychain" : "missing"}`);
|
|
142
171
|
if (account?.username) {
|
|
143
172
|
console.log(`username=${account.username}`);
|
|
144
173
|
}
|
|
@@ -150,11 +179,30 @@ async function saveKeyCommand(args) {
|
|
|
150
179
|
await saveAccountCredentials({ username, password: options.password });
|
|
151
180
|
const filePath = await saveCredentials({
|
|
152
181
|
api_key: apiKey,
|
|
153
|
-
api_base_url: await apiBaseUrl()
|
|
182
|
+
api_base_url: await apiBaseUrl(),
|
|
183
|
+
account_id: null
|
|
154
184
|
});
|
|
155
185
|
console.log(`Saved API key to ${filePath}`);
|
|
156
186
|
console.log(`Saved account login to ${accountCredentialStoreLabel()}`);
|
|
157
187
|
}
|
|
188
|
+
async function listAccountsCommand() {
|
|
189
|
+
const response = await apiFetch("/v0/accounts", {
|
|
190
|
+
method: "GET"
|
|
191
|
+
});
|
|
192
|
+
for (const account of response.accounts) {
|
|
193
|
+
console.log(`${account.account_id}\t${account.role}\t${account.name}`);
|
|
194
|
+
}
|
|
195
|
+
console.log(`default_account_id=${response.default_account_id}`);
|
|
196
|
+
}
|
|
197
|
+
async function useAccountCommand(args) {
|
|
198
|
+
const accountId = args[0];
|
|
199
|
+
if (!accountId) {
|
|
200
|
+
usage(1);
|
|
201
|
+
}
|
|
202
|
+
const filePath = await saveCredentials({ account_id: accountId });
|
|
203
|
+
console.log(`selected_account_id=${accountId}`);
|
|
204
|
+
console.log(`credentials_file=${filePath}`);
|
|
205
|
+
}
|
|
158
206
|
async function publishCommand(args) {
|
|
159
207
|
const dir = args[0];
|
|
160
208
|
const options = parseOptions(args.slice(1));
|
|
@@ -165,7 +213,7 @@ async function publishCommand(args) {
|
|
|
165
213
|
const response = await apiFetch(options.app ? `/v0/apps/${options.app}` : "/v0/apps", {
|
|
166
214
|
method: "PUT",
|
|
167
215
|
body: JSON.stringify(body)
|
|
168
|
-
});
|
|
216
|
+
}, { accountId: options.account, accountScoped: true });
|
|
169
217
|
console.log(`Published ${response.origin}`);
|
|
170
218
|
console.log(`app_id=${response.app_id}`);
|
|
171
219
|
console.log(`release_id=${response.release_id}`);
|
|
@@ -175,10 +223,11 @@ async function publishCommand(args) {
|
|
|
175
223
|
console.log(`activation_reasons=${response.activation.reasons.join("; ")}`);
|
|
176
224
|
}
|
|
177
225
|
}
|
|
178
|
-
async function listAppsCommand() {
|
|
226
|
+
async function listAppsCommand(args = []) {
|
|
227
|
+
const options = parseAccountOptions(args);
|
|
179
228
|
const response = await apiFetch("/v0/apps", {
|
|
180
229
|
method: "GET"
|
|
181
|
-
});
|
|
230
|
+
}, { accountId: options.account, accountScoped: true });
|
|
182
231
|
for (const app of response.apps) {
|
|
183
232
|
console.log(`${app.app_id}\t${app.live_release_id ?? ""}\t${app.updated_at}\t${app.name}\t${app.origin}`);
|
|
184
233
|
}
|
|
@@ -188,9 +237,10 @@ async function releasesCommand(args) {
|
|
|
188
237
|
if (!appId) {
|
|
189
238
|
usage(1);
|
|
190
239
|
}
|
|
240
|
+
const options = parseAccountOptions(args.slice(1));
|
|
191
241
|
const response = await apiFetch(`/v0/apps/${appId}/releases`, {
|
|
192
242
|
method: "GET"
|
|
193
|
-
});
|
|
243
|
+
}, { accountId: options.account, accountScoped: true });
|
|
194
244
|
for (const release of response.releases) {
|
|
195
245
|
const live = release.is_live ? " live" : "";
|
|
196
246
|
console.log(`${release.release_id}${live}\t${release.activation_status}\t${release.created_at}\t${release.message ?? ""}`);
|
|
@@ -201,10 +251,11 @@ async function rollbackCommand(args) {
|
|
|
201
251
|
if (!appId || !releaseId) {
|
|
202
252
|
usage(1);
|
|
203
253
|
}
|
|
254
|
+
const options = parseAccountOptions(args.slice(2));
|
|
204
255
|
const response = await apiFetch(`/v0/apps/${appId}/rollback`, {
|
|
205
256
|
method: "POST",
|
|
206
257
|
body: JSON.stringify({ release_id: releaseId })
|
|
207
|
-
});
|
|
258
|
+
}, { accountId: options.account, accountScoped: true });
|
|
208
259
|
console.log(`Rolled back ${response.origin}`);
|
|
209
260
|
console.log(`app_id=${response.app_id}`);
|
|
210
261
|
console.log(`release_id=${response.release_id}`);
|
|
@@ -224,7 +275,7 @@ async function setSecretCommand(args) {
|
|
|
224
275
|
const response = await apiFetch(`/v0/apps/${appId}/secrets/${name}`, {
|
|
225
276
|
method: "PUT",
|
|
226
277
|
body: JSON.stringify({ value })
|
|
227
|
-
});
|
|
278
|
+
}, { accountId: options.account, accountScoped: true });
|
|
228
279
|
console.log(`secret=${response.name}`);
|
|
229
280
|
console.log(`present=${response.present}`);
|
|
230
281
|
console.log(`updated_at=${response.updated_at}`);
|
|
@@ -247,7 +298,7 @@ async function eventsCommand(args) {
|
|
|
247
298
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
248
299
|
const response = await apiFetch(`/v0/apps/${appId}/events${suffix}`, {
|
|
249
300
|
method: "GET"
|
|
250
|
-
});
|
|
301
|
+
}, { accountId: options.account, accountScoped: true });
|
|
251
302
|
for (const event of response.events) {
|
|
252
303
|
console.log(`${event.created_at}\t${event.severity}\t${event.type}\t${event.release_id ?? ""}\t${event.message}`);
|
|
253
304
|
}
|
|
@@ -363,24 +414,41 @@ async function walk(dir) {
|
|
|
363
414
|
}));
|
|
364
415
|
return files.flat();
|
|
365
416
|
}
|
|
366
|
-
async function apiFetch(apiPath, init) {
|
|
417
|
+
async function apiFetch(apiPath, init, options = {}) {
|
|
367
418
|
const credentials = await readCredentials();
|
|
368
419
|
const apiKey = process.env.USERLAND_API_KEY ?? credentials?.api_key;
|
|
369
420
|
if (!apiKey) {
|
|
370
421
|
throw new Error("USERLAND_API_KEY is required. Run `userland signup` or `userland login` to save credentials.");
|
|
371
422
|
}
|
|
372
423
|
const baseUrl = await apiBaseUrl(credentials);
|
|
424
|
+
const accountId = options.accountScoped ? selectedAccountId(options.accountId, credentials) : undefined;
|
|
425
|
+
const headers = {
|
|
426
|
+
authorization: `Bearer ${apiKey}`,
|
|
427
|
+
...init.headers
|
|
428
|
+
};
|
|
429
|
+
if (accountId) {
|
|
430
|
+
headers["x-userland-account-id"] = accountId;
|
|
431
|
+
}
|
|
373
432
|
return await requestJson(baseUrl, apiPath, {
|
|
374
433
|
...init,
|
|
375
|
-
headers
|
|
376
|
-
authorization: `Bearer ${apiKey}`,
|
|
377
|
-
...init.headers
|
|
378
|
-
}
|
|
434
|
+
headers
|
|
379
435
|
});
|
|
380
436
|
}
|
|
381
437
|
async function unauthenticatedApiFetch(apiPath, init) {
|
|
382
438
|
return await requestJson(await apiBaseUrl(), apiPath, init);
|
|
383
439
|
}
|
|
440
|
+
async function discoverDefaultAccountId(apiKey, baseUrl) {
|
|
441
|
+
const response = await requestJson(baseUrl, "/v0/accounts", {
|
|
442
|
+
method: "GET",
|
|
443
|
+
headers: {
|
|
444
|
+
authorization: `Bearer ${apiKey}`
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
return response.default_account_id;
|
|
448
|
+
}
|
|
449
|
+
function selectedAccountId(explicitAccountId, credentials) {
|
|
450
|
+
return explicitAccountId ?? process.env.USERLAND_ACCOUNT_ID ?? credentials?.account_id;
|
|
451
|
+
}
|
|
384
452
|
async function requestJson(baseUrl, apiPath, init) {
|
|
385
453
|
const response = await fetch(`${baseUrl.replace(/\/$/u, "")}${apiPath}`, {
|
|
386
454
|
...init,
|
|
@@ -420,6 +488,7 @@ async function readCredentials() {
|
|
|
420
488
|
}
|
|
421
489
|
const credentials = parsed;
|
|
422
490
|
return {
|
|
491
|
+
account_id: stringValue(credentials.account_id),
|
|
423
492
|
api_base_url: stringValue(credentials.api_base_url),
|
|
424
493
|
api_key: stringValue(credentials.api_key),
|
|
425
494
|
updated_at: stringValue(credentials.updated_at)
|
|
@@ -428,12 +497,15 @@ async function readCredentials() {
|
|
|
428
497
|
async function saveCredentials(update) {
|
|
429
498
|
const filePath = credentialsPath();
|
|
430
499
|
const existing = (await readCredentials()) ?? {};
|
|
431
|
-
const sanitizedUpdate = Object.fromEntries(Object.entries(update).filter(([, value]) => value !== undefined));
|
|
500
|
+
const sanitizedUpdate = Object.fromEntries(Object.entries(update).filter(([, value]) => value !== undefined && value !== null));
|
|
432
501
|
const credentials = {
|
|
433
502
|
...existing,
|
|
434
503
|
...sanitizedUpdate,
|
|
435
504
|
updated_at: new Date().toISOString()
|
|
436
505
|
};
|
|
506
|
+
if (update.account_id === null) {
|
|
507
|
+
delete credentials.account_id;
|
|
508
|
+
}
|
|
437
509
|
const dir = path.dirname(filePath);
|
|
438
510
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
439
511
|
await fs.chmod(dir, 0o700).catch(() => undefined);
|
|
@@ -706,6 +778,9 @@ function parseOptions(args) {
|
|
|
706
778
|
else if (arg === "--message") {
|
|
707
779
|
options.message = args[++index];
|
|
708
780
|
}
|
|
781
|
+
else if (arg === "--account") {
|
|
782
|
+
options.account = args[++index];
|
|
783
|
+
}
|
|
709
784
|
else {
|
|
710
785
|
throw new Error(`Unknown option: ${arg}`);
|
|
711
786
|
}
|
|
@@ -744,6 +819,9 @@ function parseSecretSetOptions(args) {
|
|
|
744
819
|
if (arg === "--value") {
|
|
745
820
|
options.value = args[++index];
|
|
746
821
|
}
|
|
822
|
+
else if (arg === "--account") {
|
|
823
|
+
options.account = args[++index];
|
|
824
|
+
}
|
|
747
825
|
else {
|
|
748
826
|
throw new Error(`Unknown option: ${arg}`);
|
|
749
827
|
}
|
|
@@ -766,6 +844,22 @@ function parseEventsOptions(args) {
|
|
|
766
844
|
else if (arg === "--limit") {
|
|
767
845
|
options.limit = args[++index];
|
|
768
846
|
}
|
|
847
|
+
else if (arg === "--account") {
|
|
848
|
+
options.account = args[++index];
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return options;
|
|
855
|
+
}
|
|
856
|
+
function parseAccountOptions(args) {
|
|
857
|
+
const options = {};
|
|
858
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
859
|
+
const arg = args[index];
|
|
860
|
+
if (arg === "--account") {
|
|
861
|
+
options.account = args[++index];
|
|
862
|
+
}
|
|
769
863
|
else {
|
|
770
864
|
throw new Error(`Unknown option: ${arg}`);
|
|
771
865
|
}
|
|
@@ -879,32 +973,45 @@ function errorMessage(body) {
|
|
|
879
973
|
}
|
|
880
974
|
return message ?? code;
|
|
881
975
|
}
|
|
976
|
+
function isHelpCommand(command) {
|
|
977
|
+
return command === "--help" || command === "-h" || command === "help";
|
|
978
|
+
}
|
|
882
979
|
function usage(exitCode) {
|
|
883
|
-
|
|
980
|
+
const message = `Usage:
|
|
981
|
+
userland [--help]
|
|
884
982
|
userland signup [--username <username>] [--password <password>] [--email <email>] [--no-save]
|
|
885
983
|
userland login [--username <username>] [--password <password>] [--no-save]
|
|
886
984
|
userland auth status
|
|
887
985
|
userland auth save-key --username <username> --api-key <api-key> [--password <password>]
|
|
888
|
-
userland
|
|
889
|
-
userland
|
|
890
|
-
userland apps
|
|
891
|
-
userland apps
|
|
892
|
-
userland apps
|
|
893
|
-
userland apps
|
|
986
|
+
userland accounts list
|
|
987
|
+
userland accounts use <account-id>
|
|
988
|
+
userland apps publish <dir> [--app <app-id>] [--message <message>] [--account <account-id>]
|
|
989
|
+
userland apps list [--account <account-id>]
|
|
990
|
+
userland apps releases <app-id> [--account <account-id>]
|
|
991
|
+
userland apps rollback <app-id> <release-id> [--account <account-id>]
|
|
992
|
+
userland apps secrets set <app-id> <NAME> [--value <value>] [--account <account-id>]
|
|
993
|
+
userland apps events <app-id> [--type <event-type>] [--severity <level>] [--release <release-id>] [--limit <n>] [--account <account-id>]
|
|
894
994
|
|
|
895
995
|
Aliases:
|
|
896
996
|
userland auth signup [--username <username>] [--password <password>] [--email <email>] [--no-save]
|
|
897
997
|
userland auth login [--username <username>] [--password <password>] [--no-save]
|
|
898
|
-
userland publish <dir> [--app <app-id>] [--message <message>]
|
|
899
|
-
userland releases <app-id>
|
|
998
|
+
userland publish <dir> [--app <app-id>] [--message <message>] [--account <account-id>]
|
|
999
|
+
userland releases <app-id> [--account <account-id>]
|
|
900
1000
|
|
|
901
1001
|
Credentials:
|
|
902
1002
|
Commands use USERLAND_API_KEY first, then ~/.userland/credentials.json for API keys.
|
|
1003
|
+
App commands use --account, then USERLAND_ACCOUNT_ID, then saved account_id when set.
|
|
903
1004
|
Account username and password are stored in the OS keychain.
|
|
904
1005
|
|
|
905
1006
|
Docs:
|
|
906
1007
|
https://docs.userland.fun/reference/cli
|
|
907
|
-
https://docs.userland.fun/guides/troubleshooting
|
|
1008
|
+
https://docs.userland.fun/guides/troubleshooting`;
|
|
1009
|
+
if (exitCode === 0) {
|
|
1010
|
+
console.log(message);
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
console.error(message);
|
|
1014
|
+
}
|
|
908
1015
|
process.exit(exitCode);
|
|
909
1016
|
}
|
|
910
1017
|
main().catch((error) => {
|