@passioncode-ai/fabric-agent-adapter 0.81.0 → 0.83.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +200 -3
  2. package/README.md +11 -6
  3. package/SECURITY.md +8 -5
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +7 -2
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +9 -1
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +25 -2
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +19 -1
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +274 -17
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +24 -151
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
  17. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +3 -16
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +17 -9
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +24 -6
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +23 -10
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +9 -1
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -2
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +28 -4
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +30 -11
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +19 -1
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +4 -2
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +7 -5
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +22 -11
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +274 -0
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +1 -1
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -2
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +14 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +54 -11
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +45 -31
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +16 -6
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +29 -7
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +5 -2
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +18 -12
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +6 -1
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +27 -10
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +3 -5
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +21 -8
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +20 -1
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +5 -0
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +88 -5
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +5 -0
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +24 -3
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +455 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +17 -2
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +18 -5
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +133 -30
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +44 -5
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +180 -0
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +5 -12
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +468 -0
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +2 -1
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +404 -0
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +6 -2
  80. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +16 -3
  81. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +97 -9
  82. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +35 -48
  83. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +15 -7
  84. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +35 -159
  85. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +18 -13
  86. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +23 -14
  87. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +3 -1
  88. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +43 -11
  89. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +21 -18
  90. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +39 -17
  91. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +55 -96
  92. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +98 -1
  93. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +92 -10
  94. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +43 -1
  95. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +647 -0
  96. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +16 -4
  97. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +20 -5
  98. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +180 -162
  99. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +274 -17
  100. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +3 -0
  101. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +390 -0
  102. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +3 -8
  103. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +492 -0
  104. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +465 -55
  105. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
  106. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +16 -7
  107. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
  108. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +5 -4
  109. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
  110. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
  111. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
  112. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +9 -1
  113. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
  114. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
  115. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
  116. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +162 -0
  117. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +19 -1
  118. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  119. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
  120. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
  121. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +30 -17
  122. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +53 -35
  123. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
  124. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
  125. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +34 -21
  126. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +96 -15
  127. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
@@ -8,6 +8,7 @@ from pathlib import Path, PurePath
8
8
  from typing import Any
9
9
  from typing import List
10
10
  from typing import Optional
11
+ from typing import Tuple
11
12
 
12
13
 
13
14
  _DRIVE_MOUNT = re.compile(r"/(?:cygdrive/)?([A-Za-z])(?=/|$)")
@@ -28,13 +29,130 @@ def _windows_path(token: str) -> Optional[str]:
28
29
  return ntpath.normpath(token)
29
30
 
30
31
 
32
+ _OPERATORS = frozenset("();&|<>")
33
+
34
+
35
+ def _heredoc_word(command: str, at: int) -> Tuple[str, int]:
36
+ """A here-document's delimiter word from `at` (blanks first), its quotes taken off as bash does, and where it ends."""
37
+ while at < len(command) and command[at] in " \t":
38
+ at += 1
39
+ word, quote = [], None
40
+ while at < len(command):
41
+ char = command[at]
42
+ if quote:
43
+ if char == quote:
44
+ quote = None
45
+ else:
46
+ word.append(char)
47
+ elif char in "'\"":
48
+ quote = char
49
+ elif char == "\\" and at + 1 < len(command):
50
+ at += 1
51
+ word.append(command[at])
52
+ elif char in " \t\n" or char in _OPERATORS:
53
+ break
54
+ else:
55
+ word.append(char)
56
+ at += 1
57
+ return "".join(word), at
58
+
59
+
60
+ def prepare(command: str) -> str:
61
+ """The command as bash reads its words, before the lexer (FAA-187): a `\\` and newline outside single quotes and
62
+ outside a comment joins the lines (an escaped backslash before a newline joins nothing, (7)); a comment, a `#` that
63
+ starts a word, is cut to its line's end (a `#` inside a word is the word's, (2)); a here-document's body and its
64
+ terminator line are cut out and the redirection left as a here-string, so a quote in a body never breaks the lexer
65
+ (5), with bash's own terminator rules: the delimiter word as written, its quotes taken off, `<<-` stripping leading
66
+ tabs, a plain `<<` matching only the bare line, and `<<` inside `((` `))` a shift (9); and a quoted word made only of
67
+ operator characters (`'&'`, `")"`) becomes a plain word, so the lexer never reads it as an operator (1)."""
68
+ out: List[str] = []
69
+ at, size, quote, word_start, arithmetic = 0, len(command), None, True, 0
70
+ pending: List[Tuple[str, bool]] = []
71
+ while at < size:
72
+ char = command[at]
73
+ if quote == "'":
74
+ out.append(char)
75
+ quote = None if char == "'" else quote
76
+ at += 1
77
+ continue
78
+ if char == "\\":
79
+ if command[at + 1:at + 2] == "\n":
80
+ at += 2 # a line continuation (outside single quotes)
81
+ continue
82
+ out.append(command[at:at + 2])
83
+ at, word_start = at + 2, False
84
+ continue
85
+ if quote == '"':
86
+ out.append(char)
87
+ quote = None if char == '"' else quote
88
+ at += 1
89
+ continue
90
+ if char in "'\"":
91
+ end = command.find(char, at + 1) if char == "'" else -1
92
+ if char == '"':
93
+ look = at + 1
94
+ while look < size and command[look] != '"':
95
+ look += 2 if command[look] == "\\" else 1
96
+ end = look if look < size else -1
97
+ inner = command[at + 1:end] if end > at else ""
98
+ after = command[end + 1:end + 2] if end > at else ""
99
+ if word_start and inner and set(inner) <= _OPERATORS and (after == "" or after in " \t\n" or after in _OPERATORS):
100
+ out.append("_") # a quoted operator is a word, never an operator (1)
101
+ at, word_start = end + 1, False
102
+ continue
103
+ quote, word_start = char, False
104
+ out.append(char)
105
+ at += 1
106
+ continue
107
+ if char == "#" and word_start:
108
+ while at < size and command[at] != "\n":
109
+ at += 1 # a comment, to its line's end; the newline stays
110
+ continue
111
+ if command.startswith("((", at):
112
+ arithmetic += 1
113
+ elif command.startswith("))", at) and arithmetic:
114
+ arithmetic -= 1
115
+ if command.startswith("<<<", at): # a here-string: its word is text, no body follows
116
+ out.append("<<<")
117
+ at, word_start = at + 3, True
118
+ continue
119
+ if command.startswith("<<", at) and not arithmetic:
120
+ strip = command[at + 2:at + 3] == "-"
121
+ word, end = _heredoc_word(command, at + 3 if strip else at + 2)
122
+ if word:
123
+ pending.append((word, strip))
124
+ out.append("<<< _ ") # the body is text for the command: a here-string stands in for it
125
+ at, word_start = end, True
126
+ continue
127
+ if char == "\n" and pending:
128
+ out.append("\n")
129
+ line_start = at + 1
130
+ for delimiter, strip in pending:
131
+ while line_start <= size:
132
+ line_end = command.find("\n", line_start)
133
+ line_end = size if line_end < 0 else line_end
134
+ line = command[line_start:line_end]
135
+ line_start = line_end + 1
136
+ if (line.lstrip("\t") if strip else line) == delimiter:
137
+ break
138
+ pending = []
139
+ at, word_start = line_start - 1, True # at the terminator line's newline: it ends the command
140
+ if at >= size:
141
+ break
142
+ continue
143
+ out.append(char)
144
+ word_start = char in " \t\n" or char in _OPERATORS
145
+ at += 1
146
+ return "".join(out)
147
+
148
+
31
149
  #: Words after which a command word may still stand (reserved words and the prefixes that run their command here).
32
150
  KEYWORDS = frozenset(("{", "!", "then", "do", "else", "elif", "if", "while", "until", "time", "command", "builtin"))
33
151
  ASSIGNMENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=")
34
152
 
35
153
 
36
154
  def bash_paths(command: str, cwd: Path, windows: Optional[bool] = None,
37
- ends: Optional[List[Optional[str]]] = None) -> List[str]:
155
+ ends: Optional[List[Optional[str]]] = None, fold_case: Optional[bool] = None) -> List[str]:
38
156
  """The folders a Bash command names as `cd <absolute path>` or `git -C <path>`, in order. A relative `-C` is read
39
157
  against the folder the command last changed into with an absolute `cd`, else `cwd`; after a `cd` to a relative or
40
158
  unknown folder it is dropped (FAA-166). A `cd` inside `( )`, a pipeline's stage or a list sent to the background
@@ -44,19 +162,22 @@ def bash_paths(command: str, cwd: Path, windows: Optional[bool] = None,
44
162
  `sys.platform == "win32"`) paths are read as Git Bash writes them, with its quoting: an unquoted `C:\\work` is
45
163
  `C:work` there too, so a native path counts when it is quoted (FAA-175); Cygwin and MSYS2 Pythons read their own
46
164
  POSIX paths (FAA-177). With `ends`, the folder the command leaves the shell in is appended to it: an absolute path,
47
- or None when it is not known (a relative cd, popd, an unclosed subshell, a command that does not split; FAA-184)."""
165
+ or None when it is not known (a relative cd, popd, an unclosed subshell, a command that does not split; FAA-184).
166
+ `prepare` reads comments, line continuations, here-documents and quoted operators first (FAA-187); a line ending
167
+ in `|`, `|&`, `&&` or `||` goes on on the next, as in bash (6); a `-C` word bash would expand (`$`, a backtick, a
168
+ leading `~`) is a folder not known here (8), and `-C ""` runs where the shell is; `git` is matched without case on
169
+ Windows and, with `fold_case` (default: on macOS), where the file system finds `GIT` as git (11). A `case`
170
+ pattern's `)` inside a subshell is still read as closing it."""
48
171
  import shlex # only a Stop hook with a transcript needs it
49
172
  windows = sys.platform == "win32" if windows is None else windows
173
+ fold_case = windows or (sys.platform == "darwin" if fold_case is None else fold_case)
50
174
  if windows:
51
175
  import ntpath as paths
52
176
  else:
53
177
  import posixpath as paths
54
178
  try:
55
- # A `\` before a newline joins the lines, as in bash; a `;` and a second newline after each newline keep a `#`
56
- # comment, which the lexer reads to the end of its line, newline included, from swallowing the next command or
57
- # the line end a here-document counts.
58
- command = command.replace("\\\n", "").replace("\n", "\n;\n")
59
- lexer = shlex.shlex(command, posix=True, punctuation_chars="();<>|&\n")
179
+ lexer = shlex.shlex(prepare(command), posix=True, punctuation_chars="();<>|&\n")
180
+ lexer.commenters = "" # prepare cut the comments, where bash reads them (a `#` inside a word is the word's)
60
181
  lexer.whitespace = " \t\r" # a newline ends a command, as `;` does
61
182
  lexer.whitespace_split = True
62
183
  tokens = list(lexer)
@@ -87,61 +208,39 @@ def bash_paths(command: str, cwd: Path, windows: Optional[bool] = None,
87
208
  base = pipe_start
88
209
  pipe_start, in_pipeline = base, False
89
210
 
90
- heredocs: List[str] = [] # the delimiters of here-documents whose bodies start at the next newline
91
-
92
- def past_heredocs(at: int) -> int:
93
- """The index of the newline ending the last pending here-document's delimiter line (or the end): the body is
94
- text for a command, never commands of its own."""
95
- while heredocs and at < len(tokens):
96
- line_end = at
97
- while line_end < len(tokens) and "\n" not in tokens[line_end]:
98
- line_end += 1
99
- if tokens[at:line_end] == [heredocs[0]]:
100
- heredocs.pop(0)
101
- if not heredocs:
102
- return line_end
103
- at = line_end + 1
104
- heredocs.clear()
105
- return len(tokens)
106
-
107
211
  start = True # a command word may stand here
212
+ continued = False # the last operator was `|`, `|&`, `&&` or `||`: a newline after it goes on (FAA-187 (6))
108
213
  index = 0
109
214
  while index < len(tokens):
110
215
  token = tokens[index]
111
216
  if token and set(token) <= separators:
112
- at, ends_command, newline = 0, False, False
217
+ at, ends_command = 0, False
113
218
  while at < len(token):
114
219
  pair, char = token[at:at + 2], token[at]
220
+ if char == "\n" and continued:
221
+ at += 1 # the line ended in a pipe or a list operator: the command goes on
222
+ continue
115
223
  if pair in ("&&", "||"):
116
224
  end_pipeline()
117
- at, ends_command = at + 2, True
225
+ at, ends_command, continued = at + 2, True, True
118
226
  continue
119
- if token.startswith("<<<", at): # a here-string: its word is text, no body follows
227
+ if token.startswith("<<<", at): # a here-string (prepare leaves one for a here-document): its word is text
120
228
  at += 3
121
229
  continue
122
- if pair == "<<" and index + 1 < len(tokens):
123
- delimiter = tokens[index + 1] # <<EOF, <<'EOF', <<-EOF: shlex has taken the quotes off
124
- if delimiter == "-" and index + 2 < len(tokens):
125
- index, delimiter = index + 1, tokens[index + 2]
126
- heredocs.append(delimiter[1:] if delimiter.startswith("-") and len(delimiter) > 1 else delimiter)
127
- index += 1 # the delimiter is no command word
128
- at += 2
129
- continue
130
230
  if pair == "&>": # `&>` and `&>>` redirect, they background nothing
131
231
  at += 3 if token[at + 2:at + 3] == ">" else 2
132
232
  continue
133
233
  if char in "<>": # a redirection: `2>&1` backgrounds nothing, `>|` pipes nothing
134
234
  at += 2 if token[at + 1:at + 2] in ("&", "|") else 1
135
235
  continue
136
- ends_command = True
236
+ ends_command, continued = True, False
137
237
  if pair == "|&" or char == "|":
138
- base, in_pipeline = pipe_start, True
238
+ base, in_pipeline, continued = pipe_start, True, True
139
239
  at += 2 if pair == "|&" else 1
140
240
  continue
141
241
  if char in ";\n":
142
242
  end_pipeline()
143
243
  list_start = base
144
- newline = newline or char == "\n"
145
244
  elif char == "&":
146
245
  end_pipeline()
147
246
  base = pipe_start = list_start
@@ -155,8 +254,9 @@ def bash_paths(command: str, cwd: Path, windows: Optional[bool] = None,
155
254
  base, pipe_start, list_start, in_pipeline = stack.pop()
156
255
  at += 1
157
256
  start = start or ends_command # a redirection alone starts no command
158
- index = past_heredocs(index + 1) if newline and heredocs else index + 1
257
+ index += 1
159
258
  continue
259
+ continued = False
160
260
  if start and (token in KEYWORDS or ASSIGNMENT.match(token)):
161
261
  index += 1 # `{ cd /a; }`, `then cd`, `x=1 cd`, `command cd`: a command word may still follow
162
262
  continue
@@ -172,14 +272,16 @@ def bash_paths(command: str, cwd: Path, windows: Optional[bool] = None,
172
272
  if folder:
173
273
  found.append(folder)
174
274
  base = folder
175
- elif start and (paths.basename(token).lower() if windows else paths.basename(token)) in ("git", "git.exe"):
275
+ elif start and (paths.basename(token).lower() if fold_case else paths.basename(token)) in ("git", "git.exe"):
176
276
  look, at, moved = index + 1, base, False
177
277
  while look < len(tokens) and tokens[look].startswith("-") and not set(tokens[look]) <= separators:
178
- if tokens[look] == "-C" and look + 1 < len(tokens) and not set(tokens[look + 1]) <= separators:
179
- # git reads each relative -C against the one before it and runs in the last
180
- folder = absolute(tokens[look + 1])
181
- if folder is None and at is not None and not tokens[look + 1].startswith("/"):
182
- folder = paths.normpath(paths.join(at, tokens[look + 1]))
278
+ if tokens[look] == "-C" and look + 1 < len(tokens) and (tokens[look + 1] == "" or not set(tokens[look + 1]) <= separators):
279
+ # git reads each relative -C against the one before it and runs in the last; `-C ""` where it is
280
+ word = tokens[look + 1]
281
+ expands = "$" in word or "`" in word or word.startswith("~") # a folder not known here (FAA-187 (8))
282
+ folder = None if expands else absolute(word)
283
+ if folder is None and not expands and at is not None and not word.startswith("/"):
284
+ folder = paths.normpath(paths.join(at, word))
183
285
  if windows and not (paths.isabs(folder) and paths.splitdrive(folder)[0]):
184
286
  folder = None # `C:work`, a drive-relative path: not known here
185
287
  at, moved = folder, True