@outcomeeng/spx 0.1.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/README.md +146 -0
- package/bin/spx.js +30 -0
- package/dist/.eslintcache +1 -0
- package/dist/.validation-timings.json +50 -0
- package/dist/chunk-5L7CHFBC.js +60 -0
- package/dist/chunk-5L7CHFBC.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4023 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4023 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LEAF_KIND,
|
|
3
|
+
parseWorkItemName
|
|
4
|
+
} from "./chunk-5L7CHFBC.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/commands/claude/init.ts
|
|
10
|
+
import { execa } from "execa";
|
|
11
|
+
async function initCommand(options = {}) {
|
|
12
|
+
const cwd = options.cwd || process.cwd();
|
|
13
|
+
try {
|
|
14
|
+
const { stdout: listOutput } = await execa(
|
|
15
|
+
"claude",
|
|
16
|
+
["plugin", "marketplace", "list"],
|
|
17
|
+
{ cwd }
|
|
18
|
+
);
|
|
19
|
+
const exists = listOutput.includes("spx-claude");
|
|
20
|
+
if (!exists) {
|
|
21
|
+
await execa(
|
|
22
|
+
"claude",
|
|
23
|
+
["plugin", "marketplace", "add", "simonheimlicher/spx-claude"],
|
|
24
|
+
{ cwd }
|
|
25
|
+
);
|
|
26
|
+
return "\u2713 spx-claude marketplace installed successfully\n\nRun 'claude plugin marketplace list' to view all marketplaces.";
|
|
27
|
+
} else {
|
|
28
|
+
await execa("claude", ["plugin", "marketplace", "update", "spx-claude"], {
|
|
29
|
+
cwd
|
|
30
|
+
});
|
|
31
|
+
return "\u2713 spx-claude marketplace updated successfully\n\nThe marketplace is now up to date.";
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof Error) {
|
|
35
|
+
if (error.message.includes("ENOENT") || error.message.includes("command not found")) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"Claude CLI not found. Please install Claude Code first.\n\nVisit: https://docs.anthropic.com/claude-code"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Failed to initialize marketplace: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/commands/claude/settings/consolidate.ts
|
|
47
|
+
import os2 from "os";
|
|
48
|
+
import path4 from "path";
|
|
49
|
+
|
|
50
|
+
// src/lib/claude/permissions/discovery.ts
|
|
51
|
+
import fs from "fs/promises";
|
|
52
|
+
import path from "path";
|
|
53
|
+
async function findSettingsFiles(root, visited = /* @__PURE__ */ new Set()) {
|
|
54
|
+
const normalizedRoot = path.resolve(root.replace(/^~/, process.env.HOME || "~"));
|
|
55
|
+
if (visited.has(normalizedRoot)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
visited.add(normalizedRoot);
|
|
59
|
+
try {
|
|
60
|
+
const stats = await fs.stat(normalizedRoot);
|
|
61
|
+
if (!stats.isDirectory()) {
|
|
62
|
+
throw new Error(`Path is not a directory: ${normalizedRoot}`);
|
|
63
|
+
}
|
|
64
|
+
const entries = await fs.readdir(normalizedRoot, { withFileTypes: true });
|
|
65
|
+
const results = [];
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const fullPath = path.join(normalizedRoot, entry.name);
|
|
68
|
+
if (entry.isDirectory() && entry.name === ".claude") {
|
|
69
|
+
const settingsPath = path.join(fullPath, "settings.local.json");
|
|
70
|
+
if (await isValidSettingsFile(settingsPath)) {
|
|
71
|
+
results.push(settingsPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (entry.isDirectory() && entry.name !== ".claude") {
|
|
75
|
+
const subFiles = await findSettingsFiles(fullPath, visited);
|
|
76
|
+
results.push(...subFiles);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof Error) {
|
|
82
|
+
if (error.message.includes("ENOENT")) {
|
|
83
|
+
throw new Error(`Directory not found: ${normalizedRoot}`);
|
|
84
|
+
}
|
|
85
|
+
if (error.message.includes("EACCES")) {
|
|
86
|
+
throw new Error(`Permission denied: ${normalizedRoot}`);
|
|
87
|
+
}
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Failed to search directory "${normalizedRoot}": ${error.message}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function isValidSettingsFile(filePath) {
|
|
96
|
+
try {
|
|
97
|
+
await fs.access(filePath, fs.constants.R_OK);
|
|
98
|
+
const stats = await fs.stat(filePath);
|
|
99
|
+
if (!stats.isFile()) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return path.extname(filePath) === ".json";
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/lib/claude/permissions/parser.ts
|
|
109
|
+
import fs2 from "fs/promises";
|
|
110
|
+
async function parseSettingsFile(filePath) {
|
|
111
|
+
try {
|
|
112
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
113
|
+
const parsed = JSON.parse(content);
|
|
114
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return parsed;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function parsePermission(raw, category) {
|
|
123
|
+
const match = raw.match(/^([^(]+)\((.+)\)$/);
|
|
124
|
+
if (!match) {
|
|
125
|
+
throw new Error(`Malformed permission string: "${raw}"`);
|
|
126
|
+
}
|
|
127
|
+
const [, type, scope] = match;
|
|
128
|
+
return {
|
|
129
|
+
raw,
|
|
130
|
+
type: type.trim(),
|
|
131
|
+
scope: scope.trim(),
|
|
132
|
+
category
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function parseAllSettings(filePaths) {
|
|
136
|
+
const results = [];
|
|
137
|
+
for (const filePath of filePaths) {
|
|
138
|
+
const settings = await parseSettingsFile(filePath);
|
|
139
|
+
if (settings?.permissions) {
|
|
140
|
+
results.push(settings.permissions);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/scanner/walk.ts
|
|
147
|
+
import fs3 from "fs/promises";
|
|
148
|
+
import path2 from "path";
|
|
149
|
+
async function walkDirectory(root, visited = /* @__PURE__ */ new Set()) {
|
|
150
|
+
const normalizedRoot = path2.resolve(root);
|
|
151
|
+
if (visited.has(normalizedRoot)) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
visited.add(normalizedRoot);
|
|
155
|
+
try {
|
|
156
|
+
const entries = await fs3.readdir(normalizedRoot, { withFileTypes: true });
|
|
157
|
+
const results = [];
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const fullPath = path2.join(normalizedRoot, entry.name);
|
|
160
|
+
if (entry.isDirectory()) {
|
|
161
|
+
results.push({
|
|
162
|
+
name: entry.name,
|
|
163
|
+
path: fullPath,
|
|
164
|
+
isDirectory: true
|
|
165
|
+
});
|
|
166
|
+
const subEntries = await walkDirectory(fullPath, visited);
|
|
167
|
+
results.push(...subEntries);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return results;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof Error) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Failed to walk directory "${normalizedRoot}": ${error.message}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function filterWorkItemDirectories(entries) {
|
|
181
|
+
return entries.filter((entry) => {
|
|
182
|
+
try {
|
|
183
|
+
parseWorkItemName(entry.name);
|
|
184
|
+
return true;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function buildWorkItemList(entries) {
|
|
191
|
+
return entries.map((entry) => ({
|
|
192
|
+
...parseWorkItemName(entry.name),
|
|
193
|
+
path: entry.path
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
function normalizePath(filepath) {
|
|
197
|
+
return filepath.replace(/\\/g, "/");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/lib/claude/permissions/subsumption.ts
|
|
201
|
+
function parseScopePattern(scope) {
|
|
202
|
+
if (scope.includes("file_path:") || scope.includes("directory_path:") || scope.includes("path:")) {
|
|
203
|
+
const colonIndex = scope.indexOf(":");
|
|
204
|
+
const pattern = colonIndex >= 0 ? scope.substring(colonIndex + 1) : scope;
|
|
205
|
+
return { type: "path", pattern };
|
|
206
|
+
}
|
|
207
|
+
return { type: "command", pattern: scope };
|
|
208
|
+
}
|
|
209
|
+
function subsumes(broader, narrower) {
|
|
210
|
+
if (broader.type !== narrower.type) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (broader.scope === narrower.scope) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const broaderScope = parseScopePattern(broader.scope);
|
|
217
|
+
const narrowerScope = parseScopePattern(narrower.scope);
|
|
218
|
+
if (broaderScope.type === "command" && narrowerScope.type === "command") {
|
|
219
|
+
const broaderBase = broaderScope.pattern.replace(/:?\*+$/, "");
|
|
220
|
+
const narrowerFull = narrowerScope.pattern.replace(/:?\*+$/, "");
|
|
221
|
+
if (narrowerFull.startsWith(broaderBase)) {
|
|
222
|
+
return narrowerFull.length > broaderBase.length;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (broaderScope.type === "path" && narrowerScope.type === "path") {
|
|
226
|
+
const broaderPath = normalizePath(broaderScope.pattern.replace(/\/?\*+$/, ""));
|
|
227
|
+
const narrowerPath = normalizePath(narrowerScope.pattern.replace(/\/?\*+$/, ""));
|
|
228
|
+
return narrowerPath.startsWith(broaderPath + "/");
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function detectSubsumptions(permissions) {
|
|
233
|
+
const results = [];
|
|
234
|
+
const processedBroader = /* @__PURE__ */ new Set();
|
|
235
|
+
for (let i = 0; i < permissions.length; i++) {
|
|
236
|
+
const broader = permissions[i];
|
|
237
|
+
if (processedBroader.has(broader.raw)) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const narrowerPerms = [];
|
|
241
|
+
for (let j = 0; j < permissions.length; j++) {
|
|
242
|
+
if (i === j) continue;
|
|
243
|
+
const narrower = permissions[j];
|
|
244
|
+
if (subsumes(broader, narrower)) {
|
|
245
|
+
narrowerPerms.push(narrower);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (narrowerPerms.length > 0) {
|
|
249
|
+
results.push({
|
|
250
|
+
broader,
|
|
251
|
+
narrower: narrowerPerms
|
|
252
|
+
});
|
|
253
|
+
processedBroader.add(broader.raw);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
258
|
+
function removeSubsumed(permissionStrings, category) {
|
|
259
|
+
const permissions = permissionStrings.map((raw) => {
|
|
260
|
+
try {
|
|
261
|
+
return parsePermission(raw, category);
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}).filter((p) => p !== null);
|
|
266
|
+
const subsumptions = detectSubsumptions(permissions);
|
|
267
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
268
|
+
for (const result of subsumptions) {
|
|
269
|
+
for (const narrower of result.narrower) {
|
|
270
|
+
toRemove.add(narrower.raw);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return permissionStrings.filter((perm) => !toRemove.has(perm));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/lib/claude/permissions/merger.ts
|
|
277
|
+
function mergePermissions(global, local) {
|
|
278
|
+
const originalGlobal = {
|
|
279
|
+
allow: new Set(global.allow || []),
|
|
280
|
+
deny: new Set(global.deny || []),
|
|
281
|
+
ask: new Set(global.ask || [])
|
|
282
|
+
};
|
|
283
|
+
const combined = {
|
|
284
|
+
allow: [...global.allow || []],
|
|
285
|
+
deny: [...global.deny || []],
|
|
286
|
+
ask: [...global.ask || []]
|
|
287
|
+
};
|
|
288
|
+
let filesProcessed = 0;
|
|
289
|
+
let filesSkipped = 0;
|
|
290
|
+
for (const localPerms of local) {
|
|
291
|
+
let hasPerms = false;
|
|
292
|
+
if (localPerms.allow && localPerms.allow.length > 0) {
|
|
293
|
+
combined.allow?.push(...localPerms.allow);
|
|
294
|
+
hasPerms = true;
|
|
295
|
+
}
|
|
296
|
+
if (localPerms.deny && localPerms.deny.length > 0) {
|
|
297
|
+
combined.deny?.push(...localPerms.deny);
|
|
298
|
+
hasPerms = true;
|
|
299
|
+
}
|
|
300
|
+
if (localPerms.ask && localPerms.ask.length > 0) {
|
|
301
|
+
combined.ask?.push(...localPerms.ask);
|
|
302
|
+
hasPerms = true;
|
|
303
|
+
}
|
|
304
|
+
if (hasPerms) {
|
|
305
|
+
filesProcessed++;
|
|
306
|
+
} else {
|
|
307
|
+
filesSkipped++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const allSubsumed = [];
|
|
311
|
+
const afterSubsumption = {};
|
|
312
|
+
if (combined.allow && combined.allow.length > 0) {
|
|
313
|
+
const before = new Set(combined.allow);
|
|
314
|
+
combined.allow = removeSubsumed(combined.allow, "allow");
|
|
315
|
+
const after = new Set(combined.allow);
|
|
316
|
+
for (const perm of before) {
|
|
317
|
+
if (!after.has(perm)) {
|
|
318
|
+
allSubsumed.push(perm);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (combined.deny && combined.deny.length > 0) {
|
|
323
|
+
const before = new Set(combined.deny);
|
|
324
|
+
combined.deny = removeSubsumed(combined.deny, "deny");
|
|
325
|
+
const after = new Set(combined.deny);
|
|
326
|
+
for (const perm of before) {
|
|
327
|
+
if (!after.has(perm)) {
|
|
328
|
+
allSubsumed.push(perm);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (combined.ask && combined.ask.length > 0) {
|
|
333
|
+
const before = new Set(combined.ask);
|
|
334
|
+
combined.ask = removeSubsumed(combined.ask, "ask");
|
|
335
|
+
const after = new Set(combined.ask);
|
|
336
|
+
for (const perm of before) {
|
|
337
|
+
if (!after.has(perm)) {
|
|
338
|
+
allSubsumed.push(perm);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
afterSubsumption.allow = combined.allow;
|
|
343
|
+
afterSubsumption.deny = combined.deny;
|
|
344
|
+
afterSubsumption.ask = combined.ask;
|
|
345
|
+
const {
|
|
346
|
+
resolved,
|
|
347
|
+
conflictCount,
|
|
348
|
+
subsumed: conflictSubsumed
|
|
349
|
+
} = resolveConflicts(afterSubsumption);
|
|
350
|
+
const subsumed = [...allSubsumed, ...conflictSubsumed];
|
|
351
|
+
const merged = {};
|
|
352
|
+
if (resolved.allow && resolved.allow.length > 0) {
|
|
353
|
+
merged.allow = Array.from(new Set(resolved.allow)).sort();
|
|
354
|
+
}
|
|
355
|
+
if (resolved.deny && resolved.deny.length > 0) {
|
|
356
|
+
merged.deny = Array.from(new Set(resolved.deny)).sort();
|
|
357
|
+
}
|
|
358
|
+
if (resolved.ask && resolved.ask.length > 0) {
|
|
359
|
+
merged.ask = Array.from(new Set(resolved.ask)).sort();
|
|
360
|
+
}
|
|
361
|
+
const added = {
|
|
362
|
+
allow: [],
|
|
363
|
+
deny: [],
|
|
364
|
+
ask: []
|
|
365
|
+
};
|
|
366
|
+
for (const perm of merged.allow || []) {
|
|
367
|
+
if (!originalGlobal.allow.has(perm)) {
|
|
368
|
+
added.allow.push(perm);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const perm of merged.deny || []) {
|
|
372
|
+
if (!originalGlobal.deny.has(perm)) {
|
|
373
|
+
added.deny.push(perm);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const perm of merged.ask || []) {
|
|
377
|
+
if (!originalGlobal.ask.has(perm)) {
|
|
378
|
+
added.ask.push(perm);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const result = {
|
|
382
|
+
filesScanned: local.length,
|
|
383
|
+
filesProcessed,
|
|
384
|
+
filesSkipped,
|
|
385
|
+
added,
|
|
386
|
+
subsumed,
|
|
387
|
+
conflictsResolved: conflictCount
|
|
388
|
+
};
|
|
389
|
+
return { merged, result };
|
|
390
|
+
}
|
|
391
|
+
function resolveConflicts(permissions) {
|
|
392
|
+
const allow = permissions.allow || [];
|
|
393
|
+
const deny = permissions.deny || [];
|
|
394
|
+
const ask = permissions.ask || [];
|
|
395
|
+
const denySet = new Set(deny);
|
|
396
|
+
const subsumed = [];
|
|
397
|
+
let conflictCount = 0;
|
|
398
|
+
const allowToRemove = /* @__PURE__ */ new Set();
|
|
399
|
+
for (const allowPerm of allow) {
|
|
400
|
+
if (denySet.has(allowPerm)) {
|
|
401
|
+
allowToRemove.add(allowPerm);
|
|
402
|
+
conflictCount++;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
for (const denyPerm of deny) {
|
|
406
|
+
try {
|
|
407
|
+
const allowParsed = parsePermission(allowPerm, "allow");
|
|
408
|
+
const denyParsed = parsePermission(denyPerm, "deny");
|
|
409
|
+
if (subsumes(denyParsed, allowParsed)) {
|
|
410
|
+
allowToRemove.add(allowPerm);
|
|
411
|
+
subsumed.push(allowPerm);
|
|
412
|
+
conflictCount++;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const resolved = {
|
|
421
|
+
allow: allow.filter((p) => !allowToRemove.has(p)),
|
|
422
|
+
deny,
|
|
423
|
+
ask
|
|
424
|
+
};
|
|
425
|
+
return { resolved, conflictCount, subsumed };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/lib/claude/settings/backup.ts
|
|
429
|
+
import fs4 from "fs/promises";
|
|
430
|
+
async function createBackup(settingsPath) {
|
|
431
|
+
try {
|
|
432
|
+
await fs4.access(settingsPath, fs4.constants.R_OK);
|
|
433
|
+
const now = /* @__PURE__ */ new Date();
|
|
434
|
+
const timestamp = [
|
|
435
|
+
now.getFullYear(),
|
|
436
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
437
|
+
String(now.getDate()).padStart(2, "0")
|
|
438
|
+
].join("-") + "-" + [
|
|
439
|
+
String(now.getHours()).padStart(2, "0"),
|
|
440
|
+
String(now.getMinutes()).padStart(2, "0"),
|
|
441
|
+
String(now.getSeconds()).padStart(2, "0")
|
|
442
|
+
].join("");
|
|
443
|
+
const backupPath = `${settingsPath}.backup.${timestamp}`;
|
|
444
|
+
await fs4.copyFile(settingsPath, backupPath);
|
|
445
|
+
return backupPath;
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (error instanceof Error) {
|
|
448
|
+
if (error.message.includes("ENOENT")) {
|
|
449
|
+
throw new Error(`Settings file not found: ${settingsPath}`);
|
|
450
|
+
}
|
|
451
|
+
if (error.message.includes("EACCES")) {
|
|
452
|
+
throw new Error(`Permission denied: ${settingsPath}`);
|
|
453
|
+
}
|
|
454
|
+
throw new Error(`Failed to create backup: ${error.message}`);
|
|
455
|
+
}
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/lib/claude/settings/reporter.ts
|
|
461
|
+
function formatReport(result, previewOnly, globalSettingsPath, outputFile) {
|
|
462
|
+
const lines = [];
|
|
463
|
+
lines.push("Scanning for Claude Code settings files...");
|
|
464
|
+
lines.push("");
|
|
465
|
+
lines.push(`Found ${result.filesScanned} settings files`);
|
|
466
|
+
lines.push(` Processed: ${result.filesProcessed}`);
|
|
467
|
+
if (result.filesSkipped > 0) {
|
|
468
|
+
lines.push(` Skipped: ${result.filesSkipped} (no permissions)`);
|
|
469
|
+
}
|
|
470
|
+
lines.push("");
|
|
471
|
+
const totalAdded = result.added.allow.length + result.added.deny.length + result.added.ask.length;
|
|
472
|
+
if (totalAdded > 0) {
|
|
473
|
+
lines.push(`Permissions to add: ${totalAdded}`);
|
|
474
|
+
if (result.added.allow.length > 0) {
|
|
475
|
+
lines.push("");
|
|
476
|
+
lines.push(" allow:");
|
|
477
|
+
for (const perm of result.added.allow) {
|
|
478
|
+
lines.push(` + ${perm}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (result.added.deny.length > 0) {
|
|
482
|
+
lines.push("");
|
|
483
|
+
lines.push(" deny:");
|
|
484
|
+
for (const perm of result.added.deny) {
|
|
485
|
+
lines.push(` + ${perm}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (result.added.ask.length > 0) {
|
|
489
|
+
lines.push("");
|
|
490
|
+
lines.push(" ask:");
|
|
491
|
+
for (const perm of result.added.ask) {
|
|
492
|
+
lines.push(` + ${perm}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
lines.push("No new permissions to add (all permissions already in global settings)");
|
|
497
|
+
}
|
|
498
|
+
lines.push("");
|
|
499
|
+
if (result.subsumed.length > 0) {
|
|
500
|
+
lines.push(`Subsumed permissions removed: ${result.subsumed.length}`);
|
|
501
|
+
lines.push(" (narrower permissions replaced by broader ones)");
|
|
502
|
+
for (const perm of result.subsumed) {
|
|
503
|
+
lines.push(` - ${perm}`);
|
|
504
|
+
}
|
|
505
|
+
lines.push("");
|
|
506
|
+
}
|
|
507
|
+
if (result.conflictsResolved > 0) {
|
|
508
|
+
lines.push(`Conflicts resolved: ${result.conflictsResolved}`);
|
|
509
|
+
lines.push(" (permissions moved from allow to deny)");
|
|
510
|
+
lines.push("");
|
|
511
|
+
}
|
|
512
|
+
if (result.backupPath) {
|
|
513
|
+
lines.push(`Backup created: ${result.backupPath}`);
|
|
514
|
+
lines.push("");
|
|
515
|
+
}
|
|
516
|
+
lines.push("Summary:");
|
|
517
|
+
lines.push(` Files scanned: ${result.filesScanned}`);
|
|
518
|
+
lines.push(
|
|
519
|
+
` Permissions added: ${result.added.allow.length} allow, ${result.added.deny.length} deny, ${result.added.ask.length} ask`
|
|
520
|
+
);
|
|
521
|
+
if (result.subsumed.length > 0) {
|
|
522
|
+
lines.push(` Subsumed removed: ${result.subsumed.length}`);
|
|
523
|
+
}
|
|
524
|
+
if (result.conflictsResolved > 0) {
|
|
525
|
+
lines.push(` Conflicts resolved: ${result.conflictsResolved}`);
|
|
526
|
+
}
|
|
527
|
+
lines.push("");
|
|
528
|
+
if (previewOnly) {
|
|
529
|
+
lines.push("\u2139\uFE0F Preview mode: No changes written");
|
|
530
|
+
lines.push("");
|
|
531
|
+
lines.push("To apply changes:");
|
|
532
|
+
lines.push(` \u2022 Modify global settings: spx claude settings consolidate --write`);
|
|
533
|
+
lines.push(` \u2022 Write to file: spx claude settings consolidate --output-file /path/to/file`);
|
|
534
|
+
} else if (outputFile) {
|
|
535
|
+
lines.push(`\u2713 Settings written to: ${result.outputPath || outputFile}`);
|
|
536
|
+
lines.push("");
|
|
537
|
+
lines.push("To apply to your global settings:");
|
|
538
|
+
lines.push(` \u2022 Review the file, then copy to: ${globalSettingsPath || "~/.claude/settings.json"}`);
|
|
539
|
+
lines.push(` \u2022 Or run: spx claude settings consolidate --write`);
|
|
540
|
+
} else {
|
|
541
|
+
lines.push(`\u2713 Global settings updated: ${globalSettingsPath || "~/.claude/settings.json"}`);
|
|
542
|
+
}
|
|
543
|
+
return lines.join("\n");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/lib/claude/settings/writer.ts
|
|
547
|
+
import fs5 from "fs/promises";
|
|
548
|
+
import os from "os";
|
|
549
|
+
import path3 from "path";
|
|
550
|
+
var realFs = {
|
|
551
|
+
writeFile: (path8, content) => fs5.writeFile(path8, content, "utf-8"),
|
|
552
|
+
rename: fs5.rename,
|
|
553
|
+
unlink: fs5.unlink,
|
|
554
|
+
mkdir: async (path8, options) => {
|
|
555
|
+
await fs5.mkdir(path8, options);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
async function writeSettings(filePath, settings, deps = { fs: realFs }) {
|
|
559
|
+
const dir = path3.dirname(filePath);
|
|
560
|
+
await deps.fs.mkdir(dir, { recursive: true });
|
|
561
|
+
const tempPath = path3.join(
|
|
562
|
+
os.tmpdir(),
|
|
563
|
+
`settings-${Date.now()}-${Math.random().toString(36).substring(7)}.json`
|
|
564
|
+
);
|
|
565
|
+
try {
|
|
566
|
+
const content = JSON.stringify(settings, null, 2) + "\n";
|
|
567
|
+
await deps.fs.writeFile(tempPath, content);
|
|
568
|
+
await deps.fs.rename(tempPath, filePath);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
try {
|
|
571
|
+
await deps.fs.unlink(tempPath);
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
if (error instanceof Error) {
|
|
575
|
+
throw new Error(`Failed to write settings: ${error.message}`);
|
|
576
|
+
}
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/commands/claude/settings/consolidate.ts
|
|
582
|
+
async function consolidateCommand(options = {}) {
|
|
583
|
+
const root = options.root ? path4.resolve(options.root.replace(/^~/, os2.homedir())) : path4.join(os2.homedir(), "Code");
|
|
584
|
+
const globalSettingsPath = options.globalSettings || path4.join(os2.homedir(), ".claude", "settings.json");
|
|
585
|
+
const shouldWrite = options.write || false;
|
|
586
|
+
const outputFile = options.outputFile;
|
|
587
|
+
const previewOnly = !shouldWrite && !outputFile;
|
|
588
|
+
const settingsFiles = await findSettingsFiles(root);
|
|
589
|
+
if (settingsFiles.length === 0) {
|
|
590
|
+
return `No settings files found in ${root}
|
|
591
|
+
|
|
592
|
+
Searched for: **/.claude/settings.local.json`;
|
|
593
|
+
}
|
|
594
|
+
const localPermissions = await parseAllSettings(settingsFiles);
|
|
595
|
+
let globalSettings = await parseSettingsFile(globalSettingsPath);
|
|
596
|
+
if (!globalSettings) {
|
|
597
|
+
globalSettings = {
|
|
598
|
+
permissions: {
|
|
599
|
+
allow: [],
|
|
600
|
+
deny: [],
|
|
601
|
+
ask: []
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
if (!globalSettings.permissions) {
|
|
606
|
+
globalSettings.permissions = {
|
|
607
|
+
allow: [],
|
|
608
|
+
deny: [],
|
|
609
|
+
ask: []
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const { merged, result } = mergePermissions(
|
|
613
|
+
globalSettings.permissions,
|
|
614
|
+
localPermissions
|
|
615
|
+
);
|
|
616
|
+
if (shouldWrite) {
|
|
617
|
+
try {
|
|
618
|
+
result.backupPath = await createBackup(globalSettingsPath);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
if (error instanceof Error && !error.message.includes("not found")) {
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (shouldWrite) {
|
|
626
|
+
const updatedSettings = {
|
|
627
|
+
...globalSettings,
|
|
628
|
+
permissions: merged
|
|
629
|
+
};
|
|
630
|
+
await writeSettings(globalSettingsPath, updatedSettings);
|
|
631
|
+
} else if (outputFile) {
|
|
632
|
+
const updatedSettings = {
|
|
633
|
+
...globalSettings,
|
|
634
|
+
permissions: merged
|
|
635
|
+
};
|
|
636
|
+
const resolvedOutputPath = path4.resolve(outputFile.replace(/^~/, os2.homedir()));
|
|
637
|
+
await writeSettings(resolvedOutputPath, updatedSettings);
|
|
638
|
+
result.outputPath = resolvedOutputPath;
|
|
639
|
+
}
|
|
640
|
+
return formatReport(result, previewOnly, globalSettingsPath, outputFile);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/domains/claude/index.ts
|
|
644
|
+
function registerClaudeCommands(claudeCmd) {
|
|
645
|
+
claudeCmd.command("init").description("Initialize or update spx-claude marketplace plugin").action(async () => {
|
|
646
|
+
try {
|
|
647
|
+
const output = await initCommand({ cwd: process.cwd() });
|
|
648
|
+
console.log(output);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
console.error(
|
|
651
|
+
"Error:",
|
|
652
|
+
error instanceof Error ? error.message : String(error)
|
|
653
|
+
);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
const settingsCmd = claudeCmd.command("settings").description("Manage Claude Code settings");
|
|
658
|
+
settingsCmd.command("consolidate").description(
|
|
659
|
+
"Consolidate permissions from project-specific settings into global settings"
|
|
660
|
+
).option("--write", "Write changes to global settings file (default: preview only)").option(
|
|
661
|
+
"--output-file <path>",
|
|
662
|
+
"Write merged settings to specified file instead of global settings"
|
|
663
|
+
).option(
|
|
664
|
+
"--root <path>",
|
|
665
|
+
"Root directory to scan for settings files (default: ~/Code)"
|
|
666
|
+
).option(
|
|
667
|
+
"--global-settings <path>",
|
|
668
|
+
"Path to global settings file (default: ~/.claude/settings.json)"
|
|
669
|
+
).action(
|
|
670
|
+
async (options) => {
|
|
671
|
+
try {
|
|
672
|
+
if (options.write && options.outputFile) {
|
|
673
|
+
console.error(
|
|
674
|
+
"Error: --write and --output-file are mutually exclusive\nUse --write to modify global settings, or --output-file to write to a different location"
|
|
675
|
+
);
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
const output = await consolidateCommand({
|
|
679
|
+
write: options.write,
|
|
680
|
+
outputFile: options.outputFile,
|
|
681
|
+
root: options.root,
|
|
682
|
+
globalSettings: options.globalSettings
|
|
683
|
+
});
|
|
684
|
+
console.log(output);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error(
|
|
687
|
+
"Error:",
|
|
688
|
+
error instanceof Error ? error.message : String(error)
|
|
689
|
+
);
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
var claudeDomain = {
|
|
696
|
+
name: "claude",
|
|
697
|
+
description: "Manage Claude Code settings and plugins",
|
|
698
|
+
register: (program2) => {
|
|
699
|
+
const claudeCmd = program2.command("claude").description("Manage Claude Code settings and plugins");
|
|
700
|
+
registerClaudeCommands(claudeCmd);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
// src/commands/session/archive.ts
|
|
705
|
+
import { mkdir, rename, stat } from "fs/promises";
|
|
706
|
+
import { dirname, join as join2 } from "path";
|
|
707
|
+
|
|
708
|
+
// src/session/errors.ts
|
|
709
|
+
var SessionError = class extends Error {
|
|
710
|
+
constructor(message) {
|
|
711
|
+
super(message);
|
|
712
|
+
this.name = "SessionError";
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
var SessionNotFoundError = class extends SessionError {
|
|
716
|
+
/** The session ID that was not found */
|
|
717
|
+
sessionId;
|
|
718
|
+
constructor(sessionId) {
|
|
719
|
+
super(`Session not found: ${sessionId}. Check the session ID and try again.`);
|
|
720
|
+
this.name = "SessionNotFoundError";
|
|
721
|
+
this.sessionId = sessionId;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
var SessionNotAvailableError = class extends SessionError {
|
|
725
|
+
/** The session ID that is not available */
|
|
726
|
+
sessionId;
|
|
727
|
+
constructor(sessionId) {
|
|
728
|
+
super(`Session not available: ${sessionId}. It may have been claimed by another agent.`);
|
|
729
|
+
this.name = "SessionNotAvailableError";
|
|
730
|
+
this.sessionId = sessionId;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
var SessionInvalidContentError = class extends SessionError {
|
|
734
|
+
constructor(reason) {
|
|
735
|
+
super(`Invalid session content: ${reason}`);
|
|
736
|
+
this.name = "SessionInvalidContentError";
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
var SessionNotClaimedError = class extends SessionError {
|
|
740
|
+
/** The session ID that is not claimed */
|
|
741
|
+
sessionId;
|
|
742
|
+
constructor(sessionId) {
|
|
743
|
+
super(`Session not claimed: ${sessionId}. The session is not in the doing directory.`);
|
|
744
|
+
this.name = "SessionNotClaimedError";
|
|
745
|
+
this.sessionId = sessionId;
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
var NoSessionsAvailableError = class extends SessionError {
|
|
749
|
+
constructor() {
|
|
750
|
+
super("No sessions available. The todo directory is empty.");
|
|
751
|
+
this.name = "NoSessionsAvailableError";
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/session/show.ts
|
|
756
|
+
import { join } from "path";
|
|
757
|
+
|
|
758
|
+
// src/config/defaults.ts
|
|
759
|
+
var DEFAULT_CONFIG = {
|
|
760
|
+
specs: {
|
|
761
|
+
root: "specs",
|
|
762
|
+
work: {
|
|
763
|
+
dir: "work",
|
|
764
|
+
statusDirs: {
|
|
765
|
+
doing: "doing",
|
|
766
|
+
backlog: "backlog",
|
|
767
|
+
done: "archive"
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
decisions: "decisions",
|
|
771
|
+
templates: "templates"
|
|
772
|
+
},
|
|
773
|
+
sessions: {
|
|
774
|
+
dir: ".spx/sessions",
|
|
775
|
+
statusDirs: {
|
|
776
|
+
todo: "todo",
|
|
777
|
+
doing: "doing",
|
|
778
|
+
archive: "archive"
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// src/session/list.ts
|
|
784
|
+
import { parse as parseYaml } from "yaml";
|
|
785
|
+
|
|
786
|
+
// src/session/timestamp.ts
|
|
787
|
+
var SESSION_ID_PATTERN = /^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})$/;
|
|
788
|
+
var SESSION_ID_SEPARATOR = "_";
|
|
789
|
+
function generateSessionId(options = {}) {
|
|
790
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
791
|
+
const date = now();
|
|
792
|
+
const year = date.getFullYear();
|
|
793
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
794
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
795
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
796
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
797
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
798
|
+
return `${year}-${month}-${day}${SESSION_ID_SEPARATOR}${hours}-${minutes}-${seconds}`;
|
|
799
|
+
}
|
|
800
|
+
function parseSessionId(id) {
|
|
801
|
+
const match = SESSION_ID_PATTERN.exec(id);
|
|
802
|
+
if (!match) {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
const [, yearStr, monthStr, dayStr, hoursStr, minutesStr, secondsStr] = match;
|
|
806
|
+
const year = parseInt(yearStr, 10);
|
|
807
|
+
const month = parseInt(monthStr, 10) - 1;
|
|
808
|
+
const day = parseInt(dayStr, 10);
|
|
809
|
+
const hours = parseInt(hoursStr, 10);
|
|
810
|
+
const minutes = parseInt(minutesStr, 10);
|
|
811
|
+
const seconds = parseInt(secondsStr, 10);
|
|
812
|
+
if (month < 0 || month > 11) return null;
|
|
813
|
+
if (day < 1 || day > 31) return null;
|
|
814
|
+
if (hours < 0 || hours > 23) return null;
|
|
815
|
+
if (minutes < 0 || minutes > 59) return null;
|
|
816
|
+
if (seconds < 0 || seconds > 59) return null;
|
|
817
|
+
return new Date(year, month, day, hours, minutes, seconds);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/session/types.ts
|
|
821
|
+
var PRIORITY_ORDER = {
|
|
822
|
+
high: 0,
|
|
823
|
+
medium: 1,
|
|
824
|
+
low: 2
|
|
825
|
+
};
|
|
826
|
+
var DEFAULT_PRIORITY = "medium";
|
|
827
|
+
|
|
828
|
+
// src/session/list.ts
|
|
829
|
+
var FRONT_MATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\r?\n?/;
|
|
830
|
+
function isValidPriority(value) {
|
|
831
|
+
return value === "high" || value === "medium" || value === "low";
|
|
832
|
+
}
|
|
833
|
+
function parseSessionMetadata(content) {
|
|
834
|
+
const match = FRONT_MATTER_PATTERN.exec(content);
|
|
835
|
+
if (!match) {
|
|
836
|
+
return {
|
|
837
|
+
priority: DEFAULT_PRIORITY,
|
|
838
|
+
tags: []
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
const parsed = parseYaml(match[1]);
|
|
843
|
+
if (!parsed || typeof parsed !== "object") {
|
|
844
|
+
return {
|
|
845
|
+
priority: DEFAULT_PRIORITY,
|
|
846
|
+
tags: []
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const priority = isValidPriority(parsed.priority) ? parsed.priority : DEFAULT_PRIORITY;
|
|
850
|
+
let tags = [];
|
|
851
|
+
if (Array.isArray(parsed.tags)) {
|
|
852
|
+
tags = parsed.tags.filter((t) => typeof t === "string");
|
|
853
|
+
}
|
|
854
|
+
const metadata = {
|
|
855
|
+
priority,
|
|
856
|
+
tags
|
|
857
|
+
};
|
|
858
|
+
if (typeof parsed.id === "string") {
|
|
859
|
+
metadata.id = parsed.id;
|
|
860
|
+
}
|
|
861
|
+
if (typeof parsed.branch === "string") {
|
|
862
|
+
metadata.branch = parsed.branch;
|
|
863
|
+
}
|
|
864
|
+
if (typeof parsed.created_at === "string") {
|
|
865
|
+
metadata.createdAt = parsed.created_at;
|
|
866
|
+
}
|
|
867
|
+
if (typeof parsed.working_directory === "string") {
|
|
868
|
+
metadata.workingDirectory = parsed.working_directory;
|
|
869
|
+
}
|
|
870
|
+
if (Array.isArray(parsed.specs)) {
|
|
871
|
+
metadata.specs = parsed.specs.filter(
|
|
872
|
+
(s) => typeof s === "string"
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (Array.isArray(parsed.files)) {
|
|
876
|
+
metadata.files = parsed.files.filter(
|
|
877
|
+
(f) => typeof f === "string"
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return metadata;
|
|
881
|
+
} catch {
|
|
882
|
+
return {
|
|
883
|
+
priority: DEFAULT_PRIORITY,
|
|
884
|
+
tags: []
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function sortSessions(sessions) {
|
|
889
|
+
return [...sessions].sort((a, b) => {
|
|
890
|
+
const priorityA = PRIORITY_ORDER[a.metadata.priority];
|
|
891
|
+
const priorityB = PRIORITY_ORDER[b.metadata.priority];
|
|
892
|
+
if (priorityA !== priorityB) {
|
|
893
|
+
return priorityA - priorityB;
|
|
894
|
+
}
|
|
895
|
+
const dateA = parseSessionId(a.id);
|
|
896
|
+
const dateB = parseSessionId(b.id);
|
|
897
|
+
if (!dateA && !dateB) return 0;
|
|
898
|
+
if (!dateA) return 1;
|
|
899
|
+
if (!dateB) return -1;
|
|
900
|
+
return dateB.getTime() - dateA.getTime();
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/session/show.ts
|
|
905
|
+
var { dir: sessionsBaseDir, statusDirs } = DEFAULT_CONFIG.sessions;
|
|
906
|
+
var DEFAULT_SESSION_CONFIG = {
|
|
907
|
+
todoDir: join(sessionsBaseDir, statusDirs.todo),
|
|
908
|
+
doingDir: join(sessionsBaseDir, statusDirs.doing),
|
|
909
|
+
archiveDir: join(sessionsBaseDir, statusDirs.archive)
|
|
910
|
+
};
|
|
911
|
+
var SEARCH_ORDER = ["todo", "doing", "archive"];
|
|
912
|
+
function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
|
|
913
|
+
const filename = `${id}.md`;
|
|
914
|
+
return [
|
|
915
|
+
`${config.todoDir}/${filename}`,
|
|
916
|
+
`${config.doingDir}/${filename}`,
|
|
917
|
+
`${config.archiveDir}/${filename}`
|
|
918
|
+
];
|
|
919
|
+
}
|
|
920
|
+
function formatShowOutput(content, options) {
|
|
921
|
+
const metadata = parseSessionMetadata(content);
|
|
922
|
+
const headerLines = [
|
|
923
|
+
`Status: ${options.status}`,
|
|
924
|
+
`Priority: ${metadata.priority}`
|
|
925
|
+
];
|
|
926
|
+
if (metadata.id) {
|
|
927
|
+
headerLines.unshift(`ID: ${metadata.id}`);
|
|
928
|
+
}
|
|
929
|
+
if (metadata.branch) {
|
|
930
|
+
headerLines.push(`Branch: ${metadata.branch}`);
|
|
931
|
+
}
|
|
932
|
+
if (metadata.tags.length > 0) {
|
|
933
|
+
headerLines.push(`Tags: ${metadata.tags.join(", ")}`);
|
|
934
|
+
}
|
|
935
|
+
if (metadata.createdAt) {
|
|
936
|
+
headerLines.push(`Created: ${metadata.createdAt}`);
|
|
937
|
+
}
|
|
938
|
+
const header = headerLines.join("\n");
|
|
939
|
+
const separator = "\n" + "\u2500".repeat(40) + "\n\n";
|
|
940
|
+
return header + separator + content;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/commands/session/archive.ts
|
|
944
|
+
var SessionAlreadyArchivedError = class extends Error {
|
|
945
|
+
/** The session ID that is already archived */
|
|
946
|
+
sessionId;
|
|
947
|
+
constructor(sessionId) {
|
|
948
|
+
super(`Session already archived: ${sessionId}.`);
|
|
949
|
+
this.name = "SessionAlreadyArchivedError";
|
|
950
|
+
this.sessionId = sessionId;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
async function resolveArchivePaths(sessionId, config) {
|
|
954
|
+
const filename = `${sessionId}.md`;
|
|
955
|
+
const todoPath = join2(config.todoDir, filename);
|
|
956
|
+
const doingPath = join2(config.doingDir, filename);
|
|
957
|
+
const archivePath = join2(config.archiveDir, filename);
|
|
958
|
+
try {
|
|
959
|
+
const archiveStats = await stat(archivePath);
|
|
960
|
+
if (archiveStats.isFile()) {
|
|
961
|
+
throw new SessionAlreadyArchivedError(sessionId);
|
|
962
|
+
}
|
|
963
|
+
} catch (error) {
|
|
964
|
+
if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
if (error instanceof SessionAlreadyArchivedError) {
|
|
968
|
+
throw error;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const todoStats = await stat(todoPath);
|
|
973
|
+
if (todoStats.isFile()) {
|
|
974
|
+
return { source: todoPath, target: archivePath };
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
const doingStats = await stat(doingPath);
|
|
980
|
+
if (doingStats.isFile()) {
|
|
981
|
+
return { source: doingPath, target: archivePath };
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
throw new SessionNotFoundError(sessionId);
|
|
986
|
+
}
|
|
987
|
+
async function archiveCommand(options) {
|
|
988
|
+
const config = options.sessionsDir ? {
|
|
989
|
+
todoDir: join2(options.sessionsDir, "todo"),
|
|
990
|
+
doingDir: join2(options.sessionsDir, "doing"),
|
|
991
|
+
archiveDir: join2(options.sessionsDir, "archive")
|
|
992
|
+
} : DEFAULT_SESSION_CONFIG;
|
|
993
|
+
const { source, target } = await resolveArchivePaths(options.sessionId, config);
|
|
994
|
+
await mkdir(dirname(target), { recursive: true });
|
|
995
|
+
await rename(source, target);
|
|
996
|
+
return `Archived session: ${options.sessionId}
|
|
997
|
+
Archive location: ${target}`;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// src/commands/session/delete.ts
|
|
1001
|
+
import { stat as stat2, unlink } from "fs/promises";
|
|
1002
|
+
import { join as join3 } from "path";
|
|
1003
|
+
|
|
1004
|
+
// src/session/delete.ts
|
|
1005
|
+
function resolveDeletePath(sessionId, existingPaths) {
|
|
1006
|
+
const matchingPath = existingPaths.find((path8) => path8.includes(sessionId));
|
|
1007
|
+
if (!matchingPath) {
|
|
1008
|
+
throw new SessionNotFoundError(sessionId);
|
|
1009
|
+
}
|
|
1010
|
+
return matchingPath;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/commands/session/delete.ts
|
|
1014
|
+
async function findExistingPaths(paths) {
|
|
1015
|
+
const existing = [];
|
|
1016
|
+
for (const path8 of paths) {
|
|
1017
|
+
try {
|
|
1018
|
+
const stats = await stat2(path8);
|
|
1019
|
+
if (stats.isFile()) {
|
|
1020
|
+
existing.push(path8);
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return existing;
|
|
1026
|
+
}
|
|
1027
|
+
async function deleteCommand(options) {
|
|
1028
|
+
const config = options.sessionsDir ? {
|
|
1029
|
+
todoDir: join3(options.sessionsDir, "todo"),
|
|
1030
|
+
doingDir: join3(options.sessionsDir, "doing"),
|
|
1031
|
+
archiveDir: join3(options.sessionsDir, "archive")
|
|
1032
|
+
} : DEFAULT_SESSION_CONFIG;
|
|
1033
|
+
const paths = resolveSessionPaths(options.sessionId, config);
|
|
1034
|
+
const existingPaths = await findExistingPaths(paths);
|
|
1035
|
+
const pathToDelete = resolveDeletePath(options.sessionId, existingPaths);
|
|
1036
|
+
await unlink(pathToDelete);
|
|
1037
|
+
return `Deleted session: ${options.sessionId}`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/commands/session/handoff.ts
|
|
1041
|
+
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
1042
|
+
import { join as join5, resolve } from "path";
|
|
1043
|
+
|
|
1044
|
+
// src/git/root.ts
|
|
1045
|
+
import { execa as execa2 } from "execa";
|
|
1046
|
+
import { join as join4 } from "path";
|
|
1047
|
+
var defaultDeps = {
|
|
1048
|
+
execa: (command, args, options) => execa2(command, args, options)
|
|
1049
|
+
};
|
|
1050
|
+
var NOT_GIT_REPO_WARNING = "Warning: Not in a git repository. Sessions will be created relative to current directory.";
|
|
1051
|
+
async function detectGitRoot(cwd = process.cwd(), deps = defaultDeps) {
|
|
1052
|
+
try {
|
|
1053
|
+
const result = await deps.execa(
|
|
1054
|
+
"git",
|
|
1055
|
+
["rev-parse", "--show-toplevel"],
|
|
1056
|
+
{ cwd, reject: false }
|
|
1057
|
+
);
|
|
1058
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
1059
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : result.stdout.toString();
|
|
1060
|
+
const gitRoot = stdout.trim().replace(/\/+$/, "");
|
|
1061
|
+
return {
|
|
1062
|
+
root: gitRoot,
|
|
1063
|
+
isGitRepo: true
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
root: cwd,
|
|
1068
|
+
isGitRepo: false,
|
|
1069
|
+
warning: NOT_GIT_REPO_WARNING
|
|
1070
|
+
};
|
|
1071
|
+
} catch {
|
|
1072
|
+
return {
|
|
1073
|
+
root: cwd,
|
|
1074
|
+
isGitRepo: false,
|
|
1075
|
+
warning: NOT_GIT_REPO_WARNING
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function buildSessionPathFromRoot(gitRoot, sessionId, config) {
|
|
1080
|
+
const filename = `${sessionId}.md`;
|
|
1081
|
+
return join4(gitRoot, config.todoDir, filename);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/session/create.ts
|
|
1085
|
+
var MIN_CONTENT_LENGTH = 1;
|
|
1086
|
+
function validateSessionContent(content) {
|
|
1087
|
+
if (!content || content.trim().length < MIN_CONTENT_LENGTH) {
|
|
1088
|
+
return {
|
|
1089
|
+
valid: false,
|
|
1090
|
+
error: "Session content cannot be empty"
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
if (content.trim().length === 0) {
|
|
1094
|
+
return {
|
|
1095
|
+
valid: false,
|
|
1096
|
+
error: "Session content cannot be only whitespace"
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
return { valid: true };
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// src/commands/session/handoff.ts
|
|
1103
|
+
var { statusDirs: statusDirs2 } = DEFAULT_CONFIG.sessions;
|
|
1104
|
+
var FRONT_MATTER_START = /^---\r?\n/;
|
|
1105
|
+
function hasFrontmatter(content) {
|
|
1106
|
+
return FRONT_MATTER_START.test(content);
|
|
1107
|
+
}
|
|
1108
|
+
function buildSessionContent(content) {
|
|
1109
|
+
if (!content || content.trim().length === 0) {
|
|
1110
|
+
return `---
|
|
1111
|
+
priority: medium
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
# New Session
|
|
1115
|
+
|
|
1116
|
+
Describe your task here.`;
|
|
1117
|
+
}
|
|
1118
|
+
if (hasFrontmatter(content)) {
|
|
1119
|
+
return content;
|
|
1120
|
+
}
|
|
1121
|
+
return `---
|
|
1122
|
+
priority: medium
|
|
1123
|
+
---
|
|
1124
|
+
|
|
1125
|
+
${content}`;
|
|
1126
|
+
}
|
|
1127
|
+
async function handoffCommand(options) {
|
|
1128
|
+
const config = options.sessionsDir ? {
|
|
1129
|
+
todoDir: join5(options.sessionsDir, statusDirs2.todo),
|
|
1130
|
+
doingDir: join5(options.sessionsDir, statusDirs2.doing),
|
|
1131
|
+
archiveDir: join5(options.sessionsDir, statusDirs2.archive)
|
|
1132
|
+
} : DEFAULT_SESSION_CONFIG;
|
|
1133
|
+
let baseDir;
|
|
1134
|
+
let warningMessage;
|
|
1135
|
+
if (options.sessionsDir) {
|
|
1136
|
+
baseDir = options.sessionsDir;
|
|
1137
|
+
} else {
|
|
1138
|
+
const gitResult = await detectGitRoot();
|
|
1139
|
+
baseDir = gitResult.root;
|
|
1140
|
+
warningMessage = gitResult.warning;
|
|
1141
|
+
}
|
|
1142
|
+
const sessionId = generateSessionId();
|
|
1143
|
+
const fullContent = buildSessionContent(options.content);
|
|
1144
|
+
const validation = validateSessionContent(fullContent);
|
|
1145
|
+
if (!validation.valid) {
|
|
1146
|
+
throw new SessionInvalidContentError(validation.error ?? "Unknown validation error");
|
|
1147
|
+
}
|
|
1148
|
+
const filename = `${sessionId}.md`;
|
|
1149
|
+
const sessionPath = options.sessionsDir ? join5(config.todoDir, filename) : buildSessionPathFromRoot(baseDir, sessionId, config);
|
|
1150
|
+
const absolutePath = resolve(sessionPath);
|
|
1151
|
+
const todoDir = options.sessionsDir ? config.todoDir : join5(baseDir, config.todoDir);
|
|
1152
|
+
await mkdir2(todoDir, { recursive: true });
|
|
1153
|
+
await writeFile(sessionPath, fullContent, "utf-8");
|
|
1154
|
+
let output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
|
|
1155
|
+
<SESSION_FILE>${absolutePath}</SESSION_FILE>`;
|
|
1156
|
+
if (warningMessage) {
|
|
1157
|
+
process.stderr.write(`${warningMessage}
|
|
1158
|
+
`);
|
|
1159
|
+
}
|
|
1160
|
+
return output;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/commands/session/list.ts
|
|
1164
|
+
import { readdir, readFile } from "fs/promises";
|
|
1165
|
+
import { join as join6 } from "path";
|
|
1166
|
+
async function loadSessionsFromDir(dir, status) {
|
|
1167
|
+
try {
|
|
1168
|
+
const files = await readdir(dir);
|
|
1169
|
+
const sessions = [];
|
|
1170
|
+
for (const file of files) {
|
|
1171
|
+
if (!file.endsWith(".md")) continue;
|
|
1172
|
+
const id = file.replace(".md", "");
|
|
1173
|
+
const filePath = join6(dir, file);
|
|
1174
|
+
const content = await readFile(filePath, "utf-8");
|
|
1175
|
+
const metadata = parseSessionMetadata(content);
|
|
1176
|
+
sessions.push({
|
|
1177
|
+
id,
|
|
1178
|
+
status,
|
|
1179
|
+
path: filePath,
|
|
1180
|
+
metadata
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
return sessions;
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1186
|
+
return [];
|
|
1187
|
+
}
|
|
1188
|
+
throw error;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function formatTextOutput(sessions, _status) {
|
|
1192
|
+
if (sessions.length === 0) {
|
|
1193
|
+
return ` (no sessions)`;
|
|
1194
|
+
}
|
|
1195
|
+
return sessions.map((s) => {
|
|
1196
|
+
const priority = s.metadata.priority !== "medium" ? ` [${s.metadata.priority}]` : "";
|
|
1197
|
+
const tags = s.metadata.tags.length > 0 ? ` (${s.metadata.tags.join(", ")})` : "";
|
|
1198
|
+
return ` ${s.id}${priority}${tags}`;
|
|
1199
|
+
}).join("\n");
|
|
1200
|
+
}
|
|
1201
|
+
async function listCommand(options) {
|
|
1202
|
+
const config = options.sessionsDir ? {
|
|
1203
|
+
todoDir: join6(options.sessionsDir, "todo"),
|
|
1204
|
+
doingDir: join6(options.sessionsDir, "doing"),
|
|
1205
|
+
archiveDir: join6(options.sessionsDir, "archive")
|
|
1206
|
+
} : DEFAULT_SESSION_CONFIG;
|
|
1207
|
+
const statuses = options.status ? [options.status] : ["todo", "doing", "archive"];
|
|
1208
|
+
const allSessions = {
|
|
1209
|
+
todo: [],
|
|
1210
|
+
doing: [],
|
|
1211
|
+
archive: []
|
|
1212
|
+
};
|
|
1213
|
+
for (const status of statuses) {
|
|
1214
|
+
const dir = status === "todo" ? config.todoDir : status === "doing" ? config.doingDir : config.archiveDir;
|
|
1215
|
+
const sessions = await loadSessionsFromDir(dir, status);
|
|
1216
|
+
allSessions[status] = sortSessions(sessions);
|
|
1217
|
+
}
|
|
1218
|
+
if (options.format === "json") {
|
|
1219
|
+
return JSON.stringify(allSessions, null, 2);
|
|
1220
|
+
}
|
|
1221
|
+
const lines = [];
|
|
1222
|
+
if (statuses.includes("doing")) {
|
|
1223
|
+
lines.push("DOING:");
|
|
1224
|
+
lines.push(formatTextOutput(allSessions.doing, "doing"));
|
|
1225
|
+
lines.push("");
|
|
1226
|
+
}
|
|
1227
|
+
if (statuses.includes("todo")) {
|
|
1228
|
+
lines.push("TODO:");
|
|
1229
|
+
lines.push(formatTextOutput(allSessions.todo, "todo"));
|
|
1230
|
+
lines.push("");
|
|
1231
|
+
}
|
|
1232
|
+
if (statuses.includes("archive")) {
|
|
1233
|
+
lines.push("ARCHIVE:");
|
|
1234
|
+
lines.push(formatTextOutput(allSessions.archive, "archive"));
|
|
1235
|
+
}
|
|
1236
|
+
return lines.join("\n").trim();
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/commands/session/pickup.ts
|
|
1240
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rename as rename2 } from "fs/promises";
|
|
1241
|
+
import { join as join7 } from "path";
|
|
1242
|
+
|
|
1243
|
+
// src/session/pickup.ts
|
|
1244
|
+
function buildClaimPaths(sessionId, config) {
|
|
1245
|
+
return {
|
|
1246
|
+
source: `${config.todoDir}/${sessionId}.md`,
|
|
1247
|
+
target: `${config.doingDir}/${sessionId}.md`
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function classifyClaimError(error, sessionId) {
|
|
1251
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1252
|
+
return new SessionNotAvailableError(sessionId);
|
|
1253
|
+
}
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
function selectBestSession(sessions) {
|
|
1257
|
+
if (sessions.length === 0) {
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
const sorted = [...sessions].sort((a, b) => {
|
|
1261
|
+
const priorityA = PRIORITY_ORDER[a.metadata.priority];
|
|
1262
|
+
const priorityB = PRIORITY_ORDER[b.metadata.priority];
|
|
1263
|
+
if (priorityA !== priorityB) {
|
|
1264
|
+
return priorityA - priorityB;
|
|
1265
|
+
}
|
|
1266
|
+
const dateA = parseSessionId(a.id);
|
|
1267
|
+
const dateB = parseSessionId(b.id);
|
|
1268
|
+
if (!dateA && !dateB) return 0;
|
|
1269
|
+
if (!dateA) return 1;
|
|
1270
|
+
if (!dateB) return -1;
|
|
1271
|
+
return dateA.getTime() - dateB.getTime();
|
|
1272
|
+
});
|
|
1273
|
+
return sorted[0];
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/commands/session/pickup.ts
|
|
1277
|
+
async function loadTodoSessions(config) {
|
|
1278
|
+
try {
|
|
1279
|
+
const files = await readdir2(config.todoDir);
|
|
1280
|
+
const sessions = [];
|
|
1281
|
+
for (const file of files) {
|
|
1282
|
+
if (!file.endsWith(".md")) continue;
|
|
1283
|
+
const id = file.replace(".md", "");
|
|
1284
|
+
const filePath = join7(config.todoDir, file);
|
|
1285
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1286
|
+
const metadata = parseSessionMetadata(content);
|
|
1287
|
+
sessions.push({
|
|
1288
|
+
id,
|
|
1289
|
+
status: "todo",
|
|
1290
|
+
path: filePath,
|
|
1291
|
+
metadata
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
return sessions;
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1297
|
+
return [];
|
|
1298
|
+
}
|
|
1299
|
+
throw error;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async function pickupCommand(options) {
|
|
1303
|
+
const config = options.sessionsDir ? {
|
|
1304
|
+
todoDir: join7(options.sessionsDir, "todo"),
|
|
1305
|
+
doingDir: join7(options.sessionsDir, "doing"),
|
|
1306
|
+
archiveDir: join7(options.sessionsDir, "archive")
|
|
1307
|
+
} : DEFAULT_SESSION_CONFIG;
|
|
1308
|
+
let sessionId;
|
|
1309
|
+
if (options.auto) {
|
|
1310
|
+
const sessions = await loadTodoSessions(config);
|
|
1311
|
+
const selected = selectBestSession(sessions);
|
|
1312
|
+
if (!selected) {
|
|
1313
|
+
throw new NoSessionsAvailableError();
|
|
1314
|
+
}
|
|
1315
|
+
sessionId = selected.id;
|
|
1316
|
+
} else if (options.sessionId) {
|
|
1317
|
+
sessionId = options.sessionId;
|
|
1318
|
+
} else {
|
|
1319
|
+
throw new Error("Either session ID or --auto flag is required");
|
|
1320
|
+
}
|
|
1321
|
+
const paths = buildClaimPaths(sessionId, config);
|
|
1322
|
+
await mkdir3(config.doingDir, { recursive: true });
|
|
1323
|
+
try {
|
|
1324
|
+
await rename2(paths.source, paths.target);
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
throw classifyClaimError(error, sessionId);
|
|
1327
|
+
}
|
|
1328
|
+
const content = await readFile2(paths.target, "utf-8");
|
|
1329
|
+
const output = formatShowOutput(content, { status: "doing" });
|
|
1330
|
+
return `Claimed session <PICKUP_ID>${sessionId}</PICKUP_ID>
|
|
1331
|
+
|
|
1332
|
+
${output}`;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/commands/session/prune.ts
|
|
1336
|
+
import { readdir as readdir3, readFile as readFile3, unlink as unlink2 } from "fs/promises";
|
|
1337
|
+
import { join as join8 } from "path";
|
|
1338
|
+
var DEFAULT_KEEP_COUNT = 5;
|
|
1339
|
+
var PruneValidationError = class extends Error {
|
|
1340
|
+
constructor(message) {
|
|
1341
|
+
super(message);
|
|
1342
|
+
this.name = "PruneValidationError";
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
function validatePruneOptions(options) {
|
|
1346
|
+
if (options.keep !== void 0) {
|
|
1347
|
+
if (!Number.isInteger(options.keep) || options.keep < 1) {
|
|
1348
|
+
throw new PruneValidationError(
|
|
1349
|
+
`Invalid --keep value: ${options.keep}. Must be a positive integer.`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function loadArchiveSessions(config) {
|
|
1355
|
+
try {
|
|
1356
|
+
const files = await readdir3(config.archiveDir);
|
|
1357
|
+
const sessions = [];
|
|
1358
|
+
for (const file of files) {
|
|
1359
|
+
if (!file.endsWith(".md")) continue;
|
|
1360
|
+
const id = file.replace(".md", "");
|
|
1361
|
+
const filePath = join8(config.archiveDir, file);
|
|
1362
|
+
const content = await readFile3(filePath, "utf-8");
|
|
1363
|
+
const metadata = parseSessionMetadata(content);
|
|
1364
|
+
sessions.push({
|
|
1365
|
+
id,
|
|
1366
|
+
status: "archive",
|
|
1367
|
+
path: filePath,
|
|
1368
|
+
metadata
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
return sessions;
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1374
|
+
return [];
|
|
1375
|
+
}
|
|
1376
|
+
throw error;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
function selectSessionsToPrune(sessions, keep) {
|
|
1380
|
+
const sorted = sortSessions(sessions);
|
|
1381
|
+
if (sorted.length <= keep) {
|
|
1382
|
+
return [];
|
|
1383
|
+
}
|
|
1384
|
+
return sorted.slice(keep);
|
|
1385
|
+
}
|
|
1386
|
+
async function pruneCommand(options) {
|
|
1387
|
+
validatePruneOptions(options);
|
|
1388
|
+
const keep = options.keep ?? DEFAULT_KEEP_COUNT;
|
|
1389
|
+
const dryRun = options.dryRun ?? false;
|
|
1390
|
+
const config = options.sessionsDir ? {
|
|
1391
|
+
todoDir: join8(options.sessionsDir, "todo"),
|
|
1392
|
+
doingDir: join8(options.sessionsDir, "doing"),
|
|
1393
|
+
archiveDir: join8(options.sessionsDir, "archive")
|
|
1394
|
+
} : DEFAULT_SESSION_CONFIG;
|
|
1395
|
+
const sessions = await loadArchiveSessions(config);
|
|
1396
|
+
const toPrune = selectSessionsToPrune(sessions, keep);
|
|
1397
|
+
if (toPrune.length === 0) {
|
|
1398
|
+
return `No sessions to prune. ${sessions.length} sessions kept.`;
|
|
1399
|
+
}
|
|
1400
|
+
if (dryRun) {
|
|
1401
|
+
const lines2 = [
|
|
1402
|
+
`Would delete ${toPrune.length} sessions:`,
|
|
1403
|
+
...toPrune.map((s) => ` - ${s.id}`),
|
|
1404
|
+
"",
|
|
1405
|
+
`${sessions.length - toPrune.length} sessions would be kept.`
|
|
1406
|
+
];
|
|
1407
|
+
return lines2.join("\n");
|
|
1408
|
+
}
|
|
1409
|
+
for (const session of toPrune) {
|
|
1410
|
+
await unlink2(session.path);
|
|
1411
|
+
}
|
|
1412
|
+
const lines = [
|
|
1413
|
+
`Deleted ${toPrune.length} sessions:`,
|
|
1414
|
+
...toPrune.map((s) => ` - ${s.id}`),
|
|
1415
|
+
"",
|
|
1416
|
+
`${sessions.length - toPrune.length} sessions kept.`
|
|
1417
|
+
];
|
|
1418
|
+
return lines.join("\n");
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/commands/session/release.ts
|
|
1422
|
+
import { readdir as readdir4, rename as rename3 } from "fs/promises";
|
|
1423
|
+
import { join as join9 } from "path";
|
|
1424
|
+
|
|
1425
|
+
// src/session/release.ts
|
|
1426
|
+
function buildReleasePaths(sessionId, config) {
|
|
1427
|
+
return {
|
|
1428
|
+
source: `${config.doingDir}/${sessionId}.md`,
|
|
1429
|
+
target: `${config.todoDir}/${sessionId}.md`
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function findCurrentSession(doingSessions) {
|
|
1433
|
+
if (doingSessions.length === 0) {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
const sorted = [...doingSessions].sort((a, b) => {
|
|
1437
|
+
const dateA = parseSessionId(a.id);
|
|
1438
|
+
const dateB = parseSessionId(b.id);
|
|
1439
|
+
if (!dateA && !dateB) return 0;
|
|
1440
|
+
if (!dateA) return 1;
|
|
1441
|
+
if (!dateB) return -1;
|
|
1442
|
+
return dateB.getTime() - dateA.getTime();
|
|
1443
|
+
});
|
|
1444
|
+
return sorted[0];
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/commands/session/release.ts
|
|
1448
|
+
async function loadDoingSessions(config) {
|
|
1449
|
+
try {
|
|
1450
|
+
const files = await readdir4(config.doingDir);
|
|
1451
|
+
return files.filter((file) => file.endsWith(".md")).map((file) => ({ id: file.replace(".md", "") }));
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1454
|
+
return [];
|
|
1455
|
+
}
|
|
1456
|
+
throw error;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
async function releaseCommand(options) {
|
|
1460
|
+
const config = options.sessionsDir ? {
|
|
1461
|
+
todoDir: join9(options.sessionsDir, "todo"),
|
|
1462
|
+
doingDir: join9(options.sessionsDir, "doing"),
|
|
1463
|
+
archiveDir: join9(options.sessionsDir, "archive")
|
|
1464
|
+
} : DEFAULT_SESSION_CONFIG;
|
|
1465
|
+
let sessionId;
|
|
1466
|
+
if (options.sessionId) {
|
|
1467
|
+
sessionId = options.sessionId;
|
|
1468
|
+
} else {
|
|
1469
|
+
const sessions = await loadDoingSessions(config);
|
|
1470
|
+
const current = findCurrentSession(sessions);
|
|
1471
|
+
if (!current) {
|
|
1472
|
+
throw new SessionNotClaimedError("(none)");
|
|
1473
|
+
}
|
|
1474
|
+
sessionId = current.id;
|
|
1475
|
+
}
|
|
1476
|
+
const paths = buildReleasePaths(sessionId, config);
|
|
1477
|
+
try {
|
|
1478
|
+
await rename3(paths.source, paths.target);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1481
|
+
throw new SessionNotClaimedError(sessionId);
|
|
1482
|
+
}
|
|
1483
|
+
throw error;
|
|
1484
|
+
}
|
|
1485
|
+
return `Released session: ${sessionId}
|
|
1486
|
+
Session returned to todo directory.`;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/commands/session/show.ts
|
|
1490
|
+
import { readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
1491
|
+
import { join as join10 } from "path";
|
|
1492
|
+
async function findExistingPath(paths, _config) {
|
|
1493
|
+
for (let i = 0; i < paths.length; i++) {
|
|
1494
|
+
const filePath = paths[i];
|
|
1495
|
+
try {
|
|
1496
|
+
const stats = await stat3(filePath);
|
|
1497
|
+
if (stats.isFile()) {
|
|
1498
|
+
return { path: filePath, status: SEARCH_ORDER[i] };
|
|
1499
|
+
}
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
async function showCommand(options) {
|
|
1506
|
+
const config = options.sessionsDir ? {
|
|
1507
|
+
todoDir: join10(options.sessionsDir, "todo"),
|
|
1508
|
+
doingDir: join10(options.sessionsDir, "doing"),
|
|
1509
|
+
archiveDir: join10(options.sessionsDir, "archive")
|
|
1510
|
+
} : DEFAULT_SESSION_CONFIG;
|
|
1511
|
+
const paths = resolveSessionPaths(options.sessionId, config);
|
|
1512
|
+
const found = await findExistingPath(paths, config);
|
|
1513
|
+
if (!found) {
|
|
1514
|
+
throw new SessionNotFoundError(options.sessionId);
|
|
1515
|
+
}
|
|
1516
|
+
const content = await readFile4(found.path, "utf-8");
|
|
1517
|
+
return formatShowOutput(content, { status: found.status });
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/domains/session/help.ts
|
|
1521
|
+
var SESSION_FORMAT_HELP = `
|
|
1522
|
+
Session File Format:
|
|
1523
|
+
Sessions are markdown files with YAML frontmatter for metadata.
|
|
1524
|
+
|
|
1525
|
+
---
|
|
1526
|
+
priority: high | medium | low
|
|
1527
|
+
tags: [tag1, tag2]
|
|
1528
|
+
---
|
|
1529
|
+
# Session Title
|
|
1530
|
+
|
|
1531
|
+
Session content...
|
|
1532
|
+
|
|
1533
|
+
Workflow:
|
|
1534
|
+
1. handoff - Create session (todo)
|
|
1535
|
+
2. pickup - Claim session (todo -> doing)
|
|
1536
|
+
3. release - Return session (doing -> todo)
|
|
1537
|
+
4. delete - Remove session
|
|
1538
|
+
`;
|
|
1539
|
+
var HANDOFF_FRONTMATTER_HELP = `
|
|
1540
|
+
Usage:
|
|
1541
|
+
Option 1: Pipe content with frontmatter via stdin
|
|
1542
|
+
Option 2: Run without stdin, then edit the created file directly
|
|
1543
|
+
|
|
1544
|
+
Frontmatter Format:
|
|
1545
|
+
---
|
|
1546
|
+
priority: high # high | medium | low (default: medium)
|
|
1547
|
+
tags: [feat, api] # optional labels for categorization
|
|
1548
|
+
---
|
|
1549
|
+
# Your session content here...
|
|
1550
|
+
|
|
1551
|
+
Output Tags (for automation):
|
|
1552
|
+
<HANDOFF_ID>session-id</HANDOFF_ID> - Session identifier
|
|
1553
|
+
<SESSION_FILE>/path/to/file</SESSION_FILE> - Absolute path to edit
|
|
1554
|
+
|
|
1555
|
+
Examples:
|
|
1556
|
+
# With stdin content:
|
|
1557
|
+
echo '---
|
|
1558
|
+
priority: high
|
|
1559
|
+
---
|
|
1560
|
+
# Fix login' | spx session handoff
|
|
1561
|
+
|
|
1562
|
+
# Without stdin (creates empty session, edit file directly):
|
|
1563
|
+
spx session handoff
|
|
1564
|
+
`;
|
|
1565
|
+
var PICKUP_SELECTION_HELP = `
|
|
1566
|
+
Selection Logic (--auto):
|
|
1567
|
+
Sessions are selected by priority, then age (FIFO):
|
|
1568
|
+
1. high priority first
|
|
1569
|
+
2. medium priority second
|
|
1570
|
+
3. low priority last
|
|
1571
|
+
4. Within same priority: oldest session first
|
|
1572
|
+
|
|
1573
|
+
Output:
|
|
1574
|
+
<PICKUP_ID>session-id</PICKUP_ID> tag for automation parsing
|
|
1575
|
+
`;
|
|
1576
|
+
|
|
1577
|
+
// src/domains/session/index.ts
|
|
1578
|
+
async function readStdin() {
|
|
1579
|
+
if (process.stdin.isTTY) {
|
|
1580
|
+
return void 0;
|
|
1581
|
+
}
|
|
1582
|
+
return new Promise((resolve2) => {
|
|
1583
|
+
let data = "";
|
|
1584
|
+
process.stdin.setEncoding("utf-8");
|
|
1585
|
+
process.stdin.on("data", (chunk) => {
|
|
1586
|
+
data += chunk;
|
|
1587
|
+
});
|
|
1588
|
+
process.stdin.on("end", () => {
|
|
1589
|
+
resolve2(data.trim() || void 0);
|
|
1590
|
+
});
|
|
1591
|
+
process.stdin.on("error", () => {
|
|
1592
|
+
resolve2(void 0);
|
|
1593
|
+
});
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
function handleError(error) {
|
|
1597
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
function registerSessionCommands(sessionCmd) {
|
|
1601
|
+
sessionCmd.command("list").description("List all sessions").option("--status <status>", "Filter by status (todo|doing|archive)").option("--json", "Output as JSON").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
1602
|
+
try {
|
|
1603
|
+
const output = await listCommand({
|
|
1604
|
+
status: options.status,
|
|
1605
|
+
format: options.json ? "json" : "text",
|
|
1606
|
+
sessionsDir: options.sessionsDir
|
|
1607
|
+
});
|
|
1608
|
+
console.log(output);
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
handleError(error);
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
sessionCmd.command("show <id>").description("Show session content").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
|
|
1614
|
+
try {
|
|
1615
|
+
const output = await showCommand({
|
|
1616
|
+
sessionId: id,
|
|
1617
|
+
sessionsDir: options.sessionsDir
|
|
1618
|
+
});
|
|
1619
|
+
console.log(output);
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
handleError(error);
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
sessionCmd.command("pickup [id]").description("Claim a session (move from todo to doing)").option("--auto", "Auto-select highest priority session").option("--sessions-dir <path>", "Custom sessions directory").addHelpText("after", PICKUP_SELECTION_HELP).action(async (id, options) => {
|
|
1625
|
+
try {
|
|
1626
|
+
if (!id && !options.auto) {
|
|
1627
|
+
console.error("Error: Either session ID or --auto flag is required");
|
|
1628
|
+
process.exit(1);
|
|
1629
|
+
}
|
|
1630
|
+
const output = await pickupCommand({
|
|
1631
|
+
sessionId: id,
|
|
1632
|
+
auto: options.auto,
|
|
1633
|
+
sessionsDir: options.sessionsDir
|
|
1634
|
+
});
|
|
1635
|
+
console.log(output);
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
handleError(error);
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
sessionCmd.command("release [id]").description("Release a session (move from doing to todo)").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
|
|
1641
|
+
try {
|
|
1642
|
+
const output = await releaseCommand({
|
|
1643
|
+
sessionId: id,
|
|
1644
|
+
sessionsDir: options.sessionsDir
|
|
1645
|
+
});
|
|
1646
|
+
console.log(output);
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
handleError(error);
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
sessionCmd.command("handoff").description("Create a handoff session (reads content with frontmatter from stdin)").option("--sessions-dir <path>", "Custom sessions directory").addHelpText("after", HANDOFF_FRONTMATTER_HELP).action(async (options) => {
|
|
1652
|
+
try {
|
|
1653
|
+
const content = await readStdin();
|
|
1654
|
+
const output = await handoffCommand({
|
|
1655
|
+
content,
|
|
1656
|
+
sessionsDir: options.sessionsDir
|
|
1657
|
+
});
|
|
1658
|
+
console.log(output);
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
handleError(error);
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
sessionCmd.command("delete <id>").description("Delete a session").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
|
|
1664
|
+
try {
|
|
1665
|
+
const output = await deleteCommand({
|
|
1666
|
+
sessionId: id,
|
|
1667
|
+
sessionsDir: options.sessionsDir
|
|
1668
|
+
});
|
|
1669
|
+
console.log(output);
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
handleError(error);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
sessionCmd.command("prune").description("Remove old todo sessions, keeping the most recent N").option("--keep <count>", "Number of sessions to keep (default: 5)", "5").option("--dry-run", "Show what would be deleted without deleting").option("--sessions-dir <path>", "Custom sessions directory").action(async (options) => {
|
|
1675
|
+
try {
|
|
1676
|
+
const keep = options.keep ? Number.parseInt(options.keep, 10) : void 0;
|
|
1677
|
+
const output = await pruneCommand({
|
|
1678
|
+
keep,
|
|
1679
|
+
dryRun: options.dryRun,
|
|
1680
|
+
sessionsDir: options.sessionsDir
|
|
1681
|
+
});
|
|
1682
|
+
console.log(output);
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
if (error instanceof PruneValidationError) {
|
|
1685
|
+
console.error("Error:", error.message);
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
}
|
|
1688
|
+
handleError(error);
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
sessionCmd.command("archive <id>").description("Move a session to the archive directory").option("--sessions-dir <path>", "Custom sessions directory").action(async (id, options) => {
|
|
1692
|
+
try {
|
|
1693
|
+
const output = await archiveCommand({
|
|
1694
|
+
sessionId: id,
|
|
1695
|
+
sessionsDir: options.sessionsDir
|
|
1696
|
+
});
|
|
1697
|
+
console.log(output);
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
if (error instanceof SessionAlreadyArchivedError) {
|
|
1700
|
+
console.error("Error:", error.message);
|
|
1701
|
+
process.exit(1);
|
|
1702
|
+
}
|
|
1703
|
+
handleError(error);
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
var sessionDomain = {
|
|
1708
|
+
name: "session",
|
|
1709
|
+
description: "Manage session workflow",
|
|
1710
|
+
register: (program2) => {
|
|
1711
|
+
const sessionCmd = program2.command("session").description("Manage session workflow").addHelpText("after", SESSION_FORMAT_HELP);
|
|
1712
|
+
registerSessionCommands(sessionCmd);
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
// src/scanner/scanner.ts
|
|
1717
|
+
import path5 from "path";
|
|
1718
|
+
var Scanner = class {
|
|
1719
|
+
/**
|
|
1720
|
+
* Create a new Scanner instance
|
|
1721
|
+
*
|
|
1722
|
+
* @param projectRoot - Absolute path to the project root directory
|
|
1723
|
+
* @param config - Configuration object defining directory structure
|
|
1724
|
+
*/
|
|
1725
|
+
constructor(projectRoot, config) {
|
|
1726
|
+
this.projectRoot = projectRoot;
|
|
1727
|
+
this.config = config;
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Scan the project for work items in the "doing" status directory
|
|
1731
|
+
*
|
|
1732
|
+
* Walks the configured specs/work/doing directory, filters for valid
|
|
1733
|
+
* work item directories, and returns structured work item data.
|
|
1734
|
+
*
|
|
1735
|
+
* @returns Array of work items found in the doing directory
|
|
1736
|
+
* @throws Error if the directory doesn't exist or is inaccessible
|
|
1737
|
+
*/
|
|
1738
|
+
async scan() {
|
|
1739
|
+
const doingPath = this.getDoingPath();
|
|
1740
|
+
const allEntries = await walkDirectory(doingPath);
|
|
1741
|
+
const workItemEntries = filterWorkItemDirectories(allEntries);
|
|
1742
|
+
return buildWorkItemList(workItemEntries);
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Get the full path to the "doing" status directory
|
|
1746
|
+
*
|
|
1747
|
+
* Constructs path from config: {projectRoot}/{specs.root}/{work.dir}/{statusDirs.doing}
|
|
1748
|
+
*
|
|
1749
|
+
* @returns Absolute path to the doing directory
|
|
1750
|
+
*/
|
|
1751
|
+
getDoingPath() {
|
|
1752
|
+
return path5.join(
|
|
1753
|
+
this.projectRoot,
|
|
1754
|
+
this.config.specs.root,
|
|
1755
|
+
this.config.specs.work.dir,
|
|
1756
|
+
this.config.specs.work.statusDirs.doing
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Get the full path to the "backlog" status directory
|
|
1761
|
+
*
|
|
1762
|
+
* @returns Absolute path to the backlog directory
|
|
1763
|
+
*/
|
|
1764
|
+
getBacklogPath() {
|
|
1765
|
+
return path5.join(
|
|
1766
|
+
this.projectRoot,
|
|
1767
|
+
this.config.specs.root,
|
|
1768
|
+
this.config.specs.work.dir,
|
|
1769
|
+
this.config.specs.work.statusDirs.backlog
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Get the full path to the "done" status directory
|
|
1774
|
+
*
|
|
1775
|
+
* @returns Absolute path to the done/archive directory
|
|
1776
|
+
*/
|
|
1777
|
+
getDonePath() {
|
|
1778
|
+
return path5.join(
|
|
1779
|
+
this.projectRoot,
|
|
1780
|
+
this.config.specs.root,
|
|
1781
|
+
this.config.specs.work.dir,
|
|
1782
|
+
this.config.specs.work.statusDirs.done
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Get the full path to the specs root directory
|
|
1787
|
+
*
|
|
1788
|
+
* @returns Absolute path to the specs root
|
|
1789
|
+
*/
|
|
1790
|
+
getSpecsRootPath() {
|
|
1791
|
+
return path5.join(this.projectRoot, this.config.specs.root);
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Get the full path to the work directory
|
|
1795
|
+
*
|
|
1796
|
+
* @returns Absolute path to the work directory
|
|
1797
|
+
*/
|
|
1798
|
+
getWorkPath() {
|
|
1799
|
+
return path5.join(
|
|
1800
|
+
this.projectRoot,
|
|
1801
|
+
this.config.specs.root,
|
|
1802
|
+
this.config.specs.work.dir
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1807
|
+
// src/status/state.ts
|
|
1808
|
+
import { access, readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
1809
|
+
import path6 from "path";
|
|
1810
|
+
function determineStatus(flags) {
|
|
1811
|
+
if (!flags.hasTestsDir) {
|
|
1812
|
+
return "OPEN";
|
|
1813
|
+
}
|
|
1814
|
+
if (flags.hasDoneMd) {
|
|
1815
|
+
return "DONE";
|
|
1816
|
+
}
|
|
1817
|
+
if (flags.testsIsEmpty) {
|
|
1818
|
+
return "OPEN";
|
|
1819
|
+
}
|
|
1820
|
+
return "IN_PROGRESS";
|
|
1821
|
+
}
|
|
1822
|
+
var StatusDeterminationError = class extends Error {
|
|
1823
|
+
constructor(workItemPath, cause) {
|
|
1824
|
+
const errorMessage = cause instanceof Error ? cause.message : String(cause);
|
|
1825
|
+
super(`Failed to determine status for ${workItemPath}: ${errorMessage}`);
|
|
1826
|
+
this.workItemPath = workItemPath;
|
|
1827
|
+
this.cause = cause;
|
|
1828
|
+
this.name = "StatusDeterminationError";
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
async function getWorkItemStatus(workItemPath) {
|
|
1832
|
+
try {
|
|
1833
|
+
try {
|
|
1834
|
+
await access(workItemPath);
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
if (error.code === "ENOENT") {
|
|
1837
|
+
throw new Error(`Work item not found: ${workItemPath}`);
|
|
1838
|
+
}
|
|
1839
|
+
throw error;
|
|
1840
|
+
}
|
|
1841
|
+
const testsPath = path6.join(workItemPath, "tests");
|
|
1842
|
+
let hasTests;
|
|
1843
|
+
try {
|
|
1844
|
+
await access(testsPath);
|
|
1845
|
+
hasTests = true;
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
if (error.code === "ENOENT") {
|
|
1848
|
+
hasTests = false;
|
|
1849
|
+
} else {
|
|
1850
|
+
throw error;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (!hasTests) {
|
|
1854
|
+
return determineStatus({
|
|
1855
|
+
hasTestsDir: false,
|
|
1856
|
+
hasDoneMd: false,
|
|
1857
|
+
testsIsEmpty: true
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
const entries = await readdir5(testsPath);
|
|
1861
|
+
const hasDone = entries.includes("DONE.md");
|
|
1862
|
+
if (hasDone) {
|
|
1863
|
+
const donePath = path6.join(testsPath, "DONE.md");
|
|
1864
|
+
const stats = await stat4(donePath);
|
|
1865
|
+
if (!stats.isFile()) {
|
|
1866
|
+
return determineStatus({
|
|
1867
|
+
hasTestsDir: true,
|
|
1868
|
+
hasDoneMd: false,
|
|
1869
|
+
testsIsEmpty: isEmptyFromEntries(entries)
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
const isEmpty = isEmptyFromEntries(entries);
|
|
1874
|
+
return determineStatus({
|
|
1875
|
+
hasTestsDir: true,
|
|
1876
|
+
hasDoneMd: hasDone,
|
|
1877
|
+
testsIsEmpty: isEmpty
|
|
1878
|
+
});
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
throw new StatusDeterminationError(workItemPath, error);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function isEmptyFromEntries(entries) {
|
|
1884
|
+
const testFiles = entries.filter((entry) => {
|
|
1885
|
+
if (entry === "DONE.md") {
|
|
1886
|
+
return false;
|
|
1887
|
+
}
|
|
1888
|
+
if (entry.startsWith(".")) {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
return true;
|
|
1892
|
+
});
|
|
1893
|
+
return testFiles.length === 0;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/tree/build.ts
|
|
1897
|
+
async function buildTree(workItems, deps = {}) {
|
|
1898
|
+
const getStatus = deps.getStatus || getWorkItemStatus;
|
|
1899
|
+
const itemsWithStatus = await Promise.all(
|
|
1900
|
+
workItems.map(async (item) => ({
|
|
1901
|
+
...item,
|
|
1902
|
+
status: await getStatus(item.path)
|
|
1903
|
+
}))
|
|
1904
|
+
);
|
|
1905
|
+
const capabilities = itemsWithStatus.filter(
|
|
1906
|
+
(item) => item.kind === "capability"
|
|
1907
|
+
);
|
|
1908
|
+
const features = itemsWithStatus.filter((item) => item.kind === "feature");
|
|
1909
|
+
const stories = itemsWithStatus.filter((item) => item.kind === "story");
|
|
1910
|
+
const storyNodes = stories.map((item) => createTreeNode(item, []));
|
|
1911
|
+
const featureNodes = features.map((item) => {
|
|
1912
|
+
const children = storyNodes.filter((story) => isChildOf(story.path, item.path)).sort((a, b) => a.number - b.number);
|
|
1913
|
+
return createTreeNode(item, children);
|
|
1914
|
+
});
|
|
1915
|
+
const capabilityNodes = capabilities.map((item) => {
|
|
1916
|
+
const children = featureNodes.filter((feature) => isChildOf(feature.path, item.path)).sort((a, b) => a.number - b.number);
|
|
1917
|
+
return createTreeNode(item, children);
|
|
1918
|
+
});
|
|
1919
|
+
detectOrphans(stories, featureNodes);
|
|
1920
|
+
detectOrphans(features, capabilityNodes);
|
|
1921
|
+
const sortedCapabilities = capabilityNodes.sort((a, b) => a.number - b.number);
|
|
1922
|
+
rollupStatus(sortedCapabilities);
|
|
1923
|
+
return {
|
|
1924
|
+
nodes: sortedCapabilities
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function createTreeNode(item, children) {
|
|
1928
|
+
return {
|
|
1929
|
+
kind: item.kind,
|
|
1930
|
+
number: item.number,
|
|
1931
|
+
slug: item.slug,
|
|
1932
|
+
path: item.path,
|
|
1933
|
+
status: item.status,
|
|
1934
|
+
children
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function isChildOf(childPath, parentPath) {
|
|
1938
|
+
const normalizedChild = childPath.replace(/\/$/, "");
|
|
1939
|
+
const normalizedParent = parentPath.replace(/\/$/, "");
|
|
1940
|
+
if (!normalizedChild.startsWith(normalizedParent + "/")) {
|
|
1941
|
+
return false;
|
|
1942
|
+
}
|
|
1943
|
+
const relativePath = normalizedChild.slice(normalizedParent.length + 1);
|
|
1944
|
+
return !relativePath.includes("/");
|
|
1945
|
+
}
|
|
1946
|
+
function detectOrphans(items, potentialParents) {
|
|
1947
|
+
for (const item of items) {
|
|
1948
|
+
const hasParent = potentialParents.some((parent) => isChildOf(item.path, parent.path));
|
|
1949
|
+
if (!hasParent) {
|
|
1950
|
+
throw new Error(
|
|
1951
|
+
`Orphan work item detected: ${item.kind} "${item.slug}" at ${item.path} has no valid parent`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function rollupStatus(nodes) {
|
|
1957
|
+
for (const node of nodes) {
|
|
1958
|
+
if (node.children.length > 0) {
|
|
1959
|
+
rollupStatus(node.children);
|
|
1960
|
+
const ownStatus = node.status;
|
|
1961
|
+
const childStatuses = node.children.map((child) => child.status);
|
|
1962
|
+
const allChildrenDone = childStatuses.every(
|
|
1963
|
+
(status) => status === "DONE"
|
|
1964
|
+
);
|
|
1965
|
+
const allChildrenOpen = childStatuses.every(
|
|
1966
|
+
(status) => status === "OPEN"
|
|
1967
|
+
);
|
|
1968
|
+
if (ownStatus === "DONE" && allChildrenDone) {
|
|
1969
|
+
node.status = "DONE";
|
|
1970
|
+
} else if (ownStatus === "OPEN" && allChildrenOpen) {
|
|
1971
|
+
node.status = "OPEN";
|
|
1972
|
+
} else {
|
|
1973
|
+
node.status = "IN_PROGRESS";
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/commands/spec/next.ts
|
|
1980
|
+
function findNextWorkItem(tree) {
|
|
1981
|
+
return findFirstNonDoneLeaf(tree.nodes);
|
|
1982
|
+
}
|
|
1983
|
+
function findFirstNonDoneLeaf(nodes) {
|
|
1984
|
+
for (const node of nodes) {
|
|
1985
|
+
if (node.kind === LEAF_KIND) {
|
|
1986
|
+
if (node.status !== "DONE") {
|
|
1987
|
+
return node;
|
|
1988
|
+
}
|
|
1989
|
+
} else {
|
|
1990
|
+
const found = findFirstNonDoneLeaf(node.children);
|
|
1991
|
+
if (found) {
|
|
1992
|
+
return found;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
function formatWorkItemName(node) {
|
|
1999
|
+
const displayNum = node.kind === "capability" ? node.number + 1 : node.number;
|
|
2000
|
+
return `${node.kind}-${displayNum}_${node.slug}`;
|
|
2001
|
+
}
|
|
2002
|
+
async function nextCommand(options = {}) {
|
|
2003
|
+
const cwd = options.cwd || process.cwd();
|
|
2004
|
+
const scanner = new Scanner(cwd, DEFAULT_CONFIG);
|
|
2005
|
+
const workItems = await scanner.scan();
|
|
2006
|
+
if (workItems.length === 0) {
|
|
2007
|
+
return `No work items found in ${DEFAULT_CONFIG.specs.root}/${DEFAULT_CONFIG.specs.work.dir}/${DEFAULT_CONFIG.specs.work.statusDirs.doing}`;
|
|
2008
|
+
}
|
|
2009
|
+
const tree = await buildTree(workItems);
|
|
2010
|
+
const next = findNextWorkItem(tree);
|
|
2011
|
+
if (!next) {
|
|
2012
|
+
return "All work items are complete! \u{1F389}";
|
|
2013
|
+
}
|
|
2014
|
+
const parents = findParents(tree.nodes, next);
|
|
2015
|
+
const lines = [];
|
|
2016
|
+
lines.push("Next work item:");
|
|
2017
|
+
lines.push("");
|
|
2018
|
+
if (parents.capability && parents.feature) {
|
|
2019
|
+
lines.push(
|
|
2020
|
+
` ${formatWorkItemName(parents.capability)} > ${formatWorkItemName(parents.feature)} > ${formatWorkItemName(next)}`
|
|
2021
|
+
);
|
|
2022
|
+
} else {
|
|
2023
|
+
lines.push(` ${formatWorkItemName(next)}`);
|
|
2024
|
+
}
|
|
2025
|
+
lines.push("");
|
|
2026
|
+
lines.push(` Status: ${next.status}`);
|
|
2027
|
+
lines.push(` Path: ${next.path}`);
|
|
2028
|
+
return lines.join("\n");
|
|
2029
|
+
}
|
|
2030
|
+
function findParents(nodes, target) {
|
|
2031
|
+
for (const capability of nodes) {
|
|
2032
|
+
for (const feature of capability.children) {
|
|
2033
|
+
for (const story of feature.children) {
|
|
2034
|
+
if (story.path === target.path) {
|
|
2035
|
+
return { capability, feature };
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
return {};
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/reporter/json.ts
|
|
2044
|
+
var JSON_INDENT = 2;
|
|
2045
|
+
function formatJSON(tree, config) {
|
|
2046
|
+
const capabilities = tree.nodes.map((node) => nodeToJSON(node));
|
|
2047
|
+
const summary = calculateSummary(tree);
|
|
2048
|
+
const output = {
|
|
2049
|
+
config: {
|
|
2050
|
+
specs: config.specs,
|
|
2051
|
+
sessions: config.sessions
|
|
2052
|
+
},
|
|
2053
|
+
summary,
|
|
2054
|
+
capabilities
|
|
2055
|
+
};
|
|
2056
|
+
return JSON.stringify(output, null, JSON_INDENT);
|
|
2057
|
+
}
|
|
2058
|
+
function nodeToJSON(node) {
|
|
2059
|
+
const displayNumber = getDisplayNumber(node);
|
|
2060
|
+
const base = {
|
|
2061
|
+
kind: node.kind,
|
|
2062
|
+
number: displayNumber,
|
|
2063
|
+
slug: node.slug,
|
|
2064
|
+
status: node.status
|
|
2065
|
+
};
|
|
2066
|
+
if (node.kind === "capability") {
|
|
2067
|
+
return {
|
|
2068
|
+
...base,
|
|
2069
|
+
features: node.children.map((child) => nodeToJSON(child))
|
|
2070
|
+
};
|
|
2071
|
+
} else if (node.kind === "feature") {
|
|
2072
|
+
return {
|
|
2073
|
+
...base,
|
|
2074
|
+
stories: node.children.map((child) => nodeToJSON(child))
|
|
2075
|
+
};
|
|
2076
|
+
} else {
|
|
2077
|
+
return base;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function calculateSummary(tree) {
|
|
2081
|
+
const summary = {
|
|
2082
|
+
done: 0,
|
|
2083
|
+
inProgress: 0,
|
|
2084
|
+
open: 0
|
|
2085
|
+
};
|
|
2086
|
+
for (const capability of tree.nodes) {
|
|
2087
|
+
countNode(capability, summary);
|
|
2088
|
+
for (const feature of capability.children) {
|
|
2089
|
+
countNode(feature, summary);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return summary;
|
|
2093
|
+
}
|
|
2094
|
+
function countNode(node, summary) {
|
|
2095
|
+
switch (node.status) {
|
|
2096
|
+
case "DONE":
|
|
2097
|
+
summary.done++;
|
|
2098
|
+
break;
|
|
2099
|
+
case "IN_PROGRESS":
|
|
2100
|
+
summary.inProgress++;
|
|
2101
|
+
break;
|
|
2102
|
+
case "OPEN":
|
|
2103
|
+
summary.open++;
|
|
2104
|
+
break;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function getDisplayNumber(node) {
|
|
2108
|
+
return node.kind === "capability" ? node.number + 1 : node.number;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// src/reporter/markdown.ts
|
|
2112
|
+
function formatMarkdown(tree) {
|
|
2113
|
+
const sections = [];
|
|
2114
|
+
for (const node of tree.nodes) {
|
|
2115
|
+
formatNode(node, 1, sections);
|
|
2116
|
+
}
|
|
2117
|
+
return sections.join("\n\n");
|
|
2118
|
+
}
|
|
2119
|
+
function formatNode(node, level, sections) {
|
|
2120
|
+
const displayNumber = getDisplayNumber2(node);
|
|
2121
|
+
const name = `${node.kind}-${displayNumber}_${node.slug}`;
|
|
2122
|
+
const heading = "#".repeat(level);
|
|
2123
|
+
sections.push(`${heading} ${name}`);
|
|
2124
|
+
sections.push(`Status: ${node.status}`);
|
|
2125
|
+
for (const child of node.children) {
|
|
2126
|
+
formatNode(child, level + 1, sections);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
function getDisplayNumber2(node) {
|
|
2130
|
+
return node.kind === "capability" ? node.number + 1 : node.number;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// src/reporter/table.ts
|
|
2134
|
+
function formatTable(tree) {
|
|
2135
|
+
const rows = [];
|
|
2136
|
+
for (const node of tree.nodes) {
|
|
2137
|
+
collectRows(node, 0, rows);
|
|
2138
|
+
}
|
|
2139
|
+
const widths = calculateColumnWidths(rows);
|
|
2140
|
+
const lines = [];
|
|
2141
|
+
lines.push(
|
|
2142
|
+
formatRow(
|
|
2143
|
+
{
|
|
2144
|
+
level: "Level",
|
|
2145
|
+
number: "Number",
|
|
2146
|
+
name: "Name",
|
|
2147
|
+
status: "Status"
|
|
2148
|
+
},
|
|
2149
|
+
widths
|
|
2150
|
+
)
|
|
2151
|
+
);
|
|
2152
|
+
lines.push(
|
|
2153
|
+
`|${"-".repeat(widths.level + 2)}|${"-".repeat(widths.number + 2)}|${"-".repeat(widths.name + 2)}|${"-".repeat(widths.status + 2)}|`
|
|
2154
|
+
);
|
|
2155
|
+
for (const row of rows) {
|
|
2156
|
+
lines.push(formatRow(row, widths));
|
|
2157
|
+
}
|
|
2158
|
+
return lines.join("\n");
|
|
2159
|
+
}
|
|
2160
|
+
function collectRows(node, depth, rows) {
|
|
2161
|
+
const indent = " ".repeat(depth);
|
|
2162
|
+
const levelName = getLevelName(node.kind);
|
|
2163
|
+
const displayNumber = getDisplayNumber3(node);
|
|
2164
|
+
rows.push({
|
|
2165
|
+
level: `${indent}${levelName}`,
|
|
2166
|
+
number: String(displayNumber),
|
|
2167
|
+
name: node.slug,
|
|
2168
|
+
status: node.status
|
|
2169
|
+
});
|
|
2170
|
+
for (const child of node.children) {
|
|
2171
|
+
collectRows(child, depth + 1, rows);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
function getLevelName(kind) {
|
|
2175
|
+
return kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
2176
|
+
}
|
|
2177
|
+
function getDisplayNumber3(node) {
|
|
2178
|
+
return node.kind === "capability" ? node.number + 1 : node.number;
|
|
2179
|
+
}
|
|
2180
|
+
function calculateColumnWidths(rows) {
|
|
2181
|
+
const widths = {
|
|
2182
|
+
level: "Level".length,
|
|
2183
|
+
number: "Number".length,
|
|
2184
|
+
name: "Name".length,
|
|
2185
|
+
status: "Status".length
|
|
2186
|
+
};
|
|
2187
|
+
for (const row of rows) {
|
|
2188
|
+
widths.level = Math.max(widths.level, row.level.length);
|
|
2189
|
+
widths.number = Math.max(widths.number, row.number.length);
|
|
2190
|
+
widths.name = Math.max(widths.name, row.name.length);
|
|
2191
|
+
widths.status = Math.max(widths.status, row.status.length);
|
|
2192
|
+
}
|
|
2193
|
+
return widths;
|
|
2194
|
+
}
|
|
2195
|
+
function formatRow(row, widths) {
|
|
2196
|
+
return `| ${row.level.padEnd(widths.level)} | ${row.number.padEnd(widths.number)} | ${row.name.padEnd(widths.name)} | ${row.status.padEnd(widths.status)} |`;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/reporter/text.ts
|
|
2200
|
+
import chalk from "chalk";
|
|
2201
|
+
function formatText(tree) {
|
|
2202
|
+
const lines = [];
|
|
2203
|
+
for (const node of tree.nodes) {
|
|
2204
|
+
lines.push(formatNode2(node, 0));
|
|
2205
|
+
}
|
|
2206
|
+
return lines.join("\n");
|
|
2207
|
+
}
|
|
2208
|
+
function formatNode2(node, indent) {
|
|
2209
|
+
const lines = [];
|
|
2210
|
+
const name = formatWorkItemName2(node.kind, node.number, node.slug);
|
|
2211
|
+
const prefix = " ".repeat(indent);
|
|
2212
|
+
const status = formatStatus(node.status);
|
|
2213
|
+
const line = `${prefix}${name} ${status}`;
|
|
2214
|
+
lines.push(line);
|
|
2215
|
+
for (const child of node.children) {
|
|
2216
|
+
lines.push(formatNode2(child, indent + 2));
|
|
2217
|
+
}
|
|
2218
|
+
return lines.join("\n");
|
|
2219
|
+
}
|
|
2220
|
+
function formatWorkItemName2(kind, number, slug) {
|
|
2221
|
+
const displayNumber = kind === "capability" ? number + 1 : number;
|
|
2222
|
+
return `${kind}-${displayNumber}_${slug}`;
|
|
2223
|
+
}
|
|
2224
|
+
function formatStatus(status) {
|
|
2225
|
+
switch (status) {
|
|
2226
|
+
case "DONE":
|
|
2227
|
+
return chalk.green(`[${status}]`);
|
|
2228
|
+
case "IN_PROGRESS":
|
|
2229
|
+
return chalk.yellow(`[${status}]`);
|
|
2230
|
+
case "OPEN":
|
|
2231
|
+
return chalk.gray(`[${status}]`);
|
|
2232
|
+
default:
|
|
2233
|
+
return `[${status}]`;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
// src/commands/spec/status.ts
|
|
2238
|
+
async function statusCommand(options = {}) {
|
|
2239
|
+
const cwd = options.cwd || process.cwd();
|
|
2240
|
+
const format2 = options.format || "text";
|
|
2241
|
+
const scanner = new Scanner(cwd, DEFAULT_CONFIG);
|
|
2242
|
+
let workItems;
|
|
2243
|
+
try {
|
|
2244
|
+
workItems = await scanner.scan();
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
2247
|
+
const doingPath = `${DEFAULT_CONFIG.specs.root}/${DEFAULT_CONFIG.specs.work.dir}/${DEFAULT_CONFIG.specs.work.statusDirs.doing}`;
|
|
2248
|
+
throw new Error(
|
|
2249
|
+
`Directory ${doingPath} not found.
|
|
2250
|
+
|
|
2251
|
+
This command is for legacy specs/ projects. For CODE framework projects, check the spx/ directory for specifications.`
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
throw error;
|
|
2255
|
+
}
|
|
2256
|
+
if (workItems.length === 0) {
|
|
2257
|
+
return `No work items found in ${DEFAULT_CONFIG.specs.root}/${DEFAULT_CONFIG.specs.work.dir}/${DEFAULT_CONFIG.specs.work.statusDirs.doing}`;
|
|
2258
|
+
}
|
|
2259
|
+
const tree = await buildTree(workItems);
|
|
2260
|
+
switch (format2) {
|
|
2261
|
+
case "json":
|
|
2262
|
+
return formatJSON(tree, DEFAULT_CONFIG);
|
|
2263
|
+
case "markdown":
|
|
2264
|
+
return formatMarkdown(tree);
|
|
2265
|
+
case "table":
|
|
2266
|
+
return formatTable(tree);
|
|
2267
|
+
case "text":
|
|
2268
|
+
default:
|
|
2269
|
+
return formatText(tree);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/domains/spec/index.ts
|
|
2274
|
+
function registerSpecCommands(specCmd) {
|
|
2275
|
+
specCmd.command("status").description("Get project status").option("--json", "Output as JSON").option("--format <format>", "Output format (text|json|markdown|table)").action(async (options) => {
|
|
2276
|
+
try {
|
|
2277
|
+
let format2 = "text";
|
|
2278
|
+
if (options.json) {
|
|
2279
|
+
format2 = "json";
|
|
2280
|
+
} else if (options.format) {
|
|
2281
|
+
const validFormats = ["text", "json", "markdown", "table"];
|
|
2282
|
+
if (validFormats.includes(options.format)) {
|
|
2283
|
+
format2 = options.format;
|
|
2284
|
+
} else {
|
|
2285
|
+
console.error(
|
|
2286
|
+
`Error: Invalid format "${options.format}". Must be one of: ${validFormats.join(", ")}`
|
|
2287
|
+
);
|
|
2288
|
+
process.exit(1);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const output = await statusCommand({ cwd: process.cwd(), format: format2 });
|
|
2292
|
+
console.log(output);
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
console.error(
|
|
2295
|
+
"Error:",
|
|
2296
|
+
error instanceof Error ? error.message : String(error)
|
|
2297
|
+
);
|
|
2298
|
+
process.exit(1);
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
specCmd.command("next").description("Find next work item to work on").action(async () => {
|
|
2302
|
+
try {
|
|
2303
|
+
const output = await nextCommand({ cwd: process.cwd() });
|
|
2304
|
+
console.log(output);
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
console.error(
|
|
2307
|
+
"Error:",
|
|
2308
|
+
error instanceof Error ? error.message : String(error)
|
|
2309
|
+
);
|
|
2310
|
+
process.exit(1);
|
|
2311
|
+
}
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
var specDomain = {
|
|
2315
|
+
name: "spec",
|
|
2316
|
+
description: "Manage spec workflow",
|
|
2317
|
+
register: (program2) => {
|
|
2318
|
+
const specCmd = program2.command("spec").description("Manage spec workflow");
|
|
2319
|
+
registerSpecCommands(specCmd);
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2323
|
+
// node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
2324
|
+
function createScanner(text, ignoreTrivia = false) {
|
|
2325
|
+
const len = text.length;
|
|
2326
|
+
let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
|
|
2327
|
+
function scanHexDigits(count, exact) {
|
|
2328
|
+
let digits = 0;
|
|
2329
|
+
let value2 = 0;
|
|
2330
|
+
while (digits < count || !exact) {
|
|
2331
|
+
let ch = text.charCodeAt(pos);
|
|
2332
|
+
if (ch >= 48 && ch <= 57) {
|
|
2333
|
+
value2 = value2 * 16 + ch - 48;
|
|
2334
|
+
} else if (ch >= 65 && ch <= 70) {
|
|
2335
|
+
value2 = value2 * 16 + ch - 65 + 10;
|
|
2336
|
+
} else if (ch >= 97 && ch <= 102) {
|
|
2337
|
+
value2 = value2 * 16 + ch - 97 + 10;
|
|
2338
|
+
} else {
|
|
2339
|
+
break;
|
|
2340
|
+
}
|
|
2341
|
+
pos++;
|
|
2342
|
+
digits++;
|
|
2343
|
+
}
|
|
2344
|
+
if (digits < count) {
|
|
2345
|
+
value2 = -1;
|
|
2346
|
+
}
|
|
2347
|
+
return value2;
|
|
2348
|
+
}
|
|
2349
|
+
function setPosition(newPosition) {
|
|
2350
|
+
pos = newPosition;
|
|
2351
|
+
value = "";
|
|
2352
|
+
tokenOffset = 0;
|
|
2353
|
+
token = 16;
|
|
2354
|
+
scanError = 0;
|
|
2355
|
+
}
|
|
2356
|
+
function scanNumber() {
|
|
2357
|
+
let start = pos;
|
|
2358
|
+
if (text.charCodeAt(pos) === 48) {
|
|
2359
|
+
pos++;
|
|
2360
|
+
} else {
|
|
2361
|
+
pos++;
|
|
2362
|
+
while (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
2363
|
+
pos++;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (pos < text.length && text.charCodeAt(pos) === 46) {
|
|
2367
|
+
pos++;
|
|
2368
|
+
if (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
2369
|
+
pos++;
|
|
2370
|
+
while (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
2371
|
+
pos++;
|
|
2372
|
+
}
|
|
2373
|
+
} else {
|
|
2374
|
+
scanError = 3;
|
|
2375
|
+
return text.substring(start, pos);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
let end = pos;
|
|
2379
|
+
if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) {
|
|
2380
|
+
pos++;
|
|
2381
|
+
if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) {
|
|
2382
|
+
pos++;
|
|
2383
|
+
}
|
|
2384
|
+
if (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
2385
|
+
pos++;
|
|
2386
|
+
while (pos < text.length && isDigit(text.charCodeAt(pos))) {
|
|
2387
|
+
pos++;
|
|
2388
|
+
}
|
|
2389
|
+
end = pos;
|
|
2390
|
+
} else {
|
|
2391
|
+
scanError = 3;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
return text.substring(start, end);
|
|
2395
|
+
}
|
|
2396
|
+
function scanString() {
|
|
2397
|
+
let result = "", start = pos;
|
|
2398
|
+
while (true) {
|
|
2399
|
+
if (pos >= len) {
|
|
2400
|
+
result += text.substring(start, pos);
|
|
2401
|
+
scanError = 2;
|
|
2402
|
+
break;
|
|
2403
|
+
}
|
|
2404
|
+
const ch = text.charCodeAt(pos);
|
|
2405
|
+
if (ch === 34) {
|
|
2406
|
+
result += text.substring(start, pos);
|
|
2407
|
+
pos++;
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
if (ch === 92) {
|
|
2411
|
+
result += text.substring(start, pos);
|
|
2412
|
+
pos++;
|
|
2413
|
+
if (pos >= len) {
|
|
2414
|
+
scanError = 2;
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
const ch2 = text.charCodeAt(pos++);
|
|
2418
|
+
switch (ch2) {
|
|
2419
|
+
case 34:
|
|
2420
|
+
result += '"';
|
|
2421
|
+
break;
|
|
2422
|
+
case 92:
|
|
2423
|
+
result += "\\";
|
|
2424
|
+
break;
|
|
2425
|
+
case 47:
|
|
2426
|
+
result += "/";
|
|
2427
|
+
break;
|
|
2428
|
+
case 98:
|
|
2429
|
+
result += "\b";
|
|
2430
|
+
break;
|
|
2431
|
+
case 102:
|
|
2432
|
+
result += "\f";
|
|
2433
|
+
break;
|
|
2434
|
+
case 110:
|
|
2435
|
+
result += "\n";
|
|
2436
|
+
break;
|
|
2437
|
+
case 114:
|
|
2438
|
+
result += "\r";
|
|
2439
|
+
break;
|
|
2440
|
+
case 116:
|
|
2441
|
+
result += " ";
|
|
2442
|
+
break;
|
|
2443
|
+
case 117:
|
|
2444
|
+
const ch3 = scanHexDigits(4, true);
|
|
2445
|
+
if (ch3 >= 0) {
|
|
2446
|
+
result += String.fromCharCode(ch3);
|
|
2447
|
+
} else {
|
|
2448
|
+
scanError = 4;
|
|
2449
|
+
}
|
|
2450
|
+
break;
|
|
2451
|
+
default:
|
|
2452
|
+
scanError = 5;
|
|
2453
|
+
}
|
|
2454
|
+
start = pos;
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
if (ch >= 0 && ch <= 31) {
|
|
2458
|
+
if (isLineBreak(ch)) {
|
|
2459
|
+
result += text.substring(start, pos);
|
|
2460
|
+
scanError = 2;
|
|
2461
|
+
break;
|
|
2462
|
+
} else {
|
|
2463
|
+
scanError = 6;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
pos++;
|
|
2467
|
+
}
|
|
2468
|
+
return result;
|
|
2469
|
+
}
|
|
2470
|
+
function scanNext() {
|
|
2471
|
+
value = "";
|
|
2472
|
+
scanError = 0;
|
|
2473
|
+
tokenOffset = pos;
|
|
2474
|
+
lineStartOffset = lineNumber;
|
|
2475
|
+
prevTokenLineStartOffset = tokenLineStartOffset;
|
|
2476
|
+
if (pos >= len) {
|
|
2477
|
+
tokenOffset = len;
|
|
2478
|
+
return token = 17;
|
|
2479
|
+
}
|
|
2480
|
+
let code = text.charCodeAt(pos);
|
|
2481
|
+
if (isWhiteSpace(code)) {
|
|
2482
|
+
do {
|
|
2483
|
+
pos++;
|
|
2484
|
+
value += String.fromCharCode(code);
|
|
2485
|
+
code = text.charCodeAt(pos);
|
|
2486
|
+
} while (isWhiteSpace(code));
|
|
2487
|
+
return token = 15;
|
|
2488
|
+
}
|
|
2489
|
+
if (isLineBreak(code)) {
|
|
2490
|
+
pos++;
|
|
2491
|
+
value += String.fromCharCode(code);
|
|
2492
|
+
if (code === 13 && text.charCodeAt(pos) === 10) {
|
|
2493
|
+
pos++;
|
|
2494
|
+
value += "\n";
|
|
2495
|
+
}
|
|
2496
|
+
lineNumber++;
|
|
2497
|
+
tokenLineStartOffset = pos;
|
|
2498
|
+
return token = 14;
|
|
2499
|
+
}
|
|
2500
|
+
switch (code) {
|
|
2501
|
+
// tokens: []{}:,
|
|
2502
|
+
case 123:
|
|
2503
|
+
pos++;
|
|
2504
|
+
return token = 1;
|
|
2505
|
+
case 125:
|
|
2506
|
+
pos++;
|
|
2507
|
+
return token = 2;
|
|
2508
|
+
case 91:
|
|
2509
|
+
pos++;
|
|
2510
|
+
return token = 3;
|
|
2511
|
+
case 93:
|
|
2512
|
+
pos++;
|
|
2513
|
+
return token = 4;
|
|
2514
|
+
case 58:
|
|
2515
|
+
pos++;
|
|
2516
|
+
return token = 6;
|
|
2517
|
+
case 44:
|
|
2518
|
+
pos++;
|
|
2519
|
+
return token = 5;
|
|
2520
|
+
// strings
|
|
2521
|
+
case 34:
|
|
2522
|
+
pos++;
|
|
2523
|
+
value = scanString();
|
|
2524
|
+
return token = 10;
|
|
2525
|
+
// comments
|
|
2526
|
+
case 47:
|
|
2527
|
+
const start = pos - 1;
|
|
2528
|
+
if (text.charCodeAt(pos + 1) === 47) {
|
|
2529
|
+
pos += 2;
|
|
2530
|
+
while (pos < len) {
|
|
2531
|
+
if (isLineBreak(text.charCodeAt(pos))) {
|
|
2532
|
+
break;
|
|
2533
|
+
}
|
|
2534
|
+
pos++;
|
|
2535
|
+
}
|
|
2536
|
+
value = text.substring(start, pos);
|
|
2537
|
+
return token = 12;
|
|
2538
|
+
}
|
|
2539
|
+
if (text.charCodeAt(pos + 1) === 42) {
|
|
2540
|
+
pos += 2;
|
|
2541
|
+
const safeLength = len - 1;
|
|
2542
|
+
let commentClosed = false;
|
|
2543
|
+
while (pos < safeLength) {
|
|
2544
|
+
const ch = text.charCodeAt(pos);
|
|
2545
|
+
if (ch === 42 && text.charCodeAt(pos + 1) === 47) {
|
|
2546
|
+
pos += 2;
|
|
2547
|
+
commentClosed = true;
|
|
2548
|
+
break;
|
|
2549
|
+
}
|
|
2550
|
+
pos++;
|
|
2551
|
+
if (isLineBreak(ch)) {
|
|
2552
|
+
if (ch === 13 && text.charCodeAt(pos) === 10) {
|
|
2553
|
+
pos++;
|
|
2554
|
+
}
|
|
2555
|
+
lineNumber++;
|
|
2556
|
+
tokenLineStartOffset = pos;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (!commentClosed) {
|
|
2560
|
+
pos++;
|
|
2561
|
+
scanError = 1;
|
|
2562
|
+
}
|
|
2563
|
+
value = text.substring(start, pos);
|
|
2564
|
+
return token = 13;
|
|
2565
|
+
}
|
|
2566
|
+
value += String.fromCharCode(code);
|
|
2567
|
+
pos++;
|
|
2568
|
+
return token = 16;
|
|
2569
|
+
// numbers
|
|
2570
|
+
case 45:
|
|
2571
|
+
value += String.fromCharCode(code);
|
|
2572
|
+
pos++;
|
|
2573
|
+
if (pos === len || !isDigit(text.charCodeAt(pos))) {
|
|
2574
|
+
return token = 16;
|
|
2575
|
+
}
|
|
2576
|
+
// found a minus, followed by a number so
|
|
2577
|
+
// we fall through to proceed with scanning
|
|
2578
|
+
// numbers
|
|
2579
|
+
case 48:
|
|
2580
|
+
case 49:
|
|
2581
|
+
case 50:
|
|
2582
|
+
case 51:
|
|
2583
|
+
case 52:
|
|
2584
|
+
case 53:
|
|
2585
|
+
case 54:
|
|
2586
|
+
case 55:
|
|
2587
|
+
case 56:
|
|
2588
|
+
case 57:
|
|
2589
|
+
value += scanNumber();
|
|
2590
|
+
return token = 11;
|
|
2591
|
+
// literals and unknown symbols
|
|
2592
|
+
default:
|
|
2593
|
+
while (pos < len && isUnknownContentCharacter(code)) {
|
|
2594
|
+
pos++;
|
|
2595
|
+
code = text.charCodeAt(pos);
|
|
2596
|
+
}
|
|
2597
|
+
if (tokenOffset !== pos) {
|
|
2598
|
+
value = text.substring(tokenOffset, pos);
|
|
2599
|
+
switch (value) {
|
|
2600
|
+
case "true":
|
|
2601
|
+
return token = 8;
|
|
2602
|
+
case "false":
|
|
2603
|
+
return token = 9;
|
|
2604
|
+
case "null":
|
|
2605
|
+
return token = 7;
|
|
2606
|
+
}
|
|
2607
|
+
return token = 16;
|
|
2608
|
+
}
|
|
2609
|
+
value += String.fromCharCode(code);
|
|
2610
|
+
pos++;
|
|
2611
|
+
return token = 16;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
function isUnknownContentCharacter(code) {
|
|
2615
|
+
if (isWhiteSpace(code) || isLineBreak(code)) {
|
|
2616
|
+
return false;
|
|
2617
|
+
}
|
|
2618
|
+
switch (code) {
|
|
2619
|
+
case 125:
|
|
2620
|
+
case 93:
|
|
2621
|
+
case 123:
|
|
2622
|
+
case 91:
|
|
2623
|
+
case 34:
|
|
2624
|
+
case 58:
|
|
2625
|
+
case 44:
|
|
2626
|
+
case 47:
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2629
|
+
return true;
|
|
2630
|
+
}
|
|
2631
|
+
function scanNextNonTrivia() {
|
|
2632
|
+
let result;
|
|
2633
|
+
do {
|
|
2634
|
+
result = scanNext();
|
|
2635
|
+
} while (result >= 12 && result <= 15);
|
|
2636
|
+
return result;
|
|
2637
|
+
}
|
|
2638
|
+
return {
|
|
2639
|
+
setPosition,
|
|
2640
|
+
getPosition: () => pos,
|
|
2641
|
+
scan: ignoreTrivia ? scanNextNonTrivia : scanNext,
|
|
2642
|
+
getToken: () => token,
|
|
2643
|
+
getTokenValue: () => value,
|
|
2644
|
+
getTokenOffset: () => tokenOffset,
|
|
2645
|
+
getTokenLength: () => pos - tokenOffset,
|
|
2646
|
+
getTokenStartLine: () => lineStartOffset,
|
|
2647
|
+
getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset,
|
|
2648
|
+
getTokenError: () => scanError
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
function isWhiteSpace(ch) {
|
|
2652
|
+
return ch === 32 || ch === 9;
|
|
2653
|
+
}
|
|
2654
|
+
function isLineBreak(ch) {
|
|
2655
|
+
return ch === 10 || ch === 13;
|
|
2656
|
+
}
|
|
2657
|
+
function isDigit(ch) {
|
|
2658
|
+
return ch >= 48 && ch <= 57;
|
|
2659
|
+
}
|
|
2660
|
+
var CharacterCodes;
|
|
2661
|
+
(function(CharacterCodes2) {
|
|
2662
|
+
CharacterCodes2[CharacterCodes2["lineFeed"] = 10] = "lineFeed";
|
|
2663
|
+
CharacterCodes2[CharacterCodes2["carriageReturn"] = 13] = "carriageReturn";
|
|
2664
|
+
CharacterCodes2[CharacterCodes2["space"] = 32] = "space";
|
|
2665
|
+
CharacterCodes2[CharacterCodes2["_0"] = 48] = "_0";
|
|
2666
|
+
CharacterCodes2[CharacterCodes2["_1"] = 49] = "_1";
|
|
2667
|
+
CharacterCodes2[CharacterCodes2["_2"] = 50] = "_2";
|
|
2668
|
+
CharacterCodes2[CharacterCodes2["_3"] = 51] = "_3";
|
|
2669
|
+
CharacterCodes2[CharacterCodes2["_4"] = 52] = "_4";
|
|
2670
|
+
CharacterCodes2[CharacterCodes2["_5"] = 53] = "_5";
|
|
2671
|
+
CharacterCodes2[CharacterCodes2["_6"] = 54] = "_6";
|
|
2672
|
+
CharacterCodes2[CharacterCodes2["_7"] = 55] = "_7";
|
|
2673
|
+
CharacterCodes2[CharacterCodes2["_8"] = 56] = "_8";
|
|
2674
|
+
CharacterCodes2[CharacterCodes2["_9"] = 57] = "_9";
|
|
2675
|
+
CharacterCodes2[CharacterCodes2["a"] = 97] = "a";
|
|
2676
|
+
CharacterCodes2[CharacterCodes2["b"] = 98] = "b";
|
|
2677
|
+
CharacterCodes2[CharacterCodes2["c"] = 99] = "c";
|
|
2678
|
+
CharacterCodes2[CharacterCodes2["d"] = 100] = "d";
|
|
2679
|
+
CharacterCodes2[CharacterCodes2["e"] = 101] = "e";
|
|
2680
|
+
CharacterCodes2[CharacterCodes2["f"] = 102] = "f";
|
|
2681
|
+
CharacterCodes2[CharacterCodes2["g"] = 103] = "g";
|
|
2682
|
+
CharacterCodes2[CharacterCodes2["h"] = 104] = "h";
|
|
2683
|
+
CharacterCodes2[CharacterCodes2["i"] = 105] = "i";
|
|
2684
|
+
CharacterCodes2[CharacterCodes2["j"] = 106] = "j";
|
|
2685
|
+
CharacterCodes2[CharacterCodes2["k"] = 107] = "k";
|
|
2686
|
+
CharacterCodes2[CharacterCodes2["l"] = 108] = "l";
|
|
2687
|
+
CharacterCodes2[CharacterCodes2["m"] = 109] = "m";
|
|
2688
|
+
CharacterCodes2[CharacterCodes2["n"] = 110] = "n";
|
|
2689
|
+
CharacterCodes2[CharacterCodes2["o"] = 111] = "o";
|
|
2690
|
+
CharacterCodes2[CharacterCodes2["p"] = 112] = "p";
|
|
2691
|
+
CharacterCodes2[CharacterCodes2["q"] = 113] = "q";
|
|
2692
|
+
CharacterCodes2[CharacterCodes2["r"] = 114] = "r";
|
|
2693
|
+
CharacterCodes2[CharacterCodes2["s"] = 115] = "s";
|
|
2694
|
+
CharacterCodes2[CharacterCodes2["t"] = 116] = "t";
|
|
2695
|
+
CharacterCodes2[CharacterCodes2["u"] = 117] = "u";
|
|
2696
|
+
CharacterCodes2[CharacterCodes2["v"] = 118] = "v";
|
|
2697
|
+
CharacterCodes2[CharacterCodes2["w"] = 119] = "w";
|
|
2698
|
+
CharacterCodes2[CharacterCodes2["x"] = 120] = "x";
|
|
2699
|
+
CharacterCodes2[CharacterCodes2["y"] = 121] = "y";
|
|
2700
|
+
CharacterCodes2[CharacterCodes2["z"] = 122] = "z";
|
|
2701
|
+
CharacterCodes2[CharacterCodes2["A"] = 65] = "A";
|
|
2702
|
+
CharacterCodes2[CharacterCodes2["B"] = 66] = "B";
|
|
2703
|
+
CharacterCodes2[CharacterCodes2["C"] = 67] = "C";
|
|
2704
|
+
CharacterCodes2[CharacterCodes2["D"] = 68] = "D";
|
|
2705
|
+
CharacterCodes2[CharacterCodes2["E"] = 69] = "E";
|
|
2706
|
+
CharacterCodes2[CharacterCodes2["F"] = 70] = "F";
|
|
2707
|
+
CharacterCodes2[CharacterCodes2["G"] = 71] = "G";
|
|
2708
|
+
CharacterCodes2[CharacterCodes2["H"] = 72] = "H";
|
|
2709
|
+
CharacterCodes2[CharacterCodes2["I"] = 73] = "I";
|
|
2710
|
+
CharacterCodes2[CharacterCodes2["J"] = 74] = "J";
|
|
2711
|
+
CharacterCodes2[CharacterCodes2["K"] = 75] = "K";
|
|
2712
|
+
CharacterCodes2[CharacterCodes2["L"] = 76] = "L";
|
|
2713
|
+
CharacterCodes2[CharacterCodes2["M"] = 77] = "M";
|
|
2714
|
+
CharacterCodes2[CharacterCodes2["N"] = 78] = "N";
|
|
2715
|
+
CharacterCodes2[CharacterCodes2["O"] = 79] = "O";
|
|
2716
|
+
CharacterCodes2[CharacterCodes2["P"] = 80] = "P";
|
|
2717
|
+
CharacterCodes2[CharacterCodes2["Q"] = 81] = "Q";
|
|
2718
|
+
CharacterCodes2[CharacterCodes2["R"] = 82] = "R";
|
|
2719
|
+
CharacterCodes2[CharacterCodes2["S"] = 83] = "S";
|
|
2720
|
+
CharacterCodes2[CharacterCodes2["T"] = 84] = "T";
|
|
2721
|
+
CharacterCodes2[CharacterCodes2["U"] = 85] = "U";
|
|
2722
|
+
CharacterCodes2[CharacterCodes2["V"] = 86] = "V";
|
|
2723
|
+
CharacterCodes2[CharacterCodes2["W"] = 87] = "W";
|
|
2724
|
+
CharacterCodes2[CharacterCodes2["X"] = 88] = "X";
|
|
2725
|
+
CharacterCodes2[CharacterCodes2["Y"] = 89] = "Y";
|
|
2726
|
+
CharacterCodes2[CharacterCodes2["Z"] = 90] = "Z";
|
|
2727
|
+
CharacterCodes2[CharacterCodes2["asterisk"] = 42] = "asterisk";
|
|
2728
|
+
CharacterCodes2[CharacterCodes2["backslash"] = 92] = "backslash";
|
|
2729
|
+
CharacterCodes2[CharacterCodes2["closeBrace"] = 125] = "closeBrace";
|
|
2730
|
+
CharacterCodes2[CharacterCodes2["closeBracket"] = 93] = "closeBracket";
|
|
2731
|
+
CharacterCodes2[CharacterCodes2["colon"] = 58] = "colon";
|
|
2732
|
+
CharacterCodes2[CharacterCodes2["comma"] = 44] = "comma";
|
|
2733
|
+
CharacterCodes2[CharacterCodes2["dot"] = 46] = "dot";
|
|
2734
|
+
CharacterCodes2[CharacterCodes2["doubleQuote"] = 34] = "doubleQuote";
|
|
2735
|
+
CharacterCodes2[CharacterCodes2["minus"] = 45] = "minus";
|
|
2736
|
+
CharacterCodes2[CharacterCodes2["openBrace"] = 123] = "openBrace";
|
|
2737
|
+
CharacterCodes2[CharacterCodes2["openBracket"] = 91] = "openBracket";
|
|
2738
|
+
CharacterCodes2[CharacterCodes2["plus"] = 43] = "plus";
|
|
2739
|
+
CharacterCodes2[CharacterCodes2["slash"] = 47] = "slash";
|
|
2740
|
+
CharacterCodes2[CharacterCodes2["formFeed"] = 12] = "formFeed";
|
|
2741
|
+
CharacterCodes2[CharacterCodes2["tab"] = 9] = "tab";
|
|
2742
|
+
})(CharacterCodes || (CharacterCodes = {}));
|
|
2743
|
+
|
|
2744
|
+
// node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/string-intern.js
|
|
2745
|
+
var cachedSpaces = new Array(20).fill(0).map((_, index) => {
|
|
2746
|
+
return " ".repeat(index);
|
|
2747
|
+
});
|
|
2748
|
+
var maxCachedValues = 200;
|
|
2749
|
+
var cachedBreakLinesWithSpaces = {
|
|
2750
|
+
" ": {
|
|
2751
|
+
"\n": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
2752
|
+
return "\n" + " ".repeat(index);
|
|
2753
|
+
}),
|
|
2754
|
+
"\r": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
2755
|
+
return "\r" + " ".repeat(index);
|
|
2756
|
+
}),
|
|
2757
|
+
"\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
2758
|
+
return "\r\n" + " ".repeat(index);
|
|
2759
|
+
})
|
|
2760
|
+
},
|
|
2761
|
+
" ": {
|
|
2762
|
+
"\n": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
2763
|
+
return "\n" + " ".repeat(index);
|
|
2764
|
+
}),
|
|
2765
|
+
"\r": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
2766
|
+
return "\r" + " ".repeat(index);
|
|
2767
|
+
}),
|
|
2768
|
+
"\r\n": new Array(maxCachedValues).fill(0).map((_, index) => {
|
|
2769
|
+
return "\r\n" + " ".repeat(index);
|
|
2770
|
+
})
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
|
|
2774
|
+
// node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/parser.js
|
|
2775
|
+
var ParseOptions;
|
|
2776
|
+
(function(ParseOptions2) {
|
|
2777
|
+
ParseOptions2.DEFAULT = {
|
|
2778
|
+
allowTrailingComma: false
|
|
2779
|
+
};
|
|
2780
|
+
})(ParseOptions || (ParseOptions = {}));
|
|
2781
|
+
function parse(text, errors = [], options = ParseOptions.DEFAULT) {
|
|
2782
|
+
let currentProperty = null;
|
|
2783
|
+
let currentParent = [];
|
|
2784
|
+
const previousParents = [];
|
|
2785
|
+
function onValue(value) {
|
|
2786
|
+
if (Array.isArray(currentParent)) {
|
|
2787
|
+
currentParent.push(value);
|
|
2788
|
+
} else if (currentProperty !== null) {
|
|
2789
|
+
currentParent[currentProperty] = value;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
const visitor = {
|
|
2793
|
+
onObjectBegin: () => {
|
|
2794
|
+
const object = {};
|
|
2795
|
+
onValue(object);
|
|
2796
|
+
previousParents.push(currentParent);
|
|
2797
|
+
currentParent = object;
|
|
2798
|
+
currentProperty = null;
|
|
2799
|
+
},
|
|
2800
|
+
onObjectProperty: (name) => {
|
|
2801
|
+
currentProperty = name;
|
|
2802
|
+
},
|
|
2803
|
+
onObjectEnd: () => {
|
|
2804
|
+
currentParent = previousParents.pop();
|
|
2805
|
+
},
|
|
2806
|
+
onArrayBegin: () => {
|
|
2807
|
+
const array = [];
|
|
2808
|
+
onValue(array);
|
|
2809
|
+
previousParents.push(currentParent);
|
|
2810
|
+
currentParent = array;
|
|
2811
|
+
currentProperty = null;
|
|
2812
|
+
},
|
|
2813
|
+
onArrayEnd: () => {
|
|
2814
|
+
currentParent = previousParents.pop();
|
|
2815
|
+
},
|
|
2816
|
+
onLiteralValue: onValue,
|
|
2817
|
+
onError: (error, offset, length) => {
|
|
2818
|
+
errors.push({ error, offset, length });
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
visit(text, visitor, options);
|
|
2822
|
+
return currentParent[0];
|
|
2823
|
+
}
|
|
2824
|
+
function visit(text, visitor, options = ParseOptions.DEFAULT) {
|
|
2825
|
+
const _scanner = createScanner(text, false);
|
|
2826
|
+
const _jsonPath = [];
|
|
2827
|
+
let suppressedCallbacks = 0;
|
|
2828
|
+
function toNoArgVisit(visitFunction) {
|
|
2829
|
+
return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
|
|
2830
|
+
}
|
|
2831
|
+
function toOneArgVisit(visitFunction) {
|
|
2832
|
+
return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true;
|
|
2833
|
+
}
|
|
2834
|
+
function toOneArgVisitWithPath(visitFunction) {
|
|
2835
|
+
return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true;
|
|
2836
|
+
}
|
|
2837
|
+
function toBeginVisit(visitFunction) {
|
|
2838
|
+
return visitFunction ? () => {
|
|
2839
|
+
if (suppressedCallbacks > 0) {
|
|
2840
|
+
suppressedCallbacks++;
|
|
2841
|
+
} else {
|
|
2842
|
+
let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice());
|
|
2843
|
+
if (cbReturn === false) {
|
|
2844
|
+
suppressedCallbacks = 1;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
} : () => true;
|
|
2848
|
+
}
|
|
2849
|
+
function toEndVisit(visitFunction) {
|
|
2850
|
+
return visitFunction ? () => {
|
|
2851
|
+
if (suppressedCallbacks > 0) {
|
|
2852
|
+
suppressedCallbacks--;
|
|
2853
|
+
}
|
|
2854
|
+
if (suppressedCallbacks === 0) {
|
|
2855
|
+
visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter());
|
|
2856
|
+
}
|
|
2857
|
+
} : () => true;
|
|
2858
|
+
}
|
|
2859
|
+
const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError);
|
|
2860
|
+
const disallowComments = options && options.disallowComments;
|
|
2861
|
+
const allowTrailingComma = options && options.allowTrailingComma;
|
|
2862
|
+
function scanNext() {
|
|
2863
|
+
while (true) {
|
|
2864
|
+
const token = _scanner.scan();
|
|
2865
|
+
switch (_scanner.getTokenError()) {
|
|
2866
|
+
case 4:
|
|
2867
|
+
handleError2(
|
|
2868
|
+
14
|
|
2869
|
+
/* ParseErrorCode.InvalidUnicode */
|
|
2870
|
+
);
|
|
2871
|
+
break;
|
|
2872
|
+
case 5:
|
|
2873
|
+
handleError2(
|
|
2874
|
+
15
|
|
2875
|
+
/* ParseErrorCode.InvalidEscapeCharacter */
|
|
2876
|
+
);
|
|
2877
|
+
break;
|
|
2878
|
+
case 3:
|
|
2879
|
+
handleError2(
|
|
2880
|
+
13
|
|
2881
|
+
/* ParseErrorCode.UnexpectedEndOfNumber */
|
|
2882
|
+
);
|
|
2883
|
+
break;
|
|
2884
|
+
case 1:
|
|
2885
|
+
if (!disallowComments) {
|
|
2886
|
+
handleError2(
|
|
2887
|
+
11
|
|
2888
|
+
/* ParseErrorCode.UnexpectedEndOfComment */
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
break;
|
|
2892
|
+
case 2:
|
|
2893
|
+
handleError2(
|
|
2894
|
+
12
|
|
2895
|
+
/* ParseErrorCode.UnexpectedEndOfString */
|
|
2896
|
+
);
|
|
2897
|
+
break;
|
|
2898
|
+
case 6:
|
|
2899
|
+
handleError2(
|
|
2900
|
+
16
|
|
2901
|
+
/* ParseErrorCode.InvalidCharacter */
|
|
2902
|
+
);
|
|
2903
|
+
break;
|
|
2904
|
+
}
|
|
2905
|
+
switch (token) {
|
|
2906
|
+
case 12:
|
|
2907
|
+
case 13:
|
|
2908
|
+
if (disallowComments) {
|
|
2909
|
+
handleError2(
|
|
2910
|
+
10
|
|
2911
|
+
/* ParseErrorCode.InvalidCommentToken */
|
|
2912
|
+
);
|
|
2913
|
+
} else {
|
|
2914
|
+
onComment();
|
|
2915
|
+
}
|
|
2916
|
+
break;
|
|
2917
|
+
case 16:
|
|
2918
|
+
handleError2(
|
|
2919
|
+
1
|
|
2920
|
+
/* ParseErrorCode.InvalidSymbol */
|
|
2921
|
+
);
|
|
2922
|
+
break;
|
|
2923
|
+
case 15:
|
|
2924
|
+
case 14:
|
|
2925
|
+
break;
|
|
2926
|
+
default:
|
|
2927
|
+
return token;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
function handleError2(error, skipUntilAfter = [], skipUntil = []) {
|
|
2932
|
+
onError(error);
|
|
2933
|
+
if (skipUntilAfter.length + skipUntil.length > 0) {
|
|
2934
|
+
let token = _scanner.getToken();
|
|
2935
|
+
while (token !== 17) {
|
|
2936
|
+
if (skipUntilAfter.indexOf(token) !== -1) {
|
|
2937
|
+
scanNext();
|
|
2938
|
+
break;
|
|
2939
|
+
} else if (skipUntil.indexOf(token) !== -1) {
|
|
2940
|
+
break;
|
|
2941
|
+
}
|
|
2942
|
+
token = scanNext();
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
function parseString(isValue) {
|
|
2947
|
+
const value = _scanner.getTokenValue();
|
|
2948
|
+
if (isValue) {
|
|
2949
|
+
onLiteralValue(value);
|
|
2950
|
+
} else {
|
|
2951
|
+
onObjectProperty(value);
|
|
2952
|
+
_jsonPath.push(value);
|
|
2953
|
+
}
|
|
2954
|
+
scanNext();
|
|
2955
|
+
return true;
|
|
2956
|
+
}
|
|
2957
|
+
function parseLiteral() {
|
|
2958
|
+
switch (_scanner.getToken()) {
|
|
2959
|
+
case 11:
|
|
2960
|
+
const tokenValue = _scanner.getTokenValue();
|
|
2961
|
+
let value = Number(tokenValue);
|
|
2962
|
+
if (isNaN(value)) {
|
|
2963
|
+
handleError2(
|
|
2964
|
+
2
|
|
2965
|
+
/* ParseErrorCode.InvalidNumberFormat */
|
|
2966
|
+
);
|
|
2967
|
+
value = 0;
|
|
2968
|
+
}
|
|
2969
|
+
onLiteralValue(value);
|
|
2970
|
+
break;
|
|
2971
|
+
case 7:
|
|
2972
|
+
onLiteralValue(null);
|
|
2973
|
+
break;
|
|
2974
|
+
case 8:
|
|
2975
|
+
onLiteralValue(true);
|
|
2976
|
+
break;
|
|
2977
|
+
case 9:
|
|
2978
|
+
onLiteralValue(false);
|
|
2979
|
+
break;
|
|
2980
|
+
default:
|
|
2981
|
+
return false;
|
|
2982
|
+
}
|
|
2983
|
+
scanNext();
|
|
2984
|
+
return true;
|
|
2985
|
+
}
|
|
2986
|
+
function parseProperty() {
|
|
2987
|
+
if (_scanner.getToken() !== 10) {
|
|
2988
|
+
handleError2(3, [], [
|
|
2989
|
+
2,
|
|
2990
|
+
5
|
|
2991
|
+
/* SyntaxKind.CommaToken */
|
|
2992
|
+
]);
|
|
2993
|
+
return false;
|
|
2994
|
+
}
|
|
2995
|
+
parseString(false);
|
|
2996
|
+
if (_scanner.getToken() === 6) {
|
|
2997
|
+
onSeparator(":");
|
|
2998
|
+
scanNext();
|
|
2999
|
+
if (!parseValue()) {
|
|
3000
|
+
handleError2(4, [], [
|
|
3001
|
+
2,
|
|
3002
|
+
5
|
|
3003
|
+
/* SyntaxKind.CommaToken */
|
|
3004
|
+
]);
|
|
3005
|
+
}
|
|
3006
|
+
} else {
|
|
3007
|
+
handleError2(5, [], [
|
|
3008
|
+
2,
|
|
3009
|
+
5
|
|
3010
|
+
/* SyntaxKind.CommaToken */
|
|
3011
|
+
]);
|
|
3012
|
+
}
|
|
3013
|
+
_jsonPath.pop();
|
|
3014
|
+
return true;
|
|
3015
|
+
}
|
|
3016
|
+
function parseObject() {
|
|
3017
|
+
onObjectBegin();
|
|
3018
|
+
scanNext();
|
|
3019
|
+
let needsComma = false;
|
|
3020
|
+
while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) {
|
|
3021
|
+
if (_scanner.getToken() === 5) {
|
|
3022
|
+
if (!needsComma) {
|
|
3023
|
+
handleError2(4, [], []);
|
|
3024
|
+
}
|
|
3025
|
+
onSeparator(",");
|
|
3026
|
+
scanNext();
|
|
3027
|
+
if (_scanner.getToken() === 2 && allowTrailingComma) {
|
|
3028
|
+
break;
|
|
3029
|
+
}
|
|
3030
|
+
} else if (needsComma) {
|
|
3031
|
+
handleError2(6, [], []);
|
|
3032
|
+
}
|
|
3033
|
+
if (!parseProperty()) {
|
|
3034
|
+
handleError2(4, [], [
|
|
3035
|
+
2,
|
|
3036
|
+
5
|
|
3037
|
+
/* SyntaxKind.CommaToken */
|
|
3038
|
+
]);
|
|
3039
|
+
}
|
|
3040
|
+
needsComma = true;
|
|
3041
|
+
}
|
|
3042
|
+
onObjectEnd();
|
|
3043
|
+
if (_scanner.getToken() !== 2) {
|
|
3044
|
+
handleError2(7, [
|
|
3045
|
+
2
|
|
3046
|
+
/* SyntaxKind.CloseBraceToken */
|
|
3047
|
+
], []);
|
|
3048
|
+
} else {
|
|
3049
|
+
scanNext();
|
|
3050
|
+
}
|
|
3051
|
+
return true;
|
|
3052
|
+
}
|
|
3053
|
+
function parseArray() {
|
|
3054
|
+
onArrayBegin();
|
|
3055
|
+
scanNext();
|
|
3056
|
+
let isFirstElement = true;
|
|
3057
|
+
let needsComma = false;
|
|
3058
|
+
while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) {
|
|
3059
|
+
if (_scanner.getToken() === 5) {
|
|
3060
|
+
if (!needsComma) {
|
|
3061
|
+
handleError2(4, [], []);
|
|
3062
|
+
}
|
|
3063
|
+
onSeparator(",");
|
|
3064
|
+
scanNext();
|
|
3065
|
+
if (_scanner.getToken() === 4 && allowTrailingComma) {
|
|
3066
|
+
break;
|
|
3067
|
+
}
|
|
3068
|
+
} else if (needsComma) {
|
|
3069
|
+
handleError2(6, [], []);
|
|
3070
|
+
}
|
|
3071
|
+
if (isFirstElement) {
|
|
3072
|
+
_jsonPath.push(0);
|
|
3073
|
+
isFirstElement = false;
|
|
3074
|
+
} else {
|
|
3075
|
+
_jsonPath[_jsonPath.length - 1]++;
|
|
3076
|
+
}
|
|
3077
|
+
if (!parseValue()) {
|
|
3078
|
+
handleError2(4, [], [
|
|
3079
|
+
4,
|
|
3080
|
+
5
|
|
3081
|
+
/* SyntaxKind.CommaToken */
|
|
3082
|
+
]);
|
|
3083
|
+
}
|
|
3084
|
+
needsComma = true;
|
|
3085
|
+
}
|
|
3086
|
+
onArrayEnd();
|
|
3087
|
+
if (!isFirstElement) {
|
|
3088
|
+
_jsonPath.pop();
|
|
3089
|
+
}
|
|
3090
|
+
if (_scanner.getToken() !== 4) {
|
|
3091
|
+
handleError2(8, [
|
|
3092
|
+
4
|
|
3093
|
+
/* SyntaxKind.CloseBracketToken */
|
|
3094
|
+
], []);
|
|
3095
|
+
} else {
|
|
3096
|
+
scanNext();
|
|
3097
|
+
}
|
|
3098
|
+
return true;
|
|
3099
|
+
}
|
|
3100
|
+
function parseValue() {
|
|
3101
|
+
switch (_scanner.getToken()) {
|
|
3102
|
+
case 3:
|
|
3103
|
+
return parseArray();
|
|
3104
|
+
case 1:
|
|
3105
|
+
return parseObject();
|
|
3106
|
+
case 10:
|
|
3107
|
+
return parseString(true);
|
|
3108
|
+
default:
|
|
3109
|
+
return parseLiteral();
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
scanNext();
|
|
3113
|
+
if (_scanner.getToken() === 17) {
|
|
3114
|
+
if (options.allowEmptyContent) {
|
|
3115
|
+
return true;
|
|
3116
|
+
}
|
|
3117
|
+
handleError2(4, [], []);
|
|
3118
|
+
return false;
|
|
3119
|
+
}
|
|
3120
|
+
if (!parseValue()) {
|
|
3121
|
+
handleError2(4, [], []);
|
|
3122
|
+
return false;
|
|
3123
|
+
}
|
|
3124
|
+
if (_scanner.getToken() !== 17) {
|
|
3125
|
+
handleError2(9, [], []);
|
|
3126
|
+
}
|
|
3127
|
+
return true;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/main.js
|
|
3131
|
+
var ScanError;
|
|
3132
|
+
(function(ScanError2) {
|
|
3133
|
+
ScanError2[ScanError2["None"] = 0] = "None";
|
|
3134
|
+
ScanError2[ScanError2["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment";
|
|
3135
|
+
ScanError2[ScanError2["UnexpectedEndOfString"] = 2] = "UnexpectedEndOfString";
|
|
3136
|
+
ScanError2[ScanError2["UnexpectedEndOfNumber"] = 3] = "UnexpectedEndOfNumber";
|
|
3137
|
+
ScanError2[ScanError2["InvalidUnicode"] = 4] = "InvalidUnicode";
|
|
3138
|
+
ScanError2[ScanError2["InvalidEscapeCharacter"] = 5] = "InvalidEscapeCharacter";
|
|
3139
|
+
ScanError2[ScanError2["InvalidCharacter"] = 6] = "InvalidCharacter";
|
|
3140
|
+
})(ScanError || (ScanError = {}));
|
|
3141
|
+
var SyntaxKind;
|
|
3142
|
+
(function(SyntaxKind2) {
|
|
3143
|
+
SyntaxKind2[SyntaxKind2["OpenBraceToken"] = 1] = "OpenBraceToken";
|
|
3144
|
+
SyntaxKind2[SyntaxKind2["CloseBraceToken"] = 2] = "CloseBraceToken";
|
|
3145
|
+
SyntaxKind2[SyntaxKind2["OpenBracketToken"] = 3] = "OpenBracketToken";
|
|
3146
|
+
SyntaxKind2[SyntaxKind2["CloseBracketToken"] = 4] = "CloseBracketToken";
|
|
3147
|
+
SyntaxKind2[SyntaxKind2["CommaToken"] = 5] = "CommaToken";
|
|
3148
|
+
SyntaxKind2[SyntaxKind2["ColonToken"] = 6] = "ColonToken";
|
|
3149
|
+
SyntaxKind2[SyntaxKind2["NullKeyword"] = 7] = "NullKeyword";
|
|
3150
|
+
SyntaxKind2[SyntaxKind2["TrueKeyword"] = 8] = "TrueKeyword";
|
|
3151
|
+
SyntaxKind2[SyntaxKind2["FalseKeyword"] = 9] = "FalseKeyword";
|
|
3152
|
+
SyntaxKind2[SyntaxKind2["StringLiteral"] = 10] = "StringLiteral";
|
|
3153
|
+
SyntaxKind2[SyntaxKind2["NumericLiteral"] = 11] = "NumericLiteral";
|
|
3154
|
+
SyntaxKind2[SyntaxKind2["LineCommentTrivia"] = 12] = "LineCommentTrivia";
|
|
3155
|
+
SyntaxKind2[SyntaxKind2["BlockCommentTrivia"] = 13] = "BlockCommentTrivia";
|
|
3156
|
+
SyntaxKind2[SyntaxKind2["LineBreakTrivia"] = 14] = "LineBreakTrivia";
|
|
3157
|
+
SyntaxKind2[SyntaxKind2["Trivia"] = 15] = "Trivia";
|
|
3158
|
+
SyntaxKind2[SyntaxKind2["Unknown"] = 16] = "Unknown";
|
|
3159
|
+
SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF";
|
|
3160
|
+
})(SyntaxKind || (SyntaxKind = {}));
|
|
3161
|
+
var parse2 = parse;
|
|
3162
|
+
var ParseErrorCode;
|
|
3163
|
+
(function(ParseErrorCode2) {
|
|
3164
|
+
ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol";
|
|
3165
|
+
ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat";
|
|
3166
|
+
ParseErrorCode2[ParseErrorCode2["PropertyNameExpected"] = 3] = "PropertyNameExpected";
|
|
3167
|
+
ParseErrorCode2[ParseErrorCode2["ValueExpected"] = 4] = "ValueExpected";
|
|
3168
|
+
ParseErrorCode2[ParseErrorCode2["ColonExpected"] = 5] = "ColonExpected";
|
|
3169
|
+
ParseErrorCode2[ParseErrorCode2["CommaExpected"] = 6] = "CommaExpected";
|
|
3170
|
+
ParseErrorCode2[ParseErrorCode2["CloseBraceExpected"] = 7] = "CloseBraceExpected";
|
|
3171
|
+
ParseErrorCode2[ParseErrorCode2["CloseBracketExpected"] = 8] = "CloseBracketExpected";
|
|
3172
|
+
ParseErrorCode2[ParseErrorCode2["EndOfFileExpected"] = 9] = "EndOfFileExpected";
|
|
3173
|
+
ParseErrorCode2[ParseErrorCode2["InvalidCommentToken"] = 10] = "InvalidCommentToken";
|
|
3174
|
+
ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfComment"] = 11] = "UnexpectedEndOfComment";
|
|
3175
|
+
ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfString"] = 12] = "UnexpectedEndOfString";
|
|
3176
|
+
ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfNumber"] = 13] = "UnexpectedEndOfNumber";
|
|
3177
|
+
ParseErrorCode2[ParseErrorCode2["InvalidUnicode"] = 14] = "InvalidUnicode";
|
|
3178
|
+
ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter";
|
|
3179
|
+
ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
|
|
3180
|
+
})(ParseErrorCode || (ParseErrorCode = {}));
|
|
3181
|
+
|
|
3182
|
+
// src/validation/config/scope.ts
|
|
3183
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
3184
|
+
import { join as join11 } from "path";
|
|
3185
|
+
var TSCONFIG_FILES = {
|
|
3186
|
+
full: "tsconfig.json",
|
|
3187
|
+
production: "tsconfig.production.json"
|
|
3188
|
+
};
|
|
3189
|
+
var defaultScopeDeps = {
|
|
3190
|
+
readFileSync,
|
|
3191
|
+
existsSync,
|
|
3192
|
+
readdirSync
|
|
3193
|
+
};
|
|
3194
|
+
function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
|
|
3195
|
+
try {
|
|
3196
|
+
const configContent = deps.readFileSync(configPath, "utf-8");
|
|
3197
|
+
const parsed = parse2(configContent);
|
|
3198
|
+
return parsed;
|
|
3199
|
+
} catch {
|
|
3200
|
+
return {
|
|
3201
|
+
include: ["**/*.ts", "**/*.tsx"],
|
|
3202
|
+
exclude: ["node_modules/**", ".pnpm-store/**", "dist/**"]
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
function resolveTypeScriptConfig(scope, deps = defaultScopeDeps) {
|
|
3207
|
+
const configFile = TSCONFIG_FILES[scope];
|
|
3208
|
+
const config = parseTypeScriptConfig(configFile, deps);
|
|
3209
|
+
if (config.extends) {
|
|
3210
|
+
const baseConfig = parseTypeScriptConfig(config.extends, deps);
|
|
3211
|
+
return {
|
|
3212
|
+
include: config.include ?? baseConfig.include ?? [],
|
|
3213
|
+
exclude: [...baseConfig.exclude ?? [], ...config.exclude ?? []]
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
return {
|
|
3217
|
+
include: config.include ?? [],
|
|
3218
|
+
exclude: config.exclude ?? []
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeDeps) {
|
|
3222
|
+
if (maxDepth <= 0) return false;
|
|
3223
|
+
try {
|
|
3224
|
+
const items = deps.readdirSync(dirPath, { withFileTypes: true });
|
|
3225
|
+
const hasDirectTsFiles = items.some(
|
|
3226
|
+
(item) => item.isFile() && (item.name.endsWith(".ts") || item.name.endsWith(".tsx"))
|
|
3227
|
+
);
|
|
3228
|
+
if (hasDirectTsFiles) return true;
|
|
3229
|
+
const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
|
|
3230
|
+
for (const subdir of subdirs.slice(0, 5)) {
|
|
3231
|
+
if (hasTypeScriptFilesRecursive(join11(dirPath, subdir.name), maxDepth - 1, deps)) {
|
|
3232
|
+
return true;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
return false;
|
|
3236
|
+
} catch {
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
function getTopLevelDirectoriesWithTypeScript(config, deps = defaultScopeDeps) {
|
|
3241
|
+
const allTopLevelItems = deps.readdirSync(".", { withFileTypes: true });
|
|
3242
|
+
const directories = /* @__PURE__ */ new Set();
|
|
3243
|
+
const topLevelDirs = allTopLevelItems.filter((item) => item.isDirectory()).map((item) => item.name).filter((name) => !name.startsWith("."));
|
|
3244
|
+
for (const dir of topLevelDirs) {
|
|
3245
|
+
const isExcluded = config.exclude?.some((pattern) => {
|
|
3246
|
+
if (pattern.includes("/**")) {
|
|
3247
|
+
const dirPattern = pattern.split("/**")[0];
|
|
3248
|
+
return dirPattern === dir;
|
|
3249
|
+
}
|
|
3250
|
+
return pattern === dir || pattern.startsWith(dir + "/") || pattern === dir + "/**";
|
|
3251
|
+
});
|
|
3252
|
+
if (!isExcluded) {
|
|
3253
|
+
try {
|
|
3254
|
+
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(dir, 2, deps);
|
|
3255
|
+
if (hasTypeScriptFiles) {
|
|
3256
|
+
directories.add(dir);
|
|
3257
|
+
}
|
|
3258
|
+
} catch {
|
|
3259
|
+
continue;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
if (config.include) {
|
|
3264
|
+
for (const pattern of config.include) {
|
|
3265
|
+
if (pattern.includes("/")) {
|
|
3266
|
+
const topLevelDir = pattern.split("/")[0];
|
|
3267
|
+
if (topLevelDir && !topLevelDir.includes("*") && !topLevelDir.startsWith(".")) {
|
|
3268
|
+
directories.add(topLevelDir);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
return Array.from(directories).sort();
|
|
3274
|
+
}
|
|
3275
|
+
function getValidationDirectories(scope, deps = defaultScopeDeps) {
|
|
3276
|
+
const config = resolveTypeScriptConfig(scope, deps);
|
|
3277
|
+
const configDirectories = getTopLevelDirectoriesWithTypeScript(config, deps);
|
|
3278
|
+
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(dir));
|
|
3279
|
+
return existingDirectories;
|
|
3280
|
+
}
|
|
3281
|
+
function getTypeScriptScope(scope, deps = defaultScopeDeps) {
|
|
3282
|
+
const directories = getValidationDirectories(scope, deps);
|
|
3283
|
+
const config = resolveTypeScriptConfig(scope, deps);
|
|
3284
|
+
return {
|
|
3285
|
+
directories,
|
|
3286
|
+
filePatterns: config.include ?? [],
|
|
3287
|
+
excludePatterns: config.exclude ?? []
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
// src/validation/discovery/tool-finder.ts
|
|
3292
|
+
import { execSync } from "child_process";
|
|
3293
|
+
import fs6 from "fs";
|
|
3294
|
+
import { createRequire } from "module";
|
|
3295
|
+
import path7 from "path";
|
|
3296
|
+
|
|
3297
|
+
// src/validation/discovery/constants.ts
|
|
3298
|
+
var TOOL_DISCOVERY = {
|
|
3299
|
+
/** Tool source identifiers */
|
|
3300
|
+
SOURCES: {
|
|
3301
|
+
/** Tool bundled with spx-cli */
|
|
3302
|
+
BUNDLED: "bundled",
|
|
3303
|
+
/** Tool in project's node_modules */
|
|
3304
|
+
PROJECT: "project",
|
|
3305
|
+
/** Tool in system PATH */
|
|
3306
|
+
GLOBAL: "global"
|
|
3307
|
+
},
|
|
3308
|
+
/** Message templates */
|
|
3309
|
+
MESSAGES: {
|
|
3310
|
+
/** Prefix for skip messages */
|
|
3311
|
+
SKIP_PREFIX: "\u23ED",
|
|
3312
|
+
// ⏭ emoji
|
|
3313
|
+
/**
|
|
3314
|
+
* Format not found reason message.
|
|
3315
|
+
* @param tool - The tool name that was not found
|
|
3316
|
+
*/
|
|
3317
|
+
NOT_FOUND_REASON: (tool) => `${tool} not found in bundled deps, project node_modules, or system PATH`,
|
|
3318
|
+
/**
|
|
3319
|
+
* Format skip message for graceful degradation.
|
|
3320
|
+
* @param step - The validation step name
|
|
3321
|
+
* @param tool - The tool that was not found
|
|
3322
|
+
*/
|
|
3323
|
+
SKIP_FORMAT: (step, tool) => `${TOOL_DISCOVERY.MESSAGES.SKIP_PREFIX} Skipping ${step} (${tool} not available)`
|
|
3324
|
+
}
|
|
3325
|
+
};
|
|
3326
|
+
|
|
3327
|
+
// src/validation/discovery/tool-finder.ts
|
|
3328
|
+
var require2 = createRequire(import.meta.url);
|
|
3329
|
+
var defaultToolDiscoveryDeps = {
|
|
3330
|
+
resolveModule: (modulePath) => {
|
|
3331
|
+
try {
|
|
3332
|
+
return require2.resolve(modulePath);
|
|
3333
|
+
} catch {
|
|
3334
|
+
return null;
|
|
3335
|
+
}
|
|
3336
|
+
},
|
|
3337
|
+
existsSync: fs6.existsSync,
|
|
3338
|
+
whichSync: (tool) => {
|
|
3339
|
+
try {
|
|
3340
|
+
const result = execSync(`which ${tool}`, {
|
|
3341
|
+
encoding: "utf-8",
|
|
3342
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3343
|
+
});
|
|
3344
|
+
return result.trim() || null;
|
|
3345
|
+
} catch {
|
|
3346
|
+
return null;
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
async function discoverTool(tool, options = {}) {
|
|
3351
|
+
const { projectRoot = process.cwd(), deps = defaultToolDiscoveryDeps } = options;
|
|
3352
|
+
const bundledPath = deps.resolveModule(`${tool}/package.json`);
|
|
3353
|
+
if (bundledPath) {
|
|
3354
|
+
return {
|
|
3355
|
+
found: true,
|
|
3356
|
+
location: {
|
|
3357
|
+
tool,
|
|
3358
|
+
path: path7.dirname(bundledPath),
|
|
3359
|
+
source: TOOL_DISCOVERY.SOURCES.BUNDLED
|
|
3360
|
+
}
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
const projectBinPath = path7.join(projectRoot, "node_modules", ".bin", tool);
|
|
3364
|
+
if (deps.existsSync(projectBinPath)) {
|
|
3365
|
+
return {
|
|
3366
|
+
found: true,
|
|
3367
|
+
location: {
|
|
3368
|
+
tool,
|
|
3369
|
+
path: projectBinPath,
|
|
3370
|
+
source: TOOL_DISCOVERY.SOURCES.PROJECT
|
|
3371
|
+
}
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
const globalPath = deps.whichSync(tool);
|
|
3375
|
+
if (globalPath) {
|
|
3376
|
+
return {
|
|
3377
|
+
found: true,
|
|
3378
|
+
location: {
|
|
3379
|
+
tool,
|
|
3380
|
+
path: globalPath,
|
|
3381
|
+
source: TOOL_DISCOVERY.SOURCES.GLOBAL
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
return {
|
|
3386
|
+
found: false,
|
|
3387
|
+
notFound: {
|
|
3388
|
+
tool,
|
|
3389
|
+
reason: TOOL_DISCOVERY.MESSAGES.NOT_FOUND_REASON(tool)
|
|
3390
|
+
}
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
function formatSkipMessage(stepName, result) {
|
|
3394
|
+
if (result.found) {
|
|
3395
|
+
return "";
|
|
3396
|
+
}
|
|
3397
|
+
return TOOL_DISCOVERY.MESSAGES.SKIP_FORMAT(stepName, result.notFound.tool);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
// src/validation/steps/circular.ts
|
|
3401
|
+
import madge from "madge";
|
|
3402
|
+
|
|
3403
|
+
// src/validation/steps/constants.ts
|
|
3404
|
+
var STEP_IDS = {
|
|
3405
|
+
CIRCULAR: "circular-deps",
|
|
3406
|
+
ESLINT: "eslint",
|
|
3407
|
+
TYPESCRIPT: "typescript",
|
|
3408
|
+
KNIP: "knip"
|
|
3409
|
+
};
|
|
3410
|
+
var STEP_NAMES = {
|
|
3411
|
+
CIRCULAR: "Circular Dependencies",
|
|
3412
|
+
ESLINT: "ESLint",
|
|
3413
|
+
TYPESCRIPT: "TypeScript",
|
|
3414
|
+
KNIP: "Unused Code"
|
|
3415
|
+
};
|
|
3416
|
+
var STEP_DESCRIPTIONS = {
|
|
3417
|
+
CIRCULAR: "Checking for circular dependencies",
|
|
3418
|
+
ESLINT: "Validating ESLint compliance",
|
|
3419
|
+
TYPESCRIPT: "Validating TypeScript",
|
|
3420
|
+
KNIP: "Detecting unused exports, dependencies, and files"
|
|
3421
|
+
};
|
|
3422
|
+
var CACHE_PATHS = {
|
|
3423
|
+
ESLINT: "dist/.eslintcache",
|
|
3424
|
+
TIMINGS: "dist/.validation-timings.json"
|
|
3425
|
+
};
|
|
3426
|
+
var VALIDATION_KEYS = {
|
|
3427
|
+
TYPESCRIPT: "TYPESCRIPT",
|
|
3428
|
+
ESLINT: "ESLINT",
|
|
3429
|
+
KNIP: "KNIP"
|
|
3430
|
+
};
|
|
3431
|
+
var VALIDATION_DEFAULTS = {
|
|
3432
|
+
[VALIDATION_KEYS.TYPESCRIPT]: true,
|
|
3433
|
+
[VALIDATION_KEYS.ESLINT]: true,
|
|
3434
|
+
[VALIDATION_KEYS.KNIP]: false
|
|
3435
|
+
};
|
|
3436
|
+
|
|
3437
|
+
// src/validation/steps/circular.ts
|
|
3438
|
+
var defaultCircularDeps = {
|
|
3439
|
+
madge
|
|
3440
|
+
};
|
|
3441
|
+
async function validateCircularDependencies(scope, typescriptScope, deps = defaultCircularDeps) {
|
|
3442
|
+
try {
|
|
3443
|
+
const analyzeDirectories = typescriptScope.directories;
|
|
3444
|
+
if (analyzeDirectories.length === 0) {
|
|
3445
|
+
return { success: true };
|
|
3446
|
+
}
|
|
3447
|
+
const tsConfigFile = TSCONFIG_FILES[scope];
|
|
3448
|
+
const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
|
|
3449
|
+
const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
|
|
3450
|
+
const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3451
|
+
return new RegExp(escaped);
|
|
3452
|
+
});
|
|
3453
|
+
const result = await deps.madge(analyzeDirectories, {
|
|
3454
|
+
fileExtensions: ["ts", "tsx"],
|
|
3455
|
+
tsConfig: tsConfigFile,
|
|
3456
|
+
excludeRegExp: excludeRegExps
|
|
3457
|
+
});
|
|
3458
|
+
const circular = result.circular();
|
|
3459
|
+
if (circular.length === 0) {
|
|
3460
|
+
return { success: true };
|
|
3461
|
+
} else {
|
|
3462
|
+
return {
|
|
3463
|
+
success: false,
|
|
3464
|
+
error: `Found ${circular.length} circular dependency cycle(s)`,
|
|
3465
|
+
circularDependencies: circular
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
} catch (error) {
|
|
3469
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3470
|
+
return { success: false, error: errorMessage };
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
var circularDependencyStep = {
|
|
3474
|
+
id: STEP_IDS.CIRCULAR,
|
|
3475
|
+
name: STEP_NAMES.CIRCULAR,
|
|
3476
|
+
description: STEP_DESCRIPTIONS.CIRCULAR,
|
|
3477
|
+
enabled: (context) => !context.isFileSpecificMode && context.enabledValidations[VALIDATION_KEYS.TYPESCRIPT] === true,
|
|
3478
|
+
execute: async (context) => {
|
|
3479
|
+
const startTime = performance.now();
|
|
3480
|
+
try {
|
|
3481
|
+
const result = await validateCircularDependencies(context.scope, context.scopeConfig);
|
|
3482
|
+
return {
|
|
3483
|
+
success: result.success,
|
|
3484
|
+
error: result.error,
|
|
3485
|
+
duration: performance.now() - startTime
|
|
3486
|
+
};
|
|
3487
|
+
} catch (error) {
|
|
3488
|
+
return {
|
|
3489
|
+
success: false,
|
|
3490
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3491
|
+
duration: performance.now() - startTime
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
};
|
|
3496
|
+
|
|
3497
|
+
// src/commands/validation/circular.ts
|
|
3498
|
+
async function circularCommand(options) {
|
|
3499
|
+
const { cwd, quiet } = options;
|
|
3500
|
+
const startTime = Date.now();
|
|
3501
|
+
const toolResult = await discoverTool("madge", { projectRoot: cwd });
|
|
3502
|
+
if (!toolResult.found) {
|
|
3503
|
+
const skipMessage = formatSkipMessage("circular dependency check", toolResult);
|
|
3504
|
+
return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
|
|
3505
|
+
}
|
|
3506
|
+
const scopeConfig = getTypeScriptScope("full");
|
|
3507
|
+
const result = await validateCircularDependencies("full", scopeConfig);
|
|
3508
|
+
const durationMs = Date.now() - startTime;
|
|
3509
|
+
if (result.success) {
|
|
3510
|
+
const output = quiet ? "" : `Circular dependencies: \u2713 None found`;
|
|
3511
|
+
return { exitCode: 0, output, durationMs };
|
|
3512
|
+
} else {
|
|
3513
|
+
let output = result.error ?? "Circular dependencies found";
|
|
3514
|
+
if (result.circularDependencies && result.circularDependencies.length > 0) {
|
|
3515
|
+
const cycles = result.circularDependencies.map((cycle) => ` ${cycle.join(" \u2192 ")}`).join("\n");
|
|
3516
|
+
output = `Circular dependencies found:
|
|
3517
|
+
${cycles}`;
|
|
3518
|
+
}
|
|
3519
|
+
return { exitCode: 1, output, durationMs };
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
// src/commands/validation/format.ts
|
|
3524
|
+
var DURATION_THRESHOLD_MS = 1e3;
|
|
3525
|
+
var VALIDATION_SYMBOLS = {
|
|
3526
|
+
SUCCESS: "\u2713",
|
|
3527
|
+
FAILURE: "\u2717"
|
|
3528
|
+
};
|
|
3529
|
+
function formatDuration(ms) {
|
|
3530
|
+
if (ms < DURATION_THRESHOLD_MS) {
|
|
3531
|
+
return `${ms}ms`;
|
|
3532
|
+
}
|
|
3533
|
+
const seconds = ms / 1e3;
|
|
3534
|
+
return `${seconds.toFixed(1)}s`;
|
|
3535
|
+
}
|
|
3536
|
+
function formatSummary(options) {
|
|
3537
|
+
const { success, totalDurationMs } = options;
|
|
3538
|
+
const symbol = success ? VALIDATION_SYMBOLS.SUCCESS : VALIDATION_SYMBOLS.FAILURE;
|
|
3539
|
+
const status = success ? "passed" : "failed";
|
|
3540
|
+
const duration = formatDuration(totalDurationMs);
|
|
3541
|
+
return `${symbol} Validation ${status} (${duration} total)`;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// src/validation/steps/eslint.ts
|
|
3545
|
+
import { spawn } from "child_process";
|
|
3546
|
+
|
|
3547
|
+
// src/validation/types.ts
|
|
3548
|
+
var VALIDATION_SCOPES = {
|
|
3549
|
+
/** Validate entire codebase including tests and scripts */
|
|
3550
|
+
FULL: "full",
|
|
3551
|
+
/** Validate production files only */
|
|
3552
|
+
PRODUCTION: "production"
|
|
3553
|
+
};
|
|
3554
|
+
var EXECUTION_MODES = {
|
|
3555
|
+
/** Read-only mode - report errors without fixing */
|
|
3556
|
+
READ: "read",
|
|
3557
|
+
/** Write mode - fix errors when possible (e.g., eslint --fix) */
|
|
3558
|
+
WRITE: "write"
|
|
3559
|
+
};
|
|
3560
|
+
|
|
3561
|
+
// src/validation/steps/eslint.ts
|
|
3562
|
+
var defaultEslintProcessRunner = { spawn };
|
|
3563
|
+
function buildEslintArgs(context) {
|
|
3564
|
+
const { validatedFiles, mode, cacheFile } = context;
|
|
3565
|
+
const fixArg = mode === EXECUTION_MODES.WRITE ? ["--fix"] : [];
|
|
3566
|
+
const cacheArgs = ["--cache", "--cache-location", cacheFile];
|
|
3567
|
+
if (validatedFiles && validatedFiles.length > 0) {
|
|
3568
|
+
return ["eslint", "--config", "eslint.config.ts", ...cacheArgs, ...fixArg, "--", ...validatedFiles];
|
|
3569
|
+
}
|
|
3570
|
+
return ["eslint", ".", "--config", "eslint.config.ts", ...cacheArgs, ...fixArg];
|
|
3571
|
+
}
|
|
3572
|
+
async function validateESLint(context, runner = defaultEslintProcessRunner) {
|
|
3573
|
+
const { scope, validatedFiles, mode } = context;
|
|
3574
|
+
return new Promise((resolve2) => {
|
|
3575
|
+
if (!validatedFiles || validatedFiles.length === 0) {
|
|
3576
|
+
if (scope === VALIDATION_SCOPES.PRODUCTION) {
|
|
3577
|
+
process.env.ESLINT_PRODUCTION_ONLY = "1";
|
|
3578
|
+
} else {
|
|
3579
|
+
delete process.env.ESLINT_PRODUCTION_ONLY;
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
const eslintArgs = buildEslintArgs({
|
|
3583
|
+
validatedFiles,
|
|
3584
|
+
mode,
|
|
3585
|
+
cacheFile: CACHE_PATHS.ESLINT
|
|
3586
|
+
});
|
|
3587
|
+
const eslintProcess = runner.spawn("npx", eslintArgs, {
|
|
3588
|
+
cwd: process.cwd(),
|
|
3589
|
+
stdio: "inherit"
|
|
3590
|
+
});
|
|
3591
|
+
eslintProcess.on("close", (code) => {
|
|
3592
|
+
if (code === 0) {
|
|
3593
|
+
resolve2({ success: true });
|
|
3594
|
+
} else {
|
|
3595
|
+
resolve2({ success: false, error: `ESLint exited with code ${code}` });
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
3598
|
+
eslintProcess.on("error", (error) => {
|
|
3599
|
+
resolve2({ success: false, error: error.message });
|
|
3600
|
+
});
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
function validationEnabled(envVarKey, defaults = {}) {
|
|
3604
|
+
const envVar = `${envVarKey}_VALIDATION_ENABLED`;
|
|
3605
|
+
const explicitlyDisabled = process.env[envVar] === "0";
|
|
3606
|
+
const explicitlyEnabled = process.env[envVar] === "1";
|
|
3607
|
+
const defaultValue = defaults[envVarKey] ?? true;
|
|
3608
|
+
if (defaultValue) {
|
|
3609
|
+
return !explicitlyDisabled;
|
|
3610
|
+
}
|
|
3611
|
+
return explicitlyEnabled;
|
|
3612
|
+
}
|
|
3613
|
+
var eslintStep = {
|
|
3614
|
+
id: STEP_IDS.ESLINT,
|
|
3615
|
+
name: STEP_NAMES.ESLINT,
|
|
3616
|
+
description: STEP_DESCRIPTIONS.ESLINT,
|
|
3617
|
+
enabled: (context) => context.enabledValidations[VALIDATION_KEYS.ESLINT] === true && validationEnabled(VALIDATION_KEYS.ESLINT),
|
|
3618
|
+
execute: async (context) => {
|
|
3619
|
+
const startTime = performance.now();
|
|
3620
|
+
try {
|
|
3621
|
+
const result = await validateESLint(context);
|
|
3622
|
+
return {
|
|
3623
|
+
success: result.success,
|
|
3624
|
+
error: result.error,
|
|
3625
|
+
duration: performance.now() - startTime
|
|
3626
|
+
};
|
|
3627
|
+
} catch (error) {
|
|
3628
|
+
return {
|
|
3629
|
+
success: false,
|
|
3630
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3631
|
+
duration: performance.now() - startTime
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
|
|
3637
|
+
// src/validation/steps/knip.ts
|
|
3638
|
+
import { spawn as spawn2 } from "child_process";
|
|
3639
|
+
var defaultKnipProcessRunner = { spawn: spawn2 };
|
|
3640
|
+
async function validateKnip(typescriptScope, runner = defaultKnipProcessRunner) {
|
|
3641
|
+
try {
|
|
3642
|
+
const analyzeDirectories = typescriptScope.directories;
|
|
3643
|
+
if (analyzeDirectories.length === 0) {
|
|
3644
|
+
return { success: true };
|
|
3645
|
+
}
|
|
3646
|
+
return new Promise((resolve2) => {
|
|
3647
|
+
const knipProcess = runner.spawn("npx", ["knip"], {
|
|
3648
|
+
cwd: process.cwd(),
|
|
3649
|
+
stdio: "pipe"
|
|
3650
|
+
});
|
|
3651
|
+
let knipOutput = "";
|
|
3652
|
+
let knipError = "";
|
|
3653
|
+
knipProcess.stdout?.on("data", (data) => {
|
|
3654
|
+
knipOutput += data.toString();
|
|
3655
|
+
});
|
|
3656
|
+
knipProcess.stderr?.on("data", (data) => {
|
|
3657
|
+
knipError += data.toString();
|
|
3658
|
+
});
|
|
3659
|
+
knipProcess.on("close", (code) => {
|
|
3660
|
+
if (code === 0) {
|
|
3661
|
+
resolve2({ success: true });
|
|
3662
|
+
} else {
|
|
3663
|
+
const errorOutput = knipOutput || knipError || "Unused code detected";
|
|
3664
|
+
resolve2({
|
|
3665
|
+
success: false,
|
|
3666
|
+
error: errorOutput
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3669
|
+
});
|
|
3670
|
+
knipProcess.on("error", (error) => {
|
|
3671
|
+
resolve2({ success: false, error: error.message });
|
|
3672
|
+
});
|
|
3673
|
+
});
|
|
3674
|
+
} catch (error) {
|
|
3675
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3676
|
+
return { success: false, error: errorMessage };
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
var knipStep = {
|
|
3680
|
+
id: STEP_IDS.KNIP,
|
|
3681
|
+
name: STEP_NAMES.KNIP,
|
|
3682
|
+
description: STEP_DESCRIPTIONS.KNIP,
|
|
3683
|
+
enabled: (context) => context.enabledValidations[VALIDATION_KEYS.KNIP] === true && validationEnabled(VALIDATION_KEYS.KNIP, VALIDATION_DEFAULTS) && !context.isFileSpecificMode,
|
|
3684
|
+
execute: async (context) => {
|
|
3685
|
+
const startTime = performance.now();
|
|
3686
|
+
try {
|
|
3687
|
+
const result = await validateKnip(context.scopeConfig);
|
|
3688
|
+
return {
|
|
3689
|
+
success: result.success,
|
|
3690
|
+
error: result.error,
|
|
3691
|
+
duration: performance.now() - startTime
|
|
3692
|
+
};
|
|
3693
|
+
} catch (error) {
|
|
3694
|
+
return {
|
|
3695
|
+
success: false,
|
|
3696
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3697
|
+
duration: performance.now() - startTime
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
};
|
|
3702
|
+
|
|
3703
|
+
// src/commands/validation/knip.ts
|
|
3704
|
+
async function knipCommand(options) {
|
|
3705
|
+
const { cwd, quiet } = options;
|
|
3706
|
+
const startTime = Date.now();
|
|
3707
|
+
if (!validationEnabled("KNIP", { KNIP: false })) {
|
|
3708
|
+
const output = quiet ? "" : "Knip: skipped (disabled by default, set KNIP_VALIDATION_ENABLED=1 to enable)";
|
|
3709
|
+
return { exitCode: 0, output, durationMs: Date.now() - startTime };
|
|
3710
|
+
}
|
|
3711
|
+
const toolResult = await discoverTool("knip", { projectRoot: cwd });
|
|
3712
|
+
if (!toolResult.found) {
|
|
3713
|
+
const skipMessage = formatSkipMessage("unused code detection", toolResult);
|
|
3714
|
+
return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
|
|
3715
|
+
}
|
|
3716
|
+
const scopeConfig = getTypeScriptScope("full");
|
|
3717
|
+
const result = await validateKnip(scopeConfig);
|
|
3718
|
+
const durationMs = Date.now() - startTime;
|
|
3719
|
+
if (result.success) {
|
|
3720
|
+
const output = quiet ? "" : `Knip: \u2713 No unused code found`;
|
|
3721
|
+
return { exitCode: 0, output, durationMs };
|
|
3722
|
+
} else {
|
|
3723
|
+
const output = result.error ?? "Unused code found";
|
|
3724
|
+
return { exitCode: 1, output, durationMs };
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
// src/commands/validation/lint.ts
|
|
3729
|
+
async function lintCommand(options) {
|
|
3730
|
+
const { cwd, scope = "full", files, fix, quiet } = options;
|
|
3731
|
+
const startTime = Date.now();
|
|
3732
|
+
const toolResult = await discoverTool("eslint", { projectRoot: cwd });
|
|
3733
|
+
if (!toolResult.found) {
|
|
3734
|
+
const skipMessage = formatSkipMessage("ESLint", toolResult);
|
|
3735
|
+
return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
|
|
3736
|
+
}
|
|
3737
|
+
const scopeConfig = getTypeScriptScope(scope);
|
|
3738
|
+
const context = {
|
|
3739
|
+
projectRoot: cwd,
|
|
3740
|
+
scope,
|
|
3741
|
+
scopeConfig,
|
|
3742
|
+
mode: fix ? "write" : "read",
|
|
3743
|
+
enabledValidations: { ESLINT: true },
|
|
3744
|
+
validatedFiles: files,
|
|
3745
|
+
isFileSpecificMode: Boolean(files && files.length > 0)
|
|
3746
|
+
};
|
|
3747
|
+
const result = await validateESLint(context);
|
|
3748
|
+
const durationMs = Date.now() - startTime;
|
|
3749
|
+
if (result.success) {
|
|
3750
|
+
const output = quiet ? "" : `ESLint: \u2713 No issues found`;
|
|
3751
|
+
return { exitCode: 0, output, durationMs };
|
|
3752
|
+
} else {
|
|
3753
|
+
const output = result.error ?? "ESLint validation failed";
|
|
3754
|
+
return { exitCode: 1, output, durationMs };
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
// src/validation/steps/typescript.ts
|
|
3759
|
+
import { spawn as spawn3 } from "child_process";
|
|
3760
|
+
import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
3761
|
+
import { mkdtemp } from "fs/promises";
|
|
3762
|
+
import { tmpdir } from "os";
|
|
3763
|
+
import { isAbsolute, join as join12 } from "path";
|
|
3764
|
+
var defaultTypeScriptProcessRunner = { spawn: spawn3 };
|
|
3765
|
+
var defaultTypeScriptDeps = {
|
|
3766
|
+
mkdtemp,
|
|
3767
|
+
writeFileSync,
|
|
3768
|
+
rmSync,
|
|
3769
|
+
existsSync: existsSync2,
|
|
3770
|
+
mkdirSync
|
|
3771
|
+
};
|
|
3772
|
+
function buildTypeScriptArgs(context) {
|
|
3773
|
+
const { scope, configFile } = context;
|
|
3774
|
+
return scope === VALIDATION_SCOPES.FULL ? ["tsc", "--noEmit"] : ["tsc", "--project", configFile];
|
|
3775
|
+
}
|
|
3776
|
+
async function createFileSpecificTsconfig(scope, files, deps = defaultTypeScriptDeps) {
|
|
3777
|
+
const tempDir = await deps.mkdtemp(join12(tmpdir(), "validate-ts-"));
|
|
3778
|
+
const configPath = join12(tempDir, "tsconfig.json");
|
|
3779
|
+
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
3780
|
+
const projectRoot = process.cwd();
|
|
3781
|
+
const absoluteFiles = files.map((file) => isAbsolute(file) ? file : join12(projectRoot, file));
|
|
3782
|
+
const tempConfig = {
|
|
3783
|
+
extends: join12(projectRoot, baseConfigFile),
|
|
3784
|
+
files: absoluteFiles,
|
|
3785
|
+
include: [],
|
|
3786
|
+
exclude: [],
|
|
3787
|
+
compilerOptions: {
|
|
3788
|
+
noEmit: true,
|
|
3789
|
+
typeRoots: [join12(projectRoot, "node_modules", "@types")],
|
|
3790
|
+
types: ["node"]
|
|
3791
|
+
}
|
|
3792
|
+
};
|
|
3793
|
+
deps.writeFileSync(configPath, JSON.stringify(tempConfig, null, 2));
|
|
3794
|
+
const cleanup = () => {
|
|
3795
|
+
try {
|
|
3796
|
+
deps.rmSync(tempDir, { recursive: true, force: true });
|
|
3797
|
+
} catch {
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3800
|
+
return { configPath, tempDir, cleanup };
|
|
3801
|
+
}
|
|
3802
|
+
async function validateTypeScript(scope, typescriptScope, files, runner = defaultTypeScriptProcessRunner, deps = defaultTypeScriptDeps) {
|
|
3803
|
+
const configFile = TSCONFIG_FILES[scope];
|
|
3804
|
+
let tool;
|
|
3805
|
+
let tscArgs;
|
|
3806
|
+
if (files && files.length > 0) {
|
|
3807
|
+
const { configPath, cleanup } = await createFileSpecificTsconfig(scope, files, deps);
|
|
3808
|
+
try {
|
|
3809
|
+
return await new Promise((resolve2) => {
|
|
3810
|
+
const tscProcess = runner.spawn("npx", ["tsc", "--project", configPath], {
|
|
3811
|
+
cwd: process.cwd(),
|
|
3812
|
+
stdio: "inherit"
|
|
3813
|
+
});
|
|
3814
|
+
tscProcess.on("close", (code) => {
|
|
3815
|
+
cleanup();
|
|
3816
|
+
if (code === 0) {
|
|
3817
|
+
resolve2({ success: true, skipped: false });
|
|
3818
|
+
} else {
|
|
3819
|
+
resolve2({ success: false, error: `TypeScript exited with code ${code}` });
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
tscProcess.on("error", (error) => {
|
|
3823
|
+
cleanup();
|
|
3824
|
+
resolve2({ success: false, error: error.message });
|
|
3825
|
+
});
|
|
3826
|
+
});
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
cleanup();
|
|
3829
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3830
|
+
return { success: false, error: `Failed to create temporary config: ${errorMessage}` };
|
|
3831
|
+
}
|
|
3832
|
+
} else {
|
|
3833
|
+
tool = "npx";
|
|
3834
|
+
tscArgs = buildTypeScriptArgs({ scope, configFile });
|
|
3835
|
+
}
|
|
3836
|
+
return new Promise((resolve2) => {
|
|
3837
|
+
const tscProcess = runner.spawn(tool, tscArgs, {
|
|
3838
|
+
cwd: process.cwd(),
|
|
3839
|
+
stdio: "inherit"
|
|
3840
|
+
});
|
|
3841
|
+
tscProcess.on("close", (code) => {
|
|
3842
|
+
if (code === 0) {
|
|
3843
|
+
resolve2({ success: true, skipped: false });
|
|
3844
|
+
} else {
|
|
3845
|
+
resolve2({ success: false, error: `TypeScript exited with code ${code}` });
|
|
3846
|
+
}
|
|
3847
|
+
});
|
|
3848
|
+
tscProcess.on("error", (error) => {
|
|
3849
|
+
resolve2({ success: false, error: error.message });
|
|
3850
|
+
});
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
var typescriptStep = {
|
|
3854
|
+
id: STEP_IDS.TYPESCRIPT,
|
|
3855
|
+
name: STEP_NAMES.TYPESCRIPT,
|
|
3856
|
+
description: STEP_DESCRIPTIONS.TYPESCRIPT,
|
|
3857
|
+
enabled: (context) => context.enabledValidations[VALIDATION_KEYS.TYPESCRIPT] === true && validationEnabled(VALIDATION_KEYS.TYPESCRIPT),
|
|
3858
|
+
execute: async (context) => {
|
|
3859
|
+
const startTime = performance.now();
|
|
3860
|
+
try {
|
|
3861
|
+
const result = await validateTypeScript(
|
|
3862
|
+
context.scope,
|
|
3863
|
+
context.scopeConfig,
|
|
3864
|
+
context.validatedFiles
|
|
3865
|
+
);
|
|
3866
|
+
return {
|
|
3867
|
+
success: result.success,
|
|
3868
|
+
error: result.error,
|
|
3869
|
+
duration: performance.now() - startTime,
|
|
3870
|
+
skipped: result.skipped
|
|
3871
|
+
};
|
|
3872
|
+
} catch (error) {
|
|
3873
|
+
return {
|
|
3874
|
+
success: false,
|
|
3875
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3876
|
+
duration: performance.now() - startTime
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
};
|
|
3881
|
+
|
|
3882
|
+
// src/commands/validation/typescript.ts
|
|
3883
|
+
async function typescriptCommand(options) {
|
|
3884
|
+
const { cwd, scope = "full", files, quiet } = options;
|
|
3885
|
+
const startTime = Date.now();
|
|
3886
|
+
const toolResult = await discoverTool("typescript", { projectRoot: cwd });
|
|
3887
|
+
if (!toolResult.found) {
|
|
3888
|
+
const skipMessage = formatSkipMessage("TypeScript", toolResult);
|
|
3889
|
+
return { exitCode: 0, output: skipMessage, durationMs: Date.now() - startTime };
|
|
3890
|
+
}
|
|
3891
|
+
const scopeConfig = getTypeScriptScope(scope);
|
|
3892
|
+
const result = await validateTypeScript(scope, scopeConfig, files);
|
|
3893
|
+
const durationMs = Date.now() - startTime;
|
|
3894
|
+
if (result.success) {
|
|
3895
|
+
const output = quiet ? "" : `TypeScript: \u2713 No type errors`;
|
|
3896
|
+
return { exitCode: 0, output, durationMs };
|
|
3897
|
+
} else {
|
|
3898
|
+
const output = result.error ?? "TypeScript validation failed";
|
|
3899
|
+
return { exitCode: 1, output, durationMs };
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
// src/commands/validation/all.ts
|
|
3904
|
+
var TOTAL_STEPS = 4;
|
|
3905
|
+
function formatStepWithTiming(stepNumber, result, quiet) {
|
|
3906
|
+
if (quiet || !result.output) return "";
|
|
3907
|
+
const timing = result.durationMs === void 0 ? "" : ` (${formatDuration(result.durationMs)})`;
|
|
3908
|
+
return `[${stepNumber}/${TOTAL_STEPS}] ${result.output}${timing}`;
|
|
3909
|
+
}
|
|
3910
|
+
async function allCommand(options) {
|
|
3911
|
+
const { cwd, scope, files, fix, quiet = false, json } = options;
|
|
3912
|
+
const startTime = Date.now();
|
|
3913
|
+
const outputs = [];
|
|
3914
|
+
let hasFailure = false;
|
|
3915
|
+
const circularResult = await circularCommand({ cwd, quiet, json });
|
|
3916
|
+
const circularOutput = formatStepWithTiming(1, circularResult, quiet);
|
|
3917
|
+
if (circularOutput) outputs.push(circularOutput);
|
|
3918
|
+
if (circularResult.exitCode !== 0) hasFailure = true;
|
|
3919
|
+
const knipResult = await knipCommand({ cwd, quiet, json });
|
|
3920
|
+
const knipOutput = formatStepWithTiming(2, knipResult, quiet);
|
|
3921
|
+
if (knipOutput) outputs.push(knipOutput);
|
|
3922
|
+
const lintResult = await lintCommand({ cwd, scope, files, fix, quiet, json });
|
|
3923
|
+
const lintOutput = formatStepWithTiming(3, lintResult, quiet);
|
|
3924
|
+
if (lintOutput) outputs.push(lintOutput);
|
|
3925
|
+
if (lintResult.exitCode !== 0) hasFailure = true;
|
|
3926
|
+
const tsResult = await typescriptCommand({ cwd, scope, files, quiet, json });
|
|
3927
|
+
const tsOutput = formatStepWithTiming(4, tsResult, quiet);
|
|
3928
|
+
if (tsOutput) outputs.push(tsOutput);
|
|
3929
|
+
if (tsResult.exitCode !== 0) hasFailure = true;
|
|
3930
|
+
const totalDurationMs = Date.now() - startTime;
|
|
3931
|
+
if (!quiet) {
|
|
3932
|
+
const summary = formatSummary({ success: !hasFailure, totalDurationMs });
|
|
3933
|
+
outputs.push("", summary);
|
|
3934
|
+
}
|
|
3935
|
+
return {
|
|
3936
|
+
exitCode: hasFailure ? 1 : 0,
|
|
3937
|
+
output: outputs.join("\n"),
|
|
3938
|
+
durationMs: totalDurationMs
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
// src/domains/validation/index.ts
|
|
3943
|
+
function addCommonOptions(cmd) {
|
|
3944
|
+
return cmd.option("--scope <scope>", "Validation scope (full|production)", "full").option("--files <paths...>", "Specific files/directories to validate").option("--quiet", "Suppress progress output").option("--json", "Output results as JSON");
|
|
3945
|
+
}
|
|
3946
|
+
function registerValidationCommands(validationCmd) {
|
|
3947
|
+
const tsCmd = validationCmd.command("typescript").alias("ts").description("Run TypeScript type checking").action(async (options) => {
|
|
3948
|
+
const result = await typescriptCommand({
|
|
3949
|
+
cwd: process.cwd(),
|
|
3950
|
+
scope: options.scope,
|
|
3951
|
+
files: options.files,
|
|
3952
|
+
quiet: options.quiet,
|
|
3953
|
+
json: options.json
|
|
3954
|
+
});
|
|
3955
|
+
if (result.output) console.log(result.output);
|
|
3956
|
+
process.exit(result.exitCode);
|
|
3957
|
+
});
|
|
3958
|
+
addCommonOptions(tsCmd);
|
|
3959
|
+
const lintCmd = validationCmd.command("lint").description("Run ESLint").option("--fix", "Auto-fix issues").action(async (options) => {
|
|
3960
|
+
const result = await lintCommand({
|
|
3961
|
+
cwd: process.cwd(),
|
|
3962
|
+
scope: options.scope,
|
|
3963
|
+
files: options.files,
|
|
3964
|
+
fix: options.fix,
|
|
3965
|
+
quiet: options.quiet,
|
|
3966
|
+
json: options.json
|
|
3967
|
+
});
|
|
3968
|
+
if (result.output) console.log(result.output);
|
|
3969
|
+
process.exit(result.exitCode);
|
|
3970
|
+
});
|
|
3971
|
+
addCommonOptions(lintCmd);
|
|
3972
|
+
const circularCmd = validationCmd.command("circular").description("Check for circular dependencies").action(async (options) => {
|
|
3973
|
+
const result = await circularCommand({
|
|
3974
|
+
cwd: process.cwd(),
|
|
3975
|
+
quiet: options.quiet,
|
|
3976
|
+
json: options.json
|
|
3977
|
+
});
|
|
3978
|
+
if (result.output) console.log(result.output);
|
|
3979
|
+
process.exit(result.exitCode);
|
|
3980
|
+
});
|
|
3981
|
+
addCommonOptions(circularCmd);
|
|
3982
|
+
const knipCmd = validationCmd.command("knip").description("Detect unused code").action(async (options) => {
|
|
3983
|
+
const result = await knipCommand({
|
|
3984
|
+
cwd: process.cwd(),
|
|
3985
|
+
quiet: options.quiet,
|
|
3986
|
+
json: options.json
|
|
3987
|
+
});
|
|
3988
|
+
if (result.output) console.log(result.output);
|
|
3989
|
+
process.exit(result.exitCode);
|
|
3990
|
+
});
|
|
3991
|
+
addCommonOptions(knipCmd);
|
|
3992
|
+
const allCmd = validationCmd.command("all", { isDefault: true }).description("Run all validations").option("--fix", "Auto-fix ESLint issues").action(async (options) => {
|
|
3993
|
+
const result = await allCommand({
|
|
3994
|
+
cwd: process.cwd(),
|
|
3995
|
+
scope: options.scope,
|
|
3996
|
+
files: options.files,
|
|
3997
|
+
fix: options.fix,
|
|
3998
|
+
quiet: options.quiet,
|
|
3999
|
+
json: options.json
|
|
4000
|
+
});
|
|
4001
|
+
if (result.output) console.log(result.output);
|
|
4002
|
+
process.exit(result.exitCode);
|
|
4003
|
+
});
|
|
4004
|
+
addCommonOptions(allCmd);
|
|
4005
|
+
}
|
|
4006
|
+
var validationDomain = {
|
|
4007
|
+
name: "validation",
|
|
4008
|
+
description: "Run code validation tools",
|
|
4009
|
+
register: (program2) => {
|
|
4010
|
+
const validationCmd = program2.command("validation").alias("v").description("Run code validation tools");
|
|
4011
|
+
registerValidationCommands(validationCmd);
|
|
4012
|
+
}
|
|
4013
|
+
};
|
|
4014
|
+
|
|
4015
|
+
// src/cli.ts
|
|
4016
|
+
var program = new Command();
|
|
4017
|
+
program.name("spx").description("Fast, deterministic CLI tool for spec workflow management").version("0.2.0");
|
|
4018
|
+
claudeDomain.register(program);
|
|
4019
|
+
sessionDomain.register(program);
|
|
4020
|
+
specDomain.register(program);
|
|
4021
|
+
validationDomain.register(program);
|
|
4022
|
+
program.parse();
|
|
4023
|
+
//# sourceMappingURL=cli.js.map
|