target-supabase-sdk 0.1.8 → 0.1.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.
Files changed (144) hide show
  1. package/README.md +8 -6
  2. package/dist/auth/auth.api.d.ts.map +1 -1
  3. package/dist/auth/auth.api.js +2 -1
  4. package/dist/auth/auth.api.js.map +1 -1
  5. package/dist/command/command.api.d.ts +43 -0
  6. package/dist/command/command.api.d.ts.map +1 -0
  7. package/dist/command/command.api.js +58 -0
  8. package/dist/command/command.api.js.map +1 -0
  9. package/dist/command/command.interface.d.ts +14 -0
  10. package/dist/command/command.interface.d.ts.map +1 -0
  11. package/dist/command/command.interface.js +9 -0
  12. package/dist/command/command.interface.js.map +1 -0
  13. package/dist/core.api.d.ts +70 -6
  14. package/dist/core.api.d.ts.map +1 -1
  15. package/dist/core.api.js +191 -13
  16. package/dist/core.api.js.map +1 -1
  17. package/dist/file/file-manager.d.ts +1 -1
  18. package/dist/file/file-manager.d.ts.map +1 -1
  19. package/dist/file/file-manager.js +1 -1
  20. package/dist/file/file-manager.js.map +1 -1
  21. package/dist/index.d.ts +9 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +8 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/node/node-manager.d.ts +48 -0
  26. package/dist/node/node-manager.d.ts.map +1 -0
  27. package/dist/node/node-manager.js +619 -0
  28. package/dist/node/node-manager.js.map +1 -0
  29. package/dist/node/node-task.preset.d.ts +1 -0
  30. package/dist/node/node-task.preset.d.ts.map +1 -0
  31. package/dist/node/node-task.preset.js +14 -0
  32. package/dist/node/node-task.preset.js.map +1 -0
  33. package/dist/node/node.api.d.ts +67 -31
  34. package/dist/node/node.api.d.ts.map +1 -1
  35. package/dist/node/node.api.js +65 -102
  36. package/dist/node/node.api.js.map +1 -1
  37. package/dist/node/node.interface.d.ts +7 -0
  38. package/dist/node/node.interface.d.ts.map +1 -1
  39. package/dist/node/node.interface.js +2 -0
  40. package/dist/node/node.interface.js.map +1 -1
  41. package/dist/node/node.utils.d.ts +5 -0
  42. package/dist/node/node.utils.d.ts.map +1 -0
  43. package/dist/node/node.utils.js +36 -0
  44. package/dist/node/node.utils.js.map +1 -0
  45. package/dist/parcel/parcel-manager.d.ts +1 -1
  46. package/dist/parcel/parcel-manager.d.ts.map +1 -1
  47. package/dist/parcel/parcel-manager.js +1 -1
  48. package/dist/parcel/parcel-manager.js.map +1 -1
  49. package/dist/repo/repo-context.utils.d.ts +24 -0
  50. package/dist/repo/repo-context.utils.d.ts.map +1 -0
  51. package/dist/repo/repo-context.utils.js +81 -0
  52. package/dist/repo/repo-context.utils.js.map +1 -0
  53. package/dist/repo/repo-manager.d.ts +31 -0
  54. package/dist/repo/repo-manager.d.ts.map +1 -0
  55. package/dist/repo/repo-manager.js +156 -0
  56. package/dist/repo/repo-manager.js.map +1 -0
  57. package/dist/repo/repo.api.d.ts +16 -0
  58. package/dist/repo/repo.api.d.ts.map +1 -0
  59. package/dist/repo/repo.api.js +40 -0
  60. package/dist/repo/repo.api.js.map +1 -0
  61. package/dist/repo/repo.interface.d.ts +12 -5
  62. package/dist/repo/repo.interface.d.ts.map +1 -1
  63. package/dist/repo/repo.interface.js +2 -2
  64. package/dist/repo/repo.interface.js.map +1 -1
  65. package/dist/repo/repo.script-loader.d.ts +19 -0
  66. package/dist/repo/repo.script-loader.d.ts.map +1 -0
  67. package/dist/repo/repo.script-loader.js +108 -0
  68. package/dist/repo/repo.script-loader.js.map +1 -0
  69. package/dist/shared/log/create-api-logger.d.ts +17 -0
  70. package/dist/shared/log/create-api-logger.d.ts.map +1 -0
  71. package/dist/shared/log/create-api-logger.js +15 -0
  72. package/dist/shared/log/create-api-logger.js.map +1 -0
  73. package/dist/shared/log/log-manager.d.ts +114 -0
  74. package/dist/shared/log/log-manager.d.ts.map +1 -0
  75. package/dist/shared/log/log-manager.js +218 -0
  76. package/dist/shared/log/log-manager.js.map +1 -0
  77. package/dist/shared/utils/config-path.utils.d.ts +38 -0
  78. package/dist/shared/utils/config-path.utils.d.ts.map +1 -0
  79. package/dist/shared/utils/config-path.utils.js +85 -0
  80. package/dist/shared/utils/config-path.utils.js.map +1 -0
  81. package/dist/shared/utils/error.utils.d.ts +11 -0
  82. package/dist/shared/utils/error.utils.d.ts.map +1 -0
  83. package/dist/shared/utils/error.utils.js +15 -0
  84. package/dist/shared/utils/error.utils.js.map +1 -0
  85. package/dist/shared/utils/id.utils.d.ts +3 -0
  86. package/dist/shared/utils/id.utils.d.ts.map +1 -0
  87. package/dist/shared/utils/id.utils.js +5 -0
  88. package/dist/shared/utils/id.utils.js.map +1 -0
  89. package/dist/supabase.js +1 -1
  90. package/dist/supabase.js.map +1 -1
  91. package/dist/task/local-task-registry.d.ts +27 -0
  92. package/dist/task/local-task-registry.d.ts.map +1 -0
  93. package/dist/task/local-task-registry.js +153 -0
  94. package/dist/task/local-task-registry.js.map +1 -0
  95. package/dist/task/task-manager.d.ts +52 -0
  96. package/dist/task/task-manager.d.ts.map +1 -0
  97. package/dist/task/task-manager.js +217 -0
  98. package/dist/task/task-manager.js.map +1 -0
  99. package/dist/task/task-repo-context.d.ts +95 -0
  100. package/dist/task/task-repo-context.d.ts.map +1 -0
  101. package/dist/task/task-repo-context.js +12 -0
  102. package/dist/task/task-repo-context.js.map +1 -0
  103. package/dist/task/task.api.d.ts +240 -0
  104. package/dist/task/task.api.d.ts.map +1 -1
  105. package/dist/task/task.api.js +350 -1
  106. package/dist/task/task.api.js.map +1 -1
  107. package/dist/task/task.interface.d.ts +9 -2
  108. package/dist/task/task.interface.d.ts.map +1 -1
  109. package/dist/task/task.interface.js +10 -0
  110. package/dist/task/task.interface.js.map +1 -1
  111. package/package.json +10 -3
  112. package/src/auth/auth.api.ts +3 -1
  113. package/src/command/command.api.ts +88 -0
  114. package/src/command/command.interface.ts +17 -0
  115. package/src/core.api.ts +305 -26
  116. package/src/file/file-manager.ts +1 -1
  117. package/src/index.ts +18 -3
  118. package/src/node/node-manager.ts +673 -0
  119. package/src/node/node-task.preset.ts +13 -0
  120. package/src/node/node.api.ts +95 -117
  121. package/src/node/node.interface.ts +8 -0
  122. package/src/node/node.utils.ts +36 -0
  123. package/src/parcel/parcel-manager.ts +1 -1
  124. package/src/repo/repo-context.utils.ts +111 -0
  125. package/src/repo/repo-manager.ts +217 -0
  126. package/src/repo/repo.api.ts +50 -0
  127. package/src/repo/repo.interface.ts +13 -5
  128. package/src/repo/repo.script-loader.ts +140 -0
  129. package/src/shared/log/create-api-logger.ts +25 -0
  130. package/src/shared/log/log-manager.ts +340 -0
  131. package/src/shared/utils/config-path.utils.ts +104 -0
  132. package/src/shared/utils/error.utils.ts +15 -0
  133. package/src/shared/utils/id.utils.ts +4 -0
  134. package/src/supabase.ts +1 -1
  135. package/src/task/local-task-registry.ts +195 -0
  136. package/src/task/task-manager.ts +312 -0
  137. package/src/task/task-repo-context.ts +103 -0
  138. package/src/task/task.api.ts +429 -0
  139. package/src/task/task.interface.ts +10 -2
  140. package/dist/task/task-node-manager.d.ts +0 -1
  141. package/dist/task/task-node-manager.d.ts.map +0 -1
  142. package/dist/task/task-node-manager.js +0 -2
  143. package/dist/task/task-node-manager.js.map +0 -1
  144. package/src/task/task-node-manager.ts +0 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Normalize unknown catch / rejection values to {@link Error}.
3
+ *
4
+ * `catch` and Promise rejections are typed `unknown` because `throw` can be
5
+ * anything (string, number, plain object). Call sites that need `.message`,
6
+ * re-throw, or `instanceof Error` checks should normalize first.
7
+ */
8
+ export function toError(value: unknown): Error {
9
+ return value instanceof Error ? value : new Error(String(value));
10
+ }
11
+
12
+ /** Error message safe for log `context` fields (JSON-serializable). */
13
+ export function getErrorMessage(value: unknown): string {
14
+ return toError(value).message;
15
+ }
@@ -0,0 +1,4 @@
1
+ /** RFC 4122 v4 UUID for trace / correlation ids. Requires Node 18+ (`engines` in package.json). */
2
+ export function generateUniqueId(): string {
3
+ return globalThis.crypto.randomUUID();
4
+ }
package/src/supabase.ts CHANGED
@@ -91,7 +91,7 @@ export class SupabaseInitializer {
91
91
  const authAnonKey = supabaseNeedAuthAnonKey;
92
92
 
93
93
  if (!mainUrl || !mainAnonKey) {
94
- throw new Error("Missing required Supabase environment variables: VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY");
94
+ throw new Error("Missing required Supabase environment variables: SUPABASE_URL, SUPABASE_ANON_KEY");
95
95
  }
96
96
 
97
97
  return {
@@ -0,0 +1,195 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import { RepoManager } from "../repo/repo-manager";
5
+ import {
6
+ importJsConfigModule,
7
+ pathExists,
8
+ resolveFirstExistingPath,
9
+ resolvePathFromBaseDir,
10
+ resolvePathFromConfigFile,
11
+ } from "../shared/utils/config-path.utils";
12
+ import {
13
+ BootstrapLocalTasksResult,
14
+ TASK_LOCAL_PACKAGE_CONFIG_FILENAME,
15
+ TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH,
16
+ TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH,
17
+ TaskLocalPackageConfig,
18
+ TaskRunnerRootConfig,
19
+ } from "./task-repo-context";
20
+
21
+ export interface BootstrapLocalTasksOptions {
22
+ /** Absolute or cwd-relative path to root config; if omitted, tries default paths under `cwd` */
23
+ rootConfigPath?: string;
24
+ /** Base directory for resolving root config (default `process.cwd()`) */
25
+ cwd?: string;
26
+ }
27
+
28
+ const EMPTY_RESULT: BootstrapLocalTasksResult = {
29
+ status: "not_configured",
30
+ registered: [],
31
+ skipped: [],
32
+ errors: [],
33
+ };
34
+
35
+ function parseRootConfig(raw: unknown, configPath: string): TaskRunnerRootConfig {
36
+ if (raw == null || typeof raw !== "object") {
37
+ throw new Error(`Root config must export an object: ${configPath}`);
38
+ }
39
+ const record = raw as Record<string, unknown>;
40
+ const taskDir = record.taskDir;
41
+ if (typeof taskDir !== "string" || taskDir.trim() === "") {
42
+ throw new Error(`Root config.taskDir must be a non-empty string: ${configPath}`);
43
+ }
44
+ return { taskDir: taskDir.trim() };
45
+ }
46
+
47
+ function parseTaskPackageConfig(raw: unknown, configPath: string): TaskLocalPackageConfig {
48
+ if (raw == null || typeof raw !== "object") {
49
+ throw new Error(`Task config must export an object: ${configPath}`);
50
+ }
51
+ const record = raw as Record<string, unknown>;
52
+ const taskTypeKey = record.taskTypeKey;
53
+ const entry = record.entry;
54
+
55
+ if (typeof taskTypeKey !== "string" || taskTypeKey.trim() === "") {
56
+ throw new Error(`taskTypeKey must be a non-empty string: ${configPath}`);
57
+ }
58
+ if (typeof entry !== "string" || entry.trim() === "") {
59
+ throw new Error(`entry must be a non-empty string: ${configPath}`);
60
+ }
61
+
62
+ return {
63
+ taskTypeKey: taskTypeKey.trim(),
64
+ entry: entry.trim(),
65
+ exportName: typeof record.exportName === "string" ? record.exportName : undefined,
66
+ displayName: typeof record.displayName === "string" ? record.displayName : undefined,
67
+ enabled: record.enabled === false ? false : true,
68
+ };
69
+ }
70
+
71
+ function finalizeStatus(result: Omit<BootstrapLocalTasksResult, "status">): BootstrapLocalTasksResult["status"] {
72
+ if (result.errors.length > 0 && result.registered.length === 0) {
73
+ return "failed";
74
+ }
75
+ if (result.registered.length === 0) {
76
+ return "empty";
77
+ }
78
+ return "loaded";
79
+ }
80
+
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 = {}
99
+ ): Promise<BootstrapLocalTasksResult> {
100
+ const cwd = options.cwd ?? process.cwd();
101
+
102
+ const rootConfigPath = await resolveFirstExistingPath(cwd, {
103
+ explicitPath: options.rootConfigPath,
104
+ candidatePaths: [
105
+ TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH,
106
+ TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH,
107
+ ],
108
+ });
109
+ if (rootConfigPath == null) {
110
+ return {
111
+ ...EMPTY_RESULT,
112
+ message: `No root config (tried ${TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH}, ${TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH})`,
113
+ };
114
+ }
115
+
116
+ const result: Omit<BootstrapLocalTasksResult, "status"> = {
117
+ registered: [],
118
+ skipped: [],
119
+ errors: [],
120
+ };
121
+
122
+ try {
123
+ const rootRaw = await importJsConfigModule(rootConfigPath);
124
+ const { taskDir } = parseRootConfig(rootRaw, rootConfigPath);
125
+
126
+ const tasksRoot = resolvePathFromConfigFile(rootConfigPath, taskDir);
127
+ if (!(await pathExists(tasksRoot))) {
128
+ return {
129
+ status: "failed",
130
+ message: `taskDir does not exist: ${tasksRoot}`,
131
+ ...result,
132
+ };
133
+ }
134
+
135
+ const entries = await readdir(tasksRoot, { withFileTypes: true });
136
+ for (const entry of entries) {
137
+ if (!entry.isDirectory()) {
138
+ continue;
139
+ }
140
+
141
+ const taskPackageDir = join(tasksRoot, entry.name);
142
+ const taskConfigPath = join(taskPackageDir, TASK_LOCAL_PACKAGE_CONFIG_FILENAME);
143
+
144
+ if (!(await pathExists(taskConfigPath))) {
145
+ result.skipped.push({ taskDir: taskPackageDir, reason: "missing task.config.js" });
146
+ continue;
147
+ }
148
+
149
+ try {
150
+ const taskRaw = await importJsConfigModule(taskConfigPath);
151
+ const taskConfig = parseTaskPackageConfig(taskRaw, taskConfigPath);
152
+
153
+ if (taskConfig.enabled === false) {
154
+ result.skipped.push({ taskDir: taskPackageDir, reason: "disabled in task.config.js" });
155
+ continue;
156
+ }
157
+
158
+ const entryPath = resolvePathFromBaseDir(taskPackageDir, taskConfig.entry);
159
+ if (!(await pathExists(entryPath))) {
160
+ throw new Error(`entry module not found: ${entryPath}`);
161
+ }
162
+
163
+ RepoManager.registerLocalModule(taskConfig.taskTypeKey, entryPath, taskConfig.exportName);
164
+ result.registered.push(taskConfig.taskTypeKey);
165
+ } catch (error) {
166
+ result.errors.push({
167
+ taskDir: taskPackageDir,
168
+ error: error instanceof Error ? error.message : String(error),
169
+ });
170
+ }
171
+ }
172
+
173
+ const status = finalizeStatus(result);
174
+ return {
175
+ status,
176
+ message:
177
+ status === "empty"
178
+ ? `Root config loaded but no tasks registered under ${tasksRoot}`
179
+ : undefined,
180
+ ...result,
181
+ };
182
+ } catch (error) {
183
+ return {
184
+ status: "failed",
185
+ message: error instanceof Error ? error.message : String(error),
186
+ ...result,
187
+ };
188
+ }
189
+ }
190
+
191
+ export function getRegisteredLocalTaskTypeKeys(): string[] {
192
+ return RepoManager.getRegisteredLocalTaskTypeKeys();
193
+ }
194
+
195
+ export { TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH };
@@ -0,0 +1,312 @@
1
+ import { RepoManager } from "../repo/repo-manager";
2
+ import { getScanRemoteRepoValues } from "../repo/repo.api";
3
+ import { TASK_REPO_USAGE } from "../repo/repo.interface";
4
+ import { LoggerWithContext } from "../shared/log/log-manager";
5
+ import { ResultCode, Task } from "./task.interface";
6
+ import { TaskFn, TaskRepoContext, TaskRunResult, ExecutableTaskFn } from "./task-repo-context";
7
+
8
+ export type {
9
+ TaskFn,
10
+ TaskRepoContext,
11
+ TaskRunResult,
12
+ ExecutableTaskFn,
13
+ TaskRepoScriptDetails,
14
+ TaskRepoScriptRecord,
15
+ TaskRunnerRootConfig,
16
+ TaskLocalPackageConfig,
17
+ BootstrapLocalTasksResult,
18
+ BootstrapLocalTasksStatus,
19
+ } from "./task-repo-context";
20
+ export {
21
+ TASK_REPO_SCRIPT_CATEGORY,
22
+ TASK_RUNNER_CONFIG_DIR,
23
+ TASK_RUNNER_ROOT_CONFIG_FILENAME,
24
+ TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH,
25
+ TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH,
26
+ TASK_LOCAL_PACKAGE_CONFIG_FILENAME,
27
+ } from "./task-repo-context";
28
+ import {
29
+ bootstrapLocalTasks,
30
+ BootstrapLocalTasksOptions,
31
+ getRegisteredLocalTaskTypeKeys,
32
+ } from "./local-task-registry";
33
+ import type { BootstrapLocalTasksResult } from "./task-repo-context";
34
+
35
+ export type PrepareTaskFailureReason =
36
+ | "MISSING_PARAMS"
37
+ | "REPO_LOAD_FAILED"
38
+ | "PARAMS_VALIDATION_FAILED"
39
+ | "TASK_TYPE_KEY_MISMATCH";
40
+
41
+ export interface PrepareTaskResponse {
42
+ isSuccess: boolean;
43
+ taskFn: ExecutableTaskFn | null;
44
+ code?: ResultCode;
45
+ message?: string;
46
+ reason?: PrepareTaskFailureReason;
47
+ /** Pipeline step for log aggregation */
48
+ step?: string;
49
+ }
50
+
51
+ export interface PrepareTaskParams {
52
+ logger: LoggerWithContext;
53
+ task: Task;
54
+ }
55
+
56
+ export interface RegisterTasksOptions {
57
+ logger: LoggerWithContext;
58
+ local?: BootstrapLocalTasksOptions;
59
+ /** When `true` (default), union local keys with remote `Repo.value` from Supabase. */
60
+ includeRemote?: boolean;
61
+ }
62
+
63
+ export interface RegisterTasksResult {
64
+ availableTaskList: string[];
65
+ local: BootstrapLocalTasksResult;
66
+ remote: { values: string[] };
67
+ }
68
+
69
+ function logLocalBootstrap(logger: LoggerWithContext, bootstrap: BootstrapLocalTasksResult): void {
70
+ switch (bootstrap.status) {
71
+ case "not_configured":
72
+ logger.info("未找到本地任務配置,跳過本地註冊", {
73
+ topic: "task",
74
+ context: { message: bootstrap.message },
75
+ });
76
+ break;
77
+ case "failed":
78
+ logger.warn("本地任務註冊失敗", {
79
+ topic: "task",
80
+ context: { message: bootstrap.message, errors: bootstrap.errors },
81
+ });
82
+ break;
83
+ case "empty":
84
+ logger.info("本地任務掃描完成,無已註冊任務", {
85
+ topic: "task",
86
+ context: { message: bootstrap.message, skipped: bootstrap.skipped },
87
+ });
88
+ break;
89
+ case "loaded":
90
+ logger.info("本地任務註冊完成", {
91
+ topic: "task",
92
+ context: {
93
+ registered: bootstrap.registered,
94
+ skipped: bootstrap.skipped,
95
+ errors: bootstrap.errors,
96
+ },
97
+ });
98
+ break;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Worker startup: discover local task packages + remote Repo rows, merge into `availableTaskList`.
104
+ *
105
+ * - Local: {@link bootstrapLocalTasks} → registers modules in {@link RepoManager}
106
+ * - Remote: {@link getScanRemoteRepoValues} with {@link TASK_REPO_USAGE}
107
+ * - Merge: union of both; execution still prefers local registry in {@link RepoManager.getRepoContext}
108
+ *
109
+ * @throws When merged list is empty (no task types to claim).
110
+ */
111
+ async function registerTasks({
112
+ logger,
113
+ local: localOptions,
114
+ includeRemote = true,
115
+ }: RegisterTasksOptions): Promise<RegisterTasksResult> {
116
+ logger.info("開始註冊任務(本地 + 遠程)", { topic: "task" });
117
+
118
+ const local = await bootstrapLocalTasks(localOptions ?? {});
119
+ logLocalBootstrap(logger, local);
120
+
121
+ let remoteValues: string[] = [];
122
+ if (includeRemote) {
123
+ logger.info("查詢遠程 Repo 列表", {
124
+ topic: "task",
125
+ context: { usage: TASK_REPO_USAGE },
126
+ });
127
+ const { data, error: remoteError } = await getScanRemoteRepoValues({ usage: TASK_REPO_USAGE });
128
+ if (remoteError) {
129
+ throw new Error(remoteError.message);
130
+ }
131
+ remoteValues = data ?? [];
132
+ logger.info("遠程 Repo 查詢完成", {
133
+ topic: "task",
134
+ context: { count: remoteValues.length, values: remoteValues },
135
+ });
136
+ }
137
+
138
+ const availableTaskList = [...new Set([...local.registered, ...remoteValues])];
139
+ if (availableTaskList.length === 0) {
140
+ throw new Error("[registerTasks] 任務註冊失敗:本地與遠程均無可用任務類型");
141
+ }
142
+
143
+ logger.success("任務註冊完成", {
144
+ topic: "task",
145
+ context: {
146
+ availableTaskList,
147
+ localRegistered: local.registered,
148
+ remoteValues,
149
+ },
150
+ });
151
+
152
+ return {
153
+ availableTaskList,
154
+ local,
155
+ remote: { values: remoteValues },
156
+ };
157
+ }
158
+
159
+ /** 将 taskFn 与 params 闭包绑定,供 NodeManager 无参调用 `await taskFn()` */
160
+ function bindTaskFn(taskFn: TaskFn, taskParams: unknown): ExecutableTaskFn {
161
+ const run = async () => taskFn(taskParams);
162
+ return Object.assign(run, {
163
+ displayName: taskFn.displayName,
164
+ taskTypeKey: taskFn.taskTypeKey,
165
+ });
166
+ }
167
+
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
+ /**
186
+ * 將 Supabase 認領到的 Task 解析為可執行的 `ExecutableTaskFn`。
187
+ *
188
+ * 流程:
189
+ * 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,返回閉包後的無參執行函數
194
+ *
195
+ * 失敗時返回 `{ isSuccess: false, code, message, reason, step }`(不 throw)。
196
+ */
197
+ const prepareTask = async ({ logger, task }: PrepareTaskParams): Promise<PrepareTaskResponse> => {
198
+ const fail = (params: {
199
+ reason: PrepareTaskFailureReason;
200
+ code: ResultCode;
201
+ message: string;
202
+ step: string;
203
+ }): PrepareTaskResponse => {
204
+ logger.warn(params.message, {
205
+ topic: "task",
206
+ context: {
207
+ taskId: task.id,
208
+ taskTypeKey: task.value,
209
+ reason: params.reason,
210
+ step: params.step,
211
+ },
212
+ });
213
+ return {
214
+ isSuccess: false,
215
+ taskFn: null,
216
+ code: params.code,
217
+ message: params.message,
218
+ reason: params.reason,
219
+ step: params.step,
220
+ };
221
+ };
222
+
223
+ const { id: taskId, name: taskName, value: taskTypeKey, details } = task;
224
+ logger.info(`開始準備任務 ${taskName}-${taskTypeKey}`, {
225
+ topic: "task",
226
+ context: { taskId, taskName, taskTypeKey },
227
+ });
228
+
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",
236
+ });
237
+ }
238
+
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>({
245
+ logger,
246
+ taskTypeKey,
247
+ });
248
+
249
+ if (repoContext == null) {
250
+ return fail({
251
+ reason: "REPO_LOAD_FAILED",
252
+ code: ResultCode.REPO_NOT_VALID,
253
+ message: repoError ?? "無法加載 Repo 上下文",
254
+ step: "repo_load",
255
+ });
256
+ }
257
+
258
+ logger.info("Repo 上下文加載成功", {
259
+ topic: "task",
260
+ context: {
261
+ taskTypeKey,
262
+ taskFnDisplayName: repoContext.taskFn.displayName,
263
+ step: "repo_load",
264
+ },
265
+ });
266
+
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
+ const boundTaskFn = bindTaskFn(repoContext.taskFn, taskParams);
289
+ logger.success("任務準備完成", {
290
+ topic: "task",
291
+ context: {
292
+ taskId,
293
+ taskTypeKey,
294
+ displayName: boundTaskFn.displayName,
295
+ step: "bind",
296
+ },
297
+ });
298
+
299
+ return {
300
+ isSuccess: true,
301
+ taskFn: boundTaskFn,
302
+ };
303
+ };
304
+
305
+ const TaskManager = {
306
+ registerTasks,
307
+ prepareTask,
308
+ bootstrapLocalTasks,
309
+ getRegisteredLocalTaskTypeKeys,
310
+ };
311
+
312
+ export { TaskManager };
@@ -0,0 +1,103 @@
1
+ export interface TaskRunResult {
2
+ isSuccess: boolean;
3
+ cost: number;
4
+ extra: unknown;
5
+ }
6
+
7
+ export interface TaskFn {
8
+ displayName: string;
9
+ taskTypeKey: string;
10
+ (taskParams: unknown): Promise<TaskRunResult>;
11
+ }
12
+
13
+ /** Params already bound — used by node executor after prepareTask */
14
+ export type ExecutableTaskFn = (() => Promise<TaskRunResult>) & {
15
+ displayName: string;
16
+ taskTypeKey: string;
17
+ };
18
+
19
+ /** Repo script module must expose this shape (see repo-context.utils). */
20
+ export interface TaskRepoContext {
21
+ taskParamsValidator: (taskParams: unknown) => boolean;
22
+ taskFn: TaskFn;
23
+ }
24
+
25
+ /** Supabase `target.category` for rows that store task script payloads */
26
+ export const TASK_REPO_SCRIPT_CATEGORY = "script" as const;
27
+
28
+ /**
29
+ * `target.details` for task script rows — not a separate Target subtype in repo.interface.
30
+ * Loaded dynamically to produce {@link TaskRepoContext}.
31
+ */
32
+ export interface TaskRepoScriptDetails {
33
+ manifestVersion: number;
34
+ /** Inline ESM source — written to temp file then dynamically imported (Node) */
35
+ source?: string;
36
+ /** Absolute path to an ESM module on the worker machine */
37
+ modulePath?: string;
38
+ /** Named export to read TaskRepoContext from; default export if omitted */
39
+ exportName?: string;
40
+ /** Content hash for cache invalidation */
41
+ hash?: string;
42
+ /** Prefer this script as entry when multiple scripts exist for one repo */
43
+ isEntry?: boolean;
44
+ }
45
+
46
+ /** Minimal target row shape used by script loader (avoids a dedicated Script Target type). */
47
+ export interface TaskRepoScriptRecord {
48
+ id: string;
49
+ value: string;
50
+ details: TaskRepoScriptDetails;
51
+ }
52
+
53
+ /** Host app root config at `config/task.config.js` — where to discover local task packages */
54
+ export interface TaskRunnerRootConfig {
55
+ /** Directory containing one folder per task (relative to root config file), e.g. `"./tasks"` */
56
+ taskDir: string;
57
+ }
58
+
59
+ /** Per-task `task.config.js` inside `<taskDir>/<name>/` */
60
+ export interface TaskLocalPackageConfig {
61
+ /** Must match `task.value` when claiming from Supabase */
62
+ taskTypeKey: string;
63
+ /** Entry module relative to this task folder, e.g. `"./index.mjs"` */
64
+ entry: string;
65
+ /** Named export on entry module; default export if omitted */
66
+ exportName?: string;
67
+ displayName?: string;
68
+ /** Skip registration when `false` (default `true`) */
69
+ enabled?: boolean;
70
+ }
71
+
72
+ export type BootstrapLocalTasksStatus =
73
+ /** Root config found and scan finished (individual tasks may still have errors) */
74
+ | "loaded"
75
+ /** Root config found but zero tasks registered */
76
+ | "empty"
77
+ /** No root config at default/explicit paths — not an error; use remote scripts or manual register */
78
+ | "not_configured"
79
+ /** Root config or taskDir invalid */
80
+ | "failed";
81
+
82
+ export interface BootstrapLocalTasksResult {
83
+ status: BootstrapLocalTasksStatus;
84
+ /** Human-readable summary for logs */
85
+ message?: string;
86
+ registered: string[];
87
+ skipped: { taskDir: string; reason: string }[];
88
+ errors: { taskDir: string; error: string }[];
89
+ }
90
+
91
+ /** Host app config directory (relative to `cwd`) — recommended for library consumers */
92
+ export const TASK_RUNNER_CONFIG_DIR = "config" as const;
93
+
94
+ export const TASK_RUNNER_ROOT_CONFIG_FILENAME = "task.config.js" as const;
95
+
96
+ /** Default root config path: `config/task.config.js` */
97
+ export const TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH = `${TASK_RUNNER_CONFIG_DIR}/${TASK_RUNNER_ROOT_CONFIG_FILENAME}` as const;
98
+
99
+ /** Legacy fallback at project root */
100
+ export const TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH = TASK_RUNNER_ROOT_CONFIG_FILENAME;
101
+
102
+ /** Per-task config filename inside each folder under `taskDir` */
103
+ export const TASK_LOCAL_PACKAGE_CONFIG_FILENAME = "task.config.js" as const;