@ner076/prd-toolkit 0.1.4
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 +164 -0
- package/bin/prd-toolkit.js +74 -0
- package/package.json +51 -0
- package/skills/prd-discovery/SKILL.md +915 -0
- package/skills/prd-generator/SKILL.md +917 -0
- package/src/adapters/claude-code.js +80 -0
- package/src/adapters/opencode.js +93 -0
- package/src/detect.js +86 -0
- package/src/doctor.js +33 -0
- package/src/help.js +41 -0
- package/src/index.js +31 -0
- package/src/install.js +82 -0
- package/src/list.js +31 -0
- package/src/uninstall.js +55 -0
- package/src/version.js +13 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code adapter.
|
|
3
|
+
*
|
|
4
|
+
* Installs skills to:
|
|
5
|
+
* - user scope: ~/.claude/skills/<skill-name>/SKILL.md
|
|
6
|
+
* - project scope: ./.claude/skills/<skill-name>/SKILL.md
|
|
7
|
+
*
|
|
8
|
+
* Claude Code expects a SKILL.md file with YAML frontmatter containing
|
|
9
|
+
* at minimum `name` and `description`. Our source SKILL.md files already
|
|
10
|
+
* conform, so this adapter mostly copies content and writes a manifest.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { getPackageVersion } from '../version.js';
|
|
17
|
+
|
|
18
|
+
function resolveBasePath(scope) {
|
|
19
|
+
if (scope === 'user') return join(homedir(), '.claude', 'skills');
|
|
20
|
+
return join(process.cwd(), '.claude', 'skills');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateFrontmatter(content, skillName) {
|
|
24
|
+
// Normalize line endings: CRLF โ LF (Windows git checkout may produce CRLF)
|
|
25
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
26
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---/);
|
|
27
|
+
if (!match) {
|
|
28
|
+
throw new Error(`Skill "${skillName}" is missing YAML frontmatter โ required by Claude Code`);
|
|
29
|
+
}
|
|
30
|
+
const body = match[1];
|
|
31
|
+
if (!/^name:\s*\S+/m.test(body)) throw new Error(`Skill "${skillName}" frontmatter missing 'name'`);
|
|
32
|
+
if (!/^description:\s*\S+/m.test(body)) throw new Error(`Skill "${skillName}" frontmatter missing 'description'`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function installClaudeCode({ skills, opts }) {
|
|
36
|
+
const basePath = resolveBasePath(opts.scope);
|
|
37
|
+
const manifest = {
|
|
38
|
+
tool: '@ner076/prd-toolkit',
|
|
39
|
+
version: await getPackageVersion(),
|
|
40
|
+
target: 'claude-code',
|
|
41
|
+
scope: opts.scope,
|
|
42
|
+
installedAt: new Date().toISOString(),
|
|
43
|
+
skills: []
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!opts.dryRun) mkdirSync(basePath, { recursive: true });
|
|
47
|
+
|
|
48
|
+
for (const skill of skills) {
|
|
49
|
+
validateFrontmatter(skill.content, skill.name);
|
|
50
|
+
const targetDir = join(basePath, skill.name);
|
|
51
|
+
const targetFile = join(targetDir, 'SKILL.md');
|
|
52
|
+
|
|
53
|
+
if (!opts.dryRun) {
|
|
54
|
+
if (existsSync(targetFile) && !opts.force) {
|
|
55
|
+
const existing = readFileSync(targetFile, 'utf8');
|
|
56
|
+
if (existing === skill.content) {
|
|
57
|
+
manifest.skills.push({ name: skill.name, path: targetFile, status: 'unchanged' });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Overwrite policy: for non-force, keep existing; log warning
|
|
61
|
+
console.warn(` ! ${skill.name} exists at ${targetFile} โ skipping (use --force to overwrite)`);
|
|
62
|
+
manifest.skills.push({ name: skill.name, path: targetFile, status: 'skipped' });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
mkdirSync(targetDir, { recursive: true });
|
|
66
|
+
writeFileSync(targetFile, skill.content, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
manifest.skills.push({ name: skill.name, path: targetFile, status: 'installed' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!opts.dryRun) {
|
|
72
|
+
writeFileSync(
|
|
73
|
+
join(basePath, '.prd-toolkit.manifest.json'),
|
|
74
|
+
JSON.stringify(manifest, null, 2),
|
|
75
|
+
'utf8'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { installPath: basePath, manifest };
|
|
80
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opencode adapter.
|
|
3
|
+
*
|
|
4
|
+
* Installs skills as custom commands to:
|
|
5
|
+
* - user scope: ~/.config/opencode/command/<name>.md
|
|
6
|
+
* - project scope: ./.opencode/command/<name>.md
|
|
7
|
+
*
|
|
8
|
+
* Opencode custom commands use YAML frontmatter with a `description` field
|
|
9
|
+
* (and optionally `agent`, `model`). We transform our source SKILL.md by:
|
|
10
|
+
* 1. Extracting the source frontmatter
|
|
11
|
+
* 2. Emitting Opencode-flavored frontmatter
|
|
12
|
+
* 3. Preserving the entire body verbatim
|
|
13
|
+
*
|
|
14
|
+
* The result: user can invoke via `/prd-discovery` or `/prd-generator` inside Opencode.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { getPackageVersion } from '../version.js';
|
|
21
|
+
|
|
22
|
+
function resolveBasePath(scope) {
|
|
23
|
+
if (scope === 'user') return join(homedir(), '.config', 'opencode', 'command');
|
|
24
|
+
return join(process.cwd(), '.opencode', 'command');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseSourceFrontmatter(content) {
|
|
28
|
+
// Normalize line endings: CRLF โ LF (Windows git checkout may produce CRLF)
|
|
29
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
30
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
31
|
+
if (!match) throw new Error('Source SKILL.md is missing frontmatter');
|
|
32
|
+
const meta = {};
|
|
33
|
+
match[1].split('\n').forEach((line) => {
|
|
34
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
35
|
+
if (kv) meta[kv[1]] = kv[2].trim();
|
|
36
|
+
});
|
|
37
|
+
return { meta, body: match[2] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toOpencodeFrontmatter(meta) {
|
|
41
|
+
// Opencode commands accept: description, agent, model, subtask
|
|
42
|
+
const lines = ['---'];
|
|
43
|
+
if (meta.description) lines.push(`description: ${meta.description}`);
|
|
44
|
+
lines.push(`agent: general`); // reasonable default
|
|
45
|
+
lines.push(`subtask: true`); // multi-turn workflows run best as subtask
|
|
46
|
+
lines.push('---');
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function installOpencode({ skills, opts }) {
|
|
51
|
+
const basePath = resolveBasePath(opts.scope);
|
|
52
|
+
const manifest = {
|
|
53
|
+
tool: '@ner076/prd-toolkit',
|
|
54
|
+
version: await getPackageVersion(),
|
|
55
|
+
target: 'opencode',
|
|
56
|
+
scope: opts.scope,
|
|
57
|
+
installedAt: new Date().toISOString(),
|
|
58
|
+
skills: []
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!opts.dryRun) mkdirSync(basePath, { recursive: true });
|
|
62
|
+
|
|
63
|
+
for (const skill of skills) {
|
|
64
|
+
const { meta, body } = parseSourceFrontmatter(skill.content);
|
|
65
|
+
const transformed = `${toOpencodeFrontmatter(meta)}\n\n${body}`;
|
|
66
|
+
const targetFile = join(basePath, `${skill.name}.md`);
|
|
67
|
+
|
|
68
|
+
if (!opts.dryRun) {
|
|
69
|
+
if (existsSync(targetFile) && !opts.force) {
|
|
70
|
+
const existing = readFileSync(targetFile, 'utf8');
|
|
71
|
+
if (existing === transformed) {
|
|
72
|
+
manifest.skills.push({ name: skill.name, path: targetFile, status: 'unchanged' });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
console.warn(` ! ${skill.name} exists at ${targetFile} โ skipping (use --force to overwrite)`);
|
|
76
|
+
manifest.skills.push({ name: skill.name, path: targetFile, status: 'skipped' });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
writeFileSync(targetFile, transformed, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
manifest.skills.push({ name: skill.name, path: targetFile, status: 'installed' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!opts.dryRun) {
|
|
85
|
+
writeFileSync(
|
|
86
|
+
join(basePath, '.prd-toolkit.manifest.json'),
|
|
87
|
+
JSON.stringify(manifest, null, 2),
|
|
88
|
+
'utf8'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { installPath: basePath, manifest };
|
|
93
|
+
}
|
package/src/detect.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-detect which target environment (Claude Code / Opencode) is available.
|
|
3
|
+
*
|
|
4
|
+
* Detection signals (any = present):
|
|
5
|
+
* Claude Code:
|
|
6
|
+
* - ~/.claude/ exists
|
|
7
|
+
* - `claude` executable in PATH
|
|
8
|
+
* - CLAUDE_CONFIG env var set
|
|
9
|
+
* Opencode:
|
|
10
|
+
* - ~/.config/opencode/ exists
|
|
11
|
+
* - `opencode` executable in PATH
|
|
12
|
+
* - .opencode/ in CWD
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { execSync } from 'node:child_process';
|
|
19
|
+
|
|
20
|
+
function hasBinary(name) {
|
|
21
|
+
try {
|
|
22
|
+
execSync(process.platform === 'win32' ? `where ${name}` : `which ${name}`, {
|
|
23
|
+
stdio: 'ignore'
|
|
24
|
+
});
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function detectClaudeCode() {
|
|
32
|
+
const signals = {
|
|
33
|
+
homeDir: existsSync(join(homedir(), '.claude')),
|
|
34
|
+
binary: hasBinary('claude'),
|
|
35
|
+
envVar: !!process.env.CLAUDE_CONFIG
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
detected: Object.values(signals).some(Boolean),
|
|
39
|
+
signals
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function detectOpencode() {
|
|
44
|
+
const signals = {
|
|
45
|
+
configDir: existsSync(join(homedir(), '.config', 'opencode')),
|
|
46
|
+
binary: hasBinary('opencode'),
|
|
47
|
+
projectDir: existsSync(join(process.cwd(), '.opencode'))
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
detected: Object.values(signals).some(Boolean),
|
|
51
|
+
signals
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function detectAll() {
|
|
56
|
+
return {
|
|
57
|
+
'claude-code': detectClaudeCode(),
|
|
58
|
+
opencode: detectOpencode()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve target arg into concrete list of adapters to run.
|
|
64
|
+
* - 'auto' โ whichever is detected (both if both)
|
|
65
|
+
* - explicit target โ just that one
|
|
66
|
+
* - 'all' โ both regardless
|
|
67
|
+
*/
|
|
68
|
+
export function resolveTargets(targetArg) {
|
|
69
|
+
if (targetArg === 'all') return ['claude-code', 'opencode'];
|
|
70
|
+
if (targetArg === 'claude-code' || targetArg === 'opencode') return [targetArg];
|
|
71
|
+
|
|
72
|
+
// auto
|
|
73
|
+
const detected = detectAll();
|
|
74
|
+
const active = Object.entries(detected)
|
|
75
|
+
.filter(([, v]) => v.detected)
|
|
76
|
+
.map(([k]) => k);
|
|
77
|
+
|
|
78
|
+
if (active.length === 0) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'No supported environment detected.\n' +
|
|
81
|
+
'Install Claude Code (https://claude.ai/code) or Opencode (https://opencode.ai),\n' +
|
|
82
|
+
'or pass --target=claude-code|opencode|all explicitly.'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return active;
|
|
86
|
+
}
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor: diagnose environment + install health.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { detectAll } from './detect.js';
|
|
6
|
+
import { getPackageVersion } from './version.js';
|
|
7
|
+
|
|
8
|
+
export async function doctor() {
|
|
9
|
+
const version = await getPackageVersion();
|
|
10
|
+
const detected = detectAll();
|
|
11
|
+
|
|
12
|
+
console.log('\n๐ฉบ @ner076/prd-toolkit โ doctor\n');
|
|
13
|
+
console.log(` Toolkit version: ${version}`);
|
|
14
|
+
console.log(` Node version: ${process.version}`);
|
|
15
|
+
console.log(` Platform: ${process.platform}\n`);
|
|
16
|
+
|
|
17
|
+
for (const [target, res] of Object.entries(detected)) {
|
|
18
|
+
const status = res.detected ? 'โ detected' : 'โ not detected';
|
|
19
|
+
console.log(` ${target}: ${status}`);
|
|
20
|
+
for (const [sig, val] of Object.entries(res.signals)) {
|
|
21
|
+
console.log(` - ${sig}: ${val}`);
|
|
22
|
+
}
|
|
23
|
+
console.log('');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const anyDetected = Object.values(detected).some((v) => v.detected);
|
|
27
|
+
if (!anyDetected) {
|
|
28
|
+
console.log(' โ No target environment detected.');
|
|
29
|
+
console.log(' Install Claude Code (https://claude.ai/code) or Opencode (https://opencode.ai) first.\n');
|
|
30
|
+
} else {
|
|
31
|
+
console.log(' Ready to install. Run: npx @ner076/prd-toolkit install\n');
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/help.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getPackageVersion } from './version.js';
|
|
2
|
+
|
|
3
|
+
export function printHelp() {
|
|
4
|
+
console.log(`
|
|
5
|
+
๐ฆ @ner076/prd-toolkit โ AI-powered PRD toolkit
|
|
6
|
+
Guided discovery + auto-dev ready PRD generation for Claude Code & Opencode
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
npx @ner076/prd-toolkit <command> [flags]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
install Install skills to target environment(s)
|
|
13
|
+
uninstall Remove installed skills (tracked via manifest)
|
|
14
|
+
update Re-install to pick up latest version (implies --force)
|
|
15
|
+
list Show installed skills across all targets
|
|
16
|
+
doctor Diagnose environment & install health
|
|
17
|
+
help Show this help
|
|
18
|
+
|
|
19
|
+
Flags:
|
|
20
|
+
--target=<t> claude-code | opencode | all | auto (default: auto)
|
|
21
|
+
--scope=<s> user | project (default: project)
|
|
22
|
+
-y, --yes Skip confirmation prompts
|
|
23
|
+
-f, --force Overwrite existing files
|
|
24
|
+
--dryRun Show what would happen without writing files
|
|
25
|
+
-v, --version Print version
|
|
26
|
+
-h, --help Print this help
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
npx @ner076/prd-toolkit install # auto-detect, project scope
|
|
30
|
+
npx @ner076/prd-toolkit install --target=all --scope=user
|
|
31
|
+
npx @ner076/prd-toolkit install --target=opencode -y
|
|
32
|
+
npx @ner076/prd-toolkit uninstall --target=claude-code
|
|
33
|
+
npx @ner076/prd-toolkit doctor
|
|
34
|
+
|
|
35
|
+
Learn more: https://github.com/ner076/prd-toolkit
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function printVersion() {
|
|
40
|
+
console.log(await getPackageVersion());
|
|
41
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ner076/prd-toolkit โ programmatic API
|
|
3
|
+
*
|
|
4
|
+
* Use this when you want to script the toolkit instead of the CLI:
|
|
5
|
+
*
|
|
6
|
+
* import { install, doctor, detectAll } from '@ner076/prd-toolkit';
|
|
7
|
+
* await doctor();
|
|
8
|
+
* await install({ target: 'claude-code', scope: 'project', yes: true });
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// โโ Install / uninstall / list โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
12
|
+
|
|
13
|
+
export { install } from './install.js';
|
|
14
|
+
export { uninstall } from './uninstall.js';
|
|
15
|
+
export { list } from './list.js';
|
|
16
|
+
|
|
17
|
+
// โโ Doctor โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
18
|
+
|
|
19
|
+
export { doctor } from './doctor.js';
|
|
20
|
+
|
|
21
|
+
// โโ Detection helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
22
|
+
|
|
23
|
+
export { detectClaudeCode, detectOpencode, detectAll, resolveTargets } from './detect.js';
|
|
24
|
+
|
|
25
|
+
// โโ Display helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
26
|
+
|
|
27
|
+
export { printHelp, printVersion } from './help.js';
|
|
28
|
+
|
|
29
|
+
// โโ Version โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
30
|
+
|
|
31
|
+
export { getPackageVersion } from './version.js';
|
package/src/install.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install orchestrator.
|
|
3
|
+
* Resolves targets, calls appropriate adapters, writes manifests.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { resolveTargets } from './detect.js';
|
|
10
|
+
import { installClaudeCode } from './adapters/claude-code.js';
|
|
11
|
+
import { installOpencode } from './adapters/opencode.js';
|
|
12
|
+
import { confirm } from '@inquirer/prompts';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const SKILLS_ROOT = join(__dirname, '..', 'skills');
|
|
16
|
+
|
|
17
|
+
const ADAPTERS = {
|
|
18
|
+
'claude-code': installClaudeCode,
|
|
19
|
+
'opencode': installOpencode
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function loadSkills() {
|
|
23
|
+
const names = readdirSync(SKILLS_ROOT, { withFileTypes: true })
|
|
24
|
+
.filter((d) => d.isDirectory())
|
|
25
|
+
.map((d) => d.name);
|
|
26
|
+
|
|
27
|
+
return names.map((name) => ({
|
|
28
|
+
name,
|
|
29
|
+
sourcePath: join(SKILLS_ROOT, name, 'SKILL.md'),
|
|
30
|
+
content: readFileSync(join(SKILLS_ROOT, name, 'SKILL.md'), 'utf8')
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function install(opts) {
|
|
35
|
+
const targets = resolveTargets(opts.target);
|
|
36
|
+
const skills = loadSkills();
|
|
37
|
+
|
|
38
|
+
console.log(`\n๐ฆ @ner076/prd-toolkit โ install`);
|
|
39
|
+
console.log(` Targets: ${targets.join(', ')}`);
|
|
40
|
+
console.log(` Scope: ${opts.scope}`);
|
|
41
|
+
console.log(` Skills: ${skills.map((s) => s.name).join(', ')}\n`);
|
|
42
|
+
|
|
43
|
+
if (opts.dryRun) {
|
|
44
|
+
console.log(' (dry run โ no files will be written)\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!opts.yes && !opts.dryRun) {
|
|
48
|
+
const ok = await confirm({
|
|
49
|
+
message: 'Proceed with install?',
|
|
50
|
+
default: true
|
|
51
|
+
});
|
|
52
|
+
if (!ok) {
|
|
53
|
+
console.log('Cancelled.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const results = [];
|
|
59
|
+
for (const target of targets) {
|
|
60
|
+
const adapter = ADAPTERS[target];
|
|
61
|
+
if (!adapter) {
|
|
62
|
+
console.warn(` โ No adapter for target: ${target}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const result = await adapter({ skills, opts });
|
|
67
|
+
results.push({ target, ok: true, ...result });
|
|
68
|
+
console.log(` โ ${target} โ ${result.installPath}`);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
results.push({ target, ok: false, error: err.message });
|
|
71
|
+
console.error(` โ ${target} โ ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('\nDone.\n');
|
|
76
|
+
const successCount = results.filter((r) => r.ok).length;
|
|
77
|
+
if (successCount === 0) throw new Error('All installs failed.');
|
|
78
|
+
|
|
79
|
+
console.log('Try it out:');
|
|
80
|
+
console.log(' โข In Claude Code: run `claude` in your project, then ask "run prd-discovery"');
|
|
81
|
+
console.log(' โข In Opencode: run `opencode`, then use `/prd-discovery` command\n');
|
|
82
|
+
}
|
package/src/list.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List installed skills across all known targets & scopes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const LOCATIONS = [
|
|
10
|
+
{ target: 'claude-code', scope: 'user', path: join(homedir(), '.claude', 'skills') },
|
|
11
|
+
{ target: 'claude-code', scope: 'project', path: join(process.cwd(), '.claude', 'skills') },
|
|
12
|
+
{ target: 'opencode', scope: 'user', path: join(homedir(), '.config', 'opencode', 'command') },
|
|
13
|
+
{ target: 'opencode', scope: 'project', path: join(process.cwd(), '.opencode', 'command') }
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export async function list() {
|
|
17
|
+
console.log('\n๐ฆ @ner076/prd-toolkit โ installed skills\n');
|
|
18
|
+
let found = 0;
|
|
19
|
+
for (const loc of LOCATIONS) {
|
|
20
|
+
const manifestPath = join(loc.path, '.prd-toolkit.manifest.json');
|
|
21
|
+
if (!existsSync(manifestPath)) continue;
|
|
22
|
+
const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
23
|
+
console.log(` ${loc.target} ยท ${loc.scope} ยท v${m.version} ยท ${new Date(m.installedAt).toLocaleString()}`);
|
|
24
|
+
for (const s of m.skills) {
|
|
25
|
+
console.log(` - ${s.name} (${s.status})`);
|
|
26
|
+
}
|
|
27
|
+
console.log('');
|
|
28
|
+
found++;
|
|
29
|
+
}
|
|
30
|
+
if (found === 0) console.log(' (nothing installed yet โ run `install`)\n');
|
|
31
|
+
}
|
package/src/uninstall.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uninstall: read manifest per target, remove tracked files, keep everything else.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, rmSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { confirm } from '@inquirer/prompts';
|
|
9
|
+
import { resolveTargets } from './detect.js';
|
|
10
|
+
|
|
11
|
+
const PATHS = {
|
|
12
|
+
'claude-code': (scope) =>
|
|
13
|
+
scope === 'user' ? join(homedir(), '.claude', 'skills') : join(process.cwd(), '.claude', 'skills'),
|
|
14
|
+
'opencode': (scope) =>
|
|
15
|
+
scope === 'user' ? join(homedir(), '.config', 'opencode', 'command') : join(process.cwd(), '.opencode', 'command')
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function uninstall(opts) {
|
|
19
|
+
const targets = resolveTargets(opts.target);
|
|
20
|
+
|
|
21
|
+
console.log(`\n๐ฆ @ner076/prd-toolkit โ uninstall`);
|
|
22
|
+
console.log(` Targets: ${targets.join(', ')}`);
|
|
23
|
+
console.log(` Scope: ${opts.scope}\n`);
|
|
24
|
+
|
|
25
|
+
if (!opts.yes) {
|
|
26
|
+
const ok = await confirm({ message: 'Proceed with uninstall?', default: false });
|
|
27
|
+
if (!ok) return console.log('Cancelled.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const target of targets) {
|
|
31
|
+
const base = PATHS[target](opts.scope);
|
|
32
|
+
const manifestPath = join(base, '.prd-toolkit.manifest.json');
|
|
33
|
+
|
|
34
|
+
if (!existsSync(manifestPath)) {
|
|
35
|
+
console.warn(` โ ${target}: no manifest at ${base} โ nothing to remove`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
40
|
+
for (const skill of manifest.skills) {
|
|
41
|
+
if (!existsSync(skill.path)) continue;
|
|
42
|
+
rmSync(skill.path, { force: true });
|
|
43
|
+
|
|
44
|
+
// Claude Code puts each skill in its own dir; clean up if empty
|
|
45
|
+
const parentDir = dirname(skill.path);
|
|
46
|
+
if (parentDir !== base && existsSync(parentDir) && readdirSync(parentDir).length === 0) {
|
|
47
|
+
rmSync(parentDir, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
rmSync(manifestPath, { force: true });
|
|
51
|
+
console.log(` โ ${target} โ removed ${manifest.skills.length} skill(s)`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('\nDone.\n');
|
|
55
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
let cached;
|
|
8
|
+
export async function getPackageVersion() {
|
|
9
|
+
if (cached) return cached;
|
|
10
|
+
const pkg = JSON.parse(await readFile(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
11
|
+
cached = pkg.version;
|
|
12
|
+
return cached;
|
|
13
|
+
}
|