@userland.fun/cli 0.1.0 → 0.1.1
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 +120 -26
- 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
|
@@ -17,6 +17,10 @@ async function main() {
|
|
|
17
17
|
await authCommand(args);
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
+
if (command === "accounts") {
|
|
21
|
+
await accountsCommand(args);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
20
24
|
if (command === "signup") {
|
|
21
25
|
await signupCommand(args);
|
|
22
26
|
return;
|
|
@@ -42,7 +46,7 @@ async function appsCommand(args) {
|
|
|
42
46
|
return;
|
|
43
47
|
}
|
|
44
48
|
if (subcommand === "list") {
|
|
45
|
-
await listAppsCommand();
|
|
49
|
+
await listAppsCommand(rest);
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
52
|
if (subcommand === "releases") {
|
|
@@ -83,6 +87,18 @@ async function authCommand(args) {
|
|
|
83
87
|
}
|
|
84
88
|
usage(1);
|
|
85
89
|
}
|
|
90
|
+
async function accountsCommand(args) {
|
|
91
|
+
const [subcommand, ...rest] = args;
|
|
92
|
+
if (subcommand === "list") {
|
|
93
|
+
await listAccountsCommand();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (subcommand === "use") {
|
|
97
|
+
await useAccountCommand(rest);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
usage(1);
|
|
101
|
+
}
|
|
86
102
|
async function signupCommand(args) {
|
|
87
103
|
const options = parseAuthOptions(args);
|
|
88
104
|
const username = options.username ?? (await promptRequired("Username: "));
|
|
@@ -99,7 +115,8 @@ async function signupCommand(args) {
|
|
|
99
115
|
await saveAccountCredentials({ username: response.username, password });
|
|
100
116
|
const filePath = await saveCredentials({
|
|
101
117
|
api_key: response.api_key,
|
|
102
|
-
api_base_url: await apiBaseUrl()
|
|
118
|
+
api_base_url: await apiBaseUrl(),
|
|
119
|
+
account_id: response.account_id ?? null
|
|
103
120
|
});
|
|
104
121
|
console.log(`Created Userland account ${response.username}`);
|
|
105
122
|
console.log(`Saved API key to ${filePath}`);
|
|
@@ -119,10 +136,13 @@ async function loginCommand(args) {
|
|
|
119
136
|
body: JSON.stringify({ username, password })
|
|
120
137
|
});
|
|
121
138
|
if (options.save !== false) {
|
|
139
|
+
const baseUrl = await apiBaseUrl();
|
|
140
|
+
const accountId = response.account_id ?? (await discoverDefaultAccountId(response.api_key, baseUrl).catch(() => undefined));
|
|
122
141
|
await saveAccountCredentials({ username, password });
|
|
123
142
|
const filePath = await saveCredentials({
|
|
124
143
|
api_key: response.api_key,
|
|
125
|
-
api_base_url:
|
|
144
|
+
api_base_url: baseUrl,
|
|
145
|
+
account_id: accountId ?? null
|
|
126
146
|
});
|
|
127
147
|
console.log(`Saved API key to ${filePath}`);
|
|
128
148
|
console.log(`Saved account login to ${accountCredentialStoreLabel()}`);
|
|
@@ -135,10 +155,16 @@ async function authStatusCommand() {
|
|
|
135
155
|
const account = await readAccountCredentials();
|
|
136
156
|
const filePath = credentialsPath();
|
|
137
157
|
const apiKeySource = process.env.USERLAND_API_KEY ? "env" : credentials?.api_key ? "file" : "missing";
|
|
158
|
+
const selectedAccountId = process.env.USERLAND_ACCOUNT_ID ?? credentials?.account_id;
|
|
159
|
+
const accountSource = process.env.USERLAND_ACCOUNT_ID ? "env" : credentials?.account_id ? "file" : apiKeySource === "missing" ? "missing" : "default";
|
|
138
160
|
console.log(`api_base_url=${await apiBaseUrl(credentials)}`);
|
|
139
161
|
console.log(`api_key=${apiKeySource}`);
|
|
140
162
|
console.log(`credentials_file=${filePath}`);
|
|
141
|
-
console.log(`account=${
|
|
163
|
+
console.log(`account=${accountSource}`);
|
|
164
|
+
if (selectedAccountId) {
|
|
165
|
+
console.log(`account_id=${selectedAccountId}`);
|
|
166
|
+
}
|
|
167
|
+
console.log(`account_login=${account ? "keychain" : "missing"}`);
|
|
142
168
|
if (account?.username) {
|
|
143
169
|
console.log(`username=${account.username}`);
|
|
144
170
|
}
|
|
@@ -150,11 +176,30 @@ async function saveKeyCommand(args) {
|
|
|
150
176
|
await saveAccountCredentials({ username, password: options.password });
|
|
151
177
|
const filePath = await saveCredentials({
|
|
152
178
|
api_key: apiKey,
|
|
153
|
-
api_base_url: await apiBaseUrl()
|
|
179
|
+
api_base_url: await apiBaseUrl(),
|
|
180
|
+
account_id: null
|
|
154
181
|
});
|
|
155
182
|
console.log(`Saved API key to ${filePath}`);
|
|
156
183
|
console.log(`Saved account login to ${accountCredentialStoreLabel()}`);
|
|
157
184
|
}
|
|
185
|
+
async function listAccountsCommand() {
|
|
186
|
+
const response = await apiFetch("/v0/accounts", {
|
|
187
|
+
method: "GET"
|
|
188
|
+
});
|
|
189
|
+
for (const account of response.accounts) {
|
|
190
|
+
console.log(`${account.account_id}\t${account.role}\t${account.name}`);
|
|
191
|
+
}
|
|
192
|
+
console.log(`default_account_id=${response.default_account_id}`);
|
|
193
|
+
}
|
|
194
|
+
async function useAccountCommand(args) {
|
|
195
|
+
const accountId = args[0];
|
|
196
|
+
if (!accountId) {
|
|
197
|
+
usage(1);
|
|
198
|
+
}
|
|
199
|
+
const filePath = await saveCredentials({ account_id: accountId });
|
|
200
|
+
console.log(`selected_account_id=${accountId}`);
|
|
201
|
+
console.log(`credentials_file=${filePath}`);
|
|
202
|
+
}
|
|
158
203
|
async function publishCommand(args) {
|
|
159
204
|
const dir = args[0];
|
|
160
205
|
const options = parseOptions(args.slice(1));
|
|
@@ -165,7 +210,7 @@ async function publishCommand(args) {
|
|
|
165
210
|
const response = await apiFetch(options.app ? `/v0/apps/${options.app}` : "/v0/apps", {
|
|
166
211
|
method: "PUT",
|
|
167
212
|
body: JSON.stringify(body)
|
|
168
|
-
});
|
|
213
|
+
}, { accountId: options.account, accountScoped: true });
|
|
169
214
|
console.log(`Published ${response.origin}`);
|
|
170
215
|
console.log(`app_id=${response.app_id}`);
|
|
171
216
|
console.log(`release_id=${response.release_id}`);
|
|
@@ -175,10 +220,11 @@ async function publishCommand(args) {
|
|
|
175
220
|
console.log(`activation_reasons=${response.activation.reasons.join("; ")}`);
|
|
176
221
|
}
|
|
177
222
|
}
|
|
178
|
-
async function listAppsCommand() {
|
|
223
|
+
async function listAppsCommand(args = []) {
|
|
224
|
+
const options = parseAccountOptions(args);
|
|
179
225
|
const response = await apiFetch("/v0/apps", {
|
|
180
226
|
method: "GET"
|
|
181
|
-
});
|
|
227
|
+
}, { accountId: options.account, accountScoped: true });
|
|
182
228
|
for (const app of response.apps) {
|
|
183
229
|
console.log(`${app.app_id}\t${app.live_release_id ?? ""}\t${app.updated_at}\t${app.name}\t${app.origin}`);
|
|
184
230
|
}
|
|
@@ -188,9 +234,10 @@ async function releasesCommand(args) {
|
|
|
188
234
|
if (!appId) {
|
|
189
235
|
usage(1);
|
|
190
236
|
}
|
|
237
|
+
const options = parseAccountOptions(args.slice(1));
|
|
191
238
|
const response = await apiFetch(`/v0/apps/${appId}/releases`, {
|
|
192
239
|
method: "GET"
|
|
193
|
-
});
|
|
240
|
+
}, { accountId: options.account, accountScoped: true });
|
|
194
241
|
for (const release of response.releases) {
|
|
195
242
|
const live = release.is_live ? " live" : "";
|
|
196
243
|
console.log(`${release.release_id}${live}\t${release.activation_status}\t${release.created_at}\t${release.message ?? ""}`);
|
|
@@ -201,10 +248,11 @@ async function rollbackCommand(args) {
|
|
|
201
248
|
if (!appId || !releaseId) {
|
|
202
249
|
usage(1);
|
|
203
250
|
}
|
|
251
|
+
const options = parseAccountOptions(args.slice(2));
|
|
204
252
|
const response = await apiFetch(`/v0/apps/${appId}/rollback`, {
|
|
205
253
|
method: "POST",
|
|
206
254
|
body: JSON.stringify({ release_id: releaseId })
|
|
207
|
-
});
|
|
255
|
+
}, { accountId: options.account, accountScoped: true });
|
|
208
256
|
console.log(`Rolled back ${response.origin}`);
|
|
209
257
|
console.log(`app_id=${response.app_id}`);
|
|
210
258
|
console.log(`release_id=${response.release_id}`);
|
|
@@ -224,7 +272,7 @@ async function setSecretCommand(args) {
|
|
|
224
272
|
const response = await apiFetch(`/v0/apps/${appId}/secrets/${name}`, {
|
|
225
273
|
method: "PUT",
|
|
226
274
|
body: JSON.stringify({ value })
|
|
227
|
-
});
|
|
275
|
+
}, { accountId: options.account, accountScoped: true });
|
|
228
276
|
console.log(`secret=${response.name}`);
|
|
229
277
|
console.log(`present=${response.present}`);
|
|
230
278
|
console.log(`updated_at=${response.updated_at}`);
|
|
@@ -247,7 +295,7 @@ async function eventsCommand(args) {
|
|
|
247
295
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
248
296
|
const response = await apiFetch(`/v0/apps/${appId}/events${suffix}`, {
|
|
249
297
|
method: "GET"
|
|
250
|
-
});
|
|
298
|
+
}, { accountId: options.account, accountScoped: true });
|
|
251
299
|
for (const event of response.events) {
|
|
252
300
|
console.log(`${event.created_at}\t${event.severity}\t${event.type}\t${event.release_id ?? ""}\t${event.message}`);
|
|
253
301
|
}
|
|
@@ -363,24 +411,41 @@ async function walk(dir) {
|
|
|
363
411
|
}));
|
|
364
412
|
return files.flat();
|
|
365
413
|
}
|
|
366
|
-
async function apiFetch(apiPath, init) {
|
|
414
|
+
async function apiFetch(apiPath, init, options = {}) {
|
|
367
415
|
const credentials = await readCredentials();
|
|
368
416
|
const apiKey = process.env.USERLAND_API_KEY ?? credentials?.api_key;
|
|
369
417
|
if (!apiKey) {
|
|
370
418
|
throw new Error("USERLAND_API_KEY is required. Run `userland signup` or `userland login` to save credentials.");
|
|
371
419
|
}
|
|
372
420
|
const baseUrl = await apiBaseUrl(credentials);
|
|
421
|
+
const accountId = options.accountScoped ? selectedAccountId(options.accountId, credentials) : undefined;
|
|
422
|
+
const headers = {
|
|
423
|
+
authorization: `Bearer ${apiKey}`,
|
|
424
|
+
...init.headers
|
|
425
|
+
};
|
|
426
|
+
if (accountId) {
|
|
427
|
+
headers["x-userland-account-id"] = accountId;
|
|
428
|
+
}
|
|
373
429
|
return await requestJson(baseUrl, apiPath, {
|
|
374
430
|
...init,
|
|
375
|
-
headers
|
|
376
|
-
authorization: `Bearer ${apiKey}`,
|
|
377
|
-
...init.headers
|
|
378
|
-
}
|
|
431
|
+
headers
|
|
379
432
|
});
|
|
380
433
|
}
|
|
381
434
|
async function unauthenticatedApiFetch(apiPath, init) {
|
|
382
435
|
return await requestJson(await apiBaseUrl(), apiPath, init);
|
|
383
436
|
}
|
|
437
|
+
async function discoverDefaultAccountId(apiKey, baseUrl) {
|
|
438
|
+
const response = await requestJson(baseUrl, "/v0/accounts", {
|
|
439
|
+
method: "GET",
|
|
440
|
+
headers: {
|
|
441
|
+
authorization: `Bearer ${apiKey}`
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
return response.default_account_id;
|
|
445
|
+
}
|
|
446
|
+
function selectedAccountId(explicitAccountId, credentials) {
|
|
447
|
+
return explicitAccountId ?? process.env.USERLAND_ACCOUNT_ID ?? credentials?.account_id;
|
|
448
|
+
}
|
|
384
449
|
async function requestJson(baseUrl, apiPath, init) {
|
|
385
450
|
const response = await fetch(`${baseUrl.replace(/\/$/u, "")}${apiPath}`, {
|
|
386
451
|
...init,
|
|
@@ -420,6 +485,7 @@ async function readCredentials() {
|
|
|
420
485
|
}
|
|
421
486
|
const credentials = parsed;
|
|
422
487
|
return {
|
|
488
|
+
account_id: stringValue(credentials.account_id),
|
|
423
489
|
api_base_url: stringValue(credentials.api_base_url),
|
|
424
490
|
api_key: stringValue(credentials.api_key),
|
|
425
491
|
updated_at: stringValue(credentials.updated_at)
|
|
@@ -428,12 +494,15 @@ async function readCredentials() {
|
|
|
428
494
|
async function saveCredentials(update) {
|
|
429
495
|
const filePath = credentialsPath();
|
|
430
496
|
const existing = (await readCredentials()) ?? {};
|
|
431
|
-
const sanitizedUpdate = Object.fromEntries(Object.entries(update).filter(([, value]) => value !== undefined));
|
|
497
|
+
const sanitizedUpdate = Object.fromEntries(Object.entries(update).filter(([, value]) => value !== undefined && value !== null));
|
|
432
498
|
const credentials = {
|
|
433
499
|
...existing,
|
|
434
500
|
...sanitizedUpdate,
|
|
435
501
|
updated_at: new Date().toISOString()
|
|
436
502
|
};
|
|
503
|
+
if (update.account_id === null) {
|
|
504
|
+
delete credentials.account_id;
|
|
505
|
+
}
|
|
437
506
|
const dir = path.dirname(filePath);
|
|
438
507
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
439
508
|
await fs.chmod(dir, 0o700).catch(() => undefined);
|
|
@@ -706,6 +775,9 @@ function parseOptions(args) {
|
|
|
706
775
|
else if (arg === "--message") {
|
|
707
776
|
options.message = args[++index];
|
|
708
777
|
}
|
|
778
|
+
else if (arg === "--account") {
|
|
779
|
+
options.account = args[++index];
|
|
780
|
+
}
|
|
709
781
|
else {
|
|
710
782
|
throw new Error(`Unknown option: ${arg}`);
|
|
711
783
|
}
|
|
@@ -744,6 +816,9 @@ function parseSecretSetOptions(args) {
|
|
|
744
816
|
if (arg === "--value") {
|
|
745
817
|
options.value = args[++index];
|
|
746
818
|
}
|
|
819
|
+
else if (arg === "--account") {
|
|
820
|
+
options.account = args[++index];
|
|
821
|
+
}
|
|
747
822
|
else {
|
|
748
823
|
throw new Error(`Unknown option: ${arg}`);
|
|
749
824
|
}
|
|
@@ -766,6 +841,22 @@ function parseEventsOptions(args) {
|
|
|
766
841
|
else if (arg === "--limit") {
|
|
767
842
|
options.limit = args[++index];
|
|
768
843
|
}
|
|
844
|
+
else if (arg === "--account") {
|
|
845
|
+
options.account = args[++index];
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return options;
|
|
852
|
+
}
|
|
853
|
+
function parseAccountOptions(args) {
|
|
854
|
+
const options = {};
|
|
855
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
856
|
+
const arg = args[index];
|
|
857
|
+
if (arg === "--account") {
|
|
858
|
+
options.account = args[++index];
|
|
859
|
+
}
|
|
769
860
|
else {
|
|
770
861
|
throw new Error(`Unknown option: ${arg}`);
|
|
771
862
|
}
|
|
@@ -885,21 +976,24 @@ function usage(exitCode) {
|
|
|
885
976
|
userland login [--username <username>] [--password <password>] [--no-save]
|
|
886
977
|
userland auth status
|
|
887
978
|
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
|
|
979
|
+
userland accounts list
|
|
980
|
+
userland accounts use <account-id>
|
|
981
|
+
userland apps publish <dir> [--app <app-id>] [--message <message>] [--account <account-id>]
|
|
982
|
+
userland apps list [--account <account-id>]
|
|
983
|
+
userland apps releases <app-id> [--account <account-id>]
|
|
984
|
+
userland apps rollback <app-id> <release-id> [--account <account-id>]
|
|
985
|
+
userland apps secrets set <app-id> <NAME> [--value <value>] [--account <account-id>]
|
|
986
|
+
userland apps events <app-id> [--type <event-type>] [--severity <level>] [--release <release-id>] [--limit <n>] [--account <account-id>]
|
|
894
987
|
|
|
895
988
|
Aliases:
|
|
896
989
|
userland auth signup [--username <username>] [--password <password>] [--email <email>] [--no-save]
|
|
897
990
|
userland auth login [--username <username>] [--password <password>] [--no-save]
|
|
898
|
-
userland publish <dir> [--app <app-id>] [--message <message>]
|
|
899
|
-
userland releases <app-id>
|
|
991
|
+
userland publish <dir> [--app <app-id>] [--message <message>] [--account <account-id>]
|
|
992
|
+
userland releases <app-id> [--account <account-id>]
|
|
900
993
|
|
|
901
994
|
Credentials:
|
|
902
995
|
Commands use USERLAND_API_KEY first, then ~/.userland/credentials.json for API keys.
|
|
996
|
+
App commands use --account, then USERLAND_ACCOUNT_ID, then saved account_id when set.
|
|
903
997
|
Account username and password are stored in the OS keychain.
|
|
904
998
|
|
|
905
999
|
Docs:
|