Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
vim
|
||||
@@ -0,0 +1 @@
|
||||
${machine:account-home}/.local/bin
|
||||
@@ -0,0 +1 @@
|
||||
${machine:account-home}/scripts
|
||||
@@ -0,0 +1 @@
|
||||
${machine:account-home}/scripts/bin
|
||||
@@ -0,0 +1 @@
|
||||
vim
|
||||
@@ -0,0 +1 @@
|
||||
${machine:account-home}/.config
|
||||
@@ -0,0 +1,3 @@
|
||||
# Written by the mesh (module zsh, hq ADR 0268) from this machine's setting execute: serve or withhold.
|
||||
# Read by the zsh tools bundle when the runtime starts it; change the setting, not this.
|
||||
execute=${config:execute}
|
||||
@@ -0,0 +1,9 @@
|
||||
# The mesh's block (module zsh, novox/hq ADR 0203, ADR 0204): the account's environment, read by
|
||||
# every zsh — a login, a script, and node-login-shell's execute. Replaced at every push; everything
|
||||
# outside it is yours.
|
||||
# PATH keeps each directory once, wherever it was added: the session, a nested shell or a
|
||||
# sourced file. Without it every nested shell prepended the same entries again.
|
||||
typeset -U path PATH
|
||||
if [[ -r "${machine:account-home}/.config/mesh/environment.sh" ]]; then
|
||||
source "${machine:account-home}/.config/mesh/environment.sh"
|
||||
fi
|
||||
@@ -0,0 +1,60 @@
|
||||
# The mesh's block (module zsh, novox/hq ADR 0204): the defaults every machine shares, and the code
|
||||
# other modules contribute in three slots. It is replaced at every push. Everything below it is
|
||||
# yours, kept as you wrote it, and runs after it, so your lines win.
|
||||
${shell:zsh:first}
|
||||
# History, kept across sessions and shared between open shells. Without these zsh saves none:
|
||||
# SAVEHIST defaults to 0 (novox/hq to-be 41, found on review 2026-10-04).
|
||||
HISTFILE="$HOME/.zsh_history"
|
||||
HISTSIZE=100000
|
||||
SAVEHIST=100000
|
||||
setopt EXTENDED_HISTORY SHARE_HISTORY HIST_IGNORE_DUPS HIST_IGNORE_SPACE HIST_REDUCE_BLANKS
|
||||
|
||||
# The completion system. Nothing initialised it, so only zsh's basic completion ran. Its cache
|
||||
# goes under ~/.cache rather than beside ~/.zshrc.
|
||||
[[ -d "$HOME/.cache/zsh" ]] || mkdir -p "$HOME/.cache/zsh"
|
||||
autoload -Uz compinit && compinit -d "$HOME/.cache/zsh/zcompdump"
|
||||
zstyle ':completion:*' menu select
|
||||
|
||||
# Terminal title: host, directory, git branch
|
||||
function set_terminal_title() {
|
||||
local git_branch=""
|
||||
if git rev-parse --is-inside-work-tree &>/dev/null; then
|
||||
git_branch=" ($(git branch --show-current 2>/dev/null))"
|
||||
fi
|
||||
print -Pn "\e]2;%m: %~${git_branch}\a"
|
||||
}
|
||||
precmd_functions+=(set_terminal_title)
|
||||
|
||||
# Keybindings
|
||||
bindkey "^[[H" beginning-of-line # Home
|
||||
bindkey "^[OH" beginning-of-line # Home
|
||||
bindkey "^A" beginning-of-line # Ctrl + A
|
||||
bindkey "^[[F" end-of-line # End
|
||||
bindkey "^[OF" end-of-line # End
|
||||
bindkey "^E" end-of-line # Ctrl + E
|
||||
bindkey "^[[3~" delete-char # Del
|
||||
|
||||
# Colour support of ls and friends
|
||||
if [ -x /usr/bin/dircolors ]; then
|
||||
test -r $HOME/.dircolors && eval "$(dircolors -b $HOME/.dircolors)" || eval "$(dircolors -b)"
|
||||
alias ls='ls --color=auto'
|
||||
alias dir='dir --color=auto'
|
||||
alias vdir='vdir --color=auto'
|
||||
alias grep='grep --color=auto'
|
||||
alias fgrep='fgrep --color=auto'
|
||||
alias egrep='egrep --color=auto'
|
||||
fi
|
||||
alias ll='ls -alhF'
|
||||
alias la='ls -Ah'
|
||||
alias l='ls -CFh'
|
||||
|
||||
# A throwaway container, and what takes the space under a directory
|
||||
alias drun='docker run -it --rm'
|
||||
disksize() {
|
||||
du -h --max-depth=1 "${1:-.}" | sort -h
|
||||
}
|
||||
sudo-disksize() {
|
||||
sudo du -h --max-depth=1 "${1:-.}" | sort -h
|
||||
}
|
||||
|
||||
${shell:zsh:normal}${shell:zsh:last}
|
||||
+79
-30
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"module": "zsh",
|
||||
"version": "1",
|
||||
"moved": true,
|
||||
"capabilities": [
|
||||
"package-manager"
|
||||
],
|
||||
@@ -16,34 +17,77 @@
|
||||
"tools": [
|
||||
"zsh_config"
|
||||
],
|
||||
"settings": {
|
||||
"execute": {
|
||||
"kind": "preference",
|
||||
"default": "serve",
|
||||
"why": "execute runs any command as the operator account, which can become root without a person; a machine whose layer says withhold does not offer it (hq ADR 0268): the control-node, until a call to it needs a person's approval"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"variables": {
|
||||
"EDITOR": "vim",
|
||||
"VISUAL": "vim",
|
||||
"XDG_CONFIG_HOME": "${machine:account-home}/.config"
|
||||
},
|
||||
"path": [
|
||||
{
|
||||
"entry": "${machine:account-home}/.local/bin",
|
||||
"at": "start"
|
||||
},
|
||||
{
|
||||
"entry": "${machine:account-home}/scripts",
|
||||
"at": "start"
|
||||
},
|
||||
{
|
||||
"entry": "${machine:account-home}/scripts/bin",
|
||||
"at": "start"
|
||||
"config": {
|
||||
"execute": {
|
||||
"type": "string",
|
||||
"description": "Whether this machine offers execute, which runs any command as the operator account and so can become root without a person: serve, or withhold on a machine that does not offer it (novox/hq ADR 0268), such as the control-node until a call to it needs a person's approval.",
|
||||
"enum": [
|
||||
"serve",
|
||||
"withhold"
|
||||
],
|
||||
"trusted": true,
|
||||
"default": "serve"
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"collection": "environment",
|
||||
"name": "editor",
|
||||
"attributes": {
|
||||
"kind": "variable",
|
||||
"name": "EDITOR"
|
||||
},
|
||||
"template": "files/env-editor"
|
||||
},
|
||||
{
|
||||
"collection": "environment",
|
||||
"name": "visual",
|
||||
"attributes": {
|
||||
"kind": "variable",
|
||||
"name": "VISUAL"
|
||||
},
|
||||
"template": "files/env-visual"
|
||||
},
|
||||
{
|
||||
"collection": "environment",
|
||||
"name": "xdg-config-home",
|
||||
"attributes": {
|
||||
"kind": "variable",
|
||||
"name": "XDG_CONFIG_HOME"
|
||||
},
|
||||
"template": "files/env-xdg-config-home"
|
||||
},
|
||||
{
|
||||
"collection": "environment",
|
||||
"name": "path-local-bin",
|
||||
"attributes": {
|
||||
"kind": "path",
|
||||
"at": "start",
|
||||
"order": "10"
|
||||
},
|
||||
"template": "files/env-path-local-bin"
|
||||
},
|
||||
{
|
||||
"collection": "environment",
|
||||
"name": "path-scripts",
|
||||
"attributes": {
|
||||
"kind": "path",
|
||||
"at": "start",
|
||||
"order": "20"
|
||||
},
|
||||
"template": "files/env-path-scripts"
|
||||
},
|
||||
{
|
||||
"collection": "environment",
|
||||
"name": "path-scripts-bin",
|
||||
"attributes": {
|
||||
"kind": "path",
|
||||
"at": "start",
|
||||
"order": "30"
|
||||
},
|
||||
"template": "files/env-path-scripts-bin"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "package",
|
||||
@@ -58,7 +102,7 @@
|
||||
"mode": "0644",
|
||||
"into": "block",
|
||||
"at": "start",
|
||||
"content": "# The mesh's block (module zsh, novox/hq ADR 0203, ADR 0204): the account's environment, read by\n# every zsh — a login, a script, and node-login-shell's execute. Replaced at every push; everything\n# outside it is yours.\n# PATH keeps each directory once, wherever it was added: the session, a nested shell or a\n# sourced file. Without it every nested shell prepended the same entries again.\ntypeset -U path PATH\nif [[ -r \"${machine:account-home}/.config/mesh/environment.sh\" ]]; then\n source \"${machine:account-home}/.config/mesh/environment.sh\"\nfi\n"
|
||||
"template": "files/zshenv"
|
||||
},
|
||||
{
|
||||
"id": "rc",
|
||||
@@ -68,7 +112,7 @@
|
||||
"mode": "0644",
|
||||
"into": "block",
|
||||
"at": "start",
|
||||
"content": "# The mesh's block (module zsh, novox/hq ADR 0204): the defaults every machine shares, and the code\n# other modules contribute in three slots. It is replaced at every push. Everything below it is\n# yours, kept as you wrote it, and runs after it, so your lines win.\n${shell:zsh:first}\n# History, kept across sessions and shared between open shells. Without these zsh saves none:\n# SAVEHIST defaults to 0 (novox/hq to-be 41, found on review 2026-10-04).\nHISTFILE=\"$HOME/.zsh_history\"\nHISTSIZE=100000\nSAVEHIST=100000\nsetopt EXTENDED_HISTORY SHARE_HISTORY HIST_IGNORE_DUPS HIST_IGNORE_SPACE HIST_REDUCE_BLANKS\n\n# The completion system. Nothing initialised it, so only zsh's basic completion ran. Its cache\n# goes under ~/.cache rather than beside ~/.zshrc.\n[[ -d \"$HOME/.cache/zsh\" ]] || mkdir -p \"$HOME/.cache/zsh\"\nautoload -Uz compinit && compinit -d \"$HOME/.cache/zsh/zcompdump\"\nzstyle ':completion:*' menu select\n\n# Terminal title: host, directory, git branch\nfunction set_terminal_title() {\n local git_branch=\"\"\n if git rev-parse --is-inside-work-tree &>/dev/null; then\n git_branch=\" ($(git branch --show-current 2>/dev/null))\"\n fi\n print -Pn \"\\e]2;%m: %~${git_branch}\\a\"\n}\nprecmd_functions+=(set_terminal_title)\n\n# Keybindings\nbindkey \"^[[H\" beginning-of-line # Home\nbindkey \"^[OH\" beginning-of-line # Home\nbindkey \"^A\" beginning-of-line # Ctrl + A\nbindkey \"^[[F\" end-of-line # End\nbindkey \"^[OF\" end-of-line # End\nbindkey \"^E\" end-of-line # Ctrl + E\nbindkey \"^[[3~\" delete-char # Del\n\n# Colour support of ls and friends\nif [ -x /usr/bin/dircolors ]; then\n test -r $HOME/.dircolors && eval \"$(dircolors -b $HOME/.dircolors)\" || eval \"$(dircolors -b)\"\n alias ls='ls --color=auto'\n alias dir='dir --color=auto'\n alias vdir='vdir --color=auto'\n alias grep='grep --color=auto'\n alias fgrep='fgrep --color=auto'\n alias egrep='egrep --color=auto'\nfi\nalias ll='ls -alhF'\nalias la='ls -Ah'\nalias l='ls -CFh'\n\n# A throwaway container, and what takes the space under a directory\nalias drun='docker run -it --rm'\ndisksize() {\n du -h --max-depth=1 \"${1:-.}\" | sort -h\n}\nsudo-disksize() {\n sudo du -h --max-depth=1 \"${1:-.}\" | sort -h\n}\n\n${shell:zsh:normal}${shell:zsh:last}\n"
|
||||
"template": "files/zshrc"
|
||||
},
|
||||
{
|
||||
"id": "login",
|
||||
@@ -89,7 +133,7 @@
|
||||
"path": "${dir:state}/tools.conf",
|
||||
"owner": "root",
|
||||
"mode": "0644",
|
||||
"content": "# Written by the mesh (module zsh, hq ADR 0268) from this machine's setting execute: serve or withhold.\n# Read by the zsh tools bundle when the runtime starts it; change the setting, not this.\nexecute=${setting:execute}\n"
|
||||
"template": "files/tools.conf"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
@@ -104,6 +148,11 @@
|
||||
"env": {
|
||||
"ZSH_TOOLS_CONF": "${dir:state}/tools.conf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,20 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
const m = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8"));
|
||||
const resource = (id: string) => m.resources.find((r: { id: string }) => r.id === id);
|
||||
// A file's text is its template's, in the module's files/ (novox/hq ADR 0349 §2).
|
||||
const text = (r: { id: string; template?: string }): string => {
|
||||
assert.ok(r.template, `${r.id} names no template`);
|
||||
return readFileSync(new URL(`../${r.template}`, import.meta.url), "utf8");
|
||||
};
|
||||
|
||||
test("no file carries its text inline: each names its template, and the build packs files/ into one archive", () => {
|
||||
for (const r of m.resources) {
|
||||
assert.equal(r.content, undefined, `${r.id} keeps its text inline`);
|
||||
if (r.type === "file") text(r);
|
||||
}
|
||||
const packs = m.build.artifacts.filter((a: { kind: string; from?: string }) => a.kind === "archive" && a.from === "files");
|
||||
assert.deepEqual(packs.map((a: { name: string }) => a.name), ["files"]);
|
||||
});
|
||||
|
||||
test("it claims node-login-shell serving execute, and declares no seat", () => {
|
||||
assert.equal(m.seats, undefined);
|
||||
@@ -19,7 +33,7 @@ test("the .zshrc block is at the start and holds the three slots in order, after
|
||||
assert.equal(rc.path, "${machine:account-home}/.zshrc");
|
||||
assert.equal(rc.into, "block");
|
||||
assert.equal(rc.at, "start");
|
||||
const c: string = rc.content;
|
||||
const c = text(rc);
|
||||
const first = c.indexOf("${shell:zsh:first}");
|
||||
const normal = c.indexOf("${shell:zsh:normal}");
|
||||
const last = c.indexOf("${shell:zsh:last}");
|
||||
@@ -30,7 +44,7 @@ test("the .zshrc block is at the start and holds the three slots in order, after
|
||||
});
|
||||
|
||||
test("the .zshrc block carries the shared defaults and none of the operator's own lines", () => {
|
||||
const c: string = resource("rc").content;
|
||||
const c = text(resource("rc"));
|
||||
for (const want of ["precmd_functions+=(set_terminal_title)", "alias egrep='egrep --color=auto'", "alias ll='ls -alhF'", "alias drun=", "disksize()", "sudo-disksize()"]) {
|
||||
assert.ok(c.includes(want), want);
|
||||
}
|
||||
@@ -45,17 +59,28 @@ test("the .zshenv block sources exactly the environment file node-environment fi
|
||||
assert.equal(env.path, "${machine:account-home}/.zshenv");
|
||||
assert.equal(env.into, "block");
|
||||
assert.equal(env.at, "start");
|
||||
const sourced = [...(env.content as string).matchAll(/^\s*(?:source|\.)\s+"?([^"\s]+)"?/gm)].map((x) => x[1]);
|
||||
const sourced = [...text(env).matchAll(/^\s*(?:source|\.)\s+"?([^"\s]+)"?/gm)].map((x) => x[1]);
|
||||
assert.deepEqual(sourced, ["${machine:account-home}/.config/mesh/environment.sh"]);
|
||||
});
|
||||
|
||||
test("its environment is a contribution: the editor, the configuration home, and three PATH entries at the start", () => {
|
||||
assert.deepEqual(m.environment.variables, { EDITOR: "vim", VISUAL: "vim", XDG_CONFIG_HOME: "${machine:account-home}/.config" });
|
||||
assert.deepEqual(m.environment.path, [
|
||||
// The contribution is typed items of the environment collection, each with its value in a template (ADR 0348, 0349).
|
||||
const items = m.items.filter((i: { collection: string }) => i.collection === "environment");
|
||||
const value = (i: { template: string }) => readFileSync(new URL(`../${i.template}`, import.meta.url), "utf8").trim();
|
||||
const variables = Object.fromEntries(
|
||||
items.filter((i: { attributes: { kind: string } }) => i.attributes.kind === "variable").map((i: { attributes: { name: string }; template: string }) => [i.attributes.name, value(i)]),
|
||||
);
|
||||
assert.deepEqual(variables, { EDITOR: "vim", VISUAL: "vim", XDG_CONFIG_HOME: "${machine:account-home}/.config" });
|
||||
const path = items
|
||||
.filter((i: { attributes: { kind: string } }) => i.attributes.kind === "path")
|
||||
.sort((a: { attributes: { order: string } }, b: { attributes: { order: string } }) => Number(a.attributes.order) - Number(b.attributes.order))
|
||||
.map((i: { attributes: { at: string }; template: string }) => ({ entry: value(i), at: i.attributes.at }));
|
||||
assert.deepEqual(path, [
|
||||
{ entry: "${machine:account-home}/.local/bin", at: "start" },
|
||||
{ entry: "${machine:account-home}/scripts", at: "start" },
|
||||
{ entry: "${machine:account-home}/scripts/bin", at: "start" },
|
||||
]);
|
||||
assert.equal(m.environment, undefined);
|
||||
});
|
||||
|
||||
test("it keeps the package and sets the login shell through the user shape", () => {
|
||||
@@ -70,11 +95,14 @@ test("the tools bundle registers the seat's verb under node-login-shell", () =>
|
||||
assert.doesNotMatch(src, /runuser|"login-shell"/);
|
||||
});
|
||||
|
||||
test("execute is a per-machine setting, served unless a machine withholds it (ADR 0268)", () => {
|
||||
assert.deepEqual(Object.keys(m.settings), ["execute"]);
|
||||
assert.equal(m.settings.execute.kind, "preference");
|
||||
assert.equal(m.settings.execute.default, "serve");
|
||||
assert.match(m.settings.execute.why, /withhold/);
|
||||
test("execute is a per-machine config value, served unless a machine withholds it (ADR 0268, ADR 0348 §4)", () => {
|
||||
assert.equal(m.settings, undefined);
|
||||
assert.deepEqual(Object.keys(m.config), ["execute"]);
|
||||
assert.equal(m.config.execute.type, "string");
|
||||
assert.deepEqual(m.config.execute.enum, ["serve", "withhold"]);
|
||||
assert.equal(m.config.execute.trusted, true, "it decides what runs, so it is set at the terminal or on an approval");
|
||||
assert.equal(m.config.execute.default, "serve");
|
||||
assert.match(m.config.execute.description, /withhold/);
|
||||
const dir = resource("state");
|
||||
assert.deepEqual(dir, { id: "state", type: "directory", place: ".", owner: "root", mode: "0755" });
|
||||
const conf = resource("tools-conf");
|
||||
@@ -82,7 +110,7 @@ test("execute is a per-machine setting, served unless a machine withholds it (AD
|
||||
assert.equal(conf.path, "${dir:state}/tools.conf");
|
||||
assert.equal(conf.owner, "root", "the account the runtime runs as may read it, never change it");
|
||||
assert.equal(conf.mode, "0644");
|
||||
assert.match(conf.content, /^execute=\$\{setting:execute\}$/m);
|
||||
assert.match(text(conf), /^execute=\$\{config:execute\}$/m);
|
||||
const [bundle] = m.build.artifacts;
|
||||
assert.deepEqual(bundle.env, { ZSH_TOOLS_CONF: conf.path }, "the word names the file exactly, so its change restarts the runtime");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user