santree 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,328 @@
1
+ import * as fs from "fs";
2
+ import { run } from "../exec.js";
3
+ import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../git.js";
4
+ import { isRepoTrackerConfigured } from "../trackers/index.js";
5
+ import { getMultiplexer } from "../multiplexer/index.js";
6
+ import { resolveClaudeBinary } from "../ai.js";
7
+ import { CLAUDE_CODE_PACKAGE, detectPackageManager, getInstallCommandFor } from "../version.js";
8
+ import { installHooks as installSessionSignalHooks } from "../session-signal.js";
9
+ import { isStatuslineConfigured, configureStatusline, isRemoteControlEnabled, enableRemoteControl, isSessionSignalConfigured, } from "../claude-config.js";
10
+ import { resolveShellConfig, upsertManagedLine, isEnvVarSet, envKey, } from "./shell-config.js";
11
+ import { detectDiffPagers, detectEditors, getInstaller, which } from "./tools.js";
12
+ import { missingIgnoreEntries, addIgnoreEntries, SANTREE_IGNORE_ENTRIES, } from "./gitignore.js";
13
+ import { spawnTTY, santreeSelfArgv } from "./apply.js";
14
+ export function buildContext(dryRun) {
15
+ return {
16
+ repoRoot: findMainRepoRoot(),
17
+ shell: resolveShellConfig(),
18
+ dryRun,
19
+ };
20
+ }
21
+ /**
22
+ * Build the full step catalog with `detect` + `options` resolved against the
23
+ * current machine. The wizard filters to `actionable` steps for the checklist.
24
+ */
25
+ export function buildSteps(ctx) {
26
+ const steps = [];
27
+ const { shell, repoRoot, dryRun } = ctx;
28
+ const installer = getInstaller();
29
+ // ── Editor (SANTREE_EDITOR) ─────────────────────────────────────────────────
30
+ {
31
+ const editors = detectEditors();
32
+ const set = isEnvVarSet("SANTREE_EDITOR", shell);
33
+ const detect = set ? "ok" : editors.length > 0 ? "actionable" : "unavailable";
34
+ steps.push({
35
+ id: "editor",
36
+ title: "Default editor (SANTREE_EDITOR)",
37
+ detail: "Editor opened by the dashboard's [e] action",
38
+ scope: "global",
39
+ kind: "file",
40
+ recommended: true,
41
+ detect,
42
+ options: editors.map((e) => ({ value: e.command, label: e.command })),
43
+ optionPrompt: "Pick your editor:",
44
+ apply: (choice) => {
45
+ const editor = choice || editors[0]?.command || "vim";
46
+ if (dryRun)
47
+ return { ok: true, message: `Would set SANTREE_EDITOR=${editor}` };
48
+ upsertManagedLine(shell.rcPath, envKey("SANTREE_EDITOR"), `export SANTREE_EDITOR="${editor}"`);
49
+ return { ok: true, message: `Set SANTREE_EDITOR=${editor} in ${shell.rcPath}` };
50
+ },
51
+ });
52
+ }
53
+ // ── Diff tool (SANTREE_DIFF_TOOL) ───────────────────────────────────────────
54
+ {
55
+ const pagers = detectDiffPagers();
56
+ const hasDelta = pagers.some((p) => p.command === "delta");
57
+ const options = pagers.map((p) => ({ value: p.command, label: p.command }));
58
+ if (installer && !hasDelta) {
59
+ options.push({
60
+ value: "install:delta",
61
+ label: `git-delta — install via ${installer.name}`,
62
+ });
63
+ }
64
+ const set = isEnvVarSet("SANTREE_DIFF_TOOL", shell);
65
+ const detect = set ? "ok" : options.length > 0 ? "actionable" : "unavailable";
66
+ steps.push({
67
+ id: "diff-tool",
68
+ title: "Diff tool (SANTREE_DIFF_TOOL)",
69
+ detail: "Pretty diffs in `worktree diff` and the dashboard [v] overlay",
70
+ scope: "global",
71
+ kind: installer && !hasDelta ? "spawn" : "file",
72
+ recommended: true,
73
+ detect,
74
+ options,
75
+ optionPrompt: "Pick a diff pager:",
76
+ apply: (choice) => {
77
+ const pick = choice || options[0]?.value;
78
+ if (!pick)
79
+ return { ok: false, message: "No diff pager available" };
80
+ let tool = pick;
81
+ if (pick.startsWith("install:")) {
82
+ tool = pick.slice("install:".length);
83
+ if (dryRun) {
84
+ return {
85
+ ok: true,
86
+ message: `Would ${installer.installArgv("git-delta").join(" ")} then set SANTREE_DIFF_TOOL=${tool}`,
87
+ };
88
+ }
89
+ const argv = installer.installArgv("git-delta");
90
+ const code = spawnTTY(argv[0], argv.slice(1));
91
+ if (code !== 0)
92
+ return { ok: false, message: "git-delta install failed — SANTREE_DIFF_TOOL not set" };
93
+ }
94
+ if (dryRun)
95
+ return { ok: true, message: `Would set SANTREE_DIFF_TOOL=${tool}` };
96
+ upsertManagedLine(shell.rcPath, envKey("SANTREE_DIFF_TOOL"), `export SANTREE_DIFF_TOOL="${tool}"`);
97
+ return { ok: true, message: `Set SANTREE_DIFF_TOOL=${tool} in ${shell.rcPath}` };
98
+ },
99
+ });
100
+ }
101
+ // ── Statusline ──────────────────────────────────────────────────────────────
102
+ steps.push({
103
+ id: "statusline",
104
+ title: "Claude Code statusline",
105
+ detail: "Worktree-aware statusline (branch, context usage, session state)",
106
+ scope: "global",
107
+ kind: "file",
108
+ recommended: true,
109
+ detect: isStatuslineConfigured() ? "ok" : "actionable",
110
+ apply: () => {
111
+ if (dryRun)
112
+ return { ok: true, message: "Would configure statusLine in ~/.claude/settings.json" };
113
+ const p = configureStatusline();
114
+ return { ok: true, message: `Configured statusline in ${p}` };
115
+ },
116
+ });
117
+ // ── Session-signal hooks ────────────────────────────────────────────────────
118
+ steps.push({
119
+ id: "session-signal",
120
+ title: "Session-state signal hooks",
121
+ detail: "Surface Claude session state (waiting/active/idle) in the dashboard",
122
+ scope: "global",
123
+ kind: "file",
124
+ recommended: true,
125
+ detect: isSessionSignalConfigured() ? "ok" : "actionable",
126
+ apply: () => {
127
+ if (dryRun)
128
+ return {
129
+ ok: true,
130
+ message: "Would install session-signal hooks in ~/.claude/settings.json",
131
+ };
132
+ const p = installSessionSignalHooks();
133
+ return { ok: true, message: `Installed session-signal hooks in ${p}` };
134
+ },
135
+ });
136
+ // ── Remote control ──────────────────────────────────────────────────────────
137
+ steps.push({
138
+ id: "remote-control",
139
+ title: "Claude Code remote control",
140
+ detail: "Enable remote control at startup (drive sessions from the dashboard)",
141
+ scope: "global",
142
+ kind: "file",
143
+ recommended: false,
144
+ detect: isRemoteControlEnabled() ? "ok" : "actionable",
145
+ apply: () => {
146
+ if (dryRun)
147
+ return { ok: true, message: "Would set remoteControlAtStartup in ~/.claude.json" };
148
+ const p = enableRemoteControl();
149
+ return { ok: true, message: `Enabled remote control in ${p}` };
150
+ },
151
+ });
152
+ // ── GitHub CLI install + auth ───────────────────────────────────────────────
153
+ {
154
+ const ghInstalled = !!which("gh");
155
+ // `gh auth status` prints to stderr — redirect so it doesn't leak into the wizard.
156
+ const ghAuthed = ghInstalled && run("gh auth status >/dev/null 2>&1") !== null;
157
+ const detect = ghAuthed
158
+ ? "ok"
159
+ : ghInstalled || installer
160
+ ? "actionable"
161
+ : "unavailable";
162
+ steps.push({
163
+ id: "gh",
164
+ title: "GitHub CLI (gh) auth",
165
+ detail: "Required for PR create / review / checks",
166
+ scope: "global",
167
+ kind: "spawn",
168
+ recommended: true,
169
+ detect,
170
+ apply: () => {
171
+ if (dryRun) {
172
+ const parts = !ghInstalled && installer ? `${installer.installArgv("gh").join(" ")} then ` : "";
173
+ return { ok: true, message: `Would ${parts}gh auth login` };
174
+ }
175
+ if (!ghInstalled) {
176
+ if (!installer)
177
+ return { ok: false, message: "Install gh manually: see https://cli.github.com" };
178
+ const argv = installer.installArgv("gh");
179
+ const code = spawnTTY(argv[0], argv.slice(1));
180
+ if (code !== 0)
181
+ return { ok: false, message: "gh install failed" };
182
+ }
183
+ const code = spawnTTY("gh", ["auth", "login"]);
184
+ return code === 0
185
+ ? { ok: true, message: "gh authenticated" }
186
+ : { ok: false, message: "gh auth login did not complete" };
187
+ },
188
+ });
189
+ }
190
+ // ── Claude Code CLI ─────────────────────────────────────────────────────────
191
+ {
192
+ const installed = !!resolveClaudeBinary();
193
+ steps.push({
194
+ id: "claude",
195
+ title: "Claude Code CLI",
196
+ detail: "Powers `worktree work`, `pr fix`, `pr review`",
197
+ scope: "global",
198
+ kind: "spawn",
199
+ recommended: true,
200
+ detect: installed ? "ok" : "actionable",
201
+ apply: () => {
202
+ const cmd = getInstallCommandFor(detectPackageManager(), CLAUDE_CODE_PACKAGE);
203
+ if (dryRun)
204
+ return { ok: true, message: `Would run: ${cmd.display}` };
205
+ const code = spawnTTY(cmd.cmd, cmd.args);
206
+ return code === 0
207
+ ? { ok: true, message: "Claude Code CLI installed" }
208
+ : { ok: false, message: `Install failed — run manually: ${cmd.display}` };
209
+ },
210
+ });
211
+ }
212
+ // ── tmux (multiplexer) ──────────────────────────────────────────────────────
213
+ {
214
+ const active = getMultiplexer().kind !== "none";
215
+ const detect = active ? "ok" : installer ? "actionable" : "unavailable";
216
+ steps.push({
217
+ id: "tmux",
218
+ title: "tmux (terminal multiplexer)",
219
+ detail: "Enables new-window flows: work / fix / review / investigate",
220
+ scope: "global",
221
+ kind: "spawn",
222
+ recommended: false,
223
+ detect,
224
+ apply: () => {
225
+ if (!installer)
226
+ return { ok: false, message: "Install tmux manually: brew install tmux" };
227
+ if (dryRun)
228
+ return { ok: true, message: `Would ${installer.installArgv("tmux").join(" ")}` };
229
+ const argv = installer.installArgv("tmux");
230
+ const code = spawnTTY(argv[0], argv.slice(1));
231
+ return code === 0
232
+ ? { ok: true, message: "tmux installed (start a tmux session to use new-window flows)" }
233
+ : { ok: false, message: "tmux install failed" };
234
+ },
235
+ });
236
+ }
237
+ // ── Repo-scoped steps ───────────────────────────────────────────────────────
238
+ if (repoRoot) {
239
+ const santreeDir = getSantreeDir(repoRoot);
240
+ const initSh = getInitScriptPath(repoRoot);
241
+ // .santree scaffold
242
+ {
243
+ const folderOk = fs.existsSync(santreeDir);
244
+ const initOk = fs.existsSync(initSh);
245
+ let execOk = false;
246
+ if (initOk) {
247
+ try {
248
+ fs.accessSync(initSh, fs.constants.X_OK);
249
+ execOk = true;
250
+ }
251
+ catch { }
252
+ }
253
+ steps.push({
254
+ id: "scaffold",
255
+ title: ".santree scaffold",
256
+ detail: "Create .santree/ and an executable init.sh (runs on worktree create)",
257
+ scope: "repo",
258
+ kind: "file",
259
+ recommended: true,
260
+ detect: folderOk && initOk && execOk ? "ok" : "actionable",
261
+ apply: () => {
262
+ if (dryRun)
263
+ return { ok: true, message: `Would create ${santreeDir} and an executable init.sh` };
264
+ fs.mkdirSync(santreeDir, { recursive: true });
265
+ if (!fs.existsSync(initSh)) {
266
+ fs.writeFileSync(initSh, "#!/usr/bin/env bash\n# Runs after `santree worktree create`. Add setup steps here.\n");
267
+ }
268
+ fs.chmodSync(initSh, 0o755);
269
+ return { ok: true, message: `Scaffolded ${santreeDir}` };
270
+ },
271
+ });
272
+ }
273
+ // Ignore santree files
274
+ {
275
+ const missing = missingIgnoreEntries(repoRoot);
276
+ steps.push({
277
+ id: "gitignore",
278
+ title: "Ignore santree's machine files",
279
+ detail: `Ignore ${SANTREE_IGNORE_ENTRIES.join(", ")} (keeps .santree/issues/ tracked)`,
280
+ scope: "repo",
281
+ kind: "file",
282
+ recommended: true,
283
+ detect: missing.length === 0 ? "ok" : "actionable",
284
+ options: [
285
+ { value: "gitignore", label: ".gitignore — shared with your team" },
286
+ { value: "exclude", label: ".git/info/exclude — local to your clone" },
287
+ ],
288
+ optionPrompt: "Where should the ignore rules go?",
289
+ apply: (choice) => {
290
+ const target = (choice === "exclude" ? "exclude" : "gitignore");
291
+ const where = target === "exclude" ? ".git/info/exclude" : ".gitignore";
292
+ if (dryRun)
293
+ return {
294
+ ok: true,
295
+ message: `Would add ${missing.length} entr${missing.length === 1 ? "y" : "ies"} to ${where}`,
296
+ };
297
+ const added = addIgnoreEntries(repoRoot, target);
298
+ return {
299
+ ok: true,
300
+ message: `Added ${added.length} entr${added.length === 1 ? "y" : "ies"} to ${where}`,
301
+ };
302
+ },
303
+ });
304
+ }
305
+ // Issue tracker
306
+ {
307
+ steps.push({
308
+ id: "tracker",
309
+ title: "Issue tracker",
310
+ detail: "Pick + authenticate Linear / GitHub / Local for this repo",
311
+ scope: "repo",
312
+ kind: "spawn",
313
+ recommended: true,
314
+ detect: isRepoTrackerConfigured(repoRoot) ? "ok" : "actionable",
315
+ apply: () => {
316
+ if (dryRun)
317
+ return { ok: true, message: "Would run: santree issue setup" };
318
+ const { cmd, args } = santreeSelfArgv(["issue", "setup"]);
319
+ const code = spawnTTY(cmd, args, { cwd: repoRoot });
320
+ return code === 0
321
+ ? { ok: true, message: "Issue tracker configured" }
322
+ : { ok: false, message: "Tracker setup did not complete" };
323
+ },
324
+ });
325
+ }
326
+ }
327
+ return steps;
328
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tool detection + installation for `santree setup`.
3
+ *
4
+ * macOS (Homebrew) is the only auto-install path today. The `PlatformInstaller`
5
+ * seam keeps the door open for apt/dnf/etc. without touching call sites — add a
6
+ * resolver branch in `getInstaller()` and the steps keep working.
7
+ */
8
+ export declare function isMacOS(): boolean;
9
+ /** Resolve a command on PATH, or null if absent. */
10
+ export declare function which(cmd: string): string | null;
11
+ export interface DetectedTool {
12
+ command: string;
13
+ path: string;
14
+ }
15
+ export declare function detectDiffPagers(): DetectedTool[];
16
+ export declare function detectEditors(): DetectedTool[];
17
+ export interface PlatformInstaller {
18
+ /** Human label, e.g. "Homebrew". */
19
+ name: string;
20
+ /** argv for installing `pkg` (cmd + args), suitable for a TTY spawn. */
21
+ installArgv(pkg: string): string[];
22
+ }
23
+ export declare function getInstaller(): PlatformInstaller | null;
@@ -0,0 +1,47 @@
1
+ import * as os from "os";
2
+ import { run } from "../exec.js";
3
+ /**
4
+ * Tool detection + installation for `santree setup`.
5
+ *
6
+ * macOS (Homebrew) is the only auto-install path today. The `PlatformInstaller`
7
+ * seam keeps the door open for apt/dnf/etc. without touching call sites — add a
8
+ * resolver branch in `getInstaller()` and the steps keep working.
9
+ */
10
+ export function isMacOS() {
11
+ return os.platform() === "darwin";
12
+ }
13
+ /** Resolve a command on PATH, or null if absent. */
14
+ export function which(cmd) {
15
+ return run(`which ${cmd}`);
16
+ }
17
+ /** Known unified-diff pagers, best first. The first one on PATH wins. */
18
+ const DIFF_PAGERS = ["delta", "diff-so-fancy", "diff-highlight", "ydiff", "riff"];
19
+ export function detectDiffPagers() {
20
+ const found = [];
21
+ for (const cmd of DIFF_PAGERS) {
22
+ const p = which(cmd);
23
+ if (p)
24
+ found.push({ command: cmd, path: p });
25
+ }
26
+ return found;
27
+ }
28
+ /** Editors we know how to launch, in rough order of popularity. */
29
+ const KNOWN_EDITORS = ["code", "cursor", "zed", "subl", "windsurf", "nvim", "vim", "nano", "emacs"];
30
+ export function detectEditors() {
31
+ const found = [];
32
+ for (const cmd of KNOWN_EDITORS) {
33
+ const p = which(cmd);
34
+ if (p)
35
+ found.push({ command: cmd, path: p });
36
+ }
37
+ return found;
38
+ }
39
+ export function getInstaller() {
40
+ if (isMacOS() && which("brew")) {
41
+ return {
42
+ name: "Homebrew",
43
+ installArgv: (pkg) => ["brew", "install", pkg],
44
+ };
45
+ }
46
+ return null;
47
+ }
@@ -1,9 +1,11 @@
1
1
  interface Props {
2
2
  text?: string;
3
+ /** Optional version line (e.g. santree's running version), shown under `text`. */
4
+ version?: string;
3
5
  }
4
6
  /** Animated 3D squirrel for loading states. SDF ray-marched at module
5
7
  * load with surface-normal lighting, soft shadows, AO, and a rainbow
6
8
  * Y-gradient. Body spin + tail wag + breathing/sniff/ear-flick layers
7
9
  * combine over independent phases. */
8
- export default function SquirrelLoader({ text }: Props): import("react/jsx-runtime").JSX.Element;
10
+ export default function SquirrelLoader({ text, version }: Props): import("react/jsx-runtime").JSX.Element;
9
11
  export {};
@@ -292,7 +292,7 @@ function colorForCell(y, lit) {
292
292
  * load with surface-normal lighting, soft shadows, AO, and a rainbow
293
293
  * Y-gradient. Body spin + tail wag + breathing/sniff/ear-flick layers
294
294
  * combine over independent phases. */
295
- export default function SquirrelLoader({ text }) {
295
+ export default function SquirrelLoader({ text, version }) {
296
296
  const [frame, setFrame] = useState(0);
297
297
  useEffect(() => {
298
298
  const id = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), FRAME_MS);
@@ -318,5 +318,5 @@ export default function SquirrelLoader({ text }) {
318
318
  return spans;
319
319
  });
320
320
  }, [frame]);
321
- return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [rows.map((spans, i) => (_jsx(Box, { children: _jsx(Text, { children: spans.map((s, j) => (_jsx(Text, { color: s.color, children: s.text }, j))) }) }, i))), text && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: text }) }))] }));
321
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [rows.map((spans, i) => (_jsx(Box, { children: _jsx(Text, { children: spans.map((s, j) => (_jsx(Text, { color: s.color, children: s.text }, j))) }) }, i))), text && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: text }) })), version && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["v", version] }) }))] }));
322
322
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",
@@ -40,7 +40,6 @@
40
40
  },
41
41
  "files": [
42
42
  "dist",
43
- "shell",
44
43
  "prompts"
45
44
  ],
46
45
  "dependencies": {
@@ -1,11 +0,0 @@
1
- import { z } from "zod/v4";
2
- export declare const description = "Output shell integration script";
3
- export declare const args: z.ZodTuple<[z.ZodDefault<z.ZodEnum<{
4
- zsh: "zsh";
5
- bash: "bash";
6
- }>>], null>;
7
- type Props = {
8
- args: z.infer<typeof args>;
9
- };
10
- export default function ShellInit({ args }: Props): null;
11
- export {};
@@ -1,122 +0,0 @@
1
- import { useEffect, useState, useRef } from "react";
2
- import { argument } from "pastel";
3
- import { z } from "zod/v4";
4
- import { readdirSync, statSync, existsSync } from "fs";
5
- import { fileURLToPath } from "url";
6
- import { dirname, join } from "path";
7
- import nunjucks from "nunjucks";
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
10
- export const description = "Output shell integration script";
11
- export const args = z.tuple([
12
- z
13
- .enum(["zsh", "bash"])
14
- .default("zsh")
15
- .describe(argument({ name: "shell", description: "Shell type (zsh or bash)" })),
16
- ]);
17
- const ARG_COMPLETIONS = {};
18
- function extractOptions(mod) {
19
- const options = [];
20
- if (mod.options) {
21
- const shape = mod.options.shape ?? {};
22
- for (const [key, value] of Object.entries(shape)) {
23
- const schema = value;
24
- const desc = schema.description ?? schema._zod?.def?.description ?? "";
25
- options.push({
26
- name: key,
27
- description: desc,
28
- completion: key === "base" ? "all_branches" : null,
29
- });
30
- }
31
- }
32
- return options;
33
- }
34
- /**
35
- * Dynamically loads command modules and extracts their metadata.
36
- * Handles both top-level files and directory-based command groups.
37
- */
38
- async function getCommands() {
39
- const commandsDir = join(__dirname, "..");
40
- const entries = readdirSync(commandsDir);
41
- const commands = [];
42
- for (const entry of entries) {
43
- const entryPath = join(commandsDir, entry);
44
- const stat = statSync(entryPath);
45
- if (stat.isDirectory()) {
46
- // Directory-based command group — complete arg to subcommand names
47
- const subFiles = readdirSync(entryPath).filter((f) => f.endsWith(".js") && f !== "index.js");
48
- const subNames = subFiles.map((f) => f.replace(".js", ""));
49
- if (subNames.length === 0)
50
- continue;
51
- let description = "";
52
- const indexPath = join(entryPath, "index.js");
53
- if (existsSync(indexPath)) {
54
- try {
55
- const indexMod = await import(indexPath);
56
- description = indexMod.description ?? "";
57
- }
58
- catch {
59
- // no description
60
- }
61
- }
62
- commands.push({
63
- name: entry,
64
- funcName: entry.replace(/-/g, "_"),
65
- description,
66
- hasArgs: true,
67
- argCompletion: "static",
68
- argCompletionValues: subNames.join(" "),
69
- options: [],
70
- });
71
- continue;
72
- }
73
- if (!entry.endsWith(".js"))
74
- continue;
75
- const name = entry.replace(".js", "");
76
- try {
77
- const mod = await import(join(commandsDir, entry));
78
- if (!mod.description)
79
- continue;
80
- commands.push({
81
- name,
82
- funcName: name.replace(/-/g, "_"),
83
- description: mod.description,
84
- hasArgs: !!mod.args,
85
- argCompletion: ARG_COMPLETIONS[name] || null,
86
- argCompletionValues: "",
87
- options: extractOptions(mod),
88
- });
89
- }
90
- catch {
91
- // Skip files that can't be imported
92
- }
93
- }
94
- return commands.sort((a, b) => a.name.localeCompare(b.name));
95
- }
96
- // Configure nunjucks
97
- const templatesDir = join(__dirname, "..", "..", "..", "shell");
98
- nunjucks.configure(templatesDir, { autoescape: false });
99
- export default function ShellInit({ args }) {
100
- const [shell] = args;
101
- const [done, setDone] = useState(false);
102
- const hasOutputRef = useRef(false);
103
- useEffect(() => {
104
- async function run() {
105
- if (hasOutputRef.current)
106
- return;
107
- hasOutputRef.current = true;
108
- const commands = await getCommands();
109
- const templateFile = `init.${shell}.njk`;
110
- const output = nunjucks.render(templateFile, { commands });
111
- process.stdout.write(output);
112
- setDone(true);
113
- }
114
- run();
115
- }, [shell]);
116
- useEffect(() => {
117
- if (done) {
118
- process.exit(0);
119
- }
120
- }, [done]);
121
- return null;
122
- }