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/schemas/wxp.zod.ts
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* src/schemas/wxp.zod.ts — Zod runtime schemas for the WXP engine.
|
|
3
|
-
*
|
|
4
|
-
* Single <arg> and <out> element types regardless of context.
|
|
5
|
-
* All types inferred via z.infer<> — zero `any` except required z.lazy circular refs.
|
|
6
|
-
*/
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
|
|
9
|
-
// ─── <arg> ────────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
export const ArgTypeSchema = z.enum(["string", "number", "boolean", "flag"]);
|
|
12
|
-
export type ArgType = z.infer<typeof ArgTypeSchema>;
|
|
13
|
-
|
|
14
|
-
export const OutTypeSchema = z.enum(["string"]);
|
|
15
|
-
export type OutType = z.infer<typeof OutTypeSchema>;
|
|
16
|
-
|
|
17
|
-
export const ArgSchema = z.object({
|
|
18
|
-
string: z.string().optional(),
|
|
19
|
-
name: z.string().optional(),
|
|
20
|
-
wrap: z.string().optional(),
|
|
21
|
-
type: ArgTypeSchema.optional(),
|
|
22
|
-
value: z.string().optional(),
|
|
23
|
-
flag: z.string().optional(),
|
|
24
|
-
optional: z.boolean().optional(),
|
|
25
|
-
as: z.string().optional(),
|
|
26
|
-
});
|
|
27
|
-
export type Arg = z.infer<typeof ArgSchema>;
|
|
28
|
-
|
|
29
|
-
// ─── <out> ────────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
export const OutSchema = z.object({
|
|
32
|
-
type: OutTypeSchema,
|
|
33
|
-
name: z.string(),
|
|
34
|
-
});
|
|
35
|
-
export type Out = z.infer<typeof OutSchema>;
|
|
36
|
-
|
|
37
|
-
// ─── <delimiter> ─────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
export const DelimiterSchema = z.object({
|
|
40
|
-
type: z.literal("string"),
|
|
41
|
-
value: z.string(),
|
|
42
|
-
});
|
|
43
|
-
export type Delimiter = z.infer<typeof DelimiterSchema>;
|
|
44
|
-
|
|
45
|
-
// ─── <settings> (inside <gsd-arguments>) ────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
export const ArgumentsSettingsSchema = z.object({
|
|
48
|
-
keepExtraArgs: z.boolean().default(false),
|
|
49
|
-
strictArgs: z.boolean().default(false),
|
|
50
|
-
delimiters: z.array(DelimiterSchema).default([]),
|
|
51
|
-
});
|
|
52
|
-
export type ArgumentsSettings = z.infer<typeof ArgumentsSettingsSchema>;
|
|
53
|
-
|
|
54
|
-
// ─── <gsd-arguments> ─────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
export const ArgumentsNodeSchema = z.object({
|
|
57
|
-
type: z.literal("arguments"),
|
|
58
|
-
settings: ArgumentsSettingsSchema.default({}),
|
|
59
|
-
args: z.array(ArgSchema).default([]),
|
|
60
|
-
});
|
|
61
|
-
export type ArgumentsNode = z.infer<typeof ArgumentsNodeSchema>;
|
|
62
|
-
|
|
63
|
-
// ─── <shell> ─────────────────────────────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
export const ShellNodeSchema = z.object({
|
|
66
|
-
type: z.literal("shell"),
|
|
67
|
-
command: z.string(),
|
|
68
|
-
args: z.array(ArgSchema).default([]),
|
|
69
|
-
outs: z.array(OutSchema).default([]),
|
|
70
|
-
suppressErrors: z.boolean().default(false),
|
|
71
|
-
});
|
|
72
|
-
export type ShellNode = z.infer<typeof ShellNodeSchema>;
|
|
73
|
-
|
|
74
|
-
// ─── <string-op> ─────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
export const StringOpNodeSchema = z.object({
|
|
77
|
-
type: z.literal("string-op"),
|
|
78
|
-
op: z.literal("split"),
|
|
79
|
-
args: z.array(ArgSchema),
|
|
80
|
-
outs: z.array(OutSchema),
|
|
81
|
-
});
|
|
82
|
-
export type StringOpNode = z.infer<typeof StringOpNodeSchema>;
|
|
83
|
-
|
|
84
|
-
// ─── <json-parse> ────────────────────────────────────────────────────────────
|
|
85
|
-
// Parses a JSON string variable into a scalar string or an array of JSON strings.
|
|
86
|
-
// path: optional dot-path like "$.phases" or "$.meta.name"
|
|
87
|
-
|
|
88
|
-
export const JsonParseNodeSchema = z.object({
|
|
89
|
-
type: z.literal("json-parse"),
|
|
90
|
-
src: z.string(),
|
|
91
|
-
path: z.string().optional(),
|
|
92
|
-
out: z.string(),
|
|
93
|
-
});
|
|
94
|
-
export type JsonParseNode = z.infer<typeof JsonParseNodeSchema>;
|
|
95
|
-
|
|
96
|
-
// ─── <read-file> ─────────────────────────────────────────────────────────────
|
|
97
|
-
// Reads any accessible file into a named variable.
|
|
98
|
-
// No trusted-path restriction — same surface as <shell command="cat"> already provides.
|
|
99
|
-
|
|
100
|
-
export const ReadFileNodeSchema = z.object({
|
|
101
|
-
type: z.literal("read-file"),
|
|
102
|
-
path: z.string(),
|
|
103
|
-
out: z.string(),
|
|
104
|
-
});
|
|
105
|
-
export type ReadFileNode = z.infer<typeof ReadFileNodeSchema>;
|
|
106
|
-
|
|
107
|
-
// ─── <write-file> ────────────────────────────────────────────────────────────
|
|
108
|
-
// Create-only: fails if file already exists (never overwrites existing state).
|
|
109
|
-
// Cannot target trusted harness paths (prevents harness corruption).
|
|
110
|
-
// Creates parent directories as needed.
|
|
111
|
-
|
|
112
|
-
export const WriteFileNodeSchema = z.object({
|
|
113
|
-
type: z.literal("write-file"),
|
|
114
|
-
path: z.string(),
|
|
115
|
-
src: z.string(),
|
|
116
|
-
});
|
|
117
|
-
export type WriteFileNode = z.infer<typeof WriteFileNodeSchema>;
|
|
118
|
-
|
|
119
|
-
// ─── <display> ───────────────────────────────────────────────────────────────
|
|
120
|
-
// Emits ctx.ui.notify(). msg supports {varname} and {var.prop} interpolation.
|
|
121
|
-
|
|
122
|
-
export const DisplayLevelSchema = z.enum(["info", "warning", "error"]);
|
|
123
|
-
export type DisplayLevel = z.infer<typeof DisplayLevelSchema>;
|
|
124
|
-
|
|
125
|
-
export const DisplayNodeSchema = z.object({
|
|
126
|
-
type: z.literal("display"),
|
|
127
|
-
msg: z.string(),
|
|
128
|
-
level: DisplayLevelSchema.default("info"),
|
|
129
|
-
});
|
|
130
|
-
export type DisplayNode = z.infer<typeof DisplayNodeSchema>;
|
|
131
|
-
|
|
132
|
-
// ─── Condition operands ───────────────────────────────────────────────────────
|
|
133
|
-
// <left> and <right> use the same attribute set as <arg>
|
|
134
|
-
|
|
135
|
-
export const OperandSchema = ArgSchema;
|
|
136
|
-
export type Operand = z.infer<typeof OperandSchema>;
|
|
137
|
-
|
|
138
|
-
// ─── Condition expressions ────────────────────────────────────────────────────
|
|
139
|
-
// Binary ops: have <left> and <right> operands
|
|
140
|
-
// Logical ops: <and>/<or> wrap arrays of ConditionExpr (recursive via z.lazy)
|
|
141
|
-
|
|
142
|
-
export const BinaryCondOpSchema = z.enum([
|
|
143
|
-
"equals",
|
|
144
|
-
"not-equals",
|
|
145
|
-
"starts-with",
|
|
146
|
-
"contains",
|
|
147
|
-
"less-than",
|
|
148
|
-
"greater-than",
|
|
149
|
-
"less-than-or-equal",
|
|
150
|
-
"greater-than-or-equal",
|
|
151
|
-
]);
|
|
152
|
-
export type BinaryCondOp = z.infer<typeof BinaryCondOpSchema>;
|
|
153
|
-
|
|
154
|
-
export interface BinaryCondExpr {
|
|
155
|
-
op: BinaryCondOp;
|
|
156
|
-
left: Operand;
|
|
157
|
-
right: Operand;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface AndCondExpr {
|
|
161
|
-
op: "and";
|
|
162
|
-
children: ConditionExpr[];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export interface OrCondExpr {
|
|
166
|
-
op: "or";
|
|
167
|
-
children: ConditionExpr[];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export type ConditionExpr = BinaryCondExpr | AndCondExpr | OrCondExpr;
|
|
171
|
-
|
|
172
|
-
// Zod schemas (z.lazy for recursive and/or)
|
|
173
|
-
const BinaryCondExprSchema = z.object({
|
|
174
|
-
op: BinaryCondOpSchema,
|
|
175
|
-
left: OperandSchema,
|
|
176
|
-
right: OperandSchema,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- required for z.lazy circular ref
|
|
180
|
-
export const ConditionExprSchema: z.ZodType<any> = z.lazy(() =>
|
|
181
|
-
z.union([
|
|
182
|
-
BinaryCondExprSchema,
|
|
183
|
-
z.object({ op: z.literal("and"), children: z.array(ConditionExprSchema) }),
|
|
184
|
-
z.object({ op: z.literal("or"), children: z.array(ConditionExprSchema) }),
|
|
185
|
-
]),
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
// ─── <sort-by> (child of <for-each>) ─────────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
export const SortBySchema = z.object({
|
|
191
|
-
key: z.string(),
|
|
192
|
-
type: z.enum(["string", "number"]).default("string"),
|
|
193
|
-
order: z.enum(["asc", "desc"]).default("asc"),
|
|
194
|
-
});
|
|
195
|
-
export type SortBy = z.infer<typeof SortBySchema>;
|
|
196
|
-
|
|
197
|
-
// ─── <if> (recursive) ────────────────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
export interface IfNode {
|
|
200
|
-
type: "if";
|
|
201
|
-
condition: ConditionExpr;
|
|
202
|
-
then: WxpOperation[];
|
|
203
|
-
else?: WxpOperation[];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ─── <for-each> ──────────────────────────────────────────────────────────────
|
|
207
|
-
|
|
208
|
-
export interface ForEachNode {
|
|
209
|
-
type: "for-each";
|
|
210
|
-
/** Name of the array variable to iterate */
|
|
211
|
-
var: string;
|
|
212
|
-
/** Name assigned to each item during iteration */
|
|
213
|
-
item: string;
|
|
214
|
-
/** Optional pre-filter: only items matching this condition are iterated */
|
|
215
|
-
where?: ConditionExpr;
|
|
216
|
-
/** Optional sort before iteration */
|
|
217
|
-
sortBy?: SortBy;
|
|
218
|
-
children: WxpOperation[];
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ─── <gsd-execute> ───────────────────────────────────────────────────────────
|
|
222
|
-
|
|
223
|
-
export interface ExecuteBlock {
|
|
224
|
-
type: "execute";
|
|
225
|
-
children: WxpOperation[];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ─── <gsd-paste> ─────────────────────────────────────────────────────────────
|
|
229
|
-
|
|
230
|
-
export const PasteNodeSchema = z.object({
|
|
231
|
-
type: z.literal("paste"),
|
|
232
|
-
name: z.string(),
|
|
233
|
-
});
|
|
234
|
-
export type PasteNode = z.infer<typeof PasteNodeSchema>;
|
|
235
|
-
|
|
236
|
-
// ─── <gsd-include> ───────────────────────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
export const IncludeNodeSchema = z.object({
|
|
239
|
-
type: z.literal("include"),
|
|
240
|
-
path: z.string(),
|
|
241
|
-
select: z.string().optional(),
|
|
242
|
-
includeArguments: z.boolean().default(false),
|
|
243
|
-
argMappings: z.array(ArgSchema).default([]),
|
|
244
|
-
});
|
|
245
|
-
export type IncludeNode = z.infer<typeof IncludeNodeSchema>;
|
|
246
|
-
|
|
247
|
-
// ─── <gsd-version> ───────────────────────────────────────────────────────────
|
|
248
|
-
|
|
249
|
-
export const VersionTagSchema = z.object({
|
|
250
|
-
type: z.literal("version"),
|
|
251
|
-
v: z.string(),
|
|
252
|
-
doNotUpdate: z.boolean().default(false),
|
|
253
|
-
});
|
|
254
|
-
export type VersionTag = z.infer<typeof VersionTagSchema>;
|
|
255
|
-
|
|
256
|
-
// ─── WxpOperation union ───────────────────────────────────────────────────────
|
|
257
|
-
|
|
258
|
-
export type WxpOperation =
|
|
259
|
-
| ShellNode
|
|
260
|
-
| StringOpNode
|
|
261
|
-
| JsonParseNode
|
|
262
|
-
| ReadFileNode
|
|
263
|
-
| WriteFileNode
|
|
264
|
-
| DisplayNode
|
|
265
|
-
| IfNode
|
|
266
|
-
| ForEachNode
|
|
267
|
-
| ExecuteBlock
|
|
268
|
-
| PasteNode
|
|
269
|
-
| IncludeNode
|
|
270
|
-
| VersionTag
|
|
271
|
-
| ArgumentsNode;
|
|
272
|
-
|
|
273
|
-
// ─── Variable store entry ─────────────────────────────────────────────────────
|
|
274
|
-
|
|
275
|
-
export const WxpVariableSchema = z.object({
|
|
276
|
-
name: z.string(),
|
|
277
|
-
value: z.string(),
|
|
278
|
-
owner: z.string().optional(),
|
|
279
|
-
});
|
|
280
|
-
export type WxpVariable = z.infer<typeof WxpVariableSchema>;
|
|
281
|
-
|
|
282
|
-
// ─── XML node (parser intermediate) ──────────────────────────────────────────
|
|
283
|
-
|
|
284
|
-
export interface XmlNode {
|
|
285
|
-
tag: string;
|
|
286
|
-
attrs: Record<string, string>;
|
|
287
|
-
children: XmlNode[];
|
|
288
|
-
selfClosing: boolean;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ─── Security config ──────────────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
export const TrustedPathEntrySchema = z.object({
|
|
294
|
-
position: z.enum(["project", "pkg", "absolute"]),
|
|
295
|
-
path: z.string(),
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
export const WxpSecurityConfigSchema = z.object({
|
|
299
|
-
trustedPaths: z.array(TrustedPathEntrySchema),
|
|
300
|
-
untrustedPaths: z.array(TrustedPathEntrySchema).default([]),
|
|
301
|
-
shellAllowlist: z.array(z.string()),
|
|
302
|
-
shellBanlist: z.array(z.string()).default([]),
|
|
303
|
-
shellTimeoutMs: z.number().default(30_000),
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
export type TrustedPathEntry = z.infer<typeof TrustedPathEntrySchema>;
|
|
307
|
-
export type WxpSecurityConfig = z.infer<typeof WxpSecurityConfigSchema>;
|
|
308
|
-
|
|
309
|
-
// ─── Execution context (threads config + display callback through engine) ─────
|
|
310
|
-
|
|
311
|
-
export type DisplayCallback = (msg: string, level: DisplayLevel) => void;
|
|
312
|
-
|
|
313
|
-
export interface WxpExecContext {
|
|
314
|
-
config: WxpSecurityConfig;
|
|
315
|
-
projectRoot: string;
|
|
316
|
-
pkgRoot: string;
|
|
317
|
-
onDisplay: DisplayCallback;
|
|
318
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseArguments, WxpArgumentsError } from "../arguments.js";
|
|
3
|
-
import { createVariableStore } from "../variables.js";
|
|
4
|
-
import { x } from "./helpers.js";
|
|
5
|
-
|
|
6
|
-
// Build a <gsd-arguments> XmlNode from a declarative spec
|
|
7
|
-
function argsNode(opts: {
|
|
8
|
-
keep?: boolean;
|
|
9
|
-
strict?: boolean;
|
|
10
|
-
args: Array<{ name: string; type: string; flag?: string; optional?: boolean }>;
|
|
11
|
-
}) {
|
|
12
|
-
const settingsChildren = [];
|
|
13
|
-
if (opts.keep) settingsChildren.push(x("keep-extra-args"));
|
|
14
|
-
if (opts.strict) settingsChildren.push(x("strict-args"));
|
|
15
|
-
|
|
16
|
-
const argNodes = opts.args.map((a) => {
|
|
17
|
-
const attrs: Record<string, string> = { name: a.name, type: a.type };
|
|
18
|
-
if (a.flag) attrs["flag"] = a.flag;
|
|
19
|
-
if (a.optional) attrs["optional"] = "";
|
|
20
|
-
return x("arg", attrs);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
return x("gsd-arguments", {}, [
|
|
24
|
-
x("settings", {}, settingsChildren),
|
|
25
|
-
...argNodes,
|
|
26
|
-
]);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("parseArguments — two-pass (PRD §3.2)", () => {
|
|
30
|
-
it("extracts flag and assigns positional", () => {
|
|
31
|
-
const vars = createVariableStore();
|
|
32
|
-
parseArguments(argsNode({
|
|
33
|
-
args: [
|
|
34
|
-
{ name: "phase", type: "number" },
|
|
35
|
-
{ name: "auto-chain-active", type: "flag", flag: "--auto", optional: true },
|
|
36
|
-
],
|
|
37
|
-
}), "1 --auto", vars);
|
|
38
|
-
expect(vars.get("phase")).toBe("1");
|
|
39
|
-
expect(vars.get("auto-chain-active")).toBe("true");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("absent flag defaults to false", () => {
|
|
43
|
-
const vars = createVariableStore();
|
|
44
|
-
parseArguments(argsNode({
|
|
45
|
-
args: [{ name: "dry-run", type: "flag", flag: "--dry-run", optional: true }],
|
|
46
|
-
}), "", vars);
|
|
47
|
-
expect(vars.get("dry-run")).toBe("false");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("greedy last string consumes all remaining tokens", () => {
|
|
51
|
-
const vars = createVariableStore();
|
|
52
|
-
parseArguments(argsNode({
|
|
53
|
-
args: [
|
|
54
|
-
{ name: "phase", type: "number" },
|
|
55
|
-
{ name: "auto", type: "flag", flag: "--auto", optional: true },
|
|
56
|
-
{ name: "user-text", type: "string", optional: true },
|
|
57
|
-
],
|
|
58
|
-
}), "1 --auto fix the login bug", vars);
|
|
59
|
-
expect(vars.get("phase")).toBe("1");
|
|
60
|
-
expect(vars.get("auto")).toBe("true");
|
|
61
|
-
expect(vars.get("user-text")).toBe("fix the login bug");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("throws on missing required positional", () => {
|
|
65
|
-
const vars = createVariableStore();
|
|
66
|
-
expect(() => parseArguments(argsNode({
|
|
67
|
-
args: [{ name: "phase", type: "number" }],
|
|
68
|
-
}), "", vars)).toThrow(WxpArgumentsError);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("number type: throws on NaN", () => {
|
|
72
|
-
const vars = createVariableStore();
|
|
73
|
-
expect(() => parseArguments(argsNode({
|
|
74
|
-
args: [{ name: "phase", type: "number" }],
|
|
75
|
-
}), "notanumber", vars)).toThrow(WxpArgumentsError);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("keep-extra-args stores extra in _extra", () => {
|
|
79
|
-
const vars = createVariableStore();
|
|
80
|
-
parseArguments(argsNode({
|
|
81
|
-
keep: true,
|
|
82
|
-
args: [{ name: "phase", type: "number" }],
|
|
83
|
-
}), "1 extra stuff", vars);
|
|
84
|
-
expect(vars.get("_extra")).toBe("extra stuff");
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { evaluateCondition, evaluateCondExprNode } from "../conditions.js";
|
|
3
|
-
import { createVariableStore } from "../variables.js";
|
|
4
|
-
import { x } from "./helpers.js";
|
|
5
|
-
|
|
6
|
-
// Build an <if> node with a condition expression node inside <condition>
|
|
7
|
-
function ifNode(condExpr: ReturnType<typeof x>) {
|
|
8
|
-
return x("if", {}, [x("condition", {}, [condExpr])]);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function eq(leftAttrs: Record<string, string>, rightAttrs: Record<string, string>) {
|
|
12
|
-
return x("equals", {}, [x("left", leftAttrs), x("right", rightAttrs)]);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function sw(leftAttrs: Record<string, string>, rightAttrs: Record<string, string>) {
|
|
16
|
-
return x("starts-with", {}, [x("left", leftAttrs), x("right", rightAttrs)]);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe("evaluateCondition", () => {
|
|
20
|
-
it("equals: true when variable matches", () => {
|
|
21
|
-
const vars = createVariableStore();
|
|
22
|
-
vars.set("mode", "silent");
|
|
23
|
-
expect(evaluateCondition(
|
|
24
|
-
ifNode(eq({ name: "mode" }, { type: "string", value: "silent" })),
|
|
25
|
-
vars,
|
|
26
|
-
)).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("equals: false when variable does not match", () => {
|
|
30
|
-
const vars = createVariableStore();
|
|
31
|
-
vars.set("mode", "interactive");
|
|
32
|
-
expect(evaluateCondition(
|
|
33
|
-
ifNode(eq({ name: "mode" }, { type: "string", value: "silent" })),
|
|
34
|
-
vars,
|
|
35
|
-
)).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("starts-with: true when value starts with prefix", () => {
|
|
39
|
-
const vars = createVariableStore();
|
|
40
|
-
vars.set("init", "@file:/tmp/foo.json");
|
|
41
|
-
expect(evaluateCondition(
|
|
42
|
-
ifNode(sw({ name: "init" }, { type: "string", value: "@file:" })),
|
|
43
|
-
vars,
|
|
44
|
-
)).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("equals with boolean literal", () => {
|
|
48
|
-
const vars = createVariableStore();
|
|
49
|
-
vars.set("auto-chain-active", "false");
|
|
50
|
-
expect(evaluateCondition(
|
|
51
|
-
ifNode(eq({ name: "auto-chain-active" }, { type: "boolean", value: "false" })),
|
|
52
|
-
vars,
|
|
53
|
-
)).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("returns false (not throws) when variable is undefined", () => {
|
|
57
|
-
const vars = createVariableStore();
|
|
58
|
-
expect(evaluateCondition(
|
|
59
|
-
ifNode(eq({ name: "missing" }, { type: "string", value: "x" })),
|
|
60
|
-
vars,
|
|
61
|
-
)).toBe(false);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("evaluateCondExprNode — and/or", () => {
|
|
66
|
-
it("and: all children must be true", () => {
|
|
67
|
-
const vars = createVariableStore();
|
|
68
|
-
vars.set("a", "1"); vars.set("b", "2");
|
|
69
|
-
expect(evaluateCondExprNode(x("and", {}, [
|
|
70
|
-
eq({ name: "a" }, { value: "1" }),
|
|
71
|
-
eq({ name: "b" }, { value: "2" }),
|
|
72
|
-
]), vars)).toBe(true);
|
|
73
|
-
|
|
74
|
-
expect(evaluateCondExprNode(x("and", {}, [
|
|
75
|
-
eq({ name: "a" }, { value: "1" }),
|
|
76
|
-
eq({ name: "b" }, { value: "99" }),
|
|
77
|
-
]), vars)).toBe(false);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("or: any child true is sufficient", () => {
|
|
81
|
-
const vars = createVariableStore();
|
|
82
|
-
vars.set("x", "hello");
|
|
83
|
-
expect(evaluateCondExprNode(x("or", {}, [
|
|
84
|
-
eq({ name: "x" }, { value: "nope" }),
|
|
85
|
-
eq({ name: "x" }, { value: "hello" }),
|
|
86
|
-
]), vars)).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("not-equals", () => {
|
|
90
|
-
const vars = createVariableStore();
|
|
91
|
-
vars.set("status", "complete");
|
|
92
|
-
expect(evaluateCondExprNode(
|
|
93
|
-
x("not-equals", {}, [x("left", { name: "status" }), x("right", { value: "pending" })]),
|
|
94
|
-
vars,
|
|
95
|
-
)).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("less-than with numeric coercion", () => {
|
|
99
|
-
const vars = createVariableStore();
|
|
100
|
-
vars.set("n", "3");
|
|
101
|
-
expect(evaluateCondExprNode(
|
|
102
|
-
x("less-than", {}, [x("left", { name: "n", type: "number" }), x("right", { type: "number", value: "5" })]),
|
|
103
|
-
vars,
|
|
104
|
-
)).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import type { Mock } from "vitest";
|
|
3
|
-
|
|
4
|
-
vi.mock("node:child_process", () => ({
|
|
5
|
-
execFileSync: vi.fn().mockReturnValue("exec-output\n"),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
import { executeBlock, WxpExecutionError } from "../executor.js";
|
|
9
|
-
import { createVariableStore } from "../variables.js";
|
|
10
|
-
import type { WxpExecContext } from "../../schemas/wxp.zod.js";
|
|
11
|
-
import { x } from "./helpers.js";
|
|
12
|
-
|
|
13
|
-
const makeCtx = (): WxpExecContext => ({
|
|
14
|
-
config: {
|
|
15
|
-
trustedPaths: [],
|
|
16
|
-
untrustedPaths: [],
|
|
17
|
-
shellAllowlist: ["git", "pi-gsd-tools", "node", "cat", "ls", "echo", "find"],
|
|
18
|
-
shellBanlist: [],
|
|
19
|
-
shellTimeoutMs: 30_000,
|
|
20
|
-
},
|
|
21
|
-
projectRoot: "/project",
|
|
22
|
-
pkgRoot: "/pkg",
|
|
23
|
-
onDisplay: () => {},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Build a <gsd-execute> container
|
|
27
|
-
function exec(...children: ReturnType<typeof x>[]) {
|
|
28
|
-
return x("gsd-execute", {}, children);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("executeBlock", () => {
|
|
32
|
-
beforeEach(() => vi.clearAllMocks());
|
|
33
|
-
|
|
34
|
-
it("executes a shell child and stores result", () => {
|
|
35
|
-
const vars = createVariableStore();
|
|
36
|
-
executeBlock(exec(
|
|
37
|
-
x("shell", { command: "git" }, [
|
|
38
|
-
x("args", {}, [x("arg", { string: "status" })]),
|
|
39
|
-
x("outs", {}, [x("out", { type: "string", name: "out" })]),
|
|
40
|
-
]),
|
|
41
|
-
), vars, makeCtx());
|
|
42
|
-
expect(vars.get("out")).toBe("exec-output");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("evaluates if/condition/equals and executes then-branch when true", () => {
|
|
46
|
-
const vars = createVariableStore();
|
|
47
|
-
vars.set("auto-chain-active", "false");
|
|
48
|
-
executeBlock(exec(
|
|
49
|
-
x("if", {}, [
|
|
50
|
-
x("condition", {}, [
|
|
51
|
-
x("equals", {}, [
|
|
52
|
-
x("left", { name: "auto-chain-active" }),
|
|
53
|
-
x("right", { type: "boolean", value: "false" }),
|
|
54
|
-
]),
|
|
55
|
-
]),
|
|
56
|
-
x("then", {}, [
|
|
57
|
-
x("shell", { command: "git" }, [
|
|
58
|
-
x("args", {}, []),
|
|
59
|
-
x("outs", {}, [x("out", { type: "string", name: "branch" })]),
|
|
60
|
-
]),
|
|
61
|
-
]),
|
|
62
|
-
]),
|
|
63
|
-
), vars, makeCtx());
|
|
64
|
-
expect(vars.get("branch")).toBe("exec-output");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("skips then-branch when condition is false", () => {
|
|
68
|
-
const vars = createVariableStore();
|
|
69
|
-
vars.set("auto-chain-active", "true");
|
|
70
|
-
executeBlock(exec(
|
|
71
|
-
x("if", {}, [
|
|
72
|
-
x("condition", {}, [
|
|
73
|
-
x("equals", {}, [
|
|
74
|
-
x("left", { name: "auto-chain-active" }),
|
|
75
|
-
x("right", { type: "boolean", value: "false" }),
|
|
76
|
-
]),
|
|
77
|
-
]),
|
|
78
|
-
x("then", {}, [
|
|
79
|
-
x("shell", { command: "git" }, [
|
|
80
|
-
x("args", {}, []),
|
|
81
|
-
x("outs", {}, [x("out", { type: "string", name: "branch" })]),
|
|
82
|
-
]),
|
|
83
|
-
]),
|
|
84
|
-
]),
|
|
85
|
-
), vars, makeCtx());
|
|
86
|
-
expect(vars.get("branch")).toBeUndefined();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("wraps errors in WxpExecutionError", () => {
|
|
90
|
-
const vars = createVariableStore();
|
|
91
|
-
expect(() => executeBlock(exec(
|
|
92
|
-
x("shell", { command: "bash" }, [x("args", {}, []), x("outs", {}, [])]),
|
|
93
|
-
), vars, makeCtx())).toThrow(WxpExecutionError);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import type { XmlNode } from "../../schemas/wxp.zod.js";
|
|
3
|
-
|
|
4
|
-
// ─── XmlNode builder helper ───────────────────────────────────────────────────
|
|
5
|
-
export function x(
|
|
6
|
-
tag: string,
|
|
7
|
-
attrs: Record<string, string> = {},
|
|
8
|
-
children: XmlNode[] = [],
|
|
9
|
-
): XmlNode {
|
|
10
|
-
return { tag, attrs, children, selfClosing: children.length === 0 && !attrs["__open"] };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe("x() helper sanity", () => {
|
|
14
|
-
it("builds a self-closing node", () => {
|
|
15
|
-
const node = x("arg", { string: "hello" });
|
|
16
|
-
expect(node.tag).toBe("arg");
|
|
17
|
-
expect(node.attrs["string"]).toBe("hello");
|
|
18
|
-
expect(node.selfClosing).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("builds a node with children", () => {
|
|
22
|
-
const node = x("args", {}, [x("arg", { string: "a" })]);
|
|
23
|
-
expect(node.children).toHaveLength(1);
|
|
24
|
-
expect(node.selfClosing).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
});
|