@ngocsangairvds/vsaf 4.0.9 → 4.0.10
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/package.json +1 -1
- package/skills/vds-skill/_shared/config-check.js +76 -0
- package/skills/vds-skill/_shared/credentials.js +139 -0
- package/skills/vds-skill/create-bitbucket-pr/SKILL.md +2 -18
- package/skills/vds-skill/create-bitbucket-pr/scripts/create-pr.js +125 -0
- package/skills/vds-skill/create-jira-epic/SKILL.md +2 -20
- package/skills/vds-skill/create-jira-epic/scripts/create-epic.js +120 -0
- package/skills/vds-skill/install-deps.mjs +34 -32
- package/skills/vds-skill/pull/SKILL.md +1 -1
- package/skills/vds-skill/pull/scripts/pull.js +59 -0
- package/skills/vds-skill/push-prd/SKILL.md +11 -30
- package/skills/vds-skill/push-srs/SKILL.md +4 -23
- package/skills/vds-skill/search-confluence/SKILL.md +2 -22
- package/skills/vds-skill/search-confluence/scripts/search.js +114 -0
- package/skills/vds-skill/vds-scripts-skill/SKILL.md +3 -3
- package/skills/vds-skill/_shared/credentials.sh +0 -79
- package/skills/vds-skill/create-bitbucket-pr/scripts/create-pr.sh +0 -105
- package/skills/vds-skill/create-jira-epic/scripts/create-epic.sh +0 -113
- package/skills/vds-skill/pull/scripts/pull.sh +0 -52
- package/skills/vds-skill/search-confluence/scripts/search.sh +0 -128
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* config-check.js — reusable pre-flight checker for vds-skill-* SKILL.md files.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node config-check.js --cmd vds-cli --env VDS_CONFLUENCE_TOKEN,VDS_CONFLUENCE_SPACE_DEFAULT
|
|
9
|
+
*
|
|
10
|
+
* Output (matches legacy bash format):
|
|
11
|
+
* OK — all checks pass
|
|
12
|
+
* BLOCKED — missing: vds-cli VDS_JIRA_TOKEN — something missing
|
|
13
|
+
* NOTE: /path/to/cmd exists but fails to run — broken shim detected
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { join } = require('path');
|
|
17
|
+
const { loadCredentials, requireCommand } = require('./credentials.js');
|
|
18
|
+
|
|
19
|
+
// ── Parse args ──
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const cmds = [];
|
|
23
|
+
const envVars = [];
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i] === '--cmd' && args[i + 1]) {
|
|
27
|
+
cmds.push(args[++i]);
|
|
28
|
+
} else if (args[i] === '--env' && args[i + 1]) {
|
|
29
|
+
envVars.push(...args[++i].split(',').map(s => s.trim()).filter(Boolean));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Load credentials ──
|
|
34
|
+
|
|
35
|
+
loadCredentials();
|
|
36
|
+
|
|
37
|
+
// Prepend .claude/bin to PATH so vds-cli wrapper is found
|
|
38
|
+
const localBin = join('.claude', 'bin');
|
|
39
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
40
|
+
process.env.PATH = `${localBin}${sep}${process.env.PATH}`;
|
|
41
|
+
|
|
42
|
+
// ── Check commands ──
|
|
43
|
+
|
|
44
|
+
const missing = [];
|
|
45
|
+
const notes = [];
|
|
46
|
+
|
|
47
|
+
for (const cmd of cmds) {
|
|
48
|
+
const result = requireCommand(cmd);
|
|
49
|
+
if (!result.found) {
|
|
50
|
+
missing.push(cmd);
|
|
51
|
+
if (result.broken && result.path) {
|
|
52
|
+
notes.push(`NOTE: ${result.path} exists but fails to run (broken shim or missing venv)`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Check env vars ──
|
|
58
|
+
|
|
59
|
+
for (const varName of envVars) {
|
|
60
|
+
if (!process.env[varName]) {
|
|
61
|
+
missing.push(varName);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Output ──
|
|
66
|
+
|
|
67
|
+
for (const note of notes) {
|
|
68
|
+
console.log(note);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (missing.length === 0) {
|
|
72
|
+
console.log('OK');
|
|
73
|
+
} else {
|
|
74
|
+
console.log(`BLOCKED — missing: ${missing.join(' ')}`);
|
|
75
|
+
console.log('Fix: edit ~/.vds/sdlc-config.env (or run: vsaf install vds-skill)');
|
|
76
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, unlinkSync } = require('fs');
|
|
5
|
+
const { join, dirname } = require('path');
|
|
6
|
+
const { homedir } = require('os');
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const IS_WIN = process.platform === 'win32';
|
|
10
|
+
|
|
11
|
+
function getConfigPath() {
|
|
12
|
+
return process.env.VSAF_CONFIG_FILE || join(homedir(), '.vds', 'sdlc-config.env');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseEnvFile(filePath) {
|
|
16
|
+
if (!existsSync(filePath)) return {};
|
|
17
|
+
const vars = {};
|
|
18
|
+
for (const line of readFileSync(filePath, 'utf-8').split('\n')) {
|
|
19
|
+
const trimmed = line.trim();
|
|
20
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
21
|
+
const eq = trimmed.indexOf('=');
|
|
22
|
+
if (eq <= 0) continue;
|
|
23
|
+
const key = trimmed.slice(0, eq);
|
|
24
|
+
let val = trimmed.slice(eq + 1);
|
|
25
|
+
if ((val.startsWith("'") && val.endsWith("'")) ||
|
|
26
|
+
(val.startsWith('"') && val.endsWith('"'))) {
|
|
27
|
+
val = val.slice(1, -1);
|
|
28
|
+
}
|
|
29
|
+
if (val) vars[key] = val;
|
|
30
|
+
}
|
|
31
|
+
return vars;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadCredentials() {
|
|
35
|
+
const configPath = getConfigPath();
|
|
36
|
+
const vdsEnvPath = join(homedir(), '.vds', '.env');
|
|
37
|
+
|
|
38
|
+
const primary = parseEnvFile(configPath);
|
|
39
|
+
for (const [key, val] of Object.entries(primary)) {
|
|
40
|
+
if (!process.env[key]) process.env[key] = val;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fallback = parseEnvFile(vdsEnvPath);
|
|
44
|
+
for (const [key, val] of Object.entries(fallback)) {
|
|
45
|
+
if (!process.env[key]) process.env[key] = val;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function persistToConfig(varName, value) {
|
|
50
|
+
const configPath = getConfigPath();
|
|
51
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
52
|
+
|
|
53
|
+
let content = '';
|
|
54
|
+
if (existsSync(configPath)) {
|
|
55
|
+
content = readFileSync(configPath, 'utf-8')
|
|
56
|
+
.split('\n')
|
|
57
|
+
.filter(line => !line.startsWith(`${varName}=`))
|
|
58
|
+
.join('\n');
|
|
59
|
+
if (content && !content.endsWith('\n')) content += '\n';
|
|
60
|
+
}
|
|
61
|
+
content += `${varName}=${value}\n`;
|
|
62
|
+
|
|
63
|
+
const tmp = configPath + '.tmp';
|
|
64
|
+
writeFileSync(tmp, content);
|
|
65
|
+
if (!IS_WIN) {
|
|
66
|
+
try { chmodSync(tmp, 0o600); } catch { /* warn but don't fail */ }
|
|
67
|
+
}
|
|
68
|
+
renameSync(tmp, configPath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function ensureEnv(varName, prompt, isSecret = true) {
|
|
72
|
+
if (process.env[varName]) return true;
|
|
73
|
+
|
|
74
|
+
if (!process.stdin.isTTY) return false;
|
|
75
|
+
|
|
76
|
+
const readline = require('readline');
|
|
77
|
+
const rl = readline.createInterface({
|
|
78
|
+
input: process.stdin,
|
|
79
|
+
output: process.stderr,
|
|
80
|
+
terminal: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const value = await new Promise((resolve) => {
|
|
84
|
+
if (isSecret) {
|
|
85
|
+
process.stderr.write(`${prompt}: `);
|
|
86
|
+
const origWrite = process.stdout.write;
|
|
87
|
+
process.stdout.write = () => true;
|
|
88
|
+
rl.question('', (answer) => {
|
|
89
|
+
process.stdout.write = origWrite;
|
|
90
|
+
process.stderr.write('\n');
|
|
91
|
+
rl.close();
|
|
92
|
+
resolve(answer.trim());
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
rl.question(`${prompt}: `, (answer) => {
|
|
96
|
+
rl.close();
|
|
97
|
+
resolve(answer.trim());
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!value) {
|
|
103
|
+
process.stderr.write(`ERROR: ${varName} is required\n`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
process.env[varName] = value;
|
|
108
|
+
persistToConfig(varName, value);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function requireCommand(name) {
|
|
113
|
+
const localBin = join('.claude', 'bin', name);
|
|
114
|
+
if (existsSync(localBin)) {
|
|
115
|
+
try {
|
|
116
|
+
execFileSync(localBin, ['--version'], { stdio: 'pipe', timeout: 15000 });
|
|
117
|
+
return { found: true, path: localBin };
|
|
118
|
+
} catch {
|
|
119
|
+
return { found: false, path: localBin, broken: true };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const whichCmd = IS_WIN ? 'where' : 'which';
|
|
124
|
+
let cmdPath;
|
|
125
|
+
try {
|
|
126
|
+
cmdPath = execFileSync(whichCmd, [name], { stdio: 'pipe', encoding: 'utf-8' }).trim().split('\n')[0];
|
|
127
|
+
} catch {
|
|
128
|
+
return { found: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
execFileSync(name, ['--version'], { stdio: 'pipe', timeout: 15000 });
|
|
133
|
+
return { found: true, path: cmdPath };
|
|
134
|
+
} catch {
|
|
135
|
+
return { found: false, path: cmdPath, broken: true };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { loadCredentials, ensureEnv, requireCommand, getConfigPath, parseEnvFile, persistToConfig };
|
|
@@ -12,23 +12,7 @@ Create a Bitbucket PR on Viettel internal Bitbucket via `vds-cli`.
|
|
|
12
12
|
Before doing anything, run this check via Bash tool:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
16
|
-
MISSING=""
|
|
17
|
-
[[ -x .claude/bin/vds-cli ]] && export PATH=".claude/bin:$PATH"
|
|
18
|
-
if ! vds-cli --version >/dev/null 2>&1; then
|
|
19
|
-
MISSING="$MISSING vds-cli"
|
|
20
|
-
VDS_PATH=$(command -v vds-cli 2>/dev/null)
|
|
21
|
-
if [[ -n "$VDS_PATH" ]]; then
|
|
22
|
-
echo "NOTE: $VDS_PATH exists but fails to run (broken shim or missing venv)"
|
|
23
|
-
fi
|
|
24
|
-
fi
|
|
25
|
-
[[ -z "${VDS_BITBUCKET_TOKEN:-}" ]] && MISSING="$MISSING VDS_BITBUCKET_TOKEN"
|
|
26
|
-
if [[ -n "$MISSING" ]]; then
|
|
27
|
-
echo "BLOCKED — missing:$MISSING"
|
|
28
|
-
echo "Fix: edit ~/.vds/sdlc-config.env (or run: vsaf install vds-skill)"
|
|
29
|
-
else
|
|
30
|
-
echo "OK"
|
|
31
|
-
fi
|
|
15
|
+
node .claude/skills/_shared/vds-skill/config-check.js --cmd vds-cli --env VDS_BITBUCKET_TOKEN
|
|
32
16
|
```
|
|
33
17
|
|
|
34
18
|
If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do NOT fabricate output. `--dry-run` mode skips credential + vds-cli checks.
|
|
@@ -63,7 +47,7 @@ If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do N
|
|
|
63
47
|
## Implementation
|
|
64
48
|
|
|
65
49
|
```bash
|
|
66
|
-
|
|
50
|
+
node .claude/skills/vds-skill-create-bitbucket-pr/scripts/create-pr.js "$@"
|
|
67
51
|
```
|
|
68
52
|
|
|
69
53
|
## Notes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
const { existsSync, readdirSync } = require('fs');
|
|
6
|
+
const { join } = require('path');
|
|
7
|
+
// NOTE: path matches DEPLOYED location (.claude/skills/_shared/vds-skill/), not source repo
|
|
8
|
+
const { loadCredentials, ensureEnv, requireCommand } = require('../../_shared/vds-skill/credentials.js');
|
|
9
|
+
|
|
10
|
+
let dryRun = false;
|
|
11
|
+
let targetBranch = 'master';
|
|
12
|
+
let descFile = '';
|
|
13
|
+
let title = '';
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
switch (args[i]) {
|
|
18
|
+
case '--dry-run': dryRun = true; break;
|
|
19
|
+
case '--target': targetBranch = args[++i]; break;
|
|
20
|
+
case '--description-file': descFile = args[++i]; break;
|
|
21
|
+
case '--title': title = args[++i]; break;
|
|
22
|
+
default:
|
|
23
|
+
process.stderr.write(`Unknown arg: ${args[i]}\n`);
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let remoteUrl;
|
|
29
|
+
try {
|
|
30
|
+
remoteUrl = execFileSync('git', ['remote', 'get-url', 'origin'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
31
|
+
} catch {
|
|
32
|
+
process.stderr.write('ERROR: No git remote \'origin\' configured\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!remoteUrl.includes('bitbucket.digital.vn')) {
|
|
37
|
+
process.stderr.write(`WARNING: Remote URL does not match bitbucket.digital.vn pattern:\n ${remoteUrl}\n`);
|
|
38
|
+
process.stderr.write(' This skill is for Viettel Bitbucket. For GitHub, use \'gh pr create\'.\n');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const scmMatch = remoteUrl.match(/\/scm\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
43
|
+
const sshMatch = remoteUrl.match(/\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
44
|
+
const match = scmMatch || sshMatch;
|
|
45
|
+
|
|
46
|
+
if (!match) {
|
|
47
|
+
process.stderr.write(`ERROR: Cannot parse PROJECT/REPO from remote URL: ${remoteUrl}\n`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const project = match[1];
|
|
52
|
+
const repo = match[2];
|
|
53
|
+
|
|
54
|
+
let sourceBranch;
|
|
55
|
+
try {
|
|
56
|
+
sourceBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
57
|
+
} catch {
|
|
58
|
+
process.stderr.write('ERROR: Cannot determine current branch\n');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!sourceBranch || sourceBranch === 'HEAD') {
|
|
63
|
+
process.stderr.write('ERROR: Not on a named branch\n');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!title) {
|
|
68
|
+
try {
|
|
69
|
+
title = execFileSync('git', ['log', '-1', '--pretty=%s'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
70
|
+
} catch { title = sourceBranch; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!descFile) {
|
|
74
|
+
const searchPaths = [
|
|
75
|
+
{ dir: join('.vsaf', 'docs', 'features'), file: '09-ship.md' },
|
|
76
|
+
{ dir: join('.vsaf', 'docs', 'hotfixes'), file: '03-ship.md' },
|
|
77
|
+
];
|
|
78
|
+
for (const { dir, file } of searchPaths) {
|
|
79
|
+
if (existsSync(dir)) {
|
|
80
|
+
for (const subdir of readdirSync(dir)) {
|
|
81
|
+
const candidate = join(dir, subdir, file);
|
|
82
|
+
if (existsSync(candidate)) { descFile = candidate; break; }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (descFile) break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const cmdArgs = ['bitbucket', 'pr', 'create', `${project}/${repo}`,
|
|
90
|
+
'--source', sourceBranch, '--target', targetBranch, '--title', title];
|
|
91
|
+
if (descFile && existsSync(descFile)) cmdArgs.push('--description-file', descFile);
|
|
92
|
+
|
|
93
|
+
if (dryRun) {
|
|
94
|
+
console.log('DRY-RUN — would execute:');
|
|
95
|
+
console.log(` vds-cli ${cmdArgs.join(' ')} --yes --json-only`);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
loadCredentials();
|
|
100
|
+
|
|
101
|
+
const cmdCheck = requireCommand('vds-cli');
|
|
102
|
+
if (!cmdCheck.found) {
|
|
103
|
+
process.stderr.write('ERROR: vds-cli not found in PATH\n');
|
|
104
|
+
if (cmdCheck.broken) process.stderr.write(`NOTE: ${cmdCheck.path} exists but fails to run\n`);
|
|
105
|
+
process.exit(127);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
(async () => {
|
|
109
|
+
if (!await ensureEnv('VDS_BITBUCKET_TOKEN', 'Enter VDS Bitbucket personal access token')) process.exit(1);
|
|
110
|
+
|
|
111
|
+
console.log('About to create PR:');
|
|
112
|
+
console.log(` Project/Repo: ${project}/${repo}`);
|
|
113
|
+
console.log(` Source: ${sourceBranch}`);
|
|
114
|
+
console.log(` Target: ${targetBranch}`);
|
|
115
|
+
console.log(` Title: ${title}`);
|
|
116
|
+
console.log(` Description: ${descFile || '<empty>'}`);
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
execFileSync('vds-cli', [...cmdArgs, '--yes', '--json-only'], { stdio: 'inherit' });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
process.stderr.write(`ERROR: vds-cli failed: ${err.message}\n`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
@@ -12,24 +12,7 @@ Create a Jira Epic on Viettel Jira based on a PRD markdown file.
|
|
|
12
12
|
Before doing anything, run this check via Bash tool:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
16
|
-
MISSING=""
|
|
17
|
-
[[ -x .claude/bin/vds-cli ]] && export PATH=".claude/bin:$PATH"
|
|
18
|
-
if ! vds-cli --version >/dev/null 2>&1; then
|
|
19
|
-
MISSING="$MISSING vds-cli"
|
|
20
|
-
VDS_PATH=$(command -v vds-cli 2>/dev/null)
|
|
21
|
-
if [[ -n "$VDS_PATH" ]]; then
|
|
22
|
-
echo "NOTE: $VDS_PATH exists but fails to run (broken shim or missing venv)"
|
|
23
|
-
fi
|
|
24
|
-
fi
|
|
25
|
-
[[ -z "${VDS_JIRA_TOKEN:-}" ]] && MISSING="$MISSING VDS_JIRA_TOKEN"
|
|
26
|
-
[[ -z "${VDS_JIRA_PROJECT_DEFAULT:-}" ]] && MISSING="$MISSING VDS_JIRA_PROJECT_DEFAULT"
|
|
27
|
-
if [[ -n "$MISSING" ]]; then
|
|
28
|
-
echo "BLOCKED — missing:$MISSING"
|
|
29
|
-
echo "Fix: edit ~/.vds/sdlc-config.env (or run: vsaf install vds-skill)"
|
|
30
|
-
else
|
|
31
|
-
echo "OK"
|
|
32
|
-
fi
|
|
15
|
+
node .claude/skills/_shared/vds-skill/config-check.js --cmd vds-cli --env VDS_JIRA_TOKEN,VDS_JIRA_PROJECT_DEFAULT
|
|
33
16
|
```
|
|
34
17
|
|
|
35
18
|
If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do NOT fabricate output. `--dry-run` mode skips credential + vds-cli checks.
|
|
@@ -37,7 +20,6 @@ If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do N
|
|
|
37
20
|
## Prerequisites
|
|
38
21
|
|
|
39
22
|
- `vds-cli` installed (for non-dry-run)
|
|
40
|
-
- `python3` installed
|
|
41
23
|
- A PRD file exists at `.vsaf/docs/features/{feature}/02-prd.md`
|
|
42
24
|
- `VDS_JIRA_TOKEN` + `VDS_JIRA_PROJECT_DEFAULT` in `~/.vds/sdlc-config.env`
|
|
43
25
|
|
|
@@ -63,7 +45,7 @@ If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do N
|
|
|
63
45
|
## Implementation
|
|
64
46
|
|
|
65
47
|
```bash
|
|
66
|
-
|
|
48
|
+
node .claude/skills/vds-skill-create-jira-epic/scripts/create-epic.js "$@"
|
|
67
49
|
```
|
|
68
50
|
|
|
69
51
|
## Notes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
const { existsSync, readFileSync, writeFileSync, readdirSync, statSync } = require('fs');
|
|
6
|
+
const { join, dirname, basename } = require('path');
|
|
7
|
+
// NOTE: path matches DEPLOYED location (.claude/skills/_shared/vds-skill/), not source repo
|
|
8
|
+
const { loadCredentials, ensureEnv, requireCommand } = require('../../_shared/vds-skill/credentials.js');
|
|
9
|
+
|
|
10
|
+
let dryRun = false;
|
|
11
|
+
let projectKey = '';
|
|
12
|
+
let descFile = '';
|
|
13
|
+
let featureName = '';
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
switch (args[i]) {
|
|
18
|
+
case '--dry-run': dryRun = true; break;
|
|
19
|
+
case '--project': projectKey = args[++i]; break;
|
|
20
|
+
case '--description-file': descFile = args[++i]; break;
|
|
21
|
+
case '--feature': featureName = args[++i]; break;
|
|
22
|
+
default:
|
|
23
|
+
process.stderr.write(`Unknown arg: ${args[i]}\n`);
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!descFile) {
|
|
29
|
+
if (featureName) {
|
|
30
|
+
const candidate = join('.vsaf', 'docs', 'features', featureName, '02-prd.md');
|
|
31
|
+
if (existsSync(candidate)) descFile = candidate;
|
|
32
|
+
}
|
|
33
|
+
if (!descFile) {
|
|
34
|
+
const featuresDir = join('.vsaf', 'docs', 'features');
|
|
35
|
+
if (existsSync(featuresDir)) {
|
|
36
|
+
let newest = null;
|
|
37
|
+
let newestMtime = 0;
|
|
38
|
+
for (const dir of readdirSync(featuresDir)) {
|
|
39
|
+
const prd = join(featuresDir, dir, '02-prd.md');
|
|
40
|
+
if (existsSync(prd)) {
|
|
41
|
+
const mtime = statSync(prd).mtimeMs;
|
|
42
|
+
if (mtime > newestMtime) { newest = prd; newestMtime = mtime; }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (newest) descFile = newest;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!descFile || !existsSync(descFile)) {
|
|
51
|
+
process.stderr.write('ERROR: PRD file not found. Run /sdlc-prd first or specify --description-file\n');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!featureName) featureName = basename(dirname(descFile));
|
|
56
|
+
|
|
57
|
+
const prdContent = readFileSync(descFile, 'utf-8');
|
|
58
|
+
const h1Match = prdContent.match(/^# (.+)/m);
|
|
59
|
+
const summary = h1Match ? h1Match[1].trim() : featureName;
|
|
60
|
+
|
|
61
|
+
if (dryRun) {
|
|
62
|
+
loadCredentials();
|
|
63
|
+
const displayProject = projectKey || process.env.VDS_JIRA_PROJECT_DEFAULT || '<VDS_JIRA_PROJECT_DEFAULT>';
|
|
64
|
+
console.log('DRY-RUN — would execute:');
|
|
65
|
+
console.log(` vds-cli jira create --project ${displayProject} --issuetype Epic --summary "${summary}" --description-file ${descFile} --yes --json-only`);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
loadCredentials();
|
|
70
|
+
|
|
71
|
+
const cmdCheck = requireCommand('vds-cli');
|
|
72
|
+
if (!cmdCheck.found) {
|
|
73
|
+
process.stderr.write('ERROR: vds-cli not found in PATH\n');
|
|
74
|
+
if (cmdCheck.broken) process.stderr.write(`NOTE: ${cmdCheck.path} exists but fails to run\n`);
|
|
75
|
+
process.exit(127);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
(async () => {
|
|
79
|
+
if (!await ensureEnv('VDS_JIRA_TOKEN', 'Enter VDS Jira personal access token')) process.exit(1);
|
|
80
|
+
if (!await ensureEnv('VDS_JIRA_PROJECT_DEFAULT', 'Enter default Jira project key (e.g. NTTC)', false)) process.exit(1);
|
|
81
|
+
|
|
82
|
+
projectKey = projectKey || process.env.VDS_JIRA_PROJECT_DEFAULT;
|
|
83
|
+
|
|
84
|
+
console.log('About to create Jira Epic:');
|
|
85
|
+
console.log(` Project: ${projectKey}`);
|
|
86
|
+
console.log(` Summary: ${summary}`);
|
|
87
|
+
console.log(` Description: ${descFile}`);
|
|
88
|
+
console.log(` Feature: ${featureName}`);
|
|
89
|
+
console.log('');
|
|
90
|
+
|
|
91
|
+
const cmdArgs = ['jira', 'create', '--project', projectKey, '--issuetype', 'Epic',
|
|
92
|
+
'--summary', summary, '--description-file', descFile, '--yes', '--json-only'];
|
|
93
|
+
|
|
94
|
+
let result;
|
|
95
|
+
try {
|
|
96
|
+
result = execFileSync('vds-cli', cmdArgs, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
process.stderr.write(`ERROR: vds-cli failed: ${err.stderr || err.message}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let epicKey;
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(result);
|
|
105
|
+
epicKey = parsed.key || '';
|
|
106
|
+
} catch {
|
|
107
|
+
process.stderr.write(`ERROR: Failed to parse vds-cli response\nResponse: ${result}\n`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!epicKey) {
|
|
112
|
+
process.stderr.write(`ERROR: No epic key in response\nResponse: ${result}\n`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const epicKeyFile = join(dirname(descFile), 'jira-epic-key.txt');
|
|
117
|
+
writeFileSync(epicKeyFile, epicKey + '\n');
|
|
118
|
+
console.log(`Created Jira Epic: ${epicKey}`);
|
|
119
|
+
console.log(` Saved key to: ${epicKeyFile}`);
|
|
120
|
+
})();
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* node install-deps.mjs [projectPath]
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, chmodSync } from 'fs';
|
|
14
14
|
import { join, dirname } from 'path';
|
|
15
15
|
import { homedir } from 'os';
|
|
16
16
|
|
|
@@ -77,7 +77,7 @@ if (!existsSync(CONFIG_FILE)) {
|
|
|
77
77
|
const lines = [
|
|
78
78
|
'# VDS Skill Pack — credential config',
|
|
79
79
|
'# File: ~/.vds/sdlc-config.env',
|
|
80
|
-
'# Permissions: 600 (auto-set by credentials.
|
|
80
|
+
'# Permissions: 600 (auto-set by credentials.js)',
|
|
81
81
|
'#',
|
|
82
82
|
'# Fill in the values below. Skills will use these automatically.',
|
|
83
83
|
'# To rotate a token: update the value here, skills pick it up next run.',
|
|
@@ -91,7 +91,10 @@ if (!existsSync(CONFIG_FILE)) {
|
|
|
91
91
|
lines.push('');
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
writeFileSync(CONFIG_FILE, lines.join('\n')
|
|
94
|
+
writeFileSync(CONFIG_FILE, lines.join('\n'));
|
|
95
|
+
if (process.platform !== 'win32') {
|
|
96
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
97
|
+
}
|
|
95
98
|
log('✅', `Config created: ${CONFIG_FILE}`);
|
|
96
99
|
} else if (missing.length > 0) {
|
|
97
100
|
// Append missing vars to existing config
|
|
@@ -105,7 +108,10 @@ if (!existsSync(CONFIG_FILE)) {
|
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
if (appendLines.length > 1) {
|
|
108
|
-
writeFileSync(CONFIG_FILE, readFileSync(CONFIG_FILE, 'utf-8') + appendLines.join('\n')
|
|
111
|
+
writeFileSync(CONFIG_FILE, readFileSync(CONFIG_FILE, 'utf-8') + appendLines.join('\n'));
|
|
112
|
+
if (process.platform !== 'win32') {
|
|
113
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
114
|
+
}
|
|
109
115
|
log('✅', `Config updated: ${CONFIG_FILE}`);
|
|
110
116
|
}
|
|
111
117
|
} else {
|
|
@@ -184,17 +190,24 @@ try {
|
|
|
184
190
|
if (!vdsCliFound && vdsScriptsDir) {
|
|
185
191
|
const wrapperDir = join(projectPath, '.claude', 'bin');
|
|
186
192
|
const wrapperPath = join(wrapperDir, 'vds-cli');
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (existsSync(uvRunner)) {
|
|
193
|
+
// Check vds-scripts has valid structure (pyproject.toml = uv project)
|
|
194
|
+
if (existsSync(join(vdsScriptsDir, 'pyproject.toml'))) {
|
|
190
195
|
mkdirSync(wrapperDir, { recursive: true });
|
|
191
|
-
const wrapperContent = `#!/usr/bin/env
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
const wrapperContent = `#!/usr/bin/env node
|
|
197
|
+
// Project-local vds-cli wrapper — created by vsaf install vds-skill
|
|
198
|
+
const { execFileSync } = require('child_process');
|
|
199
|
+
try {
|
|
200
|
+
execFileSync('uv', ['run', '--directory', ${JSON.stringify(vdsScriptsDir)}, '--package', 'vds-cli', 'vds-cli', ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
201
|
+
} catch (e) {
|
|
202
|
+
process.exit(e.status || 1);
|
|
203
|
+
}
|
|
196
204
|
`;
|
|
197
|
-
writeFileSync(wrapperPath, wrapperContent
|
|
205
|
+
writeFileSync(wrapperPath, wrapperContent);
|
|
206
|
+
if (process.platform !== 'win32') {
|
|
207
|
+
chmodSync(wrapperPath, 0o755);
|
|
208
|
+
} else {
|
|
209
|
+
writeFileSync(wrapperPath + '.cmd', `@node "%~dp0\\vds-cli" %*\r\n`);
|
|
210
|
+
}
|
|
198
211
|
log('✅', `vds-cli wrapper created: ${wrapperPath}`);
|
|
199
212
|
log(' ', `Points to: ${vdsScriptsDir}`);
|
|
200
213
|
log('💡', 'Add to PATH: export PATH=".claude/bin:$PATH"');
|
|
@@ -206,39 +219,27 @@ if (!vdsCliFound) {
|
|
|
206
219
|
log('⚠️', 'vds-cli not found — required for non-dry-run execution');
|
|
207
220
|
log(' ', 'Option 1: Clone vds-scripts into .claude/vds-scripts/ then re-run install');
|
|
208
221
|
log(' ', 'Option 2: Install vds-cli globally (pip install / Viettel internal)');
|
|
209
|
-
log(' ', 'Verify:
|
|
222
|
+
log(' ', 'Verify: vds-cli --version');
|
|
210
223
|
}
|
|
211
224
|
|
|
212
225
|
// ── Step 5: Sync all vds-scripts packages (ensures subcommand binaries exist) ──
|
|
213
226
|
|
|
214
227
|
if (vdsScriptsDir) {
|
|
215
228
|
log('📂', `vds-scripts: ${vdsScriptsDir}`);
|
|
216
|
-
|
|
217
|
-
if (existsSync(uvRunner)) {
|
|
229
|
+
if (existsSync(join(vdsScriptsDir, 'pyproject.toml'))) {
|
|
218
230
|
console.log('');
|
|
219
231
|
log('🔄', 'Syncing vds-scripts packages (uv sync --all-packages)...');
|
|
220
232
|
try {
|
|
221
|
-
execSync(
|
|
233
|
+
execSync('uv sync --all-packages', {
|
|
222
234
|
stdio: 'pipe',
|
|
223
235
|
encoding: 'utf-8',
|
|
224
236
|
timeout: 120000,
|
|
225
237
|
cwd: vdsScriptsDir,
|
|
226
238
|
});
|
|
227
239
|
log('✅', 'All vds-scripts packages synced');
|
|
228
|
-
} catch
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
execSync('uv sync --all-packages', {
|
|
232
|
-
stdio: 'pipe',
|
|
233
|
-
encoding: 'utf-8',
|
|
234
|
-
timeout: 120000,
|
|
235
|
-
cwd: vdsScriptsDir,
|
|
236
|
-
});
|
|
237
|
-
log('✅', 'All vds-scripts packages synced (direct uv)');
|
|
238
|
-
} catch {
|
|
239
|
-
log('⚠️', 'Failed to sync vds-scripts packages — subcommands like confluence/jira may not work');
|
|
240
|
-
log(' ', `Fix: cd ${vdsScriptsDir} && uv sync --all-packages`);
|
|
241
|
-
}
|
|
240
|
+
} catch {
|
|
241
|
+
log('⚠️', 'Failed to sync vds-scripts packages — subcommands like confluence/jira may not work');
|
|
242
|
+
log(' ', `Fix: cd ${vdsScriptsDir} && uv sync --all-packages`);
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
} else {
|
|
@@ -268,7 +269,8 @@ try {
|
|
|
268
269
|
} catch {
|
|
269
270
|
// Check if a shim exists but is broken
|
|
270
271
|
try {
|
|
271
|
-
const
|
|
272
|
+
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
273
|
+
const shimPath = execSync(`${whichCmd} vds-cli`, { stdio: 'pipe', encoding: 'utf-8' }).trim().split('\n')[0];
|
|
272
274
|
log('❌', `vds-cli BROKEN — ${shimPath} exists but fails to run`);
|
|
273
275
|
log(' ', 'The shim likely points to a deleted venv or missing vds-scripts directory.');
|
|
274
276
|
log(' ', `Fix: rm ${shimPath} then re-run: vsaf install vds-skill`);
|