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
21 changes: 21 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,29 @@ resolve_ref() {
unset _tag
}

# The other installer (moshcoding.com/install.sh) keeps the package under
# $MOSHCODE_HOME/pkg and uses $MOSHCODE_HOME itself for the operator's own
# files: aliases.json, credentials.json, herd/, news.opml, sync.json. This one
# replaces $MOSHCODE_HOME wholesale. Run by hand on a box the other installer
# set up, that replaced the config directory — every alias, the account login
# and the herd's ledger, gone in one `rm -rf`. So look before removing: a
# directory that holds an operator's files, or a pkg/ package, is the config
# directory, and the package belongs in pkg/ beneath it.
config_dir_layout() {
[ -f "$MOSHCODE_HOME/pkg/bin/moshcode.mjs" ] && return 0
for _f in aliases.json credentials.json sync.json herd news.opml; do
[ -e "$MOSHCODE_HOME/$_f" ] && { unset _f; return 0; }
done
unset _f
return 1
}

fetch_and_unpack() {
_ref="$1"
if config_dir_layout; then
info "$MOSHCODE_HOME holds your settings — installing the package under $MOSHCODE_HOME/pkg"
MOSHCODE_HOME="$MOSHCODE_HOME/pkg"
fi
_url="https://codeload.github.com/$REPO/tar.gz/$_ref"
info "downloading moshcode@$_ref"
_tmp="$(mktemp -d 2>/dev/null || mktemp -d -t moshcode)"
Expand Down
98 changes: 98 additions & 0 deletions test/install-config-dir.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// install.sh must never replace the operator's config directory. The
// moshcoding.com installer keeps the package under ~/.moshcode/pkg and uses
// ~/.moshcode for aliases.json, credentials.json, herd/ and the rest; this
// installer replaces $MOSHCODE_HOME wholesale. Run by hand on a box the other
// one set up, it removed every alias, the account login and the herd's
// ledger in one rm -rf (seen 2026-09-13). Same harness as install-deps: the
// real script, a tarball built here, curl and npm shadowed.
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";

const INSTALL_SH = fileURLToPath(new URL("../install.sh", import.meta.url));
const scratch = [];
const cleanup = () => { for (const d of scratch.splice(0)) rmSync(d, { recursive: true, force: true }); };

function shadow(bin, name, body) {
writeFileSync(join(bin, name), `#!/bin/sh\n${body}\n`);
chmodSync(join(bin, name), 0o755);
}

/** Run the installer into a HOME whose ~/.moshcode is prepared by `seed`. */
function runInstall(seed) {
const root = mkdtempSync(join(tmpdir(), "moshcode-cfg-"));
scratch.push(root);
const home = join(root, "home");
const bin = join(root, "bin");
const cfg = join(home, ".moshcode");
mkdirSync(cfg, { recursive: true }); mkdirSync(bin);
seed(cfg);
const src = join(root, "moshcode-v9.9.9");
mkdirSync(join(src, "bin"), { recursive: true });
writeFileSync(join(src, "bin", "moshcode.mjs"), "console.log('fake');\n");
writeFileSync(join(src, "package.json"), JSON.stringify({ name: "moshcode", version: "9.9.9" }));
const tgz = join(root, "moshcode.tgz");
execFileSync("tar", ["-czf", tgz, "-C", root, "moshcode-v9.9.9"]);
shadow(bin, "curl", `case "$*" in
*api.github.com*) printf '{"tag_name":"v9.9.9"}' ;;
*codeload.github.com*) cat "${tgz}" ;;
*) exit 22 ;;
esac`);
shadow(bin, "npm", "exit 0");
const PATH = `${bin}:${dirname(process.execPath)}:${process.env.PATH}`;
let code = 0, output = "";
try {
output = execFileSync("sh", [INSTALL_SH, "install"], {
env: { PATH, HOME: home, MOSHCODE_NO_PROXY: "1", NO_COLOR: "1" },
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
});
} catch (error) { code = error.status; output = `${error.stdout ?? ""}${error.stderr ?? ""}`; }
const wrapper = existsSync(join(home, ".local/bin/moshcode")) ? readFileSync(join(home, ".local/bin/moshcode"), "utf8") : "";
return { code, output, cfg, wrapper };
}

test("a ~/.moshcode that holds the operator's files is kept, and the package goes under pkg/", () => {
try {
const r = runInstall((cfg) => {
writeFileSync(join(cfg, "aliases.json"), '{"deploy":"railway up"}');
writeFileSync(join(cfg, "credentials.json"), '{"token":"secret"}');
mkdirSync(join(cfg, "herd")); writeFileSync(join(cfg, "herd", "rules.json"), "{}");
});
assert.equal(r.code, 0, r.output);
assert.equal(readFileSync(join(r.cfg, "aliases.json"), "utf8"), '{"deploy":"railway up"}', "the aliases were replaced");
assert.ok(existsSync(join(r.cfg, "credentials.json")), "the account login was removed");
assert.ok(existsSync(join(r.cfg, "herd", "rules.json")), "the herd's files were removed");
assert.ok(existsSync(join(r.cfg, "pkg", "bin", "moshcode.mjs")), "the package did not land under pkg/");
assert.match(r.wrapper, /\.moshcode\/pkg\/bin\/moshcode\.mjs/, "the wrapper must point at the package under pkg/");
assert.match(r.output, /holds your settings/);
} finally { cleanup(); }
});

test("a ~/.moshcode that already has a pkg/ package is upgraded in place", () => {
try {
const r = runInstall((cfg) => {
mkdirSync(join(cfg, "pkg", "bin"), { recursive: true });
writeFileSync(join(cfg, "pkg", "bin", "moshcode.mjs"), "console.log('old');\n");
writeFileSync(join(cfg, "news.opml"), "<opml/>");
});
assert.equal(r.code, 0, r.output);
assert.equal(readFileSync(join(r.cfg, "pkg", "bin", "moshcode.mjs"), "utf8"), "console.log('fake');\n", "pkg/ was not replaced with the new package");
assert.ok(existsSync(join(r.cfg, "news.opml")), "a file beside pkg/ was removed");
} finally { cleanup(); }
});

test("an empty or package-only ~/.moshcode keeps today's flat layout", () => {
// The layout this installer has always produced, and what `moshcode
// upgrade` expects when it exports MOSHCODE_HOME to the dir it runs from.
try {
const r = runInstall(() => {});
assert.equal(r.code, 0, r.output);
assert.ok(existsSync(join(r.cfg, "bin", "moshcode.mjs")), "the flat layout was abandoned for a fresh install");
assert.ok(!existsSync(join(r.cfg, "pkg")), "pkg/ appeared with nothing to protect");
assert.doesNotMatch(r.output, /holds your settings/);
} finally { cleanup(); }
});
Loading