hologit 0.43.2 → 0.44.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 (3) hide show
  1. package/index.d.ts +83 -73
  2. package/lib/Studio.js +141 -126
  3. package/package.json +2 -3
package/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ declare module 'git-client' {
2
+ export class Git {
3
+ constructor(options: { gitDir: string; workTree?: string });
4
+ }
5
+ }
6
+
1
7
  declare module 'hologit' {
2
8
  import { Git as GitClient } from 'git-client';
3
9
  import { Docker } from 'dockerode';
@@ -60,15 +66,15 @@ declare module 'hologit' {
60
66
  }
61
67
 
62
68
  export class Git {
63
- static async get(): Promise<typeof GitClient>;
69
+ static get(): Promise<typeof GitClient>;
64
70
  constructor(options: GitOptions);
65
71
  gitDir: string;
66
72
  workTree?: string;
67
73
  }
68
74
 
69
75
  export class BlobObject {
70
- static async write(repo: Repo, content: string): Promise<BlobObject>;
71
- static async writeFromFile(repo: Repo, filePath: string): Promise<BlobObject>;
76
+ static write(repo: Repo, content: string): Promise<BlobObject>;
77
+ static writeFromFile(repo: Repo, filePath: string): Promise<BlobObject>;
72
78
 
73
79
  constructor(repo: Repo, options: GitObjectOptions);
74
80
 
@@ -78,12 +84,12 @@ declare module 'hologit' {
78
84
  isBlob: boolean;
79
85
  type: 'blob';
80
86
 
81
- async read(): Promise<string>;
87
+ read(): Promise<string>;
82
88
  }
83
89
 
84
90
  export class TreeObject {
85
91
  static getEmptyTreeHash(): string;
86
- static async createFromRef(repo: Repo, ref: string): Promise<TreeObject>;
92
+ static createFromRef(repo: Repo, ref: string): Promise<TreeObject>;
87
93
 
88
94
  constructor(repo: Repo, options?: { hash?: string; parent?: TreeObject | null });
89
95
 
@@ -95,19 +101,19 @@ declare module 'hologit' {
95
101
  type: 'tree';
96
102
  mode: '040000';
97
103
 
98
- async getHash(): Promise<string>;
104
+ getHash(): Promise<string>;
99
105
  getWrittenHash(): string | null;
100
106
  markDirty(): void;
101
- async getChild(childPath: string): Promise<TreeObject | BlobObject | CommitObject | null>;
102
- async writeChild(childPath: string, content: string | BlobObject): Promise<BlobObject>;
103
- async getChildren(): Promise<{ [key: string]: TreeObject | BlobObject | CommitObject }>;
104
- async getBlobMap(): Promise<{ [key: string]: BlobObject }>;
105
- async deleteChild(childPath: string): Promise<void>;
106
- async getSubtree(subtreePath: string, create?: boolean): Promise<TreeObject | null>;
107
- async getSubtreeStack(subtreePath: string, create?: boolean): Promise<TreeObject[] | null>;
108
- async write(): Promise<string>;
109
- async merge(input: TreeObject, options?: MergeOptions, basePath?: string, preloadChildren?: boolean): Promise<void>;
110
- async clone(): Promise<TreeObject>;
107
+ getChild(childPath: string): Promise<TreeObject | BlobObject | CommitObject | null>;
108
+ writeChild(childPath: string, content: string | BlobObject): Promise<BlobObject>;
109
+ getChildren(): Promise<{ [key: string]: TreeObject | BlobObject | CommitObject }>;
110
+ getBlobMap(): Promise<{ [key: string]: BlobObject }>;
111
+ deleteChild(childPath: string): Promise<void>;
112
+ getSubtree(subtreePath: string, create?: boolean): Promise<TreeObject | null>;
113
+ getSubtreeStack(subtreePath: string, create?: boolean): Promise<TreeObject[] | null>;
114
+ write(): Promise<string>;
115
+ merge(input: TreeObject, options?: MergeOptions, basePath?: string, preloadChildren?: boolean): Promise<void>;
116
+ clone(): Promise<TreeObject>;
111
117
  }
112
118
 
113
119
  export class CommitObject {
@@ -128,10 +134,10 @@ declare module 'hologit' {
128
134
 
129
135
  getWorkspace(): Workspace;
130
136
  getRepo(): Repo;
131
- async readConfig(): Promise<any>;
132
- async writeConfig(config?: any): Promise<void>;
133
- async getConfig(): Promise<any>;
134
- async getCachedConfig(): Promise<any>;
137
+ readConfig(): Promise<any>;
138
+ writeConfig(config?: any): Promise<void>;
139
+ getConfig(): Promise<any>;
140
+ getCachedConfig(): Promise<any>;
135
141
  }
136
142
 
137
143
  export class Branch extends Configurable {
@@ -141,17 +147,17 @@ declare module 'hologit' {
141
147
 
142
148
  getKind(): 'holobranch';
143
149
  getConfigPath(): string;
144
- async isDefined(): Promise<boolean>;
150
+ isDefined(): Promise<boolean>;
145
151
  getMapping(key: string): Mapping;
146
- async getMappings(): Promise<Map<string, Mapping>>;
147
- async composite(options: {
152
+ getMappings(): Promise<Map<string, Mapping>>;
153
+ composite(options: {
148
154
  outputTree?: TreeObject;
149
155
  fetch?: boolean | string[];
150
156
  cacheFrom?: string | null;
151
157
  cacheTo?: string | null;
152
158
  }): Promise<TreeObject>;
153
159
  getLens(name: string): Lens;
154
- async getLenses(): Promise<Map<string, Lens>>;
160
+ getLenses(): Promise<Map<string, Lens>>;
155
161
  }
156
162
 
157
163
  export class Source extends Configurable {
@@ -163,21 +169,21 @@ declare module 'hologit' {
163
169
 
164
170
  getKind(): 'holosource';
165
171
  getConfigPath(): string;
166
- async getSpec(): Promise<{ hash: string; ref: string; data: any }>;
167
- async getCachedSpec(): Promise<{ hash: string; ref: string; data: any }>;
168
- async queryRef(): Promise<{ hash: string; ref: string } | null>;
169
- async hashWorkTree(): Promise<string | null>;
170
- async getOutputTree(options?: {
172
+ getSpec(): Promise<{ hash: string; ref: string; data: any }>;
173
+ getCachedSpec(): Promise<{ hash: string; ref: string; data: any }>;
174
+ queryRef(): Promise<{ hash: string; ref: string } | null>;
175
+ hashWorkTree(): Promise<string | null>;
176
+ getOutputTree(options?: {
171
177
  working?: boolean | null;
172
178
  fetch?: boolean | string[];
173
179
  cacheFrom?: string | null;
174
180
  cacheTo?: string | null;
175
181
  }): Promise<string>;
176
- async getHead(options?: { required?: boolean; working?: boolean | null }): Promise<string | null>;
177
- async getCachedHead(): Promise<string | null>;
178
- async getBranch(): Promise<string | null>;
179
- async fetch(options?: { depth?: number; unshallow?: boolean | null }, ...refs: string[]): Promise<{ refs: string[] }>;
180
- async checkout(options?: { submodule?: boolean }): Promise<{
182
+ getHead(options?: { required?: boolean; working?: boolean | null }): Promise<string | null>;
183
+ getCachedHead(): Promise<string | null>;
184
+ getBranch(): Promise<string | null>;
185
+ fetch(options?: { depth?: number; unshallow?: boolean | null }, ...refs: string[]): Promise<{ refs: string[] }>;
186
+ checkout(options?: { submodule?: boolean }): Promise<{
181
187
  path: string;
182
188
  head: string;
183
189
  branch: string | null;
@@ -195,13 +201,13 @@ declare module 'hologit' {
195
201
 
196
202
  getKind(): 'hololens';
197
203
  getConfigPath(): string;
198
- async buildInputTree(inputRoot?: TreeObject): Promise<TreeObject>;
199
- async buildSpec(inputTree: TreeObject): Promise<{
204
+ buildInputTree(inputRoot?: TreeObject): Promise<TreeObject>;
205
+ buildSpec(inputTree: TreeObject): Promise<{
200
206
  hash: string;
201
207
  ref: string;
202
208
  data: any;
203
209
  }>;
204
- async executeSpec(specHash: string, options: {
210
+ executeSpec(specHash: string, options: {
205
211
  refresh?: boolean;
206
212
  save?: boolean;
207
213
  repo?: Repo | null;
@@ -209,7 +215,7 @@ declare module 'hologit' {
209
215
  cacheTo?: string | null;
210
216
  }): Promise<string>;
211
217
 
212
- static async executeSpec(specHash: string, options: {
218
+ static executeSpec(specHash: string, options: {
213
219
  refresh?: boolean;
214
220
  save?: boolean;
215
221
  repo?: Repo | null;
@@ -226,18 +232,18 @@ declare module 'hologit' {
226
232
  getWorkspace(): Workspace;
227
233
  getKind(): 'holospace';
228
234
  getConfigPath(): string;
229
- async writeWorkingChanges(): Promise<void>;
235
+ writeWorkingChanges(): Promise<void>;
230
236
  getBranch(name: string): Branch;
231
- async getBranches(): Promise<Map<string, Branch>>;
237
+ getBranches(): Promise<Map<string, Branch>>;
232
238
  getSource(name: string): Source;
233
- async getSources(): Promise<Map<string, Source>>;
234
- async getLayers(): Promise<Map<string, Map<string, Mapping>>>;
239
+ getSources(): Promise<Map<string, Source>>;
240
+ getLayers(): Promise<Map<string, Map<string, Mapping>>>;
235
241
  getLens(name: string): Lens;
236
- async getLenses(): Promise<Map<string, Lens>>;
242
+ getLenses(): Promise<Map<string, Lens>>;
237
243
  }
238
244
 
239
245
  export class Repo {
240
- static async getFromEnvironment(options?: { ref?: string; working?: boolean }): Promise<Repo>;
246
+ static getFromEnvironment(options?: { ref?: string; working?: boolean }): Promise<Repo>;
241
247
 
242
248
  constructor(options: RepoOptions);
243
249
 
@@ -245,27 +251,27 @@ declare module 'hologit' {
245
251
  ref: string;
246
252
  workTree: string | null;
247
253
 
248
- async getWorkspace(): Promise<Workspace>;
249
- async createWorkspaceFromRef(ref: string): Promise<Workspace>;
250
- async createWorkspaceFromTreeHash(hash: string): Promise<Workspace>;
251
- async getGit(): Promise<GitClient>;
252
- async resolveRef(ref?: string | null): Promise<string | null>;
254
+ getWorkspace(): Promise<Workspace>;
255
+ createWorkspaceFromRef(ref: string): Promise<Workspace>;
256
+ createWorkspaceFromTreeHash(hash: string): Promise<Workspace>;
257
+ getGit(): Promise<GitClient>;
258
+ resolveRef(ref?: string | null): Promise<string | null>;
253
259
  createBlob(options: GitObjectOptions): BlobObject;
254
- async writeBlob(content: string): Promise<BlobObject>;
255
- async writeBlobFromFile(filePath: string): Promise<BlobObject>;
260
+ writeBlob(content: string): Promise<BlobObject>;
261
+ writeBlobFromFile(filePath: string): Promise<BlobObject>;
256
262
  createTree(options?: { hash?: string; parent?: TreeObject | null }): TreeObject;
257
- async createTreeFromRef(ref: string): Promise<TreeObject>;
263
+ createTreeFromRef(ref: string): Promise<TreeObject>;
258
264
  createCommit(options: GitObjectOptions): CommitObject;
259
- async hasCommit(commit: string): Promise<boolean>;
260
- async hashWorkTree(): Promise<string>;
261
- async watch(options: { callback: (treeHash: string, commitHash?: string) => void }): Promise<{
265
+ hasCommit(commit: string): Promise<boolean>;
266
+ hashWorkTree(): Promise<string>;
267
+ watch(options: { callback: (treeHash: string, commitHash?: string) => void }): Promise<{
262
268
  watching: Promise<void>;
263
269
  cancel: () => void;
264
270
  }>;
265
271
  }
266
272
 
267
273
  export class Projection {
268
- static async projectBranch(branch: Branch, options?: ProjectionOptions): Promise<string>;
274
+ static projectBranch(branch: Branch, options?: ProjectionOptions): Promise<string>;
269
275
 
270
276
  constructor(options: { branch: Branch });
271
277
 
@@ -273,27 +279,27 @@ declare module 'hologit' {
273
279
  workspace: Workspace;
274
280
  output: Workspace;
275
281
 
276
- async composite(options: {
282
+ composite(options: {
277
283
  fetch?: boolean | string[];
278
284
  cacheFrom?: string | null;
279
285
  cacheTo?: string | null;
280
286
  }): Promise<void>;
281
- async lens(options: {
287
+ lens(options: {
282
288
  cacheFrom?: string | null;
283
289
  cacheTo?: string | null;
284
290
  }): Promise<void>;
285
- async commit(ref: string, options?: {
291
+ commit(ref: string, options?: {
286
292
  parentCommit?: string | null;
287
293
  commitMessage?: string | null;
288
294
  }): Promise<string>;
289
295
  }
290
296
 
291
297
  export class Studio {
292
- static async cleanup(): Promise<void>;
293
- static async getHab(): Promise<any>;
294
- static async getDocker(): Promise<Docker>;
295
- static async isEnvironmentStudio(): Promise<boolean>;
296
- static async get(gitDir: string): Promise<Studio>;
298
+ static cleanup(): Promise<void>;
299
+ static getHab(): Promise<any>;
300
+ static getDocker(): Promise<Docker>;
301
+ static isEnvironmentStudio(): Promise<boolean>;
302
+ static get(gitDir: string): Promise<Studio>;
297
303
 
298
304
  constructor(options: { gitDir: string; container: any });
299
305
 
@@ -301,11 +307,11 @@ declare module 'hologit' {
301
307
  gitDir: string;
302
308
 
303
309
  isLocal(): boolean;
304
- async habExec(...command: any[]): Promise<string>;
305
- async habPkgExec(pkg: string, bin: string, ...args: any[]): Promise<string>;
306
- async holoExec(...command: any[]): Promise<string>;
307
- async holoLensExec(spec: string): Promise<string>;
308
- async getPackage(query: string, options?: { install?: boolean }): Promise<string | null>;
310
+ habExec(...command: any[]): Promise<string>;
311
+ habPkgExec(pkg: string, bin: string, ...args: any[]): Promise<string>;
312
+ holoExec(...command: any[]): Promise<string>;
313
+ holoLensExec(spec: string): Promise<string>;
314
+ getPackage(query: string, options?: { install?: boolean }): Promise<string | null>;
309
315
  }
310
316
 
311
317
  export class Mapping extends Configurable {
@@ -319,13 +325,17 @@ declare module 'hologit' {
319
325
  getConfigPath(): string;
320
326
  }
321
327
 
322
- export class SpecObject extends BlobObject {
323
- static async write(repo: Repo, kind: string, data: any): Promise<{
328
+ export class SpecObject {
329
+ constructor(repo: Repo, options: GitObjectOptions);
330
+
331
+ repo: Repo;
332
+ hash: string;
333
+ isSpec: boolean;
334
+
335
+ static write(repo: Repo, kind: string, data: any): Promise<{
324
336
  hash: string;
325
337
  ref: string;
326
338
  }>;
327
339
  static buildRef(kind: string, hash: string): string;
328
-
329
- isSpec: boolean;
330
340
  }
331
341
  }
package/lib/Studio.js CHANGED
@@ -1,16 +1,54 @@
1
+ const { spawn } = require('child_process');
1
2
  const stream = require('stream');
2
- const Docker = require('dockerode');
3
3
  const os = require('os');
4
4
  const exitHook = require('async-exit-hook');
5
5
  const fs = require('mz/fs');
6
6
 
7
-
8
7
  const logger = require('./logger');
9
8
 
10
-
11
9
  const studioCache = new Map();
12
- let hab, docker;
10
+ let hab;
11
+
12
+ /**
13
+ * Helper function to execute a Docker CLI command.
14
+ * @param {Array<string>} args - The arguments to pass to the docker command.
15
+ * @param {Object} options - Options for child_process.spawn.
16
+ * @returns {Promise<string>} - Resolves with stdout data.
17
+ */
18
+ function execDocker(args, options = { $relayStderr: false, $relayStdout: false }) {
19
+ logger.debug(`docker ${args.join(' ')}`);
20
+
21
+ return new Promise((resolve, reject) => {
22
+ const dockerProcess = spawn('docker', args, { stdio: 'pipe', ...options });
23
+
24
+ if (options.$relayStderr !== false) {
25
+ dockerProcess.stderr.pipe(process.stderr);
26
+ }
27
+
28
+ if (options.$relayStdout !== false) {
29
+ dockerProcess.stdout.pipe(process.stdout);
30
+ }
31
+
32
+ let stdout = '';
33
+ let stderr = '';
34
+
35
+ dockerProcess.stdout.on('data', (data) => {
36
+ stdout += data.toString();
37
+ });
38
+
39
+ dockerProcess.stderr.on('data', (data) => {
40
+ stderr += data.toString();
41
+ });
13
42
 
43
+ dockerProcess.on('close', (code) => {
44
+ if (code === 0) {
45
+ resolve(stdout.trim());
46
+ } else {
47
+ reject(new Error(stderr.trim()));
48
+ }
49
+ });
50
+ });
51
+ }
14
52
 
15
53
  /**
16
54
  * A studio session that can be used to run multiple commands, using chroot if available or docker container
@@ -23,11 +61,15 @@ class Studio {
23
61
  for (const [gitDir, studio] of studioCache) {
24
62
  const { container } = studio;
25
63
 
26
- if (container && container.type != 'studio') {
64
+ if (container && container.type !== 'studio') {
27
65
  logger.info(`terminating studio container: ${container.id}`);
28
- await container.stop();
29
- await container.remove();
30
- cleanupCount++;
66
+ try {
67
+ await execDocker(['stop', container.id]);
68
+ await execDocker(['rm', container.id]);
69
+ cleanupCount++;
70
+ } catch (err) {
71
+ logger.error(`Failed to stop/remove container ${container.id}: ${err.message}`);
72
+ }
31
73
  }
32
74
 
33
75
  studioCache.delete(gitDir);
@@ -46,19 +88,6 @@ class Studio {
46
88
  return hab;
47
89
  }
48
90
 
49
- static async getDocker () {
50
- if (!docker) {
51
- const { DOCKER_HOST: dockerHost } = process.env;
52
- const dockerHostMatch = dockerHost && dockerHost.match(/^unix:\/\/(\/.*)$/);
53
- const socketPath = dockerHostMatch ? dockerHostMatch[1] : '/var/run/docker.sock';
54
-
55
- docker = new Docker({ socketPath });
56
- logger.info(`connected to docker on: ${socketPath}`);
57
- }
58
-
59
- return docker;
60
- }
61
-
62
91
  static async isEnvironmentStudio () {
63
92
  return Boolean(process.env.STUDIO_TYPE);
64
93
  }
@@ -91,39 +120,9 @@ class Studio {
91
120
  }
92
121
 
93
122
 
94
- // connect with Docker API
95
- const docker = await Studio.getDocker();
96
-
97
-
98
123
  // pull latest studio container
99
124
  try {
100
- await new Promise((resolve, reject) => {
101
- docker.pull('jarvus/hologit-studio:latest', (streamErr, stream) => {
102
- if (streamErr) {
103
- reject(streamErr);
104
- return;
105
- }
106
-
107
- let lastStatus;
108
-
109
- docker.modem.followProgress(
110
- stream,
111
- (err, output) => {
112
- if (err) {
113
- reject(err);
114
- } else {
115
- resolve(output);
116
- }
117
- },
118
- event => {
119
- if (event.status != lastStatus) {
120
- logger.info(`docker pull: ${event.status}`);
121
- lastStatus = event.status;
122
- }
123
- }
124
- );
125
- });
126
- });
125
+ await execDocker(['pull', 'jarvus/hologit-studio:latest'], { $relayStdout: true, $relayStderr: true });
127
126
  } catch (err) {
128
127
  logger.error(`failed to pull studio image via docker: ${err.message}`);
129
128
  }
@@ -157,65 +156,70 @@ class Studio {
157
156
  }
158
157
 
159
158
 
160
- // start studio container
161
- let container;
159
+ // create studio container
160
+ let containerId;
161
+ let defaultUser;
162
162
  try {
163
- container = await docker.createContainer({
164
- Image: 'jarvus/hologit-studio',
165
- Labels: {
166
- 'sh.holo.studio': 'yes'
167
- },
168
- AttachStdin: false,
169
- AttachStdout: true,
170
- AttachStderr: true,
171
- Env: [
172
- 'STUDIO_TYPE=holo',
173
- 'GIT_DIR=/git',
174
- 'GIT_WORK_TREE=/hab/cache',
175
- `DEBUG=${process.env.DEBUG||''}`,
176
- `HAB_LICENSE=accept-no-persist`
177
- ],
178
- WorkingDir: '/git',
179
- Volumes: volumesConfig,
180
- HostConfig: {
181
- Binds: bindsConfig,
182
- // ExposedPorts: {
183
- // "9229/tcp": { }
184
- // },
185
- // PortBindings: {
186
- // '9229/tcp': [
187
- // {
188
- // HostIp: '0.0.0.0',
189
- // HostPort: '9229'
190
- // }
191
- // ]
192
- // }
193
- }
194
- });
163
+ // Prepare environment variables
164
+ const envArgs = [
165
+ '--env', 'STUDIO_TYPE=holo',
166
+ '--env', 'GIT_DIR=/git',
167
+ '--env', 'GIT_WORK_TREE=/hab/cache',
168
+ '--env', `DEBUG=${process.env.DEBUG || ''}`,
169
+ '--env', 'HAB_LICENSE=accept-no-persist'
170
+ ];
171
+
172
+ // Prepare volume bindings
173
+ const volumeArgs = [];
174
+ for (const bind of bindsConfig) {
175
+ volumeArgs.push('-v', bind);
176
+ }
177
+
178
+ // Create container
179
+ const createArgs = [
180
+ 'create',
181
+ '--label', 'sh.holo.studio=yes',
182
+ '--workdir', '/git',
183
+ ...envArgs,
184
+ ...volumeArgs,
185
+ 'jarvus/hologit-studio:latest'
186
+ ];
187
+
188
+ containerId = await execDocker(createArgs);
189
+ containerId = containerId.split('\n').pop().trim(); // Get the container ID from output
195
190
 
196
191
  logger.info('starting studio container');
197
- await container.start();
192
+ await execDocker(['start', containerId]);
198
193
 
199
194
  const { uid, gid, username } = os.userInfo();
200
195
 
201
196
  if (uid && gid && username) {
202
197
  logger.info(`configuring container to use user: ${username}`);
203
- await containerExec(container, 'adduser', '-u', `${uid}`, '-G', 'developer', '-D', username);
204
- await containerExec(container, 'mkdir', '-p', `/home/${username}/.hab`);
205
- await containerExec(container, 'ln', '-sf', '/hab/cache', `/home/${username}/.hab/`);
206
- container.defaultUser = `${uid}`;
198
+ await containerExec({ id: containerId }, 'adduser', '-u', `${uid}`, '-G', 'developer', '-D', username);
199
+ await containerExec({ id: containerId }, 'mkdir', '-p', `/home/${username}/.hab`);
200
+ await containerExec({ id: containerId }, 'ln', '-sf', '/hab/cache', `/home/${username}/.hab/`);
201
+ if (!artifactCachePath) await containerExec({ id: containerId }, 'chown', '-R', `${uid}:${gid}`, '/hab/cache');
202
+ defaultUser = `${uid}`;
207
203
  }
208
204
 
209
- const studio = new Studio({ gitDir, container });
205
+ const studio = new Studio({ gitDir, container: { id: containerId, defaultUser } });
210
206
  studioCache.set(gitDir, studio);
211
207
  return studio;
212
208
 
213
209
  } catch (err) {
214
- logger.error('container failed: %o', err);
210
+ logger.error(`container failed: ${err.message}`);
215
211
 
216
- if (container) {
217
- await container.stop();
218
- await container.remove();
212
+ if (containerId) {
213
+ try {
214
+ await execDocker(['stop', containerId]);
215
+ } catch (stopErr) {
216
+ logger.error(`Failed to stop container ${containerId}: ${stopErr.message}`);
217
+ }
218
+ try {
219
+ await execDocker(['rm', containerId]);
220
+ } catch (rmErr) {
221
+ logger.error(`Failed to remove container ${containerId}: ${rmErr.message}`);
222
+ }
219
223
  }
220
224
  }
221
225
  }
@@ -227,14 +231,14 @@ class Studio {
227
231
  }
228
232
 
229
233
  isLocal () {
230
- return this.container.type == 'studio';
234
+ return this.container.type === 'studio';
231
235
  }
232
236
 
233
237
  /**
234
238
  * Run a command in the studio
235
239
  */
236
240
  async habExec (...command) {
237
- const options = typeof command[command.length-1] == 'object'
241
+ const options = typeof command[command.length-1] === 'object'
238
242
  ? command.pop()
239
243
  : {};
240
244
 
@@ -274,7 +278,7 @@ class Studio {
274
278
  // $env: { PATH }
275
279
  // }
276
280
  // );
277
- if (logger.level == 'debug') {
281
+ if (logger.level === 'debug') {
278
282
  command.unshift('--debug');
279
283
  }
280
284
 
@@ -286,55 +290,66 @@ class Studio {
286
290
  }
287
291
 
288
292
  async getPackage (query, { install } = { install: false }) {
289
- let packagePath = await this.habExec('pkg', 'path', query, { $nullOnError: true, $relayStderr: false });
293
+ let packagePath;
294
+ try {
295
+ packagePath = await this.habExec('pkg', 'path', query, { $nullOnError: true, $relayStderr: false });
296
+ } catch (err) {
297
+ packagePath = null;
298
+ }
290
299
 
291
300
  if (!packagePath && install) {
292
301
  await this.habExec('pkg', 'install', query);
293
- packagePath = await this.habExec('pkg', 'path', query);
302
+ try {
303
+ packagePath = await this.habExec('pkg', 'path', query);
304
+ } catch (err) {
305
+ packagePath = null;
306
+ }
294
307
  }
295
308
 
296
309
  return packagePath ? packagePath.substr(10) : null;
297
310
  }
298
311
  }
299
312
 
300
-
301
313
  exitHook(callback => Studio.cleanup().then(callback));
302
314
 
303
-
315
+ /**
316
+ * Executes a command inside the specified Docker container.
317
+ * @param {Object} container - The container object containing at least the `id` and optionally `defaultUser`.
318
+ * @param {...string} command - The command and its arguments to execute.
319
+ * @returns {Promise<string>} - Resolves with the command's stdout output.
320
+ */
304
321
  async function containerExec (container, ...command) {
305
- const options = typeof command[command.length-1] == 'object'
322
+ const options = typeof command[command.length-1] === 'object'
306
323
  ? command.pop()
307
324
  : {};
308
325
 
309
326
  logger.info(`studio-exec: ${command.join(' ')}`);
310
327
 
311
- const env = [];
328
+ const execArgs = ['exec'];
329
+
330
+ if (options.$user) {
331
+ execArgs.push('--user', options.$user);
332
+ } else if (container.defaultUser) {
333
+ execArgs.push('--user', container.defaultUser);
334
+ }
335
+
312
336
  if (options.$env) {
313
- for (const key of Object.keys(options.$env)) {
314
- env.push(`${key}=${options.$env[key]}`);
337
+ for (const [key, value] of Object.entries(options.$env)) {
338
+ execArgs.push('--env', `${key}=${value}`);
315
339
  }
316
340
  }
317
341
 
318
- const exec = await container.exec({
319
- Cmd: command,
320
- AttachStdout: true,
321
- AttachStderr: options.$relayStderr !== false,
322
- Env: env,
323
- User: `${container.defaultUser || options.$user || ''}`
324
- });
325
-
326
- const execStream = await exec.start();
342
+ execArgs.push(container.id, ...command);
327
343
 
328
- return new Promise((resolve, reject) => {
329
- const output = [];
330
- const outputStream = new stream.PassThrough();
331
-
332
- outputStream.on('data', chunk => output.push(chunk.toString('utf8')));
333
- execStream.on('end', () => resolve(output.join('').trim()));
334
-
335
- container.modem.demuxStream(execStream, outputStream, process.stderr);
336
- });
344
+ try {
345
+ const output = await execDocker(execArgs, { $relayStdout: true, $relayStderr: true });
346
+ return output;
347
+ } catch (err) {
348
+ if (options.$nullOnError) {
349
+ return null;
350
+ }
351
+ throw err;
352
+ }
337
353
  }
338
354
 
339
-
340
355
  module.exports = Studio;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hologit",
3
- "version": "0.43.2",
3
+ "version": "0.44.0",
4
4
  "description": "Hologit automates the projection of layered composite file trees based on flat, declarative plans",
5
5
  "repository": "https://github.com/EmergencePlatform/hologit",
6
6
  "main": "lib/index.js",
@@ -12,9 +12,8 @@
12
12
  "@iarna/toml": "^2.2.5",
13
13
  "async-exit-hook": "^2.0.1",
14
14
  "axios": "^1.7.7",
15
- "chokidar": "^3.5.3",
15
+ "chokidar": "^4.0.1",
16
16
  "debounce": "^2.0.0",
17
- "dockerode": "^4.0.2",
18
17
  "fb-watchman": "^2.0.1",
19
18
  "git-client": "^1.8.3",
20
19
  "hab-client": "^1.1.3",