specli 0.0.4 → 0.0.5

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "specli",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.0.4",
5
+ "version": "0.0.5",
6
6
  "bin": {
7
7
  "specli": "./cli.ts"
8
8
  },
package/src/cli/main.ts CHANGED
@@ -6,9 +6,7 @@ import { buildRuntimeContext } from "./runtime/context.ts";
6
6
  import { addGeneratedCommands } from "./runtime/generated.ts";
7
7
  import { deleteToken, getToken, setToken } from "./runtime/profile/secrets.ts";
8
8
  import {
9
- getProfile,
10
9
  readProfiles,
11
- removeProfile,
12
10
  upsertProfile,
13
11
  writeProfiles,
14
12
  } from "./runtime/profile/store.ts";
@@ -42,7 +40,6 @@ export async function main(argv: string[], options: MainOptions = {}) {
42
40
  .option("--username <username>", "Basic auth username")
43
41
  .option("--password <password>", "Basic auth password")
44
42
  .option("--api-key <key>", "API key value")
45
- .option("--profile <name>", "Profile name (stored under ~/.config/specli)")
46
43
  .option("--json", "Machine-readable output")
47
44
  .showHelpAfterError();
48
45
 
@@ -63,197 +60,109 @@ export async function main(argv: string[], options: MainOptions = {}) {
63
60
  embeddedSpecText: options.embeddedSpecText,
64
61
  });
65
62
 
66
- const profileCmd = program
67
- .command("profile")
68
- .description("Manage specli profiles");
63
+ // Simple auth commands
64
+ const defaultProfileName = "default";
69
65
 
70
- profileCmd
71
- .command("list")
72
- .description("List profiles")
73
- .action(async (_opts, command) => {
66
+ program
67
+ .command("login [token]")
68
+ .description("Store a bearer token for authentication")
69
+ .action(async (tokenArg: string | undefined, _opts, command) => {
74
70
  const globals = command.optsWithGlobals() as { json?: boolean };
75
- const file = await readProfiles();
76
-
77
- const payload = {
78
- defaultProfile: file.defaultProfile,
79
- profiles: file.profiles.map((p) => ({
80
- name: p.name,
81
- server: p.server,
82
- authScheme: p.authScheme,
83
- })),
84
- };
85
-
86
- if (globals.json) {
87
- process.stdout.write(`${JSON.stringify(payload)}\n`);
88
- return;
89
- }
90
71
 
91
- if (payload.defaultProfile) {
92
- process.stdout.write(`default: ${payload.defaultProfile}\n`);
93
- }
94
- for (const p of payload.profiles) {
95
- process.stdout.write(`${p.name}\n`);
96
- if (p.server) process.stdout.write(` server: ${p.server}\n`);
97
- if (p.authScheme)
98
- process.stdout.write(` authScheme: ${p.authScheme}\n`);
72
+ let token = tokenArg;
73
+
74
+ // If no token argument, try to read from stdin (for piping)
75
+ if (!token) {
76
+ const isTTY = process.stdin.isTTY;
77
+ if (isTTY) {
78
+ // Interactive mode - prompt user
79
+ process.stdout.write("Enter token: ");
80
+ const reader = process.stdin;
81
+ const chunks: Buffer[] = [];
82
+ for await (const chunk of reader) {
83
+ chunks.push(chunk);
84
+ // Read one line only
85
+ if (chunk.includes(10)) break; // newline
86
+ }
87
+ token = Buffer.concat(chunks).toString().trim();
88
+ } else {
89
+ // Piped input - use Bun's stdin stream
90
+ const text = await Bun.stdin.text();
91
+ token = text.trim();
92
+ }
99
93
  }
100
- });
101
94
 
102
- profileCmd
103
- .command("set")
104
- .description("Create or update a profile")
105
- .requiredOption("--name <name>", "Profile name")
106
- .option("--server <url>", "Default server/base URL")
107
- .option("--auth <scheme>", "Default auth scheme key")
108
- .option("--default", "Set as default profile")
109
- .action(async (opts, command) => {
110
- const globals = command.optsWithGlobals() as {
111
- json?: boolean;
112
- server?: string;
113
- auth?: string;
114
- };
115
-
116
- const file = await readProfiles();
117
- const next = upsertProfile(file, {
118
- name: String(opts.name),
119
- server:
120
- typeof opts.server === "string"
121
- ? opts.server
122
- : typeof globals.server === "string"
123
- ? globals.server
124
- : undefined,
125
- authScheme:
126
- typeof opts.auth === "string"
127
- ? opts.auth
128
- : typeof globals.auth === "string"
129
- ? globals.auth
130
- : undefined,
131
- });
132
- const final = opts.default
133
- ? { ...next, defaultProfile: String(opts.name) }
134
- : next;
135
- await writeProfiles(final);
136
-
137
- if (globals.json) {
138
- process.stdout.write(
139
- `${JSON.stringify({ ok: true, profile: String(opts.name) })}\n`,
95
+ if (!token) {
96
+ throw new Error(
97
+ "No token provided. Usage: login <token> or echo $TOKEN | login",
140
98
  );
141
- return;
142
99
  }
143
- process.stdout.write(`ok: profile ${String(opts.name)}\n`);
144
- });
145
100
 
146
- profileCmd
147
- .command("rm")
148
- .description("Remove a profile")
149
- .requiredOption("--name <name>", "Profile name")
150
- .action(async (opts, command) => {
151
- const globals = command.optsWithGlobals() as { json?: boolean };
101
+ // Ensure default profile exists
152
102
  const file = await readProfiles();
153
- const removed = removeProfile(file, String(opts.name));
154
-
155
- const final =
156
- file.defaultProfile === opts.name
157
- ? { ...removed, defaultProfile: undefined }
158
- : removed;
103
+ if (!file.profiles.find((p) => p.name === defaultProfileName)) {
104
+ const updated = upsertProfile(file, { name: defaultProfileName });
105
+ await writeProfiles({ ...updated, defaultProfile: defaultProfileName });
106
+ } else if (!file.defaultProfile) {
107
+ await writeProfiles({ ...file, defaultProfile: defaultProfileName });
108
+ }
159
109
 
160
- await writeProfiles(final);
110
+ await setToken(ctx.loaded.id, defaultProfileName, token);
161
111
 
162
112
  if (globals.json) {
163
- process.stdout.write(
164
- `${JSON.stringify({ ok: true, profile: String(opts.name) })}\n`,
165
- );
113
+ process.stdout.write(`${JSON.stringify({ ok: true })}\n`);
166
114
  return;
167
115
  }
168
-
169
- process.stdout.write(`ok: removed ${String(opts.name)}\n`);
116
+ process.stdout.write("ok: logged in\n");
170
117
  });
171
118
 
172
- profileCmd
173
- .command("use")
174
- .description("Set the default profile")
175
- .requiredOption("--name <name>", "Profile name")
176
- .action(async (opts, command) => {
119
+ program
120
+ .command("logout")
121
+ .description("Clear stored authentication token")
122
+ .action(async (_opts, command) => {
177
123
  const globals = command.optsWithGlobals() as { json?: boolean };
178
- const file = await readProfiles();
179
- const profile = getProfile(file, String(opts.name));
180
- if (!profile) throw new Error(`Profile not found: ${String(opts.name)}`);
181
124
 
182
- await writeProfiles({ ...file, defaultProfile: String(opts.name) });
125
+ const deleted = await deleteToken(ctx.loaded.id, defaultProfileName);
183
126
 
184
127
  if (globals.json) {
185
- process.stdout.write(
186
- `${JSON.stringify({ ok: true, defaultProfile: String(opts.name) })}\n`,
187
- );
128
+ process.stdout.write(`${JSON.stringify({ ok: deleted })}\n`);
188
129
  return;
189
130
  }
190
- process.stdout.write(`ok: default ${String(opts.name)}\n`);
131
+ process.stdout.write(
132
+ deleted ? "ok: logged out\n" : "ok: not logged in\n",
133
+ );
191
134
  });
192
135
 
193
- const authCmd = program.command("auth").description("Manage auth secrets");
136
+ program
137
+ .command("whoami")
138
+ .description("Show current authentication status")
139
+ .action(async (_opts, command) => {
140
+ const globals = command.optsWithGlobals() as { json?: boolean };
194
141
 
195
- authCmd
196
- .command("token")
197
- .description("Set or get bearer token for a profile")
198
- .option("--name <name>", "Profile name (defaults to global --profile)")
199
- .option("--set <token>", "Set token")
200
- .option("--get", "Get token")
201
- .option("--delete", "Delete token")
202
- .action(async (opts, command) => {
203
- const globals = command.optsWithGlobals() as {
204
- json?: boolean;
205
- profile?: string;
206
- };
142
+ const token = await getToken(ctx.loaded.id, defaultProfileName);
143
+ const hasToken = Boolean(token);
207
144
 
208
- const profileName = String(opts.name ?? globals.profile ?? "");
209
- if (!profileName) {
210
- throw new Error(
211
- "Missing profile name. Provide --name <name> or global --profile <name>.",
212
- );
213
- }
214
- if (opts.set && (opts.get || opts.delete)) {
215
- throw new Error("Use only one of --set, --get, --delete");
216
- }
217
- if (opts.get && opts.delete) {
218
- throw new Error("Use only one of --get or --delete");
219
- }
220
- if (!opts.set && !opts.get && !opts.delete) {
221
- throw new Error("Provide one of --set, --get, --delete");
145
+ // Mask the token for display (show first 8 and last 4 chars)
146
+ let maskedToken: string | null = null;
147
+ if (token && token.length > 16) {
148
+ maskedToken = `${token.slice(0, 8)}...${token.slice(-4)}`;
149
+ } else if (token) {
150
+ maskedToken = `${token.slice(0, 4)}...`;
222
151
  }
223
152
 
224
- if (typeof opts.set === "string") {
225
- await setToken(ctx.loaded.id, profileName, opts.set);
226
- if (globals.json) {
227
- process.stdout.write(
228
- `${JSON.stringify({ ok: true, profile: profileName })}\n`,
229
- );
230
- return;
231
- }
232
- process.stdout.write(`ok: token set for ${profileName}\n`);
233
- return;
234
- }
235
-
236
- if (opts.get) {
237
- const token = await getToken(ctx.loaded.id, profileName);
238
- if (globals.json) {
239
- process.stdout.write(
240
- `${JSON.stringify({ profile: profileName, token })}\n`,
241
- );
242
- return;
243
- }
244
- process.stdout.write(`${token ?? ""}\n`);
153
+ if (globals.json) {
154
+ process.stdout.write(
155
+ `${JSON.stringify({ authenticated: hasToken, token: maskedToken })}\n`,
156
+ );
245
157
  return;
246
158
  }
247
159
 
248
- if (opts.delete) {
249
- const ok = await deleteToken(ctx.loaded.id, profileName);
250
- if (globals.json) {
251
- process.stdout.write(
252
- `${JSON.stringify({ ok, profile: profileName })}\n`,
253
- );
254
- return;
255
- }
256
- process.stdout.write(`ok: ${ok ? "deleted" : "not-found"}\n`);
160
+ if (hasToken) {
161
+ process.stdout.write(`authenticated: yes\n`);
162
+ process.stdout.write(`token: ${maskedToken}\n`);
163
+ } else {
164
+ process.stdout.write(`authenticated: no\n`);
165
+ process.stdout.write(`Run 'login <token>' to authenticate.\n`);
257
166
  }
258
167
  });
259
168
 
@@ -4,8 +4,15 @@ export type AuthInputs = {
4
4
  flagAuthScheme?: string;
5
5
  profileAuthScheme?: string;
6
6
  embeddedAuthScheme?: string;
7
+ hasStoredToken?: boolean;
7
8
  };
8
9
 
10
+ const BEARER_COMPATIBLE_KINDS = new Set([
11
+ "http-bearer",
12
+ "oauth2",
13
+ "openIdConnect",
14
+ ]);
15
+
9
16
  export function resolveAuthScheme(
10
17
  authSchemes: AuthScheme[],
11
18
  required: import("../../auth-requirements.ts").AuthSummary,
@@ -35,5 +42,18 @@ export function resolveAuthScheme(
35
42
  // Otherwise if there is only one scheme in spec, pick it.
36
43
  if (authSchemes.length === 1) return authSchemes[0]?.key;
37
44
 
45
+ // If user has a stored token and operation accepts a bearer-compatible scheme,
46
+ // automatically pick the first one that matches.
47
+ if (inputs.hasStoredToken && alts.length > 0) {
48
+ for (const alt of alts) {
49
+ if (alt.length !== 1) continue;
50
+ const key = alt[0]?.key;
51
+ const scheme = authSchemes.find((s) => s.key === key);
52
+ if (scheme && BEARER_COMPATIBLE_KINDS.has(scheme.kind)) {
53
+ return key;
54
+ }
55
+ }
56
+ }
57
+
38
58
  return undefined;
39
59
  }
@@ -26,8 +26,6 @@ export type RuntimeGlobals = {
26
26
  username?: string;
27
27
  password?: string;
28
28
  apiKey?: string;
29
-
30
- profile?: string;
31
29
  };
32
30
 
33
31
  function parseKeyValuePairs(
@@ -155,8 +153,10 @@ export type BuildRequestInput = {
155
153
  export async function buildRequest(
156
154
  input: BuildRequestInput,
157
155
  ): Promise<{ request: Request; curl: string }> {
156
+ // Always use the "default" profile for simplicity
157
+ const defaultProfileName = "default";
158
158
  const profilesFile = await readProfiles();
159
- const profile = getProfile(profilesFile, input.globals.profile);
159
+ const profile = getProfile(profilesFile, defaultProfileName);
160
160
  const embedded = input.embeddedDefaults;
161
161
 
162
162
  // Merge server vars: CLI flags override embedded defaults
@@ -311,6 +311,11 @@ export async function buildRequest(
311
311
  }
312
312
  }
313
313
 
314
+ // Check if user has a stored token (needed for auth scheme auto-selection)
315
+ const storedToken = profile?.name
316
+ ? await getToken(input.specId, profile.name)
317
+ : null;
318
+
314
319
  // Auth resolution priority: CLI flag > profile > embedded default
315
320
  const resolvedAuthScheme = resolveAuthScheme(
316
321
  input.authSchemes,
@@ -319,13 +324,11 @@ export async function buildRequest(
319
324
  flagAuthScheme: input.globals.auth,
320
325
  profileAuthScheme: profile?.authScheme,
321
326
  embeddedAuthScheme: embedded?.auth,
327
+ hasStoredToken: Boolean(storedToken),
322
328
  },
323
329
  );
324
330
 
325
- const tokenFromProfile =
326
- profile?.name && resolvedAuthScheme
327
- ? await getToken(input.specId, profile.name)
328
- : null;
331
+ const tokenFromProfile = resolvedAuthScheme ? storedToken : null;
329
332
 
330
333
  const globalsWithProfileAuth: RuntimeGlobals = {
331
334
  ...input.globals,