@tofe98/dsplug
v0.0.1
Published
dsplug v0 — gestisci plugin host DSH (add / remove / list / find / check / update)
Downloads
276
Maintainers
Readme
dsplug v0
CLI minima per gestire i plugin host di DeepSeek Harness, stile npx skills:
invoca dsh plugin --profile <name> <add|remove> <pkg> e tiene sincronizzato
$DSH_HOME/profiles/<name>/cordis.patch.yml.
Solo 6 comandi, solo plugin HOST.
Comandi
dsplug mostra header + lista comandi (fra due righe blu)
dsplug help header + Commands/Options (o -h / --help)
dsplug add <pkg|path> [--profile <name>] [--no-dsh]
dsplug remove <pkg> [--profile <name>] [--no-dsh]
dsplug list [--profile <name>]
dsplug find [query] [--local] [--category <name>] [--index <path>]
dsplug check [--profile <name>] [--index <path>]
dsplug update [--profile <name>] [--index <path>] [--no-dsh]--profile <name>— profilo DSH da toccare (default:web).--no-dsh— non invocare il binariodsh: solo edit del patch file (usato dai test; nessuna dipendenza da rete o dal binariodsh).--index <path>— indice plugin custom perfind,add,checkeupdate(default:index.jsonbundled nel package; pensato per test e per un futuro indice remoto). Infind, passare--indeximplica--local(un indice custom è per definizione locale).--local(solofind) — usa l'indice staticoindex.jsoninvece del catalogo remoto (comportamento storico difind, invariato).--category <name>(solofind) — filtra il catalogo remoto per categoria: substring case-insensitive sul nome della categoria (es.--category uimatchaUI Enhancements).--no-color— disattiva i colori ANSI (oppure envNO_COLOR; vedi Colori).- Il binario invocato si può sovrascrivere con l'env
DSPLUG_DSH(defaultdsh).
Contratto
- add — PRIMA di installare stampa l'avviso di sicurezza su stderr
(vedi Sicurezza), poi esegue
dsh plugin --profile <name> add <pkg>, poi appende la riga al patch file se non già presente e registra la entry nel lock (id -> { pkg, version }, con laversionletta dall'indice se il plugin vi compare, altrimenti'unknown'). Idempotente (seconda esecuzione = no-op). Guardia: il package viene normalizzato prima del check (viastripVersion, vedi sotto) e se il nome normalizzato compare nell'indice con"bundled": true(plugin già incluso nell'installazione DSH),addrifiuta con messaggio su stderr + exit 1, senza invocaredshe senza toccare patch o lock.dshriceve il package come digitato (con eventuale version spec, es.[email protected]); patch e lock usano il nome normalizzato. Path locale: se<pkg>è un path (inizia con.,/,~ofile:),addnon lo tratta come un nome npm: risolve il nome reale dalpackage.jsondel path (camponame) e usa quel nome per la guardia bundled, per l'id e per la riga del patch;dshriceve il path originale (dsh plugin ... add <path>).stripVersionnon si applica al path. Se ilpackage.jsonlocale non esiste o non haname→ errore chiaro su stderr + exit 1, senza toccare nulla (vedi Installare da un path locale). - remove — elimina le due righe del patch per id (trova
- id: <id>e rimuove quella riga + la successivaname:), pulisce la entry dal lock, poi eseguedsh plugin --profile <name> remove <pkg>. Idempotente (id assente = no-op). Il match è sul solo id, quindi funziona anche con spelling del package diverso da quello usato inadd(es.add @scope/pkg→remove pkg: stesso idpkg). - list — elenca gli id dei plugin del profilo leggendo
cordis.patch.yml(un id per riga, nell'ordine del file). - find — default: inventario REMOTO CURATO del repo
awesome-dsh-plugin/awesome-dsh-plugin(vedi Inventario remoto): stampa PRIMA l'header (fra le due righe blu), poi legge ilREADME.mddel repo (solo la sezione tra i marker<!-- BEGIN PLUGINS -->/<!-- END PLUGINS -->), nell'ordine del README (deterministico), filtra per query (substring case-insensitive sunameOdescriptionOcategory) e per--category(substring case-insensitive sulla categoria), e stampa la rigaInstall with dsplug add <owner/repo>seguita, per ogni plugin, da<name> <description>(name in blu/bold, description in grigio dim su TTY) e sotto└ <url>, con una riga vuota fra le entry. Query assente (o solo spazi) = tutti i plugin. Nessun match = nessuna riga, exit 0. Con--local(o--index <path>) legge l'indice staticoindex.json(o l'indice custom), filtra per query e stampa<name> - <description>una riga per plugin, nell'ordine dell'indice (niente istruzione, niente URL). Errore di rete/API: messaggio chiaro su stderr + exit != 0, con il suggerimento--localper usare l'indice bundled. - check — legge lock + indice e, per ogni plugin del lock (ordine del
lock), confronta
version(lock) vsversion(indice). Una riga deterministica per plugin, usando l'id (comelist):<id> <lock-version> -> <index-version> (outdated)— l'indice offre una versione più nuova;<id> <version> (up to date)— versioni uguali, o lock più nuovo dell'indice;<id> <version> (not in index)— il plugin non compare nell'indice. Nessun plugin nel lock = stdout vuoto. Exit 0 (il check non modifica nulla).
- update — per ogni plugin del lock outdated ri-esegue
addcon ilpkgregistrato nel lock (dsh plugin ... add+ riga nel patch + lock aggiornato alla version dell'indice), stampando<id> (<lock-version> -> <index-version>). Idempotente: se nulla è outdated = no-op (nessuna riga, exit 0). I pluginnot in indexnon sono outdated e non vengono toccati. Supporta--no-dsh(solo aggiornamento di patch/lock, senza re-installare via dsh).
Colori
Colori ANSI a zero dipendenze (src/ansi.js), stile npx skills:
attivi solo quando stdout è un TTY — in pipe (script, test, CI) l'output
è sempre nudo e deterministico. Tre modi di controllarli, in ordine di
priorità:
| controllo | effetto |
|---|---|
| --no-color (flag CLI) | colori SPENTI (vince su tutto) |
| FORCE_COLOR=1 (env) | colori ACCESI anche in pipe (FORCE_COLOR=0 = spenti) |
| NO_COLOR (env, anche vuoto) | colori SPENTI (convenzione no-color.org) |
- Banner (
dsplug,dsplug help,dsplug find): ASCII artdsPLUGrenderizzata da figlet (fontANSI Shadow, gli stessi caratteri box-drawing del bannerSKILLSdi npx skills), gradiente di blu DeepSeek — true-color#4D6BFEcon interpolo#2F51E0 → #9CACFFse il terminale lo supporta (COLORTERM=truecolor|24bit), altrimenti fallback 256-color (38;5;69≈#4D6BFE). Le due righe blu (in cima e in fondo) sono larghe quanto l'art (83 colonne). - find: name in blu/bold, description in grigio dim (
\x1b[90m); nel find remoto la riga è<name> <description>(stessa formattazione diformatEntry) e l'url sotto└ <url>è nudo.
Inventario remoto (awesome-dsh-plugin)
dsplug find (default) legge il README.md del repo curato
awesome-dsh-plugin/awesome-dsh-plugin.
Via di fetch (scelta documentata): si usa la GitHub Contents API, NON
raw.githubusercontent.com:
https://api.github.com/repos/awesome-dsh-plugin/awesome-dsh-plugin/contents/README.md- l'API risponde con un JSON stabile
{ content: <base64>, ... }in una sola richiesta, senza redirect e senza token per repo pubblici (rate limit 60 req/h per IP); raw.githubusercontent.comè una CDN che rate-limita in modo aggressivo per IP (misurato: HTTP 429Too Many Requestssu IP condiviso) e richiede di seguire una catena di redirect.
Il campo content (base64) si decodifica e il testo si parsa come README.
Parsing: solo la sezione tra i marker <!-- BEGIN PLUGINS --> e
<!-- END PLUGINS --> (match per substringa: nel README reale il marker di
apertura è spezzato su due righe, <!-- + newline + BEGIN PLUGINS -->, ed
entrambe le forme funzionano). Formato della sezione:
### <Categoria>
- [<name>](<url>) - <description>ogni heading ### <Categoria> apre un blocco di entry
- [<name>](<url>) - <description>, da cui si estrae
{ name, url, category, description }. Le righe che non sono né heading né
entry (blank, commenti HTML, testo libero) vengono ignorate. L'ordine è
quello del README (curato, deterministico — nessun sort per stelle).
Pipeline di find:
- fetch —
src/awesome.js(fetchAwesomeReadme): usa lafetchglobale di Node (node >= 18). Unico punto di I/O: iniettabile ({ fetchImpl }) per i test, più un hook env per i test del binario (DSPLUG_AWESOME_FIXTURE=<path>= testo del README letto da un file, nessuna rete). - filtro categoria (
--category <name>) — substring case-insensitive sulla categoria (es.--category uimatchaUI Enhancements). - filtro query — substring case-insensitive su
nameOdescriptionOcategory(query vuota o solo spazi = nessun filtro). - output — la riga
Install with dsplug add <owner/repo>, poi per ogni plugin due righe:<name> <description>(name blu/bold, description grigio dim su TTY) e sotto└ <url>(nuda), con una riga vuota fra le entry.
Errori: rete irraggiungibile, HTTP != 2xx o risposta malformata →
messaggio chiaro su stderr + exit != 0, con il suggerimento --local
per usare l'indice bundled senza rete. find --local è completamente offline.
Sicurezza
Installing a plugin runs third-party code on your machine with your own permissions — it can read your files, use your credentials, and reach the network. Being on a list is not a security review: check the source before you install.
La costante SECURITY_WARNING (src/security.js, un'unica fonte di verità) è
mostrata in due punti:
dsplug add(CLI) — su stderr, prima di installare;- welcome/help (
dsplug/dsplug --help) e questo README — blocco avviso nell'header / sezioneSicurezza.
Indice plugin (index.json)
Indice curato bundled con dsplug: array JSON di
{ "name", "description", "type", "bundled", "version" }. type è "host"
(v0 supporta solo plugin host; l'id non è memorizzato — si deriva dal name
con sanitizeId, vedi sotto). bundled è opzionale (default false): true =
plugin già incluso nell'installazione DSH, che add rifiuta di installare una
seconda volta. version è opzionale ma, se presente, deve essere una stringa
semver (major.minor.patch, con pre-release/build opzionali): version
malformata = errore chiaro (stderr + exit 1). È la fonte di verità per check
e update. Seed reale: i tool host @deepseek-ai/dsh-tool-* attuali (tutti
bundled: true, version 0.1.0-rc.6 letta dai loro package.json reali), più
la fixture dsplug-fixture (bundled: false, version 0.0.0, plugin host
minimale in ../fixture-plugin/, per l'e2e reale).
Regola dell'id (documentata)
id = nome del package dopo l'ultimo /, senza i caratteri @
(lo scope @owner/ viene eliminato: l'id di DSH è il nome nudo del plugin).
| package | id |
|---|---|
| @deepseek-ai/dsh-tool-todo | dsh-tool-todo |
| dsh-tool-todo | dsh-tool-todo |
| @scope/pkg | pkg |
Normalizzazione della version spec (stripVersion)
Prima di ogni uso (guardia bundled, derivazione id, riga del patch, lock) il
package viene normalizzato togliendo l'eventuale version spec. Regola: il
separatore di versione è la prima @ a posizione > 0 (la @ a posizione 0
è lo scope npm e va conservata).
| package | normalizzato |
|---|---|
| [email protected] | pkg |
| @scope/[email protected] | @scope/pkg |
| @scope/pkg | @scope/pkg (invariato) |
| pkg | pkg (invariato) |
La riga scritta/rimossa è esattamente due righe:
- id: dsh-tool-todo
name: '@deepseek-ai/dsh-tool-todo'Installare da un path locale
add accetta anche un path locale (una directory con package.json, o il
file package.json stesso) al posto di un nome npm. Il path si riconosce dal
prefisso: . (./x, ../x, .), / (path assoluto), ~ (~/x, ~) o
file: (file:///...). Il nome reale si risolve dal campo name del
package.json; quel nome guida la guardia bundled, l'id e la riga del patch,
mentre dsh riceve il path come digitato:
dsplug add /path/to/my-plugin --profile web
# 1. legge /path/to/my-plugin/package.json -> name: 'my-plugin'
# 2. `dsh plugin --profile web add /path/to/my-plugin` (path originale)
# 3. patch: - id: my-plugin
# name: 'my-plugin'
# 4. lock: my-plugin -> { pkg: 'my-plugin', version: <indice|unknown> }Errori chiari (stderr + exit 1, nulla scritto): path inesistente, file che non
è una directory né un package.json, package.json JSON invalido, o
package.json senza campo name. La guardia bundled si applica al nome
risolto (un plugin locale il cui package.json dichiara il nome di un plugin
bundled viene rifiutato); stripVersion non viene applicato al path.
Lock file (dsplug-lock.json)
dsplug tiene un lock JSON accanto al patch
($DSH_HOME/profiles/<name>/dsplug-lock.json) con le righe che ha scritto lui
stesso:
{
"plugins": {
"dsh-tool-todo": { "pkg": "@deepseek-ai/dsh-tool-todo", "version": "0.1.0-rc.6" }
}
}- add registra/aggiorna la entry (
id -> { pkg, version }), con laversionletta dall'indice se il plugin vi compare, altrimenti'unknown'; remove la elimina per id. - Migrazione: se il lock non esiste ma il patch contiene blocchi nel formato
scritto da dsplug, il lock viene ricostruito leggendo il patch al primo
add/remove(le entry esistenti sopravvivono; il patch non ha versioni, quindi le entry migrate partono daversion: 'unknown'). Formato non-dsplug (commenti,config:manuali,name:senza virgolette) non viene registrato. - Lock assente + patch vuoto = lock
{ "plugins": {} }; JSON invalido o forma errata → errore chiaro su stderr + exit 1 (mai corretto da soli). check/updateleggono il lock così com'è (mai scritto da loro).
Confronto versioni (compareSemver)
src/semver.js implementa un confronto semver minimale a zero dipendenze:
split su . e confronto numerico parte per parte (major/minor/patch; parti
mancanti = 0). Limite documentato: i suffissi pre-release/build
(-rc.6, +build) non sono interpretati semanticamente — si confronta solo la
cifra iniziale di ogni parte, quindi due pre-release diverse vengono ordinate
solo approssimativamente e 0.1.0-rc.6 risulta diverso da 0.1.0. Versioni
identiche (stessa stringa) confrontano sempre uguali, quindi il caso reale di
check (lock == indice) è corretto. 'unknown' si comporta come 0.0.0.
L'indice valida version con isSemver (src/index.js): forma
major.minor.patch con pre-release/build opzionali.
Edit del patch file
- Solo edit testuale chirurgico (appendi/rimuovi righe): mai riscrivere il
file con un parser YAML, per preservare commenti e tag
!!js. - Se il file non esiste, viene creato con contenuto
[](lista YAML vuota); il blocco viene poi appeso in coda. $DSH_HOMEè rispettato (default~/.dsh); il file gestito è$DSH_HOME/profiles/<name>/cordis.patch.yml.removetoglie per id le due righe scritte da dsplug; eventuali righe aggiuntive oltre quelle due (es. unconfig:scritto a mano) non vengono toccate (limite documentato).
Wart noti (v1)
listmostra tutte le righe- id:del patch, incluse quelle NON scritte da dsplug (aggiunte a mano o dadsh plugin adddiretto):listlegge il file così com'è, senza distinguere la provenienza. Il lock file registra solo le righe dsplug: un futurolistpuò filtrare su di esso.
Stack
- Zero framework CLI: parsing manuale di
argv. - Dipendenze runtime: solo
figlet(banner); il resto è stdlib —node:fs,node:os,node:path,node:child_process,node:url, e lafetchglobale (node >= 18) per l'inventario remoto (src/awesome.js). Colori e banner sono ANSI puro (src/ansi.js,src/banner.js). - Test con il runner built-in
node --test(niente vitest, niente install); i test dell'inventario remoto non toccano mai la rete (fetch fake iniettato in unit, fixture del README via envDSPLUG_AWESOME_FIXTUREnei test del bin).
Test
node --test # oppure: npm test / pnpm testSuite: unit (id, stripVersion, validazione input, compareSemver, parsing/
validazione index — incluso bundled e version — matching query, logica pura
di check/update, isLocalPath + resolveLocalPkgName per i path locali,
isGitSpec + gitSpecId per gli spec git-hosted, colori ANSI e banner:
gray/blue/boldBlue/gradientBlue, priorità
NO_COLOR/FORCE_COLOR/--no-color, bannerLines/renderBanner,
formatEntry colorata) · inventario remoto curato (src/awesome.js:
estrazione della sezione tra i marker BEGIN PLUGINS/END PLUGINS — inclusa
la forma spezzata del README reale — con errore chiaro se un marker manca;
parsing di categorie/entry { name, url, category, description }; filtro query
substring case-insensitive su name/description/category; filtro
--category; formatFindEntry <name> <description> + └ <url>;
fetchAwesomeReadme con fetch iniettabile — mai rete nei test — e hook env
DSPLUG_AWESOME_FIXTURE; CLI del bin: header + Install with dsplug add
<owner/repo> + entry, ordine del README, filtro query, --category, --local
invariato, errori remoto con exit != 0) · integrazione su fixture con DSH_HOME
temporaneo e --no-dsh (contenuto corretto + idempotenza) · lock file (path,
read/write, migrazione dal patch, ciclo add/remove con version) · CLI (spawn
del bin, output deterministico, exit code, guardia anti bundled in add, add
con path locale e con spec git (github:/owner/repo/&path:) che
passa lo spec verbatim a dsh e deriva il nome reale, remove per id con spelling
diverso, check deterministico, update idempotente, dsplug senza argomenti
= header, help/-h/--help, --no-color senza escape code, security
warning integrale su stderr in add) · errori (input invalido, path locale
senza package.json o senza name, version malformata nell'indice, lock
malformato → messaggio + exit ≠ 0) · fixture (fixture-plugin/ package valido
- modulo ESM).
Come eseguire l'e2e reale
tests/e2e.test.js gira SOLO se DSPLUG_E2E=1 E DSPLUG_DSH punta al
binario dsh reale. Il bersaglio è la fixture locale
(../fixture-plugin/, con name: 'dsplug-fixture'): dsplug add riceve il
path della fixture, risolve il nome dal suo package.json e invoca il dsh
reale. Flusso verificato dal test: DSH_HOME temporaneo → dsplug add <path>
--profile web → il patch file contiene la riga dsplug-fixture → <dsh>
--dump-config mostra la riga → pulizia del temporaneo.
DSPLUG_E2E=1 DSPLUG_DSH=/path/to/dsh node --test tests/e2e.test.jsSenza questi env il test risulta skipped (mai fallito); se dsh non è
disponibile a runtime, il test si auto-skippa con messaggio chiaro.
