đŹđ§ English version
Commandes CLI TypeScript
Référence utilisateur du sous-systÚme
Orkeon.Cli.Commands.Scripting. Pour la spécification d'architecture et la justification de conception, voir l'archive de conception des mainteneurs (featurecli-ts-commands, SPEC).
De quoi s'agit-il
Un moyen d'ajouter des commandes REPL interactives Ă un runner CLI Orkeon en
dĂ©posant des fichiers *.cmd.ts dans un dossier â pas de recompilation .NET, pas
de redĂ©marrage de l'hĂŽte au-delĂ du runner lui-mĂȘme. Chaque script dĂ©clare une ou
plusieurs commandes via le helper global defineCommand({...}) ; le chargeur les
détecte au démarrage, les valide, et les expose aux cÎtés des commandes intégrées
help/exit/clear.
Les commandes peuvent aussi dispatcher du travail vers des agents vivants â en synchrone (bloquer et retourner la rĂ©ponse) ou en asynchrone (Ă©mettre un ticket, rĂ©agir quand l'agent rĂ©pond). Voir Dispatcher des commandes vers des agents.
Démarrage rapide
mkdir -p ./commands
cat > ./commands/hello.cmd.ts <<'EOF'
defineCommand({
name: "hello",
description: "Say hello to the world (or to someone in particular).",
args: {
who: { type: "string", default: "world" },
},
async handler(args, ctx) {
ctx.log.info(`Hello, ${args.who}!`);
return ctx.continue();
},
});
EOF
dotnet run --project src/apps/Orkeon.ConsoleApp -- \
--runner=scripted-commands \
--commands-dir ./commands
Ă l'invite :
scripted> help # 'hello' shows up
scripted> hello # â Hello, world!
scripted> hello --who=Ada # â Hello, Ada!
scripted> help-cmd hello # signature with typed args
scripted> exit
Options CLI
| Option | Effet |
|---|---|
--runner=scripted-commands |
Démarre directement dans le REPL scripted-commands au lieu du menu principal. |
--commands-dir <path> |
Ajoute un répertoire à scanner (répétable ; monté comme /cli-commands[-N] dans le VFS). |
--no-script-commands |
Désactive entiÚrement la découverte. Le registre est vide. |
--strict-commands |
Ăquivalent Ă FailFastOnInvalidScript=true + ContinueOnConflict=false (CI). |
--settings <chemin> |
Chemin d'appsettings explicite (rĂ©pĂ©table â les fichiers suivants surchargent les prĂ©cĂ©dents). |
--mount <phys:virt:droits> |
Ajoute un montage VFS (rĂ©pĂ©table) â p. ex. pour rendre un dossier de commandes accessible. |
--crews-dir <chemin> |
Ajoute un rĂ©pertoire de rĂ©solution de crews (rĂ©pĂ©table) â ce qui rend script-host / runCrewAsync("nom") rĂ©soluble. |
appsettings.json
Les mĂȘmes options sous Orkeon:Cli:ScriptCommands :
{
"Orkeon": {
"Cli": {
"ScriptCommands": {
"Enabled": true,
"Directories": ["/cli-commands"],
"FailFastOnInvalidScript": false,
"EsbuildTranspile": true,
"MaxScripts": 50,
"ContinueOnConflict": true,
"FallbackCommandName": "assistant",
"Limits": {
"MemoryLimitBytes": 67108864,
"RecursionLimit": 100,
"ExecutionTimeout": "00:05:00"
}
}
}
}
}
Les rĂ©pertoires sont des chemins virtuels rĂ©solus via IFileSystemService â
configurez une entrĂ©e Orkeon:FileSystem:Mounts si le rĂ©pertoire n'est pas dĂ©jĂ
monté. FallbackCommandName (défaut assistant) route toute ligne REPL non
prĂ©fixĂ©e de / vers cette commande scriptĂ©e â l'interrupteur qui fait du REPL un
agent conversationnel.
Référence defineCommand
defineCommand({
name: "deploy", // ^[a-z][a-z0-9-]*$, must not collide with help/exit/clear
aliases: ["d", "ship"], // optional, must not duplicate `name`
description: "Deploy a crew.", // single line, †200 chars
args: {
target: { type: "string", required: true, choices: ["dev", "prod"] },
crew: { type: "string", required: true },
dryRun: { type: "boolean", default: false },
},
async handler(args, ctx) {
ctx.log.info(`Deploying ${args.crew} to ${args.target}`);
return ctx.continue();
},
});
Quand args est omis, le handler reçoit { raw: string[] } Ă la place â la
signature (args, ctx) => ... reste stable entre commandes typées et non typées.
Types d'arguments supportés
type |
Notes |
|---|---|
"string" |
choices: readonly string[] optionnel, default optionnel. |
"number" |
min, max, default optionnels. |
"boolean" |
Un --flag nu â true. default optionnel. |
"string[]" |
Glouton : en positionnel, consomme la fin de la ligne ; en forme drapeau, consomme jusqu'au prochain --key. |
Les erreurs de validation remontent sous la forme d'un Error: ... imprimé par le
runner ; le handler n'est pas invoqué.
Référence ctx (le contexte d'exécution)
interface CommandRuntimeContext {
readonly command: { readonly name: string; readonly rawInput: string };
readonly log: {
debug(msg: string, data?: object): void;
info(msg: string, data?: object): void;
warn(msg: string, data?: object): void;
error(msg: string, data?: object): void;
};
write(text: string): void;
writeLine(text: string): void;
clear(): void;
prompt(spec: PromptSpec): Promise<string | boolean>;
progress(spec: { total?: number; label: string }): ProgressHandle;
table<T extends object>(rows: readonly T[], columns?: readonly (keyof T)[]): void;
readonly signal: CommandSignal; // CancellationToken â see "Cancellation"
readonly services: ServiceLocator; // whitelisted
continue(message?: string): CommandActionResult;
exit(farewell?: string): CommandActionResult;
}
Le .d.ts complet est livré comme ressource embarquée dans
Orkeon.Cli.Commands.Scripting.dll (la Phase 5 le publiera automatiquement sur disque ;
pour l'instant, copiez src/cli/Orkeon.Cli.Commands.Scripting/Typings/orkeon-cli.d.ts Ă
cÎté de vos scripts pour l'autocomplétion IDE).
Prompts
const target = await ctx.prompt({ type: "select", message: "Target?", choices: ["dev","prod"] });
const ok = await ctx.prompt({ type: "confirm", message: "Proceed?", default: false });
const name = await ctx.prompt({ type: "text", message: "Name?" });
const pwd = await ctx.prompt({ type: "password", message: "Password:" });
Annulation
ctx.signal est le CancellationToken .NET brut exposé par Jint. Les propriétés
sont en PascalCase à cause de la réflexion :
while (!ctx.signal.IsCancellationRequested) {
// long-running work
}
ctx.signal.ThrowIfCancellationRequested();
L'alias TypeScript CommandSignal déclaré dans le .d.ts est une commodité de
documentation ; les membres au runtime sont en PascalCase.
Services
const fs = ctx.services.get<IFileSystemService>("fs");
const cfg = ctx.services.get<IConfiguration>("configuration");
const tools = ctx.services.get<IBaseTool[]>("tools");
La whitelist de l'hÎte détermine ce qui est accessible. Clés par défaut : fs,
configuration, tools, plus en option llm, logger, commands (la façade
de dispatch â voir plus bas) et script-host (lancement de crews depuis une
commande â voir scripting). Les hĂŽtes ajoutent les leurs en
passant une Action<ScriptServiceWhitelist> Ă AddScriptCommands.
Dispatcher des commandes vers des agents
Une commande peut s'adresser Ă un agent vivant par son nom et laisser sa
réponse décider quand la commande est terminée. C'est la façade commands,
accessible via ctx.services.get("commands"). Sous le capot, elle s'appuie sur
l'IAgentChannel existant : la façade rĂ©sout le nom d'agent â AgentId, corrĂšle
la requĂȘte/rĂ©ponse cĂŽtĂ© hĂŽte, et trace chaque dispatch dans un registre
interrogeable.
RĂšgle clĂ© : la commande se termine quand l'agent rĂ©pond, pas quand le handler retourne. Le routage est point-Ă -point â une commande cible exactement un agent, qui est son unique finisseur (pas de fan-out, pas de join).
CĂŽtĂ© agent â onCommand
Un agent déclare qu'il répond aux commandes dispatchées avec onCommand (dans le
DSL crew â voir scripting.md). La valeur qu'il
retourne est la réponse qui termine la commande :
const echo = agentBuilder()
.name("echo").role("Echo").goal("Echo a payload back")
.onCommand("run", (env) => env.payload.toUpperCase()) // answers intent "run"
.onCommand((env) => ({ success: true, payload: "ack" })) // catch-all (any intent)
.build();
Le handler reçoit une enveloppe { intent, payload, from, correlationId } et
retourne soit une chaĂźne de payload, soit { success?, payload?, error? }.
L'agent doit ĂȘtre activĂ© sur le bus de dispatch (AgentCommandRegistrar) pour
qu'une commande .cmd.ts puisse l'atteindre par son nom. Le JS du handler
s'exĂ©cute sous le verrou moteur de l'agent ; il est donc sĂ»r mĂȘme quand un
dispatch asynchrone d'arriĂšre-plan l'invoque.
Dispatch synchrone â request
Une defineCommand normale qui attend l'agent. request bloque jusqu'Ă ce que
l'agent réponde et retourne la CommandResponse (de sorte qu'un await dessus se
rĂ©sout en la valeur â une commande sync est faite pour bloquer) :
defineCommand({
name: "ask",
description: "Ask the 'echo' agent and wait.",
args: { text: { type: "string", required: true } },
handler(args, ctx) {
const res = ctx.services.get("commands").request("echo", "run", args.text);
return res.success ? ctx.continue("â " + res.payload)
: ctx.continue("agent error: " + res.error);
},
});
request(agent, intent, payload) retourne { agent, intent, success, payload, error? }.
Dispatch asynchrone â defineAsyncCommand
Utilisez defineAsyncCommand quand le travail doit se détacher : dispatch tire
et rend l'invite immédiatement ; le completed optionnel est rejoué plus tard
quand l'agent répond.
defineAsyncCommand({
name: "ask-bg",
description: "Ask the 'echo' agent in the background.",
maxConcurrent: 3, // admission quota (see below)
args: { text: { type: "string", required: true } },
dispatch(args, ctx) { // does NOT block
const ticket = ctx.services.get("commands").post("echo", "run", args.text);
ctx.log.info("launched (ticket " + ticket + ")");
return { ticket };
},
completed(result, ctx) { // replayed at the next pump
ctx.writeLine("â " + result.agent + ": " + result.payload);
},
});
post(agent, intent, payload)retourne immĂ©diatement un ticket (string) et exĂ©cute la requĂȘte sur une tĂąche d'arriĂšre-plan.completed(result, ctx)n'est pas appelĂ© depuis le thread d'arriĂšre-plan (Jint est mono-thread). Il est rejouĂ© sur le thread moteur au prochain « pump » â typiquement la prochaine invocation de commande sur le mĂȘme script. Une ligne hĂŽte concise est aussi imprimĂ©e immĂ©diatement Ă la complĂ©tion, pour que vous voyiez quelque chose sans attendre.- Pour une valeur Ă lire Ă la demande, sondez avec
result --ticket=âŠ(ci-dessous).
Quota d'admission â maxConcurrent
Déclaré sur defineAsyncCommand, il borne le nombre d'instances en vol de cette
commande. Omis â non bornĂ© (â). L'acquisition est non bloquante : quand le quota
est plein, une nouvelle invocation est rejetée immédiatement (le dispatch ne
s'exécute jamais) avec un message Rejected: quota of N instance(s) of '<cmd>' reached.
Le slot est libéré quand l'agent répond. maxConcurrent n'a d'effet réel que pour
les commandes async â une commande sync tient dĂ©jĂ le moteur et est sĂ©rialisĂ©e.
Introspection des commandes en vol
Le registre de dispatch est exposé de deux maniÚres. Depuis un script :
const facade = ctx.services.get("commands");
facade.list({ state: "running" }); // CommandInstanceView[]
facade.get("t3"); // one view, or undefined
facade.cancel("t3"); // request cancellation; returns boolean
Et sous forme de commandes intégrées au REPL (rapides, restent réactives pendant que le travail async tourne en arriÚre-plan) :
| Commande | Effet |
|---|---|
ps [--state=âŠ] |
Liste les instances. Ătat : running (dĂ©faut), done, failed, cancelled, rejected, all. |
inspect --ticket=<t> |
Détail complet d'une instance (état, agent, durée écoulée, résultat, progression). |
result --ticket=<t> |
Imprime le payload résultat (sondage). Signale « still running » si pas terminé. |
cancel --ticket=<t> |
Demande l'annulation d'un ticket en vol. |
Une CommandInstanceView porte : ticket, name, kind (sync/async),
targetAgent, intent, correlationId, state, startedAt, completedAt?,
elapsedMs, result?, error?, progress?. Les entrées terminales sont
conservées un moment (borné) pour que result/inspect puissent lire un ticket
récent, puis évincées.
Cùblage (cÎté hÎte)
AddScriptCommands enregistre le substrat de dispatch comme singleton (son propre
canal in-memory â le bus de dispatch CLI â plus l'annuaire de noms et le registre
d'instances) et ajoute commands à la whitelist par défaut. Les agents sont
connectés avec
AgentCommandRegistrar.Register(agent, engine, engineLock, service.Channel, service.Directory),
qui enregistre le handler de canal et le mapping nomâid.
Exemple de bout en bout
examples/cli-ts-commands/ fournit dispatch.cmd.ts (les commandes ask /
ask-bg) et echo-agent.ork.ts (l'agent onCommand).
scripted> ask hello # â HELLO (sync, blocks)
scripted> ask-bg hello # launched (ticket t1) (async, returns now)
scripted> ps # t1 askbg echo running âŠ
scripted> result --ticket=t1 # [t1] HELLO
Matrice de support TypeScript (esbuild â Jint)
| Fonctionnalité | Support |
|---|---|
interface, type, enum (non-const) |
â |
import/export (chemins relatifs) |
â |
async/await, Promise, Promise.all |
â |
| DĂ©structuration, spread, valeurs par dĂ©faut, rest | â |
| Classes, getters/setters, hĂ©ritage | â |
| LittĂ©raux de gabarit (template literals) | â |
Map/Set/WeakMap/WeakSet |
â |
JSON.parse/JSON.stringify |
â |
| Regex (sans lookbehind) | â |
Modules npm (fs, path, node:*) |
â Utilisez ctx.services.get("fs"). |
fetch, setTimeout, setInterval |
â/â ïž |
Décorateurs (@experimental) |
â esbuild s'arrĂȘte au stage-3. |
Chemins/alias tsconfig |
â ïž imports relatifs uniquement. |
Contraintes (bonnes Ă connaĂźtre)
- DĂ©couverte au dĂ©marrage uniquement. Modifiez un script, redĂ©marrez le runner. Pas de hot reload (dĂ©libĂ©rĂ© â spec §14).
- Un moteur par script, gardĂ© chaud. Les invocations d'une mĂȘme commande partagent l'Ă©tat Jint ; les invocations entre commandes diffĂ©rentes sont isolĂ©es.
- Jint n'est pas thread-safe. Le runner sérialise les invocations ; un
SemaphoreSlimprotÚge chaque moteur défensivement. - Limites de sandbox (profil CLI) : 64 Mo de mémoire, 100 de récursion max,
5 min d'exécution. Surcharge via
Orkeon:Cli:ScriptCommands:Limits. - Politique de conflit : le premier script gagne, par ordre ordinal des
chemins ; le second est journalisé en Warning. Basculez
ContinueOnConflict=falsepour échouer vite. - VFS uniquement. Les scripts lisent le disque via
ctx.services.get("fs")â pas deSystem.IOdirect.
Dépannage
| SymptĂŽme | Cause probable | Action |
|---|---|---|
hello n'apparaĂźt pas dans help |
Script hors des Directories configurés, ou erreur d'évaluation journalisée en Error |
Vérifiez les logs de Orkeon.Cli.Commands.Scripting.ScriptCommandLoader. |
esbuild not found au démarrage |
Outil non installé | npm i -g esbuild, ou copie dans tools/scripting-esbuild/. |
defineCommand is not defined |
Script évalué avant les bindings (bug) | Ouvrez une issue avec le chemin du script. |
| Le prompt ne s'affiche pas dans le panneau REPL | Adaptateur autre que Terminal.Gui ou heuristique de préfixe manquée | Assurez-vous que le script écrit les prompts avec un suffixe > . |
Error: --target value 'staging' is not in choices [dev, prod] |
Faute de frappe ou choices obsolĂštes |
Utilisez help-cmd <name> pour voir la signature Ă jour. |