Skip to content

Commit 62cc527

Browse files
authored
Use new sitectl helpers (#10)
and add docs generation tooling
1 parent 787edbf commit 62cc527

10 files changed

Lines changed: 247 additions & 253 deletions

File tree

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build deps lint test work install
1+
.PHONY: build deps lint test work install docs-snippets
22

33
BINARY_NAME=sitectl-drupal
44
INSTALL_DIR ?= $(or $(dir $(shell which $(BINARY_NAME) 2>/dev/null)),/usr/local/bin/)
@@ -28,3 +28,6 @@ test: build
2828

2929
work:
3030
./scripts/use-go-work.sh
31+
32+
docs-snippets: work
33+
go run ./scripts/gen-docs-snippets/

cmd/drush.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,19 @@ var drushCmd = &cobra.Command{
1111
Use: "drush [COMMAND]",
1212
DisableFlagParsing: true,
1313
Args: cobra.ArbitraryArgs,
14-
Short: "Run drush commands on ISLE contexts",
15-
Long: `Run drush commands on ISLE contexts.
14+
Short: "Run drush commands inside the Drupal container",
15+
Long: `Run drush commands inside the Drupal container of the active context.
1616
17-
This is a shorthand for "sitectl compose exec drupal drush" with automatic --uri handling.
18-
The DRUPAL_DRUSH_URI environment variable is automatically passed unless you specify --uri or -l.
19-
20-
Special subcommands:
21-
uli - Generate and auto-open a one-time login link in your browser
17+
This wraps "docker compose exec drupal drush" and automatically injects DRUPAL_DRUSH_URI
18+
so --uri does not need to be specified manually.
2219
2320
Examples:
24-
sitectl drush status # Check Drupal status
25-
sitectl drush cr # Clear all caches
26-
sitectl drush cex # Export configuration
27-
sitectl drush cim # Import configuration
28-
sitectl drush uli # Generate login link and open in browser
29-
sitectl drush uli --uid=2 # Login link for user ID 2
30-
sitectl drush sqlq "SHOW TABLES" # Run SQL query
31-
sitectl drush --context prod status # Check status on prod context`,
21+
sitectl isle drush status # Check Drupal status
22+
sitectl isle drush cr # Clear all caches
23+
sitectl isle drush cex # Export configuration
24+
sitectl isle drush cim # Import configuration
25+
sitectl isle drush sqlq "SHOW TABLES" # Run a SQL query
26+
sitectl isle drush --context prod status # Check status on the prod context`,
3227
RunE: func(cmd *cobra.Command, args []string) error {
3328
filteredArgs, ctx, cli, containerName, err := getDrupalContainer(cmd, args)
3429
if err != nil {

cmd/extensions.go

Lines changed: 19 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ import (
1212
"strconv"
1313
"strings"
1414

15-
"charm.land/lipgloss/v2"
1615
"github.com/libops/sitectl/pkg/config"
1716
"github.com/libops/sitectl/pkg/docker"
1817
"github.com/libops/sitectl/pkg/plugin"
18+
"github.com/libops/sitectl/pkg/plugin/debugui"
1919
"github.com/spf13/cobra"
20-
"golang.org/x/term"
2120
"gopkg.in/yaml.v3"
2221
)
2322

@@ -111,27 +110,6 @@ var drupalCoreModules = map[string]struct{}{
111110
"workspaces": {},
112111
}
113112

114-
var (
115-
debugPanelStyle = lipgloss.NewStyle().
116-
Background(lipgloss.Color("#112235")).
117-
Padding(1, 2)
118-
debugTitleStyle = lipgloss.NewStyle().
119-
Bold(true).
120-
Foreground(lipgloss.Color("#98C1D9"))
121-
debugSectionDividerStyle = lipgloss.NewStyle().
122-
Foreground(lipgloss.Color("#29425E"))
123-
debugStatusOKStyle = lipgloss.NewStyle().
124-
Bold(true).
125-
Foreground(lipgloss.Color("#7BD389"))
126-
debugStatusWarningStyle = lipgloss.NewStyle().
127-
Bold(true).
128-
Foreground(lipgloss.Color("#F4C95D"))
129-
debugMutedStyle = lipgloss.NewStyle().
130-
Foreground(lipgloss.Color("#9FB3C8"))
131-
debugRowStyle = lipgloss.NewStyle().
132-
Background(lipgloss.Color("#112235"))
133-
)
134-
135113
var componentExtensionCmd = &cobra.Command{
136114
Use: "__component",
137115
Short: "Internal component extension command",
@@ -221,11 +199,11 @@ func renderDrupalDebug(runCtx context.Context) (string, error) {
221199
slog.Debug("resolved drupal root", "plugin", "drupal", "drupal_root", drupalRoot)
222200
configDir := filepath.Join(drupalRoot, "config", "sync")
223201
body := []string{
224-
debugDivider(),
202+
debugui.Divider(),
225203
"",
226-
debugTitleStyle.Render("General"),
204+
debugui.Title("General"),
227205
"",
228-
formatDebugRows([]debugRow{
206+
debugui.FormatRows([]debugui.Row{
229207
{Label: "Context", Value: ctx.Name},
230208
{Label: "Project dir", Value: ctx.ProjectDir},
231209
{Label: "Drupal root", Value: drupalRoot},
@@ -236,7 +214,7 @@ func renderDrupalDebug(runCtx context.Context) (string, error) {
236214
if strings.TrimSpace(drupalRoot) == "" {
237215
slog.Debug("drupal root unavailable; skipping extension scan", "plugin", "drupal")
238216
body = append(body, "", "Installed modules: unavailable")
239-
return renderDebugPanel("drupal", strings.Join(body, "\n")), nil
217+
return debugui.RenderPanel("drupal", strings.Join(body, "\n")), nil
240218
}
241219

242220
slog.Debug("reading core.extension.yml", "plugin", "drupal", "path", filepath.Join(configDir, "core.extension.yml"))
@@ -256,27 +234,27 @@ func renderDrupalDebug(runCtx context.Context) (string, error) {
256234
slog.Debug("rendering cache_page summary", "plugin", "drupal")
257235
cachePageSummary, err := renderCachePageSummary(runCtx)
258236
if err != nil {
259-
body = append(body, "", debugDivider(), "", debugTitleStyle.Render("Cache Page"), "", formatDebugRows([]debugRow{
260-
{Label: "Status", Value: renderStatus("warning")},
237+
body = append(body, "", debugui.Divider(), "", debugui.Title("Cache Page"), "", debugui.FormatRows([]debugui.Row{
238+
{Label: "Status", Value: debugui.Status("warning")},
261239
{Label: "cache_page", Value: fmt.Sprintf("unavailable (%v)", err)},
262240
}))
263241
} else if strings.TrimSpace(cachePageSummary) != "" {
264-
body = append(body, "", debugDivider(), "", debugTitleStyle.Render("Cache Page"), "", cachePageSummary)
242+
body = append(body, "", debugui.Divider(), "", debugui.Title("Cache Page"), "", cachePageSummary)
265243
}
266244

267245
moduleLines, err := renderModuleList(runCtx, files, drupalRoot, modules, moduleVersionInfo)
268246
if err != nil {
269247
return "", err
270248
}
271249

272-
configLines := []string{debugDivider(), "", debugTitleStyle.Render("Installed Extensions"), "", fmt.Sprintf("Installed modules (%d):", len(modules))}
250+
configLines := []string{debugui.Divider(), "", debugui.Title("Installed Extensions"), "", fmt.Sprintf("Installed modules (%d):", len(modules))}
273251
configLines = append(configLines, moduleLines...)
274252
configLines = append(configLines, "")
275253
configLines = append(configLines, fmt.Sprintf("Installed themes (%d):", len(themes)))
276254
configLines = append(configLines, formatListLines(themes, 3)...)
277255
body = append(body, "", strings.Join(configLines, "\n"))
278256

279-
patchLines := []string{debugDivider(), "", debugTitleStyle.Render("Composer Patches"), ""}
257+
patchLines := []string{debugui.Divider(), "", debugui.Title("Composer Patches"), ""}
280258
if strings.TrimSpace(composerPatches) == "" {
281259
patchLines = append(patchLines, " none")
282260
} else {
@@ -285,7 +263,7 @@ func renderDrupalDebug(runCtx context.Context) (string, error) {
285263
body = append(body, "", strings.Join(patchLines, "\n"))
286264

287265
slog.Debug("finished plugin debug", "plugin", "drupal")
288-
return renderDebugPanel("drupal", strings.Join(body, "\n")), nil
266+
return debugui.RenderPanel("drupal", strings.Join(body, "\n")), nil
289267
}
290268

291269
func renderCachePageSummary(runCtx context.Context) (string, error) {
@@ -304,23 +282,25 @@ func renderCachePageSummary(runCtx context.Context) (string, error) {
304282
return "", err
305283
}
306284

307-
rows := []debugRow{
308-
{Label: "Status", Value: renderStatus("ok")},
285+
rows := []debugui.Row{
286+
{Label: "Status", Value: debugui.Status("ok")},
309287
{Label: "cache_page", Value: humanBytes(cachePageSize)},
310288
{Label: "cache_render", Value: humanBytes(cacheRenderSize)},
311289
}
312290
if cachePageSize >= cachePageWarningThreshold || cacheRenderSize >= cachePageWarningThreshold {
313-
rows[0].Value = renderStatus("warning")
291+
rows[0].Value = debugui.Status("warning")
314292
}
315293
if cachePageSize >= cachePageWarningThreshold {
316-
rows = append(rows, debugRow{Label: "Recommendation", Value: pageCacheExclusionURL})
294+
rows = append(rows, debugui.Row{Label: "Recommendation", Value: pageCacheExclusionURL})
317295
}
318-
return formatDebugRows(rows), nil
296+
return debugui.FormatRows(rows), nil
319297
}
320298

321299
func readDrupalCacheTableSize(runCtx context.Context, cli *docker.DockerClient, containerName, containerRoot, tableName string) (int64, error) {
322300
query := fmt.Sprintf("SELECT COALESCE(data_length + index_length, 0) FROM information_schema.TABLES WHERE table_schema = DATABASE() AND table_name = '%s';", strings.TrimSpace(tableName))
323-
output, err := execDrupalCommandCapture(runCtx, cli, containerName, containerRoot, []string{"drush", "sql:query", query, "--extra=--batch", "--extra=--skip-column-names"})
301+
cmd := []string{"drush", "sql:query", query, "--extra=--batch", "--extra=--skip-column-names"}
302+
slog.Debug(strings.Join(cmd, " "), "plugin", "drupal", "container", containerName)
303+
output, err := docker.ExecCapture(runCtx, cli, containerName, containerRoot, cmd)
324304
if err != nil {
325305
return 0, err
326306
}
@@ -351,37 +331,6 @@ func getDrupalContainerForSDK(runCtx context.Context) (ctx *config.Context, cli
351331
return ctx, cli, containerName, nil
352332
}
353333

354-
func execDrupalCommandCapture(runCtx context.Context, cli *docker.DockerClient, containerName, containerRoot string, cmd []string) (string, error) {
355-
slog.Debug(strings.Join(cmd, " "), "plugin", "drupal", "container", containerName)
356-
var stdout bytes.Buffer
357-
var stderr bytes.Buffer
358-
359-
exitCode, err := cli.Exec(runCtx, docker.ExecOptions{
360-
Container: containerName,
361-
Cmd: cmd,
362-
WorkingDir: containerRoot,
363-
AttachStdout: true,
364-
AttachStderr: true,
365-
Stdout: &stdout,
366-
Stderr: &stderr,
367-
})
368-
if err != nil {
369-
return "", err
370-
}
371-
if exitCode != 0 {
372-
detail := strings.TrimSpace(stderr.String())
373-
if detail == "" {
374-
detail = strings.TrimSpace(stdout.String())
375-
}
376-
if detail != "" {
377-
return "", fmt.Errorf("drupal command failed with exit code %d: %s", exitCode, detail)
378-
}
379-
return "", fmt.Errorf("drupal command failed with exit code %d", exitCode)
380-
}
381-
382-
return strings.TrimSpace(stdout.String()), nil
383-
}
384-
385334
func parseFirstInt(output string) (int64, error) {
386335
for _, line := range strings.Split(output, "\n") {
387336
line = strings.TrimSpace(line)
@@ -650,87 +599,3 @@ func formatListLines(values []string, perLine int) []string {
650599
}
651600
return lines
652601
}
653-
654-
type debugRow struct {
655-
Label string
656-
Value string
657-
}
658-
659-
func renderDebugPanel(title, body string) string {
660-
header := debugTitleStyle.Render(strings.TrimSpace(title))
661-
content := header
662-
if strings.TrimSpace(body) != "" {
663-
content += "\n\n" + body
664-
}
665-
return debugPanelStyle.Width(debugPanelWidth()).Render(content)
666-
}
667-
668-
func formatDebugRows(rows []debugRow) string {
669-
labelWidth := 0
670-
for _, row := range rows {
671-
if width := len(strings.TrimSpace(row.Label)); width > labelWidth {
672-
labelWidth = width
673-
}
674-
}
675-
lines := make([]string, 0, len(rows))
676-
rowWidth := debugContentWidth()
677-
for _, row := range rows {
678-
label := strings.TrimSpace(row.Label)
679-
value := strings.TrimSpace(row.Value)
680-
if label == "" {
681-
lines = append(lines, renderDebugRow(rowWidth, "", value))
682-
continue
683-
}
684-
lines = append(lines, renderDebugRow(rowWidth, fmt.Sprintf("%-*s", labelWidth, label), value))
685-
}
686-
return strings.Join(lines, "\n")
687-
}
688-
689-
func renderStatus(state string) string {
690-
switch strings.ToLower(strings.TrimSpace(state)) {
691-
case "ok":
692-
return debugStatusOKStyle.Render("OK")
693-
case "warning":
694-
return debugStatusWarningStyle.Render("WARNING")
695-
default:
696-
return debugMutedStyle.Render(strings.ToUpper(strings.TrimSpace(state)))
697-
}
698-
}
699-
700-
func renderDebugRow(width int, label, value string) string {
701-
valueWidth := max(0, width-lipgloss.Width(label)-2)
702-
row := label
703-
if strings.TrimSpace(label) != "" {
704-
row += " "
705-
}
706-
row += lipgloss.NewStyle().
707-
Width(valueWidth).
708-
Background(lipgloss.Color("#112235")).
709-
Render(value)
710-
return debugRowStyle.Width(width).Render(row)
711-
}
712-
713-
func max(a, b int) int {
714-
if a > b {
715-
return a
716-
}
717-
return b
718-
}
719-
720-
func debugPanelWidth() int {
721-
if columns, err := strconv.Atoi(strings.TrimSpace(os.Getenv("COLUMNS"))); err == nil && columns > 0 {
722-
return max(40, columns)
723-
}
724-
if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && width > 0 {
725-
return max(40, width)
726-
}
727-
return 100
728-
}
729-
730-
func debugContentWidth() int {
731-
return max(20, debugPanelWidth()-4)
732-
}
733-
734-
func debugDivider() string {
735-
return debugSectionDividerStyle.Width(debugContentWidth()).Render(strings.Repeat("─", debugContentWidth()))
736-
}

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ var (
1111
)
1212

1313
func init() {
14-
loginCmd.Flags().Uint("uid", 1, "Drupal user ID to provide a direct login link for")
14+
loginCmd.Flags().Uint("uid", 1, "Drupal user ID to generate the login link for.")
1515
}
1616

1717
// RegisterCommands registers all drupal commands with the plugin SDK

cmd/sync.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,22 @@ var (
2424
var syncCmd = &cobra.Command{
2525
Use: "sync",
2626
Short: "Sync Drupal artifacts between contexts",
27+
Long: `Copy Drupal artifacts from one sitectl context to another.
28+
29+
Available artifacts: database, config (Drupal config/sync directory)`,
2730
}
2831

2932
var syncDatabaseCmd = &cobra.Command{
3033
Use: "database",
3134
Aliases: []string{"db"},
3235
Short: "Sync the Drupal database from one context to another",
36+
Long: `Copy the Drupal database from a source context to a target context.
37+
38+
This backs up the database on the source, transfers the artifact, and imports it into the
39+
target. The command asks for confirmation before importing. Pass --yolo to skip the prompt
40+
in automation.
41+
42+
Use --fresh to always take a new backup rather than reusing one from today.`,
3343
RunE: func(cmd *cobra.Command, args []string) error {
3444
progress := plugin.NewProgressLine(cmd.ErrOrStderr(), "Syncing Drupal Database", "Resolving contexts")
3545
defer progress.Close()
@@ -82,6 +92,10 @@ var syncDatabaseCmd = &cobra.Command{
8292
var syncConfigCmd = &cobra.Command{
8393
Use: "config",
8494
Short: "Sync the Drupal config/sync directory from one context to another",
95+
Long: `Copy the Drupal configuration from a source context to a target context.
96+
97+
This exports config as a tarball from the source, transfers it, and imports it on the target
98+
using drush cim.`,
8599
RunE: func(cmd *cobra.Command, args []string) error {
86100
progress := plugin.NewProgressLine(cmd.ErrOrStderr(), "Syncing Drupal Config", "Resolving contexts")
87101
defer progress.Close()
@@ -132,17 +146,17 @@ var syncConfigCmd = &cobra.Command{
132146
}
133147

134148
func init() {
135-
syncDatabaseCmd.Flags().StringVar(&syncSourceContext, "source", "", "Source sitectl context")
136-
syncDatabaseCmd.Flags().StringVar(&syncTargetContext, "target", "", "Target sitectl context")
137-
syncDatabaseCmd.Flags().BoolVar(&syncFresh, "fresh", false, "Always run a fresh source database backup instead of reusing today/yesterday if available")
138-
syncDatabaseCmd.Flags().StringVar(&syncBackupDir, "backup-dir", "/tmp/sitectl-drupal-jobs/db-backup", "Source host directory used to cache database backup artifacts for sync")
139-
syncDatabaseCmd.Flags().BoolVar(&syncYolo, "yolo", false, "Apply destructive database changes without confirmation")
149+
syncDatabaseCmd.Flags().StringVar(&syncSourceContext, "source", "", "Source sitectl context.")
150+
syncDatabaseCmd.Flags().StringVar(&syncTargetContext, "target", "", "Target sitectl context.")
151+
syncDatabaseCmd.Flags().BoolVar(&syncFresh, "fresh", false, "Always take a fresh backup instead of reusing one from today.")
152+
syncDatabaseCmd.Flags().StringVar(&syncBackupDir, "backup-dir", "/tmp/sitectl-drupal-jobs/db-backup", "Directory on the source host used to cache backup artifacts.")
153+
syncDatabaseCmd.Flags().BoolVar(&syncYolo, "yolo", false, "Skip the confirmation prompt before importing.")
140154
must(syncDatabaseCmd.MarkFlagRequired("source"))
141155
must(syncDatabaseCmd.MarkFlagRequired("target"))
142156

143-
syncConfigCmd.Flags().StringVar(&syncSourceContext, "source", "", "Source sitectl context")
144-
syncConfigCmd.Flags().StringVar(&syncTargetContext, "target", "", "Target sitectl context")
145-
syncConfigCmd.Flags().StringVar(&syncDrupalRootfs, "drupal-rootfs", "", "Drupal rootfs relative to the target context project dir")
157+
syncConfigCmd.Flags().StringVar(&syncSourceContext, "source", "", "Source sitectl context.")
158+
syncConfigCmd.Flags().StringVar(&syncTargetContext, "target", "", "Target sitectl context.")
159+
syncConfigCmd.Flags().StringVar(&syncDrupalRootfs, "drupal-rootfs", "", "Path to the Drupal web root on the target, relative to the project directory.")
146160
must(syncConfigCmd.MarkFlagRequired("source"))
147161
must(syncConfigCmd.MarkFlagRequired("target"))
148162

0 commit comments

Comments
 (0)