@crouton-kit/crouter 0.3.18 → 0.3.19
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.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +3 -2
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +2 -0
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +607 -61
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/loader.d.ts +7 -0
- package/dist/core/personas/loader.js +36 -5
- package/dist/core/personas/resolve.js +13 -3
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +32 -14
- package/dist/core/runtime/placement.js +206 -52
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +5 -0
- package/dist/core/spawn.js +20 -2
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +87 -23
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -241,7 +241,7 @@ const marketUpdate = defineLeaf({
|
|
|
241
241
|
const marketInstall = defineLeaf({
|
|
242
242
|
name: 'install',
|
|
243
243
|
description: 'install a plugin from a marketplace',
|
|
244
|
-
whenToUse: 'installing a plugin by name from
|
|
244
|
+
whenToUse: 'installing a plugin by name from a marketplace you have already registered',
|
|
245
245
|
help: {
|
|
246
246
|
name: 'pkg market manage install',
|
|
247
247
|
summary: 'install a plugin from an added marketplace by plugin name',
|
package/dist/commands/push.js
CHANGED
|
@@ -59,7 +59,7 @@ function makeTierLeaf(tier) {
|
|
|
59
59
|
effects: [
|
|
60
60
|
'Writes nodes/<nodeId>/reports/<ts>-<tier>.md.',
|
|
61
61
|
'Appends one inbox pointer per subscriber.',
|
|
62
|
-
...(tier === 'final' ? ['Marks the node done (status + intent); its
|
|
62
|
+
...(tier === 'final' ? ['Marks the node done (status + intent); its engine shuts down on next stop.'] : []),
|
|
63
63
|
],
|
|
64
64
|
},
|
|
65
65
|
run: async (input) => {
|
|
@@ -86,8 +86,8 @@ function makeTierLeaf(tier) {
|
|
|
86
86
|
}
|
|
87
87
|
// A RESIDENT node with no subscribers is human-driven and has no one to
|
|
88
88
|
// submit a canonical result to: `push final` fans to subscribers, and a
|
|
89
|
-
// resident root conversation has none. Finishing it would
|
|
90
|
-
// mid-conversation. Block that unless the user confirms (--force). Keyed on
|
|
89
|
+
// resident root conversation has none. Finishing it would shut its
|
|
90
|
+
// engine down mid-conversation. Block that unless the user confirms (--force). Keyed on
|
|
91
91
|
// lifecycle, NOT subscriber-count alone: a TERMINAL node with no subscribers
|
|
92
92
|
// was deliberately terminalized to owe a final — it self-completes here
|
|
93
93
|
// (records the result, reaps) rather than being blocked for lack of a recipient.
|
|
@@ -96,7 +96,7 @@ function makeTierLeaf(tier) {
|
|
|
96
96
|
if (noRecipient) {
|
|
97
97
|
throw new InputError({
|
|
98
98
|
error: 'no_final_recipient',
|
|
99
|
-
message: 'This node is working directly with the user — it has no manager to submit a final result to. `push final` would
|
|
99
|
+
message: 'This node is working directly with the user — it has no manager to submit a final result to. `push final` would end the conversation mid-stream.',
|
|
100
100
|
next: 'You almost certainly do NOT need to finish here — just keep working with the user. If the user has explicitly asked you to finish and close this node, confirm with them first, then rerun with `crtr push final --force "<result>"`.',
|
|
101
101
|
});
|
|
102
102
|
}
|
|
@@ -108,7 +108,7 @@ function makeTierLeaf(tier) {
|
|
|
108
108
|
render: (r) => {
|
|
109
109
|
const n = Array.isArray(r['delivered_to']) ? r['delivered_to'].length : 0;
|
|
110
110
|
const line = tier === 'final'
|
|
111
|
-
? 'Result recorded — node finished; its
|
|
111
|
+
? 'Result recorded — node finished; its engine shuts down on next stop. Nothing more to do here: STOP your turn immediately. Reply with just "Done." and nothing else.'
|
|
112
112
|
: tier === 'urgent'
|
|
113
113
|
? `Urgent report fanned to ${n} subscriber(s) — they are force-woken.`
|
|
114
114
|
: `Progress report fanned to ${n} subscriber(s).`;
|
|
@@ -122,7 +122,7 @@ function makeTierLeaf(tier) {
|
|
|
122
122
|
const feedReadLeaf = defineLeaf({
|
|
123
123
|
name: 'read',
|
|
124
124
|
description: 'drain unread pointers into a digest',
|
|
125
|
-
whenToUse: 'you want to PROACTIVELY poll what the nodes you subscribe to — your children and anyone you follow — have reported before the watcher wakes you, draining the unread pointers in your inbox into one coalesced digest. NOTE: when a subscriber push wakes you, that wake message already IS this digest (the watcher drains your inbox to wake you), so don\'t re-run feed read to "open" it — dereference the refs in the digest you already have. Reach for
|
|
125
|
+
whenToUse: 'you want to PROACTIVELY poll what the nodes you subscribe to — your children and anyone you follow — have reported before the watcher wakes you, draining the unread pointers in your inbox into one coalesced digest. NOTE: when a subscriber push wakes you, that wake message already IS this digest (the watcher drains your inbox to wake you), so don\'t re-run feed read to "open" it — dereference the refs in the digest you already have. Reach for it to poll before the next wake, to inspect another node\'s inbox, or to re-read the whole history after the cursor has advanced.',
|
|
126
126
|
help: {
|
|
127
127
|
name: 'feed read',
|
|
128
128
|
summary: 'drain unread inbox pointers for the caller (or a named node) into a compact digest',
|
package/dist/commands/revive.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getNode } from '../core/canvas/index.js';
|
|
|
13
13
|
export const reviveLeaf = defineLeaf({
|
|
14
14
|
name: 'revive',
|
|
15
15
|
description: 'reopen a window for a done/idle/dead/canceled node',
|
|
16
|
-
whenToUse: 'you want to bring a dormant node back yourself — reopen a window for one that is done, idle, dead, or canceled: resume a node you closed with `node close`, reopen a finished worker for a follow-up, or restart a crashed one now instead of waiting.
|
|
16
|
+
whenToUse: 'you want to bring a dormant node back yourself — reopen a window for one that is done, idle, dead, or canceled: resume a node you closed with `node close`, reopen a finished worker for a follow-up, or restart a crashed one now instead of waiting. It resumes the saved conversation by default, or can restart the node clean. You rarely need this for crashes — the daemon auto-revives those; reach for it to bring a node back on demand, or to revive a canceled node the daemon will never touch on its own',
|
|
17
17
|
help: {
|
|
18
18
|
name: 'canvas revive',
|
|
19
19
|
summary: 'open a fresh tmux window for a node, optionally resuming its saved pi conversation',
|
|
@@ -13,7 +13,7 @@ import { VALID_TYPES, resolveWriteScope } from './shared.js';
|
|
|
13
13
|
export const authorGuide = defineLeaf({
|
|
14
14
|
name: 'guide',
|
|
15
15
|
description: 'load authoring workflow + skeleton for a type',
|
|
16
|
-
whenToUse: 'REQUIRED reading before you author a new skill OR edit an existing one — it carries the SKILL.md format, the description-drives-discovery rule (when-to-use lives in the frontmatter description, never the body), the voice constraints, and the per-type workflow.
|
|
16
|
+
whenToUse: 'REQUIRED reading before you author a new skill OR edit an existing one — it carries the SKILL.md format, the description-drives-discovery rule (when-to-use lives in the frontmatter description, never the body), the voice constraints, and the per-type workflow. Editing an existing skill counts: read this first, because the format and voice rules govern every change, not just new files.',
|
|
17
17
|
help: {
|
|
18
18
|
name: 'skill author guide',
|
|
19
19
|
summary: 'load the skill authoring workflow — two stages: omit type to pick one, pass type for its full skeleton',
|
|
@@ -5,15 +5,8 @@ export declare function buildNeighborsSection(skill: Skill): string | null;
|
|
|
5
5
|
export declare function appendNeighbors(skill: Skill, body: string): string;
|
|
6
6
|
export declare function resolveWriteScope(scopeStr: string | undefined): Scope;
|
|
7
7
|
export declare const VALID_TYPES: readonly ["playbook", "primer", "reference", "runbook", "freeform"];
|
|
8
|
-
type CatalogSource = {
|
|
8
|
+
export type CatalogSource = {
|
|
9
9
|
plugin: string;
|
|
10
10
|
roots: string[];
|
|
11
11
|
};
|
|
12
|
-
/** The skill subtree's live state: the loaded-skills catalog as a self-named
|
|
13
|
-
* `<skills count="N">` element. The tag carries the label and the count is an
|
|
14
|
-
* attribute, so the body is the grouped tree alone — no "Loaded skills (N)"
|
|
15
|
-
* header to duplicate. Returns null (block omitted) when discovery fails or
|
|
16
|
-
* nothing is loaded. */
|
|
17
|
-
export declare function buildSkillCatalog(): string | null;
|
|
18
12
|
export declare function renderCatalogSection(label: string, sources: CatalogSource[], descriptions: Map<string, string>, out: string[]): void;
|
|
19
|
-
export {};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { stateBlock } from '../../core/help.js';
|
|
2
1
|
import { usage } from '../../core/errors.js';
|
|
3
2
|
import { SCOPE_SKILL_PLUGIN } from '../../types.js';
|
|
4
|
-
import { listSkillSiblings, listSkillChildren,
|
|
3
|
+
import { listSkillSiblings, listSkillChildren, } from '../../core/resolver.js';
|
|
5
4
|
import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
|
|
6
5
|
// ---------------------------------------------------------------------------
|
|
7
6
|
// Neighbors section (ported from old impl)
|
|
@@ -24,7 +23,7 @@ export function buildNeighborsSection(skill) {
|
|
|
24
23
|
return null;
|
|
25
24
|
const lines = [
|
|
26
25
|
'## Neighbors',
|
|
27
|
-
'*Auto-discovered from filesystem. Run `crtr
|
|
26
|
+
'*Auto-discovered from filesystem. Run `crtr memory read <name>` for full description + body.*',
|
|
28
27
|
'',
|
|
29
28
|
];
|
|
30
29
|
if (siblings.length > 0) {
|
|
@@ -68,58 +67,6 @@ export function resolveWriteScope(scopeStr) {
|
|
|
68
67
|
// ---------------------------------------------------------------------------
|
|
69
68
|
export const VALID_TYPES = ['playbook', 'primer', 'reference', 'runbook', 'freeform'];
|
|
70
69
|
const CATALOG_T = 5;
|
|
71
|
-
/** The skill subtree's live state: the loaded-skills catalog as a self-named
|
|
72
|
-
* `<skills count="N">` element. The tag carries the label and the count is an
|
|
73
|
-
* attribute, so the body is the grouped tree alone — no "Loaded skills (N)"
|
|
74
|
-
* header to duplicate. Returns null (block omitted) when discovery fails or
|
|
75
|
-
* nothing is loaded. */
|
|
76
|
-
export function buildSkillCatalog() {
|
|
77
|
-
let skills;
|
|
78
|
-
try {
|
|
79
|
-
skills = listAllSkills().filter((s) => s.enabled);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
if (skills.length === 0)
|
|
85
|
-
return null;
|
|
86
|
-
const pluginDescriptions = new Map();
|
|
87
|
-
for (const p of listAllPlugins()) {
|
|
88
|
-
if (p.manifest.description)
|
|
89
|
-
pluginDescriptions.set(p.name, p.manifest.description);
|
|
90
|
-
}
|
|
91
|
-
const bySource = new Map();
|
|
92
|
-
for (const s of skills) {
|
|
93
|
-
const key = `${s.scope}\t${s.plugin}`;
|
|
94
|
-
const arr = bySource.get(key);
|
|
95
|
-
if (arr)
|
|
96
|
-
arr.push(s);
|
|
97
|
-
else
|
|
98
|
-
bySource.set(key, [s]);
|
|
99
|
-
}
|
|
100
|
-
const projectSources = [];
|
|
101
|
-
const userSources = [];
|
|
102
|
-
for (const [key, group] of bySource) {
|
|
103
|
-
const [scope, plugin] = key.split('\t');
|
|
104
|
-
const names = group.map((g) => g.name);
|
|
105
|
-
const roots = names
|
|
106
|
-
.filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
|
|
107
|
-
.sort();
|
|
108
|
-
if (roots.length === 0)
|
|
109
|
-
continue;
|
|
110
|
-
(scope === 'project' ? projectSources : userSources).push({ plugin, roots });
|
|
111
|
-
}
|
|
112
|
-
const body = [];
|
|
113
|
-
renderCatalogSection('Project', projectSources, pluginDescriptions, body);
|
|
114
|
-
renderCatalogSection('User', userSources, pluginDescriptions, body);
|
|
115
|
-
// renderCatalogSection leads each section with a blank separator; drop the
|
|
116
|
-
// leading one so the element body starts on its first real line.
|
|
117
|
-
while (body.length > 0 && body[0] === '')
|
|
118
|
-
body.shift();
|
|
119
|
-
body.push('');
|
|
120
|
-
body.push("Groups shown as `name/ N skills` are collapsed. Read the group to get its menu before assuming a skill is or isn't there: `crtr skill read <group>` (or `crtr skill find list --plugin <group>`). Search across everything with `crtr skill find search <topic>`.");
|
|
121
|
-
return stateBlock('skills', { count: skills.length }, body.join('\n'));
|
|
122
|
-
}
|
|
123
70
|
export function renderCatalogSection(label, sources, descriptions, out) {
|
|
124
71
|
if (sources.length === 0)
|
|
125
72
|
return;
|
package/dist/commands/skill.js
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
|
-
// `crtr skill` subtree handlers
|
|
2
|
-
// Sub-branches:
|
|
3
|
-
//
|
|
1
|
+
// `crtr skill` subtree handlers.
|
|
2
|
+
// Sub-branches: author {guide, scaffold}, state {enable, disable}.
|
|
3
|
+
// Discovery and reading are now `crtr memory` verbs.
|
|
4
4
|
import { defineBranch } from '../core/command.js';
|
|
5
|
-
import { buildSkillCatalog } from './skill/shared.js';
|
|
6
|
-
import { findBranch } from './skill/find.js';
|
|
7
|
-
import { readLeaf } from './skill/read.js';
|
|
8
5
|
import { authorBranch } from './skill/author.js';
|
|
9
6
|
import { stateBranch } from './skill/state.js';
|
|
10
7
|
export function registerSkill() {
|
|
11
8
|
return defineBranch({
|
|
12
9
|
name: 'skill',
|
|
13
10
|
rootEntry: {
|
|
14
|
-
concept: 'a SKILL.md you
|
|
15
|
-
desc: '
|
|
16
|
-
useWhen: '
|
|
17
|
-
dynamicState: buildSkillCatalog,
|
|
11
|
+
concept: 'a SKILL.md you author and manage',
|
|
12
|
+
desc: 'author and manage skills',
|
|
13
|
+
useWhen: 'authoring a new skill (`crtr skill author`) or toggling a skill on/off (`crtr skill state`). To discover and read skills use `crtr memory` — `crtr memory read <name>`, `crtr memory find`, `crtr memory list`.',
|
|
18
14
|
},
|
|
19
15
|
help: {
|
|
20
16
|
name: 'skill',
|
|
21
|
-
summary: '
|
|
22
|
-
model: '`
|
|
23
|
-
dynamicState: buildSkillCatalog,
|
|
17
|
+
summary: 'author and manage skill state — use `crtr memory` to discover and read skills',
|
|
18
|
+
model: '`author` when you are writing a new skill — it carries the template workflow and the scaffolder. `state` when a skill should be hidden from discovery without being removed. Discovery and reading are `crtr memory` verbs: `crtr memory list`, `crtr memory find`, `crtr memory read`. Append `-h` at any branch or leaf for its full schema.',
|
|
24
19
|
},
|
|
25
|
-
children: [
|
|
20
|
+
children: [authorBranch, stateBranch],
|
|
26
21
|
});
|
|
27
22
|
}
|
|
@@ -326,7 +326,7 @@ function runChecksForScope(scope, opts) {
|
|
|
326
326
|
export const sysDoctorLeaf = defineLeaf({
|
|
327
327
|
name: 'doctor',
|
|
328
328
|
description: 'diagnose installation health',
|
|
329
|
-
whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or skill frontmatter has drifted from its filename. Reports each problem with a structured remediation
|
|
329
|
+
whenToUse: 'something in your crtr install looks off and you want it diagnosed — a plugin or marketplace manifest is missing, a config entry points at a directory that no longer exists, or skill frontmatter has drifted from its filename. Reports each problem with a structured remediation, and can apply the repairs for you.',
|
|
330
330
|
help: {
|
|
331
331
|
name: 'sys doctor',
|
|
332
332
|
summary: 'diagnose missing manifests, broken config entries, and skill frontmatter drift',
|
|
@@ -7,7 +7,7 @@ import { readPackageVersion } from './shared.js';
|
|
|
7
7
|
export const sysUpdateLeaf = defineLeaf({
|
|
8
8
|
name: 'update',
|
|
9
9
|
description: 'update binary and content',
|
|
10
|
-
whenToUse: 'upgrading crtr or its installed plugins and marketplaces — pull the latest binary, refresh installed content, or both
|
|
10
|
+
whenToUse: 'upgrading crtr or its installed plugins and marketplaces — pull the latest binary, refresh installed content, or both, or just check what is out of date without applying anything.',
|
|
11
11
|
help: {
|
|
12
12
|
name: 'sys update',
|
|
13
13
|
summary: 'update the crtr binary and/or installed plugins and marketplaces',
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// `crtr view cycle <dir>` — switch the view hosted in a monitor pane to the
|
|
2
|
+
// next/prev available view, in place. Backs the Alt+V then ] (next) / Alt+V
|
|
3
|
+
// then [ (prev) chord installed by installViewNavBindings, mirroring how
|
|
4
|
+
// `node cycle` backs Alt+] / Alt+[ for the node graph (view = the bracket
|
|
5
|
+
// grammar namespaced under the Alt+V view prefix).
|
|
6
|
+
//
|
|
7
|
+
// A view monitor pane is self-identifying: `view run` tags it with the tmux
|
|
8
|
+
// pane option @crtr_view=<id>. Cycle reads that tag, computes the next/prev id
|
|
9
|
+
// in `listViews()` order (the SAME set `view list` enumerates), and respawns
|
|
10
|
+
// the pane on `crtr view run <next-id>` — `respawn-pane -k` kills the current
|
|
11
|
+
// view process and re-execs in the SAME pane (cycle itself runs in tmux's
|
|
12
|
+
// run-shell context, NOT inside the pane, so the kill never targets the caller).
|
|
13
|
+
// The respawned run re-tags the pane, so a rapid re-cycle reads the right id.
|
|
14
|
+
//
|
|
15
|
+
// tmux-only; a pane with no @crtr_view tag (not a view monitor) is a no-op.
|
|
16
|
+
// Output is discarded by the keybinding, so this just acts.
|
|
17
|
+
import { defineLeaf } from '../core/command.js';
|
|
18
|
+
import { listViews, resolveView, loadView } from '../core/tui/loader.js';
|
|
19
|
+
// Commands reach the tmux driver through placement.ts (the sanctioned
|
|
20
|
+
// model-over-driver seam, §5.1) — never `./tmux.js` directly.
|
|
21
|
+
import { inTmux, currentTmux, getPaneOption, setPaneOption, paneCurrentPath, respawnPaneSync, piCommand, } from '../core/runtime/placement.js';
|
|
22
|
+
export const viewCycleLeaf = defineLeaf({
|
|
23
|
+
name: 'cycle',
|
|
24
|
+
description: 'switch the monitor pane to the next/prev view in place',
|
|
25
|
+
whenToUse: 'flipping a view monitor to the next/prev available view without re-running a command (bound to Alt+V then ] forward / Alt+V then [ back). The pane must already be hosting a view — opened with `crtr view run <name>` (or --window/--split). Use `crtr view run <name>` to open a specific view, or `crtr view list` to see the cycle order',
|
|
26
|
+
help: {
|
|
27
|
+
name: 'view cycle',
|
|
28
|
+
summary: 'switch the view hosted in a monitor pane to the next/previous available view, in place — the views walked one monitor at a time, in `view list` order (bound to Alt+V then ] forward / Alt+V then [ back)',
|
|
29
|
+
params: [
|
|
30
|
+
{ kind: 'flag', name: 'dir', type: 'enum', choices: ['next', 'prev'], required: false, default: 'next', constraint: 'Direction along the view list: next (Alt+V then ]) or prev (Alt+V then [). Wraps at the ends.' },
|
|
31
|
+
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane to cycle. Defaults to $TMUX_PANE / your current pane. The Alt+V then ]/[ bindings pass this for you.' },
|
|
32
|
+
],
|
|
33
|
+
output: [
|
|
34
|
+
{ name: 'cycled', type: 'boolean', required: true, constraint: 'True when the pane was switched to another view.' },
|
|
35
|
+
{ name: 'view', type: 'string', required: false, constraint: 'The view now hosted in the pane.' },
|
|
36
|
+
{ name: 'from', type: 'string', required: false, constraint: 'The view it switched away from (absent when the pane was not a view monitor).' },
|
|
37
|
+
],
|
|
38
|
+
outputKind: 'object',
|
|
39
|
+
effects: [
|
|
40
|
+
'Respawns the target pane (respawn-pane -k) running `crtr view run <next-id>`; the prior view process is replaced in place.',
|
|
41
|
+
'Re-tags the pane @crtr_view=<next-id>. No-op outside tmux or when the pane is not a view monitor.',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
run: async (input) => {
|
|
45
|
+
if (!inTmux())
|
|
46
|
+
return { cycled: false };
|
|
47
|
+
const dir = (input['dir'] ?? 'next');
|
|
48
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'] ?? currentTmux()?.pane;
|
|
49
|
+
if (pane === undefined || pane === '')
|
|
50
|
+
return { cycled: false };
|
|
51
|
+
// Self-identification: only a pane `view run` tagged is a view monitor.
|
|
52
|
+
const current = getPaneOption(pane, '@crtr_view');
|
|
53
|
+
if (current === undefined || current === '')
|
|
54
|
+
return { cycled: false };
|
|
55
|
+
const ids = listViews().map((v) => v.id); // SAME set + order as `view list`
|
|
56
|
+
if (ids.length < 2)
|
|
57
|
+
return { cycled: false, from: current };
|
|
58
|
+
// Build the candidate order: views after `current` in `dir`, wrapping. If
|
|
59
|
+
// `current` was renamed/removed (i === -1) start at the head (next) / tail
|
|
60
|
+
// (prev) so the cycle lands on the FIRST available view (m3). Then advance
|
|
61
|
+
// past any view that fails to load, so a malformed view never closes the
|
|
62
|
+
// monitor on respawn (m4) — bounded to one full lap.
|
|
63
|
+
const n = ids.length;
|
|
64
|
+
const mod = (x) => ((x % n) + n) % n;
|
|
65
|
+
const step = dir === 'next' ? 1 : -1;
|
|
66
|
+
const i = ids.indexOf(current);
|
|
67
|
+
const candidates = [];
|
|
68
|
+
if (i === -1) {
|
|
69
|
+
const startIdx = dir === 'next' ? 0 : n - 1;
|
|
70
|
+
for (let k = 0; k < n; k++)
|
|
71
|
+
candidates.push(ids[mod(startIdx + step * k)]);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
for (let k = 1; k < n; k++)
|
|
75
|
+
candidates.push(ids[mod(i + step * k)]);
|
|
76
|
+
}
|
|
77
|
+
let targetId;
|
|
78
|
+
for (const cand of candidates) {
|
|
79
|
+
if (cand === current)
|
|
80
|
+
continue;
|
|
81
|
+
const rv = resolveView(cand);
|
|
82
|
+
if (rv === null)
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
await loadView(rv);
|
|
86
|
+
targetId = cand;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
catch { /* skip malformed */ }
|
|
90
|
+
}
|
|
91
|
+
if (targetId === undefined)
|
|
92
|
+
return { cycled: false, from: current };
|
|
93
|
+
// Replay the connection params the monitor was opened with (M2): a view
|
|
94
|
+
// configured with --port/--target keeps them across a cycle, so a round-trip
|
|
95
|
+
// doesn't silently drop e.g. the LinkedIn view's CDP port. Params stick to
|
|
96
|
+
// the MONITOR (pane), not the view, and are harmless to views that ignore them.
|
|
97
|
+
const keepPort = getPaneOption(pane, '@crtr_view_port');
|
|
98
|
+
const keepTarget = getPaneOption(pane, '@crtr_view_target');
|
|
99
|
+
const argv = ['view', 'run', targetId];
|
|
100
|
+
if (keepPort !== undefined && keepPort !== '')
|
|
101
|
+
argv.push('--port', keepPort);
|
|
102
|
+
if (keepTarget !== undefined && keepTarget !== '')
|
|
103
|
+
argv.push('--target', keepTarget);
|
|
104
|
+
// Preserve the monitor's cwd so project-scoped views still resolve.
|
|
105
|
+
const cwd = paneCurrentPath(pane) ?? process.cwd();
|
|
106
|
+
const command = piCommand(argv, 'crtr');
|
|
107
|
+
const ok = respawnPaneSync({ pane, cwd, env: {}, command });
|
|
108
|
+
if (!ok)
|
|
109
|
+
return { cycled: false, from: current };
|
|
110
|
+
// The respawned `view run` re-tags @crtr_view (+ params), but set them now
|
|
111
|
+
// too so a rapid re-cycle reads the right state before the new run boots.
|
|
112
|
+
try {
|
|
113
|
+
setPaneOption(pane, '@crtr_view', targetId);
|
|
114
|
+
setPaneOption(pane, '@crtr_view_port', keepPort ?? '');
|
|
115
|
+
setPaneOption(pane, '@crtr_view_target', keepTarget ?? '');
|
|
116
|
+
}
|
|
117
|
+
catch { /* best-effort */ }
|
|
118
|
+
return { cycled: true, view: targetId, from: current };
|
|
119
|
+
},
|
|
120
|
+
render: (r) => r['cycled'] === true
|
|
121
|
+
? `<view-cycled to="${r['view']}" from="${r['from'] ?? ''}"/>`
|
|
122
|
+
: r['from'] !== undefined
|
|
123
|
+
? `<view-cycle-noop>no other view to switch to</view-cycle-noop>`
|
|
124
|
+
: `<view-cycle-noop>this pane is not hosting a view</view-cycle-noop>`,
|
|
125
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `crtr view list` — enumerate every resolvable view across scopes.
|
|
2
|
+
//
|
|
3
|
+
// Lists id · title · description · scope. Loads each view to read its manifest
|
|
4
|
+
// title/description; a malformed view is shown with an error marker (it never
|
|
5
|
+
// crashes the listing). Plain text — pipeable as-is. The raw array is available
|
|
6
|
+
// behind --json for tooling.
|
|
7
|
+
import { defineLeaf } from '../core/command.js';
|
|
8
|
+
import { listViews, loadView } from '../core/tui/loader.js';
|
|
9
|
+
export const viewListLeaf = defineLeaf({
|
|
10
|
+
name: 'list',
|
|
11
|
+
description: 'enumerate available views (id · title · description · scope)',
|
|
12
|
+
whenToUse: 'you want to see which views exist before running one — a flat roster across project, user, and builtin scopes. Pipeable plain text; pass --json for the raw array. Use `crtr view run <id>` to open one, `crtr view new <name>` to scaffold one',
|
|
13
|
+
help: {
|
|
14
|
+
name: 'view list',
|
|
15
|
+
summary: 'list every resolvable view (id, title, description, scope); pipeable plain text',
|
|
16
|
+
inputNote: 'No input parameters.',
|
|
17
|
+
output: [
|
|
18
|
+
{ name: 'views', type: 'array', required: true, constraint: 'One entry per view: { id, title, description, scope, error? }.' },
|
|
19
|
+
],
|
|
20
|
+
outputKind: 'object',
|
|
21
|
+
effects: ['None. Read-only.'],
|
|
22
|
+
},
|
|
23
|
+
run: async (input) => {
|
|
24
|
+
const rows = [];
|
|
25
|
+
for (const r of listViews()) {
|
|
26
|
+
try {
|
|
27
|
+
const v = await loadView(r);
|
|
28
|
+
rows.push({
|
|
29
|
+
id: r.id,
|
|
30
|
+
title: v.manifest.title,
|
|
31
|
+
description: v.manifest.description ?? '',
|
|
32
|
+
scope: r.scope,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
// Tolerate a malformed view — show it with an error marker, don't crash.
|
|
37
|
+
rows.push({
|
|
38
|
+
id: r.id,
|
|
39
|
+
title: '(failed to load)',
|
|
40
|
+
description: '',
|
|
41
|
+
scope: r.scope,
|
|
42
|
+
error: e instanceof Error ? e.message : String(e),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { views: rows };
|
|
47
|
+
},
|
|
48
|
+
render: (result) => {
|
|
49
|
+
const rows = result['views'] ?? [];
|
|
50
|
+
if (rows.length === 0) {
|
|
51
|
+
return 'No views found. Scaffold one with `crtr view new <name>`.';
|
|
52
|
+
}
|
|
53
|
+
const idW = Math.max(...rows.map((r) => r.id.length), 2);
|
|
54
|
+
const titleW = Math.max(...rows.map((r) => r.title.length), 5);
|
|
55
|
+
const lines = rows.map((r) => {
|
|
56
|
+
const id = r.id.padEnd(idW);
|
|
57
|
+
const title = r.title.padEnd(titleW);
|
|
58
|
+
if (r.error !== undefined) {
|
|
59
|
+
return `${id} ${title} [${r.scope}] ⚠ ${r.error}`;
|
|
60
|
+
}
|
|
61
|
+
const desc = r.description !== '' ? ` ${r.description}` : '';
|
|
62
|
+
return `${id} ${title} [${r.scope}]${desc}`;
|
|
63
|
+
});
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// `crtr view new <name>` — scaffold a runnable view.mjs into a scope dir.
|
|
2
|
+
//
|
|
3
|
+
// Writes <viewsDir(scope)>/<name>/view.mjs from the stub in prompts/view.ts
|
|
4
|
+
// (mkdir -p first). Refuses if a view of that name already resolves. The
|
|
5
|
+
// scaffolded view runs as-is via `crtr view run <name>`. Mirrors how skill
|
|
6
|
+
// authoring scaffolds (resolve scope → ensure dir → write → point at how to run).
|
|
7
|
+
import { mkdirSync, existsSync } from 'node:fs';
|
|
8
|
+
import { writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { defineLeaf } from '../core/command.js';
|
|
11
|
+
import { usage } from '../core/errors.js';
|
|
12
|
+
import { resolveView } from '../core/tui/loader.js';
|
|
13
|
+
import { viewsDir, projectScopeRoot, ensureProjectScopeRoot } from '../core/scope.js';
|
|
14
|
+
import { viewScaffold } from '../prompts/view.js';
|
|
15
|
+
export const viewNewLeaf = defineLeaf({
|
|
16
|
+
name: 'new',
|
|
17
|
+
description: 'scaffold a runnable view.mjs stub into a scope dir',
|
|
18
|
+
whenToUse: 'you want to author a new view — this writes a minimal, runnable view.mjs (with the contract + draw/host API documented inline) into the user or project scope, ready to open immediately with `crtr view run <name>` and to edit from there. Reach for it to start a new TUI surface rather than hand-writing the module',
|
|
19
|
+
help: {
|
|
20
|
+
name: 'view new',
|
|
21
|
+
summary: 'scaffold <viewsDir(scope)>/<name>/view.mjs from the stub; refuses if the name already resolves',
|
|
22
|
+
params: [
|
|
23
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'View id / directory name. Must not already resolve as a view.' },
|
|
24
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Where to scaffold. Default: project if available, else user.' },
|
|
25
|
+
{ kind: 'flag', name: 'title', type: 'string', required: false, constraint: 'Manifest title (header + picker). Default: the name.' },
|
|
26
|
+
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Manifest description (picker + `view list`). Default: empty.' },
|
|
27
|
+
],
|
|
28
|
+
output: [
|
|
29
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded view.mjs.' },
|
|
30
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'The scope it was written to (user|project).' },
|
|
31
|
+
{ name: 'run', type: 'string', required: true, constraint: 'The command to run the new view.' },
|
|
32
|
+
],
|
|
33
|
+
outputKind: 'object',
|
|
34
|
+
effects: [
|
|
35
|
+
'Creates the view directory and view.mjs stub at the resolved location (mkdir -p).',
|
|
36
|
+
'Initializes the project scope dir if --scope project is chosen and none exists yet.',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
run: async (input) => {
|
|
40
|
+
const name = input['name'];
|
|
41
|
+
const scopeArg = input['scope'];
|
|
42
|
+
const title = input['title'] ?? name;
|
|
43
|
+
const description = input['description'] ?? '';
|
|
44
|
+
// Default scope: project if a project scope exists, else user.
|
|
45
|
+
const scope = scopeArg === 'project' || scopeArg === 'user'
|
|
46
|
+
? scopeArg
|
|
47
|
+
: (projectScopeRoot() !== null ? 'project' : 'user');
|
|
48
|
+
// Refuse if a view of this name already resolves (any scope wins).
|
|
49
|
+
const existing = resolveView(name);
|
|
50
|
+
if (existing !== null) {
|
|
51
|
+
throw usage(`view already exists: ${name} (${existing.scope}) at ${existing.entry}. ` +
|
|
52
|
+
`Pick another name, or edit it directly.`);
|
|
53
|
+
}
|
|
54
|
+
// Ensure the project scope root exists before computing its views dir.
|
|
55
|
+
if (scope === 'project')
|
|
56
|
+
ensureProjectScopeRoot();
|
|
57
|
+
const baseDir = viewsDir(scope);
|
|
58
|
+
if (baseDir === null) {
|
|
59
|
+
throw usage(`no ${scope} scope available to scaffold into — try --scope user.`);
|
|
60
|
+
}
|
|
61
|
+
const dir = join(baseDir, name);
|
|
62
|
+
const entry = join(dir, 'view.mjs');
|
|
63
|
+
if (existsSync(entry)) {
|
|
64
|
+
throw usage(`view file already exists: ${entry}`);
|
|
65
|
+
}
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
writeFileSync(entry, viewScaffold({ id: name, title, description }), 'utf8');
|
|
68
|
+
return { path: entry, scope, run: `crtr view run ${name}` };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// `crtr view pick` — hidden raw-ANSI picker over the available views.
|
|
2
|
+
//
|
|
3
|
+
// Backs the /view popup: a minimal full-screen list of listViews(); j/k move,
|
|
4
|
+
// Enter hands off to `crtr view run <id>` (execFileSync, stdio inherit — a
|
|
5
|
+
// one-way handoff, the picker exits as the view takes over, same pattern as
|
|
6
|
+
// browse's selectAndFocus), q/Esc/Ctrl-C quit. Outside a TTY it prints the
|
|
7
|
+
// plain list and exits 0. Reuses the core/tui terminal + draw primitives.
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { defineLeaf } from '../core/command.js';
|
|
10
|
+
import { listViews } from '../core/tui/loader.js';
|
|
11
|
+
import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../core/tui/terminal.js';
|
|
12
|
+
import { createDraw, detectColorCaps } from '../core/tui/draw.js';
|
|
13
|
+
export const viewPickLeaf = defineLeaf({
|
|
14
|
+
name: 'pick',
|
|
15
|
+
tier: 'hidden',
|
|
16
|
+
description: 'raw-ANSI view picker (used by the /view popup)',
|
|
17
|
+
whenToUse: 'internal — the /view popup shells this to choose a view, then it execs `crtr view run <id>`. You normally run `crtr view run <name>` or `crtr view list` directly',
|
|
18
|
+
help: {
|
|
19
|
+
name: 'view pick',
|
|
20
|
+
summary: 'interactive picker over available views; Enter runs the chosen view. Outside a TTY it prints the list and exits',
|
|
21
|
+
inputNote: 'No input parameters.',
|
|
22
|
+
output: [],
|
|
23
|
+
outputKind: 'object',
|
|
24
|
+
effects: [
|
|
25
|
+
'Inside a TTY: takes over the terminal in raw mode until you pick (Enter) or quit (q/Esc/Ctrl-C).',
|
|
26
|
+
'On Enter: execs `crtr view run <id>` (stdio inherited) — a one-way handoff; the picker exits.',
|
|
27
|
+
'Outside a TTY: prints the plain view list and exits 0.',
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
run: async () => {
|
|
31
|
+
const views = listViews();
|
|
32
|
+
// Non-TTY: print the plain list and exit 0.
|
|
33
|
+
if (!process.stdin.isTTY) {
|
|
34
|
+
const text = views.length > 0
|
|
35
|
+
? views.map((v) => `${v.id}\t${v.scope}`).join('\n')
|
|
36
|
+
: '(no views)';
|
|
37
|
+
process.stdout.write(text + '\n');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (views.length === 0) {
|
|
41
|
+
process.stdout.write('No views found. Scaffold one with `crtr view new <name>`.\n');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const caps = detectColorCaps();
|
|
45
|
+
let cursor = 0;
|
|
46
|
+
let scroll = 0;
|
|
47
|
+
let restored = false;
|
|
48
|
+
const cleanup = () => {
|
|
49
|
+
if (restored)
|
|
50
|
+
return;
|
|
51
|
+
restored = true;
|
|
52
|
+
try {
|
|
53
|
+
restoreTerminal();
|
|
54
|
+
}
|
|
55
|
+
catch { /* best-effort */ }
|
|
56
|
+
};
|
|
57
|
+
process.once('exit', cleanup);
|
|
58
|
+
const render = () => {
|
|
59
|
+
const size = getTerminalSize();
|
|
60
|
+
const { draw, frame } = createDraw(size, caps);
|
|
61
|
+
draw.text(0, 0, 'crtr views — j/k move · Enter run · q quit', { bold: true });
|
|
62
|
+
draw.hline(1, 0, size.cols);
|
|
63
|
+
const rect = { row: 2, col: 0, width: size.cols, height: Math.max(1, size.rows - 3) };
|
|
64
|
+
const items = views.map((v) => ({
|
|
65
|
+
spans: [
|
|
66
|
+
{ text: v.id, style: { bold: true } },
|
|
67
|
+
{ text: ` [${v.scope}]`, style: { dim: true } },
|
|
68
|
+
],
|
|
69
|
+
}));
|
|
70
|
+
const res = draw.list(rect, items, cursor, scroll);
|
|
71
|
+
scroll = res.scroll;
|
|
72
|
+
process.stdout.write(frame());
|
|
73
|
+
};
|
|
74
|
+
setupTerminal();
|
|
75
|
+
render();
|
|
76
|
+
await new Promise((resolve) => {
|
|
77
|
+
const finish = () => {
|
|
78
|
+
cleanup();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
};
|
|
81
|
+
const launch = (id) => {
|
|
82
|
+
cleanup();
|
|
83
|
+
try {
|
|
84
|
+
execFileSync('crtr', ['view', 'run', id], { stdio: 'inherit' });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// The view takes the terminal over; a sync handoff may be interrupted.
|
|
88
|
+
}
|
|
89
|
+
process.exit(0);
|
|
90
|
+
};
|
|
91
|
+
process.stdin.on('data', (d) => {
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = parseKeypress(d);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const { input, key } = parsed;
|
|
100
|
+
if (key.ctrl && input === 'c')
|
|
101
|
+
finish();
|
|
102
|
+
if (input === 'q' || key.escape)
|
|
103
|
+
finish();
|
|
104
|
+
if (input === 'j' || key.downArrow) {
|
|
105
|
+
cursor = Math.min(views.length - 1, cursor + 1);
|
|
106
|
+
render();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (input === 'k' || key.upArrow) {
|
|
110
|
+
cursor = Math.max(0, cursor - 1);
|
|
111
|
+
render();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (key.return)
|
|
115
|
+
launch(views[cursor].id);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
});
|