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 packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
"command": "amicode.setupVault",
"title": "Amicode: Set up a personal vault"
},
{
"command": "amicode.setupJulia",
"title": "Amicode: Set up Julia (juliaup)"
},
{
"command": "amicode.openInspector",
"title": "Amicode: Open Run Inspector"
Expand Down
50 changes: 39 additions & 11 deletions packages/extension/scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,47 @@ LAB_TOML="$HOME/.amico/lab.toml"
say() { printf '\033[1;35m[amicode]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[amicode] %s\033[0m\n' "$*" >&2; exit 1; }

# 1. Julia present?
command -v julia >/dev/null 2>&1 || die "Julia not found. Install: curl -fsSL https://install.julialang.org | sh (then re-run)"
say "julia: $(julia --version)"
# 1. Julia present? amicode manages the toolchain via juliaup. $JULIA is the
# command used below; it becomes "julia +<minor>" when juliaup-managed, so we
# pin the Manifest minor WITHOUT clobbering the user's global default.
JULIA="julia"
pinned_ver="$(sed -nE 's/^julia_version = "([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' "$EXT_ROOT/julia/Manifest.toml" | head -1)"
pinned_minor="${pinned_ver%.*}"

install_juliaup() {
say "installing juliaup (Julia version manager)..."
curl -fsSL https://install.julialang.org | sh -s -- --yes || die "juliaup install failed"
export PATH="$HOME/.juliaup/bin:$PATH" # available in THIS shell (juliaup edits the profile for future ones)
}

# 1b. Julia version vs the pinned Manifest. Manifest.toml is minor-format-specific
# (pins `julia_version`); instantiating it on a different MINOR drifts silently or
# fails confusingly, undercutting the deterministic-no-resolver-drift guarantee.
if ! command -v julia >/dev/null 2>&1 && ! command -v juliaup >/dev/null 2>&1; then
printf '\033[1;35m[amicode]\033[0m Julia not found. Install it now via juliaup? [Y/n] '
read -r ans
case "${ans:-Y}" in [Nn]*) die "Julia is required. Install: curl -fsSL https://install.julialang.org | sh (then re-run)";; esac
install_juliaup
fi

# Pin the Manifest minor via juliaup when available (add the channel if missing),
# and route this script's Julia calls through it via `julia +<minor>`.
if command -v juliaup >/dev/null 2>&1 && [ -n "$pinned_minor" ]; then
if ! julia "+${pinned_minor}" --startup-file=no --version >/dev/null 2>&1; then
say "adding Julia ${pinned_minor} via juliaup..."
juliaup add "${pinned_minor}" || die "juliaup add ${pinned_minor} failed"
fi
JULIA="julia +${pinned_minor}"
fi

command -v julia >/dev/null 2>&1 || die "Julia still not found after setup — open a new shell (so the juliaup PATH edit takes) and re-run."
say "julia: $($JULIA --version)"

# 1b. Version vs the pinned Manifest. Manifest.toml is minor-format-specific;
# instantiating on a different MINOR drifts silently. When juliaup-pinned above
# this always matches; the check still guards a manual (non-juliaup) julia.
# Minor mismatch → fatal; patch mismatch → warn (a patch-level re-resolve is fine).
pinned_ver="$(sed -nE 's/^julia_version = "([0-9]+\.[0-9]+\.[0-9]+)".*/\1/p' "$EXT_ROOT/julia/Manifest.toml" | head -1)"
running_ver="$(julia --version | sed -nE 's/.*[^0-9]([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')"
running_ver="$($JULIA --version | sed -nE 's/.*[^0-9]([0-9]+\.[0-9]+\.[0-9]+).*/\1/p')"
if [ -n "$pinned_ver" ] && [ -n "$running_ver" ]; then
if [ "${running_ver%.*}" != "${pinned_ver%.*}" ]; then
die "Julia minor mismatch: running $running_ver, Manifest pins $pinned_ver. Match the minor and re-run, e.g.: juliaup add ${pinned_ver%.*} && juliaup default ${pinned_ver%.*}"
if [ "${running_ver%.*}" != "${pinned_minor}" ]; then
die "Julia minor mismatch: running $running_ver, Manifest pins $pinned_ver. Install juliaup and re-run, or: juliaup add ${pinned_minor} && juliaup default ${pinned_minor}"
elif [ "$running_ver" != "$pinned_ver" ]; then
say "note: running Julia $running_ver differs from the Manifest's pinned patch $pinned_ver (minor matches; proceeding)."
fi
Expand All @@ -33,7 +61,7 @@ mkdir -p "$JULIA_PROJECT"
cp "$EXT_ROOT/julia/Project.toml" "$JULIA_PROJECT/Project.toml"
cp "$EXT_ROOT/julia/Manifest.toml" "$JULIA_PROJECT/Manifest.toml"
say "instantiating Piccolo project at $JULIA_PROJECT (first run precompiles - be patient)..."
julia --project="$JULIA_PROJECT" -e 'using Pkg; Pkg.instantiate()' || die "Pkg.instantiate failed (see Julia error above)"
$JULIA --project="$JULIA_PROJECT" -e 'using Pkg; Pkg.instantiate()' || die "Pkg.instantiate failed (see Julia error above)"

# 3. Install the VSIX
if command -v code >/dev/null 2>&1; then
Expand Down
76 changes: 76 additions & 0 deletions packages/extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ import { writeStopFile, savePulseTo, catalogPulsesDir, stopPlan, forceStop, runL
import { watchSolverMode, applyEntitlementForMode, readSolverModeState } from "./solver_mode";
import { amicodeOpsDir } from "./substrate/vault_store";
import { createLocalPersonalVault, sanitizeVaultName, suggestVaultName, shouldOfferVaultSetup } from "./substrate/vault_setup";
import {
pinnedJuliaMinor,
hasJuliaup,
hasChannel,
projectInstantiated,
shouldOfferJuliaSetup,
buildSetupSteps,
} from "./substrate/julia_setup";
import { resolveMountStack, personalMount, defaultVaultsRoot } from "./substrate/mount_store";
import { initDistillerTransport, triggerRunDistill, triggerSweep, type DistillerSetup } from "./substrate/distiller";
import * as os from "node:os";
Expand Down Expand Up @@ -618,6 +626,74 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
void runVaultSetup(false);
}

// Julia setup (#8): amicode manages the Julia toolchain via juliaup — install
// juliaup if absent, add the channel pinned to the Manifest's MINOR, and
// instantiate the Piccolo project. Runs in a visible terminal (the consent
// surface for the network installer). We pin the MINOR channel (latest patch,
// e.g. 1.12.6 vs the Manifest's 1.12.3) — a patch drift install.sh already
// treats as fine. On a fresh machine juliaup makes this the default, so bare
// `julia` resolves to it; routing the runtime explicitly through the channel
// for pre-existing-juliaup setups is a follow-up (--julia / {{JULIA_BINARY}}).
const runJuliaSetup = async (fromCommand: boolean): Promise<void> => {
const manifestSrc = path.resolve(ctx.extensionPath, "julia", "Manifest.toml");
const projectSrc = path.resolve(ctx.extensionPath, "julia", "Project.toml");
const minor = pinnedJuliaMinor(manifestSrc);
if (!minor) {
if (fromCommand)
void vscode.window.showErrorMessage("Amicode: could not read the pinned Julia version (julia/Manifest.toml).");
return;
}
const project = resolveJuliaProject(vscode.workspace.getConfiguration("amicode").get<string>("juliaProject", ""));
const juliaupPresent = hasJuliaup();
const channelPresent = juliaupPresent && hasChannel(minor);
const ready = juliaupPresent && channelPresent && projectInstantiated(project);
if (ready) {
if (fromCommand) void vscode.window.showInformationMessage(`Amicode: Julia ${minor} is already set up.`);
return;
}
if (!fromCommand && ctx.globalState.get<boolean>("amicode.juliaSetup.dismissed") === true) return;
if (!fromCommand) {
const choice = await vscode.window.showInformationMessage(
`Amicode uses Julia ${minor} (via juliaup) to run solves. Set it up now? The first run installs and precompiles the Piccolo project (a few minutes).`,
"Set up Julia",
"Not now",
"Don't ask again",
);
if (choice === "Don't ask again") {
await ctx.globalState.update("amicode.juliaSetup.dismissed", true);
return;
}
if (choice !== "Set up Julia") return;
}
const steps = buildSetupSteps({ minor, juliaupPresent, channelPresent, project, projectSrc, manifestSrc });
// Run in a visible terminal: the user watches the network installer + the
// precompile, and cancels by closing it. We deliberately DON'T pipe to a
// hidden task — transparency is the consent.
const term = vscode.window.createTerminal({ name: "Amicode: Julia setup" });
term.show();
term.sendText(steps.map((s) => `echo '[amicode] ${s.label}...' && ${s.command}`).join(" && \\\n"));
opencodeChannel.appendLine(`[julia] setup started (channel ${minor}, ${steps.length} step(s)) — see the terminal`);
void vscode.window.showInformationMessage(
`Amicode: setting up Julia ${minor} in the terminal. When it finishes, run "Amicode: Healthcheck" (or reload the window).`,
);
};
ctx.subscriptions.push(vscode.commands.registerCommand("amicode.setupJulia", () => void runJuliaSetup(true)));
// Auto-offer on first run when the toolchain isn't ready (juliaup/channel/
// project missing) and the user hasn't dismissed. Command bypasses the gate.
if (
shouldOfferJuliaSetup({
juliaupPresent: hasJuliaup(),
channelPresent: (() => {
const m = pinnedJuliaMinor(path.resolve(ctx.extensionPath, "julia", "Manifest.toml"));
return m ? hasChannel(m) : false;
})(),
projectInstantiated: projectInstantiated(resolveJuliaProject(vscode.workspace.getConfiguration("amicode").get<string>("juliaProject", ""))),
dismissed: ctx.globalState.get<boolean>("amicode.juliaSetup.dismissed") === true,
})
) {
void runJuliaSetup(false);
}

// 5. Commands
ctx.subscriptions.push(
vscode.commands.registerCommand("amicode.openChat", async () => {
Expand Down
139 changes: 139 additions & 0 deletions packages/extension/src/substrate/julia_setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Managed Julia setup (#8): amicode owns the Julia it runs, via a juliaup
// channel pinned to the Manifest's MINOR (e.g. 1.12). The `1.12` channel tracks
// the latest 1.12 patch (1.12.6 as of writing) — a patch drift from the
// Manifest's pinned 1.12.3, which is fine: install.sh's long-standing policy is
// "minor must match, patch re-resolves." We never touch the user's global
// juliaup default — solves run through the channel's own binary, resolved here
// and handed to amico-run via its existing `--julia` plumbing.
//
// Pure helpers + command builders are unit-testable; the impure shell probes
// take an injectable runner (the healthcheck pattern).
import { execFileSync } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";

/** Shape of a command to run in a visible integrated-terminal Task (the consent
* surface): the user sees exactly what executes. */
export interface JuliaSetupStep {
label: string;
/** A shell-ready command line (steps may pipe, e.g. the juliaup installer). */
command: string;
}

/** Default absolute Julia project dir (matches resolveJuliaProject's default). */
export function defaultJuliaProject(): string {
return path.join(os.homedir(), ".amico", "julia");
}

/** Parse the pinned Julia MINOR (e.g. "1.12") from a Manifest.toml's
* `julia_version = "1.12.3"`. Returns null if unreadable/unparseable. */
export function pinnedJuliaMinor(manifestPath: string): string | null {
try {
const txt = fs.readFileSync(manifestPath, "utf8");
const m = txt.match(/^julia_version\s*=\s*"(\d+)\.(\d+)\.\d+"/m);
return m ? `${m[1]}.${m[2]}` : null;
} catch {
return null;
}
}

/** Injectable command runner: returns stdout (trimmed), throws on non-zero. */
export type Runner = (cmd: string, args: string[]) => string;
const defaultRunner: Runner = (cmd, args) =>
execFileSync(cmd, args, { encoding: "utf8", timeout: 30_000, stdio: ["ignore", "pipe", "ignore"] }).trim();

/** juliaup present on PATH? */
export function hasJuliaup(run: Runner = defaultRunner): boolean {
try {
run("juliaup", ["--version"]);
return true;
} catch {
return false;
}
}

/** Is the `<minor>` channel installed + usable? Probes the channel shim rather
* than parsing `juliaup status` (whose table format is not stable across
* versions). `julia +1.12 --version` exits 0 only if the channel resolves. */
export function hasChannel(minor: string, run: Runner = defaultRunner): boolean {
try {
run("julia", [`+${minor}`, "--startup-file=no", "--version"]);
return true;
} catch {
return false;
}
}

/** Resolve the channel's concrete julia binary (absolute), to hand to amico-run
* as `--julia` so solves use amicode's pinned Julia regardless of the user's
* global default. Returns null if the channel can't be resolved. */
export function resolveChannelJulia(minor: string, run: Runner = defaultRunner): string | null {
try {
const bindir = run("julia", [`+${minor}`, "--startup-file=no", "-e", "print(Sys.BINDIR)"]);
if (!bindir) return null;
const exe = process.platform === "win32" ? "julia.exe" : "julia";
const p = path.join(bindir, exe);
return fs.existsSync(p) ? p : null;
} catch {
return null;
}
}

/** Has the pinned project been instantiated? Cheap gate check (existence of the
* copied Manifest); full "using Piccolo loads" verification stays in
* healthcheck. The setup Task re-runs instantiate idempotently regardless. */
export function projectInstantiated(project: string = defaultJuliaProject()): boolean {
return fs.existsSync(path.join(project, "Manifest.toml"));
}

/** First-run gate (mirrors shouldOfferVaultSetup): offer setup if anything in
* the chain is missing and the user hasn't dismissed. */
export function shouldOfferJuliaSetup(o: {
juliaupPresent: boolean;
channelPresent: boolean;
projectInstantiated: boolean;
dismissed: boolean;
}): boolean {
return !o.dismissed && (!o.juliaupPresent || !o.channelPresent || !o.projectInstantiated);
}

/** The juliaup installer one-liner. Unix (linux-x64 + darwin-arm64, the vsix
* targets); `--yes` makes it non-interactive so it runs unattended in the Task
* after the user's explicit consent-click. */
export function juliaupInstallCommand(): string {
return "curl -fsSL https://install.julialang.org | sh -s -- --yes";
}

/** Build the ordered, consented setup steps for the given state. Only the steps
* that are actually needed are emitted (idempotent + minimal). `project` is the
* Julia project dir; `manifestSrc`/`projectSrc` are the bundled pins to seed. */
export function buildSetupSteps(o: {
minor: string;
juliaupPresent: boolean;
channelPresent: boolean;
project: string;
projectSrc: string;
manifestSrc: string;
}): JuliaSetupStep[] {
const steps: JuliaSetupStep[] = [];
if (!o.juliaupPresent) {
steps.push({ label: "Install juliaup", command: juliaupInstallCommand() });
}
if (!o.channelPresent) {
// works whether juliaup was just installed (its shim is on PATH via the
// installer's profile edit; the Task re-sources) or already present.
steps.push({ label: `Add Julia ${o.minor}`, command: `juliaup add ${o.minor}` });
}
// Seed the pinned project, then instantiate through the channel. Quote paths
// for spaces. Instantiate is idempotent + safe to re-run.
steps.push({
label: "Instantiate Piccolo project",
command:
`mkdir -p "${o.project}" && ` +
`cp "${o.projectSrc}" "${o.project}/Project.toml" && ` +
`cp "${o.manifestSrc}" "${o.project}/Manifest.toml" && ` +
`julia +${o.minor} --project="${o.project}" -e 'using Pkg; Pkg.instantiate()'`,
});
return steps;
}
Loading
Loading