sdd-jc-methodology 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/sdd-archive.md +122 -0
- package/.claude/commands/sdd-constitution.md +240 -0
- package/.claude/commands/sdd-execute.md +132 -0
- package/.claude/commands/sdd-propose.md +149 -0
- package/.claude/commands/sdd-seo.md +251 -0
- package/.claude/commands/sdd-specify.md +264 -0
- package/.claude/commands/sdd-test.md +128 -0
- package/.claude/commands/sdd-validate.md +165 -0
- package/.claude/skills/api-design-principles/SKILL.md +528 -0
- package/.claude/skills/api-design-principles/assets/api-design-checklist.md +155 -0
- package/.claude/skills/api-design-principles/assets/rest-api-template.py +182 -0
- package/.claude/skills/api-design-principles/references/graphql-schema-design.md +583 -0
- package/.claude/skills/api-design-principles/references/rest-best-practices.md +408 -0
- package/.claude/skills/aws-serverless/SKILL.md +323 -0
- package/.claude/skills/brainstorming/SKILL.md +96 -0
- package/.claude/skills/error-handling-patterns/SKILL.md +641 -0
- package/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/.claude/skills/frontend-design/SKILL.md +272 -0
- package/.claude/skills/nestjs-expert/SKILL.md +552 -0
- package/.claude/skills/product-manager-toolkit/SKILL.md +351 -0
- package/.claude/skills/product-manager-toolkit/references/prd_templates.md +317 -0
- package/.claude/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py +441 -0
- package/.claude/skills/product-manager-toolkit/scripts/rice_prioritizer.py +296 -0
- package/.claude/skills/react-doctor/AGENTS.md +15 -0
- package/.claude/skills/react-doctor/SKILL.md +19 -0
- package/.claude/skills/shadcn-ui/SKILL.md +1677 -0
- package/.claude/skills/shadcn-ui/references/learn.md +145 -0
- package/.claude/skills/shadcn-ui/references/official-ui-reference.md +1725 -0
- package/.claude/skills/shadcn-ui/references/reference.md +586 -0
- package/.claude/skills/shadcn-ui/references/ui-reference.md +1578 -0
- package/.claude/skills/stitch-design/README.md +50 -0
- package/.claude/skills/stitch-design/SKILL.md +84 -0
- package/.claude/skills/stitch-design/examples/DESIGN.md +22 -0
- package/.claude/skills/stitch-design/examples/enhanced-prompt.md +28 -0
- package/.claude/skills/stitch-design/references/design-mappings.md +45 -0
- package/.claude/skills/stitch-design/references/prompt-keywords.md +114 -0
- package/.claude/skills/stitch-design/references/tool-schemas.md +76 -0
- package/.claude/skills/stitch-design/workflows/edit-design.md +44 -0
- package/.claude/skills/stitch-design/workflows/generate-design-md.md +63 -0
- package/.claude/skills/stitch-design/workflows/text-to-design.md +47 -0
- package/.claude/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/.claude/skills/systematic-debugging/SKILL.md +296 -0
- package/.claude/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/.claude/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/.claude/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/.claude/skills/systematic-debugging/find-polluter.sh +63 -0
- package/.claude/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/.claude/skills/systematic-debugging/test-academic.md +14 -0
- package/.claude/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/.claude/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/.claude/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/.claude/skills/tailwind-design-system/SKILL.md +874 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.claude/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.claude/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/.claude/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.claude/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.claude/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.mcp.json.example +12 -0
- package/CHANGELOG.md +61 -0
- package/LICENSE +21 -0
- package/README.md +571 -0
- package/assets/jc-fox-mark.svg +10 -0
- package/assets/jc-methodology-badge.png +0 -0
- package/bin/sdd-jc.js +379 -0
- package/package.json +43 -0
- package/scripts/gsc_verify.py +162 -0
package/bin/sdd-jc.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
8
|
+
const SOURCE_CLAUDE = path.join(PACKAGE_ROOT, ".claude");
|
|
9
|
+
const SOURCE_COMMANDS = path.join(SOURCE_CLAUDE, "commands");
|
|
10
|
+
const SOURCE_SKILLS = path.join(SOURCE_CLAUDE, "skills");
|
|
11
|
+
const SOURCE_SCRIPTS = path.join(PACKAGE_ROOT, "scripts");
|
|
12
|
+
const SOURCE_MCP_EXAMPLE = path.join(PACKAGE_ROOT, ".mcp.json.example");
|
|
13
|
+
|
|
14
|
+
const TOOL_DEFAULTS = {
|
|
15
|
+
claude: path.join(os.homedir(), ".claude"),
|
|
16
|
+
opencode: path.join(os.homedir(), ".config", "opencode"),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function printHelp() {
|
|
20
|
+
console.log(`SDD JC Methodology CLI
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
sdd-jc <command> [options]
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
install Install commands, skills, and helper resources
|
|
27
|
+
update Reinstall commands, skills, and helper resources
|
|
28
|
+
doctor Check whether expected files are installed
|
|
29
|
+
list List packaged commands, skills, and helper resources
|
|
30
|
+
help Show this help
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--tool <name> Install target: claude, opencode, or both. Default: claude
|
|
34
|
+
--target <path> Target config directory for selected single tool
|
|
35
|
+
--claude-target Claude config directory. Default: ~/.claude
|
|
36
|
+
--opencode-target OpenCode config directory. Default: ~/.config/opencode
|
|
37
|
+
--force Overwrite existing files
|
|
38
|
+
--dry-run Show what would happen without writing files
|
|
39
|
+
--commands-only Install or check only commands
|
|
40
|
+
--skills-only Install or check only skills
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
sdd-jc install
|
|
44
|
+
sdd-jc install --tool opencode
|
|
45
|
+
sdd-jc install --tool both --dry-run
|
|
46
|
+
sdd-jc install --tool claude --target ./.claude
|
|
47
|
+
sdd-jc update --tool both --force
|
|
48
|
+
sdd-jc doctor --tool opencode
|
|
49
|
+
sdd-jc list
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const args = {
|
|
55
|
+
command: argv[2] || "help",
|
|
56
|
+
tool: "claude",
|
|
57
|
+
target: undefined,
|
|
58
|
+
claudeTarget: TOOL_DEFAULTS.claude,
|
|
59
|
+
opencodeTarget: TOOL_DEFAULTS.opencode,
|
|
60
|
+
force: false,
|
|
61
|
+
dryRun: false,
|
|
62
|
+
commandsOnly: false,
|
|
63
|
+
skillsOnly: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (let i = 3; i < argv.length; i += 1) {
|
|
67
|
+
const arg = argv[i];
|
|
68
|
+
|
|
69
|
+
if (arg === "--tool") {
|
|
70
|
+
const value = argv[i + 1];
|
|
71
|
+
if (!value) fail("Missing value for --tool");
|
|
72
|
+
if (!["claude", "opencode", "both"].includes(value)) {
|
|
73
|
+
fail("--tool must be one of: claude, opencode, both");
|
|
74
|
+
}
|
|
75
|
+
args.tool = value;
|
|
76
|
+
i += 1;
|
|
77
|
+
} else if (arg === "--target") {
|
|
78
|
+
const value = argv[i + 1];
|
|
79
|
+
if (!value) fail("Missing value for --target");
|
|
80
|
+
args.target = resolveUserPath(value);
|
|
81
|
+
i += 1;
|
|
82
|
+
} else if (arg === "--claude-target") {
|
|
83
|
+
const value = argv[i + 1];
|
|
84
|
+
if (!value) fail("Missing value for --claude-target");
|
|
85
|
+
args.claudeTarget = resolveUserPath(value);
|
|
86
|
+
i += 1;
|
|
87
|
+
} else if (arg === "--opencode-target") {
|
|
88
|
+
const value = argv[i + 1];
|
|
89
|
+
if (!value) fail("Missing value for --opencode-target");
|
|
90
|
+
args.opencodeTarget = resolveUserPath(value);
|
|
91
|
+
i += 1;
|
|
92
|
+
} else if (arg === "--force") {
|
|
93
|
+
args.force = true;
|
|
94
|
+
} else if (arg === "--dry-run") {
|
|
95
|
+
args.dryRun = true;
|
|
96
|
+
} else if (arg === "--commands-only") {
|
|
97
|
+
args.commandsOnly = true;
|
|
98
|
+
} else if (arg === "--skills-only") {
|
|
99
|
+
args.skillsOnly = true;
|
|
100
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
101
|
+
args.command = "help";
|
|
102
|
+
} else {
|
|
103
|
+
fail(`Unknown option: ${arg}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (args.commandsOnly && args.skillsOnly) {
|
|
108
|
+
fail("Use only one of --commands-only or --skills-only");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (args.target && args.tool === "both") {
|
|
112
|
+
fail("--target can only be used with --tool claude or --tool opencode. Use --claude-target and --opencode-target with --tool both.");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (args.target && args.tool === "claude") args.claudeTarget = args.target;
|
|
116
|
+
if (args.target && args.tool === "opencode") args.opencodeTarget = args.target;
|
|
117
|
+
|
|
118
|
+
return args;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveUserPath(input) {
|
|
122
|
+
return path.resolve(input.replace(/^~(?=$|\/|\\)/, os.homedir()));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function fail(message) {
|
|
126
|
+
console.error(`ERROR: ${message}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ensureDirectory(dir, dryRun) {
|
|
131
|
+
if (fs.existsSync(dir)) return;
|
|
132
|
+
if (dryRun) {
|
|
133
|
+
console.log(`create directory ${dir}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function listEntries(dir, filter) {
|
|
140
|
+
if (!fs.existsSync(dir)) return [];
|
|
141
|
+
return fs
|
|
142
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
143
|
+
.filter(filter)
|
|
144
|
+
.map((entry) => entry.name)
|
|
145
|
+
.sort((a, b) => a.localeCompare(b));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function listCommands() {
|
|
149
|
+
return listEntries(SOURCE_COMMANDS, (entry) => entry.isFile() && entry.name.endsWith(".md"));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function listSkills() {
|
|
153
|
+
return listEntries(SOURCE_SKILLS, (entry) => entry.isDirectory());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function selectedTools(args) {
|
|
157
|
+
if (args.tool === "both") return ["claude", "opencode"];
|
|
158
|
+
return [args.tool];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function shouldInclude(type, args) {
|
|
162
|
+
if (args.commandsOnly) return type === "commands";
|
|
163
|
+
if (args.skillsOnly) return type === "skills";
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function copySingleFile(sourcePath, targetPath, args) {
|
|
168
|
+
ensureDirectory(path.dirname(targetPath), args.dryRun);
|
|
169
|
+
|
|
170
|
+
if (fs.existsSync(targetPath) && !args.force) {
|
|
171
|
+
console.log(`skip existing ${targetPath}`);
|
|
172
|
+
return { installed: 0, skipped: 1 };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const action = fs.existsSync(targetPath) ? "overwrite" : "install";
|
|
176
|
+
console.log(`${args.dryRun ? "would " : ""}${action} ${targetPath}`);
|
|
177
|
+
|
|
178
|
+
if (!args.dryRun) {
|
|
179
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { installed: 1, skipped: 0 };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function copyDirectoryContents(sourceDir, targetDir, args) {
|
|
186
|
+
ensureDirectory(targetDir, args.dryRun);
|
|
187
|
+
|
|
188
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
189
|
+
let installed = 0;
|
|
190
|
+
let skipped = 0;
|
|
191
|
+
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
194
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
195
|
+
|
|
196
|
+
if (fs.existsSync(targetPath) && !args.force) {
|
|
197
|
+
console.log(`skip existing ${targetPath}`);
|
|
198
|
+
skipped += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const action = fs.existsSync(targetPath) ? "overwrite" : "install";
|
|
203
|
+
console.log(`${args.dryRun ? "would " : ""}${action} ${targetPath}`);
|
|
204
|
+
|
|
205
|
+
if (!args.dryRun) {
|
|
206
|
+
fs.cpSync(sourcePath, targetPath, {
|
|
207
|
+
recursive: true,
|
|
208
|
+
force: true,
|
|
209
|
+
errorOnExist: false,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
installed += 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { installed, skipped };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function installTool(tool, args) {
|
|
219
|
+
const targetRoot = tool === "claude" ? args.claudeTarget : args.opencodeTarget;
|
|
220
|
+
const targetCommands = path.join(targetRoot, "commands");
|
|
221
|
+
const targetSkills = path.join(targetRoot, "skills");
|
|
222
|
+
const targetResources = path.join(targetRoot, "sdd-jc");
|
|
223
|
+
let installed = 0;
|
|
224
|
+
let skipped = 0;
|
|
225
|
+
|
|
226
|
+
console.log(`\n${tool} target: ${targetRoot}`);
|
|
227
|
+
|
|
228
|
+
if (shouldInclude("commands", args)) {
|
|
229
|
+
const result = copyDirectoryContents(SOURCE_COMMANDS, targetCommands, args);
|
|
230
|
+
installed += result.installed;
|
|
231
|
+
skipped += result.skipped;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (shouldInclude("skills", args)) {
|
|
235
|
+
const result = copyDirectoryContents(SOURCE_SKILLS, targetSkills, args);
|
|
236
|
+
installed += result.installed;
|
|
237
|
+
skipped += result.skipped;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (shouldInclude("resources", args)) {
|
|
241
|
+
const scriptsResult = copyDirectoryContents(
|
|
242
|
+
SOURCE_SCRIPTS,
|
|
243
|
+
path.join(targetResources, "scripts"),
|
|
244
|
+
args
|
|
245
|
+
);
|
|
246
|
+
installed += scriptsResult.installed;
|
|
247
|
+
skipped += scriptsResult.skipped;
|
|
248
|
+
|
|
249
|
+
const mcpResult = copySingleFile(
|
|
250
|
+
SOURCE_MCP_EXAMPLE,
|
|
251
|
+
path.join(targetResources, ".mcp.json.example"),
|
|
252
|
+
args
|
|
253
|
+
);
|
|
254
|
+
installed += mcpResult.installed;
|
|
255
|
+
skipped += mcpResult.skipped;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { installed, skipped };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function runInstall(args) {
|
|
262
|
+
let installed = 0;
|
|
263
|
+
let skipped = 0;
|
|
264
|
+
|
|
265
|
+
for (const tool of selectedTools(args)) {
|
|
266
|
+
const result = installTool(tool, args);
|
|
267
|
+
installed += result.installed;
|
|
268
|
+
skipped += result.skipped;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(`\nDone. Installed: ${installed} | Skipped: ${skipped}`);
|
|
272
|
+
if (skipped > 0 && !args.force) {
|
|
273
|
+
console.log("Use --force to overwrite existing files.");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (selectedTools(args).includes("opencode") && !args.dryRun) {
|
|
277
|
+
console.log("Restart OpenCode for installed commands and skills to be loaded.");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function runList() {
|
|
282
|
+
const commands = listCommands();
|
|
283
|
+
const skills = listSkills();
|
|
284
|
+
const scripts = listEntries(SOURCE_SCRIPTS, (entry) => entry.isFile());
|
|
285
|
+
|
|
286
|
+
console.log("Commands:");
|
|
287
|
+
commands.forEach((name) => console.log(` ${name.replace(/\.md$/, "")}`));
|
|
288
|
+
|
|
289
|
+
console.log("\nSkills:");
|
|
290
|
+
skills.forEach((name) => console.log(` ${name}`));
|
|
291
|
+
|
|
292
|
+
console.log("\nResources:");
|
|
293
|
+
scripts.forEach((name) => console.log(` scripts/${name}`));
|
|
294
|
+
console.log(" .mcp.json.example");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function hasInstalledCommand(targetRoot, name) {
|
|
298
|
+
return fs.existsSync(path.join(targetRoot, "commands", name));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function hasInstalledSkill(targetRoot, name) {
|
|
302
|
+
return fs.existsSync(path.join(targetRoot, "skills", name, "SKILL.md"));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function doctorTool(tool, args) {
|
|
306
|
+
const targetRoot = tool === "claude" ? args.claudeTarget : args.opencodeTarget;
|
|
307
|
+
const targetResources = path.join(targetRoot, "sdd-jc");
|
|
308
|
+
let missing = 0;
|
|
309
|
+
|
|
310
|
+
console.log(`\nChecking ${tool}: ${targetRoot}`);
|
|
311
|
+
|
|
312
|
+
if (shouldInclude("commands", args)) {
|
|
313
|
+
console.log("\nCommands:");
|
|
314
|
+
for (const command of listCommands()) {
|
|
315
|
+
const ok = hasInstalledCommand(targetRoot, command);
|
|
316
|
+
console.log(` ${ok ? "OK" : "MISSING"} ${command}`);
|
|
317
|
+
if (!ok) missing += 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (shouldInclude("skills", args)) {
|
|
322
|
+
console.log("\nSkills:");
|
|
323
|
+
for (const skill of listSkills()) {
|
|
324
|
+
const ok = hasInstalledSkill(targetRoot, skill);
|
|
325
|
+
console.log(` ${ok ? "OK" : "MISSING"} ${skill}`);
|
|
326
|
+
if (!ok) missing += 1;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (shouldInclude("resources", args)) {
|
|
331
|
+
console.log("\nResources:");
|
|
332
|
+
const resourceChecks = [
|
|
333
|
+
path.join(targetResources, "scripts", "gsc_verify.py"),
|
|
334
|
+
path.join(targetResources, ".mcp.json.example"),
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
for (const resourcePath of resourceChecks) {
|
|
338
|
+
const ok = fs.existsSync(resourcePath);
|
|
339
|
+
console.log(` ${ok ? "OK" : "MISSING"} ${resourcePath}`);
|
|
340
|
+
if (!ok) missing += 1;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return missing;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function runDoctor(args) {
|
|
348
|
+
let missing = 0;
|
|
349
|
+
for (const tool of selectedTools(args)) {
|
|
350
|
+
missing += doctorTool(tool, args);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(`\nDone. Missing: ${missing}`);
|
|
354
|
+
if (missing > 0) process.exitCode = 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function main() {
|
|
358
|
+
const args = parseArgs(process.argv);
|
|
359
|
+
|
|
360
|
+
switch (args.command) {
|
|
361
|
+
case "install":
|
|
362
|
+
case "update":
|
|
363
|
+
runInstall(args);
|
|
364
|
+
break;
|
|
365
|
+
case "doctor":
|
|
366
|
+
runDoctor(args);
|
|
367
|
+
break;
|
|
368
|
+
case "list":
|
|
369
|
+
runList();
|
|
370
|
+
break;
|
|
371
|
+
case "help":
|
|
372
|
+
printHelp();
|
|
373
|
+
break;
|
|
374
|
+
default:
|
|
375
|
+
fail(`Unknown command: ${args.command}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sdd-jc-methodology",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Portable SDD JC methodology commands and skills for AI-assisted development.",
|
|
5
|
+
"homepage": "https://github.com/JuankCadavid/sdd-jc-methodology#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/JuankCadavid/sdd-jc-methodology.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/JuankCadavid/sdd-jc-methodology/issues"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"sdd-jc": "./bin/sdd-jc.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
".claude/commands",
|
|
18
|
+
".claude/skills",
|
|
19
|
+
".mcp.json.example",
|
|
20
|
+
"assets",
|
|
21
|
+
"bin",
|
|
22
|
+
"CHANGELOG.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"README.md",
|
|
25
|
+
"scripts"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
29
|
+
"verify:cli": "node bin/sdd-jc.js list"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"sdd",
|
|
33
|
+
"spec-driven-development",
|
|
34
|
+
"claude",
|
|
35
|
+
"opencode",
|
|
36
|
+
"ai-coding",
|
|
37
|
+
"methodology"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Verify a Google service account as an owner on Google Search Console
|
|
4
|
+
Domain properties via DNS TXT records.
|
|
5
|
+
|
|
6
|
+
The `gsc` MCP (@mikusnuz/gsc-mcp) does NOT expose Site Verification
|
|
7
|
+
endpoints — only Search Console. This helper covers the verification
|
|
8
|
+
half: token generation + verify call.
|
|
9
|
+
|
|
10
|
+
After verification succeeds, use the `gsc` MCP `sites_add` tool to add
|
|
11
|
+
the property to Search Console for the service account.
|
|
12
|
+
|
|
13
|
+
Setup:
|
|
14
|
+
pip install google-auth google-api-python-client
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
GSC_KEY_PATH=/abs/path/to/service-account.json \
|
|
18
|
+
python3 gsc_verify.py get <domain> [<domain> ...]
|
|
19
|
+
|
|
20
|
+
GSC_KEY_PATH=/abs/path/to/service-account.json \
|
|
21
|
+
python3 gsc_verify.py verify <domain> [<domain> ...]
|
|
22
|
+
|
|
23
|
+
Or pass the key path explicitly:
|
|
24
|
+
python3 gsc_verify.py --key /abs/path/to/key.json get example.com
|
|
25
|
+
|
|
26
|
+
Workflow:
|
|
27
|
+
1. Run with `get` to obtain a DNS TXT verification token per domain.
|
|
28
|
+
2. Add the TXT record to each domain's DNS (host: @, value: the token).
|
|
29
|
+
3. Wait for DNS propagation (a few minutes to an hour).
|
|
30
|
+
4. Run with `verify` to trigger the verification on Google's side.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import os
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from google.oauth2 import service_account
|
|
41
|
+
from googleapiclient.discovery import build
|
|
42
|
+
from googleapiclient.errors import HttpError
|
|
43
|
+
except ImportError:
|
|
44
|
+
print("ERROR: missing dependencies. Install with:", file=sys.stderr)
|
|
45
|
+
print(" pip install google-auth google-api-python-client", file=sys.stderr)
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
SCOPES = ["https://www.googleapis.com/auth/siteverification"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_client(key_path: str):
|
|
53
|
+
if not Path(key_path).exists():
|
|
54
|
+
print(f"ERROR: key file not found at: {key_path}", file=sys.stderr)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
creds = service_account.Credentials.from_service_account_file(
|
|
57
|
+
key_path, scopes=SCOPES
|
|
58
|
+
)
|
|
59
|
+
return build("siteVerification", "v1", credentials=creds, cache_discovery=False)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_token(sv, domain: str) -> str:
|
|
63
|
+
body = {
|
|
64
|
+
"site": {"type": "INET_DOMAIN", "identifier": domain},
|
|
65
|
+
"verificationMethod": "DNS_TXT",
|
|
66
|
+
}
|
|
67
|
+
return sv.webResource().getToken(body=body).execute()["token"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_dns(domain: str, token: str):
|
|
71
|
+
"""Best-effort check that the TXT record is visible via dig."""
|
|
72
|
+
try:
|
|
73
|
+
out = subprocess.check_output(
|
|
74
|
+
["dig", "+short", "TXT", domain], stderr=subprocess.DEVNULL, text=True
|
|
75
|
+
)
|
|
76
|
+
return token in out
|
|
77
|
+
except Exception:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def verify_domain(sv, domain: str):
|
|
82
|
+
body = {"site": {"type": "INET_DOMAIN", "identifier": domain}}
|
|
83
|
+
return sv.webResource().insert(verificationMethod="DNS_TXT", body=body).execute()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmd_get(sv, domains):
|
|
87
|
+
print("\n=== DNS TXT records to add ===\n")
|
|
88
|
+
print("For each domain, log into your DNS provider and add a TXT record")
|
|
89
|
+
print("at the root (host = '@' or blank) with the value shown.\n")
|
|
90
|
+
for d in domains:
|
|
91
|
+
try:
|
|
92
|
+
token = get_token(sv, d)
|
|
93
|
+
print(f"--- {d} ---")
|
|
94
|
+
print(f" Type: TXT")
|
|
95
|
+
print(f" Host: @ (root of {d})")
|
|
96
|
+
print(f" Value: {token}\n")
|
|
97
|
+
except HttpError as e:
|
|
98
|
+
print(f"--- {d} ---")
|
|
99
|
+
print(f" ERROR getting token: {e}\n")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cmd_verify(sv, domains):
|
|
103
|
+
print("\n=== Verifying domains ===\n")
|
|
104
|
+
successes, failures = [], []
|
|
105
|
+
for d in domains:
|
|
106
|
+
try:
|
|
107
|
+
token = get_token(sv, d)
|
|
108
|
+
except HttpError as e:
|
|
109
|
+
print(f"[{d}] ERROR fetching token: {e}")
|
|
110
|
+
failures.append(d)
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
dns_ok = check_dns(d, token)
|
|
114
|
+
if dns_ok is False:
|
|
115
|
+
print(f"[{d}] WARNING: TXT record not visible yet via dig. Trying anyway...")
|
|
116
|
+
elif dns_ok is True:
|
|
117
|
+
print(f"[{d}] TXT record visible in DNS")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
verify_domain(sv, d)
|
|
121
|
+
print(f"[{d}] VERIFIED — service account is now an owner.")
|
|
122
|
+
successes.append(d)
|
|
123
|
+
except HttpError as e:
|
|
124
|
+
print(f"[{d}] FAILED to verify: {e}")
|
|
125
|
+
failures.append(d)
|
|
126
|
+
print()
|
|
127
|
+
|
|
128
|
+
print(f"\nDone. Success: {len(successes)} | Failed: {len(failures)}")
|
|
129
|
+
if failures:
|
|
130
|
+
print("Failed domains:", ", ".join(failures))
|
|
131
|
+
print("Tip: DNS can take a few minutes to propagate. Try again later.")
|
|
132
|
+
return 0 if not failures else 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main():
|
|
136
|
+
parser = argparse.ArgumentParser(
|
|
137
|
+
description="Verify a service account as owner on GSC domain properties."
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--key",
|
|
141
|
+
default=os.environ.get("GSC_KEY_PATH"),
|
|
142
|
+
help="Path to the Google service account JSON key. "
|
|
143
|
+
"Defaults to $GSC_KEY_PATH.",
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument("action", choices=["get", "verify"])
|
|
146
|
+
parser.add_argument("domains", nargs="+", help="One or more apex domains.")
|
|
147
|
+
args = parser.parse_args()
|
|
148
|
+
|
|
149
|
+
if not args.key:
|
|
150
|
+
parser.error(
|
|
151
|
+
"missing service account key: pass --key /abs/path.json or set $GSC_KEY_PATH"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
sv = make_client(args.key)
|
|
155
|
+
if args.action == "get":
|
|
156
|
+
cmd_get(sv, args.domains)
|
|
157
|
+
return 0
|
|
158
|
+
return cmd_verify(sv, args.domains)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
sys.exit(main())
|