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,50 @@
1
+ import { scanTargetList, validateWithSchema } from "../core.api";
2
+ import { generateResponse, SupabaseResponse } from "../core.interface";
3
+ import { z } from "zod";
4
+ import { CategoryRepo, Repo } from "./repo.interface";
5
+
6
+ const REPO_USAGE_FIELD = "details->>usage" as const;
7
+
8
+ export const getScanRemoteRepoValuesSchema = z.object({
9
+ usage: z.string().trim().min(1),
10
+ });
11
+
12
+ export type GetScanRemoteRepoValuesPayload = z.infer<typeof getScanRemoteRepoValuesSchema>;
13
+
14
+ /**
15
+ * Scan `Repo.value` keys for `category=repo` with matching `details.usage`.
16
+ * Returns `{ data: string[] }` on success; validation and DB failures use `{ error }` (never throws).
17
+ */
18
+ export const getScanRemoteRepoValues = async (
19
+ payload: GetScanRemoteRepoValuesPayload
20
+ ): Promise<SupabaseResponse<string[]>> => {
21
+ try {
22
+ return await getScanRemoteRepoValuesValidated(payload);
23
+ } catch (error) {
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ return generateResponse.error(message) as SupabaseResponse<string[]>;
26
+ }
27
+ };
28
+
29
+ const getScanRemoteRepoValuesValidated = validateWithSchema(
30
+ getScanRemoteRepoValuesSchema,
31
+ "getScanRemoteRepoValuesSchema"
32
+ )(async ({ usage }): Promise<SupabaseResponse<string[]>> => {
33
+ try {
34
+ const { data } = await scanTargetList<Repo>({
35
+ category: CategoryRepo.REPO,
36
+ selectFields: "value",
37
+ orderBy: { field: "value", ascending: true },
38
+ filterList: [{ field: REPO_USAGE_FIELD, operator: "eq", value: usage }],
39
+ });
40
+
41
+ const values = (data ?? [])
42
+ .map((row) => row.value?.trim())
43
+ .filter((value): value is string => value != null && value !== "");
44
+
45
+ return generateResponse.success([...new Set(values)]);
46
+ } catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ return generateResponse.error(message) as SupabaseResponse<string[]>;
49
+ }
50
+ });
@@ -3,13 +3,15 @@ import { Target } from "../core.interface";
3
3
  export enum CategoryRepo {
4
4
  /** Project */
5
5
  REPO = "repo",
6
- SCRIPT = "script",
7
6
  }
8
7
 
8
+ /** `Repo.details.usage` for task worker remote discovery ({@link registerTasks}). */
9
+ export const TASK_REPO_USAGE = "task";
10
+
9
11
  export interface Repo extends Target {
10
- /** Key */
12
+ /** Human readable name */
11
13
  name: string;
12
- /** Url */
14
+ /** Unique key, consumers use this to determine which repo to use */
13
15
  value: string;
14
16
  details: RepoDetails;
15
17
  category: CategoryRepo;
@@ -17,7 +19,13 @@ export interface Repo extends Target {
17
19
 
18
20
  export interface RepoDetails {
19
21
  manifestVersion: number;
20
- context?: unknown;
22
+ /**
23
+ * Business usage partition for discovery and load (caller-defined string, e.g. `"task"`).
24
+ * Used by {@link getScanRemoteRepoValues} to narrow scan scope; validity is the caller's responsibility.
25
+ */
26
+ usage: string;
27
+ /** Repo entry module URL or filesystem path — used to dynamic-import TaskRepoContext. */
28
+ url: string;
21
29
  hash: string;
22
- };
30
+ }
23
31
 
@@ -0,0 +1,140 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ import { LoggerWithContext } from "../shared/log/log-manager";
7
+ import { resolvePathFromCwd, toFileImportHref } from "../shared/utils/config-path.utils";
8
+ import { TaskRepoScriptRecord } from "../task/task-repo-context";
9
+ import {
10
+ getScriptLoadKey,
11
+ LoadedRepoContext,
12
+ normalizeRepoContextModule,
13
+ } from "./repo-context.utils";
14
+
15
+ const importedModuleCache = new Map<string, LoadedRepoContext>();
16
+
17
+ async function importFromFilePath(modulePath: string): Promise<unknown> {
18
+ return import(toFileImportHref(modulePath));
19
+ }
20
+
21
+ /** Resolve repo `details.url` to a value suitable for dynamic `import()`. */
22
+ export function resolveRepoEntryHref(url: string, cwd = process.cwd()): string {
23
+ const trimmed = url.trim();
24
+ if (trimmed.includes("://")) {
25
+ return trimmed;
26
+ }
27
+ return toFileImportHref(resolvePathFromCwd(cwd, trimmed));
28
+ }
29
+
30
+ async function importFromSource(source: string, cacheKey: string): Promise<unknown> {
31
+ const hash = createHash("sha256").update(source).digest("hex").slice(0, 16);
32
+ const dir = join(tmpdir(), "target-supabase-sdk", "repo-scripts");
33
+ await mkdir(dir, { recursive: true });
34
+ const filePath = join(dir, `${cacheKey}-${hash}.mjs`);
35
+ await writeFile(filePath, source, "utf8");
36
+ return importFromFilePath(filePath);
37
+ }
38
+
39
+ export async function loadRepoContextFromScript({
40
+ logger,
41
+ script,
42
+ taskTypeKey,
43
+ }: {
44
+ logger: LoggerWithContext;
45
+ script: TaskRepoScriptRecord;
46
+ taskTypeKey: string;
47
+ }): Promise<LoadedRepoContext | null> {
48
+ const loadKey = getScriptLoadKey(script);
49
+ const cacheKey = `${taskTypeKey}@${loadKey}`;
50
+ const cached = importedModuleCache.get(cacheKey);
51
+ if (cached != null) {
52
+ logger.info("命中 Repo 脚本模块缓存", { topic: "repo", context: { cacheKey } });
53
+ return cached;
54
+ }
55
+
56
+ const { source, modulePath, exportName } = script.details;
57
+
58
+ try {
59
+ let moduleExports: unknown;
60
+ if (source != null && source.trim() !== "") {
61
+ logger.info("从 source 动态加载 Repo 脚本", { topic: "repo", context: { scriptId: script.id } });
62
+ moduleExports = await importFromSource(source, cacheKey);
63
+ } else if (modulePath != null && modulePath.trim() !== "") {
64
+ logger.info("从 modulePath 动态加载 Repo 脚本", {
65
+ topic: "repo",
66
+ context: { scriptId: script.id, modulePath },
67
+ });
68
+ moduleExports = await importFromFilePath(modulePath);
69
+ } else {
70
+ logger.warn("脚本缺少 source 或 modulePath", { topic: "repo", context: { scriptId: script.id } });
71
+ return null;
72
+ }
73
+
74
+ const context = normalizeRepoContextModule(moduleExports, taskTypeKey, exportName);
75
+ if (context == null) {
76
+ logger.warn("脚本模块未导出合法的 TaskRepoContext", {
77
+ topic: "repo",
78
+ context: { scriptId: script.id, exportName: exportName ?? "default" },
79
+ });
80
+ return null;
81
+ }
82
+
83
+ const loaded: LoadedRepoContext = { context, contentHash: loadKey };
84
+ importedModuleCache.set(cacheKey, loaded);
85
+ return loaded;
86
+ } catch (error) {
87
+ logger.error("动态加载 Repo 脚本失败", {
88
+ topic: "repo",
89
+ context: { scriptId: script.id, error: error instanceof Error ? error.message : error },
90
+ });
91
+ return null;
92
+ }
93
+ }
94
+
95
+ export async function loadRepoContextFromUrl({
96
+ logger,
97
+ url,
98
+ taskTypeKey,
99
+ exportName,
100
+ }: {
101
+ logger: LoggerWithContext;
102
+ url: string;
103
+ taskTypeKey: string;
104
+ exportName?: string;
105
+ }): Promise<LoadedRepoContext | null> {
106
+ const href = resolveRepoEntryHref(url);
107
+ const cacheKey = `${taskTypeKey}@url:${href}`;
108
+ const cached = importedModuleCache.get(cacheKey);
109
+ if (cached != null) {
110
+ logger.info("命中 Repo URL 模块缓存", { topic: "repo", context: { cacheKey } });
111
+ return cached;
112
+ }
113
+
114
+ try {
115
+ logger.info("从 Repo URL 动态加载脚本", { topic: "repo", context: { url, href } });
116
+ const moduleExports = await import(href);
117
+ const context = normalizeRepoContextModule(moduleExports, taskTypeKey, exportName);
118
+ if (context == null) {
119
+ logger.warn("Repo URL 模块未导出合法的 TaskRepoContext", {
120
+ topic: "repo",
121
+ context: { url, exportName: exportName ?? "default" },
122
+ });
123
+ return null;
124
+ }
125
+ const loaded: LoadedRepoContext = { context, contentHash: cacheKey };
126
+ importedModuleCache.set(cacheKey, loaded);
127
+ return loaded;
128
+ } catch (error) {
129
+ logger.error("从 Repo URL 动态加载失败", {
130
+ topic: "repo",
131
+ context: { url, error: error instanceof Error ? error.message : error },
132
+ });
133
+ return null;
134
+ }
135
+ }
136
+
137
+ /** Clear in-memory imported module cache (tests / hot reload). */
138
+ export function clearRepoScriptModuleCache(): void {
139
+ importedModuleCache.clear();
140
+ }
@@ -0,0 +1,25 @@
1
+ import { logManager, type LogContext, type LoggerWithContext } from "./log-manager";
2
+
3
+ /**
4
+ * Options for {@link createApiLogger}.
5
+ *
6
+ * - **Core:** optional `traceId` (generated when omitted)
7
+ * - **Business:** any other {@link LogContext} field (e.g. `nodeId`)
8
+ */
9
+ export type CreateApiLoggerOptions = {
10
+ traceId?: string;
11
+ } & Partial<Omit<LogContext, "module" | "traceId">>;
12
+
13
+ /**
14
+ * Logger for one SDK API invocation.
15
+ *
16
+ * Binds required `module` + optional caller `traceId`; business fields (e.g. `nodeId`) are optional.
17
+ */
18
+ export function createApiLogger(module: string, options?: CreateApiLoggerOptions): LoggerWithContext {
19
+ const { traceId, ...businessFields } = options ?? {};
20
+ return logManager.withContext({
21
+ module,
22
+ traceId: traceId ?? logManager.generateTraceId(),
23
+ ...businessFields,
24
+ }).logger;
25
+ }
@@ -0,0 +1,340 @@
1
+ import { isDevEnvironment } from "../../core.utils";
2
+ import { generateUniqueId } from "../utils/id.utils";
3
+
4
+ /**
5
+ * Log severity and semantics (low → high). `minLevel` filters by {@link LOG_LEVEL_RANK}.
6
+ *
7
+ * | Level | Rank | When to use |
8
+ * |----------|------|-------------|
9
+ * | DEBUG | 0 | Diagnostic detail for dev/troubleshooting; noisy, safe to drop in prod. |
10
+ * | INFO | 1 | Normal operational flow (startup, polling, state transitions). |
11
+ * | SUCCESS | 2 | Positive milestone within a flow (not a separate severity axis — same band as INFO). |
12
+ * | WARN | 3 | Recoverable anomaly or degradation; work continues but worth watching. |
13
+ * | ERROR | 4 | A concrete operation failed (API, task step, I/O); investigate and retry/fix locally. |
14
+ * | CRITICAL | 5 | Invariant violated — design flaw, data-integrity risk, or business rule broken; escalate and fix urgently. |
15
+ */
16
+ enum LogLevel {
17
+ DEBUG = "DEBUG",
18
+ INFO = "INFO",
19
+ SUCCESS = "SUCCESS",
20
+ WARN = "WARN",
21
+ ERROR = "ERROR",
22
+ CRITICAL = "CRITICAL",
23
+ }
24
+
25
+ const LOG_LEVEL_RANK: Record<LogLevel, number> = {
26
+ [LogLevel.DEBUG]: 0,
27
+ [LogLevel.INFO]: 1,
28
+ [LogLevel.SUCCESS]: 2,
29
+ [LogLevel.WARN]: 3,
30
+ [LogLevel.ERROR]: 4,
31
+ [LogLevel.CRITICAL]: 5,
32
+ };
33
+
34
+ interface LogEntry {
35
+ timestamp: number;
36
+ level: LogLevel;
37
+ /** Tell people what happened, but more structured */
38
+ message: string;
39
+ /** Category of a kind of log */
40
+ topic: string | null;
41
+ /** Where the log comes from */
42
+ module: string;
43
+ /** Specific process — correlates one logical chain (loop, request). */
44
+ traceId: string;
45
+ /** Worker node id when the log is node-scoped; omit for scheduler / admin / API-only paths. */
46
+ nodeId?: string;
47
+ extra?: string;
48
+ context?: unknown;
49
+ }
50
+
51
+ interface LogContext {
52
+ module: LogEntry["module"];
53
+ traceId: LogEntry["traceId"];
54
+ nodeId?: LogEntry["nodeId"];
55
+ }
56
+
57
+ type LogRestParams = Omit<LogParams, "message">;
58
+
59
+ interface LogEntryPayload {
60
+ message: LogEntry["message"];
61
+ topic?: LogEntry["topic"];
62
+ extra?: LogEntry["extra"];
63
+ context?: LogEntry["context"];
64
+ level: LogEntry["level"];
65
+ module: LogEntry["module"];
66
+ traceId: LogEntry["traceId"];
67
+ nodeId?: LogEntry["nodeId"];
68
+ }
69
+
70
+ type LogParams = Pick<LogEntryPayload, "message" | "topic" | "extra" | "context">;
71
+
72
+ interface LogOptions {
73
+ defaultTopic: LogEntry["topic"];
74
+ formatTimestamp: boolean;
75
+ formatEmoji: boolean;
76
+ formatPrefix: string;
77
+ maxHistoryLength: number;
78
+ /** Logs below this level are skipped. Defaults to DEBUG in dev, INFO in production. */
79
+ minLevel?: LogLevel;
80
+ onLog?: (entry: LogEntry) => void;
81
+ }
82
+
83
+ const DEFAULT_OPTIONS: LogOptions = {
84
+ defaultTopic: null,
85
+ formatTimestamp: true,
86
+ formatEmoji: true,
87
+ formatPrefix: "",
88
+ maxHistoryLength: 4096,
89
+ minLevel: isDevEnvironment() ? LogLevel.DEBUG : LogLevel.INFO,
90
+ };
91
+
92
+ interface LoggerWithContext {
93
+ /** Rank 0 — diagnostic detail; dev/troubleshooting only. */
94
+ debug: (message: LogParams["message"], restParams?: LogRestParams) => void;
95
+ /** Rank 1 — normal operational flow. */
96
+ info: (message: LogParams["message"], restParams?: LogRestParams) => void;
97
+ /** Rank 2 — positive milestone (semantic INFO, not higher severity). */
98
+ success: (message: LogParams["message"], restParams?: LogRestParams) => void;
99
+ /** Rank 3 — recoverable anomaly; system continues. */
100
+ warn: (message: LogParams["message"], restParams?: LogRestParams) => void;
101
+ /** Rank 4 — a concrete operation failed; needs investigation. */
102
+ error: (message: LogParams["message"], restParams?: LogRestParams) => void;
103
+ /** Rank 5 — invariant / design / integrity breach; escalate immediately. */
104
+ critical: (message: LogParams["message"], restParams?: LogRestParams) => void;
105
+ /** Merge partial fields into this logger's bound context (e.g. nodeId after register). */
106
+ resetContext: (fields: Partial<LogContext>) => void;
107
+ }
108
+
109
+ function mergeLogOptions(options?: Partial<LogOptions>): LogOptions {
110
+ return { ...DEFAULT_OPTIONS, ...options };
111
+ }
112
+
113
+ function formatContextValue(value: unknown): string {
114
+ if (value === undefined) {
115
+ return "";
116
+ }
117
+ if (typeof value === "string") {
118
+ return value;
119
+ }
120
+ try {
121
+ return JSON.stringify(value);
122
+ } catch {
123
+ return String(value);
124
+ }
125
+ }
126
+
127
+ function validateLogContext(logContext: LogContext): void {
128
+ const missing: string[] = [];
129
+ if (!logContext.module?.trim()) {
130
+ missing.push("module");
131
+ }
132
+ if (!logContext.traceId?.trim()) {
133
+ missing.push("traceId");
134
+ }
135
+ if (missing.length > 0) {
136
+ console.warn(`[LogManager] LogContext missing or empty: ${missing.join(", ")}`, logContext);
137
+ }
138
+ }
139
+
140
+ class LogManager {
141
+ private static instance: LogManager;
142
+ private options: LogOptions;
143
+ private history: LogEntry[] = [];
144
+ private readonly levelEmojis = {
145
+ [LogLevel.DEBUG]: "🔍",
146
+ [LogLevel.INFO]: "ℹ️",
147
+ [LogLevel.SUCCESS]: "✅",
148
+ [LogLevel.WARN]: "⚠️",
149
+ [LogLevel.ERROR]: "❌",
150
+ [LogLevel.CRITICAL]: "💀",
151
+ };
152
+
153
+ private constructor(options?: Partial<LogOptions>) {
154
+ this.options = mergeLogOptions(options);
155
+ }
156
+
157
+ public static getInstance(options?: Partial<LogOptions>): LogManager {
158
+ if (!LogManager.instance) {
159
+ LogManager.instance = new LogManager(options);
160
+ } else if (options != null && Object.keys(options).length > 0) {
161
+ console.warn("[LogManager] getInstance(options) ignored — singleton already created. Use setOptions() instead.");
162
+ }
163
+ return LogManager.instance;
164
+ }
165
+
166
+ public setOptions(options: Partial<LogOptions>): void {
167
+ this.options = mergeLogOptions({ ...this.options, ...options });
168
+ }
169
+
170
+ public getHistory(): LogEntry[] {
171
+ return [...this.history];
172
+ }
173
+
174
+ public clearHistory(): void {
175
+ this.history = [];
176
+ }
177
+
178
+ private getMinLevel(): LogLevel {
179
+ return this.options.minLevel ?? DEFAULT_OPTIONS.minLevel ?? LogLevel.INFO;
180
+ }
181
+
182
+ private shouldLog(level: LogLevel): boolean {
183
+ return LOG_LEVEL_RANK[level] >= LOG_LEVEL_RANK[this.getMinLevel()];
184
+ }
185
+
186
+ private createLogEntry({
187
+ level,
188
+ message,
189
+ topic,
190
+ module,
191
+ traceId,
192
+ nodeId,
193
+ extra,
194
+ context,
195
+ }: LogEntryPayload): LogEntry {
196
+ return {
197
+ timestamp: Date.now(),
198
+ level,
199
+ message,
200
+ topic: topic === undefined ? this.options.defaultTopic : topic,
201
+ module,
202
+ traceId,
203
+ nodeId,
204
+ extra,
205
+ context,
206
+ };
207
+ }
208
+
209
+ private formatLevelTag(entry: LogEntry): string {
210
+ const emoji = this.options.formatEmoji ? `${this.levelEmojis[entry.level]} ` : "";
211
+ if (entry.level === LogLevel.CRITICAL) {
212
+ return `[${emoji}*** ${entry.level} ***]`;
213
+ }
214
+ return `[${emoji}${entry.level}]`;
215
+ }
216
+
217
+ private formatLog(entry: LogEntry): string {
218
+ const parts: string[] = [];
219
+
220
+ if (this.options.formatTimestamp) {
221
+ parts.push(`[${new Date(entry.timestamp).toISOString()}]`);
222
+ }
223
+
224
+ parts.push(this.formatLevelTag(entry));
225
+ parts.push(
226
+ `traceId=${entry.traceId} nodeId=${entry.nodeId ?? "--"} module=${entry.module} topic=${entry.topic ?? "--"}`
227
+ );
228
+
229
+ if (this.options.formatPrefix) {
230
+ parts.push(`prefix=${this.options.formatPrefix}`);
231
+ }
232
+
233
+ const message =
234
+ entry.level === LogLevel.CRITICAL ? `>>> ${entry.message} <<<` : entry.message;
235
+ parts.push(`| ${message}`);
236
+
237
+ if (entry.extra) {
238
+ parts.push(`| extra=${entry.extra}`);
239
+ }
240
+
241
+ const contextText = formatContextValue(entry.context);
242
+ if (contextText) {
243
+ parts.push(`| context=${contextText}`);
244
+ }
245
+
246
+ return parts.join(" ");
247
+ }
248
+
249
+ private emitLog(entry: LogEntry, formatted: string): void {
250
+ switch (entry.level) {
251
+ case LogLevel.WARN:
252
+ console.warn(formatted);
253
+ break;
254
+ case LogLevel.ERROR:
255
+ case LogLevel.CRITICAL:
256
+ console.error(formatted);
257
+ break;
258
+ default:
259
+ console.log(formatted);
260
+ }
261
+ }
262
+
263
+ private log(
264
+ level: LogLevel,
265
+ message: LogParams["message"],
266
+ logContext: LogContext,
267
+ restParams?: LogRestParams
268
+ ) {
269
+ if (!this.shouldLog(level)) {
270
+ return;
271
+ }
272
+
273
+ validateLogContext(logContext);
274
+
275
+ const { topic, extra, context } = restParams ?? {};
276
+ const { module, traceId, nodeId } = logContext;
277
+ const entry = this.createLogEntry({ level, message, topic, extra, context, module, traceId, nodeId });
278
+
279
+ this.history.push(entry);
280
+ if (this.history.length > this.options.maxHistoryLength) {
281
+ this.history.shift();
282
+ }
283
+
284
+ const formatted = this.formatLog(entry);
285
+ this.emitLog(entry, formatted);
286
+ this.options.onLog?.(entry);
287
+ }
288
+
289
+ public generateTraceId(): string {
290
+ return generateUniqueId();
291
+ }
292
+
293
+ public debug(message: LogParams["message"], logContext: LogContext, restParams?: LogRestParams) {
294
+ this.log(LogLevel.DEBUG, message, logContext, restParams);
295
+ }
296
+
297
+ public info(message: LogParams["message"], logContext: LogContext, restParams?: LogRestParams) {
298
+ this.log(LogLevel.INFO, message, logContext, restParams);
299
+ }
300
+
301
+ public success(message: LogParams["message"], logContext: LogContext, restParams?: LogRestParams) {
302
+ this.log(LogLevel.SUCCESS, message, logContext, restParams);
303
+ }
304
+
305
+ public warn(message: LogParams["message"], logContext: LogContext, restParams?: LogRestParams) {
306
+ this.log(LogLevel.WARN, message, logContext, restParams);
307
+ }
308
+
309
+ public error(message: LogParams["message"], logContext: LogContext, restParams?: LogRestParams) {
310
+ this.log(LogLevel.ERROR, message, logContext, restParams);
311
+ }
312
+
313
+ public critical(message: LogParams["message"], logContext: LogContext, restParams?: LogRestParams) {
314
+ this.log(LogLevel.CRITICAL, message, logContext, restParams);
315
+ }
316
+
317
+ public withContext(context: LogContext): { logger: LoggerWithContext; context: LogContext } {
318
+ const boundContext: LogContext = { ...context };
319
+
320
+ const logger: LoggerWithContext = {
321
+ debug: (message, restParams) => this.debug(message, boundContext, restParams),
322
+ info: (message, restParams) => this.info(message, boundContext, restParams),
323
+ success: (message, restParams) => this.success(message, boundContext, restParams),
324
+ warn: (message, restParams) => this.warn(message, boundContext, restParams),
325
+ error: (message, restParams) => this.error(message, boundContext, restParams),
326
+ critical: (message, restParams) => this.critical(message, boundContext, restParams),
327
+ resetContext: (fields) => {
328
+ Object.assign(boundContext, fields);
329
+ },
330
+ };
331
+
332
+ return { logger, context: boundContext };
333
+ }
334
+ }
335
+
336
+ export const logManager = LogManager.getInstance();
337
+ export { LogManager, LogLevel };
338
+ export { createApiLogger } from "./create-api-logger";
339
+ export type { CreateApiLoggerOptions } from "./create-api-logger";
340
+ export type { LogOptions, LogEntry, LogContext, LoggerWithContext, LogRestParams };
@@ -0,0 +1,104 @@
1
+ import { access } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ /**
6
+ * Directory containing a config file — anchor for paths declared **inside** that file.
7
+ */
8
+ export function getConfigFileDir(configFilePath: string): string {
9
+ return dirname(configFilePath);
10
+ }
11
+
12
+ /**
13
+ * Resolve a path from a config file's directory.
14
+ *
15
+ * - Absolute `pathInConfig` → returned as-is
16
+ * - Relative `pathInConfig` → `resolve(dirname(configFilePath), pathInConfig)`
17
+ *
18
+ * **Not** relative to `process.cwd()`. See `.cursor/skills/config-file-relative-paths/SKILL.md`.
19
+ */
20
+ export function resolvePathFromConfigFile(configFilePath: string, pathInConfig: string): string {
21
+ const trimmed = pathInConfig.trim();
22
+ if (isAbsolute(trimmed)) {
23
+ return trimmed;
24
+ }
25
+ return resolve(getConfigFileDir(configFilePath), trimmed);
26
+ }
27
+
28
+ /**
29
+ * Resolve a path from an explicit base directory (e.g. task package dir for `entry`).
30
+ * Same rules as {@link resolvePathFromConfigFile}, but base is already known.
31
+ */
32
+ export function resolvePathFromBaseDir(baseDir: string, path: string): string {
33
+ const trimmed = path.trim();
34
+ if (isAbsolute(trimmed)) {
35
+ return trimmed;
36
+ }
37
+ return resolve(baseDir, trimmed);
38
+ }
39
+
40
+ /**
41
+ * Resolve a path from `cwd` — for **discovering** config files, not for fields inside them.
42
+ */
43
+ export function resolvePathFromCwd(cwd: string, path: string): string {
44
+ const trimmed = path.trim();
45
+ if (isAbsolute(trimmed)) {
46
+ return trimmed;
47
+ }
48
+ return resolve(cwd, trimmed);
49
+ }
50
+
51
+ export async function pathExists(path: string): Promise<boolean> {
52
+ try {
53
+ await access(path);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ export interface ResolveFirstExistingPathOptions {
61
+ /** When set, only this path is tried (resolved from `cwd` if relative). */
62
+ explicitPath?: string;
63
+ /** Relative to `cwd`, tried in order when `explicitPath` is omitted. */
64
+ candidatePaths: string[];
65
+ }
66
+
67
+ /**
68
+ * Find the first existing file path: optional explicit path, else scan `candidatePaths` under `cwd`.
69
+ */
70
+ export async function resolveFirstExistingPath(
71
+ cwd: string,
72
+ options: ResolveFirstExistingPathOptions
73
+ ): Promise<string | null> {
74
+ const { explicitPath, candidatePaths } = options;
75
+
76
+ if (explicitPath != null && explicitPath.trim() !== "") {
77
+ const resolved = resolvePathFromCwd(cwd, explicitPath);
78
+ return (await pathExists(resolved)) ? resolved : null;
79
+ }
80
+
81
+ for (const candidate of candidatePaths) {
82
+ const resolved = join(cwd, candidate);
83
+ if (await pathExists(resolved)) {
84
+ return resolved;
85
+ }
86
+ }
87
+
88
+ return null;
89
+ }
90
+
91
+ /** `file://` href for Node dynamic `import()` of a local module. */
92
+ export function toFileImportHref(filePath: string): string {
93
+ const trimmed = filePath.trim();
94
+ if (trimmed.includes("://")) {
95
+ return trimmed;
96
+ }
97
+ return pathToFileURL(trimmed).href;
98
+ }
99
+
100
+ /** Native ESM import of a `.js` / `.mjs` config; supports `export default` or named export object. */
101
+ export async function importJsConfigModule(configPath: string): Promise<unknown> {
102
+ const mod = await import(toFileImportHref(configPath));
103
+ return mod.default ?? mod;
104
+ }