@nmzpy/pi-ember-stack 0.1.6 → 0.2.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 +83 -83
- package/package.json +62 -48
- package/plugins/devin-auth/extensions/index.ts +68 -7
- package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
- package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
- package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
- package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
- package/plugins/devin-auth/src/models.ts +42 -196
- package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
- package/plugins/devin-auth/src/oauth/types.ts +71 -71
- package/plugins/devin-auth/src/stream.ts +1 -1
- package/plugins/index.ts +29 -6
- package/plugins/pi-compact-tools/index.ts +35 -192
- package/plugins/pi-compact-tools/renderer.ts +589 -0
- package/plugins/pi-custom-agents/index.ts +727 -373
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
- package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +118 -226
- package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
- package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +260 -170
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +975 -0
- package/plugins/pi-ember-fff/query.ts +247 -0
- package/plugins/pi-ember-fff/test/query.test.ts +222 -0
- package/plugins/pi-ember-fff/test/renderer.test.ts +727 -0
- package/plugins/pi-ember-tps/index.ts +144 -0
- package/plugins/pi-ember-ui/ember.json +99 -0
- package/plugins/pi-ember-ui/index.ts +805 -0
- package/plugins/pi-ember-ui/mode-colors.ts +196 -0
- package/tsconfig.json +2 -1
- package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// External allowlist — auto-detected pi-coding-agent package directory
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/** The default alias for the pi-coding-agent package. */
|
|
9
|
+
export const PI_CODING_AGENT_ALIAS = "pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
export type ExternalAllowlistEntry = {
|
|
12
|
+
/** User-facing alias, e.g. "pi-coding-agent" (no leading ./). */
|
|
13
|
+
alias: string;
|
|
14
|
+
/** Auto-detected absolute directory. */
|
|
15
|
+
dir: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ExternalAllowlist = {
|
|
19
|
+
entries: readonly ExternalAllowlistEntry[];
|
|
20
|
+
/** Resolve a user-facing path constraint to an allowlisted dir, or undefined. */
|
|
21
|
+
resolve(pathConstraint: string): ExternalAllowlistEntry | undefined;
|
|
22
|
+
/** True if an absolute path is inside an allowlisted dir. */
|
|
23
|
+
covers(absolutePath: string): boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ExternalTarget = {
|
|
27
|
+
entry: ExternalAllowlistEntry;
|
|
28
|
+
/** Path relative to entry.dir, forward-slashed, no leading ./, or "" for the whole dir. */
|
|
29
|
+
relativePath: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Auto-detect the installed @earendil-works/pi-coding-agent package root.
|
|
34
|
+
* Returns undefined if detection fails (package not installed or
|
|
35
|
+
* import.meta.resolve unavailable).
|
|
36
|
+
*/
|
|
37
|
+
function detect_pi_package_dir(): string | undefined {
|
|
38
|
+
try {
|
|
39
|
+
const resolved = import.meta.resolve("@earendil-works/pi-coding-agent");
|
|
40
|
+
const entryPath = fileURLToPath(resolved); // .../dist/index.js
|
|
41
|
+
return path.dirname(path.dirname(entryPath)); // package root (parent of dist/)
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the external allowlist by auto-detecting the installed
|
|
49
|
+
* @earendil-works/pi-coding-agent package directory. Returns an empty
|
|
50
|
+
* allowlist if detection fails (fail-safe: no external access).
|
|
51
|
+
*/
|
|
52
|
+
export function buildExternalAllowlist(): ExternalAllowlist {
|
|
53
|
+
const dir = detect_pi_package_dir();
|
|
54
|
+
const entries: ExternalAllowlistEntry[] = [];
|
|
55
|
+
if (dir) {
|
|
56
|
+
entries.push({ alias: PI_CODING_AGENT_ALIAS, dir });
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
entries,
|
|
60
|
+
resolve(pathConstraint: string): ExternalAllowlistEntry | undefined {
|
|
61
|
+
let trimmed = pathConstraint.trim();
|
|
62
|
+
if (!trimmed) return undefined;
|
|
63
|
+
if (trimmed.startsWith("./")) trimmed = trimmed.slice(2);
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (trimmed === entry.alias) return entry;
|
|
66
|
+
if (
|
|
67
|
+
trimmed.startsWith(entry.alias + "/") ||
|
|
68
|
+
trimmed.startsWith(entry.alias + path.sep)
|
|
69
|
+
) {
|
|
70
|
+
return entry;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
},
|
|
75
|
+
covers(absolutePath: string): boolean {
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const rel = path.relative(entry.dir, absolutePath);
|
|
78
|
+
if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* If `pathConstraint` targets an allowlisted external directory, return
|
|
89
|
+
* the resolved entry and the sub-path relative to that dir. Otherwise
|
|
90
|
+
* undefined. This lets the tool layer route to a secondary FileFinder
|
|
91
|
+
* before normalization.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveExternalTarget(
|
|
94
|
+
pathConstraint: string | undefined,
|
|
95
|
+
allowlist: ExternalAllowlist,
|
|
96
|
+
): ExternalTarget | undefined {
|
|
97
|
+
if (!pathConstraint) return undefined;
|
|
98
|
+
let trimmed = pathConstraint.trim();
|
|
99
|
+
if (!trimmed) return undefined;
|
|
100
|
+
|
|
101
|
+
// Absolute path — check if it's under an allowlisted dir.
|
|
102
|
+
if (path.isAbsolute(trimmed)) {
|
|
103
|
+
if (!allowlist.covers(trimmed)) return undefined;
|
|
104
|
+
for (const entry of allowlist.entries) {
|
|
105
|
+
const rel = path.relative(entry.dir, trimmed).replaceAll(path.sep, "/");
|
|
106
|
+
if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) {
|
|
107
|
+
return { entry, relativePath: rel };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Strip leading ./
|
|
114
|
+
if (trimmed.startsWith("./")) trimmed = trimmed.slice(2);
|
|
115
|
+
|
|
116
|
+
// Alias-prefixed relative path
|
|
117
|
+
for (const entry of allowlist.entries) {
|
|
118
|
+
if (trimmed === entry.alias) {
|
|
119
|
+
return { entry, relativePath: "" };
|
|
120
|
+
}
|
|
121
|
+
// Check alias/ or alias\ as a prefix (cross-platform)
|
|
122
|
+
const aliasPrefixFwd = entry.alias + "/";
|
|
123
|
+
const aliasPrefixSep = entry.alias + path.sep;
|
|
124
|
+
if (trimmed.startsWith(aliasPrefixFwd) || trimmed.startsWith(aliasPrefixSep)) {
|
|
125
|
+
// Normalize to forward slashes for FFF
|
|
126
|
+
const remainder = trimmed.startsWith(aliasPrefixFwd)
|
|
127
|
+
? trimmed.slice(aliasPrefixFwd.length)
|
|
128
|
+
: trimmed.slice(aliasPrefixSep.length).replaceAll(path.sep, "/");
|
|
129
|
+
return { entry, relativePath: remainder };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Path constraint normalization
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export function normalizePathConstraint(
|
|
141
|
+
pathConstraint: string,
|
|
142
|
+
cwd = process.cwd(),
|
|
143
|
+
allowlist?: ExternalAllowlist,
|
|
144
|
+
): string | null {
|
|
145
|
+
let trimmed = pathConstraint.trim();
|
|
146
|
+
if (!trimmed) return trimmed;
|
|
147
|
+
|
|
148
|
+
if (path.isAbsolute(trimmed)) {
|
|
149
|
+
// Allowlisted external absolute path — do not throw.
|
|
150
|
+
if (allowlist?.covers(trimmed)) {
|
|
151
|
+
const target = resolveExternalTarget(trimmed, allowlist);
|
|
152
|
+
if (target) {
|
|
153
|
+
// Return the relative path within the allowlisted dir.
|
|
154
|
+
// If bare dir, return null (whole dir). Otherwise return the
|
|
155
|
+
// relative subpath forward-slashed.
|
|
156
|
+
if (!target.relativePath) return null;
|
|
157
|
+
trimmed = target.relativePath;
|
|
158
|
+
// Jump to the post-absolute-path normalization logic below.
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
const relative = path.relative(cwd, trimmed).replaceAll(path.sep, "/");
|
|
162
|
+
if (relative === "") return null;
|
|
163
|
+
if (relative.startsWith("../") || relative === ".." || path.isAbsolute(relative)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Path constraint must be relative to the workspace: ${pathConstraint}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
trimmed = relative;
|
|
169
|
+
}
|
|
170
|
+
} else if (allowlist) {
|
|
171
|
+
// Check for alias-prefixed relative path (e.g. ./pi-coding-agent/docs).
|
|
172
|
+
const target = resolveExternalTarget(trimmed, allowlist);
|
|
173
|
+
if (target) {
|
|
174
|
+
if (!target.relativePath) return null;
|
|
175
|
+
trimmed = target.relativePath;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (trimmed === "." || trimmed === "./") return null;
|
|
180
|
+
// Strip a leading `./` so `./**/*.rs` and `**/*.rs` behave identically.
|
|
181
|
+
if (trimmed.startsWith("./")) trimmed = trimmed.slice(2);
|
|
182
|
+
|
|
183
|
+
// FFF's glob matcher can treat a hidden directory root glob such as
|
|
184
|
+
// `.agents/**` as empty, while the tool contract says this means "inside
|
|
185
|
+
// this directory". Collapse simple trailing recursive directory globs to the
|
|
186
|
+
// directory-prefix constraint understood by the parser. Keep real file globs
|
|
187
|
+
// such as `src/**/*.ts` unchanged.
|
|
188
|
+
const recursiveDir = trimmed.match(/^(.*)\/\*\*(?:\/\*)?$/);
|
|
189
|
+
if (recursiveDir) {
|
|
190
|
+
const dir = recursiveDir[1];
|
|
191
|
+
if (dir && !/[*?[{]/.test(dir)) return `${dir}/`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Already signals path-constraint syntax to the parser.
|
|
195
|
+
if (trimmed.startsWith("/") || trimmed.endsWith("/")) return trimmed;
|
|
196
|
+
// Globs (`*.ts`, `src/**/*.cc`, `{src,lib}`) are handled by the parser.
|
|
197
|
+
if (/[*?[{]/.test(trimmed)) return trimmed;
|
|
198
|
+
// Filename with extension (`main.rs`, `config.json`) → FilePath constraint.
|
|
199
|
+
const lastSegment = trimmed.split("/").pop() ?? "";
|
|
200
|
+
if (/\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSegment)) return trimmed;
|
|
201
|
+
// Bare directory prefix → append `/` so the parser sees a PathSegment.
|
|
202
|
+
return `${trimmed}/`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Exclusions are emitted as `!<constraint>` tokens, which the Rust parser
|
|
206
|
+
// understands (crates/fff-query-parser/src/parser.rs). We normalize each one
|
|
207
|
+
// the same way as the include path so bare dirs become PathSegment excludes.
|
|
208
|
+
// Tolerate callers passing already-negated forms like `!src/` by stripping
|
|
209
|
+
// the leading `!` before normalizing so we never double-negate (`!!src/`).
|
|
210
|
+
export function normalizeExcludes(
|
|
211
|
+
exclude: string | string[] | undefined,
|
|
212
|
+
cwd = process.cwd(),
|
|
213
|
+
allowlist?: ExternalAllowlist,
|
|
214
|
+
): string[] {
|
|
215
|
+
if (!exclude) return [];
|
|
216
|
+
const list = Array.isArray(exclude) ? exclude : [exclude];
|
|
217
|
+
const out: string[] = [];
|
|
218
|
+
for (const raw of list) {
|
|
219
|
+
const parts = raw
|
|
220
|
+
.split(/[,\s]+/)
|
|
221
|
+
.map((s) => s.trim())
|
|
222
|
+
.filter(Boolean);
|
|
223
|
+
for (const p of parts) {
|
|
224
|
+
const stripped = p.startsWith("!") ? p.slice(1) : p;
|
|
225
|
+
const normalized = normalizePathConstraint(stripped, cwd, allowlist);
|
|
226
|
+
if (normalized) out.push(`!${normalized}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function buildQuery(
|
|
233
|
+
path: string | undefined,
|
|
234
|
+
pattern: string,
|
|
235
|
+
exclude?: string | string[],
|
|
236
|
+
cwd = process.cwd(),
|
|
237
|
+
allowlist?: ExternalAllowlist,
|
|
238
|
+
): string {
|
|
239
|
+
const parts: string[] = [];
|
|
240
|
+
if (path) {
|
|
241
|
+
const pathConstraint = normalizePathConstraint(path, cwd, allowlist);
|
|
242
|
+
if (pathConstraint) parts.push(pathConstraint);
|
|
243
|
+
}
|
|
244
|
+
parts.push(...normalizeExcludes(exclude, cwd, allowlist));
|
|
245
|
+
parts.push(pattern);
|
|
246
|
+
return parts.join(" ");
|
|
247
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
normalizePathConstraint,
|
|
5
|
+
normalizeExcludes,
|
|
6
|
+
buildQuery,
|
|
7
|
+
buildExternalAllowlist,
|
|
8
|
+
resolveExternalTarget,
|
|
9
|
+
PI_CODING_AGENT_ALIAS,
|
|
10
|
+
} from "../query.ts";
|
|
11
|
+
|
|
12
|
+
describe("normalizePathConstraint", () => {
|
|
13
|
+
test("empty string returns empty", () => {
|
|
14
|
+
expect(normalizePathConstraint("")).toBe("");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("dot returns null", () => {
|
|
18
|
+
expect(normalizePathConstraint(".")).toBeNull();
|
|
19
|
+
expect(normalizePathConstraint("./")).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("bare directory gets trailing slash", () => {
|
|
23
|
+
expect(normalizePathConstraint("src")).toBe("src/");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("glob is preserved", () => {
|
|
27
|
+
expect(normalizePathConstraint("*.ts")).toBe("*.ts");
|
|
28
|
+
expect(normalizePathConstraint("src/**/*.cc")).toBe("src/**/*.cc");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("filename with extension is preserved", () => {
|
|
32
|
+
expect(normalizePathConstraint("main.rs")).toBe("main.rs");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("recursive dir glob collapses to prefix", () => {
|
|
36
|
+
expect(normalizePathConstraint("src/**")).toBe("src/");
|
|
37
|
+
expect(normalizePathConstraint("src/**/*")).toBe("src/");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("leading ./ is stripped", () => {
|
|
41
|
+
expect(normalizePathConstraint("./src")).toBe("src/");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("normalizeExcludes", () => {
|
|
46
|
+
test("undefined returns empty array", () => {
|
|
47
|
+
expect(normalizeExcludes(undefined)).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("comma-separated string is split", () => {
|
|
51
|
+
expect(normalizeExcludes("test/, *.min.js")).toEqual(["!test/", "!*.min.js"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("leading ! is tolerated", () => {
|
|
55
|
+
expect(normalizeExcludes("!src/")).toEqual(["!src/"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("array input works", () => {
|
|
59
|
+
expect(normalizeExcludes(["test/", "vendor/"])).toEqual(["!test/", "!vendor/"]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("buildQuery", () => {
|
|
64
|
+
test("assembles path + excludes + pattern", () => {
|
|
65
|
+
const q = buildQuery("src/", "MyClass", "test/");
|
|
66
|
+
expect(q).toContain("src/");
|
|
67
|
+
expect(q).toContain("!test/");
|
|
68
|
+
expect(q).toContain("MyClass");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("no path or excludes yields just pattern", () => {
|
|
72
|
+
expect(buildQuery(undefined, "foo")).toBe("foo");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// ExternalAllowlist — auto-detection and resolveExternalTarget
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe("ExternalAllowlist", () => {
|
|
81
|
+
const allowlist = buildExternalAllowlist();
|
|
82
|
+
const has_entries = allowlist.entries.length > 0;
|
|
83
|
+
const cwd = process.cwd();
|
|
84
|
+
|
|
85
|
+
test("buildExternalAllowlist auto-detects the package dir", () => {
|
|
86
|
+
expect(allowlist.entries.length).toBeGreaterThanOrEqual(1);
|
|
87
|
+
expect(allowlist.entries[0].alias).toBe(PI_CODING_AGENT_ALIAS);
|
|
88
|
+
expect(path.isAbsolute(allowlist.entries[0].dir)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("resolveExternalTarget with ./pi-coding-agent alias → bare dir", () => {
|
|
92
|
+
if (!has_entries) return;
|
|
93
|
+
const target = resolveExternalTarget("./pi-coding-agent", allowlist);
|
|
94
|
+
expect(target).toBeDefined();
|
|
95
|
+
expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
|
|
96
|
+
expect(target!.relativePath).toBe("");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("resolveExternalTarget with pi-coding-agent alias (no ./) → bare dir", () => {
|
|
100
|
+
if (!has_entries) return;
|
|
101
|
+
const target = resolveExternalTarget("pi-coding-agent", allowlist);
|
|
102
|
+
expect(target).toBeDefined();
|
|
103
|
+
expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
|
|
104
|
+
expect(target!.relativePath).toBe("");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("resolveExternalTarget with ./pi-coding-agent/docs → subpath", () => {
|
|
108
|
+
if (!has_entries) return;
|
|
109
|
+
const target = resolveExternalTarget("./pi-coding-agent/docs", allowlist);
|
|
110
|
+
expect(target).toBeDefined();
|
|
111
|
+
expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
|
|
112
|
+
expect(target!.relativePath).toBe("docs");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("resolveExternalTarget with ./pi-coding-agent/docs/extensions.md → deep subpath", () => {
|
|
116
|
+
if (!has_entries) return;
|
|
117
|
+
const target = resolveExternalTarget(
|
|
118
|
+
"./pi-coding-agent/docs/extensions.md",
|
|
119
|
+
allowlist,
|
|
120
|
+
);
|
|
121
|
+
expect(target).toBeDefined();
|
|
122
|
+
expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
|
|
123
|
+
expect(target!.relativePath).toBe("docs/extensions.md");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("resolveExternalTarget with absolute path under allowlisted dir", () => {
|
|
127
|
+
if (!has_entries) return;
|
|
128
|
+
const abs = path.join(allowlist.entries[0].dir, "docs", "extensions.md");
|
|
129
|
+
const target = resolveExternalTarget(abs, allowlist);
|
|
130
|
+
expect(target).toBeDefined();
|
|
131
|
+
expect(target!.entry.alias).toBe(PI_CODING_AGENT_ALIAS);
|
|
132
|
+
expect(target!.relativePath).toBe("docs/extensions.md");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("resolveExternalTarget returns undefined for workspace-relative src/", () => {
|
|
136
|
+
expect(resolveExternalTarget("src/", allowlist)).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("resolveExternalTarget returns undefined for ./src", () => {
|
|
140
|
+
expect(resolveExternalTarget("./src", allowlist)).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("resolveExternalTarget returns undefined for undefined input", () => {
|
|
144
|
+
expect(resolveExternalTarget(undefined, allowlist)).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("resolveExternalTarget returns undefined for empty string", () => {
|
|
148
|
+
expect(resolveExternalTarget("", allowlist)).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("resolveExternalTarget returns undefined for alias-prefix mismatch (pi-coding-agent-foo)", () => {
|
|
152
|
+
expect(resolveExternalTarget("pi-coding-agent-foo", allowlist)).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// normalizePathConstraint with allowlist
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
describe("normalizePathConstraint with allowlist", () => {
|
|
161
|
+
const allowlist = buildExternalAllowlist();
|
|
162
|
+
const has_entries = allowlist.entries.length > 0;
|
|
163
|
+
const cwd = process.cwd();
|
|
164
|
+
|
|
165
|
+
test("allowlisted ./pi-coding-agent → null (bare dir)", () => {
|
|
166
|
+
if (!has_entries) return;
|
|
167
|
+
expect(normalizePathConstraint("./pi-coding-agent", cwd, allowlist)).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("allowlisted ./pi-coding-agent/docs → docs/ (trailing slash)", () => {
|
|
171
|
+
if (!has_entries) return;
|
|
172
|
+
expect(normalizePathConstraint("./pi-coding-agent/docs", cwd, allowlist)).toBe("docs/");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("allowlisted ./pi-coding-agent/docs/extensions.md → preserved filename", () => {
|
|
176
|
+
if (!has_entries) return;
|
|
177
|
+
expect(
|
|
178
|
+
normalizePathConstraint("./pi-coding-agent/docs/extensions.md", cwd, allowlist),
|
|
179
|
+
).toBe("docs/extensions.md");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("allowlisted absolute path under package dir → relative with trailing slash", () => {
|
|
183
|
+
if (!has_entries) return;
|
|
184
|
+
const abs = path.join(allowlist.entries[0].dir, "docs");
|
|
185
|
+
expect(normalizePathConstraint(abs, cwd, allowlist)).toBe("docs/");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("non-allowlisted absolute path /etc/passwd → throws", () => {
|
|
189
|
+
expect(() => normalizePathConstraint("/etc/passwd", cwd, allowlist)).toThrow(
|
|
190
|
+
"Path constraint must be relative to the workspace: /etc/passwd",
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("workspace-relative src → src/ (unchanged with allowlist)", () => {
|
|
195
|
+
expect(normalizePathConstraint("src", cwd, allowlist)).toBe("src/");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("workspace-relative glob *.ts → *.ts (unchanged with allowlist)", () => {
|
|
199
|
+
expect(normalizePathConstraint("*.ts", cwd, allowlist)).toBe("*.ts");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("workspace-relative dot → null (unchanged with allowlist)", () => {
|
|
203
|
+
expect(normalizePathConstraint(".", cwd, allowlist)).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// buildQuery with external target
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
describe("buildQuery with external target", () => {
|
|
212
|
+
const allowlist = buildExternalAllowlist();
|
|
213
|
+
const has_entries = allowlist.entries.length > 0;
|
|
214
|
+
const cwd = process.cwd();
|
|
215
|
+
|
|
216
|
+
test("buildQuery with ./pi-coding-agent/docs includes docs/ and pattern", () => {
|
|
217
|
+
if (!has_entries) return;
|
|
218
|
+
const q = buildQuery("./pi-coding-agent/docs", "renderCall", undefined, cwd, allowlist);
|
|
219
|
+
expect(q).toContain("docs/");
|
|
220
|
+
expect(q).toContain("renderCall");
|
|
221
|
+
});
|
|
222
|
+
});
|