@poping/yome 0.0.2 → 0.0.3
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/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +308 -27
- package/README.zh-CN.md +335 -0
- package/dist/agent.d.ts +24 -2
- package/dist/agent.js +34 -2
- package/dist/agent.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.js +116 -11
- package/dist/context.js.map +1 -1
- package/dist/index.js +147 -9
- package/dist/index.js.map +1 -1
- package/dist/llm.js +45 -2
- package/dist/llm.js.map +1 -1
- package/dist/loops/chain.js +8 -0
- package/dist/loops/chain.js.map +1 -1
- package/dist/loops/evaluator.js +8 -0
- package/dist/loops/evaluator.js.map +1 -1
- package/dist/loops/orchestrator.js +8 -0
- package/dist/loops/orchestrator.js.map +1 -1
- package/dist/loops/parallel.js.map +1 -1
- package/dist/loops/route.js +8 -0
- package/dist/loops/route.js.map +1 -1
- package/dist/loops/simple.js +15 -0
- package/dist/loops/simple.js.map +1 -1
- package/dist/permissions/index.d.ts +1 -1
- package/dist/permissions/index.js +1 -1
- package/dist/permissions/index.js.map +1 -1
- package/dist/permissions/loader.d.ts +20 -1
- package/dist/permissions/loader.js +51 -0
- package/dist/permissions/loader.js.map +1 -1
- package/dist/redact.d.ts +56 -0
- package/dist/redact.js +191 -0
- package/dist/redact.js.map +1 -0
- package/dist/skills/runner/applescript.d.ts +49 -0
- package/dist/skills/runner/applescript.js +100 -0
- package/dist/skills/runner/applescript.js.map +1 -0
- package/dist/skills/runner/dispatcher.d.ts +57 -0
- package/dist/skills/runner/dispatcher.js +407 -0
- package/dist/skills/runner/dispatcher.js.map +1 -0
- package/dist/skills/runner/kernel.d.ts +8 -0
- package/dist/skills/runner/kernel.js +732 -0
- package/dist/skills/runner/kernel.js.map +1 -0
- package/dist/skills/runner/tokenizer.d.ts +36 -0
- package/dist/skills/runner/tokenizer.js +177 -0
- package/dist/skills/runner/tokenizer.js.map +1 -0
- package/dist/threadCli.d.ts +11 -0
- package/dist/threadCli.js +177 -0
- package/dist/threadCli.js.map +1 -0
- package/dist/threadShare.d.ts +21 -0
- package/dist/threadShare.js +121 -0
- package/dist/threadShare.js.map +1 -0
- package/dist/threadSubmit.d.ts +32 -0
- package/dist/threadSubmit.js +199 -0
- package/dist/threadSubmit.js.map +1 -0
- package/dist/tools/bash.js +68 -13
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/index.d.ts +22 -2
- package/dist/tools/index.js +41 -5
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/skillCall.d.ts +2 -0
- package/dist/tools/skillCall.js +77 -0
- package/dist/tools/skillCall.js.map +1 -0
- package/dist/ui/AgentPicker.js +3 -3
- package/dist/ui/AgentPicker.js.map +1 -1
- package/dist/ui/App.js +214 -61
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/Banner.d.ts +2 -1
- package/dist/ui/Banner.js +23 -4
- package/dist/ui/Banner.js.map +1 -1
- package/dist/ui/InputBar.js +6 -3
- package/dist/ui/InputBar.js.map +1 -1
- package/dist/ui/Markdown.d.ts +2 -2
- package/dist/ui/Markdown.js +22 -7
- package/dist/ui/Markdown.js.map +1 -1
- package/dist/ui/MarketplacePicker.d.ts +7 -0
- package/dist/ui/MarketplacePicker.js +122 -0
- package/dist/ui/MarketplacePicker.js.map +1 -0
- package/dist/ui/MessageList.d.ts +4 -1
- package/dist/ui/MessageList.js +72 -7
- package/dist/ui/MessageList.js.map +1 -1
- package/dist/ui/ModelPicker.js +4 -4
- package/dist/ui/ModelPicker.js.map +1 -1
- package/dist/ui/PermissionPrompt.d.ts +16 -4
- package/dist/ui/PermissionPrompt.js +60 -15
- package/dist/ui/PermissionPrompt.js.map +1 -1
- package/dist/ui/SessionPicker.js +2 -2
- package/dist/ui/SessionPicker.js.map +1 -1
- package/dist/ui/ShimmerText.d.ts +8 -0
- package/dist/ui/ShimmerText.js +40 -0
- package/dist/ui/ShimmerText.js.map +1 -0
- package/dist/ui/Spinner.js +3 -9
- package/dist/ui/Spinner.js.map +1 -1
- package/dist/ui/TogglePicker.js +4 -4
- package/dist/ui/TogglePicker.js.map +1 -1
- package/dist/ui/ToolResult.js +6 -0
- package/dist/ui/ToolResult.js.map +1 -1
- package/dist/ui/UnifiedSkillsPicker.d.ts +10 -0
- package/dist/ui/UnifiedSkillsPicker.js +63 -0
- package/dist/ui/UnifiedSkillsPicker.js.map +1 -0
- package/dist/ui/animation.d.ts +3 -0
- package/dist/ui/animation.js +48 -0
- package/dist/ui/animation.js.map +1 -0
- package/dist/ui/useThrottledStream.d.ts +7 -0
- package/dist/ui/useThrottledStream.js +63 -0
- package/dist/ui/useThrottledStream.js.map +1 -0
- package/dist/yomeSkills/auth.d.ts +20 -0
- package/dist/yomeSkills/auth.js +70 -0
- package/dist/yomeSkills/auth.js.map +1 -0
- package/dist/yomeSkills/blacklist.d.ts +33 -0
- package/dist/yomeSkills/blacklist.js +101 -0
- package/dist/yomeSkills/blacklist.js.map +1 -0
- package/dist/yomeSkills/capabilities.d.ts +54 -0
- package/dist/yomeSkills/capabilities.js +175 -0
- package/dist/yomeSkills/capabilities.js.map +1 -0
- package/dist/yomeSkills/capabilityGuard.d.ts +31 -0
- package/dist/yomeSkills/capabilityGuard.js +113 -0
- package/dist/yomeSkills/capabilityGuard.js.map +1 -0
- package/dist/yomeSkills/cli.d.ts +17 -0
- package/dist/yomeSkills/cli.js +477 -0
- package/dist/yomeSkills/cli.js.map +1 -0
- package/dist/yomeSkills/devLink.d.ts +17 -0
- package/dist/yomeSkills/devLink.js +91 -0
- package/dist/yomeSkills/devLink.js.map +1 -0
- package/dist/yomeSkills/doctor.d.ts +13 -0
- package/dist/yomeSkills/doctor.js +152 -0
- package/dist/yomeSkills/doctor.js.map +1 -0
- package/dist/yomeSkills/enable.d.ts +8 -0
- package/dist/yomeSkills/enable.js +67 -0
- package/dist/yomeSkills/enable.js.map +1 -0
- package/dist/yomeSkills/hubPing.d.ts +1 -0
- package/dist/yomeSkills/hubPing.js +41 -0
- package/dist/yomeSkills/hubPing.js.map +1 -0
- package/dist/yomeSkills/install.d.ts +18 -0
- package/dist/yomeSkills/install.js +143 -0
- package/dist/yomeSkills/install.js.map +1 -0
- package/dist/yomeSkills/installGithub.d.ts +26 -0
- package/dist/yomeSkills/installGithub.js +192 -0
- package/dist/yomeSkills/installGithub.js.map +1 -0
- package/dist/yomeSkills/installMeta.d.ts +8 -0
- package/dist/yomeSkills/installMeta.js +76 -0
- package/dist/yomeSkills/installMeta.js.map +1 -0
- package/dist/yomeSkills/integrity.d.ts +26 -0
- package/dist/yomeSkills/integrity.js +107 -0
- package/dist/yomeSkills/integrity.js.map +1 -0
- package/dist/yomeSkills/invoke.d.ts +29 -0
- package/dist/yomeSkills/invoke.js +103 -0
- package/dist/yomeSkills/invoke.js.map +1 -0
- package/dist/yomeSkills/list.d.ts +11 -0
- package/dist/yomeSkills/list.js +55 -0
- package/dist/yomeSkills/list.js.map +1 -0
- package/dist/yomeSkills/login.d.ts +41 -0
- package/dist/yomeSkills/login.js +221 -0
- package/dist/yomeSkills/login.js.map +1 -0
- package/dist/yomeSkills/manifest.d.ts +60 -0
- package/dist/yomeSkills/manifest.js +47 -0
- package/dist/yomeSkills/manifest.js.map +1 -0
- package/dist/yomeSkills/paths.d.ts +13 -0
- package/dist/yomeSkills/paths.js +33 -0
- package/dist/yomeSkills/paths.js.map +1 -0
- package/dist/yomeSkills/publish.d.ts +16 -0
- package/dist/yomeSkills/publish.js +109 -0
- package/dist/yomeSkills/publish.js.map +1 -0
- package/dist/yomeSkills/rollback.d.ts +10 -0
- package/dist/yomeSkills/rollback.js +83 -0
- package/dist/yomeSkills/rollback.js.map +1 -0
- package/dist/yomeSkills/search.d.ts +21 -0
- package/dist/yomeSkills/search.js +31 -0
- package/dist/yomeSkills/search.js.map +1 -0
- package/dist/yomeSkills/skillsIndex.d.ts +36 -0
- package/dist/yomeSkills/skillsIndex.js +111 -0
- package/dist/yomeSkills/skillsIndex.js.map +1 -0
- package/dist/yomeSkills/unified.d.ts +53 -0
- package/dist/yomeSkills/unified.js +187 -0
- package/dist/yomeSkills/unified.js.map +1 -0
- package/dist/yomeSkills/uninstall.d.ts +7 -0
- package/dist/yomeSkills/uninstall.js +22 -0
- package/dist/yomeSkills/uninstall.js.map +1 -0
- package/dist/yomeSkills/update.d.ts +18 -0
- package/dist/yomeSkills/update.js +75 -0
- package/dist/yomeSkills/update.js.map +1 -0
- package/dist/yomeSkills/validate.d.ts +11 -0
- package/dist/yomeSkills/validate.js +99 -0
- package/dist/yomeSkills/validate.js.map +1 -0
- package/package.json +14 -5
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
// cli/src/skills/runner/kernel.ts
|
|
2
|
+
//
|
|
3
|
+
// Yome agentic bash kernel.
|
|
4
|
+
//
|
|
5
|
+
// The LLM in cli only ever sees one execution tool: `Bash`. To let it
|
|
6
|
+
// invoke installed hub skills with the same ergonomic command-line shape
|
|
7
|
+
// a human would type (`ppt new ~/Desktop/x.pptx`), the Bash tool calls
|
|
8
|
+
// `tryKernel` BEFORE handing anything to /bin/sh. The kernel:
|
|
9
|
+
//
|
|
10
|
+
// 1. Tokenises the line (quote-aware; no shell expansion).
|
|
11
|
+
// 2. Refuses to handle compound lines (pipes, &&, ;, redirects, subshell)
|
|
12
|
+
// so genuine shell stays as shell.
|
|
13
|
+
// 3. Refuses to handle reserved tokens (git, ls, cd, ...) — those are
|
|
14
|
+
// always real shell, never skill invocations.
|
|
15
|
+
// 4. If tokens[0] matches an installed hub skill's `domain`, runs the
|
|
16
|
+
// action via invokeSkill — same path the macOS app uses, including
|
|
17
|
+
// capability gating.
|
|
18
|
+
// 5. Supports `--help` at three levels (global / domain / action) and
|
|
19
|
+
// renders compact TSV-ish text the LLM can scan cheaply.
|
|
20
|
+
//
|
|
21
|
+
// Anything the kernel doesn't claim falls through to plain shell.
|
|
22
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { tokenize, looksCompound, splitOnUnquotedAmpAmp } from './tokenizer.js';
|
|
25
|
+
import { getInstalledFast } from '../../yomeSkills/skillsIndex.js';
|
|
26
|
+
import { readManifest } from '../../yomeSkills/manifest.js';
|
|
27
|
+
import { invokeSkill } from '../../yomeSkills/invoke.js';
|
|
28
|
+
import { loadMacosBackend } from './dispatcher.js';
|
|
29
|
+
// Anything that's a system command we actively want to stay as a real
|
|
30
|
+
// shell call. We refuse to interpret these as a skill domain even if a
|
|
31
|
+
// future user installs a same-named skill — safety first.
|
|
32
|
+
const RESERVED_SYSTEM_COMMANDS = new Set([
|
|
33
|
+
// Coreutils + common
|
|
34
|
+
'ls', 'cd', 'pwd', 'cp', 'mv', 'rm', 'mkdir', 'rmdir', 'touch',
|
|
35
|
+
'cat', 'head', 'tail', 'less', 'more', 'grep', 'find', 'sort', 'uniq', 'wc',
|
|
36
|
+
'echo', 'printf', 'true', 'false', 'test', 'tr', 'sed', 'awk', 'cut',
|
|
37
|
+
// Shells
|
|
38
|
+
'sh', 'bash', 'zsh', 'fish', 'env',
|
|
39
|
+
// Tooling
|
|
40
|
+
'git', 'gh', 'curl', 'wget', 'ssh', 'scp', 'rsync', 'open', 'osascript',
|
|
41
|
+
'node', 'npm', 'pnpm', 'yarn', 'npx', 'tsc',
|
|
42
|
+
'python', 'python3', 'pip', 'pip3', 'ruby', 'go', 'cargo', 'rustc',
|
|
43
|
+
'java', 'javac', 'mvn', 'gradle',
|
|
44
|
+
'docker', 'kubectl', 'make',
|
|
45
|
+
// Yome itself — `yome <subcmd>` always means the cli, never a skill domain
|
|
46
|
+
'yome',
|
|
47
|
+
]);
|
|
48
|
+
// ── Public entry ────────────────────────────────────────────────────
|
|
49
|
+
export async function tryKernel(commandLine) {
|
|
50
|
+
const trimmed = commandLine.trim();
|
|
51
|
+
if (!trimmed)
|
|
52
|
+
return notHandled();
|
|
53
|
+
// ── Batch mode (must come BEFORE looksCompound — heredoc uses `<<`) ──
|
|
54
|
+
// Two accepted shapes:
|
|
55
|
+
// <domain> batch <<EOF\n...\nEOF
|
|
56
|
+
// <domain> batch\n<cmd>\n<cmd>...
|
|
57
|
+
// Both are claimed by the kernel even when the closing tag is missing
|
|
58
|
+
// so we can return a useful error instead of silently passing the
|
|
59
|
+
// heredoc to /bin/sh and letting the user wonder what happened.
|
|
60
|
+
//
|
|
61
|
+
// Carve out `<domain> batch --help` first — that's a help request, not
|
|
62
|
+
// a batch invocation with an empty body.
|
|
63
|
+
const helpSnif = trimmed.match(/^(\w+)\s+batch\b\s+(--help|-h)\b\s*$/);
|
|
64
|
+
if (helpSnif) {
|
|
65
|
+
const installedHelp = getInstalledFast().filter((s) => s.status === 'enabled');
|
|
66
|
+
const sk = installedHelp.find((s) => s.domain === helpSnif[1]);
|
|
67
|
+
if (sk)
|
|
68
|
+
return ok(renderBatchHelp(sk));
|
|
69
|
+
}
|
|
70
|
+
const batch = parseBatchCommand(trimmed);
|
|
71
|
+
if (batch)
|
|
72
|
+
return runBatch(batch);
|
|
73
|
+
// ── `xl X && xl Y && ...` chains ───────────────────────────────
|
|
74
|
+
// If a line is ONLY composed of hub-skill invocations joined by
|
|
75
|
+
// top-level `&&`, the kernel runs them sequentially with short-circuit
|
|
76
|
+
// semantics rather than handing the whole compound line to /bin/sh
|
|
77
|
+
// (which would just say "xl: command not found"). Anything that's
|
|
78
|
+
// not a pure skill chain (mixed shell + skill, presence of `|`/`;`/
|
|
79
|
+
// redirect/subshell, unknown domains, --help mid-chain) falls through
|
|
80
|
+
// to looksCompound below and out to real shell.
|
|
81
|
+
if (trimmed.includes('&&')) {
|
|
82
|
+
const chainResult = await tryRunSkillChain(trimmed);
|
|
83
|
+
if (chainResult)
|
|
84
|
+
return chainResult;
|
|
85
|
+
}
|
|
86
|
+
// Compound shell stays shell.
|
|
87
|
+
if (looksCompound(trimmed))
|
|
88
|
+
return notHandled();
|
|
89
|
+
const tokens = tokenize(trimmed);
|
|
90
|
+
if (tokens.length === 0)
|
|
91
|
+
return notHandled();
|
|
92
|
+
const first = tokens[0];
|
|
93
|
+
// Global help — only when the user explicitly typed something we own.
|
|
94
|
+
// We don't claim a bare `--help` because that might mean something else
|
|
95
|
+
// in the host shell.
|
|
96
|
+
if (first === 'yome-skills' && (tokens.length === 1 || tokens[1] === '--help')) {
|
|
97
|
+
return ok(renderGlobalHelp());
|
|
98
|
+
}
|
|
99
|
+
if (RESERVED_SYSTEM_COMMANDS.has(first))
|
|
100
|
+
return notHandled();
|
|
101
|
+
// Find the installed hub skill whose `domain` matches token[0].
|
|
102
|
+
// Domains are short strings declared in yome-skill.json (e.g. "ppt").
|
|
103
|
+
const installed = getInstalledFast().filter((s) => s.status === 'enabled');
|
|
104
|
+
const skill = installed.find((s) => s.domain === first);
|
|
105
|
+
if (!skill)
|
|
106
|
+
return notHandled();
|
|
107
|
+
// From here on we *own* the line. Help routes return ok() with help text;
|
|
108
|
+
// execution routes go through invokeSkill.
|
|
109
|
+
// ppt --help / ppt -h
|
|
110
|
+
if (tokens.length === 1 || tokens[1] === '--help' || tokens[1] === '-h') {
|
|
111
|
+
return ok(renderDomainHelp(skill));
|
|
112
|
+
}
|
|
113
|
+
// ppt --doc → list templates from skill repo's docs/
|
|
114
|
+
// ppt --doc <name> → read that specific template
|
|
115
|
+
if (tokens[1] === '--doc') {
|
|
116
|
+
if (tokens.length === 2)
|
|
117
|
+
return ok(renderDocList(skill));
|
|
118
|
+
return ok(renderDocOne(skill, tokens[2]));
|
|
119
|
+
}
|
|
120
|
+
const action = tokens[1];
|
|
121
|
+
// ppt batch --help — kernel-level meta action.
|
|
122
|
+
if (action === 'batch' && (tokens.includes('--help') || tokens.includes('-h'))) {
|
|
123
|
+
return ok(renderBatchHelp(skill));
|
|
124
|
+
}
|
|
125
|
+
// ppt <action> --help
|
|
126
|
+
if (tokens.includes('--help') || tokens.includes('-h')) {
|
|
127
|
+
return ok(renderActionHelp(skill, action));
|
|
128
|
+
}
|
|
129
|
+
// Parse argv: collect positionals + flags
|
|
130
|
+
const { positionals, flags, parseError } = parseArgs(tokens.slice(2));
|
|
131
|
+
if (parseError) {
|
|
132
|
+
return err(parseError + '\n\n' + renderActionHelp(skill, action), 2);
|
|
133
|
+
}
|
|
134
|
+
const r = await invokeSkill({
|
|
135
|
+
slugOrDomain: skill.slug,
|
|
136
|
+
action,
|
|
137
|
+
positionals,
|
|
138
|
+
flags,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
handled: true,
|
|
142
|
+
stdout: r.stdout,
|
|
143
|
+
stderr: r.stderr,
|
|
144
|
+
exitCode: r.exitCode,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// ── `<domain> X && <domain> Y && ...` chain runner ───────────────────
|
|
148
|
+
//
|
|
149
|
+
// Returns:
|
|
150
|
+
// - null → not a pure skill chain (caller should keep going / fall to shell)
|
|
151
|
+
// - KernelResult → kernel handled the whole chain
|
|
152
|
+
//
|
|
153
|
+
// Acceptance criteria for "pure skill chain":
|
|
154
|
+
// * line splits on top-level `&&` into >= 2 segments
|
|
155
|
+
// * every segment, after tokenize+trim, starts with a token that maps
|
|
156
|
+
// to an installed+enabled hub-skill domain
|
|
157
|
+
// * no segment is a meta-action we can't safely chain (batch/help/--doc) —
|
|
158
|
+
// those would interact with the chain in surprising ways, so we let
|
|
159
|
+
// real shell handle them (exits 127, but only if user really wrote
|
|
160
|
+
// `xl --help && xl batch ...` which is nonsensical anyway)
|
|
161
|
+
//
|
|
162
|
+
// Execution semantics: bash-like `&&` short-circuit. Stop on first
|
|
163
|
+
// non-zero exit; return that exit code. Stdout is the concatenation of
|
|
164
|
+
// each successful segment's stdout; stderr is the failing segment's
|
|
165
|
+
// stderr (mirroring `bash -c 'a && b && c'`).
|
|
166
|
+
async function tryRunSkillChain(line) {
|
|
167
|
+
const segments = splitOnUnquotedAmpAmp(line);
|
|
168
|
+
if (segments.length < 2)
|
|
169
|
+
return null;
|
|
170
|
+
const installed = getInstalledFast().filter((s) => s.status === 'enabled');
|
|
171
|
+
const domains = new Set(installed.map((s) => s.domain));
|
|
172
|
+
// Pre-flight: every segment must (a) tokenize to >= 1 token, (b) lead
|
|
173
|
+
// with a known skill domain, (c) NOT be a meta-action.
|
|
174
|
+
for (const seg of segments) {
|
|
175
|
+
if (looksCompound(seg))
|
|
176
|
+
return null; // segment itself has |/;/redirect
|
|
177
|
+
const toks = tokenize(seg);
|
|
178
|
+
if (toks.length === 0)
|
|
179
|
+
return null;
|
|
180
|
+
const head = toks[0];
|
|
181
|
+
if (RESERVED_SYSTEM_COMMANDS.has(head))
|
|
182
|
+
return null;
|
|
183
|
+
if (!domains.has(head))
|
|
184
|
+
return null;
|
|
185
|
+
// Refuse to chain meta-actions — they have non-standard exit semantics.
|
|
186
|
+
const sub = toks[1];
|
|
187
|
+
if (sub === 'batch' || sub === '--help' || sub === '-h' || sub === '--doc')
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
// All-skill chain confirmed. Execute sequentially, short-circuiting on
|
|
191
|
+
// first non-zero exit.
|
|
192
|
+
const stdoutParts = [];
|
|
193
|
+
let lastResult = { handled: true, stdout: '', stderr: '', exitCode: 0 };
|
|
194
|
+
for (const seg of segments) {
|
|
195
|
+
const r = await tryKernel(seg);
|
|
196
|
+
if (!r.handled) {
|
|
197
|
+
// Defensive — pre-flight should have caught this. Bail to shell.
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (r.stdout)
|
|
201
|
+
stdoutParts.push(r.stdout);
|
|
202
|
+
lastResult = r;
|
|
203
|
+
if (r.exitCode !== 0) {
|
|
204
|
+
return {
|
|
205
|
+
handled: true,
|
|
206
|
+
stdout: stdoutParts.join('\n'),
|
|
207
|
+
stderr: r.stderr,
|
|
208
|
+
exitCode: r.exitCode,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
handled: true,
|
|
214
|
+
stdout: stdoutParts.join('\n'),
|
|
215
|
+
stderr: lastResult.stderr,
|
|
216
|
+
exitCode: 0,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function parseArgs(rest) {
|
|
220
|
+
const positionals = [];
|
|
221
|
+
const flags = {};
|
|
222
|
+
for (let i = 0; i < rest.length; i++) {
|
|
223
|
+
const tok = rest[i];
|
|
224
|
+
// --flag=value
|
|
225
|
+
if (tok.startsWith('--')) {
|
|
226
|
+
const eq = tok.indexOf('=');
|
|
227
|
+
if (eq > 0) {
|
|
228
|
+
const key = tok.slice(2, eq);
|
|
229
|
+
flags[key] = tok.slice(eq + 1);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
// --flag (bare). Either consume next non-flag token as value, or
|
|
233
|
+
// treat as boolean true.
|
|
234
|
+
const key = tok.slice(2);
|
|
235
|
+
const next = rest[i + 1];
|
|
236
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
237
|
+
flags[key] = next;
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
flags[key] = true;
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Plain positional
|
|
246
|
+
positionals.push(tok);
|
|
247
|
+
}
|
|
248
|
+
return { positionals, flags };
|
|
249
|
+
}
|
|
250
|
+
// ── Help renderers ──────────────────────────────────────────────────
|
|
251
|
+
/**
|
|
252
|
+
* `yome-skills` (global) — list all installed hub skills.
|
|
253
|
+
*/
|
|
254
|
+
function renderGlobalHelp() {
|
|
255
|
+
const all = getInstalledFast().filter((s) => s.status === 'enabled');
|
|
256
|
+
if (all.length === 0) {
|
|
257
|
+
return [
|
|
258
|
+
'No hub skills installed.',
|
|
259
|
+
'',
|
|
260
|
+
'Install one with:',
|
|
261
|
+
' yome skill install github:<owner>/<repo>',
|
|
262
|
+
'Browse the hub:',
|
|
263
|
+
' https://yome.work/skills',
|
|
264
|
+
].join('\n');
|
|
265
|
+
}
|
|
266
|
+
const lines = [];
|
|
267
|
+
lines.push('Installed hub skills (use `<domain> --help` for actions):');
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push(['DOMAIN', 'SLUG', 'VERSION', 'DESCRIPTION'].join('\t'));
|
|
270
|
+
for (const s of all) {
|
|
271
|
+
lines.push([s.domain ?? '?', s.slug, s.version ?? '?', s.description ?? ''].join('\t'));
|
|
272
|
+
}
|
|
273
|
+
return lines.join('\n');
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* `<domain> --help` — Layer-2 signature documentation.
|
|
277
|
+
*
|
|
278
|
+
* Resolution order:
|
|
279
|
+
* 1. Skill repo's `SIGNATURE.md` (authored by the skill maintainer —
|
|
280
|
+
* this is what shows up to the LLM and should be hand-tuned for
|
|
281
|
+
* LLM consumption: one line per action, defaults inline, no
|
|
282
|
+
* hidden compression tricks).
|
|
283
|
+
* 2. Auto-generated from backends/macos/manifest.json args (so newly
|
|
284
|
+
* published skills always have *something* to show).
|
|
285
|
+
*
|
|
286
|
+
* We append a tiny footer about batch + --doc so the model knows those
|
|
287
|
+
* exist without the skill author having to remember.
|
|
288
|
+
*/
|
|
289
|
+
function renderDomainHelp(skill) {
|
|
290
|
+
const sigPath = join(skill.installedAt, 'SIGNATURE.md');
|
|
291
|
+
let body;
|
|
292
|
+
if (existsSync(sigPath)) {
|
|
293
|
+
try {
|
|
294
|
+
body = readFileSync(sigPath, 'utf-8').trim();
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
body = renderAutoSignature(skill);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
body = renderAutoSignature(skill);
|
|
302
|
+
}
|
|
303
|
+
const footer = [
|
|
304
|
+
'',
|
|
305
|
+
`--- batch + docs ---`,
|
|
306
|
+
`${skill.domain} batch [--keep-going] [--merge] <<EOF\\n<cmd1>\\n<cmd2>\\nEOF`,
|
|
307
|
+
`${skill.domain} --doc list available templates / cookbooks`,
|
|
308
|
+
`${skill.domain} --doc <name> read one template`,
|
|
309
|
+
`${skill.domain} <action> --help per-action argument detail`,
|
|
310
|
+
].join('\n');
|
|
311
|
+
return body + '\n' + footer;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Fallback when the skill repo doesn't ship a SIGNATURE.md. We render
|
|
315
|
+
* a usable signature from the backend manifest — verbose but always works.
|
|
316
|
+
* Skill authors are encouraged to hand-write SIGNATURE.md to override this.
|
|
317
|
+
*/
|
|
318
|
+
function renderAutoSignature(skill) {
|
|
319
|
+
const manifest = readManifest(skill.installedAt);
|
|
320
|
+
const backend = loadMacosBackend(skill.installedAt);
|
|
321
|
+
const lines = [];
|
|
322
|
+
lines.push(`${skill.domain} — ${skill.name ?? skill.slug} (${skill.slug} v${skill.version ?? '?'})`);
|
|
323
|
+
if (skill.description)
|
|
324
|
+
lines.push(skill.description);
|
|
325
|
+
lines.push('');
|
|
326
|
+
// Build a one-line-per-action signature from the manifest args.
|
|
327
|
+
if (backend) {
|
|
328
|
+
for (const [action, spec] of Object.entries(backend.actions)) {
|
|
329
|
+
const positionals = [];
|
|
330
|
+
const flags = [];
|
|
331
|
+
for (const a of spec.args ?? []) {
|
|
332
|
+
const isPositional = a.from.split('|').some((s) => s.trim() === 'positional');
|
|
333
|
+
if (isPositional) {
|
|
334
|
+
positionals.push(a.required ? `<${a.name}>` : `[${a.name}]`);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
const flagName = a.from.split('|').map((s) => s.trim()).find((s) => s.startsWith('--')) ?? `--${a.name}`;
|
|
338
|
+
if (a.type === 'bool') {
|
|
339
|
+
flags.push(`[${flagName}]`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
flags.push(a.required ? `${flagName}=<${a.name}>` : `[${flagName}=<${a.name}>]`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const tail = [...positionals, ...flags].join(' ');
|
|
347
|
+
// Pull description from manifest.commands when available.
|
|
348
|
+
let desc = '';
|
|
349
|
+
if (manifest && Array.isArray(manifest.commands)) {
|
|
350
|
+
for (const c of manifest.commands) {
|
|
351
|
+
if (c.action === action && c.desc) {
|
|
352
|
+
desc = ` # ${c.desc}`;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
lines.push(` ${skill.domain} ${action}${tail ? ' ' + tail : ''}${desc}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
lines.push(' (no backend installed for this platform)');
|
|
362
|
+
}
|
|
363
|
+
return lines.join('\n');
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* `<domain> <action> --help` — list args for one action by reading the
|
|
367
|
+
* macOS backend manifest. Falls back to a generic message when there's
|
|
368
|
+
* no backend installed.
|
|
369
|
+
*/
|
|
370
|
+
function renderActionHelp(skill, action) {
|
|
371
|
+
const backend = loadMacosBackend(skill.installedAt);
|
|
372
|
+
const manifest = readManifest(skill.installedAt);
|
|
373
|
+
// Pull the human-friendly description from manifest.commands when available.
|
|
374
|
+
let desc;
|
|
375
|
+
if (manifest && Array.isArray(manifest.commands)) {
|
|
376
|
+
for (const c of manifest.commands) {
|
|
377
|
+
if (c.action === action) {
|
|
378
|
+
desc = c.desc;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!backend) {
|
|
384
|
+
return `${skill.domain} ${action} — no macOS backend installed for this skill.`;
|
|
385
|
+
}
|
|
386
|
+
const spec = backend.actions[action];
|
|
387
|
+
if (!spec) {
|
|
388
|
+
const known = Object.keys(backend.actions).join(', ');
|
|
389
|
+
return `Unknown action: ${skill.domain} ${action}\nAvailable: ${known}`;
|
|
390
|
+
}
|
|
391
|
+
const lines = [];
|
|
392
|
+
lines.push(`${skill.domain} ${action}${desc ? ' — ' + desc : ''}`);
|
|
393
|
+
lines.push('');
|
|
394
|
+
const positionalArgs = (spec.args ?? []).filter((a) => a.from.split('|').some((s) => s.trim() === 'positional'));
|
|
395
|
+
const flagArgs = (spec.args ?? []).filter((a) => a.from.split('|').some((s) => s.trim().startsWith('--')));
|
|
396
|
+
if (positionalArgs.length > 0) {
|
|
397
|
+
lines.push('POSITIONAL');
|
|
398
|
+
for (const a of positionalArgs) {
|
|
399
|
+
const req = a.required ? ' (required)' : '';
|
|
400
|
+
const def = a.default !== undefined && a.default !== '' && a.default !== false
|
|
401
|
+
? ` [default: ${String(a.default)}]`
|
|
402
|
+
: '';
|
|
403
|
+
lines.push(` <${a.name}>${req}${def}`);
|
|
404
|
+
}
|
|
405
|
+
lines.push('');
|
|
406
|
+
}
|
|
407
|
+
if (flagArgs.length > 0) {
|
|
408
|
+
lines.push('FLAGS');
|
|
409
|
+
for (const a of flagArgs) {
|
|
410
|
+
const req = a.required ? ' (required)' : '';
|
|
411
|
+
const def = a.default !== undefined && a.default !== '' && a.default !== false
|
|
412
|
+
? ` [default: ${String(a.default)}]`
|
|
413
|
+
: '';
|
|
414
|
+
const ty = a.type ? ` (${a.type})` : '';
|
|
415
|
+
// Pick the first --flag form for the display name (handles "positional|--path").
|
|
416
|
+
const flagName = a.from.split('|').map((s) => s.trim()).find((s) => s.startsWith('--')) ?? `--${a.name}`;
|
|
417
|
+
lines.push(` ${flagName}${ty}${req}${def}`);
|
|
418
|
+
}
|
|
419
|
+
lines.push('');
|
|
420
|
+
}
|
|
421
|
+
if (spec.uses && spec.uses.length > 0) {
|
|
422
|
+
lines.push(`Capabilities: ${spec.uses.join(', ')}`);
|
|
423
|
+
}
|
|
424
|
+
return lines.join('\n');
|
|
425
|
+
}
|
|
426
|
+
function listDocs(skill) {
|
|
427
|
+
const docDir = join(skill.installedAt, 'docs');
|
|
428
|
+
if (!existsSync(docDir))
|
|
429
|
+
return [];
|
|
430
|
+
const entries = [];
|
|
431
|
+
let names = [];
|
|
432
|
+
try {
|
|
433
|
+
names = readdirSync(docDir);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
for (const file of names) {
|
|
439
|
+
if (!file.toLowerCase().endsWith('.md'))
|
|
440
|
+
continue;
|
|
441
|
+
const full = join(docDir, file);
|
|
442
|
+
try {
|
|
443
|
+
const raw = readFileSync(full, 'utf-8');
|
|
444
|
+
const fm = parseFrontmatter(raw);
|
|
445
|
+
const fallbackName = file.replace(/\.md$/i, '');
|
|
446
|
+
entries.push({
|
|
447
|
+
name: fm?.name ?? fallbackName,
|
|
448
|
+
label: fm?.label ?? fm?.name ?? fallbackName,
|
|
449
|
+
summary: fm?.summary ?? '',
|
|
450
|
+
tags: fm?.tags ?? [],
|
|
451
|
+
path: full,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
catch { /* skip unreadable files */ }
|
|
455
|
+
}
|
|
456
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
457
|
+
}
|
|
458
|
+
function renderDocList(skill) {
|
|
459
|
+
const docs = listDocs(skill);
|
|
460
|
+
if (docs.length === 0) {
|
|
461
|
+
return [
|
|
462
|
+
`${skill.domain} — no templates / docs available.`,
|
|
463
|
+
`(skill maintainer hasn't shipped a docs/ folder)`,
|
|
464
|
+
].join('\n');
|
|
465
|
+
}
|
|
466
|
+
const lines = [];
|
|
467
|
+
lines.push(`${skill.domain} — ${docs.length} template(s) available. Read one with \`${skill.domain} --doc <name>\`.`);
|
|
468
|
+
lines.push('');
|
|
469
|
+
lines.push(['NAME', 'LABEL', 'SUMMARY'].join('\t'));
|
|
470
|
+
for (const d of docs) {
|
|
471
|
+
lines.push([d.name, d.label, d.summary].join('\t'));
|
|
472
|
+
}
|
|
473
|
+
return lines.join('\n');
|
|
474
|
+
}
|
|
475
|
+
function renderDocOne(skill, name) {
|
|
476
|
+
const docs = listDocs(skill);
|
|
477
|
+
const doc = docs.find((d) => d.name === name)
|
|
478
|
+
?? docs.find((d) => d.name.toLowerCase() === name.toLowerCase())
|
|
479
|
+
?? docs.find((d) => d.label === name);
|
|
480
|
+
if (!doc) {
|
|
481
|
+
const known = docs.map((d) => d.name).join(', ') || '(none)';
|
|
482
|
+
return `${skill.domain} --doc: unknown template '${name}'.\nAvailable: ${known}`;
|
|
483
|
+
}
|
|
484
|
+
// Strip the frontmatter on output — the body is what the LLM consumes.
|
|
485
|
+
const raw = readFileSync(doc.path, 'utf-8');
|
|
486
|
+
const body = stripFrontmatter(raw);
|
|
487
|
+
return `# ${doc.label}\n\n${body.trim()}\n\n— from ${skill.domain} --doc ${doc.name}`;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Parse a `--- ... ---` YAML-ish frontmatter block. We only support a
|
|
491
|
+
* tiny subset (key: value, key: [v1, v2]) — enough for skill docs and
|
|
492
|
+
* trivially diffable. Returns null when the file has no frontmatter.
|
|
493
|
+
*/
|
|
494
|
+
function parseFrontmatter(raw) {
|
|
495
|
+
const m = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
496
|
+
if (!m)
|
|
497
|
+
return null;
|
|
498
|
+
const obj = {};
|
|
499
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
500
|
+
const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
|
|
501
|
+
if (!kv)
|
|
502
|
+
continue;
|
|
503
|
+
const key = kv[1].trim();
|
|
504
|
+
let val = kv[2].trim();
|
|
505
|
+
// tiny array literal: [a, b, "c d"]
|
|
506
|
+
if (typeof val === 'string' && val.startsWith('[') && val.endsWith(']')) {
|
|
507
|
+
val = val
|
|
508
|
+
.slice(1, -1)
|
|
509
|
+
.split(',')
|
|
510
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
511
|
+
.filter(Boolean);
|
|
512
|
+
}
|
|
513
|
+
else if (typeof val === 'string') {
|
|
514
|
+
val = val.replace(/^["']|["']$/g, '');
|
|
515
|
+
}
|
|
516
|
+
obj[key] = val;
|
|
517
|
+
}
|
|
518
|
+
if (typeof obj.name !== 'string')
|
|
519
|
+
return null;
|
|
520
|
+
return {
|
|
521
|
+
name: obj.name,
|
|
522
|
+
label: typeof obj.label === 'string' ? obj.label : undefined,
|
|
523
|
+
summary: typeof obj.summary === 'string' ? obj.summary : undefined,
|
|
524
|
+
tags: Array.isArray(obj.tags) ? obj.tags : undefined,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function stripFrontmatter(raw) {
|
|
528
|
+
return raw.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '');
|
|
529
|
+
}
|
|
530
|
+
function renderBatchHelp(skill) {
|
|
531
|
+
return [
|
|
532
|
+
`${skill.domain} batch — run several actions in a single Bash call`,
|
|
533
|
+
``,
|
|
534
|
+
`USAGE`,
|
|
535
|
+
` ${skill.domain} batch [flags] <<TAG`,
|
|
536
|
+
` <cmd1>`,
|
|
537
|
+
` <cmd2>`,
|
|
538
|
+
` ...`,
|
|
539
|
+
` TAG`,
|
|
540
|
+
``,
|
|
541
|
+
`or simple newline form:`,
|
|
542
|
+
` ${skill.domain} batch [flags]`,
|
|
543
|
+
` <cmd1>`,
|
|
544
|
+
` <cmd2>`,
|
|
545
|
+
``,
|
|
546
|
+
`FLAGS`,
|
|
547
|
+
` --keep-going don't stop on first failure (default: fail-fast)`,
|
|
548
|
+
` --merge render all sub-commands as one osascript invocation`,
|
|
549
|
+
` (5-10x faster; skips Launch Services actions like 'open')`,
|
|
550
|
+
``,
|
|
551
|
+
`BODY RULES`,
|
|
552
|
+
` - One sub-command per line, NO domain prefix`,
|
|
553
|
+
` - '# inline comments' and blank lines are skipped`,
|
|
554
|
+
` - Quotes (\"...\" or '...') work the same as single-line commands`,
|
|
555
|
+
` - Heredoc tag can be any word: <<EOF / <<BATCH / <<DONE`,
|
|
556
|
+
``,
|
|
557
|
+
`EXAMPLE`,
|
|
558
|
+
` ${skill.domain} batch <<EOF`,
|
|
559
|
+
` new ~/Desktop/x.pptx`,
|
|
560
|
+
` title 1 --text=\"Hello\"`,
|
|
561
|
+
` slide.add`,
|
|
562
|
+
` save`,
|
|
563
|
+
` EOF`,
|
|
564
|
+
].join('\n');
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Detects + parses the two batch forms. Returns null when the line is
|
|
568
|
+
* NOT a batch invocation; returns a populated object otherwise (even on
|
|
569
|
+
* malformed heredoc — the runner will turn that into a useful error so
|
|
570
|
+
* we don't silently fall through to /bin/sh).
|
|
571
|
+
*/
|
|
572
|
+
function parseBatchCommand(commandLine) {
|
|
573
|
+
const normalised = commandLine.replace(/\r\n/g, '\n').trim();
|
|
574
|
+
// Heredoc form. First line: `<domain> batch [flags] <<TAG`
|
|
575
|
+
// Body until matching TAG on its own line.
|
|
576
|
+
const heredoc = normalised.match(/^(\w+)\s+batch\b([^\n]*?)<<(\w+)[ \t]*\n([\s\S]*?)\n[ \t]*\3[ \t]*$/);
|
|
577
|
+
if (heredoc) {
|
|
578
|
+
const [, domain, headFlags, , body] = heredoc;
|
|
579
|
+
return { domain, body, flags: parseBatchFlags(headFlags) };
|
|
580
|
+
}
|
|
581
|
+
// Simple newline form. First line: `<domain> batch [flags]`. Body = rest.
|
|
582
|
+
const simple = normalised.match(/^(\w+)\s+batch\b([^\n]*)\n([\s\S]+)$/);
|
|
583
|
+
if (simple) {
|
|
584
|
+
const [, domain, headFlags, body] = simple;
|
|
585
|
+
return { domain, body, flags: parseBatchFlags(headFlags) };
|
|
586
|
+
}
|
|
587
|
+
// Bare `<domain> batch` with no body — likely a typo. Claim it so we
|
|
588
|
+
// can surface a help message rather than running it as a real shell
|
|
589
|
+
// command (which would just fail with `batch: command not found`).
|
|
590
|
+
const bare = normalised.match(/^(\w+)\s+batch\b([^\n]*)$/);
|
|
591
|
+
if (bare) {
|
|
592
|
+
const [, domain, headFlags] = bare;
|
|
593
|
+
return { domain, body: '', flags: parseBatchFlags(headFlags) };
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
function parseBatchFlags(headRest) {
|
|
598
|
+
const tokens = tokenize(headRest.trim());
|
|
599
|
+
return {
|
|
600
|
+
keepGoing: tokens.includes('--keep-going') || tokens.includes('-k'),
|
|
601
|
+
merge: tokens.includes('--merge'),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Strip a trailing inline `# comment` while respecting quotes.
|
|
606
|
+
* `#` is only a comment marker at line start or after whitespace, so
|
|
607
|
+
* `--color=#FF0000` doesn't get truncated.
|
|
608
|
+
*/
|
|
609
|
+
function stripInlineComment(line) {
|
|
610
|
+
let inQuote = null;
|
|
611
|
+
for (let i = 0; i < line.length; i++) {
|
|
612
|
+
const c = line[i];
|
|
613
|
+
if (inQuote) {
|
|
614
|
+
if (c === inQuote)
|
|
615
|
+
inQuote = null;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (c === '"' || c === "'") {
|
|
619
|
+
inQuote = c;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) {
|
|
623
|
+
return line.slice(0, i).trimEnd();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return line;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Tokenise + group each non-empty body line into a sub-command. We don't
|
|
630
|
+
* validate against the manifest here — the dispatcher does that, and we
|
|
631
|
+
* want the same error reporting whether the call came from batch or a
|
|
632
|
+
* single line.
|
|
633
|
+
*/
|
|
634
|
+
function splitBatchBody(body) {
|
|
635
|
+
const out = [];
|
|
636
|
+
for (const rawLine of body.split('\n')) {
|
|
637
|
+
const stripped = stripInlineComment(rawLine).trim();
|
|
638
|
+
if (!stripped)
|
|
639
|
+
continue;
|
|
640
|
+
if (stripped.startsWith('#'))
|
|
641
|
+
continue;
|
|
642
|
+
const tokens = tokenize(stripped);
|
|
643
|
+
if (tokens.length === 0)
|
|
644
|
+
continue;
|
|
645
|
+
const action = tokens[0];
|
|
646
|
+
// Reuse the same argv parser we use for the single-command path.
|
|
647
|
+
const { positionals, flags } = parseArgs(tokens.slice(1));
|
|
648
|
+
out.push({ action, positionals, flags, raw: stripped });
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
async function runBatch(b) {
|
|
653
|
+
const installed = getInstalledFast().filter((s) => s.status === 'enabled');
|
|
654
|
+
const skill = installed.find((s) => s.domain === b.domain);
|
|
655
|
+
if (!skill) {
|
|
656
|
+
return err(`batch: unknown domain '${b.domain}' (no installed skill owns it)`, 127);
|
|
657
|
+
}
|
|
658
|
+
if (!b.body.trim()) {
|
|
659
|
+
return err(`batch: empty body. Usage:\n` +
|
|
660
|
+
` ${b.domain} batch <<EOF\n <cmd1>\n <cmd2>\n EOF\n` +
|
|
661
|
+
`or\n` +
|
|
662
|
+
` ${b.domain} batch\n <cmd1>\n <cmd2>\n` +
|
|
663
|
+
`Flags: --keep-going (don't stop on first failure), --merge (single osascript run)`, 2);
|
|
664
|
+
}
|
|
665
|
+
const subs = splitBatchBody(b.body);
|
|
666
|
+
if (subs.length === 0) {
|
|
667
|
+
return err(`batch: no executable commands found (only blank lines / comments?)`, 2);
|
|
668
|
+
}
|
|
669
|
+
if (b.flags.merge) {
|
|
670
|
+
return runBatchMerged(skill, subs, b.flags.keepGoing);
|
|
671
|
+
}
|
|
672
|
+
// Default: simple loop, fail-fast unless --keep-going.
|
|
673
|
+
const lines = [];
|
|
674
|
+
let lastErr = '';
|
|
675
|
+
let failed = 0;
|
|
676
|
+
for (let i = 0; i < subs.length; i++) {
|
|
677
|
+
const s = subs[i];
|
|
678
|
+
const r = await invokeSkill({
|
|
679
|
+
slugOrDomain: skill.slug,
|
|
680
|
+
action: s.action,
|
|
681
|
+
positionals: s.positionals,
|
|
682
|
+
flags: s.flags,
|
|
683
|
+
});
|
|
684
|
+
if (r.exitCode !== 0) {
|
|
685
|
+
failed++;
|
|
686
|
+
lastErr = `[line ${i + 1}: ${s.raw}] ${r.stderr || r.stdout || `exit ${r.exitCode}`}`;
|
|
687
|
+
lines.push(`✗ ${s.action}: ${r.stderr.trim() || r.stdout.trim() || `exit ${r.exitCode}`}`);
|
|
688
|
+
if (!b.flags.keepGoing)
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
const txt = (r.stdout || '').trim();
|
|
693
|
+
lines.push(`✓ ${s.action}${txt ? ': ' + txt : ''}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const stdout = lines.join('\n');
|
|
697
|
+
const summary = failed === 0
|
|
698
|
+
? `\n— batch ok (${subs.length} commands)`
|
|
699
|
+
: `\n— batch finished: ${subs.length - failed}/${subs.length} ok, ${failed} failed`;
|
|
700
|
+
return {
|
|
701
|
+
handled: true,
|
|
702
|
+
stdout: stdout + summary,
|
|
703
|
+
stderr: lastErr,
|
|
704
|
+
exitCode: failed === 0 ? 0 : 1,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* --merge path: render every sub-command's AppleScript template, splice
|
|
709
|
+
* them into a single source, and call osascript once. We import lazily
|
|
710
|
+
* (and from the dispatcher module so the merged path stays in one file).
|
|
711
|
+
*/
|
|
712
|
+
async function runBatchMerged(skill, subs, keepGoing) {
|
|
713
|
+
const { dispatchMacosBatchMerged } = await import('./dispatcher.js');
|
|
714
|
+
const r = await dispatchMacosBatchMerged(skill.installedAt, subs.map((s) => ({ action: s.action, call: { positionals: s.positionals, flags: s.flags } })), { keepGoing });
|
|
715
|
+
return {
|
|
716
|
+
handled: true,
|
|
717
|
+
stdout: r.stdout,
|
|
718
|
+
stderr: r.stderr,
|
|
719
|
+
exitCode: r.exitCode,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
// ── small helpers ───────────────────────────────────────────────────
|
|
723
|
+
function ok(out) {
|
|
724
|
+
return { handled: true, stdout: out, stderr: '', exitCode: 0 };
|
|
725
|
+
}
|
|
726
|
+
function err(msg, code = 1) {
|
|
727
|
+
return { handled: true, stdout: '', stderr: msg, exitCode: code };
|
|
728
|
+
}
|
|
729
|
+
function notHandled() {
|
|
730
|
+
return { handled: false, stdout: '', stderr: '', exitCode: 0 };
|
|
731
|
+
}
|
|
732
|
+
//# sourceMappingURL=kernel.js.map
|