@hasna/skills 0.1.61 → 0.1.62

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.
@@ -90,7 +90,7 @@ If tests fail → fix them before continuing. Never publish broken tests.
90
90
  ```bash
91
91
  git add package.json bun.lock
92
92
  SECRET_PATTERN='sk-(ant|proj)-|npm_[[:alpha:]]|gh[op]_|AKIA[[:upper:][:digit:]]|xai[-]|AI[z]a'
93
- if git diff --cached --diff-filter=ACM --unified=0 | grep -iE "^\\+[^+].*($SECRET_PATTERN)"; then
93
+ if git diff --cached --diff-filter=ACM --unified=0 | grep -iE "^\\+([^+].*)?($SECRET_PATTERN)"; then
94
94
  echo "SECRETS - STOP"
95
95
  exit 1
96
96
  fi
@@ -121,7 +121,7 @@ test "$(cat package.json | python3 -c "import sys,json; print(json.load(sys.stdi
121
121
  ### 7. Post publish intent to git-publishing (BEFORE publishing)
122
122
 
123
123
  Fleet rule: publish intent goes to the `git-publishing` channel BEFORE any
124
- `bun publish`/`npm publish`, and the result is confirmed in the same thread
124
+ `npm publish`, and the result is confirmed in the same thread
125
125
  after. Re-check blockers first — an unread `[FREEZE]` means stop and escalate
126
126
  to `help`.
127
127
 
@@ -136,17 +136,62 @@ echo "Intent posted: message $INTENT_ID"
136
136
 
137
137
  ### 8. Publish
138
138
 
139
+ Npm 10.9.8 and 11.16.0 both omit `gitHead` when they publish directly from a
140
+ linked Git worktree, because their package normalizer treats `.git` as a
141
+ directory and silently ignores the worktree's `.git` file. Publish through the
142
+ deterministic helper beside this SKILL.md. It packs the prepared package, stages
143
+ those files in a disposable normal clone at the exact source HEAD/tree, preserves
144
+ the package's repository-relative subdirectory, and never mutates the source
145
+ worktree's Git metadata.
146
+
147
+ Resolve `PUBLISH_HELPER` to `scripts/publish_with_git_head.sh` beside this
148
+ SKILL.md in the installed skill directory. Do not copy the helper into the
149
+ package repo or replace it with raw `npm publish`.
150
+
139
151
  ```bash
140
152
  ACCESS=public
141
153
  [ "$ORG" = "hasnaxyz" ] && ACCESS=restricted
142
- if command -v bun >/dev/null 2>&1; then
143
- bun publish --access "$ACCESS"
144
- elif command -v npm >/dev/null 2>&1; then
145
- npm publish --access "$ACCESS"
146
- else
147
- echo "BLOCKED: neither bun nor npm publish is available"
154
+ TOKEN_PATH="$ORG/npm/live/publish-token"
155
+ EXPECTED_GIT_HEAD=$(git rev-parse HEAD)
156
+ PUBLISH_HELPER="<directory containing this SKILL.md>/scripts/publish_with_git_head.sh"
157
+ test -f "$PUBLISH_HELPER"
158
+
159
+ (
160
+ NPMRC=$(mktemp)
161
+ trap 'rm -f "$NPMRC"' EXIT
162
+ chmod 600 "$NPMRC"
163
+ printf '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n' > "$NPMRC"
164
+ secrets exec "$TOKEN_PATH" --as NODE_AUTH_TOKEN -- \
165
+ bash "$PUBLISH_HELPER" --userconfig "$NPMRC" --access "$ACCESS"
166
+ )
167
+
168
+ VERIFY_DIR=$(mktemp -d)
169
+ PUBLISHED_GIT_HEAD=""
170
+ for attempt in 1 2 3 4 5; do
171
+ if npm view "$PKG@$NEW_VERSION" gitHead --json \
172
+ > "$VERIFY_DIR/githead.out" \
173
+ 2> "$VERIFY_DIR/githead.err"; then
174
+ if [ -s "$VERIFY_DIR/githead.out" ]; then
175
+ if PUBLISHED_GIT_HEAD=$(node -e '
176
+ const fs = require("node:fs");
177
+ const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
178
+ if (typeof value !== "string") process.exit(2);
179
+ process.stdout.write(value);
180
+ ' "$VERIFY_DIR/githead.out"); then
181
+ break
182
+ fi
183
+ fi
184
+ fi
185
+ sleep 2
186
+ done
187
+ rm -rf "$VERIFY_DIR"
188
+
189
+ if [ "$PUBLISHED_GIT_HEAD" != "$EXPECTED_GIT_HEAD" ]; then
190
+ printf 'GITHEAD_OUTPUT: %s\n' "${PUBLISHED_GIT_HEAD:-ABSENT}"
191
+ printf 'GITHEAD_EXPECTED: %s\n' "$EXPECTED_GIT_HEAD"
148
192
  exit 1
149
193
  fi
194
+ printf 'GITHEAD_VERIFIED: %s\n' "$PUBLISHED_GIT_HEAD"
150
195
  ```
151
196
 
152
197
  ### 9. Install locally via bun (NOT npm)
@@ -199,6 +244,8 @@ Repo releases (GitHub tags/releases) are announced on `git-releases`, not
199
244
  - NEVER publish without running tests and build first
200
245
  - NEVER publish with secrets in the staged diff
201
246
  - NEVER publish without posting intent to `git-publishing` first (step 7)
247
+ - NEVER publish through raw `bun publish` or `npm publish`; use the worktree-safe helper
248
+ - NEVER report success until registry `gitHead` equals the exact source HEAD
202
249
  - ALWAYS confirm in the intent thread after — published or aborted, no silent exits
203
250
  - NEVER bump minor or major — patch only unless user explicitly says otherwise
204
251
  - NEVER skip the CLI verification step
@@ -0,0 +1,47 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { createServer } from "node:http";
3
+
4
+ const [prefix, portFile] = process.argv.slice(2);
5
+ const putStatus = Number(process.env.CAPTURE_PUT_STATUS ?? "201");
6
+ let requestIndex = 0;
7
+
8
+ if (!prefix || !portFile) {
9
+ throw new Error("usage: capture_registry.js <output-prefix> <port-file>");
10
+ }
11
+
12
+ const server = createServer((request, response) => {
13
+ const chunks = [];
14
+
15
+ request.on("data", (chunk) => chunks.push(chunk));
16
+ request.on("end", () => {
17
+ requestIndex += 1;
18
+ const body = Buffer.concat(chunks);
19
+ writeFileSync(
20
+ `${prefix}.request-${requestIndex}.json`,
21
+ JSON.stringify(
22
+ {
23
+ method: request.method,
24
+ url: request.url,
25
+ bodyBytes: body.length,
26
+ },
27
+ null,
28
+ 2,
29
+ ),
30
+ );
31
+
32
+ if (request.method === "PUT") {
33
+ writeFileSync(`${prefix}.put.json`, body);
34
+ response.writeHead(putStatus, { "content-type": "application/json" });
35
+ response.end(JSON.stringify(putStatus < 400 ? { ok: true } : { error: "probe_failure" }));
36
+ setTimeout(() => server.close(), 25);
37
+ return;
38
+ }
39
+
40
+ response.writeHead(404, { "content-type": "application/json" });
41
+ response.end(JSON.stringify({ error: "not_found" }));
42
+ });
43
+ });
44
+
45
+ server.listen(0, "127.0.0.1", () => {
46
+ writeFileSync(portFile, String(server.address().port));
47
+ });
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ for required_command in git node npm tar; do
5
+ if ! command -v "$required_command" >/dev/null 2>&1; then
6
+ printf 'publish_with_git_head: %s is required\n' "$required_command" >&2
7
+ exit 127
8
+ fi
9
+ done
10
+
11
+ package_dir=$(pwd -P)
12
+ if [ ! -f "$package_dir/package.json" ]; then
13
+ printf 'publish_with_git_head: no package.json in %s\n' "$package_dir" >&2
14
+ exit 2
15
+ fi
16
+
17
+ repo_root=$(git -C "$package_dir" rev-parse --show-toplevel)
18
+ head_sha=$(git -C "$repo_root" rev-parse HEAD)
19
+ source_tree=$(git -C "$repo_root" rev-parse "$head_sha^{tree}")
20
+ package_prefix=$(git -C "$package_dir" rev-parse --show-prefix)
21
+ package_relative=${package_prefix%/}
22
+ source_status=$(git -C "$repo_root" status --porcelain --untracked-files=no)
23
+
24
+ if [ -n "$source_status" ]; then
25
+ printf 'publish_with_git_head: tracked source changes must be committed before publish\n' >&2
26
+ exit 2
27
+ fi
28
+
29
+ temp_root=$(mktemp -d "${TMPDIR:-/tmp}/hasna-publish-githead.XXXXXX")
30
+ clone_root="$temp_root/repo"
31
+ pack_dir="$temp_root/pack"
32
+ unpack_dir="$temp_root/unpack"
33
+ pack_json="$temp_root/pack.json"
34
+
35
+ cleanup() {
36
+ local temp_name
37
+ temp_name=$(basename "$temp_root")
38
+ case "$temp_name" in
39
+ hasna-publish-githead.*)
40
+ rm -rf -- "$temp_root"
41
+ ;;
42
+ *)
43
+ printf 'publish_with_git_head: refusing to remove unexpected temp path %s\n' \
44
+ "$temp_root" >&2
45
+ return 1
46
+ ;;
47
+ esac
48
+ }
49
+
50
+ handle_exit() {
51
+ local command_rc=$?
52
+ trap - EXIT INT TERM
53
+ if ! cleanup; then
54
+ exit 70
55
+ fi
56
+ exit "$command_rc"
57
+ }
58
+
59
+ trap handle_exit EXIT
60
+ trap 'exit 130' INT
61
+ trap 'exit 143' TERM
62
+
63
+ mkdir -p "$pack_dir" "$unpack_dir"
64
+ (
65
+ cd "$package_dir"
66
+ npm pack --ignore-scripts --json --pack-destination "$pack_dir"
67
+ ) > "$pack_json"
68
+
69
+ tarball_name=$(
70
+ node -e '
71
+ const fs = require("node:fs");
72
+ const result = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
73
+ if (!Array.isArray(result) || result.length !== 1 || !result[0]?.filename) {
74
+ process.exit(2);
75
+ }
76
+ process.stdout.write(result[0].filename);
77
+ ' "$pack_json"
78
+ )
79
+ tarball="$pack_dir/$tarball_name"
80
+ if [ ! -f "$tarball" ]; then
81
+ printf 'publish_with_git_head: npm pack did not create %s\n' "$tarball" >&2
82
+ exit 2
83
+ fi
84
+
85
+ git clone --quiet --no-hardlinks --no-checkout "$repo_root" "$clone_root"
86
+ git -C "$clone_root" checkout --quiet --detach "$head_sha"
87
+
88
+ clone_head=$(git -C "$clone_root" rev-parse HEAD)
89
+ clone_tree=$(git -C "$clone_root" rev-parse "HEAD^{tree}")
90
+ clone_status=$(git -C "$clone_root" status --porcelain)
91
+ if [ "$clone_head" != "$head_sha" ] || [ "$clone_tree" != "$source_tree" ]; then
92
+ printf 'publish_with_git_head: disposable clone does not match source HEAD/tree\n' >&2
93
+ exit 2
94
+ fi
95
+ if [ -n "$clone_status" ]; then
96
+ printf 'publish_with_git_head: disposable clone is not clean before staging package files\n' >&2
97
+ exit 2
98
+ fi
99
+
100
+ if [ -n "$package_relative" ]; then
101
+ clone_package_dir="$clone_root/$package_relative"
102
+ else
103
+ clone_package_dir="$clone_root"
104
+ fi
105
+ if [ ! -d "$clone_package_dir" ]; then
106
+ printf 'publish_with_git_head: package subdirectory missing in clone: %s\n' \
107
+ "$package_relative" >&2
108
+ exit 2
109
+ fi
110
+
111
+ tar -xzf "$tarball" -C "$unpack_dir"
112
+ cp -a "$unpack_dir/package/." "$clone_package_dir/"
113
+
114
+ if [ -d "$repo_root/node_modules" ] && [ ! -e "$clone_root/node_modules" ]; then
115
+ ln -s "$repo_root/node_modules" "$clone_root/node_modules"
116
+ fi
117
+ if [ "$package_dir" != "$repo_root" ] &&
118
+ [ -d "$package_dir/node_modules" ] &&
119
+ [ ! -e "$clone_package_dir/node_modules" ]; then
120
+ ln -s "$package_dir/node_modules" "$clone_package_dir/node_modules"
121
+ fi
122
+
123
+ node -e '
124
+ const fs = require("node:fs");
125
+ const source = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
126
+ const staged = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
127
+ if (source.name !== staged.name || source.version !== staged.version) {
128
+ process.exit(2);
129
+ }
130
+ ' "$package_dir/package.json" "$clone_package_dir/package.json"
131
+
132
+ if [ "$(git -C "$repo_root" rev-parse HEAD)" != "$head_sha" ] ||
133
+ [ "$(git -C "$repo_root" rev-parse "HEAD^{tree}")" != "$source_tree" ]; then
134
+ printf 'publish_with_git_head: source HEAD/tree changed while preparing publish\n' >&2
135
+ exit 2
136
+ fi
137
+
138
+ printf 'publish_with_git_head: publishing %s from disposable clone at HEAD %s\n' \
139
+ "${package_relative:-.}" "$head_sha" >&2
140
+ (
141
+ cd "$clone_package_dir"
142
+ npm publish "$@"
143
+ )
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
+ helper="$script_dir/publish_with_git_head.sh"
6
+ temp_root=$(mktemp -d)
7
+ source_repo="$temp_root/source"
8
+ source_package="$source_repo/packages/npm-githead-regression"
9
+ linked_worktree="$temp_root/linked-worktree"
10
+ linked_package="$linked_worktree/packages/npm-githead-regression"
11
+
12
+ cleanup() {
13
+ rm -rf "$temp_root"
14
+ }
15
+ trap cleanup EXIT INT TERM
16
+
17
+ if [ ! -f "$helper" ]; then
18
+ printf 'MISSING_HELPER: %s\n' "$helper"
19
+ exit 1
20
+ fi
21
+
22
+ mkdir -p "$source_package"
23
+ git init -q -b main "$source_repo"
24
+ (
25
+ cd "$source_package"
26
+ npm init -y > "$temp_root/npm-init.out" 2> "$temp_root/npm-init.err"
27
+ npm pkg set \
28
+ name=npm-githead-regression \
29
+ version=1.0.0 \
30
+ description="Local npm gitHead regression" \
31
+ license=MIT \
32
+ repository.type=git \
33
+ repository.url=git+https://example.invalid/npm-githead-regression.git \
34
+ scripts.prepublishOnly="node prepublish-probe.cjs" \
35
+ > "$temp_root/npm-pkg-set.out" \
36
+ 2> "$temp_root/npm-pkg-set.err"
37
+ )
38
+ node -e '
39
+ require("node:fs").writeFileSync(
40
+ process.argv[1],
41
+ [
42
+ "const fs = require(\"node:fs\");",
43
+ "const marker = process.env.PROBE_KILL_MARKER;",
44
+ "if (marker) {",
45
+ " fs.writeFileSync(marker, \"ready\\n\");",
46
+ " setInterval(() => {}, 1000);",
47
+ "}",
48
+ "",
49
+ ].join("\n"),
50
+ );
51
+ ' "$source_package/prepublish-probe.cjs"
52
+ git -C "$source_repo" config user.name "Provenance Regression"
53
+ git -C "$source_repo" config user.email "provenance-regression@example.invalid"
54
+ git -C "$source_repo" add packages/npm-githead-regression/package.json \
55
+ packages/npm-githead-regression/prepublish-probe.cjs
56
+ git -C "$source_repo" commit --no-verify -q -m "test: seed provenance package"
57
+ git -C "$source_repo" worktree add -q -b publish-regression "$linked_worktree" HEAD
58
+
59
+ expected_head=$(git -C "$linked_worktree" rev-parse HEAD)
60
+ cp "$linked_worktree/.git" "$temp_root/original-gitfile"
61
+
62
+ start_registry() {
63
+ local label=$1
64
+ local status=$2
65
+ capture_prefix="$temp_root/$label"
66
+ port_file="$capture_prefix.port"
67
+
68
+ CAPTURE_PUT_STATUS="$status" \
69
+ node "$script_dir/capture_registry.js" "$capture_prefix" "$port_file" \
70
+ > "$capture_prefix.server.out" \
71
+ 2> "$capture_prefix.server.err" &
72
+ registry_pid=$!
73
+
74
+ for _ in $(seq 1 100); do
75
+ if [ -s "$port_file" ]; then
76
+ break
77
+ fi
78
+ sleep 0.05
79
+ done
80
+ if [ ! -s "$port_file" ]; then
81
+ printf 'REGISTRY_START: FAILED\n'
82
+ return 1
83
+ fi
84
+ registry_port=$(<"$port_file")
85
+ }
86
+
87
+ read_git_head() {
88
+ node -e '
89
+ const body = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"));
90
+ const version = Object.values(body.versions)[0];
91
+ process.stdout.write(version.gitHead ?? "");
92
+ ' "$1"
93
+ }
94
+
95
+ assert_source_unchanged() {
96
+ test -f "$linked_worktree/.git"
97
+ cmp -s "$linked_worktree/.git" "$temp_root/original-gitfile"
98
+ test -z "$(git -C "$linked_worktree" status --porcelain)"
99
+ }
100
+
101
+ assert_no_helper_temp() {
102
+ local temp_parent=$1
103
+ if compgen -G "$temp_parent/hasna-publish-githead.*" >/dev/null; then
104
+ printf 'HELPER_TEMP_REMAINS: %s\n' "$temp_parent"
105
+ return 1
106
+ fi
107
+ }
108
+
109
+ publish_args=(
110
+ --access public
111
+ --json
112
+ --fetch-retries=0
113
+ --fetch-retry-mintimeout=1
114
+ --fetch-retry-maxtimeout=1
115
+ )
116
+
117
+ start_registry plain 201
118
+ (
119
+ cd "$linked_package"
120
+ npm publish \
121
+ --registry "http://127.0.0.1:$registry_port" \
122
+ "${publish_args[@]}" \
123
+ "--//127.0.0.1:$registry_port/:_authToken=fake-probe-token"
124
+ ) > "$capture_prefix.publish.out" 2> "$capture_prefix.publish.err"
125
+ wait "$registry_pid" 2>/dev/null || true
126
+ plain_head=$(read_git_head "$capture_prefix.put.json")
127
+ if [ -n "$plain_head" ]; then
128
+ printf 'PLAIN_GITHEAD: %s\n' "$plain_head"
129
+ else
130
+ printf 'PLAIN_GITHEAD: ABSENT\n'
131
+ fi
132
+
133
+ success_tmp="$temp_root/success-tmp"
134
+ mkdir "$success_tmp"
135
+ start_registry helper-success 201
136
+ (
137
+ cd "$linked_package"
138
+ TMPDIR="$success_tmp" bash "$helper" \
139
+ --registry "http://127.0.0.1:$registry_port" \
140
+ "${publish_args[@]}" \
141
+ "--//127.0.0.1:$registry_port/:_authToken=fake-probe-token"
142
+ ) > "$capture_prefix.publish.out" 2> "$capture_prefix.publish.err"
143
+ wait "$registry_pid" 2>/dev/null || true
144
+ helper_head=$(read_git_head "$capture_prefix.put.json")
145
+ printf 'EXPECTED_HEAD: %s\n' "$expected_head"
146
+ printf 'HELPER_GITHEAD: %s\n' "${helper_head:-ABSENT}"
147
+ test "$helper_head" = "$expected_head"
148
+ assert_source_unchanged
149
+ assert_no_helper_temp "$success_tmp"
150
+ printf 'SUCCESS_SOURCE: unchanged\n'
151
+
152
+ dirty_tmp="$temp_root/dirty-tmp"
153
+ mkdir "$dirty_tmp"
154
+ (
155
+ cd "$linked_package"
156
+ npm pkg set description="dirty source must be refused"
157
+ )
158
+ set +e
159
+ (
160
+ cd "$linked_package"
161
+ TMPDIR="$dirty_tmp" bash "$helper" --dry-run
162
+ ) > "$temp_root/dirty-source.out" 2> "$temp_root/dirty-source.err"
163
+ dirty_rc=$?
164
+ set -e
165
+ test "$dirty_rc" -ne 0
166
+ grep -q 'tracked source changes must be committed' "$temp_root/dirty-source.err"
167
+ assert_no_helper_temp "$dirty_tmp"
168
+ git -C "$linked_worktree" restore packages/npm-githead-regression/package.json
169
+ assert_source_unchanged
170
+ printf 'DIRTY_SOURCE: refused (helper rc=%s)\n' "$dirty_rc"
171
+
172
+ failure_tmp="$temp_root/failure-tmp"
173
+ mkdir "$failure_tmp"
174
+ start_registry helper-failure 500
175
+ set +e
176
+ (
177
+ cd "$linked_package"
178
+ TMPDIR="$failure_tmp" bash "$helper" \
179
+ --registry "http://127.0.0.1:$registry_port" \
180
+ "${publish_args[@]}" \
181
+ "--//127.0.0.1:$registry_port/:_authToken=fake-probe-token"
182
+ ) > "$capture_prefix.publish.out" 2> "$capture_prefix.publish.err"
183
+ failure_rc=$?
184
+ set -e
185
+ wait "$registry_pid" 2>/dev/null || true
186
+ test "$failure_rc" -ne 0
187
+ assert_source_unchanged
188
+ assert_no_helper_temp "$failure_tmp"
189
+ printf 'FAILURE_SOURCE: unchanged (publish rc=%s)\n' "$failure_rc"
190
+
191
+ kill_tmp="$temp_root/kill-tmp"
192
+ mkdir "$kill_tmp"
193
+ kill_marker="$temp_root/kill-ready"
194
+ start_registry helper-kill 201
195
+ (
196
+ cd "$linked_package"
197
+ exec setsid env TMPDIR="$kill_tmp" PROBE_KILL_MARKER="$kill_marker" \
198
+ bash "$helper" \
199
+ --registry "http://127.0.0.1:$registry_port" \
200
+ "${publish_args[@]}" \
201
+ "--//127.0.0.1:$registry_port/:_authToken=fake-probe-token"
202
+ ) > "$capture_prefix.publish.out" 2> "$capture_prefix.publish.err" &
203
+ helper_pid=$!
204
+
205
+ for _ in $(seq 1 400); do
206
+ if [ -s "$kill_marker" ]; then
207
+ break
208
+ fi
209
+ sleep 0.05
210
+ done
211
+ if [ ! -s "$kill_marker" ]; then
212
+ printf 'FORCED_TERMINATION_MARKER: ABSENT\n'
213
+ kill -KILL -- "-$helper_pid" 2>/dev/null || true
214
+ kill "$registry_pid" 2>/dev/null || true
215
+ wait "$helper_pid" 2>/dev/null || true
216
+ wait "$registry_pid" 2>/dev/null || true
217
+ exit 1
218
+ fi
219
+
220
+ kill -KILL -- "-$helper_pid" 2>/dev/null || true
221
+ set +e
222
+ wait "$helper_pid" 2>/dev/null
223
+ kill_rc=$?
224
+ set -e
225
+ kill "$registry_pid" 2>/dev/null || true
226
+ wait "$registry_pid" 2>/dev/null || true
227
+ test "$kill_rc" -ne 0
228
+ assert_source_unchanged
229
+ compgen -G "$kill_tmp/hasna-publish-githead.*" >/dev/null
230
+ printf 'FORCED_TERMINATION_SOURCE: unchanged (process rc=%s; disposable clone orphaned)\n' \
231
+ "$kill_rc"