dotdo 0.0.2 → 0.1.0

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 (313) hide show
  1. package/cli/README.md +238 -0
  2. package/cli/agent.ts +72 -0
  3. package/cli/bin.js +44 -0
  4. package/cli/bin.ts +38 -0
  5. package/cli/build.ts +157 -0
  6. package/cli/commands/auth/login.ts +14 -0
  7. package/cli/commands/auth/logout.ts +6 -0
  8. package/cli/commands/auth/whoami.ts +16 -0
  9. package/cli/commands/deploy-multi.ts +245 -0
  10. package/cli/commands/dev/deploy.ts +100 -0
  11. package/cli/commands/dev/dev.ts +95 -0
  12. package/cli/commands/dev/logs.ts +91 -0
  13. package/cli/commands/dev-local.ts +88 -0
  14. package/cli/commands/do-ops.ts +314 -0
  15. package/cli/commands/index.ts +100 -0
  16. package/cli/commands/init.ts +247 -0
  17. package/cli/commands/introspect/emitter.ts +315 -0
  18. package/cli/commands/introspect/index.ts +193 -0
  19. package/cli/commands/link.ts +598 -0
  20. package/cli/commands/snippets.ts +415 -0
  21. package/cli/commands/tunnel.ts +239 -0
  22. package/cli/device-auth.ts +289 -0
  23. package/cli/fallback.ts +12 -0
  24. package/cli/index.ts +121 -0
  25. package/cli/main.ts +246 -0
  26. package/cli/mcp-stdio.ts +790 -0
  27. package/cli/package.json +62 -0
  28. package/cli/runtime/do-registry.ts +193 -0
  29. package/cli/runtime/embedded-db.ts +344 -0
  30. package/cli/runtime/index.ts +9 -0
  31. package/cli/runtime/miniflare-adapter.ts +162 -0
  32. package/cli/sandbox.ts +82 -0
  33. package/cli/src/args.ts +174 -0
  34. package/cli/src/auth.ts +55 -0
  35. package/cli/src/commands/call.ts +84 -0
  36. package/cli/src/commands/charge.ts +96 -0
  37. package/cli/src/commands/config.ts +115 -0
  38. package/cli/src/commands/email.ts +112 -0
  39. package/cli/src/commands/llm.ts +115 -0
  40. package/cli/src/commands/queue.ts +134 -0
  41. package/cli/src/commands/text.ts +86 -0
  42. package/cli/src/config.ts +185 -0
  43. package/cli/src/output.ts +246 -0
  44. package/cli/src/rpc.ts +192 -0
  45. package/cli/utils/config.ts +282 -0
  46. package/cli/utils/detect.ts +73 -0
  47. package/cli/utils/index.ts +15 -0
  48. package/cli/utils/logger.ts +232 -0
  49. package/dist/ai/template-literals.js +2 -2
  50. package/dist/ai/template-literals.js.map +1 -1
  51. package/dist/api/middleware/auth.js +3 -2
  52. package/dist/api/middleware/auth.js.map +1 -1
  53. package/dist/db/iceberg/inverted-index.js +1 -1
  54. package/dist/db/iceberg/inverted-index.js.map +1 -1
  55. package/dist/db/iceberg/puffin.js.map +1 -1
  56. package/dist/db/json-indexes.js.map +1 -1
  57. package/dist/db/objects.js.map +1 -1
  58. package/dist/db/primitives/dag-scheduler/index.js +1 -1
  59. package/dist/db/primitives/dag-scheduler/index.js.map +1 -1
  60. package/dist/db/primitives/observability.js.map +1 -1
  61. package/dist/db/primitives/schema-evolution.js.map +1 -1
  62. package/dist/db/primitives/temporal-store.js.map +1 -1
  63. package/dist/db/primitives/typed-column-store.js.map +1 -1
  64. package/dist/db/primitives/utils/duration.js.map +1 -1
  65. package/dist/db/primitives/utils/murmur3.js +12 -14
  66. package/dist/db/primitives/utils/murmur3.js.map +1 -1
  67. package/dist/db/primitives/window-manager.js.map +1 -1
  68. package/dist/db/stores.js.map +1 -1
  69. package/dist/db/things.js.map +1 -1
  70. package/dist/lib/DODispatcher.js +2 -2
  71. package/dist/lib/DODispatcher.js.map +1 -1
  72. package/dist/lib/auto-wiring.js.map +1 -1
  73. package/dist/lib/channels/email.js +1 -1
  74. package/dist/lib/channels/email.js.map +1 -1
  75. package/dist/lib/channels/slack-blockkit.js.map +1 -1
  76. package/dist/lib/cloudflare/ai.js +1 -1
  77. package/dist/lib/cloudflare/ai.js.map +1 -1
  78. package/dist/lib/cloudflare/kv.js +1 -1
  79. package/dist/lib/cloudflare/kv.js.map +1 -1
  80. package/dist/lib/cloudflare/r2.js +3 -3
  81. package/dist/lib/cloudflare/r2.js.map +1 -1
  82. package/dist/lib/cloudflare/vectorize.js.map +1 -1
  83. package/dist/lib/cloudflare/workflows.js.map +1 -1
  84. package/dist/lib/executors/AgenticFunctionExecutor.js.map +1 -1
  85. package/dist/lib/executors/CodeFunctionExecutor.js.map +1 -1
  86. package/dist/lib/executors/GenerativeFunctionExecutor.js.map +1 -1
  87. package/dist/lib/executors/HumanFunctionExecutor.js +1 -1
  88. package/dist/lib/executors/HumanFunctionExecutor.js.map +1 -1
  89. package/dist/lib/executors/ParallelStepExecutor.js.map +1 -1
  90. package/dist/lib/experiments.js.map +1 -1
  91. package/dist/lib/flags/store.js.map +1 -1
  92. package/dist/lib/functions/FunctionComposition.js.map +1 -1
  93. package/dist/lib/functions/FunctionMiddleware.js.map +1 -1
  94. package/dist/lib/functions/FunctionRegistry.js.map +1 -1
  95. package/dist/lib/humans/templates.js.map +1 -1
  96. package/dist/lib/identity.js +2 -2
  97. package/dist/lib/identity.js.map +1 -1
  98. package/dist/lib/logging/index.js.map +1 -1
  99. package/dist/lib/mixins/bash.js +1 -73
  100. package/dist/lib/mixins/bash.js.map +1 -1
  101. package/dist/lib/mixins/git.js +0 -5
  102. package/dist/lib/mixins/git.js.map +1 -1
  103. package/dist/lib/mixins/npm.js.map +1 -1
  104. package/dist/lib/noun-id.js.map +1 -1
  105. package/dist/lib/rate-limit/sliding-window.js.map +1 -1
  106. package/dist/lib/rpc/bindings.js.map +1 -1
  107. package/dist/lib/safe-stringify.js.map +1 -1
  108. package/dist/lib/sandbox/miniflare-sandbox.js.map +1 -1
  109. package/dist/lib/sqids.js.map +1 -1
  110. package/dist/lib/sql/adapters/node-sql-parser.js.map +1 -1
  111. package/dist/lib/sql/adapters/pgsql-parser.js +19 -18
  112. package/dist/lib/sql/adapters/pgsql-parser.js.map +1 -1
  113. package/dist/metrics/hunch.js.map +1 -1
  114. package/dist/objects/API.js +1 -1
  115. package/dist/objects/API.js.map +1 -1
  116. package/dist/objects/Agent.js.map +1 -1
  117. package/dist/objects/Browser.js.map +1 -1
  118. package/dist/objects/CLI.js.map +1 -1
  119. package/dist/objects/DOBase.js.map +1 -1
  120. package/dist/objects/DOCache.js +153 -0
  121. package/dist/objects/DOCache.js.map +1 -0
  122. package/dist/objects/DOFull.js.map +1 -1
  123. package/dist/objects/Entity.js.map +1 -1
  124. package/dist/objects/Human.js.map +1 -1
  125. package/dist/objects/IcebergMetadataDO.js.map +1 -1
  126. package/dist/objects/IntegrationsDO.js.map +1 -1
  127. package/dist/objects/ObservabilityBroadcaster.js.map +1 -1
  128. package/dist/objects/Package.js.map +1 -1
  129. package/dist/objects/Product.js +1 -1
  130. package/dist/objects/Product.js.map +1 -1
  131. package/dist/objects/SaaS.js.map +1 -1
  132. package/dist/objects/SandboxDO.js.map +1 -1
  133. package/dist/objects/Service.js.map +1 -1
  134. package/dist/objects/VectorShardDO.js +9 -7
  135. package/dist/objects/VectorShardDO.js.map +1 -1
  136. package/dist/objects/Workflow.js.map +1 -1
  137. package/dist/objects/WorkflowFactory.js.map +1 -1
  138. package/dist/objects/WorkflowRuntime.js.map +1 -1
  139. package/dist/objects/lifecycle/Branch.js.map +1 -1
  140. package/dist/objects/lifecycle/Clone.js +1 -1
  141. package/dist/objects/lifecycle/Clone.js.map +1 -1
  142. package/dist/objects/lifecycle/Compact.js.map +1 -1
  143. package/dist/objects/lifecycle/Shard.js.map +1 -1
  144. package/dist/objects/persistence/checkpoint-manager.js.map +1 -1
  145. package/dist/objects/persistence/migration-runner.js.map +1 -1
  146. package/dist/objects/persistence/replication-manager.js +2 -2
  147. package/dist/objects/persistence/replication-manager.js.map +1 -1
  148. package/dist/objects/persistence/tiered-storage-manager.js.map +1 -1
  149. package/dist/objects/persistence/wal-manager.js.map +1 -1
  150. package/dist/objects/transport/auth-layer.js.map +1 -1
  151. package/dist/objects/transport/chain.js.map +1 -1
  152. package/dist/objects/transport/mcp-server.js +7 -6
  153. package/dist/objects/transport/mcp-server.js.map +1 -1
  154. package/dist/objects/transport/rest-autowire.js +3 -2
  155. package/dist/objects/transport/rest-autowire.js.map +1 -1
  156. package/dist/objects/transport/rest-router.js.map +1 -1
  157. package/dist/objects/transport/rpc-server.js +18 -15
  158. package/dist/objects/transport/rpc-server.js.map +1 -1
  159. package/dist/objects/transport/shared.js +2 -1
  160. package/dist/objects/transport/shared.js.map +1 -1
  161. package/dist/snippets/artifacts-ingest.js.map +1 -1
  162. package/dist/snippets/artifacts-serve.js.map +1 -1
  163. package/dist/snippets/search.js.map +1 -1
  164. package/dist/workflows/ScheduleManager.js.map +1 -1
  165. package/dist/workflows/StepResultStorage.js.map +1 -1
  166. package/dist/workflows/WaitForEventManager.js.map +1 -1
  167. package/dist/workflows/compat/backends/cloudflare-workflows.js.map +1 -1
  168. package/dist/workflows/compat/inngest/index.js.map +1 -1
  169. package/dist/workflows/compat/qstash/index.js.map +1 -1
  170. package/dist/workflows/compat/temporal/client.js.map +1 -1
  171. package/dist/workflows/compat/temporal/index.js.map +1 -1
  172. package/dist/workflows/compat/trigger/index.js.map +1 -1
  173. package/dist/workflows/compat/utils/index.js.map +1 -1
  174. package/dist/workflows/context/correlation.js +2 -2
  175. package/dist/workflows/context/correlation.js.map +1 -1
  176. package/dist/workflows/context/experiment.js +1 -1
  177. package/dist/workflows/context/experiment.js.map +1 -1
  178. package/dist/workflows/context/flag.js +1 -1
  179. package/dist/workflows/context/flag.js.map +1 -1
  180. package/dist/workflows/context/measure.js +1 -1
  181. package/dist/workflows/context/measure.js.map +1 -1
  182. package/dist/workflows/context/rate-limit.js.map +1 -1
  183. package/dist/workflows/data/entity-events/entity-events.js.map +1 -1
  184. package/dist/workflows/data/experiment/index.js.map +1 -1
  185. package/dist/workflows/data/goal/context.js +1 -1
  186. package/dist/workflows/data/goal/context.js.map +1 -1
  187. package/dist/workflows/data/measure/index.js +1 -1
  188. package/dist/workflows/data/measure/index.js.map +1 -1
  189. package/dist/workflows/data/stream/index.js +10 -76
  190. package/dist/workflows/data/stream/index.js.map +1 -1
  191. package/dist/workflows/data/track/context.js.map +1 -1
  192. package/dist/workflows/data/view/context.js.map +1 -1
  193. package/dist/workflows/domain.js.map +1 -1
  194. package/dist/workflows/flags.js +1 -1
  195. package/dist/workflows/flags.js.map +1 -1
  196. package/dist/workflows/hash.js.map +1 -1
  197. package/dist/workflows/on.js +1 -1
  198. package/dist/workflows/on.js.map +1 -1
  199. package/dist/workflows/schedule-builder.js.map +1 -1
  200. package/dist/workflows/visibility/index.js +0 -2
  201. package/dist/workflows/visibility/index.js.map +1 -1
  202. package/dist/workflows/visibility/query-parser.js.map +1 -1
  203. package/package.json +18 -3
  204. package/dist/api/analytics/router.js +0 -601
  205. package/dist/api/analytics/router.js.map +0 -1
  206. package/dist/api/index.js +0 -158
  207. package/dist/api/index.js.map +0 -1
  208. package/dist/api/middleware/error-handling.js +0 -176
  209. package/dist/api/middleware/error-handling.js.map +0 -1
  210. package/dist/api/middleware/request-id.js +0 -21
  211. package/dist/api/middleware/request-id.js.map +0 -1
  212. package/dist/api/pages.js +0 -1180
  213. package/dist/api/pages.js.map +0 -1
  214. package/dist/api/routes/api.js +0 -612
  215. package/dist/api/routes/api.js.map +0 -1
  216. package/dist/api/routes/browsers.js +0 -471
  217. package/dist/api/routes/browsers.js.map +0 -1
  218. package/dist/api/routes/do.js +0 -188
  219. package/dist/api/routes/do.js.map +0 -1
  220. package/dist/api/routes/mcp.js +0 -459
  221. package/dist/api/routes/mcp.js.map +0 -1
  222. package/dist/api/routes/obs.js +0 -445
  223. package/dist/api/routes/obs.js.map +0 -1
  224. package/dist/api/routes/openapi.js +0 -794
  225. package/dist/api/routes/openapi.js.map +0 -1
  226. package/dist/api/routes/rpc.js +0 -1103
  227. package/dist/api/routes/rpc.js.map +0 -1
  228. package/dist/api/routes/sandboxes.js +0 -389
  229. package/dist/api/routes/sandboxes.js.map +0 -1
  230. package/dist/api/test-do.js +0 -38
  231. package/dist/api/test-do.js.map +0 -1
  232. package/dist/api/types.js +0 -11
  233. package/dist/api/types.js.map +0 -1
  234. package/dist/cli/bin.js +0 -2
  235. package/dist/cli/main.js +0 -52342
  236. package/dist/do/bash.js +0 -35
  237. package/dist/do/bash.js.map +0 -1
  238. package/dist/do/fs.js +0 -25
  239. package/dist/do/fs.js.map +0 -1
  240. package/dist/do/full.js +0 -61
  241. package/dist/do/full.js.map +0 -1
  242. package/dist/do/git.js +0 -28
  243. package/dist/do/git.js.map +0 -1
  244. package/dist/do/index.js +0 -52
  245. package/dist/do/index.js.map +0 -1
  246. package/dist/lib/agent/tools/bash.js +0 -336
  247. package/dist/lib/agent/tools/bash.js.map +0 -1
  248. package/dist/lib/agent/tools/edit.js +0 -157
  249. package/dist/lib/agent/tools/edit.js.map +0 -1
  250. package/dist/lib/agent/tools/glob.js +0 -137
  251. package/dist/lib/agent/tools/glob.js.map +0 -1
  252. package/dist/lib/agent/tools/grep.js +0 -315
  253. package/dist/lib/agent/tools/grep.js.map +0 -1
  254. package/dist/lib/agent/tools/index.js +0 -71
  255. package/dist/lib/agent/tools/index.js.map +0 -1
  256. package/dist/lib/agent/tools/read.js +0 -212
  257. package/dist/lib/agent/tools/read.js.map +0 -1
  258. package/dist/lib/agent/tools/types.js +0 -197
  259. package/dist/lib/agent/tools/types.js.map +0 -1
  260. package/dist/lib/agent/tools/write.js +0 -159
  261. package/dist/lib/agent/tools/write.js.map +0 -1
  262. package/dist/lib/mixins/index.js +0 -29
  263. package/dist/lib/mixins/index.js.map +0 -1
  264. package/dist/primitives/bashx/src/ast/analyze.js +0 -1472
  265. package/dist/primitives/bashx/src/ast/analyze.js.map +0 -1
  266. package/dist/primitives/bashx/src/ast/parser.js +0 -1488
  267. package/dist/primitives/bashx/src/ast/parser.js.map +0 -1
  268. package/dist/primitives/bashx/src/do/commands/crypto.js +0 -1954
  269. package/dist/primitives/bashx/src/do/commands/crypto.js.map +0 -1
  270. package/dist/primitives/bashx/src/do/commands/data-processing.js +0 -1812
  271. package/dist/primitives/bashx/src/do/commands/data-processing.js.map +0 -1
  272. package/dist/primitives/bashx/src/do/commands/extended-utils.js +0 -804
  273. package/dist/primitives/bashx/src/do/commands/extended-utils.js.map +0 -1
  274. package/dist/primitives/bashx/src/do/commands/math-control.js +0 -1122
  275. package/dist/primitives/bashx/src/do/commands/math-control.js.map +0 -1
  276. package/dist/primitives/bashx/src/do/commands/posix-utils.js +0 -1015
  277. package/dist/primitives/bashx/src/do/commands/posix-utils.js.map +0 -1
  278. package/dist/primitives/bashx/src/do/commands/system-utils.js +0 -687
  279. package/dist/primitives/bashx/src/do/commands/system-utils.js.map +0 -1
  280. package/dist/primitives/bashx/src/do/commands/test-command.js +0 -523
  281. package/dist/primitives/bashx/src/do/commands/test-command.js.map +0 -1
  282. package/dist/primitives/bashx/src/do/commands/text-processing.js +0 -1550
  283. package/dist/primitives/bashx/src/do/commands/text-processing.js.map +0 -1
  284. package/dist/primitives/bashx/src/do/container-executor.js +0 -429
  285. package/dist/primitives/bashx/src/do/container-executor.js.map +0 -1
  286. package/dist/primitives/bashx/src/do/index.js +0 -668
  287. package/dist/primitives/bashx/src/do/index.js.map +0 -1
  288. package/dist/primitives/bashx/src/do/tiered-executor.js +0 -2647
  289. package/dist/primitives/bashx/src/do/tiered-executor.js.map +0 -1
  290. package/dist/primitives/bashx/src/do/worker.js +0 -352
  291. package/dist/primitives/bashx/src/do/worker.js.map +0 -1
  292. package/dist/primitives/bashx/src/types.js +0 -10
  293. package/dist/primitives/bashx/src/types.js.map +0 -1
  294. package/dist/primitives/fsx/core/backend.js +0 -480
  295. package/dist/primitives/fsx/core/backend.js.map +0 -1
  296. package/dist/primitives/fsx/core/constants.js +0 -140
  297. package/dist/primitives/fsx/core/constants.js.map +0 -1
  298. package/dist/primitives/fsx/core/fsx.js +0 -1184
  299. package/dist/primitives/fsx/core/fsx.js.map +0 -1
  300. package/dist/primitives/fsx/core/glob/glob.js +0 -438
  301. package/dist/primitives/fsx/core/glob/glob.js.map +0 -1
  302. package/dist/primitives/fsx/core/glob/index.js +0 -8
  303. package/dist/primitives/fsx/core/glob/index.js.map +0 -1
  304. package/dist/primitives/fsx/core/glob/match.js +0 -392
  305. package/dist/primitives/fsx/core/glob/match.js.map +0 -1
  306. package/dist/primitives/fsx/core/types.js +0 -307
  307. package/dist/primitives/fsx/core/types.js.map +0 -1
  308. package/dist/sdk/capnweb-compat.js +0 -42
  309. package/dist/sdk/capnweb-compat.js.map +0 -1
  310. package/dist/sdk/client.js +0 -20
  311. package/dist/sdk/client.js.map +0 -1
  312. package/dist/sdk/index.js +0 -17
  313. package/dist/sdk/index.js.map +0 -1
@@ -1,1488 +0,0 @@
1
- /**
2
- * Bash AST Parser
3
- *
4
- * A lightweight parser for bash commands that detects common syntax errors.
5
- * Implements error detection for:
6
- * - Unclosed quotes (single and double)
7
- * - Missing terminators (fi, done, esac)
8
- * - Unbalanced brackets/braces
9
- * - Invalid pipe/redirect syntax
10
- */
11
- // ============================================================================
12
- // Lexer
13
- // ============================================================================
14
- class Lexer {
15
- input;
16
- pos = 0;
17
- line = 1;
18
- column = 1;
19
- errors = [];
20
- constructor(input) {
21
- this.input = input;
22
- }
23
- peek(offset = 0) {
24
- return this.input[this.pos + offset] ?? '';
25
- }
26
- advance() {
27
- const ch = this.input[this.pos] ?? '';
28
- this.pos++;
29
- if (ch === '\n') {
30
- this.line++;
31
- this.column = 1;
32
- }
33
- else {
34
- this.column++;
35
- }
36
- return ch;
37
- }
38
- skipWhitespace() {
39
- while (this.pos < this.input.length) {
40
- const ch = this.peek();
41
- if (ch === ' ' || ch === '\t') {
42
- this.advance();
43
- }
44
- else if (ch === '\\' && this.peek(1) === '\n') {
45
- // Line continuation
46
- this.advance();
47
- this.advance();
48
- }
49
- else {
50
- break;
51
- }
52
- }
53
- }
54
- isWordChar(ch) {
55
- // Not a special character
56
- return ch !== '' &&
57
- ch !== ' ' && ch !== '\t' && ch !== '\n' &&
58
- ch !== '|' && ch !== '&' && ch !== ';' &&
59
- ch !== '(' && ch !== ')' && ch !== '{' && ch !== '}' &&
60
- ch !== '<' && ch !== '>' && ch !== '#';
61
- }
62
- readQuotedString(quote) {
63
- const startLine = this.line;
64
- const startColumn = this.column;
65
- let result = quote;
66
- this.advance(); // consume opening quote
67
- while (this.pos < this.input.length) {
68
- const ch = this.peek();
69
- if (ch === quote) {
70
- result += this.advance();
71
- return result;
72
- }
73
- if (ch === '\\' && quote === '"') {
74
- result += this.advance();
75
- if (this.pos < this.input.length) {
76
- result += this.advance();
77
- }
78
- }
79
- else if (ch === '$' && quote === '"') {
80
- // Handle parameter expansion and command/arithmetic substitution inside double quotes
81
- result += this.readParameterExpansion();
82
- }
83
- else {
84
- result += this.advance();
85
- }
86
- }
87
- // Unclosed quote
88
- const quoteType = quote === '"' ? 'double' : 'single';
89
- this.errors.push({
90
- message: `Unclosed ${quoteType} quote`,
91
- line: startLine,
92
- column: startColumn,
93
- suggestion: `Add closing ${quote} at the end`,
94
- });
95
- return result;
96
- }
97
- /**
98
- * Read a quoted string inside a parameter expansion - doesn't report errors itself
99
- */
100
- readQuotedStringInExpansion(quote) {
101
- let result = quote;
102
- this.advance(); // consume opening quote
103
- while (this.pos < this.input.length) {
104
- const ch = this.peek();
105
- if (ch === quote) {
106
- result += this.advance();
107
- return result;
108
- }
109
- if (ch === '\\' && quote === '"') {
110
- result += this.advance();
111
- if (this.pos < this.input.length) {
112
- result += this.advance();
113
- }
114
- }
115
- else {
116
- result += this.advance();
117
- }
118
- }
119
- // Return without the closing quote - parent will detect
120
- return result;
121
- }
122
- readParameterExpansion() {
123
- const startLine = this.line;
124
- const startColumn = this.column;
125
- let result = '$';
126
- this.advance(); // consume $
127
- const next = this.peek();
128
- if (next === '(') {
129
- // Command substitution $(...) or arithmetic $((...))
130
- result += this.advance(); // consume (
131
- if (this.peek() === '(') {
132
- // Arithmetic expansion $((
133
- result += this.advance(); // consume second (
134
- let depth = 2;
135
- let foundClosing = false;
136
- while (this.pos < this.input.length) {
137
- const ch = this.peek();
138
- if (ch === '(') {
139
- depth++;
140
- result += this.advance();
141
- }
142
- else if (ch === ')') {
143
- depth--;
144
- result += this.advance();
145
- if (depth === 0) {
146
- foundClosing = true;
147
- break;
148
- }
149
- }
150
- else {
151
- result += this.advance();
152
- }
153
- }
154
- if (!foundClosing) {
155
- this.errors.push({
156
- message: 'Unclosed arithmetic expansion $((',
157
- line: startLine,
158
- column: startColumn,
159
- suggestion: 'Add closing )) at the end',
160
- });
161
- }
162
- }
163
- else {
164
- // Command substitution $(
165
- let depth = 1;
166
- let foundClosing = false;
167
- while (this.pos < this.input.length) {
168
- const ch = this.peek();
169
- if (ch === '"' || ch === "'") {
170
- result += this.readQuotedString(ch);
171
- }
172
- else if (ch === '(') {
173
- depth++;
174
- result += this.advance();
175
- }
176
- else if (ch === ')') {
177
- depth--;
178
- result += this.advance();
179
- if (depth === 0) {
180
- foundClosing = true;
181
- break;
182
- }
183
- }
184
- else {
185
- result += this.advance();
186
- }
187
- }
188
- if (!foundClosing) {
189
- this.errors.push({
190
- message: 'Unclosed command substitution $(',
191
- line: startLine,
192
- column: startColumn,
193
- suggestion: 'Add closing ) at the end',
194
- });
195
- }
196
- }
197
- }
198
- else if (next === '{') {
199
- // Parameter expansion ${...}
200
- result += this.advance(); // consume {
201
- let depth = 1;
202
- let foundClosing = false;
203
- let hasUnclosedQuote = false;
204
- while (this.pos < this.input.length) {
205
- const ch = this.peek();
206
- if (ch === '"' || ch === "'") {
207
- const quotedStr = this.readQuotedStringInExpansion(ch);
208
- result += quotedStr;
209
- // Check if quote was unclosed (doesn't end with the same quote char it started with)
210
- if (!quotedStr.endsWith(ch)) {
211
- hasUnclosedQuote = true;
212
- }
213
- }
214
- else if (ch === '{') {
215
- depth++;
216
- result += this.advance();
217
- }
218
- else if (ch === '}') {
219
- depth--;
220
- result += this.advance();
221
- if (depth === 0) {
222
- foundClosing = true;
223
- break;
224
- }
225
- }
226
- else {
227
- result += this.advance();
228
- }
229
- }
230
- if (!foundClosing) {
231
- if (hasUnclosedQuote) {
232
- this.errors.push({
233
- message: 'Unclosed quote in parameter expansion',
234
- line: startLine,
235
- column: startColumn,
236
- suggestion: 'Add closing quote and } at the end',
237
- });
238
- }
239
- else {
240
- this.errors.push({
241
- message: 'Unclosed brace in parameter expansion ${',
242
- line: startLine,
243
- column: startColumn,
244
- suggestion: 'Add closing } at the end',
245
- });
246
- }
247
- }
248
- }
249
- else {
250
- // Simple variable $VAR
251
- while (this.pos < this.input.length) {
252
- const ch = this.peek();
253
- if (/[a-zA-Z0-9_]/.test(ch)) {
254
- result += this.advance();
255
- }
256
- else {
257
- break;
258
- }
259
- }
260
- }
261
- return result;
262
- }
263
- readWord() {
264
- let result = '';
265
- while (this.pos < this.input.length) {
266
- const ch = this.peek();
267
- if (ch === '"' || ch === "'") {
268
- result += this.readQuotedString(ch);
269
- }
270
- else if (ch === '$') {
271
- result += this.readParameterExpansion();
272
- }
273
- else if (ch === '\\' && this.peek(1) !== '') {
274
- // Escape sequence
275
- result += this.advance();
276
- result += this.advance();
277
- }
278
- else if (ch === '`') {
279
- // Backtick command substitution
280
- const startLine = this.line;
281
- const startColumn = this.column;
282
- result += this.advance();
283
- let foundClosing = false;
284
- while (this.pos < this.input.length) {
285
- const c = this.peek();
286
- if (c === '`') {
287
- result += this.advance();
288
- foundClosing = true;
289
- break;
290
- }
291
- else if (c === '\\') {
292
- result += this.advance();
293
- if (this.pos < this.input.length) {
294
- result += this.advance();
295
- }
296
- }
297
- else {
298
- result += this.advance();
299
- }
300
- }
301
- if (!foundClosing) {
302
- this.errors.push({
303
- message: 'Unclosed backtick command substitution',
304
- line: startLine,
305
- column: startColumn,
306
- suggestion: 'Add closing ` at the end',
307
- });
308
- }
309
- }
310
- else if (this.isWordChar(ch)) {
311
- result += this.advance();
312
- }
313
- else {
314
- break;
315
- }
316
- }
317
- return result;
318
- }
319
- nextToken() {
320
- this.skipWhitespace();
321
- if (this.pos >= this.input.length) {
322
- return { type: 'EOF', value: '', line: this.line, column: this.column };
323
- }
324
- const startLine = this.line;
325
- const startColumn = this.column;
326
- const ch = this.peek();
327
- // Comments
328
- if (ch === '#') {
329
- while (this.pos < this.input.length && this.peek() !== '\n') {
330
- this.advance();
331
- }
332
- return this.nextToken();
333
- }
334
- // Newline
335
- if (ch === '\n') {
336
- this.advance();
337
- return { type: 'NEWLINE', value: '\n', line: startLine, column: startColumn };
338
- }
339
- // Semicolons and case pattern end
340
- if (ch === ';') {
341
- this.advance();
342
- if (this.peek() === ';') {
343
- this.advance();
344
- return { type: 'CASE_PATTERN_END', value: ';;', line: startLine, column: startColumn };
345
- }
346
- return { type: 'SEMICOLON', value: ';', line: startLine, column: startColumn };
347
- }
348
- // Pipes and logical operators
349
- if (ch === '|') {
350
- this.advance();
351
- if (this.peek() === '|') {
352
- this.advance();
353
- return { type: 'OR', value: '||', line: startLine, column: startColumn };
354
- }
355
- return { type: 'PIPE', value: '|', line: startLine, column: startColumn };
356
- }
357
- // And/background/combined redirect
358
- if (ch === '&') {
359
- this.advance();
360
- if (this.peek() === '&') {
361
- this.advance();
362
- return { type: 'AND', value: '&&', line: startLine, column: startColumn };
363
- }
364
- if (this.peek() === '>') {
365
- this.advance();
366
- // &> redirects both stdout and stderr
367
- return { type: 'REDIRECT_BOTH', value: '&>', line: startLine, column: startColumn };
368
- }
369
- return { type: 'BACKGROUND', value: '&', line: startLine, column: startColumn };
370
- }
371
- // Parentheses
372
- if (ch === '(') {
373
- this.advance();
374
- return { type: 'LPAREN', value: '(', line: startLine, column: startColumn };
375
- }
376
- if (ch === ')') {
377
- this.advance();
378
- return { type: 'RPAREN', value: ')', line: startLine, column: startColumn };
379
- }
380
- // Braces
381
- if (ch === '{') {
382
- this.advance();
383
- return { type: 'LBRACE', value: '{', line: startLine, column: startColumn };
384
- }
385
- if (ch === '}') {
386
- this.advance();
387
- return { type: 'RBRACE', value: '}', line: startLine, column: startColumn };
388
- }
389
- // Brackets
390
- if (ch === '[') {
391
- this.advance();
392
- if (this.peek() === '[') {
393
- this.advance();
394
- return { type: 'DOUBLE_LBRACKET', value: '[[', line: startLine, column: startColumn };
395
- }
396
- return { type: 'LBRACKET', value: '[', line: startLine, column: startColumn };
397
- }
398
- if (ch === ']') {
399
- this.advance();
400
- if (this.peek() === ']') {
401
- this.advance();
402
- return { type: 'DOUBLE_RBRACKET', value: ']]', line: startLine, column: startColumn };
403
- }
404
- return { type: 'RBRACKET', value: ']', line: startLine, column: startColumn };
405
- }
406
- // Redirects
407
- if (ch === '>') {
408
- this.advance();
409
- if (this.peek() === '>') {
410
- this.advance();
411
- return { type: 'REDIRECT_APPEND', value: '>>', line: startLine, column: startColumn };
412
- }
413
- if (this.peek() === '&') {
414
- this.advance();
415
- return { type: 'REDIRECT_OUT', value: '>&', line: startLine, column: startColumn };
416
- }
417
- return { type: 'REDIRECT_OUT', value: '>', line: startLine, column: startColumn };
418
- }
419
- if (ch === '<') {
420
- this.advance();
421
- if (this.peek() === '<') {
422
- this.advance();
423
- if (this.peek() === '<') {
424
- this.advance();
425
- return { type: 'REDIRECT_HERESTRING', value: '<<<', line: startLine, column: startColumn };
426
- }
427
- return { type: 'REDIRECT_HEREDOC', value: '<<', line: startLine, column: startColumn };
428
- }
429
- if (this.peek() === '&') {
430
- this.advance();
431
- return { type: 'REDIRECT_IN', value: '<&', line: startLine, column: startColumn };
432
- }
433
- return { type: 'REDIRECT_IN', value: '<', line: startLine, column: startColumn };
434
- }
435
- // Check for digit followed by redirect (e.g., 2>&1, 2>file, 1>out)
436
- if (/[0-9]/.test(ch)) {
437
- const nextCh = this.peek(1);
438
- if (nextCh === '>' || nextCh === '<') {
439
- const fd = parseInt(this.advance(), 10);
440
- const redirectCh = this.advance();
441
- if (redirectCh === '>') {
442
- if (this.peek() === '>') {
443
- this.advance();
444
- return { type: 'REDIRECT_APPEND', value: '>>', line: startLine, column: startColumn, fd };
445
- }
446
- if (this.peek() === '&') {
447
- this.advance();
448
- return { type: 'REDIRECT_OUT', value: '>&', line: startLine, column: startColumn, fd };
449
- }
450
- return { type: 'REDIRECT_OUT', value: '>', line: startLine, column: startColumn, fd };
451
- }
452
- else {
453
- // <
454
- if (this.peek() === '<') {
455
- this.advance();
456
- if (this.peek() === '<') {
457
- this.advance();
458
- return { type: 'REDIRECT_HERESTRING', value: '<<<', line: startLine, column: startColumn, fd };
459
- }
460
- return { type: 'REDIRECT_HEREDOC', value: '<<', line: startLine, column: startColumn, fd };
461
- }
462
- if (this.peek() === '&') {
463
- this.advance();
464
- return { type: 'REDIRECT_IN', value: '<&', line: startLine, column: startColumn, fd };
465
- }
466
- return { type: 'REDIRECT_IN', value: '<', line: startLine, column: startColumn, fd };
467
- }
468
- }
469
- }
470
- // Words (including keywords)
471
- const word = this.readWord();
472
- if (word === '') {
473
- // Unknown character, try to recover
474
- const badChar = this.advance();
475
- this.errors.push({
476
- message: `Unexpected character '${badChar}'`,
477
- line: startLine,
478
- column: startColumn,
479
- });
480
- return { type: 'ERROR', value: badChar, line: startLine, column: startColumn };
481
- }
482
- // Check for keywords
483
- const keywords = {
484
- if: 'IF',
485
- then: 'THEN',
486
- else: 'ELSE',
487
- elif: 'ELIF',
488
- fi: 'FI',
489
- for: 'FOR',
490
- while: 'WHILE',
491
- until: 'UNTIL',
492
- do: 'DO',
493
- done: 'DONE',
494
- case: 'CASE',
495
- esac: 'ESAC',
496
- in: 'IN',
497
- };
498
- const keywordType = keywords[word];
499
- if (keywordType) {
500
- return { type: keywordType, value: word, line: startLine, column: startColumn };
501
- }
502
- // Check for assignment (VAR=value)
503
- const assignMatch = word.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(=|\+=)(.*)$/);
504
- if (assignMatch) {
505
- return { type: 'ASSIGNMENT', value: word, line: startLine, column: startColumn };
506
- }
507
- return { type: 'WORD', value: word, line: startLine, column: startColumn };
508
- }
509
- tokenize() {
510
- const tokens = [];
511
- let token;
512
- while ((token = this.nextToken()).type !== 'EOF') {
513
- if (token.type !== 'ERROR') {
514
- tokens.push(token);
515
- }
516
- }
517
- tokens.push(token); // Include EOF
518
- return tokens;
519
- }
520
- }
521
- // ============================================================================
522
- // Parser
523
- // ============================================================================
524
- class Parser {
525
- tokens;
526
- pos = 0;
527
- errors = [];
528
- constructor(tokens, lexerErrors) {
529
- this.tokens = tokens;
530
- this.errors = [...lexerErrors];
531
- }
532
- peek(offset = 0) {
533
- const idx = this.pos + offset;
534
- if (idx < 0 || idx >= this.tokens.length) {
535
- return { type: 'EOF', value: '', line: 0, column: 0 };
536
- }
537
- return this.tokens[idx];
538
- }
539
- advance() {
540
- const token = this.tokens[this.pos];
541
- if (this.pos < this.tokens.length) {
542
- this.pos++;
543
- }
544
- return token;
545
- }
546
- /** @internal Reserved for future use */
547
- expect(type, message) {
548
- const token = this.peek();
549
- if (token.type !== type) {
550
- if (message) {
551
- this.errors.push({
552
- message,
553
- line: token.line,
554
- column: token.column,
555
- });
556
- }
557
- return null;
558
- }
559
- return this.advance();
560
- }
561
- skipNewlines() {
562
- while (this.peek().type === 'NEWLINE') {
563
- this.advance();
564
- }
565
- }
566
- isCommandTerminator(token) {
567
- return token.type === 'NEWLINE' ||
568
- token.type === 'SEMICOLON' ||
569
- token.type === 'PIPE' ||
570
- token.type === 'AND' ||
571
- token.type === 'OR' ||
572
- token.type === 'BACKGROUND' ||
573
- token.type === 'RPAREN' ||
574
- token.type === 'RBRACE' ||
575
- token.type === 'EOF';
576
- }
577
- isRedirectToken(token) {
578
- return token.type === 'REDIRECT_OUT' ||
579
- token.type === 'REDIRECT_APPEND' ||
580
- token.type === 'REDIRECT_IN' ||
581
- token.type === 'REDIRECT_HEREDOC' ||
582
- token.type === 'REDIRECT_HERESTRING' ||
583
- token.type === 'REDIRECT_BOTH';
584
- }
585
- parseRedirect() {
586
- const token = this.peek();
587
- if (!this.isRedirectToken(token)) {
588
- return null;
589
- }
590
- this.advance();
591
- this.skipWhitespaceTokens();
592
- const targetToken = this.peek();
593
- // Check for consecutive redirects (like > >)
594
- if (this.isRedirectToken(targetToken)) {
595
- this.errors.push({
596
- message: `Consecutive redirects: unexpected ${targetToken.value} after ${token.value}`,
597
- line: targetToken.line,
598
- column: targetToken.column,
599
- suggestion: 'Remove one of the redirect operators',
600
- });
601
- return null;
602
- }
603
- // Accept WORD, ASSIGNMENT, or IN (which can be a filename like 'in')
604
- if (targetToken.type !== 'WORD' && targetToken.type !== 'ASSIGNMENT' && targetToken.type !== 'IN') {
605
- this.errors.push({
606
- message: `Redirect incomplete: missing file after ${token.value}`,
607
- line: token.line,
608
- column: token.column,
609
- suggestion: 'Specify a file name after the redirect operator',
610
- });
611
- return null;
612
- }
613
- const target = this.advance();
614
- const opMap = {
615
- '>': '>',
616
- '>&': '>&',
617
- '>>': '>>',
618
- '<': '<',
619
- '<&': '<&',
620
- '<<': '<<',
621
- '<<<': '<<<',
622
- '&>': '>&', // &> is equivalent to >& for redirecting both stdout and stderr
623
- };
624
- const redirect = {
625
- type: 'Redirect',
626
- op: opMap[token.value] ?? '>',
627
- target: { type: 'Word', value: target.value },
628
- };
629
- // Include file descriptor if present
630
- if (token.fd !== undefined) {
631
- redirect.fd = token.fd;
632
- }
633
- return redirect;
634
- }
635
- skipWhitespaceTokens() {
636
- // No explicit whitespace tokens in our lexer, but keep for consistency
637
- }
638
- parseWord() {
639
- const token = this.peek();
640
- if (token.type === 'WORD' || token.type === 'ASSIGNMENT') {
641
- this.advance();
642
- let quoted = undefined;
643
- // Check if quoted
644
- if (token.value.startsWith('"') || token.value.includes('"')) {
645
- quoted = 'double';
646
- }
647
- else if (token.value.startsWith("'") || token.value.includes("'")) {
648
- quoted = 'single';
649
- }
650
- return {
651
- type: 'Word',
652
- value: token.value,
653
- quoted,
654
- };
655
- }
656
- return null;
657
- }
658
- parseSimpleCommand() {
659
- const prefix = [];
660
- const args = [];
661
- const redirects = [];
662
- let name = null;
663
- // Parse assignments at the start
664
- while (this.peek().type === 'ASSIGNMENT') {
665
- const token = this.advance();
666
- const match = token.value.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(=|\+=)(.*)$/);
667
- if (match) {
668
- prefix.push({
669
- type: 'Assignment',
670
- name: match[1],
671
- operator: match[2],
672
- value: match[3] ? { type: 'Word', value: match[3] } : null,
673
- });
674
- }
675
- }
676
- // Handle single [ test command
677
- if (this.peek().type === 'LBRACKET') {
678
- const startToken = this.advance();
679
- name = { type: 'Word', value: '[' };
680
- // Parse test expression arguments until ]
681
- while (this.peek().type !== 'RBRACKET' && this.peek().type !== 'EOF' &&
682
- !this.isCommandTerminator(this.peek())) {
683
- const token = this.peek();
684
- if (token.type === 'WORD') {
685
- const word = this.parseWord();
686
- if (word) {
687
- args.push(word);
688
- }
689
- }
690
- else {
691
- break;
692
- }
693
- }
694
- // Expect closing ]
695
- if (this.peek().type !== 'RBRACKET') {
696
- this.errors.push({
697
- message: 'Unclosed [ test bracket: missing ]',
698
- line: startToken.line,
699
- column: startToken.column,
700
- suggestion: 'Add ] to close the test bracket',
701
- });
702
- }
703
- else {
704
- this.advance(); // consume ]
705
- args.push({ type: 'Word', value: ']' });
706
- }
707
- return { type: 'Command', name, prefix, args, redirects };
708
- }
709
- // Parse command name and arguments
710
- while (true) {
711
- const token = this.peek();
712
- if (this.isRedirectToken(token)) {
713
- // Check for consecutive redirects (like > >)
714
- const prevToken = this.peek(-1);
715
- if (prevToken && this.isRedirectToken(prevToken)) {
716
- this.errors.push({
717
- message: `Consecutive redirects: unexpected ${token.value} after ${prevToken.value}`,
718
- line: token.line,
719
- column: token.column,
720
- suggestion: 'Remove one of the redirect operators',
721
- });
722
- }
723
- const redirect = this.parseRedirect();
724
- if (redirect) {
725
- redirects.push(redirect);
726
- }
727
- }
728
- else if (token.type === 'WORD') {
729
- const word = this.parseWord();
730
- if (word) {
731
- if (!name) {
732
- name = word;
733
- }
734
- else {
735
- args.push(word);
736
- }
737
- }
738
- }
739
- else if (token.type === 'IN') {
740
- // 'in' can appear as a filename (e.g., cmd < in), treat as word in command context
741
- this.advance();
742
- const word = { type: 'Word', value: 'in' };
743
- if (!name) {
744
- name = word;
745
- }
746
- else {
747
- args.push(word);
748
- }
749
- }
750
- else if (this.isCommandTerminator(token) || token.type === 'THEN' || token.type === 'DO' ||
751
- token.type === 'ELSE' || token.type === 'ELIF' || token.type === 'FI' ||
752
- token.type === 'DONE' || token.type === 'ESAC' || token.type === 'CASE_PATTERN_END') {
753
- break;
754
- }
755
- else {
756
- break;
757
- }
758
- }
759
- if (!name && prefix.length === 0 && redirects.length === 0) {
760
- return null;
761
- }
762
- return {
763
- type: 'Command',
764
- name,
765
- prefix,
766
- args,
767
- redirects,
768
- };
769
- }
770
- parseSubshell() {
771
- if (this.peek().type !== 'LPAREN') {
772
- return null;
773
- }
774
- const startToken = this.advance(); // consume (
775
- this.skipNewlines();
776
- const body = [];
777
- while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
778
- const node = this.parseCompoundList();
779
- if (node) {
780
- body.push(node);
781
- }
782
- this.skipNewlines();
783
- if (this.peek().type === 'SEMICOLON') {
784
- this.advance();
785
- this.skipNewlines();
786
- }
787
- }
788
- if (this.peek().type !== 'RPAREN') {
789
- this.errors.push({
790
- message: 'Unclosed subshell: missing )',
791
- line: startToken.line,
792
- column: startToken.column,
793
- suggestion: 'Add ) to close the subshell',
794
- });
795
- return { type: 'Subshell', body };
796
- }
797
- this.advance(); // consume )
798
- return { type: 'Subshell', body };
799
- }
800
- parseBraceGroup() {
801
- if (this.peek().type !== 'LBRACE') {
802
- return null;
803
- }
804
- const startToken = this.advance(); // consume {
805
- this.skipNewlines();
806
- const body = [];
807
- while (this.peek().type !== 'RBRACE' && this.peek().type !== 'EOF') {
808
- const node = this.parseCompoundList();
809
- if (node) {
810
- body.push(node);
811
- }
812
- this.skipNewlines();
813
- if (this.peek().type === 'SEMICOLON') {
814
- this.advance();
815
- this.skipNewlines();
816
- }
817
- }
818
- if (this.peek().type !== 'RBRACE') {
819
- this.errors.push({
820
- message: 'Unclosed brace group: missing }',
821
- line: startToken.line,
822
- column: startToken.column,
823
- suggestion: 'Add } to close the brace group',
824
- });
825
- return { type: 'CompoundCommand', kind: 'brace', body };
826
- }
827
- this.advance(); // consume }
828
- return { type: 'CompoundCommand', kind: 'brace', body };
829
- }
830
- parseIfStatement() {
831
- if (this.peek().type !== 'IF') {
832
- return null;
833
- }
834
- const startToken = this.advance(); // consume 'if'
835
- this.skipNewlines();
836
- const body = [];
837
- // Parse condition
838
- const condition = this.parseCondition();
839
- if (condition) {
840
- body.push(condition);
841
- }
842
- this.skipNewlines();
843
- // Skip semicolon before 'then' (common in one-line if statements)
844
- if (this.peek().type === 'SEMICOLON') {
845
- this.advance();
846
- this.skipNewlines();
847
- }
848
- // Expect 'then'
849
- if (this.peek().type !== 'THEN') {
850
- this.errors.push({
851
- message: "Missing 'then' in if statement",
852
- line: this.peek().line,
853
- column: this.peek().column,
854
- suggestion: "Add 'then' after the condition",
855
- });
856
- }
857
- else {
858
- this.advance();
859
- }
860
- this.skipNewlines();
861
- // Parse 'then' body
862
- while (this.peek().type !== 'ELSE' && this.peek().type !== 'ELIF' &&
863
- this.peek().type !== 'FI' && this.peek().type !== 'EOF') {
864
- const node = this.parseCompoundList();
865
- if (node) {
866
- body.push(node);
867
- }
868
- else {
869
- break;
870
- }
871
- this.skipNewlines();
872
- if (this.peek().type === 'SEMICOLON') {
873
- this.advance();
874
- this.skipNewlines();
875
- }
876
- }
877
- // Parse 'elif' or 'else'
878
- while (this.peek().type === 'ELIF' || this.peek().type === 'ELSE') {
879
- this.advance();
880
- this.skipNewlines();
881
- if (this.peek(-1).type === 'ELIF') {
882
- // Parse elif condition
883
- const elifCondition = this.parseCondition();
884
- if (elifCondition) {
885
- body.push(elifCondition);
886
- }
887
- this.skipNewlines();
888
- // Skip semicolon before 'then'
889
- if (this.peek().type === 'SEMICOLON') {
890
- this.advance();
891
- this.skipNewlines();
892
- }
893
- if (this.peek().type === 'THEN') {
894
- this.advance();
895
- }
896
- this.skipNewlines();
897
- }
898
- // Parse else/elif body
899
- while (this.peek().type !== 'ELSE' && this.peek().type !== 'ELIF' &&
900
- this.peek().type !== 'FI' && this.peek().type !== 'EOF') {
901
- const node = this.parseCompoundList();
902
- if (node) {
903
- body.push(node);
904
- }
905
- else {
906
- break;
907
- }
908
- this.skipNewlines();
909
- if (this.peek().type === 'SEMICOLON') {
910
- this.advance();
911
- this.skipNewlines();
912
- }
913
- }
914
- }
915
- // Expect 'fi'
916
- if (this.peek().type !== 'FI') {
917
- this.errors.push({
918
- message: "Missing 'fi' to close if statement",
919
- line: startToken.line,
920
- column: startToken.column,
921
- suggestion: "Add 'fi' at the end",
922
- });
923
- }
924
- else {
925
- this.advance();
926
- }
927
- return { type: 'CompoundCommand', kind: 'if', body };
928
- }
929
- parseForLoop() {
930
- if (this.peek().type !== 'FOR') {
931
- return null;
932
- }
933
- const startToken = this.advance(); // consume 'for'
934
- this.skipNewlines();
935
- const body = [];
936
- // Check for C-style for loop: for ((...))
937
- if (this.peek().type === 'LPAREN' && this.peek(1).type === 'LPAREN') {
938
- this.advance(); // consume first (
939
- this.advance(); // consume second (
940
- // Parse the arithmetic expression until ))
941
- let depth = 2;
942
- let exprContent = '';
943
- while (depth > 0 && this.peek().type !== 'EOF') {
944
- const currentToken = this.peek();
945
- if (currentToken.type === 'LPAREN') {
946
- depth++;
947
- exprContent += '(';
948
- this.advance();
949
- }
950
- else if (currentToken.type === 'RPAREN') {
951
- depth--;
952
- if (depth > 0) {
953
- exprContent += ')';
954
- }
955
- this.advance();
956
- }
957
- else {
958
- exprContent += currentToken.value;
959
- this.advance();
960
- }
961
- // Add space between tokens (simplified)
962
- if (depth > 0) {
963
- exprContent += ' ';
964
- }
965
- }
966
- // Store the C-style expression
967
- if (exprContent.trim()) {
968
- body.push({
969
- type: 'Command',
970
- name: { type: 'Word', value: exprContent.trim() },
971
- prefix: [],
972
- args: [],
973
- redirects: []
974
- });
975
- }
976
- this.skipNewlines();
977
- // Allow semicolon before do
978
- if (this.peek().type === 'SEMICOLON') {
979
- this.advance();
980
- this.skipNewlines();
981
- }
982
- // Expect 'do'
983
- if (this.peek().type !== 'DO') {
984
- this.errors.push({
985
- message: "Missing 'do' in for loop",
986
- line: this.peek().line,
987
- column: this.peek().column,
988
- suggestion: "Add 'do' before the loop body",
989
- });
990
- }
991
- else {
992
- this.advance();
993
- }
994
- this.skipNewlines();
995
- // Parse loop body
996
- while (this.peek().type !== 'DONE' && this.peek().type !== 'EOF') {
997
- const node = this.parseCompoundList();
998
- if (node) {
999
- body.push(node);
1000
- }
1001
- else {
1002
- break;
1003
- }
1004
- this.skipNewlines();
1005
- if (this.peek().type === 'SEMICOLON') {
1006
- this.advance();
1007
- this.skipNewlines();
1008
- }
1009
- }
1010
- // Expect 'done'
1011
- if (this.peek().type !== 'DONE') {
1012
- this.errors.push({
1013
- message: "Missing 'done' to close for loop",
1014
- line: startToken.line,
1015
- column: startToken.column,
1016
- suggestion: "Add 'done' at the end",
1017
- });
1018
- }
1019
- else {
1020
- this.advance();
1021
- }
1022
- return { type: 'CompoundCommand', kind: 'for', body };
1023
- }
1024
- // Standard for loop: for var in list
1025
- // Parse variable name
1026
- if (this.peek().type === 'WORD') {
1027
- body.push({ type: 'Command', name: this.parseWord(), prefix: [], args: [], redirects: [] });
1028
- }
1029
- this.skipNewlines();
1030
- // Expect 'in' (optional)
1031
- if (this.peek().type === 'IN') {
1032
- this.advance();
1033
- this.skipNewlines();
1034
- // Parse word list
1035
- while (this.peek().type === 'WORD') {
1036
- body.push({ type: 'Command', name: this.parseWord(), prefix: [], args: [], redirects: [] });
1037
- }
1038
- }
1039
- this.skipNewlines();
1040
- // Allow semicolon before do
1041
- if (this.peek().type === 'SEMICOLON') {
1042
- this.advance();
1043
- this.skipNewlines();
1044
- }
1045
- // Expect 'do'
1046
- if (this.peek().type !== 'DO') {
1047
- this.errors.push({
1048
- message: "Missing 'do' in for loop",
1049
- line: this.peek().line,
1050
- column: this.peek().column,
1051
- suggestion: "Add 'do' before the loop body",
1052
- });
1053
- }
1054
- else {
1055
- this.advance();
1056
- }
1057
- this.skipNewlines();
1058
- // Parse loop body
1059
- while (this.peek().type !== 'DONE' && this.peek().type !== 'EOF') {
1060
- const node = this.parseCompoundList();
1061
- if (node) {
1062
- body.push(node);
1063
- }
1064
- else {
1065
- break;
1066
- }
1067
- this.skipNewlines();
1068
- if (this.peek().type === 'SEMICOLON') {
1069
- this.advance();
1070
- this.skipNewlines();
1071
- }
1072
- }
1073
- // Expect 'done'
1074
- if (this.peek().type !== 'DONE') {
1075
- this.errors.push({
1076
- message: "Missing 'done' to close for loop",
1077
- line: startToken.line,
1078
- column: startToken.column,
1079
- suggestion: "Add 'done' at the end",
1080
- });
1081
- }
1082
- else {
1083
- this.advance();
1084
- }
1085
- return { type: 'CompoundCommand', kind: 'for', body };
1086
- }
1087
- parseWhileLoop() {
1088
- const tokenType = this.peek().type;
1089
- if (tokenType !== 'WHILE' && tokenType !== 'UNTIL') {
1090
- return null;
1091
- }
1092
- const startToken = this.advance(); // consume 'while' or 'until'
1093
- const kind = tokenType === 'WHILE' ? 'while' : 'until';
1094
- this.skipNewlines();
1095
- const body = [];
1096
- // Parse condition
1097
- const condition = this.parseCondition();
1098
- if (condition) {
1099
- body.push(condition);
1100
- }
1101
- this.skipNewlines();
1102
- // Allow semicolon before do
1103
- if (this.peek().type === 'SEMICOLON') {
1104
- this.advance();
1105
- this.skipNewlines();
1106
- }
1107
- // Expect 'do'
1108
- if (this.peek().type !== 'DO') {
1109
- this.errors.push({
1110
- message: `Missing 'do' in ${kind} loop`,
1111
- line: this.peek().line,
1112
- column: this.peek().column,
1113
- suggestion: "Add 'do' before the loop body",
1114
- });
1115
- }
1116
- else {
1117
- this.advance();
1118
- }
1119
- this.skipNewlines();
1120
- // Parse loop body
1121
- while (this.peek().type !== 'DONE' && this.peek().type !== 'EOF') {
1122
- const node = this.parseCompoundList();
1123
- if (node) {
1124
- body.push(node);
1125
- }
1126
- else {
1127
- break;
1128
- }
1129
- this.skipNewlines();
1130
- if (this.peek().type === 'SEMICOLON') {
1131
- this.advance();
1132
- this.skipNewlines();
1133
- }
1134
- }
1135
- // Expect 'done'
1136
- if (this.peek().type !== 'DONE') {
1137
- this.errors.push({
1138
- message: `Missing 'done' to close ${kind} loop`,
1139
- line: startToken.line,
1140
- column: startToken.column,
1141
- suggestion: "Add 'done' at the end",
1142
- });
1143
- }
1144
- else {
1145
- this.advance();
1146
- }
1147
- return { type: 'CompoundCommand', kind, body };
1148
- }
1149
- parseCaseStatement() {
1150
- if (this.peek().type !== 'CASE') {
1151
- return null;
1152
- }
1153
- const startToken = this.advance(); // consume 'case'
1154
- this.skipNewlines();
1155
- const body = [];
1156
- // Parse word
1157
- if (this.peek().type === 'WORD') {
1158
- body.push({ type: 'Command', name: this.parseWord(), prefix: [], args: [], redirects: [] });
1159
- }
1160
- this.skipNewlines();
1161
- // Expect 'in'
1162
- if (this.peek().type === 'IN') {
1163
- this.advance();
1164
- }
1165
- this.skipNewlines();
1166
- // Parse case items
1167
- while (this.peek().type !== 'ESAC' && this.peek().type !== 'EOF') {
1168
- // Parse pattern
1169
- if (this.peek().type === 'WORD' || this.peek().type === 'LPAREN') {
1170
- // Skip optional (
1171
- if (this.peek().type === 'LPAREN') {
1172
- this.advance();
1173
- }
1174
- // Parse pattern word(s)
1175
- while (this.peek().type === 'WORD') {
1176
- this.advance();
1177
- if (this.peek().type === 'PIPE') {
1178
- this.advance(); // pattern separator
1179
- }
1180
- else {
1181
- break;
1182
- }
1183
- }
1184
- // Expect )
1185
- if (this.peek().type === 'RPAREN') {
1186
- this.advance();
1187
- }
1188
- this.skipNewlines();
1189
- // Parse case body until ;;
1190
- while (this.peek().type !== 'CASE_PATTERN_END' &&
1191
- this.peek().type !== 'ESAC' &&
1192
- this.peek().type !== 'EOF') {
1193
- const node = this.parseCompoundList();
1194
- if (node) {
1195
- body.push(node);
1196
- }
1197
- else {
1198
- break;
1199
- }
1200
- this.skipNewlines();
1201
- if (this.peek().type === 'SEMICOLON') {
1202
- this.advance();
1203
- this.skipNewlines();
1204
- }
1205
- }
1206
- // Consume ;;
1207
- if (this.peek().type === 'CASE_PATTERN_END') {
1208
- this.advance();
1209
- }
1210
- this.skipNewlines();
1211
- }
1212
- else {
1213
- break;
1214
- }
1215
- }
1216
- // Expect 'esac'
1217
- if (this.peek().type !== 'ESAC') {
1218
- this.errors.push({
1219
- message: "Missing 'esac' to close case statement",
1220
- line: startToken.line,
1221
- column: startToken.column,
1222
- suggestion: "Add 'esac' at the end",
1223
- });
1224
- }
1225
- else {
1226
- this.advance();
1227
- }
1228
- return { type: 'CompoundCommand', kind: 'case', body };
1229
- }
1230
- parseTestCommand() {
1231
- const token = this.peek();
1232
- // [[ test ]]
1233
- if (token.type === 'DOUBLE_LBRACKET') {
1234
- const startToken = this.advance();
1235
- const body = [];
1236
- // Parse test expression - [[ ]] can contain && and || as operators within the test
1237
- while (this.peek().type !== 'DOUBLE_RBRACKET' && this.peek().type !== 'EOF') {
1238
- const currentToken = this.peek();
1239
- // Handle && and || inside [[ ]]
1240
- if (currentToken.type === 'AND' || currentToken.type === 'OR') {
1241
- const opToken = this.advance();
1242
- body.push({ type: 'Command', name: { type: 'Word', value: opToken.value }, prefix: [], args: [], redirects: [] });
1243
- }
1244
- else {
1245
- const word = this.parseWord();
1246
- if (word) {
1247
- body.push({ type: 'Command', name: word, prefix: [], args: [], redirects: [] });
1248
- }
1249
- else {
1250
- break;
1251
- }
1252
- }
1253
- }
1254
- if (this.peek().type !== 'DOUBLE_RBRACKET') {
1255
- this.errors.push({
1256
- message: 'Unclosed [[ test: missing ]]',
1257
- line: startToken.line,
1258
- column: startToken.column,
1259
- suggestion: 'Add ]] to close the test',
1260
- });
1261
- }
1262
- else {
1263
- this.advance();
1264
- }
1265
- return { type: 'CompoundCommand', kind: 'arithmetic', body };
1266
- }
1267
- // [ test ] - this is handled as a regular command
1268
- return null;
1269
- }
1270
- parseCompoundCommand() {
1271
- const token = this.peek();
1272
- switch (token.type) {
1273
- case 'IF':
1274
- return this.parseIfStatement();
1275
- case 'FOR':
1276
- return this.parseForLoop();
1277
- case 'WHILE':
1278
- case 'UNTIL':
1279
- return this.parseWhileLoop();
1280
- case 'CASE':
1281
- return this.parseCaseStatement();
1282
- case 'LPAREN':
1283
- return this.parseSubshell();
1284
- case 'LBRACE':
1285
- return this.parseBraceGroup();
1286
- case 'DOUBLE_LBRACKET':
1287
- return this.parseTestCommand();
1288
- default:
1289
- return null;
1290
- }
1291
- }
1292
- parsePipeline() {
1293
- const commands = [];
1294
- let negated = false;
1295
- // Check for negation (! at start of pipeline)
1296
- if (this.peek().type === 'WORD' && this.peek().value === '!') {
1297
- negated = true;
1298
- this.advance(); // consume !
1299
- this.skipWhitespaceTokens();
1300
- }
1301
- // Check for leading pipe (error)
1302
- if (this.peek().type === 'PIPE') {
1303
- this.errors.push({
1304
- message: 'Pipe at start of command: command expected before |',
1305
- line: this.peek().line,
1306
- column: this.peek().column,
1307
- suggestion: 'Add a command before the pipe',
1308
- });
1309
- this.advance(); // consume the pipe and continue
1310
- }
1311
- // Try compound command first
1312
- const compound = this.parseCompoundCommand();
1313
- if (compound) {
1314
- // Check for pipe after compound
1315
- this.skipNewlines();
1316
- if (this.peek().type === 'PIPE') {
1317
- // Compound commands in pipelines - for now, return as-is
1318
- return compound;
1319
- }
1320
- return compound;
1321
- }
1322
- // Parse first simple command
1323
- const firstCmd = this.parseSimpleCommand();
1324
- if (!firstCmd) {
1325
- return null;
1326
- }
1327
- commands.push(firstCmd);
1328
- // Parse pipeline continuation
1329
- while (this.peek().type === 'PIPE') {
1330
- const pipeToken = this.advance();
1331
- // Check for consecutive pipes
1332
- if (this.peek().type === 'PIPE') {
1333
- this.errors.push({
1334
- message: 'Consecutive pipes: remove extra |',
1335
- line: this.peek().line,
1336
- column: this.peek().column,
1337
- suggestion: 'Remove one of the consecutive pipes',
1338
- });
1339
- this.advance(); // consume extra pipe
1340
- }
1341
- this.skipNewlines();
1342
- // Check for missing command after pipe
1343
- if (this.isCommandTerminator(this.peek()) || this.peek().type === 'EOF') {
1344
- this.errors.push({
1345
- message: 'Pipe incomplete: missing command after |',
1346
- line: pipeToken.line,
1347
- column: pipeToken.column,
1348
- suggestion: 'Add a command after the pipe',
1349
- });
1350
- break;
1351
- }
1352
- const nextCmd = this.parseSimpleCommand();
1353
- if (nextCmd) {
1354
- commands.push(nextCmd);
1355
- }
1356
- else {
1357
- break;
1358
- }
1359
- }
1360
- if (commands.length === 1 && !negated) {
1361
- return commands[0];
1362
- }
1363
- return {
1364
- type: 'Pipeline',
1365
- negated,
1366
- commands,
1367
- };
1368
- }
1369
- parseAndOr() {
1370
- let left = this.parsePipeline();
1371
- if (!left) {
1372
- return null;
1373
- }
1374
- while (this.peek().type === 'AND' || this.peek().type === 'OR') {
1375
- const opToken = this.advance();
1376
- const operator = opToken.type === 'AND' ? '&&' : '||';
1377
- this.skipNewlines();
1378
- // Check for missing command after && or ||
1379
- if (this.isCommandTerminator(this.peek()) || this.peek().type === 'EOF') {
1380
- this.errors.push({
1381
- message: `Incomplete ${operator === '&&' ? 'AND' : 'OR'} list: missing command after ${operator}`,
1382
- line: opToken.line,
1383
- column: opToken.column,
1384
- suggestion: `Add a command after ${operator}`,
1385
- });
1386
- return left;
1387
- }
1388
- const right = this.parsePipeline();
1389
- if (!right) {
1390
- return left;
1391
- }
1392
- left = {
1393
- type: 'List',
1394
- operator,
1395
- left,
1396
- right,
1397
- };
1398
- }
1399
- return left;
1400
- }
1401
- /**
1402
- * Parse a condition for if/while/until statements.
1403
- * Unlike parseCompoundList, this doesn't report "unexpected semicolon" errors
1404
- * since conditions are typically followed by semicolons in one-line syntax.
1405
- */
1406
- parseCondition() {
1407
- this.skipNewlines();
1408
- return this.parseAndOr();
1409
- }
1410
- parseCompoundList() {
1411
- this.skipNewlines();
1412
- // Check for unexpected token at start
1413
- if (this.peek().type === 'SEMICOLON') {
1414
- const token = this.peek();
1415
- this.errors.push({
1416
- message: 'Unexpected semicolon at start of command',
1417
- line: token.line,
1418
- column: token.column,
1419
- suggestion: 'Remove the leading semicolon',
1420
- });
1421
- this.advance();
1422
- this.skipNewlines();
1423
- }
1424
- return this.parseAndOr();
1425
- }
1426
- parse() {
1427
- const body = [];
1428
- while (this.peek().type !== 'EOF') {
1429
- this.skipNewlines();
1430
- if (this.peek().type === 'EOF') {
1431
- break;
1432
- }
1433
- const node = this.parseCompoundList();
1434
- if (node) {
1435
- body.push(node);
1436
- }
1437
- // Consume terminators
1438
- const nextToken = this.peek();
1439
- if (nextToken.type === 'SEMICOLON' || nextToken.type === 'NEWLINE' || nextToken.type === 'BACKGROUND') {
1440
- this.advance();
1441
- }
1442
- else if (nextToken.type !== 'EOF' && !node) {
1443
- // Stuck, skip token to avoid infinite loop
1444
- this.advance();
1445
- }
1446
- }
1447
- return {
1448
- type: 'Program',
1449
- body,
1450
- errors: this.errors.length > 0 ? this.errors : undefined,
1451
- };
1452
- }
1453
- }
1454
- // ============================================================================
1455
- // Public API
1456
- // ============================================================================
1457
- /**
1458
- * Parse a bash command string into an AST
1459
- *
1460
- * @example
1461
- * ```typescript
1462
- * const ast = parse('ls -la | grep foo')
1463
- * // Returns Program with Pipeline containing two Commands
1464
- * ```
1465
- */
1466
- export function parse(input) {
1467
- // Handle empty or whitespace-only input
1468
- const trimmed = input.trim();
1469
- if (trimmed === '') {
1470
- return {
1471
- type: 'Program',
1472
- body: [],
1473
- errors: undefined,
1474
- };
1475
- }
1476
- const lexer = new Lexer(input);
1477
- const tokens = lexer.tokenize();
1478
- const parser = new Parser(tokens, lexer.errors);
1479
- return parser.parse();
1480
- }
1481
- /**
1482
- * Check if input is syntactically valid bash
1483
- */
1484
- export function isValidSyntax(input) {
1485
- const ast = parse(input);
1486
- return !ast.errors || ast.errors.length === 0;
1487
- }
1488
- //# sourceMappingURL=parser.js.map