@devspecai/pi-plugin 0.0.0-stage → 0.10.3
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/CHANGELOG.md +899 -0
- package/LICENSE +21 -0
- package/README.md +103 -2
- package/extensions/.gitkeep +0 -0
- package/extensions/core/activation-recovery.ts +135 -0
- package/extensions/core/active-deliveries.ts +171 -0
- package/extensions/core/active-session-plans.ts +374 -0
- package/extensions/core/adapter-overlap.ts +247 -0
- package/extensions/core/assistant-speech.ts +44 -0
- package/extensions/core/attach-context.ts +25 -0
- package/extensions/core/attachment-limits.ts +20 -0
- package/extensions/core/attachments.ts +487 -0
- package/extensions/core/auth-store.ts +91 -0
- package/extensions/core/automation-run-retry.ts +31 -0
- package/extensions/core/background-journal.ts +85 -0
- package/extensions/core/background-observer.ts +279 -0
- package/extensions/core/bond-recovery.ts +65 -0
- package/extensions/core/bond-state.ts +80 -0
- package/extensions/core/clear-conversation.ts +53 -0
- package/extensions/core/client-versions.ts +56 -0
- package/extensions/core/client.ts +704 -0
- package/extensions/core/command-offer.ts +88 -0
- package/extensions/core/command-runner.ts +62 -0
- package/extensions/core/command-turn.ts +88 -0
- package/extensions/core/commit-ledger.ts +195 -0
- package/extensions/core/commit-stamp.ts +98 -0
- package/extensions/core/config.ts +63 -0
- package/extensions/core/connection-context.ts +28 -0
- package/extensions/core/context-budget.ts +175 -0
- package/extensions/core/control-result-outbox.ts +80 -0
- package/extensions/core/control.ts +156 -0
- package/extensions/core/ctx-guard.ts +33 -0
- package/extensions/core/debug.ts +136 -0
- package/extensions/core/delivery-queue.ts +92 -0
- package/extensions/core/dispatch.ts +108 -0
- package/extensions/core/duck.ts +128 -0
- package/extensions/core/git-observer.ts +382 -0
- package/extensions/core/github-identity.ts +26 -0
- package/extensions/core/implementation-contract.ts +773 -0
- package/extensions/core/instructions.ts +40 -0
- package/extensions/core/interaction-events.ts +834 -0
- package/extensions/core/json-schema.ts +16 -0
- package/extensions/core/local-activity.ts +34 -0
- package/extensions/core/mcp-config-guard.ts +117 -0
- package/extensions/core/model-catalog.ts +76 -0
- package/extensions/core/model-surface.ts +125 -0
- package/extensions/core/notice-reply.ts +215 -0
- package/extensions/core/owner-inject.ts +148 -0
- package/extensions/core/post-session-receipt.ts +70 -0
- package/extensions/core/preempt.ts +100 -0
- package/extensions/core/project-choice.ts +145 -0
- package/extensions/core/project-scope.ts +335 -0
- package/extensions/core/project-selection-ui.ts +202 -0
- package/extensions/core/provenance-refs.ts +158 -0
- package/extensions/core/push-channel.ts +281 -0
- package/extensions/core/question-receipt-lifecycle.ts +158 -0
- package/extensions/core/recall.ts +121 -0
- package/extensions/core/remote-payload.ts +1329 -0
- package/extensions/core/remote-session-arg.ts +24 -0
- package/extensions/core/repository-context.ts +35 -0
- package/extensions/core/request-chain.ts +1244 -0
- package/extensions/core/result-projection.ts +939 -0
- package/extensions/core/result.ts +100 -0
- package/extensions/core/review-context.ts +70 -0
- package/extensions/core/review-watch.ts +94 -0
- package/extensions/core/room-access.ts +12 -0
- package/extensions/core/room-context.ts +178 -0
- package/extensions/core/room-reader.ts +308 -0
- package/extensions/core/room-store.ts +381 -0
- package/extensions/core/room-sync.ts +79 -0
- package/extensions/core/room-tools.ts +86 -0
- package/extensions/core/room-unread.ts +173 -0
- package/extensions/core/session-events.ts +62 -0
- package/extensions/core/session-polls.ts +292 -0
- package/extensions/core/settings.ts +39 -0
- package/extensions/core/status-line.ts +432 -0
- package/extensions/core/still-to-discuss.ts +124 -0
- package/extensions/core/subagent-fleet.ts +238 -0
- package/extensions/core/system-notice.ts +227 -0
- package/extensions/core/telemetry.ts +177 -0
- package/extensions/core/tool-selection.ts +72 -0
- package/extensions/core/trail-events.ts +247 -0
- package/extensions/core/trail-projection.ts +373 -0
- package/extensions/core/trail.ts +465 -0
- package/extensions/core/turn-lifecycle.ts +472 -0
- package/extensions/core/version-mismatch.ts +165 -0
- package/extensions/core/work-prompt.ts +44 -0
- package/extensions/core/work-state.ts +179 -0
- package/extensions/devspec-adapter-guard.ts +106 -0
- package/extensions/devspec-auth.ts +191 -0
- package/extensions/devspec-brand.ts +140 -0
- package/extensions/devspec-commit-ledger.ts +477 -0
- package/extensions/devspec-context.ts +146 -0
- package/extensions/devspec-memory.ts +136 -0
- package/extensions/devspec-provenance-assistance.ts +233 -0
- package/extensions/devspec-remote.ts +5152 -0
- package/extensions/devspec-tools.ts +552 -0
- package/extensions/devspec-work.ts +244 -0
- package/extensions/generated/README.md +18 -0
- package/extensions/generated/mcp-catalog.ts +15811 -0
- package/package.json +71 -4
- package/prompts/.gitkeep +0 -0
- package/runtime/commit-stamp.mjs +279 -0
- package/runtime/git-observer-shim.mjs +390 -0
- package/skills/.gitkeep +0 -0
- package/themes/.gitkeep +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devspecai/pi-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.10.3",
|
|
4
|
+
"description": "DevSpec for Pi — native tools, auth, remote control, and memory for the Pi agent. One install, everything included.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"devspec",
|
|
10
|
+
"coding-agent"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/DevSpecAI/DevSpec-Pi.git"
|
|
15
|
+
},
|
|
16
|
+
"pi": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"./extensions"
|
|
19
|
+
],
|
|
20
|
+
"skills": [
|
|
21
|
+
"./skills"
|
|
22
|
+
],
|
|
23
|
+
"prompts": [
|
|
24
|
+
"./prompts"
|
|
25
|
+
],
|
|
26
|
+
"themes": [
|
|
27
|
+
"./themes"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"benchmark:context": "node --experimental-strip-types scripts/context-benchmark.ts",
|
|
32
|
+
"check": "npm run typecheck && npm test && npm run benchmark:context",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"audit:gate": "node scripts/audit-gate.mjs",
|
|
35
|
+
"generate:catalog": "node scripts/generate-mcp-catalog.mjs",
|
|
36
|
+
"test": "node --test --test-concurrency=2 tests/*.test.ts",
|
|
37
|
+
"test:room-runtime": "node scripts/e2e-room-store.mjs",
|
|
38
|
+
"test:room-readers-runtime": "node scripts/e2e-room-store.mjs --readers",
|
|
39
|
+
"test:core-paths": "node --test --test-concurrency=2 tests/turn-lifecycle.test.ts tests/command-turn-wiring.test.ts tests/canonical-ingress-wiring.test.ts tests/assistant-speech-delivery.test.ts tests/post-session-receipt.test.ts tests/catalog-drift.test.ts tests/attach-context.test.ts tests/recovery-wiring.test.ts tests/debug-privacy.test.ts tests/provenance-refs.test.ts"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
43
|
+
"minisearch": "^7.2.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0",
|
|
47
|
+
"@earendil-works/pi-tui": "*"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@earendil-works/pi-ai": "^1.1.0",
|
|
51
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
52
|
+
"@earendil-works/pi-tui": "^1.1.0",
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
"typebox": "^1.3.12",
|
|
55
|
+
"typescript": "^5.6.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=22.19.0"
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"extensions",
|
|
62
|
+
"runtime",
|
|
63
|
+
"skills",
|
|
64
|
+
"prompts",
|
|
65
|
+
"themes",
|
|
66
|
+
"README.md",
|
|
67
|
+
"CHANGELOG.md",
|
|
68
|
+
"LICENSE"
|
|
69
|
+
],
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/prompts/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevSpec-Pi commit tag stamping, inside the private git stand-in (DevSpec item
|
|
3
|
+
* df71ab6c; the owner settled the reversal of 22f10809, 2c971806 and 5f9fd5dc).
|
|
4
|
+
*
|
|
5
|
+
* The stand-in sees git's exact argv after the shell has evaluated it, so nothing
|
|
6
|
+
* here reads shell text. A plain `git commit` gets the claimed task's
|
|
7
|
+
* `[devspec:<uuid>]` appended to its subject line only when every condition holds:
|
|
8
|
+
* exactly one task is claimed, the repository's origin remote or project pin
|
|
9
|
+
* matches that task's project, the message comes from `-m`, `-F <file>` or `-F -`,
|
|
10
|
+
* none of the message-inheriting forms is used, and the message carries no
|
|
11
|
+
* `[devspec:` reference already. Anything else runs exactly as before. When Pi
|
|
12
|
+
* cannot tag, it says why; it never blocks a commit.
|
|
13
|
+
*
|
|
14
|
+
* Self-contained on purpose: the stand-in copies this file next to itself.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
18
|
+
import { readFileSync, readSync, realpathSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { isatty } from "node:tty";
|
|
20
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
21
|
+
|
|
22
|
+
export const STAMP_ENV = Object.freeze({
|
|
23
|
+
project: "DEVSPEC_PI_STAMP_PROJECT",
|
|
24
|
+
claims: "DEVSPEC_PI_STAMP_CLAIMS",
|
|
25
|
+
task: "DEVSPEC_PI_STAMP_TASK",
|
|
26
|
+
remote: "DEVSPEC_PI_STAMP_REMOTE_SHA256",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
30
|
+
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
31
|
+
const REFERENCE = /\[devspec:/iu;
|
|
32
|
+
|
|
33
|
+
/** What the host armed for this one tool call, or null when nothing may be stamped. */
|
|
34
|
+
export function readStampScope(env) {
|
|
35
|
+
const projectId = env[STAMP_ENV.project]?.toLowerCase();
|
|
36
|
+
const claims = Number.parseInt(env[STAMP_ENV.claims] ?? "", 10);
|
|
37
|
+
if (!projectId || !UUID.test(projectId) || !Number.isSafeInteger(claims) || claims < 1) return null;
|
|
38
|
+
const task = env[STAMP_ENV.task]?.toLowerCase() || null;
|
|
39
|
+
if (claims === 1 ? !task || !UUID.test(task) : task !== null) return null;
|
|
40
|
+
const remote = env[STAMP_ENV.remote]?.toLowerCase() || null;
|
|
41
|
+
if (remote !== null && !SHA256.test(remote)) return null;
|
|
42
|
+
return { projectId, claims, taskId: task, remoteSha256: remote };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function remoteFingerprint(url) {
|
|
46
|
+
return createHash("sha256").update(url.trim()).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The directory git runs in after its own `-C` options, or null for a git-dir override. */
|
|
50
|
+
export function effectiveCwd(globals, cwd) {
|
|
51
|
+
let dir = cwd;
|
|
52
|
+
for (let i = 0; i < globals.length; i += 1) {
|
|
53
|
+
const arg = globals[i];
|
|
54
|
+
if (arg === "-C") { dir = resolve(dir, globals[i + 1]); i += 1; continue; }
|
|
55
|
+
if (arg.startsWith("-C")) { dir = resolve(dir, arg.slice(2)); continue; }
|
|
56
|
+
if (arg === "--git-dir" || arg === "--work-tree" || arg.startsWith("--git-dir=") || arg.startsWith("--work-tree=")) return null;
|
|
57
|
+
}
|
|
58
|
+
return dir;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const SKIP_LONG = new Map([
|
|
62
|
+
["--amend", "amend"], ["--fixup", "fixup"], ["--squash", "squash"],
|
|
63
|
+
["--reedit-message", "reuse_message"], ["--reuse-message", "reuse_message"],
|
|
64
|
+
["--no-edit", "no_edit"], ["--edit", "edit"], ["--template", "template"],
|
|
65
|
+
["--interactive", "interactive"], ["--patch", "interactive"],
|
|
66
|
+
["--dry-run", "dry_run"], ["--short", "dry_run"], ["--porcelain", "dry_run"], ["--long", "dry_run"], ["--null", "dry_run"], ["--branch", "dry_run"],
|
|
67
|
+
]);
|
|
68
|
+
const FLAG_LONG = new Set([
|
|
69
|
+
"--all", "--no-all", "--quiet", "--no-quiet", "--verbose", "--no-verbose", "--signoff", "--no-signoff",
|
|
70
|
+
"--no-verify", "--verify", "--allow-empty", "--allow-empty-message", "--no-post-rewrite", "--post-rewrite",
|
|
71
|
+
"--include", "--no-include", "--only", "--no-only", "--reset-author", "--no-reset-author",
|
|
72
|
+
"--status", "--no-status", "--no-gpg-sign", "--pathspec-file-nul", "--ahead-behind", "--no-ahead-behind",
|
|
73
|
+
]);
|
|
74
|
+
const VALUE_LONG = new Set(["--author", "--date", "--cleanup", "--trailer", "--pathspec-from-file", "--unified", "--inter-hunk-context"]);
|
|
75
|
+
const ATTACHED_ONLY_LONG = new Set(["--gpg-sign", "--untracked-files"]);
|
|
76
|
+
const SHORT_FLAGS = new Set(["a", "q", "v", "s", "n", "i", "o"]);
|
|
77
|
+
const SHORT_SKIP = new Map([["e", "edit"], ["p", "interactive"], ["z", "dry_run"], ["c", "reuse_message"], ["C", "reuse_message"], ["t", "template"]]);
|
|
78
|
+
const SHORT_OPTIONAL = new Set(["u", "S"]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Where a commit's message comes from, read from `git commit`'s own argv (the
|
|
82
|
+
* arguments after `commit`). Every shape this does not know is a reason to leave
|
|
83
|
+
* the commit alone.
|
|
84
|
+
*/
|
|
85
|
+
export function readCommitMessageSource(rest) {
|
|
86
|
+
const messages = [];
|
|
87
|
+
const files = [];
|
|
88
|
+
let options = true;
|
|
89
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
90
|
+
const arg = rest[i];
|
|
91
|
+
if (!options || arg === "-" || !arg.startsWith("-")) continue; // pathspecs
|
|
92
|
+
if (arg === "--") { options = false; continue; }
|
|
93
|
+
if (arg.startsWith("--")) {
|
|
94
|
+
const eq = arg.indexOf("=");
|
|
95
|
+
const name = eq === -1 ? arg : arg.slice(0, eq);
|
|
96
|
+
if (name === "--message" || name === "--file") {
|
|
97
|
+
const target = name === "--message" ? messages : files;
|
|
98
|
+
if (eq !== -1) target.push({ index: i, prefix: `${name}=` });
|
|
99
|
+
else if (i + 1 < rest.length) { target.push({ index: i + 1, prefix: "" }); i += 1; }
|
|
100
|
+
else return { skip: "unrecognised_option" };
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (SKIP_LONG.has(name)) return { skip: SKIP_LONG.get(name) };
|
|
104
|
+
if (FLAG_LONG.has(name) && eq === -1) continue;
|
|
105
|
+
if (VALUE_LONG.has(name)) {
|
|
106
|
+
if (eq === -1) { if (i + 1 >= rest.length) return { skip: "unrecognised_option" }; i += 1; }
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ATTACHED_ONLY_LONG.has(name)) continue;
|
|
110
|
+
return { skip: "unrecognised_option" };
|
|
111
|
+
}
|
|
112
|
+
for (let j = 1; j < arg.length; j += 1) {
|
|
113
|
+
const c = arg[j];
|
|
114
|
+
if (SHORT_FLAGS.has(c)) continue;
|
|
115
|
+
if (SHORT_SKIP.has(c)) return { skip: SHORT_SKIP.get(c) };
|
|
116
|
+
if (SHORT_OPTIONAL.has(c)) break; // the rest of the cluster is its value
|
|
117
|
+
if (c === "m" || c === "F" || c === "U") {
|
|
118
|
+
const target = c === "m" ? messages : c === "F" ? files : null;
|
|
119
|
+
if (j + 1 < arg.length) target?.push({ index: i, prefix: arg.slice(0, j + 1) });
|
|
120
|
+
else if (i + 1 < rest.length) { target?.push({ index: i + 1, prefix: "" }); i += 1; }
|
|
121
|
+
else return { skip: "unrecognised_option" };
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return { skip: "unrecognised_option" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (messages.length > 0 && files.length > 0) return { skip: "conflicting_messages" };
|
|
128
|
+
if (files.length > 1) return { skip: "conflicting_messages" };
|
|
129
|
+
if (messages.length > 0) {
|
|
130
|
+
return { source: "message", first: messages[0], values: messages.map((entry) => rest[entry.index].slice(entry.prefix.length)) };
|
|
131
|
+
}
|
|
132
|
+
if (files.length === 1) {
|
|
133
|
+
const value = rest[files[0].index].slice(files[0].prefix.length);
|
|
134
|
+
return value === "-" ? { source: "stdin" } : { source: "file", entry: files[0], path: value };
|
|
135
|
+
}
|
|
136
|
+
return { skip: "editor" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The message with the tag at the end of its first line that has text and is not
|
|
141
|
+
* a `#` comment: the subject for every ordinary message, never after a trailer.
|
|
142
|
+
* Null when there is no such line. Works on a latin1 view, so every byte of a
|
|
143
|
+
* file or stdin round-trips exactly.
|
|
144
|
+
*/
|
|
145
|
+
export function stampMessage(text, taskId) {
|
|
146
|
+
const lines = text.split("\n");
|
|
147
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
148
|
+
const line = lines[i];
|
|
149
|
+
if (line.trim() === "" || line.startsWith("#")) continue;
|
|
150
|
+
const cr = line.endsWith("\r");
|
|
151
|
+
lines[i] = `${cr ? line.slice(0, -1) : line} [devspec:${taskId}]${cr ? "\r" : ""}`;
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function stampMessageBytes(bytes, taskId) {
|
|
158
|
+
const stamped = stampMessage(bytes.toString("latin1"), taskId);
|
|
159
|
+
return stamped === null ? null : Buffer.from(stamped, "latin1");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function carriesReference(value) {
|
|
163
|
+
return REFERENCE.test(Buffer.isBuffer(value) ? value.toString("latin1") : value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readPin(dir) {
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(readFileSync(join(dir, ".devspec", "project.json"), "utf8"));
|
|
169
|
+
const id = parsed && typeof parsed === "object" ? parsed.project_id : null;
|
|
170
|
+
return typeof id === "string" && id.trim() ? id.trim().toLowerCase() : null;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Whether the commit's repository belongs to the claimed task's project: its
|
|
178
|
+
* origin remote is the one the host verified, or its nearest pin (walking up to
|
|
179
|
+
* the worktree root, then the main worktree) names that project.
|
|
180
|
+
*/
|
|
181
|
+
export function repositoryInProject(run, worktree, cwd, scope) {
|
|
182
|
+
if (scope.remoteSha256) {
|
|
183
|
+
const remote = run(["-C", worktree, "remote", "get-url", "origin"]);
|
|
184
|
+
if (remote.status === 0 && remote.stdout.trim() && remoteFingerprint(remote.stdout) === scope.remoteSha256) return true;
|
|
185
|
+
}
|
|
186
|
+
const root = resolve(worktree);
|
|
187
|
+
let dir = resolve(cwd ?? worktree);
|
|
188
|
+
try { dir = realpathSync(dir); } catch { /* a missing directory has no pin */ }
|
|
189
|
+
if (dir === root || dir.startsWith(`${root}${sep}`)) {
|
|
190
|
+
for (;;) {
|
|
191
|
+
const pin = readPin(dir);
|
|
192
|
+
if (pin !== null) return pin === scope.projectId;
|
|
193
|
+
if (dir === root) break;
|
|
194
|
+
dir = dirname(dir);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const common = run(["-C", worktree, "rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
198
|
+
const commonDir = common.status === 0 ? common.stdout.trim() : "";
|
|
199
|
+
if (commonDir && basename(commonDir) === ".git") {
|
|
200
|
+
const pin = readPin(dirname(commonDir));
|
|
201
|
+
if (pin !== null) return pin === scope.projectId;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Read all of stdin. `complete` is false when a read failed part-way. */
|
|
207
|
+
export function readAllStdin(fd = 0) {
|
|
208
|
+
const chunks = [];
|
|
209
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
210
|
+
for (;;) {
|
|
211
|
+
let read;
|
|
212
|
+
try {
|
|
213
|
+
read = readSync(fd, buffer, 0, buffer.length, null);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error && error.code === "EAGAIN") { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5); continue; }
|
|
216
|
+
if (error && error.code === "EOF") return { bytes: Buffer.concat(chunks), complete: true };
|
|
217
|
+
return { bytes: Buffer.concat(chunks), complete: false };
|
|
218
|
+
}
|
|
219
|
+
if (read === 0) return { bytes: Buffer.concat(chunks), complete: true };
|
|
220
|
+
chunks.push(Buffer.from(buffer.subarray(0, read)));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function report(scope, outcome, reason) {
|
|
225
|
+
return { v: 1, outcome, reason, task: scope.taskId, claims: scope.claims };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Why a commit that never reached the stamping step was left alone. */
|
|
229
|
+
export function unplannedReport(scope, inProject) {
|
|
230
|
+
if (!inProject) return report(scope, "skipped", "outside_project");
|
|
231
|
+
if (scope.claims !== 1) return report(scope, "skipped", "several_claims");
|
|
232
|
+
return report(scope, "skipped", "not_plain_commit");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Decide, for one `git commit` invocation, whether and how to tag it. Returns the
|
|
237
|
+
* report to journal and, when stamping, the replacement argv, stdin or private
|
|
238
|
+
* message file. The person's own message file is never written.
|
|
239
|
+
*/
|
|
240
|
+
export function planCommitStamp({ args, commandIndex, globals, cwd, worktree, scope, run, privateDir, stdinIsTerminal = () => isatty(0), readStdin = readAllStdin }) {
|
|
241
|
+
const dir = effectiveCwd(globals, cwd);
|
|
242
|
+
if (dir === null) return { report: report(scope, "skipped", repositoryInProject(run, worktree, null, scope) ? "not_plain_commit" : "outside_project") };
|
|
243
|
+
if (!repositoryInProject(run, worktree, dir, scope)) return { report: report(scope, "skipped", "outside_project") };
|
|
244
|
+
if (scope.claims !== 1 || !scope.taskId) return { report: report(scope, "skipped", "several_claims") };
|
|
245
|
+
const rest = args.slice(commandIndex + 1);
|
|
246
|
+
const source = readCommitMessageSource(rest);
|
|
247
|
+
if (source.skip) return { report: report(scope, "skipped", source.skip) };
|
|
248
|
+
const taskId = scope.taskId;
|
|
249
|
+
if (source.source === "message") {
|
|
250
|
+
if (source.values.some((value) => carriesReference(value))) return { report: report(scope, "skipped", "has_reference") };
|
|
251
|
+
const stamped = stampMessage(source.values[0], taskId);
|
|
252
|
+
if (stamped === null) return { report: report(scope, "skipped", "no_subject") };
|
|
253
|
+
const next = [...args];
|
|
254
|
+
next[commandIndex + 1 + source.first.index] = `${source.first.prefix}${stamped}`;
|
|
255
|
+
return { report: report(scope, "stamped", null), args: next };
|
|
256
|
+
}
|
|
257
|
+
if (source.source === "file") {
|
|
258
|
+
let bytes;
|
|
259
|
+
try { bytes = readFileSync(resolve(dir, source.path)); } catch { return { report: report(scope, "skipped", "message_file_unreadable") }; }
|
|
260
|
+
if (carriesReference(bytes)) return { report: report(scope, "skipped", "has_reference") };
|
|
261
|
+
const stamped = stampMessageBytes(bytes, taskId);
|
|
262
|
+
if (stamped === null) return { report: report(scope, "skipped", "no_subject") };
|
|
263
|
+
const copy = join(privateDir, `message-${randomUUID()}.txt`);
|
|
264
|
+
try { writeFileSync(copy, stamped, { mode: 0o600, flag: "wx" }); } catch { return { report: report(scope, "skipped", "message_file_unreadable") }; }
|
|
265
|
+
const next = [...args];
|
|
266
|
+
next[commandIndex + 1 + source.entry.index] = `${source.entry.prefix}${copy}`;
|
|
267
|
+
return { report: report(scope, "stamped", null), args: next, privateFile: copy };
|
|
268
|
+
}
|
|
269
|
+
// `-F -`: git would read all of stdin anyway. Whatever is read reaches git
|
|
270
|
+
// exactly as it was unless it is tagged; a failed read hands over what was read
|
|
271
|
+
// and then the rest of stdin untouched.
|
|
272
|
+
if (stdinIsTerminal()) return { report: report(scope, "skipped", "terminal_stdin") };
|
|
273
|
+
const { bytes, complete } = readStdin();
|
|
274
|
+
if (!complete) return { report: report(scope, "skipped", "message_unreadable"), stdin: bytes, stdinRest: true };
|
|
275
|
+
if (carriesReference(bytes)) return { report: report(scope, "skipped", "has_reference"), stdin: bytes };
|
|
276
|
+
const stamped = stampMessageBytes(bytes, taskId);
|
|
277
|
+
if (stamped === null) return { report: report(scope, "skipped", "no_subject"), stdin: bytes };
|
|
278
|
+
return { report: report(scope, "stamped", null), stdin: stamped };
|
|
279
|
+
}
|