@beignet/cli 0.0.31 → 0.0.33
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 +107 -0
- package/README.md +70 -17
- package/dist/check.d.ts +2 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +16 -0
- package/dist/check.js.map +1 -1
- package/dist/choices.d.ts +22 -2
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +62 -0
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +6 -2
- package/dist/create-prompts.d.ts.map +1 -1
- package/dist/create-prompts.js +1 -1
- package/dist/create-prompts.js.map +1 -1
- package/dist/db.d.ts +5 -5
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +5 -5
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -27
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +5 -0
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +805 -62
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +5 -3
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +58 -29
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +12 -3
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +52 -11
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.d.ts.map +1 -1
- package/dist/make/tenancy.js +6 -4
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +136 -65
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +114 -1
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +190 -0
- package/dist/outbox.js.map +1 -1
- package/dist/preflight.d.ts +90 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +423 -0
- package/dist/preflight.js.map +1 -0
- package/dist/provider-add.d.ts +10 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +255 -16
- package/dist/provider-add.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +2 -3
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +6 -3
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +5 -11
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +2 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +10 -5
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +5 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +13 -1
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.d.ts +5 -0
- package/dist/templates/testing.d.ts.map +1 -0
- package/dist/templates/testing.js +542 -0
- package/dist/templates/testing.js.map +1 -0
- package/dist/templates/todos.js +2 -2
- package/package.json +3 -2
- package/skills/app-structure/SKILL.md +12 -4
- package/src/check.ts +23 -0
- package/src/choices.ts +84 -0
- package/src/config.ts +4 -0
- package/src/create-prompts.ts +8 -2
- package/src/db.ts +12 -12
- package/src/index.ts +592 -37
- package/src/inspect.ts +1253 -156
- package/src/make/inbox.ts +5 -3
- package/src/make/payments.ts +58 -29
- package/src/make/shared.ts +87 -13
- package/src/make/tenancy.ts +6 -4
- package/src/make.ts +179 -65
- package/src/outbox.ts +363 -0
- package/src/preflight.ts +596 -0
- package/src/provider-add.ts +270 -16
- package/src/task.ts +2 -3
- package/src/templates/agents.ts +6 -3
- package/src/templates/base.ts +5 -13
- package/src/templates/index.ts +2 -0
- package/src/templates/server.ts +10 -5
- package/src/templates/shared.ts +15 -1
- package/src/templates/testing.ts +545 -0
- package/src/templates/todos.ts +2 -2
- package/src/test-helpers/generated-app.ts +10 -6
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
export const testHelperTemplateFile = {
|
|
2
|
+
path: "lib/beignet-test.ts",
|
|
3
|
+
content: `import assert from "node:assert/strict";
|
|
4
|
+
import {
|
|
5
|
+
after,
|
|
6
|
+
afterEach,
|
|
7
|
+
before,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe as nodeDescribe,
|
|
10
|
+
it as nodeIt,
|
|
11
|
+
test as nodeTest,
|
|
12
|
+
} from "node:test";
|
|
13
|
+
import { inspect, isDeepStrictEqual } from "node:util";
|
|
14
|
+
|
|
15
|
+
export { after, afterEach, before, beforeEach };
|
|
16
|
+
|
|
17
|
+
type TestBody = () => void | Promise<void>;
|
|
18
|
+
type TestEach = {
|
|
19
|
+
<T>(cases: readonly T[]): (
|
|
20
|
+
name: string,
|
|
21
|
+
body: (value: T) => void | Promise<void>,
|
|
22
|
+
) => void;
|
|
23
|
+
<T extends readonly unknown[]>(cases: readonly T[]): (
|
|
24
|
+
name: string,
|
|
25
|
+
body: (...values: T) => void | Promise<void>,
|
|
26
|
+
) => void;
|
|
27
|
+
};
|
|
28
|
+
type TestRunner = ((name: string, body: TestBody) => unknown) & {
|
|
29
|
+
each: TestEach;
|
|
30
|
+
skip: (name: string, body: TestBody) => unknown;
|
|
31
|
+
skipIf: (condition: boolean) => TestRunner;
|
|
32
|
+
};
|
|
33
|
+
type NodeRunner = {
|
|
34
|
+
(name: string, body: TestBody): unknown;
|
|
35
|
+
(
|
|
36
|
+
name: string,
|
|
37
|
+
options: { skip?: boolean },
|
|
38
|
+
body: TestBody,
|
|
39
|
+
): unknown;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type AsymmetricMatcher =
|
|
43
|
+
| { kind: "any"; expected: unknown }
|
|
44
|
+
| { kind: "arrayContaining"; expected: readonly unknown[] }
|
|
45
|
+
| { kind: "objectContaining"; expected: Record<string, unknown> };
|
|
46
|
+
|
|
47
|
+
type BaseMatchers = {
|
|
48
|
+
toBe(expected: unknown): void;
|
|
49
|
+
toEqual(expected: unknown): void;
|
|
50
|
+
toMatchObject(expected: unknown): void;
|
|
51
|
+
toContain(expected: unknown): void;
|
|
52
|
+
toContainEqual(expected: unknown): void;
|
|
53
|
+
toHaveLength(expected: number): void;
|
|
54
|
+
toBeDefined(): void;
|
|
55
|
+
toBeUndefined(): void;
|
|
56
|
+
toBeNull(): void;
|
|
57
|
+
toBeGreaterThan(expected: number): void;
|
|
58
|
+
toMatch(expected: string | RegExp): void;
|
|
59
|
+
toThrow(expected?: string | RegExp | (new (...args: unknown[]) => object)): void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type AsyncMatchers = {
|
|
63
|
+
toBe(expected: unknown): Promise<void>;
|
|
64
|
+
toEqual(expected: unknown): Promise<void>;
|
|
65
|
+
toMatchObject(expected: unknown): Promise<void>;
|
|
66
|
+
toBeNull(): Promise<void>;
|
|
67
|
+
toBeUndefined(): Promise<void>;
|
|
68
|
+
toThrow(expected?: string | RegExp | (new (...args: unknown[]) => object)): Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type Matchers = BaseMatchers & {
|
|
72
|
+
not: BaseMatchers;
|
|
73
|
+
resolves: AsyncMatchers;
|
|
74
|
+
rejects: AsyncMatchers;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type Expect = ((actual: unknown) => Matchers) & {
|
|
78
|
+
any(expected: unknown): AsymmetricMatcher;
|
|
79
|
+
arrayContaining(expected: readonly unknown[]): AsymmetricMatcher;
|
|
80
|
+
objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const describe = createRunner(nodeDescribe as NodeRunner);
|
|
84
|
+
export const it = createRunner(nodeIt as NodeRunner);
|
|
85
|
+
export const test = createRunner(nodeTest as NodeRunner);
|
|
86
|
+
|
|
87
|
+
export const expect = Object.assign(
|
|
88
|
+
(actual: unknown) => createMatchers(actual),
|
|
89
|
+
{
|
|
90
|
+
any: (expected: unknown): AsymmetricMatcher => ({
|
|
91
|
+
kind: "any",
|
|
92
|
+
expected,
|
|
93
|
+
}),
|
|
94
|
+
arrayContaining: (expected: readonly unknown[]): AsymmetricMatcher => ({
|
|
95
|
+
kind: "arrayContaining",
|
|
96
|
+
expected,
|
|
97
|
+
}),
|
|
98
|
+
objectContaining: (
|
|
99
|
+
expected: Record<string, unknown>,
|
|
100
|
+
): AsymmetricMatcher => ({ kind: "objectContaining", expected }),
|
|
101
|
+
},
|
|
102
|
+
) satisfies Expect;
|
|
103
|
+
|
|
104
|
+
function createRunner(
|
|
105
|
+
base: NodeRunner,
|
|
106
|
+
options?: { skip?: boolean },
|
|
107
|
+
): TestRunner {
|
|
108
|
+
const runner = ((name: string, body: TestBody) =>
|
|
109
|
+
runTest(base, name, body, options)) as TestRunner;
|
|
110
|
+
runner.each = ((cases: readonly unknown[]) => {
|
|
111
|
+
return (
|
|
112
|
+
name: string,
|
|
113
|
+
body: (...values: unknown[]) => void | Promise<void>,
|
|
114
|
+
) => {
|
|
115
|
+
for (const testCase of cases) {
|
|
116
|
+
const values = (
|
|
117
|
+
Array.isArray(testCase) ? testCase : [testCase]
|
|
118
|
+
) as unknown[];
|
|
119
|
+
runTest(base, formatEachName(name, values), () => body(...values), options);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}) as TestEach;
|
|
123
|
+
runner.skip = (name: string, body: TestBody) =>
|
|
124
|
+
runTest(base, name, body, { skip: true });
|
|
125
|
+
runner.skipIf = (condition: boolean) =>
|
|
126
|
+
condition ? createRunner(base, { skip: true }) : runner;
|
|
127
|
+
return runner;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runTest(
|
|
131
|
+
base: NodeRunner,
|
|
132
|
+
name: string,
|
|
133
|
+
body: TestBody,
|
|
134
|
+
options?: { skip?: boolean },
|
|
135
|
+
): unknown {
|
|
136
|
+
return options ? base(name, options, body) : base(name, body);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatEachName(name: string, values: readonly unknown[]): string {
|
|
140
|
+
let index = 0;
|
|
141
|
+
return name.replace(/%[sdifoO]/g, () => formatInline(values[index++]));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createMatchers(actual: unknown): Matchers {
|
|
145
|
+
return {
|
|
146
|
+
...createBaseMatchers(actual, false),
|
|
147
|
+
not: createBaseMatchers(actual, true),
|
|
148
|
+
resolves: createAsyncMatchers(actual, "resolves"),
|
|
149
|
+
rejects: createAsyncMatchers(actual, "rejects"),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function createBaseMatchers(actual: unknown, negated: boolean): BaseMatchers {
|
|
154
|
+
return {
|
|
155
|
+
toBe(expected) {
|
|
156
|
+
check(
|
|
157
|
+
Object.is(actual, expected),
|
|
158
|
+
negated,
|
|
159
|
+
"Expected values to be identical.",
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
toEqual(expected) {
|
|
163
|
+
check(matchesExpected(actual, expected), negated, "Expected values to be equal.");
|
|
164
|
+
},
|
|
165
|
+
toMatchObject(expected) {
|
|
166
|
+
check(
|
|
167
|
+
matchesObject(actual, expected),
|
|
168
|
+
negated,
|
|
169
|
+
"Expected value to match object.",
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
toContain(expected) {
|
|
173
|
+
const matches =
|
|
174
|
+
typeof actual === "string"
|
|
175
|
+
? actual.includes(String(expected))
|
|
176
|
+
: Array.isArray(actual) && actual.includes(expected);
|
|
177
|
+
check(matches, negated, "Expected value to contain item.");
|
|
178
|
+
},
|
|
179
|
+
toContainEqual(expected) {
|
|
180
|
+
check(
|
|
181
|
+
Array.isArray(actual) &&
|
|
182
|
+
actual.some((item) => matchesExpected(item, expected)),
|
|
183
|
+
negated,
|
|
184
|
+
"Expected array to contain equal item.",
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
toHaveLength(expected) {
|
|
188
|
+
const length =
|
|
189
|
+
actual !== null && typeof actual === "object"
|
|
190
|
+
? (actual as { length?: unknown }).length
|
|
191
|
+
: undefined;
|
|
192
|
+
check(length === expected, negated, "Expected value to have length.");
|
|
193
|
+
},
|
|
194
|
+
toBeDefined() {
|
|
195
|
+
check(actual !== undefined, negated, "Expected value to be defined.");
|
|
196
|
+
},
|
|
197
|
+
toBeUndefined() {
|
|
198
|
+
check(actual === undefined, negated, "Expected value to be undefined.");
|
|
199
|
+
},
|
|
200
|
+
toBeNull() {
|
|
201
|
+
check(actual === null, negated, "Expected value to be null.");
|
|
202
|
+
},
|
|
203
|
+
toBeGreaterThan(expected) {
|
|
204
|
+
check(
|
|
205
|
+
typeof actual === "number" && actual > expected,
|
|
206
|
+
negated,
|
|
207
|
+
"Expected value to be greater than " + expected + ".",
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
toMatch(expected) {
|
|
211
|
+
const text = String(actual);
|
|
212
|
+
const matches =
|
|
213
|
+
typeof expected === "string" ? text.includes(expected) : expected.test(text);
|
|
214
|
+
check(matches, negated, "Expected value to match pattern.");
|
|
215
|
+
},
|
|
216
|
+
toThrow(expected) {
|
|
217
|
+
assert.equal(typeof actual, "function", "toThrow expects a function.");
|
|
218
|
+
let didThrow = false;
|
|
219
|
+
let thrown: unknown;
|
|
220
|
+
try {
|
|
221
|
+
(actual as () => unknown)();
|
|
222
|
+
} catch (error) {
|
|
223
|
+
didThrow = true;
|
|
224
|
+
thrown = error;
|
|
225
|
+
}
|
|
226
|
+
check(
|
|
227
|
+
didThrow && thrownMatches(thrown, expected),
|
|
228
|
+
negated,
|
|
229
|
+
"Expected function to throw.",
|
|
230
|
+
);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function createAsyncMatchers(
|
|
236
|
+
actual: unknown,
|
|
237
|
+
kind: "resolves" | "rejects",
|
|
238
|
+
): AsyncMatchers {
|
|
239
|
+
const resolveActual = async () =>
|
|
240
|
+
kind === "resolves" ? await Promise.resolve(actual) : await rejected(actual);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
async toBe(expected) {
|
|
244
|
+
createBaseMatchers(await resolveActual(), false).toBe(expected);
|
|
245
|
+
},
|
|
246
|
+
async toEqual(expected) {
|
|
247
|
+
createBaseMatchers(await resolveActual(), false).toEqual(expected);
|
|
248
|
+
},
|
|
249
|
+
async toMatchObject(expected) {
|
|
250
|
+
createBaseMatchers(await resolveActual(), false).toMatchObject(expected);
|
|
251
|
+
},
|
|
252
|
+
async toBeNull() {
|
|
253
|
+
createBaseMatchers(await resolveActual(), false).toBeNull();
|
|
254
|
+
},
|
|
255
|
+
async toBeUndefined() {
|
|
256
|
+
createBaseMatchers(await resolveActual(), false).toBeUndefined();
|
|
257
|
+
},
|
|
258
|
+
async toThrow(expected) {
|
|
259
|
+
const thrown =
|
|
260
|
+
kind === "rejects" ? await rejected(actual) : await resolveActual();
|
|
261
|
+
check(
|
|
262
|
+
thrownMatches(thrown, expected),
|
|
263
|
+
false,
|
|
264
|
+
"Expected promise to reject with matching error.",
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function rejected(actual: unknown): Promise<unknown> {
|
|
271
|
+
try {
|
|
272
|
+
await Promise.resolve(actual);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return error;
|
|
275
|
+
}
|
|
276
|
+
assert.fail("Expected promise to reject.");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function check(condition: boolean, negated: boolean, message: string): void {
|
|
280
|
+
assert.ok(
|
|
281
|
+
negated ? !condition : condition,
|
|
282
|
+
negated ? "Negated assertion failed: " + message : message,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function matchesExpected(actual: unknown, expected: unknown): boolean {
|
|
287
|
+
if (!containsAsymmetric(expected)) {
|
|
288
|
+
return isDeepStrictEqual(actual, expected);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (isAsymmetricMatcher(expected)) {
|
|
292
|
+
switch (expected.kind) {
|
|
293
|
+
case "any":
|
|
294
|
+
return matchesAny(actual, expected.expected);
|
|
295
|
+
case "arrayContaining":
|
|
296
|
+
return (
|
|
297
|
+
Array.isArray(actual) &&
|
|
298
|
+
expected.expected.every((expectedItem) =>
|
|
299
|
+
actual.some((actualItem) => matchesExpected(actualItem, expectedItem)),
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
case "objectContaining":
|
|
303
|
+
return matchesObject(actual, expected.expected);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (Array.isArray(expected)) {
|
|
308
|
+
return (
|
|
309
|
+
Array.isArray(actual) &&
|
|
310
|
+
actual.length === expected.length &&
|
|
311
|
+
expected.every((item, index) => matchesExpected(actual[index], item))
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (isObjectRecord(expected)) {
|
|
316
|
+
if (!isObjectRecord(actual)) return false;
|
|
317
|
+
const actualKeys = Object.keys(actual);
|
|
318
|
+
const expectedKeys = Object.keys(expected);
|
|
319
|
+
return (
|
|
320
|
+
actualKeys.length === expectedKeys.length &&
|
|
321
|
+
expectedKeys.every(
|
|
322
|
+
(key) =>
|
|
323
|
+
Object.hasOwn(actual, key) &&
|
|
324
|
+
matchesExpected(actual[key], expected[key]),
|
|
325
|
+
)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return Object.is(actual, expected);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function matchesObject(actual: unknown, expected: unknown): boolean {
|
|
333
|
+
if (isAsymmetricMatcher(expected)) return matchesExpected(actual, expected);
|
|
334
|
+
if (Array.isArray(expected)) {
|
|
335
|
+
return (
|
|
336
|
+
Array.isArray(actual) &&
|
|
337
|
+
actual.length >= expected.length &&
|
|
338
|
+
expected.every((item, index) => matchesObject(actual[index], item))
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (!isObjectRecord(expected)) return matchesExpected(actual, expected);
|
|
342
|
+
if (!isObjectRecord(actual)) return false;
|
|
343
|
+
return Object.entries(expected).every(([key, expectedValue]) =>
|
|
344
|
+
matchesObject(actual[key], expectedValue),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function containsAsymmetric(value: unknown): boolean {
|
|
349
|
+
if (isAsymmetricMatcher(value)) return true;
|
|
350
|
+
if (Array.isArray(value)) return value.some(containsAsymmetric);
|
|
351
|
+
if (!isObjectRecord(value)) return false;
|
|
352
|
+
return Object.values(value).some(containsAsymmetric);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function isAsymmetricMatcher(value: unknown): value is AsymmetricMatcher {
|
|
356
|
+
return (
|
|
357
|
+
isObjectRecord(value) &&
|
|
358
|
+
(value.kind === "any" ||
|
|
359
|
+
value.kind === "arrayContaining" ||
|
|
360
|
+
value.kind === "objectContaining")
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function matchesAny(actual: unknown, expected: unknown): boolean {
|
|
365
|
+
if (expected === String) return typeof actual === "string";
|
|
366
|
+
if (expected === Number) return typeof actual === "number";
|
|
367
|
+
if (expected === Boolean) return typeof actual === "boolean";
|
|
368
|
+
if (expected === Array) return Array.isArray(actual);
|
|
369
|
+
if (expected === Object) return isObjectRecord(actual);
|
|
370
|
+
if (expected instanceof Function) {
|
|
371
|
+
return actual instanceof (expected as new (...args: unknown[]) => object);
|
|
372
|
+
}
|
|
373
|
+
return actual !== undefined && actual !== null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function thrownMatches(
|
|
377
|
+
error: unknown,
|
|
378
|
+
expected: string | RegExp | (new (...args: unknown[]) => object) | undefined,
|
|
379
|
+
): boolean {
|
|
380
|
+
if (expected === undefined) return true;
|
|
381
|
+
if (typeof expected === "string") return errorMessage(error).includes(expected);
|
|
382
|
+
if (expected instanceof RegExp) return expected.test(errorMessage(error));
|
|
383
|
+
return error instanceof expected;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function errorMessage(error: unknown): string {
|
|
387
|
+
return error instanceof Error ? error.message : String(error);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
391
|
+
return value !== null && typeof value === "object";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function formatInline(value: unknown): string {
|
|
395
|
+
return typeof value === "string"
|
|
396
|
+
? value
|
|
397
|
+
: inspect(value, { breakLength: Number.POSITIVE_INFINITY, depth: 2 });
|
|
398
|
+
}
|
|
399
|
+
`,
|
|
400
|
+
};
|
|
401
|
+
export const testRunnerTemplateFile = {
|
|
402
|
+
path: "lib/beignet-test-runner.ts",
|
|
403
|
+
content: `import { spawnSync } from "node:child_process";
|
|
404
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
405
|
+
import path from "node:path";
|
|
406
|
+
|
|
407
|
+
const defaultTestRoots = [
|
|
408
|
+
"app",
|
|
409
|
+
"features",
|
|
410
|
+
"infra",
|
|
411
|
+
"lib",
|
|
412
|
+
"ports",
|
|
413
|
+
"server",
|
|
414
|
+
"tests",
|
|
415
|
+
"src/tests",
|
|
416
|
+
];
|
|
417
|
+
const ignoredDirectories = new Set([
|
|
418
|
+
".beignet-local",
|
|
419
|
+
".git",
|
|
420
|
+
".next",
|
|
421
|
+
".turbo",
|
|
422
|
+
"coverage",
|
|
423
|
+
"dist",
|
|
424
|
+
"node_modules",
|
|
425
|
+
]);
|
|
426
|
+
const testFilePattern = /\\.(test|spec)\\.[cm]?[tj]sx?$/;
|
|
427
|
+
|
|
428
|
+
const explicitArgs = process.argv.slice(2);
|
|
429
|
+
const testArgs =
|
|
430
|
+
explicitArgs.length > 0
|
|
431
|
+
? resolveTestArgs(process.cwd(), explicitArgs)
|
|
432
|
+
: discoverTestFiles(process.cwd());
|
|
433
|
+
|
|
434
|
+
if (testArgs.length === 0) {
|
|
435
|
+
console.log("No test files found.");
|
|
436
|
+
process.exit(0);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const result = spawnSync(
|
|
440
|
+
process.execPath,
|
|
441
|
+
["--import", "tsx", "--test", ...testArgs],
|
|
442
|
+
{
|
|
443
|
+
cwd: process.cwd(),
|
|
444
|
+
env: process.env,
|
|
445
|
+
stdio: "inherit",
|
|
446
|
+
},
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if (result.error) {
|
|
450
|
+
console.error(result.error.message);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
process.exit(result.status ?? 1);
|
|
455
|
+
|
|
456
|
+
function discoverTestFiles(cwd: string): string[] {
|
|
457
|
+
const files: string[] = [];
|
|
458
|
+
for (const root of testRoots(cwd)) {
|
|
459
|
+
const rootPath = path.join(cwd, root);
|
|
460
|
+
if (existsSync(rootPath)) {
|
|
461
|
+
walk(rootPath, cwd, files);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return files.sort();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function resolveTestArgs(cwd: string, args: string[]): string[] {
|
|
468
|
+
const resolved: string[] = [];
|
|
469
|
+
for (const arg of args) {
|
|
470
|
+
if (arg.startsWith("-")) {
|
|
471
|
+
resolved.push(arg);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const argPath = path.resolve(cwd, arg);
|
|
476
|
+
if (!existsSync(argPath)) {
|
|
477
|
+
resolved.push(arg);
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const stats = statSync(argPath);
|
|
482
|
+
if (stats.isDirectory()) {
|
|
483
|
+
const files: string[] = [];
|
|
484
|
+
walk(argPath, cwd, files);
|
|
485
|
+
resolved.push(...files.sort());
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
resolved.push(path.relative(cwd, argPath).split(path.sep).join("/"));
|
|
490
|
+
}
|
|
491
|
+
return Array.from(new Set(resolved));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function testRoots(cwd: string): string[] {
|
|
495
|
+
return Array.from(new Set([...defaultTestRoots, ...configuredRoots(cwd)]));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function configuredRoots(cwd: string): string[] {
|
|
499
|
+
const configPath = path.join(cwd, "beignet.config.json");
|
|
500
|
+
if (!existsSync(configPath)) return [];
|
|
501
|
+
try {
|
|
502
|
+
const config = JSON.parse(readFileSync(configPath, "utf8")) as {
|
|
503
|
+
paths?: Record<string, string>;
|
|
504
|
+
};
|
|
505
|
+
const paths = config.paths ?? {};
|
|
506
|
+
return [
|
|
507
|
+
paths.tests,
|
|
508
|
+
paths.features,
|
|
509
|
+
paths.routes ? path.dirname(paths.routes) : undefined,
|
|
510
|
+
paths.server ? path.dirname(paths.server) : undefined,
|
|
511
|
+
paths.infrastructurePorts
|
|
512
|
+
? path.dirname(paths.infrastructurePorts)
|
|
513
|
+
: undefined,
|
|
514
|
+
paths.ports ? path.dirname(paths.ports) : undefined,
|
|
515
|
+
paths.useCaseBuilder ? path.dirname(paths.useCaseBuilder) : undefined,
|
|
516
|
+
].filter((root): root is string => Boolean(root) && root !== ".");
|
|
517
|
+
} catch {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function walk(directory: string, cwd: string, files: string[]): void {
|
|
523
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
524
|
+
const entryPath = path.join(directory, entry.name);
|
|
525
|
+
if (entry.isDirectory()) {
|
|
526
|
+
if (!ignoredDirectories.has(entry.name)) {
|
|
527
|
+
walk(entryPath, cwd, files);
|
|
528
|
+
}
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (entry.isFile() && testFilePattern.test(entry.name)) {
|
|
532
|
+
files.push(path.relative(cwd, entryPath).split(path.sep).join("/"));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
`,
|
|
537
|
+
};
|
|
538
|
+
export const testSupportTemplateFiles = [
|
|
539
|
+
testHelperTemplateFile,
|
|
540
|
+
testRunnerTemplateFile,
|
|
541
|
+
];
|
|
542
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/templates/testing.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAClD,IAAI,EAAE,qBAAqB;IAC3B,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4YV;CACA,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAClD,IAAI,EAAE,4BAA4B;IAClC,OAAO,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqIV;CACA,CAAC;AAEF,MAAM,CAAC,MAAM,wBAAwB,GAAmB;IACtD,sBAAsB;IACtB,sBAAsB;CACvB,CAAC"}
|
package/dist/templates/todos.js
CHANGED
|
@@ -359,7 +359,7 @@ export function createTestTodoRepository(): TodoRepository {
|
|
|
359
359
|
};
|
|
360
360
|
}
|
|
361
361
|
`,
|
|
362
|
-
useCaseTest: `import { describe, expect, it } from "
|
|
362
|
+
useCaseTest: `import { describe, expect, it } from "@/lib/beignet-test";
|
|
363
363
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
364
364
|
import { createTestUserActor } from "@beignet/core/ports/testing";
|
|
365
365
|
import {
|
|
@@ -476,7 +476,7 @@ describe("todos use cases", () => {
|
|
|
476
476
|
});
|
|
477
477
|
});
|
|
478
478
|
`,
|
|
479
|
-
routesTest: `import { describe, expect, it } from "
|
|
479
|
+
routesTest: `import { describe, expect, it } from "@/lib/beignet-test";
|
|
480
480
|
import { createStaticAuth } from "@beignet/core/ports";
|
|
481
481
|
import { createIdempotencyHooks, defineRoutes } from "@beignet/core/server";
|
|
482
482
|
import { createTestPorts } from "@beignet/core/testing";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beignet/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for creating and maintaining Beignet apps.",
|
|
6
6
|
"main": "./dist/lib.js",
|
|
@@ -65,10 +65,11 @@
|
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/bun": "^1.3.13",
|
|
67
67
|
"@types/node": "^20.10.0",
|
|
68
|
+
"next-themes": "^0.4.6",
|
|
68
69
|
"typescript": "^5.3.0"
|
|
69
70
|
},
|
|
70
71
|
"dependencies": {
|
|
71
|
-
"@beignet/core": "^0.0.
|
|
72
|
+
"@beignet/core": "^0.0.33",
|
|
72
73
|
"@clack/prompts": "^1.5.1",
|
|
73
74
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
74
75
|
"@stricli/core": "^1.2.7",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: app-structure
|
|
3
|
-
description: "Maintain Beignet app structure with @beignet/cli: create starter output, make generators, full-slice feature recipes, db schema
|
|
3
|
+
description: "Maintain Beignet app structure with @beignet/cli: create starter output, make generators, full-slice feature recipes, db schema sync, TenantScope resources, workflow registries, runtime-integrity manifests, beignet.config paths, routes inspection, lint dependency boundaries, doctor drift checks and fixes, MCP tools, generated AGENTS.md guidance, Intent package skills, and generated-app validation. Use when scaffolding, generating, fixing, or reviewing a Beignet app's structure."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Beignet CLI App Structure
|
|
@@ -63,12 +63,16 @@ Hand-written files do not run until registered:
|
|
|
63
63
|
- schedules -> `server/schedules.ts`
|
|
64
64
|
- tasks -> `server/tasks.ts`
|
|
65
65
|
- outbox events/jobs -> `server/outbox.ts`
|
|
66
|
-
- listeners -> `registerListeners(...)` in provider
|
|
66
|
+
- listeners -> `server/listeners.ts` plus `registerListeners(...)` in server provider wiring
|
|
67
67
|
- seeds -> the app-owned seed entrypoint, usually `server/seed.ts`
|
|
68
68
|
|
|
69
|
+
When an app opts into runtime integrity, keep `server/runtime-integrity.ts`
|
|
70
|
+
aligned with those central registries. The boot check is pure and
|
|
71
|
+
serverless-safe: it compares declared listeners, schedules, tasks, and outbox
|
|
72
|
+
artifacts against runtime registries before providers start.
|
|
73
|
+
|
|
69
74
|
Run `beignet doctor --strict` after hand edits. Use `beignet doctor --fix` for
|
|
70
|
-
low-risk route-group, schedule, task, and
|
|
71
|
-
drift remains manual.
|
|
75
|
+
low-risk route-group, schedule, task, outbox, and listener registry repair.
|
|
72
76
|
|
|
73
77
|
## Path Config
|
|
74
78
|
|
|
@@ -113,6 +117,10 @@ root at `src/client/index.ts`.
|
|
|
113
117
|
- route-owned error catalog drift
|
|
114
118
|
- misplaced workflow artifacts
|
|
115
119
|
- unregistered schedules, tasks, outbox events/jobs, and listeners
|
|
120
|
+
- runtime manifest and registry drift for apps that opt into boot integrity
|
|
121
|
+
- tenant-scoped Drizzle repository drift, including generated `TenantScope`
|
|
122
|
+
boundaries, raw `tenantId` app-facing repository methods, and scoped
|
|
123
|
+
`tenantScopeId(scope)` predicates for tenant/workspace tables
|
|
116
124
|
|
|
117
125
|
Fix structure or config instead of suppressing diagnostics.
|
|
118
126
|
|
package/src/check.ts
CHANGED
|
@@ -38,6 +38,8 @@ export type CheckAppOptions = {
|
|
|
38
38
|
cwd?: string;
|
|
39
39
|
fix?: boolean;
|
|
40
40
|
color?: boolean;
|
|
41
|
+
/** Append a `beignet preflight` step after the doctor step. */
|
|
42
|
+
preflight?: boolean;
|
|
41
43
|
/** Called after each step completes, for incremental progress output. */
|
|
42
44
|
onStep?: (step: CheckStep) => void;
|
|
43
45
|
};
|
|
@@ -69,6 +71,10 @@ export async function checkApp(
|
|
|
69
71
|
const { step: doctorStep, fixes } = await runDoctorStep(targetDir, options);
|
|
70
72
|
record(doctorStep);
|
|
71
73
|
|
|
74
|
+
if (options.preflight) {
|
|
75
|
+
record(await preflightStep(targetDir));
|
|
76
|
+
}
|
|
77
|
+
|
|
72
78
|
const scripts = await readPackageScripts(targetDir);
|
|
73
79
|
for (const script of scriptStepNames) {
|
|
74
80
|
record(await scriptStep(targetDir, runner, script, scripts));
|
|
@@ -244,3 +250,20 @@ async function readPackageScripts(
|
|
|
244
250
|
};
|
|
245
251
|
return parsed.scripts ?? {};
|
|
246
252
|
}
|
|
253
|
+
|
|
254
|
+
async function preflightStep(targetDir: string): Promise<CheckStep> {
|
|
255
|
+
const startedAt = Date.now();
|
|
256
|
+
const { formatPreflight, runPreflight } = await import("./preflight.js");
|
|
257
|
+
const result = await runPreflight({ cwd: targetDir });
|
|
258
|
+
const failed = result.findings.some(
|
|
259
|
+
(finding) => finding.severity === "error",
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
name: "beignet preflight",
|
|
264
|
+
command: "beignet preflight",
|
|
265
|
+
status: failed ? "failed" : "passed",
|
|
266
|
+
...(failed ? { output: formatPreflight(result, targetDir) } : {}),
|
|
267
|
+
durationMs: Date.now() - startedAt,
|
|
268
|
+
};
|
|
269
|
+
}
|