@indigoai-us/hq-cloud 6.14.6 → 6.14.7
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/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +5 -3
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +6 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +55 -6
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue.test.js +24 -0
- package/dist/cli/rescue.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +12 -11
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +28 -6
- package/dist/cli/sync.js.map +1 -1
- package/dist/local-path-codec.d.ts +19 -0
- package/dist/local-path-codec.d.ts.map +1 -0
- package/dist/local-path-codec.js +69 -0
- package/dist/local-path-codec.js.map +1 -0
- package/dist/s3.d.ts +8 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +52 -22
- package/dist/s3.js.map +1 -1
- package/dist/scope-shrink.d.ts.map +1 -1
- package/dist/scope-shrink.js +4 -3
- package/dist/scope-shrink.js.map +1 -1
- package/package.json +1 -1
- package/scripts/presign-transport-e2e.mjs +57 -10
- package/scripts/vault-rebaseline.sh +49 -1
- package/scripts/vault-rescue.sh +42 -1
- package/src/cli/reindex.ts +5 -3
- package/src/cli/rescue-core.ts +57 -10
- package/src/cli/rescue.test.ts +34 -0
- package/src/cli/share.ts +12 -11
- package/src/cli/sync.ts +33 -7
- package/src/local-path-codec.ts +95 -0
- package/src/s3.ts +61 -12
- package/src/scope-shrink.ts +4 -3
package/dist/scope-shrink.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import * as fs from "fs";
|
|
35
35
|
import * as path from "path";
|
|
36
36
|
import { hashFile, tombstoneEntry } from "./journal.js";
|
|
37
|
+
import { localPathForVaultKey } from "./local-path-codec.js";
|
|
37
38
|
import { isCoveredByAny, } from "./prefix-coalesce.js";
|
|
38
39
|
/**
|
|
39
40
|
* Build a scope-shrink plan: find orphans, classify each clean/dirty.
|
|
@@ -101,7 +102,7 @@ export function buildScopeShrinkPlan(input) {
|
|
|
101
102
|
* and the operator-facing message points at the path either way.
|
|
102
103
|
*/
|
|
103
104
|
function classifyOrphan(relPath, entry, hqRoot) {
|
|
104
|
-
const absPath =
|
|
105
|
+
const absPath = localPathForVaultKey(hqRoot, relPath);
|
|
105
106
|
let stat;
|
|
106
107
|
try {
|
|
107
108
|
stat = fs.lstatSync(absPath);
|
|
@@ -280,9 +281,9 @@ export function applyScopeShrink(input) {
|
|
|
280
281
|
const quarantinedPaths = [];
|
|
281
282
|
const dirtyKeptPaths = [];
|
|
282
283
|
for (const orphan of plan.clean) {
|
|
283
|
-
const absPath =
|
|
284
|
+
const absPath = localPathForVaultKey(hqRoot, orphan.path);
|
|
284
285
|
if (quarantining) {
|
|
285
|
-
const destAbs =
|
|
286
|
+
const destAbs = localPathForVaultKey(input.quarantineRoot, orphan.path);
|
|
286
287
|
quarantineOrphan(absPath, destAbs);
|
|
287
288
|
tombstoneEntry(journal, orphan.path, reason);
|
|
288
289
|
cleanQuarantined++;
|
package/dist/scope-shrink.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-shrink.js","sourceRoot":"","sources":["../src/scope-shrink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAM7B,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EACL,cAAc,GAEf,MAAM,sBAAsB,CAAC;AAwD9B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAAgC;IAEhC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,gBAAgB,EAAE,GAAG,KAAK,CAAC;IACnE,MAAM,EAAE,SAAS,EAAE,qBAAqB,EAAE,GAAG,KAAK,CAAC;IACnD,MAAM,OAAO,GAA2B,EAAE,CAAC;IAE3C,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7D,IAAI,KAAK,CAAC,SAAS;YAAE,SAAS,CAAC,6BAA6B;QAC5D,IAAI,KAAK,CAAC,SAAS,KAAK,MAAM;YAAE,SAAS;QACzC,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,aAAa,CAAC;YAAE,SAAS;QACtD,IAAI,cAAc,CAAC,OAAO,EAAE,gBAAgB,CAAC;YAAE,SAAS;QACxD,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,qEAAqE;QACrE,wEAAwE;QACxE,sEAAsE;QACtE,IAAI,SAAS,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,SAAS;QAC5D,IAAI,qBAAqB,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,SAAS;QACxE,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO;QACL,OAAO;QACP,KAAK;QACL,KAAK;QACL,mBAAmB,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC;KACxC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAS,cAAc,CACrB,OAAe,EACf,KAAmB,EACnB,MAAc;IAEd,MAAM,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"scope-shrink.js","sourceRoot":"","sources":["../src/scope-shrink.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -82,17 +82,19 @@ async function main() {
|
|
|
82
82
|
|
|
83
83
|
const stamp = Date.now().toString(36);
|
|
84
84
|
const prefix = `shared/_presign_e2e_${stamp}/`;
|
|
85
|
-
const fileKey = `${prefix}
|
|
85
|
+
const fileKey = `${prefix}hooks/presign-e2e-hook.sh`;
|
|
86
|
+
const legacyKey = `${prefix}hooks/legacy-e2e-hook.sh`;
|
|
86
87
|
const linkKey = `${prefix}link`;
|
|
87
88
|
const author = { userSub: "e2e-probe-sub", email: "hassaan@getindigo.ai" };
|
|
88
89
|
|
|
89
90
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "presign-e2e-"));
|
|
90
|
-
const localUp = path.join(tmp, "
|
|
91
|
-
const localDown = path.join(tmp, "
|
|
91
|
+
const localUp = path.join(tmp, "presign-e2e-hook.sh");
|
|
92
|
+
const localDown = path.join(tmp, "downloaded-hook.sh");
|
|
93
|
+
const legacyDown = path.join(tmp, "legacy-hook.sh");
|
|
92
94
|
const localLink = path.join(tmp, "link");
|
|
93
|
-
const content =
|
|
95
|
+
const content = `#!/usr/bin/env bash\n# presign-transport probe ${stamp}\n${"x".repeat(2000)}`;
|
|
94
96
|
fs.writeFileSync(localUp, content);
|
|
95
|
-
fs.chmodSync(localUp,
|
|
97
|
+
fs.chmodSync(localUp, 0o755);
|
|
96
98
|
|
|
97
99
|
let allCreated = [];
|
|
98
100
|
try {
|
|
@@ -125,8 +127,8 @@ async function main() {
|
|
|
125
127
|
head?.metadata?.["created-by"],
|
|
126
128
|
);
|
|
127
129
|
check(
|
|
128
|
-
"metadata.hq-mode round-trips as
|
|
129
|
-
head?.metadata?.["hq-mode"] === "
|
|
130
|
+
"metadata.hq-mode round-trips as 755",
|
|
131
|
+
head?.metadata?.["hq-mode"] === "755",
|
|
130
132
|
head?.metadata?.["hq-mode"],
|
|
131
133
|
);
|
|
132
134
|
check(
|
|
@@ -135,13 +137,13 @@ async function main() {
|
|
|
135
137
|
`${head?.etag} vs ${row?.etag}`,
|
|
136
138
|
);
|
|
137
139
|
|
|
138
|
-
// ---- GET (presigned download, bytes + mode
|
|
140
|
+
// ---- GET (presigned download to an empty destination, bytes + mode) --
|
|
139
141
|
const dl = await downloadFile(ctx, fileKey, localDown);
|
|
140
142
|
const got = fs.readFileSync(localDown, "utf-8");
|
|
141
143
|
check("downloadFile bytes match upload", got === content);
|
|
142
144
|
check(
|
|
143
|
-
"downloaded
|
|
144
|
-
(fs.lstatSync(localDown).mode & 0o777) ===
|
|
145
|
+
"downloaded hook mode is 0755 (hq-mode applied)",
|
|
146
|
+
(fs.lstatSync(localDown).mode & 0o777) === 0o755,
|
|
145
147
|
"0" + (fs.lstatSync(localDown).mode & 0o777).toString(8),
|
|
146
148
|
);
|
|
147
149
|
check(
|
|
@@ -149,6 +151,50 @@ async function main() {
|
|
|
149
151
|
dl?.metadata?.["created-by"] === author.email,
|
|
150
152
|
);
|
|
151
153
|
|
|
154
|
+
// ---- Legacy hq-mode fallback ----------------------------------------
|
|
155
|
+
// Create a pre-hq-mode object through the same real presigned PUT
|
|
156
|
+
// endpoint, deliberately omitting metadata. A legacy remote overwrite
|
|
157
|
+
// must retain the known-good local executable mode instead of silently
|
|
158
|
+
// replacing the hook with the receiver's usual 0644 umask default.
|
|
159
|
+
const legacyPresign = await vault.presign({
|
|
160
|
+
companyUid,
|
|
161
|
+
op: "put",
|
|
162
|
+
keys: [{ key: legacyKey, op: "put", contentType: "text/x-shellscript" }],
|
|
163
|
+
});
|
|
164
|
+
const legacyPut = legacyPresign.results[0];
|
|
165
|
+
if (!legacyPut?.url) throw new Error("legacy PUT presign was not returned");
|
|
166
|
+
const legacyPutResponse = await fetch(legacyPut.url, {
|
|
167
|
+
method: "PUT",
|
|
168
|
+
headers: legacyPut.headers,
|
|
169
|
+
body: "#!/usr/bin/env bash\necho legacy hook\n",
|
|
170
|
+
});
|
|
171
|
+
if (!legacyPutResponse.ok) {
|
|
172
|
+
throw new Error(`legacy presigned PUT failed: ${legacyPutResponse.status}`);
|
|
173
|
+
}
|
|
174
|
+
allCreated.push(legacyKey);
|
|
175
|
+
const legacyHead = await headRemoteFile(ctx, legacyKey);
|
|
176
|
+
check(
|
|
177
|
+
"legacy object deliberately has no hq-mode metadata",
|
|
178
|
+
legacyHead?.metadata?.["hq-mode"] === undefined,
|
|
179
|
+
legacyHead?.metadata?.["hq-mode"],
|
|
180
|
+
);
|
|
181
|
+
fs.writeFileSync(legacyDown, "#!/usr/bin/env bash\necho prior hook\n");
|
|
182
|
+
fs.chmodSync(legacyDown, 0o755);
|
|
183
|
+
const legacyDownload = await downloadFile(ctx, legacyKey, legacyDown);
|
|
184
|
+
check(
|
|
185
|
+
"legacy hook fallback retains existing 0755 mode (not 0644)",
|
|
186
|
+
(fs.lstatSync(legacyDown).mode & 0o777) === 0o755,
|
|
187
|
+
"0" + (fs.lstatSync(legacyDown).mode & 0o777).toString(8),
|
|
188
|
+
);
|
|
189
|
+
check(
|
|
190
|
+
"legacy hq-mode fallback is surfaced for repair telemetry",
|
|
191
|
+
legacyDownload.modeWarnings?.some(
|
|
192
|
+
(warning) =>
|
|
193
|
+
warning.reason === "missing-hq-mode" &&
|
|
194
|
+
warning.fallback === "preserved-local-mode",
|
|
195
|
+
) === true,
|
|
196
|
+
);
|
|
197
|
+
|
|
152
198
|
// ---- SYMLINK round-trip --------------------------------------------
|
|
153
199
|
const ln = await uploadSymlink(ctx, "../target/path.txt", linkKey, author);
|
|
154
200
|
allCreated.push(linkKey);
|
|
@@ -164,6 +210,7 @@ async function main() {
|
|
|
164
210
|
|
|
165
211
|
// ---- DELETE (presigned) --------------------------------------------
|
|
166
212
|
await deleteRemoteFile(ctx, fileKey);
|
|
213
|
+
await deleteRemoteFile(ctx, legacyKey);
|
|
167
214
|
await deleteRemoteFile(ctx, linkKey);
|
|
168
215
|
allCreated = [];
|
|
169
216
|
const afterFile = await headRemoteFile(ctx, fileKey);
|
|
@@ -91,12 +91,17 @@ AWS_ARGS=()
|
|
|
91
91
|
[ -n "$AWS_PROFILE_OVERRIDE" ] && AWS_ARGS+=(--profile "$AWS_PROFILE_OVERRIDE")
|
|
92
92
|
AWS_ARGS+=(--region "$AWS_REGION_OVERRIDE")
|
|
93
93
|
|
|
94
|
+
command -v jq >/dev/null || {
|
|
95
|
+
echo "error: jq is required to preserve existing S3 metadata during hq-mode repair" >&2
|
|
96
|
+
exit 2
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
# Tmp dirs.
|
|
95
100
|
BASELINE="$(mktemp -d -t hq-rebaseline-baseline-XXXXXX)"
|
|
96
101
|
STAGING="$(mktemp -d -t hq-rebaseline-stage-XXXXXX)"
|
|
97
102
|
|
|
98
103
|
cleanup() {
|
|
99
|
-
local rc
|
|
104
|
+
local rc="${1:-$?}"
|
|
100
105
|
if [ "$KEEP_TMP" = "1" ]; then
|
|
101
106
|
echo "==> --keep-tmp set; preserving $BASELINE and $STAGING" >&2
|
|
102
107
|
else
|
|
@@ -212,6 +217,48 @@ SYNC_ARGS=("s3" "sync" "$STAGING/" "s3://$BUCKET/" "--size-only")
|
|
|
212
217
|
|
|
213
218
|
aws "${AWS_ARGS[@]}" "${SYNC_ARGS[@]}"
|
|
214
219
|
|
|
220
|
+
# `aws s3 sync` carries bytes but does not derive hq-cloud's per-file hq-mode
|
|
221
|
+
# metadata. Re-upload ONLY regular objects that lack a valid mode, taking the
|
|
222
|
+
# permission bits from this freshly cloned release tree (the canonical git
|
|
223
|
+
# manifest). This is the one-time repair for legacy objects: a fresh pull can
|
|
224
|
+
# now restore shipped hooks/scripts from metadata rather than guessing from
|
|
225
|
+
# content. Existing user metadata is retained, including created-by fields.
|
|
226
|
+
n_mode_backfilled=0
|
|
227
|
+
MODE_META_DIR="$(mktemp -d -t hq-rebaseline-mode-meta-XXXXXX)"
|
|
228
|
+
cleanup_mode_meta() { rm -rf "${MODE_META_DIR:-}"; }
|
|
229
|
+
trap 'rc=$?; cleanup_mode_meta; cleanup "$rc"' EXIT
|
|
230
|
+
mode_index=0
|
|
231
|
+
while IFS= read -r -d '' src; do
|
|
232
|
+
rel="${src#"$STAGING/"}"
|
|
233
|
+
# Symlink records intentionally have no hq-mode (their mode is OS-owned).
|
|
234
|
+
grep -Fqx -- "$rel" "$SYMLIST" && continue
|
|
235
|
+
|
|
236
|
+
mode="$(stat -c '%a' "$src")"
|
|
237
|
+
head_json="$MODE_META_DIR/$mode_index.head.json"
|
|
238
|
+
metadata_json="$MODE_META_DIR/$mode_index.metadata.json"
|
|
239
|
+
mode_index=$((mode_index + 1))
|
|
240
|
+
aws "${AWS_ARGS[@]}" s3api head-object \
|
|
241
|
+
--bucket "$BUCKET" --key "$rel" --output json > "$head_json"
|
|
242
|
+
existing_mode="$(jq -r '.Metadata["hq-mode"] // empty' "$head_json")"
|
|
243
|
+
if [[ "$existing_mode" =~ ^[0-7]{1,4}$ ]]; then
|
|
244
|
+
continue
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
jq -c --arg mode "$mode" \
|
|
248
|
+
'(.Metadata // {}) + {"hq-mode": $mode}' \
|
|
249
|
+
"$head_json" > "$metadata_json"
|
|
250
|
+
content_type="$(jq -r '.ContentType // empty' "$head_json")"
|
|
251
|
+
if [ "$DRY_RUN" = "1" ]; then
|
|
252
|
+
echo " would backfill hq-mode=$mode: $rel" >&2
|
|
253
|
+
else
|
|
254
|
+
PUT_ARGS=(s3api put-object --bucket "$BUCKET" --key "$rel" --body "$src" --metadata "file://$metadata_json")
|
|
255
|
+
[ -n "$content_type" ] && PUT_ARGS+=(--content-type "$content_type")
|
|
256
|
+
aws "${AWS_ARGS[@]}" "${PUT_ARGS[@]}" >/dev/null
|
|
257
|
+
echo " backfilled hq-mode=$mode: $rel" >&2
|
|
258
|
+
fi
|
|
259
|
+
n_mode_backfilled=$((n_mode_backfilled + 1))
|
|
260
|
+
done < <(find "$STAGING" -type f -print0)
|
|
261
|
+
|
|
215
262
|
# Stamp the hq-symlink-target metadata header on every marker so the menubar
|
|
216
263
|
# pull materializes them as real symlinks (apps/hq-cloud/src/s3.ts:466-468
|
|
217
264
|
# prefers header detection; body sniff is fallback).
|
|
@@ -271,5 +318,6 @@ echo " version: $VERSION ($SOURCE_REPO@$REF)" >&2
|
|
|
271
318
|
echo " pushed: $n_files files, $n_syms symlinks (gross — preserved excluded)" >&2
|
|
272
319
|
echo " user-state preserved: $n_preserved files" >&2
|
|
273
320
|
echo " stamped: $n_stamped" >&2
|
|
321
|
+
echo " hq-mode backfilled: $n_mode_backfilled" >&2
|
|
274
322
|
echo " deleted: $n_deleted (v14 cruft)" >&2
|
|
275
323
|
exit 0
|
package/scripts/vault-rescue.sh
CHANGED
|
@@ -120,6 +120,11 @@ AWS_ARGS=()
|
|
|
120
120
|
[ -n "$AWS_PROFILE_OVERRIDE" ] && AWS_ARGS+=(--profile "$AWS_PROFILE_OVERRIDE")
|
|
121
121
|
AWS_ARGS+=(--region "$AWS_REGION_OVERRIDE")
|
|
122
122
|
|
|
123
|
+
command -v jq >/dev/null || {
|
|
124
|
+
echo "error: jq is required to preserve existing S3 metadata during hq-mode repair" >&2
|
|
125
|
+
exit 2
|
|
126
|
+
}
|
|
127
|
+
|
|
123
128
|
# Tmp dir setup.
|
|
124
129
|
if [ -n "$TMP_DIR_OVERRIDE" ]; then
|
|
125
130
|
TMP_DIR="$TMP_DIR_OVERRIDE"
|
|
@@ -201,8 +206,9 @@ echo "==> [3/3] push $TMP_DIR/ -> s3://$BUCKET/" >&2
|
|
|
201
206
|
# per apps/hq-cloud/src/s3.ts:417). Skip a minimal denylist that should
|
|
202
207
|
# never round-trip through S3.
|
|
203
208
|
STAGING="$(mktemp -d -t hq-vault-rescue-stage-XXXXXX)"
|
|
209
|
+
MODE_META_DIR="$(mktemp -d -t hq-vault-rescue-mode-meta-XXXXXX)"
|
|
204
210
|
add_staging_cleanup() {
|
|
205
|
-
rm -rf "${STAGING:-}"
|
|
211
|
+
rm -rf "${STAGING:-}" "${MODE_META_DIR:-}"
|
|
206
212
|
}
|
|
207
213
|
# Chain cleanup: existing trap removes TMP_DIR; we also need STAGING gone.
|
|
208
214
|
trap '{ add_staging_cleanup; cleanup; }' EXIT
|
|
@@ -261,6 +267,40 @@ echo " staged files=$n_files symlinks=$n_symlinks skipped=$n_skipped" >&2
|
|
|
261
267
|
echo " aws s3 sync $STAGING/ -> s3://$BUCKET/ --size-only --delete" >&2
|
|
262
268
|
aws "${AWS_ARGS[@]}" s3 sync "$STAGING/" "s3://$BUCKET/" --size-only --delete
|
|
263
269
|
|
|
270
|
+
# The AWS CLI moves bytes without hq-cloud's hq-mode metadata. Re-upload only
|
|
271
|
+
# regular objects that need the one-time repair, preserving existing S3 user
|
|
272
|
+
# metadata. For release-shipped files the rescue overlay supplied these modes
|
|
273
|
+
# from the trusted hq-core git tree, so a fresh pull restores hooks/scripts
|
|
274
|
+
# from canonical metadata instead of trying to infer executability from bytes.
|
|
275
|
+
n_mode_backfilled=0
|
|
276
|
+
mode_index=0
|
|
277
|
+
while IFS= read -r -d '' src; do
|
|
278
|
+
rel="${src#"$STAGING/"}"
|
|
279
|
+
# Symlink records intentionally have no hq-mode (their mode is OS-owned).
|
|
280
|
+
grep -Fqx -- "$rel" "$SYMLINK_LIST" && continue
|
|
281
|
+
|
|
282
|
+
mode="$(stat -c '%a' "$src")"
|
|
283
|
+
head_json="$MODE_META_DIR/$mode_index.head.json"
|
|
284
|
+
metadata_json="$MODE_META_DIR/$mode_index.metadata.json"
|
|
285
|
+
mode_index=$((mode_index + 1))
|
|
286
|
+
aws "${AWS_ARGS[@]}" s3api head-object \
|
|
287
|
+
--bucket "$BUCKET" --key "$rel" --output json > "$head_json"
|
|
288
|
+
existing_mode="$(jq -r '.Metadata["hq-mode"] // empty' "$head_json")"
|
|
289
|
+
if [[ "$existing_mode" =~ ^[0-7]{1,4}$ ]]; then
|
|
290
|
+
continue
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
jq -c --arg mode "$mode" \
|
|
294
|
+
'(.Metadata // {}) + {"hq-mode": $mode}' \
|
|
295
|
+
"$head_json" > "$metadata_json"
|
|
296
|
+
content_type="$(jq -r '.ContentType // empty' "$head_json")"
|
|
297
|
+
PUT_ARGS=(s3api put-object --bucket "$BUCKET" --key "$rel" --body "$src" --metadata "file://$metadata_json")
|
|
298
|
+
[ -n "$content_type" ] && PUT_ARGS+=(--content-type "$content_type")
|
|
299
|
+
aws "${AWS_ARGS[@]}" "${PUT_ARGS[@]}" >/dev/null
|
|
300
|
+
echo " backfilled hq-mode=$mode: $rel" >&2
|
|
301
|
+
n_mode_backfilled=$((n_mode_backfilled + 1))
|
|
302
|
+
done < <(find "$STAGING" -type f -print0)
|
|
303
|
+
|
|
264
304
|
# Stamp the hq-symlink metadata header on every symlink-marker object so
|
|
265
305
|
# the menubar pull materializes them as real symlinks (apps/hq-cloud/src/
|
|
266
306
|
# s3.ts:466-468 prefers header detection; body-sniff is fallback).
|
|
@@ -288,4 +328,5 @@ echo " version: $VERSION ($SOURCE_REPO@$REF)" >&2
|
|
|
288
328
|
echo " files: $n_files" >&2
|
|
289
329
|
echo " symlinks: $n_symlinks" >&2
|
|
290
330
|
echo " stamped: $n_stamped" >&2
|
|
331
|
+
echo " hq-mode backfilled: $n_mode_backfilled" >&2
|
|
291
332
|
echo " skipped: $n_skipped" >&2
|
package/src/cli/reindex.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type CloudTelemetryClient,
|
|
33
33
|
type TelemetryClaims,
|
|
34
34
|
} from "../telemetry-events.js";
|
|
35
|
+
import { decodeLocalVaultSegment, encodeLocalVaultSegment } from "../local-path-codec.js";
|
|
35
36
|
|
|
36
37
|
export interface ReindexOptions {
|
|
37
38
|
/** HQ root to operate on. Defaults to process.cwd(). */
|
|
@@ -565,7 +566,7 @@ export function reindex(opts: ReindexOptions = {}): ReindexResult {
|
|
|
565
566
|
if (skillName.startsWith("_")) continue;
|
|
566
567
|
if (!isFile(path.join(skillPath, "SKILL.md"))) continue;
|
|
567
568
|
|
|
568
|
-
const wrapperName = `${ns}:${skillName}
|
|
569
|
+
const wrapperName = encodeLocalVaultSegment(`${ns}:${skillName}`);
|
|
569
570
|
const wrapper = path.join(root, ".claude", "skills", wrapperName);
|
|
570
571
|
expectedWrappers.add(wrapperName);
|
|
571
572
|
|
|
@@ -641,13 +642,14 @@ export function reindex(opts: ReindexOptions = {}): ReindexResult {
|
|
|
641
642
|
const entryPath = path.join(skillsDir, entry);
|
|
642
643
|
const lst = lstatOrNull(entryPath);
|
|
643
644
|
if (!lst) continue; // accept broken symlinks (lstat still succeeds)
|
|
644
|
-
|
|
645
|
+
const canonicalEntry = decodeLocalVaultSegment(entry);
|
|
646
|
+
if (!canonicalEntry.includes(":")) continue; // not a namespaced wrapper
|
|
645
647
|
if (expectedWrappers.has(entry)) continue; // maintained this run
|
|
646
648
|
|
|
647
649
|
// A wrapper is "managed" only when its namespace prefix matches the
|
|
648
650
|
// namespace encoded in its symlink target — this distinguishes
|
|
649
651
|
// script-produced wrappers from hand-authored composite wrappers.
|
|
650
|
-
const ns =
|
|
652
|
+
const ns = canonicalEntry.slice(0, canonicalEntry.indexOf(":"));
|
|
651
653
|
const matchTarget = (t: string | null, p: string): boolean => {
|
|
652
654
|
if (!t) return false;
|
|
653
655
|
if (t.startsWith(`${p}/personal/skills/`)) return ns === "personal";
|
package/src/cli/rescue-core.ts
CHANGED
|
@@ -105,6 +105,22 @@ function run(
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Cygwin's rsync is a POSIX program even when Node gave us a native Win32
|
|
110
|
+
* path. Keep UNC paths in Cygwin's documented `//server/share` form and map
|
|
111
|
+
* drive roots explicitly instead of relying on Cygwin's native-path fallback.
|
|
112
|
+
*/
|
|
113
|
+
export function toCygwinRsyncPath(localPath: string): string {
|
|
114
|
+
const slashPath = localPath.replaceAll("\\", "/");
|
|
115
|
+
if (/^\/\/[^/]+\/[^/]+(?:\/|$)/.test(slashPath)) return slashPath;
|
|
116
|
+
|
|
117
|
+
const drivePath = /^([a-z]):\/(.*)$/i.exec(slashPath);
|
|
118
|
+
if (drivePath) {
|
|
119
|
+
return `/cygdrive/${drivePath[1]!.toLowerCase()}/${drivePath[2]}`;
|
|
120
|
+
}
|
|
121
|
+
return localPath;
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
function redactGitDiagnostic(output: string, ghToken: string): string {
|
|
109
125
|
let redacted = output;
|
|
110
126
|
if (ghToken) redacted = redacted.replaceAll(ghToken, "***");
|
|
@@ -739,7 +755,13 @@ function doRescue(
|
|
|
739
755
|
if (rsyncAvailable) {
|
|
740
756
|
const r = run(
|
|
741
757
|
"rsync",
|
|
742
|
-
[
|
|
758
|
+
[
|
|
759
|
+
"-a",
|
|
760
|
+
"--exclude=node_modules/",
|
|
761
|
+
"--exclude=.git/",
|
|
762
|
+
toCygwinRsyncPath(srcAbs),
|
|
763
|
+
toCygwinRsyncPath(destParent + "/"),
|
|
764
|
+
],
|
|
743
765
|
{ env },
|
|
744
766
|
);
|
|
745
767
|
if (r.status !== 0) cpA(srcAbs, destParent);
|
|
@@ -841,6 +863,24 @@ function doRescue(
|
|
|
841
863
|
// Actions run in classification (walk) order — the same order the old
|
|
842
864
|
// interleaved walk mutated in — so per-file output and on-disk results are
|
|
843
865
|
// unchanged versus before the two-phase split.
|
|
866
|
+
const overlayProtect: string[] = [];
|
|
867
|
+
if (fs.statSync(unchangedList).size > 0) {
|
|
868
|
+
overlayProtect.push(`--exclude-from=${unchangedList}`);
|
|
869
|
+
}
|
|
870
|
+
const overlayArgs = [
|
|
871
|
+
"-a",
|
|
872
|
+
...overlayProtect,
|
|
873
|
+
...rsyncExcludes,
|
|
874
|
+
toCygwinRsyncPath(srcDir + "/"),
|
|
875
|
+
toCygwinRsyncPath(hqRoot + "/"),
|
|
876
|
+
];
|
|
877
|
+
// Exercise the exact overlay command before any classified delete/move. A
|
|
878
|
+
// Cygwin path conversion or filter failure therefore leaves the root intact.
|
|
879
|
+
const overlayPreflight = run("rsync", ["--dry-run", ...overlayArgs], { env });
|
|
880
|
+
if (overlayPreflight.status !== 0) {
|
|
881
|
+
err(overlayPreflight.stderr);
|
|
882
|
+
throw new ExitError(overlayPreflight.status || 1);
|
|
883
|
+
}
|
|
844
884
|
ctx.actions = buildRescueActions(ctx);
|
|
845
885
|
applyClassifiedActions(ctx, backupDir);
|
|
846
886
|
|
|
@@ -866,15 +906,22 @@ function doRescue(
|
|
|
866
906
|
|
|
867
907
|
// --- Overlay source onto HQ root ---
|
|
868
908
|
out("==> Overlaying source onto HQ root ...\n");
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
909
|
+
// `unchangedList` is only populated during applyClassifiedActions above, so
|
|
910
|
+
// re-derive the --exclude-from protect list HERE. Building overlayArgs before
|
|
911
|
+
// classification left the list empty, so unchanged files were overlaid and
|
|
912
|
+
// their mtimes reset to clone time (rescue-mtime-preserve regression). The
|
|
913
|
+
// early overlayArgs remain correct for the pre-classification dry-run.
|
|
914
|
+
const overlayRunArgs =
|
|
915
|
+
fs.statSync(unchangedList).size > 0
|
|
916
|
+
? [
|
|
917
|
+
"-a",
|
|
918
|
+
`--exclude-from=${unchangedList}`,
|
|
919
|
+
...rsyncExcludes,
|
|
920
|
+
toCygwinRsyncPath(srcDir + "/"),
|
|
921
|
+
toCygwinRsyncPath(hqRoot + "/"),
|
|
922
|
+
]
|
|
923
|
+
: overlayArgs;
|
|
924
|
+
const overlay = run("rsync", overlayRunArgs, { env });
|
|
878
925
|
if (overlay.status !== 0) {
|
|
879
926
|
// bash runs under set -e: a failed rsync aborts with its status.
|
|
880
927
|
err(overlay.stderr);
|
package/src/cli/rescue.test.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { buildRescueArgs, extractLockTimeout } from "./rescue.js";
|
|
3
|
+
import { toCygwinRsyncPath } from "./rescue-core.js";
|
|
4
|
+
import {
|
|
5
|
+
decodeLocalVaultSegment,
|
|
6
|
+
encodeLocalVaultSegment,
|
|
7
|
+
localPathForVaultKey,
|
|
8
|
+
vaultKeyForLocalPath,
|
|
9
|
+
} from "../local-path-codec.js";
|
|
3
10
|
|
|
4
11
|
describe("extractLockTimeout", () => {
|
|
5
12
|
it("returns the explicit option untouched when extraArgs is empty/absent", () => {
|
|
@@ -95,3 +102,30 @@ describe("buildRescueArgs", () => {
|
|
|
95
102
|
expect(buildRescueArgs({ paths: [] })).toEqual([]);
|
|
96
103
|
});
|
|
97
104
|
});
|
|
105
|
+
|
|
106
|
+
describe("Windows sync path adapters", () => {
|
|
107
|
+
it("converts drive-letter and UNC rsync arguments to Cygwin paths", () => {
|
|
108
|
+
expect(toCygwinRsyncPath("C:\\HQ\\core\\")).toBe("/cygdrive/c/HQ/core/");
|
|
109
|
+
expect(toCygwinRsyncPath("D:/HQ/workspace")).toBe("/cygdrive/d/HQ/workspace");
|
|
110
|
+
expect(toCygwinRsyncPath("\\\\fileserver\\hq-share\\HQ\\")).toBe(
|
|
111
|
+
"//fileserver/hq-share/HQ/",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("round-trips Windows-safe local names without changing canonical vault keys", () => {
|
|
116
|
+
const win32 = true;
|
|
117
|
+
expect(encodeLocalVaultSegment("core:skill", win32)).toBe("core%3Askill");
|
|
118
|
+
expect(encodeLocalVaultSegment("core%3Askill", win32)).toBe("core%253Askill");
|
|
119
|
+
expect(encodeLocalVaultSegment("core\u0000skill", win32)).toBe("core%00skill");
|
|
120
|
+
expect(decodeLocalVaultSegment("core%3Askill", win32)).toBe("core:skill");
|
|
121
|
+
|
|
122
|
+
// Pull materializes the key under a safe filename, while the journal and
|
|
123
|
+
// the following push retain the original canonical S3 key.
|
|
124
|
+
const canonicalKey = "factory:slack/apps/web/oauth-token.json";
|
|
125
|
+
const pulledLocalPath = localPathForVaultKey("C:\\HQ\\companies\\acme", canonicalKey, win32);
|
|
126
|
+
expect(pulledLocalPath).toContain("factory%3Aslack");
|
|
127
|
+
const journalKey = canonicalKey;
|
|
128
|
+
const pushedKey = vaultKeyForLocalPath("C:\\HQ\\companies\\acme", pulledLocalPath, win32);
|
|
129
|
+
expect(pushedKey).toBe(journalKey);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/cli/share.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as path from "path";
|
|
|
11
11
|
import type { EntityContext, VaultServiceConfig, SyncJournal } from "../types.js";
|
|
12
12
|
import { resolveEntityContext, isExpiringSoon, refreshEntityContext } from "../context.js";
|
|
13
13
|
import { createSyncProgressRecorder } from "../sync-progress.js";
|
|
14
|
+
import { localPathForVaultKey, vaultKeyForLocalPath } from "../local-path-codec.js";
|
|
14
15
|
import {
|
|
15
16
|
uploadFile,
|
|
16
17
|
uploadSymlink,
|
|
@@ -788,7 +789,7 @@ function wrapFilterWithScope(
|
|
|
788
789
|
): (absPath: string, isDir?: boolean) => boolean {
|
|
789
790
|
return (absPath: string, isDir?: boolean) => {
|
|
790
791
|
if (!underlying(absPath, isDir)) return false;
|
|
791
|
-
const rel =
|
|
792
|
+
const rel = vaultKeyForLocalPath(syncRoot, absPath);
|
|
792
793
|
if (rel === "" || rel.startsWith("..")) return true; // root / outside — defer
|
|
793
794
|
if (isDir) {
|
|
794
795
|
if (isDirInScope(rel, prefixSet)) return true;
|
|
@@ -819,7 +820,7 @@ export function wrapFilterWithIgnoreVisibility(
|
|
|
819
820
|
if (allowed) return true;
|
|
820
821
|
|
|
821
822
|
onAnyExcluded?.();
|
|
822
|
-
const rel =
|
|
823
|
+
const rel = vaultKeyForLocalPath(hqRoot, absPath);
|
|
823
824
|
if (rel === "" || rel.startsWith("..")) return false;
|
|
824
825
|
if (!isExpectedIgnore(rel)) onIgnoreExcluded(rel);
|
|
825
826
|
return false;
|
|
@@ -1456,13 +1457,13 @@ async function writePushConflictMirror(
|
|
|
1456
1457
|
try {
|
|
1457
1458
|
const detectedAt = new Date().toISOString();
|
|
1458
1459
|
const machineId = readShortMachineId(run.hqRoot);
|
|
1459
|
-
const originalRelative =
|
|
1460
|
+
const originalRelative = vaultKeyForLocalPath(run.hqRoot, item.absolutePath);
|
|
1460
1461
|
const conflictRelative = buildConflictPath(
|
|
1461
1462
|
originalRelative,
|
|
1462
1463
|
detectedAt,
|
|
1463
1464
|
machineId,
|
|
1464
1465
|
);
|
|
1465
|
-
const conflictAbs =
|
|
1466
|
+
const conflictAbs = localPathForVaultKey(run.hqRoot, conflictRelative);
|
|
1466
1467
|
if (!isMaterializationPathStillContained(run.syncRoot, conflictAbs)) {
|
|
1467
1468
|
run.emit({
|
|
1468
1469
|
type: "error",
|
|
@@ -1787,7 +1788,7 @@ function collectFiles(
|
|
|
1787
1788
|
console.error(` Warning: ${p} is outside company folder, skipping.`);
|
|
1788
1789
|
continue;
|
|
1789
1790
|
}
|
|
1790
|
-
const relativePath =
|
|
1791
|
+
const relativePath = vaultKeyForLocalPath(syncRoot, absolutePath);
|
|
1791
1792
|
// Probe the filter with both isDir hints — we don't know whether
|
|
1792
1793
|
// the link's target is a file or a directory without
|
|
1793
1794
|
// stat-following the link, which we explicitly avoid (it would
|
|
@@ -1817,7 +1818,7 @@ function collectFiles(
|
|
|
1817
1818
|
if (!filter(absolutePath, true)) continue;
|
|
1818
1819
|
results.push(...walkDir(absolutePath, syncRoot, filter));
|
|
1819
1820
|
} else if (lstat.isFile()) {
|
|
1820
|
-
const relativePath =
|
|
1821
|
+
const relativePath = vaultKeyForLocalPath(syncRoot, absolutePath);
|
|
1821
1822
|
if (filter(absolutePath)) {
|
|
1822
1823
|
results.push({ kind: "file", absolutePath, relativePath });
|
|
1823
1824
|
}
|
|
@@ -1867,7 +1868,7 @@ function walkDir(
|
|
|
1867
1868
|
results.push({
|
|
1868
1869
|
kind: "symlink",
|
|
1869
1870
|
absolutePath,
|
|
1870
|
-
relativePath:
|
|
1871
|
+
relativePath: vaultKeyForLocalPath(syncRoot, absolutePath),
|
|
1871
1872
|
target: fs.readlinkSync(absolutePath),
|
|
1872
1873
|
});
|
|
1873
1874
|
continue;
|
|
@@ -1883,7 +1884,7 @@ function walkDir(
|
|
|
1883
1884
|
results.push({
|
|
1884
1885
|
kind: "file",
|
|
1885
1886
|
absolutePath,
|
|
1886
|
-
relativePath:
|
|
1887
|
+
relativePath: vaultKeyForLocalPath(syncRoot, absolutePath),
|
|
1887
1888
|
});
|
|
1888
1889
|
}
|
|
1889
1890
|
}
|
|
@@ -2002,11 +2003,11 @@ function resolveDeleteScopeRoots(
|
|
|
2002
2003
|
if (!isWithin(syncRoot, absolutePath)) continue;
|
|
2003
2004
|
const stat = fs.statSync(absolutePath);
|
|
2004
2005
|
if (!stat.isDirectory()) continue;
|
|
2005
|
-
const rel =
|
|
2006
|
+
const rel = vaultKeyForLocalPath(syncRoot, absolutePath);
|
|
2006
2007
|
if (rel === "" || rel === ".") {
|
|
2007
2008
|
return [""];
|
|
2008
2009
|
}
|
|
2009
|
-
prefixes.add(rel
|
|
2010
|
+
prefixes.add(rel);
|
|
2010
2011
|
}
|
|
2011
2012
|
return Array.from(prefixes);
|
|
2012
2013
|
}
|
|
@@ -2244,7 +2245,7 @@ async function computeDeletePlan(
|
|
|
2244
2245
|
);
|
|
2245
2246
|
if (!inScope) continue;
|
|
2246
2247
|
inScopeJournalEntries++;
|
|
2247
|
-
const localPath =
|
|
2248
|
+
const localPath = localPathForVaultKey(syncRoot, relativeKey);
|
|
2248
2249
|
let presentLocally = true;
|
|
2249
2250
|
try {
|
|
2250
2251
|
fs.lstatSync(localPath);
|