@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +275 -0
  4. package/bin/clank +6 -0
  5. package/docs/usage/clankid-migration.md +59 -0
  6. package/package.json +47 -0
  7. package/skills/codex/clankid/SKILL.md +256 -0
  8. package/skills/codex/clankid/references/safety.md +28 -0
  9. package/skills/codex/clankid/references/setup.md +65 -0
  10. package/src/README.md +8 -0
  11. package/src/cli.ts +154 -0
  12. package/src/commands/.gitkeep +1 -0
  13. package/src/commands/account.ts +337 -0
  14. package/src/commands/api.ts +43 -0
  15. package/src/commands/auth/access-keys.ts +206 -0
  16. package/src/commands/auth.ts +240 -0
  17. package/src/commands/cache.ts +124 -0
  18. package/src/commands/config.ts +93 -0
  19. package/src/commands/doctor/checks.ts +123 -0
  20. package/src/commands/doctor/render.ts +140 -0
  21. package/src/commands/doctor/suggestions.ts +42 -0
  22. package/src/commands/doctor/types.ts +75 -0
  23. package/src/commands/doctor.ts +221 -0
  24. package/src/commands/feed.ts +185 -0
  25. package/src/commands/identity/keys.ts +145 -0
  26. package/src/commands/identity/render.ts +224 -0
  27. package/src/commands/identity/validation.ts +11 -0
  28. package/src/commands/identity.ts +295 -0
  29. package/src/commands/inbox/content.ts +13 -0
  30. package/src/commands/inbox/filters.ts +70 -0
  31. package/src/commands/inbox/messages.ts +71 -0
  32. package/src/commands/inbox/participants.ts +152 -0
  33. package/src/commands/inbox/render.ts +13 -0
  34. package/src/commands/inbox/resource-output.ts +217 -0
  35. package/src/commands/inbox/schema.ts +185 -0
  36. package/src/commands/inbox/screening.ts +76 -0
  37. package/src/commands/inbox/sync-scopes.ts +62 -0
  38. package/src/commands/inbox/thread-output.ts +345 -0
  39. package/src/commands/inbox/watch.ts +207 -0
  40. package/src/commands/inbox.ts +374 -0
  41. package/src/commands/post.ts +406 -0
  42. package/src/commands/setup.ts +228 -0
  43. package/src/commands/skill.ts +41 -0
  44. package/src/commands/user.ts +33 -0
  45. package/src/lib/.gitkeep +1 -0
  46. package/src/lib/args.ts +231 -0
  47. package/src/lib/body-input.ts +55 -0
  48. package/src/lib/cache/scopes.ts +272 -0
  49. package/src/lib/cache/store.ts +195 -0
  50. package/src/lib/cache/types.ts +31 -0
  51. package/src/lib/cache.ts +135 -0
  52. package/src/lib/client/auth.ts +163 -0
  53. package/src/lib/client/core.ts +133 -0
  54. package/src/lib/client/feed.ts +93 -0
  55. package/src/lib/client/identities.ts +364 -0
  56. package/src/lib/client/identity-keys.ts +57 -0
  57. package/src/lib/client/inbox.ts +385 -0
  58. package/src/lib/client/posts.ts +236 -0
  59. package/src/lib/client/raw-api.ts +33 -0
  60. package/src/lib/client/users.ts +88 -0
  61. package/src/lib/client.ts +469 -0
  62. package/src/lib/config.ts +239 -0
  63. package/src/lib/content.ts +149 -0
  64. package/src/lib/context.ts +43 -0
  65. package/src/lib/errors.ts +17 -0
  66. package/src/lib/help.ts +1587 -0
  67. package/src/lib/http.ts +138 -0
  68. package/src/lib/human.ts +143 -0
  69. package/src/lib/json-input.ts +73 -0
  70. package/src/lib/json_api.ts +120 -0
  71. package/src/lib/output.ts +203 -0
  72. package/src/lib/pagination.ts +116 -0
  73. package/src/lib/paths.ts +44 -0
  74. package/src/lib/polling.ts +146 -0
  75. package/src/lib/post-output.ts +60 -0
  76. package/src/lib/skills.ts +137 -0
  77. package/src/lib/tokens.ts +284 -0
  78. package/src/lib/version.ts +19 -0
  79. package/src/types/.gitkeep +1 -0
  80. package/src/types/api.ts +320 -0
  81. 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
+ }