feat: zero-friction setup — guided CLI install, onboarding wizard, guided create flows#1879
feat: zero-friction setup — guided CLI install, onboarding wizard, guided create flows#1879yannickmonney wants to merge 16 commits into
Conversation
…ided create flows CLI / install - Zero prerequisites: auto-install Docker behind one consent prompt (macOS Homebrew→dmg, Windows winget→official installer+WSL2, Linux get.docker.com, with curl/wget bootstrap) via a pure, fully-tested planDockerInstall(); new `tale setup` orchestrator; thin installers now verify SHA256 checksums and hand off to setup - `tale init` asks one "local trial vs production domain" question and produces a deploy-ready .env for both; `tale deploy` preflight validates the Docker daemon + Let's Encrypt prerequisites before mutating anything - `default` is a scaffold template, never an organization; real orgs live under .tale/orgs/<slug>/; grouped, example-rich `tale --help` + banner Platform - Reusable multi-step Wizard primitive (host-agnostic; stories + a11y tests) - First-run onboarding / per-org setup wizard; removed the silently auto-created `default` org (auth hook + dashboard); added userPreferences.onboardingCompleted; updated E2E auth setup + new spec - Field info-tooltips on Label/Input (agent, provider, automation) and "Create" empty-state CTAs across all entity lists (agents, projects, teams, api-keys, websites, products, knowledge, customers, vendors) - Products create dialog rebuilt as a 3-step wizard (Basics → Pricing → Review) Docs - Simplified README (en/de/fr) for the zero-prereq / trial-vs-prod flow - Full CLI command reference with every argument documented and explicit required / optional / default conventions (en/de/fr)
📝 WalkthroughWalkthroughThis PR updates the CLI bootstrap path, Docker readiness handling, deploy preflight checks, onboarding flow, shared UI controls, docs, and locale strings. It also changes release automation to generate and upload checksums, adds smoke tests, and rewires the dashboard to send new users to the onboarding wizard. Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. |
There was a problem hiding this comment.
Actionable comments posted: 27
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
services/platform/app/routes/dashboard/index.tsx (1)
66-75: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHandle pre-
trysession fetch failures to avoid a permanent loading dead-end.At Line 74,
authClient.getSession()is awaited before the guarded block that starts at Line 95. If that await rejects, the async flow exits whileresolvedRef.currentis alreadytrue(set at Line 64), so this route can stay on<DashboardShellFrame />without recovery. Please move session fetch inside protected error handling and add an explicit retry/error path instead of relying only on the ref gate.Also applies to: 95-112
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/platform/app/routes/dashboard/index.tsx` around lines 66 - 75, The code currently awaits authClient.getSession() before entering the guarded try/catch guarded by resolvedRef.current, which can leave resolvedRef.current true on rejection and cause a permanent loading state; move the call to authClient.getSession() inside the try block that contains the rest of the resolution logic (the block that references sessionActive, user.lastActiveOrganizationId, and membership fallback) so any rejection is caught, and in the catch set a recovery path (e.g., set an error/retry state or reset resolvedRef.current = false) and surface a retry UI or re-invoke the resolution logic; update the logic that follows (the code around resolvedRef.current usage and the membership/org selection) to handle the new error/retry state so the component can recover instead of remaining stuck.services/platform/app/features/products/components/product-create-dialog.tsx (1)
72-73: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPrevent duplicate creates from repeated Finish clicks.
onFinishcallshandleSubmit(onSubmit), butonSubmitfirescreateProductwithout awaiting completion, so users can trigger multiple writes whileisPendingis true.💡 Suggested fix
- const { mutate: createProduct, isPending: isSubmitting } = useCreateProduct(); + const { mutateAsync: createProduct, isPending: isSubmitting } = + useCreateProduct(); - const onSubmit = (data: ProductFormData) => { + const onSubmit = async (data: ProductFormData) => { const statusValue = data.status || undefined; - createProduct( - { + await createProduct({ organizationId, name: data.name.trim(), description: data.description.trim() || undefined, imageUrl: data.imageUrl.trim() || undefined, stock: data.stock ? parseInt(data.stock) : undefined, price: data.price ? parseFloat(data.price) : undefined, currency: data.currency || undefined, category: data.category.trim() || undefined, status: statusValue && isProductStatus(statusValue) ? statusValue : undefined, - }, - { - onSuccess: () => { - toast({ - title: tProducts('create.toast.success'), - variant: 'success', - }); - handleClose(); - }, - onError: (err) => { - console.error('Create error:', err); - toast({ - title: tProducts('create.toast.error'), - variant: 'destructive', - }); - }, - }, - ); + }); + toast({ + title: tProducts('create.toast.success'), + variant: 'success', + }); + handleClose(); }; ... - onFinish={handleSubmit(onSubmit)} + onFinish={handleSubmit(async (data) => { + if (isSubmitting) return; + try { + await onSubmit(data); + } catch (err) { + console.error('Create error:', err); + toast({ + title: tProducts('create.toast.error'), + variant: 'destructive', + }); + } + })}Also applies to: 137-169, 191-195
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/platform/app/features/products/components/product-create-dialog.tsx` around lines 72 - 73, The form allows duplicate submissions because onSubmit calls createProduct without awaiting completion; update the submit flow in product-create-dialog.tsx (handlers onFinish, onSubmit where handleSubmit is used) to prevent concurrent creates by either awaiting the createProduct call (e.g., await createProduct(...)) or by early-returning when isSubmitting / isPending is true, and ensure the Finish button is disabled while isSubmitting; reference the mutate alias createProduct from useCreateProduct and the isSubmitting flag to gate or await the mutation to stop repeated writes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/cli.yml:
- Around line 213-218: The gh release upload invocation is passing the templated
variable github.event.inputs.release_tag unquoted which allows shell
interpretation; update the command that calls gh release upload (the line
starting with "gh release upload" and referencing
github.event.inputs.release_tag) to quote the release tag variable so it is
treated as data (e.g., wrap the templated expression in quotes) while leaving
the artifact paths (release/tale_linux, release/tale_macos,
release/tale_windows.exe, release/tale_checksums.txt) and the --repo option
unchanged.
- Around line 16-19: The PR path filter in the pull_request trigger of the
workflow is missing installer scripts under scripts/, so update the paths array
in the cli.yml pull_request section (the pull_request -> paths list) to include
the installer script pattern (e.g., 'scripts/**') alongside 'tools/cli/**' and
'.github/workflows/cli.yml' so PRs touching installer scripts will also trigger
this workflow.
In `@docs/de/self-hosted/install/quickstart.md`:
- Line 40: Aktualisiere die deutsche Beschreibung für "tale init" so sie die
neue Ein-Frage-Setup-Flow widerspiegelt: statt zu sagen, dass "tale init"
explizit nach dem OpenRouter-API-Key fragt und Provider-Konfiguration sofort
verdrahtet, beschreibe, dass "tale init" eine einzelne Setup-Modus-Auswahl
stellt (z. B. automatischer Modus vs. manuelle Einrichtung), anschließend alle
Security-Secrets generiert, die .env schreibt und
Beispiel-Agents/-Workflows/-Integrationen unter "default/" anlegt sowie
Editor-Konfigurationen erzeugt; entferne oder passe die explizite Aussage, dass
der OpenRouter-API-Key immer abgefragt und die Provider-Konfiguration direkt
gesetzt wird.
In `@docs/en/self-hosted/install/first-admin.md`:
- Line 28: Replace the brittle container-name scraping fallback with the CLI
admin command: update the docs to prefer using the tale CLI admin command ("tale
convex admin") for CLI-based installs and reference the implementation in
tools/cli/src/commands/convex.ts so readers use the stable `tale convex admin`
flow rather than `docker ps | grep platform | head -1`.
In `@docs/fr/self-hosted/install/quickstart.md`:
- Around line 8-12: Update the "Il te faut deux choses" prerequisites to match
the new zero-prereq install flow: remove the absolute requirement that Docker is
already installed/running and instead state that the installer will
auto-install/configure Docker Engine (and Compose plugin on Linux) if needed;
keep the note about an OpenRouter API key as optional/recommended for agent
models and clarify that other providers can be configured later. Locate and edit
the prerequisites paragraph and the two bullets in quickstart.md to reflect this
wording change.
In `@scripts/install-cli.ps1`:
- Around line 119-125: The current catch block around the Invoke-WebRequest for
$sumsUrl in Verify-Checksum indiscriminately skips checksum verification on any
error; change it so you only treat a 404 (checksum not published) as a skip and
abort on all other failures. In the catch for the Invoke-WebRequest that fetches
$sums, inspect the exception/response status (from
$_.Exception.Response.StatusCode or equivalent) and if it equals 404, log the
existing "No checksum file published for $tag; skipping verification." and
return; otherwise rethrow or terminate the installer (e.g., call throw or exit
with an error) so transient/network/proxy errors do not silently bypass
integrity checks. Ensure you update the catch in the same scope where $sumsUrl,
$tag and Verify-Checksum are used.
In `@services/platform/app/components/ui/wizard/index.ts`:
- Around line 1-15: This index barrel exports Wizard, WizardStep, WizardProps,
WizardStepProps (from './wizard'), WizardProgress and WizardProgressProps (from
'./wizard-progress'), WizardFooter and WizardFooterProps (from
'./wizard-footer'), and useWizard-related types (from './use-wizard'); remove
this barrel module file entirely and update all import sites to import those
symbols directly from their concrete files (e.g., import { Wizard, WizardStep }
from './wizard'; import { WizardProgress } from './wizard-progress'; import {
WizardFooter } from './wizard-footer'; import { useWizard, type
WizardContextValue, type WizardBeforeNext } from './use-wizard') so dependency
edges remain explicit.
In `@services/platform/app/components/ui/wizard/wizard.tsx`:
- Around line 42-48: The defaultFormatProgress function currently returns a
hardcoded English string; update it to resolve text via the app's translation
flow instead of hardcoding English: either make defaultFormatProgress accept a
translation function (e.g., t) and use it to build the string, or move the
translation call into the Wizard component (where useTranslation or i18n is
available) and pass a localized formatter to the formatProgress prop; update
references to defaultFormatProgress and the Wizard component's formatProgress
prop so aria-live uses the translated message rather than the hardcoded "Step X
of Y: Z".
- Around line 111-137: The goNext function calls onFinish() without the same
submitting guard and error handling used for beforeNextHandlers; update goNext
so both branches that call onFinish (the final-step path and any other direct
onFinish calls) setStatus('submitting') before invoking await onFinish(), wrap
the await in try/catch to log/handle errors and prevent unhandled rejections,
and use finally to restore setStatus('idle') and avoid duplicate submissions
(e.g., skip calling onFinish if status === 'submitting'); reference goNext,
beforeNextHandlers.current, setStatus, and moveTo when making the changes.
- Around line 67-78: maxVisitedIndex is only set from activeIndex once and can
get out of sync when controlledIndex or steps.length change; add a useEffect
that watches controlledIndex (and/or activeIndex), isControlled, and
steps.length and updates maxVisitedIndex via setMaxVisitedIndex(prev =>
Math.max(prev, newActiveIndex)) to ensure gating never falls behind the actual
active step, and also clamp or reset uncontrolledIndex with setUncontrolledIndex
when steps.length shrinks so uncontrolledIndex <= steps.length - 1; apply the
same synchronization logic referenced around the other block (lines ~150-156)
where navigation state is initialized/used.
In
`@services/platform/app/features/customers/components/customers-action-menu.tsx`:
- Around line 33-36: The effect in useEffect that runs on controlledCreateOpen
unconditionally calls setImportMode('manual') and overwrites an upload
selection; change it so the effect only sets manual when opening and the current
importMode is not already 'upload' (i.e., check importMode before calling
setImportMode). Update the useEffect body that references controlledCreateOpen
and setImportMode (and importMode) to early-return when importMode === 'upload'
so handleUploadClick can set 'upload' without being flipped back.
In
`@services/platform/app/features/organization/components/onboarding/onboarding-wizard.tsx`:
- Around line 258-271: The finish checklist's <ul> in onboarding-wizard.tsx (the
element with className "mt-2 flex flex-col gap-3") lacks explicit list
semantics; add role="list" to that <ul> so VoiceOver/Safari treat it correctly
(keep the existing children KeyRound, Bot, Users and their spans unchanged).
In
`@services/platform/app/features/settings/api-keys/components/api-keys-action-menu.tsx`:
- Around line 24-27: Both action menus use a half-controlled pattern: they pick
controlledCreateOpen when present but still fall back to internal setters even
if onCreateOpenChange is missing; fix by treating the component as controlled
only when BOTH controlledCreateOpen and onCreateOpenChange are provided —
compute a boolean (e.g., isControlled = controlledCreateOpen != null &&
onCreateOpenChange != null) and then set isCreateDialogOpen = isControlled ?
controlledCreateOpen : internalCreateOpen and setIsCreateDialogOpen =
isControlled ? onCreateOpenChange : setInternalCreateOpen in ApiKeysActionMenu
and TeamsActionMenu so internal state is only used when uncontrolled.
In `@services/platform/app/features/vendors/components/vendors-action-menu.tsx`:
- Around line 33-36: The effect unconditionally forces importMode to 'manual'
when controlledCreateOpen changes, which overrides a user-set 'upload' mode from
VendorsTable; update the useEffect that watches controlledCreateOpen so it only
calls setImportMode('manual') when the current importMode is not 'upload' (or
otherwise absent) — i.e., guard the call with a check like if
(controlledCreateOpen && importMode !== 'upload') setImportMode('manual'); and
apply the same guarded logic to the other similar effect/block that handles
controlled-open flow so controlled usages preserve the selected import mode.
In `@services/platform/messages/de.json`:
- Line 575: Replace the leftover English term "Automation" in the German tooltip
by updating the value of the "nameTooltip" string to use "Automatisierung" so
the copy reads consistently (the JSON key is "nameTooltip" in
services/platform/messages/de.json); just change the text content accordingly
without altering the surrounding punctuation or key name.
In `@services/platform/messages/fr.json`:
- Around line 2125-2155: The French onboarding strings use formal "vous" forms;
update the keys in this JSON block (title, subtitle, stepsAriaLabel, progress,
steps.workspace/provider/team/finish, workspace.heading/why,
provider.heading/why/cta, team.heading/why/cta,
finish.heading/subtitle/providerItem/agentItem/inviteItem/goToDashboard) to
informal "tu" phrasing and imperatives (e.g., change "Configurez votre espace de
travail" → "Configure ton espace de travail", "Nommez votre espace de travail" →
"Nomme ton espace de travail", "Connecter un fournisseur d'IA" → "Connecte un
fournisseur d'IA", "Invitez votre équipe" → "Invite ton équipe", and update
possessives from "votre" to "ton/tes"); keep meanings identical and consistent
with existing keys and interpolation tokens like "{current}" and "{total}"
intact.
In `@tools/cli/src/commands/deploy/index.ts`:
- Around line 104-107: runDeployPreflight is currently relying on loadEnv()
mutating process.env, creating hidden coupling; instead call loadEnv() here,
store its return value in a local (e.g. env) and pass that into
runDeployPreflight alongside options.dryRun, and update runDeployPreflight's
signature/implementation to accept and use that env object rather than reading
process.env; reference the symbols runDeployPreflight, loadEnv, options.dryRun
and ensure any internal reads inside runDeployPreflight use the passed env
parameter.
In `@tools/cli/src/commands/doctor.ts`:
- Around line 49-63: checkDaemon currently always tells users to start Docker
but daemonReachable can fail because the docker CLI is missing (ENOENT); update
checkDaemon to differentiate CLI-missing vs daemon-not-running by inspecting the
error from daemonReachable (e.g., a returned code/property or parse
status.detail for ENOENT) and set the fix message accordingly: if missing CLI ->
instruct to install Docker / Docker Desktop, otherwise keep the existing
platform-specific "start the Docker daemon" / "Start Docker Desktop" guidance;
reference the checkDaemon function, the status returned by daemonReachable,
status.reachable, and status.detail (or status.code) when implementing the
branching.
In `@tools/cli/src/commands/setup/index.ts`:
- Around line 36-43: Currently findProject(directory) searches ancestors and
short-circuits when any ancestor has a tale.json; change this so an explicit
directory argument is honored: only treat it as “existing project” if a
tale.json exists exactly at the requested directory (i.e., check the target path
for tale.json rather than walking upward). Update the code around the call to
findProject(directory) in setup/index.ts to: when directory is the default
(e.g., '.' or undefined) keep the ancestor search behavior, but when an explicit
directory is provided, perform a direct existence check for tale.json inside
that directory and only then call logger.info and return; leave logger.info
messages and surrounding flow unchanged.
In `@tools/cli/src/lib/actions/deploy-preflight.test.ts`:
- Around line 43-53: Replace brittle English substring assertions in the
deploy-preflight tests that call validateTlsPrereqs (the tests named about
"public domain" and "letsencrypt + missing email → blocks") with assertions on
machine-readable fields instead of message text: remove
expect(issues[0].message).toContain('public domain') and
expect(issues[0].message).toContain('TLS_EMAIL') and instead assert the
non-localized properties on the issues array returned by validateTlsPrereqs
(e.g., expect(issues[0].code).toBe('<expected_code>') or
expect(issues[0].field).toBe('tlsEmail'); if no such properties exist, assert
structure like typeof issues[0].message === 'string' and the expected
issues.length/order) so tests no longer rely on English literals.
In `@tools/cli/src/lib/actions/init.ts`:
- Around line 43-45: The init function currently conflates "aborted" and
"initialized without a mode" by returning undefined; change the function
contract from Promise<DeployMode | undefined> to an explicit discriminated union
(e.g., Promise<{ status: 'initialized'; mode: DeployMode } | { status: 'no-mode'
} | { status: 'aborted' }>), update the abort early returns in init (the abort
paths referenced around the current Line 62 and Line 118) to return { status:
'aborted' }, return { status: 'initialized', mode } on successful init, and {
status: 'no-mode' } where applicable; update all callers (including the setup
flow messaging and the other affected spots around the previously noted ranges
48 and 357-367) to branch on the status field instead of treating undefined as
success or failure.
In `@tools/cli/src/lib/config/ensure-env.ts`:
- Around line 60-62: Update isLocalHostname to treat bare IPv6 literals as local
hosts (in addition to the existing IPv4 literal check and ::1 special-case) so
deriveDomainTls won't choose letsencrypt for plain IPv6 addresses; modify the
isLocalHostname function to detect IPv6 address literals (including bracketed
forms) and return true when they match, ensuring existing hostname/name checks
remain unchanged.
In `@tools/cli/src/lib/docker/ensure-docker.ts`:
- Around line 128-145: The planLinux function builds a "get-docker" strategy but
always uses curl at execution; update the logic that populates the steps array
(in planLinux, referencing probe.hasCurl and probe.hasWget and the local steps
variable) to use wget when curl is not available (e.g., push 'wget -qO-
https://get.docker.com | sh' instead of the curl command) and ensure the
bootstrap step that installs curl via probe.packageManager remains correct;
adjust the steps construction so the actual command added matches the available
downloader (prefer curl if probe.hasCurl, otherwise use probe.hasWget) before
pushing the { kind: 'get-docker', label: 'get.docker.com', steps } strategy.
- Around line 342-350: The runDmgInstall function currently hardcodes the Apple
Silicon DMG URL and fails on Intel macs; update runDmgInstall to detect the host
CPU architecture (e.g., via process.arch or running uname -m) and select the
correct Docker DMG URL (arm64 vs x86_64 / x86) before building the curl command
passed to exec; change the URL assembly inside runDmgInstall (the array passed
to exec('/bin/bash', [...] ) so it uses the chosen URL, and add a sensible
fallback or error if the arch is unrecognized.
In `@tools/cli/src/lib/project/org-dirs.ts`:
- Line 21: ORGS_SUBDIR currently uses OS-dependent join('.tale', 'orgs') which
yields backslashes on Windows and can break compose bind-mount paths; change the
definition of ORGS_SUBDIR to use a POSIX path (e.g. use
path.posix.join('.tale','orgs') or a POSIX literal like '.tale/orgs') so the
resulting string always uses forward slashes and keeps compose mount paths
stable.
In `@tools/cli/src/utils/logger.ts`:
- Around line 85-89: Add an explicit return type to the exported banner
function: change its signature to include ": void" (i.e., export function
banner(version: string): void) and apply the same explicit void return type
consistently to other exported logger functions such as info, success, warn,
error to standardize the file; keep internal helper references (bannerText,
blank) unchanged.
In `@tools/cli/tests/smoke.test.ts`:
- Around line 69-260: Replace English-literal stdout/stderr assertions in the
smoke tests with locale-agnostic invariants: for help/bare invocations (tests
that call run(['--help'], run(['setup','--help'], run([]), etc.) remove
expectations like toContain('Tale CLI - deployment and management tools'),
'Setup:', 'Tale', and other English phrases and instead assert stable signals
such as exit code, presence of command tokens (keep checks that look for command
names like 'setup','init','config'), the existence of grouped section
identifiers if the CLI emits stable IDs or a machine-readable mode (use --json
or --format flags if available via run), and for init/config/status tests rely
on filesystem/state invariants (tale.json keys, checksums, existSync checks
already present) or stderr status codes rather than English messages (replace
toContain('Use --force to overwrite', 'No Tale project found. Run "tale init"
first', 'unknown command') with checks for non-zero exitCode and either a stable
error code/token or a regex that matches locale-neutral markers).
---
Outside diff comments:
In
`@services/platform/app/features/products/components/product-create-dialog.tsx`:
- Around line 72-73: The form allows duplicate submissions because onSubmit
calls createProduct without awaiting completion; update the submit flow in
product-create-dialog.tsx (handlers onFinish, onSubmit where handleSubmit is
used) to prevent concurrent creates by either awaiting the createProduct call
(e.g., await createProduct(...)) or by early-returning when isSubmitting /
isPending is true, and ensure the Finish button is disabled while isSubmitting;
reference the mutate alias createProduct from useCreateProduct and the
isSubmitting flag to gate or await the mutation to stop repeated writes.
In `@services/platform/app/routes/dashboard/index.tsx`:
- Around line 66-75: The code currently awaits authClient.getSession() before
entering the guarded try/catch guarded by resolvedRef.current, which can leave
resolvedRef.current true on rejection and cause a permanent loading state; move
the call to authClient.getSession() inside the try block that contains the rest
of the resolution logic (the block that references sessionActive,
user.lastActiveOrganizationId, and membership fallback) so any rejection is
caught, and in the catch set a recovery path (e.g., set an error/retry state or
reset resolvedRef.current = false) and surface a retry UI or re-invoke the
resolution logic; update the logic that follows (the code around
resolvedRef.current usage and the membership/org selection) to handle the new
error/retry state so the component can recover instead of remaining stuck.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 754e33e8-c112-4064-99bf-cb3676455e3b
⛔ Files ignored due to path filters (2)
.github/assets/logo-dark.svgis excluded by!**/*.svg.github/assets/logo-light.svgis excluded by!**/*.svg
📒 Files selected for processing (92)
.github/workflows/cli.ymlREADME.de.mdREADME.fr.mdREADME.mddocs/de/index.mddocs/de/self-hosted/install/cli-install.mddocs/de/self-hosted/install/first-admin.mddocs/de/self-hosted/install/quickstart.mddocs/en/index.mddocs/en/self-hosted/install/cli-install.mddocs/en/self-hosted/install/first-admin.mddocs/en/self-hosted/install/quickstart.mddocs/fr/index.mddocs/fr/self-hosted/install/cli-install.mddocs/fr/self-hosted/install/first-admin.mddocs/fr/self-hosted/install/quickstart.mdscripts/install-cli.ps1scripts/install-cli.shservices/docs/app/content/frontmatter.jsonservices/platform/app/components/ui/data-table/data-table-empty-state.stories.tsxservices/platform/app/components/ui/data-table/data-table-empty-state.test.tsxservices/platform/app/components/ui/data-table/data-table-empty-state.tsxservices/platform/app/components/ui/data-table/data-table.tsxservices/platform/app/components/ui/forms/input.tsxservices/platform/app/components/ui/forms/label.test.tsxservices/platform/app/components/ui/forms/label.tsxservices/platform/app/components/ui/wizard/index.tsservices/platform/app/components/ui/wizard/use-wizard.tsservices/platform/app/components/ui/wizard/wizard-footer.tsxservices/platform/app/components/ui/wizard/wizard-progress.tsxservices/platform/app/components/ui/wizard/wizard.stories.tsxservices/platform/app/components/ui/wizard/wizard.test.tsxservices/platform/app/components/ui/wizard/wizard.tsxservices/platform/app/features/agents/components/agent-create-dialog.tsxservices/platform/app/features/agents/components/agents-action-menu.tsxservices/platform/app/features/agents/components/agents-table.tsxservices/platform/app/features/automations/components/automation-create-dialog.tsxservices/platform/app/features/customers/components/customers-action-menu.tsxservices/platform/app/features/customers/components/customers-table.tsxservices/platform/app/features/knowledge-entries/components/knowledge-entries-action-menu.tsxservices/platform/app/features/knowledge-entries/components/knowledge-entries-table.tsxservices/platform/app/features/organization/components/onboarding/onboarding-wizard.tsxservices/platform/app/features/organization/components/organization-form.tsxservices/platform/app/features/products/components/product-create-dialog.tsxservices/platform/app/features/products/components/products-action-menu.tsxservices/platform/app/features/products/components/products-table.tsxservices/platform/app/features/projects/components/projects-table.tsxservices/platform/app/features/settings/api-keys/components/api-keys-action-menu.tsxservices/platform/app/features/settings/api-keys/components/api-keys-table.tsxservices/platform/app/features/settings/providers/components/provider-add-panel.tsxservices/platform/app/features/settings/teams/components/teams-action-menu.tsxservices/platform/app/features/settings/teams/components/teams-table.tsxservices/platform/app/features/vendors/components/vendors-action-menu.tsxservices/platform/app/features/vendors/components/vendors-table.tsxservices/platform/app/features/websites/components/websites-action-menu.tsxservices/platform/app/features/websites/components/websites-table.tsxservices/platform/app/routes/dashboard/create-organization.tsxservices/platform/app/routes/dashboard/index.tsxservices/platform/convex/auth.tsservices/platform/convex/user_preferences/mutations.tsservices/platform/convex/user_preferences/schema.tsservices/platform/e2e/setup/auth.setup.tsservices/platform/e2e/specs/onboarding.spec.tsservices/platform/messages/de.jsonservices/platform/messages/en.jsonservices/platform/messages/fr.jsonservices/web/app/components/blocks/hero-headline.tsxservices/web/lib/docs-url.tsservices/web/messages/de.jsonservices/web/messages/en.jsonservices/web/messages/fr.jsontools/cli/src/commands/deploy/index.tstools/cli/src/commands/doctor.tstools/cli/src/commands/setup/index.tstools/cli/src/index.tstools/cli/src/lib/actions/deploy-preflight.test.tstools/cli/src/lib/actions/deploy-preflight.tstools/cli/src/lib/actions/deploy.tstools/cli/src/lib/actions/init.tstools/cli/src/lib/actions/start.tstools/cli/src/lib/compose/generators/generate-dev-compose.tstools/cli/src/lib/config/ensure-env.test.tstools/cli/src/lib/config/ensure-env.tstools/cli/src/lib/docker/daemon-reachable.test.tstools/cli/src/lib/docker/daemon-reachable.tstools/cli/src/lib/docker/ensure-docker.test.tstools/cli/src/lib/docker/ensure-docker.tstools/cli/src/lib/project/org-dirs.test.tstools/cli/src/lib/project/org-dirs.tstools/cli/src/utils/logger.tstools/cli/tests/smoke.test.tstools/cli/tsconfig.json
💤 Files with no reviewable changes (1)
- services/platform/app/features/organization/components/organization-form.tsx
Knip flagged 8 functions/consts and 14 types that were exported but used only within their own module; drop the unnecessary `export` (doctor, org-dirs, deploy-preflight, ensure-env, daemon-reachable, ensure-docker). No behavior change. The dmg install fallback hardcoded the arm64 disk image, so on an Intel Mac it downloaded a non-runnable image and stalled on the daemon-ready poll before dropping to manual — the dead end the install plan is meant to avoid. Pick the image that matches process.arch.
pull-image.test.ts mocked the shared docker/logger modules via mock.module, which is process-global in Bun and not reset between test files. New CLI test files shifted Bun's file-evaluation order on Windows so a sibling suite's mock won the shared-module race, breaking all 6 pull-image tests (green on macOS/Linux, red on Windows). Refactor pullImage to accept injected deps (defaulted to the real docker/logger) and pass fakes directly in the test — no mock.module, so the suite is deterministic on every platform. Production callers unchanged.
The real cause of the Windows-only pull-image failures: rollback.test.ts
did `mock.module('../docker/pull-image', …)`, which resolves to the same
module pull-image.test.ts imports for real. Bun's mock.module is
process-global and not reset between files; on Windows its per-file
scoping doesn't hold, so the whole pull-image module was replaced by
rollback's `pullImageMock` (returns undefined) inside pull-image.test.ts,
failing all 6 assertions.
Inject pullImage into rollback() via an optional `deps` arg (defaulted to
the real import) and pass the fake through deps in the test. No file
mock.modules pull-image anymore, so pull-image.test.ts resolves the real
function on every platform. rollback's other collaborators keep their
mock.module stubs — none has a sibling suite importing the real module.
CLI:
- isLocalHostname: treat bare IPv6 literals as non-certifiable local hosts
(no letsencrypt for IP hosts) + tests
- runGetDockerScript: honor wget when curl is absent (the Linux plan already
accepts wget-only hosts)
- doctor: distinguish "docker CLI missing" from "daemon down" in the fix hint
- tale setup <dir>: honor an explicit target instead of short-circuiting on an
ancestor project (findProject walks upward)
- init(): return a discriminated {status} result so `tale setup` no longer
prints "you're set up" after an aborted init
- ORGS_SUBDIR: POSIX literal so Compose bind-mount paths don't get Windows
backslashes
- deploy preflight: pass the env source explicitly
- logger.banner: explicit ": void" return type
Platform:
- Wizard: guard onFinish against double-fire + handle rejection; require a
caller-provided formatProgress (drop the hardcoded English default); sync
maxVisitedIndex to a controlled activeIndex; drop the index.ts barrel
(AGENTS.md) for direct imports
- customers/vendors action menus: don't reset import mode to manual on every
controlled open (preserves the "from device" upload path)
- api-keys/teams action menus: treat the dialog as controlled only when BOTH
createOpen and onCreateOpenChange are provided
- onboarding finish checklist: add role="list" (Safari/VoiceOver)
- de "Automation" -> "Automatisierung"; fr onboarding: formal vous -> informal tu
Installer / CI:
- install-cli.ps1: fail closed on checksum-fetch errors; only a 404 skips
- cli.yml: pass release_tag through env and quote it; add scripts/install-cli.*
to the PR/push path filters
Docs:
- first-admin: recommend `tale convex admin` over scraping `docker ps`
Left as-is (false positives): the de/fr quickstart "init prompts for OpenRouter
key" / "Docker prerequisite" notes faithfully mirror the canonical EN quickstart
this PR ships — changing only the translations would break locale parity. The
two CLI-test "English-literal assertion" comments don't apply: the CLI is
English-only (not localized), so those assertions can't drift.
CodeRabbit feedback addressedWorked through all 27 review threads — fixed 23, left 4 as-is with reasoning below. Pushed in Fixed
Left as-is (false positives)
Verified locally: format, CLI (typecheck / lint / 118 tests), platform (typecheck / type-aware lint / wizard UI tests), i18n parity (23), docs (140). |
Continues the zero-friction setup work: - Provision default prompt library: new prompts schema/validators, provision_defaults action + migration, example prompt JSONs, and canonicalize-config helper for stable config diffing. - Replace dagre with ELK for automation and organigram flow layout (shared flow/layout module, use-elk-layout hook); drop dagre types and dagre-layout util; add organigram draft model. - Split the onboarding wizard into per-step components (account, workspace, openrouter, preferences, finish) and rework sign-up/ log-in plus a new /setup route. - Refactor audio/dictation transcription onto a shared transcription_request module; OpenRouter provider consolidation (drop default openai + vercel-gateway providers). - Sync docs (DPA sub-processors, models, providers, cli-install) and en/de/fr messages and READMEs.
| @@ -0,0 +1,128 @@ | |||
| 'use node'; | |||
| @@ -0,0 +1,129 @@ | |||
| 'use node'; | |||
| @@ -0,0 +1,63 @@ | |||
| 'use node'; | |||
| @@ -0,0 +1,167 @@ | |||
| 'use node'; | |||
Self-review + CodeRabbit (local) findings:
- deploy: fix pluralized log ("directorries" -> "directories")
- docs(models): "provider" -> "providers" in the page description
- onboarding: ARIA radiogroup keyboard nav (roving tabindex +
arrows/Home/End) on the language picker; <main> landmark; log
locale-parse failures instead of swallowing them
- a11y: role="list" on the wizard progress <ol>; motion-reduce on the
organigram save spinner
- canonicalize-config: deterministic code-unit sort, dropping the
locale-dependent localeCompare so canonical output is byte-identical
across machines
- automations: import the shared ElkPoint type instead of redefining it
- prompts: drop the hardcoded "Untitled prompt" fallback; skip a
malformed catalog entry instead; move the re-export to the file bottom
- transcription: normalize dictation text to '' so v.string() holds
- projects: persist agent + model settings atomically via Promise.all
CI gates:
- knip: drop three exports unused outside their module
(AGENT_SET_ARRAY_FIELDS, RecommendedModel, ResolvedPromptDisplay)
- e2e: onboarding setup skips the single optional step
(workspace -> provider -> finish); the team step never shipped
- security: override esbuild to 0.28.1 to clear GHSA-gv7w-rqvm-qjhr
(HIGH), flagged by both bun audit and trivy via vite/storybook
Addresses PR review: the third-party telemetry opt-out vars (DO_NOT_TRACK, TURBO/STORYBOOK/HF telemetry, SCARF) no longer live in .env.example. The local dev orchestrator (scripts/dev.ts) sets them via the same process.env default pattern it already uses for PORT/SITE_URL, so .env stays free of non-secret tool toggles. CI (setup-turbo), Docker, and Storybook config continue to set them in their own contexts.
The `tale` help footer hardcoded https://docs.tale.dev. Resolve it from TALE_DOCS_URL (default https://tale.dev/docs), mirroring @tale/ui/seo/globals, so a self-hosted or staging deployment can point users at its own docs site.
CodeQL flagged `let nextIndex = tabbableIndex` as a useless assignment: every non-returning switch branch overwrites it and the default returns, so the initializer is never read. Declare it without one.
After 'Writing tale.json...', `tale init` queried the Docker daemon (docker volume ls) and SOPS-encrypted the provider key with no output — on a daemon that's still warming up (e.g. just installed by `tale setup`) the volume query blocks for seconds and the CLI looks frozen. Announce both steps and bound the volume query with a 15s timeout so an unresponsive daemon can't hang init.
The create-org wizard flow is workspace -> provider(optional) -> finish, so there is exactly one Skip. The spec clicked Skip twice (provider -> team -> finish) against a team step that never shipped, timing out on the second click. Matches the same fix already made in auth.setup.ts.
The onboarding wizard now collects and persists the OpenRouter key in app (OpenRouterStep), so `tale init` no longer prompts for it or writes openrouter.secrets.json — removing the 'API Configuration' step and the init-time SOPS encryption. Next steps are trimmed to the essentials: cd into the project (relative path) and `tale start` (or `tale deploy`); the 'edit the default/ template' and AI-editor lines are gone.
The two launch commands were never executed in the binary smoke suite, so a crash or SIGKILL in their import graph would slip past the grep-only bundle check. Parsing --help proves each command's dependency tree loads and runs.
…path Now that the onboarding wizard owns the OpenRouter key (saved server-side to the platform DB), `tale init` never writes providers/openrouter.secrets.json on the host — so the 'not found … run tale init to provision a key' warning fired falsely on every start/deploy/upgrade and pointed at a command that no longer provisions it. Remove the warning, the now-unused sops-encrypt.ts module, and its age-encryption dependency (also clears the Knip unused-file/dependency gate).
With Docker installed but the daemon stopped, `docker version`/`docker info` hang indefinitely (socket present, engine not answering). daemonReachable() and doctor's tryRun() ran them with no timeout, and doctor only prints after every check resolves — so the command froze with zero output (SIGKILL under a watchdog). Bound every probe with a timeout and skip the daemon-dependent checks (gVisor/userns) when the daemon is unreachable. Adds a doctor smoke test that fails if the command ever hangs again.
Why
Starting and deploying Tale carried friction at every layer: Docker was a hard prerequisite, the CLI split into two mental models (
tale startvstale deploy),tale --helpwas an ungrouped dump, the first run silently created adefaultorg, there was no onboarding, and every "Create X" was an unguided single-form dump. This brings the friction toward zero — one appealing, guided path that's the same for a local trial and a production deployment, plus in-app guidance on every step.What changed
CLI / install (zero prerequisites)
.dmg(installs Homebrew first if missing), Windows winget→official installer (+WSL2), Linuxget.docker.com(bootstraps curl/wget if needed). The decision is a pure, fully unit-testedplanDockerInstall(); every clean-device branch ends in a guided fallback (never a dead end).tale setuporchestrator the installers hand off to; thin installers now verify SHA256 checksums (tale_checksums.txt).tale initasks one question — local trial or production domain — and produces a deploy-ready.enveither way (footgun guard downgrades local hosts to self-signed).tale deployruns a preflight that validates the Docker daemon + Let's Encrypt prerequisites before mutating anything.defaultis a scaffold template, never an organization; real orgs live under.tale/orgs/<slug>/(CLI discovery + dev-compose only — backend path resolution is unchanged). Grouped, example-richtale --help+ banner.Platform (guided onboarding + create flows)
Wizardprimitive (app/components/ui/wizard/) — host-agnostic, per-step validity gating, asynconBeforeNext, focus +aria-live; Storybook stories + a11y tests.defaultorg (auth hook + dashboard route to the wizard); addeduserPreferences.onboardingCompleted; updated E2E auth setup + new onboarding spec.Docs
Verification
bun run format:check✅ ·lint+typecheckacross@tale/platform,@tale/cli,@tale/docs,@tale/web✅ (type-aware, 208 rules)Follow-up (not in this PR)
auth.setup.tsare included; executing needs the running stack + mock-LLM).Summary by CodeRabbit
New Features
tale setupguided entry point with Docker detectionDocumentation
UI/UX Improvements