Skip to content

Commit 93469b4

Browse files
authored
feat: mise bootstrap support (#247)
1 parent 99264a4 commit 93469b4

17 files changed

Lines changed: 1681 additions & 3 deletions

File tree

.vscode-test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,27 @@ module.exports = defineConfig([
3838
timeout: 60_000,
3939
},
4040
},
41+
{
42+
label: "bootstrap",
43+
files: "src/e2e-tests/bootstrap/*.e2e.ts",
44+
workspaceFolder: path.join(fixturesPath, "bootstrap-workspace"),
45+
env: {
46+
MISE_CEILING_PATHS: fixturesPath,
47+
MISE_LOCKED: "0",
48+
MISE_TRUSTED_CONFIG_PATHS: fixturesPath,
49+
// Keep the machine's real global config (tools, bootstrap sections)
50+
// out of the tests so results match between dev machines and CI.
51+
MISE_GLOBAL_CONFIG_FILE: path.join(
52+
fixturesPath,
53+
"bootstrap-workspace",
54+
"global-config.toml",
55+
),
56+
},
57+
mocha: {
58+
require: ["tsx/cjs"],
59+
timeout: 60_000,
60+
},
61+
},
4162
{
4263
label: "monorepo",
4364
files: "src/e2e-tests/monorepo/*.e2e.ts",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
title: Bootstrap
3+
description: View mise bootstrap status in VS Code
4+
sidebar:
5+
order: 306
6+
---
7+
8+
[`mise bootstrap`](https://mise.jdx.dev/bootstrap.html) sets up a machine for
9+
the current configuration in one command: OS packages, git repos, dotfiles,
10+
shell activation, macOS defaults, launchd agents, systemd units, login shell,
11+
and tools.
12+
13+
Note that `mise bootstrap` requires mise `2026.7.16` or later.
14+
15+
## Bootstrap panel
16+
17+
The `Bootstrap` panel in the activity bar shows the status of each configured
18+
bootstrap section (packages, repos, dotfiles, shell activation, macOS defaults,
19+
launchd agents, systemd units, and login shell). It is collapsed by default and
20+
only shows sections that are configured.
21+
22+
Sections that are not yet in their desired state are expanded and marked with a
23+
warning icon, along with the number of pending entries.
24+
25+
From the panel title bar, you can:
26+
27+
- open the bootstrap status webview
28+
- run `mise bootstrap` (with a confirmation prompt, or as a dry run)
29+
- refresh the status
30+
31+
## Bootstrap status view
32+
33+
Use the `Mise: Show bootstrap status` command (or the list icon in the
34+
`Bootstrap` panel) to open a table with all bootstrap entries and their state.
35+
From there, you can filter for pending entries and run `mise bootstrap` or a
36+
dry run.

package.json

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,21 @@
622622
"view": "miseEnvsView",
623623
"contents": "Error loading environment variables. [Open logs](command:mise.openLogs)",
624624
"when": "mise.envProviderError && config.mise.enable"
625+
},
626+
{
627+
"view": "miseBootstrapView",
628+
"contents": "[Bootstrap](https://mise.jdx.dev/bootstrap.html) status will be displayed here once bootstrap sections are configured. (requires mise 2026.7.16+)\n[Show bootstrap status](command:mise.showBootstrap)",
629+
"when": "config.mise.binPath && config.mise.enable && !mise.bootstrapProviderError"
630+
},
631+
{
632+
"view": "miseBootstrapView",
633+
"contents": "Error loading bootstrap status. [Open logs](command:mise.openLogs)",
634+
"when": "mise.bootstrapProviderError && config.mise.enable"
635+
},
636+
{
637+
"view": "miseBootstrapView",
638+
"contents": "Mise extension is disabled.\n[Open settings](command:mise.openExtensionSettings)",
639+
"when": "!config.mise.enable"
625640
}
626641
],
627642
"views": {
@@ -640,6 +655,12 @@
640655
"id": "miseEnvsView",
641656
"name": "Environment Variables",
642657
"when": "!isWeb"
658+
},
659+
{
660+
"id": "miseBootstrapView",
661+
"name": "Bootstrap",
662+
"when": "!isWeb",
663+
"visibility": "collapsed"
643664
}
644665
]
645666
},
@@ -863,6 +884,28 @@
863884
"command": "mise.doctor",
864885
"title": "Mise: Run mise doctor",
865886
"enablement": "!isWeb"
887+
},
888+
{
889+
"command": "mise.runBootstrap",
890+
"title": "Mise: Run mise bootstrap",
891+
"icon": "$(play)",
892+
"enablement": "!isWeb"
893+
},
894+
{
895+
"command": "mise.runBootstrapDryRun",
896+
"title": "Mise: Run mise bootstrap (dry run)",
897+
"enablement": "!isWeb"
898+
},
899+
{
900+
"command": "mise.showBootstrap",
901+
"title": "Mise: Show bootstrap status",
902+
"icon": "$(list-ordered)",
903+
"enablement": "!isWeb"
904+
},
905+
{
906+
"command": "mise.openBootstrapEntryDefinition",
907+
"title": "Mise: Open Bootstrap Entry Definition",
908+
"enablement": "!isWeb"
866909
}
867910
],
868911
"menus": {
@@ -902,6 +945,10 @@
902945
{
903946
"command": "mise.doctor",
904947
"when": "false"
948+
},
949+
{
950+
"command": "mise.openBootstrapEntryDefinition",
951+
"when": "false"
905952
}
906953
],
907954
"view/title": [
@@ -950,9 +997,19 @@
950997
"when": "view == miseEnvsView",
951998
"group": "navigation@2"
952999
},
1000+
{
1001+
"command": "mise.showBootstrap",
1002+
"when": "view == miseBootstrapView",
1003+
"group": "navigation@1"
1004+
},
1005+
{
1006+
"command": "mise.runBootstrap",
1007+
"when": "view == miseBootstrapView",
1008+
"group": "navigation@2"
1009+
},
9531010
{
9541011
"command": "mise.refreshEntry",
955-
"when": "view == miseTasksView || view == miseToolsView || view == miseEnvsView",
1012+
"when": "view == miseTasksView || view == miseToolsView || view == miseEnvsView || view == miseBootstrapView",
9561013
"group": "navigation@4"
9571014
}
9581015
],

src/commands.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export const MISE_OPEN_TASK_DEFINITION = "mise.openTaskDefinition";
2525
export const MISE_OPEN_TOOL_DEFINITION = "mise.openToolDefinition";
2626
export const MISE_RELOAD = "mise.refreshEntry";
2727
export const MISE_REMOVE_TOOL = "mise.removeTool";
28+
export const MISE_OPEN_BOOTSTRAP_ENTRY_DEFINITION =
29+
"mise.openBootstrapEntryDefinition";
30+
export const MISE_RUN_BOOTSTRAP = "mise.runBootstrap";
31+
export const MISE_RUN_BOOTSTRAP_DRY_RUN = "mise.runBootstrapDryRun";
32+
export const MISE_SHOW_BOOTSTRAP = "mise.showBootstrap";
2833
export const MISE_RUN_TASK = "mise.runTask";
2934
export const MISE_SET_ENV_VARIABLE = "mise.setEnvVariable";
3035
export const MISE_USE_TOOL = "mise.useTool";
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import * as assert from "node:assert";
2+
import * as path from "node:path";
3+
import * as vscode from "vscode";
4+
5+
/**
6+
* Bootstrap e2e tests. Everything here is read-only and machine-agnostic:
7+
* `mise bootstrap` is never executed (only definition navigation and command
8+
* registration are exercised), the fixture entries point at resources that do
9+
* not exist anywhere, and the suite isolates the machine's global mise config
10+
* via MISE_GLOBAL_CONFIG_FILE (see .vscode-test.js), so results are identical
11+
* on macOS (dev machines) and Linux (CI).
12+
*/
13+
suite("Bootstrap Test Suite", function () {
14+
this.timeout(30_000);
15+
16+
let workspaceRoot: string;
17+
let miseTomlDocument: vscode.TextDocument;
18+
19+
const lineOf = (text: string): number => {
20+
for (let i = 0; i < miseTomlDocument.lineCount; i++) {
21+
if (miseTomlDocument.lineAt(i).text.includes(text)) {
22+
return i;
23+
}
24+
}
25+
assert.fail(`Line containing "${text}" not found in fixture`);
26+
};
27+
28+
const openDefinition = async (tablePath: string[], key: string) => {
29+
await vscode.commands.executeCommand("mise.openBootstrapEntryDefinition", {
30+
label: key,
31+
state: "missing",
32+
definition: { tablePath, key },
33+
});
34+
const editor = vscode.window.activeTextEditor;
35+
assert.ok(editor, "An editor should be opened");
36+
return editor;
37+
};
38+
39+
suiteSetup(async () => {
40+
const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
41+
assert.ok(root, "Workspace root should be available");
42+
workspaceRoot = root;
43+
44+
const extension = vscode.extensions.getExtension("hverlin.mise-vscode");
45+
assert.ok(extension, "Extension should be available");
46+
await extension.activate();
47+
48+
miseTomlDocument = await vscode.workspace.openTextDocument(
49+
path.join(workspaceRoot, "mise.toml"),
50+
);
51+
});
52+
53+
test("bootstrap commands are registered", async () => {
54+
const commands = await vscode.commands.getCommands(true);
55+
for (const command of [
56+
"mise.runBootstrap",
57+
"mise.runBootstrapDryRun",
58+
"mise.showBootstrap",
59+
"mise.openBootstrapEntryDefinition",
60+
]) {
61+
assert.ok(
62+
commands.includes(command),
63+
`Command ${command} should be registered`,
64+
);
65+
}
66+
});
67+
68+
test("navigates to a [bootstrap.repos] entry declaration", async () => {
69+
const editor = await openDefinition(
70+
["bootstrap", "repos"],
71+
"vendor/mise-e2e-repo",
72+
);
73+
74+
assert.ok(
75+
editor.document.fileName.endsWith("mise.toml"),
76+
`Should open mise.toml, got ${editor.document.fileName}`,
77+
);
78+
assert.equal(
79+
editor.selection.start.line,
80+
lineOf('"vendor/mise-e2e-repo"'),
81+
"Selection should be on the repo declaration line",
82+
);
83+
});
84+
85+
test("navigates to a [dotfiles] entry declaration", async () => {
86+
const editor = await openDefinition(
87+
["dotfiles"],
88+
"~/.mise-vscode-e2e-dotfile",
89+
);
90+
91+
assert.equal(
92+
editor.selection.start.line,
93+
lineOf('"~/.mise-vscode-e2e-dotfile"'),
94+
"Selection should be on the dotfile declaration line",
95+
);
96+
});
97+
98+
test("navigates to a [bootstrap.macos.defaults] key declaration", async () => {
99+
const editor = await openDefinition(
100+
["bootstrap", "macos", "defaults", "com.example.mise-vscode-e2e"],
101+
"ExampleKey",
102+
);
103+
104+
assert.equal(
105+
editor.selection.start.line,
106+
lineOf("ExampleKey"),
107+
"Selection should be on the defaults key line",
108+
);
109+
});
110+
111+
test("navigates to a [bootstrap.macos.finder] shorthand declaration", async () => {
112+
// mise resolves `[bootstrap.macos.finder] show_pathbar` to
113+
// `com.apple.finder ShowPathbar` in status output; navigation must find
114+
// the shorthand declaration via the alternates
115+
await vscode.commands.executeCommand("mise.openBootstrapEntryDefinition", {
116+
label: "com.apple.finder ShowPathbar",
117+
state: "unset",
118+
definition: {
119+
tablePath: ["bootstrap", "macos", "defaults", "com.apple.finder"],
120+
key: "ShowPathbar",
121+
},
122+
alternates: [
123+
{ tablePath: ["bootstrap", "macos", "finder"], key: "show_pathbar" },
124+
],
125+
});
126+
const editor = vscode.window.activeTextEditor;
127+
assert.ok(editor, "An editor should be opened");
128+
129+
assert.equal(
130+
editor.selection.start.line,
131+
lineOf("show_pathbar"),
132+
"Selection should be on the shorthand declaration line",
133+
);
134+
});
135+
136+
test("falls back to the enclosing table when the key does not exist", async () => {
137+
const editor = await openDefinition(
138+
["bootstrap", "macos", "defaults", "com.example.mise-vscode-e2e"],
139+
"KeyThatDoesNotExist",
140+
);
141+
142+
assert.equal(
143+
editor.selection.start.line,
144+
lineOf('"com.example.mise-vscode-e2e"'),
145+
"Selection should fall back to the domain table declaration",
146+
);
147+
});
148+
149+
test("does not throw for an entry declared nowhere", async () => {
150+
await vscode.commands.executeCommand("mise.openBootstrapEntryDefinition", {
151+
label: "ghost",
152+
state: "missing",
153+
definition: { tablePath: ["bootstrap", "repos"], key: "does/not-exist" },
154+
});
155+
});
156+
157+
test("show bootstrap status webview opens without error", async () => {
158+
await vscode.commands.executeCommand("mise.showBootstrap");
159+
});
160+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Used as MISE_GLOBAL_CONFIG_FILE for the bootstrap e2e suite so the machine's
2+
# real global config (tools, bootstrap sections) never leaks into the tests.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Fixture for the bootstrap e2e suite. The entries deliberately point at
2+
# resources that do not exist on any machine: the tests only read status and
3+
# navigate to declarations, they never run `mise bootstrap`.
4+
5+
[bootstrap.repos]
6+
"vendor/mise-e2e-repo" = { url = "https://example.invalid/mise-e2e-repo.git" }
7+
8+
[dotfiles]
9+
"~/.mise-vscode-e2e-dotfile" = { mode = "symlink" }
10+
11+
[bootstrap.macos.defaults]
12+
"com.example.mise-vscode-e2e" = { ExampleKey = true }
13+
14+
# friendly shorthand: resolves to `com.apple.finder ShowPathbar` in status
15+
[bootstrap.macos.finder]
16+
show_pathbar = true

src/extensionMenu.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
MISE_OPEN_MENU,
1010
MISE_RELOAD,
1111
MISE_SELECT_WORKSPACE_FOLDER,
12+
MISE_SHOW_BOOTSTRAP,
1213
MISE_SHOW_SETTINGS,
1314
MISE_SHOW_TRACKED_CONFIG,
1415
MISE_VISUALIZE_TASKS_DEPS,
@@ -46,6 +47,11 @@ export function createMenu(miseService: MiseService) {
4647
detail: "Visualize task dependencies & workspace projects",
4748
iconPath: new vscode.ThemeIcon("type-hierarchy"),
4849
},
50+
{
51+
label: "Bootstrap status",
52+
detail: "Show mise bootstrap status",
53+
iconPath: new vscode.ThemeIcon("rocket"),
54+
},
4955
(vscode.workspace.workspaceFolders?.length ?? 0) > 1
5056
? {
5157
label: "Select workspace",
@@ -99,6 +105,9 @@ export function createMenu(miseService: MiseService) {
99105
case "Task dependencies":
100106
await vscode.commands.executeCommand(MISE_VISUALIZE_TASKS_DEPS);
101107
break;
108+
case "Bootstrap status":
109+
await vscode.commands.executeCommand(MISE_SHOW_BOOTSTRAP);
110+
break;
102111
case "Reload configuration":
103112
await vscode.commands.executeCommand(MISE_RELOAD);
104113
break;

0 commit comments

Comments
 (0)