</p>
<p>
- A person takes on a role by sourcing the top-level setup script and providing the target role. In a bash shell to take on the role of <RT-term>developer</RT-term> the command is: <RT-code>. setup developer</RT-code>, or <RT-code>source setup developer</RT-code>. As of this writing, the supported roles are: <strong>administrator</strong>, <strong>consumer</strong>, <strong>developer</strong>, and <strong>tester</strong>.
+ A person takes on a role by sourcing the top-level setup script and providing the target role. In a bash shell to take on the role of <RT-term>developer</RT-term> the command is, for example: <RT-code>. setup developer</RT-code>, or <RT-code>source setup developer</RT-code>. As of this writing, the supported roles are: <strong>administrator</strong>, <strong>consumer</strong>, <strong>developer</strong>, and <strong>tester</strong>.
</p>
<p>
</p>
<ul>
- <li>Sources the project-wide setup (<RT-code>shared/authored/setup</RT-code>) to establish the core environment variables (e.g., <RT-code>REPO_HOME</RT-code> and <RT-code>PROJECT</RT-code>).</li>
+ <li>Sources the project-wide setup (<RT-code>shared/authored/setup</RT-code>) to establish the core environment variables (e.g. <RT-code>REPO_HOME</RT-code> and <RT-code>PROJECT</RT-code>).</li>
<li>Configures the <RT-code>PATH</RT-code> to include shared tools, library environments, and the specific <RT-code><role>/tool</RT-code> directory.</li>
<li>Changes the working directory into the specified role's workspace.</li>
<li>Sources the <RT-code><role>/tool/setup</RT-code> script. While the earlier steps apply the standard Harmony skeleton setup, this final step applies the role setup that is customized for this specific project.</li>
<li><RT-code>shared/document/</RT-code> : Documentation on installing and configuring shared tools.</li>
</ul>
- <h1>Key concepts</h1>
- <h2>Authored vs. Made</h2>
+ <h1>Authored, made, scratchpad, third_party, inherited, customizations</h1>
<p>
- Harmony divides the world into two categories:
+ Files found in a directory named <RT-code>authored</RT-code> were written by project team members. They did not come with the Harmony skeleton, nor with the installation of other software. The project tools only read from <RT-code>authored</RT-code> directories. Typically these files constitute the intellectual property of a project.
+ </p>
+ <p>
+ Files found in a directory named <RT-code>made</RT-code> were generated by tools in the project. Authors should not edit these files, nor create new files in such a directory, as the directory content is under control of the tools. If a made file requires editing, it is best to find the source of the file, and edit at that point.
+ </p>
+ <p>
+ Files found in a directory named <RT-code>scratchpad</RT-code> are not tracked. Hence, a <RT-code>git clone</RT-code> will always return empty <RT-code>scratchpad</RT-code> directories. (Currently this is accomplished by putting a <RT-code>.gitignore</RT-code> file in every <RT-code>scratchpad</RT-code> directory.) It is common for tools to place intermediate files on a scratchpad. It is also common for files to be staged on a scratchpad. Tools play nice and use subdirectories on the pad, so a person can use a scratchpad as a temporary directory for files. There is a tool that comes with the Harmony skeleton with the same name, <RT-code>scratchpad</RT-code>, that is used to maintain scratchpads. Note that one of its commands is <RT-code>clear</RT-code> which deletes everything on a scratchpad.
+ </p>
+ <p>
+ Third party software is installed under <RT-code>shared/third_party</RT-code>.
+ </p>
+ <p>
+ Other files are said to be <RT-term>inherited</RT-term>, or to be <RT-term>customizations</RT-term>.
+ </p>
+ <p>
+ If an authored file must be added to a project in a location other than an <RT-code>authored</RT-code> directory, it is best to create and edit the file in an <RT-code>authored</RT-code> directory, then to create a symbolic link that points back to it from that other location.
</p>
- <ul>
- <li><strong>Authored:</strong> Human-written (or AI-written) source files, docs, and design notes.</li>
- <li><strong>Made:</strong> Tool-produced binaries, generated sources, and intermediates.</li>
- </ul>
<p>
- This separation protects authored material from accidental overwrite and makes build artifacts fully disposable.
+ This approach makes it clear where to find team work product, what is generated by the tools in the project, and what was 'installed' with the project.
</p>
<h1>Top-Level repository layout</h1>
<li><RT-code>shared/</RT-code> : Shared ecosystem tools and global environments.</li>
</ul>
- <h1>The administrator work area</h1>
+ <h2>The administrator work area</h2>
<p>
This directory holds the tools and documentation used to manage the project as a whole. It includes the HTML documentation for the project ontology and workflow, as well as project-local tools utilized by the administrator to maintain the Harmony skeleton.
</p>
- <h1>The developer work area</h1>
+ <h2>The developer work area</h2>
<p>
This directory is entered by first going to the top-level directory of the project, then sourcing <RT-code>. setup developer</RT-code>.
</p>
<li><RT-code>tool/</RT-code> : Developer-specific tools (like the release and make scripts).</li>
</ul>
- <h1>The tester work area</h1>
+ <h2>The tester work area</h2>
<p>
This directory is dedicated to formal testing, including regression suites. While a developer can run and keep informal spot tests in their <RT-code>experiment/</RT-code> directory, any experiment promoted to a formal test is moved here. This enforces the boundary between writing code and validating it.
</p>
- <h1>The shared tree</h1>
+ <h2>The shared tree</h2>
<p>
This directory contains ecosystem tools and global environments available to all roles. This includes shared authored tools, as well as third-party installations (like Python virtual environments or compilers) required by the project.
</p>
- <h1>The consumer tree</h1>
+ <h2>The consumer tree</h2>
<p>
The <RT-code>consumer/release/</RT-code> tree is where developers put work product that is ready to be consumed. The entire <RT-code>consumer/release</RT-code> directory is git-ignored and treated as a transient deployment target.
</p>
<li><strong>Project Release:</strong> A project is released when the tester and other team members determine the code in the <RT-code>consumer/release/</RT-code> directory is fully validated and ready for end users. This is enacted by creating a release branch on the version control system (e.g., GitHub), not by moving files to another directory.</li>
</ul>
- <h2>Directory names</h2>
+ <h1>Directory names</h1>
<p>
Each directory name is a <RT-term>property</RT-term> shared among the files contained within it. Hence, a full path forms a semantic sentence describing each file within it.
</p>
In the Unix file system, a person must choose a single string to use as the directory name. Hence, we typically choose a primary property, though when needed, we use multiple property names separated by an underscore.
</p>
- <h1>The version 2.2 Harmony directory tree</h1>
+ <h1>The version 2.2 Harmony directory tree</h1>
<RT-code>
2026-03-07 10:38:58 Z [Harmony:administrator] Thomas_developer@StanleyPark
§/home/Thomas/subu_data/developer/project§
+++ /dev/null
-#!/usr/bin/env -S python3 -B
-# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-
-import os, sys, shutil, stat, pwd, grp, subprocess
-
-HELP = """usage: scratchpad {ls|clear|help|make|write|size|find|lock|unlock} [ARGS]
- ls List scratchpad in an indented tree with perms and owner (quiet if missing).
- clear Remove all contents of scratchpad/ except top-level .gitignore.
- clear NAME Remove scratchpad/NAME only.
- make [NAME] Ensure scratchpad/ exists with .gitignore; with NAME, mkdir scratchpad/NAME.
- write SRC [DST] Copy file/dir SRC into scratchpad (to DST if given; parents created).
- size Print 'empty' if only .gitignore; else total bytes and item count.
- find [OPTS...] Run system 'find' rooted at scratchpad/ with OPTS (omit literal 'scratchpad').
- lock PATH... Attempt 'chattr +i' on given paths under scratchpad/ (no state kept).
- unlock PATH... Attempt 'chattr -i' on given paths under scratchpad/.
-
-Examples:
- scratchpad make
- scratchpad write ~/Downloads/test.tar.gz
- scratchpad find -type f -mtime +30 -print # files older than 30 days
- scratchpad lock some/dir important.txt
- scratchpad unlock some/dir important.txt
-"""
-
-CWD = os.getcwd()
-SP = os.path.join(CWD, "scratchpad")
-GITIGNORE = os.path.join(SP, ".gitignore")
-
-def have_sp() -> bool:
- return os.path.isdir(SP)
-
-def ensure_sp():
- os.makedirs(SP, exist_ok=True)
- ensure_gitignore()
-
-def ensure_gitignore():
- os.makedirs(SP, exist_ok=True)
- if not os.path.isfile(GITIGNORE):
- with open(GITIGNORE, "w", encoding="utf-8") as f:
- f.write("*\n!.gitignore\n")
-
-def filemode(mode: int) -> str:
- try:
- return stat.filemode(mode)
- except Exception:
- return oct(mode & 0o777)
-
-def owner_group(st) -> str:
- try:
- return f"{pwd.getpwuid(st.st_uid).pw_name}:{grp.getgrgid(st.st_gid).gr_name}"
- except Exception:
- return f"{st.st_uid}:{st.st_gid}"
-
-def rel_depth(base: str, root: str) -> int:
- rel = os.path.relpath(base, root)
- return 0 if rel == "." else rel.count(os.sep) + 1
-
-def ls_tree(root: str) -> None:
- if not have_sp():
- return
- print("scratchpad/")
-
- def walk(path: str, indent: str, is_root: bool) -> None:
- try:
- it = list(os.scandir(path))
- except FileNotFoundError:
- return
-
- dirs = [e for e in it if e.is_dir(follow_symlinks=False)]
- files = [e for e in it if not e.is_dir(follow_symlinks=False)]
- dirs.sort(key=lambda e: e.name)
- files.sort(key=lambda e: e.name)
-
- if is_root:
- # 1) root-level hidden files first
- for f in (e for e in files if e.name.startswith(".")):
- st = os.lstat(f.path)
- print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{f.name}")
- # 2) then directories (and recurse so children sit under the parent)
- for d in dirs:
- st = os.lstat(d.path)
- print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{d.name}/")
- walk(d.path, indent + ' ', False)
- # 3) then non-hidden files
- for f in (e for e in files if not e.name.startswith(".")):
- st = os.lstat(f.path)
- print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{f.name}")
- else:
- # subdirs: keep previous order (dirs first, then files; dotfiles naturally sort first)
- for d in dirs:
- st = os.lstat(d.path)
- print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{d.name}/")
- walk(d.path, indent + ' ', False)
- for f in files:
- st = os.lstat(f.path)
- print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{f.name}")
-
- walk(root, " ", True)
-
-
-def clear_all() -> None:
- if not have_sp():
- return
- for name in os.listdir(SP):
- p = os.path.join(SP, name)
- if name == ".gitignore" and os.path.isfile(p):
- continue # preserve only top-level .gitignore
- if os.path.isdir(p) and not os.path.islink(p):
- shutil.rmtree(p, ignore_errors=True)
- else:
- try: os.unlink(p)
- except FileNotFoundError: pass
-
-def clear_subdir(sub: str) -> None:
- if not have_sp():
- return
- target = os.path.normpath(os.path.join(SP, sub))
- try:
- if os.path.commonpath([SP]) != os.path.commonpath([SP, target]):
- return
- except Exception:
- return
- if os.path.isdir(target) and not os.path.islink(target):
- shutil.rmtree(target, ignore_errors=True)
-
-def cmd_make(args):
- ensure_sp()
- if args:
- os.makedirs(os.path.join(SP, args[0]), exist_ok=True)
-
-def cmd_write(args):
- if len(args) < 1:
- print(HELP); return
- if not have_sp():
- ensure_sp()
- src = args[0]
- dst = args[1] if len(args) >= 2 else (os.path.basename(src.rstrip(os.sep)) or "untitled")
- dst_path = os.path.normpath(os.path.join(SP, dst))
- try:
- if os.path.commonpath([SP]) != os.path.commonpath([SP, dst_path]):
- print("refusing to write outside scratchpad", file=sys.stderr); return
- except Exception:
- print("invalid destination", file=sys.stderr); return
- os.makedirs(os.path.dirname(dst_path), exist_ok=True)
- if os.path.isdir(src):
- if os.path.exists(dst_path):
- shutil.rmtree(dst_path, ignore_errors=True)
- shutil.copytree(src, dst_path, dirs_exist_ok=False)
- else:
- shutil.copy2(src, dst_path)
-
-def cmd_size():
- if not have_sp():
- return
- names = os.listdir(SP)
- if [n for n in names if n != ".gitignore"] == []:
- print("empty"); return
- total = 0; count = 0
- for base, dirs, files in os.walk(SP):
- for fn in files:
- if fn == ".gitignore":
- continue
- p = os.path.join(base, fn)
- try:
- total += os.path.getsize(p); count += 1
- except OSError:
- pass
- print(f"bytes={total} items={count}")
-
-def cmd_find(args):
- if not have_sp():
- return
- try:
- subprocess.run(["find", SP] + args, check=False)
- except FileNotFoundError:
- print("find not available", file=sys.stderr)
-
-def cmd_chattr(flag: str, paths):
- if not have_sp() or not paths:
- return
- try:
- subprocess.run(["chattr", "-V"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
- except FileNotFoundError:
- print("chattr not available; lock/unlock skipped", file=sys.stderr); return
- for rel in paths:
- target = os.path.normpath(os.path.join(SP, rel))
- try:
- if os.path.commonpath([SP]) != os.path.commonpath([SP, target]):
- continue
- except Exception:
- continue
- try:
- subprocess.run(["chattr", flag, target], check=False)
- except Exception:
- pass
-
-def CLI():
- if len(sys.argv) < 2:
- print(HELP); return
- cmd, *args = sys.argv[1:]
- if cmd == "ls":
- if have_sp(): ls_tree(SP)
- else: return
- elif cmd == "clear":
- if len(args) >= 1: clear_subdir(args[0])
- else: clear_all()
- elif cmd == "help":
- print(HELP)
- elif cmd == "make":
- cmd_make(args)
- elif cmd == "write":
- cmd_write(args)
- elif cmd == "size":
- cmd_size()
- elif cmd == "find":
- cmd_find(args)
- elif cmd == "lock":
- cmd_chattr("+i", args)
- elif cmd == "unlock":
- cmd_chattr("-i", args)
- else:
- print(HELP)
-
-if __name__ == "__main__":
- CLI()
+++ /dev/null
-#!/usr/bin/env bash
-script_afp=$(realpath "${BASH_SOURCE[0]}")
-if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
- echo "$script_afp:: This script must be sourced, not executed."
- exit 1
-fi
-
-# without this bash takes non-matching globs literally
-shopt -s nullglob
-
-# does not presume sharing or world permissions
-umask 0077
-
-# --------------------------------------------------------------------------------
-# project definition
-
-# actual absolute director path for this script file
-
- script_adp(){
- dirname "$script_afp"
- }
-
-# assume this script is located $REPO_HOME/tools_shared/authored and work backwards
-# to get $REPO_HOME, etc.
-
- REPO_HOME=$(dirname "$(dirname "$(script_adp)")")
- echo REPO_HOME "$REPO_HOME"
-
- PROJECT=$(basename "$REPO_HOME")
- echo PROJECT "$PROJECT"
-
- # set the prompt decoration to the name of the project
- PROMPT_DECOR=$PROJECT
-
- export REPO_HOME PROJECT PROMPT_DECOR
-
-# --------------------------------------------------------------------------------
-# Project wide Tool setup
-#
-
-export VIRTUAL_ENV="$REPO_HOME/shared/third_party/Python"
-export PYTHON_HOME="$VIRTUAL_ENV"
-unset PYTHONHOME
-
-
-# --------------------------------------------------------------------------------
-# PATH
-# precedence: last defined, first discovered
-
- PATH="$REPO_HOME/shared/third_party/RT-project-share/release/bash:$PATH"
- PATH="$REPO_HOME/shared/third_party/RT-project-share/release/amd64:$PATH"
- PATH="$REPO_HOME/shared/third_party:$PATH"
- PATH="$REPO_HOME/shared/authored:$PATH"
- PATH="$REPO_HOME/shared/made:$PATH"
-
- # Remove duplicates
- clean_path() {
- PATH=$(echo ":$PATH" | awk -v RS=: -v ORS=: '!seen[$0]++' | sed 's/^://; s/:$//')
- }
- clean_path
- export PATH
-
-# --------------------------------------------------------------------------------
-# the following functions are provided for other scripts to use.
-# at the top of files that make use of these functions put the following line:
-# script_afp=$(realpath "${BASH_SOURCE[0]}")
-#
-
- ## script's filename
- script_fn(){
- basename "$script_afp"
- }
-
- ## script's dirpath relative to $REPO_HOME
- script_fp(){
- realpath --relative-to="${REPO_HOME}" "$script_afp"
- }
-
- ## script's dirpath relative to $REPO_HOME
- script_dp(){
- dirname "$(script_fp)"
- }
-
- export -f script_adp script_fn script_dp script_fp
-
-#--------------------------------------------------------------------------------
-# used by release scripts
-#
-
- install_file() {
- if [ "$#" -lt 3 ]; then
- echo "env::install_file usage: install_file <source1> <source2> ... <target_dir> <permissions>"
- return 1
- fi
-
- perms="${@: -1}" # Last argument is permissions
- target_dp="${@: -2:1}" # Second-to-last argument is the target directory
- sources=("${@:1:$#-2}") # All other arguments are source files
-
- if [ ! -d "$target_dp" ]; then
- echo "env::install_file no install done: target directory '$target_dp' does not exist."
- return 1
- fi
-
- for source_fp in "${sources[@]}"; do
- if [ ! -f "$source_fp" ]; then
- echo "env::install_file: source file '$source_fp' does not exist."
- return 1
- fi
-
- target_file="$target_dp/$(basename "$source_fp")"
-
- if ! install -m "$perms" "$source_fp" "$target_file"; then
- echo "env::install_file: Failed to install $(basename "$source_fp") to $target_dp"
- return 1
- else
- echo "env::install_file: installed $(basename "$source_fp") to $target_dp with permissions $perms"
- fi
- done
- }
-
- export -f install_file
-
-# --------------------------------------------------------------------------------
-# closing
-#
- if [[ -z "$ENV" ]]; then
- export ENV=$(script_fp)
- fi
-
+++ /dev/null
-echo "Harmony v2.2 2026-03-06"
-
-
-
--- /dev/null
+#!/usr/bin/env -S python3 -B
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+import os, sys, shutil, stat, pwd, grp, subprocess
+
+HELP = """usage: scratchpad {ls|clear|help|make|write|size|find|lock|unlock} [ARGS]
+ ls List scratchpad in an indented tree with perms and owner (quiet if missing).
+ clear Remove all contents of scratchpad/ except top-level .gitignore.
+ clear NAME Remove scratchpad/NAME only.
+ make [NAME] Ensure scratchpad/ exists with .gitignore; with NAME, mkdir scratchpad/NAME.
+ write SRC [DST] Copy file/dir SRC into scratchpad (to DST if given; parents created).
+ size Print 'empty' if only .gitignore; else total bytes and item count.
+ find [OPTS...] Run system 'find' rooted at scratchpad/ with OPTS (omit literal 'scratchpad').
+ lock PATH... Attempt 'chattr +i' on given paths under scratchpad/ (no state kept).
+ unlock PATH... Attempt 'chattr -i' on given paths under scratchpad/.
+
+Examples:
+ scratchpad make
+ scratchpad write ~/Downloads/test.tar.gz
+ scratchpad find -type f -mtime +30 -print # files older than 30 days
+ scratchpad lock some/dir important.txt
+ scratchpad unlock some/dir important.txt
+"""
+
+CWD = os.getcwd()
+SP = os.path.join(CWD, "scratchpad")
+GITIGNORE = os.path.join(SP, ".gitignore")
+
+def have_sp() -> bool:
+ return os.path.isdir(SP)
+
+def ensure_sp():
+ os.makedirs(SP, exist_ok=True)
+ ensure_gitignore()
+
+def ensure_gitignore():
+ os.makedirs(SP, exist_ok=True)
+ if not os.path.isfile(GITIGNORE):
+ with open(GITIGNORE, "w", encoding="utf-8") as f:
+ f.write("*\n!.gitignore\n")
+
+def filemode(mode: int) -> str:
+ try:
+ return stat.filemode(mode)
+ except Exception:
+ return oct(mode & 0o777)
+
+def owner_group(st) -> str:
+ try:
+ return f"{pwd.getpwuid(st.st_uid).pw_name}:{grp.getgrgid(st.st_gid).gr_name}"
+ except Exception:
+ return f"{st.st_uid}:{st.st_gid}"
+
+def rel_depth(base: str, root: str) -> int:
+ rel = os.path.relpath(base, root)
+ return 0 if rel == "." else rel.count(os.sep) + 1
+
+def ls_tree(root: str) -> None:
+ if not have_sp():
+ return
+ print("scratchpad/")
+
+ def walk(path: str, indent: str, is_root: bool) -> None:
+ try:
+ it = list(os.scandir(path))
+ except FileNotFoundError:
+ return
+
+ dirs = [e for e in it if e.is_dir(follow_symlinks=False)]
+ files = [e for e in it if not e.is_dir(follow_symlinks=False)]
+ dirs.sort(key=lambda e: e.name)
+ files.sort(key=lambda e: e.name)
+
+ if is_root:
+ # 1) root-level hidden files first
+ for f in (e for e in files if e.name.startswith(".")):
+ st = os.lstat(f.path)
+ print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{f.name}")
+ # 2) then directories (and recurse so children sit under the parent)
+ for d in dirs:
+ st = os.lstat(d.path)
+ print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{d.name}/")
+ walk(d.path, indent + ' ', False)
+ # 3) then non-hidden files
+ for f in (e for e in files if not e.name.startswith(".")):
+ st = os.lstat(f.path)
+ print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{f.name}")
+ else:
+ # subdirs: keep previous order (dirs first, then files; dotfiles naturally sort first)
+ for d in dirs:
+ st = os.lstat(d.path)
+ print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{d.name}/")
+ walk(d.path, indent + ' ', False)
+ for f in files:
+ st = os.lstat(f.path)
+ print(f"{filemode(st.st_mode)} {owner_group(st)} {indent}{f.name}")
+
+ walk(root, " ", True)
+
+
+def clear_all() -> None:
+ if not have_sp():
+ return
+ for name in os.listdir(SP):
+ p = os.path.join(SP, name)
+ if name == ".gitignore" and os.path.isfile(p):
+ continue # preserve only top-level .gitignore
+ if os.path.isdir(p) and not os.path.islink(p):
+ shutil.rmtree(p, ignore_errors=True)
+ else:
+ try: os.unlink(p)
+ except FileNotFoundError: pass
+
+def clear_subdir(sub: str) -> None:
+ if not have_sp():
+ return
+ target = os.path.normpath(os.path.join(SP, sub))
+ try:
+ if os.path.commonpath([SP]) != os.path.commonpath([SP, target]):
+ return
+ except Exception:
+ return
+ if os.path.isdir(target) and not os.path.islink(target):
+ shutil.rmtree(target, ignore_errors=True)
+
+def cmd_make(args):
+ ensure_sp()
+ if args:
+ os.makedirs(os.path.join(SP, args[0]), exist_ok=True)
+
+def cmd_write(args):
+ if len(args) < 1:
+ print(HELP); return
+ if not have_sp():
+ ensure_sp()
+ src = args[0]
+ dst = args[1] if len(args) >= 2 else (os.path.basename(src.rstrip(os.sep)) or "untitled")
+ dst_path = os.path.normpath(os.path.join(SP, dst))
+ try:
+ if os.path.commonpath([SP]) != os.path.commonpath([SP, dst_path]):
+ print("refusing to write outside scratchpad", file=sys.stderr); return
+ except Exception:
+ print("invalid destination", file=sys.stderr); return
+ os.makedirs(os.path.dirname(dst_path), exist_ok=True)
+ if os.path.isdir(src):
+ if os.path.exists(dst_path):
+ shutil.rmtree(dst_path, ignore_errors=True)
+ shutil.copytree(src, dst_path, dirs_exist_ok=False)
+ else:
+ shutil.copy2(src, dst_path)
+
+def cmd_size():
+ if not have_sp():
+ return
+ names = os.listdir(SP)
+ if [n for n in names if n != ".gitignore"] == []:
+ print("empty"); return
+ total = 0; count = 0
+ for base, dirs, files in os.walk(SP):
+ for fn in files:
+ if fn == ".gitignore":
+ continue
+ p = os.path.join(base, fn)
+ try:
+ total += os.path.getsize(p); count += 1
+ except OSError:
+ pass
+ print(f"bytes={total} items={count}")
+
+def cmd_find(args):
+ if not have_sp():
+ return
+ try:
+ subprocess.run(["find", SP] + args, check=False)
+ except FileNotFoundError:
+ print("find not available", file=sys.stderr)
+
+def cmd_chattr(flag: str, paths):
+ if not have_sp() or not paths:
+ return
+ try:
+ subprocess.run(["chattr", "-V"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
+ except FileNotFoundError:
+ print("chattr not available; lock/unlock skipped", file=sys.stderr); return
+ for rel in paths:
+ target = os.path.normpath(os.path.join(SP, rel))
+ try:
+ if os.path.commonpath([SP]) != os.path.commonpath([SP, target]):
+ continue
+ except Exception:
+ continue
+ try:
+ subprocess.run(["chattr", flag, target], check=False)
+ except Exception:
+ pass
+
+def CLI():
+ if len(sys.argv) < 2:
+ print(HELP); return
+ cmd, *args = sys.argv[1:]
+ if cmd == "ls":
+ if have_sp(): ls_tree(SP)
+ else: return
+ elif cmd == "clear":
+ if len(args) >= 1: clear_subdir(args[0])
+ else: clear_all()
+ elif cmd == "help":
+ print(HELP)
+ elif cmd == "make":
+ cmd_make(args)
+ elif cmd == "write":
+ cmd_write(args)
+ elif cmd == "size":
+ cmd_size()
+ elif cmd == "find":
+ cmd_find(args)
+ elif cmd == "lock":
+ cmd_chattr("+i", args)
+ elif cmd == "unlock":
+ cmd_chattr("-i", args)
+ else:
+ print(HELP)
+
+if __name__ == "__main__":
+ CLI()
--- /dev/null
+#!/usr/bin/env bash
+script_afp=$(realpath "${BASH_SOURCE[0]}")
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ echo "$script_afp:: This script must be sourced, not executed."
+ exit 1
+fi
+
+# without this bash takes non-matching globs literally
+shopt -s nullglob
+
+# does not presume sharing or world permissions
+umask 0077
+
+# --------------------------------------------------------------------------------
+# project definition
+
+# actual absolute director path for this script file
+
+ script_adp(){
+ dirname "$script_afp"
+ }
+
+# assume this script is located $REPO_HOME/tools_shared/authored and work backwards
+# to get $REPO_HOME, etc.
+
+ REPO_HOME=$(dirname "$(dirname "$(script_adp)")")
+ echo REPO_HOME "$REPO_HOME"
+
+ PROJECT=$(basename "$REPO_HOME")
+ echo PROJECT "$PROJECT"
+
+ # set the prompt decoration to the name of the project
+ PROMPT_DECOR=$PROJECT
+
+ export REPO_HOME PROJECT PROMPT_DECOR
+
+# --------------------------------------------------------------------------------
+# Project wide Tool setup
+#
+
+export VIRTUAL_ENV="$REPO_HOME/shared/third_party/Python"
+export PYTHON_HOME="$VIRTUAL_ENV"
+unset PYTHONHOME
+
+
+# --------------------------------------------------------------------------------
+# PATH
+# precedence: last defined, first discovered
+
+ PATH="$REPO_HOME/shared/third_party/RT-project-share/release/bash:$PATH"
+ PATH="$REPO_HOME/shared/third_party/RT-project-share/release/amd64:$PATH"
+ PATH="$REPO_HOME/shared/third_party:$PATH"
+ PATH="$REPO_HOME/shared/authored:$PATH"
+ PATH="$REPO_HOME/shared/made:$PATH"
+
+ # Remove duplicates
+ clean_path() {
+ PATH=$(echo ":$PATH" | awk -v RS=: -v ORS=: '!seen[$0]++' | sed 's/^://; s/:$//')
+ }
+ clean_path
+ export PATH
+
+# --------------------------------------------------------------------------------
+# the following functions are provided for other scripts to use.
+# at the top of files that make use of these functions put the following line:
+# script_afp=$(realpath "${BASH_SOURCE[0]}")
+#
+
+ ## script's filename
+ script_fn(){
+ basename "$script_afp"
+ }
+
+ ## script's dirpath relative to $REPO_HOME
+ script_fp(){
+ realpath --relative-to="${REPO_HOME}" "$script_afp"
+ }
+
+ ## script's dirpath relative to $REPO_HOME
+ script_dp(){
+ dirname "$(script_fp)"
+ }
+
+ export -f script_adp script_fn script_dp script_fp
+
+#--------------------------------------------------------------------------------
+# used by release scripts
+#
+
+ install_file() {
+ if [ "$#" -lt 3 ]; then
+ echo "env::install_file usage: install_file <source1> <source2> ... <target_dir> <permissions>"
+ return 1
+ fi
+
+ perms="${@: -1}" # Last argument is permissions
+ target_dp="${@: -2:1}" # Second-to-last argument is the target directory
+ sources=("${@:1:$#-2}") # All other arguments are source files
+
+ if [ ! -d "$target_dp" ]; then
+ echo "env::install_file no install done: target directory '$target_dp' does not exist."
+ return 1
+ fi
+
+ for source_fp in "${sources[@]}"; do
+ if [ ! -f "$source_fp" ]; then
+ echo "env::install_file: source file '$source_fp' does not exist."
+ return 1
+ fi
+
+ target_file="$target_dp/$(basename "$source_fp")"
+
+ if ! install -m "$perms" "$source_fp" "$target_file"; then
+ echo "env::install_file: Failed to install $(basename "$source_fp") to $target_dp"
+ return 1
+ else
+ echo "env::install_file: installed $(basename "$source_fp") to $target_dp with permissions $perms"
+ fi
+ done
+ }
+
+ export -f install_file
+
+# --------------------------------------------------------------------------------
+# closing
+#
+ if [[ -z "$ENV" ]]; then
+ export ENV=$(script_fp)
+ fi
+
--- /dev/null
+echo "Harmony v2.2 2026-03-06"
+
+
+