pi-usereq 0.63.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PI-useReq/pi-usereq (0.63.0)
1
+ # PI-useReq/pi-usereq (0.65.0)
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-usereq",
3
- "version": "0.63.0",
3
+ "version": "0.65.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Ogekuri/PI-useReq.git"
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env bash
2
+ # @file scripts/pi-cli-update-src-docs.sh
3
+ # @brief Updates read-only pi CLI reference sources and docs from the latest pi CLI release.
4
+ # @details Clones the official pi CLI repository (https://github.com/earendil-works/pi),
5
+ # resolves the newest release tag reachable from the upstream default branch, and
6
+ # performs a full one-way synchronization of the upstream repository root into the
7
+ # local read-only `pi.dev-src/pi` target. Every dot-prefixed upstream entry
8
+ # (`.git`, `.github`, `.gitignore`, and any other file or directory whose name
9
+ # starts with `.`) is excluded at every tree level. Full synchronization
10
+ # overwrites every present file and removes every file that no longer exists
11
+ # upstream by replacing each target directory with a freshly staged copy.
12
+ # Upstream file access and modification timestamps are preserved through `cp -p`.
13
+ # After the source synchronization completes, three documentation targets are
14
+ # refreshed from the synchronized sources: `docs/pi.dev/coding-agent-docs`
15
+ # from `pi.dev-src/pi/packages/coding-agent/docs`,
16
+ # `docs/pi.dev/coding-agent-examples` from
17
+ # `pi.dev-src/pi/packages/coding-agent/examples`, and `docs/pi.dev/durable-docs`
18
+ # from `pi.dev-src/pi/packages/durable/docs`. Existing target files whose
19
+ # bytes differ from the upstream candidate only in carriage-return
20
+ # line-ending characters are preserved with their committed bytes and
21
+ # timestamps, so repeated updates against an unchanged upstream release
22
+ # leave the git working tree clean. Before any modification, the
23
+ # updater prints the resolved latest version and the stored versions read from
24
+ # `docs/pi.dev/pi-cli-version.txt` and `pi.dev-src/pi-cli-version.txt`
25
+ # (missing or empty files report `unknown`), then requires an explicit `Y`
26
+ # confirmation; any other input aborts without modifications. On success, the
27
+ # resolved version is written into both version marker files. Runtime is
28
+ # dominated by one git clone plus one tag checkout and per-file copies.
29
+ # Side effects include temporary-directory creation, git subprocesses, and
30
+ # replacement of the four target directories plus the two version marker
31
+ # files. No other repository content is modified.
32
+ # @satisfies DES-023, REQ-385, REQ-386, REQ-387, REQ-388, REQ-389, REQ-390, REQ-391, REQ-392, REQ-393, REQ-394, REQ-395
33
+
34
+ set -euo pipefail
35
+
36
+ PI_REPO_URL_PRIMARY="https://github.com/earendil-works/pi.git"
37
+ PI_REPO_URL_FALLBACK="https://github.com/earendil-works/pi"
38
+
39
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
40
+ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
41
+
42
+ ## @brief Stores the resolved read-only pi CLI source synchronization target.
43
+ ## @details The updater replaces the whole content of `${REPO_ROOT}/${SRC_TARGET_REL}`
44
+ ## with the non-dot upstream repository tree. The constant is immutable at
45
+ ## runtime and costs O(1) per access.
46
+ SRC_TARGET_REL="pi.dev-src/pi"
47
+
48
+ ## @brief Stores the ordered `source-relative:target-relative` documentation synchronization pairs.
49
+ ## @details Each entry maps one directory inside the synchronized `${SRC_TARGET_REL}`
50
+ ## tree to one `docs/pi.dev` target directory synchronized in declaration order
51
+ ## after the source synchronization completes. The constant is immutable at
52
+ ## runtime and costs O(1) per element access.
53
+ DOC_SYNC_SPECS=(
54
+ "pi.dev-src/pi/packages/coding-agent/docs:docs/pi.dev/coding-agent-docs"
55
+ "pi.dev-src/pi/packages/coding-agent/examples:docs/pi.dev/coding-agent-examples"
56
+ "pi.dev-src/pi/packages/durable/docs:docs/pi.dev/durable-docs"
57
+ )
58
+
59
+ ## @brief Stores the documentation version marker path relative to the repository root.
60
+ ## @details Read before the update and overwritten with the resolved version after
61
+ ## successful synchronization. The constant is immutable at runtime.
62
+ DOC_VERSION_REL="docs/pi.dev/pi-cli-version.txt"
63
+
64
+ ## @brief Stores the source version marker path relative to the repository root.
65
+ ## @details Read before the update and overwritten with the resolved version after
66
+ ## successful synchronization. The constant is immutable at runtime.
67
+ SRC_VERSION_REL="pi.dev-src/pi-cli-version.txt"
68
+
69
+ WORK_DIR=""
70
+ STAGING_DIR=""
71
+
72
+ ## @brief Writes one progress message to stdout.
73
+ ## @details Emits the supplied message fragments joined by single spaces followed
74
+ ## by one newline. Runtime is O(n) in message length. Side effect: writes to
75
+ ## `stdout`.
76
+ ## @param[in] ... {string} Progress message fragments.
77
+ ## @return {void} No return value.
78
+ log() {
79
+ printf '%s\n' "$*"
80
+ }
81
+
82
+ ## @brief Aborts the updater with one deterministic diagnostic.
83
+ ## @details Writes the supplied diagnostic prefixed with `ERROR: ` to `stderr`
84
+ ## and terminates the process with exit code `1`. Runtime is O(n) in message
85
+ ## length. Side effects include `stderr` output and process exit; the `EXIT`
86
+ ## trap still removes updater temporary directories.
87
+ ## @param[in] message {string} Failure diagnostic.
88
+ ## @return {never} Never returns.
89
+ fail() {
90
+ printf 'ERROR: %s\n' "$*" >&2
91
+ exit 1
92
+ }
93
+
94
+ ## @brief Removes updater temporary directories.
95
+ ## @details Deletes the cloned upstream working tree and any pending staging
96
+ ## directory registered in `WORK_DIR` plus `STAGING_DIR`. Invoked through the
97
+ ## `EXIT` trap on every exit path. Runtime is O(1) plus filesystem removal
98
+ ## cost. Side effects include directory removal.
99
+ ## @return {void} No return value.
100
+ cleanup() {
101
+ if [ -n "${STAGING_DIR}" ]; then
102
+ rm -rf "${STAGING_DIR}"
103
+ fi
104
+ if [ -n "${WORK_DIR}" ]; then
105
+ rm -rf "${WORK_DIR}"
106
+ fi
107
+ }
108
+
109
+ ## @brief Verifies that one required external tool is available.
110
+ ## @details Probes the executable search path for the requested tool name and
111
+ ## aborts through `fail(...)` when the probe fails. Runtime is O(1) plus one
112
+ ## `PATH` lookup. No external state is mutated.
113
+ ## @param[in] tool {string} Executable name to probe.
114
+ ## @return {void} No return value.
115
+ require_tool() {
116
+ command -v "$1" >/dev/null 2>&1 || fail "Required tool not found: $1"
117
+ }
118
+
119
+ ## @brief Reads one stored pi CLI version marker file.
120
+ ## @details Returns the first line of the marker file with all surrounding
121
+ ## whitespace removed when the file exists and is non-empty; otherwise
122
+ ## returns the literal `unknown` so missing or empty version markers are
123
+ ## handled deterministically. Runtime is O(n) in file size. Side effects are
124
+ ## limited to filesystem reads.
125
+ ## @param[in] marker_path {string} Absolute version marker file path.
126
+ ## @return {string} Trimmed stored version or `unknown` on `stdout`.
127
+ read_version_file() {
128
+ local marker_path="$1"
129
+ local version=""
130
+ if [ -f "${marker_path}" ] && [ -s "${marker_path}" ]; then
131
+ version="$(head -n 1 "${marker_path}" | tr -d '[:space:]' || true)"
132
+ fi
133
+ if [ -z "${version}" ]; then
134
+ version="unknown"
135
+ fi
136
+ printf '%s\n' "${version}"
137
+ }
138
+
139
+ ## @brief Clones the upstream pi CLI repository default branch into one target directory.
140
+ ## @details Attempts a quiet, no-checkout clone from `${PI_REPO_URL_PRIMARY}`,
141
+ ## removes the partial destination on failure, and retries once with
142
+ ## `${PI_REPO_URL_FALLBACK}`. Prints the resolved upstream URL on `stdout` so
143
+ ## callers can capture provenance. Runtime is dominated by git subprocess
144
+ ## execution. Side effects include destination-directory creation and removal.
145
+ ## @param[in] destination {string} Target directory path for the clone.
146
+ ## @return {string} Resolved upstream repository URL on stdout.
147
+ ## @throws Exits through `fail(...)` when both upstream URLs are unreachable.
148
+ clone_upstream() {
149
+ local destination="$1"
150
+ rm -rf "${destination}"
151
+ if git clone --quiet --no-checkout "${PI_REPO_URL_PRIMARY}" "${destination}" 2>/dev/null; then
152
+ printf '%s\n' "${PI_REPO_URL_PRIMARY}"
153
+ return 0
154
+ fi
155
+ log "Primary clone failed, retrying with the fallback repository URL ..."
156
+ rm -rf "${destination}"
157
+ if git clone --quiet --no-checkout "${PI_REPO_URL_FALLBACK}" "${destination}" 2>/dev/null; then
158
+ printf '%s\n' "${PI_REPO_URL_FALLBACK}"
159
+ return 0
160
+ fi
161
+ fail "Unable to clone pi CLI from ${PI_REPO_URL_PRIMARY} or ${PI_REPO_URL_FALLBACK}."
162
+ }
163
+
164
+ ## @brief Resolves the latest released pi CLI tag reachable from the default branch.
165
+ ## @details Prefers the default branch reported by `refs/remotes/origin/HEAD` and
166
+ ## falls back to `origin/master` then `origin/main` when the symbolic ref is
167
+ ## absent. Selects the newest version-sorted tag matching `v[0-9]*` that is
168
+ ## merged into the resolved default ref, which represents the latest release
169
+ ## reachable from that branch. Prints the resolved tag on `stdout`. Runtime is
170
+ ## dominated by git ref enumeration. No external state is mutated.
171
+ ## @param[in] repository {string} Cloned upstream repository path.
172
+ ## @return {string} Latest release tag name on stdout.
173
+ ## @throws Exits through `fail(...)` when no release tag is resolvable.
174
+ resolve_release_tag() {
175
+ local repository="$1"
176
+ local default_ref=""
177
+ local release_tag=""
178
+
179
+ default_ref="$(git -C "${repository}" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true)"
180
+ if [ -z "${default_ref}" ]; then
181
+ if git -C "${repository}" rev-parse --verify --quiet refs/remotes/origin/master >/dev/null 2>&1; then
182
+ default_ref="origin/master"
183
+ else
184
+ default_ref="origin/main"
185
+ fi
186
+ fi
187
+
188
+ release_tag="$(git -C "${repository}" tag --list 'v[0-9]*' --sort=-v:refname --merged "${default_ref}" 2>/dev/null | head -n 1 || true)"
189
+ if [ -z "${release_tag}" ]; then
190
+ fail "Unable to resolve the latest pi CLI release tag from ${default_ref}."
191
+ fi
192
+ printf '%s\n' "${release_tag}"
193
+ }
194
+
195
+ ## @brief Decides whether one staged upstream file must replace the existing target file.
196
+ ## @details Returns success (replace) when the target file does not exist or when
197
+ ## its bytes differ from the staged upstream file beyond line-ending
198
+ ## characters. Returns failure (preserve) when both files are byte-equal or
199
+ ## when both files are equal after removing every carriage-return byte, so
200
+ ## committed reference bytes and timestamps stay stable when upstream
201
+ ## releases change only line endings. Runtime is O(n) in compared file size
202
+ ## plus at most three subprocesses. Side effects are limited to subprocess
203
+ ## execution and stdin pipes; no file is mutated.
204
+ ## @param[in] source_file {string} Absolute staged upstream candidate file path.
205
+ ## @param[in] target_file {string} Absolute existing target file path.
206
+ ## @return {integer} `0` when the staged file must replace the target; `1` when
207
+ ## the existing target bytes must be preserved.
208
+ staged_copy_replaces_existing() {
209
+ local source_file="$1"
210
+ local target_file="$2"
211
+ local source_normalized=""
212
+ local target_normalized=""
213
+
214
+ if [ ! -f "${target_file}" ]; then
215
+ return 0
216
+ fi
217
+ if cmp -s -- "${source_file}" "${target_file}"; then
218
+ return 0
219
+ fi
220
+ source_normalized="$(tr -d '\r' < "${source_file}" | git hash-object --stdin)" || return 0
221
+ target_normalized="$(tr -d '\r' < "${target_file}" | git hash-object --stdin)" || return 0
222
+ [ "${source_normalized}" != "${target_normalized}" ]
223
+ }
224
+
225
+ ## @brief Fully synchronizes one upstream directory into one target directory.
226
+ ## @details Stages every upstream entry below `source_dir` into a fresh sibling
227
+ ## staging directory using timestamp-preserving copies, excluding every
228
+ ## dot-prefixed entry and its whole subtree at every tree level, then
229
+ ## replaces `target_dir` with the staged tree through a backup-and-swap
230
+ ## sequence so files removed upstream disappear and existing files are
231
+ ## overwritten with an exact file-set match. Existing target files
232
+ ## whose bytes differ from the staged upstream file only in carriage-return
233
+ ## line-ending characters are preserved through
234
+ ## `staged_copy_replaces_existing(...)` so unchanged upstream releases do
235
+ ## not dirty the git working tree. Runtime is dominated by per-file copy
236
+ ## cost. Side effects include staging plus backup directory creation and
237
+ ## removal, target-directory replacement, and one progress line on `stdout`.
238
+ ## @param[in] source_dir {string} Absolute upstream source directory path.
239
+ ## @param[in] target_dir {string} Absolute read-only target directory path.
240
+ ## @return {void} No return value.
241
+ ## @throws Exits through `fail(...)` when `source_dir` is not a directory.
242
+ sync_directory() {
243
+ local source_dir="$1"
244
+ local target_dir="$2"
245
+
246
+ [ -d "${source_dir}" ] || fail "Upstream directory not found: ${source_dir}"
247
+
248
+ STAGING_DIR="${target_dir}.staging.$$"
249
+ rm -rf "${STAGING_DIR}"
250
+ mkdir -p "${STAGING_DIR}"
251
+
252
+ local entry destination parent_dir copied=0
253
+ while IFS= read -r -d '' entry; do
254
+ entry="${entry#./}"
255
+ [ -n "${entry}" ] || continue
256
+ destination="${STAGING_DIR}/${entry}"
257
+ if [ -d "${source_dir}/${entry}" ]; then
258
+ mkdir -p "${destination}"
259
+ else
260
+ parent_dir="$(dirname "${destination}")"
261
+ mkdir -p "${parent_dir}"
262
+ if staged_copy_replaces_existing "${source_dir}/${entry}" "${target_dir}/${entry}"; then
263
+ cp -p "${source_dir}/${entry}" "${destination}"
264
+ else
265
+ cp -p "${target_dir}/${entry}" "${destination}"
266
+ fi
267
+ fi
268
+ copied=$((copied + 1))
269
+ done < <(cd "${source_dir}" && find . -mindepth 1 \( -name '.*' -prune \) -o -print0)
270
+
271
+ local backup_dir="${target_dir}.backup.$$"
272
+ rm -rf "${backup_dir}"
273
+ if [ -d "${target_dir}" ]; then
274
+ mv "${target_dir}" "${backup_dir}"
275
+ fi
276
+ mv "${STAGING_DIR}" "${target_dir}"
277
+ STAGING_DIR=""
278
+ rm -rf "${backup_dir}"
279
+
280
+ log "Synchronized ${copied} file(s) into ${target_dir#"${REPO_ROOT}"/}"
281
+ }
282
+
283
+ ## @brief Requests explicit user confirmation before any modification.
284
+ ## @details Prints the versions plus one English confirmation sentence, reads one
285
+ ## line from `stdin`, and returns success only when the confirmation input is
286
+ ## exactly `Y`; any other input, including end-of-stream input, returns
287
+ ## failure so callers abort without modifications. Runtime is O(1) plus one
288
+ ## user interaction. Side effects include `stdout` and `stderr` writes.
289
+ ## @param[in] latest_version {string} Resolved upstream release version.
290
+ ## @param[in] doc_version {string} Stored version of `${DOC_VERSION_REL}`.
291
+ ## @param[in] src_version {string} Stored version of `${SRC_VERSION_REL}`.
292
+ ## @return {integer} `0` when the user confirmed with exactly `Y`; `1` otherwise.
293
+ confirm_update() {
294
+ local latest_version="$1"
295
+ local doc_version="$2"
296
+ local src_version="$3"
297
+ local answer=""
298
+
299
+ log "Latest pi CLI release : ${latest_version}"
300
+ log "Stored version (${DOC_VERSION_REL}): ${doc_version}"
301
+ log "Stored version (${SRC_VERSION_REL}): ${src_version}"
302
+ log "This update replaces the read-only reference sources in ${SRC_TARGET_REL} and the documentation in docs/pi.dev with the content of the latest pi CLI release."
303
+ printf 'Proceed with the update? [Y/n] '
304
+ read -r answer || answer=""
305
+ [ "${answer}" = "Y" ]
306
+ }
307
+
308
+ ## @brief Executes one full pi CLI reference update run.
309
+ ## @details Validates required tools, allocates the temporary upstream working
310
+ ## tree, clones the pi CLI default branch, resolves and checks out the newest
311
+ ## release tag, prints the resolved plus stored versions, requires an explicit
312
+ ## `Y` confirmation, synchronizes the upstream repository root into the
313
+ ## read-only source target excluding every dot-prefixed entry, synchronizes
314
+ ## every ordered documentation pair declared in `DOC_SYNC_SPECS`, and writes
315
+ ## the resolved version into both version marker files. Runtime is dominated
316
+ ## by the upstream clone plus the four directory synchronizations. Side
317
+ ## effects include temporary-directory creation and removal, git subprocesses,
318
+ ## and replacement of the four target directories plus the two version marker
319
+ ## files.
320
+ ## @return {void} No return value.
321
+ ## @throws Exits through `fail(...)` when any required tool, clone, tag
322
+ ## resolution, or synchronization step fails.
323
+ main() {
324
+ require_tool git
325
+ require_tool find
326
+ require_tool mkdir
327
+ require_tool cp
328
+ require_tool cmp
329
+
330
+ local doc_version src_version
331
+ doc_version="$(read_version_file "${REPO_ROOT}/${DOC_VERSION_REL}")"
332
+ src_version="$(read_version_file "${REPO_ROOT}/${SRC_VERSION_REL}")"
333
+
334
+ WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pi-cli-update-src-docs.XXXXXX")" \
335
+ || fail "Unable to create the temporary working directory."
336
+ local repository="${WORK_DIR}/pi"
337
+
338
+ local upstream_url
339
+ upstream_url="$(clone_upstream "${repository}")"
340
+ log "Upstream repository: ${upstream_url}"
341
+
342
+ local release_tag
343
+ release_tag="$(resolve_release_tag "${repository}")"
344
+
345
+ if ! confirm_update "${release_tag}" "${doc_version}" "${src_version}"; then
346
+ log "Update aborted; no repository content was modified."
347
+ return 0
348
+ fi
349
+
350
+ git -C "${repository}" checkout --quiet --detach "${release_tag}" \
351
+ || fail "Unable to check out ${release_tag}."
352
+ log "Synchronizing from release: ${release_tag}"
353
+
354
+ sync_directory "${repository}" "${REPO_ROOT}/${SRC_TARGET_REL}"
355
+
356
+ local spec source_rel target_rel
357
+ for spec in "${DOC_SYNC_SPECS[@]}"; do
358
+ source_rel="${spec%%:*}"
359
+ target_rel="${spec#*:}"
360
+ sync_directory "${REPO_ROOT}/${source_rel}" "${REPO_ROOT}/${target_rel}"
361
+ done
362
+
363
+ printf '%s\n' "${release_tag}" > "${REPO_ROOT}/${DOC_VERSION_REL}"
364
+ printf '%s\n' "${release_tag}" > "${REPO_ROOT}/${SRC_VERSION_REL}"
365
+
366
+ log "pi CLI reference sources and documentation updated to ${release_tag}."
367
+ }
368
+
369
+ trap cleanup EXIT
370
+ main "$@"