skillshop 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -104
- package/agents-content/000_communication-writing.block.md +11 -0
- package/agents-content/001_external-actions.block.md +9 -0
- package/agents-content/AGENTS.md +11 -0
- package/package.json +6 -3
- package/skills-content/architecture-scanner/SKILL.md +274 -0
- package/skills-content/architecture-scanner/files.json +10 -0
- package/skills-content/architecture-scanner/scan.sh +823 -0
- package/skills-content/change-review/SKILL.md +76 -0
- package/skills-content/change-review/agents/openai.yaml +4 -0
- package/skills-content/changelog/SKILL.md +99 -0
- package/skills-content/changeset/changeset.md +80 -0
- package/skills-content/code-buddy/SKILL.md +136 -0
- package/skills-content/design-system-catalog/SKILL.md +106 -0
- package/skills-content/design-system-catalog/extract-components.mjs +441 -0
- package/skills-content/design-system-catalog/generate-catalog.sh +255 -0
- package/skills-content/flare/SKILL.md +96 -0
- package/skills-content/grilling/SKILL.md +31 -0
- package/skills-content/install-sigmap/SKILL.md +97 -0
- package/skills-content/one-shot/SKILL.md +51 -0
- package/skills-content/release/SKILL.md +128 -0
- package/skills-content/shadow-clone/SKILL.md +228 -0
- package/skills-content/shadow-clone/extract-signatures.mjs +202 -0
- package/skills-content/shadow-clone/scan-repo.mjs +343 -0
- package/skills-content/ship/SKILL.md +95 -0
- package/skills-content/shots/SKILL.md +89 -0
- package/skills-content/shots/agents/openai.yaml +4 -0
- package/skills-content/skillshop/SKILL.md +104 -0
- package/skills-content/skillz/SKILL.md +15 -0
- package/skills-content/transcript-clean/SKILL.md +55 -0
- package/skills-content/transcript-edit/SKILL.md +80 -0
- package/skills-content/vitest/GENERATION.md +5 -0
- package/skills-content/vitest/SKILL.md +52 -0
- package/skills-content/vitest/references/advanced-environments.md +264 -0
- package/skills-content/vitest/references/advanced-projects.md +300 -0
- package/skills-content/vitest/references/advanced-type-testing.md +237 -0
- package/skills-content/vitest/references/advanced-vi.md +249 -0
- package/skills-content/vitest/references/core-cli.md +166 -0
- package/skills-content/vitest/references/core-config.md +174 -0
- package/skills-content/vitest/references/core-describe.md +193 -0
- package/skills-content/vitest/references/core-expect.md +219 -0
- package/skills-content/vitest/references/core-hooks.md +244 -0
- package/skills-content/vitest/references/core-test-api.md +233 -0
- package/skills-content/vitest/references/features-concurrency.md +250 -0
- package/skills-content/vitest/references/features-context.md +238 -0
- package/skills-content/vitest/references/features-coverage.md +207 -0
- package/skills-content/vitest/references/features-filtering.md +211 -0
- package/skills-content/vitest/references/features-mocking.md +265 -0
- package/skills-content/vitest/references/features-snapshots.md +207 -0
- package/skills-content/workleaf/SKILL.md +77 -0
- package/skills-content/worklog/SKILL.md +145 -0
- package/skills-content/worklog/references/summary-format.md +114 -0
- package/skills-content/worktree/SKILL.md +71 -0
- package/src/agents-instructions.js +219 -0
- package/src/cli.js +87 -23
- package/src/import.js +12 -7
- package/src/index.js +1 -0
- package/src/legacy-agents-content.js +36 -0
- package/src/legacy-cli.js +1234 -0
- package/src/library.js +9 -0
- package/src/specifier.js +11 -0
- package/src/user-install.js +48 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* extract-signatures.mjs — Extracts exported symbols and structural signatures
|
|
5
|
+
* from source files to help shadow-clone understand the public API surface
|
|
6
|
+
* without reading every line.
|
|
7
|
+
*
|
|
8
|
+
* Supports: JavaScript/TypeScript, Python, Go, Rust, Ruby (basic heuristics).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node extract-signatures.mjs <file-or-directory> [--ext .ts,.tsx,.js]
|
|
12
|
+
*
|
|
13
|
+
* Outputs Markdown to stdout with extracted signatures grouped by file.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { execSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
|
|
22
|
+
function flag(name) {
|
|
23
|
+
const i = args.indexOf(name);
|
|
24
|
+
if (i === -1) return undefined;
|
|
25
|
+
return args.splice(i, 2)[1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const extFilter = flag("--ext")?.split(",") ?? null;
|
|
29
|
+
const target = path.resolve(args[0] || ".");
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// File collection
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function collectFiles(dir) {
|
|
36
|
+
try {
|
|
37
|
+
const out = execSync("git ls-files --cached --others --exclude-standard", {
|
|
38
|
+
cwd: dir, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024,
|
|
39
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
return out.split("\n").map(l => l.trim()).filter(Boolean).map(f => path.join(dir, f));
|
|
42
|
+
} catch {
|
|
43
|
+
return walkDir(dir);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function walkDir(dir) {
|
|
48
|
+
const skip = new Set(["node_modules", "vendor", "__pycache__", "target", "dist", "build", ".git"]);
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
+
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
52
|
+
if (skip.has(entry.name)) continue;
|
|
53
|
+
const full = path.join(dir, entry.name);
|
|
54
|
+
if (entry.isDirectory()) results.push(...walkDir(full));
|
|
55
|
+
else results.push(full);
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Signature extractors
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
const extractors = {
|
|
65
|
+
// JS / TS
|
|
66
|
+
".js": extractJS, ".jsx": extractJS, ".ts": extractJS, ".tsx": extractJS, ".mjs": extractJS,
|
|
67
|
+
// Python
|
|
68
|
+
".py": extractPython,
|
|
69
|
+
// Go
|
|
70
|
+
".go": extractGo,
|
|
71
|
+
// Rust
|
|
72
|
+
".rs": extractRust,
|
|
73
|
+
// Ruby
|
|
74
|
+
".rb": extractRuby,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function extractJS(content) {
|
|
78
|
+
const sigs = [];
|
|
79
|
+
const patterns = [
|
|
80
|
+
// export function / export default function
|
|
81
|
+
/^export\s+(default\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm,
|
|
82
|
+
// export const/let/var
|
|
83
|
+
/^export\s+(?:const|let|var)\s+(\w+)\s*(?::\s*([^=]+?))?\s*=/gm,
|
|
84
|
+
// export class / export default class
|
|
85
|
+
/^export\s+(default\s+)?class\s+(\w+)/gm,
|
|
86
|
+
// export interface / export type (TS)
|
|
87
|
+
/^export\s+(?:interface|type)\s+(\w+)/gm,
|
|
88
|
+
// module.exports
|
|
89
|
+
/^module\.exports\s*=\s*(\{[\s\S]*?\}|[\w.]+)/gm,
|
|
90
|
+
// Named exports block: export { ... }
|
|
91
|
+
/^export\s+\{([^}]+)\}/gm,
|
|
92
|
+
];
|
|
93
|
+
for (const p of patterns) {
|
|
94
|
+
for (const m of content.matchAll(p)) {
|
|
95
|
+
sigs.push(m[0].trim().slice(0, 200));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// React components: const X = (props) => / function X(
|
|
99
|
+
for (const m of content.matchAll(/^(?:export\s+)?(?:const|function)\s+([A-Z]\w+)\s*(?:=\s*(?:\([^)]*\)|[^=])\s*=>|[\s(])/gm)) {
|
|
100
|
+
const line = m[0].trim().slice(0, 200);
|
|
101
|
+
if (!sigs.includes(line)) sigs.push(line);
|
|
102
|
+
}
|
|
103
|
+
return sigs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractPython(content) {
|
|
107
|
+
const sigs = [];
|
|
108
|
+
for (const m of content.matchAll(/^(?:class|def|async\s+def)\s+\w+[^:]*:/gm)) {
|
|
109
|
+
sigs.push(m[0].trim().slice(0, 200));
|
|
110
|
+
}
|
|
111
|
+
// Top-level assignments that look like exports
|
|
112
|
+
for (const m of content.matchAll(/^[A-Z_][A-Z_0-9]*\s*=\s*.+/gm)) {
|
|
113
|
+
sigs.push(m[0].trim().slice(0, 200));
|
|
114
|
+
}
|
|
115
|
+
return sigs;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractGo(content) {
|
|
119
|
+
const sigs = [];
|
|
120
|
+
// Exported functions/methods (start with uppercase)
|
|
121
|
+
for (const m of content.matchAll(/^func\s+(?:\([^)]+\)\s+)?([A-Z]\w*)\s*\([^)]*\)[^{]*/gm)) {
|
|
122
|
+
sigs.push(m[0].trim().slice(0, 200));
|
|
123
|
+
}
|
|
124
|
+
// Exported types
|
|
125
|
+
for (const m of content.matchAll(/^type\s+([A-Z]\w*)\s+(?:struct|interface)\s*\{/gm)) {
|
|
126
|
+
sigs.push(m[0].trim().slice(0, 200));
|
|
127
|
+
}
|
|
128
|
+
return sigs;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractRust(content) {
|
|
132
|
+
const sigs = [];
|
|
133
|
+
for (const m of content.matchAll(/^pub\s+(?:async\s+)?(?:fn|struct|enum|trait|type|const|static|mod)\s+\w+[^{;]*/gm)) {
|
|
134
|
+
sigs.push(m[0].trim().slice(0, 200));
|
|
135
|
+
}
|
|
136
|
+
return sigs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function extractRuby(content) {
|
|
140
|
+
const sigs = [];
|
|
141
|
+
for (const m of content.matchAll(/^\s*(?:class|module|def)\s+\S+/gm)) {
|
|
142
|
+
sigs.push(m[0].trim().slice(0, 200));
|
|
143
|
+
}
|
|
144
|
+
return sigs;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Main
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
const stat = fs.statSync(target);
|
|
152
|
+
let files;
|
|
153
|
+
if (stat.isDirectory()) {
|
|
154
|
+
files = collectFiles(target);
|
|
155
|
+
} else {
|
|
156
|
+
files = [target];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Filter by extension
|
|
160
|
+
if (extFilter) {
|
|
161
|
+
files = files.filter(f => extFilter.includes(path.extname(f)));
|
|
162
|
+
} else {
|
|
163
|
+
files = files.filter(f => extractors[path.extname(f)]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort for deterministic output
|
|
167
|
+
files.sort();
|
|
168
|
+
|
|
169
|
+
const output = [];
|
|
170
|
+
output.push("# Extracted Signatures");
|
|
171
|
+
output.push("");
|
|
172
|
+
|
|
173
|
+
let totalSigs = 0;
|
|
174
|
+
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
const ext = path.extname(file);
|
|
177
|
+
const extractor = extractors[ext];
|
|
178
|
+
if (!extractor) continue;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const stat = fs.statSync(file);
|
|
182
|
+
if (stat.size > 512_000) continue; // skip large files
|
|
183
|
+
|
|
184
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
185
|
+
const sigs = extractor(content);
|
|
186
|
+
if (sigs.length === 0) continue;
|
|
187
|
+
|
|
188
|
+
totalSigs += sigs.length;
|
|
189
|
+
const rel = path.relative(target, file) || path.basename(file);
|
|
190
|
+
output.push(`## \`${rel}\``);
|
|
191
|
+
output.push("");
|
|
192
|
+
for (const sig of sigs) {
|
|
193
|
+
output.push(`- \`${sig}\``);
|
|
194
|
+
}
|
|
195
|
+
output.push("");
|
|
196
|
+
} catch { /* skip unreadable files */ }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
output.push(`---`);
|
|
200
|
+
output.push(`**Total:** ${totalSigs} signatures across ${files.length} files`);
|
|
201
|
+
|
|
202
|
+
console.log(output.join("\n"));
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scan-repo.mjs — Deterministic repository scanner for the shadow-clone skill.
|
|
5
|
+
*
|
|
6
|
+
* Produces a structured Markdown report of a local repository including:
|
|
7
|
+
* - File tree (respecting .gitignore via `git ls-files`)
|
|
8
|
+
* - Per-extension statistics (file count, lines of code)
|
|
9
|
+
* - Key/manifest files detected
|
|
10
|
+
* - Dependency summaries extracted from manifest files
|
|
11
|
+
* - Entry-point candidates
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node scan-repo.mjs <directory> [--focus <glob>] [--max-depth <n>] [--json]
|
|
15
|
+
*
|
|
16
|
+
* Outputs Markdown to stdout (or JSON with --json).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// CLI args
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const args = process.argv.slice(2);
|
|
28
|
+
function flag(name) {
|
|
29
|
+
const i = args.indexOf(name);
|
|
30
|
+
if (i === -1) return undefined;
|
|
31
|
+
return args.splice(i, 2)[1];
|
|
32
|
+
}
|
|
33
|
+
function bool(name) {
|
|
34
|
+
const i = args.indexOf(name);
|
|
35
|
+
if (i === -1) return false;
|
|
36
|
+
args.splice(i, 1);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const jsonOutput = bool("--json");
|
|
41
|
+
const focusGlob = flag("--focus");
|
|
42
|
+
const maxDepth = Number(flag("--max-depth")) || Infinity;
|
|
43
|
+
const targetDir = path.resolve(args[0] || ".");
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(targetDir)) {
|
|
46
|
+
console.error(`Error: directory does not exist: ${targetDir}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// File listing — prefer git ls-files, fallback to recursive readdir
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
function listFilesGit(dir) {
|
|
55
|
+
try {
|
|
56
|
+
const out = execSync("git ls-files --cached --others --exclude-standard", {
|
|
57
|
+
cwd: dir,
|
|
58
|
+
encoding: "utf-8",
|
|
59
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
60
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
return out
|
|
63
|
+
.split("\n")
|
|
64
|
+
.map((l) => l.trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function listFilesFallback(dir, prefix = "") {
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
74
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "vendor" || entry.name === "__pycache__" || entry.name === "target" || entry.name === "dist" || entry.name === "build") continue;
|
|
75
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
results.push(...listFilesFallback(path.join(dir, entry.name), rel));
|
|
78
|
+
} else {
|
|
79
|
+
results.push(rel);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let allFiles = listFilesGit(targetDir) ?? listFilesFallback(targetDir);
|
|
86
|
+
|
|
87
|
+
// Apply focus filter
|
|
88
|
+
if (focusGlob) {
|
|
89
|
+
const pattern = new RegExp(
|
|
90
|
+
"^" +
|
|
91
|
+
focusGlob
|
|
92
|
+
.replace(/\./g, "\\.")
|
|
93
|
+
.replace(/\*\*/g, "⚡")
|
|
94
|
+
.replace(/\*/g, "[^/]*")
|
|
95
|
+
.replace(/⚡/g, ".*") +
|
|
96
|
+
"$"
|
|
97
|
+
);
|
|
98
|
+
allFiles = allFiles.filter((f) => pattern.test(f));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Apply max-depth filter
|
|
102
|
+
if (maxDepth < Infinity) {
|
|
103
|
+
allFiles = allFiles.filter((f) => f.split("/").length <= maxDepth);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Analysis
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
const MANIFEST_FILES = new Set([
|
|
111
|
+
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
112
|
+
"Cargo.toml", "Cargo.lock",
|
|
113
|
+
"go.mod", "go.sum",
|
|
114
|
+
"requirements.txt", "setup.py", "setup.cfg", "pyproject.toml", "Pipfile",
|
|
115
|
+
"Gemfile", "Gemfile.lock",
|
|
116
|
+
"composer.json",
|
|
117
|
+
"build.gradle", "build.gradle.kts", "pom.xml",
|
|
118
|
+
"Makefile", "CMakeLists.txt",
|
|
119
|
+
"Dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
120
|
+
".env.example",
|
|
121
|
+
"tsconfig.json", "jsconfig.json",
|
|
122
|
+
"vite.config.js", "vite.config.ts", "webpack.config.js", "rollup.config.js",
|
|
123
|
+
"next.config.js", "next.config.mjs", "nuxt.config.ts",
|
|
124
|
+
"tailwind.config.js", "tailwind.config.ts", "postcss.config.js",
|
|
125
|
+
".eslintrc.js", ".eslintrc.json", "eslint.config.js",
|
|
126
|
+
".prettierrc", ".prettierrc.json",
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const ENTRY_PATTERNS = [
|
|
130
|
+
/^src\/(main|index|app|entry)\.\w+$/,
|
|
131
|
+
/^(main|index|app)\.\w+$/,
|
|
132
|
+
/^cmd\/[^/]+\/main\.go$/,
|
|
133
|
+
/^lib\.rs$/,
|
|
134
|
+
/^src\/lib\.rs$/,
|
|
135
|
+
/^src\/main\.rs$/,
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const extStats = {}; // ext -> { count, lines }
|
|
139
|
+
const manifests = []; // relative paths
|
|
140
|
+
const entryPoints = [];
|
|
141
|
+
const dirSet = new Set();
|
|
142
|
+
|
|
143
|
+
for (const rel of allFiles) {
|
|
144
|
+
const ext = path.extname(rel) || "(none)";
|
|
145
|
+
if (!extStats[ext]) extStats[ext] = { count: 0, lines: 0 };
|
|
146
|
+
extStats[ext].count++;
|
|
147
|
+
|
|
148
|
+
// Count lines for source-like files (skip binaries / large blobs)
|
|
149
|
+
const abs = path.join(targetDir, rel);
|
|
150
|
+
try {
|
|
151
|
+
const stat = fs.statSync(abs);
|
|
152
|
+
if (stat.size < 512_000) {
|
|
153
|
+
const content = fs.readFileSync(abs, "utf-8");
|
|
154
|
+
extStats[ext].lines += content.split("\n").length;
|
|
155
|
+
}
|
|
156
|
+
} catch { /* skip unreadable */ }
|
|
157
|
+
|
|
158
|
+
// Track directories
|
|
159
|
+
const parts = rel.split("/");
|
|
160
|
+
for (let i = 1; i < parts.length; i++) {
|
|
161
|
+
dirSet.add(parts.slice(0, i).join("/"));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Manifests
|
|
165
|
+
const basename = path.basename(rel);
|
|
166
|
+
if (MANIFEST_FILES.has(basename) || basename === "README.md" || basename === "README.rst") {
|
|
167
|
+
manifests.push(rel);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Entry points
|
|
171
|
+
if (ENTRY_PATTERNS.some((p) => p.test(rel))) {
|
|
172
|
+
entryPoints.push(rel);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Dependency extraction from key manifests
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
function extractDeps(relPath) {
|
|
181
|
+
const abs = path.join(targetDir, relPath);
|
|
182
|
+
const basename = path.basename(relPath);
|
|
183
|
+
try {
|
|
184
|
+
const raw = fs.readFileSync(abs, "utf-8");
|
|
185
|
+
if (basename === "package.json") {
|
|
186
|
+
const pkg = JSON.parse(raw);
|
|
187
|
+
return {
|
|
188
|
+
type: "npm",
|
|
189
|
+
name: pkg.name,
|
|
190
|
+
deps: Object.keys(pkg.dependencies || {}),
|
|
191
|
+
devDeps: Object.keys(pkg.devDependencies || {}),
|
|
192
|
+
scripts: Object.keys(pkg.scripts || {}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (basename === "go.mod") {
|
|
196
|
+
const modLine = raw.match(/^module\s+(.+)$/m);
|
|
197
|
+
const requires = [...raw.matchAll(/^\t(.+?)\s/gm)].map((m) => m[1]);
|
|
198
|
+
return { type: "go", module: modLine?.[1], deps: requires };
|
|
199
|
+
}
|
|
200
|
+
if (basename === "requirements.txt") {
|
|
201
|
+
const deps = raw.split("\n").map((l) => l.trim().split(/[>=<!\s]/)[0]).filter(Boolean);
|
|
202
|
+
return { type: "python-requirements", deps };
|
|
203
|
+
}
|
|
204
|
+
if (basename === "pyproject.toml") {
|
|
205
|
+
const nameLine = raw.match(/^name\s*=\s*"(.+?)"/m);
|
|
206
|
+
return { type: "python-pyproject", name: nameLine?.[1], raw_snippet: raw.slice(0, 500) };
|
|
207
|
+
}
|
|
208
|
+
if (basename === "Cargo.toml") {
|
|
209
|
+
const nameLine = raw.match(/^name\s*=\s*"(.+?)"/m);
|
|
210
|
+
const depsSection = raw.match(/\[dependencies\]([\s\S]*?)(\[|$)/);
|
|
211
|
+
const deps = depsSection
|
|
212
|
+
? [...depsSection[1].matchAll(/^(\w[\w-]*)/gm)].map((m) => m[1])
|
|
213
|
+
: [];
|
|
214
|
+
return { type: "cargo", name: nameLine?.[1], deps };
|
|
215
|
+
}
|
|
216
|
+
if (basename === "Gemfile") {
|
|
217
|
+
const gems = [...raw.matchAll(/gem\s+['"](.+?)['"]/g)].map((m) => m[1]);
|
|
218
|
+
return { type: "ruby", deps: gems };
|
|
219
|
+
}
|
|
220
|
+
} catch { /* skip */ }
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const depSummaries = {};
|
|
225
|
+
for (const m of manifests) {
|
|
226
|
+
const info = extractDeps(m);
|
|
227
|
+
if (info) depSummaries[m] = info;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Build file tree (compact)
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
function buildTree(files, maxItems = 500) {
|
|
235
|
+
const truncated = files.length > maxItems;
|
|
236
|
+
const subset = truncated ? files.slice(0, maxItems) : files;
|
|
237
|
+
const lines = [];
|
|
238
|
+
const dirs = new Map(); // dir -> files[]
|
|
239
|
+
|
|
240
|
+
for (const f of subset) {
|
|
241
|
+
const dir = path.dirname(f);
|
|
242
|
+
if (!dirs.has(dir)) dirs.set(dir, []);
|
|
243
|
+
dirs.get(dir).push(path.basename(f));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const sortedDirs = [...dirs.keys()].sort();
|
|
247
|
+
for (const dir of sortedDirs) {
|
|
248
|
+
if (dir !== ".") lines.push(`${dir}/`);
|
|
249
|
+
for (const file of dirs.get(dir).sort()) {
|
|
250
|
+
lines.push(dir === "." ? ` ${file}` : ` ${dir}/${file}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (truncated) lines.push(` ... and ${files.length - maxItems} more files`);
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Output
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
const report = {
|
|
263
|
+
target: targetDir,
|
|
264
|
+
totalFiles: allFiles.length,
|
|
265
|
+
totalDirs: dirSet.size,
|
|
266
|
+
extensions: Object.entries(extStats)
|
|
267
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
268
|
+
.map(([ext, s]) => ({ ext, ...s })),
|
|
269
|
+
manifests,
|
|
270
|
+
entryPoints,
|
|
271
|
+
depSummaries,
|
|
272
|
+
tree: buildTree(allFiles),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
if (jsonOutput) {
|
|
276
|
+
console.log(JSON.stringify(report, null, 2));
|
|
277
|
+
} else {
|
|
278
|
+
const lines = [];
|
|
279
|
+
lines.push(`# Repository Scan: ${path.basename(targetDir)}`);
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push(`**Path:** \`${targetDir}\``);
|
|
282
|
+
lines.push(`**Files:** ${report.totalFiles} | **Directories:** ${report.totalDirs}`);
|
|
283
|
+
lines.push("");
|
|
284
|
+
|
|
285
|
+
// Extension stats
|
|
286
|
+
lines.push("## File Statistics");
|
|
287
|
+
lines.push("");
|
|
288
|
+
lines.push("| Extension | Files | Lines |");
|
|
289
|
+
lines.push("|-----------|------:|------:|");
|
|
290
|
+
for (const { ext, count, lines: loc } of report.extensions) {
|
|
291
|
+
lines.push(`| ${ext} | ${count} | ${loc.toLocaleString()} |`);
|
|
292
|
+
}
|
|
293
|
+
lines.push("");
|
|
294
|
+
|
|
295
|
+
// Key files
|
|
296
|
+
lines.push("## Key / Manifest Files");
|
|
297
|
+
lines.push("");
|
|
298
|
+
for (const m of manifests) lines.push(`- \`${m}\``);
|
|
299
|
+
lines.push("");
|
|
300
|
+
|
|
301
|
+
// Entry points
|
|
302
|
+
if (entryPoints.length) {
|
|
303
|
+
lines.push("## Detected Entry Points");
|
|
304
|
+
lines.push("");
|
|
305
|
+
for (const e of entryPoints) lines.push(`- \`${e}\``);
|
|
306
|
+
lines.push("");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Dependencies
|
|
310
|
+
if (Object.keys(depSummaries).length) {
|
|
311
|
+
lines.push("## Dependency Summary");
|
|
312
|
+
lines.push("");
|
|
313
|
+
for (const [file, info] of Object.entries(depSummaries)) {
|
|
314
|
+
lines.push(`### \`${file}\` (${info.type})`);
|
|
315
|
+
if (info.name) lines.push(`**Name:** ${info.name}`);
|
|
316
|
+
if (info.module) lines.push(`**Module:** ${info.module}`);
|
|
317
|
+
if (info.deps?.length) {
|
|
318
|
+
lines.push(`**Dependencies (${info.deps.length}):** ${info.deps.join(", ")}`);
|
|
319
|
+
}
|
|
320
|
+
if (info.devDeps?.length) {
|
|
321
|
+
lines.push(`**Dev Dependencies (${info.devDeps.length}):** ${info.devDeps.join(", ")}`);
|
|
322
|
+
}
|
|
323
|
+
if (info.scripts?.length) {
|
|
324
|
+
lines.push(`**Scripts:** ${info.scripts.join(", ")}`);
|
|
325
|
+
}
|
|
326
|
+
if (info.raw_snippet) {
|
|
327
|
+
lines.push("```toml");
|
|
328
|
+
lines.push(info.raw_snippet);
|
|
329
|
+
lines.push("```");
|
|
330
|
+
}
|
|
331
|
+
lines.push("");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// File tree
|
|
336
|
+
lines.push("## File Tree");
|
|
337
|
+
lines.push("");
|
|
338
|
+
lines.push("```");
|
|
339
|
+
lines.push(report.tree);
|
|
340
|
+
lines.push("```");
|
|
341
|
+
|
|
342
|
+
console.log(lines.join("\n"));
|
|
343
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ship
|
|
3
|
+
description: Runs a repository-aware workflow to isolate, plan, implement, review, validate, and push a change.
|
|
4
|
+
metadata:
|
|
5
|
+
author: Daniel Fosco
|
|
6
|
+
version: "2026.8.30"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Ship Skill
|
|
10
|
+
|
|
11
|
+
> Triggered by: "ship", "ship this", "ship a feature", "ship it", "ship a change"
|
|
12
|
+
>
|
|
13
|
+
> This skill ships a change to a reviewable remote branch. It must not publish a package or create a release unless the user separately invokes the release workflow.
|
|
14
|
+
|
|
15
|
+
## What This Does
|
|
16
|
+
|
|
17
|
+
Carries a change from repository discovery through implementation, validation, review, documentation, and push. It follows the target repository's branch, worktree, planning, testing, commit, and review conventions rather than prescribing a particular toolchain.
|
|
18
|
+
|
|
19
|
+
## Step 1: Discover Repository Policy
|
|
20
|
+
|
|
21
|
+
Before choosing a branch or path, inspect repository instructions, current worktrees, branch state, remotes, package or build tooling, required checks, contribution guidance, and available local skills. Identify whether the repository requires:
|
|
22
|
+
|
|
23
|
+
- a worktree or permits work on the current branch;
|
|
24
|
+
- a branch naming convention or issue identifier;
|
|
25
|
+
- a checked-in plan or change request;
|
|
26
|
+
- specific test, lint, build, formatting, or documentation commands;
|
|
27
|
+
- commit-message conventions or attribution trailers;
|
|
28
|
+
- a pull request instead of a direct branch push.
|
|
29
|
+
|
|
30
|
+
Do not turn examples from this skill into repository policy.
|
|
31
|
+
|
|
32
|
+
## Step 2: Isolate The Change
|
|
33
|
+
|
|
34
|
+
Use the `worktree` skill when available and appropriate. Derive a short branch name from the requested outcome, preserving any user-provided branch name and documented prefix. Confirm the name only when it is ambiguous or repository policy requires confirmation.
|
|
35
|
+
|
|
36
|
+
Use the path returned by the worktree workflow for all subsequent commands. Do not assume it is `worktrees/<branch-name>`. Never discard changes in the source or target worktree.
|
|
37
|
+
|
|
38
|
+
If the repository explicitly does not use worktrees, create or use the required branch without silently switching a dirty working tree.
|
|
39
|
+
|
|
40
|
+
## Step 3: Plan Against Goals
|
|
41
|
+
|
|
42
|
+
Explore the relevant code before writing a plan. Define one to three measurable user-facing goals, then describe the smallest approach that satisfies them, affected files, implementation steps, edge cases, compatibility concerns, and validation.
|
|
43
|
+
|
|
44
|
+
Write the plan only when the repository has an established plan location or the user asks for a plan artifact. Otherwise keep it in task tracking. If approval is needed because the request is ambiguous, risky, destructive, or materially larger than stated, present the tradeoff and ask before implementation; do not add an unconditional approval gate to routine work.
|
|
45
|
+
|
|
46
|
+
Use repository-local issue or task tooling when available and useful. Do not require a specific tracker.
|
|
47
|
+
|
|
48
|
+
## Step 4: Implement
|
|
49
|
+
|
|
50
|
+
Make the smallest correct changes consistent with existing architecture and style. Preserve unrelated worktree changes. Add or update tests alongside the behavior they cover using the repository's existing framework and patterns.
|
|
51
|
+
|
|
52
|
+
Do not split implementation and tests into separate commits by default. Commit boundaries should represent coherent reviewable changes, not phases imposed by this skill.
|
|
53
|
+
|
|
54
|
+
## Step 5: Validate
|
|
55
|
+
|
|
56
|
+
Run the checks discovered in Step 1. Prefer documented aggregate commands. If no aggregate command exists, run the relevant tests and static checks for the changed area, expanding to the full suite when risk or repository policy warrants it.
|
|
57
|
+
|
|
58
|
+
Do not assume Node.js, npm, Vitest, or scripts named `lint`, `build`, `test`, or `dev`. Report any check that could not run and why.
|
|
59
|
+
|
|
60
|
+
## Step 6: Review
|
|
61
|
+
|
|
62
|
+
Review the full branch diff against its base, not only the last commit. Check for behavioral regressions, security problems, broken interfaces, concurrency or error-path failures, missing tests, and needless complexity.
|
|
63
|
+
|
|
64
|
+
Use an available review agent or skill when it adds value, but do not require an agent name that may not exist. Apply high-confidence fixes, rerun affected checks, and avoid speculative refactors that do not serve a stated goal.
|
|
65
|
+
|
|
66
|
+
Read existing architecture documentation for changed files when present. Update generated or authored architecture docs only through the repository's documented process.
|
|
67
|
+
|
|
68
|
+
## Step 7: Document And Commit
|
|
69
|
+
|
|
70
|
+
Update user-facing or architectural documentation when behavior, interfaces, operations, or durable design contracts changed. Do not edit every common documentation filename mechanically.
|
|
71
|
+
|
|
72
|
+
Stage only intended files and inspect the staged diff. Follow the repository's commit conventions. Add co-author or sign-off trailers only when required by repository policy or requested by the user; never hardcode an identity.
|
|
73
|
+
|
|
74
|
+
## Step 8: Push And Report
|
|
75
|
+
|
|
76
|
+
Push the branch to the configured remote only when shipping includes a push under the user's request and repository policy. Use the actual remote name rather than assuming `origin`. If the repository expects a pull request and the user asked for one, create it using the repository template and return its URL.
|
|
77
|
+
|
|
78
|
+
Report:
|
|
79
|
+
|
|
80
|
+
- branch and worktree path;
|
|
81
|
+
- concise change summary;
|
|
82
|
+
- validation commands and results;
|
|
83
|
+
- review findings resolved or left open;
|
|
84
|
+
- commit, remote branch, and pull request URL when created;
|
|
85
|
+
- any manual verification still required.
|
|
86
|
+
|
|
87
|
+
Start a development server only when requested or needed for verification. Use the documented command and report how to stop it.
|
|
88
|
+
|
|
89
|
+
## Rules
|
|
90
|
+
|
|
91
|
+
- Never ship directly to a protected or default branch unless repository policy and the user explicitly require it.
|
|
92
|
+
- Never release or publish as part of this workflow.
|
|
93
|
+
- Never assume a worktree directory, branch prefix, package manager, test framework, tracker, plan path, dev command, remote name, or commit trailer.
|
|
94
|
+
- Stop before destructive operations or unresolved high-severity failures; ordinary recoverable failures should be investigated and fixed.
|
|
95
|
+
- Preserve unrelated changes and state exactly what was pushed.
|