@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngocsangairvds/vsaf",
3
- "version": "4.0.9",
3
+ "version": "4.0.10",
4
4
  "description": "is it ready to use ?",
5
5
  "main": "packages/core/dist/index.js",
6
6
  "types": "packages/core/dist/index.d.ts",
@@ -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
- source .claude/skills/_shared/vds-skill/credentials.sh 2>/dev/null
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
- bash .claude/skills/vds-skill-create-bitbucket-pr/scripts/create-pr.sh "$@"
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
- source .claude/skills/_shared/vds-skill/credentials.sh 2>/dev/null
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
- bash .claude/skills/vds-skill-create-jira-epic/scripts/create-epic.sh "$@"
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.sh)',
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'), { mode: 0o600 });
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'), { mode: 0o600 });
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
- const uvRunner = join(vdsScriptsDir, 'scripts', 'worktree_uv.sh');
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 bash
192
- # Project-local vds-cli wrapper — created by vsaf install vds-skill
193
- # Delegates to worktree_uv.sh in the detected vds-scripts directory.
194
- set -euo pipefail
195
- exec "${uvRunner}" run --directory "${vdsScriptsDir}" --package vds-cli vds-cli "$@"
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, { mode: 0o755 });
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: command -v vds-cli && vds-cli --version');
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
- const uvRunner = join(vdsScriptsDir, 'scripts', 'worktree_uv.sh');
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(`"${uvRunner}" sync --directory "${vdsScriptsDir}" --all-packages`, {
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 (e) {
229
- // Fallback: try direct uv sync
230
- try {
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 shimPath = execSync('command -v vds-cli', { stdio: 'pipe', encoding: 'utf-8' }).trim();
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`);