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
15 changes: 8 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Changed
- 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.
- A non-interactive run (no TTY, e.g. invoked by a coding agent or in CI) now fails fast with a clear, agent-addressed message when no `--agent` is given — instead of hanging on the agent picker. The message tells the agent to re-run with `--agent claude|codex|cursor`. Nothing is scaffolded before the check.
- Refreshed the bundled minimal workspace `uv.lock`, bumping `dlt` to 1.28.1.
- `dlthub-start` now hands off to your coding agent instead of running the sample pipeline or opening the dashboard itself. Once the workspace is scaffolded, dependencies are installed, and you're logged in to a `playground` workspace, you pick an agent and either launch it to deploy and run the sample pipeline (via the `deploy-run-sample-pipeline` skill) or skip and get the prompt copied to your clipboard to paste into an agent yourself. If a setup step fails, it still hands off — with a prompt to help you resolve it.
- Cleaner, quieter setup output: scaffolding and dependency install show as one step and login and playground connection as another; login no longer prints its logs unless it errors.
- The closing panel matches the outcome — "You're all set" on success, "Almost there" when setup stopped early or a step failed.
- Replaced leftover "local DuckDB" copy with the managed `playground` destination, and standardized the product name to "dltHub platform".
- The startup banner is hidden when output isn't a TTY (piped/CI/agent-run), where it was just log noise.
- A non-interactive run with no `--agent` now fails fast with a clear, agent-addressed message instead of hanging on the agent picker.
- Refreshed the bundled minimal workspace `uv.lock` (`dlt` 1.28.1).
- Refreshed the bundled AI workbench (Claude/Cursor/Codex skills and rules): the `toolkit-dispatch` skill is now `dlthub-router`, which points you to the right workflow toolkit and installs it on demand.

## [0.8.2] - 2026-06-18

Expand Down
18 changes: 9 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,21 @@ Run the CLI from the checkout:
uv run dlthub-start --help
```

Create a workspace non-interactively (for tests/CI). `--yes`/`-y` and
`--skip-uv-sync` are hidden testing shortcuts — they cut the guided setup short
(`--yes` skips the prompts and the first run; `--skip-uv-sync` also skips the
dependency sync), so the workspace is scaffolded but not run. Both are
deliberately absent from `--help`; the normal, complete path is interactive
(no flag):
Create a workspace non-interactively (for tests/CI). `--setup-only` and
`--scaffold-only` are hidden testing shortcuts that cut the guided setup short
both skip login, playground connection, and the agent hand-off. `--setup-only`
still installs dependencies (and uses the default agent); `--scaffold-only` stops
right after scaffolding, before the dependency sync. Both are deliberately absent
from `--help`; the normal, complete path is interactive (no flag):

```bash
uv run dlthub-start my-workspace --yes
uv run dlthub-start my-workspace --setup-only
```

Create a workspace without running the generated workspace dependency sync:
Create a workspace without installing the generated workspace's dependencies:

```bash
uv run dlthub-start my-workspace --yes --skip-uv-sync
uv run dlthub-start my-workspace --scaffold-only
```

Choose an AI workbench explicitly:
Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ dlthub-start
```

The CLI scaffolds the workspace, checks for `uv` (offering to install it if
missing), installs dependencies with `uv sync`, runs your first pipeline on
dltHub, then prompts for your coding agent and sets up its files. Finally it
launches that agent in the workspace, seeded with a starter prompt — or, if the
agent has no command-line launcher, prints the prompt and copies it to your
clipboard so you can paste it in.
missing), installs dependencies with `uv sync`, logs you in to dltHub and
connects a `playground` workspace, then prompts for your coding agent and sets
up its files. Finally it asks whether to launch that agent in the workspace —
seeded with a prompt to deploy and run the sample pipeline, so the agent (not
the CLI) runs it. Decline, or use an agent with no command-line launcher, and it
prints that prompt (with the bundled skill's location) and copies it to your
clipboard so you can paste it into an agent you start yourself.

The full setup runs through the interactive prompts:

Expand Down Expand Up @@ -101,7 +103,7 @@ The workspace is initialized at the project root, shaped roughly like this:
|-- README.md
|-- .dlt/
|-- .mcp.json
`-- .claude/ # your selected agent (or .cursor/ / .codex/)
`-- .claude/ # your selected agent (.cursor/, or .agents/ for codex)
```

## Next Steps
Expand All @@ -113,8 +115,7 @@ uv run dlthub run load_sample_shop
uv run dlthub show
```

If you created the workspace with `--skip-uv-sync`, finish setup first with
`uv sync`. (If you scaffolded into a subdirectory, `cd` into it first.)
(If you scaffolded into a subdirectory, `cd` into it first.)

## Troubleshooting

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "dlthub-start"
version = "0.8.2"
description = "Create a dltHub workspace and run a guided first experience — scaffold, install, run a sample pipeline, and open your coding agent."
description = "Create a dltHub workspace and hand off to your coding agent — scaffold, install, log in, and connect a playground, then the agent deploys and runs the sample pipeline."
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
Expand Down
155 changes: 86 additions & 69 deletions src/create_dlthub_workspace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@
from pathlib import Path

from . import strings
from .config import AGENT_LAUNCH_COMMANDS, AGENTS, DISTRIBUTION_NAME, PLAYGROUND_WORKSPACE, RECOMMENDED
from .config import (
AGENT_LAUNCH_COMMANDS,
AGENT_SKILLS_DIR,
AGENTS,
DISTRIBUTION_NAME,
ONE_SHOT_ENTRY_SKILL,
PLAYGROUND_WORKSPACE,
RECOMMENDED,
)
from .display import (
console,
copy_to_clipboard,
Expand All @@ -21,7 +29,6 @@
print_next_steps,
substep,
substep_detail,
substep_streaming,
)
from .errors import UvError, WorkspaceDirectoryNotEmptyError, WorkspaceError
from .project_metadata import apply_dlthub_client_source, apply_runtime_base_urls, apply_workspace_name
Expand Down Expand Up @@ -61,8 +68,8 @@ def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog=DISTRIBUTION_NAME,
description=(
"Create a dltHub workspace and run a guided first experience — scaffold, "
"install, run a sample pipeline, and open your coding agent."
"Create a dltHub workspace and hand off to your coding agent — scaffold, install, "
"log in, and connect a playground, then the agent deploys and runs the sample pipeline."
),
)
parser.add_argument(
Expand Down Expand Up @@ -104,7 +111,7 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Stream output from underlying subprocesses (uv, dlthub).",
)
# Dev/CI only; hidden from --help. Both skip the first run.
# Dev/CI only; hidden from --help. Both skip login + playground connection.
# --setup-only installs deps; --scaffold-only skips deps too.
parser.add_argument(
"--setup-only",
Expand Down Expand Up @@ -170,9 +177,16 @@ def run(args: argparse.Namespace) -> None:
if resolution.relocated_from is not None:
console.print(strings.MSG_RELOCATED.format(relocated_from=resolution.relocated_from, project_dir=project_dir))

uv_executable = find_uv()
if uv_executable is None and (
args.setup_only or confirm(strings.PROMPT_INSTALL_UV, recommended=RECOMMENDED.install_uv)
):
uv_executable = execute_uv_install(verbose=verbose)

install_deps = uv_executable is not None and not args.scaffold_only
with substep(
strings.MSG_CREATING_WORKSPACE.format(project_dir=project_dir),
strings.MSG_CREATED.format(project_dir=project_dir),
strings.MSG_CREATING_WORKSPACE,
strings.MSG_WORKSPACE_READY if install_deps else strings.MSG_WORKSPACE_CREATED,
verbose=verbose,
):
copy_scaffold(project_dir, scaffold=scaffold, agent=None)
Expand All @@ -181,54 +195,70 @@ def run(args: argparse.Namespace) -> None:
apply_runtime_base_urls(project_dir, api_base_url=args.api_base_url, auth_base_url=args.auth_base_url)
if args.dlthub_client_source:
apply_dlthub_client_source(project_dir, args.dlthub_client_source)
if uv_executable is not None and not args.scaffold_only:
run_uv_sync(uv_executable, project_dir, verbose=verbose)
substep_detail(strings.MSG_PACKAGE_NAME.format(package_name=package_name))
print_created_tree(scaffold)

uv_executable = find_uv()
if uv_executable is 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, ran=False, verbose=verbose)
console.print(strings.MSG_SKIPPED_UV_AND_SYNC)
print_next_steps(project_dir, scaffold=scaffold, ran=False, needs_uv_install=True, needs_deps=True)
return
_finalize_agent(project_dir, scaffold, args, verbose=verbose)
console.print(strings.MSG_SKIPPED_UV_AND_SYNC)
print_next_steps(project_dir, scaffold=scaffold, needs_uv_install=True, needs_deps=True)
return

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

with substep(strings.MSG_INSTALLING_DEPS, strings.MSG_INSTALLED_DEPS, verbose=verbose):
run_uv_sync(uv_executable, project_dir, verbose=verbose)

# Skipped under --setup-only: the first run's login is interactive. A run that exits
# non-zero degrades to a warning so the rest of setup still completes.
# LIMITATION: `dlthub run --follow` can exit 0 on a failed remote run, so a
# genuine run failure isn't caught here — we'd still report success. Revisit.
first_pipeline_ran = not args.setup_only
if first_pipeline_ran:
try:
_run_first_pipeline(uv_executable, project_dir, verbose=verbose)
console.print(strings.MSG_PLAYGROUND_READY)
except UvError as exc:
first_pipeline_ran = False
console.print(strings.MSG_FIRST_RUN_FAILED.format(message=exc))

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):
if args.setup_only:
_finalize_agent(project_dir, scaffold, args, verbose=verbose)
console.print()
print_next_steps(project_dir, scaffold=scaffold)
return

setup_ok = True
try:
_login_and_connect_playground(uv_executable, project_dir, verbose=verbose)
except UvError as exc:
setup_ok = False
console.print(strings.MSG_SETUP_FAILED.format(message=exc))

agent = _finalize_agent(project_dir, scaffold, args, verbose=verbose)
template = strings.CMD_DEPLOY_RUN_HANDOFF_PROMPT if setup_ok else strings.CMD_RESOLVE_HANDOFF_PROMPT
plan = strings.MSG_LAUNCH_PLAN if setup_ok else strings.MSG_LAUNCH_PLAN_RESOLVE
panel_title = strings.TITLE_ALL_SET if setup_ok else strings.TITLE_ALMOST_THERE
handoff_prompt = template.format(skill_path=_entry_skill_path(project_dir, agent))

if interactive and _agent_launchable(agent):
plan_prompt = template.format(skill_path=f"{AGENT_SKILLS_DIR[agent]}/{ONE_SHOT_ENTRY_SKILL}")
console.print(plan.format(agent=agent, project_dir=project_dir, prompt=plan_prompt))
launch = confirm(
strings.PROMPT_LAUNCH_AGENT,
yes_label=strings.PROMPT_LAUNCH_YES.format(agent=agent),
no_label=strings.PROMPT_LAUNCH_NO,
)
if launch and _launch_agent(project_dir, agent, prompt=handoff_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, ran=first_pipeline_ran, prompt_copied=prompt_copied)
if not interactive:
console.print(handoff_prompt)
return
prompt_copied = copy_to_clipboard(handoff_prompt)
print_next_steps(
project_dir,
scaffold=scaffold,
agent_prompt=handoff_prompt,
panel_title=panel_title,
prompt_copied=prompt_copied,
)


def _finalize_agent(project_dir: Path, scaffold: str, args: argparse.Namespace, *, ran: bool, verbose: bool) -> str:
def _finalize_agent(project_dir: Path, scaffold: str, args: argparse.Namespace, *, 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(ran=ran))
agent = args.agent or (RECOMMENDED.agent if args.setup_only else choose_agent())
with substep(
strings.MSG_ADDING_AGENT_FILES.format(agent=agent),
strings.MSG_ADDED_AGENT_FILES.format(agent=agent),
Expand All @@ -238,6 +268,17 @@ def _finalize_agent(project_dir: Path, scaffold: str, args: argparse.Namespace,
return agent


def _entry_skill_path(project_dir: Path, agent: str) -> Path:
"""Absolute path to the bundled entry skill for ``agent`` in the workspace."""
return project_dir / AGENT_SKILLS_DIR[agent] / ONE_SHOT_ENTRY_SKILL


def _agent_launchable(agent: str) -> bool:
"""True if ``agent`` has a terminal CLI on PATH we can launch."""
base = AGENT_LAUNCH_COMMANDS.get(agent)
return base is not None and shutil.which(base[0]) is not None


def _launch_agent(project_dir: Path, agent: str, *, prompt: str) -> bool:
"""Launch ``agent`` in the workspace, seeded with ``prompt``; False if it has no
terminal CLI on PATH, so the caller falls back to the clipboard panel."""
Expand All @@ -247,48 +288,24 @@ def _launch_agent(project_dir: Path, agent: str, *, prompt: str) -> bool:
executable = shutil.which(base[0])
if executable is None:
return False
console.print(strings.MSG_LAUNCHING_AGENT.format(agent=agent))
console.print(strings.MSG_LAUNCHING_AGENT.format(agent=agent, project_dir=project_dir))
try:
subprocess.run([executable, *base[1:], prompt], cwd=project_dir, check=False)
except OSError:
return False
return True


def _run_first_pipeline(uv_executable: str, project_dir: Path, *, verbose: bool) -> None:
"""Log in, bind the playground workspace, run load_sample_shop, show the run."""
# Login is the only interactive step, so stream it; it also authenticates the steps below.
with substep_streaming(strings.MSG_LOGGING_IN, strings.MSG_LOGGED_IN):
run_uv_command(uv_executable, project_dir, ["run", "dlthub", "login"], verbose=True)

with substep(
strings.MSG_CONNECTING_PLAYGROUND.format(workspace=PLAYGROUND_WORKSPACE),
strings.MSG_CONNECTED_PLAYGROUND.format(workspace=PLAYGROUND_WORKSPACE),
verbose=verbose,
):
def _login_and_connect_playground(uv_executable: str, project_dir: Path, *, verbose: bool) -> None:
"""Log in and bind the playground workspace, the setup the entry skill assumes is done."""
with substep(strings.MSG_CONNECTING_DLTHUB, strings.MSG_CONNECTED_DLTHUB, verbose=verbose):
run_uv_command(uv_executable, project_dir, ["run", "dlthub", "login"], verbose=verbose)
# connect --create errors on an existing workspace, so pass it only when absent.
connect_args = ["run", "dlthub", "workspace", "connect", PLAYGROUND_WORKSPACE]
if not _playground_exists(uv_executable, project_dir):
connect_args.append("--create")
run_uv_command(uv_executable, project_dir, connect_args, verbose=verbose)

# No --follow: submit the run without blocking/streaming; the show step below surfaces its logs.
with substep(strings.MSG_RUNNING_FIRST_PIPELINE, strings.MSG_RAN_FIRST_PIPELINE, verbose=verbose):
run_uv_command(
uv_executable,
project_dir,
["run", "dlthub", "run", "load_sample_shop"],
verbose=verbose,
)

with substep(strings.MSG_SHOWING_RUN, strings.MSG_SHOWED_RUN, verbose=verbose):
run_uv_command(
uv_executable,
project_dir,
["run", "dlthub", "job", "runs", "show", "pipeline.load_sample_shop"],
verbose=verbose,
)


def _workspace_in_list(list_output: str, name: str) -> bool:
"""True if ``name`` appears in the Name column of `dlthub workspace list`.
Expand Down
14 changes: 13 additions & 1 deletion src/create_dlthub_workspace/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,25 @@
"codex": ("codex",),
}

# Per-agent skills directory, relative to the workspace root (validated against the
# generated scaffold in test_config). Used to show where the entry skill lives.
AGENT_SKILLS_DIR = {
"claude": ".claude/skills",
"codex": ".agents/skills",
"cursor": ".cursor/skills",
}

# Name of the platform workspace the first pipeline run binds the project to.
# A binding only (no destination) — the warehouse still comes from the profile.
# Adjust here to rename it everywhere (CLI command + user-facing messages).
PLAYGROUND_WORKSPACE = "playground"

TOOLKITS = ("one-shot",)

# Workflow entry skill of the one-shot toolkit. The non-TTY handoff points the calling
# agent at it; test_config asserts it exists in the generated scaffold so it can't drift.
ONE_SHOT_ENTRY_SKILL = "deploy-run-sample-pipeline"

# The dltHub AI workbench repo that `make generate-ai` / `make update-ai` pull from.
WORKBENCH_REPO_NAME = "dlthub-ai-workbench"
WORKBENCH_REPO = f"https://github.com/{GITHUB_ORG}/{WORKBENCH_REPO_NAME}.git"
Expand All @@ -41,7 +53,7 @@
# To bump: pick a new SHA (the workbench repo has no tags today), update the
# constant below, run `make generate-ai`, commit the resulting scaffold diff
# alongside this change.
WORKBENCH_REF: str | None = "a6a4b08eed5cf362e8ce8115188a7aa903279f32"
WORKBENCH_REF: str | None = "1853f504a88d018c9d47f62bcbdfa43aa3384a35"


@dataclass(frozen=True)
Expand Down
Loading
Loading