@jwilger/pi-development-system 0.3.5 → 0.3.6

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": "@jwilger/pi-development-system",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "A pi extension package representing a seasoned approach to software development using a full AI SDLC.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -1,4 +1,4 @@
1
- import { parse } from "shell-quote";
1
+ import { parseShell } from "./shell-parse.ts";
2
2
 
3
3
  export type GitIntent =
4
4
  | "history-rewrite"
@@ -58,7 +58,7 @@ const isRedirect = (op: string): boolean => /^[<>]/.test(op) && !op.endsWith("("
58
58
  /** Tokens of one line grouped into simple commands (operators separate segments). */
59
59
  function segments(line: string): string[][] {
60
60
  const result: string[][] = [[]];
61
- for (const token of parse(line, (name) => `$${name}`)) {
61
+ for (const token of parseShell(line, (name) => `$${name}`)) {
62
62
  if (typeof token === "string") result[result.length - 1]?.push(token);
63
63
  else if ("pattern" in token) result[result.length - 1]?.push(token.pattern);
64
64
  else if ("op" in token && isRedirect(token.op)) continue;
@@ -0,0 +1,21 @@
1
+ import { type ParseEntry, parse } from "shell-quote";
2
+
3
+ /**
4
+ * shell-quote throws on text that is valid shell or valid file content (a template literal inside a
5
+ * heredoc), and a guard must never crash the tool call. Retry with the `${` openers neutralised, then
6
+ * fall back to whitespace-split words so command classification still sees every token.
7
+ */
8
+ export function parseShell(line: string, env?: (name: string) => string): ParseEntry[] {
9
+ const attempt = (text: string): ParseEntry[] | undefined => {
10
+ try {
11
+ return env === undefined ? parse(text) : parse(text, env);
12
+ } catch {
13
+ return undefined;
14
+ }
15
+ };
16
+ return (
17
+ attempt(line) ??
18
+ attempt(line.replaceAll("${", "$")) ??
19
+ line.split(/\s+/).filter((word) => word !== "")
20
+ );
21
+ }
@@ -1,5 +1,5 @@
1
1
  import { posix } from "node:path";
2
- import { parse } from "shell-quote";
2
+ import { parseShell } from "./shell-parse.ts";
3
3
 
4
4
  export type WeakeningSignals = { addsSkip: boolean; emptied: boolean; commentedOut: boolean };
5
5
 
@@ -151,7 +151,7 @@ function splitLines(command: string): string {
151
151
 
152
152
  /** Boundary decode of shell-quote's output into domain tokens; globs count as words. */
153
153
  function tokenize(command: string): Token[] {
154
- return parse(splitLines(command)).flatMap((entry): Token[] => {
154
+ return parseShell(splitLines(command)).flatMap((entry): Token[] => {
155
155
  if (typeof entry === "string") return [{ kind: "word", value: entry }];
156
156
  if ("pattern" in entry) return [{ kind: "word", value: entry.pattern }];
157
157
  if ("op" in entry) return [{ kind: "op", op: entry.op }];
@@ -90,10 +90,13 @@ export function registerTestGuard(deps: TestGuardDeps): void {
90
90
 
91
91
  const consumeOpenDeparture = (): void => {
92
92
  const { openDepartures, activeSlice } = deps.state.get();
93
- const used = openDepartures.find(
93
+ const applicable = openDepartures.filter(
94
94
  (d) => d.gate === gate && (d.scope.kind !== "slice" || d.scope.slice === activeSlice),
95
95
  );
96
- if (used?.scope.kind !== "once") return;
96
+ // A broader departure covers the call without being spent; only a lone once-scoped one is used up.
97
+ if (applicable.some((d) => d.scope.kind !== "once")) return;
98
+ const used = applicable[0];
99
+ if (used === undefined) return;
97
100
  deps.state.update((s) => ({
98
101
  ...s,
99
102
  openDepartures: s.openDepartures.filter((d) => d.id !== used.id),