pi-graphite 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/lib/argv.ts +22 -0
- package/src/lib/result.ts +8 -3
- package/src/tools/change.ts +2 -2
- package/src/tools/navigate.ts +2 -2
- package/src/tools/recover.ts +2 -1
- package/src/tools/setup.ts +2 -2
- package/src/tools/submit.ts +16 -4
- package/src/tools/sync.ts +2 -1
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ dependent branches.
|
|
|
79
79
|
- Editor / pager / browser env is forced safe (`GT_EDITOR=true`, `GT_PAGER=`,
|
|
80
80
|
`BROWSER=true`, …). Commands have a hard timeout.
|
|
81
81
|
- Interactive editor / hunk / browser / reorder paths are not exposed.
|
|
82
|
+
- Commands echoed in tool output are safe to copy-paste back into a shell.
|
|
82
83
|
- `graphite_setup action=track_branch` requires explicit `branch`, explicit
|
|
83
84
|
`parent`, and `confirmParent:true`; do not guess parent if unclear.
|
|
84
85
|
- `graphite_setup action=init_repo reset:true` needs `confirmDestructive:true`.
|
|
@@ -94,6 +95,11 @@ dependent branches.
|
|
|
94
95
|
`branchNotTracked`, `noChangesStaged`, `checkedOutElsewhere`,
|
|
95
96
|
`operatingOnTrunk`, …).
|
|
96
97
|
|
|
98
|
+
### Git hooks
|
|
99
|
+
|
|
100
|
+
Git hooks in the target repository run as normal; this extension does not
|
|
101
|
+
bypass them. Treat them as part of your repo's trust boundary.
|
|
102
|
+
|
|
97
103
|
## License
|
|
98
104
|
|
|
99
105
|
MIT
|
package/package.json
CHANGED
package/src/lib/argv.ts
CHANGED
|
@@ -57,3 +57,25 @@ export function flagEq(flag: string, value: string | number): string {
|
|
|
57
57
|
}
|
|
58
58
|
return `${flag}=${value}`;
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* POSIX shell single-quote a value so the rendered command line is safe to
|
|
63
|
+
* copy-paste into a shell. argv execution itself never goes through a shell
|
|
64
|
+
* (we use spawn with an argv array), but rendered commands appear in tool
|
|
65
|
+
* output and labels; a user pasting them must not trigger command
|
|
66
|
+
* substitution, word splitting, or metacharacter interpretation.
|
|
67
|
+
*
|
|
68
|
+
* Rule: wrap in single quotes, and replace each embedded single quote with
|
|
69
|
+
* the POSIX-portable sequence '\''. Tokens consisting solely of
|
|
70
|
+
* [A-Za-z0-9_=:,.@/+-] are left unquoted for readability.
|
|
71
|
+
*/
|
|
72
|
+
export function shellQuote(arg: string): string {
|
|
73
|
+
if (arg.length === 0) return "''";
|
|
74
|
+
if (/^[A-Za-z0-9_=:,.@\/+\-]+$/.test(arg)) return arg;
|
|
75
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Join argv tokens into a shell-safe single line. */
|
|
79
|
+
export function shellJoin(args: readonly string[]): string {
|
|
80
|
+
return args.map(shellQuote).join(" ");
|
|
81
|
+
}
|
package/src/lib/result.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { shellJoin } from "./argv";
|
|
3
|
+
import { DEFAULT_COMMAND_TIMEOUT_MS, killProcessGroup, runGt, safeNoninteractiveEnv, type GtRunResult } from "./exec";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Structured failure hints. Only populated when the underlying `gt` command
|
|
@@ -108,7 +109,7 @@ export function formatResult(r: GtRunResult): FormattedResult {
|
|
|
108
109
|
export function renderText(label: string, f: FormattedResult): string {
|
|
109
110
|
const r = f.result;
|
|
110
111
|
const lines: string[] = [];
|
|
111
|
-
lines.push(`$ gt ${r.args
|
|
112
|
+
lines.push(`$ gt ${shellJoin(r.args)}`);
|
|
112
113
|
lines.push(
|
|
113
114
|
`# cwd=${r.cwd} exit=${r.exitCode}${r.timedOut ? " (aborted)" : ""}${
|
|
114
115
|
r.spawnError ? ` (spawn-error: ${r.spawnError})` : ""
|
|
@@ -176,7 +177,11 @@ async function detectCurrentBranch(cwd: string): Promise<string | undefined> {
|
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
async function detectTrunk(cwd: string): Promise<string | undefined> {
|
|
179
|
-
|
|
180
|
+
// Route through the hardened runner so cwd resolve, forbidden-token scan,
|
|
181
|
+
// trailing --no-interactive injection, and env scrubbing all apply.
|
|
182
|
+
const r = await runGt(["trunk"], { cwd }).catch(() => undefined);
|
|
183
|
+
if (!r || r.exitCode !== 0) return undefined;
|
|
184
|
+
const out = r.stdout;
|
|
180
185
|
// gt trunk prints the trunk name on its own line. Take last non-empty line.
|
|
181
186
|
const cleaned = out
|
|
182
187
|
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
|
package/src/tools/change.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import { assertSafeRef, flagEq } from "../lib/argv";
|
|
3
|
+
import { assertSafeRef, flagEq, shellJoin } from "../lib/argv";
|
|
4
4
|
import { ensureSuccess, renderText } from "../lib/result";
|
|
5
5
|
import {
|
|
6
6
|
CwdParam,
|
|
@@ -128,7 +128,7 @@ export function registerChange(pi: ExtensionAPI) {
|
|
|
128
128
|
break;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
const label = `gt ${args
|
|
131
|
+
const label = `gt ${shellJoin(args)}`;
|
|
132
132
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
133
133
|
const f = await ensureSuccess(label, r, p.cwd);
|
|
134
134
|
return {
|
package/src/tools/navigate.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import { assertSafeRef, flagEq } from "../lib/argv";
|
|
3
|
+
import { assertSafeRef, flagEq, shellJoin } from "../lib/argv";
|
|
4
4
|
import { ensureSuccess, renderText } from "../lib/result";
|
|
5
5
|
import {
|
|
6
6
|
CwdParam,
|
|
@@ -87,7 +87,7 @@ export function registerNavigate(pi: ExtensionAPI) {
|
|
|
87
87
|
args = ["bottom"];
|
|
88
88
|
break;
|
|
89
89
|
}
|
|
90
|
-
const label = `gt ${args
|
|
90
|
+
const label = `gt ${shellJoin(args)}`;
|
|
91
91
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
92
92
|
const f = await ensureSuccess(label, r, p.cwd);
|
|
93
93
|
return {
|
package/src/tools/recover.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
runGt,
|
|
8
8
|
safeNoninteractiveEnv,
|
|
9
9
|
} from "../lib/exec";
|
|
10
|
+
import { shellJoin } from "../lib/argv";
|
|
10
11
|
import { ensureSuccess, renderText } from "../lib/result";
|
|
11
12
|
import { CwdParam, StringEnum, Type, type ToolReturn } from "../lib/schema";
|
|
12
13
|
|
|
@@ -135,7 +136,7 @@ export function registerRecover(pi: ExtensionAPI) {
|
|
|
135
136
|
if (p.force) args.push("--force");
|
|
136
137
|
break;
|
|
137
138
|
}
|
|
138
|
-
const label = `gt ${args
|
|
139
|
+
const label = `gt ${shellJoin(args)}`;
|
|
139
140
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
140
141
|
const f = await ensureSuccess(label, r, p.cwd);
|
|
141
142
|
return {
|
package/src/tools/setup.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import { assertSafeRef, flagEq } from "../lib/argv";
|
|
3
|
+
import { assertSafeRef, flagEq, shellJoin } from "../lib/argv";
|
|
4
4
|
import { ensureSuccess, renderText } from "../lib/result";
|
|
5
5
|
import {
|
|
6
6
|
CwdParam,
|
|
@@ -109,7 +109,7 @@ export function registerSetup(pi: ExtensionAPI) {
|
|
|
109
109
|
if (p.force) args.push("--force");
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const label = `gt ${args
|
|
112
|
+
const label = `gt ${shellJoin(args)}`;
|
|
113
113
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
114
114
|
const f = await ensureSuccess(label, r, p.cwd);
|
|
115
115
|
return {
|
package/src/tools/submit.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { runGt } from "../lib/exec";
|
|
3
|
-
import { assertSafeRef, flagEq } from "../lib/argv";
|
|
3
|
+
import { assertSafeRef, flagEq, shellJoin } from "../lib/argv";
|
|
4
4
|
import { ensureSuccess, renderText } from "../lib/result";
|
|
5
5
|
import {
|
|
6
6
|
CwdParam,
|
|
@@ -104,18 +104,30 @@ export function registerSubmitStack(pi: ExtensionAPI) {
|
|
|
104
104
|
if (p.mergeWhenReady) args.push("--merge-when-ready");
|
|
105
105
|
if (p.rerequestReview) args.push("--rerequest-review");
|
|
106
106
|
|
|
107
|
+
const assertReviewer = (rv: string, label: string) => {
|
|
108
|
+
assertSafeRef(rv, label);
|
|
109
|
+
// gt joins reviewers on ',' before calling gh. Reject any element
|
|
110
|
+
// that itself contains a comma or whitespace so a single array
|
|
111
|
+
// entry cannot expand into multiple reviewers.
|
|
112
|
+
if (/[,\s]/.test(rv)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`${label} must not contain commas or whitespace (got ${JSON.stringify(rv)}). ` +
|
|
115
|
+
`Pass each reviewer as a separate array element.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
107
119
|
if (p.reviewers && p.reviewers.length) {
|
|
108
|
-
for (const rv of p.reviewers)
|
|
120
|
+
for (const rv of p.reviewers) assertReviewer(rv, "reviewers[]");
|
|
109
121
|
args.push(flagEq("--reviewers", p.reviewers.join(",")));
|
|
110
122
|
}
|
|
111
123
|
if (p.teamReviewers && p.teamReviewers.length) {
|
|
112
|
-
for (const rv of p.teamReviewers)
|
|
124
|
+
for (const rv of p.teamReviewers) assertReviewer(rv, "teamReviewers[]");
|
|
113
125
|
args.push(flagEq("--team-reviewers", p.teamReviewers.join(",")));
|
|
114
126
|
}
|
|
115
127
|
if (p.forcePush) args.push("--force");
|
|
116
128
|
if (p.ignoreOutOfSyncTrunk) args.push("--ignore-out-of-sync-trunk");
|
|
117
129
|
|
|
118
|
-
const label = `gt ${args
|
|
130
|
+
const label = `gt ${shellJoin(args)}`;
|
|
119
131
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
120
132
|
const f = await ensureSuccess(label, r, p.cwd);
|
|
121
133
|
return {
|
package/src/tools/sync.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { shellJoin } from "../lib/argv";
|
|
2
3
|
import { runGt } from "../lib/exec";
|
|
3
4
|
import { ensureSuccess, renderText } from "../lib/result";
|
|
4
5
|
import {
|
|
@@ -67,7 +68,7 @@ export function registerSync(pi: ExtensionAPI) {
|
|
|
67
68
|
if (p.force) args.push("--force");
|
|
68
69
|
if (p.restack === false) args.push("--no-restack");
|
|
69
70
|
|
|
70
|
-
const label = `gt ${args
|
|
71
|
+
const label = `gt ${shellJoin(args)}`;
|
|
71
72
|
const r = await runGt(args, { cwd: p.cwd, signal });
|
|
72
73
|
const f = await ensureSuccess(label, r, p.cwd);
|
|
73
74
|
return {
|