@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 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":"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"}
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
@@ -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
@@ -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"}