@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.
- package/API-CLI-WORKFLOW.md +418 -0
- package/LICENSE +201 -0
- package/README.md +220 -128
- package/dist/api/comment.d.ts +11 -0
- package/dist/api/comment.d.ts.map +1 -0
- package/dist/api/comment.js +33 -0
- package/dist/api/comment.js.map +1 -0
- package/dist/api/plan.d.ts +13 -0
- package/dist/api/plan.d.ts.map +1 -0
- package/dist/api/plan.js +43 -0
- package/dist/api/plan.js.map +1 -0
- package/dist/api/postmortem.d.ts +6 -0
- package/dist/api/postmortem.d.ts.map +1 -0
- package/dist/api/postmortem.js +33 -0
- package/dist/api/postmortem.js.map +1 -0
- package/dist/api/report.d.ts +6 -0
- package/dist/api/report.d.ts.map +1 -0
- package/dist/api/report.js +33 -0
- package/dist/api/report.js.map +1 -0
- package/dist/api/status.d.ts +12 -0
- package/dist/api/status.d.ts.map +1 -0
- package/dist/api/status.js +33 -0
- package/dist/api/status.js.map +1 -0
- package/dist/commands/agentConfig.d.ts.map +1 -1
- package/dist/commands/agentConfig.js +10 -3
- package/dist/commands/agentConfig.js.map +1 -1
- package/dist/commands/agentConfigCommand.d.ts +2 -0
- package/dist/commands/agentConfigCommand.d.ts.map +1 -0
- package/dist/commands/agentConfigCommand.js +20 -0
- package/dist/commands/agentConfigCommand.js.map +1 -0
- package/dist/commands/comment.d.ts +2 -0
- package/dist/commands/comment.d.ts.map +1 -0
- package/dist/commands/comment.js +55 -0
- package/dist/commands/comment.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +30 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/convention.d.ts +25 -0
- package/dist/commands/convention.d.ts.map +1 -1
- package/dist/commands/convention.js +492 -43
- package/dist/commands/convention.js.map +1 -1
- package/dist/commands/conventionRouter.d.ts +3 -0
- package/dist/commands/conventionRouter.d.ts.map +1 -0
- package/dist/commands/conventionRouter.js +46 -0
- package/dist/commands/conventionRouter.js.map +1 -0
- package/dist/commands/dependency.d.ts.map +1 -1
- package/dist/commands/dependency.js +2 -1
- package/dist/commands/dependency.js.map +1 -1
- package/dist/commands/dependencyCommand.d.ts +2 -0
- package/dist/commands/dependencyCommand.d.ts.map +1 -0
- package/dist/commands/dependencyCommand.js +27 -0
- package/dist/commands/dependencyCommand.js.map +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +13 -485
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/plan.d.ts +11 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +370 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/postmortem.d.ts +2 -0
- package/dist/commands/postmortem.d.ts.map +1 -0
- package/dist/commands/postmortem.js +114 -0
- package/dist/commands/postmortem.js.map +1 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +221 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +60 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/index.js +214 -34
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +11 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +60 -6
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/formatter.js +11 -1
- package/dist/utils/formatter.js.map +1 -1
- package/dist/utils/git.d.ts +19 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +41 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/httpHeaders.d.ts +3 -0
- package/dist/utils/httpHeaders.d.ts.map +1 -0
- package/dist/utils/httpHeaders.js +11 -0
- package/dist/utils/httpHeaders.js.map +1 -0
- package/dist/utils/initOutput.d.ts +3 -0
- package/dist/utils/initOutput.d.ts.map +1 -0
- package/dist/utils/initOutput.js +34 -0
- package/dist/utils/initOutput.js.map +1 -0
- package/dist/utils/legacyCompat.d.ts +3 -0
- package/dist/utils/legacyCompat.d.ts.map +1 -0
- package/dist/utils/legacyCompat.js +20 -0
- package/dist/utils/legacyCompat.js.map +1 -0
- package/dist/utils/outputPolicy.d.ts +12 -0
- package/dist/utils/outputPolicy.d.ts.map +1 -0
- package/dist/utils/outputPolicy.js +132 -0
- package/dist/utils/outputPolicy.js.map +1 -0
- package/dist/utils/parsers.d.ts +7 -0
- package/dist/utils/parsers.d.ts.map +1 -0
- package/dist/utils/parsers.js +52 -0
- package/dist/utils/parsers.js.map +1 -0
- package/dist/utils/planFormat.d.ts +17 -0
- package/dist/utils/planFormat.d.ts.map +1 -0
- package/dist/utils/planFormat.js +80 -0
- package/dist/utils/planFormat.js.map +1 -0
- package/dist/utils/spinner.d.ts +6 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +34 -0
- package/dist/utils/spinner.js.map +1 -0
- package/package.json +8 -3
- 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
|
|
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
|
|
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${
|
|
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
|
|
50
|
-
const conventions = response.data?.data;
|
|
276
|
+
const conventions = await fetchAllConventions(apiUrl, config.projectId, headers);
|
|
51
277
|
if (!Array.isArray(conventions)) {
|
|
52
|
-
return
|
|
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:
|
|
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'
|
|
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
|
|
170
|
-
|
|
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
|
|
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
|
|
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
|