instrlint 0.1.2 → 0.1.5
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 +3 -1
- package/README.zh-TW.md +3 -1
- package/dist/cli.cjs +341 -118
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +337 -114
- package/dist/cli.js.map +1 -1
- package/package.json +5 -2
- package/skills/claude-code/SKILL.md +24 -25
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/run-command.ts
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
|
+
import { createInterface } from "readline";
|
|
8
9
|
import { basename as basename2 } from "path";
|
|
9
10
|
import chalk4 from "chalk";
|
|
10
11
|
|
|
@@ -787,8 +788,12 @@ function analyzeBudget(instructions) {
|
|
|
787
788
|
autoFixable: false
|
|
788
789
|
});
|
|
789
790
|
}
|
|
790
|
-
const { tokens: rulesTokens, method: rulesMethod } = sumTokens(
|
|
791
|
-
|
|
791
|
+
const { tokens: rulesTokens, method: rulesMethod } = sumTokens(
|
|
792
|
+
instructions.rules
|
|
793
|
+
);
|
|
794
|
+
const { tokens: skillsTokens, method: skillsMethod } = sumTokens(
|
|
795
|
+
instructions.skills
|
|
796
|
+
);
|
|
792
797
|
const { tokens: subFilesTokens, method: subFilesMethod } = sumTokens(
|
|
793
798
|
instructions.subFiles
|
|
794
799
|
);
|
|
@@ -826,11 +831,18 @@ function analyzeBudget(instructions) {
|
|
|
826
831
|
autoFixable: false
|
|
827
832
|
});
|
|
828
833
|
}
|
|
829
|
-
const tokenMethod = [
|
|
830
|
-
|
|
831
|
-
|
|
834
|
+
const tokenMethod = [
|
|
835
|
+
rootFileMethod,
|
|
836
|
+
rulesMethod,
|
|
837
|
+
skillsMethod,
|
|
838
|
+
subFilesMethod
|
|
839
|
+
].every((m) => m === "measured") ? "measured" : "estimated";
|
|
832
840
|
const fileBreakdown = [
|
|
833
|
-
{
|
|
841
|
+
{
|
|
842
|
+
path: instructions.rootFile.path,
|
|
843
|
+
tokenCount: rootFileTokens,
|
|
844
|
+
tokenMethod: rootFileMethod
|
|
845
|
+
},
|
|
834
846
|
...instructions.rules.map((r) => ({
|
|
835
847
|
path: r.path,
|
|
836
848
|
tokenCount: r.tokenCount,
|
|
@@ -850,6 +862,7 @@ function analyzeBudget(instructions) {
|
|
|
850
862
|
const summary = {
|
|
851
863
|
systemPromptTokens: SYSTEM_PROMPT_TOKENS,
|
|
852
864
|
rootFileTokens,
|
|
865
|
+
rootFileLines: rootLines,
|
|
853
866
|
rootFileMethod,
|
|
854
867
|
rulesTokens,
|
|
855
868
|
rulesMethod,
|
|
@@ -1426,6 +1439,8 @@ var INFO_DEDUCTION = 1;
|
|
|
1426
1439
|
var MAX_CRITICAL_DEDUCTION = 40;
|
|
1427
1440
|
var MAX_WARNING_DEDUCTION = 30;
|
|
1428
1441
|
var MAX_INFO_DEDUCTION = 10;
|
|
1442
|
+
var MAX_ROOT_FILE_PENALTY = 30;
|
|
1443
|
+
var MAX_BUDGET_DEDUCTION = 30;
|
|
1429
1444
|
function gradeFromScore(score) {
|
|
1430
1445
|
if (score >= 90) return "A";
|
|
1431
1446
|
if (score >= 80) return "B";
|
|
@@ -1437,16 +1452,32 @@ function calculateScore(findings, budget) {
|
|
|
1437
1452
|
const criticals = findings.filter((f) => f.severity === "critical").length;
|
|
1438
1453
|
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
1439
1454
|
const infos = findings.filter((f) => f.severity === "info").length;
|
|
1440
|
-
const criticalDeduction = Math.min(
|
|
1441
|
-
|
|
1455
|
+
const criticalDeduction = Math.min(
|
|
1456
|
+
criticals * CRITICAL_DEDUCTION,
|
|
1457
|
+
MAX_CRITICAL_DEDUCTION
|
|
1458
|
+
);
|
|
1459
|
+
const warningDeduction = Math.min(
|
|
1460
|
+
warnings * WARNING_DEDUCTION,
|
|
1461
|
+
MAX_WARNING_DEDUCTION
|
|
1462
|
+
);
|
|
1442
1463
|
const infoDeduction = Math.min(infos * INFO_DEDUCTION, MAX_INFO_DEDUCTION);
|
|
1464
|
+
const rootLines = budget.rootFileLines;
|
|
1465
|
+
let rootFilePenalty = 0;
|
|
1466
|
+
if (rootLines > 400) {
|
|
1467
|
+
rootFilePenalty = 10 + Math.floor((rootLines - 400) / 100) * 5;
|
|
1468
|
+
} else if (rootLines > 200) {
|
|
1469
|
+
rootFilePenalty = 5 + Math.floor((rootLines - 200) / 100) * 3;
|
|
1470
|
+
}
|
|
1471
|
+
rootFilePenalty = Math.min(rootFilePenalty, MAX_ROOT_FILE_PENALTY);
|
|
1443
1472
|
const baselinePct = budget.totalBaseline / CONTEXT_WINDOW2;
|
|
1444
1473
|
let budgetDeduction = 0;
|
|
1445
|
-
if (baselinePct > 0.
|
|
1446
|
-
|
|
1474
|
+
if (baselinePct > 0.25) {
|
|
1475
|
+
budgetDeduction = 5 + Math.floor((baselinePct - 0.25) * 40);
|
|
1476
|
+
}
|
|
1477
|
+
budgetDeduction = Math.min(budgetDeduction, MAX_BUDGET_DEDUCTION);
|
|
1447
1478
|
const score = Math.max(
|
|
1448
1479
|
0,
|
|
1449
|
-
100 - criticalDeduction - warningDeduction - infoDeduction - budgetDeduction
|
|
1480
|
+
100 - criticalDeduction - warningDeduction - infoDeduction - rootFilePenalty - budgetDeduction
|
|
1450
1481
|
);
|
|
1451
1482
|
return { score, grade: gradeFromScore(score) };
|
|
1452
1483
|
}
|
|
@@ -1558,6 +1589,8 @@ var en_default = {
|
|
|
1558
1589
|
"markdown.budgetIssues": "Budget Issues",
|
|
1559
1590
|
"markdown.refactoringOpportunities": "Refactoring Opportunities",
|
|
1560
1591
|
"markdown.lineRef": "(line {{line}})",
|
|
1592
|
+
"markdown.budgetCategory": "Category",
|
|
1593
|
+
"markdown.budgetTokens": "Tokens",
|
|
1561
1594
|
"markdown.actionPlan": "## Action Plan",
|
|
1562
1595
|
"markdown.attribution": "*Generated by [instrlint](https://github.com/jed1978/instrlint)*",
|
|
1563
1596
|
"ci.passed": "\u2713 instrlint passed score={{score}} grade={{grade}}",
|
|
@@ -1565,9 +1598,13 @@ var en_default = {
|
|
|
1565
1598
|
"ci.writtenTo": "\u2192 written to {{file}}",
|
|
1566
1599
|
"initCi.created": "\u2713 Created {{path}}",
|
|
1567
1600
|
"initCi.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
|
|
1568
|
-
"install.installed": "\u2713 Installed to {{path}}",
|
|
1601
|
+
"install.installed": "\u2713 Installed to {{path}}\n \u2192 Restart Claude Code to activate /instrlint",
|
|
1569
1602
|
"install.alreadyExists": "{{path}} already exists. Use --force to overwrite.",
|
|
1570
1603
|
"install.unknownTarget": "Specify --claude-code or --codex",
|
|
1604
|
+
"install.outdatedTitle": "instrlint skill is outdated",
|
|
1605
|
+
"install.outdatedVersions": "installed: {{installed}} \u2192 current: {{current}}",
|
|
1606
|
+
"install.updateCmd": "npx instrlint install {{flag}} --force",
|
|
1607
|
+
"install.updatePrompt": "Update skill now?",
|
|
1571
1608
|
"fix.manualActions": "MANUAL ACTIONS NEEDED",
|
|
1572
1609
|
"fix.hookCreate": "Add to .claude/settings.json:",
|
|
1573
1610
|
"fix.hookWarning": "\u26A0 Hook executes shell commands \u2014 review carefully before adding",
|
|
@@ -1659,6 +1696,8 @@ var zh_TW_default = {
|
|
|
1659
1696
|
"markdown.budgetIssues": "Token \u9810\u7B97\u554F\u984C",
|
|
1660
1697
|
"markdown.refactoringOpportunities": "\u91CD\u69CB\u5EFA\u8B70",
|
|
1661
1698
|
"markdown.lineRef": "\uFF08\u7B2C {{line}} \u884C\uFF09",
|
|
1699
|
+
"markdown.budgetCategory": "\u985E\u5225",
|
|
1700
|
+
"markdown.budgetTokens": "Token \u6578",
|
|
1662
1701
|
"markdown.actionPlan": "## \u884C\u52D5\u8A08\u756B",
|
|
1663
1702
|
"markdown.attribution": "*\u7531 [instrlint](https://github.com/jed1978/instrlint) \u751F\u6210*",
|
|
1664
1703
|
"ci.passed": "\u2713 instrlint \u901A\u904E \u5206\u6578={{score}} \u7B49\u7D1A={{grade}}",
|
|
@@ -1666,9 +1705,13 @@ var zh_TW_default = {
|
|
|
1666
1705
|
"ci.writtenTo": "\u2192 \u5DF2\u5BEB\u5165 {{file}}",
|
|
1667
1706
|
"initCi.created": "\u2713 \u5DF2\u5EFA\u7ACB {{path}}",
|
|
1668
1707
|
"initCi.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
|
|
1669
|
-
"install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}",
|
|
1708
|
+
"install.installed": "\u2713 \u5DF2\u5B89\u88DD\u81F3 {{path}}\n \u2192 \u91CD\u65B0\u555F\u52D5 Claude Code \u5F8C\u5373\u53EF\u4F7F\u7528 /instrlint",
|
|
1670
1709
|
"install.alreadyExists": "{{path}} \u5DF2\u5B58\u5728\u3002\u4F7F\u7528 --force \u8986\u84CB\u3002",
|
|
1671
1710
|
"install.unknownTarget": "\u8ACB\u6307\u5B9A --claude-code \u6216 --codex",
|
|
1711
|
+
"install.outdatedTitle": "instrlint skill \u7248\u672C\u904E\u820A",
|
|
1712
|
+
"install.outdatedVersions": "\u5DF2\u5B89\u88DD\uFF1A{{installed}} \u2192 \u6700\u65B0\uFF1A{{current}}",
|
|
1713
|
+
"install.updateCmd": "npx instrlint install {{flag}} --force",
|
|
1714
|
+
"install.updatePrompt": "\u662F\u5426\u7ACB\u5373\u66F4\u65B0\uFF1F",
|
|
1672
1715
|
"fix.manualActions": "\u9700\u8981\u624B\u52D5\u64CD\u4F5C",
|
|
1673
1716
|
"fix.hookCreate": "\u52A0\u5165 .claude/settings.json\uFF1A",
|
|
1674
1717
|
"fix.hookWarning": "\u26A0 Hook \u6703\u57F7\u884C shell command\uFF0C\u8ACB\u4ED4\u7D30\u78BA\u8A8D\u5F8C\u518D\u52A0\u5165",
|
|
@@ -1998,15 +2041,25 @@ function mdSeverityIcon(f) {
|
|
|
1998
2041
|
if (f.severity === "warning") return "\u{1F7E1}";
|
|
1999
2042
|
return "\u2139\uFE0F";
|
|
2000
2043
|
}
|
|
2044
|
+
function mdBar(fraction, width = 20) {
|
|
2045
|
+
const filled = Math.round(Math.min(1, Math.max(0, fraction)) * width);
|
|
2046
|
+
const empty = width - filled;
|
|
2047
|
+
return "`" + "\u2588".repeat(filled) + "\u2591".repeat(empty) + "`";
|
|
2048
|
+
}
|
|
2049
|
+
function mdFormatTokens(count, method) {
|
|
2050
|
+
const fmt = new Intl.NumberFormat(getLocale());
|
|
2051
|
+
return method === "estimated" ? `~${fmt.format(count)}` : fmt.format(count);
|
|
2052
|
+
}
|
|
2001
2053
|
function reportMarkdown(report, extraSections = []) {
|
|
2002
|
-
const { project, tool, score, grade, findings } = report;
|
|
2054
|
+
const { project, tool, score, grade, findings, budget } = report;
|
|
2003
2055
|
const criticals = findings.filter((f) => f.severity === "critical").length;
|
|
2004
2056
|
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
2005
2057
|
const infos = findings.filter((f) => f.severity === "info").length;
|
|
2058
|
+
const gradeEmoji = grade === "A" ? "\u{1F7E2}" : grade === "B" ? "\u{1F535}" : grade === "C" ? "\u{1F7E1}" : grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
|
|
2006
2059
|
const lines = [
|
|
2007
2060
|
`# ${t("markdown.title", { project })}`,
|
|
2008
2061
|
"",
|
|
2009
|
-
|
|
2062
|
+
`${gradeEmoji} **${score}/100 (${grade})** ${mdBar(score / 100, 25)} \xB7 \`${tool}\``,
|
|
2010
2063
|
"",
|
|
2011
2064
|
t("markdown.summary"),
|
|
2012
2065
|
"",
|
|
@@ -2017,6 +2070,58 @@ function reportMarkdown(report, extraSections = []) {
|
|
|
2017
2070
|
`| ${t("markdown.info")} | ${infos} |`,
|
|
2018
2071
|
""
|
|
2019
2072
|
];
|
|
2073
|
+
const window = budget.totalBaseline + budget.availableTokens;
|
|
2074
|
+
const budgetRows = [
|
|
2075
|
+
{
|
|
2076
|
+
labelKey: "label.systemPrompt",
|
|
2077
|
+
tokens: budget.systemPromptTokens,
|
|
2078
|
+
method: "estimated"
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
labelKey: "label.rootFile",
|
|
2082
|
+
tokens: budget.rootFileTokens,
|
|
2083
|
+
method: budget.rootFileMethod
|
|
2084
|
+
},
|
|
2085
|
+
{
|
|
2086
|
+
labelKey: "label.ruleFiles",
|
|
2087
|
+
tokens: budget.rulesTokens,
|
|
2088
|
+
method: budget.rulesMethod
|
|
2089
|
+
},
|
|
2090
|
+
{
|
|
2091
|
+
labelKey: "label.skillFiles",
|
|
2092
|
+
tokens: budget.skillsTokens,
|
|
2093
|
+
method: budget.skillsMethod
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
labelKey: "label.subDirFiles",
|
|
2097
|
+
tokens: budget.subFilesTokens,
|
|
2098
|
+
method: budget.subFilesMethod
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
labelKey: "label.mcpServers",
|
|
2102
|
+
tokens: budget.mcpTokens,
|
|
2103
|
+
method: "estimated"
|
|
2104
|
+
}
|
|
2105
|
+
].filter((r) => r.tokens > 0);
|
|
2106
|
+
lines.push(`## ${t("label.tokenBudget")}`, "");
|
|
2107
|
+
lines.push(
|
|
2108
|
+
`| ${t("markdown.budgetCategory")} | ${t("markdown.budgetTokens")} | % | |`,
|
|
2109
|
+
"|------|--------|---|---|"
|
|
2110
|
+
);
|
|
2111
|
+
for (const row of budgetRows) {
|
|
2112
|
+
const pctVal = Math.round(row.tokens / window * 100);
|
|
2113
|
+
lines.push(
|
|
2114
|
+
`| ${t(row.labelKey)} | ${mdFormatTokens(row.tokens, row.method)} | ${pctVal}% | ${mdBar(row.tokens / window, 12)} |`
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
const baselinePct = Math.round(budget.totalBaseline / window * 100);
|
|
2118
|
+
lines.push(
|
|
2119
|
+
`| **${t("label.baselineTotal")}** | **${mdFormatTokens(budget.totalBaseline, budget.tokenMethod)}** | **${baselinePct}%** | ${mdBar(budget.totalBaseline / window, 12)} |`
|
|
2120
|
+
);
|
|
2121
|
+
lines.push(
|
|
2122
|
+
`| ${t("label.available")} | ${mdFormatTokens(budget.availableTokens, "estimated")} | ${100 - baselinePct}% | |`
|
|
2123
|
+
);
|
|
2124
|
+
lines.push("");
|
|
2020
2125
|
const categories = [
|
|
2021
2126
|
{
|
|
2022
2127
|
labelKey: "markdown.contradictions",
|
|
@@ -2263,6 +2368,163 @@ function markdownStructureSuggestions(suggestions, projectRoot) {
|
|
|
2263
2368
|
return lines;
|
|
2264
2369
|
}
|
|
2265
2370
|
|
|
2371
|
+
// src/utils/skill-version.ts
|
|
2372
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
2373
|
+
import { join as join7 } from "path";
|
|
2374
|
+
import { homedir } from "os";
|
|
2375
|
+
import { fileURLToPath } from "url";
|
|
2376
|
+
function readPackageVersion() {
|
|
2377
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
2378
|
+
for (const levels of [2, 3]) {
|
|
2379
|
+
const pkgPath = join7(thisFile, ...Array(levels).fill(".."), "package.json");
|
|
2380
|
+
if (existsSync7(pkgPath)) {
|
|
2381
|
+
const raw = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
2382
|
+
if (typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string") {
|
|
2383
|
+
return raw.version;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return "0.0.0";
|
|
2388
|
+
}
|
|
2389
|
+
var CURRENT_VERSION = readPackageVersion();
|
|
2390
|
+
var VERSION_RE = /^instrlint-version:\s*(.+)$/m;
|
|
2391
|
+
function extractVersion(content) {
|
|
2392
|
+
const m = VERSION_RE.exec(content);
|
|
2393
|
+
return m ? m[1].trim() : null;
|
|
2394
|
+
}
|
|
2395
|
+
function readInstalledVersion(path) {
|
|
2396
|
+
if (!existsSync7(path)) return null;
|
|
2397
|
+
try {
|
|
2398
|
+
return extractVersion(readFileSync8(path, "utf8"));
|
|
2399
|
+
} catch {
|
|
2400
|
+
return null;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
function checkSkillUpdate(projectRoot) {
|
|
2404
|
+
const candidates = [
|
|
2405
|
+
{
|
|
2406
|
+
path: join7(projectRoot, ".claude", "commands", "instrlint.md"),
|
|
2407
|
+
isProject: true
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
path: join7(homedir(), ".claude", "commands", "instrlint.md"),
|
|
2411
|
+
isProject: false
|
|
2412
|
+
}
|
|
2413
|
+
];
|
|
2414
|
+
for (const { path, isProject } of candidates) {
|
|
2415
|
+
const installed = readInstalledVersion(path);
|
|
2416
|
+
if (installed !== null && installed !== CURRENT_VERSION) {
|
|
2417
|
+
return {
|
|
2418
|
+
installedVersion: installed,
|
|
2419
|
+
currentVersion: CURRENT_VERSION,
|
|
2420
|
+
installPath: path,
|
|
2421
|
+
isProject
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
return null;
|
|
2426
|
+
}
|
|
2427
|
+
function injectVersion(content, version) {
|
|
2428
|
+
if (/^instrlint-version:/m.test(content)) {
|
|
2429
|
+
return content.replace(
|
|
2430
|
+
/^instrlint-version:.*$/m,
|
|
2431
|
+
`instrlint-version: ${version}`
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
const updated = content.replace(
|
|
2435
|
+
/^(---\n[\s\S]*?)(---)$/m,
|
|
2436
|
+
`$1instrlint-version: ${version}
|
|
2437
|
+
$2`
|
|
2438
|
+
);
|
|
2439
|
+
if (updated === content) {
|
|
2440
|
+
throw new Error("injectVersion: no YAML frontmatter found in skill file");
|
|
2441
|
+
}
|
|
2442
|
+
return updated;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// src/commands/install-command.ts
|
|
2446
|
+
import { existsSync as existsSync8, mkdirSync, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "fs";
|
|
2447
|
+
import { join as join8 } from "path";
|
|
2448
|
+
import { homedir as homedir2 } from "os";
|
|
2449
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2450
|
+
function resolveSkillFile(target) {
|
|
2451
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
2452
|
+
const subDir = target === "claude-code" ? "claude-code" : "codex";
|
|
2453
|
+
for (const levels of [2, 3]) {
|
|
2454
|
+
const parts = Array(levels).fill("..");
|
|
2455
|
+
const candidate = join8(thisFile, ...parts, "skills", subDir, "SKILL.md");
|
|
2456
|
+
if (existsSync8(candidate)) return candidate;
|
|
2457
|
+
}
|
|
2458
|
+
return join8(thisFile, "..", "..", "skills", subDir, "SKILL.md");
|
|
2459
|
+
}
|
|
2460
|
+
function readSkillContent(target) {
|
|
2461
|
+
const skillPath = resolveSkillFile(target);
|
|
2462
|
+
try {
|
|
2463
|
+
const raw = readFileSync9(skillPath, "utf8");
|
|
2464
|
+
return injectVersion(raw, CURRENT_VERSION);
|
|
2465
|
+
} catch {
|
|
2466
|
+
throw new Error(
|
|
2467
|
+
`Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
function installClaudeCode(content, projectRoot, isProject, force, output) {
|
|
2472
|
+
const targetDir = isProject ? join8(projectRoot, ".claude", "commands") : join8(homedir2(), ".claude", "commands");
|
|
2473
|
+
const targetPath = join8(targetDir, "instrlint.md");
|
|
2474
|
+
if (existsSync8(targetPath) && !force) {
|
|
2475
|
+
output.error(t("install.alreadyExists", { path: targetPath }));
|
|
2476
|
+
return { exitCode: 1, errorMessage: "file already exists" };
|
|
2477
|
+
}
|
|
2478
|
+
mkdirSync(targetDir, { recursive: true });
|
|
2479
|
+
writeFileSync2(targetPath, content, "utf8");
|
|
2480
|
+
output.log(t("install.installed", { path: targetPath }));
|
|
2481
|
+
return { exitCode: 0 };
|
|
2482
|
+
}
|
|
2483
|
+
function installCodex(content, projectRoot, force, output) {
|
|
2484
|
+
const targetDir = join8(projectRoot, ".agents", "skills", "instrlint");
|
|
2485
|
+
const targetPath = join8(targetDir, "SKILL.md");
|
|
2486
|
+
if (existsSync8(targetPath) && !force) {
|
|
2487
|
+
output.error(t("install.alreadyExists", { path: targetPath }));
|
|
2488
|
+
return { exitCode: 1, errorMessage: "file already exists" };
|
|
2489
|
+
}
|
|
2490
|
+
mkdirSync(targetDir, { recursive: true });
|
|
2491
|
+
writeFileSync2(targetPath, content, "utf8");
|
|
2492
|
+
output.log(t("install.installed", { path: targetPath }));
|
|
2493
|
+
return { exitCode: 0 };
|
|
2494
|
+
}
|
|
2495
|
+
function runInstall(opts, output = console) {
|
|
2496
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
2497
|
+
const force = opts.force ?? false;
|
|
2498
|
+
if (opts.claudeCode) {
|
|
2499
|
+
let content;
|
|
2500
|
+
try {
|
|
2501
|
+
content = readSkillContent("claude-code");
|
|
2502
|
+
} catch (err) {
|
|
2503
|
+
output.error(String(err));
|
|
2504
|
+
return { exitCode: 1, errorMessage: String(err) };
|
|
2505
|
+
}
|
|
2506
|
+
return installClaudeCode(
|
|
2507
|
+
content,
|
|
2508
|
+
projectRoot,
|
|
2509
|
+
opts.project ?? false,
|
|
2510
|
+
force,
|
|
2511
|
+
output
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
if (opts.codex) {
|
|
2515
|
+
let content;
|
|
2516
|
+
try {
|
|
2517
|
+
content = readSkillContent("codex");
|
|
2518
|
+
} catch (err) {
|
|
2519
|
+
output.error(String(err));
|
|
2520
|
+
return { exitCode: 1, errorMessage: String(err) };
|
|
2521
|
+
}
|
|
2522
|
+
return installCodex(content, projectRoot, force, output);
|
|
2523
|
+
}
|
|
2524
|
+
output.error(t("install.unknownTarget"));
|
|
2525
|
+
return { exitCode: 1, errorMessage: "no target specified" };
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2266
2528
|
// src/commands/run-command.ts
|
|
2267
2529
|
function isGitClean(cwd) {
|
|
2268
2530
|
try {
|
|
@@ -2352,6 +2614,7 @@ async function runAll(opts, output = console) {
|
|
|
2352
2614
|
printStructureSuggestions(suggestions, projectRoot, output);
|
|
2353
2615
|
return { exitCode: 0 };
|
|
2354
2616
|
}
|
|
2617
|
+
const skillUpdate = checkSkillUpdate(projectRoot);
|
|
2355
2618
|
if (opts.format === "json") {
|
|
2356
2619
|
output.log(reportJson(report));
|
|
2357
2620
|
return { exitCode: 0 };
|
|
@@ -2359,12 +2622,54 @@ async function runAll(opts, output = console) {
|
|
|
2359
2622
|
if (opts.format === "markdown") {
|
|
2360
2623
|
const mdSuggestions = buildStructureSuggestions(allFindings);
|
|
2361
2624
|
const mdExtra = markdownStructureSuggestions(mdSuggestions, projectRoot);
|
|
2362
|
-
|
|
2625
|
+
const updateSection = skillUpdate ? [
|
|
2626
|
+
"",
|
|
2627
|
+
`> \u26A0\uFE0F **${t("install.outdatedTitle")}** (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`,
|
|
2628
|
+
`> \`${t("install.updateCmd", { flag: skillUpdate.isProject ? "--claude-code --project" : "--claude-code" })}\``
|
|
2629
|
+
] : [];
|
|
2630
|
+
output.log(reportMarkdown(report, [...mdExtra, ...updateSection]));
|
|
2363
2631
|
return { exitCode: 0 };
|
|
2364
2632
|
}
|
|
2365
2633
|
printCombinedTerminal(report, output);
|
|
2634
|
+
if (skillUpdate && process.stdout.isTTY) {
|
|
2635
|
+
output.log("");
|
|
2636
|
+
output.log(
|
|
2637
|
+
` ${chalk4.yellow("\u26A0")} ${chalk4.bold(t("install.outdatedTitle"))} (${t("install.outdatedVersions", { installed: skillUpdate.installedVersion, current: skillUpdate.currentVersion })})`
|
|
2638
|
+
);
|
|
2639
|
+
const confirmed = await promptYesNo(` ${t("install.updatePrompt")}`);
|
|
2640
|
+
if (confirmed) {
|
|
2641
|
+
const installResult = runInstall(
|
|
2642
|
+
{
|
|
2643
|
+
claudeCode: true,
|
|
2644
|
+
project: skillUpdate.isProject,
|
|
2645
|
+
force: true,
|
|
2646
|
+
projectRoot
|
|
2647
|
+
},
|
|
2648
|
+
output
|
|
2649
|
+
);
|
|
2650
|
+
if (installResult.exitCode !== 0) {
|
|
2651
|
+
output.error?.(
|
|
2652
|
+
`Update failed: ${installResult.errorMessage ?? "unknown error"}`
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
output.log("");
|
|
2657
|
+
}
|
|
2366
2658
|
return { exitCode: 0 };
|
|
2367
2659
|
}
|
|
2660
|
+
function promptYesNo(question) {
|
|
2661
|
+
return new Promise((resolve) => {
|
|
2662
|
+
const rl = createInterface({
|
|
2663
|
+
input: process.stdin,
|
|
2664
|
+
output: process.stdout
|
|
2665
|
+
});
|
|
2666
|
+
rl.question(`${question} ${chalk4.gray("[Y/n]")} `, (answer) => {
|
|
2667
|
+
rl.close();
|
|
2668
|
+
const trimmed = answer.trim().toLowerCase();
|
|
2669
|
+
resolve(trimmed === "" || trimmed === "y");
|
|
2670
|
+
});
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2368
2673
|
|
|
2369
2674
|
// src/commands/deadrules-command.ts
|
|
2370
2675
|
import chalk5 from "chalk";
|
|
@@ -2525,7 +2830,7 @@ async function runStructure(opts, output = console) {
|
|
|
2525
2830
|
}
|
|
2526
2831
|
|
|
2527
2832
|
// src/commands/ci-command.ts
|
|
2528
|
-
import { writeFileSync as
|
|
2833
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
2529
2834
|
import { basename as basename3 } from "path";
|
|
2530
2835
|
|
|
2531
2836
|
// src/reporters/sarif.ts
|
|
@@ -2653,7 +2958,7 @@ async function runCi(opts, output = console) {
|
|
|
2653
2958
|
formatted = reportJson(report);
|
|
2654
2959
|
}
|
|
2655
2960
|
if (opts.output != null) {
|
|
2656
|
-
|
|
2961
|
+
writeFileSync3(opts.output, formatted, "utf8");
|
|
2657
2962
|
const pass = !shouldFail(allFindings, failOn);
|
|
2658
2963
|
const statusKey = pass ? "ci.passed" : "ci.failed";
|
|
2659
2964
|
output.error(
|
|
@@ -2667,8 +2972,8 @@ async function runCi(opts, output = console) {
|
|
|
2667
2972
|
}
|
|
2668
2973
|
|
|
2669
2974
|
// src/commands/init-ci-command.ts
|
|
2670
|
-
import { existsSync as
|
|
2671
|
-
import { join as
|
|
2975
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2976
|
+
import { join as join9 } from "path";
|
|
2672
2977
|
function githubWorkflow() {
|
|
2673
2978
|
return `name: instrlint
|
|
2674
2979
|
|
|
@@ -2737,14 +3042,14 @@ instrlint:
|
|
|
2737
3042
|
function runInitCi(opts, output = console) {
|
|
2738
3043
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
2739
3044
|
if (opts.github) {
|
|
2740
|
-
const workflowDir =
|
|
2741
|
-
const workflowPath =
|
|
2742
|
-
if (
|
|
3045
|
+
const workflowDir = join9(projectRoot, ".github", "workflows");
|
|
3046
|
+
const workflowPath = join9(workflowDir, "instrlint.yml");
|
|
3047
|
+
if (existsSync9(workflowPath) && !opts.force) {
|
|
2743
3048
|
output.error(t("initCi.alreadyExists", { path: workflowPath }));
|
|
2744
3049
|
return { exitCode: 1, errorMessage: "file already exists" };
|
|
2745
3050
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
3051
|
+
mkdirSync2(workflowDir, { recursive: true });
|
|
3052
|
+
writeFileSync4(workflowPath, githubWorkflow(), "utf8");
|
|
2748
3053
|
output.log(t("initCi.created", { path: workflowPath }));
|
|
2749
3054
|
return { exitCode: 0 };
|
|
2750
3055
|
}
|
|
@@ -2756,93 +3061,11 @@ function runInitCi(opts, output = console) {
|
|
|
2756
3061
|
return { exitCode: 1, errorMessage: "no target specified" };
|
|
2757
3062
|
}
|
|
2758
3063
|
|
|
2759
|
-
// src/commands/install-command.ts
|
|
2760
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
2761
|
-
import { join as join8 } from "path";
|
|
2762
|
-
import { homedir } from "os";
|
|
2763
|
-
import { fileURLToPath } from "url";
|
|
2764
|
-
function resolveSkillFile(target) {
|
|
2765
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
2766
|
-
const subDir = target === "claude-code" ? "claude-code" : "codex";
|
|
2767
|
-
for (const levels of [2, 3]) {
|
|
2768
|
-
const parts = Array(levels).fill("..");
|
|
2769
|
-
const candidate = join8(thisFile, ...parts, "skills", subDir, "SKILL.md");
|
|
2770
|
-
if (existsSync8(candidate)) return candidate;
|
|
2771
|
-
}
|
|
2772
|
-
return join8(thisFile, "..", "..", "skills", subDir, "SKILL.md");
|
|
2773
|
-
}
|
|
2774
|
-
function readSkillContent(target) {
|
|
2775
|
-
const skillPath = resolveSkillFile(target);
|
|
2776
|
-
try {
|
|
2777
|
-
return readFileSync8(skillPath, "utf8");
|
|
2778
|
-
} catch {
|
|
2779
|
-
throw new Error(
|
|
2780
|
-
`Could not read skill file at ${skillPath}. Make sure the package is properly installed.`
|
|
2781
|
-
);
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
function installClaudeCode(content, projectRoot, isProject, force, output) {
|
|
2785
|
-
const targetDir = isProject ? join8(projectRoot, ".claude", "commands") : join8(homedir(), ".claude", "commands");
|
|
2786
|
-
const targetPath = join8(targetDir, "instrlint.md");
|
|
2787
|
-
if (existsSync8(targetPath) && !force) {
|
|
2788
|
-
output.error(t("install.alreadyExists", { path: targetPath }));
|
|
2789
|
-
return { exitCode: 1, errorMessage: "file already exists" };
|
|
2790
|
-
}
|
|
2791
|
-
mkdirSync2(targetDir, { recursive: true });
|
|
2792
|
-
writeFileSync4(targetPath, content, "utf8");
|
|
2793
|
-
output.log(t("install.installed", { path: targetPath }));
|
|
2794
|
-
return { exitCode: 0 };
|
|
2795
|
-
}
|
|
2796
|
-
function installCodex(content, projectRoot, force, output) {
|
|
2797
|
-
const targetDir = join8(projectRoot, ".agents", "skills", "instrlint");
|
|
2798
|
-
const targetPath = join8(targetDir, "SKILL.md");
|
|
2799
|
-
if (existsSync8(targetPath) && !force) {
|
|
2800
|
-
output.error(t("install.alreadyExists", { path: targetPath }));
|
|
2801
|
-
return { exitCode: 1, errorMessage: "file already exists" };
|
|
2802
|
-
}
|
|
2803
|
-
mkdirSync2(targetDir, { recursive: true });
|
|
2804
|
-
writeFileSync4(targetPath, content, "utf8");
|
|
2805
|
-
output.log(t("install.installed", { path: targetPath }));
|
|
2806
|
-
return { exitCode: 0 };
|
|
2807
|
-
}
|
|
2808
|
-
function runInstall(opts, output = console) {
|
|
2809
|
-
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
2810
|
-
const force = opts.force ?? false;
|
|
2811
|
-
if (opts.claudeCode) {
|
|
2812
|
-
let content;
|
|
2813
|
-
try {
|
|
2814
|
-
content = readSkillContent("claude-code");
|
|
2815
|
-
} catch (err) {
|
|
2816
|
-
output.error(String(err));
|
|
2817
|
-
return { exitCode: 1, errorMessage: String(err) };
|
|
2818
|
-
}
|
|
2819
|
-
return installClaudeCode(
|
|
2820
|
-
content,
|
|
2821
|
-
projectRoot,
|
|
2822
|
-
opts.project ?? false,
|
|
2823
|
-
force,
|
|
2824
|
-
output
|
|
2825
|
-
);
|
|
2826
|
-
}
|
|
2827
|
-
if (opts.codex) {
|
|
2828
|
-
let content;
|
|
2829
|
-
try {
|
|
2830
|
-
content = readSkillContent("codex");
|
|
2831
|
-
} catch (err) {
|
|
2832
|
-
output.error(String(err));
|
|
2833
|
-
return { exitCode: 1, errorMessage: String(err) };
|
|
2834
|
-
}
|
|
2835
|
-
return installCodex(content, projectRoot, force, output);
|
|
2836
|
-
}
|
|
2837
|
-
output.error(t("install.unknownTarget"));
|
|
2838
|
-
return { exitCode: 1, errorMessage: "no target specified" };
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
3064
|
// src/cli.ts
|
|
2842
3065
|
var program = new Command();
|
|
2843
3066
|
program.enablePositionalOptions().name("instrlint").description(
|
|
2844
3067
|
"Lint and optimize your CLAUDE.md / AGENTS.md \u2014 find dead rules, token waste, and structural issues"
|
|
2845
|
-
).version(
|
|
3068
|
+
).version(CURRENT_VERSION).option(
|
|
2846
3069
|
"--format <type>",
|
|
2847
3070
|
"output format (terminal|json|markdown)",
|
|
2848
3071
|
"terminal"
|
|
@@ -2855,27 +3078,27 @@ program.command("budget").description("Token budget analysis only").option(
|
|
|
2855
3078
|
"--format <type>",
|
|
2856
3079
|
"output format (terminal|json|markdown)",
|
|
2857
3080
|
"terminal"
|
|
2858
|
-
).option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
|
|
3081
|
+
).option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
|
|
2859
3082
|
const opts = this.opts();
|
|
2860
|
-
const lang = this.parent?.opts()?.lang;
|
|
3083
|
+
const lang = opts.lang ?? this.parent?.opts()?.lang;
|
|
2861
3084
|
const result = await runBudget({
|
|
2862
3085
|
...opts,
|
|
2863
3086
|
...lang !== void 0 && { lang }
|
|
2864
3087
|
});
|
|
2865
3088
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
2866
3089
|
});
|
|
2867
|
-
program.command("deadrules").description("Dead rule detection only").option("--format <type>", "output format (terminal|json)", "terminal").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
|
|
3090
|
+
program.command("deadrules").description("Dead rule detection only").option("--format <type>", "output format (terminal|json)", "terminal").option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
|
|
2868
3091
|
const opts = this.opts();
|
|
2869
|
-
const lang = this.parent?.opts()?.lang;
|
|
3092
|
+
const lang = opts.lang ?? this.parent?.opts()?.lang;
|
|
2870
3093
|
const result = await runDeadRules({
|
|
2871
3094
|
...opts,
|
|
2872
3095
|
...lang !== void 0 && { lang }
|
|
2873
3096
|
});
|
|
2874
3097
|
if (result.exitCode !== 0) process.exit(result.exitCode);
|
|
2875
3098
|
});
|
|
2876
|
-
program.command("structure").description("Structural analysis only").option("--format <type>", "output format (terminal|json)", "terminal").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
|
|
3099
|
+
program.command("structure").description("Structural analysis only").option("--format <type>", "output format (terminal|json)", "terminal").option("--lang <locale>", "output language (en|zh-TW)").option("--tool <name>", "force tool detection (claude-code|codex|cursor)").action(async function() {
|
|
2877
3100
|
const opts = this.opts();
|
|
2878
|
-
const lang = this.parent?.opts()?.lang;
|
|
3101
|
+
const lang = opts.lang ?? this.parent?.opts()?.lang;
|
|
2879
3102
|
const result = await runStructure({
|
|
2880
3103
|
...opts,
|
|
2881
3104
|
...lang !== void 0 && { lang }
|