pi-gsd 2.0.1 → 2.0.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/dist/pi-gsd-hooks.js +1533 -0
- package/dist/pi-gsd-tools.js +53 -52
- package/package.json +3 -5
- package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
- package/src/cli.ts +0 -644
- package/src/commands/base.ts +0 -67
- package/src/commands/commit.ts +0 -22
- package/src/commands/config.ts +0 -71
- package/src/commands/frontmatter.ts +0 -51
- package/src/commands/index.ts +0 -76
- package/src/commands/init.ts +0 -43
- package/src/commands/milestone.ts +0 -37
- package/src/commands/phase.ts +0 -92
- package/src/commands/progress.ts +0 -71
- package/src/commands/roadmap.ts +0 -40
- package/src/commands/scaffold.ts +0 -19
- package/src/commands/state.ts +0 -102
- package/src/commands/template.ts +0 -52
- package/src/commands/verify.ts +0 -70
- package/src/commands/workstream.ts +0 -98
- package/src/commands/wxp.ts +0 -65
- package/src/lib/commands.ts +0 -1040
- package/src/lib/config.ts +0 -385
- package/src/lib/core.ts +0 -1167
- package/src/lib/frontmatter.ts +0 -462
- package/src/lib/init.ts +0 -517
- package/src/lib/milestone.ts +0 -290
- package/src/lib/model-profiles.ts +0 -272
- package/src/lib/phase.ts +0 -1012
- package/src/lib/profile-output.ts +0 -237
- package/src/lib/profile-pipeline.ts +0 -556
- package/src/lib/roadmap.ts +0 -378
- package/src/lib/schemas.ts +0 -290
- package/src/lib/security.ts +0 -176
- package/src/lib/state.ts +0 -1175
- package/src/lib/template.ts +0 -246
- package/src/lib/uat.ts +0 -289
- package/src/lib/verify.ts +0 -879
- package/src/lib/workstream.ts +0 -524
- package/src/output.ts +0 -45
- package/src/schemas/pi-gsd-settings.schema.json +0 -80
- package/src/schemas/wxp.xsd +0 -619
- package/src/schemas/wxp.zod.ts +0 -318
- package/src/wxp/__tests__/arguments.test.ts +0 -86
- package/src/wxp/__tests__/conditions.test.ts +0 -106
- package/src/wxp/__tests__/executor.test.ts +0 -95
- package/src/wxp/__tests__/helpers.ts +0 -26
- package/src/wxp/__tests__/integration.test.ts +0 -166
- package/src/wxp/__tests__/new-features.test.ts +0 -222
- package/src/wxp/__tests__/parser.test.ts +0 -159
- package/src/wxp/__tests__/paste.test.ts +0 -66
- package/src/wxp/__tests__/schema.test.ts +0 -120
- package/src/wxp/__tests__/security.test.ts +0 -87
- package/src/wxp/__tests__/shell.test.ts +0 -85
- package/src/wxp/__tests__/string-ops.test.ts +0 -25
- package/src/wxp/__tests__/variables.test.ts +0 -65
- package/src/wxp/arguments.ts +0 -89
- package/src/wxp/conditions.ts +0 -78
- package/src/wxp/executor.ts +0 -191
- package/src/wxp/index.ts +0 -191
- package/src/wxp/parser.ts +0 -198
- package/src/wxp/paste.ts +0 -51
- package/src/wxp/security.ts +0 -102
- package/src/wxp/shell.ts +0 -81
- package/src/wxp/string-ops.ts +0 -44
- package/src/wxp/variables.ts +0 -109
package/src/wxp/security.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import type { WxpSecurityConfig, TrustedPathEntry } from "../schemas/wxp.zod.js";
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_SHELL_ALLOWLIST: readonly string[] = [
|
|
5
|
-
"pi-gsd-tools",
|
|
6
|
-
"git",
|
|
7
|
-
"node",
|
|
8
|
-
"cat",
|
|
9
|
-
"ls",
|
|
10
|
-
"echo",
|
|
11
|
-
"find",
|
|
12
|
-
] as const;
|
|
13
|
-
|
|
14
|
-
export type SecurityCheckResult = { ok: true } | { ok: false; reason: string };
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Resolve a TrustedPathEntry to an absolute path given the project root and package root.
|
|
18
|
-
*/
|
|
19
|
-
export function resolveTrustedEntry(
|
|
20
|
-
entry: TrustedPathEntry,
|
|
21
|
-
projectRoot: string,
|
|
22
|
-
pkgRoot: string,
|
|
23
|
-
): string {
|
|
24
|
-
switch (entry.position) {
|
|
25
|
-
case "project":
|
|
26
|
-
return path.resolve(projectRoot, entry.path);
|
|
27
|
-
case "pkg":
|
|
28
|
-
return path.resolve(pkgRoot, entry.path);
|
|
29
|
-
case "absolute":
|
|
30
|
-
return path.resolve(entry.path);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Returns ok=true only if filePath resolves into a trusted path.
|
|
36
|
-
* Hard invariant: .planning/ is NEVER trusted regardless of config.
|
|
37
|
-
*/
|
|
38
|
-
export function checkTrustedPath(
|
|
39
|
-
filePath: string,
|
|
40
|
-
config: WxpSecurityConfig,
|
|
41
|
-
projectRoot: string,
|
|
42
|
-
pkgRoot: string,
|
|
43
|
-
): SecurityCheckResult {
|
|
44
|
-
const resolved = path.resolve(filePath);
|
|
45
|
-
const planningSegment = `${path.sep}.planning`;
|
|
46
|
-
|
|
47
|
-
// Hard invariant: .planning/ is never processed
|
|
48
|
-
if (
|
|
49
|
-
resolved.includes(`${planningSegment}${path.sep}`) ||
|
|
50
|
-
resolved.endsWith(planningSegment)
|
|
51
|
-
) {
|
|
52
|
-
return {
|
|
53
|
-
ok: false,
|
|
54
|
-
reason: ".planning/ files are never processed by WXP (hard security invariant)",
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Check untrusted paths first (they override trusted)
|
|
59
|
-
for (const entry of config.untrustedPaths) {
|
|
60
|
-
const untrustedAbs = resolveTrustedEntry(entry, projectRoot, pkgRoot);
|
|
61
|
-
if (resolved.startsWith(untrustedAbs + path.sep) || resolved === untrustedAbs) {
|
|
62
|
-
return { ok: false, reason: `File '${filePath}' is in an explicitly untrusted path: ${untrustedAbs}` };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check trusted paths
|
|
67
|
-
for (const entry of config.trustedPaths) {
|
|
68
|
-
const trustedAbs = resolveTrustedEntry(entry, projectRoot, pkgRoot);
|
|
69
|
-
if (resolved.startsWith(trustedAbs + path.sep) || resolved === trustedAbs) {
|
|
70
|
-
return { ok: true };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
ok: false,
|
|
76
|
-
reason: `File '${filePath}' is not in a trusted WXP path.`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Returns ok=true only if the bare command name is allowlisted and not banned.
|
|
82
|
-
*/
|
|
83
|
-
export function checkAllowlist(
|
|
84
|
-
command: string,
|
|
85
|
-
config: WxpSecurityConfig,
|
|
86
|
-
): SecurityCheckResult {
|
|
87
|
-
const bare = path.basename(command);
|
|
88
|
-
|
|
89
|
-
// Banlist overrides allowlist
|
|
90
|
-
if (config.shellBanlist.includes(bare)) {
|
|
91
|
-
return { ok: false, reason: `Command '${bare}' is explicitly banned by WXP security config.` };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (config.shellAllowlist.includes(bare)) {
|
|
95
|
-
return { ok: true };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
ok: false,
|
|
100
|
-
reason: `Command '${bare}' is not in the WXP shell allowlist. Allowed: ${config.shellAllowlist.join(", ")}`,
|
|
101
|
-
};
|
|
102
|
-
}
|
package/src/wxp/shell.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { checkAllowlist } from "./security.js";
|
|
3
|
-
import type { WxpSecurityConfig, XmlNode } from "../schemas/wxp.zod.js";
|
|
4
|
-
import type { VariableStore } from "./variables.js";
|
|
5
|
-
|
|
6
|
-
export class WxpShellError extends Error {
|
|
7
|
-
constructor(
|
|
8
|
-
public readonly command: string,
|
|
9
|
-
public readonly stderr: string,
|
|
10
|
-
public readonly variableSnapshot: Record<string, string>,
|
|
11
|
-
message: string,
|
|
12
|
-
) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = "WxpShellError";
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Resolve a single <arg> node to its string value. */
|
|
19
|
-
export function resolveArgNode(arg: XmlNode, vars: VariableStore): string {
|
|
20
|
-
if (arg.attrs["string"] !== undefined) return arg.attrs["string"];
|
|
21
|
-
if (arg.attrs["name"] !== undefined) {
|
|
22
|
-
const raw = vars.resolve(arg.attrs["name"]) ?? "";
|
|
23
|
-
const wrap = arg.attrs["wrap"];
|
|
24
|
-
return wrap ? `${wrap}${raw}${wrap}` : raw;
|
|
25
|
-
}
|
|
26
|
-
if (arg.attrs["value"] !== undefined) return arg.attrs["value"];
|
|
27
|
-
return "";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function executeShell(
|
|
31
|
-
node: XmlNode,
|
|
32
|
-
vars: VariableStore,
|
|
33
|
-
config: WxpSecurityConfig,
|
|
34
|
-
): void {
|
|
35
|
-
const command = node.attrs["command"] ?? "";
|
|
36
|
-
const check = checkAllowlist(command, config);
|
|
37
|
-
if (!check.ok) {
|
|
38
|
-
throw new WxpShellError(command, "", vars.snapshot(), check.reason);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const argsContainer = node.children.find((c) => c.tag === "args");
|
|
42
|
-
const outsContainer = node.children.find((c) => c.tag === "outs");
|
|
43
|
-
|
|
44
|
-
const resolvedArgs = argsContainer
|
|
45
|
-
? argsContainer.children.filter((c) => c.tag === "arg").map((a) => resolveArgNode(a, vars))
|
|
46
|
-
: [];
|
|
47
|
-
|
|
48
|
-
const suppressErrors = outsContainer
|
|
49
|
-
? outsContainer.children.some((c) => c.tag === "suppress-errors")
|
|
50
|
-
: false;
|
|
51
|
-
|
|
52
|
-
const outVars = outsContainer
|
|
53
|
-
? outsContainer.children
|
|
54
|
-
.filter((c) => c.tag === "out" && c.attrs["name"])
|
|
55
|
-
.map((c) => c.attrs["name"] as string)
|
|
56
|
-
: [];
|
|
57
|
-
|
|
58
|
-
let stdout = "";
|
|
59
|
-
try {
|
|
60
|
-
stdout = execFileSync(command, resolvedArgs, {
|
|
61
|
-
encoding: "utf8",
|
|
62
|
-
timeout: config.shellTimeoutMs,
|
|
63
|
-
windowsHide: true,
|
|
64
|
-
}).trim();
|
|
65
|
-
} catch (err) {
|
|
66
|
-
if (suppressErrors) {
|
|
67
|
-
for (const name of outVars) vars.set(name, "", undefined);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const e = err as { stderr?: string; message?: string };
|
|
71
|
-
const stderr = (e.stderr ?? e.message ?? String(err)).trim();
|
|
72
|
-
throw new WxpShellError(
|
|
73
|
-
command,
|
|
74
|
-
stderr,
|
|
75
|
-
vars.snapshot(),
|
|
76
|
-
`Shell '${command} ${resolvedArgs.join(" ")}' failed: ${stderr}`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (outVars.length > 0) vars.set(outVars[0], stdout, undefined);
|
|
81
|
-
}
|
package/src/wxp/string-ops.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { XmlNode } from "../schemas/wxp.zod.js";
|
|
2
|
-
import type { VariableStore } from "./variables.js";
|
|
3
|
-
import { resolveArgNode } from "./shell.js";
|
|
4
|
-
|
|
5
|
-
export class WxpStringOpError extends Error {
|
|
6
|
-
constructor(message: string) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = "WxpStringOpError";
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function executeStringOp(node: XmlNode, vars: VariableStore): void {
|
|
13
|
-
const op = node.attrs["op"];
|
|
14
|
-
if (op !== "split") throw new WxpStringOpError(`<string-op> only op="split" is supported in v1`);
|
|
15
|
-
|
|
16
|
-
const argsContainer = node.children.find((c) => c.tag === "args");
|
|
17
|
-
const outsContainer = node.children.find((c) => c.tag === "outs");
|
|
18
|
-
|
|
19
|
-
if (!argsContainer || !outsContainer) {
|
|
20
|
-
throw new WxpStringOpError(`<string-op> requires <args> and <outs>`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const args = argsContainer.children.filter((c) => c.tag === "arg");
|
|
24
|
-
const outs = outsContainer.children.filter((c) => c.tag === "out");
|
|
25
|
-
|
|
26
|
-
const srcArg = args[0];
|
|
27
|
-
const delimArg = args[1];
|
|
28
|
-
|
|
29
|
-
if (!srcArg) throw new WxpStringOpError(`<string-op op="split"> requires at least 2 <arg> children`);
|
|
30
|
-
|
|
31
|
-
const source = resolveArgNode(srcArg, vars);
|
|
32
|
-
if (srcArg.attrs["name"] && vars.get(srcArg.attrs["name"]) === undefined) {
|
|
33
|
-
throw new WxpStringOpError(`string-op split: source variable '${srcArg.attrs["name"]}' is not defined`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const delimiter = delimArg ? resolveArgNode(delimArg, vars) : "";
|
|
37
|
-
const parts = source.split(delimiter);
|
|
38
|
-
|
|
39
|
-
// Each <out> gets one part starting from index 1 (part after the delimiter prefix)
|
|
40
|
-
outs.forEach((out, i) => {
|
|
41
|
-
const name = out.attrs["name"];
|
|
42
|
-
if (name) vars.set(name, parts[i + 1] ?? parts[i] ?? "", undefined);
|
|
43
|
-
});
|
|
44
|
-
}
|
package/src/wxp/variables.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import type { WxpVariable } from "../schemas/wxp.zod.js";
|
|
2
|
-
|
|
3
|
-
export interface VariableStore {
|
|
4
|
-
/** Set a string variable, with optional owner for collision detection */
|
|
5
|
-
set(name: string, value: string, owner?: string): void;
|
|
6
|
-
/** Get a string variable value (supports dot-notation: "item.prop") */
|
|
7
|
-
get(name: string): string | undefined;
|
|
8
|
-
/** Resolve a name — handles both plain variables and dot-notation property access on JSON items */
|
|
9
|
-
resolve(name: string): string | undefined;
|
|
10
|
-
/** Store an array of JSON strings for <for-each> iteration */
|
|
11
|
-
setArray(name: string, items: string[], owner?: string): void;
|
|
12
|
-
/** Retrieve an array variable */
|
|
13
|
-
getArray(name: string): string[] | undefined;
|
|
14
|
-
has(name: string): boolean;
|
|
15
|
-
entries(): IterableIterator<[string, WxpVariable]>;
|
|
16
|
-
snapshot(): Record<string, string>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createVariableStore(): VariableStore {
|
|
20
|
-
const scalars = new Map<string, WxpVariable>();
|
|
21
|
-
const arrays = new Map<string, string[]>();
|
|
22
|
-
|
|
23
|
-
const resolveScalar = (name: string): string | undefined => {
|
|
24
|
-
// Plain lookup first
|
|
25
|
-
const direct = scalars.get(name)?.value;
|
|
26
|
-
if (direct !== undefined) return direct;
|
|
27
|
-
|
|
28
|
-
// Dot-notation: "item.prop.sub" → look up "item", parse JSON, traverse path
|
|
29
|
-
const dotIdx = name.indexOf(".");
|
|
30
|
-
if (dotIdx === -1) return undefined;
|
|
31
|
-
|
|
32
|
-
const varPart = name.slice(0, dotIdx);
|
|
33
|
-
const pathPart = name.slice(dotIdx + 1);
|
|
34
|
-
const jsonStr = scalars.get(varPart)?.value;
|
|
35
|
-
if (jsonStr === undefined) return undefined;
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON traversal requires dynamic access
|
|
39
|
-
let obj: any = JSON.parse(jsonStr);
|
|
40
|
-
for (const key of pathPart.split(".")) {
|
|
41
|
-
if (obj === null || typeof obj !== "object") return undefined;
|
|
42
|
-
obj = obj[key];
|
|
43
|
-
}
|
|
44
|
-
return obj === undefined || obj === null ? undefined : String(obj);
|
|
45
|
-
} catch {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
set(name, value, owner) {
|
|
52
|
-
const existing = scalars.get(name);
|
|
53
|
-
if (existing?.owner && owner && existing.owner !== owner) {
|
|
54
|
-
scalars.delete(name);
|
|
55
|
-
scalars.set(`${existing.owner}:${name}`, {
|
|
56
|
-
name: `${existing.owner}:${name}`,
|
|
57
|
-
value: existing.value,
|
|
58
|
-
owner: existing.owner,
|
|
59
|
-
});
|
|
60
|
-
scalars.set(`${owner}:${name}`, { name: `${owner}:${name}`, value, owner });
|
|
61
|
-
} else {
|
|
62
|
-
scalars.set(name, { name, value, owner });
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
get(name) {
|
|
67
|
-
return scalars.get(name)?.value;
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
resolve(name) {
|
|
71
|
-
return resolveScalar(name);
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
setArray(name, items, owner) {
|
|
75
|
-
arrays.set(name, items);
|
|
76
|
-
// Also store as a JSON string in scalars so it's accessible via get()
|
|
77
|
-
scalars.set(name, { name, value: JSON.stringify(items), owner });
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
getArray(name) {
|
|
81
|
-
// Try direct array store first
|
|
82
|
-
if (arrays.has(name)) return arrays.get(name);
|
|
83
|
-
// Fall back: try to parse the scalar as a JSON array
|
|
84
|
-
const str = scalars.get(name)?.value;
|
|
85
|
-
if (!str) return undefined;
|
|
86
|
-
try {
|
|
87
|
-
const parsed: unknown = JSON.parse(str);
|
|
88
|
-
if (Array.isArray(parsed)) return parsed.map((item) =>
|
|
89
|
-
typeof item === "string" ? item : JSON.stringify(item)
|
|
90
|
-
);
|
|
91
|
-
} catch { /* not a JSON array */ }
|
|
92
|
-
return undefined;
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
has(name) {
|
|
96
|
-
return scalars.has(name) || arrays.has(name);
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
entries() {
|
|
100
|
-
return scalars.entries();
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
snapshot() {
|
|
104
|
-
const out: Record<string, string> = {};
|
|
105
|
-
for (const [k, v] of scalars) out[k] = v.value;
|
|
106
|
-
return out;
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|