@agentteams/cli 0.1.56 → 0.1.58-dev.224
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/.prettierignore +4 -0
- package/.prettierrc.json +6 -0
- package/README.md +7 -7
- package/dist/api/coaction.d.ts.map +1 -1
- package/dist/api/coaction.js +3 -9
- package/dist/api/coaction.js.map +1 -1
- package/dist/api/codeReview.d.ts.map +1 -1
- package/dist/api/codeReview.js +1 -3
- package/dist/api/codeReview.js.map +1 -1
- package/dist/api/comment.d.ts.map +1 -1
- package/dist/api/comment.js +1 -3
- package/dist/api/comment.js.map +1 -1
- package/dist/api/document.d.ts.map +1 -1
- package/dist/api/document.js +8 -4
- package/dist/api/document.js.map +1 -1
- package/dist/api/feedback.d.ts +1 -1
- package/dist/api/feedback.js +1 -1
- package/dist/api/linear.d.ts.map +1 -1
- package/dist/api/linear.js.map +1 -1
- package/dist/api/member.d.ts +1 -1
- package/dist/api/plan.d.ts.map +1 -1
- package/dist/api/plan.js +1 -3
- package/dist/api/plan.js.map +1 -1
- package/dist/api/postmortem.d.ts.map +1 -1
- package/dist/api/postmortem.js +1 -3
- package/dist/api/postmortem.js.map +1 -1
- package/dist/api/report.d.ts.map +1 -1
- package/dist/api/report.js +1 -3
- package/dist/api/report.js.map +1 -1
- package/dist/commands/agentConfig.d.ts.map +1 -1
- package/dist/commands/agentConfig.js +9 -3
- package/dist/commands/agentConfig.js.map +1 -1
- package/dist/commands/agentConfigCommand.d.ts.map +1 -1
- package/dist/commands/agentConfigCommand.js.map +1 -1
- package/dist/commands/attachment.d.ts.map +1 -1
- package/dist/commands/attachment.js +31 -31
- package/dist/commands/attachment.js.map +1 -1
- package/dist/commands/coaction.d.ts.map +1 -1
- package/dist/commands/coaction.js +11 -9
- package/dist/commands/coaction.js.map +1 -1
- package/dist/commands/codeReview.d.ts.map +1 -1
- package/dist/commands/codeReview.js +5 -2
- package/dist/commands/codeReview.js.map +1 -1
- package/dist/commands/convention.d.ts +20 -2
- package/dist/commands/convention.d.ts.map +1 -1
- package/dist/commands/convention.js +152 -107
- package/dist/commands/convention.js.map +1 -1
- package/dist/commands/conventionRouter.d.ts.map +1 -1
- package/dist/commands/conventionRouter.js +4 -2
- package/dist/commands/conventionRouter.js.map +1 -1
- package/dist/commands/dependency.d.ts.map +1 -1
- package/dist/commands/dependency.js +3 -1
- package/dist/commands/dependency.js.map +1 -1
- package/dist/commands/dependencyCommand.d.ts.map +1 -1
- package/dist/commands/dependencyCommand.js.map +1 -1
- package/dist/commands/document.d.ts.map +1 -1
- package/dist/commands/document.js +79 -83
- package/dist/commands/document.js.map +1 -1
- package/dist/commands/feedback.js +13 -13
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +13 -14
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/linear.d.ts.map +1 -1
- package/dist/commands/linear.js +1 -1
- package/dist/commands/linear.js.map +1 -1
- package/dist/commands/plan.d.ts +1 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +104 -100
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/postmortem.d.ts.map +1 -1
- package/dist/commands/postmortem.js +6 -4
- package/dist/commands/postmortem.js.map +1 -1
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +12 -58
- package/dist/commands/report.js.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/atomicWrite.d.ts.map +1 -1
- package/dist/utils/atomicWrite.js +2 -2
- package/dist/utils/atomicWrite.js.map +1 -1
- package/dist/utils/authServer.d.ts.map +1 -1
- package/dist/utils/authServer.js +2 -5
- package/dist/utils/authServer.js.map +1 -1
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +9 -15
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +1 -3
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/httpClient.d.ts +1 -1
- package/dist/utils/httpClient.d.ts.map +1 -1
- package/dist/utils/httpClient.js +7 -7
- package/dist/utils/httpClient.js.map +1 -1
- package/dist/utils/initOutput.d.ts.map +1 -1
- package/dist/utils/initOutput.js.map +1 -1
- package/dist/utils/outputPolicy.d.ts.map +1 -1
- package/dist/utils/outputPolicy.js +1 -3
- package/dist/utils/outputPolicy.js.map +1 -1
- package/dist/utils/parsers.d.ts +11 -3
- package/dist/utils/parsers.d.ts.map +1 -1
- package/dist/utils/parsers.js +34 -17
- package/dist/utils/parsers.js.map +1 -1
- package/dist/utils/planFormat.d.ts.map +1 -1
- package/dist/utils/planFormat.js +1 -1
- package/dist/utils/planFormat.js.map +1 -1
- package/dist/utils/planPreviewHtmlSafety.d.ts.map +1 -1
- package/dist/utils/planPreviewHtmlSafety.js +15 -15
- package/dist/utils/planPreviewHtmlSafety.js.map +1 -1
- package/dist/utils/report.d.ts +22 -0
- package/dist/utils/report.d.ts.map +1 -0
- package/dist/utils/report.js +79 -0
- package/dist/utils/report.js.map +1 -0
- package/dist/utils/spinner.d.ts.map +1 -1
- package/dist/utils/spinner.js.map +1 -1
- package/dist/utils/updateCheck.d.ts.map +1 -1
- package/dist/utils/updateCheck.js +11 -3
- package/dist/utils/updateCheck.js.map +1 -1
- package/eslint.config.mjs +111 -0
- package/package.json +9 -2
- package/tsconfig.test.json +10 -0
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from
|
|
2
|
-
import { atomicWriteFileSync } from
|
|
3
|
-
import { basename, join, relative, resolve, sep } from
|
|
4
|
-
import httpClient from
|
|
5
|
-
import { isAxiosError } from
|
|
6
|
-
import matter from
|
|
7
|
-
import { diffLines, createTwoFilesPatch } from
|
|
8
|
-
import { loadConfig, findProjectConfig } from
|
|
9
|
-
import { withSpinner } from
|
|
10
|
-
import { withoutJsonContentType } from
|
|
11
|
-
const CONVENTION_DIR =
|
|
12
|
-
const LEGACY_CONVENTION_DOWNLOAD_DIR =
|
|
13
|
-
const CONVENTION_INDEX_FILE =
|
|
14
|
-
const CONVENTION_MANIFEST_FILE =
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { atomicWriteFileSync } from '../utils/atomicWrite.js';
|
|
3
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import httpClient from '../utils/httpClient.js';
|
|
5
|
+
import { isAxiosError } from 'axios';
|
|
6
|
+
import matter from 'gray-matter';
|
|
7
|
+
import { diffLines, createTwoFilesPatch } from 'diff';
|
|
8
|
+
import { loadConfig, findProjectConfig } from '../utils/config.js';
|
|
9
|
+
import { withSpinner } from '../utils/spinner.js';
|
|
10
|
+
import { withoutJsonContentType } from '../utils/httpHeaders.js';
|
|
11
|
+
const CONVENTION_DIR = '.agentteams';
|
|
12
|
+
const LEGACY_CONVENTION_DOWNLOAD_DIR = 'conventions';
|
|
13
|
+
const CONVENTION_INDEX_FILE = 'convention.md';
|
|
14
|
+
const CONVENTION_MANIFEST_FILE = 'conventions.manifest.json';
|
|
15
15
|
function findProjectRoot(cwd) {
|
|
16
16
|
const configPath = findProjectConfig(cwd ?? process.cwd());
|
|
17
17
|
if (!configPath)
|
|
18
18
|
return null;
|
|
19
19
|
// configPath = /path/.agentteams/config.json → resolve up 2 levels to project root
|
|
20
|
-
return resolve(configPath,
|
|
20
|
+
return resolve(configPath, '..', '..');
|
|
21
21
|
}
|
|
22
22
|
function getApiBaseUrl(apiUrl) {
|
|
23
|
-
return apiUrl.endsWith(
|
|
23
|
+
return apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
|
|
24
24
|
}
|
|
25
25
|
function getApiConfigOrThrow(options) {
|
|
26
26
|
const config = options?.config ?? loadConfig();
|
|
@@ -31,13 +31,13 @@ function getApiConfigOrThrow(options) {
|
|
|
31
31
|
config,
|
|
32
32
|
apiUrl: getApiBaseUrl(config.apiUrl),
|
|
33
33
|
headers: {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
'X-API-Key': config.apiKey,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
function normalizeRelativePath(input) {
|
|
40
|
-
return input.replaceAll(
|
|
40
|
+
return input.replaceAll('\\', '/');
|
|
41
41
|
}
|
|
42
42
|
function resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput) {
|
|
43
43
|
// If absolute path, keep as-is.
|
|
@@ -59,7 +59,7 @@ function resolveConventionFileAbsolutePath(projectRoot, cwd, fileInput) {
|
|
|
59
59
|
function validatePathBoundary(absolutePath, projectRoot) {
|
|
60
60
|
const normalized = resolve(absolutePath);
|
|
61
61
|
if (!normalized.startsWith(resolve(projectRoot) + sep)) {
|
|
62
|
-
throw new Error(
|
|
62
|
+
throw new Error('Path traversal detected: file must be within project root');
|
|
63
63
|
}
|
|
64
64
|
return normalized;
|
|
65
65
|
}
|
|
@@ -71,7 +71,7 @@ function loadManifestOrThrow(projectRoot) {
|
|
|
71
71
|
if (!existsSync(manifestPath)) {
|
|
72
72
|
throw new Error(`Download manifest not found: ${manifestPath}\nRun 'agentteams convention download' first.`);
|
|
73
73
|
}
|
|
74
|
-
const raw = readFileSync(manifestPath,
|
|
74
|
+
const raw = readFileSync(manifestPath, 'utf-8');
|
|
75
75
|
const parsed = JSON.parse(raw);
|
|
76
76
|
if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
77
77
|
throw new Error(`Invalid manifest format: ${manifestPath}`);
|
|
@@ -87,7 +87,7 @@ function loadManifestOrCreate(projectRoot) {
|
|
|
87
87
|
entries: [],
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
-
const raw = readFileSync(manifestPath,
|
|
90
|
+
const raw = readFileSync(manifestPath, 'utf-8');
|
|
91
91
|
const parsed = JSON.parse(raw);
|
|
92
92
|
if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
93
93
|
throw new Error(`Invalid manifest format: ${manifestPath}`);
|
|
@@ -96,7 +96,7 @@ function loadManifestOrCreate(projectRoot) {
|
|
|
96
96
|
}
|
|
97
97
|
function writeManifest(projectRoot, manifest) {
|
|
98
98
|
const manifestPath = buildManifestPath(projectRoot);
|
|
99
|
-
atomicWriteFileSync(manifestPath, JSON.stringify(manifest, null, 2) +
|
|
99
|
+
atomicWriteFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
100
100
|
}
|
|
101
101
|
function toFileList(input) {
|
|
102
102
|
return Array.isArray(input) ? input : [input];
|
|
@@ -106,21 +106,19 @@ function hasAnyDiff(a, b) {
|
|
|
106
106
|
return parts.some((p) => p.added || p.removed);
|
|
107
107
|
}
|
|
108
108
|
function createUnifiedDiff(fileLabel, serverText, localText) {
|
|
109
|
-
return createTwoFilesPatch(`${fileLabel} (server)`, `${fileLabel} (local)`, serverText, localText,
|
|
109
|
+
return createTwoFilesPatch(`${fileLabel} (server)`, `${fileLabel} (local)`, serverText, localText, '', '', {
|
|
110
|
+
context: 3,
|
|
111
|
+
});
|
|
110
112
|
}
|
|
111
113
|
function toOptionalString(value) {
|
|
112
|
-
return typeof value ===
|
|
114
|
+
return typeof value === 'string' ? value : undefined;
|
|
113
115
|
}
|
|
114
116
|
function fileNameToTitle(fileName) {
|
|
115
|
-
return fileName
|
|
116
|
-
.replace(/\.md$/i, "")
|
|
117
|
-
.replace(/[-_]+/g, " ")
|
|
118
|
-
.replace(/\s+/g, " ")
|
|
119
|
-
.trim();
|
|
117
|
+
return fileName.replace(/\.md$/i, '').replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
120
118
|
}
|
|
121
119
|
function parseCategoryFromAgentteamsPath(fileRelativePath) {
|
|
122
120
|
const normalized = normalizeRelativePath(fileRelativePath);
|
|
123
|
-
const parts = normalized.split(
|
|
121
|
+
const parts = normalized.split('/');
|
|
124
122
|
const agentteamsIndex = parts.indexOf(CONVENTION_DIR);
|
|
125
123
|
if (agentteamsIndex === -1) {
|
|
126
124
|
throw new Error(`Convention create requires a file under ${CONVENTION_DIR}/<category>/: ${fileRelativePath}`);
|
|
@@ -129,7 +127,7 @@ function parseCategoryFromAgentteamsPath(fileRelativePath) {
|
|
|
129
127
|
if (!category || category.length === 0) {
|
|
130
128
|
throw new Error(`Convention create requires a category directory under ${CONVENTION_DIR}/: ${fileRelativePath}`);
|
|
131
129
|
}
|
|
132
|
-
if (category ===
|
|
130
|
+
if (category === 'platform' || category === 'active-plan') {
|
|
133
131
|
throw new Error(`Convention create does not allow reserved directories under ${CONVENTION_DIR}/: ${category}`);
|
|
134
132
|
}
|
|
135
133
|
return category;
|
|
@@ -140,14 +138,17 @@ async function fetchAllConventions(apiUrl, projectId, headers) {
|
|
|
140
138
|
let totalPages;
|
|
141
139
|
const items = [];
|
|
142
140
|
while (true) {
|
|
143
|
-
const response = await httpClient.get(`${apiUrl}/api/projects/${projectId}/conventions`, {
|
|
141
|
+
const response = await httpClient.get(`${apiUrl}/api/projects/${projectId}/conventions`, {
|
|
142
|
+
headers,
|
|
143
|
+
params: { page, pageSize },
|
|
144
|
+
});
|
|
144
145
|
const data = response.data?.data;
|
|
145
146
|
if (!Array.isArray(data)) {
|
|
146
147
|
break;
|
|
147
148
|
}
|
|
148
149
|
items.push(...data);
|
|
149
150
|
const meta = response.data?.meta;
|
|
150
|
-
if (typeof meta?.totalPages ===
|
|
151
|
+
if (typeof meta?.totalPages === 'number') {
|
|
151
152
|
totalPages = meta.totalPages;
|
|
152
153
|
}
|
|
153
154
|
if (totalPages !== undefined) {
|
|
@@ -167,32 +168,32 @@ async function fetchConventionsWithContent(apiUrl, projectId, headers) {
|
|
|
167
168
|
const response = await httpClient.get(`${apiUrl}/api/projects/${projectId}/conventions/download-all`, { headers });
|
|
168
169
|
const data = response.data?.data;
|
|
169
170
|
if (!Array.isArray(data)) {
|
|
170
|
-
throw new Error(
|
|
171
|
+
throw new Error('Invalid download-all response format');
|
|
171
172
|
}
|
|
172
173
|
return data;
|
|
173
174
|
}
|
|
174
175
|
async function fetchPlatformGuidesHash(apiUrl, headers) {
|
|
175
176
|
const response = await httpClient.get(`${apiUrl}/api/platform/guides/hash`, { headers });
|
|
176
177
|
const hash = response.data?.data?.hash;
|
|
177
|
-
if (typeof hash !==
|
|
178
|
-
throw new Error(
|
|
178
|
+
if (typeof hash !== 'string' || hash.length === 0) {
|
|
179
|
+
throw new Error('Invalid platform guides hash response format');
|
|
179
180
|
}
|
|
180
181
|
return hash;
|
|
181
182
|
}
|
|
182
183
|
function toConventionName(convention) {
|
|
183
|
-
const title = typeof convention.title ===
|
|
184
|
+
const title = typeof convention.title === 'string' ? convention.title.trim() : '';
|
|
184
185
|
if (title.length > 0)
|
|
185
186
|
return title;
|
|
186
|
-
const fileName = typeof convention.fileName ===
|
|
187
|
+
const fileName = typeof convention.fileName === 'string' ? convention.fileName.trim() : '';
|
|
187
188
|
if (fileName.length > 0)
|
|
188
189
|
return fileName;
|
|
189
190
|
return convention.id;
|
|
190
191
|
}
|
|
191
192
|
function toConventionNameFromManifest(entry) {
|
|
192
|
-
const title = typeof entry.title ===
|
|
193
|
+
const title = typeof entry.title === 'string' ? entry.title.trim() : '';
|
|
193
194
|
if (title.length > 0)
|
|
194
195
|
return title;
|
|
195
|
-
const fileName = typeof entry.fileName ===
|
|
196
|
+
const fileName = typeof entry.fileName === 'string' ? entry.fileName.trim() : '';
|
|
196
197
|
if (fileName.length > 0)
|
|
197
198
|
return fileName;
|
|
198
199
|
return entry.conventionId;
|
|
@@ -205,7 +206,7 @@ function toOptionalStringOrNullIfPresent(data, key) {
|
|
|
205
206
|
if (value === null) {
|
|
206
207
|
return null;
|
|
207
208
|
}
|
|
208
|
-
if (typeof value ===
|
|
209
|
+
if (typeof value === 'string') {
|
|
209
210
|
const trimmed = value.trim();
|
|
210
211
|
return trimmed.length > 0 ? trimmed : null;
|
|
211
212
|
}
|
|
@@ -215,15 +216,15 @@ export async function conventionShow() {
|
|
|
215
216
|
const { config, apiUrl, headers } = getApiConfigOrThrow();
|
|
216
217
|
const conventions = await fetchConventionsWithContent(apiUrl, config.projectId, headers);
|
|
217
218
|
if (!conventions || conventions.length === 0) {
|
|
218
|
-
throw new Error(
|
|
219
|
+
throw new Error('No conventions found for this project. Create one via the web dashboard first.');
|
|
219
220
|
}
|
|
220
221
|
const sections = [];
|
|
221
222
|
for (const convention of conventions) {
|
|
222
|
-
const contentMarkdown = typeof convention.contentMarkdown ===
|
|
223
|
-
const sectionHeader = `# ${convention.title ??
|
|
223
|
+
const contentMarkdown = typeof convention.contentMarkdown === 'string' ? convention.contentMarkdown : '';
|
|
224
|
+
const sectionHeader = `# ${convention.title ?? 'untitled'}\ncategory: ${convention.category ?? 'uncategorized'}\nid: ${convention.id}`;
|
|
224
225
|
sections.push(`${sectionHeader}\n\n${contentMarkdown}`);
|
|
225
226
|
}
|
|
226
|
-
return sections.join(
|
|
227
|
+
return sections.join('\n\n---\n\n');
|
|
227
228
|
}
|
|
228
229
|
export async function checkConventionFreshness(apiUrl, projectId, headers, projectRoot) {
|
|
229
230
|
const manifestPath = buildManifestPath(projectRoot);
|
|
@@ -235,9 +236,9 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
|
|
|
235
236
|
}
|
|
236
237
|
const manifest = loadManifestOrThrow(projectRoot);
|
|
237
238
|
const currentPlatformGuidesHash = await fetchPlatformGuidesHash(apiUrl, headers);
|
|
238
|
-
const platformGuidesChanged = typeof manifest.platformGuidesHash ===
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
const platformGuidesChanged = typeof manifest.platformGuidesHash === 'string' &&
|
|
240
|
+
manifest.platformGuidesHash.length > 0 &&
|
|
241
|
+
manifest.platformGuidesHash !== currentPlatformGuidesHash;
|
|
241
242
|
const serverConventions = await fetchAllConventions(apiUrl, projectId, headers);
|
|
242
243
|
const serverById = new Map(serverConventions.map((item) => [item.id, item]));
|
|
243
244
|
const localById = new Map(manifest.entries.map((entry) => [entry.conventionId, entry]));
|
|
@@ -247,18 +248,18 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
|
|
|
247
248
|
if (!local) {
|
|
248
249
|
conventionChanges.push({
|
|
249
250
|
id: serverConvention.id,
|
|
250
|
-
type:
|
|
251
|
+
type: 'new',
|
|
251
252
|
title: toConventionName(serverConvention),
|
|
252
253
|
fileName: serverConvention.fileName ?? undefined,
|
|
253
254
|
});
|
|
254
255
|
continue;
|
|
255
256
|
}
|
|
256
|
-
if (typeof serverConvention.updatedAt ===
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
if (typeof serverConvention.updatedAt === 'string' &&
|
|
258
|
+
typeof local.updatedAt === 'string' &&
|
|
259
|
+
serverConvention.updatedAt !== local.updatedAt) {
|
|
259
260
|
conventionChanges.push({
|
|
260
261
|
id: serverConvention.id,
|
|
261
|
-
type:
|
|
262
|
+
type: 'updated',
|
|
262
263
|
title: toConventionName(serverConvention),
|
|
263
264
|
fileName: serverConvention.fileName ?? local.fileName,
|
|
264
265
|
});
|
|
@@ -269,7 +270,7 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
|
|
|
269
270
|
continue;
|
|
270
271
|
conventionChanges.push({
|
|
271
272
|
id: localEntry.conventionId,
|
|
272
|
-
type:
|
|
273
|
+
type: 'deleted',
|
|
273
274
|
title: toConventionNameFromManifest(localEntry),
|
|
274
275
|
fileName: localEntry.fileName,
|
|
275
276
|
});
|
|
@@ -279,6 +280,56 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
|
|
|
279
280
|
conventionChanges,
|
|
280
281
|
};
|
|
281
282
|
}
|
|
283
|
+
export function buildStatusSummary(result) {
|
|
284
|
+
const parts = [];
|
|
285
|
+
if (result.platformGuidesChanged)
|
|
286
|
+
parts.push('platform guides');
|
|
287
|
+
const counts = { new: 0, updated: 0, deleted: 0 };
|
|
288
|
+
for (const change of result.conventionChanges)
|
|
289
|
+
counts[change.type] += 1;
|
|
290
|
+
if (counts.new > 0)
|
|
291
|
+
parts.push(`${counts.new} new`);
|
|
292
|
+
if (counts.updated > 0)
|
|
293
|
+
parts.push(`${counts.updated} updated`);
|
|
294
|
+
if (counts.deleted > 0)
|
|
295
|
+
parts.push(`${counts.deleted} deleted`);
|
|
296
|
+
if (parts.length === 0)
|
|
297
|
+
return '✓ Conventions up to date';
|
|
298
|
+
return `⚠ Updates available (${parts.join(', ')}). Run 'agentteams convention download' to sync.`;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Read-only freshness check exposed as `agentteams convention status`.
|
|
302
|
+
*
|
|
303
|
+
* Compares the local download manifest against the server and reports whether an
|
|
304
|
+
* update is available — it never downloads or mutates anything. Degrades gracefully
|
|
305
|
+
* (exit 0, updateAvailable=false) when the project is not configured or has no local
|
|
306
|
+
* conventions yet, so callers can safely "check then skip when unavailable".
|
|
307
|
+
*/
|
|
308
|
+
export async function conventionStatus(options) {
|
|
309
|
+
const config = options?.config ?? loadConfig();
|
|
310
|
+
const projectRoot = findProjectRoot(options?.cwd);
|
|
311
|
+
// Not configured or no local conventions → nothing to compare; treat as up to date.
|
|
312
|
+
if (!config || !projectRoot) {
|
|
313
|
+
return {
|
|
314
|
+
updateAvailable: false,
|
|
315
|
+
platformGuidesChanged: false,
|
|
316
|
+
conventionChanges: [],
|
|
317
|
+
summary: '✓ Conventions up to date',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const apiUrl = getApiBaseUrl(config.apiUrl);
|
|
321
|
+
const headers = {
|
|
322
|
+
'X-API-Key': config.apiKey,
|
|
323
|
+
'Content-Type': 'application/json',
|
|
324
|
+
};
|
|
325
|
+
const freshness = await checkConventionFreshness(apiUrl, config.projectId, headers, projectRoot);
|
|
326
|
+
return {
|
|
327
|
+
updateAvailable: freshness.platformGuidesChanged || freshness.conventionChanges.length > 0,
|
|
328
|
+
platformGuidesChanged: freshness.platformGuidesChanged,
|
|
329
|
+
conventionChanges: freshness.conventionChanges,
|
|
330
|
+
summary: buildStatusSummary(freshness),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
282
333
|
export async function conventionList() {
|
|
283
334
|
const { config, apiUrl, headers } = getApiConfigOrThrow();
|
|
284
335
|
const conventions = await fetchAllConventions(apiUrl, config.projectId, headers);
|
|
@@ -299,33 +350,31 @@ export async function conventionList() {
|
|
|
299
350
|
page: 1,
|
|
300
351
|
pageSize: conventions.length,
|
|
301
352
|
totalPages: 1,
|
|
302
|
-
}
|
|
353
|
+
},
|
|
303
354
|
};
|
|
304
355
|
}
|
|
305
356
|
function toSafeFileName(input) {
|
|
306
357
|
return input
|
|
307
358
|
.toLowerCase()
|
|
308
|
-
.replace(/[^a-z0-9]+/g,
|
|
309
|
-
.replace(/^-+|-+$/g,
|
|
359
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
360
|
+
.replace(/^-+|-+$/g, '')
|
|
310
361
|
.slice(0, 60);
|
|
311
362
|
}
|
|
312
363
|
function toSafeDirectoryName(input) {
|
|
313
364
|
const normalized = toSafeFileName(input);
|
|
314
|
-
return normalized.length > 0 ? normalized :
|
|
365
|
+
return normalized.length > 0 ? normalized : 'uncategorized';
|
|
315
366
|
}
|
|
316
367
|
function buildConventionFileName(convention) {
|
|
317
368
|
if (convention.fileName && convention.fileName.trim().length > 0) {
|
|
318
369
|
return convention.fileName.trim();
|
|
319
370
|
}
|
|
320
|
-
const titleSegment = convention.title ? toSafeFileName(convention.title) :
|
|
321
|
-
const prefix = titleSegment.length > 0 ? titleSegment :
|
|
371
|
+
const titleSegment = convention.title ? toSafeFileName(convention.title) : '';
|
|
372
|
+
const prefix = titleSegment.length > 0 ? titleSegment : 'convention';
|
|
322
373
|
return `${prefix}.md`;
|
|
323
374
|
}
|
|
324
375
|
function normalizeMarkdownFileName(input) {
|
|
325
376
|
const trimmed = input.trim();
|
|
326
|
-
const base = trimmed.toLowerCase().endsWith('.md')
|
|
327
|
-
? trimmed.slice(0, -3)
|
|
328
|
-
: trimmed;
|
|
377
|
+
const base = trimmed.toLowerCase().endsWith('.md') ? trimmed.slice(0, -3) : trimmed;
|
|
329
378
|
const safeBase = toSafeFileName(base);
|
|
330
379
|
const resolvedBase = safeBase.length > 0 ? safeBase : 'guide';
|
|
331
380
|
return `${resolvedBase}.md`;
|
|
@@ -358,9 +407,7 @@ async function downloadPlatformGuides(projectRoot, apiUrl, headers) {
|
|
|
358
407
|
const baseFileName = buildPlatformGuideFileName(guide);
|
|
359
408
|
const seenCount = fileNameCount.get(baseFileName) ?? 0;
|
|
360
409
|
fileNameCount.set(baseFileName, seenCount + 1);
|
|
361
|
-
const fileName = seenCount === 0
|
|
362
|
-
? baseFileName
|
|
363
|
-
: baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
|
|
410
|
+
const fileName = seenCount === 0 ? baseFileName : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
|
|
364
411
|
const filePath = join(baseDir, fileName);
|
|
365
412
|
atomicWriteFileSync(filePath, guide.content, 'utf-8');
|
|
366
413
|
written += 1;
|
|
@@ -375,22 +422,24 @@ async function downloadPlatformGuides(projectRoot, apiUrl, headers) {
|
|
|
375
422
|
}
|
|
376
423
|
}
|
|
377
424
|
async function downloadReportingTemplate(projectRoot, config, apiUrl, headers) {
|
|
378
|
-
const agentConfigResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs`, {
|
|
425
|
+
const agentConfigResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs`, {
|
|
426
|
+
headers,
|
|
427
|
+
});
|
|
379
428
|
const agentConfigs = agentConfigResponse.data?.data;
|
|
380
429
|
if (!Array.isArray(agentConfigs) || agentConfigs.length === 0) {
|
|
381
430
|
return false;
|
|
382
431
|
}
|
|
383
432
|
const firstAgentConfig = agentConfigs[0];
|
|
384
|
-
if (!firstAgentConfig?.id || typeof firstAgentConfig.id !==
|
|
433
|
+
if (!firstAgentConfig?.id || typeof firstAgentConfig.id !== 'string') {
|
|
385
434
|
return false;
|
|
386
435
|
}
|
|
387
436
|
const templateResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs/${firstAgentConfig.id}/convention`, { headers });
|
|
388
437
|
const content = templateResponse.data?.data?.content;
|
|
389
|
-
if (typeof content !==
|
|
438
|
+
if (typeof content !== 'string') {
|
|
390
439
|
return false;
|
|
391
440
|
}
|
|
392
441
|
const conventionPath = join(projectRoot, CONVENTION_DIR, CONVENTION_INDEX_FILE);
|
|
393
|
-
atomicWriteFileSync(conventionPath, content,
|
|
442
|
+
atomicWriteFileSync(conventionPath, content, 'utf-8');
|
|
394
443
|
return true;
|
|
395
444
|
}
|
|
396
445
|
export async function conventionDownload(options) {
|
|
@@ -414,7 +463,7 @@ export async function conventionDownload(options) {
|
|
|
414
463
|
rmSync(legacyDir, { recursive: true, force: true });
|
|
415
464
|
const categoryDirs = new Set();
|
|
416
465
|
for (const convention of conventionList) {
|
|
417
|
-
const categoryName = typeof convention.category ===
|
|
466
|
+
const categoryName = typeof convention.category === 'string' ? convention.category : '';
|
|
418
467
|
categoryDirs.add(toSafeDirectoryName(categoryName));
|
|
419
468
|
}
|
|
420
469
|
for (const categoryDir of categoryDirs) {
|
|
@@ -430,18 +479,16 @@ export async function conventionDownload(options) {
|
|
|
430
479
|
entries: [],
|
|
431
480
|
};
|
|
432
481
|
for (const convention of conventionList) {
|
|
433
|
-
const contentMarkdown = typeof convention.contentMarkdown ===
|
|
482
|
+
const contentMarkdown = typeof convention.contentMarkdown === 'string' ? convention.contentMarkdown : '';
|
|
434
483
|
const baseFileName = buildConventionFileName(convention);
|
|
435
|
-
const categoryName = typeof convention.category ===
|
|
484
|
+
const categoryName = typeof convention.category === 'string' ? convention.category : '';
|
|
436
485
|
const categoryDir = toSafeDirectoryName(categoryName);
|
|
437
486
|
const duplicateKey = `${categoryDir}/${baseFileName}`;
|
|
438
487
|
const seenCount = fileNameCount.get(duplicateKey) ?? 0;
|
|
439
488
|
fileNameCount.set(duplicateKey, seenCount + 1);
|
|
440
|
-
const fileName = seenCount === 0
|
|
441
|
-
? baseFileName
|
|
442
|
-
: baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
|
|
489
|
+
const fileName = seenCount === 0 ? baseFileName : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
|
|
443
490
|
const filePath = join(projectRoot, CONVENTION_DIR, categoryDir, fileName);
|
|
444
|
-
atomicWriteFileSync(filePath, contentMarkdown,
|
|
491
|
+
atomicWriteFileSync(filePath, contentMarkdown, 'utf-8');
|
|
445
492
|
manifest.entries.push({
|
|
446
493
|
conventionId: String(convention.id),
|
|
447
494
|
fileRelativePath: normalizeRelativePath(relative(projectRoot, filePath)),
|
|
@@ -464,14 +511,12 @@ export async function conventionDownload(options) {
|
|
|
464
511
|
: '';
|
|
465
512
|
return `Convention sync completed.\nUpdated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\nNo project conventions found.${platformLine}`;
|
|
466
513
|
}
|
|
467
|
-
throw new Error(
|
|
514
|
+
throw new Error('No conventions found for this project. Create one via the web dashboard first.');
|
|
468
515
|
}
|
|
469
|
-
const reportingLine = hasReportingTemplate
|
|
470
|
-
? `Updated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\n`
|
|
471
|
-
: "";
|
|
516
|
+
const reportingLine = hasReportingTemplate ? `Updated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\n` : '';
|
|
472
517
|
const platformLine = platformGuideCount > 0
|
|
473
518
|
? `Downloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform\n`
|
|
474
|
-
:
|
|
519
|
+
: '';
|
|
475
520
|
return `Convention sync completed.\n${reportingLine}${platformLine}Downloaded ${conventions.length} file(s) into category directories under ${CONVENTION_DIR}`;
|
|
476
521
|
}
|
|
477
522
|
function parseConventionMarkdown(fileRelativePath, markdown) {
|
|
@@ -501,7 +546,7 @@ export async function conventionCreate(options) {
|
|
|
501
546
|
const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
|
|
502
547
|
const category = parseCategoryFromAgentteamsPath(fileRelativePath);
|
|
503
548
|
const fileName = basename(absolutePath);
|
|
504
|
-
if (!fileName.toLowerCase().endsWith(
|
|
549
|
+
if (!fileName.toLowerCase().endsWith('.md')) {
|
|
505
550
|
throw new Error(`Convention create requires a .md file: ${fileRelativePath}`);
|
|
506
551
|
}
|
|
507
552
|
const existingEntry = manifest.entries.find((e) => e.fileRelativePath === fileRelativePath);
|
|
@@ -509,10 +554,10 @@ export async function conventionCreate(options) {
|
|
|
509
554
|
throw new Error(`File is already tracked in the manifest (use update instead): ${fileRelativePath}\n` +
|
|
510
555
|
`- conventionId: ${existingEntry.conventionId}`);
|
|
511
556
|
}
|
|
512
|
-
const localMarkdown = readFileSync(absolutePath,
|
|
557
|
+
const localMarkdown = readFileSync(absolutePath, 'utf-8');
|
|
513
558
|
const parsed = parseConventionMarkdown(fileRelativePath, localMarkdown);
|
|
514
559
|
const frontmatter = (parsed.data ?? {});
|
|
515
|
-
const bodyMarkdown = String(parsed.content ??
|
|
560
|
+
const bodyMarkdown = String(parsed.content ?? '');
|
|
516
561
|
const content = bodyMarkdown;
|
|
517
562
|
const title = toOptionalString(frontmatter.title)?.trim() || fileNameToTitle(fileName);
|
|
518
563
|
const payload = {
|
|
@@ -528,14 +573,14 @@ export async function conventionCreate(options) {
|
|
|
528
573
|
payload.trigger = trigger;
|
|
529
574
|
if (description)
|
|
530
575
|
payload.description = description;
|
|
531
|
-
if (typeof agentInstruction ===
|
|
576
|
+
if (typeof agentInstruction === 'string' && agentInstruction.trim().length > 0) {
|
|
532
577
|
payload.agentInstruction = agentInstruction.trimEnd();
|
|
533
578
|
}
|
|
534
579
|
const response = await withSpinner(`Creating convention for ${fileRelativePath}...`, () => httpClient.post(`${apiUrl}/api/projects/${config.projectId}/conventions`, payload, { headers }));
|
|
535
580
|
const created = response.data?.data;
|
|
536
|
-
const createdId = typeof created?.id ===
|
|
537
|
-
const createdUpdatedAt = typeof created?.updatedAt ===
|
|
538
|
-
const createdWebUrl = typeof created?.webUrl ===
|
|
581
|
+
const createdId = typeof created?.id === 'string' ? created.id : 'unknown';
|
|
582
|
+
const createdUpdatedAt = typeof created?.updatedAt === 'string' ? created.updatedAt : undefined;
|
|
583
|
+
const createdWebUrl = typeof created?.webUrl === 'string' ? created.webUrl : undefined;
|
|
539
584
|
const now = new Date().toISOString();
|
|
540
585
|
manifest.generatedAt = now;
|
|
541
586
|
manifest.entries.push({
|
|
@@ -558,7 +603,7 @@ export async function conventionCreate(options) {
|
|
|
558
603
|
results.push(`[OK] ${CONVENTION_DIR}/${CONVENTION_MANIFEST_FILE}: Updated.`);
|
|
559
604
|
results.push(`[NEXT] Run 'agentteams convention download' to refresh convention.md and canonical markdown.`);
|
|
560
605
|
}
|
|
561
|
-
return results.join(
|
|
606
|
+
return results.join('\n');
|
|
562
607
|
}
|
|
563
608
|
export async function conventionUpdate(options) {
|
|
564
609
|
const { config, apiUrl, headers } = getApiConfigOrThrow(options);
|
|
@@ -583,13 +628,13 @@ export async function conventionUpdate(options) {
|
|
|
583
628
|
throw new Error(`Only downloaded convention files can be updated: ${fileInput}\n` +
|
|
584
629
|
`- resolved: ${fileRelativePath}\n` +
|
|
585
630
|
`Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
|
|
586
|
-
(available.length > 0 ? `Examples (partial):\n- ${available.join(
|
|
631
|
+
(available.length > 0 ? `Examples (partial):\n- ${available.join('\n- ')}` : ''));
|
|
587
632
|
}
|
|
588
633
|
const conventionId = manifestEntry.conventionId;
|
|
589
634
|
const [serverDetail, serverMarkdown, localMarkdown] = await withSpinner(`Preparing update for ${fileRelativePath}...`, async () => {
|
|
590
635
|
const detailResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, { headers });
|
|
591
|
-
const downloadResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}/download`, { headers, responseType:
|
|
592
|
-
const local = readFileSync(absolutePath,
|
|
636
|
+
const downloadResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}/download`, { headers, responseType: 'text' });
|
|
637
|
+
const local = readFileSync(absolutePath, 'utf-8');
|
|
593
638
|
return [detailResponse.data?.data, String(downloadResponse.data), local];
|
|
594
639
|
});
|
|
595
640
|
if (!hasAnyDiff(serverMarkdown, localMarkdown)) {
|
|
@@ -604,18 +649,18 @@ export async function conventionUpdate(options) {
|
|
|
604
649
|
}
|
|
605
650
|
const parsed = parseConventionMarkdown(fileRelativePath, localMarkdown);
|
|
606
651
|
const frontmatter = (parsed.data ?? {});
|
|
607
|
-
const bodyMarkdown = String(parsed.content ??
|
|
652
|
+
const bodyMarkdown = String(parsed.content ?? '');
|
|
608
653
|
const content = bodyMarkdown;
|
|
609
|
-
if (typeof serverDetail?.updatedAt !==
|
|
654
|
+
if (typeof serverDetail?.updatedAt !== 'string' || serverDetail.updatedAt.length === 0) {
|
|
610
655
|
throw new Error(`[ERROR] ${fileRelativePath}: Server response is missing updatedAt.`);
|
|
611
656
|
}
|
|
612
657
|
const payload = {
|
|
613
658
|
updatedAt: serverDetail.updatedAt,
|
|
614
659
|
content,
|
|
615
660
|
};
|
|
616
|
-
const trigger = toOptionalStringOrNullIfPresent(frontmatter,
|
|
617
|
-
const description = toOptionalStringOrNullIfPresent(frontmatter,
|
|
618
|
-
const agentInstruction = toOptionalStringOrNullIfPresent(frontmatter,
|
|
661
|
+
const trigger = toOptionalStringOrNullIfPresent(frontmatter, 'trigger');
|
|
662
|
+
const description = toOptionalStringOrNullIfPresent(frontmatter, 'description');
|
|
663
|
+
const agentInstruction = toOptionalStringOrNullIfPresent(frontmatter, 'agentInstruction');
|
|
619
664
|
if (trigger !== undefined)
|
|
620
665
|
payload.trigger = trigger;
|
|
621
666
|
if (description !== undefined)
|
|
@@ -624,12 +669,10 @@ export async function conventionUpdate(options) {
|
|
|
624
669
|
payload.agentInstruction = agentInstruction;
|
|
625
670
|
const updatedResponse = await withSpinner(`Uploading ${fileRelativePath}...`, () => httpClient.put(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, payload, { headers }));
|
|
626
671
|
const newUpdatedAt = updatedResponse.data?.data?.updatedAt;
|
|
627
|
-
const newWebUrl = typeof updatedResponse.data?.data?.webUrl ===
|
|
628
|
-
? updatedResponse.data.data.webUrl
|
|
629
|
-
: undefined;
|
|
672
|
+
const newWebUrl = typeof updatedResponse.data?.data?.webUrl === 'string' ? updatedResponse.data.data.webUrl : undefined;
|
|
630
673
|
const now = new Date().toISOString();
|
|
631
674
|
manifestEntry.lastUploadedAt = now;
|
|
632
|
-
if (typeof newUpdatedAt ===
|
|
675
|
+
if (typeof newUpdatedAt === 'string') {
|
|
633
676
|
manifestEntry.lastKnownUpdatedAt = newUpdatedAt;
|
|
634
677
|
}
|
|
635
678
|
writeManifest(projectRoot, manifest);
|
|
@@ -638,7 +681,7 @@ export async function conventionUpdate(options) {
|
|
|
638
681
|
results.push(`webUrl: ${newWebUrl}`);
|
|
639
682
|
}
|
|
640
683
|
}
|
|
641
|
-
return results.join(
|
|
684
|
+
return results.join('\n\n');
|
|
642
685
|
}
|
|
643
686
|
export async function conventionDelete(options) {
|
|
644
687
|
const { config, apiUrl, headers } = getApiConfigOrThrow(options);
|
|
@@ -663,7 +706,7 @@ export async function conventionDelete(options) {
|
|
|
663
706
|
throw new Error(`Only downloaded convention files can be deleted: ${fileInput}\n` +
|
|
664
707
|
`- resolved: ${fileRelativePath}\n` +
|
|
665
708
|
`Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
|
|
666
|
-
(available.length > 0 ? `Examples (partial):\n- ${available.join(
|
|
709
|
+
(available.length > 0 ? `Examples (partial):\n- ${available.join('\n- ')}` : ''));
|
|
667
710
|
}
|
|
668
711
|
const entry = manifest.entries[entryIndex];
|
|
669
712
|
const conventionId = entry.conventionId;
|
|
@@ -672,7 +715,9 @@ export async function conventionDelete(options) {
|
|
|
672
715
|
results.push(`[DRY-RUN] ${fileRelativePath}: Planned only (no server delete).`);
|
|
673
716
|
continue;
|
|
674
717
|
}
|
|
675
|
-
await withSpinner(`Deleting convention for ${fileRelativePath}...`, () => httpClient.delete(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, {
|
|
718
|
+
await withSpinner(`Deleting convention for ${fileRelativePath}...`, () => httpClient.delete(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, {
|
|
719
|
+
headers: withoutJsonContentType(headers),
|
|
720
|
+
}));
|
|
676
721
|
// After a successful server delete, also clean up local files/manifest.
|
|
677
722
|
try {
|
|
678
723
|
unlinkSync(absolutePath);
|
|
@@ -684,6 +729,6 @@ export async function conventionDelete(options) {
|
|
|
684
729
|
writeManifest(projectRoot, manifest);
|
|
685
730
|
results.push(`[OK] ${fileRelativePath}: Deleted. (conventionId=${conventionId})`);
|
|
686
731
|
}
|
|
687
|
-
return results.join(
|
|
732
|
+
return results.join('\n');
|
|
688
733
|
}
|
|
689
734
|
//# sourceMappingURL=convention.js.map
|