SLOPSHOPPER

ddev-status

Status line with git branch and pending changes, DDEV state and OpenSpec progress.

newstatusprocesstimer
v0.1.0MITupdated 2026-10-20o0Charly0o/drupal-mods/ddev-status
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ddev-status
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ ddev-status: feat/auth-refresh ●2
README

drupal-mods

Claude Code mods para proyectos Drupal que usan DDEV.

Plugins

drupal-guard

Pide confirmación antes de comandos que destruyen datos o código que git no puede recuperar: reinstalar el sitio, vaciar tablas, borrar entidades en bloque, rm -rf, ddev delete, restaurar un snapshot, o descartar cambios de git. Una regla deny existente se respeta igual.

drupal-phpcs

Tras editar o crear un fichero de un módulo custom (.php, .module, .install, .inc, .theme, .profile), ejecuta phpcs (estándares Drupal y DrupalPractice) dentro de DDEV sobre ese fichero y añade el informe al contexto para que Claude lo corrija en el mismo turno.

Por defecto busca los módulos en web/modules/custom/. Si el proyecto usa otra raíz (p. ej. docroot/), fija la variable de entorno DRUPAL_WEBROOT:

claude mcp add ... # no aplica aquí; para plugins usa:
claude plugin install drupal-phpcs --marketplace o0Charly0o/drupal-mods -s user -e DRUPAL_WEBROOT=docroot

Si DDEV no está levantado o phpcs no existe, el hook nunca bloquea la edición.

ddev-status

Barra de estado: rama de git y cambios pendientes, si el contenedor web de DDEV está arriba, y el progreso (hecho/total) de cada change activo en openspec/changes/.

Instalar

claude plugin install drupal-guard --marketplace o0Charly0o/drupal-mods -s user
claude plugin install drupal-phpcs --marketplace o0Charly0o/drupal-mods -s user
claude plugin install ddev-status --marketplace o0Charly0o/drupal-mods -s user

Con -s user quedan disponibles en todos los proyectos; fuera de un proyecto Drupal/DDEV simplemente no se activan.

License

MIT

Source 1 files
hooks/register.ts 103 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3
4const TASK = /^\s*- \[([ xX])\]/gm
5
6/**
7 * Status line: "master ●3 · DDEV ✓ · user-library 13/15".
8 * Refreshed when the session starts, after every turn and every 30 seconds.
9 */
10export const register: Register = on => {
11  let cancel: (() => void) | undefined
12
13  on('session.start', async ($, e, next) => {
14    const started = await next(e)
15    void refresh($)
16    cancel?.()
17    cancel = $.clock.every(30000, () => refresh($)).cancel
18    return started
19  })
20
21  on('turn.complete', async ($, e, next) => {
22    const done = await next(e)
23    void refresh($)
24    return done
25  })
26}
27
28/**
29 * Recomputes the status line.
30 */
31async function refresh($: EngineInterface): Promise<void> {
32  const parts: string[] = []
33
34  const branch = await run($, ['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
35  if (branch !== null) {
36    const changes = (await run($, ['git', 'status', '--porcelain']))?.split('\n').filter(Boolean).length ?? 0
37    parts.push(changes > 0 ? `${branch} ●${changes}` : branch)
38  }
39
40  const ddev = await readDdevName($)
41  if (ddev !== null) {
42    const state = await run($, ['docker', 'ps', '--filter', `name=ddev-${ddev}-web`, '--format', '{{.Status}}'])
43    parts.push(state ? 'DDEV ✓' : 'DDEV ✗')
44  }
45
46  parts.push(...(await openspecProgress($)))
47  $.ui.status(parts.length ? parts.join(' · ') : undefined)
48}
49
50/**
51 * Runs a command; its trimmed output on success, null otherwise.
52 */
53async function run($: EngineInterface, argv: readonly string[]): Promise<string | null> {
54  try {
55    const { exitCode, stdout } = await $.process.run(argv, { timeoutMs: 10000 })
56    return exitCode === 0 ? stdout.trim() : null
57  }
58  catch {
59    return null
60  }
61}
62
63/**
64 * Returns the DDEV project name from .ddev/config.yaml, if any.
65 */
66async function readDdevName($: EngineInterface): Promise<string | null> {
67  try {
68    const config = String(await $.fs.read('.ddev/config.yaml'))
69    return config.match(/^name:\s*["']?([\w.-]+)/m)?.[1] ?? null
70  }
71  catch {
72    return null
73  }
74}
75
76/**
77 * Returns "change done/total" for each active OpenSpec change.
78 */
79async function openspecProgress($: EngineInterface): Promise<string[]> {
80  try {
81    const entries = await $.fs.list('openspec/changes')
82    const progress: string[] = []
83    for (const entry of entries) {
84      if (entry.kind !== 'dir' || entry.name === 'archive') {
85        continue
86      }
87      try {
88        const tasks = String(await $.fs.read(`openspec/changes/${entry.name}/tasks.md`))
89        const marks = [...tasks.matchAll(TASK)].map(match => match[1])
90        const done = marks.filter(mark => mark !== ' ').length
91        progress.push(done === marks.length && marks.length > 0 ? `${entry.name} ✓` : `${entry.name} ${done}/${marks.length}`)
92      }
93      catch {
94        progress.push(`${entry.name} (no tasks)`)
95      }
96    }
97    return progress
98  }
99  catch {
100    return []
101  }
102}
103