Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Replaced leftover "local DuckDB" copy (README, agent-picker prompt, sample pipeline docstring) with the managed `playground` destination, and dropped the misleading "on your local machine" claim from the first-run summary.
- Standardized the product name to "dltHub platform" in the CLI copy (was "dltHub Pro" in a couple of strings).
- The startup banner is now suppressed when output isn't a TTY (piped/CI/agent-run), where it was just noise in captured logs.
- Reworked the post-setup panel so its framing matches the outcome: it now reads "You're all set" only after a successful first run, and "Almost there" when the run failed or setup stopped early. When setup is unfinished, the panel also points forward — listing the remaining install steps followed by the sample-run steps — instead of dead-ending at `uv sync`.
- The coding-agent prompt now adapts to the outcome too: it invites you to "create your own pipeline" only after the demo run succeeded, and shows a neutral "set up your coding agent" line otherwise (e.g. after a failed run), so it no longer presumes a run that didn't happen.

## [0.8.2] - 2026-06-18

Expand Down
17 changes: 8 additions & 9 deletions src/create_dlthub_workspace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
print_created_tree,
print_dir_not_empty,
print_next_steps,
print_resume_steps,
substep,
substep_detail,
substep_streaming,
Expand Down Expand Up @@ -163,15 +162,15 @@ def run(args: argparse.Namespace) -> None:
if args.setup_only or confirm(strings.PROMPT_INSTALL_UV, recommended=RECOMMENDED.install_uv):
uv_executable = execute_uv_install(verbose=verbose)
else:
_finalize_agent(project_dir, scaffold, args, verbose=verbose)
_finalize_agent(project_dir, scaffold, args, ran=False, verbose=verbose)
console.print(strings.MSG_SKIPPED_UV_AND_SYNC)
print_resume_steps(project_dir, uv_installed=False)
print_next_steps(project_dir, scaffold=scaffold, ran=False, needs_uv_install=True, needs_deps=True)
return

if args.scaffold_only:
_finalize_agent(project_dir, scaffold, args, verbose=verbose)
_finalize_agent(project_dir, scaffold, args, ran=False, verbose=verbose)
console.print(strings.MSG_SKIPPED_SYNC)
print_resume_steps(project_dir, uv_installed=True)
print_next_steps(project_dir, scaffold=scaffold, ran=False, needs_deps=True)
return

with substep(strings.MSG_INSTALLING_DEPS, strings.MSG_INSTALLED_DEPS, verbose=verbose):
Expand All @@ -190,19 +189,19 @@ def run(args: argparse.Namespace) -> None:
first_pipeline_ran = False
console.print(strings.MSG_FIRST_RUN_FAILED.format(message=exc))

agent = _finalize_agent(project_dir, scaffold, args, verbose=verbose)
agent = _finalize_agent(project_dir, scaffold, args, ran=first_pipeline_ran, verbose=verbose)

if first_pipeline_ran and _launch_agent(project_dir, agent, prompt=strings.CMD_BUILD_OWN_SOURCE_PROMPT):
return

console.print()
prompt_copied = first_pipeline_ran and copy_to_clipboard(strings.CMD_BUILD_OWN_SOURCE_PROMPT)
print_next_steps(project_dir, scaffold=scaffold, first_pipeline_ran=first_pipeline_ran, prompt_copied=prompt_copied)
print_next_steps(project_dir, scaffold=scaffold, ran=first_pipeline_ran, prompt_copied=prompt_copied)


def _finalize_agent(project_dir: Path, scaffold: str, args: argparse.Namespace, *, verbose: bool) -> str:
def _finalize_agent(project_dir: Path, scaffold: str, args: argparse.Namespace, *, ran: bool, verbose: bool) -> str:
"""Resolve the agent (prompting unless --agent/--setup-only set it) and lay down its AI files."""
agent = args.agent or (RECOMMENDED.agent if args.setup_only else choose_agent())
agent = args.agent or (RECOMMENDED.agent if args.setup_only else choose_agent(ran=ran))
with substep(
strings.MSG_ADDING_AGENT_FILES.format(agent=agent),
strings.MSG_ADDED_AGENT_FILES.format(agent=agent),
Expand Down
97 changes: 47 additions & 50 deletions src/create_dlthub_workspace/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,77 +301,74 @@ def print_created_tree(scaffold: str) -> None:
console.print(f"[dim]{branch}{entry}[/dim]")


def _print_steps_panel(body: Text, *, title: str) -> None:
console.print(
Panel(
body,
title=title,
title_align="left",
border_style="#C6D300",
padding=(1, 2),
)
)


def print_next_steps(
project_dir: Path,
*,
scaffold: str,
first_pipeline_ran: bool = False,
ran: bool = False,
needs_uv_install: bool = False,
needs_deps: bool = False,
prompt_copied: bool = False,
) -> None:
"""Post-setup panel. After the first run, it's just the prompt to hand to the
agent; otherwise it lists the run/edit steps for the scaffold."""
"""The build-your-own prompt when ``ran``, else any remaining setup commands
followed by the steps to run the sample pipeline."""
body = Text()
if first_pipeline_ran:

if ran:
body.append(f"{strings.STEPS_LABEL_BUILD_OWN_SOURCE}\n\n")
body.append(f" {strings.CMD_BUILD_OWN_SOURCE_PROMPT}", style="bold #59C1D5")
if prompt_copied:
body.append(f"\n\n {strings.HINT_PROMPT_COPIED}", style="bold #C6D300")
body.append(f"\n\n {strings.LABEL_DOCS} ", style="dim")
body.append(strings.LINK_DOCS_LABEL, style=f"underline #59C1D5 link {strings.LINK_DOCS_URL}")
else:
# Lead with `cd` only for a subdirectory; in the cwd it's noise (`cd .`).
cd = _cd_target(project_dir)
cd_step: tuple[tuple[str, str | None], ...] = (
() if cd == "." else ((strings.STEPS_LABEL_CD, strings.CMD_CD.format(project_dir=cd)),)
)
steps: tuple[tuple[str, str | None], ...] = (*cd_step, *NEXT_STEPS[scaffold])
body.append(f"{strings.LABEL_WHAT_TO_TRY}\n\n", style="bold #C6D300")
for index, (label, command) in enumerate(steps, start=1):
body.append(f" {index}. {label}\n", style="dim")
if command is not None:
body.append(f" {command}\n", style="bold #59C1D5")
body.append("\n")
_print_steps_panel(body, title=strings.TITLE_ALL_SET)
return

console.print(
Panel(
body,
title=strings.TITLE_NEXT_STEPS_PANEL,
title_align="left",
border_style="#C6D300",
padding=(1, 2),
)
cd = _cd_target(project_dir)
cd_step: tuple[tuple[str, str | None], ...] = (
() if cd == "." else ((strings.STEPS_LABEL_CD, strings.CMD_CD.format(project_dir=cd)),)
)

sections: list[tuple[str, tuple[tuple[str, str | None], ...]]] = []
if needs_uv_install or needs_deps:
finish: list[tuple[str, str | None]] = [*cd_step]
if needs_uv_install:
finish.append((strings.STEPS_LABEL_INSTALL_UV, strings.CMD_INSTALL_UV_UNIX))
if needs_deps:
finish.append((strings.STEPS_LABEL_INSTALL_DEPS, strings.CMD_UV_SYNC))
sections.append((strings.LABEL_FINISH_SETUP, tuple(finish)))
sections.append((strings.LABEL_WHAT_TO_TRY, NEXT_STEPS[scaffold]))
else:
sections.append((strings.LABEL_WHAT_TO_TRY, (*cd_step, *NEXT_STEPS[scaffold])))

def print_resume_steps(project_dir: Path, *, uv_installed: bool) -> None:
"""Remaining setup commands. AI workbench files are already in the
workspace (vendored into the scaffold), so the only thing the user still
needs to do is finish the uv setup."""
steps: list[tuple[str, str]] = []
cd = _cd_target(project_dir)
if cd != ".":
steps.append((strings.STEPS_LABEL_CD, strings.CMD_CD.format(project_dir=cd)))
if not uv_installed:
steps.append((strings.STEPS_LABEL_INSTALL_UV, strings.CMD_INSTALL_UV_UNIX))
steps.append((strings.STEPS_LABEL_INSTALL_DEPS, strings.CMD_UV_SYNC))
step = 1
for index, (header, steps) in enumerate(sections):
if index:
body.append("\n")
body.append(f"{header}\n\n", style="bold #C6D300")
for label, command in steps:
body.append(f" {step}. {label}\n", style="dim")
if command is not None:
body.append(f" {command}\n", style="bold #59C1D5")
body.append("\n")
step += 1

body = Text()
body.append(f"{strings.LABEL_FINISH_SETUP}\n\n", style="bold #C6D300")
for index, (label, command) in enumerate(steps, start=1):
body.append(f" {index}. {label}\n", style="dim")
body.append(f" {command}\n\n", style="bold #59C1D5")
if cd != ".":
body.append(f" {strings.MSG_AGENT_WORKSPACE_NOTE}", style="dim")

console.print(
Panel(
body,
title=strings.TITLE_RESUME_PANEL,
title_align="left",
border_style="#C6D300",
padding=(1, 2),
)
)
_print_steps_panel(body, title=strings.TITLE_ALMOST_THERE)


def print_dir_not_empty(project_dir: Path) -> None:
Expand Down
5 changes: 3 additions & 2 deletions src/create_dlthub_workspace/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def _echo_selection(value: str) -> None:
console.print(f"[{CURSOR_STYLE}]{TICK_CHAR}[/{CURSOR_STYLE}] [bold]{value}[/bold]")


def choose_agent(default: str = RECOMMENDED.agent) -> str:
def choose_agent(default: str = RECOMMENDED.agent, *, ran: bool = False) -> str:
"""Arrow-key select for the coding agent. Exactly one is chosen."""
agents = list(AGENTS)
options = [
Expand All @@ -41,7 +41,8 @@ def choose_agent(default: str = RECOMMENDED.agent) -> str:
]
default_index = agents.index(default) if default in agents else 0

console.print(strings.PROMPT_AGENT_HEADER)
lead = strings.PROMPT_AGENT_LEAD_RAN if ran else strings.PROMPT_AGENT_LEAD_SETUP
console.print(strings.PROMPT_AGENT_HEADER.format(lead=lead))
index = cast(
int,
beaupy.select(
Expand Down
13 changes: 8 additions & 5 deletions src/create_dlthub_workspace/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@

# Prompts ---------------------------------------------------------------
PROMPT_AGENT_HEADER = (
"\n[bold]2. Claude/Cursor/Codex-native dlt pipeline building with dltHub platform[/bold]"
"\nNow create your own dlt pipeline to load data from a REST API source into the dltHub platform."
"\n[bold]Claude/Cursor/Codex-native dlt pipeline building with dltHub platform[/bold]"
"\n{lead}"
"\n\n[bold]Which coding agent do you want to use?[/bold] [dim](↑/↓ to move, enter to confirm)[/dim]"
)
# The lead line swaps on whether the demo run succeeded.
PROMPT_AGENT_LEAD_RAN = "Now create your own dlt pipeline to load data from a REST API source into your playground."
PROMPT_AGENT_LEAD_SETUP = "Set up your coding agent to build dlt pipelines."
PROMPT_INSTALL_UV = "uv is required but was not found. Install uv now?"


Expand Down Expand Up @@ -78,7 +81,7 @@
MSG_SHOWED_RUN = "Showed your pipeline run"
# "What just happened" summary, shown after the first run and before the agent picker.
MSG_PLAYGROUND_READY = (
"\n[bold]1. The dltHub platform playground experience is set[/bold]"
"\n[bold]The dltHub platform playground experience is set[/bold]"
"\nWe created a demo dlt pipeline and set up your dltHub playground."
)
MSG_FIRST_RUN_FAILED = (
Expand All @@ -89,8 +92,8 @@

# Panel titles ----------------------------------------------------------
TITLE_BANNER = "dlthub-start v{version} [bold #C6D300](beta)[/bold #C6D300]"
TITLE_NEXT_STEPS_PANEL = "You're all set"
TITLE_RESUME_PANEL = "Almost there"
TITLE_ALL_SET = "You're all set"
TITLE_ALMOST_THERE = "Almost there"
TITLE_DIR_NOT_EMPTY = "Directory not empty"

# Shown on the next-steps / resume panels only when scaffolding into a
Expand Down
22 changes: 13 additions & 9 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ def _make_args(**overrides: object) -> argparse.Namespace:
"print_banner",
"print_created_tree",
"print_next_steps",
"print_resume_steps",
)


Expand Down Expand Up @@ -177,7 +176,7 @@ def test_agent_files_added_after_the_first_run(self) -> None:
self.m["overlay_agent"].assert_called_once()
self.m["choose_agent"].assert_called_once()
self.m["print_next_steps"].assert_called_once()
self.assertTrue(self.m["print_next_steps"].call_args.kwargs["first_pipeline_ran"])
self.assertTrue(self.m["print_next_steps"].call_args.kwargs["ran"])

def test_launched_agent_replaces_the_next_steps_panel(self) -> None:
self.m["_launch_agent"].return_value = True
Expand All @@ -198,7 +197,7 @@ def test_first_run_failure_degrades_but_finishes_setup(self) -> None:

self.m["overlay_agent"].assert_called_once()
self.m["_launch_agent"].assert_not_called()
self.assertFalse(self.m["print_next_steps"].call_args.kwargs["first_pipeline_ran"])
self.assertFalse(self.m["print_next_steps"].call_args.kwargs["ran"])

def test_explicit_agent_skips_the_prompt(self) -> None:
self._run(agent="codex")
Expand All @@ -210,7 +209,7 @@ def test_setup_only_skips_first_run_and_prompt_but_still_adds_agent_files(self)
self.m["run_uv_command"].assert_not_called()
self.m["choose_agent"].assert_not_called()
self.assertEqual(self.m["overlay_agent"].call_args.kwargs["agent"], RECOMMENDED.agent)
self.assertFalse(self.m["print_next_steps"].call_args.kwargs["first_pipeline_ran"])
self.assertFalse(self.m["print_next_steps"].call_args.kwargs["ran"])

def test_uv_declined_stops_at_scaffold_only_but_adds_agent_files(self) -> None:
self.m["find_uv"].return_value = None
Expand All @@ -221,15 +220,21 @@ def test_uv_declined_stops_at_scaffold_only_but_adds_agent_files(self) -> None:
self.m["execute_uv_install"].assert_not_called()
self.m["run_uv_sync"].assert_not_called()
self.m["overlay_agent"].assert_called_once()
self.m["print_next_steps"].assert_not_called()
self.m["print_resume_steps"].assert_called_once_with(Path("/tmp/test_workspace"), uv_installed=False)
self.m["print_next_steps"].assert_called_once()
kwargs = self.m["print_next_steps"].call_args.kwargs
self.assertFalse(kwargs["ran"])
self.assertTrue(kwargs["needs_uv_install"])
self.assertTrue(kwargs["needs_deps"])

def test_scaffold_only_stops_after_install_but_adds_agent_files(self) -> None:
self._run(scaffold_only=True)
self.m["run_uv_sync"].assert_not_called()
self.m["overlay_agent"].assert_called_once()
self.m["print_next_steps"].assert_not_called()
self.m["print_resume_steps"].assert_called_once_with(Path("/tmp/test_workspace"), uv_installed=True)
self.m["print_next_steps"].assert_called_once()
kwargs = self.m["print_next_steps"].call_args.kwargs
self.assertFalse(kwargs["ran"])
self.assertTrue(kwargs["needs_deps"])
self.assertFalse(kwargs.get("needs_uv_install", False))

def test_first_run_creates_playground_when_absent(self) -> None:
self.m["capture_uv_command"].return_value = "Name\n----\nMy Workspace\n"
Expand Down Expand Up @@ -297,7 +302,6 @@ def _run_with(self, **flags: bool) -> MagicMock:
patch("create_dlthub_workspace.cli._launch_agent", return_value=False),
patch("create_dlthub_workspace.cli.choose_agent", return_value="claude"),
patch("create_dlthub_workspace.cli.print_next_steps"),
patch("create_dlthub_workspace.cli.print_resume_steps"),
patch("create_dlthub_workspace.cli.err_console") as err_console,
_silenced(),
):
Expand Down
34 changes: 28 additions & 6 deletions tests/test_display.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for display panels (next-steps + resume-steps + banner).
"""Tests for display panels (next-steps + banner).

Uses `console.capture()` to grab rendered text so we can assert on
the content of rich panels.
Expand All @@ -21,7 +21,6 @@
copy_to_clipboard,
print_banner,
print_next_steps,
print_resume_steps,
)
from create_dlthub_workspace.scaffold import SCAFFOLDS_DIR

Expand All @@ -40,7 +39,7 @@ def test_first_pipeline_ran_shows_only_the_agent_prompt(self) -> None:
# After the first run, the panel drops the run/edit steps and shows just
# the instruction + the verbatim prompt to hand to the agent.
with console.capture() as cap:
print_next_steps(Path.cwd(), scaffold="minimal_workspace", first_pipeline_ran=True, prompt_copied=True)
print_next_steps(Path.cwd(), scaffold="minimal_workspace", ran=True, prompt_copied=True)
# Strip panel borders (Unicode │ on POSIX, ASCII | on the Windows console)
# and collapse line-wrapping so the prompt is one contiguous string.
output = " ".join(cap.get().replace("│", " ").replace("|", " ").split())
Expand Down Expand Up @@ -111,10 +110,16 @@ def test_created_tree_covers_every_next_steps_scaffold(self) -> None:
self.assertEqual(set(CREATED_TREE), set(NEXT_STEPS))


class PrintResumeStepsTests(unittest.TestCase):
class ResumeStepsTests(unittest.TestCase):
def test_uv_not_installed_includes_install_command(self) -> None:
with console.capture() as cap:
print_resume_steps(Path("/tmp/my_workspace"), uv_installed=False)
print_next_steps(
Path("/tmp/my_workspace"),
scaffold="minimal_workspace",
ran=False,
needs_uv_install=True,
needs_deps=True,
)
output = cap.get()

self.assertIn("Install uv", output)
Expand All @@ -123,12 +128,29 @@ def test_uv_not_installed_includes_install_command(self) -> None:

def test_uv_installed_omits_install_command(self) -> None:
with console.capture() as cap:
print_resume_steps(Path("/tmp/my_workspace"), uv_installed=True)
print_next_steps(
Path("/tmp/my_workspace"),
scaffold="minimal_workspace",
ran=False,
needs_deps=True,
)
output = cap.get()

self.assertNotIn("curl -LsSf", output)
self.assertIn("uv sync", output)

def test_finish_setup_still_points_to_the_sample_run(self) -> None:
with console.capture() as cap:
print_next_steps(
Path("/tmp/my_workspace"),
scaffold="minimal_workspace",
ran=False,
needs_deps=True,
)
output = cap.get()

self.assertIn("uv run dlthub run load_sample_shop", output)


class PrintBannerTests(unittest.TestCase):
def test_renders_with_version_in_title(self) -> None:
Expand Down
Loading
Loading