Skip to content

feat: zero-friction setup — guided CLI install, onboarding wizard, guided create flows#1879

Open
yannickmonney wants to merge 16 commits into
mainfrom
feat/zero-friction-setup
Open

feat: zero-friction setup — guided CLI install, onboarding wizard, guided create flows#1879
yannickmonney wants to merge 16 commits into
mainfrom
feat/zero-friction-setup

Conversation

@yannickmonney

@yannickmonney yannickmonney commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Why

Starting and deploying Tale carried friction at every layer: Docker was a hard prerequisite, the CLI split into two mental models (tale start vs tale deploy), tale --help was an ungrouped dump, the first run silently created a default org, 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)

  • Auto-installs Docker behind one consent prompt — macOS Homebrew→official .dmg (installs Homebrew first if missing), Windows winget→official installer (+WSL2), Linux get.docker.com (bootstraps curl/wget if needed). The decision is a pure, fully unit-tested planDockerInstall(); every clean-device branch ends in a guided fallback (never a dead end).
  • New tale setup orchestrator the installers hand off to; thin installers now verify SHA256 checksums (tale_checksums.txt).
  • tale init asks one question — local trial or production domain — and produces a deploy-ready .env either way (footgun guard downgrades local hosts to self-signed). tale deploy runs a preflight that 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>/ (CLI discovery + dev-compose only — backend path resolution is unchanged). Grouped, example-rich tale --help + banner.

Platform (guided onboarding + create flows)

  • Reusable multi-step Wizard primitive (app/components/ui/wizard/) — host-agnostic, per-step validity gating, async onBeforeNext, focus + aria-live; Storybook stories + a11y tests.
  • First-run onboarding / per-org setup wizard: names the workspace → connect provider → invite team → finish checklist. Removed the silently auto-created default org (auth hook + dashboard route to the wizard); added userPreferences.onboardingCompleted; updated E2E auth setup + new onboarding spec.
  • Field info-tooltips (agent slug vs display-name, provider baseUrl/apiKey, automation name) and "Create" empty-state CTAs across all 9 entity lists (agents, projects, teams, api-keys, websites, products, knowledge, customers, vendors).
  • Products create dialog rebuilt as a 3-step wizard (Basics → Pricing & inventory → Review). Prompt (inline auto-gen hint) and Task (two-column) kept their proven structure with guided copy — wrap-don't-break.

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).

Verification

  • bun run format:check ✅ · lint + typecheck across @tale/platform, @tale/cli, @tale/docs, @tale/web ✅ (type-aware, 208 rules)
  • Platform UI tests 329 pass / 53 files · Docs tests 140 pass · CLI 114 unit + 12 smoke (smoke run against the compiled binary) ✅
  • i18n parity green across en/de/fr; README heading-parity test green

Follow-up (not in this PR)

  • Run the Playwright E2E suite (onboarding spec + updated auth.setup.ts are included; executing needs the running stack + mock-LLM).
  • Optional deeper multi-step rewrites of the prompt (OCC) and task (two-column) dialogs — deliberately left as wrap-don't-break.

Summary by CodeRabbit

  • New Features

    • Added onboarding wizard for new organization setup
    • Introduced tale setup guided entry point with Docker detection
    • Added automatic Docker installation support across platforms
    • Empty state tables now show primary action buttons
    • Form fields now support tooltip/info labels
    • Product creation uses multi-step wizard workflow
    • Deploy pre-flight configuration validation checks
  • Documentation

    • Rewrote self-hosted quickstart guides (English, German, French) with CLI-first approach
    • Added comprehensive CLI command reference documentation
    • Updated getting-started content with improved onboarding flow
  • UI/UX Improvements

    • Enhanced form inputs with helpful information tooltips
    • Tables now display context-aware action buttons in empty states
    • Permission-based visibility for create/add actions across features
    • Improved progress tracking and step navigation

…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)
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This 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

  • tale-project/tale#1702: Updates the same dashboard routing path and organization bootstrap behavior in services/platform/app/routes/dashboard/index.tsx.
  • tale-project/tale#1698: Also replaces default-org seeding with a redirect to /dashboard/create-organization.
  • tale-project/tale#1752: Shares the org-first filesystem layout and CLI deploy staging refactor in the CLI layer.

Suggested reviewers

  • Israeltheminer
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/zero-friction-setup

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Handle pre-try session 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 while resolvedRef.current is already true (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 win

Prevent duplicate creates from repeated Finish clicks.

onFinish calls handleSubmit(onSubmit), but onSubmit fires createProduct without awaiting completion, so users can trigger multiple writes while isPending is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4eae5ee and 1fad9b0.

⛔ Files ignored due to path filters (2)
  • .github/assets/logo-dark.svg is excluded by !**/*.svg
  • .github/assets/logo-light.svg is excluded by !**/*.svg
📒 Files selected for processing (92)
  • .github/workflows/cli.yml
  • README.de.md
  • README.fr.md
  • README.md
  • docs/de/index.md
  • docs/de/self-hosted/install/cli-install.md
  • docs/de/self-hosted/install/first-admin.md
  • docs/de/self-hosted/install/quickstart.md
  • docs/en/index.md
  • docs/en/self-hosted/install/cli-install.md
  • docs/en/self-hosted/install/first-admin.md
  • docs/en/self-hosted/install/quickstart.md
  • docs/fr/index.md
  • docs/fr/self-hosted/install/cli-install.md
  • docs/fr/self-hosted/install/first-admin.md
  • docs/fr/self-hosted/install/quickstart.md
  • scripts/install-cli.ps1
  • scripts/install-cli.sh
  • services/docs/app/content/frontmatter.json
  • services/platform/app/components/ui/data-table/data-table-empty-state.stories.tsx
  • services/platform/app/components/ui/data-table/data-table-empty-state.test.tsx
  • services/platform/app/components/ui/data-table/data-table-empty-state.tsx
  • services/platform/app/components/ui/data-table/data-table.tsx
  • services/platform/app/components/ui/forms/input.tsx
  • services/platform/app/components/ui/forms/label.test.tsx
  • services/platform/app/components/ui/forms/label.tsx
  • services/platform/app/components/ui/wizard/index.ts
  • services/platform/app/components/ui/wizard/use-wizard.ts
  • services/platform/app/components/ui/wizard/wizard-footer.tsx
  • services/platform/app/components/ui/wizard/wizard-progress.tsx
  • services/platform/app/components/ui/wizard/wizard.stories.tsx
  • services/platform/app/components/ui/wizard/wizard.test.tsx
  • services/platform/app/components/ui/wizard/wizard.tsx
  • services/platform/app/features/agents/components/agent-create-dialog.tsx
  • services/platform/app/features/agents/components/agents-action-menu.tsx
  • services/platform/app/features/agents/components/agents-table.tsx
  • services/platform/app/features/automations/components/automation-create-dialog.tsx
  • services/platform/app/features/customers/components/customers-action-menu.tsx
  • services/platform/app/features/customers/components/customers-table.tsx
  • services/platform/app/features/knowledge-entries/components/knowledge-entries-action-menu.tsx
  • services/platform/app/features/knowledge-entries/components/knowledge-entries-table.tsx
  • services/platform/app/features/organization/components/onboarding/onboarding-wizard.tsx
  • services/platform/app/features/organization/components/organization-form.tsx
  • services/platform/app/features/products/components/product-create-dialog.tsx
  • services/platform/app/features/products/components/products-action-menu.tsx
  • services/platform/app/features/products/components/products-table.tsx
  • services/platform/app/features/projects/components/projects-table.tsx
  • services/platform/app/features/settings/api-keys/components/api-keys-action-menu.tsx
  • services/platform/app/features/settings/api-keys/components/api-keys-table.tsx
  • services/platform/app/features/settings/providers/components/provider-add-panel.tsx
  • services/platform/app/features/settings/teams/components/teams-action-menu.tsx
  • services/platform/app/features/settings/teams/components/teams-table.tsx
  • services/platform/app/features/vendors/components/vendors-action-menu.tsx
  • services/platform/app/features/vendors/components/vendors-table.tsx
  • services/platform/app/features/websites/components/websites-action-menu.tsx
  • services/platform/app/features/websites/components/websites-table.tsx
  • services/platform/app/routes/dashboard/create-organization.tsx
  • services/platform/app/routes/dashboard/index.tsx
  • services/platform/convex/auth.ts
  • services/platform/convex/user_preferences/mutations.ts
  • services/platform/convex/user_preferences/schema.ts
  • services/platform/e2e/setup/auth.setup.ts
  • services/platform/e2e/specs/onboarding.spec.ts
  • services/platform/messages/de.json
  • services/platform/messages/en.json
  • services/platform/messages/fr.json
  • services/web/app/components/blocks/hero-headline.tsx
  • services/web/lib/docs-url.ts
  • services/web/messages/de.json
  • services/web/messages/en.json
  • services/web/messages/fr.json
  • tools/cli/src/commands/deploy/index.ts
  • tools/cli/src/commands/doctor.ts
  • tools/cli/src/commands/setup/index.ts
  • tools/cli/src/index.ts
  • tools/cli/src/lib/actions/deploy-preflight.test.ts
  • tools/cli/src/lib/actions/deploy-preflight.ts
  • tools/cli/src/lib/actions/deploy.ts
  • tools/cli/src/lib/actions/init.ts
  • tools/cli/src/lib/actions/start.ts
  • tools/cli/src/lib/compose/generators/generate-dev-compose.ts
  • tools/cli/src/lib/config/ensure-env.test.ts
  • tools/cli/src/lib/config/ensure-env.ts
  • tools/cli/src/lib/docker/daemon-reachable.test.ts
  • tools/cli/src/lib/docker/daemon-reachable.ts
  • tools/cli/src/lib/docker/ensure-docker.test.ts
  • tools/cli/src/lib/docker/ensure-docker.ts
  • tools/cli/src/lib/project/org-dirs.test.ts
  • tools/cli/src/lib/project/org-dirs.ts
  • tools/cli/src/utils/logger.ts
  • tools/cli/tests/smoke.test.ts
  • tools/cli/tsconfig.json
💤 Files with no reviewable changes (1)
  • services/platform/app/features/organization/components/organization-form.tsx

Comment thread .github/workflows/cli.yml
Comment thread .github/workflows/cli.yml Outdated
Comment thread docs/de/self-hosted/install/quickstart.md
Comment thread docs/en/self-hosted/install/first-admin.md Outdated
Comment thread docs/fr/self-hosted/install/quickstart.md
Comment thread tools/cli/src/lib/docker/ensure-docker.ts
Comment thread tools/cli/src/lib/docker/ensure-docker.ts
Comment thread tools/cli/src/lib/project/org-dirs.ts Outdated
Comment thread tools/cli/src/utils/logger.ts Outdated
Comment thread tools/cli/tests/smoke.test.ts
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.
@yannickmonney

Copy link
Copy Markdown
Contributor Author

CodeRabbit feedback addressed

Worked through all 27 review threads — fixed 23, left 4 as-is with reasoning below. Pushed in f590b233f.

Fixed

  • CLI: IPv6 hosts treated as non-certifiable (no Let's Encrypt for IP literals); get-docker honors wget when curl is absent; doctor distinguishes "docker CLI missing" from "daemon down"; tale setup <dir> honors an explicit target instead of short-circuiting on an ancestor project; init() returns a discriminated {status} so setup no longer prints "you're set up" after an aborted init; ORGS_SUBDIR is a POSIX literal (no Windows backslashes in Compose mounts); deploy preflight gets the env source explicitly; logger.banner typed : void.
  • Wizard: onFinish guarded against double-fire + handles rejection; formatProgress is now required (no hardcoded English fallback); maxVisitedIndex syncs to a controlled activeIndex; the index.ts barrel is removed in favor of direct imports (AGENTS.md).
  • Action menus: customers/vendors keep the "from device" upload mode on a controlled open; api-keys/teams treat the dialog as controlled only when both createOpen and onCreateOpenChange are provided.
  • a11y: onboarding finish checklist gets role="list".
  • i18n: de AutomationAutomatisierung; fr onboarding formal vous → informal tu.
  • Installer / CI: install-cli.ps1 fails closed on checksum-fetch errors (only a 404 skips); cli.yml passes release_tag through the environment and quotes it, and adds scripts/install-cli.* to the path filters.
  • Docs: first-admin recommends tale convex admin over scraping docker ps.

Left as-is (false positives)

  • de/fr quickstart — "init asks for OpenRouter key" / "Docker prerequisite": these faithfully mirror the canonical EN quickstart this PR ships, which still lists both. Changing only the translations would diverge from EN and break locale parity. If the flow description should change, that's an EN-source edit across all three locales — out of scope here.
  • English-literal assertions in deploy-preflight.test.ts / smoke.test.ts: that guideline exists because the UI ships localized labels that drift silently. The CLI is English-only (no i18n), so these assertions are stable — the rationale doesn't apply.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant