codacus commited on
Commit
d1f3e8c
·
1 Parent(s): 1ba0606

feat: added bolt dedicated shell

Browse files
app/components/chat/Artifact.tsx CHANGED
@@ -171,6 +171,10 @@ const ActionList = memo(({ actions }: ActionListProps) => {
171
  <div className="flex items-center w-full min-h-[28px]">
172
  <span className="flex-1">Run command</span>
173
  </div>
 
 
 
 
174
  ) : null}
175
  </div>
176
  {type === 'shell' && (
 
171
  <div className="flex items-center w-full min-h-[28px]">
172
  <span className="flex-1">Run command</span>
173
  </div>
174
+ ) : type === 'start' ? (
175
+ <div className="flex items-center w-full min-h-[28px]">
176
+ <span className="flex-1">Start Application</span>
177
+ </div>
178
  ) : null}
179
  </div>
180
  {type === 'shell' && (
app/components/workbench/EditorPanel.tsx CHANGED
@@ -199,25 +199,48 @@ export const EditorPanel = memo(
199
  <div className="h-full">
200
  <div className="bg-bolt-elements-terminals-background h-full flex flex-col">
201
  <div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
202
- {Array.from({ length: terminalCount }, (_, index) => {
203
  const isActive = activeTerminal === index;
204
 
205
  return (
206
- <button
207
- key={index}
208
- className={classNames(
209
- 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
210
- {
211
- 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
212
- 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
213
- !isActive,
214
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  )}
216
- onClick={() => setActiveTerminal(index)}
217
- >
218
- <div className="i-ph:terminal-window-duotone text-lg" />
219
- Terminal {terminalCount > 1 && index + 1}
220
- </button>
221
  );
222
  })}
223
  {terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
@@ -229,9 +252,26 @@ export const EditorPanel = memo(
229
  onClick={() => workbenchStore.toggleTerminal(false)}
230
  />
231
  </div>
232
- {Array.from({ length: terminalCount }, (_, index) => {
233
  const isActive = activeTerminal === index;
 
 
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  return (
236
  <Terminal
237
  key={index}
 
199
  <div className="h-full">
200
  <div className="bg-bolt-elements-terminals-background h-full flex flex-col">
201
  <div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
202
+ {Array.from({ length: terminalCount + 1 }, (_, index) => {
203
  const isActive = activeTerminal === index;
204
 
205
  return (
206
+ <>
207
+ {index == 0 ? (
208
+ <button
209
+ key={index}
210
+ className={classNames(
211
+ 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
212
+ {
213
+ 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary':
214
+ isActive,
215
+ 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
216
+ !isActive,
217
+ },
218
+ )}
219
+ onClick={() => setActiveTerminal(index)}
220
+ >
221
+ <div className="i-ph:terminal-window-duotone text-lg" />
222
+ Bolt Terminal
223
+ </button>
224
+ ) : (
225
+ <>
226
+ <button
227
+ key={index}
228
+ className={classNames(
229
+ 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
230
+ {
231
+ 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
232
+ 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
233
+ !isActive,
234
+ },
235
+ )}
236
+ onClick={() => setActiveTerminal(index)}
237
+ >
238
+ <div className="i-ph:terminal-window-duotone text-lg" />
239
+ Terminal {terminalCount > 1 && index}
240
+ </button>
241
+ </>
242
  )}
243
+ </>
 
 
 
 
244
  );
245
  })}
246
  {terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
 
252
  onClick={() => workbenchStore.toggleTerminal(false)}
253
  />
254
  </div>
255
+ {Array.from({ length: terminalCount + 1 }, (_, index) => {
256
  const isActive = activeTerminal === index;
257
+ if (index == 0) {
258
+ console.log('starting bolt terminal');
259
 
260
+ return (
261
+ <Terminal
262
+ key={index}
263
+ className={classNames('h-full overflow-hidden', {
264
+ hidden: !isActive,
265
+ })}
266
+ ref={(ref) => {
267
+ terminalRefs.current.push(ref);
268
+ }}
269
+ onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)}
270
+ onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
271
+ theme={theme}
272
+ />
273
+ );
274
+ }
275
  return (
276
  <Terminal
277
  key={index}
app/lib/.server/llm/prompts.ts CHANGED
@@ -174,10 +174,16 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
174
 
175
  - When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
176
  - When running multiple shell commands, use \`&&\` to run them sequentially.
177
- - ULTRA IMPORTANT: Do NOT re-run a dev command if there is one that starts a dev server and new dependencies were installed or files updated! If a dev server has started already, assume that installing dependencies will be executed in a different process and will be picked up by the dev server.
178
 
179
  - file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.
180
 
 
 
 
 
 
 
181
  9. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
182
 
183
  10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
@@ -265,7 +271,7 @@ Here are some examples of correct usage of artifacts:
265
  ...
266
  </boltAction>
267
 
268
- <boltAction type="shell">
269
  npm run dev
270
  </boltAction>
271
  </boltArtifact>
@@ -322,7 +328,7 @@ Here are some examples of correct usage of artifacts:
322
  ...
323
  </boltAction>
324
 
325
- <boltAction type="shell">
326
  npm run dev
327
  </boltAction>
328
  </boltArtifact>
 
174
 
175
  - When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
176
  - When running multiple shell commands, use \`&&\` to run them sequentially.
177
+ - ULTRA IMPORTANT: Do NOT re-run a dev command with shell action use dev action to run dev commands
178
 
179
  - file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory.
180
 
181
+ - start: For starting development server.
182
+ - Use to start application if not already started or NEW dependencies added
183
+ - Only use this action when you need to run a dev server or start the application
184
+ - ULTRA IMORTANT: do NOT re-run a dev server if files updated, existing dev server can autometically detect changes and executes the file changes
185
+
186
+
187
  9. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
188
 
189
  10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
 
271
  ...
272
  </boltAction>
273
 
274
+ <boltAction type="start">
275
  npm run dev
276
  </boltAction>
277
  </boltArtifact>
 
328
  ...
329
  </boltAction>
330
 
331
+ <boltAction type="start">
332
  npm run dev
333
  </boltAction>
334
  </boltArtifact>
app/lib/runtime/action-runner.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { WebContainer } from '@webcontainer/api';
2
- import { map, type MapStore } from 'nanostores';
3
  import * as nodePath from 'node:path';
4
  import type { BoltAction } from '~/types/actions';
5
  import { createScopedLogger } from '~/utils/logger';
6
  import { unreachable } from '~/utils/unreachable';
7
  import type { ActionCallbackData } from './message-parser';
 
 
8
 
9
  const logger = createScopedLogger('ActionRunner');
10
 
@@ -36,11 +38,14 @@ type ActionsMap = MapStore<Record<string, ActionState>>;
36
  export class ActionRunner {
37
  #webcontainer: Promise<WebContainer>;
38
  #currentExecutionPromise: Promise<void> = Promise.resolve();
39
-
 
40
  actions: ActionsMap = map({});
41
 
42
- constructor(webcontainerPromise: Promise<WebContainer>) {
43
  this.#webcontainer = webcontainerPromise;
 
 
44
  }
45
 
46
  addAction(data: ActionCallbackData) {
@@ -110,6 +115,10 @@ export class ActionRunner {
110
  await this.#runFileAction(action);
111
  break;
112
  }
 
 
 
 
113
  }
114
 
115
  this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
@@ -125,28 +134,35 @@ export class ActionRunner {
125
  if (action.type !== 'shell') {
126
  unreachable('Expected shell action');
127
  }
 
 
 
 
 
 
 
 
128
 
129
- const webcontainer = await this.#webcontainer;
130
-
131
- const process = await webcontainer.spawn('jsh', ['-c', action.content], {
132
- env: { npm_config_yes: true },
133
- });
134
-
135
- action.abortSignal.addEventListener('abort', () => {
136
- process.kill();
137
- });
138
-
139
- process.output.pipeTo(
140
- new WritableStream({
141
- write(data) {
142
- console.log(data);
143
- },
144
- }),
145
- );
146
 
147
- const exitCode = await process.exit;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
- logger.debug(`Process terminated with code ${exitCode}`);
150
  }
151
 
152
  async #runFileAction(action: ActionState) {
@@ -177,6 +193,23 @@ export class ActionRunner {
177
  logger.error('Failed to write file\n\n', error);
178
  }
179
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  #updateAction(id: string, newState: ActionStateUpdate) {
182
  const actions = this.actions.get();
 
1
+ import { WebContainer, type WebContainerProcess } from '@webcontainer/api';
2
+ import { atom, map, type MapStore } from 'nanostores';
3
  import * as nodePath from 'node:path';
4
  import type { BoltAction } from '~/types/actions';
5
  import { createScopedLogger } from '~/utils/logger';
6
  import { unreachable } from '~/utils/unreachable';
7
  import type { ActionCallbackData } from './message-parser';
8
+ import type { ITerminal } from '~/types/terminal';
9
+ import type { BoltShell } from '~/utils/shell';
10
 
11
  const logger = createScopedLogger('ActionRunner');
12
 
 
38
  export class ActionRunner {
39
  #webcontainer: Promise<WebContainer>;
40
  #currentExecutionPromise: Promise<void> = Promise.resolve();
41
+ #shellTerminal: () => BoltShell;
42
+ runnerId = atom<string>(`${Date.now()}`);
43
  actions: ActionsMap = map({});
44
 
45
+ constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) {
46
  this.#webcontainer = webcontainerPromise;
47
+ this.#shellTerminal = getShellTerminal;
48
+
49
  }
50
 
51
  addAction(data: ActionCallbackData) {
 
115
  await this.#runFileAction(action);
116
  break;
117
  }
118
+ case 'start': {
119
+ await this.#runStartAction(action);
120
+ break;
121
+ }
122
  }
123
 
124
  this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
 
134
  if (action.type !== 'shell') {
135
  unreachable('Expected shell action');
136
  }
137
+ const shell = this.#shellTerminal()
138
+ await shell.ready()
139
+ if (!shell || !shell.terminal || !shell.process) {
140
+ unreachable('Shell terminal not found');
141
+ }
142
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content)
143
+ if (resp?.exitCode != 0) {
144
+ throw new Error("Failed To Start Application");
145
 
146
+ }
147
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ async #runStartAction(action: ActionState) {
150
+ if (action.type !== 'start') {
151
+ unreachable('Expected shell action');
152
+ }
153
+ if (!this.#shellTerminal) {
154
+ unreachable('Shell terminal not found');
155
+ }
156
+ const shell = this.#shellTerminal()
157
+ await shell.ready()
158
+ if (!shell || !shell.terminal || !shell.process) {
159
+ unreachable('Shell terminal not found');
160
+ }
161
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content)
162
+ if (resp?.exitCode != 0) {
163
+ throw new Error("Failed To Start Application");
164
 
165
+ }
166
  }
167
 
168
  async #runFileAction(action: ActionState) {
 
193
  logger.error('Failed to write file\n\n', error);
194
  }
195
  }
196
+ async getCurrentExecutionResult(output: ReadableStreamDefaultReader<string>) {
197
+ let fullOutput = '';
198
+ let exitCode: number = 0;
199
+ while (true) {
200
+ const { value, done } = await output.read();
201
+ if (done) break;
202
+ const text = value || '';
203
+ fullOutput += text;
204
+ // Check if command completion signal with exit code
205
+ const exitMatch = fullOutput.match(/\]654;exit=-?\d+:(\d+)/);
206
+ if (exitMatch) {
207
+ exitCode = parseInt(exitMatch[1], 10);
208
+ break;
209
+ }
210
+ }
211
+ return { output: fullOutput, exitCode };
212
+ }
213
 
214
  #updateAction(id: string, newState: ActionStateUpdate) {
215
  const actions = this.actions.get();
app/lib/runtime/message-parser.ts CHANGED
@@ -54,7 +54,7 @@ interface MessageState {
54
  export class StreamingMessageParser {
55
  #messages = new Map<string, MessageState>();
56
 
57
- constructor(private _options: StreamingMessageParserOptions = {}) {}
58
 
59
  parse(messageId: string, input: string) {
60
  let state = this.#messages.get(messageId);
@@ -256,7 +256,7 @@ export class StreamingMessageParser {
256
  }
257
 
258
  (actionAttributes as FileAction).filePath = filePath;
259
- } else if (actionType !== 'shell') {
260
  logger.warn(`Unknown action type '${actionType}'`);
261
  }
262
 
 
54
  export class StreamingMessageParser {
55
  #messages = new Map<string, MessageState>();
56
 
57
+ constructor(private _options: StreamingMessageParserOptions = {}) { }
58
 
59
  parse(messageId: string, input: string) {
60
  let state = this.#messages.get(messageId);
 
256
  }
257
 
258
  (actionAttributes as FileAction).filePath = filePath;
259
+ } else if (!(['shell', 'start'].includes(actionType))) {
260
  logger.warn(`Unknown action type '${actionType}'`);
261
  }
262
 
app/lib/stores/terminal.ts CHANGED
@@ -1,14 +1,15 @@
1
  import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import { atom, type WritableAtom } from 'nanostores';
3
  import type { ITerminal } from '~/types/terminal';
4
- import { newShellProcess } from '~/utils/shell';
5
  import { coloredText } from '~/utils/terminal';
6
 
7
  export class TerminalStore {
8
  #webcontainer: Promise<WebContainer>;
9
  #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
 
10
 
11
- showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(false);
12
 
13
  constructor(webcontainerPromise: Promise<WebContainer>) {
14
  this.#webcontainer = webcontainerPromise;
@@ -17,10 +18,22 @@ export class TerminalStore {
17
  import.meta.hot.data.showTerminal = this.showTerminal;
18
  }
19
  }
 
 
 
20
 
21
  toggleTerminal(value?: boolean) {
22
  this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
23
  }
 
 
 
 
 
 
 
 
 
24
 
25
  async attachTerminal(terminal: ITerminal) {
26
  try {
 
1
  import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import { atom, type WritableAtom } from 'nanostores';
3
  import type { ITerminal } from '~/types/terminal';
4
+ import { newBoltShellProcess, newShellProcess } from '~/utils/shell';
5
  import { coloredText } from '~/utils/terminal';
6
 
7
  export class TerminalStore {
8
  #webcontainer: Promise<WebContainer>;
9
  #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
10
+ #boltTerminal = newBoltShellProcess()
11
 
12
+ showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(true);
13
 
14
  constructor(webcontainerPromise: Promise<WebContainer>) {
15
  this.#webcontainer = webcontainerPromise;
 
18
  import.meta.hot.data.showTerminal = this.showTerminal;
19
  }
20
  }
21
+ get boltTerminal() {
22
+ return this.#boltTerminal;
23
+ }
24
 
25
  toggleTerminal(value?: boolean) {
26
  this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
27
  }
28
+ async attachBoltTerminal(terminal: ITerminal) {
29
+ try {
30
+ let wc = await this.#webcontainer
31
+ await this.#boltTerminal.init(wc, terminal)
32
+ } catch (error: any) {
33
+ terminal.write(coloredText.red('Failed to spawn bolt shell\n\n') + error.message);
34
+ return;
35
+ }
36
+ }
37
 
38
  async attachTerminal(terminal: ITerminal) {
39
  try {
app/lib/stores/workbench.ts CHANGED
@@ -12,6 +12,7 @@ import { TerminalStore } from './terminal';
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
  import { Octokit } from "@octokit/rest";
 
15
 
16
  export interface ArtifactState {
17
  id: string;
@@ -39,6 +40,7 @@ export class WorkbenchStore {
39
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
40
  modifiedFiles = new Set<string>();
41
  artifactIdList: string[] = [];
 
42
 
43
  constructor() {
44
  if (import.meta.hot) {
@@ -76,6 +78,9 @@ export class WorkbenchStore {
76
  get showTerminal() {
77
  return this.#terminalStore.showTerminal;
78
  }
 
 
 
79
 
80
  toggleTerminal(value?: boolean) {
81
  this.#terminalStore.toggleTerminal(value);
@@ -84,6 +89,10 @@ export class WorkbenchStore {
84
  attachTerminal(terminal: ITerminal) {
85
  this.#terminalStore.attachTerminal(terminal);
86
  }
 
 
 
 
87
 
88
  onTerminalResize(cols: number, rows: number) {
89
  this.#terminalStore.onTerminalResize(cols, rows);
@@ -232,7 +241,7 @@ export class WorkbenchStore {
232
  id,
233
  title,
234
  closed: false,
235
- runner: new ActionRunner(webcontainer),
236
  });
237
  }
238
 
@@ -336,20 +345,20 @@ export class WorkbenchStore {
336
  }
337
 
338
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
339
-
340
  try {
341
  // Get the GitHub auth token from environment variables
342
  const githubToken = ghToken;
343
-
344
  const owner = githubUsername;
345
-
346
  if (!githubToken) {
347
  throw new Error('GitHub token is not set in environment variables');
348
  }
349
-
350
  // Initialize Octokit with the auth token
351
  const octokit = new Octokit({ auth: githubToken });
352
-
353
  // Check if the repository already exists before creating it
354
  let repo
355
  try {
@@ -368,13 +377,13 @@ export class WorkbenchStore {
368
  throw error; // Some other error occurred
369
  }
370
  }
371
-
372
  // Get all files
373
  const files = this.files.get();
374
  if (!files || Object.keys(files).length === 0) {
375
  throw new Error('No files found to push');
376
  }
377
-
378
  // Create blobs for each file
379
  const blobs = await Promise.all(
380
  Object.entries(files).map(async ([filePath, dirent]) => {
@@ -389,13 +398,13 @@ export class WorkbenchStore {
389
  }
390
  })
391
  );
392
-
393
  const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
394
-
395
  if (validBlobs.length === 0) {
396
  throw new Error('No valid files to push');
397
  }
398
-
399
  // Get the latest commit SHA (assuming main branch, update dynamically if needed)
400
  const { data: ref } = await octokit.git.getRef({
401
  owner: repo.owner.login,
@@ -403,7 +412,7 @@ export class WorkbenchStore {
403
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
404
  });
405
  const latestCommitSha = ref.object.sha;
406
-
407
  // Create a new tree
408
  const { data: newTree } = await octokit.git.createTree({
409
  owner: repo.owner.login,
@@ -416,7 +425,7 @@ export class WorkbenchStore {
416
  sha: blob!.sha,
417
  })),
418
  });
419
-
420
  // Create a new commit
421
  const { data: newCommit } = await octokit.git.createCommit({
422
  owner: repo.owner.login,
@@ -425,7 +434,7 @@ export class WorkbenchStore {
425
  tree: newTree.sha,
426
  parents: [latestCommitSha],
427
  });
428
-
429
  // Update the reference
430
  await octokit.git.updateRef({
431
  owner: repo.owner.login,
@@ -433,7 +442,7 @@ export class WorkbenchStore {
433
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
434
  sha: newCommit.sha,
435
  });
436
-
437
  alert(`Repository created and code pushed: ${repo.html_url}`);
438
  } catch (error) {
439
  console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error));
 
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
  import { Octokit } from "@octokit/rest";
15
+ import type { WebContainerProcess } from '@webcontainer/api';
16
 
17
  export interface ArtifactState {
18
  id: string;
 
40
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
41
  modifiedFiles = new Set<string>();
42
  artifactIdList: string[] = [];
43
+ #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined;
44
 
45
  constructor() {
46
  if (import.meta.hot) {
 
78
  get showTerminal() {
79
  return this.#terminalStore.showTerminal;
80
  }
81
+ get boltTerminal() {
82
+ return this.#terminalStore.boltTerminal;
83
+ }
84
 
85
  toggleTerminal(value?: boolean) {
86
  this.#terminalStore.toggleTerminal(value);
 
89
  attachTerminal(terminal: ITerminal) {
90
  this.#terminalStore.attachTerminal(terminal);
91
  }
92
+ attachBoltTerminal(terminal: ITerminal) {
93
+
94
+ this.#terminalStore.attachBoltTerminal(terminal);
95
+ }
96
 
97
  onTerminalResize(cols: number, rows: number) {
98
  this.#terminalStore.onTerminalResize(cols, rows);
 
241
  id,
242
  title,
243
  closed: false,
244
+ runner: new ActionRunner(webcontainer, () => this.boltTerminal),
245
  });
246
  }
247
 
 
345
  }
346
 
347
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
348
+
349
  try {
350
  // Get the GitHub auth token from environment variables
351
  const githubToken = ghToken;
352
+
353
  const owner = githubUsername;
354
+
355
  if (!githubToken) {
356
  throw new Error('GitHub token is not set in environment variables');
357
  }
358
+
359
  // Initialize Octokit with the auth token
360
  const octokit = new Octokit({ auth: githubToken });
361
+
362
  // Check if the repository already exists before creating it
363
  let repo
364
  try {
 
377
  throw error; // Some other error occurred
378
  }
379
  }
380
+
381
  // Get all files
382
  const files = this.files.get();
383
  if (!files || Object.keys(files).length === 0) {
384
  throw new Error('No files found to push');
385
  }
386
+
387
  // Create blobs for each file
388
  const blobs = await Promise.all(
389
  Object.entries(files).map(async ([filePath, dirent]) => {
 
398
  }
399
  })
400
  );
401
+
402
  const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
403
+
404
  if (validBlobs.length === 0) {
405
  throw new Error('No valid files to push');
406
  }
407
+
408
  // Get the latest commit SHA (assuming main branch, update dynamically if needed)
409
  const { data: ref } = await octokit.git.getRef({
410
  owner: repo.owner.login,
 
412
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
413
  });
414
  const latestCommitSha = ref.object.sha;
415
+
416
  // Create a new tree
417
  const { data: newTree } = await octokit.git.createTree({
418
  owner: repo.owner.login,
 
425
  sha: blob!.sha,
426
  })),
427
  });
428
+
429
  // Create a new commit
430
  const { data: newCommit } = await octokit.git.createCommit({
431
  owner: repo.owner.login,
 
434
  tree: newTree.sha,
435
  parents: [latestCommitSha],
436
  });
437
+
438
  // Update the reference
439
  await octokit.git.updateRef({
440
  owner: repo.owner.login,
 
442
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
443
  sha: newCommit.sha,
444
  });
445
+
446
  alert(`Repository created and code pushed: ${repo.html_url}`);
447
  } catch (error) {
448
  console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error));
app/types/actions.ts CHANGED
@@ -13,6 +13,10 @@ export interface ShellAction extends BaseAction {
13
  type: 'shell';
14
  }
15
 
16
- export type BoltAction = FileAction | ShellAction;
 
 
 
 
17
 
18
  export type BoltActionData = BoltAction | BaseAction;
 
13
  type: 'shell';
14
  }
15
 
16
+ export interface StartAction extends BaseAction {
17
+ type: 'start';
18
+ }
19
+
20
+ export type BoltAction = FileAction | ShellAction | StartAction;
21
 
22
  export type BoltActionData = BoltAction | BaseAction;
app/types/terminal.ts CHANGED
@@ -5,4 +5,5 @@ export interface ITerminal {
5
  reset: () => void;
6
  write: (data: string) => void;
7
  onData: (cb: (data: string) => void) => void;
 
8
  }
 
5
  reset: () => void;
6
  write: (data: string) => void;
7
  onData: (cb: (data: string) => void) => void;
8
+ input: (data: string) => void;
9
  }
app/utils/shell.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { WebContainer } from '@webcontainer/api';
2
  import type { ITerminal } from '~/types/terminal';
3
  import { withResolvers } from './promises';
 
4
 
5
  export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
6
  const args: string[] = [];
@@ -19,7 +20,6 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
19
  const jshReady = withResolvers<void>();
20
 
21
  let isInteractive = false;
22
-
23
  output.pipeTo(
24
  new WritableStream({
25
  write(data) {
@@ -40,6 +40,8 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
40
  );
41
 
42
  terminal.onData((data) => {
 
 
43
  if (isInteractive) {
44
  input.write(data);
45
  }
@@ -49,3 +51,129 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
49
 
50
  return process;
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import type { ITerminal } from '~/types/terminal';
3
  import { withResolvers } from './promises';
4
+ import { atom } from 'nanostores';
5
 
6
  export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
7
  const args: string[] = [];
 
20
  const jshReady = withResolvers<void>();
21
 
22
  let isInteractive = false;
 
23
  output.pipeTo(
24
  new WritableStream({
25
  write(data) {
 
40
  );
41
 
42
  terminal.onData((data) => {
43
+ // console.log('terminal onData', { data, isInteractive });
44
+
45
  if (isInteractive) {
46
  input.write(data);
47
  }
 
51
 
52
  return process;
53
  }
54
+
55
+
56
+
57
+ export class BoltShell {
58
+ #initialized: (() => void) | undefined
59
+ #readyPromise: Promise<void>
60
+ #webcontainer: WebContainer | undefined
61
+ #terminal: ITerminal | undefined
62
+ #process: WebContainerProcess | undefined
63
+ executionState = atom<{ sessionId: string, active: boolean } | undefined>()
64
+ #outputStream: ReadableStream<string> | undefined
65
+ constructor() {
66
+ this.#readyPromise = new Promise((resolve) => {
67
+ this.#initialized = resolve
68
+ })
69
+ }
70
+ ready() {
71
+ return this.#readyPromise;
72
+ }
73
+ async init(webcontainer: WebContainer, terminal: ITerminal) {
74
+ this.#webcontainer = webcontainer
75
+ this.#terminal = terminal
76
+ let callback = (data: string) => {
77
+ console.log(data)
78
+ }
79
+ let { process, output } = await this.newBoltShellProcess(webcontainer, terminal)
80
+ this.#process = process
81
+ this.#outputStream = output
82
+ this.#initialized?.()
83
+ }
84
+ get terminal() {
85
+ return this.#terminal
86
+ }
87
+ get process() {
88
+ return this.#process
89
+ }
90
+ async executeCommand(sessionId: string, command: string) {
91
+ if (!this.process || !this.terminal) {
92
+ return
93
+ }
94
+ let state = this.executionState.get()
95
+ if (state && state.sessionId !== sessionId && state.active) {
96
+ this.terminal.input('\x03');
97
+ }
98
+ this.executionState.set({ sessionId, active: true })
99
+ this.terminal.input(command.trim() + '\n');
100
+ let resp = await this.getCurrentExecutionResult()
101
+ this.executionState.set({ sessionId, active: false })
102
+ return resp
103
+
104
+ }
105
+ async newBoltShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
106
+ const args: string[] = [];
107
+
108
+ // we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal
109
+ const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {
110
+ terminal: {
111
+ cols: terminal.cols ?? 80,
112
+ rows: terminal.rows ?? 15,
113
+ },
114
+ });
115
+
116
+ const input = process.input.getWriter();
117
+ const [internalOutput, terminalOutput] = process.output.tee();
118
+
119
+ const jshReady = withResolvers<void>();
120
+
121
+ let isInteractive = false;
122
+ terminalOutput.pipeTo(
123
+ new WritableStream({
124
+ write(data) {
125
+ if (!isInteractive) {
126
+ const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || [];
127
+
128
+ if (osc === 'interactive') {
129
+ // wait until we see the interactive OSC
130
+ isInteractive = true;
131
+
132
+ jshReady.resolve();
133
+ }
134
+ }
135
+
136
+ terminal.write(data);
137
+ },
138
+ }),
139
+ );
140
+
141
+ terminal.onData((data) => {
142
+ // console.log('terminal onData', { data, isInteractive });
143
+
144
+ if (isInteractive) {
145
+ input.write(data);
146
+ }
147
+ });
148
+
149
+ await jshReady.promise;
150
+
151
+ return { process, output: internalOutput };
152
+ }
153
+ async getCurrentExecutionResult() {
154
+ let fullOutput = '';
155
+ let exitCode: number = 0;
156
+ if (!this.#outputStream) return;
157
+ let tappedStream = this.#outputStream.getReader()
158
+
159
+ while (true) {
160
+ const { value, done } = await tappedStream.read();
161
+ if (done) break;
162
+ const text = value || '';
163
+ fullOutput += text;
164
+
165
+ // Check if command completion signal with exit code
166
+ const exitMatch = fullOutput.match(/\]654;exit=-?\d+:(\d+)/);
167
+ if (exitMatch) {
168
+ console.log(exitMatch);
169
+ exitCode = parseInt(exitMatch[1], 10);
170
+ tappedStream.releaseLock()
171
+ break;
172
+ }
173
+ }
174
+ return { output: fullOutput, exitCode };
175
+ }
176
+ }
177
+ export function newBoltShellProcess() {
178
+ return new BoltShell();
179
+ }