@outcomeeng/spx 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +317 -28
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -817,6 +817,42 @@ async function resolveSessionConfig(options = {}) {
|
|
|
817
817
|
};
|
|
818
818
|
}
|
|
819
819
|
|
|
820
|
+
// src/session/batch.ts
|
|
821
|
+
var BatchError = class extends Error {
|
|
822
|
+
results;
|
|
823
|
+
constructor(results) {
|
|
824
|
+
const failures = results.filter((r) => !r.ok);
|
|
825
|
+
const successes = results.filter((r) => r.ok);
|
|
826
|
+
super(
|
|
827
|
+
`${failures.length} of ${results.length} operations failed. ${successes.length} succeeded.`
|
|
828
|
+
);
|
|
829
|
+
this.name = "BatchError";
|
|
830
|
+
this.results = results;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
async function processBatch(ids, handler) {
|
|
834
|
+
const results = [];
|
|
835
|
+
for (const id of ids) {
|
|
836
|
+
try {
|
|
837
|
+
const output2 = await handler(id);
|
|
838
|
+
results.push({ id, ok: true, message: output2 });
|
|
839
|
+
} catch (error) {
|
|
840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
841
|
+
results.push({ id, ok: false, message });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const output = results.map((r) => r.ok ? r.message : `Error (${r.id}): ${r.message}`).join("\n\n");
|
|
845
|
+
const hasFailures = results.some((r) => !r.ok);
|
|
846
|
+
if (hasFailures) {
|
|
847
|
+
const err = new BatchError(results);
|
|
848
|
+
err.message = `${err.message}
|
|
849
|
+
|
|
850
|
+
${output}`;
|
|
851
|
+
throw err;
|
|
852
|
+
}
|
|
853
|
+
return output;
|
|
854
|
+
}
|
|
855
|
+
|
|
820
856
|
// src/session/errors.ts
|
|
821
857
|
var SessionError = class extends Error {
|
|
822
858
|
constructor(message) {
|
|
@@ -908,14 +944,17 @@ async function resolveArchivePaths(sessionId, config) {
|
|
|
908
944
|
}
|
|
909
945
|
throw new SessionNotFoundError(sessionId);
|
|
910
946
|
}
|
|
911
|
-
async function
|
|
912
|
-
const {
|
|
913
|
-
const { source, target } = await resolveArchivePaths(options.sessionId, config);
|
|
947
|
+
async function archiveSingle(sessionId, config) {
|
|
948
|
+
const { source, target } = await resolveArchivePaths(sessionId, config);
|
|
914
949
|
await mkdir(dirname2(target), { recursive: true });
|
|
915
950
|
await rename(source, target);
|
|
916
|
-
return `Archived session: ${
|
|
951
|
+
return `Archived session: ${sessionId}
|
|
917
952
|
Archive location: ${target}`;
|
|
918
953
|
}
|
|
954
|
+
async function archiveCommand(options) {
|
|
955
|
+
const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
|
|
956
|
+
return processBatch(options.sessionIds, (id) => archiveSingle(id, config));
|
|
957
|
+
}
|
|
919
958
|
|
|
920
959
|
// src/commands/session/delete.ts
|
|
921
960
|
import { stat as stat2, unlink } from "fs/promises";
|
|
@@ -1108,13 +1147,16 @@ async function findExistingPaths(paths) {
|
|
|
1108
1147
|
}
|
|
1109
1148
|
return existing;
|
|
1110
1149
|
}
|
|
1111
|
-
async function
|
|
1112
|
-
const
|
|
1113
|
-
const paths = resolveSessionPaths(options.sessionId, config);
|
|
1150
|
+
async function deleteSingle(sessionId, config) {
|
|
1151
|
+
const paths = resolveSessionPaths(sessionId, config);
|
|
1114
1152
|
const existingPaths = await findExistingPaths(paths);
|
|
1115
|
-
const pathToDelete = resolveDeletePath(
|
|
1153
|
+
const pathToDelete = resolveDeletePath(sessionId, existingPaths);
|
|
1116
1154
|
await unlink(pathToDelete);
|
|
1117
|
-
return `Deleted session: ${
|
|
1155
|
+
return `Deleted session: ${sessionId}`;
|
|
1156
|
+
}
|
|
1157
|
+
async function deleteCommand(options) {
|
|
1158
|
+
const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
|
|
1159
|
+
return processBatch(options.sessionIds, (id) => deleteSingle(id, config));
|
|
1118
1160
|
}
|
|
1119
1161
|
|
|
1120
1162
|
// src/commands/session/handoff.ts
|
|
@@ -1511,16 +1553,19 @@ async function findExistingPath(paths, _config) {
|
|
|
1511
1553
|
}
|
|
1512
1554
|
return null;
|
|
1513
1555
|
}
|
|
1514
|
-
async function
|
|
1515
|
-
const
|
|
1516
|
-
const paths = resolveSessionPaths(options.sessionId, config);
|
|
1556
|
+
async function showSingle(sessionId, config) {
|
|
1557
|
+
const paths = resolveSessionPaths(sessionId, config);
|
|
1517
1558
|
const found = await findExistingPath(paths, config);
|
|
1518
1559
|
if (!found) {
|
|
1519
|
-
throw new SessionNotFoundError(
|
|
1560
|
+
throw new SessionNotFoundError(sessionId);
|
|
1520
1561
|
}
|
|
1521
1562
|
const content = await readFile4(found.path, "utf-8");
|
|
1522
1563
|
return formatShowOutput(content, { status: found.status });
|
|
1523
1564
|
}
|
|
1565
|
+
async function showCommand(options) {
|
|
1566
|
+
const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
|
|
1567
|
+
return processBatch(options.sessionIds, (id) => showSingle(id, config));
|
|
1568
|
+
}
|
|
1524
1569
|
|
|
1525
1570
|
// src/domains/session/help.ts
|
|
1526
1571
|
var SESSION_FORMAT_HELP = `
|
|
@@ -1539,7 +1584,8 @@ Workflow:
|
|
|
1539
1584
|
1. handoff - Create session (todo)
|
|
1540
1585
|
2. pickup - Claim session (todo -> doing)
|
|
1541
1586
|
3. release - Return session (doing -> todo)
|
|
1542
|
-
4.
|
|
1587
|
+
4. archive - Move session to archive
|
|
1588
|
+
5. delete - Remove session permanently
|
|
1543
1589
|
`;
|
|
1544
1590
|
var HANDOFF_FRONTMATTER_HELP = `
|
|
1545
1591
|
Usage:
|
|
@@ -1627,10 +1673,10 @@ function registerSessionCommands(sessionCmd) {
|
|
|
1627
1673
|
handleError(error);
|
|
1628
1674
|
}
|
|
1629
1675
|
});
|
|
1630
|
-
sessionCmd.command("show <id
|
|
1676
|
+
sessionCmd.command("show <id...>").description("Show session content").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
|
|
1631
1677
|
try {
|
|
1632
1678
|
const output = await showCommand({
|
|
1633
|
-
|
|
1679
|
+
sessionIds: ids,
|
|
1634
1680
|
sessionsDir: options.sessionsDir
|
|
1635
1681
|
});
|
|
1636
1682
|
console.log(output);
|
|
@@ -1677,10 +1723,10 @@ function registerSessionCommands(sessionCmd) {
|
|
|
1677
1723
|
handleError(error);
|
|
1678
1724
|
}
|
|
1679
1725
|
});
|
|
1680
|
-
sessionCmd.command("delete <id
|
|
1726
|
+
sessionCmd.command("delete <id...>").description("Delete one or more sessions").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
|
|
1681
1727
|
try {
|
|
1682
1728
|
const output = await deleteCommand({
|
|
1683
|
-
|
|
1729
|
+
sessionIds: ids,
|
|
1684
1730
|
sessionsDir: options.sessionsDir
|
|
1685
1731
|
});
|
|
1686
1732
|
console.log(output);
|
|
@@ -1705,10 +1751,10 @@ function registerSessionCommands(sessionCmd) {
|
|
|
1705
1751
|
handleError(error);
|
|
1706
1752
|
}
|
|
1707
1753
|
});
|
|
1708
|
-
sessionCmd.command("archive <id
|
|
1754
|
+
sessionCmd.command("archive <id...>").description("Move one or more sessions to the archive directory").option("--sessions-dir <path>", "Custom sessions directory").action(async (ids, options) => {
|
|
1709
1755
|
try {
|
|
1710
1756
|
const output = await archiveCommand({
|
|
1711
|
-
|
|
1757
|
+
sessionIds: ids,
|
|
1712
1758
|
sessionsDir: options.sessionsDir
|
|
1713
1759
|
});
|
|
1714
1760
|
console.log(output);
|
|
@@ -1730,6 +1776,10 @@ var sessionDomain = {
|
|
|
1730
1776
|
}
|
|
1731
1777
|
};
|
|
1732
1778
|
|
|
1779
|
+
// src/domains/spec/index.ts
|
|
1780
|
+
import { access as access2 } from "fs/promises";
|
|
1781
|
+
import { readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
1782
|
+
|
|
1733
1783
|
// src/scanner/scanner.ts
|
|
1734
1784
|
import path5 from "path";
|
|
1735
1785
|
var Scanner = class {
|
|
@@ -2287,6 +2337,226 @@ This command is for legacy specs/ projects. For CODE framework projects, check t
|
|
|
2287
2337
|
}
|
|
2288
2338
|
}
|
|
2289
2339
|
|
|
2340
|
+
// src/spec/apply/exclude/adapters/detect.ts
|
|
2341
|
+
import { join as join8 } from "path";
|
|
2342
|
+
|
|
2343
|
+
// src/spec/apply/exclude/constants.ts
|
|
2344
|
+
var SPX_PREFIX = "spx/";
|
|
2345
|
+
var NODE_SUFFIXES = [".outcome/", ".enabler/", ".capability/", ".feature/", ".story/"];
|
|
2346
|
+
var COMMENT_CHAR = "#";
|
|
2347
|
+
var EXCLUDE_FILENAME = "EXCLUDE";
|
|
2348
|
+
|
|
2349
|
+
// src/spec/apply/exclude/mappings.ts
|
|
2350
|
+
function toPytestIgnore(node) {
|
|
2351
|
+
return `--ignore=${SPX_PREFIX}${node}/`;
|
|
2352
|
+
}
|
|
2353
|
+
function escapeRegex(str) {
|
|
2354
|
+
return str.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
|
|
2355
|
+
}
|
|
2356
|
+
function toMypyRegex(node) {
|
|
2357
|
+
const escaped = escapeRegex(`${SPX_PREFIX}${node}/`);
|
|
2358
|
+
return `^${escaped}`;
|
|
2359
|
+
}
|
|
2360
|
+
function toPyrightPath(node) {
|
|
2361
|
+
return `${SPX_PREFIX}${node}/`;
|
|
2362
|
+
}
|
|
2363
|
+
function isExcludedEntry(val) {
|
|
2364
|
+
const hasPrefix = val.includes(SPX_PREFIX);
|
|
2365
|
+
const hasSuffix = NODE_SUFFIXES.some((suffix) => val.includes(suffix));
|
|
2366
|
+
return hasPrefix && hasSuffix;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// src/spec/apply/exclude/adapters/python.ts
|
|
2370
|
+
var PYTHON_CONFIG_FILE = "pyproject.toml";
|
|
2371
|
+
var PYTEST_SECTION = "tool.pytest.ini_options";
|
|
2372
|
+
var MYPY_SECTION = "tool.mypy";
|
|
2373
|
+
var PYRIGHT_SECTION = "tool.pyright";
|
|
2374
|
+
var ARRAY_INDENT = " ";
|
|
2375
|
+
function updatePytestAddopts(content, nodes) {
|
|
2376
|
+
const sectionPattern = new RegExp(`^\\[${PYTEST_SECTION.replace(/\./g, "\\.")}\\]`, "m");
|
|
2377
|
+
const sectionMatch = sectionPattern.exec(content);
|
|
2378
|
+
if (!sectionMatch) return content;
|
|
2379
|
+
const afterSection = content.slice(sectionMatch.index);
|
|
2380
|
+
const addoptsPattern = /^([ \t]*addopts\s*=\s*")((?:[^"\\]|\\.)*)(")/m;
|
|
2381
|
+
const addoptsMatch = addoptsPattern.exec(afterSection);
|
|
2382
|
+
if (!addoptsMatch) return content;
|
|
2383
|
+
const prefix = addoptsMatch[1];
|
|
2384
|
+
const currentValue = addoptsMatch[2];
|
|
2385
|
+
const suffix = addoptsMatch[3];
|
|
2386
|
+
const parts = currentValue.split(/\s+/).filter((p) => p.length > 0);
|
|
2387
|
+
const kept = parts.filter((p) => !isExcludedEntry(p));
|
|
2388
|
+
const newIgnores = nodes.map(toPytestIgnore);
|
|
2389
|
+
const updatedValue = [...kept, ...newIgnores].join(" ");
|
|
2390
|
+
const absoluteStart = sectionMatch.index + addoptsMatch.index;
|
|
2391
|
+
const absoluteEnd = absoluteStart + addoptsMatch[0].length;
|
|
2392
|
+
return content.slice(0, absoluteStart) + prefix + updatedValue + suffix + content.slice(absoluteEnd);
|
|
2393
|
+
}
|
|
2394
|
+
function findTomlArray(content, sectionHeader, key) {
|
|
2395
|
+
const sectionPattern = new RegExp(`^\\[${sectionHeader.replace(/\./g, "\\.")}\\]`, "m");
|
|
2396
|
+
const sectionMatch = sectionPattern.exec(content);
|
|
2397
|
+
if (!sectionMatch) return null;
|
|
2398
|
+
const afterSection = content.slice(sectionMatch.index);
|
|
2399
|
+
const nextSectionMatch = /^\[(?!.*\].*=)/m.exec(afterSection.slice(sectionMatch[0].length));
|
|
2400
|
+
const sectionEnd = nextSectionMatch ? sectionMatch.index + sectionMatch[0].length + nextSectionMatch.index : content.length;
|
|
2401
|
+
const keyPattern = new RegExp(`^([ \\t]*${key}\\s*=\\s*)\\[`, "m");
|
|
2402
|
+
const regionToSearch = content.slice(sectionMatch.index, sectionEnd);
|
|
2403
|
+
const keyMatch = keyPattern.exec(regionToSearch);
|
|
2404
|
+
if (!keyMatch) return null;
|
|
2405
|
+
const arrayStart = sectionMatch.index + keyMatch.index + keyMatch[1].length;
|
|
2406
|
+
let depth = 0;
|
|
2407
|
+
let arrayEnd = arrayStart;
|
|
2408
|
+
for (let i = arrayStart; i < content.length; i++) {
|
|
2409
|
+
if (content[i] === "[") depth++;
|
|
2410
|
+
if (content[i] === "]") {
|
|
2411
|
+
depth--;
|
|
2412
|
+
if (depth === 0) {
|
|
2413
|
+
arrayEnd = i + 1;
|
|
2414
|
+
break;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return { arrayStart, arrayEnd };
|
|
2419
|
+
}
|
|
2420
|
+
function updateTomlArray(content, sectionHeader, key, newEntries) {
|
|
2421
|
+
const info = findTomlArray(content, sectionHeader, key);
|
|
2422
|
+
if (!info) return content;
|
|
2423
|
+
const arrayContent = content.slice(info.arrayStart + 1, info.arrayEnd - 1);
|
|
2424
|
+
const lines = arrayContent.split("\n");
|
|
2425
|
+
const keptLines = [];
|
|
2426
|
+
for (const line of lines) {
|
|
2427
|
+
const trimmed = line.trim();
|
|
2428
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
2429
|
+
keptLines.push(line);
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
const stringMatch = /"((?:[^"\\]|\\.)*)"/.exec(trimmed);
|
|
2433
|
+
if (stringMatch && isExcludedEntry(stringMatch[1])) {
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
keptLines.push(line);
|
|
2437
|
+
}
|
|
2438
|
+
const newLines = newEntries.map((entry) => `${ARRAY_INDENT}"${entry}",`);
|
|
2439
|
+
while (keptLines.length > 0 && keptLines[keptLines.length - 1].trim() === "") {
|
|
2440
|
+
keptLines.pop();
|
|
2441
|
+
}
|
|
2442
|
+
const reconstructed = [...keptLines, ...newLines, ""].join("\n");
|
|
2443
|
+
return content.slice(0, info.arrayStart + 1) + reconstructed + content.slice(info.arrayEnd - 1);
|
|
2444
|
+
}
|
|
2445
|
+
var pythonAdapter = {
|
|
2446
|
+
language: "Python",
|
|
2447
|
+
configFile: PYTHON_CONFIG_FILE,
|
|
2448
|
+
tools: ["pytest", "mypy", "pyright"],
|
|
2449
|
+
excluded: ["ruff (style checked regardless of implementation existence)"],
|
|
2450
|
+
applyExclusions(content, nodes) {
|
|
2451
|
+
let result = content;
|
|
2452
|
+
result = updatePytestAddopts(result, nodes);
|
|
2453
|
+
const mypyEntries = nodes.map(toMypyRegex);
|
|
2454
|
+
result = updateTomlArray(result, MYPY_SECTION, "exclude", mypyEntries);
|
|
2455
|
+
const pyrightEntries = nodes.map(toPyrightPath);
|
|
2456
|
+
result = updateTomlArray(result, PYRIGHT_SECTION, "exclude", pyrightEntries);
|
|
2457
|
+
return {
|
|
2458
|
+
changed: result !== content,
|
|
2459
|
+
content: result
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
|
|
2464
|
+
// src/spec/apply/exclude/adapters/detect.ts
|
|
2465
|
+
var ADAPTERS = [pythonAdapter];
|
|
2466
|
+
async function detectLanguage(projectRoot, deps) {
|
|
2467
|
+
for (const adapter of ADAPTERS) {
|
|
2468
|
+
const configPath = join8(projectRoot, adapter.configFile);
|
|
2469
|
+
const exists = await deps.fileExists(configPath);
|
|
2470
|
+
if (exists) {
|
|
2471
|
+
return adapter;
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return null;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// src/spec/apply/exclude/help.ts
|
|
2478
|
+
function buildApplyHelp() {
|
|
2479
|
+
const excludePath = `${SPX_PREFIX}${EXCLUDE_FILENAME}`;
|
|
2480
|
+
const languageLines = ADAPTERS.map((adapter) => {
|
|
2481
|
+
const tools = adapter.tools.join(", ");
|
|
2482
|
+
const excluded = adapter.excluded.length > 0 ? `
|
|
2483
|
+
NOT configured: ${adapter.excluded.join(", ")}` : "";
|
|
2484
|
+
return ` ${adapter.language} (${adapter.configFile})
|
|
2485
|
+
Configures: ${tools}${excluded}`;
|
|
2486
|
+
});
|
|
2487
|
+
return `
|
|
2488
|
+
Reads ${excludePath} and applies exclusions to the project's tool configuration.
|
|
2489
|
+
Each excluded node is translated into the appropriate format for the detected
|
|
2490
|
+
language's test runner, type checker, and other quality gate tools.
|
|
2491
|
+
|
|
2492
|
+
Source:
|
|
2493
|
+
${excludePath}
|
|
2494
|
+
One node path per line. Comments (#) and blank lines are ignored.
|
|
2495
|
+
Path traversal (..) and absolute paths are rejected.
|
|
2496
|
+
|
|
2497
|
+
Supported languages:
|
|
2498
|
+
${languageLines.join("\n\n")}
|
|
2499
|
+
|
|
2500
|
+
Detection:
|
|
2501
|
+
The language is auto-detected by checking for config files in order.
|
|
2502
|
+
The first match wins.
|
|
2503
|
+
|
|
2504
|
+
Examples:
|
|
2505
|
+
spx spec apply # Apply exclusions to detected config file
|
|
2506
|
+
`;
|
|
2507
|
+
}
|
|
2508
|
+
var APPLY_HELP = buildApplyHelp();
|
|
2509
|
+
|
|
2510
|
+
// src/spec/apply/exclude/exclude-file.ts
|
|
2511
|
+
var TOML_UNSAFE_PATTERN = /["\\\n\r\t]/;
|
|
2512
|
+
var PATH_TRAVERSAL_PATTERN = /(?:^|\/)\.\.(?:\/|$)/;
|
|
2513
|
+
function validateNodePath(path8) {
|
|
2514
|
+
if (path8.startsWith("/")) {
|
|
2515
|
+
return `absolute path rejected: ${path8}`;
|
|
2516
|
+
}
|
|
2517
|
+
if (PATH_TRAVERSAL_PATTERN.test(path8)) {
|
|
2518
|
+
return `path traversal rejected: ${path8}`;
|
|
2519
|
+
}
|
|
2520
|
+
if (TOML_UNSAFE_PATTERN.test(path8)) {
|
|
2521
|
+
return `TOML-unsafe characters rejected: ${path8}`;
|
|
2522
|
+
}
|
|
2523
|
+
return null;
|
|
2524
|
+
}
|
|
2525
|
+
function readExcludedNodes(content) {
|
|
2526
|
+
return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith(COMMENT_CHAR)).filter((line) => validateNodePath(line) === null);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/spec/apply/exclude/command.ts
|
|
2530
|
+
import { join as join9 } from "path";
|
|
2531
|
+
async function applyExcludeCommand(options) {
|
|
2532
|
+
const { cwd, deps } = options;
|
|
2533
|
+
const excludePath = join9(cwd, SPX_PREFIX, EXCLUDE_FILENAME);
|
|
2534
|
+
const excludeExists = await deps.fileExists(excludePath);
|
|
2535
|
+
if (!excludeExists) {
|
|
2536
|
+
return { exitCode: 1, output: `error: ${excludePath} not found` };
|
|
2537
|
+
}
|
|
2538
|
+
const adapter = await detectLanguage(cwd, deps);
|
|
2539
|
+
if (!adapter) {
|
|
2540
|
+
return { exitCode: 1, output: "error: no supported config file found (checked: pyproject.toml)" };
|
|
2541
|
+
}
|
|
2542
|
+
const configPath = join9(cwd, adapter.configFile);
|
|
2543
|
+
const excludeContent = await deps.readFile(excludePath);
|
|
2544
|
+
const configContent = await deps.readFile(configPath);
|
|
2545
|
+
const nodes = readExcludedNodes(excludeContent);
|
|
2546
|
+
if (nodes.length === 0) {
|
|
2547
|
+
return { exitCode: 0, output: "spx/EXCLUDE is empty \u2014 no excluded nodes to apply." };
|
|
2548
|
+
}
|
|
2549
|
+
const result = adapter.applyExclusions(configContent, nodes);
|
|
2550
|
+
if (result.changed) {
|
|
2551
|
+
await deps.writeFile(configPath, result.content);
|
|
2552
|
+
return {
|
|
2553
|
+
exitCode: 0,
|
|
2554
|
+
output: `Updated ${adapter.configFile} from spx/EXCLUDE (${nodes.length} nodes).`
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
return { exitCode: 0, output: `${adapter.configFile} is already in sync with spx/EXCLUDE.` };
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2290
2560
|
// src/domains/spec/index.ts
|
|
2291
2561
|
function registerSpecCommands(specCmd) {
|
|
2292
2562
|
specCmd.command("status").description("Get project status").option("--json", "Output as JSON").option("--format <format>", "Output format (text|json|markdown|table)").action(async (options) => {
|
|
@@ -2327,6 +2597,25 @@ function registerSpecCommands(specCmd) {
|
|
|
2327
2597
|
process.exit(1);
|
|
2328
2598
|
}
|
|
2329
2599
|
});
|
|
2600
|
+
specCmd.command("apply").description("Apply spec-tree state to project configuration").addHelpText("after", APPLY_HELP).action(async () => {
|
|
2601
|
+
const result = await applyExcludeCommand({
|
|
2602
|
+
cwd: process.cwd(),
|
|
2603
|
+
deps: {
|
|
2604
|
+
readFile: (path8) => readFile5(path8, "utf-8"),
|
|
2605
|
+
writeFile: (path8, content) => writeFile2(path8, content, "utf-8"),
|
|
2606
|
+
fileExists: async (path8) => {
|
|
2607
|
+
try {
|
|
2608
|
+
await access2(path8);
|
|
2609
|
+
return true;
|
|
2610
|
+
} catch {
|
|
2611
|
+
return false;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
});
|
|
2616
|
+
if (result.output) console.log(result.output);
|
|
2617
|
+
process.exit(result.exitCode);
|
|
2618
|
+
});
|
|
2330
2619
|
}
|
|
2331
2620
|
var specDomain = {
|
|
2332
2621
|
name: "spec",
|
|
@@ -3198,7 +3487,7 @@ var ParseErrorCode;
|
|
|
3198
3487
|
|
|
3199
3488
|
// src/validation/config/scope.ts
|
|
3200
3489
|
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
3201
|
-
import { join as
|
|
3490
|
+
import { join as join10 } from "path";
|
|
3202
3491
|
var TSCONFIG_FILES = {
|
|
3203
3492
|
full: "tsconfig.json",
|
|
3204
3493
|
production: "tsconfig.production.json"
|
|
@@ -3245,7 +3534,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
|
|
|
3245
3534
|
if (hasDirectTsFiles) return true;
|
|
3246
3535
|
const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
|
|
3247
3536
|
for (const subdir of subdirs.slice(0, 5)) {
|
|
3248
|
-
if (hasTypeScriptFilesRecursive(
|
|
3537
|
+
if (hasTypeScriptFilesRecursive(join10(dirPath, subdir.name), maxDepth - 1, deps)) {
|
|
3249
3538
|
return true;
|
|
3250
3539
|
}
|
|
3251
3540
|
}
|
|
@@ -3777,7 +4066,7 @@ import { spawn as spawn3 } from "child_process";
|
|
|
3777
4066
|
import { existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
3778
4067
|
import { mkdtemp } from "fs/promises";
|
|
3779
4068
|
import { tmpdir } from "os";
|
|
3780
|
-
import { isAbsolute as isAbsolute2, join as
|
|
4069
|
+
import { isAbsolute as isAbsolute2, join as join11 } from "path";
|
|
3781
4070
|
var defaultTypeScriptProcessRunner = { spawn: spawn3 };
|
|
3782
4071
|
var defaultTypeScriptDeps = {
|
|
3783
4072
|
mkdtemp,
|
|
@@ -3791,19 +4080,19 @@ function buildTypeScriptArgs(context) {
|
|
|
3791
4080
|
return scope === VALIDATION_SCOPES.FULL ? ["tsc", "--noEmit"] : ["tsc", "--project", configFile];
|
|
3792
4081
|
}
|
|
3793
4082
|
async function createFileSpecificTsconfig(scope, files, deps = defaultTypeScriptDeps) {
|
|
3794
|
-
const tempDir = await deps.mkdtemp(
|
|
3795
|
-
const configPath =
|
|
4083
|
+
const tempDir = await deps.mkdtemp(join11(tmpdir(), "validate-ts-"));
|
|
4084
|
+
const configPath = join11(tempDir, "tsconfig.json");
|
|
3796
4085
|
const baseConfigFile = TSCONFIG_FILES[scope];
|
|
3797
4086
|
const projectRoot = process.cwd();
|
|
3798
|
-
const absoluteFiles = files.map((file) => isAbsolute2(file) ? file :
|
|
4087
|
+
const absoluteFiles = files.map((file) => isAbsolute2(file) ? file : join11(projectRoot, file));
|
|
3799
4088
|
const tempConfig = {
|
|
3800
|
-
extends:
|
|
4089
|
+
extends: join11(projectRoot, baseConfigFile),
|
|
3801
4090
|
files: absoluteFiles,
|
|
3802
4091
|
include: [],
|
|
3803
4092
|
exclude: [],
|
|
3804
4093
|
compilerOptions: {
|
|
3805
4094
|
noEmit: true,
|
|
3806
|
-
typeRoots: [
|
|
4095
|
+
typeRoots: [join11(projectRoot, "node_modules", "@types")],
|
|
3807
4096
|
types: ["node"]
|
|
3808
4097
|
}
|
|
3809
4098
|
};
|