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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ step --help

Upgrade later with `step update`\.

On macOS/Linux/WSL, installation and upgrades also prepare StepPage, which requires
Node.js 20 or newer on PATH. If StepPage setup fails, Step remains usable and retries
on the next session. Run `step mcp prepare` to retry setup manually.

## Sign in to the Step provider

The default Step Code entrypoint exposes one built-in model provider: **Step (StepFun)**. The profiles below select the Step service region and billing method; they are not separate providers.
Expand Down
27 changes: 24 additions & 3 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
decorateStepCodeSettingsManager,
describeStepMcpImportOutcome,
ensureStepGlobalConfig,
ensureStepShellPath,
flushStderrDevLog,
getLegacyStepAuthPath,
getStepAuthPath,
Expand All @@ -44,6 +45,7 @@ import {
normalizeStepSessionSelectorArgs,
parseStepUpdateCommand,
prepareMain,
prepareStepInstallation,
readFeedbackUsername,
readGlobalStepConfig,
readOrCreateStepDeviceId,
Expand Down Expand Up @@ -240,6 +242,7 @@ const stepMainOptions: MainOptions = {
// Auth-only startup uses the same InteractiveMode for its OAuth dialog,
// but must never be interrupted by a binary update prompt.
if (process.argv[2] === "login" || process.argv[2] === "logout") return true;
for (const warning of await ensureStepShellPath()) ui.notify(warning, "warning");
const outcome = await maybeUpdateStep({
version: STEPCODE_VERSION,
storageRootDir: resolveStepStorageRoot(),
Expand Down Expand Up @@ -309,7 +312,11 @@ let telemetryExitReason: "normal" | "error" = "normal";
let forceOneShotExit = false;
try {
await telemetryIdentityReady;
if (!isTopLevelLogout && !(isTopLevelLogin && topLevelAuthHelp)) {
if (
!isTopLevelLogout &&
!(isTopLevelLogin && topLevelAuthHelp) &&
!(isTopLevelMcp && process.argv[3] === "prepare")
) {
// Normalize only an old product-shaped credential file that is already
// inside the canonical StepCode path. This is the credential store, not
// settings: Step reads no configuration from a legacy layout.
Expand Down Expand Up @@ -354,7 +361,21 @@ try {
const json = args.includes("--json");
const config = readGlobalStepConfig(process.env);
const servers = config.mcp_servers ?? {};
if (subcommand === "list" || subcommand === "get") {
if (subcommand === "prepare") {
if (args.length !== 1) {
process.stderr.write("Usage: step mcp prepare\n");
process.exitCode = 1;
} else {
try {
for (const warning of await prepareStepInstallation()) process.stderr.write(`Warning: ${warning}\n`);
} catch (error) {
process.stderr.write(
`Warning: StepPage preparation failed; Step remains usable. ${error instanceof Error ? error.message : String(error)}\n`,
);
}
}
forceOneShotExit = true;
} else if (subcommand === "list" || subcommand === "get") {
const name = subcommand === "get" ? args[1] : undefined;
const selected = name ? (servers[name] ? { [name]: servers[name] } : {}) : servers;
if (json) process.stdout.write(`${JSON.stringify(selected)}\n`);
Expand Down Expand Up @@ -492,7 +513,7 @@ try {
);
}
} else {
process.stderr.write("Usage: step mcp list|get|add|remove|login|logout\n");
process.stderr.write("Usage: step mcp list|get|add|remove|login|logout|prepare\n");
process.exitCode = 1;
}
} else if (isTopLevelLogin) {
Expand Down
25 changes: 25 additions & 0 deletions docs/step-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,28 @@ requests. Print and RPC sessions still wait for the initial tool catalog before
accepting work.
Closing or replacing a session cancels pending MCP connections and prevents their
late tools or warnings from reaching the new session.

The macOS/Linux installer and `step update` run `step mcp prepare` after
installing the new binary. This prepares the built-in StepPage executable before
the installer finishes. StepPage requires Node.js 20 or newer on PATH; Step itself
does not require a separately installed Node.js. Download or prerequisite failures
show a warning and leave Step usable.

Before connecting `steppage__steppage`, Step verifies `steppage-mcp --version`
and repairs a missing or broken managed executable. Existing preinstallation
markers do not suppress this repair. Healthy executables in `~/.local/bin` can
be used even when the current shell's PATH lacks that directory. Failed setup is
retried on the next session; `step mcp prepare` also retries without requiring
sign-in. The server name combines the plugin ID and its server name with `__`.
Automatic StepPage installation is supported on macOS/Linux, including WSL.

Preparation preserves credentials, custom server commands, global overrides,
disabled servers, and intentionally uninstalled plugins. A global declaration
that still uses the default `steppage-mcp` command without arguments shares the
built-in executable repair; its environment and other settings remain unchanged.
Global configuration takes precedence over user plugins, which take precedence
over trusted project plugins. An explicit disable prevents a lower-priority copy
from starting. Standalone interactive
startup also repairs the managed Step PATH block for users upgraded by an older
updater. The installer checks the shell profile even when the current terminal
already has Step on PATH. Set `STEP_NO_MODIFY_PATH=1` to manage PATH yourself.
8 changes: 8 additions & 0 deletions infra/release/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,14 @@ try {

Write-Progress-Step -Step 6 -Total 6 -Message 'running smoke test'
Test-Install -BinaryPath (Join-Path $InstallDir 'step.exe')
try {
& (Join-Path $InstallDir 'step.exe') mcp prepare
if ($LASTEXITCODE -ne 0) {
Write-Warning 'StepPage is not ready; Step remains usable. Run step mcp prepare after upgrading.'
}
} catch {
Write-Warning "StepPage preparation failed; Step remains usable. $($_.Exception.Message)"
}
Write-Result -ResolvedVersion $resolvedVersion
} catch {
Write-Host " error: $($_.Exception.Message)" -ForegroundColor Red
Expand Down
25 changes: 18 additions & 7 deletions infra/release/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,17 @@ strip_step_block() {
# Remove a previously written, WELL-FORMED "# stepcode ... # stepcode end"
# block (and any blank lines directly above it) so reinstalls stay idempotent.
# A lone start marker with no matching end (a hand-edited rc, an interrupted
# prior write, or an unrelated "# stepcode" comment) is emitted verbatim
# rather than truncating everything below it to EOF. Returns non-zero (leaving
# the file untouched) if the rewrite could not be produced, so the caller can
# prior write, or an unrelated "# stepcode" comment) leaves the file untouched.
# Returns non-zero for malformed markers or a failed rewrite, so the caller can
# avoid appending a duplicate block on top of one it failed to remove.
local file="$1" tmp
# Never combine a dangling/nested marker with a newly appended block: the
# next reinstall could otherwise remove user content between those markers.
awk '
$0 == "# stepcode" { if (inblock) exit 1; inblock = 1 }
$0 == "# stepcode end" { if (!inblock) exit 1; inblock = 0 }
END { if (inblock) exit 1 }
' "$file" || return 1
tmp="$(mktemp "${file}.step.XXXXXX")" || return 1
if awk '
{
Expand Down Expand Up @@ -192,14 +198,14 @@ configure_shell_path() {
# drops the binary into a directory nothing sources, so a fresh shell reports
# "command not found: step". Opt out with STEP_NO_MODIFY_PATH=1 (package
# managers, CI, or callers that manage PATH themselves).
case ":${PATH}:" in *":${INSTALL_DIR}:"*) return 0 ;; esac
# An inherited PATH says nothing about what a fresh shell will load.
if [[ -n "${STEP_NO_MODIFY_PATH:-}" ]]; then path_hint; return 0; fi

# The install dir is interpolated into a shell-sourced file. Refuse to edit an
# rc when a path contains characters that could break quoting or inject a
# command, and fall back to a manual hint instead.
case "${INSTALL_DIR}:${AGENT_DIR}/bin" in
*'"'* | *'`'* | *'$'* | *$'\n'*)
*'"'* | *'`'* | *'$'* | *'\'* | *$'\n'* | *$'\r'*)
printf 'note: install dir contains characters unsafe to write into a shell profile; add it to PATH manually:\n export PATH="%s:%s/bin:$PATH"\n' "$INSTALL_DIR" "$AGENT_DIR" >&2
return 0
;;
Expand Down Expand Up @@ -238,7 +244,7 @@ configure_shell_path() {
printf 'warning: could not update the existing stepcode block in %s; left it unchanged\n' "$rc" >&2
continue
fi
if printf '\n# stepcode\ncase ":$PATH:" in\n *":%s:"*) ;;\n *) export PATH="%s:%s/bin:$PATH" ;;\nesac\n# stepcode end\n' "$INSTALL_DIR" "$INSTALL_DIR" "$AGENT_DIR" >>"$rc"; then
if printf '\n# stepcode\ncase ":$PATH:" in\n *":%s:"*) ;;\n *) export PATH="%s:$PATH" ;;\nesac\ncase ":$PATH:" in\n *":%s/bin:"*) ;;\n *) export PATH="%s/bin:$PATH" ;;\nesac\n# stepcode end\n' "$INSTALL_DIR" "$INSTALL_DIR" "$AGENT_DIR" "$AGENT_DIR" >>"$rc"; then
printf 'added %s to PATH in %s\n' "$INSTALL_DIR" "$rc"
updated=1
fi
Expand Down Expand Up @@ -316,8 +322,13 @@ main() {
for dir in native theme assets export-html docs examples; do copy_runtime_dir "$archive_root" "$dir"; done
install_managed_tools "$archive_root"
"$destination" --version >/dev/null || die 'installed Step failed its smoke test'
printf 'installed StepCode %s to %s\n' "$VERSION" "$destination"
configure_shell_path
# The new binary owns built-in preparation, including legacy markers and
# executable health checks. Older pinned binaries may not know this command.
if ! "$destination" mcp prepare; then
printf 'warning: StepPage is not ready; Step remains usable. Run step mcp prepare after upgrading.\n' >&2
fi
printf 'installed StepCode %s to %s\n' "$VERSION" "$destination"
}

main "$@"
2 changes: 2 additions & 0 deletions packages/coding-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ export {
export { runFeedbackCommand } from "./step/feedback/command.ts";
export { readFeedbackUsername } from "./step/feedback/context.ts";
export { STEP_INIT_PROMPT } from "./step/init-prompt.ts";
export { prepareStepInstallation } from "./step/installation.ts";
export { maybeUpdateStep, runStepUpdateCommand } from "./step/local-update.ts";
export {
isStepInteractiveLoginStartup,
Expand Down Expand Up @@ -673,6 +674,7 @@ export {
type StepSettingsManagerCreateOptions,
type StepSettingsPaths,
} from "./step/settings-manager.ts";
export { ensureStepShellPath } from "./step/shell-path.ts";
export {
flushStderrDevLog,
installProcessStderrDevLogCapture,
Expand Down
54 changes: 54 additions & 0 deletions packages/coding-agent/src/step/installation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { readGlobalStepConfig } from "./config-toml.ts";
import { resolveStepMcpEnvironment } from "./mcp-environment.ts";
import {
defaultStepMarketplacesDir,
defaultStepPluginsDir,
ensureBuiltinPluginsInstalled,
listStepPluginDirectories,
readStepPluginManifest,
} from "./plugins.ts";
import { ensureStepShellPath } from "./shell-path.ts";
import { ensureStepPageReady, isManagedStepPageCommand } from "./steppage-provision.ts";

/** Used by installers and the new binary after an update; failures remain warnings. */
export async function prepareStepInstallation(
input: { env?: NodeJS.ProcessEnv; executablePath?: string } = {},
): Promise<string[]> {
const env = input.env ?? process.env;
const warnings = await ensureStepShellPath(input);
const pluginsDir = defaultStepPluginsDir(env);
const preinstalled = await ensureBuiltinPluginsInstalled({
pluginsDir,
marketplacesDir: defaultStepMarketplacesDir(env),
});
warnings.push(...preinstalled.warnings);
const globalServer = readGlobalStepConfig(env).mcp_servers?.steppage__steppage;
if (globalServer?.enabled === false) return warnings;
for (const directory of await listStepPluginDirectories(pluginsDir)) {
const { manifest } = await readStepPluginManifest(directory);
const declared = manifest?.mcpServers;
if (manifest?.id !== "steppage" || !declared || typeof declared === "string") continue;
const builtinServer = declared.steppage;
if (!builtinServer || typeof builtinServer !== "object" || Array.isArray(builtinServer)) continue;
const server = globalServer ?? builtinServer;
if (!server || typeof server !== "object" || Array.isArray(server)) continue;
const declaration = server as Record<string, unknown>;
if (
declaration.enabled === false ||
!isManagedStepPageCommand(declaration, manifest.provision) ||
!isManagedStepPageCommand(builtinServer as Record<string, unknown>, manifest.provision)
)
continue;
const declaredEnv =
declaration.env && typeof declaration.env === "object" && !Array.isArray(declaration.env)
? Object.fromEntries(
Object.entries(declaration.env).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
)
: undefined;
const result = await ensureStepPageReady({ env: { ...env, ...resolveStepMcpEnvironment(declaredEnv, { env }) } });
if (result.error) warnings.push(result.error);
}
return warnings;
}
14 changes: 14 additions & 0 deletions packages/coding-agent/src/step/local-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ export async function runStepUpdateCommand(input: StepUpdateCommandInput = {}):
const smoke = await verifyBinary(stagedBinary, targetVersion);
if (!smoke.ok) throw new Error(smoke.message);
await replaceInstallation(installDir, stagedBinary, archiveRoot, binaryName, tempRoot);
// Run the installed version's preparation code, never the old updater's
// manifest assumptions. Optional resources must not roll back a good Step.
try {
const child = spawn(path.join(installDir, binaryName), ["mcp", "prepare"], { env, stdio: "inherit" });
const code = await waitForChildProcess(child);
if (code !== 0)
process.stderr.write(
"Warning: StepPage is not ready; Step remains usable. Run step mcp prepare to retry.\n",
);
} catch (error) {
process.stderr.write(
`Warning: StepPage preparation failed; Step remains usable. ${error instanceof Error ? error.message : String(error)}\n`,
);
}
process.stdout.write(`Updated Step from ${currentVersion} to ${targetVersion}.\n`);
return 0;
} catch (error) {
Expand Down
Loading
Loading