@dotdotgod/cli 0.2.14 → 0.2.15
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/README.md +0 -2
- package/package.json +1 -1
- package/src/cli/usage.mjs +0 -16
- package/src/commands/config.mjs +0 -1
- package/src/core.mjs +2 -18
- package/src/memory/config.mjs +2 -39
- package/src/validate/run.mjs +18 -14
- package/src/trello/client.mjs +0 -93
- package/src/trello/custom-fields.mjs +0 -102
- package/src/trello/github-url.mjs +0 -107
- package/src/trello/metadata.mjs +0 -52
- package/src/trello/report.mjs +0 -60
- package/src/trello/summary.mjs +0 -60
- package/src/trello/sync.mjs +0 -205
package/README.md
CHANGED
|
@@ -40,7 +40,6 @@ dotdotgod graph impact . --changed <path> --compact
|
|
|
40
40
|
- `resolve` and `expand` map explicit or high-signal prompt references to project files.
|
|
41
41
|
- `graph impact` ranks likely related specs, tests, docs, commands, and source files for one or more changed paths, with a combined ranking and per-file top five.
|
|
42
42
|
- `traceability links` checks or repairs generated Markdown traceability-link sections.
|
|
43
|
-
- `trello sync` plans or writes Trello linked-doc metadata from configured docs paths.
|
|
44
43
|
|
|
45
44
|
## Commands
|
|
46
45
|
|
|
@@ -77,7 +76,6 @@ dotdotgod graph communities .
|
|
|
77
76
|
| Resolve `[[...]]` references from a prompt | `dotdotgod expand . "Update [[PLAN_MODE]]"` |
|
|
78
77
|
| Repair generated traceability-link sections | `dotdotgod traceability links . --write` |
|
|
79
78
|
| See what else to inspect after a change | `dotdotgod graph impact . --changed <path> --compact` |
|
|
80
|
-
| Preview Trello linked-doc updates | `dotdotgod trello sync . --dry-run` |
|
|
81
79
|
|
|
82
80
|
## Changed-File Impact
|
|
83
81
|
|
package/package.json
CHANGED
package/src/cli/usage.mjs
CHANGED
|
@@ -42,20 +42,6 @@ Search shared project documentation with the local multilingual E5 vector index.
|
|
|
42
42
|
case 'expand':
|
|
43
43
|
return `Usage:
|
|
44
44
|
dotdotgod expand <root> <prompt> [--max-results n] [--include-archive] [--with-impact] [--fuzzy] [--json]`;
|
|
45
|
-
case 'trello':
|
|
46
|
-
return `Usage:
|
|
47
|
-
dotdotgod trello sync <root> [--dry-run]`;
|
|
48
|
-
case 'trello sync':
|
|
49
|
-
return `Usage:
|
|
50
|
-
dotdotgod trello sync <root> [--dry-run]
|
|
51
|
-
|
|
52
|
-
Options:
|
|
53
|
-
--dry-run Preview planned Trello updates without calling Trello APIs.
|
|
54
|
-
|
|
55
|
-
Write mode:
|
|
56
|
-
Omit --dry-run only in the trusted GitHub Actions default-branch push workflow.
|
|
57
|
-
Requires TRELLO_API_KEY and TRELLO_TOKEN in the trusted workflow environment.
|
|
58
|
-
Local/manual and pull request usage should run --dry-run.`;
|
|
59
45
|
case 'traceability':
|
|
60
46
|
return `Usage:
|
|
61
47
|
dotdotgod traceability links <root> [--check|--write] [--json]`;
|
|
@@ -95,7 +81,6 @@ Commands:
|
|
|
95
81
|
traceability links Check or write generated traceability links.
|
|
96
82
|
graph impact Find files related to changed files.
|
|
97
83
|
graph communities Find groups of related project-memory nodes.
|
|
98
|
-
trello sync Synchronize Trello cards and documentation.
|
|
99
84
|
|
|
100
85
|
Options:
|
|
101
86
|
-h, --help Show help.
|
|
@@ -144,7 +129,6 @@ export function printVersion() {
|
|
|
144
129
|
export function helpCommandFromArgs(args) {
|
|
145
130
|
const nonHelp = args.filter((arg) => !isHelpToken(arg));
|
|
146
131
|
if (nonHelp[0] === 'graph' && nonHelp[1]) return `graph ${nonHelp[1]}`;
|
|
147
|
-
if (nonHelp[0] === 'trello' && nonHelp[1]) return `trello ${nonHelp[1]}`;
|
|
148
132
|
if (nonHelp[0] === 'traceability' && nonHelp[1]) return `traceability ${nonHelp[1]}`;
|
|
149
133
|
if (nonHelp[0] === 'config' && nonHelp[1] === 'init') return 'config init';
|
|
150
134
|
return nonHelp[0] ?? 'root';
|
package/src/commands/config.mjs
CHANGED
|
@@ -43,7 +43,6 @@ function formatConfigOutput(payload) {
|
|
|
43
43
|
lines.push(`- load pinned bodies: ${(payload.config.load?.pinnedBodies ?? []).join(', ') || 'none'}`);
|
|
44
44
|
lines.push(`- load documentation summary exclude: ${(payload.config.load?.documentationSummary?.exclude ?? []).join(', ') || 'none'}`);
|
|
45
45
|
lines.push(`- plan mode writable paths: ${(payload.config.planMode?.writablePaths ?? []).join(', ') || 'none'}`);
|
|
46
|
-
lines.push(`- trello sync paths: ${(payload.config.integrations?.trello?.syncPaths ?? []).join(', ') || 'none'}`);
|
|
47
46
|
const lowSignal = payload.config.referenceExpansion?.fuzzy?.lowSignal ?? { terms: [], add: [], remove: [] };
|
|
48
47
|
lines.push(`- fuzzy low-signal terms: ${(lowSignal.terms ?? []).join(', ') || 'none'} (add: ${(lowSignal.add ?? []).join(', ') || 'none'}; remove: ${(lowSignal.remove ?? []).join(', ') || 'none'})`);
|
|
49
48
|
if (errors.length > 0) {
|
package/src/core.mjs
CHANGED
|
@@ -7,14 +7,13 @@ import { runQuery } from './commands/query.mjs';
|
|
|
7
7
|
import { runResolve, runExpand } from './reference/resolve.mjs';
|
|
8
8
|
import { runTraceability } from './commands/traceability.mjs';
|
|
9
9
|
import { runGraph } from './commands/graph.mjs';
|
|
10
|
-
import { runTrelloSync } from './trello/sync.mjs';
|
|
11
10
|
|
|
12
11
|
export { CACHE_VERSION } from './index/constants.mjs';
|
|
13
12
|
export { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersionToken, parseCommon, printVersion, usage } from './cli/usage.mjs';
|
|
14
13
|
export { rel } from './common/paths.mjs';
|
|
15
14
|
export { extractAnchors, extractFirstHeading, extractLinks, headingToAnchor, isKebabCase, isNumberedSeriesFilename, isUpperSnakeMarkdown, removeCodeBlocks, suggestFilenameFromHeading } from './docs/markdown.mjs';
|
|
16
15
|
export { extractDotdotgodTraceabilityBlocks, findTraceabilityLinksRegion, isLocalRelativeTraceabilityPath, renderCompactTraceabilityBlock, stripTraceabilityLinksRegion, syncTraceabilityLinksInContent, traceabilityExample, validateTraceabilityBlock, validateTraceabilityLinksRegion, validateTraceabilityPlacement } from './docs/traceability.mjs';
|
|
17
|
-
export { DEFAULT_IMPACT_RANKING_POLICY,
|
|
16
|
+
export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_LOAD_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneLoadPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, isSecretLikePathPattern, isValidPathPattern, matchPathPattern, memoryAreaForPath, memoryConfigSummary, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, validateMemoryConfigData } from './memory/config.mjs';
|
|
18
17
|
export { buildCommunities, relationWeight } from './graph/communities.mjs';
|
|
19
18
|
export { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
|
|
20
19
|
export { cacheFile, collectIndexFiles, fingerprint, shouldIndexPath } from './index/files.mjs';
|
|
@@ -34,13 +33,6 @@ export { extractBracketReferences, extractFuzzyReferences, normalizeReferenceAli
|
|
|
34
33
|
export { resolveReferenceCandidates, runResolve, runExpand } from './reference/resolve.mjs';
|
|
35
34
|
export { parseTraceabilityOptions, runTraceability } from './commands/traceability.mjs';
|
|
36
35
|
export { parseGraphOptions, runGraph } from './commands/graph.mjs';
|
|
37
|
-
export { planTrelloDryRun, runTrelloSync } from './trello/sync.mjs';
|
|
38
|
-
export { parseTrelloMetadata, extractTrelloShortLink } from './trello/metadata.mjs';
|
|
39
|
-
export { normalizeGitHubRemote, resolveGitHubFileUrl, resolveGitHubRepositoryIdentity } from './trello/github-url.mjs';
|
|
40
|
-
export { buildLinkedDocsData, summarizeTraceability } from './trello/summary.mjs';
|
|
41
|
-
export { formatTrelloDryRunReport, formatTrelloWriteReport } from './trello/report.mjs';
|
|
42
|
-
export { createTrelloClient, resolveTrelloCredentials } from './trello/client.mjs';
|
|
43
|
-
export { customFieldTextValue, DEFAULT_DOTDOTGOD_CUSTOM_FIELD_NAME, dotdotgodCustomFieldName, mergeLinkedDocsCustomFieldValue, parseLinkedDocsCustomFieldPayload, serializeLinkedDocsCustomFieldPayload } from './trello/custom-fields.mjs';
|
|
44
36
|
|
|
45
37
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
46
38
|
const [command = 'help', ...args] = argv;
|
|
@@ -58,13 +50,5 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
58
50
|
else if (command === 'expand') runExpand(args);
|
|
59
51
|
else if (command === 'traceability') runTraceability(args);
|
|
60
52
|
else if (command === 'graph') await runGraph(args);
|
|
61
|
-
else
|
|
62
|
-
const result = await Promise.resolve(runTrelloSync(args));
|
|
63
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
64
|
-
if (result.stderr) {
|
|
65
|
-
console.error(result.stderr);
|
|
66
|
-
console.error(commandUsage('trello'));
|
|
67
|
-
}
|
|
68
|
-
process.exit(result.exitCode);
|
|
69
|
-
} else usage(`Unknown command: ${command}`);
|
|
53
|
+
else usage(`Unknown command: ${command}`);
|
|
70
54
|
}
|
package/src/memory/config.mjs
CHANGED
|
@@ -21,9 +21,6 @@ export const DEFAULT_VALIDATION_POLICY = {
|
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
|
-
export const DEFAULT_INTEGRATIONS_POLICY = {
|
|
25
|
-
trello: { syncPaths: [] },
|
|
26
|
-
};
|
|
27
24
|
export const DEFAULT_LOAD_POLICY = {
|
|
28
25
|
pinnedPaths: [],
|
|
29
26
|
pinnedBodies: [],
|
|
@@ -177,10 +174,6 @@ function normalizeImpactRankingPolicy(raw) {
|
|
|
177
174
|
return cloneImpactRankingPolicy({ ...preset, ...raw, preset: presetName, weights: { ...(preset.weights ?? {}), ...(raw?.weights ?? {}) }, ppr: { ...(preset.ppr ?? {}), ...(raw?.ppr ?? {}) }, semantic: { ...(preset.semantic ?? {}), ...(raw?.semantic ?? {}) } });
|
|
178
175
|
}
|
|
179
176
|
|
|
180
|
-
export function cloneIntegrationsPolicy(policy = DEFAULT_INTEGRATIONS_POLICY) {
|
|
181
|
-
return { trello: { syncPaths: [...(policy.trello?.syncPaths ?? [])] } };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
177
|
export function cloneLoadPolicy(policy = DEFAULT_LOAD_POLICY) {
|
|
185
178
|
return {
|
|
186
179
|
pinnedPaths: [...(policy.pinnedPaths ?? [])],
|
|
@@ -196,7 +189,7 @@ export function clonePlanModePolicy(policy = DEFAULT_PLAN_MODE_POLICY) {
|
|
|
196
189
|
}
|
|
197
190
|
|
|
198
191
|
export function defaultMemoryConfig() {
|
|
199
|
-
return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(),
|
|
192
|
+
return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), load: cloneLoadPolicy(), planMode: clonePlanModePolicy() };
|
|
200
193
|
}
|
|
201
194
|
|
|
202
195
|
export function normalizePathPattern(value = '') {
|
|
@@ -264,12 +257,6 @@ function normalizeValidationPolicy(raw) {
|
|
|
264
257
|
});
|
|
265
258
|
}
|
|
266
259
|
|
|
267
|
-
function normalizeIntegrationsPolicy(raw) {
|
|
268
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneIntegrationsPolicy();
|
|
269
|
-
const syncPaths = Array.isArray(raw.trello?.syncPaths) ? raw.trello.syncPaths.map(normalizePathPattern) : [];
|
|
270
|
-
return cloneIntegrationsPolicy({ trello: { syncPaths: [...new Set(syncPaths)] } });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
260
|
export function isSecretLikePathPattern(value = '') {
|
|
274
261
|
const normalized = normalizePathPattern(value);
|
|
275
262
|
return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials?|secrets?)(\.|\/|$)/i.test(normalized);
|
|
@@ -297,10 +284,6 @@ function normalizeLoadPolicy(raw) {
|
|
|
297
284
|
});
|
|
298
285
|
}
|
|
299
286
|
|
|
300
|
-
export function trelloSyncPaths(config = defaultMemoryConfig()) {
|
|
301
|
-
return [...new Set(['docs/trello/**', ...((config.integrations ?? DEFAULT_INTEGRATIONS_POLICY).trello?.syncPaths ?? [])])];
|
|
302
|
-
}
|
|
303
|
-
|
|
304
287
|
export function isMarkdownSizeExcluded(path = '', config = defaultMemoryConfig()) {
|
|
305
288
|
const policy = config.validation ?? DEFAULT_VALIDATION_POLICY;
|
|
306
289
|
return (policy.markdown?.exclude ?? []).some((pattern) => matchPathPattern(path, pattern));
|
|
@@ -383,23 +366,6 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
383
366
|
}
|
|
384
367
|
}
|
|
385
368
|
}
|
|
386
|
-
const integrations = data.integrations;
|
|
387
|
-
if (integrations !== undefined) {
|
|
388
|
-
if (!integrations || typeof integrations !== 'object' || Array.isArray(integrations)) {
|
|
389
|
-
add('INTEGRATIONS_CONFIG_INVALID', 'integrations', 'Expected an object.');
|
|
390
|
-
} else if (integrations.trello !== undefined) {
|
|
391
|
-
const trello = integrations.trello;
|
|
392
|
-
if (!trello || typeof trello !== 'object' || Array.isArray(trello)) {
|
|
393
|
-
add('INTEGRATIONS_TRELLO_CONFIG_INVALID', 'integrations.trello', 'Expected an object.');
|
|
394
|
-
} else if (trello.syncPaths !== undefined) {
|
|
395
|
-
if (!Array.isArray(trello.syncPaths)) add('INTEGRATIONS_TRELLO_INVALID_SYNC_PATHS', 'integrations.trello.syncPaths', 'Expected an array of path strings.');
|
|
396
|
-
else {
|
|
397
|
-
if (trello.syncPaths.some((value) => !isValidPathPattern(value) || (typeof value === 'string' && value.trim().startsWith('/')))) add('INTEGRATIONS_TRELLO_INVALID_SYNC_PATHS', 'integrations.trello.syncPaths', 'Expected repository-relative path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
|
|
398
|
-
if (trello.syncPaths.some((value) => typeof value === 'string' && isSecretLikePathPattern(value))) add('INTEGRATIONS_TRELLO_SECRET_SYNC_PATH', 'integrations.trello.syncPaths', 'Sync paths must not target secrets, credentials, environment files, or private keys.');
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
369
|
const load = data.load;
|
|
404
370
|
if (load !== undefined) {
|
|
405
371
|
if (!load || typeof load !== 'object' || Array.isArray(load)) {
|
|
@@ -544,10 +510,9 @@ export function readMemoryConfig(root = '.') {
|
|
|
544
510
|
const validation = data.validation === undefined ? cloneValidationPolicy() : normalizeValidationPolicy(data.validation);
|
|
545
511
|
const impactRanking = normalizeImpactRankingPolicy(data.impactRanking);
|
|
546
512
|
const referenceExpansion = normalizeReferenceExpansionPolicy(data.referenceExpansion);
|
|
547
|
-
const integrations = normalizeIntegrationsPolicy(data.integrations);
|
|
548
513
|
const load = normalizeLoadPolicy(data.load);
|
|
549
514
|
const planMode = normalizePlanModePolicy(data.planMode);
|
|
550
|
-
return configuredAreas.length > 0 ? { source: MEMORY_CONFIG_FILE, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion,
|
|
515
|
+
return configuredAreas.length > 0 ? { source: MEMORY_CONFIG_FILE, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, load, planMode, errors: [] } : { ...defaultMemoryConfig(), traceability, validation, impactRanking, referenceExpansion, load, planMode, source: MEMORY_CONFIG_FILE, errors: [] };
|
|
551
516
|
} catch (error) {
|
|
552
517
|
return { ...defaultMemoryConfig(), source: MEMORY_CONFIG_FILE, errors: [{ file: MEMORY_CONFIG_FILE, code: 'MEMORY_CONFIG_INVALID_JSON', message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}\nFix: repair ${MEMORY_CONFIG_FILE} so it is valid JSON, or remove it before regenerating the default config with \`dotdotgod config init <root>\`.` }] };
|
|
553
518
|
}
|
|
@@ -577,7 +542,6 @@ export function defaultDotdotgodConfigData() {
|
|
|
577
542
|
validation: cloneValidationPolicy(config.validation),
|
|
578
543
|
impactRanking: cloneImpactRankingPolicy(config.impactRanking),
|
|
579
544
|
referenceExpansion: { fuzzy: { lowSignal: { add: [], remove: [] } } },
|
|
580
|
-
integrations: cloneIntegrationsPolicy(config.integrations),
|
|
581
545
|
load: cloneLoadPolicy(config.load),
|
|
582
546
|
planMode: clonePlanModePolicy(config.planMode),
|
|
583
547
|
};
|
|
@@ -605,7 +569,6 @@ export function memoryConfigSummary(config) {
|
|
|
605
569
|
validation: cloneValidationPolicy(config.validation ?? DEFAULT_VALIDATION_POLICY),
|
|
606
570
|
impactRanking: cloneImpactRankingPolicy(config.impactRanking ?? DEFAULT_IMPACT_RANKING_POLICY),
|
|
607
571
|
referenceExpansion: cloneReferenceExpansionPolicy(config.referenceExpansion),
|
|
608
|
-
integrations: cloneIntegrationsPolicy(config.integrations),
|
|
609
572
|
load: cloneLoadPolicy(config.load),
|
|
610
573
|
planMode: clonePlanModePolicy(config.planMode),
|
|
611
574
|
};
|
package/src/validate/run.mjs
CHANGED
|
@@ -61,9 +61,17 @@ export function runValidate(argv) {
|
|
|
61
61
|
const maxChars = options.maxChars ?? validationPolicy.markdown.maxChars;
|
|
62
62
|
const markdownSizePrompt = (path) => {
|
|
63
63
|
const area = resolveMemoryArea(path, memoryConfig);
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
64
|
+
const clarify = area?.clarify;
|
|
65
|
+
const guidance = [
|
|
66
|
+
clarify?.documentType ? `document type: ${clarify.documentType}` : null,
|
|
67
|
+
clarify?.audience?.length ? `audience: ${clarify.audience.join(', ')}` : null,
|
|
68
|
+
clarify?.clarityGoal ? `clarity goal: ${clarify.clarityGoal}` : null,
|
|
69
|
+
clarify?.editRules?.length ? `edit rules: ${clarify.editRules.join('; ')}` : null,
|
|
70
|
+
].filter(Boolean);
|
|
71
|
+
const evidence = guidance.length > 0
|
|
72
|
+
? ` Follow this resolved clarification guidance: ${guidance.join('; ')}.`
|
|
73
|
+
: ' Preserve the document\'s current purpose and established meaning, using its content, links, and nearest README.md as evidence.';
|
|
74
|
+
return `Split ${path} into focused UPPER_SNAKE_CASE markdown files.${evidence} Keep each file focused and update the nearest README.md index with every new file's purpose.`;
|
|
67
75
|
};
|
|
68
76
|
for (const error of memoryConfig.errors ?? []) errors.push(error);
|
|
69
77
|
const FILENAME_QUALITY_AREAS = new Set(
|
|
@@ -73,18 +81,14 @@ export function runValidate(argv) {
|
|
|
73
81
|
.map(p => { const m = p.match(/^docs\/([^/*]+)\/\*\*$/); return m ? m[1] : null; })
|
|
74
82
|
.filter(Boolean)
|
|
75
83
|
);
|
|
84
|
+
const filenameQualityScope = [...FILENAME_QUALITY_AREAS].sort().map(area => `docs/${area}`).join(', ') || 'configured shared documentation areas';
|
|
76
85
|
const FILENAME_QUALITY_PROMPT =
|
|
77
|
-
'
|
|
78
|
-
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'Avoid - numbered splits: API_1.md, API_2.md, API_3_1_*.md, 01_*.md, *_01.md\n' +
|
|
84
|
-
'Avoid - overly abstract: COMMON.md, OVERVIEW.md, DETAILS.md, NOTES.md, MISC.md, PART_1.md, SECTION_1.md\n' +
|
|
85
|
-
'Acceptable exceptions: README.md (directory index), files with clear HTTP status code semantics (e.g. RESPONSE_200_OK.md), ' +
|
|
86
|
-
'files in the project validation.markdown.filename.allow allowlist.\n' +
|
|
87
|
-
'Good renames: API_1.md → BIZ_RESERVATIONS_SCHEDULE.md, COMMON.md → BIZ_AUTH_ERROR_CONVENTIONS.md, OVERVIEW.md → REGISTRATION_HANDOFF_SUMMARY.md';
|
|
86
|
+
'Filenames and paths are LLM/agent context signals used for impact ranking, search, and retrieval.\n' +
|
|
87
|
+
`Use meaning-based filenames in ${filenameQualityScope}. A filename should identify its primary endpoint, screen, policy, state, response type, or domain subject. ` +
|
|
88
|
+
'Derive split-document names from headings, API paths, and established domain terms.\n' +
|
|
89
|
+
'Rename numbered or abstract series such as API_1.md, PART_1.md, COMMON.md, or DETAILS.md to subject-specific names. ' +
|
|
90
|
+
'Examples: API_1.md → BIZ_RESERVATIONS_SCHEDULE.md; COMMON.md → BIZ_AUTH_ERROR_CONVENTIONS.md.\n' +
|
|
91
|
+
'README.md directory indexes, clear HTTP-status filenames such as RESPONSE_200_OK.md, and configured filename allowlist entries remain valid.';
|
|
88
92
|
walk(docs);
|
|
89
93
|
const dirSiblingMap = new Map();
|
|
90
94
|
for (const f of markdownFiles) {
|
package/src/trello/client.mjs
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
const TRELLO_API_BASE = 'https://api.trello.com';
|
|
2
|
-
|
|
3
|
-
function clean(value) {
|
|
4
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function credentialError(message) {
|
|
8
|
-
return {
|
|
9
|
-
ok: false,
|
|
10
|
-
error: {
|
|
11
|
-
code: 'TRELLO_CREDENTIALS_MISSING',
|
|
12
|
-
message: `${message} Fix: configure GitHub Actions secrets or environment variables TRELLO_API_KEY and TRELLO_TOKEN, then retry from the trusted Trello sync workflow.`,
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function resolveTrelloCredentials({ env = process.env } = {}) {
|
|
18
|
-
const apiKey = clean(env.TRELLO_API_KEY);
|
|
19
|
-
const token = clean(env.TRELLO_TOKEN);
|
|
20
|
-
if (!apiKey || !token) return credentialError('Trello write mode credentials are missing or empty.');
|
|
21
|
-
return { ok: true, credentials: { apiKey, token, source: 'env' } };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function trelloApiError({ status = 0, statusText = '', body = '', retryAfter = null, operation = 'Trello API request', idOrShortLink = '', redact = (value) => value } = {}) {
|
|
25
|
-
const bodyText = redact(typeof body === 'string' ? body : JSON.stringify(body ?? ''));
|
|
26
|
-
const code = bodyText.includes('API_TOKEN_LIMIT_EXCEEDED') ? 'API_TOKEN_LIMIT_EXCEEDED' : bodyText.includes('API_KEY_LIMIT_EXCEEDED') ? 'API_KEY_LIMIT_EXCEEDED' : 'TRELLO_API_FAILED';
|
|
27
|
-
const rateLimitGuidance = status === 429 ? ` Wait and retry${retryAfter ? ` after ${retryAfter} seconds` : ''}.` : '';
|
|
28
|
-
return {
|
|
29
|
-
code,
|
|
30
|
-
status,
|
|
31
|
-
retryAfter,
|
|
32
|
-
message: `${operation} failed${idOrShortLink ? ` for Trello card ${idOrShortLink}` : ''}${status ? ` with HTTP ${status}${statusText ? ` ${statusText}` : ''}` : ''}${bodyText ? `: ${bodyText}` : ''}. Fix:${rateLimitGuidance || ' check card access, credentials, and Trello response details, then retry.'}`,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function createTrelloClient({ credentials, fetchImpl = globalThis.fetch, baseUrl = TRELLO_API_BASE } = {}) {
|
|
37
|
-
if (typeof fetchImpl !== 'function') throw new Error('Trello client requires a fetch implementation.');
|
|
38
|
-
const auth = { key: credentials?.apiKey ?? '', token: credentials?.token ?? '' };
|
|
39
|
-
const redact = (value = '') => String(value).split(auth.key).join('[REDACTED_API_KEY]').split(auth.token).join('[REDACTED_TOKEN]');
|
|
40
|
-
|
|
41
|
-
async function request(path, { method = 'GET', params = {}, body = null, operation, idOrShortLink } = {}) {
|
|
42
|
-
const url = new URL(path, baseUrl);
|
|
43
|
-
for (const [key, value] of Object.entries({ ...params, ...auth })) url.searchParams.set(key, value);
|
|
44
|
-
const init = { method };
|
|
45
|
-
if (body !== null) {
|
|
46
|
-
init.headers = { 'Content-Type': 'application/json' };
|
|
47
|
-
init.body = JSON.stringify(body);
|
|
48
|
-
}
|
|
49
|
-
let response;
|
|
50
|
-
try {
|
|
51
|
-
response = await fetchImpl(url, init);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
throw trelloApiError({ operation, idOrShortLink, body: error?.message ?? 'network error', redact });
|
|
54
|
-
}
|
|
55
|
-
const bodyText = await response.text();
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
throw trelloApiError({ status: response.status, statusText: response.statusText, body: bodyText, retryAfter: response.headers?.get?.('Retry-After') ?? null, operation, idOrShortLink, redact });
|
|
58
|
-
}
|
|
59
|
-
if (!bodyText) return null;
|
|
60
|
-
try {
|
|
61
|
-
return JSON.parse(bodyText);
|
|
62
|
-
} catch {
|
|
63
|
-
throw trelloApiError({ status: response.status, statusText: response.statusText, body: 'Malformed JSON response', operation, idOrShortLink, redact });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
async getCard(idOrShortLink) {
|
|
69
|
-
const card = await request(`/1/cards/${encodeURIComponent(idOrShortLink)}`, { params: { fields: 'id,idBoard,desc', customFieldItems: 'true' }, operation: 'Card lookup', idOrShortLink });
|
|
70
|
-
if (!card || typeof card.id !== 'string' || typeof card.idBoard !== 'string') throw trelloApiError({ body: 'Malformed card response', operation: 'Card lookup', idOrShortLink, redact });
|
|
71
|
-
return card;
|
|
72
|
-
},
|
|
73
|
-
async listAttachments(idOrShortLink) {
|
|
74
|
-
const attachments = await request(`/1/cards/${encodeURIComponent(idOrShortLink)}/attachments`, { operation: 'Attachment lookup', idOrShortLink });
|
|
75
|
-
if (!Array.isArray(attachments)) throw trelloApiError({ body: 'Malformed attachments response', operation: 'Attachment lookup', idOrShortLink, redact });
|
|
76
|
-
return attachments;
|
|
77
|
-
},
|
|
78
|
-
async createAttachment(idOrShortLink, githubUrl) {
|
|
79
|
-
return request(`/1/cards/${encodeURIComponent(idOrShortLink)}/attachments`, { method: 'POST', params: { url: githubUrl }, operation: 'Attachment creation', idOrShortLink });
|
|
80
|
-
},
|
|
81
|
-
async listCustomFields(idBoard) {
|
|
82
|
-
const fields = await request(`/1/boards/${encodeURIComponent(idBoard)}/customFields`, { operation: 'Custom field lookup' });
|
|
83
|
-
if (!Array.isArray(fields)) throw trelloApiError({ body: 'Malformed custom fields response', operation: 'Custom field lookup', redact });
|
|
84
|
-
return fields;
|
|
85
|
-
},
|
|
86
|
-
async createCustomField(idBoard, { name, type = 'text' } = {}) {
|
|
87
|
-
return request('/1/customFields', { method: 'POST', params: { idModel: idBoard, modelType: 'board', name, type, pos: 'bottom', display_cardFront: 'false' }, operation: 'Custom field creation' });
|
|
88
|
-
},
|
|
89
|
-
async updateCardCustomFieldText(idCard, idCustomField, text) {
|
|
90
|
-
return request(`/1/cards/${encodeURIComponent(idCard)}/customField/${encodeURIComponent(idCustomField)}/item`, { method: 'PUT', body: { value: { text } }, operation: 'Custom field update', idOrShortLink: idCard });
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_DOTDOTGOD_CUSTOM_FIELD_NAME = 'dotdotgod-view';
|
|
2
|
-
|
|
3
|
-
function cleanString(value) {
|
|
4
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function dotdotgodCustomFieldName({ env = process.env, name = '' } = {}) {
|
|
8
|
-
return cleanString(name) || cleanString(env.DOTDOTGOD_TRELLO_CUSTOM_FIELD_NAME) || DEFAULT_DOTDOTGOD_CUSTOM_FIELD_NAME;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function buildLinkedDocsCustomFieldData({ docsPath, githubUrl, trelloUrl, repositoryKey, repositoryLabel, title, summary, traceabilityState, traceabilityDetails = [] } = {}) {
|
|
12
|
-
return {
|
|
13
|
-
repositoryKey: cleanString(repositoryKey),
|
|
14
|
-
repositoryLabel: cleanString(repositoryLabel) || cleanString(repositoryKey).split('/').filter(Boolean).at(-1) || '',
|
|
15
|
-
docsPath: cleanString(docsPath),
|
|
16
|
-
githubUrl: cleanString(githubUrl),
|
|
17
|
-
trelloUrl: cleanString(trelloUrl),
|
|
18
|
-
title: cleanString(title),
|
|
19
|
-
summary: cleanString(summary),
|
|
20
|
-
traceabilityState: cleanString(traceabilityState) || 'not_required',
|
|
21
|
-
traceabilityDetails: Array.isArray(traceabilityDetails) ? traceabilityDetails.map((item) => String(item)) : [],
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function serializeLinkedDocsCustomFieldPayload(entries = []) {
|
|
26
|
-
return JSON.stringify({ version: 1, entries: entries.map((entry) => buildLinkedDocsCustomFieldData(entry)) });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function parseLinkedDocsCustomFieldPayload(text = '') {
|
|
30
|
-
const value = cleanString(text);
|
|
31
|
-
if (!value) return { ok: true, entries: [] };
|
|
32
|
-
let parsed;
|
|
33
|
-
try {
|
|
34
|
-
parsed = JSON.parse(value);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
return {
|
|
37
|
-
ok: false,
|
|
38
|
-
error: {
|
|
39
|
-
code: 'TRELLO_CUSTOM_FIELD_INVALID',
|
|
40
|
-
message: `dotdotgod custom field contains invalid JSON. Cause: ${error.message}. Fix: clear the custom field or restore a valid dotdotgod linked-docs payload, then retry sync.`,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
45
|
-
return {
|
|
46
|
-
ok: false,
|
|
47
|
-
error: {
|
|
48
|
-
code: 'TRELLO_CUSTOM_FIELD_INVALID',
|
|
49
|
-
message: 'dotdotgod custom field has an unsupported shape. Fix: clear the custom field or restore a version 1 linked-docs payload with an entries array, then retry sync.',
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
const entries = parsed.entries.map((entry) => buildLinkedDocsCustomFieldData(entry));
|
|
54
|
-
const keys = entries.map((entry) => entry.repositoryKey).filter(Boolean);
|
|
55
|
-
if (keys.length !== new Set(keys).size) {
|
|
56
|
-
return {
|
|
57
|
-
ok: false,
|
|
58
|
-
error: {
|
|
59
|
-
code: 'TRELLO_CUSTOM_FIELD_DUPLICATE_REPOSITORY',
|
|
60
|
-
message: 'dotdotgod custom field contains duplicate repository entries. Fix: keep one entry per repository key, then retry sync.',
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return { ok: true, entries };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function mergeLinkedDocsCustomFieldValue(existingText = '', entry = {}) {
|
|
68
|
-
const current = buildLinkedDocsCustomFieldData(entry);
|
|
69
|
-
if (!current.repositoryKey) {
|
|
70
|
-
return {
|
|
71
|
-
ok: false,
|
|
72
|
-
error: {
|
|
73
|
-
code: 'TRELLO_CUSTOM_FIELD_REPOSITORY_MISSING',
|
|
74
|
-
message: 'Cannot update dotdotgod custom field without a repository key. Fix: run from a trusted GitHub Actions repository or configure repository metadata, then retry sync.',
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const parsed = parseLinkedDocsCustomFieldPayload(existingText);
|
|
79
|
-
if (!parsed.ok) return parsed;
|
|
80
|
-
const entries = [...parsed.entries];
|
|
81
|
-
const index = entries.findIndex((item) => item.repositoryKey === current.repositoryKey);
|
|
82
|
-
let action = 'create-payload';
|
|
83
|
-
if (index >= 0) {
|
|
84
|
-
const before = JSON.stringify(entries[index]);
|
|
85
|
-
const after = JSON.stringify(current);
|
|
86
|
-
action = before === after ? 'unchanged-entry' : 'replace-entry';
|
|
87
|
-
entries[index] = current;
|
|
88
|
-
} else if (entries.length > 0) {
|
|
89
|
-
action = 'append-entry';
|
|
90
|
-
entries.push(current);
|
|
91
|
-
} else {
|
|
92
|
-
entries.push(current);
|
|
93
|
-
}
|
|
94
|
-
const text = serializeLinkedDocsCustomFieldPayload(entries);
|
|
95
|
-
const status = cleanString(existingText) === text ? 'unchanged' : 'written';
|
|
96
|
-
return { ok: true, status, action: status === 'unchanged' ? 'unchanged-entry' : action, text, entries, preservedRepositoryEntries: entries.filter((item) => item.repositoryKey !== current.repositoryKey).length };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function customFieldTextValue(card = {}, idCustomField = '') {
|
|
100
|
-
const item = Array.isArray(card.customFieldItems) ? card.customFieldItems.find((fieldItem) => fieldItem?.idCustomField === idCustomField) : null;
|
|
101
|
-
return typeof item?.value?.text === 'string' ? item.value.text : '';
|
|
102
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
function normalizeGitHubRepositoryKey(value = '') {
|
|
6
|
-
const text = String(value).trim();
|
|
7
|
-
const match = text.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/u);
|
|
8
|
-
if (!match) return null;
|
|
9
|
-
return `${match[1]}/${match[2]}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function normalizeGitHubRemote(value = '') {
|
|
13
|
-
const text = String(value).trim();
|
|
14
|
-
if (!text) return null;
|
|
15
|
-
let match = text.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/u);
|
|
16
|
-
if (match) return { repositoryUrl: `https://github.com/${match[1]}/${match[2]}`, repositoryKey: `${match[1]}/${match[2]}` };
|
|
17
|
-
match = text.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/u);
|
|
18
|
-
if (match) return { repositoryUrl: `https://github.com/${match[1]}/${match[2]}`, repositoryKey: `${match[1]}/${match[2]}` };
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function encodePath(path = '') {
|
|
23
|
-
return path.split('/').map((part) => encodeURIComponent(part)).join('/');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function repositoryFromPackage(root) {
|
|
27
|
-
try {
|
|
28
|
-
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
29
|
-
const raw = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
|
|
30
|
-
return normalizeGitHubRemote(String(raw ?? '').replace(/^git\+/, ''));
|
|
31
|
-
} catch {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function gitValue(root, args) {
|
|
37
|
-
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
38
|
-
if (result.status !== 0) return '';
|
|
39
|
-
return result.stdout.trim();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function repositoryResult(repositoryUrl, repositoryKey, source, warnings = []) {
|
|
43
|
-
return { ok: true, repositoryUrl, repositoryKey, source, warnings, errors: [] };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function resolveGitHubRepositoryIdentity({ root = '.', repository, repositoryUrl, remoteUrl, env = process.env, git = true, mode = 'dry-run' } = {}) {
|
|
47
|
-
const warnings = [];
|
|
48
|
-
const explicitKey = normalizeGitHubRepositoryKey(repository);
|
|
49
|
-
if (explicitKey) return repositoryResult(`https://github.com/${explicitKey}`, explicitKey, 'explicit-repository', warnings);
|
|
50
|
-
|
|
51
|
-
const explicitRemote = normalizeGitHubRemote(repositoryUrl);
|
|
52
|
-
if (explicitRemote) return repositoryResult(explicitRemote.repositoryUrl, explicitRemote.repositoryKey, 'explicit-repository-url', warnings);
|
|
53
|
-
|
|
54
|
-
const actionsKey = normalizeGitHubRepositoryKey(env?.GITHUB_REPOSITORY);
|
|
55
|
-
if (actionsKey) return repositoryResult(`https://github.com/${actionsKey}`, actionsKey, 'github-actions', warnings);
|
|
56
|
-
if (mode === 'write' && env?.GITHUB_REPOSITORY && !actionsKey) {
|
|
57
|
-
return { ok: false, repositoryUrl: '', repositoryKey: '', source: '', warnings, errors: [{ code: 'GITHUB_REPOSITORY_KEY_INVALID', message: 'Could not resolve a valid GitHub repository key from GITHUB_REPOSITORY. Fix: run the trusted workflow with GITHUB_REPOSITORY in owner/name form, then retry sync.' }] };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const injectedRemote = normalizeGitHubRemote(remoteUrl);
|
|
61
|
-
if (injectedRemote) {
|
|
62
|
-
warnings.push({ code: 'GITHUB_URL_REMOTE_FALLBACK', message: 'GitHub URL resolved from injected git remote fallback.' });
|
|
63
|
-
return repositoryResult(injectedRemote.repositoryUrl, injectedRemote.repositoryKey, 'remote-url', warnings);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (mode !== 'write') {
|
|
67
|
-
const packageRepo = repositoryFromPackage(root);
|
|
68
|
-
if (packageRepo) {
|
|
69
|
-
warnings.push({ code: 'GITHUB_URL_PACKAGE_FALLBACK', message: 'GitHub URL resolved from package repository metadata.' });
|
|
70
|
-
return repositoryResult(packageRepo.repositoryUrl, packageRepo.repositoryKey, 'package', warnings);
|
|
71
|
-
}
|
|
72
|
-
if (git) {
|
|
73
|
-
const remote = gitValue(root, ['remote', 'get-url', 'origin']);
|
|
74
|
-
const gitRepo = normalizeGitHubRemote(remote);
|
|
75
|
-
if (gitRepo) {
|
|
76
|
-
warnings.push({ code: 'GITHUB_URL_GIT_REMOTE_FALLBACK', message: 'GitHub URL resolved from local git origin remote.' });
|
|
77
|
-
return repositoryResult(gitRepo.repositoryUrl, gitRepo.repositoryKey, 'git-remote', warnings);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const code = mode === 'write' ? 'GITHUB_REPOSITORY_KEY_UNRESOLVED' : 'GITHUB_URL_UNRESOLVED_REPOSITORY';
|
|
83
|
-
const message = mode === 'write'
|
|
84
|
-
? 'Could not resolve a trusted GitHub repository key for Trello custom field entry. Fix: run write mode from GitHub Actions with GITHUB_REPOSITORY or provide explicit GitHub repository metadata, then retry sync.'
|
|
85
|
-
: 'Could not resolve a GitHub repository URL.';
|
|
86
|
-
return { ok: false, repositoryUrl: '', repositoryKey: '', source: '', warnings, errors: [{ code, message }] };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function resolveGitHubFileUrl({ root = '.', file, repository, repositoryUrl, branch, remoteUrl, env = process.env, git = true, mode = 'dry-run' } = {}) {
|
|
90
|
-
const identity = resolveGitHubRepositoryIdentity({ root, repository, repositoryUrl, remoteUrl, env, git, mode });
|
|
91
|
-
const warnings = [...(identity.warnings ?? [])];
|
|
92
|
-
let resolvedBranch = branch;
|
|
93
|
-
if (!resolvedBranch && mode === 'write') {
|
|
94
|
-
if (env?.GITHUB_REF_NAME) resolvedBranch = env.GITHUB_REF_NAME;
|
|
95
|
-
else if (typeof env?.GITHUB_REF === 'string' && env.GITHUB_REF.startsWith('refs/heads/')) resolvedBranch = env.GITHUB_REF.slice('refs/heads/'.length);
|
|
96
|
-
else if (env?.GITHUB_DEFAULT_BRANCH) resolvedBranch = env.GITHUB_DEFAULT_BRANCH;
|
|
97
|
-
}
|
|
98
|
-
if (!resolvedBranch && git && mode !== 'write') {
|
|
99
|
-
resolvedBranch = gitValue(root, ['branch', '--show-current']);
|
|
100
|
-
if (resolvedBranch) warnings.push({ code: 'GITHUB_URL_GIT_BRANCH_FALLBACK', message: 'GitHub branch resolved from local git state.' });
|
|
101
|
-
}
|
|
102
|
-
const errors = [];
|
|
103
|
-
if (!identity.ok) errors.push(...identity.errors);
|
|
104
|
-
if (!resolvedBranch) errors.push({ code: 'GITHUB_URL_UNRESOLVED_BRANCH', message: 'Could not resolve a GitHub branch.' });
|
|
105
|
-
if (errors.length > 0) return { ok: false, url: '', repositoryKey: identity.repositoryKey ?? '', repositoryUrl: identity.repositoryUrl ?? '', warnings, errors };
|
|
106
|
-
return { ok: true, url: `${identity.repositoryUrl}/blob/${encodeURIComponent(resolvedBranch)}/${encodePath(file)}`, repositoryKey: identity.repositoryKey, repositoryUrl: identity.repositoryUrl, warnings, errors: [] };
|
|
107
|
-
}
|
package/src/trello/metadata.mjs
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
const TRELLO_KEYS = new Set(['trelloUrl', 'trelloShortLink', 'trelloCardId']);
|
|
2
|
-
|
|
3
|
-
export function parseFrontmatter(content = '') {
|
|
4
|
-
if (!String(content).startsWith('---\n')) return { data: {}, errors: [] };
|
|
5
|
-
const end = String(content).indexOf('\n---', 4);
|
|
6
|
-
if (end === -1) return { data: {}, errors: [{ code: 'TRELLO_FRONTMATTER_INVALID', message: 'Frontmatter start marker is missing a closing marker.' }] };
|
|
7
|
-
const raw = String(content).slice(4, end).split('\n');
|
|
8
|
-
const data = {};
|
|
9
|
-
const errors = [];
|
|
10
|
-
for (const [index, line] of raw.entries()) {
|
|
11
|
-
const trimmed = line.trim();
|
|
12
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
13
|
-
const match = trimmed.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
|
|
14
|
-
if (!match) continue;
|
|
15
|
-
const key = match[1];
|
|
16
|
-
let value = match[2].trim();
|
|
17
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
18
|
-
if (Object.hasOwn(data, key)) errors.push({ code: 'TRELLO_FRONTMATTER_DUPLICATE_KEY', message: `Duplicate frontmatter key: ${key}.`, line: index + 2 });
|
|
19
|
-
data[key] = value;
|
|
20
|
-
}
|
|
21
|
-
return { data, errors };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function extractTrelloShortLink(trelloUrl = '') {
|
|
25
|
-
let url;
|
|
26
|
-
try {
|
|
27
|
-
url = new URL(trelloUrl);
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
if (url.protocol !== 'https:' || url.hostname !== 'trello.com') return null;
|
|
32
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
33
|
-
if (parts[0] !== 'c' || !parts[1] || parts.length > 3) return null;
|
|
34
|
-
return parts[1];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function parseTrelloMetadata(content = '') {
|
|
38
|
-
const { data, errors } = parseFrontmatter(content);
|
|
39
|
-
const warnings = [];
|
|
40
|
-
const unsupported = [...TRELLO_KEYS].filter((key) => key !== 'trelloUrl' && Object.hasOwn(data, key));
|
|
41
|
-
const trelloUrl = typeof data.trelloUrl === 'string' ? data.trelloUrl.trim() : '';
|
|
42
|
-
if (unsupported.length > 0) {
|
|
43
|
-
warnings.push({ code: 'TRELLO_UNSUPPORTED_METADATA', message: `Unsupported Trello metadata ignored in Phase 1: ${unsupported.join(', ')}. Use trelloUrl only.` });
|
|
44
|
-
}
|
|
45
|
-
if (!trelloUrl) {
|
|
46
|
-
errors.push({ code: 'TRELLO_METADATA_MISSING_URL', message: 'Missing required frontmatter field: trelloUrl.' });
|
|
47
|
-
return { ok: false, trelloUrl: '', shortLink: '', warnings, errors };
|
|
48
|
-
}
|
|
49
|
-
const shortLink = extractTrelloShortLink(trelloUrl);
|
|
50
|
-
if (!shortLink) errors.push({ code: 'TRELLO_METADATA_INVALID_URL', message: 'trelloUrl must match https://trello.com/c/<shortLink>/ or https://trello.com/c/<shortLink>/<slug>.' });
|
|
51
|
-
return { ok: errors.length === 0, trelloUrl, shortLink: shortLink ?? '', warnings, errors };
|
|
52
|
-
}
|
package/src/trello/report.mjs
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
function plural(value, label) {
|
|
2
|
-
return `${value} ${label}${value === 1 ? '' : 's'}`;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function pushMessages(lines, label, messages, indent = '') {
|
|
6
|
-
if ((messages ?? []).length === 0) return;
|
|
7
|
-
lines.push(`${indent}${label}:`);
|
|
8
|
-
for (const message of messages) lines.push(`${indent} - ${message.code}: ${message.message}`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function formatTrelloDryRunReport(report) {
|
|
12
|
-
const lines = ['Trello docs sync dry-run'];
|
|
13
|
-
lines.push(`Root: ${report.root}`);
|
|
14
|
-
lines.push(`Scan paths: ${report.scanPaths.join(', ')}`);
|
|
15
|
-
lines.push(`Matched: ${plural(report.totals.matchedFiles, 'file')}; planned: ${plural(report.totals.plannedUpdates, 'update')}; warnings: ${report.totals.warnings}; errors: ${report.totals.errors}`);
|
|
16
|
-
pushMessages(lines, 'Warnings', report.warnings);
|
|
17
|
-
pushMessages(lines, 'Errors', report.errors);
|
|
18
|
-
for (const file of report.files) {
|
|
19
|
-
lines.push('');
|
|
20
|
-
lines.push(`File: ${file.file}`);
|
|
21
|
-
lines.push(` Trello URL: ${file.trelloUrl || '(missing)'}`);
|
|
22
|
-
if (file.shortLink) lines.push(` Trello short link: ${file.shortLink}`);
|
|
23
|
-
lines.push(` GitHub URL: ${file.githubUrl || '(unresolved)'}`);
|
|
24
|
-
if (file.repositoryKey) lines.push(` Repository entry: ${file.repositoryKey}`);
|
|
25
|
-
lines.push(` Planned attachment: ${file.plannedAttachmentAction}`);
|
|
26
|
-
lines.push(` Planned custom field: ${file.plannedCustomFieldAction}`);
|
|
27
|
-
lines.push(` Sync status: ${file.syncStatus}`);
|
|
28
|
-
lines.push(` Traceability: ${file.traceability.state}${file.traceability.details.length ? ` (${file.traceability.details.join(', ')})` : ''}`);
|
|
29
|
-
pushMessages(lines, 'Warnings', file.warnings, ' ');
|
|
30
|
-
pushMessages(lines, 'Errors', file.errors, ' ');
|
|
31
|
-
}
|
|
32
|
-
return `${lines.join('\n')}\n`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function formatTrelloWriteReport(report) {
|
|
36
|
-
const lines = ['Trello docs sync write'];
|
|
37
|
-
lines.push(`Root: ${report.root}`);
|
|
38
|
-
lines.push(`Scan paths: ${report.scanPaths.join(', ')}`);
|
|
39
|
-
lines.push(`Matched: ${plural(report.totals.matchedFiles, 'file')}; written: ${report.totals.written}; unchanged: ${report.totals.unchanged}; blocked: ${report.totals.blocked}; conflict: ${report.totals.conflict}; api-failed: ${report.totals.apiFailed}; warnings: ${report.totals.warnings}; errors: ${report.totals.errors}`);
|
|
40
|
-
pushMessages(lines, 'Warnings', report.warnings);
|
|
41
|
-
pushMessages(lines, 'Errors', report.errors);
|
|
42
|
-
for (const file of report.files) {
|
|
43
|
-
lines.push('');
|
|
44
|
-
lines.push(`File: ${file.file}`);
|
|
45
|
-
lines.push(` Trello URL: ${file.trelloUrl || '(missing)'}`);
|
|
46
|
-
if (file.shortLink) lines.push(` Trello short link: ${file.shortLink}`);
|
|
47
|
-
lines.push(` GitHub URL: ${file.githubUrl || '(unresolved)'}`);
|
|
48
|
-
if (file.repositoryKey) lines.push(` Repository entry: ${file.repositoryKey}`);
|
|
49
|
-
lines.push(` Attachment: ${file.attachment?.status ?? 'not-run'}`);
|
|
50
|
-
const customFieldAction = file.customField?.action ? ` (${file.customField.action})` : '';
|
|
51
|
-
lines.push(` Custom field: ${file.customField?.status ?? 'not-run'}${customFieldAction}`);
|
|
52
|
-
if (file.customField?.name) lines.push(` Custom field name: ${file.customField.name}`);
|
|
53
|
-
if (file.customField?.preservedRepositoryEntries > 0) lines.push(` Preserved repository entries: ${file.customField.preservedRepositoryEntries}`);
|
|
54
|
-
lines.push(` Final status: ${file.finalStatus ?? file.syncStatus}`);
|
|
55
|
-
lines.push(` Traceability: ${file.traceability.state}${file.traceability.details.length ? ` (${file.traceability.details.join(', ')})` : ''}`);
|
|
56
|
-
pushMessages(lines, 'Warnings', file.warnings, ' ');
|
|
57
|
-
pushMessages(lines, 'Errors', file.errors, ' ');
|
|
58
|
-
}
|
|
59
|
-
return `${lines.join('\n')}\n`;
|
|
60
|
-
}
|
package/src/trello/summary.mjs
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { extractDotdotgodTraceabilityBlocks, validateTraceabilityBlock } from '../docs/traceability.mjs';
|
|
2
|
-
import { requiresTraceability } from '../memory/config.mjs';
|
|
3
|
-
|
|
4
|
-
export function summarizeTraceability({ content = '', root = '.', file = '', config } = {}) {
|
|
5
|
-
const blocks = extractDotdotgodTraceabilityBlocks(content);
|
|
6
|
-
if (blocks.some((block) => block.error)) {
|
|
7
|
-
return { state: 'invalid', details: blocks.filter((block) => block.error).map((block) => `Invalid JSON at line ${block.line}: ${block.error}`), errors: [{ code: 'TRELLO_TRACEABILITY_INVALID', message: 'Malformed json dotdotgod traceability block.' }] };
|
|
8
|
-
}
|
|
9
|
-
if (blocks.length === 0) {
|
|
10
|
-
const required = requiresTraceability(file, config);
|
|
11
|
-
return { state: required ? 'missing' : 'not_required', details: [], errors: required ? [{ code: 'TRELLO_TRACEABILITY_MISSING', message: 'Missing required json dotdotgod traceability block.' }] : [] };
|
|
12
|
-
}
|
|
13
|
-
const errors = blocks.flatMap((block) => validateTraceabilityBlock(block.data, root, file, block.line, config));
|
|
14
|
-
if (errors.length > 0) return { state: 'invalid', details: errors.map((error) => `${error.code}: ${error.message.split('\n')[0]}`), errors: [{ code: 'TRELLO_TRACEABILITY_INVALID', message: 'Invalid json dotdotgod traceability block.' }] };
|
|
15
|
-
const latest = blocks.at(-1).data;
|
|
16
|
-
const details = [];
|
|
17
|
-
if (Array.isArray(latest.implementedBy) && latest.implementedBy.length > 0) details.push(`implementedBy: ${latest.implementedBy.length}`);
|
|
18
|
-
if (Array.isArray(latest.verifiedBy) && latest.verifiedBy.length > 0) details.push(`verifiedBy: ${latest.verifiedBy.length}`);
|
|
19
|
-
if (Array.isArray(latest.relatedDocs) && latest.relatedDocs.length > 0) details.push(`relatedDocs: ${latest.relatedDocs.length}`);
|
|
20
|
-
return { state: 'present', details, errors: [] };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function stripFrontmatter(content = '') {
|
|
24
|
-
return String(content).replace(/^---\n[\s\S]*?\n---\n?/u, '');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function stripCodeBlocks(content = '') {
|
|
28
|
-
return String(content).replace(/```[\s\S]*?```/gu, '');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function compactText(value = '', maxLength = 280) {
|
|
32
|
-
const compact = String(value).replace(/\s+/gu, ' ').trim();
|
|
33
|
-
if (compact.length <= maxLength) return compact;
|
|
34
|
-
return `${compact.slice(0, maxLength - 1).trimEnd()}…`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function summarizeMarkdownPreview({ content = '', file = '' } = {}) {
|
|
38
|
-
const body = stripCodeBlocks(stripFrontmatter(content));
|
|
39
|
-
const lines = body.split(/\r?\n/u).map((line) => line.trim());
|
|
40
|
-
const title = lines.find((line) => /^#\s+\S/u.test(line))?.replace(/^#\s+/u, '').trim() || file.split('/').at(-1)?.replace(/\.md$/u, '') || '';
|
|
41
|
-
const summary = compactText(lines.find((line) => line && !line.startsWith('#') && !line.startsWith('<!--') && !line.startsWith('- ') && !line.startsWith('|')) ?? '');
|
|
42
|
-
return { title: compactText(title, 120), summary };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function buildLinkedDocsData({ file, content = '', githubUrl, trelloUrl, syncStatus, traceability, repositoryKey } = {}) {
|
|
46
|
-
const preview = summarizeMarkdownPreview({ content, file });
|
|
47
|
-
return {
|
|
48
|
-
docsPath: file,
|
|
49
|
-
githubUrl,
|
|
50
|
-
trelloUrl,
|
|
51
|
-
syncStatus,
|
|
52
|
-
repositoryKey,
|
|
53
|
-
repositoryLabel: String(repositoryKey ?? '').split('/').filter(Boolean).at(-1) || '',
|
|
54
|
-
title: preview.title,
|
|
55
|
-
summary: preview.summary,
|
|
56
|
-
traceabilityState: traceability?.state ?? 'not_required',
|
|
57
|
-
traceabilityDetails: [...(traceability?.details ?? [])],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
package/src/trello/sync.mjs
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
3
|
-
import { rel } from '../common/paths.mjs';
|
|
4
|
-
import { matchPathPattern, readMemoryConfig, trelloSyncPaths } from '../memory/config.mjs';
|
|
5
|
-
import { parseTrelloMetadata } from './metadata.mjs';
|
|
6
|
-
import { resolveGitHubFileUrl } from './github-url.mjs';
|
|
7
|
-
import { buildLinkedDocsData, summarizeTraceability } from './summary.mjs';
|
|
8
|
-
import { formatTrelloDryRunReport, formatTrelloWriteReport } from './report.mjs';
|
|
9
|
-
import { createTrelloClient, resolveTrelloCredentials } from './client.mjs';
|
|
10
|
-
import { customFieldTextValue, dotdotgodCustomFieldName, mergeLinkedDocsCustomFieldValue } from './custom-fields.mjs';
|
|
11
|
-
|
|
12
|
-
function shouldSkipDir(path) {
|
|
13
|
-
return path === '.git' || path === 'node_modules' || path === '.dotdotgod';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function walkMarkdown(root, dir = root, files = []) {
|
|
17
|
-
if (!existsSync(dir)) return files;
|
|
18
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
19
|
-
if (entry.isDirectory()) {
|
|
20
|
-
if (!shouldSkipDir(entry.name)) walkMarkdown(root, join(dir, entry.name), files);
|
|
21
|
-
} else if (entry.isFile() && entry.name.endsWith('.md')) files.push(join(dir, entry.name));
|
|
22
|
-
}
|
|
23
|
-
return files;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function matchedByPatterns(path, patterns) {
|
|
27
|
-
return patterns.some((pattern) => matchPathPattern(path, pattern));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function fileReport({ file, trelloUrl = '', shortLink = '', githubUrl = '', repositoryKey = '', syncStatus = 'blocked', traceability = { state: 'not_required', details: [] }, warnings = [], errors = [], linkedDocsData = null }) {
|
|
31
|
-
return { file, trelloUrl, shortLink, githubUrl, repositoryKey, plannedAttachmentAction: errors.length > 0 ? 'blocked' : 'add-or-verify', plannedCustomFieldAction: errors.length > 0 ? 'blocked' : 'sync-linked-docs', syncStatus, traceability, linkedDocsData, warnings, errors };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function actionableError(error, fallback = 'Trello API request failed. Fix: check credentials, card access, and Trello response details, then retry.') {
|
|
35
|
-
if (error && typeof error === 'object' && error.code && error.message) return error;
|
|
36
|
-
return { code: 'TRELLO_API_FAILED', message: error?.message ? `${error.message} Fix: check credentials, card access, and Trello response details, then retry.` : fallback };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function duplicateErrors(files) {
|
|
40
|
-
const byShortLink = new Map();
|
|
41
|
-
for (const file of files.filter((item) => item.syncStatus === 'planned')) {
|
|
42
|
-
byShortLink.set(file.shortLink, [...(byShortLink.get(file.shortLink) ?? []), file]);
|
|
43
|
-
}
|
|
44
|
-
const duplicateShortLinks = new Set([...byShortLink.entries()].filter(([, group]) => group.length > 1).map(([shortLink]) => shortLink));
|
|
45
|
-
for (const shortLink of duplicateShortLinks) {
|
|
46
|
-
const paths = byShortLink.get(shortLink).map((file) => file.file).join(', ');
|
|
47
|
-
for (const file of byShortLink.get(shortLink)) {
|
|
48
|
-
file.errors.push({ code: 'TRELLO_DUPLICATE_CARD', message: `Multiple markdown files point to Trello card ${shortLink}: ${paths}. Fix: keep only one markdown file linked to this Trello card, then retry sync.` });
|
|
49
|
-
file.syncStatus = 'conflict';
|
|
50
|
-
file.plannedAttachmentAction = 'conflict';
|
|
51
|
-
file.plannedCustomFieldAction = 'conflict';
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return duplicateShortLinks.size;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function planTrelloDryRun(rootInput = '.', options = {}) {
|
|
58
|
-
const root = resolve(rootInput);
|
|
59
|
-
const config = options.config ?? readMemoryConfig(root);
|
|
60
|
-
const scanPaths = trelloSyncPaths(config);
|
|
61
|
-
const reportWarnings = [];
|
|
62
|
-
const reportErrors = [];
|
|
63
|
-
if (config.errors?.length) reportErrors.push(...config.errors.map((error) => ({ code: error.code, message: error.message })));
|
|
64
|
-
const allFiles = walkMarkdown(root).map((file) => ({ abs: file, rel: rel(root, file) })).filter((file) => matchedByPatterns(file.rel, scanPaths)).sort((a, b) => a.rel.localeCompare(b.rel));
|
|
65
|
-
const files = [];
|
|
66
|
-
if (allFiles.length === 0) reportWarnings.push({ code: 'TRELLO_NO_MATCHING_DOCS', message: 'No markdown files matched Trello sync paths.' });
|
|
67
|
-
for (const item of allFiles) {
|
|
68
|
-
const content = readFileSync(item.abs, 'utf8');
|
|
69
|
-
const metadata = parseTrelloMetadata(content);
|
|
70
|
-
const warnings = [...metadata.warnings];
|
|
71
|
-
const errors = [...metadata.errors];
|
|
72
|
-
let githubUrl = '';
|
|
73
|
-
let repositoryKey = '';
|
|
74
|
-
if (errors.length === 0) {
|
|
75
|
-
const resolved = resolveGitHubFileUrl({ root, file: item.rel, env: options.env ?? process.env, mode: options.mode ?? 'dry-run', ...(options.github ?? {}) });
|
|
76
|
-
warnings.push(...resolved.warnings);
|
|
77
|
-
if (resolved.ok) {
|
|
78
|
-
githubUrl = resolved.url;
|
|
79
|
-
repositoryKey = resolved.repositoryKey;
|
|
80
|
-
} else errors.push(...resolved.errors);
|
|
81
|
-
}
|
|
82
|
-
const traceability = summarizeTraceability({ content, root, file: item.rel, config });
|
|
83
|
-
if (traceability.errors.length > 0) errors.push(...traceability.errors);
|
|
84
|
-
const syncStatus = errors.length > 0 ? 'blocked' : 'planned';
|
|
85
|
-
const linkedDocsData = buildLinkedDocsData({ file: item.rel, content, githubUrl, trelloUrl: metadata.trelloUrl, syncStatus, traceability, repositoryKey });
|
|
86
|
-
files.push(fileReport({ file: item.rel, trelloUrl: metadata.trelloUrl, shortLink: metadata.shortLink, githubUrl, repositoryKey, syncStatus, traceability: { state: traceability.state, details: traceability.details }, warnings, errors, linkedDocsData }));
|
|
87
|
-
}
|
|
88
|
-
duplicateErrors(files);
|
|
89
|
-
const warningCount = reportWarnings.length + files.reduce((sum, file) => sum + file.warnings.length, 0);
|
|
90
|
-
const errorCount = reportErrors.length + files.reduce((sum, file) => sum + file.errors.length, 0);
|
|
91
|
-
const report = { ok: errorCount === 0, command: 'trello sync', mode: 'dry-run', root, scanPaths, totals: { matchedFiles: files.length, plannedUpdates: files.filter((file) => file.syncStatus === 'planned').length, warnings: warningCount, errors: errorCount }, warnings: reportWarnings, errors: reportErrors, files };
|
|
92
|
-
return { report, exitCode: errorCount > 0 ? 1 : 0 };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function trustedCiWriteContext(env = process.env) {
|
|
96
|
-
const defaultBranch = env.GITHUB_DEFAULT_BRANCH || env.DOTDOTGOD_DEFAULT_BRANCH || env.GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH;
|
|
97
|
-
return env.GITHUB_ACTIONS === 'true' && env.GITHUB_EVENT_NAME === 'push' && Boolean(defaultBranch) && env.GITHUB_REF === `refs/heads/${defaultBranch}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function runTrelloWrite(rootInput = '.', options = {}) {
|
|
101
|
-
const env = options.env ?? process.env;
|
|
102
|
-
const { report: dryRunReport } = planTrelloDryRun(rootInput, { ...options, mode: 'write', env });
|
|
103
|
-
const report = { ...dryRunReport, mode: 'write', customFieldName: dotdotgodCustomFieldName({ env, name: options.customFieldName }), files: dryRunReport.files.map((file) => ({ ...file, attachment: { status: 'not-run' }, customField: { status: 'not-run' }, finalStatus: file.syncStatus === 'blocked' ? 'blocked' : 'pending' })) };
|
|
104
|
-
|
|
105
|
-
if (!trustedCiWriteContext(env)) {
|
|
106
|
-
return { exitCode: 2, stderr: 'Trello write mode is only allowed from the trusted GitHub Actions default-branch push workflow. Fix: run dotdotgod trello sync <root> --dry-run locally or merge docs to the default branch so the workflow can write to Trello.', report };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (report.files.length === 0) {
|
|
110
|
-
report.totals = { matchedFiles: 0, written: 0, unchanged: 0, blocked: 0, conflict: 0, apiFailed: 0, warnings: report.totals.warnings, errors: report.totals.errors };
|
|
111
|
-
report.ok = report.totals.errors === 0;
|
|
112
|
-
return { report, exitCode: report.ok ? 0 : 1 };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const credentials = options.credentials ?? resolveTrelloCredentials({ env });
|
|
116
|
-
if (!credentials.ok) return { exitCode: 2, stderr: credentials.error.message, report };
|
|
117
|
-
|
|
118
|
-
const client = options.trelloClient ?? createTrelloClient({ credentials: credentials.credentials, fetchImpl: options.fetch, baseUrl: options.trelloBaseUrl });
|
|
119
|
-
|
|
120
|
-
for (const file of report.files) {
|
|
121
|
-
if (file.syncStatus === 'blocked') {
|
|
122
|
-
file.finalStatus = 'blocked';
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (file.syncStatus === 'conflict') {
|
|
126
|
-
file.finalStatus = 'conflict';
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
let step = 'card';
|
|
130
|
-
try {
|
|
131
|
-
const card = await client.getCard(file.shortLink);
|
|
132
|
-
step = 'attachments';
|
|
133
|
-
const attachments = await client.listAttachments(file.shortLink);
|
|
134
|
-
if (attachments.some((attachment) => attachment?.url === file.githubUrl)) {
|
|
135
|
-
file.attachment.status = 'unchanged';
|
|
136
|
-
} else {
|
|
137
|
-
step = 'attachment-create';
|
|
138
|
-
await client.createAttachment(file.shortLink, file.githubUrl);
|
|
139
|
-
file.attachment.status = 'written';
|
|
140
|
-
}
|
|
141
|
-
step = 'custom-field-definition';
|
|
142
|
-
const fields = await client.listCustomFields(card.idBoard);
|
|
143
|
-
let field = fields.find((item) => item?.name === report.customFieldName && item?.type === 'text');
|
|
144
|
-
if (!field) field = await client.createCustomField(card.idBoard, { name: report.customFieldName, type: 'text' });
|
|
145
|
-
if (!field?.id) throw { code: 'TRELLO_CUSTOM_FIELD_INVALID', message: 'Trello custom field creation returned no field id. Fix: check board custom field permissions and retry.' };
|
|
146
|
-
const merged = mergeLinkedDocsCustomFieldValue(customFieldTextValue(card, field.id), file.linkedDocsData);
|
|
147
|
-
if (!merged.ok) {
|
|
148
|
-
file.customField.status = 'conflict';
|
|
149
|
-
file.errors.push(merged.error);
|
|
150
|
-
file.finalStatus = 'conflict';
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
file.customField.action = merged.action;
|
|
154
|
-
file.customField.preservedRepositoryEntries = merged.preservedRepositoryEntries ?? 0;
|
|
155
|
-
file.customField.name = report.customFieldName;
|
|
156
|
-
if (merged.status === 'unchanged') {
|
|
157
|
-
file.customField.status = 'unchanged';
|
|
158
|
-
} else {
|
|
159
|
-
step = 'custom-field-update';
|
|
160
|
-
await client.updateCardCustomFieldText(card.id, field.id, merged.text);
|
|
161
|
-
file.customField.status = 'written';
|
|
162
|
-
}
|
|
163
|
-
file.finalStatus = file.attachment.status === 'written' || file.customField.status === 'written' ? 'written' : 'unchanged';
|
|
164
|
-
} catch (error) {
|
|
165
|
-
const normalized = actionableError(error);
|
|
166
|
-
if (step === 'attachment-create') file.attachment.status = 'failed';
|
|
167
|
-
else if (step.startsWith('custom-field')) file.customField.status = 'failed';
|
|
168
|
-
file.finalStatus = 'api-failed';
|
|
169
|
-
file.errors.push(normalized);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const counts = { written: 0, unchanged: 0, blocked: 0, conflict: 0, apiFailed: 0 };
|
|
174
|
-
for (const file of report.files) {
|
|
175
|
-
if (file.finalStatus === 'written') counts.written += 1;
|
|
176
|
-
else if (file.finalStatus === 'unchanged') counts.unchanged += 1;
|
|
177
|
-
else if (file.finalStatus === 'blocked') counts.blocked += 1;
|
|
178
|
-
else if (file.finalStatus === 'conflict') counts.conflict += 1;
|
|
179
|
-
else if (file.finalStatus === 'api-failed') counts.apiFailed += 1;
|
|
180
|
-
}
|
|
181
|
-
const warningCount = report.warnings.length + report.files.reduce((sum, file) => sum + file.warnings.length, 0);
|
|
182
|
-
const errorCount = report.errors.length + report.files.reduce((sum, file) => sum + file.errors.length, 0);
|
|
183
|
-
report.totals = { matchedFiles: report.files.length, ...counts, warnings: warningCount, errors: errorCount };
|
|
184
|
-
report.ok = counts.blocked === 0 && counts.conflict === 0 && counts.apiFailed === 0 && errorCount === 0;
|
|
185
|
-
return { report, exitCode: report.ok ? 0 : 1 };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function runTrelloSync(argv = [], options = {}) {
|
|
189
|
-
if (argv[0] !== 'sync') return { exitCode: 2, stderr: `Unknown Trello command: ${argv[0] ?? ''}`.trim() };
|
|
190
|
-
const args = argv.slice(1);
|
|
191
|
-
let root = '.';
|
|
192
|
-
let dryRun = false;
|
|
193
|
-
for (const arg of args) {
|
|
194
|
-
if (arg === '--dry-run') dryRun = true;
|
|
195
|
-
else if (arg.startsWith('-')) return { exitCode: 2, stderr: `Unknown option: ${arg}` };
|
|
196
|
-
else root = arg;
|
|
197
|
-
}
|
|
198
|
-
const resolvedRoot = resolve(root);
|
|
199
|
-
if (!existsSync(resolvedRoot) || !statSync(resolvedRoot).isDirectory()) return { exitCode: 2, stderr: `Project root not found: ${resolvedRoot}` };
|
|
200
|
-
if (dryRun) {
|
|
201
|
-
const { report, exitCode } = planTrelloDryRun(resolvedRoot, options);
|
|
202
|
-
return { exitCode, stdout: formatTrelloDryRunReport(report), report };
|
|
203
|
-
}
|
|
204
|
-
return runTrelloWrite(resolvedRoot, options).then((result) => ({ ...result, stdout: result.stdout ?? (result.report ? formatTrelloWriteReport(result.report) : '') }));
|
|
205
|
-
}
|