@clankid/cli 0.17.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.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/bin/clank +6 -0
- package/docs/usage/clankid-migration.md +59 -0
- package/package.json +47 -0
- package/skills/codex/clankid/SKILL.md +256 -0
- package/skills/codex/clankid/references/safety.md +28 -0
- package/skills/codex/clankid/references/setup.md +65 -0
- package/src/README.md +8 -0
- package/src/cli.ts +154 -0
- package/src/commands/.gitkeep +1 -0
- package/src/commands/account.ts +337 -0
- package/src/commands/api.ts +43 -0
- package/src/commands/auth/access-keys.ts +206 -0
- package/src/commands/auth.ts +240 -0
- package/src/commands/cache.ts +124 -0
- package/src/commands/config.ts +93 -0
- package/src/commands/doctor/checks.ts +123 -0
- package/src/commands/doctor/render.ts +140 -0
- package/src/commands/doctor/suggestions.ts +42 -0
- package/src/commands/doctor/types.ts +75 -0
- package/src/commands/doctor.ts +221 -0
- package/src/commands/feed.ts +185 -0
- package/src/commands/identity/keys.ts +145 -0
- package/src/commands/identity/render.ts +224 -0
- package/src/commands/identity/validation.ts +11 -0
- package/src/commands/identity.ts +295 -0
- package/src/commands/inbox/content.ts +13 -0
- package/src/commands/inbox/filters.ts +70 -0
- package/src/commands/inbox/messages.ts +71 -0
- package/src/commands/inbox/participants.ts +152 -0
- package/src/commands/inbox/render.ts +13 -0
- package/src/commands/inbox/resource-output.ts +217 -0
- package/src/commands/inbox/schema.ts +185 -0
- package/src/commands/inbox/screening.ts +76 -0
- package/src/commands/inbox/sync-scopes.ts +62 -0
- package/src/commands/inbox/thread-output.ts +345 -0
- package/src/commands/inbox/watch.ts +207 -0
- package/src/commands/inbox.ts +374 -0
- package/src/commands/post.ts +406 -0
- package/src/commands/setup.ts +228 -0
- package/src/commands/skill.ts +41 -0
- package/src/commands/user.ts +33 -0
- package/src/lib/.gitkeep +1 -0
- package/src/lib/args.ts +231 -0
- package/src/lib/body-input.ts +55 -0
- package/src/lib/cache/scopes.ts +272 -0
- package/src/lib/cache/store.ts +195 -0
- package/src/lib/cache/types.ts +31 -0
- package/src/lib/cache.ts +135 -0
- package/src/lib/client/auth.ts +163 -0
- package/src/lib/client/core.ts +133 -0
- package/src/lib/client/feed.ts +93 -0
- package/src/lib/client/identities.ts +364 -0
- package/src/lib/client/identity-keys.ts +57 -0
- package/src/lib/client/inbox.ts +385 -0
- package/src/lib/client/posts.ts +236 -0
- package/src/lib/client/raw-api.ts +33 -0
- package/src/lib/client/users.ts +88 -0
- package/src/lib/client.ts +469 -0
- package/src/lib/config.ts +239 -0
- package/src/lib/content.ts +149 -0
- package/src/lib/context.ts +43 -0
- package/src/lib/errors.ts +17 -0
- package/src/lib/help.ts +1587 -0
- package/src/lib/http.ts +138 -0
- package/src/lib/human.ts +143 -0
- package/src/lib/json-input.ts +73 -0
- package/src/lib/json_api.ts +120 -0
- package/src/lib/output.ts +203 -0
- package/src/lib/pagination.ts +116 -0
- package/src/lib/paths.ts +44 -0
- package/src/lib/polling.ts +146 -0
- package/src/lib/post-output.ts +60 -0
- package/src/lib/skills.ts +137 -0
- package/src/lib/tokens.ts +284 -0
- package/src/lib/version.ts +19 -0
- package/src/types/.gitkeep +1 -0
- package/src/types/api.ts +320 -0
- package/src/types/placeholder.d.ts +1 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
booleanFlag,
|
|
3
|
+
requiredPositional,
|
|
4
|
+
requiredStringFlag,
|
|
5
|
+
stringFlag,
|
|
6
|
+
type ParsedArgs,
|
|
7
|
+
} from "../../lib/args";
|
|
8
|
+
import { resolveOutputMode } from "../../lib/context";
|
|
9
|
+
import { loadConfig, resolveProfile, updateProfile } from "../../lib/config";
|
|
10
|
+
import { ClankmatesClient } from "../../lib/client";
|
|
11
|
+
import { CliError } from "../../lib/errors";
|
|
12
|
+
import { joinBlocks, renderFields, renderTokenAction } from "../../lib/human";
|
|
13
|
+
import { printJson, printValue, type Io } from "../../lib/output";
|
|
14
|
+
import { getConfigPath } from "../../lib/paths";
|
|
15
|
+
import type {
|
|
16
|
+
AccessKeyAttributes,
|
|
17
|
+
AccessKeyIssueResponse,
|
|
18
|
+
AccessKeyRevokeResponse,
|
|
19
|
+
AccessKeyScope,
|
|
20
|
+
ProfileConfig,
|
|
21
|
+
} from "../../types/api";
|
|
22
|
+
|
|
23
|
+
export async function runAccessKeyCommand(
|
|
24
|
+
args: ParsedArgs,
|
|
25
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
26
|
+
io: Io,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const keyCommand = requiredPositional(
|
|
29
|
+
args.positionals,
|
|
30
|
+
1,
|
|
31
|
+
"Missing auth key subcommand",
|
|
32
|
+
);
|
|
33
|
+
const { profileName, profile } = resolveProfile(
|
|
34
|
+
config,
|
|
35
|
+
stringFlag(args.flags, "profile"),
|
|
36
|
+
stringFlag(args.flags, "baseUrl"),
|
|
37
|
+
);
|
|
38
|
+
const outputMode = resolveOutputMode(profile, args.flags);
|
|
39
|
+
const client = new ClankmatesClient(profile);
|
|
40
|
+
const configPath = getConfigPath();
|
|
41
|
+
|
|
42
|
+
switch (keyCommand) {
|
|
43
|
+
case "list": {
|
|
44
|
+
const scope = parseAccessKeyScope(stringFlag(args.flags, "scope"), false);
|
|
45
|
+
const response = await client.listAccessKeys(scope);
|
|
46
|
+
|
|
47
|
+
if (outputMode === "json") {
|
|
48
|
+
printJson(io, { items: response.items });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
printValue(
|
|
53
|
+
io,
|
|
54
|
+
outputMode,
|
|
55
|
+
response.items.map((item) => formatAccessKeyRow(item)),
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
case "issue": {
|
|
61
|
+
const scope = requireAccessKeyScope(
|
|
62
|
+
requiredStringFlag(args.flags, "scope"),
|
|
63
|
+
);
|
|
64
|
+
const response = await client.issueAccessKey({
|
|
65
|
+
scope,
|
|
66
|
+
name: requiredStringFlag(args.flags, "name"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (booleanFlag(args.flags, "tokenOnly")) {
|
|
70
|
+
io.stdout(response.token);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
printValue(
|
|
75
|
+
io,
|
|
76
|
+
outputMode,
|
|
77
|
+
outputMode === "json"
|
|
78
|
+
? response
|
|
79
|
+
: renderAccessKeyIssue("Issued owner access key", response),
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "revoke": {
|
|
85
|
+
const response = await client.revokeAccessKey(
|
|
86
|
+
requiredPositional(args.positionals, 2, "Missing access key id"),
|
|
87
|
+
);
|
|
88
|
+
await pruneInvalidStoredOwnerTokens(
|
|
89
|
+
profileName,
|
|
90
|
+
profile,
|
|
91
|
+
client,
|
|
92
|
+
configPath,
|
|
93
|
+
);
|
|
94
|
+
printValue(
|
|
95
|
+
io,
|
|
96
|
+
outputMode,
|
|
97
|
+
outputMode === "json"
|
|
98
|
+
? response
|
|
99
|
+
: renderAccessKeyRevoke("Revoked owner access key", response),
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
default:
|
|
105
|
+
throw new CliError("Unknown auth key subcommand", 2);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseAccessKeyScope(
|
|
110
|
+
scope: string | undefined,
|
|
111
|
+
required: boolean,
|
|
112
|
+
): AccessKeyScope | undefined {
|
|
113
|
+
if (scope === undefined) {
|
|
114
|
+
if (required) {
|
|
115
|
+
throw new CliError("Missing `--scope`", 2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (scope !== "master" && scope !== "read_only") {
|
|
122
|
+
throw new CliError("`--scope` must be either `master` or `read_only`.", 2);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return scope;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function requireAccessKeyScope(scope: string): AccessKeyScope {
|
|
129
|
+
const parsed = parseAccessKeyScope(scope, true);
|
|
130
|
+
|
|
131
|
+
if (!parsed) {
|
|
132
|
+
throw new CliError("Missing `--scope`", 2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatAccessKeyRow(item: { id: string; attributes: AccessKeyAttributes }) {
|
|
139
|
+
return {
|
|
140
|
+
id: item.id,
|
|
141
|
+
scope: item.attributes.scope,
|
|
142
|
+
name: item.attributes.name ?? "",
|
|
143
|
+
expiresAt: item.attributes.expires_at,
|
|
144
|
+
issuedAt: item.attributes.inserted_at ?? "",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderAccessKeyIssue(
|
|
149
|
+
title: string,
|
|
150
|
+
response: AccessKeyIssueResponse,
|
|
151
|
+
): string {
|
|
152
|
+
return renderTokenAction({
|
|
153
|
+
title,
|
|
154
|
+
id: response.id,
|
|
155
|
+
name: response.name,
|
|
156
|
+
token: response.token,
|
|
157
|
+
issuedAt: response.issued_at,
|
|
158
|
+
expiresAt: response.expires_at,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderAccessKeyRevoke(
|
|
163
|
+
title: string,
|
|
164
|
+
response: AccessKeyRevokeResponse,
|
|
165
|
+
): string {
|
|
166
|
+
return joinBlocks([
|
|
167
|
+
title,
|
|
168
|
+
renderFields([
|
|
169
|
+
["ID", response.id],
|
|
170
|
+
["Scope", response.scope],
|
|
171
|
+
["Name", response.name],
|
|
172
|
+
]),
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function pruneInvalidStoredOwnerTokens(
|
|
177
|
+
profileName: string,
|
|
178
|
+
profile: ProfileConfig,
|
|
179
|
+
client: ClankmatesClient,
|
|
180
|
+
configPath: string,
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
const invalidMasterToken = profile.masterToken
|
|
183
|
+
? !(await client.canAuthenticate(profile.masterToken))
|
|
184
|
+
: false;
|
|
185
|
+
const invalidReadOnlyToken = profile.readOnlyToken
|
|
186
|
+
? !(await client.canAuthenticate(profile.readOnlyToken))
|
|
187
|
+
: false;
|
|
188
|
+
|
|
189
|
+
if (!invalidMasterToken && !invalidReadOnlyToken) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await updateProfile(
|
|
194
|
+
profileName,
|
|
195
|
+
(storedProfile) => {
|
|
196
|
+
if (invalidMasterToken) {
|
|
197
|
+
storedProfile.masterToken = undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (invalidReadOnlyToken) {
|
|
201
|
+
storedProfile.readOnlyToken = undefined;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
configPath,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { resolveOutputMode } from "../lib/context";
|
|
2
|
+
import {
|
|
3
|
+
booleanFlag,
|
|
4
|
+
requiredPositional,
|
|
5
|
+
stringFlag,
|
|
6
|
+
type ParsedArgs,
|
|
7
|
+
} from "../lib/args";
|
|
8
|
+
import {
|
|
9
|
+
loadConfig,
|
|
10
|
+
resolveBaseUrl,
|
|
11
|
+
resolveProfile,
|
|
12
|
+
resolveProfileName,
|
|
13
|
+
updateProfile,
|
|
14
|
+
} from "../lib/config";
|
|
15
|
+
import { ClankmatesClient } from "../lib/client";
|
|
16
|
+
import { CliError } from "../lib/errors";
|
|
17
|
+
import { printValue, type Io } from "../lib/output";
|
|
18
|
+
import { getConfigPath } from "../lib/paths";
|
|
19
|
+
import {
|
|
20
|
+
resolveMasterToken,
|
|
21
|
+
resolveOwnerReadToken,
|
|
22
|
+
resolveReadOnlyToken,
|
|
23
|
+
} from "../lib/tokens";
|
|
24
|
+
import type { ProfileConfig, WhoamiResponse } from "../types/api";
|
|
25
|
+
import { runAccessKeyCommand } from "./auth/access-keys";
|
|
26
|
+
|
|
27
|
+
export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
28
|
+
const subcommand = args.positionals[0];
|
|
29
|
+
const configPath = getConfigPath();
|
|
30
|
+
const config = await loadConfig(configPath);
|
|
31
|
+
|
|
32
|
+
switch (subcommand) {
|
|
33
|
+
case "login": {
|
|
34
|
+
const requestedProfile = stringFlag(args.flags, "profile");
|
|
35
|
+
const profileName = resolveProfileName(config, requestedProfile);
|
|
36
|
+
const profile = config.profiles[profileName] ?? defaultProfileConfig();
|
|
37
|
+
const outputMode = resolveOutputMode(profile, args.flags);
|
|
38
|
+
const masterToken = stringFlag(args.flags, "masterToken");
|
|
39
|
+
const readOnlyToken = stringFlag(args.flags, "readOnlyToken");
|
|
40
|
+
const baseUrl = stringFlag(args.flags, "baseUrl");
|
|
41
|
+
const resolvedBaseUrl = resolveBaseUrl(baseUrl, profile.baseUrl);
|
|
42
|
+
const validationProfile = {
|
|
43
|
+
...profile,
|
|
44
|
+
baseUrl: resolvedBaseUrl,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (Boolean(masterToken) === Boolean(readOnlyToken)) {
|
|
48
|
+
throw new CliError(
|
|
49
|
+
"Provide exactly one of `--master-token` or `--read-only-token`.",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const client = new ClankmatesClient(validationProfile);
|
|
54
|
+
|
|
55
|
+
if (masterToken) {
|
|
56
|
+
await client.validateMasterToken(masterToken);
|
|
57
|
+
} else if (readOnlyToken) {
|
|
58
|
+
await client.validateReadOnlyToken(readOnlyToken);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await updateProfile(
|
|
62
|
+
profileName,
|
|
63
|
+
(storedProfile) => {
|
|
64
|
+
if (masterToken) {
|
|
65
|
+
storedProfile.masterToken = masterToken;
|
|
66
|
+
storedProfile.readOnlyToken = undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (readOnlyToken) {
|
|
70
|
+
storedProfile.readOnlyToken = readOnlyToken;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (baseUrl) {
|
|
74
|
+
storedProfile.baseUrl = baseUrl;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
configPath,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
printValue(io, outputMode, {
|
|
81
|
+
authenticated: true,
|
|
82
|
+
profile: profileName,
|
|
83
|
+
baseUrl: resolvedBaseUrl,
|
|
84
|
+
tokenKind: masterToken ? "master" : "read_only",
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case "whoami": {
|
|
90
|
+
const { profileName, profile } = resolveProfile(
|
|
91
|
+
config,
|
|
92
|
+
stringFlag(args.flags, "profile"),
|
|
93
|
+
stringFlag(args.flags, "baseUrl"),
|
|
94
|
+
);
|
|
95
|
+
const outputMode = resolveOutputMode(profile, args.flags);
|
|
96
|
+
const explicitIdentityKey = stringFlag(args.flags, "identityKey");
|
|
97
|
+
const resolvedOwnerReadToken = resolveOwnerReadToken(profile);
|
|
98
|
+
const resolvedToken = explicitIdentityKey
|
|
99
|
+
? {
|
|
100
|
+
token: explicitIdentityKey,
|
|
101
|
+
source: "flag",
|
|
102
|
+
tokenKind: "identity" as const,
|
|
103
|
+
}
|
|
104
|
+
: {
|
|
105
|
+
token: resolvedOwnerReadToken.token,
|
|
106
|
+
source: resolvedOwnerReadToken.source,
|
|
107
|
+
tokenKind: "owner_read" as const,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (!resolvedToken.token) {
|
|
111
|
+
throw new CliError(
|
|
112
|
+
"No token configured for `auth whoami`. Provide `--identity-key`, set `CLANKID_READ_ONLY_TOKEN`, `CLANKID_MASTER_TOKEN`, or log in for the selected profile.",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const whoami = await new ClankmatesClient(profile).whoami(
|
|
117
|
+
resolvedToken.token,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
printValue(
|
|
121
|
+
io,
|
|
122
|
+
outputMode,
|
|
123
|
+
formatWhoamiOutput(profileName, profile.baseUrl, resolvedToken, whoami),
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "logout": {
|
|
129
|
+
const { profileName } = resolveProfile(
|
|
130
|
+
config,
|
|
131
|
+
stringFlag(args.flags, "profile"),
|
|
132
|
+
);
|
|
133
|
+
await updateProfile(
|
|
134
|
+
profileName,
|
|
135
|
+
(profile) => {
|
|
136
|
+
profile.masterToken = undefined;
|
|
137
|
+
profile.readOnlyToken = undefined;
|
|
138
|
+
},
|
|
139
|
+
configPath,
|
|
140
|
+
);
|
|
141
|
+
io.stdout(`Cleared owner tokens for profile ${profileName}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case "token": {
|
|
146
|
+
const tokenCommand = requiredPositional(
|
|
147
|
+
args.positionals,
|
|
148
|
+
1,
|
|
149
|
+
"Missing auth token subcommand",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (tokenCommand !== "inspect") {
|
|
153
|
+
throw new CliError("Unknown auth token subcommand", 2);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { profileName, profile } = resolveProfile(
|
|
157
|
+
config,
|
|
158
|
+
stringFlag(args.flags, "profile"),
|
|
159
|
+
stringFlag(args.flags, "baseUrl"),
|
|
160
|
+
);
|
|
161
|
+
const outputMode = resolveOutputMode(profile, args.flags);
|
|
162
|
+
const resolvedMasterToken = resolveMasterToken(profile);
|
|
163
|
+
const resolvedReadOnlyToken = resolveReadOnlyToken(profile);
|
|
164
|
+
const resolvedOwnerReadToken = resolveOwnerReadToken(profile);
|
|
165
|
+
|
|
166
|
+
printValue(io, outputMode, {
|
|
167
|
+
profile: profileName,
|
|
168
|
+
baseUrl: profile.baseUrl,
|
|
169
|
+
hasMasterToken: Boolean(resolvedMasterToken.token),
|
|
170
|
+
masterTokenSource: resolvedMasterToken.source,
|
|
171
|
+
hasReadOnlyToken: Boolean(resolvedReadOnlyToken.token),
|
|
172
|
+
readOnlyTokenSource: resolvedReadOnlyToken.source,
|
|
173
|
+
ownerReadTokenAvailable: Boolean(resolvedOwnerReadToken.token),
|
|
174
|
+
ownerReadTokenSource: resolvedOwnerReadToken.source,
|
|
175
|
+
storedIdentityKeys: Object.keys(profile.identityKeys).length,
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "key":
|
|
181
|
+
case "access-key": {
|
|
182
|
+
await runAccessKeyCommand(args, config, io);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
default:
|
|
187
|
+
throw new CliError("Unknown auth subcommand", 2);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function defaultProfileConfig(): ProfileConfig {
|
|
192
|
+
return {
|
|
193
|
+
baseUrl: resolveBaseUrl(),
|
|
194
|
+
output: "table",
|
|
195
|
+
identityKeys: {},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function formatWhoamiOutput(
|
|
200
|
+
profileName: string,
|
|
201
|
+
baseUrl: string,
|
|
202
|
+
resolvedToken: { source: string; tokenKind: "owner_read" | "identity" },
|
|
203
|
+
whoami: WhoamiResponse,
|
|
204
|
+
) {
|
|
205
|
+
const base = {
|
|
206
|
+
authenticated: whoami.authenticated,
|
|
207
|
+
profile: profileName,
|
|
208
|
+
baseUrl,
|
|
209
|
+
tokenKind: resolvedToken.tokenKind,
|
|
210
|
+
tokenSource: resolvedToken.source,
|
|
211
|
+
ownerTokenSource:
|
|
212
|
+
resolvedToken.tokenKind === "owner_read" ? resolvedToken.source : undefined,
|
|
213
|
+
actorType: whoami.actor.type,
|
|
214
|
+
actorId: whoami.actor.id,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (whoami.actor.type === "user") {
|
|
218
|
+
return {
|
|
219
|
+
...base,
|
|
220
|
+
email: whoami.actor.email,
|
|
221
|
+
actorScope: whoami.actor.scope ?? "master",
|
|
222
|
+
authenticatedVia: whoami.actor.authenticated_via ?? "",
|
|
223
|
+
publicHandle: whoami.actor.public_handle ?? "",
|
|
224
|
+
publicProfilePath: whoami.actor.public_profile_path ?? "",
|
|
225
|
+
publicProfileUrl: whoami.actor.public_profile_url ?? "",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
...base,
|
|
231
|
+
name: whoami.actor.name,
|
|
232
|
+
visibility: whoami.actor.visibility,
|
|
233
|
+
publiclyListed: whoami.actor.publicly_listed ?? false,
|
|
234
|
+
postingPausedUntil: whoami.actor.posting_paused_until ?? "",
|
|
235
|
+
ownerId: whoami.actor.owner_id ?? "",
|
|
236
|
+
ownerPublicHandle: whoami.actor.owner_public_handle ?? "",
|
|
237
|
+
publicPath: whoami.actor.public_path ?? "",
|
|
238
|
+
publicUrl: whoami.actor.public_url ?? "",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { stringFlag, type ParsedArgs } from "../lib/args";
|
|
2
|
+
import {
|
|
3
|
+
openSyncCache,
|
|
4
|
+
type SyncScopeRow,
|
|
5
|
+
} from "../lib/cache";
|
|
6
|
+
import { createCommandContext } from "../lib/context";
|
|
7
|
+
import { CliError } from "../lib/errors";
|
|
8
|
+
import { formatTimestamp, renderFields } from "../lib/human";
|
|
9
|
+
import { getCachePath } from "../lib/paths";
|
|
10
|
+
import { printValue, type Io } from "../lib/output";
|
|
11
|
+
|
|
12
|
+
export async function runCacheCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
13
|
+
const subcommand = args.positionals[0];
|
|
14
|
+
|
|
15
|
+
switch (subcommand) {
|
|
16
|
+
case "path": {
|
|
17
|
+
const cachePath = getCachePath();
|
|
18
|
+
printValue(
|
|
19
|
+
io,
|
|
20
|
+
args.flags.json === true ? "json" : "table",
|
|
21
|
+
args.flags.json === true ? { path: cachePath } : cachePath,
|
|
22
|
+
);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
case "status": {
|
|
27
|
+
const context = await createCommandContext(args, io);
|
|
28
|
+
const cache = await openSyncCache();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const rows = cache.list({
|
|
32
|
+
baseUrl: context.profile.baseUrl,
|
|
33
|
+
profile: context.profileName,
|
|
34
|
+
});
|
|
35
|
+
printValue(
|
|
36
|
+
io,
|
|
37
|
+
context.outputMode,
|
|
38
|
+
context.outputMode === "json"
|
|
39
|
+
? {
|
|
40
|
+
path: cache.path(),
|
|
41
|
+
profile: context.profileName,
|
|
42
|
+
baseUrl: context.profile.baseUrl,
|
|
43
|
+
scopes: rows.map(renderJsonScope),
|
|
44
|
+
}
|
|
45
|
+
: renderStatus(cache.path(), rows),
|
|
46
|
+
);
|
|
47
|
+
} finally {
|
|
48
|
+
cache.close();
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "clear": {
|
|
54
|
+
const context = await createCommandContext(args, io);
|
|
55
|
+
const scope = stringFlag(args.flags, "scope");
|
|
56
|
+
const cache = await openSyncCache();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const deleted = cache.clear(
|
|
60
|
+
scope
|
|
61
|
+
? { scopeKey: scope }
|
|
62
|
+
: {
|
|
63
|
+
baseUrl: context.profile.baseUrl,
|
|
64
|
+
profile: context.profileName,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
printValue(
|
|
68
|
+
io,
|
|
69
|
+
context.outputMode,
|
|
70
|
+
context.outputMode === "json"
|
|
71
|
+
? { ok: true, deleted, scope: scope ?? null }
|
|
72
|
+
: `Cleared ${deleted} cache ${deleted === 1 ? "scope" : "scopes"}.`,
|
|
73
|
+
);
|
|
74
|
+
} finally {
|
|
75
|
+
cache.close();
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
default:
|
|
81
|
+
throw new CliError("Unknown cache subcommand", 2);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderJsonScope(row: SyncScopeRow): Record<string, unknown> {
|
|
86
|
+
return {
|
|
87
|
+
scopeKey: row.scope_key,
|
|
88
|
+
baseUrl: row.base_url,
|
|
89
|
+
profile: row.profile,
|
|
90
|
+
actorKey: row.actor_key,
|
|
91
|
+
resource: row.resource,
|
|
92
|
+
params: JSON.parse(row.params_json),
|
|
93
|
+
serverTimestamp: row.server_timestamp,
|
|
94
|
+
cachedAt: row.cached_at,
|
|
95
|
+
cliVersion: row.cli_version,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderStatus(cachePath: string, rows: SyncScopeRow[]): string {
|
|
100
|
+
if (rows.length === 0) {
|
|
101
|
+
return renderFields([
|
|
102
|
+
["Cache", cachePath],
|
|
103
|
+
["Scopes", "0"],
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const table = rows
|
|
108
|
+
.map((row) => ({
|
|
109
|
+
resource: row.resource,
|
|
110
|
+
scopeKey: row.scope_key,
|
|
111
|
+
serverTimestamp: formatTimestamp(row.server_timestamp),
|
|
112
|
+
cachedAt: formatTimestamp(row.cached_at),
|
|
113
|
+
}))
|
|
114
|
+
.map(
|
|
115
|
+
(row) =>
|
|
116
|
+
`${row.resource}\n scope: ${row.scopeKey}\n server timestamp: ${row.serverTimestamp}\n cached: ${row.cachedAt}`,
|
|
117
|
+
)
|
|
118
|
+
.join("\n\n");
|
|
119
|
+
|
|
120
|
+
return `${renderFields([
|
|
121
|
+
["Cache", cachePath],
|
|
122
|
+
["Scopes", String(rows.length)],
|
|
123
|
+
])}\n\n${table}`;
|
|
124
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureConfig,
|
|
3
|
+
ensureProfile,
|
|
4
|
+
loadConfig,
|
|
5
|
+
resolveProfile,
|
|
6
|
+
resolveProfileName,
|
|
7
|
+
setActiveProfile,
|
|
8
|
+
setProfileBaseUrl,
|
|
9
|
+
setProfileOutput
|
|
10
|
+
} from "../lib/config";
|
|
11
|
+
import { booleanFlag, requiredPositional, stringFlag, type ParsedArgs } from "../lib/args";
|
|
12
|
+
import { CliError } from "../lib/errors";
|
|
13
|
+
import { printValue, type Io } from "../lib/output";
|
|
14
|
+
import { getConfigPath } from "../lib/paths";
|
|
15
|
+
|
|
16
|
+
export async function runConfigCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
17
|
+
const subcommand = requiredPositional(args.positionals, 0, "Missing config subcommand");
|
|
18
|
+
const configPath = getConfigPath();
|
|
19
|
+
|
|
20
|
+
switch (subcommand) {
|
|
21
|
+
case "init": {
|
|
22
|
+
const baseUrl = stringFlag(args.flags, "baseUrl");
|
|
23
|
+
const requestedProfile = stringFlag(args.flags, "profile");
|
|
24
|
+
const config = requestedProfile
|
|
25
|
+
? await ensureProfile(resolveProfileName(await loadConfig(configPath), requestedProfile), configPath, baseUrl)
|
|
26
|
+
: await ensureConfig(configPath, baseUrl);
|
|
27
|
+
const { profileName, profile } = resolveProfile(config, requestedProfile);
|
|
28
|
+
|
|
29
|
+
printValue(io, booleanFlag(args.flags, "json") ? "json" : "table", {
|
|
30
|
+
configPath,
|
|
31
|
+
profile: profileName,
|
|
32
|
+
activeProfile: config.activeProfile,
|
|
33
|
+
baseUrl: profile.baseUrl,
|
|
34
|
+
output: profile.output
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case "set": {
|
|
40
|
+
const key = requiredPositional(args.positionals, 1, "Missing config key");
|
|
41
|
+
const value = requiredPositional(args.positionals, 2, "Missing config value");
|
|
42
|
+
const config = await loadConfig(configPath);
|
|
43
|
+
const { profileName } = resolveProfile(config, stringFlag(args.flags, "profile"));
|
|
44
|
+
|
|
45
|
+
if (key === "base-url") {
|
|
46
|
+
await setProfileBaseUrl(profileName, value, configPath);
|
|
47
|
+
io.stdout(`Set base URL for profile ${profileName} to ${value}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (key === "output") {
|
|
52
|
+
if (value !== "json" && value !== "table") {
|
|
53
|
+
throw new CliError("Output must be `json` or `table`", 2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await setProfileOutput(profileName, value, configPath);
|
|
57
|
+
io.stdout(`Set output for profile ${profileName} to ${value}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new CliError(`Unknown config key "${key}"`, 2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "profile": {
|
|
65
|
+
const profileCommand = requiredPositional(args.positionals, 1, "Missing profile subcommand");
|
|
66
|
+
|
|
67
|
+
if (profileCommand === "list") {
|
|
68
|
+
const config = await loadConfig(configPath);
|
|
69
|
+
const rows = Object.entries(config.profiles).map(([name, profile]) => ({
|
|
70
|
+
name,
|
|
71
|
+
active: name === config.activeProfile ? "yes" : "",
|
|
72
|
+
baseUrl: profile.baseUrl,
|
|
73
|
+
output: profile.output
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
printValue(io, booleanFlag(args.flags, "json") ? "json" : "table", rows);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (profileCommand === "use") {
|
|
81
|
+
const name = requiredPositional(args.positionals, 2, "Missing profile name");
|
|
82
|
+
const config = await setActiveProfile(name, configPath);
|
|
83
|
+
io.stdout(`Active profile is now ${config.activeProfile}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new CliError(`Unknown profile subcommand "${profileCommand}"`, 2);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
default:
|
|
91
|
+
throw new CliError(`Unknown config subcommand "${subcommand}"`, 2);
|
|
92
|
+
}
|
|
93
|
+
}
|