@f5xc-salesdemos/xcsh 18.25.2 → 18.27.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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.27.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
49
49
|
"@mozilla/readability": "^0.6",
|
|
50
|
-
"@f5xc-salesdemos/xcsh-stats": "18.
|
|
51
|
-
"@f5xc-salesdemos/pi-agent-core": "18.
|
|
52
|
-
"@f5xc-salesdemos/pi-ai": "18.
|
|
53
|
-
"@f5xc-salesdemos/pi-natives": "18.
|
|
54
|
-
"@f5xc-salesdemos/pi-tui": "18.
|
|
55
|
-
"@f5xc-salesdemos/pi-utils": "18.
|
|
50
|
+
"@f5xc-salesdemos/xcsh-stats": "18.27.0",
|
|
51
|
+
"@f5xc-salesdemos/pi-agent-core": "18.27.0",
|
|
52
|
+
"@f5xc-salesdemos/pi-ai": "18.27.0",
|
|
53
|
+
"@f5xc-salesdemos/pi-natives": "18.27.0",
|
|
54
|
+
"@f5xc-salesdemos/pi-tui": "18.27.0",
|
|
55
|
+
"@f5xc-salesdemos/pi-utils": "18.27.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34",
|
|
57
57
|
"@xterm/headless": "^6.0",
|
|
58
58
|
"ajv": "^8.18",
|
|
@@ -122,7 +122,7 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<
|
|
|
122
122
|
if (cmd.subcommands) {
|
|
123
123
|
return {
|
|
124
124
|
...cmd,
|
|
125
|
-
getArgumentCompletions: buildArgumentCompletions(cmd.subcommands),
|
|
125
|
+
getArgumentCompletions: cmd.getArgumentCompletions ?? buildArgumentCompletions(cmd.subcommands),
|
|
126
126
|
getInlineHint: buildSubcommandInlineHint(cmd.subcommands),
|
|
127
127
|
};
|
|
128
128
|
}
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "18.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "18.27.0",
|
|
21
|
+
"commit": "5c365003b8532b2252bde64eeb9cc751e927752d",
|
|
22
|
+
"shortCommit": "5c36500",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v18.
|
|
25
|
-
"commitDate": "2026-04-
|
|
26
|
-
"buildDate": "2026-04-
|
|
24
|
+
"tag": "v18.27.0",
|
|
25
|
+
"commitDate": "2026-04-29T19:40:39Z",
|
|
26
|
+
"buildDate": "2026-04-29T20:01:37.365Z",
|
|
27
27
|
"dirty": false,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
|
|
30
30
|
"repoSlug": "f5xc-salesdemos/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.
|
|
31
|
+
"commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/5c365003b8532b2252bde64eeb9cc751e927752d",
|
|
32
|
+
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v18.27.0"
|
|
33
33
|
};
|
|
@@ -80,14 +80,16 @@ export async function handleContextCommand(
|
|
|
80
80
|
case "remove":
|
|
81
81
|
case "clear":
|
|
82
82
|
return handleEnvUnset(ctx, service, arg);
|
|
83
|
-
default:
|
|
84
|
-
|
|
83
|
+
default: {
|
|
84
|
+
ENV_SET_PATTERN.lastIndex = 0;
|
|
85
85
|
if (ENV_SET_PATTERN.test(command.args)) {
|
|
86
86
|
return handleEnvSet(ctx, service, command.args);
|
|
87
87
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
if (sub === "-") {
|
|
89
|
+
return handleActivatePrevious(ctx, service);
|
|
90
|
+
}
|
|
91
|
+
return handleDirectSwitch(ctx, service, sub!);
|
|
92
|
+
}
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
|
|
@@ -161,6 +163,36 @@ async function handleActivate(ctx: CommandContext, service: ContextService, name
|
|
|
161
163
|
}
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
async function handleDirectSwitch(ctx: CommandContext, service: ContextService, name: string): Promise<void> {
|
|
167
|
+
try {
|
|
168
|
+
await service.activate(name);
|
|
169
|
+
ctx.statusLine?.invalidate();
|
|
170
|
+
ctx.updateEditorTopBorder?.();
|
|
171
|
+
ctx.ui?.requestRender();
|
|
172
|
+
return handleShow(ctx, service);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (err instanceof ContextError && err.message.includes("not found")) {
|
|
175
|
+
ctx.showError(
|
|
176
|
+
`Context '${name}' not found. Run /context list to see available contexts, or /context create ${name} <url> <token> to create one.`,
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
ctx.showError(err instanceof ContextError ? err.message : String(err));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function handleActivatePrevious(ctx: CommandContext, service: ContextService): Promise<void> {
|
|
185
|
+
try {
|
|
186
|
+
await service.activatePrevious();
|
|
187
|
+
ctx.statusLine?.invalidate();
|
|
188
|
+
ctx.updateEditorTopBorder?.();
|
|
189
|
+
ctx.ui?.requestRender();
|
|
190
|
+
return handleShow(ctx, service);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
ctx.showError(err instanceof ContextError ? err.message : String(err));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
164
196
|
function isSensitiveKey(key: string): boolean {
|
|
165
197
|
return SECRET_ENV_PATTERNS.test(key);
|
|
166
198
|
}
|
|
@@ -498,6 +530,7 @@ const ENV_SET_PATTERN = /([A-Za-z_][A-Za-z0-9_]*)=(\S+)/g;
|
|
|
498
530
|
|
|
499
531
|
function parseEnvPairs(text: string): Record<string, string> {
|
|
500
532
|
const vars: Record<string, string> = {};
|
|
533
|
+
ENV_SET_PATTERN.lastIndex = 0;
|
|
501
534
|
for (const match of text.matchAll(ENV_SET_PATTERN)) {
|
|
502
535
|
vars[match[1]] = match[2];
|
|
503
536
|
}
|
|
@@ -72,6 +72,7 @@ export interface BuiltinSlashCommand {
|
|
|
72
72
|
subcommands?: SubcommandDef[];
|
|
73
73
|
/** Static inline hint when command takes a simple argument (no subcommands). */
|
|
74
74
|
inlineHint?: string;
|
|
75
|
+
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
interface ParsedBuiltinSlashCommand {
|
|
@@ -129,6 +130,235 @@ const shutdownHandler = (_command: ParsedBuiltinSlashCommand, runtime: BuiltinSl
|
|
|
129
130
|
void runtime.ctx.shutdown();
|
|
130
131
|
};
|
|
131
132
|
|
|
133
|
+
const CONTEXT_SUBCOMMANDS: SubcommandDef[] = [
|
|
134
|
+
{ name: "list", description: "List all contexts" },
|
|
135
|
+
{
|
|
136
|
+
name: "activate",
|
|
137
|
+
description: "Switch to a named context",
|
|
138
|
+
usage: "<name>",
|
|
139
|
+
getArgumentCompletions(prefix: string) {
|
|
140
|
+
if (prefix.includes(" ")) return null;
|
|
141
|
+
const svc = tryGetContextService();
|
|
142
|
+
if (!svc) return null;
|
|
143
|
+
const lower = prefix.toLowerCase();
|
|
144
|
+
const items = svc
|
|
145
|
+
.listContextNamesCached()
|
|
146
|
+
.filter(n => n.toLowerCase().startsWith(lower))
|
|
147
|
+
.map(n => {
|
|
148
|
+
const hint = svc.getContextHint(n);
|
|
149
|
+
const parts: string[] = [];
|
|
150
|
+
if (hint?.apiUrl) parts.push(hint.apiUrl);
|
|
151
|
+
if (hint?.incompatible && hint.schemaVersion !== undefined) {
|
|
152
|
+
parts.push(`incompatible: v${hint.schemaVersion}`);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
value: n,
|
|
156
|
+
label: n,
|
|
157
|
+
description: parts.length > 0 ? parts.join(" · ") : undefined,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
return items.length > 0 ? items : null;
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "validate",
|
|
165
|
+
description: "Validate credentials for a context without activating",
|
|
166
|
+
usage: "<name>",
|
|
167
|
+
getArgumentCompletions(prefix: string) {
|
|
168
|
+
if (prefix.includes(" ")) return null;
|
|
169
|
+
const svc = tryGetContextService();
|
|
170
|
+
if (!svc) return null;
|
|
171
|
+
const lower = prefix.toLowerCase();
|
|
172
|
+
const items = svc
|
|
173
|
+
.listContextNamesCached()
|
|
174
|
+
.filter(n => n.toLowerCase().startsWith(lower))
|
|
175
|
+
.map(n => {
|
|
176
|
+
const hint = svc.getContextHint(n);
|
|
177
|
+
const parts: string[] = [];
|
|
178
|
+
if (hint?.apiUrl) parts.push(hint.apiUrl);
|
|
179
|
+
if (hint?.incompatible && hint.schemaVersion !== undefined) {
|
|
180
|
+
parts.push(`incompatible: v${hint.schemaVersion}`);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
value: n,
|
|
184
|
+
label: n,
|
|
185
|
+
description: parts.length > 0 ? parts.join(" · ") : undefined,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
return items.length > 0 ? items : null;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{ name: "show", description: "Show context details (masked)", usage: "[name]" },
|
|
192
|
+
{ name: "status", description: "Show current auth status" },
|
|
193
|
+
{ name: "create", description: "Create a new context", usage: "<name> <url> <token> [namespace]" },
|
|
194
|
+
{ name: "delete", description: "Delete a context", usage: "<name> --confirm" },
|
|
195
|
+
{
|
|
196
|
+
name: "rename",
|
|
197
|
+
description: "Rename a context",
|
|
198
|
+
usage: "<old> <new>",
|
|
199
|
+
getArgumentCompletions(prefix: string) {
|
|
200
|
+
if (prefix.includes(" ")) return null;
|
|
201
|
+
const svc = tryGetContextService();
|
|
202
|
+
if (!svc) return null;
|
|
203
|
+
const lower = prefix.toLowerCase();
|
|
204
|
+
const items = svc
|
|
205
|
+
.listContextNamesCached()
|
|
206
|
+
.filter(n => n.toLowerCase().startsWith(lower))
|
|
207
|
+
.map(n => ({ value: n, label: n }));
|
|
208
|
+
return items.length > 0 ? items : null;
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "export",
|
|
213
|
+
description: "Export a context (or all contexts) as JSON",
|
|
214
|
+
usage: "[name] [--include-token]",
|
|
215
|
+
getArgumentCompletions(prefix: string) {
|
|
216
|
+
const svc = tryGetContextService();
|
|
217
|
+
if (!svc) return null;
|
|
218
|
+
const tokens = prefix.split(/\s+/).filter(Boolean);
|
|
219
|
+
const hasIncludeToken = tokens.includes("--include-token");
|
|
220
|
+
const positionalsTyped = tokens.filter(t => !t.startsWith("--"));
|
|
221
|
+
// Last token is "in-progress" if the prefix does not end with space.
|
|
222
|
+
const trailingSpace = prefix.endsWith(" ") || prefix === "";
|
|
223
|
+
const typedPositionalCount = trailingSpace
|
|
224
|
+
? positionalsTyped.length
|
|
225
|
+
: Math.max(0, positionalsTyped.length - 1);
|
|
226
|
+
const completingToken = trailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
|
|
227
|
+
// `head` is every already-typed token EXCEPT the one being
|
|
228
|
+
// completed. getArgumentCompletions.value replaces the whole
|
|
229
|
+
// argument tail, so value must carry every token the user
|
|
230
|
+
// should keep — otherwise accepting a suggestion silently
|
|
231
|
+
// drops the other args. Contract: see SubcommandDef JSDoc
|
|
232
|
+
// above (line ~58).
|
|
233
|
+
const headTokens = trailingSpace ? tokens : tokens.slice(0, -1);
|
|
234
|
+
const head = headTokens.length > 0 ? `${headTokens.join(" ")} ` : "";
|
|
235
|
+
|
|
236
|
+
const items: { value: string; label: string; description?: string }[] = [];
|
|
237
|
+
|
|
238
|
+
// Offer context names only if no positional has been filled yet.
|
|
239
|
+
// No startsWith("--") guard: context names legitimately allow
|
|
240
|
+
// leading dashes (the regex is /^[a-zA-Z0-9_-]{1,64}$/), and
|
|
241
|
+
// the handler's splitArgs uses a known-flags allowlist that
|
|
242
|
+
// treats only --include-token as a flag. So a context like
|
|
243
|
+
// `--prod` is valid; the completion filters by prefix and
|
|
244
|
+
// matches it naturally. When the user types `--in`, the
|
|
245
|
+
// flag-completion branch below matches `--include-token` by
|
|
246
|
+
// prefix; if there's ALSO a context starting with `--in` it
|
|
247
|
+
// is offered here. Both lists are disjoint by filter so
|
|
248
|
+
// there's no double-offer of the same token.
|
|
249
|
+
if (typedPositionalCount === 0) {
|
|
250
|
+
const lower = completingToken.toLowerCase();
|
|
251
|
+
for (const n of svc.listContextNamesCached()) {
|
|
252
|
+
if (!n.toLowerCase().startsWith(lower)) continue;
|
|
253
|
+
const hint = svc.getContextHint(n);
|
|
254
|
+
items.push({
|
|
255
|
+
value: `${head}${n}`,
|
|
256
|
+
label: n,
|
|
257
|
+
description: hint?.apiUrl,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Offer --include-token unless already present. Match is
|
|
263
|
+
// case-sensitive because the handler's flag check uses
|
|
264
|
+
// exact-match `flags.has("--include-token")` — offering
|
|
265
|
+
// the suggestion for mis-cased prefixes (e.g. `--INCLUDE`)
|
|
266
|
+
// would produce a suggestion the handler then ignores.
|
|
267
|
+
if (!hasIncludeToken && "--include-token".startsWith(completingToken)) {
|
|
268
|
+
items.push({
|
|
269
|
+
value: `${head}--include-token`,
|
|
270
|
+
label: "--include-token",
|
|
271
|
+
description: "emit unmasked tokens",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return items.length > 0 ? items : null;
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "import",
|
|
280
|
+
description: "Import contexts from a file path or inline JSON",
|
|
281
|
+
usage: "<path-or-json> [--overwrite]",
|
|
282
|
+
// No dynamic completion — paths are hard to complete correctly,
|
|
283
|
+
// and faking it would only mislead. Users pre-expand paths in
|
|
284
|
+
// their shell.
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "namespace",
|
|
288
|
+
description: "Switch namespace within active context",
|
|
289
|
+
usage: "<namespace>",
|
|
290
|
+
getArgumentCompletions(prefix: string) {
|
|
291
|
+
if (prefix.includes(" ")) return null;
|
|
292
|
+
const svc = tryGetContextService();
|
|
293
|
+
if (!svc) return null;
|
|
294
|
+
// setNamespace() requires an active context. Don't offer completions that
|
|
295
|
+
// would lead the user into a command path that cannot succeed (e.g. an
|
|
296
|
+
// env-backed session where cached namespaces came from startup validation
|
|
297
|
+
// but there is no active context to apply them to).
|
|
298
|
+
if (!svc.getStatus().activeContextName) return null;
|
|
299
|
+
const lower = prefix.toLowerCase();
|
|
300
|
+
const items = svc
|
|
301
|
+
.getCachedNamespaces()
|
|
302
|
+
.filter(n => n.toLowerCase().startsWith(lower))
|
|
303
|
+
.map(n => ({ value: n, label: n }));
|
|
304
|
+
return items.length > 0 ? items : null;
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{ name: "env", description: "Manage environment variables", usage: "set|unset|list [KEY=VALUE ...]" },
|
|
308
|
+
{ name: "set", description: "Set environment variable(s)", usage: "KEY=VALUE [KEY2=VALUE2 ...]" },
|
|
309
|
+
{
|
|
310
|
+
name: "unset",
|
|
311
|
+
description: "Remove environment variable(s)",
|
|
312
|
+
usage: "KEY [KEY2 ...]",
|
|
313
|
+
getArgumentCompletions(prefix: string) {
|
|
314
|
+
const lastSpace = prefix.lastIndexOf(" ");
|
|
315
|
+
const headRaw = lastSpace === -1 ? "" : prefix.slice(0, lastSpace + 1);
|
|
316
|
+
const tail = lastSpace === -1 ? prefix : prefix.slice(lastSpace + 1);
|
|
317
|
+
const svc = tryGetContextService();
|
|
318
|
+
if (!svc) return null;
|
|
319
|
+
const knownKeys = svc.getActiveEnvKeys();
|
|
320
|
+
const knownExact = new Set(knownKeys);
|
|
321
|
+
// Group known keys by their lowercased form so we can detect
|
|
322
|
+
// case-distinct collisions (e.g. both `Foo` and `FOO` present).
|
|
323
|
+
const variantsByLower = new Map<string, string[]>();
|
|
324
|
+
for (const k of knownKeys) {
|
|
325
|
+
const lower = k.toLowerCase();
|
|
326
|
+
const existing = variantsByLower.get(lower);
|
|
327
|
+
if (existing) existing.push(k);
|
|
328
|
+
else variantsByLower.set(lower, [k]);
|
|
329
|
+
}
|
|
330
|
+
// Normalization priority:
|
|
331
|
+
// 1. Exact-case match → preserve user's token verbatim
|
|
332
|
+
// 2. Lowercase maps to exactly one canonical → rewrite (the common
|
|
333
|
+
// "user typed lowercase, context has uppercase" path)
|
|
334
|
+
// 3. Lowercase maps to multiple canonicals (ambiguous) → preserve
|
|
335
|
+
// as-typed. Auto-picking one would silently target the wrong
|
|
336
|
+
// variable. The handler will match nothing and report a no-op,
|
|
337
|
+
// letting the user retype the exact case they meant.
|
|
338
|
+
// 4. No match → preserve as-typed so typos surface via handler.
|
|
339
|
+
const typedTokens = headRaw.trim().split(/\s+/).filter(Boolean);
|
|
340
|
+
const normalizedTokens = typedTokens.map(t => {
|
|
341
|
+
if (knownExact.has(t)) return t;
|
|
342
|
+
const variants = variantsByLower.get(t.toLowerCase());
|
|
343
|
+
if (variants && variants.length === 1) return variants[0];
|
|
344
|
+
return t;
|
|
345
|
+
});
|
|
346
|
+
const head = normalizedTokens.length > 0 ? `${normalizedTokens.join(" ")} ` : "";
|
|
347
|
+
const alreadyExact = new Set(normalizedTokens);
|
|
348
|
+
const items = knownKeys
|
|
349
|
+
.filter(k => !alreadyExact.has(k))
|
|
350
|
+
.filter(k => k.toLowerCase().startsWith(tail.toLowerCase()))
|
|
351
|
+
.map(k => ({
|
|
352
|
+
value: `${head}${k} `,
|
|
353
|
+
label: k,
|
|
354
|
+
description: "env var on active context",
|
|
355
|
+
}));
|
|
356
|
+
return items.length > 0 ? items : null;
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
{ name: "wizard", description: "Guided interactive context setup" },
|
|
360
|
+
];
|
|
361
|
+
|
|
132
362
|
const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
133
363
|
{
|
|
134
364
|
name: "settings",
|
|
@@ -985,234 +1215,50 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
985
1215
|
name: "context",
|
|
986
1216
|
description: "Manage F5 XC authentication contexts",
|
|
987
1217
|
allowArgs: true,
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
{
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
getArgumentCompletions
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
value: n,
|
|
1011
|
-
label: n,
|
|
1012
|
-
description: parts.length > 0 ? parts.join(" · ") : undefined,
|
|
1013
|
-
};
|
|
1014
|
-
});
|
|
1015
|
-
return items.length > 0 ? items : null;
|
|
1016
|
-
},
|
|
1017
|
-
},
|
|
1018
|
-
{
|
|
1019
|
-
name: "validate",
|
|
1020
|
-
description: "Validate credentials for a context without activating",
|
|
1021
|
-
usage: "<name>",
|
|
1022
|
-
getArgumentCompletions(prefix: string) {
|
|
1023
|
-
if (prefix.includes(" ")) return null;
|
|
1024
|
-
const svc = tryGetContextService();
|
|
1025
|
-
if (!svc) return null;
|
|
1026
|
-
const lower = prefix.toLowerCase();
|
|
1027
|
-
const items = svc
|
|
1028
|
-
.listContextNamesCached()
|
|
1029
|
-
.filter(n => n.toLowerCase().startsWith(lower))
|
|
1030
|
-
.map(n => {
|
|
1031
|
-
const hint = svc.getContextHint(n);
|
|
1032
|
-
const parts: string[] = [];
|
|
1033
|
-
if (hint?.apiUrl) parts.push(hint.apiUrl);
|
|
1034
|
-
if (hint?.incompatible && hint.schemaVersion !== undefined) {
|
|
1035
|
-
parts.push(`incompatible: v${hint.schemaVersion}`);
|
|
1036
|
-
}
|
|
1037
|
-
return {
|
|
1038
|
-
value: n,
|
|
1039
|
-
label: n,
|
|
1040
|
-
description: parts.length > 0 ? parts.join(" · ") : undefined,
|
|
1041
|
-
};
|
|
1042
|
-
});
|
|
1043
|
-
return items.length > 0 ? items : null;
|
|
1044
|
-
},
|
|
1045
|
-
},
|
|
1046
|
-
{ name: "show", description: "Show context details (masked)", usage: "[name]" },
|
|
1047
|
-
{ name: "status", description: "Show current auth status" },
|
|
1048
|
-
{ name: "create", description: "Create a new context", usage: "<name> <url> <token> [namespace]" },
|
|
1049
|
-
{ name: "delete", description: "Delete a context", usage: "<name> --confirm" },
|
|
1050
|
-
{
|
|
1051
|
-
name: "rename",
|
|
1052
|
-
description: "Rename a context",
|
|
1053
|
-
usage: "<old> <new>",
|
|
1054
|
-
getArgumentCompletions(prefix: string) {
|
|
1055
|
-
if (prefix.includes(" ")) return null;
|
|
1056
|
-
const svc = tryGetContextService();
|
|
1057
|
-
if (!svc) return null;
|
|
1058
|
-
const lower = prefix.toLowerCase();
|
|
1059
|
-
const items = svc
|
|
1060
|
-
.listContextNamesCached()
|
|
1061
|
-
.filter(n => n.toLowerCase().startsWith(lower))
|
|
1062
|
-
.map(n => ({ value: n, label: n }));
|
|
1063
|
-
return items.length > 0 ? items : null;
|
|
1064
|
-
},
|
|
1065
|
-
},
|
|
1066
|
-
{
|
|
1067
|
-
name: "export",
|
|
1068
|
-
description: "Export a context (or all contexts) as JSON",
|
|
1069
|
-
usage: "[name] [--include-token]",
|
|
1070
|
-
getArgumentCompletions(prefix: string) {
|
|
1071
|
-
const svc = tryGetContextService();
|
|
1072
|
-
if (!svc) return null;
|
|
1073
|
-
const tokens = prefix.split(/\s+/).filter(Boolean);
|
|
1074
|
-
const hasIncludeToken = tokens.includes("--include-token");
|
|
1075
|
-
const positionalsTyped = tokens.filter(t => !t.startsWith("--"));
|
|
1076
|
-
// Last token is "in-progress" if the prefix does not end with space.
|
|
1077
|
-
const trailingSpace = prefix.endsWith(" ") || prefix === "";
|
|
1078
|
-
const typedPositionalCount = trailingSpace
|
|
1079
|
-
? positionalsTyped.length
|
|
1080
|
-
: Math.max(0, positionalsTyped.length - 1);
|
|
1081
|
-
const completingToken = trailingSpace ? "" : (tokens[tokens.length - 1] ?? "");
|
|
1082
|
-
// `head` is every already-typed token EXCEPT the one being
|
|
1083
|
-
// completed. getArgumentCompletions.value replaces the whole
|
|
1084
|
-
// argument tail, so value must carry every token the user
|
|
1085
|
-
// should keep — otherwise accepting a suggestion silently
|
|
1086
|
-
// drops the other args. Contract: see SubcommandDef JSDoc
|
|
1087
|
-
// above (line ~58).
|
|
1088
|
-
const headTokens = trailingSpace ? tokens : tokens.slice(0, -1);
|
|
1089
|
-
const head = headTokens.length > 0 ? `${headTokens.join(" ")} ` : "";
|
|
1090
|
-
|
|
1091
|
-
const items: { value: string; label: string; description?: string }[] = [];
|
|
1092
|
-
|
|
1093
|
-
// Offer context names only if no positional has been filled yet.
|
|
1094
|
-
// No startsWith("--") guard: context names legitimately allow
|
|
1095
|
-
// leading dashes (the regex is /^[a-zA-Z0-9_-]{1,64}$/), and
|
|
1096
|
-
// the handler's splitArgs uses a known-flags allowlist that
|
|
1097
|
-
// treats only --include-token as a flag. So a context like
|
|
1098
|
-
// `--prod` is valid; the completion filters by prefix and
|
|
1099
|
-
// matches it naturally. When the user types `--in`, the
|
|
1100
|
-
// flag-completion branch below matches `--include-token` by
|
|
1101
|
-
// prefix; if there's ALSO a context starting with `--in` it
|
|
1102
|
-
// is offered here. Both lists are disjoint by filter so
|
|
1103
|
-
// there's no double-offer of the same token.
|
|
1104
|
-
if (typedPositionalCount === 0) {
|
|
1105
|
-
const lower = completingToken.toLowerCase();
|
|
1106
|
-
for (const n of svc.listContextNamesCached()) {
|
|
1107
|
-
if (!n.toLowerCase().startsWith(lower)) continue;
|
|
1108
|
-
const hint = svc.getContextHint(n);
|
|
1109
|
-
items.push({
|
|
1110
|
-
value: `${head}${n}`,
|
|
1111
|
-
label: n,
|
|
1112
|
-
description: hint?.apiUrl,
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// Offer --include-token unless already present. Match is
|
|
1118
|
-
// case-sensitive because the handler's flag check uses
|
|
1119
|
-
// exact-match `flags.has("--include-token")` — offering
|
|
1120
|
-
// the suggestion for mis-cased prefixes (e.g. `--INCLUDE`)
|
|
1121
|
-
// would produce a suggestion the handler then ignores.
|
|
1122
|
-
if (!hasIncludeToken && "--include-token".startsWith(completingToken)) {
|
|
1123
|
-
items.push({
|
|
1124
|
-
value: `${head}--include-token`,
|
|
1125
|
-
label: "--include-token",
|
|
1126
|
-
description: "emit unmasked tokens",
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
return items.length > 0 ? items : null;
|
|
1131
|
-
},
|
|
1132
|
-
},
|
|
1133
|
-
{
|
|
1134
|
-
name: "import",
|
|
1135
|
-
description: "Import contexts from a file path or inline JSON",
|
|
1136
|
-
usage: "<path-or-json> [--overwrite]",
|
|
1137
|
-
// No dynamic completion — paths are hard to complete correctly,
|
|
1138
|
-
// and faking it would only mislead. Users pre-expand paths in
|
|
1139
|
-
// their shell.
|
|
1140
|
-
},
|
|
1141
|
-
{
|
|
1142
|
-
name: "namespace",
|
|
1143
|
-
description: "Switch namespace within active context",
|
|
1144
|
-
usage: "<namespace>",
|
|
1145
|
-
getArgumentCompletions(prefix: string) {
|
|
1146
|
-
if (prefix.includes(" ")) return null;
|
|
1147
|
-
const svc = tryGetContextService();
|
|
1148
|
-
if (!svc) return null;
|
|
1149
|
-
// setNamespace() requires an active context. Don't offer completions that
|
|
1150
|
-
// would lead the user into a command path that cannot succeed (e.g. an
|
|
1151
|
-
// env-backed session where cached namespaces came from startup validation
|
|
1152
|
-
// but there is no active context to apply them to).
|
|
1153
|
-
if (!svc.getStatus().activeContextName) return null;
|
|
1154
|
-
const lower = prefix.toLowerCase();
|
|
1155
|
-
const items = svc
|
|
1156
|
-
.getCachedNamespaces()
|
|
1157
|
-
.filter(n => n.toLowerCase().startsWith(lower))
|
|
1158
|
-
.map(n => ({ value: n, label: n }));
|
|
1159
|
-
return items.length > 0 ? items : null;
|
|
1160
|
-
},
|
|
1161
|
-
},
|
|
1162
|
-
{ name: "env", description: "Manage environment variables", usage: "set|unset|list [KEY=VALUE ...]" },
|
|
1163
|
-
{ name: "set", description: "Set environment variable(s)", usage: "KEY=VALUE [KEY2=VALUE2 ...]" },
|
|
1164
|
-
{
|
|
1165
|
-
name: "unset",
|
|
1166
|
-
description: "Remove environment variable(s)",
|
|
1167
|
-
usage: "KEY [KEY2 ...]",
|
|
1168
|
-
getArgumentCompletions(prefix: string) {
|
|
1169
|
-
const lastSpace = prefix.lastIndexOf(" ");
|
|
1170
|
-
const headRaw = lastSpace === -1 ? "" : prefix.slice(0, lastSpace + 1);
|
|
1171
|
-
const tail = lastSpace === -1 ? prefix : prefix.slice(lastSpace + 1);
|
|
1172
|
-
const svc = tryGetContextService();
|
|
1173
|
-
if (!svc) return null;
|
|
1174
|
-
const knownKeys = svc.getActiveEnvKeys();
|
|
1175
|
-
const knownExact = new Set(knownKeys);
|
|
1176
|
-
// Group known keys by their lowercased form so we can detect
|
|
1177
|
-
// case-distinct collisions (e.g. both `Foo` and `FOO` present).
|
|
1178
|
-
const variantsByLower = new Map<string, string[]>();
|
|
1179
|
-
for (const k of knownKeys) {
|
|
1180
|
-
const lower = k.toLowerCase();
|
|
1181
|
-
const existing = variantsByLower.get(lower);
|
|
1182
|
-
if (existing) existing.push(k);
|
|
1183
|
-
else variantsByLower.set(lower, [k]);
|
|
1184
|
-
}
|
|
1185
|
-
// Normalization priority:
|
|
1186
|
-
// 1. Exact-case match → preserve user's token verbatim
|
|
1187
|
-
// 2. Lowercase maps to exactly one canonical → rewrite (the common
|
|
1188
|
-
// "user typed lowercase, context has uppercase" path)
|
|
1189
|
-
// 3. Lowercase maps to multiple canonicals (ambiguous) → preserve
|
|
1190
|
-
// as-typed. Auto-picking one would silently target the wrong
|
|
1191
|
-
// variable. The handler will match nothing and report a no-op,
|
|
1192
|
-
// letting the user retype the exact case they meant.
|
|
1193
|
-
// 4. No match → preserve as-typed so typos surface via handler.
|
|
1194
|
-
const typedTokens = headRaw.trim().split(/\s+/).filter(Boolean);
|
|
1195
|
-
const normalizedTokens = typedTokens.map(t => {
|
|
1196
|
-
if (knownExact.has(t)) return t;
|
|
1197
|
-
const variants = variantsByLower.get(t.toLowerCase());
|
|
1198
|
-
if (variants && variants.length === 1) return variants[0];
|
|
1199
|
-
return t;
|
|
1218
|
+
getArgumentCompletions(argumentPrefix: string) {
|
|
1219
|
+
const firstSpace = argumentPrefix.indexOf(" ");
|
|
1220
|
+
if (firstSpace !== -1) {
|
|
1221
|
+
const subName = argumentPrefix.slice(0, firstSpace).toLowerCase();
|
|
1222
|
+
const subPrefix = argumentPrefix.slice(firstSpace + 1).replace(/^ +/, "");
|
|
1223
|
+
const sub = CONTEXT_SUBCOMMANDS.find(s => s.name === subName);
|
|
1224
|
+
if (!sub?.getArgumentCompletions) return null;
|
|
1225
|
+
const items = sub.getArgumentCompletions(subPrefix);
|
|
1226
|
+
if (!items || items.length === 0) return null;
|
|
1227
|
+
return items.map(item => ({ ...item, value: `${subName} ${item.value}` }));
|
|
1228
|
+
}
|
|
1229
|
+
const lower = argumentPrefix.toLowerCase();
|
|
1230
|
+
const items: { value: string; label: string; description?: string; hint?: string }[] = [];
|
|
1231
|
+
const svc = tryGetContextService();
|
|
1232
|
+
if (svc) {
|
|
1233
|
+
for (const n of svc.listContextNamesCached()) {
|
|
1234
|
+
if (!n.toLowerCase().startsWith(lower)) continue;
|
|
1235
|
+
const hint = svc.getContextHint(n);
|
|
1236
|
+
items.push({
|
|
1237
|
+
value: `${n} `,
|
|
1238
|
+
label: n,
|
|
1239
|
+
description: hint?.apiUrl,
|
|
1200
1240
|
});
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1241
|
+
}
|
|
1242
|
+
if (svc.previousContextName && "-".startsWith(lower)) {
|
|
1243
|
+
items.push({
|
|
1244
|
+
value: "- ",
|
|
1245
|
+
label: "-",
|
|
1246
|
+
description: `Switch to ${svc.previousContextName}`,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
for (const sub of CONTEXT_SUBCOMMANDS) {
|
|
1251
|
+
if (!sub.name.toLowerCase().startsWith(lower)) continue;
|
|
1252
|
+
items.push({
|
|
1253
|
+
value: `${sub.name} `,
|
|
1254
|
+
label: sub.name,
|
|
1255
|
+
description: sub.description,
|
|
1256
|
+
hint: sub.usage,
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
return items.length > 0 ? items : null;
|
|
1260
|
+
},
|
|
1261
|
+
subcommands: CONTEXT_SUBCOMMANDS,
|
|
1216
1262
|
handle: async (command, runtime) => {
|
|
1217
1263
|
runtime.ctx.editor.addToHistory(command.text);
|
|
1218
1264
|
runtime.ctx.editor.setText("");
|
|
@@ -1238,6 +1284,7 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
|
|
|
1238
1284
|
description: command.description,
|
|
1239
1285
|
subcommands: command.subcommands,
|
|
1240
1286
|
inlineHint: command.inlineHint,
|
|
1287
|
+
getArgumentCompletions: command.getArgumentCompletions,
|
|
1241
1288
|
}),
|
|
1242
1289
|
);
|
|
1243
1290
|
|