@rlarua/agentteams-cli 0.0.9 → 0.0.11

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 (118) hide show
  1. package/API-CLI-WORKFLOW.md +418 -0
  2. package/LICENSE +201 -0
  3. package/README.md +220 -128
  4. package/dist/api/comment.d.ts +11 -0
  5. package/dist/api/comment.d.ts.map +1 -0
  6. package/dist/api/comment.js +33 -0
  7. package/dist/api/comment.js.map +1 -0
  8. package/dist/api/plan.d.ts +13 -0
  9. package/dist/api/plan.d.ts.map +1 -0
  10. package/dist/api/plan.js +43 -0
  11. package/dist/api/plan.js.map +1 -0
  12. package/dist/api/postmortem.d.ts +6 -0
  13. package/dist/api/postmortem.d.ts.map +1 -0
  14. package/dist/api/postmortem.js +33 -0
  15. package/dist/api/postmortem.js.map +1 -0
  16. package/dist/api/report.d.ts +6 -0
  17. package/dist/api/report.d.ts.map +1 -0
  18. package/dist/api/report.js +33 -0
  19. package/dist/api/report.js.map +1 -0
  20. package/dist/api/status.d.ts +12 -0
  21. package/dist/api/status.d.ts.map +1 -0
  22. package/dist/api/status.js +33 -0
  23. package/dist/api/status.js.map +1 -0
  24. package/dist/commands/agentConfig.d.ts.map +1 -1
  25. package/dist/commands/agentConfig.js +10 -3
  26. package/dist/commands/agentConfig.js.map +1 -1
  27. package/dist/commands/agentConfigCommand.d.ts +2 -0
  28. package/dist/commands/agentConfigCommand.d.ts.map +1 -0
  29. package/dist/commands/agentConfigCommand.js +20 -0
  30. package/dist/commands/agentConfigCommand.js.map +1 -0
  31. package/dist/commands/comment.d.ts +2 -0
  32. package/dist/commands/comment.d.ts.map +1 -0
  33. package/dist/commands/comment.js +55 -0
  34. package/dist/commands/comment.js.map +1 -0
  35. package/dist/commands/config.d.ts +2 -0
  36. package/dist/commands/config.d.ts.map +1 -0
  37. package/dist/commands/config.js +30 -0
  38. package/dist/commands/config.js.map +1 -0
  39. package/dist/commands/convention.d.ts +25 -0
  40. package/dist/commands/convention.d.ts.map +1 -1
  41. package/dist/commands/convention.js +492 -43
  42. package/dist/commands/convention.js.map +1 -1
  43. package/dist/commands/conventionRouter.d.ts +3 -0
  44. package/dist/commands/conventionRouter.d.ts.map +1 -0
  45. package/dist/commands/conventionRouter.js +46 -0
  46. package/dist/commands/conventionRouter.js.map +1 -0
  47. package/dist/commands/dependency.d.ts.map +1 -1
  48. package/dist/commands/dependency.js +2 -1
  49. package/dist/commands/dependency.js.map +1 -1
  50. package/dist/commands/dependencyCommand.d.ts +2 -0
  51. package/dist/commands/dependencyCommand.d.ts.map +1 -0
  52. package/dist/commands/dependencyCommand.js +27 -0
  53. package/dist/commands/dependencyCommand.js.map +1 -0
  54. package/dist/commands/index.d.ts.map +1 -1
  55. package/dist/commands/index.js +13 -485
  56. package/dist/commands/index.js.map +1 -1
  57. package/dist/commands/init.d.ts.map +1 -1
  58. package/dist/commands/init.js +3 -2
  59. package/dist/commands/init.js.map +1 -1
  60. package/dist/commands/plan.d.ts +11 -0
  61. package/dist/commands/plan.d.ts.map +1 -0
  62. package/dist/commands/plan.js +370 -0
  63. package/dist/commands/plan.js.map +1 -0
  64. package/dist/commands/postmortem.d.ts +2 -0
  65. package/dist/commands/postmortem.d.ts.map +1 -0
  66. package/dist/commands/postmortem.js +114 -0
  67. package/dist/commands/postmortem.js.map +1 -0
  68. package/dist/commands/report.d.ts +2 -0
  69. package/dist/commands/report.d.ts.map +1 -0
  70. package/dist/commands/report.js +221 -0
  71. package/dist/commands/report.js.map +1 -0
  72. package/dist/commands/status.d.ts +2 -0
  73. package/dist/commands/status.d.ts.map +1 -0
  74. package/dist/commands/status.js +60 -0
  75. package/dist/commands/status.js.map +1 -0
  76. package/dist/index.js +214 -34
  77. package/dist/index.js.map +1 -1
  78. package/dist/types/index.d.ts +11 -6
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/dist/utils/errors.d.ts.map +1 -1
  81. package/dist/utils/errors.js +60 -6
  82. package/dist/utils/errors.js.map +1 -1
  83. package/dist/utils/formatter.js +11 -1
  84. package/dist/utils/formatter.js.map +1 -1
  85. package/dist/utils/git.d.ts +19 -0
  86. package/dist/utils/git.d.ts.map +1 -0
  87. package/dist/utils/git.js +41 -0
  88. package/dist/utils/git.js.map +1 -0
  89. package/dist/utils/httpHeaders.d.ts +3 -0
  90. package/dist/utils/httpHeaders.d.ts.map +1 -0
  91. package/dist/utils/httpHeaders.js +11 -0
  92. package/dist/utils/httpHeaders.js.map +1 -0
  93. package/dist/utils/initOutput.d.ts +3 -0
  94. package/dist/utils/initOutput.d.ts.map +1 -0
  95. package/dist/utils/initOutput.js +34 -0
  96. package/dist/utils/initOutput.js.map +1 -0
  97. package/dist/utils/legacyCompat.d.ts +3 -0
  98. package/dist/utils/legacyCompat.d.ts.map +1 -0
  99. package/dist/utils/legacyCompat.js +20 -0
  100. package/dist/utils/legacyCompat.js.map +1 -0
  101. package/dist/utils/outputPolicy.d.ts +12 -0
  102. package/dist/utils/outputPolicy.d.ts.map +1 -0
  103. package/dist/utils/outputPolicy.js +132 -0
  104. package/dist/utils/outputPolicy.js.map +1 -0
  105. package/dist/utils/parsers.d.ts +7 -0
  106. package/dist/utils/parsers.d.ts.map +1 -0
  107. package/dist/utils/parsers.js +52 -0
  108. package/dist/utils/parsers.js.map +1 -0
  109. package/dist/utils/planFormat.d.ts +17 -0
  110. package/dist/utils/planFormat.d.ts.map +1 -0
  111. package/dist/utils/planFormat.js +80 -0
  112. package/dist/utils/planFormat.js.map +1 -0
  113. package/dist/utils/spinner.d.ts +6 -0
  114. package/dist/utils/spinner.d.ts.map +1 -0
  115. package/dist/utils/spinner.js +34 -0
  116. package/dist/utils/spinner.js.map +1 -0
  117. package/package.json +8 -3
  118. package/DEVELOPMENT.md +0 -234
@@ -1,10 +1,16 @@
1
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, unlinkSync } from "node:fs";
2
+ import { basename, join, relative, resolve } from "node:path";
3
3
  import axios from "axios";
4
+ import matter from "gray-matter";
5
+ import { diffLines, createTwoFilesPatch } from "diff";
6
+ import { normalizeMarkdownToTiptap } from "@agentteams/markdown-tiptap";
4
7
  import { loadConfig, findProjectConfig } from "../utils/config.js";
8
+ import { withSpinner } from "../utils/spinner.js";
9
+ import { withoutJsonContentType } from "../utils/httpHeaders.js";
5
10
  const CONVENTION_DIR = ".agentteams";
6
11
  const LEGACY_CONVENTION_DOWNLOAD_DIR = "conventions";
7
12
  const CONVENTION_INDEX_FILE = "convention.md";
13
+ const CONVENTION_MANIFEST_FILE = "conventions.manifest.json";
8
14
  function findProjectRoot(cwd) {
9
15
  const configPath = findProjectConfig(cwd ?? process.cwd());
10
16
  if (!configPath)
@@ -29,27 +35,247 @@ function getApiConfigOrThrow(options) {
29
35
  },
30
36
  };
31
37
  }
38
+ function normalizeRelativePath(input) {
39
+ return input.replaceAll("\\", "/");
40
+ }
41
+ function resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput) {
42
+ // If absolute path, keep as-is.
43
+ const resolvedFromCwd = resolve(cwd, fileInput);
44
+ if (resolvedFromCwd === fileInput && existsSync(fileInput)) {
45
+ return fileInput;
46
+ }
47
+ // Common usage: pass `.agentteams/...` from any working directory.
48
+ if (fileInput.startsWith(`${CONVENTION_DIR}/`) || fileInput.startsWith(`${CONVENTION_DIR}\\`)) {
49
+ return resolve(projectRoot, fileInput);
50
+ }
51
+ // Fallback: if the cwd-based resolution exists, use it.
52
+ if (existsSync(resolvedFromCwd)) {
53
+ return resolvedFromCwd;
54
+ }
55
+ // Otherwise, return cwd-based resolution to preserve a stable error path.
56
+ return resolvedFromCwd;
57
+ }
58
+ function buildManifestPath(projectRoot) {
59
+ return join(projectRoot, CONVENTION_DIR, CONVENTION_MANIFEST_FILE);
60
+ }
61
+ function loadManifestOrThrow(projectRoot) {
62
+ const manifestPath = buildManifestPath(projectRoot);
63
+ if (!existsSync(manifestPath)) {
64
+ throw new Error(`Download manifest not found: ${manifestPath}\nRun 'agentteams convention download' first.`);
65
+ }
66
+ const raw = readFileSync(manifestPath, "utf-8");
67
+ const parsed = JSON.parse(raw);
68
+ if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
69
+ throw new Error(`Invalid manifest format: ${manifestPath}`);
70
+ }
71
+ return parsed;
72
+ }
73
+ function loadManifestOrCreate(projectRoot) {
74
+ const manifestPath = buildManifestPath(projectRoot);
75
+ if (!existsSync(manifestPath)) {
76
+ return {
77
+ version: 1,
78
+ generatedAt: new Date().toISOString(),
79
+ entries: [],
80
+ };
81
+ }
82
+ const raw = readFileSync(manifestPath, "utf-8");
83
+ const parsed = JSON.parse(raw);
84
+ if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
85
+ throw new Error(`Invalid manifest format: ${manifestPath}`);
86
+ }
87
+ return parsed;
88
+ }
89
+ function writeManifest(projectRoot, manifest) {
90
+ const manifestPath = buildManifestPath(projectRoot);
91
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
92
+ }
93
+ function toFileList(input) {
94
+ return Array.isArray(input) ? input : [input];
95
+ }
96
+ function hasAnyDiff(a, b) {
97
+ const parts = diffLines(a, b);
98
+ return parts.some((p) => p.added || p.removed);
99
+ }
100
+ function createUnifiedDiff(fileLabel, serverText, localText) {
101
+ return createTwoFilesPatch(`${fileLabel} (server)`, `${fileLabel} (local)`, serverText, localText, "", "", { context: 3 });
102
+ }
103
+ function toOptionalString(value) {
104
+ return typeof value === "string" ? value : undefined;
105
+ }
106
+ function fileNameToTitle(fileName) {
107
+ return fileName
108
+ .replace(/\.md$/i, "")
109
+ .replace(/[-_]+/g, " ")
110
+ .replace(/\s+/g, " ")
111
+ .trim();
112
+ }
113
+ function parseCategoryFromAgentteamsPath(fileRelativePath) {
114
+ const normalized = normalizeRelativePath(fileRelativePath);
115
+ const parts = normalized.split("/");
116
+ const agentteamsIndex = parts.indexOf(CONVENTION_DIR);
117
+ if (agentteamsIndex === -1) {
118
+ throw new Error(`Convention create requires a file under ${CONVENTION_DIR}/<category>/: ${fileRelativePath}`);
119
+ }
120
+ const category = parts[agentteamsIndex + 1];
121
+ if (!category || category.length === 0) {
122
+ throw new Error(`Convention create requires a category directory under ${CONVENTION_DIR}/: ${fileRelativePath}`);
123
+ }
124
+ if (category === "platform" || category === "active-plan") {
125
+ throw new Error(`Convention create does not allow reserved directories under ${CONVENTION_DIR}/: ${category}`);
126
+ }
127
+ return category;
128
+ }
129
+ async function fetchAllConventions(apiUrl, projectId, headers) {
130
+ const pageSize = 100;
131
+ let page = 1;
132
+ let totalPages;
133
+ const items = [];
134
+ while (true) {
135
+ const response = await axios.get(`${apiUrl}/api/projects/${projectId}/conventions`, { headers, params: { page, pageSize } });
136
+ const data = response.data?.data;
137
+ if (!Array.isArray(data)) {
138
+ break;
139
+ }
140
+ items.push(...data);
141
+ const meta = response.data?.meta;
142
+ if (typeof meta?.totalPages === "number") {
143
+ totalPages = meta.totalPages;
144
+ }
145
+ if (totalPages !== undefined) {
146
+ if (page >= totalPages)
147
+ break;
148
+ page += 1;
149
+ continue;
150
+ }
151
+ // Fallback if meta is missing: stop when we got less than a full page.
152
+ if (data.length < pageSize)
153
+ break;
154
+ page += 1;
155
+ }
156
+ return items;
157
+ }
158
+ async function fetchConventionsWithContent(apiUrl, projectId, headers) {
159
+ const response = await axios.get(`${apiUrl}/api/projects/${projectId}/conventions/download-all`, { headers });
160
+ const data = response.data?.data;
161
+ if (!Array.isArray(data)) {
162
+ throw new Error("Invalid download-all response format");
163
+ }
164
+ return data;
165
+ }
166
+ async function fetchPlatformGuidesHash(apiUrl, headers) {
167
+ const response = await axios.get(`${apiUrl}/api/platform/guides/hash`, { headers });
168
+ const hash = response.data?.data?.hash;
169
+ if (typeof hash !== "string" || hash.length === 0) {
170
+ throw new Error("Invalid platform guides hash response format");
171
+ }
172
+ return hash;
173
+ }
174
+ function toConventionName(convention) {
175
+ const title = typeof convention.title === "string" ? convention.title.trim() : "";
176
+ if (title.length > 0)
177
+ return title;
178
+ const fileName = typeof convention.fileName === "string" ? convention.fileName.trim() : "";
179
+ if (fileName.length > 0)
180
+ return fileName;
181
+ return convention.id;
182
+ }
183
+ function toConventionNameFromManifest(entry) {
184
+ const title = typeof entry.title === "string" ? entry.title.trim() : "";
185
+ if (title.length > 0)
186
+ return title;
187
+ const fileName = typeof entry.fileName === "string" ? entry.fileName.trim() : "";
188
+ if (fileName.length > 0)
189
+ return fileName;
190
+ return entry.conventionId;
191
+ }
192
+ function toOptionalStringOrNullIfPresent(data, key) {
193
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
194
+ return undefined;
195
+ }
196
+ const value = data[key];
197
+ if (value === null) {
198
+ return null;
199
+ }
200
+ if (typeof value === "string") {
201
+ const trimmed = value.trim();
202
+ return trimmed.length > 0 ? trimmed : null;
203
+ }
204
+ return undefined;
205
+ }
32
206
  export async function conventionShow() {
33
207
  const { config, apiUrl, headers } = getApiConfigOrThrow();
34
- const listResponse = await axios.get(`${apiUrl}/api/projects/${config.projectId}/conventions`, { headers });
35
- const conventions = listResponse.data?.data;
208
+ const conventions = await fetchConventionsWithContent(apiUrl, config.projectId, headers);
36
209
  if (!conventions || conventions.length === 0) {
37
210
  throw new Error("No conventions found for this project. Create one via the web dashboard first.");
38
211
  }
39
212
  const sections = [];
40
213
  for (const convention of conventions) {
41
- const downloadResponse = await axios.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${convention.id}/download`, { headers, responseType: "text" });
214
+ const contentMarkdown = typeof convention.contentMarkdown === "string" ? convention.contentMarkdown : "";
42
215
  const sectionHeader = `# ${convention.title ?? "untitled"}\ncategory: ${convention.category ?? "uncategorized"}\nid: ${convention.id}`;
43
- sections.push(`${sectionHeader}\n\n${downloadResponse.data}`);
216
+ sections.push(`${sectionHeader}\n\n${contentMarkdown}`);
44
217
  }
45
218
  return sections.join("\n\n---\n\n");
46
219
  }
220
+ export async function checkConventionFreshness(apiUrl, projectId, headers, projectRoot) {
221
+ const manifestPath = buildManifestPath(projectRoot);
222
+ if (!existsSync(manifestPath)) {
223
+ return {
224
+ platformGuidesChanged: false,
225
+ conventionChanges: [],
226
+ };
227
+ }
228
+ const manifest = loadManifestOrThrow(projectRoot);
229
+ const currentPlatformGuidesHash = await fetchPlatformGuidesHash(apiUrl, headers);
230
+ const platformGuidesChanged = typeof manifest.platformGuidesHash === "string"
231
+ && manifest.platformGuidesHash.length > 0
232
+ && manifest.platformGuidesHash !== currentPlatformGuidesHash;
233
+ const serverConventions = await fetchAllConventions(apiUrl, projectId, headers);
234
+ const serverById = new Map(serverConventions.map((item) => [item.id, item]));
235
+ const localById = new Map(manifest.entries.map((entry) => [entry.conventionId, entry]));
236
+ const conventionChanges = [];
237
+ for (const serverConvention of serverConventions) {
238
+ const local = localById.get(serverConvention.id);
239
+ if (!local) {
240
+ conventionChanges.push({
241
+ id: serverConvention.id,
242
+ type: "new",
243
+ title: toConventionName(serverConvention),
244
+ fileName: serverConvention.fileName ?? undefined,
245
+ });
246
+ continue;
247
+ }
248
+ if (typeof serverConvention.updatedAt === "string"
249
+ && typeof local.updatedAt === "string"
250
+ && serverConvention.updatedAt !== local.updatedAt) {
251
+ conventionChanges.push({
252
+ id: serverConvention.id,
253
+ type: "updated",
254
+ title: toConventionName(serverConvention),
255
+ fileName: serverConvention.fileName ?? local.fileName,
256
+ });
257
+ }
258
+ }
259
+ for (const localEntry of manifest.entries) {
260
+ if (serverById.has(localEntry.conventionId))
261
+ continue;
262
+ conventionChanges.push({
263
+ id: localEntry.conventionId,
264
+ type: "deleted",
265
+ title: toConventionNameFromManifest(localEntry),
266
+ fileName: localEntry.fileName,
267
+ });
268
+ }
269
+ return {
270
+ platformGuidesChanged,
271
+ conventionChanges,
272
+ };
273
+ }
47
274
  export async function conventionList() {
48
275
  const { config, apiUrl, headers } = getApiConfigOrThrow();
49
- const response = await axios.get(`${apiUrl}/api/projects/${config.projectId}/conventions`, { headers });
50
- const conventions = response.data?.data;
276
+ const conventions = await fetchAllConventions(apiUrl, config.projectId, headers);
51
277
  if (!Array.isArray(conventions)) {
52
- return response.data;
278
+ return { data: conventions };
53
279
  }
54
280
  return {
55
281
  data: conventions.map((item) => ({
@@ -60,7 +286,12 @@ export async function conventionList() {
60
286
  updatedAt: item.updatedAt,
61
287
  createdAt: item.createdAt,
62
288
  })),
63
- meta: response.data?.meta,
289
+ meta: {
290
+ total: conventions.length,
291
+ page: 1,
292
+ pageSize: conventions.length,
293
+ totalPages: 1,
294
+ }
64
295
  };
65
296
  }
66
297
  function toSafeFileName(input) {
@@ -107,7 +338,7 @@ async function downloadPlatformGuides(projectRoot, apiUrl, headers) {
107
338
  if (!Array.isArray(guides) || guides.length === 0) {
108
339
  return 0;
109
340
  }
110
- const baseDir = join(projectRoot, CONVENTION_DIR, 'platform', 'guides');
341
+ const baseDir = join(projectRoot, CONVENTION_DIR, 'platform');
111
342
  rmSync(baseDir, { recursive: true, force: true });
112
343
  mkdirSync(baseDir, { recursive: true });
113
344
  const fileNameCount = new Map();
@@ -164,51 +395,269 @@ export async function conventionDownload(options) {
164
395
  if (!existsSync(conventionRoot)) {
165
396
  throw new Error(`Convention directory not found: ${conventionRoot}\nRun 'agentteams init' first.`);
166
397
  }
167
- const hasReportingTemplate = await downloadReportingTemplate(projectRoot, config, apiUrl, headers);
168
- const platformGuideCount = await downloadPlatformGuides(projectRoot, apiUrl, headers);
169
- const listResponse = await axios.get(`${apiUrl}/api/projects/${config.projectId}/conventions`, { headers });
170
- const conventions = listResponse.data?.data;
398
+ const hasReportingTemplate = await withSpinner('Downloading reporting template...', () => downloadReportingTemplate(projectRoot, config, apiUrl, headers));
399
+ const platformGuideCount = await withSpinner('Downloading platform guides...', () => downloadPlatformGuides(projectRoot, apiUrl, headers));
400
+ const conventions = await withSpinner('Downloading conventions...', async () => {
401
+ const conventionList = await fetchConventionsWithContent(apiUrl, config.projectId, headers);
402
+ if (!conventionList || conventionList.length === 0) {
403
+ return conventionList;
404
+ }
405
+ const legacyDir = join(projectRoot, CONVENTION_DIR, LEGACY_CONVENTION_DOWNLOAD_DIR);
406
+ rmSync(legacyDir, { recursive: true, force: true });
407
+ const categoryDirs = new Set();
408
+ for (const convention of conventionList) {
409
+ const categoryName = typeof convention.category === "string" ? convention.category : "";
410
+ categoryDirs.add(toSafeDirectoryName(categoryName));
411
+ }
412
+ for (const categoryDir of categoryDirs) {
413
+ rmSync(join(projectRoot, CONVENTION_DIR, categoryDir), { recursive: true, force: true });
414
+ mkdirSync(join(projectRoot, CONVENTION_DIR, categoryDir), { recursive: true });
415
+ }
416
+ const fileNameCount = new Map();
417
+ const platformGuidesHash = await fetchPlatformGuidesHash(apiUrl, headers);
418
+ const manifest = {
419
+ version: 1,
420
+ generatedAt: new Date().toISOString(),
421
+ platformGuidesHash,
422
+ entries: [],
423
+ };
424
+ for (const convention of conventionList) {
425
+ const contentMarkdown = typeof convention.contentMarkdown === "string" ? convention.contentMarkdown : "";
426
+ const baseFileName = buildConventionFileName(convention);
427
+ const categoryName = typeof convention.category === "string" ? convention.category : "";
428
+ const categoryDir = toSafeDirectoryName(categoryName);
429
+ const duplicateKey = `${categoryDir}/${baseFileName}`;
430
+ const seenCount = fileNameCount.get(duplicateKey) ?? 0;
431
+ fileNameCount.set(duplicateKey, seenCount + 1);
432
+ const fileName = seenCount === 0
433
+ ? baseFileName
434
+ : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
435
+ const filePath = join(projectRoot, CONVENTION_DIR, categoryDir, fileName);
436
+ writeFileSync(filePath, contentMarkdown, "utf-8");
437
+ manifest.entries.push({
438
+ conventionId: String(convention.id),
439
+ fileRelativePath: normalizeRelativePath(relative(projectRoot, filePath)),
440
+ fileName,
441
+ categoryDir,
442
+ title: toOptionalString(convention.title),
443
+ category: toOptionalString(convention.category),
444
+ updatedAt: toOptionalString(convention.updatedAt),
445
+ downloadedAt: new Date().toISOString(),
446
+ });
447
+ }
448
+ writeManifest(projectRoot, manifest);
449
+ return conventionList;
450
+ });
171
451
  if (!conventions || conventions.length === 0) {
172
452
  if (hasReportingTemplate) {
173
453
  const platformLine = platformGuideCount > 0
174
- ? `\nDownloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform/guides`
454
+ ? `\nDownloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform`
175
455
  : '';
176
456
  return `Convention sync completed.\nUpdated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\nNo project conventions found.${platformLine}`;
177
457
  }
178
458
  throw new Error("No conventions found for this project. Create one via the web dashboard first.");
179
459
  }
180
- const legacyDir = join(projectRoot, CONVENTION_DIR, LEGACY_CONVENTION_DOWNLOAD_DIR);
181
- rmSync(legacyDir, { recursive: true, force: true });
182
- const categoryDirs = new Set();
183
- for (const convention of conventions) {
184
- const categoryName = typeof convention.category === "string" ? convention.category : "";
185
- categoryDirs.add(toSafeDirectoryName(categoryName));
186
- }
187
- for (const categoryDir of categoryDirs) {
188
- rmSync(join(projectRoot, CONVENTION_DIR, categoryDir), { recursive: true, force: true });
189
- mkdirSync(join(projectRoot, CONVENTION_DIR, categoryDir), { recursive: true });
190
- }
191
- const fileNameCount = new Map();
192
- for (const convention of conventions) {
193
- const downloadResponse = await axios.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${convention.id}/download`, { headers, responseType: "text" });
194
- const baseFileName = buildConventionFileName(convention);
195
- const categoryName = typeof convention.category === "string" ? convention.category : "";
196
- const categoryDir = toSafeDirectoryName(categoryName);
197
- const duplicateKey = `${categoryDir}/${baseFileName}`;
198
- const seenCount = fileNameCount.get(duplicateKey) ?? 0;
199
- fileNameCount.set(duplicateKey, seenCount + 1);
200
- const fileName = seenCount === 0
201
- ? baseFileName
202
- : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
203
- const filePath = join(projectRoot, CONVENTION_DIR, categoryDir, fileName);
204
- writeFileSync(filePath, downloadResponse.data, "utf-8");
205
- }
206
460
  const reportingLine = hasReportingTemplate
207
461
  ? `Updated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\n`
208
462
  : "";
209
463
  const platformLine = platformGuideCount > 0
210
- ? `Downloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform/guides\n`
464
+ ? `Downloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform\n`
211
465
  : "";
212
466
  return `Convention sync completed.\n${reportingLine}${platformLine}Downloaded ${conventions.length} file(s) into category directories under ${CONVENTION_DIR}`;
213
467
  }
468
+ export async function conventionCreate(options) {
469
+ const { config, apiUrl, headers } = getApiConfigOrThrow(options);
470
+ const projectRoot = findProjectRoot(options?.cwd);
471
+ if (!projectRoot) {
472
+ throw new Error("No .agentteams directory found. Run 'agentteams init' first.");
473
+ }
474
+ const manifest = loadManifestOrCreate(projectRoot);
475
+ const files = toFileList(options.file);
476
+ const results = [];
477
+ for (const fileInput of files) {
478
+ const cwd = options.cwd ?? process.cwd();
479
+ const absolutePath = resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput);
480
+ if (!existsSync(absolutePath)) {
481
+ throw new Error(`File not found: ${absolutePath}`);
482
+ }
483
+ const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
484
+ const category = parseCategoryFromAgentteamsPath(fileRelativePath);
485
+ const fileName = basename(absolutePath);
486
+ if (!fileName.toLowerCase().endsWith(".md")) {
487
+ throw new Error(`Convention create requires a .md file: ${fileRelativePath}`);
488
+ }
489
+ const existingEntry = manifest.entries.find((e) => e.fileRelativePath === fileRelativePath);
490
+ if (existingEntry) {
491
+ throw new Error(`File is already tracked in the manifest (use update instead): ${fileRelativePath}\n` +
492
+ `- conventionId: ${existingEntry.conventionId}`);
493
+ }
494
+ const localMarkdown = readFileSync(absolutePath, "utf-8");
495
+ const parsed = matter(localMarkdown);
496
+ const frontmatter = (parsed.data ?? {});
497
+ const bodyMarkdown = String(parsed.content ?? "");
498
+ const content = await normalizeMarkdownToTiptap(bodyMarkdown);
499
+ const title = toOptionalString(frontmatter.title)?.trim() || fileNameToTitle(fileName);
500
+ const payload = {
501
+ title,
502
+ category,
503
+ fileName,
504
+ content,
505
+ };
506
+ const trigger = toOptionalString(frontmatter.trigger)?.trim();
507
+ const description = toOptionalString(frontmatter.description)?.trim();
508
+ const agentInstruction = toOptionalString(frontmatter.agentInstruction);
509
+ if (trigger)
510
+ payload.trigger = trigger;
511
+ if (description)
512
+ payload.description = description;
513
+ if (typeof agentInstruction === "string" && agentInstruction.trim().length > 0) {
514
+ payload.agentInstruction = agentInstruction.trimEnd();
515
+ }
516
+ const response = await withSpinner(`Creating convention for ${fileRelativePath}...`, () => axios.post(`${apiUrl}/api/projects/${config.projectId}/conventions`, payload, { headers }));
517
+ const created = response.data?.data;
518
+ const createdId = typeof created?.id === "string" ? created.id : "unknown";
519
+ const createdUpdatedAt = typeof created?.updatedAt === "string" ? created.updatedAt : undefined;
520
+ const now = new Date().toISOString();
521
+ manifest.generatedAt = now;
522
+ manifest.entries.push({
523
+ conventionId: createdId,
524
+ fileRelativePath,
525
+ fileName,
526
+ categoryDir: category,
527
+ title,
528
+ category,
529
+ ...(createdUpdatedAt ? { updatedAt: createdUpdatedAt } : {}),
530
+ downloadedAt: now,
531
+ lastUploadedAt: now,
532
+ ...(createdUpdatedAt ? { lastKnownUpdatedAt: createdUpdatedAt } : {}),
533
+ });
534
+ writeManifest(projectRoot, manifest);
535
+ results.push(`[OK] ${fileRelativePath}: Created. (conventionId=${createdId})`);
536
+ results.push(`[OK] ${CONVENTION_DIR}/${CONVENTION_MANIFEST_FILE}: Updated.`);
537
+ results.push(`[NEXT] Run 'agentteams convention download' to refresh convention.md and canonical markdown.`);
538
+ }
539
+ return results.join("\n");
540
+ }
541
+ export async function conventionUpdate(options) {
542
+ const { config, apiUrl, headers } = getApiConfigOrThrow(options);
543
+ const projectRoot = findProjectRoot(options?.cwd);
544
+ if (!projectRoot) {
545
+ throw new Error("No .agentteams directory found. Run 'agentteams init' first.");
546
+ }
547
+ const manifest = loadManifestOrThrow(projectRoot);
548
+ const files = toFileList(options.file);
549
+ const apply = options.apply === true;
550
+ const results = [];
551
+ for (const fileInput of files) {
552
+ const cwd = options.cwd ?? process.cwd();
553
+ const absolutePath = resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput);
554
+ const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
555
+ const manifestEntry = manifest.entries.find((e) => e.fileRelativePath === fileRelativePath);
556
+ if (!manifestEntry) {
557
+ const available = manifest.entries
558
+ .map((e) => e.fileRelativePath)
559
+ .sort()
560
+ .slice(0, 30);
561
+ throw new Error(`Only downloaded convention files can be updated: ${fileInput}\n` +
562
+ `- resolved: ${absolutePath}\n` +
563
+ `- relative: ${fileRelativePath}\n` +
564
+ `Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
565
+ (available.length > 0 ? `Examples (partial):\n- ${available.join("\n- ")}` : ""));
566
+ }
567
+ const conventionId = manifestEntry.conventionId;
568
+ const [serverDetail, serverMarkdown, localMarkdown] = await withSpinner(`Preparing update for ${fileRelativePath}...`, async () => {
569
+ const detailResponse = await axios.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, { headers });
570
+ const downloadResponse = await axios.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}/download`, { headers, responseType: "text" });
571
+ const local = readFileSync(absolutePath, "utf-8");
572
+ return [detailResponse.data?.data, String(downloadResponse.data), local];
573
+ });
574
+ if (!hasAnyDiff(serverMarkdown, localMarkdown)) {
575
+ results.push(`[SKIP] ${fileRelativePath}: No changes detected.`);
576
+ continue;
577
+ }
578
+ const diffText = createUnifiedDiff(fileRelativePath, serverMarkdown, localMarkdown);
579
+ results.push(diffText.trimEnd());
580
+ if (!apply) {
581
+ results.push(`[DRY-RUN] ${fileRelativePath}: Printed diff only (no server changes).`);
582
+ continue;
583
+ }
584
+ const parsed = matter(localMarkdown);
585
+ const frontmatter = (parsed.data ?? {});
586
+ const bodyMarkdown = String(parsed.content ?? "");
587
+ const content = await normalizeMarkdownToTiptap(bodyMarkdown);
588
+ if (typeof serverDetail?.updatedAt !== "string" || serverDetail.updatedAt.length === 0) {
589
+ throw new Error(`[ERROR] ${fileRelativePath}: Server response is missing updatedAt.`);
590
+ }
591
+ const payload = {
592
+ updatedAt: serverDetail.updatedAt,
593
+ content,
594
+ };
595
+ const trigger = toOptionalStringOrNullIfPresent(frontmatter, "trigger");
596
+ const description = toOptionalStringOrNullIfPresent(frontmatter, "description");
597
+ const agentInstruction = toOptionalStringOrNullIfPresent(frontmatter, "agentInstruction");
598
+ if (trigger !== undefined)
599
+ payload.trigger = trigger;
600
+ if (description !== undefined)
601
+ payload.description = description;
602
+ if (agentInstruction !== undefined)
603
+ payload.agentInstruction = agentInstruction;
604
+ const updatedResponse = await withSpinner(`Uploading ${fileRelativePath}...`, () => axios.put(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, payload, { headers }));
605
+ const newUpdatedAt = updatedResponse.data?.data?.updatedAt;
606
+ const now = new Date().toISOString();
607
+ manifestEntry.lastUploadedAt = now;
608
+ if (typeof newUpdatedAt === "string") {
609
+ manifestEntry.lastKnownUpdatedAt = newUpdatedAt;
610
+ }
611
+ writeManifest(projectRoot, manifest);
612
+ results.push(`[OK] ${fileRelativePath}: Update applied. (conventionId=${conventionId})`);
613
+ }
614
+ return results.join("\n\n");
615
+ }
616
+ export async function conventionDelete(options) {
617
+ const { config, apiUrl, headers } = getApiConfigOrThrow(options);
618
+ const projectRoot = findProjectRoot(options?.cwd);
619
+ if (!projectRoot) {
620
+ throw new Error("No .agentteams directory found. Run 'agentteams init' first.");
621
+ }
622
+ const manifest = loadManifestOrThrow(projectRoot);
623
+ const files = toFileList(options.file);
624
+ const apply = options.apply === true;
625
+ const results = [];
626
+ for (const fileInput of files) {
627
+ const cwd = options.cwd ?? process.cwd();
628
+ const absolutePath = resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput);
629
+ const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
630
+ const entryIndex = manifest.entries.findIndex((e) => e.fileRelativePath === fileRelativePath);
631
+ if (entryIndex === -1) {
632
+ const available = manifest.entries
633
+ .map((e) => e.fileRelativePath)
634
+ .sort()
635
+ .slice(0, 30);
636
+ throw new Error(`Only downloaded convention files can be deleted: ${fileInput}\n` +
637
+ `- resolved: ${absolutePath}\n` +
638
+ `- relative: ${fileRelativePath}\n` +
639
+ `Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
640
+ (available.length > 0 ? `Examples (partial):\n- ${available.join("\n- ")}` : ""));
641
+ }
642
+ const entry = manifest.entries[entryIndex];
643
+ const conventionId = entry.conventionId;
644
+ results.push(`[PLAN] ${fileRelativePath}: Will delete conventionId=${conventionId}`);
645
+ if (!apply) {
646
+ results.push(`[DRY-RUN] ${fileRelativePath}: Planned only (no server delete).`);
647
+ continue;
648
+ }
649
+ await withSpinner(`Deleting convention for ${fileRelativePath}...`, () => axios.delete(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, { headers: withoutJsonContentType(headers) }));
650
+ // After a successful server delete, also clean up local files/manifest.
651
+ try {
652
+ unlinkSync(absolutePath);
653
+ }
654
+ catch {
655
+ // ignore
656
+ }
657
+ manifest.entries.splice(entryIndex, 1);
658
+ writeManifest(projectRoot, manifest);
659
+ results.push(`[OK] ${fileRelativePath}: Deleted. (conventionId=${conventionId})`);
660
+ }
661
+ return results.join("\n");
662
+ }
214
663
  //# sourceMappingURL=convention.js.map