target-supabase-sdk 0.1.9 → 0.2.0

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.
Files changed (135) hide show
  1. package/README.md +66 -0
  2. package/dist/browser.d.ts +52 -0
  3. package/dist/browser.d.ts.map +1 -0
  4. package/dist/browser.js +45 -0
  5. package/dist/browser.js.map +1 -0
  6. package/dist/command/command.api.js +5 -5
  7. package/dist/command/command.api.js.map +1 -1
  8. package/dist/index.d.ts +2 -48
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -41
  11. package/dist/index.js.map +1 -1
  12. package/dist/link/link.interface.d.ts +1 -1
  13. package/dist/link/link.interface.d.ts.map +1 -1
  14. package/dist/node/node-log-scope.d.ts +11 -0
  15. package/dist/node/node-log-scope.d.ts.map +1 -0
  16. package/dist/node/node-log-scope.js +7 -0
  17. package/dist/node/node-log-scope.js.map +1 -0
  18. package/dist/node/node-runtime.base.d.ts +42 -0
  19. package/dist/node/node-runtime.base.d.ts.map +1 -0
  20. package/dist/node/node-runtime.base.js +397 -0
  21. package/dist/node/node-runtime.base.js.map +1 -0
  22. package/dist/node/node.api.d.ts +3 -3
  23. package/dist/node/node.api.js +3 -3
  24. package/dist/node/node.api.js.map +1 -1
  25. package/dist/node/task-node.d.ts +20 -0
  26. package/dist/node/task-node.d.ts.map +1 -0
  27. package/dist/node/task-node.js +245 -0
  28. package/dist/node/task-node.js.map +1 -0
  29. package/dist/node/trigger-node.d.ts +18 -0
  30. package/dist/node/trigger-node.d.ts.map +1 -0
  31. package/dist/node/trigger-node.js +150 -0
  32. package/dist/node/trigger-node.js.map +1 -0
  33. package/dist/node.d.ts +22 -0
  34. package/dist/node.d.ts.map +1 -0
  35. package/dist/node.js +19 -0
  36. package/dist/node.js.map +1 -0
  37. package/dist/repo/repo-manager.d.ts +14 -3
  38. package/dist/repo/repo-manager.d.ts.map +1 -1
  39. package/dist/repo/repo-manager.js +27 -17
  40. package/dist/repo/repo-manager.js.map +1 -1
  41. package/dist/repo/repo.script-loader.d.ts +3 -3
  42. package/dist/repo/repo.script-loader.d.ts.map +1 -1
  43. package/dist/repo/repo.script-loader.js +10 -10
  44. package/dist/repo/repo.script-loader.js.map +1 -1
  45. package/dist/shared/log/create-api-logger.d.ts +10 -6
  46. package/dist/shared/log/create-api-logger.d.ts.map +1 -1
  47. package/dist/shared/log/create-api-logger.js +10 -6
  48. package/dist/shared/log/create-api-logger.js.map +1 -1
  49. package/dist/shared/log/index.d.ts +10 -0
  50. package/dist/shared/log/index.d.ts.map +1 -0
  51. package/dist/shared/log/index.js +10 -0
  52. package/dist/shared/log/index.js.map +1 -0
  53. package/dist/shared/log/log-manager.d.ts +25 -46
  54. package/dist/shared/log/log-manager.d.ts.map +1 -1
  55. package/dist/shared/log/log-manager.js +59 -40
  56. package/dist/shared/log/log-manager.js.map +1 -1
  57. package/dist/shared/log/log-scope.d.ts +49 -0
  58. package/dist/shared/log/log-scope.d.ts.map +1 -0
  59. package/dist/shared/log/log-scope.js +70 -0
  60. package/dist/shared/log/log-scope.js.map +1 -0
  61. package/dist/task/index.d.ts +15 -0
  62. package/dist/task/index.d.ts.map +1 -0
  63. package/dist/task/index.js +14 -0
  64. package/dist/task/index.js.map +1 -0
  65. package/dist/task/local-task-registry.d.ts +10 -0
  66. package/dist/task/local-task-registry.d.ts.map +1 -1
  67. package/dist/task/local-task-registry.js +83 -22
  68. package/dist/task/local-task-registry.js.map +1 -1
  69. package/dist/task/task-manager.d.ts +18 -6
  70. package/dist/task/task-manager.d.ts.map +1 -1
  71. package/dist/task/task-manager.js +46 -67
  72. package/dist/task/task-manager.js.map +1 -1
  73. package/dist/task/task-post.api.d.ts +49 -0
  74. package/dist/task/task-post.api.d.ts.map +1 -0
  75. package/dist/task/task-post.api.js +91 -0
  76. package/dist/task/task-post.api.js.map +1 -0
  77. package/dist/task/task-repo-context.d.ts +2 -0
  78. package/dist/task/task-repo-context.d.ts.map +1 -1
  79. package/dist/task/task-repo-context.js.map +1 -1
  80. package/dist/task/task-repo-validation.d.ts +46 -0
  81. package/dist/task/task-repo-validation.d.ts.map +1 -0
  82. package/dist/task/task-repo-validation.js +96 -0
  83. package/dist/task/task-repo-validation.js.map +1 -0
  84. package/dist/task/task.api.d.ts +11 -53
  85. package/dist/task/task.api.d.ts.map +1 -1
  86. package/dist/task/task.api.js +13 -61
  87. package/dist/task/task.api.js.map +1 -1
  88. package/dist/task/task.interface.d.ts +2 -0
  89. package/dist/task/task.interface.d.ts.map +1 -1
  90. package/dist/task/task.interface.js.map +1 -1
  91. package/dist/trigger/trigger.api.d.ts +140 -0
  92. package/dist/trigger/trigger.api.d.ts.map +1 -0
  93. package/dist/trigger/trigger.api.js +125 -0
  94. package/dist/trigger/trigger.api.js.map +1 -0
  95. package/dist/trigger/trigger.interface.d.ts +43 -0
  96. package/dist/trigger/trigger.interface.d.ts.map +1 -0
  97. package/dist/trigger/trigger.interface.js +11 -0
  98. package/dist/trigger/trigger.interface.js.map +1 -0
  99. package/dist/trigger/trigger.utils.d.ts +6 -0
  100. package/dist/trigger/trigger.utils.d.ts.map +1 -0
  101. package/dist/trigger/trigger.utils.js +39 -0
  102. package/dist/trigger/trigger.utils.js.map +1 -0
  103. package/package.json +21 -6
  104. package/src/browser.ts +100 -0
  105. package/src/command/command.api.ts +5 -5
  106. package/src/index.ts +2 -79
  107. package/src/link/link.interface.ts +1 -1
  108. package/src/node/node-log-scope.ts +15 -0
  109. package/src/node/node-runtime.base.ts +450 -0
  110. package/src/node/node.api.ts +3 -3
  111. package/src/node/task-node.ts +284 -0
  112. package/src/node/trigger-node.ts +180 -0
  113. package/src/node.ts +62 -0
  114. package/src/repo/repo-manager.ts +49 -21
  115. package/src/repo/repo.script-loader.ts +13 -13
  116. package/src/shared/log/create-api-logger.ts +23 -12
  117. package/src/shared/log/index.ts +23 -0
  118. package/src/shared/log/log-manager.ts +85 -90
  119. package/src/shared/log/log-scope.ts +123 -0
  120. package/src/task/index.ts +48 -0
  121. package/src/task/local-task-registry.ts +110 -23
  122. package/src/task/task-manager.ts +73 -84
  123. package/src/task/task-post.api.ts +116 -0
  124. package/src/task/task-repo-context.ts +2 -0
  125. package/src/task/task-repo-validation.ts +171 -0
  126. package/src/task/task.api.ts +13 -73
  127. package/src/task/task.interface.ts +2 -0
  128. package/src/trigger/trigger.api.ts +166 -0
  129. package/src/trigger/trigger.interface.ts +50 -0
  130. package/src/trigger/trigger.utils.ts +56 -0
  131. package/dist/node/node-manager.d.ts +0 -48
  132. package/dist/node/node-manager.d.ts.map +0 -1
  133. package/dist/node/node-manager.js +0 -619
  134. package/dist/node/node-manager.js.map +0 -1
  135. package/src/node/node-manager.ts +0 -673
@@ -1,4 +1,4 @@
1
- import { readdir } from "node:fs/promises";
1
+ import { readdir, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { RepoManager } from "../repo/repo-manager";
@@ -23,6 +23,11 @@ export interface BootstrapLocalTasksOptions {
23
23
  rootConfigPath?: string;
24
24
  /** Base directory for resolving root config (default `process.cwd()`) */
25
25
  cwd?: string;
26
+ /**
27
+ * When set with an unchanged config fingerprint, skip full scan if this key is already
28
+ * registered in {@link RepoManager}.
29
+ */
30
+ forTaskTypeKey?: string;
26
31
  }
27
32
 
28
33
  const EMPTY_RESULT: BootstrapLocalTasksResult = {
@@ -32,6 +37,14 @@ const EMPTY_RESULT: BootstrapLocalTasksResult = {
32
37
  errors: [],
33
38
  };
34
39
 
40
+ type BootstrapCacheState = {
41
+ fingerprint: string | null;
42
+ lastResult: BootstrapLocalTasksResult | null;
43
+ };
44
+
45
+ let bootstrapCache: BootstrapCacheState = { fingerprint: null, lastResult: null };
46
+ let bootstrapInFlight: Promise<BootstrapLocalTasksResult> | null = null;
47
+
35
48
  function parseRootConfig(raw: unknown, configPath: string): TaskRunnerRootConfig {
36
49
  if (raw == null || typeof raw !== "object") {
37
50
  throw new Error(`Root config must export an object: ${configPath}`);
@@ -78,24 +91,46 @@ function finalizeStatus(result: Omit<BootstrapLocalTasksResult, "status">): Boot
78
91
  return "loaded";
79
92
  }
80
93
 
81
- /**
82
- * Discover local task packages from config files and register them with {@link RepoManager}.
83
- *
84
- * **Never throws** for missing root config — returns `{ status: "not_configured" }`.
85
- *
86
- * Host app layout (recommended):
87
- * ```
88
- * ./config/task.config.js → { taskDir: "../tasks" }
89
- * ./tasks/my-task/task.config.js
90
- * ./tasks/my-task/index.mjs
91
- * ```
92
- *
93
- * `taskDir` / `entry` paths are relative to **their config file's directory** (see config-file-relative-paths skill).
94
- *
95
- * Legacy fallback: `./task.config.js` at project root (`taskDir: "./tasks"`).
96
- */
97
- export async function bootstrapLocalTasks(
98
- options: BootstrapLocalTasksOptions = {}
94
+ async function computeConfigFingerprint(rootConfigPath: string, tasksRoot: string): Promise<string> {
95
+ const rootStat = await stat(rootConfigPath);
96
+ const tasksRootStat = await stat(tasksRoot);
97
+ const entries = await readdir(tasksRoot, { withFileTypes: true });
98
+ const configMtims: number[] = [];
99
+
100
+ for (const entry of entries) {
101
+ if (!entry.isDirectory()) {
102
+ continue;
103
+ }
104
+ const configPath = join(tasksRoot, entry.name, TASK_LOCAL_PACKAGE_CONFIG_FILENAME);
105
+ if (await pathExists(configPath)) {
106
+ const configStat = await stat(configPath);
107
+ configMtims.push(configStat.mtimeMs);
108
+ }
109
+ }
110
+
111
+ configMtims.sort((a, b) => a - b);
112
+ return `${rootConfigPath}:${rootStat.mtimeMs}:${tasksRoot}:${tasksRootStat.mtimeMs}:${configMtims.join(",")}`;
113
+ }
114
+
115
+ function canUseCachedBootstrap(
116
+ fingerprint: string,
117
+ forTaskTypeKey: string | undefined
118
+ ): BootstrapLocalTasksResult | null {
119
+ if (bootstrapCache.fingerprint !== fingerprint || bootstrapCache.lastResult == null) {
120
+ return null;
121
+ }
122
+
123
+ if (forTaskTypeKey != null && forTaskTypeKey !== "") {
124
+ if (!RepoManager.hasLocalRepo(forTaskTypeKey)) {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ return { ...bootstrapCache.lastResult, cached: true };
130
+ }
131
+
132
+ async function runBootstrapScan(
133
+ options: BootstrapLocalTasksOptions
99
134
  ): Promise<BootstrapLocalTasksResult> {
100
135
  const cwd = options.cwd ?? process.cwd();
101
136
 
@@ -107,10 +142,12 @@ export async function bootstrapLocalTasks(
107
142
  ],
108
143
  });
109
144
  if (rootConfigPath == null) {
110
- return {
145
+ const result: BootstrapLocalTasksResult = {
111
146
  ...EMPTY_RESULT,
112
147
  message: `No root config (tried ${TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH}, ${TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH})`,
113
148
  };
149
+ bootstrapCache = { fingerprint: null, lastResult: result };
150
+ return result;
114
151
  }
115
152
 
116
153
  const result: Omit<BootstrapLocalTasksResult, "status"> = {
@@ -125,11 +162,19 @@ export async function bootstrapLocalTasks(
125
162
 
126
163
  const tasksRoot = resolvePathFromConfigFile(rootConfigPath, taskDir);
127
164
  if (!(await pathExists(tasksRoot))) {
128
- return {
165
+ const failed: BootstrapLocalTasksResult = {
129
166
  status: "failed",
130
167
  message: `taskDir does not exist: ${tasksRoot}`,
131
168
  ...result,
132
169
  };
170
+ bootstrapCache = { fingerprint: null, lastResult: failed };
171
+ return failed;
172
+ }
173
+
174
+ const fingerprint = await computeConfigFingerprint(rootConfigPath, tasksRoot);
175
+ const cached = canUseCachedBootstrap(fingerprint, options.forTaskTypeKey);
176
+ if (cached != null) {
177
+ return cached;
133
178
  }
134
179
 
135
180
  const entries = await readdir(tasksRoot, { withFileTypes: true });
@@ -171,7 +216,7 @@ export async function bootstrapLocalTasks(
171
216
  }
172
217
 
173
218
  const status = finalizeStatus(result);
174
- return {
219
+ const finalResult: BootstrapLocalTasksResult = {
175
220
  status,
176
221
  message:
177
222
  status === "empty"
@@ -179,13 +224,55 @@ export async function bootstrapLocalTasks(
179
224
  : undefined,
180
225
  ...result,
181
226
  };
227
+ bootstrapCache = { fingerprint, lastResult: finalResult };
228
+ return finalResult;
182
229
  } catch (error) {
183
- return {
230
+ const failed: BootstrapLocalTasksResult = {
184
231
  status: "failed",
185
232
  message: error instanceof Error ? error.message : String(error),
186
233
  ...result,
187
234
  };
235
+ bootstrapCache = { fingerprint: null, lastResult: failed };
236
+ return failed;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Discover local task packages from config files and register them with {@link RepoManager}.
242
+ *
243
+ * **Never throws** for missing root config — returns `{ status: "not_configured" }`.
244
+ *
245
+ * Re-scans are skipped when the config fingerprint is unchanged (root config + taskDir mtimes).
246
+ * Concurrent calls share a single in-flight scan (single-flight).
247
+ *
248
+ * Host app layout (recommended):
249
+ * ```
250
+ * ./config/task.config.js → { taskDir: "../tasks" }
251
+ * ./tasks/my-task/task.config.js
252
+ * ./tasks/my-task/index.mjs
253
+ * ```
254
+ *
255
+ * `taskDir` / `entry` paths are relative to **their config file's directory** (see config-file-relative-paths skill).
256
+ *
257
+ * Legacy fallback: `./task.config.js` at project root (`taskDir: "./tasks"`).
258
+ */
259
+ export async function bootstrapLocalTasks(
260
+ options: BootstrapLocalTasksOptions = {}
261
+ ): Promise<BootstrapLocalTasksResult> {
262
+ if (bootstrapInFlight != null) {
263
+ return bootstrapInFlight;
188
264
  }
265
+
266
+ bootstrapInFlight = runBootstrapScan(options).finally(() => {
267
+ bootstrapInFlight = null;
268
+ });
269
+ return bootstrapInFlight;
270
+ }
271
+
272
+ /** Clear fingerprint cache (tests / hot-reload tooling). */
273
+ export function clearBootstrapLocalTasksCache(): void {
274
+ bootstrapCache = { fingerprint: null, lastResult: null };
275
+ bootstrapInFlight = null;
189
276
  }
190
277
 
191
278
  export function getRegisteredLocalTaskTypeKeys(): string[] {
@@ -1,9 +1,8 @@
1
- import { RepoManager } from "../repo/repo-manager";
2
1
  import { getScanRemoteRepoValues } from "../repo/repo.api";
3
2
  import { TASK_REPO_USAGE } from "../repo/repo.interface";
4
- import { LoggerWithContext } from "../shared/log/log-manager";
3
+ import { LoggerWithScope } from "../shared/log";
5
4
  import { ResultCode, Task } from "./task.interface";
6
- import { TaskFn, TaskRepoContext, TaskRunResult, ExecutableTaskFn } from "./task-repo-context";
5
+ import { TaskFn, TaskRunResult, ExecutableTaskFn } from "./task-repo-context";
7
6
 
8
7
  export type {
9
8
  TaskFn,
@@ -28,15 +27,21 @@ export {
28
27
  import {
29
28
  bootstrapLocalTasks,
30
29
  BootstrapLocalTasksOptions,
30
+ clearBootstrapLocalTasksCache,
31
31
  getRegisteredLocalTaskTypeKeys,
32
32
  } from "./local-task-registry";
33
33
  import type { BootstrapLocalTasksResult } from "./task-repo-context";
34
+ import { TaskRepoValidation, type TaskRepoValidationFailureReason } from "./task-repo-validation";
34
35
 
35
- export type PrepareTaskFailureReason =
36
- | "MISSING_PARAMS"
37
- | "REPO_LOAD_FAILED"
38
- | "PARAMS_VALIDATION_FAILED"
39
- | "TASK_TYPE_KEY_MISMATCH";
36
+ /** @deprecated use {@link TaskRepoValidationFailureReason} */
37
+ export type PrepareTaskFailureReason = TaskRepoValidationFailureReason;
38
+
39
+ export type {
40
+ TaskRepoValidationFailureReason,
41
+ ValidateTaskRepoAndParamsInput,
42
+ ValidateTaskRepoAndParamsResult,
43
+ } from "./task-repo-validation";
44
+ export { TaskRepoValidation } from "./task-repo-validation";
40
45
 
41
46
  export interface PrepareTaskResponse {
42
47
  isSuccess: boolean;
@@ -49,12 +54,18 @@ export interface PrepareTaskResponse {
49
54
  }
50
55
 
51
56
  export interface PrepareTaskParams {
52
- logger: LoggerWithContext;
57
+ logger: LoggerWithScope;
53
58
  task: Task;
59
+ /**
60
+ * When false, do not fetch Repo from Supabase during validation.
61
+ * Should match {@link RegisterTasksOptions.includeRemote} on the worker.
62
+ */
63
+ includeRemote?: boolean;
64
+ bootstrapLocalOptions?: BootstrapLocalTasksOptions;
54
65
  }
55
66
 
56
67
  export interface RegisterTasksOptions {
57
- logger: LoggerWithContext;
68
+ logger: LoggerWithScope;
58
69
  local?: BootstrapLocalTasksOptions;
59
70
  /** When `true` (default), union local keys with remote `Repo.value` from Supabase. */
60
71
  includeRemote?: boolean;
@@ -64,32 +75,33 @@ export interface RegisterTasksResult {
64
75
  availableTaskList: string[];
65
76
  local: BootstrapLocalTasksResult;
66
77
  remote: { values: string[] };
78
+ includeRemote: boolean;
67
79
  }
68
80
 
69
- function logLocalBootstrap(logger: LoggerWithContext, bootstrap: BootstrapLocalTasksResult): void {
81
+ function logLocalBootstrap(logger: LoggerWithScope, bootstrap: BootstrapLocalTasksResult): void {
70
82
  switch (bootstrap.status) {
71
83
  case "not_configured":
72
84
  logger.info("未找到本地任務配置,跳過本地註冊", {
73
85
  topic: "task",
74
- context: { message: bootstrap.message },
86
+ data: { message: bootstrap.message },
75
87
  });
76
88
  break;
77
89
  case "failed":
78
90
  logger.warn("本地任務註冊失敗", {
79
91
  topic: "task",
80
- context: { message: bootstrap.message, errors: bootstrap.errors },
92
+ data: { message: bootstrap.message, errors: bootstrap.errors },
81
93
  });
82
94
  break;
83
95
  case "empty":
84
96
  logger.info("本地任務掃描完成,無已註冊任務", {
85
97
  topic: "task",
86
- context: { message: bootstrap.message, skipped: bootstrap.skipped },
98
+ data: { message: bootstrap.message, skipped: bootstrap.skipped },
87
99
  });
88
100
  break;
89
101
  case "loaded":
90
102
  logger.info("本地任務註冊完成", {
91
103
  topic: "task",
92
- context: {
104
+ data: {
93
105
  registered: bootstrap.registered,
94
106
  skipped: bootstrap.skipped,
95
107
  errors: bootstrap.errors,
@@ -122,7 +134,7 @@ async function registerTasks({
122
134
  if (includeRemote) {
123
135
  logger.info("查詢遠程 Repo 列表", {
124
136
  topic: "task",
125
- context: { usage: TASK_REPO_USAGE },
137
+ data: { usage: TASK_REPO_USAGE },
126
138
  });
127
139
  const { data, error: remoteError } = await getScanRemoteRepoValues({ usage: TASK_REPO_USAGE });
128
140
  if (remoteError) {
@@ -131,7 +143,7 @@ async function registerTasks({
131
143
  remoteValues = data ?? [];
132
144
  logger.info("遠程 Repo 查詢完成", {
133
145
  topic: "task",
134
- context: { count: remoteValues.length, values: remoteValues },
146
+ data: { count: remoteValues.length, values: remoteValues },
135
147
  });
136
148
  }
137
149
 
@@ -142,7 +154,7 @@ async function registerTasks({
142
154
 
143
155
  logger.success("任務註冊完成", {
144
156
  topic: "task",
145
- context: {
157
+ data: {
146
158
  availableTaskList,
147
159
  localRegistered: local.registered,
148
160
  remoteValues,
@@ -153,10 +165,11 @@ async function registerTasks({
153
165
  availableTaskList,
154
166
  local,
155
167
  remote: { values: remoteValues },
168
+ includeRemote,
156
169
  };
157
170
  }
158
171
 
159
- /** 将 taskFn 与 params 闭包绑定,供 NodeManager 无参调用 `await taskFn()` */
172
+ /** 将 taskFn 与 params 闭包绑定,供 TaskNode 无参调用 `await taskFn()` */
160
173
  function bindTaskFn(taskFn: TaskFn, taskParams: unknown): ExecutableTaskFn {
161
174
  const run = async () => taskFn(taskParams);
162
175
  return Object.assign(run, {
@@ -165,36 +178,23 @@ function bindTaskFn(taskFn: TaskFn, taskParams: unknown): ExecutableTaskFn {
165
178
  });
166
179
  }
167
180
 
168
- /** 调用 Repo 模块导出的 taskParamsValidator;抛错视为校验失败 */
169
- function validateTaskParams(
170
- validator: TaskRepoContext["taskParamsValidator"],
171
- taskParams: unknown,
172
- logger: LoggerWithContext
173
- ): boolean {
174
- try {
175
- return validator(taskParams) === true;
176
- } catch (error) {
177
- logger.warn("taskParamsValidator 拋出異常", {
178
- topic: "task",
179
- context: { error: error instanceof Error ? error.message : error },
180
- });
181
- return false;
182
- }
183
- }
184
-
185
181
  /**
186
182
  * 將 Supabase 認領到的 Task 解析為可執行的 `ExecutableTaskFn`。
187
183
  *
188
184
  * 流程:
189
185
  * 1. 從 task.details 取出 `params`;`task.value` 作為 taskTypeKey(同 {@link Repo.value})
190
- * 2. {@link RepoManager.getRepoContext} — 本地註冊優先,否則按 task.value 拉取 Repo
191
- * 3. 校驗 taskFn.taskTypeKey 與 task.value 一致
192
- * 4. 用 Repo 導出的 taskParamsValidator 校驗 params
193
- * 5. bindTaskFn,返回閉包後的無參執行函數
186
+ * 2. {@link TaskRepoValidation.validate} — 本地註冊優先;未命中時可選遠端(`includeRemote`)
187
+ * Worker 在 registry 未命中時會補掃本地(`local_bootstrap_on_miss`)
188
+ * 3. bindTaskFn,返回閉包後的無參執行函數
194
189
  *
195
190
  * 失敗時返回 `{ isSuccess: false, code, message, reason, step }`(不 throw)。
196
191
  */
197
- const prepareTask = async ({ logger, task }: PrepareTaskParams): Promise<PrepareTaskResponse> => {
192
+ const prepareTask = async ({
193
+ logger,
194
+ task,
195
+ includeRemote = true,
196
+ bootstrapLocalOptions,
197
+ }: PrepareTaskParams): Promise<PrepareTaskResponse> => {
198
198
  const fail = (params: {
199
199
  reason: PrepareTaskFailureReason;
200
200
  code: ResultCode;
@@ -203,7 +203,7 @@ const prepareTask = async ({ logger, task }: PrepareTaskParams): Promise<Prepare
203
203
  }): PrepareTaskResponse => {
204
204
  logger.warn(params.message, {
205
205
  topic: "task",
206
- context: {
206
+ data: {
207
207
  taskId: task.id,
208
208
  taskTypeKey: task.value,
209
209
  reason: params.reason,
@@ -223,72 +223,60 @@ const prepareTask = async ({ logger, task }: PrepareTaskParams): Promise<Prepare
223
223
  const { id: taskId, name: taskName, value: taskTypeKey, details } = task;
224
224
  logger.info(`開始準備任務 ${taskName}-${taskTypeKey}`, {
225
225
  topic: "task",
226
- context: { taskId, taskName, taskTypeKey },
226
+ data: { taskId, taskName, taskTypeKey },
227
227
  });
228
228
 
229
229
  const { params: taskParams } = details;
230
- if (taskParams == null) {
231
- return fail({
232
- reason: "MISSING_PARAMS",
233
- code: ResultCode.PARAMS_NOT_VALID,
234
- message: "任務缺少 params",
235
- step: "params",
230
+
231
+ if (!getRegisteredLocalTaskTypeKeys().includes(taskTypeKey)) {
232
+ const bootstrap = await bootstrapLocalTasks({
233
+ ...bootstrapLocalOptions,
234
+ forTaskTypeKey: taskTypeKey,
235
+ });
236
+ logger.debug("本地 registry 未命中,補掃本地任務", {
237
+ topic: "task",
238
+ data: {
239
+ taskId,
240
+ taskTypeKey,
241
+ step: "local_bootstrap_on_miss",
242
+ status: bootstrap.status,
243
+ registered: bootstrap.registered,
244
+ cached: bootstrap.cached ?? false,
245
+ message: bootstrap.message,
246
+ },
236
247
  });
237
248
  }
238
249
 
239
- logger.debug("任務詳情校驗通過,開始解析 Repo 上下文", {
240
- topic: "task",
241
- context: { taskTypeKey, step: "repo_load" },
242
- });
243
-
244
- const { context: repoContext, error: repoError } = await RepoManager.getRepoContext<TaskRepoContext>({
250
+ const validation = await TaskRepoValidation.validate({
245
251
  logger,
246
252
  taskTypeKey,
253
+ params: taskParams,
254
+ includeRemote,
247
255
  });
248
-
249
- if (repoContext == null) {
256
+ if (!validation.isValid) {
250
257
  return fail({
251
- reason: "REPO_LOAD_FAILED",
252
- code: ResultCode.REPO_NOT_VALID,
253
- message: repoError ?? "無法加載 Repo 上下文",
254
- step: "repo_load",
258
+ reason: validation.reason,
259
+ code: validation.code,
260
+ message: validation.message,
261
+ step: validation.step,
255
262
  });
256
263
  }
257
264
 
265
+ const { repoContext } = validation;
266
+
258
267
  logger.info("Repo 上下文加載成功", {
259
268
  topic: "task",
260
- context: {
269
+ data: {
261
270
  taskTypeKey,
262
271
  taskFnDisplayName: repoContext.taskFn.displayName,
263
272
  step: "repo_load",
264
273
  },
265
274
  });
266
275
 
267
- if (repoContext.taskFn.taskTypeKey !== taskTypeKey) {
268
- return fail({
269
- reason: "TASK_TYPE_KEY_MISMATCH",
270
- code: ResultCode.REPO_NOT_VALID,
271
- message: `taskFn.taskTypeKey (${repoContext.taskFn.taskTypeKey}) 與 task.value (${taskTypeKey}) 不一致`,
272
- step: "task_type_key",
273
- });
274
- }
275
-
276
- logger.debug("開始校驗任務參數", { topic: "task", context: { taskTypeKey, step: "params_validation" } });
277
- if (!validateTaskParams(repoContext.taskParamsValidator, taskParams, logger)) {
278
- return fail({
279
- reason: "PARAMS_VALIDATION_FAILED",
280
- code: ResultCode.PARAMS_NOT_VALID,
281
- message: "任務參數校驗失敗",
282
- step: "params_validation",
283
- });
284
- }
285
-
286
- logger.debug("任務參數校驗通過", { topic: "task", context: { taskTypeKey, step: "params_validation" } });
287
-
288
276
  const boundTaskFn = bindTaskFn(repoContext.taskFn, taskParams);
289
277
  logger.success("任務準備完成", {
290
278
  topic: "task",
291
- context: {
279
+ data: {
292
280
  taskId,
293
281
  taskTypeKey,
294
282
  displayName: boundTaskFn.displayName,
@@ -307,6 +295,7 @@ const TaskManager = {
307
295
  prepareTask,
308
296
  bootstrapLocalTasks,
309
297
  getRegisteredLocalTaskTypeKeys,
298
+ clearBootstrapLocalTasksCache,
310
299
  };
311
300
 
312
301
  export { TaskManager };
@@ -0,0 +1,116 @@
1
+ import { createTarget, validateWithSchema } from "../core.api";
2
+ import { generateResponse, type SupabaseResponse } from "../core.interface";
3
+ import { createApiLogger, logManager } from "../shared/log";
4
+ import { z } from "zod";
5
+ import { CategoryTask, ResultCode, Task, TaskStatus } from "./task.interface";
6
+ import { TaskRepoValidation } from "./task-repo-validation";
7
+
8
+ const traceIdSchema = z.string().trim().min(1).optional();
9
+ const traceParentIdSchema = z.string().trim().min(1).nullable().optional();
10
+
11
+ /** Initial status for a new task — OPEN (draft) or TODO (claimable by workers). */
12
+ const postTaskInitialStatusSchema = z
13
+ .union([z.literal(TaskStatus.OPEN), z.literal(TaskStatus.TODO)])
14
+ .optional()
15
+ .default(TaskStatus.TODO);
16
+
17
+ export const postTaskSchema = z.object({
18
+ name: z.string().trim().min(1),
19
+ /** Task type key — matches `task.value`, {@link patchClaimTask}, {@link Repo.value}, and local Repo registry. */
20
+ value: z.string().trim().min(1),
21
+ params: z.unknown(),
22
+ taskStatus: postTaskInitialStatusSchema,
23
+ tagList: z.array(z.string()).optional().default([]),
24
+ extra: z.string().optional(),
25
+ traceId: traceIdSchema,
26
+ traceParentId: traceParentIdSchema,
27
+ });
28
+
29
+ export type PostTaskPayload = z.infer<typeof postTaskSchema>;
30
+
31
+ /**
32
+ * Create a task row (scheduler / admin). **Node entry only** — requires local Repo bootstrap.
33
+ *
34
+ * @see target-supabase-sdk/node
35
+ */
36
+ export const postTask = validateWithSchema(
37
+ postTaskSchema,
38
+ "postTaskSchema",
39
+ )(async ({
40
+ name,
41
+ value,
42
+ params,
43
+ taskStatus,
44
+ tagList,
45
+ extra,
46
+ traceId,
47
+ traceParentId,
48
+ }) => {
49
+ const taskTraceId = traceId?.trim() || logManager.generateTraceId();
50
+ const logger = createApiLogger("postTask", {
51
+ traceId: taskTraceId,
52
+ traceParentId: traceParentId ?? null,
53
+ });
54
+
55
+ const validation = await TaskRepoValidation.validate({
56
+ logger,
57
+ taskTypeKey: value,
58
+ params,
59
+ bootstrapLocal: true,
60
+ });
61
+ if (!validation.isValid) {
62
+ logger.warn(validation.message, {
63
+ topic: "task",
64
+ data: {
65
+ taskTypeKey: value,
66
+ reason: validation.reason,
67
+ step: validation.step,
68
+ },
69
+ });
70
+ return generateResponse.error(
71
+ validation.message,
72
+ undefined,
73
+ String(validation.code),
74
+ ) as SupabaseResponse<Task>;
75
+ }
76
+
77
+ const result = await createTarget<Task, PostTaskPayload>({
78
+ payload: {
79
+ name,
80
+ value,
81
+ params,
82
+ taskStatus,
83
+ tagList,
84
+ extra,
85
+ traceId: taskTraceId,
86
+ traceParentId,
87
+ },
88
+ createFn: () => ({
89
+ name,
90
+ value,
91
+ category: CategoryTask.TASK,
92
+ tagList,
93
+ extra,
94
+ details: {
95
+ manifestVersion: 0,
96
+ status: taskStatus,
97
+ params,
98
+ progress: 0,
99
+ nodeId: null,
100
+ traceId: taskTraceId,
101
+ },
102
+ }),
103
+ });
104
+
105
+ logger.info("任務已創建", {
106
+ topic: "task",
107
+ data: {
108
+ taskId: result.data?.id,
109
+ taskTypeKey: value,
110
+ status: taskStatus,
111
+ traceId: taskTraceId,
112
+ },
113
+ });
114
+
115
+ return result;
116
+ });
@@ -86,6 +86,8 @@ export interface BootstrapLocalTasksResult {
86
86
  registered: string[];
87
87
  skipped: { taskDir: string; reason: string }[];
88
88
  errors: { taskDir: string; error: string }[];
89
+ /** True when scan was skipped due to unchanged config fingerprint */
90
+ cached?: boolean;
89
91
  }
90
92
 
91
93
  /** Host app config directory (relative to `cwd`) — recommended for library consumers */