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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/docs/reference/security-faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,13 @@ A single `first_launch` event is sent containing only:

- The installed version (e.g., "0.5.9")
- Whether this is a fresh install or upgrade (boolean)
- Which installer was used (`curl`, `powershell`, or `npm`)
- Your anonymous machine ID (random UUID)

No code, queries, file paths, or personal information is included. This event helps us understand adoption and is fully opt-out-able.

The install scripts (`altimate.sh/install`, `install.ps1`) and the npm postinstall send nothing themselves and contact no telemetry endpoint. They only record the version and installer name to a local file that the CLI reads on its next run, so the opt-out above still decides whether anything is ever transmitted.

## What happens when I authenticate via a well-known URL?

When you run `altimate auth login <url>`, the CLI fetches `<url>/.well-known/altimate-code` to discover the server's auth command. Before executing anything:
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ We collect the following categories of events:
| `feature_suggestion` | A post-connection feature suggestion is shown (suggestion_type, suggestions_shown, warehouse_type — no user input) |
| `sql_execute_failure` | A SQL execution fails (warehouse type, query type, error message, PII-masked SQL — no raw values) |
| `core_failure` | An internal tool error occurs (tool name, category, error class, truncated error message, PII-safe input signature, and optionally masked arguments — no raw values or credentials) |
| `first_launch` | Fired once on first CLI run after installation. Contains version and is_upgrade flag. No PII. |
| `first_launch` | Fired once on the first CLI run after an install or upgrade, triggered by a marker file the installer wrote — the installers themselves send nothing. Contains the installed version, `is_upgrade` (whether this machine had ever run altimate-code before), and `install_method` (`curl`, `powershell`, `npm`, or `unknown` for markers written before the field existed). A brand-new install is `is_upgrade: false`. No PII. |
| `task_outcome_signal` | Behavioral quality signal at session end — accepted, error, abandoned, or cancelled. Includes tool count, step count, duration, and last tool category. No user content. |
| `task_classified` | Intent classification of the first user message using keyword matching — category (e.g. `debug_dbt`, `write_sql`, `optimize_query`), confidence score, and detected warehouse type. No user text is sent — only the classified category. |
| `tool_chain_outcome` | Aggregated tool execution sequence at session end — ordered tool names (capped at 50), error count, recovery count, final outcome, duration, and cost. No tool arguments or outputs. |
Expand Down
27 changes: 27 additions & 0 deletions install
Original file line number Diff line number Diff line change
Expand Up @@ -487,13 +487,40 @@ install_from_binary() {
chmod 755 "$dest_path"
}

# Write the same post-install marker that npm's postinstall.mjs writes, so the
# CLI emits its `first_launch` telemetry event on the next run. Without this the
# curl install path — the one advertised at altimate.sh/install — produces no
# install event at all, and every curl user is invisible in install metrics.
#
# The path MUST match welcome.ts's data-dir resolution ($XDG_DATA_HOME, falling
# back to ~/.local/share) on every platform, including Windows: the CLI reads it
# via Node's os.homedir() and never consults %LOCALAPPDATA%.
#
# No network call and no identifier is written here — this only hands the CLI the
# version it was installed at. Whether anything is ever sent remains entirely up
# to the CLI's existing telemetry opt-out gates.
write_install_marker() {
local data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/altimate-code"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: On the Windows bash path this marker lands where the CLI never reads it. The install script explicitly supports Windows (MINGW/MSYS/CYGWIN resolve os="windows", seen around line 92), but write_install_marker resolves the data dir from $HOME, while welcome.ts resolves it via Node's os.homedir() (getDataDir(): process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share")). Under MSYS2/Cygwin $HOME is the POSIX home (/home/<user>), which does not match Windows' os.homedir() (%USERPROFILE%), so the .installed-version/.install-source files would be written to a location the CLI never checks — the exact silent failure this PR intends to fix. The in-diff comment claims the path "MUST match ... on every platform, including Windows", which is not guaranteed on the bash path. Consider deriving the fallback from the user profile on the Windows branches (e.g. test "$os" = windows using $USERPROFILE instead of $HOME), or document/limit the bash installer's Windows support.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install, line 503:

<comment>On the Windows bash path this marker lands where the CLI never reads it. The `install` script explicitly supports Windows (MINGW/MSYS/CYGWIN resolve `os="windows"`, seen around line 92), but `write_install_marker` resolves the data dir from `$HOME`, while welcome.ts resolves it via Node's `os.homedir()` (`getDataDir()`: `process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share")`). Under MSYS2/Cygwin `$HOME` is the POSIX home (`/home/<user>`), which does not match Windows' `os.homedir()` (`%USERPROFILE%`), so the `.installed-version`/`.install-source` files would be written to a location the CLI never checks — the exact silent failure this PR intends to fix. The in-diff comment claims the path "MUST match ... on every platform, including Windows", which is not guaranteed on the bash path. Consider deriving the fallback from the user profile on the Windows branches (e.g. `test "$os" = windows` using `$USERPROFILE` instead of `$HOME`), or document/limit the bash installer's Windows support.</comment>

<file context>
@@ -487,13 +487,40 @@ install_from_binary() {
+# version it was installed at. Whether anything is ever sent remains entirely up
+# to the CLI's existing telemetry opt-out gates.
+write_install_marker() {
+    local data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/altimate-code"
+    # An empty marker is deleted unread by the CLI, so fall back to "unknown"
+    # rather than losing the install: $specific_version is empty whenever the
</file context>

# An empty marker is deleted unread by the CLI, so fall back to "unknown"
# rather than losing the install: $specific_version is empty whenever the
# GitHub API could not be reached (see check_version).
local marker_version="${specific_version:-unknown}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Binary installs now report the literal version local in telemetry. On the --binary path specific_version="local" is set (install line 77), so write_install_marker writes .installed-version = local. welcome.ts then emits first_launch with version: "local" (and the banner reads vlocal installed). Since this change is specifically about counting/measuring installs, local pollutes the version dimension for every --binary install. Either skip the marker on the binary path, or map it to unknown rather than local.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install, line 507:

<comment>Binary installs now report the literal version `local` in telemetry. On the `--binary` path `specific_version="local"` is set (install line 77), so `write_install_marker` writes `.installed-version` = `local`. welcome.ts then emits `first_launch` with `version: "local"` (and the banner reads `vlocal installed`). Since this change is specifically about *counting/measuring* installs, `local` pollutes the version dimension for every `--binary` install. Either skip the marker on the binary path, or map it to `unknown` rather than `local`.</comment>

<file context>
@@ -487,13 +487,40 @@ install_from_binary() {
+    # An empty marker is deleted unread by the CLI, so fall back to "unknown"
+    # rather than losing the install: $specific_version is empty whenever the
+    # GitHub API could not be reached (see check_version).
+    local marker_version="${specific_version:-unknown}"
+    mkdir -p "$data_dir" 2>/dev/null || return 0
+    printf '%s' "${marker_version#v}" > "$data_dir/.installed-version" 2>/dev/null || return 0
</file context>

mkdir -p "$data_dir" 2>/dev/null || return 0
printf '%s' "${marker_version#v}" > "$data_dir/.installed-version" 2>/dev/null || return 0
printf '%s' "curl" > "$data_dir/.install-source" 2>/dev/null || return 0
Comment on lines +502 to +510

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Fail closed for config-only telemetry opt-out before enabling shell-install telemetry.

If a user sets telemetry.disabled: true without an environment flag, early Telemetry.init() can run before Instance.provide() makes Config.get() available. Its catch path proceeds with telemetry enabled. These new markers then queue and send first_launch for curl and PowerShell installs, and can mint a machine ID despite the user’s configuration.

Defer telemetry initialization until configuration is available, or fail closed and re-initialize after instance setup. Do not state that config opt-out controls transmission until this path is fixed.

  • install#L502-L510: do not enable curl first-launch telemetry while config-only opt-out can be bypassed.
  • install.ps1#L321-L336: do not enable PowerShell first-launch telemetry while config-only opt-out can be bypassed.
  • docs/docs/reference/security-faq.md#L146-L148: correct this opt-out guarantee after the runtime behavior is fixed.

Based on learnings, the config-only telemetry opt-out cold-start gap occurs when doInit() runs before Instance.provide() makes Config.get() available, and its configuration catch path can mint a machine ID despite telemetry.disabled.

📍 Affects 3 files
  • install#L502-L510 (this comment)
  • install.ps1#L321-L336
  • docs/docs/reference/security-faq.md#L146-L148
🤖 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 `@install` around lines 502 - 510, Fix the cold-start telemetry opt-out gap by
ensuring telemetry initialization fails closed or is deferred until Config.get()
is available, then re-initialized after Instance.provide(); do not mint a
machine ID or enable first-launch telemetry when telemetry.disabled is
configured. Apply this to the write_install_marker flow in install (lines
502-510) and the equivalent PowerShell install flow in install.ps1 (lines
321-336). After runtime behavior is corrected, update
docs/docs/reference/security-faq.md (lines 146-148) to accurately state the
config-only opt-out guarantee.

Source: Learnings

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When users use the documented ./install --binary ... mode, this line records curl even though the download branch was skipped. Write unknown for local-binary installs; otherwise first_launch.install_method misattributes them as curl.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install, line 510:

<comment>When users use the documented `./install --binary ...` mode, this line records `curl` even though the download branch was skipped. Write `unknown` for local-binary installs; otherwise `first_launch.install_method` misattributes them as curl.</comment>

<file context>
@@ -487,13 +487,40 @@ install_from_binary() {
+    local marker_version="${specific_version:-unknown}"
+    mkdir -p "$data_dir" 2>/dev/null || return 0
+    printf '%s' "${marker_version#v}" > "$data_dir/.installed-version" 2>/dev/null || return 0
+    printf '%s' "curl" > "$data_dir/.install-source" 2>/dev/null || return 0
+}
+
</file context>
Suggested change
printf '%s' "curl" > "$data_dir/.install-source" 2>/dev/null || return 0
if [ -n "$binary_path" ]; then
printf '%s' "unknown" > "$data_dir/.install-source" 2>/dev/null || return 0
else
printf '%s' "curl" > "$data_dir/.install-source" 2>/dev/null || return 0
fi

}

if [ -n "$binary_path" ]; then
install_from_binary
else
check_version
download_and_install
fi

# Only reached when an install actually happened: check_version exits 0 early
# when the requested version is already present.
write_install_marker

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: --binary installs are misattributed as curl

write_install_marker runs after both install branches, including install_from_binary (the install --binary <path> path). That branch sets specific_version="local", so the marker records install_method: "curl" and version "local" for a local dev build rather than a curl download. Guarding the call keeps the curl metric clean.

Suggested change
write_install_marker
[ -z "$binary_path" ] && write_install_marker

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The new curl/powershell install markers cause a first_launch telemetry event and machine-id generation on next CLI launch. If Telemetry.init() executes before Instance.provide() loads Config.get() (e.g., cold start), and its error-handling path defaults to telemetry enabled, config-only telemetry.disabled settings (without an env var) will be bypassed for these newly-instrumented install paths. Verify Telemetry.init() fails closed when config is not yet available, or defer initialization until config is loaded, before relying on marker-triggered first_launch events here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install, line 522:

<comment>The new curl/powershell install markers cause a first_launch telemetry event and machine-id generation on next CLI launch. If Telemetry.init() executes before Instance.provide() loads Config.get() (e.g., cold start), and its error-handling path defaults to telemetry enabled, config-only telemetry.disabled settings (without an env var) will be bypassed for these newly-instrumented install paths. Verify Telemetry.init() fails closed when config is not yet available, or defer initialization until config is loaded, before relying on marker-triggered first_launch events here.</comment>

<file context>
@@ -487,13 +487,40 @@ install_from_binary() {
 
+# Only reached when an install actually happened: check_version exits 0 early
+# when the requested version is already present.
+write_install_marker
+
 
</file context>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When --binary points to the already-installed file, install_from_binary copies nothing but this unconditional call still creates telemetry markers. Skip marker creation for that no-op path so the next launch does not emit a false first_launch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install, line 522:

<comment>When `--binary` points to the already-installed file, `install_from_binary` copies nothing but this unconditional call still creates telemetry markers. Skip marker creation for that no-op path so the next launch does not emit a false `first_launch`.</comment>

<file context>
@@ -487,13 +487,40 @@ install_from_binary() {
 
+# Only reached when an install actually happened: check_version exits 0 early
+# when the requested version is already present.
+write_install_marker
+
 
</file context>
Suggested change
write_install_marker
if [ -z "$binary_path" ] || ! [ "$binary_path" -ef "${INSTALL_DIR}/$(basename "$binary_path")" ]; then
write_install_marker
fi



add_to_path() {
local config_file=$1
Expand Down
34 changes: 34 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,40 @@ if (-not $needsBaseline) {
}
}

# ---------------------------------------------------------------------------
# Post-install marker (install telemetry)
# ---------------------------------------------------------------------------
# Mirrors npm's postinstall.mjs so the CLI emits its `first_launch` event on the
# next run; without it this install path is invisible in install metrics.
#
# The directory MUST match welcome.ts's resolution - $XDG_DATA_HOME, else
# <home>\.local\share - because the CLI reads it through Node's os.homedir() and
# never looks at %LOCALAPPDATA%. Writing to LOCALAPPDATA here would be silently
# ignored at read time.
#
# No network call and no identifier is written; only the installed version is
# recorded. The CLI's existing telemetry opt-out gates still decide whether
# anything is ever sent.
$dataRoot = if ($env:XDG_DATA_HOME) { $env:XDG_DATA_HOME } else { Join-Path $env:USERPROFILE ".local\share" }
$dataDir = Join-Path $dataRoot "altimate-code"
try {
New-Item -ItemType Directory -Force -Path $dataDir | Out-Null
# The CLI deletes an empty marker without reporting, so fall back to "unknown"
# when the version could not be resolved (GitHub API unreachable).
$markerVersion = if ($specificVersion) { $specificVersion -replace '^v', '' } else { "unknown" }
# -NoNewline: the CLI trims, but keep the file byte-identical to the npm path.
#
# -Encoding ascii, not utf8: the documented entrypoint is `powershell -c "irm ... | iex"`,
# i.e. Windows PowerShell 5.1, where `-Encoding utf8` prepends a UTF-8 BOM. Both values are
# ASCII by construction, so ascii is lossless here and cannot emit one. The CLI's .trim()
# happens to strip a leading BOM (U+FEFF is JS whitespace), but the install-source value is
# matched against a fixed allowlist and must not depend on that.
Set-Content -Path (Join-Path $dataDir ".installed-version") -Value $markerVersion -NoNewline -Encoding ascii
Set-Content -Path (Join-Path $dataDir ".install-source") -Value "powershell" -NoNewline -Encoding ascii
} catch {
# Non-fatal - a missing marker only costs us the install event.
}

# ---------------------------------------------------------------------------
# PATH (user scope, via registry + broadcast)
# ---------------------------------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/script/postinstall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ function writeUpgradeMarker(version) {
const dataDir = path.join(xdgData, "altimate-code")
fs.mkdirSync(dataDir, { recursive: true })
fs.writeFileSync(path.join(dataDir, ".installed-version"), version.replace(/^v/, ""))
// Record the installer so first_launch can distinguish npm from the curl /
// PowerShell install scripts, which write the same marker.
fs.writeFileSync(path.join(dataDir, ".install-source"), "npm")
} catch {
// Non-fatal — the CLI just won't show a welcome banner
}
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,11 @@ export namespace Telemetry {
session_id: string
version: string
is_upgrade: boolean
// altimate_change — which installer wrote the marker. Recorded by the
// installer itself; "unknown" when the marker predates this field or the
// source file was unreadable. Without it, curl and npm installs are
// indistinguishable in the same metric.
install_method: "curl" | "powershell" | "npm" | "unknown"
}
// altimate_change end
// altimate_change start — telemetry for skill management operations
Expand Down
35 changes: 34 additions & 1 deletion packages/opencode/src/cli/welcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@ import { Telemetry } from "../altimate/telemetry"

const APP_NAME = "altimate-code"
const MARKER_FILE = ".installed-version"
// altimate_change start — written alongside MARKER_FILE by whichever installer ran
// (postinstall.mjs, install, install.ps1) so first_launch can attribute the install.
const SOURCE_FILE = ".install-source"
const INSTALL_METHODS = ["curl", "powershell", "npm"] as const
type InstallMethod = (typeof INSTALL_METHODS)[number] | "unknown"

/**
* Read the installer that wrote the marker, then remove the file so it stays in
* lockstep with MARKER_FILE — a stale value must never be attributed to a later
* install whose installer did not write one.
*
* Returns "unknown" for a missing, unreadable, or unrecognized value: the marker
* predates this field on upgrade from an older version, and an unrecognized
* string must not reach the event as a free-form value.
*/
function readInstallMethod(dataDir: string): InstallMethod {
const sourcePath = path.join(dataDir, SOURCE_FILE)
try {
const raw = fs.readFileSync(sourcePath, "utf-8").trim()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When the CLI starts between the two installer writes, this reader consumes the version marker without a committed source marker and misclassifies install_method. Publish both values through one atomic metadata record, or write a complete pair before exposing the version marker.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/welcome.ts, line 30:

<comment>When the CLI starts between the two installer writes, this reader consumes the version marker without a committed source marker and misclassifies `install_method`. Publish both values through one atomic metadata record, or write a complete pair before exposing the version marker.</comment>

<file context>
@@ -9,6 +9,32 @@ import { Telemetry } from "../altimate/telemetry"
+function readInstallMethod(dataDir: string): InstallMethod {
+  const sourcePath = path.join(dataDir, SOURCE_FILE)
+  try {
+    const raw = fs.readFileSync(sourcePath, "utf-8").trim()
+    fs.unlinkSync(sourcePath)
+    return (INSTALL_METHODS as readonly string[]).includes(raw) ? (raw as InstallMethod) : "unknown"
</file context>

fs.unlinkSync(sourcePath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: In readInstallMethod, the read and unlink share one try block. If readFileSync succeeds but unlinkSync throws, the catch returns "unknown" and leaves the stale .install-source on disk — contradicting the function's own "stale value must never be attributed" invariant, since a later install that writes the version marker but fails to write a source would pick up the leftover value. Separate the unlink from the read (e.g. unlink in its own try) so a valid method is still reported and the file is still removed when possible.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/welcome.ts, line 31:

<comment>In `readInstallMethod`, the read and unlink share one try block. If `readFileSync` succeeds but `unlinkSync` throws, the catch returns "unknown" and leaves the stale `.install-source` on disk — contradicting the function's own "stale value must never be attributed" invariant, since a later install that writes the version marker but fails to write a source would pick up the leftover value. Separate the unlink from the read (e.g. unlink in its own try) so a valid method is still reported and the file is still removed when possible.</comment>

<file context>
@@ -9,6 +9,32 @@ import { Telemetry } from "../altimate/telemetry"
+  const sourcePath = path.join(dataDir, SOURCE_FILE)
+  try {
+    const raw = fs.readFileSync(sourcePath, "utf-8").trim()
+    fs.unlinkSync(sourcePath)
+    return (INSTALL_METHODS as readonly string[]).includes(raw) ? (raw as InstallMethod) : "unknown"
+  } catch {
</file context>

return (INSTALL_METHODS as readonly string[]).includes(raw) ? (raw as InstallMethod) : "unknown"
} catch {
return "unknown"
}
}
// altimate_change end

/** Resolve the data directory at call time (respects XDG_DATA_HOME changes in tests). */
function getDataDir(): string {
Expand All @@ -27,12 +53,18 @@ function getDataDir(): string {
*/
export function showWelcomeBannerIfNeeded(): void {
try {
const markerPath = path.join(getDataDir(), MARKER_FILE)
const dataDir = getDataDir()
const markerPath = path.join(dataDir, MARKER_FILE)
if (!fs.existsSync(markerPath)) return

const installedVersion = fs.readFileSync(markerPath, "utf-8").trim()
if (!installedVersion) {
fs.unlinkSync(markerPath)
// altimate_change — clear the companion file too, so an orphaned source value
// cannot be attributed to a later install. Both install scripts write "unknown"
// rather than an empty version, so this path should now only be reachable from
// a truncated or hand-edited marker.
readInstallMethod(dataDir)
return
}

Expand Down Expand Up @@ -64,6 +96,7 @@ export function showWelcomeBannerIfNeeded(): void {
session_id: "",
version: installedVersion,
is_upgrade: isUpgrade,
install_method: readInstallMethod(dataDir),
})
// altimate_change end

Expand Down
121 changes: 120 additions & 1 deletion packages/opencode/test/cli/welcome.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"
import { describe, test, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"
import fs from "fs"
import path from "path"
import os from "os"
import { Telemetry } from "@/altimate/telemetry"

describe("showWelcomeBannerIfNeeded", () => {
let tmpDir: string
Expand Down Expand Up @@ -70,4 +71,122 @@ describe("showWelcomeBannerIfNeeded", () => {
const { showWelcomeBannerIfNeeded } = await import("../../src/cli/welcome")
expect(() => showWelcomeBannerIfNeeded()).not.toThrow()
})

// altimate_change start — first_launch is the only install metric, and after AI-8448 the curl and
// PowerShell installers feed it too. These assert the two fields the install dashboard reads.
describe("first_launch event", () => {
const dataFiles = (version = "1.2.3", source?: string) => {
const dir = path.join(tmpDir, "altimate-code")
fs.writeFileSync(path.join(dir, ".installed-version"), version)
if (source !== undefined) fs.writeFileSync(path.join(dir, ".install-source"), source)
return dir
}

/**
* The machine-id probe reads os.homedir(), which must be stubbed rather than
* driven through $HOME: Bun resolves homedir() once at startup and ignores
* later mutation of process.env.HOME. Without this the result depends on
* whether the developer running the suite has ever launched the CLI.
*/
function withHome<T>(home: string, fn: () => T): T {
const spy = spyOn(os, "homedir").mockImplementation(() => home)
try {
return fn()
} finally {
spy.mockRestore()
}
}

function captureEvents() {
const events: Telemetry.Event[] = []
spyOn(Telemetry, "track").mockImplementation((e: Telemetry.Event) => {
events.push(e)
})
return events
}

afterEach(() => mock.restore())

test("a machine with no prior identity reports is_upgrade false — the brand-new-install signal", async () => {
dataFiles()
const events = captureEvents()
const cleanHome = fs.mkdtempSync(path.join(os.tmpdir(), "welcome-home-"))

const { showWelcomeBannerIfNeeded } = await import("../../src/cli/welcome")
withHome(cleanHome, () => showWelcomeBannerIfNeeded())

const e = events[0] as any
expect(e.type).toBe("first_launch")
expect(e.is_upgrade).toBe(false)
expect(e.version).toBe("1.2.3")
fs.rmSync(cleanHome, { recursive: true, force: true })
})

test("a pre-existing machine-id reports is_upgrade true", async () => {
dataFiles()
const events = captureEvents()
const usedHome = fs.mkdtempSync(path.join(os.tmpdir(), "welcome-home-"))
fs.mkdirSync(path.join(usedHome, ".altimate"), { recursive: true })
fs.writeFileSync(path.join(usedHome, ".altimate", "machine-id"), "8f1c0c4e-0a5e-4f4e-9c1a-2b3c4d5e6f70")

const { showWelcomeBannerIfNeeded } = await import("../../src/cli/welcome")
withHome(usedHome, () => showWelcomeBannerIfNeeded())

expect((events[0] as any).is_upgrade).toBe(true)
fs.rmSync(usedHome, { recursive: true, force: true })
})

test("attributes the installer that wrote the marker and consumes the source file", async () => {
const dir = dataFiles("1.2.3", "curl")
const events = captureEvents()
const cleanHome = fs.mkdtempSync(path.join(os.tmpdir(), "welcome-home-"))

const { showWelcomeBannerIfNeeded } = await import("../../src/cli/welcome")
withHome(cleanHome, () => showWelcomeBannerIfNeeded())

expect((events[0] as any).install_method).toBe("curl")
// Left behind, it would be attributed to the next install whose installer wrote none.
expect(fs.existsSync(path.join(dir, ".install-source"))).toBe(false)
fs.rmSync(cleanHome, { recursive: true, force: true })
})

test("an absent source file reports unknown rather than dropping the event", async () => {
dataFiles("1.2.3")
const events = captureEvents()
const cleanHome = fs.mkdtempSync(path.join(os.tmpdir(), "welcome-home-"))

const { showWelcomeBannerIfNeeded } = await import("../../src/cli/welcome")
withHome(cleanHome, () => showWelcomeBannerIfNeeded())

// Upgrades from a version whose installer predates the source file land here.
expect((events[0] as any).install_method).toBe("unknown")
fs.rmSync(cleanHome, { recursive: true, force: true })
})

test("an unrecognised source value cannot mint a new dimension", async () => {
dataFiles("1.2.3", "hand-edited-nonsense")
const events = captureEvents()
const cleanHome = fs.mkdtempSync(path.join(os.tmpdir(), "welcome-home-"))

const { showWelcomeBannerIfNeeded } = await import("../../src/cli/welcome")
withHome(cleanHome, () => showWelcomeBannerIfNeeded())

expect((events[0] as any).install_method).toBe("unknown")
fs.rmSync(cleanHome, { recursive: true, force: true })
})

test("an empty marker emits nothing and clears the orphaned source file", async () => {
const dir = dataFiles("", "curl")
const events = captureEvents()
const cleanHome = fs.mkdtempSync(path.join(os.tmpdir(), "welcome-home-"))

const { showWelcomeBannerIfNeeded } = await import("../../src/cli/welcome")
withHome(cleanHome, () => showWelcomeBannerIfNeeded())

expect(events).toHaveLength(0)
expect(fs.existsSync(path.join(dir, ".install-source"))).toBe(false)
fs.rmSync(cleanHome, { recursive: true, force: true })
})
})
// altimate_change end
})
Loading
Loading