@crouter/api 0.3.387 → 0.3.389
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/dist/api/__tests__/integration/client.test.js +97 -0
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +40 -21
- package/dist/api/dto/config.d.ts +11 -1
- package/dist/core/asset-root.d.ts +7 -0
- package/dist/core/asset-root.js +18 -0
- package/dist/core/canvas/boot-id.d.ts +6 -0
- package/dist/core/canvas/boot-id.js +26 -0
- package/dist/core/canvas/paths.d.ts +72 -0
- package/dist/core/canvas/paths.js +163 -0
- package/dist/core/canvas/pid.d.ts +391 -0
- package/dist/core/canvas/pid.js +948 -0
- package/dist/core/command-plugins/bundle.d.ts +149 -0
- package/dist/core/command-plugins/bundle.js +588 -0
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +51 -0
- package/dist/core/config.d.ts +233 -0
- package/dist/core/config.js +1120 -0
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/errors.d.ts +38 -0
- package/dist/core/errors.js +90 -0
- package/dist/core/events/emit.d.ts +6 -0
- package/dist/core/events/emit.js +42 -0
- package/dist/core/events/envelope.d.ts +2 -0
- package/dist/core/events/envelope.js +84 -0
- package/dist/core/events/errors.d.ts +4 -0
- package/dist/core/events/errors.js +69 -0
- package/dist/core/events/operation-id.d.ts +4 -0
- package/dist/core/events/operation-id.js +24 -0
- package/dist/core/events/serialize.d.ts +4 -0
- package/dist/core/events/serialize.js +199 -0
- package/dist/core/events/source.d.ts +16 -0
- package/dist/core/events/source.js +31 -0
- package/dist/core/events/types.d.ts +68 -0
- package/dist/core/events/types.js +11 -0
- package/dist/core/exclusive-lock.d.ts +34 -0
- package/dist/core/exclusive-lock.js +197 -0
- package/dist/core/fs-utils.d.ts +44 -0
- package/dist/core/fs-utils.js +208 -0
- package/dist/core/help.d.ts +309 -0
- package/dist/core/help.js +406 -0
- package/dist/core/human/page-catalog.d.ts +57 -0
- package/dist/core/human/page-catalog.js +172 -0
- package/dist/core/installed-plugins.d.ts +2 -0
- package/dist/core/installed-plugins.js +79 -0
- package/dist/core/io.d.ts +122 -0
- package/dist/core/io.js +373 -0
- package/dist/core/keybindings/attach-control.d.ts +49 -0
- package/dist/core/keybindings/attach-control.js +42 -0
- package/dist/core/keybindings/catalog.d.ts +18 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/types.d.ts +42 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/layout.d.ts +26 -0
- package/dist/core/layout.js +94 -0
- package/dist/core/locked-file.d.ts +27 -0
- package/dist/core/locked-file.js +118 -0
- package/dist/core/log.d.ts +9 -0
- package/dist/core/log.js +89 -0
- package/dist/core/manifest.d.ts +5 -0
- package/dist/core/manifest.js +15 -0
- package/dist/core/plugin-env.d.ts +8 -0
- package/dist/core/plugin-env.js +31 -0
- package/dist/core/plugin-extensions.d.ts +29 -0
- package/dist/core/plugin-extensions.js +191 -0
- package/dist/core/plugin-swap-lock.d.ts +9 -0
- package/dist/core/plugin-swap-lock.js +31 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +26 -0
- package/dist/core/profiles/env-store.d.ts +22 -0
- package/dist/core/profiles/env-store.js +163 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +120 -0
- package/dist/core/profiles/manifest.js +529 -0
- package/dist/core/rate-limit-scope.d.ts +25 -0
- package/dist/core/rate-limit-scope.js +64 -0
- package/dist/core/render.d.ts +12 -0
- package/dist/core/render.js +138 -0
- package/dist/core/resolver.d.ts +14 -0
- package/dist/core/resolver.js +111 -0
- package/dist/core/runtime/branded-host.d.ts +25 -0
- package/dist/core/runtime/branded-host.js +264 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
- package/dist/core/runtime/broker/daemon-ops.js +177 -0
- package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
- package/dist/core/runtime/broker/signal-stream.js +149 -0
- package/dist/core/scope.d.ts +32 -0
- package/dist/core/scope.js +184 -0
- package/dist/core/scoped-state/db.d.ts +17 -0
- package/dist/core/scoped-state/db.js +247 -0
- package/dist/core/scoped-state/migrate.d.ts +8 -0
- package/dist/core/scoped-state/migrate.js +187 -0
- package/dist/core/scoped-state/paths.d.ts +9 -0
- package/dist/core/scoped-state/paths.js +27 -0
- package/dist/core/scoped-state/profiles.d.ts +27 -0
- package/dist/core/scoped-state/profiles.js +93 -0
- package/dist/core/scoped-state/providers.d.ts +24 -0
- package/dist/core/scoped-state/providers.js +19 -0
- package/dist/core/scoped-state/schema.d.ts +6 -0
- package/dist/core/scoped-state/schema.js +43 -0
- package/dist/core/scoped-state/settings.d.ts +28 -0
- package/dist/core/scoped-state/settings.js +83 -0
- package/dist/core/spaces/open-beneath.d.ts +71 -0
- package/dist/core/spaces/open-beneath.js +581 -0
- package/dist/core/sqlite-statements.d.ts +4 -0
- package/dist/core/sqlite-statements.js +17 -0
- package/dist/core/subscription-state.d.ts +121 -0
- package/dist/core/subscription-state.js +287 -0
- package/dist/core/user-settings.d.ts +377 -0
- package/dist/core/user-settings.js +458 -0
- package/dist/daemon/broker-signals/bus.d.ts +30 -0
- package/dist/daemon/broker-signals/bus.js +87 -0
- package/dist/daemon/manage.d.ts +176 -0
- package/dist/daemon/manage.js +664 -0
- package/dist/daemon/pidfile.d.ts +8 -0
- package/dist/daemon/pidfile.js +37 -0
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/native/linux.d.ts +29 -0
- package/dist/native/linux.js +20 -0
- package/dist/shared/env.d.ts +116 -0
- package/dist/shared/env.js +271 -0
- package/dist/shared/inbox-entry-body.d.ts +22 -0
- package/dist/shared/inbox-entry-body.js +116 -0
- package/dist/shared/working-activity.d.ts +9 -0
- package/dist/shared/working-activity.js +27 -0
- package/dist/types.d.ts +562 -0
- package/dist/types.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// installed-plugins.ts — the LEAF-LEVEL installed-plugin reader: list the
|
|
2
|
+
// plugin packages physically present under one scope root, with their
|
|
3
|
+
// manifests and enabled-state from that root's config.json `plugins` block.
|
|
4
|
+
//
|
|
5
|
+
// This lives below resolver.ts (which re-exports it for its existing callers)
|
|
6
|
+
// so that config.ts can layer plugin-declared launch config (`kinds`) into
|
|
7
|
+
// `readMergedLaunchConfig` without an import cycle: resolver.ts imports
|
|
8
|
+
// config.ts, so config.ts can never import resolver.ts. Imports here stay
|
|
9
|
+
// leaf-safe: types, fs-utils, manifest only.
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { CONFIG_FILE } from '../types.js';
|
|
12
|
+
import { listDirs, pathExists, readJsonIfExists } from './fs-utils.js';
|
|
13
|
+
import { readPluginManifest } from './manifest.js';
|
|
14
|
+
import { awaitBundleSwap, bundleSwapInFlightElsewhere } from './plugin-swap-lock.js';
|
|
15
|
+
import { userStore } from './scoped-state/paths.js';
|
|
16
|
+
import { readRawSettings } from './scoped-state/settings.js';
|
|
17
|
+
import { pluginsDir } from './scope.js';
|
|
18
|
+
function pluginConfigForRoot(scope, root) {
|
|
19
|
+
const store = scope === 'user' ? userStore(root) : undefined;
|
|
20
|
+
const cfg = store
|
|
21
|
+
? readRawSettings({ store, key: 'user', kind: 'user', root: store.root })
|
|
22
|
+
: readJsonIfExists(join(root, CONFIG_FILE));
|
|
23
|
+
return cfg && cfg.plugins && typeof cfg.plugins === 'object' ? cfg.plugins : {};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A bundle-plugin swap replaces `<plugins>/<name>` with two renames, so there
|
|
27
|
+
* is a window in which the config names a plugin whose directory is not there.
|
|
28
|
+
* A list taken inside that window would drop a plugin that is installed and
|
|
29
|
+
* about to be present again — the caller would build a command tree missing it.
|
|
30
|
+
*
|
|
31
|
+
* That exact signal — named in config, absent on disk — is what we re-check,
|
|
32
|
+
* and only when another process's swap lock says a swap is actually running: a
|
|
33
|
+
* genuinely deleted directory holds no lock and is reported as gone at once.
|
|
34
|
+
* Every normal invocation, where each config-named plugin has its directory,
|
|
35
|
+
* pays one Set lookup per name and nothing else.
|
|
36
|
+
*/
|
|
37
|
+
function swapsToWaitOut(scopeRootPath, cfg, present) {
|
|
38
|
+
const names = Object.keys(cfg);
|
|
39
|
+
if (names.every((name) => present.has(name)))
|
|
40
|
+
return [];
|
|
41
|
+
return names.filter((name) => !present.has(name) && bundleSwapInFlightElsewhere(scopeRootPath, name));
|
|
42
|
+
}
|
|
43
|
+
export function listInstalledPluginsInRoot(scope, scopeRootPath) {
|
|
44
|
+
const dir = scope === 'user' ? pluginsDir('user') : join(scopeRootPath, 'plugins');
|
|
45
|
+
if (!pathExists(dir))
|
|
46
|
+
return [];
|
|
47
|
+
const cfg = pluginConfigForRoot(scope, scopeRootPath);
|
|
48
|
+
const first = collectInstalledPlugins(scope, dir, cfg);
|
|
49
|
+
const waiting = swapsToWaitOut(scopeRootPath, cfg, new Set(first.map((plugin) => plugin.name)));
|
|
50
|
+
if (waiting.length === 0)
|
|
51
|
+
return first;
|
|
52
|
+
for (const name of waiting)
|
|
53
|
+
awaitBundleSwap(scopeRootPath, name);
|
|
54
|
+
// One re-list, never a loop: the second read is taken after every swap we
|
|
55
|
+
// observed has finished. A swap that starts after it is a package this
|
|
56
|
+
// invocation was never going to see anyway.
|
|
57
|
+
return collectInstalledPlugins(scope, dir, cfg);
|
|
58
|
+
}
|
|
59
|
+
function collectInstalledPlugins(scope, dir, cfg) {
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const name of listDirs(dir)) {
|
|
62
|
+
const root = join(dir, name);
|
|
63
|
+
const manifest = readPluginManifest(root);
|
|
64
|
+
if (!manifest)
|
|
65
|
+
continue;
|
|
66
|
+
const entry = cfg[name];
|
|
67
|
+
const version = typeof entry?.version === 'string' ? entry.version : manifest.version;
|
|
68
|
+
out.push({
|
|
69
|
+
name,
|
|
70
|
+
scope,
|
|
71
|
+
root,
|
|
72
|
+
manifest,
|
|
73
|
+
enabled: typeof entry?.enabled === 'boolean' ? entry.enabled : true,
|
|
74
|
+
sourceMarketplace: typeof entry?.source_marketplace === 'string' ? entry.source_marketplace : undefined,
|
|
75
|
+
version,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CrtrError } from './errors.js';
|
|
2
|
+
import { type ExitCodeValue } from '../types.js';
|
|
3
|
+
import { ApiError } from '../api/index.js';
|
|
4
|
+
/** The private result mirror for one crtr CLI invocation launched by the
|
|
5
|
+
* canvas bash-preview extension. This deliberately never enters stdout: bash
|
|
6
|
+
* text is model context, while Pi tool-result details are viewer-only.
|
|
7
|
+
*
|
|
8
|
+
* One bash tool call may run several crtr invocations (`read a && read b`), so
|
|
9
|
+
* the mirror file is JSONL: one record per invocation, appended in order. Each
|
|
10
|
+
* invocation is its own process and `beginPreview` resets this module's state,
|
|
11
|
+
* so the appends compose across them with no shared state or locking. */
|
|
12
|
+
export interface CrtrPreviewRecord {
|
|
13
|
+
path: string;
|
|
14
|
+
result?: Record<string, unknown>;
|
|
15
|
+
jsonl?: Record<string, unknown>[];
|
|
16
|
+
error?: ErrorPayload;
|
|
17
|
+
}
|
|
18
|
+
/** A preview is bounded UI metadata, never a bulk-data channel. One
|
|
19
|
+
* `crtr integration run` once mirrored a 3 MB CRM dump into every copy of the
|
|
20
|
+
* transcript that contains its tool result — the session file, every replay
|
|
21
|
+
* frame, and the host product's stored conversation — because pi's 50 KB
|
|
22
|
+
* stdout truncation never sees this channel. The mirror write is the one
|
|
23
|
+
* choke point every record passes, so the bound lives here: an oversized
|
|
24
|
+
* record is dropped, and the viewer falls back to rendering the agent-facing
|
|
25
|
+
* stdout text, which pi bounds itself.
|
|
26
|
+
*
|
|
27
|
+
* Because records accumulate, this bounds the FILE TOTAL, not one line: a
|
|
28
|
+
* record that would overrun the remaining budget is dropped whole, so every
|
|
29
|
+
* line on disk stays valid JSON. */
|
|
30
|
+
export declare const PREVIEW_RESULT_MAX_BYTES: number;
|
|
31
|
+
/** Begin collecting the current leaf's structured response, if its caller
|
|
32
|
+
* supplied the private per-tool-call result path. */
|
|
33
|
+
export declare function beginPreview(path: string): void;
|
|
34
|
+
/** Publish the single-object or collected JSONL result after leaf dispatch. */
|
|
35
|
+
export declare function publishPreviewResult(result: Record<string, unknown> | void): void;
|
|
36
|
+
/** Mirror a structured record whose caller renders it itself instead of using
|
|
37
|
+
* the generic dispatcher (JSONL and event-stream leaves). */
|
|
38
|
+
export declare function recordPreviewLine(obj: Record<string, unknown>): void;
|
|
39
|
+
/** Preserve a verbatim JSONL stream's structured frames without changing the
|
|
40
|
+
* bytes written to stdout. Invalid third-party lines remain raw-only. */
|
|
41
|
+
export declare function recordPreviewJsonLine(line: string): void;
|
|
42
|
+
/** Record a terminal streaming failure that correctly remains off stdout
|
|
43
|
+
* because prior frames have already been delivered. */
|
|
44
|
+
export declare function recordPreviewError(payload: ErrorPayload): void;
|
|
45
|
+
/** Set by the dispatcher when `--json` is present anywhere in argv. */
|
|
46
|
+
export declare function setJsonOutput(v: boolean): void;
|
|
47
|
+
/** True when the caller asked for raw JSON instead of rendered prose. */
|
|
48
|
+
export declare function isJsonOutput(): boolean;
|
|
49
|
+
/** Structured error payload. `error` is a stable code the agent branches on;
|
|
50
|
+
* `next` is the recovery road sign. */
|
|
51
|
+
interface ErrorPayloadBase {
|
|
52
|
+
error: string;
|
|
53
|
+
message: string;
|
|
54
|
+
received?: unknown;
|
|
55
|
+
field?: string;
|
|
56
|
+
/** Transport status of a failed backend call (HTTP-transport plugins). It is kept out
|
|
57
|
+
* of `received` on purpose: `received` names the caller's offending VALUE, and
|
|
58
|
+
* reporting an HTTP status there is a false claim about what was sent. */
|
|
59
|
+
http_status?: number;
|
|
60
|
+
next: string;
|
|
61
|
+
}
|
|
62
|
+
export type ErrorPayload = ErrorPayloadBase & ({
|
|
63
|
+
primaryCompleted: true;
|
|
64
|
+
result: Record<string, unknown>;
|
|
65
|
+
} | {
|
|
66
|
+
primaryCompleted?: never;
|
|
67
|
+
result?: never;
|
|
68
|
+
});
|
|
69
|
+
/** A command-level failure: surfaces as the JSON response on stdout. */
|
|
70
|
+
export declare class InputError extends CrtrError {
|
|
71
|
+
payload: ErrorPayload;
|
|
72
|
+
constructor(payload: ErrorPayload, exitCode?: ExitCodeValue);
|
|
73
|
+
}
|
|
74
|
+
/** Read raw stdin to EOF. Returns empty string when stdin is a TTY (no pipe).
|
|
75
|
+
* Called by the argv parser for leaves declaring a `stdin` parameter. */
|
|
76
|
+
export declare function readStdinRaw(): Promise<string>;
|
|
77
|
+
/** Best-effort, non-hanging peek at whatever stdin bytes are already
|
|
78
|
+
* buffered — used ONLY to detect a positional/stdin conflict, never to
|
|
79
|
+
* consume the leaf's actual required stdin body. Genuine piped/redirected
|
|
80
|
+
* content (a heredoc, `< file`, `cat x |`) is fully written into the pipe
|
|
81
|
+
* before this process starts reading, so it resolves within one tick; a
|
|
82
|
+
* non-TTY stdin that's simply inherited and held open with nothing written
|
|
83
|
+
* to it (e.g. a caller subprocess that never closes its own stdin) would
|
|
84
|
+
* otherwise hang `readStdinRaw()` forever — this bounds that wait instead
|
|
85
|
+
* of blocking the whole invocation on an ambiguous pipe. Returns '' on a
|
|
86
|
+
* TTY (nothing can be piped) or once the bound elapses with no bytes. */
|
|
87
|
+
export declare function peekStdinRaw(timeoutMs?: number): Promise<string>;
|
|
88
|
+
/** Raw-JSON mirror of a single-shot response (the `--json` escape hatch). The
|
|
89
|
+
* default path renders the result as prose instead — see render.ts. */
|
|
90
|
+
export declare function emit(obj: Record<string, unknown>): void;
|
|
91
|
+
/** One JSONL record. Call per event in a stream; partial reads stay parseable. */
|
|
92
|
+
export declare function emitLine(obj: Record<string, unknown>): void;
|
|
93
|
+
/**
|
|
94
|
+
* Write to stdout and resolve true ONLY once the bytes are confirmed flushed to
|
|
95
|
+
* a connected reader. Resolves false if the consumer is gone (EPIPE) or the
|
|
96
|
+
* write fails. This is the reliable "the caller actually received it" signal:
|
|
97
|
+
* use it to gate side effects that must only happen on genuine delivery (e.g.
|
|
98
|
+
* acking a collected result). A killed process never resolves at all — also
|
|
99
|
+
* safe, since the gated side effect then never runs.
|
|
100
|
+
*/
|
|
101
|
+
export declare function writeStdout(s: string): Promise<boolean>;
|
|
102
|
+
export declare function diag(message: string): void;
|
|
103
|
+
/** Translate an `ApiError` into the clean CLI error a leaf should surface.
|
|
104
|
+
* Three tiers, in priority order:
|
|
105
|
+
* 1. Daemon-down (`daemon_unavailable`) → a `network`-class `CrtrError`
|
|
106
|
+
* so the exit code and next-hint match the operational failure it is.
|
|
107
|
+
* 2. A server-thrown `InputError` round-trips its FULL structured payload
|
|
108
|
+
* through `toErrorBody` into `ApiError.details` (`{error,message,next,…}`) —
|
|
109
|
+
* reconstruct it verbatim so a gate/guard error (revive reopen-gate, push
|
|
110
|
+
* worktree/finalization guard, …) surfaces byte-for-byte as the leaf raised
|
|
111
|
+
* it before the re-plumb, preserving its SPECIFIC code + next.
|
|
112
|
+
* 3. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
|
|
113
|
+
* and a caller-supplied-or-default `next`. */
|
|
114
|
+
export declare function apiErrorToCliError(err: ApiError, next?: string): CrtrError;
|
|
115
|
+
/** Terminal error handler. Command-level failures (bad input, not-found,
|
|
116
|
+
* ambiguous) surface as the JSON response on stdout so the caller parses one
|
|
117
|
+
* contract. A daemon `ApiError` is translated to that same clean contract
|
|
118
|
+
* (`apiErrorToCliError`) as a safety net for any leaf that lets it propagate.
|
|
119
|
+
* Runtime/internal failures go to stderr as `{error:"internal"}` — raw traces
|
|
120
|
+
* never reach the agent. Exits non-zero either way. */
|
|
121
|
+
export declare function handle(e: unknown): void;
|
|
122
|
+
export {};
|
package/dist/core/io.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// The agent-facing I/O contract. Flags and positional args on input; stdout is
|
|
2
|
+
// agent-ready markdown/XML the caller acts on directly (the result rendered FOR
|
|
3
|
+
// the model, not data it parses); structured errors; stderr is diagnostics only
|
|
4
|
+
// and never carries the result. The raw JSON object is available behind the
|
|
5
|
+
// `--json` global for tooling. See the cli-design reference.
|
|
6
|
+
import { appendFileSync, statSync } from 'node:fs';
|
|
7
|
+
import { CrtrError } from './errors.js';
|
|
8
|
+
import { ExitCode } from '../types.js';
|
|
9
|
+
import { PREVIEW_RESULT_PATH_ENV } from './preview-result-path.js';
|
|
10
|
+
import { renderError } from './render.js';
|
|
11
|
+
import { ApiError } from '../api/index.js';
|
|
12
|
+
// output mode — prose (default) vs raw JSON (--json global, for tooling)
|
|
13
|
+
let jsonOutput = false;
|
|
14
|
+
let preview;
|
|
15
|
+
/** A preview is bounded UI metadata, never a bulk-data channel. One
|
|
16
|
+
* `crtr integration run` once mirrored a 3 MB CRM dump into every copy of the
|
|
17
|
+
* transcript that contains its tool result — the session file, every replay
|
|
18
|
+
* frame, and the host product's stored conversation — because pi's 50 KB
|
|
19
|
+
* stdout truncation never sees this channel. The mirror write is the one
|
|
20
|
+
* choke point every record passes, so the bound lives here: an oversized
|
|
21
|
+
* record is dropped, and the viewer falls back to rendering the agent-facing
|
|
22
|
+
* stdout text, which pi bounds itself.
|
|
23
|
+
*
|
|
24
|
+
* Because records accumulate, this bounds the FILE TOTAL, not one line: a
|
|
25
|
+
* record that would overrun the remaining budget is dropped whole, so every
|
|
26
|
+
* line on disk stays valid JSON. */
|
|
27
|
+
export const PREVIEW_RESULT_MAX_BYTES = 256 * 1024;
|
|
28
|
+
/** Begin collecting the current leaf's structured response, if its caller
|
|
29
|
+
* supplied the private per-tool-call result path. */
|
|
30
|
+
export function beginPreview(path) {
|
|
31
|
+
const resultPath = process.env[PREVIEW_RESULT_PATH_ENV];
|
|
32
|
+
preview = resultPath === undefined || resultPath === '' ? undefined : { path, jsonl: [] };
|
|
33
|
+
}
|
|
34
|
+
function publishPreview(record) {
|
|
35
|
+
const resultPath = process.env[PREVIEW_RESULT_PATH_ENV];
|
|
36
|
+
if (resultPath === undefined || resultPath === '')
|
|
37
|
+
return;
|
|
38
|
+
const line = `${JSON.stringify({ path: preview?.path ?? '', ...record })}\n`;
|
|
39
|
+
try {
|
|
40
|
+
const spent = statSync(resultPath, { throwIfNoEntry: false })?.size ?? 0;
|
|
41
|
+
if (spent + Buffer.byteLength(line, 'utf8') > PREVIEW_RESULT_MAX_BYTES)
|
|
42
|
+
return;
|
|
43
|
+
appendFileSync(resultPath, line, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Preview transport is optional UI metadata. Its failure must never alter
|
|
47
|
+
// the CLI's stdout/error contract.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Publish the single-object or collected JSONL result after leaf dispatch. */
|
|
51
|
+
export function publishPreviewResult(result) {
|
|
52
|
+
if (preview === undefined)
|
|
53
|
+
return;
|
|
54
|
+
if (result !== undefined && result !== null)
|
|
55
|
+
publishPreview({ result });
|
|
56
|
+
else if (preview.jsonl.length > 0 || preview.error !== undefined) {
|
|
57
|
+
publishPreview({ ...(preview.jsonl.length > 0 ? { jsonl: preview.jsonl } : {}), ...(preview.error === undefined ? {} : { error: preview.error }) });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Mirror a structured record whose caller renders it itself instead of using
|
|
61
|
+
* the generic dispatcher (JSONL and event-stream leaves). */
|
|
62
|
+
export function recordPreviewLine(obj) {
|
|
63
|
+
preview?.jsonl.push(obj);
|
|
64
|
+
}
|
|
65
|
+
/** Preserve a verbatim JSONL stream's structured frames without changing the
|
|
66
|
+
* bytes written to stdout. Invalid third-party lines remain raw-only. */
|
|
67
|
+
export function recordPreviewJsonLine(line) {
|
|
68
|
+
try {
|
|
69
|
+
const value = JSON.parse(line);
|
|
70
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value))
|
|
71
|
+
recordPreviewLine(value);
|
|
72
|
+
}
|
|
73
|
+
catch { /* a malformed remote frame remains the command's raw truth */ }
|
|
74
|
+
}
|
|
75
|
+
/** Record a terminal streaming failure that correctly remains off stdout
|
|
76
|
+
* because prior frames have already been delivered. */
|
|
77
|
+
export function recordPreviewError(payload) {
|
|
78
|
+
if (preview !== undefined)
|
|
79
|
+
preview.error = payload;
|
|
80
|
+
}
|
|
81
|
+
/** Set by the dispatcher when `--json` is present anywhere in argv. */
|
|
82
|
+
export function setJsonOutput(v) {
|
|
83
|
+
jsonOutput = v;
|
|
84
|
+
}
|
|
85
|
+
/** True when the caller asked for raw JSON instead of rendered prose. */
|
|
86
|
+
export function isJsonOutput() {
|
|
87
|
+
return jsonOutput;
|
|
88
|
+
}
|
|
89
|
+
/** A command-level failure: surfaces as the JSON response on stdout. */
|
|
90
|
+
export class InputError extends CrtrError {
|
|
91
|
+
payload;
|
|
92
|
+
constructor(payload, exitCode = ExitCode.USAGE) {
|
|
93
|
+
super(payload.error, payload.message, exitCode, { ...payload });
|
|
94
|
+
this.name = 'InputError';
|
|
95
|
+
this.payload = payload;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// stdin
|
|
99
|
+
/** Read raw stdin to EOF. Returns empty string when stdin is a TTY (no pipe).
|
|
100
|
+
* Called by the argv parser for leaves declaring a `stdin` parameter. */
|
|
101
|
+
export async function readStdinRaw() {
|
|
102
|
+
if (process.stdin.isTTY)
|
|
103
|
+
return '';
|
|
104
|
+
const chunks = [];
|
|
105
|
+
for await (const chunk of process.stdin)
|
|
106
|
+
chunks.push(chunk);
|
|
107
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
108
|
+
}
|
|
109
|
+
/** Best-effort, non-hanging peek at whatever stdin bytes are already
|
|
110
|
+
* buffered — used ONLY to detect a positional/stdin conflict, never to
|
|
111
|
+
* consume the leaf's actual required stdin body. Genuine piped/redirected
|
|
112
|
+
* content (a heredoc, `< file`, `cat x |`) is fully written into the pipe
|
|
113
|
+
* before this process starts reading, so it resolves within one tick; a
|
|
114
|
+
* non-TTY stdin that's simply inherited and held open with nothing written
|
|
115
|
+
* to it (e.g. a caller subprocess that never closes its own stdin) would
|
|
116
|
+
* otherwise hang `readStdinRaw()` forever — this bounds that wait instead
|
|
117
|
+
* of blocking the whole invocation on an ambiguous pipe. Returns '' on a
|
|
118
|
+
* TTY (nothing can be piped) or once the bound elapses with no bytes. */
|
|
119
|
+
export async function peekStdinRaw(timeoutMs = 200) {
|
|
120
|
+
if (process.stdin.isTTY)
|
|
121
|
+
return '';
|
|
122
|
+
return await new Promise((resolve) => {
|
|
123
|
+
const chunks = [];
|
|
124
|
+
let settled = false;
|
|
125
|
+
const finish = () => {
|
|
126
|
+
if (settled)
|
|
127
|
+
return;
|
|
128
|
+
settled = true;
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
process.stdin.removeListener('data', onData);
|
|
131
|
+
process.stdin.removeListener('end', onEnd);
|
|
132
|
+
process.stdin.removeListener('error', onError);
|
|
133
|
+
process.stdin.pause();
|
|
134
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
135
|
+
};
|
|
136
|
+
const onData = (chunk) => chunks.push(chunk);
|
|
137
|
+
const onEnd = () => finish();
|
|
138
|
+
const onError = () => finish();
|
|
139
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
140
|
+
process.stdin.on('data', onData);
|
|
141
|
+
process.stdin.on('end', onEnd);
|
|
142
|
+
process.stdin.on('error', onError);
|
|
143
|
+
process.stdin.resume();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// stdout — the result, nothing else
|
|
147
|
+
/** Raw-JSON mirror of a single-shot response (the `--json` escape hatch). The
|
|
148
|
+
* default path renders the result as prose instead — see render.ts. */
|
|
149
|
+
export function emit(obj) {
|
|
150
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
151
|
+
}
|
|
152
|
+
/** One JSONL record. Call per event in a stream; partial reads stay parseable. */
|
|
153
|
+
export function emitLine(obj) {
|
|
154
|
+
recordPreviewLine(obj);
|
|
155
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Write to stdout and resolve true ONLY once the bytes are confirmed flushed to
|
|
159
|
+
* a connected reader. Resolves false if the consumer is gone (EPIPE) or the
|
|
160
|
+
* write fails. This is the reliable "the caller actually received it" signal:
|
|
161
|
+
* use it to gate side effects that must only happen on genuine delivery (e.g.
|
|
162
|
+
* acking a collected result). A killed process never resolves at all — also
|
|
163
|
+
* safe, since the gated side effect then never runs.
|
|
164
|
+
*/
|
|
165
|
+
export function writeStdout(s) {
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
let settled = false;
|
|
168
|
+
const onErr = (e) => {
|
|
169
|
+
if (e.code === 'EPIPE')
|
|
170
|
+
finish(false);
|
|
171
|
+
};
|
|
172
|
+
const finish = (ok) => {
|
|
173
|
+
if (settled)
|
|
174
|
+
return;
|
|
175
|
+
settled = true;
|
|
176
|
+
process.stdout.off('error', onErr);
|
|
177
|
+
resolve(ok);
|
|
178
|
+
};
|
|
179
|
+
process.stdout.on('error', onErr);
|
|
180
|
+
try {
|
|
181
|
+
process.stdout.write(s, (err) => finish(err === null || err === undefined));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
finish(false);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// stderr — diagnostics the agent MAY capture, never the result
|
|
189
|
+
export function diag(message) {
|
|
190
|
+
process.stderr.write(message + '\n');
|
|
191
|
+
}
|
|
192
|
+
// errors
|
|
193
|
+
function payloadOf(e) {
|
|
194
|
+
if (e instanceof InputError)
|
|
195
|
+
return e.payload;
|
|
196
|
+
const d = (e.details !== undefined ? e.details : {});
|
|
197
|
+
const result = d.primaryCompleted === true
|
|
198
|
+
&& d.result !== null
|
|
199
|
+
&& typeof d.result === 'object'
|
|
200
|
+
&& !Array.isArray(d.result)
|
|
201
|
+
? jsonSafeResult(d.result)
|
|
202
|
+
: undefined;
|
|
203
|
+
const next = d.next !== undefined
|
|
204
|
+
? d.next
|
|
205
|
+
: 'Inspect the error and adjust the call. See -h for the schema.';
|
|
206
|
+
const base = {
|
|
207
|
+
error: e.code,
|
|
208
|
+
message: e.message,
|
|
209
|
+
received: d.received,
|
|
210
|
+
field: d.field,
|
|
211
|
+
...(typeof d.http_status === 'number' ? { http_status: d.http_status } : {}),
|
|
212
|
+
};
|
|
213
|
+
return result === undefined
|
|
214
|
+
? { ...base, next }
|
|
215
|
+
: { ...base, primaryCompleted: true, result, next };
|
|
216
|
+
}
|
|
217
|
+
/** Detach completed results from getters/prototypes and keep every error sink
|
|
218
|
+
* JSON-safe. Tagged values preserve a truthful representation when a trusted
|
|
219
|
+
* primary violates its declared JSON result contract. */
|
|
220
|
+
function jsonSafeResult(result) {
|
|
221
|
+
const seen = new WeakSet();
|
|
222
|
+
try {
|
|
223
|
+
const encoded = JSON.stringify(result, (_key, value) => {
|
|
224
|
+
if (typeof value === 'bigint')
|
|
225
|
+
return { $crtrType: 'bigint', value: value.toString() };
|
|
226
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
227
|
+
return { $crtrType: 'number', value: String(value) };
|
|
228
|
+
if (typeof value === 'undefined' || typeof value === 'function' || typeof value === 'symbol') {
|
|
229
|
+
return { $crtrType: typeof value };
|
|
230
|
+
}
|
|
231
|
+
if (value !== null && typeof value === 'object') {
|
|
232
|
+
if (seen.has(value))
|
|
233
|
+
return { $crtrType: 'circular' };
|
|
234
|
+
seen.add(value);
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
});
|
|
238
|
+
if (encoded !== undefined) {
|
|
239
|
+
const decoded = JSON.parse(encoded);
|
|
240
|
+
if (decoded !== null && typeof decoded === 'object' && !Array.isArray(decoded)) {
|
|
241
|
+
return decoded;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch { /* a throwing getter/toJSON must not suppress the partial failure */ }
|
|
246
|
+
return {
|
|
247
|
+
$crtrType: 'unavailable',
|
|
248
|
+
message: 'The completed primary result could not be represented as JSON.',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// ApiError → clean CLI error — the ONE shared translation (plan C-5)
|
|
252
|
+
//
|
|
253
|
+
// A daemon `ApiError` is NOT a `CrtrError`, so left unhandled it falls through
|
|
254
|
+
// `handle()` as an "internal crtr bug" — the wrong UX for an operational
|
|
255
|
+
// failure. This is the single place that maps it back to the clean, structured
|
|
256
|
+
// error the pre-API leaves surfaced, so every re-plumbed verb shares one
|
|
257
|
+
// contract instead of re-implementing the mapping per command. io.ts importing
|
|
258
|
+
// `ApiError` from `src/api` is canvas-clean (src/api is Node-built-ins only).
|
|
259
|
+
/** Map a daemon HTTP status to the stable CLI error code the pre-API leaves
|
|
260
|
+
* used, so agent-facing error contracts survive the re-plumb. Used only as the
|
|
261
|
+
* fallback when the server did NOT round-trip a full structured payload. */
|
|
262
|
+
const STATUS_TO_CLI_CODE = {
|
|
263
|
+
400: 'invalid_request',
|
|
264
|
+
404: 'not_found',
|
|
265
|
+
409: 'conflict',
|
|
266
|
+
422: 'unprocessable',
|
|
267
|
+
};
|
|
268
|
+
/** Translate an `ApiError` into the clean CLI error a leaf should surface.
|
|
269
|
+
* Three tiers, in priority order:
|
|
270
|
+
* 1. Daemon-down (`daemon_unavailable`) → a `network`-class `CrtrError`
|
|
271
|
+
* so the exit code and next-hint match the operational failure it is.
|
|
272
|
+
* 2. A server-thrown `InputError` round-trips its FULL structured payload
|
|
273
|
+
* through `toErrorBody` into `ApiError.details` (`{error,message,next,…}`) —
|
|
274
|
+
* reconstruct it verbatim so a gate/guard error (revive reopen-gate, push
|
|
275
|
+
* worktree/finalization guard, …) surfaces byte-for-byte as the leaf raised
|
|
276
|
+
* it before the re-plumb, preserving its SPECIFIC code + next.
|
|
277
|
+
* 3. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
|
|
278
|
+
* and a caller-supplied-or-default `next`. */
|
|
279
|
+
export function apiErrorToCliError(err, next) {
|
|
280
|
+
// A handover is NOT a down daemon: the successor is already up (the client
|
|
281
|
+
// waited for its /healthz), so telling anyone to start one is wrong — the
|
|
282
|
+
// only thing left is to re-issue the call.
|
|
283
|
+
if (err.code === 'daemon_restarting') {
|
|
284
|
+
return new CrtrError('daemon_restarting', err.message, ExitCode.NETWORK, {
|
|
285
|
+
next: 'crtrd is now on the new generation — retry the command.',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (err.code === 'layout_migration_required') {
|
|
289
|
+
return new CrtrError('layout_migration_required', err.message, ExitCode.NETWORK, {
|
|
290
|
+
next: 'Run `crtr sys migrate` while the runtime is offline, then restart it.',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (err.code === 'daemon_unavailable') {
|
|
294
|
+
return new CrtrError('daemon_unavailable', err.message, ExitCode.NETWORK, {
|
|
295
|
+
next: 'Start it with `crtr sys daemon start`, then retry.',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// A responding daemon can relay an unavailable provider or storage service.
|
|
299
|
+
// HTTP 503 alone does not mean that crtrd is down.
|
|
300
|
+
if (err.status === 503) {
|
|
301
|
+
return new CrtrError(err.code, err.message, ExitCode.NETWORK, {
|
|
302
|
+
next: next ?? 'Retry the command; if it persists, inspect the unavailable service named in the error.',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const d = err.details;
|
|
306
|
+
// A `/v1` request validator reports each bad field in `details.violations`;
|
|
307
|
+
// the message alone only says the request was invalid.
|
|
308
|
+
const violations = Array.isArray(d?.violations)
|
|
309
|
+
? d.violations
|
|
310
|
+
.map((v) => `${String(v.field)} ${String(v.problem)}${v.received !== undefined ? ` (received ${JSON.stringify(v.received)})` : ''}`)
|
|
311
|
+
: [];
|
|
312
|
+
const payload = d && typeof d.error === 'string' && typeof d.message === 'string' && typeof d.next === 'string'
|
|
313
|
+
? d
|
|
314
|
+
: {
|
|
315
|
+
error: STATUS_TO_CLI_CODE[err.status] ?? err.code,
|
|
316
|
+
message: violations.length > 0 ? `${err.message}: ${violations.join('; ')}` : err.message,
|
|
317
|
+
next: next ??
|
|
318
|
+
(err.status >= 500
|
|
319
|
+
? 'This is a crtr bug, not a bad call. Retry; if it persists, report it (the daemon log holds the full cause).'
|
|
320
|
+
: err.status === 404
|
|
321
|
+
? 'List nodes with `crtr node inspect list`.'
|
|
322
|
+
: 'Inspect the error and adjust the call. See -h for the schema.'),
|
|
323
|
+
};
|
|
324
|
+
// Preserve the exit-code class the server leaf carried instead of collapsing
|
|
325
|
+
// every non-503 to USAGE(2): a not-found (404 / `not_found`) surfaces exit 3
|
|
326
|
+
// and an ambiguous match exit 4, matching the codes the local-only leaves
|
|
327
|
+
// (memory/pkg/sys) still emit so the taxonomy stays uniform across the CLI.
|
|
328
|
+
return new InputError(payload, exitForCliCode(payload.error, err.status));
|
|
329
|
+
}
|
|
330
|
+
/** The `ExitCode` class a reconstructed API error should carry, keyed off the
|
|
331
|
+
* round-tripped CLI code (and the 404 status as the not-found signal — the
|
|
332
|
+
* server maps NOT_FOUND's exit class to status 404, but folds AMBIGUOUS and
|
|
333
|
+
* USAGE both onto 400, so `ambiguous` is recoverable only from the code). */
|
|
334
|
+
function exitForCliCode(code, status) {
|
|
335
|
+
if (code === 'not_found' || status === 404)
|
|
336
|
+
return ExitCode.NOT_FOUND;
|
|
337
|
+
if (code === 'ambiguous')
|
|
338
|
+
return ExitCode.AMBIGUOUS;
|
|
339
|
+
return ExitCode.USAGE;
|
|
340
|
+
}
|
|
341
|
+
/** Terminal error handler. Command-level failures (bad input, not-found,
|
|
342
|
+
* ambiguous) surface as the JSON response on stdout so the caller parses one
|
|
343
|
+
* contract. A daemon `ApiError` is translated to that same clean contract
|
|
344
|
+
* (`apiErrorToCliError`) as a safety net for any leaf that lets it propagate.
|
|
345
|
+
* Runtime/internal failures go to stderr as `{error:"internal"}` — raw traces
|
|
346
|
+
* never reach the agent. Exits non-zero either way. */
|
|
347
|
+
export function handle(e) {
|
|
348
|
+
if (e instanceof ApiError) {
|
|
349
|
+
handle(apiErrorToCliError(e));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (e instanceof CrtrError) {
|
|
353
|
+
const payload = payloadOf(e);
|
|
354
|
+
if (preview !== undefined)
|
|
355
|
+
publishPreview({ error: payload });
|
|
356
|
+
const out = jsonOutput
|
|
357
|
+
? JSON.stringify(payload, null, 2)
|
|
358
|
+
: renderError(payload);
|
|
359
|
+
process.stdout.write(out + '\n');
|
|
360
|
+
process.exitCode = e.exitCode;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const err = e;
|
|
364
|
+
const message = err !== null && err !== undefined && typeof err.message === 'string'
|
|
365
|
+
? err.message
|
|
366
|
+
: String(e);
|
|
367
|
+
process.stderr.write(JSON.stringify({
|
|
368
|
+
error: 'internal',
|
|
369
|
+
message,
|
|
370
|
+
next: 'This is a crtr bug, not a bad call. Retry; if it persists, report it.',
|
|
371
|
+
}, null, 2) + '\n');
|
|
372
|
+
process.exitCode = ExitCode.GENERAL;
|
|
373
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export declare const ATTACH_CONTROL_BINDINGS: readonly [{
|
|
2
|
+
readonly menuId: "crtr.tmux.menu.attach.detach";
|
|
3
|
+
readonly actionId: "crtr.attach.detach";
|
|
4
|
+
}, {
|
|
5
|
+
readonly menuId: "crtr.tmux.menu.attach.clear-or-detach";
|
|
6
|
+
readonly actionId: "crtr.attach.clear-or-detach";
|
|
7
|
+
}, {
|
|
8
|
+
readonly menuId: "crtr.tmux.menu.attach.graph.toggle";
|
|
9
|
+
readonly actionId: "crtr.attach.graph.toggle";
|
|
10
|
+
}, {
|
|
11
|
+
readonly menuId: "crtr.tmux.menu.attach.help.toggle";
|
|
12
|
+
readonly actionId: "crtr.attach.help.toggle";
|
|
13
|
+
}, {
|
|
14
|
+
readonly menuId: "crtr.tmux.menu.attach.inbox.toggle";
|
|
15
|
+
readonly actionId: "crtr.attach.inbox.toggle";
|
|
16
|
+
}, {
|
|
17
|
+
readonly menuId: "crtr.tmux.menu.attach.inbox-strip";
|
|
18
|
+
readonly actionId: "crtr.attach.inbox-strip";
|
|
19
|
+
}, {
|
|
20
|
+
readonly menuId: "crtr.tmux.menu.attach.model-ladder.next";
|
|
21
|
+
readonly actionId: "crtr.attach.model-ladder.next";
|
|
22
|
+
}, {
|
|
23
|
+
readonly menuId: "crtr.tmux.menu.attach.model-ladder.previous";
|
|
24
|
+
readonly actionId: "crtr.attach.model-ladder.previous";
|
|
25
|
+
}, {
|
|
26
|
+
readonly menuId: "crtr.tmux.menu.attach.command.inspect";
|
|
27
|
+
readonly actionId: "crtr.attach.command.inspect";
|
|
28
|
+
}, {
|
|
29
|
+
readonly menuId: "crtr.tmux.menu.attach.file-review";
|
|
30
|
+
readonly actionId: "crtr.attach.file-review";
|
|
31
|
+
}, {
|
|
32
|
+
readonly menuId: "crtr.tmux.menu.attach.profile-files";
|
|
33
|
+
readonly actionId: "crtr.attach.profile-files";
|
|
34
|
+
}, {
|
|
35
|
+
readonly menuId: "crtr.tmux.menu.attach.search";
|
|
36
|
+
readonly actionId: "crtr.attach.search";
|
|
37
|
+
}, {
|
|
38
|
+
readonly menuId: "crtr.tmux.menu.attach.whip";
|
|
39
|
+
readonly actionId: "crtr.attach.whip";
|
|
40
|
+
}];
|
|
41
|
+
export type AttachBindingId = typeof ATTACH_CONTROL_BINDINGS[number]['actionId'];
|
|
42
|
+
export type AttachMenuBindingId = typeof ATTACH_CONTROL_BINDINGS[number]['menuId'];
|
|
43
|
+
export interface ExtractedAttachControlInput {
|
|
44
|
+
readonly actionId: AttachBindingId;
|
|
45
|
+
readonly remaining: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function decodeAttachControlInput(data: string): AttachBindingId | undefined;
|
|
48
|
+
export declare function encodeAttachControlInput(actionId: AttachBindingId): string;
|
|
49
|
+
export declare function extractAttachControlInput(data: string): ExtractedAttachControlInput | undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const ATTACH_CONTROL_BINDINGS = [
|
|
2
|
+
{ menuId: 'crtr.tmux.menu.attach.detach', actionId: 'crtr.attach.detach' },
|
|
3
|
+
{ menuId: 'crtr.tmux.menu.attach.clear-or-detach', actionId: 'crtr.attach.clear-or-detach' },
|
|
4
|
+
{ menuId: 'crtr.tmux.menu.attach.graph.toggle', actionId: 'crtr.attach.graph.toggle' },
|
|
5
|
+
{ menuId: 'crtr.tmux.menu.attach.help.toggle', actionId: 'crtr.attach.help.toggle' },
|
|
6
|
+
{ menuId: 'crtr.tmux.menu.attach.inbox.toggle', actionId: 'crtr.attach.inbox.toggle' },
|
|
7
|
+
{ menuId: 'crtr.tmux.menu.attach.inbox-strip', actionId: 'crtr.attach.inbox-strip' },
|
|
8
|
+
{ menuId: 'crtr.tmux.menu.attach.model-ladder.next', actionId: 'crtr.attach.model-ladder.next' },
|
|
9
|
+
{ menuId: 'crtr.tmux.menu.attach.model-ladder.previous', actionId: 'crtr.attach.model-ladder.previous' },
|
|
10
|
+
{ menuId: 'crtr.tmux.menu.attach.command.inspect', actionId: 'crtr.attach.command.inspect' },
|
|
11
|
+
{ menuId: 'crtr.tmux.menu.attach.file-review', actionId: 'crtr.attach.file-review' },
|
|
12
|
+
{ menuId: 'crtr.tmux.menu.attach.profile-files', actionId: 'crtr.attach.profile-files' },
|
|
13
|
+
{ menuId: 'crtr.tmux.menu.attach.search', actionId: 'crtr.attach.search' },
|
|
14
|
+
{ menuId: 'crtr.tmux.menu.attach.whip', actionId: 'crtr.attach.whip' },
|
|
15
|
+
];
|
|
16
|
+
const ATTACH_CONTROL_PREFIX = '\x1b_crtr;';
|
|
17
|
+
const ATTACH_CONTROL_SUFFIX = '\x1b\\';
|
|
18
|
+
const ATTACH_BINDING_IDS = new Set(ATTACH_CONTROL_BINDINGS.map(({ actionId }) => actionId));
|
|
19
|
+
export function decodeAttachControlInput(data) {
|
|
20
|
+
if (!data.startsWith(ATTACH_CONTROL_PREFIX) || !data.endsWith(ATTACH_CONTROL_SUFFIX))
|
|
21
|
+
return undefined;
|
|
22
|
+
const actionId = data.slice(ATTACH_CONTROL_PREFIX.length, -ATTACH_CONTROL_SUFFIX.length);
|
|
23
|
+
return ATTACH_BINDING_IDS.has(actionId) ? actionId : undefined;
|
|
24
|
+
}
|
|
25
|
+
export function encodeAttachControlInput(actionId) {
|
|
26
|
+
return `${ATTACH_CONTROL_PREFIX}${actionId}${ATTACH_CONTROL_SUFFIX}`;
|
|
27
|
+
}
|
|
28
|
+
export function extractAttachControlInput(data) {
|
|
29
|
+
let match;
|
|
30
|
+
for (const { actionId } of ATTACH_CONTROL_BINDINGS) {
|
|
31
|
+
const encoded = encodeAttachControlInput(actionId);
|
|
32
|
+
const index = data.indexOf(encoded);
|
|
33
|
+
if (index >= 0 && (match === undefined || index < match.index))
|
|
34
|
+
match = { actionId, encoded, index };
|
|
35
|
+
}
|
|
36
|
+
if (match === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
return {
|
|
39
|
+
actionId: match.actionId,
|
|
40
|
+
remaining: data.slice(0, match.index) + data.slice(match.index + match.encoded.length),
|
|
41
|
+
};
|
|
42
|
+
}
|