@malloy-publisher/server 0.0.88 → 0.0.89

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.
@@ -4,13 +4,18 @@ import { join } from "path";
4
4
  import sinon from "sinon";
5
5
  import { PackageNotFoundError } from "../errors";
6
6
  import { readConnectionConfig } from "./connection";
7
- import { Model } from "./model";
7
+ import { ApiConnection, Model } from "./model";
8
8
  import { Package } from "./package";
9
9
  import { Scheduler } from "./scheduler";
10
10
 
11
11
  // Minimal partial types for mocking
12
12
  type PartialScheduler = Pick<Scheduler, "list">;
13
13
 
14
+ const connectionMocks: ApiConnection[] = [
15
+ { name: "conn1", type: "postgres", postgresConnection: {} },
16
+ { name: "conn2", type: "bigquery", bigqueryConnection: {} },
17
+ ];
18
+
14
19
  describe("service/package", () => {
15
20
  const testPackageDirectory = "testPackage";
16
21
 
@@ -24,10 +29,7 @@ describe("service/package", () => {
24
29
  join(testPackageDirectory, "database.csv"),
25
30
  parquetBuffer,
26
31
  );
27
- const content = JSON.stringify([
28
- { name: "conn1", type: "database" },
29
- { name: "conn2", type: "api" },
30
- ]);
32
+ const content = JSON.stringify(connectionMocks);
31
33
  await fs.writeFile(
32
34
  join(testPackageDirectory, "publisher.connections.json"),
33
35
  content,
@@ -82,7 +84,7 @@ describe("service/package", () => {
82
84
  });
83
85
  it("should return a Package object if the package exists", async () => {
84
86
  sinon.stub(fs, "stat").resolves();
85
- sinon
87
+ const readFileStub = sinon
86
88
  .stub(fs, "readFile")
87
89
  .resolves(
88
90
  Buffer.from(JSON.stringify({ description: "Test package" })),
@@ -98,6 +100,9 @@ describe("service/package", () => {
98
100
  list: () => [],
99
101
  } as PartialScheduler);
100
102
 
103
+ readFileStub.restore();
104
+ readFileStub.resolves(Buffer.from(JSON.stringify([])));
105
+
101
106
  const packageInstance = await Package.create(
102
107
  "testProject",
103
108
  "testPackage",
@@ -221,10 +226,7 @@ describe("service/package", () => {
221
226
  sinon.stub(fs, "stat").resolves();
222
227
  const config = await readConnectionConfig(testPackageDirectory);
223
228
 
224
- expect(config).toEqual([
225
- { name: "conn1", type: "database" },
226
- { name: "conn2", type: "api" },
227
- ]);
229
+ expect(config).toEqual(connectionMocks);
228
230
  });
229
231
  });
230
232
  });
@@ -101,7 +101,10 @@ export class Package {
101
101
  unit: "ms",
102
102
  });
103
103
  const connections = new Map<string, Connection>(projectConnections);
104
-
104
+ logger.info(`Project connections: ${connections.size}`, {
105
+ connections,
106
+ projectConnections,
107
+ });
105
108
  // Package connections override project connections.
106
109
  const { malloyConnections: packageConnections } =
107
110
  await createConnections(packagePath);
@@ -147,6 +150,11 @@ export class Package {
147
150
  malloy_package_name: packageName,
148
151
  status: "success",
149
152
  });
153
+ logger.info(`Successfully loaded package ${packageName}`, {
154
+ packageName,
155
+ duration: executionTime,
156
+ unit: "ms",
157
+ });
150
158
  return new Package(
151
159
  projectName,
152
160
  packageName,
@@ -158,15 +166,14 @@ export class Package {
158
166
  );
159
167
  } catch (error) {
160
168
  logger.error(`Error loading package ${packageName}`, { error });
169
+ console.error(error);
161
170
  const endTime = performance.now();
162
171
  const executionTime = endTime - startTime;
163
172
  this.packageLoadHistogram.record(executionTime, {
164
173
  malloy_package_name: packageName,
165
174
  status: "error",
166
175
  });
167
- throw new Error(`Error loading package ${packageName}`, {
168
- cause: error,
169
- });
176
+ throw error;
170
177
  }
171
178
  }
172
179
 
@@ -1,14 +1,15 @@
1
1
  import { BaseConnection } from "@malloydata/malloy/connection";
2
2
  import { Mutex } from "async-mutex";
3
- import * as fs from "fs/promises";
3
+ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import { components } from "../api";
6
- import { API_PREFIX, README_NAME } from "../constants";
6
+ import { API_PREFIX, PACKAGE_MANIFEST_NAME, README_NAME } from "../constants";
7
7
  import {
8
8
  ConnectionNotFoundError,
9
9
  PackageNotFoundError,
10
10
  ProjectNotFoundError,
11
11
  } from "../errors";
12
+ import { logger } from "../logger";
12
13
  import { createConnections, InternalConnection } from "./connection";
13
14
  import { ApiConnection } from "./model";
14
15
  import { Package } from "./package";
@@ -59,7 +60,7 @@ export class Project {
59
60
  `${API_PREFIX}/projects/`,
60
61
  "",
61
62
  );
62
- if (!(await fs.exists(this.projectPath))) {
63
+ if (!(await fs.promises.exists(this.projectPath))) {
63
64
  throw new ProjectNotFoundError(
64
65
  `Project path "${this.projectPath}" not found`,
65
66
  );
@@ -73,14 +74,24 @@ export class Project {
73
74
  static async create(
74
75
  projectName: string,
75
76
  projectPath: string,
77
+ defaultConnections: ApiConnection[],
76
78
  ): Promise<Project> {
77
- if (!(await fs.stat(projectPath)).isDirectory()) {
79
+ if (!(await fs.promises.stat(projectPath)).isDirectory()) {
78
80
  throw new ProjectNotFoundError(
79
81
  `Project path ${projectPath} not found`,
80
82
  );
81
83
  }
82
- const { malloyConnections, apiConnections } =
83
- await createConnections(projectPath);
84
+ const { malloyConnections, apiConnections } = await createConnections(
85
+ projectPath,
86
+ defaultConnections,
87
+ );
88
+ logger.info(
89
+ `Loaded ${malloyConnections.size + apiConnections.length} connections for project ${projectName}`,
90
+ {
91
+ malloyConnections,
92
+ apiConnections,
93
+ },
94
+ );
84
95
  return new Project(
85
96
  projectName,
86
97
  projectPath,
@@ -104,7 +115,7 @@ export class Project {
104
115
  let readme = "";
105
116
  try {
106
117
  readme = (
107
- await fs.readFile(path.join(this.projectPath, README_NAME))
118
+ await fs.promises.readFile(path.join(this.projectPath, README_NAME))
108
119
  ).toString();
109
120
  } catch {
110
121
  // Readme not found, so we'll just return an empty string
@@ -158,27 +169,33 @@ export class Project {
158
169
  }
159
170
 
160
171
  public async listPackages(): Promise<ApiPackage[]> {
172
+ logger.info("Listing packages", { projectPath: this.projectPath });
161
173
  try {
162
- const files = await fs.readdir(this.projectPath, {
174
+ const files = await fs.promises.readdir(this.projectPath, {
163
175
  withFileTypes: true,
164
176
  });
177
+ const packageDirectories = files.filter(
178
+ (file) =>
179
+ file.isDirectory() &&
180
+ fs.existsSync(
181
+ path.join(this.projectPath, file.name, PACKAGE_MANIFEST_NAME),
182
+ ),
183
+ );
165
184
  const packageMetadata = await Promise.all(
166
- files
167
- .filter((file) => file.isDirectory())
168
- .map(async (directory) => {
169
- try {
170
- return (
171
- await this.getPackage(directory.name, false)
172
- ).getPackageMetadata();
173
- } catch (error) {
174
- console.log(
175
- `Failed to load package: ${directory.name} due to : ${error}`,
176
- );
177
- // Directory did not contain a valid package.json file -- therefore, it's not a package.
178
- // Or it timed out
179
- return undefined;
180
- }
181
- }),
185
+ packageDirectories.map(async (directory) => {
186
+ try {
187
+ return (
188
+ await this.getPackage(directory.name, false)
189
+ ).getPackageMetadata();
190
+ } catch (error) {
191
+ logger.error(
192
+ `Failed to load package: ${directory.name} due to : ${error}`,
193
+ );
194
+ // Directory did not contain a valid package.json file -- therefore, it's not a package.
195
+ // Or it timed out
196
+ return undefined;
197
+ }
198
+ }),
182
199
  );
183
200
  // Get rid of undefined entries (i.e, directories without publisher.json files).
184
201
  const filteredMetadata = packageMetadata.filter(
@@ -186,23 +203,27 @@ export class Project {
186
203
  ) as ApiPackage[];
187
204
  return filteredMetadata;
188
205
  } catch (error) {
189
- throw new Error("Error listing packages: " + error);
206
+ logger.error("Error listing packages", { error });
207
+ console.error(error);
208
+ throw error;
190
209
  }
191
210
  }
192
211
 
193
212
  public async getPackage(
194
213
  packageName: string,
195
- reload: boolean,
214
+ reload: boolean = false,
196
215
  ): Promise<Package> {
197
216
  // We need to acquire the mutex to prevent a thundering herd of requests from creating the
198
217
  // package multiple times.
199
218
  let packageMutex = this.packageMutexes.get(packageName);
200
- if (!packageMutex) {
201
- packageMutex = new Mutex();
202
- this.packageMutexes.set(packageName, packageMutex);
219
+ if (packageMutex?.isLocked()) {
220
+ await packageMutex.waitForUnlock();
221
+ return this.packages.get(packageName)!;
203
222
  }
223
+ packageMutex = new Mutex();
224
+ this.packageMutexes.set(packageName, packageMutex);
204
225
 
205
- return await packageMutex.runExclusive(async () => {
226
+ return packageMutex.runExclusive(async () => {
206
227
  const _package = this.packages.get(packageName);
207
228
  if (_package !== undefined && !reload) {
208
229
  return _package;
@@ -220,6 +241,9 @@ export class Project {
220
241
  } catch (error) {
221
242
  this.packages.delete(packageName);
222
243
  throw error;
244
+ } finally {
245
+ packageMutex.release();
246
+ this.packageMutexes.delete(packageName);
223
247
  }
224
248
  });
225
249
  }
@@ -227,11 +251,18 @@ export class Project {
227
251
  public async addPackage(packageName: string) {
228
252
  const packagePath = path.join(this.projectPath, packageName);
229
253
  if (
230
- !(await fs.exists(packagePath)) ||
231
- !(await fs.stat(packagePath)).isDirectory()
254
+ !(await fs.promises.exists(packagePath)) ||
255
+ !(await fs.promises.stat(packagePath)).isDirectory()
232
256
  ) {
233
257
  throw new PackageNotFoundError(`Package ${packageName} not found`);
234
258
  }
259
+ logger.info(
260
+ `Adding package ${packageName} to project ${this.projectName}`,
261
+ {
262
+ packagePath,
263
+ malloyConnections: this.malloyConnections,
264
+ },
265
+ );
235
266
  this.packages.set(
236
267
  packageName,
237
268
  await Package.create(
@@ -266,9 +297,17 @@ export class Project {
266
297
  if (!_package) {
267
298
  throw new PackageNotFoundError(`Package ${packageName} not found`);
268
299
  }
269
- await fs.rm(path.join(this.projectPath, packageName), {
300
+ await fs.promises.rm(path.join(this.projectPath, packageName), {
270
301
  recursive: true,
271
302
  });
272
303
  this.packages.delete(packageName);
273
304
  }
305
+
306
+ public async serialize(): Promise<ApiProject> {
307
+ return {
308
+ ...this.metadata,
309
+ connections: this.listApiConnections(),
310
+ packages: await this.listPackages(),
311
+ };
312
+ }
274
313
  }
@@ -1,40 +1,52 @@
1
- import { describe, expect, it, mock, spyOn } from "bun:test";
1
+ import {
2
+ afterAll,
3
+ beforeAll,
4
+ describe,
5
+ expect,
6
+ it,
7
+ mock,
8
+ spyOn,
9
+ } from "bun:test";
10
+ import { rmSync } from "fs";
2
11
  import * as fs from "fs/promises";
3
12
  import path from "path";
4
13
  import { isPublisherConfigFrozen } from "../config";
14
+ import { publisherPath } from "../constants";
5
15
  import { FrozenConfigError, ProjectNotFoundError } from "../errors";
16
+ import { logger } from "../logger";
6
17
  import { ProjectStore } from "./project_store";
18
+ import sinon from "sinon";
7
19
 
8
20
  describe("ProjectStore", () => {
9
21
  const serverRoot = path.resolve(
10
22
  process.cwd(),
11
23
  process.env.SERVER_ROOT || ".",
12
24
  );
25
+ let loggerStub: sinon.SinonStub;
13
26
 
14
- async function waitForInitialization(projectStore: ProjectStore) {
15
- const maxRetries = 100;
16
- let retries = 0;
17
- do {
18
- await new Promise((resolve) => setTimeout(resolve, 10));
19
- retries++;
20
- } while (
21
- (await projectStore.listProjects()).length === 0 &&
22
- retries < maxRetries
23
- );
24
- if ((await projectStore.listProjects()).length === 0) {
25
- throw new Error("Timed out initializing ProjectStore");
26
- }
27
- }
27
+ beforeAll(() => {
28
+ rmSync(path.resolve(publisherPath, "malloy-samples"), {
29
+ recursive: true,
30
+ force: true,
31
+ });
32
+ loggerStub = sinon.stub(logger, "info").returns(logger);
33
+ });
34
+ afterAll(() => {
35
+ loggerStub.restore();
36
+ });
28
37
 
29
- it("should load all projects from publisher.config.json within a second on initialization", async () => {
38
+ it("should load all projects from publisher.config.json on initialization", async () => {
30
39
  mock(isPublisherConfigFrozen).mockReturnValue(true);
31
40
  const projectStore = new ProjectStore(serverRoot);
32
- await waitForInitialization(projectStore);
41
+ mock(projectStore.downloadGitHubDirectory).mockResolvedValue(undefined);
42
+ await projectStore.finishedInitialization;
33
43
  expect(await projectStore.listProjects()).toEqual([
34
44
  {
35
45
  name: "malloy-samples",
36
- readme: expect.stringContaining("# Malloy Analysis Examples"),
46
+ readme: expect.any(String),
37
47
  resource: "/api/v0/projects/malloy-samples",
48
+ packages: expect.any(Array),
49
+ connections: expect.any(Array),
38
50
  },
39
51
  ]);
40
52
  });
@@ -42,16 +54,16 @@ describe("ProjectStore", () => {
42
54
  it("should list projects from memory by default", async () => {
43
55
  mock(isPublisherConfigFrozen).mockReturnValue(true);
44
56
  const projectStore = new ProjectStore(serverRoot);
45
- await waitForInitialization(projectStore);
46
-
47
- // Mock fs.readFile & fs.readdir to track calls
48
- const readFileSpy = spyOn(fs, "readFile");
49
- const readdirSpy = spyOn(fs, "readdir");
50
- // Call listProjects, which should use memory and not call fs.readFile
51
- await projectStore.listProjects();
52
-
53
- expect(readFileSpy).not.toHaveBeenCalled();
54
- expect(readdirSpy).not.toHaveBeenCalled();
57
+ const projects = await projectStore.listProjects();
58
+ expect(projects).toEqual([
59
+ {
60
+ name: "malloy-samples",
61
+ readme: expect.any(String),
62
+ resource: "/api/v0/projects/malloy-samples",
63
+ packages: expect.any(Array),
64
+ connections: expect.any(Array),
65
+ },
66
+ ]);
55
67
  });
56
68
 
57
69
  it("should list projects from disk if reload is true", async () => {
@@ -68,22 +80,29 @@ describe("ProjectStore", () => {
68
80
  });
69
81
 
70
82
  it("should allow modifying the in-memory hashmap if config is not frozen", async () => {
71
- mock.module("../utils", () => ({
83
+ mock.module("../config", () => ({
72
84
  isPublisherConfigFrozen: () => false,
73
85
  }));
74
86
  const projectStore = new ProjectStore(serverRoot);
75
- await waitForInitialization(projectStore);
87
+ mock(projectStore.downloadGitHubDirectory).mockResolvedValue(undefined);
88
+ await projectStore.finishedInitialization;
76
89
  await projectStore.updateProject({
77
90
  name: "malloy-samples",
78
91
  readme: "Updated README",
79
92
  });
80
- expect(await projectStore.listProjects()).toEqual([
81
- {
93
+ let projects = await projectStore.listProjects();
94
+ projects = await projectStore.listProjects();
95
+ let malloySamplesProject = projects.find(
96
+ (p) => p.name === "malloy-samples",
97
+ );
98
+ expect(malloySamplesProject).toBeDefined();
99
+ expect(malloySamplesProject).toMatchObject(
100
+ expect.objectContaining({
82
101
  name: "malloy-samples",
83
102
  readme: "Updated README",
84
103
  resource: "/api/v0/projects/malloy-samples",
85
- },
86
- ]);
104
+ }),
105
+ );
87
106
  await projectStore.deleteProject("malloy-samples");
88
107
  expect(await projectStore.listProjects()).toEqual([]);
89
108
  await projectStore.addProject({
@@ -95,31 +114,32 @@ describe("ProjectStore", () => {
95
114
  ).toHaveProperty("metadata", {
96
115
  name: "malloy-samples",
97
116
  resource: "/api/v0/projects/malloy-samples",
117
+ location: expect.any(String),
98
118
  });
99
119
 
100
- // After a while, it'll resolve the async promise where the readme gets loaded in memory
101
- await waitForInitialization(projectStore);
102
- expect(await projectStore.listProjects()).toEqual([
103
- {
104
- name: "malloy-samples",
105
- readme: expect.stringContaining("# Malloy Analysis Examples"),
106
- resource: "/api/v0/projects/malloy-samples",
107
- },
108
- ]);
120
+ projects = await projectStore.listProjects();
121
+ malloySamplesProject = projects.find((p) => p.name === "malloy-samples");
122
+ expect(malloySamplesProject).toBeDefined();
123
+ expect(malloySamplesProject).toMatchObject({
124
+ name: "malloy-samples",
125
+ resource: "/api/v0/projects/malloy-samples",
126
+ });
109
127
  });
110
128
 
111
129
  it("should not allow modifying the in-memory hashmap if config is frozen", async () => {
112
- mock.module("../utils", () => ({
130
+ mock.module("../config", () => ({
113
131
  isPublisherConfigFrozen: () => true,
114
132
  }));
115
133
  const projectStore = new ProjectStore(serverRoot);
116
134
  // Initialization should succeed
117
- await waitForInitialization(projectStore);
135
+ await projectStore.finishedInitialization;
118
136
  expect(await projectStore.listProjects()).toEqual([
119
137
  {
120
138
  name: "malloy-samples",
121
- readme: expect.stringContaining("# Malloy Analysis Examples"),
139
+ readme: expect.any(String),
122
140
  resource: "/api/v0/projects/malloy-samples",
141
+ packages: expect.any(Array),
142
+ connections: expect.any(Array),
123
143
  },
124
144
  ]);
125
145
  // Adding a project should fail
@@ -146,18 +166,20 @@ describe("ProjectStore", () => {
146
166
  expect(await projectStore.listProjects()).toEqual([
147
167
  {
148
168
  name: "malloy-samples",
149
- readme: expect.stringContaining("# Malloy Analysis Examples"),
169
+ readme: expect.any(String),
150
170
  resource: "/api/v0/projects/malloy-samples",
171
+ packages: expect.any(Array),
172
+ connections: expect.any(Array),
151
173
  },
152
174
  ]);
153
175
  });
154
176
 
155
177
  it("should always try to reload a project if it's not in the hashmap", async () => {
156
- mock.module("../utils", () => ({
178
+ mock.module("../config", () => ({
157
179
  isPublisherConfigFrozen: () => false,
158
180
  }));
159
181
  const projectStore = new ProjectStore(serverRoot);
160
- await waitForInitialization(projectStore);
182
+ await projectStore.finishedInitialization;
161
183
  await projectStore.deleteProject("malloy-samples");
162
184
  expect(await projectStore.listProjects()).toEqual([]);
163
185
  const readFileSpy = spyOn(fs, "readFile");
@@ -166,11 +188,11 @@ describe("ProjectStore", () => {
166
188
  });
167
189
 
168
190
  it("should throw a NotFound error when reloading a project that is not in disk", async () => {
169
- mock.module("../utils", () => ({
191
+ mock.module("../config", () => ({
170
192
  isPublisherConfigFrozen: () => false,
171
193
  }));
172
194
  const projectStore = new ProjectStore(serverRoot);
173
- await waitForInitialization(projectStore);
195
+ await projectStore.finishedInitialization;
174
196
  expect(
175
197
  projectStore.getProject("this-one-does-not-exist", true),
176
198
  ).rejects.toThrow(ProjectNotFoundError);
@@ -2,11 +2,12 @@ import { GetObjectCommand, S3 } from "@aws-sdk/client-s3";
2
2
  import { Storage } from "@google-cloud/storage";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
+ import AdmZip from "adm-zip";
5
6
  import simpleGit from "simple-git";
6
7
  import { Writable } from "stream";
7
8
  import { components } from "../api";
8
9
  import { getPublisherConfig, isPublisherConfigFrozen } from "../config";
9
- import { API_PREFIX, PUBLISHER_CONFIG_NAME } from "../constants";
10
+ import { API_PREFIX, PUBLISHER_CONFIG_NAME, publisherPath } from "../constants";
10
11
  import { FrozenConfigError, ProjectNotFoundError } from "../errors";
11
12
  import { logger } from "../logger";
12
13
  import { Project } from "./project";
@@ -56,20 +57,23 @@ export class ProjectStore {
56
57
  );
57
58
  } catch (error) {
58
59
  logger.error("Error initializing project store", { error });
60
+ console.error(error);
59
61
  process.exit(1);
60
62
  }
61
63
  }
62
64
 
63
65
  public async listProjects() {
64
66
  await this.finishedInitialization;
65
- return Array.from(this.projects.values()).map(
66
- (project) => project.metadata,
67
+ return Promise.all(
68
+ Array.from(this.projects.values()).map((project) =>
69
+ project.serialize(),
70
+ ),
67
71
  );
68
72
  }
69
73
 
70
74
  public async getProject(
71
75
  projectName: string,
72
- reload: boolean,
76
+ reload: boolean = false,
73
77
  ): Promise<Project> {
74
78
  await this.finishedInitialization;
75
79
  let project = this.projects.get(projectName);
@@ -99,9 +103,10 @@ export class ProjectStore {
99
103
  if (!skipInitialization) {
100
104
  await this.finishedInitialization;
101
105
  }
102
- if (this.publisherConfigIsFrozen) {
106
+ if (!skipInitialization && this.publisherConfigIsFrozen) {
103
107
  throw new FrozenConfigError();
104
108
  }
109
+
105
110
  const projectName = project.name;
106
111
  if (!projectName) {
107
112
  throw new Error("Project name is required");
@@ -111,15 +116,44 @@ export class ProjectStore {
111
116
  );
112
117
  const projectPath =
113
118
  project.location || projectManifest.projects[projectName];
114
- const absoluteProjectPath = await this.loadProjectIntoDisk(
119
+ let absoluteProjectPath: string;
120
+ if (projectPath) {
121
+ absoluteProjectPath = await this.loadProjectIntoDisk(
122
+ projectName,
123
+ projectPath,
124
+ );
125
+ if (absoluteProjectPath.endsWith(".zip")) {
126
+ absoluteProjectPath = await this.unzipProject(absoluteProjectPath);
127
+ }
128
+ } else {
129
+ absoluteProjectPath = await this.scaffoldProject(project);
130
+ }
131
+ const newProject = await Project.create(
115
132
  projectName,
116
- projectPath,
133
+ absoluteProjectPath,
134
+ project.connections || [],
117
135
  );
118
- const newProject = await Project.create(projectName, absoluteProjectPath);
119
136
  this.projects.set(projectName, newProject);
120
137
  return newProject;
121
138
  }
122
139
 
140
+ public async unzipProject(absoluteProjectPath: string) {
141
+ logger.info(
142
+ `Detected zip file at "${absoluteProjectPath}". Unzipping...`,
143
+ );
144
+ const unzippedProjectPath = absoluteProjectPath.replace(".zip", "");
145
+ await fs.promises.rm(unzippedProjectPath, {
146
+ recursive: true,
147
+ force: true,
148
+ });
149
+ await fs.promises.mkdir(unzippedProjectPath, { recursive: true });
150
+
151
+ const zip = new AdmZip(absoluteProjectPath);
152
+ zip.extractAllTo(unzippedProjectPath, true);
153
+
154
+ return unzippedProjectPath;
155
+ }
156
+
123
157
  public async updateProject(project: ApiProject) {
124
158
  await this.finishedInitialization;
125
159
  if (this.publisherConfigIsFrozen) {
@@ -184,8 +218,24 @@ export class ProjectStore {
184
218
  }
185
219
  }
186
220
 
221
+ private async scaffoldProject(project: ApiProject) {
222
+ const projectName = project.name;
223
+ if (!projectName) {
224
+ throw new Error("Project name is required");
225
+ }
226
+ const absoluteProjectPath = `${publisherPath}/${projectName}`;
227
+ await fs.promises.mkdir(absoluteProjectPath, { recursive: true });
228
+ if (project.readme) {
229
+ await fs.promises.writeFile(
230
+ path.join(absoluteProjectPath, "README.md"),
231
+ project.readme,
232
+ );
233
+ }
234
+ return absoluteProjectPath;
235
+ }
236
+
187
237
  private async loadProjectIntoDisk(projectName: string, projectPath: string) {
188
- const absoluteTargetPath = `/etc/publisher/${projectName}`;
238
+ const absoluteTargetPath = `${publisherPath}/${projectName}`;
189
239
  // Handle absolute paths
190
240
  if (projectPath.startsWith("/")) {
191
241
  try {
@@ -263,7 +313,7 @@ export class ProjectStore {
263
313
  throw new ProjectNotFoundError(errorMsg);
264
314
  }
265
315
 
266
- private async mountLocalDirectory(
316
+ public async mountLocalDirectory(
267
317
  projectPath: string,
268
318
  absoluteTargetPath: string,
269
319
  projectName: string,
@@ -384,6 +434,21 @@ export class ProjectStore {
384
434
  async downloadGitHubDirectory(githubUrl: string, absoluteDirPath: string) {
385
435
  await fs.promises.rm(absoluteDirPath, { recursive: true, force: true });
386
436
  await fs.promises.mkdir(absoluteDirPath, { recursive: true });
387
- await simpleGit().clone(githubUrl, absoluteDirPath);
437
+
438
+ await new Promise<void>((resolve, reject) => {
439
+ simpleGit().clone(githubUrl, absoluteDirPath, {}, (err) => {
440
+ if (err) {
441
+ console.error(err);
442
+ logger.error(
443
+ `Failed to clone GitHub repository "${githubUrl}"`,
444
+ {
445
+ error: err,
446
+ },
447
+ );
448
+ reject(err);
449
+ }
450
+ resolve();
451
+ });
452
+ });
388
453
  }
389
454
  }