hypomnema 1.10.1 → 1.10.2
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -0
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/commands/crystallize.md +18 -12
- package/commands/upgrade.md +4 -2
- package/docs/ARCHITECTURE.md +2 -2
- package/hooks/base-store.mjs +31 -17
- package/hooks/hypo-auto-minimal-crystallize.mjs +39 -6
- package/hooks/hypo-auto-stage.mjs +123 -63
- package/hooks/hypo-close-guard.mjs +3 -37
- package/hooks/hypo-compact-guard.mjs +9 -6
- package/hooks/hypo-file-watch.mjs +18 -10
- package/hooks/hypo-personal-check.mjs +137 -55
- package/hooks/hypo-session-start.mjs +17 -6
- package/hooks/hypo-shared.mjs +1206 -504
- package/hooks/hypo-stop.mjs +8 -0
- package/package.json +1 -1
- package/scripts/crystallize.mjs +1 -1
- package/scripts/doctor.mjs +148 -8
- package/scripts/lib/crystallize-close-apply.mjs +74 -22
- package/scripts/lib/crystallize-close-check.mjs +1 -1
- package/scripts/lib/wikilink.mjs +10 -1
- package/scripts/upgrade.mjs +12 -2
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +53 -17
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from 'child_process';
|
|
9
|
-
import {
|
|
9
|
+
import { join } from 'path';
|
|
10
10
|
import {
|
|
11
11
|
HYPO_DIR,
|
|
12
12
|
loadHypoIgnore,
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
readAutoStagedRecord,
|
|
18
18
|
recordAutoStagedBlob,
|
|
19
19
|
recordTouchedPaths,
|
|
20
|
+
vaultRelatives,
|
|
21
|
+
remainingSpawnTimeoutMs,
|
|
20
22
|
vaultCommitLockTarget,
|
|
21
23
|
withFileLock,
|
|
22
24
|
} from './hypo-shared.mjs';
|
|
@@ -30,6 +32,20 @@ import { advanceBaseForWrite, hashContent } from './base-store.mjs';
|
|
|
30
32
|
// guard at close. tool_name, not file_path, is the write signal.
|
|
31
33
|
const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
|
|
32
34
|
|
|
35
|
+
// One time budget for every wait in this hook. hooks.json gives the hook 10 seconds and kills it
|
|
36
|
+
// after that, so waits that each ran to their own limit (vault lock, git, cache lock, session lock,
|
|
37
|
+
// base lock: 5 seconds apiece) could be cut off before the failure flag was written. The evidence
|
|
38
|
+
// comes first, in time as well as in budget: the ownership record (and, if it fails, the failure
|
|
39
|
+
// flag) runs before anything else, the report keeps its own share, and the side work (staging, the
|
|
40
|
+
// auto-staged cache, the base advance) uses only what the record left. A hook killed during the
|
|
41
|
+
// side work has already left its record. Dropped side work is redone or harmless: Stop stages
|
|
42
|
+
// again, a missing cache entry only means one more backup, a missing base advance parks the close
|
|
43
|
+
// on a proposal (the safe direction).
|
|
44
|
+
const AUTO_STAGE_BUDGET_MS = 8000;
|
|
45
|
+
const REPORT_SHARE_MS = 1000; // kept for the output that must still be printed
|
|
46
|
+
const deadline = { end: performance.now() + AUTO_STAGE_BUDGET_MS };
|
|
47
|
+
const until = (shareMs) => ({ end: deadline.end - shareMs });
|
|
48
|
+
|
|
33
49
|
let input = {};
|
|
34
50
|
try {
|
|
35
51
|
const raw = await new Promise((r) => {
|
|
@@ -44,75 +60,43 @@ try {
|
|
|
44
60
|
process.exit(0);
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
const
|
|
63
|
+
const givenPath = input.tool_input?.file_path ?? '';
|
|
64
|
+
// Inside the vault by a path comparison on real paths, not a string prefix: a HYPO_DIR with a
|
|
65
|
+
// trailing separator, or a vault reached through a symlink or a relative spelling, still counts.
|
|
66
|
+
// A string test that misses leaves no touched-paths record and no failure flag for a write that
|
|
67
|
+
// is really in the vault, and the close then does not see that uncommitted file as this session's.
|
|
68
|
+
const rels = typeof givenPath === 'string' && givenPath ? vaultRelatives(HYPO_DIR, givenPath) : [];
|
|
69
|
+
const rel = rels[0];
|
|
70
|
+
// The same file spelled from HYPO_DIR, for every git and ignore call below.
|
|
71
|
+
const filePath = rel === undefined ? givenPath : join(HYPO_DIR, rel);
|
|
48
72
|
|
|
49
|
-
if (
|
|
73
|
+
if (rel !== undefined) {
|
|
50
74
|
const patterns = loadHypoIgnore(HYPO_DIR);
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// an add landing in between would stage bytes it is about to set aside. A lock that cannot be
|
|
59
|
-
// taken skips the add: the session's scoped auto-commit stages its own paths, so nothing is
|
|
60
|
-
// lost.
|
|
61
|
-
try {
|
|
62
|
-
withFileLock(
|
|
63
|
-
vaultCommitLockTarget(HYPO_DIR),
|
|
64
|
-
() => {
|
|
65
|
-
const rel = relative(HYPO_DIR, filePath);
|
|
66
|
-
// A merge in progress (open conflict, or a resolution waiting for its commit): staging
|
|
67
|
-
// now would change that resolution. The path is still in the session's touched set
|
|
68
|
-
// below, so Stop commits it once the merge is done.
|
|
69
|
-
if (
|
|
70
|
-
spawnSync('git', ['-C', HYPO_DIR, 'rev-parse', '-q', '--verify', 'MERGE_HEAD'], {
|
|
71
|
-
stdio: 'ignore',
|
|
72
|
-
}).status === 0
|
|
73
|
-
)
|
|
74
|
-
return;
|
|
75
|
-
// A version a person already staged for this path must reach the commit intact, so that
|
|
76
|
-
// it can be backed up there: `git add` would put the tool's bytes over it first. The
|
|
77
|
-
// path is left unstaged (it is still in the session's touched set below, so Stop commits
|
|
78
|
-
// it). A staged deletion (`git rm`) is skipped the same way: `git add` would turn it into a
|
|
79
|
-
// modification. With no answer from git about the index, skip too: Stop stages it either
|
|
80
|
-
// way.
|
|
81
|
-
const before = indexAndHeadBlobs(HYPO_DIR, rel);
|
|
82
|
-
if (
|
|
83
|
-
!before ||
|
|
84
|
-
isStagedDeletion(before.head, before.indexed) ||
|
|
85
|
-
(before.index &&
|
|
86
|
-
before.index !== before.head &&
|
|
87
|
-
!isAutoStagedBlob(readAutoStagedRecord(HYPO_DIR), rel, before.index, before.head))
|
|
88
|
-
)
|
|
89
|
-
return;
|
|
90
|
-
const add = spawnSync('git', ['-C', HYPO_DIR, 'add', filePath], { stdio: 'ignore' });
|
|
91
|
-
if (add.status !== 0) return;
|
|
92
|
-
// Record the blob this hook staged (and the HEAD blob it sat on), so the commit can tell
|
|
93
|
-
// it from one a person staged later and does not back up Claude's own write.
|
|
94
|
-
// Best-effort: with no record the commit saves the blob as it would for a person's.
|
|
95
|
-
const after = indexAndHeadBlobs(HYPO_DIR, rel);
|
|
96
|
-
if (after?.index) recordAutoStagedBlob(HYPO_DIR, rel, after.index, before.head);
|
|
97
|
-
},
|
|
98
|
-
{ timeoutMs: Number(process.env.HYPO_VAULT_LOCK_TIMEOUT_MS) || 5000 },
|
|
99
|
-
);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
process.stderr.write(
|
|
102
|
-
`[hypo-auto-stage] 볼트 잠금을 못 잡아 ${filePath} 의 git add 를 건너뛰었습니다 (${err?.code ?? err?.message})\n`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
75
|
+
// A pattern may name the symlink or the directory it points to, so every spelling is checked
|
|
76
|
+
// (hypo-file-watch does the same). Staging and recording use the real path, so an ignored alias
|
|
77
|
+
// would otherwise bring an ignored directory's content into the commit through its other name.
|
|
78
|
+
const ignoredSpellings =
|
|
79
|
+
patterns.length === 0
|
|
80
|
+
? []
|
|
81
|
+
: rels.filter((r) => isIgnored(join(HYPO_DIR, r), HYPO_DIR, patterns));
|
|
107
82
|
if (WRITE_TOOLS.has(input.tool_name)) {
|
|
108
|
-
const rel = relative(HYPO_DIR, filePath);
|
|
109
|
-
|
|
110
83
|
// Accumulate this write into the session's scoped auto-commit
|
|
111
84
|
// set, keyed by session_id (no-op without one; never a shared bucket).
|
|
112
85
|
// hypo-auto-commit.mjs drains this at Stop instead of sweeping the whole
|
|
113
86
|
// working tree, so another session's concurrent writes to this vault
|
|
114
87
|
// never land in THIS session's commit.
|
|
115
|
-
|
|
88
|
+
// The record runs first, before staging: a hook killed while it stages (the harness timeout, a
|
|
89
|
+
// git that hangs) has then already recorded the write, and nothing staged is left unclaimed. It
|
|
90
|
+
// and a failure flag may use everything up to the report share; staging gets what is left.
|
|
91
|
+
// A path that is ignored only under another spelling is not recorded: the commit would
|
|
92
|
+
// check the real path alone and take it. One ignored under its real path is still recorded
|
|
93
|
+
// (the commit leaves it out and the close gate reports it).
|
|
94
|
+
const aliasOnlyIgnored = ignoredSpellings.length > 0 && !ignoredSpellings.includes(rel);
|
|
95
|
+
const recorded =
|
|
96
|
+
aliasOnlyIgnored ||
|
|
97
|
+
recordTouchedPaths(HYPO_DIR, input.session_id, rel, {
|
|
98
|
+
deadline: until(REPORT_SHARE_MS),
|
|
99
|
+
});
|
|
116
100
|
if (!recorded) {
|
|
117
101
|
// A lock-timeout or write failure here leaves the file just written
|
|
118
102
|
// with nothing in ANY session's touched-paths set to bring it into an
|
|
@@ -126,6 +110,77 @@ if (filePath.startsWith(HYPO_DIR + '/') || filePath === HYPO_DIR) {
|
|
|
126
110
|
);
|
|
127
111
|
}
|
|
128
112
|
|
|
113
|
+
// A path ignored under any of its spellings is recorded (above) but never staged.
|
|
114
|
+
if (ignoredSpellings.length === 0) {
|
|
115
|
+
// Under the vault commit lock, like every other writer of the index: the catch-up pre-step
|
|
116
|
+
// (clearGeneratedPathsBlockingPull) reads and releases the index while it holds that lock, and
|
|
117
|
+
// an add landing in between would stage bytes it is about to set aside. A lock that cannot be
|
|
118
|
+
// taken skips the add: the session's scoped auto-commit stages its own paths, so nothing is
|
|
119
|
+
// lost. Every wait below is cut to what the record left of the hook budget; with no time left
|
|
120
|
+
// the add is skipped the same way.
|
|
121
|
+
const staging = until(REPORT_SHARE_MS);
|
|
122
|
+
const lockWaitMs = Math.min(
|
|
123
|
+
Number(process.env.HYPO_VAULT_LOCK_TIMEOUT_MS) || 5000,
|
|
124
|
+
remainingSpawnTimeoutMs(staging),
|
|
125
|
+
);
|
|
126
|
+
try {
|
|
127
|
+
if (lockWaitMs > 0)
|
|
128
|
+
withFileLock(
|
|
129
|
+
vaultCommitLockTarget(HYPO_DIR),
|
|
130
|
+
() => {
|
|
131
|
+
// A merge in progress (open conflict, or a resolution waiting for its commit): staging
|
|
132
|
+
// now would change that resolution. The path is already in the session's touched
|
|
133
|
+
// set, so Stop commits it once the merge is done. A check that got no answer in time
|
|
134
|
+
// is treated the same: staging on a guess could change the resolution.
|
|
135
|
+
const mergeTimeout = remainingSpawnTimeoutMs(staging);
|
|
136
|
+
if (mergeTimeout === 0) return;
|
|
137
|
+
const merging = spawnSync(
|
|
138
|
+
'git',
|
|
139
|
+
['-C', HYPO_DIR, 'rev-parse', '-q', '--verify', 'MERGE_HEAD'],
|
|
140
|
+
{ stdio: 'ignore', timeout: mergeTimeout },
|
|
141
|
+
);
|
|
142
|
+
if (merging.status === 0 || merging.error) return;
|
|
143
|
+
// A version a person already staged for this path must reach the commit intact, so that
|
|
144
|
+
// it can be backed up there: `git add` would put the tool's bytes over it first. The
|
|
145
|
+
// path is left unstaged (it is already in the session's touched set, so Stop commits
|
|
146
|
+
// it). A staged deletion (`git rm`) is skipped the same way: `git add` would turn it into a
|
|
147
|
+
// modification. With no answer from git about the index, skip too: Stop stages it either
|
|
148
|
+
// way.
|
|
149
|
+
const before = indexAndHeadBlobs(HYPO_DIR, rel, { deadline: staging });
|
|
150
|
+
if (
|
|
151
|
+
!before ||
|
|
152
|
+
isStagedDeletion(before.head, before.indexed) ||
|
|
153
|
+
(before.index &&
|
|
154
|
+
before.index !== before.head &&
|
|
155
|
+
!isAutoStagedBlob(readAutoStagedRecord(HYPO_DIR), rel, before.index, before.head))
|
|
156
|
+
)
|
|
157
|
+
return;
|
|
158
|
+
const addTimeout = remainingSpawnTimeoutMs(staging);
|
|
159
|
+
if (addTimeout === 0) return;
|
|
160
|
+
const add = spawnSync('git', ['-C', HYPO_DIR, 'add', filePath], {
|
|
161
|
+
stdio: 'ignore',
|
|
162
|
+
timeout: addTimeout,
|
|
163
|
+
});
|
|
164
|
+
if (add.status !== 0) return;
|
|
165
|
+
// Record the blob this hook staged (and the HEAD blob it sat on), so the commit can tell
|
|
166
|
+
// it from one a person staged later and does not back up Claude's own write.
|
|
167
|
+
// Best-effort: with no record the commit saves the blob as it would for a person's.
|
|
168
|
+
const after = indexAndHeadBlobs(HYPO_DIR, rel, { deadline: staging });
|
|
169
|
+
const cacheWaitMs = Math.min(5000, remainingSpawnTimeoutMs(staging));
|
|
170
|
+
if (after?.index && cacheWaitMs > 0)
|
|
171
|
+
recordAutoStagedBlob(HYPO_DIR, rel, after.index, before.head, {
|
|
172
|
+
timeoutMs: cacheWaitMs,
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
{ timeoutMs: lockWaitMs },
|
|
176
|
+
);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
`[hypo-auto-stage] 볼트 잠금을 못 잡아 ${filePath} 의 git add 를 건너뛰었습니다 (${err?.code ?? err?.message})\n`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
129
184
|
// Write=proposal gate provenance: when this session's own write lands on one of
|
|
130
185
|
// the overwrite targets it snapshotted at start, advance that target's base so
|
|
131
186
|
// the close guard reads the change as "I wrote this", not "someone else did"
|
|
@@ -142,7 +197,12 @@ if (filePath.startsWith(HYPO_DIR + '/') || filePath === HYPO_DIR) {
|
|
|
142
197
|
input.tool_name === 'Write' && typeof input.tool_input?.content === 'string'
|
|
143
198
|
? hashContent(input.tool_input.content)
|
|
144
199
|
: null;
|
|
145
|
-
|
|
200
|
+
// Only what is left after the report share; none left skips the advance.
|
|
201
|
+
const baseWaitMs = remainingSpawnTimeoutMs(until(REPORT_SHARE_MS));
|
|
202
|
+
if (baseWaitMs > 0)
|
|
203
|
+
advanceBaseForWrite(HYPO_DIR, input.session_id, rel, filePath, known, {
|
|
204
|
+
timeoutMs: Math.min(5000, baseWaitMs),
|
|
205
|
+
});
|
|
146
206
|
}
|
|
147
207
|
}
|
|
148
208
|
}
|
|
@@ -49,9 +49,8 @@
|
|
|
49
49
|
* before anything else runs.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import { HYPO_DIR, isGateSkipped } from './hypo-shared.mjs';
|
|
52
|
+
import { isAbsolute } from 'path';
|
|
53
|
+
import { HYPO_DIR, isGateSkipped, vaultRelatives } from './hypo-shared.mjs';
|
|
55
54
|
import { isGeneratedViewPath, isSessionEntryPath } from './session-entries.mjs';
|
|
56
55
|
|
|
57
56
|
// Mirrors hypo-auto-stage.mjs's WRITE_TOOLS: the tools that replace file bytes.
|
|
@@ -61,39 +60,6 @@ const GENERATED_DENY =
|
|
|
61
60
|
'생성 파일입니다. close payload의 summary/tracks로 기록하세요 (직접 쓰면 다음 close가 덮어씁니다).';
|
|
62
61
|
const ENTRY_DENY = '원본 항목은 close apply만 만들고 고치지 않습니다.';
|
|
63
62
|
|
|
64
|
-
// The vault-relative spellings of `filePath` that are inside `root`: the path as given and, when
|
|
65
|
-
// the files exist, their real paths (a symlinked vault written through its real path). A string
|
|
66
|
-
// prefix test is not used: a trailing separator on HYPO_DIR or a symlink would switch it off.
|
|
67
|
-
function insideRelatives(root, filePath) {
|
|
68
|
-
const real = (p) => {
|
|
69
|
-
try {
|
|
70
|
-
return realpathSync(p);
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
// The real path of the nearest ancestor that exists, with the segments below it put back: a file
|
|
76
|
-
// that does not exist yet, in a directory that does not either, still resolves. `dirname` reaches
|
|
77
|
-
// a fixed point at the filesystem root, which ends the walk (`null` when not even that resolves).
|
|
78
|
-
const realThroughAncestor = (p) => {
|
|
79
|
-
const rest = [];
|
|
80
|
-
for (let d = p; ; d = dirname(d)) {
|
|
81
|
-
const r = real(d);
|
|
82
|
-
if (r !== null) return join(r, ...rest);
|
|
83
|
-
if (dirname(d) === d) return null;
|
|
84
|
-
rest.unshift(basename(d));
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
const pairs = [[root, filePath]];
|
|
88
|
-
const realRoot = real(root);
|
|
89
|
-
const realFile = realThroughAncestor(filePath);
|
|
90
|
-
if (realRoot !== null && realFile !== null) pairs.push([realRoot, realFile]);
|
|
91
|
-
return pairs
|
|
92
|
-
.map(([r, f]) => relative(r, f))
|
|
93
|
-
.filter((rel) => rel !== '' && rel.split(sep)[0] !== '..' && !isAbsolute(rel))
|
|
94
|
-
.map((rel) => rel.replace(/\\/g, '/'));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
63
|
let input = {};
|
|
98
64
|
try {
|
|
99
65
|
const raw = await new Promise((r) => {
|
|
@@ -120,7 +86,7 @@ try {
|
|
|
120
86
|
const root = HYPO_DIR.replace(/[\\/]+$/, '') || HYPO_DIR;
|
|
121
87
|
let rel = null;
|
|
122
88
|
let reason = null;
|
|
123
|
-
for (const candidate of
|
|
89
|
+
for (const candidate of vaultRelatives(root, filePath)) {
|
|
124
90
|
const spellings = [candidate, candidate.toLowerCase()];
|
|
125
91
|
reason = spellings.some(isGeneratedViewPath)
|
|
126
92
|
? GENERATED_DENY
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
* This hook never calls precompactGateStatus: its own hooks.json timeout is
|
|
17
17
|
* 10s and the gate's lint spawn alone budgets 30s, three times over. Instead
|
|
18
18
|
* it re-checks only the cheap axes (session log, git, hot.md) directly, and
|
|
19
|
-
* narrows the git axis with
|
|
20
|
-
*
|
|
21
|
-
* a different project's dangling close file does not fire a false alarm here
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* narrows the git axis with a project-vs-foreign path rule
|
|
20
|
+
* (isForeignProjectFile / classifyForeignOnlyDirty in hypo-shared.mjs), so
|
|
21
|
+
* a different project's dangling close file does not fire a false alarm here.
|
|
22
|
+
* The gate no longer uses that rule: it judges the git axis by this session's
|
|
23
|
+
* touched-paths record, so the two now differ. The record is a per-session
|
|
24
|
+
* file under .cache/sessions, which this hook does not read, and it never
|
|
25
|
+
* parses the transcript either, to stay inside the 10s budget. Only the
|
|
26
|
+
* cheap path-prefix axis runs here. Moving this hook onto the record is a
|
|
27
|
+
* follow-up, not part of the gate change.
|
|
25
28
|
*/
|
|
26
29
|
|
|
27
30
|
import {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
currentDevice,
|
|
21
21
|
scopeVisible,
|
|
22
22
|
readVisibilityScope,
|
|
23
|
+
vaultRelatives,
|
|
23
24
|
} from './hypo-shared.mjs';
|
|
24
25
|
|
|
25
26
|
const MAX_CHARS = 2000;
|
|
@@ -34,12 +35,20 @@ process.stdin.on('end', () => {
|
|
|
34
35
|
data = JSON.parse(raw);
|
|
35
36
|
} catch {}
|
|
36
37
|
|
|
37
|
-
const
|
|
38
|
+
const givenPath = data.file_path || data.path || '';
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
// Inside the vault by a path comparison on real paths, not a string prefix: a HYPO_DIR with a
|
|
41
|
+
// trailing separator, or a vault reached through a symlink, still counts. Every check below
|
|
42
|
+
// then uses the file spelled from HYPO_DIR. The .hypoignore guard checks every spelling
|
|
43
|
+
// (a pattern may name the symlink or the directory it points to); the real-path one is read.
|
|
44
|
+
const rels =
|
|
45
|
+
typeof givenPath === 'string' && givenPath ? vaultRelatives(HYPO_DIR, givenPath) : [];
|
|
46
|
+
if (rels.length === 0) {
|
|
40
47
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
41
48
|
return;
|
|
42
49
|
}
|
|
50
|
+
const rel = rels[0];
|
|
51
|
+
const filePath = join(HYPO_DIR, rel);
|
|
43
52
|
|
|
44
53
|
// Privacy guard: refuse to emit .hypoignore-matched paths. Without this,
|
|
45
54
|
// `.env*` or other secrets under HYPO_DIR are re-emitted in this hook's
|
|
@@ -48,7 +57,7 @@ process.stdin.on('end', () => {
|
|
|
48
57
|
// FileChanged actually appears on a given machine is unmeasured, so the
|
|
49
58
|
// only safe assumption is that a matched path must never be put in it.
|
|
50
59
|
const patterns = loadHypoIgnore(HYPO_DIR);
|
|
51
|
-
if (patterns.length > 0 && isIgnored(
|
|
60
|
+
if (patterns.length > 0 && rels.some((r) => isIgnored(join(HYPO_DIR, r), HYPO_DIR, patterns))) {
|
|
52
61
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
53
62
|
return;
|
|
54
63
|
}
|
|
@@ -71,7 +80,7 @@ process.stdin.on('end', () => {
|
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
const content = fileRaw.slice(0, MAX_CHARS);
|
|
74
|
-
const relPath =
|
|
83
|
+
const relPath = rel;
|
|
75
84
|
|
|
76
85
|
// Built inline rather than through buildOutput(): the hook reference's
|
|
77
86
|
// FileChanged section documents the same two fields as its CwdChanged
|
|
@@ -95,12 +104,11 @@ process.stdin.on('end', () => {
|
|
|
95
104
|
// this package returns watchPaths to seed that list. The watch list is
|
|
96
105
|
// session-global though, not scoped to this plugin: if another hook or
|
|
97
106
|
// plugin seeds it, this matcher-less group fires on every watched file,
|
|
98
|
-
// not just wiki ones. The
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// still starts with HYPO_DIR and passes the check above, but
|
|
107
|
+
// not just wiki ones. The vaultRelatives check above still returns
|
|
108
|
+
// immediately for any path that is not inside HYPO_DIR (as given or by its
|
|
109
|
+
// real path), so a watch seeded elsewhere cannot point this hook at a path
|
|
110
|
+
// outside the wiki. A symlink living inside the wiki is a separate case:
|
|
111
|
+
// its path is inside HYPO_DIR as given and passes the check above, but
|
|
104
112
|
// readFileSync follows it, so whatever that link points to, inside the
|
|
105
113
|
// wiki or not, is what actually gets emitted.
|
|
106
114
|
console.log(
|