chore(repo): put the template under version control
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.
This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.
secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:44:26 -05:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
#
|
|
|
|
|
# Create the documentation tree a project is expected to have, copied from the
|
|
|
|
|
# template's own copy of it, without ever overwriting a document that is
|
|
|
|
|
# already there.
|
|
|
|
|
#
|
|
|
|
|
# bash scripts/scaffold.sh # into the current directory
|
|
|
|
|
# bash scripts/scaffold.sh --dry-run # list everything, write nothing
|
|
|
|
|
# bash scripts/scaffold.sh --into ../new-app
|
|
|
|
|
# bash scripts/scaffold.sh --force # overwrite; needs a clean git tree
|
|
|
|
|
# SCAFFOLD_TEMPLATE_ROOT=/path/to/Template bash scripts/scaffold.sh
|
|
|
|
|
#
|
|
|
|
|
# ===========================================================================
|
|
|
|
|
# TEMPLATE COPY — configure this before the first run
|
|
|
|
|
# ===========================================================================
|
|
|
|
|
#
|
|
|
|
|
# Copy to `scripts/scaffold.sh` and set TEMPLATE_ROOT in the CONFIGURATION
|
|
|
|
|
# block below, or pass it as SCAFFOLD_TEMPLATE_ROOT. It is empty and it stays
|
|
|
|
|
# empty: the template sits at a different absolute path on every machine, and
|
|
|
|
|
# a plausible-looking default would quietly resolve to whatever checkout
|
feat(docs): OPERATIONS.md, the runbook that did not exist
The template shipped scripts to back up, deploy, check health and read the
deployed version, and no document saying where errors go, what alerts, who
receives it, or what to run first when it is down. Grep across docs/** found
zero mentions of error tracking, observability or database restore.
Five sections. Where errors go, with the distinction that matters at 3am --
healthcheck.sh answers "is it up", error tracking answers "is it working", and a
service returning 500 to everything is up. What alerts and to whom, naming a
person rather than a channel nobody owns. Backups, whose last row is the date of
the last verified restore, because a backup nobody has restored is a guess.
Rate limits and cost ceilings, *(precautionary)*. And an ordered "it is down,
what now" where every step is a command that changes nothing.
Marked *(only for a deployed service)*, with the instruction to delete rather
than keep the headings unanswered: an empty runbook reads as one nobody wrote,
which is worse than one that never applied.
scaffold.sh now lays it down (17 files, 0 skipped) and DOC_TRUST_MAP.md points
at it from both tables. Two prose counts in scaffold.sh's header said
"thirteen documents" and were already stale; they no longer carry a number,
since a count in prose beside a list in code drifts the moment the list grows --
which is what just happened.
closes #6
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:57:20 -05:00
|
|
|
# happens to be there. That is not a "file not found" — it is a whole
|
|
|
|
|
# documentation tree arriving with another project's Owner lines, another
|
|
|
|
|
# project's review dates and another project's branding, each document reading
|
|
|
|
|
# as this project's own the moment it lands.
|
chore(repo): put the template under version control
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.
This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.
secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:44:26 -05:00
|
|
|
#
|
|
|
|
|
# Assumes: bash and coreutils. git is used only by --force, and only to read.
|
|
|
|
|
#
|
|
|
|
|
# ## Why this exists
|
|
|
|
|
#
|
|
|
|
|
# The convention this tree encodes is cheap to state and, on the evidence,
|
|
|
|
|
# almost never adopted whole. Eight repositories were surveyed before the
|
|
|
|
|
# template was last rewritten: exactly one kept the ledger the template asked
|
|
|
|
|
# for, three had invented their own dialects, and one had pushed nothing but a
|
|
|
|
|
# README. Nobody refused the convention. They started projects, and a
|
|
|
|
|
# convention that has to be re-typed from memory at the start of each one is a
|
|
|
|
|
# convention that decays into four different ones.
|
|
|
|
|
#
|
|
|
|
|
# So the first act of a new project is a command, not a reading exercise. What
|
|
|
|
|
# is left afterwards — the placeholders, the tracker, the branding — is work
|
|
|
|
|
# that genuinely needs a person, and it is visible precisely because the
|
|
|
|
|
# skeleton around it is already correct.
|
|
|
|
|
#
|
|
|
|
|
# ## Never overwriting is the whole safety property
|
|
|
|
|
#
|
|
|
|
|
# Every other rule here follows from one: a scaffold that clobbers a written
|
|
|
|
|
# document destroys work that nothing would notice was gone. There is no test
|
|
|
|
|
# for "the architecture notes used to say more than this", no build failure, no
|
|
|
|
|
# diff to read if the file was never committed. It is silent at the moment it
|
|
|
|
|
# happens and silent forever afterwards, and the natural time to re-run a
|
|
|
|
|
# scaffold — partway into a project, to pick up a file that was missed — is
|
|
|
|
|
# exactly the time the tree is full of real writing.
|
|
|
|
|
#
|
|
|
|
|
# So an existing file is reported as "kept" and left untouched, always. --force
|
|
|
|
|
# exists for the one honest case (a tree scaffolded from a stale template, not
|
|
|
|
|
# yet written into) and it refuses twice over: once for the tree, which must be
|
|
|
|
|
# clean, and again for each file, which git must actually track. A clean tree
|
|
|
|
|
# alone is not enough — it says nothing about ignored files — so the worst
|
|
|
|
|
# --force can do is something `git checkout` undoes.
|
|
|
|
|
#
|
|
|
|
|
# ## What it deliberately does not do
|
|
|
|
|
#
|
|
|
|
|
# It does not touch git. No init, no add, no commit — creating files is enough,
|
|
|
|
|
# and committing them is a judgement about what belongs in the history of this
|
|
|
|
|
# project, made by whoever will answer for that history.
|
|
|
|
|
#
|
|
|
|
|
# It does not fill anything in. The angle-bracket placeholders are left
|
|
|
|
|
# standing because a document whose header says it was reviewed on a date
|
|
|
|
|
# nobody reviewed it is worse than one that visibly has not been filled in: the
|
|
|
|
|
# first is trusted at the exact moment it should not be. An unfilled header
|
|
|
|
|
# keeps asking; an invented one stops.
|
|
|
|
|
#
|
|
|
|
|
# It does not copy branding. See the docs/data/img note near the bottom.
|
|
|
|
|
#
|
|
|
|
|
# It does not copy the scripts that live in this folder. They are in the
|
|
|
|
|
# template's docs tree for the template's own reasons, and each one needs
|
|
|
|
|
# configuring before its first run — an unconfigured release.sh landing in
|
|
|
|
|
# every new repository is a loaded gun, not a head start. Copy those one at a
|
|
|
|
|
# time, having read them.
|
|
|
|
|
#
|
|
|
|
|
# It does not create the tracker labels, the first milestone or the project's
|
|
|
|
|
# README, and it does not check conformance. It reports what it did. Whether
|
|
|
|
|
# the project passes is a different question, asked by a different thing.
|
|
|
|
|
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# CONFIGURATION — set this one, then delete this banner.
|
|
|
|
|
#
|
|
|
|
|
# Empty on purpose. See the note at the top: a default here does not fail, it
|
feat(docs): OPERATIONS.md, the runbook that did not exist
The template shipped scripts to back up, deploy, check health and read the
deployed version, and no document saying where errors go, what alerts, who
receives it, or what to run first when it is down. Grep across docs/** found
zero mentions of error tracking, observability or database restore.
Five sections. Where errors go, with the distinction that matters at 3am --
healthcheck.sh answers "is it up", error tracking answers "is it working", and a
service returning 500 to everything is up. What alerts and to whom, naming a
person rather than a channel nobody owns. Backups, whose last row is the date of
the last verified restore, because a backup nobody has restored is a guess.
Rate limits and cost ceilings, *(precautionary)*. And an ordered "it is down,
what now" where every step is a command that changes nothing.
Marked *(only for a deployed service)*, with the instruction to delete rather
than keep the headings unanswered: an empty runbook reads as one nobody wrote,
which is worse than one that never applied.
scaffold.sh now lays it down (17 files, 0 skipped) and DOC_TRUST_MAP.md points
at it from both tables. Two prose counts in scaffold.sh's header said
"thirteen documents" and were already stale; they no longer carry a number,
since a count in prose beside a list in code drifts the moment the list grows --
which is what just happened.
closes #6
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:57:20 -05:00
|
|
|
# copies the wrong project's documents and says "created" for every one.
|
chore(repo): put the template under version control
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.
This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.
secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:44:26 -05:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
# Absolute path to the template checkout — the directory holding `docs/`.
|
|
|
|
|
TEMPLATE_ROOT="${SCAFFOLD_TEMPLATE_ROOT:-}"
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# The tree, listed explicitly.
|
|
|
|
|
#
|
|
|
|
|
# A recursive copy would be shorter and would drag in whatever else is sitting
|
|
|
|
|
# in the template's docs tree today, including this script. An explicit list is
|
|
|
|
|
# also the only thing that can tell the difference between "the template does
|
|
|
|
|
# not have that file" and "the copy quietly produced twelve of thirteen" — the
|
|
|
|
|
# second looks identical to success from the outside.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
DOCS=(
|
|
|
|
|
docs/DOC_TRUST_MAP.md
|
|
|
|
|
docs/WORK_CYCLE.md
|
feat(docs): OPERATIONS.md, the runbook that did not exist
The template shipped scripts to back up, deploy, check health and read the
deployed version, and no document saying where errors go, what alerts, who
receives it, or what to run first when it is down. Grep across docs/** found
zero mentions of error tracking, observability or database restore.
Five sections. Where errors go, with the distinction that matters at 3am --
healthcheck.sh answers "is it up", error tracking answers "is it working", and a
service returning 500 to everything is up. What alerts and to whom, naming a
person rather than a channel nobody owns. Backups, whose last row is the date of
the last verified restore, because a backup nobody has restored is a guess.
Rate limits and cost ceilings, *(precautionary)*. And an ordered "it is down,
what now" where every step is a command that changes nothing.
Marked *(only for a deployed service)*, with the instruction to delete rather
than keep the headings unanswered: an empty runbook reads as one nobody wrote,
which is worse than one that never applied.
scaffold.sh now lays it down (17 files, 0 skipped) and DOC_TRUST_MAP.md points
at it from both tables. Two prose counts in scaffold.sh's header said
"thirteen documents" and were already stale; they no longer carry a number,
since a count in prose beside a list in code drifts the moment the list grows --
which is what just happened.
closes #6
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:57:20 -05:00
|
|
|
docs/OPERATIONS.md
|
chore(repo): put the template under version control
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.
This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.
secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:44:26 -05:00
|
|
|
docs/TOOLS.md
|
|
|
|
|
docs/planning/PROJECT_PLAN.md
|
|
|
|
|
docs/qa/ClaudeQAPlan.md
|
|
|
|
|
docs/qa/ClaudeQACoverage.md
|
|
|
|
|
docs/qa/ClaudeReport.md
|
|
|
|
|
docs/history/HISTORY.md
|
|
|
|
|
docs/history/DEVELOPMENT_LOG.md
|
|
|
|
|
docs/history/BATCH_LEDGER.md
|
|
|
|
|
docs/security/SECURITY.md
|
|
|
|
|
docs/security/SECURITY_CHECKLIST.md
|
|
|
|
|
docs/architecture/README.md
|
fix(scaffold): new projects were missing two documents they reference
docs/data/README.md links to `img/README.md` for the dimensions, weights and
magic-byte rule of the three marks. scaffold.sh never copied it: DOCS did not
list it and DIRS created docs/data/img empty. Every scaffolded project therefore
started with a broken link in a required document, a doc-claims failure on its
first run, and no copy of the spec the link promises.
The omission looks like a misreading of the script's own rule. Its header says
it does not copy branding and that the template's docs/data/img is not a source
-- both true of the three .webp marks, which must not be invented because a
placeholder that looks deliberate outlives the issue that would have replaced
it. img/README.md is documentation about those files, not one of them.
Verifying that fix surfaced a second, plainer one: docs/architecture/GUARDS.md
was never scaffolded either, while being referenced by TOOLS.md,
DOC_TRUST_MAP.md and architecture/README.md twice. It is a document, not a
script, and nothing argued for leaving it out.
A scaffold now writes 19 files, and a freshly scaffolded project no longer names
a document that is not there.
This is the fourth instance here of one shape -- a document naming a path that
is not present. The others were docs/planning/FUTURE.md in the batch ledger, an
Exempt: line inside a code fence, and docs/data/logo.webp as an example of where
NOT to put an asset. The first three were wrong in the template; this one was
correct in the template and wrong in every copy of it, which is why running
doc-claims here never caught it and scaffolding into a scratch directory did.
closes #15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:34:12 -05:00
|
|
|
docs/architecture/GUARDS.md
|
chore(repo): put the template under version control
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.
This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.
secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:44:26 -05:00
|
|
|
docs/design/README.md
|
|
|
|
|
docs/data/README.md
|
fix(scaffold): new projects were missing two documents they reference
docs/data/README.md links to `img/README.md` for the dimensions, weights and
magic-byte rule of the three marks. scaffold.sh never copied it: DOCS did not
list it and DIRS created docs/data/img empty. Every scaffolded project therefore
started with a broken link in a required document, a doc-claims failure on its
first run, and no copy of the spec the link promises.
The omission looks like a misreading of the script's own rule. Its header says
it does not copy branding and that the template's docs/data/img is not a source
-- both true of the three .webp marks, which must not be invented because a
placeholder that looks deliberate outlives the issue that would have replaced
it. img/README.md is documentation about those files, not one of them.
Verifying that fix surfaced a second, plainer one: docs/architecture/GUARDS.md
was never scaffolded either, while being referenced by TOOLS.md,
DOC_TRUST_MAP.md and architecture/README.md twice. It is a document, not a
script, and nothing argued for leaving it out.
A scaffold now writes 19 files, and a freshly scaffolded project no longer names
a document that is not there.
This is the fourth instance here of one shape -- a document naming a path that
is not present. The others were docs/planning/FUTURE.md in the batch ledger, an
Exempt: line inside a code fence, and docs/data/logo.webp as an example of where
NOT to put an asset. The first three were wrong in the template; this one was
correct in the template and wrong in every copy of it, which is why running
doc-claims here never caught it and scaffolding into a scratch directory did.
closes #15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:34:12 -05:00
|
|
|
# The spec for the three marks, not one of the marks. `docs/data/README.md`
|
|
|
|
|
# links to it, so leaving it behind gave every scaffolded project a broken
|
|
|
|
|
# link and a doc-claims failure on its first run -- the rule above about not
|
|
|
|
|
# copying branding is about the .webp files, which genuinely must not be
|
|
|
|
|
# invented, and this is documentation about them.
|
|
|
|
|
docs/data/img/README.md
|
chore(repo): put the template under version control
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.
This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.
secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:44:26 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Directories made empty, holding nothing this script is willing to invent.
|
|
|
|
|
DIRS=(docs/data/img)
|
|
|
|
|
|
|
|
|
|
# The three names the conformance check looks for, by name, in docs/data/img.
|
|
|
|
|
BRANDING=(icon.webp logo.webp banner.webp)
|
|
|
|
|
|
|
|
|
|
# The placeholders the template leaves for a person to replace. Used only to
|
|
|
|
|
# count what is still outstanding at the end — if the template's placeholder
|
|
|
|
|
# style changes this finds nothing, which is why finding nothing prints nothing
|
|
|
|
|
# rather than an all-clear.
|
|
|
|
|
PLACEHOLDER_RE='<(Project|YYYY-MM-DD|who maintains this)>'
|
|
|
|
|
|
|
|
|
|
say() { printf '\033[1mscaffold:\033[0m %s\n' "$*" >&2; }
|
|
|
|
|
die() { printf '\033[1mscaffold:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
|
|
|
|
|
|
usage() {
|
|
|
|
|
cat >&2 <<'EOF'
|
|
|
|
|
scaffold: create the documentation tree a project is expected to have.
|
|
|
|
|
|
|
|
|
|
bash scripts/scaffold.sh into the current directory
|
|
|
|
|
bash scripts/scaffold.sh --into <path> into another project root
|
|
|
|
|
bash scripts/scaffold.sh --dry-run list what would happen, write nothing
|
|
|
|
|
bash scripts/scaffold.sh --force overwrite; refuses on a dirty git tree
|
|
|
|
|
|
|
|
|
|
SCAFFOLD_TEMPLATE_ROOT path to the template checkout (required, no default)
|
|
|
|
|
SCAFFOLD_INTO same as --into
|
|
|
|
|
|
|
|
|
|
An existing file is never overwritten without --force. Exits non-zero if any
|
|
|
|
|
path was skipped, including one the template itself is missing.
|
|
|
|
|
EOF
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DRY_RUN=""
|
|
|
|
|
FORCE=""
|
|
|
|
|
TARGET="${SCAFFOLD_INTO:-}"
|
|
|
|
|
|
|
|
|
|
# Set only when --force resolves a repository. Declared here so the copy loop
|
|
|
|
|
# can test it under `set -u` whether or not --force was passed.
|
|
|
|
|
force_top=""
|
|
|
|
|
|
|
|
|
|
# Whether the operator named the target, as opposed to inheriting the current
|
|
|
|
|
# directory. The guard below is only for the inherited case.
|
|
|
|
|
TARGET_NAMED=""
|
|
|
|
|
[ -n "$TARGET" ] && TARGET_NAMED="yes"
|
|
|
|
|
|
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
|
|
|
case "$1" in
|
|
|
|
|
--dry-run) DRY_RUN="yes" ;;
|
|
|
|
|
--force) FORCE="yes" ;;
|
|
|
|
|
--into)
|
|
|
|
|
# A bare --into would otherwise swallow the next flag as a path and
|
|
|
|
|
# scaffold into a directory called "--dry-run".
|
|
|
|
|
[ "$#" -ge 2 ] || die "--into needs a path."
|
|
|
|
|
case "$2" in -*) die "--into needs a path, got '$2'." ;; esac
|
|
|
|
|
# An empty string is not "here". Without this it becomes "." while still
|
|
|
|
|
# counting as named, which silently disables the project-root guard below
|
|
|
|
|
# — the one case where the guard is most likely to be right.
|
|
|
|
|
[ -n "$2" ] || die "--into was given an empty path. Use --into . to mean the
|
|
|
|
|
current directory."
|
|
|
|
|
TARGET="$2"; TARGET_NAMED="yes"; shift ;;
|
|
|
|
|
--into=*)
|
|
|
|
|
TARGET="${1#--into=}"; TARGET_NAMED="yes"
|
|
|
|
|
[ -n "$TARGET" ] || die "--into needs a path." ;;
|
|
|
|
|
-h|--help) usage; exit 0 ;;
|
|
|
|
|
*) die "unknown argument '$1'. Run --help for usage." ;;
|
|
|
|
|
esac
|
|
|
|
|
shift
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Resolve both ends before touching either. Everything checkable is checked
|
|
|
|
|
# before the first file is written.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
[ -n "$TEMPLATE_ROOT" ] || die "set TEMPLATE_ROOT (or SCAFFOLD_TEMPLATE_ROOT) — the path to the
|
|
|
|
|
template checkout holding docs/. See the CONFIGURATION block."
|
|
|
|
|
|
|
|
|
|
# A leading ~ inside a variable is a literal character, not $HOME. The failure
|
|
|
|
|
# is otherwise "no such directory" naming a path the operator can see exists,
|
|
|
|
|
# which sends them looking in the wrong place.
|
|
|
|
|
case "$TEMPLATE_ROOT" in
|
|
|
|
|
'~'*) die "TEMPLATE_ROOT is '$TEMPLATE_ROOT' — a leading ~ is not expanded inside a
|
|
|
|
|
variable. Write the path out, or use \"\$HOME/...\"." ;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
[ -d "$TEMPLATE_ROOT" ] || die "TEMPLATE_ROOT '$TEMPLATE_ROOT' is not a directory."
|
|
|
|
|
|
|
|
|
|
TEMPLATE_ABS=$(cd "$TEMPLATE_ROOT" 2>/dev/null && pwd -P) \
|
|
|
|
|
|| die "cannot enter TEMPLATE_ROOT '$TEMPLATE_ROOT' — check permissions."
|
|
|
|
|
|
|
|
|
|
[ -d "$TEMPLATE_ABS/docs" ] || die "'$TEMPLATE_ABS' has no docs/ directory, so it is not the template
|
|
|
|
|
root. TEMPLATE_ROOT is the checkout, not the docs folder inside it."
|
|
|
|
|
|
|
|
|
|
# Same trap as TEMPLATE_ROOT above, and worse here: the bare "does not exist"
|
|
|
|
|
# message names a path the operator can see does exist.
|
|
|
|
|
case "$TARGET" in
|
|
|
|
|
'~'*) die "target is '$TARGET' — a leading ~ is not expanded inside a variable or
|
|
|
|
|
inside quotes. Write the path out, or use \"\$HOME/...\"." ;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
[ -n "$TARGET" ] || TARGET="."
|
|
|
|
|
|
|
|
|
|
# Refused rather than created. `mkdir -p` on a typo succeeds, and the result is
|
|
|
|
|
# a complete, correct documentation tree in a directory that should not exist —
|
|
|
|
|
# which reads as success and is found weeks later, if at all.
|
|
|
|
|
[ -d "$TARGET" ] || die "target '$TARGET' does not exist. Create it first — scaffolding into a
|
|
|
|
|
mistyped path produces a tree that looks entirely correct."
|
|
|
|
|
|
|
|
|
|
TARGET_ABS=$(cd "$TARGET" 2>/dev/null && pwd -P) \
|
|
|
|
|
|| die "cannot enter target '$TARGET' — check permissions."
|
|
|
|
|
|
|
|
|
|
[ "$TARGET_ABS" != "$TEMPLATE_ABS" ] \
|
|
|
|
|
|| die "the target is the template root itself. There is nothing to scaffold."
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# An inherited current directory is the one input nobody typed.
|
|
|
|
|
#
|
|
|
|
|
# Run from src/ or from docs/ itself — both of which are where you are when you
|
|
|
|
|
# think to run this — the default would build a second, wrong-place tree that
|
|
|
|
|
# every convention here then fails to find. Only guessed targets are checked;
|
|
|
|
|
# --into is an instruction and is obeyed.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
if [ -z "$TARGET_NAMED" ]; then
|
|
|
|
|
if command -v git >/dev/null 2>&1; then
|
|
|
|
|
guess_top=$(git -C "$TARGET_ABS" rev-parse --show-toplevel 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
if [ -n "$guess_top" ]; then
|
|
|
|
|
guess_top=$(cd "$guess_top" 2>/dev/null && pwd -P)
|
|
|
|
|
|
|
|
|
|
if [ -n "$guess_top" ] && [ "$guess_top" != "$TARGET_ABS" ]; then
|
|
|
|
|
die "you are in a subdirectory of '$guess_top'. docs/ belongs at the project
|
|
|
|
|
root. Re-run with --into '$guess_top', or --into . to mean here."
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
# Said out loud rather than passed over. Without git this check cannot run
|
|
|
|
|
# at all, and a silent skip reads exactly like a check that passed.
|
|
|
|
|
say "note: git is not installed, so the \"is this the project root?\" check did"
|
|
|
|
|
say " not run. '$TARGET_ABS' is being taken as the root on trust."
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# --force may only run where the damage is undoable.
|
|
|
|
|
#
|
|
|
|
|
# Overwriting is recoverable exactly when git already holds the current
|
|
|
|
|
# contents. A dirty tree means it does not, and an untracked file is the worst
|
|
|
|
|
# case of all — scaffolded an hour ago, written into since, never committed,
|
|
|
|
|
# and nothing anywhere remembers what it said. So untracked counts as dirty.
|
|
|
|
|
#
|
|
|
|
|
# This is the tree-wide half of the check, and on its own it is not enough:
|
|
|
|
|
# `git status --porcelain` is silent about ignored files, so the per-file
|
|
|
|
|
# `ls-files` test in the copy loop below is what actually closes the gap.
|
|
|
|
|
#
|
|
|
|
|
# Every way of failing to answer the question is refused, not passed over.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
if [ -n "$FORCE" ]; then
|
|
|
|
|
command -v git >/dev/null 2>&1 \
|
|
|
|
|
|| die "--force needs git, so an overwrite can be undone with 'git checkout'."
|
|
|
|
|
|
|
|
|
|
force_top=$(git -C "$TARGET_ABS" rev-parse --show-toplevel 2>/dev/null) \
|
|
|
|
|
|| force_top=""
|
|
|
|
|
|
|
|
|
|
[ -n "$force_top" ] || die "--force refused: '$TARGET_ABS' is not inside a git repository, so an
|
|
|
|
|
overwrite would be permanent. Commit the tree somewhere first, or drop
|
|
|
|
|
--force and let existing files be kept."
|
|
|
|
|
|
|
|
|
|
if ! dirty=$(git -C "$force_top" status --porcelain 2>/dev/null); then
|
|
|
|
|
die "--force refused: could not read git status in '$force_top'. Not knowing
|
|
|
|
|
whether the tree is clean is a reason to stop, not to continue."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -n "$dirty" ]; then
|
|
|
|
|
say "--force refused: '$force_top' has uncommitted or untracked changes:"
|
|
|
|
|
printf '%s\n' "$dirty" | head -n 10 >&2
|
|
|
|
|
die "commit or stash them first. --force is only safe when git already holds
|
|
|
|
|
what it is about to replace."
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
say "template $TEMPLATE_ABS"
|
|
|
|
|
say "target $TARGET_ABS"
|
|
|
|
|
[ -n "$DRY_RUN" ] && say "--dry-run: nothing will be written."
|
|
|
|
|
[ -n "$FORCE" ] && say "--force: existing files WILL be replaced (git tree is clean)."
|
|
|
|
|
say ""
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# The copy.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
CREATED=0
|
|
|
|
|
KEPT=0
|
|
|
|
|
OVERWRITTEN=0
|
|
|
|
|
MISSING_TEMPLATE=0
|
|
|
|
|
UNREADABLE=0
|
|
|
|
|
CONFLICTED=0
|
|
|
|
|
FAILED=0
|
|
|
|
|
HEADERLESS=0
|
|
|
|
|
|
|
|
|
|
MISSING_LIST=()
|
|
|
|
|
PLACEHOLDER_LIST=()
|
|
|
|
|
|
|
|
|
|
report() { printf ' %-13s %s%s\n' "$1" "$2" "${3:+ ($3)}" >&2; }
|
|
|
|
|
|
|
|
|
|
# Every document opens with an H1 and then a fenced status header. Checked on
|
|
|
|
|
# the template's copy rather than the result, because a header missing here
|
|
|
|
|
# means the template drifted and every project scaffolded from it starts
|
|
|
|
|
# non-conformant — a fact about the template, reported as one.
|
|
|
|
|
has_status_header() {
|
|
|
|
|
awk 'NR==1 { if ($0 !~ /^# /) { bad=1; exit } }
|
|
|
|
|
NR>1 && NR<=4 { if ($0 ~ /^```/) { ok=1; exit } }
|
|
|
|
|
NR>4 { exit }
|
|
|
|
|
END { exit (ok && !bad) ? 0 : 1 }' "$1" 2>/dev/null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for rel in "${DIRS[@]}"; do
|
|
|
|
|
dst="$TARGET_ABS/$rel"
|
|
|
|
|
|
|
|
|
|
if [ -d "$dst" ]; then
|
|
|
|
|
report "kept" "$rel/" "directory exists"
|
|
|
|
|
KEPT=$((KEPT + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -e "$dst" ] || [ -L "$dst" ]; then
|
|
|
|
|
report "SKIPPED" "$rel/" "exists and is not a directory"
|
|
|
|
|
CONFLICTED=$((CONFLICTED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -n "$DRY_RUN" ]; then
|
|
|
|
|
report "would create" "$rel/" "empty directory"
|
|
|
|
|
CREATED=$((CREATED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if mkdir -p "$dst" 2>/dev/null; then
|
|
|
|
|
report "created" "$rel/" "empty directory"
|
|
|
|
|
CREATED=$((CREATED + 1))
|
|
|
|
|
else
|
|
|
|
|
report "FAILED" "$rel/" "could not create"
|
|
|
|
|
FAILED=$((FAILED + 1))
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
for rel in "${DOCS[@]}"; do
|
|
|
|
|
src="$TEMPLATE_ABS/$rel"
|
|
|
|
|
dst="$TARGET_ABS/$rel"
|
|
|
|
|
|
|
|
|
|
# Reported, counted and returned in the exit status. A template missing a
|
|
|
|
|
# file it is supposed to carry produces a tree that is wrong in a way only
|
|
|
|
|
# this run can see — by the next run, "not in the template" and "already
|
|
|
|
|
# here" look the same from the target's side.
|
|
|
|
|
if [ ! -e "$src" ]; then
|
|
|
|
|
report "SKIPPED" "$rel" "missing from the template"
|
|
|
|
|
MISSING_TEMPLATE=$((MISSING_TEMPLATE + 1))
|
|
|
|
|
MISSING_LIST+=("$rel")
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Present but unreadable is a different fact with a different fix — a mode
|
|
|
|
|
# bit here, a missing document there. Reporting both as "missing from the
|
|
|
|
|
# template" sends someone to write a file that is already sitting in front
|
|
|
|
|
# of them.
|
|
|
|
|
if [ ! -f "$src" ] || [ ! -r "$src" ]; then
|
|
|
|
|
report "SKIPPED" "$rel" "in the template but not a readable regular file"
|
|
|
|
|
UNREADABLE=$((UNREADABLE + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
has_status_header "$src" || HEADERLESS=$((HEADERLESS + 1))
|
|
|
|
|
|
|
|
|
|
# A symlink is followed by cp, so "writing into docs/" could land anywhere on
|
|
|
|
|
# the disk, including on top of the file it points at. Never written through,
|
|
|
|
|
# in either direction, --force included.
|
|
|
|
|
if [ -L "$dst" ]; then
|
|
|
|
|
report "SKIPPED" "$rel" "is a symlink — never written through"
|
|
|
|
|
CONFLICTED=$((CONFLICTED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -e "$dst" ] && [ ! -f "$dst" ]; then
|
|
|
|
|
report "SKIPPED" "$rel" "exists and is not a regular file"
|
|
|
|
|
CONFLICTED=$((CONFLICTED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -f "$dst" ]; then
|
|
|
|
|
if [ -z "$FORCE" ]; then
|
|
|
|
|
report "kept" "$rel" "already here, not touched"
|
|
|
|
|
KEPT=$((KEPT + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# An identical file is not an overwrite worth announcing, and calling it
|
|
|
|
|
# one inflates the number that is supposed to mean "work was replaced".
|
|
|
|
|
if cmp -s "$src" "$dst"; then
|
|
|
|
|
report "kept" "$rel" "identical to the template"
|
|
|
|
|
KEPT=$((KEPT + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# The clean-tree check above is necessary and not sufficient. `git status
|
|
|
|
|
# --porcelain` says nothing about ignored files, so a repository with
|
|
|
|
|
# docs/qa/ClaudeReport.md in .gitignore reports clean while git holds no
|
|
|
|
|
# copy of it at all. Overwriting that is exactly the permanent, silent loss
|
|
|
|
|
# the whole script is built to refuse, announced as "(git tree is clean)".
|
|
|
|
|
#
|
|
|
|
|
# So the question is asked per file, of git, in the only form that answers
|
|
|
|
|
# it: does git track this path. Anything else — ignored, untracked, or an
|
|
|
|
|
# unreadable index — is refused and counted, never overwritten.
|
|
|
|
|
if ! git -C "$force_top" ls-files --error-unmatch -- "$dst" >/dev/null 2>&1; then
|
|
|
|
|
report "SKIPPED" "$rel" "--force: git does not track it (ignored?) — overwrite would be permanent"
|
|
|
|
|
CONFLICTED=$((CONFLICTED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -n "$DRY_RUN" ]; then
|
|
|
|
|
report "would REPLACE" "$rel" "--force, differs from the template"
|
|
|
|
|
OVERWRITTEN=$((OVERWRITTEN + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
elif [ -n "$DRY_RUN" ]; then
|
|
|
|
|
report "would create" "$rel"
|
|
|
|
|
CREATED=$((CREATED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if ! mkdir -p "$(dirname "$dst")" 2>/dev/null; then
|
|
|
|
|
report "FAILED" "$rel" "could not create its directory"
|
|
|
|
|
FAILED=$((FAILED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
existed=""
|
|
|
|
|
[ -f "$dst" ] && existed="yes"
|
|
|
|
|
|
|
|
|
|
if ! cp -- "$src" "$dst" 2>/dev/null; then
|
|
|
|
|
report "FAILED" "$rel" "copy failed"
|
|
|
|
|
FAILED=$((FAILED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Verified rather than assumed. A short write on a full disk leaves a file
|
|
|
|
|
# that exists, reports as created, and is a truncated document — which is the
|
|
|
|
|
# one outcome here that is worse than not copying at all.
|
|
|
|
|
if ! cmp -s "$src" "$dst"; then
|
|
|
|
|
report "FAILED" "$rel" "copied file does not match the template — check disk space"
|
|
|
|
|
FAILED=$((FAILED + 1))
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -n "$existed" ]; then
|
|
|
|
|
report "REPLACED" "$rel" "--force"
|
|
|
|
|
OVERWRITTEN=$((OVERWRITTEN + 1))
|
|
|
|
|
else
|
|
|
|
|
report "created" "$rel"
|
|
|
|
|
CREATED=$((CREATED + 1))
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
grep -Eq "$PLACEHOLDER_RE" "$dst" 2>/dev/null && PLACEHOLDER_LIST+=("$rel")
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
SKIPPED=$((MISSING_TEMPLATE + UNREADABLE + CONFLICTED + FAILED))
|
|
|
|
|
|
|
|
|
|
say ""
|
|
|
|
|
|
|
|
|
|
if [ -n "$DRY_RUN" ]; then
|
|
|
|
|
say "--dry-run: nothing was written. It would have made"
|
|
|
|
|
say " ${CREATED} created, ${KEPT} kept, ${OVERWRITTEN} replaced, ${SKIPPED} skipped."
|
|
|
|
|
else
|
|
|
|
|
say "${CREATED} created, ${KEPT} kept, ${OVERWRITTEN} replaced, ${SKIPPED} skipped."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
say " skipped breaks down as ${MISSING_TEMPLATE} missing from the template,"
|
|
|
|
|
say " ${UNREADABLE} present in the template but unreadable, ${CONFLICTED} conflicting"
|
|
|
|
|
say " with something already at that path, ${FAILED} failed."
|
|
|
|
|
|
|
|
|
|
if [ "$MISSING_TEMPLATE" -gt 0 ]; then
|
|
|
|
|
say ""
|
|
|
|
|
say "WARNING: the template at $TEMPLATE_ABS does not have:"
|
|
|
|
|
printf ' %s\n' ${MISSING_LIST[@]+"${MISSING_LIST[@]}"} >&2
|
|
|
|
|
say " This tree is incomplete and nothing downstream will say so."
|
|
|
|
|
say " Fix the template, or write those files by hand."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ "$HEADERLESS" -gt 0 ]; then
|
|
|
|
|
say ""
|
|
|
|
|
say "WARNING: ${HEADERLESS} template document(s) do not open with an H1 followed by"
|
|
|
|
|
say " a fenced block within the first four lines — the shape a status"
|
|
|
|
|
say " header is written in. Only that shape was checked: the field"
|
|
|
|
|
say " names (Status / Owner / Last reviewed / Governs / Review"
|
|
|
|
|
say " trigger) were not read, so a passing document is not a"
|
|
|
|
|
say " conformant one. Every project scaffolded from this template"
|
|
|
|
|
say " inherits whatever is there. Fix it in the template."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Counted only among files this run created, and silent when there are none —
|
|
|
|
|
# an "all placeholders filled in" line would be a claim about documents this
|
|
|
|
|
# run never looked at, and about a placeholder style that may simply have
|
|
|
|
|
# changed underneath the pattern above.
|
|
|
|
|
if [ "${#PLACEHOLDER_LIST[@]}" -gt 0 ]; then
|
|
|
|
|
say ""
|
|
|
|
|
say "${#PLACEHOLDER_LIST[@]} file(s) written by this run still carry placeholders. Every status"
|
|
|
|
|
say "header needs a real Owner, a real Last reviewed date and a real Review"
|
|
|
|
|
say "trigger — that last line is what stops a document going quietly stale."
|
|
|
|
|
say "Find them with:"
|
|
|
|
|
say " grep -rnE '$PLACEHOLDER_RE' docs/"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Branding: named, measured, and never invented.
|
|
|
|
|
#
|
|
|
|
|
# The template's own docs/data/img is not a source. When this was written it
|
|
|
|
|
# held one zero-byte logo.webp, and copying that in would satisfy a check that
|
|
|
|
|
# looks for the name while producing a project card with nothing on it. The
|
|
|
|
|
# deeper reason is the one docs/data/README.md gives: a placeholder that looks
|
|
|
|
|
# deliberate outlives the issue that would have replaced it, because nobody
|
|
|
|
|
# files a ticket against an image that appears finished.
|
|
|
|
|
#
|
|
|
|
|
# So the directory is created empty and the three names are reported by what is
|
|
|
|
|
# actually there — including the difference between absent and present-but-
|
|
|
|
|
# empty, which the check itself distinguishes and a file listing does not.
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
img_dir="$TARGET_ABS/docs/data/img"
|
|
|
|
|
|
|
|
|
|
say ""
|
|
|
|
|
say "docs/data/img/ needs this project's own branding. The conformance check"
|
|
|
|
|
say "looks for exactly these three names, and reads webp by magic bytes rather"
|
|
|
|
|
say "than by extension — a renamed PNG does not pass:"
|
|
|
|
|
|
|
|
|
|
if [ -d "$img_dir" ]; then
|
|
|
|
|
for name in "${BRANDING[@]}"; do
|
|
|
|
|
if [ -s "$img_dir/$name" ]; then
|
|
|
|
|
report "present" "docs/data/img/$name"
|
|
|
|
|
elif [ -e "$img_dir/$name" ]; then
|
|
|
|
|
report "EMPTY" "docs/data/img/$name" "0 bytes — fails the magic-byte check"
|
|
|
|
|
else
|
|
|
|
|
report "absent" "docs/data/img/$name"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
else
|
|
|
|
|
# Not "none present". The directory does not exist yet, so nothing about
|
|
|
|
|
# these three files has been measured and no count would be honest.
|
|
|
|
|
say " not checked — $img_dir does not exist yet."
|
|
|
|
|
printf ' %s\n' ${BRANDING[@]+"${BRANDING[@]}"} >&2
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
say ""
|
|
|
|
|
say "Nothing here can draw them. When one is absent, file an issue — title it"
|
|
|
|
|
say "for the asset, label it P2, and end the body with its Verify: line — rather"
|
|
|
|
|
say "than committing a placeholder."
|
|
|
|
|
|
|
|
|
|
say ""
|
|
|
|
|
say "note: no git command was run. Nothing is staged and nothing is committed;"
|
|
|
|
|
say " what belongs in this project's history is your call."
|
|
|
|
|
|
|
|
|
|
# Non-zero when the tree that came out is not the tree that was asked for. The
|
|
|
|
|
# caller — often another script, or an agent — needs that distinction to survive
|
|
|
|
|
# past the last line of output.
|
|
|
|
|
if [ "$SKIPPED" -gt 0 ]; then
|
|
|
|
|
say ""
|
|
|
|
|
die "${SKIPPED} path(s) were skipped, so this project is not fully scaffolded.
|
|
|
|
|
Resolve each one listed above and run this again — re-running is safe,
|
|
|
|
|
because everything already here is kept."
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
exit 0
|