@ktpartners/dgs-platform 3.3.0 → 3.3.1

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/CHANGELOG.md CHANGED
@@ -8,6 +8,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [3.3.1] - 2026-05-26
12
+
13
+ ### Fixed
14
+ - **`execGit` works on Windows** — `bin/lib/core.cjs:execGit` previously POSIX-quoted args (`'arg with spaces'`) then ran them through `execSync`'s shell. On Windows `execSync` invokes `cmd.exe`, which treats single quotes as literal characters — so `git commit -m 'docs(88): create phase plan'` was tokenised into separate args and git interpreted the trailing words as pathspecs. Every multi-word commit message broke on Windows, affecting essentially every DGS command that commits (`/dgs:fast`, `/dgs:quick`, `/dgs:execute-phase`, `/dgs:complete-quick`, the v1→v2 migration self-commit, etc.). Replaced with `execFileSync('git', args, ...)` which bypasses the shell entirely on both platforms — no quoting needed since `args` is already an array. Preserved the existing `{exitCode, stdout, stderr}` return shape and `.trim()` behaviour. Added round-trip regression test in `tests/core.test.cjs` that asserts a message with spaces, parens, colon, ampersand, and percent (`docs(88): create phase plan & 100% coverage`) survives `init`→`commit`→`log` verbatim (quick-260526-dip).
15
+ - **`execGitWithTimeout` works on Windows** — `bin/lib/sync.cjs:execGitWithTimeout` was a fork of `execGit` (added a `timeout` option per its `(NOT execGit)` comment) and inherited the identical POSIX-quoting bug. Affected `/dgs:sync` pre-flight checks across every registered repo (`remote`, `rev-parse --abbrev-ref HEAD`, etc.). Same fix: `execFileSync('git', args, { ..., timeout: timeoutMs })`. Preserved the `isTimeout`/`isAuth` error classification in the catch block (quick-260526-e5l).
16
+ - **`hasCodeFiles` no longer shells out** — `bin/lib/init.cjs` used `execSync('find . -maxdepth 3 \\( -name "*.ts" -o ... \\) | grep -v node_modules | head -5')` during `/dgs:new-project` to detect existing source files for template selection. `find`/`grep`/`head` aren't on Windows by default and the bash-style `\\(` escaping is shell-specific; the catch-block silently fell back to `hasCode = false`, producing wrong init heuristics on Windows. Extracted a pure-Node `hasCodeFiles(cwd)` helper using `fs.readdirSync({ withFileTypes: true })` — depth 3, skips `node_modules` and `.git`, early-exits at 5 matches across `.ts/.js/.py/.go/.rs/.swift/.java`. Exported for testability; added unit tests in `tests/init.test.cjs` covering depth-3 boundary, depth-4 cap, `node_modules`/`.git` skip, the 7-extension matrix, and nonexistent paths (quick-260526-e5l).
17
+ - **`~/` tilde expansion works on Windows** — `bin/lib/verify.cjs` resolved `~/path` canonical_refs via `path.join(process.env.HOME || '', cleanRef.slice(2))`. On Windows `HOME` is unset, so `|| ''` produced a relative path and `cmdVerifyReferences` falsely reported the ref as missing. Replaced with `path.join(os.homedir(), cleanRef.slice(2))` — `os.homedir()` returns `USERPROFILE` on Windows and `HOME` elsewhere. Added a sentinel-file test in `tests/verify.test.cjs` that creates a real file under `os.homedir()` and verifies the `~/` resolution finds it (quick-260526-e5l).
18
+ - **PDF/DOCX/XLSX extraction no longer shells out** — `bin/lib/docs.cjs` ran the async `pdf-parse`/`mammoth`/`exceljs` libs in a sync context via three near-identical `` execSync(`node -e ${JSON.stringify(script)}`) `` sites. Although `JSON.stringify`'s double quotes are cmd.exe-friendly, this path (a) triggered cmd.exe %-variable expansion on filenames containing `%`, and (b) pushed the script body toward cmd.exe's 8191-char command-line limit on long inputs. Replaced all three with `spawnSync('node', ['-e', script], { shell: false, ... })` — same script body, no shell at all. Added structural source-grep guard in `tests/docs.test.cjs` (`!/execSync\(\s*\`node -e/.test(src)`) so the bug pattern can't regress (quick-260526-e5l).
19
+
11
20
  ## [3.3.0] - 2026-05-15
12
21
 
13
22
  ### Added
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { execSync } = require('child_process');
7
+ const { execSync, execFileSync } = require('child_process');
8
8
  const { getPlanningRoot, isV2Install, PROJECTS_DIR } = require('./paths.cjs');
9
9
 
10
10
  // ─── Path helpers ────────────────────────────────────────────────────────────
@@ -181,11 +181,7 @@ function isGitIgnored(cwd, targetPath) {
181
181
 
182
182
  function execGit(cwd, args) {
183
183
  try {
184
- const escaped = args.map(a => {
185
- if (/^[a-zA-Z0-9._\-/=:@]+$/.test(a)) return a;
186
- return "'" + a.replace(/'/g, "'\\''") + "'";
187
- });
188
- const stdout = execSync('git ' + escaped.join(' '), {
184
+ const stdout = execFileSync('git', args, {
189
185
  cwd,
190
186
  stdio: 'pipe',
191
187
  encoding: 'utf-8',
@@ -14,6 +14,7 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
  const crypto = require('crypto');
17
+ const { spawnSync } = require('child_process');
17
18
  const { safeReadFile, execGit, generateSlugInternal, output, error } = require('./core.cjs');
18
19
  const { getPlanningRoot } = require('./paths.cjs');
19
20
  const { findIdeaFile } = require('./ideas.cjs');
@@ -129,10 +130,11 @@ function extractText(filePath, ext) {
129
130
  try {
130
131
  const pdfParse = require('pdf-parse');
131
132
  const buffer = fs.readFileSync(filePath);
132
- // pdf-parse returns a promise; we use execSync workaround for sync context
133
- // Instead, we'll use a sync extraction approach
133
+ // pdf-parse returns a promise; run it in a child node process via
134
+ // spawnSync so the parent can stay synchronous. spawnSync (not
135
+ // execSync with a shell-built command) is required on Windows where
136
+ // cmd.exe applies different quoting rules.
134
137
  let result = null;
135
- const { execSync } = require('child_process');
136
138
  const script = `
137
139
  const pdfParse = require('pdf-parse');
138
140
  const fs = require('fs');
@@ -143,12 +145,15 @@ function extractText(filePath, ext) {
143
145
  process.stdout.write(JSON.stringify({ error: err.message }));
144
146
  });
145
147
  `;
146
- const out = execSync(`node -e ${JSON.stringify(script)}`, {
148
+ const r = spawnSync('node', ['-e', script], {
147
149
  encoding: 'utf-8',
148
150
  timeout: 30000,
149
151
  stdio: ['pipe', 'pipe', 'pipe'],
150
152
  });
151
- result = JSON.parse(out.trim());
153
+ if (r.status !== 0) {
154
+ return { text: null, error: `PDF extraction failed: ${r.stderr || r.error?.message || 'unknown'}` };
155
+ }
156
+ result = JSON.parse(r.stdout.trim());
152
157
  if (result.error) {
153
158
  return { text: null, error: result.error };
154
159
  }
@@ -160,7 +165,6 @@ function extractText(filePath, ext) {
160
165
 
161
166
  if (ext === '.xlsx') {
162
167
  try {
163
- const { execSync } = require('child_process');
164
168
  const script = `
165
169
  const ExcelJS = require('exceljs');
166
170
  const workbook = new ExcelJS.Workbook();
@@ -178,11 +182,15 @@ function extractText(filePath, ext) {
178
182
  process.stdout.write(JSON.stringify({ error: err.message }));
179
183
  });
180
184
  `;
181
- const out = execSync(`node -e ${JSON.stringify(script)}`, {
185
+ const r = spawnSync('node', ['-e', script], {
182
186
  encoding: 'utf-8',
183
187
  timeout: 30000,
184
- }).trim();
185
- const parsed = JSON.parse(out);
188
+ stdio: ['pipe', 'pipe', 'pipe'],
189
+ });
190
+ if (r.status !== 0) {
191
+ return { text: null, error: `XLSX extraction failed: ${r.stderr || r.error?.message || 'unknown'}` };
192
+ }
193
+ const parsed = JSON.parse(r.stdout.trim());
186
194
  if (parsed.error) return { text: null, error: parsed.error };
187
195
  return { text: parsed.text, error: null };
188
196
  } catch (e) {
@@ -202,7 +210,6 @@ function extractText(filePath, ext) {
202
210
  if (ext === '.docx') {
203
211
  try {
204
212
  const mammoth = require('mammoth');
205
- const { execSync } = require('child_process');
206
213
  const script = `
207
214
  const mammoth = require('mammoth');
208
215
  mammoth.extractRawText({ path: ${JSON.stringify(filePath)} })
@@ -213,12 +220,15 @@ function extractText(filePath, ext) {
213
220
  process.stdout.write(JSON.stringify({ error: err.message }));
214
221
  });
215
222
  `;
216
- const out = execSync(`node -e ${JSON.stringify(script)}`, {
223
+ const r = spawnSync('node', ['-e', script], {
217
224
  encoding: 'utf-8',
218
225
  timeout: 30000,
219
226
  stdio: ['pipe', 'pipe', 'pipe'],
220
227
  });
221
- const result = JSON.parse(out.trim());
228
+ if (r.status !== 0) {
229
+ return { text: null, error: `DOCX extraction failed: ${r.stderr || r.error?.message || 'unknown'}` };
230
+ }
231
+ const result = JSON.parse(r.stdout.trim());
222
232
  if (result.error) {
223
233
  return { text: null, error: result.error };
224
234
  }
@@ -88,6 +88,54 @@ function applySyncPull(cwd, workflowName, result) {
88
88
  result.needs_pull = false;
89
89
  }
90
90
 
91
+ // ─── Brownfield detection ───────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Detect whether the directory tree rooted at cwd contains any source
95
+ * files in a small set of common languages, ignoring node_modules and .git,
96
+ * with a maximum recursion depth of 3 (cwd itself is depth 0).
97
+ *
98
+ * Returns true on first match (early termination).
99
+ * Returns false on any error (e.g. permission denied on top-level dir).
100
+ *
101
+ * Pure Node — replaces the prior `find ... | grep ... | head -5` shell
102
+ * pipeline that does not exist on Windows.
103
+ *
104
+ * @param {string} cwd - Directory to scan.
105
+ * @returns {boolean}
106
+ */
107
+ function hasCodeFiles(cwd) {
108
+ const CODE_EXTENSIONS = new Set(['.ts', '.js', '.py', '.go', '.rs', '.swift', '.java']);
109
+ const SKIP_DIRS = new Set(['node_modules', '.git']);
110
+ const MAX_DEPTH = 3;
111
+
112
+ function walk(dir, depth) {
113
+ if (depth > MAX_DEPTH) return false;
114
+ let entries;
115
+ try {
116
+ entries = fs.readdirSync(dir, { withFileTypes: true });
117
+ } catch {
118
+ return false;
119
+ }
120
+ for (const entry of entries) {
121
+ if (entry.isDirectory()) {
122
+ if (SKIP_DIRS.has(entry.name)) continue;
123
+ if (walk(path.join(dir, entry.name), depth + 1)) return true;
124
+ } else if (entry.isFile()) {
125
+ const ext = path.extname(entry.name).toLowerCase();
126
+ if (CODE_EXTENSIONS.has(ext)) return true;
127
+ }
128
+ }
129
+ return false;
130
+ }
131
+
132
+ try {
133
+ return walk(cwd, 0);
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
91
139
  // ─── v2 Project Context Resolution ──────────────────────────────────────────
92
140
 
93
141
  /**
@@ -411,17 +459,9 @@ function cmdInitNewProject(cwd, slugArg, raw) {
411
459
  const braveKeyFile = path.join(homedir, '.dgs', 'brave_api_key');
412
460
  const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
413
461
 
414
- // Detect existing code
415
- let hasCode = false;
462
+ // Detect existing code (pure-Node walk, Windows-safe)
463
+ const hasCode = hasCodeFiles(cwd);
416
464
  let hasPackageFile = false;
417
- try {
418
- const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', {
419
- cwd,
420
- encoding: 'utf-8',
421
- stdio: ['pipe', 'pipe', 'pipe'],
422
- });
423
- hasCode = files.trim().length > 0;
424
- } catch {}
425
465
 
426
466
  hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
427
467
  pathExistsInternal(cwd, 'requirements.txt') ||
@@ -1547,4 +1587,5 @@ module.exports = {
1547
1587
  cmdInitProgress,
1548
1588
  cmdInitProgressAll,
1549
1589
  applySyncPull,
1590
+ hasCodeFiles,
1550
1591
  };
@@ -5,7 +5,7 @@
5
5
  * with pre-flight checks, error handling, and structured result reporting.
6
6
  */
7
7
 
8
- const { execSync } = require('child_process');
8
+ const { execSync, execFileSync } = require('child_process');
9
9
  const path = require('path');
10
10
  const fs = require('fs');
11
11
  const { execGit, safeReadFile, loadConfig } = require('./core.cjs');
@@ -29,11 +29,7 @@ const { getLocalConfigPath } = require('./config.cjs');
29
29
  */
30
30
  function execGitWithTimeout(cwd, args, timeoutMs = 30000) {
31
31
  try {
32
- const escaped = args.map(a => {
33
- if (/^[a-zA-Z0-9._\-/=:@]+$/.test(a)) return a;
34
- return "'" + a.replace(/'/g, "'\\''") + "'";
35
- });
36
- const stdout = execSync('git ' + escaped.join(' '), {
32
+ const stdout = execFileSync('git', args, {
37
33
  cwd,
38
34
  stdio: 'pipe',
39
35
  encoding: 'utf-8',
@@ -4,6 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
+ const os = require('os');
7
8
  const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, output, error } = require('./core.cjs');
8
9
  const { getPlanningRoot } = require('./paths.cjs');
9
10
  const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
@@ -227,7 +228,7 @@ function cmdVerifyReferences(cwd, filePath, raw) {
227
228
  for (const ref of atRefs) {
228
229
  const cleanRef = ref.slice(1); // remove @
229
230
  const resolved = cleanRef.startsWith('~/')
230
- ? path.join(process.env.HOME || '', cleanRef.slice(2))
231
+ ? path.join(os.homedir(), cleanRef.slice(2))
231
232
  : path.join(cwd, cleanRef);
232
233
  if (fs.existsSync(resolved)) {
233
234
  found.push(cleanRef);
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "bugs": {
9
9
  "url": "https://github.com/KT-Partners-Ltd/dgs-platform-docs/issues"
10
10
  },
11
- "version": "3.3.0",
11
+ "version": "3.3.1",
12
12
  "description": "Deliver Great Systems Platform — A meta-prompting, context engineering and spec-driven development system for Claude Code and Gemini by KT Partners.",
13
13
  "bin": {
14
14
  "dgs": "bin/install.js"