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 +1 -1
- package/src/cli/main.ts +72 -163
- package/src/cli/runtime/auth/resolve.ts +20 -0
- package/src/cli/runtime/request.ts +10 -7
package/package.json
CHANGED
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
|
-
|
|
67
|
-
|
|
68
|
-
.description("Manage specli profiles");
|
|
63
|
+
// Simple auth commands
|
|
64
|
+
const defaultProfileName = "default";
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
.command("
|
|
72
|
-
.description("
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
process.
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
process.stdout.write(
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
-
|
|
173
|
-
.command("
|
|
174
|
-
.description("
|
|
175
|
-
.
|
|
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
|
-
|
|
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(
|
|
131
|
+
process.stdout.write(
|
|
132
|
+
deleted ? "ok: logged out\n" : "ok: not logged in\n",
|
|
133
|
+
);
|
|
191
134
|
});
|
|
192
135
|
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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,
|
|
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,
|