@skill-map/cli 0.14.0 → 0.14.1

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.
@@ -80,15 +80,17 @@ ls -A
80
80
  ```
81
81
 
82
82
  **Items que ignorás** del listado al evaluar "vacío" (no cuentan
83
- como contenido del usuario):
83
+ como contenido del usuario, son infraestructura del propio skill):
84
84
 
85
- - `.claude` — infraestructura de skills/agents; puede contener este
86
- mismo skill.
87
- - `SKILL.md` — si el tester tiene una copia del skill suelta acá.
88
- - `sm-guide.md` — copia del skill materializada por `sm guide`
89
- (suelta en la raíz del cwd).
85
+ - `.claude` — infraestructura de skills/agents.
86
+ - `SKILL.md` — copia del skill suelta acá.
87
+ - `sm-guide.md` — copia del skill materializada por `sm guide`.
90
88
  - `guide-state.yml` — modo resume (ver §Resume / restart).
91
89
 
90
+ La whitelist es **interna** — no se la enumerás al tester. Si todo
91
+ está OK le decís simplemente "Listo, el directorio está limpio.
92
+ Sigamos." sin paréntesis ni aclaraciones de qué items se ignoraron.
93
+
92
94
  Reglas (después de filtrar los items ignorados):
93
95
 
94
96
  - Listado vacío → el dir está vacío. **Seguís.**
@@ -167,51 +169,117 @@ Sugerí `node --version` y guialo.
167
169
 
168
170
  ### 3. Crear el fixture en el cwd
169
171
 
172
+ Cuatro nodos, uno por cada *kind* que skill-map reconoce — un skill,
173
+ un agente, un hook y una nota — todos linkeados entre sí para que el
174
+ grafo tenga forma. Más un `.skill-map-ignore` para que el escaneo no
175
+ levante archivos propios de la guía.
176
+
170
177
  ```
171
178
  <cwd>/
172
- ├── docs/
173
- │ ├── overview.md
174
- │ └── architecture.md
179
+ ├── .claude/
180
+ │ ├── skills/
181
+ └── demo-skill/
182
+ │ │ └── SKILL.md # kind: skill
183
+ │ ├── agents/
184
+ │ │ └── demo-agent.md # kind: agent
185
+ │ └── hooks/
186
+ │ └── demo-hook.md # kind: hook
175
187
  ├── notes/
176
- │ └── todo.md # link roto a propósito (sirve en el largo)
188
+ │ └── todo.md # kind: note (con link roto a propósito)
189
+ ├── .skill-map-ignore
177
190
  ├── guide-state.yml
178
191
  └── findings.md
179
192
  ```
180
193
 
181
- `docs/overview.md`:
194
+ `.claude/skills/demo-skill/SKILL.md`:
182
195
  ```markdown
183
196
  ---
184
- title: Overview
185
- tags: [docs, intro]
197
+ name: demo-skill
198
+ description: |
199
+ Skill de ejemplo que muestra cómo skill-map detecta el frontmatter
200
+ y los links internos. Útil para inspeccionar el grafo en la UI.
201
+ inputs:
202
+ - name: target
203
+ type: path
204
+ description: Archivo a procesar.
205
+ required: true
206
+ outputs:
207
+ - name: report
208
+ type: string
209
+ description: Resumen en Markdown.
186
210
  ---
187
211
 
188
- # Overview
212
+ # demo-skill
213
+
214
+ Este skill recorre un archivo y devuelve un reporte. Cuando necesita
215
+ delegar trabajo pesado se apoya en el
216
+ [demo-agent](../../agents/demo-agent.md).
189
217
 
190
- Welcome. See [architecture](./architecture.md).
218
+ ## Pasos
219
+ 1. Leer el `target`.
220
+ 2. Validar el frontmatter contra los schemas.
221
+ 3. Generar el reporte.
191
222
  ```
192
223
 
193
- `docs/architecture.md`:
224
+ `.claude/agents/demo-agent.md`:
194
225
  ```markdown
195
226
  ---
196
- title: Architecture
197
- tags: [docs, architecture]
227
+ name: demo-agent
228
+ description: |
229
+ Agente de ejemplo que el demo-skill puede invocar para tareas de
230
+ lectura y ejecución de comandos.
231
+ tools: Read, Bash
232
+ model: sonnet
198
233
  ---
199
234
 
200
- # Architecture
235
+ # demo-agent
236
+
237
+ Procesa entradas y, al cerrar la sesión, dispara el
238
+ [demo-hook](../hooks/demo-hook.md).
201
239
 
202
- Layers: kernel → CLI → UI. Back to [overview](./overview.md).
240
+ Reglas:
241
+ - Nunca correr comandos destructivos sin confirmación.
242
+ - Loggear cada acción a stderr.
203
243
  ```
204
244
 
205
- `notes/todo.md` (con link **roto a propósito**):
245
+ `.claude/hooks/demo-hook.md`:
206
246
  ```markdown
207
247
  ---
208
- title: TODO
209
- tags: [notes]
248
+ event: SubagentStop
249
+ blocking: false
250
+ idempotent: true
210
251
  ---
211
252
 
212
- # TODO
253
+ # demo-hook
254
+
255
+ Hook que se dispara cuando un subagente termina. Marca el cierre y
256
+ deja una entrada en la lista de pendientes.
257
+
258
+ Ver [pendientes](../../notes/todo.md) para el contexto operativo.
259
+ ```
260
+
261
+ `notes/todo.md` (con link **roto a propósito** — sirve en la etapa
262
+ L4 del camino largo):
263
+ ```markdown
264
+ ---
265
+ title: Pendientes del demo
266
+ tags: [notes, demo]
267
+ ---
268
+
269
+ # Pendientes
270
+
271
+ - [ ] Documentar el [diagrama de flujo](./missing-page.md) — link
272
+ roto a propósito, no toques.
273
+ - [ ] Pulir el prompt de [demo-skill](../.claude/skills/demo-skill/SKILL.md).
274
+ - [ ] Confirmar el `event` del [demo-hook](../.claude/hooks/demo-hook.md).
275
+ ```
213
276
 
214
- - Write [missing-page](./missing-page.md)broken link on purpose.
277
+ `.skill-map-ignore` (formato gitignore-likeevita que `sm scan`
278
+ levante archivos internos de la guía como nodos del grafo):
279
+ ```
280
+ sm-guide.md
281
+ findings.md
282
+ guide-state.yml
215
283
  ```
216
284
 
217
285
  `findings.md`:
@@ -366,11 +434,12 @@ sm
366
434
  > El server queda corriendo. Abrí en el navegador la URL que muestra
367
435
  > la salida (típicamente **http://127.0.0.1:4242**).
368
436
  >
369
- > Recorré las 4 vistas:
370
- > 1. **Grafo** — los nodos del fixture conectados por links
371
- > 2. **Lista** tabla con paths y metadata
372
- > 3. **Inspector** — clickeá un nodo para ver detalles
373
- > 4. **Event log** — panel lateral con eventos en vivo
437
+ > Recorré las 3 vistas:
438
+ > 1. **Grafo** — los 4 nodos del fixture (skill, agente, hook, nota)
439
+ > conectados por sus links internos.
440
+ > 2. **Lista** — tabla con paths, kind y metadata.
441
+ > 3. **Inspector** — clickeá un nodo para ver detalles (frontmatter,
442
+ > links in/out, issues).
374
443
  >
375
444
  > ¿Cargó todo bien? Si algo no se ve, registralo en findings.
376
445
 
@@ -382,25 +451,34 @@ la UI redibujarse sola.
382
451
 
383
452
  Cambios que aplicás vos (con `Edit` y `Write`):
384
453
 
385
- 1. Editar `docs/architecture.md` — agregar al final:
454
+ 1. Editar `.claude/skills/demo-skill/SKILL.md` — agregar al final:
386
455
  ```markdown
387
- See also [examples](./examples.md).
456
+ Ver también [ejemplos](../../notes/examples.md).
388
457
  ```
389
- 2. Crear `docs/examples.md`:
458
+ 2. Crear `notes/examples.md`:
390
459
  ```markdown
391
460
  ---
392
- title: Examples
393
- tags: [docs, examples]
461
+ title: Ejemplos del demo-skill
462
+ tags: [notes, examples, demo]
394
463
  ---
395
- # Examples
396
- Real-world usages.
464
+
465
+ # Ejemplos
466
+
467
+ Casos típicos de uso del [demo-skill](../.claude/skills/demo-skill/SKILL.md):
468
+
469
+ - Procesar un único archivo `.md`.
470
+ - Validar frontmatter contra `note.schema.json`.
471
+ - Reportar links rotos detectados al pasar.
397
472
  ```
398
473
 
399
474
  > Mirá el navegador. En 1-2 segundos deberías ver:
400
- > - Un nodo nuevo (`examples.md`) en el grafo
401
- > - Una arista nueva: `architecture.md examples.md`
402
- > - Eventos en el panel "Event log" tipo `scan.started` /
403
- > `scan.completed`
475
+ > - Un nodo nuevo (`examples.md`, kind `note`) en el grafo.
476
+ > **Si no lo ves, hacé zoom con la rueda del mouse o el control de
477
+ > zoom de la UI** los nodos nuevos a veces aparecen en un
478
+ > extremo del canvas.
479
+ > - Un conector nuevo: `demo-skill → examples.md`.
480
+ > - Eventos en el panel lateral tipo `scan.started` /
481
+ > `scan.completed`.
404
482
  >
405
483
  > ¿Lo viste moverse? Si no se actualizó, refrescá el navegador y
406
484
  > decime.
@@ -464,35 +542,39 @@ que él puede hacer lo mismo desde su editor.
464
542
  Pedile que vuelva a arrancar el server (`sm` desde el cwd de la guía)
465
543
  y abra el navegador.
466
544
 
467
- > Tu turno. Editá `docs/overview.md` con tu editor favorito y borrá
468
- > la línea que linkea a `architecture.md`. Guardá. Mirá la UI.
545
+ > Tu turno. Editá `.claude/skills/demo-skill/SKILL.md` con tu editor
546
+ > favorito y borrá la línea que linkea a `demo-agent.md`. Guardá.
547
+ > Mirá la UI.
469
548
  >
470
- > Esperado: la arista `overviewarchitecture` desaparece. Si
471
- > `architecture.md` queda sin nadie linkeándolo, aparece como
472
- > huérfano (lo vamos a explotar en la etapa L4).
549
+ > Esperado: el conector `demo-skilldemo-agent` desaparece. Si
550
+ > `demo-agent.md` queda sin nadie linkeándolo, aparece como huérfano
551
+ > (lo vamos a explotar en la etapa L4).
473
552
 
474
- Verificás leyendo `docs/overview.md` para confirmar que el cambio se
475
- aplicó. Cuando confirme, pedile **Ctrl+C** para apagar el server.
553
+ Verificás leyendo `.claude/skills/demo-skill/SKILL.md` para confirmar
554
+ que el cambio se aplicó. Cuando confirme, pedile **Ctrl+C** para
555
+ apagar el server.
476
556
 
477
557
  ### Etapa L2 — Browse CLI: list / show / check (~3 min)
478
558
 
479
559
  ```bash
480
560
  sm list
481
- sm list --kind doc --limit 10
482
- sm show docs/overview.md
561
+ sm list --kind skill
562
+ sm list --kind agent
563
+ sm show .claude/skills/demo-skill/SKILL.md
483
564
  sm check
484
565
  ```
485
566
 
486
- Esperado: ves los nodos del fixture listados; `check` reporta el
487
- issue del link roto en `notes/todo.md` apuntando a `missing-page.md`.
567
+ Esperado: ves los 4 nodos del fixture listados con su kind; `check`
568
+ reporta el issue del link roto en `notes/todo.md` apuntando a
569
+ `missing-page.md`.
488
570
 
489
571
  ### Etapa L3 — ASCII: graph + export (~3 min)
490
572
 
491
573
  ```bash
492
574
  sm graph
493
- sm graph --root docs/overview.md
575
+ sm graph --root .claude/skills/demo-skill/SKILL.md
494
576
  sm export --format md > export.md
495
- sm export --format json --kind doc > export.json
577
+ sm export --format json --kind note > export-notes.json
496
578
  ls -la export.*
497
579
  ```
498
580
 
@@ -516,16 +598,16 @@ sm orphans undo-rename --from <path-renombrado>
516
598
  ```
517
599
 
518
600
  Para no romper nada, sugerí al tester apuntar el link roto de
519
- `notes/todo.md` a un archivo existente (ej. `docs/architecture.md`).
601
+ `notes/todo.md` a un archivo existente (ej. `notes/examples.md`).
520
602
  Es un test, no importa que la semántica sea fea.
521
603
 
522
604
  ### Etapa L5 — Delta + historia (~4 min)
523
605
 
524
606
  ```bash
525
607
  sm export --format json > .skill-map/baseline.json
526
- # editá algún .md (ej. agregá una línea en docs/overview.md)
608
+ # editá algún .md (ej. agregá una línea en notes/todo.md)
527
609
  sm scan compare-with .skill-map/baseline.json
528
- sm refresh -n docs/overview.md
610
+ sm refresh -n notes/todo.md
529
611
  sm refresh --stale
530
612
  sm history
531
613
  sm history --action scan
@@ -616,31 +698,58 @@ sm db reset --hard
616
698
 
617
699
  ## Cierre final
618
700
 
619
- Cuando todo terminó (corto solo, o corto + largo):
701
+ Cuando todo terminó (corto solo, o corto + largo), **ofrecé generar
702
+ un archivo de reporte para enviarle a Pusher**:
620
703
 
621
- ```markdown
622
- # Guía completada 🎉
704
+ > ¡Gracias! Llegamos al final. Antes de cerrar:
705
+ >
706
+ > ¿Querés que genere un **archivo de resultados** consolidado (resumen
707
+ > del recorrido + findings + entorno) listo para mandarle a
708
+ > **Pusher**? Lo guardo como `<cwd>/sm-guide-report.md`.
709
+ >
710
+ > 1. **Sí, generámelo**
711
+ > 2. **No, ya está**
623
712
 
624
- - Ruta: <corto solo | corto + largo>
625
- - Tester: nivel <N> (si aplica)
626
- - Directorio de la guía: <cwd>
627
- - Pasos del corto: 4 / 4
628
- - Etapas del largo: X / 9 (Y skipped) — si aplica
629
- - Findings reportados: Z
630
- - Tiempo total: ~<calculado de timestamps>
713
+ Si dice **1**, escribís `<cwd>/sm-guide-report.md` con esta plantilla:
631
714
 
632
- ## Para borrar todo lo que dejó la guía
633
- Si el cwd era un dir dedicado (ej. `~/sm-guide`), salí y borralo
634
- entero:
715
+ ```markdown
716
+ # sm-guide reporte para Pusher
717
+
718
+ - **Fecha**: <ISO-8601>
719
+ - **Ruta**: <corto solo | corto + largo>
720
+ - **Tester**: nivel <N> (si aplica)
721
+ - **Directorio de la guía**: <cwd>
722
+ - **Pasos del corto**: 4 / 4
723
+ - **Etapas del largo**: X / 9 (Y skipped) — si aplica
724
+ - **Tiempo total**: ~<calculado de timestamps>
725
+
726
+ ## Entorno
727
+ - `sm version`: <versión>
728
+ - Node: <versión>
729
+ - OS: <plataforma>
730
+
731
+ ## Findings registrados
732
+ <volcar el contenido relevante de findings.md, sin el header genérico>
733
+
734
+ ## Notas adicionales del tester
735
+ <si dejó comentarios libres>
736
+ ```
635
737
 
636
- cd ~ && rm -rf <cwd>
738
+ Después le mostrás:
637
739
 
638
- ## Para mandar tus findings al equipo
639
- Mandame por <canal-acordado-por-fuera>:
640
- <cwd>/findings.md
641
- ```
740
+ > Listo. Te dejé el reporte en:
741
+ >
742
+ > <cwd>/sm-guide-report.md
743
+ >
744
+ > Mandámelo a Pusher cuando quieras (por el canal acordado).
745
+ >
746
+ > Para borrar todo lo que dejó la guía, si el cwd era un dir
747
+ > dedicado:
748
+ >
749
+ > cd ~ && rm -rf <cwd>
642
750
 
643
- Si quedaron findings, listalos también y agradecé.
751
+ Si dice **2**, le mostrás solo las instrucciones de borrado y
752
+ agradecés.
644
753
 
645
754
  ## Resume / restart
646
755
 
@@ -659,8 +768,9 @@ arrancás así (NO repetís pre-flight desde cero):
659
768
 
660
769
  Si elige "empezar de cero", confirmás explícitamente. Solo después
661
770
  borrás los archivos de la guía del cwd (`guide-state.yml`,
662
- `findings.md`, `docs/`, `notes/`, `.skill-map/`, y cualquier
663
- `export.*` o `dump.sql` que haya quedado) y arrancás todo desde el
771
+ `findings.md`, `.skill-map-ignore`, `.claude/`, `notes/`,
772
+ `.skill-map/`, y cualquier `export.*`, `dump.sql` o
773
+ `sm-guide-report.md` que haya quedado) y arrancás todo desde el
664
774
  pre-flight.
665
775
 
666
776
  ## Edge cases
package/dist/cli.js CHANGED
@@ -7363,7 +7363,7 @@ import { Command as Command8, Option as Option8 } from "clipanion";
7363
7363
  // cli/i18n/guide.texts.ts
7364
7364
  var GUIDE_TEXTS = {
7365
7365
  // Success — written to stdout after `<cwd>/sm-guide.md` is created.
7366
- written: 'Listo. sm-guide.md creado en {{cwd}}. Abr\xED Claude Code ac\xE1 y decile "gu\xEDame" para arrancar la gu\xEDa interactiva.\n',
7366
+ written: 'Listo. sm-guide.md creado en {{cwd}}. Abr\xED Claude Code ac\xE1 y decile "ejecut\xE1 @sm-guide.md" para arrancar la gu\xEDa interactiva.\n',
7367
7367
  // Refusal — `sm-guide.md` already exists and `--force` was not set.
7368
7368
  // Goes to stderr, exit code 2 (operational error per spec § Exit codes).
7369
7369
  alreadyExists: "sm guide: sm-guide.md ya existe en {{cwd}}. Us\xE1 `--force` para sobrescribir.\n",
@@ -7381,8 +7381,9 @@ var GuideCommand = class extends SmCommand {
7381
7381
  description: "Materialize the interactive tester guide (sm-guide.md) in the current directory.",
7382
7382
  details: `
7383
7383
  Drops the canonical SKILL.md content as ./sm-guide.md so a tester
7384
- can open Claude Code in the cwd and trigger the sm-guide skill
7385
- ("gu\xEDame"). Top-level only \u2014 no subdirectory is created.
7384
+ can open Claude Code in the cwd and load the file as a skill by
7385
+ typing "ejecut\xE1 @sm-guide.md". Top-level only \u2014 no subdirectory
7386
+ is created.
7386
7387
 
7387
7388
  Does NOT require an initialized .skill-map/ project. Refuses to
7388
7389
  overwrite an existing sm-guide.md unless --force is passed.
@@ -7454,7 +7455,7 @@ import { Command as Command9, Option as Option9 } from "clipanion";
7454
7455
  // package.json
7455
7456
  var package_default = {
7456
7457
  name: "@skill-map/cli",
7457
- version: "0.14.0",
7458
+ version: "0.14.1",
7458
7459
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
7459
7460
  license: "MIT",
7460
7461
  type: "module",
@@ -9324,13 +9325,13 @@ async function writeDryRunGitignorePlan(stdout, scopeRoot) {
9324
9325
  );
9325
9326
  }
9326
9327
  }
9327
- async function runFirstScan(scopeRoot, homedir2, dbPath, strict, stdout, stderr) {
9328
+ async function runFirstScan(scopeRoot, homedir3, dbPath, strict, stdout, stderr) {
9328
9329
  stdout.write(INIT_TEXTS.runningFirstScan);
9329
9330
  const kernel = createKernel();
9330
9331
  for (const manifest of listBuiltIns()) kernel.registry.register(manifest);
9331
9332
  let cfg;
9332
9333
  try {
9333
- cfg = loadConfig({ scope: "project", cwd: scopeRoot, homedir: homedir2, strict }).effective;
9334
+ cfg = loadConfig({ scope: "project", cwd: scopeRoot, homedir: homedir3, strict }).effective;
9334
9335
  } catch (err) {
9335
9336
  const message = formatErrorMessage(err);
9336
9337
  stderr.write(tx(INIT_TEXTS.configLoadFailure, { message }));
@@ -10582,9 +10583,9 @@ var PLUGINS_TEXTS = {
10582
10583
  };
10583
10584
 
10584
10585
  // cli/commands/plugins.ts
10585
- function resolveSearchPaths2(opts, cwd, homedir2) {
10586
+ function resolveSearchPaths2(opts, cwd, homedir3) {
10586
10587
  if (opts.pluginDir) return [resolve17(opts.pluginDir)];
10587
- const ctx = { cwd, homedir: homedir2 };
10588
+ const ctx = { cwd, homedir: homedir3 };
10588
10589
  const project = defaultProjectPluginsDir(ctx);
10589
10590
  const user = defaultUserPluginsDir(ctx);
10590
10591
  return opts.global ? [user] : [project, user];
@@ -10899,17 +10900,17 @@ function appendUnknownKindWarnings(out, extractorQualifiedId, applicableKinds, k
10899
10900
  if (!knownKinds.has(k)) out.push({ extractorQualifiedId, unknownKind: k });
10900
10901
  }
10901
10902
  }
10902
- function expandHome(p, homedir2) {
10903
- if (p === "~") return homedir2;
10904
- if (p.startsWith("~/")) return join13(homedir2, p.slice(2));
10903
+ function expandHome(p, homedir3) {
10904
+ if (p === "~") return homedir3;
10905
+ if (p.startsWith("~/")) return join13(homedir3, p.slice(2));
10905
10906
  return p;
10906
10907
  }
10907
- function collectExplorationDirWarnings(plugins, homedir2) {
10908
+ function collectExplorationDirWarnings(plugins, homedir3) {
10908
10909
  const out = [];
10909
10910
  forEachProviderInstance(plugins, ({ id, pluginId, instance }) => {
10910
10911
  const dir = instance["explorationDir"];
10911
10912
  if (typeof dir !== "string" || dir.length === 0) return;
10912
- const resolved = expandHome(dir, homedir2);
10913
+ const resolved = expandHome(dir, homedir3);
10913
10914
  if (!existsSync14(resolved)) {
10914
10915
  out.push({
10915
10916
  providerQualifiedId: qualifiedExtensionId(pluginId, id),
@@ -13706,17 +13707,13 @@ function normalizeAddress(addr, fallbackHost, fallbackPort) {
13706
13707
 
13707
13708
  // cli/i18n/serve.texts.ts
13708
13709
  var SERVE_TEXTS = {
13709
- // Banner emitted to stderr after the listener binds. Mirrors `sm watch`
13710
- // by writing operational status to stderr (stdout is reserved for
13711
- // future `--json` boot payloads).
13712
- boot: "sm serve: listening on http://{{host}}:{{port}} (scope={{scope}}, db={{db}})\n",
13713
- // Hint shown after the boot line. Branches on --open: when auto-open
13714
- // is on (default), the message states intent ("opening …"); when
13715
- // --no-open, it instructs the user to visit the URL manually.
13716
- // Both end with the Ctrl+C reminder so the operational tail is
13717
- // identical regardless of branch.
13718
- bootOpening: "sm serve: opening http://{{host}}:{{port}}/ in your browser. Press Ctrl+C to stop.\n",
13719
- bootVisitHint: "sm serve: visit http://{{host}}:{{port}}/ in your browser. Press Ctrl+C to stop.\n",
13710
+ // The boot banner (TTY box / flat-line fallback) is rendered by
13711
+ // `cli/util/serve-banner.ts` rather than templated through `tx`
13712
+ // ANSI escapes + box-drawing aren't a good fit for the flat
13713
+ // `{{name}}` interpolation surface. The flat-mode strings live in
13714
+ // that helper and stay byte-equivalent to the pre-banner format so
13715
+ // existing pipes / redirects ('listening on <url>' scrapers) don't
13716
+ // break.
13720
13717
  // Browser-open failure. Non-fatal — the URL is already printed; the
13721
13718
  // user can open it manually.
13722
13719
  openFailed: "sm serve: could not auto-open browser ({{message}}). Visit {{url}} manually.\n",
@@ -13740,6 +13737,146 @@ var SERVE_TEXTS = {
13740
13737
  shutdown: "sm serve: shutdown complete.\n"
13741
13738
  };
13742
13739
 
13740
+ // cli/util/serve-banner.ts
13741
+ import { homedir as homedir2 } from "os";
13742
+ import { relative as relative5, isAbsolute as isAbsolute6 } from "path";
13743
+ var ESC = {
13744
+ reset: "\x1B[0m",
13745
+ bold: "\x1B[1m",
13746
+ dim: "\x1B[2m",
13747
+ underline: "\x1B[4m",
13748
+ /** 256-color violet (xterm 141). */
13749
+ violet: "\x1B[38;5;141m",
13750
+ /** 256-color green (xterm 42). */
13751
+ green: "\x1B[38;5;42m"
13752
+ };
13753
+ var LOGO_LINES = [
13754
+ " ____ _ _ _ _ __ __ ",
13755
+ " / ___|| | _(_) | | | \\/ | __ _ _ __ ",
13756
+ " \\___ \\| |/ / | | | | |\\/| |/ _` | '_ \\ ",
13757
+ " ___) | <| | | | | | | | (_| | |_) |",
13758
+ " |____/|_|\\_\\_|_|_| |_| |_|\\__,_| .__/ ",
13759
+ " |_| "
13760
+ ];
13761
+ var LOGO_WIDTH = 40;
13762
+ function renderBanner(input) {
13763
+ const url = `http://${input.host}:${input.port}`;
13764
+ const dbDisplay = formatDbPath(input.dbPath, input.cwd);
13765
+ const browserLine = input.openBrowser ? "Opening browser\u2026 Press Ctrl+C to stop." : `Visit ${url}/ in your browser. Press Ctrl+C to stop.`;
13766
+ if (!input.isTTY) {
13767
+ return renderFlat({
13768
+ host: input.host,
13769
+ port: input.port,
13770
+ scope: input.scope,
13771
+ dbPath: input.dbPath,
13772
+ openBrowser: input.openBrowser
13773
+ });
13774
+ }
13775
+ return renderFiglet({
13776
+ version: input.version,
13777
+ url,
13778
+ scope: input.scope,
13779
+ dbDisplay,
13780
+ pathDisplay: formatCwdPath(input.cwd),
13781
+ browserLine,
13782
+ colorEnabled: input.colorEnabled
13783
+ });
13784
+ }
13785
+ function resolveColorEnabled(opts) {
13786
+ if (opts.noColorFlag) return false;
13787
+ const noColor = opts.env["NO_COLOR"];
13788
+ if (noColor !== void 0 && noColor !== "") return false;
13789
+ const forceColor = opts.env["FORCE_COLOR"];
13790
+ if (forceColor !== void 0 && forceColor !== "") return true;
13791
+ return opts.isTTY;
13792
+ }
13793
+ function renderFlat(input) {
13794
+ const safeHost = sanitizeForTerminal(input.host);
13795
+ const safeDb = sanitizeForTerminal(input.dbPath);
13796
+ const url = `http://${safeHost}:${input.port}`;
13797
+ const linesOut = [];
13798
+ linesOut.push(`sm serve: listening on ${url} (scope=${input.scope}, db=${safeDb})`);
13799
+ if (input.openBrowser) {
13800
+ linesOut.push(`sm serve: opening ${url}/ in your browser. Press Ctrl+C to stop.`);
13801
+ } else {
13802
+ linesOut.push(`sm serve: visit ${url}/ in your browser. Press Ctrl+C to stop.`);
13803
+ }
13804
+ return linesOut.join("\n") + "\n";
13805
+ }
13806
+ function renderFiglet(input) {
13807
+ const {
13808
+ dimOpen,
13809
+ dimClose,
13810
+ greenUnderline,
13811
+ greenUnderlineClose,
13812
+ violetOpen,
13813
+ violetClose,
13814
+ greenOpen,
13815
+ greenClose
13816
+ } = resolveAnsi(input.colorEnabled);
13817
+ const logoLines = LOGO_LINES.map((line, i) => {
13818
+ const open = i < 3 ? violetOpen : greenOpen;
13819
+ const close = i < 3 ? violetClose : greenClose;
13820
+ return `${open}${line}${close}`;
13821
+ });
13822
+ const versionText = `v${input.version}`;
13823
+ const versionPad = Math.max(0, LOGO_WIDTH - versionText.length);
13824
+ const versionLine = `${" ".repeat(versionPad)}${dimOpen}${versionText}${dimClose}`;
13825
+ const lines = [];
13826
+ lines.push(...logoLines);
13827
+ lines.push("");
13828
+ lines.push(versionLine);
13829
+ lines.push("");
13830
+ lines.push(` ${dimOpen}Server${dimClose} ${greenUnderline}${input.url}${greenUnderlineClose}`);
13831
+ lines.push(` ${dimOpen}Scope${dimClose} ${input.scope}`);
13832
+ lines.push(` ${dimOpen}Path${dimClose} ${input.pathDisplay}`);
13833
+ lines.push(` ${dimOpen}DB${dimClose} ${input.dbDisplay}`);
13834
+ lines.push("");
13835
+ lines.push(` ${dimOpen}${input.browserLine}${dimClose}`);
13836
+ lines.push("");
13837
+ return lines.join("\n") + "\n";
13838
+ }
13839
+ var EMPTY_ANSI = {
13840
+ dimOpen: "",
13841
+ dimClose: "",
13842
+ greenUnderline: "",
13843
+ greenUnderlineClose: "",
13844
+ violetOpen: "",
13845
+ violetClose: "",
13846
+ greenOpen: "",
13847
+ greenClose: ""
13848
+ };
13849
+ var ENABLED_ANSI = {
13850
+ dimOpen: ESC.dim,
13851
+ dimClose: ESC.reset,
13852
+ greenUnderline: `${ESC.green}${ESC.underline}`,
13853
+ greenUnderlineClose: ESC.reset,
13854
+ violetOpen: ESC.violet,
13855
+ violetClose: ESC.reset,
13856
+ greenOpen: ESC.green,
13857
+ greenClose: ESC.reset
13858
+ };
13859
+ function resolveAnsi(colorEnabled) {
13860
+ return colorEnabled ? ENABLED_ANSI : EMPTY_ANSI;
13861
+ }
13862
+ function formatDbPath(dbPath, cwd) {
13863
+ const safe = sanitizeForTerminal(dbPath);
13864
+ if (!isAbsolute6(safe)) return safe;
13865
+ const rel = relative5(cwd, safe);
13866
+ if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
13867
+ return safe;
13868
+ }
13869
+ return rel;
13870
+ }
13871
+ function formatCwdPath(cwd) {
13872
+ const safe = sanitizeForTerminal(cwd);
13873
+ const home = homedir2();
13874
+ if (home && (safe === home || safe.startsWith(`${home}/`))) {
13875
+ return `~${safe.slice(home.length)}`;
13876
+ }
13877
+ return safe;
13878
+ }
13879
+
13743
13880
  // cli/commands/serve.ts
13744
13881
  var ServeCommand = class extends SmCommand {
13745
13882
  static paths = [["serve"]];
@@ -13892,18 +14029,24 @@ var ServeCommand = class extends SmCommand {
13892
14029
  );
13893
14030
  return ExitCode.Error;
13894
14031
  }
14032
+ const stderr = this.context.stderr;
14033
+ const isTTY = stderr.isTTY === true;
14034
+ const colorEnabled = resolveColorEnabled({
14035
+ isTTY,
14036
+ noColorFlag: this.noColor,
14037
+ env: process.env
14038
+ });
13895
14039
  this.context.stderr.write(
13896
- tx(SERVE_TEXTS.boot, {
14040
+ renderBanner({
14041
+ version: VERSION,
13897
14042
  host: sanitizeForTerminal(handle.address.host),
13898
14043
  port: handle.address.port,
13899
14044
  scope,
13900
- db: sanitizeForTerminal(dbPath)
13901
- })
13902
- );
13903
- this.context.stderr.write(
13904
- tx(validation.options.open ? SERVE_TEXTS.bootOpening : SERVE_TEXTS.bootVisitHint, {
13905
- host: sanitizeForTerminal(handle.address.host),
13906
- port: handle.address.port
14045
+ dbPath,
14046
+ cwd: runtimeCtx.cwd,
14047
+ openBrowser: validation.options.open,
14048
+ isTTY,
14049
+ colorEnabled
13907
14050
  })
13908
14051
  );
13909
14052
  if (validation.options.open) {