@cratis/pi 7.100.0 → 7.102.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/package/corpus/agents/backend-developer.md +1 -0
- package/package/corpus/agents/browser-operator.md +44 -0
- package/package/corpus/agents/code-reviewer.md +4 -0
- package/package/corpus/agents/coordinator.md +1 -0
- package/package/corpus/agents/frontend-developer.md +1 -0
- package/package/corpus/agents/orchestrator.md +1 -0
- package/package/corpus/agents/performance-reviewer.md +4 -0
- package/package/corpus/agents/planner.md +1 -0
- package/package/corpus/agents/repository-investigation-reviewer.md +4 -0
- package/package/corpus/agents/repository-investigator.md +1 -0
- package/package/corpus/agents/screenplay-modeler.md +1 -0
- package/package/corpus/agents/screenplay-renderer.md +1 -0
- package/package/corpus/agents/screenplay-reviewer.md +4 -0
- package/package/corpus/agents/security-reviewer.md +4 -0
- package/package/corpus/agents/slice-implementer.md +1 -0
- package/package/corpus/agents/spec-writer.md +1 -0
- package/package/corpus/harnesses/copilot/instructions/browser.instructions.md +46 -0
- package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +17 -0
- package/package/corpus/harnesses/copilot/instructions/local-work-artifacts.instructions.md +3 -0
- package/package/corpus/harnesses/copilot/instructions/review-reports.instructions.md +11 -0
- package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +35 -0
- package/package/corpus/harnesses/pi/agents/backend-developer.md +1 -0
- package/package/corpus/harnesses/pi/agents/browser-operator.md +41 -0
- package/package/corpus/harnesses/pi/agents/code-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/coordinator.md +1 -0
- package/package/corpus/harnesses/pi/agents/frontend-developer.md +1 -0
- package/package/corpus/harnesses/pi/agents/orchestrator.md +1 -0
- package/package/corpus/harnesses/pi/agents/performance-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/planner.md +1 -0
- package/package/corpus/harnesses/pi/agents/repository-investigation-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/repository-investigator.md +1 -0
- package/package/corpus/harnesses/pi/agents/screenplay-modeler.md +1 -0
- package/package/corpus/harnesses/pi/agents/screenplay-renderer.md +1 -0
- package/package/corpus/harnesses/pi/agents/screenplay-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/security-reviewer.md +4 -0
- package/package/corpus/harnesses/pi/agents/slice-implementer.md +1 -0
- package/package/corpus/harnesses/pi/agents/spec-writer.md +1 -0
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +19 -6
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/quality-gate.ts +10 -1
- package/package/corpus/hooks/README.md +8 -0
- package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +458 -0
- package/package/corpus/hooks/settings.template.json +4 -0
- package/package/corpus/rules/browser.md +45 -0
- package/package/corpus/rules/general.md +2 -0
- package/package/corpus/rules/github-actions.md +17 -0
- package/package/corpus/rules/local-work-artifacts.md +3 -0
- package/package/corpus/rules/review-reports.md +11 -0
- package/package/corpus/rules/secrets.md +34 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/LICENSE +2 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/SKILL.md +201 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/build-once-and-shard.md +158 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/caching.md +86 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/flaky-and-hung-jobs.md +70 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/publish-pipelines.md +67 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/required-checks-and-path-filters.md +117 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/runner-sizing-and-images.md +65 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/security-and-pinning.md +73 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/sources.md +33 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/references/test-setup-cost.md +82 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/ci-timings.sh +356 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1003.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1007.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1010.json +79 -0
- package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/runs.json +102 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +244 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/docker-images.md +86 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +87 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/go-jvm-python-elixir.md +83 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +78 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/other-environments.md +60 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +97 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +778 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/npm-restore-check.cjs +109 -0
- package/package/profile-catalog.json +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Copyright (c) Cratis. All rights reserved.
|
|
3
|
+
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
usage() {
|
|
7
|
+
cat <<'HELP'
|
|
8
|
+
Usage: cratis-local-packages.sh [--dry-run] COMMAND [ARGS]
|
|
9
|
+
Portable local package verification for macOS/Linux (Bash 3.2+). Windows: use WSL.
|
|
10
|
+
Exit codes: 0 success, 1 findings/failed operation, 2 cannot run/invalid input.
|
|
11
|
+
|
|
12
|
+
Commands (COMMAND --help shows these instructions too):
|
|
13
|
+
stamp PRODUCER
|
|
14
|
+
Next patch of the highest reachable vX.Y.Z tag, UTC minute and HEAD sha7.
|
|
15
|
+
pack-nuget PRODUCER STAMP PROJECT-OR-SLN...
|
|
16
|
+
Release-pack each target into the NuGet feed; requires dotnet.
|
|
17
|
+
pack-npm PRODUCER STAMP
|
|
18
|
+
Requires node and Yarn 4, and a completed repository build. Temporarily
|
|
19
|
+
version non-private workspaces, pack with %s-%v.tgz (Yarn slugifies scoped
|
|
20
|
+
names), and restore tracked manifests even on failure. Pack lifecycle hooks
|
|
21
|
+
are Yarn's own behavior; this helper does not run a separate build.
|
|
22
|
+
use-nuget CONSUMER STAMP [PACKAGE-ID...]
|
|
23
|
+
Create a CPM overlay importing the repository's own Directory.Packages.props.
|
|
24
|
+
Defaults to every stamped package ID in the feed. Prints properties and sourceable
|
|
25
|
+
env. With source mapping, also prints the required restore --configfile.
|
|
26
|
+
Non-CPM: manually use an untracked project copy with explicit PackageReference
|
|
27
|
+
versions and dotnet restore --source FEED/nuget; do not edit shipped versions.
|
|
28
|
+
use-npm CONSUMER STAMP
|
|
29
|
+
Requires node and tar. Back up package.json, yarn.lock (including existing edits),
|
|
30
|
+
and git diff; add file-tarball resolutions for every stamped feed package.
|
|
31
|
+
Run yarn install afterward. Only one active npm overlay per session.
|
|
32
|
+
restore CONSUMER
|
|
33
|
+
Restore npm backups when only overlay resolutions and their local lockfile
|
|
34
|
+
entries changed (including yarn install). Refuse unrelated changes and retain
|
|
35
|
+
backups for review. Remove CPM/env overlays
|
|
36
|
+
and run check. No git checkout is used on consumer files.
|
|
37
|
+
check REPO [--staged]
|
|
38
|
+
Scan added/changed tracked lines (staged-only if requested), plus all contents
|
|
39
|
+
of untracked, non-ignored files. Reports path:line:pattern and file counts.
|
|
40
|
+
clean [CONSUMER] [--stamp STAMP | --all] [--apply]
|
|
41
|
+
Default: list all local feed/cache/image/session subjects, without deleting.
|
|
42
|
+
--apply deletes listed subjects (docker rmi, never force). Refuses more than
|
|
43
|
+
CRATIS_LOCAL_CLEAN_MAX subjects (default 500), or unresolved npm backups.
|
|
44
|
+
Known sessions are registered in FEED/sessions; an optional consumer also
|
|
45
|
+
discovers its per-worktree session. Docker probes have a 10-second deadline.
|
|
46
|
+
--self-test
|
|
47
|
+
Isolated scratch HOME tests; npm uses inherited PATH to discover node.
|
|
48
|
+
Other tests use system PATH. Reports passed and skipped counts.
|
|
49
|
+
|
|
50
|
+
Configuration: source ${XDG_CONFIG_HOME:-$HOME/.config}/cratis/local-packages.env
|
|
51
|
+
if present (trusted shell code), then use these defaults for unset variables:
|
|
52
|
+
CRATIS_LOCAL_FEED=${XDG_CACHE_HOME:-$HOME/.cache}/cratis/local-feed
|
|
53
|
+
CRATIS_LOCAL_SESSION_DIR=<absolute-git-dir>/cratis-local (per checkout)
|
|
54
|
+
NUGET_PACKAGES=$HOME/.nuget/packages
|
|
55
|
+
CRATIS_LOCAL_CLEAN_MAX=500
|
|
56
|
+
CRATIS_LOCAL_RUN=optional executable wrapper, invoked for each heavy step as:
|
|
57
|
+
"$CRATIS_LOCAL_RUN" build LABEL -- COMMAND ARGUMENTS...
|
|
58
|
+
Global --dry-run prints mutations instead of executing them; read-only discovery
|
|
59
|
+
still runs. Use a unique stamp once per producer build. Never publish local builds.
|
|
60
|
+
HELP
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
die() { printf 'ERROR: %s\n' "$*" >&2; exit 2; }
|
|
64
|
+
failed() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
|
65
|
+
need() { command -v "$1" >/dev/null 2>&1 || die "required executable not found: $1"; }
|
|
66
|
+
quote_command() { printf ' +'; printf ' %q' "$@"; printf '\n'; }
|
|
67
|
+
DRY=0
|
|
68
|
+
ARGS=()
|
|
69
|
+
for argument in "$@"; do
|
|
70
|
+
case "$argument" in --dry-run) DRY=1 ;; *) ARGS+=("$argument") ;; esac
|
|
71
|
+
done
|
|
72
|
+
set -- ${ARGS[@]+"${ARGS[@]}"}
|
|
73
|
+
case " ${*:-} " in *' --help '*|*' -h '*) usage; exit 0 ;; esac
|
|
74
|
+
[ $# -gt 0 ] || { usage; exit 2; }
|
|
75
|
+
SELF=$(cd "$(dirname "$0")" && pwd)/$(basename "$0")
|
|
76
|
+
# Self-test dispatch precedes configuration so the real HOME/config cannot affect it.
|
|
77
|
+
MODE=$1
|
|
78
|
+
shift
|
|
79
|
+
if [ "$MODE" != --self-test ]; then
|
|
80
|
+
CONFIG=${XDG_CONFIG_HOME:-$HOME/.config}/cratis/local-packages.env
|
|
81
|
+
if [ -f "$CONFIG" ]; then
|
|
82
|
+
# shellcheck source=/dev/null
|
|
83
|
+
source "$CONFIG"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
FEED=${CRATIS_LOCAL_FEED:-${XDG_CACHE_HOME:-$HOME/.cache}/cratis/local-feed}
|
|
87
|
+
NUGET_CACHE=${NUGET_PACKAGES:-$HOME/.nuget/packages}
|
|
88
|
+
case "$FEED" in /*) ;; *) FEED=$PWD/$FEED ;; esac
|
|
89
|
+
case "$NUGET_CACHE" in /*) ;; *) NUGET_CACHE=$PWD/$NUGET_CACHE ;; esac
|
|
90
|
+
TMP=''
|
|
91
|
+
PACK_REPO=''
|
|
92
|
+
PACK_MANIFESTS=()
|
|
93
|
+
cleanup() {
|
|
94
|
+
local status=$?
|
|
95
|
+
trap - EXIT
|
|
96
|
+
if [ ${#PACK_MANIFESTS[@]} -gt 0 ]; then
|
|
97
|
+
if ! git -C "$PACK_REPO" checkout -- "${PACK_MANIFESTS[@]}"; then
|
|
98
|
+
printf 'ERROR: producer manifest restoration failed: %s\n' "$PACK_REPO" >&2
|
|
99
|
+
status=1
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
if [ -n "$TMP" ]; then rm -rf -- "$TMP"; fi
|
|
103
|
+
exit "$status"
|
|
104
|
+
}
|
|
105
|
+
trap cleanup EXIT
|
|
106
|
+
trap 'exit 1' HUP INT TERM
|
|
107
|
+
scratch() { if [ -z "$TMP" ]; then TMP=$(mktemp -d "${TMPDIR:-/tmp}/cratis-local.XXXXXXXX"); fi; }
|
|
108
|
+
mutate() { if [ "$DRY" -eq 1 ]; then quote_command "$@"; else "$@"; fi; }
|
|
109
|
+
heavy() {
|
|
110
|
+
local kind=$1 label=$2
|
|
111
|
+
shift 2
|
|
112
|
+
if [ -n "${CRATIS_LOCAL_RUN:-}" ]; then
|
|
113
|
+
mutate "$CRATIS_LOCAL_RUN" "$kind" "$label" -- "$@"
|
|
114
|
+
else
|
|
115
|
+
mutate "$@"
|
|
116
|
+
fi
|
|
117
|
+
}
|
|
118
|
+
repo_root() { git -C "$1" rev-parse --show-toplevel 2>/dev/null || die "not a Git repository: $1"; }
|
|
119
|
+
valid_stamp() {
|
|
120
|
+
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+-local\.[0-9]{12}\.g[0-9a-f]{7}$ ]] || die "invalid local stamp: $1"
|
|
121
|
+
}
|
|
122
|
+
session_path() {
|
|
123
|
+
local path git_dir
|
|
124
|
+
git_dir=$(git -C "$1" rev-parse --absolute-git-dir)
|
|
125
|
+
path=${CRATIS_LOCAL_SESSION_DIR:-$git_dir/cratis-local}
|
|
126
|
+
case "$path" in /*) ;; *) path=$PWD/$path ;; esac
|
|
127
|
+
[ "$path" != / ] && [ "$path" != "$1" ] && [ ! -L "$path" ] || die "unsafe session directory: $path"
|
|
128
|
+
# Git's own directory is never tracked, but check-ignore does not classify it
|
|
129
|
+
# as ignored. Other in-checkout overrides must still be explicitly ignored.
|
|
130
|
+
case "$path/" in
|
|
131
|
+
"$git_dir/"*) ;;
|
|
132
|
+
"$1/"*) git -C "$1" check-ignore -q "$path/" || die "session override must be outside tracked files or ignored" ;;
|
|
133
|
+
esac
|
|
134
|
+
printf '%s\n' "$path"
|
|
135
|
+
}
|
|
136
|
+
validate_session() {
|
|
137
|
+
local session=$1 entry
|
|
138
|
+
[ ! -L "$session" ] || die "symlink session directory: $session"
|
|
139
|
+
[ -d "$session" ] || return 0
|
|
140
|
+
for entry in "$session"/* "$session"/.[!.]* "$session"/..?*; do
|
|
141
|
+
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
142
|
+
[ ! -L "$entry" ] || die "symlink in session: $entry"
|
|
143
|
+
case "${entry##*/}" in
|
|
144
|
+
repo|stamp|env|Directory.Packages.props|nuget.config) [ -f "$entry" ] || die "unexpected session entry: $entry" ;;
|
|
145
|
+
npm-backup) [ -d "$entry" ] || die "unexpected session entry: $entry" ;;
|
|
146
|
+
*) die "unrecognized file in session (preserved): $entry" ;;
|
|
147
|
+
esac
|
|
148
|
+
done
|
|
149
|
+
}
|
|
150
|
+
register_session() {
|
|
151
|
+
local repo=$1 session=$2 stamp=$3
|
|
152
|
+
validate_session "$session"
|
|
153
|
+
if [ -f "$session/repo" ] && [ "$(cat "$session/repo")" != "$repo" ]; then die 'session belongs to another checkout'; fi
|
|
154
|
+
mkdir -p "$session" "$FEED"
|
|
155
|
+
printf '%s\n' "$repo" > "$session/repo"
|
|
156
|
+
printf '%s\n' "$stamp" > "$session/stamp"
|
|
157
|
+
if [ ! -f "$FEED/sessions" ] || ! grep -Fxq -- "$session" "$FEED/sessions"; then
|
|
158
|
+
printf '%s\n' "$session" >> "$FEED/sessions"
|
|
159
|
+
fi
|
|
160
|
+
}
|
|
161
|
+
xml_escape() { printf '%s' "$1" | sed -e 's/\&/\&/g' -e 's/</\</g' -e 's/>/\>/g' -e 's/"/\"/g' -e "s/'/\'/g"; }
|
|
162
|
+
|
|
163
|
+
stamp() {
|
|
164
|
+
[ $# -eq 1 ] || die 'stamp requires a producer repository'
|
|
165
|
+
local repo tag major minor patch sha
|
|
166
|
+
repo=$(repo_root "$1")
|
|
167
|
+
tag=$(git -C "$repo" tag --merged HEAD | awk '/^v[0-9]+\.[0-9]+\.[0-9]+$/' | sort -t. -k1.2,1n -k2,2n -k3,3n | tail -n 1)
|
|
168
|
+
if [ -n "$tag" ]; then
|
|
169
|
+
IFS=. read -r major minor patch <<< "${tag#v}"
|
|
170
|
+
tag="$major.$minor.$((10#$patch + 1))"
|
|
171
|
+
else
|
|
172
|
+
printf 'WARNING: no reachable vX.Y.Z tag; using 0.1.0\n' >&2
|
|
173
|
+
tag=0.1.0
|
|
174
|
+
fi
|
|
175
|
+
sha=$(git -C "$repo" rev-parse HEAD) || die 'producer has no HEAD commit'
|
|
176
|
+
printf '%s-local.%s.g%.7s\n' "$tag" "$(date -u +%Y%m%d%H%M)" "$sha"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
pack_nuget() {
|
|
180
|
+
[ $# -ge 3 ] || die 'pack-nuget requires producer, stamp and targets'
|
|
181
|
+
local repo version target path count=0
|
|
182
|
+
repo=$(repo_root "$1"); version=$2; shift 2
|
|
183
|
+
valid_stamp "$version"; need dotnet
|
|
184
|
+
mutate mkdir -p "$FEED/nuget"
|
|
185
|
+
for target in "$@"; do
|
|
186
|
+
(cd "$repo" && heavy build "pack NuGet $target" dotnet pack "$target" -c Release "-p:PackageVersion=$version" "-p:Version=$version" -o "$FEED/nuget")
|
|
187
|
+
done
|
|
188
|
+
[ "$DRY" -eq 0 ] || return 0
|
|
189
|
+
while IFS= read -r -d '' path; do
|
|
190
|
+
printf '%s\n' "$path"; count=$((count + 1))
|
|
191
|
+
done < <(find "$FEED/nuget" -type f -name "*.$version.nupkg" -print0)
|
|
192
|
+
[ "$count" -gt 0 ] || failed 'pack produced no matching nupkgs'
|
|
193
|
+
printf 'produced %s nupkgs\n' "$count"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
pack_npm() {
|
|
197
|
+
[ $# -eq 2 ] || die 'pack-npm requires producer and stamp'
|
|
198
|
+
local repo version path count=0
|
|
199
|
+
repo=$(repo_root "$1"); version=$2
|
|
200
|
+
valid_stamp "$version"; need node; need yarn; scratch
|
|
201
|
+
git -C "$repo" diff --name-only HEAD -- 'package.json' '*/package.json' > "$TMP/modified"
|
|
202
|
+
git -C "$repo" diff --cached --name-only -- 'package.json' '*/package.json' >> "$TMP/modified"
|
|
203
|
+
git -C "$repo" diff --name-only -- 'package.json' '*/package.json' >> "$TMP/modified"
|
|
204
|
+
[ ! -s "$TMP/modified" ] || die 'tracked package.json is already modified; preserve it before packing'
|
|
205
|
+
(cd "$repo" && yarn workspaces list --json) > "$TMP/workspaces"
|
|
206
|
+
node - "$repo" "$TMP/workspaces" > "$TMP/manifests" <<'NODE'
|
|
207
|
+
const fs = require('fs'), path = require('path');
|
|
208
|
+
const [repo, list] = process.argv.slice(2);
|
|
209
|
+
for (const line of fs.readFileSync(list, 'utf8').trim().split('\n')) {
|
|
210
|
+
const workspace = JSON.parse(line);
|
|
211
|
+
const file = path.join(workspace.location, 'package.json');
|
|
212
|
+
if (!JSON.parse(fs.readFileSync(path.join(repo, file), 'utf8')).private)
|
|
213
|
+
process.stdout.write(file + '\0');
|
|
214
|
+
}
|
|
215
|
+
NODE
|
|
216
|
+
PACK_REPO=$repo
|
|
217
|
+
while IFS= read -r -d '' path; do
|
|
218
|
+
git -C "$repo" ls-files --error-unmatch -- "$path" >/dev/null || die "untracked workspace manifest: $path"
|
|
219
|
+
PACK_MANIFESTS+=("$path")
|
|
220
|
+
done < "$TMP/manifests"
|
|
221
|
+
[ ${#PACK_MANIFESTS[@]} -gt 0 ] || die 'no non-private workspaces found'
|
|
222
|
+
if [ "$DRY" -eq 1 ]; then
|
|
223
|
+
quote_command node '<set non-private workspace versions>' "$version" "${PACK_MANIFESTS[@]}"
|
|
224
|
+
PACK_MANIFESTS=()
|
|
225
|
+
else
|
|
226
|
+
node - "$repo" "$version" "${PACK_MANIFESTS[@]}" <<'NODE'
|
|
227
|
+
const fs = require('fs'), path = require('path');
|
|
228
|
+
const [repo, version, ...files] = process.argv.slice(2);
|
|
229
|
+
for (const file of files) {
|
|
230
|
+
const target = path.join(repo, file), json = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
231
|
+
json.version = version;
|
|
232
|
+
fs.writeFileSync(target, JSON.stringify(json, null, 2) + '\n');
|
|
233
|
+
}
|
|
234
|
+
NODE
|
|
235
|
+
fi
|
|
236
|
+
mutate mkdir -p "$FEED/npm"
|
|
237
|
+
(cd "$repo" && heavy build 'pack npm workspaces' yarn workspaces foreach -A --no-private pack --out "$FEED/npm/%s-%v.tgz")
|
|
238
|
+
if [ "$DRY" -eq 1 ]; then quote_command git -C "$repo" checkout -- '<versioned manifests>'; return 0; fi
|
|
239
|
+
git -C "$repo" checkout -- "${PACK_MANIFESTS[@]}"
|
|
240
|
+
PACK_MANIFESTS=()
|
|
241
|
+
while IFS= read -r -d '' path; do
|
|
242
|
+
printf '%s\n' "$path"; count=$((count + 1))
|
|
243
|
+
done < <(find "$FEED/npm" -type f -name "*-$version.tgz" -print0)
|
|
244
|
+
[ "$count" -gt 0 ] || failed 'pack produced no matching npm tarballs'
|
|
245
|
+
printf 'produced %s npm tarballs\n' "$count"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Read Include/Update attributes without assuming one element per line.
|
|
249
|
+
package_ids() {
|
|
250
|
+
awk 'BEGIN { RS="<" } /^PackageVersion[[:space:]]/ {
|
|
251
|
+
gsub(sprintf("%c",39),"\"")
|
|
252
|
+
if (match($0, /(Include|Update)[[:space:]]*=[[:space:]]*"[^"]+"/)) {
|
|
253
|
+
value=substr($0,RSTART,RLENGTH); sub(/^[^"]*"/,"",value); sub(/"$/,"",value); print value
|
|
254
|
+
}
|
|
255
|
+
}' "$1"
|
|
256
|
+
}
|
|
257
|
+
source_mapping() {
|
|
258
|
+
local config=$1 output=$2 id
|
|
259
|
+
shift 2
|
|
260
|
+
scratch
|
|
261
|
+
printf '%s\n' "$@" > "$TMP/ids"
|
|
262
|
+
# Preserve all existing configuration, but remove exact mappings for overridden
|
|
263
|
+
# IDs: NuGet must not select both the remote source and our local exact match.
|
|
264
|
+
awk -v ids="$TMP/ids" -v feed="$(xml_escape "$FEED/nuget")" -v base="$(xml_escape "$(dirname "$config")")" '
|
|
265
|
+
BEGIN { while ((getline id < ids)>0) { selected[tolower(id)]=1; ordered[++count]=id }; RS="<"; ORS="" }
|
|
266
|
+
NR==1 { print; next }
|
|
267
|
+
/^package[[:space:]]/ {
|
|
268
|
+
value=$0; gsub(sprintf("%c",39),"\"",value)
|
|
269
|
+
sub(/^.*pattern[[:space:]]*=[[:space:]]*"/,"",value); sub(/".*$/,"",value)
|
|
270
|
+
if (tolower(value) in selected) next
|
|
271
|
+
}
|
|
272
|
+
/^packageSources[[:space:]]*>/ { sources=1 }
|
|
273
|
+
/^\/packageSources[[:space:]]*>/ { sources=0 }
|
|
274
|
+
sources && /^add[[:space:]]/ {
|
|
275
|
+
# Relative source paths must retain their meaning after relocation.
|
|
276
|
+
value=$0; gsub(sprintf("%c",39),"\"",value)
|
|
277
|
+
if (match(value,/value[[:space:]]*=[[:space:]]*"[^"]*"/)) {
|
|
278
|
+
attribute=substr(value,RSTART,RLENGTH); sub(/^[^"]*"/,"",attribute); sub(/"$/,"",attribute)
|
|
279
|
+
if (attribute !~ /^([A-Za-z][A-Za-z0-9+.-]*:|\/|\\\\|%)/) {
|
|
280
|
+
sub(/value[[:space:]]*=[[:space:]]*"[^"]*"/,"value=\"" base "/" attribute "\"",value)
|
|
281
|
+
$0=value
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/^\/packageSources[[:space:]]*>/ { print "<add key=\"cratis-local\" value=\"" feed "\" />\n" }
|
|
286
|
+
/^\/packageSourceMapping[[:space:]]*>/ {
|
|
287
|
+
print "<packageSource key=\"cratis-local\">\n"
|
|
288
|
+
for (i=1;i<=count;i++) print "<package pattern=\"" ordered[i] "\" />\n"
|
|
289
|
+
print "</packageSource>\n"
|
|
290
|
+
}
|
|
291
|
+
{ print "<" $0 }
|
|
292
|
+
' "$config" > "$output"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
use_nuget() {
|
|
296
|
+
[ $# -ge 2 ] || die 'use-nuget requires consumer and stamp'
|
|
297
|
+
local repo version session props id file config='' props_key sources_key action
|
|
298
|
+
local ids=()
|
|
299
|
+
repo=$(repo_root "$1"); version=$2; shift 2
|
|
300
|
+
valid_stamp "$version"; props=$repo/Directory.Packages.props
|
|
301
|
+
[ -f "$props" ] || die 'no Directory.Packages.props (non-CPM). Manual alternative: untracked project copy, explicit PackageReference versions, dotnet restore --source FEED/nuget'
|
|
302
|
+
session=$(session_path "$repo"); scratch
|
|
303
|
+
package_ids "$props" > "$TMP/consumer-ids"
|
|
304
|
+
if [ $# -gt 0 ]; then
|
|
305
|
+
ids=("$@")
|
|
306
|
+
elif [ -d "$FEED/nuget" ]; then
|
|
307
|
+
while IFS= read -r -d '' file; do
|
|
308
|
+
id=${file##*/}; id=${id%."$version".nupkg}
|
|
309
|
+
ids+=("$id")
|
|
310
|
+
done < <(find "$FEED/nuget" -type f -name "*.$version.nupkg" -print0)
|
|
311
|
+
fi
|
|
312
|
+
[ ${#ids[@]} -gt 0 ] || die 'no matching NuGet package IDs found'
|
|
313
|
+
for id in "${ids[@]}"; do
|
|
314
|
+
[[ "$id" =~ ^[A-Za-z0-9_.-]+$ ]] || die "invalid package ID: $id"
|
|
315
|
+
[ -f "$FEED/nuget/$id.$version.nupkg" ] || die "package missing from feed: $id $version"
|
|
316
|
+
done
|
|
317
|
+
while IFS= read -r -d '' file; do
|
|
318
|
+
case "$file" in
|
|
319
|
+
nuget.config|NuGet.Config|NuGet.config)
|
|
320
|
+
if grep -q '<packageSourceMapping' "$repo/$file"; then config=$repo/$file; fi ;;
|
|
321
|
+
esac
|
|
322
|
+
done < <(git -C "$repo" ls-files -z)
|
|
323
|
+
if [ -n "$config" ]; then
|
|
324
|
+
if ! grep -q '</packageSources>' "$config" || ! grep -q '</packageSourceMapping>' "$config"; then
|
|
325
|
+
die 'cannot overlay incomplete source mapping configuration'
|
|
326
|
+
fi
|
|
327
|
+
if grep -q 'key="cratis-local"' "$config"; then die 'source key cratis-local already exists'; fi
|
|
328
|
+
fi
|
|
329
|
+
if [ "$DRY" -eq 1 ]; then
|
|
330
|
+
quote_command mkdir -p "$session"
|
|
331
|
+
printf 'write %s importing %s; override %s IDs to %s\n' "$session/Directory.Packages.props" "$props" "${#ids[@]}" "$version"
|
|
332
|
+
printf 'write %s/env%s\n' "$session" "${config:+ and nuget.config}"
|
|
333
|
+
return 0
|
|
334
|
+
fi
|
|
335
|
+
register_session "$repo" "$session" "$version"
|
|
336
|
+
{
|
|
337
|
+
printf '<Project>\n <Import Project="%s" />\n' "$(xml_escape "$props")"
|
|
338
|
+
printf ' <PropertyGroup><CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled></PropertyGroup>\n <ItemGroup>\n'
|
|
339
|
+
for id in "${ids[@]}"; do
|
|
340
|
+
action=Include
|
|
341
|
+
if grep -Fxiq -- "$id" "$TMP/consumer-ids"; then action=Update; fi
|
|
342
|
+
printf ' <PackageVersion %s="%s" Version="%s" />\n' "$action" "$id" "$version"
|
|
343
|
+
done
|
|
344
|
+
printf ' </ItemGroup>\n</Project>\n'
|
|
345
|
+
} > "$session/Directory.Packages.props"
|
|
346
|
+
props_key=DirectoryPackages"PropsPath"; sources_key=RestoreAdditional"ProjectSources"
|
|
347
|
+
{
|
|
348
|
+
printf 'export %s=%q\n' "$props_key" "$session/Directory.Packages.props"
|
|
349
|
+
printf 'export %s=%q\n' "$sources_key" "$FEED/nuget"
|
|
350
|
+
} > "$session/env"
|
|
351
|
+
printf ' %q' "-p:$props_key=$session/Directory.Packages.props" "-p:$sources_key=$FEED/nuget"; printf '\n'
|
|
352
|
+
printf 'source %q\n' "$session/env"
|
|
353
|
+
if [ -n "$config" ]; then
|
|
354
|
+
source_mapping "$config" "$session/nuget.config" "${ids[@]}"
|
|
355
|
+
printf 'For restore, also pass: --configfile %q\n' "$session/nuget.config"
|
|
356
|
+
elif [ -f "$session/nuget.config" ]; then
|
|
357
|
+
rm -- "$session/nuget.config"
|
|
358
|
+
fi
|
|
359
|
+
printf 'overrode %s NuGet package IDs\n' "${#ids[@]}"
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
use_npm() {
|
|
363
|
+
[ $# -eq 2 ] || die 'use-npm requires consumer and stamp'
|
|
364
|
+
local repo version session backup
|
|
365
|
+
repo=$(repo_root "$1"); version=$2
|
|
366
|
+
valid_stamp "$version"; need node; need tar; scratch
|
|
367
|
+
session=$(session_path "$repo"); backup=$session/npm-backup
|
|
368
|
+
[ -f "$repo/package.json" ] || die 'consumer has no root package.json'
|
|
369
|
+
[ ! -e "$backup" ] || die "active npm backup exists: $backup; restore first"
|
|
370
|
+
node - "$repo" "$FEED/npm" "$version" "$TMP/package.json" <<'NODE'
|
|
371
|
+
const fs = require('fs'), path = require('path'), {execFileSync} = require('child_process');
|
|
372
|
+
const [repo, feed, stamp, output] = process.argv.slice(2);
|
|
373
|
+
const root = JSON.parse(fs.readFileSync(path.join(repo, 'package.json'), 'utf8'));
|
|
374
|
+
let count = 0;
|
|
375
|
+
for (const file of fs.existsSync(feed) ? fs.readdirSync(feed) : []) {
|
|
376
|
+
if (!file.endsWith('-' + stamp + '.tgz')) continue;
|
|
377
|
+
const tarball = path.join(feed, file);
|
|
378
|
+
// Read the packed name: slugified scoped names are not reversibly encoded.
|
|
379
|
+
const {name} = JSON.parse(execFileSync('tar', ['-xOf', tarball, 'package/package.json'], {encoding: 'utf8'}));
|
|
380
|
+
if (typeof name !== 'string' || !/^(@[^/\s]+\/)?[^/\s]+$/.test(name))
|
|
381
|
+
throw new Error('Invalid packed package name: ' + tarball);
|
|
382
|
+
root.resolutions = root.resolutions || {};
|
|
383
|
+
root.resolutions[name] = 'file:' + tarball;
|
|
384
|
+
count++;
|
|
385
|
+
}
|
|
386
|
+
if (!count) { console.error('ERROR: no matching npm tarballs'); process.exit(2); }
|
|
387
|
+
fs.writeFileSync(output, JSON.stringify(root, null, 2) + '\n');
|
|
388
|
+
console.log(`overrode ${count} npm packages`);
|
|
389
|
+
NODE
|
|
390
|
+
if [ "$DRY" -eq 1 ]; then
|
|
391
|
+
quote_command mkdir -p "$backup"
|
|
392
|
+
quote_command cp "$TMP/package.json" "$repo/package.json"
|
|
393
|
+
return 0
|
|
394
|
+
fi
|
|
395
|
+
register_session "$repo" "$session" "$version"
|
|
396
|
+
mkdir "$backup"
|
|
397
|
+
cp -p "$repo/package.json" "$backup/package.json"
|
|
398
|
+
if [ -f "$repo/yarn.lock" ]; then cp -p "$repo/yarn.lock" "$backup/yarn.lock"; fi
|
|
399
|
+
git -C "$repo" diff HEAD -- package.json yarn.lock > "$backup/before.diff"
|
|
400
|
+
cp "$TMP/package.json" "$backup/expected-package.json"
|
|
401
|
+
cp "$TMP/package.json" "$repo/package.json"
|
|
402
|
+
printf 'Next: cd %q && yarn install\n' "$repo"
|
|
403
|
+
printf 'Vite: clear stale prebundles with rm -rf node_modules/.vite\n'
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
restore() {
|
|
407
|
+
[ $# -eq 1 ] || die 'restore requires a consumer'
|
|
408
|
+
local repo session backup
|
|
409
|
+
repo=$(repo_root "$1"); session=$(session_path "$repo"); backup=$session/npm-backup
|
|
410
|
+
validate_session "$session"
|
|
411
|
+
if [ -f "$session/repo" ] && [ "$(cat "$session/repo")" != "$repo" ]; then die 'session belongs to another checkout'; fi
|
|
412
|
+
if [ -d "$backup" ]; then
|
|
413
|
+
if ! cmp -s "$repo/package.json" "$backup/expected-package.json" ||
|
|
414
|
+
{ [ -f "$backup/yarn.lock" ] && ! cmp -s "$repo/yarn.lock" "$backup/yarn.lock"; } ||
|
|
415
|
+
{ [ ! -f "$backup/yarn.lock" ] && [ -e "$repo/yarn.lock" ]; }; then
|
|
416
|
+
need node
|
|
417
|
+
node "$(dirname "$SELF")/npm-restore-check.cjs" "$repo" "$backup" ||
|
|
418
|
+
die "npm files changed beyond the overlay; review $backup (nothing restored)"
|
|
419
|
+
fi
|
|
420
|
+
mutate cp -p "$backup/package.json" "$repo/package.json"
|
|
421
|
+
if [ -f "$backup/yarn.lock" ]; then mutate cp -p "$backup/yarn.lock" "$repo/yarn.lock"; else mutate rm -f -- "$repo/yarn.lock"; fi
|
|
422
|
+
mutate rm -rf -- "$backup"
|
|
423
|
+
fi
|
|
424
|
+
mutate rm -f -- "$session/Directory.Packages.props" "$session/nuget.config" "$session/env" "$session/stamp"
|
|
425
|
+
if [ "$DRY" -eq 0 ]; then check "$repo"; fi
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# Check only changed lines, not unchanged context or removed local overrides.
|
|
429
|
+
# NUL-delimited Git paths preserve spaces; diagnostics are relative to the root.
|
|
430
|
+
check() {
|
|
431
|
+
[ $# -ge 1 ] && [ $# -le 2 ] || die 'check requires repo and optional --staged'
|
|
432
|
+
local repo path count=0 hits=0 staged=0
|
|
433
|
+
repo=$(repo_root "$1"); shift
|
|
434
|
+
if [ $# -gt 0 ]; then [ "$1" = --staged ] || die "unknown check option: $1"; staged=1; fi
|
|
435
|
+
scratch
|
|
436
|
+
if [ "$staged" -eq 1 ]; then
|
|
437
|
+
git -C "$repo" diff --cached --name-only -z --diff-filter=ACMRT > "$TMP/tracked"
|
|
438
|
+
else
|
|
439
|
+
git -C "$repo" diff HEAD --name-only -z --diff-filter=ACMRT > "$TMP/tracked"
|
|
440
|
+
fi
|
|
441
|
+
git -C "$repo" ls-files --others --exclude-standard -z > "$TMP/untracked"
|
|
442
|
+
while IFS= read -r -d '' path; do
|
|
443
|
+
count=$((count + 1))
|
|
444
|
+
if [ "$staged" -eq 1 ]; then
|
|
445
|
+
git -C "$repo" diff --cached --no-ext-diff --no-textconv --unified=0 -- "$path" > "$TMP/diff"
|
|
446
|
+
else
|
|
447
|
+
git -C "$repo" diff HEAD --no-ext-diff --no-textconv --unified=0 -- "$path" > "$TMP/diff"
|
|
448
|
+
fi
|
|
449
|
+
awk '/^@@ / { split($3,a,","); n=substr(a[1],2); next } /^\+\+\+/ { next } /^\+/ { print n ":" substr($0,2); n++; next } /^ / { n++ }' "$TMP/diff" > "$TMP/lines"
|
|
450
|
+
scan_lines "$path" "$TMP/lines" > "$TMP/hits"
|
|
451
|
+
if [ -s "$TMP/hits" ]; then cat "$TMP/hits"; hits=$((hits + 1)); fi
|
|
452
|
+
done < "$TMP/tracked"
|
|
453
|
+
while IFS= read -r -d '' path; do
|
|
454
|
+
[ ! -L "$repo/$path" ] || die "cannot scan untracked symlink without following outside repo: $path"
|
|
455
|
+
[ -f "$repo/$path" ] || continue
|
|
456
|
+
count=$((count + 1))
|
|
457
|
+
awk '{print NR ":" $0}' "$repo/$path" > "$TMP/lines"
|
|
458
|
+
scan_lines "$path" "$TMP/lines" > "$TMP/hits"
|
|
459
|
+
if [ -s "$TMP/hits" ]; then cat "$TMP/hits"; hits=$((hits + 1)); fi
|
|
460
|
+
done < "$TMP/untracked"
|
|
461
|
+
printf 'checked %s files, %s leaks\n' "$count" "$hits"
|
|
462
|
+
[ "$hits" -eq 0 ]
|
|
463
|
+
}
|
|
464
|
+
scan_lines() {
|
|
465
|
+
local path=$1 input=$2
|
|
466
|
+
awk -v path="$path" -v feed="$FEED" '
|
|
467
|
+
function hit(pattern) { print path ":" number ":" pattern }
|
|
468
|
+
{
|
|
469
|
+
number=$0; sub(/:.*/,"",number); line=$0; sub(/^[^:]*:/,"",line)
|
|
470
|
+
if (line ~ /-local\.[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\.g[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]/) hit("local stamp")
|
|
471
|
+
if (index(line,feed)) hit("local feed path")
|
|
472
|
+
if (index(line,"DirectoryPackages" "PropsPath")) hit("CPM overlay property")
|
|
473
|
+
if (index(line,"RestoreAdditional" "ProjectSources")) hit("additional restore sources")
|
|
474
|
+
if (line ~ /<PackageVersion[[:space:]]+Update[[:space:]]*=/) hit("package version override")
|
|
475
|
+
if (path ~ /(^|\/)(package.json|yarn.lock)$/ && line ~ /(file|portal|link):/ &&
|
|
476
|
+
(line ~ /\.tgz/ || line ~ /(file|portal|link):(\/|\.\.?\/|~\/|[A-Za-z]:)/)) hit("local npm reference")
|
|
477
|
+
}
|
|
478
|
+
' "$input"
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
# Portable deadline for a read-only Docker probe; no dependency on GNU timeout.
|
|
482
|
+
bounded_docker() {
|
|
483
|
+
local pid second status=0
|
|
484
|
+
docker "$@" > "$TMP/docker-output" 2> "$TMP/docker-error" & pid=$!
|
|
485
|
+
for ((second=0; second<10; second++)); do
|
|
486
|
+
if ! kill -0 "$pid" 2>/dev/null; then break; fi
|
|
487
|
+
sleep 1
|
|
488
|
+
done
|
|
489
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
490
|
+
kill -TERM "$pid" 2>/dev/null || :
|
|
491
|
+
kill -KILL "$pid" 2>/dev/null || :
|
|
492
|
+
wait "$pid" 2>/dev/null || :
|
|
493
|
+
return 1
|
|
494
|
+
fi
|
|
495
|
+
wait "$pid" || status=$?
|
|
496
|
+
return "$status"
|
|
497
|
+
}
|
|
498
|
+
local_artifact() {
|
|
499
|
+
[[ "$1" =~ -local\.[0-9]{12}\.g[0-9a-f]{7}($|[^0-9A-Za-z]) ]]
|
|
500
|
+
}
|
|
501
|
+
clean() {
|
|
502
|
+
local version='' apply=0 consumer='' arg path cache image session maximum count=0
|
|
503
|
+
local subjects=() kinds=()
|
|
504
|
+
while [ $# -gt 0 ]; do
|
|
505
|
+
arg=$1; shift
|
|
506
|
+
case "$arg" in
|
|
507
|
+
--stamp) [ $# -gt 0 ] || die '--stamp requires a stamp'; [ -z "$version" ] || die 'choose --stamp or --all'; version=$1; shift; valid_stamp "$version" ;;
|
|
508
|
+
--all) [ -z "$version" ] || die 'choose --stamp or --all'; version=all ;;
|
|
509
|
+
--apply) apply=1 ;;
|
|
510
|
+
-*) die "unknown clean option: $arg" ;;
|
|
511
|
+
*) [ -z "$consumer" ] || die 'clean takes at most one consumer'; consumer=$(repo_root "$arg") ;;
|
|
512
|
+
esac
|
|
513
|
+
done
|
|
514
|
+
maximum=${CRATIS_LOCAL_CLEAN_MAX:-500}
|
|
515
|
+
[[ "$maximum" =~ ^[0-9]+$ ]] || die 'CRATIS_LOCAL_CLEAN_MAX must be a whole number'
|
|
516
|
+
[ ${#maximum} -le 6 ] || die 'CRATIS_LOCAL_CLEAN_MAX is too large'
|
|
517
|
+
maximum=$((10#$maximum))
|
|
518
|
+
scratch
|
|
519
|
+
for cache in "$FEED/nuget" "$FEED/npm"; do
|
|
520
|
+
[ -d "$cache" ] || continue
|
|
521
|
+
while IFS= read -r -d '' path; do
|
|
522
|
+
local_artifact "${path##*/}" || continue
|
|
523
|
+
if [ -n "$version" ] && [ "$version" != all ]; then case "$path" in *"$version"*) ;; *) continue ;; esac; fi
|
|
524
|
+
subjects+=("$path"); kinds+=(file)
|
|
525
|
+
done < <(find "$cache" -type f \( -name '*.nupkg' -o -name '*.tgz' \) -print0)
|
|
526
|
+
done
|
|
527
|
+
if [ -d "$NUGET_CACHE" ]; then
|
|
528
|
+
for cache in "$NUGET_CACHE"/*; do
|
|
529
|
+
[ -d "$cache" ] && [ ! -L "$cache" ] || continue
|
|
530
|
+
for path in "$cache"/*-local.*; do
|
|
531
|
+
[ -d "$path" ] && [ ! -L "$path" ] || continue
|
|
532
|
+
local_artifact "${path##*/}" || continue
|
|
533
|
+
if [ -n "$version" ] && [ "$version" != all ] && [ "${path##*/}" != "$version" ]; then continue; fi
|
|
534
|
+
subjects+=("$path"); kinds+=(directory)
|
|
535
|
+
done
|
|
536
|
+
done
|
|
537
|
+
fi
|
|
538
|
+
if command -v yarn >/dev/null 2>&1 && [ -n "$consumer" ]; then
|
|
539
|
+
cache=$(cd "$consumer" && yarn config get cacheFolder) || die 'cannot discover Yarn cache'
|
|
540
|
+
if [ -d "$cache" ]; then
|
|
541
|
+
while IFS= read -r -d '' path; do
|
|
542
|
+
local_artifact "${path##*/}" || continue
|
|
543
|
+
if [ -n "$version" ] && [ "$version" != all ]; then case "$path" in *"$version"*) ;; *) continue ;; esac; fi
|
|
544
|
+
subjects+=("$path"); kinds+=(file)
|
|
545
|
+
done < <(find "$cache" -type f -name '*-local.*' -print0)
|
|
546
|
+
fi
|
|
547
|
+
else
|
|
548
|
+
printf 'SKIP: Yarn cache discovery needs yarn and an explicit consumer\n' >&2
|
|
549
|
+
fi
|
|
550
|
+
if command -v docker >/dev/null 2>&1; then
|
|
551
|
+
if bounded_docker image ls --format '{{.Repository}}:{{.Tag}}'; then
|
|
552
|
+
while IFS= read -r image; do
|
|
553
|
+
local_artifact "${image##*:}" || continue
|
|
554
|
+
if [ -n "$version" ] && [ "$version" != all ]; then case "${image##*:}" in *"$version"*) ;; *) continue ;; esac; fi
|
|
555
|
+
subjects+=("$image"); kinds+=(image)
|
|
556
|
+
done < "$TMP/docker-output"
|
|
557
|
+
else
|
|
558
|
+
printf 'SKIP: Docker unavailable or did not respond within 10 seconds\n' >&2
|
|
559
|
+
fi
|
|
560
|
+
else printf 'SKIP: docker is not on PATH\n' >&2; fi
|
|
561
|
+
if [ -f "$FEED/sessions" ]; then cat "$FEED/sessions" > "$TMP/sessions"; else : > "$TMP/sessions"; fi
|
|
562
|
+
if [ -n "$consumer" ]; then session_path "$consumer" >> "$TMP/sessions"; fi
|
|
563
|
+
sort -u "$TMP/sessions" > "$TMP/unique-sessions"
|
|
564
|
+
while IFS= read -r session; do
|
|
565
|
+
[ -d "$session" ] && [ -f "$session/repo" ] && [ -f "$session/stamp" ] || continue
|
|
566
|
+
local_artifact "$(cat "$session/stamp")" || continue
|
|
567
|
+
if [ -n "$version" ] && [ "$version" != all ] && [ "$(cat "$session/stamp")" != "$version" ]; then continue; fi
|
|
568
|
+
[ ! -L "$session" ] && [ "$session" != / ] || die "unsafe session: $session"
|
|
569
|
+
validate_session "$session"
|
|
570
|
+
path=$(cat "$session/repo")
|
|
571
|
+
[ "$(session_path "$path")" = "$session" ] || die "session directory no longer matches repository: $session"
|
|
572
|
+
[ ! -d "$session/npm-backup" ] || die "unresolved npm backup: $session; restore before cleaning"
|
|
573
|
+
subjects+=("$session"); kinds+=(session)
|
|
574
|
+
done < "$TMP/unique-sessions"
|
|
575
|
+
count=${#subjects[@]}
|
|
576
|
+
[ "$count" -le "$maximum" ] || die "clean fuse: $count subjects exceeds $maximum"
|
|
577
|
+
for ((arg=0; arg<count; arg++)); do printf '%s: %s\n' "${kinds[$arg]}" "${subjects[$arg]}"; done
|
|
578
|
+
if [ "$apply" -eq 1 ] && [ "$DRY" -eq 0 ]; then
|
|
579
|
+
for ((arg=0; arg<count; arg++)); do
|
|
580
|
+
path=${subjects[$arg]}
|
|
581
|
+
case "${kinds[$arg]}" in
|
|
582
|
+
image) docker rmi "$path" ;;
|
|
583
|
+
file) [ -f "$path" ] && [ ! -L "$path" ] || die "clean subject changed: $path"; rm -- "$path" ;;
|
|
584
|
+
directory) [ -d "$path" ] && [ ! -L "$path" ] || die "clean subject changed: $path"; rm -rf -- "$path" ;;
|
|
585
|
+
session)
|
|
586
|
+
[ -f "$path/stamp" ] && [ ! -L "$path" ] && [ ! -e "$path/npm-backup" ] || die "session changed: $path"
|
|
587
|
+
validate_session "$path"
|
|
588
|
+
rm -f -- "$path/repo" "$path/stamp" "$path/env" "$path/Directory.Packages.props" "$path/nuget.config"
|
|
589
|
+
rmdir -- "$path" ;;
|
|
590
|
+
esac
|
|
591
|
+
done
|
|
592
|
+
printf 'removed %s subjects\n' "$count"
|
|
593
|
+
else printf 'dry-run: %s subjects (use --apply to remove)\n' "$count"; fi
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
self_test() {
|
|
597
|
+
[ $# -eq 0 ] || die '--self-test takes no arguments'
|
|
598
|
+
local root home repo feed version output status passed=0 skipped=0 test_key arg
|
|
599
|
+
scratch; root=$TMP/self-test; mkdir -p "$root/home" "$root/repo"
|
|
600
|
+
home=$root/home; repo=$root/repo; feed=$home/.cache/cratis/local-feed
|
|
601
|
+
# System-only tests stay hermetic; npm tests inherit PATH to discover node.
|
|
602
|
+
# Both isolate HOME/config and omit the host adapter.
|
|
603
|
+
st() { env -i HOME="$home" PATH=/usr/bin:/bin:/usr/sbin:/sbin TMPDIR="$TMP" /bin/bash "$SELF" "$@"; }
|
|
604
|
+
st_npm() { env -i HOME="$home" PATH="$PATH" TMPDIR="$TMP" /bin/bash "$SELF" "$@"; }
|
|
605
|
+
git -C "$repo" init -q
|
|
606
|
+
repo=$(repo_root "$repo")
|
|
607
|
+
git -C "$repo" config user.email test@example.invalid
|
|
608
|
+
git -C "$repo" config user.name 'Local package self-test'
|
|
609
|
+
printf '<Project><ItemGroup><PackageVersion Include="Cratis.Test" Version="1.2.3" /></ItemGroup></Project>\n' > "$repo/Directory.Packages.props"
|
|
610
|
+
printf '{"private":true,"dependencies":{"@cratis/test":"1.2.3"}}\n' > "$repo/package.json"
|
|
611
|
+
cat > "$repo/yarn.lock" <<'LOCK'
|
|
612
|
+
# This file is generated by running "yarn install" inside your project.
|
|
613
|
+
|
|
614
|
+
__metadata:
|
|
615
|
+
version: 8
|
|
616
|
+
cacheKey: 10c0
|
|
617
|
+
|
|
618
|
+
"@cratis/test@npm:1.2.3":
|
|
619
|
+
version: 1.2.3
|
|
620
|
+
resolution: "@cratis/test@npm:1.2.3"
|
|
621
|
+
checksum: original
|
|
622
|
+
languageName: node
|
|
623
|
+
linkType: hard
|
|
624
|
+
|
|
625
|
+
"unrelated@npm:1.0.0":
|
|
626
|
+
version: 1.0.0
|
|
627
|
+
resolution: "unrelated@npm:1.0.0"
|
|
628
|
+
checksum: keep
|
|
629
|
+
languageName: node
|
|
630
|
+
linkType: hard
|
|
631
|
+
LOCK
|
|
632
|
+
printf 'clean\n' > "$repo/probe"
|
|
633
|
+
git -C "$repo" add .; git -C "$repo" commit -qm initial; git -C "$repo" tag v1.2.3
|
|
634
|
+
status=0; st > "$root/output" || status=$?
|
|
635
|
+
if [ "$status" -ne 2 ] || ! grep -q '^Usage:' "$root/output"; then
|
|
636
|
+
failed 'self-test: no arguments must show usage and exit 2'
|
|
637
|
+
fi
|
|
638
|
+
passed=$((passed + 1))
|
|
639
|
+
version=$(st stamp "$repo")
|
|
640
|
+
[[ "$version" =~ ^1\.2\.4-local\.[0-9]{12}\.g[0-9a-f]{7}$ ]] || failed 'self-test: stamp format'
|
|
641
|
+
passed=$((passed + 1))
|
|
642
|
+
mkdir -p "$feed/nuget" "$feed/npm"
|
|
643
|
+
: > "$feed/nuget/Cratis.Test.1.2.4-local.200001010000.g1234567.nupkg"
|
|
644
|
+
st --dry-run use-nuget "$repo" 1.2.4-local.200001010000.g1234567 Cratis.Test > "$root/output"
|
|
645
|
+
status=0; st --dry-run use-nuget "$repo" 1.2.4-local.200001010000.1234567 Cratis.Test > "$root/output" 2>&1 || status=$?
|
|
646
|
+
[ "$status" -eq 2 ] || failed 'self-test: unprefixed numeric SHA accepted'
|
|
647
|
+
rm "$feed/nuget/Cratis.Test.1.2.4-local.200001010000.g1234567.nupkg"
|
|
648
|
+
passed=$((passed + 2))
|
|
649
|
+
: > "$feed/nuget/Cratis.Test.$version.nupkg"
|
|
650
|
+
: > "$feed/nuget/Cratis.Transitive.$version.nupkg"
|
|
651
|
+
mkdir -p "$root/tarball/package"
|
|
652
|
+
for arg in test transitive; do
|
|
653
|
+
printf '{"name":"@cratis/%s","version":"%s"}\n' "$arg" "$version" > "$root/tarball/package/package.json"
|
|
654
|
+
tar -czf "$feed/npm/@cratis-$arg-$version.tgz" -C "$root/tarball" package
|
|
655
|
+
done
|
|
656
|
+
st use-nuget "$repo" "$version" > "$root/output"
|
|
657
|
+
output=$(git -C "$repo" rev-parse --absolute-git-dir)/cratis-local
|
|
658
|
+
grep -Fq "Import Project=\"$repo/Directory.Packages.props\"" "$output/Directory.Packages.props" || failed 'self-test: overlay import'
|
|
659
|
+
grep -Fq "PackageVersion Update=\"Cratis.Test\" Version=\"$version\"" "$output/Directory.Packages.props" || failed 'self-test: pinned package Update'
|
|
660
|
+
grep -Fq "PackageVersion Include=\"Cratis.Transitive\" Version=\"$version\"" "$output/Directory.Packages.props" || failed 'self-test: transitive package Include'
|
|
661
|
+
grep -Fq 'CentralPackageTransitivePinningEnabled>true' "$output/Directory.Packages.props" || failed 'self-test: transitive pinning'
|
|
662
|
+
test_key=DirectoryPackages"PropsPath"
|
|
663
|
+
grep -Fq "export $test_key=" "$output/env" || failed 'self-test: env property'
|
|
664
|
+
passed=$((passed + 5))
|
|
665
|
+
st check "$repo" > "$root/output" || failed 'self-test: clean repo'
|
|
666
|
+
passed=$((passed + 1))
|
|
667
|
+
for test_key in stamp npm props sources override feed; do
|
|
668
|
+
case "$test_key" in
|
|
669
|
+
stamp) printf '%s\n' "$version" > "$repo/probe" ;;
|
|
670
|
+
npm) printf '{"resolutions":{"@cratis/test":"%s%s"}}\n' 'file:/tmp/' 'package.tgz' > "$repo/package.json" ;;
|
|
671
|
+
props) printf '%s=/tmp/overlay\n' DirectoryPackages"PropsPath" > "$repo/probe" ;;
|
|
672
|
+
sources) printf '%s=/tmp/feed\n' RestoreAdditional"ProjectSources" > "$repo/probe" ;;
|
|
673
|
+
override) printf '<PackageVersion %s="Cratis.Test" />\n' Update > "$repo/probe" ;;
|
|
674
|
+
feed) printf '%s\n' "$feed" > "$repo/probe" ;;
|
|
675
|
+
esac
|
|
676
|
+
status=0; st check "$repo" > "$root/output" || status=$?
|
|
677
|
+
[ "$status" -eq 1 ] || failed "self-test: did not find $test_key leak"
|
|
678
|
+
passed=$((passed + 1))
|
|
679
|
+
git -C "$repo" checkout -- probe package.json
|
|
680
|
+
done
|
|
681
|
+
printf '%s\n' "$version" > "$repo/untracked"
|
|
682
|
+
status=0; st check "$repo" > "$root/output" || status=$?
|
|
683
|
+
[ "$status" -eq 1 ] || failed 'self-test: untracked leak'
|
|
684
|
+
rm "$repo/untracked"; passed=$((passed + 1))
|
|
685
|
+
printf '%s\n' "$version" > "$repo/probe"; git -C "$repo" add probe
|
|
686
|
+
printf 'clean again\n' > "$repo/probe"
|
|
687
|
+
status=0; st check "$repo" --staged > "$root/output" || status=$?
|
|
688
|
+
[ "$status" -eq 1 ] || failed 'self-test: staged content'
|
|
689
|
+
git -C "$repo" restore --staged probe; git -C "$repo" checkout -- probe
|
|
690
|
+
passed=$((passed + 1))
|
|
691
|
+
if command -v node >/dev/null 2>&1; then
|
|
692
|
+
# Preserve pre-existing user edits as well as the committed contents.
|
|
693
|
+
st_npm use-npm "$repo" "$version"
|
|
694
|
+
grep -Fq '"@cratis/transitive": "file:' "$repo/package.json" || failed 'self-test: transitive npm resolution'
|
|
695
|
+
grep -Fq '"@cratis/test": "file:' "$repo/package.json" || failed 'self-test: direct npm resolution'
|
|
696
|
+
st_npm restore "$repo" > "$root/output"
|
|
697
|
+
passed=$((passed + 2))
|
|
698
|
+
printf '{"private":true,"dependencies":{"@cratis/test":"1.2.3"},"userSetting":true}\n' > "$repo/package.json"
|
|
699
|
+
cp "$repo/package.json" "$root/original-package.json"
|
|
700
|
+
cp "$repo/yarn.lock" "$root/original-yarn.lock"
|
|
701
|
+
st_npm use-npm "$repo" "$version"
|
|
702
|
+
# Model Yarn's replacement of a remote entry with a file-tarball entry.
|
|
703
|
+
node - "$repo/yarn.lock" "$feed/npm/@cratis-test-$version.tgz" "$version" <<'NODE'
|
|
704
|
+
const fs = require('fs');
|
|
705
|
+
const [file, input, stamp] = process.argv.slice(2);
|
|
706
|
+
const tarball = require('path').normalize(input);
|
|
707
|
+
const lock = fs.readFileSync(file, 'utf8');
|
|
708
|
+
fs.writeFileSync(file, lock.replace(/"@cratis\/test@npm:1\.2\.3":[\s\S]*?(?="unrelated)/,
|
|
709
|
+
`"@cratis/test@file:${tarball}::locator=consumer%40workspace%3A.":\n` +
|
|
710
|
+
` version: ${stamp}\n resolution: "@cratis/test@file:${tarball}::hash=abc&locator=consumer%40workspace%3A."\n` +
|
|
711
|
+
' checksum: local\n languageName: node\n linkType: hard\n\n'));
|
|
712
|
+
NODE
|
|
713
|
+
cp "$repo/yarn.lock" "$root/overlay-yarn.lock"
|
|
714
|
+
sed 's|^"@cratis/test@file:.*":$|"@cratis/test@npm:1.2.3":|' "$root/overlay-yarn.lock" > "$repo/yarn.lock"
|
|
715
|
+
node "$(dirname "$SELF")/npm-restore-check.cjs" "$repo" "$output/npm-backup" || failed 'self-test: local resolution with original descriptor'
|
|
716
|
+
passed=$((passed + 1))
|
|
717
|
+
cp "$root/overlay-yarn.lock" "$repo/yarn.lock"
|
|
718
|
+
printf '\n"unrelated-user-addition@npm:2":\n version: 2\n' >> "$repo/yarn.lock"
|
|
719
|
+
status=0; st_npm restore "$repo" > "$root/output" 2>&1 || status=$?
|
|
720
|
+
[ "$status" -eq 2 ] && [ -d "$output/npm-backup" ] || failed 'self-test: unrelated lockfile addition refused'
|
|
721
|
+
grep -Fq 'file:' "$repo/package.json" || failed 'self-test: partial restore on conflict'
|
|
722
|
+
passed=$((passed + 2))
|
|
723
|
+
sed 's/checksum: keep/checksum: user-edit/' "$root/overlay-yarn.lock" > "$repo/yarn.lock"
|
|
724
|
+
status=0; st_npm restore "$repo" > "$root/output" 2>&1 || status=$?
|
|
725
|
+
[ "$status" -eq 2 ] || failed 'self-test: unrelated lockfile edit refused'
|
|
726
|
+
passed=$((passed + 1))
|
|
727
|
+
cp "$root/overlay-yarn.lock" "$repo/yarn.lock"
|
|
728
|
+
node - "$repo/package.json" <<'NODE'
|
|
729
|
+
const fs = require('fs'), file = process.argv[2];
|
|
730
|
+
const json = JSON.parse(fs.readFileSync(file)); json.userSetting = false;
|
|
731
|
+
fs.writeFileSync(file, JSON.stringify(json));
|
|
732
|
+
NODE
|
|
733
|
+
status=0; st_npm restore "$repo" > "$root/output" 2>&1 || status=$?
|
|
734
|
+
[ "$status" -eq 2 ] || failed 'self-test: unrelated manifest edit refused'
|
|
735
|
+
passed=$((passed + 1))
|
|
736
|
+
cp "$output/npm-backup/expected-package.json" "$repo/package.json"
|
|
737
|
+
st_npm restore "$repo" > "$root/output"
|
|
738
|
+
cmp -s "$repo/package.json" "$root/original-package.json" || failed 'self-test: npm restore preserves prior manifest edits'
|
|
739
|
+
cmp -s "$repo/yarn.lock" "$root/original-yarn.lock" || failed 'self-test: npm restore after install'
|
|
740
|
+
[ ! -d "$output/npm-backup" ] || failed 'self-test: resolved backup removed'
|
|
741
|
+
passed=$((passed + 3))
|
|
742
|
+
git -C "$repo" checkout -- package.json
|
|
743
|
+
else printf 'SKIP: npm mutation/restore needs node on inherited PATH\n'; skipped=$((skipped + 10)); fi
|
|
744
|
+
mkdir -p "$home/.nuget/packages/cratis.test/$version" "$home/.nuget/packages/cratis.test/1.2.3"
|
|
745
|
+
mkdir -p "$home/.nuget/packages/cratis.test/1.2.3-local.keep"
|
|
746
|
+
for arg in nuget npm; do
|
|
747
|
+
: > "$feed/$arg/keep-local.notes.tgz"
|
|
748
|
+
: > "$feed/$arg/keep-local.202610221735.g12345678.tgz"
|
|
749
|
+
done
|
|
750
|
+
for arg in default --all; do
|
|
751
|
+
if [ "$arg" = default ]; then st clean > "$root/output"; else st clean "$arg" > "$root/output"; fi
|
|
752
|
+
grep -Fq "$version" "$root/output" || failed 'self-test: clean finds stamped subjects'
|
|
753
|
+
if grep -Eq 'keep-local|local.keep' "$root/output"; then failed 'self-test: clean selected non-stamp'; fi
|
|
754
|
+
passed=$((passed + 2))
|
|
755
|
+
done
|
|
756
|
+
st clean --stamp "$version" > "$root/output"
|
|
757
|
+
grep -Fq "$home/.nuget/packages/cratis.test/$version" "$root/output" || failed 'self-test: clean lists local cache'
|
|
758
|
+
[ -d "$home/.nuget/packages/cratis.test/$version" ] || failed 'self-test: dry-run mutated cache'
|
|
759
|
+
st clean --stamp "$version" --apply > "$root/output"
|
|
760
|
+
[ ! -e "$home/.nuget/packages/cratis.test/$version" ] || failed 'self-test: clean failed to remove cache'
|
|
761
|
+
[ -d "$home/.nuget/packages/cratis.test/1.2.3" ] || failed 'self-test: clean removed real version'
|
|
762
|
+
passed=$((passed + 4))
|
|
763
|
+
printf 'SKIP: real dotnet/Yarn/Docker packing is not a hermetic self-test\n'; skipped=$((skipped + 3))
|
|
764
|
+
printf 'self-test: %s passed, %s skipped\n' "$passed" "$skipped"
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
case "$MODE" in
|
|
768
|
+
stamp) stamp "$@" ;;
|
|
769
|
+
pack-nuget) pack_nuget "$@" ;;
|
|
770
|
+
pack-npm) pack_npm "$@" ;;
|
|
771
|
+
use-nuget) use_nuget "$@" ;;
|
|
772
|
+
use-npm) use_npm "$@" ;;
|
|
773
|
+
restore) restore "$@" ;;
|
|
774
|
+
check) check "$@" ;;
|
|
775
|
+
clean) clean "$@" ;;
|
|
776
|
+
--self-test) self_test "$@" ;;
|
|
777
|
+
*) die "unknown command: $MODE (use --help)" ;;
|
|
778
|
+
esac
|