@tanstack/intent 0.0.29 → 0.0.33
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/LICENSE +21 -0
- package/README.md +25 -12
- package/dist/artifact-coverage-BAN2W6aH.mjs +3 -0
- package/dist/artifact-coverage-wLNVX8yC.mjs +128 -0
- package/dist/cli.d.mts +0 -0
- package/dist/cli.mjs +77 -76
- package/dist/{display-hdsqb4w-.mjs → display-BTZWCjzT.mjs} +10 -1
- package/dist/display-DvLbcWzq.mjs +5 -0
- package/dist/index.d.mts +76 -5
- package/dist/index.mjs +126 -6
- package/dist/install-PUnIfBNC.mjs +542 -0
- package/dist/intent-library.mjs +9 -5
- package/dist/{library-scanner-B51qV5aX.mjs → library-scanner-Cl-XPEMf.mjs} +10 -3
- package/dist/library-scanner.d.mts +2 -2
- package/dist/library-scanner.mjs +2 -1
- package/dist/{project-context-D6A5sBBO.mjs → project-context-IDLpJU3S.mjs} +1 -1
- package/dist/resolver-D2CgIYGg.mjs +70 -0
- package/dist/{scanner-B-bbXBLY.mjs → scanner-CW59cxE_.mjs} +176 -117
- package/dist/scanner-DlkcbVye.mjs +6 -0
- package/dist/{setup-B4ZwN5Hg.mjs → setup-DfLsziXU.mjs} +2 -2
- package/dist/setup.d.mts +1 -1
- package/dist/setup.mjs +3 -3
- package/dist/skill-paths-8k9K9y26.mjs +33 -0
- package/dist/skill-use-uwGleSOz.mjs +42 -0
- package/dist/staleness-DpbmYod4.mjs +5 -0
- package/dist/staleness-PdgakrCQ.mjs +243 -0
- package/dist/{types-BTQ9efv-.d.mts → types-_y9b00bI.d.mts} +54 -1
- package/dist/{workspace-patterns-Cndd-7vB.mjs → workspace-patterns-BN2A_60g.mjs} +6 -1
- package/dist/workspace-patterns-x-dLZxx4.mjs +4 -0
- package/meta/templates/workflows/check-skills.yml +199 -91
- package/package.json +3 -4
- package/dist/display-DdmZXLZm.mjs +0 -3
- package/dist/install-BzDmD5yI.mjs +0 -69
- package/dist/scanner-CP4U8F_n.mjs +0 -5
- package/dist/staleness-LRbiWWZK.mjs +0 -4
- package/dist/staleness-SY7-mZMH.mjs +0 -104
- package/dist/workspace-patterns-D_y6rlqX.mjs +0 -4
- package/meta/templates/workflows/notify-intent.yml +0 -51
- /package/dist/{setup-BA9RkENh.d.mts → setup-D2CGdTsx.d.mts} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,133 @@
|
|
|
1
1
|
import { a as parseFrontmatter, n as findSkillFiles, o as resolveDepDir, r as getDeps } from "./utils-COlDcU72.mjs";
|
|
2
|
-
import "./
|
|
3
|
-
import { t as scanForIntents } from "./scanner-
|
|
4
|
-
import
|
|
5
|
-
import "./
|
|
6
|
-
import {
|
|
2
|
+
import "./skill-paths-8k9K9y26.mjs";
|
|
3
|
+
import { t as scanForIntents } from "./scanner-CW59cxE_.mjs";
|
|
4
|
+
import "./workspace-patterns-BN2A_60g.mjs";
|
|
5
|
+
import { t as readIntentArtifacts } from "./artifact-coverage-wLNVX8yC.mjs";
|
|
6
|
+
import { n as checkStaleness } from "./staleness-PdgakrCQ.mjs";
|
|
7
|
+
import { i as parseSkillUse, n as formatSkillUse, r as isSkillUseParseError, t as SkillUseParseError } from "./skill-use-uwGleSOz.mjs";
|
|
8
|
+
import { n as isResolveSkillUseError, r as resolveSkillUse, t as ResolveSkillUseError } from "./resolver-D2CgIYGg.mjs";
|
|
9
|
+
import "./project-context-IDLpJU3S.mjs";
|
|
10
|
+
import { r as runSetupGithubActions, t as runEditPackageJson } from "./setup-DfLsziXU.mjs";
|
|
7
11
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
8
12
|
import { join } from "node:path";
|
|
9
13
|
import { execFileSync, execSync } from "node:child_process";
|
|
10
14
|
|
|
15
|
+
//#region src/workflow-review.ts
|
|
16
|
+
function collectStaleReviewItems(reports) {
|
|
17
|
+
const items = [];
|
|
18
|
+
for (const report of reports) {
|
|
19
|
+
for (const skill of report.skills ?? []) {
|
|
20
|
+
if (!skill?.needsReview) continue;
|
|
21
|
+
items.push({
|
|
22
|
+
type: "stale-skill",
|
|
23
|
+
library: report.library,
|
|
24
|
+
subject: skill.name,
|
|
25
|
+
reasons: skill.reasons ?? []
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const signal of report.signals ?? []) {
|
|
29
|
+
if (signal?.needsReview === false) continue;
|
|
30
|
+
items.push({
|
|
31
|
+
type: signal?.type ?? "review-signal",
|
|
32
|
+
library: signal?.library ?? report.library,
|
|
33
|
+
subject: signal?.packageName ?? signal?.packageRoot ?? signal?.skill ?? signal?.artifactPath ?? signal?.subject ?? report.library,
|
|
34
|
+
reasons: signal?.reasons ?? [],
|
|
35
|
+
artifactPath: signal?.artifactPath,
|
|
36
|
+
packageName: signal?.packageName,
|
|
37
|
+
packageRoot: signal?.packageRoot,
|
|
38
|
+
skill: signal?.skill
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return items;
|
|
43
|
+
}
|
|
44
|
+
function createFailedStaleReviewItem(library) {
|
|
45
|
+
return {
|
|
46
|
+
type: "stale-check-failed",
|
|
47
|
+
library,
|
|
48
|
+
subject: "intent stale --json",
|
|
49
|
+
reasons: ["The stale check command failed. Review the workflow logs before updating skills."]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function buildStaleReviewBody(items) {
|
|
53
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const item of items) grouped.set(item.type, (grouped.get(item.type) ?? 0) + 1);
|
|
55
|
+
const signalRows = [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([type, count]) => `| \`${type}\` | ${count} |`);
|
|
56
|
+
const itemRows = items.map((item) => {
|
|
57
|
+
const subject = item.subject ? `\`${item.subject}\`` : "-";
|
|
58
|
+
const reasons = item.reasons?.length ? item.reasons.join("; ") : "-";
|
|
59
|
+
return `| \`${item.type}\` | ${subject} | \`${item.library}\` | ${reasons} |`;
|
|
60
|
+
});
|
|
61
|
+
const prompt = [
|
|
62
|
+
"You are helping maintain Intent skills for this repository.",
|
|
63
|
+
"",
|
|
64
|
+
"Goal:",
|
|
65
|
+
"Resolve the Intent skill review signals below while preserving the existing scope, taxonomy, and maintainer-reviewed artifacts.",
|
|
66
|
+
"",
|
|
67
|
+
"Review signals:",
|
|
68
|
+
JSON.stringify(items, null, 2),
|
|
69
|
+
"",
|
|
70
|
+
"Required workflow:",
|
|
71
|
+
"1. Read the existing `_artifacts/*domain_map.yaml`, `_artifacts/*skill_tree.yaml`, and generated `skills/**/SKILL.md` files.",
|
|
72
|
+
"2. Read each flagged package package.json, public exports, README/docs if present, and source entry points.",
|
|
73
|
+
"3. Compare flagged packages against the existing domains, skills, tasks, packages, covers, sources, tensions, and cross-references in the artifacts.",
|
|
74
|
+
"4. For each signal, decide whether it means existing skill coverage, a missing generated skill, a new skill candidate, out-of-scope coverage, or deferred work.",
|
|
75
|
+
"",
|
|
76
|
+
"Maintainer questions:",
|
|
77
|
+
"Before editing skills or artifacts, ask the maintainer:",
|
|
78
|
+
"1. For each flagged package, is this package user-facing enough to need agent guidance?",
|
|
79
|
+
"2. If yes, should it extend an existing skill or become a new skill?",
|
|
80
|
+
"3. If it extends an existing skill, which current skill should own it?",
|
|
81
|
+
"4. If it is out of scope, what short reason should be recorded in artifact coverage ignores?",
|
|
82
|
+
"5. Are any of these packages experimental or unstable enough to exclude for now?",
|
|
83
|
+
"",
|
|
84
|
+
"Decision rules:",
|
|
85
|
+
"- Do not auto-generate skills.",
|
|
86
|
+
"- Do not create broad new skill areas without maintainer confirmation.",
|
|
87
|
+
"- Prefer adding package coverage to an existing skill when the package is an implementation variant of an existing domain.",
|
|
88
|
+
"- Create a new skill only when the package introduces a distinct developer task or failure mode.",
|
|
89
|
+
"- Preserve current naming, path, and package layout conventions.",
|
|
90
|
+
"- Keep generated skills under the package-local `skills/` directory.",
|
|
91
|
+
"- Keep repo-root `_artifacts` as the reviewed plan.",
|
|
92
|
+
"",
|
|
93
|
+
"If maintainer confirms updates:",
|
|
94
|
+
"1. Update the relevant `_artifacts/*domain_map.yaml` or `_artifacts/*skill_tree.yaml`.",
|
|
95
|
+
"2. Update or create `SKILL.md` files only for confirmed coverage changes.",
|
|
96
|
+
"3. Keep `sources` aligned between artifact skill entries and SKILL frontmatter.",
|
|
97
|
+
"4. Bump `library_version` only for skills whose covered source package version changed.",
|
|
98
|
+
"5. Run `npx @tanstack/intent@latest validate` on touched skill directories.",
|
|
99
|
+
"6. Summarize every package as one of: existing-skill coverage, new skill, ignored, or deferred."
|
|
100
|
+
].join("\n");
|
|
101
|
+
return [
|
|
102
|
+
"## Intent Skill Review Needed",
|
|
103
|
+
"",
|
|
104
|
+
"Intent found skills, artifact coverage, or workspace package coverage that need maintainer review.",
|
|
105
|
+
"",
|
|
106
|
+
"### Summary",
|
|
107
|
+
"",
|
|
108
|
+
"| Signal | Count |",
|
|
109
|
+
"| --- | ---: |",
|
|
110
|
+
...signalRows,
|
|
111
|
+
"",
|
|
112
|
+
"### Review Items",
|
|
113
|
+
"",
|
|
114
|
+
"| Signal | Subject | Library | Reason |",
|
|
115
|
+
"| --- | --- | --- | --- |",
|
|
116
|
+
...itemRows,
|
|
117
|
+
"",
|
|
118
|
+
"### Agent Prompt",
|
|
119
|
+
"",
|
|
120
|
+
"Paste this into your coding agent:",
|
|
121
|
+
"",
|
|
122
|
+
"```text",
|
|
123
|
+
prompt,
|
|
124
|
+
"```",
|
|
125
|
+
"",
|
|
126
|
+
"This PR is a review reminder only. It does not update skills automatically."
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
11
131
|
//#region src/feedback.ts
|
|
12
132
|
const META_FEEDBACK_REPO = "TanStack/intent";
|
|
13
133
|
const SECRET_PATTERNS = [
|
|
@@ -271,4 +391,4 @@ function submitMetaFeedback(payload, opts) {
|
|
|
271
391
|
}
|
|
272
392
|
|
|
273
393
|
//#endregion
|
|
274
|
-
export { checkStaleness, containsSecrets, findSkillFiles, getDeps, hasGhCli, metaToMarkdown, parseFrontmatter, resolveDepDir, resolveFrequency, runEditPackageJson, runSetupGithubActions, scanForIntents, submitFeedback, submitMetaFeedback, toMarkdown, validateMetaPayload, validatePayload };
|
|
394
|
+
export { ResolveSkillUseError, SkillUseParseError, buildStaleReviewBody, checkStaleness, collectStaleReviewItems, containsSecrets, createFailedStaleReviewItem, findSkillFiles, formatSkillUse, getDeps, hasGhCli, isResolveSkillUseError, isSkillUseParseError, metaToMarkdown, parseFrontmatter, parseSkillUse, readIntentArtifacts, resolveDepDir, resolveFrequency, resolveSkillUse, runEditPackageJson, runSetupGithubActions, scanForIntents, submitFeedback, submitMetaFeedback, toMarkdown, validateMetaPayload, validatePayload };
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { i as parseSkillUse, n as formatSkillUse } from "./skill-use-uwGleSOz.mjs";
|
|
2
|
+
import { t as resolveProjectContext } from "./project-context-IDLpJU3S.mjs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { parse } from "yaml";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
//#region src/cli-error.ts
|
|
9
|
+
const CLI_FAILURE = Symbol("CliFailure");
|
|
10
|
+
function fail(message, exitCode = 1) {
|
|
11
|
+
throw {
|
|
12
|
+
[CLI_FAILURE]: true,
|
|
13
|
+
message,
|
|
14
|
+
exitCode
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function isCliFailure(value) {
|
|
18
|
+
return !!value && typeof value === "object" && CLI_FAILURE in value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/cli-output.ts
|
|
23
|
+
function printWarnings(warnings) {
|
|
24
|
+
if (warnings.length === 0) return;
|
|
25
|
+
console.log("Warnings:");
|
|
26
|
+
for (const warning of warnings) console.log(` ⚠ ${warning}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/cli-support.ts
|
|
31
|
+
const INTENT_CHECK_SKILLS_WORKFLOW_VERSION = 2;
|
|
32
|
+
function getMetaDir() {
|
|
33
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "meta");
|
|
34
|
+
}
|
|
35
|
+
function getCheckSkillsWorkflowAdvisories(root) {
|
|
36
|
+
const workflowPath = join(root, ".github", "workflows", "check-skills.yml");
|
|
37
|
+
if (!existsSync(workflowPath)) return [];
|
|
38
|
+
let content;
|
|
39
|
+
try {
|
|
40
|
+
content = readFileSync(workflowPath, "utf8");
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const versionMatch = content.match(/intent-workflow-version:\s*(\d+)/);
|
|
45
|
+
if ((versionMatch ? Number(versionMatch[1]) : 0) >= INTENT_CHECK_SKILLS_WORKFLOW_VERSION) return [];
|
|
46
|
+
return [`Intent workflow update available: run \`npx @tanstack/intent@latest setup\` to refresh ${relative(process.cwd(), workflowPath) || workflowPath}.`];
|
|
47
|
+
}
|
|
48
|
+
async function scanIntentsOrFail(options) {
|
|
49
|
+
const { scanForIntents } = await import("./scanner-DlkcbVye.mjs");
|
|
50
|
+
try {
|
|
51
|
+
return scanForIntents(void 0, options);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function scanOptionsFromGlobalFlags(options) {
|
|
57
|
+
if (options.global && options.globalOnly) fail("Use either --global or --global-only, not both.");
|
|
58
|
+
if (options.globalOnly) return { scope: "global" };
|
|
59
|
+
if (options.global) return { scope: "local-and-global" };
|
|
60
|
+
return { scope: "local" };
|
|
61
|
+
}
|
|
62
|
+
async function resolveStaleTargets(targetDir) {
|
|
63
|
+
const resolvedRoot = targetDir ? resolve(process.cwd(), targetDir) : process.cwd();
|
|
64
|
+
const context = resolveProjectContext({
|
|
65
|
+
cwd: process.cwd(),
|
|
66
|
+
targetPath: targetDir
|
|
67
|
+
});
|
|
68
|
+
const workflowAdvisories = getCheckSkillsWorkflowAdvisories(context.workspaceRoot ?? context.packageRoot ?? resolvedRoot);
|
|
69
|
+
const { buildWorkspaceCoverageSignals, checkStaleness, readPackageName } = await import("./staleness-DpbmYod4.mjs");
|
|
70
|
+
const isWorkspaceRootTarget = context.workspaceRoot !== null && resolvedRoot === context.workspaceRoot;
|
|
71
|
+
if (context.packageRoot && !isWorkspaceRootTarget && (context.targetSkillsDir !== null || context.workspaceRoot === null)) return {
|
|
72
|
+
reports: [await checkStaleness(context.packageRoot, readPackageName(context.packageRoot), context.workspaceRoot ?? context.packageRoot)],
|
|
73
|
+
workflowAdvisories
|
|
74
|
+
};
|
|
75
|
+
const { findPackagesWithSkills, findWorkspacePackages, findWorkspaceRoot } = await import("./workspace-patterns-x-dLZxx4.mjs");
|
|
76
|
+
const workspaceRoot = findWorkspaceRoot(resolvedRoot);
|
|
77
|
+
if (workspaceRoot) {
|
|
78
|
+
const packageDirsWithSkills = findPackagesWithSkills(workspaceRoot);
|
|
79
|
+
const allPackageDirs = findWorkspacePackages(workspaceRoot);
|
|
80
|
+
const reports = await Promise.all(packageDirsWithSkills.map((packageDir) => checkStaleness(packageDir, readPackageName(packageDir), workspaceRoot)));
|
|
81
|
+
const { readIntentArtifacts } = await import("./artifact-coverage-BAN2W6aH.mjs");
|
|
82
|
+
const coverageSignals = buildWorkspaceCoverageSignals({
|
|
83
|
+
artifactRoot: workspaceRoot,
|
|
84
|
+
artifacts: existsSync(join(workspaceRoot, "_artifacts")) ? readIntentArtifacts(workspaceRoot) : null,
|
|
85
|
+
packageDirs: allPackageDirs
|
|
86
|
+
});
|
|
87
|
+
if (coverageSignals.length > 0) reports.push({
|
|
88
|
+
library: relative(process.cwd(), workspaceRoot) || "workspace",
|
|
89
|
+
currentVersion: null,
|
|
90
|
+
skillVersion: null,
|
|
91
|
+
versionDrift: null,
|
|
92
|
+
skills: [],
|
|
93
|
+
signals: coverageSignals
|
|
94
|
+
});
|
|
95
|
+
if (reports.length > 0) return {
|
|
96
|
+
reports,
|
|
97
|
+
workflowAdvisories
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (existsSync(join(resolvedRoot, "skills"))) return {
|
|
101
|
+
reports: [await checkStaleness(resolvedRoot, readPackageName(resolvedRoot))],
|
|
102
|
+
workflowAdvisories
|
|
103
|
+
};
|
|
104
|
+
const staleResult = await scanIntentsOrFail();
|
|
105
|
+
return {
|
|
106
|
+
reports: await Promise.all(staleResult.packages.map((pkg) => checkStaleness(pkg.packageRoot, pkg.name))),
|
|
107
|
+
workflowAdvisories
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/commands/install-writer.ts
|
|
113
|
+
const INTENT_SKILLS_START = "<!-- intent-skills:start -->";
|
|
114
|
+
const INTENT_SKILLS_END = "<!-- intent-skills:end -->";
|
|
115
|
+
const SUPPORTED_AGENT_CONFIG_FILES = [
|
|
116
|
+
"AGENTS.md",
|
|
117
|
+
"CLAUDE.md",
|
|
118
|
+
".cursorrules",
|
|
119
|
+
".github/copilot-instructions.md"
|
|
120
|
+
];
|
|
121
|
+
const NON_ACTIONABLE_SKILL_TYPES = new Set([
|
|
122
|
+
"maintainer",
|
|
123
|
+
"maintainer-only",
|
|
124
|
+
"meta",
|
|
125
|
+
"reference"
|
|
126
|
+
]);
|
|
127
|
+
function normalizeBlock(content) {
|
|
128
|
+
return content.replace(/\r\n/g, "\n").trimEnd();
|
|
129
|
+
}
|
|
130
|
+
function readManagedBlock(content) {
|
|
131
|
+
const start = content.indexOf(INTENT_SKILLS_START);
|
|
132
|
+
const errors = [];
|
|
133
|
+
if (start === -1) errors.push("Missing intent-skills start marker.");
|
|
134
|
+
const endMarkerStart = start === -1 ? content.indexOf(INTENT_SKILLS_END) : content.indexOf(INTENT_SKILLS_END, start);
|
|
135
|
+
if (endMarkerStart === -1) errors.push("Missing intent-skills end marker.");
|
|
136
|
+
const hasMarker = start !== -1 || endMarkerStart !== -1;
|
|
137
|
+
if (errors.length > 0 || start === -1 || endMarkerStart === -1) return {
|
|
138
|
+
errors,
|
|
139
|
+
hasMarker,
|
|
140
|
+
managedBlock: null
|
|
141
|
+
};
|
|
142
|
+
const end = endMarkerStart + 26;
|
|
143
|
+
return {
|
|
144
|
+
errors,
|
|
145
|
+
hasMarker,
|
|
146
|
+
managedBlock: {
|
|
147
|
+
end,
|
|
148
|
+
start,
|
|
149
|
+
text: content.slice(start, end)
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function parseSkillsList(block) {
|
|
154
|
+
const yamlBody = normalizeBlock(block).split("\n").filter((line) => line !== INTENT_SKILLS_START && line !== INTENT_SKILLS_END).join("\n");
|
|
155
|
+
try {
|
|
156
|
+
const parsed = parse(yamlBody);
|
|
157
|
+
if (!parsed || !Array.isArray(parsed.skills)) return {
|
|
158
|
+
errors: ["Managed block must contain a skills list."],
|
|
159
|
+
skills: []
|
|
160
|
+
};
|
|
161
|
+
return {
|
|
162
|
+
errors: [],
|
|
163
|
+
skills: parsed.skills
|
|
164
|
+
};
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return {
|
|
167
|
+
errors: [`Managed block contains invalid YAML: ${err instanceof Error ? err.message : String(err)}`],
|
|
168
|
+
skills: []
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function verifyIntentSkillsBlockFile({ expectedBlock, expectedMappingCount, targetPath }) {
|
|
173
|
+
const errors = [];
|
|
174
|
+
if (!existsSync(targetPath)) return {
|
|
175
|
+
errors: [`Agent config file was not created: ${targetPath}`],
|
|
176
|
+
ok: false
|
|
177
|
+
};
|
|
178
|
+
const { managedBlock, errors: markerErrors } = readManagedBlock(readFileSync(targetPath, "utf8"));
|
|
179
|
+
errors.push(...markerErrors);
|
|
180
|
+
if (!managedBlock) return {
|
|
181
|
+
errors,
|
|
182
|
+
ok: false
|
|
183
|
+
};
|
|
184
|
+
const block = managedBlock.text;
|
|
185
|
+
if (normalizeBlock(block) !== normalizeBlock(expectedBlock)) errors.push("Managed block does not match generated mappings.");
|
|
186
|
+
if (expectedMappingCount === void 0) return {
|
|
187
|
+
errors,
|
|
188
|
+
ok: errors.length === 0
|
|
189
|
+
};
|
|
190
|
+
const { skills, errors: parseErrors } = parseSkillsList(block);
|
|
191
|
+
errors.push(...parseErrors);
|
|
192
|
+
if (skills.length !== expectedMappingCount) errors.push(`Expected ${expectedMappingCount} skill mappings, found ${skills.length}.`);
|
|
193
|
+
for (const skill of skills) {
|
|
194
|
+
if (!skill || typeof skill !== "object") {
|
|
195
|
+
errors.push("Each skill mapping must be an object.");
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const mapping = skill;
|
|
199
|
+
if (mapping.load !== void 0) errors.push("Skill mappings must use compact `use` entries, not `load`.");
|
|
200
|
+
if (typeof mapping.when !== "string" || mapping.when.trim() === "") errors.push("Each skill mapping must include a non-empty `when` field.");
|
|
201
|
+
if (typeof mapping.use !== "string") errors.push("Each skill mapping must include a `use` field.");
|
|
202
|
+
else try {
|
|
203
|
+
parseSkillUse(mapping.use);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
errors,
|
|
210
|
+
ok: errors.length === 0
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function resolveIntentSkillsBlockTargetPath(root, mappingCount) {
|
|
214
|
+
if (mappingCount === 0) return null;
|
|
215
|
+
return findExistingConfigWithManagedBlock(root)?.filePath ?? join(root, "AGENTS.md");
|
|
216
|
+
}
|
|
217
|
+
function compareNames(a, b) {
|
|
218
|
+
return a.name.localeCompare(b.name);
|
|
219
|
+
}
|
|
220
|
+
function quoteYamlString(value) {
|
|
221
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
|
|
222
|
+
}
|
|
223
|
+
function isActionableSkill(skill) {
|
|
224
|
+
const type = skill.type?.trim().toLowerCase();
|
|
225
|
+
return !type || !NON_ACTIONABLE_SKILL_TYPES.has(type);
|
|
226
|
+
}
|
|
227
|
+
function formatWhen(packageName, skill) {
|
|
228
|
+
return skill.description.replace(/\s+/g, " ").trim() || `Use ${packageName} ${skill.name}`;
|
|
229
|
+
}
|
|
230
|
+
function buildIntentSkillsBlock(scanResult) {
|
|
231
|
+
const lines = [
|
|
232
|
+
INTENT_SKILLS_START,
|
|
233
|
+
"# Skill mappings - load `use` with `npx @tanstack/intent@latest load <use>`.",
|
|
234
|
+
"skills:"
|
|
235
|
+
];
|
|
236
|
+
let mappingCount = 0;
|
|
237
|
+
for (const pkg of [...scanResult.packages].sort(compareNames)) for (const skill of [...pkg.skills].sort(compareNames)) {
|
|
238
|
+
if (!isActionableSkill(skill)) continue;
|
|
239
|
+
mappingCount++;
|
|
240
|
+
lines.push(` - when: ${quoteYamlString(formatWhen(pkg.name, skill))}`);
|
|
241
|
+
lines.push(` use: ${quoteYamlString(formatSkillUse(pkg.name, skill.name))}`);
|
|
242
|
+
}
|
|
243
|
+
if (mappingCount === 0) lines[2] = "skills: []";
|
|
244
|
+
lines.push(INTENT_SKILLS_END);
|
|
245
|
+
return {
|
|
246
|
+
block: `${lines.join("\n")}\n`,
|
|
247
|
+
mappingCount
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function buildIntentSkillGuidanceBlock() {
|
|
251
|
+
return {
|
|
252
|
+
block: `${[
|
|
253
|
+
INTENT_SKILLS_START,
|
|
254
|
+
"## Skill Loading",
|
|
255
|
+
"",
|
|
256
|
+
"Before substantial work:",
|
|
257
|
+
"- Skill check: run `npx @tanstack/intent@latest list`, or use skills already listed in context.",
|
|
258
|
+
"- Skill guidance: if one local skill clearly matches the task, run `npx @tanstack/intent@latest load <package>#<skill>` and follow the returned `SKILL.md`.",
|
|
259
|
+
"- Monorepos: when working across packages, run the skill check from the workspace root and prefer the local skill for the package being changed.",
|
|
260
|
+
"- Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns.",
|
|
261
|
+
INTENT_SKILLS_END
|
|
262
|
+
].join("\n")}\n`,
|
|
263
|
+
mappingCount: 0
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function detectNewline(content) {
|
|
267
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
268
|
+
}
|
|
269
|
+
function withNewlineStyle(content, newline) {
|
|
270
|
+
return newline === "\n" ? content : content.replace(/\n/g, newline);
|
|
271
|
+
}
|
|
272
|
+
function findExistingConfigWithManagedBlock(root) {
|
|
273
|
+
for (const file of SUPPORTED_AGENT_CONFIG_FILES) {
|
|
274
|
+
const filePath = join(root, file);
|
|
275
|
+
if (!existsSync(filePath)) continue;
|
|
276
|
+
const content = readFileSync(filePath, "utf8");
|
|
277
|
+
const { managedBlock, errors, hasMarker } = readManagedBlock(content);
|
|
278
|
+
if (managedBlock) return {
|
|
279
|
+
content,
|
|
280
|
+
filePath,
|
|
281
|
+
managedBlock
|
|
282
|
+
};
|
|
283
|
+
if (hasMarker) throw new Error(`Invalid intent-skills block in ${filePath}: ${errors.join(" ")}`);
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function replaceManagedBlock(content, managedBlock, block) {
|
|
288
|
+
const newline = detectNewline(content);
|
|
289
|
+
const styledBlock = withNewlineStyle(block.trimEnd(), newline);
|
|
290
|
+
return `${content.slice(0, managedBlock.start)}${styledBlock}${content.slice(managedBlock.end)}`;
|
|
291
|
+
}
|
|
292
|
+
function writeIntentSkillsBlock({ block, mappingCount, root, skipWhenEmpty = true }) {
|
|
293
|
+
if (mappingCount === 0 && skipWhenEmpty) return {
|
|
294
|
+
mappingCount,
|
|
295
|
+
status: "skipped",
|
|
296
|
+
targetPath: null
|
|
297
|
+
};
|
|
298
|
+
const existingTarget = findExistingConfigWithManagedBlock(root);
|
|
299
|
+
const targetPath = existingTarget?.filePath ?? join(root, "AGENTS.md");
|
|
300
|
+
if (existingTarget) {
|
|
301
|
+
const nextContent = replaceManagedBlock(existingTarget.content, existingTarget.managedBlock, block);
|
|
302
|
+
if (nextContent === existingTarget.content) return {
|
|
303
|
+
mappingCount,
|
|
304
|
+
status: "unchanged",
|
|
305
|
+
targetPath
|
|
306
|
+
};
|
|
307
|
+
writeFileSync(targetPath, nextContent);
|
|
308
|
+
return {
|
|
309
|
+
mappingCount,
|
|
310
|
+
status: "updated",
|
|
311
|
+
targetPath
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (existsSync(targetPath)) {
|
|
315
|
+
const currentContent = readFileSync(targetPath, "utf8");
|
|
316
|
+
const newline = detectNewline(currentContent);
|
|
317
|
+
const separator = currentContent === "" ? "" : newline;
|
|
318
|
+
writeFileSync(targetPath, `${withNewlineStyle(block, newline)}${separator}${currentContent}`);
|
|
319
|
+
return {
|
|
320
|
+
mappingCount,
|
|
321
|
+
status: "updated",
|
|
322
|
+
targetPath
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
326
|
+
writeFileSync(targetPath, block);
|
|
327
|
+
return {
|
|
328
|
+
mappingCount,
|
|
329
|
+
status: "created",
|
|
330
|
+
targetPath
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/commands/install.ts
|
|
336
|
+
const INSTALL_PROMPT = `You are an AI assistant helping a developer set up skill-to-task mappings for their project.
|
|
337
|
+
|
|
338
|
+
Goal: create or update one agent config file with an intent-skills mapping block.
|
|
339
|
+
|
|
340
|
+
Hard rules:
|
|
341
|
+
- Do not report success until a file was created or updated, or an existing mapping block was confirmed.
|
|
342
|
+
- If skills are discovered and no mapping block exists, create AGENTS.md unless the user asks for another supported config file.
|
|
343
|
+
- If a mapping block already exists in a supported config file, update that file.
|
|
344
|
+
- Preserve all content outside the managed block unchanged.
|
|
345
|
+
- Store compact \`use\` values in the managed block; do not write \`load\` paths.
|
|
346
|
+
- Never write absolute local file paths, node_modules paths, or package-manager-internal paths in the managed block.
|
|
347
|
+
- Verify the target file before your final response.
|
|
348
|
+
|
|
349
|
+
Follow these steps in order:
|
|
350
|
+
|
|
351
|
+
1. CHECK FOR EXISTING MAPPINGS
|
|
352
|
+
Search the project's agent config files (AGENTS.md, CLAUDE.md, .cursorrules,
|
|
353
|
+
.github/copilot-instructions.md) for a block delimited by:
|
|
354
|
+
<!-- intent-skills:start -->
|
|
355
|
+
<!-- intent-skills:end -->
|
|
356
|
+
- If found: show the user the current mappings, keep that file as the source of truth,
|
|
357
|
+
and ask "What would you like to update?" Then skip to step 4 with their requested changes.
|
|
358
|
+
- If not found: continue to step 2.
|
|
359
|
+
|
|
360
|
+
2. DISCOVER AVAILABLE SKILLS
|
|
361
|
+
Run: \`npx @tanstack/intent@latest list\`
|
|
362
|
+
This scans project-local node_modules by default and outputs each package and skill's name,
|
|
363
|
+
description, and source.
|
|
364
|
+
If the user explicitly wants globally installed skills included, run:
|
|
365
|
+
\`npx @tanstack/intent@latest list --global\`
|
|
366
|
+
This works best in Node-compatible environments (npm, pnpm, Bun, or Deno npm interop
|
|
367
|
+
with node_modules enabled).
|
|
368
|
+
If no skills are found, do not create a config file. Report: "No intent-enabled skills found."
|
|
369
|
+
|
|
370
|
+
3. SCAN THE REPOSITORY
|
|
371
|
+
Build a picture of the project's structure and patterns:
|
|
372
|
+
- Read package.json for library dependencies
|
|
373
|
+
- Survey the directory layout (src/, app/, routes/, components/, api/, etc.)
|
|
374
|
+
- Note recurring patterns (routing, data fetching, auth, UI components, etc.)
|
|
375
|
+
|
|
376
|
+
Mapping coverage rule:
|
|
377
|
+
- Create mappings for all discovered actionable skills.
|
|
378
|
+
- Do not omit an actionable skill only because the repo does not currently appear to use it.
|
|
379
|
+
- Do not map reference, meta, maintainer, or maintainer-only skills by default.
|
|
380
|
+
- Include slash-named sub-skills when no parent mapping exists, or when they describe distinct user tasks.
|
|
381
|
+
- If the proposed block would exceed 12 mappings, show the full discovered list and ask which packages
|
|
382
|
+
or skill groups to include before writing.
|
|
383
|
+
- Add one fallback note telling the agent to run \`npx @tanstack/intent@latest list\` for less common local skills.
|
|
384
|
+
|
|
385
|
+
Based on the repository scan and the coverage rule, propose the skill-to-task mappings.
|
|
386
|
+
For each one explain:
|
|
387
|
+
- The task or code area (in plain language the user would recognise)
|
|
388
|
+
- Which skill applies and why
|
|
389
|
+
|
|
390
|
+
Then ask: "What other tasks do you commonly use AI coding agents for?
|
|
391
|
+
I'll create mappings for those too."
|
|
392
|
+
Also ask: "I'll default to AGENTS.md unless you want another supported config file.
|
|
393
|
+
Do you have a preference?"
|
|
394
|
+
|
|
395
|
+
4. WRITE THE MAPPINGS BLOCK
|
|
396
|
+
Once you have the full set of mappings, write or update the agent config file.
|
|
397
|
+
- If you found an existing intent-skills block, update that file in place.
|
|
398
|
+
- Otherwise prefer AGENTS.md by default, unless the user asked for another supported file.
|
|
399
|
+
- Do not stop after discovery. If skills were found, the task is incomplete until this file exists
|
|
400
|
+
and contains the managed block.
|
|
401
|
+
|
|
402
|
+
Use this exact block:
|
|
403
|
+
|
|
404
|
+
<!-- intent-skills:start -->
|
|
405
|
+
# Skill mappings - load \`use\` with \`npx @tanstack/intent@latest load <use>\`.
|
|
406
|
+
skills:
|
|
407
|
+
- when: "describe the task or code area here"
|
|
408
|
+
use: "@scope/package#skill-name"
|
|
409
|
+
<!-- intent-skills:end -->
|
|
410
|
+
|
|
411
|
+
Rules:
|
|
412
|
+
- Use the user's own words for \`when\` descriptions
|
|
413
|
+
- Use compact \`use\` values in \`<package>#<skill>\` format
|
|
414
|
+
- Do not include \`load\`
|
|
415
|
+
- Do not include machine-specific directories such as \`/Users/...\`, \`/home/...\`, \`/private/...\`,
|
|
416
|
+
drive letters, temp workspace paths, \`.pnpm/\`, \`.bun/\`, or \`.yarn/\`.
|
|
417
|
+
- Agents should load \`use\` at runtime with \`npx @tanstack/intent@latest load <use>\`
|
|
418
|
+
- Keep entries concise - this block is read on every agent task
|
|
419
|
+
- Preserve all content outside the block tags unchanged
|
|
420
|
+
- If the user is on Deno, note that this setup is best-effort today and relies on npm interop
|
|
421
|
+
|
|
422
|
+
5. VERIFY AND REPORT
|
|
423
|
+
Before reporting completion:
|
|
424
|
+
- Confirm the target file exists
|
|
425
|
+
- Confirm it contains both managed block markers
|
|
426
|
+
- Confirm every mapping has \`when\` and \`use\`
|
|
427
|
+
- Confirm every \`use\` parses as \`<package>#<skill>\`
|
|
428
|
+
- Confirm no mapping includes \`load\`
|
|
429
|
+
- Confirm no path-like machine-specific values are stored in the managed block
|
|
430
|
+
- Confirm every discovered actionable skill is mapped, skipped by rule, or deferred by user choice
|
|
431
|
+
|
|
432
|
+
Final response must include:
|
|
433
|
+
- The target file path
|
|
434
|
+
- Whether it was created, updated, or already contained a valid block
|
|
435
|
+
- The number of mappings
|
|
436
|
+
- The verification result`;
|
|
437
|
+
function formatTargetPath(targetPath) {
|
|
438
|
+
return relative(process.cwd(), targetPath) || targetPath;
|
|
439
|
+
}
|
|
440
|
+
function formatMappingCount(mappingCount) {
|
|
441
|
+
return `${mappingCount} ${mappingCount === 1 ? "mapping" : "mappings"}`;
|
|
442
|
+
}
|
|
443
|
+
function printNoActionableSkills(warnings) {
|
|
444
|
+
console.log("No intent-enabled skills found.");
|
|
445
|
+
printWarnings(warnings);
|
|
446
|
+
}
|
|
447
|
+
function printPlacementTip(targetPath) {
|
|
448
|
+
console.log(`Tip: Keep the intent-skills block near the top of ${formatTargetPath(targetPath)} so agents read it before task-specific instructions.`);
|
|
449
|
+
}
|
|
450
|
+
function printWriteResult({ mappingCount, status, targetPath }) {
|
|
451
|
+
const target = formatTargetPath(targetPath);
|
|
452
|
+
if (mappingCount === 0) {
|
|
453
|
+
switch (status) {
|
|
454
|
+
case "created":
|
|
455
|
+
console.log(`Created ${target} with skill loading guidance.`);
|
|
456
|
+
break;
|
|
457
|
+
case "updated":
|
|
458
|
+
console.log(`Updated ${target} with skill loading guidance.`);
|
|
459
|
+
break;
|
|
460
|
+
case "unchanged":
|
|
461
|
+
console.log(`No changes to ${target}; skill loading guidance already current.`);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
switch (status) {
|
|
467
|
+
case "created":
|
|
468
|
+
console.log(`Created ${target} with ${formatMappingCount(mappingCount)}.`);
|
|
469
|
+
break;
|
|
470
|
+
case "updated":
|
|
471
|
+
console.log(`Updated ${target} with ${formatMappingCount(mappingCount)}.`);
|
|
472
|
+
break;
|
|
473
|
+
case "unchanged":
|
|
474
|
+
console.log(`No changes to ${target}; ${formatMappingCount(mappingCount)} already current.`);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function runInstallCommand(options, scanIntentsOrFail$1) {
|
|
479
|
+
if (options.printPrompt) {
|
|
480
|
+
console.log(INSTALL_PROMPT);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
scanOptionsFromGlobalFlags(options);
|
|
484
|
+
if (!options.map) {
|
|
485
|
+
const generated$1 = buildIntentSkillGuidanceBlock();
|
|
486
|
+
if (options.dryRun) {
|
|
487
|
+
const targetPath = resolveIntentSkillsBlockTargetPath(process.cwd(), 1);
|
|
488
|
+
console.log(`Generated skill loading guidance for ${formatTargetPath(targetPath)}.`);
|
|
489
|
+
console.log(generated$1.block);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const result$1 = writeIntentSkillsBlock({
|
|
493
|
+
...generated$1,
|
|
494
|
+
root: process.cwd(),
|
|
495
|
+
skipWhenEmpty: false
|
|
496
|
+
});
|
|
497
|
+
if (!result$1.targetPath) fail("Install guidance target was not created.");
|
|
498
|
+
const verification$1 = verifyIntentSkillsBlockFile({
|
|
499
|
+
expectedBlock: generated$1.block,
|
|
500
|
+
targetPath: result$1.targetPath
|
|
501
|
+
});
|
|
502
|
+
const target$1 = formatTargetPath(result$1.targetPath);
|
|
503
|
+
if (!verification$1.ok) fail([`Install verification failed for ${target$1}:`, ...verification$1.errors.map((error) => `- ${error}`)].join("\n"));
|
|
504
|
+
printWriteResult(result$1);
|
|
505
|
+
printPlacementTip(result$1.targetPath);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const scanResult = await scanIntentsOrFail$1(scanOptionsFromGlobalFlags(options));
|
|
509
|
+
const generated = buildIntentSkillsBlock(scanResult);
|
|
510
|
+
if (options.dryRun) {
|
|
511
|
+
const targetPath = resolveIntentSkillsBlockTargetPath(process.cwd(), generated.mappingCount);
|
|
512
|
+
if (!targetPath) {
|
|
513
|
+
printNoActionableSkills(scanResult.warnings);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
console.log(`Generated ${formatMappingCount(generated.mappingCount)} for ${formatTargetPath(targetPath)}.`);
|
|
517
|
+
console.log(generated.block);
|
|
518
|
+
printWarnings(scanResult.warnings);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const result = writeIntentSkillsBlock({
|
|
522
|
+
...generated,
|
|
523
|
+
root: process.cwd()
|
|
524
|
+
});
|
|
525
|
+
if (!result.targetPath) {
|
|
526
|
+
printNoActionableSkills(scanResult.warnings);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const target = formatTargetPath(result.targetPath);
|
|
530
|
+
const verification = verifyIntentSkillsBlockFile({
|
|
531
|
+
expectedBlock: generated.block,
|
|
532
|
+
expectedMappingCount: generated.mappingCount,
|
|
533
|
+
targetPath: result.targetPath
|
|
534
|
+
});
|
|
535
|
+
if (!verification.ok) fail([`Install verification failed for ${target}:`, ...verification.errors.map((error) => `- ${error}`)].join("\n"));
|
|
536
|
+
printWriteResult(result);
|
|
537
|
+
printPlacementTip(result.targetPath);
|
|
538
|
+
printWarnings(scanResult.warnings);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
//#endregion
|
|
542
|
+
export { scanIntentsOrFail as a, fail as c, resolveStaleTargets as i, isCliFailure as l, runInstallCommand as n, scanOptionsFromGlobalFlags as o, getMetaDir as r, printWarnings as s, INSTALL_PROMPT as t };
|