@beignet/cli 0.0.1 → 0.0.4
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 +231 -0
- package/README.md +497 -90
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +72 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +88 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +42 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +136 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +12 -0
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +19 -24
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +37 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +146 -0
- package/dist/db.js.map +1 -0
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +710 -400
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +45 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +2191 -100
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +22 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +370 -38
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +109 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +2225 -333
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates.d.ts +12 -8
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +2144 -385
- package/dist/templates.js.map +1 -1
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +137 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +47 -0
- package/src/create-prompts.ts +182 -0
- package/src/create.ts +32 -28
- package/src/db.ts +222 -0
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1119 -535
- package/src/inspect.ts +3372 -134
- package/src/lib.ts +45 -0
- package/src/lint.ts +533 -45
- package/src/make.ts +3010 -397
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates.ts +2282 -462
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/src/create-bin.ts +0 -11
package/src/outbox.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
DrainOutboxOptions,
|
|
5
|
+
DrainOutboxResult,
|
|
6
|
+
OutboxRegistry,
|
|
7
|
+
} from "@beignet/core/outbox";
|
|
8
|
+
import { createJiti } from "jiti";
|
|
9
|
+
import { loadBeignetConfig, normalizePath } from "./config.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for running one bounded outbox drain pass.
|
|
13
|
+
*/
|
|
14
|
+
export type RunOutboxDrainOptions = {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
modulePath?: string;
|
|
17
|
+
batchSize?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result returned by `beignet outbox drain`.
|
|
22
|
+
*/
|
|
23
|
+
export type RunOutboxDrainResult = {
|
|
24
|
+
schemaVersion: 1;
|
|
25
|
+
cwd: string;
|
|
26
|
+
modulePath: string;
|
|
27
|
+
result: DrainOutboxResult;
|
|
28
|
+
durationMs: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type OutboxDrainLogger = {
|
|
32
|
+
error(message: string, meta?: Record<string, unknown>): unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type OutboxDrainInstrumentation = {
|
|
36
|
+
record(event: {
|
|
37
|
+
type: "custom";
|
|
38
|
+
watcher: string;
|
|
39
|
+
name: string;
|
|
40
|
+
label: string;
|
|
41
|
+
summary: string;
|
|
42
|
+
requestId?: string;
|
|
43
|
+
traceId?: string;
|
|
44
|
+
details: unknown;
|
|
45
|
+
}): unknown;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type OutboxDrainPorts = {
|
|
49
|
+
outbox: DrainOutboxOptions["outbox"];
|
|
50
|
+
eventBus?: DrainOutboxOptions["eventBus"];
|
|
51
|
+
jobs?: DrainOutboxOptions["jobs"];
|
|
52
|
+
logger?: OutboxDrainLogger;
|
|
53
|
+
devtools?: OutboxDrainInstrumentation;
|
|
54
|
+
instrumentation?: OutboxDrainInstrumentation;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type OutboxDrainContext = {
|
|
58
|
+
requestId?: string;
|
|
59
|
+
traceId?: string;
|
|
60
|
+
ports: OutboxDrainPorts;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type OutboxDrainContextArgs = {
|
|
64
|
+
registry: OutboxRegistry;
|
|
65
|
+
batchSize?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type OutboxModule = {
|
|
69
|
+
outboxRegistry?: unknown;
|
|
70
|
+
createOutboxDrainContext?: (args: OutboxDrainContextArgs) => unknown;
|
|
71
|
+
stopOutboxDrainContext?: (
|
|
72
|
+
ctx: OutboxDrainContext,
|
|
73
|
+
args: OutboxDrainContextArgs,
|
|
74
|
+
) => unknown;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run one app-owned outbox drain pass from the app's outbox registry.
|
|
79
|
+
*/
|
|
80
|
+
export async function runOutboxDrain(
|
|
81
|
+
options: RunOutboxDrainOptions = {},
|
|
82
|
+
): Promise<RunOutboxDrainResult> {
|
|
83
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
84
|
+
const config = await loadBeignetConfig(cwd);
|
|
85
|
+
const modulePath = normalizePath(options.modulePath ?? config.paths.outbox);
|
|
86
|
+
const batchSize = options.batchSize;
|
|
87
|
+
if (batchSize !== undefined) assertPositiveInteger("batchSize", batchSize);
|
|
88
|
+
|
|
89
|
+
const startedAt = performance.now();
|
|
90
|
+
const outboxModule = await loadOutboxModule(cwd, modulePath);
|
|
91
|
+
const registry = assertOutboxRegistry(
|
|
92
|
+
outboxModule.outboxRegistry,
|
|
93
|
+
modulePath,
|
|
94
|
+
);
|
|
95
|
+
const contextArgs = { registry, batchSize };
|
|
96
|
+
const rawContext = outboxModule.createOutboxDrainContext
|
|
97
|
+
? await outboxModule.createOutboxDrainContext(contextArgs)
|
|
98
|
+
: undefined;
|
|
99
|
+
const ctx = assertOutboxDrainContext(rawContext, modulePath);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Loaded lazily so CLI commands that never touch the app runtime, such as
|
|
103
|
+
// `beignet lint`, work without a built @beignet/core.
|
|
104
|
+
const { drainOutbox } = await import("@beignet/core/outbox");
|
|
105
|
+
const result = await drainOutbox({
|
|
106
|
+
outbox: ctx.ports.outbox,
|
|
107
|
+
registry,
|
|
108
|
+
eventBus: ctx.ports.eventBus,
|
|
109
|
+
jobs: ctx.ports.jobs,
|
|
110
|
+
batchSize,
|
|
111
|
+
instrumentation: ctx.ports.instrumentation ?? ctx.ports.devtools,
|
|
112
|
+
instrumentationContext: {
|
|
113
|
+
requestId: ctx.requestId,
|
|
114
|
+
traceId: ctx.traceId,
|
|
115
|
+
},
|
|
116
|
+
onError(error, message) {
|
|
117
|
+
ctx.ports.logger?.error("Outbox delivery failed", {
|
|
118
|
+
error,
|
|
119
|
+
outboxMessageId: message.id,
|
|
120
|
+
kind: message.kind,
|
|
121
|
+
name: message.name,
|
|
122
|
+
attempts: message.attempts,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await recordOutboxDrain(ctx, result);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
schemaVersion: 1,
|
|
131
|
+
cwd,
|
|
132
|
+
modulePath,
|
|
133
|
+
result,
|
|
134
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
135
|
+
};
|
|
136
|
+
} finally {
|
|
137
|
+
await outboxModule.stopOutboxDrainContext?.(ctx, contextArgs);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function loadOutboxModule(
|
|
142
|
+
cwd: string,
|
|
143
|
+
modulePath: string,
|
|
144
|
+
): Promise<OutboxModule> {
|
|
145
|
+
const absolutePath = path.join(cwd, modulePath);
|
|
146
|
+
try {
|
|
147
|
+
await stat(absolutePath);
|
|
148
|
+
} catch {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Could not find app outbox registry at ${modulePath}. Create server/outbox.ts or configure paths.outbox in beignet.config.ts.`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tsconfigPath = path.join(cwd, "tsconfig.json");
|
|
155
|
+
const jiti = createJiti(absolutePath, {
|
|
156
|
+
tsconfigPaths: (await fileExists(tsconfigPath)) ? tsconfigPath : false,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return (await jiti.import(absolutePath)) as OutboxModule;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function assertOutboxRegistry(
|
|
163
|
+
registry: unknown,
|
|
164
|
+
modulePath: string,
|
|
165
|
+
): OutboxRegistry {
|
|
166
|
+
if (!isRecord(registry)) {
|
|
167
|
+
throw new Error(`${modulePath} must export an outboxRegistry.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const events = registry.events;
|
|
171
|
+
const jobs = registry.jobs;
|
|
172
|
+
if (!(events instanceof Map) || !(jobs instanceof Map)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`${modulePath} outboxRegistry must be created with defineOutboxRegistry(...).`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { events, jobs } as unknown as OutboxRegistry;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertOutboxDrainContext(
|
|
182
|
+
ctx: unknown,
|
|
183
|
+
modulePath: string,
|
|
184
|
+
): OutboxDrainContext {
|
|
185
|
+
if (!isRecord(ctx) || !isRecord(ctx.ports)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`${modulePath} must export createOutboxDrainContext that returns an app context with ports.outbox.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!isOutboxPort(ctx.ports.outbox)) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`${modulePath} createOutboxDrainContext must return ports.outbox.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return ctx as OutboxDrainContext;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isOutboxPort(value: unknown): value is DrainOutboxOptions["outbox"] {
|
|
201
|
+
return (
|
|
202
|
+
isRecord(value) &&
|
|
203
|
+
typeof value.enqueue === "function" &&
|
|
204
|
+
typeof value.claimBatch === "function" &&
|
|
205
|
+
typeof value.markDelivered === "function" &&
|
|
206
|
+
typeof value.markFailed === "function"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function recordOutboxDrain(
|
|
211
|
+
ctx: OutboxDrainContext,
|
|
212
|
+
result: DrainOutboxResult,
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
try {
|
|
215
|
+
await (ctx.ports.instrumentation ?? ctx.ports.devtools)?.record({
|
|
216
|
+
type: "custom",
|
|
217
|
+
watcher: "outbox",
|
|
218
|
+
name: "outbox.drain",
|
|
219
|
+
label: "Outbox drain",
|
|
220
|
+
summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered`,
|
|
221
|
+
requestId: ctx.requestId,
|
|
222
|
+
traceId: ctx.traceId,
|
|
223
|
+
details: result,
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
ctx.ports.logger?.error("Outbox drain instrumentation recording failed", {
|
|
227
|
+
error,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function assertPositiveInteger(name: string, value: number): void {
|
|
233
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
234
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
239
|
+
return typeof value === "object" && value !== null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
243
|
+
try {
|
|
244
|
+
await readFile(filePath, "utf8");
|
|
245
|
+
return true;
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
package/src/schedule.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
ScheduleDef,
|
|
5
|
+
ScheduleInstrumentation,
|
|
6
|
+
ScheduleRunOptions,
|
|
7
|
+
StandardSchema,
|
|
8
|
+
} from "@beignet/core/schedules";
|
|
9
|
+
import { createJiti } from "jiti";
|
|
10
|
+
import { loadBeignetConfig, normalizePath } from "./config.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for running one app-owned schedule.
|
|
14
|
+
*/
|
|
15
|
+
export type RunAppScheduleOptions = {
|
|
16
|
+
name: string;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
modulePath?: string;
|
|
19
|
+
payload?: string | Record<string, unknown>;
|
|
20
|
+
id?: string;
|
|
21
|
+
attempt?: number;
|
|
22
|
+
scheduledAt?: string;
|
|
23
|
+
triggeredAt?: string;
|
|
24
|
+
source?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Result returned by `beignet schedule run`.
|
|
29
|
+
*/
|
|
30
|
+
export type RunAppScheduleResult = {
|
|
31
|
+
schemaVersion: 1;
|
|
32
|
+
name: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
modulePath: string;
|
|
35
|
+
run: {
|
|
36
|
+
id?: string;
|
|
37
|
+
attempt?: number;
|
|
38
|
+
scheduledAt?: string;
|
|
39
|
+
triggeredAt?: string;
|
|
40
|
+
source: string;
|
|
41
|
+
};
|
|
42
|
+
durationMs: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type AnyScheduleDef = ScheduleDef<string, StandardSchema, unknown>;
|
|
46
|
+
|
|
47
|
+
type ScheduleLogger = {
|
|
48
|
+
error(message: string, meta?: Record<string, unknown>): unknown;
|
|
49
|
+
warn?(message: string, meta?: Record<string, unknown>): unknown;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type ScheduleContext = {
|
|
53
|
+
requestId?: string;
|
|
54
|
+
traceId?: string;
|
|
55
|
+
ports?: {
|
|
56
|
+
logger?: ScheduleLogger;
|
|
57
|
+
devtools?: ScheduleInstrumentation;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type ScheduleContextArgs = {
|
|
62
|
+
schedule: AnyScheduleDef;
|
|
63
|
+
scheduleName: string;
|
|
64
|
+
run: ScheduleRunOptions<unknown>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type ScheduleModule = {
|
|
68
|
+
schedules?: readonly unknown[];
|
|
69
|
+
createScheduleContext?: (args: ScheduleContextArgs) => unknown;
|
|
70
|
+
stopScheduleContext?: (ctx: unknown, args: ScheduleContextArgs) => unknown;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run an app-owned schedule from the app's schedule registry.
|
|
75
|
+
*/
|
|
76
|
+
export async function runAppSchedule(
|
|
77
|
+
options: RunAppScheduleOptions,
|
|
78
|
+
): Promise<RunAppScheduleResult> {
|
|
79
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
80
|
+
const config = await loadBeignetConfig(cwd);
|
|
81
|
+
const modulePath = normalizePath(
|
|
82
|
+
options.modulePath ?? config.paths.schedules,
|
|
83
|
+
);
|
|
84
|
+
const runOptions = buildScheduleRunOptions(options);
|
|
85
|
+
const startedAt = performance.now();
|
|
86
|
+
const scheduleModule = await loadScheduleModule(cwd, modulePath);
|
|
87
|
+
const schedule = findSchedule(
|
|
88
|
+
scheduleModule.schedules,
|
|
89
|
+
options.name,
|
|
90
|
+
modulePath,
|
|
91
|
+
);
|
|
92
|
+
const contextArgs = {
|
|
93
|
+
schedule,
|
|
94
|
+
scheduleName: schedule.name,
|
|
95
|
+
run: runOptions,
|
|
96
|
+
};
|
|
97
|
+
const ctx = scheduleModule.createScheduleContext
|
|
98
|
+
? await scheduleModule.createScheduleContext(contextArgs)
|
|
99
|
+
: {};
|
|
100
|
+
const scheduleContext = toScheduleContext(ctx);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Loaded lazily so CLI commands that never touch the app runtime, such as
|
|
104
|
+
// `beignet lint`, work without a built @beignet/core.
|
|
105
|
+
const { createInlineScheduleRunner } = await import(
|
|
106
|
+
"@beignet/core/schedules"
|
|
107
|
+
);
|
|
108
|
+
const runner = createInlineScheduleRunner<unknown>({
|
|
109
|
+
ctx,
|
|
110
|
+
instrumentation: scheduleContext.ports?.devtools,
|
|
111
|
+
instrumentationContext: {
|
|
112
|
+
requestId: scheduleContext.requestId,
|
|
113
|
+
traceId: scheduleContext.traceId,
|
|
114
|
+
},
|
|
115
|
+
onError({ error }) {
|
|
116
|
+
scheduleContext.ports?.logger?.error("Schedule failed", {
|
|
117
|
+
error,
|
|
118
|
+
scheduleName: schedule.name,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
onHookError({ error, hook }) {
|
|
122
|
+
scheduleContext.ports?.logger?.warn?.(
|
|
123
|
+
"Schedule lifecycle hook failed",
|
|
124
|
+
{
|
|
125
|
+
error,
|
|
126
|
+
hook,
|
|
127
|
+
scheduleName: schedule.name,
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await runner.run(schedule, runOptions);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
schemaVersion: 1,
|
|
137
|
+
name: schedule.name,
|
|
138
|
+
cwd,
|
|
139
|
+
modulePath,
|
|
140
|
+
run: {
|
|
141
|
+
id: runOptions.id,
|
|
142
|
+
attempt: runOptions.attempt,
|
|
143
|
+
scheduledAt: stringifyDateInput(runOptions.scheduledAt),
|
|
144
|
+
triggeredAt: stringifyDateInput(runOptions.triggeredAt),
|
|
145
|
+
source: runOptions.source ?? "beignet-cli",
|
|
146
|
+
},
|
|
147
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
148
|
+
};
|
|
149
|
+
} finally {
|
|
150
|
+
await scheduleModule.stopScheduleContext?.(ctx, contextArgs);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function loadScheduleModule(
|
|
155
|
+
cwd: string,
|
|
156
|
+
modulePath: string,
|
|
157
|
+
): Promise<ScheduleModule> {
|
|
158
|
+
const absolutePath = path.join(cwd, modulePath);
|
|
159
|
+
try {
|
|
160
|
+
await stat(absolutePath);
|
|
161
|
+
} catch {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Could not find app schedule registry at ${modulePath}. Create server/schedules.ts or configure paths.schedules in beignet.config.ts.`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const tsconfigPath = path.join(cwd, "tsconfig.json");
|
|
168
|
+
const jiti = createJiti(absolutePath, {
|
|
169
|
+
tsconfigPaths: (await fileExists(tsconfigPath)) ? tsconfigPath : false,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return (await jiti.import(absolutePath)) as ScheduleModule;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildScheduleRunOptions(
|
|
176
|
+
options: RunAppScheduleOptions,
|
|
177
|
+
): ScheduleRunOptions<unknown> {
|
|
178
|
+
const runOptions: ScheduleRunOptions<unknown> = {
|
|
179
|
+
source: options.source ?? "beignet-cli",
|
|
180
|
+
triggeredAt: options.triggeredAt ?? new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (options.payload !== undefined) {
|
|
184
|
+
runOptions.payload = parseSchedulePayloadFlag(options.payload);
|
|
185
|
+
}
|
|
186
|
+
if (options.id !== undefined) runOptions.id = options.id;
|
|
187
|
+
if (options.attempt !== undefined) runOptions.attempt = options.attempt;
|
|
188
|
+
if (options.scheduledAt !== undefined)
|
|
189
|
+
runOptions.scheduledAt = options.scheduledAt;
|
|
190
|
+
|
|
191
|
+
return runOptions;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseSchedulePayloadFlag(
|
|
195
|
+
input: NonNullable<RunAppScheduleOptions["payload"]>,
|
|
196
|
+
): unknown {
|
|
197
|
+
if (typeof input !== "string") return input;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
return JSON.parse(input) as unknown;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Invalid --payload JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function findSchedule(
|
|
209
|
+
schedules: readonly unknown[] | undefined,
|
|
210
|
+
name: string,
|
|
211
|
+
modulePath: string,
|
|
212
|
+
): AnyScheduleDef {
|
|
213
|
+
if (!schedules) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Schedule registry ${modulePath} must export a schedules array.`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const schedule = schedules.find(
|
|
220
|
+
(candidate): candidate is AnyScheduleDef =>
|
|
221
|
+
typeof candidate === "object" &&
|
|
222
|
+
candidate !== null &&
|
|
223
|
+
(candidate as AnyScheduleDef).kind === "schedule" &&
|
|
224
|
+
(candidate as AnyScheduleDef).name === name &&
|
|
225
|
+
typeof (candidate as AnyScheduleDef).handle === "function",
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (!schedule) {
|
|
229
|
+
const available = schedules
|
|
230
|
+
.filter(
|
|
231
|
+
(candidate): candidate is AnyScheduleDef =>
|
|
232
|
+
typeof candidate === "object" &&
|
|
233
|
+
candidate !== null &&
|
|
234
|
+
(candidate as AnyScheduleDef).kind === "schedule" &&
|
|
235
|
+
typeof (candidate as AnyScheduleDef).name === "string",
|
|
236
|
+
)
|
|
237
|
+
.map((candidate) => candidate.name)
|
|
238
|
+
.sort();
|
|
239
|
+
throw new Error(
|
|
240
|
+
available.length > 0
|
|
241
|
+
? `Unknown schedule "${name}". Available schedules: ${available.join(", ")}.`
|
|
242
|
+
: `Unknown schedule "${name}". No schedules were registered in ${modulePath}.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return schedule;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function toScheduleContext(ctx: unknown): ScheduleContext {
|
|
250
|
+
return isRecord(ctx) ? (ctx as ScheduleContext) : {};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function stringifyDateInput(
|
|
254
|
+
value: ScheduleRunOptions<unknown>["scheduledAt"],
|
|
255
|
+
): string | undefined {
|
|
256
|
+
if (value === undefined) return undefined;
|
|
257
|
+
if (value instanceof Date) return value.toISOString();
|
|
258
|
+
return String(value);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
262
|
+
return typeof value === "object" && value !== null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
266
|
+
try {
|
|
267
|
+
await readFile(filePath, "utf8");
|
|
268
|
+
return true;
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/task.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { TaskDef } from "@beignet/core/tasks";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
import { loadBeignetConfig, normalizePath } from "./config.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for running an app-owned operational task.
|
|
9
|
+
*/
|
|
10
|
+
export type RunAppTaskOptions = {
|
|
11
|
+
name: string;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
input?: string | Record<string, unknown>;
|
|
14
|
+
modulePath?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Result returned by `beignet task run`.
|
|
19
|
+
*/
|
|
20
|
+
export type RunAppTaskResult = {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
name: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
modulePath: string;
|
|
25
|
+
input: unknown;
|
|
26
|
+
output: unknown;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type TaskContextArgs = {
|
|
31
|
+
task: TaskDef;
|
|
32
|
+
taskName: string;
|
|
33
|
+
input: unknown;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type TaskModule = {
|
|
37
|
+
tasks?: readonly unknown[];
|
|
38
|
+
createTaskContext?: (args: TaskContextArgs) => unknown;
|
|
39
|
+
stopTaskContext?: (ctx: unknown, args: TaskContextArgs) => unknown;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run an app-owned operational task from the app's task registry.
|
|
44
|
+
*/
|
|
45
|
+
export async function runAppTask(
|
|
46
|
+
options: RunAppTaskOptions,
|
|
47
|
+
): Promise<RunAppTaskResult> {
|
|
48
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
49
|
+
const config = await loadBeignetConfig(cwd);
|
|
50
|
+
const modulePath = normalizePath(options.modulePath ?? config.paths.tasks);
|
|
51
|
+
const rawInput = parseTaskInputFlag(options.input);
|
|
52
|
+
const startedAt = performance.now();
|
|
53
|
+
const taskModule = await loadTaskModule(cwd, modulePath);
|
|
54
|
+
const task = findTask(taskModule.tasks, options.name, modulePath);
|
|
55
|
+
// Loaded lazily so CLI commands that never touch the app runtime, such as
|
|
56
|
+
// `beignet lint`, work without a built @beignet/core.
|
|
57
|
+
const { parseTaskInput } = await import("@beignet/core/tasks");
|
|
58
|
+
const parsedInput = await parseTaskInput(task, rawInput);
|
|
59
|
+
const contextArgs = {
|
|
60
|
+
task,
|
|
61
|
+
taskName: task.name,
|
|
62
|
+
input: parsedInput,
|
|
63
|
+
};
|
|
64
|
+
const ctx = taskModule.createTaskContext
|
|
65
|
+
? await taskModule.createTaskContext(contextArgs)
|
|
66
|
+
: {};
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const output = await task.handle({
|
|
70
|
+
task,
|
|
71
|
+
input: parsedInput,
|
|
72
|
+
ctx,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
schemaVersion: 1,
|
|
77
|
+
name: task.name,
|
|
78
|
+
cwd,
|
|
79
|
+
modulePath,
|
|
80
|
+
input: parsedInput,
|
|
81
|
+
output,
|
|
82
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
83
|
+
};
|
|
84
|
+
} finally {
|
|
85
|
+
await taskModule.stopTaskContext?.(ctx, contextArgs);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseTaskInputFlag(input: RunAppTaskOptions["input"]): unknown {
|
|
90
|
+
if (input === undefined) return {};
|
|
91
|
+
if (typeof input !== "string") return input;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(input) as unknown;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Invalid --input JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadTaskModule(
|
|
103
|
+
cwd: string,
|
|
104
|
+
modulePath: string,
|
|
105
|
+
): Promise<TaskModule> {
|
|
106
|
+
const absolutePath = path.join(cwd, modulePath);
|
|
107
|
+
try {
|
|
108
|
+
await stat(absolutePath);
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Could not find app task registry at ${modulePath}. Create it with beignet make task <feature>/<name> or configure paths.tasks in beignet.config.ts.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const tsconfigPath = path.join(cwd, "tsconfig.json");
|
|
116
|
+
const jiti = createJiti(absolutePath, {
|
|
117
|
+
tsconfigPaths: (await fileExists(tsconfigPath)) ? tsconfigPath : false,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return (await jiti.import(absolutePath)) as TaskModule;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function findTask(
|
|
124
|
+
tasks: readonly unknown[] | undefined,
|
|
125
|
+
name: string,
|
|
126
|
+
modulePath: string,
|
|
127
|
+
): TaskDef {
|
|
128
|
+
if (!tasks) {
|
|
129
|
+
throw new Error(`Task registry ${modulePath} must export a tasks array.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const task = tasks.find(
|
|
133
|
+
(candidate): candidate is TaskDef =>
|
|
134
|
+
typeof candidate === "object" &&
|
|
135
|
+
candidate !== null &&
|
|
136
|
+
(candidate as TaskDef).kind === "task" &&
|
|
137
|
+
(candidate as TaskDef).name === name &&
|
|
138
|
+
typeof (candidate as TaskDef).handle === "function",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (!task) {
|
|
142
|
+
const available = tasks
|
|
143
|
+
.filter(
|
|
144
|
+
(candidate): candidate is TaskDef =>
|
|
145
|
+
typeof candidate === "object" &&
|
|
146
|
+
candidate !== null &&
|
|
147
|
+
(candidate as TaskDef).kind === "task" &&
|
|
148
|
+
typeof (candidate as TaskDef).name === "string",
|
|
149
|
+
)
|
|
150
|
+
.map((candidate) => candidate.name)
|
|
151
|
+
.sort();
|
|
152
|
+
throw new Error(
|
|
153
|
+
available.length > 0
|
|
154
|
+
? `Unknown task "${name}". Available tasks: ${available.join(", ")}.`
|
|
155
|
+
: `Unknown task "${name}". No tasks were registered in ${modulePath}.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return task;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
163
|
+
try {
|
|
164
|
+
await readFile(filePath, "utf8");
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|