specli 0.0.3 → 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/compile.ts +13 -8
- package/src/cli/main.ts +72 -163
- package/src/cli/runtime/auth/resolve.ts +20 -0
- package/src/cli/runtime/request.ts +16 -11
- package/src/cli/runtime/server-url.ts +2 -1
package/package.json
CHANGED
package/src/cli/compile.ts
CHANGED
|
@@ -65,18 +65,23 @@ export async function compileCommand(
|
|
|
65
65
|
|
|
66
66
|
buildArgs.push("./src/compiled.ts");
|
|
67
67
|
|
|
68
|
+
// Only set env vars that have actual values - avoid empty strings
|
|
69
|
+
// because the macros will embed them and they will override defaults.
|
|
70
|
+
const buildEnv: Record<string, string> = {
|
|
71
|
+
...process.env,
|
|
72
|
+
SPECLI_SPEC: spec,
|
|
73
|
+
SPECLI_NAME: name,
|
|
74
|
+
};
|
|
75
|
+
if (options.server) buildEnv.SPECLI_SERVER = options.server;
|
|
76
|
+
if (options.serverVar?.length)
|
|
77
|
+
buildEnv.SPECLI_SERVER_VARS = options.serverVar.join(",");
|
|
78
|
+
if (options.auth) buildEnv.SPECLI_AUTH = options.auth;
|
|
79
|
+
|
|
68
80
|
const proc = Bun.spawn({
|
|
69
81
|
cmd: ["bun", ...buildArgs],
|
|
70
82
|
stdout: "pipe",
|
|
71
83
|
stderr: "pipe",
|
|
72
|
-
env:
|
|
73
|
-
...process.env,
|
|
74
|
-
SPECLI_SPEC: spec,
|
|
75
|
-
SPECLI_NAME: name,
|
|
76
|
-
SPECLI_SERVER: options.server ?? "",
|
|
77
|
-
SPECLI_SERVER_VARS: options.serverVar?.join(",") ?? "",
|
|
78
|
-
SPECLI_AUTH: options.auth ?? "",
|
|
79
|
-
},
|
|
84
|
+
env: buildEnv,
|
|
80
85
|
});
|
|
81
86
|
|
|
82
87
|
const output = await new Response(proc.stdout).text();
|
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
|
|
@@ -188,10 +188,12 @@ export async function buildRequest(
|
|
|
188
188
|
|
|
189
189
|
const path = applyTemplate(input.action.path, pathVars, { encode: true });
|
|
190
190
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
191
|
+
// Build the full URL by combining server URL and path.
|
|
192
|
+
// We need to handle the case where path starts with "/" carefully:
|
|
193
|
+
// URL constructor treats absolute paths as relative to origin, not base path.
|
|
194
|
+
const baseUrl = serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
195
|
+
const relativePath = path.startsWith("/") ? path.slice(1) : path;
|
|
196
|
+
const url = new URL(relativePath, baseUrl);
|
|
195
197
|
|
|
196
198
|
const headers = new Headers();
|
|
197
199
|
|
|
@@ -309,6 +311,11 @@ export async function buildRequest(
|
|
|
309
311
|
}
|
|
310
312
|
}
|
|
311
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
|
+
|
|
312
319
|
// Auth resolution priority: CLI flag > profile > embedded default
|
|
313
320
|
const resolvedAuthScheme = resolveAuthScheme(
|
|
314
321
|
input.authSchemes,
|
|
@@ -317,13 +324,11 @@ export async function buildRequest(
|
|
|
317
324
|
flagAuthScheme: input.globals.auth,
|
|
318
325
|
profileAuthScheme: profile?.authScheme,
|
|
319
326
|
embeddedAuthScheme: embedded?.auth,
|
|
327
|
+
hasStoredToken: Boolean(storedToken),
|
|
320
328
|
},
|
|
321
329
|
);
|
|
322
330
|
|
|
323
|
-
const tokenFromProfile =
|
|
324
|
-
profile?.name && resolvedAuthScheme
|
|
325
|
-
? await getToken(input.specId, profile.name)
|
|
326
|
-
: null;
|
|
331
|
+
const tokenFromProfile = resolvedAuthScheme ? storedToken : null;
|
|
327
332
|
|
|
328
333
|
const globalsWithProfileAuth: RuntimeGlobals = {
|
|
329
334
|
...input.globals,
|
|
@@ -9,7 +9,8 @@ export type ResolveServerInput = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export function resolveServerUrl(input: ResolveServerInput): string {
|
|
12
|
-
|
|
12
|
+
// Treat empty string as undefined (serverOverride can come from env vars or profiles)
|
|
13
|
+
const base = input.serverOverride || input.servers[0]?.url;
|
|
13
14
|
if (!base) {
|
|
14
15
|
throw new Error(
|
|
15
16
|
"No server URL found. Provide --server <url> or define servers in the OpenAPI spec.",
|