@wooksjs/event-wf 0.7.7 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,7 @@
1
- import { createEventContext, current, defineEventKind, key, slot, useLogger, useRouteParams } from "@wooksjs/event-core";
1
+ import { createEventContext, current, defineEventKind, defineWook, key, slot, useLogger, useRouteParams } from "@wooksjs/event-core";
2
+ import { useCookies, useResponse, useUrlParams } from "@wooksjs/event-http";
3
+ import { useBody } from "@wooksjs/http-body";
4
+ import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from "node:crypto";
2
5
  import { StepRetriableError, Workflow, createStep } from "@prostojs/wf";
3
6
  import { WooksAdapterBase } from "wooks";
4
7
 
@@ -23,17 +26,14 @@ const resumeKey = key("wf.resume");
23
26
  * const stepInput = input<MyInput>()
24
27
  * ```
25
28
  */
26
- function useWfState() {
27
- const c = current();
28
- return {
29
- ctx: () => c.get(wfKind.keys.inputContext),
30
- input: () => c.get(wfKind.keys.input),
31
- schemaId: c.get(wfKind.keys.schemaId),
32
- stepId: () => c.get(wfKind.keys.stepId),
33
- indexes: () => c.get(wfKind.keys.indexes),
34
- resume: c.get(resumeKey)
35
- };
36
- }
29
+ const useWfState = defineWook((c) => ({
30
+ ctx: () => c.get(wfKind.keys.inputContext),
31
+ input: () => c.get(wfKind.keys.input),
32
+ schemaId: c.get(wfKind.keys.schemaId),
33
+ stepId: () => c.get(wfKind.keys.stepId),
34
+ indexes: () => c.get(wfKind.keys.indexes),
35
+ resume: c.get(resumeKey)
36
+ }));
37
37
 
38
38
  //#endregion
39
39
  //#region packages/event-wf/src/event-wf.ts
@@ -52,6 +52,446 @@ function resumeWfContext(options, seeds, fn) {
52
52
  });
53
53
  }
54
54
 
55
+ //#endregion
56
+ //#region packages/event-wf/src/outlets/outlet-context.ts
57
+ /** Registered outlet handlers, keyed by name */
58
+ const outletsRegistryKey = key("wf.outlets.registry");
59
+ /** Active state strategy for current request */
60
+ const stateStrategyKey = key("wf.outlets.stateStrategy");
61
+ /** Finished response set by workflow steps */
62
+ const wfFinishedKey = key("wf.outlets.finished");
63
+
64
+ //#endregion
65
+ //#region packages/event-wf/src/outlets/use-wf-outlet.ts
66
+ /**
67
+ * Composable for accessing outlet infrastructure from within workflow steps.
68
+ *
69
+ * Most steps don't need this — they just return `outletHttp(form)` or
70
+ * `outletEmail(to, template)`. This composable is for advanced cases
71
+ * where steps need to inspect or modify outlet state directly.
72
+ */
73
+ const useWfOutlet = defineWook((ctx) => ({
74
+ getStateStrategy: () => ctx.get(stateStrategyKey),
75
+ getOutlets: () => ctx.get(outletsRegistryKey),
76
+ getOutlet: (name) => ctx.get(outletsRegistryKey)?.get(name) ?? null
77
+ }));
78
+
79
+ //#endregion
80
+ //#region packages/event-wf/src/outlets/use-wf-finished.ts
81
+ /**
82
+ * Composable to set the completion response for a finished workflow.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * // Redirect after login
87
+ * useWfFinished().set({ type: 'redirect', value: '/dashboard' })
88
+ *
89
+ * // Return data
90
+ * useWfFinished().set({ type: 'data', value: { success: true } })
91
+ * ```
92
+ */
93
+ const useWfFinished = defineWook((ctx) => ({
94
+ set: (response) => ctx.set(wfFinishedKey, response),
95
+ get: () => ctx.has(wfFinishedKey) ? ctx.get(wfFinishedKey) : void 0
96
+ }));
97
+
98
+ //#endregion
99
+ //#region packages/event-wf/src/outlets/trigger.ts
100
+ const DEFAULT_CONSUME_TOKEN = { email: true };
101
+ /**
102
+ * Handle an HTTP request that starts or resumes a workflow.
103
+ *
104
+ * Reads wfs (state token) and wfid (workflow ID) from request body, query params,
105
+ * or cookies — configurable via `config.token`. On workflow pause, persists state
106
+ * and dispatches to the named outlet. On finish, returns the finished response.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * // In a wooks HTTP handler:
111
+ * app.post('/workflow', () => handleWfOutletRequest(config, deps))
112
+ *
113
+ * // Better — use createOutletHandler():
114
+ * const handle = createOutletHandler(wfApp)
115
+ * app.post('/workflow', () => handle(config))
116
+ * ```
117
+ */
118
+ async function handleWfOutletRequest(config, deps) {
119
+ const tok = config.token ?? {};
120
+ const tokenName = tok.name ?? "wfs";
121
+ const tokenRead = tok.read ?? [
122
+ "body",
123
+ "query",
124
+ "cookie"
125
+ ];
126
+ const tokenWrite = tok.write ?? "body";
127
+ const wfidName = config.wfidName ?? "wfid";
128
+ const ctx = current();
129
+ const registry = new Map(config.outlets.map((o) => [o.name, o]));
130
+ ctx.set(outletsRegistryKey, registry);
131
+ ctx.set(wfFinishedKey, void 0);
132
+ const { parseBody } = useBody();
133
+ const { params } = useUrlParams();
134
+ const { getCookie } = useCookies();
135
+ const response = useResponse();
136
+ const body = await parseBody().catch(() => void 0);
137
+ const queryParams = params();
138
+ let token;
139
+ for (const source of tokenRead) {
140
+ if (source === "body") token = body?.[tokenName];
141
+ else if (source === "query") token = queryParams.get(tokenName) ?? void 0;
142
+ else if (source === "cookie") token = getCookie(tokenName) ?? void 0;
143
+ if (token) break;
144
+ }
145
+ const wfid = body?.[wfidName] ?? queryParams.get(wfidName) ?? void 0;
146
+ const input = body?.input;
147
+ const resolveStrategy = (id) => typeof config.state === "function" ? config.state(id) : config.state;
148
+ const shouldConsume = (outletName) => {
149
+ if (typeof tok.consume === "boolean") return tok.consume;
150
+ return (tok.consume ?? DEFAULT_CONSUME_TOKEN)[outletName] ?? false;
151
+ };
152
+ let output;
153
+ if (token) {
154
+ const strategy = resolveStrategy(wfid ?? "");
155
+ ctx.set(stateStrategyKey, strategy);
156
+ const state = await strategy.retrieve(token);
157
+ if (!state) return {
158
+ error: "Invalid or expired workflow state",
159
+ status: 400
160
+ };
161
+ if (state.schemaId !== (wfid ?? "")) {
162
+ const realStrategy = resolveStrategy(state.schemaId);
163
+ ctx.set(stateStrategyKey, realStrategy);
164
+ }
165
+ const outletName = state.meta?.outlet;
166
+ if (outletName && shouldConsume(outletName)) await strategy.consume(token);
167
+ output = await deps.resume(state, {
168
+ input,
169
+ eventContext: ctx
170
+ });
171
+ } else if (wfid) {
172
+ if (config.allow?.length && !config.allow.includes(wfid)) return {
173
+ error: `Workflow '${wfid}' is not allowed`,
174
+ status: 403
175
+ };
176
+ if (config.block?.includes(wfid)) return {
177
+ error: `Workflow '${wfid}' is blocked`,
178
+ status: 403
179
+ };
180
+ const strategy = resolveStrategy(wfid);
181
+ ctx.set(stateStrategyKey, strategy);
182
+ const initialContext = config.initialContext ? config.initialContext(body, wfid) : {};
183
+ output = await deps.start(wfid, initialContext, {
184
+ input,
185
+ eventContext: ctx
186
+ });
187
+ } else return {
188
+ error: "Missing wfs (state token) or wfid (workflow ID)",
189
+ status: 400
190
+ };
191
+ if (output.finished) {
192
+ if (config.onFinished) return config.onFinished({
193
+ context: output.state.context,
194
+ schemaId: output.state.schemaId
195
+ });
196
+ const finished = ctx.get(wfFinishedKey);
197
+ if (finished?.cookies) for (const [name, cookie] of Object.entries(finished.cookies)) response.setCookie(name, cookie.value, cookie.options);
198
+ if (finished?.type === "redirect") {
199
+ response.setHeader("location", finished.value);
200
+ return { status: finished.status ?? 302 };
201
+ }
202
+ if (finished) return finished.value;
203
+ return { finished: true };
204
+ }
205
+ if (output.inputRequired) {
206
+ const outletReq = output.inputRequired;
207
+ const outletHandler = registry.get(outletReq.outlet);
208
+ if (!outletHandler) return {
209
+ error: `Unknown outlet: '${outletReq.outlet}'`,
210
+ status: 500
211
+ };
212
+ const strategy = ctx.get(stateStrategyKey);
213
+ const stateWithMeta = {
214
+ ...output.state,
215
+ meta: { outlet: outletReq.outlet }
216
+ };
217
+ const newToken = await strategy.persist(stateWithMeta, output.expires ? { ttl: output.expires - Date.now() } : void 0);
218
+ if (tokenWrite === "cookie") response.setCookie(tokenName, newToken, {
219
+ httpOnly: true,
220
+ sameSite: "Strict",
221
+ path: "/"
222
+ });
223
+ const result = await outletHandler.deliver(outletReq, newToken);
224
+ if (tokenWrite === "body" && result?.response && typeof result.response === "object") return {
225
+ ...result.response,
226
+ [tokenName]: newToken
227
+ };
228
+ return result?.response ?? { waiting: true };
229
+ }
230
+ if (output.error) return {
231
+ error: output.error.message,
232
+ errorList: output.errorList
233
+ };
234
+ return { error: "Unexpected workflow state" };
235
+ }
236
+
237
+ //#endregion
238
+ //#region packages/event-wf/src/outlets/create-outlet.ts
239
+ /**
240
+ * Creates an HTTP outlet that passes through the outlet payload as
241
+ * the HTTP response body. This is the most common outlet — it returns
242
+ * forms, prompts, or data to the client.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const httpOutlet = createHttpOutlet()
247
+ * // Step does: return outletHttp({ fields: ['email', 'password'] })
248
+ * // Client receives: { fields: ['email', 'password'] }
249
+ * ```
250
+ */
251
+ function createHttpOutlet(opts) {
252
+ return {
253
+ name: "http",
254
+ async deliver(request, _token) {
255
+ const body = opts?.transform ? opts.transform(request.payload, request.context) : typeof request.payload === "object" && request.payload !== null ? {
256
+ ...request.payload,
257
+ ...request.context
258
+ } : request.payload;
259
+ return { response: body };
260
+ }
261
+ };
262
+ }
263
+ /**
264
+ * Creates an email outlet that delegates to a user-provided send function.
265
+ * The send function receives the target, template, context, and the state
266
+ * token (for embedding in magic links / verification URLs).
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * const emailOutlet = createEmailOutlet(async (opts) => {
271
+ * await mailer.send({
272
+ * to: opts.target,
273
+ * template: opts.template,
274
+ * data: { ...opts.context, verifyUrl: `/verify?wfs=${opts.token}` },
275
+ * })
276
+ * })
277
+ * ```
278
+ */
279
+ function createEmailOutlet(send) {
280
+ return {
281
+ name: "email",
282
+ async deliver(request, token) {
283
+ await send({
284
+ target: request.target ?? "",
285
+ template: request.template ?? "",
286
+ context: request.context ?? {},
287
+ token
288
+ });
289
+ return { response: {
290
+ sent: true,
291
+ outlet: "email"
292
+ } };
293
+ }
294
+ };
295
+ }
296
+
297
+ //#endregion
298
+ //#region packages/event-wf/src/outlets/create-handler.ts
299
+ /**
300
+ * Creates a pre-wired outlet handler from a workflow app instance.
301
+ * Eliminates the need to manually construct `WfOutletTriggerDeps`.
302
+ *
303
+ * Accepts any object with `start` and `resume` methods (WooksWf, MoostWf, etc.).
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * const handle = createOutletHandler(wfApp)
308
+ * httpApp.post('/workflow', () => handle(config))
309
+ * ```
310
+ */
311
+ function createOutletHandler(wfApp) {
312
+ return (config) => handleWfOutletRequest(config, {
313
+ start: (schemaId, context, opts) => wfApp.start(schemaId, context, opts),
314
+ resume: (state, opts) => wfApp.resume(state, opts)
315
+ });
316
+ }
317
+
318
+ //#endregion
319
+ //#region node_modules/.pnpm/@prostojs+wf@0.1.1/node_modules/@prostojs/wf/dist/outlets/index.mjs
320
+ /**
321
+ * Generic outlet request. Use for custom outlets.
322
+ *
323
+ * @example
324
+ * return outlet('pending-task', {
325
+ * payload: ApprovalForm,
326
+ * target: managerId,
327
+ * context: { orderId, amount },
328
+ * })
329
+ */
330
+ function outlet(name, data) {
331
+ return { inputRequired: {
332
+ outlet: name,
333
+ ...data
334
+ } };
335
+ }
336
+ /**
337
+ * Pause for HTTP form input. The outlet returns the payload (form definition)
338
+ * and state token in the HTTP response.
339
+ *
340
+ * @example
341
+ * return outletHttp(LoginForm)
342
+ * return outletHttp(LoginForm, { error: 'Invalid credentials' })
343
+ */
344
+ function outletHttp(payload, context) {
345
+ return outlet("http", {
346
+ payload,
347
+ context
348
+ });
349
+ }
350
+ /**
351
+ * Pause and send email with a magic link containing the state token.
352
+ *
353
+ * @example
354
+ * return outletEmail('user@test.com', 'invite', { name: 'Alice' })
355
+ */
356
+ function outletEmail(target, template, context) {
357
+ return outlet("email", {
358
+ target,
359
+ template,
360
+ context
361
+ });
362
+ }
363
+ /**
364
+ * Self-contained AES-256-GCM encrypted state strategy.
365
+ *
366
+ * Workflow state is encrypted into a base64url token that travels with the
367
+ * transport (cookie, URL param, hidden field). No server-side storage needed.
368
+ *
369
+ * Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
370
+ *
371
+ * @example
372
+ * const strategy = new EncapsulatedStateStrategy({
373
+ * secret: crypto.randomBytes(32),
374
+ * defaultTtl: 3600_000, // 1 hour
375
+ * });
376
+ * const token = await strategy.persist(state);
377
+ * const recovered = await strategy.retrieve(token);
378
+ */
379
+ var EncapsulatedStateStrategy = class {
380
+ /** @throws if secret is not exactly 32 bytes */
381
+ constructor(config) {
382
+ this.config = config;
383
+ this.key = typeof config.secret === "string" ? Buffer.from(config.secret, "hex") : config.secret;
384
+ if (this.key.length !== 32) throw new Error("EncapsulatedStateStrategy: secret must be exactly 32 bytes");
385
+ }
386
+ /**
387
+ * Encrypt workflow state into a self-contained token.
388
+ * @param state — workflow state to persist
389
+ * @param options.ttl — time-to-live in ms (overrides defaultTtl)
390
+ * @returns base64url-encoded encrypted token
391
+ */
392
+ async persist(state, options) {
393
+ const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
394
+ const exp = ttl > 0 ? Date.now() + ttl : 0;
395
+ const payload = JSON.stringify({
396
+ s: state,
397
+ e: exp
398
+ });
399
+ const iv = randomBytes(12);
400
+ const cipher = createCipheriv("aes-256-gcm", this.key, iv);
401
+ const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
402
+ const tag = cipher.getAuthTag();
403
+ return Buffer.concat([
404
+ iv,
405
+ tag,
406
+ encrypted
407
+ ]).toString("base64url");
408
+ }
409
+ /** Decrypt and return workflow state. Returns null if token is invalid, expired, or tampered. */
410
+ async retrieve(token) {
411
+ return this.decrypt(token);
412
+ }
413
+ /** Same as retrieve (stateless — cannot truly invalidate a token). */
414
+ async consume(token) {
415
+ return this.decrypt(token);
416
+ }
417
+ decrypt(token) {
418
+ try {
419
+ const buf = Buffer.from(token, "base64url");
420
+ if (buf.length < 28) return null;
421
+ const iv = buf.subarray(0, 12);
422
+ const tag = buf.subarray(12, 28);
423
+ const ciphertext = buf.subarray(28);
424
+ const decipher = createDecipheriv("aes-256-gcm", this.key, iv);
425
+ decipher.setAuthTag(tag);
426
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
427
+ const { s: state, e: exp } = JSON.parse(decrypted.toString("utf8"));
428
+ if (exp > 0 && Date.now() > exp) return null;
429
+ return state;
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
434
+ };
435
+ var HandleStateStrategy = class {
436
+ constructor(config) {
437
+ this.config = config;
438
+ }
439
+ async persist(state, options) {
440
+ const handle = (this.config.generateHandle ?? randomUUID)();
441
+ const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
442
+ const expiresAt = ttl > 0 ? Date.now() + ttl : void 0;
443
+ await this.config.store.set(handle, state, expiresAt);
444
+ return handle;
445
+ }
446
+ async retrieve(token) {
447
+ return (await this.config.store.get(token))?.state ?? null;
448
+ }
449
+ async consume(token) {
450
+ return (await this.config.store.getAndDelete(token))?.state ?? null;
451
+ }
452
+ };
453
+ /**
454
+ * In-memory state store for development and testing.
455
+ * State is lost on process restart.
456
+ */
457
+ var WfStateStoreMemory = class {
458
+ constructor() {
459
+ this.store = /* @__PURE__ */ new Map();
460
+ }
461
+ async set(handle, state, expiresAt) {
462
+ this.store.set(handle, {
463
+ state,
464
+ expiresAt
465
+ });
466
+ }
467
+ async get(handle) {
468
+ const entry = this.store.get(handle);
469
+ if (!entry) return null;
470
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
471
+ this.store.delete(handle);
472
+ return null;
473
+ }
474
+ return entry;
475
+ }
476
+ async delete(handle) {
477
+ this.store.delete(handle);
478
+ }
479
+ async getAndDelete(handle) {
480
+ const entry = await this.get(handle);
481
+ if (entry) this.store.delete(handle);
482
+ return entry;
483
+ }
484
+ async cleanup() {
485
+ const now = Date.now();
486
+ let count = 0;
487
+ for (const [handle, entry] of this.store) if (entry.expiresAt && now > entry.expiresAt) {
488
+ this.store.delete(handle);
489
+ count++;
490
+ }
491
+ return count;
492
+ }
493
+ };
494
+
55
495
  //#endregion
56
496
  //#region packages/event-wf/src/workflow.ts
57
497
  /** Workflow engine that resolves steps via Wooks router lookup. */
@@ -244,4 +684,4 @@ function createWfApp(opts, wooks) {
244
684
  }
245
685
 
246
686
  //#endregion
247
- export { StepRetriableError, WooksWf, createWfApp, createWfContext, resumeKey, resumeWfContext, useLogger, useRouteParams, useWfState, wfKind, wfShortcuts };
687
+ export { EncapsulatedStateStrategy, HandleStateStrategy, StepRetriableError, WfStateStoreMemory, WooksWf, createEmailOutlet, createHttpOutlet, createOutletHandler, createWfApp, createWfContext, handleWfOutletRequest, outlet, outletEmail, outletHttp, resumeKey, resumeWfContext, useLogger, useRouteParams, useWfFinished, useWfOutlet, useWfState, wfKind, wfShortcuts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooksjs/event-wf",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "@wooksjs/event-wf",
5
5
  "keywords": [
6
6
  "app",
@@ -22,13 +22,8 @@
22
22
  "url": "git+https://github.com/wooksjs/wooksjs.git",
23
23
  "directory": "packages/event-wf"
24
24
  },
25
- "bin": {
26
- "wooksjs-event-wf-skill": "scripts/setup-skills.js"
27
- },
28
25
  "files": [
29
- "dist",
30
- "skills",
31
- "scripts"
26
+ "dist"
32
27
  ],
33
28
  "main": "dist/index.cjs",
34
29
  "module": "dist/index.mjs",
@@ -42,21 +37,32 @@
42
37
  }
43
38
  },
44
39
  "dependencies": {
45
- "@prostojs/wf": "^0.0.18"
40
+ "@prostojs/wf": "^0.1.1"
46
41
  },
47
42
  "devDependencies": {
48
43
  "typescript": "^5.9.3",
49
44
  "vitest": "^3.2.4",
50
- "@wooksjs/event-core": "^0.7.7",
51
- "wooks": "^0.7.7"
45
+ "@wooksjs/event-core": "^0.7.9",
46
+ "@wooksjs/event-http": "^0.7.9",
47
+ "@wooksjs/http-body": "^0.7.9",
48
+ "wooks": "^0.7.9"
52
49
  },
53
50
  "peerDependencies": {
54
51
  "@prostojs/logger": "^0.4.3",
55
- "@wooksjs/event-core": "^0.7.7",
56
- "wooks": "^0.7.7"
52
+ "@wooksjs/event-core": "^0.7.9",
53
+ "@wooksjs/event-http": "^0.7.9",
54
+ "@wooksjs/http-body": "^0.7.9",
55
+ "wooks": "^0.7.9"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@wooksjs/event-http": {
59
+ "optional": true
60
+ },
61
+ "@wooksjs/http-body": {
62
+ "optional": true
63
+ }
57
64
  },
58
65
  "scripts": {
59
- "build": "rolldown -c ../../rolldown.config.mjs",
60
- "setup-skills": "node scripts/setup-skills.js"
66
+ "build": "rolldown -c ../../rolldown.config.mjs"
61
67
  }
62
68
  }
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env node
2
- /* prettier-ignore */
3
- 'use strict'
4
-
5
- const fs = require('fs')
6
- const path = require('path')
7
- const os = require('os')
8
-
9
- const SKILL_NAME = 'wooksjs-event-wf'
10
- const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
11
-
12
- if (!fs.existsSync(SKILL_SRC)) {
13
- console.error(`No skills found at ${SKILL_SRC}`)
14
- console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
15
- process.exit(1)
16
- }
17
-
18
- const AGENTS = {
19
- 'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
20
- 'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
21
- 'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
22
- 'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
23
- 'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
24
- }
25
-
26
- const args = process.argv.slice(2)
27
- const isGlobal = args.includes('--global') || args.includes('-g')
28
- const isPostinstall = args.includes('--postinstall')
29
- let installed = 0, skipped = 0
30
- const installedDirs = []
31
-
32
- for (const [agentName, cfg] of Object.entries(AGENTS)) {
33
- const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
34
- const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
35
-
36
- // In postinstall mode: silently skip agents that aren't set up globally
37
- if (isPostinstall || isGlobal) {
38
- if (!fs.existsSync(agentRootDir)) { skipped++; continue }
39
- }
40
-
41
- const dest = path.join(targetBase, SKILL_NAME)
42
- try {
43
- fs.mkdirSync(dest, { recursive: true })
44
- fs.cpSync(SKILL_SRC, dest, { recursive: true })
45
- console.log(`✅ ${agentName}: installed to ${dest}`)
46
- installed++
47
- if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
48
- } catch (err) {
49
- console.warn(`⚠️ ${agentName}: failed — ${err.message}`)
50
- }
51
- }
52
-
53
- // Add locally-installed skill dirs to .gitignore
54
- if (!isGlobal && installedDirs.length > 0) {
55
- const gitignorePath = path.join(process.cwd(), '.gitignore')
56
- let gitignoreContent = ''
57
- try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
58
- const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
59
- if (linesToAdd.length > 0) {
60
- const hasHeader = gitignoreContent.includes('# AI agent skills')
61
- const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
62
- + (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
63
- + linesToAdd.join('\n') + '\n'
64
- fs.appendFileSync(gitignorePath, block)
65
- console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`)
66
- }
67
- }
68
-
69
- if (installed === 0 && isPostinstall) {
70
- // Silence is fine — no agents present, nothing to do
71
- } else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
72
- console.log('No agent directories detected. Try --global or run without it for project-local install.')
73
- } else if (installed === 0) {
74
- console.log('Nothing installed. Run without --global to install project-locally.')
75
- } else {
76
- console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
77
- }
@@ -1,42 +0,0 @@
1
- ---
2
- name: wooksjs-event-wf
3
- description: Wooks Workflow framework — composable, step-based workflow engine for Node.js. Load when building workflow/process automation with wooks; defining workflow steps and flows; using workflow composables (useWfState, useRouteParams, useLogger); working with @wooksjs/event-core context (key, cached, defineWook, defineEventKind); creating conditional and looping flows; resuming paused workflows; handling user input requirements; using string-based step handlers; attaching workflow spies; working with StepRetriableError; creating custom event context composables for workflows; sharing parent event context (eventContext) for HTTP integration; accessing HTTP composables from workflow steps.
4
- ---
5
-
6
- # @wooksjs/event-wf
7
-
8
- A composable workflow framework for Node.js built on async context (AsyncLocalStorage) and `@prostojs/wf`. Define steps and flows as composable units — steps execute sequentially with conditional branching, loops, pause/resume, and user input handling. Context is scoped per workflow execution.
9
-
10
- ## How to use this skill
11
-
12
- Read the domain file that matches the task. Do not load all files — only what you need.
13
-
14
- | Domain | File | Load when... |
15
- | ------------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
- | Event context (core machinery) | [event-core.md](event-core.md) | Understanding `EventContext`, `key()`/`cached()`/`defineWook()`/`defineEventKind()`/`slot()`, creating custom composables, lazy evaluation and caching, building your own `use*()` functions |
17
- | Workflow app setup | [core.md](core.md) | Creating a workflow app, `createWfApp`, starting/resuming workflows, error handling, spies, testing, logging, sharing parent event context (`eventContext`), HTTP integration |
18
- | Steps & flows | [workflows.md](workflows.md) | Defining steps (`app.step`), defining flows (`app.flow`), workflow schemas, conditions, loops, user input, parametric steps, `useWfState`, `StepRetriableError`, string-based handlers |
19
-
20
- ## Quick reference
21
-
22
- ```ts
23
- import { createWfApp } from '@wooksjs/event-wf'
24
-
25
- const app = createWfApp<{ result: number }>()
26
-
27
- app.step('add', {
28
- input: 'number',
29
- handler: 'ctx.result += input',
30
- })
31
-
32
- app.flow('calculate', [
33
- { id: 'add', input: 5 },
34
- { id: 'add', input: 2 },
35
- { condition: 'result < 10', steps: [{ id: 'add', input: 3 }] },
36
- ])
37
-
38
- const output = await app.start('calculate', { result: 0 })
39
- console.log(output.state.context) // { result: 10 }
40
- ```
41
-
42
- Key exports: `createWfApp()`, `useWfState()`, `useRouteParams()`, `useLogger()`, `StepRetriableError`.