@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64
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/README.md +2 -2
- package/knip.json +1 -1
- package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
- package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +18 -0
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
- package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
- package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
- package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
- package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
- package/node_modules/@mycadet/service-contracts/package.json +2 -2
- package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +6 -4
- package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
- package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
- package/src/__tests__/assistant-client-refresh.test.ts +2 -2
- package/src/__tests__/backup.test.ts +86 -91
- package/src/__tests__/client-token.test.ts +1 -1
- package/src/__tests__/client-tui-refresh.test.ts +1 -1
- package/src/__tests__/client-web-config.test.ts +1 -2
- package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
- package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +28 -18
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +38 -38
- package/src/__tests__/ensure-binary.test.ts +182 -0
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +15 -20
- package/src/__tests__/ngrok-spawn.test.ts +49 -0
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +149 -85
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -26
- package/src/__tests__/ps-table.test.ts +79 -0
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/retire.test.ts +58 -199
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/statefulset.test.ts +8 -8
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +2 -2
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +180 -211
- package/src/__tests__/tunnel.test.ts +490 -270
- package/src/__tests__/uninstall.test.ts +150 -9
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/upgrade-replay-env.test.ts +12 -12
- package/src/__tests__/wake.test.ts +260 -43
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +24 -24
- package/src/commands/client.ts +76 -78
- package/src/commands/completions.test.ts +104 -0
- package/src/commands/completions.ts +87 -0
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +12 -12
- package/src/commands/flags.ts +2 -2
- package/src/commands/hatch.ts +27 -24
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +8 -8
- package/src/commands/pair.ts +140 -37
- package/src/commands/ps.ts +232 -86
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +140 -22
- package/src/commands/retire.ts +48 -150
- package/src/commands/rollback.ts +20 -26
- package/src/commands/setup.ts +2 -2
- package/src/commands/sleep.ts +2 -2
- package/src/commands/teleport.ts +110 -129
- package/src/commands/terminal.ts +158 -18
- package/src/commands/tunnel.ts +156 -175
- package/src/commands/uninstall.ts +137 -54
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +30 -54
- package/src/commands/use.ts +1 -1
- package/src/commands/wake.ts +166 -50
- package/src/index.ts +18 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/docker.test.ts +168 -20
- package/src/lib/__tests__/job-polling.test.ts +2 -2
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
- package/src/lib/__tests__/runtime-url.test.ts +1 -1
- package/src/lib/assistant-api-key-resolution.ts +5 -5
- package/src/lib/assistant-client.ts +8 -8
- package/src/lib/assistant-config.ts +58 -12
- package/src/lib/{platform-client.ts → cloud-client.ts} +105 -99
- package/src/lib/cloud-login.ts +23 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/color.ts +4 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +300 -111
- package/src/lib/ensure-binary.ts +171 -0
- package/src/lib/environments/__tests__/paths.test.ts +29 -3
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +10 -12
- package/src/lib/flag-args.test.ts +1 -1
- package/src/lib/flag-args.ts +1 -1
- package/src/lib/guardian-token.ts +37 -5
- package/src/lib/hatch-local.ts +32 -29
- package/src/lib/hatch-next-steps.ts +5 -9
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +2 -2
- package/src/lib/live-voice/connection.ts +12 -12
- package/src/lib/live-voice/tunnel.test.ts +11 -11
- package/src/lib/live-voice/tunnel.ts +15 -15
- package/src/lib/local-runtime-client.ts +20 -20
- package/src/lib/local.ts +46 -34
- package/src/lib/nginx-ingress.ts +40 -15
- package/src/lib/pair.ts +1 -1
- package/src/lib/process.ts +1 -1
- package/src/lib/provider-secrets.ts +3 -3
- package/src/lib/retire-apple-container.ts +1 -1
- package/src/lib/runtime-url.ts +8 -8
- package/src/lib/statefulset.ts +2 -2
- package/src/lib/step-runner.ts +6 -1
- package/src/lib/sync-cloud-assistants.ts +19 -19
- package/src/lib/teleport-backup.ts +4 -4
- package/src/lib/terminal-client.ts +17 -17
- package/src/lib/terminal-session.ts +24 -24
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
- package/src/lib/upgrade-lifecycle.ts +8 -21
- package/src/lib/upgrade-preflight.ts +2 -2
- package/src/lib/workos-pkce.ts +5 -5
- package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
- package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
- package/src/commands/recover.ts +0 -131
- package/src/commands/roadmap.ts +0 -459
- package/src/commands/ssh.ts +0 -144
- package/src/commands/workflows.ts +0 -305
- package/src/lib/managed-cloud.ts +0 -3
package/src/commands/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isCloudHome } from "../lib/cloud-values.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
getLockfileData,
|
|
31
31
|
renameLockfileAssistantIfPresent,
|
|
32
32
|
upsertRendererLockfileAssistant,
|
|
33
|
-
|
|
33
|
+
replaceCloudAssistants,
|
|
34
34
|
isActiveAssistant,
|
|
35
35
|
isPairedLockfileEntry,
|
|
36
36
|
PAIRED_GUARDIAN_TOKEN_HOST_ONLY_ERROR,
|
|
@@ -64,13 +64,13 @@ import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
|
64
64
|
import { parseFeatureFlagArgs, readAmbientFlagEnvVars } from "../lib/flag-args";
|
|
65
65
|
import {
|
|
66
66
|
fetchOrganizationId,
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
fetchCloudAssistants,
|
|
68
|
+
getCloudUrl,
|
|
69
69
|
getWebUrl,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
} from "../lib/
|
|
70
|
+
readCloudToken,
|
|
71
|
+
saveCloudToken,
|
|
72
|
+
clearCloudToken,
|
|
73
|
+
} from "../lib/cloud-client";
|
|
74
74
|
import { tuiLog } from "../lib/tui-log";
|
|
75
75
|
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
76
76
|
import { relaunchDetached } from "../lib/detached-process.js";
|
|
@@ -87,7 +87,7 @@ const TokenBodySchema = z.object({ token: z.unknown() }).nullable();
|
|
|
87
87
|
|
|
88
88
|
const SyncPlatformBodySchema = z.object({
|
|
89
89
|
syncPlatform: z.unknown().optional(),
|
|
90
|
-
|
|
90
|
+
cloudAssistants: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
91
91
|
organizationId: z.string().optional(),
|
|
92
92
|
rename: z
|
|
93
93
|
.object({ assistantId: z.string(), name: z.string() })
|
|
@@ -133,10 +133,10 @@ interface ParsedArgs {
|
|
|
133
133
|
assistantName?: string;
|
|
134
134
|
/** Assistant data root (home-collapsed) for the TUI details panel. */
|
|
135
135
|
workspaceRoot?: string;
|
|
136
|
-
/** "cadet" for
|
|
136
|
+
/** "cadet" for cloud-hosted assistants, undefined for local. */
|
|
137
137
|
cloud?: string;
|
|
138
|
-
/** Platform session token (X-Session-Token), set when
|
|
139
|
-
|
|
138
|
+
/** Platform session token (X-Session-Token), set when isCloudHome(cloud). */
|
|
139
|
+
cloudToken?: string;
|
|
140
140
|
/** Guardian JWT (Authorization: Bearer), set for local assistants. */
|
|
141
141
|
bearerToken?: string;
|
|
142
142
|
/** Interface identifier sent as X-Cadet-Interface-Id on all requests. */
|
|
@@ -145,7 +145,7 @@ interface ParsedArgs {
|
|
|
145
145
|
flagEnvVars: Record<string, string>;
|
|
146
146
|
/** Parsed --flag overrides: kebab-case key -> typed value (for web injection). */
|
|
147
147
|
parsedFlagOverrides: Record<string, boolean | string>;
|
|
148
|
-
|
|
148
|
+
disableCloud: boolean;
|
|
149
149
|
/** Auto-open the web interface in the default browser (--interface web only). */
|
|
150
150
|
openBrowser: boolean;
|
|
151
151
|
/** Explicit web server port (--interface web only). Binds strictly — no scan. */
|
|
@@ -179,10 +179,10 @@ export function parseArgs(): ParsedArgs {
|
|
|
179
179
|
const { envVars: cliFlagVars, remaining: argsWithoutFlags } =
|
|
180
180
|
parseFeatureFlagArgs(process.argv.slice(3));
|
|
181
181
|
const flagEnvVars = { ...readAmbientFlagEnvVars(), ...cliFlagVars };
|
|
182
|
-
const
|
|
183
|
-
process.env.
|
|
184
|
-
let
|
|
185
|
-
|
|
182
|
+
const disableCloudAmbient =
|
|
183
|
+
process.env.CADET_DISABLE_CLOUD?.trim().toLowerCase();
|
|
184
|
+
let disableCloud =
|
|
185
|
+
disableCloudAmbient === "true" || disableCloudAmbient === "1";
|
|
186
186
|
const args = argsWithoutFlags;
|
|
187
187
|
|
|
188
188
|
// Build parsedFlagOverrides from the extracted env vars:
|
|
@@ -222,8 +222,8 @@ export function parseArgs(): ParsedArgs {
|
|
|
222
222
|
if (arg === "--help" || arg === "-h") {
|
|
223
223
|
printUsage();
|
|
224
224
|
process.exit(0);
|
|
225
|
-
} else if (arg === "--disable-
|
|
226
|
-
|
|
225
|
+
} else if (arg === "--disable-cloud") {
|
|
226
|
+
disableCloud = true;
|
|
227
227
|
} else if (arg === "--no-open") {
|
|
228
228
|
openBrowserPref = false;
|
|
229
229
|
} else if (arg === "--background") {
|
|
@@ -312,14 +312,14 @@ export function parseArgs(): ParsedArgs {
|
|
|
312
312
|
// other topology — local, docker, and "paired" (a remote assistant paired
|
|
313
313
|
// from another machine) — uses a bearer guardian JWT. Both are skipped
|
|
314
314
|
// entirely when --token supplies the credential, so no saved creds are read.
|
|
315
|
-
const
|
|
315
|
+
const cloudToken = bearerTokenOverride
|
|
316
316
|
? undefined
|
|
317
|
-
:
|
|
318
|
-
? (
|
|
317
|
+
: isCloudHome(cloud)
|
|
318
|
+
? (readCloudToken() ?? undefined)
|
|
319
319
|
: undefined;
|
|
320
320
|
const bearerToken = bearerTokenOverride
|
|
321
321
|
? bearerTokenOverride
|
|
322
|
-
:
|
|
322
|
+
: isCloudHome(cloud)
|
|
323
323
|
? undefined
|
|
324
324
|
: (loadGuardianToken(entry?.assistantId ?? "")?.accessToken ?? undefined);
|
|
325
325
|
|
|
@@ -365,12 +365,12 @@ export function parseArgs(): ParsedArgs {
|
|
|
365
365
|
assistantName,
|
|
366
366
|
workspaceRoot,
|
|
367
367
|
cloud,
|
|
368
|
-
|
|
368
|
+
cloudToken,
|
|
369
369
|
bearerToken,
|
|
370
370
|
interfaceId,
|
|
371
371
|
flagEnvVars,
|
|
372
372
|
parsedFlagOverrides,
|
|
373
|
-
|
|
373
|
+
disableCloud,
|
|
374
374
|
openBrowser: openBrowserPref,
|
|
375
375
|
webPort,
|
|
376
376
|
background,
|
|
@@ -401,7 +401,7 @@ ${ANSI.bold}OPTIONS:${ANSI.reset}
|
|
|
401
401
|
(--interface web). Prints the URL, PID, and log
|
|
402
402
|
path, then returns to the shell.
|
|
403
403
|
--flag <key=value> Feature flag override (repeatable, kebab-case key)
|
|
404
|
-
--disable-
|
|
404
|
+
--disable-cloud Suppress all outbound platform API calls
|
|
405
405
|
-h, --help Show this help message
|
|
406
406
|
|
|
407
407
|
${ANSI.bold}EXAMPLES:${ANSI.reset}
|
|
@@ -421,19 +421,19 @@ ${ANSI.bold}EXAMPLES:${ANSI.reset}
|
|
|
421
421
|
`);
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
async function
|
|
424
|
+
async function maybeHydrateCloudAssistantName(
|
|
425
425
|
assistantId: string,
|
|
426
426
|
assistantName: string | undefined,
|
|
427
427
|
cloud: string | undefined,
|
|
428
|
-
|
|
428
|
+
cloudToken: string | undefined,
|
|
429
429
|
): Promise<string | undefined> {
|
|
430
|
-
if (!
|
|
430
|
+
if (!isCloudHome(cloud) || assistantName || !cloudToken) {
|
|
431
431
|
return assistantName;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
434
|
try {
|
|
435
435
|
const matchedAssistant = (
|
|
436
|
-
await
|
|
436
|
+
await fetchCloudAssistants(cloudToken)
|
|
437
437
|
).find((assistant) => assistant.id === assistantId);
|
|
438
438
|
const hydratedName = matchedAssistant?.name.trim();
|
|
439
439
|
if (!hydratedName) {
|
|
@@ -500,18 +500,18 @@ const PAIRING_CANCEL_PATTERN = /^(?:\/assistant)?\/__local\/pairing-cancel$/;
|
|
|
500
500
|
const GUARDIAN_TOKEN_PATTERN =
|
|
501
501
|
/^(?:\/assistant)?\/__local\/guardian-token\/([^/]+)$/;
|
|
502
502
|
const PLATFORM_SESSION_PATTERN =
|
|
503
|
-
/^(?:\/assistant)?\/__local\/
|
|
503
|
+
/^(?:\/assistant)?\/__local\/cloud-session$/;
|
|
504
504
|
|
|
505
505
|
// The loopback platform session token. Persisted via the same store the CLI
|
|
506
506
|
// uses (so `cadet client` restarts and CLI logins stay in sync), cached here
|
|
507
507
|
// to keep it off the per-request proxy path. Set only after the SPA validates
|
|
508
508
|
// the loopback `state`, so an unsolicited /callback can't fixate a session.
|
|
509
|
-
let
|
|
509
|
+
let cloudSessionToken: string | null | undefined;
|
|
510
510
|
function currentPlatformToken(): string | null {
|
|
511
|
-
if (
|
|
512
|
-
|
|
511
|
+
if (cloudSessionToken === undefined) {
|
|
512
|
+
cloudSessionToken = readCloudToken();
|
|
513
513
|
}
|
|
514
|
-
return
|
|
514
|
+
return cloudSessionToken;
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
// Whether to attach a host-owned credential to a proxied request. The browser
|
|
@@ -582,8 +582,8 @@ async function handleLocalEndpoints(
|
|
|
582
582
|
// platform. The browser never holds a session cookie.
|
|
583
583
|
if (PLATFORM_SESSION_PATTERN.test(pathname)) {
|
|
584
584
|
if (req.method === "DELETE") {
|
|
585
|
-
|
|
586
|
-
|
|
585
|
+
clearCloudToken();
|
|
586
|
+
cloudSessionToken = null;
|
|
587
587
|
return Response.json({ ok: true });
|
|
588
588
|
}
|
|
589
589
|
if (req.method === "POST") {
|
|
@@ -598,8 +598,8 @@ async function handleLocalEndpoints(
|
|
|
598
598
|
{ status: 400 },
|
|
599
599
|
);
|
|
600
600
|
}
|
|
601
|
-
|
|
602
|
-
|
|
601
|
+
saveCloudToken(token);
|
|
602
|
+
cloudSessionToken = token;
|
|
603
603
|
return Response.json({ ok: true });
|
|
604
604
|
}
|
|
605
605
|
return new Response(null, { status: 405 });
|
|
@@ -627,10 +627,10 @@ async function handleLocalEndpoints(
|
|
|
627
627
|
);
|
|
628
628
|
}
|
|
629
629
|
let result;
|
|
630
|
-
if (body.syncPlatform && body.
|
|
631
|
-
result =
|
|
630
|
+
if (body.syncPlatform && body.cloudAssistants) {
|
|
631
|
+
result = replaceCloudAssistants(
|
|
632
632
|
lockfilePaths,
|
|
633
|
-
body.
|
|
633
|
+
body.cloudAssistants,
|
|
634
634
|
body.organizationId,
|
|
635
635
|
);
|
|
636
636
|
} else if (body.rename) {
|
|
@@ -1223,7 +1223,7 @@ const WEB_BACKGROUND_START_TIMEOUT_MS = 30_000;
|
|
|
1223
1223
|
* The port is resolved up front (explicit --port must be free; otherwise the
|
|
1224
1224
|
* first free port at/above 3000) and pinned via `--port` on the child so the
|
|
1225
1225
|
* URL printed here is the one the child binds. The child's stdout/stderr go to
|
|
1226
|
-
* `<xdg-log-dir>/client-web.log` — same detach idiom as the nginx/
|
|
1226
|
+
* `<xdg-log-dir>/client-web.log` — same detach idiom as the nginx/tunnel
|
|
1227
1227
|
* spawns. Success is only reported once the child is accepting connections on
|
|
1228
1228
|
* the port; an early child exit (e.g. missing @mycadet/web assets) or a
|
|
1229
1229
|
* startup timeout fails with a pointer at the log file.
|
|
@@ -1303,17 +1303,16 @@ async function spawnBackgroundWebInterface(
|
|
|
1303
1303
|
*/
|
|
1304
1304
|
export function buildWebInterfaceConfig(opts: {
|
|
1305
1305
|
webUrl: string;
|
|
1306
|
-
|
|
1307
|
-
disablePlatform: boolean;
|
|
1306
|
+
cloudUrl: string;
|
|
1308
1307
|
}): Record<string, unknown> {
|
|
1309
|
-
const { webUrl,
|
|
1310
|
-
return { webUrl,
|
|
1308
|
+
const { webUrl, cloudUrl } = opts;
|
|
1309
|
+
return { webUrl, cloudUrl };
|
|
1311
1310
|
}
|
|
1312
1311
|
|
|
1313
1312
|
async function runWebInterface(
|
|
1314
1313
|
flagEnvVars: Record<string, string>,
|
|
1315
1314
|
parsedFlagOverrides: Record<string, boolean | string>,
|
|
1316
|
-
|
|
1315
|
+
disableCloud: boolean,
|
|
1317
1316
|
openInBrowser: boolean,
|
|
1318
1317
|
webPort: number | undefined,
|
|
1319
1318
|
): Promise<void> {
|
|
@@ -1327,7 +1326,7 @@ async function runWebInterface(
|
|
|
1327
1326
|
return runViteDevServer(
|
|
1328
1327
|
webSourceDir,
|
|
1329
1328
|
flagEnvVars,
|
|
1330
|
-
|
|
1329
|
+
disableCloud,
|
|
1331
1330
|
openInBrowser,
|
|
1332
1331
|
webPort,
|
|
1333
1332
|
);
|
|
@@ -1339,19 +1338,17 @@ async function runWebInterface(
|
|
|
1339
1338
|
`${ANSI.bold}--interface web${ANSI.reset}: unable to locate ` +
|
|
1340
1339
|
`@mycadet/web assets.\n\n` +
|
|
1341
1340
|
` npm/bunx install: npm install @mycadet/web\n` +
|
|
1342
|
-
` source checkout: cd clients/web &&
|
|
1341
|
+
` source checkout: cd clients/web && VITE_CLOUD_MODE=false bun run build`,
|
|
1343
1342
|
);
|
|
1344
1343
|
process.exit(1);
|
|
1345
1344
|
}
|
|
1346
1345
|
|
|
1347
1346
|
const rawIndexHtml = await Bun.file(path.join(distDir, "index.html")).text();
|
|
1348
|
-
const
|
|
1347
|
+
const cloudUrl = getCloudUrl();
|
|
1349
1348
|
const webUrl = getWebUrl();
|
|
1350
1349
|
const safeJson = (v: unknown) =>
|
|
1351
1350
|
JSON.stringify(v).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
1352
|
-
const configJson = safeJson(
|
|
1353
|
-
buildWebInterfaceConfig({ webUrl, platformUrl, disablePlatform }),
|
|
1354
|
-
);
|
|
1351
|
+
const configJson = safeJson(buildWebInterfaceConfig({ webUrl, cloudUrl }));
|
|
1355
1352
|
const hasOverrides = Object.keys(parsedFlagOverrides).length > 0;
|
|
1356
1353
|
const flagOverridesSnippet = hasOverrides
|
|
1357
1354
|
? `;window.__CADET_FLAG_OVERRIDES__=${safeJson(parsedFlagOverrides)}`
|
|
@@ -1380,9 +1377,9 @@ async function runWebInterface(
|
|
|
1380
1377
|
return Response.redirect(SPA_BASE, 302);
|
|
1381
1378
|
}
|
|
1382
1379
|
|
|
1383
|
-
// Loopback auth: the
|
|
1380
|
+
// Loopback auth: the cloud redirects here after login with
|
|
1384
1381
|
// ?state=...&session_token=... — forward into the SPA, which validates the
|
|
1385
|
-
// `state` nonce before registering the token via /__local/
|
|
1382
|
+
// `state` nonce before registering the token via /__local/cloud-session.
|
|
1386
1383
|
if (pathname === "/callback") {
|
|
1387
1384
|
return Response.redirect(`/account/platform-callback${url.search}`, 302);
|
|
1388
1385
|
}
|
|
@@ -1404,9 +1401,9 @@ async function runWebInterface(
|
|
|
1404
1401
|
pathname.startsWith("/_allauth/") ||
|
|
1405
1402
|
pathname.startsWith("/accounts/")
|
|
1406
1403
|
) {
|
|
1407
|
-
const target = new URL(pathname + url.search,
|
|
1404
|
+
const target = new URL(pathname + url.search, cloudUrl);
|
|
1408
1405
|
const headers = new Headers(req.headers);
|
|
1409
|
-
headers.set("Host", new URL(
|
|
1406
|
+
headers.set("Host", new URL(cloudUrl).host);
|
|
1410
1407
|
headers.delete("Origin");
|
|
1411
1408
|
headers.delete("Referer");
|
|
1412
1409
|
|
|
@@ -1430,7 +1427,7 @@ async function runWebInterface(
|
|
|
1430
1427
|
headers.set("X-Session-Token", sessionToken);
|
|
1431
1428
|
}
|
|
1432
1429
|
} else if (sessionToken) {
|
|
1433
|
-
// allauth / accounts: the
|
|
1430
|
+
// allauth / accounts: the cloud expects the Django session cookie.
|
|
1434
1431
|
headers.set(
|
|
1435
1432
|
"Cookie",
|
|
1436
1433
|
`sessionid=${sessionToken}; __Secure-sessionid=${sessionToken}`,
|
|
@@ -1507,7 +1504,7 @@ async function runWebInterface(
|
|
|
1507
1504
|
console.log(`Port ${preferredPort} in use; using ${port}.`);
|
|
1508
1505
|
}
|
|
1509
1506
|
// Advertise `localhost` (not `127.0.0.1`) so the app origin matches the host
|
|
1510
|
-
// the
|
|
1507
|
+
// the cloud hardcodes in its loopback callback. We bind both loopback
|
|
1511
1508
|
// families above so `localhost` reaches us whichever one it resolves to.
|
|
1512
1509
|
const webInterfaceUrl = `http://localhost:${port}${SPA_BASE}`;
|
|
1513
1510
|
console.log(`Cadet web interface: ${webInterfaceUrl}`);
|
|
@@ -1526,11 +1523,11 @@ async function runWebInterface(
|
|
|
1526
1523
|
async function runViteDevServer(
|
|
1527
1524
|
webSourceDir: string,
|
|
1528
1525
|
flagEnvVars: Record<string, string>,
|
|
1529
|
-
|
|
1526
|
+
disableCloud: boolean,
|
|
1530
1527
|
openInBrowser: boolean,
|
|
1531
1528
|
webPort: number | undefined,
|
|
1532
1529
|
): Promise<void> {
|
|
1533
|
-
const
|
|
1530
|
+
const cloudUrl = getCloudUrl();
|
|
1534
1531
|
|
|
1535
1532
|
// Build VITE_CADET_FLAG_* vars so Vite exposes them to the browser bundle.
|
|
1536
1533
|
const viteFlagVars: Record<string, string> = {};
|
|
@@ -1555,11 +1552,12 @@ async function runViteDevServer(
|
|
|
1555
1552
|
...process.env,
|
|
1556
1553
|
...flagEnvVars,
|
|
1557
1554
|
...viteFlagVars,
|
|
1558
|
-
...(
|
|
1559
|
-
|
|
1560
|
-
|
|
1555
|
+
...(disableCloud ? { VITE_CADET_DISABLE_CLOUD: "true" } : {}),
|
|
1556
|
+
VITE_CLOUD_MODE: "false",
|
|
1557
|
+
VITE_WEB_MODE: "full",
|
|
1558
|
+
API_PROXY_TARGET: cloudUrl,
|
|
1561
1559
|
CADET_WEB_URL: getWebUrl(),
|
|
1562
|
-
|
|
1560
|
+
CADET_CLOUD_URL: cloudUrl,
|
|
1563
1561
|
PORT: String(port),
|
|
1564
1562
|
},
|
|
1565
1563
|
});
|
|
@@ -1590,7 +1588,7 @@ async function runViteDevServer(
|
|
|
1590
1588
|
* Return a possibly-refreshed bearer token for the TUI's startup auth.
|
|
1591
1589
|
*
|
|
1592
1590
|
* Only a STORED guardian token is refreshable: platform session auth
|
|
1593
|
-
* (`
|
|
1591
|
+
* (`isCloudHome(cloud)`) and ephemeral `--token` overrides (whose token won't
|
|
1594
1592
|
* match the store) are left untouched, as is a token that's still fresh. When
|
|
1595
1593
|
* the stored token has passed its `refreshAfter` (or expiry) and a refresh
|
|
1596
1594
|
* token is available, refresh once via the concurrency-safe refreshGuardianToken
|
|
@@ -1603,7 +1601,7 @@ export async function resolveFreshBearerToken(
|
|
|
1603
1601
|
bearerToken: string | undefined,
|
|
1604
1602
|
cloud: string | undefined,
|
|
1605
1603
|
): Promise<string | undefined> {
|
|
1606
|
-
if (
|
|
1604
|
+
if (isCloudHome(cloud) || !bearerToken || !assistantId) return bearerToken;
|
|
1607
1605
|
|
|
1608
1606
|
const stored = loadGuardianToken(assistantId);
|
|
1609
1607
|
// Refresh only the stored token (an ephemeral --token won't match), and only
|
|
@@ -1637,19 +1635,19 @@ export const client = Object.assign(
|
|
|
1637
1635
|
assistantName: parsedAssistantName,
|
|
1638
1636
|
workspaceRoot,
|
|
1639
1637
|
cloud,
|
|
1640
|
-
|
|
1638
|
+
cloudToken,
|
|
1641
1639
|
bearerToken,
|
|
1642
1640
|
interfaceId,
|
|
1643
1641
|
flagEnvVars,
|
|
1644
1642
|
parsedFlagOverrides,
|
|
1645
|
-
|
|
1643
|
+
disableCloud,
|
|
1646
1644
|
openBrowser: openInBrowser,
|
|
1647
1645
|
webPort,
|
|
1648
1646
|
background,
|
|
1649
1647
|
} = parseArgs();
|
|
1650
1648
|
|
|
1651
|
-
if (
|
|
1652
|
-
process.env.
|
|
1649
|
+
if (disableCloud) {
|
|
1650
|
+
process.env.CADET_DISABLE_CLOUD = "true";
|
|
1653
1651
|
}
|
|
1654
1652
|
|
|
1655
1653
|
if (interfaceId === WEB_INTERFACE_ID) {
|
|
@@ -1660,7 +1658,7 @@ export const client = Object.assign(
|
|
|
1660
1658
|
await runWebInterface(
|
|
1661
1659
|
flagEnvVars,
|
|
1662
1660
|
parsedFlagOverrides,
|
|
1663
|
-
|
|
1661
|
+
disableCloud,
|
|
1664
1662
|
openInBrowser,
|
|
1665
1663
|
webPort,
|
|
1666
1664
|
);
|
|
@@ -1675,23 +1673,23 @@ export const client = Object.assign(
|
|
|
1675
1673
|
interfaceId,
|
|
1676
1674
|
});
|
|
1677
1675
|
|
|
1678
|
-
const assistantName = await
|
|
1676
|
+
const assistantName = await maybeHydrateCloudAssistantName(
|
|
1679
1677
|
assistantId,
|
|
1680
1678
|
parsedAssistantName,
|
|
1681
1679
|
cloud,
|
|
1682
|
-
|
|
1680
|
+
cloudToken,
|
|
1683
1681
|
);
|
|
1684
1682
|
|
|
1685
1683
|
// Build pre-constructed request headers merged from auth + client registration.
|
|
1686
1684
|
// Spreading into every fetch site ensures consistency across REST and SSE endpoints.
|
|
1687
1685
|
let auth: Record<string, string> | undefined;
|
|
1688
|
-
if (
|
|
1689
|
-
const orgId = await fetchOrganizationId(
|
|
1686
|
+
if (isCloudHome(cloud) && cloudToken) {
|
|
1687
|
+
const orgId = await fetchOrganizationId(cloudToken).catch((err) => {
|
|
1690
1688
|
tuiLog.warn("failed to fetch organization id", { err: String(err) });
|
|
1691
1689
|
return undefined;
|
|
1692
1690
|
});
|
|
1693
1691
|
auth = {
|
|
1694
|
-
"X-Session-Token":
|
|
1692
|
+
"X-Session-Token": cloudToken,
|
|
1695
1693
|
...(orgId ? { "Cadet-Organization-Id": orgId } : {}),
|
|
1696
1694
|
...getClientRegistrationHeaders(interfaceId),
|
|
1697
1695
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
import { saveAssistantEntry } from "../lib/assistant-config.js";
|
|
7
|
+
import { completeCadet } from "./completions.js";
|
|
8
|
+
|
|
9
|
+
const COMMAND_NAMES = [
|
|
10
|
+
"backup",
|
|
11
|
+
"clean",
|
|
12
|
+
"client",
|
|
13
|
+
"completions",
|
|
14
|
+
"confirm",
|
|
15
|
+
"connect",
|
|
16
|
+
"hatch",
|
|
17
|
+
"ps",
|
|
18
|
+
"retire",
|
|
19
|
+
"sleep",
|
|
20
|
+
"use",
|
|
21
|
+
"wake",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
describe("completeCadet", () => {
|
|
25
|
+
let lockfileDir: string;
|
|
26
|
+
let prevLockfileDir: string | undefined;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
lockfileDir = mkdtempSync(join(tmpdir(), "cadet-completions-"));
|
|
30
|
+
prevLockfileDir = process.env.CADET_LOCKFILE_DIR;
|
|
31
|
+
process.env.CADET_LOCKFILE_DIR = lockfileDir;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
if (prevLockfileDir === undefined) delete process.env.CADET_LOCKFILE_DIR;
|
|
36
|
+
else process.env.CADET_LOCKFILE_DIR = prevLockfileDir;
|
|
37
|
+
rmSync(lockfileDir, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function seed(id: string): void {
|
|
41
|
+
saveAssistantEntry({
|
|
42
|
+
assistantId: id,
|
|
43
|
+
cloud: "docker",
|
|
44
|
+
runtimeUrl: "http://localhost:7830",
|
|
45
|
+
hatchedAt: new Date().toISOString(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("no words yet offers every command name, sorted", () => {
|
|
50
|
+
const input: string[] = [];
|
|
51
|
+
expect(completeCadet(input, COMMAND_NAMES)).toEqual(
|
|
52
|
+
[...COMMAND_NAMES].sort(),
|
|
53
|
+
);
|
|
54
|
+
// The word under the cursor is popped from a COPY — the caller's array
|
|
55
|
+
// (bash's COMP_WORDS) must come back untouched.
|
|
56
|
+
expect(input).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("first positional of an assistant-name command offers known assistant names", () => {
|
|
60
|
+
seed("quill-owl-d4e5f6");
|
|
61
|
+
seed("fable-fox-a1b2c3");
|
|
62
|
+
// `cadet retire <TAB>` — the empty trailing word is the word being completed.
|
|
63
|
+
expect(completeCadet(["retire", ""], COMMAND_NAMES)).toEqual([
|
|
64
|
+
"fable-fox-a1b2c3",
|
|
65
|
+
"quill-owl-d4e5f6",
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("partial words are returned unfiltered — the shell does prefix filtering", () => {
|
|
70
|
+
seed("quill-owl-d4e5f6");
|
|
71
|
+
seed("fable-fox-a1b2c3");
|
|
72
|
+
expect(completeCadet(["retire", "fable"], COMMAND_NAMES)).toEqual([
|
|
73
|
+
"fable-fox-a1b2c3",
|
|
74
|
+
"quill-owl-d4e5f6",
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("flag prefixes fall through to the shell default (no candidates)", () => {
|
|
79
|
+
seed("fable-fox-a1b2c3");
|
|
80
|
+
expect(completeCadet(["retire", "--"], COMMAND_NAMES)).toEqual([]);
|
|
81
|
+
expect(completeCadet(["retire", "--name"], COMMAND_NAMES)).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("deeper positionals of an assistant-name command offer nothing", () => {
|
|
85
|
+
seed("fable-fox-a1b2c3");
|
|
86
|
+
expect(
|
|
87
|
+
completeCadet(["exec", "fable-fox-a1b2c3", ""], COMMAND_NAMES),
|
|
88
|
+
).toEqual([]);
|
|
89
|
+
expect(
|
|
90
|
+
completeCadet(["logs", "fable-fox-a1b2c3", "tail", ""], COMMAND_NAMES),
|
|
91
|
+
).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("commands that take no assistant name offer nothing after the command word", () => {
|
|
95
|
+
seed("fable-fox-a1b2c3");
|
|
96
|
+
expect(completeCadet(["hatch", ""], COMMAND_NAMES)).toEqual([]);
|
|
97
|
+
expect(completeCadet(["ps", ""], COMMAND_NAMES)).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("an unreadable/empty lockfile completes quietly with no names", () => {
|
|
101
|
+
// No entries seeded — the lockfile dir exists but has no lockfile.json.
|
|
102
|
+
expect(completeCadet(["retire", ""], COMMAND_NAMES)).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { loadAllAssistants } from "../lib/assistant-config.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Commands whose first positional argument is an assistant name — completing
|
|
5
|
+
* their first word offers the names of known assistants (from the lockfile)
|
|
6
|
+
* instead of falling through to shell file completion.
|
|
7
|
+
*/
|
|
8
|
+
const ASSISTANT_NAME_COMMANDS = new Set([
|
|
9
|
+
"exec",
|
|
10
|
+
"logs",
|
|
11
|
+
"message",
|
|
12
|
+
"restart",
|
|
13
|
+
"restore",
|
|
14
|
+
"retire",
|
|
15
|
+
"rollback",
|
|
16
|
+
"sleep",
|
|
17
|
+
"teleport",
|
|
18
|
+
"terminal",
|
|
19
|
+
"tunnel",
|
|
20
|
+
"unpair",
|
|
21
|
+
"upgrade",
|
|
22
|
+
"use",
|
|
23
|
+
"wake",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Assistant names known to this CLI's lockfile; [] when the lockfile is unreadable. */
|
|
27
|
+
function assistantNames(): string[] {
|
|
28
|
+
try {
|
|
29
|
+
return loadAllAssistants()
|
|
30
|
+
.map((entry) => entry.assistantId)
|
|
31
|
+
.sort();
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve completion candidates for the word the cursor is on.
|
|
39
|
+
*
|
|
40
|
+
* `words` carries every command-line word after `cadet` up to the cursor,
|
|
41
|
+
* INCLUDING the (possibly empty) word being completed — the same shape bash
|
|
42
|
+
* passes via `COMP_WORDS[@]:1`. The returned list is UNFILTERED: the shell
|
|
43
|
+
* (`compgen -W`) does the prefix filtering, so the CLI only decides WHAT
|
|
44
|
+
* KIND of thing completes here.
|
|
45
|
+
*/
|
|
46
|
+
export function completeCadet(
|
|
47
|
+
input: string[],
|
|
48
|
+
commandNames: readonly string[],
|
|
49
|
+
): string[] {
|
|
50
|
+
const words = [...input];
|
|
51
|
+
const toComplete = words.pop() ?? "";
|
|
52
|
+
const preceding = words;
|
|
53
|
+
|
|
54
|
+
// Completing the command word itself (or nothing typed yet).
|
|
55
|
+
if (preceding.length === 0) {
|
|
56
|
+
return [...commandNames].sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// First positional of an assistant-name command: offer instance names.
|
|
60
|
+
// Flag prefixes fall through to shell default (no flag completion yet).
|
|
61
|
+
if (
|
|
62
|
+
preceding.length === 1 &&
|
|
63
|
+
ASSISTANT_NAME_COMMANDS.has(preceding[0]) &&
|
|
64
|
+
!toComplete.startsWith("-")
|
|
65
|
+
) {
|
|
66
|
+
return assistantNames();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const BASH_COMPLETION_SCRIPT = `# bash completion for the cadet CLI — load with: eval "$(cadet completions)"
|
|
73
|
+
_cadet_completions() {
|
|
74
|
+
local candidates
|
|
75
|
+
candidates=$(cadet __complete "\${COMP_WORDS[@]:1}" 2>/dev/null)
|
|
76
|
+
COMPREPLY=($(compgen -W "$candidates" -- "\${COMP_WORDS[COMP_CWORD]}"))
|
|
77
|
+
}
|
|
78
|
+
complete -F _cadet_completions cadet`;
|
|
79
|
+
|
|
80
|
+
// ── Entry point ─────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export const completions = Object.assign(
|
|
83
|
+
async function completions(): Promise<void> {
|
|
84
|
+
console.log(BASH_COMPLETION_SCRIPT);
|
|
85
|
+
},
|
|
86
|
+
{ description: `Print bash completion setup (eval "$(cadet completions)")` },
|
|
87
|
+
);
|