@beignet/cli 0.0.32 → 0.0.34
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 +60 -0
- package/README.md +29 -19
- package/dist/choices.d.ts +3 -3
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +2 -0
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/db.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +279 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +800 -60
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +8 -6
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +61 -32
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +11 -2
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +45 -11
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.d.ts.map +1 -1
- package/dist/make/tenancy.js +9 -7
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +134 -60
- 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.js +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +91 -62
- package/dist/provider-add.js.map +1 -1
- package/dist/registry-edits.js +2 -1
- package/dist/registry-edits.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.js +6 -6
- 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 +17 -10
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +3 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +11 -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 +4 -4
- package/package.json +3 -2
- package/skills/app-structure/SKILL.md +14 -4
- package/src/choices.ts +3 -0
- package/src/config.ts +2 -0
- package/src/db.ts +1 -1
- package/src/index.ts +437 -1
- package/src/inspect.ts +1248 -156
- package/src/make/inbox.ts +8 -6
- package/src/make/payments.ts +61 -32
- package/src/make/shared.ts +79 -12
- package/src/make/tenancy.ts +9 -7
- package/src/make.ts +177 -59
- package/src/outbox.ts +363 -0
- package/src/preflight.ts +1 -1
- package/src/provider-add.ts +92 -62
- package/src/registry-edits.ts +2 -1
- package/src/task.ts +2 -3
- package/src/templates/agents.ts +6 -3
- package/src/templates/base.ts +6 -6
- package/src/templates/index.ts +2 -0
- package/src/templates/server.ts +17 -10
- package/src/templates/shared.ts +13 -1
- package/src/templates/testing.ts +545 -0
- package/src/templates/todos.ts +4 -4
- 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
|
@@ -175,7 +175,7 @@ export const todoPolicy = definePolicy({
|
|
|
175
175
|
});
|
|
176
176
|
`,
|
|
177
177
|
routes: `import "@beignet/core/server-only";
|
|
178
|
-
import { defineRouteGroup } from "@beignet/
|
|
178
|
+
import { defineRouteGroup } from "@beignet/core/server";
|
|
179
179
|
import type { AppContext } from "@/app-context";
|
|
180
180
|
import {
|
|
181
181
|
createTodo,
|
|
@@ -359,9 +359,9 @@ 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
|
-
import { createTestUserActor } from "@beignet/core/
|
|
364
|
+
import { createTestUserActor } from "@beignet/core/testing";
|
|
365
365
|
import {
|
|
366
366
|
createTestContextFactory,
|
|
367
367
|
createTestPorts,
|
|
@@ -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.34",
|
|
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.34",
|
|
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 sync, 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."
|
|
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,18 @@ 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
|
+
- queued notifications -> `server/notifications.ts`, plus the delivery job in
|
|
68
|
+
every job worker or outbox registry that can receive it
|
|
67
69
|
- seeds -> the app-owned seed entrypoint, usually `server/seed.ts`
|
|
68
70
|
|
|
71
|
+
When an app opts into runtime integrity, keep `server/runtime-integrity.ts`
|
|
72
|
+
aligned with those central registries. The boot check is pure and
|
|
73
|
+
serverless-safe: it compares declared listeners, schedules, tasks, and outbox
|
|
74
|
+
artifacts against runtime registries before providers start.
|
|
75
|
+
|
|
69
76
|
Run `beignet doctor --strict` after hand edits. Use `beignet doctor --fix` for
|
|
70
|
-
low-risk route-group, schedule, task, and
|
|
71
|
-
drift remains manual.
|
|
77
|
+
low-risk route-group, schedule, task, outbox, and listener registry repair.
|
|
72
78
|
|
|
73
79
|
## Path Config
|
|
74
80
|
|
|
@@ -113,6 +119,10 @@ root at `src/client/index.ts`.
|
|
|
113
119
|
- route-owned error catalog drift
|
|
114
120
|
- misplaced workflow artifacts
|
|
115
121
|
- unregistered schedules, tasks, outbox events/jobs, and listeners
|
|
122
|
+
- runtime manifest and registry drift for apps that opt into boot integrity
|
|
123
|
+
- tenant-scoped Drizzle repository drift, including generated `TenantScope`
|
|
124
|
+
boundaries, raw `tenantId` app-facing repository methods, and scoped
|
|
125
|
+
`tenantScopeId(scope)` predicates for tenant/workspace tables
|
|
116
126
|
|
|
117
127
|
Fix structure or config instead of suppressing diagnostics.
|
|
118
128
|
|
package/src/choices.ts
CHANGED
|
@@ -38,6 +38,7 @@ export type ProviderPresetName =
|
|
|
38
38
|
| "sentry"
|
|
39
39
|
| "upstash-rate-limit"
|
|
40
40
|
| "redis-cache"
|
|
41
|
+
| "event-bus-redis"
|
|
41
42
|
| "redis-locks"
|
|
42
43
|
| "s3-storage"
|
|
43
44
|
| "vercel-blob-storage";
|
|
@@ -91,6 +92,7 @@ export const providerPresetChoices = [
|
|
|
91
92
|
"sentry",
|
|
92
93
|
"upstash-rate-limit",
|
|
93
94
|
"redis-cache",
|
|
95
|
+
"event-bus-redis",
|
|
94
96
|
"redis-locks",
|
|
95
97
|
"s3-storage",
|
|
96
98
|
"vercel-blob-storage",
|
|
@@ -117,6 +119,7 @@ export const createIntegrationChoices = [
|
|
|
117
119
|
"mail-smtp",
|
|
118
120
|
"payments-stripe",
|
|
119
121
|
"redis-cache",
|
|
122
|
+
"event-bus-redis",
|
|
120
123
|
"redis-locks",
|
|
121
124
|
"s3-storage",
|
|
122
125
|
"search-meilisearch",
|
package/src/config.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type BeignetPaths = {
|
|
|
16
16
|
contracts: string;
|
|
17
17
|
routes: string;
|
|
18
18
|
server: string;
|
|
19
|
+
listeners: string;
|
|
19
20
|
openapiRoute: string;
|
|
20
21
|
tasks: string;
|
|
21
22
|
outbox: string;
|
|
@@ -85,6 +86,7 @@ export const defaultBeignetConfig: ResolvedBeignetConfig = {
|
|
|
85
86
|
contracts: "features",
|
|
86
87
|
routes: "app/api",
|
|
87
88
|
server: "server/index.ts",
|
|
89
|
+
listeners: "server/listeners.ts",
|
|
88
90
|
openapiRoute: "app/api/openapi/route.ts",
|
|
89
91
|
tasks: "server/tasks.ts",
|
|
90
92
|
outbox: "server/outbox.ts",
|
package/src/db.ts
CHANGED
|
@@ -464,7 +464,7 @@ function missingDatabaseScriptMessage(
|
|
|
464
464
|
|
|
465
465
|
const entrypoint =
|
|
466
466
|
command === "seed" ? "server/seed.ts" : "infra/db/reset.ts";
|
|
467
|
-
return `Missing package.json script "${script}". Standard Drizzle apps use "${script}": "
|
|
467
|
+
return `Missing package.json script "${script}". Standard Drizzle apps use "${script}": "tsx ${entrypoint}" so beignet db ${command} can run the app-owned ${command} entrypoint.`;
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
async function readPackageJson(cwd: string): Promise<PackageJson> {
|