@north-light/crouter 0.3.163 → 0.3.165
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/build-root.d.ts +6 -6
- package/dist/build-root.js +12 -13
- package/dist/builtin-memory/00-runtime-base.md +3 -4
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
- package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
- package/dist/builtin-memory/advisor/council.md +40 -0
- package/dist/builtin-memory/design.md +8 -11
- package/dist/builtin-memory/development.md +6 -9
- package/dist/builtin-memory/internal/INDEX.md +5 -5
- package/dist/builtin-memory/internal/agent-shaping.md +5 -7
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
- package/dist/builtin-memory/internal/marketplaces.md +13 -14
- package/dist/builtin-memory/internal/memory-loading.md +45 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
- package/dist/builtin-memory/internal/plugins.md +61 -45
- package/dist/builtin-memory/planning.md +4 -7
- package/dist/builtin-memory/spec.md +9 -12
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
- package/dist/cli.js +1 -1
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
- package/dist/clients/attach/__tests__/context-message.test.js +31 -20
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +1 -1
- package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
- package/dist/clients/attach/__tests__/edit-diff.test.js +21 -20
- package/dist/clients/attach/render/chat-view.d.ts +20 -20
- package/dist/clients/attach/render/chat-view.js +88 -89
- package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
- package/dist/clients/attach/render/condensed-history.js +60 -0
- package/dist/clients/attach/render/context-message.js +9 -14
- package/dist/clients/attach/render/crtr-output.d.ts +2 -1
- package/dist/clients/attach/render/crtr-output.js +4 -4
- package/dist/clients/attach/render/edit-diff.d.ts +21 -6
- package/dist/clients/attach/render/edit-diff.js +90 -91
- package/dist/clients/attach/render/tool-calls.d.ts +14 -18
- package/dist/clients/attach/render/tool-calls.js +66 -41
- package/dist/clients/attach/session/bindings.d.ts +5 -3
- package/dist/clients/attach/session/bindings.js +7 -13
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +36 -37
- package/dist/clients/attach/session/profile-files.d.ts +8 -0
- package/dist/clients/attach/session/profile-files.js +157 -0
- package/dist/clients/attach/viewer.js +530 -528
- package/dist/commands/memory/lint.js +1 -1
- package/dist/commands/memory/write.js +1 -1
- package/dist/commands/node.js +2 -2
- package/dist/commands/pkg/plugin-inspect.js +6 -7
- package/dist/commands/pkg/plugin-manage.d.ts +1 -1
- package/dist/commands/pkg/plugin-manage.js +131 -19
- package/dist/commands/pkg/plugin.js +2 -2
- package/dist/commands/pkg.js +6 -11
- package/dist/commands/profile/env.js +3 -3
- package/dist/commands/sys/config.js +17 -76
- package/dist/commands/sys/doctor.js +5 -91
- package/dist/commands/sys/setup-wizard.d.ts +9 -11
- package/dist/commands/sys/setup-wizard.js +47 -81
- package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
- package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
- package/dist/core/__tests__/command-plugins.test.js +36 -16
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -1
- package/dist/core/command-manifests/manifest.d.ts +24 -0
- package/dist/core/{configured-clis → command-manifests}/manifest.js +21 -25
- package/dist/core/command-manifests/registry.d.ts +1 -2
- package/dist/core/command-manifests/registry.js +2 -2
- package/dist/core/command-manifests/schema.d.ts +11 -13
- package/dist/core/command-manifests/schema.js +53 -192
- package/dist/core/command-plugins/compose.d.ts +0 -6
- package/dist/core/command-plugins/compose.js +28 -75
- package/dist/core/command-plugins/discovery.d.ts +25 -58
- package/dist/core/command-plugins/discovery.js +152 -259
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +48 -0
- package/dist/core/command-plugins/store.d.ts +16 -0
- package/dist/core/command-plugins/store.js +64 -0
- package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
- package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
- package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
- package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
- package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
- package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
- package/dist/core/command.d.ts +8 -9
- package/dist/core/command.js +6 -9
- package/dist/core/config.js +6 -10
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/io.d.ts +1 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
- package/dist/core/keybindings/attach-control.d.ts +37 -0
- package/dist/core/keybindings/attach-control.js +38 -0
- package/dist/core/keybindings/catalog.d.ts +5 -4
- package/dist/core/keybindings/catalog.js +16 -8
- package/dist/core/keybindings/index.d.ts +1 -0
- package/dist/core/keybindings/index.js +1 -0
- package/dist/core/keybindings/types.d.ts +1 -1
- package/dist/core/preview-registry.js +41 -74
- package/dist/core/runtime/bearings.d.ts +2 -5
- package/dist/core/runtime/bearings.js +2 -5
- package/dist/core/runtime/front-door.d.ts +1 -1
- package/dist/core/runtime/front-door.js +2 -2
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +4 -3
- package/dist/core/runtime/situational-context.d.ts +1 -1
- package/dist/core/runtime/situational-context.js +1 -1
- package/dist/core/runtime/spawn.js +9 -9
- package/dist/core/runtime/tmux.js +29 -2
- package/dist/core/scope.d.ts +0 -5
- package/dist/core/scope.js +0 -10
- package/dist/core/user-settings.d.ts +193 -0
- package/dist/core/user-settings.js +252 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -2
- package/dist/pi-extensions/canvas-tool-guide.js +4 -1
- package/dist/shared/generated-context.d.ts +34 -0
- package/dist/shared/generated-context.js +98 -0
- package/dist/types.d.ts +23 -16
- package/dist/types.js +3 -14
- package/dist/web-client/assets/{index-NIuSCOHM.js → index-BMTOGuOZ.js} +19 -19
- package/dist/web-client/assets/index-DvA6Zw-R.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/public-api.md +1 -0
- package/package.json +2 -2
- package/runtime.lock.json +6 -6
- package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
- package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
- package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
- package/dist/builtin-memory/internal/workflow-codification.md +0 -82
- package/dist/builtin-memory/product.md +0 -80
- package/dist/clients/attach/render/frozen-history.js +0 -100
- package/dist/commands/pkg/cli-inspect.d.ts +0 -17
- package/dist/commands/pkg/cli-inspect.js +0 -190
- package/dist/commands/pkg/cli-manage.d.ts +0 -3
- package/dist/commands/pkg/cli-manage.js +0 -206
- package/dist/commands/pkg/cli.d.ts +0 -1
- package/dist/commands/pkg/cli.js +0 -14
- package/dist/core/configured-clis/cache.d.ts +0 -16
- package/dist/core/configured-clis/cache.js +0 -57
- package/dist/core/configured-clis/compose.d.ts +0 -14
- package/dist/core/configured-clis/compose.js +0 -60
- package/dist/core/configured-clis/discovery.d.ts +0 -47
- package/dist/core/configured-clis/discovery.js +0 -173
- package/dist/core/configured-clis/manifest.d.ts +0 -24
- package/dist/core/configured-clis/registration.d.ts +0 -40
- package/dist/core/configured-clis/registration.js +0 -201
- package/dist/web-client/assets/index-CqLKj8Xu.css +0 -2
|
@@ -5,24 +5,18 @@
|
|
|
5
5
|
// tmux installer binds from (`core/keybindings`), never from ad-hoc key
|
|
6
6
|
// literals, so a user's rebind is honored by the viewer and tmux alike.
|
|
7
7
|
import { matchesKey } from '@earendil-works/pi-tui';
|
|
8
|
-
import { formatBinding, matchesPiTuiInput, } from '../../../core/keybindings/index.js';
|
|
8
|
+
import { ATTACH_CONTROL_BINDINGS, formatBinding, matchesPiTuiInput, } from '../../../core/keybindings/index.js';
|
|
9
9
|
export { inboxOpenHint } from '../../../core/keybindings/index.js';
|
|
10
10
|
export function matchesAttachBinding(bindings, id, data) {
|
|
11
11
|
return matchesPiTuiInput(bindings, id, data, (input, gesture) => matchesKey(input, gesture));
|
|
12
12
|
}
|
|
13
|
+
export function dispatchAttachAction(id, actions) {
|
|
14
|
+
actions[id]();
|
|
15
|
+
}
|
|
13
16
|
export function dispatchAttachBinding(bindings, data, actions) {
|
|
14
|
-
for (const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'crtr.mode.cycle',
|
|
18
|
-
'crtr.attach.graph.toggle',
|
|
19
|
-
'crtr.attach.model-ladder.next',
|
|
20
|
-
'crtr.attach.model-ladder.previous',
|
|
21
|
-
'crtr.attach.command.inspect',
|
|
22
|
-
'crtr.attach.file-review',
|
|
23
|
-
]) {
|
|
24
|
-
if (matchesAttachBinding(bindings, id, data)) {
|
|
25
|
-
actions[id]();
|
|
17
|
+
for (const { actionId } of ATTACH_CONTROL_BINDINGS) {
|
|
18
|
+
if (matchesAttachBinding(bindings, actionId, data)) {
|
|
19
|
+
dispatchAttachAction(actionId, actions);
|
|
26
20
|
return true;
|
|
27
21
|
}
|
|
28
22
|
}
|
|
@@ -17,6 +17,8 @@ export interface KeyHooks {
|
|
|
17
17
|
inspectLoadedCommand: () => void;
|
|
18
18
|
/** The file-review chord — open the transcript-file picker + review surface. */
|
|
19
19
|
openFileReview: () => void;
|
|
20
|
+
/** Search the current node's local profile project files. */
|
|
21
|
+
openProfileFiles: () => void;
|
|
20
22
|
}
|
|
21
23
|
/** Install the listener; returns the remover, which teardown MUST call. */
|
|
22
24
|
export declare function installGlobalKeys(ctx: Pick<AttachSession, 'tui' | 'editor'>, hooks: KeyHooks): () => void;
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// A TUI input listener fires BEFORE the focused component and before any dialog
|
|
4
4
|
// overlay, so the configured attach actions work regardless of what has focus.
|
|
5
5
|
// That privilege is also the hazard: anything consumed here never reaches the
|
|
6
|
-
// editor. Only
|
|
7
|
-
// roster while it holds the cursor, and a configured crtr binding
|
|
8
|
-
// returns
|
|
9
|
-
import {
|
|
6
|
+
// editor. Only terminal focus reports, a private crouter control input, the
|
|
7
|
+
// roster while it holds the cursor, and a configured crtr binding may claim a
|
|
8
|
+
// chunk; each returns ordinary editor input untouched when it does not apply.
|
|
9
|
+
import { extractAttachControlInput } from '../../../core/keybindings/index.js';
|
|
10
|
+
import { dispatchAttachAction, dispatchAttachBinding } from './bindings.js';
|
|
10
11
|
import { FOCUS_IN, FOCUS_OUT } from './focus-events.js';
|
|
11
12
|
/** Press the clear-or-detach binding twice inside this window to detach. */
|
|
12
13
|
const CLEAR_OR_DETACH_MS = 1000;
|
|
@@ -14,47 +15,45 @@ const CLEAR_OR_DETACH_MS = 1000;
|
|
|
14
15
|
export function installGlobalKeys(ctx, hooks) {
|
|
15
16
|
const { tui, editor } = ctx;
|
|
16
17
|
let lastClearOrDetachAt = 0;
|
|
18
|
+
const actions = {
|
|
19
|
+
'crtr.attach.detach': hooks.detach,
|
|
20
|
+
'crtr.attach.clear-or-detach': () => {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (now - lastClearOrDetachAt < CLEAR_OR_DETACH_MS) {
|
|
23
|
+
lastClearOrDetachAt = 0;
|
|
24
|
+
hooks.detach();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
lastClearOrDetachAt = now;
|
|
28
|
+
editor.setText('');
|
|
29
|
+
hooks.setNotice(`Editor cleared — press ${hooks.detachHint()} again to detach`);
|
|
30
|
+
tui.requestRender();
|
|
31
|
+
},
|
|
32
|
+
'crtr.mode.cycle': hooks.cycleMode,
|
|
33
|
+
'crtr.attach.graph.toggle': hooks.toggleGraph,
|
|
34
|
+
// A plain broker frame, so this works locally and remotely alike; the broker gates observers on controller role.
|
|
35
|
+
'crtr.attach.model-ladder.next': () => hooks.cycleModelLadder('forward'),
|
|
36
|
+
'crtr.attach.model-ladder.previous': () => hooks.cycleModelLadder('backward'),
|
|
37
|
+
'crtr.attach.command.inspect': hooks.inspectLoadedCommand,
|
|
38
|
+
'crtr.attach.file-review': hooks.openFileReview,
|
|
39
|
+
'crtr.attach.profile-files': hooks.openProfileFiles,
|
|
40
|
+
};
|
|
17
41
|
return tui.addInputListener((data) => {
|
|
18
|
-
// Terminal focus reporting (DECSET 1004, enabled after tui.start; tmux
|
|
19
|
-
// forwards it per-pane with `focus-events on`). Pane focus drives the
|
|
20
|
-
// editor's fake cursor — solid block focused, hollow outline blurred — so a
|
|
21
|
-
// wall of attach panes shows exactly which one keys go to. The events are
|
|
22
|
-
// stripped rather than blanket-consumed in case the chunk coalesced them
|
|
23
|
-
// with real keystrokes.
|
|
42
|
+
// Terminal focus reporting (DECSET 1004, enabled after tui.start; tmux forwards it per-pane with `focus-events on`). Pane focus drives the editor's fake cursor — solid block focused, hollow outline blurred — so a wall of attach panes shows exactly which one keys go to. The events are stripped rather than blanket-consumed in case the chunk coalesced them with real keystrokes.
|
|
24
43
|
if (data.includes(FOCUS_IN) || data.includes(FOCUS_OUT)) {
|
|
25
44
|
hooks.setPaneFocused(data.lastIndexOf(FOCUS_IN) > data.lastIndexOf(FOCUS_OUT));
|
|
26
45
|
tui.requestRender();
|
|
27
46
|
const rest = data.replaceAll(FOCUS_IN, '').replaceAll(FOCUS_OUT, '');
|
|
28
47
|
return rest.length > 0 ? { data: rest } : { consume: true };
|
|
29
48
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
const control = extractAttachControlInput(data);
|
|
50
|
+
if (control !== undefined) {
|
|
51
|
+
dispatchAttachAction(control.actionId, actions);
|
|
52
|
+
return control.remaining.length > 0 ? { data: control.remaining } : { consume: true };
|
|
53
|
+
}
|
|
54
|
+
// Inline subagent scroll+swap: once the editor hands off at its ↓ boundary, ↑/↓ move over the manager/report roster and Enter dives into a node. An inactive roster never intercepts editor input.
|
|
33
55
|
if (hooks.rosterHandleKey(data))
|
|
34
56
|
return { consume: true };
|
|
35
|
-
|
|
36
|
-
'crtr.attach.detach': hooks.detach,
|
|
37
|
-
'crtr.attach.clear-or-detach': () => {
|
|
38
|
-
const now = Date.now();
|
|
39
|
-
if (now - lastClearOrDetachAt < CLEAR_OR_DETACH_MS) {
|
|
40
|
-
lastClearOrDetachAt = 0;
|
|
41
|
-
hooks.detach();
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
lastClearOrDetachAt = now;
|
|
45
|
-
editor.setText('');
|
|
46
|
-
hooks.setNotice(`Editor cleared — press ${hooks.detachHint()} again to detach`);
|
|
47
|
-
tui.requestRender();
|
|
48
|
-
},
|
|
49
|
-
'crtr.mode.cycle': hooks.cycleMode,
|
|
50
|
-
'crtr.attach.graph.toggle': hooks.toggleGraph,
|
|
51
|
-
// A plain broker frame, so this works locally and remotely alike; the
|
|
52
|
-
// broker gates observers on controller role.
|
|
53
|
-
'crtr.attach.model-ladder.next': () => hooks.cycleModelLadder('forward'),
|
|
54
|
-
'crtr.attach.model-ladder.previous': () => hooks.cycleModelLadder('backward'),
|
|
55
|
-
'crtr.attach.command.inspect': hooks.inspectLoadedCommand,
|
|
56
|
-
'crtr.attach.file-review': hooks.openFileReview,
|
|
57
|
-
});
|
|
58
|
-
return dispatched ? { consume: true } : undefined;
|
|
57
|
+
return dispatchAttachBinding(hooks.bindings(), data, actions) ? { consume: true } : undefined;
|
|
59
58
|
});
|
|
60
59
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// session/profile-files.ts — fuzzy file search across a node's local profile.
|
|
2
|
+
//
|
|
3
|
+
// The popup searches the current workspace plus every project named by the
|
|
4
|
+
// node's profile manifest. Ancestor `.crouter/memory/` stores are added with
|
|
5
|
+
// ignore rules disabled, so project memory remains findable even when a parent
|
|
6
|
+
// ignore file hides `.crouter/`. fzf owns selection and nvim owns viewing; the
|
|
7
|
+
// attach viewer only resolves the relevant roots and wires the two popups.
|
|
8
|
+
import { constants, accessSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { delimiter, dirname, join, resolve, sep } from 'node:path';
|
|
11
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
12
|
+
import { loadProfileManifest } from '../../../core/profiles/manifest.js';
|
|
13
|
+
import { shellQuote } from '../../../core/runtime/placement-tmux.js';
|
|
14
|
+
import { surfaceTmuxStyleArgs } from '../../../core/runtime/surface-bg.js';
|
|
15
|
+
function isDirectory(path) {
|
|
16
|
+
try {
|
|
17
|
+
return statSync(path).isDirectory();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function executableCandidates(name, preferred) {
|
|
24
|
+
const candidates = [
|
|
25
|
+
...(preferred === undefined || preferred === null ? [] : [preferred]),
|
|
26
|
+
...(process.env['PATH'] ?? '').split(delimiter).filter(Boolean).map((dir) => join(dir, name)),
|
|
27
|
+
];
|
|
28
|
+
return [...new Set(candidates)];
|
|
29
|
+
}
|
|
30
|
+
/** Return the first PATH candidate that can actually execute. Checking only
|
|
31
|
+
* X_OK is insufficient when a managed tool cache contains a binary for a
|
|
32
|
+
* different host platform. This runs only when the user opens the search. */
|
|
33
|
+
function workingExecutable(name, preferred) {
|
|
34
|
+
for (const candidate of executableCandidates(name, preferred)) {
|
|
35
|
+
try {
|
|
36
|
+
accessSync(candidate, constants.X_OK);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const probe = spawnSync(candidate, ['--version'], { stdio: 'ignore' });
|
|
42
|
+
if (probe.status === 0)
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function profileProjectDirs(profileId) {
|
|
48
|
+
if (profileId === null || profileId === '')
|
|
49
|
+
return [];
|
|
50
|
+
try {
|
|
51
|
+
return loadProfileManifest(profileId).manifest.projects;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function dedupeExistingDirs(paths) {
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const path of paths) {
|
|
61
|
+
const absolute = resolve(path);
|
|
62
|
+
if (seen.has(absolute) || !isDirectory(absolute))
|
|
63
|
+
continue;
|
|
64
|
+
seen.add(absolute);
|
|
65
|
+
out.push(absolute);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/** Keep only the outermost search roots. An outer profile project already
|
|
70
|
+
* covers a cwd or another project nested beneath it, and avoiding the second
|
|
71
|
+
* fd walk keeps every candidate unique without a separate dedupe process. */
|
|
72
|
+
function outermostDirs(paths) {
|
|
73
|
+
return paths.filter((path) => !paths.some((other) => other !== path && path.startsWith(other + sep)));
|
|
74
|
+
}
|
|
75
|
+
/** Every existing `<ancestor>/.crouter/memory/` from each pointer through the
|
|
76
|
+
* filesystem root. A dedicated no-ignore fd pass guarantees these files are
|
|
77
|
+
* present even when the wider project scan respects an ignore rule. */
|
|
78
|
+
function ancestorMemoryDirs(pointers) {
|
|
79
|
+
const found = [];
|
|
80
|
+
for (const pointer of pointers) {
|
|
81
|
+
let cursor = resolve(pointer);
|
|
82
|
+
for (;;) {
|
|
83
|
+
const memory = join(cursor, '.crouter', 'memory');
|
|
84
|
+
if (isDirectory(memory))
|
|
85
|
+
found.push(memory);
|
|
86
|
+
const parent = dirname(cursor);
|
|
87
|
+
if (parent === cursor)
|
|
88
|
+
break;
|
|
89
|
+
cursor = parent;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return dedupeExistingDirs(found);
|
|
93
|
+
}
|
|
94
|
+
function selectionPath(path) {
|
|
95
|
+
try {
|
|
96
|
+
const bytes = readFileSync(path);
|
|
97
|
+
if (bytes.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
const end = bytes[bytes.length - 1] === 0 ? bytes.length - 1 : bytes.length;
|
|
100
|
+
return bytes.subarray(0, end).toString('utf8') || null;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function launchProfileFileSearch(opts) {
|
|
107
|
+
const fd = workingExecutable('fd', opts.fdPath);
|
|
108
|
+
if (fd === null) {
|
|
109
|
+
opts.onNotice('fd is required for profile file search');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const fzf = workingExecutable('fzf');
|
|
113
|
+
if (fzf === null) {
|
|
114
|
+
opts.onNotice('fzf is required for profile file search');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const nvim = workingExecutable('nvim');
|
|
118
|
+
if (nvim === null) {
|
|
119
|
+
opts.onNotice('Neovim is required for profile file search');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const pointers = dedupeExistingDirs([opts.cwd, ...profileProjectDirs(opts.profileId)]);
|
|
123
|
+
if (pointers.length === 0) {
|
|
124
|
+
opts.onNotice('No profile file-search directories exist');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const roots = outermostDirs(pointers);
|
|
128
|
+
const memoryDirs = ancestorMemoryDirs(pointers);
|
|
129
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'crtr-profile-files-'));
|
|
130
|
+
const selected = join(tempDir, 'selection');
|
|
131
|
+
const scans = [
|
|
132
|
+
...roots.map((root) => `${shellQuote(fd)} --type f --hidden --exclude .git --exclude ${shellQuote('**/.crouter/memory/**')} --print0 . ${shellQuote(root)}`),
|
|
133
|
+
...memoryDirs.map((memory) => `${shellQuote(fd)} --type f --hidden --exclude .git --no-ignore --print0 . ${shellQuote(memory)}`),
|
|
134
|
+
];
|
|
135
|
+
const command = `{ ${scans.map((scan) => `${scan} &`).join(' ')} wait; } | ${shellQuote(fzf)} --read0 --print0 --preview ${shellQuote('head -100 -- {}')} > ${shellQuote(selected)}`;
|
|
136
|
+
try {
|
|
137
|
+
execFile('tmux', ['display-popup', '-E', '-T', ' profile files ', '-w', '80%', '-h', '60%', ...surfaceTmuxStyleArgs(), command], () => {
|
|
138
|
+
const file = selectionPath(selected);
|
|
139
|
+
if (file === null || !existsSync(file)) {
|
|
140
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const open = `exec ${shellQuote(nvim)} -- ${shellQuote(file)}`;
|
|
144
|
+
try {
|
|
145
|
+
execFile('tmux', ['display-popup', '-E', '-T', ' profile file ', '-w', '90%', '-h', '90%', ...surfaceTmuxStyleArgs(), open], () => rmSync(tempDir, { recursive: true, force: true }));
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
149
|
+
opts.onNotice(`Could not open profile file: ${err instanceof Error ? err.message : String(err)}`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
155
|
+
opts.onNotice(`Could not open profile file search: ${err instanceof Error ? err.message : String(err)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|