agentweaver 0.1.18 → 0.1.20

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 (50) hide show
  1. package/README.md +54 -6
  2. package/dist/artifacts.js +9 -0
  3. package/dist/executors/git-commit-executor.js +24 -6
  4. package/dist/flow-state.js +3 -8
  5. package/dist/git/git-diff-parser.js +223 -0
  6. package/dist/git/git-service.js +562 -0
  7. package/dist/git/git-stage-selection.js +24 -0
  8. package/dist/git/git-status-parser.js +171 -0
  9. package/dist/git/git-types.js +1 -0
  10. package/dist/index.js +454 -108
  11. package/dist/interactive/auto-flow.js +644 -0
  12. package/dist/interactive/controller.js +489 -7
  13. package/dist/interactive/progress.js +194 -1
  14. package/dist/interactive/state.js +34 -0
  15. package/dist/interactive/web/index.js +237 -5
  16. package/dist/interactive/web/protocol.js +222 -1
  17. package/dist/interactive/web/server.js +497 -3
  18. package/dist/interactive/web/static/app.js +2462 -37
  19. package/dist/interactive/web/static/index.html +113 -11
  20. package/dist/interactive/web/static/styles.css +1 -1
  21. package/dist/interactive/web/static/styles.input.css +1383 -149
  22. package/dist/pipeline/auto-flow-blocks.js +307 -0
  23. package/dist/pipeline/auto-flow-config.js +273 -0
  24. package/dist/pipeline/auto-flow-identity.js +49 -0
  25. package/dist/pipeline/auto-flow-presets.js +52 -0
  26. package/dist/pipeline/auto-flow-resolver.js +830 -0
  27. package/dist/pipeline/auto-flow-types.js +17 -0
  28. package/dist/pipeline/context.js +1 -0
  29. package/dist/pipeline/declarative-flows.js +27 -1
  30. package/dist/pipeline/flow-specs/auto-common-guided.json +11 -0
  31. package/dist/pipeline/flow-specs/auto-golang.json +12 -1
  32. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +54 -1
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +19 -1
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +33 -1
  35. package/dist/pipeline/flow-specs/review/review-project.json +19 -1
  36. package/dist/pipeline/flow-specs/task-source/manual-jira-input.json +70 -0
  37. package/dist/pipeline/node-registry.js +9 -0
  38. package/dist/pipeline/nodes/codex-prompt-node.js +8 -1
  39. package/dist/pipeline/nodes/flow-run-node.js +5 -3
  40. package/dist/pipeline/nodes/git-status-node.js +2 -168
  41. package/dist/pipeline/nodes/manual-jira-task-input-node.js +146 -0
  42. package/dist/pipeline/nodes/opencode-prompt-node.js +8 -1
  43. package/dist/pipeline/nodes/plan-codex-node.js +8 -1
  44. package/dist/pipeline/spec-loader.js +14 -4
  45. package/dist/runtime/artifact-catalog.js +403 -0
  46. package/dist/runtime/settings.js +114 -0
  47. package/dist/scope.js +14 -4
  48. package/package.json +1 -1
  49. package/dist/pipeline/flow-specs/auto-common.json +0 -179
  50. package/dist/pipeline/flow-specs/auto-simple.json +0 -141
@@ -0,0 +1,146 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { printSummary } from "../../tui.js";
3
+ import { requestUserInputInTerminal, validateUserInputValues, } from "../../user-input.js";
4
+ function summarizeDescription(description) {
5
+ return description
6
+ .split(/\r?\n/)
7
+ .map((line) => line.trim())
8
+ .find((line) => line.length > 0)
9
+ ?.slice(0, 140) || "Manual Jira task description";
10
+ }
11
+ function buildManualJiraPayload(taskKey, description) {
12
+ const summary = summarizeDescription(description);
13
+ return {
14
+ id: `manual-${taskKey}`,
15
+ key: taskKey,
16
+ self: null,
17
+ source: "manual-jira-fallback",
18
+ fields: {
19
+ summary,
20
+ description,
21
+ issuetype: {
22
+ name: "Manual task",
23
+ },
24
+ labels: ["manual-jira-fallback"],
25
+ attachment: [],
26
+ comment: {
27
+ comments: [],
28
+ },
29
+ },
30
+ manual_input: {
31
+ description,
32
+ captured_at: new Date().toISOString(),
33
+ },
34
+ };
35
+ }
36
+ export const manualJiraTaskInputNode = {
37
+ kind: "manual-jira-task-input",
38
+ version: 1,
39
+ async run(context, params) {
40
+ const form = {
41
+ formId: "manual-jira-task-input",
42
+ title: "Manual Jira Task",
43
+ description: "Paste the Jira task description when Jira access is unavailable.",
44
+ submitLabel: "Continue",
45
+ fields: [
46
+ {
47
+ id: "task_description",
48
+ type: "text",
49
+ label: "Task description",
50
+ help: "Paste the Jira task text here. This will be stored as the raw Jira task artifact for this flow.",
51
+ required: true,
52
+ multiline: true,
53
+ rows: 10,
54
+ placeholder: "Paste Jira task title, description, acceptance criteria, comments, and links here.",
55
+ },
56
+ ],
57
+ };
58
+ const requester = context.requestUserInput ?? requestUserInputInTerminal;
59
+ const result = await requester(form);
60
+ validateUserInputValues(form, result.values);
61
+ const description = String(result.values.task_description ?? "").trim();
62
+ writeFileSync(params.outputFile, `${JSON.stringify(buildManualJiraPayload(params.taskKey, description), null, 2)}\n`, "utf8");
63
+ if (params.attachmentsManifestFile) {
64
+ writeFileSync(params.attachmentsManifestFile, `${JSON.stringify({ source: "manual-jira-fallback", issueKey: params.taskKey, attachments: [] }, null, 2)}\n`, "utf8");
65
+ }
66
+ if (params.attachmentsContextFile) {
67
+ writeFileSync(params.attachmentsContextFile, "No Jira attachments were provided for the manual Jira fallback.\n", "utf8");
68
+ }
69
+ printSummary("Manual Jira Task", description);
70
+ const outputs = [
71
+ {
72
+ kind: "file",
73
+ path: params.outputFile,
74
+ required: true,
75
+ manifest: {
76
+ publish: true,
77
+ logicalKey: "artifacts/jira-task.json",
78
+ payloadFamily: "helper-json",
79
+ schemaId: "helper-json/v1",
80
+ schemaVersion: 1,
81
+ },
82
+ },
83
+ ];
84
+ if (params.attachmentsManifestFile) {
85
+ outputs.push({
86
+ kind: "artifact",
87
+ path: params.attachmentsManifestFile,
88
+ required: true,
89
+ manifest: {
90
+ publish: true,
91
+ logicalKey: "artifacts/jira-attachments.json",
92
+ payloadFamily: "helper-json",
93
+ schemaId: "helper-json/v1",
94
+ schemaVersion: 1,
95
+ },
96
+ });
97
+ }
98
+ if (params.attachmentsContextFile) {
99
+ outputs.push({
100
+ kind: "artifact",
101
+ path: params.attachmentsContextFile,
102
+ required: true,
103
+ manifest: {
104
+ publish: true,
105
+ logicalKey: "jira-attachments-context.txt",
106
+ payloadFamily: "plain-text",
107
+ schemaId: "plain-text/v1",
108
+ schemaVersion: 1,
109
+ },
110
+ });
111
+ }
112
+ return {
113
+ value: {
114
+ outputFile: params.outputFile,
115
+ ...(params.attachmentsManifestFile ? { attachmentsManifestFile: params.attachmentsManifestFile } : {}),
116
+ ...(params.attachmentsContextFile ? { attachmentsContextFile: params.attachmentsContextFile } : {}),
117
+ descriptionLength: description.length,
118
+ },
119
+ outputs,
120
+ };
121
+ },
122
+ checks(_context, params) {
123
+ const checks = [
124
+ {
125
+ kind: "require-file",
126
+ path: params.outputFile,
127
+ message: `Manual Jira task input did not produce ${params.outputFile}.`,
128
+ },
129
+ ];
130
+ if (params.attachmentsManifestFile) {
131
+ checks.push({
132
+ kind: "require-file",
133
+ path: params.attachmentsManifestFile,
134
+ message: `Manual Jira task input did not produce ${params.attachmentsManifestFile}.`,
135
+ });
136
+ }
137
+ if (params.attachmentsContextFile) {
138
+ checks.push({
139
+ kind: "require-file",
140
+ path: params.attachmentsContextFile,
141
+ message: `Manual Jira task input did not produce ${params.attachmentsContextFile}.`,
142
+ });
143
+ }
144
+ return checks;
145
+ },
146
+ };
@@ -14,7 +14,14 @@ export const opencodePromptNode = {
14
14
  }, executor.defaultConfig);
15
15
  return {
16
16
  value,
17
- outputs: (params.requiredArtifacts ?? []).map((path) => ({ kind: "artifact", path, required: true })),
17
+ outputs: (params.requiredArtifacts ?? []).map((path) => ({
18
+ kind: "artifact",
19
+ path,
20
+ required: true,
21
+ manifest: {
22
+ publish: true,
23
+ },
24
+ })),
18
25
  };
19
26
  },
20
27
  checks(_context, params) {
@@ -17,7 +17,14 @@ export const planCodexNode = {
17
17
  const value = await executor.execute(toExecutorContext(context), input, executor.defaultConfig);
18
18
  return {
19
19
  value,
20
- outputs: params.requiredArtifacts.map((path) => ({ kind: "artifact", path, required: true })),
20
+ outputs: params.requiredArtifacts.map((path) => ({
21
+ kind: "artifact",
22
+ path,
23
+ required: true,
24
+ manifest: {
25
+ publish: true,
26
+ },
27
+ })),
21
28
  };
22
29
  },
23
30
  checks(_context, params) {
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { TaskRunnerError } from "../errors.js";
5
5
  import { agentweaverConfigDir } from "../runtime/env-loader.js";
6
+ import { resolveBuiltInAutoFlowSpecByFileName } from "./auto-flow-resolver.js";
7
+ import { VIRTUAL_BUILT_IN_AUTO_FLOW_FILE_NAMES } from "./auto-flow-presets.js";
6
8
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
7
9
  const BUILT_IN_FLOW_SPECS_DIR = path.join(MODULE_DIR, "flow-specs");
8
10
  function parseFlowSpec(filePath) {
@@ -23,12 +25,14 @@ export function globalFlowSpecsDir() {
23
25
  return path.join(agentweaverConfigDir(), ".flows");
24
26
  }
25
27
  export function listBuiltInFlowSpecFiles() {
28
+ const files = new Set(VIRTUAL_BUILT_IN_AUTO_FLOW_FILE_NAMES);
26
29
  if (!existsSync(BUILT_IN_FLOW_SPECS_DIR)) {
27
- return [];
30
+ return [...files].sort((left, right) => left.localeCompare(right));
31
+ }
32
+ for (const filePath of collectJsonFilesRecursively(BUILT_IN_FLOW_SPECS_DIR)) {
33
+ files.add(path.relative(BUILT_IN_FLOW_SPECS_DIR, filePath));
28
34
  }
29
- return collectJsonFilesRecursively(BUILT_IN_FLOW_SPECS_DIR)
30
- .map((filePath) => path.relative(BUILT_IN_FLOW_SPECS_DIR, filePath))
31
- .sort((left, right) => left.localeCompare(right));
35
+ return [...files].sort((left, right) => left.localeCompare(right));
32
36
  }
33
37
  function collectJsonFilesRecursively(directory) {
34
38
  const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
@@ -60,6 +64,12 @@ export function listGlobalFlowSpecFiles() {
60
64
  return collectJsonFilesRecursively(directory);
61
65
  }
62
66
  export function loadFlowSpecSync(source) {
67
+ if (source.source === "built-in") {
68
+ const resolvedAutoFlowSpec = resolveBuiltInAutoFlowSpecByFileName(source.fileName);
69
+ if (resolvedAutoFlowSpec) {
70
+ return resolvedAutoFlowSpec;
71
+ }
72
+ }
63
73
  return parseFlowSpec(source.source === "built-in" ? resolveBuiltInFlowSpecPath(source.fileName) : source.filePath);
64
74
  }
65
75
  export function loadBuiltInFlowSpecSync(fileName) {
@@ -0,0 +1,403 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { buildLogicalKeyForPayload } from "../artifact-manifest.js";
4
+ import { artifactIndexFile, artifactManifestSidecarPath, scopeArtifactsDir, scopeWorkspaceDir, } from "../artifacts.js";
5
+ const UNCLASSIFIED_PHASE_ID = "unclassified";
6
+ const ROLE_MAPPINGS = [
7
+ { prefix: "bug-fix-design", role: "design", title: "Bug Fix Design" },
8
+ { prefix: "bug-fix-plan", role: "plan", title: "Bug Fix Plan" },
9
+ { prefix: "bug-analyze", role: "analysis", title: "Bug Analysis" },
10
+ { prefix: "task-context", role: "context", title: "Task Context" },
11
+ { prefix: "gitlab-diff", role: "diff", title: "GitLab Diff" },
12
+ { prefix: "jira-description", role: "context", title: "Jira Description" },
13
+ { prefix: "design", role: "design", title: "Design" },
14
+ { prefix: "plan", role: "plan", title: "Plan" },
15
+ { prefix: "review", role: "review", title: "Review" },
16
+ { prefix: "qa", role: "qa", title: "QA Plan" },
17
+ { prefix: "task", role: "summary", title: "Task Summary" },
18
+ ];
19
+ const ROLE_ORDER = new Map([
20
+ ["context", 10],
21
+ ["analysis", 20],
22
+ ["design", 30],
23
+ ["plan", 40],
24
+ ["qa", 50],
25
+ ["review", 60],
26
+ ["diff", 70],
27
+ ["summary", 80],
28
+ ["artifact", 90],
29
+ ]);
30
+ const BINARY_EXTENSIONS = new Set([
31
+ ".avif",
32
+ ".bin",
33
+ ".bmp",
34
+ ".gif",
35
+ ".gz",
36
+ ".ico",
37
+ ".jpeg",
38
+ ".jpg",
39
+ ".pdf",
40
+ ".png",
41
+ ".tar",
42
+ ".tgz",
43
+ ".webp",
44
+ ".zip",
45
+ ]);
46
+ function normalizePathSeparators(value) {
47
+ return value.replace(/\\/g, "/");
48
+ }
49
+ function normalizedAbsolutePath(filePath) {
50
+ return path.normalize(path.resolve(filePath));
51
+ }
52
+ function isInsideDirectory(parent, candidate) {
53
+ const relative = path.relative(parent, candidate);
54
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
55
+ }
56
+ function scopeRelativePath(scopeKey, payloadPath) {
57
+ const workspaceDir = normalizedAbsolutePath(scopeWorkspaceDir(scopeKey));
58
+ const normalizedPayloadPath = normalizedAbsolutePath(payloadPath);
59
+ if (!isInsideDirectory(workspaceDir, normalizedPayloadPath)) {
60
+ return normalizePathSeparators(path.basename(normalizedPayloadPath));
61
+ }
62
+ const relativePath = path.relative(workspaceDir, normalizedPayloadPath);
63
+ return normalizePathSeparators(relativePath || path.basename(normalizedPayloadPath));
64
+ }
65
+ function fileMetadata(filePath, fallbackIso) {
66
+ try {
67
+ const stats = statSync(filePath);
68
+ return {
69
+ sizeBytes: stats.size,
70
+ updatedAt: stats.mtime.toISOString(),
71
+ };
72
+ }
73
+ catch {
74
+ return {
75
+ sizeBytes: 0,
76
+ updatedAt: fallbackIso,
77
+ };
78
+ }
79
+ }
80
+ function cleanBaseName(value) {
81
+ const extension = path.extname(value);
82
+ return extension ? value.slice(0, -extension.length) : value;
83
+ }
84
+ function stripScopeAndVersionSuffix(value, scopeKey) {
85
+ const escapedScope = scopeKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
86
+ return value
87
+ .replace(new RegExp(`-${escapedScope}-iter-\\d+$`), "")
88
+ .replace(new RegExp(`-${escapedScope}-\\d+$`), "")
89
+ .replace(new RegExp(`-${escapedScope}$`), "")
90
+ .replace(/-\d+$/, "");
91
+ }
92
+ function toTitleCase(value) {
93
+ return value
94
+ .replace(/[/_.-]+/g, " ")
95
+ .trim()
96
+ .split(/\s+/)
97
+ .filter(Boolean)
98
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
99
+ .join(" ");
100
+ }
101
+ function mappingForKey(logicalKeyOrPath) {
102
+ const normalized = logicalKeyOrPath.replace(/^\.artifacts\//, "").replace(/^artifacts\//, "");
103
+ const fileStem = cleanBaseName(path.posix.basename(normalized));
104
+ const candidates = [normalized, fileStem];
105
+ return ROLE_MAPPINGS.find((mapping) => (candidates.some((candidate) => candidate === mapping.prefix || candidate.startsWith(`${mapping.prefix}-`)))) ?? null;
106
+ }
107
+ export function inferArtifactRole(logicalKey, relativePath) {
108
+ const mapping = mappingForKey(logicalKey ?? relativePath);
109
+ return mapping?.role ?? "artifact";
110
+ }
111
+ export function inferArtifactTitle(scopeKey, logicalKey, relativePath) {
112
+ const source = logicalKey ?? relativePath;
113
+ const mapping = mappingForKey(source);
114
+ if (mapping) {
115
+ return mapping.title;
116
+ }
117
+ const stem = stripScopeAndVersionSuffix(cleanBaseName(path.posix.basename(normalizePathSeparators(source))), scopeKey);
118
+ return toTitleCase(stem) || "Artifact";
119
+ }
120
+ function kindFromPayloadFamily(payloadFamily) {
121
+ if (payloadFamily === "markdown") {
122
+ return "markdown";
123
+ }
124
+ if (payloadFamily === "structured-json" || payloadFamily === "helper-json") {
125
+ return "json";
126
+ }
127
+ if (payloadFamily === "plain-text") {
128
+ return "text";
129
+ }
130
+ if (payloadFamily === "opaque-file") {
131
+ return "binary";
132
+ }
133
+ return null;
134
+ }
135
+ function kindFromSchemaId(schemaId) {
136
+ if (!schemaId) {
137
+ return null;
138
+ }
139
+ const normalized = schemaId.toLowerCase();
140
+ if (normalized.includes("markdown")) {
141
+ return "markdown";
142
+ }
143
+ if (normalized.includes("diff") || normalized.includes("patch")) {
144
+ return "diff";
145
+ }
146
+ if (normalized.includes("text") || normalized.includes("log")) {
147
+ return "text";
148
+ }
149
+ if (normalized.endsWith("/v1") || normalized.includes("json")) {
150
+ return "json";
151
+ }
152
+ return null;
153
+ }
154
+ function kindFromExtension(filePath) {
155
+ const extension = path.extname(filePath).toLowerCase();
156
+ if (extension === ".md" || extension === ".markdown") {
157
+ return "markdown";
158
+ }
159
+ if (extension === ".json") {
160
+ return "json";
161
+ }
162
+ if (extension === ".txt" || extension === ".log") {
163
+ return "text";
164
+ }
165
+ if (extension === ".diff" || extension === ".patch") {
166
+ return "diff";
167
+ }
168
+ if (BINARY_EXTENSIONS.has(extension)) {
169
+ return "binary";
170
+ }
171
+ return "unknown";
172
+ }
173
+ export function inferArtifactRenderKind(input) {
174
+ return kindFromPayloadFamily(input.payloadFamily)
175
+ ?? kindFromSchemaId(input.schemaId)
176
+ ?? kindFromExtension(input.filePath);
177
+ }
178
+ function projectManifestRecord(scopeKey, record) {
179
+ const manifest = record.manifest;
180
+ const metadata = fileMetadata(manifest.payload_path, manifest.created_at);
181
+ const relativePath = scopeRelativePath(scopeKey, manifest.payload_path);
182
+ const logicalKey = record.logical_key || manifest.logical_key;
183
+ return {
184
+ id: record.artifact_id,
185
+ scopeKey,
186
+ runId: manifest.run_id,
187
+ logicalKey,
188
+ title: inferArtifactTitle(scopeKey, logicalKey, relativePath),
189
+ relativePath,
190
+ kind: inferArtifactRenderKind({
191
+ payloadFamily: manifest.payload_family,
192
+ schemaId: record.schema_id || manifest.schema_id,
193
+ filePath: manifest.payload_path,
194
+ }),
195
+ role: inferArtifactRole(logicalKey, relativePath),
196
+ phaseId: manifest.phase_id || null,
197
+ stepId: manifest.step_id || null,
198
+ schemaId: record.schema_id || manifest.schema_id || null,
199
+ sizeBytes: metadata.sizeBytes,
200
+ updatedAt: metadata.updatedAt,
201
+ isLatest: record.is_latest,
202
+ source: "manifest",
203
+ };
204
+ }
205
+ function isExcludedDirectory(name) {
206
+ return name === "manifest-history" || name === "restart-archives";
207
+ }
208
+ function isRegistryTempFile(filePath) {
209
+ const baseName = path.basename(filePath);
210
+ return /\.tmp-[^/\\]+$/.test(baseName)
211
+ || baseName.endsWith(".tmp")
212
+ || baseName.endsWith(".temp")
213
+ || baseName.endsWith(".swp")
214
+ || baseName.endsWith("~");
215
+ }
216
+ function isInternalIndexFile(filePath) {
217
+ const baseName = path.basename(filePath);
218
+ return baseName === "artifact-index.json"
219
+ || baseName === "artifact-registry-index.json"
220
+ || baseName === "manifest-index.json"
221
+ || baseName === "internal-index.json"
222
+ || baseName === ".index.json";
223
+ }
224
+ function isExcludedCatalogPath(scopeKey, filePath) {
225
+ const normalizedPath = normalizedAbsolutePath(filePath);
226
+ const workspaceDir = normalizedAbsolutePath(scopeWorkspaceDir(scopeKey));
227
+ if (!isInsideDirectory(workspaceDir, normalizedPath)) {
228
+ return true;
229
+ }
230
+ const relativePath = normalizePathSeparators(path.relative(workspaceDir, normalizedPath));
231
+ const segments = relativePath.split("/");
232
+ if (segments.some((segment) => segment === "manifest-history" || segment === "restart-archives")) {
233
+ return true;
234
+ }
235
+ if (normalizedPath === normalizedAbsolutePath(artifactIndexFile(scopeKey))) {
236
+ return true;
237
+ }
238
+ if (normalizedPath.endsWith(".manifest.json") || normalizedPath === normalizedAbsolutePath(artifactManifestSidecarPath(normalizedPath))) {
239
+ return true;
240
+ }
241
+ if (isInternalIndexFile(normalizedPath) || isRegistryTempFile(normalizedPath)) {
242
+ return true;
243
+ }
244
+ return path.basename(normalizedPath) === ".DS_Store";
245
+ }
246
+ function scanRoot(scopeKey, rootDir, seenDirectories) {
247
+ const normalizedRoot = normalizedAbsolutePath(rootDir);
248
+ const workspaceDir = normalizedAbsolutePath(scopeWorkspaceDir(scopeKey));
249
+ if (!existsSync(normalizedRoot) || !isInsideDirectory(workspaceDir, normalizedRoot)) {
250
+ return [];
251
+ }
252
+ const files = [];
253
+ const queue = [normalizedRoot];
254
+ while (queue.length > 0) {
255
+ const current = queue.shift();
256
+ if (!current) {
257
+ continue;
258
+ }
259
+ const normalizedCurrent = normalizedAbsolutePath(current);
260
+ if (seenDirectories.has(normalizedCurrent)) {
261
+ continue;
262
+ }
263
+ seenDirectories.add(normalizedCurrent);
264
+ let entries;
265
+ try {
266
+ entries = readdirSync(normalizedCurrent, { withFileTypes: true })
267
+ .sort((left, right) => left.name.localeCompare(right.name));
268
+ }
269
+ catch {
270
+ continue;
271
+ }
272
+ for (const entry of entries) {
273
+ const fullPath = path.join(normalizedCurrent, entry.name);
274
+ if (entry.isSymbolicLink()) {
275
+ continue;
276
+ }
277
+ if (entry.isDirectory()) {
278
+ if (!isExcludedDirectory(entry.name)) {
279
+ queue.push(fullPath);
280
+ }
281
+ continue;
282
+ }
283
+ if (entry.isFile() && !isExcludedCatalogPath(scopeKey, fullPath)) {
284
+ files.push(normalizedAbsolutePath(fullPath));
285
+ }
286
+ }
287
+ }
288
+ return files;
289
+ }
290
+ function scanScopeFiles(scopeKey) {
291
+ const seenDirectories = new Set();
292
+ const files = [
293
+ ...scanRoot(scopeKey, scopeWorkspaceDir(scopeKey), seenDirectories),
294
+ ...scanRoot(scopeKey, scopeArtifactsDir(scopeKey), seenDirectories),
295
+ ];
296
+ return [...new Set(files)].sort((left, right) => scopeRelativePath(scopeKey, left).localeCompare(scopeRelativePath(scopeKey, right)));
297
+ }
298
+ function projectScannedFile(scopeKey, filePath) {
299
+ const relativePath = scopeRelativePath(scopeKey, filePath);
300
+ const logicalKey = buildLogicalKeyForPayload(scopeKey, filePath);
301
+ const metadata = fileMetadata(filePath, new Date(0).toISOString());
302
+ return {
303
+ id: `scanner:${scopeKey}:${relativePath}`,
304
+ scopeKey,
305
+ runId: null,
306
+ logicalKey,
307
+ title: inferArtifactTitle(scopeKey, logicalKey, relativePath),
308
+ relativePath,
309
+ kind: inferArtifactRenderKind({ filePath }),
310
+ role: inferArtifactRole(logicalKey, relativePath),
311
+ phaseId: null,
312
+ stepId: null,
313
+ schemaId: null,
314
+ sizeBytes: metadata.sizeBytes,
315
+ updatedAt: metadata.updatedAt,
316
+ isLatest: true,
317
+ source: "scanner",
318
+ };
319
+ }
320
+ function roleRank(role) {
321
+ return ROLE_ORDER.get(role) ?? 100;
322
+ }
323
+ function phaseSortKey(phaseId) {
324
+ return phaseId ?? UNCLASSIFIED_PHASE_ID;
325
+ }
326
+ function typeGroupKey(item) {
327
+ return `${item.role}:${item.title}`;
328
+ }
329
+ function compareCatalogItems(left, right) {
330
+ const phaseComparison = phaseSortKey(left.phaseId).localeCompare(phaseSortKey(right.phaseId));
331
+ if (phaseComparison !== 0) {
332
+ return phaseComparison;
333
+ }
334
+ const roleComparison = roleRank(left.role) - roleRank(right.role);
335
+ if (roleComparison !== 0) {
336
+ return roleComparison;
337
+ }
338
+ const titleComparison = left.title.localeCompare(right.title);
339
+ if (titleComparison !== 0) {
340
+ return titleComparison;
341
+ }
342
+ const updatedComparison = right.updatedAt.localeCompare(left.updatedAt);
343
+ if (updatedComparison !== 0) {
344
+ return updatedComparison;
345
+ }
346
+ return left.relativePath.localeCompare(right.relativePath);
347
+ }
348
+ export function groupArtifactCatalog(items) {
349
+ const grouped = new Map();
350
+ for (const item of items) {
351
+ const phaseId = item.phaseId ?? UNCLASSIFIED_PHASE_ID;
352
+ const phaseItems = grouped.get(phaseId) ?? [];
353
+ phaseItems.push(item);
354
+ grouped.set(phaseId, phaseItems);
355
+ }
356
+ return Array.from(grouped.entries())
357
+ .sort(([left], [right]) => left.localeCompare(right))
358
+ .map(([phaseId, phaseItems]) => {
359
+ const sortedPhaseItems = phaseItems.slice().sort(compareCatalogItems);
360
+ const typeGroups = new Map();
361
+ for (const item of sortedPhaseItems) {
362
+ const key = typeGroupKey(item);
363
+ const groupItems = typeGroups.get(key) ?? [];
364
+ groupItems.push(item);
365
+ typeGroups.set(key, groupItems);
366
+ }
367
+ const nestedGroups = Array.from(typeGroups.entries())
368
+ .map(([key, groupItems]) => ({
369
+ phaseId: `${phaseId}:${key}`,
370
+ title: groupItems[0]?.title ?? "Artifacts",
371
+ items: groupItems.slice().sort(compareCatalogItems),
372
+ }));
373
+ return {
374
+ phaseId,
375
+ title: phaseId === UNCLASSIFIED_PHASE_ID ? "Unclassified" : toTitleCase(phaseId),
376
+ items: sortedPhaseItems,
377
+ ...(nestedGroups.length > 1 || nestedGroups.some((group) => group.items.length > 1) ? { groups: nestedGroups } : {}),
378
+ };
379
+ });
380
+ }
381
+ export function listArtifactCatalog(input) {
382
+ const seenPayloadPaths = new Set();
383
+ const items = [];
384
+ for (const record of input.artifactRegistry.listScopeArtifacts(input.scopeKey)) {
385
+ const payloadPath = normalizedAbsolutePath(record.manifest.payload_path);
386
+ seenPayloadPaths.add(payloadPath);
387
+ items.push(projectManifestRecord(input.scopeKey, record));
388
+ }
389
+ for (const filePath of scanScopeFiles(input.scopeKey)) {
390
+ const normalizedPath = normalizedAbsolutePath(filePath);
391
+ if (seenPayloadPaths.has(normalizedPath)) {
392
+ continue;
393
+ }
394
+ seenPayloadPaths.add(normalizedPath);
395
+ items.push(projectScannedFile(input.scopeKey, normalizedPath));
396
+ }
397
+ const sortedItems = items.slice().sort(compareCatalogItems);
398
+ return {
399
+ scopeKey: input.scopeKey,
400
+ items: sortedItems,
401
+ groups: groupArtifactCatalog(sortedItems),
402
+ };
403
+ }