@losthex/scribe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/config.js +102 -0
- package/dist/config.js.map +1 -0
- package/dist/git.js +110 -0
- package/dist/git.js.map +1 -0
- package/dist/hook.js +115 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.js +491 -0
- package/dist/index.js.map +1 -0
- package/dist/install.js +140 -0
- package/dist/install.js.map +1 -0
- package/dist/keychain.js +181 -0
- package/dist/keychain.js.map +1 -0
- package/dist/lock.js +59 -0
- package/dist/lock.js.map +1 -0
- package/dist/login.js +99 -0
- package/dist/login.js.map +1 -0
- package/dist/mcp.js +138 -0
- package/dist/mcp.js.map +1 -0
- package/dist/paths.js +28 -0
- package/dist/paths.js.map +1 -0
- package/dist/pkce.js +137 -0
- package/dist/pkce.js.map +1 -0
- package/dist/queue.js +223 -0
- package/dist/queue.js.map +1 -0
- package/dist/send.js +151 -0
- package/dist/send.js.map +1 -0
- package/dist/tokens.js +106 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Scribe CLI
|
|
2
|
+
|
|
3
|
+
Lost Hex's command-line client for Scribe.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --global @losthex/scribe
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Use the onboarding command provided for your engagement, then sign in:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
scribe install --project <project> --match <repo> \
|
|
17
|
+
--issuer <issuer-url> --amend-url <amend-url>
|
|
18
|
+
scribe login
|
|
19
|
+
scribe status
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
scribe login
|
|
26
|
+
scribe install
|
|
27
|
+
scribe capture
|
|
28
|
+
scribe flush
|
|
29
|
+
scribe amend "<note>"
|
|
30
|
+
scribe status
|
|
31
|
+
scribe log
|
|
32
|
+
scribe uninstall
|
|
33
|
+
```
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EMPTY_CONFIG = void 0;
|
|
4
|
+
exports.loadConfig = loadConfig;
|
|
5
|
+
exports.saveConfig = saveConfig;
|
|
6
|
+
exports.upsertEngagement = upsertEngagement;
|
|
7
|
+
exports.globMatch = globMatch;
|
|
8
|
+
exports.matchEngagement = matchEngagement;
|
|
9
|
+
exports.resolveSetting = resolveSetting;
|
|
10
|
+
exports.requireSetting = requireSetting;
|
|
11
|
+
/**
|
|
12
|
+
* `~/.config/scribe/config.json` — what `scribe install` wrote down, and
|
|
13
|
+
* the one place that decides which repos the collector may look at.
|
|
14
|
+
*
|
|
15
|
+
* Engagement matching is a deliberate allowlist (DATA-ITD-005): a commit
|
|
16
|
+
* is captured only when its repo matches an engagement the engineer
|
|
17
|
+
* installed, so nothing leaves the machine for personal projects, other
|
|
18
|
+
* clients, or the many repos that happen to sit on the same laptop. There
|
|
19
|
+
* is no server-side list to fetch — the amend endpoint is write-only by
|
|
20
|
+
* design — so the patterns are local config, set once at onboarding.
|
|
21
|
+
*/
|
|
22
|
+
const node_fs_1 = require("node:fs");
|
|
23
|
+
const node_path_1 = require("node:path");
|
|
24
|
+
const paths_1 = require("./paths");
|
|
25
|
+
exports.EMPTY_CONFIG = { engagements: [] };
|
|
26
|
+
function loadConfig(paths = (0, paths_1.resolvePaths)()) {
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = (0, node_fs_1.readFileSync)(paths.config, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ...exports.EMPTY_CONFIG };
|
|
33
|
+
}
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
return {
|
|
36
|
+
...parsed,
|
|
37
|
+
engagements: (parsed.engagements ?? []).map((e) => ({
|
|
38
|
+
project: e.project,
|
|
39
|
+
match: [...(e.match ?? [])],
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function saveConfig(config, paths = (0, paths_1.resolvePaths)()) {
|
|
44
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(paths.config), { recursive: true });
|
|
45
|
+
(0, node_fs_1.writeFileSync)(paths.config, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Adds or extends an engagement, keeping `match` de-duplicated so
|
|
49
|
+
* re-running `scribe install` is idempotent.
|
|
50
|
+
*/
|
|
51
|
+
function upsertEngagement(config, project, match) {
|
|
52
|
+
const engagements = [...config.engagements];
|
|
53
|
+
const existing = engagements.findIndex((e) => e.project === project);
|
|
54
|
+
const patterns = existing >= 0 ? engagements[existing].match : [];
|
|
55
|
+
const merged = [...new Set([...patterns, ...match])];
|
|
56
|
+
const entry = { project, match: merged };
|
|
57
|
+
if (existing >= 0)
|
|
58
|
+
engagements[existing] = entry;
|
|
59
|
+
else
|
|
60
|
+
engagements.push(entry);
|
|
61
|
+
return { ...config, engagements };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Case-insensitive match with `*` as the only metacharacter, unanchored —
|
|
65
|
+
* `bonterratech` and `git@*:bonterratech/*` both match the same remote,
|
|
66
|
+
* because an engineer writing a pattern is describing a repo, not authoring
|
|
67
|
+
* a regex.
|
|
68
|
+
*/
|
|
69
|
+
function globMatch(pattern, value) {
|
|
70
|
+
const needle = pattern.trim().toLowerCase();
|
|
71
|
+
const haystack = value.toLowerCase();
|
|
72
|
+
if (needle.length === 0)
|
|
73
|
+
return false;
|
|
74
|
+
if (!needle.includes('*'))
|
|
75
|
+
return haystack.includes(needle);
|
|
76
|
+
const escaped = needle.replace(/[.+^${}()|[\]\\?]/g, '\\$&').replace(/\*/g, '.*');
|
|
77
|
+
return new RegExp(escaped).test(haystack);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The first engagement any of `identifiers` matches, or null — null means
|
|
81
|
+
* "not a mandated engagement", which means nothing is captured.
|
|
82
|
+
*/
|
|
83
|
+
function matchEngagement(engagements, identifiers) {
|
|
84
|
+
const candidates = identifiers.filter((value) => Boolean(value));
|
|
85
|
+
for (const engagement of engagements) {
|
|
86
|
+
for (const pattern of engagement.match) {
|
|
87
|
+
if (candidates.some((candidate) => globMatch(pattern, candidate)))
|
|
88
|
+
return engagement;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/** Flag beats environment beats config, so a one-off override is easy. */
|
|
94
|
+
function resolveSetting(flag, envValue, configured) {
|
|
95
|
+
return flag ?? envValue ?? configured;
|
|
96
|
+
}
|
|
97
|
+
function requireSetting(value, name, hint) {
|
|
98
|
+
if (!value)
|
|
99
|
+
throw new Error(`${name} is not configured — ${hint}`);
|
|
100
|
+
return value.replace(/\/+$/, '');
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAoDA,gCAeC;AAED,gCAGC;AAMD,4CASC;AAQD,8BAOC;AAMD,0CAWC;AAGD,wCAMC;AAED,wCAGC;AArID;;;;;;;;;;GAUG;AACH,qCAAiE;AACjE,yCAAoC;AAEpC,mCAAmD;AAoCtC,QAAA,YAAY,GAAW,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;AAExD,SAAgB,UAAU,CAAC,QAAe,IAAA,oBAAY,GAAE;IACtD,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,IAAA,sBAAY,EAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,GAAG,oBAAY,EAAE,CAAC;IAC7B,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAoB,CAAC;IAClD,OAAO;QACL,GAAG,MAAM;QACT,WAAW,EAAE,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAClD,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;SAC5B,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED,SAAgB,UAAU,CAAC,MAAc,EAAE,QAAe,IAAA,oBAAY,GAAE;IACtE,IAAA,mBAAS,EAAC,IAAA,mBAAO,EAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,IAAA,uBAAa,EAAC,KAAK,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACvF,CAAC;AAED;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,MAAc,EAAE,OAAe,EAAE,KAAe;IAC/E,MAAM,WAAW,GAAG,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5C,MAAM,QAAQ,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC;IACrE,MAAM,QAAQ,GAAG,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACzC,IAAI,QAAQ,IAAI,CAAC;QAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC;;QAC5C,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,OAAO,EAAE,GAAG,MAAM,EAAE,WAAW,EAAE,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,SAAgB,SAAS,CAAC,OAAe,EAAE,KAAa;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC5C,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAClF,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe,CAC7B,WAAyB,EACzB,WAA0C;IAE1C,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAClF,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,KAAK,MAAM,OAAO,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;YACvC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;gBAAE,OAAO,UAAU,CAAC;QACvF,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0EAA0E;AAC1E,SAAgB,cAAc,CAC5B,IAAwB,EACxB,QAA4B,EAC5B,UAA8B;IAE9B,OAAO,IAAI,IAAI,QAAQ,IAAI,UAAU,CAAC;AACxC,CAAC;AAED,SAAgB,cAAc,CAAC,KAAyB,EAAE,IAAY,EAAE,IAAY;IAClF,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,wBAAwB,IAAI,EAAE,CAAC,CAAC;IACnE,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC"}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scrubRemote = scrubRemote;
|
|
4
|
+
exports.repoIdentity = repoIdentity;
|
|
5
|
+
exports.commitFacts = commitFacts;
|
|
6
|
+
exports.gitConfig = gitConfig;
|
|
7
|
+
exports.setGitConfig = setGitConfig;
|
|
8
|
+
exports.unsetGitConfig = unsetGitConfig;
|
|
9
|
+
/**
|
|
10
|
+
* The only git the collector runs, and the only data it reads.
|
|
11
|
+
*
|
|
12
|
+
* Every command here answers one of two questions: which repo is this, and
|
|
13
|
+
* what did the commit that just landed look like from the outside. That is
|
|
14
|
+
* the same shape the server-side collector pulls from the GitHub/GitLab
|
|
15
|
+
* APIs for engagements Lost Hex can read (DATA-ITD-005) — repo, branch,
|
|
16
|
+
* sha, subject, timestamp, and a *count* of files. No file names, no
|
|
17
|
+
* diffs, no message bodies: `git show --stat` and friends are deliberately
|
|
18
|
+
* absent from this file.
|
|
19
|
+
*/
|
|
20
|
+
const node_child_process_1 = require("node:child_process");
|
|
21
|
+
function runGit(args, cwd) {
|
|
22
|
+
return (0, node_child_process_1.execFileSync)('git', args, {
|
|
23
|
+
cwd,
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
26
|
+
}).trim();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Strips any credentials a remote URL might carry — an installed hook
|
|
30
|
+
* should never queue `https://user:token@host/...` for transmission.
|
|
31
|
+
*/
|
|
32
|
+
function scrubRemote(remote) {
|
|
33
|
+
return remote.replace(/^([a-z+]+:\/\/)[^@/]*@/i, '$1');
|
|
34
|
+
}
|
|
35
|
+
function repoIdentity({ cwd = process.cwd(), run = runGit, } = {}) {
|
|
36
|
+
let root;
|
|
37
|
+
try {
|
|
38
|
+
root = run(['rev-parse', '--show-toplevel'], cwd);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (!root)
|
|
44
|
+
return null;
|
|
45
|
+
const quiet = (args) => {
|
|
46
|
+
try {
|
|
47
|
+
const value = run(args, cwd);
|
|
48
|
+
return value.length > 0 ? value : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const remote = quiet(['config', '--get', 'remote.origin.url']);
|
|
55
|
+
return {
|
|
56
|
+
root,
|
|
57
|
+
name: root.split('/').filter(Boolean).pop() ?? root,
|
|
58
|
+
remote: remote ? scrubRemote(remote) : null,
|
|
59
|
+
branch: quiet(['rev-parse', '--abbrev-ref', 'HEAD']),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Facts about `ref` (HEAD by default). The subject is `%s`, which git
|
|
64
|
+
* already limits to the first line of the message — the body, where people
|
|
65
|
+
* paste stack traces and snippets, never enters the payload.
|
|
66
|
+
*/
|
|
67
|
+
function commitFacts({ cwd = process.cwd(), run = runGit } = {}, ref = 'HEAD') {
|
|
68
|
+
let raw;
|
|
69
|
+
try {
|
|
70
|
+
raw = run(['log', '-1', '--no-color', '--format=%H%x00%s%x00%aI', ref], cwd);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const [hash, subject, at] = raw.split('\0');
|
|
76
|
+
if (!hash || !at)
|
|
77
|
+
return null;
|
|
78
|
+
let filesChanged = 0;
|
|
79
|
+
try {
|
|
80
|
+
// A count, never the names. `-m --first-parent` gives a merge commit a
|
|
81
|
+
// sane number instead of nothing.
|
|
82
|
+
const names = run(['show', '--pretty=format:', '--name-only', '-m', '--first-parent', ref], cwd);
|
|
83
|
+
filesChanged = new Set(names.split('\n').filter((line) => line.trim().length > 0)).size;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
filesChanged = 0;
|
|
87
|
+
}
|
|
88
|
+
return { hash, subject: subject || '(no subject)', at, filesChanged };
|
|
89
|
+
}
|
|
90
|
+
/** The value of a git config key, looked up in the given scope. */
|
|
91
|
+
function gitConfig(key, scope = 'global') {
|
|
92
|
+
try {
|
|
93
|
+
return runGit(['config', `--${scope}`, '--get', key], process.cwd()) || null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function setGitConfig(key, value, scope = 'global') {
|
|
100
|
+
runGit(['config', `--${scope}`, key, value], process.cwd());
|
|
101
|
+
}
|
|
102
|
+
function unsetGitConfig(key, scope = 'global') {
|
|
103
|
+
try {
|
|
104
|
+
runGit(['config', `--${scope}`, '--unset', key], process.cwd());
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Already unset.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
|
package/dist/hook.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The post-commit shim `scribe install` plants, and the rules for planting
|
|
4
|
+
* it without breaking anything that was already there.
|
|
5
|
+
*
|
|
6
|
+
* Why a hook at all: capture is event-driven because hooks solve repo
|
|
7
|
+
* discovery for free (SPEC-002) — a post-commit hook fires inside whatever
|
|
8
|
+
* checkout the commit landed in, so a clone made months after onboarding
|
|
9
|
+
* self-registers on its first commit, with no path registry to go stale and
|
|
10
|
+
* no filesystem sweeps across a machine holding client property.
|
|
11
|
+
*
|
|
12
|
+
* Why chaining is the hard requirement: `core.hooksPath` is global and it
|
|
13
|
+
* *overrides* per-repo hooks rather than adding to them, and client repos
|
|
14
|
+
* routinely carry their own (husky and friends). The shim therefore runs,
|
|
15
|
+
* in order: the hook that already lived at this path (adopted at install
|
|
16
|
+
* time), the repo's own `.git/hooks/post-commit` that the global path would
|
|
17
|
+
* otherwise silence, and only then Scribe's capture — whose failure can
|
|
18
|
+
* never change the commit's outcome.
|
|
19
|
+
*
|
|
20
|
+
* Everything here is a pure string/plan function; install.ts does the IO.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.ADOPTED_NAME = exports.HOOK_MARKER = void 0;
|
|
24
|
+
exports.postCommitHook = postCommitHook;
|
|
25
|
+
exports.isScribeHook = isScribeHook;
|
|
26
|
+
exports.planHookInstall = planHookInstall;
|
|
27
|
+
/** Version marker — identifies our own hook on reinstall and uninstall. */
|
|
28
|
+
exports.HOOK_MARKER = '# scribe-local-collector v1';
|
|
29
|
+
/** Where an adopted pre-existing hook is kept, and chained from. */
|
|
30
|
+
exports.ADOPTED_NAME = 'post-commit.pre-scribe';
|
|
31
|
+
/**
|
|
32
|
+
* The shim itself: POSIX sh, no dependencies, and fail-open by
|
|
33
|
+
* construction — a broken or missing `scribe` leaves the commit exactly as
|
|
34
|
+
* it was.
|
|
35
|
+
*
|
|
36
|
+
* `scribe capture` is called with the binary's absolute path because GUI
|
|
37
|
+
* git clients hand hooks a minimal PATH; `command -v scribe` is the
|
|
38
|
+
* fallback for the day the CLI moves.
|
|
39
|
+
*/
|
|
40
|
+
function postCommitHook({ scribeBin, scribeHome = null }) {
|
|
41
|
+
const home = scribeHome ? `SCRIBE_HOME="${scribeHome}"\nexport SCRIBE_HOME\n` : '';
|
|
42
|
+
return `#!/bin/sh
|
|
43
|
+
${exports.HOOK_MARKER} — installed by \`scribe install\` (DATA-ITD-005).
|
|
44
|
+
#
|
|
45
|
+
# Captures commit metadata for configured no-access engagements: repo,
|
|
46
|
+
# branch, sha, subject, timestamp, and a count of files touched. Never
|
|
47
|
+
# diffs, never file names, never code — \`scribe log\` prints everything
|
|
48
|
+
# that was ever sent.
|
|
49
|
+
#
|
|
50
|
+
# Order matters. Anything that was here before scribe runs first, then the
|
|
51
|
+
# repo's own hook (which a global core.hooksPath would otherwise silence),
|
|
52
|
+
# then scribe. The commit's outcome is whatever those hooks decided; scribe
|
|
53
|
+
# never changes it.
|
|
54
|
+
set -u
|
|
55
|
+
|
|
56
|
+
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
|
57
|
+
status=0
|
|
58
|
+
|
|
59
|
+
if [ -x "$here/${exports.ADOPTED_NAME}" ]; then
|
|
60
|
+
"$here/${exports.ADOPTED_NAME}" "$@" || status=$?
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# .git/hooks is resolved from the git dir, not via --git-path hooks, which
|
|
64
|
+
# would answer with core.hooksPath — this very directory.
|
|
65
|
+
repo_git_dir=$(git rev-parse --absolute-git-dir 2>/dev/null) || repo_git_dir=""
|
|
66
|
+
if [ -n "$repo_git_dir" ] && [ -x "$repo_git_dir/hooks/post-commit" ]; then
|
|
67
|
+
repo_hooks=$(CDPATH= cd -- "$repo_git_dir/hooks" && pwd -P)
|
|
68
|
+
if [ "$repo_hooks" != "$here" ]; then
|
|
69
|
+
"$repo_git_dir/hooks/post-commit" "$@" || status=$?
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
${home}SCRIBE_BIN="${scribeBin}"
|
|
74
|
+
if [ ! -x "$SCRIBE_BIN" ]; then
|
|
75
|
+
SCRIBE_BIN=$(command -v scribe 2>/dev/null) || SCRIBE_BIN=""
|
|
76
|
+
fi
|
|
77
|
+
if [ -n "$SCRIBE_BIN" ]; then
|
|
78
|
+
"$SCRIBE_BIN" capture --quiet >/dev/null 2>&1 || true
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
exit $status
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
/** Whether a hook file on disk is one of ours. */
|
|
85
|
+
function isScribeHook(contents) {
|
|
86
|
+
return contents.includes(exports.HOOK_MARKER);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decides what installing means on this machine, without touching it.
|
|
90
|
+
*
|
|
91
|
+
* The rule that matters: an existing global `core.hooksPath` is honoured,
|
|
92
|
+
* never replaced — the engineer (or their dotfiles, or a company-wide
|
|
93
|
+
* config) put it there, and Scribe joins that directory instead of taking
|
|
94
|
+
* the setting over.
|
|
95
|
+
*/
|
|
96
|
+
function planHookInstall(survey) {
|
|
97
|
+
const hooksPath = survey.globalHooksPath ?? survey.defaultHooksPath;
|
|
98
|
+
const existing = survey.existingPostCommit;
|
|
99
|
+
const ours = existing !== null && isScribeHook(existing);
|
|
100
|
+
let conflict = null;
|
|
101
|
+
if (existing !== null && !ours && survey.adoptedExists) {
|
|
102
|
+
conflict =
|
|
103
|
+
`${hooksPath}/post-commit is not Scribe's and ${hooksPath}/${exports.ADOPTED_NAME} ` +
|
|
104
|
+
'already exists — move one of them aside, or re-run with --force to replace the shim';
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
hooksPath,
|
|
108
|
+
setGlobalHooksPath: survey.globalHooksPath === null,
|
|
109
|
+
adoptExisting: existing !== null && !ours && !survey.adoptedExists,
|
|
110
|
+
reinstall: ours,
|
|
111
|
+
conflict,
|
|
112
|
+
chained: survey.adoptedExists || (existing !== null && !ours) ? exports.ADOPTED_NAME : null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=hook.js.map
|
package/dist/hook.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;AAwBH,wCA2CC;AAGD,oCAEC;AAwCD,0CAoBC;AAlID,2EAA2E;AAC9D,QAAA,WAAW,GAAG,6BAA6B,CAAC;AAEzD,oEAAoE;AACvD,QAAA,YAAY,GAAG,wBAAwB,CAAC;AASrD;;;;;;;;GAQG;AACH,SAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,UAAU,GAAG,IAAI,EAAqB;IAChF,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,gBAAgB,UAAU,yBAAyB,CAAC,CAAC,CAAC,EAAE,CAAC;IACnF,OAAO;EACP,mBAAW;;;;;;;;;;;;;;;;iBAgBI,oBAAY;WAClB,oBAAY;;;;;;;;;;;;;EAarB,IAAI,eAAe,SAAS;;;;;;;;;CAS7B,CAAC;AACF,CAAC;AAED,kDAAkD;AAClD,SAAgB,YAAY,CAAC,QAAgB;IAC3C,OAAO,QAAQ,CAAC,QAAQ,CAAC,mBAAW,CAAC,CAAC;AACxC,CAAC;AAgCD;;;;;;;GAOG;AACH,SAAgB,eAAe,CAAC,MAAkB;IAChD,MAAM,SAAS,GAAG,MAAM,CAAC,eAAe,IAAI,MAAM,CAAC,gBAAgB,CAAC;IACpE,MAAM,QAAQ,GAAG,MAAM,CAAC,kBAAkB,CAAC;IAC3C,MAAM,IAAI,GAAG,QAAQ,KAAK,IAAI,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAC;IAEzD,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,QAAQ,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QACvD,QAAQ;YACN,GAAG,SAAS,oCAAoC,SAAS,IAAI,oBAAY,GAAG;gBAC5E,qFAAqF,CAAC;IAC1F,CAAC;IAED,OAAO;QACL,SAAS;QACT,kBAAkB,EAAE,MAAM,CAAC,eAAe,KAAK,IAAI;QACnD,aAAa,EAAE,QAAQ,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa;QAClE,SAAS,EAAE,IAAI;QACf,QAAQ;QACR,OAAO,EAAE,MAAM,CAAC,aAAa,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,oBAAY,CAAC,CAAC,CAAC,IAAI;KACpF,CAAC;AACJ,CAAC"}
|