@agentteams/cli 0.1.56-dev.218 → 0.1.56-dev.221
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 +2 -2
- package/dist/commands/convention.d.ts.map +1 -1
- package/dist/commands/convention.js +108 -113
- package/dist/commands/convention.js.map +1 -1
- package/dist/commands/conventionRouter.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/commands/plan.js +46 -36
- 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 +5 -3
- 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 +5 -4
- 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 +7 -1
- package/dist/utils/parsers.d.ts.map +1 -1
- package/dist/utils/parsers.js +18 -11
- 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/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 +75 -79
- package/package.json +4 -1
|
@@ -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
|
});
|
|
@@ -282,7 +283,7 @@ export async function checkConventionFreshness(apiUrl, projectId, headers, proje
|
|
|
282
283
|
export function buildStatusSummary(result) {
|
|
283
284
|
const parts = [];
|
|
284
285
|
if (result.platformGuidesChanged)
|
|
285
|
-
parts.push(
|
|
286
|
+
parts.push('platform guides');
|
|
286
287
|
const counts = { new: 0, updated: 0, deleted: 0 };
|
|
287
288
|
for (const change of result.conventionChanges)
|
|
288
289
|
counts[change.type] += 1;
|
|
@@ -293,8 +294,8 @@ export function buildStatusSummary(result) {
|
|
|
293
294
|
if (counts.deleted > 0)
|
|
294
295
|
parts.push(`${counts.deleted} deleted`);
|
|
295
296
|
if (parts.length === 0)
|
|
296
|
-
return
|
|
297
|
-
return `⚠ Updates available (${parts.join(
|
|
297
|
+
return '✓ Conventions up to date';
|
|
298
|
+
return `⚠ Updates available (${parts.join(', ')}). Run 'agentteams convention download' to sync.`;
|
|
298
299
|
}
|
|
299
300
|
/**
|
|
300
301
|
* Read-only freshness check exposed as `agentteams convention status`.
|
|
@@ -313,13 +314,13 @@ export async function conventionStatus(options) {
|
|
|
313
314
|
updateAvailable: false,
|
|
314
315
|
platformGuidesChanged: false,
|
|
315
316
|
conventionChanges: [],
|
|
316
|
-
summary:
|
|
317
|
+
summary: '✓ Conventions up to date',
|
|
317
318
|
};
|
|
318
319
|
}
|
|
319
320
|
const apiUrl = getApiBaseUrl(config.apiUrl);
|
|
320
321
|
const headers = {
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
'X-API-Key': config.apiKey,
|
|
323
|
+
'Content-Type': 'application/json',
|
|
323
324
|
};
|
|
324
325
|
const freshness = await checkConventionFreshness(apiUrl, config.projectId, headers, projectRoot);
|
|
325
326
|
return {
|
|
@@ -349,33 +350,31 @@ export async function conventionList() {
|
|
|
349
350
|
page: 1,
|
|
350
351
|
pageSize: conventions.length,
|
|
351
352
|
totalPages: 1,
|
|
352
|
-
}
|
|
353
|
+
},
|
|
353
354
|
};
|
|
354
355
|
}
|
|
355
356
|
function toSafeFileName(input) {
|
|
356
357
|
return input
|
|
357
358
|
.toLowerCase()
|
|
358
|
-
.replace(/[^a-z0-9]+/g,
|
|
359
|
-
.replace(/^-+|-+$/g,
|
|
359
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
360
|
+
.replace(/^-+|-+$/g, '')
|
|
360
361
|
.slice(0, 60);
|
|
361
362
|
}
|
|
362
363
|
function toSafeDirectoryName(input) {
|
|
363
364
|
const normalized = toSafeFileName(input);
|
|
364
|
-
return normalized.length > 0 ? normalized :
|
|
365
|
+
return normalized.length > 0 ? normalized : 'uncategorized';
|
|
365
366
|
}
|
|
366
367
|
function buildConventionFileName(convention) {
|
|
367
368
|
if (convention.fileName && convention.fileName.trim().length > 0) {
|
|
368
369
|
return convention.fileName.trim();
|
|
369
370
|
}
|
|
370
|
-
const titleSegment = convention.title ? toSafeFileName(convention.title) :
|
|
371
|
-
const prefix = titleSegment.length > 0 ? titleSegment :
|
|
371
|
+
const titleSegment = convention.title ? toSafeFileName(convention.title) : '';
|
|
372
|
+
const prefix = titleSegment.length > 0 ? titleSegment : 'convention';
|
|
372
373
|
return `${prefix}.md`;
|
|
373
374
|
}
|
|
374
375
|
function normalizeMarkdownFileName(input) {
|
|
375
376
|
const trimmed = input.trim();
|
|
376
|
-
const base = trimmed.toLowerCase().endsWith('.md')
|
|
377
|
-
? trimmed.slice(0, -3)
|
|
378
|
-
: trimmed;
|
|
377
|
+
const base = trimmed.toLowerCase().endsWith('.md') ? trimmed.slice(0, -3) : trimmed;
|
|
379
378
|
const safeBase = toSafeFileName(base);
|
|
380
379
|
const resolvedBase = safeBase.length > 0 ? safeBase : 'guide';
|
|
381
380
|
return `${resolvedBase}.md`;
|
|
@@ -408,9 +407,7 @@ async function downloadPlatformGuides(projectRoot, apiUrl, headers) {
|
|
|
408
407
|
const baseFileName = buildPlatformGuideFileName(guide);
|
|
409
408
|
const seenCount = fileNameCount.get(baseFileName) ?? 0;
|
|
410
409
|
fileNameCount.set(baseFileName, seenCount + 1);
|
|
411
|
-
const fileName = seenCount === 0
|
|
412
|
-
? baseFileName
|
|
413
|
-
: baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
|
|
410
|
+
const fileName = seenCount === 0 ? baseFileName : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
|
|
414
411
|
const filePath = join(baseDir, fileName);
|
|
415
412
|
atomicWriteFileSync(filePath, guide.content, 'utf-8');
|
|
416
413
|
written += 1;
|
|
@@ -425,22 +422,24 @@ async function downloadPlatformGuides(projectRoot, apiUrl, headers) {
|
|
|
425
422
|
}
|
|
426
423
|
}
|
|
427
424
|
async function downloadReportingTemplate(projectRoot, config, apiUrl, headers) {
|
|
428
|
-
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
|
+
});
|
|
429
428
|
const agentConfigs = agentConfigResponse.data?.data;
|
|
430
429
|
if (!Array.isArray(agentConfigs) || agentConfigs.length === 0) {
|
|
431
430
|
return false;
|
|
432
431
|
}
|
|
433
432
|
const firstAgentConfig = agentConfigs[0];
|
|
434
|
-
if (!firstAgentConfig?.id || typeof firstAgentConfig.id !==
|
|
433
|
+
if (!firstAgentConfig?.id || typeof firstAgentConfig.id !== 'string') {
|
|
435
434
|
return false;
|
|
436
435
|
}
|
|
437
436
|
const templateResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/agent-configs/${firstAgentConfig.id}/convention`, { headers });
|
|
438
437
|
const content = templateResponse.data?.data?.content;
|
|
439
|
-
if (typeof content !==
|
|
438
|
+
if (typeof content !== 'string') {
|
|
440
439
|
return false;
|
|
441
440
|
}
|
|
442
441
|
const conventionPath = join(projectRoot, CONVENTION_DIR, CONVENTION_INDEX_FILE);
|
|
443
|
-
atomicWriteFileSync(conventionPath, content,
|
|
442
|
+
atomicWriteFileSync(conventionPath, content, 'utf-8');
|
|
444
443
|
return true;
|
|
445
444
|
}
|
|
446
445
|
export async function conventionDownload(options) {
|
|
@@ -464,7 +463,7 @@ export async function conventionDownload(options) {
|
|
|
464
463
|
rmSync(legacyDir, { recursive: true, force: true });
|
|
465
464
|
const categoryDirs = new Set();
|
|
466
465
|
for (const convention of conventionList) {
|
|
467
|
-
const categoryName = typeof convention.category ===
|
|
466
|
+
const categoryName = typeof convention.category === 'string' ? convention.category : '';
|
|
468
467
|
categoryDirs.add(toSafeDirectoryName(categoryName));
|
|
469
468
|
}
|
|
470
469
|
for (const categoryDir of categoryDirs) {
|
|
@@ -480,18 +479,16 @@ export async function conventionDownload(options) {
|
|
|
480
479
|
entries: [],
|
|
481
480
|
};
|
|
482
481
|
for (const convention of conventionList) {
|
|
483
|
-
const contentMarkdown = typeof convention.contentMarkdown ===
|
|
482
|
+
const contentMarkdown = typeof convention.contentMarkdown === 'string' ? convention.contentMarkdown : '';
|
|
484
483
|
const baseFileName = buildConventionFileName(convention);
|
|
485
|
-
const categoryName = typeof convention.category ===
|
|
484
|
+
const categoryName = typeof convention.category === 'string' ? convention.category : '';
|
|
486
485
|
const categoryDir = toSafeDirectoryName(categoryName);
|
|
487
486
|
const duplicateKey = `${categoryDir}/${baseFileName}`;
|
|
488
487
|
const seenCount = fileNameCount.get(duplicateKey) ?? 0;
|
|
489
488
|
fileNameCount.set(duplicateKey, seenCount + 1);
|
|
490
|
-
const fileName = seenCount === 0
|
|
491
|
-
? baseFileName
|
|
492
|
-
: baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
|
|
489
|
+
const fileName = seenCount === 0 ? baseFileName : baseFileName.replace(/\.md$/, `-${seenCount + 1}.md`);
|
|
493
490
|
const filePath = join(projectRoot, CONVENTION_DIR, categoryDir, fileName);
|
|
494
|
-
atomicWriteFileSync(filePath, contentMarkdown,
|
|
491
|
+
atomicWriteFileSync(filePath, contentMarkdown, 'utf-8');
|
|
495
492
|
manifest.entries.push({
|
|
496
493
|
conventionId: String(convention.id),
|
|
497
494
|
fileRelativePath: normalizeRelativePath(relative(projectRoot, filePath)),
|
|
@@ -514,14 +511,12 @@ export async function conventionDownload(options) {
|
|
|
514
511
|
: '';
|
|
515
512
|
return `Convention sync completed.\nUpdated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\nNo project conventions found.${platformLine}`;
|
|
516
513
|
}
|
|
517
|
-
throw new Error(
|
|
514
|
+
throw new Error('No conventions found for this project. Create one via the web dashboard first.');
|
|
518
515
|
}
|
|
519
|
-
const reportingLine = hasReportingTemplate
|
|
520
|
-
? `Updated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\n`
|
|
521
|
-
: "";
|
|
516
|
+
const reportingLine = hasReportingTemplate ? `Updated ${CONVENTION_DIR}/${CONVENTION_INDEX_FILE}\n` : '';
|
|
522
517
|
const platformLine = platformGuideCount > 0
|
|
523
518
|
? `Downloaded ${platformGuideCount} platform guide file(s) into ${CONVENTION_DIR}/platform\n`
|
|
524
|
-
:
|
|
519
|
+
: '';
|
|
525
520
|
return `Convention sync completed.\n${reportingLine}${platformLine}Downloaded ${conventions.length} file(s) into category directories under ${CONVENTION_DIR}`;
|
|
526
521
|
}
|
|
527
522
|
function parseConventionMarkdown(fileRelativePath, markdown) {
|
|
@@ -551,7 +546,7 @@ export async function conventionCreate(options) {
|
|
|
551
546
|
const fileRelativePath = normalizeRelativePath(relative(projectRoot, absolutePath));
|
|
552
547
|
const category = parseCategoryFromAgentteamsPath(fileRelativePath);
|
|
553
548
|
const fileName = basename(absolutePath);
|
|
554
|
-
if (!fileName.toLowerCase().endsWith(
|
|
549
|
+
if (!fileName.toLowerCase().endsWith('.md')) {
|
|
555
550
|
throw new Error(`Convention create requires a .md file: ${fileRelativePath}`);
|
|
556
551
|
}
|
|
557
552
|
const existingEntry = manifest.entries.find((e) => e.fileRelativePath === fileRelativePath);
|
|
@@ -559,10 +554,10 @@ export async function conventionCreate(options) {
|
|
|
559
554
|
throw new Error(`File is already tracked in the manifest (use update instead): ${fileRelativePath}\n` +
|
|
560
555
|
`- conventionId: ${existingEntry.conventionId}`);
|
|
561
556
|
}
|
|
562
|
-
const localMarkdown = readFileSync(absolutePath,
|
|
557
|
+
const localMarkdown = readFileSync(absolutePath, 'utf-8');
|
|
563
558
|
const parsed = parseConventionMarkdown(fileRelativePath, localMarkdown);
|
|
564
559
|
const frontmatter = (parsed.data ?? {});
|
|
565
|
-
const bodyMarkdown = String(parsed.content ??
|
|
560
|
+
const bodyMarkdown = String(parsed.content ?? '');
|
|
566
561
|
const content = bodyMarkdown;
|
|
567
562
|
const title = toOptionalString(frontmatter.title)?.trim() || fileNameToTitle(fileName);
|
|
568
563
|
const payload = {
|
|
@@ -578,14 +573,14 @@ export async function conventionCreate(options) {
|
|
|
578
573
|
payload.trigger = trigger;
|
|
579
574
|
if (description)
|
|
580
575
|
payload.description = description;
|
|
581
|
-
if (typeof agentInstruction ===
|
|
576
|
+
if (typeof agentInstruction === 'string' && agentInstruction.trim().length > 0) {
|
|
582
577
|
payload.agentInstruction = agentInstruction.trimEnd();
|
|
583
578
|
}
|
|
584
579
|
const response = await withSpinner(`Creating convention for ${fileRelativePath}...`, () => httpClient.post(`${apiUrl}/api/projects/${config.projectId}/conventions`, payload, { headers }));
|
|
585
580
|
const created = response.data?.data;
|
|
586
|
-
const createdId = typeof created?.id ===
|
|
587
|
-
const createdUpdatedAt = typeof created?.updatedAt ===
|
|
588
|
-
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;
|
|
589
584
|
const now = new Date().toISOString();
|
|
590
585
|
manifest.generatedAt = now;
|
|
591
586
|
manifest.entries.push({
|
|
@@ -608,7 +603,7 @@ export async function conventionCreate(options) {
|
|
|
608
603
|
results.push(`[OK] ${CONVENTION_DIR}/${CONVENTION_MANIFEST_FILE}: Updated.`);
|
|
609
604
|
results.push(`[NEXT] Run 'agentteams convention download' to refresh convention.md and canonical markdown.`);
|
|
610
605
|
}
|
|
611
|
-
return results.join(
|
|
606
|
+
return results.join('\n');
|
|
612
607
|
}
|
|
613
608
|
export async function conventionUpdate(options) {
|
|
614
609
|
const { config, apiUrl, headers } = getApiConfigOrThrow(options);
|
|
@@ -633,13 +628,13 @@ export async function conventionUpdate(options) {
|
|
|
633
628
|
throw new Error(`Only downloaded convention files can be updated: ${fileInput}\n` +
|
|
634
629
|
`- resolved: ${fileRelativePath}\n` +
|
|
635
630
|
`Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
|
|
636
|
-
(available.length > 0 ? `Examples (partial):\n- ${available.join(
|
|
631
|
+
(available.length > 0 ? `Examples (partial):\n- ${available.join('\n- ')}` : ''));
|
|
637
632
|
}
|
|
638
633
|
const conventionId = manifestEntry.conventionId;
|
|
639
634
|
const [serverDetail, serverMarkdown, localMarkdown] = await withSpinner(`Preparing update for ${fileRelativePath}...`, async () => {
|
|
640
635
|
const detailResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, { headers });
|
|
641
|
-
const downloadResponse = await httpClient.get(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}/download`, { headers, responseType:
|
|
642
|
-
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');
|
|
643
638
|
return [detailResponse.data?.data, String(downloadResponse.data), local];
|
|
644
639
|
});
|
|
645
640
|
if (!hasAnyDiff(serverMarkdown, localMarkdown)) {
|
|
@@ -654,18 +649,18 @@ export async function conventionUpdate(options) {
|
|
|
654
649
|
}
|
|
655
650
|
const parsed = parseConventionMarkdown(fileRelativePath, localMarkdown);
|
|
656
651
|
const frontmatter = (parsed.data ?? {});
|
|
657
|
-
const bodyMarkdown = String(parsed.content ??
|
|
652
|
+
const bodyMarkdown = String(parsed.content ?? '');
|
|
658
653
|
const content = bodyMarkdown;
|
|
659
|
-
if (typeof serverDetail?.updatedAt !==
|
|
654
|
+
if (typeof serverDetail?.updatedAt !== 'string' || serverDetail.updatedAt.length === 0) {
|
|
660
655
|
throw new Error(`[ERROR] ${fileRelativePath}: Server response is missing updatedAt.`);
|
|
661
656
|
}
|
|
662
657
|
const payload = {
|
|
663
658
|
updatedAt: serverDetail.updatedAt,
|
|
664
659
|
content,
|
|
665
660
|
};
|
|
666
|
-
const trigger = toOptionalStringOrNullIfPresent(frontmatter,
|
|
667
|
-
const description = toOptionalStringOrNullIfPresent(frontmatter,
|
|
668
|
-
const agentInstruction = toOptionalStringOrNullIfPresent(frontmatter,
|
|
661
|
+
const trigger = toOptionalStringOrNullIfPresent(frontmatter, 'trigger');
|
|
662
|
+
const description = toOptionalStringOrNullIfPresent(frontmatter, 'description');
|
|
663
|
+
const agentInstruction = toOptionalStringOrNullIfPresent(frontmatter, 'agentInstruction');
|
|
669
664
|
if (trigger !== undefined)
|
|
670
665
|
payload.trigger = trigger;
|
|
671
666
|
if (description !== undefined)
|
|
@@ -674,12 +669,10 @@ export async function conventionUpdate(options) {
|
|
|
674
669
|
payload.agentInstruction = agentInstruction;
|
|
675
670
|
const updatedResponse = await withSpinner(`Uploading ${fileRelativePath}...`, () => httpClient.put(`${apiUrl}/api/projects/${config.projectId}/conventions/${conventionId}`, payload, { headers }));
|
|
676
671
|
const newUpdatedAt = updatedResponse.data?.data?.updatedAt;
|
|
677
|
-
const newWebUrl = typeof updatedResponse.data?.data?.webUrl ===
|
|
678
|
-
? updatedResponse.data.data.webUrl
|
|
679
|
-
: undefined;
|
|
672
|
+
const newWebUrl = typeof updatedResponse.data?.data?.webUrl === 'string' ? updatedResponse.data.data.webUrl : undefined;
|
|
680
673
|
const now = new Date().toISOString();
|
|
681
674
|
manifestEntry.lastUploadedAt = now;
|
|
682
|
-
if (typeof newUpdatedAt ===
|
|
675
|
+
if (typeof newUpdatedAt === 'string') {
|
|
683
676
|
manifestEntry.lastKnownUpdatedAt = newUpdatedAt;
|
|
684
677
|
}
|
|
685
678
|
writeManifest(projectRoot, manifest);
|
|
@@ -688,7 +681,7 @@ export async function conventionUpdate(options) {
|
|
|
688
681
|
results.push(`webUrl: ${newWebUrl}`);
|
|
689
682
|
}
|
|
690
683
|
}
|
|
691
|
-
return results.join(
|
|
684
|
+
return results.join('\n\n');
|
|
692
685
|
}
|
|
693
686
|
export async function conventionDelete(options) {
|
|
694
687
|
const { config, apiUrl, headers } = getApiConfigOrThrow(options);
|
|
@@ -713,7 +706,7 @@ export async function conventionDelete(options) {
|
|
|
713
706
|
throw new Error(`Only downloaded convention files can be deleted: ${fileInput}\n` +
|
|
714
707
|
`- resolved: ${fileRelativePath}\n` +
|
|
715
708
|
`Run 'agentteams convention download' first, or pass a file path listed in the manifest.\n` +
|
|
716
|
-
(available.length > 0 ? `Examples (partial):\n- ${available.join(
|
|
709
|
+
(available.length > 0 ? `Examples (partial):\n- ${available.join('\n- ')}` : ''));
|
|
717
710
|
}
|
|
718
711
|
const entry = manifest.entries[entryIndex];
|
|
719
712
|
const conventionId = entry.conventionId;
|
|
@@ -722,7 +715,9 @@ export async function conventionDelete(options) {
|
|
|
722
715
|
results.push(`[DRY-RUN] ${fileRelativePath}: Planned only (no server delete).`);
|
|
723
716
|
continue;
|
|
724
717
|
}
|
|
725
|
-
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
|
+
}));
|
|
726
721
|
// After a successful server delete, also clean up local files/manifest.
|
|
727
722
|
try {
|
|
728
723
|
unlinkSync(absolutePath);
|
|
@@ -734,6 +729,6 @@ export async function conventionDelete(options) {
|
|
|
734
729
|
writeManifest(projectRoot, manifest);
|
|
735
730
|
results.push(`[OK] ${fileRelativePath}: Deleted. (conventionId=${conventionId})`);
|
|
736
731
|
}
|
|
737
|
-
return results.join(
|
|
732
|
+
return results.join('\n');
|
|
738
733
|
}
|
|
739
734
|
//# sourceMappingURL=convention.js.map
|