@vibe-commit/capture 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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/agents/claude_code.d.ts +11 -0
- package/dist/agents/claude_code.js +162 -0
- package/dist/agents/claude_code.js.map +1 -0
- package/dist/agents/codex_cli.d.ts +15 -0
- package/dist/agents/codex_cli.js +67 -0
- package/dist/agents/codex_cli.js.map +1 -0
- package/dist/agents/cursor.d.ts +21 -0
- package/dist/agents/cursor.js +89 -0
- package/dist/agents/cursor.js.map +1 -0
- package/dist/agents/registry.d.ts +184 -0
- package/dist/agents/registry.js +212 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/types.d.ts +279 -0
- package/dist/agents/types.js +67 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/commands/connect.d.ts +36 -0
- package/dist/commands/connect.js +499 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/context.d.ts +45 -0
- package/dist/commands/context.js +14 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/report.d.ts +105 -0
- package/dist/commands/report.js +430 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/status.d.ts +29 -0
- package/dist/commands/status.js +236 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/why.d.ts +349 -0
- package/dist/commands/why.js +937 -0
- package/dist/commands/why.js.map +1 -0
- package/dist/consent.d.ts +59 -0
- package/dist/consent.js +111 -0
- package/dist/consent.js.map +1 -0
- package/dist/copy/claims.d.ts +79 -0
- package/dist/copy/claims.js +343 -0
- package/dist/copy/claims.js.map +1 -0
- package/dist/copy/grades.d.ts +117 -0
- package/dist/copy/grades.js +96 -0
- package/dist/copy/grades.js.map +1 -0
- package/dist/copy/index.d.ts +12 -0
- package/dist/copy/index.js +13 -0
- package/dist/copy/index.js.map +1 -0
- package/dist/copy/strings.d.ts +1328 -0
- package/dist/copy/strings.js +1243 -0
- package/dist/copy/strings.js.map +1 -0
- package/dist/copy/time.d.ts +27 -0
- package/dist/copy/time.js +43 -0
- package/dist/copy/time.js.map +1 -0
- package/dist/credential.d.ts +211 -0
- package/dist/credential.js +298 -0
- package/dist/credential.js.map +1 -0
- package/dist/exit.d.ts +59 -0
- package/dist/exit.js +60 -0
- package/dist/exit.js.map +1 -0
- package/dist/git.d.ts +299 -0
- package/dist/git.js +653 -0
- package/dist/git.js.map +1 -0
- package/dist/hooks/entry.d.ts +277 -0
- package/dist/hooks/entry.js +1092 -0
- package/dist/hooks/entry.js.map +1 -0
- package/dist/hooks/post_commit.d.ts +59 -0
- package/dist/hooks/post_commit.js +160 -0
- package/dist/hooks/post_commit.js.map +1 -0
- package/dist/hooks/post_rewrite.d.ts +59 -0
- package/dist/hooks/post_rewrite.js +161 -0
- package/dist/hooks/post_rewrite.js.map +1 -0
- package/dist/hooks/session_end.d.ts +101 -0
- package/dist/hooks/session_end.js +120 -0
- package/dist/hooks/session_end.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +360 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +153 -0
- package/dist/install.js +284 -0
- package/dist/install.js.map +1 -0
- package/dist/json.d.ts +100 -0
- package/dist/json.js +94 -0
- package/dist/json.js.map +1 -0
- package/dist/oauth/discovery.d.ts +96 -0
- package/dist/oauth/discovery.js +163 -0
- package/dist/oauth/discovery.js.map +1 -0
- package/dist/oauth/lock.d.ts +100 -0
- package/dist/oauth/lock.js +287 -0
- package/dist/oauth/lock.js.map +1 -0
- package/dist/oauth/loopback.d.ts +85 -0
- package/dist/oauth/loopback.js +124 -0
- package/dist/oauth/loopback.js.map +1 -0
- package/dist/oauth/pkce.d.ts +58 -0
- package/dist/oauth/pkce.js +77 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/read.d.ts +120 -0
- package/dist/oauth/read.js +144 -0
- package/dist/oauth/read.js.map +1 -0
- package/dist/oauth/session.d.ts +80 -0
- package/dist/oauth/session.js +112 -0
- package/dist/oauth/session.js.map +1 -0
- package/dist/oauth/signin.d.ts +178 -0
- package/dist/oauth/signin.js +225 -0
- package/dist/oauth/signin.js.map +1 -0
- package/dist/oauth/token.d.ts +139 -0
- package/dist/oauth/token.js +183 -0
- package/dist/oauth/token.js.map +1 -0
- package/dist/paths.d.ts +366 -0
- package/dist/paths.js +460 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy.d.ts +195 -0
- package/dist/policy.js +244 -0
- package/dist/policy.js.map +1 -0
- package/dist/post.d.ts +302 -0
- package/dist/post.js +384 -0
- package/dist/post.js.map +1 -0
- package/dist/project.d.ts +99 -0
- package/dist/project.js +192 -0
- package/dist/project.js.map +1 -0
- package/dist/redact.d.ts +175 -0
- package/dist/redact.js +700 -0
- package/dist/redact.js.map +1 -0
- package/dist/runtime.d.ts +30 -0
- package/dist/runtime.js +37 -0
- package/dist/runtime.js.map +1 -0
- package/dist/spawn_budget.d.ts +101 -0
- package/dist/spawn_budget.js +124 -0
- package/dist/spawn_budget.js.map +1 -0
- package/dist/spool.d.ts +390 -0
- package/dist/spool.js +556 -0
- package/dist/spool.js.map +1 -0
- package/dist/state.d.ts +109 -0
- package/dist/state.js +217 -0
- package/dist/state.js.map +1 -0
- package/dist/system_message.d.ts +101 -0
- package/dist/system_message.js +141 -0
- package/dist/system_message.js.map +1 -0
- package/dist/term.d.ts +112 -0
- package/dist/term.js +134 -0
- package/dist/term.js.map +1 -0
- package/dist/version.d.ts +16 -0
- package/dist/version.js +17 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the `vibecommit` a user types OURS? — `CR-025`, D55.
|
|
3
|
+
*
|
|
4
|
+
* D55 named the npm package `@vibe-commit/capture` because the bare
|
|
5
|
+
* `vibecommit` is taken — by `vibecommit@1.0.6`, which is **itself a commit
|
|
6
|
+
* CLI**. The BIN name stayed `vibecommit`, and D55 accepted the residual
|
|
7
|
+
* collision as unlikely. The register corrects that to closer to certain: the
|
|
8
|
+
* overlap population is exactly developers who install commit tooling.
|
|
9
|
+
*
|
|
10
|
+
* **This detects the collision. It does not reopen the decision** — the bin
|
|
11
|
+
* name is settled and D57 §DX5's verb surface is locked.
|
|
12
|
+
*
|
|
13
|
+
* What the clash costs is narrow and worth stating precisely, because the error
|
|
14
|
+
* copy has to: hook capture is UNAFFECTED, because the plugin invokes the binary
|
|
15
|
+
* by path. What breaks is the verbs a human types — `status`, `why`, `report`
|
|
16
|
+
* would run the other program.
|
|
17
|
+
*
|
|
18
|
+
* ## Resolved in-process, not by shelling out
|
|
19
|
+
*
|
|
20
|
+
* PATH lookup is a `readdir`-free stat of one candidate per PATH entry, so doing
|
|
21
|
+
* it here costs nothing and avoids a subprocess entirely — no argument array to
|
|
22
|
+
* get wrong, no shell to quote for, and no dependency on `which`/`command -v`
|
|
23
|
+
* being present or behaving the same across platforms. It also makes the whole
|
|
24
|
+
* thing testable by handing in an `env`, which a subprocess-based version could
|
|
25
|
+
* only fake by building a directory of shims.
|
|
26
|
+
*/
|
|
27
|
+
export type InstallVerdict =
|
|
28
|
+
/** The `vibecommit` on PATH resolves to this same program. */
|
|
29
|
+
{
|
|
30
|
+
readonly kind: "ours";
|
|
31
|
+
}
|
|
32
|
+
/** Something else owns the name. `resolved` is the real path it points at. */
|
|
33
|
+
| {
|
|
34
|
+
readonly kind: "foreign";
|
|
35
|
+
readonly resolved: string;
|
|
36
|
+
}
|
|
37
|
+
/** Nothing on PATH answers to `vibecommit`. */
|
|
38
|
+
| {
|
|
39
|
+
readonly kind: "absent";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Classify the `vibecommit` on PATH against the program actually running.
|
|
43
|
+
*
|
|
44
|
+
* `selfPath` is this process's entry point. Both sides are `realpath`'d before
|
|
45
|
+
* comparison, because the normal install IS a symlink — npm puts a link in
|
|
46
|
+
* `.../bin/vibecommit` pointing at the package's `dist/index.js`, so a string
|
|
47
|
+
* compare would call our own correct install foreign.
|
|
48
|
+
*
|
|
49
|
+
* Unresolvable either side is treated as NOT ours. That is the conservative
|
|
50
|
+
* direction here: the cost of a false "foreign" is one printed block a user can
|
|
51
|
+
* read and dismiss; the cost of a false "ours" is silence about a real clash,
|
|
52
|
+
* which is the failure this exists to catch.
|
|
53
|
+
*/
|
|
54
|
+
export declare function classifyInstall(env: NodeJS.ProcessEnv, selfPath: string): InstallVerdict;
|
|
55
|
+
/** The first executable `name` on PATH, or null. */
|
|
56
|
+
export declare function resolveOnPath(env: NodeJS.ProcessEnv, name: string): string | null;
|
|
57
|
+
/** Marks a `post-commit` as ours. Also the idempotency check. */
|
|
58
|
+
export declare const HOOK_MARKER = "vibecommit-capture-hook v1";
|
|
59
|
+
/**
|
|
60
|
+
* Where a pre-existing hook is moved so the chain can still run it.
|
|
61
|
+
*
|
|
62
|
+
* ⚠ **THE SPELLING IS LOAD-BEARING, AND THE FIRST TWO ATTEMPTS FAILED A GUARD.**
|
|
63
|
+
* `test/provenance.test.ts` is a wall over RAW TEXT with two separate tripwires,
|
|
64
|
+
* and ordinary explanatory prose sets off both:
|
|
65
|
+
*
|
|
66
|
+
* 1. it scans for the product name followed by a hyphen and a word, and reads
|
|
67
|
+
* every hit as a sibling REPOSITORY that must be declared. A filename
|
|
68
|
+
* suffix of that shape is indistinguishable from a repo name to a regex,
|
|
69
|
+
* so `.<product>-original` failed for a reason unrelated to provenance;
|
|
70
|
+
* 2. it counts bare marker tags and requires each to parse as a full
|
|
71
|
+
* three-part declaration, so merely NAMING the tag in a sentence registers
|
|
72
|
+
* a malformed one.
|
|
73
|
+
*
|
|
74
|
+
* Both were hit while writing this comment, in that order. The suffix is
|
|
75
|
+
* renamed and the prose avoids both literals — the guards are not weakened.
|
|
76
|
+
* `git.ts` carries the same warning about its linkage regex, for the same reason.
|
|
77
|
+
*/
|
|
78
|
+
export declare const CHAINED_SUFFIX = ".chained-by-vibecommit";
|
|
79
|
+
export type HookInstall =
|
|
80
|
+
/** Written into an empty slot. */
|
|
81
|
+
{
|
|
82
|
+
readonly kind: "installed";
|
|
83
|
+
readonly path: string;
|
|
84
|
+
}
|
|
85
|
+
/** A foreign `post-commit` was moved aside and is invoked FIRST by ours. */
|
|
86
|
+
| {
|
|
87
|
+
readonly kind: "chained";
|
|
88
|
+
readonly path: string;
|
|
89
|
+
readonly original: string;
|
|
90
|
+
}
|
|
91
|
+
/** Ours is already there; the binary path was refreshed. */
|
|
92
|
+
| {
|
|
93
|
+
readonly kind: "already";
|
|
94
|
+
readonly path: string;
|
|
95
|
+
}
|
|
96
|
+
/** ⛔ `core.hooksPath` is set, so `.git/hooks` is DEAD. Nothing was written. */
|
|
97
|
+
| {
|
|
98
|
+
readonly kind: "hooks-path";
|
|
99
|
+
readonly configured: string;
|
|
100
|
+
}
|
|
101
|
+
/** Not a work tree, or the write failed. */
|
|
102
|
+
| {
|
|
103
|
+
readonly kind: "failed";
|
|
104
|
+
readonly why: string;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Install (or re-point) the `post-commit` hook for the work tree at `toplevel`.
|
|
108
|
+
*
|
|
109
|
+
* ⛔ **`core.hooksPath` IS CHECKED FIRST AND IS A REFUSAL, NOT A WARNING.** When
|
|
110
|
+
* it is set — husky sets it, and so do many monorepos — git does not look in
|
|
111
|
+
* `.git/hooks` at all. Writing there would succeed at the filesystem level and
|
|
112
|
+
* report success, while capture silently observed nothing for the life of the
|
|
113
|
+
* clone. We do not write into the configured directory either: that directory is
|
|
114
|
+
* owned by whatever tool configured it, and a file we drop in gets regenerated
|
|
115
|
+
* away without warning. The honest move is to say so.
|
|
116
|
+
*
|
|
117
|
+
* ⚠ **VERIFIED that our own repos set NEITHER `core.hooksPath` NOR husky**, so
|
|
118
|
+
* dogfooding cannot exercise this path — it is covered by deliberate tests.
|
|
119
|
+
*/
|
|
120
|
+
export declare function installPostCommitHook(toplevel: string, binPath: string): HookInstall;
|
|
121
|
+
/** One verdict per hook. Both are installed, or neither is. */
|
|
122
|
+
export interface CaptureHooks {
|
|
123
|
+
readonly commit: HookInstall;
|
|
124
|
+
readonly rewrite: HookInstall;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Install BOTH capture hooks for the work tree at `toplevel`.
|
|
128
|
+
*
|
|
129
|
+
* ⛔ **`post-rewrite` IS NOT AN EXTRA, IT IS THE OTHER HALF OF `post-commit`.**
|
|
130
|
+
* `--amend` and `rebase` fire `post-commit` for a sha they then DESTROY (`M5`),
|
|
131
|
+
* so a clone with only the commit hook records observations that name commits
|
|
132
|
+
* unreachable from every branch and gone after `gc --prune=now`. The two go in
|
|
133
|
+
* together, behind the same `core.hooksPath` refusal and the same chaining.
|
|
134
|
+
*
|
|
135
|
+
* ⚠ **`installPostCommitHook` RETURNS ONLY THE COMMIT VERDICT, and the rewrite
|
|
136
|
+
* verdict is currently PRINTED BY NOTHING.** `commands/connect.ts` reports one
|
|
137
|
+
* install and `src/copy/` has one string for it, both outside this lane's scope.
|
|
138
|
+
* The gap is narrow — `core.hooksPath` and a `not a git work tree` failure are
|
|
139
|
+
* shared, so they are reported by the commit verdict — and is limited to a clone
|
|
140
|
+
* that has a pre-existing `post-rewrite` and no pre-existing `post-commit`: that
|
|
141
|
+
* chaining happens correctly and is not announced. ⛔ Recorded here rather than
|
|
142
|
+
* left to be discovered, because an unreported install outcome is precisely what
|
|
143
|
+
* the block at the top of this section calls the third silent no-op.
|
|
144
|
+
*/
|
|
145
|
+
export declare function installCaptureHooks(toplevel: string, binPath: string): CaptureHooks;
|
|
146
|
+
/**
|
|
147
|
+
* The hooks directory git itself would use.
|
|
148
|
+
*
|
|
149
|
+
* `rev-parse --git-path hooks` rather than `<toplevel>/.git/hooks`, because the
|
|
150
|
+
* two differ for a linked work tree and for a repository with a separate git
|
|
151
|
+
* dir — and it answers RELATIVE to the work tree, so it is resolved here.
|
|
152
|
+
*/
|
|
153
|
+
export declare function resolveHooksDir(toplevel: string): string | null;
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the `vibecommit` a user types OURS? — `CR-025`, D55.
|
|
3
|
+
*
|
|
4
|
+
* D55 named the npm package `@vibe-commit/capture` because the bare
|
|
5
|
+
* `vibecommit` is taken — by `vibecommit@1.0.6`, which is **itself a commit
|
|
6
|
+
* CLI**. The BIN name stayed `vibecommit`, and D55 accepted the residual
|
|
7
|
+
* collision as unlikely. The register corrects that to closer to certain: the
|
|
8
|
+
* overlap population is exactly developers who install commit tooling.
|
|
9
|
+
*
|
|
10
|
+
* **This detects the collision. It does not reopen the decision** — the bin
|
|
11
|
+
* name is settled and D57 §DX5's verb surface is locked.
|
|
12
|
+
*
|
|
13
|
+
* What the clash costs is narrow and worth stating precisely, because the error
|
|
14
|
+
* copy has to: hook capture is UNAFFECTED, because the plugin invokes the binary
|
|
15
|
+
* by path. What breaks is the verbs a human types — `status`, `why`, `report`
|
|
16
|
+
* would run the other program.
|
|
17
|
+
*
|
|
18
|
+
* ## Resolved in-process, not by shelling out
|
|
19
|
+
*
|
|
20
|
+
* PATH lookup is a `readdir`-free stat of one candidate per PATH entry, so doing
|
|
21
|
+
* it here costs nothing and avoids a subprocess entirely — no argument array to
|
|
22
|
+
* get wrong, no shell to quote for, and no dependency on `which`/`command -v`
|
|
23
|
+
* being present or behaving the same across platforms. It also makes the whole
|
|
24
|
+
* thing testable by handing in an `env`, which a subprocess-based version could
|
|
25
|
+
* only fake by building a directory of shims.
|
|
26
|
+
*/
|
|
27
|
+
import { accessSync, chmodSync, constants, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
29
|
+
import { gitProbe } from "./git.js";
|
|
30
|
+
/** The command name D55 kept. */
|
|
31
|
+
const BIN = "vibecommit";
|
|
32
|
+
/**
|
|
33
|
+
* Classify the `vibecommit` on PATH against the program actually running.
|
|
34
|
+
*
|
|
35
|
+
* `selfPath` is this process's entry point. Both sides are `realpath`'d before
|
|
36
|
+
* comparison, because the normal install IS a symlink — npm puts a link in
|
|
37
|
+
* `.../bin/vibecommit` pointing at the package's `dist/index.js`, so a string
|
|
38
|
+
* compare would call our own correct install foreign.
|
|
39
|
+
*
|
|
40
|
+
* Unresolvable either side is treated as NOT ours. That is the conservative
|
|
41
|
+
* direction here: the cost of a false "foreign" is one printed block a user can
|
|
42
|
+
* read and dismiss; the cost of a false "ours" is silence about a real clash,
|
|
43
|
+
* which is the failure this exists to catch.
|
|
44
|
+
*/
|
|
45
|
+
export function classifyInstall(env, selfPath) {
|
|
46
|
+
const found = resolveOnPath(env, BIN);
|
|
47
|
+
if (found === null)
|
|
48
|
+
return { kind: "absent" };
|
|
49
|
+
const self = realpathOrNull(selfPath);
|
|
50
|
+
const other = realpathOrNull(found);
|
|
51
|
+
if (self !== null && other !== null && self === other)
|
|
52
|
+
return { kind: "ours" };
|
|
53
|
+
return { kind: "foreign", resolved: other ?? found };
|
|
54
|
+
}
|
|
55
|
+
/** The first executable `name` on PATH, or null. */
|
|
56
|
+
export function resolveOnPath(env, name) {
|
|
57
|
+
const raw = env.PATH;
|
|
58
|
+
if (raw === undefined || raw === "")
|
|
59
|
+
return null;
|
|
60
|
+
for (const dir of raw.split(delimiter)) {
|
|
61
|
+
if (dir === "")
|
|
62
|
+
continue;
|
|
63
|
+
const candidate = join(dir, name);
|
|
64
|
+
try {
|
|
65
|
+
// A directory named `vibecommit` on PATH is not a command.
|
|
66
|
+
if (!statSync(candidate).isFile())
|
|
67
|
+
continue;
|
|
68
|
+
accessSync(candidate, constants.X_OK);
|
|
69
|
+
return candidate;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Not there, not readable, not executable — keep looking.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function realpathOrNull(path) {
|
|
78
|
+
if (path === "")
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
return realpathSync(path);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// The `post-commit` hook install — `CR-170`, D154.
|
|
89
|
+
//
|
|
90
|
+
// ⛔ THREE OF THIS PATH'S FAILURE MODES ARE SILENT NO-OPS (D98's class), and a
|
|
91
|
+
// silent no-op here means capture quietly observes nothing forever:
|
|
92
|
+
//
|
|
93
|
+
// 1. `core.hooksPath` set -> `.git/hooks` is ignored ENTIRELY by git, so
|
|
94
|
+
// writing there succeeds, reports success, and does nothing at all.
|
|
95
|
+
// 2. an existing `post-commit` -> overwriting it breaks the user's own tooling.
|
|
96
|
+
// 3. no report -> the user cannot tell (1) or (2) happened.
|
|
97
|
+
//
|
|
98
|
+
// All three are LOUD below: each is its own verdict, and `connect` prints it.
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
/** Marks a `post-commit` as ours. Also the idempotency check. */
|
|
101
|
+
export const HOOK_MARKER = "vibecommit-capture-hook v1";
|
|
102
|
+
/**
|
|
103
|
+
* Where a pre-existing hook is moved so the chain can still run it.
|
|
104
|
+
*
|
|
105
|
+
* ⚠ **THE SPELLING IS LOAD-BEARING, AND THE FIRST TWO ATTEMPTS FAILED A GUARD.**
|
|
106
|
+
* `test/provenance.test.ts` is a wall over RAW TEXT with two separate tripwires,
|
|
107
|
+
* and ordinary explanatory prose sets off both:
|
|
108
|
+
*
|
|
109
|
+
* 1. it scans for the product name followed by a hyphen and a word, and reads
|
|
110
|
+
* every hit as a sibling REPOSITORY that must be declared. A filename
|
|
111
|
+
* suffix of that shape is indistinguishable from a repo name to a regex,
|
|
112
|
+
* so `.<product>-original` failed for a reason unrelated to provenance;
|
|
113
|
+
* 2. it counts bare marker tags and requires each to parse as a full
|
|
114
|
+
* three-part declaration, so merely NAMING the tag in a sentence registers
|
|
115
|
+
* a malformed one.
|
|
116
|
+
*
|
|
117
|
+
* Both were hit while writing this comment, in that order. The suffix is
|
|
118
|
+
* renamed and the prose avoids both literals — the guards are not weakened.
|
|
119
|
+
* `git.ts` carries the same warning about its linkage regex, for the same reason.
|
|
120
|
+
*/
|
|
121
|
+
export const CHAINED_SUFFIX = ".chained-by-vibecommit";
|
|
122
|
+
/**
|
|
123
|
+
* The hook body.
|
|
124
|
+
*
|
|
125
|
+
* ⚠ **The original runs FIRST and its exit status is preserved**; ours runs
|
|
126
|
+
* after and can never change the outcome. MEASURED 2026-08-17: git IGNORES
|
|
127
|
+
* `post-commit`'s exit code entirely (a hook exiting 7 still leaves
|
|
128
|
+
* `git commit` at 0), so this cannot break a commit either way — but preserving
|
|
129
|
+
* the status keeps the chained hook's own contract with anything that reads it.
|
|
130
|
+
*
|
|
131
|
+
* `>/dev/null 2>&1` on our side: hook output lands in the user's own terminal,
|
|
132
|
+
* and a capture tool has no business writing there.
|
|
133
|
+
*
|
|
134
|
+
* ## ⛔ `post-rewrite` TAKES ITS INPUT ON STDIN, AND A STREAM IS READ ONCE
|
|
135
|
+
*
|
|
136
|
+
* `post-commit` is handed nothing. `post-rewrite` is handed the ancestor/successor
|
|
137
|
+
* pairs on stdin (`M6`) — and **a chained hook that reads stdin leaves ours with
|
|
138
|
+
* an empty stream, and ours would leave theirs with one.** Copying the
|
|
139
|
+
* `post-commit` body would therefore have silently broken every pre-existing
|
|
140
|
+
* `post-rewrite` hook the moment we chained it: their tooling would run, read
|
|
141
|
+
* nothing, and conclude nothing was rewritten.
|
|
142
|
+
*
|
|
143
|
+
* So the stdin variant buffers the stream ONCE and feeds a copy to each. ⚠
|
|
144
|
+
* `$(cat)` strips trailing newlines and cannot carry a NUL, which is fine and is
|
|
145
|
+
* checked rather than assumed: this stream is `<40-hex> <40-hex>` lines, ASCII,
|
|
146
|
+
* one per rewrite. `printf '%s\n'` restores the single trailing newline.
|
|
147
|
+
*/
|
|
148
|
+
function hookScript(binPath, hookName, readsStdin) {
|
|
149
|
+
const chained = `"$d/${hookName}${CHAINED_SUFFIX}"`;
|
|
150
|
+
const ours = `${shellQuote(binPath)} ${hookName}`;
|
|
151
|
+
return [
|
|
152
|
+
"#!/bin/sh",
|
|
153
|
+
`# ${HOOK_MARKER}`,
|
|
154
|
+
"# Installed by `vibecommit connect`. Safe to delete; capture then observes",
|
|
155
|
+
"# no commits for this clone.",
|
|
156
|
+
'd=$(dirname "$0")',
|
|
157
|
+
"status=0",
|
|
158
|
+
...(readsStdin ? ["input=$(cat)"] : []),
|
|
159
|
+
`if [ -x "$d/${hookName}${CHAINED_SUFFIX}" ]; then`,
|
|
160
|
+
readsStdin
|
|
161
|
+
? ` printf '%s\\n' "$input" | ${chained} "$@" || status=$?`
|
|
162
|
+
: ` ${chained} "$@" || status=$?`,
|
|
163
|
+
"fi",
|
|
164
|
+
readsStdin
|
|
165
|
+
? `printf '%s\\n' "$input" | ${ours} "$@" >/dev/null 2>&1 || true`
|
|
166
|
+
: `${ours} >/dev/null 2>&1 || true`,
|
|
167
|
+
"exit $status",
|
|
168
|
+
"",
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
171
|
+
/** Single-quote for `sh`. A repository path containing a space is ordinary. */
|
|
172
|
+
function shellQuote(value) {
|
|
173
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Install (or re-point) the `post-commit` hook for the work tree at `toplevel`.
|
|
177
|
+
*
|
|
178
|
+
* ⛔ **`core.hooksPath` IS CHECKED FIRST AND IS A REFUSAL, NOT A WARNING.** When
|
|
179
|
+
* it is set — husky sets it, and so do many monorepos — git does not look in
|
|
180
|
+
* `.git/hooks` at all. Writing there would succeed at the filesystem level and
|
|
181
|
+
* report success, while capture silently observed nothing for the life of the
|
|
182
|
+
* clone. We do not write into the configured directory either: that directory is
|
|
183
|
+
* owned by whatever tool configured it, and a file we drop in gets regenerated
|
|
184
|
+
* away without warning. The honest move is to say so.
|
|
185
|
+
*
|
|
186
|
+
* ⚠ **VERIFIED that our own repos set NEITHER `core.hooksPath` NOR husky**, so
|
|
187
|
+
* dogfooding cannot exercise this path — it is covered by deliberate tests.
|
|
188
|
+
*/
|
|
189
|
+
export function installPostCommitHook(toplevel, binPath) {
|
|
190
|
+
return installCaptureHooks(toplevel, binPath).commit;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Install BOTH capture hooks for the work tree at `toplevel`.
|
|
194
|
+
*
|
|
195
|
+
* ⛔ **`post-rewrite` IS NOT AN EXTRA, IT IS THE OTHER HALF OF `post-commit`.**
|
|
196
|
+
* `--amend` and `rebase` fire `post-commit` for a sha they then DESTROY (`M5`),
|
|
197
|
+
* so a clone with only the commit hook records observations that name commits
|
|
198
|
+
* unreachable from every branch and gone after `gc --prune=now`. The two go in
|
|
199
|
+
* together, behind the same `core.hooksPath` refusal and the same chaining.
|
|
200
|
+
*
|
|
201
|
+
* ⚠ **`installPostCommitHook` RETURNS ONLY THE COMMIT VERDICT, and the rewrite
|
|
202
|
+
* verdict is currently PRINTED BY NOTHING.** `commands/connect.ts` reports one
|
|
203
|
+
* install and `src/copy/` has one string for it, both outside this lane's scope.
|
|
204
|
+
* The gap is narrow — `core.hooksPath` and a `not a git work tree` failure are
|
|
205
|
+
* shared, so they are reported by the commit verdict — and is limited to a clone
|
|
206
|
+
* that has a pre-existing `post-rewrite` and no pre-existing `post-commit`: that
|
|
207
|
+
* chaining happens correctly and is not announced. ⛔ Recorded here rather than
|
|
208
|
+
* left to be discovered, because an unreported install outcome is precisely what
|
|
209
|
+
* the block at the top of this section calls the third silent no-op.
|
|
210
|
+
*/
|
|
211
|
+
export function installCaptureHooks(toplevel, binPath) {
|
|
212
|
+
const configured = gitProbe(toplevel, ["config", "--get", "core.hooksPath"]);
|
|
213
|
+
// `--get` exits 1 when unset, which `gitProbe` reports as null. A SET value
|
|
214
|
+
// is the failure case here, which is the opposite of the usual reading.
|
|
215
|
+
if (configured !== null && configured.trim() !== "") {
|
|
216
|
+
// ⛔ BOTH refuse. `.git/hooks` is dead for every hook in it, not just ours.
|
|
217
|
+
const verdict = { kind: "hooks-path", configured: configured.trim() };
|
|
218
|
+
return { commit: verdict, rewrite: verdict };
|
|
219
|
+
}
|
|
220
|
+
const hooksDir = resolveHooksDir(toplevel);
|
|
221
|
+
if (hooksDir === null) {
|
|
222
|
+
const verdict = { kind: "failed", why: "not a git work tree" };
|
|
223
|
+
return { commit: verdict, rewrite: verdict };
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
commit: writeHook(hooksDir, "post-commit", hookScript(binPath, "post-commit", false)),
|
|
227
|
+
rewrite: writeHook(hooksDir, "post-rewrite", hookScript(binPath, "post-rewrite", true)),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** Write one hook into an empty slot, over ours, or in front of theirs. */
|
|
231
|
+
function writeHook(hooksDir, hookName, script) {
|
|
232
|
+
const hookPath = join(hooksDir, hookName);
|
|
233
|
+
const existing = readIfPresent(hookPath);
|
|
234
|
+
try {
|
|
235
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
236
|
+
if (existing !== null && existing.includes(HOOK_MARKER)) {
|
|
237
|
+
// Ours already. Rewrite anyway — the binary may have moved since.
|
|
238
|
+
writeFileSync(hookPath, script, { mode: 0o755 });
|
|
239
|
+
chmodSync(hookPath, 0o755);
|
|
240
|
+
return { kind: "already", path: hookPath };
|
|
241
|
+
}
|
|
242
|
+
if (existing !== null) {
|
|
243
|
+
// ⛔ CHAIN, NEVER CLOBBER. Their hook keeps running, from a path ours
|
|
244
|
+
// invokes explicitly, and it goes first.
|
|
245
|
+
const original = `${hookPath}${CHAINED_SUFFIX}`;
|
|
246
|
+
renameSync(hookPath, original);
|
|
247
|
+
chmodSync(original, 0o755);
|
|
248
|
+
writeFileSync(hookPath, script, { mode: 0o755 });
|
|
249
|
+
chmodSync(hookPath, 0o755);
|
|
250
|
+
return { kind: "chained", path: hookPath, original };
|
|
251
|
+
}
|
|
252
|
+
writeFileSync(hookPath, script, { mode: 0o755 });
|
|
253
|
+
chmodSync(hookPath, 0o755);
|
|
254
|
+
return { kind: "installed", path: hookPath };
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return { kind: "failed", why: error instanceof Error ? error.message : "write failed" };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The hooks directory git itself would use.
|
|
262
|
+
*
|
|
263
|
+
* `rev-parse --git-path hooks` rather than `<toplevel>/.git/hooks`, because the
|
|
264
|
+
* two differ for a linked work tree and for a repository with a separate git
|
|
265
|
+
* dir — and it answers RELATIVE to the work tree, so it is resolved here.
|
|
266
|
+
*/
|
|
267
|
+
export function resolveHooksDir(toplevel) {
|
|
268
|
+
const out = gitProbe(toplevel, ["rev-parse", "--git-path", "hooks"]);
|
|
269
|
+
if (out === null)
|
|
270
|
+
return null;
|
|
271
|
+
const path = out.trim();
|
|
272
|
+
if (path === "")
|
|
273
|
+
return null;
|
|
274
|
+
return isAbsolute(path) ? path : join(toplevel, path);
|
|
275
|
+
}
|
|
276
|
+
function readIfPresent(path) {
|
|
277
|
+
try {
|
|
278
|
+
return readFileSync(path, "utf8");
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The machine-readable surface of the read verbs — DESIGN.md §13.3, D122 §2.
|
|
3
|
+
*
|
|
4
|
+
* §13.3 is the authority and it says three things this module exists to make
|
|
5
|
+
* true:
|
|
6
|
+
*
|
|
7
|
+
* 1. `--json` **implies the whole no-colour detection order** and emits
|
|
8
|
+
* **NOTHING BUT** the JSON document on stdout. "Nothing but" is literal:
|
|
9
|
+
* no banner, no progress line, no prose. `resolveColour` already returns
|
|
10
|
+
* `false` for `json: true`, so half of this was true before; the other half
|
|
11
|
+
* is `emitJson` being the ONLY thing a verb writes to stdout under the flag.
|
|
12
|
+
* 2. **Grade words appear in `--json` ONLY.** Prose never prints a bare grade
|
|
13
|
+
* (claims-audit §R3, D65 §DR4), so this is the one place the three-way
|
|
14
|
+
* `derived` / `observed` / `declared` distinction is machine-readable.
|
|
15
|
+
* 3. The three status states stay **distinguishable**. §13.2's `GLYPH` rule
|
|
16
|
+
* ("a dropped glyph turns three distinguishable states into three identical
|
|
17
|
+
* ones") has no glyph here, and `outcome` carries the distinction instead.
|
|
18
|
+
*
|
|
19
|
+
* ## ⛔ THE RULE THAT GOVERNS EVERY FIELD: A ZERO IS A MEASUREMENT CLAIM
|
|
20
|
+
*
|
|
21
|
+
* `CR-129a`'s title, and it is the single most likely way to get this surface
|
|
22
|
+
* wrong. **`null` means NOT KNOWN. `0` and `[]` mean MEASURED NONE.** They are
|
|
23
|
+
* different facts and this document keeps them different, because the two
|
|
24
|
+
* absences it actually has to express today are both of the first kind:
|
|
25
|
+
*
|
|
26
|
+
* - `commit_file_attribution` **has no writer** (D104 §2b), so mcp hardcodes
|
|
27
|
+
* `files: []` on every turn. That is *we do not know which files*, which is
|
|
28
|
+
* why `turnDocument` renders it as `null` and NEVER as `[]`.
|
|
29
|
+
* - `observed` **has no producer in any repo** (D104 §2a). So no field here is
|
|
30
|
+
* ever populated with a grade the wire did not send: `grades` and `by_grade`
|
|
31
|
+
* are ECHOED, never widened to the full enum with zeros. A key the payload
|
|
32
|
+
* did not carry stays absent rather than becoming `0`.
|
|
33
|
+
*
|
|
34
|
+
* ⚠ The corollary is that `[]` and `0` are legal in this schema and simply do
|
|
35
|
+
* not occur on the paths that have no producer. That is deliberate: the document
|
|
36
|
+
* must be ABLE to say *none* distinctly from *not known*, or the day a producer
|
|
37
|
+
* lands there is no way to say it.
|
|
38
|
+
*
|
|
39
|
+
* ⚠ **THE WORD `from` IS NEVER FOLLOWED BY A DOUBLE-QUOTED PHRASE IN THIS
|
|
40
|
+
* FILE**, and that is a constraint rather than a style. `test/provenance.test.ts`
|
|
41
|
+
* runs a linkage wall as a REGEX over raw text — `(?:from|import)` then optional
|
|
42
|
+
* space then a quoted specifier — so an ordinary sentence that quotes a phrase
|
|
43
|
+
* right after that word reads as an import escaping the package and turns the
|
|
44
|
+
* wall RED against correct prose. It did, on the first run of this file, and
|
|
45
|
+
* then again on the first wording of this very warning. `src/commands/why.ts`
|
|
46
|
+
* and `src/oauth/signin.ts` already carry the same note; a false RED is the more
|
|
47
|
+
* dangerous direction, because it justifies "fixing" correct content and nothing
|
|
48
|
+
* later flags it. Quote such phrases in *italics* here instead.
|
|
49
|
+
*
|
|
50
|
+
* ## What is NOT here, deliberately
|
|
51
|
+
*
|
|
52
|
+
* **A failure taxonomy.** A verb that could not do its job renders §13.6's
|
|
53
|
+
* what/why/fix block to **stderr** and writes **nothing** to stdout, exactly as
|
|
54
|
+
* it does without the flag — clause 1 holds trivially, the exit code already
|
|
55
|
+
* carries the branch a wrapper needs (§13.7 gives five distinct codes), and
|
|
56
|
+
* minting a parallel error vocabulary would be a contract nobody asked for.
|
|
57
|
+
* **Absence is not a failure** and DOES emit a document: `why` on a commit with
|
|
58
|
+
* no edge and `report` on an empty window are ANSWERS (exit 4 says so), and a
|
|
59
|
+
* machine surface that went silent on them would lose the state that matters.
|
|
60
|
+
*
|
|
61
|
+
* ⚠ §13.3 does not settle that split in either direction. It is reported as an
|
|
62
|
+
* open question rather than smoothed over — `DESIGN.md` is orchestrator-owned
|
|
63
|
+
* (D66 §3) and this module's reading of it is a builder's, not a ruling.
|
|
64
|
+
*/
|
|
65
|
+
import type { Writable } from "node:stream";
|
|
66
|
+
/**
|
|
67
|
+
* The document format version, and it is the FIRST key so a wrapper can branch
|
|
68
|
+
* before it parses anything else. Bumped only when a field changes meaning —
|
|
69
|
+
* adding a key is not a bump.
|
|
70
|
+
*/
|
|
71
|
+
export declare const JSON_SCHEMA_VERSION = 1;
|
|
72
|
+
/** The read verbs. `connect` is an install moment and `off` is a mutation. */
|
|
73
|
+
export type JsonVerb = "status" | "why" | "report";
|
|
74
|
+
/**
|
|
75
|
+
* The document, minus the two keys every document carries.
|
|
76
|
+
*
|
|
77
|
+
* `unknown` rather than a union of the three shapes: each verb owns its own
|
|
78
|
+
* body, and a shared discriminated union here would put three verbs' fields in
|
|
79
|
+
* one place for no reader's benefit.
|
|
80
|
+
*/
|
|
81
|
+
export type JsonBody = Readonly<Record<string, unknown>>;
|
|
82
|
+
/**
|
|
83
|
+
* The exact bytes of one document.
|
|
84
|
+
*
|
|
85
|
+
* ⚠ **ONE LINE, NO INDENTATION, ONE TRAILING NEWLINE.** Compact is what makes
|
|
86
|
+
* "nothing before or after the document" checkable on the RAW BYTES rather than
|
|
87
|
+
* by trusting a lenient parser — a leading banner on its own line still parses
|
|
88
|
+
* under most readers, and this shape makes that impossible to miss. §13.4's
|
|
89
|
+
* 80-column rule does not apply: it governs terminal prose, and this is not
|
|
90
|
+
* prose. The trailing newline is the POSIX line-termination convention, not
|
|
91
|
+
* copy; `JSON.parse` ignores trailing whitespace, so it cannot break a consumer.
|
|
92
|
+
*/
|
|
93
|
+
export declare function renderJson(verb: JsonVerb, outcome: string, body: JsonBody): string;
|
|
94
|
+
/**
|
|
95
|
+
* Write one document to stdout, and nothing else.
|
|
96
|
+
*
|
|
97
|
+
* Takes the stream rather than the whole `CommandContext` so the same helper
|
|
98
|
+
* serves all three verbs without importing a command module into a leaf.
|
|
99
|
+
*/
|
|
100
|
+
export declare function emitJson(stdout: Writable, verb: JsonVerb, outcome: string, body: JsonBody): void;
|
package/dist/json.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The machine-readable surface of the read verbs — DESIGN.md §13.3, D122 §2.
|
|
3
|
+
*
|
|
4
|
+
* §13.3 is the authority and it says three things this module exists to make
|
|
5
|
+
* true:
|
|
6
|
+
*
|
|
7
|
+
* 1. `--json` **implies the whole no-colour detection order** and emits
|
|
8
|
+
* **NOTHING BUT** the JSON document on stdout. "Nothing but" is literal:
|
|
9
|
+
* no banner, no progress line, no prose. `resolveColour` already returns
|
|
10
|
+
* `false` for `json: true`, so half of this was true before; the other half
|
|
11
|
+
* is `emitJson` being the ONLY thing a verb writes to stdout under the flag.
|
|
12
|
+
* 2. **Grade words appear in `--json` ONLY.** Prose never prints a bare grade
|
|
13
|
+
* (claims-audit §R3, D65 §DR4), so this is the one place the three-way
|
|
14
|
+
* `derived` / `observed` / `declared` distinction is machine-readable.
|
|
15
|
+
* 3. The three status states stay **distinguishable**. §13.2's `GLYPH` rule
|
|
16
|
+
* ("a dropped glyph turns three distinguishable states into three identical
|
|
17
|
+
* ones") has no glyph here, and `outcome` carries the distinction instead.
|
|
18
|
+
*
|
|
19
|
+
* ## ⛔ THE RULE THAT GOVERNS EVERY FIELD: A ZERO IS A MEASUREMENT CLAIM
|
|
20
|
+
*
|
|
21
|
+
* `CR-129a`'s title, and it is the single most likely way to get this surface
|
|
22
|
+
* wrong. **`null` means NOT KNOWN. `0` and `[]` mean MEASURED NONE.** They are
|
|
23
|
+
* different facts and this document keeps them different, because the two
|
|
24
|
+
* absences it actually has to express today are both of the first kind:
|
|
25
|
+
*
|
|
26
|
+
* - `commit_file_attribution` **has no writer** (D104 §2b), so mcp hardcodes
|
|
27
|
+
* `files: []` on every turn. That is *we do not know which files*, which is
|
|
28
|
+
* why `turnDocument` renders it as `null` and NEVER as `[]`.
|
|
29
|
+
* - `observed` **has no producer in any repo** (D104 §2a). So no field here is
|
|
30
|
+
* ever populated with a grade the wire did not send: `grades` and `by_grade`
|
|
31
|
+
* are ECHOED, never widened to the full enum with zeros. A key the payload
|
|
32
|
+
* did not carry stays absent rather than becoming `0`.
|
|
33
|
+
*
|
|
34
|
+
* ⚠ The corollary is that `[]` and `0` are legal in this schema and simply do
|
|
35
|
+
* not occur on the paths that have no producer. That is deliberate: the document
|
|
36
|
+
* must be ABLE to say *none* distinctly from *not known*, or the day a producer
|
|
37
|
+
* lands there is no way to say it.
|
|
38
|
+
*
|
|
39
|
+
* ⚠ **THE WORD `from` IS NEVER FOLLOWED BY A DOUBLE-QUOTED PHRASE IN THIS
|
|
40
|
+
* FILE**, and that is a constraint rather than a style. `test/provenance.test.ts`
|
|
41
|
+
* runs a linkage wall as a REGEX over raw text — `(?:from|import)` then optional
|
|
42
|
+
* space then a quoted specifier — so an ordinary sentence that quotes a phrase
|
|
43
|
+
* right after that word reads as an import escaping the package and turns the
|
|
44
|
+
* wall RED against correct prose. It did, on the first run of this file, and
|
|
45
|
+
* then again on the first wording of this very warning. `src/commands/why.ts`
|
|
46
|
+
* and `src/oauth/signin.ts` already carry the same note; a false RED is the more
|
|
47
|
+
* dangerous direction, because it justifies "fixing" correct content and nothing
|
|
48
|
+
* later flags it. Quote such phrases in *italics* here instead.
|
|
49
|
+
*
|
|
50
|
+
* ## What is NOT here, deliberately
|
|
51
|
+
*
|
|
52
|
+
* **A failure taxonomy.** A verb that could not do its job renders §13.6's
|
|
53
|
+
* what/why/fix block to **stderr** and writes **nothing** to stdout, exactly as
|
|
54
|
+
* it does without the flag — clause 1 holds trivially, the exit code already
|
|
55
|
+
* carries the branch a wrapper needs (§13.7 gives five distinct codes), and
|
|
56
|
+
* minting a parallel error vocabulary would be a contract nobody asked for.
|
|
57
|
+
* **Absence is not a failure** and DOES emit a document: `why` on a commit with
|
|
58
|
+
* no edge and `report` on an empty window are ANSWERS (exit 4 says so), and a
|
|
59
|
+
* machine surface that went silent on them would lose the state that matters.
|
|
60
|
+
*
|
|
61
|
+
* ⚠ §13.3 does not settle that split in either direction. It is reported as an
|
|
62
|
+
* open question rather than smoothed over — `DESIGN.md` is orchestrator-owned
|
|
63
|
+
* (D66 §3) and this module's reading of it is a builder's, not a ruling.
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* The document format version, and it is the FIRST key so a wrapper can branch
|
|
67
|
+
* before it parses anything else. Bumped only when a field changes meaning —
|
|
68
|
+
* adding a key is not a bump.
|
|
69
|
+
*/
|
|
70
|
+
export const JSON_SCHEMA_VERSION = 1;
|
|
71
|
+
/**
|
|
72
|
+
* The exact bytes of one document.
|
|
73
|
+
*
|
|
74
|
+
* ⚠ **ONE LINE, NO INDENTATION, ONE TRAILING NEWLINE.** Compact is what makes
|
|
75
|
+
* "nothing before or after the document" checkable on the RAW BYTES rather than
|
|
76
|
+
* by trusting a lenient parser — a leading banner on its own line still parses
|
|
77
|
+
* under most readers, and this shape makes that impossible to miss. §13.4's
|
|
78
|
+
* 80-column rule does not apply: it governs terminal prose, and this is not
|
|
79
|
+
* prose. The trailing newline is the POSIX line-termination convention, not
|
|
80
|
+
* copy; `JSON.parse` ignores trailing whitespace, so it cannot break a consumer.
|
|
81
|
+
*/
|
|
82
|
+
export function renderJson(verb, outcome, body) {
|
|
83
|
+
return `${JSON.stringify({ schema: JSON_SCHEMA_VERSION, verb, outcome, ...body })}\n`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Write one document to stdout, and nothing else.
|
|
87
|
+
*
|
|
88
|
+
* Takes the stream rather than the whole `CommandContext` so the same helper
|
|
89
|
+
* serves all three verbs without importing a command module into a leaf.
|
|
90
|
+
*/
|
|
91
|
+
export function emitJson(stdout, verb, outcome, body) {
|
|
92
|
+
stdout.write(renderJson(verb, outcome, body));
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=json.js.map
|
package/dist/json.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAIH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAcrC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,IAAc,EAAE,OAAe,EAAE,IAAc;IACxE,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC;AACxF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CACtB,MAAgB,EAChB,IAAc,EACd,OAAe,EACf,IAAc;IAEd,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;AAChD,CAAC"}
|