@spekn/cli 1.0.0
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/dist/__tests__/export-cli.test.d.ts +1 -0
- package/dist/__tests__/export-cli.test.js +70 -0
- package/dist/__tests__/tui-args-policy.test.d.ts +1 -0
- package/dist/__tests__/tui-args-policy.test.js +50 -0
- package/dist/acp-S2MHZOAD.mjs +23 -0
- package/dist/acp-UCCI44JY.mjs +25 -0
- package/dist/auth/credentials-store.d.ts +2 -0
- package/dist/auth/credentials-store.js +5 -0
- package/dist/auth/device-flow.d.ts +36 -0
- package/dist/auth/device-flow.js +189 -0
- package/dist/auth/jwt.d.ts +1 -0
- package/dist/auth/jwt.js +6 -0
- package/dist/auth/session.d.ts +67 -0
- package/dist/auth/session.js +86 -0
- package/dist/auth-login.d.ts +34 -0
- package/dist/auth-login.js +202 -0
- package/dist/auth-logout.d.ts +25 -0
- package/dist/auth-logout.js +115 -0
- package/dist/auth-status.d.ts +24 -0
- package/dist/auth-status.js +109 -0
- package/dist/backlog-generate.d.ts +11 -0
- package/dist/backlog-generate.js +308 -0
- package/dist/backlog-health.d.ts +11 -0
- package/dist/backlog-health.js +287 -0
- package/dist/bridge-login.d.ts +40 -0
- package/dist/bridge-login.js +277 -0
- package/dist/chunk-3PAYRI4G.mjs +2428 -0
- package/dist/chunk-M4CS3A25.mjs +2426 -0
- package/dist/commands/auth/login.d.ts +30 -0
- package/dist/commands/auth/login.js +164 -0
- package/dist/commands/auth/logout.d.ts +25 -0
- package/dist/commands/auth/logout.js +115 -0
- package/dist/commands/auth/status.d.ts +24 -0
- package/dist/commands/auth/status.js +109 -0
- package/dist/commands/backlog/generate.d.ts +11 -0
- package/dist/commands/backlog/generate.js +308 -0
- package/dist/commands/backlog/health.d.ts +11 -0
- package/dist/commands/backlog/health.js +287 -0
- package/dist/commands/bridge/login.d.ts +36 -0
- package/dist/commands/bridge/login.js +258 -0
- package/dist/commands/export.d.ts +35 -0
- package/dist/commands/export.js +485 -0
- package/dist/commands/marketplace-export.d.ts +21 -0
- package/dist/commands/marketplace-export.js +214 -0
- package/dist/commands/project-clean.d.ts +1 -0
- package/dist/commands/project-clean.js +126 -0
- package/dist/commands/repo/common.d.ts +105 -0
- package/dist/commands/repo/common.js +775 -0
- package/dist/commands/repo/detach.d.ts +2 -0
- package/dist/commands/repo/detach.js +120 -0
- package/dist/commands/repo/register.d.ts +21 -0
- package/dist/commands/repo/register.js +175 -0
- package/dist/commands/repo/sync.d.ts +22 -0
- package/dist/commands/repo/sync.js +873 -0
- package/dist/commands/skills-import-local.d.ts +16 -0
- package/dist/commands/skills-import-local.js +352 -0
- package/dist/commands/spec/drift-check.d.ts +3 -0
- package/dist/commands/spec/drift-check.js +186 -0
- package/dist/commands/spec/frontmatter.d.ts +11 -0
- package/dist/commands/spec/frontmatter.js +219 -0
- package/dist/commands/spec/lint.d.ts +11 -0
- package/dist/commands/spec/lint.js +499 -0
- package/dist/commands/spec/parse.d.ts +11 -0
- package/dist/commands/spec/parse.js +162 -0
- package/dist/export.d.ts +35 -0
- package/dist/export.js +485 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +115280 -0
- package/dist/marketplace-export.d.ts +21 -0
- package/dist/marketplace-export.js +214 -0
- package/dist/project-clean.d.ts +1 -0
- package/dist/project-clean.js +126 -0
- package/dist/project-context.d.ts +99 -0
- package/dist/project-context.js +376 -0
- package/dist/repo-common.d.ts +101 -0
- package/dist/repo-common.js +671 -0
- package/dist/repo-detach.d.ts +2 -0
- package/dist/repo-detach.js +102 -0
- package/dist/repo-ingest.d.ts +29 -0
- package/dist/repo-ingest.js +305 -0
- package/dist/repo-register.d.ts +21 -0
- package/dist/repo-register.js +175 -0
- package/dist/repo-sync.d.ts +16 -0
- package/dist/repo-sync.js +152 -0
- package/dist/resources/prompt-loader.d.ts +1 -0
- package/dist/resources/prompt-loader.js +62 -0
- package/dist/resources/prompts/README.md +21 -0
- package/dist/resources/prompts/prompts/repo-analysis.prompt.md +126 -0
- package/dist/resources/prompts/repo-analysis.prompt.md +151 -0
- package/dist/resources/prompts/repo-sync-analysis.prompt.md +85 -0
- package/dist/skills-import-local.d.ts +16 -0
- package/dist/skills-import-local.js +352 -0
- package/dist/spec-drift-check.d.ts +3 -0
- package/dist/spec-drift-check.js +186 -0
- package/dist/spec-frontmatter.d.ts +11 -0
- package/dist/spec-frontmatter.js +219 -0
- package/dist/spec-lint.d.ts +11 -0
- package/dist/spec-lint.js +499 -0
- package/dist/spec-parse.d.ts +11 -0
- package/dist/spec-parse.js +162 -0
- package/dist/stubs/dotenv.d.ts +5 -0
- package/dist/stubs/dotenv.js +6 -0
- package/dist/stubs/typeorm.d.ts +22 -0
- package/dist/stubs/typeorm.js +28 -0
- package/dist/tui/app.d.ts +7 -0
- package/dist/tui/app.js +122 -0
- package/dist/tui/args.d.ts +8 -0
- package/dist/tui/args.js +57 -0
- package/dist/tui/capabilities/policy.d.ts +7 -0
- package/dist/tui/capabilities/policy.js +64 -0
- package/dist/tui/components/frame.d.ts +8 -0
- package/dist/tui/components/frame.js +8 -0
- package/dist/tui/components/status-bar.d.ts +8 -0
- package/dist/tui/components/status-bar.js +8 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.js +23 -0
- package/dist/tui/index.mjs +7563 -0
- package/dist/tui/keymap/use-global-keymap.d.ts +19 -0
- package/dist/tui/keymap/use-global-keymap.js +82 -0
- package/dist/tui/navigation/nav-items.d.ts +3 -0
- package/dist/tui/navigation/nav-items.js +18 -0
- package/dist/tui/screens/bridge.d.ts +8 -0
- package/dist/tui/screens/bridge.js +19 -0
- package/dist/tui/screens/decisions.d.ts +5 -0
- package/dist/tui/screens/decisions.js +28 -0
- package/dist/tui/screens/export.d.ts +5 -0
- package/dist/tui/screens/export.js +16 -0
- package/dist/tui/screens/home.d.ts +5 -0
- package/dist/tui/screens/home.js +33 -0
- package/dist/tui/screens/locked.d.ts +5 -0
- package/dist/tui/screens/locked.js +9 -0
- package/dist/tui/screens/specs.d.ts +5 -0
- package/dist/tui/screens/specs.js +31 -0
- package/dist/tui/services/client.d.ts +1 -0
- package/dist/tui/services/client.js +18 -0
- package/dist/tui/services/context-service.d.ts +19 -0
- package/dist/tui/services/context-service.js +246 -0
- package/dist/tui/shared-enums.d.ts +16 -0
- package/dist/tui/shared-enums.js +19 -0
- package/dist/tui/state/use-app-state.d.ts +35 -0
- package/dist/tui/state/use-app-state.js +177 -0
- package/dist/tui/types.d.ts +77 -0
- package/dist/tui/types.js +2 -0
- package/dist/tui-bundle.d.ts +1 -0
- package/dist/tui-bundle.js +5 -0
- package/dist/tui-entry.mjs +1407 -0
- package/dist/utils/cli-runtime.d.ts +5 -0
- package/dist/utils/cli-runtime.js +22 -0
- package/dist/utils/help-error.d.ts +7 -0
- package/dist/utils/help-error.js +14 -0
- package/dist/utils/interaction.d.ts +19 -0
- package/dist/utils/interaction.js +93 -0
- package/dist/utils/structured-log.d.ts +7 -0
- package/dist/utils/structured-log.js +112 -0
- package/dist/utils/trpc-url.d.ts +4 -0
- package/dist/utils/trpc-url.js +15 -0
- package/package.json +59 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* spec-lint CLI Tool
|
|
5
|
+
*
|
|
6
|
+
* Validate specification markdown quality and structure.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npm run spec-lint <spec-file-path>
|
|
9
|
+
* Example: npm run spec-lint specs/DECISIONS.md
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.main = main;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const agents_1 = require("@spekn/agents");
|
|
49
|
+
const shared_1 = require("@spekn/shared");
|
|
50
|
+
const cli_runtime_1 = require("../../utils/cli-runtime");
|
|
51
|
+
function parseArgs(args) {
|
|
52
|
+
const options = {};
|
|
53
|
+
let filePath;
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
const arg = args[i];
|
|
56
|
+
if (arg === "--verbose" || arg === "-v") {
|
|
57
|
+
options.verbose = true;
|
|
58
|
+
}
|
|
59
|
+
else if (arg === "--fix" || arg === "-f") {
|
|
60
|
+
options.fix = true;
|
|
61
|
+
}
|
|
62
|
+
else if (!arg.startsWith("-")) {
|
|
63
|
+
filePath = arg;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!filePath) {
|
|
67
|
+
throw new Error("missing required argument: <spec-file-path>");
|
|
68
|
+
}
|
|
69
|
+
return { filePath, options };
|
|
70
|
+
}
|
|
71
|
+
function printHelp() {
|
|
72
|
+
console.log(`
|
|
73
|
+
spec-lint - Validate specification markdown quality
|
|
74
|
+
|
|
75
|
+
USAGE:
|
|
76
|
+
npm run spec-lint <spec-file-path> [OPTIONS]
|
|
77
|
+
|
|
78
|
+
ARGUMENTS:
|
|
79
|
+
<spec-file-path> Path to the specification markdown file
|
|
80
|
+
|
|
81
|
+
OPTIONS:
|
|
82
|
+
-v, --verbose Show detailed output and suggestions
|
|
83
|
+
-f, --fix Auto-fix deterministic anchor formatting issues
|
|
84
|
+
-h, --help Show this help message
|
|
85
|
+
|
|
86
|
+
LINTING RULES:
|
|
87
|
+
frontmatter-required Specification has valid YAML frontmatter
|
|
88
|
+
anchor-naming Anchors follow naming convention (lowercase, dots)
|
|
89
|
+
anchor-duplicates No duplicate anchors
|
|
90
|
+
anchor-acceptance All anchors have acceptance criteria
|
|
91
|
+
dependency-valid Dependencies reference valid anchors
|
|
92
|
+
markdown-quality Basic markdown quality checks
|
|
93
|
+
|
|
94
|
+
EXAMPLES:
|
|
95
|
+
npm run spec-lint specs/SPECIFICATION.md
|
|
96
|
+
npm run spec-lint specs/DECISIONS.md --verbose
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
function escapeRegExp(value) {
|
|
100
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
101
|
+
}
|
|
102
|
+
function normalizeAnchorForFix(anchor) {
|
|
103
|
+
return anchor
|
|
104
|
+
.trim()
|
|
105
|
+
.toLowerCase()
|
|
106
|
+
.replace(/\s+/g, "-")
|
|
107
|
+
.replace(/[^a-z0-9._-]/g, "-")
|
|
108
|
+
.replace(/\.\./g, ".")
|
|
109
|
+
.replace(/^\./, "")
|
|
110
|
+
.replace(/\.$/, "");
|
|
111
|
+
}
|
|
112
|
+
function applyDeterministicFixes(content) {
|
|
113
|
+
const anchors = (0, agents_1.extractAnchors)(content);
|
|
114
|
+
const uniqueAnchors = Array.from(new Set(anchors.map((a) => a.anchor)));
|
|
115
|
+
let nextContent = content;
|
|
116
|
+
let fixesApplied = 0;
|
|
117
|
+
const replaceWithCount = (input, pattern, replacement) => {
|
|
118
|
+
let count = 0;
|
|
119
|
+
const output = input.replace(pattern, () => {
|
|
120
|
+
count += 1;
|
|
121
|
+
return replacement;
|
|
122
|
+
});
|
|
123
|
+
return { output, count };
|
|
124
|
+
};
|
|
125
|
+
for (const anchor of uniqueAnchors) {
|
|
126
|
+
const normalized = normalizeAnchorForFix(anchor);
|
|
127
|
+
if (!normalized || normalized === anchor) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const escaped = escapeRegExp(anchor);
|
|
131
|
+
const bracket = replaceWithCount(nextContent, new RegExp(`\\[${escaped}\\]`, "g"), `[${normalized}]`);
|
|
132
|
+
nextContent = bracket.output;
|
|
133
|
+
fixesApplied += bracket.count;
|
|
134
|
+
const prefixed = replaceWithCount(nextContent, new RegExp(`spec:${escaped}\\b`, "gi"), `spec:${normalized}`);
|
|
135
|
+
nextContent = prefixed.output;
|
|
136
|
+
fixesApplied += prefixed.count;
|
|
137
|
+
const hashDeps = replaceWithCount(nextContent, new RegExp(`#${escaped}\\b`, "g"), `#${normalized}`);
|
|
138
|
+
nextContent = hashDeps.output;
|
|
139
|
+
fixesApplied += hashDeps.count;
|
|
140
|
+
}
|
|
141
|
+
return { content: nextContent, fixesApplied };
|
|
142
|
+
}
|
|
143
|
+
function checkAnchorNaming(anchors) {
|
|
144
|
+
const issues = [];
|
|
145
|
+
anchors.forEach((anchor) => {
|
|
146
|
+
// Check lowercase
|
|
147
|
+
if (anchor.anchor !== anchor.anchor.toLowerCase()) {
|
|
148
|
+
issues.push({
|
|
149
|
+
severity: "error",
|
|
150
|
+
rule: "anchor-naming",
|
|
151
|
+
message: "Anchor should be lowercase",
|
|
152
|
+
anchor: anchor.anchor,
|
|
153
|
+
line: anchor.lineNumber,
|
|
154
|
+
suggestion: anchor.anchor.toLowerCase(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Check valid characters (only lowercase letters, numbers, dots, hyphens)
|
|
158
|
+
if (!anchor.anchor.match(/^[a-z0-9._-]+$/)) {
|
|
159
|
+
issues.push({
|
|
160
|
+
severity: "error",
|
|
161
|
+
rule: "anchor-naming",
|
|
162
|
+
message: "Anchor contains invalid characters (only a-z, 0-9, ., -, _ allowed)",
|
|
163
|
+
anchor: anchor.anchor,
|
|
164
|
+
line: anchor.lineNumber,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Check for double dots or leading/trailing dots
|
|
168
|
+
if (anchor.anchor.match(/\.\./)) {
|
|
169
|
+
issues.push({
|
|
170
|
+
severity: "warning",
|
|
171
|
+
rule: "anchor-naming",
|
|
172
|
+
message: "Anchor contains consecutive dots",
|
|
173
|
+
anchor: anchor.anchor,
|
|
174
|
+
line: anchor.lineNumber,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (anchor.anchor.startsWith(".") || anchor.anchor.endsWith(".")) {
|
|
178
|
+
issues.push({
|
|
179
|
+
severity: "warning",
|
|
180
|
+
rule: "anchor-naming",
|
|
181
|
+
message: "Anchor should not start or end with a dot",
|
|
182
|
+
anchor: anchor.anchor,
|
|
183
|
+
line: anchor.lineNumber,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Recommend dot notation for hierarchical anchors
|
|
187
|
+
if (anchor.anchor.includes("_") && !anchor.anchor.includes(".")) {
|
|
188
|
+
issues.push({
|
|
189
|
+
severity: "info",
|
|
190
|
+
rule: "anchor-naming",
|
|
191
|
+
message: "Consider using dot notation for hierarchical structure",
|
|
192
|
+
anchor: anchor.anchor,
|
|
193
|
+
line: anchor.lineNumber,
|
|
194
|
+
suggestion: anchor.anchor.replace(/_/g, "."),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return issues;
|
|
199
|
+
}
|
|
200
|
+
function checkDuplicateAnchors(anchors) {
|
|
201
|
+
const issues = [];
|
|
202
|
+
const anchorMap = new Map();
|
|
203
|
+
anchors.forEach((anchor) => {
|
|
204
|
+
const normalized = anchor.anchor.toLowerCase();
|
|
205
|
+
if (!anchorMap.has(normalized)) {
|
|
206
|
+
anchorMap.set(normalized, []);
|
|
207
|
+
}
|
|
208
|
+
anchorMap.get(normalized).push(anchor.lineNumber);
|
|
209
|
+
});
|
|
210
|
+
anchorMap.forEach((lines, anchor) => {
|
|
211
|
+
if (lines.length > 1) {
|
|
212
|
+
issues.push({
|
|
213
|
+
severity: "error",
|
|
214
|
+
rule: "anchor-duplicates",
|
|
215
|
+
message: `Duplicate anchor found on lines: ${lines.join(", ")}`,
|
|
216
|
+
anchor,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return issues;
|
|
221
|
+
}
|
|
222
|
+
function checkAcceptanceCriteria(content, anchors) {
|
|
223
|
+
const issues = [];
|
|
224
|
+
anchors.forEach((anchor) => {
|
|
225
|
+
const anchorContent = (0, agents_1.extractAnchorContent)(anchor.anchor, content);
|
|
226
|
+
if (!anchorContent)
|
|
227
|
+
return;
|
|
228
|
+
// Check for acceptance criteria section
|
|
229
|
+
const hasAcceptanceCriteria = anchorContent.match(/\*\*Acceptance Criteria\*\*:|##\s*Acceptance Criteria|###\s*Acceptance Criteria/i);
|
|
230
|
+
if (!hasAcceptanceCriteria) {
|
|
231
|
+
issues.push({
|
|
232
|
+
severity: "warning",
|
|
233
|
+
rule: "anchor-acceptance",
|
|
234
|
+
message: "Anchor lacks explicit acceptance criteria section",
|
|
235
|
+
anchor: anchor.anchor,
|
|
236
|
+
line: anchor.lineNumber,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return issues;
|
|
241
|
+
}
|
|
242
|
+
function checkDependencyValidity(content, anchors) {
|
|
243
|
+
const issues = [];
|
|
244
|
+
const validAnchors = (0, agents_1.getValidAnchors)(content);
|
|
245
|
+
anchors.forEach((anchor) => {
|
|
246
|
+
const anchorContent = (0, agents_1.extractAnchorContent)(anchor.anchor, content);
|
|
247
|
+
if (!anchorContent)
|
|
248
|
+
return;
|
|
249
|
+
// Look for dependency references
|
|
250
|
+
const dependencyMatch = anchorContent.match(/\*\*Dependencies\*\*:([^\n]+)/i);
|
|
251
|
+
if (dependencyMatch) {
|
|
252
|
+
const dependencyText = dependencyMatch[1];
|
|
253
|
+
const dependencyAnchors = dependencyText.match(/#[a-z0-9._-]+/gi);
|
|
254
|
+
if (dependencyAnchors) {
|
|
255
|
+
dependencyAnchors.forEach((dep) => {
|
|
256
|
+
const depNormalized = dep.replace(/^#/, "").toLowerCase();
|
|
257
|
+
const isValid = validAnchors.some((valid) => valid.toLowerCase() === depNormalized);
|
|
258
|
+
if (!isValid) {
|
|
259
|
+
issues.push({
|
|
260
|
+
severity: "error",
|
|
261
|
+
rule: "dependency-valid",
|
|
262
|
+
message: `Dependency references non-existent anchor: ${dep}`,
|
|
263
|
+
anchor: anchor.anchor,
|
|
264
|
+
line: anchor.lineNumber,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return issues;
|
|
272
|
+
}
|
|
273
|
+
function checkMarkdownQuality(content) {
|
|
274
|
+
const issues = [];
|
|
275
|
+
const lines = content.split("\n");
|
|
276
|
+
lines.forEach((line, index) => {
|
|
277
|
+
const lineNumber = index + 1;
|
|
278
|
+
// Check for trailing whitespace
|
|
279
|
+
if (line.endsWith(" ") || line.endsWith("\t")) {
|
|
280
|
+
issues.push({
|
|
281
|
+
severity: "info",
|
|
282
|
+
rule: "markdown-quality",
|
|
283
|
+
message: "Line has trailing whitespace",
|
|
284
|
+
line: lineNumber,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// Check for inconsistent heading markers
|
|
288
|
+
const headingMatch = line.match(/^(#{1,6})\s/);
|
|
289
|
+
if (headingMatch && line.includes("#", headingMatch[1].length + 1)) {
|
|
290
|
+
const afterHeading = line.substring(headingMatch[0].length);
|
|
291
|
+
if (!afterHeading.startsWith("#")) {
|
|
292
|
+
// Check if there's a # not at the start (could be an anchor)
|
|
293
|
+
if (afterHeading.includes("#") &&
|
|
294
|
+
!afterHeading.match(/^#[a-z0-9._-]+/i)) {
|
|
295
|
+
issues.push({
|
|
296
|
+
severity: "warning",
|
|
297
|
+
rule: "markdown-quality",
|
|
298
|
+
message: "Unexpected # character in heading",
|
|
299
|
+
line: lineNumber,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
return issues;
|
|
306
|
+
}
|
|
307
|
+
function checkFrontmatter(content) {
|
|
308
|
+
const issues = [];
|
|
309
|
+
const result = (0, shared_1.parseSpecificationFrontmatter)(content);
|
|
310
|
+
if (!result.hasFrontmatter) {
|
|
311
|
+
issues.push({
|
|
312
|
+
severity: "error",
|
|
313
|
+
rule: "frontmatter-required",
|
|
314
|
+
message: "Specification is missing required YAML frontmatter (delimited by --- markers)",
|
|
315
|
+
});
|
|
316
|
+
return issues;
|
|
317
|
+
}
|
|
318
|
+
if (result.errors?.length) {
|
|
319
|
+
for (const error of result.errors) {
|
|
320
|
+
issues.push({
|
|
321
|
+
severity: "error",
|
|
322
|
+
rule: "frontmatter-required",
|
|
323
|
+
message: `Frontmatter validation error: ${error}`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return issues;
|
|
327
|
+
}
|
|
328
|
+
const fm = result.frontmatter;
|
|
329
|
+
if (!fm)
|
|
330
|
+
return issues;
|
|
331
|
+
// Check hint lengths
|
|
332
|
+
if (fm.hints) {
|
|
333
|
+
for (const [layer, hints] of Object.entries(fm.hints)) {
|
|
334
|
+
if (!hints)
|
|
335
|
+
continue;
|
|
336
|
+
for (let i = 0; i < hints.length; i++) {
|
|
337
|
+
if (hints[i].length > shared_1.HINT_MAX_LENGTH) {
|
|
338
|
+
issues.push({
|
|
339
|
+
severity: "error",
|
|
340
|
+
rule: "frontmatter-required",
|
|
341
|
+
message: `Hint hints.${layer}[${i}] exceeds ${shared_1.HINT_MAX_LENGTH} character limit (${hints[i].length} chars)`,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Check type-specific properties match declared type
|
|
348
|
+
const typeSpecific = ["capability", "architectural", "workflow", "operational"];
|
|
349
|
+
const present = typeSpecific.filter((t) => fm[t]);
|
|
350
|
+
if (present.length > 1) {
|
|
351
|
+
issues.push({
|
|
352
|
+
severity: "error",
|
|
353
|
+
rule: "frontmatter-required",
|
|
354
|
+
message: `Multiple type-specific properties found (${present.join(", ")}). Only one is allowed.`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (present.length === 1 && present[0] !== fm.type) {
|
|
358
|
+
issues.push({
|
|
359
|
+
severity: "warning",
|
|
360
|
+
rule: "frontmatter-required",
|
|
361
|
+
message: `Type-specific property "${present[0]}" does not match declared type "${fm.type}"`,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return issues;
|
|
365
|
+
}
|
|
366
|
+
function main(argv) {
|
|
367
|
+
const args = argv ?? process.argv.slice(2);
|
|
368
|
+
if ((0, cli_runtime_1.hasHelpFlag)(args)) {
|
|
369
|
+
printHelp();
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
let filePath;
|
|
373
|
+
let options;
|
|
374
|
+
try {
|
|
375
|
+
({ filePath, options } = parseArgs(args));
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
console.error("Error: Missing required argument <spec-file-path>");
|
|
379
|
+
printHelp();
|
|
380
|
+
return 1;
|
|
381
|
+
}
|
|
382
|
+
// Resolve file path
|
|
383
|
+
const resolvedPath = path.resolve(process.cwd(), filePath);
|
|
384
|
+
// Check if file exists
|
|
385
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
386
|
+
console.error(`Error: File not found: ${resolvedPath}`);
|
|
387
|
+
return 1;
|
|
388
|
+
}
|
|
389
|
+
// Read file content
|
|
390
|
+
let content;
|
|
391
|
+
try {
|
|
392
|
+
content = fs.readFileSync(resolvedPath, "utf-8");
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
console.error(`Error reading file: ${(0, cli_runtime_1.errorMessage)(error)}`);
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
let fixSummary = null;
|
|
399
|
+
if (options.fix) {
|
|
400
|
+
const fixed = applyDeterministicFixes(content);
|
|
401
|
+
const fileModified = fixed.content !== content;
|
|
402
|
+
if (fileModified) {
|
|
403
|
+
fs.writeFileSync(resolvedPath, fixed.content, "utf-8");
|
|
404
|
+
content = fixed.content;
|
|
405
|
+
}
|
|
406
|
+
fixSummary = {
|
|
407
|
+
attempted: true,
|
|
408
|
+
fixesApplied: fixed.fixesApplied,
|
|
409
|
+
fileModified,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
// Extract anchors
|
|
413
|
+
const anchors = (0, agents_1.extractAnchors)(content);
|
|
414
|
+
console.log(`\nLinting ${path.basename(filePath)}...`);
|
|
415
|
+
console.log(`Found ${anchors.length} anchor(s)\n`);
|
|
416
|
+
// Run all lint checks
|
|
417
|
+
const issues = [
|
|
418
|
+
...checkFrontmatter(content),
|
|
419
|
+
...checkAnchorNaming(anchors),
|
|
420
|
+
...checkDuplicateAnchors(anchors),
|
|
421
|
+
...checkAcceptanceCriteria(content, anchors),
|
|
422
|
+
...checkDependencyValidity(content, anchors),
|
|
423
|
+
...checkMarkdownQuality(content),
|
|
424
|
+
];
|
|
425
|
+
// Group issues by severity
|
|
426
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
427
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
428
|
+
const infos = issues.filter((i) => i.severity === "info");
|
|
429
|
+
// Display results
|
|
430
|
+
if (issues.length === 0) {
|
|
431
|
+
console.log("✓ No issues found. Specification is clean!\n");
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
if (errors.length > 0) {
|
|
435
|
+
console.log(`\n❌ Errors (${errors.length}):\n`);
|
|
436
|
+
errors.forEach((issue, index) => {
|
|
437
|
+
console.log(`${index + 1}. [${issue.rule}] ${issue.message}`);
|
|
438
|
+
if (issue.anchor)
|
|
439
|
+
console.log(` Anchor: ${issue.anchor}`);
|
|
440
|
+
if (issue.line)
|
|
441
|
+
console.log(` Line: ${issue.line}`);
|
|
442
|
+
if (issue.suggestion && options.verbose)
|
|
443
|
+
console.log(` Suggestion: ${issue.suggestion}`);
|
|
444
|
+
console.log();
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
if (warnings.length > 0) {
|
|
448
|
+
console.log(`\n⚠️ Warnings (${warnings.length}):\n`);
|
|
449
|
+
warnings.forEach((issue, index) => {
|
|
450
|
+
console.log(`${index + 1}. [${issue.rule}] ${issue.message}`);
|
|
451
|
+
if (issue.anchor)
|
|
452
|
+
console.log(` Anchor: ${issue.anchor}`);
|
|
453
|
+
if (issue.line)
|
|
454
|
+
console.log(` Line: ${issue.line}`);
|
|
455
|
+
if (issue.suggestion && options.verbose)
|
|
456
|
+
console.log(` Suggestion: ${issue.suggestion}`);
|
|
457
|
+
console.log();
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (infos.length > 0 && options.verbose) {
|
|
461
|
+
console.log(`\nℹ️ Info (${infos.length}):\n`);
|
|
462
|
+
infos.forEach((issue, index) => {
|
|
463
|
+
console.log(`${index + 1}. [${issue.rule}] ${issue.message}`);
|
|
464
|
+
if (issue.anchor)
|
|
465
|
+
console.log(` Anchor: ${issue.anchor}`);
|
|
466
|
+
if (issue.line)
|
|
467
|
+
console.log(` Line: ${issue.line}`);
|
|
468
|
+
if (issue.suggestion)
|
|
469
|
+
console.log(` Suggestion: ${issue.suggestion}`);
|
|
470
|
+
console.log();
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
// Summary
|
|
474
|
+
console.log("Summary:");
|
|
475
|
+
console.log(` Errors: ${errors.length}`);
|
|
476
|
+
console.log(` Warnings: ${warnings.length}`);
|
|
477
|
+
console.log(` Info: ${infos.length}`);
|
|
478
|
+
if (fixSummary?.attempted) {
|
|
479
|
+
console.log(` Fixes applied: ${fixSummary.fixesApplied}`);
|
|
480
|
+
console.log(` File modified: ${fixSummary.fileModified ? "yes" : "no"}`);
|
|
481
|
+
}
|
|
482
|
+
console.log();
|
|
483
|
+
// Exit with error code if there are errors
|
|
484
|
+
if (errors.length > 0) {
|
|
485
|
+
console.log("❌ Linting failed due to errors.\n");
|
|
486
|
+
return 1;
|
|
487
|
+
}
|
|
488
|
+
else if (warnings.length > 0) {
|
|
489
|
+
console.log("⚠️ Linting passed with warnings.\n");
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
console.log("✓ Linting passed.\n");
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (require.main === module) {
|
|
498
|
+
void (0, cli_runtime_1.runCliMain)(main, { errorPrefix: "spec-lint failed" });
|
|
499
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* spec-parse CLI Tool
|
|
4
|
+
*
|
|
5
|
+
* Parse specification markdown and extract all valid anchors.
|
|
6
|
+
*
|
|
7
|
+
* Usage: npm run spec-parse <spec-file-path>
|
|
8
|
+
* Example: npm run spec-parse specs/SPECIFICATION.md
|
|
9
|
+
*/
|
|
10
|
+
declare function main(argv?: string[]): number;
|
|
11
|
+
export { main };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* spec-parse CLI Tool
|
|
5
|
+
*
|
|
6
|
+
* Parse specification markdown and extract all valid anchors.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npm run spec-parse <spec-file-path>
|
|
9
|
+
* Example: npm run spec-parse specs/SPECIFICATION.md
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.main = main;
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const agents_1 = require("@spekn/agents");
|
|
49
|
+
const cli_runtime_1 = require("../../utils/cli-runtime");
|
|
50
|
+
function parseArgs(args) {
|
|
51
|
+
const options = {};
|
|
52
|
+
let filePath;
|
|
53
|
+
for (let i = 0; i < args.length; i++) {
|
|
54
|
+
const arg = args[i];
|
|
55
|
+
if (arg === "--verbose" || arg === "-v") {
|
|
56
|
+
options.verbose = true;
|
|
57
|
+
}
|
|
58
|
+
else if (arg === "--json" || arg === "-j") {
|
|
59
|
+
options.json = true;
|
|
60
|
+
}
|
|
61
|
+
else if (!arg.startsWith("-")) {
|
|
62
|
+
filePath = arg;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!filePath) {
|
|
66
|
+
throw new Error("missing required argument: <spec-file-path>");
|
|
67
|
+
}
|
|
68
|
+
return { filePath, options };
|
|
69
|
+
}
|
|
70
|
+
function printHelp() {
|
|
71
|
+
console.log(`
|
|
72
|
+
spec-parse - Parse specification markdown and extract anchors
|
|
73
|
+
|
|
74
|
+
USAGE:
|
|
75
|
+
npm run spec-parse <spec-file-path> [OPTIONS]
|
|
76
|
+
|
|
77
|
+
ARGUMENTS:
|
|
78
|
+
<spec-file-path> Path to the specification markdown file
|
|
79
|
+
|
|
80
|
+
OPTIONS:
|
|
81
|
+
-v, --verbose Show detailed output
|
|
82
|
+
-j, --json Output in JSON format
|
|
83
|
+
-h, --help Show this help message
|
|
84
|
+
|
|
85
|
+
EXAMPLES:
|
|
86
|
+
npm run spec-parse specs/SPECIFICATION.md
|
|
87
|
+
npm run spec-parse specs/WORKFLOW.md --json
|
|
88
|
+
npm run spec-parse specs/DECISIONS.md --verbose
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
function main(argv) {
|
|
92
|
+
const args = argv ?? process.argv.slice(2);
|
|
93
|
+
if ((0, cli_runtime_1.hasHelpFlag)(args)) {
|
|
94
|
+
printHelp();
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
let filePath;
|
|
98
|
+
let options;
|
|
99
|
+
try {
|
|
100
|
+
({ filePath, options } = parseArgs(args));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
console.error("Error: Missing required argument <spec-file-path>");
|
|
104
|
+
printHelp();
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
// Resolve file path
|
|
108
|
+
const resolvedPath = path.resolve(process.cwd(), filePath);
|
|
109
|
+
// Check if file exists
|
|
110
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
111
|
+
console.error(`Error: File not found: ${resolvedPath}`);
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
// Read file content
|
|
115
|
+
let content;
|
|
116
|
+
try {
|
|
117
|
+
content = fs.readFileSync(resolvedPath, "utf-8");
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error(`Error reading file: ${(0, cli_runtime_1.errorMessage)(error)}`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
// Extract anchors
|
|
124
|
+
const anchors = (0, agents_1.extractAnchors)(content);
|
|
125
|
+
if (anchors.length === 0) {
|
|
126
|
+
console.log("No anchors found in the specification.");
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
// Output results
|
|
130
|
+
if (options.json) {
|
|
131
|
+
console.log(JSON.stringify(anchors, null, 2));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(`\nFound ${anchors.length} anchor(s) in ${path.basename(filePath)}:\n`);
|
|
135
|
+
// Create table data
|
|
136
|
+
const tableData = anchors.map((anchor) => ({
|
|
137
|
+
Anchor: anchor.anchor,
|
|
138
|
+
Level: "H" + anchor.level,
|
|
139
|
+
Line: anchor.lineNumber,
|
|
140
|
+
Heading: options.verbose
|
|
141
|
+
? anchor.headingText
|
|
142
|
+
: anchor.headingText.substring(0, 60) +
|
|
143
|
+
(anchor.headingText.length > 60 ? "..." : ""),
|
|
144
|
+
}));
|
|
145
|
+
// Print table
|
|
146
|
+
console.table(tableData);
|
|
147
|
+
if (options.verbose) {
|
|
148
|
+
console.log("\nAnchor Details:");
|
|
149
|
+
anchors.forEach((anchor, index) => {
|
|
150
|
+
console.log(`\n${index + 1}. ${anchor.anchor}`);
|
|
151
|
+
console.log(` Level: ${anchor.level}`);
|
|
152
|
+
console.log(` Line: ${anchor.lineNumber}`);
|
|
153
|
+
console.log(` Heading: ${anchor.headingText}`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
console.log(`\nTotal: ${anchors.length} anchor(s)\n`);
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
if (require.main === module) {
|
|
161
|
+
void (0, cli_runtime_1.runCliMain)(main, { errorPrefix: "spec-parse failed" });
|
|
162
|
+
}
|