@stravica-ai/digital-operator-pa 0.1.0 → 0.1.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.
@@ -23,8 +23,21 @@
23
23
  // under that subtree) — exactly the case a brand-new mount hits. `=all`
24
24
  // makes git enumerate every untracked file individually, so each path
25
25
  // is checked against the exclusion prefixes on its own merits.
26
+ //
27
+ // `-z` is equally load-bearing: without it, git C-quotes any path
28
+ // containing a space or non-ASCII byte (`core.quotePath` defaults true),
29
+ // emitting `?? "dir/Club photo.jpeg"` with the double-quotes and octal
30
+ // escapes as LITERAL characters in the output. registry-scope.mjs then
31
+ // slices that quoted string off the line and hands it to `git add --`
32
+ // verbatim, so git looks for a file literally named `"dir/Club
33
+ // photo.jpeg"` (quotes included) and fails with `fatal: pathspec … did
34
+ // not match any files`, taking the whole sync tick down with exit 3.
35
+ // `-z` switches the output to NUL-delimited records that are NEVER
36
+ // quoted (git emits the raw bytes), eliminating the entire quoted-
37
+ // pathspec bug class — spaces, quotes, backslashes, and unicode alike.
38
+ // See parsePorcelainPaths in registry-scope.mjs for the NUL parser.
26
39
  export async function statusPorcelain(runner, { cwd }) {
27
- return runner.spawn('git', ['status', '--porcelain', '--untracked-files=all'], { cwd });
40
+ return runner.spawn('git', ['status', '--porcelain', '-z', '--untracked-files=all'], { cwd });
28
41
  }
29
42
 
30
43
  export async function currentBranch(runner, { cwd }) {
@@ -93,22 +106,37 @@ export async function push(runner, { cwd }) {
93
106
  // git-status(1) `Short Format` — "Unmerged entries have the following
94
107
  // second column"): DD, AU, UD, UA, DU, AA, UU. Anything else is a normal
95
108
  // modification (M ), untracked (??), etc.
109
+ //
110
+ // Uses `-z` (NUL-delimited, never C-quoted) for the same reason
111
+ // statusPorcelain does: a conflicted path with a space or non-ASCII byte
112
+ // would otherwise be emitted C-quoted and flow — quotes and all — into
113
+ // `git checkout --ours -- <path>` / `git add -- <path>` during
114
+ // auto-resolve, failing the same way the staging path did. NUL records
115
+ // carry the raw bytes, so the paths passed downstream match on disk.
96
116
  export async function listConflictedPaths(runner, { cwd }) {
97
- const r = await runner.spawn('git', ['status', '--porcelain'], { cwd });
117
+ const r = await runner.spawn('git', ['status', '--porcelain', '-z'], { cwd });
98
118
  if (r.code !== 0) {
99
119
  return { code: r.code, stderr: r.stderr, paths: [] };
100
120
  }
101
121
  const paths = [];
102
- for (const line of r.stdout.split('\n')) {
103
- // Porcelain v1 line: `XY <path>` where XY is exactly two chars
104
- // followed by a space. Renamed entries use `XY <old> -> <new>` — the
105
- // conflict codes below never apply to renames, so we treat the tail
106
- // as the path verbatim.
107
- if (line.length < 4) continue;
108
- const xy = line.slice(0, 2);
122
+ // `-z` records are NUL-terminated (trailing empty token after the final
123
+ // NUL). Each record is `XY <path>`; XY is exactly two chars followed by
124
+ // a space. Renamed/copied entries (`R`/`C` in the X column) carry a
125
+ // SECOND NUL-terminated field — the source path — which we must step
126
+ // over so it is not mis-scanned as its own record. Conflict codes never
127
+ // apply to renames, so a rename record never contributes a path here.
128
+ const records = r.stdout.split('\0');
129
+ for (let i = 0; i < records.length; i += 1) {
130
+ const rec = records[i];
131
+ if (rec.length < 4) continue;
132
+ const xy = rec.slice(0, 2);
133
+ if (xy[0] === 'R' || xy[0] === 'C') {
134
+ i += 1; // consume the trailing source-path field
135
+ continue;
136
+ }
109
137
  if (xy === 'DD' || xy === 'AU' || xy === 'UD' || xy === 'UA'
110
138
  || xy === 'DU' || xy === 'AA' || xy === 'UU') {
111
- paths.push(line.slice(3));
139
+ paths.push(rec.slice(3));
112
140
  }
113
141
  }
114
142
  return { code: 0, stderr: '', paths };
@@ -171,30 +171,44 @@ export function computeCommitScope({ repoRoot, hostnameFn = osHostname }) {
171
171
  }
172
172
 
173
173
  /**
174
- * Parse `git status --porcelain` output into a flat list of paths. Rename
175
- * entries (`R old -> new`) contribute BOTH paths — `git add -- <path>`
176
- * correctly stages a deletion when given an explicit path that no longer
177
- * exists on disk (git >= 2.0), so staging both sides of a rename is
178
- * sufficient without needing `-A` semantics.
174
+ * Parse `git status --porcelain -z` output into a flat list of paths.
175
+ * Rename entries contribute BOTH paths — `git add -- <path>` correctly
176
+ * stages a deletion when given an explicit path that no longer exists on
177
+ * disk (git >= 2.0), so staging both sides of a rename is sufficient
178
+ * without needing `-A` semantics.
179
179
  *
180
- * Shares the same C-style-quoting limitation as `listConflictedPaths` in
181
- * git.mjs (paths with unusual characters are not unquoted) — acceptable,
182
- * matches existing precedent in this module.
180
+ * Expects the `-z` (NUL-delimited) format produced by
181
+ * `git.mjs`'s statusPorcelain — NOT the default newline format. `-z`
182
+ * records are never C-quoted (git emits raw path bytes), which is the
183
+ * whole point: the default format wraps any path containing a space or
184
+ * non-ASCII byte in literal double-quotes with octal escapes, and those
185
+ * quoted strings then fail `git add -- <path>` with `fatal: pathspec …
186
+ * did not match any files`. Parsing `-z` eliminates that entire bug class
187
+ * (spaces, quotes, backslashes, unicode).
188
+ *
189
+ * `-z` record shape:
190
+ * - normal entry: `XY <path>` terminated by NUL
191
+ * - rename/copy: `XY <newPath>` NUL `<oldPath>` NUL — the source path
192
+ * is a SEPARATE bare field (no XY prefix) that FOLLOWS the new path.
193
+ * Note the order is reversed vs the newline format's `old -> new`.
194
+ * A trailing empty token follows the final NUL; the length guard skips it.
183
195
  *
184
196
  * @param {string} porcelainOutput
185
197
  * @returns {string[]}
186
198
  */
187
199
  export function parsePorcelainPaths(porcelainOutput) {
188
200
  const out = [];
189
- for (const rawLine of porcelainOutput.split('\n')) {
190
- if (rawLine.length < 4) continue;
191
- const rest = rawLine.slice(3);
192
- const arrowIdx = rest.indexOf(' -> ');
193
- if (arrowIdx !== -1) {
194
- out.push(rest.slice(0, arrowIdx));
195
- out.push(rest.slice(arrowIdx + 4));
196
- } else {
197
- out.push(rest);
201
+ const records = porcelainOutput.split('\0');
202
+ for (let i = 0; i < records.length; i += 1) {
203
+ const rec = records[i];
204
+ if (rec.length < 4) continue;
205
+ const xy = rec.slice(0, 2);
206
+ out.push(rec.slice(3));
207
+ // Rename/copy: the next NUL field is the source path (bare, unquoted).
208
+ if (xy[0] === 'R' || xy[0] === 'C') {
209
+ const source = records[i + 1];
210
+ if (source !== undefined && source.length > 0) out.push(source);
211
+ i += 1;
198
212
  }
199
213
  }
200
214
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravica-ai/digital-operator-pa",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Digital Operator PA — single-bundled public package surface (bins + library) for the platform.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -35,7 +35,7 @@
35
35
  "license": "MIT",
36
36
  "repository": {
37
37
  "type": "git",
38
- "url": "git+https://github.com/Stravica/engineering-pa-dev.git",
38
+ "url": "git+https://github.com/Stravica/digital-operator-pa.git",
39
39
  "directory": "packages/engineering-pa"
40
40
  },
41
41
  "publishConfig": {