@theia/dev-container 1.72.0-next.59 → 1.72.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 (90) hide show
  1. package/lib/electron-browser/container-connection-contribution.d.ts +5 -2
  2. package/lib/electron-browser/container-connection-contribution.d.ts.map +1 -1
  3. package/lib/electron-browser/container-connection-contribution.js +130 -30
  4. package/lib/electron-browser/container-connection-contribution.js.map +1 -1
  5. package/lib/electron-browser/container-output-provider.d.ts.map +1 -1
  6. package/lib/electron-browser/container-output-provider.js +3 -1
  7. package/lib/electron-browser/container-output-provider.js.map +1 -1
  8. package/lib/electron-browser/dev-container-frontend-module.d.ts.map +1 -1
  9. package/lib/electron-browser/dev-container-frontend-module.js +5 -0
  10. package/lib/electron-browser/dev-container-frontend-module.js.map +1 -1
  11. package/lib/electron-browser/dev-container-startup-contribution.d.ts +15 -0
  12. package/lib/electron-browser/dev-container-startup-contribution.d.ts.map +1 -0
  13. package/lib/electron-browser/dev-container-startup-contribution.js +94 -0
  14. package/lib/electron-browser/dev-container-startup-contribution.js.map +1 -0
  15. package/lib/electron-common/dev-container-preferences.d.ts +12 -0
  16. package/lib/electron-common/dev-container-preferences.d.ts.map +1 -0
  17. package/lib/electron-common/dev-container-preferences.js +44 -0
  18. package/lib/electron-common/dev-container-preferences.js.map +1 -0
  19. package/lib/electron-common/remote-container-connection-provider.d.ts +20 -1
  20. package/lib/electron-common/remote-container-connection-provider.d.ts.map +1 -1
  21. package/lib/electron-node/dev-container-backend-module.d.ts.map +1 -1
  22. package/lib/electron-node/dev-container-backend-module.js +4 -0
  23. package/lib/electron-node/dev-container-backend-module.js.map +1 -1
  24. package/lib/electron-node/dev-container-cli-contribution.d.ts +19 -0
  25. package/lib/electron-node/dev-container-cli-contribution.d.ts.map +1 -0
  26. package/lib/electron-node/dev-container-cli-contribution.js +66 -0
  27. package/lib/electron-node/dev-container-cli-contribution.js.map +1 -0
  28. package/lib/electron-node/dev-container-cli-contribution.spec.d.ts +2 -0
  29. package/lib/electron-node/dev-container-cli-contribution.spec.d.ts.map +1 -0
  30. package/lib/electron-node/dev-container-cli-contribution.spec.js +91 -0
  31. package/lib/electron-node/dev-container-cli-contribution.spec.js.map +1 -0
  32. package/lib/electron-node/dev-container-file-service.d.ts +4 -4
  33. package/lib/electron-node/dev-container-file-service.d.ts.map +1 -1
  34. package/lib/electron-node/dev-container-file-service.js +9 -9
  35. package/lib/electron-node/dev-container-file-service.js.map +1 -1
  36. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts +6 -2
  37. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts.map +1 -1
  38. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js +24 -4
  39. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js.map +1 -1
  40. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts +7 -6
  41. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts.map +1 -1
  42. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js +4 -9
  43. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js.map +1 -1
  44. package/lib/electron-node/devcontainer-util.d.ts +19 -0
  45. package/lib/electron-node/devcontainer-util.d.ts.map +1 -0
  46. package/lib/electron-node/devcontainer-util.js +48 -0
  47. package/lib/electron-node/devcontainer-util.js.map +1 -0
  48. package/lib/electron-node/devcontainer-util.spec.d.ts +2 -0
  49. package/lib/electron-node/devcontainer-util.spec.d.ts.map +1 -0
  50. package/lib/electron-node/devcontainer-util.spec.js +128 -0
  51. package/lib/electron-node/devcontainer-util.spec.js.map +1 -0
  52. package/lib/electron-node/docker-container-service.d.ts +3 -3
  53. package/lib/electron-node/docker-container-service.d.ts.map +1 -1
  54. package/lib/electron-node/docker-container-service.js +3 -4
  55. package/lib/electron-node/docker-container-service.js.map +1 -1
  56. package/lib/electron-node/remote-container-connection-provider.d.ts +27 -66
  57. package/lib/electron-node/remote-container-connection-provider.d.ts.map +1 -1
  58. package/lib/electron-node/remote-container-connection-provider.js +269 -311
  59. package/lib/electron-node/remote-container-connection-provider.js.map +1 -1
  60. package/lib/electron-node/remote-docker-container-connection.d.ts +50 -0
  61. package/lib/electron-node/remote-docker-container-connection.d.ts.map +1 -0
  62. package/lib/electron-node/remote-docker-container-connection.js +239 -0
  63. package/lib/electron-node/remote-docker-container-connection.js.map +1 -0
  64. package/lib/electron-node/remote-docker-container-connection.spec.d.ts +2 -0
  65. package/lib/electron-node/remote-docker-container-connection.spec.d.ts.map +1 -0
  66. package/lib/electron-node/remote-docker-container-connection.spec.js +217 -0
  67. package/lib/electron-node/remote-docker-container-connection.spec.js.map +1 -0
  68. package/package.json +7 -7
  69. package/src/electron-browser/container-connection-contribution.ts +155 -38
  70. package/src/electron-browser/container-output-provider.ts +3 -1
  71. package/src/electron-browser/dev-container-frontend-module.ts +6 -0
  72. package/src/electron-browser/dev-container-startup-contribution.ts +99 -0
  73. package/src/electron-common/dev-container-preferences.ts +53 -0
  74. package/src/electron-common/remote-container-connection-provider.ts +23 -1
  75. package/src/electron-node/dev-container-backend-module.ts +5 -0
  76. package/src/electron-node/dev-container-cli-contribution.spec.ts +106 -0
  77. package/src/electron-node/dev-container-cli-contribution.ts +68 -0
  78. package/src/electron-node/dev-container-file-service.ts +10 -10
  79. package/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts +29 -5
  80. package/src/electron-node/devcontainer-contributions/variable-resolver-contribution.ts +11 -11
  81. package/src/electron-node/devcontainer-util.spec.ts +154 -0
  82. package/src/electron-node/devcontainer-util.ts +49 -0
  83. package/src/electron-node/docker-container-service.ts +6 -7
  84. package/src/electron-node/remote-container-connection-provider.ts +274 -366
  85. package/src/electron-node/{remote-container-connection-provider.spec.ts → remote-docker-container-connection.spec.ts} +105 -4
  86. package/src/electron-node/remote-docker-container-connection.ts +290 -0
  87. package/lib/electron-node/remote-container-connection-provider.spec.d.ts +0 -2
  88. package/lib/electron-node/remote-container-connection-provider.spec.d.ts.map +0 -1
  89. package/lib/electron-node/remote-container-connection-provider.spec.js +0 -131
  90. package/lib/electron-node/remote-container-connection-provider.spec.js.map +0 -1
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@theia/dev-container",
3
- "version": "1.72.0-next.59+044f7bfc5",
3
+ "version": "1.72.1",
4
4
  "description": "Theia - Editor Preview Extension",
5
5
  "dependencies": {
6
- "@theia/core": "1.72.0-next.59+044f7bfc5",
7
- "@theia/output": "1.72.0-next.59+044f7bfc5",
8
- "@theia/remote": "1.72.0-next.59+044f7bfc5",
9
- "@theia/workspace": "1.72.0-next.59+044f7bfc5",
6
+ "@theia/core": "1.72.1",
7
+ "@theia/output": "1.72.1",
8
+ "@theia/remote": "1.72.1",
9
+ "@theia/workspace": "1.72.1",
10
10
  "dockerode": "^4.0.12",
11
11
  "jsonc-parser": "^3.3.1",
12
12
  "uuid": "^8.3.2"
@@ -45,11 +45,11 @@
45
45
  "watch": "theiaext watch"
46
46
  },
47
47
  "devDependencies": {
48
- "@theia/ext-scripts": "1.71.0",
48
+ "@theia/ext-scripts": "1.72.1",
49
49
  "@types/dockerode": "^3.3.47"
50
50
  },
51
51
  "nyc": {
52
52
  "extends": "../../configs/nyc.json"
53
53
  },
54
- "gitHead": "044f7bfc503932372f324434beabbf5422dc583f"
54
+ "gitHead": "037c4b3e52055fc9962c9761564f431fc4864eb4"
55
55
  }
@@ -16,15 +16,20 @@
16
16
 
17
17
  import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
18
18
  import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
19
- import { DevContainerFile, LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
19
+ import {
20
+ DevContainerFile, LastContainerInfo, RemoteContainerConnectionProvider,
21
+ RunningContainerInfo, WorkspaceCandidate
22
+ } from '../electron-common/remote-container-connection-provider';
20
23
  import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
21
- import { Command, MaybePromise, MessageService, nls, QuickInputService, URI } from '@theia/core';
24
+ import { Command, ILogger, MaybePromise, MessageService, nls, QuickInputService, URI } from '@theia/core';
22
25
  import { WorkspaceInput, WorkspaceOpenHandlerContribution, WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
23
26
  import { ContainerOutputProvider } from './container-output-provider';
24
27
  import { WorkspaceServer } from '@theia/workspace/lib/common';
25
28
  import { DEV_CONTAINER_PATH_QUERY, DEV_CONTAINER_WORKSPACE_SCHEME } from '../electron-common/dev-container-workspaces';
26
29
  import { RemotePreferences } from '@theia/remote/lib/electron-common/remote-preferences';
27
30
  import { LocalStorageService } from '@theia/core/lib/browser';
31
+ import { ConfirmDialog, Dialog } from '@theia/core/lib/browser/dialogs';
32
+ import { DevContainerPreferences } from '../electron-common/dev-container-preferences';
28
33
 
29
34
  export namespace RemoteContainerCommands {
30
35
  export const REOPEN_IN_CONTAINER = Command.toLocalizedCommand({
@@ -32,7 +37,6 @@ export namespace RemoteContainerCommands {
32
37
  label: 'Reopen in Container',
33
38
  category: 'Dev Container'
34
39
  }, 'theia/remote/dev-container/connect');
35
-
36
40
  export const ATTACH_TO_CONTAINER = Command.toLocalizedCommand({
37
41
  id: 'dev-container:attach-to-container',
38
42
  label: 'Attach to Running Container',
@@ -85,6 +89,12 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
85
89
  @inject(ContainerOutputProvider)
86
90
  protected readonly containerOutputProvider: ContainerOutputProvider;
87
91
 
92
+ @inject(DevContainerPreferences)
93
+ protected readonly devContainerPreferences: DevContainerPreferences;
94
+
95
+ @inject(ILogger)
96
+ protected readonly logger: ILogger;
97
+
88
98
  protected hasDevContainerFiles = false;
89
99
 
90
100
  @postConstruct()
@@ -123,13 +133,13 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
123
133
  execute: () => this.openInContainer(),
124
134
  isVisible: () => !this.isRemoteSession() && this.hasDevContainerFiles
125
135
  });
126
- registry.registerCommand(RemoteContainerCommands.ATTACH_TO_CONTAINER, {
127
- execute: () => this.attachToContainer()
128
- });
129
136
  registry.registerCommand(RemoteContainerCommands.REBUILD_CONTAINER, {
130
137
  execute: () => this.rebuildContainer(),
131
138
  isVisible: () => this.isRemoteSession()
132
139
  });
140
+ registry.registerCommand(RemoteContainerCommands.ATTACH_TO_CONTAINER, {
141
+ execute: () => this.attachToRunningContainer()
142
+ });
133
143
  }
134
144
 
135
145
  protected isRemoteSession(): boolean {
@@ -187,33 +197,6 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
187
197
  this.doOpenInContainer(devcontainerFile);
188
198
  }
189
199
 
190
- async attachToContainer(): Promise<void> {
191
- const containers = await this.connectionProvider.listRunningContainers();
192
- if (containers.length === 0) {
193
- this.messageService.info(nls.localize('theia/remote/dev-container/noRunningContainers', 'No running containers found.'));
194
- return;
195
- }
196
-
197
- const selected = await this.quickInputService.pick(containers.map(container => ({
198
- type: 'item' as const,
199
- label: container.name || container.id.substring(0, 12),
200
- description: container.image,
201
- detail: container.status,
202
- container
203
- })), {
204
- title: nls.localize('theia/remote/dev-container/selectContainer', 'Select a running container to attach to')
205
- });
206
-
207
- if (!selected) {
208
- return;
209
- }
210
-
211
- this.containerOutputProvider.openChannel();
212
-
213
- const connectionResult = await this.connectionProvider.attachToContainer(selected.container.id);
214
- this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
215
- }
216
-
217
200
  async rebuildContainer(): Promise<void> {
218
201
  this.containerOutputProvider.openChannel();
219
202
  const progress = await this.messageService.showProgress({
@@ -230,7 +213,7 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
230
213
  try {
231
214
  await this.connectionProvider.removeContainer(ctx.containerId);
232
215
  } catch (error) {
233
- // Container may already be gone, ignore error
216
+ this.logger.debug('Container removal failed (may already be gone):', error);
234
217
  }
235
218
  const lastContainerKey = `${LAST_USED_CONTAINER}:${ctx.devcontainerFilePath}`;
236
219
  await this.storageService.setData(lastContainerKey, undefined);
@@ -254,7 +237,7 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
254
237
  try {
255
238
  await this.connectionProvider.removeContainer(lastContainerInfo.id);
256
239
  } catch (error) {
257
- // Container may already be gone, ignore error
240
+ this.logger.debug('Container removal failed (may already be gone):', error);
258
241
  }
259
242
  await this.storageService.setData(lastContainerInfoKey, undefined);
260
243
  }
@@ -281,25 +264,159 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
281
264
  workspacePath: hostWorkspacePath
282
265
  });
283
266
 
284
- this.storageService.setData<LastContainerInfo>(lastContainerInfoKey, {
267
+ await this.storageService.setData<LastContainerInfo>(lastContainerInfoKey, {
285
268
  id: connectionResult.containerId,
286
269
  lastUsed: Date.now()
287
270
  });
288
271
 
289
272
  // Store full context so rebuild works from inside the container
290
- this.storageService.setData<DevContainerContext>(ACTIVE_DEV_CONTAINER_CONTEXT, {
273
+ await this.storageService.setData<DevContainerContext>(ACTIVE_DEV_CONTAINER_CONTEXT, {
291
274
  devcontainerFilePath: devcontainerFile.path,
292
275
  devcontainerFileName: devcontainerFile.name,
293
276
  hostWorkspacePath: hostWorkspacePath ?? '',
294
277
  containerId: connectionResult.containerId,
295
278
  });
296
279
 
297
- this.workspaceServer.setMostRecentlyUsedWorkspace(
280
+ await this.workspaceServer.setMostRecentlyUsedWorkspace(
298
281
  `${DEV_CONTAINER_WORKSPACE_SCHEME}:${hostWorkspacePath}?${DEV_CONTAINER_PATH_QUERY}=${devcontainerFile.path}`);
299
282
 
300
283
  this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
301
284
  }
302
285
 
286
+ async attachToRunningContainer(): Promise<void> {
287
+ try {
288
+ const confirmed = await new ConfirmDialog({
289
+ title: nls.localize('theia/remote/dev-container/attachWarningTitle', 'Attach to Container'),
290
+ msg: nls.localize('theia/remote/dev-container/attachWarning',
291
+ 'Attaching to a container will execute code inside it. Only attach to containers whose origin you trust.'),
292
+ ok: nls.localizeByDefault('Continue'),
293
+ cancel: Dialog.CANCEL,
294
+ }).open();
295
+ if (!confirmed) {
296
+ return;
297
+ }
298
+
299
+ const containers = await this.connectionProvider.listRunningContainers();
300
+ if (containers.length === 0) {
301
+ this.messageService.info(
302
+ nls.localize('theia/remote/dev-container/noRunningContainers', 'No running containers found.')
303
+ );
304
+ return;
305
+ }
306
+
307
+ const selectedItem = await this.quickInputService.pick(containers.map(container => ({
308
+ type: 'item' as const,
309
+ label: container.name,
310
+ description: container.image,
311
+ detail: container.status,
312
+ container
313
+ })), {
314
+ title: nls.localize('theia/remote/dev-container/selectContainer', 'Select a running container to attach to')
315
+ });
316
+
317
+ if (!selectedItem) {
318
+ return;
319
+ }
320
+
321
+ const selectedContainer: RunningContainerInfo = selectedItem.container;
322
+
323
+ const candidates: WorkspaceCandidate[] = await this.connectionProvider.getWorkspaceCandidates(selectedContainer.id);
324
+
325
+ const customPathId = '__custom_path__';
326
+ const pathItem = await this.quickInputService.pick([
327
+ ...candidates.map(candidate => ({
328
+ type: 'item' as const,
329
+ label: candidate.path,
330
+ description: candidate.source
331
+ })),
332
+ {
333
+ type: 'item' as const,
334
+ id: customPathId,
335
+ label: nls.localize('theia/remote/dev-container/enterCustomPath', 'Enter custom path...'),
336
+ description: ''
337
+ }
338
+ ], {
339
+ title: nls.localize('theia/remote/dev-container/selectWorkspacePath', 'Select a workspace path inside the container')
340
+ });
341
+
342
+ if (!pathItem) {
343
+ return;
344
+ }
345
+
346
+ let selectedPath: string | undefined;
347
+ if ('id' in pathItem && pathItem.id === customPathId) {
348
+ selectedPath = await this.quickInputService.input({
349
+ prompt: nls.localize('theia/remote/dev-container/enterWorkspacePath', 'Enter the workspace path inside the container'),
350
+ value: '/'
351
+ });
352
+ } else {
353
+ selectedPath = pathItem.label;
354
+ }
355
+
356
+ if (!selectedPath) {
357
+ return;
358
+ }
359
+
360
+ let devcontainerFile: string | undefined;
361
+ const applyConfigPref = this.devContainerPreferences['devcontainer.attach.applyFoundConfig'];
362
+
363
+ if (applyConfigPref !== 'never') {
364
+ const foundConfig = await this.connectionProvider.scanForDevContainerConfig(selectedContainer.id, selectedPath);
365
+ if (foundConfig) {
366
+ if (applyConfigPref === 'always') {
367
+ devcontainerFile = foundConfig;
368
+ } else {
369
+ // 'ask'
370
+ const applyConfig = await new ConfirmDialog({
371
+ title: nls.localize('theia/remote/dev-container/foundConfigTitle', 'Dev Container Configuration Found'),
372
+ msg: nls.localize('theia/remote/dev-container/foundConfigMsg',
373
+ 'A devcontainer.json was found at {0}. Would you like to apply its post-attach settings (extensions, port forwarding, etc.)?',
374
+ foundConfig),
375
+ ok: nls.localizeByDefault('Apply'),
376
+ cancel: Dialog.CANCEL,
377
+ }).open();
378
+ if (applyConfig) {
379
+ devcontainerFile = foundConfig;
380
+ }
381
+ }
382
+ }
383
+ }
384
+
385
+ this.containerOutputProvider.openChannel();
386
+
387
+ const progress = await this.messageService.showProgress({
388
+ text: nls.localize('theia/remote/dev-container/attaching', 'Attaching to container')
389
+ });
390
+
391
+ try {
392
+ const connectionResult = await this.connectionProvider.attachToContainer({
393
+ containerId: selectedContainer.id,
394
+ workspacePath: selectedPath,
395
+ nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
396
+ devcontainerFile,
397
+ });
398
+
399
+ // Store context so rebuild works from inside the container
400
+ await this.storageService.setData<DevContainerContext>(ACTIVE_DEV_CONTAINER_CONTEXT, {
401
+ devcontainerFilePath: devcontainerFile ?? '',
402
+ devcontainerFileName: devcontainerFile ? URI.fromFilePath(devcontainerFile).path.base : '',
403
+ hostWorkspacePath: '',
404
+ containerId: connectionResult.containerId,
405
+ });
406
+
407
+ this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
408
+ } finally {
409
+ progress.cancel();
410
+ }
411
+ } catch (e) {
412
+ this.messageService.error(nls.localize(
413
+ 'theia/remote/dev-container/attachError',
414
+ 'Failed to attach to container: {0}',
415
+ e instanceof Error ? e.message : String(e)
416
+ ));
417
+ }
418
+ }
419
+
303
420
  async getOrSelectDevcontainerFile(): Promise<DevContainerFile | undefined> {
304
421
  const workspace = this.workspaceService.workspace;
305
422
  if (!workspace) {
@@ -31,6 +31,8 @@ export class ContainerOutputProvider implements ContainerOutputProvider {
31
31
  };
32
32
 
33
33
  onRemoteOutput(output: string): void {
34
- this.currentChannel?.appendLine(output);
34
+ if (output) {
35
+ this.currentChannel?.appendLine(output);
36
+ }
35
37
  }
36
38
  }
@@ -24,6 +24,8 @@ import { FrontendApplicationContribution, LabelProviderContribution } from '@the
24
24
  import { WorkspaceOpenHandlerContribution } from '@theia/workspace/lib/browser/workspace-service';
25
25
  import { WindowTitleContribution } from '@theia/core/lib/browser/window/window-title-service';
26
26
  import { DevContainerSuggestionContribution } from './dev-container-suggestion-contribution';
27
+ import { bindDevContainerPreferences } from '../electron-common/dev-container-preferences';
28
+ import { DevContainerStartupContribution } from './dev-container-startup-contribution';
27
29
 
28
30
  export default new ContainerModule(bind => {
29
31
  bind(ContainerConnectionContribution).toSelf().inSingletonScope();
@@ -31,6 +33,7 @@ export default new ContainerModule(bind => {
31
33
  bind(WorkspaceOpenHandlerContribution).toService(ContainerConnectionContribution);
32
34
 
33
35
  bind(ContainerOutputProvider).toSelf().inSingletonScope();
36
+ bindDevContainerPreferences(bind);
34
37
 
35
38
  bind(RemoteContainerConnectionProvider).toDynamicValue(ctx => {
36
39
  const outputProvider = ctx.container.get(ContainerOutputProvider);
@@ -44,4 +47,7 @@ export default new ContainerModule(bind => {
44
47
 
45
48
  bind(DevContainerSuggestionContribution).toSelf().inSingletonScope();
46
49
  bind(FrontendApplicationContribution).toService(DevContainerSuggestionContribution);
50
+
51
+ bind(DevContainerStartupContribution).toSelf().inSingletonScope();
52
+ bind(FrontendApplicationContribution).toService(DevContainerStartupContribution);
47
53
  });
@@ -0,0 +1,99 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { inject, injectable } from '@theia/core/shared/inversify';
18
+ import { FrontendApplicationContribution } from '@theia/core/lib/browser';
19
+ import { RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
20
+ import { AbstractRemoteRegistryContribution } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
21
+ import { ILogger, MessageService, nls } from '@theia/core';
22
+ import { RemotePreferences } from '@theia/remote/lib/electron-common/remote-preferences';
23
+
24
+ @injectable()
25
+ export class DevContainerStartupContribution extends AbstractRemoteRegistryContribution implements FrontendApplicationContribution {
26
+
27
+ @inject(RemoteContainerConnectionProvider)
28
+ protected readonly connectionProvider: RemoteContainerConnectionProvider;
29
+
30
+ @inject(ILogger)
31
+ protected readonly logger: ILogger;
32
+
33
+ @inject(MessageService)
34
+ protected readonly messageService: MessageService;
35
+
36
+ @inject(RemotePreferences)
37
+ protected readonly remotePreferences: RemotePreferences;
38
+
39
+ registerRemoteCommands(): void {
40
+ // no commands to register — this contribution only handles startup
41
+ }
42
+
43
+ onStart(): void {
44
+ this.handleStartupAttach();
45
+ }
46
+
47
+ protected async handleStartupAttach(): Promise<void> {
48
+ try {
49
+ const args = await this.connectionProvider.getAttachContainerArgs();
50
+ if (!args) {
51
+ return;
52
+ }
53
+
54
+ const { containerId, scanForDevJson } = args;
55
+ this.logger.info(`CLI: --attach-container ${containerId}, initiating attach from frontend...`);
56
+
57
+ const containers = await this.connectionProvider.listRunningContainers();
58
+ // Match by ID prefix (either direction — user may pass a short prefix or a full 64-char ID
59
+ // while listRunningContainers returns 12-char truncated IDs) or exact name.
60
+ const matches = containers.filter(c => c.id.startsWith(containerId) || containerId.startsWith(c.id) || c.name === containerId);
61
+ if (matches.length > 1) {
62
+ this.logger.warn(`CLI: container identifier "${containerId}" matches ${matches.length} containers, using first match: ${matches[0].name || matches[0].id}`);
63
+ }
64
+ const target = matches[0];
65
+
66
+ if (!target) {
67
+ const msg = nls.localize('theia/remote/dev-container/cliContainerNotFound',
68
+ 'Container "{0}" not found or not running.', containerId);
69
+ this.logger.error(`CLI: ${msg}`);
70
+ this.messageService.error(msg);
71
+ return;
72
+ }
73
+
74
+ const candidates = await this.connectionProvider.getWorkspaceCandidates(target.id);
75
+ const workspacePath = candidates.length > 0 ? candidates[0].path : '/';
76
+
77
+ const devcontainerFile = scanForDevJson
78
+ ? await this.connectionProvider.scanForDevContainerConfig(target.id, workspacePath)
79
+ : undefined;
80
+
81
+ const result = await this.connectionProvider.attachToContainer({
82
+ containerId: target.id,
83
+ workspacePath,
84
+ devcontainerFile,
85
+ nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
86
+ });
87
+
88
+ this.logger.info(`CLI: startup attach ready, proxy on port ${result.port}, workspace: ${result.workspacePath}`);
89
+ this.openRemote(result.port, false, result.workspacePath);
90
+ } catch (e) {
91
+ this.logger.error('CLI: Failed to attach to container during startup:', e);
92
+ this.messageService.error(nls.localize(
93
+ 'theia/remote/dev-container/cliAttachError',
94
+ 'Failed to attach to container: {0}',
95
+ e instanceof Error ? e.message : String(e)
96
+ ));
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,53 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { interfaces } from '@theia/core/shared/inversify';
18
+ import { PreferenceProxy } from '@theia/core/lib/common/preferences/preference-proxy';
19
+ import { nls } from '@theia/core/lib/common/nls';
20
+ import { PreferenceProxyFactory } from '@theia/core/lib/common/preferences/injectable-preference-proxy';
21
+ import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
22
+
23
+ export const DevContainerPreferenceSchema: PreferenceSchema = {
24
+ properties: {
25
+ 'devcontainer.attach.applyFoundConfig': {
26
+ type: 'string',
27
+ enum: ['always', 'ask', 'never'],
28
+ default: 'ask',
29
+ markdownDescription: nls.localize(
30
+ 'theia/devContainer/attach/applyFoundConfig',
31
+ 'Controls whether to apply a devcontainer.json found inside a container when attaching. '
32
+ + 'Extensions, settings, port forwarding, and post-attach commands from the configuration will be applied.'
33
+ )
34
+ }
35
+ }
36
+ };
37
+
38
+ export interface DevContainerPreferenceConfiguration {
39
+ 'devcontainer.attach.applyFoundConfig': 'always' | 'ask' | 'never';
40
+ }
41
+
42
+ export const DevContainerPreferenceContribution = Symbol('DevContainerPreferenceContribution');
43
+ export const DevContainerPreferences = Symbol('DevContainerPreferences');
44
+ export type DevContainerPreferences = PreferenceProxy<DevContainerPreferenceConfiguration>;
45
+
46
+ export function bindDevContainerPreferences(bind: interfaces.Bind): void {
47
+ bind(DevContainerPreferences).toDynamicValue(ctx => {
48
+ const factory = ctx.container.get<PreferenceProxyFactory>(PreferenceProxyFactory);
49
+ return factory(DevContainerPreferenceSchema);
50
+ }).inSingletonScope();
51
+ bind(DevContainerPreferenceContribution).toConstantValue({ schema: DevContainerPreferenceSchema });
52
+ bind(PreferenceContribution).toService(DevContainerPreferenceContribution);
53
+ }
@@ -50,6 +50,25 @@ export interface RunningContainerInfo {
50
50
  name: string;
51
51
  image: string;
52
52
  status: string;
53
+ created: number;
54
+ }
55
+
56
+ export interface WorkspaceCandidate {
57
+ path: string;
58
+ /** Describes where this candidate came from */
59
+ source: 'working-dir' | 'bind-mount' | 'devcontainer-label' | 'fallback';
60
+ }
61
+
62
+ export interface AttachContainerOptions {
63
+ containerId: string;
64
+ workspacePath: string;
65
+ nodeDownloadTemplate?: string;
66
+ devcontainerFile?: string;
67
+ }
68
+
69
+ export interface AttachContainerArgs {
70
+ containerId: string;
71
+ scanForDevJson: boolean;
53
72
  }
54
73
 
55
74
  export interface RemoteContainerConnectionProvider extends RpcServer<ContainerOutputProvider> {
@@ -57,6 +76,9 @@ export interface RemoteContainerConnectionProvider extends RpcServer<ContainerOu
57
76
  getDevContainerFiles(workspacePath: string): Promise<DevContainerFile[]>;
58
77
  getCurrentContainerInfo(port: number): Promise<ContainerInspectInfo | undefined>;
59
78
  listRunningContainers(): Promise<RunningContainerInfo[]>;
60
- attachToContainer(containerId: string): Promise<ContainerConnectionResult>;
79
+ getWorkspaceCandidates(containerId: string): Promise<WorkspaceCandidate[]>;
80
+ scanForDevContainerConfig(containerId: string, workspacePath: string): Promise<string | undefined>;
81
+ getAttachContainerArgs(): Promise<AttachContainerArgs | undefined>;
82
+ attachToContainer(options: AttachContainerOptions): Promise<ContainerConnectionResult>;
61
83
  removeContainer(containerId: string): Promise<void>;
62
84
  }
@@ -16,6 +16,8 @@
16
16
 
17
17
  import { ContainerModule } from '@theia/core/shared/inversify';
18
18
  import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
19
+ import { DevContainerCliContribution } from './dev-container-cli-contribution';
20
+ import { CliContribution } from '@theia/core/lib/node';
19
21
  import { DevContainerConnectionProvider } from './remote-container-connection-provider';
20
22
  import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPath } from '../electron-common/remote-container-connection-provider';
21
23
  import { ContainerCreationContribution, DockerContainerService } from './docker-container-service';
@@ -66,4 +68,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
66
68
 
67
69
  bind(DevContainerWorkspaceHandler).toSelf().inSingletonScope();
68
70
  bind(WorkspaceHandlerContribution).toService(DevContainerWorkspaceHandler);
71
+
72
+ bind(DevContainerCliContribution).toSelf().inSingletonScope();
73
+ bind(CliContribution).toService(DevContainerCliContribution);
69
74
  });
@@ -0,0 +1,106 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { DevContainerCliContribution } from './dev-container-cli-contribution';
19
+ import { Arguments } from '@theia/core/shared/yargs';
20
+
21
+ function createArgs(overrides: Record<string, unknown> = {}): Arguments {
22
+ return {
23
+ _: [],
24
+ '$0': '',
25
+ ...overrides
26
+ } as unknown as Arguments;
27
+ }
28
+
29
+ describe('DevContainerCliContribution', () => {
30
+
31
+ describe('setArguments / getAttachContainerId', () => {
32
+
33
+ it('should return undefined when --attach-container not provided', () => {
34
+ const contribution = new DevContainerCliContribution();
35
+ contribution.setArguments(createArgs());
36
+ expect(contribution.getAttachContainerId()).to.be.undefined;
37
+ });
38
+
39
+ it('should stash container ID from --attach-container', () => {
40
+ const contribution = new DevContainerCliContribution();
41
+ contribution.setArguments(createArgs({ 'attach-container': 'abc123' }));
42
+ expect(contribution.getAttachContainerId()).to.equal('abc123');
43
+ });
44
+
45
+ it('should convert numeric container ID to string', () => {
46
+ const contribution = new DevContainerCliContribution();
47
+ contribution.setArguments(createArgs({ 'attach-container': 12345 }));
48
+ expect(contribution.getAttachContainerId()).to.equal('12345');
49
+ });
50
+
51
+ it('should ignore empty string --attach-container', () => {
52
+ const contribution = new DevContainerCliContribution();
53
+ contribution.setArguments(createArgs({ 'attach-container': '' }));
54
+ expect(contribution.getAttachContainerId()).to.be.undefined;
55
+ });
56
+
57
+ it('should ignore whitespace-only --attach-container', () => {
58
+ const contribution = new DevContainerCliContribution();
59
+ contribution.setArguments(createArgs({ 'attach-container': ' ' }));
60
+ expect(contribution.getAttachContainerId()).to.be.undefined;
61
+ });
62
+ });
63
+
64
+ describe('consumeAttachContainerId', () => {
65
+
66
+ it('should return the container ID and clear it', () => {
67
+ const contribution = new DevContainerCliContribution();
68
+ contribution.setArguments(createArgs({ 'attach-container': 'abc123' }));
69
+ expect(contribution.consumeAttachContainerId()).to.equal('abc123');
70
+ expect(contribution.consumeAttachContainerId()).to.be.undefined;
71
+ expect(contribution.getAttachContainerId()).to.be.undefined;
72
+ });
73
+
74
+ it('should return undefined when no ID was set', () => {
75
+ const contribution = new DevContainerCliContribution();
76
+ contribution.setArguments(createArgs());
77
+ expect(contribution.consumeAttachContainerId()).to.be.undefined;
78
+ });
79
+ });
80
+
81
+ describe('shouldScanForDevJson', () => {
82
+
83
+ it('should default to true', () => {
84
+ const contribution = new DevContainerCliContribution();
85
+ expect(contribution.shouldScanForDevJson()).to.be.true;
86
+ });
87
+
88
+ it('should remain true when --dev-json is true', () => {
89
+ const contribution = new DevContainerCliContribution();
90
+ contribution.setArguments(createArgs({ 'dev-json': true }));
91
+ expect(contribution.shouldScanForDevJson()).to.be.true;
92
+ });
93
+
94
+ it('should be false when --dev-json is false', () => {
95
+ const contribution = new DevContainerCliContribution();
96
+ contribution.setArguments(createArgs({ 'dev-json': false }));
97
+ expect(contribution.shouldScanForDevJson()).to.be.false;
98
+ });
99
+
100
+ it('should be true when --dev-json is undefined', () => {
101
+ const contribution = new DevContainerCliContribution();
102
+ contribution.setArguments(createArgs({ 'dev-json': undefined }));
103
+ expect(contribution.shouldScanForDevJson()).to.be.true;
104
+ });
105
+ });
106
+ });