@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.
@@ -27,7 +27,7 @@ Wrapper for `git pull` that detects stale local knowledge index after pull and s
27
27
  Invoke the bundled script:
28
28
 
29
29
  ```bash
30
- bash .claude/skills/vds-skill-pull/scripts/pull.sh "$@"
30
+ node .claude/skills/vds-skill-pull/scripts/pull.js "$@"
31
31
  ```
32
32
 
33
33
  ## Notes
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * vds-skill-pull: git pull + detect stale local knowledge index.
6
+ * Usage: node pull.js [...git pull args]
7
+ * Exit: forwards git pull exit code. Advisory stale warnings never block.
8
+ */
9
+
10
+ const { execSync } = require('child_process');
11
+ const { existsSync, statSync } = require('fs');
12
+
13
+ const args = process.argv.slice(2);
14
+
15
+ // Forward git pull with all args, inherit I/O
16
+ let exitCode = 0;
17
+ try {
18
+ execSync(`git pull ${args.map(a => JSON.stringify(a)).join(' ')}`, { stdio: 'inherit' });
19
+ } catch (err) {
20
+ exitCode = err.status || 1;
21
+ process.exit(exitCode);
22
+ }
23
+
24
+ // Skip stale check if FETCH_HEAD doesn't exist
25
+ if (!existsSync('.git/FETCH_HEAD')) {
26
+ process.exit(0);
27
+ }
28
+
29
+ const fetchMtime = statSync('.git/FETCH_HEAD').mtimeMs;
30
+ const staleReasons = [];
31
+
32
+ // Check .gitnexus/
33
+ if (existsSync('.gitnexus')) {
34
+ const gnMtime = statSync('.gitnexus').mtimeMs;
35
+ if (gnMtime < fetchMtime) {
36
+ staleReasons.push('.gitnexus/ index older than fetched commits');
37
+ }
38
+ }
39
+
40
+ // Check graphify-out/graph.json
41
+ if (existsSync('graphify-out/graph.json')) {
42
+ const gMtime = statSync('graphify-out/graph.json').mtimeMs;
43
+ if (gMtime < fetchMtime) {
44
+ staleReasons.push('graphify-out/graph.json older than fetched commits');
45
+ }
46
+ }
47
+
48
+ if (staleReasons.length > 0) {
49
+ console.log('');
50
+ console.log(' Knowledge index may be stale after pull:');
51
+ for (const reason of staleReasons) {
52
+ console.log(` - ${reason}`);
53
+ }
54
+ console.log('');
55
+ console.log(' Recommend: run /sdlc-onboard-code to rebuild GitNexus + Graphify locally.');
56
+ console.log('');
57
+ }
58
+
59
+ process.exit(0);
@@ -17,32 +17,14 @@ Publish the current PRD to Viettel Confluence via `vds-cli` — create a new pag
17
17
  Before doing anything, run this check via Bash tool:
18
18
 
19
19
  ```bash
20
- source .claude/skills/_shared/vds-skill/credentials.sh 2>/dev/null
21
- MISSING=""
22
- [[ -x .claude/bin/vds-cli ]] && export PATH=".claude/bin:$PATH"
23
- if ! vds-cli --version >/dev/null 2>&1; then
24
- MISSING="$MISSING vds-cli"
25
- # Show why: broken shim vs truly missing
26
- VDS_PATH=$(command -v vds-cli 2>/dev/null)
27
- if [[ -n "$VDS_PATH" ]]; then
28
- echo "NOTE: $VDS_PATH exists but fails to run (broken shim or missing venv)"
29
- fi
30
- fi
31
- [[ -z "${VDS_CONFLUENCE_TOKEN:-}" ]] && MISSING="$MISSING VDS_CONFLUENCE_TOKEN"
32
- [[ -z "${VDS_CONFLUENCE_SPACE_DEFAULT:-}" ]] && MISSING="$MISSING VDS_CONFLUENCE_SPACE_DEFAULT"
33
- if [[ -n "$MISSING" ]]; then
34
- echo "BLOCKED — missing:$MISSING"
35
- echo "Fix: edit ~/.vds/sdlc-config.env (or run: vsaf install vds-skill)"
36
- else
37
- echo "OK"
38
- fi
20
+ node .claude/skills/_shared/vds-skill/config-check.js --cmd vds-cli --env VDS_CONFLUENCE_TOKEN,VDS_CONFLUENCE_SPACE_DEFAULT
39
21
  ```
40
22
 
41
23
  If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do NOT fabricate output.
42
24
 
43
25
  ## Prerequisites
44
26
 
45
- - `vds-cli` installed (`command -v vds-cli`)
27
+ - `vds-cli` installed
46
28
  - A PRD markdown file must exist
47
29
  - `VDS_CONFLUENCE_TOKEN` + `VDS_CONFLUENCE_SPACE_DEFAULT` in `~/.vds/sdlc-config.env`
48
30
  - Confluence parent page configured in `.vsaf/config.yaml` (or lazy-prompted)
@@ -63,7 +45,7 @@ Read `.vsaf/config.yaml` (or `.vsaf/_bmad/bmm/config.yaml`) and extract:
63
45
  - `confluence_space_key`
64
46
  - `confluence_parent_page`
65
47
 
66
- If missing: lazy-prompt + persist via `_shared/credentials.sh ensure_env`.
48
+ If missing: lazy-prompt + persist via `_shared/credentials.js ensureEnv`.
67
49
 
68
50
  ### Step 3 — Determine page title
69
51
 
@@ -150,9 +132,6 @@ Save converted XHTML to a temp file: `BODY_FILE=$(mktemp --suffix=.html)`.
150
132
  Use Bash tool to run:
151
133
 
152
134
  ```bash
153
- source .claude/skills/_shared/vds-skill/credentials.sh
154
- ensure_env VDS_CONFLUENCE_TOKEN "Enter VDS Confluence personal access token"
155
-
156
135
  SPACE_KEY="<from config>"
157
136
  PAGE_TITLE="<title from Step 3>"
158
137
 
@@ -160,8 +139,9 @@ SEARCH_RESULT=$(vds-cli confluence search \
160
139
  --cql "space=$SPACE_KEY AND title = \"$PAGE_TITLE\"" \
161
140
  --limit 1 --json-only)
162
141
 
163
- EXISTING_PAGE_ID=$(echo "$SEARCH_RESULT" | python3 -c \
164
- 'import json,sys; d=json.load(sys.stdin); print(d["results"][0]["id"] if d.get("results") else "")')
142
+ EXISTING_PAGE_ID=$(node -e \
143
+ "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); process.stdout.write(d.results?.[0]?.id ?? '');" \
144
+ <<< "$SEARCH_RESULT")
165
145
  ```
166
146
 
167
147
  If `$EXISTING_PAGE_ID` non-empty → Step 6 (update). Otherwise → Step 7 (create).
@@ -169,8 +149,9 @@ If `$EXISTING_PAGE_ID` non-empty → Step 6 (update). Otherwise → Step 7 (crea
169
149
  ### Step 6 — Update existing page (via vds-cli)
170
150
 
171
151
  ```bash
172
- EXISTING_VERSION=$(echo "$SEARCH_RESULT" | python3 -c \
173
- 'import json,sys; d=json.load(sys.stdin); print(d["results"][0].get("version",{}).get("number",1))')
152
+ EXISTING_VERSION=$(node -e \
153
+ "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); process.stdout.write(String(d.results?.[0]?.version?.number ?? 1));" \
154
+ <<< "$SEARCH_RESULT")
174
155
 
175
156
  VERSION_COMMENT="${COMMENT:-Updated via vds-skill-push-prd}"
176
157
 
@@ -188,7 +169,7 @@ rm -f "$BODY_FILE"
188
169
 
189
170
  ```bash
190
171
  LATEST_VERSION=$(vds-cli confluence content page "$EXISTING_PAGE_ID" --json-only \
191
- | python3 -c 'import json,sys; print(json.load(sys.stdin)["version"]["number"])')
172
+ | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); process.stdout.write(String(d.version.number));")
192
173
 
193
174
  vds-cli confluence content update \
194
175
  --page-id "$EXISTING_PAGE_ID" \
@@ -207,7 +188,7 @@ if [[ -n "$CONFLUENCE_PARENT_PAGE" ]]; then
207
188
  PARENT_ID=$(vds-cli confluence search \
208
189
  --cql "space=$SPACE_KEY AND title = \"$CONFLUENCE_PARENT_PAGE\"" \
209
190
  --limit 1 --json-only \
210
- | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["results"][0]["id"] if d.get("results") else "")')
191
+ | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); process.stdout.write(d.results?.[0]?.id ?? '');")
211
192
  fi
212
193
 
213
194
  CREATE_ARGS=(vds-cli confluence content create-page
@@ -17,24 +17,7 @@ Publish the current SRS to Viettel Confluence via `vds-cli` — create or update
17
17
  Before doing anything, run this check via Bash tool:
18
18
 
19
19
  ```bash
20
- source .claude/skills/_shared/vds-skill/credentials.sh 2>/dev/null
21
- MISSING=""
22
- [[ -x .claude/bin/vds-cli ]] && export PATH=".claude/bin:$PATH"
23
- if ! vds-cli --version >/dev/null 2>&1; then
24
- MISSING="$MISSING vds-cli"
25
- VDS_PATH=$(command -v vds-cli 2>/dev/null)
26
- if [[ -n "$VDS_PATH" ]]; then
27
- echo "NOTE: $VDS_PATH exists but fails to run (broken shim or missing venv)"
28
- fi
29
- fi
30
- [[ -z "${VDS_CONFLUENCE_TOKEN:-}" ]] && MISSING="$MISSING VDS_CONFLUENCE_TOKEN"
31
- [[ -z "${VDS_CONFLUENCE_SPACE_DEFAULT:-}" ]] && MISSING="$MISSING VDS_CONFLUENCE_SPACE_DEFAULT"
32
- if [[ -n "$MISSING" ]]; then
33
- echo "BLOCKED — missing:$MISSING"
34
- echo "Fix: edit ~/.vds/sdlc-config.env (or run: vsaf install vds-skill)"
35
- else
36
- echo "OK"
37
- fi
20
+ node .claude/skills/_shared/vds-skill/config-check.js --cmd vds-cli --env VDS_CONFLUENCE_TOKEN,VDS_CONFLUENCE_SPACE_DEFAULT
38
21
  ```
39
22
 
40
23
  If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do NOT fabricate output.
@@ -84,9 +67,6 @@ Same as `/vds-skill-push-prd` Step 7. Use `Initial SRS push via vds-skill-push-s
84
67
  ### Step 8 — Link SRS page to PRD page (if PRD exists)
85
68
 
86
69
  ```bash
87
- source .claude/skills/_shared/vds-skill/credentials.sh
88
- ensure_env VDS_CONFLUENCE_TOKEN "Enter VDS Confluence personal access token"
89
-
90
70
  # Derive PRD page title from feature name
91
71
  FEATURE_NAME="<from folder>"
92
72
  PRD_TITLE="[PRD] $FEATURE_NAME"
@@ -95,8 +75,9 @@ PRD_PAGE=$(vds-cli confluence search \
95
75
  --cql "space=$SPACE_KEY AND title = \"$PRD_TITLE\"" \
96
76
  --limit 1 --json-only)
97
77
 
98
- PRD_URL=$(echo "$PRD_PAGE" | python3 -c \
99
- 'import json,sys; d=json.load(sys.stdin); r=d.get("results"); print(r[0].get("url","") if r else "")')
78
+ PRD_URL=$(node -e \
79
+ "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); process.stdout.write(d.results?.[0]?.url ?? '');" \
80
+ <<< "$PRD_PAGE")
100
81
 
101
82
  if [[ -n "$PRD_URL" ]]; then
102
83
  # Prepend "Related Documents" macro to SRS body BEFORE push (modify $BODY_FILE)
@@ -12,26 +12,7 @@ Search Viettel Confluence + Jira for pages/tickets related to a feature, then wr
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_CONFLUENCE_TOKEN:-}" ]] && MISSING="$MISSING VDS_CONFLUENCE_TOKEN"
26
- [[ -z "${VDS_JIRA_TOKEN:-}" ]] && MISSING="$MISSING VDS_JIRA_TOKEN"
27
- [[ -z "${VDS_CONFLUENCE_SPACE_DEFAULT:-}" ]] && MISSING="$MISSING VDS_CONFLUENCE_SPACE_DEFAULT"
28
- [[ -z "${VDS_JIRA_PROJECT_DEFAULT:-}" ]] && MISSING="$MISSING VDS_JIRA_PROJECT_DEFAULT"
29
- if [[ -n "$MISSING" ]]; then
30
- echo "BLOCKED — missing:$MISSING"
31
- echo "Fix: edit ~/.vds/sdlc-config.env (or run: vsaf install vds-skill)"
32
- else
33
- echo "OK"
34
- fi
15
+ node .claude/skills/_shared/vds-skill/config-check.js --cmd vds-cli --env VDS_CONFLUENCE_TOKEN,VDS_JIRA_TOKEN,VDS_CONFLUENCE_SPACE_DEFAULT,VDS_JIRA_PROJECT_DEFAULT
35
16
  ```
36
17
 
37
18
  If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do NOT fabricate output. `--dry-run` mode skips vds-cli check.
@@ -39,7 +20,6 @@ If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do N
39
20
  ## Prerequisites
40
21
 
41
22
  - `vds-cli` installed
42
- - `python3` installed
43
23
  - Inside a project root with `.vsaf/docs/features/`
44
24
  - `VDS_CONFLUENCE_TOKEN`, `VDS_JIRA_TOKEN`, `VDS_CONFLUENCE_SPACE_DEFAULT`, `VDS_JIRA_PROJECT_DEFAULT` in `~/.vds/sdlc-config.env`
45
25
 
@@ -62,7 +42,7 @@ If BLOCKED: tell the user exactly what's missing and how to fix, then STOP. Do N
62
42
  ## Implementation
63
43
 
64
44
  ```bash
65
- bash .claude/skills/vds-skill-search-confluence/scripts/search.sh "$@"
45
+ node .claude/skills/vds-skill-search-confluence/scripts/search.js "$@"
66
46
  ```
67
47
 
68
48
  ## Notes
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { execFileSync } = require('child_process');
5
+ const { existsSync, mkdirSync, writeFileSync } = 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 feature = '';
11
+ let query = '';
12
+ let days = 180;
13
+ let limit = 30;
14
+ let dryRun = false;
15
+
16
+ const args = process.argv.slice(2);
17
+ for (let i = 0; i < args.length; i++) {
18
+ switch (args[i]) {
19
+ case '--feature': feature = args[++i]; break;
20
+ case '--query': query = args[++i]; break;
21
+ case '--days': days = parseInt(args[++i], 10); break;
22
+ case '--limit': limit = parseInt(args[++i], 10); break;
23
+ case '--dry-run': dryRun = true; break;
24
+ default:
25
+ process.stderr.write(`Unknown arg: ${args[i]}\n`);
26
+ process.exit(2);
27
+ }
28
+ }
29
+
30
+ if (!feature) { process.stderr.write('ERROR: --feature is required\n'); process.exit(2); }
31
+ if (!query) { process.stderr.write('ERROR: --query (search keywords) is required\n'); process.exit(2); }
32
+
33
+ const outDir = join('.vsaf', 'docs', 'features', feature);
34
+ const outJson = join(outDir, '01-discovery-historical.json');
35
+ const outMd = join(outDir, '01-discovery-historical.md');
36
+
37
+ if (dryRun) {
38
+ loadCredentials();
39
+ const space = process.env.VDS_CONFLUENCE_SPACE_DEFAULT || '<VDS_CONFLUENCE_SPACE_DEFAULT>';
40
+ const project = process.env.VDS_JIRA_PROJECT_DEFAULT || '<VDS_JIRA_PROJECT_DEFAULT>';
41
+ const cql = `space=${space} AND text ~ "${query}" AND lastmodified > now("-${days}d")`;
42
+ const jql = `project = ${project} AND text ~ "${query}" AND created > -${days}d ORDER BY created DESC`;
43
+ console.log('DRY-RUN — would execute:');
44
+ console.log(` vds-cli confluence search --cql "${cql}" --limit ${limit} --json-only`);
45
+ console.log(` vds-cli jira search "${jql}" --limit ${limit} --json-only`);
46
+ console.log(` Write to: ${outJson} + ${outMd}`);
47
+ process.exit(0);
48
+ }
49
+
50
+ loadCredentials();
51
+
52
+ const cmdCheck = requireCommand('vds-cli');
53
+ if (!cmdCheck.found) {
54
+ process.stderr.write('ERROR: vds-cli not found in PATH\n');
55
+ if (cmdCheck.broken) process.stderr.write(`NOTE: ${cmdCheck.path} exists but fails to run (broken shim or missing venv)\n`);
56
+ process.exit(127);
57
+ }
58
+
59
+ (async () => {
60
+ if (!await ensureEnv('VDS_CONFLUENCE_TOKEN', 'Enter VDS Confluence personal access token')) process.exit(1);
61
+ if (!await ensureEnv('VDS_JIRA_TOKEN', 'Enter VDS Jira personal access token')) process.exit(1);
62
+ if (!await ensureEnv('VDS_CONFLUENCE_SPACE_DEFAULT', 'Enter default Confluence space key (e.g. ENG)', false)) process.exit(1);
63
+ if (!await ensureEnv('VDS_JIRA_PROJECT_DEFAULT', 'Enter default Jira project key (e.g. NTTC)', false)) process.exit(1);
64
+
65
+ const space = process.env.VDS_CONFLUENCE_SPACE_DEFAULT;
66
+ const project = process.env.VDS_JIRA_PROJECT_DEFAULT;
67
+ const cql = `space=${space} AND text ~ "${query}" AND lastmodified > now("-${days}d")`;
68
+ const jql = `project = ${project} AND text ~ "${query}" AND created > -${days}d ORDER BY created DESC`;
69
+
70
+ mkdirSync(outDir, { recursive: true });
71
+
72
+ console.log(`Searching Confluence (limit ${limit}, last ${days}d)...`);
73
+ let confResults;
74
+ try {
75
+ const stdout = execFileSync('vds-cli', ['confluence', 'search', '--cql', cql, '--limit', String(limit), '--json-only'], {
76
+ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
77
+ });
78
+ confResults = JSON.parse(stdout);
79
+ } catch { confResults = { results: [] }; }
80
+
81
+ console.log(`Searching Jira (limit ${limit}, last ${days}d)...`);
82
+ let jiraResults;
83
+ try {
84
+ const stdout = execFileSync('vds-cli', ['jira', 'search', jql, '--limit', String(limit), '--json-only'], {
85
+ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
86
+ });
87
+ jiraResults = JSON.parse(stdout);
88
+ } catch { jiraResults = { issues: [] }; }
89
+
90
+ const combined = { query, days_back: days, confluence: confResults.results || [], jira: jiraResults.issues || [] };
91
+ writeFileSync(outJson, JSON.stringify(combined, null, 2) + '\n');
92
+
93
+ const lines = [
94
+ `# Discovery Historical Context — ${query}`, '',
95
+ `> Generated by /vds-skill-search-confluence — searched last ${days} days`, '',
96
+ `## Confluence pages (${combined.confluence.length} results)`, '',
97
+ ];
98
+ for (const page of combined.confluence.slice(0, 10)) {
99
+ const title = page.title || '<no title>';
100
+ const url = page.url || (page._links && page._links.webui) || '';
101
+ const modified = page.lastModified || (page.version && page.version.when) || '?';
102
+ lines.push(`- [${title}](${url}) — modified ${modified}`);
103
+ }
104
+ lines.push('', `## Jira tickets (${combined.jira.length} results)`, '');
105
+ for (const issue of combined.jira.slice(0, 15)) {
106
+ const key = issue.key || '?';
107
+ const summary = (issue.fields && issue.fields.summary) || '<no summary>';
108
+ const status = (issue.fields && issue.fields.status && issue.fields.status.name) || '?';
109
+ lines.push(`- **${key}** — ${summary} (${status})`);
110
+ }
111
+ writeFileSync(outMd, lines.join('\n') + '\n');
112
+ console.log(`Wrote: ${outJson}`);
113
+ console.log(`Wrote: ${outMd}`);
114
+ })();
@@ -57,11 +57,11 @@ vds-cli bitbucket --help
57
57
  vds-cli git --help
58
58
 
59
59
  # Direct uv invocation — project-local
60
- .claude/vds-scripts/scripts/worktree_uv.sh run --directory .claude/vds-scripts --package vds-cli vds-cli --help
61
- .claude/vds-scripts/scripts/worktree_uv.sh run --directory .claude/vds-scripts --package audit_orchestrator vds-audit --help
60
+ uv run --directory .claude/vds-scripts --package vds-cli vds-cli --help
61
+ uv run --directory .claude/vds-scripts --package audit_orchestrator vds-audit --help
62
62
 
63
63
  # Direct uv invocation — global fallback
64
- ~/.claude/vds-scripts/scripts/worktree_uv.sh run --directory ~/.claude/vds-scripts --package vds-cli vds-cli --help
64
+ uv run --directory ~/.claude/vds-scripts --package vds-cli vds-cli --help
65
65
  ```
66
66
 
67
67
  ## Platform Command Families
@@ -1,79 +0,0 @@
1
- #!/bin/bash
2
- # Shared credential helper for vds-skill-* skills.
3
- # Sourced by skill scripts to lazy-prompt + persist credentials.
4
- # Config file: ~/.vds/sdlc-config.env (chmod 600)
5
- #
6
- # Portable across bash and zsh (uses eval for indirect expansion).
7
-
8
- VSAF_CONFIG_FILE="${VSAF_CONFIG_FILE:-$HOME/.vds/sdlc-config.env}"
9
- readonly VSAF_CONFIG_FILE
10
-
11
- # Load existing config if present
12
- if [[ -f "$VSAF_CONFIG_FILE" ]]; then
13
- # shellcheck source=/dev/null
14
- source "$VSAF_CONFIG_FILE"
15
- fi
16
-
17
- # Also load vds-cli's own env if present (for token reuse)
18
- if [[ -f "$HOME/.vds/.env" ]]; then
19
- set -a
20
- # shellcheck source=/dev/null
21
- source "$HOME/.vds/.env"
22
- set +a
23
- fi
24
-
25
- # ensure_env VAR_NAME "Prompt message" [is_secret]
26
- # Prompts user if VAR is empty, persists to config file.
27
- # Re-persists if VAR is pre-set (supports token rotation).
28
- ensure_env() {
29
- local var="$1"
30
- local prompt="${2:-Enter $var}"
31
- local is_secret="${3:-true}"
32
-
33
- # Portable indirect expansion (works in both bash and zsh)
34
- local value
35
- eval "value=\${$var:-}"
36
-
37
- if [[ -z "$value" ]]; then
38
- # Prompt user
39
- if [[ "$is_secret" == "true" ]]; then
40
- read -rsp "$prompt: " value
41
- echo "" >&2
42
- else
43
- read -rp "$prompt: " value
44
- fi
45
-
46
- if [[ -z "$value" ]]; then
47
- echo "ERROR: $var is required" >&2
48
- return 1
49
- fi
50
-
51
- export "$var=$value"
52
- fi
53
-
54
- # Persist to config file (idempotent — replace existing entry or append)
55
- mkdir -p "$(dirname "$VSAF_CONFIG_FILE")" || { echo "ERROR: Cannot create config dir $(dirname "$VSAF_CONFIG_FILE")" >&2; return 1; }
56
-
57
- if grep -q "^${var}=" "$VSAF_CONFIG_FILE" 2>/dev/null; then
58
- # Update in place (BSD/GNU sed compatible via tmp file)
59
- local tmp
60
- tmp=$(mktemp) || { echo "ERROR: mktemp failed" >&2; return 1; }
61
- trap 'rm -f "$tmp"' EXIT INT TERM
62
- grep -v "^${var}=" "$VSAF_CONFIG_FILE" > "$tmp"
63
- printf '%s=%q\n' "$var" "$value" >> "$tmp"
64
- mv "$tmp" "$VSAF_CONFIG_FILE"
65
- trap - EXIT INT TERM
66
- else
67
- printf '%s=%q\n' "$var" "$value" >> "$VSAF_CONFIG_FILE"
68
- fi
69
- chmod 600 "$VSAF_CONFIG_FILE" || echo "WARN: Could not set chmod 600 on $VSAF_CONFIG_FILE" >&2
70
- }
71
-
72
- # require_command COMMAND_NAME — exits 127 if missing
73
- require_command() {
74
- if ! command -v "$1" >/dev/null 2>&1; then
75
- echo "ERROR: Required command '$1' not found in PATH" >&2
76
- echo " Install it before running this skill." >&2
77
- return 127
78
- fi
79
- }
@@ -1,105 +0,0 @@
1
- #!/bin/bash
2
- # vds-skill-create-bitbucket-pr: create Bitbucket PR via vds-cli
3
- # Usage: create-pr.sh [--dry-run] [--target BRANCH] [--description-file FILE] [--title TITLE]
4
- # Auto-extracts project/repo from `git remote get-url origin`.
5
-
6
- # NOTE: No `set -e` — we want full control of the exit-code path.
7
-
8
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
- # shellcheck source=/dev/null
10
- source "$SCRIPT_DIR/../../_shared/vds-skill/credentials.sh"
11
-
12
- require_command git
13
- # Note: require_command vds-cli moved AFTER dry-run check.
14
-
15
- DRY_RUN=false
16
- TARGET_BRANCH="master"
17
- DESC_FILE=""
18
- TITLE=""
19
-
20
- while [[ $# -gt 0 ]]; do
21
- case "$1" in
22
- --dry-run) DRY_RUN=true; shift ;;
23
- --target) TARGET_BRANCH="$2"; shift 2 ;;
24
- --description-file) DESC_FILE="$2"; shift 2 ;;
25
- --title) TITLE="$2"; shift 2 ;;
26
- *) echo "Unknown arg: $1" >&2; exit 2 ;;
27
- esac
28
- done
29
-
30
- REMOTE_URL=$(git remote get-url origin 2>/dev/null || true)
31
- if [[ -z "$REMOTE_URL" ]]; then
32
- echo "ERROR: No git remote 'origin' configured" >&2
33
- exit 1
34
- fi
35
-
36
- if [[ ! "$REMOTE_URL" =~ bitbucket\.digital\.vn ]]; then
37
- echo "WARNING: Remote URL does not match bitbucket.digital.vn pattern:" >&2
38
- echo " $REMOTE_URL" >&2
39
- echo " This skill is for Viettel Bitbucket. For GitHub, use 'gh pr create'." >&2
40
- exit 1
41
- fi
42
-
43
- # Extract PROJECT/REPO from URL
44
- # Handles:
45
- # ssh://git@bitbucket.digital.vn/PROJECT/repo.git
46
- # ssh://git@bitbucket.digital.vn:7999/PROJECT/repo.git
47
- # https://bitbucket.digital.vn/scm/PROJECT/repo.git
48
- if [[ "$REMOTE_URL" =~ /scm/([^/]+)/([^/]+)\.git$ ]]; then
49
- PROJECT="${BASH_REMATCH[1]}"
50
- REPO="${BASH_REMATCH[2]}"
51
- elif [[ "$REMOTE_URL" =~ /([^/]+)/([^/]+)\.git$ ]]; then
52
- PROJECT="${BASH_REMATCH[1]}"
53
- REPO="${BASH_REMATCH[2]}"
54
- else
55
- echo "ERROR: Cannot parse PROJECT/REPO from remote URL: $REMOTE_URL" >&2
56
- exit 1
57
- fi
58
-
59
- SOURCE_BRANCH=$(git rev-parse --abbrev-ref HEAD)
60
- if [[ -z "$SOURCE_BRANCH" ]] || [[ "$SOURCE_BRANCH" == "HEAD" ]]; then
61
- echo "ERROR: Not on a named branch" >&2
62
- exit 1
63
- fi
64
-
65
- if [[ -z "$TITLE" ]]; then
66
- TITLE=$(git log -1 --pretty=%s)
67
- fi
68
-
69
- if [[ -z "$DESC_FILE" ]]; then
70
- DESC_FILE=$(find .vsaf/docs/features -name '09-ship.md' 2>/dev/null | head -1)
71
- [[ -z "$DESC_FILE" ]] && DESC_FILE=$(find .vsaf/docs/hotfixes -name '03-ship.md' 2>/dev/null | head -1)
72
- fi
73
-
74
- CMD=(vds-cli bitbucket pr create "$PROJECT/$REPO"
75
- --source "$SOURCE_BRANCH"
76
- --target "$TARGET_BRANCH"
77
- --title "$TITLE")
78
- [[ -n "$DESC_FILE" ]] && [[ -f "$DESC_FILE" ]] && CMD+=(--description-file "$DESC_FILE")
79
-
80
- if [[ "$DRY_RUN" == "true" ]]; then
81
- echo "DRY-RUN — would execute:"
82
- printf ' %q' "${CMD[@]}"
83
- echo ""
84
- echo " + --yes --json-only"
85
- exit 0
86
- fi
87
-
88
- require_command vds-cli
89
-
90
- ensure_env VDS_BITBUCKET_TOKEN "Enter VDS Bitbucket personal access token"
91
-
92
- echo "About to create PR:"
93
- echo " Project/Repo: $PROJECT/$REPO"
94
- echo " Source: $SOURCE_BRANCH"
95
- echo " Target: $TARGET_BRANCH"
96
- echo " Title: $TITLE"
97
- echo " Description: ${DESC_FILE:-<empty>}"
98
- echo ""
99
- read -rp "Proceed? [y/N]: " confirm
100
- if [[ "$confirm" != "y" ]] && [[ "$confirm" != "Y" ]]; then
101
- echo "Aborted"
102
- exit 1
103
- fi
104
-
105
- "${CMD[@]}" --yes --json-only